refactor(opencode): simplify tool output disable config
This commit is contained in:
@@ -49,13 +49,7 @@ const log = Log.create({ service: "config" })
|
||||
// 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.
|
||||
function mergeConfig(target: Info, source: Info): Info {
|
||||
const merged = 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
|
||||
return mergeDeep(target, source) as Info
|
||||
}
|
||||
|
||||
function mergeConfigConcatArrays(target: Info, source: Info): Info {
|
||||
@@ -260,13 +254,8 @@ export const Info = Schema.Struct({
|
||||
),
|
||||
tool_output: Schema.optional(
|
||||
Schema.Union([
|
||||
Schema.Literal(false).annotate({ description: "Disable tool output truncation" }),
|
||||
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({
|
||||
description: "Maximum lines of tool output before it is truncated and saved to disk (default: 2000)",
|
||||
}),
|
||||
|
||||
@@ -42,7 +42,7 @@ export interface Interface {
|
||||
readonly output: (text: string, options?: Options, agent?: Agent.Info) => Effect.Effect<Result>
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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 })
|
||||
const cfg = yield* configSvc.value.get().pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
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({
|
||||
maxLines: tool_output?.max_lines ?? MAX_LINES,
|
||||
maxBytes: tool_output?.max_bytes ?? MAX_BYTES,
|
||||
|
||||
@@ -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", () => {
|
||||
expect(ConfigParse.schema(Config.Info, { tool_output: { truncate: false } }, "test").tool_output).toEqual({
|
||||
truncate: false,
|
||||
})
|
||||
test("tool_output accepts thresholds or disables truncation", () => {
|
||||
expect(ConfigParse.schema(Config.Info, { tool_output: false }, "test").tool_output).toBe(false)
|
||||
expect(
|
||||
ConfigParse.schema(Config.Info, { tool_output: { max_lines: 200, max_bytes: 8192 } }, "test").tool_output,
|
||||
).toEqual({
|
||||
max_lines: 200,
|
||||
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", () =>
|
||||
withConfigTree(
|
||||
{
|
||||
global: { tool_output: { truncate: false } },
|
||||
global: { tool_output: false },
|
||||
project: { tool_output: { max_lines: 200 } },
|
||||
},
|
||||
Effect.gen(function* () {
|
||||
@@ -1323,10 +1318,10 @@ it.effect("project disabled tool_output replaces global limits", () =>
|
||||
withConfigTree(
|
||||
{
|
||||
global: { tool_output: { max_lines: 200, max_bytes: 8192 } },
|
||||
project: { tool_output: { truncate: false } },
|
||||
project: { tool_output: false },
|
||||
},
|
||||
Effect.gen(function* () {
|
||||
expect((yield* Config.use.get()).tool_output).toEqual({ truncate: false })
|
||||
expect((yield* Config.use.get()).tool_output).toBe(false)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1207,7 +1207,7 @@ describe("tool.shell truncation", () => {
|
||||
|
||||
it.live("does not truncate output when tool_output is disabled", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* tmpdirScoped({ config: { tool_output: { truncate: false } } })
|
||||
const tmp = yield* tmpdirScoped({ config: { tool_output: false } })
|
||||
yield* runIn(
|
||||
tmp,
|
||||
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", () =>
|
||||
Effect.gen(function* () {
|
||||
const content = "a".repeat(Truncate.MAX_BYTES + 1)
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
tool_output?:
|
||||
| false
|
||||
| {
|
||||
/**
|
||||
* Disable tool output truncation
|
||||
*/
|
||||
truncate: false
|
||||
}
|
||||
| {
|
||||
truncate?: true
|
||||
max_lines?: number
|
||||
max_bytes?: number
|
||||
}
|
||||
|
||||
@@ -361,32 +361,26 @@ You can control when tool output is truncated using the `tool_output` option. Wh
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"tool_output": {
|
||||
"truncate": true,
|
||||
"max_lines": 2000,
|
||||
"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_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.
|
||||
|
||||
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"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"tool_output": {
|
||||
"truncate": false
|
||||
}
|
||||
"tool_output": false
|
||||
}
|
||||
```
|
||||
|
||||
`truncate: false` cannot be combined with `max_lines` or `max_bytes`; thresholds only apply when truncation is enabled.
|
||||
|
||||
---
|
||||
|
||||
### Models
|
||||
|
||||
Reference in New Issue
Block a user