Files
correx/tool-result-schema.md
kami aee2e67c66 refactor(detekt): extract magic-number constants, wrap long lines, drop legit suppressions
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.
2026-07-12 23:12:28 +04:00

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: ToolOutputToolResultValue

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: ..."