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:
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
+14
-9
@@ -32,15 +32,20 @@ export const JOB_TOOLS = ["research", "demo", "visual"] as const;
|
||||
|
||||
// ─── Tool availability (2026-09 settings seam) ─────────────────────────────
|
||||
|
||||
/** The tools a user may toggle off in settings (Wave 2 T2a, user-decided
|
||||
* 2026-09-15): for now only the SUB-AGENT dispatch tools — quiz,
|
||||
* ask_user_question and the read tool are always on;
|
||||
* the long-term vision is that everything becomes toggleable as new
|
||||
* tool variants (e.g. alternative question tools) ship, so the factory
|
||||
* seam below is GENERIC: it honors any tool name it receives. New
|
||||
* toggleables join this list when settings exposes them.
|
||||
* The names are the MODEL-FACING tool names (research, not researcher;
|
||||
* pdf is the PDF-reader dispatch, sources v0 2026-09). */
|
||||
/** The tools a user may toggle off in settings — THE DEFINITIVE LIST
|
||||
* (adjudicated 2026-09-19, P2 reconciliation: the sub-agent JOB tools
|
||||
* only — research/demo/visual/pdf). Deliberately NOT toggleable, ever:
|
||||
* quiz + ask_user_question (the prober's flow is graded-quiz brackets →
|
||||
* probe_done; quiz-off would collapse the assessment and teaching
|
||||
* doctrine is built on graded verification — the wedge danger), the
|
||||
* learner-filesystem set (user-decided 2026-09, sources v1: always-on),
|
||||
* plan tools, and probe_done (it is how the phase ENDS — a prober that
|
||||
* can never conclude is a lockout).
|
||||
* The write side validates against this list (settings-body.ts: unknown
|
||||
* key = 400 with the supported list); the read-side sanitizer stays
|
||||
* lenient for forward-compat. The names are the MODEL-FACING tool names
|
||||
* (research, not researcher; pdf is the PDF-reader dispatch, sources v0
|
||||
* 2026-09). */
|
||||
export const TOGGLEABLE_TOOLS = ["research", "demo", "visual", "pdf"] as const;
|
||||
|
||||
/** The resolved settings → factory seam shape (Platform wires chat-handler
|
||||
|
||||
@@ -210,13 +210,14 @@ export async function handleChatRequest(
|
||||
// Tests inject a factory: the agent is stateless per turn (docs/
|
||||
// pi-engine-swap.md §2), so each request builds a fresh one off the
|
||||
// transcript — the factory mirrors that seam exactly.
|
||||
// CROSS-STREAM SEAM (agreed 2026-09-19, settings wave): `toolToggles`
|
||||
// CROSS-STREAM SEAM (P2 reconciliation, 2026-09-19): `toolToggles`
|
||||
// rides the build opts as the user-level DELTA map (absent key =
|
||||
// enabled — quiz, ask_user_question, research, demo, visual). The
|
||||
// Teacher stream adds the field to AgentBuildOptions + the filtering in
|
||||
// createTeacherTools / the prober's registry this same wave; until that
|
||||
// merge, the extra property is structurally ignored (a harmless no-op).
|
||||
const agentOpts: AgentBuildOptions & { toolToggles?: Record<string, boolean> } = {
|
||||
// enabled). The definitive toggleable set is TOGGLEABLE_TOOLS — the
|
||||
// sub-agent JOB tools (research/demo/visual/pdf; adjudicated with the
|
||||
// user: quiz/ask/fs/plan/probe_done are always-on). First-class on
|
||||
// AgentBuildOptions; the factories drop anything explicitly false and
|
||||
// the settings write side rejects unknown keys.
|
||||
const agentOpts: AgentBuildOptions = {
|
||||
engine,
|
||||
messages: recordToPiMessages(outgoing),
|
||||
onUsage: collectUsage,
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
RoleModelOverride,
|
||||
} from "../core/settings.ts";
|
||||
import { BACKGROUND_MODEL_ROLES } from "../core/settings.ts";
|
||||
import { TOGGLEABLE_TOOLS } from "../core/pending.ts";
|
||||
|
||||
/** Parsed-or-rejected — `ok: false` carries the 400's error message. */
|
||||
export type SettingsBodyResult<T> =
|
||||
@@ -166,7 +167,20 @@ export function parseAgentSettingsBody(body: unknown): SettingsBodyResult<AgentS
|
||||
}
|
||||
const tools = parseBooleanMap(raw.tools, "tools");
|
||||
if (tools.error) return { ok: false, error: tools.error };
|
||||
if (tools.map) settings.tools = tools.map;
|
||||
if (tools.map) {
|
||||
// Unknown tool keys are rejected at WRITE time (user decision
|
||||
// 2026-09-19): the delta is inert while unread, but once the bridge
|
||||
// honors it a persisted `quiz: false` would genuinely drop the quiz —
|
||||
// and the definitive list is the adjudicated job-tools-only set.
|
||||
// The read-side sanitizer stays lenient for forward-compat.
|
||||
const supported = TOGGLEABLE_TOOLS as readonly string[];
|
||||
for (const key of Object.keys(tools.map)) {
|
||||
if (!supported.includes(key)) {
|
||||
return { ok: false, error: `unknown tool "${key}" (supported: ${supported.join(", ")})` };
|
||||
}
|
||||
}
|
||||
settings.tools = tools.map;
|
||||
}
|
||||
const prompts = parseBooleanMap(raw.prompts, "prompts");
|
||||
if (prompts.error) return { ok: false, error: prompts.error };
|
||||
if (prompts.map) settings.prompts = prompts.map;
|
||||
|
||||
Reference in New Issue
Block a user