Special Topic · Inside DeepSeek Harness

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 FIRST

What is the key idea behind “Tool Output Contract: Value Separated from Display”?

Same result — what the model sees and what people see can differ

DECISION RULE

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.

TRY NEXT

Write one question you could answer with evidence after trying this idea.

WATCH FOR

A conclusion that sounds complete but leaves the key assumption untested.

Course goalAfter this lesson you can explain three things: in DSH a tool’s return is a schema-validated structured JSON value, and both the model-facing text and the UI card are projections of that value; the UI renders via a card-tagged union type and never needs to know tool names; persistence stores projections, not the value — so replay can recreate every card, but can never rebuild the intermediate value.
Interactive demo · Dual-view gallery
canonical value Waiting for execute()… schema check
Model viewrender(args, value)
The text that enters context and is billed by token
UI viewpresentResult(args, result)
Render intent for the client: a card tagged with card
Session log to disk: content meta value
Pick a scenario and hit Play, or scroll here to auto-play the read scenario.
Teaching simulation: values, text, and cards are course-adapted examples. The projection relation matches the output contract in 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.
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. 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 time

Persisted 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 crash

Schema 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 hand

Search 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”.

Core visual · Projection relationship
execute() returns canonical value (JSON) output.schema enforced every time render(args, value) content blocks model-facing, into context presentationMeta(args, value) meta presentation data replayable, persisted with the log presentResult(args, result) card render intent UI-facing, switch(card) session log content + meta value never persisted discarded when execution ends
Teaching diagram: three projections map to ToolOutputDefinition in index.ts lines 211–219 and the two present callbacks at lines 84–92.
Key evidence · Contract & either-or

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.

packages/core/tools/src/index.tslines 211–219
/** 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
}
Source snapshot note:Based on the local deepseek-harness-master repo; verified against 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.

Side-by-side · Where rendering lives

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.

Classroom Exercise
01

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.

Takeaway:A tool produces a schema-validated value; model text and UI cards are pure projections of it — pick one channel per change, never mix. The UI knows card tags, not tool names; swap implementations without touching the UI. Persistence stores projections, not values: replay recreates every display; the value itself vanishes when execution ends.

“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.

“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.

Mark as learned Your reading progress updates automatically
← PreviousNext →

Keep reading

The next useful article in the thread.

ARTICLE DISCUSSION

Leave one useful thought here.

Keep the idea that clicked, the question that stayed open, or a small note for the next learner.

Discussing Tool Output Contract: Value Separated from Display Inside DeepSeek Harness
3discussionsArticle discussion · synced with the Circle
View in the learning circle
AM
Asha MorganContent editor
INSIGHTField note

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.

ARTICLE DISCUSSION7 helpful
LH
Lin HarperIndie developer
INSIGHTInsight

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.

ARTICLE DISCUSSION5 helpful
KM
Kiki MooreProduct operations
QUESTIONQuestion

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.

ARTICLE DISCUSSION4 helpful