feat(truncate): allow configuring tool output truncation limits (#23770)
Co-authored-by: rgs_ramp <rgs@ramp.com> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
This commit is contained in:
@@ -201,6 +201,19 @@ export const Info = Schema.Struct({
|
||||
url: Schema.optional(Schema.String).annotate({ description: "Enterprise URL" }),
|
||||
}),
|
||||
),
|
||||
tool_output: Schema.optional(
|
||||
Schema.Struct({
|
||||
max_lines: Schema.optional(PositiveInt).annotate({
|
||||
description: "Maximum lines of tool output before it is truncated and saved to disk (default: 2000)",
|
||||
}),
|
||||
max_bytes: Schema.optional(PositiveInt).annotate({
|
||||
description: "Maximum bytes of tool output before it is truncated and saved to disk (default: 51200)",
|
||||
}),
|
||||
}),
|
||||
).annotate({
|
||||
description:
|
||||
"Thresholds for truncating tool output. When output exceeds either limit, the full text is written to the truncation directory and a preview is returned.",
|
||||
}),
|
||||
compaction: Schema.optional(
|
||||
Schema.Struct({
|
||||
auto: Schema.optional(Schema.Boolean).annotate({
|
||||
|
||||
@@ -416,9 +416,8 @@ export const BashTool = Tool.define(
|
||||
},
|
||||
ctx: Tool.Context,
|
||||
) {
|
||||
const bytes = Truncate.MAX_BYTES
|
||||
const lines = Truncate.MAX_LINES
|
||||
const keep = bytes * 2
|
||||
const limits = yield* trunc.limits()
|
||||
const keep = limits.maxBytes * 2
|
||||
let full = ""
|
||||
let last = ""
|
||||
const list: Chunk[] = []
|
||||
@@ -458,7 +457,7 @@ export const BashTool = Tool.define(
|
||||
sink?.write(chunk)
|
||||
} else {
|
||||
full += chunk
|
||||
if (Buffer.byteLength(full, "utf-8") > bytes) {
|
||||
if (Buffer.byteLength(full, "utf-8") > limits.maxBytes) {
|
||||
return trunc.write(full).pipe(
|
||||
Effect.andThen((next) =>
|
||||
Effect.sync(() => {
|
||||
@@ -525,7 +524,7 @@ export const BashTool = Tool.define(
|
||||
}
|
||||
if (aborted) meta.push("User aborted the command")
|
||||
const raw = list.map((item) => item.text).join("")
|
||||
const end = tail(raw, lines, bytes)
|
||||
const end = tail(raw, limits.maxLines, limits.maxBytes)
|
||||
if (end.cut) cut = true
|
||||
if (!file && end.cut) {
|
||||
file = yield* trunc.write(raw)
|
||||
@@ -566,7 +565,7 @@ export const BashTool = Tool.define(
|
||||
})
|
||||
|
||||
return () =>
|
||||
Effect.sync(() => {
|
||||
Effect.gen(function* () {
|
||||
const shell = Shell.acceptable()
|
||||
const name = Shell.name(shell)
|
||||
const chain =
|
||||
@@ -575,13 +574,15 @@ export const BashTool = Tool.define(
|
||||
: "If the commands depend on each other and must run sequentially, use a single Bash call with '&&' to chain them together (e.g., `git add . && git commit -m \"message\" && git push`). For instance, if one operation must complete before another starts (like mkdir before cp, Write before Bash for git operations, or git add before git commit), run these operations sequentially instead."
|
||||
log.info("bash tool using shell", { shell })
|
||||
|
||||
const limits = yield* trunc.limits()
|
||||
|
||||
return {
|
||||
description: DESCRIPTION.replaceAll("${directory}", Instance.directory)
|
||||
.replaceAll("${os}", process.platform)
|
||||
.replaceAll("${shell}", name)
|
||||
.replaceAll("${chaining}", chain)
|
||||
.replaceAll("${maxLines}", String(Truncate.MAX_LINES))
|
||||
.replaceAll("${maxBytes}", String(Truncate.MAX_BYTES)),
|
||||
.replaceAll("${maxLines}", String(limits.maxLines))
|
||||
.replaceAll("${maxBytes}", String(limits.maxBytes)),
|
||||
parameters: Parameters,
|
||||
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { NodePath } from "@effect/platform-node"
|
||||
import { Cause, Duration, Effect, Layer, Schedule, Context } from "effect"
|
||||
import { Cause, Duration, Effect, Layer, Option, Schedule, Context } from "effect"
|
||||
import path from "path"
|
||||
import type { Agent } from "../agent/agent"
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
import { evaluate } from "@/permission/evaluate"
|
||||
import { Config } from "../config"
|
||||
import { Identifier } from "../id/id"
|
||||
import { Log } from "../util"
|
||||
import { ToolID } from "./schema"
|
||||
@@ -38,6 +39,10 @@ export interface Interface {
|
||||
* to the truncation directory and returns a preview plus a hint to inspect the saved file.
|
||||
*/
|
||||
readonly output: (text: string, options?: Options, agent?: Agent.Info) => Effect.Effect<Result>
|
||||
/**
|
||||
* Resolved truncation limits: values from `tool_output` in opencode config, or MAX_LINES / MAX_BYTES if unset.
|
||||
*/
|
||||
readonly limits: () => Effect.Effect<{ maxLines: number; maxBytes: number }>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Truncate") {}
|
||||
@@ -68,9 +73,20 @@ export const layer = Layer.effect(
|
||||
return file
|
||||
})
|
||||
|
||||
const limits = Effect.fn("Truncate.limits")(function* () {
|
||||
const configSvc = yield* Effect.serviceOption(Config.Service)
|
||||
if (Option.isNone(configSvc)) return { maxLines: MAX_LINES, maxBytes: MAX_BYTES }
|
||||
const cfg = yield* configSvc.value.get().pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
return {
|
||||
maxLines: cfg?.tool_output?.max_lines ?? MAX_LINES,
|
||||
maxBytes: cfg?.tool_output?.max_bytes ?? MAX_BYTES,
|
||||
}
|
||||
})
|
||||
|
||||
const output = Effect.fn("Truncate.output")(function* (text: string, options: Options = {}, agent?: Agent.Info) {
|
||||
const maxLines = options.maxLines ?? MAX_LINES
|
||||
const maxBytes = options.maxBytes ?? MAX_BYTES
|
||||
const resolved = yield* limits()
|
||||
const maxLines = options.maxLines ?? resolved.maxLines
|
||||
const maxBytes = options.maxBytes ?? resolved.maxBytes
|
||||
const direction = options.direction ?? "head"
|
||||
const lines = text.split("\n")
|
||||
const totalBytes = Buffer.byteLength(text, "utf-8")
|
||||
@@ -135,7 +151,7 @@ export const layer = Layer.effect(
|
||||
Effect.forkScoped,
|
||||
)
|
||||
|
||||
return Service.of({ cleanup, write, output })
|
||||
return Service.of({ cleanup, write, output, limits })
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user