mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
# Conflicts: # .agents/notes/implemented/feature/2026-06-30-interception-seams.md # docs/config-catalog.md # docs/cookbook/adding-a-tool.i18n.yaml # docs/cookbook/adding-a-tool.md # docs/cookbook/adding-a-tool.zh.md # docs/cordis-catalog/events.md # docs/cordis-catalog/services.md # docs/core-data-structures/tools.md # docs/event-producer-consumer.md # docs/persistence-catalog.md # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl # packages/bash/tool-bash/src/index.ts # packages/core/agent-loop/src/tool-calls.ts # packages/core/agent-loop/tests/cancel.spec.ts # packages/core/agent-loop/tests/contract-regressions.spec.ts # packages/core/agent-loop/tests/tool-calls.spec.ts # packages/core/tools/README.md # packages/core/tools/src/index.ts # packages/core/tools/tests/code-mode.spec.ts # packages/core/tools/tests/tools.spec.ts # packages/fs/tool-fs-search/tests/integration.spec.ts # packages/fs/tool-fs-search/tests/tools.spec.ts # packages/fs/tool-fs/tests/integration.spec.ts # packages/mcp/mcp-client/src/tools.ts # packages/timeout/timeout-policy/tests/timeout-policy.spec.ts # packages/web/tool-web/tests/integration.spec.ts # packages/web/tool-web/tests/tool-web.spec.ts
76 lines
3.1 KiB
TypeScript
76 lines
3.1 KiB
TypeScript
/**
|
|
* Cooperative tool-call timeout enforcer. A tool declares `timeoutMs` and
|
|
* promises to honor `exec.signal`; this wrapper arms that deadline and maps its
|
|
* own expiry to `TOOL_TIMEOUT` without racing or abandoning the tool promise.
|
|
* @module @deepseek-ai/dsh-timeout-policy
|
|
*/
|
|
|
|
import type { Context } from 'cordis'
|
|
import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout'
|
|
import type { ToolExecutionResult } from '@deepseek-ai/dsh-tools'
|
|
|
|
/**
|
|
* The code owned by this plugin, used BOTH as the internal {@link deadline}
|
|
* classification code AND as the structured error `code` on the replacement
|
|
* tool result. Scoping {@link timeoutOf} to it keeps a nested outer deadline
|
|
* (another `tools/execute` wrapper's timer that fired first) from being misread
|
|
* as this plugin's own timeout — it reads as an ordinary upstream cancel.
|
|
*/
|
|
export const TOOL_TIMEOUT = 'TOOL_TIMEOUT'
|
|
|
|
/** Cordis plugin name used by loader diagnostics. */
|
|
export const name = 'timeout-policy'
|
|
|
|
/** The tool registry seam this plugin wraps (`tools/execute`) and reads (`get`). */
|
|
export const inject = ['tools']
|
|
|
|
/**
|
|
* The structured result substituted when this plugin's deadline wins. `content`
|
|
* is the model-facing message; `error.code` is the same {@link TOOL_TIMEOUT}
|
|
* this plugin owns, so a retry/sandbox plugin (and replay) can route on it.
|
|
*
|
|
* @param timeoutMs - the elapsed budget, rendered into the model-facing message.
|
|
* @returns the `isError` {@link ToolExecutionResult} with a `TOOL_TIMEOUT` error.
|
|
*/
|
|
function toolTimeoutResult(timeoutMs: number): ToolExecutionResult {
|
|
const message = `tool call timed out after ${timeoutMs}ms`
|
|
return {
|
|
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
isError: true,
|
|
error: { message, info: { name: 'ToolTimeoutError', code: TOOL_TIMEOUT } },
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register the timeout wrapper. It resolves the caller-visible tool definition,
|
|
* temporarily replaces `exec.signal`, delegates, restores the upstream signal,
|
|
* and replaces the result only when this wrapper's own timer fired.
|
|
*/
|
|
export function apply(ctx: Context): void {
|
|
ctx.on('tools/execute', async (exec, next): Promise<ToolExecutionResult> => {
|
|
const timeoutMs = ctx.tools.get(exec.name, exec.agent)?.timeoutMs
|
|
// A tool that declares no budget: no deadline, delegate unchanged.
|
|
if (timeoutMs === undefined) return next()
|
|
|
|
using d = deadline(exec.signal, timeoutMs, TOOL_TIMEOUT)
|
|
// Swap the derived deadline onto exec for dispatch, then restore the
|
|
// caller's own signal so post-execute listeners never see this plugin's
|
|
// (possibly already-aborted) timeout signal.
|
|
const upstream = exec.signal
|
|
exec.signal = d.signal
|
|
try {
|
|
const result = await next()
|
|
// If OUR timer fired (scoped by code — a nested outer deadline reads as
|
|
// undefined here), the tool/capability saw the abort and reached
|
|
// quiescence; replace whatever it returned (its own abort result) with the
|
|
// structured TOOL_TIMEOUT the model sees.
|
|
if (timeoutOf(d.signal, TOOL_TIMEOUT) !== undefined) {
|
|
return toolTimeoutResult(timeoutMs)
|
|
}
|
|
return result
|
|
} finally {
|
|
exec.signal = upstream
|
|
}
|
|
})
|
|
}
|