# 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`:

```typescript
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`:

```typescript
ToolContent = Union([
  { type: "text", text: string },
  { type: "file", uri: string, mime: string, name?: string },
])
```

### `ToolResultPart` — message part injected into conversation

```typescript
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:
```json
{"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: ..." |
