@mono-agent/agent-runtime 0.6.1 → 0.8.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 (44) 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 +15 -42
  6. package/src/agent/tools/shared/ripgrep.js +12 -8
  7. package/src/ai/file-change-stats.js +0 -21
  8. package/src/ai/index.js +8 -0
  9. package/src/ai/providers/claude-cli.js +109 -5
  10. package/src/ai/providers/claude-sandbox.js +71 -0
  11. package/src/ai/providers/claude-sdk-discovery-worker.js +53 -0
  12. package/src/ai/providers/claude-sdk-discovery.js +352 -0
  13. package/src/ai/providers/claude-sdk.js +315 -163
  14. package/src/ai/providers/codex-app.js +823 -78
  15. package/src/ai/providers/opencode-app.js +682 -96
  16. package/src/ai/providers/opencode-server.js +508 -0
  17. package/src/ai/providers/pi-native/stream-subscriber.js +9 -0
  18. package/src/ai/runtime/capabilities.js +12 -0
  19. package/src/ai/runtime/context-windows.js +8 -0
  20. package/src/ai/runtime/registry.js +8 -2
  21. package/src/ai/runtime/router.js +627 -29
  22. package/src/ai/streaming/codex-events.js +7 -15
  23. package/src/ai/types.js +29 -2
  24. package/src/index.js +6 -0
  25. package/src/runtime.js +17 -1
  26. package/types/agent/approval.d.ts +4 -7
  27. package/types/agent/sandbox-seam.d.ts +5 -0
  28. package/types/ai/backend.d.ts +16 -0
  29. package/types/ai/file-change-stats.d.ts +0 -24
  30. package/types/ai/index.d.ts +1 -0
  31. package/types/ai/providers/claude-cli.d.ts +116 -0
  32. package/types/ai/providers/claude-sandbox.d.ts +79 -0
  33. package/types/ai/providers/claude-sdk-discovery-worker.d.ts +1 -0
  34. package/types/ai/providers/claude-sdk-discovery.d.ts +97 -0
  35. package/types/ai/providers/claude-sdk.d.ts +81 -5
  36. package/types/ai/providers/codex-app.d.ts +11 -7
  37. package/types/ai/providers/opencode-app.d.ts +15 -16
  38. package/types/ai/providers/opencode-server.d.ts +20 -0
  39. package/types/ai/runtime/capabilities.d.ts +19 -0
  40. package/types/ai/runtime/context-windows.d.ts +1 -0
  41. package/types/ai/runtime/router.d.ts +24 -23
  42. package/types/ai/streaming/codex-events.d.ts +15 -6
  43. package/types/ai/types.d.ts +75 -2
  44. package/types/index.d.ts +1 -0
@@ -1,13 +1,5 @@
1
1
  import { query } from "@anthropic-ai/claude-agent-sdk";
2
2
  import { randomUUID } from "node:crypto";
3
- import { resolve } from "node:path";
4
- import {
5
- createFileEditToolResultEvent,
6
- createFileEditToolUseEvent,
7
- fileChangeSummary,
8
- readFileChangeSnapshot,
9
- statsForCompletedChange,
10
- } from "../file-change-stats.js";
11
3
  import { formatLiveInputGuidance } from "../live-input-prompt.js";
12
4
  import { estimateCost } from "../cost.js";
13
5
  import { modelWithContextWindow } from "../runtime/context-windows.js";
@@ -22,10 +14,55 @@ import {
22
14
  claudeNativeAgentDefinitions,
23
15
  resolveClaudeAllowedTools,
24
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
+ }
53
+
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
+ }
25
62
 
26
- function thinkingForEffort(effort) {
27
- if (effort === "low") return { thinking: { type: "disabled" } };
28
- return { thinking: { type: "adaptive" }, effort };
63
+ /** @param {string} model @param {unknown} contextWindow */
64
+ export function claudeSdkModelForQuery(model, contextWindow) {
65
+ return modelWithContextWindow(model, contextWindow);
29
66
  }
30
67
 
31
68
  function extractText(event) {
@@ -44,6 +81,12 @@ function assistantToolNames(event) {
44
81
  .map((block) => block.name);
45
82
  }
46
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
+
47
90
  function extractResultText(event) {
48
91
  if (event.type !== "result") return "";
49
92
  if (typeof event.result === "string") return event.result;
@@ -60,6 +103,117 @@ function stringifyError(value) {
60
103
  try { return JSON.stringify(value); } catch { return String(value); }
61
104
  }
62
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
+
63
217
  function humanizeSubtype(subtype) {
64
218
  return String(subtype || "").replace(/^error_/, "").replace(/_/g, " ").trim();
65
219
  }
@@ -77,7 +231,7 @@ function resultEventError(event) {
77
231
  ? "Claude stopped before final output: max turns reached"
78
232
  : `Claude result error${label ? ` (${label})` : ""}${detail ? `: ${detail}` : ""}`;
79
233
  return {
80
- message,
234
+ message: boundedText(message),
81
235
  failureKind: subtype === "error_max_turns"
82
236
  ? "usage_limit"
83
237
  : subtype === "error_max_structured_output_retries"
@@ -169,21 +323,32 @@ function buildClaudeErrorDetails({
169
323
  toolResultsSeen = 0,
170
324
  numTurns = 0,
171
325
  lastStructuredOutputRejection = null,
326
+ failureCode = null,
327
+ failureCategory = null,
328
+ retryable = null,
329
+ requestId = null,
172
330
  }) {
173
- 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);
174
333
  const turnCount = Number(event?.num_turns ?? numTurns) || 0;
175
334
  const excerpt = lastTextSnippet(assistantTexts);
176
335
  return {
177
336
  claude_error_subtype: resolvedSubtype,
178
337
  last_text_excerpt: excerpt,
179
- last_tool_name: lastToolName || null,
338
+ last_tool_name: lastToolName ? boundedText(lastToolName, 160) : null,
180
339
  had_partial_progress: !!(excerpt || lastToolName || toolResultsSeen > 0),
181
340
  tool_results_seen: toolResultsSeen,
182
341
  turn_count: turnCount,
183
342
  max_turns_hit: resolvedSubtype === "error_max_turns",
184
343
  structured_output_retry_exhausted: resolvedSubtype === "error_max_structured_output_retries",
185
- last_structured_output_rejection: lastStructuredOutputRejection || null,
186
- 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,
187
352
  };
188
353
  }
189
354
 
@@ -209,8 +374,6 @@ function structuredOutputRejectionFromEvent(event) {
209
374
  return /structured output|required schema|did not match schema|schema violation/i.test(result) ? result : null;
210
375
  }
211
376
 
212
- const CLAUDE_FILE_EDIT_MATCHER = "Edit|Write|NotebookEdit";
213
-
214
377
  function mergeHookMatchers(existing = {}, additions = {}) {
215
378
  const merged = {};
216
379
  for (const [name, groups] of Object.entries(existing || {})) {
@@ -291,122 +454,7 @@ export function toolPayloadLimit(options) {
291
454
  return { bytes: MAX_TOOL_RESULT_BYTES, usedSettings: false };
292
455
  }
293
456
 
294
- function claudeEditPath(toolName, toolInput) {
295
- const input = objectInput(toolInput);
296
- if (toolName === "NotebookEdit") return input.notebook_path || input.file_path || "";
297
- return input.file_path || "";
298
- }
299
-
300
- function claudeEditKind(toolName, before) {
301
- if (toolName === "Write" && before && !before.exists) return "add";
302
- return "update";
303
- }
304
-
305
- function fileEditStateKey(input, toolUseID, path) {
306
- return toolUseID || input?.tool_use_id || input?.toolUseID || `${input?.tool_name || "file_edit"}:${path}`;
307
- }
308
-
309
- /**
310
- * @param {any} change
311
- * @param {{status?: any, before?: any, after?: any, error?: any}} [options]
312
- */
313
- function fileEditPayload(change, { status, before, after, error } = {}) {
314
- const lineStats = statsForCompletedChange(change, before, after);
315
- const completedChange = lineStats ? { ...change, line_stats: lineStats } : change;
316
- const summary = fileChangeSummary([completedChange]);
317
- return {
318
- changes: [completedChange],
319
- status,
320
- ...(summary ? { summary } : {}),
321
- ...(error ? { error } : {}),
322
- };
323
- }
324
-
325
- function createClaudeFileEditHooks({ cwd, emitEvent }) {
326
- const edits = new Map();
327
- const runCwd = cwd || process.cwd();
328
-
329
- function createState(input, toolUseID, { readBefore = true } = {}) {
330
- const toolName = input?.tool_name;
331
- const path = claudeEditPath(toolName, input?.tool_input);
332
- if (!path) return null;
333
- const resolvedPath = resolve(runCwd, path);
334
- const key = fileEditStateKey(input, toolUseID, resolvedPath);
335
- const before = readBefore ? readFileChangeSnapshot(resolvedPath) : null;
336
- return {
337
- key,
338
- id: `file_edit:${key}`,
339
- path: resolvedPath,
340
- change: { path: resolvedPath, kind: claudeEditKind(toolName, before) },
341
- before,
342
- started: false,
343
- };
344
- }
345
-
346
- function emitStart(state) {
347
- if (!state || state.started) return;
348
- emitEvent(createFileEditToolUseEvent(state.id, {
349
- changes: [state.change],
350
- status: "in_progress",
351
- }));
352
- state.started = true;
353
- }
354
-
355
- /**
356
- * @param {any} input
357
- * @param {any} toolUseID
358
- * @param {{status?: any, error?: any}} [options]
359
- */
360
- function complete(input, toolUseID, { status, error } = {}) {
361
- const directKey = toolUseID || input?.tool_use_id || input?.toolUseID;
362
- const fallback = createState(input, toolUseID, { readBefore: false });
363
- const state = (directKey && edits.get(directKey)) || (fallback?.key && edits.get(fallback.key)) || fallback;
364
- if (!state) return;
365
- emitStart(state);
366
- const after = readFileChangeSnapshot(state.path);
367
- const payload = fileEditPayload(state.change, {
368
- status,
369
- before: state.before,
370
- after,
371
- error,
372
- });
373
- emitEvent(createFileEditToolResultEvent(state.id, payload, { isError: status === "failed" }));
374
- edits.delete(state.key);
375
- }
376
-
377
- return {
378
- PreToolUse: [{
379
- matcher: CLAUDE_FILE_EDIT_MATCHER,
380
- hooks: [async (input, toolUseID) => {
381
- const state = createState(input, toolUseID);
382
- if (!state) return {};
383
- edits.set(state.key, state);
384
- emitStart(state);
385
- return {};
386
- }],
387
- }],
388
- PostToolUse: [{
389
- matcher: CLAUDE_FILE_EDIT_MATCHER,
390
- hooks: [async (input, toolUseID) => {
391
- complete(input, toolUseID, { status: "completed" });
392
- return {};
393
- }],
394
- }],
395
- PostToolUseFailure: [{
396
- matcher: CLAUDE_FILE_EDIT_MATCHER,
397
- hooks: [async (input, toolUseID) => {
398
- complete(input, toolUseID, {
399
- status: "failed",
400
- error: stringifyError(input?.error) || "tool failed",
401
- });
402
- return {};
403
- }],
404
- }],
405
- };
406
- }
407
-
408
457
  function createClaudeRuntimeHooks({
409
- cwd,
410
458
  emitEvent,
411
459
  persistArtifact,
412
460
  qaOutputDir,
@@ -414,7 +462,7 @@ function createClaudeRuntimeHooks({
414
462
  onToolUse,
415
463
  onToolResult,
416
464
  }) {
417
- return mergeHookMatchers(createClaudeFileEditHooks({ cwd, emitEvent }), {
465
+ return {
418
466
  PreToolUse: [{
419
467
  matcher: "*",
420
468
  hooks: [async (input) => {
@@ -469,7 +517,7 @@ function createClaudeRuntimeHooks({
469
517
  return {};
470
518
  }],
471
519
  }],
472
- });
520
+ };
473
521
  }
474
522
 
475
523
  function promptStringFromMessages(messages) {
@@ -497,7 +545,7 @@ function createClaudeCanUseTool(approvalManager, modelName) {
497
545
  toolName,
498
546
  input,
499
547
  model: modelName,
500
- toolUseId: context?.toolUseId || context?.tool_use_id || null,
548
+ toolUseId: context?.toolUseID || context?.toolUseId || context?.tool_use_id || null,
501
549
  });
502
550
  if (decision.decision === "deny") {
503
551
  return {
@@ -520,7 +568,7 @@ export async function generateClaudeResponse(systemPrompt, options) {
520
568
  const {
521
569
  messages,
522
570
  model,
523
- effort = "medium",
571
+ effort,
524
572
  cwd,
525
573
  mcpServers,
526
574
  allowedTools,
@@ -532,7 +580,45 @@ export async function generateClaudeResponse(systemPrompt, options) {
532
580
  onEvent = () => {},
533
581
  } = options;
534
582
 
535
- 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
+ }
536
622
 
537
623
  const promptString = promptStringFromMessages(messages);
538
624
  const runtimeWarnings = [];
@@ -592,22 +678,37 @@ export async function generateClaudeResponse(systemPrompt, options) {
592
678
  // toolset (every tool, incl. Task — not double-added). disallowedTools still
593
679
  // flows through, so deny-wins holds under allow-all.
594
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;
595
687
  // Assembled incrementally, then handed across the SDK `query` boundary
596
688
  // (outputFormat/resume/maxTurns are attached conditionally below).
597
689
  /** @type {any} */
598
690
  const queryOptions = {
599
691
  systemPrompt,
600
- model: modelWithContextWindow(model.model, options.contextWindow),
692
+ model: claudeSdkModelForQuery(model.model, options.contextWindow),
601
693
  cwd,
602
694
  permissionMode: effectivePermissionMode,
603
695
  ...(effectivePermissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {}),
604
- 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 } : {}),
605
702
  disallowedTools,
606
- 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 } : {}),
607
709
  ...(approvalManager ? { canUseTool: createClaudeCanUseTool(approvalManager, model.model) } : {}),
608
710
  ...(nativeAgents ? { agents: nativeAgents } : {}),
609
711
  hooks: mergeHookMatchers(hooks, createClaudeRuntimeHooks({
610
- cwd,
611
712
  emitEvent,
612
713
  persistArtifact,
613
714
  qaOutputDir,
@@ -615,7 +716,7 @@ export async function generateClaudeResponse(systemPrompt, options) {
615
716
  onToolUse: noteToolUse,
616
717
  onToolResult: noteToolResult,
617
718
  })),
618
- ...thinkingOpts,
719
+ ...effortOptions,
619
720
  };
620
721
  if (options.outputSchema) {
621
722
  queryOptions.outputFormat = {
@@ -641,7 +742,8 @@ export async function generateClaudeResponse(systemPrompt, options) {
641
742
  runtime: "sdk",
642
743
  timestamp: providerRequestStartedAt,
643
744
  });
644
- const stream = query({ prompt: /** @type {any} */ (prompt), options: queryOptions });
745
+ /** @type {ReturnType<typeof query> | null} */
746
+ let stream = null;
645
747
 
646
748
  let text = "";
647
749
  let usage = {};
@@ -657,6 +759,9 @@ export async function generateClaudeResponse(systemPrompt, options) {
657
759
  let structuredResult = undefined;
658
760
  let errorDetails = null;
659
761
  let lastStructuredOutputRejection = null;
762
+ let totalCostUsd = null;
763
+ let thinkingObserved = false;
764
+ let structuredTerminalFailure = null;
660
765
  const pendingStructuredOutputById = new Map();
661
766
 
662
767
  const rawFinalText = () => resultText || text;
@@ -675,16 +780,18 @@ export async function generateClaudeResponse(systemPrompt, options) {
675
780
  runtimeWarnings.push(makeRuntimeWarning(message));
676
781
  }
677
782
 
678
- const abortHandler = async () => {
783
+ const abortHandler = () => {
679
784
  cancelled = true;
680
- if (stream.return) await stream.return();
785
+ internalAbortController.abort();
786
+ try { stream?.close?.(); } catch { /* best effort; finally closes again */ }
681
787
  };
682
788
  if (abortSignal) {
683
- if (abortSignal.aborted) await abortHandler();
789
+ if (abortSignal.aborted) abortHandler();
684
790
  else abortSignal.addEventListener("abort", abortHandler, { once: true });
685
791
  }
686
792
 
687
793
  try {
794
+ stream = query({ prompt: /** @type {any} */ (prompt), options: queryOptions });
688
795
  for await (const event of stream) {
689
796
  const nextSessionId = sessionIdFromEvent(event);
690
797
  if (nextSessionId) providerSessionId = nextSessionId;
@@ -710,6 +817,25 @@ export async function generateClaudeResponse(systemPrompt, options) {
710
817
  emitEvent(structuredOutputEvent(eventStructuredOutput));
711
818
  }
712
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
+ }
713
839
  const delta = extractText(event);
714
840
  if (delta) assistantTextFragments.push(delta);
715
841
  text += delta;
@@ -720,8 +846,12 @@ export async function generateClaudeResponse(systemPrompt, options) {
720
846
  // narrowed union.
721
847
  else if (/** @type {any} */ (event).type === "error") {
722
848
  const errorEvent = /** @type {any} */ (event);
723
- const message = errorEvent.error?.message || errorEvent.error || "sdk stream error";
724
- 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()) {
725
855
  preservePostSuccessError(`Claude SDK emitted an error after final output; preserved final result. ${message}`);
726
856
  } else {
727
857
  errorMessage = message;
@@ -739,6 +869,7 @@ export async function generateClaudeResponse(systemPrompt, options) {
739
869
  }
740
870
  break;
741
871
  } else if (event.type === "result") {
872
+ if (Number.isFinite(Number(event.total_cost_usd))) totalCostUsd = Number(event.total_cost_usd);
742
873
  const resultError = resultEventError(event);
743
874
  if (resultError) {
744
875
  if (!successfulResultSeen) {
@@ -746,15 +877,19 @@ export async function generateClaudeResponse(systemPrompt, options) {
746
877
  durationMs = event.duration_ms || durationMs;
747
878
  numTurns = event.num_turns || numTurns;
748
879
  }
749
- 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()) {
750
884
  preservePostSuccessError(`Claude SDK emitted an error after final output; preserved final result. ${resultError.message}`);
751
885
  successfulResultSeen = true;
752
886
  } else {
887
+ const categorized = resultFailureCategory(event, resultError);
753
888
  usage = event.usage || usage;
754
889
  durationMs = event.duration_ms || durationMs;
755
890
  numTurns = event.num_turns || numTurns;
756
- errorMessage = resultError.message;
757
- failureKind = resultError.failureKind;
891
+ errorMessage = categorized.message;
892
+ failureKind = categorized.failureKind;
758
893
  if (failureKind === "invalid_result") {
759
894
  runtimeWarnings.push(makeRuntimeWarning(
760
895
  resultError.message,
@@ -770,6 +905,10 @@ export async function generateClaudeResponse(systemPrompt, options) {
770
905
  toolResultsSeen,
771
906
  numTurns,
772
907
  lastStructuredOutputRejection,
908
+ failureCode: categorized.code,
909
+ failureCategory: categorized.category,
910
+ retryable: categorized.retryable,
911
+ requestId: categorized.requestId,
773
912
  });
774
913
  }
775
914
  } else {
@@ -785,8 +924,10 @@ export async function generateClaudeResponse(systemPrompt, options) {
785
924
  }
786
925
  } catch (err) {
787
926
  if (!cancelled) {
788
- const message = err?.message || String(err);
789
- 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()) {
790
931
  preservePostSuccessError(`Claude SDK stream failed after final output; preserved final result. ${message}`);
791
932
  } else {
792
933
  errorMessage = message;
@@ -799,26 +940,37 @@ export async function generateClaudeResponse(systemPrompt, options) {
799
940
  toolResultsSeen,
800
941
  numTurns,
801
942
  lastStructuredOutputRejection,
943
+ failureCode: "exception",
944
+ failureCategory: "unknown",
945
+ retryable: false,
802
946
  });
803
947
  }
804
948
  }
805
949
  } finally {
950
+ try { stream?.close?.(); } catch { /* best effort after every terminal path */ }
806
951
  if (abortSignal) abortSignal.removeEventListener?.("abort", abortHandler);
807
952
  }
808
953
 
954
+ if (structuredTerminalFailure) {
955
+ errorMessage = structuredTerminalFailure.message;
956
+ failureKind = structuredTerminalFailure.failureKind;
957
+ }
958
+
809
959
  const reference = model.reference || `claude:${model.model}`;
810
960
  const inputTokens = usage?.input_tokens ?? usage?.inputTokens ?? 0;
811
961
  const outputTokens = usage?.output_tokens ?? usage?.outputTokens ?? 0;
812
962
  const cachedTokens = usage?.cache_read_input_tokens ?? usage?.cache_read_tokens ?? 0;
813
963
  const cacheCreationTokens = usage?.cache_creation_input_tokens ?? usage?.cache_creation_tokens ?? 0;
814
- const costUsd = estimateCost({
815
- resolveCustomPricing: options.resolveCustomPricing,
816
- model: reference,
817
- inputTokens,
818
- outputTokens,
819
- cachedTokens,
820
- cacheWriteTokens: cacheCreationTokens,
821
- });
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
+ });
822
974
  const enrichedUsage = {
823
975
  ...usage,
824
976
  input_tokens: inputTokens || null,
@@ -862,7 +1014,7 @@ export async function generateClaudeResponse(systemPrompt, options) {
862
1014
  : [];
863
1015
  const capabilitiesUsed = buildCapabilitiesUsed({
864
1016
  promptCacheActive: cachedTokens > 0 || cacheCreationTokens > 0,
865
- thinkingEnabled: effort !== "low",
1017
+ thinkingEnabled: thinkingObserved ? true : null,
866
1018
  structuredOutputEnforced: !!options.outputSchema,
867
1019
  // Claude SDK doesn't surface a per-call "subagent was invoked" signal,
868
1020
  // so we report null when subagents were configured (unknown) and false