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:
@@ -11,7 +11,7 @@ import type { CallUsage, GraphContextData } from "./chat.ts";
|
||||
|
||||
// ─── Async sub-agent jobs (vision §2.5; plan.md Wave 0 protocol) ────────────
|
||||
|
||||
export type JobKind = "research" | "demo" | "visual" | "graph-context";
|
||||
export type JobKind = "research" | "demo" | "visual" | "pdf" | "graph-context";
|
||||
export type JobStatus = "running" | "done" | "error";
|
||||
|
||||
export interface JobRecord {
|
||||
@@ -119,7 +119,7 @@ export function accumulateJobUsage(
|
||||
* THE ASYNC PROTOCOL — "ack now, steer later" (the pi sub-agents model,
|
||||
* vision §2.5):
|
||||
*
|
||||
* 1. Job tools (research / demo / visual) are SERVER-EXECUTED. The model
|
||||
* 1. Job tools (research / demo / visual / pdf) are SERVER-EXECUTED. The model
|
||||
* calls one MID-TURN; execute creates the job record, fires the
|
||||
* background runner, and resolves IMMEDIATELY with an acknowledgement
|
||||
* ("dispatched — continue your turn"). The teacher's turn does NOT end
|
||||
@@ -213,6 +213,28 @@ export interface VisualResult {
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
/** The pdf-reader (sources v0, 2026-09): reads a learner-provided PDF on
|
||||
* the teacher's behalf — triage, targeted extraction, look-at-the-page
|
||||
* renders inside its own loop — and returns a text brief with page
|
||||
* citations (user-decided: rendered images never leave the sub-agent's
|
||||
* context; the teacher gets text). */
|
||||
export interface PdfToolInput {
|
||||
/** Path to the learner's PDF (relative or absolute). */
|
||||
path: string;
|
||||
/** What the teacher needs from the document — focuses the brief. */
|
||||
question: string;
|
||||
}
|
||||
|
||||
export interface PdfResult {
|
||||
path: string;
|
||||
question: string;
|
||||
/** The distilled brief. Page citations inside the text point at
|
||||
* pagesConsulted. Failure shape: /^PDF failed:/i (mirrors research). */
|
||||
brief: string;
|
||||
/** Pages the reader actually consulted. */
|
||||
pagesConsulted: number[];
|
||||
}
|
||||
|
||||
// ─── Sub-agent steer messages (THE ASYNC PROTOCOL §3–4) ─────────────────────
|
||||
|
||||
/** Message metadata marking a sub-agent steer (contracts.ts §3). */
|
||||
@@ -230,7 +252,7 @@ export interface SubagentResultMeta {
|
||||
/** The data-subagent-artifact part payload: what the UI renders. */
|
||||
export interface SubagentArtifactData {
|
||||
kind: JobKind;
|
||||
result: ResearchResult | DemoResult | VisualResult;
|
||||
result: ResearchResult | DemoResult | VisualResult | PdfResult;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -270,7 +292,10 @@ export type SteerMessage =
|
||||
| GraphContextSteerMessage;
|
||||
|
||||
/** Whether a result is the failure shape (html/svg empty, failure brief). */
|
||||
export function isSubagentFailure(kind: JobKind, result: ResearchResult | DemoResult | VisualResult): boolean {
|
||||
export function isSubagentFailure(
|
||||
kind: JobKind,
|
||||
result: ResearchResult | DemoResult | VisualResult | PdfResult,
|
||||
): boolean {
|
||||
if (kind === "research") {
|
||||
const r = result as ResearchResult;
|
||||
return r.sources.length === 0 && /^Research failed:/i.test(r.brief);
|
||||
@@ -279,12 +304,16 @@ export function isSubagentFailure(kind: JobKind, result: ResearchResult | DemoRe
|
||||
const r = result as DemoResult;
|
||||
return typeof r.html !== "string" || r.html.length === 0;
|
||||
}
|
||||
if (kind === "pdf") {
|
||||
const r = result as PdfResult;
|
||||
return /^PDF failed:/i.test(r.brief);
|
||||
}
|
||||
const r = result as VisualResult;
|
||||
return typeof r.svg !== "string" || r.svg.length === 0;
|
||||
}
|
||||
|
||||
/** The model-facing text of a steer: what the result IS, what to do with it. */
|
||||
function steerText(kind: JobKind, result: ResearchResult | DemoResult | VisualResult): string {
|
||||
function steerText(kind: JobKind, result: ResearchResult | DemoResult | VisualResult | PdfResult): string {
|
||||
if (isSubagentFailure(kind, result)) {
|
||||
if (kind === "research")
|
||||
return `${(result as ResearchResult).brief}\n\nContinue with what you can establish and flag the uncertainty to the learner.`;
|
||||
@@ -292,10 +321,14 @@ function steerText(kind: JobKind, result: ResearchResult | DemoResult | VisualRe
|
||||
return `The demo "${(result as DemoResult).title}" could not be built: ${
|
||||
(result as DemoResult).notes ?? "unknown reason"
|
||||
}. Fall back to explaining in prose; if the brief was the problem, re-call demo with a tighter one.`;
|
||||
if (kind === "pdf")
|
||||
return `${(result as PdfResult).brief}\n\nContinue with what you can establish from the session and flag the gap to the learner.`;
|
||||
return `The figure could not be made: ${(result as VisualResult).notes ?? "unknown reason"}. Fall back to prose or mermaid, or re-call visual with a tighter brief.`;
|
||||
}
|
||||
if (kind === "research")
|
||||
return `Research complete — the researcher's full brief follows. Weave it into the lesson and cite its sources when you use its findings:\n\n${(result as ResearchResult).brief}`;
|
||||
if (kind === "pdf")
|
||||
return `PDF reading complete — the reader's brief on "${(result as PdfResult).path}" follows, with page citations. Weave it into the lesson and cite pages when you use it:\n\n${(result as PdfResult).brief}`;
|
||||
if (kind === "demo")
|
||||
return `The demo "${(result as DemoResult).title}" is built, headlessly tested, and rendered in the lesson. Builder notes: ${
|
||||
(result as DemoResult).notes ?? "(none)"
|
||||
@@ -312,7 +345,7 @@ function steerText(kind: JobKind, result: ResearchResult | DemoResult | VisualRe
|
||||
*/
|
||||
export function buildSubagentSteer(
|
||||
job: Pick<JobRecord, "id" | "kind" | "toolCallId">,
|
||||
result: ResearchResult | DemoResult | VisualResult,
|
||||
result: ResearchResult | DemoResult | VisualResult | PdfResult,
|
||||
): SubagentSteerMessage {
|
||||
return {
|
||||
id: `steer_${job.id}`,
|
||||
@@ -352,6 +385,8 @@ function abortText(kind: JobKind): string {
|
||||
return "Research aborted — the learner aborted the research job, so no brief is coming. Acknowledge it in one line and continue teaching without it (or offer to re-dispatch it).";
|
||||
if (kind === "demo")
|
||||
return "The demo job was aborted by the learner — no demo is coming. Acknowledge it in one line and teach the topic in prose instead (a re-dispatch is fine if the learner wants it).";
|
||||
if (kind === "pdf")
|
||||
return "The PDF reading job was aborted by the learner — no brief is coming. Acknowledge it in one line and continue without it (a re-dispatch is fine if the learner wants it).";
|
||||
return "The figure job was aborted by the learner — no figure is coming. Acknowledge it in one line and teach without it (a re-dispatch is fine if the learner wants it).";
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -34,13 +34,14 @@ export const JOB_TOOLS = ["research", "demo", "visual"] as const;
|
||||
|
||||
/** 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, when it lands) are always on;
|
||||
* 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). */
|
||||
export const TOGGLEABLE_TOOLS = ["research", "demo", "visual"] as const;
|
||||
* 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
|
||||
* to pass this through from the settings record; storage and defaults are
|
||||
|
||||
Reference in New Issue
Block a user