feat(phase8 #76 D8.4a): FE AI SDK-compatible stream transport + part reducer#1700
Merged
Conversation
…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>
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>
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
D8.4a first-cut. Replaces the legacy
AgentRuntimeRedisStoreSSE consumer with afetch + ReadableStreamtransport that speaks the AI SDK v5 UI Message Stream Protocol, hooks the new client intochat-messages.tsxthrough a narrowlegacy-snapshot-shim, and lays theparts + transientActivity + status/error/abort/resumeseam that #77 (parts renderer) and #78 (interactive consent + elicitation UI) will plug into. Architect msg=bad0cd0f locked the 6 client-side contracts; this PR delivers each in a verifiable form (see below).Module layout (
web/src/features/agent-runtime/)types.tsStreamParttyped union (mirrorsaperag/domains/agent_runtime/wire/parts.py:1) + at-restAgentMessagePart(text/tool/source-url/source-document/citation/tool-consent/elicitation) shaped to align with@ai-sdk/react'sUIMessagePart.stream-parser.tsid:+data:only, comments/heartbeats skipped, trailing partial frames carried.stream-client.tsx-vercel-ai-ui-message-stream: v1response header, forwardsLast-Event-IDon resume, terminates onfinish/error/abortand on localAbortSignal.reducer.tstool-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.tsapi.tslegacy-snapshot-shim.tsConsumer rationale: AI SDK-compatible transport (not `useChat`)
Per architect lock msg=ed98280c + Weston msg=563157a8 + symphony msg=592de041 + dongdong msg=8883b85f, this PR ships an AI SDK-compatible stream transport rather than a `useChat` adoption. Why:
(Captured for D10 reference: ApeRAG agent runtime SSE is two-phase — POST `/turns` → GET `stream_url`. Worth normalising in the D8 doc later if symphony agrees, follow-up of D8.0c.)
6 client-side contracts (architect msg=bad0cd0f) — all delivered
Tool-output failure shape (minor architect-canonical drift, please confirm)
BE `parts.py:184` emits `tool-output-available` with an optional `errorText` field set on failure. The strict AI SDK v5 spec splits this into `tool-output-available` (success) / `tool-output-error` (failure). The reducer normalises both shapes onto a single `AgentToolPart.state ∈ {output-available, output-error}` with `errorText`, so consumers don't need to care. Calling out for symphony's review — happy to amend either side.
`legacy-snapshot-shim.ts` boundary (per PM lock msg=ed98280c)
Verification
Test plan
Hand-off seams for #77 / #78
🤖 Generated with Claude Code