Files
oh-my-pi/packages/coding-agent/src/eval/agent-bridge.ts
T
2026-07-22 23:54:15 +02:00

225 lines
8.2 KiB
TypeScript

/**
* Host-side handler for the eval `agent()` helper.
*/
import { type } from "arktype";
import {
buildStructuredSubagentRecoveryHint,
runStructuredSubagent,
StructuredSubagentError,
type StructuredSubagentSchemaMode,
} from "../task/structured-subagent";
import type { AgentProgress, SingleResult } from "../task/types";
import type { NestedRepoPatch } from "../task/worktree";
import type { ToolSession } from "../tools";
import { ToolError } from "../tools/tool-errors";
import { withBridgeTimeoutPause } from "./bridge-timeout";
import type { JsStatusEvent } from "./js/shared/types";
// Import review tools for side effects (registers subagent tool handlers).
import "../tools/review";
/** Synthetic bridge name reserved for the `agent()` helper across both runtimes. */
export const EVAL_AGENT_BRIDGE_NAME = "__agent__";
const agentArgsSchema = type({
prompt: "string>0",
"agent?": "string>0",
"model?": "string>0|string>0[]",
"label?": "string",
"schema?": "unknown",
"schemaMode?": "'permissive' | 'strict'",
"isolated?": "boolean",
"apply?": "boolean",
"merge?": "boolean",
"handle?": "boolean",
});
interface EvalAgentArgs {
prompt: string;
agent?: string;
model?: string | string[];
label?: string;
schema?: unknown;
schemaMode?: StructuredSubagentSchemaMode;
isolated?: boolean;
apply?: boolean;
merge?: boolean;
handle?: boolean;
}
export interface EvalAgentBridgeOptions {
session: ToolSession;
signal?: AbortSignal;
emitStatus?: (event: JsStatusEvent) => void;
}
export interface EvalAgentResult {
text: string;
/** Parsed structured data returned by the child executor. */
data?: unknown;
details: {
agent: string;
id: string;
model?: string | string[];
structured: boolean;
schemaSource?: "caller" | "agent" | "session";
schemaMode?: StructuredSubagentSchemaMode;
schemaStatus?: "valid" | "invalid";
isolated?: boolean;
patchPath?: string;
branchName?: string;
nestedPatches?: NestedRepoPatch[];
changesApplied?: boolean | null;
isolationSummary?: string;
};
}
function parseAgentArgs(args: unknown): EvalAgentArgs {
const result = agentArgsSchema(args);
if (result instanceof type.errors) {
throw new ToolError(`agent() received invalid arguments: ${result.summary}`);
}
return result;
}
function trimToUndefined(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
}
function emitProgressStatus(emitStatus: ((event: JsStatusEvent) => void) | undefined, progress: AgentProgress): void {
if (!emitStatus) return;
const preview = (progress.assignment ?? progress.task ?? "").split("\n")[0]?.slice(0, 120);
emitStatus({
op: "agent",
id: progress.id,
agent: progress.agent,
status: progress.status,
lastIntent: progress.lastIntent,
currentTool: progress.currentTool,
currentToolArgs: progress.currentToolArgs,
taskPreview: preview || undefined,
toolCount: progress.toolCount,
tokens: progress.tokens,
contextTokens: progress.contextTokens,
contextWindow: progress.contextWindow,
cost: progress.cost,
durationMs: progress.durationMs,
model: progress.resolvedModel,
});
}
function buildSubagentFailureMessage(agentName: string, result: SingleResult): string {
const abortReason = trimToUndefined(result.abortReason);
if (result.aborted && abortReason) return abortReason;
return (
trimToUndefined(result.error) ??
trimToUndefined(result.stderr) ??
abortReason ??
`agent() subagent '${agentName}' failed.`
);
}
/**
* Run a single subagent on behalf of an eval cell's `agent()` call.
*/
export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOptions): Promise<EvalAgentResult> {
const parsed = parseAgentArgs(args);
const turnBudget = options.session.getTurnBudget?.();
if (turnBudget?.hard && turnBudget.total !== null && turnBudget.spent >= turnBudget.total) {
throw new ToolError(
`agent() blocked: turn token budget exhausted (${turnBudget.spent}/${turnBudget.total} output tokens). Raise or drop the +Nk! ceiling to continue.`,
);
}
const isolation =
Object.hasOwn(parsed, "isolated") || Object.hasOwn(parsed, "apply") || Object.hasOwn(parsed, "merge")
? {
...(parsed.isolated !== undefined ? { requested: parsed.isolated } : {}),
...(parsed.merge === false ? { merge: "patch" as const } : {}),
...(parsed.apply !== undefined ? { apply: parsed.apply } : {}),
}
: undefined;
try {
const execution = await withBridgeTimeoutPause(
options.emitStatus,
() =>
runStructuredSubagent({
session: options.session,
invocationKind: "eval",
assignment: parsed.prompt,
...(parsed.agent !== undefined ? { agent: parsed.agent } : {}),
...(parsed.model !== undefined ? { model: parsed.model } : {}),
...(Object.hasOwn(parsed, "schema") ? { outputSchema: parsed.schema } : {}),
...(parsed.schemaMode !== undefined ? { schemaMode: parsed.schemaMode } : {}),
...(parsed.label !== undefined ? { identity: { label: parsed.label } } : {}),
...(isolation ? { isolation } : {}),
...(parsed.handle ? { retainArtifacts: true } : {}),
keepAlive: false,
maxRuntimeMs: 0,
shareEvalSession: false,
...(options.signal !== undefined ? { signal: options.signal } : {}),
...(options.emitStatus
? { onProgress: (progress: AgentProgress) => emitProgressStatus(options.emitStatus, progress) }
: {}),
}),
{ deferExternalAbort: true },
);
const { result, policy, mergeSummary, changesApplied, artifactsDir } = execution;
if (result.exitCode !== 0 || result.error || result.aborted) {
const failureMessage = buildSubagentFailureMessage(policy.agentName, result)
.replace(/<\/?system-notification>/g, "")
.trim();
const recoveryHint = policy.isIsolated ? await buildStructuredSubagentRecoveryHint(result, artifactsDir) : "";
throw new ToolError(`${failureMessage}${recoveryHint}`);
}
if (policy.isIsolated && changesApplied === false) {
const summary = mergeSummary.replace(/<\/?system-notification>/g, "").trim();
const recoveryHint = await buildStructuredSubagentRecoveryHint(result, artifactsDir);
throw new ToolError(
`agent() isolated apply failed for ${result.id}${summary ? `: ${summary}` : ""}${recoveryHint}`,
);
}
const structuredOutput = result.structuredOutput;
const structured = structuredOutput?.source !== undefined && structuredOutput.source !== "none";
if (structured && mergeSummary.includes("<system-notification>")) {
const recoveryHint = await buildStructuredSubagentRecoveryHint(result, artifactsDir);
throw new ToolError(
`agent() isolated nested patch apply failed for ${result.id}: ${mergeSummary.replace(/<\/?system-notification>/g, "").trim()}${recoveryHint}`,
);
}
const hasData = structured && structuredOutput !== undefined && Object.hasOwn(structuredOutput, "data");
const data = structuredOutput?.data;
const text = structured ? result.output : result.output + mergeSummary;
const schemaSource = structuredOutput?.source === "none" ? undefined : structuredOutput?.source;
const schemaMode = structured ? structuredOutput?.mode : undefined;
const schemaStatus = structuredOutput?.status === "unavailable" ? undefined : structuredOutput?.status;
const model = result.resolvedModel ?? policy.modelOverride;
const nestedPatches = result.nestedPatches?.length ? result.nestedPatches : undefined;
const isolationSummary = mergeSummary ? mergeSummary.trim() : undefined;
return {
text,
...(hasData ? { data } : {}),
details: {
agent: result.agent,
id: result.id,
...(model !== undefined ? { model } : {}),
structured,
...(schemaSource !== undefined ? { schemaSource } : {}),
...(schemaMode !== undefined ? { schemaMode } : {}),
...(schemaStatus !== undefined ? { schemaStatus } : {}),
...(policy.isIsolated ? { isolated: true, changesApplied } : {}),
...(result.patchPath !== undefined ? { patchPath: result.patchPath } : {}),
...(result.branchName !== undefined ? { branchName: result.branchName } : {}),
...(nestedPatches !== undefined ? { nestedPatches } : {}),
...(isolationSummary !== undefined ? { isolationSummary } : {}),
},
};
} catch (error) {
if (error instanceof StructuredSubagentError) throw new ToolError(error.message);
throw error;
}
}