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:
@@ -53,7 +53,10 @@ export type ModelRole =
|
|||||||
| "visualMaker"
|
| "visualMaker"
|
||||||
| "pdfReader"
|
| "pdfReader"
|
||||||
| "extractor"
|
| "extractor"
|
||||||
| "auditor";
|
| "auditor"
|
||||||
|
| "partitioner"
|
||||||
|
| "problemGenerator"
|
||||||
|
| "grader";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The LIVE graph store (src/store/graph.ts) — owned by the app's extraction
|
* The LIVE graph store (src/store/graph.ts) — owned by the app's extraction
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { batchRuns, coverageOf, messageViewLines } from "../../core/coverage.ts"
|
|||||||
import { teacherProseBlocks } from "../../core/coverage.ts";
|
import { teacherProseBlocks } from "../../core/coverage.ts";
|
||||||
import type { LhMessage } from "../../core/native-messages.ts";
|
import type { LhMessage } from "../../core/native-messages.ts";
|
||||||
import { auditorGraphTools } from "../tools/graph.ts";
|
import { auditorGraphTools } from "../tools/graph.ts";
|
||||||
|
import { runSubmitAgent } from "./submit-agent.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The extraction pipeline (vision §1 construction, 2026-09-02 redesign;
|
* 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.`;
|
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). */
|
/** The extractor over ONE session transcript (the chat front door). */
|
||||||
function runExtractor(
|
function runExtractor(
|
||||||
engine: PiEngine | undefined,
|
engine: PiEngine | undefined,
|
||||||
@@ -432,6 +364,7 @@ function runExtractor(
|
|||||||
signal,
|
signal,
|
||||||
onStep,
|
onStep,
|
||||||
neverSubmitted: "the extractor never submitted findings",
|
neverSubmitted: "the extractor never submitted findings",
|
||||||
|
prepareArguments: healStringifiedArrays,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,6 +388,7 @@ function runNotesExtractor(
|
|||||||
signal,
|
signal,
|
||||||
onStep,
|
onStep,
|
||||||
neverSubmitted: "the extractor never submitted findings",
|
neverSubmitted: "the extractor never submitted findings",
|
||||||
|
prepareArguments: healStringifiedArrays,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,6 +455,7 @@ function runAuditor(
|
|||||||
neverSubmitted: "the auditor never submitted a review",
|
neverSubmitted: "the auditor never submitted a review",
|
||||||
extraTools: tools,
|
extraTools: tools,
|
||||||
maxTurns: 16,
|
maxTurns: 16,
|
||||||
|
prepareArguments: healStringifiedArrays,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -226,6 +226,12 @@ export const ROLE_SLOT: Readonly<Record<ModelRole, ModelSlotName>> = {
|
|||||||
auditor: "cheap",
|
auditor: "cheap",
|
||||||
visualMaker: "vision",
|
visualMaker: "vision",
|
||||||
pdfReader: "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
|
/** General settings — app-level behavior. Version DISPLAY and plans login
|
||||||
|
|||||||
Reference in New Issue
Block a user