mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(tools): complete cancellation boundary
This commit is contained in:
@@ -1224,7 +1224,7 @@ export interface Config {
|
||||
export type ToolPresentationMode = 'native' | 'code' | 'both'
|
||||
```
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:397`](../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:399`](../packages/core/tools/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-tui`
|
||||
|
||||
|
||||
@@ -697,7 +697,7 @@ A tool was registered or unregistered, or a scoped restriction changed (the avai
|
||||
'tools/change'(): void
|
||||
```
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:120`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:122`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
### `tools/execute` — waterfall
|
||||
|
||||
@@ -723,12 +723,14 @@ Source: [`packages/core/tools/src/index.ts:93`](../../packages/core/tools/src/in
|
||||
|
||||
### `tools/post-execute` — waterfall
|
||||
|
||||
Accept, replace, enrich, or block a normalized dispatch result. `next()` accepts it unchanged; thrown tools still reach this seam as errors. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls.
|
||||
Accept, replace, enrich, or block a normalized dispatch result. `next()` accepts it unchanged; thrown tools still reach this seam as errors. Async listeners must observe `exec.signal`; after they settle, caller cancellation replaces only a successful accepted outcome with `ABORTED`. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Accept, replace, enrich, or block a normalized dispatch result. `next()`
|
||||
* accepts it unchanged; thrown tools still reach this seam as errors.
|
||||
* accepts it unchanged; thrown tools still reach this seam as errors. Async
|
||||
* listeners must observe `exec.signal`; after they settle, caller
|
||||
* cancellation replaces only a successful accepted outcome with `ABORTED`.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls.
|
||||
* @param exec - the call that just ran (name, parsed arguments, caller agent).
|
||||
* @param result - the dispatch outcome a listener may accept, replace, or block.
|
||||
@@ -739,7 +741,7 @@ Accept, replace, enrich, or block a normalized dispatch result. `next()` accepts
|
||||
|
||||
Types: [PostToolDecision](../core-data-structures/tools.md) · [Scoped](../core-data-structures/scope.md) · [ToolExecution](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md) · [ToolRegistry](../core-data-structures/tools.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:102`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:104`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
### `tools/pre-execute` — waterfall
|
||||
|
||||
@@ -779,7 +781,7 @@ Observe the frozen, lossless-JSON final outcome. Listener failures are contained
|
||||
|
||||
Types: [Scoped](../core-data-structures/scope.md) · [ToolExecution](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md) · [ToolRegistry](../core-data-structures/tools.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:110`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:112`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
## `workflow/*`
|
||||
|
||||
|
||||
@@ -1175,9 +1175,10 @@ executionMode(exec: ToolExecutionInput): ToolExecutionMode
|
||||
* notification. Tool and listener failures resolve as materialized error
|
||||
* results; an invisible tool reports `UNKNOWN_TOOL`. The returned outcome is
|
||||
* the same lossless, frozen snapshot final observers receive. Cancellation
|
||||
* arriving after entry skips a not-yet-started body or replaces a successful
|
||||
* dispatch outcome with `ABORTED`; already-started work is still drained and
|
||||
* may retain a tool-owned structured error.
|
||||
* arriving after entry and before final result materialization skips a
|
||||
* not-yet-started body or replaces a successful pipeline outcome with
|
||||
* `ABORTED`; already-started work is still drained and may retain a
|
||||
* tool-owned structured error.
|
||||
* @param exec - the typed same-process call input. The registry assigns its
|
||||
* correlation token before policy begins.
|
||||
* @returns the materialized final result.
|
||||
@@ -1187,7 +1188,7 @@ async execute(exec: ToolExecutionInput): Promise<ToolExecutionResult>
|
||||
|
||||
Types: [ScopeKey](../core-data-structures/scope.md) · [ToolDefinition](../core-data-structures/tools.md) · [ToolExecutionInput](../core-data-structures/tools.md) · [ToolExecutionMode](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md) · [ToolGuard](../core-data-structures/tools.md) · [ToolRestriction](../core-data-structures/tools.md) · [ToolSchema](../core-data-structures/tools.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:465`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:467`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
## `ctx.userInteraction` — `UserInteractionService`
|
||||
|
||||
|
||||
@@ -38,11 +38,11 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:103`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude) |
|
||||
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:27`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`acp`](../packages/ui/acp) |
|
||||
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:33`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
|
||||
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:120`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - |
|
||||
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:122`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - |
|
||||
| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:93`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`timeout-policy`](../packages/timeout/timeout-policy) |
|
||||
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:102`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`spill-policy`](../packages/spill/spill-policy), [`workspace-context`](../packages/context/workspace-context) |
|
||||
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:104`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`spill-policy`](../packages/spill/spill-policy), [`workspace-context`](../packages/context/workspace-context) |
|
||||
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:82`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:110`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`workspace-context`](../packages/context/workspace-context) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:112`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`workspace-context`](../packages/context/workspace-context) |
|
||||
| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:81`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - |
|
||||
| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:70`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - |
|
||||
| `workflow/end` | `emit` | [`packages/workflow/workflow/src/index.ts:91`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - |
|
||||
|
||||
@@ -167,6 +167,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand;
|
||||
| [Initiating Agent scope over AsyncLocalStorage](implemented/architecture/2026-07-15-agent-initiator-scope.md) | 2026-07-15 |
|
||||
| [Advisory LLM catalogs and per-session ACP model selection](implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md) | 2026-07-15 |
|
||||
| [Replay token meter service](implemented/architecture/2026-07-15-replay-token-meter-service.md) | 2026-07-15 |
|
||||
| [Cooperative tool cancellation at the registry boundary](implemented/architecture/2026-07-19-cooperative-tool-cancellation.md) | 2026-07-19 |
|
||||
|
||||
### Process
|
||||
|
||||
|
||||
@@ -50,9 +50,9 @@ The plugin is `@deepseek-ai/dsh-timeout-policy`, a zero-config function/namespac
|
||||
searchTimeoutMs: 30000
|
||||
```
|
||||
|
||||
Timeouts live on tool definitions rather than a free-text name map, eliminating misspelled unused policy. `defineTool` validates a positive finite budget. During dispatch the enforcer derives a deadline signal, restores the caller signal afterward, and converts its own expiry into `TOOL_TIMEOUT`; tools without a budget pass through unchanged.
|
||||
Timeouts live on tool definitions rather than a free-text name map, eliminating misspelled unused policy. `defineTool` validates a positive finite budget. During dispatch the enforcer derives a deadline signal and assigns it to `exec.signal`; the registry fuses that deadline with the original caller signal before the body under the [tool-cancellation contract](2026-07-19-cooperative-tool-cancellation.md). The enforcer restores the caller signal afterward and converts its own expiry into `TOOL_TIMEOUT`; tools without a budget pass through unchanged.
|
||||
|
||||
Signal replacement is by **in-place mutation of `exec.signal`**, not by passing a new object to `next()`. Cordis's waterfall `next()` ignores any arguments handed to it and re-invokes downstream listeners with the shared payload array (`vendor/cordis/src/events.ts`), so the documented cordis idiom — mutate the shared object, then delegate — is the only mechanism that reaches dispatch. The plugin restores `exec.signal` to the caller's original in a `finally` so `tools/post-execute` never sees this plugin's (possibly already-aborted) deadline signal.
|
||||
Signal replacement is by **in-place mutation of `exec.signal`**, not by passing a new object to `next()`. Cordis's waterfall `next()` ignores any arguments handed to it and re-invokes downstream listeners with the shared payload array (`vendor/cordis/src/events.ts`), so mutation is how the wrapper supplies its deadline to the registry. The registry re-fuses the captured caller signal immediately before the body, and the plugin restores `exec.signal` to the caller's original in a `finally` so `tools/post-execute` never sees the plugin's deadline signal.
|
||||
|
||||
`timeout-policy` owns both uses of the `TOOL_TIMEOUT` code: the internal deadline code passed to `deadline()`/`timeoutOf()` (scoped so a nested outer deadline reads as an ordinary cancel) and the structured tool-result error code. Its replacement result is:
|
||||
|
||||
@@ -104,6 +104,6 @@ A future model-facing grep/glob tool can be implemented on top of `ctx.bash` wit
|
||||
|
||||
- `@deepseek-ai/dsh-tools` gains an around-dispatch surface after the interception seams deliberately split pre/post tool hooks. Its contract is narrow — wrap registry dispatch, not replace the pre-gate or post-result policy — and the base `next()` is dispatch-with-normalization so a wrapper never sees a raw tool throw.
|
||||
- Multiple `tools/execute` listeners compose by ordinary Cordis waterfall order: a listener that calls `next()` wraps downstream listeners plus dispatch; one that returns without `next()` short-circuits them. A deployment combining timeout with a future retry/sandbox/metrics wrapper chooses semantics by registration order ("timeout covers the whole retry" vs "timeout covers each attempt").
|
||||
- Opt-in by declaration is a deliberate misconfiguration risk: a tool can declare a `timeoutMs` without honoring `exec.signal`, and that tool will not stop on timeout. The plugin contract states that declaring a budget means cooperative; the web tools prove the pattern on tools that already forward the signal.
|
||||
- Opt-in by declaration is a deliberate misconfiguration risk: a tool can declare a `timeoutMs` without honoring `exec.signal`, and that tool will not stop on timeout. The registry awaits that non-quiescent body rather than racing it, while the plugin contract states that declaring a budget means cooperative; the web tools prove the pattern on tools that already forward the signal.
|
||||
- During the transition `bash` and the migrated web tools use different timeout paths on purpose: `TOOL_TIMEOUT` is the model-facing tool-call budget, while `BASH_TIMEOUT` remains the bash backend timeout used by bash and hooks.
|
||||
- Deviation from the literal proposal, recorded per the implemented-RFC rule: the plugin package is `@deepseek-ai/dsh-timeout-policy` (not `tool-timeout`), signal replacement is in-place `exec.signal` mutation before `next()` (not `next({ ...exec, signal })`, which cordis ignores), and the per-tool budget is declared on the `ToolDefinition` (`timeoutMs`, set by the owning tool plugin from its config) rather than mapped by tool name in this plugin's config — so the enforcer is zero-config and a mistyped tool name is impossible. All three are described in `## Decision` above.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-19-cooperative-tool-cancellation.md: d810b8d31aceffd10c10014c3f53173ca81f283f
|
||||
2026-07-19-cooperative-tool-cancellation.zh.md: b0e0d6107b385ca19917bc8379a04814504d0283
|
||||
@@ -0,0 +1,65 @@
|
||||
# RFC: Cooperative tool cancellation at the registry boundary
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-19-cooperative-tool-cancellation.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Every registered tool receives an optional `AbortSignal`, but a signal alone does not define a reliable cancellation boundary. Cancellation can arrive while pre-execution policy or approval is waiting, while an around-dispatch wrapper is waiting before or after delegation, or after the tool body has started. If each tool and wrapper interprets those races independently, a body can start after its caller has cancelled or a late success can escape after cancellation.
|
||||
|
||||
Around-dispatch plugins also need to replace `exec.signal` to add deadlines or other operational cancellation. Treating that mutable slot as the only caller signal lets a wrapper accidentally detach caller cancellation. Forbidding replacement would remove the lexical composition used by the [tool-call timeout policy](2026-07-07-tool-call-timeout-policy.md).
|
||||
|
||||
Returning `ABORTED` by racing the tool promise is not a safe fallback. Same-process JavaScript keeps running after the losing promise is abandoned, so subprocesses, network activity, nested dispatches, and deferred context production can outlive the reported result. The registry cannot generically hard-kill that work because termination belongs to the capability that owns it, as established by the [timeout/deadline decision](2026-07-06-timeout-deadline-library.md).
|
||||
|
||||
## Decision
|
||||
|
||||
`ToolRegistry` owns a cooperative, quiescent cancellation boundary for every call through `ctx.tools.execute()`. It preserves caller cancellation independently of around-dispatch mutation, prevents a body from starting after live cancellation, awaits every body that did start, and lets cancellation that wins before final result materialization supersede every successful pipeline outcome.
|
||||
|
||||
This is a control-plane guarantee, not universal hard termination. Every asynchronous `ToolDefinition.execute()` observes or forwards `exec.signal` and settles only after its owned work stops. The registry does not claim bounded-time settlement for same-process code that violates that contract.
|
||||
|
||||
### Caller cancellation survives the pipeline
|
||||
|
||||
The registry captures the caller's signal and whether it was already aborted when it materializes the execution. That state is kept outside the wrapper-mutable `ToolRunContext`.
|
||||
|
||||
A signal that was live on entry is rechecked after `tools/pre-execute`, approval, and immediately before the tool body. Cancellation during any of those waits yields structured `ABORTED` without starting the body. Immediately before dispatch, the registry fuses the original caller signal with the current wrapper-supplied `exec.signal`, so adding, replacing, or removing the public slot cannot detach the caller from a running body. Dispatch-scoped listeners are removed when the body settles.
|
||||
|
||||
The registry also rechecks the original caller after the around-dispatch waterfall and post-result policy settle. A wrapper or post-policy listener cannot return a late successful result after caller cancellation merely because the body completed earlier. A wrapper- or policy-owned failure remains a failure; the timeout-policy wrapper may therefore classify its own winning deadline as `TOOL_TIMEOUT` instead of losing that information to generic cancellation.
|
||||
|
||||
### Started work reaches quiescence
|
||||
|
||||
Once `ToolDefinition.execute()` starts, the registry awaits it. Cancellation that arrives after the body starts notifies it through the fused signal but does not race or abandon its promise. If the body settles successfully after that cancellation, the registry replaces success with `{ name: 'AbortError', code: 'ABORTED' }`; a structured tool failure remains the more specific result. Context deferred by a composite tool is retained when generic cancellation replaces success.
|
||||
|
||||
This applies even to an uncooperative body: the registry remains pending until the body settles. That cost is deliberate because returning early would make the call appear complete while its side effects remain live. Process, worker, network, and provider implementations supply their own termination mechanism and use the signal to reach quiescence; the registry only owns dispatch and result integrity.
|
||||
|
||||
A cancellation result produced before `tools/post-execute` continues through that policy; cancellation while an asynchronous post listener is waiting replaces only its successful outcome. The frozen `tools/result` notification is the completion boundary, and the agent loop records the resulting model-visible `tool/result`, preserving reconstructability.
|
||||
|
||||
### Pre-aborted entry is a distinct direct-call contract
|
||||
|
||||
A signal already aborted when registry entry begins still reaches the tool body. Direct service callers use that state for capability-specific cleanup or error translation, and the more specific result remains observable. The agent-loop scheduler does not start a new model-driven body under an already-aborted turn signal, so this exception does not reopen late model dispatch.
|
||||
|
||||
## Verification
|
||||
|
||||
[`tools.spec.ts`](../../../../packages/core/tools/tests/tools.spec.ts) pins cancellation during pre-policy and around/post waits, signal replacement and removal, no-late-success behavior, context retention, started-body drainage, and pre-aborted direct entry. [`tool-calls.spec.ts`](../../../../packages/core/agent-loop/tests/tool-calls.spec.ts) and [`contract-regressions.spec.ts`](../../../../packages/core/agent-loop/tests/contract-regressions.spec.ts) pin the no-late-start rule and balanced session-log results for undispatched sibling calls. [`timeout-policy.spec.ts`](../../../../packages/timeout/timeout-policy/tests/timeout-policy.spec.ts) pins caller-cancel-first and timeout-owned classification.
|
||||
|
||||
No registry test can prove that arbitrary third-party same-process code stops in bounded time. Capability tests remain responsible for proving their subprocess, worker, socket, or provider cancellation reaches quiescence.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Race the tool promise against cancellation.** Rejected because it reports completion while the losing promise and its side effects remain live. This violates the [quiescent-disposal rule](../../../defensive-patterns.md#dispose-must-reach-quiescence-not-just-request-it) and can let work mutate state after the session records `ABORTED`.
|
||||
|
||||
**Make the registry hard-kill every tool.** Rejected because same-process JavaScript has no safe generic preemption mechanism, while real termination differs by capability: process groups need signals and escalation, workers need termination, and network clients need protocol-aware abort. Moving those mechanisms into `ToolRegistry` would couple the core registry to every implementation.
|
||||
|
||||
**Trust each tool and around wrapper to preserve caller cancellation.** Rejected because the mutable signal slot and asynchronous pre/around waits form one shared scheduling boundary. Central capture and rechecks give every registered tool the same no-late-start and no-late-success rules without duplicating race handling.
|
||||
|
||||
**Forbid around wrappers from replacing `exec.signal`.** Rejected because deadlines and nested operational scopes need to derive a signal for one lexical dispatch. Re-fusing the caller immediately before the body preserves both composition and cancellation.
|
||||
|
||||
**Skip every call whose signal is aborted at entry.** Rejected because direct callers may need the tool body to perform cleanup or translate cancellation into a capability-specific result. The registry distinguishes that explicit entry state from a live signal that aborts during scheduling, while the agent loop independently prevents new model-driven dispatch after turn cancellation.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Every registry invocation has one service-layer cancellation contract, including tools supplied by plugins or MCP bridges, but only cooperative implementations are guaranteed to stop promptly.
|
||||
- Caller cancellation is monotonic across pre-policy, around-dispatch, and post-policy success: once a live caller signal aborts before final materialization, a body does not start late and a normal success does not become authoritative.
|
||||
- Started work can delay cancellation indefinitely when an implementation ignores its signal. The registry deliberately exposes that defect as a non-quiescent call instead of hiding it behind an early result.
|
||||
- Capability-specific failures and timeout ownership remain intact. Generic `ABORTED` replaces success, not a more informative error result.
|
||||
- Around wrappers retain signal replacement as their composition mechanism, while the original caller signal remains non-detachable at dispatch.
|
||||
@@ -0,0 +1,65 @@
|
||||
# RFC: 注册表边界上的协作式工具取消
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-19-cooperative-tool-cancellation.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
每个已注册工具都会收到可选的 `AbortSignal`,但仅提供信号不足以构成可靠的取消边界。取消可能发生在执行前策略或审批等待期间、环绕调度包装层委托前后的等待期间,或工具主体启动后。若各工具和包装层各自处理这些竞态,调用方取消后工具主体仍可能启动,延迟完成的成功结果也可能在取消后生效。
|
||||
|
||||
环绕调度插件还需要替换 `exec.signal`,以加入截止时间或其他运行时取消来源。若把这个可变槽位视为唯一的调用方信号,包装层就可能意外切断调用方的取消。禁止替换又会移除[工具调用超时策略](2026-07-07-tool-call-timeout-policy.md)所采用的词法作用域组合方式。
|
||||
|
||||
通过工具 promise 与取消竞速来返回 `ABORTED` 也不安全。同进程 JavaScript 即使在竞速中落败、其 promise 被丢弃,仍会继续运行,因此子进程、网络活动、嵌套调度和延后产生的上下文都可能超过已报告结果的生命周期。注册表无法用通用方式强制终止这些工作,因为终止机制属于工作所属的能力,正如[超时与截止时间决策](2026-07-06-timeout-deadline-library.md)所规定。
|
||||
|
||||
## 决策
|
||||
|
||||
`ToolRegistry` 为每次通过 `ctx.tools.execute()` 发起的调用提供协作式、保证完全停稳的取消边界。它独立于环绕调度对执行对象的修改来保留调用方取消,阻止工具主体在取消后才启动,等待所有已经启动的工具主体完成,并让最终结果物化前先发生的取消覆盖所有成功的流水线结果。
|
||||
|
||||
这项保证只覆盖控制平面,不等同于通用的强制终止。所有异步 `ToolDefinition.execute()` 都必须观察或转发 `exec.signal`,并且仅在自己负责的工作停止后完成。同进程代码若违反这项契约,注册表不保证其能在有界时间内完成。
|
||||
|
||||
### 调用方取消不会在流水线中丢失
|
||||
|
||||
注册表在物化执行对象时捕获调用方信号,并记录该信号在进入时是否已经中止。这份状态存放在包装层可修改的 `ToolRunContext` 之外。
|
||||
|
||||
对于进入时仍有效的信号,注册表会在 `tools/pre-execute`、审批以及工具主体启动前再次检查。若取消发生在这些等待期间,注册表会返回结构化 `ABORTED`,且不会启动工具主体。调度前一刻,注册表把原始调用方信号与包装层当前提供的 `exec.signal` 融合,因此无论包装层新增、替换还是移除公开槽位,都无法让运行中的工具主体脱离调用方取消。仅属于本次调度的监听器会在工具主体完成时移除。
|
||||
|
||||
环绕调度 waterfall(瀑布式事件)和结果后置策略完成后,注册表还会再次检查原始调用方信号。即使工具主体更早完成,包装层或后置策略监听器也不能在调用方取消后返回延迟成功结果。包装层或策略自身产生的失败仍按失败处理,因此 timeout-policy 包装层可以把自身先到达的截止时间归类为 `TOOL_TIMEOUT`,而不会被通用取消覆盖。
|
||||
|
||||
### 已启动的工作必须完全停稳
|
||||
|
||||
`ToolDefinition.execute()` 一旦启动,注册表就会等待它完成。工具主体启动后发生的取消会通过融合信号通知它,但注册表不会与其 promise 竞速,也不会丢弃该 promise。若工具主体在这次取消后仍以成功结果完成,注册表会用 `{ name: 'AbortError', code: 'ABORTED' }` 替换成功结果;工具自身的结构化失败仍是信息更具体的结果。通用取消替换成功结果时,会保留组合工具延后附加的上下文。
|
||||
|
||||
即使工具主体不协作,这项规则仍然适用:注册表调用会保持未完成,直到工具主体完成。这项代价是刻意保留的,因为提前返回会让调用看似已经结束,但其副作用仍在运行。进程、worker、网络和提供方实现各自提供终止机制,并使用信号使工作完全停稳;注册表只负责调度与结果完整性。
|
||||
|
||||
在 `tools/post-execute` 之前产生的取消结果会继续经过该策略;若取消发生在异步后置监听器等待期间,注册表只替换其成功结果。冻结的 `tools/result` 通知是完成边界,agent loop(智能体循环)会记录最终的模型可见 `tool/result`,从而保持可重建性。
|
||||
|
||||
### 进入时已中止属于独立的直接调用契约
|
||||
|
||||
若信号在进入注册表时已经中止,工具主体仍会收到它。直接调用服务的代码可利用该状态执行能力特定的清理或错误转换,信息更具体的结果也会保持可见。agent loop 调度器不会在轮次信号已经中止时启动新的模型驱动工具主体,因此这项例外不会重新允许模型工具延迟调度。
|
||||
|
||||
## 验证
|
||||
|
||||
[`tools.spec.ts`](../../../../packages/core/tools/tests/tools.spec.ts) 固定了执行前策略、环绕调度和后置策略等待期间的取消行为,以及信号替换与移除、禁止延迟成功、上下文保留、已启动工具主体排空和进入前已中止的直接调用行为。[`tool-calls.spec.ts`](../../../../packages/core/agent-loop/tests/tool-calls.spec.ts) 与 [`contract-regressions.spec.ts`](../../../../packages/core/agent-loop/tests/contract-regressions.spec.ts) 固定了禁止延迟启动的规则,以及未调度同批调用在会话日志中仍具有配对结果。[`timeout-policy.spec.ts`](../../../../packages/timeout/timeout-policy/tests/timeout-policy.spec.ts) 固定了调用方先取消和超时归属方分类行为。
|
||||
|
||||
任何注册表测试都无法证明任意第三方同进程代码会在有界时间内停止。各能力的测试仍需证明其子进程、worker、套接字或提供方取消能够使工作完全停稳。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**让工具 promise 与取消竞速。** 不予采纳,因为这种方式会在落败的 promise 及其副作用仍在运行时报告完成。这违反了[资源释放必须完全停稳的规则](../../../defensive-patterns.md#dispose-must-reach-quiescence-not-just-request-it),并可能让会话记录 `ABORTED` 后仍有工作修改状态。
|
||||
|
||||
**由注册表强制终止每个工具。** 不予采纳,因为同进程 JavaScript 没有安全、通用的抢占机制,而各能力的实际终止方式不同:进程组需要信号和升级处理,worker 需要终止,网络客户端则需要按协议中止。把这些机制移入 `ToolRegistry` 会让核心注册表耦合到每种实现。
|
||||
|
||||
**相信各工具和环绕包装层自行保留调用方取消。** 不予采纳,因为可变信号槽位与异步执行前、环绕调度等待共同构成一处共享调度边界。集中捕获并重复检查可以让所有已注册工具遵守相同的禁止延迟启动和禁止延迟成功规则,无需重复实现竞态处理。
|
||||
|
||||
**禁止环绕包装层替换 `exec.signal`。** 不予采纳,因为截止时间和嵌套运行时作用域需要为一次词法调度派生信号。在工具主体启动前重新融合调用方信号,可以同时保留组合能力与取消语义。
|
||||
|
||||
**跳过进入时信号已经中止的所有调用。** 不予采纳,因为直接调用方可能需要工具主体执行清理,或把取消转换为能力特定的结果。注册表会区分这种显式进入状态与调度期间由有效变为中止的信号,而 agent loop 会独立阻止轮次取消后产生新的模型驱动调度。
|
||||
|
||||
## 后果
|
||||
|
||||
- 每次注册表调用都遵循同一份服务层取消契约,包括插件或 MCP 桥接提供的工具;但只有协作式实现才能保证及时停止。
|
||||
- 调用方取消在执行前策略、环绕调度和后置策略的成功路径上保持单调:只要进入时有效的调用方信号在最终结果物化前发生中止,工具主体就不会延迟启动,普通成功也不会成为权威结果。
|
||||
- 若实现忽略信号,已启动的工作可以无限期推迟取消。注册表会刻意把这一缺陷暴露为无法完全停稳的调用,而不是用提前返回的结果掩盖它。
|
||||
- 能力特定失败与超时归属保持不变。通用 `ABORTED` 只替换成功结果,不替换信息更具体的错误结果。
|
||||
- 环绕包装层继续通过替换信号来完成组合,而原始调用方信号在调度时无法被切断。
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -552,7 +552,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
},
|
||||
{
|
||||
signature: 'async execute(exec: ToolExecutionInput): Promise<ToolExecutionResult>',
|
||||
jsDoc: '/**\n * Execute through pre-policy, guards, around-dispatch, post-policy, and final\n * notification. Tool and listener failures resolve as materialized error\n * results; an invisible tool reports `UNKNOWN_TOOL`. The returned outcome is\n * the same lossless, frozen snapshot final observers receive. Cancellation\n * arriving after entry skips a not-yet-started body or replaces a successful\n * dispatch outcome with `ABORTED`; already-started work is still drained and\n * may retain a tool-owned structured error.\n * @param exec - the typed same-process call input. The registry assigns its\n * correlation token before policy begins.\n * @returns the materialized final result.\n */',
|
||||
jsDoc: '/**\n * Execute through pre-policy, guards, around-dispatch, post-policy, and final\n * notification. Tool and listener failures resolve as materialized error\n * results; an invisible tool reports `UNKNOWN_TOOL`. The returned outcome is\n * the same lossless, frozen snapshot final observers receive. Cancellation\n * arriving after entry and before final result materialization skips a\n * not-yet-started body or replaces a successful pipeline outcome with\n * `ABORTED`; already-started work is still drained and may retain a\n * tool-owned structured error.\n * @param exec - the typed same-process call input. The registry assigns its\n * correlation token before policy begins.\n * @returns the materialized final result.\n */',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -841,7 +841,7 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
||||
name: 'tools/post-execute',
|
||||
mode: 'waterfall',
|
||||
signature: '\'tools/post-execute\'(this: Scoped<ToolRegistry>, exec: ToolExecution, result: Readonly<ToolExecutionResult>, next: () => Promise<PostToolDecision>): Promise<PostToolDecision>',
|
||||
jsDoc: '/**\n * Accept, replace, enrich, or block a normalized dispatch result. `next()`\n * accepts it unchanged; thrown tools still reach this seam as errors.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent\'s calls.\n * @param exec - the call that just ran (name, parsed arguments, caller agent).\n * @param result - the dispatch outcome a listener may accept, replace, or block.\n * @mode waterfall\n */',
|
||||
jsDoc: '/**\n * Accept, replace, enrich, or block a normalized dispatch result. `next()`\n * accepts it unchanged; thrown tools still reach this seam as errors. Async\n * listeners must observe `exec.signal`; after they settle, caller\n * cancellation replaces only a successful accepted outcome with `ABORTED`.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent\'s calls.\n * @param exec - the call that just ran (name, parsed arguments, caller agent).\n * @param result - the dispatch outcome a listener may accept, replace, or block.\n * @mode waterfall\n */',
|
||||
summary: 'Accept, replace, enrich, or block a normalized dispatch result.',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@ tools:
|
||||
|
||||
### Cancellation
|
||||
|
||||
Cancellation is cooperative and quiescent. A cancellation that arrives after registry entry is rechecked after pre-policy, approval, and around-dispatch waits, so a body cannot start late; if the body has started, the registry preserves the caller signal through wrapper replacement, awaits settlement, and replaces a successful dispatch outcome with structured `ABORTED`. A tool-owned structured error still wins. The registry never races away from a live same-process promise: every async tool must observe or forward `exec.signal` and settle only after owned work stops. A signal already aborted on entry still reaches the body for domain-specific cleanup; the agent-loop scheduler prevents model-driven calls from entering in that state. A timeout wrapper may replace the intermediate `ABORTED` with its owned `TOOL_TIMEOUT` when its deadline won. See the [quiescent-disposal rule](../../../docs/defensive-patterns.md#dispose-must-reach-quiescence-not-just-request-it) and [timeout ownership decision](../../../docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.md).
|
||||
Cancellation is cooperative and quiescent. A cancellation that arrives after registry entry is rechecked after pre-policy, approval, around-dispatch, and post-result policy waits, so a body cannot start late and cancellation that wins before final result materialization supersedes a successful pipeline outcome; if the body has started, the registry preserves the caller signal through wrapper replacement and awaits settlement. A tool-owned structured error still wins. The registry never races away from a live same-process promise: every async tool must observe or forward `exec.signal` and settle only after owned work stops. A signal already aborted on entry still reaches the body for domain-specific cleanup; the agent-loop scheduler prevents model-driven calls from entering in that state. A timeout wrapper may replace the intermediate `ABORTED` with its owned `TOOL_TIMEOUT` when its deadline won. The [tool-cancellation RFC](../../../docs/rfc/implemented/architecture/2026-07-19-cooperative-tool-cancellation.md) owns the service boundary and its hard-termination limit.
|
||||
|
||||
### Live events
|
||||
|
||||
|
||||
@@ -93,7 +93,9 @@ declare module 'cordis' {
|
||||
'tools/execute'(this: Scoped<ToolRegistry>, exec: ToolExecution, next: () => Promise<ToolExecutionResult>): Promise<ToolExecutionResult>
|
||||
/**
|
||||
* Accept, replace, enrich, or block a normalized dispatch result. `next()`
|
||||
* accepts it unchanged; thrown tools still reach this seam as errors.
|
||||
* accepts it unchanged; thrown tools still reach this seam as errors. Async
|
||||
* listeners must observe `exec.signal`; after they settle, caller
|
||||
* cancellation replaces only a successful accepted outcome with `ABORTED`.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls.
|
||||
* @param exec - the call that just ran (name, parsed arguments, caller agent).
|
||||
* @param result - the dispatch outcome a listener may accept, replace, or block.
|
||||
@@ -804,9 +806,10 @@ export class ToolRegistry extends Service {
|
||||
* notification. Tool and listener failures resolve as materialized error
|
||||
* results; an invisible tool reports `UNKNOWN_TOOL`. The returned outcome is
|
||||
* the same lossless, frozen snapshot final observers receive. Cancellation
|
||||
* arriving after entry skips a not-yet-started body or replaces a successful
|
||||
* dispatch outcome with `ABORTED`; already-started work is still drained and
|
||||
* may retain a tool-owned structured error.
|
||||
* arriving after entry and before final result materialization skips a
|
||||
* not-yet-started body or replaces a successful pipeline outcome with
|
||||
* `ABORTED`; already-started work is still drained and may retain a
|
||||
* tool-owned structured error.
|
||||
* @param exec - the typed same-process call input. The registry assigns its
|
||||
* correlation token before policy begins.
|
||||
* @returns the materialized final result.
|
||||
@@ -1015,7 +1018,13 @@ export class ToolRegistry extends Service {
|
||||
*/
|
||||
private async finalizeScheduledExecution(exec: ToolRunContext, result: ToolExecutionResult): Promise<ToolExecutionResult> {
|
||||
try {
|
||||
return this.finishScheduledExecution(exec, await this.postExecute(exec, result))
|
||||
const postResult = await this.postExecute(exec, result)
|
||||
return this.finishScheduledExecution(
|
||||
exec,
|
||||
this.callerCancelledAfterEntry(exec) && !postResult.isError
|
||||
? toolAbortedResult(postResult)
|
||||
: postResult,
|
||||
)
|
||||
} catch (error: unknown) {
|
||||
return this.finishScheduledExecution(exec, toolErrorResult(error))
|
||||
}
|
||||
|
||||
@@ -667,6 +667,52 @@ describe('ToolRegistry', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('replaces a late post-execute success with ABORTED and preserves contexts', async () => {
|
||||
const ctx = await setup()
|
||||
ctx.tools.register({
|
||||
...echoTool,
|
||||
name: 'completed-before-post',
|
||||
async execute(_args, exec) {
|
||||
exec.deferContext({
|
||||
content: [{ type: 'text', text: 'completed child work' }],
|
||||
source: { kind: 'plugin', plugin: 'child' },
|
||||
})
|
||||
return [{ type: 'text', text: 'body complete' }]
|
||||
},
|
||||
})
|
||||
const entered = Promise.withResolvers<undefined>()
|
||||
const release = Promise.withResolvers<undefined>()
|
||||
ctx.on('tools/post-execute', async (_exec, _result, next) => {
|
||||
const decision = await next()
|
||||
entered.resolve(undefined)
|
||||
await release.promise
|
||||
return {
|
||||
...decision,
|
||||
additionalContexts: [{
|
||||
content: [{ type: 'text', text: 'post context' }],
|
||||
source: { kind: 'plugin', plugin: 'post' },
|
||||
}],
|
||||
}
|
||||
})
|
||||
const controller = new AbortController()
|
||||
const pending = ctx.tools.execute({
|
||||
callId: CallId('cancelled-in-post'), name: 'completed-before-post', arguments: {}, signal: controller.signal,
|
||||
})
|
||||
await entered.promise
|
||||
controller.abort('cancelled while post policy waits')
|
||||
release.resolve(undefined)
|
||||
|
||||
await expect(pending).resolves.toMatchObject({
|
||||
content: [{ type: 'text', text: 'Error: tool call aborted' }],
|
||||
isError: true,
|
||||
error: { name: 'AbortError', code: 'ABORTED' },
|
||||
additionalContexts: [
|
||||
{ source: { kind: 'plugin', plugin: 'child' } },
|
||||
{ source: { kind: 'plugin', plugin: 'post' } },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('fuses caller cancellation back into a wrapper replacement for the running body', async () => {
|
||||
const ctx = await setup()
|
||||
const entered = Promise.withResolvers<undefined>()
|
||||
|
||||
@@ -794,7 +794,7 @@ Emitted when any prompt provider changes. This registry notification is unfilter
|
||||
|
||||
A tool was registered or unregistered, or a scoped restriction changed (the available tool set changed — possibly for one scope only). An UNFILTERED registry-subject notification, deliberately not scope-filtered dispatch: a global change concerns every agent's next assembly, so a scoped listener subscribing here sees every change, not just its own scope's.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L120)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L122)
|
||||
|
||||
### tools/execute
|
||||
|
||||
@@ -827,7 +827,9 @@ Around-dispatch waterfall for timeout, retry, or metrics. `next()` returns a nor
|
||||
```ts website-api
|
||||
/**
|
||||
* Accept, replace, enrich, or block a normalized dispatch result. `next()`
|
||||
* accepts it unchanged; thrown tools still reach this seam as errors.
|
||||
* accepts it unchanged; thrown tools still reach this seam as errors. Async
|
||||
* listeners must observe `exec.signal`; after they settle, caller
|
||||
* cancellation replaces only a successful accepted outcome with `ABORTED`.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls.
|
||||
* @param exec - the call that just ran (name, parsed arguments, caller agent).
|
||||
* @param result - the dispatch outcome a listener may accept, replace, or block.
|
||||
@@ -836,12 +838,12 @@ Around-dispatch waterfall for timeout, retry, or metrics. `next()` returns a nor
|
||||
'tools/post-execute'(this: Scoped<ToolRegistry>, exec: ToolExecution, result: Readonly<ToolExecutionResult>, next: () => Promise<PostToolDecision>): Promise<PostToolDecision>
|
||||
```
|
||||
|
||||
Accept, replace, enrich, or block a normalized dispatch result. `next()` accepts it unchanged; thrown tools still reach this seam as errors. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls.
|
||||
Accept, replace, enrich, or block a normalized dispatch result. `next()` accepts it unchanged; thrown tools still reach this seam as errors. Async listeners must observe `exec.signal`; after they settle, caller cancellation replaces only a successful accepted outcome with `ABORTED`. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls.
|
||||
|
||||
- `exec` — the call that just ran (name, parsed arguments, caller agent).
|
||||
- `result` — the dispatch outcome a listener may accept, replace, or block.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L102)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L104)
|
||||
|
||||
### tools/pre-execute
|
||||
|
||||
@@ -886,7 +888,7 @@ Observe the frozen, lossless-JSON final outcome. Listener failures are contained
|
||||
- `exec` — the execution object that traversed the pipeline.
|
||||
- `result` — a deep-frozen snapshot of the final returned result.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L110)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L112)
|
||||
|
||||
## workflow/*
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
Tool registry and execution pipeline. Scoped registrations shadow globals; one visibility resolver feeds presentation, lookup, and dispatch.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L465)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L467)
|
||||
|
||||
### ctx.tools.register(definition)
|
||||
|
||||
@@ -26,7 +26,7 @@ Register globally or in the calling agent scope. Scoped tools shadow globals; du
|
||||
|
||||
**Returns** the exact disposer that unregisters the tool.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L567)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L569)
|
||||
|
||||
### ctx.tools.restrict(filter)
|
||||
|
||||
@@ -47,7 +47,7 @@ Restrict global tools for the calling agent scope. Empty filters, unknown names,
|
||||
|
||||
**Returns** the exact disposer that lifts this restriction.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L607)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L609)
|
||||
|
||||
### ctx.tools.guard(guard)
|
||||
|
||||
@@ -71,7 +71,7 @@ Register a monotonic guard after the extensible `tools/pre-execute` waterfall. A
|
||||
|
||||
**Returns** the exact disposer that unregisters the guard.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L658)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L660)
|
||||
|
||||
### ctx.tools.get(name, scope?)
|
||||
|
||||
@@ -95,7 +95,7 @@ Look up a tool as one scope sees it (scoped shadows global; a restricted-away gl
|
||||
|
||||
**Returns** the definition the scope resolves, or undefined when none is visible.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L760)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L762)
|
||||
|
||||
### ctx.tools.schemas(scope?)
|
||||
|
||||
@@ -115,7 +115,7 @@ Project visible definitions onto the allowlisted model-facing schema fields, exc
|
||||
|
||||
**Returns** one deep-cloned schema per visible tool.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L770)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L772)
|
||||
|
||||
### ctx.tools.executionMode(exec)
|
||||
|
||||
@@ -136,7 +136,7 @@ Classify a pending call through the caller's visible tool definition. Only an ex
|
||||
|
||||
**Returns** the fail-closed scheduling mode.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L791)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L793)
|
||||
|
||||
### ctx.tools.execute(exec)
|
||||
|
||||
@@ -146,9 +146,10 @@ Classify a pending call through the caller's visible tool definition. Only an ex
|
||||
* notification. Tool and listener failures resolve as materialized error
|
||||
* results; an invisible tool reports `UNKNOWN_TOOL`. The returned outcome is
|
||||
* the same lossless, frozen snapshot final observers receive. Cancellation
|
||||
* arriving after entry skips a not-yet-started body or replaces a successful
|
||||
* dispatch outcome with `ABORTED`; already-started work is still drained and
|
||||
* may retain a tool-owned structured error.
|
||||
* arriving after entry and before final result materialization skips a
|
||||
* not-yet-started body or replaces a successful pipeline outcome with
|
||||
* `ABORTED`; already-started work is still drained and may retain a
|
||||
* tool-owned structured error.
|
||||
* @param exec - the typed same-process call input. The registry assigns its
|
||||
* correlation token before policy begins.
|
||||
* @returns the materialized final result.
|
||||
@@ -156,10 +157,10 @@ Classify a pending call through the caller's visible tool definition. Only an ex
|
||||
async execute(exec: ToolExecutionInput): Promise<ToolExecutionResult>
|
||||
```
|
||||
|
||||
Execute through pre-policy, guards, around-dispatch, post-policy, and final notification. Tool and listener failures resolve as materialized error results; an invisible tool reports `UNKNOWN_TOOL`. The returned outcome is the same lossless, frozen snapshot final observers receive. Cancellation arriving after entry skips a not-yet-started body or replaces a successful dispatch outcome with `ABORTED`; already-started work is still drained and may retain a tool-owned structured error.
|
||||
Execute through pre-policy, guards, around-dispatch, post-policy, and final notification. Tool and listener failures resolve as materialized error results; an invisible tool reports `UNKNOWN_TOOL`. The returned outcome is the same lossless, frozen snapshot final observers receive. Cancellation arriving after entry and before final result materialization skips a not-yet-started body or replaces a successful pipeline outcome with `ABORTED`; already-started work is still drained and may retain a tool-owned structured error.
|
||||
|
||||
- `exec` — the typed same-process call input. The registry assigns its correlation token before policy begins.
|
||||
|
||||
**Returns** the materialized final result.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L814)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L817)
|
||||
|
||||
Reference in New Issue
Block a user