Files
oh-my-pi/packages/coding-agent/src/eval/js/executor.ts
T
can1357 9fabd5e4d5 feat(coding-agent): added onStatus in eval backends for status streams
- Added optional `onStatus` callback wiring across eval backends and JS/Python executors for live status streams.
- Added collectDisplay-based forwarding so `emitStatus` and `onDisplay` route status outputs consistently.
- Expanded agent status payloads with preview/model/token-cost context and kept completion updates single-pass.
- Added status upsert and render adjustments in `tools/eval.ts` to coalesce agent events with progress stats.
- Added status/progress test coverage for running/completed agent events, final metric retention, and parallel placement.
- Updated CHANGELOG Unreleased notes to record live progress updates and completion-status metric fixes.
2026-05-31 08:45:12 +02:00

138 lines
4.1 KiB
TypeScript

import { DEFAULT_MAX_BYTES, OutputSink } from "../../session/streaming-output";
import type { ToolSession } from "../../tools";
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../../tools/output-meta";
import { executeInVmContext, type JsDisplayOutput } from "./context-manager";
import type { JsStatusEvent } from "./shared/types";
export interface JsExecutorOptions {
cwd?: string;
timeoutMs?: number;
deadlineMs?: number;
onChunk?: (chunk: string) => Promise<void> | void;
onStatus?: (event: JsStatusEvent) => void;
signal?: AbortSignal;
sessionId: string;
reset?: boolean;
sessionFile?: string;
artifactPath?: string;
artifactId?: string;
session: ToolSession;
}
export interface JsResult {
output: string;
exitCode: number | undefined;
cancelled: boolean;
truncated: boolean;
artifactId?: string;
totalLines: number;
totalBytes: number;
outputLines: number;
outputBytes: number;
displayOutputs: JsDisplayOutput[];
}
function getExecutionTimeoutMs(options: Pick<JsExecutorOptions, "deadlineMs" | "timeoutMs">): number | undefined {
if (options.deadlineMs !== undefined) {
return Math.max(1, options.deadlineMs - Date.now());
}
return options.timeoutMs;
}
function isAbortError(error: unknown): boolean {
return (
(error instanceof DOMException && (error.name === "AbortError" || error.name === "TimeoutError")) ||
(error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError"))
);
}
export async function executeJs(code: string, options: JsExecutorOptions): Promise<JsResult> {
const displayOutputs: JsDisplayOutput[] = [];
const outputSink = new OutputSink({
artifactPath: options.artifactPath,
artifactId: options.artifactId,
spillThreshold: DEFAULT_MAX_BYTES,
headBytes: resolveOutputSinkHeadBytes(options.session.settings),
maxColumns: resolveOutputMaxColumns(options.session.settings),
onChunk: chunk => options.onChunk?.(chunk),
});
const timeoutMs = getExecutionTimeoutMs(options);
const timeoutSignal =
typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0
? AbortSignal.timeout(timeoutMs)
: undefined;
const signal =
options.signal && timeoutSignal
? AbortSignal.any([options.signal, timeoutSignal])
: (options.signal ?? timeoutSignal);
try {
await executeInVmContext({
sessionKey: options.sessionId,
sessionId: options.sessionId,
cwd: options.cwd ?? options.session.cwd,
session: options.session,
reset: options.reset,
code,
filename: `js-cell-${crypto.randomUUID()}.js`,
timeoutMs,
runState: {
signal,
onText: chunk => outputSink.push(chunk),
onDisplay: output => {
displayOutputs.push(output);
if (output.type === "status") options.onStatus?.(output.event);
},
},
});
const summary = await outputSink.dump();
return {
output: summary.output,
exitCode: 0,
cancelled: false,
truncated: summary.truncated,
artifactId: summary.artifactId,
totalLines: summary.totalLines,
totalBytes: summary.totalBytes,
outputLines: summary.outputLines,
outputBytes: summary.outputBytes,
displayOutputs,
};
} catch (error) {
if (signal?.aborted || isAbortError(error)) {
const timeoutReason = timeoutSignal?.aborted ? "Command timed out" : "";
if (timeoutReason) {
outputSink.push(timeoutReason);
}
const summary = await outputSink.dump();
return {
output: summary.output,
exitCode: undefined,
cancelled: true,
truncated: summary.truncated,
artifactId: summary.artifactId,
totalLines: summary.totalLines,
totalBytes: summary.totalBytes,
outputLines: summary.outputLines,
outputBytes: summary.outputBytes,
displayOutputs,
};
}
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
outputSink.push(message);
const summary = await outputSink.dump();
return {
output: summary.output,
exitCode: 1,
cancelled: false,
truncated: summary.truncated,
artifactId: summary.artifactId,
totalLines: summary.totalLines,
totalBytes: summary.totalBytes,
outputLines: summary.outputLines,
outputBytes: summary.outputBytes,
displayOutputs,
};
}
}