co-mono/packages/coding-agent/src/tools/bash.ts
Mario Zechner 159075cad7 Improve tool execution rendering and error handling
- Show tool execution components immediately when tool calls appear in streaming
- Update components with streaming arguments as they come in
- Handle incomplete/partial arguments gracefully with optional chaining
- Fix error handling: tools now throw exceptions instead of returning error messages
- Fix bash abort handling to properly reject on abort/timeout
- Clean up error display
2025-11-11 23:05:58 +01:00

128 lines
3.1 KiB
TypeScript

import type { AgentTool } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
import { spawn } from "child_process";
const bashSchema = Type.Object({
command: Type.String({ description: "Bash command to execute" }),
});
export const bashTool: AgentTool<typeof bashSchema> = {
name: "bash",
label: "bash",
description:
"Execute a bash command in the current working directory. Returns stdout and stderr. Commands run with a 30 second timeout.",
parameters: bashSchema,
execute: async (_toolCallId: string, { command }: { command: string }, signal?: AbortSignal) => {
return new Promise((resolve, reject) => {
const child = spawn("sh", ["-c", command], {
detached: true,
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
let timedOut = false;
// Set timeout
const timeout = setTimeout(() => {
timedOut = true;
onAbort();
}, 30000);
// Collect stdout
if (child.stdout) {
child.stdout.on("data", (data) => {
stdout += data.toString();
// Limit buffer size
if (stdout.length > 10 * 1024 * 1024) {
stdout = stdout.slice(0, 10 * 1024 * 1024);
}
});
}
// Collect stderr
if (child.stderr) {
child.stderr.on("data", (data) => {
stderr += data.toString();
// Limit buffer size
if (stderr.length > 10 * 1024 * 1024) {
stderr = stderr.slice(0, 10 * 1024 * 1024);
}
});
}
// Handle process exit
child.on("close", (code) => {
clearTimeout(timeout);
if (signal) {
signal.removeEventListener("abort", onAbort);
}
if (signal?.aborted) {
let output = "";
if (stdout) output += stdout;
if (stderr) {
if (output) output += "\n";
output += stderr;
}
if (output) output += "\n\n";
output += "Command aborted";
reject(new Error(output));
return;
}
if (timedOut) {
let output = "";
if (stdout) output += stdout;
if (stderr) {
if (output) output += "\n";
output += stderr;
}
if (output) output += "\n\n";
output += "Command timed out after 30 seconds";
reject(new Error(output));
return;
}
let output = "";
if (stdout) output += stdout;
if (stderr) {
if (output) output += "\n";
output += stderr;
}
if (code !== 0 && code !== null) {
if (output) output += "\n\n";
reject(new Error(`${output}Command exited with code ${code}`));
} else {
resolve({ output: output || "(no output)", details: undefined });
}
});
// Handle abort signal - kill entire process tree
const onAbort = () => {
if (child.pid) {
// Kill the entire process group (negative PID kills all processes in the group)
try {
process.kill(-child.pid, "SIGKILL");
} catch (e) {
// Fallback to killing just the child if process group kill fails
try {
child.kill("SIGKILL");
} catch (e2) {
// Process already dead
}
}
}
};
if (signal) {
if (signal.aborted) {
onAbort();
} else {
signal.addEventListener("abort", onAbort, { once: true });
}
}
});
},
};