feat(tools): the P2 toggle bridge — toolToggles first-class, definitive vocabulary

Reconciliation of the diverged tool-toggles seam (2026-09-19, master's
P2): Platform injected the resolved delta as an ad-hoc intersection on
AgentBuildOptions while the factories' enabled? param sat unread — the
seam was inert on both sides.

- teacher-agent: AgentBuildOptions gains toolToggles?: EnabledTools;
  both constructors thread it to the factories (createTeacherTools /
  createProberTools). An explicitly injected opts.tools registry is
  never filtered — the caller owns it.
- Vocabulary ADJUDICATED with the user (2026-09-19): TOGGLEABLE_TOOLS is
  the definitive list — the job tools only (research/demo/visual/pdf).
  quiz/ask_user_question stay always-on forever (the prober's flow is
  graded-quiz brackets -> probe_done; quiz-off collapses assessment and
  teaching doctrine is built on graded verification — the wedge danger);
  the learner-fs set, plan tools, and probe_done also always-on.
- settings-body (write side): unknown tool keys reject with 400 + the
  supported list, mirroring the prompt-switch validation; the read-side
  sanitizer stays lenient for forward-compat.
- chat-handler: the intersection is gone (field now first-class) and the
  seam comment states the adjudicated vocabulary.

Gates: typecheck, 28 new/updated tests green (tool-toggles-bridge,
question-tools, settings-body), chat-flow + settings suites green.
This commit is contained in:
amosblomqvist
2026-09-19 18:16:17 +03:00
parent cb4fe25c89
commit 6c42393e42
6 changed files with 132 additions and 24 deletions
+12 -4
View File
@@ -15,7 +15,7 @@ import {
} from "./tools.ts";
import { toPiTools, type LhTool } from "./lh-tool.ts";
import { quizInputObject, quizInputProblems } from "../core/question-core.ts";
import { QUESTION_TOOLS } from "../core/pending.ts";
import { QUESTION_TOOLS, type EnabledTools } from "../core/pending.ts";
/**
* The two teaching-side agents (docs/probe-teach-split.md): the PROBER and
@@ -88,8 +88,15 @@ export interface AgentBuildOptions {
onUsage?: StepUsageCollector;
/** Safety net (the old stepCountIs). Default 20 turns. */
maxTurns?: number;
/** Tests may pass a bare tool registry to drive deterministic flows. */
/** Tests may pass a bare tool registry to drive deterministic flows.
* An explicit registry is NEVER toggle-filtered — the caller owns it. */
tools?: Record<string, LhTool<any, any>>;
/** The resolved per-user tool toggles (core/pending EnabledTools — the
* 2026-09-19 P2 bridge: Platform's chat-handler passes
* effective.agent.tools; the factories drop any tool explicitly false.
* THE DEFINITIVE toggleable set is TOGGLEABLE_TOOLS (job tools only);
* the filtering is generic, but writes validate against the list). */
toolToggles?: EnabledTools;
}
/**
@@ -159,7 +166,7 @@ function baseAgentOptions(kind: AgentKind, opts: AgentBuildOptions, tools: Agent
* handler keys teacher construction on.
*/
export function createProberAgent(ctx: ProbeDoneContext, opts: AgentBuildOptions = {}) {
return baseAgentOptions("prober", opts, toPiTools(createProberTools(ctx)));
return baseAgentOptions("prober", opts, toPiTools(createProberTools(ctx, opts.toolToggles)));
}
/**
@@ -175,7 +182,8 @@ export function createTeacherAgent(
ctx?: TeacherToolContext,
opts: AgentBuildOptions = {},
) {
const tools = opts.tools ?? (ctx ? createTeacherTools(ctx) : bareQuestionTools());
const tools =
opts.tools ?? (ctx ? createTeacherTools(ctx, opts.toolToggles) : bareQuestionTools());
return baseAgentOptions("teacher", opts, toPiTools(tools));
}