Skip to content

Commit 63a9d52

Browse files
earayuclaude
andauthored
feat(phase8 #76 D8.4a): FE AI SDK-compatible stream transport + part reducer (#1700)
* feat(phase8 #76 D8.4a): FE AI SDK-compatible stream transport + part reducer D8.4a first-cut. Replaces the legacy AgentRuntimeRedisStore SSE consumer with a fetch+ReadableStream transport that speaks the AI SDK v5 UI Message Stream Protocol. Hooks the new client into `chat-messages.tsx` through a narrow `legacy-snapshot-shim` so `AgentTurnCard` keeps rendering until the parts renderer (#77) ships. Module layout (`web/src/features/agent-runtime/`): * `types.ts` — wire `StreamPart` typed union (mirrors `aperag/domains/agent_runtime/wire/parts.py`) + at-rest `AgentMessagePart` (text / tool / source / citation / consent / elicitation) shaped to align with `@ai-sdk/react`'s `UIMessagePart`. * `stream-parser.ts` — SSE frame parser (handles `id:` + `data:` only, ignores comments/heartbeats; carries trailing partial frames). * `stream-client.ts` — single-connection consumer; validates `x-vercel-ai-ui-message-stream: v1` response header, forwards `Last-Event-ID` on resume, terminates on `finish` / `error` / `abort` and on local `AbortSignal`. * `reducer.ts` — collapses lifecycle wire parts (`tool-input-*` / `tool-output-available`) into consolidated tool parts; dedups by stable id (text-block id / toolCallId / sourceId / elicitationId / citation fingerprint); transient `data-activity` is replace-last only and never reaches the persistent parts list. * `use-agent-turn-stream.ts` — React hook with reconnect loop; surfaces `{ parts, transientActivity, status, errorText, lastSequence, abort }` to consumers (#77 / #78). * `api.ts` — typed JSON wrappers for create/cancel/snapshot/artifact + consent/elicitation submit endpoints (#78 plug-in surface). * `legacy-snapshot-shim.ts` — TODO(#77 dongdong) projection back to `AgentTurnSnapshot { turn, timeline, artifacts }` so the existing card renders during the transition. Boundary: streamingAnswer (grouped per text-block id), patched turn status; timeline + artifacts pass through from the baseline snapshot only. Wire-protocol contracts (architect msg=bad0cd0f) — all verifiable in the consumer: 1. AI SDK v5 typed parts surface (`StreamPart` mirrors BE; index re-exports SDK-aligned `AgentMessagePart` shapes). 2. Header marker — `x-vercel-ai-ui-message-stream: v1` checked before any `onPart` dispatch. 3. Resume / error / abort — `Last-Event-ID` header + `after_sequence` query on every reconnect; `error` part dispatched then connection terminates (no auto-retry on protocol failure beyond reconnect loop bounded at 5 attempts); `abort` part flips status and the `AbortController` cleans up. 4. Part-level dedup — by stable identifier per part type (architect msg=f35c5a3d Lock C); envelope-atomic replay tolerated. 5. Wire shape adoption — wrapped `{type, data:{...}}` for `data-citation/data-tool-consent/data-elicitation/data-activity` passes through unchanged; outer keys camelCase. 6. Transient `data-activity` — never persisted; surfaced on the separate `transientActivity` slot. Two-phase lifecycle (ApeRAG-specific, captured for D10 reference): client must POST `/agent/chats/{cid}/turns` first to obtain the stream URL, then GET that URL to begin the SSE body. `useChat` is not adopted because its single-step POST+stream lifecycle does not match. `web/package.json`: adds `@ai-sdk/react@^2.0.0` + `ai@^5.0.0` for typed parts surface (used today via re-exports; #77 will lean on `isTextUIPart` / `isToolUIPart` / `isDataUIPart` directly). Verified: `yarn lint` clean; `tsc --noEmit` clean for the touched files (pre-existing main-branch errors in `chat-input.tsx` / `page.tsx` / `collection-form.tsx` unrelated); `yarn dev` boots in 2.3s, GET / / `/auth/signin` / `/workspace/collections` / `/workspace` all return 200. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(phase8 #76 D8.4a): forward-compat tool-output-error wire shape Per architect canonical decision (msg=2f9225f5) — strict AI SDK v5 spec splits tool failure into a separate `tool-output-error` part type (`{toolCallId, errorText}`). BE migration tracked as task #89 (D8.0c+ hygiene fix-forward, owner @cuiwenbo). The reducer now accepts both the current `tool-output-available + errorText` shape and the post-#89 `tool-output-error` shape so the FE rolls forward without coupling to BE timing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(phase8 #76 D8.4a): address Weston B1/B2 — terminal-driven close + SDK-compatible parts Weston msg=63a796f3 review identified two blockers within the locked review boundary; both fixed in-PR. ## B1 — Terminal-driven completion (stream-client.ts) Before: `consumeAgentStream()` returned `{reason:'completed'}` on `reader.read()` `done`, regardless of whether a `finish` / `error` / `abort` part had been dispatched. A clean mid-turn TCP close at the HTTP layer would mark the turn completed instead of triggering the reconnect loop, leaving #77 to render half-streamed parts as the final message. After: EOF without a terminal part returns `{reason:'error', error: 'stream closed before terminal frame'}` so the hook reconnects with `Last-Event-ID` from the highest-seen `id:` field. Existing reconnect budget (5 attempts) bounds persistent failures. ## B2 — SDK-compatible part union (types.ts + reducer.ts + ## legacy-snapshot-shim.ts) Before: `AgentMessagePart` used an ApeRAG-local `{kind: ...}` discriminator. The PR claimed #77 could lean on `@ai-sdk/react`'s `isTextUIPart` / `isToolUIPart` / `isDataUIPart` guards, but the SDK guards branch on `type`, not `kind` — so the seam was nominally SDK-aligned, factually divergent. After: every part uses a `type:` discriminator that matches the SDK exactly: * `text` / `source-url` / `source-document` mirror the corresponding SDK `*UIPart` shapes structurally. * Tool parts use `type: \`tool-${SafeToolName}\`` so the SDK's `isToolUIPart` `startsWith('tool-')` guard accepts them. `toolName` is also kept as a sibling field for direct render access. * `data-citation` / `data-tool-consent` / `data-elicitation` use the SDK `DataUIPart` shape (`{type: 'data-${name}', id, data}`); `id` is the dedup key (citation fingerprint, toolCallId, elicitationId respectively). A compile-time `_AgentMessagePartIsSDKCompatible` assertion in `types.ts` enforces structural assignment to the SDK's `TextUIPart` / `SourceUrlUIPart` / `SourceDocumentUIPart` / `DataUIPart<ApeRAGUIDataTypes>` types — drift fails type-check. Reducer is rewritten to produce the new shapes; consent and elicitation now correctly replace existing parts when their state transitions (the previous `kind:` shape relied on `update?` callback that was a no-op for the consent/elicitation flow). `null` fields from the wire are coerced to `undefined` to satisfy SDK shape expectations. `legacy-snapshot-shim.ts`: top comment claim "minimal timeline (one entry per running tool call)" was a drift — the actual code only passes through `baselineSnapshot.timeline` / `.artifacts`. Comment realigned to actual coverage (per dongdong msg=f33e9039 minor). Verified: `yarn lint` clean; `tsc --noEmit` clean for the touched files (the SDK compatibility assertion compiles, proving structural assignment); `yarn dev` boots in 3.5s on port 3011 with `GET /`, `/auth/signin`, `/workspace/collections`, `/workspace` all 200. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 8ed1d7b commit 63a9d52

11 files changed

Lines changed: 1986 additions & 492 deletions

File tree

web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"i18n:watch": "node scripts/i18n-watch.mjs --watch"
1515
},
1616
"dependencies": {
17+
"@ai-sdk/react": "^2.0.0",
1718
"@diceui/mention": "^0.7.1",
1819
"@hookform/resolvers": "^5.2.1",
1920
"@jsdevtools/rehype-toc": "^3.0.2",
@@ -40,6 +41,7 @@
4041
"@radix-ui/react-tooltip": "^1.2.7",
4142
"@tanstack/react-table": "^8.21.3",
4243
"ahooks": "^3.9.3",
44+
"ai": "^5.0.0",
4345
"async": "^3.2.6",
4446
"axios": "^1.15.2",
4547
"class-variance-authority": "^0.7.1",

0 commit comments

Comments
 (0)