diff --git a/packages/opencode/src/session/truncation.ts b/packages/opencode/src/session/truncation.ts index d1bb933403..471c86b57c 100644 --- a/packages/opencode/src/session/truncation.ts +++ b/packages/opencode/src/session/truncation.ts @@ -4,7 +4,10 @@ import { Global } from "../global" import { Identifier } from "../id/id" import { iife } from "../util/iife" import { lazy } from "../util/lazy" +import { PermissionNext } from "../permission/next" +import type { Agent } from "../agent/agent" +// what models does opencode provider support? Read: https://models.dev/api.json export namespace Truncate { export const MAX_LINES = 2000 export const MAX_BYTES = 50 * 1024 @@ -38,7 +41,13 @@ export namespace Truncate { } }) - export async function output(text: string, options: Options = {}): Promise { + function hasTaskTool(agent?: Agent.Info): boolean { + if (!agent?.permission) return false + const rule = PermissionNext.evaluate("task", "*", agent.permission) + return rule.action !== "deny" + } + + export async function output(text: string, options: Options = {}, agent?: Agent.Info): Promise { const maxLines = options.maxLines ?? MAX_LINES const maxBytes = options.maxBytes ?? MAX_BYTES const direction = options.direction ?? "head" @@ -85,10 +94,12 @@ export namespace Truncate { const filepath = path.join(DIR, id) await Bun.write(Bun.file(filepath), text) + const base = `Full output written to: ${filepath}\nUse Grep to search the full content and Read with offset/limit to read specific sections` + const hint = hasTaskTool(agent) ? `${base} (or use Task tool to delegate and save context).` : `${base}.` const message = direction === "head" - ? `${preview}\n\n...${removed} ${unit} truncated...\n\nFull output written to: ${filepath}\nUse Read or Grep to view the full content.` - : `...${removed} ${unit} truncated...\n\nFull output written to: ${filepath}\nUse Read or Grep to view the full content.\n\n${preview}` + ? `${preview}\n\n...${removed} ${unit} truncated...\n\n${hint}` + : `...${removed} ${unit} truncated...\n\n${hint}\n\n${preview}` return { content: message, truncated: true, outputPath: filepath } } diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 657a5b3182..96432369f6 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -60,12 +60,12 @@ export namespace ToolRegistry { function fromPlugin(id: string, def: ToolDefinition): Tool.Info { return { id, - init: async () => ({ + init: async (initCtx) => ({ parameters: z.object(def.args), description: def.description, execute: async (args, ctx) => { const result = await def.execute(args as any, ctx) - const out = await Truncate.output(result) + const out = await Truncate.output(result, {}, initCtx?.agent) return { title: "", output: out.truncated ? out.content : result, diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index 94385b0f31..7545d36b1a 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -50,8 +50,8 @@ export namespace Tool { ): Info { return { id, - init: async (ctx) => { - const toolInfo = init instanceof Function ? await init(ctx) : init + init: async (initCtx) => { + const toolInfo = init instanceof Function ? await init(initCtx) : init const execute = toolInfo.execute toolInfo.execute = async (args, ctx) => { try { @@ -66,7 +66,7 @@ export namespace Tool { ) } const result = await execute(args, ctx) - const truncated = await Truncate.output(result.output) + const truncated = await Truncate.output(result.output, {}, initCtx?.agent) return { ...result, output: truncated.content, diff --git a/packages/opencode/test/session/truncation.test.ts b/packages/opencode/test/session/truncation.test.ts index e4b952f056..242109f5c3 100644 --- a/packages/opencode/test/session/truncation.test.ts +++ b/packages/opencode/test/session/truncation.test.ts @@ -83,12 +83,32 @@ describe("Truncate", () => { expect(result.outputPath).toBeDefined() expect(result.outputPath).toContain("tool_") expect(result.content).toContain("Full output written to:") - expect(result.content).toContain("Use Read or Grep to view the full content") + expect(result.content).toContain("Grep") const written = await Bun.file(result.outputPath!).text() expect(written).toBe(lines) }) + test("suggests Task tool when agent has task permission", async () => { + const lines = Array.from({ length: 100 }, (_, i) => `line${i}`).join("\n") + const agent = { permission: [{ permission: "task", pattern: "*", action: "allow" as const }] } + const result = await Truncate.output(lines, { maxLines: 10 }, agent as any) + + expect(result.truncated).toBe(true) + expect(result.content).toContain("Grep") + expect(result.content).toContain("Task tool") + }) + + test("omits Task tool hint when agent lacks task permission", async () => { + const lines = Array.from({ length: 100 }, (_, i) => `line${i}`).join("\n") + const agent = { permission: [{ permission: "task", pattern: "*", action: "deny" as const }] } + const result = await Truncate.output(lines, { maxLines: 10 }, agent as any) + + expect(result.truncated).toBe(true) + expect(result.content).toContain("Grep") + expect(result.content).not.toContain("Task tool") + }) + test("does not write file when not truncated", async () => { const content = "short content" const result = await Truncate.output(content)