Spaces:
Sleeping
Sleeping
| 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, | |
| }; | |
| } | |
| } | |