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:
@@ -282,3 +282,48 @@ export function hasProbeDone(messages: readonly LhMessage[]): boolean {
|
||||
export function answeredCallIds(messages: readonly LhMessage[]): Set<string> {
|
||||
return resultIdsFrom(messages);
|
||||
}
|
||||
|
||||
// ─── the continuation fold (Wave D: the SDK's
|
||||
// lastAssistantMessageIsCompleteWithToolCalls, natively) ──────────────────
|
||||
|
||||
/**
|
||||
* Does the record END on un-consumed tool results — a turn that stopped
|
||||
* after its tools ran (a job dispatch's ack, an aborted answer turn),
|
||||
* leaving the agent owing a continuation? This is the mount/sync
|
||||
* continuation seam's gate (chat.tsx): when true and nothing runs, the
|
||||
* client sends {type:"continue"} and the teacher picks up from the record.
|
||||
*
|
||||
* The pre-Wave-D client asked the SDK's
|
||||
* lastAssistantMessageIsCompleteWithToolCalls (last view message is
|
||||
* assistant; its last step's tool calls all output-available or
|
||||
* output-error; at least one). The native fold over the record: the last
|
||||
* message is a toolResult, and the results' OWNING assistant message has
|
||||
* at least one call, ALL of them answered (a dangling question — the
|
||||
* dock's domain — fails the all-answered rule and never fires a turn,
|
||||
* exactly as the SDK's every(output-available) did).
|
||||
*/
|
||||
export function needsContinuation(messages: readonly LhMessage[]): boolean {
|
||||
const last = messages[messages.length - 1];
|
||||
if (!last || (last as { role?: string }).role !== "toolResult") return false;
|
||||
// The trailing results batch (contiguous by construction — the agent
|
||||
// appends its batch; the question gate keeps a quiz call and its
|
||||
// learner answer adjacent).
|
||||
let start = messages.length - 1;
|
||||
while (
|
||||
start > 0 &&
|
||||
(messages[start - 1] as { role?: string } | undefined)?.role === "toolResult"
|
||||
) {
|
||||
start--;
|
||||
}
|
||||
const owner = messages[start - 1];
|
||||
if (!owner || (owner as { role?: string }).role !== "assistant") return false;
|
||||
const content = (owner as { content?: unknown }).content;
|
||||
if (!Array.isArray(content)) return false;
|
||||
const answered = new Set(
|
||||
messages.slice(start).map((m) => (m as ToolResultMessage).toolCallId),
|
||||
);
|
||||
const calls = (content as Array<{ type?: string; id?: string }>).filter(
|
||||
(b) => b.type === "toolCall" && typeof b.id === "string",
|
||||
);
|
||||
return calls.length > 0 && calls.every((b) => answered.has(b.id!));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user