AI_PROJECT / src /ui-server /run-manager.mjs
chenchenaoyang's picture
Deploy from Codex
f395b70 verified
Raw
History Blame Contribute Delete
4.35 kB
import { spawn } from "node:child_process";
import path from "node:path";
import {
buildCliArgs,
buildCommandPreview,
getToolDefinition,
requiresConfirmation,
sanitizeParams,
validateRequiredFields,
} from "./tool-registry.mjs";
const REPO_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..", "..");
const MAX_LOG_ENTRIES = 800;
function nowIso() {
return new Date().toISOString();
}
function summarizeParams(toolId, params) {
if (toolId === "llm.probe") {
return `baseUrl=${params.baseUrl}`;
}
return "";
}
export class RunManager {
constructor(historyStore) {
this.historyStore = historyStore;
this.runs = new Map();
this.nextId = 1;
}
getRun(runId) {
return this.runs.get(runId) || null;
}
appendLog(run, stream, chunk) {
const normalized = String(chunk || "").replace(/\r\n/g, "\n");
const pieces = normalized.split("\n");
for (const piece of pieces) {
if (!piece) continue;
run.logs.push({
timestamp: nowIso(),
stream,
text: piece,
});
}
if (run.logs.length > MAX_LOG_ENTRIES) {
run.logs.splice(0, run.logs.length - MAX_LOG_ENTRIES);
}
}
async execute({ toolId, params: rawParams = {}, confirmed = false }) {
const tool = getToolDefinition(toolId);
if (!tool) {
const error = new Error(`Unknown tool: ${toolId}`);
error.statusCode = 404;
throw error;
}
if (tool.virtual) {
const error = new Error("Virtual tools cannot be executed as scripts.");
error.statusCode = 400;
throw error;
}
if (tool.longRunning) {
const error = new Error("Long-running tools must use dedicated proxy endpoints.");
error.statusCode = 400;
throw error;
}
const params = sanitizeParams(tool, rawParams);
const missing = validateRequiredFields(tool, params);
if (missing.length > 0) {
const error = new Error(`Missing required fields: ${missing.join(", ")}`);
error.statusCode = 400;
throw error;
}
if (requiresConfirmation(tool, params) && !confirmed) {
const error = new Error("Confirmation required before running this tool.");
error.statusCode = 409;
throw error;
}
const runId = `run-${Date.now()}-${this.nextId++}`;
const commandPreview = buildCommandPreview(tool, params, {
hiddenFields: ["apiKey", "key", "localApiKey"],
});
const run = {
id: runId,
toolId,
status: "queued",
createdAt: nowIso(),
startedAt: null,
finishedAt: null,
exitCode: null,
commandPreview,
params,
logs: [],
error: null,
};
this.runs.set(runId, run);
const child = spawn(tool.command, buildCliArgs(tool, params), {
cwd: REPO_ROOT,
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
});
run.status = "running";
run.startedAt = nowIso();
run.childPid = child.pid;
child.stdout.on("data", (chunk) => {
this.appendLog(run, "stdout", chunk.toString("utf8"));
});
child.stderr.on("data", (chunk) => {
this.appendLog(run, "stderr", chunk.toString("utf8"));
});
child.on("error", async (error) => {
run.status = "failed";
run.finishedAt = nowIso();
run.error = error?.message || String(error);
this.appendLog(run, "stderr", run.error);
await this.historyStore.add({
id: run.id,
toolId,
status: run.status,
exitCode: run.exitCode,
createdAt: run.createdAt,
startedAt: run.startedAt,
finishedAt: run.finishedAt,
commandPreview: run.commandPreview,
paramsSummary: summarizeParams(toolId, params),
});
});
child.on("close", async (code) => {
run.exitCode = code ?? 1;
run.finishedAt = nowIso();
run.status = run.exitCode === 0 ? "succeeded" : "failed";
await this.historyStore.add({
id: run.id,
toolId,
status: run.status,
exitCode: run.exitCode,
createdAt: run.createdAt,
startedAt: run.startedAt,
finishedAt: run.finishedAt,
commandPreview: run.commandPreview,
paramsSummary: summarizeParams(toolId, params),
});
});
return {
runId,
status: run.status,
commandPreview,
};
}
}