9fabd5e4d5
- 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.
138 lines
4.1 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
}
|