// Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai"; import { formatError } from "@gadget/ai"; import { GadgetTool } from "@gadget/ai-toolbox"; import SubProcessService from "../../services/subprocess.ts"; const VALID_CMDS = [ "create", "list", "kill", "killAll", "halt", "log", ] as const; type SubprocessCmd = (typeof VALID_CMDS)[number]; export class SubprocessTool extends GadgetTool { get name(): string { return "subprocess"; } get category(): string { return "system"; } get definition(): IToolDefinition { return { type: "function", function: { name: this.name, description: "Manage child processes spawned by this agent. Use 'create' to spawn a new process, 'list' to see running processes, 'kill' to stop a process and remove its log files, 'halt' to stop a process while retaining its log files for later inspection, 'killAll' to stop all running processes, and 'log' to read stdout or stderr output from a running or halted process.", parameters: { type: "object", properties: { cmd: { type: "string", enum: [...VALID_CMDS], description: "The subprocess command to execute. create: spawn a new process. list: show all managed processes. kill: terminate a process and remove its logs. halt: terminate a process but keep its logs. killAll: terminate all managed processes. log: read stdout or stderr output from a process.", }, command: { type: "string", description: "The shell command to execute (required for cmd=create). Example: 'node', 'npm', 'python', './my-server'.", }, args: { type: "array", items: { type: "string" }, description: "Command-line arguments for the process (optional, used with cmd=create). Example: ['run', 'dev'].", }, pid: { type: "number", description: "Process ID of the managed subprocess (required for cmd=kill, cmd=halt, cmd=log).", }, which: { type: "string", enum: ["stdout", "stderr"], description: "Which log stream to read (required for cmd=log). 'stdout' for standard output, 'stderr' for standard error.", }, }, required: ["cmd"], }, }, }; } async execute(args: IToolArguments, logger: IAiLogger): Promise { const cmd = args.cmd as string | undefined; if (!cmd) { return formatError({ code: "MISSING_PARAMETER", message: "The 'cmd' parameter is required.", parameter: "cmd", expected: `One of: ${VALID_CMDS.join(", ")}`, recoveryHint: `Specify a valid command: ${VALID_CMDS.join(", ")}.`, }); } if (!VALID_CMDS.includes(cmd as SubprocessCmd)) { return formatError({ code: "INVALID_PARAMETER", message: `Invalid cmd: '${cmd}'. Must be one of: ${VALID_CMDS.join(", ")}`, parameter: "cmd", expected: `One of: ${VALID_CMDS.join(", ")}`, recoveryHint: `Use one of: ${VALID_CMDS.join(", ")}.`, }); } switch (cmd) { case "create": return this.cmdCreate(args, logger); case "list": return this.cmdList(args, logger); case "kill": return this.cmdKill(args, logger); case "killAll": return this.cmdKillAll(args, logger); case "halt": return this.cmdHalt(args, logger); case "log": return this.cmdLog(args, logger); default: return formatError({ code: "INVALID_PARAMETER", message: `Unknown subprocess command: '${cmd}'`, parameter: "cmd", expected: `One of: ${VALID_CMDS.join(", ")}`, }); } } private async cmdCreate( args: IToolArguments, logger: IAiLogger, ): Promise { const command = args.command as string | undefined; if (!command || typeof command !== "string" || command.trim().length === 0) { return formatError({ code: "MISSING_PARAMETER", message: "The 'command' parameter is required for cmd=create.", parameter: "command", recoveryHint: "Provide the command to execute, e.g. 'node', 'npm', 'python'.", }); } const rawArgs = args.args; const cmdArgs: string[] | undefined = Array.isArray(rawArgs) ? rawArgs.map((a) => String(a)) : undefined; const project = this.toolbox.env.workspace?.projectDir ? { _id: "workspace", slug: this.toolbox.env.workspace.projectDir.split("/").pop() || "project" } : { _id: "workspace", slug: "project" }; const projectObj = { _id: project._id, slug: project.slug, name: project.slug, createdAt: new Date(), updatedAt: new Date(), createdBy: "system", }; try { const sp = await SubProcessService.create(projectObj as any, command, cmdArgs); return [ "SUBPROCESS CREATED", `pid: ${sp.pid}`, `stdout: ${sp.stdoutFileName}`, `stderr: ${sp.stderrFileName}`, ].join("\n"); } catch (error) { const message = error instanceof Error ? error.message : String(error); logger.error("failed to create subprocess", { command, args: cmdArgs, error: message, }); return formatError({ code: "OPERATION_FAILED", message: `Failed to spawn subprocess: ${message}`, }); } } private async cmdList( _args: IToolArguments, _logger: IAiLogger, ): Promise { const processes = SubProcessService.ps(); if (processes.length === 0) { return "SUBPROCESS LIST\n(no running subprocesses)"; } const lines = ["SUBPROCESS LIST"]; for (const sp of processes) { lines.push( `pid: ${sp.pid} | project: ${sp.project.slug} | created: ${sp.createdAt.toISOString()} | updated: ${sp.updatedAt.toISOString()}`, ); } return lines.join("\n"); } private async cmdKill( args: IToolArguments, logger: IAiLogger, ): Promise { const pid = args.pid as number | undefined; if (typeof pid !== "number" || !Number.isFinite(pid)) { return formatError({ code: "MISSING_PARAMETER", message: "The 'pid' parameter is required for cmd=kill.", parameter: "pid", recoveryHint: "Provide the numeric process ID to kill.", }); } try { await SubProcessService.killPid(pid); return `SUBPROCESS KILLED\npid: ${pid}`; } catch (error) { const message = error instanceof Error ? error.message : String(error); logger.error("failed to kill subprocess", { pid, error: message }); return formatError({ code: "OPERATION_FAILED", message: `Failed to kill subprocess: ${message}`, }); } } private async cmdKillAll( _args: IToolArguments, logger: IAiLogger, ): Promise { try { await SubProcessService.killAll(); return "SUBPROCESS KILLALL\nAll managed subprocesses have been terminated and their logs removed."; } catch (error) { const message = error instanceof Error ? error.message : String(error); logger.error("failed to kill all subprocesses", { error: message }); return formatError({ code: "OPERATION_FAILED", message: `Failed to kill all subprocesses: ${message}`, }); } } private async cmdHalt( args: IToolArguments, logger: IAiLogger, ): Promise { const pid = args.pid as number | undefined; if (typeof pid !== "number" || !Number.isFinite(pid)) { return formatError({ code: "MISSING_PARAMETER", message: "The 'pid' parameter is required for cmd=halt.", parameter: "pid", recoveryHint: "Provide the numeric process ID to halt.", }); } try { await SubProcessService.haltPid(pid); return [ "SUBPROCESS HALTED", `pid: ${pid}`, "The process has been stopped. Log files are retained for inspection.", ].join("\n"); } catch (error) { const message = error instanceof Error ? error.message : String(error); logger.error("failed to halt subprocess", { pid, error: message }); return formatError({ code: "OPERATION_FAILED", message: `Failed to halt subprocess: ${message}`, }); } } private async cmdLog( args: IToolArguments, logger: IAiLogger, ): Promise { const pid = args.pid as number | undefined; if (typeof pid !== "number" || !Number.isFinite(pid)) { return formatError({ code: "MISSING_PARAMETER", message: "The 'pid' parameter is required for cmd=log.", parameter: "pid", recoveryHint: "Provide the numeric process ID to read logs from.", }); } const which = args.which as string | undefined; if (!which || (which !== "stdout" && which !== "stderr")) { return formatError({ code: "MISSING_PARAMETER", message: "The 'which' parameter is required for cmd=log.", parameter: "which", expected: "'stdout' or 'stderr'", recoveryHint: "Specify 'stdout' to read standard output or 'stderr' to read standard error.", }); } try { const content = await SubProcessService.getLog(pid, which); return [ `SUBPROCESS LOG (${which})`, `pid: ${pid}`, "---", content || "(log is empty)", ].join("\n"); } catch (error) { const message = error instanceof Error ? error.message : String(error); logger.error("failed to read subprocess log", { pid, which, error: message, }); return formatError({ code: "OPERATION_FAILED", message: `Failed to read subprocess log: ${message}`, }); } } }