Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/opencode/src/flag/flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export namespace Flag {
truthy("OPENCODE_ENABLE_EXA") || OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EXA")
export const OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH = number("OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH")
export const OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS = number("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS")
export const OPENCODE_EXPERIMENTAL_MCP_MAX_OUTPUT_LENGTH = number("OPENCODE_EXPERIMENTAL_MCP_MAX_OUTPUT_LENGTH")
export const OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX = number("OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX")
export const OPENCODE_EXPERIMENTAL_OXFMT = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_OXFMT")
export const OPENCODE_EXPERIMENTAL_LSP_TY = truthy("OPENCODE_EXPERIMENTAL_LSP_TY")
Expand Down
5 changes: 5 additions & 0 deletions packages/opencode/src/session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { Decimal } from "decimal.js"
import z from "zod"
import path from "path"
import fs from "fs/promises"
import { type LanguageModelUsage, type ProviderMetadata } from "ai"
import { Config } from "../config/config"
import { Flag } from "../flag/flag"
import { Global } from "../global"
import { Identifier } from "../id/id"
import { Installation } from "../installation"

Expand Down Expand Up @@ -307,6 +310,8 @@ export namespace Session {
}
await Storage.remove(msg)
}
const toolResultsDir = path.join(Global.Path.data, "storage", "tool_results", sessionID)
await fs.rm(toolResultsDir, { recursive: true, force: true }).catch(() => {})
await Storage.remove(["session", project.id, sessionID])
Bus.publish(Event.Deleted, {
info: session,
Expand Down
19 changes: 17 additions & 2 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { clone, mergeDeep, pipe } from "remeda"
import { ToolRegistry } from "../tool/registry"
import { Wildcard } from "../util/wildcard"
import { MCP } from "../mcp"
import { ToolResults } from "./tool-results"
import { LSP } from "../lsp"
import { ReadTool } from "../tool/read"
import { ListTool } from "../tool/ls"
Expand All @@ -50,6 +51,7 @@ globalThis.AI_SDK_LOG_WARNINGS = false
export namespace SessionPrompt {
const log = Log.create({ service: "session.prompt" })
export const OUTPUT_TOKEN_MAX = Flag.OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX || 32_000
const MAX_MCP_OUTPUT_LENGTH = Flag.OPENCODE_EXPERIMENTAL_MCP_MAX_OUTPUT_LENGTH || 30_000

const state = Instance.state(
() => {
Expand Down Expand Up @@ -695,12 +697,25 @@ export namespace SessionPrompt {
// Add support for other types if needed
}

let output = textParts.join("\n\n")
let content = result.content

if (output.length > MAX_MCP_OUTPUT_LENGTH) {
const filePath = await ToolResults.save(input.sessionID, key, output)
const isJson = output.trim().startsWith("[") || output.trim().startsWith("{")
output = `Output (${output.length.toLocaleString()} characters) exceeds maximum allowed (${MAX_MCP_OUTPUT_LENGTH.toLocaleString()}).
Output has been saved to ${filePath}
${isJson ? "Format: JSON - use jq via bash tool for structured queries.\n" : ""}Use the Read tool with offset/limit parameters to read specific portions,
or use Grep to search for specific content within the file.`
content = [{ type: "text" as const, text: output }]
}

return {
title: "",
metadata: result.metadata ?? {},
output: textParts.join("\n\n"),
output,
attachments,
content: result.content, // directly return content to preserve ordering when outputting to model
content,
}
}
item.toModelOutput = (result) => {
Expand Down
16 changes: 16 additions & 0 deletions packages/opencode/src/session/tool-results.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import path from "path"
import fs from "fs/promises"
import { ulid } from "ulid"
import { Global } from "../global"

export namespace ToolResults {
export async function save(sessionID: string, toolName: string, content: string): Promise<string> {
const sanitizedToolName = toolName.replace(/[^a-zA-Z0-9_-]/g, "_")
const filename = `${sanitizedToolName}-${ulid()}.txt`
const dir = path.join(Global.Path.data, "storage", "tool_results", sessionID)
await fs.mkdir(dir, { recursive: true })
const filePath = path.join(dir, filename)
await fs.writeFile(filePath, content, "utf-8")
return filePath
}
}
26 changes: 20 additions & 6 deletions packages/opencode/src/tool/bash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import { fileURLToPath } from "url"
import { Flag } from "@/flag/flag.ts"
import path from "path"
import { Shell } from "@/shell/shell"
import { ToolResults } from "@/session/tool-results"

const MAX_OUTPUT_LENGTH = Flag.OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH || 30_000
const MAX_OUTPUT_HARD_LIMIT = 10_000_000 // 10MB hard limit to prevent memory issues
const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000

export const log = Log.create({ service: "bash-tool" })
Expand Down Expand Up @@ -216,8 +218,10 @@ export const BashTool = Tool.define("bash", async () => {
})

const append = (chunk: Buffer) => {
if (output.length <= MAX_OUTPUT_LENGTH) {
if (output.length < MAX_OUTPUT_HARD_LIMIT) {
output += chunk.toString()
}
if (output.length <= MAX_OUTPUT_LENGTH) {
ctx.metadata({
metadata: {
output,
Expand Down Expand Up @@ -274,11 +278,6 @@ export const BashTool = Tool.define("bash", async () => {

let resultMetadata: String[] = ["<bash_metadata>"]

if (output.length > MAX_OUTPUT_LENGTH) {
output = output.slice(0, MAX_OUTPUT_LENGTH)
resultMetadata.push(`bash tool truncated output as it exceeded ${MAX_OUTPUT_LENGTH} char limit`)
}

if (timedOut) {
resultMetadata.push(`bash tool terminated commmand after exceeding timeout ${timeout} ms`)
}
Expand All @@ -292,6 +291,21 @@ export const BashTool = Tool.define("bash", async () => {
output += "\n\n" + resultMetadata.join("\n")
}

if (output.length > MAX_OUTPUT_LENGTH) {
const wasHardLimited = output.length >= MAX_OUTPUT_HARD_LIMIT
const filePath = await ToolResults.save(ctx.sessionID, "bash", output)
const statusInfo: string[] = []
if (proc.exitCode != null && proc.exitCode !== 0) statusInfo.push(`Exit code: ${proc.exitCode}`)
if (timedOut) statusInfo.push(`Command timed out after ${timeout}ms`)
if (aborted) statusInfo.push("Command was aborted by user")
if (wasHardLimited) statusInfo.push(`Output was truncated at ${MAX_OUTPUT_HARD_LIMIT.toLocaleString()} character limit`)
const statusLine = statusInfo.length > 0 ? `\n${statusInfo.join(". ")}.` : ""
output = `Output (${output.length.toLocaleString()} characters) exceeds maximum allowed (${MAX_OUTPUT_LENGTH.toLocaleString()}).
Output has been saved to ${filePath}${statusLine}
Use the Read tool with offset/limit parameters to read specific portions,
or use Grep to search for specific content within the file.`
}

return {
title: params.description,
metadata: {
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/src/tool/bash.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ Usage notes:
- The command argument is required.
- You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout 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 30000 characters, output will be truncated before being returned to you.
- If the output exceeds 30000 characters, it will be saved to a file and you will receive the file path. Use the Read tool with offset/limit parameters to read specific portions, or use Grep to search for specific content within the file.
- You can use the `run_in_background` parameter to run the command in the background, which allows you to continue working while the command runs. You can monitor the output using the Bash tool as it becomes available. You do not need to use '&' at the end of the command when using this parameter.

- 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)
- Content search: Use Grep (NOT grep or rg)
Expand Down
13 changes: 12 additions & 1 deletion packages/opencode/src/tool/codesearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { Tool } from "./tool"
import DESCRIPTION from "./codesearch.txt"
import { Config } from "../config/config"
import { Permission } from "../permission"
import { ToolResults } from "../session/tool-results"

const MAX_OUTPUT_LENGTH = 30_000

const API_CONFIG = {
BASE_URL: "https://mcp.exa.ai",
Expand Down Expand Up @@ -110,8 +113,16 @@ export const CodeSearchTool = Tool.define("codesearch", {
if (line.startsWith("data: ")) {
const data: McpCodeResponse = JSON.parse(line.substring(6))
if (data.result && data.result.content && data.result.content.length > 0) {
let output = data.result.content[0].text
if (output.length > MAX_OUTPUT_LENGTH) {
const filePath = await ToolResults.save(ctx.sessionID, "codesearch", output)
output = `Output (${output.length.toLocaleString()} characters) exceeds maximum allowed (${MAX_OUTPUT_LENGTH.toLocaleString()}).
Output has been saved to ${filePath}
Use the Read tool with offset/limit parameters to read specific portions,
or use Grep to search for specific content within the file.`
}
return {
output: data.result.content[0].text,
output,
title: `Code search: ${params.query}`,
metadata: {},
}
Expand Down
60 changes: 27 additions & 33 deletions packages/opencode/src/tool/webfetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import TurndownService from "turndown"
import DESCRIPTION from "./webfetch.txt"
import { Config } from "../config/config"
import { Permission } from "../permission"
import { ToolResults } from "../session/tool-results"

const MAX_RESPONSE_SIZE = 5 * 1024 * 1024 // 5MB
const MAX_OUTPUT_LENGTH = 30_000
const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds
const MAX_TIMEOUT = 120 * 1000 // 2 minutes

Expand Down Expand Up @@ -94,50 +96,42 @@ export const WebFetchTool = Tool.define("webfetch", {
const title = `${params.url} (${contentType})`

// Handle content based on requested format and actual content type
let output: string
switch (params.format) {
case "markdown":
if (contentType.includes("text/html")) {
const markdown = convertHTMLToMarkdown(content)
return {
output: markdown,
title,
metadata: {},
}
}
return {
output: content,
title,
metadata: {},
output = convertHTMLToMarkdown(content)
} else {
output = content
}
break

case "text":
if (contentType.includes("text/html")) {
const text = await extractTextFromHTML(content)
return {
output: text,
title,
metadata: {},
}
}
return {
output: content,
title,
metadata: {},
output = await extractTextFromHTML(content)
} else {
output = content
}
break

case "html":
return {
output: content,
title,
metadata: {},
}

default:
return {
output: content,
title,
metadata: {},
}
output = content
break
}

if (output.length > MAX_OUTPUT_LENGTH) {
const filePath = await ToolResults.save(ctx.sessionID, "webfetch", output)
output = `Output (${output.length.toLocaleString()} characters) exceeds maximum allowed (${MAX_OUTPUT_LENGTH.toLocaleString()}).
Output has been saved to ${filePath}
Use the Read tool with offset/limit parameters to read specific portions,
or use Grep to search for specific content within the file.`
}

return {
output,
title,
metadata: {},
}
},
})
Expand Down
13 changes: 12 additions & 1 deletion packages/opencode/src/tool/websearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { Tool } from "./tool"
import DESCRIPTION from "./websearch.txt"
import { Config } from "../config/config"
import { Permission } from "../permission"
import { ToolResults } from "../session/tool-results"

const MAX_OUTPUT_LENGTH = 30_000

const API_CONFIG = {
BASE_URL: "https://mcp.exa.ai",
Expand Down Expand Up @@ -123,8 +126,16 @@ export const WebSearchTool = Tool.define("websearch", {
if (line.startsWith("data: ")) {
const data: McpSearchResponse = JSON.parse(line.substring(6))
if (data.result && data.result.content && data.result.content.length > 0) {
let output = data.result.content[0].text
if (output.length > MAX_OUTPUT_LENGTH) {
const filePath = await ToolResults.save(ctx.sessionID, "websearch", output)
output = `Output (${output.length.toLocaleString()} characters) exceeds maximum allowed (${MAX_OUTPUT_LENGTH.toLocaleString()}).
Output has been saved to ${filePath}
Use the Read tool with offset/limit parameters to read specific portions,
or use Grep to search for specific content within the file.`
}
return {
output: data.result.content[0].text,
output,
title: `Web search: ${params.query}`,
metadata: {},
}
Expand Down