Files
deepseek-harness/packages/timeout/timeout-policy/README.md
T
Dudu-0223 8190016e2b feat(timeout): add tools/execute seam + tool-timeout policy plugin
Model-facing tool-call budgets were tangled into each capability's schema
(bash timeoutMs, web_fetch timeout_ms) with no shared home. Add a
tools/execute around-dispatch waterfall to dsh-tools whose base next() is
the dispatch-with-normalization thunk, and a new @deepseek-ai/dsh-timeout-policy
plugin (packages/timeout/) that arms a per-tool deadline on exec.signal and
returns a structured TOOL_TIMEOUT when it wins. Migrate web_fetch (drop the
model-facing timeout_ms) and web_search onto it; the fetch provider keeps its
timeout only as a resource backstop for direct callers. bash and hook command
execution keep BASH_TIMEOUT unchanged.

Named the plugin timeout-policy (not the RFC's tool-timeout) so it does not
trip the gen-tool-catalog packages/*/tool-* completeness guard, and replace
exec.signal by in-place mutation before next() since cordis waterfall next()
ignores passed arguments. RFC moved to implemented/ recording both deviations.
2026-07-08 10:10:37 +08:00

3.6 KiB

dsh-timeout-policy

Tool-call timeout policy: a single tools/execute around-dispatch listener that arms a per-call cooperative deadline on exec.signal for each configured tool and returns a structured TOOL_TIMEOUT result when that deadline wins. It is the reference tools/execute wrapper and the deployment-owned home for model-facing tool-call budgets (the timeout-library RFC's foreseen middleware).

Plugin (namespace: timeout-policy)

A function/namespace plugin (name / Config / apply), not a service. It registers no tool and injects nothing — it consumes ctx.tools's tools/execute waterfall, which the dsh-tools registry always provides.

Config

Per-tool policy, keyed by the model-facing tool name. There is deliberately no global default (a global budget would silently start failing any tool that runs long once the plugin loads) and no model-facing override (timeout is deployment policy, not prompt semantics) in this version.

- id: timeout-policy
  name: '@deepseek-ai/dsh-timeout-policy'
  config:
    tools:
      web_fetch:
        timeoutMs: 30000
      web_search:
        timeoutMs: 30000
Key Type Meaning
tools Record<string, { timeoutMs }> Per-tool timeout policy; an unlisted tool gets no deadline. timeoutMs is required per configured tool and must be positive finite.

Behavior

For a configured tool the listener:

  1. Arms deadline(exec.signal, timeoutMs, 'TOOL_TIMEOUT') — one signal fusing the caller's abort with this plugin's timer (@deepseek-ai/dsh-timeout).
  2. Swaps that derived signal onto exec for the downstream dispatch, then restores the caller's own signal afterward (cordis next() ignores passed arguments, so the wrapper mutates the shared exec in place; restoring keeps tools/post-execute seeing the caller's signal).
  3. After dispatch, if timeoutOf(d.signal, 'TOOL_TIMEOUT') matches — this plugin's own timer fired — replaces the result with a structured TOOL_TIMEOUT tool result: { isError: true, error: { name: 'ToolTimeoutError', code: 'TOOL_TIMEOUT' }, content: 'Error: tool call timed out after <ms>ms' }.

An unconfigured tool delegates untouched (no deadline).

The base next() of tools/execute is the registry's dispatch-with-normalization thunk, so when the timeout signal reaches a provider that throws its own upstream-abort error, dispatch first turns it into a normal error result, and this wrapper then replaces that with TOOL_TIMEOUT. That ordering is why the replacement is keyed off the signal (timeoutOf), not off the dispatched result's shape.

Cooperative, not a hard kill

The derived signal only notifies; termination stays with the tool and the capability it forwards exec.signal to (the dsh-timeout library owns no kill). "Configured" therefore means "cooperative with exec.signal": a tool that ignores the signal will not stop on timeout. A deployment must only configure tools that forward the signal to their implementation — the shipped web_fetch/web_search (which forward through ctx.web to providers) are the reference. TOOL_TIMEOUT needs no session event for reconstructability: it is the final model-facing tool/result, already logged by the loop.

Composing with other tools/execute wrappers

Multiple tools/execute listeners compose by cordis registration order. Combined with a future retry/sandbox/metrics wrapper, registration order chooses the semantics — "timeout covers the whole retry operation" (timeout registered outer) versus "timeout covers each attempt" (timeout registered inner).