From a1596556dfac8bfee5046ae7b120cedafb429ead Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Mon, 5 Oct 2026 06:49:15 +0300 Subject: [PATCH] =?UTF-8?q?practice:=20the=20rarely-needed=20set=20?= =?UTF-8?q?=E2=80=94=20files,=20research,=20pdf,=20map=20readers=20(round?= =?UTF-8?q?=2010=20slice=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/agent/prompts/practice/00-base.md | 2 ++ src/agent/tools/practice.ts | 27 +++++++++++++++++++++++++++ src/server/chat-handler.ts | 11 +++++++++++ tests/practice-agents.test.ts | 21 +++++++++++++++++++-- 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/agent/prompts/practice/00-base.md b/src/agent/prompts/practice/00-base.md index cdad4bc..a8e1074 100644 --- a/src/agent/prompts/practice/00-base.md +++ b/src/agent/prompts/practice/00-base.md @@ -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. diff --git a/src/agent/tools/practice.ts b/src/agent/tools/practice.ts index e5e6f18..32e0cc5 100644 --- a/src/agent/tools/practice.ts +++ b/src/agent/tools/practice.ts @@ -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>; + /** 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> { // 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) : {}), }; } diff --git a/src/server/chat-handler.ts b/src/server/chat-handler.ts index 363fdba..2fa44f7 100644 --- a/src/server/chat-handler.ts +++ b/src/server/chat-handler.ts @@ -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, ) diff --git a/tests/practice-agents.test.ts b/tests/practice-agents.test.ts index 816aa18..b6eff12 100644 --- a/tests/practice-agents.test.ts +++ b/tests/practice-agents.test.ts @@ -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", () => {