wip
parent
ff2c1c4267
commit
2e0c43966c
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue