aee2e67c66
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.
255 lines
5.4 KiB
Markdown
255 lines
5.4 KiB
Markdown
# 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: ..." |
|