feat(T2b): sources v0 — read tool + pdf-reader sub-agent
Learner source files, on the teacher's own terms (nothing auto-injects): - read tool (src/agent/tools/read.ts): pi-read port — path/offset/limit, 2000-line/50KB truncation with use-offset continuation notices, image files (jpg/png/gif/webp/bmp) returned as native image content parts (new LhToolImageResult escape hatch in lh-tool.ts). Always-on in both the prober and teacher registries (user-decided 2026-09). - pdf-reader sub-agent (JobKind 'pdf', toggleable, ack-and-steer): triage via pdf_info (pdfjs text layer: per-page text/image/math-density), targeted pdf_extract, regex pdf_search (hits count as consultation), and pdf_render — pages painted by pdfjs onto a canvas shim in headless Chrome (the pdfium viewer doesn't paint headless), screenshot fed back image-in-the-loop (visual-maker idiom, providerSafePng applied). The unread-finish gate bounces a zero-consultation brief once. Teacher gets a text brief with page citations only; renders never leave the loop. - MODELS.pdfReader = qwen/qwen3.8-flash (user override 2026-09-19: the look-at-renders workload wants the vision-proven model; catalog listed glm-5.3-flash as image-capable but qwen is the known-good daily driver). - UI: PdfCompleteBadge + PdfBriefCard (steer-card, job-card), dispatch card + activity-dock kind entries (type-forced, flagged for UI review). - Prompts: 'Learner source files' section in teacher/30-jobs.md; read line in prober/10-probe-doctrine.md. Doctrine rides tool descriptions per the self-contained-tools principle.
This commit is contained in:
@@ -143,10 +143,10 @@ describe("the prober's quiz: no nodeId field at all", () => {
|
||||
assert.equal("nodeId" in smuggled.data, false, "stripped, not tolerated");
|
||||
});
|
||||
|
||||
it("the prober's tool registry carries the nodeId-less quiz and probe_done only", () => {
|
||||
it("the prober's tool registry carries the nodeId-less quiz, probe_done, and the shared read tool", () => {
|
||||
const chatId = newChat("Prober: registry");
|
||||
const tools = createProberTools({ chatId, chatStore, deliver: () => {} });
|
||||
assert.deepEqual(Object.keys(tools).sort(), ["ask_user_question", "probe_done", "quiz"]);
|
||||
assert.deepEqual(Object.keys(tools).sort(), ["ask_user_question", "probe_done", "quiz", "read"]);
|
||||
const schema = quizSchemaOf(tools);
|
||||
assert.equal(schema.safeParse({ ...validQuiz, nodeId: "anything" }).success, true, "no session check to fail");
|
||||
});
|
||||
@@ -154,25 +154,26 @@ describe("the prober's quiz: no nodeId field at all", () => {
|
||||
// ─── 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", () => {
|
||||
it("TOGGLEABLE_TOOLS: the sub-agent dispatch tools; quiz/ask/read 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"]);
|
||||
// quiz/ask_user_question/read are always-on FOR NOW (user-decided
|
||||
// 2026-09-15, sources v0); they join when settings exposes them — the
|
||||
// factory seam is generic.
|
||||
assert.deepEqual(TOGGLEABLE_TOOLS, ["research", "demo", "visual", "pdf"]);
|
||||
});
|
||||
|
||||
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"]);
|
||||
assert.deepEqual(Object.keys(createProberTools(ctx)).sort(), ["ask_user_question", "probe_done", "quiz", "read"]);
|
||||
|
||||
const noAsk = createProberTools(ctx, { ask_user_question: false });
|
||||
assert.deepEqual(Object.keys(noAsk).sort(), ["probe_done", "quiz"]);
|
||||
assert.deepEqual(Object.keys(noAsk).sort(), ["probe_done", "quiz", "read"]);
|
||||
|
||||
// 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.deepEqual(Object.keys(noQuiz).sort(), ["ask_user_question", "probe_done", "read"]);
|
||||
assert.ok(createProberTools(ctx, { probe_done: false }).probe_done, "probe_done always-on");
|
||||
});
|
||||
|
||||
@@ -185,10 +186,10 @@ describe("tool toggles (core/pending.ts + factories)", () => {
|
||||
spawn: () => {},
|
||||
};
|
||||
const full = Object.keys(createTeacherTools(ctx)).sort();
|
||||
assert.deepEqual(full, ["ask_user_question", "demo", "plan_init", "quiz", "research", "visual"]);
|
||||
assert.deepEqual(full, ["ask_user_question", "demo", "pdf", "plan_init", "quiz", "read", "research", "visual"]);
|
||||
|
||||
const noResearch = Object.keys(createTeacherTools(ctx, { research: false })).sort();
|
||||
assert.deepEqual(noResearch, ["ask_user_question", "demo", "plan_init", "quiz", "visual"]);
|
||||
assert.deepEqual(noResearch, ["ask_user_question", "demo", "pdf", "plan_init", "quiz", "read", "visual"]);
|
||||
|
||||
// absent = enabled: an empty record and undefined behave identically
|
||||
assert.deepEqual(
|
||||
|
||||
Reference in New Issue
Block a user