feat(chat-workflow): wire POST /api/chat/workflow to durable Vercel Workflow (PR 3 of 4)

Merged
SW
6mo ago
+1478-94

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)

  1. ✅ docs contract (recoupable/docs#221)
  2. ✅ route stub (recoupable/api#579)
  3. ✅ this PR — workflow wire-up (no tools)
  4. ⏳ port 12 sandbox tool files + buildAgentTools + wire into runAgentStep

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 predicate
  • chats/touchChat.ts — bump updated_at
  • chats/updateChat.ts — generic partial update mirroring updateSession
  • chat_messages/createChatMessageIfNotExists.ts — upsert with ignoreDuplicates
  • chat_messages/isFirstChatMessage.ts — title-from-first-message check

Chat/recoupable helpers (lib/)

  • recoupable/extractOrgId.ts — org-<slug>-<uuid> → uuid
  • chat/assistantFileLinks.ts — workspace-file deep-link prompt
  • chat/recoupApiSkillPrompt.ts — recoup-api + artist-workspace nudge
  • chat/agentCustomInstructions.ts — composed system-prompt suffix
  • chat/persistLatestUserMessage.ts — fire-and-forget user msg + auto-title
  • chat/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", single streamText turn 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)
  • buildAgentTools factory
  • Wiring tools into runAgentStep via experimental_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 with x-workflow-run-id.
    • Resumes in-flight runs through maybeResumeChatStream; returns 409 on conflicts. Treats transient workflow/api errors as conflicts without clearing the slot.
    • Pre-claims chats.active_stream_id with pending-<uuid> before start(...), 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_id when set, else defaults to anthropic/claude-haiku-4.5.
  • Refactors

    • Consolidated active-stream CAS into compareAndSetChatActiveStreamId; lib/supabase/chats/updateChat is a generic helper with where predicates and discriminated results. Fixed Next.js build typing by switching to in-operator narrowing in CAS and rewriting the updateChat predicate builder to avoid Supabase type-depth issues (behavior unchanged).
    • Switched reconcileExistingActiveStream to a top‑level workflow/api import; tightened cancel/error-path tests.

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

Review Change Stack