Commit 63a9d52
* 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
- src
- components/chat
- features/agent-runtime
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
| |||
40 | 41 | | |
41 | 42 | | |
42 | 43 | | |
| 44 | + | |
43 | 45 | | |
44 | 46 | | |
45 | 47 | | |
| |||
0 commit comments