practice: the rarely-needed set — files, research, pdf, map readers (round 10 slice 1)

The practice registry widens beyond problem+conclude: read/ls/find/grep
(the learner-filesystem set, always), research + pdf jobs (research
honestly absent when no engine is live; demo/visual stay teach-only),
and the prober's map readers behind the SAME settings gate as the
prober's (graphToolsEnabled at the composition root). Base prompt
carries the conditional doctrine: reach for them only when the
situation genuinely requires — no propositions → look at the map;
learner references a file → read it; load-bearing uncertainty →
research it. Not toggle-filtered: problem/conclude are the session,
these are exceptions.
This commit is contained in:
amosblomqvist
2026-10-05 06:49:15 +03:00
parent 83dad65481
commit a1596556df
4 changed files with 59 additions and 2 deletions
+2
View File
@@ -33,3 +33,5 @@ Only one problem may be active at a time — the tool rejects a pose while one i
**`conclude`** — ends the active problem: records the outcome and verdict. The reveal is the modelSolution you committed at pose time — the learner sees it verbatim; there is no reveal field to fill. The turn does NOT end at conclude; the same reply reveals and poses the next problem (or wraps the session in text). The tool rejects a conclude with nothing active.
**Dismissals.** The learner may discard a problem without concluding it. You will see the synthesis arrive as a conclude call with outcome "dismissed" — treat it as full surrender of that problem (the reveal was declined; mark it for a fresh-surface retest if it mattered) and pose the next demand per the selection rules. Do not comment on the dismissal beyond what the stream needs.
**The rarely-needed set.** Beyond the two tools sit the learner's files (read, ls, find, grep), the research and pdf jobs, and the map readers (list_sessions, list_propositions). Reach for them only when the situation genuinely requires: the session opened without picked propositions and targets must be chosen — look at the map; the learner references a file — read it; a load-bearing fact is uncertain — research it. These are exceptions, not habits — the session's own material and its proposition list come first.
+27
View File
@@ -17,6 +17,12 @@
import { Type, type Static } from "typebox";
import type { AgentTool } from "@earendil-works/pi-agent-core";
import { resolveTargetId } from "../../core/chains.ts";
import { readTool } from "./read.ts";
import { lsTool } from "./ls.ts";
import { findTool } from "./find.ts";
import { grepTool } from "./grep.ts";
import { jobTools, type JobToolContext } from "./jobs.ts";
import { proberGraphTools, type GraphToolContext } from "./graph.ts";
import { activeProblemOf } from "../../core/native-messages.ts";
import type { LhMessage } from "../../core/native-messages.ts";
import { concludeResult, invalidInputResult, problemResult } from "../tool-execute.ts";
@@ -115,6 +121,12 @@ export interface PracticeToolContext {
* not just the session's picks, so pickless sessions and future
* graph-tools poses stay valid. Absent (tests) = no validation. */
graphNodes?: () => ReadonlyArray<Pick<import("../../core/contracts/graph.ts").GraphNode, "id" | "name">>;
/** THE RARELY-NEEDED SET (owner round 10, 2026-10-05): the map readers
* behind the SAME settings gate as the prober's (graphToolsEnabled at
* the composition root); the research + pdf jobs with the honest
* searchAvailable drop. Each absent (tests) = absent from the registry. */
graph?: GraphToolContext;
jobs?: JobToolContext;
}
/** TURN-SCOPED GATE STATE (the BKT incident, 2026-10-05): the gates used
@@ -276,8 +288,23 @@ export function concludeTool(gate: PracticeGate): AgentTool<typeof concludeInput
export function practiceTools(ctx: PracticeToolContext): Record<string, AgentTool<any>> {
// ONE gate per turn, shared by both tools — the intra-turn truth.
const gate = createPracticeGate(ctx);
// THE RARELY-NEEDED SET (owner round 10): read/ls/find/grep over the
// learner's files (always — the same read-only set the teacher and
// prober carry), the research + pdf jobs (research honestly absent
// when no search engine is live; demo/visual stay teach-only), and
// the prober's map readers when the settings gate opened. None are
// toggle-filtered: problem/conclude are the session; these are
// exceptions the doctrine says to reach for rarely.
const jobs = ctx.jobs ? jobTools(ctx.jobs) : null;
return {
problem: problemTool(gate, ctx.graphNodes),
conclude: concludeTool(gate),
read: readTool,
ls: lsTool,
find: findTool,
grep: grepTool,
...(jobs?.research ? { research: jobs.research } : {}),
...(jobs ? { pdf: jobs.pdf! } : {}),
...(ctx.graph ? proberGraphTools(ctx.graph) : {}),
};
}
+11
View File
@@ -337,6 +337,17 @@ export async function handleChatInput(
// proposition, not just the session's picks (pickless sessions
// and future graph-tools poses stay valid).
graphNodes: () => stores.graphStore.readNodes(),
// THE RARELY-NEEDED SET (owner round 10): research + pdf jobs
// (same runner/gating as the teacher's — research honestly
// absent when no engine is live) and the map readers behind
// the SAME settings gate as the prober's.
jobs: {
chatId,
jobStore: stores.jobStore,
spawn: (job, input) => void jobRunner.runJob(job.id, input),
searchAvailable,
},
...(graphToolsEnabled(effective) ? { graph: { chatStore: stores.chatStore, graphStore: stores.graphStore } } : {}),
},
agentOpts,
)
+19 -2
View File
@@ -182,9 +182,26 @@ describe("target validation — names normalize to ids, unknowns reject", () =>
// ── the registry + the agent ────────────────────────────────────────────────
describe("the practice agent", () => {
it("registry is problem + conclude ONLY — no question, plan, or job tools", () => {
it("bare registry: the session pair + the learner-filesystem set — no question, plan, or lesson tools", () => {
const tools = practiceTools(ctxOf([]));
assert.deepEqual(Object.keys(tools).sort(), ["conclude", "problem"]);
assert.deepEqual(
Object.keys(tools).sort(),
["conclude", "find", "grep", "ls", "problem", "read"],
);
});
it("THE RARELY-NEEDED SET (owner round 10): jobs + map readers compose in when their roots are passed", () => {
const jobs = { chatId: "c1", jobStore: null as never, spawn: (() => {}) as never, searchAvailable: () => false };
const graph = { chatStore: null as never, graphStore: null as never };
const tools = practiceTools({ ...ctxOf([]), jobs, graph });
// research honestly absent (no search engine live); pdf always; the
// prober's two map readers behind the settings gate.
assert.deepEqual(
Object.keys(tools).sort(),
["conclude", "find", "grep", "list_propositions", "list_sessions", "ls", "pdf", "problem", "read"],
);
const withSearch = practiceTools({ ...ctxOf([]), jobs: { ...jobs, searchAvailable: () => true }, graph });
assert.ok(withSearch.research, "a live engine adds research");
});
it("constructs with exactly the two practice tools", () => {