pull/7239/head
Aiden Cline 2026-01-07 13:44:29 -06:00
parent ff2c1c4267
commit 2e0c43966c
2 changed files with 99 additions and 19 deletions

View File

@ -16,7 +16,6 @@ import { Shell } from "@/shell/shell"
import { BashArity } from "@/permission/arity"
const MAX_OUTPUT_LENGTH = Flag.OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH || 30_000
const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000
export const log = Log.create({ service: "bash-tool" })
@ -172,15 +171,13 @@ export const BashTool = Tool.define("bash", async () => {
})
const append = (chunk: Buffer) => {
if (output.length <= MAX_OUTPUT_LENGTH) {
output += chunk.toString()
ctx.metadata({
metadata: {
output,
description: params.description,
},
})
}
output += chunk.toString()
ctx.metadata({
metadata: {
output,
description: params.description,
},
})
}
proc.stdout?.on("data", append)
@ -228,12 +225,7 @@ export const BashTool = Tool.define("bash", async () => {
})
})
let resultMetadata: String[] = ["<bash_metadata>"]
if (output.length > MAX_OUTPUT_LENGTH) {
output = output.slice(0, MAX_OUTPUT_LENGTH)
resultMetadata.push(`bash tool truncated output as it exceeded ${MAX_OUTPUT_LENGTH} char limit`)
}
const resultMetadata: string[] = []
if (timedOut) {
resultMetadata.push(`bash tool terminated command after exceeding timeout ${timeout} ms`)
@ -243,9 +235,8 @@ export const BashTool = Tool.define("bash", async () => {
resultMetadata.push("User aborted the command")
}
if (resultMetadata.length > 1) {
resultMetadata.push("</bash_metadata>")
output += "\n\n" + resultMetadata.join("\n")
if (resultMetadata.length > 0) {
output += "\n\n<bash_metadata>\n" + resultMetadata.join("\n") + "\n</bash_metadata>"
}
return {

View File

@ -4,6 +4,7 @@ import { BashTool } from "../../src/tool/bash"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
import type { PermissionNext } from "../../src/permission/next"
import { Truncate } from "../../src/tool/truncation"
const ctx = {
sessionID: "test",
@ -230,3 +231,91 @@ describe("tool.bash permissions", () => {
})
})
})
describe("tool.bash truncation", () => {
test("truncates output exceeding line limit", async () => {
await Instance.provide({
directory: projectRoot,
fn: async () => {
const bash = await BashTool.init()
const lineCount = Truncate.MAX_LINES + 500
const result = await bash.execute(
{
command: `seq 1 ${lineCount}`,
description: "Generate lines exceeding limit",
},
ctx,
)
expect((result.metadata as any).truncated).toBe(true)
expect(result.output).toContain("truncated")
expect(result.output).toContain("Full output written to:")
},
})
})
test("truncates output exceeding byte limit", async () => {
await Instance.provide({
directory: projectRoot,
fn: async () => {
const bash = await BashTool.init()
const byteCount = Truncate.MAX_BYTES + 10000
const result = await bash.execute(
{
command: `head -c ${byteCount} /dev/zero | tr '\\0' 'a'`,
description: "Generate bytes exceeding limit",
},
ctx,
)
expect((result.metadata as any).truncated).toBe(true)
expect(result.output).toContain("truncated")
expect(result.output).toContain("Full output written to:")
},
})
})
test("does not truncate small output", async () => {
await Instance.provide({
directory: projectRoot,
fn: async () => {
const bash = await BashTool.init()
const result = await bash.execute(
{
command: "echo hello",
description: "Echo hello",
},
ctx,
)
expect((result.metadata as any).truncated).toBe(false)
expect(result.output).toBe("hello\n")
},
})
})
test("full output is saved to file when truncated", async () => {
await Instance.provide({
directory: projectRoot,
fn: async () => {
const bash = await BashTool.init()
const lineCount = Truncate.MAX_LINES + 100
const result = await bash.execute(
{
command: `seq 1 ${lineCount}`,
description: "Generate lines for file check",
},
ctx,
)
expect((result.metadata as any).truncated).toBe(true)
const match = result.output.match(/Full output written to: (.+)/)
expect(match).toBeTruthy()
const filepath = match![1].split("\n")[0]
const saved = await Bun.file(filepath).text()
const lines = saved.trim().split("\n")
expect(lines.length).toBe(lineCount)
expect(lines[0]).toBe("1")
expect(lines[lineCount - 1]).toBe(String(lineCount))
},
})
})
})