From 6d3c4c916ba2c24e4b828466186b703168999716 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 22 May 2026 16:55:00 -0500 Subject: [PATCH 1/7] docs: document tool output truncation settings --- packages/web/src/content/docs/config.mdx | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index 3cb9d93748b9..823fd5b91e88 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -353,6 +353,27 @@ You can manage the tools an LLM can use through the `tools` option. --- +### Tool output + +You can control when tool output is truncated using the `tool_output` option. When output exceeds either threshold, OpenCode saves the complete output to disk and returns a truncated preview with the saved file path. + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "tool_output": { + "max_lines": 2000, + "max_bytes": 51200 + } +} +``` + +- `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. + +--- + ### Models You can configure the providers and models you want to use in your OpenCode config through the `provider`, `model` and `small_model` options. From 1edba25dfba16c482933086cc1ab8a08fe4aee89 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 22 May 2026 17:20:58 -0500 Subject: [PATCH 2/7] feat(core): allow disabling tool output truncation --- packages/opencode/src/config/config.ts | 19 ++++++++------- .../src/skill/prompt/customize-opencode.md | 3 ++- packages/opencode/src/tool/shell.ts | 6 ++--- packages/opencode/src/tool/shell/prompt.ts | 15 +++++++----- packages/opencode/src/tool/truncate.ts | 10 +++++--- packages/opencode/test/tool/shell.test.ts | 23 +++++++++++++++++++ .../opencode/test/tool/truncation.test.ts | 14 +++++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 13 +++++++---- packages/web/src/content/docs/config.mdx | 9 ++++++++ 9 files changed, 87 insertions(+), 25 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 349b7e6a074e..c017cf73cd54 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -253,17 +253,20 @@ export const Info = Schema.Struct({ }), ), 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)", + Schema.Union([ + Schema.Literal(false), + 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.", + "Configure tool output truncation, or set to false to disable it. When output exceeds either limit, the full text is written to the truncation directory and a preview is returned.", }), compaction: Schema.optional( Schema.Struct({ diff --git a/packages/opencode/src/skill/prompt/customize-opencode.md b/packages/opencode/src/skill/prompt/customize-opencode.md index 4ba118b0902b..820195daf563 100644 --- a/packages/opencode/src/skill/prompt/customize-opencode.md +++ b/packages/opencode/src/skill/prompt/customize-opencode.md @@ -126,7 +126,7 @@ Every field is optional. "mcp_timeout": 30000 }, - "tool_output": { "max_lines": 200, "max_bytes": 8192 }, + "tool_output": false | { "max_lines": 200, "max_bytes": 8192 }, "compaction": { "auto": true, "tail_turns": 15 } } @@ -140,6 +140,7 @@ Shape notes worth being explicit about: - `plugin` is an array of strings or `[name, options]` tuples, not an object. - `mcp[name].command` is an array of strings, never a single string. `type` is required. - `permission` is either a string action or an object keyed by tool name. +- `tool_output: false` disables shared tool output truncation; use an object to override its line and byte thresholds. ## Skills diff --git a/packages/opencode/src/tool/shell.ts b/packages/opencode/src/tool/shell.ts index 506d98466e76..b263fc6d566c 100644 --- a/packages/opencode/src/tool/shell.ts +++ b/packages/opencode/src/tool/shell.ts @@ -433,7 +433,7 @@ export const ShellTool = Tool.define( ctx: Tool.Context, ) { const limits = yield* trunc.limits() - const keep = limits.maxBytes * 2 + const keep = limits.enabled ? limits.maxBytes * 2 : Number.POSITIVE_INFINITY let full = "" let last = "" const list: Chunk[] = [] @@ -499,7 +499,7 @@ export const ShellTool = Tool.define( sink?.write(chunk) } else { full += chunk - if (Buffer.byteLength(full, "utf-8") > limits.maxBytes) { + if (limits.enabled && Buffer.byteLength(full, "utf-8") > limits.maxBytes) { return trunc.write(full).pipe( Effect.andThen((next) => Effect.sync(() => { @@ -566,7 +566,7 @@ export const ShellTool = Tool.define( } if (aborted) meta.push("User aborted the command") const raw = list.map((item) => item.text).join("") - const end = tail(raw, limits.maxLines, limits.maxBytes) + const end = limits.enabled ? tail(raw, limits.maxLines, limits.maxBytes) : { text: raw, cut: false } if (end.cut) cut = true if (!file && end.cut) { file = yield* trunc.write(raw) diff --git a/packages/opencode/src/tool/shell/prompt.ts b/packages/opencode/src/tool/shell/prompt.ts index f26e364b612c..f953deb00f91 100644 --- a/packages/opencode/src/tool/shell/prompt.ts +++ b/packages/opencode/src/tool/shell/prompt.ts @@ -15,6 +15,7 @@ const descriptions = { } export type Limits = { + enabled: boolean maxLines: number maxBytes: number } @@ -83,6 +84,11 @@ function chainGuidance(name: string) { return "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." } +function truncationGuidance(limits: Limits, commands: string) { + if (!limits.enabled) return "" + return `\n - If the output exceeds ${limits.maxLines} lines or ${limits.maxBytes} bytes, it will be truncated and the full output will be written to a file. You can use Read with offset/limit to read specific sections or Grep to search the full content. Do NOT use ${commands} to limit output; the full output will already be captured to a file for more precise searching.` +} + function bashCommandSection(chain: string, limits: Limits) { return `Before executing the command, please follow these steps: @@ -103,8 +109,7 @@ function bashCommandSection(chain: string, limits: Limits) { Usage notes: - The command argument is required. - You can specify an optional timeout in milliseconds. If not specified, commands will time out after 120000ms (2 minutes). - - It is very helpful if you write a clear, concise description of what this command does in 5-10 words. - - If the output exceeds ${limits.maxLines} lines or ${limits.maxBytes} bytes, it will be truncated and the full output will be written to a file. You can use Read with offset/limit to read specific sections or Grep to search the full content. Do NOT use \`head\`, \`tail\`, or other truncation commands to limit output; the full output will already be captured to a file for more precise searching. + - It is very helpful if you write a clear, concise description of what this command does in 5-10 words.${truncationGuidance(limits, "`head`, `tail`, or other truncation commands")} - Avoid using Bash with the \`find\`, \`grep\`, \`cat\`, \`head\`, \`tail\`, \`sed\`, \`awk\`, or \`echo\` commands, unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands: - File search: Use Glob (NOT find or ls) @@ -149,8 +154,7 @@ Before executing the command, please follow these steps: Usage notes: - The command argument is required. - You can specify an optional timeout in milliseconds. If not specified, commands will time out after 120000ms (2 minutes). - - It is very helpful if you write a clear, concise description of what this command does in 5-10 words. - - If the output exceeds ${limits.maxLines} lines or ${limits.maxBytes} bytes, it will be truncated and the full output will be written to a file. You can use Read with offset/limit to read specific sections or Grep to search the full content. Do NOT use \`Select-Object -First\`, \`Select-Object -Last\`, or other truncation commands to limit output; the full output will already be captured to a file for more precise searching. + - It is very helpful if you write a clear, concise description of what this command does in 5-10 words.${truncationGuidance(limits, "`Select-Object -First`, `Select-Object -Last`, or other truncation commands")} - Avoid using Shell with PowerShell file/content cmdlets unless explicitly instructed or when these cmdlets are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands: - File search: Use Glob (NOT Get-ChildItem) @@ -199,8 +203,7 @@ Before executing the command, please follow these steps: Usage notes: - The command argument is required. - You can specify an optional timeout in milliseconds. If not specified, commands will time out after 120000ms (2 minutes). - - It is very helpful if you write a clear, concise description of what this command does in 5-10 words. - - If the output exceeds ${limits.maxLines} lines or ${limits.maxBytes} bytes, it will be truncated and the full output will be written to a file. You can use Read with offset/limit to read specific sections or Grep to search the full content. Do NOT use \`more\` or other pagination commands to limit output; the full output will already be captured to a file for more precise searching. + - It is very helpful if you write a clear, concise description of what this command does in 5-10 words.${truncationGuidance(limits, "`more` or other pagination commands")} - Avoid using Shell with cmd.exe file/content commands unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands: - File search: Use Glob (NOT dir /s) diff --git a/packages/opencode/src/tool/truncate.ts b/packages/opencode/src/tool/truncate.ts index ffc16c0b9f99..00b6c327be6d 100644 --- a/packages/opencode/src/tool/truncate.ts +++ b/packages/opencode/src/tool/truncate.ts @@ -19,6 +19,7 @@ export const DIR = TRUNCATION_DIR export const GLOB = path.join(TRUNCATION_DIR, "*") export type Result = { content: string; truncated: false } | { content: string; truncated: true; outputPath: string } +export type Limits = { enabled: boolean; maxLines: number; maxBytes: number } export interface Options { maxLines?: number @@ -40,9 +41,9 @@ export interface Interface { */ readonly output: (text: string, options?: Options, agent?: Agent.Info) => Effect.Effect /** - * Resolved truncation limits: values from `tool_output` in opencode config, or MAX_LINES / MAX_BYTES if unset. + * Resolved truncation state and limits from `tool_output` in opencode config. */ - readonly limits: () => Effect.Effect<{ maxLines: number; maxBytes: number }> + readonly limits: () => Effect.Effect } export class Service extends Context.Service()("@opencode/Truncate") {} @@ -75,9 +76,11 @@ export const layer = Layer.effect( 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 } + if (Option.isNone(configSvc)) return { enabled: true, maxLines: MAX_LINES, maxBytes: MAX_BYTES } const cfg = yield* configSvc.value.get().pipe(Effect.catch(() => Effect.succeed(undefined))) + if (cfg?.tool_output === false) return { enabled: false, maxLines: MAX_LINES, maxBytes: MAX_BYTES } return { + enabled: true, maxLines: cfg?.tool_output?.max_lines ?? MAX_LINES, maxBytes: cfg?.tool_output?.max_bytes ?? MAX_BYTES, } @@ -85,6 +88,7 @@ export const layer = Layer.effect( const output = Effect.fn("Truncate.output")(function* (text: string, options: Options = {}, agent?: Agent.Info) { const resolved = yield* limits() + if (!resolved.enabled) return { content: text, truncated: false } as const const maxLines = options.maxLines ?? resolved.maxLines const maxBytes = options.maxBytes ?? resolved.maxBytes const direction = options.direction ?? "head" diff --git a/packages/opencode/test/tool/shell.test.ts b/packages/opencode/test/tool/shell.test.ts index fe4f5a483468..96307557b3fa 100644 --- a/packages/opencode/test/tool/shell.test.ts +++ b/packages/opencode/test/tool/shell.test.ts @@ -1200,6 +1200,29 @@ 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: false } }) + yield* runIn( + tmp, + Effect.gen(function* () { + const bash = yield* initShell() + expect(bash.description).not.toContain("If the output exceeds") + const result = yield* bash.execute( + { + command: fill("bytes", Truncate.MAX_BYTES + 10000), + description: "Generate bytes with truncation disabled", + }, + ctx, + ) + expect(result.metadata.truncated).toBe(false) + expect(result.output).not.toContain("...output truncated...") + expect(Buffer.byteLength(result.output, "utf-8")).toBeGreaterThan(Truncate.MAX_BYTES) + }), + ) + }), + ) + it.live("full output is saved to file when truncated", () => runIn( projectRoot, diff --git a/packages/opencode/test/tool/truncation.test.ts b/packages/opencode/test/tool/truncation.test.ts index 804bbd67266a..4233dd1b01c0 100644 --- a/packages/opencode/test/tool/truncation.test.ts +++ b/packages/opencode/test/tool/truncation.test.ts @@ -110,6 +110,7 @@ describe("Truncate", () => { Effect.gen(function* () { const svc = yield* Truncate.Service const resolved = yield* svc.limits() + expect(resolved.enabled).toBe(true) expect(resolved.maxLines).toBe(Truncate.MAX_LINES) expect(resolved.maxBytes).toBe(Truncate.MAX_BYTES) }), @@ -120,6 +121,7 @@ describe("Truncate", () => { limitsIt.live("limits() reflects config overrides", () => Effect.gen(function* () { const resolved = yield* (yield* Truncate.Service).limits() + expect(resolved.enabled).toBe(true) expect(resolved.maxLines).toBe(123) expect(resolved.maxBytes).toBe(456) }), @@ -159,6 +161,18 @@ describe("Truncate", () => { expect(result.truncated).toBe(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) + const svc = yield* Truncate.Service + const resolved = yield* svc.limits() + const result = yield* svc.output(content) + expect(resolved.enabled).toBe(false) + expect(result).toEqual({ content, truncated: false }) + }), + ) }) it.live("large single-line file truncates with byte message", () => diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index aae1b06ad320..7393d9d70f66 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1294,10 +1294,15 @@ export type Config = { enterprise?: { url?: string } - tool_output?: { - max_lines?: number - max_bytes?: number - } + /** + * Configure tool output truncation, or set to false to disable it. When output exceeds either limit, the full text is written to the truncation directory and a preview is returned. + */ + tool_output?: + | false + | { + max_lines?: number + max_bytes?: number + } compaction?: { auto?: boolean prune?: boolean diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index 823fd5b91e88..7aa9ff89340c 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -372,6 +372,15 @@ You can control when tool output is truncated using the `tool_output` option. Wh 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 `tool_output` to `false`: + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "tool_output": false +} +``` + --- ### Models From 7a58560161c82f1fa9a569f27a1d273372ac128e Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 22 May 2026 17:31:22 -0500 Subject: [PATCH 3/7] fix(core): nest tool output truncation toggle --- packages/opencode/src/config/config.ts | 22 +++++++++---------- .../src/skill/prompt/customize-opencode.md | 4 ++-- packages/opencode/src/tool/truncate.ts | 3 +-- packages/opencode/test/tool/shell.test.ts | 2 +- .../opencode/test/tool/truncation.test.ts | 2 +- packages/sdk/js/src/v2/gen/types.gen.ts | 14 +++++------- packages/web/src/content/docs/config.mdx | 8 +++++-- 7 files changed, 27 insertions(+), 28 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index c017cf73cd54..d2bdc527807d 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -253,20 +253,20 @@ export const Info = Schema.Struct({ }), ), tool_output: Schema.optional( - Schema.Union([ - Schema.Literal(false), - 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)", - }), + Schema.Struct({ + truncate: Schema.optional(Schema.Boolean).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)", }), - ]), + max_bytes: Schema.optional(PositiveInt).annotate({ + description: "Maximum bytes of tool output before it is truncated and saved to disk (default: 51200)", + }), + }), ).annotate({ description: - "Configure tool output truncation, or set to false to disable it. 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.", }), compaction: Schema.optional( Schema.Struct({ diff --git a/packages/opencode/src/skill/prompt/customize-opencode.md b/packages/opencode/src/skill/prompt/customize-opencode.md index 820195daf563..efb963f6a0ce 100644 --- a/packages/opencode/src/skill/prompt/customize-opencode.md +++ b/packages/opencode/src/skill/prompt/customize-opencode.md @@ -126,7 +126,7 @@ Every field is optional. "mcp_timeout": 30000 }, - "tool_output": false | { "max_lines": 200, "max_bytes": 8192 }, + "tool_output": { "truncate": false, "max_lines": 200, "max_bytes": 8192 }, "compaction": { "auto": true, "tail_turns": 15 } } @@ -140,7 +140,7 @@ Shape notes worth being explicit about: - `plugin` is an array of strings or `[name, options]` tuples, not an object. - `mcp[name].command` is an array of strings, never a single string. `type` is required. - `permission` is either a string action or an object keyed by tool name. -- `tool_output: false` disables shared tool output truncation; use an object to override its line and byte thresholds. +- `tool_output.truncate: false` disables shared tool output truncation; use `max_lines` and `max_bytes` to override its thresholds. ## Skills diff --git a/packages/opencode/src/tool/truncate.ts b/packages/opencode/src/tool/truncate.ts index 00b6c327be6d..0ea9ad08bca2 100644 --- a/packages/opencode/src/tool/truncate.ts +++ b/packages/opencode/src/tool/truncate.ts @@ -78,9 +78,8 @@ export const layer = Layer.effect( const configSvc = yield* Effect.serviceOption(Config.Service) if (Option.isNone(configSvc)) return { enabled: true, maxLines: MAX_LINES, maxBytes: MAX_BYTES } const cfg = yield* configSvc.value.get().pipe(Effect.catch(() => Effect.succeed(undefined))) - if (cfg?.tool_output === false) return { enabled: false, maxLines: MAX_LINES, maxBytes: MAX_BYTES } return { - enabled: true, + enabled: cfg?.tool_output?.truncate !== false, maxLines: cfg?.tool_output?.max_lines ?? MAX_LINES, maxBytes: cfg?.tool_output?.max_bytes ?? MAX_BYTES, } diff --git a/packages/opencode/test/tool/shell.test.ts b/packages/opencode/test/tool/shell.test.ts index 96307557b3fa..7d58120cd77c 100644 --- a/packages/opencode/test/tool/shell.test.ts +++ b/packages/opencode/test/tool/shell.test.ts @@ -1202,7 +1202,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: false } }) + const tmp = yield* tmpdirScoped({ config: { tool_output: { truncate: false } } }) yield* runIn( tmp, Effect.gen(function* () { diff --git a/packages/opencode/test/tool/truncation.test.ts b/packages/opencode/test/tool/truncation.test.ts index 4233dd1b01c0..b0f946f5851a 100644 --- a/packages/opencode/test/tool/truncation.test.ts +++ b/packages/opencode/test/tool/truncation.test.ts @@ -162,7 +162,7 @@ describe("Truncate", () => { }), ) - const disabledIt = configuredIt({ tool_output: false }) + const disabledIt = configuredIt({ tool_output: { truncate: false } }) disabledIt.live("does not truncate output when disabled", () => Effect.gen(function* () { const content = "a".repeat(Truncate.MAX_BYTES + 1) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 7393d9d70f66..f20f4e8a5487 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1294,15 +1294,11 @@ export type Config = { enterprise?: { url?: string } - /** - * Configure tool output truncation, or set to false to disable it. When output exceeds either limit, the full text is written to the truncation directory and a preview is returned. - */ - tool_output?: - | false - | { - max_lines?: number - max_bytes?: number - } + tool_output?: { + truncate?: boolean + max_lines?: number + max_bytes?: number + } compaction?: { auto?: boolean prune?: boolean diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index 7aa9ff89340c..0be2699d3711 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -361,23 +361,27 @@ 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 `tool_output` to `false`: +To disable shared tool output truncation, set `truncate` to `false`: ```json title="opencode.json" { "$schema": "https://opencode.ai/config.json", - "tool_output": false + "tool_output": { + "truncate": false + } } ``` From 7cfcb644ab47e19703ba5ce77612aa6c63c4ee33 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 22 May 2026 17:37:43 -0500 Subject: [PATCH 4/7] fix(core): enforce tool output truncation variants --- packages/opencode/src/config/config.ts | 25 +++++++++++-------- .../src/skill/prompt/customize-opencode.md | 7 ++++-- packages/opencode/test/config/config.test.ts | 15 +++++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 20 +++++++++++---- packages/web/src/content/docs/config.mdx | 2 ++ 5 files changed, 52 insertions(+), 17 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index d2bdc527807d..6086f0c3da8a 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -253,17 +253,22 @@ export const Info = Schema.Struct({ }), ), tool_output: Schema.optional( - Schema.Struct({ - truncate: Schema.optional(Schema.Boolean).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)", - }), - max_bytes: Schema.optional(PositiveInt).annotate({ - description: "Maximum bytes of tool output before it is truncated and saved to disk (default: 51200)", + Schema.Union([ + 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)", + }), + max_bytes: Schema.optional(PositiveInt).annotate({ + description: "Maximum bytes of tool output before it is truncated and saved to disk (default: 51200)", + }), }), - }), + ]), ).annotate({ description: "Configure tool output truncation. When output exceeds either limit, the full text is written to the truncation directory and a preview is returned.", diff --git a/packages/opencode/src/skill/prompt/customize-opencode.md b/packages/opencode/src/skill/prompt/customize-opencode.md index efb963f6a0ce..95e7dc353e83 100644 --- a/packages/opencode/src/skill/prompt/customize-opencode.md +++ b/packages/opencode/src/skill/prompt/customize-opencode.md @@ -23,6 +23,10 @@ shape before writing config, **fetch that URL and read the schema directly** rather than guessing. opencode hard-fails on invalid config, so the cost of a wrong shape is a broken startup. +The full schema is large. Prefer using JavaScript or Bash to fetch and extract +the relevant property definition instead of reading the entire schema into +context when you only need to check one setting. + Independently, every `opencode.json` should declare `"$schema": "https://opencode.ai/config.json"` so the user's editor catches mistakes as they type. @@ -126,7 +130,7 @@ Every field is optional. "mcp_timeout": 30000 }, - "tool_output": { "truncate": false, "max_lines": 200, "max_bytes": 8192 }, + "tool_output": { "max_lines": 200, "max_bytes": 8192 }, "compaction": { "auto": true, "tail_turns": 15 } } @@ -140,7 +144,6 @@ Shape notes worth being explicit about: - `plugin` is an array of strings or `[name, options]` tuples, not an object. - `mcp[name].command` is an array of strings, never a single string. `type` is required. - `permission` is either a string action or an object keyed by tool name. -- `tool_output.truncate: false` disables shared tool output truncation; use `max_lines` and `max_bytes` to override its thresholds. ## Skills diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 04dcde32e11d..cd1db63da3ce 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -1455,6 +1455,21 @@ 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, + }) + 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() +}) + // MCP config merging tests it.instance("project config can override MCP server enabled status", () => diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index f20f4e8a5487..1cbfa799acb9 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1294,11 +1294,21 @@ export type Config = { enterprise?: { url?: string } - tool_output?: { - truncate?: boolean - max_lines?: number - max_bytes?: number - } + /** + * 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?: + | { + /** + * Disable tool output truncation + */ + truncate: false + } + | { + truncate?: true + max_lines?: number + max_bytes?: number + } compaction?: { auto?: boolean prune?: boolean diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index 0be2699d3711..f6927384c83b 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -385,6 +385,8 @@ To disable shared tool output truncation, set `truncate` to `false`: } ``` +`truncate: false` cannot be combined with `max_lines` or `max_bytes`; thresholds only apply when truncation is enabled. + --- ### Models From 93cc5e8dff7f06cdf27a04edc7f7c928fd4e7fdd Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 24 May 2026 23:06:06 -0500 Subject: [PATCH 5/7] refactor(opencode): model truncation limits as Option Replace the `{ enabled: boolean; maxLines: number; maxBytes: number }` shape with `Option<{ maxLines: number; maxBytes: number }>` so the absence of limits is represented by the type system instead of a boolean flag with dead numeric fields. - Truncate.limits() now returns Effect>. - Truncate.output() short-circuits on None instead of checking .enabled. - shell tool gates rolling buffer, disk spill, and tail truncation on Option.isSome(limits); behavior unchanged when truncation is enabled. - ShellPrompt.render and helpers accept Option; the truncation guidance line is omitted when None. - Tests updated to assert Option.isSome / Option.isNone. --- packages/opencode/src/tool/shell.ts | 14 +++++++---- packages/opencode/src/tool/shell/prompt.ts | 19 ++++++++------- packages/opencode/src/tool/truncate.ts | 23 ++++++++++--------- .../opencode/test/tool/truncation.test.ts | 20 +++++++++------- 4 files changed, 43 insertions(+), 33 deletions(-) diff --git a/packages/opencode/src/tool/shell.ts b/packages/opencode/src/tool/shell.ts index 78e7e976d8a0..722ee6d2b519 100644 --- a/packages/opencode/src/tool/shell.ts +++ b/packages/opencode/src/tool/shell.ts @@ -1,4 +1,4 @@ -import { Effect, Stream } from "effect" +import { Effect, Option, Stream } from "effect" import os from "os" import { createWriteStream } from "node:fs" import * as Tool from "./tool" @@ -433,7 +433,10 @@ export const ShellTool = Tool.define( ctx: Tool.Context, ) { const limits = yield* trunc.limits() - const keep = limits.enabled ? limits.maxBytes * 2 : Number.POSITIVE_INFINITY + const keep = Option.match(limits, { + onNone: () => Number.POSITIVE_INFINITY, + onSome: (l) => l.maxBytes * 2, + }) let full = "" let last = "" const list: Chunk[] = [] @@ -499,7 +502,7 @@ export const ShellTool = Tool.define( sink?.write(chunk) } else { full += chunk - if (limits.enabled && Buffer.byteLength(full, "utf-8") > limits.maxBytes) { + if (Option.isSome(limits) && Buffer.byteLength(full, "utf-8") > limits.value.maxBytes) { return trunc.write(full).pipe( Effect.andThen((next) => Effect.sync(() => { @@ -566,7 +569,10 @@ export const ShellTool = Tool.define( } if (aborted) meta.push("User aborted the command") const raw = list.map((item) => item.text).join("") - const end = limits.enabled ? tail(raw, limits.maxLines, limits.maxBytes) : { text: raw, cut: false } + const end = Option.match(limits, { + onNone: () => ({ text: raw, cut: false }), + onSome: (l) => tail(raw, l.maxLines, l.maxBytes), + }) if (end.cut) cut = true if (!file && end.cut) { file = yield* trunc.write(raw) diff --git a/packages/opencode/src/tool/shell/prompt.ts b/packages/opencode/src/tool/shell/prompt.ts index bd0fd274d662..a5dcfb59a75b 100644 --- a/packages/opencode/src/tool/shell/prompt.ts +++ b/packages/opencode/src/tool/shell/prompt.ts @@ -1,4 +1,4 @@ -import { Schema } from "effect" +import { Option, Schema } from "effect" import DESCRIPTION from "./shell.txt" import { PositiveInt } from "@opencode-ai/core/schema" import { Global } from "@opencode-ai/core/global" @@ -15,7 +15,6 @@ const descriptions = { } export type Limits = { - enabled: boolean maxLines: number maxBytes: number } @@ -84,12 +83,12 @@ function chainGuidance(name: string) { return "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." } -function truncationGuidance(limits: Limits, commands: string) { - if (!limits.enabled) return "" - return `\n - If the output exceeds ${limits.maxLines} lines or ${limits.maxBytes} bytes, it will be truncated and the full output will be written to a file. You can use Read with offset/limit to read specific sections or Grep to search the full content. Do NOT use ${commands} to limit output; the full output will already be captured to a file for more precise searching.` +function truncationGuidance(limits: Option.Option, commands: string) { + if (Option.isNone(limits)) return "" + return `\n - If the output exceeds ${limits.value.maxLines} lines or ${limits.value.maxBytes} bytes, it will be truncated and the full output will be written to a file. You can use Read with offset/limit to read specific sections or Grep to search the full content. Do NOT use ${commands} to limit output; the full output will already be captured to a file for more precise searching.` } -function bashCommandSection(chain: string, limits: Limits, defaultTimeoutMs: number) { +function bashCommandSection(chain: string, limits: Option.Option, defaultTimeoutMs: number) { return `Before executing the command, please follow these steps: 1. Directory Verification: @@ -136,7 +135,7 @@ function powershellCommandSection( name: string, chain: string, pathSep: string, - limits: Limits, + limits: Option.Option, defaultTimeoutMs: number, ) { return `${powershellNotes(name)} @@ -183,7 +182,7 @@ Usage notes: ` } -function cmdCommandSection(chain: string, limits: Limits, defaultTimeoutMs: number) { +function cmdCommandSection(chain: string, limits: Option.Option, defaultTimeoutMs: number) { return `# cmd.exe shell notes - Use double quotes for paths with spaces. - Use %VAR% for environment variables. @@ -232,7 +231,7 @@ Usage notes: ` } -function profile(name: string, platform: NodeJS.Platform, limits: Limits, defaultTimeoutMs: number) { +function profile(name: string, platform: NodeJS.Platform, limits: Option.Option, defaultTimeoutMs: number) { const isPowerShell = PS.has(name) const chain = chainGuidance(name) if (CMD.has(name)) { @@ -287,7 +286,7 @@ function profile(name: string, platform: NodeJS.Platform, limits: Limits, defaul } } -export function render(name: string, platform: NodeJS.Platform, limits: Limits, defaultTimeoutMs: number) { +export function render(name: string, platform: NodeJS.Platform, limits: Option.Option, defaultTimeoutMs: number) { const selected = profile(name, platform, limits, defaultTimeoutMs) return { description: renderPrompt(DESCRIPTION, { diff --git a/packages/opencode/src/tool/truncate.ts b/packages/opencode/src/tool/truncate.ts index 455dc12bf564..c92d8e31a74a 100644 --- a/packages/opencode/src/tool/truncate.ts +++ b/packages/opencode/src/tool/truncate.ts @@ -19,7 +19,7 @@ export const DIR = TRUNCATION_DIR export const GLOB = path.join(TRUNCATION_DIR, "*") export type Result = { content: string; truncated: false } | { content: string; truncated: true; outputPath: string } -export type Limits = { enabled: boolean; maxLines: number; maxBytes: number } +export type Limits = { maxLines: number; maxBytes: number } export interface Options { maxLines?: number @@ -41,9 +41,11 @@ export interface Interface { */ readonly output: (text: string, options?: Options, agent?: Agent.Info) => Effect.Effect /** - * Resolved truncation state and 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`), + * in which case callers should pass output through without enforcing thresholds. */ - readonly limits: () => Effect.Effect + readonly limits: () => Effect.Effect> } export class Service extends Context.Service()("@opencode/Truncate") {} @@ -76,22 +78,21 @@ export const layer = Layer.effect( const limits = Effect.fn("Truncate.limits")(function* () { const configSvc = yield* Effect.serviceOption(Config.Service) - if (Option.isNone(configSvc)) return { enabled: true, 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 tool_output = cfg?.tool_output - if (tool_output?.truncate === false) return { enabled: false, maxLines: MAX_LINES, maxBytes: MAX_BYTES } - return { - enabled: true, + if (tool_output?.truncate === false) return Option.none() + return Option.some({ maxLines: tool_output?.max_lines ?? MAX_LINES, maxBytes: tool_output?.max_bytes ?? MAX_BYTES, - } + }) }) const output = Effect.fn("Truncate.output")(function* (text: string, options: Options = {}, agent?: Agent.Info) { const resolved = yield* limits() - if (!resolved.enabled) return { content: text, truncated: false } as const - const maxLines = options.maxLines ?? resolved.maxLines - const maxBytes = options.maxBytes ?? resolved.maxBytes + if (Option.isNone(resolved)) return { content: text, truncated: false } as const + const maxLines = options.maxLines ?? resolved.value.maxLines + const maxBytes = options.maxBytes ?? resolved.value.maxBytes const direction = options.direction ?? "head" const lines = text.split("\n") const totalBytes = Buffer.byteLength(text, "utf-8") diff --git a/packages/opencode/test/tool/truncation.test.ts b/packages/opencode/test/tool/truncation.test.ts index b0f946f5851a..dd0ca558b5db 100644 --- a/packages/opencode/test/tool/truncation.test.ts +++ b/packages/opencode/test/tool/truncation.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect } from "bun:test" import { NodeFileSystem } from "@effect/platform-node" import { AppFileSystem } from "@opencode-ai/core/filesystem" -import { Effect, FileSystem, Layer } from "effect" +import { Effect, FileSystem, Layer, Option } from "effect" import { Truncate } from "@/tool/truncate" import { Config } from "@/config/config" import { Identifier } from "../../src/id/id" @@ -110,9 +110,11 @@ describe("Truncate", () => { Effect.gen(function* () { const svc = yield* Truncate.Service const resolved = yield* svc.limits() - expect(resolved.enabled).toBe(true) - expect(resolved.maxLines).toBe(Truncate.MAX_LINES) - expect(resolved.maxBytes).toBe(Truncate.MAX_BYTES) + expect(Option.isSome(resolved)).toBe(true) + if (Option.isSome(resolved)) { + expect(resolved.value.maxLines).toBe(Truncate.MAX_LINES) + expect(resolved.value.maxBytes).toBe(Truncate.MAX_BYTES) + } }), ) @@ -121,9 +123,11 @@ describe("Truncate", () => { limitsIt.live("limits() reflects config overrides", () => Effect.gen(function* () { const resolved = yield* (yield* Truncate.Service).limits() - expect(resolved.enabled).toBe(true) - expect(resolved.maxLines).toBe(123) - expect(resolved.maxBytes).toBe(456) + expect(Option.isSome(resolved)).toBe(true) + if (Option.isSome(resolved)) { + expect(resolved.value.maxLines).toBe(123) + expect(resolved.value.maxBytes).toBe(456) + } }), ) @@ -169,7 +173,7 @@ describe("Truncate", () => { const svc = yield* Truncate.Service const resolved = yield* svc.limits() const result = yield* svc.output(content) - expect(resolved.enabled).toBe(false) + expect(Option.isNone(resolved)).toBe(true) expect(result).toEqual({ content, truncated: false }) }), ) From d4f83ba2b79e3d91037aad95517fbe9b84995f0c Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 26 May 2026 14:49:22 -0500 Subject: [PATCH 6/7] fix(opencode): normalize merged tool output config --- packages/opencode/src/config/config.ts | 8 ++++- packages/opencode/test/config/config.test.ts | 36 ++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index c51136d198b1..327dea464f36 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -49,7 +49,13 @@ 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 { - return mergeDeep(target, source) as 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 } function mergeConfigConcatArrays(target: Info, source: Info): Info { diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 913ad049e15f..a2ac219c15fd 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -1307,6 +1307,42 @@ test("tool_output only accepts thresholds when truncation is enabled", () => { ).toThrow() }) +it.effect("project tool_output limits replace disabled global truncation", () => + withConfigTree( + { + global: { tool_output: { truncate: false } }, + project: { tool_output: { max_lines: 200 } }, + }, + Effect.gen(function* () { + expect((yield* Config.use.get()).tool_output).toEqual({ max_lines: 200 }) + }), + ), +) + +it.effect("project disabled tool_output replaces global limits", () => + withConfigTree( + { + global: { tool_output: { max_lines: 200, max_bytes: 8192 } }, + project: { tool_output: { truncate: false } }, + }, + Effect.gen(function* () { + expect((yield* Config.use.get()).tool_output).toEqual({ truncate: false }) + }), + ), +) + +it.effect("enabled tool_output limits still merge across layers", () => + withConfigTree( + { + global: { tool_output: { max_bytes: 8192 } }, + project: { tool_output: { max_lines: 200 } }, + }, + Effect.gen(function* () { + expect((yield* Config.use.get()).tool_output).toEqual({ max_bytes: 8192, max_lines: 200 }) + }), + ), +) + // MCP config merging tests it.instance("project config can override MCP server enabled status", () => From 5d929f78c353f446f7d3e7959c99b0522e2131f1 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 26 May 2026 15:33:08 -0500 Subject: [PATCH 7/7] refactor(opencode): simplify tool output disable config --- packages/opencode/src/config/config.ts | 15 ++------------- packages/opencode/src/tool/truncate.ts | 4 ++-- packages/opencode/test/config/config.test.ts | 15 +++++---------- packages/opencode/test/tool/shell.test.ts | 2 +- packages/opencode/test/tool/truncation.test.ts | 2 +- packages/sdk/js/src/v2/gen/types.gen.ts | 8 +------- packages/web/src/content/docs/config.mdx | 10 ++-------- 7 files changed, 14 insertions(+), 42 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 327dea464f36..09d43e2440af 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -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)", }), diff --git a/packages/opencode/src/tool/truncate.ts b/packages/opencode/src/tool/truncate.ts index c92d8e31a74a..d76eacb92127 100644 --- a/packages/opencode/src/tool/truncate.ts +++ b/packages/opencode/src/tool/truncate.ts @@ -42,7 +42,7 @@ export interface Interface { readonly output: (text: string, options?: Options, agent?: Agent.Info) => Effect.Effect /** * 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> @@ -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() + if (tool_output === false) return Option.none() return Option.some({ maxLines: tool_output?.max_lines ?? MAX_LINES, maxBytes: tool_output?.max_bytes ?? MAX_BYTES, diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index a2ac219c15fd..b99fafc0588a 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -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) }), ), ) diff --git a/packages/opencode/test/tool/shell.test.ts b/packages/opencode/test/tool/shell.test.ts index 919a10b5c481..6eef878a1990 100644 --- a/packages/opencode/test/tool/shell.test.ts +++ b/packages/opencode/test/tool/shell.test.ts @@ -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* () { diff --git a/packages/opencode/test/tool/truncation.test.ts b/packages/opencode/test/tool/truncation.test.ts index dd0ca558b5db..4fecac671fa9 100644 --- a/packages/opencode/test/tool/truncation.test.ts +++ b/packages/opencode/test/tool/truncation.test.ts @@ -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) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 1cbfa799acb9..a3a40019039d 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -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 } diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index f6927384c83b..7aa9ff89340c 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -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