feat(agents): Update task comment requirements and add in_progress_at tracking
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
"""add task in_progress_at
|
||||
|
||||
Revision ID: c1a2b3c4d5e7
|
||||
Revises: b9d22e2a4d50
|
||||
Create Date: 2026-02-04 13:34:25.000000
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "c1a2b3c4d5e7"
|
||||
down_revision = "b9d22e2a4d50"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"ALTER TABLE tasks ADD COLUMN IF NOT EXISTS in_progress_at TIMESTAMP WITHOUT TIME ZONE"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE INDEX IF NOT EXISTS ix_tasks_in_progress_at ON tasks (in_progress_at)"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP INDEX IF EXISTS ix_tasks_in_progress_at")
|
||||
op.execute("ALTER TABLE tasks DROP COLUMN IF EXISTS in_progress_at")
|
||||
@@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import asc
|
||||
from sqlalchemy import asc, desc
|
||||
from sqlmodel import Session, col, select
|
||||
|
||||
from app.api.deps import (
|
||||
@@ -30,6 +31,42 @@ from app.services.activity_log import record_activity
|
||||
|
||||
router = APIRouter(prefix="/boards/{board_id}/tasks", tags=["tasks"])
|
||||
|
||||
REQUIRED_COMMENT_FIELDS = ("summary:", "details:", "next:")
|
||||
|
||||
|
||||
def is_valid_markdown_comment(message: str) -> bool:
|
||||
content = message.strip()
|
||||
if not content:
|
||||
return False
|
||||
lowered = content.lower()
|
||||
if not all(field in lowered for field in REQUIRED_COMMENT_FIELDS):
|
||||
return False
|
||||
if "- " not in content and "* " not in content:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def has_valid_recent_comment(
|
||||
session: Session,
|
||||
task: Task,
|
||||
agent_id: UUID | None,
|
||||
since: datetime | None,
|
||||
) -> bool:
|
||||
if agent_id is None or since is None:
|
||||
return False
|
||||
statement = (
|
||||
select(ActivityEvent)
|
||||
.where(col(ActivityEvent.task_id) == task.id)
|
||||
.where(col(ActivityEvent.event_type) == "task.comment")
|
||||
.where(col(ActivityEvent.agent_id) == agent_id)
|
||||
.where(col(ActivityEvent.created_at) >= since)
|
||||
.order_by(desc(col(ActivityEvent.created_at)))
|
||||
)
|
||||
event = session.exec(statement).first()
|
||||
if event is None or event.message is None:
|
||||
return False
|
||||
return is_valid_markdown_comment(event.message)
|
||||
|
||||
|
||||
@router.get("", response_model=list[TaskRead])
|
||||
def list_tasks(
|
||||
@@ -74,20 +111,28 @@ def update_task(
|
||||
) -> Task:
|
||||
previous_status = task.status
|
||||
updates = payload.model_dump(exclude_unset=True)
|
||||
comment = updates.pop("comment", None)
|
||||
if actor.actor_type == "agent":
|
||||
if actor.agent and actor.agent.board_id and task.board_id:
|
||||
if actor.agent.board_id != task.board_id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
allowed_fields = {"status"}
|
||||
allowed_fields = {"status", "comment"}
|
||||
if not set(updates).issubset(allowed_fields):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
if "status" in updates:
|
||||
if updates["status"] == "inbox":
|
||||
task.assigned_agent_id = None
|
||||
task.in_progress_at = None
|
||||
else:
|
||||
task.assigned_agent_id = actor.agent.id if actor.agent else None
|
||||
elif "status" in updates and updates["status"] == "inbox":
|
||||
task.assigned_agent_id = None
|
||||
if updates["status"] == "in_progress":
|
||||
task.in_progress_at = datetime.utcnow()
|
||||
elif "status" in updates:
|
||||
if updates["status"] == "inbox":
|
||||
task.assigned_agent_id = None
|
||||
task.in_progress_at = None
|
||||
elif updates["status"] == "in_progress":
|
||||
task.in_progress_at = datetime.utcnow()
|
||||
if "assigned_agent_id" in updates and updates["assigned_agent_id"]:
|
||||
agent = session.get(Agent, updates["assigned_agent_id"])
|
||||
if agent is None:
|
||||
@@ -98,10 +143,35 @@ def update_task(
|
||||
setattr(task, key, value)
|
||||
task.updated_at = datetime.utcnow()
|
||||
|
||||
if "status" in updates and updates["status"] == "review":
|
||||
if comment is not None and comment.strip():
|
||||
if not is_valid_markdown_comment(comment):
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
|
||||
else:
|
||||
if not has_valid_recent_comment(
|
||||
session,
|
||||
task,
|
||||
task.assigned_agent_id,
|
||||
task.in_progress_at,
|
||||
):
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
|
||||
|
||||
session.add(task)
|
||||
session.commit()
|
||||
session.refresh(task)
|
||||
|
||||
if comment is not None and comment.strip():
|
||||
if actor.actor_type == "agent" and not is_valid_markdown_comment(comment):
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
|
||||
event = ActivityEvent(
|
||||
event_type="task.comment",
|
||||
message=comment.strip(),
|
||||
task_id=task.id,
|
||||
agent_id=actor.agent.id if actor.actor_type == "agent" and actor.agent else None,
|
||||
)
|
||||
session.add(event)
|
||||
session.commit()
|
||||
|
||||
if "status" in updates and task.status != previous_status:
|
||||
event_type = "task.status_changed"
|
||||
message = f"Task moved to {task.status}: {task.title}."
|
||||
@@ -160,6 +230,8 @@ def create_task_comment(
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
if not payload.message.strip():
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
|
||||
if actor.actor_type == "agent" and not is_valid_markdown_comment(payload.message):
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
|
||||
event = ActivityEvent(
|
||||
event_type="task.comment",
|
||||
message=payload.message.strip(),
|
||||
|
||||
@@ -19,6 +19,7 @@ class Task(TenantScoped, table=True):
|
||||
status: str = Field(default="inbox", index=True)
|
||||
priority: str = Field(default="medium", index=True)
|
||||
due_at: datetime | None = None
|
||||
in_progress_at: datetime | None = None
|
||||
|
||||
created_by_user_id: UUID | None = Field(default=None, foreign_key="users.id", index=True)
|
||||
assigned_agent_id: UUID | None = Field(default=None, foreign_key="agents.id", index=True)
|
||||
|
||||
@@ -26,12 +26,14 @@ class TaskUpdate(SQLModel):
|
||||
priority: str | None = None
|
||||
due_at: datetime | None = None
|
||||
assigned_agent_id: UUID | None = None
|
||||
comment: str | None = None
|
||||
|
||||
|
||||
class TaskRead(TaskBase):
|
||||
id: UUID
|
||||
board_id: UUID | None
|
||||
created_by_user_id: UUID | None
|
||||
in_progress_at: datetime | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
@@ -30,5 +30,12 @@ Write things down. Do not rely on short-term context.
|
||||
- HEARTBEAT.md defines what to do on each heartbeat.
|
||||
|
||||
## Task updates
|
||||
- Log all task progress and results via the task comments endpoint.
|
||||
- Do not post task updates in chat/web channels.
|
||||
- All task updates MUST be posted to the task comments endpoint.
|
||||
- Do not post task updates in chat/web channels under any circumstance.
|
||||
- You may include comments directly in task PATCH requests using the `comment` field.
|
||||
- Required comment fields (markdown):
|
||||
- `status`: inbox | in_progress | review | done
|
||||
- `summary`: one line
|
||||
- `details`: 1–3 bullets
|
||||
- `next`: next step or handoff request
|
||||
- Every status change must include a comment within 30 seconds (see HEARTBEAT.md).
|
||||
|
||||
@@ -4,6 +4,7 @@ On startup:
|
||||
1) Verify API reachability (GET {{ base_url }}/api/v1/gateway/status).
|
||||
- A 401 Unauthorized response is acceptable here for agents (auth-protected endpoint).
|
||||
2) Connect to Mission Control once by sending a heartbeat check-in.
|
||||
2a) Use task comments for updates; do not send task updates to chat/web.
|
||||
2a) Use task comments for all updates; do not send task updates to chat/web.
|
||||
2b) Follow the required comment format in AGENTS.md / HEARTBEAT.md.
|
||||
3) If you send a boot message, end with NO_REPLY.
|
||||
4) If BOOTSTRAP.md exists in this workspace, the agent should run it once and delete it.
|
||||
|
||||
@@ -21,6 +21,15 @@ curl -s -X POST "$BASE_URL/api/v1/agents/heartbeat" \
|
||||
-d '{"name": "'$AGENT_NAME'", "board_id": "'$BOARD_ID'", "status": "online"}'
|
||||
```
|
||||
|
||||
## Commenting rules (mandatory)
|
||||
- Every task state change MUST be followed by a task comment within 30 seconds.
|
||||
- Never post task updates to chat/web channels. Task comments are the only update channel.
|
||||
- Minimum comment format:
|
||||
- `status`: inbox | in_progress | review | done
|
||||
- `summary`: one-line progress update
|
||||
- `details`: 1–3 bullets of what changed / what you did
|
||||
- `next`: next step or handoff request
|
||||
|
||||
2) List boards:
|
||||
```bash
|
||||
curl -s "$BASE_URL/api/v1/boards" \
|
||||
@@ -40,25 +49,29 @@ curl -s "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks" \
|
||||
curl -s -X PATCH "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks/{TASK_ID}" \
|
||||
-H "X-Agent-Token: $AUTH_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"status": "in_progress"}'
|
||||
-d '{"status": "in_progress", "comment": "[status=in_progress] Claimed by '$AGENT_NAME'.\\nsummary: Starting work.\\ndetails: - Triage task and plan approach.\\nnext: Begin execution."}'
|
||||
```
|
||||
|
||||
5) Work the task:
|
||||
- Update status as you progress.
|
||||
- Post a brief work log to the task comments endpoint (do not use chat).
|
||||
- When complete, move to "review":
|
||||
- When complete, use the following mandatory steps:
|
||||
|
||||
5a) Post the completion comment (required, markdown). Include:
|
||||
- status, summary, details (bullets), next, and the full response text.
|
||||
Use the task comments endpoint for this step.
|
||||
|
||||
5b) Move the task to "review":
|
||||
```bash
|
||||
curl -s -X PATCH "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks/{TASK_ID}" \
|
||||
-H "X-Agent-Token: $AUTH_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"status": "review"}'
|
||||
```
|
||||
```bash
|
||||
curl -s -X POST "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks/{TASK_ID}/comments" \
|
||||
-H "X-Agent-Token: $AUTH_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"message": "Summary of work, result, and next steps."}'
|
||||
```
|
||||
|
||||
## Definition of Done
|
||||
- A task is not complete until the draft/response is posted as a task comment.
|
||||
- Comments must be markdown and include: summary, details (bullets), next.
|
||||
|
||||
## Status flow
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user