| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081 |
- /**
- * 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.
- *
- * FIXME: settle the intended `@deepseek-ai/dsh-timeout-guard` rename before the
- * first tagged release — suggestion only, aligning the name with its `guard/`
- * home; decide at resolution time
- * ([regrouping Agent Note](../../../../.agents/notes/archived/architecture/2026-07-29-package-regrouping.md)).
- *
- * @module @deepseek-ai/dsh-tool-call-timeout-policy
- */
- import type { Context } from '@deepseek-ai/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 service 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
- }
- })
- }
|