mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-19 07:03:44 +00:00
- Add truncate.ts with 2000 lines / 50KB limits - Update bash tool with tail truncation and temp file output - Update read tool with head truncation and offset hints - Remove redundant context history truncation (tools already provide actionable hints) fixes #155
97 lines
3.6 KiB
TypeScript
97 lines
3.6 KiB
TypeScript
import { randomBytes } from "node:crypto";
|
|
import { createWriteStream } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import type { AgentTool } from "@mariozechner/pi-ai";
|
|
import { Type } from "@sinclair/typebox";
|
|
import type { Executor } from "../sandbox.js";
|
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from "./truncate.js";
|
|
|
|
/**
|
|
* Generate a unique temp file path for bash output
|
|
*/
|
|
function getTempFilePath(): string {
|
|
const id = randomBytes(8).toString("hex");
|
|
return join(tmpdir(), `mom-bash-${id}.log`);
|
|
}
|
|
|
|
const bashSchema = Type.Object({
|
|
label: Type.String({ description: "Brief description of what this command does (shown to user)" }),
|
|
command: Type.String({ description: "Bash command to execute" }),
|
|
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
|
|
});
|
|
|
|
interface BashToolDetails {
|
|
truncation?: TruncationResult;
|
|
fullOutputPath?: string;
|
|
}
|
|
|
|
export function createBashTool(executor: Executor): AgentTool<typeof bashSchema> {
|
|
return {
|
|
name: "bash",
|
|
label: "bash",
|
|
description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,
|
|
parameters: bashSchema,
|
|
execute: async (
|
|
_toolCallId: string,
|
|
{ command, timeout }: { label: string; command: string; timeout?: number },
|
|
signal?: AbortSignal,
|
|
) => {
|
|
// Track output for potential temp file writing
|
|
let tempFilePath: string | undefined;
|
|
let tempFileStream: ReturnType<typeof createWriteStream> | undefined;
|
|
|
|
const result = await executor.exec(command, { timeout, signal });
|
|
let output = "";
|
|
if (result.stdout) output += result.stdout;
|
|
if (result.stderr) {
|
|
if (output) output += "\n";
|
|
output += result.stderr;
|
|
}
|
|
|
|
const totalBytes = Buffer.byteLength(output, "utf-8");
|
|
|
|
// Write to temp file if output exceeds limit
|
|
if (totalBytes > DEFAULT_MAX_BYTES) {
|
|
tempFilePath = getTempFilePath();
|
|
tempFileStream = createWriteStream(tempFilePath);
|
|
tempFileStream.write(output);
|
|
tempFileStream.end();
|
|
}
|
|
|
|
// Apply tail truncation
|
|
const truncation = truncateTail(output);
|
|
let outputText = truncation.content || "(no output)";
|
|
|
|
// Build details with truncation info
|
|
let details: BashToolDetails | undefined;
|
|
|
|
if (truncation.truncated) {
|
|
details = {
|
|
truncation,
|
|
fullOutputPath: tempFilePath,
|
|
};
|
|
|
|
// Build actionable notice
|
|
const startLine = truncation.totalLines - truncation.outputLines + 1;
|
|
const endLine = truncation.totalLines;
|
|
|
|
if (truncation.lastLinePartial) {
|
|
// Edge case: last line alone > 50KB
|
|
const lastLineSize = formatSize(Buffer.byteLength(output.split("\n").pop() || "", "utf-8"));
|
|
outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;
|
|
} else if (truncation.truncatedBy === "lines") {
|
|
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;
|
|
} else {
|
|
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;
|
|
}
|
|
}
|
|
|
|
if (result.code !== 0) {
|
|
throw new Error(`${outputText}\n\nCommand exited with code ${result.code}`.trim());
|
|
}
|
|
|
|
return { content: [{ type: "text", text: outputText }], details };
|
|
},
|
|
};
|
|
}
|