@namzu/sdk 32.0.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 +127 -0
- 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/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/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/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/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 +2 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +2 -2
- package/dist/public-runtime.js.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/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 +626 -161
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +46 -4
- package/dist/runtime/query/index.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/request-rich-content.d.ts.map +1 -1
- package/dist/runtime/query/request-rich-content.js +33 -13
- package/dist/runtime/query/request-rich-content.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/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/run-code.d.ts.map +1 -1
- package/dist/tools/builtins/run-code.js +9 -11
- 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/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 +1 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +2 -1
- 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/interface.d.ts +14 -0
- package/dist/types/provider/interface.d.ts.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 +77 -6
- 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/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/execution/base.ts +124 -7
- 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/plugin/lifecycle.ts +9 -3
- package/src/process/kill-tree.ts +11 -13
- 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 +7 -1
- package/src/registry/tool/execute.ts +230 -64
- package/src/run/command-gate.ts +12 -0
- 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 +769 -163
- package/src/runtime/query/index.ts +53 -6
- package/src/runtime/query/iteration/phases/tool-review.ts +87 -23
- package/src/runtime/query/request-rich-content.ts +39 -14
- 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/tools/builtins/computer-use.ts +45 -2
- package/src/tools/builtins/run-code.ts +9 -11
- package/src/tools/builtins/skill.ts +325 -7
- package/src/tools/coordinator/agent.ts +108 -33
- 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 +3 -2
- package/src/types/plugin/index.ts +15 -0
- package/src/types/provider/interface.ts +18 -0
- package/src/types/sandbox/index.ts +15 -10
- package/src/types/tool/index.ts +85 -6
- 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,10 +23,12 @@ 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,
|
|
@@ -55,6 +58,74 @@ import {
|
|
|
55
58
|
|
|
56
59
|
export type EmitEvent = (event: RunEvent) => Promise<void>
|
|
57
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
|
+
|
|
58
129
|
/**
|
|
59
130
|
* Default per-tool deadline. Long enough for a real build or test run,
|
|
60
131
|
* short enough that a wedged tool does not hold a turn open indefinitely.
|
|
@@ -319,6 +390,17 @@ export interface ToolExecutorConfig {
|
|
|
319
390
|
* reaches it. See {@link RepairToolCall}.
|
|
320
391
|
*/
|
|
321
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>
|
|
322
404
|
}
|
|
323
405
|
|
|
324
406
|
/**
|
|
@@ -336,7 +418,7 @@ interface PostToolOverride {
|
|
|
336
418
|
}
|
|
337
419
|
|
|
338
420
|
type PreToolHookOutcome =
|
|
339
|
-
| { kind: 'continue'; input: unknown }
|
|
421
|
+
| { kind: 'continue'; input: unknown; modified: boolean }
|
|
340
422
|
| { kind: 'skip'; input: unknown; output: string }
|
|
341
423
|
| { kind: 'error'; input: unknown; output: string }
|
|
342
424
|
|
|
@@ -399,6 +481,7 @@ export class ToolExecutor {
|
|
|
399
481
|
private workingStateManager?: WorkingStateManager
|
|
400
482
|
private probes: ProbeEnforcement
|
|
401
483
|
private parentSpan?: Span
|
|
484
|
+
private readonly preparedBatches = new WeakSet<PreparedToolBatch>()
|
|
402
485
|
/** Set per turn by the orchestrator; see {@link setStepAllowedTools}. */
|
|
403
486
|
private stepAllowedTools?: readonly string[]
|
|
404
487
|
private readonly readPaths: Set<string> = new Set()
|
|
@@ -553,15 +636,78 @@ export class ToolExecutor {
|
|
|
553
636
|
return typeof configured === 'function' ? configured() : configured
|
|
554
637
|
}
|
|
555
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
|
+
|
|
556
700
|
async executeBatch(
|
|
557
701
|
response: ChatCompletionResponse,
|
|
558
702
|
denials?: ToolCallDenials,
|
|
559
703
|
prior?: PriorToolResults,
|
|
704
|
+
preparedBatch?: PreparedToolBatch,
|
|
560
705
|
): Promise<ToolExecutionBatch> {
|
|
561
706
|
const toolCalls = response.message.toolCalls
|
|
562
707
|
if (!toolCalls) {
|
|
563
708
|
return { messages: [], results: [], observations: [] }
|
|
564
709
|
}
|
|
710
|
+
assertUniqueToolCallIds(toolCalls)
|
|
565
711
|
|
|
566
712
|
this.batchCounter += 1
|
|
567
713
|
|
|
@@ -569,7 +715,11 @@ export class ToolExecutor {
|
|
|
569
715
|
// `permissionMode` in the config type.
|
|
570
716
|
this.batchMode = this.resolvePermissionMode()
|
|
571
717
|
try {
|
|
572
|
-
|
|
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)
|
|
573
723
|
} finally {
|
|
574
724
|
// Cleared so a later single execution outside a batch resolves
|
|
575
725
|
// live rather than inheriting the last batch's sample.
|
|
@@ -581,6 +731,7 @@ export class ToolExecutor {
|
|
|
581
731
|
toolCalls: readonly ToolCall[],
|
|
582
732
|
denials?: ToolCallDenials,
|
|
583
733
|
prior?: PriorToolResults,
|
|
734
|
+
preparedBatch?: OwnedPreparedToolBatch,
|
|
584
735
|
): Promise<ToolExecutionBatch> {
|
|
585
736
|
this.log.debug('Executing tool batch', {
|
|
586
737
|
[NAMZU.RUN_ID]: this.config.runId,
|
|
@@ -615,6 +766,7 @@ export class ToolExecutor {
|
|
|
615
766
|
// order independence.
|
|
616
767
|
const gate = new Semaphore(this.config.maxToolConcurrency ?? DEFAULT_TOOL_CONCURRENCY)
|
|
617
768
|
toolCalls.forEach((toolCall, i) => {
|
|
769
|
+
const preparedCall = preparedBatch?.calls.get(toolCall.id)
|
|
618
770
|
const recovered = prior?.get(toolCall.id)
|
|
619
771
|
if (recovered !== undefined) {
|
|
620
772
|
// This call already ran, in a process that died before the
|
|
@@ -637,7 +789,7 @@ export class ToolExecutor {
|
|
|
637
789
|
// on the parallel branch — they perform no side effects, so
|
|
638
790
|
// serialization would only add latency.
|
|
639
791
|
parallel.push(
|
|
640
|
-
this.recordDenial(toolCall, denialReason).then((r) => {
|
|
792
|
+
this.recordDenial(toolCall, denialReason, preparedCall).then((r) => {
|
|
641
793
|
results[i] = r
|
|
642
794
|
}),
|
|
643
795
|
)
|
|
@@ -669,8 +821,12 @@ export class ToolExecutor {
|
|
|
669
821
|
}
|
|
670
822
|
const run = async () => {
|
|
671
823
|
try {
|
|
672
|
-
results[i] = await this.executeSingle(
|
|
673
|
-
|
|
824
|
+
results[i] = await this.executeSingle(
|
|
825
|
+
toolCall,
|
|
826
|
+
ctx,
|
|
827
|
+
recordObservation,
|
|
828
|
+
() => progress.close(),
|
|
829
|
+
preparedCall,
|
|
674
830
|
)
|
|
675
831
|
} finally {
|
|
676
832
|
await progress.close()
|
|
@@ -684,14 +840,19 @@ export class ToolExecutor {
|
|
|
684
840
|
gate.release()
|
|
685
841
|
}
|
|
686
842
|
}
|
|
687
|
-
let input: unknown = {}
|
|
843
|
+
let input: unknown = preparedCall?.input ?? {}
|
|
688
844
|
try {
|
|
689
|
-
|
|
845
|
+
if (!preparedBatch?.calls.has(toolCall.id)) {
|
|
846
|
+
input = JSON.parse(toolCall.function.arguments || '{}')
|
|
847
|
+
}
|
|
690
848
|
} catch {
|
|
691
849
|
// non-JSON args → treat as unsafe (serialize), the conservative path
|
|
692
850
|
}
|
|
851
|
+
const preparedToolName = preparedCall?.toolName
|
|
693
852
|
const safe =
|
|
694
|
-
this.config.tools
|
|
853
|
+
this.config.tools
|
|
854
|
+
.get(preparedToolName ?? toolCall.function.name)
|
|
855
|
+
?.isConcurrencySafe?.(input) === true
|
|
695
856
|
if (safe) parallel.push(gated())
|
|
696
857
|
else serial = serial.then(run)
|
|
697
858
|
})
|
|
@@ -754,6 +915,17 @@ export class ToolExecutor {
|
|
|
754
915
|
parentToolName?: string,
|
|
755
916
|
options?: ToolDispatchOptions,
|
|
756
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
|
+
|
|
757
929
|
const parent = context.toolUseId
|
|
758
930
|
const via =
|
|
759
931
|
parent && parentToolName
|
|
@@ -769,9 +941,6 @@ export class ToolExecutor {
|
|
|
769
941
|
// it rather than to itself.
|
|
770
942
|
const nestedId = generateToolCallId() as unknown as ToolUseId
|
|
771
943
|
const startedAt = Date.now()
|
|
772
|
-
const signal = options?.signal
|
|
773
|
-
? AbortSignal.any([context.abortSignal, options.signal])
|
|
774
|
-
: context.abortSignal
|
|
775
944
|
const source = parent
|
|
776
945
|
? options?.runtimeToolCallId
|
|
777
946
|
? {
|
|
@@ -787,6 +956,79 @@ export class ToolExecutor {
|
|
|
787
956
|
toolUseId: nestedId,
|
|
788
957
|
toolName: name,
|
|
789
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
|
+
|
|
790
1032
|
const childContext: ToolContext = {
|
|
791
1033
|
...context,
|
|
792
1034
|
abortSignal: signal,
|
|
@@ -815,16 +1057,51 @@ export class ToolExecutor {
|
|
|
815
1057
|
runId: this.config.runId,
|
|
816
1058
|
toolUseId: nestedId,
|
|
817
1059
|
toolName: name,
|
|
818
|
-
input,
|
|
1060
|
+
input: preparedInput,
|
|
819
1061
|
...(via ? { via } : {}),
|
|
820
1062
|
})
|
|
821
1063
|
|
|
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
|
+
|
|
822
1094
|
let result: ToolResult
|
|
823
1095
|
try {
|
|
824
1096
|
// Use the same deadline layer as a model-issued call. Calling the
|
|
825
1097
|
// registry directly made nested tools the only tools whose own
|
|
826
1098
|
// `timeoutMs` declaration was ignored.
|
|
827
|
-
result = await this.runOnce(
|
|
1099
|
+
result = await this.runOnce(
|
|
1100
|
+
name,
|
|
1101
|
+
preparedInput,
|
|
1102
|
+
childContext,
|
|
1103
|
+
preparedCall.kind === 'ready' ? preparedCall.prepared : undefined,
|
|
1104
|
+
)
|
|
828
1105
|
} finally {
|
|
829
1106
|
await progress.close()
|
|
830
1107
|
}
|
|
@@ -873,7 +1150,7 @@ export class ToolExecutor {
|
|
|
873
1150
|
runId: this.config.runId,
|
|
874
1151
|
toolUseId: nestedId,
|
|
875
1152
|
toolName: name,
|
|
876
|
-
input,
|
|
1153
|
+
input: preparedInput,
|
|
877
1154
|
result,
|
|
878
1155
|
...(parent ? { parentToolUseId: parent } : {}),
|
|
879
1156
|
})
|
|
@@ -906,6 +1183,7 @@ export class ToolExecutor {
|
|
|
906
1183
|
adoptSkillScope: (scope) => {
|
|
907
1184
|
this.skillScope = { ...scope, adoptedInBatch: this.batchCounter }
|
|
908
1185
|
},
|
|
1186
|
+
maxToolOutputChars: this.config.maxToolOutputChars ?? DEFAULT_MAX_TOOL_OUTPUT_CHARS,
|
|
909
1187
|
...(this.config.skills ? { skills: this.config.skills } : {}),
|
|
910
1188
|
...(this.config.web ? { web: this.config.web } : {}),
|
|
911
1189
|
// The SAME registry and the SAME context a model-issued call
|
|
@@ -948,99 +1226,111 @@ export class ToolExecutor {
|
|
|
948
1226
|
toolContext: ToolContext,
|
|
949
1227
|
recordObservation: (observation: ToolResultObservation) => void,
|
|
950
1228
|
settleProgress: () => Promise<void>,
|
|
1229
|
+
preparedCall?: PreparedDirectCall,
|
|
951
1230
|
): Promise<ToolCallOutcome> {
|
|
952
|
-
|
|
1231
|
+
if (preparedCall?.kind === 'synthetic') {
|
|
1232
|
+
return this.recordSyntheticPreparation(preparedCall)
|
|
1233
|
+
}
|
|
953
1234
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
// was answered with a generic hint while the configured repairer sat
|
|
958
|
-
// unused. Offer it the partial buffer first.
|
|
959
|
-
const truncationRepair =
|
|
960
|
-
toolCall.metadata?.inputTruncated === true
|
|
961
|
-
? await this.repairTruncatedCall(toolCall, toolName)
|
|
962
|
-
: null
|
|
1235
|
+
let toolName = preparedCall?.toolName ?? toolCall.function.name
|
|
1236
|
+
let input: unknown
|
|
1237
|
+
let prepared: PreparedToolExecution | undefined
|
|
963
1238
|
|
|
964
|
-
if (
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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
|
+
}
|
|
986
1276
|
}
|
|
987
|
-
}
|
|
988
1277
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
+
}
|
|
1034
1324
|
}
|
|
1035
|
-
}
|
|
1036
1325
|
|
|
1037
|
-
|
|
1326
|
+
input = resolved.input
|
|
1038
1327
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
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
|
|
1042
1333
|
}
|
|
1043
|
-
input = preOutcome.input
|
|
1044
1334
|
|
|
1045
1335
|
const activity = this.activityStore.create({
|
|
1046
1336
|
type: 'tool_call',
|
|
@@ -1128,7 +1418,7 @@ export class ToolExecutor {
|
|
|
1128
1418
|
// version silently DROPPED `content`, so a tool returning an image
|
|
1129
1419
|
// block had it discarded here — before the wire mapper that was
|
|
1130
1420
|
// built to carry it ever saw it.
|
|
1131
|
-
let result: ToolResult = await this.runOnce(toolName, input, toolContext)
|
|
1421
|
+
let result: ToolResult = await this.runOnce(toolName, input, toolContext, prepared)
|
|
1132
1422
|
let post = await this.runPostToolHook(toolName, input, result)
|
|
1133
1423
|
|
|
1134
1424
|
// In-loop retry. A transient failure used to cost a full model round
|
|
@@ -1197,7 +1487,7 @@ export class ToolExecutor {
|
|
|
1197
1487
|
break
|
|
1198
1488
|
}
|
|
1199
1489
|
|
|
1200
|
-
result = await this.runOnce(toolName, input, toolContext)
|
|
1490
|
+
result = await this.runOnce(toolName, input, toolContext, prepared)
|
|
1201
1491
|
post = await this.runPostToolHook(toolName, input, result)
|
|
1202
1492
|
}
|
|
1203
1493
|
const durationMs = Date.now() - startMs
|
|
@@ -1206,16 +1496,35 @@ export class ToolExecutor {
|
|
|
1206
1496
|
? result.output
|
|
1207
1497
|
: formatFailedToolOutput(result.output, result.error)
|
|
1208
1498
|
|
|
1209
|
-
|
|
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
|
+
}
|
|
1210
1519
|
|
|
1211
1520
|
// Compression is opportunistic and shell-only; the budget is the
|
|
1212
|
-
// hard bound that applies to every tool
|
|
1213
|
-
//
|
|
1521
|
+
// hard bound that applies to every final tool result, including a
|
|
1522
|
+
// post-tool hook's replacement and the rich-content omission notice.
|
|
1214
1523
|
const budgeted = applyToolOutputBudget({
|
|
1215
1524
|
toolName,
|
|
1216
1525
|
toolUseId: toolCall.id,
|
|
1217
1526
|
output,
|
|
1218
|
-
maxChars:
|
|
1527
|
+
maxChars: maxToolOutputChars,
|
|
1219
1528
|
spillDir: this.config.toolOutputDir,
|
|
1220
1529
|
onError: (message) =>
|
|
1221
1530
|
this.log.warn('Failed to spill oversized tool output', {
|
|
@@ -1234,22 +1543,6 @@ export class ToolExecutor {
|
|
|
1234
1543
|
}
|
|
1235
1544
|
output = budgeted.output
|
|
1236
1545
|
|
|
1237
|
-
// A truncated text half drops the rich half with it — the preview is
|
|
1238
|
-
// no longer the tool's own payload, so an image alongside it would be
|
|
1239
|
-
// illustrating something the model can no longer read. Dropping is
|
|
1240
|
-
// right; doing it SILENTLY is not. The model saw a preview and had no
|
|
1241
|
-
// way to know an image existed at all, so it reasoned as though the
|
|
1242
|
-
// tool had returned text only.
|
|
1243
|
-
if (budgeted.truncated && result.content !== undefined) {
|
|
1244
|
-
const dropped = describeDroppedContent(result.content)
|
|
1245
|
-
if (dropped) output = `${output}\n\n${dropped}`
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
const postOverride = post.override
|
|
1249
|
-
if (postOverride !== null) {
|
|
1250
|
-
output = postOverride.output
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
1546
|
// A failed call, or an override that says the call failed. A `replace`
|
|
1254
1547
|
// says the opposite, and reading it as a failure is what made redaction
|
|
1255
1548
|
// unusable: the model was told a successful call had gone wrong, and
|
|
@@ -1312,12 +1605,8 @@ export class ToolExecutor {
|
|
|
1312
1605
|
})
|
|
1313
1606
|
|
|
1314
1607
|
const resolveContent = (): { content?: ToolResultContent } => {
|
|
1315
|
-
if (
|
|
1316
|
-
|
|
1317
|
-
}
|
|
1318
|
-
if (postOverride?.isError) return {}
|
|
1319
|
-
if (budgeted.truncated || result.content === undefined) return {}
|
|
1320
|
-
return { content: this.budgetContent(result.content, toolName) }
|
|
1608
|
+
if (budgeted.truncated || selectedContent === undefined) return {}
|
|
1609
|
+
return { content: this.budgetContent(selectedContent, toolName) }
|
|
1321
1610
|
}
|
|
1322
1611
|
|
|
1323
1612
|
return {
|
|
@@ -1363,16 +1652,13 @@ export class ToolExecutor {
|
|
|
1363
1652
|
toolName: string,
|
|
1364
1653
|
input: unknown,
|
|
1365
1654
|
toolContext: ToolContext,
|
|
1655
|
+
prepared?: PreparedToolExecution,
|
|
1366
1656
|
): Promise<ToolResult> {
|
|
1367
1657
|
const timeoutMs =
|
|
1368
1658
|
this.config.tools.get(toolName)?.timeoutMs ??
|
|
1369
1659
|
this.config.toolTimeoutMs ??
|
|
1370
1660
|
DEFAULT_TOOL_TIMEOUT_MS
|
|
1371
1661
|
|
|
1372
|
-
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
1373
|
-
return this.config.tools.execute(toolName, input, toolContext)
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
1662
|
const controller = new AbortController()
|
|
1377
1663
|
const parentSignal = toolContext.abortSignal
|
|
1378
1664
|
const onParentAbort = () => controller.abort(parentSignal.reason)
|
|
@@ -1381,15 +1667,20 @@ export class ToolExecutor {
|
|
|
1381
1667
|
|
|
1382
1668
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
1383
1669
|
let timedOut = false
|
|
1670
|
+
let invocationOpen = true
|
|
1671
|
+
const nestedDispatches = new Set<Promise<ToolResult>>()
|
|
1384
1672
|
|
|
1385
1673
|
try {
|
|
1386
|
-
const expired =
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
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
|
|
1393
1684
|
|
|
1394
1685
|
const aborted = new Promise<'aborted'>((resolve) => {
|
|
1395
1686
|
if (controller.signal.aborted && !timedOut) {
|
|
@@ -1406,26 +1697,53 @@ export class ToolExecutor {
|
|
|
1406
1697
|
})
|
|
1407
1698
|
|
|
1408
1699
|
const inheritedDispatch = toolContext.dispatchTool
|
|
1409
|
-
const
|
|
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 = {
|
|
1410
1733
|
...toolContext,
|
|
1411
1734
|
abortSignal: controller.signal,
|
|
1412
|
-
...(
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
signal: options?.signal
|
|
1418
|
-
? AbortSignal.any([controller.signal, options.signal])
|
|
1419
|
-
: controller.signal,
|
|
1420
|
-
}),
|
|
1421
|
-
}
|
|
1422
|
-
: {}),
|
|
1423
|
-
})
|
|
1735
|
+
...(scopedDispatch ? { dispatchTool: scopedDispatch } : {}),
|
|
1736
|
+
}
|
|
1737
|
+
const execution = prepared
|
|
1738
|
+
? this.config.tools.executePrepared(prepared, context)
|
|
1739
|
+
: this.config.tools.execute(toolName, input, context)
|
|
1424
1740
|
// The loser of the race may still reject later; neutralize it so
|
|
1425
1741
|
// it is never an unhandled rejection.
|
|
1426
1742
|
execution.catch(() => {})
|
|
1427
1743
|
|
|
1428
|
-
const outcome = await Promise.race(
|
|
1744
|
+
const outcome = await Promise.race(
|
|
1745
|
+
expired ? [execution, expired, aborted] : [execution, aborted],
|
|
1746
|
+
)
|
|
1429
1747
|
|
|
1430
1748
|
if (outcome === 'timeout') {
|
|
1431
1749
|
this.log.warn('Tool timed out', {
|
|
@@ -1459,38 +1777,312 @@ export class ToolExecutor {
|
|
|
1459
1777
|
|
|
1460
1778
|
return outcome
|
|
1461
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
|
+
}
|
|
1462
1793
|
if (timer !== undefined) clearTimeout(timer)
|
|
1463
1794
|
parentSignal.removeEventListener('abort', onParentAbort)
|
|
1464
1795
|
}
|
|
1465
1796
|
}
|
|
1466
1797
|
|
|
1467
|
-
private async runPreToolHook(
|
|
1468
|
-
|
|
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 }
|
|
1469
1804
|
const results = await this.config.pluginManager.executeHooks(
|
|
1470
1805
|
'pre_tool_use',
|
|
1471
1806
|
{
|
|
1472
1807
|
runId: this.config.runId,
|
|
1473
1808
|
toolName,
|
|
1474
1809
|
toolInput: input,
|
|
1475
|
-
signal
|
|
1810
|
+
signal,
|
|
1476
1811
|
},
|
|
1477
1812
|
this.emitEvent,
|
|
1478
1813
|
)
|
|
1479
1814
|
return this.interpretPreToolResults(toolName, input, results)
|
|
1480
1815
|
}
|
|
1481
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
|
+
|
|
1482
2072
|
private interpretPreToolResults(
|
|
1483
2073
|
toolName: string,
|
|
1484
2074
|
initialInput: unknown,
|
|
1485
2075
|
results: readonly PluginHookResult[],
|
|
1486
2076
|
): PreToolHookOutcome {
|
|
1487
2077
|
let currentInput = initialInput
|
|
2078
|
+
let modified = false
|
|
1488
2079
|
for (const result of results) {
|
|
1489
2080
|
switch (result.action) {
|
|
1490
2081
|
case 'continue':
|
|
1491
2082
|
continue
|
|
1492
2083
|
case 'modify':
|
|
1493
2084
|
currentInput = result.input
|
|
2085
|
+
modified = true
|
|
1494
2086
|
continue
|
|
1495
2087
|
case 'skip':
|
|
1496
2088
|
return {
|
|
@@ -1519,7 +2111,7 @@ export class ToolExecutor {
|
|
|
1519
2111
|
}
|
|
1520
2112
|
}
|
|
1521
2113
|
}
|
|
1522
|
-
return { kind: 'continue', input: currentInput }
|
|
2114
|
+
return { kind: 'continue', input: currentInput, modified }
|
|
1523
2115
|
}
|
|
1524
2116
|
|
|
1525
2117
|
/**
|
|
@@ -1699,9 +2291,10 @@ export class ToolExecutor {
|
|
|
1699
2291
|
toolName: string,
|
|
1700
2292
|
input: unknown,
|
|
1701
2293
|
toolContext: ToolContext,
|
|
2294
|
+
prepared?: PreparedToolExecution,
|
|
1702
2295
|
): Promise<ToolResult> {
|
|
1703
2296
|
try {
|
|
1704
|
-
return await this.executeWithDeadline(toolName, input, toolContext)
|
|
2297
|
+
return await this.executeWithDeadline(toolName, input, toolContext, prepared)
|
|
1705
2298
|
} catch (err) {
|
|
1706
2299
|
const message = toErrorMessage(err)
|
|
1707
2300
|
this.log.warn('Tool execution threw', {
|
|
@@ -1782,13 +2375,19 @@ export class ToolExecutor {
|
|
|
1782
2375
|
* execution so UI cards reach a terminal state instead of hanging in
|
|
1783
2376
|
* `executing`, and records a failed activity for the trace.
|
|
1784
2377
|
*/
|
|
1785
|
-
private async recordDenial(
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
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
|
+
}
|
|
1792
2391
|
}
|
|
1793
2392
|
|
|
1794
2393
|
const output = deniedToolOutput(toolName, reason)
|
|
@@ -1875,6 +2474,13 @@ export class ToolExecutor {
|
|
|
1875
2474
|
}
|
|
1876
2475
|
}
|
|
1877
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
|
+
|
|
1878
2484
|
/**
|
|
1879
2485
|
* Bound the rich channel, or leave it alone when no cap is configured.
|
|
1880
2486
|
*
|