@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
|
@@ -14,6 +14,15 @@ import { generateToolCallId } from '../../utils/id.js';
|
|
|
14
14
|
import { compressShellOutput } from '../../utils/shell-compress.js';
|
|
15
15
|
import { bindOwner } from '../jobs/registry.js';
|
|
16
16
|
import { DEFAULT_MAX_TOOL_OUTPUT_CHARS, applyToolOutputBudget, describeDroppedContent, measureContentBytes, } from './tool-output-budget.js';
|
|
17
|
+
function assertUniqueToolCallIds(toolCalls) {
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
for (const [index, toolCall] of toolCalls.entries()) {
|
|
20
|
+
if (seen.has(toolCall.id)) {
|
|
21
|
+
throw new Error(`Provider returned duplicate tool call id "${toolCall.id}" at batch index ${index}; the batch is refused because review, denial and result ownership require one unique id per call.`);
|
|
22
|
+
}
|
|
23
|
+
seen.add(toolCall.id);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
17
26
|
/**
|
|
18
27
|
* Default per-tool deadline. Long enough for a real build or test run,
|
|
19
28
|
* short enough that a wedged tool does not hold a turn open indefinitely.
|
|
@@ -183,6 +192,7 @@ export class ToolExecutor {
|
|
|
183
192
|
workingStateManager;
|
|
184
193
|
probes;
|
|
185
194
|
parentSpan;
|
|
195
|
+
preparedBatches = new WeakSet();
|
|
186
196
|
/** Set per turn by the orchestrator; see {@link setStepAllowedTools}. */
|
|
187
197
|
stepAllowedTools;
|
|
188
198
|
readPaths = new Set();
|
|
@@ -319,17 +329,72 @@ export class ToolExecutor {
|
|
|
319
329
|
const configured = this.config.permissionMode;
|
|
320
330
|
return typeof configured === 'function' ? configured() : configured;
|
|
321
331
|
}
|
|
322
|
-
|
|
332
|
+
/** Evaluate the run's operator policy against one already-prepared value. */
|
|
333
|
+
evaluatePreparedAuthorization(toolName, input) {
|
|
334
|
+
return this.config.authorizationGate?.evaluate({
|
|
335
|
+
toolName,
|
|
336
|
+
toolInput: input,
|
|
337
|
+
toolDef: this.config.tools.get(toolName),
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Resolve repairs and pre-tool hooks, then decode each call exactly once.
|
|
342
|
+
* The returned projection is what policy and a human review; execution later
|
|
343
|
+
* consumes the registry-owned preparations rather than parsing again.
|
|
344
|
+
*/
|
|
345
|
+
async prepareBatchForReview(response) {
|
|
346
|
+
assertUniqueToolCallIds(response.message.toolCalls ?? []);
|
|
347
|
+
const calls = new Map();
|
|
348
|
+
for (const toolCall of response.message.toolCalls ?? []) {
|
|
349
|
+
calls.set(toolCall.id, await this.prepareDirectCall(toolCall));
|
|
350
|
+
}
|
|
351
|
+
return this.publishPreparedBatch(calls);
|
|
352
|
+
}
|
|
353
|
+
/** Re-prepare only calls whose raw input a reviewer actually changed. */
|
|
354
|
+
async reprepareBatchForReview(response, previous, changedCallIds) {
|
|
355
|
+
assertUniqueToolCallIds(response.message.toolCalls ?? []);
|
|
356
|
+
if (!this.preparedBatches.has(previous)) {
|
|
357
|
+
throw new Error('Prepared tool batch is not owned by this executor.');
|
|
358
|
+
}
|
|
359
|
+
const calls = new Map(previous.calls);
|
|
360
|
+
for (const toolCall of response.message.toolCalls ?? []) {
|
|
361
|
+
if (changedCallIds.has(toolCall.id)) {
|
|
362
|
+
calls.set(toolCall.id, await this.prepareDirectCall(toolCall));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return this.publishPreparedBatch(calls);
|
|
366
|
+
}
|
|
367
|
+
publishPreparedBatch(calls) {
|
|
368
|
+
const reviewCalls = [...calls.values()]
|
|
369
|
+
.filter((call) => call.kind !== 'synthetic')
|
|
370
|
+
.map((call) => ({
|
|
371
|
+
id: call.toolCall.id,
|
|
372
|
+
name: call.toolName,
|
|
373
|
+
input: call.input,
|
|
374
|
+
}));
|
|
375
|
+
const batch = Object.freeze({
|
|
376
|
+
reviewCalls: Object.freeze(reviewCalls),
|
|
377
|
+
calls: new Map(calls),
|
|
378
|
+
});
|
|
379
|
+
this.preparedBatches.add(batch);
|
|
380
|
+
return batch;
|
|
381
|
+
}
|
|
382
|
+
async executeBatch(response, denials, prior, preparedBatch) {
|
|
323
383
|
const toolCalls = response.message.toolCalls;
|
|
324
384
|
if (!toolCalls) {
|
|
325
385
|
return { messages: [], results: [], observations: [] };
|
|
326
386
|
}
|
|
387
|
+
assertUniqueToolCallIds(toolCalls);
|
|
327
388
|
this.batchCounter += 1;
|
|
328
389
|
// Sampled here, once, and held for every call below. See the note on
|
|
329
390
|
// `permissionMode` in the config type.
|
|
330
391
|
this.batchMode = this.resolvePermissionMode();
|
|
331
392
|
try {
|
|
332
|
-
|
|
393
|
+
const owned = preparedBatch;
|
|
394
|
+
if (owned && !this.preparedBatches.has(owned)) {
|
|
395
|
+
throw new Error('Prepared tool batch is not owned by this executor.');
|
|
396
|
+
}
|
|
397
|
+
return await this.runBatch(toolCalls, denials, prior, owned);
|
|
333
398
|
}
|
|
334
399
|
finally {
|
|
335
400
|
// Cleared so a later single execution outside a batch resolves
|
|
@@ -337,7 +402,7 @@ export class ToolExecutor {
|
|
|
337
402
|
this.batchMode = undefined;
|
|
338
403
|
}
|
|
339
404
|
}
|
|
340
|
-
async runBatch(toolCalls, denials, prior) {
|
|
405
|
+
async runBatch(toolCalls, denials, prior, preparedBatch) {
|
|
341
406
|
this.log.debug('Executing tool batch', {
|
|
342
407
|
[NAMZU.RUN_ID]: this.config.runId,
|
|
343
408
|
'namzu.runtime.tool_count': toolCalls.length,
|
|
@@ -369,6 +434,7 @@ export class ToolExecutor {
|
|
|
369
434
|
// order independence.
|
|
370
435
|
const gate = new Semaphore(this.config.maxToolConcurrency ?? DEFAULT_TOOL_CONCURRENCY);
|
|
371
436
|
toolCalls.forEach((toolCall, i) => {
|
|
437
|
+
const preparedCall = preparedBatch?.calls.get(toolCall.id);
|
|
372
438
|
const recovered = prior?.get(toolCall.id);
|
|
373
439
|
if (recovered !== undefined) {
|
|
374
440
|
// This call already ran, in a process that died before the
|
|
@@ -389,7 +455,7 @@ export class ToolExecutor {
|
|
|
389
455
|
// message so the assistant turn stays fully answered. Run them
|
|
390
456
|
// on the parallel branch — they perform no side effects, so
|
|
391
457
|
// serialization would only add latency.
|
|
392
|
-
parallel.push(this.recordDenial(toolCall, denialReason).then((r) => {
|
|
458
|
+
parallel.push(this.recordDenial(toolCall, denialReason, preparedCall).then((r) => {
|
|
393
459
|
results[i] = r;
|
|
394
460
|
}));
|
|
395
461
|
return;
|
|
@@ -419,7 +485,7 @@ export class ToolExecutor {
|
|
|
419
485
|
};
|
|
420
486
|
const run = async () => {
|
|
421
487
|
try {
|
|
422
|
-
results[i] = await this.executeSingle(toolCall, ctx, recordObservation, () => progress.close());
|
|
488
|
+
results[i] = await this.executeSingle(toolCall, ctx, recordObservation, () => progress.close(), preparedCall);
|
|
423
489
|
}
|
|
424
490
|
finally {
|
|
425
491
|
await progress.close();
|
|
@@ -434,14 +500,19 @@ export class ToolExecutor {
|
|
|
434
500
|
gate.release();
|
|
435
501
|
}
|
|
436
502
|
};
|
|
437
|
-
let input = {};
|
|
503
|
+
let input = preparedCall?.input ?? {};
|
|
438
504
|
try {
|
|
439
|
-
|
|
505
|
+
if (!preparedBatch?.calls.has(toolCall.id)) {
|
|
506
|
+
input = JSON.parse(toolCall.function.arguments || '{}');
|
|
507
|
+
}
|
|
440
508
|
}
|
|
441
509
|
catch {
|
|
442
510
|
// non-JSON args → treat as unsafe (serialize), the conservative path
|
|
443
511
|
}
|
|
444
|
-
const
|
|
512
|
+
const preparedToolName = preparedCall?.toolName;
|
|
513
|
+
const safe = this.config.tools
|
|
514
|
+
.get(preparedToolName ?? toolCall.function.name)
|
|
515
|
+
?.isConcurrencySafe?.(input) === true;
|
|
445
516
|
if (safe)
|
|
446
517
|
parallel.push(gated());
|
|
447
518
|
else
|
|
@@ -494,6 +565,16 @@ export class ToolExecutor {
|
|
|
494
565
|
* eleven children.
|
|
495
566
|
*/
|
|
496
567
|
async dispatchNested(name, input, context, recordObservation, parentToolName, options) {
|
|
568
|
+
const signal = options?.signal
|
|
569
|
+
? AbortSignal.any([context.abortSignal, options.signal])
|
|
570
|
+
: context.abortSignal;
|
|
571
|
+
// Authority is checked before an id, activity or event is created. A
|
|
572
|
+
// retained closure must be observationally inert after its invocation
|
|
573
|
+
// ends, not merely unable to finish the registry call it already started.
|
|
574
|
+
signal.throwIfAborted();
|
|
575
|
+
const preparedCall = await this.prepareNestedCall(name, input, signal);
|
|
576
|
+
signal.throwIfAborted();
|
|
577
|
+
const preparedInput = preparedCall.input;
|
|
497
578
|
const parent = context.toolUseId;
|
|
498
579
|
const via = parent && parentToolName
|
|
499
580
|
? {
|
|
@@ -508,9 +589,6 @@ export class ToolExecutor {
|
|
|
508
589
|
// it rather than to itself.
|
|
509
590
|
const nestedId = generateToolCallId();
|
|
510
591
|
const startedAt = Date.now();
|
|
511
|
-
const signal = options?.signal
|
|
512
|
-
? AbortSignal.any([context.abortSignal, options.signal])
|
|
513
|
-
: context.abortSignal;
|
|
514
592
|
const source = parent
|
|
515
593
|
? options?.runtimeToolCallId
|
|
516
594
|
? {
|
|
@@ -526,6 +604,74 @@ export class ToolExecutor {
|
|
|
526
604
|
toolUseId: nestedId,
|
|
527
605
|
toolName: name,
|
|
528
606
|
});
|
|
607
|
+
if (preparedCall.kind === 'synthetic') {
|
|
608
|
+
await this.emitEvent({
|
|
609
|
+
type: 'tool_executing',
|
|
610
|
+
runId: this.config.runId,
|
|
611
|
+
toolUseId: nestedId,
|
|
612
|
+
toolName: name,
|
|
613
|
+
input: preparedInput,
|
|
614
|
+
...(via ? { via } : {}),
|
|
615
|
+
});
|
|
616
|
+
await progress.close();
|
|
617
|
+
await this.emitEvent({
|
|
618
|
+
type: 'tool_completed',
|
|
619
|
+
runId: this.config.runId,
|
|
620
|
+
toolUseId: nestedId,
|
|
621
|
+
toolName: name,
|
|
622
|
+
result: preparedCall.message,
|
|
623
|
+
isError: preparedCall.isError,
|
|
624
|
+
durationMs: Date.now() - startedAt,
|
|
625
|
+
outputLength: preparedCall.message.length,
|
|
626
|
+
...(via ? { via } : {}),
|
|
627
|
+
});
|
|
628
|
+
return preparedCall.isError
|
|
629
|
+
? { success: false, output: '', error: preparedCall.message }
|
|
630
|
+
: { success: true, output: preparedCall.message };
|
|
631
|
+
}
|
|
632
|
+
const gateResult = this.config.authorizationGate?.evaluate({
|
|
633
|
+
toolName: name,
|
|
634
|
+
toolInput: preparedInput,
|
|
635
|
+
toolDef: this.config.tools.get(name),
|
|
636
|
+
});
|
|
637
|
+
if (gateResult && gateResult.decision !== 'allow') {
|
|
638
|
+
const reason = gateResult.decision === 'deny'
|
|
639
|
+
? `Blocked by the authorization gate: ${gateResult.reason}`
|
|
640
|
+
: `Blocked by the authorization gate: this nested call requires an explicit allow rule because an operator review cannot be opened from inside another tool. ${gateResult.reason}`;
|
|
641
|
+
const output = deniedToolOutput(name, reason);
|
|
642
|
+
// Same fail-closed durability rule as a direct gate denial: if the
|
|
643
|
+
// configured run store cannot record the refusal, do not quietly carry
|
|
644
|
+
// on with an unaudited execution.
|
|
645
|
+
if (!this.config.recordAudit) {
|
|
646
|
+
throw new Error(`Nested tool "${name}" was refused, but no durable audit recorder is configured.`);
|
|
647
|
+
}
|
|
648
|
+
await this.config.recordAudit({
|
|
649
|
+
what: { action: 'tool_call', tool: name },
|
|
650
|
+
outcome: 'refused',
|
|
651
|
+
reason,
|
|
652
|
+
});
|
|
653
|
+
await progress.close();
|
|
654
|
+
await this.emitEvent({
|
|
655
|
+
type: 'tool_executing',
|
|
656
|
+
runId: this.config.runId,
|
|
657
|
+
toolUseId: nestedId,
|
|
658
|
+
toolName: name,
|
|
659
|
+
input: preparedInput,
|
|
660
|
+
...(via ? { via } : {}),
|
|
661
|
+
});
|
|
662
|
+
await this.emitEvent({
|
|
663
|
+
type: 'tool_completed',
|
|
664
|
+
runId: this.config.runId,
|
|
665
|
+
toolUseId: nestedId,
|
|
666
|
+
toolName: name,
|
|
667
|
+
result: output,
|
|
668
|
+
isError: true,
|
|
669
|
+
durationMs: Date.now() - startedAt,
|
|
670
|
+
outputLength: output.length,
|
|
671
|
+
...(via ? { via } : {}),
|
|
672
|
+
});
|
|
673
|
+
return { success: false, output: '', error: reason };
|
|
674
|
+
}
|
|
529
675
|
const childContext = {
|
|
530
676
|
...context,
|
|
531
677
|
abortSignal: signal,
|
|
@@ -545,15 +691,41 @@ export class ToolExecutor {
|
|
|
545
691
|
runId: this.config.runId,
|
|
546
692
|
toolUseId: nestedId,
|
|
547
693
|
toolName: name,
|
|
548
|
-
input,
|
|
694
|
+
input: preparedInput,
|
|
549
695
|
...(via ? { via } : {}),
|
|
550
696
|
});
|
|
697
|
+
const vetoOutcome = this.probes.queryVeto({
|
|
698
|
+
type: 'tool_executing',
|
|
699
|
+
runId: this.config.runId,
|
|
700
|
+
toolUseId: nestedId,
|
|
701
|
+
toolName: name,
|
|
702
|
+
input: preparedInput,
|
|
703
|
+
...(via ? { via } : {}),
|
|
704
|
+
}, buildProbeContext({ runId: this.config.runId }));
|
|
705
|
+
if (vetoOutcome.action === 'deny') {
|
|
706
|
+
const probeName = vetoOutcome.probeName ?? 'unnamed';
|
|
707
|
+
const reason = vetoOutcome.reason ?? 'no reason provided';
|
|
708
|
+
const message = new ProbeVetoError(probeName, reason, 'tool_executing').message;
|
|
709
|
+
await progress.close();
|
|
710
|
+
await this.emitEvent({
|
|
711
|
+
type: 'tool_completed',
|
|
712
|
+
runId: this.config.runId,
|
|
713
|
+
toolUseId: nestedId,
|
|
714
|
+
toolName: name,
|
|
715
|
+
result: `Error: ${message}`,
|
|
716
|
+
isError: true,
|
|
717
|
+
durationMs: Date.now() - startedAt,
|
|
718
|
+
outputLength: message.length + 7,
|
|
719
|
+
...(via ? { via } : {}),
|
|
720
|
+
});
|
|
721
|
+
return { success: false, output: '', error: message };
|
|
722
|
+
}
|
|
551
723
|
let result;
|
|
552
724
|
try {
|
|
553
725
|
// Use the same deadline layer as a model-issued call. Calling the
|
|
554
726
|
// registry directly made nested tools the only tools whose own
|
|
555
727
|
// `timeoutMs` declaration was ignored.
|
|
556
|
-
result = await this.runOnce(name,
|
|
728
|
+
result = await this.runOnce(name, preparedInput, childContext, preparedCall.kind === 'ready' ? preparedCall.prepared : undefined);
|
|
557
729
|
}
|
|
558
730
|
finally {
|
|
559
731
|
await progress.close();
|
|
@@ -600,7 +772,7 @@ export class ToolExecutor {
|
|
|
600
772
|
runId: this.config.runId,
|
|
601
773
|
toolUseId: nestedId,
|
|
602
774
|
toolName: name,
|
|
603
|
-
input,
|
|
775
|
+
input: preparedInput,
|
|
604
776
|
result,
|
|
605
777
|
...(parent ? { parentToolUseId: parent } : {}),
|
|
606
778
|
});
|
|
@@ -629,6 +801,7 @@ export class ToolExecutor {
|
|
|
629
801
|
adoptSkillScope: (scope) => {
|
|
630
802
|
this.skillScope = { ...scope, adoptedInBatch: this.batchCounter };
|
|
631
803
|
},
|
|
804
|
+
maxToolOutputChars: this.config.maxToolOutputChars ?? DEFAULT_MAX_TOOL_OUTPUT_CHARS,
|
|
632
805
|
...(this.config.skills ? { skills: this.config.skills } : {}),
|
|
633
806
|
...(this.config.web ? { web: this.config.web } : {}),
|
|
634
807
|
// The SAME registry and the SAME context a model-issued call
|
|
@@ -664,90 +837,101 @@ export class ToolExecutor {
|
|
|
664
837
|
};
|
|
665
838
|
return context;
|
|
666
839
|
}
|
|
667
|
-
async executeSingle(toolCall, toolContext, recordObservation, settleProgress) {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
// for, and it used to be the one case that never reached it: this
|
|
671
|
-
// branch returned before `resolveCall` ran, so the motivating failure
|
|
672
|
-
// was answered with a generic hint while the configured repairer sat
|
|
673
|
-
// unused. Offer it the partial buffer first.
|
|
674
|
-
const truncationRepair = toolCall.metadata?.inputTruncated === true
|
|
675
|
-
? await this.repairTruncatedCall(toolCall, toolName)
|
|
676
|
-
: null;
|
|
677
|
-
if (toolCall.metadata?.inputTruncated === true && !truncationRepair) {
|
|
678
|
-
const message = truncatedToolInputMessage(toolName);
|
|
679
|
-
await this.emitEvent({
|
|
680
|
-
type: 'tool_executing',
|
|
681
|
-
runId: this.config.runId,
|
|
682
|
-
toolUseId: toolCall.id,
|
|
683
|
-
toolName,
|
|
684
|
-
input: {},
|
|
685
|
-
});
|
|
686
|
-
await this.emitEvent({
|
|
687
|
-
type: 'tool_completed',
|
|
688
|
-
runId: this.config.runId,
|
|
689
|
-
toolUseId: toolCall.id,
|
|
690
|
-
toolName,
|
|
691
|
-
result: message,
|
|
692
|
-
isError: true,
|
|
693
|
-
});
|
|
694
|
-
return {
|
|
695
|
-
toolCallId: toolCall.id,
|
|
696
|
-
toolName,
|
|
697
|
-
output: message,
|
|
698
|
-
isError: true,
|
|
699
|
-
};
|
|
840
|
+
async executeSingle(toolCall, toolContext, recordObservation, settleProgress, preparedCall) {
|
|
841
|
+
if (preparedCall?.kind === 'synthetic') {
|
|
842
|
+
return this.recordSyntheticPreparation(preparedCall);
|
|
700
843
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
?
|
|
707
|
-
...toolCall,
|
|
708
|
-
function: {
|
|
709
|
-
...toolCall.function,
|
|
710
|
-
name: truncationRepair.toolName ?? toolName,
|
|
711
|
-
arguments: truncationRepair.arguments,
|
|
712
|
-
},
|
|
713
|
-
metadata: {},
|
|
714
|
-
}
|
|
715
|
-
: toolCall);
|
|
716
|
-
toolName = resolved.toolName;
|
|
717
|
-
if (!resolved.ok) {
|
|
718
|
-
// malformed JSON args used to return without ever
|
|
719
|
-
// emitting tool_executing or tool_completed, leaving UI cards
|
|
720
|
-
// orphaned in `streaming_input`. Emit the executing→completed
|
|
721
|
-
// terminal pair so the card lifecycle closes.
|
|
722
|
-
const message = resolved.message;
|
|
723
|
-
await this.emitEvent({
|
|
724
|
-
type: 'tool_executing',
|
|
725
|
-
runId: this.config.runId,
|
|
726
|
-
toolUseId: toolCall.id,
|
|
727
|
-
toolName,
|
|
728
|
-
input: {},
|
|
729
|
-
});
|
|
730
|
-
await this.emitEvent({
|
|
731
|
-
type: 'tool_completed',
|
|
732
|
-
runId: this.config.runId,
|
|
733
|
-
toolUseId: toolCall.id,
|
|
734
|
-
toolName,
|
|
735
|
-
result: message,
|
|
736
|
-
isError: true,
|
|
737
|
-
});
|
|
738
|
-
return {
|
|
739
|
-
toolCallId: toolCall.id,
|
|
740
|
-
toolName,
|
|
741
|
-
output: message,
|
|
742
|
-
isError: true,
|
|
743
|
-
};
|
|
844
|
+
let toolName = preparedCall?.toolName ?? toolCall.function.name;
|
|
845
|
+
let input;
|
|
846
|
+
let prepared;
|
|
847
|
+
if (preparedCall?.kind === 'ready' || preparedCall?.kind === 'legacy') {
|
|
848
|
+
input = preparedCall.input;
|
|
849
|
+
prepared = preparedCall.kind === 'ready' ? preparedCall.prepared : undefined;
|
|
744
850
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
851
|
+
else {
|
|
852
|
+
// A stream that cut off mid-JSON is the case `repairToolCall` exists
|
|
853
|
+
// for, and it used to be the one case that never reached it: this
|
|
854
|
+
// branch returned before `resolveCall` ran, so the motivating failure
|
|
855
|
+
// was answered with a generic hint while the configured repairer sat
|
|
856
|
+
// unused. Offer it the partial buffer first.
|
|
857
|
+
const truncationRepair = toolCall.metadata?.inputTruncated === true
|
|
858
|
+
? await this.repairTruncatedCall(toolCall, toolName)
|
|
859
|
+
: null;
|
|
860
|
+
if (toolCall.metadata?.inputTruncated === true && !truncationRepair) {
|
|
861
|
+
const message = truncatedToolInputMessage(toolName);
|
|
862
|
+
await this.emitEvent({
|
|
863
|
+
type: 'tool_executing',
|
|
864
|
+
runId: this.config.runId,
|
|
865
|
+
toolUseId: toolCall.id,
|
|
866
|
+
toolName,
|
|
867
|
+
input: {},
|
|
868
|
+
});
|
|
869
|
+
await this.emitEvent({
|
|
870
|
+
type: 'tool_completed',
|
|
871
|
+
runId: this.config.runId,
|
|
872
|
+
toolUseId: toolCall.id,
|
|
873
|
+
toolName,
|
|
874
|
+
result: message,
|
|
875
|
+
isError: true,
|
|
876
|
+
});
|
|
877
|
+
return {
|
|
878
|
+
toolCallId: toolCall.id,
|
|
879
|
+
toolName,
|
|
880
|
+
output: message,
|
|
881
|
+
isError: true,
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
// A malformed call used to cost a full model round trip to fix: the
|
|
885
|
+
// error went back as a `tool_result`, the model re-read the whole
|
|
886
|
+
// context and tried again. A host that can repair it locally turns
|
|
887
|
+
// that into nothing. No-op when no repairer is configured.
|
|
888
|
+
const resolved = await this.resolveCall(truncationRepair
|
|
889
|
+
? {
|
|
890
|
+
...toolCall,
|
|
891
|
+
function: {
|
|
892
|
+
...toolCall.function,
|
|
893
|
+
name: truncationRepair.toolName ?? toolName,
|
|
894
|
+
arguments: truncationRepair.arguments,
|
|
895
|
+
},
|
|
896
|
+
metadata: {},
|
|
897
|
+
}
|
|
898
|
+
: toolCall);
|
|
899
|
+
toolName = resolved.toolName;
|
|
900
|
+
if (!resolved.ok) {
|
|
901
|
+
// malformed JSON args used to return without ever
|
|
902
|
+
// emitting tool_executing or tool_completed, leaving UI cards
|
|
903
|
+
// orphaned in `streaming_input`. Emit the executing→completed
|
|
904
|
+
// terminal pair so the card lifecycle closes.
|
|
905
|
+
const message = resolved.message;
|
|
906
|
+
await this.emitEvent({
|
|
907
|
+
type: 'tool_executing',
|
|
908
|
+
runId: this.config.runId,
|
|
909
|
+
toolUseId: toolCall.id,
|
|
910
|
+
toolName,
|
|
911
|
+
input: {},
|
|
912
|
+
});
|
|
913
|
+
await this.emitEvent({
|
|
914
|
+
type: 'tool_completed',
|
|
915
|
+
runId: this.config.runId,
|
|
916
|
+
toolUseId: toolCall.id,
|
|
917
|
+
toolName,
|
|
918
|
+
result: message,
|
|
919
|
+
isError: true,
|
|
920
|
+
});
|
|
921
|
+
return {
|
|
922
|
+
toolCallId: toolCall.id,
|
|
923
|
+
toolName,
|
|
924
|
+
output: message,
|
|
925
|
+
isError: true,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
input = resolved.input;
|
|
929
|
+
const preOutcome = await this.runPreToolHook(toolName, input);
|
|
930
|
+
if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
|
|
931
|
+
return this.recordSyntheticHookOutcome(toolCall.id, toolName, preOutcome.input, preOutcome);
|
|
932
|
+
}
|
|
933
|
+
input = preOutcome.input;
|
|
749
934
|
}
|
|
750
|
-
input = preOutcome.input;
|
|
751
935
|
const activity = this.activityStore.create({
|
|
752
936
|
type: 'tool_call',
|
|
753
937
|
description: toolName,
|
|
@@ -827,7 +1011,7 @@ export class ToolExecutor {
|
|
|
827
1011
|
// version silently DROPPED `content`, so a tool returning an image
|
|
828
1012
|
// block had it discarded here — before the wire mapper that was
|
|
829
1013
|
// built to carry it ever saw it.
|
|
830
|
-
let result = await this.runOnce(toolName, input, toolContext);
|
|
1014
|
+
let result = await this.runOnce(toolName, input, toolContext, prepared);
|
|
831
1015
|
let post = await this.runPostToolHook(toolName, input, result);
|
|
832
1016
|
// In-loop retry. A transient failure used to cost a full model round
|
|
833
1017
|
// trip: the error went back as a `tool_result`, the model read it and
|
|
@@ -894,22 +1078,38 @@ export class ToolExecutor {
|
|
|
894
1078
|
// resume to trip over.
|
|
895
1079
|
break;
|
|
896
1080
|
}
|
|
897
|
-
result = await this.runOnce(toolName, input, toolContext);
|
|
1081
|
+
result = await this.runOnce(toolName, input, toolContext, prepared);
|
|
898
1082
|
post = await this.runPostToolHook(toolName, input, result);
|
|
899
1083
|
}
|
|
900
1084
|
const durationMs = Date.now() - startMs;
|
|
901
1085
|
const rawOutput = result.success
|
|
902
1086
|
? result.output
|
|
903
1087
|
: formatFailedToolOutput(result.output, result.error);
|
|
904
|
-
|
|
1088
|
+
const postOverride = post.override;
|
|
1089
|
+
let output = postOverride?.output ?? (result.success ? this.maybeCompress(toolName, rawOutput) : rawOutput);
|
|
1090
|
+
const selectedContent = postOverride?.isError
|
|
1091
|
+
? undefined
|
|
1092
|
+
: (postOverride?.content ?? result.content);
|
|
1093
|
+
const maxToolOutputChars = this.config.maxToolOutputChars ?? DEFAULT_MAX_TOOL_OUTPUT_CHARS;
|
|
1094
|
+
// If the text preview forces rich content out, say so inside the SAME
|
|
1095
|
+
// budget. Appending this after truncation made the diagnostic itself a
|
|
1096
|
+
// cap bypass; selecting a post-hook override after truncation was a
|
|
1097
|
+
// larger bypass that could replace a bounded result with anything.
|
|
1098
|
+
if (maxToolOutputChars > 0 &&
|
|
1099
|
+
output.length > maxToolOutputChars &&
|
|
1100
|
+
selectedContent !== undefined) {
|
|
1101
|
+
const dropped = describeDroppedContent(selectedContent);
|
|
1102
|
+
if (dropped)
|
|
1103
|
+
output = `${output}\n\n${dropped}`;
|
|
1104
|
+
}
|
|
905
1105
|
// Compression is opportunistic and shell-only; the budget is the
|
|
906
|
-
// hard bound that applies to every tool
|
|
907
|
-
//
|
|
1106
|
+
// hard bound that applies to every final tool result, including a
|
|
1107
|
+
// post-tool hook's replacement and the rich-content omission notice.
|
|
908
1108
|
const budgeted = applyToolOutputBudget({
|
|
909
1109
|
toolName,
|
|
910
1110
|
toolUseId: toolCall.id,
|
|
911
1111
|
output,
|
|
912
|
-
maxChars:
|
|
1112
|
+
maxChars: maxToolOutputChars,
|
|
913
1113
|
spillDir: this.config.toolOutputDir,
|
|
914
1114
|
onError: (message) => this.log.warn('Failed to spill oversized tool output', {
|
|
915
1115
|
[NAMZU.RUN_ID]: this.config.runId,
|
|
@@ -926,21 +1126,6 @@ export class ToolExecutor {
|
|
|
926
1126
|
});
|
|
927
1127
|
}
|
|
928
1128
|
output = budgeted.output;
|
|
929
|
-
// A truncated text half drops the rich half with it — the preview is
|
|
930
|
-
// no longer the tool's own payload, so an image alongside it would be
|
|
931
|
-
// illustrating something the model can no longer read. Dropping is
|
|
932
|
-
// right; doing it SILENTLY is not. The model saw a preview and had no
|
|
933
|
-
// way to know an image existed at all, so it reasoned as though the
|
|
934
|
-
// tool had returned text only.
|
|
935
|
-
if (budgeted.truncated && result.content !== undefined) {
|
|
936
|
-
const dropped = describeDroppedContent(result.content);
|
|
937
|
-
if (dropped)
|
|
938
|
-
output = `${output}\n\n${dropped}`;
|
|
939
|
-
}
|
|
940
|
-
const postOverride = post.override;
|
|
941
|
-
if (postOverride !== null) {
|
|
942
|
-
output = postOverride.output;
|
|
943
|
-
}
|
|
944
1129
|
// A failed call, or an override that says the call failed. A `replace`
|
|
945
1130
|
// says the opposite, and reading it as a failure is what made redaction
|
|
946
1131
|
// unusable: the model was told a successful call had gone wrong, and
|
|
@@ -1000,14 +1185,9 @@ export class ToolExecutor {
|
|
|
1000
1185
|
result,
|
|
1001
1186
|
});
|
|
1002
1187
|
const resolveContent = () => {
|
|
1003
|
-
if (
|
|
1004
|
-
return { content: this.budgetContent(postOverride.content, toolName) };
|
|
1005
|
-
}
|
|
1006
|
-
if (postOverride?.isError)
|
|
1188
|
+
if (budgeted.truncated || selectedContent === undefined)
|
|
1007
1189
|
return {};
|
|
1008
|
-
|
|
1009
|
-
return {};
|
|
1010
|
-
return { content: this.budgetContent(result.content, toolName) };
|
|
1190
|
+
return { content: this.budgetContent(selectedContent, toolName) };
|
|
1011
1191
|
};
|
|
1012
1192
|
return {
|
|
1013
1193
|
toolCallId: toolCall.id,
|
|
@@ -1047,13 +1227,10 @@ export class ToolExecutor {
|
|
|
1047
1227
|
* model sees "this timed out" as a `tool_result` and can route around
|
|
1048
1228
|
* it. A throw would end the run over one slow tool.
|
|
1049
1229
|
*/
|
|
1050
|
-
async executeWithDeadline(toolName, input, toolContext) {
|
|
1230
|
+
async executeWithDeadline(toolName, input, toolContext, prepared) {
|
|
1051
1231
|
const timeoutMs = this.config.tools.get(toolName)?.timeoutMs ??
|
|
1052
1232
|
this.config.toolTimeoutMs ??
|
|
1053
1233
|
DEFAULT_TOOL_TIMEOUT_MS;
|
|
1054
|
-
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
1055
|
-
return this.config.tools.execute(toolName, input, toolContext);
|
|
1056
|
-
}
|
|
1057
1234
|
const controller = new AbortController();
|
|
1058
1235
|
const parentSignal = toolContext.abortSignal;
|
|
1059
1236
|
const onParentAbort = () => controller.abort(parentSignal.reason);
|
|
@@ -1063,14 +1240,18 @@ export class ToolExecutor {
|
|
|
1063
1240
|
parentSignal.addEventListener('abort', onParentAbort, { once: true });
|
|
1064
1241
|
let timer;
|
|
1065
1242
|
let timedOut = false;
|
|
1243
|
+
let invocationOpen = true;
|
|
1244
|
+
const nestedDispatches = new Set();
|
|
1066
1245
|
try {
|
|
1067
|
-
const expired =
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1246
|
+
const expired = Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
1247
|
+
? new Promise((resolve) => {
|
|
1248
|
+
timer = setTimeout(() => {
|
|
1249
|
+
timedOut = true;
|
|
1250
|
+
controller.abort(new Error(`Tool "${toolName}" exceeded ${timeoutMs}ms`));
|
|
1251
|
+
resolve('timeout');
|
|
1252
|
+
}, timeoutMs);
|
|
1253
|
+
})
|
|
1254
|
+
: undefined;
|
|
1074
1255
|
const aborted = new Promise((resolve) => {
|
|
1075
1256
|
if (controller.signal.aborted && !timedOut) {
|
|
1076
1257
|
resolve('aborted');
|
|
@@ -1082,24 +1263,46 @@ export class ToolExecutor {
|
|
|
1082
1263
|
}, { once: true });
|
|
1083
1264
|
});
|
|
1084
1265
|
const inheritedDispatch = toolContext.dispatchTool;
|
|
1085
|
-
const
|
|
1266
|
+
const scopedDispatch = inheritedDispatch
|
|
1267
|
+
? (name, nestedInput, options) => {
|
|
1268
|
+
if (!invocationOpen) {
|
|
1269
|
+
return Promise.reject(new Error(`Tool "${toolName}" invocation has settled; nested dispatch is closed.`));
|
|
1270
|
+
}
|
|
1271
|
+
const nested = inheritedDispatch(name, nestedInput, {
|
|
1272
|
+
...options,
|
|
1273
|
+
signal: options?.signal
|
|
1274
|
+
? AbortSignal.any([controller.signal, options.signal])
|
|
1275
|
+
: controller.signal,
|
|
1276
|
+
});
|
|
1277
|
+
nestedDispatches.add(nested);
|
|
1278
|
+
// `finally()` creates a second promise. Observe that promise too, or a
|
|
1279
|
+
// rejected nested call which its owner intentionally awaits later would
|
|
1280
|
+
// also create an unhandled cleanup rejection here.
|
|
1281
|
+
void nested.finally(() => nestedDispatches.delete(nested)).catch(() => { });
|
|
1282
|
+
return nested;
|
|
1283
|
+
}
|
|
1284
|
+
: undefined;
|
|
1285
|
+
if (controller.signal.aborted) {
|
|
1286
|
+
return {
|
|
1287
|
+
success: false,
|
|
1288
|
+
output: '',
|
|
1289
|
+
error: abortReasonText(controller.signal.reason)
|
|
1290
|
+
? `Tool "${toolName}" was cancelled: ${abortReasonText(controller.signal.reason)}`
|
|
1291
|
+
: `Tool "${toolName}" was cancelled.`,
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
const context = {
|
|
1086
1295
|
...toolContext,
|
|
1087
1296
|
abortSignal: controller.signal,
|
|
1088
|
-
...(
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
? AbortSignal.any([controller.signal, options.signal])
|
|
1094
|
-
: controller.signal,
|
|
1095
|
-
}),
|
|
1096
|
-
}
|
|
1097
|
-
: {}),
|
|
1098
|
-
});
|
|
1297
|
+
...(scopedDispatch ? { dispatchTool: scopedDispatch } : {}),
|
|
1298
|
+
};
|
|
1299
|
+
const execution = prepared
|
|
1300
|
+
? this.config.tools.executePrepared(prepared, context)
|
|
1301
|
+
: this.config.tools.execute(toolName, input, context);
|
|
1099
1302
|
// The loser of the race may still reject later; neutralize it so
|
|
1100
1303
|
// it is never an unhandled rejection.
|
|
1101
1304
|
execution.catch(() => { });
|
|
1102
|
-
const outcome = await Promise.race([execution, expired, aborted]);
|
|
1305
|
+
const outcome = await Promise.race(expired ? [execution, expired, aborted] : [execution, aborted]);
|
|
1103
1306
|
if (outcome === 'timeout') {
|
|
1104
1307
|
this.log.warn('Tool timed out', {
|
|
1105
1308
|
[NAMZU.RUN_ID]: this.config.runId,
|
|
@@ -1131,30 +1334,284 @@ export class ToolExecutor {
|
|
|
1131
1334
|
return outcome;
|
|
1132
1335
|
}
|
|
1133
1336
|
finally {
|
|
1337
|
+
// Revoke synchronously before awaiting anything. A retained closure
|
|
1338
|
+
// called from another microtask is refused here; the aborted signal is
|
|
1339
|
+
// the structural backstop inside dispatchNested itself.
|
|
1340
|
+
invocationOpen = false;
|
|
1341
|
+
if (!controller.signal.aborted) {
|
|
1342
|
+
controller.abort(new Error(`Tool "${toolName}" invocation has settled.`));
|
|
1343
|
+
}
|
|
1344
|
+
// Calls already admitted before closure own event rows and registry
|
|
1345
|
+
// work. Their executor races observe the abort, emit a terminal result,
|
|
1346
|
+
// and settle before the parent is allowed to report completion.
|
|
1347
|
+
while (nestedDispatches.size > 0) {
|
|
1348
|
+
await Promise.allSettled([...nestedDispatches]);
|
|
1349
|
+
}
|
|
1134
1350
|
if (timer !== undefined)
|
|
1135
1351
|
clearTimeout(timer);
|
|
1136
1352
|
parentSignal.removeEventListener('abort', onParentAbort);
|
|
1137
1353
|
}
|
|
1138
1354
|
}
|
|
1139
|
-
async runPreToolHook(toolName, input) {
|
|
1355
|
+
async runPreToolHook(toolName, input, signal = this.config.abortSignal) {
|
|
1140
1356
|
if (!this.config.pluginManager)
|
|
1141
|
-
return { kind: 'continue', input };
|
|
1357
|
+
return { kind: 'continue', input, modified: false };
|
|
1142
1358
|
const results = await this.config.pluginManager.executeHooks('pre_tool_use', {
|
|
1143
1359
|
runId: this.config.runId,
|
|
1144
1360
|
toolName,
|
|
1145
1361
|
toolInput: input,
|
|
1146
|
-
signal
|
|
1362
|
+
signal,
|
|
1147
1363
|
}, this.emitEvent);
|
|
1148
1364
|
return this.interpretPreToolResults(toolName, input, results);
|
|
1149
1365
|
}
|
|
1366
|
+
async prepareNestedCall(toolName, input, signal) {
|
|
1367
|
+
const prepare = this.config.tools.prepareExecution;
|
|
1368
|
+
const executePrepared = this.config.tools.executePrepared;
|
|
1369
|
+
if (typeof prepare !== 'function' || typeof executePrepared !== 'function') {
|
|
1370
|
+
if (this.config.authorizationGate) {
|
|
1371
|
+
return {
|
|
1372
|
+
kind: 'synthetic',
|
|
1373
|
+
input,
|
|
1374
|
+
message: `Tool "${toolName}" was not executed because its registry cannot bind authorization to one prepared input.`,
|
|
1375
|
+
isError: true,
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
const preOutcome = await this.runPreToolHook(toolName, input, signal);
|
|
1379
|
+
if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
|
|
1380
|
+
return {
|
|
1381
|
+
kind: 'synthetic',
|
|
1382
|
+
input: preOutcome.input,
|
|
1383
|
+
message: preOutcome.output,
|
|
1384
|
+
isError: preOutcome.kind === 'error',
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
return { kind: 'legacy', input: preOutcome.input };
|
|
1388
|
+
}
|
|
1389
|
+
let preparation;
|
|
1390
|
+
try {
|
|
1391
|
+
preparation = prepare.call(this.config.tools, toolName, input);
|
|
1392
|
+
}
|
|
1393
|
+
catch (err) {
|
|
1394
|
+
return {
|
|
1395
|
+
kind: 'synthetic',
|
|
1396
|
+
input,
|
|
1397
|
+
message: `Tool "${toolName}" could not be prepared: ${toErrorMessage(err)}`,
|
|
1398
|
+
isError: true,
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
if (!preparation.success) {
|
|
1402
|
+
return {
|
|
1403
|
+
kind: 'synthetic',
|
|
1404
|
+
input,
|
|
1405
|
+
message: formatFailedToolOutput(preparation.result.output, preparation.result.error),
|
|
1406
|
+
isError: true,
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
const preOutcome = await this.runPreToolHook(toolName, preparation.prepared.input, signal);
|
|
1410
|
+
if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
|
|
1411
|
+
return {
|
|
1412
|
+
kind: 'synthetic',
|
|
1413
|
+
input: preOutcome.input,
|
|
1414
|
+
message: preOutcome.output,
|
|
1415
|
+
isError: preOutcome.kind === 'error',
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
if (!preOutcome.modified) {
|
|
1419
|
+
return {
|
|
1420
|
+
kind: 'ready',
|
|
1421
|
+
input: preparation.prepared.input,
|
|
1422
|
+
prepared: preparation.prepared,
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
const modified = prepare.call(this.config.tools, toolName, preOutcome.input);
|
|
1426
|
+
if (!modified.success) {
|
|
1427
|
+
return {
|
|
1428
|
+
kind: 'synthetic',
|
|
1429
|
+
input: preOutcome.input,
|
|
1430
|
+
message: formatFailedToolOutput(modified.result.output, modified.result.error),
|
|
1431
|
+
isError: true,
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
return { kind: 'ready', input: modified.prepared.input, prepared: modified.prepared };
|
|
1435
|
+
}
|
|
1436
|
+
async prepareDirectCall(toolCall) {
|
|
1437
|
+
let toolName = toolCall.function.name;
|
|
1438
|
+
const truncationRepair = toolCall.metadata?.inputTruncated === true
|
|
1439
|
+
? await this.repairTruncatedCall(toolCall, toolName)
|
|
1440
|
+
: null;
|
|
1441
|
+
if (toolCall.metadata?.inputTruncated === true && !truncationRepair) {
|
|
1442
|
+
return {
|
|
1443
|
+
kind: 'synthetic',
|
|
1444
|
+
toolCall,
|
|
1445
|
+
toolName,
|
|
1446
|
+
input: {},
|
|
1447
|
+
message: truncatedToolInputMessage(toolName),
|
|
1448
|
+
isError: true,
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
const prepare = this.config.tools.prepareExecution;
|
|
1452
|
+
const executePrepared = this.config.tools.executePrepared;
|
|
1453
|
+
if (typeof prepare !== 'function' || typeof executePrepared !== 'function') {
|
|
1454
|
+
const resolved = await this.resolveCall(truncationRepair
|
|
1455
|
+
? {
|
|
1456
|
+
...toolCall,
|
|
1457
|
+
function: {
|
|
1458
|
+
...toolCall.function,
|
|
1459
|
+
name: truncationRepair.toolName ?? toolName,
|
|
1460
|
+
arguments: truncationRepair.arguments,
|
|
1461
|
+
},
|
|
1462
|
+
metadata: {},
|
|
1463
|
+
}
|
|
1464
|
+
: toolCall);
|
|
1465
|
+
toolName = resolved.toolName;
|
|
1466
|
+
if (!resolved.ok) {
|
|
1467
|
+
return {
|
|
1468
|
+
kind: 'synthetic',
|
|
1469
|
+
toolCall,
|
|
1470
|
+
toolName,
|
|
1471
|
+
input: {},
|
|
1472
|
+
message: resolved.message,
|
|
1473
|
+
isError: true,
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
const preOutcome = await this.runPreToolHook(toolName, resolved.input);
|
|
1477
|
+
if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
|
|
1478
|
+
return {
|
|
1479
|
+
kind: 'synthetic',
|
|
1480
|
+
toolCall,
|
|
1481
|
+
toolName,
|
|
1482
|
+
input: preOutcome.input,
|
|
1483
|
+
message: preOutcome.output,
|
|
1484
|
+
isError: preOutcome.kind === 'error',
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
if (!this.config.authorizationGate) {
|
|
1488
|
+
return {
|
|
1489
|
+
kind: 'legacy',
|
|
1490
|
+
toolCall,
|
|
1491
|
+
toolName,
|
|
1492
|
+
input: preOutcome.input,
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
return {
|
|
1496
|
+
kind: 'synthetic',
|
|
1497
|
+
toolCall,
|
|
1498
|
+
toolName,
|
|
1499
|
+
input: preOutcome.input,
|
|
1500
|
+
message: `Tool "${toolName}" was not executed because its registry cannot bind authorization to one prepared input.`,
|
|
1501
|
+
isError: true,
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
let raw = truncationRepair?.arguments ?? toolCall.function.arguments;
|
|
1505
|
+
toolName = truncationRepair?.toolName ?? toolName;
|
|
1506
|
+
let repairUsed = truncationRepair !== null;
|
|
1507
|
+
let preparation;
|
|
1508
|
+
for (;;) {
|
|
1509
|
+
let parsed;
|
|
1510
|
+
try {
|
|
1511
|
+
parsed = parseArguments(raw);
|
|
1512
|
+
}
|
|
1513
|
+
catch {
|
|
1514
|
+
const message = `Error: Invalid JSON in tool arguments for "${toolName}"`;
|
|
1515
|
+
const repair = !repairUsed && this.config.repairToolCall
|
|
1516
|
+
? await this.requestRepair(toolCall, toolName, {
|
|
1517
|
+
reason: 'invalid_json',
|
|
1518
|
+
message,
|
|
1519
|
+
})
|
|
1520
|
+
: null;
|
|
1521
|
+
if (repair) {
|
|
1522
|
+
repairUsed = true;
|
|
1523
|
+
toolName = repair.toolName ?? toolName;
|
|
1524
|
+
raw = repair.arguments;
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
return { kind: 'synthetic', toolCall, toolName, input: {}, message, isError: true };
|
|
1528
|
+
}
|
|
1529
|
+
try {
|
|
1530
|
+
preparation = prepare.call(this.config.tools, toolName, parsed);
|
|
1531
|
+
}
|
|
1532
|
+
catch (err) {
|
|
1533
|
+
const message = `Error: Unknown or unavailable tool "${toolName}": ${toErrorMessage(err)}`;
|
|
1534
|
+
const repair = !repairUsed && this.config.repairToolCall
|
|
1535
|
+
? await this.requestRepair(toolCall, toolName, {
|
|
1536
|
+
reason: 'unknown_tool',
|
|
1537
|
+
message,
|
|
1538
|
+
})
|
|
1539
|
+
: null;
|
|
1540
|
+
if (repair) {
|
|
1541
|
+
repairUsed = true;
|
|
1542
|
+
toolName = repair.toolName ?? toolName;
|
|
1543
|
+
raw = repair.arguments;
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1546
|
+
return { kind: 'synthetic', toolCall, toolName, input: parsed, message, isError: true };
|
|
1547
|
+
}
|
|
1548
|
+
if (preparation.success)
|
|
1549
|
+
break;
|
|
1550
|
+
const message = formatFailedToolOutput(preparation.result.output, preparation.result.error);
|
|
1551
|
+
const repair = !repairUsed && this.config.repairToolCall
|
|
1552
|
+
? await this.requestRepair(toolCall, toolName, {
|
|
1553
|
+
reason: 'schema_validation',
|
|
1554
|
+
message,
|
|
1555
|
+
})
|
|
1556
|
+
: null;
|
|
1557
|
+
if (repair) {
|
|
1558
|
+
repairUsed = true;
|
|
1559
|
+
toolName = repair.toolName ?? toolName;
|
|
1560
|
+
raw = repair.arguments;
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
return {
|
|
1564
|
+
kind: 'synthetic',
|
|
1565
|
+
toolCall,
|
|
1566
|
+
toolName,
|
|
1567
|
+
input: parsed,
|
|
1568
|
+
message,
|
|
1569
|
+
isError: true,
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
const preOutcome = await this.runPreToolHook(toolName, preparation.prepared.input);
|
|
1573
|
+
if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
|
|
1574
|
+
return {
|
|
1575
|
+
kind: 'synthetic',
|
|
1576
|
+
toolCall,
|
|
1577
|
+
toolName,
|
|
1578
|
+
input: preOutcome.input,
|
|
1579
|
+
message: preOutcome.output,
|
|
1580
|
+
isError: preOutcome.kind === 'error',
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
if (preOutcome.modified) {
|
|
1584
|
+
const modified = prepare.call(this.config.tools, toolName, preOutcome.input);
|
|
1585
|
+
if (!modified.success) {
|
|
1586
|
+
return {
|
|
1587
|
+
kind: 'synthetic',
|
|
1588
|
+
toolCall,
|
|
1589
|
+
toolName,
|
|
1590
|
+
input: preOutcome.input,
|
|
1591
|
+
message: formatFailedToolOutput(modified.result.output, modified.result.error),
|
|
1592
|
+
isError: true,
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
preparation = modified;
|
|
1596
|
+
}
|
|
1597
|
+
return {
|
|
1598
|
+
kind: 'ready',
|
|
1599
|
+
toolCall,
|
|
1600
|
+
toolName,
|
|
1601
|
+
input: preparation.prepared.input,
|
|
1602
|
+
prepared: preparation.prepared,
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1150
1605
|
interpretPreToolResults(toolName, initialInput, results) {
|
|
1151
1606
|
let currentInput = initialInput;
|
|
1607
|
+
let modified = false;
|
|
1152
1608
|
for (const result of results) {
|
|
1153
1609
|
switch (result.action) {
|
|
1154
1610
|
case 'continue':
|
|
1155
1611
|
continue;
|
|
1156
1612
|
case 'modify':
|
|
1157
1613
|
currentInput = result.input;
|
|
1614
|
+
modified = true;
|
|
1158
1615
|
continue;
|
|
1159
1616
|
case 'skip':
|
|
1160
1617
|
return {
|
|
@@ -1181,7 +1638,7 @@ export class ToolExecutor {
|
|
|
1181
1638
|
}
|
|
1182
1639
|
}
|
|
1183
1640
|
}
|
|
1184
|
-
return { kind: 'continue', input: currentInput };
|
|
1641
|
+
return { kind: 'continue', input: currentInput, modified };
|
|
1185
1642
|
}
|
|
1186
1643
|
/**
|
|
1187
1644
|
* Turn the call the model issued into a name and a parsed input, giving
|
|
@@ -1328,9 +1785,9 @@ export class ToolExecutor {
|
|
|
1328
1785
|
* image block had it discarded here — before the wire mapper built to
|
|
1329
1786
|
* carry it ever saw it.
|
|
1330
1787
|
*/
|
|
1331
|
-
async runOnce(toolName, input, toolContext) {
|
|
1788
|
+
async runOnce(toolName, input, toolContext, prepared) {
|
|
1332
1789
|
try {
|
|
1333
|
-
return await this.executeWithDeadline(toolName, input, toolContext);
|
|
1790
|
+
return await this.executeWithDeadline(toolName, input, toolContext, prepared);
|
|
1334
1791
|
}
|
|
1335
1792
|
catch (err) {
|
|
1336
1793
|
const message = toErrorMessage(err);
|
|
@@ -1401,14 +1858,16 @@ export class ToolExecutor {
|
|
|
1401
1858
|
* execution so UI cards reach a terminal state instead of hanging in
|
|
1402
1859
|
* `executing`, and records a failed activity for the trace.
|
|
1403
1860
|
*/
|
|
1404
|
-
async recordDenial(toolCall, reason) {
|
|
1405
|
-
const toolName = toolCall.function.name;
|
|
1406
|
-
let input = {};
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1861
|
+
async recordDenial(toolCall, reason, preparedCall) {
|
|
1862
|
+
const toolName = preparedCall?.toolName ?? toolCall.function.name;
|
|
1863
|
+
let input = preparedCall?.input ?? {};
|
|
1864
|
+
if (!preparedCall) {
|
|
1865
|
+
try {
|
|
1866
|
+
input = JSON.parse(toolCall.function.arguments || '{}');
|
|
1867
|
+
}
|
|
1868
|
+
catch {
|
|
1869
|
+
input = toolCall.function.arguments;
|
|
1870
|
+
}
|
|
1412
1871
|
}
|
|
1413
1872
|
const output = deniedToolOutput(toolName, reason);
|
|
1414
1873
|
this.log.info('Tool call denied — synthesizing tool_result', {
|
|
@@ -1484,6 +1943,12 @@ export class ToolExecutor {
|
|
|
1484
1943
|
isError: outcome.kind === 'error',
|
|
1485
1944
|
};
|
|
1486
1945
|
}
|
|
1946
|
+
recordSyntheticPreparation(call) {
|
|
1947
|
+
return this.recordSyntheticHookOutcome(call.toolCall.id, call.toolName, call.input, {
|
|
1948
|
+
kind: call.isError ? 'error' : 'skip',
|
|
1949
|
+
output: call.message,
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1487
1952
|
/**
|
|
1488
1953
|
* Bound the rich channel, or leave it alone when no cap is configured.
|
|
1489
1954
|
*
|