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
+188 -47
View File
@@ -1,7 +1,9 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import * as fs from "node:fs";
import type { UIMessage } from "ai";
import type { LhMessage } from "../src/core/native-messages.ts";
import { newAnswerResult, newPlanBlockMessage, newUserMessage } from "../src/core/native-messages.ts";
import type { ToolResultMessage } from "@earendil-works/pi-ai";
import {
batchRuns,
@@ -11,20 +13,47 @@ import {
messageFingerprint,
messageViewLines,
} from "../src/core/coverage.ts";
import type { CoverageEntry } from "../src/core/contracts.ts";
import type { CoverageEntry } from "../src/core/contracts/graph.ts";
/**
* Extraction coverage — the pure core (vision-extraction-coverage.md,
* settled 2026-09-15). THE MESSAGE IS THE UNIT OF COVERAGE; the fingerprint
* and the LLM prompt share ONE line builder (messageViewLines), so "covered"
* is by construction exactly "what the extractor saw".
* settled 2026-09-15; Wave D native rebase 2026-09-26). THE NATIVE MESSAGE
* IS THE UNIT OF COVERAGE (owner ruling); the fingerprint and the LLM prompt
* share ONE line builder (messageViewLines), so "covered" is by construction
* exactly "what the extractor saw". Batches obey the ONE INSEPARABILITY RULE
* (owner fork 3): a tool-call message and its tool-result messages always
* appear in the same chunk.
*/
function user(id: string, text: string): UIMessage {
return { id, role: "user", parts: [{ type: "text", text }] } as unknown as UIMessage;
function user(id: string, text: string): LhMessage {
return { ...newUserMessage(text), id };
}
function slot(messages: UIMessage[], id: string) {
function teacher(id: string, text: string): LhMessage {
return {
id,
role: "assistant",
content: [{ type: "text", text }],
} as unknown as LhMessage;
}
/** An assistant message calling `calls` (each {id,name}) — the bond anchor. */
function assistantWithCalls(
id: string,
calls: Array<{ id: string; name: string; args?: unknown }>,
): LhMessage {
return {
id,
role: "assistant",
content: calls.map((c) => ({ type: "toolCall", id: c.id, name: c.name, arguments: c.args ?? {} })),
} as unknown as LhMessage;
}
function result(id: string, callId: string, name: string, details: unknown): LhMessage {
return { ...newAnswerResult(callId, name, details), id } as LhMessage;
}
function slot(messages: ReadonlyArray<LhMessage>, id: string) {
const found = coverageSlots(messages).find((s) => s.message.id === id);
assert.ok(found, `slot ${id}`);
return found;
@@ -37,9 +66,8 @@ describe("messageFingerprint — what changes it, what never does", () => {
const same = messageFingerprint(user("m1", "hello world"));
assert.notEqual(a, b);
assert.equal(a, same);
// Role is hashed too — a user and an assistant line never collide.
const teacher = { id: "m1", role: "assistant", parts: [{ type: "text", text: "hello world" }] } as unknown as UIMessage;
assert.notEqual(a, messageFingerprint(teacher));
// Role is hashed too — a learner and a teacher line never collide.
assert.notEqual(a, messageFingerprint(teacher("m1", "hello world")));
});
it("the message id is the provenance tag — part of exactly what the extractor saw", () => {
@@ -48,39 +76,61 @@ describe("messageFingerprint — what changes it, what never does", () => {
// extraction view byte-for-byte. (Ids are stable in practice — the
// write-ahead persist owns them.)
assert.notEqual(messageFingerprint(user("m1", "same")), messageFingerprint(user("m2", "same")));
// The id is the ONLY tag: the same id + content re-fingerprints equal.
// The same id + content re-fingerprints equal.
assert.equal(messageFingerprint(user("m1", "same")), messageFingerprint(user("m1", "same")));
});
it("data parts are invisible — plan-block attach never re-opens a covered message", () => {
const plain = user("m1", "teach me the chain rule");
const withData = {
...plain,
parts: [...plain.parts, { type: "data-plan-blocks", planBlocks: [] }],
} as unknown as UIMessage;
assert.equal(messageFingerprint(plain), messageFingerprint(withData));
it("marker messages are invisible — a plan-block attach never re-opens anything", () => {
// Wave D: the marker is its OWN message (not a part on another one);
// its view is empty, so it neither re-opens others nor itself
// carries any learning evidence.
const marker = newPlanBlockMessage("call_p1", {
kind: "full",
at: "2026-09-26T00:00:00.000Z",
current: null,
nodes: [],
edges: [],
}) as LhMessage;
assert.deepEqual(messageViewLines(marker, marker.id), []);
// And the message it rides beside keeps its fingerprint untouched:
const before = messageFingerprint(teacher("t1", "the chain rule"));
const withMarker = [teacher("t1", "the chain rule"), marker];
assert.equal(
messageFingerprint(withMarker[0]!),
before,
"the neighbour's fingerprint is unchanged by the attach",
);
});
it("a quiz answer merging in changes the fingerprint — and only that message's", () => {
const asking = {
id: "a1",
role: "assistant",
parts: [{ type: "tool-quiz", toolCallId: "q1", state: "input-available", input: { question: "Q?" } }],
} as unknown as UIMessage;
const answered = {
id: "a1",
role: "assistant",
parts: [
{ type: "tool-quiz", toolCallId: "q1", state: "output-available", input: { question: "Q?" }, output: { questionText: "Q?", correct: true } },
],
} as unknown as UIMessage;
const fpBefore = messageFingerprint(asking);
assert.notEqual(fpBefore, messageFingerprint(answered));
// An output-bearing line's mass counts toward coverage tokens.
assert.ok(
messageViewLines(answered, answered.id).join("").length >
messageViewLines(asking, asking.id).join("").length,
);
it("an answered quiz is its OWN message — the asking message never re-opens", () => {
// Wave D: the answer arrives as the call's toolResult message; the
// asking assistant message's fingerprint is STABLE (the old wire
// merged the answer into the asking group and re-opened it).
const asking = assistantWithCalls("a1", [{ id: "q1", name: "quiz", args: { question: "Q?" } }]);
const answer = result("r1", "q1", "quiz", { questionText: "Q?", correct: true });
const fpAsking = messageFingerprint(asking);
assert.equal(fpAsking, messageFingerprint(assistantWithCalls("a1", [{ id: "q1", name: "quiz", args: { question: "Q?" } }])));
// The answer's own line carries the graded outcome:
const lines = messageViewLines(answer, "r1");
assert.deepEqual(lines, ["QUIZ [q1] Q? — the learner answered correctly"]);
assert.ok(slot([asking, answer], "r1").tokens > 0, "the quiz outcome counts toward coverage tokens");
});
it("quiz outcome wording — dontKnow and incorrect each get their line", () => {
const dk = result("r1", "q1", "quiz", { questionText: "Q?", dontKnow: true });
assert.deepEqual(messageViewLines(dk, "r1"), ["QUIZ [q1] Q? — the learner did not know"]);
const wrong = result("r2", "q2", "quiz", { questionText: "W?", correct: false, note: "mixed up signs" });
assert.deepEqual(messageViewLines(wrong, "r2"), ["QUIZ [q2] W? — the learner answered incorrectly (note: mixed up signs)"]);
});
it("other clean tool results get one generic line; errors are invisible", () => {
const demo = result("r1", "j1", "demo", { title: "T" });
assert.deepEqual(messageViewLines(demo, "r1"), ["(demo tool result delivered)"]);
const failed = {
...newAnswerResult("j2", "visual", { oops: true }),
isError: true,
} as LhMessage;
assert.deepEqual(messageViewLines(failed, "r9"), [], "a failed call is retry context, not learning evidence");
});
});
@@ -96,7 +146,9 @@ describe("coverageOf — uncovered is DERIVED, never dual-tracked", () => {
{ chatId: "c2", fp: slot(messages, "m3").fp, tokens: 1, foldedAt: "2026-09-15T00:00:00.000Z" },
];
const cov = coverageOf(messages, entries, "c1");
assert.deepEqual(cov.uncovered.map((s) => s.message.id), ["m3"]);
// `uncovered` is INDEXES (the batcher needs positions for bonding):
assert.deepEqual(cov.uncovered, [2]);
assert.deepEqual(cov.slots[cov.uncovered[0]!]!.message.id, "m3");
assert.equal(cov.coveredCount, 2);
assert.equal(cov.uncoveredCount, 1);
assert.equal(cov.uncoveredTokens, slot(messages, "m3").tokens);
@@ -114,7 +166,8 @@ describe("coverageOf — uncovered is DERIVED, never dual-tracked", () => {
assert.equal(coverageOf(messages, entries, "c1").uncoveredCount, 0);
const mutated = [user("m1", "aaaa!"), messages[1]!];
const cov = coverageOf(mutated, entries, "c1");
assert.deepEqual(cov.uncovered.map((s) => s.message.id), ["m1"]);
assert.deepEqual(cov.uncovered, [0]);
assert.equal(cov.slots[0]!.message.id, "m1");
});
});
@@ -128,19 +181,106 @@ describe("batchRuns — token-budgeted, consecutive, never splits a message", ()
user("m3", "c".repeat(40)),
user("m4", "d".repeat(40)),
]);
const runs = batchRuns(slots, 45);
assert.deepEqual(runs.map((r) => r.map((s) => s.message.id)), [["m1", "m2", "m3"], ["m4"]]);
const runs = batchRuns(slots, [0, 1, 2, 3], 45);
assert.deepEqual(
runs.map((r) => r.map((s) => s.message.id)),
[["m1", "m2", "m3"], ["m4"]],
);
});
it("a single message larger than the budget becomes its own over-budget run", () => {
const slots = coverageSlots([user("m1", "x".repeat(400)), user("m2", "y".repeat(20))]);
const runs = batchRuns(slots, 10);
assert.deepEqual(runs.map((r) => r.map((s) => s.message.id)), [["m1"], ["m2"]]);
assert.deepEqual(
batchRuns(slots, [0, 1], 10).map((r) => r.map((s) => s.message.id)),
[["m1"], ["m2"]],
);
});
it("an empty or non-positive budget never drops messages into a void", () => {
assert.deepEqual(batchRuns(coverageSlots([user("m1", "hi")]), 0), []);
assert.deepEqual(batchRuns([], 4096), []);
assert.deepEqual(batchRuns(coverageSlots([user("m1", "hi")]), [0], 0), []);
assert.deepEqual(batchRuns([], [], 4096), []);
});
it("THE BOND RULE (owner fork 3): a call and its results never split, even under a tiny budget", () => {
// The call's assistant message is COVERED; only the (new) answer is
// uncovered — the asking message rides into the chunk anyway, and
// the two NEVER split despite a budget of 1.
const messages = [
user("u1", "topic"),
assistantWithCalls("a1", [{ id: "q1", name: "quiz", args: { question: "Q?" } }]),
result("r1", "q1", "quiz", { questionText: "Q?", correct: true }),
];
const slots = coverageSlots(messages);
const cov = coverageOf(
messages,
coverageSlots([messages[0]!, messages[1]!]).map((s) => ({
chatId: "c1",
fp: s.fp,
tokens: s.tokens,
foldedAt: "2026-09-26T00:00:00.000Z",
})),
"c1",
);
assert.deepEqual(cov.uncovered, [2], "only the answer is uncovered");
const runs = batchRuns(slots, cov.uncovered, 1);
assert.deepEqual(
runs.map((r) => r.map((s) => s.message.id)),
[["a1", "r1"]],
"the covered asking message rides along as context — one chunk, over budget, never split",
);
});
it("the reverse bond: an uncovered CALL pulls its covered results into the chunk", () => {
const messages = [
{
id: "a1",
role: "assistant",
content: [
{ type: "text", text: "Let me research that and build a demo." },
{ type: "toolCall", id: "j1", name: "research", arguments: { topic: "x" } },
{ type: "toolCall", id: "j2", name: "demo", arguments: { title: "T" } },
],
} as unknown as LhMessage,
result("r1", "j1", "research", { summary: "s" }),
result("r2", "j2", "demo", { done: true }),
];
const slots = coverageSlots(messages);
const cov = coverageOf(
messages,
coverageSlots([messages[1]!, messages[2]!]).map((s) => ({
chatId: "c1",
fp: s.fp,
tokens: s.tokens,
foldedAt: "2026-09-26T00:00:00.000Z",
})),
"c1",
);
assert.deepEqual(cov.uncovered, [0], "only the calling message is uncovered");
const runs = batchRuns(slots, cov.uncovered, 10_000);
assert.deepEqual(
runs.map((r) => r.map((s) => s.message.id)),
[["a1", "r1", "r2"]],
"all three travel together (checkpoint idempotency makes re-marking the results a no-op)",
);
});
it("adjacent messages WITHOUT a bond still batch greedily (only calls bond)", () => {
// Two plain teacher messages after a bonded pair: the pair is one
// range, the plain messages group by budget alone.
const messages = [
assistantWithCalls("a1", [{ id: "q1", name: "quiz" }]),
result("r1", "q1", "quiz", { questionText: "Q?", correct: true }),
teacher("t1", "x".repeat(40)),
teacher("t2", "y".repeat(40)),
];
const slots = coverageSlots(messages);
assert.equal(slots[0]!.tokens, 0, "a call-only assistant message has no view lines");
// budget 30: the pair (~12) + t1 (~14) = 26 fits; t2 overflows.
const runs = batchRuns(slots, [0, 2, 3], 30);
assert.deepEqual(
runs.map((r) => r.map((s) => s.message.id)),
[["a1", "r1", "t1"], ["t2"]],
);
});
});
@@ -158,6 +298,7 @@ describe("batchTag — deterministic, ref-stable, mutation-sensitive", () => {
assert.notEqual(batchTag(slots), batchTag(mutated));
});
});
describe("the all-sessions coverage GET is dead (extraction-UX close-out, 2026-09-24)", () => {
it("the route file is gone — the only coverage endpoint is the Extract POST", () => {
// UI confirmation (ff1ec7a): nothing client-side consumes the