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:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { ActivityIcon, BookOpenIcon, BotIcon, FileTextIcon, PenToolIcon, PlayIcon, SearchIcon, SquareIcon, WaypointsIcon } from "lucide-react";
|
||||
import { ActivityIcon, BotIcon, FileTextIcon, PenToolIcon, PlayIcon, SearchIcon, SquareIcon, WaypointsIcon } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { CallUsage, JobKind, JobRecord } from "@/src/core";
|
||||
import { CONTEXT_LIMITS, isModelsRefusal, jobCost } from "@/src/core";
|
||||
@@ -236,7 +236,6 @@ const KIND_ICON: Record<JobKind, typeof SearchIcon> = {
|
||||
demo: PlayIcon,
|
||||
visual: PenToolIcon,
|
||||
pdf: FileTextIcon, // sources v0 — type-forced entry (JobKind grew "pdf")
|
||||
"graph-context": BookOpenIcon,
|
||||
};
|
||||
|
||||
/** Abort states per blob id: pending (fired) / failed (backend absent). */
|
||||
@@ -353,9 +352,7 @@ function ActivityBlobRow({
|
||||
? "Figure maker"
|
||||
: blob.jobKind === "pdf"
|
||||
? "PDF reader"
|
||||
: blob.jobKind === "graph-context"
|
||||
? "Graph context"
|
||||
: "Sub-agent";
|
||||
: "Sub-agent";
|
||||
return (
|
||||
<div
|
||||
data-testid={`activity-blob-${blob.process}`}
|
||||
|
||||
@@ -23,9 +23,6 @@ const KIND_LABEL: Record<JobKind, string> = {
|
||||
demo: "Demo agent",
|
||||
visual: "Figure agent",
|
||||
pdf: "PDF reader",
|
||||
// The injection is server-initiated — it has no tool part, so never a
|
||||
// dispatch card. The entry exists only for the exhaustive record.
|
||||
"graph-context": "Graph context",
|
||||
};
|
||||
|
||||
export function DispatchCard({ toolCallId, kind }: { toolCallId: string; kind: JobKind }) {
|
||||
|
||||
+5
-13
@@ -24,7 +24,6 @@ import { Composer } from "@/components/composer";
|
||||
import { DictationBar, useDictation } from "@/components/dictation";
|
||||
import { JobsContext } from "@/components/jobs-context";
|
||||
import { StatsBar } from "@/components/stats-bar";
|
||||
import { GraphContextMarker } from "@/components/graph-context-marker";
|
||||
import { useActivity } from "@/components/activity-dock";
|
||||
import { mergeAdopted, type ChatSettings, type ModelSlotName } from "@/src/core";
|
||||
import { PlanBlock, PlanPopup, latestTree } from "@/components/plan-block";
|
||||
@@ -453,10 +452,10 @@ export function Chat({
|
||||
setTimeout(() => {
|
||||
if (adoptionWindowOpenRef.current) {
|
||||
void adoptAppendOnly();
|
||||
// Turn-start nudge re-fire (accuracy wave): the graph-context
|
||||
// job starts at send — the initial refresh above can race the
|
||||
// server's job-record write; the window's ticks retry it so the
|
||||
// dock always catches the job within the window (≤2s).
|
||||
// Turn-start nudge re-fire (accuracy wave): extraction and
|
||||
// sub-agent jobs start at send — the initial refresh above can
|
||||
// race the server's job-record write; the window's ticks retry
|
||||
// it so the dock always catches the job within the window (≤2s).
|
||||
refreshActivity();
|
||||
}
|
||||
}, ms),
|
||||
@@ -945,14 +944,7 @@ export function Chat({
|
||||
// PROTOCOL" §4). Key fallback: legacy transcripts (pre id-hygiene)
|
||||
// can still hold empty ids — the index keeps keys unique for them.
|
||||
<SubagentSteerCard key={m.id || `m-${mi}`} message={m} />
|
||||
) : m.role === "user" && m.parts.some((p) => p.type === "data-graph-context") ? (
|
||||
// The prior-knowledge injection's delivery (vision-context-injection.md):
|
||||
// a quiet, CLICKABLE line (owner observability ask, 2026-09-15) —
|
||||
// the popover shows which propositions were delivered, in delivery
|
||||
// order; the block itself was for the model (adapter flattening),
|
||||
// the part data is the audit trail this reads from.
|
||||
<GraphContextMarker key={m.id || `m-${mi}`} message={m} />
|
||||
) : m.role === "user" && m.parts.some((p) => p.type === "data-probe-handoff") ? (
|
||||
) : m.role === "user" && m.parts.some((p) => p.type === "data-probe-handoff") ? (
|
||||
// The probe→teach handoff wake (custom-message scheme): quiet
|
||||
// system line, never a user bubble — the wake is scaffolding,
|
||||
// the "Planning" divider marks the seam, and the model-facing
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { BookOpenIcon } from "lucide-react";
|
||||
import {
|
||||
GRAPH_CONTEXT_TOKENS_DEFAULT,
|
||||
GRAPH_CONTEXT_TOKENS_STOPS,
|
||||
type ChatSettings,
|
||||
} from "@/src/core";
|
||||
import { useSettingsSaved } from "@/components/settings-signal";
|
||||
import { useSettingsDialog } from "./settings/settings-dialog.tsx";
|
||||
|
||||
/**
|
||||
* The knowledge-context budget control (vision-context-injection.md) — the
|
||||
* "session data" section's one input, shown where sessions START: the home
|
||||
* (graph) view's new-session dock and the /chat/new draft. It renders in
|
||||
* the StatsBar's LEFT slot — the very place session data (context/cost)
|
||||
* lives once a session exists, which pre-chat is free real estate (just
|
||||
* "—"). Input before output: the panel that will later show what the
|
||||
* session contributed to the graph asks first what the graph will
|
||||
* contribute to the session.
|
||||
*
|
||||
* A DISCRETE 6-position slider (owner, 2026-09-15): off, then powers of four
|
||||
* from 64 — a continuous 0–4096 dial was precision theater (a 128-token
|
||||
* budget is ~2 propositions). The native range input walks positions 0–5
|
||||
* and maps through GRAPH_CONTEXT_TOKENS_STOPS; every stop is a real
|
||||
* decision. Non-stop values from a stale client snap to the nearest stop
|
||||
* for display. The readout shows the FULL number (no k-rounding). The
|
||||
* value rides ChatSettings (the same lifted settings the model picker
|
||||
* uses), so it persists pre-chat via the same deferred PUT. Sessions
|
||||
* never show this control — the injection is one-shot at session start.
|
||||
*/
|
||||
export function GraphContextControl({
|
||||
value,
|
||||
onChange,
|
||||
testId = "graph-context-control",
|
||||
}: {
|
||||
value: number;
|
||||
onChange: (tokens: number) => void;
|
||||
testId?: string;
|
||||
}) {
|
||||
// Wave 6: context injection rides the embedding model — with none
|
||||
// configured (graph.embeddingModel null) the control is inert, so it
|
||||
// grays out and its hint deep-links to the Graph section's picker.
|
||||
// Loading renders the normal control (the searchStatus precedent:
|
||||
// never gray on null/unknown). Every settings save re-reads.
|
||||
const { open: openSettingsSection } = useSettingsDialog();
|
||||
const [modelUnset, setModelUnset] = useState(false);
|
||||
const [known, setKnown] = useState(false);
|
||||
const readModel = useCallback(async () => {
|
||||
try {
|
||||
const resp = await fetch("/api/settings");
|
||||
if (!resp.ok) return;
|
||||
const body = (await resp.json()) as { settings?: { graph?: { embeddingModel?: string | null } } };
|
||||
setModelUnset(!body.settings?.graph?.embeddingModel);
|
||||
setKnown(true);
|
||||
} catch {
|
||||
// leave the last known state
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
void readModel();
|
||||
}, [readModel]);
|
||||
useSettingsSaved(() => {
|
||||
void readModel();
|
||||
});
|
||||
|
||||
if (known && modelUnset) {
|
||||
return (
|
||||
<div
|
||||
data-testid={`${testId}-needs-model`}
|
||||
className="flex items-center gap-2 text-xs text-muted-foreground opacity-40"
|
||||
>
|
||||
<BookOpenIcon className="size-3 shrink-0" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openSettingsSection("graph")}
|
||||
className="underline underline-offset-2 transition-colors hover:text-foreground"
|
||||
>
|
||||
Graph context needs an embedding model
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Snap for display only: a legacy non-stop value shows its nearest stop
|
||||
// and the next drag lands on a real one.
|
||||
const stop = nearestStop(value);
|
||||
// indexOf on a readonly tuple rejects a plain number; the snap above
|
||||
// always yields a member, so the cast is honest.
|
||||
const index = (GRAPH_CONTEXT_TOKENS_STOPS as readonly number[]).indexOf(stop);
|
||||
return (
|
||||
<div data-testid={testId} className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<BookOpenIcon className="size-3 shrink-0" />
|
||||
<span className="shrink-0">Graph context</span>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={GRAPH_CONTEXT_TOKENS_STOPS.length - 1}
|
||||
step={1}
|
||||
value={index}
|
||||
onChange={(e) => {
|
||||
const next = (GRAPH_CONTEXT_TOKENS_STOPS as readonly number[])[Number(e.target.value)];
|
||||
if (next !== undefined) onChange(next);
|
||||
}}
|
||||
aria-label="Prior-knowledge token budget"
|
||||
className="h-1 w-24 shrink-0 accent-primary"
|
||||
data-testid="graph-context-slider"
|
||||
/>
|
||||
{/* Content-sized (owner, fix wave): the control hugs its value —
|
||||
no fixed-width readout means no dead gap when "off", and the
|
||||
dictation strip in the stats bar's middle always fills whatever
|
||||
space the slider leaves. tabular-nums keeps the digits steady. */}
|
||||
<span
|
||||
className="shrink-0 tabular-nums"
|
||||
data-testid="graph-context-value"
|
||||
>
|
||||
{value === 0 ? "off" : `${value} tok`}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** The nearest stop to any budget value (display snapping). */
|
||||
export function nearestStop(value: number): number {
|
||||
let best: number = GRAPH_CONTEXT_TOKENS_STOPS[0] ?? 0;
|
||||
for (const s of GRAPH_CONTEXT_TOKENS_STOPS) {
|
||||
if (Math.abs(s - value) < Math.abs(best - value)) best = s;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/** The control's effective value for a chat's settings — absent = the
|
||||
* default (on), never 0: the decided default is a modest slice of prior
|
||||
* knowledge for every fresh session. */
|
||||
export function effectiveContextTokens(
|
||||
settings: Pick<ChatSettings, "graphContextTokens"> | undefined,
|
||||
): number {
|
||||
return settings?.graphContextTokens ?? GRAPH_CONTEXT_TOKENS_DEFAULT;
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { BookOpenIcon } from "lucide-react";
|
||||
import { graphContextOfMessage, type GraphContextData } from "@/src/core";
|
||||
import { formatTokens } from "@/components/usage-format";
|
||||
|
||||
/**
|
||||
* The knowledge-context delivery marker (vision-context-injection.md): the
|
||||
* quiet transcript line for the injected prior-knowledge block — CLICKABLE
|
||||
* (owner observability ask, 2026-09-15): the popover shows exactly which
|
||||
* propositions were delivered, IN DELIVERY ORDER (most similar first —
|
||||
* the order the selection filled the budget), each with its folded current
|
||||
* understanding level. The record part is the audit trail; this makes it
|
||||
* readable at the seam where it landed.
|
||||
*
|
||||
* Same quiet-line discipline as the SystemLine family (centered, muted,
|
||||
* small); same popover discipline as the stats-detail (close on outside
|
||||
* pointerdown + Escape, anchored below the marker).
|
||||
*/
|
||||
export function GraphContextMarker({
|
||||
message,
|
||||
testId = "graph-context",
|
||||
}: {
|
||||
/** The steer message (marker-only user message). */
|
||||
message: { parts: ReadonlyArray<{ type: string; data?: unknown }> };
|
||||
testId?: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const data = graphContextOfMessage(message);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onPointerDown = (e: PointerEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
||||
};
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") setOpen(false);
|
||||
};
|
||||
document.addEventListener("pointerdown", onPointerDown);
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", onPointerDown);
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const summary = data
|
||||
? `Graph context — ~${formatTokens(data.tokens)} · ${data.propositions.length} propositions`
|
||||
: "Graph context";
|
||||
|
||||
return (
|
||||
<div ref={ref} data-testid={testId} className="relative flex items-center justify-center py-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
aria-expanded={open}
|
||||
title="What was delivered — the propositions, in delivery order"
|
||||
className="flex items-center justify-center gap-1.5 text-xs text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
<BookOpenIcon className="size-3" />
|
||||
{summary}
|
||||
</button>
|
||||
{open && data && <GraphContextList data={data} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** The popover body — exported for render tests. Delivery order preserved. */
|
||||
export function GraphContextList({ data }: { data: GraphContextData }): ReactNode {
|
||||
return (
|
||||
<div
|
||||
data-testid="graph-context-list"
|
||||
className="absolute top-full left-1/2 z-40 mt-1.5 w-[36rem] max-w-[80vw] -translate-x-1/2 rounded-xl border bg-card p-3 text-xs shadow-xl"
|
||||
>
|
||||
<div className="mb-1 flex items-baseline justify-between gap-2">
|
||||
<span className="font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Prior knowledge delivered
|
||||
</span>
|
||||
<span className="tabular-nums text-muted-foreground">
|
||||
~{formatTokens(data.tokens)} tok of {data.budget} budget
|
||||
</span>
|
||||
</div>
|
||||
<p className="mb-2 text-muted-foreground">
|
||||
Most similar first — the order the budget filled. The tags are your current
|
||||
understanding level for each proposition.
|
||||
</p>
|
||||
<ol className="flex max-h-72 flex-col gap-1.5 overflow-y-auto">
|
||||
{data.propositions.map((p, i) => (
|
||||
<li key={p.id} className="flex items-baseline gap-2">
|
||||
<span className="shrink-0 tabular-nums text-muted-foreground">{i + 1}.</span>
|
||||
{p.level && (
|
||||
<span className="shrink-0 rounded bg-foreground/10 px-1 py-0.5 text-xs text-muted-foreground">
|
||||
{p.level}
|
||||
</span>
|
||||
)}
|
||||
<span className="min-w-0 flex-1">{p.statement}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { CornerDownLeftIcon } from "lucide-react";
|
||||
import { GraphView } from "@/components/graph-view";
|
||||
import { SetupDialog } from "@/components/setup-dialog";
|
||||
import { useSettingsDialog } from "@/components/settings/settings-dialog.tsx";
|
||||
import { GraphContextControl, effectiveContextTokens } from "@/components/graph-context-control";
|
||||
import { useDefaultSettings } from "@/components/default-settings";
|
||||
import { DictationBar, useDictation } from "@/components/dictation";
|
||||
import { SidebarToggle } from "@/components/sidebar";
|
||||
@@ -172,12 +171,6 @@ export function Home({ initialSettings, initialConfigured }: { initialSettings:
|
||||
<StatsBar
|
||||
dictationSlot={<DictationBar dictation={dictation} />}
|
||||
turns={[] as TurnUsage[]}
|
||||
leftSlot={
|
||||
<GraphContextControl
|
||||
value={effectiveContextTokens(settings)}
|
||||
onChange={(tokens) => saveDraft({ ...settings, graphContextTokens: tokens })}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useRouter } from "next/navigation";
|
||||
import { Conversation, ConversationContent, ConversationEmptyState } from "@/components/ai-elements/conversation";
|
||||
import { Composer } from "@/components/composer";
|
||||
import { DictationBar, useDictation } from "@/components/dictation";
|
||||
import { GraphContextControl, effectiveContextTokens } from "@/components/graph-context-control";
|
||||
import { useDefaultSettings } from "@/components/default-settings";
|
||||
import { PhaseChip } from "@/components/phase-divider";
|
||||
import { SidebarToggle } from "@/components/sidebar";
|
||||
@@ -131,17 +130,6 @@ export function DraftShell({
|
||||
<StatsBar
|
||||
dictationSlot={<DictationBar dictation={dictation} />}
|
||||
turns={[]}
|
||||
// The session-data slot (owner, 2026-09-15): pre-chat the left
|
||||
// side is free — the knowledge-context budget lives exactly
|
||||
// where session data will sit once a session exists.
|
||||
leftSlot={
|
||||
<GraphContextControl
|
||||
value={effectiveContextTokens(settings)}
|
||||
onChange={(tokens) =>
|
||||
onSettingsSaved({ ...settings, graphContextTokens: tokens })
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user