Skip to content

Commit c15df05

Browse files
committed
refactor: use metadata object for prune tool reason and distillation
- Replace reason-as-first-id pattern with structured metadata object - metadata.reason: 'completion' | 'noise' | 'consolidation' - metadata.distillation: optional object for consolidation findings - Update prompts to clarify distillation rules per reason type - Add TUI rendering documentation explaining hidden arguments
1 parent f3d7e1c commit c15df05

File tree

5 files changed

+208
-46
lines changed

5 files changed

+208
-46
lines changed
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# OpenCode TUI Tool Rendering: Argument Visibility
2+
3+
This document explains how opencode's TUI determines which tool arguments are displayed to users and how to design custom plugin tools that keep certain arguments hidden.
4+
5+
## Key Files
6+
7+
All rendering logic is located in the opencode reference repository:
8+
9+
- **Main rendering file**: `packages/opencode/src/cli/cmd/tui/routes/session/index.tsx`
10+
11+
## Rendering Flow
12+
13+
### 1. Tool Part Rendering Entry Point (Line 1230)
14+
15+
When a tool call is rendered, the TUI checks the `ToolRegistry` for a custom renderer:
16+
17+
```tsx
18+
const render = ToolRegistry.render(props.part.tool) ?? GenericTool
19+
```
20+
21+
If no custom renderer is registered for the tool name, it falls back to the `GenericTool` component.
22+
23+
### 2. GenericTool Component (Lines 1329-1335)
24+
25+
The fallback renderer for all plugin-defined tools:
26+
27+
```tsx
28+
function GenericTool(props: ToolProps<any>) {
29+
return (
30+
<ToolTitle icon="" fallback="Writing command..." when={true}>
31+
{props.tool} {input(props.input)}
32+
</ToolTitle>
33+
)
34+
}
35+
```
36+
37+
This renders:
38+
1. The tool name (`props.tool`)
39+
2. A formatted string of arguments via the `input()` helper function
40+
41+
### 3. The `input()` Helper Function (Lines 1702-1709)
42+
43+
This is the critical "gatekeeper" that determines which arguments appear in the TUI:
44+
45+
```tsx
46+
function input(input: Record<string, any>, omit?: string[]): string {
47+
const primitives = Object.entries(input).filter(([key, value]) => {
48+
if (omit?.includes(key)) return false
49+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean"
50+
})
51+
if (primitives.length === 0) return ""
52+
return `[${primitives.map(([key, value]) => `${key}=${value}`).join(", ")}]`
53+
}
54+
```
55+
56+
## Argument Visibility Rules
57+
58+
### Arguments That ARE Displayed
59+
60+
| Type | Example | TUI Output |
61+
|------|---------|------------|
62+
| `string` | `reason: "completion"` | `[reason=completion]` |
63+
| `number` | `count: 5` | `[count=5]` |
64+
| `boolean` | `force: true` | `[force=true]` |
65+
66+
### Arguments That Are NOT Displayed
67+
68+
| Type | Example | TUI Output |
69+
|------|---------|------------|
70+
| `array` | `ids: ["1", "2", "3"]` | *(nothing)* |
71+
| `object` | `metadata: { reason: "..." }` | *(nothing)* |
72+
| `null` | `data: null` | *(nothing)* |
73+
| `undefined` | `data: undefined` | *(nothing)* |
74+
75+
## Designing Hidden Arguments
76+
77+
To keep arguments hidden from the TUI while still passing data to your tool:
78+
79+
### Option 1: Use Arrays
80+
81+
```typescript
82+
args: {
83+
ids: tool.schema.array(tool.schema.string())
84+
}
85+
```
86+
87+
Arrays are filtered out by the `input()` function's primitive check.
88+
89+
### Option 2: Use Objects
90+
91+
```typescript
92+
args: {
93+
metadata: tool.schema.object({
94+
reason: tool.schema.enum(["a", "b", "c"]),
95+
distillation: tool.schema.record(tool.schema.string(), tool.schema.any()).optional()
96+
})
97+
}
98+
```
99+
100+
Objects are also filtered out, making them ideal for structured hidden data.
101+
102+
### Option 3: Use Records
103+
104+
```typescript
105+
args: {
106+
data: tool.schema.record(tool.schema.string(), tool.schema.any())
107+
}
108+
```
109+
110+
Records (key-value maps) are objects and thus hidden.
111+
112+
## Example: The `prune` Tool
113+
114+
The `prune` tool in this plugin uses a combination of these strategies:
115+
116+
```typescript
117+
args: {
118+
ids: tool.schema.array(tool.schema.string()), // Hidden (array)
119+
metadata: tool.schema.object({ // Hidden (object)
120+
reason: tool.schema.enum(["completion", "noise", "consolidation"]),
121+
distillation: tool.schema.record(tool.schema.string(), tool.schema.any()).optional()
122+
})
123+
}
124+
```
125+
126+
**TUI Output**: `⚙ prune`
127+
128+
No arguments are displayed because:
129+
1. `ids` is an array (filtered out)
130+
2. `metadata` is an object (filtered out)
131+
132+
## ToolRegistry for Custom Renderers
133+
134+
If you want complete control over how a tool is rendered, you can register a custom renderer:
135+
136+
```tsx
137+
const ToolRegistry = (() => {
138+
const state: Record<string, ToolRegistration> = {}
139+
function register<T extends Tool.Info>(input: ToolRegistration<T>) {
140+
state[input.name] = input
141+
return input
142+
}
143+
return {
144+
register,
145+
container(name: string) { return state[name]?.container },
146+
render(name: string) { return state[name]?.render },
147+
}
148+
})()
149+
```
150+
151+
However, plugin-defined tools cannot currently register custom TUI renderers from outside the opencode core package, so the `GenericTool` fallback and the `input()` filter are the primary mechanisms for controlling visibility.
152+
153+
## Summary
154+
155+
| Goal | Strategy |
156+
|------|----------|
157+
| Show argument in TUI | Use `string`, `number`, or `boolean` |
158+
| Hide argument from TUI | Use `array`, `object`, or `record` |
159+
| Hide structured data | Wrap in a single `object` or `record` argument |
160+
| Clean TUI output | Ensure all top-level args are non-primitives |

lib/prompts/prune-nudge.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
**CRITICAL CONTEXT WARNING:** Your context window is filling with tool outputs. Strict adherence to context hygiene is required.
33

44
**Immediate Actions Required:**
5-
1. **Garbage Collect:** If you read files or ran commands that yielded no value, prune them NOW. Do not summarize them.
6-
2. **Task Cleanup:** If a sub-task is complete, prune the tools used.
7-
3. **Consolidate:** If you are holding valuable raw data, you *must* distill the insights into your narrative and prune the raw entry.
5+
1. **Task Completion:** If a sub-task is complete, prune the tools used. No distillation.
6+
2. **Noise Removal:** If you read files or ran commands that yielded no value, prune them NOW. No distillation.
7+
3. **Consolidation:** If you are holding valuable raw data, you *must* distill the insights into `metadata.distillation` and prune the raw entry.
88

99
**Protocol:** You should prioritize this cleanup, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must prune.
1010
</instruction>

lib/prompts/prune-system-prompt.txt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ PRUNE METHODICALLY - CONSOLIDATE YOUR ACTIONS
88
Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by pruning. Consolidate your prunes for efficiency; it is rarely worth pruning a single tiny tool output unless it is pure noise. Evaluate what SHOULD be pruned before jumping the gun.
99

1010
WHEN TO PRUNE? THE THREE SCENARIOS TO CONSIDER
11-
### 1. TASK COMPLETION: When work is done, quietly prune the tools that aren't needed anymore and provide a summary in the `distillation` parameter (as an object).
12-
2. NOISE REMOVAL: If outputs are irrelevant, unhelpful, or superseded by newer info, prune IMMEDIATELY. No distillation - gun it down
13-
3. CONTEXT CONSOLIDATION: When pruning valuable context to the task at hand, you MUST ALWAYS provide the key findings in the `distillation` parameter of the `prune` tool (as an object). Be surgical and strategic in what you extract. THINK: high signal, low noise
11+
1. TASK COMPLETION: When work is done, quietly prune the tools that aren't needed anymore. No distillation.
12+
2. NOISE REMOVAL: If outputs are irrelevant, unhelpful, or superseded by newer info, prune. No distillation.
13+
3. CONTEXT CONSOLIDATION: When pruning valuable context to the task at hand, you MUST ALWAYS provide the key findings in the `metadata.distillation` parameter of the `prune` tool (as an object). Be surgical and strategic in what you extract. THINK: high signal, low noise
1414

1515
You WILL use the `prune` tool when ANY of these are true:
1616
- Task or sub-task is complete
1717
- You are about to start a new phase of work
18-
- You have gathered enough information to prune related tools and preserve their value in the `distillation` parameter
18+
- You have gathered enough information to prune related tools and preserve their value in the `metadata.distillation` parameter
1919
- Context contains tools output that are unhelpful, noise, or made obsolete by newer outputs
2020
- Write or edit operations are complete (pruning removes the large input content)
2121

@@ -26,7 +26,7 @@ You MUST NOT prune when:
2626
Pruning that forces you to re-call the same tool later is a net loss. Only prune when you're confident the information won't be needed again.
2727

2828
NOTES
29-
When in doubt, keep it. Prune frequently yet remain strategic and consolidate your actions.
29+
When in doubt, keep it. Consolidate your actions and aim for high-impact prunes that significantly reduce context size.
3030
FAILURE TO PRUNE will result in context leakage and DEGRADED PERFORMANCES.
3131
There may be tools in session context that do not appear in the <prunable-tools> list, this is expected, you can ONLY prune what you see in <prunable-tools>.
3232

lib/prompts/prune-tool-spec.txt

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Prunes tool outputs from context to manage conversation size and reduce noise. For `write` and `edit` tools, the input content is pruned instead of the output.
1+
Prunes tool outputs from context to manage conversation size and reduce noise.
22

33
## IMPORTANT: The Prunable List
44
A `<prunable-tools>` list is injected into user messages showing available tool outputs you can prune when there are tools available for pruning. Each line has the format `ID: tool, parameter` (e.g., `20: read, /path/to/file.ts`). You MUST only use numeric IDs that appear in this list to select which tools to prune.
@@ -7,58 +7,63 @@ A `<prunable-tools>` list is injected into user messages showing available tool
77

88
## CRITICAL: When and How to Prune
99

10-
You must use this tool in three specific scenarios. The rules for distillation (summarizing findings) differ for each. **You must specify the reason as the first element of the `ids` array** to indicate which scenario applies.
10+
You must use this tool in three specific scenarios. The rules for distillation (summarizing findings) differ for each. **You must provide a `metadata` object with a `reason` and optional `distillation`** to indicate which scenario applies.
1111

1212
### 1. Task Completion (Clean Up) — reason: `completion`
1313
**When:** You have successfully completed a specific unit of work (e.g., fixed a bug, wrote a file, answered a question).
1414
**Action:** Prune the tools used for that task.
15-
**Distillation:** Use the `distillation` parameter (as an object) to provide a final confirmation that the task is complete (e.g., `{ "status": "Tests passed, file updated" }`).
15+
**Distillation:** FORBIDDEN. Do not summarize completed work.
1616

1717
### 2. Removing Noise (Garbage Collection) — reason: `noise`
1818
**When:** You have read files or run commands that turned out to be irrelevant, unhelpful, or outdated (meaning later tools have provided fresher, more valid information).
1919
**Action:** Prune these specific tool outputs immediately.
20-
**Distillation:** NOT REQUIRED for noise.
20+
**Distillation:** FORBIDDEN. Do not summarize noise.
2121

2222
### 3. Context Conservation (Research & Consolidation) — reason: `consolidation`
23-
**When:** You have gathered useful information. Prune frequently as you work (e.g., after reading a few files), rather than waiting for a "long" phase to end.
23+
**When:** You have gathered useful information. Wait until you have several items or a few large outputs to prune, rather than doing tiny, frequent prunes. Aim for high-impact prunes that significantly reduce context size.
2424
**Action:** Convert raw data into distilled knowledge. This allows you to discard large outputs (like full file reads) while keeping only the specific parts you need (like a single function signature or constant).
25-
**Distillation:** MANDATORY. Use the `distillation` parameter (MUST be an object) to explicitly summarize the key findings from *every* tool you plan to prune.
25+
**Distillation:** MANDATORY. You MUST provide the distilled findings in the `metadata.distillation` parameter of the `prune` tool (as an object).
2626
- **Extract specific value:** If you read a large file but only care about one function, record that function's details.
27-
- Structure: `{ "file_path": { "findings": "...", "logic": "..." } }`
28-
- Capture all relevant details (function names, logic, constraints).
29-
- Once distilled into the `distillation` object, the raw tool output can be safely pruned.
27+
- **Consolidate:** When pruning multiple tools, your distillation object MUST aggregate findings from ALL of them. Ensure you capture any information necessary to solve the current task.
28+
- Structure: Map the `ID` from the `<prunable-tools>` list to its distilled findings.
29+
Example: `{ "20": { "findings": "...", "logic": "..." } }`
30+
- Capture all relevant details (function names, logic, constraints) to ensure no signal is lost.
31+
- Prioritize information that is essential for the immediate next steps of your plan.
32+
- Once distilled into the `metadata` object, the raw tool output can be safely pruned.
3033
- **Know when distillation isn't enough:** If you'll need to edit a file, grep for exact strings, or reference precise syntax, keep the raw output. Distillation works for understanding; implementation often requires the original.
3134
- **Prefer keeping over re-fetching:** If uncertain whether you'll need the output again, keep it. The cost of retaining context is lower than the cost of redundant tool calls.
3235

3336
## Best Practices
34-
- **Consolidate your prunes:** Don't prune a single small tool output (like a short bash command) unless it's pure noise. Wait until you have several items or a few large outputs to prune. Aim for high-impact prunes that significantly reduce context size or noise.
35-
- **Don't wait too long:** Prune frequently to keep the context agile, but balance this with the need for consolidation.
36-
- **Be surgical:** You can mix strategies. Prune noise without comment, while distilling useful context in the same turn.
37-
- **Verify:** Ensure you have captured what you need before deleting useful raw data.
38-
- **Think ahead:** Before pruning, ask: "Will I need this output for an upcoming task?" If you researched a file you'll later edit, or gathered context for implementation, do NOT prune it—even if you've distilled findings. Distillation captures *knowledge*; implementation requires *context*.
37+
- **Strategic Consolidation:** Don't prune single small tool outputs (like short bash commands) unless they are pure noise. Instead, wait until you have several items or large outputs to perform high-impact prunes. This balances the need for an agile context with the efficiency of larger batches.
38+
- **Think ahead:** Before pruning, ask: "Will I need this output for an upcoming task?" If you researched a file you'll later edit, or gathered context for implementation, do NOT prune it.
3939

4040
## Examples
4141

4242
<example_noise>
4343
Assistant: [Reads 'wrong_file.ts']
4444
This file isn't relevant to the auth system. I'll remove it to clear the context.
45-
[Uses prune with ids: ["noise", "5"]]
45+
[Uses prune with ids: ["5"], metadata: { "reason": "noise" }]
4646
</example_noise>
4747

4848
<example_consolidation>
4949
Assistant: [Reads 5 different config files]
5050
I'll preserve the configuration details and prune the raw reads.
51-
[Uses prune with ids: ["consolidation", "10", "11", "12", "13", "14"], distillation: {
52-
"config.ts": "uses port 3000",
53-
"db.ts": "connects to mongo:27017",
54-
"others": "defaults"
51+
[Uses prune with ids: ["10", "11", "12", "13", "14"], metadata: {
52+
"reason": "consolidation",
53+
"distillation": {
54+
"10": "uses port 3000",
55+
"11": "connects to mongo:27017",
56+
"12": "defines shared constants",
57+
"13": "export defaults",
58+
"14": "unused fallback"
59+
}
5560
}]
5661
</example_consolidation>
5762

5863
<example_completion>
5964
Assistant: [Runs tests, they pass]
6065
The tests passed. I'll clean up now.
61-
[Uses prune with ids: ["completion", "20", "21"], distillation: "Verified feature implementation and passed all tests."]
66+
[Uses prune with ids: ["20", "21"], metadata: { "reason": "completion" }]
6267
</example_completion>
6368

6469
<example_keep>
@@ -69,5 +74,5 @@ I've understood the auth flow. I'll need to modify this file to add the new vali
6974
<example_edit_completion>
7075
Assistant: [Edits 'auth.ts' to add validation]
7176
The edit was successful. I no longer need the raw edit content in context.
72-
[Uses prune with ids: ["completion", "15"]]
77+
[Uses prune with ids: ["15"], metadata: { "reason": "completion" }]
7378
</example_edit_completion>

lib/strategies/prune-tool.ts

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,14 @@ export function createPruneTool(
3434
ids: tool.schema.array(
3535
tool.schema.string()
3636
).describe(
37-
"First element is the reason ('completion', 'noise', 'consolidation'), followed by numeric IDs as strings to prune"
38-
),
39-
distillation: tool.schema.record(tool.schema.string(), tool.schema.any()).optional().describe(
40-
"An object containing detailed summaries or extractions of the key findings from the tools being pruned. This is REQUIRED for 'consolidation'."
37+
"Numeric IDs as strings to prune from the <prunable-tools> list"
4138
),
39+
metadata: tool.schema.object({
40+
reason: tool.schema.enum(["completion", "noise", "consolidation"]).describe("The reason for pruning"),
41+
distillation: tool.schema.record(tool.schema.string(), tool.schema.any()).optional().describe(
42+
"An object containing detailed summaries or extractions of the key findings from the tools being pruned. This is REQUIRED for 'consolidation'."
43+
),
44+
}).describe("Metadata about the pruning operation."),
4245
},
4346
async execute(args, toolCtx) {
4447
const { client, state, logger, config, workingDirectory } = ctx
@@ -52,26 +55,20 @@ export function createPruneTool(
5255
return "No IDs provided. Check the <prunable-tools> list for available IDs to prune."
5356
}
5457

55-
// Parse reason from first element, numeric IDs from the rest
56-
57-
const reason = args.ids[0];
58-
const validReasons = ["completion", "noise", "consolidation"] as const
59-
if (typeof reason !== "string" || !validReasons.includes(reason as any)) {
60-
logger.debug("Invalid pruning reason provided: " + reason)
61-
return "No valid pruning reason found. Use 'completion', 'noise', or 'consolidation' as the first element."
58+
if (!args.metadata || !args.metadata.reason) {
59+
logger.debug("Prune tool called without metadata.reason: " + JSON.stringify(args))
60+
return "Missing metadata.reason. Provide metadata: { reason: 'completion' | 'noise' | 'consolidation' }"
6261
}
6362

64-
const numericToolIds: number[] = args.ids.slice(1)
63+
const { reason, distillation } = args.metadata;
64+
65+
const numericToolIds: number[] = args.ids
6566
.map(id => parseInt(id, 10))
6667
.filter((n): n is number => !isNaN(n))
6768

68-
// Extract distillation if present in the IDs array (packed as an object or long string)
69-
// or if we add a dedicated non-primitive argument.
70-
// For now, let's keep the schema simple and use the logic that Objects don't show in TUI.
71-
const distillation = (args as any).distillation;
7269
if (numericToolIds.length === 0) {
7370
logger.debug("No numeric tool IDs provided for pruning, yet prune tool was called: " + JSON.stringify(args))
74-
return "No numeric IDs provided. Format: [reason, id1, id2, ...] where reason is 'completion', 'noise', or 'consolidation'."
71+
return "No numeric IDs provided. Format: ids: [id1, id2, ...]"
7572
}
7673

7774
// Fetch messages to calculate tokens and find current agent

0 commit comments

Comments
 (0)