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:
@@ -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.
|
||||
|
||||
@@ -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) : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user