UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

109 lines (108 loc) 4.41 kB
/** * Experiment runner — spawn, timeout, capture. */ import { spawn } from "node:child_process"; import { writeFileSync } from "node:fs"; import path from "node:path"; import { logger } from "../utils/logger.js"; import { parseExperimentSummary } from "./summaryParser.js"; export class ExperimentRunner { config; constructor(config) { this.config = config; } /** Runs the experiment with hard timeout, returns summary */ async run() { const logPath = path.join(this.config.repoPath, this.config.logPath); // Redact potential inline env vars or tokens before logging. // sanitizeForLog handles in-string `Bearer`/`Token`/`sk-` patterns but // not CLI flag forms like `--token foo` (which is a different shape — // separate argv tokens rather than concatenated values). The flag-form // redactor below is narrowly scoped; not the H04 anti-pattern. const redactedCmd = this.config.runCommand .replace(/[A-Z_]+=\S+\s/g, (m) => m.split("=")[0] + "=*** ") .replace( // eslint-disable-next-line neurolink/no-inline-secret-regex -- narrowly-scoped CLI-flag redactor /--(?:token|key|secret|password)\s+\S+/gi, (m) => m.split(/\s+/)[0] + " ***"); logger.info("[Autoresearch] Starting experiment", { command: redactedCmd, timeoutMs: this.config.timeoutMs, }); // eslint-disable-next-line no-useless-assignment -- catch block assigns on spawn failure let logContent = ""; let timedOut = false; let exitCode = 0; try { logContent = await new Promise((resolve, reject) => { let output = ""; const proc = spawn(this.config.runCommand, { shell: true, cwd: this.config.repoPath, stdio: ["ignore", "pipe", "pipe"], }); // Capture stdout and stderr proc.stdout?.on("data", (chunk) => { output += chunk.toString(); }); proc.stderr?.on("data", (chunk) => { output += chunk.toString(); }); // Hard timeout const timer = setTimeout(() => { timedOut = true; try { proc.kill("SIGKILL"); } catch { // Process may have already exited } }, this.config.timeoutMs); proc.on("close", (code, signal) => { clearTimeout(timer); // Signal-terminated (SIGKILL, SIGSEGV, etc.) = crash unless we timed it out if (signal && !timedOut) { exitCode = 1; } else { exitCode = code ?? 0; } logger.debug("[Autoresearch] Experiment process exited", { code, signal, exitCode, timedOut, }); resolve(output); }); proc.on("error", (error) => { clearTimeout(timer); reject(error); }); }); } catch (error) { // Spawn failure — treat as crash with non-zero exit code const errorMsg = error instanceof Error ? error.message : String(error); logContent = `SPAWN ERROR: ${errorMsg}\nFAIL`; exitCode = 1; logger.error("[Autoresearch] Experiment spawn failed", { error: errorMsg, }); } // Write log to file try { writeFileSync(logPath, logContent, "utf-8"); } catch (writeError) { logger.warn("[Autoresearch] Failed to write run.log", { error: writeError instanceof Error ? writeError.message : String(writeError), }); } // Parse summary with exit code and timeout info const summary = parseExperimentSummary(logContent, this.config.metric, this.config.memoryMetric, { timedOut, exitCode, }); return summary; } }