@namzu/sdk 31.1.0 → 33.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +188 -0
- package/dist/agents/ReactiveAgent.d.ts.map +1 -1
- package/dist/agents/ReactiveAgent.js +4 -0
- package/dist/agents/ReactiveAgent.js.map +1 -1
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js +4 -0
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/agents/runAgent.d.ts +4 -0
- package/dist/agents/runAgent.d.ts.map +1 -1
- package/dist/agents/runAgent.js +4 -0
- package/dist/agents/runAgent.js.map +1 -1
- package/dist/authorization/gate.d.ts +1 -1
- package/dist/authorization/gate.js +1 -1
- package/dist/connector/index.d.ts +1 -1
- package/dist/connector/index.d.ts.map +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/connector/index.js.map +1 -1
- package/dist/connector/mcp/adapter.d.ts.map +1 -1
- package/dist/connector/mcp/adapter.js +71 -16
- package/dist/connector/mcp/adapter.js.map +1 -1
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +3 -2
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/connector/mcp/http-redirect.d.ts +17 -0
- package/dist/connector/mcp/http-redirect.d.ts.map +1 -0
- package/dist/connector/mcp/http-redirect.js +31 -0
- package/dist/connector/mcp/http-redirect.js.map +1 -0
- package/dist/connector/mcp/http-sse.d.ts.map +1 -1
- package/dist/connector/mcp/http-sse.js +5 -0
- package/dist/connector/mcp/http-sse.js.map +1 -1
- package/dist/connector/mcp/image-admission.d.ts +16 -0
- package/dist/connector/mcp/image-admission.d.ts.map +1 -0
- package/dist/connector/mcp/image-admission.js +288 -0
- package/dist/connector/mcp/image-admission.js.map +1 -0
- package/dist/connector/mcp/stdio.d.ts +13 -0
- package/dist/connector/mcp/stdio.d.ts.map +1 -1
- package/dist/connector/mcp/stdio.js +66 -23
- package/dist/connector/mcp/stdio.js.map +1 -1
- package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
- package/dist/connector/mcp/streamable-http.js +3 -0
- package/dist/connector/mcp/streamable-http.js.map +1 -1
- package/dist/constants/tools/index.js +1 -1
- package/dist/constants/tools/index.js.map +1 -1
- package/dist/directory/derive-supervisor.d.ts.map +1 -1
- package/dist/directory/derive-supervisor.js +5 -0
- package/dist/directory/derive-supervisor.js.map +1 -1
- package/dist/directory/derive.d.ts.map +1 -1
- package/dist/directory/derive.js +5 -0
- package/dist/directory/derive.js.map +1 -1
- package/dist/directory/load.d.ts.map +1 -1
- package/dist/directory/load.js +1 -0
- package/dist/directory/load.js.map +1 -1
- package/dist/directory/types.d.ts +2 -0
- package/dist/directory/types.d.ts.map +1 -1
- package/dist/execution/base.d.ts +13 -0
- package/dist/execution/base.d.ts.map +1 -1
- package/dist/execution/base.js +103 -8
- package/dist/execution/base.js.map +1 -1
- package/dist/execution/code-runtime/types.d.ts +11 -1
- package/dist/execution/code-runtime/types.d.ts.map +1 -1
- package/dist/execution/code-runtime/types.js.map +1 -1
- package/dist/execution/code-runtime/worker.d.ts.map +1 -1
- package/dist/execution/code-runtime/worker.js +67 -9
- package/dist/execution/code-runtime/worker.js.map +1 -1
- package/dist/execution/errors.d.ts +12 -0
- package/dist/execution/errors.d.ts.map +1 -0
- package/dist/execution/errors.js +21 -0
- package/dist/execution/errors.js.map +1 -0
- package/dist/execution/factory.d.ts.map +1 -1
- package/dist/execution/factory.js +1 -0
- package/dist/execution/factory.js.map +1 -1
- package/dist/execution/hybrid.d.ts +5 -0
- package/dist/execution/hybrid.d.ts.map +1 -1
- package/dist/execution/hybrid.js +36 -16
- package/dist/execution/hybrid.js.map +1 -1
- package/dist/execution/index.d.ts +1 -0
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/index.js +1 -0
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/local.d.ts +13 -0
- package/dist/execution/local.d.ts.map +1 -1
- package/dist/execution/local.js +145 -19
- package/dist/execution/local.js.map +1 -1
- package/dist/execution/output.d.ts +27 -0
- package/dist/execution/output.d.ts.map +1 -0
- package/dist/execution/output.js +105 -0
- package/dist/execution/output.js.map +1 -0
- package/dist/execution/remote.d.ts +26 -2
- package/dist/execution/remote.d.ts.map +1 -1
- package/dist/execution/remote.js +86 -15
- package/dist/execution/remote.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +1 -0
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +9 -3
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/process/kill-tree.d.ts +11 -13
- package/dist/process/kill-tree.d.ts.map +1 -1
- package/dist/process/kill-tree.js +11 -13
- package/dist/process/kill-tree.js.map +1 -1
- package/dist/provider/errors.d.ts +1 -0
- package/dist/provider/errors.d.ts.map +1 -1
- package/dist/provider/errors.js +39 -1
- package/dist/provider/errors.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +31 -0
- package/dist/provider/fallback.js.map +1 -1
- package/dist/provider/idle-timeout.d.ts.map +1 -1
- package/dist/provider/idle-timeout.js +5 -0
- package/dist/provider/idle-timeout.js.map +1 -1
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +5 -0
- package/dist/provider/retry.js.map +1 -1
- package/dist/public-runtime.d.ts +7 -5
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +9 -4
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +2 -1
- package/dist/public-types.d.ts.map +1 -1
- package/dist/registry/tool/execute.d.ts +7 -1
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +201 -54
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/run/command-gate.d.ts.map +1 -1
- package/dist/run/command-gate.js +11 -0
- package/dist/run/command-gate.js.map +1 -1
- package/dist/run/reporter.d.ts.map +1 -1
- package/dist/run/reporter.js +6 -1
- package/dist/run/reporter.js.map +1 -1
- package/dist/run/workspace-fingerprint.d.ts +4 -4
- package/dist/run/workspace-fingerprint.d.ts.map +1 -1
- package/dist/run/workspace-fingerprint.js +6 -4
- package/dist/run/workspace-fingerprint.js.map +1 -1
- package/dist/runtime/bidi/index.d.ts +1 -1
- package/dist/runtime/bidi/index.d.ts.map +1 -1
- package/dist/runtime/bidi/index.js +1 -1
- package/dist/runtime/bidi/index.js.map +1 -1
- package/dist/runtime/bidi/session.d.ts +19 -0
- package/dist/runtime/bidi/session.d.ts.map +1 -1
- package/dist/runtime/bidi/session.js +150 -26
- package/dist/runtime/bidi/session.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +43 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +824 -174
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +5 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +47 -4
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +1 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +34 -14
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/advisory.js +2 -2
- package/dist/runtime/query/iteration/phases/advisory.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +84 -22
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/iteration/provider-rejected-image.d.ts +13 -0
- package/dist/runtime/query/iteration/provider-rejected-image.d.ts.map +1 -0
- package/dist/runtime/query/iteration/provider-rejected-image.js +91 -0
- package/dist/runtime/query/iteration/provider-rejected-image.js.map +1 -0
- package/dist/runtime/query/iteration/stream-turn.d.ts +4 -1
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +7 -3
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/request-rich-content.d.ts +13 -0
- package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
- package/dist/runtime/query/request-rich-content.js +173 -17
- package/dist/runtime/query/request-rich-content.js.map +1 -1
- package/dist/runtime/query/result.d.ts.map +1 -1
- package/dist/runtime/query/result.js +1 -0
- package/dist/runtime/query/result.js.map +1 -1
- package/dist/runtime/query/resume-pending.d.ts +5 -1
- package/dist/runtime/query/resume-pending.d.ts.map +1 -1
- package/dist/runtime/query/resume-pending.js +66 -1
- package/dist/runtime/query/resume-pending.js.map +1 -1
- package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
- package/dist/runtime/query/tool-output-budget.js +45 -16
- package/dist/runtime/query/tool-output-budget.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +6 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +4 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +193 -41
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/scheduler/delegating.d.ts +2 -1
- package/dist/scheduler/delegating.d.ts.map +1 -1
- package/dist/scheduler/delegating.js +6 -5
- package/dist/scheduler/delegating.js.map +1 -1
- package/dist/scheduler/local.d.ts +2 -1
- package/dist/scheduler/local.d.ts.map +1 -1
- package/dist/scheduler/local.js +2 -2
- package/dist/scheduler/local.js.map +1 -1
- package/dist/session/workspace/driver.d.ts +12 -4
- package/dist/session/workspace/driver.d.ts.map +1 -1
- package/dist/session/workspace/git-worktree.d.ts +17 -3
- package/dist/session/workspace/git-worktree.d.ts.map +1 -1
- package/dist/session/workspace/git-worktree.js +153 -34
- package/dist/session/workspace/git-worktree.js.map +1 -1
- package/dist/skills/registry.d.ts +15 -0
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +33 -0
- package/dist/skills/registry.js.map +1 -1
- package/dist/store/attachment/index.d.ts +24 -2
- package/dist/store/attachment/index.d.ts.map +1 -1
- package/dist/store/attachment/index.js +52 -5
- package/dist/store/attachment/index.js.map +1 -1
- package/dist/tools/builtins/computer-use.d.ts.map +1 -1
- package/dist/tools/builtins/computer-use.js +39 -2
- package/dist/tools/builtins/computer-use.js.map +1 -1
- package/dist/tools/builtins/lsp.d.ts +1 -1
- package/dist/tools/builtins/run-code.d.ts.map +1 -1
- package/dist/tools/builtins/run-code.js +24 -16
- package/dist/tools/builtins/run-code.js.map +1 -1
- package/dist/tools/builtins/skill.d.ts +2 -1
- package/dist/tools/builtins/skill.d.ts.map +1 -1
- package/dist/tools/builtins/skill.js +250 -6
- package/dist/tools/builtins/skill.js.map +1 -1
- package/dist/tools/coordinator/agent.d.ts.map +1 -1
- package/dist/tools/coordinator/agent.js +100 -30
- package/dist/tools/coordinator/agent.js.map +1 -1
- package/dist/types/agent/base.d.ts +4 -0
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/agent/factory.d.ts +1 -0
- package/dist/types/agent/factory.d.ts.map +1 -1
- package/dist/types/agent/manager.d.ts +1 -1
- package/dist/types/agent/manager.d.ts.map +1 -1
- package/dist/types/agent/scheduler.d.ts +10 -1
- package/dist/types/agent/scheduler.d.ts.map +1 -1
- package/dist/types/authorization/index.d.ts +12 -12
- package/dist/types/bidi/index.d.ts +19 -2
- package/dist/types/bidi/index.d.ts.map +1 -1
- package/dist/types/computer-use/index.d.ts +18 -0
- package/dist/types/computer-use/index.d.ts.map +1 -1
- package/dist/types/connector/execution-context.d.ts +3 -1
- package/dist/types/connector/execution-context.d.ts.map +1 -1
- package/dist/types/execution/index.d.ts +43 -1
- package/dist/types/execution/index.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +11 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/message/index.d.ts +31 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +43 -0
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/plugin/index.d.ts +14 -1
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +1 -0
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/provider/error.d.ts +4 -1
- package/dist/types/provider/error.d.ts.map +1 -1
- package/dist/types/provider/interface.d.ts +14 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/run/events.d.ts +10 -2
- package/dist/types/run/events.d.ts.map +1 -1
- package/dist/types/run/events.js.map +1 -1
- package/dist/types/sandbox/index.d.ts +15 -10
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/dist/types/tool/index.d.ts +129 -12
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/workspace/ref.d.ts +5 -0
- package/dist/types/workspace/ref.d.ts.map +1 -1
- package/dist/utils/abort.d.ts +8 -0
- package/dist/utils/abort.d.ts.map +1 -1
- package/dist/utils/abort.js +16 -0
- package/dist/utils/abort.js.map +1 -1
- package/package.json +4 -4
- package/src/agents/ReactiveAgent.ts +4 -0
- package/src/agents/SupervisorAgent.ts +4 -0
- package/src/agents/runAgent.ts +8 -0
- package/src/authorization/gate.ts +1 -1
- package/src/connector/index.ts +2 -0
- package/src/connector/mcp/adapter.ts +82 -21
- package/src/connector/mcp/client.ts +2 -1
- package/src/connector/mcp/http-redirect.ts +32 -0
- package/src/connector/mcp/http-sse.ts +5 -0
- package/src/connector/mcp/image-admission.ts +272 -0
- package/src/connector/mcp/stdio.ts +63 -21
- package/src/connector/mcp/streamable-http.ts +3 -0
- package/src/constants/tools/index.ts +1 -1
- package/src/directory/derive-supervisor.ts +5 -0
- package/src/directory/derive.ts +5 -0
- package/src/directory/load.ts +1 -0
- package/src/directory/types.ts +2 -0
- package/src/execution/base.ts +124 -7
- package/src/execution/code-runtime/types.ts +15 -1
- package/src/execution/code-runtime/worker.ts +72 -9
- package/src/execution/errors.ts +24 -0
- package/src/execution/factory.ts +1 -0
- package/src/execution/hybrid.ts +47 -16
- package/src/execution/index.ts +1 -0
- package/src/execution/local.ts +164 -19
- package/src/execution/output.ts +104 -0
- package/src/execution/remote.ts +122 -17
- package/src/manager/agent/lifecycle.ts +1 -0
- package/src/plugin/lifecycle.ts +9 -3
- package/src/process/kill-tree.ts +11 -13
- package/src/provider/errors.ts +40 -1
- package/src/provider/fallback.ts +39 -0
- package/src/provider/idle-timeout.ts +8 -0
- package/src/provider/retry.ts +8 -0
- package/src/public-runtime.ts +23 -2
- package/src/public-types.ts +11 -0
- package/src/registry/tool/execute.ts +230 -64
- package/src/run/command-gate.ts +12 -0
- package/src/run/reporter.ts +7 -1
- package/src/run/workspace-fingerprint.ts +5 -4
- package/src/runtime/bidi/index.ts +1 -1
- package/src/runtime/bidi/session.ts +177 -28
- package/src/runtime/query/executor.ts +987 -172
- package/src/runtime/query/index.ts +59 -6
- package/src/runtime/query/iteration/index.ts +69 -29
- package/src/runtime/query/iteration/phases/advisory.ts +2 -2
- package/src/runtime/query/iteration/phases/tool-review.ts +87 -23
- package/src/runtime/query/iteration/provider-rejected-image.ts +106 -0
- package/src/runtime/query/iteration/stream-turn.ts +12 -2
- package/src/runtime/query/request-rich-content.ts +213 -15
- package/src/runtime/query/result.ts +1 -0
- package/src/runtime/query/resume-pending.ts +83 -1
- package/src/runtime/query/tool-output-budget.ts +52 -17
- package/src/runtime/query/tooling.ts +10 -0
- package/src/sandbox/provider/local.ts +213 -45
- package/src/scheduler/delegating.ts +6 -5
- package/src/scheduler/local.ts +3 -2
- package/src/session/workspace/driver.ts +12 -4
- package/src/session/workspace/git-worktree.ts +173 -34
- package/src/skills/registry.ts +47 -0
- package/src/store/attachment/index.ts +100 -15
- package/src/tools/builtins/computer-use.ts +45 -2
- package/src/tools/builtins/run-code.ts +24 -16
- package/src/tools/builtins/skill.ts +325 -7
- package/src/tools/coordinator/agent.ts +108 -33
- package/src/types/agent/base.ts +4 -0
- package/src/types/agent/factory.ts +1 -0
- package/src/types/agent/manager.ts +1 -1
- package/src/types/agent/scheduler.ts +10 -1
- package/src/types/bidi/index.ts +19 -2
- package/src/types/computer-use/index.ts +19 -0
- package/src/types/connector/execution-context.ts +3 -0
- package/src/types/execution/index.ts +46 -1
- package/src/types/hitl/index.ts +11 -0
- package/src/types/message/index.ts +78 -1
- package/src/types/plugin/index.ts +15 -0
- package/src/types/provider/error.ts +4 -1
- package/src/types/provider/interface.ts +18 -0
- package/src/types/run/events.ts +18 -4
- package/src/types/sandbox/index.ts +15 -10
- package/src/types/tool/index.ts +159 -15
- package/src/types/workspace/ref.ts +5 -0
- package/src/utils/abort.ts +18 -0
|
@@ -14,6 +14,15 @@ import { generateToolCallId } from '../../utils/id.js';
|
|
|
14
14
|
import { compressShellOutput } from '../../utils/shell-compress.js';
|
|
15
15
|
import { bindOwner } from '../jobs/registry.js';
|
|
16
16
|
import { DEFAULT_MAX_TOOL_OUTPUT_CHARS, applyToolOutputBudget, describeDroppedContent, measureContentBytes, } from './tool-output-budget.js';
|
|
17
|
+
function assertUniqueToolCallIds(toolCalls) {
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
for (const [index, toolCall] of toolCalls.entries()) {
|
|
20
|
+
if (seen.has(toolCall.id)) {
|
|
21
|
+
throw new Error(`Provider returned duplicate tool call id "${toolCall.id}" at batch index ${index}; the batch is refused because review, denial and result ownership require one unique id per call.`);
|
|
22
|
+
}
|
|
23
|
+
seen.add(toolCall.id);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
17
26
|
/**
|
|
18
27
|
* Default per-tool deadline. Long enough for a real build or test run,
|
|
19
28
|
* short enough that a wedged tool does not hold a turn open indefinitely.
|
|
@@ -29,6 +38,97 @@ export const DEFAULT_TOOL_TIMEOUT_MS = 120_000;
|
|
|
29
38
|
* already one-at-a-time by construction.
|
|
30
39
|
*/
|
|
31
40
|
export const DEFAULT_TOOL_CONCURRENCY = 8;
|
|
41
|
+
/** Maximum UTF-8 size of one ephemeral tool-progress update. */
|
|
42
|
+
const MAX_TOOL_PROGRESS_BYTES = 8 * 1024;
|
|
43
|
+
/** A visible marker: this event is a display projection, not durable output. */
|
|
44
|
+
const TOOL_PROGRESS_OMISSION = '… ';
|
|
45
|
+
function boundedToolProgress(message) {
|
|
46
|
+
if (Buffer.byteLength(message, 'utf8') <= MAX_TOOL_PROGRESS_BYTES)
|
|
47
|
+
return message;
|
|
48
|
+
const tailBudget = MAX_TOOL_PROGRESS_BYTES - Buffer.byteLength(TOOL_PROGRESS_OMISSION, 'utf8');
|
|
49
|
+
let low = 0;
|
|
50
|
+
let high = message.length;
|
|
51
|
+
while (low < high) {
|
|
52
|
+
const middle = Math.floor((low + high) / 2);
|
|
53
|
+
if (Buffer.byteLength(message.slice(middle), 'utf8') > tailBudget)
|
|
54
|
+
low = middle + 1;
|
|
55
|
+
else
|
|
56
|
+
high = middle;
|
|
57
|
+
}
|
|
58
|
+
// Do not turn the low half of a retained surrogate pair into U+FFFD.
|
|
59
|
+
if (low > 0 &&
|
|
60
|
+
low < message.length &&
|
|
61
|
+
message.charCodeAt(low) >= 0xdc00 &&
|
|
62
|
+
message.charCodeAt(low) <= 0xdfff &&
|
|
63
|
+
message.charCodeAt(low - 1) >= 0xd800 &&
|
|
64
|
+
message.charCodeAt(low - 1) <= 0xdbff) {
|
|
65
|
+
low += 1;
|
|
66
|
+
}
|
|
67
|
+
return `${TOOL_PROGRESS_OMISSION}${message.slice(low)}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Latest-state publisher for one tool call.
|
|
71
|
+
*
|
|
72
|
+
* `ToolContext.report()` is synchronous by contract, while host event
|
|
73
|
+
* listeners may be arbitrarily slow. Starting one promise per report makes a
|
|
74
|
+
* chatty tool an unbounded allocation source. Progress is state rather than a
|
|
75
|
+
* transcript, so one in-flight update plus the latest pending update is the
|
|
76
|
+
* complete useful working set; intermediate states may be replaced.
|
|
77
|
+
*/
|
|
78
|
+
class ToolProgressPublisher {
|
|
79
|
+
emitEvent;
|
|
80
|
+
base;
|
|
81
|
+
pending;
|
|
82
|
+
draining = null;
|
|
83
|
+
accepting = true;
|
|
84
|
+
constructor(emitEvent, base) {
|
|
85
|
+
this.emitEvent = emitEvent;
|
|
86
|
+
this.base = base;
|
|
87
|
+
}
|
|
88
|
+
report(message, fraction) {
|
|
89
|
+
if (!this.accepting)
|
|
90
|
+
return;
|
|
91
|
+
this.pending = {
|
|
92
|
+
message,
|
|
93
|
+
...(fraction !== undefined ? { fraction: Math.min(1, Math.max(0, fraction)) } : {}),
|
|
94
|
+
};
|
|
95
|
+
this.startDrain();
|
|
96
|
+
}
|
|
97
|
+
async close() {
|
|
98
|
+
this.accepting = false;
|
|
99
|
+
this.startDrain();
|
|
100
|
+
while (this.draining)
|
|
101
|
+
await this.draining;
|
|
102
|
+
}
|
|
103
|
+
startDrain() {
|
|
104
|
+
if (this.draining || !this.pending)
|
|
105
|
+
return;
|
|
106
|
+
const current = this.drain();
|
|
107
|
+
this.draining = current;
|
|
108
|
+
void current.finally(() => {
|
|
109
|
+
if (this.draining !== current)
|
|
110
|
+
return;
|
|
111
|
+
this.draining = null;
|
|
112
|
+
// A report can land after the final loop check but before this
|
|
113
|
+
// settlement callback. It is still an accepted update and must be
|
|
114
|
+
// drained even when close() has since stopped new reports.
|
|
115
|
+
this.startDrain();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async drain() {
|
|
119
|
+
while (this.pending) {
|
|
120
|
+
const update = this.pending;
|
|
121
|
+
this.pending = undefined;
|
|
122
|
+
// A progress observer is diagnostic. Its failure cannot become a tool
|
|
123
|
+
// failure, and report() never hands a rejection back to the tool.
|
|
124
|
+
await this.emitEvent({
|
|
125
|
+
...this.base,
|
|
126
|
+
message: boundedToolProgress(update.message),
|
|
127
|
+
...(update.fraction !== undefined ? { fraction: update.fraction } : {}),
|
|
128
|
+
}).catch(() => { });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
32
132
|
/**
|
|
33
133
|
* Re-runs granted to a `post_tool_use` hook that returns `{action:'retry'}`
|
|
34
134
|
* on a tool which did not opt into {@link ToolDefinition.maxRetries}.
|
|
@@ -92,6 +192,7 @@ export class ToolExecutor {
|
|
|
92
192
|
workingStateManager;
|
|
93
193
|
probes;
|
|
94
194
|
parentSpan;
|
|
195
|
+
preparedBatches = new WeakSet();
|
|
95
196
|
/** Set per turn by the orchestrator; see {@link setStepAllowedTools}. */
|
|
96
197
|
stepAllowedTools;
|
|
97
198
|
readPaths = new Set();
|
|
@@ -228,17 +329,72 @@ export class ToolExecutor {
|
|
|
228
329
|
const configured = this.config.permissionMode;
|
|
229
330
|
return typeof configured === 'function' ? configured() : configured;
|
|
230
331
|
}
|
|
231
|
-
|
|
332
|
+
/** Evaluate the run's operator policy against one already-prepared value. */
|
|
333
|
+
evaluatePreparedAuthorization(toolName, input) {
|
|
334
|
+
return this.config.authorizationGate?.evaluate({
|
|
335
|
+
toolName,
|
|
336
|
+
toolInput: input,
|
|
337
|
+
toolDef: this.config.tools.get(toolName),
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Resolve repairs and pre-tool hooks, then decode each call exactly once.
|
|
342
|
+
* The returned projection is what policy and a human review; execution later
|
|
343
|
+
* consumes the registry-owned preparations rather than parsing again.
|
|
344
|
+
*/
|
|
345
|
+
async prepareBatchForReview(response) {
|
|
346
|
+
assertUniqueToolCallIds(response.message.toolCalls ?? []);
|
|
347
|
+
const calls = new Map();
|
|
348
|
+
for (const toolCall of response.message.toolCalls ?? []) {
|
|
349
|
+
calls.set(toolCall.id, await this.prepareDirectCall(toolCall));
|
|
350
|
+
}
|
|
351
|
+
return this.publishPreparedBatch(calls);
|
|
352
|
+
}
|
|
353
|
+
/** Re-prepare only calls whose raw input a reviewer actually changed. */
|
|
354
|
+
async reprepareBatchForReview(response, previous, changedCallIds) {
|
|
355
|
+
assertUniqueToolCallIds(response.message.toolCalls ?? []);
|
|
356
|
+
if (!this.preparedBatches.has(previous)) {
|
|
357
|
+
throw new Error('Prepared tool batch is not owned by this executor.');
|
|
358
|
+
}
|
|
359
|
+
const calls = new Map(previous.calls);
|
|
360
|
+
for (const toolCall of response.message.toolCalls ?? []) {
|
|
361
|
+
if (changedCallIds.has(toolCall.id)) {
|
|
362
|
+
calls.set(toolCall.id, await this.prepareDirectCall(toolCall));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return this.publishPreparedBatch(calls);
|
|
366
|
+
}
|
|
367
|
+
publishPreparedBatch(calls) {
|
|
368
|
+
const reviewCalls = [...calls.values()]
|
|
369
|
+
.filter((call) => call.kind !== 'synthetic')
|
|
370
|
+
.map((call) => ({
|
|
371
|
+
id: call.toolCall.id,
|
|
372
|
+
name: call.toolName,
|
|
373
|
+
input: call.input,
|
|
374
|
+
}));
|
|
375
|
+
const batch = Object.freeze({
|
|
376
|
+
reviewCalls: Object.freeze(reviewCalls),
|
|
377
|
+
calls: new Map(calls),
|
|
378
|
+
});
|
|
379
|
+
this.preparedBatches.add(batch);
|
|
380
|
+
return batch;
|
|
381
|
+
}
|
|
382
|
+
async executeBatch(response, denials, prior, preparedBatch) {
|
|
232
383
|
const toolCalls = response.message.toolCalls;
|
|
233
384
|
if (!toolCalls) {
|
|
234
385
|
return { messages: [], results: [], observations: [] };
|
|
235
386
|
}
|
|
387
|
+
assertUniqueToolCallIds(toolCalls);
|
|
236
388
|
this.batchCounter += 1;
|
|
237
389
|
// Sampled here, once, and held for every call below. See the note on
|
|
238
390
|
// `permissionMode` in the config type.
|
|
239
391
|
this.batchMode = this.resolvePermissionMode();
|
|
240
392
|
try {
|
|
241
|
-
|
|
393
|
+
const owned = preparedBatch;
|
|
394
|
+
if (owned && !this.preparedBatches.has(owned)) {
|
|
395
|
+
throw new Error('Prepared tool batch is not owned by this executor.');
|
|
396
|
+
}
|
|
397
|
+
return await this.runBatch(toolCalls, denials, prior, owned);
|
|
242
398
|
}
|
|
243
399
|
finally {
|
|
244
400
|
// Cleared so a later single execution outside a batch resolves
|
|
@@ -246,7 +402,7 @@ export class ToolExecutor {
|
|
|
246
402
|
this.batchMode = undefined;
|
|
247
403
|
}
|
|
248
404
|
}
|
|
249
|
-
async runBatch(toolCalls, denials, prior) {
|
|
405
|
+
async runBatch(toolCalls, denials, prior, preparedBatch) {
|
|
250
406
|
this.log.debug('Executing tool batch', {
|
|
251
407
|
[NAMZU.RUN_ID]: this.config.runId,
|
|
252
408
|
'namzu.runtime.tool_count': toolCalls.length,
|
|
@@ -278,6 +434,7 @@ export class ToolExecutor {
|
|
|
278
434
|
// order independence.
|
|
279
435
|
const gate = new Semaphore(this.config.maxToolConcurrency ?? DEFAULT_TOOL_CONCURRENCY);
|
|
280
436
|
toolCalls.forEach((toolCall, i) => {
|
|
437
|
+
const preparedCall = preparedBatch?.calls.get(toolCall.id);
|
|
281
438
|
const recovered = prior?.get(toolCall.id);
|
|
282
439
|
if (recovered !== undefined) {
|
|
283
440
|
// This call already ran, in a process that died before the
|
|
@@ -298,7 +455,7 @@ export class ToolExecutor {
|
|
|
298
455
|
// message so the assistant turn stays fully answered. Run them
|
|
299
456
|
// on the parallel branch — they perform no side effects, so
|
|
300
457
|
// serialization would only add latency.
|
|
301
|
-
parallel.push(this.recordDenial(toolCall, denialReason).then((r) => {
|
|
458
|
+
parallel.push(this.recordDenial(toolCall, denialReason, preparedCall).then((r) => {
|
|
302
459
|
results[i] = r;
|
|
303
460
|
}));
|
|
304
461
|
return;
|
|
@@ -306,33 +463,33 @@ export class ToolExecutor {
|
|
|
306
463
|
// Per-call, because the event has to name which call it is about:
|
|
307
464
|
// a batch can run several tools at once and a host rendering them
|
|
308
465
|
// side by side needs to know whose progress this is.
|
|
466
|
+
const progress = new ToolProgressPublisher(this.emitEvent, {
|
|
467
|
+
type: 'tool_progress',
|
|
468
|
+
runId: this.config.runId,
|
|
469
|
+
toolUseId: toolCall.id,
|
|
470
|
+
toolName: toolCall.function.name,
|
|
471
|
+
});
|
|
309
472
|
const ctx = {
|
|
310
473
|
...baseContext,
|
|
311
474
|
toolUseId: toolCall.id,
|
|
475
|
+
source: { kind: 'direct' },
|
|
312
476
|
// Overridden per call, so a nested dispatch can name the call
|
|
313
477
|
// that made it. The base context has no `toolUseId`, and a
|
|
314
478
|
// closure built there would report every nested call as
|
|
315
479
|
// parentless.
|
|
316
|
-
dispatchTool: (name, input) => this.dispatchNested(name, input, ctx, recordObservation),
|
|
480
|
+
dispatchTool: (name, input, options) => this.dispatchNested(name, input, ctx, recordObservation, toolCall.function.name, options),
|
|
317
481
|
// Per-call for the same reason: a pause has to be routed back
|
|
318
482
|
// to the call that raised it, and a batch can raise several.
|
|
319
483
|
...(this.config.toolPause ? { requestPause: this.config.toolPause(toolCall.id) } : {}),
|
|
320
|
-
report: (message, fraction) =>
|
|
321
|
-
// Fire-and-forget: a tool reporting progress must never be
|
|
322
|
-
// able to fail because the host's listener threw, and must
|
|
323
|
-
// never have to await the emit mid-work.
|
|
324
|
-
void this.emitEvent({
|
|
325
|
-
type: 'tool_progress',
|
|
326
|
-
runId: this.config.runId,
|
|
327
|
-
toolUseId: toolCall.id,
|
|
328
|
-
toolName: toolCall.function.name,
|
|
329
|
-
message,
|
|
330
|
-
...(fraction !== undefined ? { fraction: Math.min(1, Math.max(0, fraction)) } : {}),
|
|
331
|
-
}).catch(() => { });
|
|
332
|
-
},
|
|
484
|
+
report: (message, fraction) => progress.report(message, fraction),
|
|
333
485
|
};
|
|
334
486
|
const run = async () => {
|
|
335
|
-
|
|
487
|
+
try {
|
|
488
|
+
results[i] = await this.executeSingle(toolCall, ctx, recordObservation, () => progress.close(), preparedCall);
|
|
489
|
+
}
|
|
490
|
+
finally {
|
|
491
|
+
await progress.close();
|
|
492
|
+
}
|
|
336
493
|
};
|
|
337
494
|
const gated = async () => {
|
|
338
495
|
await gate.acquire();
|
|
@@ -343,14 +500,19 @@ export class ToolExecutor {
|
|
|
343
500
|
gate.release();
|
|
344
501
|
}
|
|
345
502
|
};
|
|
346
|
-
let input = {};
|
|
503
|
+
let input = preparedCall?.input ?? {};
|
|
347
504
|
try {
|
|
348
|
-
|
|
505
|
+
if (!preparedBatch?.calls.has(toolCall.id)) {
|
|
506
|
+
input = JSON.parse(toolCall.function.arguments || '{}');
|
|
507
|
+
}
|
|
349
508
|
}
|
|
350
509
|
catch {
|
|
351
510
|
// non-JSON args → treat as unsafe (serialize), the conservative path
|
|
352
511
|
}
|
|
353
|
-
const
|
|
512
|
+
const preparedToolName = preparedCall?.toolName;
|
|
513
|
+
const safe = this.config.tools
|
|
514
|
+
.get(preparedToolName ?? toolCall.function.name)
|
|
515
|
+
?.isConcurrencySafe?.(input) === true;
|
|
354
516
|
if (safe)
|
|
355
517
|
parallel.push(gated());
|
|
356
518
|
else
|
|
@@ -402,43 +564,219 @@ export class ToolExecutor {
|
|
|
402
564
|
* rendering a timeline draws eleven siblings where there is one call with
|
|
403
565
|
* eleven children.
|
|
404
566
|
*/
|
|
405
|
-
async dispatchNested(name, input, context, recordObservation) {
|
|
567
|
+
async dispatchNested(name, input, context, recordObservation, parentToolName, options) {
|
|
568
|
+
const signal = options?.signal
|
|
569
|
+
? AbortSignal.any([context.abortSignal, options.signal])
|
|
570
|
+
: context.abortSignal;
|
|
571
|
+
// Authority is checked before an id, activity or event is created. A
|
|
572
|
+
// retained closure must be observationally inert after its invocation
|
|
573
|
+
// ends, not merely unable to finish the registry call it already started.
|
|
574
|
+
signal.throwIfAborted();
|
|
575
|
+
const preparedCall = await this.prepareNestedCall(name, input, signal);
|
|
576
|
+
signal.throwIfAborted();
|
|
577
|
+
const preparedInput = preparedCall.input;
|
|
406
578
|
const parent = context.toolUseId;
|
|
407
|
-
const via = parent
|
|
579
|
+
const via = parent && parentToolName
|
|
580
|
+
? {
|
|
581
|
+
tool: parentToolName,
|
|
582
|
+
toolUseId: parent,
|
|
583
|
+
...(options?.runtimeToolCallId ? { runtimeToolCallId: options.runtimeToolCallId } : {}),
|
|
584
|
+
}
|
|
585
|
+
: undefined;
|
|
408
586
|
// Its own id, minted here. Reusing the parent's would make two
|
|
409
587
|
// different calls indistinguishable in any log keyed by it, which is
|
|
410
588
|
// exactly how a nested write gets attributed to the program that ran
|
|
411
589
|
// it rather than to itself.
|
|
412
590
|
const nestedId = generateToolCallId();
|
|
413
591
|
const startedAt = Date.now();
|
|
592
|
+
const source = parent
|
|
593
|
+
? options?.runtimeToolCallId
|
|
594
|
+
? {
|
|
595
|
+
kind: 'code',
|
|
596
|
+
parentToolUseId: parent,
|
|
597
|
+
runtimeToolCallId: options.runtimeToolCallId,
|
|
598
|
+
}
|
|
599
|
+
: { kind: 'nested', parentToolUseId: parent }
|
|
600
|
+
: { kind: 'direct' };
|
|
601
|
+
const progress = new ToolProgressPublisher(this.emitEvent, {
|
|
602
|
+
type: 'tool_progress',
|
|
603
|
+
runId: this.config.runId,
|
|
604
|
+
toolUseId: nestedId,
|
|
605
|
+
toolName: name,
|
|
606
|
+
});
|
|
607
|
+
if (preparedCall.kind === 'synthetic') {
|
|
608
|
+
await this.emitEvent({
|
|
609
|
+
type: 'tool_executing',
|
|
610
|
+
runId: this.config.runId,
|
|
611
|
+
toolUseId: nestedId,
|
|
612
|
+
toolName: name,
|
|
613
|
+
input: preparedInput,
|
|
614
|
+
...(via ? { via } : {}),
|
|
615
|
+
});
|
|
616
|
+
await progress.close();
|
|
617
|
+
await this.emitEvent({
|
|
618
|
+
type: 'tool_completed',
|
|
619
|
+
runId: this.config.runId,
|
|
620
|
+
toolUseId: nestedId,
|
|
621
|
+
toolName: name,
|
|
622
|
+
result: preparedCall.message,
|
|
623
|
+
isError: preparedCall.isError,
|
|
624
|
+
durationMs: Date.now() - startedAt,
|
|
625
|
+
outputLength: preparedCall.message.length,
|
|
626
|
+
...(via ? { via } : {}),
|
|
627
|
+
});
|
|
628
|
+
return preparedCall.isError
|
|
629
|
+
? { success: false, output: '', error: preparedCall.message }
|
|
630
|
+
: { success: true, output: preparedCall.message };
|
|
631
|
+
}
|
|
632
|
+
const gateResult = this.config.authorizationGate?.evaluate({
|
|
633
|
+
toolName: name,
|
|
634
|
+
toolInput: preparedInput,
|
|
635
|
+
toolDef: this.config.tools.get(name),
|
|
636
|
+
});
|
|
637
|
+
if (gateResult && gateResult.decision !== 'allow') {
|
|
638
|
+
const reason = gateResult.decision === 'deny'
|
|
639
|
+
? `Blocked by the authorization gate: ${gateResult.reason}`
|
|
640
|
+
: `Blocked by the authorization gate: this nested call requires an explicit allow rule because an operator review cannot be opened from inside another tool. ${gateResult.reason}`;
|
|
641
|
+
const output = deniedToolOutput(name, reason);
|
|
642
|
+
// Same fail-closed durability rule as a direct gate denial: if the
|
|
643
|
+
// configured run store cannot record the refusal, do not quietly carry
|
|
644
|
+
// on with an unaudited execution.
|
|
645
|
+
if (!this.config.recordAudit) {
|
|
646
|
+
throw new Error(`Nested tool "${name}" was refused, but no durable audit recorder is configured.`);
|
|
647
|
+
}
|
|
648
|
+
await this.config.recordAudit({
|
|
649
|
+
what: { action: 'tool_call', tool: name },
|
|
650
|
+
outcome: 'refused',
|
|
651
|
+
reason,
|
|
652
|
+
});
|
|
653
|
+
await progress.close();
|
|
654
|
+
await this.emitEvent({
|
|
655
|
+
type: 'tool_executing',
|
|
656
|
+
runId: this.config.runId,
|
|
657
|
+
toolUseId: nestedId,
|
|
658
|
+
toolName: name,
|
|
659
|
+
input: preparedInput,
|
|
660
|
+
...(via ? { via } : {}),
|
|
661
|
+
});
|
|
662
|
+
await this.emitEvent({
|
|
663
|
+
type: 'tool_completed',
|
|
664
|
+
runId: this.config.runId,
|
|
665
|
+
toolUseId: nestedId,
|
|
666
|
+
toolName: name,
|
|
667
|
+
result: output,
|
|
668
|
+
isError: true,
|
|
669
|
+
durationMs: Date.now() - startedAt,
|
|
670
|
+
outputLength: output.length,
|
|
671
|
+
...(via ? { via } : {}),
|
|
672
|
+
});
|
|
673
|
+
return { success: false, output: '', error: reason };
|
|
674
|
+
}
|
|
675
|
+
const childContext = {
|
|
676
|
+
...context,
|
|
677
|
+
abortSignal: signal,
|
|
678
|
+
toolUseId: nestedId,
|
|
679
|
+
source,
|
|
680
|
+
dispatchTool: (childName, childInput, childOptions) => this.dispatchNested(childName, childInput, childContext, recordObservation, name, childOptions),
|
|
681
|
+
// A nested execution has its own event/progress identity, but its
|
|
682
|
+
// durable pause belongs to the nearest model-issued ancestor. The
|
|
683
|
+
// checkpoint transcript contains that ancestor call and not this
|
|
684
|
+
// ephemeral child id; minting a pause route for `nestedId` makes the
|
|
685
|
+
// answer impossible to match after process restart. `...context`
|
|
686
|
+
// intentionally preserves the ancestor route here.
|
|
687
|
+
report: (message, fraction) => progress.report(message, fraction),
|
|
688
|
+
};
|
|
414
689
|
await this.emitEvent({
|
|
415
690
|
type: 'tool_executing',
|
|
416
691
|
runId: this.config.runId,
|
|
417
692
|
toolUseId: nestedId,
|
|
418
693
|
toolName: name,
|
|
419
|
-
input,
|
|
694
|
+
input: preparedInput,
|
|
695
|
+
...(via ? { via } : {}),
|
|
696
|
+
});
|
|
697
|
+
const vetoOutcome = this.probes.queryVeto({
|
|
698
|
+
type: 'tool_executing',
|
|
699
|
+
runId: this.config.runId,
|
|
700
|
+
toolUseId: nestedId,
|
|
701
|
+
toolName: name,
|
|
702
|
+
input: preparedInput,
|
|
420
703
|
...(via ? { via } : {}),
|
|
704
|
+
}, buildProbeContext({ runId: this.config.runId }));
|
|
705
|
+
if (vetoOutcome.action === 'deny') {
|
|
706
|
+
const probeName = vetoOutcome.probeName ?? 'unnamed';
|
|
707
|
+
const reason = vetoOutcome.reason ?? 'no reason provided';
|
|
708
|
+
const message = new ProbeVetoError(probeName, reason, 'tool_executing').message;
|
|
709
|
+
await progress.close();
|
|
710
|
+
await this.emitEvent({
|
|
711
|
+
type: 'tool_completed',
|
|
712
|
+
runId: this.config.runId,
|
|
713
|
+
toolUseId: nestedId,
|
|
714
|
+
toolName: name,
|
|
715
|
+
result: `Error: ${message}`,
|
|
716
|
+
isError: true,
|
|
717
|
+
durationMs: Date.now() - startedAt,
|
|
718
|
+
outputLength: message.length + 7,
|
|
719
|
+
...(via ? { via } : {}),
|
|
720
|
+
});
|
|
721
|
+
return { success: false, output: '', error: message };
|
|
722
|
+
}
|
|
723
|
+
let result;
|
|
724
|
+
try {
|
|
725
|
+
// Use the same deadline layer as a model-issued call. Calling the
|
|
726
|
+
// registry directly made nested tools the only tools whose own
|
|
727
|
+
// `timeoutMs` declaration was ignored.
|
|
728
|
+
result = await this.runOnce(name, preparedInput, childContext, preparedCall.kind === 'ready' ? preparedCall.prepared : undefined);
|
|
729
|
+
}
|
|
730
|
+
finally {
|
|
731
|
+
await progress.close();
|
|
732
|
+
}
|
|
733
|
+
const rawOutput = result.success
|
|
734
|
+
? result.output
|
|
735
|
+
: formatFailedToolOutput(result.output, result.error);
|
|
736
|
+
const budgeted = applyToolOutputBudget({
|
|
737
|
+
toolName: name,
|
|
738
|
+
toolUseId: nestedId,
|
|
739
|
+
output: rawOutput,
|
|
740
|
+
maxChars: this.config.maxToolOutputChars ?? DEFAULT_MAX_TOOL_OUTPUT_CHARS,
|
|
741
|
+
spillDir: this.config.toolOutputDir,
|
|
742
|
+
onError: (message) => this.log.warn('Failed to spill oversized nested tool output', {
|
|
743
|
+
[NAMZU.RUN_ID]: this.config.runId,
|
|
744
|
+
[GENAI.TOOL_NAME]: name,
|
|
745
|
+
'exception.message': message,
|
|
746
|
+
}),
|
|
421
747
|
});
|
|
422
|
-
const
|
|
748
|
+
const visibleResult = result.success
|
|
749
|
+
? { ...result, output: budgeted.output }
|
|
750
|
+
: {
|
|
751
|
+
...result,
|
|
752
|
+
output: '',
|
|
753
|
+
// `run_code` sends `error`, not `output`, across the worker
|
|
754
|
+
// bridge on a failed host call. Leaving the raw error here would
|
|
755
|
+
// make the success path bounded and the failure path unbounded.
|
|
756
|
+
error: budgeted.output,
|
|
757
|
+
};
|
|
423
758
|
await this.emitEvent({
|
|
424
759
|
type: 'tool_completed',
|
|
425
760
|
runId: this.config.runId,
|
|
426
761
|
toolUseId: nestedId,
|
|
427
762
|
toolName: name,
|
|
428
|
-
result:
|
|
763
|
+
result: budgeted.output,
|
|
429
764
|
isError: !result.success,
|
|
430
765
|
durationMs: Date.now() - startedAt,
|
|
766
|
+
outputLength: budgeted.originalLength,
|
|
767
|
+
...(budgeted.truncated ? { outputTruncated: true } : {}),
|
|
768
|
+
...(budgeted.spillPath ? { outputSpillPath: budgeted.spillPath } : {}),
|
|
431
769
|
...(via ? { via } : {}),
|
|
432
770
|
});
|
|
433
771
|
recordObservation({
|
|
434
772
|
runId: this.config.runId,
|
|
435
773
|
toolUseId: nestedId,
|
|
436
774
|
toolName: name,
|
|
437
|
-
input,
|
|
775
|
+
input: preparedInput,
|
|
438
776
|
result,
|
|
439
777
|
...(parent ? { parentToolUseId: parent } : {}),
|
|
440
778
|
});
|
|
441
|
-
return
|
|
779
|
+
return visibleResult;
|
|
442
780
|
}
|
|
443
781
|
buildToolContext(recordObservation = () => { }) {
|
|
444
782
|
const context = {
|
|
@@ -463,6 +801,7 @@ export class ToolExecutor {
|
|
|
463
801
|
adoptSkillScope: (scope) => {
|
|
464
802
|
this.skillScope = { ...scope, adoptedInBatch: this.batchCounter };
|
|
465
803
|
},
|
|
804
|
+
maxToolOutputChars: this.config.maxToolOutputChars ?? DEFAULT_MAX_TOOL_OUTPUT_CHARS,
|
|
466
805
|
...(this.config.skills ? { skills: this.config.skills } : {}),
|
|
467
806
|
...(this.config.web ? { web: this.config.web } : {}),
|
|
468
807
|
// The SAME registry and the SAME context a model-issued call
|
|
@@ -473,7 +812,7 @@ export class ToolExecutor {
|
|
|
473
812
|
// dispatching outside a batch has no parent call to name. The
|
|
474
813
|
// per-call context below overrides it with one that does; see
|
|
475
814
|
// `dispatchNested`.
|
|
476
|
-
dispatchTool: (name, input) => this.dispatchNested(name, input, context, recordObservation),
|
|
815
|
+
dispatchTool: (name, input, options) => this.dispatchNested(name, input, context, recordObservation, undefined, options),
|
|
477
816
|
sandbox: this.config.sandbox,
|
|
478
817
|
fileReadTracker: this.fileReadTracker,
|
|
479
818
|
// Bound to this run, once. Binding here rather than passing the
|
|
@@ -498,90 +837,101 @@ export class ToolExecutor {
|
|
|
498
837
|
};
|
|
499
838
|
return context;
|
|
500
839
|
}
|
|
501
|
-
async executeSingle(toolCall, toolContext, recordObservation) {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
// for, and it used to be the one case that never reached it: this
|
|
505
|
-
// branch returned before `resolveCall` ran, so the motivating failure
|
|
506
|
-
// was answered with a generic hint while the configured repairer sat
|
|
507
|
-
// unused. Offer it the partial buffer first.
|
|
508
|
-
const truncationRepair = toolCall.metadata?.inputTruncated === true
|
|
509
|
-
? await this.repairTruncatedCall(toolCall, toolName)
|
|
510
|
-
: null;
|
|
511
|
-
if (toolCall.metadata?.inputTruncated === true && !truncationRepair) {
|
|
512
|
-
const message = truncatedToolInputMessage(toolName);
|
|
513
|
-
await this.emitEvent({
|
|
514
|
-
type: 'tool_executing',
|
|
515
|
-
runId: this.config.runId,
|
|
516
|
-
toolUseId: toolCall.id,
|
|
517
|
-
toolName,
|
|
518
|
-
input: {},
|
|
519
|
-
});
|
|
520
|
-
await this.emitEvent({
|
|
521
|
-
type: 'tool_completed',
|
|
522
|
-
runId: this.config.runId,
|
|
523
|
-
toolUseId: toolCall.id,
|
|
524
|
-
toolName,
|
|
525
|
-
result: message,
|
|
526
|
-
isError: true,
|
|
527
|
-
});
|
|
528
|
-
return {
|
|
529
|
-
toolCallId: toolCall.id,
|
|
530
|
-
toolName,
|
|
531
|
-
output: message,
|
|
532
|
-
isError: true,
|
|
533
|
-
};
|
|
840
|
+
async executeSingle(toolCall, toolContext, recordObservation, settleProgress, preparedCall) {
|
|
841
|
+
if (preparedCall?.kind === 'synthetic') {
|
|
842
|
+
return this.recordSyntheticPreparation(preparedCall);
|
|
534
843
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
?
|
|
541
|
-
...toolCall,
|
|
542
|
-
function: {
|
|
543
|
-
...toolCall.function,
|
|
544
|
-
name: truncationRepair.toolName ?? toolName,
|
|
545
|
-
arguments: truncationRepair.arguments,
|
|
546
|
-
},
|
|
547
|
-
metadata: {},
|
|
548
|
-
}
|
|
549
|
-
: toolCall);
|
|
550
|
-
toolName = resolved.toolName;
|
|
551
|
-
if (!resolved.ok) {
|
|
552
|
-
// malformed JSON args used to return without ever
|
|
553
|
-
// emitting tool_executing or tool_completed, leaving UI cards
|
|
554
|
-
// orphaned in `streaming_input`. Emit the executing→completed
|
|
555
|
-
// terminal pair so the card lifecycle closes.
|
|
556
|
-
const message = resolved.message;
|
|
557
|
-
await this.emitEvent({
|
|
558
|
-
type: 'tool_executing',
|
|
559
|
-
runId: this.config.runId,
|
|
560
|
-
toolUseId: toolCall.id,
|
|
561
|
-
toolName,
|
|
562
|
-
input: {},
|
|
563
|
-
});
|
|
564
|
-
await this.emitEvent({
|
|
565
|
-
type: 'tool_completed',
|
|
566
|
-
runId: this.config.runId,
|
|
567
|
-
toolUseId: toolCall.id,
|
|
568
|
-
toolName,
|
|
569
|
-
result: message,
|
|
570
|
-
isError: true,
|
|
571
|
-
});
|
|
572
|
-
return {
|
|
573
|
-
toolCallId: toolCall.id,
|
|
574
|
-
toolName,
|
|
575
|
-
output: message,
|
|
576
|
-
isError: true,
|
|
577
|
-
};
|
|
844
|
+
let toolName = preparedCall?.toolName ?? toolCall.function.name;
|
|
845
|
+
let input;
|
|
846
|
+
let prepared;
|
|
847
|
+
if (preparedCall?.kind === 'ready' || preparedCall?.kind === 'legacy') {
|
|
848
|
+
input = preparedCall.input;
|
|
849
|
+
prepared = preparedCall.kind === 'ready' ? preparedCall.prepared : undefined;
|
|
578
850
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
851
|
+
else {
|
|
852
|
+
// A stream that cut off mid-JSON is the case `repairToolCall` exists
|
|
853
|
+
// for, and it used to be the one case that never reached it: this
|
|
854
|
+
// branch returned before `resolveCall` ran, so the motivating failure
|
|
855
|
+
// was answered with a generic hint while the configured repairer sat
|
|
856
|
+
// unused. Offer it the partial buffer first.
|
|
857
|
+
const truncationRepair = toolCall.metadata?.inputTruncated === true
|
|
858
|
+
? await this.repairTruncatedCall(toolCall, toolName)
|
|
859
|
+
: null;
|
|
860
|
+
if (toolCall.metadata?.inputTruncated === true && !truncationRepair) {
|
|
861
|
+
const message = truncatedToolInputMessage(toolName);
|
|
862
|
+
await this.emitEvent({
|
|
863
|
+
type: 'tool_executing',
|
|
864
|
+
runId: this.config.runId,
|
|
865
|
+
toolUseId: toolCall.id,
|
|
866
|
+
toolName,
|
|
867
|
+
input: {},
|
|
868
|
+
});
|
|
869
|
+
await this.emitEvent({
|
|
870
|
+
type: 'tool_completed',
|
|
871
|
+
runId: this.config.runId,
|
|
872
|
+
toolUseId: toolCall.id,
|
|
873
|
+
toolName,
|
|
874
|
+
result: message,
|
|
875
|
+
isError: true,
|
|
876
|
+
});
|
|
877
|
+
return {
|
|
878
|
+
toolCallId: toolCall.id,
|
|
879
|
+
toolName,
|
|
880
|
+
output: message,
|
|
881
|
+
isError: true,
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
// A malformed call used to cost a full model round trip to fix: the
|
|
885
|
+
// error went back as a `tool_result`, the model re-read the whole
|
|
886
|
+
// context and tried again. A host that can repair it locally turns
|
|
887
|
+
// that into nothing. No-op when no repairer is configured.
|
|
888
|
+
const resolved = await this.resolveCall(truncationRepair
|
|
889
|
+
? {
|
|
890
|
+
...toolCall,
|
|
891
|
+
function: {
|
|
892
|
+
...toolCall.function,
|
|
893
|
+
name: truncationRepair.toolName ?? toolName,
|
|
894
|
+
arguments: truncationRepair.arguments,
|
|
895
|
+
},
|
|
896
|
+
metadata: {},
|
|
897
|
+
}
|
|
898
|
+
: toolCall);
|
|
899
|
+
toolName = resolved.toolName;
|
|
900
|
+
if (!resolved.ok) {
|
|
901
|
+
// malformed JSON args used to return without ever
|
|
902
|
+
// emitting tool_executing or tool_completed, leaving UI cards
|
|
903
|
+
// orphaned in `streaming_input`. Emit the executing→completed
|
|
904
|
+
// terminal pair so the card lifecycle closes.
|
|
905
|
+
const message = resolved.message;
|
|
906
|
+
await this.emitEvent({
|
|
907
|
+
type: 'tool_executing',
|
|
908
|
+
runId: this.config.runId,
|
|
909
|
+
toolUseId: toolCall.id,
|
|
910
|
+
toolName,
|
|
911
|
+
input: {},
|
|
912
|
+
});
|
|
913
|
+
await this.emitEvent({
|
|
914
|
+
type: 'tool_completed',
|
|
915
|
+
runId: this.config.runId,
|
|
916
|
+
toolUseId: toolCall.id,
|
|
917
|
+
toolName,
|
|
918
|
+
result: message,
|
|
919
|
+
isError: true,
|
|
920
|
+
});
|
|
921
|
+
return {
|
|
922
|
+
toolCallId: toolCall.id,
|
|
923
|
+
toolName,
|
|
924
|
+
output: message,
|
|
925
|
+
isError: true,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
input = resolved.input;
|
|
929
|
+
const preOutcome = await this.runPreToolHook(toolName, input);
|
|
930
|
+
if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
|
|
931
|
+
return this.recordSyntheticHookOutcome(toolCall.id, toolName, preOutcome.input, preOutcome);
|
|
932
|
+
}
|
|
933
|
+
input = preOutcome.input;
|
|
583
934
|
}
|
|
584
|
-
input = preOutcome.input;
|
|
585
935
|
const activity = this.activityStore.create({
|
|
586
936
|
type: 'tool_call',
|
|
587
937
|
description: toolName,
|
|
@@ -661,7 +1011,7 @@ export class ToolExecutor {
|
|
|
661
1011
|
// version silently DROPPED `content`, so a tool returning an image
|
|
662
1012
|
// block had it discarded here — before the wire mapper that was
|
|
663
1013
|
// built to carry it ever saw it.
|
|
664
|
-
let result = await this.runOnce(toolName, input, toolContext);
|
|
1014
|
+
let result = await this.runOnce(toolName, input, toolContext, prepared);
|
|
665
1015
|
let post = await this.runPostToolHook(toolName, input, result);
|
|
666
1016
|
// In-loop retry. A transient failure used to cost a full model round
|
|
667
1017
|
// trip: the error went back as a `tool_result`, the model read it and
|
|
@@ -728,22 +1078,38 @@ export class ToolExecutor {
|
|
|
728
1078
|
// resume to trip over.
|
|
729
1079
|
break;
|
|
730
1080
|
}
|
|
731
|
-
result = await this.runOnce(toolName, input, toolContext);
|
|
1081
|
+
result = await this.runOnce(toolName, input, toolContext, prepared);
|
|
732
1082
|
post = await this.runPostToolHook(toolName, input, result);
|
|
733
1083
|
}
|
|
734
1084
|
const durationMs = Date.now() - startMs;
|
|
735
1085
|
const rawOutput = result.success
|
|
736
1086
|
? result.output
|
|
737
1087
|
: formatFailedToolOutput(result.output, result.error);
|
|
738
|
-
|
|
1088
|
+
const postOverride = post.override;
|
|
1089
|
+
let output = postOverride?.output ?? (result.success ? this.maybeCompress(toolName, rawOutput) : rawOutput);
|
|
1090
|
+
const selectedContent = postOverride?.isError
|
|
1091
|
+
? undefined
|
|
1092
|
+
: (postOverride?.content ?? result.content);
|
|
1093
|
+
const maxToolOutputChars = this.config.maxToolOutputChars ?? DEFAULT_MAX_TOOL_OUTPUT_CHARS;
|
|
1094
|
+
// If the text preview forces rich content out, say so inside the SAME
|
|
1095
|
+
// budget. Appending this after truncation made the diagnostic itself a
|
|
1096
|
+
// cap bypass; selecting a post-hook override after truncation was a
|
|
1097
|
+
// larger bypass that could replace a bounded result with anything.
|
|
1098
|
+
if (maxToolOutputChars > 0 &&
|
|
1099
|
+
output.length > maxToolOutputChars &&
|
|
1100
|
+
selectedContent !== undefined) {
|
|
1101
|
+
const dropped = describeDroppedContent(selectedContent);
|
|
1102
|
+
if (dropped)
|
|
1103
|
+
output = `${output}\n\n${dropped}`;
|
|
1104
|
+
}
|
|
739
1105
|
// Compression is opportunistic and shell-only; the budget is the
|
|
740
|
-
// hard bound that applies to every tool
|
|
741
|
-
//
|
|
1106
|
+
// hard bound that applies to every final tool result, including a
|
|
1107
|
+
// post-tool hook's replacement and the rich-content omission notice.
|
|
742
1108
|
const budgeted = applyToolOutputBudget({
|
|
743
1109
|
toolName,
|
|
744
1110
|
toolUseId: toolCall.id,
|
|
745
1111
|
output,
|
|
746
|
-
maxChars:
|
|
1112
|
+
maxChars: maxToolOutputChars,
|
|
747
1113
|
spillDir: this.config.toolOutputDir,
|
|
748
1114
|
onError: (message) => this.log.warn('Failed to spill oversized tool output', {
|
|
749
1115
|
[NAMZU.RUN_ID]: this.config.runId,
|
|
@@ -760,21 +1126,6 @@ export class ToolExecutor {
|
|
|
760
1126
|
});
|
|
761
1127
|
}
|
|
762
1128
|
output = budgeted.output;
|
|
763
|
-
// A truncated text half drops the rich half with it — the preview is
|
|
764
|
-
// no longer the tool's own payload, so an image alongside it would be
|
|
765
|
-
// illustrating something the model can no longer read. Dropping is
|
|
766
|
-
// right; doing it SILENTLY is not. The model saw a preview and had no
|
|
767
|
-
// way to know an image existed at all, so it reasoned as though the
|
|
768
|
-
// tool had returned text only.
|
|
769
|
-
if (budgeted.truncated && result.content !== undefined) {
|
|
770
|
-
const dropped = describeDroppedContent(result.content);
|
|
771
|
-
if (dropped)
|
|
772
|
-
output = `${output}\n\n${dropped}`;
|
|
773
|
-
}
|
|
774
|
-
const postOverride = post.override;
|
|
775
|
-
if (postOverride !== null) {
|
|
776
|
-
output = postOverride.output;
|
|
777
|
-
}
|
|
778
1129
|
// A failed call, or an override that says the call failed. A `replace`
|
|
779
1130
|
// says the opposite, and reading it as a failure is what made redaction
|
|
780
1131
|
// unusable: the model was told a successful call had gone wrong, and
|
|
@@ -807,6 +1158,11 @@ export class ToolExecutor {
|
|
|
807
1158
|
this.activityStore.complete(activity.id, output);
|
|
808
1159
|
}
|
|
809
1160
|
}
|
|
1161
|
+
// The terminal event closes the live row. Every accepted progress update
|
|
1162
|
+
// must settle before it, otherwise a slow host can receive an update for a
|
|
1163
|
+
// call it has already removed. Detached work reporting after a timeout is
|
|
1164
|
+
// ignored because close() also revokes the publisher.
|
|
1165
|
+
await settleProgress();
|
|
810
1166
|
await this.emitEvent({
|
|
811
1167
|
type: 'tool_completed',
|
|
812
1168
|
runId: this.config.runId,
|
|
@@ -829,14 +1185,9 @@ export class ToolExecutor {
|
|
|
829
1185
|
result,
|
|
830
1186
|
});
|
|
831
1187
|
const resolveContent = () => {
|
|
832
|
-
if (
|
|
833
|
-
return { content: this.budgetContent(postOverride.content, toolName) };
|
|
834
|
-
}
|
|
835
|
-
if (postOverride?.isError)
|
|
1188
|
+
if (budgeted.truncated || selectedContent === undefined)
|
|
836
1189
|
return {};
|
|
837
|
-
|
|
838
|
-
return {};
|
|
839
|
-
return { content: this.budgetContent(result.content, toolName) };
|
|
1190
|
+
return { content: this.budgetContent(selectedContent, toolName) };
|
|
840
1191
|
};
|
|
841
1192
|
return {
|
|
842
1193
|
toolCallId: toolCall.id,
|
|
@@ -876,30 +1227,31 @@ export class ToolExecutor {
|
|
|
876
1227
|
* model sees "this timed out" as a `tool_result` and can route around
|
|
877
1228
|
* it. A throw would end the run over one slow tool.
|
|
878
1229
|
*/
|
|
879
|
-
async executeWithDeadline(toolName, input, toolContext) {
|
|
1230
|
+
async executeWithDeadline(toolName, input, toolContext, prepared) {
|
|
880
1231
|
const timeoutMs = this.config.tools.get(toolName)?.timeoutMs ??
|
|
881
1232
|
this.config.toolTimeoutMs ??
|
|
882
1233
|
DEFAULT_TOOL_TIMEOUT_MS;
|
|
883
|
-
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
884
|
-
return this.config.tools.execute(toolName, input, toolContext);
|
|
885
|
-
}
|
|
886
1234
|
const controller = new AbortController();
|
|
887
|
-
const
|
|
888
|
-
const
|
|
889
|
-
if (
|
|
890
|
-
controller.abort(
|
|
1235
|
+
const parentSignal = toolContext.abortSignal;
|
|
1236
|
+
const onParentAbort = () => controller.abort(parentSignal.reason);
|
|
1237
|
+
if (parentSignal.aborted)
|
|
1238
|
+
controller.abort(parentSignal.reason);
|
|
891
1239
|
else
|
|
892
|
-
|
|
1240
|
+
parentSignal.addEventListener('abort', onParentAbort, { once: true });
|
|
893
1241
|
let timer;
|
|
894
1242
|
let timedOut = false;
|
|
1243
|
+
let invocationOpen = true;
|
|
1244
|
+
const nestedDispatches = new Set();
|
|
895
1245
|
try {
|
|
896
|
-
const expired =
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1246
|
+
const expired = Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
1247
|
+
? new Promise((resolve) => {
|
|
1248
|
+
timer = setTimeout(() => {
|
|
1249
|
+
timedOut = true;
|
|
1250
|
+
controller.abort(new Error(`Tool "${toolName}" exceeded ${timeoutMs}ms`));
|
|
1251
|
+
resolve('timeout');
|
|
1252
|
+
}, timeoutMs);
|
|
1253
|
+
})
|
|
1254
|
+
: undefined;
|
|
903
1255
|
const aborted = new Promise((resolve) => {
|
|
904
1256
|
if (controller.signal.aborted && !timedOut) {
|
|
905
1257
|
resolve('aborted');
|
|
@@ -910,14 +1262,47 @@ export class ToolExecutor {
|
|
|
910
1262
|
resolve('aborted');
|
|
911
1263
|
}, { once: true });
|
|
912
1264
|
});
|
|
913
|
-
const
|
|
1265
|
+
const inheritedDispatch = toolContext.dispatchTool;
|
|
1266
|
+
const scopedDispatch = inheritedDispatch
|
|
1267
|
+
? (name, nestedInput, options) => {
|
|
1268
|
+
if (!invocationOpen) {
|
|
1269
|
+
return Promise.reject(new Error(`Tool "${toolName}" invocation has settled; nested dispatch is closed.`));
|
|
1270
|
+
}
|
|
1271
|
+
const nested = inheritedDispatch(name, nestedInput, {
|
|
1272
|
+
...options,
|
|
1273
|
+
signal: options?.signal
|
|
1274
|
+
? AbortSignal.any([controller.signal, options.signal])
|
|
1275
|
+
: controller.signal,
|
|
1276
|
+
});
|
|
1277
|
+
nestedDispatches.add(nested);
|
|
1278
|
+
// `finally()` creates a second promise. Observe that promise too, or a
|
|
1279
|
+
// rejected nested call which its owner intentionally awaits later would
|
|
1280
|
+
// also create an unhandled cleanup rejection here.
|
|
1281
|
+
void nested.finally(() => nestedDispatches.delete(nested)).catch(() => { });
|
|
1282
|
+
return nested;
|
|
1283
|
+
}
|
|
1284
|
+
: undefined;
|
|
1285
|
+
if (controller.signal.aborted) {
|
|
1286
|
+
return {
|
|
1287
|
+
success: false,
|
|
1288
|
+
output: '',
|
|
1289
|
+
error: abortReasonText(controller.signal.reason)
|
|
1290
|
+
? `Tool "${toolName}" was cancelled: ${abortReasonText(controller.signal.reason)}`
|
|
1291
|
+
: `Tool "${toolName}" was cancelled.`,
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
const context = {
|
|
914
1295
|
...toolContext,
|
|
915
1296
|
abortSignal: controller.signal,
|
|
916
|
-
|
|
1297
|
+
...(scopedDispatch ? { dispatchTool: scopedDispatch } : {}),
|
|
1298
|
+
};
|
|
1299
|
+
const execution = prepared
|
|
1300
|
+
? this.config.tools.executePrepared(prepared, context)
|
|
1301
|
+
: this.config.tools.execute(toolName, input, context);
|
|
917
1302
|
// The loser of the race may still reject later; neutralize it so
|
|
918
1303
|
// it is never an unhandled rejection.
|
|
919
1304
|
execution.catch(() => { });
|
|
920
|
-
const outcome = await Promise.race([execution, expired, aborted]);
|
|
1305
|
+
const outcome = await Promise.race(expired ? [execution, expired, aborted] : [execution, aborted]);
|
|
921
1306
|
if (outcome === 'timeout') {
|
|
922
1307
|
this.log.warn('Tool timed out', {
|
|
923
1308
|
[NAMZU.RUN_ID]: this.config.runId,
|
|
@@ -949,30 +1334,284 @@ export class ToolExecutor {
|
|
|
949
1334
|
return outcome;
|
|
950
1335
|
}
|
|
951
1336
|
finally {
|
|
1337
|
+
// Revoke synchronously before awaiting anything. A retained closure
|
|
1338
|
+
// called from another microtask is refused here; the aborted signal is
|
|
1339
|
+
// the structural backstop inside dispatchNested itself.
|
|
1340
|
+
invocationOpen = false;
|
|
1341
|
+
if (!controller.signal.aborted) {
|
|
1342
|
+
controller.abort(new Error(`Tool "${toolName}" invocation has settled.`));
|
|
1343
|
+
}
|
|
1344
|
+
// Calls already admitted before closure own event rows and registry
|
|
1345
|
+
// work. Their executor races observe the abort, emit a terminal result,
|
|
1346
|
+
// and settle before the parent is allowed to report completion.
|
|
1347
|
+
while (nestedDispatches.size > 0) {
|
|
1348
|
+
await Promise.allSettled([...nestedDispatches]);
|
|
1349
|
+
}
|
|
952
1350
|
if (timer !== undefined)
|
|
953
1351
|
clearTimeout(timer);
|
|
954
|
-
|
|
1352
|
+
parentSignal.removeEventListener('abort', onParentAbort);
|
|
955
1353
|
}
|
|
956
1354
|
}
|
|
957
|
-
async runPreToolHook(toolName, input) {
|
|
1355
|
+
async runPreToolHook(toolName, input, signal = this.config.abortSignal) {
|
|
958
1356
|
if (!this.config.pluginManager)
|
|
959
|
-
return { kind: 'continue', input };
|
|
1357
|
+
return { kind: 'continue', input, modified: false };
|
|
960
1358
|
const results = await this.config.pluginManager.executeHooks('pre_tool_use', {
|
|
961
1359
|
runId: this.config.runId,
|
|
962
1360
|
toolName,
|
|
963
1361
|
toolInput: input,
|
|
964
|
-
signal
|
|
1362
|
+
signal,
|
|
965
1363
|
}, this.emitEvent);
|
|
966
1364
|
return this.interpretPreToolResults(toolName, input, results);
|
|
967
1365
|
}
|
|
1366
|
+
async prepareNestedCall(toolName, input, signal) {
|
|
1367
|
+
const prepare = this.config.tools.prepareExecution;
|
|
1368
|
+
const executePrepared = this.config.tools.executePrepared;
|
|
1369
|
+
if (typeof prepare !== 'function' || typeof executePrepared !== 'function') {
|
|
1370
|
+
if (this.config.authorizationGate) {
|
|
1371
|
+
return {
|
|
1372
|
+
kind: 'synthetic',
|
|
1373
|
+
input,
|
|
1374
|
+
message: `Tool "${toolName}" was not executed because its registry cannot bind authorization to one prepared input.`,
|
|
1375
|
+
isError: true,
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
const preOutcome = await this.runPreToolHook(toolName, input, signal);
|
|
1379
|
+
if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
|
|
1380
|
+
return {
|
|
1381
|
+
kind: 'synthetic',
|
|
1382
|
+
input: preOutcome.input,
|
|
1383
|
+
message: preOutcome.output,
|
|
1384
|
+
isError: preOutcome.kind === 'error',
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
return { kind: 'legacy', input: preOutcome.input };
|
|
1388
|
+
}
|
|
1389
|
+
let preparation;
|
|
1390
|
+
try {
|
|
1391
|
+
preparation = prepare.call(this.config.tools, toolName, input);
|
|
1392
|
+
}
|
|
1393
|
+
catch (err) {
|
|
1394
|
+
return {
|
|
1395
|
+
kind: 'synthetic',
|
|
1396
|
+
input,
|
|
1397
|
+
message: `Tool "${toolName}" could not be prepared: ${toErrorMessage(err)}`,
|
|
1398
|
+
isError: true,
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
if (!preparation.success) {
|
|
1402
|
+
return {
|
|
1403
|
+
kind: 'synthetic',
|
|
1404
|
+
input,
|
|
1405
|
+
message: formatFailedToolOutput(preparation.result.output, preparation.result.error),
|
|
1406
|
+
isError: true,
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
const preOutcome = await this.runPreToolHook(toolName, preparation.prepared.input, signal);
|
|
1410
|
+
if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
|
|
1411
|
+
return {
|
|
1412
|
+
kind: 'synthetic',
|
|
1413
|
+
input: preOutcome.input,
|
|
1414
|
+
message: preOutcome.output,
|
|
1415
|
+
isError: preOutcome.kind === 'error',
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
if (!preOutcome.modified) {
|
|
1419
|
+
return {
|
|
1420
|
+
kind: 'ready',
|
|
1421
|
+
input: preparation.prepared.input,
|
|
1422
|
+
prepared: preparation.prepared,
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
const modified = prepare.call(this.config.tools, toolName, preOutcome.input);
|
|
1426
|
+
if (!modified.success) {
|
|
1427
|
+
return {
|
|
1428
|
+
kind: 'synthetic',
|
|
1429
|
+
input: preOutcome.input,
|
|
1430
|
+
message: formatFailedToolOutput(modified.result.output, modified.result.error),
|
|
1431
|
+
isError: true,
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
return { kind: 'ready', input: modified.prepared.input, prepared: modified.prepared };
|
|
1435
|
+
}
|
|
1436
|
+
async prepareDirectCall(toolCall) {
|
|
1437
|
+
let toolName = toolCall.function.name;
|
|
1438
|
+
const truncationRepair = toolCall.metadata?.inputTruncated === true
|
|
1439
|
+
? await this.repairTruncatedCall(toolCall, toolName)
|
|
1440
|
+
: null;
|
|
1441
|
+
if (toolCall.metadata?.inputTruncated === true && !truncationRepair) {
|
|
1442
|
+
return {
|
|
1443
|
+
kind: 'synthetic',
|
|
1444
|
+
toolCall,
|
|
1445
|
+
toolName,
|
|
1446
|
+
input: {},
|
|
1447
|
+
message: truncatedToolInputMessage(toolName),
|
|
1448
|
+
isError: true,
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
const prepare = this.config.tools.prepareExecution;
|
|
1452
|
+
const executePrepared = this.config.tools.executePrepared;
|
|
1453
|
+
if (typeof prepare !== 'function' || typeof executePrepared !== 'function') {
|
|
1454
|
+
const resolved = await this.resolveCall(truncationRepair
|
|
1455
|
+
? {
|
|
1456
|
+
...toolCall,
|
|
1457
|
+
function: {
|
|
1458
|
+
...toolCall.function,
|
|
1459
|
+
name: truncationRepair.toolName ?? toolName,
|
|
1460
|
+
arguments: truncationRepair.arguments,
|
|
1461
|
+
},
|
|
1462
|
+
metadata: {},
|
|
1463
|
+
}
|
|
1464
|
+
: toolCall);
|
|
1465
|
+
toolName = resolved.toolName;
|
|
1466
|
+
if (!resolved.ok) {
|
|
1467
|
+
return {
|
|
1468
|
+
kind: 'synthetic',
|
|
1469
|
+
toolCall,
|
|
1470
|
+
toolName,
|
|
1471
|
+
input: {},
|
|
1472
|
+
message: resolved.message,
|
|
1473
|
+
isError: true,
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
const preOutcome = await this.runPreToolHook(toolName, resolved.input);
|
|
1477
|
+
if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
|
|
1478
|
+
return {
|
|
1479
|
+
kind: 'synthetic',
|
|
1480
|
+
toolCall,
|
|
1481
|
+
toolName,
|
|
1482
|
+
input: preOutcome.input,
|
|
1483
|
+
message: preOutcome.output,
|
|
1484
|
+
isError: preOutcome.kind === 'error',
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
if (!this.config.authorizationGate) {
|
|
1488
|
+
return {
|
|
1489
|
+
kind: 'legacy',
|
|
1490
|
+
toolCall,
|
|
1491
|
+
toolName,
|
|
1492
|
+
input: preOutcome.input,
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
return {
|
|
1496
|
+
kind: 'synthetic',
|
|
1497
|
+
toolCall,
|
|
1498
|
+
toolName,
|
|
1499
|
+
input: preOutcome.input,
|
|
1500
|
+
message: `Tool "${toolName}" was not executed because its registry cannot bind authorization to one prepared input.`,
|
|
1501
|
+
isError: true,
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
let raw = truncationRepair?.arguments ?? toolCall.function.arguments;
|
|
1505
|
+
toolName = truncationRepair?.toolName ?? toolName;
|
|
1506
|
+
let repairUsed = truncationRepair !== null;
|
|
1507
|
+
let preparation;
|
|
1508
|
+
for (;;) {
|
|
1509
|
+
let parsed;
|
|
1510
|
+
try {
|
|
1511
|
+
parsed = parseArguments(raw);
|
|
1512
|
+
}
|
|
1513
|
+
catch {
|
|
1514
|
+
const message = `Error: Invalid JSON in tool arguments for "${toolName}"`;
|
|
1515
|
+
const repair = !repairUsed && this.config.repairToolCall
|
|
1516
|
+
? await this.requestRepair(toolCall, toolName, {
|
|
1517
|
+
reason: 'invalid_json',
|
|
1518
|
+
message,
|
|
1519
|
+
})
|
|
1520
|
+
: null;
|
|
1521
|
+
if (repair) {
|
|
1522
|
+
repairUsed = true;
|
|
1523
|
+
toolName = repair.toolName ?? toolName;
|
|
1524
|
+
raw = repair.arguments;
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
return { kind: 'synthetic', toolCall, toolName, input: {}, message, isError: true };
|
|
1528
|
+
}
|
|
1529
|
+
try {
|
|
1530
|
+
preparation = prepare.call(this.config.tools, toolName, parsed);
|
|
1531
|
+
}
|
|
1532
|
+
catch (err) {
|
|
1533
|
+
const message = `Error: Unknown or unavailable tool "${toolName}": ${toErrorMessage(err)}`;
|
|
1534
|
+
const repair = !repairUsed && this.config.repairToolCall
|
|
1535
|
+
? await this.requestRepair(toolCall, toolName, {
|
|
1536
|
+
reason: 'unknown_tool',
|
|
1537
|
+
message,
|
|
1538
|
+
})
|
|
1539
|
+
: null;
|
|
1540
|
+
if (repair) {
|
|
1541
|
+
repairUsed = true;
|
|
1542
|
+
toolName = repair.toolName ?? toolName;
|
|
1543
|
+
raw = repair.arguments;
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1546
|
+
return { kind: 'synthetic', toolCall, toolName, input: parsed, message, isError: true };
|
|
1547
|
+
}
|
|
1548
|
+
if (preparation.success)
|
|
1549
|
+
break;
|
|
1550
|
+
const message = formatFailedToolOutput(preparation.result.output, preparation.result.error);
|
|
1551
|
+
const repair = !repairUsed && this.config.repairToolCall
|
|
1552
|
+
? await this.requestRepair(toolCall, toolName, {
|
|
1553
|
+
reason: 'schema_validation',
|
|
1554
|
+
message,
|
|
1555
|
+
})
|
|
1556
|
+
: null;
|
|
1557
|
+
if (repair) {
|
|
1558
|
+
repairUsed = true;
|
|
1559
|
+
toolName = repair.toolName ?? toolName;
|
|
1560
|
+
raw = repair.arguments;
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
return {
|
|
1564
|
+
kind: 'synthetic',
|
|
1565
|
+
toolCall,
|
|
1566
|
+
toolName,
|
|
1567
|
+
input: parsed,
|
|
1568
|
+
message,
|
|
1569
|
+
isError: true,
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
const preOutcome = await this.runPreToolHook(toolName, preparation.prepared.input);
|
|
1573
|
+
if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
|
|
1574
|
+
return {
|
|
1575
|
+
kind: 'synthetic',
|
|
1576
|
+
toolCall,
|
|
1577
|
+
toolName,
|
|
1578
|
+
input: preOutcome.input,
|
|
1579
|
+
message: preOutcome.output,
|
|
1580
|
+
isError: preOutcome.kind === 'error',
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
if (preOutcome.modified) {
|
|
1584
|
+
const modified = prepare.call(this.config.tools, toolName, preOutcome.input);
|
|
1585
|
+
if (!modified.success) {
|
|
1586
|
+
return {
|
|
1587
|
+
kind: 'synthetic',
|
|
1588
|
+
toolCall,
|
|
1589
|
+
toolName,
|
|
1590
|
+
input: preOutcome.input,
|
|
1591
|
+
message: formatFailedToolOutput(modified.result.output, modified.result.error),
|
|
1592
|
+
isError: true,
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
preparation = modified;
|
|
1596
|
+
}
|
|
1597
|
+
return {
|
|
1598
|
+
kind: 'ready',
|
|
1599
|
+
toolCall,
|
|
1600
|
+
toolName,
|
|
1601
|
+
input: preparation.prepared.input,
|
|
1602
|
+
prepared: preparation.prepared,
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
968
1605
|
interpretPreToolResults(toolName, initialInput, results) {
|
|
969
1606
|
let currentInput = initialInput;
|
|
1607
|
+
let modified = false;
|
|
970
1608
|
for (const result of results) {
|
|
971
1609
|
switch (result.action) {
|
|
972
1610
|
case 'continue':
|
|
973
1611
|
continue;
|
|
974
1612
|
case 'modify':
|
|
975
1613
|
currentInput = result.input;
|
|
1614
|
+
modified = true;
|
|
976
1615
|
continue;
|
|
977
1616
|
case 'skip':
|
|
978
1617
|
return {
|
|
@@ -999,7 +1638,7 @@ export class ToolExecutor {
|
|
|
999
1638
|
}
|
|
1000
1639
|
}
|
|
1001
1640
|
}
|
|
1002
|
-
return { kind: 'continue', input: currentInput };
|
|
1641
|
+
return { kind: 'continue', input: currentInput, modified };
|
|
1003
1642
|
}
|
|
1004
1643
|
/**
|
|
1005
1644
|
* Turn the call the model issued into a name and a parsed input, giving
|
|
@@ -1114,7 +1753,10 @@ export class ToolExecutor {
|
|
|
1114
1753
|
reason: failure.reason,
|
|
1115
1754
|
message: failure.message,
|
|
1116
1755
|
...(tool
|
|
1117
|
-
? {
|
|
1756
|
+
? {
|
|
1757
|
+
tool,
|
|
1758
|
+
jsonSchema: tool.modelInputSchema ?? renderToolSchema(tool.inputSchema),
|
|
1759
|
+
}
|
|
1118
1760
|
: {}),
|
|
1119
1761
|
availableTools: this.config.tools.listNames(),
|
|
1120
1762
|
});
|
|
@@ -1143,9 +1785,9 @@ export class ToolExecutor {
|
|
|
1143
1785
|
* image block had it discarded here — before the wire mapper built to
|
|
1144
1786
|
* carry it ever saw it.
|
|
1145
1787
|
*/
|
|
1146
|
-
async runOnce(toolName, input, toolContext) {
|
|
1788
|
+
async runOnce(toolName, input, toolContext, prepared) {
|
|
1147
1789
|
try {
|
|
1148
|
-
return await this.executeWithDeadline(toolName, input, toolContext);
|
|
1790
|
+
return await this.executeWithDeadline(toolName, input, toolContext, prepared);
|
|
1149
1791
|
}
|
|
1150
1792
|
catch (err) {
|
|
1151
1793
|
const message = toErrorMessage(err);
|
|
@@ -1216,14 +1858,16 @@ export class ToolExecutor {
|
|
|
1216
1858
|
* execution so UI cards reach a terminal state instead of hanging in
|
|
1217
1859
|
* `executing`, and records a failed activity for the trace.
|
|
1218
1860
|
*/
|
|
1219
|
-
async recordDenial(toolCall, reason) {
|
|
1220
|
-
const toolName = toolCall.function.name;
|
|
1221
|
-
let input = {};
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1861
|
+
async recordDenial(toolCall, reason, preparedCall) {
|
|
1862
|
+
const toolName = preparedCall?.toolName ?? toolCall.function.name;
|
|
1863
|
+
let input = preparedCall?.input ?? {};
|
|
1864
|
+
if (!preparedCall) {
|
|
1865
|
+
try {
|
|
1866
|
+
input = JSON.parse(toolCall.function.arguments || '{}');
|
|
1867
|
+
}
|
|
1868
|
+
catch {
|
|
1869
|
+
input = toolCall.function.arguments;
|
|
1870
|
+
}
|
|
1227
1871
|
}
|
|
1228
1872
|
const output = deniedToolOutput(toolName, reason);
|
|
1229
1873
|
this.log.info('Tool call denied — synthesizing tool_result', {
|
|
@@ -1299,6 +1943,12 @@ export class ToolExecutor {
|
|
|
1299
1943
|
isError: outcome.kind === 'error',
|
|
1300
1944
|
};
|
|
1301
1945
|
}
|
|
1946
|
+
recordSyntheticPreparation(call) {
|
|
1947
|
+
return this.recordSyntheticHookOutcome(call.toolCall.id, call.toolName, call.input, {
|
|
1948
|
+
kind: call.isError ? 'error' : 'skip',
|
|
1949
|
+
output: call.message,
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1302
1952
|
/**
|
|
1303
1953
|
* Bound the rich channel, or leave it alone when no cap is configured.
|
|
1304
1954
|
*
|