Mechanical detekt cleanup across apps/core/infrastructure (behavior-preserving): - MagicNumber 62->10: named constants + removed 'legit' MagicNumber suppressions (Tier level from ordinal, ReplayInferenceProvider CHARS_PER_TOKEN, ConfigLoader DEFAULT_* consts, capability scores, HTTP ranges, column widths, etc.) - MaxLineLength cut to ~0 in production modules (line wraps, no logic change) - Dropped one dead parameter (ConfigLoader.parseArray lineNum) Structural findings (ReturnCount/LongMethod/Complexity/LargeClass) left for a deliberate decomposition pass. Full build + detekt gate green.
5.4 KiB
Tool Result Schema — What the Model Sees
Documenting how opencode formats tool results into the LLM context.
Source: github.com/anomalyco/opencode (branch dev).
Internal Types (TypeScript @opencode-ai/schema)
ToolResultValue — the 4-variant result envelope
Defined in packages/llm/src/schema/messages.ts:
ToolResultValue = Union([
{ type: "json", value: unknown }, // structured data, no text content
{ type: "text", value: unknown }, // single text item
{ type: "error", value: unknown }, // tool error
{ type: "content", value: ToolContent[] }, // multiple items (text + files)
])
ToolContent — inner content items
Defined in packages/schema/src/llm.ts:
ToolContent = Union([
{ type: "text", text: string },
{ type: "file", uri: string, mime: string, name?: string },
])
ToolResultPart — message part injected into conversation
ToolResultPart = Struct({
type: "tool-result",
id: string, // matches ToolCallPart.id
name: string,
result: ToolResultValue,
providerExecuted?: boolean,
cache?: CacheHint,
metadata?: Record<string, unknown>,
providerMetadata?: ProviderMetadata,
})
Conversion: ToolOutput → ToolResultValue
In ToolOutput.toResultValue() (packages/llm/src/schema/messages.ts):
content.length === 0 → {type: "json", value: output.structured}
content.length === 1 && is text → {type: "text", value: text}
otherwise → {type: "content", value: output.content}
Per-Tool Formatting
Each tool defines toModelOutput() which returns {type: "text" | "file"}[].
Bash / Shell
File: packages/core/src/tool/bash.ts
<stdout/stderr content>
Command exited with code 0.
If truncated by ToolOutputStore.bound() (default 2000 lines / 50KB):
... output truncated; full content saved to /path/to/tool-output/tool_abc123 ...
[last lines here]
Command exited with code 0.
If timed out:
Command exceeded timeout of 120000 ms. Retry with a larger timeout.
Read (text — opencode tool)
File: packages/opencode/src/tool/read.ts
<path>/path/to/file.ts</path>
<type>file</type>
<content>
1: line one
2: line two
3: line three
...
</content>
(Showing lines 1-50 of 200. Use offset=51 to continue.)
If a context file (marked as context in config):
<system-reminder>
...
</system-reminder>
Read (image — core tool)
File: packages/core/src/tool/read.ts
Image read successfully
Plus a file attachment (base64 data URI with mime type).
Write
File: packages/core/src/tool/write.ts
Wrote file successfully: /path/to/file
or
Created file successfully: /path/to/file
Edit
File: packages/core/src/tool/edit.ts
Edited file successfully: /path/to/file
Replacements: 1
```diff
-old line
+new line
Preview shows first 6 lines of old (prefixed -) and new (prefixed +).
Glob
File: packages/core/src/tool/glob.ts
src/file1.ts
src/file2.ts
src/subdir/file3.ts
or No files found
Grep
File: packages/core/src/tool/grep.ts
Found 3 matches
src/file.ts:
Line 10: matching text
Line 20: more matching text
src/other.ts:
Line 5: another match
WebFetch
File: packages/core/src/tool/webfetch.ts
Raw fetched content as plain text.
Question
File: packages/core/src/tool/question.ts
User has answered your questions: "What color?"="Blue". You can now continue with the user's answers in mind.
Truncation (ToolOutputStore.bound)
File: packages/core/src/tool-output-store.ts
Default limits: 2000 lines / 50 KB.
When exceeded:
- Full output saved to
tool-output/tool_<id>in managed storage - In-memory text replaced with head/tail preview (~25 lines / ~25KB each) + marker
The marker text between head and tail:
... output truncated; full content saved to /path/to/tool-output/tool_abc123 ...
Protocol Lowering (Wire Format)
OpenAI Chat (packages/llm/src/protocols/openai-chat.ts)
Always a flat string:
{"role": "tool", "tool_call_id": "...", "content": "<formatted text>"}
Images are appended as a separate {role: "user", content: [{type: "image_url", ...}]} message.
Anthropic Messages (packages/llm/src/protocols/anthropic-messages.ts)
tool_result.content =
string // for text/error/json variants
[{type: "text", text: ...}, // for "content" variant
{type: "image", source: {type: "base64", media_type, data}}]
Gemini / Vertex (packages/llm/src/protocols/google-gemini.ts)
tool_result → {role: "user", parts: [
{text: "<formatted>"},
{inlineData: {mimeType, data: base64}}
]}
Summary Table
| Tool | What Model Sees |
|---|---|
| bash | stdout/stderr + Command exited with code N |
| read (text) | XML-tagged <path><type><content> with line numbers |
| read (image) | "Image read successfully" + base64 file attachment |
| write | "Wrote/Created file successfully: /path" |
| edit | "Edited file successfully" + diff block |
| glob | Newline-separated paths (or "No files found") |
| grep | "Found N matches" + formatted line listing |
| webfetch | Raw fetched content |
| question | "User has answered your questions: ..." |