From 00b3c23e063d91c5afacf4d15362eead39c04dba Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Tue, 29 Sep 2026 17:57:53 +0300 Subject: [PATCH] =?UTF-8?q?practice=20slice=203A=20=E2=80=94=20runSubmitAg?= =?UTF-8?q?ent=20extracted=20to=20shared=20module;=20practice=20model=20ro?= =?UTF-8?q?les?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit runSubmitAgent moved verbatim from extraction.ts to src/agent/subagents/ submit-agent.ts (exported; extraction imports it — zero behavior change, extraction suites green untouched). New optional prepareArguments seam: extraction wires its healStringifiedArrays heal; default is identity pass-through. ModelRole += partitioner | problemGenerator | grader; ROLE_SLOT pins all three to strong (quality-critical trio, plan default). Note: BACKGROUND_MODEL_ROLES (per-role settings overrides) deliberately NOT extended — settings-surface decision, flagged for the planning session. --- config.ts | 5 +- src/agent/subagents/extraction.ts | 73 ++---------------------- src/agent/subagents/submit-agent.ts | 87 +++++++++++++++++++++++++++++ src/core/settings.ts | 6 ++ 4 files changed, 101 insertions(+), 70 deletions(-) create mode 100644 src/agent/subagents/submit-agent.ts diff --git a/config.ts b/config.ts index c798f21..d1fe32e 100644 --- a/config.ts +++ b/config.ts @@ -53,7 +53,10 @@ export type ModelRole = | "visualMaker" | "pdfReader" | "extractor" - | "auditor"; + | "auditor" + | "partitioner" + | "problemGenerator" + | "grader"; /** * The LIVE graph store (src/store/graph.ts) — owned by the app's extraction diff --git a/src/agent/subagents/extraction.ts b/src/agent/subagents/extraction.ts index 8b65e33..6f0ff66 100644 --- a/src/agent/subagents/extraction.ts +++ b/src/agent/subagents/extraction.ts @@ -29,6 +29,7 @@ import { batchRuns, coverageOf, messageViewLines } from "../../core/coverage.ts" import { teacherProseBlocks } from "../../core/coverage.ts"; import type { LhMessage } from "../../core/native-messages.ts"; import { auditorGraphTools } from "../tools/graph.ts"; +import { runSubmitAgent } from "./submit-agent.ts"; /** * The extraction pipeline (vision §1 construction, 2026-09-02 redesign; @@ -344,75 +345,6 @@ field because it is empty. Your entire deliverable is ONE submit_findings call.`; -/** One single-submit agent run — the shared shape of EVERY LLM stage - * here (session + notes): the whole deliverable is ONE TypeBox-validated - * submit call (a pi agent purely so the model can retry a schema-invalid - * call). The submitted value is captured from the execute seam, which - * only fires on a schema-valid call — the ??-fill (the old zod defaults) - * applies at the capture. */ -async function runSubmitAgent(opts: { - role: ModelRole; - toolName: string; - toolDescription: string; - parameters: TSchema; - /** Weak-model tolerance: the old zod `.default()`s, applied to the - * schema-valid raw input at the submit seam. */ - fill: (input: any) => T; - systemPrompt: string; - prompt: string; - engine: PiEngine | undefined; - signal: AbortSignal | undefined; - onStep: (usage: Usage, model: string, step: number, finishReason: string, tools: string) => void; - neverSubmitted: string; - /** Extra tools beside the submit call (the auditor's graph-exploration - * set, 2026-09-27): a small agentic loop instead of a bare retry. */ - extraTools?: Record>; - /** Turn budget for the loop (default 4 = retry-only; the auditor runs - * an exploration loop instead — 16). */ - maxTurns?: number; -}): Promise { - let submitted: T | undefined; - const result = await runSubagent( - [ - ...(opts.extraTools ? Object.values(opts.extraTools) : []), - { - name: opts.toolName, - label: opts.toolName, - description: opts.toolDescription, - parameters: opts.parameters, - prepareArguments: (args) => healStringifiedArrays(args) as never, - execute: async (_toolCallId, input) => { - submitted = opts.fill(input); - return toToolResult(submitted); - }, - } satisfies AgentTool, - ], - { - role: opts.role, - engine: opts.engine, - systemPrompt: opts.systemPrompt, - prompt: opts.prompt, - maxTurns: opts.maxTurns ?? 4, - abortSignal: opts.signal, - onAssistantMessage: (message, step) => - opts.onStep( - message.usage, - // Provider-qualified stamp (model slots wave, 2026-09-20): - // pi reports the bare id; usage stamps carry provider:id to - // match the price table's uniform keys. - `${message.provider}:${message.model}`, - step, - message.stopReason, - message.content.filter((c) => c.type === "toolCall").map((c) => (c as { name: string }).name).join(", ") || "(none)", - ), - shouldStop: (_message, toolResults) => toolResults.some((r) => r.toolName === opts.toolName && !r.isError), - }, - ); - void result; - if (!submitted) throw new Error(opts.neverSubmitted); - return submitted; -} - /** The extractor over ONE session transcript (the chat front door). */ function runExtractor( engine: PiEngine | undefined, @@ -432,6 +364,7 @@ function runExtractor( signal, onStep, neverSubmitted: "the extractor never submitted findings", + prepareArguments: healStringifiedArrays, }); } @@ -455,6 +388,7 @@ function runNotesExtractor( signal, onStep, neverSubmitted: "the extractor never submitted findings", + prepareArguments: healStringifiedArrays, }); } @@ -521,6 +455,7 @@ function runAuditor( neverSubmitted: "the auditor never submitted a review", extraTools: tools, maxTurns: 16, + prepareArguments: healStringifiedArrays, }); } diff --git a/src/agent/subagents/submit-agent.ts b/src/agent/subagents/submit-agent.ts new file mode 100644 index 0000000..9561dae --- /dev/null +++ b/src/agent/subagents/submit-agent.ts @@ -0,0 +1,87 @@ +/** + * runSubmitAgent — the shared single-submit agent seam (moved verbatim from + * extraction.ts, practice slice 3, 2026-09-29): one TypeBox-validated submit + * call is the whole deliverable (a pi agent purely so the model can retry a + * schema-invalid call). The submitted value is captured from the execute + * seam, which only fires on a schema-valid call — the ??-fill (the old zod + * defaults) applies at the capture. + * + * Extraction passes its own weak-model argument heal (healStringifiedArrays) + * through `prepareArguments`; the default is identity pass-through. + */ + +import type { Usage } from "@earendil-works/pi-ai"; +import type { AgentTool } from "@earendil-works/pi-agent-core"; +import type { TSchema } from "typebox"; +import { toToolResult } from "../tool-execute.ts"; +import type { ModelRole } from "../../../config.ts"; +import type { PiEngine } from "../models.ts"; +import { runSubagent } from "./pi-run.ts"; + +export async function runSubmitAgent(opts: { + role: ModelRole; + toolName: string; + toolDescription: string; + parameters: TSchema; + /** Weak-model tolerance: the old zod `.default()`s, applied to the + * schema-valid raw input at the submit seam. */ + fill: (input: any) => T; + systemPrompt: string; + prompt: string; + engine: PiEngine | undefined; + signal: AbortSignal | undefined; + onStep: (usage: Usage, model: string, step: number, finishReason: string, tools: string) => void; + neverSubmitted: string; + /** Extra tools beside the submit call (the auditor's graph-exploration + * set, 2026-09-27): a small agentic loop instead of a bare retry. */ + extraTools?: Record>; + /** Turn budget for the loop (default 4 = retry-only; the auditor runs + * an exploration loop instead — 16). */ + maxTurns?: number; + /** Weak-model argument heal at pi's prepareArguments seam (runs BEFORE + * TypeBox validation — the sanctioned shim point). Identity by + * default; extraction wires healStringifiedArrays here. */ + prepareArguments?: (args: unknown) => unknown; +}): Promise { + let submitted: T | undefined; + const result = await runSubagent( + [ + ...(opts.extraTools ? Object.values(opts.extraTools) : []), + { + name: opts.toolName, + label: opts.toolName, + description: opts.toolDescription, + parameters: opts.parameters, + prepareArguments: (args) => + (opts.prepareArguments ? opts.prepareArguments(args) : args) as never, + execute: async (_toolCallId, input) => { + submitted = opts.fill(input); + return toToolResult(submitted); + }, + } satisfies AgentTool, + ], + { + role: opts.role, + engine: opts.engine, + systemPrompt: opts.systemPrompt, + prompt: opts.prompt, + maxTurns: opts.maxTurns ?? 4, + abortSignal: opts.signal, + onAssistantMessage: (message, step) => + opts.onStep( + message.usage, + // Provider-qualified stamp (model slots wave, 2026-09-20): + // pi reports the bare id; usage stamps carry provider:id to + // match the price table's uniform keys. + `${message.provider}:${message.model}`, + step, + message.stopReason, + message.content.filter((c) => c.type === "toolCall").map((c) => (c as { name: string }).name).join(", ") || "(none)", + ), + shouldStop: (_message, toolResults) => toolResults.some((r) => r.toolName === opts.toolName && !r.isError), + }, + ); + void result; + if (!submitted) throw new Error(opts.neverSubmitted); + return submitted; +} diff --git a/src/core/settings.ts b/src/core/settings.ts index fbfbe28..2158d5a 100644 --- a/src/core/settings.ts +++ b/src/core/settings.ts @@ -226,6 +226,12 @@ export const ROLE_SLOT: Readonly> = { auditor: "cheap", visualMaker: "vision", pdfReader: "vision", + // The practice trio (2026-09-29): problem engineering is the product's + // quality-critical component — strong tier for partitioner/generator/ + // grader alike (owner-approved plan default; one edit away from cheap). + partitioner: "strong", + problemGenerator: "strong", + grader: "strong", }; /** General settings — app-level behavior. Version DISPLAY and plans login