wave 7: the prober reads its map — graph-context injection dies whole
The owner's zero-configuration ruling (2026-09-23): nothing configures the graph beyond chat models. The prior-knowledge injection — its budget dial, its runner, its marker part, its dock/dispatch entries — is removed across all bands; the prober now reads the learner's persistent knowledge map on demand through two gated tools instead: - list_sessions: every birth session, names + kinds + counts + dates (graphSessionIndex, Platform's core seam) - list_propositions(session): what was born there + the session's own latest understanding level + that evaluation's date (graphSessionPropositions) — names/levels/dating ONLY, no statement text (owner ruling: the map's organization is the prober's business, its prose is not) Registered via createProberTools when chat-handler passes graphStore (the graphToolsEnabled gate decided server-side, per-turn fresh); the prober doctrine gains the map-review nudge at session start. Also: runner's JobKind special-casing collapses (KIND_ROLE/KIND_RUNNERS are plain records again), semantic-clusters.ts dies with its test, the embedder dep's doc tells the wave-7 stub story, and ports.ts's readClusterLabels/readNamingRuns surface stays untouched for Platform's ledger-port interlude. 848/848 tests, typecheck/lint/build green.
This commit is contained in:
@@ -12,6 +12,7 @@ import { effectiveSettings, stores } from "./stores.ts";
|
||||
import { searchAvailable, searchProvider } from "./search.ts";
|
||||
import {
|
||||
extractionEnabled,
|
||||
graphToolsEnabled,
|
||||
resolveConversationSpec,
|
||||
resolveRoleSpec,
|
||||
} from "../core/settings-resolution.ts";
|
||||
@@ -274,6 +275,10 @@ export async function handleChatRequest(
|
||||
jobStore: stores.jobStore,
|
||||
spawn: (job, input) => void jobRunner.runJob(job.id, input),
|
||||
searchAvailable,
|
||||
// The map reader (wave 7): the prober reads the learner's
|
||||
// knowledge graph through tools when the gate opens —
|
||||
// graph master switch AND the agent.graphTools toggle.
|
||||
...(graphToolsEnabled(effective) ? { graphStore: stores.graphStore } : {}),
|
||||
},
|
||||
agentOpts,
|
||||
);
|
||||
|
||||
+6
-55
@@ -34,7 +34,6 @@
|
||||
import type { UIMessage } from "ai";
|
||||
import {
|
||||
buildAbortSteer,
|
||||
buildGraphContextSteer,
|
||||
buildSubagentSteer,
|
||||
isSubagentFailure,
|
||||
steerDelivered,
|
||||
@@ -61,9 +60,6 @@ interface QueuedSteer {
|
||||
interface WakeState {
|
||||
turnRunner?: TurnRunner;
|
||||
respawn?: JobRespawn;
|
||||
/** Crash recovery for context-injection jobs (they are not sub-agent
|
||||
* loops — their runner takes the graph deps; chat-handler wires it). */
|
||||
contextRespawn?: (jobId: string) => void;
|
||||
/** chatId → steers awaiting delivery, in arrival order. */
|
||||
queues: Map<string, QueuedSteer[]>;
|
||||
/** Chats whose pump loop is currently active (serialization per chat). */
|
||||
@@ -91,13 +87,6 @@ export function setJobRespawn(respawn: JobRespawn): void {
|
||||
state().respawn = respawn;
|
||||
}
|
||||
|
||||
/** chat-handler wires this at module init: the graph-context runner
|
||||
* (subagents/graph-context.ts runGraphContextJob — re-runnable from the
|
||||
* job record alone, which is all crash recovery has). */
|
||||
export function setContextRespawn(respawn: (jobId: string) => void): void {
|
||||
state().contextRespawn = respawn;
|
||||
}
|
||||
|
||||
const POLL_MS = 400;
|
||||
/** After the gates open, wait a beat before firing — a client that just
|
||||
* answered a question is about to send its continuation; don't 409 it.
|
||||
@@ -124,11 +113,7 @@ function delay(ms: number): Promise<void> {
|
||||
/** The job a steer belongs to, if it is a job steer (result/abort/context);
|
||||
* probe-handoff steers have no job — null. */
|
||||
function jobIdOf(steer: SteerMessage): string | null {
|
||||
const meta = steer.metadata as {
|
||||
subagentResult?: { jobId: string };
|
||||
graphContext?: { jobId: string };
|
||||
};
|
||||
return meta.subagentResult?.jobId ?? meta.graphContext?.jobId ?? null;
|
||||
return (steer.metadata as { subagentResult?: { jobId: string } }).subagentResult?.jobId ?? null;
|
||||
}
|
||||
|
||||
/** Finalize a delivered steer's job bookkeeping — job steers only. */
|
||||
@@ -250,12 +235,6 @@ export function abortJob(jobId: string): AbortOutcome {
|
||||
if (!job) return { ok: false, reason: "not_found" };
|
||||
if (job.status !== "running") return { ok: false, reason: "not_running" };
|
||||
|
||||
// Context-injection jobs abort SILENTLY: nothing was ever promised in the
|
||||
// lesson (server-initiated scaffolding, unlike a teacher-dispatched
|
||||
// sub-agent), so no abort steer — flipping the record and killing the
|
||||
// run is the whole story. Steps 1–3 below are shared; step 4 is skipped.
|
||||
const isContext = job.kind === "graph-context";
|
||||
|
||||
fireJobAbort(jobId); // no-op when the loop already returned (delivering)
|
||||
|
||||
// Drop any queued steers for this job (their deliver() resolves unfired).
|
||||
@@ -270,22 +249,17 @@ export function abortJob(jobId: string): AbortOutcome {
|
||||
}
|
||||
|
||||
stores.jobStore.updateJob(jobId, { status: "error", error: "Aborted by learner", activity: "aborted" });
|
||||
if (!isContext) {
|
||||
void deliverJobResult(buildAbortSteer(job)).catch(() => {
|
||||
/* deliverJobResult marks the job error on unrecoverable failure */
|
||||
});
|
||||
}
|
||||
void deliverJobResult(buildAbortSteer(job)).catch(() => {
|
||||
/* deliverJobResult marks the job error on unrecoverable failure */
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
function chatIdOf(steer: SteerMessage): string {
|
||||
// The probe handoff and the context injection carry their chat directly
|
||||
// (the context steer's job record would say it too, but the metadata is
|
||||
// the cheap path — same rule as the handoff).
|
||||
// The probe handoff carries its chat directly (the job record would
|
||||
// say it too, but the metadata is the cheap path).
|
||||
const handoff = (steer.metadata as { probeHandoff?: { chatId: string } }).probeHandoff;
|
||||
if (handoff) return handoff.chatId;
|
||||
const injected = (steer.metadata as { graphContext?: { chatId: string } }).graphContext;
|
||||
if (injected) return injected.chatId;
|
||||
// The chat is resolvable from the job record; the steer's metadata
|
||||
// carries jobId, which indexes the store.
|
||||
const jobId = jobIdOf(steer);
|
||||
@@ -394,12 +368,6 @@ async function pump(chatId: string): Promise<void> {
|
||||
function finalizeJob(jobId: string): void {
|
||||
const job = stores.jobStore.getJob(jobId);
|
||||
if (!job || job.status !== "running") return;
|
||||
// Context-injection jobs have no sub-agent result shape — a delivered
|
||||
// block is a success, full stop (isSubagentFailure would misread it).
|
||||
if (job.kind === "graph-context") {
|
||||
stores.jobStore.updateJob(jobId, { status: "done", activity: "delivered" });
|
||||
return;
|
||||
}
|
||||
const result = job.result;
|
||||
const failed = result ? isSubagentFailure(job.kind, result as never) : true;
|
||||
stores.jobStore.updateJob(jobId, {
|
||||
@@ -474,23 +442,6 @@ export function recoverStaleJobs(): void {
|
||||
if (isJobLive(job.id)) continue; // provably executing here — never re-run beside it
|
||||
if (!stores.chatStore.getChat(job.chatId)) continue;
|
||||
if (isTurnRunning(job.chatId)) continue; // a wake turn may be delivering it right now
|
||||
if (job.kind === "graph-context") {
|
||||
// Context-injection orphans: a stored block re-delivers in its own
|
||||
// steer shape; a job that died before selecting re-runs from the
|
||||
// record (the graph runner — not the sub-agent respawn seam).
|
||||
if (job.result) {
|
||||
stores.jobStore.updateJob(job.id, { activity: "recovering: re-delivering" });
|
||||
void deliverJobResult(
|
||||
buildGraphContextSteer({ chatId: job.chatId, jobId: job.id }, job.result as never),
|
||||
).catch(() => {
|
||||
/* deliverJobResult marks the job error on failure */
|
||||
});
|
||||
} else if (s.contextRespawn) {
|
||||
stores.jobStore.updateJob(job.id, { activity: "recovering: re-running" });
|
||||
s.contextRespawn(job.id);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (job.result) {
|
||||
// The wake never fired (or crashed mid-flight): re-deliver.
|
||||
const steer = buildSubagentSteer(job, job.result as never);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { UIMessage } from "ai";
|
||||
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
||||
import type { AssistantMessage, Message, ToolResultMessage } from "@earendil-works/pi-ai";
|
||||
import { graphContextOfMessage } from "../../core/graph-context.ts";
|
||||
import { graphContextText, HANDOFF_TEXT, verdictText } from "../../core/contracts/chat.ts";
|
||||
import { HANDOFF_TEXT, verdictText } from "../../core/contracts/chat.ts";
|
||||
|
||||
/**
|
||||
* The record → pi-ai transcript adapter (docs/pi-engine-swap.md §4). The
|
||||
@@ -62,13 +61,6 @@ function systemTextOf(message: { parts: ReadonlyArray<{ type: string; data?: unk
|
||||
return verdictText((part.data as { accepted?: unknown } | undefined)?.accepted === true);
|
||||
}
|
||||
if (part.type === "data-probe-handoff") return HANDOFF_TEXT;
|
||||
// Graph context injection: the block text derives from the part's
|
||||
// validated data (parse-don't-cast — a malformed part has no model
|
||||
// face, and the message then never reaches the provider).
|
||||
if (part.type === "data-graph-context") {
|
||||
const data = graphContextOfMessage({ parts: message.parts });
|
||||
return data ? graphContextText(data) : null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
*/
|
||||
|
||||
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
import { GRAPH_CONTEXT_TOKENS_MAX, isHexColor, type ChatSettings } from "../core";
|
||||
import { isHexColor, type ChatSettings } from "../core";
|
||||
import type {
|
||||
AgentSettings,
|
||||
AppearanceSettings,
|
||||
@@ -37,7 +37,6 @@ export function parseSettingsBody(body: unknown): SettingsBodyResult<ChatSetting
|
||||
const raw = body as {
|
||||
model?: { provider?: unknown; id?: unknown };
|
||||
thinkingLevel?: unknown;
|
||||
graphContextTokens?: unknown;
|
||||
};
|
||||
const settings: ChatSettings = {};
|
||||
if (raw.model !== undefined) {
|
||||
@@ -57,23 +56,10 @@ export function parseSettingsBody(body: unknown): SettingsBodyResult<ChatSetting
|
||||
}
|
||||
settings.thinkingLevel = raw.thinkingLevel as ThinkingLevel;
|
||||
}
|
||||
// Graph context injection budget (vision-context-injection.md): integer
|
||||
// tokens, 0 (off) to GRAPH_CONTEXT_TOKENS_MAX, no silent clamping — the
|
||||
// slider is the only writer, and it only ever sends multiples of the step.
|
||||
if (raw.graphContextTokens !== undefined) {
|
||||
if (
|
||||
typeof raw.graphContextTokens !== "number" ||
|
||||
!Number.isInteger(raw.graphContextTokens) ||
|
||||
raw.graphContextTokens < 0 ||
|
||||
raw.graphContextTokens > GRAPH_CONTEXT_TOKENS_MAX
|
||||
) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `graphContextTokens must be an integer between 0 and ${GRAPH_CONTEXT_TOKENS_MAX}`,
|
||||
};
|
||||
}
|
||||
settings.graphContextTokens = raw.graphContextTokens;
|
||||
}
|
||||
// Wave 7 killed the graph-context injection (owner: zero configuration
|
||||
// beyond chat models — the prober reads its map through tools now); its
|
||||
// budget dial is gone with it. A stale client sending the old key has
|
||||
// it silently dropped here (forward-compatible, like any unknown key).
|
||||
return { ok: true, settings };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user