refactor(opencode): simplify tool output disable config

This commit is contained in:
Aiden Cline
2026-05-26 15:33:08 -05:00
parent d4f83ba2b7
commit 5d929f78c3
7 changed files with 14 additions and 42 deletions
+2 -13
View File
@@ -49,13 +49,7 @@ const log = Log.create({ service: "config" })
// Custom merge function that concatenates array fields instead of replacing them // Custom merge function that concatenates array fields instead of replacing them
// Keep remeda's deep conditional merge type out of hot config-loading paths; TS profiling showed it dominates here. // Keep remeda's deep conditional merge type out of hot config-loading paths; TS profiling showed it dominates here.
function mergeConfig(target: Info, source: Info): Info { function mergeConfig(target: Info, source: Info): Info {
const merged = mergeDeep(target, source) as Info return mergeDeep(target, source) as Info
if (!target.tool_output || !source.tool_output) return merged
if (target.tool_output.truncate !== false && source.tool_output.truncate !== false) return merged
// Disabled truncation and custom limits are separate config modes; the later layer selects the mode.
merged.tool_output = source.tool_output
return merged
} }
function mergeConfigConcatArrays(target: Info, source: Info): Info { function mergeConfigConcatArrays(target: Info, source: Info): Info {
@@ -260,13 +254,8 @@ export const Info = Schema.Struct({
), ),
tool_output: Schema.optional( tool_output: Schema.optional(
Schema.Union([ Schema.Union([
Schema.Literal(false).annotate({ description: "Disable tool output truncation" }),
Schema.Struct({ Schema.Struct({
truncate: Schema.Literal(false).annotate({ description: "Disable tool output truncation" }),
}).annotate({ parseOptions: { onExcessProperty: "error" } }),
Schema.Struct({
truncate: Schema.optional(Schema.Literal(true)).annotate({
description: "Enable truncating tool output that exceeds the configured limits (default: true)",
}),
max_lines: Schema.optional(PositiveInt).annotate({ max_lines: Schema.optional(PositiveInt).annotate({
description: "Maximum lines of tool output before it is truncated and saved to disk (default: 2000)", description: "Maximum lines of tool output before it is truncated and saved to disk (default: 2000)",
}), }),
+2 -2
View File
@@ -42,7 +42,7 @@ export interface Interface {
readonly output: (text: string, options?: Options, agent?: Agent.Info) => Effect.Effect<Result> readonly output: (text: string, options?: Options, agent?: Agent.Info) => Effect.Effect<Result>
/** /**
* Resolved truncation limits from `tool_output` in opencode config. * Resolved truncation limits from `tool_output` in opencode config.
* Returns `None` when the user has disabled truncation (`tool_output.truncate: false`), * Returns `None` when the user has disabled truncation (`tool_output: false`),
* in which case callers should pass output through without enforcing thresholds. * in which case callers should pass output through without enforcing thresholds.
*/ */
readonly limits: () => Effect.Effect<Option.Option<Limits>> readonly limits: () => Effect.Effect<Option.Option<Limits>>
@@ -81,7 +81,7 @@ export const layer = Layer.effect(
if (Option.isNone(configSvc)) return Option.some({ maxLines: MAX_LINES, maxBytes: MAX_BYTES }) if (Option.isNone(configSvc)) return Option.some({ maxLines: MAX_LINES, maxBytes: MAX_BYTES })
const cfg = yield* configSvc.value.get().pipe(Effect.catch(() => Effect.succeed(undefined))) const cfg = yield* configSvc.value.get().pipe(Effect.catch(() => Effect.succeed(undefined)))
const tool_output = cfg?.tool_output const tool_output = cfg?.tool_output
if (tool_output?.truncate === false) return Option.none<Limits>() if (tool_output === false) return Option.none<Limits>()
return Option.some({ return Option.some({
maxLines: tool_output?.max_lines ?? MAX_LINES, maxLines: tool_output?.max_lines ?? MAX_LINES,
maxBytes: tool_output?.max_bytes ?? MAX_BYTES, maxBytes: tool_output?.max_bytes ?? MAX_BYTES,
+5 -10
View File
@@ -1292,25 +1292,20 @@ test("config parser preserves permission order while rejecting unknown top-level
} }
}) })
test("tool_output only accepts thresholds when truncation is enabled", () => { test("tool_output accepts thresholds or disables truncation", () => {
expect(ConfigParse.schema(Config.Info, { tool_output: { truncate: false } }, "test").tool_output).toEqual({ expect(ConfigParse.schema(Config.Info, { tool_output: false }, "test").tool_output).toBe(false)
truncate: false,
})
expect( expect(
ConfigParse.schema(Config.Info, { tool_output: { max_lines: 200, max_bytes: 8192 } }, "test").tool_output, ConfigParse.schema(Config.Info, { tool_output: { max_lines: 200, max_bytes: 8192 } }, "test").tool_output,
).toEqual({ ).toEqual({
max_lines: 200, max_lines: 200,
max_bytes: 8192, max_bytes: 8192,
}) })
expect(() =>
ConfigParse.schema(Config.Info, { tool_output: { truncate: false, max_lines: 200, max_bytes: 8192 } }, "test"),
).toThrow()
}) })
it.effect("project tool_output limits replace disabled global truncation", () => it.effect("project tool_output limits replace disabled global truncation", () =>
withConfigTree( withConfigTree(
{ {
global: { tool_output: { truncate: false } }, global: { tool_output: false },
project: { tool_output: { max_lines: 200 } }, project: { tool_output: { max_lines: 200 } },
}, },
Effect.gen(function* () { Effect.gen(function* () {
@@ -1323,10 +1318,10 @@ it.effect("project disabled tool_output replaces global limits", () =>
withConfigTree( withConfigTree(
{ {
global: { tool_output: { max_lines: 200, max_bytes: 8192 } }, global: { tool_output: { max_lines: 200, max_bytes: 8192 } },
project: { tool_output: { truncate: false } }, project: { tool_output: false },
}, },
Effect.gen(function* () { Effect.gen(function* () {
expect((yield* Config.use.get()).tool_output).toEqual({ truncate: false }) expect((yield* Config.use.get()).tool_output).toBe(false)
}), }),
), ),
) )
+1 -1
View File
@@ -1207,7 +1207,7 @@ describe("tool.shell truncation", () => {
it.live("does not truncate output when tool_output is disabled", () => it.live("does not truncate output when tool_output is disabled", () =>
Effect.gen(function* () { Effect.gen(function* () {
const tmp = yield* tmpdirScoped({ config: { tool_output: { truncate: false } } }) const tmp = yield* tmpdirScoped({ config: { tool_output: false } })
yield* runIn( yield* runIn(
tmp, tmp,
Effect.gen(function* () { Effect.gen(function* () {
@@ -166,7 +166,7 @@ describe("Truncate", () => {
}), }),
) )
const disabledIt = configuredIt({ tool_output: { truncate: false } }) const disabledIt = configuredIt({ tool_output: false })
disabledIt.live("does not truncate output when disabled", () => disabledIt.live("does not truncate output when disabled", () =>
Effect.gen(function* () { Effect.gen(function* () {
const content = "a".repeat(Truncate.MAX_BYTES + 1) const content = "a".repeat(Truncate.MAX_BYTES + 1)
+1 -7
View File
@@ -1298,14 +1298,8 @@ export type Config = {
* Configure tool output truncation. When output exceeds either limit, the full text is written to the truncation directory and a preview is returned. * Configure tool output truncation. When output exceeds either limit, the full text is written to the truncation directory and a preview is returned.
*/ */
tool_output?: tool_output?:
| false
| { | {
/**
* Disable tool output truncation
*/
truncate: false
}
| {
truncate?: true
max_lines?: number max_lines?: number
max_bytes?: number max_bytes?: number
} }
+2 -8
View File
@@ -361,32 +361,26 @@ You can control when tool output is truncated using the `tool_output` option. Wh
{ {
"$schema": "https://opencode.ai/config.json", "$schema": "https://opencode.ai/config.json",
"tool_output": { "tool_output": {
"truncate": true,
"max_lines": 2000, "max_lines": 2000,
"max_bytes": 51200 "max_bytes": 51200
} }
} }
``` ```
- `truncate` - Truncate tool output when it exceeds a configured threshold (default: `true`).
- `max_lines` - Maximum number of lines before output is truncated (default: `2000`). - `max_lines` - Maximum number of lines before output is truncated (default: `2000`).
- `max_bytes` - Maximum size in bytes before output is truncated (default: `51200`). - `max_bytes` - Maximum size in bytes before output is truncated (default: `51200`).
These thresholds apply to output handled by OpenCode's shared truncation layer, including MCP and plugin tool output. Individual tools that page or cap their own results can have separate limits. These thresholds apply to output handled by OpenCode's shared truncation layer, including MCP and plugin tool output. Individual tools that page or cap their own results can have separate limits.
To disable shared tool output truncation, set `truncate` to `false`: To disable shared tool output truncation, set `tool_output` to `false`:
```json title="opencode.json" ```json title="opencode.json"
{ {
"$schema": "https://opencode.ai/config.json", "$schema": "https://opencode.ai/config.json",
"tool_output": { "tool_output": false
"truncate": false
}
} }
``` ```
`truncate: false` cannot be combined with `max_lines` or `max_bytes`; thresholds only apply when truncation is enabled.
--- ---
### Models ### Models