{"version":3,"file":"external-cli-runner.d.ts","sourceRoot":"","sources":["../../../../src/runs/shared/external-cli-runner.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAGtF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAOzD,wBAAgB,sBAAsB,CAAC,kBAAkB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvF;AAED,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAAC,iBAAiB,EAAE;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,CAAC,CAAC;AAE3F,MAAM,WAAW,qBAAqB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,KAAK,IAAI,CAAC;IAC3D,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,KAAK,IAAI,CAAC;IACxD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,qBAAqB,KAAK,IAAI,CAAC;CACrD;AAED,MAAM,WAAW,oBAAoB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,eAAe,EAAE,qBAAqB,CAAC;CACvC;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA0H1F;AAED,qBAAa,iBAAkB,YAAW,aAAa,CAAC,qBAAqB,EAAE,oBAAoB,CAAC;IACnG,QAAQ,CAAC,IAAI,kBAAkB;IAC/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;IAEjD,YAAY,MAAM,EAAE,uBAAuB,EAG1C;IAEK,GAAG,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAkBrE;CACD","sourcesContent":["import { spawn } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { finished } from \"node:stream/promises\";\nimport { trySignalChild } from \"../../shared/post-exit-stdio-guard.ts\";\nimport type { ExternalProcessStatus, AgentRunnerConfig } from \"../../shared/types.ts\";\nimport { parseRunnerRetryConfig } from \"../../shared/runner-config.ts\";\nimport { waitForSubagentStartupRetry } from \"./subagent-startup-retry.ts\";\nimport type { RunnerAdapter } from \"./runner-adapter.ts\";\n\nconst HARD_KILL_DELAY_MS = 2_000;\nconst MAX_OUTPUT_TAIL_BYTES = 64 * 1024;\nconst MAX_ERROR_TAIL_BYTES = 64 * 1024;\nconst DEFAULT_RETRY_BACKOFF_MS = 250;\n\nexport function buildExternalCliPrompt(systemInstructions: string, task: string): string {\n\treturn `<System instructions>\\n${systemInstructions.trim()}\\n\\n<Task>\\n${task}`;\n}\n\nexport type ExternalCliRunnerConfig = Extract<AgentRunnerConfig, { type: \"external-cli\" }>;\n\nexport interface ExternalCliRunRequest {\n\tcommand: string;\n\targs?: string[];\n\tcwd: string;\n\tprompt: string;\n\tasyncDir: string;\n\tstepIndex: number;\n\tattempt?: number;\n\tmaxAttempts?: number;\n\tsignal?: AbortSignal;\n\tregisterTimeout?: (stop: (() => void) | undefined) => void;\n\tregisterStop?: (stop: (() => void) | undefined) => void;\n\ttimeoutMessage?: string;\n\tstopMessage?: string;\n\tonProcess?: (process: ExternalProcessStatus) => void;\n}\n\nexport interface ExternalCliRunResult {\n\toutput: string;\n\texitCode: number | null;\n\terror?: string;\n\ttimedOut?: boolean;\n\tstopped?: boolean;\n\tprocessSignal?: string | null;\n\texternalProcess: ExternalProcessStatus;\n}\n\nexport function runExternalCli(input: ExternalCliRunRequest): Promise<ExternalCliRunResult> {\n\tlet resolveResult!: (value: ExternalCliRunResult) => void;\n\tlet rejectResult!: (reason?: unknown) => void;\n\tconst promise = new Promise<ExternalCliRunResult>((resolve, reject) => {\n\t\tresolveResult = resolve;\n\t\trejectResult = reject;\n\t});\n\tconst startedAt = Date.now();\n\tconst attempt = input.attempt ?? 0;\n\tconst suffix = attempt > 0 ? `-retry-${attempt}` : \"\";\n\tconst stdoutPath = path.join(input.asyncDir, `external-${input.stepIndex}${suffix}.stdout.log`);\n\tconst stderrPath = path.join(input.asyncDir, `external-${input.stepIndex}${suffix}.stderr.log`);\n\tfs.mkdirSync(input.asyncDir, { recursive: true });\n\tconst stdoutStream = fs.createWriteStream(stdoutPath, { flags: \"w\" });\n\tconst stderrStream = fs.createWriteStream(stderrPath, { flags: \"w\" });\n\tconst streamsFinished = Promise.allSettled([finished(stdoutStream), finished(stderrStream)]);\n\tlet stdoutTail = Buffer.alloc(0);\n\tlet stderrTail = Buffer.alloc(0);\n\tlet timedOut = false;\n\tlet stopped = false;\n\tlet settled = false;\n\tlet hardKillTimer: NodeJS.Timeout | undefined;\n\tconst child = spawn(input.command, input.args ?? [], {\n\t\tcwd: input.cwd,\n\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\tshell: false,\n\t\twindowsHide: true,\n\t});\n\tconst initialProcess: ExternalProcessStatus = {\n\t\t...(typeof child.pid === \"number\" ? { pid: child.pid } : {}),\n\t\t...(input.attempt !== undefined ? { attempt: input.attempt + 1 } : {}),\n\t\t...(input.maxAttempts !== undefined ? { maxAttempts: input.maxAttempts } : {}),\n\t\tstartedAt,\n\t\tstdoutPath,\n\t\tstderrPath,\n\t};\n\tinput.onProcess?.(initialProcess);\n\tchild.stdout.on(\"data\", (chunk: Buffer) => {\n\t\tstdoutStream.write(chunk);\n\t\tstdoutTail = Buffer.concat([stdoutTail, chunk]);\n\t\tif (stdoutTail.length > MAX_OUTPUT_TAIL_BYTES)\n\t\t\tstdoutTail = stdoutTail.subarray(stdoutTail.length - MAX_OUTPUT_TAIL_BYTES);\n\t});\n\tchild.stderr.on(\"data\", (chunk: Buffer) => {\n\t\tstderrStream.write(chunk);\n\t\tstderrTail = Buffer.concat([stderrTail, chunk]);\n\t\tif (stderrTail.length > MAX_ERROR_TAIL_BYTES)\n\t\t\tstderrTail = stderrTail.subarray(stderrTail.length - MAX_ERROR_TAIL_BYTES);\n\t});\n\tconst terminate = (reason: \"timeout\" | \"stop\") => {\n\t\tif (settled || timedOut || stopped) return;\n\t\ttimedOut = reason === \"timeout\";\n\t\tstopped = reason === \"stop\";\n\t\ttrySignalChild(child, \"SIGTERM\");\n\t\thardKillTimer = setTimeout(() => {\n\t\t\tif (!settled) trySignalChild(child, \"SIGKILL\");\n\t\t}, HARD_KILL_DELAY_MS);\n\t\thardKillTimer.unref?.();\n\t};\n\tconst hasLifecycleRegistration = input.registerTimeout !== undefined || input.registerStop !== undefined;\n\tconst abortSignal = input.signal;\n\tconst abortListener = () => {\n\t\tif (!hasLifecycleRegistration) {\n\t\t\tterminate(\"stop\");\n\t\t\treturn;\n\t\t}\n\t\tqueueMicrotask(() => {\n\t\t\tif (!settled && !timedOut && !stopped) terminate(\"stop\");\n\t\t});\n\t};\n\tif (abortSignal) {\n\t\tif (abortSignal.aborted) abortListener();\n\t\telse abortSignal.addEventListener(\"abort\", abortListener, { once: true });\n\t}\n\tinput.registerTimeout?.(() => terminate(\"timeout\"));\n\tinput.registerStop?.(() => terminate(\"stop\"));\n\tlet spawnError: Error | undefined;\n\tchild.once(\"error\", (error) => {\n\t\tspawnError = error;\n\t});\n\tchild.stdin.on(\"error\", () => {});\n\tchild.stdin.end(input.prompt);\n\tchild.once(\"close\", (exitCode, signal) => {\n\t\tsettled = true;\n\t\tif (hardKillTimer) clearTimeout(hardKillTimer);\n\t\tif (abortSignal) abortSignal.removeEventListener(\"abort\", abortListener);\n\t\tinput.registerTimeout?.(undefined);\n\t\tinput.registerStop?.(undefined);\n\t\tconst endedAt = Date.now();\n\t\tconst externalProcess: ExternalProcessStatus = {\n\t\t\t...initialProcess,\n\t\t\tendedAt,\n\t\t\tdurationMs: endedAt - startedAt,\n\t\t\texitCode,\n\t\t\tprocessSignal: signal,\n\t\t};\n\t\tinput.onProcess?.(externalProcess);\n\t\tconst stderr = stderrTail.toString(\"utf-8\").trim();\n\t\tconst error = stopped\n\t\t\t? (input.stopMessage ?? \"Subagent stopped by user.\")\n\t\t\t: timedOut\n\t\t\t\t? (input.timeoutMessage ?? \"Subagent timed out.\")\n\t\t\t\t: (spawnError?.message ??\n\t\t\t\t\t(exitCode === 0 ? undefined : stderr || `External CLI exited with code ${exitCode}.`));\n\t\tconst result: ExternalCliRunResult = {\n\t\t\toutput: stdoutTail.toString(\"utf-8\").trim(),\n\t\t\texitCode: timedOut || stopped || spawnError ? 1 : exitCode,\n\t\t\t...(error ? { error } : {}),\n\t\t\t...(timedOut ? { timedOut: true } : {}),\n\t\t\t...(stopped ? { stopped: true } : {}),\n\t\t\tprocessSignal: signal,\n\t\t\texternalProcess,\n\t\t};\n\t\tstdoutStream.end();\n\t\tstderrStream.end();\n\t\tvoid streamsFinished.then((streamResults) => {\n\t\t\tconst streamFailure = streamResults.find((streamResult) => streamResult.status === \"rejected\");\n\t\t\tif (streamFailure?.status === \"rejected\") rejectResult(streamFailure.reason);\n\t\t\telse resolveResult(result);\n\t\t});\n\t});\n\treturn promise;\n}\n\nexport class ExternalCliRunner implements RunnerAdapter<ExternalCliRunRequest, ExternalCliRunResult> {\n\treadonly type = \"external-cli\";\n\tprivate readonly config: ExternalCliRunnerConfig;\n\n\tconstructor(config: ExternalCliRunnerConfig) {\n\t\tconst retry = parseRunnerRetryConfig(config.retry, \"External CLI retry\");\n\t\tthis.config = retry ? { ...config, retry } : config;\n\t}\n\n\tasync run(input: ExternalCliRunRequest): Promise<ExternalCliRunResult> {\n\t\tconst retry = this.config.retry;\n\t\tconst maxAttempts = retry?.maxAttempts ?? 1;\n\t\tconst backoffMs = retry?.backoffMs ?? DEFAULT_RETRY_BACKOFF_MS;\n\t\tlet result: ExternalCliRunResult | undefined;\n\t\tfor (let attempt = 0; attempt < maxAttempts; attempt++) {\n\t\t\tresult = await runExternalCli({\n\t\t\t\t...input,\n\t\t\t\tcommand: this.config.command,\n\t\t\t\targs: this.config.args,\n\t\t\t\tattempt,\n\t\t\t\tmaxAttempts,\n\t\t\t});\n\t\t\tif (result.exitCode === 0 || result.timedOut || result.stopped || attempt + 1 >= maxAttempts) return result;\n\t\t\tconst shouldRetry = await waitForSubagentStartupRetry(backoffMs, [input.signal]);\n\t\t\tif (!shouldRetry) return result;\n\t\t}\n\t\treturn result!;\n\t}\n}\n"]}