Tool Output Contract: Value Separated from Display
Same result — what the model sees and what people see can differ
THE QUESTION THIS PAGE ANSWERS
ANSWER FIRSTWhat is the key idea behind “Tool Output Contract: Value Separated from Display”?
Same result — what the model sees and what people see can differ
Make the claim earn its place. Use this page as a decision aid, not a definition to memorize. Connect the idea to one real task, one observable result, and one failure that would change your mind.
Write one question you could answer with evidence after trying this idea.
A conclusion that sounds complete but leaves the key assumption untested.
Waiting for execute()…
schema check
packages/core/tools/src/index.ts lines 211–219 and the card union in presentation.ts. Compare the two panes: same value, two totally different presentations.First, the title question: is a tool result a string or a structured value? In DSH it’s both — with different status. execute returns only one canonical JSON value, which must pass the tool’s own output.schema. Strings come later: the registry takes the validated value, calls render(args, value), and projects the content blocks the model sees.
So the chain is: execute produces the value, schema gates it, render projects model content, optional presentationMeta projects replayable UI data, and presentResult turns that into a card. Both render and presentResult are pure — no I/O — because they run on live streaming and session-log replay, and must produce the same output.
What the UI gets is render intent: a card-tagged union whose domain is six cards — generic, terminal, diff, read, search, web. The client only switches on card; it never needs tool names. Swap the search backend provider and the whole tool implementation — as long as it still emits a search card, the UI doesn’t change a line. That’s UI contract decoupled from tool implementation.
value lives only at execution timePersisted tool/result events store only content, error, and meta — the canonical value never hits disk. Replay can recreate every card and every model text, but not the intermediate value (docs/subsystems/tools.zh.md, “结果仅承载产出” section).
A broken projection ≠ a crashSchema failure, render throwing, or presentationMeta emitting non-JSON — all become a JSON-safe isError result. The model sees an error string; the pipeline still finishes. Source: createSuccessResult from line 1793 in index.ts.
Truncation must show its handSearch cards must carry truncated and total, so the UI never paints a cut result as complete (presentation.ts lines 223–231). Read cards likewise carry offset and totalLines, so they can show “lines N, of M total”.
The whole output contract is nine lines. schema is required, render is required, presentationMeta is optional. Both projector comments stress Pure — that’s the foundation of replay determinism.
/** Tool-owned canonical output contract used after the body returns a JSON value. */
export interface ToolOutputDefinition {
/** Raw supported JSON Schema enforced against every successful canonical value. */
readonly schema: JsonSchemaNode
/** Pure projection from validated arguments and value to Native/model content. */
render(args: unknown, value: JsonValue): ContentBlock[]
/** Pure replayable presentation projection, computed only for top-level calls. */
presentationMeta?(args: unknown, value: JsonValue): JsonValue
}
packages/core/tools/src/index.ts, verified on 2026-08-13. Code blocks keep the original source text.Value/display separation also explains a quirky post-execute plugin rule: on accept, you may replace content or value — never both. It’s not a doc convention; it’s baked into the types. PostToolDecision accept has two branches: one allows content and types value as never; the other allows value and types content as never. In TypeScript, never has no legal inhabitants — stuff both fields into one decision and the compiler errors. The third branch is block, turning corrective feedback into an error result.
Source:packages/core/tools/src/index.ts PostToolDecision type definition, lines 593–600, verified on 2026-08-13。
Why not both at once? Different semantics. Replacing content is a display-layer move: keep the value, change only model-facing text. Replacing value is a data-layer move: the registry re-runs schema on the new value, then recomputes content and meta so all three projections share one source. Allow both and you can get text saying A while the value is B. Docs add a key warning: content swap is a presentation tactic — plugins that must hide a value from programs must swap the value or block; rewriting text alone won’t fool Code Mode code that reads the value (docs/subsystems/tools.zh.md, “后置策略” section).
Two fallback questions are answered too. If render throws, the registry turns it into a JSON-safe isError and the model sees error text. If a third-party tool skips presentCall / presentResult, the client falls back to a generic card: title = tool name, raw args as input (index.ts lines 79–83 comments spell out this fallback). Nothing crashes; everything has a landing place.
Claude Code keeps rendering on the tool interface itself. The Tool interface has renderToolResultMessage() for UI and mapToolResultToToolResultBlockParam() for format conversion (study/chapters/02-tool-system.md lines 96–98 interface map). Tool files are .tsx; rendering is the tool’s own React component. Upside: authors control every pixel. Cost: swap clients (terminal → editor plugin) and you rewrite the render layer; replay must re-run render code. DSH turns that layer into data: tools only declare render intent; six card vocabularies are a neutral host↔client protocol — anyone can render.
Oversized-result handling lines up too: CC uses maxResultSizeChars — spill to disk, leave the model a preview plus path (study/chapters/02-tool-system.md lines 463–496). DSH’s counterpart is the spill policy, covered in Compaction Dual Paths. Both sides nailed the same point: someone must police tool-result bulk so it can’t blow up context.
Grok Build types tool output with Rust enums: e.g. search_replace yields SearchReplaceOutput, with InvalidInput, NoMatchesFound and other failure shapes fixed at compile time (crates/codegen/xai-grok-tools/src/implementations/grok_build/search_replace/mod.rs). Inputs are equally careful — model-facing canonical input is a stable projection; see Canonical input is a stable projection. Whether output UI and model text share a unified card vocabulary like DSH — no equivalent showed up in reviewed Grok materials; that conclusion stays provisional on public evidence.
Design an output contract for a SQL query tool
You’re integrating a third-party sql_query tool: the query returns 1200 rows but you keep only the first 50. Write: roughly what the value schema looks like (hint: rows, total, truncated are non-negotiable); whether render’s model text should include all 50 rows; which of the six cards presentResult should pick, and where truncation info goes. Final question: a security plugin wants to hide a phone-number column from the model — in post-execute, swap content or value? Think about what Code Mode programs actually receive.
The handoffs inside “Interactive demo · Dual-view gallery”
“First, the title question: is a tool result a string or a structured value?” shows that an Agent is not defined by the model alone. Each handoff between model, context, tools, state, permissions, and people affects both progress and recovery.
Write the state before adding capability
Starting from “So the chain is: execute produces the value, schema gates it, render projects model content, optional presentationMeta projects replayable UI data, and presentResult turns that int…”, split the workflow into starting state, next action, tool result, state update, and stop condition. Debugging then means finding the first lost piece of information or authority instead of saying vaguely that the model “got worse”.
A happy path is not reliability
Use “You’re integrating a third-party sql_query tool: the query returns 1200 rows but you keep only the first 50.” to replay one successful and one failed run. Record the context, tool result, and owner at each turn; the workflow is maintainable when a second person can follow it without the original builder.
From “Interactive demo · Dual-view gallery” to “Mechanism · One value, three projections”
“Interactive demo · Dual-view gallery” grounds the problem in “read · file grep · search no presentation render throws Play Step Reset canonical value Waiting for execute()… schema check Model view render(args, value) The text that enters context and is billed by token UI…”. “Mechanism · One value, three projections” then moves it toward “First, the title question: is a tool result a string or a structured value? In DSH it’s both — with different status. execute returns only one canonical JSON value, which must pass the tool’s own output.schema…”. Together, they show that the lesson is not just a conclusion to remember, but a claim with conditions.
Carry the judgment into the next situation
When analyzing an Agent, trace state, action, tool result, and next step in order. Each handoff should explain where information came from, who confirmed it, and where failure stops.
- “Interactive demo · Dual-view gallery”: read · file grep · search no presentation render throws Play Step Reset canonical value Waiting for execute()… schema check Model view render(args, value) The text that enters context and is billed by token UI…
- “Mechanism · One value, three projections”: First, the title question: is a tool result a string or a structured value? In DSH it’s both — with different status. execute returns only one canonical JSON value, which must pass the tool’s own output.schema…
- “The closing point”: Value/display separation also explains a quirky post-execute plugin rule: on accept, you may replace content or value — never both. It’s not a doc convention; it’s baked into the types. PostToolDecision accept…
The final “The closing point” brings the discussion to “Value/display separation also explains a quirky post-execute plugin rule: on accept, you may replace content or value — never both. It’s not a doc convention; it’s baked into the types. PostToolDecision accept…”. The useful thing to carry forward is knowing which judgments must be revisited when input, scale, or risk changes.
I turned one judgment from this article into a small experiment I could run today. Knowing what to observe next is more useful than simply remembering the conclusion.
After reading this, I first looked for the conditions behind the idea instead of copying the method into a project. That order made the later trade-offs much clearer.
When this judgment reaches real work, which constraint should be added first? I am curious which step matters most between reading and the first practical attempt.
No discussion on this article yet.