@aitne/daemon 0.1.2 → 0.1.4

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 (253) hide show
  1. package/LICENSE +21 -0
  2. package/dist/adapters/whatsapp-adapter.d.ts.map +1 -1
  3. package/dist/adapters/whatsapp-adapter.js +0 -1
  4. package/dist/adapters/whatsapp-adapter.js.map +1 -1
  5. package/dist/api/integration-route-gate.d.ts +15 -11
  6. package/dist/api/integration-route-gate.d.ts.map +1 -1
  7. package/dist/api/integration-route-gate.js +60 -23
  8. package/dist/api/integration-route-gate.js.map +1 -1
  9. package/dist/api/json-body.d.ts +22 -7
  10. package/dist/api/json-body.d.ts.map +1 -1
  11. package/dist/api/json-body.js +27 -8
  12. package/dist/api/json-body.js.map +1 -1
  13. package/dist/api/routes/agent.d.ts.map +1 -1
  14. package/dist/api/routes/agent.js +18 -0
  15. package/dist/api/routes/agent.js.map +1 -1
  16. package/dist/api/routes/backends.d.ts.map +1 -1
  17. package/dist/api/routes/backends.js +96 -1
  18. package/dist/api/routes/backends.js.map +1 -1
  19. package/dist/api/routes/books.js +1 -1
  20. package/dist/api/routes/books.js.map +1 -1
  21. package/dist/api/routes/context.d.ts.map +1 -1
  22. package/dist/api/routes/context.js +13 -1
  23. package/dist/api/routes/context.js.map +1 -1
  24. package/dist/api/routes/dashboard.d.ts.map +1 -1
  25. package/dist/api/routes/dashboard.js +75 -5
  26. package/dist/api/routes/dashboard.js.map +1 -1
  27. package/dist/api/routes/github.d.ts.map +1 -1
  28. package/dist/api/routes/github.js +38 -5
  29. package/dist/api/routes/github.js.map +1 -1
  30. package/dist/api/routes/integrations.d.ts +35 -6
  31. package/dist/api/routes/integrations.d.ts.map +1 -1
  32. package/dist/api/routes/integrations.js +191 -16
  33. package/dist/api/routes/integrations.js.map +1 -1
  34. package/dist/api/routes/mail.d.ts.map +1 -1
  35. package/dist/api/routes/mail.js +112 -46
  36. package/dist/api/routes/mail.js.map +1 -1
  37. package/dist/api/routes/observations.d.ts.map +1 -1
  38. package/dist/api/routes/observations.js +161 -8
  39. package/dist/api/routes/observations.js.map +1 -1
  40. package/dist/api/routes/setup-migrate.d.ts +9 -1
  41. package/dist/api/routes/setup-migrate.d.ts.map +1 -1
  42. package/dist/api/routes/setup-migrate.js +4 -2
  43. package/dist/api/routes/setup-migrate.js.map +1 -1
  44. package/dist/api/routes/skills.d.ts.map +1 -1
  45. package/dist/api/routes/skills.js +39 -1
  46. package/dist/api/routes/skills.js.map +1 -1
  47. package/dist/api/routes/voice.d.ts.map +1 -1
  48. package/dist/api/routes/voice.js +154 -14
  49. package/dist/api/routes/voice.js.map +1 -1
  50. package/dist/bootstrap/adapters.d.ts +109 -0
  51. package/dist/bootstrap/adapters.d.ts.map +1 -0
  52. package/dist/bootstrap/adapters.js +237 -0
  53. package/dist/bootstrap/adapters.js.map +1 -0
  54. package/dist/bootstrap/catchup.d.ts +23 -0
  55. package/dist/bootstrap/catchup.d.ts.map +1 -0
  56. package/dist/bootstrap/catchup.js +124 -0
  57. package/dist/bootstrap/catchup.js.map +1 -0
  58. package/dist/bootstrap/schedule-helpers.d.ts +18 -0
  59. package/dist/bootstrap/schedule-helpers.d.ts.map +1 -0
  60. package/dist/bootstrap/schedule-helpers.js +96 -0
  61. package/dist/bootstrap/schedule-helpers.js.map +1 -0
  62. package/dist/bootstrap/services.d.ts +60 -0
  63. package/dist/bootstrap/services.d.ts.map +1 -0
  64. package/dist/bootstrap/services.js +209 -0
  65. package/dist/bootstrap/services.js.map +1 -0
  66. package/dist/core/backends/backend-router.d.ts +23 -0
  67. package/dist/core/backends/backend-router.d.ts.map +1 -1
  68. package/dist/core/backends/backend-router.js +48 -3
  69. package/dist/core/backends/backend-router.js.map +1 -1
  70. package/dist/core/backends/claude-auth.d.ts +70 -0
  71. package/dist/core/backends/claude-auth.d.ts.map +1 -0
  72. package/dist/core/backends/claude-auth.js +198 -0
  73. package/dist/core/backends/claude-auth.js.map +1 -0
  74. package/dist/core/backends/claude-code-core.d.ts +47 -119
  75. package/dist/core/backends/claude-code-core.d.ts.map +1 -1
  76. package/dist/core/backends/claude-code-core.js +112 -1565
  77. package/dist/core/backends/claude-code-core.js.map +1 -1
  78. package/dist/core/backends/claude-delegated.d.ts +86 -0
  79. package/dist/core/backends/claude-delegated.d.ts.map +1 -0
  80. package/dist/core/backends/claude-delegated.js +801 -0
  81. package/dist/core/backends/claude-delegated.js.map +1 -0
  82. package/dist/core/backends/claude-errors.d.ts +39 -0
  83. package/dist/core/backends/claude-errors.d.ts.map +1 -0
  84. package/dist/core/backends/claude-errors.js +71 -0
  85. package/dist/core/backends/claude-errors.js.map +1 -0
  86. package/dist/core/backends/claude-probe.d.ts +103 -0
  87. package/dist/core/backends/claude-probe.d.ts.map +1 -0
  88. package/dist/core/backends/claude-probe.js +336 -0
  89. package/dist/core/backends/claude-probe.js.map +1 -0
  90. package/dist/core/backends/claude-tool-collection.d.ts +135 -0
  91. package/dist/core/backends/claude-tool-collection.d.ts.map +1 -0
  92. package/dist/core/backends/claude-tool-collection.js +831 -0
  93. package/dist/core/backends/claude-tool-collection.js.map +1 -0
  94. package/dist/core/backends/gemini-cli-core.d.ts +21 -0
  95. package/dist/core/backends/gemini-cli-core.d.ts.map +1 -1
  96. package/dist/core/backends/gemini-cli-core.js +84 -6
  97. package/dist/core/backends/gemini-cli-core.js.map +1 -1
  98. package/dist/core/backends/prompt-utils.d.ts +1 -0
  99. package/dist/core/backends/prompt-utils.d.ts.map +1 -1
  100. package/dist/core/backends/prompt-utils.js +60 -3
  101. package/dist/core/backends/prompt-utils.js.map +1 -1
  102. package/dist/core/context-builder.d.ts +36 -12
  103. package/dist/core/context-builder.d.ts.map +1 -1
  104. package/dist/core/context-builder.js +179 -89
  105. package/dist/core/context-builder.js.map +1 -1
  106. package/dist/core/dispatcher-date-utils.d.ts +49 -0
  107. package/dist/core/dispatcher-date-utils.d.ts.map +1 -0
  108. package/dist/core/dispatcher-date-utils.js +132 -0
  109. package/dist/core/dispatcher-date-utils.js.map +1 -0
  110. package/dist/core/dispatcher-error-handling.d.ts +159 -0
  111. package/dist/core/dispatcher-error-handling.d.ts.map +1 -0
  112. package/dist/core/dispatcher-error-handling.js +393 -0
  113. package/dist/core/dispatcher-error-handling.js.map +1 -0
  114. package/dist/core/dispatcher-hourly-check.d.ts +150 -0
  115. package/dist/core/dispatcher-hourly-check.d.ts.map +1 -0
  116. package/dist/core/dispatcher-hourly-check.js +665 -0
  117. package/dist/core/dispatcher-hourly-check.js.map +1 -0
  118. package/dist/core/dispatcher-message-handler.d.ts +170 -0
  119. package/dist/core/dispatcher-message-handler.d.ts.map +1 -0
  120. package/dist/core/dispatcher-message-handler.js +1054 -0
  121. package/dist/core/dispatcher-message-handler.js.map +1 -0
  122. package/dist/core/dispatcher-morning-routine.d.ts +169 -0
  123. package/dist/core/dispatcher-morning-routine.d.ts.map +1 -0
  124. package/dist/core/dispatcher-morning-routine.js +434 -0
  125. package/dist/core/dispatcher-morning-routine.js.map +1 -0
  126. package/dist/core/dispatcher-prompt.d.ts +107 -0
  127. package/dist/core/dispatcher-prompt.d.ts.map +1 -0
  128. package/dist/core/dispatcher-prompt.js +227 -0
  129. package/dist/core/dispatcher-prompt.js.map +1 -0
  130. package/dist/core/dispatcher-repository-helpers.d.ts +39 -0
  131. package/dist/core/dispatcher-repository-helpers.d.ts.map +1 -0
  132. package/dist/core/dispatcher-repository-helpers.js +86 -0
  133. package/dist/core/dispatcher-repository-helpers.js.map +1 -0
  134. package/dist/core/dispatcher-result-processor.d.ts +145 -0
  135. package/dist/core/dispatcher-result-processor.d.ts.map +1 -0
  136. package/dist/core/dispatcher-result-processor.js +414 -0
  137. package/dist/core/dispatcher-result-processor.js.map +1 -0
  138. package/dist/core/dispatcher-scheduled-tasks.d.ts +406 -0
  139. package/dist/core/dispatcher-scheduled-tasks.d.ts.map +1 -0
  140. package/dist/core/dispatcher-scheduled-tasks.js +998 -0
  141. package/dist/core/dispatcher-scheduled-tasks.js.map +1 -0
  142. package/dist/core/dispatcher-types.d.ts +296 -0
  143. package/dist/core/dispatcher-types.d.ts.map +1 -0
  144. package/dist/core/dispatcher-types.js +106 -0
  145. package/dist/core/dispatcher-types.js.map +1 -0
  146. package/dist/core/dispatcher.d.ts +86 -610
  147. package/dist/core/dispatcher.d.ts.map +1 -1
  148. package/dist/core/dispatcher.js +293 -3542
  149. package/dist/core/dispatcher.js.map +1 -1
  150. package/dist/core/integration-health.d.ts +18 -10
  151. package/dist/core/integration-health.d.ts.map +1 -1
  152. package/dist/core/integration-health.js +31 -1
  153. package/dist/core/integration-health.js.map +1 -1
  154. package/dist/core/integration-lifecycle.d.ts +65 -0
  155. package/dist/core/integration-lifecycle.d.ts.map +1 -1
  156. package/dist/core/integration-lifecycle.js +167 -16
  157. package/dist/core/integration-lifecycle.js.map +1 -1
  158. package/dist/core/integration-main-backend.d.ts +40 -0
  159. package/dist/core/integration-main-backend.d.ts.map +1 -1
  160. package/dist/core/integration-main-backend.js +89 -2
  161. package/dist/core/integration-main-backend.js.map +1 -1
  162. package/dist/core/management-md.d.ts +51 -17
  163. package/dist/core/management-md.d.ts.map +1 -1
  164. package/dist/core/management-md.js +233 -56
  165. package/dist/core/management-md.js.map +1 -1
  166. package/dist/core/output-language-policy.d.ts +74 -0
  167. package/dist/core/output-language-policy.d.ts.map +1 -0
  168. package/dist/core/output-language-policy.js +194 -0
  169. package/dist/core/output-language-policy.js.map +1 -0
  170. package/dist/core/prompts.d.ts +1 -0
  171. package/dist/core/prompts.d.ts.map +1 -1
  172. package/dist/core/prompts.js +121 -3
  173. package/dist/core/prompts.js.map +1 -1
  174. package/dist/core/repository-management-docs.d.ts +24 -0
  175. package/dist/core/repository-management-docs.d.ts.map +1 -1
  176. package/dist/core/repository-management-docs.js +210 -26
  177. package/dist/core/repository-management-docs.js.map +1 -1
  178. package/dist/core/routine-acquisition-plan.d.ts +131 -0
  179. package/dist/core/routine-acquisition-plan.d.ts.map +1 -0
  180. package/dist/core/routine-acquisition-plan.js +268 -0
  181. package/dist/core/routine-acquisition-plan.js.map +1 -0
  182. package/dist/core/routine-fetch-window-runner.d.ts +201 -0
  183. package/dist/core/routine-fetch-window-runner.d.ts.map +1 -0
  184. package/dist/core/routine-fetch-window-runner.js +661 -0
  185. package/dist/core/routine-fetch-window-runner.js.map +1 -0
  186. package/dist/core/routine-windows.d.ts +156 -0
  187. package/dist/core/routine-windows.d.ts.map +1 -0
  188. package/dist/core/routine-windows.js +330 -0
  189. package/dist/core/routine-windows.js.map +1 -0
  190. package/dist/core/skills-compiler.d.ts +11 -0
  191. package/dist/core/skills-compiler.d.ts.map +1 -1
  192. package/dist/core/skills-compiler.js +102 -13
  193. package/dist/core/skills-compiler.js.map +1 -1
  194. package/dist/core/skills-manifest.d.ts.map +1 -1
  195. package/dist/core/skills-manifest.js +26 -0
  196. package/dist/core/skills-manifest.js.map +1 -1
  197. package/dist/core/system-reset.d.ts.map +1 -1
  198. package/dist/core/system-reset.js +25 -2
  199. package/dist/core/system-reset.js.map +1 -1
  200. package/dist/db/observations.d.ts +45 -2
  201. package/dist/db/observations.d.ts.map +1 -1
  202. package/dist/db/observations.js +112 -14
  203. package/dist/db/observations.js.map +1 -1
  204. package/dist/db/schema.d.ts.map +1 -1
  205. package/dist/db/schema.js +13 -25
  206. package/dist/db/schema.js.map +1 -1
  207. package/dist/index.js +83 -610
  208. package/dist/index.js.map +1 -1
  209. package/dist/observers/delegated-sync-worker.d.ts +45 -2
  210. package/dist/observers/delegated-sync-worker.d.ts.map +1 -1
  211. package/dist/observers/delegated-sync-worker.js +71 -21
  212. package/dist/observers/delegated-sync-worker.js.map +1 -1
  213. package/dist/observers/mail-poller.d.ts +12 -5
  214. package/dist/observers/mail-poller.d.ts.map +1 -1
  215. package/dist/observers/mail-poller.js +36 -14
  216. package/dist/observers/mail-poller.js.map +1 -1
  217. package/dist/observers/manager.d.ts +37 -5
  218. package/dist/observers/manager.d.ts.map +1 -1
  219. package/dist/observers/manager.js +28 -10
  220. package/dist/observers/manager.js.map +1 -1
  221. package/dist/safety/risk-classifier.d.ts.map +1 -1
  222. package/dist/safety/risk-classifier.js +5 -0
  223. package/dist/safety/risk-classifier.js.map +1 -1
  224. package/dist/services/delegated-backend-invoker.d.ts +1 -51
  225. package/dist/services/delegated-backend-invoker.d.ts.map +1 -1
  226. package/dist/services/delegated-backend-invoker.js +41 -480
  227. package/dist/services/delegated-backend-invoker.js.map +1 -1
  228. package/dist/services/delegated-invoker-audit.d.ts +94 -0
  229. package/dist/services/delegated-invoker-audit.d.ts.map +1 -0
  230. package/dist/services/delegated-invoker-audit.js +238 -0
  231. package/dist/services/delegated-invoker-audit.js.map +1 -0
  232. package/dist/services/delegated-invoker-cache-hits.d.ts +34 -0
  233. package/dist/services/delegated-invoker-cache-hits.d.ts.map +1 -0
  234. package/dist/services/delegated-invoker-cache-hits.js +104 -0
  235. package/dist/services/delegated-invoker-cache-hits.js.map +1 -0
  236. package/dist/services/delegated-invoker-janitors.d.ts +28 -0
  237. package/dist/services/delegated-invoker-janitors.d.ts.map +1 -0
  238. package/dist/services/delegated-invoker-janitors.js +104 -0
  239. package/dist/services/delegated-invoker-janitors.js.map +1 -0
  240. package/dist/services/delegated-invoker-utils.d.ts +42 -0
  241. package/dist/services/delegated-invoker-utils.d.ts.map +1 -0
  242. package/dist/services/delegated-invoker-utils.js +100 -0
  243. package/dist/services/delegated-invoker-utils.js.map +1 -0
  244. package/dist/services/delegated-task-runtime.d.ts +1 -1
  245. package/dist/services/delegated-task-runtime.js +1 -1
  246. package/dist/services/integrations/snapshot-partitions.d.ts +5 -0
  247. package/dist/services/integrations/snapshot-partitions.d.ts.map +1 -1
  248. package/dist/services/integrations/snapshot-partitions.js +12 -0
  249. package/dist/services/integrations/snapshot-partitions.js.map +1 -1
  250. package/dist/services/voice/transcriber-impl.d.ts.map +1 -1
  251. package/dist/services/voice/transcriber-impl.js +46 -0
  252. package/dist/services/voice/transcriber-impl.js.map +1 -1
  253. package/package.json +12 -12
@@ -0,0 +1,801 @@
1
+ /**
2
+ * Claude-backend delegated-execution surface — pattern B split out of
3
+ * `claude-code-core.ts` as part of the file-split plan (Tier 2, §8).
4
+ *
5
+ * Two responsibilities, both stateless from a module perspective:
6
+ *
7
+ * 1. **`runDelegatedTool`** — DELEGATED-PROXY-API-DESIGN.md §4.5 single-tool
8
+ * proxy. Spawns a Claude SDK stream constrained to one named tool (plus
9
+ * the `ToolSearch` deferred-schema loader), captures the tool's result or
10
+ * classifies a failure into the canonical `errorClass` union, and returns
11
+ * a `DelegatedToolResult` to the invoker.
12
+ *
13
+ * 2. **`runDelegatedTask`** — DELEGATED-TASK-MODE-DESIGN.md §9.1 multi-tool
14
+ * task mode. Plans + executes 1..N MCP calls under a `maxToolCalls`
15
+ * ceiling, optionally bound to a JSON-schema (§13 Phase 3.1 structured
16
+ * output), and returns the validated final emission (or a classified
17
+ * failure).
18
+ *
19
+ * The two functions share no mutable state beyond a deps record holding
20
+ * `apiPort` (for `buildDaemonApiCliEnv`) and the read-token surface
21
+ * (`readToken` legacy fallback + scoped `readTokenManager`). Picking
22
+ * standalone async functions over a `ClaudeDelegatedRunner` class produces
23
+ * fewer cross-references and lets the test suite invoke them without
24
+ * instantiating `ClaudeCodeCore`. The thin `runDelegatedTool` /
25
+ * `runDelegatedTask` methods on `ClaudeCodeCore` remain as transitional
26
+ * shims (file-split-plan §15) — they forward to the functions here so
27
+ * existing callers (BackendRouter, DelegatedBackendInvoker, the test suite)
28
+ * continue to dispatch through `core.runDelegated*`.
29
+ */
30
+ import { query, } from "@anthropic-ai/claude-agent-sdk";
31
+ import { matchRunAllowedToolPattern } from "@aitne/shared";
32
+ import { classifyAbortReason, DelegatedProxyTimeoutError, } from "../agent-core.js";
33
+ import { buildDaemonApiCliEnv } from "../daemon-api-cli.js";
34
+ import { createLogger } from "../../logging.js";
35
+ import { ALWAYS_DISALLOWED_TOOLS } from "../../safety/always-disallowed.js";
36
+ import { DELEGATED_PROXY_DEFAULTS } from "../../services/delegated-proxy-config.js";
37
+ import { buildDelegatedToolPrompt, emptyCost, flattenToolResultContent, tryParseToolResult, withDurationMs, } from "../../services/delegated-tool-runtime.js";
38
+ import { IdleWatchdog } from "./idle-watchdog.js";
39
+ const logger = createLogger("claude-delegated");
40
+ /**
41
+ * The built-in Claude Code tool that loads schemas for deferred MCP tools.
42
+ * When a session inherits many MCP servers from the user's global config,
43
+ * the CLI defers a portion of the tool schemas; the model must call
44
+ * `ToolSearch` to bring a specific tool's schema into the working set
45
+ * before invoking it. The proxy explicitly allows it (see `runDelegatedTool`)
46
+ * and the stream parser excludes it from `wrongToolName` capture so a
47
+ * partial-trace failure (`ToolSearch` + max_turns before the connector
48
+ * call) classifies as `no_tool_call` rather than the misleading
49
+ * `wrong_tool=ToolSearch`.
50
+ */
51
+ const DEFERRED_TOOL_DISCOVERY_TOOL_NAME = "ToolSearch";
52
+ /**
53
+ * DELEGATED-PROXY-API-DESIGN.md §4.5 — Claude SDK single-tool proxy.
54
+ *
55
+ * Failure classification (errorClass values):
56
+ * - `wrong_tool` — model called a tool other than `toolName`
57
+ * (excluding `ToolSearch`, which is the expected deferred-schema loader).
58
+ * - `tool_error` — the tool call returned `is_error: true`.
59
+ * - `auth_error` — the SDK surfaced an authentication failure either
60
+ * mid-stream or as a thrown exception.
61
+ * - `no_tool_call` — the stream terminated without invoking `toolName`.
62
+ * - `parse_error` — the stream ended without a terminal `result` message.
63
+ * - `timeout` / `cancelled` — caller signal or idle-watchdog trip.
64
+ * - `subprocess_crashed` — exception thrown out of the iterator.
65
+ *
66
+ * Cost is captured from the terminal `SDKResultMessage` regardless of
67
+ * subtype so the invoker can attribute partial spend on the failure
68
+ * paths (no_tool_call, wrong_tool, tool_error).
69
+ */
70
+ export async function runDelegatedTool(deps, params) {
71
+ const startMs = Date.now();
72
+ const { toolName, toolArgs, modelId, maxTurns, maxBudgetUsd, sessionDir } = params;
73
+ const prompt = buildDelegatedToolPrompt(toolName, toolArgs);
74
+ const daemonReadToken = deps.readTokenManager?.issue(sessionDir) ?? deps.readToken;
75
+ let stream = null;
76
+ const aborted = { value: false };
77
+ // `abortReason` carries the reason that caused `aborted.value=true`
78
+ // so the post-loop classifier can map idle-watchdog aborts to
79
+ // `errorClass="timeout"`. Falls back to the caller's signal reason
80
+ // when only the caller initiated the abort.
81
+ let abortReason = null;
82
+ const closeStream = () => {
83
+ void (async () => {
84
+ try {
85
+ await stream?.return?.(undefined);
86
+ }
87
+ catch {
88
+ /* stream already closed */
89
+ }
90
+ })();
91
+ };
92
+ const onAbort = () => {
93
+ aborted.value = true;
94
+ abortReason = params.abortSignal?.reason ?? null;
95
+ closeStream();
96
+ };
97
+ if (params.abortSignal) {
98
+ if (params.abortSignal.aborted) {
99
+ aborted.value = true;
100
+ abortReason = params.abortSignal.reason ?? null;
101
+ }
102
+ else {
103
+ params.abortSignal.addEventListener("abort", onAbort, { once: true });
104
+ }
105
+ }
106
+ // Idle watchdog. Claude SDK runs in-process; cold-start is
107
+ // negligible (no MCP/CLI load) so the typical first message lands
108
+ // within 1-3 s. A 30 s idle threshold catches a stuck SDK iterator
109
+ // (network stall, server-side hang) without false-tripping a slow
110
+ // tool. On trip we close the stream the same way `onAbort` does and
111
+ // record the trip in `abortReason` so the classifier returns
112
+ // `errorClass="timeout"` (uniform with CLI backends).
113
+ const idleTimeoutMs = DELEGATED_PROXY_DEFAULTS.idleTimeoutMsByBackend.claude
114
+ ?? DELEGATED_PROXY_DEFAULTS.idleTimeoutMs;
115
+ const idleWatchdog = new IdleWatchdog({
116
+ idleTimeoutMs,
117
+ onTimeout: (idleMs) => {
118
+ if (aborted.value)
119
+ return;
120
+ aborted.value = true;
121
+ abortReason = new DelegatedProxyTimeoutError(`claude SDK stream idle for ${idleMs}ms (limit ${idleTimeoutMs}ms)`);
122
+ logger.warn({ idleMs, idleTimeoutMs, toolName }, "claude delegated proxy idle watchdog tripped");
123
+ closeStream();
124
+ },
125
+ });
126
+ try {
127
+ stream = query({
128
+ prompt,
129
+ options: {
130
+ model: modelId,
131
+ maxTurns,
132
+ maxBudgetUsd,
133
+ cwd: sessionDir,
134
+ env: buildDaemonApiCliEnv(sessionDir, deps.apiPort, { readToken: daemonReadToken, sessionBackend: "claude" }),
135
+ systemPrompt: { type: "preset", preset: "claude_code" },
136
+ permissionMode: "dontAsk",
137
+ // The connector tool must be pre-authorized — Claude SDK with
138
+ // permissionMode="dontAsk" silently denies anything not in
139
+ // allowedTools.
140
+ //
141
+ // ToolSearch is Claude Code's deferred-tool discovery mechanism:
142
+ // when many MCP servers are registered in the user's global
143
+ // config (~/.claude.json: Notion, Gmail, GCal, Drive, Figma,
144
+ // Canva, Hugging Face, …) the CLI ships only a working set of
145
+ // tool schemas and defers the rest. To call a deferred tool, the
146
+ // model must first call ToolSearch to load its schema. Without
147
+ // ToolSearch allowed, the proxy's first turn was wasted on a
148
+ // denied ToolSearch call (audit log 2026-04-29: 1 Notion failure
149
+ // logged as `wrong_tool=ToolSearch`, 5 logged as
150
+ // `subprocess_crashed: Reached maximum number of turns (2)` —
151
+ // the model retried other approaches and exhausted the budget).
152
+ //
153
+ // Allowing ToolSearch is safe: allowedTools enforcement still
154
+ // gates which tools can be CALLED, and ToolSearch only loads
155
+ // schemas into context. The proxy parser below also skips
156
+ // ToolSearch when capturing `wrongToolName` so a ToolSearch+
157
+ // partial-result trace classifies as `no_tool_call` rather than
158
+ // misleading `wrong_tool=ToolSearch`.
159
+ //
160
+ // TODO(future): a cleaner architectural fix is to materialize a
161
+ // session-local `.mcp.json` containing only the relevant
162
+ // connector's MCP server and pass `strictMcpConfig: true` —
163
+ // that prevents deferral entirely (one MCP server → schemas fit
164
+ // in the working set). Punted because it requires extracting
165
+ // server configs from the user's global file per integration.
166
+ allowedTools: [toolName, DEFERRED_TOOL_DISCOVERY_TOOL_NAME],
167
+ // Defense-in-depth: even with allowedTools restricted to a tight
168
+ // set, keep the absolute-block layer (rm -rf, sudo, secret file
169
+ // reads) merged so a future relaxation of allowedTools can't
170
+ // accidentally drop these guarantees.
171
+ disallowedTools: [...ALWAYS_DISALLOWED_TOOLS],
172
+ // Adaptive thinking is the SDK default for thinking-capable
173
+ // models (Haiku 4.5+ / Sonnet 4.6+). Per Anthropic's docs
174
+ // thinking happens within a single API call so it does not
175
+ // typically burn an extra turn — but for a proxy that issues
176
+ // one named tool call with explicit args, thinking adds latency
177
+ // and tokens for no benefit. The proxy.md profile says "no
178
+ // narration"; disabling thinking aligns runtime behavior with
179
+ // that intent.
180
+ thinking: { type: "disabled" },
181
+ },
182
+ });
183
+ let capturedToolUseId = null;
184
+ let capturedToolResult = undefined;
185
+ let capturedToolErrorMessage = null;
186
+ let wrongToolName = null;
187
+ let cost = emptyCost();
188
+ let terminalSubtype = null;
189
+ let terminalIsError = false;
190
+ let terminalErrors = [];
191
+ try {
192
+ idleWatchdog.start();
193
+ for await (const message of stream) {
194
+ idleWatchdog.beat();
195
+ if (aborted.value) {
196
+ break;
197
+ }
198
+ if (message.type === "assistant") {
199
+ const assistantMsg = message;
200
+ const blocks = assistantMsg.message?.content;
201
+ if (!Array.isArray(blocks))
202
+ continue;
203
+ for (const block of blocks) {
204
+ if (!block || typeof block !== "object")
205
+ continue;
206
+ const blockType = block.type;
207
+ if (blockType !== "tool_use")
208
+ continue;
209
+ const blockName = block.name;
210
+ const blockId = block.id;
211
+ if (typeof blockName !== "string" || typeof blockId !== "string") {
212
+ continue;
213
+ }
214
+ if (blockName === toolName) {
215
+ if (capturedToolUseId === null) {
216
+ capturedToolUseId = blockId;
217
+ }
218
+ }
219
+ else if (blockName === DEFERRED_TOOL_DISCOVERY_TOOL_NAME) {
220
+ // Expected intermediate step for loading the connector's
221
+ // deferred MCP schema — not a violation. Do not capture as
222
+ // wrongToolName so a partial trace (ToolSearch + max_turns
223
+ // before the connector call) classifies as `no_tool_call`
224
+ // instead of misleading `wrong_tool=ToolSearch`.
225
+ }
226
+ else if (wrongToolName === null) {
227
+ wrongToolName = blockName;
228
+ // Early abort: bound the wall-clock spend on a wrong_tool
229
+ // failure to ~5s. Set `aborted` so the next loop iteration
230
+ // breaks; close the SDK stream so any pending tool_use
231
+ // doesn't continue. The post-loop classifier checks
232
+ // `wrongToolName` BEFORE the abort branch, so the failure
233
+ // is correctly attributed.
234
+ aborted.value = true;
235
+ closeStream();
236
+ }
237
+ }
238
+ }
239
+ else if (message.type === "user") {
240
+ const userMsg = message;
241
+ const content = userMsg.message?.content;
242
+ if (!Array.isArray(content))
243
+ continue;
244
+ for (const block of content) {
245
+ if (!block || typeof block !== "object")
246
+ continue;
247
+ if (block.type !== "tool_result")
248
+ continue;
249
+ const tuid = block.tool_use_id;
250
+ if (tuid !== capturedToolUseId)
251
+ continue;
252
+ const isToolError = block.is_error === true;
253
+ const rawContent = block.content;
254
+ const flat = flattenToolResultContent(rawContent);
255
+ if (isToolError) {
256
+ capturedToolErrorMessage =
257
+ flat.trim().length > 0 ? flat : "tool returned is_error";
258
+ }
259
+ else if (capturedToolResult === undefined) {
260
+ capturedToolResult = tryParseToolResult(flat);
261
+ }
262
+ }
263
+ }
264
+ else if (message.type === "result") {
265
+ const r = message;
266
+ terminalSubtype = r.subtype;
267
+ terminalIsError = r.is_error;
268
+ cost = {
269
+ tokensInput: r.usage.input_tokens ?? 0,
270
+ tokensOutput: r.usage.output_tokens ?? 0,
271
+ cacheCreationTokens: r.usage.cache_creation_input_tokens ?? 0,
272
+ cacheReadTokens: r.usage.cache_read_input_tokens ?? 0,
273
+ costUsd: r.total_cost_usd ?? 0,
274
+ durationMs: r.duration_ms ?? Date.now() - startMs,
275
+ numTurns: r.num_turns ?? 0,
276
+ };
277
+ if (r.subtype !== "success" && "errors" in r && Array.isArray(r.errors)) {
278
+ terminalErrors = r.errors;
279
+ }
280
+ // The result message is terminal per SDK semantics. Break out
281
+ // before the next iterator step. When `r.is_error` is true, the
282
+ // SDK's transport sets `lastErrorResultText` and throws on the
283
+ // next `readMessages` iteration — wrapping it as
284
+ // `Error("Claude Code returned an error result: <text>")`. That
285
+ // throw would land in the outer catch and misclassify as
286
+ // `subprocess_crashed`, discarding the captured cost. Audit log
287
+ // (2026-04-29) showed 5 such failures with num_turns=0,
288
+ // tokens=0, masking that this was actually `error_max_turns`.
289
+ // Breaking here lets the post-loop classifier run with the
290
+ // captured terminalSubtype, terminalErrors, wrongToolName, and
291
+ // cost intact.
292
+ break;
293
+ }
294
+ }
295
+ }
296
+ finally {
297
+ idleWatchdog.stop();
298
+ try {
299
+ await stream?.return?.(undefined);
300
+ }
301
+ catch {
302
+ /* stream already closed */
303
+ }
304
+ }
305
+ cost = withDurationMs(cost, startMs);
306
+ // wrong_tool check hoisted above the abort branch because the
307
+ // early-abort path sets `aborted.value` AND `wrongToolName`.
308
+ // Without this ordering the failure would surface as `cancelled`
309
+ // instead of the actual upstream cause. The `abortReason` field
310
+ // distinguishes idle-watchdog aborts (errorClass="timeout") from
311
+ // caller-initiated cancels (errorClass="cancelled" unless the
312
+ // caller's reason was itself a `DelegatedProxyTimeoutError`).
313
+ if (wrongToolName !== null) {
314
+ return {
315
+ ok: false,
316
+ errorClass: "wrong_tool",
317
+ message: `model called '${wrongToolName}' instead of requested '${toolName}'`,
318
+ cost,
319
+ };
320
+ }
321
+ if (aborted.value) {
322
+ const reason = abortReason ?? params.abortSignal?.reason;
323
+ const errorClass = classifyAbortReason(reason);
324
+ const idleAbort = reason instanceof DelegatedProxyTimeoutError
325
+ && /idle/.test(reason.message);
326
+ return {
327
+ ok: false,
328
+ errorClass,
329
+ message: errorClass === "timeout"
330
+ ? (idleAbort
331
+ ? `delegated proxy stream went idle (no claude SDK events for ${idleTimeoutMs}ms)`
332
+ : "delegated proxy timed out (wall-clock)")
333
+ : "delegated proxy cancelled by caller",
334
+ cost,
335
+ };
336
+ }
337
+ if (capturedToolResult !== undefined) {
338
+ return { ok: true, toolResult: capturedToolResult, cost };
339
+ }
340
+ if (capturedToolErrorMessage !== null) {
341
+ return {
342
+ ok: false,
343
+ errorClass: "tool_error",
344
+ message: capturedToolErrorMessage,
345
+ cost,
346
+ };
347
+ }
348
+ // Map specific terminal subtypes before falling through to
349
+ // no_tool_call. The model can fail for auth or budget reasons
350
+ // before ever emitting a tool_use block.
351
+ if (terminalSubtype === "error_during_execution" && terminalErrors.length > 0) {
352
+ const joined = terminalErrors.join("; ");
353
+ if (/auth|unauthorized|authentication_failed|invalid api key/i.test(joined)) {
354
+ return {
355
+ ok: false,
356
+ errorClass: "auth_error",
357
+ message: joined,
358
+ cost,
359
+ };
360
+ }
361
+ return {
362
+ ok: false,
363
+ errorClass: "tool_error",
364
+ message: joined,
365
+ cost,
366
+ };
367
+ }
368
+ if (terminalSubtype === null && !terminalIsError) {
369
+ // Stream ended before any terminal `result` arrived — abnormal
370
+ // termination not classified as an abort. Treat as parse_error
371
+ // so the route handler can surface the bug rather than retrying.
372
+ return {
373
+ ok: false,
374
+ errorClass: "parse_error",
375
+ message: "Claude SDK stream ended without a terminal result message",
376
+ cost,
377
+ };
378
+ }
379
+ return {
380
+ ok: false,
381
+ errorClass: "no_tool_call",
382
+ message: `model did not invoke '${toolName}' within ${maxTurns} turns (subtype=${terminalSubtype ?? "unknown"})`,
383
+ cost,
384
+ };
385
+ }
386
+ catch (err) {
387
+ const message = err instanceof Error ? err.message : String(err);
388
+ const cost = withDurationMs(emptyCost(), startMs);
389
+ // Map auth-shape exceptions before the catch-all subprocess_crashed.
390
+ if (/authentication_failed|unauthorized|invalid api key|sk-ant-/i.test(message)) {
391
+ return { ok: false, errorClass: "auth_error", message, cost };
392
+ }
393
+ if (aborted.value) {
394
+ return {
395
+ ok: false,
396
+ errorClass: classifyAbortReason(abortReason ?? params.abortSignal?.reason),
397
+ message,
398
+ cost,
399
+ };
400
+ }
401
+ return { ok: false, errorClass: "subprocess_crashed", message, cost };
402
+ }
403
+ finally {
404
+ params.abortSignal?.removeEventListener("abort", onAbort);
405
+ deps.readTokenManager?.revoke(sessionDir);
406
+ }
407
+ }
408
+ /**
409
+ * DELEGATED-TASK-MODE-DESIGN.md §9.1 — Claude SDK task mode. The
410
+ * subprocess plans + executes 1..N MCP calls within `allowedTools` and
411
+ * emits a final assistant message that the runtime helper validates
412
+ * against the caller's `outputSchema`.
413
+ *
414
+ * Stream parsing differences from `runDelegatedTool`:
415
+ * - We accept multiple `tool_use` blocks (counted against `maxToolCalls`).
416
+ * - We track per-tool durations to feed `onToolStep`.
417
+ * - We capture the *final* assistant text (after the last tool turn)
418
+ * as the validation target, not a single tool's `tool_result`.
419
+ *
420
+ * Safety:
421
+ * - `allowedTools` already excludes the destructive set when
422
+ * `allowDestructive: false`; the SDK will not surface those tools.
423
+ * - `disallowedTools` is the absolute-block layer + the destructive
424
+ * set as defense-in-depth (so a future relaxation of allowedTools
425
+ * can't accidentally widen the surface).
426
+ *
427
+ * The §6.2 "no retry after write" rule is enforced at the invoker
428
+ * layer; this method just signals via `writeClassToolFired` whether
429
+ * any destructive tool ran during the task.
430
+ */
431
+ export async function runDelegatedTask(deps, params) {
432
+ const startMs = Date.now();
433
+ const { systemPrompt, allowedTools, destructiveTools, writeClassTools, modelId, maxToolCalls, maxBudgetUsd, sessionDir, onToolStep, } = params;
434
+ const daemonReadToken = deps.readTokenManager?.issue(sessionDir) ?? deps.readToken;
435
+ const trace = [];
436
+ // §6.2 / §7.4 — match against the *write-class* set (destructive ∪
437
+ // reversible writes), not just destructive. Otherwise reversible
438
+ // write tools like `create_draft` slip past the retry guard and the
439
+ // single retry creates a duplicate side effect.
440
+ //
441
+ // Phase 1 (`/exec`) entries are fully-qualified exact names — the
442
+ // exact-equality fast path inside `matchRunAllowedToolPattern` covers
443
+ // them at one comparison. Phase 2 (`/api/delegated/run`) may pass
444
+ // `*`-suffixed glob patterns derived from the caller's allowedTools
445
+ // (DELEGATED-TASK-MODE-DESIGN.md §4.2); the shared helper handles both.
446
+ const writeClassMatcher = (name) => writeClassTools.some((pattern) => matchRunAllowedToolPattern(pattern, name));
447
+ let writeClassToolFired = false;
448
+ // DELEGATED-TASK-MODE-DESIGN.md §13 Phase 3.1 — Claude SDK
449
+ // structured-output. When the invoker passed `structuredOutputEnabled:
450
+ // true` AND a `wrappedSchema`, configure `outputFormat` so the SDK
451
+ // validates the model's final emission against the schema (with its
452
+ // own internal retries) and surfaces it on `SDKResultSuccess.structured_output`.
453
+ // We still capture the assistant text as a fallback — if the SDK
454
+ // returns success without `structured_output` (older subtype, future
455
+ // shape change, kill-switch flips off mid-call), the existing text
456
+ // path takes over.
457
+ const useStructuredOutput = params.structuredOutputEnabled === true
458
+ && !!params.wrappedSchema;
459
+ let capturedStructured;
460
+ let sawStructuredOutputRetryError = false;
461
+ let stream = null;
462
+ const aborted = { value: false };
463
+ const onAbort = () => {
464
+ aborted.value = true;
465
+ void (async () => {
466
+ try {
467
+ await stream?.return?.(undefined);
468
+ }
469
+ catch {
470
+ /* stream already closed */
471
+ }
472
+ })();
473
+ };
474
+ if (params.abortSignal) {
475
+ if (params.abortSignal.aborted) {
476
+ aborted.value = true;
477
+ }
478
+ else {
479
+ params.abortSignal.addEventListener("abort", onAbort, { once: true });
480
+ }
481
+ }
482
+ const pendingByUseId = new Map();
483
+ let toolCallCount = 0;
484
+ let loopAborted = false;
485
+ let assistantTextChunks = [];
486
+ /** The "final" assistant message is the most recent assistant message
487
+ * that contained NO `tool_use` block. The SDK emits one assistant
488
+ * message per turn; the planning turns mix text + tool_use, the
489
+ * closing turn is text-only. */
490
+ let lastAssistantTextOnlyChunks = [];
491
+ try {
492
+ stream = query({
493
+ prompt: systemPrompt,
494
+ options: {
495
+ model: modelId,
496
+ maxTurns: Math.max(2, maxToolCalls + 1),
497
+ maxBudgetUsd,
498
+ cwd: sessionDir,
499
+ env: buildDaemonApiCliEnv(sessionDir, deps.apiPort, { readToken: daemonReadToken, sessionBackend: "claude" }),
500
+ systemPrompt: { type: "preset", preset: "claude_code" },
501
+ permissionMode: "dontAsk",
502
+ allowedTools: [...allowedTools],
503
+ // Defense-in-depth: absolute-block layer + destructive denies.
504
+ // Destructive entries are redundant with the allowedTools
505
+ // subtraction (when allowDestructive=false) but kept so a
506
+ // future allowedTools widening doesn't drop the guarantee.
507
+ disallowedTools: [
508
+ ...ALWAYS_DISALLOWED_TOOLS,
509
+ ...(params.allowDestructive ? [] : destructiveTools),
510
+ ],
511
+ // §13 Phase 3.1 — bind the wrapped schema (user schema OR
512
+ // confirmation envelope OR error envelope) to SDK 0.2.98's
513
+ // `outputFormat`. Result message carries `structured_output`
514
+ // which we read below. Off when the kill switch is false or
515
+ // the invoker omitted the wrapped schema.
516
+ ...(useStructuredOutput && params.wrappedSchema
517
+ ? {
518
+ outputFormat: {
519
+ type: "json_schema",
520
+ schema: params.wrappedSchema,
521
+ },
522
+ }
523
+ : {}),
524
+ },
525
+ });
526
+ let cost = emptyCost();
527
+ try {
528
+ for await (const message of stream) {
529
+ if (aborted.value || loopAborted)
530
+ break;
531
+ if (message.type === "assistant") {
532
+ const assistantMsg = message;
533
+ const blocks = assistantMsg.message?.content;
534
+ if (!Array.isArray(blocks))
535
+ continue;
536
+ const textChunks = [];
537
+ let sawToolUse = false;
538
+ for (const block of blocks) {
539
+ if (!block || typeof block !== "object")
540
+ continue;
541
+ const blockType = block.type;
542
+ if (blockType === "text") {
543
+ const text = block.text;
544
+ if (typeof text === "string")
545
+ textChunks.push(text);
546
+ continue;
547
+ }
548
+ if (blockType !== "tool_use")
549
+ continue;
550
+ sawToolUse = true;
551
+ const blockName = block.name;
552
+ const blockId = block.id;
553
+ const blockArgs = block.input;
554
+ if (typeof blockName !== "string" || typeof blockId !== "string") {
555
+ continue;
556
+ }
557
+ toolCallCount += 1;
558
+ if (toolCallCount > maxToolCalls) {
559
+ // §7.5 — once the cap is exceeded, abort. The next
560
+ // tool_use is treated as overrun.
561
+ loopAborted = true;
562
+ aborted.value = true;
563
+ try {
564
+ await stream?.return?.(undefined);
565
+ }
566
+ catch {
567
+ /* already closed */
568
+ }
569
+ break;
570
+ }
571
+ if (writeClassMatcher(blockName)) {
572
+ writeClassToolFired = true;
573
+ }
574
+ pendingByUseId.set(blockId, {
575
+ name: blockName,
576
+ args: blockArgs,
577
+ startedAt: Date.now(),
578
+ });
579
+ }
580
+ assistantTextChunks = assistantTextChunks.concat(textChunks);
581
+ if (!sawToolUse && textChunks.length > 0) {
582
+ lastAssistantTextOnlyChunks = textChunks;
583
+ }
584
+ }
585
+ else if (message.type === "user") {
586
+ const userMsg = message;
587
+ const content = userMsg.message?.content;
588
+ if (!Array.isArray(content))
589
+ continue;
590
+ for (const block of content) {
591
+ if (!block || typeof block !== "object")
592
+ continue;
593
+ if (block.type !== "tool_result")
594
+ continue;
595
+ const tuid = block.tool_use_id;
596
+ if (typeof tuid !== "string")
597
+ continue;
598
+ const pending = pendingByUseId.get(tuid);
599
+ if (!pending)
600
+ continue;
601
+ pendingByUseId.delete(tuid);
602
+ const isToolError = block.is_error === true;
603
+ // `tool_result` content is either a string or an array of
604
+ // content blocks (typically a single `{type:"text", text}`).
605
+ // The MCP SDK wraps connector JSON responses by serializing
606
+ // to that text body, so the response-shape walker
607
+ // downstream wants the parsed object. Pull the first text
608
+ // block, JSON-parse when possible, fallback to the raw
609
+ // string so the field is always populated for ok steps.
610
+ let parsedToolResult;
611
+ const blockContent = block.content;
612
+ if (typeof blockContent === "string") {
613
+ try {
614
+ parsedToolResult = JSON.parse(blockContent);
615
+ }
616
+ catch {
617
+ parsedToolResult = blockContent;
618
+ }
619
+ }
620
+ else if (Array.isArray(blockContent)) {
621
+ const firstText = blockContent.find((b) => !!b
622
+ && typeof b === "object"
623
+ && b.type === "text"
624
+ && typeof b.text === "string");
625
+ if (firstText) {
626
+ try {
627
+ parsedToolResult = JSON.parse(firstText.text);
628
+ }
629
+ catch {
630
+ parsedToolResult = firstText.text;
631
+ }
632
+ }
633
+ else {
634
+ parsedToolResult = blockContent;
635
+ }
636
+ }
637
+ const step = {
638
+ toolName: pending.name,
639
+ toolArgs: pending.args,
640
+ durationMs: Date.now() - pending.startedAt,
641
+ status: isToolError ? "error" : "ok",
642
+ costUsd: null,
643
+ tokensInput: null,
644
+ tokensOutput: null,
645
+ toolResult: parsedToolResult,
646
+ };
647
+ trace.push(step);
648
+ onToolStep?.(step);
649
+ }
650
+ }
651
+ else if (message.type === "result") {
652
+ const r = message;
653
+ cost = {
654
+ tokensInput: r.usage.input_tokens ?? 0,
655
+ tokensOutput: r.usage.output_tokens ?? 0,
656
+ cacheCreationTokens: r.usage.cache_creation_input_tokens ?? 0,
657
+ cacheReadTokens: r.usage.cache_read_input_tokens ?? 0,
658
+ costUsd: r.total_cost_usd ?? 0,
659
+ durationMs: r.duration_ms ?? Date.now() - startMs,
660
+ numTurns: r.num_turns ?? 0,
661
+ };
662
+ // §13 Phase 3.1 — capture structured output when present, and
663
+ // map the SDK's structured-output-retry-exhausted subtype to
664
+ // a parse_error so the invoker classifies it consistently
665
+ // with the text-extract path.
666
+ if (r.subtype === "success") {
667
+ const success = r;
668
+ if (success.structured_output !== undefined) {
669
+ capturedStructured = success.structured_output;
670
+ }
671
+ }
672
+ else if (r.subtype === "error_max_structured_output_retries") {
673
+ sawStructuredOutputRetryError = true;
674
+ }
675
+ }
676
+ }
677
+ }
678
+ finally {
679
+ try {
680
+ await stream?.return?.(undefined);
681
+ }
682
+ catch {
683
+ /* already closed */
684
+ }
685
+ }
686
+ cost = withDurationMs(cost, startMs);
687
+ if (loopAborted) {
688
+ return {
689
+ ok: false,
690
+ errorClass: "loop_aborted",
691
+ message: `subprocess exceeded maxToolCalls=${maxToolCalls}`,
692
+ cost,
693
+ trace,
694
+ writeClassToolFired,
695
+ };
696
+ }
697
+ if (aborted.value) {
698
+ const errorClass = classifyAbortReason(params.abortSignal?.reason);
699
+ return {
700
+ ok: false,
701
+ errorClass,
702
+ message: errorClass === "timeout"
703
+ ? "delegated task timed out (wall-clock)"
704
+ : "delegated task cancelled by caller",
705
+ cost,
706
+ trace,
707
+ writeClassToolFired,
708
+ };
709
+ }
710
+ const finalText = lastAssistantTextOnlyChunks.length > 0
711
+ ? lastAssistantTextOnlyChunks.join("\n").trim()
712
+ : assistantTextChunks.join("\n").trim();
713
+ // §13 Phase 3.1 — `error_max_structured_output_retries` typically
714
+ // fires when the model wanted to emit a §7.2 confirmation envelope
715
+ // or §5.1 error envelope, neither of which satisfies the user's
716
+ // narrow schema. The assistant text emissions captured during those
717
+ // retries land in `assistantTextChunks` / `lastAssistantTextOnlyChunks`,
718
+ // so the invoker's text-extract chain can route them via
719
+ // `detectConfirmationEnvelope` / `detectErrorEnvelope`. Only return
720
+ // `parse_error` if there is also no usable text — otherwise fall
721
+ // through to the text-emission path (no `structuredOutput` field
722
+ // set, so the invoker uses `rawAssistantText`).
723
+ if (sawStructuredOutputRetryError
724
+ && capturedStructured === undefined
725
+ && finalText.length === 0) {
726
+ return {
727
+ ok: false,
728
+ errorClass: "parse_error",
729
+ message: "Claude SDK exhausted structured-output retries and emitted no text fallback.",
730
+ cost,
731
+ trace,
732
+ writeClassToolFired,
733
+ };
734
+ }
735
+ // §13 Phase 3.1 — when the SDK supplied `structured_output`, that
736
+ // is the validated final emission; the assistant text may be empty
737
+ // (the SDK consumes the JSON internally on success). Skip the
738
+ // empty-text parse_error guard in that case.
739
+ if (capturedStructured === undefined && finalText.length === 0) {
740
+ return {
741
+ ok: false,
742
+ errorClass: "parse_error",
743
+ message: "Claude SDK stream ended without a text-only assistant turn",
744
+ cost,
745
+ trace,
746
+ writeClassToolFired,
747
+ };
748
+ }
749
+ return {
750
+ ok: true,
751
+ // When structured output is present, `rawAssistantText` is purely
752
+ // a fallback; the invoker prefers `structuredOutput`. Carry both
753
+ // so a future kill-switch flip mid-restart still sees an
754
+ // extractable text emission.
755
+ rawAssistantText: finalText,
756
+ cost,
757
+ trace,
758
+ writeClassToolFired,
759
+ ...(capturedStructured !== undefined
760
+ ? { structuredOutput: capturedStructured }
761
+ : {}),
762
+ };
763
+ }
764
+ catch (err) {
765
+ const message = err instanceof Error ? err.message : String(err);
766
+ const cost = withDurationMs(emptyCost(), startMs);
767
+ if (/authentication_failed|unauthorized|invalid api key|sk-ant-/i.test(message)) {
768
+ return {
769
+ ok: false,
770
+ errorClass: "auth_error",
771
+ message,
772
+ cost,
773
+ trace,
774
+ writeClassToolFired,
775
+ };
776
+ }
777
+ if (aborted.value) {
778
+ return {
779
+ ok: false,
780
+ errorClass: classifyAbortReason(params.abortSignal?.reason),
781
+ message,
782
+ cost,
783
+ trace,
784
+ writeClassToolFired,
785
+ };
786
+ }
787
+ return {
788
+ ok: false,
789
+ errorClass: "subprocess_crashed",
790
+ message,
791
+ cost,
792
+ trace,
793
+ writeClassToolFired,
794
+ };
795
+ }
796
+ finally {
797
+ params.abortSignal?.removeEventListener("abort", onAbort);
798
+ deps.readTokenManager?.revoke(sessionDir);
799
+ }
800
+ }
801
+ //# sourceMappingURL=claude-delegated.js.map