@mono-agent/agent-runtime 0.6.2 → 0.9.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.
Files changed (40) hide show
  1. package/README.md +36 -16
  2. package/package.json +14 -7
  3. package/src/agent/approval.js +52 -17
  4. package/src/agent/sandbox-seam.js +1 -0
  5. package/src/agent/tools/pi-bridge.js +7 -0
  6. package/src/agent/tools/shared/ripgrep.js +12 -8
  7. package/src/ai/index.js +8 -0
  8. package/src/ai/providers/claude-cli.js +109 -5
  9. package/src/ai/providers/claude-sandbox.js +71 -0
  10. package/src/ai/providers/claude-sdk-discovery-worker.js +53 -0
  11. package/src/ai/providers/claude-sdk-discovery.js +352 -0
  12. package/src/ai/providers/claude-sdk.js +313 -35
  13. package/src/ai/providers/codex-app.js +823 -78
  14. package/src/ai/providers/opencode-app.js +682 -96
  15. package/src/ai/providers/opencode-server.js +508 -0
  16. package/src/ai/providers/pi-native/turn-runner.js +8 -0
  17. package/src/ai/runtime/capabilities.js +12 -0
  18. package/src/ai/runtime/context-windows.js +8 -0
  19. package/src/ai/runtime/registry.js +8 -2
  20. package/src/ai/runtime/router.js +627 -29
  21. package/src/ai/types.js +29 -2
  22. package/src/index.js +6 -0
  23. package/src/runtime.js +17 -1
  24. package/types/agent/approval.d.ts +4 -7
  25. package/types/agent/sandbox-seam.d.ts +5 -0
  26. package/types/ai/backend.d.ts +16 -0
  27. package/types/ai/index.d.ts +1 -0
  28. package/types/ai/providers/claude-cli.d.ts +116 -0
  29. package/types/ai/providers/claude-sandbox.d.ts +79 -0
  30. package/types/ai/providers/claude-sdk-discovery-worker.d.ts +1 -0
  31. package/types/ai/providers/claude-sdk-discovery.d.ts +97 -0
  32. package/types/ai/providers/claude-sdk.d.ts +81 -5
  33. package/types/ai/providers/codex-app.d.ts +11 -7
  34. package/types/ai/providers/opencode-app.d.ts +15 -16
  35. package/types/ai/providers/opencode-server.d.ts +20 -0
  36. package/types/ai/runtime/capabilities.d.ts +19 -0
  37. package/types/ai/runtime/context-windows.d.ts +1 -0
  38. package/types/ai/runtime/router.d.ts +24 -23
  39. package/types/ai/types.d.ts +75 -2
  40. package/types/index.d.ts +1 -0
@@ -14,10 +14,55 @@ import {
14
14
  claudeNativeAgentDefinitions,
15
15
  resolveClaudeAllowedTools,
16
16
  } from "./claude-subagents.js";
17
+ import {
18
+ claudeSandboxCapabilityMismatchResult,
19
+ claudeSandboxPolicyProblem,
20
+ } from "./claude-sandbox.js";
21
+
22
+ const CLAUDE_EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh", "max"]);
23
+ const MAX_CLAUDE_ERROR_CHARS = 2_000;
24
+
25
+ /**
26
+ * Preserve the provider default when effort is omitted. The current Agent SDK
27
+ * accepts the five values below verbatim; mono-agent must not infer thinking
28
+ * enablement/disablement from a requested effort level.
29
+ * @param {unknown} effort
30
+ * @returns {{effort?: "low" | "medium" | "high" | "xhigh" | "max"}}
31
+ */
32
+ export function claudeEffortOptions(effort) {
33
+ if (effort == null || String(effort).trim() === "") return {};
34
+ const normalized = String(effort).trim();
35
+ if (normalized === "none") {
36
+ throw new Error(
37
+ 'Claude Agent SDK does not support effort "none". Omit effort to use the provider default, or choose low, medium, high, xhigh, or max.',
38
+ );
39
+ }
40
+ if (!CLAUDE_EFFORT_LEVELS.has(normalized)) {
41
+ throw new Error(
42
+ `Claude Agent SDK does not support effort "${boundedText(normalized, 64)}". Choose low, medium, high, xhigh, or max, or omit effort.`,
43
+ );
44
+ }
45
+ return { effort: /** @type {"low" | "medium" | "high" | "xhigh" | "max"} */ (normalized) };
46
+ }
47
+
48
+ function boundedText(value, limit = MAX_CLAUDE_ERROR_CHARS) {
49
+ const text = String(value ?? "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ").trim();
50
+ if (text.length <= limit) return text;
51
+ return `${text.slice(0, Math.max(0, limit - 16))}… [truncated]`;
52
+ }
17
53
 
18
- function thinkingForEffort(effort) {
19
- if (effort === "low") return { thinking: { type: "disabled" } };
20
- return { thinking: { type: "adaptive" }, effort };
54
+ function createClaudeSdkEnvironment(overrides, providerEnvironment) {
55
+ return {
56
+ ...process.env,
57
+ ...(overrides && typeof overrides === "object" ? overrides : {}),
58
+ ...(providerEnvironment && typeof providerEnvironment === "object" ? providerEnvironment : {}),
59
+ MCP_CONNECTION_NONBLOCKING: "0",
60
+ };
61
+ }
62
+
63
+ /** @param {string} model @param {unknown} contextWindow */
64
+ export function claudeSdkModelForQuery(model, contextWindow) {
65
+ return modelWithContextWindow(model, contextWindow);
21
66
  }
22
67
 
23
68
  function extractText(event) {
@@ -36,6 +81,12 @@ function assistantToolNames(event) {
36
81
  .map((block) => block.name);
37
82
  }
38
83
 
84
+ function assistantThinkingObserved(event) {
85
+ return event?.type === "assistant"
86
+ && Array.isArray(event.message?.content)
87
+ && event.message.content.some((block) => block?.type === "thinking" || block?.type === "redacted_thinking");
88
+ }
89
+
39
90
  function extractResultText(event) {
40
91
  if (event.type !== "result") return "";
41
92
  if (typeof event.result === "string") return event.result;
@@ -52,6 +103,117 @@ function stringifyError(value) {
52
103
  try { return JSON.stringify(value); } catch { return String(value); }
53
104
  }
54
105
 
106
+ function claudeAssistantFailure(code, requestId = null) {
107
+ const normalizedCode = boundedText(code || "unknown", 80);
108
+ const mapping = {
109
+ authentication_failed: {
110
+ message: "Claude authentication failed. Sign in again or provide a valid Claude credential.",
111
+ failureKind: "provider_auth",
112
+ category: "authentication",
113
+ retryable: false,
114
+ },
115
+ oauth_org_not_allowed: {
116
+ message: "Claude authentication succeeded, but this organization does not allow the OAuth session.",
117
+ failureKind: "provider_auth",
118
+ category: "authentication",
119
+ retryable: false,
120
+ },
121
+ rate_limit: {
122
+ message: "Claude usage or rate limit reached.",
123
+ failureKind: "usage_limit",
124
+ category: "usage_limit",
125
+ retryable: false,
126
+ },
127
+ max_output_tokens: {
128
+ message: "Claude reached the maximum output-token limit.",
129
+ failureKind: "usage_limit",
130
+ category: "usage_limit",
131
+ retryable: false,
132
+ },
133
+ overloaded: {
134
+ message: "Claude is temporarily overloaded.",
135
+ failureKind: "provider_unavailable",
136
+ category: "provider_unavailable",
137
+ retryable: true,
138
+ },
139
+ server_error: {
140
+ message: "Claude returned a temporary server error.",
141
+ failureKind: "provider_unavailable",
142
+ category: "provider_unavailable",
143
+ retryable: true,
144
+ },
145
+ billing_error: {
146
+ message: "Claude rejected the request because the account needs billing attention.",
147
+ failureKind: "provider_unavailable",
148
+ category: "nonretryable",
149
+ retryable: false,
150
+ },
151
+ invalid_request: {
152
+ message: "Claude rejected the request as invalid.",
153
+ failureKind: "provider_unavailable",
154
+ category: "nonretryable",
155
+ retryable: false,
156
+ },
157
+ model_not_found: {
158
+ message: "Claude could not find or access the requested model.",
159
+ failureKind: "provider_unavailable",
160
+ category: "nonretryable",
161
+ retryable: false,
162
+ },
163
+ unknown: {
164
+ message: "Claude reported an unknown provider error.",
165
+ failureKind: "provider_unavailable",
166
+ category: "unknown",
167
+ retryable: false,
168
+ },
169
+ };
170
+ const selected = mapping[normalizedCode] || mapping.unknown;
171
+ const safeRequestId = typeof requestId === "string" && requestId.trim()
172
+ ? boundedText(requestId, 160)
173
+ : null;
174
+ return {
175
+ ...selected,
176
+ code: normalizedCode,
177
+ requestId: safeRequestId,
178
+ message: `${selected.message}${safeRequestId ? ` Request ID: ${safeRequestId}.` : ""}`,
179
+ };
180
+ }
181
+
182
+ function resultFailureCategory(event, resultError) {
183
+ const text = `${resultError?.message || ""} ${Array.isArray(event?.errors) ? event.errors.join(" ") : ""}`;
184
+ if (/auth|oauth|api key|401|403|sign[ -]?in|log[ -]?in/i.test(text)) {
185
+ return claudeAssistantFailure("authentication_failed");
186
+ }
187
+ if (event?.subtype === "error_max_turns" || event?.subtype === "error_max_budget_usd") {
188
+ return {
189
+ message: boundedText(resultError?.message || "Claude usage limit reached."),
190
+ failureKind: "usage_limit",
191
+ category: "usage_limit",
192
+ retryable: false,
193
+ code: event.subtype,
194
+ requestId: null,
195
+ };
196
+ }
197
+ if (/overload|temporar|server error|\b50[0234]\b/i.test(text)) {
198
+ return {
199
+ message: boundedText(resultError?.message || "Claude is temporarily unavailable."),
200
+ failureKind: "provider_unavailable",
201
+ category: "provider_unavailable",
202
+ retryable: true,
203
+ code: event?.subtype || "result_error",
204
+ requestId: null,
205
+ };
206
+ }
207
+ return {
208
+ message: boundedText(resultError?.message || "Claude request failed."),
209
+ failureKind: resultError?.failureKind || "provider_unavailable",
210
+ category: resultError?.failureKind === "invalid_result" ? "nonretryable" : "unknown",
211
+ retryable: false,
212
+ code: event?.subtype || "result_error",
213
+ requestId: null,
214
+ };
215
+ }
216
+
55
217
  function humanizeSubtype(subtype) {
56
218
  return String(subtype || "").replace(/^error_/, "").replace(/_/g, " ").trim();
57
219
  }
@@ -69,7 +231,7 @@ function resultEventError(event) {
69
231
  ? "Claude stopped before final output: max turns reached"
70
232
  : `Claude result error${label ? ` (${label})` : ""}${detail ? `: ${detail}` : ""}`;
71
233
  return {
72
- message,
234
+ message: boundedText(message),
73
235
  failureKind: subtype === "error_max_turns"
74
236
  ? "usage_limit"
75
237
  : subtype === "error_max_structured_output_retries"
@@ -161,21 +323,32 @@ function buildClaudeErrorDetails({
161
323
  toolResultsSeen = 0,
162
324
  numTurns = 0,
163
325
  lastStructuredOutputRejection = null,
326
+ failureCode = null,
327
+ failureCategory = null,
328
+ retryable = null,
329
+ requestId = null,
164
330
  }) {
165
- const resolvedSubtype = subtype || event?.subtype || event?.type || null;
331
+ const rawSubtype = subtype || event?.subtype || event?.type || null;
332
+ const resolvedSubtype = rawSubtype == null ? null : boundedText(rawSubtype, 160);
166
333
  const turnCount = Number(event?.num_turns ?? numTurns) || 0;
167
334
  const excerpt = lastTextSnippet(assistantTexts);
168
335
  return {
169
336
  claude_error_subtype: resolvedSubtype,
170
337
  last_text_excerpt: excerpt,
171
- last_tool_name: lastToolName || null,
338
+ last_tool_name: lastToolName ? boundedText(lastToolName, 160) : null,
172
339
  had_partial_progress: !!(excerpt || lastToolName || toolResultsSeen > 0),
173
340
  tool_results_seen: toolResultsSeen,
174
341
  turn_count: turnCount,
175
342
  max_turns_hit: resolvedSubtype === "error_max_turns",
176
343
  structured_output_retry_exhausted: resolvedSubtype === "error_max_structured_output_retries",
177
- last_structured_output_rejection: lastStructuredOutputRejection || null,
178
- provider_session_id: providerSessionId || null,
344
+ last_structured_output_rejection: lastStructuredOutputRejection
345
+ ? boundedText(lastStructuredOutputRejection, 500)
346
+ : null,
347
+ provider_session_id: providerSessionId ? boundedText(providerSessionId, 160) : null,
348
+ claude_error_code: failureCode ? boundedText(failureCode, 80) : null,
349
+ claude_error_category: failureCategory || null,
350
+ retryable: typeof retryable === "boolean" ? retryable : null,
351
+ request_id: requestId || null,
179
352
  };
180
353
  }
181
354
 
@@ -372,7 +545,7 @@ function createClaudeCanUseTool(approvalManager, modelName) {
372
545
  toolName,
373
546
  input,
374
547
  model: modelName,
375
- toolUseId: context?.toolUseId || context?.tool_use_id || null,
548
+ toolUseId: context?.toolUseID || context?.toolUseId || context?.tool_use_id || null,
376
549
  });
377
550
  if (decision.decision === "deny") {
378
551
  return {
@@ -395,7 +568,7 @@ export async function generateClaudeResponse(systemPrompt, options) {
395
568
  const {
396
569
  messages,
397
570
  model,
398
- effort = "medium",
571
+ effort,
399
572
  cwd,
400
573
  mcpServers,
401
574
  allowedTools,
@@ -407,7 +580,45 @@ export async function generateClaudeResponse(systemPrompt, options) {
407
580
  onEvent = () => {},
408
581
  } = options;
409
582
 
410
- const thinkingOpts = thinkingForEffort(effort);
583
+ let effortOptions;
584
+ try {
585
+ effortOptions = claudeEffortOptions(effort);
586
+ } catch (error) {
587
+ const message = boundedText(error?.message || error);
588
+ return {
589
+ text: "",
590
+ structuredResult: undefined,
591
+ structuredResultSource: null,
592
+ events: [],
593
+ usage: {},
594
+ durationMs: 0,
595
+ numTurns: 0,
596
+ model: model.model,
597
+ effort: effort ?? null,
598
+ sdk: "claude",
599
+ cancelled: false,
600
+ error: message,
601
+ errorDetails: {
602
+ claude_error_code: "claude_effort_unsupported",
603
+ claude_error_category: "nonretryable",
604
+ retryable: false,
605
+ },
606
+ failureKind: "skipped_capability_mismatch",
607
+ providerSessionId: pickSessionId(options.sessionId, options.providerSessionId),
608
+ runtimeWarnings: [],
609
+ capabilitiesUsed: buildCapabilitiesUsed({ thinkingEnabled: null }),
610
+ };
611
+ }
612
+
613
+ if (claudeSandboxPolicyProblem(options)) {
614
+ return claudeSandboxCapabilityMismatchResult({
615
+ model: model.reference || `claude:${model.model}`,
616
+ effort,
617
+ sdk: "claude",
618
+ providerSessionId: pickSessionId(options.sessionId, options.providerSessionId),
619
+ outputSchema: options.outputSchema,
620
+ });
621
+ }
411
622
 
412
623
  const promptString = promptStringFromMessages(messages);
413
624
  const runtimeWarnings = [];
@@ -467,18 +678,34 @@ export async function generateClaudeResponse(systemPrompt, options) {
467
678
  // toolset (every tool, incl. Task — not double-added). disallowedTools still
468
679
  // flows through, so deny-wins holds under allow-all.
469
680
  const { allowAll: allowAllTools, tools: resolvedAllowedTools } = resolveClaudeAllowedTools(allowedTools, options.nativeSubagents);
681
+ const hasExplicitToolProjection = Array.isArray(allowedTools) && !allowAllTools;
682
+ const internalAbortController = new AbortController();
683
+ const disposableSession = options.persistSession === false
684
+ || options.disposable === true
685
+ || options.readinessProbe === true
686
+ || options.sessionKeepAlive === false;
470
687
  // Assembled incrementally, then handed across the SDK `query` boundary
471
688
  // (outputFormat/resume/maxTurns are attached conditionally below).
472
689
  /** @type {any} */
473
690
  const queryOptions = {
474
691
  systemPrompt,
475
- model: modelWithContextWindow(model.model, options.contextWindow),
692
+ model: claudeSdkModelForQuery(model.model, options.contextWindow),
476
693
  cwd,
477
694
  permissionMode: effectivePermissionMode,
478
695
  ...(effectivePermissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {}),
479
- allowedTools: allowAllTools ? undefined : resolvedAllowedTools,
696
+ // `tools` is the SDK's availability projection. In particular, [] must
697
+ // remain [] so a readiness/discovery call cannot silently regain defaults.
698
+ ...(hasExplicitToolProjection ? { tools: resolvedAllowedTools } : {}),
699
+ // `allowedTools` only controls auto-approval. Never provide it alongside
700
+ // canUseTool, where it would bypass the host approval callback.
701
+ ...(!approvalManager && hasExplicitToolProjection ? { allowedTools: resolvedAllowedTools } : {}),
480
702
  disallowedTools,
481
- mcpServers,
703
+ mcpServers: mcpServers || {},
704
+ strictMcpConfig: true,
705
+ settingSources: [],
706
+ env: createClaudeSdkEnvironment(options.env, options.providerEnv),
707
+ abortController: internalAbortController,
708
+ ...(disposableSession ? { persistSession: false } : options.persistSession === true ? { persistSession: true } : {}),
482
709
  ...(approvalManager ? { canUseTool: createClaudeCanUseTool(approvalManager, model.model) } : {}),
483
710
  ...(nativeAgents ? { agents: nativeAgents } : {}),
484
711
  hooks: mergeHookMatchers(hooks, createClaudeRuntimeHooks({
@@ -489,7 +716,7 @@ export async function generateClaudeResponse(systemPrompt, options) {
489
716
  onToolUse: noteToolUse,
490
717
  onToolResult: noteToolResult,
491
718
  })),
492
- ...thinkingOpts,
719
+ ...effortOptions,
493
720
  };
494
721
  if (options.outputSchema) {
495
722
  queryOptions.outputFormat = {
@@ -515,7 +742,8 @@ export async function generateClaudeResponse(systemPrompt, options) {
515
742
  runtime: "sdk",
516
743
  timestamp: providerRequestStartedAt,
517
744
  });
518
- const stream = query({ prompt: /** @type {any} */ (prompt), options: queryOptions });
745
+ /** @type {ReturnType<typeof query> | null} */
746
+ let stream = null;
519
747
 
520
748
  let text = "";
521
749
  let usage = {};
@@ -531,6 +759,9 @@ export async function generateClaudeResponse(systemPrompt, options) {
531
759
  let structuredResult = undefined;
532
760
  let errorDetails = null;
533
761
  let lastStructuredOutputRejection = null;
762
+ let totalCostUsd = null;
763
+ let thinkingObserved = false;
764
+ let structuredTerminalFailure = null;
534
765
  const pendingStructuredOutputById = new Map();
535
766
 
536
767
  const rawFinalText = () => resultText || text;
@@ -549,16 +780,18 @@ export async function generateClaudeResponse(systemPrompt, options) {
549
780
  runtimeWarnings.push(makeRuntimeWarning(message));
550
781
  }
551
782
 
552
- const abortHandler = async () => {
783
+ const abortHandler = () => {
553
784
  cancelled = true;
554
- if (stream.return) await stream.return();
785
+ internalAbortController.abort();
786
+ try { stream?.close?.(); } catch { /* best effort; finally closes again */ }
555
787
  };
556
788
  if (abortSignal) {
557
- if (abortSignal.aborted) await abortHandler();
789
+ if (abortSignal.aborted) abortHandler();
558
790
  else abortSignal.addEventListener("abort", abortHandler, { once: true });
559
791
  }
560
792
 
561
793
  try {
794
+ stream = query({ prompt: /** @type {any} */ (prompt), options: queryOptions });
562
795
  for await (const event of stream) {
563
796
  const nextSessionId = sessionIdFromEvent(event);
564
797
  if (nextSessionId) providerSessionId = nextSessionId;
@@ -584,6 +817,25 @@ export async function generateClaudeResponse(systemPrompt, options) {
584
817
  emitEvent(structuredOutputEvent(eventStructuredOutput));
585
818
  }
586
819
  if (event.type === "assistant") {
820
+ thinkingObserved = thinkingObserved || assistantThinkingObserved(event);
821
+ if (event.error && !structuredTerminalFailure) {
822
+ const assistantFailure = claudeAssistantFailure(event.error, event.request_id);
823
+ structuredTerminalFailure = assistantFailure;
824
+ errorDetails = buildClaudeErrorDetails({
825
+ event,
826
+ subtype: event.error,
827
+ providerSessionId,
828
+ assistantTexts: assistantTextFragments,
829
+ lastToolName,
830
+ toolResultsSeen,
831
+ numTurns,
832
+ lastStructuredOutputRejection,
833
+ failureCode: assistantFailure.code,
834
+ failureCategory: assistantFailure.category,
835
+ retryable: assistantFailure.retryable,
836
+ requestId: assistantFailure.requestId,
837
+ });
838
+ }
587
839
  const delta = extractText(event);
588
840
  if (delta) assistantTextFragments.push(delta);
589
841
  text += delta;
@@ -594,8 +846,12 @@ export async function generateClaudeResponse(systemPrompt, options) {
594
846
  // narrowed union.
595
847
  else if (/** @type {any} */ (event).type === "error") {
596
848
  const errorEvent = /** @type {any} */ (event);
597
- const message = errorEvent.error?.message || errorEvent.error || "sdk stream error";
598
- if (hasPreservableFinalOutput()) {
849
+ const message = boundedText(errorEvent.error?.message || errorEvent.error || "sdk stream error");
850
+ if (structuredTerminalFailure) {
851
+ // A typed assistant error is authoritative. A later transport error
852
+ // cannot turn authentication/billing diagnostics into a generic
853
+ // provider failure.
854
+ } else if (hasPreservableFinalOutput()) {
599
855
  preservePostSuccessError(`Claude SDK emitted an error after final output; preserved final result. ${message}`);
600
856
  } else {
601
857
  errorMessage = message;
@@ -613,6 +869,7 @@ export async function generateClaudeResponse(systemPrompt, options) {
613
869
  }
614
870
  break;
615
871
  } else if (event.type === "result") {
872
+ if (Number.isFinite(Number(event.total_cost_usd))) totalCostUsd = Number(event.total_cost_usd);
616
873
  const resultError = resultEventError(event);
617
874
  if (resultError) {
618
875
  if (!successfulResultSeen) {
@@ -620,15 +877,19 @@ export async function generateClaudeResponse(systemPrompt, options) {
620
877
  durationMs = event.duration_ms || durationMs;
621
878
  numTurns = event.num_turns || numTurns;
622
879
  }
623
- if (hasPreservableFinalOutput()) {
880
+ if (structuredTerminalFailure) {
881
+ // Retain the typed assistant error and request id. The result still
882
+ // contributes usage/duration/cost above.
883
+ } else if (hasPreservableFinalOutput()) {
624
884
  preservePostSuccessError(`Claude SDK emitted an error after final output; preserved final result. ${resultError.message}`);
625
885
  successfulResultSeen = true;
626
886
  } else {
887
+ const categorized = resultFailureCategory(event, resultError);
627
888
  usage = event.usage || usage;
628
889
  durationMs = event.duration_ms || durationMs;
629
890
  numTurns = event.num_turns || numTurns;
630
- errorMessage = resultError.message;
631
- failureKind = resultError.failureKind;
891
+ errorMessage = categorized.message;
892
+ failureKind = categorized.failureKind;
632
893
  if (failureKind === "invalid_result") {
633
894
  runtimeWarnings.push(makeRuntimeWarning(
634
895
  resultError.message,
@@ -644,6 +905,10 @@ export async function generateClaudeResponse(systemPrompt, options) {
644
905
  toolResultsSeen,
645
906
  numTurns,
646
907
  lastStructuredOutputRejection,
908
+ failureCode: categorized.code,
909
+ failureCategory: categorized.category,
910
+ retryable: categorized.retryable,
911
+ requestId: categorized.requestId,
647
912
  });
648
913
  }
649
914
  } else {
@@ -659,8 +924,10 @@ export async function generateClaudeResponse(systemPrompt, options) {
659
924
  }
660
925
  } catch (err) {
661
926
  if (!cancelled) {
662
- const message = err?.message || String(err);
663
- if (successfulResultSeen && hasUsableFinalOutput()) {
927
+ const message = boundedText(err?.message || String(err));
928
+ if (structuredTerminalFailure) {
929
+ // Keep the earlier typed provider failure and its request id.
930
+ } else if (successfulResultSeen && hasUsableFinalOutput()) {
664
931
  preservePostSuccessError(`Claude SDK stream failed after final output; preserved final result. ${message}`);
665
932
  } else {
666
933
  errorMessage = message;
@@ -673,26 +940,37 @@ export async function generateClaudeResponse(systemPrompt, options) {
673
940
  toolResultsSeen,
674
941
  numTurns,
675
942
  lastStructuredOutputRejection,
943
+ failureCode: "exception",
944
+ failureCategory: "unknown",
945
+ retryable: false,
676
946
  });
677
947
  }
678
948
  }
679
949
  } finally {
950
+ try { stream?.close?.(); } catch { /* best effort after every terminal path */ }
680
951
  if (abortSignal) abortSignal.removeEventListener?.("abort", abortHandler);
681
952
  }
682
953
 
954
+ if (structuredTerminalFailure) {
955
+ errorMessage = structuredTerminalFailure.message;
956
+ failureKind = structuredTerminalFailure.failureKind;
957
+ }
958
+
683
959
  const reference = model.reference || `claude:${model.model}`;
684
960
  const inputTokens = usage?.input_tokens ?? usage?.inputTokens ?? 0;
685
961
  const outputTokens = usage?.output_tokens ?? usage?.outputTokens ?? 0;
686
962
  const cachedTokens = usage?.cache_read_input_tokens ?? usage?.cache_read_tokens ?? 0;
687
963
  const cacheCreationTokens = usage?.cache_creation_input_tokens ?? usage?.cache_creation_tokens ?? 0;
688
- const costUsd = estimateCost({
689
- resolveCustomPricing: options.resolveCustomPricing,
690
- model: reference,
691
- inputTokens,
692
- outputTokens,
693
- cachedTokens,
694
- cacheWriteTokens: cacheCreationTokens,
695
- });
964
+ const costUsd = Number.isFinite(totalCostUsd)
965
+ ? totalCostUsd
966
+ : estimateCost({
967
+ resolveCustomPricing: options.resolveCustomPricing,
968
+ model: reference,
969
+ inputTokens,
970
+ outputTokens,
971
+ cachedTokens,
972
+ cacheWriteTokens: cacheCreationTokens,
973
+ });
696
974
  const enrichedUsage = {
697
975
  ...usage,
698
976
  input_tokens: inputTokens || null,
@@ -736,7 +1014,7 @@ export async function generateClaudeResponse(systemPrompt, options) {
736
1014
  : [];
737
1015
  const capabilitiesUsed = buildCapabilitiesUsed({
738
1016
  promptCacheActive: cachedTokens > 0 || cacheCreationTokens > 0,
739
- thinkingEnabled: effort !== "low",
1017
+ thinkingEnabled: thinkingObserved ? true : null,
740
1018
  structuredOutputEnforced: !!options.outputSchema,
741
1019
  // Claude SDK doesn't surface a per-call "subagent was invoked" signal,
742
1020
  // so we report null when subagents were configured (unknown) and false