2026-09-25 23:32:13 +03:00
import { Type } from "typebox" ;
import type { AgentTool } from "@earendil-works/pi-agent-core" ;
2026-10-04 20:21:10 +03:00
import { invalidInputResult , questionResult , serverExecute } from "./tool-execute.ts" ;
2026-09-11 06:44:20 +03:00
import {
askInputSchema ,
2026-09-25 23:32:13 +03:00
askOptionProblems ,
2026-09-27 08:55:13 +03:00
explainInputBase ,
explainInputObject ,
explainInputProblems ,
2026-09-26 19:08:39 +03:00
parseOptionsIfString ,
2026-09-11 06:44:20 +03:00
probeQuizInputSchema ,
quizInputObject ,
quizInputProblems ,
} from "../core/question-core.ts" ;
2026-09-19 14:06:07 +03:00
import { filterEnabledTools , type EnabledTools } from "../core/pending.ts" ;
2026-09-01 19:15:43 +03:00
import type { JobRecord } from "../core/contracts.ts" ;
import type { JobStore } from "../core/ports.ts" ;
2026-09-22 16:22:57 +03:00
import { jobTools , type DemoInput , type PdfInput , type ResearchInput , type VisualInput } from "./tools/jobs.ts" ;
2026-09-01 19:15:43 +03:00
import { planTools , type PlanToolContext } from "./tools/plan.ts" ;
2026-09-11 06:44:20 +03:00
import { probeDoneTool , type ProbeDoneContext } from "./tools/probe.ts" ;
2026-09-23 15:24:48 +03:00
import { proberGraphTools } from "./tools/graph.ts" ;
2026-09-19 15:24:26 +03:00
import { readTool } from "./tools/read.ts" ;
2026-09-19 16:50:20 +03:00
import { lsTool } from "./tools/ls.ts" ;
import { findTool } from "./tools/find.ts" ;
import { grepTool } from "./tools/grep.ts" ;
2026-10-09 04:54:34 +03:00
import { healToolRecord } from "./tools/heal.ts" ;
2026-09-23 15:24:48 +03:00
import type { GraphStore } from "../core/ports.ts" ;
2026-08-31 22:16:03 +03:00
export { jobTools };
2026-09-11 06:44:20 +03:00
export type { ProbeDoneContext } from "./tools/probe.ts" ;
2026-08-22 02:18:27 +03:00
/**
2026-09-11 06:44:20 +03:00
* The agent-split tool composition (docs/probe-teach-split.md §1): the
* PROBER and the TEACHER are different agents with different tool sets.
* The question tools share a NAME but are genuinely DIFFERENT tools —
2026-09-25 23:32:13 +03:00
* the prober's quiz has NO nodeId field at all (its declaration is built
* from quizInputBase; the model's schema never mentions nodeId), the
2026-09-11 06:44:20 +03:00
* teacher's carries the validated position field.
2026-08-22 02:18:27 +03:00
*
2026-09-25 23:32:13 +03:00
* - Question tools (quiz / ask_user_question): CLIENT-ANSWERED. Their
* execute validates the authoring refinements (the cross-field rules no
* schema carries — quizInputProblems / askOptionProblems; a failure
* throws ToolInputError, which pi's loop converts to an error tool
* result the model retries on), then returns the synthetic
* "question posed" placeholder (questionResult: terminate: true — the
* turn ends; the call streams to the browser in `input-available`
* state; the learner answers in the dock; the client grades and sends
* the verdict with addToolOutput). Input schemas live in
* core/question-core. Question tools opt into SEQUENTIAL execution so
* parallel calls resolve in the model's call order (the dock's subject
* can never flip mid-turn).
2026-09-11 06:44:20 +03:00
* - probe_done (src/agent/tools/probe.ts): SERVER-SIDE seam tool.
* - planTools (src/agent/tools/plan.ts): SERVER-SIDE — author-once
* plan_init. Teacher only.
* - jobTools (src/agent/tools/jobs.ts): SERVER-EXECUTED dispatch. Teacher
* only.
2026-08-22 02:18:27 +03:00
*/
2026-09-26 19:08:39 +03:00
/** GLM wobble guard (owner-approved, 2026-09-26): models sometimes emit
* `options` as a JSON-encoded string instead of an array; pi runs this
* BEFORE schema validation, so a double-encoded call lands instead of
* bouncing back as a validation error + wasted retry turn. NOTE: pi
* persists the tool call with its RAW arguments — the client coerces
* again in normalizeOptions (question-core), the same parseOptionsIfString.
* Anything unparseable passes through untouched: validation then reports
* honestly and the agent retries with a real error to learn from. */
function prepareQuestionArguments < T extends object >( args : unknown ) : T {
if ( args && typeof args === "object" && ! Array . isArray ( args )) {
const o = args as { options? : unknown };
if ( typeof o . options === "string" ) {
const parsed = parseOptionsIfString ( o . options );
if ( parsed !== o . options ) return { ... o , options : parsed } as T ;
}
return o as T ;
}
return args as T ;
}
2026-09-25 23:32:13 +03:00
export const askUserQuestionTool : AgentTool < typeof askInputSchema > = {
2026-09-12 02:34:18 +03:00
name : "ask_user_question" ,
2026-09-25 23:32:13 +03:00
label : "ask_user_question" ,
2026-08-22 02:18:27 +03:00
description :
"Ask the learner a single question and pause until they answer. NOT graded — there is no correct answer. " +
"Use when requirements are ambiguous, preferences are needed, a decision would materially affect the lesson, " +
2026-09-25 20:51:45 +03:00
"or you need an open-ended response. ONE question per turn: call a single ask_user_question, then end your " +
"turn — his answer determines the next." ,
2026-09-25 23:32:13 +03:00
parameters : askInputSchema ,
2026-09-26 19:08:39 +03:00
prepareArguments : prepareQuestionArguments ,
2026-09-25 23:32:13 +03:00
executionMode : "sequential" ,
execute : async ( _toolCallId , input ) => {
const problems = askOptionProblems ( input . options ?? []);
2026-10-04 20:21:10 +03:00
if ( problems . length > 0 ) return invalidInputResult ( "ask_user_question" , problems );
2026-09-25 23:32:13 +03:00
return questionResult ();
},
2026-09-12 02:34:18 +03:00
};
2026-08-22 02:18:27 +03:00
2026-10-04 00:53:29 +03:00
// ─── the question tools ───────────────────────────────────────────────────────
//
// MECHANICAL DESCRIPTIONS ONLY (owner 2026-10-03): a tool's description
// carries what the tool DOES and how its fields work — nothing about what
// the questions should BE. The craft (option-building, free-recall
// grading, question roles) lives in the system prompt — a single-sourced
// chunk both agents compose (prompts/shared/20-question-tools.md and the
// teacher's question fragments), reversing the 2026-09-27 placement now
// that the question family has grown (predict/check roles, future
2026-10-04 21:34:24 +03:00
// quiz variants): duplicates stay honest by sharing SCHEMA ATOMS
2026-10-04 00:53:29 +03:00
// (question-core) + PROMPT CHUNKS, and each tool declares only its
// mechanical delta.
2026-08-22 02:18:27 +03:00
2026-10-04 00:53:29 +03:00
/** The quiz mechanics every variant shares; `delta` appends the variant's
2026-10-09 07:59:23 +03:00
* own fields (its NAME among them — the probe-phase tools self-reference
* as probe/recall, owner 2026-10-09). All mechanics, zero doctrine. */
function quizDescription ( delta : { name : "quiz" | "probe" ; nodeId : boolean ; types : boolean }) : string {
2026-10-04 00:53:29 +03:00
return (
"Ask the learner a GRADED question with a known correct answer, then instantly grade and give feedback. " +
2026-10-09 07:59:23 +03:00
"Unlike ask_user_question (which collects preferences/decisions with no right answer), " +
delta . name +
" always has a " +
2026-10-04 00:53:29 +03:00
"correct answer supplied by you, marks the learner's selection right/wrong, reveals the correct answer, and " +
"can show an explanation. Options-only: single-select or multi-select, plus an automatic 'I don't know' " +
"choice so the learner can signal a genuine gap instead of guessing. An always-present optional thinking " +
"field lets the learner attach his reasoning to ANY answer; it reaches you only when non-empty. " +
"No free-text answers — for non-graded questions use ask_user_question instead. " +
( delta . types
? "Every call declares `type` — the question's role in the teaching cycle; the field's own description says when each kind fires. "
: "" ) +
( delta . nodeId
? "Set `nodeId` (required) to the plan node this question is about — validated at call time against the session's plan map. For off-map checks use ask_user_question. "
: "" ) +
2026-10-09 07:59:23 +03:00
"ONE question per turn: call a single " +
delta . name +
", then end your turn — his answer determines the next."
2026-10-04 00:53:29 +03:00
);
}
2026-09-27 08:55:13 +03:00
2026-10-04 00:53:29 +03:00
/** The explain mechanics (free-recall quizzing): the commitment rule is
* the GRADING CONTRACT — mechanics, not doctrine. The softer doctrine
2026-10-09 07:59:23 +03:00
* (near-miss reading, genuine gaps) lives in the shared prompt chunk.
* `name`: the teach tool self-references as explain, the probe variant
* as recall (owner 2026-10-09). */
function explainDescription ( name : "explain" | "recall" , nodeId : boolean ) : string {
2026-10-04 00:53:29 +03:00
return (
"Ask the learner to EXPLAIN something in his own words — a graded check with NO options: he types a free-text " +
2026-10-09 07:59:23 +03:00
"answer. Unlike ask_user_question (no right answer), " +
name +
" always has one. THE COMMITMENT RULE: " +
2026-10-04 00:53:29 +03:00
"correctAnswer is the FULL correct answer in your own words, reasoning included, COMMITTED before he ever " +
"sees the question. When his answer returns, grade it strictly against the commitment — never revise it " +
"after seeing his words. He sees the committed answer on submit. " +
( nodeId
? "Set `nodeId` (required) to the plan node this question is about — validated at call time against the session's plan map. "
: "" ) +
2026-10-09 07:59:23 +03:00
"ONE question per turn: call a single " + name + ", then end your turn — his answer determines the next."
2026-10-04 00:53:29 +03:00
);
}
2026-09-27 08:55:13 +03:00
2026-10-09 07:59:23 +03:00
/** The prober's multiple-choice quizzing tool — named `probe`, not
* `quiz` (owner, 2026-10-09): the probe and teach schemas differ
* (neither `type` nor `nodeId` exists while probing), and a shared name
* invited the model to carry its probe-phase call shape into teaching
* — the first teach quiz after plan_init dropped `nodeId` on exactly
* that habit (the tools_fails_2 incident). Distinct names make the
* phase boundary visible in the transcript itself: history written by
* `probe` never lies about what `quiz` requires. */
2026-09-25 23:32:13 +03:00
export const probeQuizTool : AgentTool < typeof probeQuizInputSchema > = {
2026-10-09 07:59:23 +03:00
name : "probe" ,
label : "probe" ,
description : quizDescription ({ name : "probe" , nodeId : false , types : false }),
2026-09-25 23:32:13 +03:00
parameters : probeQuizInputSchema ,
2026-09-26 19:08:39 +03:00
prepareArguments : prepareQuestionArguments ,
2026-09-25 23:32:13 +03:00
executionMode : "sequential" ,
execute : async ( _toolCallId , input ) => {
const problems = quizInputProblems ( input );
2026-10-09 07:59:23 +03:00
if ( problems . length > 0 ) return invalidInputResult ( "probe" , problems );
2026-09-25 23:32:13 +03:00
return questionResult ();
},
2026-09-12 02:34:18 +03:00
};
2026-09-11 06:44:20 +03:00
2026-09-27 08:55:13 +03:00
/** The teacher's free-recall variant (nodeId-validated, same seam as the
* teach quiz — phases/plan recognize BOTH tool names). */
export function teachExplainToolFor (
ctx : Pick < PlanToolContext , "chatId" | "chatStore" >,
) : AgentTool < typeof explainInputObject > {
return {
name : "explain" ,
label : "explain" ,
2026-10-09 07:59:23 +03:00
description : explainDescription ( "explain" , true ),
2026-09-27 08:55:13 +03:00
parameters : explainInputObject ,
prepareArguments : prepareQuestionArguments ,
executionMode : "sequential" ,
execute : async ( _toolCallId , input ) => {
const problems = [... explainInputProblems ( input )];
const nodeIdProblemText = nodeIdProblem ( ctx , input . nodeId );
if ( nodeIdProblemText ) problems . push ( nodeIdProblemText );
2026-10-04 20:21:10 +03:00
if ( problems . length > 0 ) return invalidInputResult ( "explain" , problems );
2026-09-27 08:55:13 +03:00
return questionResult ();
},
};
}
/** The prober's free-recall variant (no nodeId — same pattern as the
* probe quiz). */
2026-10-09 07:59:23 +03:00
/** The prober's free-recall variant — named `recall`, not `explain`
* (owner, 2026-10-09), for the same phase-boundary reason as `probe`:
* the teach explain adds `type` and `nodeId`; a shared name carried the
* probe call shape into teaching. Old probe-phase records keep the
* `explain` name and parse identically (the display seam accepts
* both). */
2026-09-27 08:55:13 +03:00
export const probeExplainTool : AgentTool < typeof explainInputBase > = {
2026-10-09 07:59:23 +03:00
name : "recall" ,
label : "recall" ,
description : explainDescription ( "recall" , false ),
2026-09-27 08:55:13 +03:00
parameters : explainInputBase ,
prepareArguments : prepareQuestionArguments ,
executionMode : "sequential" ,
execute : async ( _toolCallId , input ) => {
const problems = explainInputProblems ( input );
2026-10-09 07:59:23 +03:00
if ( problems . length > 0 ) return invalidInputResult ( "recall" , problems );
2026-09-27 08:55:13 +03:00
return questionResult ();
},
};
2026-09-22 16:22:57 +03:00
/** The prober's registry context: the probe's own needs (probe_done —
* ProbeDoneContext) PLUS the job-dispatch wiring, since the owner ruling
* (2026-09-20) puts research + pdf in the probe phase's registry too. */
export interface ProberToolContext extends ProbeDoneContext {
/** Creates the job record (deduped by toolCallId). */
jobStore : JobStore ;
/** Fire the background runner for a freshly created job. */
spawn : ( job : JobRecord , input : ResearchInput | DemoInput | VisualInput | PdfInput ) => void ;
/** Search availability — the research dispatch tool's honest-disable
* signal (same gate as the teacher's). Absent = available. */
searchAvailable ?: () => boolean ;
2026-09-23 15:24:48 +03:00
/** The prober's graph-map reader (wave 7): present = the two map tools
* (list_sessions / list_propositions) are registered. The composition
* root passes it only when graphToolsEnabled(effective) — the graph
* master switch AND the agent.graphTools toggle (settings-resolution).
* Absent = the gate is closed, no map tools (tests drive this seam). */
graphStore? : GraphStore ;
2026-09-22 16:22:57 +03:00
}
/** The prober's tool set: probe quiz (no nodeId) + ask + the learner-fs
2026-09-23 15:24:48 +03:00
* set + probe_done + the two phase-omnipresent job dispatch tools +
* (wave 7, gated) the graph-map reader. `enabled` is the resolved
* settings seam (core/pending.ts): any tool explicitly false is dropped
* from the registry. probe_done stays always-on regardless — it is how
* the phase ENDS; a prober that can never conclude is a lockout. */
2026-09-19 14:06:07 +03:00
export function createProberTools (
2026-09-22 16:22:57 +03:00
ctx : ProberToolContext ,
2026-09-19 14:06:07 +03:00
enabled? : EnabledTools ,
2026-09-25 23:32:13 +03:00
) : Record < string , AgentTool < any >> {
2026-09-19 14:06:07 +03:00
// probe_done is re-added AFTER the filter, always-on by construction — it
// is how the phase ENDS; a prober that can never conclude is a lockout.
const done = probeDoneTool ( ctx );
2026-09-22 16:22:57 +03:00
// The job tools build over the SAME context/gates as the teacher's
// (one builder, one gating semantic); the prober carries a SUBSET:
const jobs = jobTools ({
chatId : ctx.chatId ,
jobStore : ctx.jobStore ,
spawn : ctx.spawn ,
searchAvailable : ctx.searchAvailable ,
});
2026-09-27 08:55:13 +03:00
// Quizzing mechanics (owner 2026-09-27): free-recall swaps the probe
// quiz for the probe explain — one quizzing tool at a time, whichever
2026-10-09 07:59:23 +03:00
// the settings arm. ask stays always-on. The registry keys are the
// PHASE names (probe/recall, owner 2026-10-09) — distinct from the
// teach tools so the schema boundary is visible to the model.
2026-09-27 08:55:13 +03:00
const mechanics = quizMechanics ( enabled );
// The armed quizzing tool, widened to the registry's heterogeneous
// AgentTool record BEFORE the filter (a conditional spread of two
// differently-typed literals makes the whole object's type a union —
// unusable as Record<string, AgentTool<any>> downstream).
const quizzing : Record < string , AgentTool < any >> =
2026-10-09 07:59:23 +03:00
mechanics === "free" ? { recall : probeExplainTool } : { probe : probeQuizTool };
2026-10-09 04:54:34 +03:00
// healToolRecord (owner, 2026-10-09 — the whitehead generalization):
// the standard argument heal over the WHOLE registry — every tool,
// not just the one the latest wobble incident landed on.
return healToolRecord ({
2026-09-19 14:06:07 +03:00
... filterEnabledTools (
{
2026-09-27 08:55:13 +03:00
... quizzing ,
2026-09-19 14:06:07 +03:00
ask_user_question : askUserQuestionTool ,
2026-09-19 16:50:20 +03:00
// The learner-filesystem set is always-on (user-decided 2026-09,
// sources v1): the prober inspects and LOCATES learner-referenced
// files while assessing the edge. Read-only by construction.
2026-09-19 15:24:26 +03:00
read : readTool ,
2026-09-19 16:50:20 +03:00
ls : lsTool ,
find : findTool ,
grep : grepTool ,
2026-09-22 16:22:57 +03:00
// Job dispatch in the probe phase (owner ruling, 2026-09-20):
// research + pdf are ALWAYS available in every phase — the
// prober can verify facts or read learner PDFs while assessing.
// demo/visual stay teach-phase-only (they need the lesson's plan
// context). Same gating as the teacher: toggle-filtered here,
// research subject to the searchAvailable gate (honestly absent
// when no engine is live).
...( jobs . research ? { research : jobs.research } : {}),
pdf : jobs.pdf ! ,
2026-09-23 15:24:48 +03:00
// The map reader (wave 7): present only when the settings gate
// opened — same registration-time drop as the search gate.
...( ctx . graphStore
? proberGraphTools ({ chatStore : ctx.chatStore , graphStore : ctx.graphStore })
: {}),
2026-09-19 14:06:07 +03:00
... done ,
},
2026-10-09 04:54:34 +03:00
withoutMechanics ( enabled ),
),
... done ,
});
2026-09-11 06:44:20 +03:00
}
// ─── the teacher's question tools (quiz carries the position field) ──────────
2026-09-25 23:32:13 +03:00
/** The nodeId-against-live-tree check the teacher's quiz chains after the
* shared authoring contract (the old superRefine, verbatim). */
function nodeIdProblem ( ctx : Pick < PlanToolContext , "chatId" | "chatStore" >, nodeId : string ) : string | null {
const record = ctx . chatStore . getChat ( ctx . chatId );
const known = record ? . tree ? . nodes . map (( n ) => n . id ) ?? [];
if ( ! known . includes ( nodeId )) {
return (
`nodeId " ${ nodeId } " matches no node in this session's plan map. ` +
( known . length > 0
2026-09-27 02:05:51 +03:00
? `Copy one of your plan_init slugs exactly ( ${ known . slice ( 0 , 5 ). join ( ", " ) }${ known . length > 5 ? ", …" : "" } ).`
: "No plan map exists yet — call plan_init before posing quizzes." )
2026-09-25 23:32:13 +03:00
);
}
return null ;
}
2026-09-11 06:44:20 +03:00
/** Build the teach-side quiz tool for one request (exported for tests): the authoring contract
2026-09-25 23:32:13 +03:00
* + the per-session plan-nodeId validation (both run in execute — the
2026-09-11 06:44:20 +03:00
* grading contract is untouched). Reads the LIVE chat record, so rewrites
* mid-turn are honored, and with no tree yet every nodeId is rejected.
2026-09-27 02:05:51 +03:00
* nodeId is REQUIRED (owner ruling 2026-09-26): the untagged ad-hoc quiz is
* gone — off-map checks route through ask_user_question, and the teach
* seam (first tagged quiz) is structurally guaranteed. */
2026-09-25 23:32:13 +03:00
export function teachQuizToolFor ( ctx : Pick < PlanToolContext , "chatId" | "chatStore" >) : AgentTool < typeof quizInputObject > {
return {
2026-09-12 02:34:18 +03:00
name : "quiz" ,
2026-09-25 23:32:13 +03:00
label : "quiz" ,
2026-10-09 07:59:23 +03:00
description : quizDescription ({ name : "quiz" , nodeId : true , types : true }),
2026-09-25 23:32:13 +03:00
parameters : quizInputObject ,
2026-09-26 19:08:39 +03:00
prepareArguments : prepareQuestionArguments ,
2026-09-25 23:32:13 +03:00
executionMode : "sequential" ,
execute : async ( _toolCallId , input ) => {
const problems = [... quizInputProblems ( input )];
2026-09-27 02:05:51 +03:00
const nodeIdProblemText = nodeIdProblem ( ctx , input . nodeId );
if ( nodeIdProblemText ) problems . push ( nodeIdProblemText );
2026-10-04 20:21:10 +03:00
if ( problems . length > 0 ) return invalidInputResult ( "quiz" , problems );
2026-09-25 23:32:13 +03:00
return questionResult ();
},
};
2026-09-09 03:04:16 +03:00
}
2026-09-01 19:15:43 +03:00
export interface TeacherToolContext extends PlanToolContext {
/** Job-tool wiring (sub-agent dispatch): the job store + the runner's
* spawn seam. The composition root injects both. */
jobStore : JobStore ;
/** Fire the background runner for a freshly created job. The tool input
* rides along — the spawn fires mid-stream, before the tool part is
* persisted to the transcript (a transcript scan would race it). */
2026-09-22 16:22:57 +03:00
spawn : ( job : JobRecord , input : ResearchInput | DemoInput | VisualInput | PdfInput ) => void ;
2026-09-22 13:55:01 +03:00
/** Search availability (search port wave, 2026-09-20): the research
* dispatch tool's honest-disable signal — no live engine = the tool
* drops from this registry (JobToolContext.searchAvailable). */
searchAvailable ?: () => boolean ;
2026-08-22 09:06:24 +03:00
}
/**
2026-09-11 06:44:20 +03:00
* Per-request teacher tool registry: teach questions (validated nodeId) +
* plan tools + job tools, all closing over the ctx.
2026-09-19 14:06:07 +03:00
*
* `enabled` is the resolved settings seam (core/pending.ts EnabledTools —
* Platform's chat-handler wiring passes it through from settings). Today
* only the sub-agent tools (research/demo/visual) are user-toggleable, but
* the filtering is GENERIC: any name explicitly false is dropped, so
* future toggleables (question-tool variants, …) need no change here.
* Absent/undefined = enabled — tests and old records see today's set.
2026-08-22 09:06:24 +03:00
*/
2026-09-27 08:55:13 +03:00
/** The quizzing mechanics (owner 2026-09-27): `tools.explain === true` in
* the resolved settings ⇒ free-recall checks (explain armed, quiz dropped)
* — anything else ⇒ multiple choice, exactly today's set. Mechanics-
* managed, NOT individually toggleable: quiz/explain never pass through
* filterEnabledTools (the 2026-09-19 wedge ruling stands — a quiz-less
* prober collapses its assessment flow). */
export function quizMechanics ( enabled? : EnabledTools ) : "choice" | "free" {
return enabled ? . explain === true ? "free" : "choice" ;
}
/** Strip the mechanics keys so filterEnabledTools can never drop the
* quizzing tools (the derivation above is their only switch). */
function withoutMechanics ( enabled? : EnabledTools ) : EnabledTools | undefined {
if ( ! enabled ) return undefined ;
const { explain : _e , quiz : _q , ... rest } = enabled ;
return rest ;
}
2026-09-19 14:06:07 +03:00
export function createTeacherTools (
ctx : TeacherToolContext ,
enabled? : EnabledTools ,
2026-09-25 23:32:13 +03:00
) : Record < string , AgentTool < any >> {
2026-09-27 08:55:13 +03:00
// Quizzing mechanics (owner 2026-09-27): free-recall swaps the teach
// quiz for the teach explain — one quizzing tool at a time, whichever
// the settings arm. ask stays always-on.
const mechanics = quizMechanics ( enabled );
// Same widening as the prober's (see above) — the conditional spread of
// two differently-typed literals collapses into an unusable union type.
const quizzing : Record < string , AgentTool < any >> =
mechanics === "free" ? { explain : teachExplainToolFor ( ctx ) } : { quiz : teachQuizToolFor ( ctx ) };
2026-10-09 04:54:34 +03:00
// healToolRecord — same standard as the prober's registry (above).
return healToolRecord (
filterEnabledTools (
{
2026-09-27 08:55:13 +03:00
... quizzing ,
2026-09-19 14:06:07 +03:00
ask_user_question : askUserQuestionTool ,
2026-09-19 16:50:20 +03:00
// The learner-filesystem set is always-on (user-decided 2026-09,
// sources v1): the teacher locates and reads learner-referenced
// files on its own terms — nothing auto-injects. Read-only by
// construction (fs-shared.ts).
2026-09-19 15:24:26 +03:00
read : readTool ,
2026-09-19 16:50:20 +03:00
ls : lsTool ,
find : findTool ,
grep : grepTool ,
2026-09-19 14:06:07 +03:00
... planTools ( ctx ),
2026-09-22 16:22:57 +03:00
// Fix wave (2026-09-20): searchAvailable was RECEIVED here but
// DROPPED when building the inner JobToolContext — the drop-gate
// always took its ?? true default. Threaded now: the live signal
// reaches jobTools' research gate.
... jobTools ({ chatId : ctx.chatId , jobStore : ctx.jobStore , spawn : ctx.spawn , searchAvailable : ctx.searchAvailable }),
2026-09-19 14:06:07 +03:00
},
2026-09-27 08:55:13 +03:00
withoutMechanics ( enabled ),
2026-10-09 04:54:34 +03:00
),
2026-09-19 14:06:07 +03:00
);
2026-08-22 09:06:24 +03:00
}