practice slice 3A — runSubmitAgent extracted to shared module; practice model roles

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.
This commit is contained in:
amosblomqvist
2026-09-29 17:57:53 +03:00
parent b7a19609cb
commit 00b3c23e06
4 changed files with 101 additions and 70 deletions
+4 -69
View File
@@ -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<T>(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<string, AgentTool<any>>;
/** Turn budget for the loop (default 4 = retry-only; the auditor runs
* an exploration loop instead — 16). */
maxTurns?: number;
}): Promise<T> {
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<any>,
],
{
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,
});
}
+87
View File
@@ -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<T>(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<string, AgentTool<any>>;
/** 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<T> {
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<any>,
],
{
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;
}