@navels/neal 0.1.0 → 0.2.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/dist/neal/adjudicator/planning.js +48 -0
- package/dist/neal/agents/prompts.js +3 -0
- package/dist/neal/agents/rounds.js +8 -0
- package/dist/neal/agents/schemas.js +575 -496
- package/dist/neal/agents/structured-json.js +36 -0
- package/dist/neal/config.js +24 -0
- package/dist/neal/git.js +9 -3
- package/dist/neal/orchestrator/completion.js +167 -112
- package/dist/neal/orchestrator/phases/planning.js +14 -39
- package/dist/neal/orchestrator/split-plan.js +12 -11
- package/dist/neal/orchestrator/transitions.js +30 -71
- package/dist/neal/plan-doc.js +24 -1
- package/dist/neal/prompts/assert-builder.js +8 -1
- package/dist/neal/prompts/execute.js +4 -0
- package/dist/neal/prompts/specialized.js +22 -6
- package/dist/neal/prompts/specs.js +58 -0
- package/dist/neal/providers/anthropic-claude.js +291 -247
- package/dist/neal/providers/generic-agentic.js +18 -0
- package/dist/neal/providers/openai-codex.js +77 -201
- package/dist/neal/providers/openai-compatible.js +33 -5
- package/dist/neal/providers/pricing.js +124 -0
- package/dist/neal/providers/rate-card.js +2301 -0
- package/dist/neal/providers/telemetry.js +4 -0
- package/dist/neal/retrospective.js +33 -4
- package/dist/neal/run-metrics.js +74 -9
- package/docs/compatible-models.md +11 -0
- package/docs/issue-pipeline.md +124 -0
- package/docs/maintenance.md +30 -19
- package/docs/providers.md +117 -0
- package/docs/release.md +29 -25
- package/package.json +7 -3
|
@@ -125,9 +125,44 @@ async function appendClaudeText(label, state, text, events, sessionHandle, role
|
|
|
125
125
|
}
|
|
126
126
|
return emittedTexts;
|
|
127
127
|
}
|
|
128
|
-
|
|
128
|
+
function getClaudeBashCommand(toolInput) {
|
|
129
|
+
if (toolInput === null || typeof toolInput !== 'object') {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const command = toolInput.command;
|
|
133
|
+
return typeof command === 'string' && command.trim() ? command : null;
|
|
134
|
+
}
|
|
135
|
+
// Extracts the human-readable text of a tool_result block: string content is
|
|
136
|
+
// used as-is; block-array content contributes its text blocks. Non-text
|
|
137
|
+
// content (images, documents) yields null.
|
|
138
|
+
function getClaudeToolResultText(content) {
|
|
139
|
+
if (typeof content === 'string') {
|
|
140
|
+
return content;
|
|
141
|
+
}
|
|
142
|
+
if (!Array.isArray(content)) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const texts = [];
|
|
146
|
+
for (const block of content) {
|
|
147
|
+
if (block !== null &&
|
|
148
|
+
typeof block === 'object' &&
|
|
149
|
+
block.type === 'text' &&
|
|
150
|
+
typeof block.text === 'string') {
|
|
151
|
+
texts.push(block.text);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return texts.length > 0 ? texts.join('\n') : null;
|
|
155
|
+
}
|
|
156
|
+
async function logClaudeMessage(label, message, events, state, sessionHandle, role = roleForClaudeLabel(label), cwd) {
|
|
129
157
|
switch (message.type) {
|
|
130
158
|
case 'assistant': {
|
|
159
|
+
if (state) {
|
|
160
|
+
for (const block of message.message.content) {
|
|
161
|
+
if (block.type === 'tool_use') {
|
|
162
|
+
state.pendingToolUses.set(block.id, { name: block.name, input: block.input });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
131
166
|
if (state?.sawTextDelta) {
|
|
132
167
|
return [];
|
|
133
168
|
}
|
|
@@ -150,6 +185,72 @@ async function logClaudeMessage(label, message, events, state, sessionHandle, ro
|
|
|
150
185
|
}
|
|
151
186
|
break;
|
|
152
187
|
}
|
|
188
|
+
case 'user': {
|
|
189
|
+
// SDK tool executions surface as user messages carrying tool_result
|
|
190
|
+
// blocks. Matching them against the turn's pending tool_use blocks maps
|
|
191
|
+
// Bash runs to `command_completed` and successful write-tool calls to
|
|
192
|
+
// `file_changed`. Read-class tools (Read/Grep/Glob) intentionally emit
|
|
193
|
+
// nothing here — they are not verification evidence. The mapping is
|
|
194
|
+
// role-agnostic: advisor rounds run a read-only toolset, so they never
|
|
195
|
+
// produce Bash or write tool_uses.
|
|
196
|
+
if (!state) {
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
const content = message.message.content;
|
|
200
|
+
if (!Array.isArray(content)) {
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
for (const block of content) {
|
|
204
|
+
if (block.type !== 'tool_result') {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const pending = state.pendingToolUses.get(block.tool_use_id);
|
|
208
|
+
if (!pending) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
state.pendingToolUses.delete(block.tool_use_id);
|
|
212
|
+
if (pending.name === 'Bash') {
|
|
213
|
+
const command = getClaudeBashCommand(pending.input);
|
|
214
|
+
if (!command) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
// The SDK's tool_result exposes no exit code, so `exitCode` is
|
|
218
|
+
// omitted; `status` carries the is_error signal instead.
|
|
219
|
+
const output = getClaudeToolResultText(block.content);
|
|
220
|
+
await emitProviderEvent(events, {
|
|
221
|
+
type: 'command_completed',
|
|
222
|
+
provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
|
|
223
|
+
role,
|
|
224
|
+
label,
|
|
225
|
+
sessionHandle,
|
|
226
|
+
itemId: block.tool_use_id,
|
|
227
|
+
command,
|
|
228
|
+
status: block.is_error === true ? 'failed' : 'completed',
|
|
229
|
+
...(output !== null ? { output } : {}),
|
|
230
|
+
...(cwd ? { cwd } : {}),
|
|
231
|
+
providerData: { sdkMessageType: message.type, toolName: pending.name, toolUseId: block.tool_use_id },
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
else if (CLAUDE_WRITE_TOOL_NAMES.has(pending.name) && block.is_error !== true) {
|
|
235
|
+
// An is_error result means the write did not happen (e.g. a
|
|
236
|
+
// write-path guard deny), so it must not claim a file change.
|
|
237
|
+
const filePath = getClaudeWriteToolPath(pending.input);
|
|
238
|
+
if (!filePath) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
await emitProviderEvent(events, {
|
|
242
|
+
type: 'file_changed',
|
|
243
|
+
provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
|
|
244
|
+
role,
|
|
245
|
+
label,
|
|
246
|
+
sessionHandle,
|
|
247
|
+
files: [filePath],
|
|
248
|
+
providerData: { sdkMessageType: message.type, toolName: pending.name, toolUseId: block.tool_use_id },
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
153
254
|
case 'stream_event':
|
|
154
255
|
if (message.event.type === 'content_block_delta') {
|
|
155
256
|
if (message.event.delta.type === 'text_delta' && state) {
|
|
@@ -274,6 +375,9 @@ async function logClaudeMessage(label, message, events, state, sessionHandle, ro
|
|
|
274
375
|
}
|
|
275
376
|
break;
|
|
276
377
|
case 'result':
|
|
378
|
+
// Turn boundary: a tool_use whose result never arrived must not match a
|
|
379
|
+
// tool_result from a later turn in the same stream.
|
|
380
|
+
state?.pendingToolUses.clear();
|
|
277
381
|
await emitProviderEvent(events, {
|
|
278
382
|
type: 'turn_completed',
|
|
279
383
|
provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
|
|
@@ -281,6 +385,10 @@ async function logClaudeMessage(label, message, events, state, sessionHandle, ro
|
|
|
281
385
|
label,
|
|
282
386
|
sessionHandle,
|
|
283
387
|
usage: message.usage,
|
|
388
|
+
// One `result` message is one query invocation = one turn, so
|
|
389
|
+
// `total_cost_usd` is a per-turn figure that sums cleanly across turns.
|
|
390
|
+
costUsd: message.total_cost_usd,
|
|
391
|
+
costSource: 'provider',
|
|
284
392
|
providerData: { sdkMessageType: message.type, subtype: message.subtype },
|
|
285
393
|
});
|
|
286
394
|
if (message.usage !== undefined) {
|
|
@@ -291,6 +399,8 @@ async function logClaudeMessage(label, message, events, state, sessionHandle, ro
|
|
|
291
399
|
label,
|
|
292
400
|
sessionHandle,
|
|
293
401
|
usage: message.usage,
|
|
402
|
+
costUsd: message.total_cost_usd,
|
|
403
|
+
costSource: 'provider',
|
|
294
404
|
providerData: { sdkMessageType: message.type, subtype: message.subtype },
|
|
295
405
|
});
|
|
296
406
|
}
|
|
@@ -401,7 +511,12 @@ async function collectClaudeResult(stream, cwd, label, inactivityTimeoutMs, even
|
|
|
401
511
|
let sessionHandle = null;
|
|
402
512
|
let lastResult = null;
|
|
403
513
|
let firstStructuredResult = null;
|
|
404
|
-
const logState = {
|
|
514
|
+
const logState = {
|
|
515
|
+
textBuffer: '',
|
|
516
|
+
sawTextDelta: false,
|
|
517
|
+
lastThinkingEmitMs: 0,
|
|
518
|
+
pendingToolUses: new Map(),
|
|
519
|
+
};
|
|
405
520
|
const assistantTexts = [];
|
|
406
521
|
const iterator = stream[Symbol.asyncIterator]();
|
|
407
522
|
while (true) {
|
|
@@ -435,7 +550,7 @@ async function collectClaudeResult(stream, cwd, label, inactivityTimeoutMs, even
|
|
|
435
550
|
providerData: { sdkMessageType: message.type },
|
|
436
551
|
});
|
|
437
552
|
}
|
|
438
|
-
const emittedAssistantTexts = await logClaudeMessage(label, message, events, logState, sessionHandle, role);
|
|
553
|
+
const emittedAssistantTexts = await logClaudeMessage(label, message, events, logState, sessionHandle, role, cwd);
|
|
439
554
|
if (emittedAssistantTexts.length > 0) {
|
|
440
555
|
assistantTexts.push(emittedAssistantTexts.join('\n\n'));
|
|
441
556
|
}
|
|
@@ -484,36 +599,57 @@ function createClaudeTurnAbortController(signal) {
|
|
|
484
599
|
function deriveClaudeAbortController(signal) {
|
|
485
600
|
return signal ? createClaudeTurnAbortController(signal) : undefined;
|
|
486
601
|
}
|
|
487
|
-
function
|
|
602
|
+
function buildClaudeCoreQueryOptions(spec) {
|
|
488
603
|
return {
|
|
489
|
-
cwd:
|
|
490
|
-
...(abortController ? { abortController } : {}),
|
|
491
|
-
...(
|
|
492
|
-
...(
|
|
493
|
-
tools:
|
|
604
|
+
cwd: spec.cwd,
|
|
605
|
+
...(spec.abortController ? { abortController: spec.abortController } : {}),
|
|
606
|
+
...(spec.model ? { model: spec.model } : {}),
|
|
607
|
+
...(spec.effort ? { effort: spec.effort } : {}),
|
|
608
|
+
tools: spec.tools,
|
|
609
|
+
...(spec.hooks ? { hooks: spec.hooks } : {}),
|
|
494
610
|
permissionMode: 'bypassPermissions',
|
|
495
611
|
allowDangerouslySkipPermissions: true,
|
|
496
|
-
...(
|
|
497
|
-
...(claudeExecutablePath ? { pathToClaudeCodeExecutable: claudeExecutablePath } : {}),
|
|
612
|
+
...(spec.resumeHandle ? { resume: spec.resumeHandle } : {}),
|
|
613
|
+
...(spec.claudeExecutablePath ? { pathToClaudeCodeExecutable: spec.claudeExecutablePath } : {}),
|
|
614
|
+
...(spec.outputSchema
|
|
615
|
+
? {
|
|
616
|
+
outputFormat: {
|
|
617
|
+
type: 'json_schema',
|
|
618
|
+
schema: spec.outputSchema,
|
|
619
|
+
},
|
|
620
|
+
}
|
|
621
|
+
: {}),
|
|
498
622
|
stderr: (data) => {
|
|
499
|
-
void
|
|
623
|
+
void spec.events?.({
|
|
500
624
|
type: 'tool_progress',
|
|
501
625
|
provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
|
|
502
|
-
role:
|
|
503
|
-
label:
|
|
504
|
-
sessionHandle:
|
|
626
|
+
role: spec.stderrRole,
|
|
627
|
+
label: spec.stderrLabel,
|
|
628
|
+
sessionHandle: spec.stderrSessionHandle,
|
|
505
629
|
toolName: 'stderr',
|
|
506
630
|
message: data,
|
|
507
631
|
isError: true,
|
|
508
632
|
providerData: { stream: 'stderr' },
|
|
509
633
|
});
|
|
510
634
|
},
|
|
511
|
-
outputFormat: {
|
|
512
|
-
type: 'json_schema',
|
|
513
|
-
schema: args.schema,
|
|
514
|
-
},
|
|
515
635
|
};
|
|
516
636
|
}
|
|
637
|
+
function buildClaudeQueryOptions(args, defaultModel, claudeExecutablePath = getClaudeCodeExecutablePath(), defaultEffort, abortController = deriveClaudeAbortController(args.signal)) {
|
|
638
|
+
return buildClaudeCoreQueryOptions({
|
|
639
|
+
cwd: args.cwd,
|
|
640
|
+
abortController,
|
|
641
|
+
model: args.model ?? defaultModel,
|
|
642
|
+
effort: defaultEffort,
|
|
643
|
+
tools: ['Read', 'Grep', 'Glob'],
|
|
644
|
+
resumeHandle: args.resumeHandle,
|
|
645
|
+
claudeExecutablePath,
|
|
646
|
+
outputSchema: args.schema,
|
|
647
|
+
events: args.events,
|
|
648
|
+
stderrRole: 'structured-advisor',
|
|
649
|
+
stderrLabel: args.label,
|
|
650
|
+
stderrSessionHandle: args.resumeHandle ?? null,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
517
653
|
function buildClaudeQueryStream(args, defaultModel, defaultEffort, abortController) {
|
|
518
654
|
return query({
|
|
519
655
|
prompt: args.prompt,
|
|
@@ -527,30 +663,19 @@ function buildClaudeJsonBlockReviewPrompt(args) {
|
|
|
527
663
|
return args.prompt;
|
|
528
664
|
}
|
|
529
665
|
function buildClaudeJsonBlockQueryOptions(args, defaultModel, claudeExecutablePath = getClaudeCodeExecutablePath(), defaultEffort, abortController = deriveClaudeAbortController(args.signal)) {
|
|
530
|
-
return {
|
|
666
|
+
return buildClaudeCoreQueryOptions({
|
|
531
667
|
cwd: args.cwd,
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
668
|
+
abortController,
|
|
669
|
+
model: args.model ?? defaultModel,
|
|
670
|
+
effort: defaultEffort,
|
|
535
671
|
tools: ['Read', 'Grep', 'Glob'],
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
|
|
544
|
-
role: 'structured-advisor',
|
|
545
|
-
label: args.label,
|
|
546
|
-
sessionHandle: args.resumeHandle ?? null,
|
|
547
|
-
toolName: 'stderr',
|
|
548
|
-
message: data,
|
|
549
|
-
isError: true,
|
|
550
|
-
providerData: { stream: 'stderr' },
|
|
551
|
-
});
|
|
552
|
-
},
|
|
553
|
-
};
|
|
672
|
+
resumeHandle: args.resumeHandle,
|
|
673
|
+
claudeExecutablePath,
|
|
674
|
+
events: args.events,
|
|
675
|
+
stderrRole: 'structured-advisor',
|
|
676
|
+
stderrLabel: args.label,
|
|
677
|
+
stderrSessionHandle: args.resumeHandle ?? null,
|
|
678
|
+
});
|
|
554
679
|
}
|
|
555
680
|
function buildClaudeJsonBlockQueryStream(args, defaultModel, defaultEffort, abortController) {
|
|
556
681
|
return query({
|
|
@@ -559,29 +684,18 @@ function buildClaudeJsonBlockQueryStream(args, defaultModel, defaultEffort, abor
|
|
|
559
684
|
});
|
|
560
685
|
}
|
|
561
686
|
function buildClaudeJsonBlockRepairQueryOptions(args, defaultModel, claudeExecutablePath = getClaudeCodeExecutablePath(), defaultEffort, abortController) {
|
|
562
|
-
return {
|
|
687
|
+
return buildClaudeCoreQueryOptions({
|
|
563
688
|
cwd: args.cwd,
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
689
|
+
abortController,
|
|
690
|
+
model: args.model ?? defaultModel,
|
|
691
|
+
effort: defaultEffort,
|
|
567
692
|
tools: [],
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
|
|
575
|
-
role: 'structured-advisor',
|
|
576
|
-
label: `${args.label}:structured-json-repair`,
|
|
577
|
-
sessionHandle: null,
|
|
578
|
-
toolName: 'stderr',
|
|
579
|
-
message: data,
|
|
580
|
-
isError: true,
|
|
581
|
-
providerData: { stream: 'stderr' },
|
|
582
|
-
});
|
|
583
|
-
},
|
|
584
|
-
};
|
|
693
|
+
claudeExecutablePath,
|
|
694
|
+
events: args.events,
|
|
695
|
+
stderrRole: 'structured-advisor',
|
|
696
|
+
stderrLabel: `${args.label}:structured-json-repair`,
|
|
697
|
+
stderrSessionHandle: null,
|
|
698
|
+
});
|
|
585
699
|
}
|
|
586
700
|
function buildClaudeJsonBlockRepairQueryStream(args, repairPrompt, defaultModel, defaultEffort, abortController) {
|
|
587
701
|
return query({
|
|
@@ -959,7 +1073,33 @@ async function validateOrRepairClaudeJsonBlockResponse(args) {
|
|
|
959
1073
|
throw providerError;
|
|
960
1074
|
}
|
|
961
1075
|
}
|
|
962
|
-
|
|
1076
|
+
// Both advisor protocols give up on an unsuccessful Claude result with the
|
|
1077
|
+
// same normalized provider error; only the loop position at which each
|
|
1078
|
+
// protocol reaches this differs (see runClaudeStructuredAdvisorRoundLoop).
|
|
1079
|
+
async function createClaudeAdvisorUnsuccessfulResultError(details) {
|
|
1080
|
+
const providerError = createClaudeProviderError({
|
|
1081
|
+
message: details.resultErrorMessage
|
|
1082
|
+
? `Claude ${details.label} did not return a successful result${details.subtype ? ` (${details.subtype})` : ''}: ${details.resultErrorMessage}`
|
|
1083
|
+
: `Claude ${details.label} did not return a successful result${details.subtype ? ` (${details.subtype})` : ''}`,
|
|
1084
|
+
role: 'structured-advisor',
|
|
1085
|
+
sessionHandle: details.sessionHandle,
|
|
1086
|
+
kind: getClaudeStructuredFailureKind(details.subtype, details.resultErrorMessage),
|
|
1087
|
+
subtype: details.subtype,
|
|
1088
|
+
});
|
|
1089
|
+
await emitClaudeProviderError(details.events, providerError, details.label);
|
|
1090
|
+
return providerError;
|
|
1091
|
+
}
|
|
1092
|
+
// One advisor round loop serves both structured-output protocols. The shared
|
|
1093
|
+
// skeleton — per-turn abort controller, assistant-text mirroring, transient
|
|
1094
|
+
// api_error retries with exponential backoff, transient error-result
|
|
1095
|
+
// retries, and the final unsuccessful-result error — is identical for both;
|
|
1096
|
+
// the `run.protocol` discriminator selects the json-block behavior (local
|
|
1097
|
+
// extraction/validation with the repair sub-loop, plus the mirrored-prose
|
|
1098
|
+
// recovery when the stream fails after emitting assistant text) or the
|
|
1099
|
+
// native behavior (SDK `json_schema` structured output with stream-error and
|
|
1100
|
+
// retry-exhaustion telemetry).
|
|
1101
|
+
async function runClaudeStructuredAdvisorRoundLoop(run, defaultModel, defaultEffort, sleep = defaultSleep) {
|
|
1102
|
+
const args = run.args;
|
|
963
1103
|
let sessionHandle = args.resumeHandle ?? null;
|
|
964
1104
|
let apiRetryCount = 0;
|
|
965
1105
|
const apiRetryLimit = args.apiRetryLimit;
|
|
@@ -974,56 +1114,9 @@ async function runClaudeJsonBlockStructuredAdvisorRound(args, defaultModel, crea
|
|
|
974
1114
|
await args.events?.(event);
|
|
975
1115
|
}
|
|
976
1116
|
: undefined;
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
result = await collectClaudeResult(createStream(args, defaultModel, defaultEffort, turnAbortController), args.cwd, args.label, args.inactivityTimeoutMs, primaryEvents, 'structured-advisor', turnAbortController);
|
|
981
|
-
}
|
|
982
|
-
catch (error) {
|
|
983
|
-
const providerError = normalizeClaudeProviderError(error, {
|
|
984
|
-
role: 'structured-advisor',
|
|
985
|
-
sessionHandle,
|
|
986
|
-
});
|
|
987
|
-
if (providerError.kind === 'api_error' && apiRetryCount < apiRetryLimit) {
|
|
988
|
-
apiRetryCount += 1;
|
|
989
|
-
await emitProviderEvent(args.events, {
|
|
990
|
-
type: 'tool_progress',
|
|
991
|
-
provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
|
|
992
|
-
role: 'structured-advisor',
|
|
993
|
-
label: args.label,
|
|
994
|
-
sessionHandle,
|
|
995
|
-
toolName: 'api_retry',
|
|
996
|
-
message: `transient API failure; retrying review (${apiRetryCount}/${apiRetryLimit})`,
|
|
997
|
-
isError: true,
|
|
998
|
-
providerData: {
|
|
999
|
-
retryCount: apiRetryCount,
|
|
1000
|
-
retryLimit: apiRetryLimit,
|
|
1001
|
-
message: providerError.message,
|
|
1002
|
-
},
|
|
1003
|
-
});
|
|
1004
|
-
await sleep(getApiRetryDelayMs(apiRetryCount));
|
|
1005
|
-
continue;
|
|
1006
|
-
}
|
|
1007
|
-
const mirroredAssistantText = mirroredAssistantTexts.join('\n\n').trim();
|
|
1008
|
-
if (mirroredAssistantText &&
|
|
1009
|
-
(providerError.kind === 'structured_output_invalid' || providerError.kind === 'structured_output_missing')) {
|
|
1010
|
-
return await validateOrRepairClaudeJsonBlockResponse({
|
|
1011
|
-
roundArgs: args,
|
|
1012
|
-
assistantText: mirroredAssistantText,
|
|
1013
|
-
defaultModel,
|
|
1014
|
-
defaultEffort,
|
|
1015
|
-
sessionHandle: providerError.sessionHandle ?? sessionHandle,
|
|
1016
|
-
createRepairStream,
|
|
1017
|
-
});
|
|
1018
|
-
}
|
|
1019
|
-
await emitClaudeProviderError(args.events, providerError, args.label);
|
|
1020
|
-
throw providerError;
|
|
1021
|
-
}
|
|
1022
|
-
sessionHandle = result.sessionHandle ?? sessionHandle;
|
|
1023
|
-
const subtype = result.lastResult?.subtype ?? null;
|
|
1024
|
-
const resultErrorMessage = getClaudeResultErrorMessage(result.lastResult);
|
|
1025
|
-
const isErrorResult = result.lastResult?.is_error === true;
|
|
1026
|
-
if (isErrorResult && isTransientClaudeFailure(subtype, resultErrorMessage) && apiRetryCount < apiRetryLimit) {
|
|
1117
|
+
// Reads `sessionHandle` at call time, so post-collect callers observe the
|
|
1118
|
+
// handle updated from the just-collected result.
|
|
1119
|
+
const retryAfterTransientErrorResult = async (subtype, message) => {
|
|
1027
1120
|
apiRetryCount += 1;
|
|
1028
1121
|
await emitProviderEvent(args.events, {
|
|
1029
1122
|
type: 'tool_progress',
|
|
@@ -1038,60 +1131,15 @@ async function runClaudeJsonBlockStructuredAdvisorRound(args, defaultModel, crea
|
|
|
1038
1131
|
retryCount: apiRetryCount,
|
|
1039
1132
|
retryLimit: apiRetryLimit,
|
|
1040
1133
|
subtype,
|
|
1041
|
-
message
|
|
1134
|
+
message,
|
|
1042
1135
|
},
|
|
1043
1136
|
});
|
|
1044
1137
|
await sleep(getApiRetryDelayMs(apiRetryCount));
|
|
1045
|
-
|
|
1046
|
-
}
|
|
1047
|
-
const structuredRetryExhausted = isStructuredOutputRetryExhaustion(subtype, resultErrorMessage);
|
|
1048
|
-
if (subtype && subtype !== 'success' && !structuredRetryExhausted) {
|
|
1049
|
-
const providerError = createClaudeProviderError({
|
|
1050
|
-
message: resultErrorMessage
|
|
1051
|
-
? `Claude ${args.label} did not return a successful result${subtype ? ` (${subtype})` : ''}: ${resultErrorMessage}`
|
|
1052
|
-
: `Claude ${args.label} did not return a successful result${subtype ? ` (${subtype})` : ''}`,
|
|
1053
|
-
role: 'structured-advisor',
|
|
1054
|
-
sessionHandle,
|
|
1055
|
-
kind: getClaudeStructuredFailureKind(subtype, resultErrorMessage),
|
|
1056
|
-
subtype,
|
|
1057
|
-
});
|
|
1058
|
-
await emitClaudeProviderError(args.events, providerError, args.label);
|
|
1059
|
-
throw providerError;
|
|
1060
|
-
}
|
|
1061
|
-
const assistantText = getClaudeAssistantTextForLocalJson(result, mirroredAssistantTexts.join('\n\n').trim());
|
|
1062
|
-
return await validateOrRepairClaudeJsonBlockResponse({
|
|
1063
|
-
roundArgs: args,
|
|
1064
|
-
assistantText,
|
|
1065
|
-
defaultModel,
|
|
1066
|
-
defaultEffort,
|
|
1067
|
-
sessionHandle,
|
|
1068
|
-
createRepairStream,
|
|
1069
|
-
});
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
async function runClaudeStructuredAdvisorRound(args, defaultModel, createStream = buildClaudeQueryStream, defaultEffort, sleep = defaultSleep) {
|
|
1073
|
-
if (isClaudeJsonBlockStructuredAdvisorRound(args)) {
|
|
1074
|
-
return await runClaudeJsonBlockStructuredAdvisorRound(args, defaultModel, undefined, undefined, defaultEffort, sleep);
|
|
1075
|
-
}
|
|
1076
|
-
let sessionHandle = args.resumeHandle ?? null;
|
|
1077
|
-
let apiRetryCount = 0;
|
|
1078
|
-
const apiRetryLimit = args.apiRetryLimit;
|
|
1079
|
-
while (true) {
|
|
1138
|
+
};
|
|
1080
1139
|
let result;
|
|
1081
|
-
const mirroredAssistantTexts = [];
|
|
1082
|
-
const primaryEvents = args.events
|
|
1083
|
-
? async (event) => {
|
|
1084
|
-
const text = getMirroredAssistantText(event, 'structured-advisor', args.label);
|
|
1085
|
-
if (text) {
|
|
1086
|
-
mirroredAssistantTexts.push(text);
|
|
1087
|
-
}
|
|
1088
|
-
await args.events?.(event);
|
|
1089
|
-
}
|
|
1090
|
-
: undefined;
|
|
1091
1140
|
try {
|
|
1092
1141
|
const turnAbortController = createClaudeTurnAbortController(args.signal);
|
|
1093
|
-
|
|
1094
|
-
result = await collectClaudeResult(stream, args.cwd, args.label, args.inactivityTimeoutMs, primaryEvents, 'structured-advisor', turnAbortController);
|
|
1142
|
+
result = await collectClaudeResult(run.createStream(run.args, defaultModel, defaultEffort, turnAbortController), args.cwd, args.label, args.inactivityTimeoutMs, primaryEvents, 'structured-advisor', turnAbortController);
|
|
1095
1143
|
}
|
|
1096
1144
|
catch (error) {
|
|
1097
1145
|
const providerError = normalizeClaudeProviderError(error, {
|
|
@@ -1099,7 +1147,7 @@ async function runClaudeStructuredAdvisorRound(args, defaultModel, createStream
|
|
|
1099
1147
|
sessionHandle,
|
|
1100
1148
|
});
|
|
1101
1149
|
const mirroredAssistantText = mirroredAssistantTexts.join('\n\n').trim();
|
|
1102
|
-
if (providerError.kind === 'structured_output_invalid') {
|
|
1150
|
+
if (run.protocol === 'native' && providerError.kind === 'structured_output_invalid') {
|
|
1103
1151
|
await emitProviderEvent(args.events, {
|
|
1104
1152
|
type: 'tool_progress',
|
|
1105
1153
|
provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
|
|
@@ -1137,11 +1185,51 @@ async function runClaudeStructuredAdvisorRound(args, defaultModel, createStream
|
|
|
1137
1185
|
await sleep(getApiRetryDelayMs(apiRetryCount));
|
|
1138
1186
|
continue;
|
|
1139
1187
|
}
|
|
1188
|
+
if (run.protocol === 'json-block' &&
|
|
1189
|
+
mirroredAssistantText &&
|
|
1190
|
+
(providerError.kind === 'structured_output_invalid' || providerError.kind === 'structured_output_missing')) {
|
|
1191
|
+
return await validateOrRepairClaudeJsonBlockResponse({
|
|
1192
|
+
roundArgs: run.args,
|
|
1193
|
+
assistantText: mirroredAssistantText,
|
|
1194
|
+
defaultModel,
|
|
1195
|
+
defaultEffort,
|
|
1196
|
+
sessionHandle: providerError.sessionHandle ?? sessionHandle,
|
|
1197
|
+
createRepairStream: run.createRepairStream,
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1140
1200
|
await emitClaudeProviderError(args.events, providerError, args.label);
|
|
1141
1201
|
throw providerError;
|
|
1142
1202
|
}
|
|
1143
1203
|
sessionHandle = result.sessionHandle ?? sessionHandle;
|
|
1144
1204
|
const lastResult = result.lastResult;
|
|
1205
|
+
const subtype = lastResult?.subtype ?? null;
|
|
1206
|
+
const resultErrorMessage = getClaudeResultErrorMessage(lastResult);
|
|
1207
|
+
const structuredRetryExhausted = isStructuredOutputRetryExhaustion(subtype, resultErrorMessage);
|
|
1208
|
+
const isTransientErrorResult = lastResult?.is_error === true && isTransientClaudeFailure(subtype, resultErrorMessage);
|
|
1209
|
+
if (run.protocol === 'json-block') {
|
|
1210
|
+
if (isTransientErrorResult && apiRetryCount < apiRetryLimit) {
|
|
1211
|
+
await retryAfterTransientErrorResult(subtype, resultErrorMessage);
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
if (subtype && subtype !== 'success' && !structuredRetryExhausted) {
|
|
1215
|
+
throw await createClaudeAdvisorUnsuccessfulResultError({
|
|
1216
|
+
events: args.events,
|
|
1217
|
+
label: args.label,
|
|
1218
|
+
subtype,
|
|
1219
|
+
resultErrorMessage,
|
|
1220
|
+
sessionHandle,
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
const assistantText = getClaudeAssistantTextForLocalJson(result, mirroredAssistantTexts.join('\n\n').trim());
|
|
1224
|
+
return await validateOrRepairClaudeJsonBlockResponse({
|
|
1225
|
+
roundArgs: run.args,
|
|
1226
|
+
assistantText,
|
|
1227
|
+
defaultModel,
|
|
1228
|
+
defaultEffort,
|
|
1229
|
+
sessionHandle,
|
|
1230
|
+
createRepairStream: run.createRepairStream,
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1145
1233
|
const structured = getPreferredStructuredOutput(result);
|
|
1146
1234
|
if (structured !== undefined) {
|
|
1147
1235
|
await emitProviderEvent(args.events, {
|
|
@@ -1156,9 +1244,6 @@ async function runClaudeStructuredAdvisorRound(args, defaultModel, createStream
|
|
|
1156
1244
|
structured,
|
|
1157
1245
|
};
|
|
1158
1246
|
}
|
|
1159
|
-
const subtype = lastResult?.subtype ?? null;
|
|
1160
|
-
const resultErrorMessage = getClaudeResultErrorMessage(lastResult);
|
|
1161
|
-
const structuredRetryExhausted = isStructuredOutputRetryExhaustion(subtype, resultErrorMessage);
|
|
1162
1247
|
if (structuredRetryExhausted) {
|
|
1163
1248
|
await emitProviderEvent(args.events, {
|
|
1164
1249
|
type: 'tool_progress',
|
|
@@ -1177,40 +1262,28 @@ async function runClaudeStructuredAdvisorRound(args, defaultModel, createStream
|
|
|
1177
1262
|
},
|
|
1178
1263
|
});
|
|
1179
1264
|
}
|
|
1180
|
-
if (
|
|
1181
|
-
|
|
1182
|
-
await emitProviderEvent(args.events, {
|
|
1183
|
-
type: 'tool_progress',
|
|
1184
|
-
provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
|
|
1185
|
-
role: 'structured-advisor',
|
|
1186
|
-
label: args.label,
|
|
1187
|
-
sessionHandle,
|
|
1188
|
-
toolName: 'api_retry',
|
|
1189
|
-
message: `transient Claude error result; retrying review (${apiRetryCount}/${apiRetryLimit})`,
|
|
1190
|
-
isError: true,
|
|
1191
|
-
providerData: {
|
|
1192
|
-
retryCount: apiRetryCount,
|
|
1193
|
-
retryLimit: apiRetryLimit,
|
|
1194
|
-
subtype,
|
|
1195
|
-
message: resultErrorMessage,
|
|
1196
|
-
},
|
|
1197
|
-
});
|
|
1198
|
-
await sleep(getApiRetryDelayMs(apiRetryCount));
|
|
1265
|
+
if (isTransientErrorResult && apiRetryCount < apiRetryLimit) {
|
|
1266
|
+
await retryAfterTransientErrorResult(subtype, resultErrorMessage);
|
|
1199
1267
|
continue;
|
|
1200
1268
|
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
: `Claude ${args.label} did not return a successful result${subtype ? ` (${subtype})` : ''}`,
|
|
1205
|
-
role: 'structured-advisor',
|
|
1206
|
-
sessionHandle,
|
|
1207
|
-
kind: getClaudeStructuredFailureKind(subtype, resultErrorMessage),
|
|
1269
|
+
throw await createClaudeAdvisorUnsuccessfulResultError({
|
|
1270
|
+
events: args.events,
|
|
1271
|
+
label: args.label,
|
|
1208
1272
|
subtype,
|
|
1273
|
+
resultErrorMessage,
|
|
1274
|
+
sessionHandle,
|
|
1209
1275
|
});
|
|
1210
|
-
await emitClaudeProviderError(args.events, providerError, args.label);
|
|
1211
|
-
throw providerError;
|
|
1212
1276
|
}
|
|
1213
1277
|
}
|
|
1278
|
+
async function runClaudeJsonBlockStructuredAdvisorRound(args, defaultModel, createStream = buildClaudeJsonBlockQueryStream, createRepairStream = buildClaudeJsonBlockRepairQueryStream, defaultEffort, sleep = defaultSleep) {
|
|
1279
|
+
return runClaudeStructuredAdvisorRoundLoop({ protocol: 'json-block', args, createStream, createRepairStream }, defaultModel, defaultEffort, sleep);
|
|
1280
|
+
}
|
|
1281
|
+
async function runClaudeStructuredAdvisorRound(args, defaultModel, createStream = buildClaudeQueryStream, defaultEffort, sleep = defaultSleep) {
|
|
1282
|
+
if (isClaudeJsonBlockStructuredAdvisorRound(args)) {
|
|
1283
|
+
return await runClaudeJsonBlockStructuredAdvisorRound(args, defaultModel, undefined, undefined, defaultEffort, sleep);
|
|
1284
|
+
}
|
|
1285
|
+
return runClaudeStructuredAdvisorRoundLoop({ protocol: 'native', args, createStream }, defaultModel, defaultEffort, sleep);
|
|
1286
|
+
}
|
|
1214
1287
|
class AnthropicClaudeStructuredAdvisorAdapter {
|
|
1215
1288
|
options;
|
|
1216
1289
|
constructor(options = {}) {
|
|
@@ -1264,45 +1337,27 @@ function buildClaudeWritePathGuardHooks(cwd, allowedWritePaths) {
|
|
|
1264
1337
|
return { PreToolUse: [{ hooks: [guard] }] };
|
|
1265
1338
|
}
|
|
1266
1339
|
function buildClaudeCoderQueryOptions(args, defaultModel, claudeExecutablePath = getClaudeCodeExecutablePath(), defaultEffort, abortController = deriveClaudeAbortController(args.signal)) {
|
|
1267
|
-
return {
|
|
1340
|
+
return buildClaudeCoreQueryOptions({
|
|
1268
1341
|
cwd: args.cwd,
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1342
|
+
abortController,
|
|
1343
|
+
model: defaultModel,
|
|
1344
|
+
effort: defaultEffort,
|
|
1272
1345
|
// `toolPolicy.allowRun: false` (plan-authoring turns) removes Bash so the
|
|
1273
1346
|
// "no shell" jail is enforced by tool absence, not prompt text.
|
|
1274
1347
|
tools: args.toolPolicy?.allowRun === false
|
|
1275
1348
|
? ['Read', 'Grep', 'Glob', 'Edit', 'Write']
|
|
1276
1349
|
: ['Read', 'Grep', 'Glob', 'Bash', 'Edit', 'Write'],
|
|
1277
|
-
|
|
1278
|
-
?
|
|
1279
|
-
:
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
schema: args.outputSchema,
|
|
1289
|
-
},
|
|
1290
|
-
}
|
|
1291
|
-
: {}),
|
|
1292
|
-
stderr: (data) => {
|
|
1293
|
-
void args.events?.({
|
|
1294
|
-
type: 'tool_progress',
|
|
1295
|
-
provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
|
|
1296
|
-
role: 'coder',
|
|
1297
|
-
label: 'coder',
|
|
1298
|
-
sessionHandle: args.resumeHandle ?? null,
|
|
1299
|
-
toolName: 'stderr',
|
|
1300
|
-
message: data,
|
|
1301
|
-
isError: true,
|
|
1302
|
-
providerData: { stream: 'stderr' },
|
|
1303
|
-
});
|
|
1304
|
-
},
|
|
1305
|
-
};
|
|
1350
|
+
hooks: args.toolPolicy?.allowedWritePaths
|
|
1351
|
+
? buildClaudeWritePathGuardHooks(args.cwd, args.toolPolicy.allowedWritePaths)
|
|
1352
|
+
: undefined,
|
|
1353
|
+
resumeHandle: args.resumeHandle,
|
|
1354
|
+
claudeExecutablePath,
|
|
1355
|
+
outputSchema: args.outputSchema,
|
|
1356
|
+
events: args.events,
|
|
1357
|
+
stderrRole: 'coder',
|
|
1358
|
+
stderrLabel: 'coder',
|
|
1359
|
+
stderrSessionHandle: args.resumeHandle ?? null,
|
|
1360
|
+
});
|
|
1306
1361
|
}
|
|
1307
1362
|
function buildClaudeCoderQueryStream(args, defaultModel, defaultEffort, abortController) {
|
|
1308
1363
|
return query({
|
|
@@ -1311,29 +1366,18 @@ function buildClaudeCoderQueryStream(args, defaultModel, defaultEffort, abortCon
|
|
|
1311
1366
|
});
|
|
1312
1367
|
}
|
|
1313
1368
|
function buildClaudeCoderRepairQueryOptions(args, defaultModel, claudeExecutablePath = getClaudeCodeExecutablePath(), defaultEffort, abortController) {
|
|
1314
|
-
return {
|
|
1369
|
+
return buildClaudeCoreQueryOptions({
|
|
1315
1370
|
cwd: args.cwd,
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1371
|
+
abortController,
|
|
1372
|
+
model: defaultModel,
|
|
1373
|
+
effort: defaultEffort,
|
|
1319
1374
|
tools: [],
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
provider: ANTHROPIC_CLAUDE_PROVIDER_ID,
|
|
1327
|
-
role: 'coder',
|
|
1328
|
-
label: `${args.label}:structured-json-repair`,
|
|
1329
|
-
sessionHandle: null,
|
|
1330
|
-
toolName: 'stderr',
|
|
1331
|
-
message: data,
|
|
1332
|
-
isError: true,
|
|
1333
|
-
providerData: { stream: 'stderr' },
|
|
1334
|
-
});
|
|
1335
|
-
},
|
|
1336
|
-
};
|
|
1375
|
+
claudeExecutablePath,
|
|
1376
|
+
events: args.events,
|
|
1377
|
+
stderrRole: 'coder',
|
|
1378
|
+
stderrLabel: `${args.label}:structured-json-repair`,
|
|
1379
|
+
stderrSessionHandle: null,
|
|
1380
|
+
});
|
|
1337
1381
|
}
|
|
1338
1382
|
function buildClaudeCoderRepairQueryStream(args, repairPrompt, defaultModel, defaultEffort, abortController) {
|
|
1339
1383
|
return query({
|