co-mono/packages/coding-agent/src/tools/bash.ts
Mario Zechner 84dcab219b Add image support in tool results across all providers
Tool results now use content blocks and can include both text and images.
All providers (Anthropic, Google, OpenAI Completions, OpenAI Responses)
correctly pass images from tool results to LLMs.

- Update ToolResultMessage type to use content blocks
- Add placeholder text for image-only tool results in Google/Anthropic
- OpenAI providers send tool result + follow-up user message with images
- Fix Anthropic JSON parsing for empty tool arguments
- Add comprehensive tests for image-only and text+image tool results
- Update README with tool result content blocks API
2025-11-12 10:45:56 +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({ content: [{ type: "text", text: 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 });
}
}
});
},
};