fix(tools): enforce cooperative cancellation

This commit is contained in:
Tianyi Cui
2026-07-19 18:05:54 +08:00
parent b6858a5ce1
commit 2bc4e05a08
16 files changed
+583 -85

No files matched your search

+14 -3
View File
@@ -11,6 +11,15 @@ A `ToolSchema` (the model-facing fields) plus the `execute` function, host-only
```ts type-equiv
/** A registered tool: its schema plus the execution function. */
interface ToolDefinition extends ToolSchema {
/**
* Run one accepted call. Async work must observe or forward `exec.signal` and
* settle only after its owned work reaches quiescence. The registry preserves
* caller cancellation through around-dispatch signal replacement and does
* not abandon this promise, but it cannot hard-kill same-process code.
* @param args - losslessly snapshotted, frozen model arguments.
* @param exec - execution identity, cancellation signal, and context deferral.
* @returns model-facing content plus optional private presentation metadata.
*/
execute(args: unknown, exec: ToolRunContext): Promise<ToolExecuteReturn>
/**
* Cooperative tool-call timeout budget in milliseconds. Omit for no deadline.
@@ -204,8 +213,10 @@ type ToolExecutionMode =
* One pending tool call inside the registry pipeline. Parsed arguments cross
* one lossless-JSON materialization boundary before policy and are deep-frozen;
* call identity and the registry-assigned {@link token} are readonly. An
* around-dispatch wrapper may set, replace, or remove `signal`. The registry
* freezes the complete object before `tools/result` observers run.
* around-dispatch wrapper may set, replace, or remove `signal`; immediately
* before the body, the registry re-fuses the original caller signal so a
* wrapper cannot detach caller cancellation. The registry freezes the complete
* object before `tools/result` observers run.
*/
interface ToolExecution extends ToolExecutionInput {
/** Registry-assigned identity shared with nested calls only as their opaque `parent` token. */
@@ -213,7 +224,7 @@ interface ToolExecution extends ToolExecutionInput {
}
```
`ToolExecutionToken` is an opaque runtime `Symbol` used only for identity comparison. Before policy, `execute()` materializes and freezes arguments, rejects non-JSON input, and assigns the token. Identity fields and the optional parent token remain readonly; only `signal` may change around dispatch. Final observers receive the frozen execution identity.
`ToolExecutionToken` is an opaque runtime `Symbol` used only for identity comparison. Before policy, `execute()` materializes and freezes arguments, rejects non-JSON input, and assigns the token. Identity fields and the optional parent token remain readonly; only `signal` may change around dispatch, and the registry re-fuses the caller signal before invoking the body. Final observers receive the frozen execution identity.
A `ToolGuard` is scope-aware final pre-dispatch policy. Its shape deliberately has no allow result: `undefined` preserves the waterfall decision, while a returned reason can only reduce permission, so a later listener cannot undo it.