@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Span } from '@opentelemetry/api'
|
|
2
|
+
import type { AuthorizationGate } from '../../authorization/gate.js'
|
|
2
3
|
import { extractFromToolCall, extractFromToolResult } from '../../compaction/extractor.js'
|
|
3
4
|
import type { WorkingStateManager } from '../../compaction/manager.js'
|
|
4
5
|
import { GENAI, NAMZU } from '../../constants/telemetry/index.js'
|
|
@@ -22,13 +23,16 @@ import {
|
|
|
22
23
|
import type { PermissionMode } from '../../types/permission/index.js'
|
|
23
24
|
import type { PluginHookResult } from '../../types/plugin/index.js'
|
|
24
25
|
import type { ChatCompletionResponse } from '../../types/provider/index.js'
|
|
26
|
+
import type { AuditEventInput } from '../../types/run/audit.js'
|
|
25
27
|
import type { RunEvent } from '../../types/run/index.js'
|
|
26
28
|
import type { Sandbox } from '../../types/sandbox/index.js'
|
|
27
29
|
import type {
|
|
28
30
|
FileReadTracker,
|
|
31
|
+
PreparedToolExecution,
|
|
29
32
|
RequestToolPause,
|
|
30
33
|
SkillRegistryRef,
|
|
31
34
|
ToolContext,
|
|
35
|
+
ToolDispatchOptions,
|
|
32
36
|
ToolRegistryContract,
|
|
33
37
|
ToolResult,
|
|
34
38
|
} from '../../types/tool/index.js'
|
|
@@ -54,6 +58,74 @@ import {
|
|
|
54
58
|
|
|
55
59
|
export type EmitEvent = (event: RunEvent) => Promise<void>
|
|
56
60
|
|
|
61
|
+
type PreparedDirectCall =
|
|
62
|
+
| {
|
|
63
|
+
readonly kind: 'ready'
|
|
64
|
+
readonly toolCall: ToolCall
|
|
65
|
+
readonly toolName: string
|
|
66
|
+
readonly input: unknown
|
|
67
|
+
readonly prepared: PreparedToolExecution
|
|
68
|
+
}
|
|
69
|
+
| {
|
|
70
|
+
readonly kind: 'legacy'
|
|
71
|
+
readonly toolCall: ToolCall
|
|
72
|
+
readonly toolName: string
|
|
73
|
+
readonly input: unknown
|
|
74
|
+
}
|
|
75
|
+
| {
|
|
76
|
+
readonly kind: 'synthetic'
|
|
77
|
+
readonly toolCall: ToolCall
|
|
78
|
+
readonly toolName: string
|
|
79
|
+
readonly input: unknown
|
|
80
|
+
readonly message: string
|
|
81
|
+
readonly isError: boolean
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Executor-owned, single-use preparation of one provider tool-call batch.
|
|
86
|
+
*
|
|
87
|
+
* Consumers may inspect `reviewCalls`; only the creating executor can consume
|
|
88
|
+
* the opaque call preparations. This keeps schema transforms, plugin rewrites,
|
|
89
|
+
* authorization and execution on one value instead of reparsing between them.
|
|
90
|
+
*/
|
|
91
|
+
export interface PreparedToolBatch {
|
|
92
|
+
readonly reviewCalls: readonly {
|
|
93
|
+
readonly id: string
|
|
94
|
+
readonly name: string
|
|
95
|
+
readonly input: unknown
|
|
96
|
+
}[]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface OwnedPreparedToolBatch extends PreparedToolBatch {
|
|
100
|
+
readonly calls: ReadonlyMap<string, PreparedDirectCall>
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function assertUniqueToolCallIds(toolCalls: readonly ToolCall[]): void {
|
|
104
|
+
const seen = new Set<string>()
|
|
105
|
+
for (const [index, toolCall] of toolCalls.entries()) {
|
|
106
|
+
if (seen.has(toolCall.id)) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`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.`,
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
seen.add(toolCall.id)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
type PreparedNestedCall =
|
|
116
|
+
| {
|
|
117
|
+
readonly kind: 'ready'
|
|
118
|
+
readonly input: unknown
|
|
119
|
+
readonly prepared: PreparedToolExecution
|
|
120
|
+
}
|
|
121
|
+
| { readonly kind: 'legacy'; readonly input: unknown }
|
|
122
|
+
| {
|
|
123
|
+
readonly kind: 'synthetic'
|
|
124
|
+
readonly input: unknown
|
|
125
|
+
readonly message: string
|
|
126
|
+
readonly isError: boolean
|
|
127
|
+
}
|
|
128
|
+
|
|
57
129
|
/**
|
|
58
130
|
* Default per-tool deadline. Long enough for a real build or test run,
|
|
59
131
|
* short enough that a wedged tool does not hold a turn open indefinitely.
|
|
@@ -71,6 +143,103 @@ export const DEFAULT_TOOL_TIMEOUT_MS = 120_000
|
|
|
71
143
|
*/
|
|
72
144
|
export const DEFAULT_TOOL_CONCURRENCY = 8
|
|
73
145
|
|
|
146
|
+
/** Maximum UTF-8 size of one ephemeral tool-progress update. */
|
|
147
|
+
const MAX_TOOL_PROGRESS_BYTES = 8 * 1024
|
|
148
|
+
|
|
149
|
+
/** A visible marker: this event is a display projection, not durable output. */
|
|
150
|
+
const TOOL_PROGRESS_OMISSION = '… '
|
|
151
|
+
|
|
152
|
+
function boundedToolProgress(message: string): string {
|
|
153
|
+
if (Buffer.byteLength(message, 'utf8') <= MAX_TOOL_PROGRESS_BYTES) return message
|
|
154
|
+
|
|
155
|
+
const tailBudget = MAX_TOOL_PROGRESS_BYTES - Buffer.byteLength(TOOL_PROGRESS_OMISSION, 'utf8')
|
|
156
|
+
let low = 0
|
|
157
|
+
let high = message.length
|
|
158
|
+
while (low < high) {
|
|
159
|
+
const middle = Math.floor((low + high) / 2)
|
|
160
|
+
if (Buffer.byteLength(message.slice(middle), 'utf8') > tailBudget) low = middle + 1
|
|
161
|
+
else high = middle
|
|
162
|
+
}
|
|
163
|
+
// Do not turn the low half of a retained surrogate pair into U+FFFD.
|
|
164
|
+
if (
|
|
165
|
+
low > 0 &&
|
|
166
|
+
low < message.length &&
|
|
167
|
+
message.charCodeAt(low) >= 0xdc00 &&
|
|
168
|
+
message.charCodeAt(low) <= 0xdfff &&
|
|
169
|
+
message.charCodeAt(low - 1) >= 0xd800 &&
|
|
170
|
+
message.charCodeAt(low - 1) <= 0xdbff
|
|
171
|
+
) {
|
|
172
|
+
low += 1
|
|
173
|
+
}
|
|
174
|
+
return `${TOOL_PROGRESS_OMISSION}${message.slice(low)}`
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Latest-state publisher for one tool call.
|
|
179
|
+
*
|
|
180
|
+
* `ToolContext.report()` is synchronous by contract, while host event
|
|
181
|
+
* listeners may be arbitrarily slow. Starting one promise per report makes a
|
|
182
|
+
* chatty tool an unbounded allocation source. Progress is state rather than a
|
|
183
|
+
* transcript, so one in-flight update plus the latest pending update is the
|
|
184
|
+
* complete useful working set; intermediate states may be replaced.
|
|
185
|
+
*/
|
|
186
|
+
class ToolProgressPublisher {
|
|
187
|
+
private pending: { readonly message: string; readonly fraction?: number } | undefined
|
|
188
|
+
private draining: Promise<void> | null = null
|
|
189
|
+
private accepting = true
|
|
190
|
+
|
|
191
|
+
constructor(
|
|
192
|
+
private readonly emitEvent: EmitEvent,
|
|
193
|
+
private readonly base: Omit<
|
|
194
|
+
Extract<RunEvent, { type: 'tool_progress' }>,
|
|
195
|
+
'message' | 'fraction'
|
|
196
|
+
>,
|
|
197
|
+
) {}
|
|
198
|
+
|
|
199
|
+
report(message: string, fraction?: number): void {
|
|
200
|
+
if (!this.accepting) return
|
|
201
|
+
this.pending = {
|
|
202
|
+
message,
|
|
203
|
+
...(fraction !== undefined ? { fraction: Math.min(1, Math.max(0, fraction)) } : {}),
|
|
204
|
+
}
|
|
205
|
+
this.startDrain()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async close(): Promise<void> {
|
|
209
|
+
this.accepting = false
|
|
210
|
+
this.startDrain()
|
|
211
|
+
while (this.draining) await this.draining
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private startDrain(): void {
|
|
215
|
+
if (this.draining || !this.pending) return
|
|
216
|
+
const current = this.drain()
|
|
217
|
+
this.draining = current
|
|
218
|
+
void current.finally(() => {
|
|
219
|
+
if (this.draining !== current) return
|
|
220
|
+
this.draining = null
|
|
221
|
+
// A report can land after the final loop check but before this
|
|
222
|
+
// settlement callback. It is still an accepted update and must be
|
|
223
|
+
// drained even when close() has since stopped new reports.
|
|
224
|
+
this.startDrain()
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private async drain(): Promise<void> {
|
|
229
|
+
while (this.pending) {
|
|
230
|
+
const update = this.pending
|
|
231
|
+
this.pending = undefined
|
|
232
|
+
// A progress observer is diagnostic. Its failure cannot become a tool
|
|
233
|
+
// failure, and report() never hands a rejection back to the tool.
|
|
234
|
+
await this.emitEvent({
|
|
235
|
+
...this.base,
|
|
236
|
+
message: boundedToolProgress(update.message),
|
|
237
|
+
...(update.fraction !== undefined ? { fraction: update.fraction } : {}),
|
|
238
|
+
}).catch(() => {})
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
74
243
|
/**
|
|
75
244
|
* Re-runs granted to a `post_tool_use` hook that returns `{action:'retry'}`
|
|
76
245
|
* on a tool which did not opt into {@link ToolDefinition.maxRetries}.
|
|
@@ -221,6 +390,17 @@ export interface ToolExecutorConfig {
|
|
|
221
390
|
* reaches it. See {@link RepairToolCall}.
|
|
222
391
|
*/
|
|
223
392
|
repairToolCall?: RepairToolCall
|
|
393
|
+
/**
|
|
394
|
+
* Operator policy applied to calls dispatched by another tool.
|
|
395
|
+
*
|
|
396
|
+
* Model-issued calls are reviewed by the iteration orchestrator. Nested
|
|
397
|
+
* calls cannot open a second durable review while their parent is already
|
|
398
|
+
* executing, so only an explicit `allow` may proceed; `deny` and `review`
|
|
399
|
+
* both fail closed before the registry is touched.
|
|
400
|
+
*/
|
|
401
|
+
authorizationGate?: AuthorizationGate
|
|
402
|
+
/** Durable refusal sink paired with {@link authorizationGate}. */
|
|
403
|
+
recordAudit?: (input: AuditEventInput) => Promise<unknown>
|
|
224
404
|
}
|
|
225
405
|
|
|
226
406
|
/**
|
|
@@ -238,7 +418,7 @@ interface PostToolOverride {
|
|
|
238
418
|
}
|
|
239
419
|
|
|
240
420
|
type PreToolHookOutcome =
|
|
241
|
-
| { kind: 'continue'; input: unknown }
|
|
421
|
+
| { kind: 'continue'; input: unknown; modified: boolean }
|
|
242
422
|
| { kind: 'skip'; input: unknown; output: string }
|
|
243
423
|
| { kind: 'error'; input: unknown; output: string }
|
|
244
424
|
|
|
@@ -301,6 +481,7 @@ export class ToolExecutor {
|
|
|
301
481
|
private workingStateManager?: WorkingStateManager
|
|
302
482
|
private probes: ProbeEnforcement
|
|
303
483
|
private parentSpan?: Span
|
|
484
|
+
private readonly preparedBatches = new WeakSet<PreparedToolBatch>()
|
|
304
485
|
/** Set per turn by the orchestrator; see {@link setStepAllowedTools}. */
|
|
305
486
|
private stepAllowedTools?: readonly string[]
|
|
306
487
|
private readonly readPaths: Set<string> = new Set()
|
|
@@ -455,15 +636,78 @@ export class ToolExecutor {
|
|
|
455
636
|
return typeof configured === 'function' ? configured() : configured
|
|
456
637
|
}
|
|
457
638
|
|
|
639
|
+
/** Evaluate the run's operator policy against one already-prepared value. */
|
|
640
|
+
evaluatePreparedAuthorization(toolName: string, input: unknown) {
|
|
641
|
+
return this.config.authorizationGate?.evaluate({
|
|
642
|
+
toolName,
|
|
643
|
+
toolInput: input,
|
|
644
|
+
toolDef: this.config.tools.get(toolName),
|
|
645
|
+
})
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Resolve repairs and pre-tool hooks, then decode each call exactly once.
|
|
650
|
+
* The returned projection is what policy and a human review; execution later
|
|
651
|
+
* consumes the registry-owned preparations rather than parsing again.
|
|
652
|
+
*/
|
|
653
|
+
async prepareBatchForReview(response: ChatCompletionResponse): Promise<PreparedToolBatch> {
|
|
654
|
+
assertUniqueToolCallIds(response.message.toolCalls ?? [])
|
|
655
|
+
const calls = new Map<string, PreparedDirectCall>()
|
|
656
|
+
for (const toolCall of response.message.toolCalls ?? []) {
|
|
657
|
+
calls.set(toolCall.id, await this.prepareDirectCall(toolCall))
|
|
658
|
+
}
|
|
659
|
+
return this.publishPreparedBatch(calls)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** Re-prepare only calls whose raw input a reviewer actually changed. */
|
|
663
|
+
async reprepareBatchForReview(
|
|
664
|
+
response: ChatCompletionResponse,
|
|
665
|
+
previous: PreparedToolBatch,
|
|
666
|
+
changedCallIds: ReadonlySet<string>,
|
|
667
|
+
): Promise<PreparedToolBatch> {
|
|
668
|
+
assertUniqueToolCallIds(response.message.toolCalls ?? [])
|
|
669
|
+
if (!this.preparedBatches.has(previous)) {
|
|
670
|
+
throw new Error('Prepared tool batch is not owned by this executor.')
|
|
671
|
+
}
|
|
672
|
+
const calls = new Map((previous as OwnedPreparedToolBatch).calls)
|
|
673
|
+
for (const toolCall of response.message.toolCalls ?? []) {
|
|
674
|
+
if (changedCallIds.has(toolCall.id)) {
|
|
675
|
+
calls.set(toolCall.id, await this.prepareDirectCall(toolCall))
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return this.publishPreparedBatch(calls)
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
private publishPreparedBatch(calls: ReadonlyMap<string, PreparedDirectCall>): PreparedToolBatch {
|
|
682
|
+
const reviewCalls = [...calls.values()]
|
|
683
|
+
.filter(
|
|
684
|
+
(call): call is Exclude<PreparedDirectCall, { kind: 'synthetic' }> =>
|
|
685
|
+
call.kind !== 'synthetic',
|
|
686
|
+
)
|
|
687
|
+
.map((call) => ({
|
|
688
|
+
id: call.toolCall.id,
|
|
689
|
+
name: call.toolName,
|
|
690
|
+
input: call.input,
|
|
691
|
+
}))
|
|
692
|
+
const batch: OwnedPreparedToolBatch = Object.freeze({
|
|
693
|
+
reviewCalls: Object.freeze(reviewCalls),
|
|
694
|
+
calls: new Map(calls),
|
|
695
|
+
})
|
|
696
|
+
this.preparedBatches.add(batch)
|
|
697
|
+
return batch
|
|
698
|
+
}
|
|
699
|
+
|
|
458
700
|
async executeBatch(
|
|
459
701
|
response: ChatCompletionResponse,
|
|
460
702
|
denials?: ToolCallDenials,
|
|
461
703
|
prior?: PriorToolResults,
|
|
704
|
+
preparedBatch?: PreparedToolBatch,
|
|
462
705
|
): Promise<ToolExecutionBatch> {
|
|
463
706
|
const toolCalls = response.message.toolCalls
|
|
464
707
|
if (!toolCalls) {
|
|
465
708
|
return { messages: [], results: [], observations: [] }
|
|
466
709
|
}
|
|
710
|
+
assertUniqueToolCallIds(toolCalls)
|
|
467
711
|
|
|
468
712
|
this.batchCounter += 1
|
|
469
713
|
|
|
@@ -471,7 +715,11 @@ export class ToolExecutor {
|
|
|
471
715
|
// `permissionMode` in the config type.
|
|
472
716
|
this.batchMode = this.resolvePermissionMode()
|
|
473
717
|
try {
|
|
474
|
-
|
|
718
|
+
const owned = preparedBatch as OwnedPreparedToolBatch | undefined
|
|
719
|
+
if (owned && !this.preparedBatches.has(owned)) {
|
|
720
|
+
throw new Error('Prepared tool batch is not owned by this executor.')
|
|
721
|
+
}
|
|
722
|
+
return await this.runBatch(toolCalls, denials, prior, owned)
|
|
475
723
|
} finally {
|
|
476
724
|
// Cleared so a later single execution outside a batch resolves
|
|
477
725
|
// live rather than inheriting the last batch's sample.
|
|
@@ -483,6 +731,7 @@ export class ToolExecutor {
|
|
|
483
731
|
toolCalls: readonly ToolCall[],
|
|
484
732
|
denials?: ToolCallDenials,
|
|
485
733
|
prior?: PriorToolResults,
|
|
734
|
+
preparedBatch?: OwnedPreparedToolBatch,
|
|
486
735
|
): Promise<ToolExecutionBatch> {
|
|
487
736
|
this.log.debug('Executing tool batch', {
|
|
488
737
|
[NAMZU.RUN_ID]: this.config.runId,
|
|
@@ -517,6 +766,7 @@ export class ToolExecutor {
|
|
|
517
766
|
// order independence.
|
|
518
767
|
const gate = new Semaphore(this.config.maxToolConcurrency ?? DEFAULT_TOOL_CONCURRENCY)
|
|
519
768
|
toolCalls.forEach((toolCall, i) => {
|
|
769
|
+
const preparedCall = preparedBatch?.calls.get(toolCall.id)
|
|
520
770
|
const recovered = prior?.get(toolCall.id)
|
|
521
771
|
if (recovered !== undefined) {
|
|
522
772
|
// This call already ran, in a process that died before the
|
|
@@ -539,7 +789,7 @@ export class ToolExecutor {
|
|
|
539
789
|
// on the parallel branch — they perform no side effects, so
|
|
540
790
|
// serialization would only add latency.
|
|
541
791
|
parallel.push(
|
|
542
|
-
this.recordDenial(toolCall, denialReason).then((r) => {
|
|
792
|
+
this.recordDenial(toolCall, denialReason, preparedCall).then((r) => {
|
|
543
793
|
results[i] = r
|
|
544
794
|
}),
|
|
545
795
|
)
|
|
@@ -548,33 +798,39 @@ export class ToolExecutor {
|
|
|
548
798
|
// Per-call, because the event has to name which call it is about:
|
|
549
799
|
// a batch can run several tools at once and a host rendering them
|
|
550
800
|
// side by side needs to know whose progress this is.
|
|
801
|
+
const progress = new ToolProgressPublisher(this.emitEvent, {
|
|
802
|
+
type: 'tool_progress',
|
|
803
|
+
runId: this.config.runId,
|
|
804
|
+
toolUseId: toolCall.id as ToolUseId,
|
|
805
|
+
toolName: toolCall.function.name,
|
|
806
|
+
})
|
|
551
807
|
const ctx: ToolContext = {
|
|
552
808
|
...baseContext,
|
|
553
809
|
toolUseId: toolCall.id,
|
|
810
|
+
source: { kind: 'direct' },
|
|
554
811
|
// Overridden per call, so a nested dispatch can name the call
|
|
555
812
|
// that made it. The base context has no `toolUseId`, and a
|
|
556
813
|
// closure built there would report every nested call as
|
|
557
814
|
// parentless.
|
|
558
|
-
dispatchTool: (name, input) =>
|
|
815
|
+
dispatchTool: (name, input, options) =>
|
|
816
|
+
this.dispatchNested(name, input, ctx, recordObservation, toolCall.function.name, options),
|
|
559
817
|
// Per-call for the same reason: a pause has to be routed back
|
|
560
818
|
// to the call that raised it, and a batch can raise several.
|
|
561
819
|
...(this.config.toolPause ? { requestPause: this.config.toolPause(toolCall.id) } : {}),
|
|
562
|
-
report: (message: string, fraction?: number) =>
|
|
563
|
-
// Fire-and-forget: a tool reporting progress must never be
|
|
564
|
-
// able to fail because the host's listener threw, and must
|
|
565
|
-
// never have to await the emit mid-work.
|
|
566
|
-
void this.emitEvent({
|
|
567
|
-
type: 'tool_progress',
|
|
568
|
-
runId: this.config.runId,
|
|
569
|
-
toolUseId: toolCall.id as ToolUseId,
|
|
570
|
-
toolName: toolCall.function.name,
|
|
571
|
-
message,
|
|
572
|
-
...(fraction !== undefined ? { fraction: Math.min(1, Math.max(0, fraction)) } : {}),
|
|
573
|
-
}).catch(() => {})
|
|
574
|
-
},
|
|
820
|
+
report: (message: string, fraction?: number) => progress.report(message, fraction),
|
|
575
821
|
}
|
|
576
822
|
const run = async () => {
|
|
577
|
-
|
|
823
|
+
try {
|
|
824
|
+
results[i] = await this.executeSingle(
|
|
825
|
+
toolCall,
|
|
826
|
+
ctx,
|
|
827
|
+
recordObservation,
|
|
828
|
+
() => progress.close(),
|
|
829
|
+
preparedCall,
|
|
830
|
+
)
|
|
831
|
+
} finally {
|
|
832
|
+
await progress.close()
|
|
833
|
+
}
|
|
578
834
|
}
|
|
579
835
|
const gated = async () => {
|
|
580
836
|
await gate.acquire()
|
|
@@ -584,14 +840,19 @@ export class ToolExecutor {
|
|
|
584
840
|
gate.release()
|
|
585
841
|
}
|
|
586
842
|
}
|
|
587
|
-
let input: unknown = {}
|
|
843
|
+
let input: unknown = preparedCall?.input ?? {}
|
|
588
844
|
try {
|
|
589
|
-
|
|
845
|
+
if (!preparedBatch?.calls.has(toolCall.id)) {
|
|
846
|
+
input = JSON.parse(toolCall.function.arguments || '{}')
|
|
847
|
+
}
|
|
590
848
|
} catch {
|
|
591
849
|
// non-JSON args → treat as unsafe (serialize), the conservative path
|
|
592
850
|
}
|
|
851
|
+
const preparedToolName = preparedCall?.toolName
|
|
593
852
|
const safe =
|
|
594
|
-
this.config.tools
|
|
853
|
+
this.config.tools
|
|
854
|
+
.get(preparedToolName ?? toolCall.function.name)
|
|
855
|
+
?.isConcurrencySafe?.(input) === true
|
|
595
856
|
if (safe) parallel.push(gated())
|
|
596
857
|
else serial = serial.then(run)
|
|
597
858
|
})
|
|
@@ -651,47 +912,250 @@ export class ToolExecutor {
|
|
|
651
912
|
input: unknown,
|
|
652
913
|
context: ToolContext,
|
|
653
914
|
recordObservation: (observation: ToolResultObservation) => void,
|
|
915
|
+
parentToolName?: string,
|
|
916
|
+
options?: ToolDispatchOptions,
|
|
654
917
|
): Promise<ToolResult> {
|
|
918
|
+
const signal = options?.signal
|
|
919
|
+
? AbortSignal.any([context.abortSignal, options.signal])
|
|
920
|
+
: context.abortSignal
|
|
921
|
+
// Authority is checked before an id, activity or event is created. A
|
|
922
|
+
// retained closure must be observationally inert after its invocation
|
|
923
|
+
// ends, not merely unable to finish the registry call it already started.
|
|
924
|
+
signal.throwIfAborted()
|
|
925
|
+
const preparedCall = await this.prepareNestedCall(name, input, signal)
|
|
926
|
+
signal.throwIfAborted()
|
|
927
|
+
const preparedInput = preparedCall.input
|
|
928
|
+
|
|
655
929
|
const parent = context.toolUseId
|
|
656
|
-
const via =
|
|
930
|
+
const via =
|
|
931
|
+
parent && parentToolName
|
|
932
|
+
? {
|
|
933
|
+
tool: parentToolName,
|
|
934
|
+
toolUseId: parent as ToolUseId,
|
|
935
|
+
...(options?.runtimeToolCallId ? { runtimeToolCallId: options.runtimeToolCallId } : {}),
|
|
936
|
+
}
|
|
937
|
+
: undefined
|
|
657
938
|
// Its own id, minted here. Reusing the parent's would make two
|
|
658
939
|
// different calls indistinguishable in any log keyed by it, which is
|
|
659
940
|
// exactly how a nested write gets attributed to the program that ran
|
|
660
941
|
// it rather than to itself.
|
|
661
942
|
const nestedId = generateToolCallId() as unknown as ToolUseId
|
|
662
943
|
const startedAt = Date.now()
|
|
944
|
+
const source = parent
|
|
945
|
+
? options?.runtimeToolCallId
|
|
946
|
+
? {
|
|
947
|
+
kind: 'code' as const,
|
|
948
|
+
parentToolUseId: parent,
|
|
949
|
+
runtimeToolCallId: options.runtimeToolCallId,
|
|
950
|
+
}
|
|
951
|
+
: { kind: 'nested' as const, parentToolUseId: parent }
|
|
952
|
+
: { kind: 'direct' as const }
|
|
953
|
+
const progress = new ToolProgressPublisher(this.emitEvent, {
|
|
954
|
+
type: 'tool_progress',
|
|
955
|
+
runId: this.config.runId,
|
|
956
|
+
toolUseId: nestedId,
|
|
957
|
+
toolName: name,
|
|
958
|
+
})
|
|
959
|
+
if (preparedCall.kind === 'synthetic') {
|
|
960
|
+
await this.emitEvent({
|
|
961
|
+
type: 'tool_executing',
|
|
962
|
+
runId: this.config.runId,
|
|
963
|
+
toolUseId: nestedId,
|
|
964
|
+
toolName: name,
|
|
965
|
+
input: preparedInput,
|
|
966
|
+
...(via ? { via } : {}),
|
|
967
|
+
})
|
|
968
|
+
await progress.close()
|
|
969
|
+
await this.emitEvent({
|
|
970
|
+
type: 'tool_completed',
|
|
971
|
+
runId: this.config.runId,
|
|
972
|
+
toolUseId: nestedId,
|
|
973
|
+
toolName: name,
|
|
974
|
+
result: preparedCall.message,
|
|
975
|
+
isError: preparedCall.isError,
|
|
976
|
+
durationMs: Date.now() - startedAt,
|
|
977
|
+
outputLength: preparedCall.message.length,
|
|
978
|
+
...(via ? { via } : {}),
|
|
979
|
+
})
|
|
980
|
+
return preparedCall.isError
|
|
981
|
+
? { success: false, output: '', error: preparedCall.message }
|
|
982
|
+
: { success: true, output: preparedCall.message }
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const gateResult = this.config.authorizationGate?.evaluate({
|
|
986
|
+
toolName: name,
|
|
987
|
+
toolInput: preparedInput,
|
|
988
|
+
toolDef: this.config.tools.get(name),
|
|
989
|
+
})
|
|
990
|
+
if (gateResult && gateResult.decision !== 'allow') {
|
|
991
|
+
const reason =
|
|
992
|
+
gateResult.decision === 'deny'
|
|
993
|
+
? `Blocked by the authorization gate: ${gateResult.reason}`
|
|
994
|
+
: `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}`
|
|
995
|
+
const output = deniedToolOutput(name, reason)
|
|
996
|
+
// Same fail-closed durability rule as a direct gate denial: if the
|
|
997
|
+
// configured run store cannot record the refusal, do not quietly carry
|
|
998
|
+
// on with an unaudited execution.
|
|
999
|
+
if (!this.config.recordAudit) {
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
`Nested tool "${name}" was refused, but no durable audit recorder is configured.`,
|
|
1002
|
+
)
|
|
1003
|
+
}
|
|
1004
|
+
await this.config.recordAudit({
|
|
1005
|
+
what: { action: 'tool_call', tool: name },
|
|
1006
|
+
outcome: 'refused',
|
|
1007
|
+
reason,
|
|
1008
|
+
})
|
|
1009
|
+
await progress.close()
|
|
1010
|
+
await this.emitEvent({
|
|
1011
|
+
type: 'tool_executing',
|
|
1012
|
+
runId: this.config.runId,
|
|
1013
|
+
toolUseId: nestedId,
|
|
1014
|
+
toolName: name,
|
|
1015
|
+
input: preparedInput,
|
|
1016
|
+
...(via ? { via } : {}),
|
|
1017
|
+
})
|
|
1018
|
+
await this.emitEvent({
|
|
1019
|
+
type: 'tool_completed',
|
|
1020
|
+
runId: this.config.runId,
|
|
1021
|
+
toolUseId: nestedId,
|
|
1022
|
+
toolName: name,
|
|
1023
|
+
result: output,
|
|
1024
|
+
isError: true,
|
|
1025
|
+
durationMs: Date.now() - startedAt,
|
|
1026
|
+
outputLength: output.length,
|
|
1027
|
+
...(via ? { via } : {}),
|
|
1028
|
+
})
|
|
1029
|
+
return { success: false, output: '', error: reason }
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const childContext: ToolContext = {
|
|
1033
|
+
...context,
|
|
1034
|
+
abortSignal: signal,
|
|
1035
|
+
toolUseId: nestedId,
|
|
1036
|
+
source,
|
|
1037
|
+
dispatchTool: (childName, childInput, childOptions) =>
|
|
1038
|
+
this.dispatchNested(
|
|
1039
|
+
childName,
|
|
1040
|
+
childInput,
|
|
1041
|
+
childContext,
|
|
1042
|
+
recordObservation,
|
|
1043
|
+
name,
|
|
1044
|
+
childOptions,
|
|
1045
|
+
),
|
|
1046
|
+
// A nested execution has its own event/progress identity, but its
|
|
1047
|
+
// durable pause belongs to the nearest model-issued ancestor. The
|
|
1048
|
+
// checkpoint transcript contains that ancestor call and not this
|
|
1049
|
+
// ephemeral child id; minting a pause route for `nestedId` makes the
|
|
1050
|
+
// answer impossible to match after process restart. `...context`
|
|
1051
|
+
// intentionally preserves the ancestor route here.
|
|
1052
|
+
report: (message: string, fraction?: number) => progress.report(message, fraction),
|
|
1053
|
+
}
|
|
663
1054
|
|
|
664
1055
|
await this.emitEvent({
|
|
665
1056
|
type: 'tool_executing',
|
|
666
1057
|
runId: this.config.runId,
|
|
667
1058
|
toolUseId: nestedId,
|
|
668
1059
|
toolName: name,
|
|
669
|
-
input,
|
|
1060
|
+
input: preparedInput,
|
|
670
1061
|
...(via ? { via } : {}),
|
|
671
1062
|
})
|
|
672
1063
|
|
|
673
|
-
const
|
|
1064
|
+
const vetoOutcome = this.probes.queryVeto(
|
|
1065
|
+
{
|
|
1066
|
+
type: 'tool_executing',
|
|
1067
|
+
runId: this.config.runId,
|
|
1068
|
+
toolUseId: nestedId,
|
|
1069
|
+
toolName: name,
|
|
1070
|
+
input: preparedInput,
|
|
1071
|
+
...(via ? { via } : {}),
|
|
1072
|
+
},
|
|
1073
|
+
buildProbeContext({ runId: this.config.runId }),
|
|
1074
|
+
)
|
|
1075
|
+
if (vetoOutcome.action === 'deny') {
|
|
1076
|
+
const probeName = vetoOutcome.probeName ?? 'unnamed'
|
|
1077
|
+
const reason = vetoOutcome.reason ?? 'no reason provided'
|
|
1078
|
+
const message = new ProbeVetoError(probeName, reason, 'tool_executing').message
|
|
1079
|
+
await progress.close()
|
|
1080
|
+
await this.emitEvent({
|
|
1081
|
+
type: 'tool_completed',
|
|
1082
|
+
runId: this.config.runId,
|
|
1083
|
+
toolUseId: nestedId,
|
|
1084
|
+
toolName: name,
|
|
1085
|
+
result: `Error: ${message}`,
|
|
1086
|
+
isError: true,
|
|
1087
|
+
durationMs: Date.now() - startedAt,
|
|
1088
|
+
outputLength: message.length + 7,
|
|
1089
|
+
...(via ? { via } : {}),
|
|
1090
|
+
})
|
|
1091
|
+
return { success: false, output: '', error: message }
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
let result: ToolResult
|
|
1095
|
+
try {
|
|
1096
|
+
// Use the same deadline layer as a model-issued call. Calling the
|
|
1097
|
+
// registry directly made nested tools the only tools whose own
|
|
1098
|
+
// `timeoutMs` declaration was ignored.
|
|
1099
|
+
result = await this.runOnce(
|
|
1100
|
+
name,
|
|
1101
|
+
preparedInput,
|
|
1102
|
+
childContext,
|
|
1103
|
+
preparedCall.kind === 'ready' ? preparedCall.prepared : undefined,
|
|
1104
|
+
)
|
|
1105
|
+
} finally {
|
|
1106
|
+
await progress.close()
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const rawOutput = result.success
|
|
1110
|
+
? result.output
|
|
1111
|
+
: formatFailedToolOutput(result.output, result.error)
|
|
1112
|
+
const budgeted = applyToolOutputBudget({
|
|
1113
|
+
toolName: name,
|
|
1114
|
+
toolUseId: nestedId,
|
|
1115
|
+
output: rawOutput,
|
|
1116
|
+
maxChars: this.config.maxToolOutputChars ?? DEFAULT_MAX_TOOL_OUTPUT_CHARS,
|
|
1117
|
+
spillDir: this.config.toolOutputDir,
|
|
1118
|
+
onError: (message) =>
|
|
1119
|
+
this.log.warn('Failed to spill oversized nested tool output', {
|
|
1120
|
+
[NAMZU.RUN_ID]: this.config.runId,
|
|
1121
|
+
[GENAI.TOOL_NAME]: name,
|
|
1122
|
+
'exception.message': message,
|
|
1123
|
+
}),
|
|
1124
|
+
})
|
|
1125
|
+
const visibleResult: ToolResult = result.success
|
|
1126
|
+
? { ...result, output: budgeted.output }
|
|
1127
|
+
: {
|
|
1128
|
+
...result,
|
|
1129
|
+
output: '',
|
|
1130
|
+
// `run_code` sends `error`, not `output`, across the worker
|
|
1131
|
+
// bridge on a failed host call. Leaving the raw error here would
|
|
1132
|
+
// make the success path bounded and the failure path unbounded.
|
|
1133
|
+
error: budgeted.output,
|
|
1134
|
+
}
|
|
674
1135
|
|
|
675
1136
|
await this.emitEvent({
|
|
676
1137
|
type: 'tool_completed',
|
|
677
1138
|
runId: this.config.runId,
|
|
678
1139
|
toolUseId: nestedId,
|
|
679
1140
|
toolName: name,
|
|
680
|
-
result:
|
|
1141
|
+
result: budgeted.output,
|
|
681
1142
|
isError: !result.success,
|
|
682
1143
|
durationMs: Date.now() - startedAt,
|
|
1144
|
+
outputLength: budgeted.originalLength,
|
|
1145
|
+
...(budgeted.truncated ? { outputTruncated: true } : {}),
|
|
1146
|
+
...(budgeted.spillPath ? { outputSpillPath: budgeted.spillPath } : {}),
|
|
683
1147
|
...(via ? { via } : {}),
|
|
684
1148
|
})
|
|
685
1149
|
recordObservation({
|
|
686
1150
|
runId: this.config.runId,
|
|
687
1151
|
toolUseId: nestedId,
|
|
688
1152
|
toolName: name,
|
|
689
|
-
input,
|
|
1153
|
+
input: preparedInput,
|
|
690
1154
|
result,
|
|
691
1155
|
...(parent ? { parentToolUseId: parent } : {}),
|
|
692
1156
|
})
|
|
693
1157
|
|
|
694
|
-
return
|
|
1158
|
+
return visibleResult
|
|
695
1159
|
}
|
|
696
1160
|
|
|
697
1161
|
private buildToolContext(
|
|
@@ -719,6 +1183,7 @@ export class ToolExecutor {
|
|
|
719
1183
|
adoptSkillScope: (scope) => {
|
|
720
1184
|
this.skillScope = { ...scope, adoptedInBatch: this.batchCounter }
|
|
721
1185
|
},
|
|
1186
|
+
maxToolOutputChars: this.config.maxToolOutputChars ?? DEFAULT_MAX_TOOL_OUTPUT_CHARS,
|
|
722
1187
|
...(this.config.skills ? { skills: this.config.skills } : {}),
|
|
723
1188
|
...(this.config.web ? { web: this.config.web } : {}),
|
|
724
1189
|
// The SAME registry and the SAME context a model-issued call
|
|
@@ -729,7 +1194,8 @@ export class ToolExecutor {
|
|
|
729
1194
|
// dispatching outside a batch has no parent call to name. The
|
|
730
1195
|
// per-call context below overrides it with one that does; see
|
|
731
1196
|
// `dispatchNested`.
|
|
732
|
-
dispatchTool: (name, input) =>
|
|
1197
|
+
dispatchTool: (name, input, options) =>
|
|
1198
|
+
this.dispatchNested(name, input, context, recordObservation, undefined, options),
|
|
733
1199
|
sandbox: this.config.sandbox,
|
|
734
1200
|
fileReadTracker: this.fileReadTracker,
|
|
735
1201
|
// Bound to this run, once. Binding here rather than passing the
|
|
@@ -759,99 +1225,112 @@ export class ToolExecutor {
|
|
|
759
1225
|
toolCall: ToolCall,
|
|
760
1226
|
toolContext: ToolContext,
|
|
761
1227
|
recordObservation: (observation: ToolResultObservation) => void,
|
|
1228
|
+
settleProgress: () => Promise<void>,
|
|
1229
|
+
preparedCall?: PreparedDirectCall,
|
|
762
1230
|
): Promise<ToolCallOutcome> {
|
|
763
|
-
|
|
1231
|
+
if (preparedCall?.kind === 'synthetic') {
|
|
1232
|
+
return this.recordSyntheticPreparation(preparedCall)
|
|
1233
|
+
}
|
|
764
1234
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
// was answered with a generic hint while the configured repairer sat
|
|
769
|
-
// unused. Offer it the partial buffer first.
|
|
770
|
-
const truncationRepair =
|
|
771
|
-
toolCall.metadata?.inputTruncated === true
|
|
772
|
-
? await this.repairTruncatedCall(toolCall, toolName)
|
|
773
|
-
: null
|
|
1235
|
+
let toolName = preparedCall?.toolName ?? toolCall.function.name
|
|
1236
|
+
let input: unknown
|
|
1237
|
+
let prepared: PreparedToolExecution | undefined
|
|
774
1238
|
|
|
775
|
-
if (
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1239
|
+
if (preparedCall?.kind === 'ready' || preparedCall?.kind === 'legacy') {
|
|
1240
|
+
input = preparedCall.input
|
|
1241
|
+
prepared = preparedCall.kind === 'ready' ? preparedCall.prepared : undefined
|
|
1242
|
+
} else {
|
|
1243
|
+
// A stream that cut off mid-JSON is the case `repairToolCall` exists
|
|
1244
|
+
// for, and it used to be the one case that never reached it: this
|
|
1245
|
+
// branch returned before `resolveCall` ran, so the motivating failure
|
|
1246
|
+
// was answered with a generic hint while the configured repairer sat
|
|
1247
|
+
// unused. Offer it the partial buffer first.
|
|
1248
|
+
const truncationRepair =
|
|
1249
|
+
toolCall.metadata?.inputTruncated === true
|
|
1250
|
+
? await this.repairTruncatedCall(toolCall, toolName)
|
|
1251
|
+
: null
|
|
1252
|
+
|
|
1253
|
+
if (toolCall.metadata?.inputTruncated === true && !truncationRepair) {
|
|
1254
|
+
const message = truncatedToolInputMessage(toolName)
|
|
1255
|
+
await this.emitEvent({
|
|
1256
|
+
type: 'tool_executing',
|
|
1257
|
+
runId: this.config.runId,
|
|
1258
|
+
toolUseId: toolCall.id,
|
|
1259
|
+
toolName,
|
|
1260
|
+
input: {},
|
|
1261
|
+
})
|
|
1262
|
+
await this.emitEvent({
|
|
1263
|
+
type: 'tool_completed',
|
|
1264
|
+
runId: this.config.runId,
|
|
1265
|
+
toolUseId: toolCall.id,
|
|
1266
|
+
toolName,
|
|
1267
|
+
result: message,
|
|
1268
|
+
isError: true,
|
|
1269
|
+
})
|
|
1270
|
+
return {
|
|
1271
|
+
toolCallId: toolCall.id,
|
|
1272
|
+
toolName,
|
|
1273
|
+
output: message,
|
|
1274
|
+
isError: true,
|
|
1275
|
+
}
|
|
797
1276
|
}
|
|
798
|
-
}
|
|
799
1277
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1278
|
+
// A malformed call used to cost a full model round trip to fix: the
|
|
1279
|
+
// error went back as a `tool_result`, the model re-read the whole
|
|
1280
|
+
// context and tried again. A host that can repair it locally turns
|
|
1281
|
+
// that into nothing. No-op when no repairer is configured.
|
|
1282
|
+
const resolved = await this.resolveCall(
|
|
1283
|
+
truncationRepair
|
|
1284
|
+
? {
|
|
1285
|
+
...toolCall,
|
|
1286
|
+
function: {
|
|
1287
|
+
...toolCall.function,
|
|
1288
|
+
name: truncationRepair.toolName ?? toolName,
|
|
1289
|
+
arguments: truncationRepair.arguments,
|
|
1290
|
+
},
|
|
1291
|
+
metadata: {},
|
|
1292
|
+
}
|
|
1293
|
+
: toolCall,
|
|
1294
|
+
)
|
|
1295
|
+
toolName = resolved.toolName
|
|
1296
|
+
|
|
1297
|
+
if (!resolved.ok) {
|
|
1298
|
+
// malformed JSON args used to return without ever
|
|
1299
|
+
// emitting tool_executing or tool_completed, leaving UI cards
|
|
1300
|
+
// orphaned in `streaming_input`. Emit the executing→completed
|
|
1301
|
+
// terminal pair so the card lifecycle closes.
|
|
1302
|
+
const message = resolved.message
|
|
1303
|
+
await this.emitEvent({
|
|
1304
|
+
type: 'tool_executing',
|
|
1305
|
+
runId: this.config.runId,
|
|
1306
|
+
toolUseId: toolCall.id,
|
|
1307
|
+
toolName,
|
|
1308
|
+
input: {},
|
|
1309
|
+
})
|
|
1310
|
+
await this.emitEvent({
|
|
1311
|
+
type: 'tool_completed',
|
|
1312
|
+
runId: this.config.runId,
|
|
1313
|
+
toolUseId: toolCall.id,
|
|
1314
|
+
toolName,
|
|
1315
|
+
result: message,
|
|
1316
|
+
isError: true,
|
|
1317
|
+
})
|
|
1318
|
+
return {
|
|
1319
|
+
toolCallId: toolCall.id,
|
|
1320
|
+
toolName,
|
|
1321
|
+
output: message,
|
|
1322
|
+
isError: true,
|
|
1323
|
+
}
|
|
845
1324
|
}
|
|
846
|
-
}
|
|
847
1325
|
|
|
848
|
-
|
|
1326
|
+
input = resolved.input
|
|
849
1327
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
1328
|
+
const preOutcome = await this.runPreToolHook(toolName, input)
|
|
1329
|
+
if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
|
|
1330
|
+
return this.recordSyntheticHookOutcome(toolCall.id, toolName, preOutcome.input, preOutcome)
|
|
1331
|
+
}
|
|
1332
|
+
input = preOutcome.input
|
|
853
1333
|
}
|
|
854
|
-
input = preOutcome.input
|
|
855
1334
|
|
|
856
1335
|
const activity = this.activityStore.create({
|
|
857
1336
|
type: 'tool_call',
|
|
@@ -939,7 +1418,7 @@ export class ToolExecutor {
|
|
|
939
1418
|
// version silently DROPPED `content`, so a tool returning an image
|
|
940
1419
|
// block had it discarded here — before the wire mapper that was
|
|
941
1420
|
// built to carry it ever saw it.
|
|
942
|
-
let result: ToolResult = await this.runOnce(toolName, input, toolContext)
|
|
1421
|
+
let result: ToolResult = await this.runOnce(toolName, input, toolContext, prepared)
|
|
943
1422
|
let post = await this.runPostToolHook(toolName, input, result)
|
|
944
1423
|
|
|
945
1424
|
// In-loop retry. A transient failure used to cost a full model round
|
|
@@ -1008,7 +1487,7 @@ export class ToolExecutor {
|
|
|
1008
1487
|
break
|
|
1009
1488
|
}
|
|
1010
1489
|
|
|
1011
|
-
result = await this.runOnce(toolName, input, toolContext)
|
|
1490
|
+
result = await this.runOnce(toolName, input, toolContext, prepared)
|
|
1012
1491
|
post = await this.runPostToolHook(toolName, input, result)
|
|
1013
1492
|
}
|
|
1014
1493
|
const durationMs = Date.now() - startMs
|
|
@@ -1017,16 +1496,35 @@ export class ToolExecutor {
|
|
|
1017
1496
|
? result.output
|
|
1018
1497
|
: formatFailedToolOutput(result.output, result.error)
|
|
1019
1498
|
|
|
1020
|
-
|
|
1499
|
+
const postOverride = post.override
|
|
1500
|
+
let output =
|
|
1501
|
+
postOverride?.output ?? (result.success ? this.maybeCompress(toolName, rawOutput) : rawOutput)
|
|
1502
|
+
const selectedContent = postOverride?.isError
|
|
1503
|
+
? undefined
|
|
1504
|
+
: (postOverride?.content ?? result.content)
|
|
1505
|
+
const maxToolOutputChars = this.config.maxToolOutputChars ?? DEFAULT_MAX_TOOL_OUTPUT_CHARS
|
|
1506
|
+
|
|
1507
|
+
// If the text preview forces rich content out, say so inside the SAME
|
|
1508
|
+
// budget. Appending this after truncation made the diagnostic itself a
|
|
1509
|
+
// cap bypass; selecting a post-hook override after truncation was a
|
|
1510
|
+
// larger bypass that could replace a bounded result with anything.
|
|
1511
|
+
if (
|
|
1512
|
+
maxToolOutputChars > 0 &&
|
|
1513
|
+
output.length > maxToolOutputChars &&
|
|
1514
|
+
selectedContent !== undefined
|
|
1515
|
+
) {
|
|
1516
|
+
const dropped = describeDroppedContent(selectedContent)
|
|
1517
|
+
if (dropped) output = `${output}\n\n${dropped}`
|
|
1518
|
+
}
|
|
1021
1519
|
|
|
1022
1520
|
// Compression is opportunistic and shell-only; the budget is the
|
|
1023
|
-
// hard bound that applies to every tool
|
|
1024
|
-
//
|
|
1521
|
+
// hard bound that applies to every final tool result, including a
|
|
1522
|
+
// post-tool hook's replacement and the rich-content omission notice.
|
|
1025
1523
|
const budgeted = applyToolOutputBudget({
|
|
1026
1524
|
toolName,
|
|
1027
1525
|
toolUseId: toolCall.id,
|
|
1028
1526
|
output,
|
|
1029
|
-
maxChars:
|
|
1527
|
+
maxChars: maxToolOutputChars,
|
|
1030
1528
|
spillDir: this.config.toolOutputDir,
|
|
1031
1529
|
onError: (message) =>
|
|
1032
1530
|
this.log.warn('Failed to spill oversized tool output', {
|
|
@@ -1045,22 +1543,6 @@ export class ToolExecutor {
|
|
|
1045
1543
|
}
|
|
1046
1544
|
output = budgeted.output
|
|
1047
1545
|
|
|
1048
|
-
// A truncated text half drops the rich half with it — the preview is
|
|
1049
|
-
// no longer the tool's own payload, so an image alongside it would be
|
|
1050
|
-
// illustrating something the model can no longer read. Dropping is
|
|
1051
|
-
// right; doing it SILENTLY is not. The model saw a preview and had no
|
|
1052
|
-
// way to know an image existed at all, so it reasoned as though the
|
|
1053
|
-
// tool had returned text only.
|
|
1054
|
-
if (budgeted.truncated && result.content !== undefined) {
|
|
1055
|
-
const dropped = describeDroppedContent(result.content)
|
|
1056
|
-
if (dropped) output = `${output}\n\n${dropped}`
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
const postOverride = post.override
|
|
1060
|
-
if (postOverride !== null) {
|
|
1061
|
-
output = postOverride.output
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
1546
|
// A failed call, or an override that says the call failed. A `replace`
|
|
1065
1547
|
// says the opposite, and reading it as a failure is what made redaction
|
|
1066
1548
|
// unusable: the model was told a successful call had gone wrong, and
|
|
@@ -1095,6 +1577,11 @@ export class ToolExecutor {
|
|
|
1095
1577
|
}
|
|
1096
1578
|
}
|
|
1097
1579
|
|
|
1580
|
+
// The terminal event closes the live row. Every accepted progress update
|
|
1581
|
+
// must settle before it, otherwise a slow host can receive an update for a
|
|
1582
|
+
// call it has already removed. Detached work reporting after a timeout is
|
|
1583
|
+
// ignored because close() also revokes the publisher.
|
|
1584
|
+
await settleProgress()
|
|
1098
1585
|
await this.emitEvent({
|
|
1099
1586
|
type: 'tool_completed',
|
|
1100
1587
|
runId: this.config.runId,
|
|
@@ -1118,12 +1605,8 @@ export class ToolExecutor {
|
|
|
1118
1605
|
})
|
|
1119
1606
|
|
|
1120
1607
|
const resolveContent = (): { content?: ToolResultContent } => {
|
|
1121
|
-
if (
|
|
1122
|
-
|
|
1123
|
-
}
|
|
1124
|
-
if (postOverride?.isError) return {}
|
|
1125
|
-
if (budgeted.truncated || result.content === undefined) return {}
|
|
1126
|
-
return { content: this.budgetContent(result.content, toolName) }
|
|
1608
|
+
if (budgeted.truncated || selectedContent === undefined) return {}
|
|
1609
|
+
return { content: this.budgetContent(selectedContent, toolName) }
|
|
1127
1610
|
}
|
|
1128
1611
|
|
|
1129
1612
|
return {
|
|
@@ -1169,33 +1652,35 @@ export class ToolExecutor {
|
|
|
1169
1652
|
toolName: string,
|
|
1170
1653
|
input: unknown,
|
|
1171
1654
|
toolContext: ToolContext,
|
|
1655
|
+
prepared?: PreparedToolExecution,
|
|
1172
1656
|
): Promise<ToolResult> {
|
|
1173
1657
|
const timeoutMs =
|
|
1174
1658
|
this.config.tools.get(toolName)?.timeoutMs ??
|
|
1175
1659
|
this.config.toolTimeoutMs ??
|
|
1176
1660
|
DEFAULT_TOOL_TIMEOUT_MS
|
|
1177
1661
|
|
|
1178
|
-
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
1179
|
-
return this.config.tools.execute(toolName, input, toolContext)
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
1662
|
const controller = new AbortController()
|
|
1183
|
-
const
|
|
1184
|
-
const
|
|
1185
|
-
if (
|
|
1186
|
-
else
|
|
1663
|
+
const parentSignal = toolContext.abortSignal
|
|
1664
|
+
const onParentAbort = () => controller.abort(parentSignal.reason)
|
|
1665
|
+
if (parentSignal.aborted) controller.abort(parentSignal.reason)
|
|
1666
|
+
else parentSignal.addEventListener('abort', onParentAbort, { once: true })
|
|
1187
1667
|
|
|
1188
1668
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
1189
1669
|
let timedOut = false
|
|
1670
|
+
let invocationOpen = true
|
|
1671
|
+
const nestedDispatches = new Set<Promise<ToolResult>>()
|
|
1190
1672
|
|
|
1191
1673
|
try {
|
|
1192
|
-
const expired =
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1674
|
+
const expired =
|
|
1675
|
+
Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
1676
|
+
? new Promise<'timeout'>((resolve) => {
|
|
1677
|
+
timer = setTimeout(() => {
|
|
1678
|
+
timedOut = true
|
|
1679
|
+
controller.abort(new Error(`Tool "${toolName}" exceeded ${timeoutMs}ms`))
|
|
1680
|
+
resolve('timeout')
|
|
1681
|
+
}, timeoutMs)
|
|
1682
|
+
})
|
|
1683
|
+
: undefined
|
|
1199
1684
|
|
|
1200
1685
|
const aborted = new Promise<'aborted'>((resolve) => {
|
|
1201
1686
|
if (controller.signal.aborted && !timedOut) {
|
|
@@ -1211,15 +1696,54 @@ export class ToolExecutor {
|
|
|
1211
1696
|
)
|
|
1212
1697
|
})
|
|
1213
1698
|
|
|
1214
|
-
const
|
|
1699
|
+
const inheritedDispatch = toolContext.dispatchTool
|
|
1700
|
+
const scopedDispatch = inheritedDispatch
|
|
1701
|
+
? (name: string, nestedInput: unknown, options?: ToolDispatchOptions) => {
|
|
1702
|
+
if (!invocationOpen) {
|
|
1703
|
+
return Promise.reject(
|
|
1704
|
+
new Error(`Tool "${toolName}" invocation has settled; nested dispatch is closed.`),
|
|
1705
|
+
)
|
|
1706
|
+
}
|
|
1707
|
+
const nested = inheritedDispatch(name, nestedInput, {
|
|
1708
|
+
...options,
|
|
1709
|
+
signal: options?.signal
|
|
1710
|
+
? AbortSignal.any([controller.signal, options.signal])
|
|
1711
|
+
: controller.signal,
|
|
1712
|
+
})
|
|
1713
|
+
nestedDispatches.add(nested)
|
|
1714
|
+
// `finally()` creates a second promise. Observe that promise too, or a
|
|
1715
|
+
// rejected nested call which its owner intentionally awaits later would
|
|
1716
|
+
// also create an unhandled cleanup rejection here.
|
|
1717
|
+
void nested.finally(() => nestedDispatches.delete(nested)).catch(() => {})
|
|
1718
|
+
return nested
|
|
1719
|
+
}
|
|
1720
|
+
: undefined
|
|
1721
|
+
|
|
1722
|
+
if (controller.signal.aborted) {
|
|
1723
|
+
return {
|
|
1724
|
+
success: false,
|
|
1725
|
+
output: '',
|
|
1726
|
+
error: abortReasonText(controller.signal.reason)
|
|
1727
|
+
? `Tool "${toolName}" was cancelled: ${abortReasonText(controller.signal.reason)}`
|
|
1728
|
+
: `Tool "${toolName}" was cancelled.`,
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
const context = {
|
|
1215
1733
|
...toolContext,
|
|
1216
1734
|
abortSignal: controller.signal,
|
|
1217
|
-
|
|
1735
|
+
...(scopedDispatch ? { dispatchTool: scopedDispatch } : {}),
|
|
1736
|
+
}
|
|
1737
|
+
const execution = prepared
|
|
1738
|
+
? this.config.tools.executePrepared(prepared, context)
|
|
1739
|
+
: this.config.tools.execute(toolName, input, context)
|
|
1218
1740
|
// The loser of the race may still reject later; neutralize it so
|
|
1219
1741
|
// it is never an unhandled rejection.
|
|
1220
1742
|
execution.catch(() => {})
|
|
1221
1743
|
|
|
1222
|
-
const outcome = await Promise.race(
|
|
1744
|
+
const outcome = await Promise.race(
|
|
1745
|
+
expired ? [execution, expired, aborted] : [execution, aborted],
|
|
1746
|
+
)
|
|
1223
1747
|
|
|
1224
1748
|
if (outcome === 'timeout') {
|
|
1225
1749
|
this.log.warn('Tool timed out', {
|
|
@@ -1253,38 +1777,312 @@ export class ToolExecutor {
|
|
|
1253
1777
|
|
|
1254
1778
|
return outcome
|
|
1255
1779
|
} finally {
|
|
1780
|
+
// Revoke synchronously before awaiting anything. A retained closure
|
|
1781
|
+
// called from another microtask is refused here; the aborted signal is
|
|
1782
|
+
// the structural backstop inside dispatchNested itself.
|
|
1783
|
+
invocationOpen = false
|
|
1784
|
+
if (!controller.signal.aborted) {
|
|
1785
|
+
controller.abort(new Error(`Tool "${toolName}" invocation has settled.`))
|
|
1786
|
+
}
|
|
1787
|
+
// Calls already admitted before closure own event rows and registry
|
|
1788
|
+
// work. Their executor races observe the abort, emit a terminal result,
|
|
1789
|
+
// and settle before the parent is allowed to report completion.
|
|
1790
|
+
while (nestedDispatches.size > 0) {
|
|
1791
|
+
await Promise.allSettled([...nestedDispatches])
|
|
1792
|
+
}
|
|
1256
1793
|
if (timer !== undefined) clearTimeout(timer)
|
|
1257
|
-
|
|
1794
|
+
parentSignal.removeEventListener('abort', onParentAbort)
|
|
1258
1795
|
}
|
|
1259
1796
|
}
|
|
1260
1797
|
|
|
1261
|
-
private async runPreToolHook(
|
|
1262
|
-
|
|
1798
|
+
private async runPreToolHook(
|
|
1799
|
+
toolName: string,
|
|
1800
|
+
input: unknown,
|
|
1801
|
+
signal: AbortSignal = this.config.abortSignal,
|
|
1802
|
+
): Promise<PreToolHookOutcome> {
|
|
1803
|
+
if (!this.config.pluginManager) return { kind: 'continue', input, modified: false }
|
|
1263
1804
|
const results = await this.config.pluginManager.executeHooks(
|
|
1264
1805
|
'pre_tool_use',
|
|
1265
1806
|
{
|
|
1266
1807
|
runId: this.config.runId,
|
|
1267
1808
|
toolName,
|
|
1268
1809
|
toolInput: input,
|
|
1269
|
-
signal
|
|
1810
|
+
signal,
|
|
1270
1811
|
},
|
|
1271
1812
|
this.emitEvent,
|
|
1272
1813
|
)
|
|
1273
1814
|
return this.interpretPreToolResults(toolName, input, results)
|
|
1274
1815
|
}
|
|
1275
1816
|
|
|
1817
|
+
private async prepareNestedCall(
|
|
1818
|
+
toolName: string,
|
|
1819
|
+
input: unknown,
|
|
1820
|
+
signal: AbortSignal,
|
|
1821
|
+
): Promise<PreparedNestedCall> {
|
|
1822
|
+
const prepare = this.config.tools.prepareExecution
|
|
1823
|
+
const executePrepared = this.config.tools.executePrepared
|
|
1824
|
+
if (typeof prepare !== 'function' || typeof executePrepared !== 'function') {
|
|
1825
|
+
if (this.config.authorizationGate) {
|
|
1826
|
+
return {
|
|
1827
|
+
kind: 'synthetic',
|
|
1828
|
+
input,
|
|
1829
|
+
message: `Tool "${toolName}" was not executed because its registry cannot bind authorization to one prepared input.`,
|
|
1830
|
+
isError: true,
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
const preOutcome = await this.runPreToolHook(toolName, input, signal)
|
|
1834
|
+
if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
|
|
1835
|
+
return {
|
|
1836
|
+
kind: 'synthetic',
|
|
1837
|
+
input: preOutcome.input,
|
|
1838
|
+
message: preOutcome.output,
|
|
1839
|
+
isError: preOutcome.kind === 'error',
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
return { kind: 'legacy', input: preOutcome.input }
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
let preparation: ReturnType<typeof prepare>
|
|
1846
|
+
try {
|
|
1847
|
+
preparation = prepare.call(this.config.tools, toolName, input)
|
|
1848
|
+
} catch (err) {
|
|
1849
|
+
return {
|
|
1850
|
+
kind: 'synthetic',
|
|
1851
|
+
input,
|
|
1852
|
+
message: `Tool "${toolName}" could not be prepared: ${toErrorMessage(err)}`,
|
|
1853
|
+
isError: true,
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
if (!preparation.success) {
|
|
1857
|
+
return {
|
|
1858
|
+
kind: 'synthetic',
|
|
1859
|
+
input,
|
|
1860
|
+
message: formatFailedToolOutput(preparation.result.output, preparation.result.error),
|
|
1861
|
+
isError: true,
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
const preOutcome = await this.runPreToolHook(toolName, preparation.prepared.input, signal)
|
|
1865
|
+
if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
|
|
1866
|
+
return {
|
|
1867
|
+
kind: 'synthetic',
|
|
1868
|
+
input: preOutcome.input,
|
|
1869
|
+
message: preOutcome.output,
|
|
1870
|
+
isError: preOutcome.kind === 'error',
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
if (!preOutcome.modified) {
|
|
1874
|
+
return {
|
|
1875
|
+
kind: 'ready',
|
|
1876
|
+
input: preparation.prepared.input,
|
|
1877
|
+
prepared: preparation.prepared,
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
const modified = prepare.call(this.config.tools, toolName, preOutcome.input)
|
|
1881
|
+
if (!modified.success) {
|
|
1882
|
+
return {
|
|
1883
|
+
kind: 'synthetic',
|
|
1884
|
+
input: preOutcome.input,
|
|
1885
|
+
message: formatFailedToolOutput(modified.result.output, modified.result.error),
|
|
1886
|
+
isError: true,
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
return { kind: 'ready', input: modified.prepared.input, prepared: modified.prepared }
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
private async prepareDirectCall(toolCall: ToolCall): Promise<PreparedDirectCall> {
|
|
1893
|
+
let toolName = toolCall.function.name
|
|
1894
|
+
const truncationRepair =
|
|
1895
|
+
toolCall.metadata?.inputTruncated === true
|
|
1896
|
+
? await this.repairTruncatedCall(toolCall, toolName)
|
|
1897
|
+
: null
|
|
1898
|
+
if (toolCall.metadata?.inputTruncated === true && !truncationRepair) {
|
|
1899
|
+
return {
|
|
1900
|
+
kind: 'synthetic',
|
|
1901
|
+
toolCall,
|
|
1902
|
+
toolName,
|
|
1903
|
+
input: {},
|
|
1904
|
+
message: truncatedToolInputMessage(toolName),
|
|
1905
|
+
isError: true,
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
const prepare = this.config.tools.prepareExecution
|
|
1910
|
+
const executePrepared = this.config.tools.executePrepared
|
|
1911
|
+
if (typeof prepare !== 'function' || typeof executePrepared !== 'function') {
|
|
1912
|
+
const resolved = await this.resolveCall(
|
|
1913
|
+
truncationRepair
|
|
1914
|
+
? {
|
|
1915
|
+
...toolCall,
|
|
1916
|
+
function: {
|
|
1917
|
+
...toolCall.function,
|
|
1918
|
+
name: truncationRepair.toolName ?? toolName,
|
|
1919
|
+
arguments: truncationRepair.arguments,
|
|
1920
|
+
},
|
|
1921
|
+
metadata: {},
|
|
1922
|
+
}
|
|
1923
|
+
: toolCall,
|
|
1924
|
+
)
|
|
1925
|
+
toolName = resolved.toolName
|
|
1926
|
+
if (!resolved.ok) {
|
|
1927
|
+
return {
|
|
1928
|
+
kind: 'synthetic',
|
|
1929
|
+
toolCall,
|
|
1930
|
+
toolName,
|
|
1931
|
+
input: {},
|
|
1932
|
+
message: resolved.message,
|
|
1933
|
+
isError: true,
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
const preOutcome = await this.runPreToolHook(toolName, resolved.input)
|
|
1937
|
+
if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
|
|
1938
|
+
return {
|
|
1939
|
+
kind: 'synthetic',
|
|
1940
|
+
toolCall,
|
|
1941
|
+
toolName,
|
|
1942
|
+
input: preOutcome.input,
|
|
1943
|
+
message: preOutcome.output,
|
|
1944
|
+
isError: preOutcome.kind === 'error',
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
if (!this.config.authorizationGate) {
|
|
1948
|
+
return {
|
|
1949
|
+
kind: 'legacy',
|
|
1950
|
+
toolCall,
|
|
1951
|
+
toolName,
|
|
1952
|
+
input: preOutcome.input,
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
return {
|
|
1956
|
+
kind: 'synthetic',
|
|
1957
|
+
toolCall,
|
|
1958
|
+
toolName,
|
|
1959
|
+
input: preOutcome.input,
|
|
1960
|
+
message: `Tool "${toolName}" was not executed because its registry cannot bind authorization to one prepared input.`,
|
|
1961
|
+
isError: true,
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
let raw = truncationRepair?.arguments ?? toolCall.function.arguments
|
|
1966
|
+
toolName = truncationRepair?.toolName ?? toolName
|
|
1967
|
+
let repairUsed = truncationRepair !== null
|
|
1968
|
+
let preparation: ReturnType<typeof prepare>
|
|
1969
|
+
for (;;) {
|
|
1970
|
+
let parsed: unknown
|
|
1971
|
+
try {
|
|
1972
|
+
parsed = parseArguments(raw)
|
|
1973
|
+
} catch {
|
|
1974
|
+
const message = `Error: Invalid JSON in tool arguments for "${toolName}"`
|
|
1975
|
+
const repair =
|
|
1976
|
+
!repairUsed && this.config.repairToolCall
|
|
1977
|
+
? await this.requestRepair(toolCall, toolName, {
|
|
1978
|
+
reason: 'invalid_json',
|
|
1979
|
+
message,
|
|
1980
|
+
})
|
|
1981
|
+
: null
|
|
1982
|
+
if (repair) {
|
|
1983
|
+
repairUsed = true
|
|
1984
|
+
toolName = repair.toolName ?? toolName
|
|
1985
|
+
raw = repair.arguments
|
|
1986
|
+
continue
|
|
1987
|
+
}
|
|
1988
|
+
return { kind: 'synthetic', toolCall, toolName, input: {}, message, isError: true }
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
try {
|
|
1992
|
+
preparation = prepare.call(this.config.tools, toolName, parsed)
|
|
1993
|
+
} catch (err) {
|
|
1994
|
+
const message = `Error: Unknown or unavailable tool "${toolName}": ${toErrorMessage(err)}`
|
|
1995
|
+
const repair =
|
|
1996
|
+
!repairUsed && this.config.repairToolCall
|
|
1997
|
+
? await this.requestRepair(toolCall, toolName, {
|
|
1998
|
+
reason: 'unknown_tool',
|
|
1999
|
+
message,
|
|
2000
|
+
})
|
|
2001
|
+
: null
|
|
2002
|
+
if (repair) {
|
|
2003
|
+
repairUsed = true
|
|
2004
|
+
toolName = repair.toolName ?? toolName
|
|
2005
|
+
raw = repair.arguments
|
|
2006
|
+
continue
|
|
2007
|
+
}
|
|
2008
|
+
return { kind: 'synthetic', toolCall, toolName, input: parsed, message, isError: true }
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
if (preparation.success) break
|
|
2012
|
+
const message = formatFailedToolOutput(preparation.result.output, preparation.result.error)
|
|
2013
|
+
const repair =
|
|
2014
|
+
!repairUsed && this.config.repairToolCall
|
|
2015
|
+
? await this.requestRepair(toolCall, toolName, {
|
|
2016
|
+
reason: 'schema_validation',
|
|
2017
|
+
message,
|
|
2018
|
+
})
|
|
2019
|
+
: null
|
|
2020
|
+
if (repair) {
|
|
2021
|
+
repairUsed = true
|
|
2022
|
+
toolName = repair.toolName ?? toolName
|
|
2023
|
+
raw = repair.arguments
|
|
2024
|
+
continue
|
|
2025
|
+
}
|
|
2026
|
+
return {
|
|
2027
|
+
kind: 'synthetic',
|
|
2028
|
+
toolCall,
|
|
2029
|
+
toolName,
|
|
2030
|
+
input: parsed,
|
|
2031
|
+
message,
|
|
2032
|
+
isError: true,
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
const preOutcome = await this.runPreToolHook(toolName, preparation.prepared.input)
|
|
2037
|
+
if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
|
|
2038
|
+
return {
|
|
2039
|
+
kind: 'synthetic',
|
|
2040
|
+
toolCall,
|
|
2041
|
+
toolName,
|
|
2042
|
+
input: preOutcome.input,
|
|
2043
|
+
message: preOutcome.output,
|
|
2044
|
+
isError: preOutcome.kind === 'error',
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
if (preOutcome.modified) {
|
|
2049
|
+
const modified = prepare.call(this.config.tools, toolName, preOutcome.input)
|
|
2050
|
+
if (!modified.success) {
|
|
2051
|
+
return {
|
|
2052
|
+
kind: 'synthetic',
|
|
2053
|
+
toolCall,
|
|
2054
|
+
toolName,
|
|
2055
|
+
input: preOutcome.input,
|
|
2056
|
+
message: formatFailedToolOutput(modified.result.output, modified.result.error),
|
|
2057
|
+
isError: true,
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
preparation = modified
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
return {
|
|
2064
|
+
kind: 'ready',
|
|
2065
|
+
toolCall,
|
|
2066
|
+
toolName,
|
|
2067
|
+
input: preparation.prepared.input,
|
|
2068
|
+
prepared: preparation.prepared,
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
|
|
1276
2072
|
private interpretPreToolResults(
|
|
1277
2073
|
toolName: string,
|
|
1278
2074
|
initialInput: unknown,
|
|
1279
2075
|
results: readonly PluginHookResult[],
|
|
1280
2076
|
): PreToolHookOutcome {
|
|
1281
2077
|
let currentInput = initialInput
|
|
2078
|
+
let modified = false
|
|
1282
2079
|
for (const result of results) {
|
|
1283
2080
|
switch (result.action) {
|
|
1284
2081
|
case 'continue':
|
|
1285
2082
|
continue
|
|
1286
2083
|
case 'modify':
|
|
1287
2084
|
currentInput = result.input
|
|
2085
|
+
modified = true
|
|
1288
2086
|
continue
|
|
1289
2087
|
case 'skip':
|
|
1290
2088
|
return {
|
|
@@ -1313,7 +2111,7 @@ export class ToolExecutor {
|
|
|
1313
2111
|
}
|
|
1314
2112
|
}
|
|
1315
2113
|
}
|
|
1316
|
-
return { kind: 'continue', input: currentInput }
|
|
2114
|
+
return { kind: 'continue', input: currentInput, modified }
|
|
1317
2115
|
}
|
|
1318
2116
|
|
|
1319
2117
|
/**
|
|
@@ -1457,7 +2255,10 @@ export class ToolExecutor {
|
|
|
1457
2255
|
reason: failure.reason,
|
|
1458
2256
|
message: failure.message,
|
|
1459
2257
|
...(tool
|
|
1460
|
-
? {
|
|
2258
|
+
? {
|
|
2259
|
+
tool,
|
|
2260
|
+
jsonSchema: tool.modelInputSchema ?? renderToolSchema(tool.inputSchema),
|
|
2261
|
+
}
|
|
1461
2262
|
: {}),
|
|
1462
2263
|
availableTools: this.config.tools.listNames(),
|
|
1463
2264
|
})
|
|
@@ -1490,9 +2291,10 @@ export class ToolExecutor {
|
|
|
1490
2291
|
toolName: string,
|
|
1491
2292
|
input: unknown,
|
|
1492
2293
|
toolContext: ToolContext,
|
|
2294
|
+
prepared?: PreparedToolExecution,
|
|
1493
2295
|
): Promise<ToolResult> {
|
|
1494
2296
|
try {
|
|
1495
|
-
return await this.executeWithDeadline(toolName, input, toolContext)
|
|
2297
|
+
return await this.executeWithDeadline(toolName, input, toolContext, prepared)
|
|
1496
2298
|
} catch (err) {
|
|
1497
2299
|
const message = toErrorMessage(err)
|
|
1498
2300
|
this.log.warn('Tool execution threw', {
|
|
@@ -1573,13 +2375,19 @@ export class ToolExecutor {
|
|
|
1573
2375
|
* execution so UI cards reach a terminal state instead of hanging in
|
|
1574
2376
|
* `executing`, and records a failed activity for the trace.
|
|
1575
2377
|
*/
|
|
1576
|
-
private async recordDenial(
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
2378
|
+
private async recordDenial(
|
|
2379
|
+
toolCall: ToolCall,
|
|
2380
|
+
reason: string,
|
|
2381
|
+
preparedCall?: PreparedDirectCall,
|
|
2382
|
+
): Promise<ToolCallOutcome> {
|
|
2383
|
+
const toolName = preparedCall?.toolName ?? toolCall.function.name
|
|
2384
|
+
let input: unknown = preparedCall?.input ?? {}
|
|
2385
|
+
if (!preparedCall) {
|
|
2386
|
+
try {
|
|
2387
|
+
input = JSON.parse(toolCall.function.arguments || '{}')
|
|
2388
|
+
} catch {
|
|
2389
|
+
input = toolCall.function.arguments
|
|
2390
|
+
}
|
|
1583
2391
|
}
|
|
1584
2392
|
|
|
1585
2393
|
const output = deniedToolOutput(toolName, reason)
|
|
@@ -1666,6 +2474,13 @@ export class ToolExecutor {
|
|
|
1666
2474
|
}
|
|
1667
2475
|
}
|
|
1668
2476
|
|
|
2477
|
+
private recordSyntheticPreparation(call: Extract<PreparedDirectCall, { kind: 'synthetic' }>) {
|
|
2478
|
+
return this.recordSyntheticHookOutcome(call.toolCall.id, call.toolName, call.input, {
|
|
2479
|
+
kind: call.isError ? 'error' : 'skip',
|
|
2480
|
+
output: call.message,
|
|
2481
|
+
})
|
|
2482
|
+
}
|
|
2483
|
+
|
|
1669
2484
|
/**
|
|
1670
2485
|
* Bound the rich channel, or leave it alone when no cap is configured.
|
|
1671
2486
|
*
|