Wave D: native AgentEvent wire + client session rewrite (SDK purged)

Server: ui-stream.ts is verbatim pi AgentEvent passthrough (SSE data
lines + [DONE]); only app-level events are the loop-level error and
turn-settled terminal; onError carries raw provider text (owner ruling).

Client: useChat + the AI-SDK transport replaced by components/session-core.ts
(SessionModel: 409 identity-retry, optimistic c_ appends, absorbed()
adoption, needsContinuation, shared consume for POST + reattach) and the
session.ts hook; the golden-master grouping canon moved client-side into
core/view.ts (projectView); scanners read the native record (phaseAt3Lh,
danglingQuestionOf, graph toolParts, plan-blocks, runner, extraction).

Coverage: the native message is the unit (fork 3) with the call+result
inseparability bond rule; view-line-less messages (markers, error
results) are invisible to coverage and never checkpointed.

Deletes: chat-transport, projection, ui-message, their tests (ported to
tests/session-core.test.ts), and the ai + @ai-sdk/react dependencies —
the Vercel SDK is fully out of the tree. Wire pins in chat-flow rewritten
onto the AgentEvent dialect (eventsOf parser).

Gates: tsc clean, 994/994 tests, eslint clean, build green; live smoke
on PORT=3004 (verbatim wire → native record).
This commit is contained in:
amosblomqvist
2026-09-26 01:52:47 +03:00
parent 7c0e8d9a50
commit f3003d357a
44 changed files with 2299 additions and 2205 deletions
+16 -2
View File
@@ -1,4 +1,18 @@
import { isToolUIPart, type UIMessage } from "ai";
/**
* Wave D: these scans read VIEW messages (core/view.ts LhViewMessage — the
* client's grouped transcript). The part shapes are the same vocabulary the
* pre-Wave-D client dialect used; a tool part is structural: `tool-` prefix +
* toolCallId. The server-side record scans (danglingQuestionOf over native
* LhMessage[]) live in native-messages.ts.
*/
interface UIMessage {
role?: string;
parts: Array<Record<string, unknown> & { type: string }>;
}
function isToolUIPart(part: UIMessage["parts"][number]): boolean {
return part.type.startsWith("tool-") && typeof part.toolCallId === "string";
}
/**
* The pending-question gate: an unanswered QUESTION tool part (quiz/ask)
@@ -76,7 +90,7 @@ function findPending(messages: UIMessage[], names: readonly string[]): PendingTo
if (isToolUIPart(part) && part.state === "input-available") {
const tool = part.type.slice("tool-".length);
if (names.includes(tool)) {
return { toolCallId: part.toolCallId, tool, input: part.input };
return { toolCallId: part.toolCallId as string, tool, input: part.input };
}
}
}