@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.
- package/README.md +36 -16
- package/package.json +14 -7
- package/src/agent/approval.js +52 -17
- package/src/agent/sandbox-seam.js +1 -0
- package/src/agent/tools/pi-bridge.js +15 -42
- package/src/agent/tools/shared/ripgrep.js +12 -8
- package/src/ai/file-change-stats.js +0 -21
- package/src/ai/index.js +8 -0
- package/src/ai/providers/claude-cli.js +109 -5
- package/src/ai/providers/claude-sandbox.js +71 -0
- package/src/ai/providers/claude-sdk-discovery-worker.js +53 -0
- package/src/ai/providers/claude-sdk-discovery.js +352 -0
- package/src/ai/providers/claude-sdk.js +315 -163
- package/src/ai/providers/codex-app.js +823 -78
- package/src/ai/providers/opencode-app.js +682 -96
- package/src/ai/providers/opencode-server.js +508 -0
- package/src/ai/providers/pi-native/stream-subscriber.js +9 -0
- package/src/ai/runtime/capabilities.js +12 -0
- package/src/ai/runtime/context-windows.js +8 -0
- package/src/ai/runtime/registry.js +8 -2
- package/src/ai/runtime/router.js +627 -29
- package/src/ai/streaming/codex-events.js +7 -15
- package/src/ai/types.js +29 -2
- package/src/index.js +6 -0
- package/src/runtime.js +17 -1
- package/types/agent/approval.d.ts +4 -7
- package/types/agent/sandbox-seam.d.ts +5 -0
- package/types/ai/backend.d.ts +16 -0
- package/types/ai/file-change-stats.d.ts +0 -24
- package/types/ai/index.d.ts +1 -0
- package/types/ai/providers/claude-cli.d.ts +116 -0
- package/types/ai/providers/claude-sandbox.d.ts +79 -0
- package/types/ai/providers/claude-sdk-discovery-worker.d.ts +1 -0
- package/types/ai/providers/claude-sdk-discovery.d.ts +97 -0
- package/types/ai/providers/claude-sdk.d.ts +81 -5
- package/types/ai/providers/codex-app.d.ts +11 -7
- package/types/ai/providers/opencode-app.d.ts +15 -16
- package/types/ai/providers/opencode-server.d.ts +20 -0
- package/types/ai/runtime/capabilities.d.ts +19 -0
- package/types/ai/runtime/context-windows.d.ts +1 -0
- package/types/ai/runtime/router.d.ts +24 -23
- package/types/ai/streaming/codex-events.d.ts +15 -6
- package/types/ai/types.d.ts +75 -2
- 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
|
-
|
|
27
|
-
|
|
28
|
-
return
|
|
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
|
|
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
|
|
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
|
|
186
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
692
|
+
model: claudeSdkModelForQuery(model.model, options.contextWindow),
|
|
601
693
|
cwd,
|
|
602
694
|
permissionMode: effectivePermissionMode,
|
|
603
695
|
...(effectivePermissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {}),
|
|
604
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
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 =
|
|
783
|
+
const abortHandler = () => {
|
|
679
784
|
cancelled = true;
|
|
680
|
-
|
|
785
|
+
internalAbortController.abort();
|
|
786
|
+
try { stream?.close?.(); } catch { /* best effort; finally closes again */ }
|
|
681
787
|
};
|
|
682
788
|
if (abortSignal) {
|
|
683
|
-
if (abortSignal.aborted)
|
|
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 (
|
|
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 (
|
|
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 =
|
|
757
|
-
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 (
|
|
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 =
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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:
|
|
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
|