feat(chat-workflow): wire POST /api/chat/workflow to durable Vercel Workflow (PR 3 of 4)
Summary
Replaces the stub UIMessage stream from #579 with a real Vercel Workflow agent loop. Stub run-ids (stub-<uuid>) are now real ones (wrun_<id>) emitted by WDK. Tools are intentionally NOT wired — the workflow runs streamText with the gateway model + Recoup custom instructions only. Sandbox tool surface lands in PR 4.
Re-scoped plan: 4 PRs total (per agreement after a scope reality check)
- ✅ docs contract (recoupable/docs#221)
- ✅ route stub (recoupable/api#579)
- ✅ this PR — workflow wire-up (no tools)
- ⏳ port 12 sandbox tool files +
buildAgentTools+ wire intorunAgentStep
What lands end-to-end
validateChatWorkflow → session+chat ownership → isSandboxActive
→ reconcileExistingActiveStream (resume / 409 / fall-through)
→ refresh session lifecycle activity
→ fire-and-forget persist user message
→ start(runAgentWorkflow, [...])
→ CAS chats.active_stream_id (cancel + 409 on race)
→ return run.getReadable() + x-workflow-run-id header
New code
Supabase helpers (lib/supabase/)
chats/compareAndSetChatActiveStreamId.ts— atomic CAS via predicatechats/touchChat.ts— bump updated_atchats/updateChat.ts— generic partial update mirroring updateSessionchat_messages/createChatMessageIfNotExists.ts— upsert with ignoreDuplicateschat_messages/isFirstChatMessage.ts— title-from-first-message check
Chat/recoupable helpers (lib/)
recoupable/extractOrgId.ts—org-<slug>-<uuid>→ uuidchat/assistantFileLinks.ts— workspace-file deep-link promptchat/recoupApiSkillPrompt.ts—recoup-api+artist-workspacenudgechat/agentCustomInstructions.ts— composed system-prompt suffixchat/persistLatestUserMessage.ts— fire-and-forget user msg + auto-titlechat/reconcileExistingActiveStream.ts— 3-attempt resume/clear/conflict loop
Workflow (app/workflows/)
runAgentWorkflow.ts—"use workflow"agent loop (no-tool: 1 iteration)runAgentStep.ts—"use step", singlestreamTextturn via@ai-sdk/gateway
Wired — lib/chat/handleChatWorkflowStream.ts replaces hardcoded stream with start(runAgentWorkflow, ...).
Test results
- 46 new unit tests (8 + 5 + 2 + 3 + 5 + 3 + 7 + 6 + 18 across the new modules)
- Full suite: 2946/2946 pass (was 2900 before this PR)
- Lint: clean
E2E verification on preview deployment
https://api-git-feat-api-chat-workflow-wire-up-recoup.vercel.app
Setup: agent signup → session → sandbox provisioned (11s) → POST /api/chat/workflow with prompt "Reply with exactly the word ZAP, nothing else."
Response (truncated to first 50 lines):
HTTP/2 200
content-type: text/event-stream
x-vercel-ai-ui-message-stream: v1
x-workflow-run-id: wrun_01KS5H9SA4696XMFV24H9Y5HD1 ← REAL Vercel Workflow run-id
data: {"type":"start"}
data: {"type":"start-step"}
data: {"type":"text-start","id":"0","providerMetadata":{"gateway":{"generationId":"gen_01KS5H9WBMRQE4984FH98DBP1C"}}}
data: {"type":"text-delta","id":"0","delta":"Z"} ← REAL LLM-driven streaming
data: {"type":"text-delta","id":"0","delta":"AP"}
data: {"type":"text-end","id":"0"}
data: {"type":"finish-step"}
data: {"type":"finish","finishReason":"stop"}
Matches the prod sandbox.recoupable.com /api/chat response shape (captured via Chrome DevTools MCP in #579's comments) — same start → start-step → text-* → finish-step → finish framing, same providerMetadata.gateway.generationId, same x-vercel-ai-ui-message-stream: v1 header. Only difference: prod also emits a message-metadata chunk with model + usage + cost — minor gap, easy follow-up if needed (just messageMetadata callback on toUIMessageStream).
Out of scope (PR 4)
- 12 sandbox tool ports from
open-agents/packages/agent/tools/(bash,read,write,grep,glob,todo,task,ask_user_question,skill,fetch, plus utils + recoup helpers) buildAgentToolsfactory- Wiring tools into
runAgentStepviaexperimental_context createChatRuntime(sandbox handle + skill discovery for tool exec)persistAssistantMessagesWithToolResults(client-side tool results persistence)
Test plan
- Unit tests pass (2946/2946)
- Lint clean
- E2E preview: real
wrun_run-id, real LLM-driven stream, full SSE protocol verified - Optional: capture a duplicate-request race against preview to verify 409 / resume paths
🤖 Generated with Claude Code
Summary by cubic
Wires POST /api/chat/workflow to a durable Vercel Workflow with real SSE and wrun_ run IDs. Adds safe pre-claim CAS, resume-or-409 handling, and single‑turn streaming; tools are still out.
-
New Features
- Streams model output via
runAgentWorkflow→runAgentStep(one turn) using@ai-sdk/gateway+agentCustomInstructions; SSE withx-workflow-run-id. - Resumes in-flight runs through
maybeResumeChatStream; returns 409 on conflicts. Treats transientworkflow/apierrors as conflicts without clearing the slot. - Pre-claims
chats.active_stream_idwithpending-<uuid>beforestart(...), then promotes to the real run id; cancels our run and returns 409 if promotion loses. - Refreshes session activity; fire-and-forget persists the latest user message and auto-sets the chat title (exact 80‑char cap).
- Uses the chat’s
model_idwhen set, else defaults toanthropic/claude-haiku-4.5.
- Streams model output via
-
Refactors
- Consolidated active-stream CAS into
compareAndSetChatActiveStreamId;lib/supabase/chats/updateChatis a generic helper withwherepredicates and discriminated results. Fixed Next.js build typing by switching to in-operator narrowing in CAS and rewriting theupdateChatpredicate builder to avoid Supabase type-depth issues (behavior unchanged). - Switched
reconcileExistingActiveStreamto a top‑levelworkflow/apiimport; tightened cancel/error-path tests.
- Consolidated active-stream CAS into
Summary by CodeRabbit
- New Features
- Chat agents can now process language model steps and stream responses with improved reliability
- Agents can create and reference deep links to workspace files within conversations
- Enhanced concurrent chat request management with automatic stream resumption and built-in conflict detection
- Improved agent skill selection and API routing capabilities