@namzu/sdk 42.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.
- package/CHANGELOG.md +174 -0
- package/dist/manager/resident/outbox.d.ts +8 -8
- package/dist/manager/resident/store.d.ts +4 -4
- package/dist/runtime/query/cancelled-before-start.d.ts +34 -0
- package/dist/runtime/query/cancelled-before-start.d.ts.map +1 -0
- package/dist/runtime/query/cancelled-before-start.js +152 -0
- package/dist/runtime/query/cancelled-before-start.js.map +1 -0
- package/dist/runtime/query/checkpoint.d.ts +21 -0
- package/dist/runtime/query/checkpoint.d.ts.map +1 -1
- package/dist/runtime/query/checkpoint.js +23 -0
- package/dist/runtime/query/checkpoint.js.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.d.ts +57 -0
- package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -0
- package/dist/runtime/query/executor/tool-call-admission.js +373 -0
- package/dist/runtime/query/executor/tool-call-admission.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +70 -35
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +46 -380
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/finalize-run.d.ts +55 -0
- package/dist/runtime/query/finalize-run.d.ts.map +1 -0
- package/dist/runtime/query/finalize-run.js +113 -0
- package/dist/runtime/query/finalize-run.js.map +1 -0
- package/dist/runtime/query/index.d.ts +4 -9
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +238 -893
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +6 -161
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +23 -523
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.d.ts +158 -0
- package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -0
- package/dist/runtime/query/iteration/outstanding-work.js +365 -0
- package/dist/runtime/query/iteration/outstanding-work.js.map +1 -0
- package/dist/runtime/query/iteration/phases/plan.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/plan.js +13 -2
- package/dist/runtime/query/iteration/phases/plan.js.map +1 -1
- package/dist/runtime/query/iteration/step-shaping.d.ts +41 -0
- package/dist/runtime/query/iteration/step-shaping.d.ts.map +1 -0
- package/dist/runtime/query/iteration/step-shaping.js +184 -0
- package/dist/runtime/query/iteration/step-shaping.js.map +1 -0
- package/dist/runtime/query/prepare-run.d.ts +94 -0
- package/dist/runtime/query/prepare-run.d.ts.map +1 -0
- package/dist/runtime/query/prepare-run.js +589 -0
- package/dist/runtime/query/prepare-run.js.map +1 -0
- package/dist/runtime/query/release-run.d.ts +56 -0
- package/dist/runtime/query/release-run.d.ts.map +1 -0
- package/dist/runtime/query/release-run.js +101 -0
- package/dist/runtime/query/release-run.js.map +1 -0
- package/dist/runtime/query/resume-pending.d.ts +112 -1
- package/dist/runtime/query/resume-pending.d.ts.map +1 -1
- package/dist/runtime/query/resume-pending.js +133 -0
- package/dist/runtime/query/resume-pending.js.map +1 -1
- package/dist/store/evidence/compaction-archive.d.ts +2 -2
- package/dist/types/run/config.d.ts +12 -5
- package/dist/types/run/config.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/runtime/query/cancelled-before-start.ts +189 -0
- package/src/runtime/query/checkpoint.ts +22 -0
- package/src/runtime/query/executor/tool-call-admission.ts +473 -0
- package/src/runtime/query/executor.ts +63 -442
- package/src/runtime/query/finalize-run.ts +192 -0
- package/src/runtime/query/index.ts +270 -1011
- package/src/runtime/query/iteration/index.ts +40 -586
- package/src/runtime/query/iteration/outstanding-work.ts +386 -0
- package/src/runtime/query/iteration/phases/plan.ts +18 -2
- package/src/runtime/query/iteration/step-shaping.ts +271 -0
- package/src/runtime/query/prepare-run.ts +718 -0
- package/src/runtime/query/release-run.ts +168 -0
- package/src/runtime/query/resume-pending.ts +158 -0
- package/src/types/run/config.ts +12 -5
|
@@ -9,7 +9,6 @@ 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'
|
|
@@ -38,11 +37,7 @@ import type {
|
|
|
38
37
|
ToolRegistryContract,
|
|
39
38
|
ToolResult,
|
|
40
39
|
} from '../../types/tool/index.js'
|
|
41
|
-
import type {
|
|
42
|
-
RepairToolCall,
|
|
43
|
-
ToolCallRepair,
|
|
44
|
-
ToolCallRepairReason,
|
|
45
|
-
} from '../../types/tool/repair.js'
|
|
40
|
+
import type { RepairToolCall } from '../../types/tool/repair.js'
|
|
46
41
|
import { abortReasonText } from '../../utils/abort.js'
|
|
47
42
|
import { awaitWithAbort } from '../../utils/await-with-abort.js'
|
|
48
43
|
import { type BackoffPolicy, backoffWithJitter, sleep } from '../../utils/backoff.js'
|
|
@@ -51,10 +46,18 @@ import { generateToolCallId } from '../../utils/id.js'
|
|
|
51
46
|
import type { Logger } from '../../utils/logger.js'
|
|
52
47
|
import { compressShellOutput } from '../../utils/shell-compress.js'
|
|
53
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'
|
|
54
58
|
import { describeVisibleFileEvidence } from './file-evidence-context.js'
|
|
55
59
|
import { seedObservationLedger } from './file-evidence-seed.js'
|
|
56
60
|
import { DEFAULT_TOOL_RESULT_GUARDRAILS } from './guardrail-presets.js'
|
|
57
|
-
import { skippedToolResultText } from './plugin-hooks.js'
|
|
58
61
|
import type { ToolResultObservation } from './project-instructions.js'
|
|
59
62
|
import { ToolCallBudget, assertMaxToolCalls } from './tool-call-budget.js'
|
|
60
63
|
import {
|
|
@@ -67,7 +70,7 @@ import {
|
|
|
67
70
|
|
|
68
71
|
export type EmitEvent = (event: RunEvent) => Promise<void>
|
|
69
72
|
|
|
70
|
-
type PreparedDirectCall =
|
|
73
|
+
export type PreparedDirectCall =
|
|
71
74
|
| {
|
|
72
75
|
readonly kind: 'ready'
|
|
73
76
|
readonly toolCall: ToolCall
|
|
@@ -287,14 +290,6 @@ export const DEFAULT_TOOL_RETRY_BACKOFF: BackoffPolicy = {
|
|
|
287
290
|
maxDelayMs: 16_000,
|
|
288
291
|
}
|
|
289
292
|
|
|
290
|
-
/**
|
|
291
|
-
* An empty arguments string means "no arguments", not "malformed" — the
|
|
292
|
-
* shape a no-parameter tool arrives in.
|
|
293
|
-
*/
|
|
294
|
-
function parseArguments(raw: string): unknown {
|
|
295
|
-
return JSON.parse(raw || '{}')
|
|
296
|
-
}
|
|
297
|
-
|
|
298
293
|
export interface ToolExecutorConfig {
|
|
299
294
|
fileReadTracker?: FileReadTracker
|
|
300
295
|
tools: ToolRegistryContract
|
|
@@ -459,7 +454,7 @@ interface PostToolOverride {
|
|
|
459
454
|
readonly content?: ToolResultContent
|
|
460
455
|
}
|
|
461
456
|
|
|
462
|
-
type PreToolHookOutcome =
|
|
457
|
+
export type PreToolHookOutcome =
|
|
463
458
|
| { kind: 'continue'; input: unknown; modified: boolean }
|
|
464
459
|
| { kind: 'skip'; input: unknown; output: string }
|
|
465
460
|
| { kind: 'error'; input: unknown; output: string }
|
|
@@ -642,10 +637,28 @@ export class ToolExecutor {
|
|
|
642
637
|
*
|
|
643
638
|
* `denials` marks ids that must NOT run: each is answered with a
|
|
644
639
|
* synthetic error result carrying the caller's reason instead of being
|
|
645
|
-
* executed.
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
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.
|
|
649
662
|
*
|
|
650
663
|
* Answering with `is_error` semantics rather than dropping the call is
|
|
651
664
|
* the universal contract across providers: an unanswered `tool_use`
|
|
@@ -745,7 +758,7 @@ export class ToolExecutor {
|
|
|
745
758
|
assertUniqueToolCallIds(response.message.toolCalls ?? [])
|
|
746
759
|
const calls = new Map<string, PreparedDirectCall>()
|
|
747
760
|
for (const toolCall of response.message.toolCalls ?? []) {
|
|
748
|
-
calls.set(toolCall.id, await this.
|
|
761
|
+
calls.set(toolCall.id, await prepareDirectCall(this.admissionHost(), toolCall))
|
|
749
762
|
}
|
|
750
763
|
return this.publishPreparedBatch(calls)
|
|
751
764
|
}
|
|
@@ -763,7 +776,7 @@ export class ToolExecutor {
|
|
|
763
776
|
const calls = new Map((previous as OwnedPreparedToolBatch).calls)
|
|
764
777
|
for (const toolCall of response.message.toolCalls ?? []) {
|
|
765
778
|
if (changedCallIds.has(toolCall.id)) {
|
|
766
|
-
calls.set(toolCall.id, await this.
|
|
779
|
+
calls.set(toolCall.id, await prepareDirectCall(this.admissionHost(), toolCall))
|
|
767
780
|
}
|
|
768
781
|
}
|
|
769
782
|
return this.publishPreparedBatch(calls)
|
|
@@ -1441,7 +1454,7 @@ export class ToolExecutor {
|
|
|
1441
1454
|
// unused. Offer it the partial buffer first.
|
|
1442
1455
|
const truncationRepair =
|
|
1443
1456
|
toolCall.metadata?.inputTruncated === true
|
|
1444
|
-
? await this.
|
|
1457
|
+
? await repairTruncatedCall(this.admissionHost(), toolCall, toolName)
|
|
1445
1458
|
: null
|
|
1446
1459
|
|
|
1447
1460
|
if (toolCall.metadata?.inputTruncated === true && !truncationRepair) {
|
|
@@ -1473,7 +1486,8 @@ export class ToolExecutor {
|
|
|
1473
1486
|
// error went back as a `tool_result`, the model re-read the whole
|
|
1474
1487
|
// context and tried again. A host that can repair it locally turns
|
|
1475
1488
|
// that into nothing. No-op when no repairer is configured.
|
|
1476
|
-
const resolved = await
|
|
1489
|
+
const resolved = await resolveCall(
|
|
1490
|
+
this.admissionHost(),
|
|
1477
1491
|
truncationRepair
|
|
1478
1492
|
? {
|
|
1479
1493
|
...toolCall,
|
|
@@ -1521,7 +1535,7 @@ export class ToolExecutor {
|
|
|
1521
1535
|
|
|
1522
1536
|
let preOutcome: PreToolHookOutcome
|
|
1523
1537
|
try {
|
|
1524
|
-
preOutcome = await this.
|
|
1538
|
+
preOutcome = await runPreToolHook(this.admissionHost(), toolName, input)
|
|
1525
1539
|
} catch (error) {
|
|
1526
1540
|
if (!this.config.abortSignal.aborted) throw error
|
|
1527
1541
|
// A later call's interrupted preparation must not reject the batch
|
|
@@ -2033,23 +2047,22 @@ export class ToolExecutor {
|
|
|
2033
2047
|
}
|
|
2034
2048
|
}
|
|
2035
2049
|
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
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 }
|
|
2053
2066
|
}
|
|
2054
2067
|
|
|
2055
2068
|
private async prepareNestedCall(
|
|
@@ -2068,7 +2081,7 @@ export class ToolExecutor {
|
|
|
2068
2081
|
isError: true,
|
|
2069
2082
|
}
|
|
2070
2083
|
}
|
|
2071
|
-
const preOutcome = await this.
|
|
2084
|
+
const preOutcome = await runPreToolHook(this.admissionHost(), toolName, input, signal)
|
|
2072
2085
|
if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
|
|
2073
2086
|
return {
|
|
2074
2087
|
kind: 'synthetic',
|
|
@@ -2099,7 +2112,12 @@ export class ToolExecutor {
|
|
|
2099
2112
|
isError: true,
|
|
2100
2113
|
}
|
|
2101
2114
|
}
|
|
2102
|
-
const preOutcome = await
|
|
2115
|
+
const preOutcome = await runPreToolHook(
|
|
2116
|
+
this.admissionHost(),
|
|
2117
|
+
toolName,
|
|
2118
|
+
preparation.prepared.input,
|
|
2119
|
+
signal,
|
|
2120
|
+
)
|
|
2103
2121
|
if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
|
|
2104
2122
|
return {
|
|
2105
2123
|
kind: 'synthetic',
|
|
@@ -2127,393 +2145,6 @@ export class ToolExecutor {
|
|
|
2127
2145
|
return { kind: 'ready', input: modified.prepared.input, prepared: modified.prepared }
|
|
2128
2146
|
}
|
|
2129
2147
|
|
|
2130
|
-
private async prepareDirectCall(toolCall: ToolCall): Promise<PreparedDirectCall> {
|
|
2131
|
-
let toolName = toolCall.function.name
|
|
2132
|
-
const truncationRepair =
|
|
2133
|
-
toolCall.metadata?.inputTruncated === true
|
|
2134
|
-
? await this.repairTruncatedCall(toolCall, toolName)
|
|
2135
|
-
: null
|
|
2136
|
-
if (toolCall.metadata?.inputTruncated === true && !truncationRepair) {
|
|
2137
|
-
return {
|
|
2138
|
-
kind: 'synthetic',
|
|
2139
|
-
toolCall,
|
|
2140
|
-
toolName,
|
|
2141
|
-
input: {},
|
|
2142
|
-
message: truncatedToolInputMessage(toolName),
|
|
2143
|
-
isError: true,
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
|
|
2147
|
-
const prepare = this.config.tools.prepareExecution
|
|
2148
|
-
const executePrepared = this.config.tools.executePrepared
|
|
2149
|
-
if (typeof prepare !== 'function' || typeof executePrepared !== 'function') {
|
|
2150
|
-
const resolved = await this.resolveCall(
|
|
2151
|
-
truncationRepair
|
|
2152
|
-
? {
|
|
2153
|
-
...toolCall,
|
|
2154
|
-
function: {
|
|
2155
|
-
...toolCall.function,
|
|
2156
|
-
name: truncationRepair.toolName ?? toolName,
|
|
2157
|
-
arguments: truncationRepair.arguments,
|
|
2158
|
-
},
|
|
2159
|
-
metadata: {},
|
|
2160
|
-
}
|
|
2161
|
-
: toolCall,
|
|
2162
|
-
)
|
|
2163
|
-
toolName = resolved.toolName
|
|
2164
|
-
if (!resolved.ok) {
|
|
2165
|
-
return {
|
|
2166
|
-
kind: 'synthetic',
|
|
2167
|
-
toolCall,
|
|
2168
|
-
toolName,
|
|
2169
|
-
input: {},
|
|
2170
|
-
message: resolved.message,
|
|
2171
|
-
isError: true,
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
|
-
const preOutcome = await this.runPreToolHook(toolName, resolved.input)
|
|
2175
|
-
if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
|
|
2176
|
-
return {
|
|
2177
|
-
kind: 'synthetic',
|
|
2178
|
-
toolCall,
|
|
2179
|
-
toolName,
|
|
2180
|
-
input: preOutcome.input,
|
|
2181
|
-
message: preOutcome.output,
|
|
2182
|
-
isError: preOutcome.kind === 'error',
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
if (!this.config.authorizationGate) {
|
|
2186
|
-
return {
|
|
2187
|
-
kind: 'legacy',
|
|
2188
|
-
toolCall,
|
|
2189
|
-
toolName,
|
|
2190
|
-
input: preOutcome.input,
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
return {
|
|
2194
|
-
kind: 'synthetic',
|
|
2195
|
-
toolCall,
|
|
2196
|
-
toolName,
|
|
2197
|
-
input: preOutcome.input,
|
|
2198
|
-
message: `Tool "${toolName}" was not executed because its registry cannot bind authorization to one prepared input.`,
|
|
2199
|
-
isError: true,
|
|
2200
|
-
}
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
-
let raw = truncationRepair?.arguments ?? toolCall.function.arguments
|
|
2204
|
-
toolName = truncationRepair?.toolName ?? toolName
|
|
2205
|
-
let repairUsed = truncationRepair !== null
|
|
2206
|
-
let preparation: ReturnType<typeof prepare>
|
|
2207
|
-
for (;;) {
|
|
2208
|
-
let parsed: unknown
|
|
2209
|
-
try {
|
|
2210
|
-
parsed = parseArguments(raw)
|
|
2211
|
-
} catch {
|
|
2212
|
-
const message = `Error: Invalid JSON in tool arguments for "${toolName}"`
|
|
2213
|
-
const repair =
|
|
2214
|
-
!repairUsed && this.config.repairToolCall
|
|
2215
|
-
? await this.requestRepair(toolCall, toolName, {
|
|
2216
|
-
reason: 'invalid_json',
|
|
2217
|
-
message,
|
|
2218
|
-
})
|
|
2219
|
-
: null
|
|
2220
|
-
if (repair) {
|
|
2221
|
-
repairUsed = true
|
|
2222
|
-
toolName = repair.toolName ?? toolName
|
|
2223
|
-
raw = repair.arguments
|
|
2224
|
-
continue
|
|
2225
|
-
}
|
|
2226
|
-
return { kind: 'synthetic', toolCall, toolName, input: {}, message, isError: true }
|
|
2227
|
-
}
|
|
2228
|
-
|
|
2229
|
-
try {
|
|
2230
|
-
preparation = prepare.call(this.config.tools, toolName, parsed)
|
|
2231
|
-
} catch (err) {
|
|
2232
|
-
const message = `Error: Unknown or unavailable tool "${toolName}": ${toErrorMessage(err)}`
|
|
2233
|
-
const repair =
|
|
2234
|
-
!repairUsed && this.config.repairToolCall
|
|
2235
|
-
? await this.requestRepair(toolCall, toolName, {
|
|
2236
|
-
reason: 'unknown_tool',
|
|
2237
|
-
message,
|
|
2238
|
-
})
|
|
2239
|
-
: null
|
|
2240
|
-
if (repair) {
|
|
2241
|
-
repairUsed = true
|
|
2242
|
-
toolName = repair.toolName ?? toolName
|
|
2243
|
-
raw = repair.arguments
|
|
2244
|
-
continue
|
|
2245
|
-
}
|
|
2246
|
-
return { kind: 'synthetic', toolCall, toolName, input: parsed, message, isError: true }
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
if (preparation.success) break
|
|
2250
|
-
const message = formatFailedToolOutput(preparation.result.output, preparation.result.error)
|
|
2251
|
-
const repair =
|
|
2252
|
-
!repairUsed && this.config.repairToolCall
|
|
2253
|
-
? await this.requestRepair(toolCall, toolName, {
|
|
2254
|
-
reason: 'schema_validation',
|
|
2255
|
-
message,
|
|
2256
|
-
})
|
|
2257
|
-
: null
|
|
2258
|
-
if (repair) {
|
|
2259
|
-
repairUsed = true
|
|
2260
|
-
toolName = repair.toolName ?? toolName
|
|
2261
|
-
raw = repair.arguments
|
|
2262
|
-
continue
|
|
2263
|
-
}
|
|
2264
|
-
return {
|
|
2265
|
-
kind: 'synthetic',
|
|
2266
|
-
toolCall,
|
|
2267
|
-
toolName,
|
|
2268
|
-
input: parsed,
|
|
2269
|
-
message,
|
|
2270
|
-
isError: true,
|
|
2271
|
-
}
|
|
2272
|
-
}
|
|
2273
|
-
|
|
2274
|
-
const preOutcome = await this.runPreToolHook(toolName, preparation.prepared.input)
|
|
2275
|
-
if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
|
|
2276
|
-
return {
|
|
2277
|
-
kind: 'synthetic',
|
|
2278
|
-
toolCall,
|
|
2279
|
-
toolName,
|
|
2280
|
-
input: preOutcome.input,
|
|
2281
|
-
message: preOutcome.output,
|
|
2282
|
-
isError: preOutcome.kind === 'error',
|
|
2283
|
-
}
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
if (preOutcome.modified) {
|
|
2287
|
-
const modified = prepare.call(this.config.tools, toolName, preOutcome.input)
|
|
2288
|
-
if (!modified.success) {
|
|
2289
|
-
return {
|
|
2290
|
-
kind: 'synthetic',
|
|
2291
|
-
toolCall,
|
|
2292
|
-
toolName,
|
|
2293
|
-
input: preOutcome.input,
|
|
2294
|
-
message: formatFailedToolOutput(modified.result.output, modified.result.error),
|
|
2295
|
-
isError: true,
|
|
2296
|
-
}
|
|
2297
|
-
}
|
|
2298
|
-
preparation = modified
|
|
2299
|
-
}
|
|
2300
|
-
|
|
2301
|
-
return {
|
|
2302
|
-
kind: 'ready',
|
|
2303
|
-
toolCall,
|
|
2304
|
-
toolName,
|
|
2305
|
-
input: preparation.prepared.input,
|
|
2306
|
-
prepared: preparation.prepared,
|
|
2307
|
-
}
|
|
2308
|
-
}
|
|
2309
|
-
|
|
2310
|
-
private interpretPreToolResults(
|
|
2311
|
-
toolName: string,
|
|
2312
|
-
initialInput: unknown,
|
|
2313
|
-
results: readonly PluginHookResult[],
|
|
2314
|
-
): PreToolHookOutcome {
|
|
2315
|
-
let currentInput = initialInput
|
|
2316
|
-
let modified = false
|
|
2317
|
-
for (const result of results) {
|
|
2318
|
-
switch (result.action) {
|
|
2319
|
-
case 'continue':
|
|
2320
|
-
continue
|
|
2321
|
-
case 'modify':
|
|
2322
|
-
currentInput = result.input
|
|
2323
|
-
modified = true
|
|
2324
|
-
continue
|
|
2325
|
-
case 'skip':
|
|
2326
|
-
return {
|
|
2327
|
-
kind: 'skip',
|
|
2328
|
-
input: currentInput,
|
|
2329
|
-
output: skippedToolResultText(toolName, result.reason),
|
|
2330
|
-
}
|
|
2331
|
-
case 'error':
|
|
2332
|
-
return {
|
|
2333
|
-
kind: 'error',
|
|
2334
|
-
input: currentInput,
|
|
2335
|
-
output: `Error: ${result.message}`,
|
|
2336
|
-
}
|
|
2337
|
-
case 'retry':
|
|
2338
|
-
case 'annotate':
|
|
2339
|
-
// There is no result to replace yet. Rejecting loudly beats
|
|
2340
|
-
// silently ignoring it: a hook author who returned this here
|
|
2341
|
-
// meant to redact something and would otherwise watch the secret
|
|
2342
|
-
// go through.
|
|
2343
|
-
case 'replace':
|
|
2344
|
-
throw new Error(
|
|
2345
|
-
`Plugin hook pre_tool_use returned unsupported action '${result.action}' for tool ${toolName}`,
|
|
2346
|
-
)
|
|
2347
|
-
default: {
|
|
2348
|
-
const _exhaustive: never = result
|
|
2349
|
-
throw new Error(`Unknown PluginHookResult: ${JSON.stringify(_exhaustive)}`)
|
|
2350
|
-
}
|
|
2351
|
-
}
|
|
2352
|
-
}
|
|
2353
|
-
return { kind: 'continue', input: currentInput, modified }
|
|
2354
|
-
}
|
|
2355
|
-
|
|
2356
|
-
/**
|
|
2357
|
-
* Turn the call the model issued into a name and a parsed input, giving
|
|
2358
|
-
* a configured repairer one chance to fix it first.
|
|
2359
|
-
*
|
|
2360
|
-
* Exactly one chance: a repairer that produces a call which is still
|
|
2361
|
-
* broken will not do better on a second look, and an unbounded loop
|
|
2362
|
-
* here is a hang rather than a degradation.
|
|
2363
|
-
*
|
|
2364
|
-
* `invalid_json` is the ONLY failure that stops the call here, and it
|
|
2365
|
-
* stopped it before this function existed too. `unknown_tool` and
|
|
2366
|
-
* `schema_validation` merely OFFER the repair and otherwise fall
|
|
2367
|
-
* through to the registry, which reports both with better messages —
|
|
2368
|
-
* its schema error already ships a "Required: <field>: <type>" hint the
|
|
2369
|
-
* model can self-correct from. So with no repairer configured this is
|
|
2370
|
-
* behaviorally identical to the bare `JSON.parse` it replaced.
|
|
2371
|
-
*/
|
|
2372
|
-
private async resolveCall(
|
|
2373
|
-
toolCall: ToolCall,
|
|
2374
|
-
): Promise<
|
|
2375
|
-
| { ok: true; toolName: string; input: unknown }
|
|
2376
|
-
| { ok: false; toolName: string; message: string }
|
|
2377
|
-
> {
|
|
2378
|
-
let toolName = toolCall.function.name
|
|
2379
|
-
let raw = toolCall.function.arguments
|
|
2380
|
-
|
|
2381
|
-
for (let attempt = 0; ; attempt++) {
|
|
2382
|
-
const failure = this.inspectCall(toolName, raw)
|
|
2383
|
-
if (!failure) return { ok: true, toolName, input: parseArguments(raw) }
|
|
2384
|
-
|
|
2385
|
-
const repair =
|
|
2386
|
-
attempt === 0 && this.config.repairToolCall
|
|
2387
|
-
? await this.requestRepair(toolCall, toolName, failure)
|
|
2388
|
-
: null
|
|
2389
|
-
|
|
2390
|
-
if (!repair) {
|
|
2391
|
-
if (failure.reason === 'invalid_json') {
|
|
2392
|
-
return { ok: false, toolName, message: failure.message }
|
|
2393
|
-
}
|
|
2394
|
-
return { ok: true, toolName, input: parseArguments(raw) }
|
|
2395
|
-
}
|
|
2396
|
-
|
|
2397
|
-
this.log.info('Repaired a malformed tool call', {
|
|
2398
|
-
[NAMZU.RUN_ID]: this.config.runId,
|
|
2399
|
-
[GENAI.TOOL_NAME]: toolName,
|
|
2400
|
-
'namzu.runtime.reason': failure.reason,
|
|
2401
|
-
...(repair.toolName && repair.toolName !== toolName
|
|
2402
|
-
? { 'namzu.runtime.repaired_to': repair.toolName }
|
|
2403
|
-
: {}),
|
|
2404
|
-
})
|
|
2405
|
-
toolName = repair.toolName ?? toolName
|
|
2406
|
-
raw = repair.arguments
|
|
2407
|
-
}
|
|
2408
|
-
}
|
|
2409
|
-
|
|
2410
|
-
/**
|
|
2411
|
-
* What is wrong with this call, or `null` if nothing is.
|
|
2412
|
-
*
|
|
2413
|
-
* JSON is checked before the tool is looked up: an unparseable argument
|
|
2414
|
-
* string is broken regardless of which tool it was aimed at, and it is
|
|
2415
|
-
* the one problem the executor itself has to answer.
|
|
2416
|
-
*/
|
|
2417
|
-
private async repairTruncatedCall(
|
|
2418
|
-
toolCall: ToolCall,
|
|
2419
|
-
toolName: string,
|
|
2420
|
-
): Promise<ToolCallRepair | null> {
|
|
2421
|
-
if (!this.config.repairToolCall) return null
|
|
2422
|
-
|
|
2423
|
-
// Present the PARTIAL buffer, not the normalized `"{}"` — a repairer
|
|
2424
|
-
// handed an empty object has nothing to work from.
|
|
2425
|
-
const partial = toolCall.metadata?.partialArguments ?? ''
|
|
2426
|
-
const repair = await this.requestRepair(
|
|
2427
|
-
{ ...toolCall, function: { ...toolCall.function, arguments: partial } },
|
|
2428
|
-
toolName,
|
|
2429
|
-
{ reason: 'invalid_json', message: truncatedToolInputMessage(toolName) },
|
|
2430
|
-
)
|
|
2431
|
-
if (repair) {
|
|
2432
|
-
this.log.info('Repaired a tool call whose input stream was truncated', {
|
|
2433
|
-
[NAMZU.RUN_ID]: this.config.runId,
|
|
2434
|
-
[GENAI.TOOL_NAME]: toolName,
|
|
2435
|
-
'namzu.runtime.partial_length': partial.length,
|
|
2436
|
-
})
|
|
2437
|
-
}
|
|
2438
|
-
return repair
|
|
2439
|
-
}
|
|
2440
|
-
|
|
2441
|
-
private inspectCall(
|
|
2442
|
-
toolName: string,
|
|
2443
|
-
raw: string,
|
|
2444
|
-
): { reason: ToolCallRepairReason; message: string } | null {
|
|
2445
|
-
let parsed: unknown
|
|
2446
|
-
try {
|
|
2447
|
-
parsed = parseArguments(raw)
|
|
2448
|
-
} catch {
|
|
2449
|
-
return {
|
|
2450
|
-
reason: 'invalid_json',
|
|
2451
|
-
message: `Error: Invalid JSON in tool arguments for "${toolName}"`,
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2454
|
-
|
|
2455
|
-
const tool = this.config.tools.get?.(toolName)
|
|
2456
|
-
if (!tool) {
|
|
2457
|
-
// Either the model named a tool that does not exist, or this
|
|
2458
|
-
// registry does not implement `get`. Both are the registry's to
|
|
2459
|
-
// answer; a repairer still gets offered the `unknown_tool` case.
|
|
2460
|
-
return {
|
|
2461
|
-
reason: 'unknown_tool',
|
|
2462
|
-
message: `Error: Unknown tool "${toolName}"`,
|
|
2463
|
-
}
|
|
2464
|
-
}
|
|
2465
|
-
|
|
2466
|
-
// A registry that hands back a tool with no schema has nothing to
|
|
2467
|
-
// validate against; that is not a repairable condition, just an
|
|
2468
|
-
// unvalidatable one.
|
|
2469
|
-
const validation = tool.inputSchema?.safeParse(parsed)
|
|
2470
|
-
if (validation && !validation.success) {
|
|
2471
|
-
return {
|
|
2472
|
-
reason: 'schema_validation',
|
|
2473
|
-
message: `Error: Invalid arguments for "${toolName}": ${validation.error.issues
|
|
2474
|
-
.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
|
|
2475
|
-
.join('; ')}`,
|
|
2476
|
-
}
|
|
2477
|
-
}
|
|
2478
|
-
|
|
2479
|
-
return null
|
|
2480
|
-
}
|
|
2481
|
-
|
|
2482
|
-
private async requestRepair(
|
|
2483
|
-
toolCall: ToolCall,
|
|
2484
|
-
toolName: string,
|
|
2485
|
-
failure: { reason: ToolCallRepairReason; message: string },
|
|
2486
|
-
): Promise<ToolCallRepair | null> {
|
|
2487
|
-
const repairToolCall = this.config.repairToolCall
|
|
2488
|
-
if (!repairToolCall) return null
|
|
2489
|
-
|
|
2490
|
-
const tool = this.config.tools.get(toolName)
|
|
2491
|
-
try {
|
|
2492
|
-
return await repairToolCall({
|
|
2493
|
-
toolCall,
|
|
2494
|
-
reason: failure.reason,
|
|
2495
|
-
message: failure.message,
|
|
2496
|
-
...(tool
|
|
2497
|
-
? {
|
|
2498
|
-
tool,
|
|
2499
|
-
jsonSchema: tool.modelInputSchema ?? renderToolSchema(tool.inputSchema),
|
|
2500
|
-
}
|
|
2501
|
-
: {}),
|
|
2502
|
-
availableTools: this.config.tools.listNames(),
|
|
2503
|
-
})
|
|
2504
|
-
} catch (err) {
|
|
2505
|
-
// A broken repairer must not turn a recoverable tool error into a
|
|
2506
|
-
// failed run: the original error is still a perfectly good answer
|
|
2507
|
-
// to give the model.
|
|
2508
|
-
this.log.error('repairToolCall threw — falling back to the original error', {
|
|
2509
|
-
[NAMZU.RUN_ID]: this.config.runId,
|
|
2510
|
-
[GENAI.TOOL_NAME]: toolName,
|
|
2511
|
-
'exception.message': toErrorMessage(err),
|
|
2512
|
-
})
|
|
2513
|
-
return null
|
|
2514
|
-
}
|
|
2515
|
-
}
|
|
2516
|
-
|
|
2517
2148
|
/**
|
|
2518
2149
|
* One execution attempt, with a throw materialized as an error result.
|
|
2519
2150
|
*
|
|
@@ -2861,13 +2492,3 @@ class Semaphore {
|
|
|
2861
2492
|
else this.available++
|
|
2862
2493
|
}
|
|
2863
2494
|
}
|
|
2864
|
-
|
|
2865
|
-
function formatFailedToolOutput(output: string | undefined, error: string | undefined): string {
|
|
2866
|
-
const errorText = `Error: ${error ?? 'Tool execution failed'}`
|
|
2867
|
-
if (!output || output.trim().length === 0) return errorText
|
|
2868
|
-
return `${output}\n\n${errorText}`
|
|
2869
|
-
}
|
|
2870
|
-
|
|
2871
|
-
function truncatedToolInputMessage(toolName: string): string {
|
|
2872
|
-
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.`
|
|
2873
|
-
}
|