@namzu/sdk 41.0.0 → 42.0.1

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.
Files changed (141) hide show
  1. package/CHANGELOG.md +233 -0
  2. package/dist/agents/ReactiveAgent.d.ts.map +1 -1
  3. package/dist/agents/ReactiveAgent.js +3 -0
  4. package/dist/agents/ReactiveAgent.js.map +1 -1
  5. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  6. package/dist/agents/SupervisorAgent.js +11 -0
  7. package/dist/agents/SupervisorAgent.js.map +1 -1
  8. package/dist/agents/runAgent.d.ts +14 -0
  9. package/dist/agents/runAgent.d.ts.map +1 -1
  10. package/dist/agents/runAgent.js +3 -0
  11. package/dist/agents/runAgent.js.map +1 -1
  12. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  13. package/dist/manager/agent/lifecycle.js +20 -0
  14. package/dist/manager/agent/lifecycle.js.map +1 -1
  15. package/dist/manager/resident/outbox.d.ts +8 -8
  16. package/dist/manager/resident/store.d.ts +4 -4
  17. package/dist/public-runtime.d.ts +4 -1
  18. package/dist/public-runtime.d.ts.map +1 -1
  19. package/dist/public-runtime.js +13 -1
  20. package/dist/public-runtime.js.map +1 -1
  21. package/dist/public-tools.d.ts +1 -1
  22. package/dist/public-tools.d.ts.map +1 -1
  23. package/dist/public-tools.js +4 -2
  24. package/dist/public-tools.js.map +1 -1
  25. package/dist/registry/tool/execute.d.ts.map +1 -1
  26. package/dist/registry/tool/execute.js +10 -1
  27. package/dist/registry/tool/execute.js.map +1 -1
  28. package/dist/runtime/bidi/session.d.ts +11 -0
  29. package/dist/runtime/bidi/session.d.ts.map +1 -1
  30. package/dist/runtime/bidi/session.js +2 -0
  31. package/dist/runtime/bidi/session.js.map +1 -1
  32. package/dist/runtime/query/cancelled-before-start.d.ts +34 -0
  33. package/dist/runtime/query/cancelled-before-start.d.ts.map +1 -0
  34. package/dist/runtime/query/cancelled-before-start.js +152 -0
  35. package/dist/runtime/query/cancelled-before-start.js.map +1 -0
  36. package/dist/runtime/query/checkpoint.d.ts +21 -0
  37. package/dist/runtime/query/checkpoint.d.ts.map +1 -1
  38. package/dist/runtime/query/checkpoint.js +23 -0
  39. package/dist/runtime/query/checkpoint.js.map +1 -1
  40. package/dist/runtime/query/executor/tool-call-admission.d.ts +57 -0
  41. package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -0
  42. package/dist/runtime/query/executor/tool-call-admission.js +373 -0
  43. package/dist/runtime/query/executor/tool-call-admission.js.map +1 -0
  44. package/dist/runtime/query/executor.d.ts +76 -35
  45. package/dist/runtime/query/executor.d.ts.map +1 -1
  46. package/dist/runtime/query/executor.js +52 -380
  47. package/dist/runtime/query/executor.js.map +1 -1
  48. package/dist/runtime/query/finalize-run.d.ts +55 -0
  49. package/dist/runtime/query/finalize-run.d.ts.map +1 -0
  50. package/dist/runtime/query/finalize-run.js +113 -0
  51. package/dist/runtime/query/finalize-run.js.map +1 -0
  52. package/dist/runtime/query/guardrail-presets.d.ts +187 -1
  53. package/dist/runtime/query/guardrail-presets.d.ts.map +1 -1
  54. package/dist/runtime/query/guardrail-presets.js +298 -0
  55. package/dist/runtime/query/guardrail-presets.js.map +1 -1
  56. package/dist/runtime/query/index.d.ts +18 -9
  57. package/dist/runtime/query/index.d.ts.map +1 -1
  58. package/dist/runtime/query/index.js +241 -893
  59. package/dist/runtime/query/index.js.map +1 -1
  60. package/dist/runtime/query/iteration/index.d.ts +6 -161
  61. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  62. package/dist/runtime/query/iteration/index.js +23 -523
  63. package/dist/runtime/query/iteration/index.js.map +1 -1
  64. package/dist/runtime/query/iteration/outstanding-work.d.ts +158 -0
  65. package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -0
  66. package/dist/runtime/query/iteration/outstanding-work.js +365 -0
  67. package/dist/runtime/query/iteration/outstanding-work.js.map +1 -0
  68. package/dist/runtime/query/iteration/phases/plan.d.ts.map +1 -1
  69. package/dist/runtime/query/iteration/phases/plan.js +13 -2
  70. package/dist/runtime/query/iteration/phases/plan.js.map +1 -1
  71. package/dist/runtime/query/iteration/step-shaping.d.ts +41 -0
  72. package/dist/runtime/query/iteration/step-shaping.d.ts.map +1 -0
  73. package/dist/runtime/query/iteration/step-shaping.js +184 -0
  74. package/dist/runtime/query/iteration/step-shaping.js.map +1 -0
  75. package/dist/runtime/query/prepare-run.d.ts +94 -0
  76. package/dist/runtime/query/prepare-run.d.ts.map +1 -0
  77. package/dist/runtime/query/prepare-run.js +589 -0
  78. package/dist/runtime/query/prepare-run.js.map +1 -0
  79. package/dist/runtime/query/release-run.d.ts +56 -0
  80. package/dist/runtime/query/release-run.d.ts.map +1 -0
  81. package/dist/runtime/query/release-run.js +101 -0
  82. package/dist/runtime/query/release-run.js.map +1 -0
  83. package/dist/runtime/query/resume-pending.d.ts +112 -1
  84. package/dist/runtime/query/resume-pending.d.ts.map +1 -1
  85. package/dist/runtime/query/resume-pending.js +133 -0
  86. package/dist/runtime/query/resume-pending.js.map +1 -1
  87. package/dist/runtime/query/tooling.d.ts +2 -0
  88. package/dist/runtime/query/tooling.d.ts.map +1 -1
  89. package/dist/runtime/query/tooling.js +3 -0
  90. package/dist/runtime/query/tooling.js.map +1 -1
  91. package/dist/store/evidence/compaction-archive.d.ts +2 -2
  92. package/dist/tools/coordinator/agent.d.ts.map +1 -1
  93. package/dist/tools/coordinator/agent.js +17 -2
  94. package/dist/tools/coordinator/agent.js.map +1 -1
  95. package/dist/tools/coordinator/index.d.ts.map +1 -1
  96. package/dist/tools/coordinator/index.js +17 -3
  97. package/dist/tools/coordinator/index.js.map +1 -1
  98. package/dist/tools/untrusted-envelope.d.ts +35 -0
  99. package/dist/tools/untrusted-envelope.d.ts.map +1 -1
  100. package/dist/tools/untrusted-envelope.js +91 -3
  101. package/dist/tools/untrusted-envelope.js.map +1 -1
  102. package/dist/types/agent/base.d.ts +23 -0
  103. package/dist/types/agent/base.d.ts.map +1 -1
  104. package/dist/types/agent/task.d.ts +19 -0
  105. package/dist/types/agent/task.d.ts.map +1 -1
  106. package/dist/types/run/config.d.ts +12 -5
  107. package/dist/types/run/config.d.ts.map +1 -1
  108. package/dist/types/tool/index.d.ts +19 -0
  109. package/dist/types/tool/index.d.ts.map +1 -1
  110. package/dist/types/tool/index.js.map +1 -1
  111. package/package.json +1 -1
  112. package/src/agents/ReactiveAgent.ts +3 -0
  113. package/src/agents/SupervisorAgent.ts +11 -0
  114. package/src/agents/runAgent.ts +18 -0
  115. package/src/manager/agent/lifecycle.ts +22 -0
  116. package/src/public-runtime.ts +14 -0
  117. package/src/public-tools.ts +8 -2
  118. package/src/registry/tool/execute.ts +9 -1
  119. package/src/runtime/bidi/session.ts +13 -0
  120. package/src/runtime/query/cancelled-before-start.ts +189 -0
  121. package/src/runtime/query/checkpoint.ts +22 -0
  122. package/src/runtime/query/executor/tool-call-admission.ts +473 -0
  123. package/src/runtime/query/executor.ts +76 -442
  124. package/src/runtime/query/finalize-run.ts +192 -0
  125. package/src/runtime/query/guardrail-presets.ts +356 -0
  126. package/src/runtime/query/index.ts +287 -1011
  127. package/src/runtime/query/iteration/index.ts +40 -586
  128. package/src/runtime/query/iteration/outstanding-work.ts +386 -0
  129. package/src/runtime/query/iteration/phases/plan.ts +18 -2
  130. package/src/runtime/query/iteration/step-shaping.ts +271 -0
  131. package/src/runtime/query/prepare-run.ts +718 -0
  132. package/src/runtime/query/release-run.ts +168 -0
  133. package/src/runtime/query/resume-pending.ts +158 -0
  134. package/src/runtime/query/tooling.ts +5 -0
  135. package/src/tools/coordinator/agent.ts +17 -2
  136. package/src/tools/coordinator/index.ts +17 -3
  137. package/src/tools/untrusted-envelope.ts +94 -3
  138. package/src/types/agent/base.ts +24 -0
  139. package/src/types/agent/task.ts +20 -0
  140. package/src/types/run/config.ts +12 -5
  141. package/src/types/tool/index.ts +20 -0
@@ -9,10 +9,10 @@ import { buildProbeContext } from '../../probe/context.js'
9
9
  import { ProbeVetoError } from '../../probe/errors.js'
10
10
  import { probe as defaultProbeRegistry } from '../../probe/registry.js'
11
11
  import type { ProbeEnforcement } from '../../probe/registry.js'
12
- import { renderToolSchema } from '../../registry/tool/schema.js'
13
12
  import type { ActivityStore } from '../../store/activity/memory.js'
14
13
  import { SKILL_TOOL_NAME } from '../../tools/builtins/skill.js'
15
14
  import { createFileReadTracker } from '../../tools/file-read-tracker.js'
15
+ import type { ToolResultGuardrailSpec } from '../../types/guardrail/index.js'
16
16
  import type { RunId, ToolUseId } from '../../types/ids/index.js'
17
17
  import type { InvocationState } from '../../types/invocation/index.js'
18
18
  import {
@@ -37,11 +37,7 @@ import type {
37
37
  ToolRegistryContract,
38
38
  ToolResult,
39
39
  } from '../../types/tool/index.js'
40
- import type {
41
- RepairToolCall,
42
- ToolCallRepair,
43
- ToolCallRepairReason,
44
- } from '../../types/tool/repair.js'
40
+ import type { RepairToolCall } from '../../types/tool/repair.js'
45
41
  import { abortReasonText } from '../../utils/abort.js'
46
42
  import { awaitWithAbort } from '../../utils/await-with-abort.js'
47
43
  import { type BackoffPolicy, backoffWithJitter, sleep } from '../../utils/backoff.js'
@@ -50,9 +46,18 @@ import { generateToolCallId } from '../../utils/id.js'
50
46
  import type { Logger } from '../../utils/logger.js'
51
47
  import { compressShellOutput } from '../../utils/shell-compress.js'
52
48
  import { type BackgroundJobRegistry, type JobProcess, bindOwner } from '../jobs/registry.js'
49
+ import {
50
+ type ToolAdmissionHost,
51
+ formatFailedToolOutput,
52
+ prepareDirectCall,
53
+ repairTruncatedCall,
54
+ resolveCall,
55
+ runPreToolHook,
56
+ truncatedToolInputMessage,
57
+ } from './executor/tool-call-admission.js'
53
58
  import { describeVisibleFileEvidence } from './file-evidence-context.js'
54
59
  import { seedObservationLedger } from './file-evidence-seed.js'
55
- import { skippedToolResultText } from './plugin-hooks.js'
60
+ import { DEFAULT_TOOL_RESULT_GUARDRAILS } from './guardrail-presets.js'
56
61
  import type { ToolResultObservation } from './project-instructions.js'
57
62
  import { ToolCallBudget, assertMaxToolCalls } from './tool-call-budget.js'
58
63
  import {
@@ -65,7 +70,7 @@ import {
65
70
 
66
71
  export type EmitEvent = (event: RunEvent) => Promise<void>
67
72
 
68
- type PreparedDirectCall =
73
+ export type PreparedDirectCall =
69
74
  | {
70
75
  readonly kind: 'ready'
71
76
  readonly toolCall: ToolCall
@@ -285,14 +290,6 @@ export const DEFAULT_TOOL_RETRY_BACKOFF: BackoffPolicy = {
285
290
  maxDelayMs: 16_000,
286
291
  }
287
292
 
288
- /**
289
- * An empty arguments string means "no arguments", not "malformed" — the
290
- * shape a no-parameter tool arrives in.
291
- */
292
- function parseArguments(raw: string): unknown {
293
- return JSON.parse(raw || '{}')
294
- }
295
-
296
293
  export interface ToolExecutorConfig {
297
294
  fileReadTracker?: FileReadTracker
298
295
  tools: ToolRegistryContract
@@ -393,6 +390,12 @@ export interface ToolExecutorConfig {
393
390
  /** See QueryParams.retainedToolPreviewChars; applies to the recorded host output. */
394
391
  retainedToolPreviewChars?: number
395
392
 
393
+ /**
394
+ * See QueryParams.toolResultGuardrails. Absent installs
395
+ * {@link DEFAULT_TOOL_RESULT_GUARDRAILS}; an empty array installs none.
396
+ */
397
+ toolResultGuardrails?: readonly ToolResultGuardrailSpec[]
398
+
396
399
  /**
397
400
  * Cap on the RICH channel of a single tool result, in base64
398
401
  * characters. `0` or absent disables it.
@@ -451,7 +454,7 @@ interface PostToolOverride {
451
454
  readonly content?: ToolResultContent
452
455
  }
453
456
 
454
- type PreToolHookOutcome =
457
+ export type PreToolHookOutcome =
455
458
  | { kind: 'continue'; input: unknown; modified: boolean }
456
459
  | { kind: 'skip'; input: unknown; output: string }
457
460
  | { kind: 'error'; input: unknown; output: string }
@@ -634,10 +637,28 @@ export class ToolExecutor {
634
637
  *
635
638
  * `denials` marks ids that must NOT run: each is answered with a
636
639
  * synthetic error result carrying the caller's reason instead of being
637
- * executed. This is what makes the invariant hold by construction —
638
- * a gate denial, a human rejection and a partial approval all leave
639
- * the history valid, because there is exactly one place that turns a
640
- * batch of tool calls into messages and it always covers all of them.
640
+ * executed. A gate denial, a human rejection and a partial approval all
641
+ * leave the history valid, because there is exactly one place that turns
642
+ * a batch of tool calls into messages and it covers all of them.
643
+ *
644
+ * **That is a property of every path that RETURNS, not of the batch as
645
+ * a whole.** A per-call throw rejects the batch before the fill-the-holes
646
+ * loop below can run: `serial = serial.then(run)` means one rejection
647
+ * skips every LATER serial call, and `Promise.all([...parallel, serial])`
648
+ * then rejects — so this method produces no messages at all and the
649
+ * assistant turn keeps its `tool_use` blocks unanswered. A resume is what
650
+ * repairs that turn; see the `unfinished` step `iteration/index.ts`
651
+ * records for it.
652
+ *
653
+ * Reachable, not hypothetical, and demonstrated end to end by
654
+ * `a-throwing-batch-answers-nothing.test.ts`: `executeSingle` rethrows a
655
+ * retry's budget-admission error, and a `runPreToolHook` failure on a
656
+ * call whose preparation did not already run the hook.
657
+ *
658
+ * So do not read the guarantee below as covering a throw. The invariant
659
+ * holds for denials, for approvals, for a rejected batch and for a
660
+ * generation that partially failed while still returning: each of those
661
+ * leaves a hole that the fill-the-holes loop closes.
641
662
  *
642
663
  * Answering with `is_error` semantics rather than dropping the call is
643
664
  * the universal contract across providers: an unanswered `tool_use`
@@ -737,7 +758,7 @@ export class ToolExecutor {
737
758
  assertUniqueToolCallIds(response.message.toolCalls ?? [])
738
759
  const calls = new Map<string, PreparedDirectCall>()
739
760
  for (const toolCall of response.message.toolCalls ?? []) {
740
- calls.set(toolCall.id, await this.prepareDirectCall(toolCall))
761
+ calls.set(toolCall.id, await prepareDirectCall(this.admissionHost(), toolCall))
741
762
  }
742
763
  return this.publishPreparedBatch(calls)
743
764
  }
@@ -755,7 +776,7 @@ export class ToolExecutor {
755
776
  const calls = new Map((previous as OwnedPreparedToolBatch).calls)
756
777
  for (const toolCall of response.message.toolCalls ?? []) {
757
778
  if (changedCallIds.has(toolCall.id)) {
758
- calls.set(toolCall.id, await this.prepareDirectCall(toolCall))
779
+ calls.set(toolCall.id, await prepareDirectCall(this.admissionHost(), toolCall))
759
780
  }
760
781
  }
761
782
  return this.publishPreparedBatch(calls)
@@ -1335,6 +1356,11 @@ export class ToolExecutor {
1335
1356
  this.skillScope = { ...scope, adoptedInBatch: this.batchCounter }
1336
1357
  },
1337
1358
  maxToolOutputChars: this.config.maxToolOutputChars ?? DEFAULT_MAX_TOOL_OUTPUT_CHARS,
1359
+ // The run's screens, defaulted HERE rather than on the registry: a
1360
+ // host builds the registry and hands it over, so a registry-side
1361
+ // default is the host's to write and the shipped one reaches
1362
+ // nobody. `[]` survives the `??` and is how a run says "none".
1363
+ toolResultGuardrails: this.config.toolResultGuardrails ?? DEFAULT_TOOL_RESULT_GUARDRAILS,
1338
1364
  ...(this.config.skills ? { skills: this.config.skills } : {}),
1339
1365
  ...(this.config.web ? { web: this.config.web } : {}),
1340
1366
  // The SAME registry and the SAME context a model-issued call
@@ -1428,7 +1454,7 @@ export class ToolExecutor {
1428
1454
  // unused. Offer it the partial buffer first.
1429
1455
  const truncationRepair =
1430
1456
  toolCall.metadata?.inputTruncated === true
1431
- ? await this.repairTruncatedCall(toolCall, toolName)
1457
+ ? await repairTruncatedCall(this.admissionHost(), toolCall, toolName)
1432
1458
  : null
1433
1459
 
1434
1460
  if (toolCall.metadata?.inputTruncated === true && !truncationRepair) {
@@ -1460,7 +1486,8 @@ export class ToolExecutor {
1460
1486
  // error went back as a `tool_result`, the model re-read the whole
1461
1487
  // context and tried again. A host that can repair it locally turns
1462
1488
  // that into nothing. No-op when no repairer is configured.
1463
- const resolved = await this.resolveCall(
1489
+ const resolved = await resolveCall(
1490
+ this.admissionHost(),
1464
1491
  truncationRepair
1465
1492
  ? {
1466
1493
  ...toolCall,
@@ -1508,7 +1535,7 @@ export class ToolExecutor {
1508
1535
 
1509
1536
  let preOutcome: PreToolHookOutcome
1510
1537
  try {
1511
- preOutcome = await this.runPreToolHook(toolName, input)
1538
+ preOutcome = await runPreToolHook(this.admissionHost(), toolName, input)
1512
1539
  } catch (error) {
1513
1540
  if (!this.config.abortSignal.aborted) throw error
1514
1541
  // A later call's interrupted preparation must not reject the batch
@@ -2020,23 +2047,22 @@ export class ToolExecutor {
2020
2047
  }
2021
2048
  }
2022
2049
 
2023
- private async runPreToolHook(
2024
- toolName: string,
2025
- input: unknown,
2026
- signal: AbortSignal = this.config.abortSignal,
2027
- ): Promise<PreToolHookOutcome> {
2028
- if (!this.config.pluginManager) return { kind: 'continue', input, modified: false }
2029
- const results = await this.config.pluginManager.executeHooks(
2030
- 'pre_tool_use',
2031
- {
2032
- runId: this.config.runId,
2033
- toolName,
2034
- toolInput: input,
2035
- signal,
2036
- },
2037
- this.emitEvent,
2038
- )
2039
- return this.interpretPreToolResults(toolName, input, results)
2050
+ /**
2051
+ * The three things the admission family reads off this executor.
2052
+ *
2053
+ * Built per call rather than held: `setSandbox` REPLACES `config`, so a
2054
+ * host captured once would hand the next admission a stale sandbox.
2055
+ *
2056
+ * The one way this differs from the inline code it replaced, which
2057
+ * re-read `this.config` at every use: an admission that spans a
2058
+ * `setSandbox()` now finishes against the config it STARTED with rather
2059
+ * than against the new one. Distinguishing the two readings needs
2060
+ * `setSandbox` to be called from a hook awaited in the middle of one
2061
+ * admission — its only call site is the run's sandbox acquisition,
2062
+ * before the loop, so nothing in this tree can tell them apart.
2063
+ */
2064
+ private admissionHost(): ToolAdmissionHost {
2065
+ return { config: this.config, emitEvent: this.emitEvent, log: this.log }
2040
2066
  }
2041
2067
 
2042
2068
  private async prepareNestedCall(
@@ -2055,7 +2081,7 @@ export class ToolExecutor {
2055
2081
  isError: true,
2056
2082
  }
2057
2083
  }
2058
- const preOutcome = await this.runPreToolHook(toolName, input, signal)
2084
+ const preOutcome = await runPreToolHook(this.admissionHost(), toolName, input, signal)
2059
2085
  if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
2060
2086
  return {
2061
2087
  kind: 'synthetic',
@@ -2086,7 +2112,12 @@ export class ToolExecutor {
2086
2112
  isError: true,
2087
2113
  }
2088
2114
  }
2089
- const preOutcome = await this.runPreToolHook(toolName, preparation.prepared.input, signal)
2115
+ const preOutcome = await runPreToolHook(
2116
+ this.admissionHost(),
2117
+ toolName,
2118
+ preparation.prepared.input,
2119
+ signal,
2120
+ )
2090
2121
  if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
2091
2122
  return {
2092
2123
  kind: 'synthetic',
@@ -2114,393 +2145,6 @@ export class ToolExecutor {
2114
2145
  return { kind: 'ready', input: modified.prepared.input, prepared: modified.prepared }
2115
2146
  }
2116
2147
 
2117
- private async prepareDirectCall(toolCall: ToolCall): Promise<PreparedDirectCall> {
2118
- let toolName = toolCall.function.name
2119
- const truncationRepair =
2120
- toolCall.metadata?.inputTruncated === true
2121
- ? await this.repairTruncatedCall(toolCall, toolName)
2122
- : null
2123
- if (toolCall.metadata?.inputTruncated === true && !truncationRepair) {
2124
- return {
2125
- kind: 'synthetic',
2126
- toolCall,
2127
- toolName,
2128
- input: {},
2129
- message: truncatedToolInputMessage(toolName),
2130
- isError: true,
2131
- }
2132
- }
2133
-
2134
- const prepare = this.config.tools.prepareExecution
2135
- const executePrepared = this.config.tools.executePrepared
2136
- if (typeof prepare !== 'function' || typeof executePrepared !== 'function') {
2137
- const resolved = await this.resolveCall(
2138
- truncationRepair
2139
- ? {
2140
- ...toolCall,
2141
- function: {
2142
- ...toolCall.function,
2143
- name: truncationRepair.toolName ?? toolName,
2144
- arguments: truncationRepair.arguments,
2145
- },
2146
- metadata: {},
2147
- }
2148
- : toolCall,
2149
- )
2150
- toolName = resolved.toolName
2151
- if (!resolved.ok) {
2152
- return {
2153
- kind: 'synthetic',
2154
- toolCall,
2155
- toolName,
2156
- input: {},
2157
- message: resolved.message,
2158
- isError: true,
2159
- }
2160
- }
2161
- const preOutcome = await this.runPreToolHook(toolName, resolved.input)
2162
- if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
2163
- return {
2164
- kind: 'synthetic',
2165
- toolCall,
2166
- toolName,
2167
- input: preOutcome.input,
2168
- message: preOutcome.output,
2169
- isError: preOutcome.kind === 'error',
2170
- }
2171
- }
2172
- if (!this.config.authorizationGate) {
2173
- return {
2174
- kind: 'legacy',
2175
- toolCall,
2176
- toolName,
2177
- input: preOutcome.input,
2178
- }
2179
- }
2180
- return {
2181
- kind: 'synthetic',
2182
- toolCall,
2183
- toolName,
2184
- input: preOutcome.input,
2185
- message: `Tool "${toolName}" was not executed because its registry cannot bind authorization to one prepared input.`,
2186
- isError: true,
2187
- }
2188
- }
2189
-
2190
- let raw = truncationRepair?.arguments ?? toolCall.function.arguments
2191
- toolName = truncationRepair?.toolName ?? toolName
2192
- let repairUsed = truncationRepair !== null
2193
- let preparation: ReturnType<typeof prepare>
2194
- for (;;) {
2195
- let parsed: unknown
2196
- try {
2197
- parsed = parseArguments(raw)
2198
- } catch {
2199
- const message = `Error: Invalid JSON in tool arguments for "${toolName}"`
2200
- const repair =
2201
- !repairUsed && this.config.repairToolCall
2202
- ? await this.requestRepair(toolCall, toolName, {
2203
- reason: 'invalid_json',
2204
- message,
2205
- })
2206
- : null
2207
- if (repair) {
2208
- repairUsed = true
2209
- toolName = repair.toolName ?? toolName
2210
- raw = repair.arguments
2211
- continue
2212
- }
2213
- return { kind: 'synthetic', toolCall, toolName, input: {}, message, isError: true }
2214
- }
2215
-
2216
- try {
2217
- preparation = prepare.call(this.config.tools, toolName, parsed)
2218
- } catch (err) {
2219
- const message = `Error: Unknown or unavailable tool "${toolName}": ${toErrorMessage(err)}`
2220
- const repair =
2221
- !repairUsed && this.config.repairToolCall
2222
- ? await this.requestRepair(toolCall, toolName, {
2223
- reason: 'unknown_tool',
2224
- message,
2225
- })
2226
- : null
2227
- if (repair) {
2228
- repairUsed = true
2229
- toolName = repair.toolName ?? toolName
2230
- raw = repair.arguments
2231
- continue
2232
- }
2233
- return { kind: 'synthetic', toolCall, toolName, input: parsed, message, isError: true }
2234
- }
2235
-
2236
- if (preparation.success) break
2237
- const message = formatFailedToolOutput(preparation.result.output, preparation.result.error)
2238
- const repair =
2239
- !repairUsed && this.config.repairToolCall
2240
- ? await this.requestRepair(toolCall, toolName, {
2241
- reason: 'schema_validation',
2242
- message,
2243
- })
2244
- : null
2245
- if (repair) {
2246
- repairUsed = true
2247
- toolName = repair.toolName ?? toolName
2248
- raw = repair.arguments
2249
- continue
2250
- }
2251
- return {
2252
- kind: 'synthetic',
2253
- toolCall,
2254
- toolName,
2255
- input: parsed,
2256
- message,
2257
- isError: true,
2258
- }
2259
- }
2260
-
2261
- const preOutcome = await this.runPreToolHook(toolName, preparation.prepared.input)
2262
- if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
2263
- return {
2264
- kind: 'synthetic',
2265
- toolCall,
2266
- toolName,
2267
- input: preOutcome.input,
2268
- message: preOutcome.output,
2269
- isError: preOutcome.kind === 'error',
2270
- }
2271
- }
2272
-
2273
- if (preOutcome.modified) {
2274
- const modified = prepare.call(this.config.tools, toolName, preOutcome.input)
2275
- if (!modified.success) {
2276
- return {
2277
- kind: 'synthetic',
2278
- toolCall,
2279
- toolName,
2280
- input: preOutcome.input,
2281
- message: formatFailedToolOutput(modified.result.output, modified.result.error),
2282
- isError: true,
2283
- }
2284
- }
2285
- preparation = modified
2286
- }
2287
-
2288
- return {
2289
- kind: 'ready',
2290
- toolCall,
2291
- toolName,
2292
- input: preparation.prepared.input,
2293
- prepared: preparation.prepared,
2294
- }
2295
- }
2296
-
2297
- private interpretPreToolResults(
2298
- toolName: string,
2299
- initialInput: unknown,
2300
- results: readonly PluginHookResult[],
2301
- ): PreToolHookOutcome {
2302
- let currentInput = initialInput
2303
- let modified = false
2304
- for (const result of results) {
2305
- switch (result.action) {
2306
- case 'continue':
2307
- continue
2308
- case 'modify':
2309
- currentInput = result.input
2310
- modified = true
2311
- continue
2312
- case 'skip':
2313
- return {
2314
- kind: 'skip',
2315
- input: currentInput,
2316
- output: skippedToolResultText(toolName, result.reason),
2317
- }
2318
- case 'error':
2319
- return {
2320
- kind: 'error',
2321
- input: currentInput,
2322
- output: `Error: ${result.message}`,
2323
- }
2324
- case 'retry':
2325
- case 'annotate':
2326
- // There is no result to replace yet. Rejecting loudly beats
2327
- // silently ignoring it: a hook author who returned this here
2328
- // meant to redact something and would otherwise watch the secret
2329
- // go through.
2330
- case 'replace':
2331
- throw new Error(
2332
- `Plugin hook pre_tool_use returned unsupported action '${result.action}' for tool ${toolName}`,
2333
- )
2334
- default: {
2335
- const _exhaustive: never = result
2336
- throw new Error(`Unknown PluginHookResult: ${JSON.stringify(_exhaustive)}`)
2337
- }
2338
- }
2339
- }
2340
- return { kind: 'continue', input: currentInput, modified }
2341
- }
2342
-
2343
- /**
2344
- * Turn the call the model issued into a name and a parsed input, giving
2345
- * a configured repairer one chance to fix it first.
2346
- *
2347
- * Exactly one chance: a repairer that produces a call which is still
2348
- * broken will not do better on a second look, and an unbounded loop
2349
- * here is a hang rather than a degradation.
2350
- *
2351
- * `invalid_json` is the ONLY failure that stops the call here, and it
2352
- * stopped it before this function existed too. `unknown_tool` and
2353
- * `schema_validation` merely OFFER the repair and otherwise fall
2354
- * through to the registry, which reports both with better messages —
2355
- * its schema error already ships a "Required: <field>: <type>" hint the
2356
- * model can self-correct from. So with no repairer configured this is
2357
- * behaviorally identical to the bare `JSON.parse` it replaced.
2358
- */
2359
- private async resolveCall(
2360
- toolCall: ToolCall,
2361
- ): Promise<
2362
- | { ok: true; toolName: string; input: unknown }
2363
- | { ok: false; toolName: string; message: string }
2364
- > {
2365
- let toolName = toolCall.function.name
2366
- let raw = toolCall.function.arguments
2367
-
2368
- for (let attempt = 0; ; attempt++) {
2369
- const failure = this.inspectCall(toolName, raw)
2370
- if (!failure) return { ok: true, toolName, input: parseArguments(raw) }
2371
-
2372
- const repair =
2373
- attempt === 0 && this.config.repairToolCall
2374
- ? await this.requestRepair(toolCall, toolName, failure)
2375
- : null
2376
-
2377
- if (!repair) {
2378
- if (failure.reason === 'invalid_json') {
2379
- return { ok: false, toolName, message: failure.message }
2380
- }
2381
- return { ok: true, toolName, input: parseArguments(raw) }
2382
- }
2383
-
2384
- this.log.info('Repaired a malformed tool call', {
2385
- [NAMZU.RUN_ID]: this.config.runId,
2386
- [GENAI.TOOL_NAME]: toolName,
2387
- 'namzu.runtime.reason': failure.reason,
2388
- ...(repair.toolName && repair.toolName !== toolName
2389
- ? { 'namzu.runtime.repaired_to': repair.toolName }
2390
- : {}),
2391
- })
2392
- toolName = repair.toolName ?? toolName
2393
- raw = repair.arguments
2394
- }
2395
- }
2396
-
2397
- /**
2398
- * What is wrong with this call, or `null` if nothing is.
2399
- *
2400
- * JSON is checked before the tool is looked up: an unparseable argument
2401
- * string is broken regardless of which tool it was aimed at, and it is
2402
- * the one problem the executor itself has to answer.
2403
- */
2404
- private async repairTruncatedCall(
2405
- toolCall: ToolCall,
2406
- toolName: string,
2407
- ): Promise<ToolCallRepair | null> {
2408
- if (!this.config.repairToolCall) return null
2409
-
2410
- // Present the PARTIAL buffer, not the normalized `"{}"` — a repairer
2411
- // handed an empty object has nothing to work from.
2412
- const partial = toolCall.metadata?.partialArguments ?? ''
2413
- const repair = await this.requestRepair(
2414
- { ...toolCall, function: { ...toolCall.function, arguments: partial } },
2415
- toolName,
2416
- { reason: 'invalid_json', message: truncatedToolInputMessage(toolName) },
2417
- )
2418
- if (repair) {
2419
- this.log.info('Repaired a tool call whose input stream was truncated', {
2420
- [NAMZU.RUN_ID]: this.config.runId,
2421
- [GENAI.TOOL_NAME]: toolName,
2422
- 'namzu.runtime.partial_length': partial.length,
2423
- })
2424
- }
2425
- return repair
2426
- }
2427
-
2428
- private inspectCall(
2429
- toolName: string,
2430
- raw: string,
2431
- ): { reason: ToolCallRepairReason; message: string } | null {
2432
- let parsed: unknown
2433
- try {
2434
- parsed = parseArguments(raw)
2435
- } catch {
2436
- return {
2437
- reason: 'invalid_json',
2438
- message: `Error: Invalid JSON in tool arguments for "${toolName}"`,
2439
- }
2440
- }
2441
-
2442
- const tool = this.config.tools.get?.(toolName)
2443
- if (!tool) {
2444
- // Either the model named a tool that does not exist, or this
2445
- // registry does not implement `get`. Both are the registry's to
2446
- // answer; a repairer still gets offered the `unknown_tool` case.
2447
- return {
2448
- reason: 'unknown_tool',
2449
- message: `Error: Unknown tool "${toolName}"`,
2450
- }
2451
- }
2452
-
2453
- // A registry that hands back a tool with no schema has nothing to
2454
- // validate against; that is not a repairable condition, just an
2455
- // unvalidatable one.
2456
- const validation = tool.inputSchema?.safeParse(parsed)
2457
- if (validation && !validation.success) {
2458
- return {
2459
- reason: 'schema_validation',
2460
- message: `Error: Invalid arguments for "${toolName}": ${validation.error.issues
2461
- .map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
2462
- .join('; ')}`,
2463
- }
2464
- }
2465
-
2466
- return null
2467
- }
2468
-
2469
- private async requestRepair(
2470
- toolCall: ToolCall,
2471
- toolName: string,
2472
- failure: { reason: ToolCallRepairReason; message: string },
2473
- ): Promise<ToolCallRepair | null> {
2474
- const repairToolCall = this.config.repairToolCall
2475
- if (!repairToolCall) return null
2476
-
2477
- const tool = this.config.tools.get(toolName)
2478
- try {
2479
- return await repairToolCall({
2480
- toolCall,
2481
- reason: failure.reason,
2482
- message: failure.message,
2483
- ...(tool
2484
- ? {
2485
- tool,
2486
- jsonSchema: tool.modelInputSchema ?? renderToolSchema(tool.inputSchema),
2487
- }
2488
- : {}),
2489
- availableTools: this.config.tools.listNames(),
2490
- })
2491
- } catch (err) {
2492
- // A broken repairer must not turn a recoverable tool error into a
2493
- // failed run: the original error is still a perfectly good answer
2494
- // to give the model.
2495
- this.log.error('repairToolCall threw — falling back to the original error', {
2496
- [NAMZU.RUN_ID]: this.config.runId,
2497
- [GENAI.TOOL_NAME]: toolName,
2498
- 'exception.message': toErrorMessage(err),
2499
- })
2500
- return null
2501
- }
2502
- }
2503
-
2504
2148
  /**
2505
2149
  * One execution attempt, with a throw materialized as an error result.
2506
2150
  *
@@ -2848,13 +2492,3 @@ class Semaphore {
2848
2492
  else this.available++
2849
2493
  }
2850
2494
  }
2851
-
2852
- function formatFailedToolOutput(output: string | undefined, error: string | undefined): string {
2853
- const errorText = `Error: ${error ?? 'Tool execution failed'}`
2854
- if (!output || output.trim().length === 0) return errorText
2855
- return `${output}\n\n${errorText}`
2856
- }
2857
-
2858
- function truncatedToolInputMessage(toolName: string): string {
2859
- return `Error: Tool "${toolName}" call was cut off while the model was streaming JSON arguments. The tool was NOT executed. Retry with a much shorter input. Self-budget content/new_string under 12000 characters before calling file tools. For long files, create a short opening with write and a deterministic marker, then advance that marker with bounded exact edit calls; for delegated work, pass a shared workspace filename/reference instead of embedding the content in the tool call.`
2860
- }