One shot per chat, fired at the first turn's onFinish: the session's topic (its only typed message) is embedded as an EmbeddingGemma retrieval query; the semantically closest graph propositions are selected under the session's token budget (pure budget, no similarity threshold — the decided doctrine) and delivered as a marker-only steer through the wake pipeline, queuing behind pending questions like every sub-agent result. - contracts: ChatSettings.graphContextTokens (default 1024, max 4096, step 128, 0 = off); GraphContextData + graphContextText; the graph-context JobKind + buildGraphContextSteer (id steer_ctx_<chatId>) - core/graph-context.ts: brute-force cosine top-K, greedy chars/4 fill under budget, most-recent UnderstandingLevel fold, count cap 64, parse-don't-cast at the marker seam - server: embedQuery (role-asymmetric query format, same warm singleton — vector-space parity by construction); the runner (startGraphContext- Injection one-shot gates; runGraphContextJob validates ledger parity); wake branches (chatIdOf/jobIdOf, finalizeJob done, recoverStaleJobs re-deliver or contextRespawn); abortJob kills context jobs SILENTLY (nothing was promised in the lesson); turn-service trigger seam - adapter: data-graph-context flattens to the canonical block (malformed parts have no model face) - UI: the budget slider (home dock + /chat/new draft, the session-data strip); the quiet SystemLine "~537 · 10 propositions"; dock kind "Knowledge context"; model-picker PUT now spreads settings (a model pick can no longer clear the sibling override) Live-smoked end to end on the real embedder + real GLM: seeded ledger (10 propositions), session on "modal logic: possible worlds and the necessity box" — job in the dock, queued behind the prober's question, delivered on the answer, SystemLine in the transcript, wake turn reads the block and continues the probe. Gates: tsc, lint, 459/459, build.
120 lines
4.7 KiB
TypeScript
120 lines
4.7 KiB
TypeScript
/**
|
|
* Unit tests — the record→pi adapter's SYSTEM MESSAGE flattening (the
|
|
* custom-message scheme, user-decided 2026-09-12).
|
|
*
|
|
* Wake and verdict messages are user-role messages carrying ONLY a marker
|
|
* data part — no human text exists. The adapter owns their model-facing
|
|
* wording (flattens the marker to the canonical text from core contracts):
|
|
* the transcript never persists fake human speech, and the wording can't
|
|
* drift between the seam, the tests, and reality.
|
|
*/
|
|
|
|
import assert from "node:assert/strict";
|
|
import { describe, it } from "node:test";
|
|
import type { UIMessage } from "ai";
|
|
import { recordToPiMessages } from "../src/server/pi/record-adapter.ts";
|
|
import { HANDOFF_TEXT, verdictText } from "../src/core/contracts/chat.ts";
|
|
import { buildAbortSteer, buildProbeHandoffSteer } from "../src/core/contracts/jobs.ts";
|
|
|
|
function userText(text: string): UIMessage {
|
|
return { id: `u_${text.length}`, role: "user", parts: [{ type: "text", text }] };
|
|
}
|
|
|
|
function userParts(parts: unknown[]): UIMessage {
|
|
return { id: "u_sys", role: "user", parts: parts as UIMessage["parts"] };
|
|
}
|
|
|
|
const texts = (msgs: ReturnType<typeof recordToPiMessages>): string[] => {
|
|
const out: string[] = [];
|
|
for (const m of msgs) {
|
|
if (m.role === "user") {
|
|
for (const c of m.content) {
|
|
if ((c as { type?: string }).type === "text") out.push((c as { text: string }).text);
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
};
|
|
|
|
describe("record-adapter: system message flattening", () => {
|
|
it("an ACCEPT verdict flattens to the canonical model-facing text (no human speech persisted)", () => {
|
|
const out = recordToPiMessages([
|
|
userText("teach me"),
|
|
userParts([{ type: "data-verdict", id: "vd_1", data: { accepted: true, at: "…" } }]),
|
|
]);
|
|
assert.deepEqual(texts(out), ["teach me", verdictText(true)]);
|
|
assert.equal(texts(out)[1], "The learner accepted the plan — begin teaching.");
|
|
});
|
|
|
|
it("a DECLINE verdict flattens to the canonical revision text", () => {
|
|
const out = recordToPiMessages([
|
|
userParts([{ type: "data-verdict", id: "vd_2", data: { accepted: false, at: "…" } }]),
|
|
]);
|
|
assert.deepEqual(texts(out), [verdictText(false)]);
|
|
assert.match(texts(out)[0]!, /revised/);
|
|
});
|
|
|
|
it("the probe handoff steer flattens to the canonical plan-authoring instruction", () => {
|
|
const handoff = buildProbeHandoffSteer("chat_x");
|
|
assert.equal(handoff.parts.length, 1, "the handoff is marker-ONLY — no text part");
|
|
assert.equal(handoff.parts[0]!.type, "data-probe-handoff");
|
|
const out = recordToPiMessages([handoff]);
|
|
assert.deepEqual(texts(out), [HANDOFF_TEXT]);
|
|
assert.match(texts(out)[0]!, /plan_init/);
|
|
});
|
|
|
|
it("note + verdict (the two-message accept) reach the model as two user texts, in order", () => {
|
|
const out = recordToPiMessages([
|
|
userText("Start with the basics, skip the history."),
|
|
userParts([{ type: "data-verdict", id: "vd_3", data: { accepted: true, at: "…" } }]),
|
|
]);
|
|
assert.deepEqual(texts(out), ["Start with the basics, skip the history.", verdictText(true)]);
|
|
});
|
|
|
|
it("sub-agent steers keep their text (payload, not boilerplate): the artifact part adds nothing", () => {
|
|
const abort = buildAbortSteer({ id: "j1", kind: "research", toolCallId: "c1" });
|
|
const out = recordToPiMessages([abort]);
|
|
const got = texts(out);
|
|
assert.equal(got.length, 1, "one user text — the marker part is UI-only");
|
|
assert.match(got[0]!, /aborted/i);
|
|
});
|
|
|
|
it("a pure UI data part with no system meaning still never reaches the model", () => {
|
|
const out = recordToPiMessages([
|
|
userParts([{ type: "data-plan-block", id: "pb", data: { kind: "full", at: "…", current: null, nodes: [], edges: [] } }]),
|
|
]);
|
|
assert.equal(out.length, 0, "no model face at all — skipped");
|
|
});
|
|
|
|
it("the graph-context steer flattens to the canonical prior-knowledge block", () => {
|
|
const out = recordToPiMessages([
|
|
userParts([
|
|
{
|
|
type: "data-graph-context",
|
|
id: "ctx1",
|
|
data: {
|
|
chatId: "chat_1",
|
|
budget: 1024,
|
|
tokens: 64,
|
|
propositions: [
|
|
{ id: "n1", statement: "Water boils at 100°C.", level: "solid" },
|
|
{ id: "n2", statement: "Evaporation cools things.", level: null },
|
|
],
|
|
},
|
|
},
|
|
]),
|
|
]);
|
|
assert.equal(out.length, 1, "one user message — the marker part is the model face");
|
|
const got = texts(out);
|
|
assert.equal(got.length, 1);
|
|
assert.match(got[0]!, /Prior-knowledge context/);
|
|
assert.match(got[0]!, /\[solid\] Water boils at 100°C\./);
|
|
});
|
|
|
|
it("a malformed graph-context part has no model face — the message is skipped wholesale", () => {
|
|
const out = recordToPiMessages([
|
|
userParts([{ type: "data-graph-context", id: "ctx2", data: { chatId: "chat_1" } }]),
|
|
]);
|
|
assert.equal(out.length, 0, "parse-don't-cast: no validated data, no face");
|
|
});
|
|
}); |