feat(T2a): tool-toggle seam + phase-based question principles
Part 1 — tool availability (the settings seam, machinery generic): - core/pending.ts: TOGGLEABLE_TOOLS (today: research, demo, visual — the model-facing tool names), EnabledTools = the JSON-native seam shape Partial<Record<string, boolean>> (absent key = enabled, so old records behave exactly as today), filterEnabledTools shared helper. quiz/ask_user_question are always-on FOR NOW (user-decided); the filtering is generic so future toggleables (question-tool variants, the read tool, …) need no factory change. - createProberTools/createTeacherTools take enabled?: EnabledTools; the composition root (chat-handler, Platform's lane this wave) passes the resolved settings through. probe_done is re-added after the filter — always-on by construction (it ends the phase; a prober that can never conclude is a lockout). - Prompts stay STATIC under toggles this wave (user-decided); standing principle recorded: tools ship self-contained usage guidance in their descriptions; system prompts reference only always-on tools. Part 2 — phase-based question-writing principles (user-locked wording, drafted with the user over three iterations): - NEW teacher/21-question-principles.md (composes after 20-teaching): answerable-by-reasoning-from-the-session (the fairness rule that filters unanswerable questions without forbidding visible answers), never-just-reciting-your-words-back, PREFER problems that apply the knowledge (preference, not hard rule — honest recall is the fallback, not the habit), catch-confusion-early-and-nameable (wrong options as real misconceptions), answer-is-signal-not-verdict. - prober/10-probe-doctrine.md: 'no rush' retired; quick-measurement directive in — every question moves an edge (bracket, narrow, or characterize); a bracketed strand is done. Tests: TOGGLEABLE_TOOLS pin, prober/teacher filtering (absent = full, false drops exactly that tool, probe_done survives), prompt pins (the principles compose verbatim into the teacher prompt, the no-rush framing is gone from the prober). 547/547, typecheck/lint/build green.
This commit is contained in:
@@ -14,7 +14,9 @@ import assert from "node:assert/strict";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { after, describe, it } from "node:test";
|
||||
import { createProberTools, teachQuizToolFor } from "../src/agent/tools.ts";
|
||||
import { createProberTools, createTeacherTools, teachQuizToolFor, type TeacherToolContext } from "../src/agent/tools.ts";
|
||||
import { TOGGLEABLE_TOOLS } from "../src/core/pending.ts";
|
||||
import { jobStore } from "../src/store/jobs.ts";
|
||||
import { probeQuizInputSchema } from "../src/core/question-core.ts";
|
||||
import { chatStore } from "../src/store/chats.ts";
|
||||
import { PATHS } from "../config.ts";
|
||||
@@ -148,4 +150,51 @@ describe("the prober's quiz: no nodeId field at all", () => {
|
||||
const schema = quizSchemaOf(tools);
|
||||
assert.equal(schema.safeParse({ ...validQuiz, nodeId: "anything" }).success, true, "no session check to fail");
|
||||
});
|
||||
});
|
||||
});
|
||||
// ─── tool availability (2026-09 settings seam) ───────────────────────────────
|
||||
|
||||
describe("tool toggles (core/pending.ts + factories)", () => {
|
||||
it("TOGGLEABLE_TOOLS: today only the sub-agent dispatch tools; quiz/ask stay off the list", () => {
|
||||
// The user-toggleable vocabulary Platform's settings validates against.
|
||||
// quiz/ask_user_question are always-on FOR NOW (user-decided 2026-09-15);
|
||||
// they join when settings exposes them — the factory seam is generic.
|
||||
assert.deepEqual(TOGGLEABLE_TOOLS, ["research", "demo", "visual"]);
|
||||
});
|
||||
|
||||
it("createProberTools: absent record = full set; explicit false drops exactly that tool; probe_done survives everything", () => {
|
||||
const chatId = newChat("Prober: toggles");
|
||||
const ctx = { chatId, chatStore, deliver: () => {} };
|
||||
assert.deepEqual(Object.keys(createProberTools(ctx)).sort(), ["ask_user_question", "probe_done", "quiz"]);
|
||||
|
||||
const noAsk = createProberTools(ctx, { ask_user_question: false });
|
||||
assert.deepEqual(Object.keys(noAsk).sort(), ["probe_done", "quiz"]);
|
||||
|
||||
// quiz off is honored by the generic machinery too (future toggleable),
|
||||
// but probe_done is never droppable — it is how the phase ends.
|
||||
const noQuiz = createProberTools(ctx, { quiz: false });
|
||||
assert.deepEqual(Object.keys(noQuiz).sort(), ["ask_user_question", "probe_done"]);
|
||||
assert.ok(createProberTools(ctx, { probe_done: false }).probe_done, "probe_done always-on");
|
||||
});
|
||||
|
||||
it("createTeacherTools: {research: false} removes exactly research; unknown keys are no-ops", () => {
|
||||
const chatId = newChat("Teacher: toggles");
|
||||
const ctx: TeacherToolContext = {
|
||||
chatId,
|
||||
chatStore,
|
||||
jobStore,
|
||||
spawn: () => {},
|
||||
};
|
||||
const full = Object.keys(createTeacherTools(ctx)).sort();
|
||||
assert.deepEqual(full, ["ask_user_question", "demo", "plan_init", "quiz", "research", "visual"]);
|
||||
|
||||
const noResearch = Object.keys(createTeacherTools(ctx, { research: false })).sort();
|
||||
assert.deepEqual(noResearch, ["ask_user_question", "demo", "plan_init", "quiz", "visual"]);
|
||||
|
||||
// absent = enabled: an empty record and undefined behave identically
|
||||
assert.deepEqual(
|
||||
Object.keys(createTeacherTools(ctx, {})).sort(),
|
||||
full,
|
||||
"absent keys mean enabled — old records behave exactly as today",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user