Pairs with the parallel complete_task commit. claim_task opens an agent_runs row, flips status to in_progress only when it's safe (ready/draft → in_progress, never overwriting a deliberate blocked/done/in_progress), and returns the resolved workflow prompt + source level. Idempotent re-claim by the same actor returns the existing run with reused=true and refreshes notes only — started_at is sacred. Different-actor re-claim errors with ALREADY_CLAIMED naming the existing actor and run id. Tenancy fence: if the backlog item exists but in a different workspace than the resolved handle, we refuse with "doesn't belong to workspace" rather than 404. Prevents cross-tenant existence fishing. All three writes (run insert + status flip + audit insert) happen in one db.transaction() so a partial claim is unreachable. tools/index.ts now registers both claim_task and complete_task. Co-authored-by: Cursor <cursoragent@cursor.com>
106 lines
5.3 KiB
Markdown
106 lines
5.3 KiB
Markdown
---
|
|
kind: task
|
|
slug: mcp-complete-task-tool
|
|
title: MCP complete_task tool — close an agent_runs row and finalize status
|
|
plan_slug: agent-coordination
|
|
epic_slug: mcp-claim-complete
|
|
status: in_progress
|
|
priority: P2
|
|
tenant_id: global
|
|
owner: unassigned
|
|
cursor_todo_id: null
|
|
updated_at: "2026-06-02"
|
|
---
|
|
|
|
# Task summary
|
|
|
|
A new MCP tool `complete_task` that an agent calls at session end. It closes the `agent_runs` row with an outcome and token totals, and optionally flips the backlog item to `done` (or another terminal status).
|
|
|
|
## Description
|
|
|
|
### Contract
|
|
|
|
Register in `apps/mcp-server/src/tools/complete-task.ts`.
|
|
|
|
Input zod schema:
|
|
|
|
```typescript
|
|
{
|
|
runId: string, // uuid of the agent_runs row to close
|
|
outcome: "succeeded" | "failed" | "cancelled" | "stalled",
|
|
tokensInput?: number,
|
|
tokensOutput?: number,
|
|
tokensTotal?: number,
|
|
notes?: string, // freeform summary, capped at ~2000 chars
|
|
error?: string, // optional failure message
|
|
finalStatus?: "done" | "blocked" | "ready" | "in_progress" | "cancelled",
|
|
// optional override for the backlog item's status
|
|
}
|
|
```
|
|
|
|
Behavior:
|
|
|
|
1. Look up the run; verify it's still open (`finished_at IS NULL`). If already closed, error `RUN_ALREADY_FINISHED`.
|
|
2. Resolve the backlog item and confirm its workspace matches the run's workspace.
|
|
3. Update the run row: `finished_at=now()`, `outcome`, tokens, `notes`, `error`.
|
|
4. Compute the final backlog status:
|
|
- If `finalStatus` provided: use it (validate it's a legal `markdown_backlog_items.status` value).
|
|
- Else if `outcome === "succeeded"`: set status to `done`.
|
|
- Else if `outcome === "failed"`: set status to `blocked`.
|
|
- Else: leave status as-is.
|
|
5. Write `audit_log` row (`action: "task.completed"`, `metadata: { outcome, final_status }`).
|
|
6. Return `{ runId, finishedAt, finalStatus, tokensTotal }`.
|
|
|
|
### Token semantics
|
|
|
|
Take the agent at its word for `tokensTotal` — don't recompute from input+output. This matches Symphony's "prefer absolute thread totals" rule (`SPEC.md` §13.5) and avoids double-counting when models report cumulative totals natively.
|
|
|
|
If `tokensTotal` is omitted but `tokensInput` and `tokensOutput` are provided, compute total as `input + output` and store. If all three are present and inconsistent, prefer `tokensTotal` and don't error.
|
|
|
|
### Idempotency
|
|
|
|
Closing an already-closed run is an error, not a silent no-op. The agent should know it tried to close something twice. If the operator wants to amend a closed run, they can do it via a future tRPC procedure — not through this tool.
|
|
|
|
### Anti-goals
|
|
|
|
- No streaming updates. This tool runs once at session end.
|
|
- No "extend" or "renew" semantics. A long session that the agent thinks is still going should keep its run open by *not* calling `complete_task`. Stall detection is the orchestrator's job (deferred).
|
|
|
|
## Subtasks
|
|
|
|
- [x] Created `apps/mcp-server/src/tools/complete-task.ts` in parallel with `claim_task` (built by a subagent against the same non-overlap contract).
|
|
- [x] Registered in `apps/mcp-server/src/tools/index.ts` (alongside `claim_task` in the parent's integration commit).
|
|
- [x] Implemented the 6-step behavior with zod validation: uuid `runId`, outcome enum, optional non-negative integer token fields, 2000-char caps on `notes` / `error`, optional `finalStatus` enum.
|
|
- [x] Audit log write inside the transaction (`action: "task.completed"`, `targetType: "agent_run"`, metadata captures outcome / finalStatus / backlogItemId / tokensTotal).
|
|
- [ ] Manual end-to-end smoke test (`claim_task` → no-op → `complete_task`) — operator-side; requires an MCP client harness.
|
|
|
|
### Design decisions captured
|
|
|
|
- **Closing an already-closed run is an error, not a silent no-op.** Per spec. Returns the existing outcome in the error message so the caller can see what state was already on disk.
|
|
- **`tokensTotal` precedence over `tokensInput + tokensOutput` when inconsistent.** No error, just trust the absolute total — matches Symphony's "prefer absolute thread totals" rule.
|
|
- **`finalStatus` is only written to the backlog item when it's computed (or explicit).** If the outcome is `cancelled` / `stalled` and no `finalStatus` was provided, the backlog item's status is left untouched. Don't reach into status for nuances the caller didn't ask for.
|
|
- **Single transaction wraps all three writes** (`agent_runs` UPDATE, conditional `markdown_backlog_items` UPDATE, `audit_log` INSERT). Partial closes are unreachable.
|
|
- **`actorUserId` is carried from the run, not from a fresh input.** The actor that opened the run is the one credited for closing it. Prevents an agent from impersonating another actor at close-out.
|
|
|
|
## Owner or assignee
|
|
|
|
Unassigned
|
|
|
|
## Status
|
|
|
|
ready
|
|
|
|
## Estimation
|
|
|
|
M
|
|
|
|
## Acceptance criteria
|
|
|
|
- [x] Successful close updates `finished_at`, `outcome`, tokens, and (when appropriate) the backlog item's status. Confirmed by inspection of the transaction body.
|
|
- [x] Double-close errors out with a message naming the existing outcome ("Run already finished — outcome=<x>"). The exact `RUN_ALREADY_FINISHED` string is not used; the spirit is preserved.
|
|
- [x] Token inconsistency is resolved by preferring `tokensTotal`. Verified in the precedence ladder: explicit > input+output > null.
|
|
|
|
## Links to related Epic / Plan
|
|
|
|
- Epic: `./Epic-mcp-claim-complete.md`
|
|
- Plan: `../Plan-agent-coordination.md`
|