DeepSeek Harness DSH plugin tool execute contract: values
A DSH plugin tool is not "fine as long as it runs": the official tool authoring reference lays down eight execute rules, from parameter validation and read-only definitions to return values and error semantics, and breaking any one of them shows up on the real path (source). This article covers the contract clauses themselves — the minimal defineTool form and its four elements are already in tool plugin development, and how tools run inside the pipeline is in the tool pipeline explained, so neither is repeated here.
Parameter validation in a DSH plugin tool: what defineTool checks and what you still must
Parameters are already validated for you: defineTool checks the model-generated arguments against the unified ParameterSchemaSpec before execute runs, so args inside execute matches InferArgs (source). Know the boundaries:
- What it covers — types, required keys, literal constraints, unions that match exactly one branch, and nested values.
additionalPropertiesmust be explicit — explicit object nodes must declareadditionalProperties: true | false; an implicit parameter root object stays open. Skipping this lets illegal fields slip through silently.- Constraints the DSL cannot express are yours — non-empty strings, positive numbers, and cross-field rules are not expressible in the schema DSL, so check them inside
execute. - Raw JSON Schema tools self-validate — a raw JSON Schema
ToolDefinitionregistered directly (which is how MCP-sourced tools arrive) does not go throughdefineToolvalidation and owns its own input checks.
Remember one line: defineTool blocks type-and-structure errors; semantic errors are on you.
Read-only definitions and execution identity in a DSH plugin tool
Registration borrows your read-only definition, and execution identity is protected during dispatch (source). Two disciplines:
- Do not touch the definition after registering — a typed in-process contribution is not a serialization boundary, so do not modify the schema or replace callbacks afterward;
schemas()only materializes the explicit model-visible projection. To hot-swap a tool, dispose its owning effect and register a replacement; mutable state inside the callback closure is still ordinary plugin state and is fine to use. - Execution identity is frozen throughout — in one recursive pass the registry materializes
argumentsinto detached lossless JSON, freezes it before policy starts, and assigns an opaqueexec.token.callId,name,arguments,agent,token, the required caller-heldsignal, and the optional outer-transportparenttoken stay immutable for the whole dispatch;parentis identity only and never exposes an active outer execution. Treatargsas read-only input.
The one exception is exec.signal: only an around-dispatch wrapper receives a mutable view, and it may replace and restore the required exec.signal to impose a deadline, but it can never remove it. To wrap dispatch with a timeout, this is the only route.
What a DSH plugin tool should return: canonical JSON and isError semantics
Declaring and returning a canonical JSON value is the hard interface between the tool and its callers (source).
- Return only the derived value —
output.schemausesValueSchemaSpec, whose root may be an object, array, scalar, or null.executereturns only the derived value; the registry snapshots it as lossless JSON, validates and freezes it, then passes it tooutput.render(args, value). - Do not return content blocks — tool bodies should not return content blocks or force callers to parse ids and fields out of natural language. Doing so breaks both rendering and programmatic use (Code Mode's
ToolOutputMapderives from the same schemas). - An exception or invalid value means
isError— the registry catches exceptions and converges schema, renderer, metadata projector, and lossless JSON failures before observers run. Throw for infrastructure failures; a successful domain outcome — even an undesirable state like a process exiting non-zero — should be written as a canonical value that its Native renderer explains.
Signal, replay metadata, and async notifications in a DSH plugin tool
The last three rules are about runtime cooperation (source).
- Honor
exec.signal— cancel in-progress work when it fires. Passsignaldown through long I/O (for examplereadFile(path, { signal })) instead of ignoring it. - Project persisted card data with
presentationMeta(optional) —output.presentationMeta(args, value)derives replayable JSON from the same canonical value; the core persists it ontool/resultand passes it topresentResult, so result-time facts such as applied hunks fromwrite/editreappear during replay without persisting the canonical value. Nested Code dispatch has no card and skips this projector. - Send async notifications with
exec.agent—agent.inject({ content, source: { kind: 'plugin', plugin: '<name>' } })appends persisted context that the next model request will see; this is not a wake-up, and an idle agent stays idle. Guard against a disposed agent (wrap in try/catch).
Four self-checks when done: did explicit object nodes declare additionalProperties; does anything still mutate the schema or swap callbacks after registration; does execute return only canonical values instead of assembled text; and is exec.signal passed down through long operations. To compare production-grade implementations, find tool plugins on the DSH Plugin Hub; for how tool results render see the tool card render contract, and for where gates, timeouts, and observation belong see the feature-to-mechanism map.
FAQ
A DSH plugin defineTool validates the model-generated arguments before execute runs, using the unified ParameterSchemaSpec — types, required keys, literal constraints, unions that match exactly one branch, and nested values — so args inside execute matches InferArgs. But explicit object nodes must declare additionalProperties: true | false, constraints the schema DSL cannot express (non-empty strings, positive numbers, cross-field rules) still need manual checks, and raw JSON Schema tools you register directly are responsible for their own input validation.
No — once a DSH plugin tool is registered it borrows your read-only definition, so you cannot change the schema or swap the execute callback afterward. A typed in-process contribution is not a serialization boundary, and schemas() only materializes the explicit model-visible projection. To hot-swap a tool, dispose its owning effect and register a replacement; mutable state inside the callback closure is still ordinary plugin state.
execute in a DSH plugin should return only the derived canonical JSON value: output.schema uses ValueSchemaSpec, whose root may be an object, array, scalar, or null, and the registry snapshots the return value as lossless JSON, validates and freezes it, then passes it to output.render(args, value). Tool bodies should not return content blocks or force callers to parse ids and fields out of natural language.
In a DSH plugin tool, throwing an exception or returning an invalid value both mean isError: the registry catches exceptions and converges schema, renderer, metadata projector, and lossless JSON failures before observers run. Throw for infrastructure failures; a successful domain outcome — even an undesirable state like a process exiting non-zero — should be written as a canonical value that its Native renderer can explain.
In a DSH plugin, exec.token is an opaque, fully immutable execution identity: callId, name, arguments, agent, token, signal, and the optional parent stay immutable throughout dispatch, and args must be treated as read-only input. exec.signal is the only replaceable operational field — only an around-dispatch wrapper may replace and restore it to impose a deadline, and it can never remove the signal; execute must honor it and cancel in-progress work when it fires.
Related Terms
- ParameterSchemaSpec
- ParameterSchemaSpec is DeepSeek Harness's unified parameter schema description: defineTool validates the model-generated arguments against it before execute runs, covering types, required keys, literal constraints, unions that match exactly one branch, and nested values, after which args matches InferArgs.— https://deepseek-harness.github.io/deepseek-harness/en/reference/cookbook/adding-a-tool
- exec.token
- exec.token is the opaque execution identity token the registry assigns before dispatch; together with callId, name, arguments, agent, signal, and the optional parent it stays immutable for the whole dispatch, used only for identity and never exposing an active outer execution.— https://deepseek-harness.github.io/deepseek-harness/en/reference/cookbook/adding-a-tool
- ValueSchemaSpec
- ValueSchemaSpec is the return-value schema specification used by output.schema, whose root may be an object, array, scalar, or null; execute returns only the derived canonical JSON value, which the registry snapshots, validates, and freezes before handing it to output.render.— https://deepseek-harness.github.io/deepseek-harness/en/reference/cookbook/adding-a-tool
- presentationMeta
- presentationMeta is the optional output.presentationMeta(args, value) projector that derives replayable JSON from the same canonical value; the core persists it on tool/result and passes it to presentResult, so result-time facts can be reproduced during replay without persisting the canonical value.— https://deepseek-harness.github.io/deepseek-harness/en/reference/cookbook/adding-a-tool
Sources
- DeepSeek Harness official docs - Tool authoring reference· deepseek-ai
- DeepSeek Harness official docs - Adding a tool· deepseek-ai