Cowork A: SSE v2 tool events, artifacts table + API #60

Merged
ryan merged 5 commits from feat/cowork-stream into main 2026-10-12 18:06:03 -04:00
Owner

Task A from docs/cowork-ui-plan.md.

What

  • SSE v2 tool events in stream-query: tool_start {run_id, tool, label, input} and tool_end {run_id, tool, label, status, ms, artifact}. on_tool_error maps to status:"error". Payload shaping lives in pure functions in blueprints/conversation/tool_events.py. Additive, so old clients keep working.
  • Label registry blueprints/conversation/tool_labels.py: every tool (both old and #55 YNAB names), prefix matches for homebox_/bookstack_, and the fallback Using <tool>….
  • Artifacts: new blueprints/artifacts/ (model, logic, routes).
    • Created in on_tool_end when a tool returns a content_and_artifact dict with a kind, and linked to the saved tool message.
    • 256 KB cap on data.
    • GET /api/artifacts?conversation_id= and GET /api/artifacts/<id>, owner-scoped. Malformed ids are validated before querying (400/404, not a 500 from asyncpg).
  • Conversation payload: tool messages gain tool, label, artifact; text is still the header line.
  • Migration 17_…_add_artifacts.py (aerich-generated, creates a table only, blue/green-safe).

Notes

  • tool_end is now sent after the tool message and artifact are saved, so it can carry the artifact ref.
  • ⚠️ #55's migration also needs renumbering. Whichever of #55 and this PR merges second must regenerate its migration.
  • Nothing produces artifacts until worker B's tool changes land; until then artifact is always null.

Test plan

  • pytest tests/ -q: 153 passed (new test_tool_events.py, test_artifacts.py incl. a full fake-agent stream with parallel tools, the error path, the cap and owner scoping)
  • Coordinator review, 2 passes
  • After deploy: make logs shows the artifacts migration applied; a tool-using question streams with labels

🤖 Generated with Claude Code

Task A from `docs/cowork-ui-plan.md`. ## What - **SSE v2 tool events** in `stream-query`: `tool_start {run_id, tool, label, input}` and `tool_end {run_id, tool, label, status, ms, artifact}`. `on_tool_error` maps to `status:"error"`. Payload shaping lives in pure functions in `blueprints/conversation/tool_events.py`. Additive, so old clients keep working. - **Label registry** `blueprints/conversation/tool_labels.py`: every tool (both old and #55 YNAB names), prefix matches for `homebox_`/`bookstack_`, and the fallback `Using <tool>…`. - **Artifacts**: new `blueprints/artifacts/` (model, logic, routes). - Created in `on_tool_end` when a tool returns a `content_and_artifact` dict with a `kind`, and linked to the saved tool message. - 256 KB cap on `data`. - `GET /api/artifacts?conversation_id=` and `GET /api/artifacts/<id>`, owner-scoped. Malformed ids are validated before querying (400/404, not a 500 from asyncpg). - **Conversation payload**: tool messages gain `tool`, `label`, `artifact`; `text` is still the header line. - **Migration** `17_…_add_artifacts.py` (aerich-generated, creates a table only, blue/green-safe). ## Notes - `tool_end` is now sent *after* the tool message and artifact are saved, so it can carry the artifact ref. - ⚠️ #55's migration also needs renumbering. Whichever of #55 and this PR merges second must regenerate its migration. - Nothing produces artifacts until worker B's tool changes land; until then `artifact` is always `null`. ## Test plan - [x] `pytest tests/ -q`: 153 passed (new `test_tool_events.py`, `test_artifacts.py` incl. a full fake-agent stream with parallel tools, the error path, the cap and owner scoping) - [x] Coordinator review, 2 passes - [ ] After deploy: `make logs` shows the artifacts migration applied; a tool-using question streams with labels 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ryan added 5 commits 2026-10-12 17:57:43 -04:00
Pure functions build tool_start/tool_end payloads with run_id, friendly
label, truncated input (config dropped), status, duration and artifact ref.
Labels cover every agent tool, both old and #55 YNAB names, and the
homebox_/bookstack_ MCP prefixes, falling back to "Using <tool>…".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /api/artifacts?conversation_id= lists ArtifactRefs without data;
GET /api/artifacts/<id> returns the full artifact. Both 404 for other
users. create_artifact_from_output persists ToolArtifact dicts and drops
ones over 256 KB serialised.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stream_query emits the new tool_start/tool_end payloads, maps
on_tool_error to tool_end status=error, and saves an artifact linked to
the tool message. GET /api/conversation/<id> adds tool, label and
artifact to tool messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
asyncpg raises OperationalError (not ValueError) on malformed UUIDs, so
bad artifact ids now 404 and a bad conversation_id 400s before any query.
get_conversation keys artifacts by str(message_id) so matching doesn't
depend on the driver's UUID types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated with aerich against a fresh Postgres after applying 1-16.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ryan merged commit 3dbd320fe9 into main 2026-10-12 18:06:03 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: ryan/simbarag#60