feat(api): streaming agent turns — LlmClient.stream() + SSE chat endpoint #341
No reviewers
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol!341
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "340-agent-streaming"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Token-by-token streaming for the built-in agent (ADR-0029 §3): a
stream()method on the LlmClient, a unified streaming agent-loop core, and content-negotiated SSE on the two conversation POST routes. No DB schema change (reusesconversations/messages). Part of epic #47.What's in it
LlmClient.stream(req)(required interface method) yieldingtext_delta/tool_call/doneevents;generate()stays for non-loop callers. Implemented on both adapters:messages.stream(...): forwards text deltas live, accumulatestool_useblocks, emitsdonefromnormalizeResponse(stream.finalMessage())(reused sodone.result≡generate()).fetchwithstream: true, hand-parsed SSE chunks (buffered line split, indexedtool_callsargument accumulation,finish_reason); a sharedbuildResult(...)keeps streaming and non-streaming in lockstep.driveLoopEvents).runTurn/resumeTurnkeep their exact signatures and drain it (JSON behaviour byte-for-byte unchanged — the #339 loop tests stayed green untouched);streamTurn/streamResumeforward itsConversationEvents. Same persistence order,MAX_TOOL_ROUNDSguard, and propose-then-confirm write-pause as before.Accept: text/event-stream. Setup failures (404/401/409/llm_not_configured) stay real HTTP status codes via a pre-pull of the first event before the 200 is sent; once headers are out, a mid-stream provider failure surfaces as a terminalerrorevent. JSON mode is unchanged.Adapter event shape:
text_delta|tool_call|done. Conversation/SSE event shape (lib/agent/events.ts):text_delta,tool_call,tool_result,message(carriestoMessageDto, never the raw entity),awaiting_confirmation,done,error. SSE wire:event: <type>\ndata: <json>\n\n.Verification (all run locally on this branch)
typecheck✓ ·lint✓ (0 warnings)testagainst both engines (ephemeral Postgres viaTEST_POSTGRES_URL): 1206 passed, 0 skipped — Postgres leg ran, no engine-specific SQL; +23 tests (adapter streaming, streaming loop event sequences + persisted-row parity, SSE route incl. JSON-mode-unchanged and 401/404-as-JSON-not-SSE)openapi:check✓ up to date ·openapi:coverage116 pairs (SSE isn't a JSON DTO; no new schema)Out of scope
Closes #340
🤖 Generated with Claude Code
📊 Test coverage
Patch coverage: no testable lines changed.
Overall (
app/,lib/,db/, excluding UI per ADR-0019):Soft thresholds per ADR-0019. Coverage is informational and does not block merge.