fix(tools): wire the searchAvailable gate + research/pdf in the probe phase

Fix wave (2026-09-20), two owner-approved fixes:

Fix 1 — the searchAvailable gate was dead code on the live wiring path:
createTeacherTools RECEIVED ctx.searchAvailable but dropped it when
building the inner JobToolContext, so jobTools' research gate always took
its ?? true default (jobTools-direct tests passed; the production path
never saw the signal). Threaded through — the gate now reads the live
server signal on the teacher registry too.

Fix 2 — the prober registry gains the research + pdf dispatch tools
(owner ruling, 2026-09-20): these two are phase-omnipresent — 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 side: toggle-filtered, research subject to the search gate
(honestly absent when no engine is live — the prober's dispatch closes
over the SAME jobTools builder, one gate, one semantics). probe_done
stays always-on-by-construction; the learner-fs set untouched.

- ProberToolContext (extends ProbeDoneContext with the job wiring);
  createProberAgent takes it; chat-handler injects jobStore + spawn +
  searchAvailable into the prober ctx.
- TeacherToolContext.spawn's stale input union fixed (PdfInput was
  missing from the TYPE — runtime already passed it).
- Tests: the threaded-gate describe in web-search.test.ts pins the LIVE
  factory path both ways (teacher + prober); question-tools' registry
  key-sets gain research/pdf; the prober toggle test pins the
  research:false path.

Gates: typecheck, 807/807 tests, lint, build — all green.
This commit is contained in:
amosblomqvist
2026-09-22 16:22:57 +03:00
parent 57dd25993c
commit d85d8a58a0
6 changed files with 135 additions and 14 deletions
+2 -1
View File
@@ -10,6 +10,7 @@ import {
createProberTools,
createTeacherTools,
type ProbeDoneContext,
type ProberToolContext,
type TeacherToolContext,
} from "./tools.ts";
import { toPiTools, type LhTool } from "./lh-tool.ts";
@@ -181,7 +182,7 @@ function baseAgentOptions(kind: AgentKind, opts: AgentBuildOptions, tools: Agent
* handoff wake (see tools/probe.ts) and is the transcript seam the
* handler keys teacher construction on.
*/
export function createProberAgent(ctx: ProbeDoneContext, opts: AgentBuildOptions = {}) {
export function createProberAgent(ctx: ProberToolContext, opts: AgentBuildOptions = {}) {
return baseAgentOptions("prober", opts, toPiTools(createProberTools(ctx, opts.toolToggles)));
}
+40 -5
View File
@@ -8,7 +8,7 @@ import {
import { filterEnabledTools, type EnabledTools } from "../core/pending.ts";
import type { JobRecord } from "../core/contracts.ts";
import type { JobStore } from "../core/ports.ts";
import { jobTools, type DemoInput, type ResearchInput, type VisualInput } from "./tools/jobs.ts";
import { jobTools, type DemoInput, type PdfInput, type ResearchInput, type VisualInput } from "./tools/jobs.ts";
import { planTools, type PlanToolContext } from "./tools/plan.ts";
import { probeDoneTool, type ProbeDoneContext } from "./tools/probe.ts";
import { readTool } from "./tools/read.ts";
@@ -67,18 +67,40 @@ export const probeQuizTool: LhTool<any, any> = {
schema: probeQuizInputSchema,
};
/** The prober's tool set: probe quiz (no nodeId) + ask + probe_done.
/** 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;
}
/** The prober's tool set: probe quiz (no nodeId) + ask + the learner-fs
* set + probe_done + the two phase-omnipresent job dispatch tools.
* `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. */
export function createProberTools(
ctx: ProbeDoneContext,
ctx: ProberToolContext,
enabled?: EnabledTools,
): Record<string, LhTool<any, any>> {
// 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);
// 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,
});
return {
...filterEnabledTools(
{
@@ -91,6 +113,15 @@ export function createProberTools(
ls: lsTool,
find: findTool,
grep: grepTool,
// 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!,
...done,
},
enabled,
@@ -153,7 +184,7 @@ export interface TeacherToolContext extends PlanToolContext {
/** 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). */
spawn: (job: JobRecord, input: ResearchInput | DemoInput | VisualInput) => void;
spawn: (job: JobRecord, input: ResearchInput | DemoInput | VisualInput | PdfInput) => void;
/** 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). */
@@ -188,7 +219,11 @@ export function createTeacherTools(
find: findTool,
grep: grepTool,
...planTools(ctx),
...jobTools({ chatId: ctx.chatId, jobStore: ctx.jobStore, spawn: ctx.spawn }),
// 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 }),
},
enabled,
);