@tanstack/ai 0.42.0 → 0.43.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 (272) hide show
  1. package/README.md +15 -1
  2. package/dist/esm/activities/chat/adapter.js +23 -16
  3. package/dist/esm/activities/chat/adapter.js.map +1 -1
  4. package/dist/esm/activities/chat/agent-loop-strategies.d.ts +5 -36
  5. package/dist/esm/activities/chat/agent-loop-strategies.js +75 -21
  6. package/dist/esm/activities/chat/agent-loop-strategies.js.map +1 -1
  7. package/dist/esm/activities/chat/cancel.d.ts +40 -0
  8. package/dist/esm/activities/chat/cancel.js +54 -0
  9. package/dist/esm/activities/chat/cancel.js.map +1 -0
  10. package/dist/esm/activities/chat/index.d.ts +28 -21
  11. package/dist/esm/activities/chat/index.js +2100 -1813
  12. package/dist/esm/activities/chat/index.js.map +1 -1
  13. package/dist/esm/activities/chat/mcp/manager.d.ts +2 -2
  14. package/dist/esm/activities/chat/mcp/manager.js +90 -77
  15. package/dist/esm/activities/chat/mcp/manager.js.map +1 -1
  16. package/dist/esm/activities/chat/mcp/types.d.ts +2 -2
  17. package/dist/esm/activities/chat/messages.js +397 -346
  18. package/dist/esm/activities/chat/messages.js.map +1 -1
  19. package/dist/esm/activities/chat/middleware/builder.js +17 -15
  20. package/dist/esm/activities/chat/middleware/builder.js.map +1 -1
  21. package/dist/esm/activities/chat/middleware/capabilities.js +78 -43
  22. package/dist/esm/activities/chat/middleware/capabilities.js.map +1 -1
  23. package/dist/esm/activities/chat/middleware/compose.d.ts +94 -1
  24. package/dist/esm/activities/chat/middleware/compose.js +623 -531
  25. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  26. package/dist/esm/activities/chat/middleware/define.js +12 -5
  27. package/dist/esm/activities/chat/middleware/define.js.map +1 -1
  28. package/dist/esm/activities/chat/middleware/index.d.ts +5 -1
  29. package/dist/esm/activities/chat/middleware/locks.d.ts +50 -0
  30. package/dist/esm/activities/chat/middleware/locks.js +71 -0
  31. package/dist/esm/activities/chat/middleware/locks.js.map +1 -0
  32. package/dist/esm/activities/chat/middleware/pending-turn.d.ts +15 -0
  33. package/dist/esm/activities/chat/middleware/pending-turn.js +35 -0
  34. package/dist/esm/activities/chat/middleware/pending-turn.js.map +1 -0
  35. package/dist/esm/activities/chat/middleware/run-disconnect.d.ts +23 -0
  36. package/dist/esm/activities/chat/middleware/run-disconnect.js +42 -0
  37. package/dist/esm/activities/chat/middleware/run-disconnect.js.map +1 -0
  38. package/dist/esm/activities/chat/middleware/run-store.d.ts +283 -0
  39. package/dist/esm/activities/chat/middleware/run-store.js +176 -0
  40. package/dist/esm/activities/chat/middleware/run-store.js.map +1 -0
  41. package/dist/esm/activities/chat/middleware/sandbox-runtime.js +14 -8
  42. package/dist/esm/activities/chat/middleware/sandbox-runtime.js.map +1 -1
  43. package/dist/esm/activities/chat/middleware/tool-cache-middleware.js +79 -70
  44. package/dist/esm/activities/chat/middleware/tool-cache-middleware.js.map +1 -1
  45. package/dist/esm/activities/chat/middleware/types.d.ts +59 -2
  46. package/dist/esm/activities/chat/middleware/validate.js +23 -28
  47. package/dist/esm/activities/chat/middleware/validate.js.map +1 -1
  48. package/dist/esm/activities/chat/stream/json-parser.js +39 -25
  49. package/dist/esm/activities/chat/stream/json-parser.js.map +1 -1
  50. package/dist/esm/activities/chat/stream/message-updaters.js +275 -234
  51. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  52. package/dist/esm/activities/chat/stream/processor.d.ts +24 -4
  53. package/dist/esm/activities/chat/stream/processor.js +1341 -1542
  54. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  55. package/dist/esm/activities/chat/stream/strategies.js +69 -53
  56. package/dist/esm/activities/chat/stream/strategies.js.map +1 -1
  57. package/dist/esm/activities/chat/tools/approval-schema.d.ts +19 -0
  58. package/dist/esm/activities/chat/tools/approval-schema.js +117 -0
  59. package/dist/esm/activities/chat/tools/approval-schema.js.map +1 -0
  60. package/dist/esm/activities/chat/tools/lazy-tool-manager.js +164 -191
  61. package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
  62. package/dist/esm/activities/chat/tools/lazy-tools.js +24 -12
  63. package/dist/esm/activities/chat/tools/lazy-tools.js.map +1 -1
  64. package/dist/esm/activities/chat/tools/schema-converter.js +293 -146
  65. package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
  66. package/dist/esm/activities/chat/tools/tool-calls.d.ts +18 -2
  67. package/dist/esm/activities/chat/tools/tool-calls.js +522 -531
  68. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  69. package/dist/esm/activities/chat/tools/tool-definition.d.ts +75 -16
  70. package/dist/esm/activities/chat/tools/tool-definition.js +95 -23
  71. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  72. package/dist/esm/activities/error-payload.js +85 -47
  73. package/dist/esm/activities/error-payload.js.map +1 -1
  74. package/dist/esm/activities/generateAudio/adapter.js +22 -15
  75. package/dist/esm/activities/generateAudio/adapter.js.map +1 -1
  76. package/dist/esm/activities/generateAudio/index.d.ts +4 -0
  77. package/dist/esm/activities/generateAudio/index.js +141 -105
  78. package/dist/esm/activities/generateAudio/index.js.map +1 -1
  79. package/dist/esm/activities/generateImage/adapter.js +22 -15
  80. package/dist/esm/activities/generateImage/adapter.js.map +1 -1
  81. package/dist/esm/activities/generateImage/index.d.ts +4 -0
  82. package/dist/esm/activities/generateImage/index.js +155 -111
  83. package/dist/esm/activities/generateImage/index.js.map +1 -1
  84. package/dist/esm/activities/generateSpeech/adapter.js +22 -15
  85. package/dist/esm/activities/generateSpeech/adapter.js.map +1 -1
  86. package/dist/esm/activities/generateSpeech/index.d.ts +4 -0
  87. package/dist/esm/activities/generateSpeech/index.js +159 -110
  88. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  89. package/dist/esm/activities/generateTranscription/adapter.js +22 -15
  90. package/dist/esm/activities/generateTranscription/adapter.js.map +1 -1
  91. package/dist/esm/activities/generateTranscription/index.d.ts +4 -0
  92. package/dist/esm/activities/generateTranscription/index.js +159 -100
  93. package/dist/esm/activities/generateTranscription/index.js.map +1 -1
  94. package/dist/esm/activities/generateVideo/adapter.js +36 -29
  95. package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
  96. package/dist/esm/activities/generateVideo/index.d.ts +143 -19
  97. package/dist/esm/activities/generateVideo/index.js +456 -279
  98. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  99. package/dist/esm/activities/generateVideo/snap.js +60 -48
  100. package/dist/esm/activities/generateVideo/snap.js.map +1 -1
  101. package/dist/esm/activities/index.js +8 -34
  102. package/dist/esm/activities/middleware/index.d.ts +1 -1
  103. package/dist/esm/activities/middleware/run.d.ts +10 -0
  104. package/dist/esm/activities/middleware/run.js +53 -29
  105. package/dist/esm/activities/middleware/run.js.map +1 -1
  106. package/dist/esm/activities/middleware/types.d.ts +44 -6
  107. package/dist/esm/activities/stream-generation-result.d.ts +4 -1
  108. package/dist/esm/activities/stream-generation-result.js +79 -44
  109. package/dist/esm/activities/stream-generation-result.js.map +1 -1
  110. package/dist/esm/activities/summarize/adapter.js +22 -15
  111. package/dist/esm/activities/summarize/adapter.js.map +1 -1
  112. package/dist/esm/activities/summarize/chat-stream-summarize.js +252 -202
  113. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
  114. package/dist/esm/activities/summarize/index.d.ts +27 -0
  115. package/dist/esm/activities/summarize/index.js +268 -102
  116. package/dist/esm/activities/summarize/index.js.map +1 -1
  117. package/dist/esm/adapter-internals.d.ts +2 -1
  118. package/dist/esm/adapter-internals.js +4 -11
  119. package/dist/esm/client.d.ts +25 -3
  120. package/dist/esm/client.js +131 -64
  121. package/dist/esm/client.js.map +1 -1
  122. package/dist/esm/custom-events.d.ts +76 -0
  123. package/dist/esm/custom-events.js +37 -0
  124. package/dist/esm/custom-events.js.map +1 -0
  125. package/dist/esm/delivery-detach.d.ts +50 -0
  126. package/dist/esm/delivery-detach.js +71 -0
  127. package/dist/esm/delivery-detach.js.map +1 -0
  128. package/dist/esm/delivery-disconnect.d.ts +62 -0
  129. package/dist/esm/delivery-disconnect.js +81 -0
  130. package/dist/esm/delivery-disconnect.js.map +1 -0
  131. package/dist/esm/extend-adapter.js +19 -17
  132. package/dist/esm/extend-adapter.js.map +1 -1
  133. package/dist/esm/index.d.ts +24 -6
  134. package/dist/esm/index.js +30 -98
  135. package/dist/esm/interrupt-resume.d.ts +71 -0
  136. package/dist/esm/interrupt-resume.js +438 -0
  137. package/dist/esm/interrupt-resume.js.map +1 -0
  138. package/dist/esm/interrupt-serialization.d.ts +12 -0
  139. package/dist/esm/interrupt-serialization.js +178 -0
  140. package/dist/esm/interrupt-serialization.js.map +1 -0
  141. package/dist/esm/interrupts.d.ts +84 -0
  142. package/dist/esm/interrupts.js +31 -0
  143. package/dist/esm/interrupts.js.map +1 -0
  144. package/dist/esm/locks.d.ts +10 -0
  145. package/dist/esm/locks.js +2 -0
  146. package/dist/esm/logger/console-logger.js +101 -78
  147. package/dist/esm/logger/console-logger.js.map +1 -1
  148. package/dist/esm/logger/internal-logger.js +104 -89
  149. package/dist/esm/logger/internal-logger.js.map +1 -1
  150. package/dist/esm/logger/resolve.js +54 -49
  151. package/dist/esm/logger/resolve.js.map +1 -1
  152. package/dist/esm/logger/types.d.ts +1 -1
  153. package/dist/esm/middlewares/content-guard.js +142 -148
  154. package/dist/esm/middlewares/content-guard.js.map +1 -1
  155. package/dist/esm/middlewares/index.js +2 -6
  156. package/dist/esm/middlewares/otel.js +598 -732
  157. package/dist/esm/middlewares/otel.js.map +1 -1
  158. package/dist/esm/middlewares/usage-attributes.js +47 -40
  159. package/dist/esm/middlewares/usage-attributes.js.map +1 -1
  160. package/dist/esm/realtime/event-emitter.js +24 -25
  161. package/dist/esm/realtime/event-emitter.js.map +1 -1
  162. package/dist/esm/realtime/index.d.ts +5 -9
  163. package/dist/esm/realtime/index.js +29 -6
  164. package/dist/esm/realtime/index.js.map +1 -1
  165. package/dist/esm/scope.d.ts +47 -0
  166. package/dist/esm/stream-durability.d.ts +171 -0
  167. package/dist/esm/stream-durability.js +295 -0
  168. package/dist/esm/stream-durability.js.map +1 -0
  169. package/dist/esm/stream-to-response.d.ts +178 -13
  170. package/dist/esm/stream-to-response.js +663 -115
  171. package/dist/esm/stream-to-response.js.map +1 -1
  172. package/dist/esm/strip-to-spec-middleware.js +30 -16
  173. package/dist/esm/strip-to-spec-middleware.js.map +1 -1
  174. package/dist/esm/system-prompts.js +27 -21
  175. package/dist/esm/system-prompts.js.map +1 -1
  176. package/dist/esm/tool-registry.js +72 -45
  177. package/dist/esm/tool-registry.js.map +1 -1
  178. package/dist/esm/tools/provider-tool.js +14 -5
  179. package/dist/esm/tools/provider-tool.js.map +1 -1
  180. package/dist/esm/types.d.ts +321 -42
  181. package/dist/esm/types.js +2 -0
  182. package/dist/esm/utilities/ag-ui-wire.js +79 -93
  183. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  184. package/dist/esm/utilities/chat-params.d.ts +26 -4
  185. package/dist/esm/utilities/chat-params.js +218 -92
  186. package/dist/esm/utilities/chat-params.js.map +1 -1
  187. package/dist/esm/utilities/errors.js +28 -18
  188. package/dist/esm/utilities/errors.js.map +1 -1
  189. package/dist/esm/utilities/media-prompt.js +46 -41
  190. package/dist/esm/utilities/media-prompt.js.map +1 -1
  191. package/dist/esm/utilities/numbers.js +13 -10
  192. package/dist/esm/utilities/numbers.js.map +1 -1
  193. package/dist/esm/utilities/provider-executed.js +20 -11
  194. package/dist/esm/utilities/provider-executed.js.map +1 -1
  195. package/dist/esm/utilities/sampling-keys.js +31 -19
  196. package/dist/esm/utilities/sampling-keys.js.map +1 -1
  197. package/dist/esm/utilities/tool-result.js +42 -30
  198. package/dist/esm/utilities/tool-result.js.map +1 -1
  199. package/dist/esm/utilities/usage.js +27 -9
  200. package/dist/esm/utilities/usage.js.map +1 -1
  201. package/dist/esm/utils.js +26 -18
  202. package/dist/esm/utils.js.map +1 -1
  203. package/package.json +10 -6
  204. package/skills/ai-core/SKILL.md +69 -18
  205. package/skills/ai-core/adapter-configuration/SKILL.md +44 -21
  206. package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +1 -3
  207. package/skills/ai-core/adapter-configuration/references/byteplus-adapter.md +148 -0
  208. package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +2 -6
  209. package/skills/ai-core/adapter-configuration/references/groq-adapter.md +2 -6
  210. package/skills/ai-core/adapter-configuration/references/openai-adapter.md +1 -3
  211. package/skills/ai-core/ag-ui-protocol/SKILL.md +1 -1
  212. package/skills/ai-core/chat-experience/SKILL.md +98 -11
  213. package/skills/ai-core/client-persistence/SKILL.md +277 -0
  214. package/skills/ai-core/custom-backend-integration/SKILL.md +1 -1
  215. package/skills/ai-core/debug-logging/SKILL.md +1 -1
  216. package/skills/ai-core/locks/SKILL.md +143 -0
  217. package/skills/ai-core/media-generation/SKILL.md +144 -12
  218. package/skills/ai-core/middleware/SKILL.md +258 -33
  219. package/skills/ai-core/structured-outputs/SKILL.md +1 -1
  220. package/skills/ai-core/tool-calling/SKILL.md +54 -61
  221. package/src/activities/chat/agent-loop-strategies.ts +5 -39
  222. package/src/activities/chat/cancel.ts +81 -0
  223. package/src/activities/chat/index.ts +1091 -200
  224. package/src/activities/chat/mcp/manager.ts +4 -4
  225. package/src/activities/chat/mcp/types.ts +2 -2
  226. package/src/activities/chat/messages.ts +5 -3
  227. package/src/activities/chat/middleware/builder.ts +1 -1
  228. package/src/activities/chat/middleware/compose.ts +186 -9
  229. package/src/activities/chat/middleware/index.ts +26 -0
  230. package/src/activities/chat/middleware/locks.ts +102 -0
  231. package/src/activities/chat/middleware/pending-turn.ts +47 -0
  232. package/src/activities/chat/middleware/run-disconnect.ts +62 -0
  233. package/src/activities/chat/middleware/run-store.ts +412 -0
  234. package/src/activities/chat/middleware/types.ts +62 -1
  235. package/src/activities/chat/stream/processor.ts +189 -5
  236. package/src/activities/chat/tools/approval-schema.ts +205 -0
  237. package/src/activities/chat/tools/tool-calls.ts +106 -13
  238. package/src/activities/chat/tools/tool-definition.ts +210 -39
  239. package/src/activities/generateAudio/index.ts +20 -3
  240. package/src/activities/generateImage/index.ts +20 -3
  241. package/src/activities/generateSpeech/index.ts +25 -3
  242. package/src/activities/generateTranscription/index.ts +26 -3
  243. package/src/activities/generateVideo/index.ts +345 -82
  244. package/src/activities/middleware/index.ts +2 -0
  245. package/src/activities/middleware/run.ts +31 -0
  246. package/src/activities/middleware/types.ts +49 -5
  247. package/src/activities/stream-generation-result.ts +30 -2
  248. package/src/activities/summarize/chat-stream-summarize.ts +5 -0
  249. package/src/activities/summarize/index.ts +200 -10
  250. package/src/adapter-internals.ts +10 -1
  251. package/src/client.ts +244 -0
  252. package/src/custom-events.ts +107 -0
  253. package/src/delivery-detach.ts +72 -0
  254. package/src/delivery-disconnect.ts +84 -0
  255. package/src/index.ts +138 -1
  256. package/src/interrupt-resume.ts +824 -0
  257. package/src/interrupt-serialization.ts +183 -0
  258. package/src/interrupts.ts +146 -0
  259. package/src/locks.ts +17 -0
  260. package/src/logger/types.ts +1 -1
  261. package/src/middlewares/otel.ts +1 -0
  262. package/src/realtime/index.ts +5 -9
  263. package/src/scope.ts +47 -0
  264. package/src/stream-durability.ts +598 -0
  265. package/src/stream-to-response.ts +1051 -95
  266. package/src/strip-to-spec-middleware.ts +3 -3
  267. package/src/types.ts +405 -45
  268. package/src/utilities/chat-params.ts +245 -55
  269. package/dist/esm/activities/index.js.map +0 -1
  270. package/dist/esm/adapter-internals.js.map +0 -1
  271. package/dist/esm/index.js.map +0 -1
  272. package/dist/esm/middlewares/index.js.map +0 -1
@@ -1,1842 +1,2129 @@
1
- import { devtoolsMiddleware } from "@tanstack/ai-event-client";
2
- import { undoNullWidening } from "@tanstack/ai-utils";
3
1
  import { stripToSpecMiddleware } from "../../strip-to-spec-middleware.js";
4
- import { streamToText } from "../../stream-to-response.js";
2
+ import { isCancelRequestedReason } from "./cancel.js";
3
+ import { CapabilityRegistry } from "./middleware/capabilities.js";
4
+ import { getRunDetached } from "./middleware/run-store.js";
5
+ import { publishRunDetachedSignal } from "../../delivery-detach.js";
6
+ import { publishRunDisconnectHandler } from "../../delivery-disconnect.js";
7
+ import { EventType } from "../../types.js";
5
8
  import { resolveDebugOption } from "../../logger/resolve.js";
9
+ import { streamToText } from "../../stream-to-response.js";
10
+ import { canonicalInterruptJson, digestInterruptJson } from "../../interrupt-serialization.js";
11
+ import "../../interrupts.js";
12
+ import { convertSchemaForStructuredOutput, convertSchemaToJsonSchema, isStandardSchema, parseWithStandardSchema } from "./tools/schema-converter.js";
13
+ import { hashSchemaInput, normalizeApprovalSchema } from "./tools/approval-schema.js";
14
+ import { INTERRUPT_BINDING_METADATA_KEY, InterruptResumeValidationError, readUnopenedInterruptBinding, validateInterruptResumeBatch } from "../../interrupt-resume.js";
6
15
  import { normalizeToolResult } from "../../utilities/tool-result.js";
7
16
  import { isProviderExecutedToolCall } from "../../utilities/provider-executed.js";
8
17
  import { LazyToolManager } from "./tools/lazy-tool-manager.js";
9
- import { ToolCallManager, MiddlewareAbortError, executeToolCalls } from "./tools/tool-calls.js";
10
- import { convertSchemaForStructuredOutput, isStandardSchema, parseWithStandardSchema, convertSchemaToJsonSchema } from "./tools/schema-converter.js";
18
+ import { MiddlewareAbortError, ToolCallManager, executeToolCalls } from "./tools/tool-calls.js";
11
19
  import { maxIterations } from "./agent-loop-strategies.js";
12
20
  import { convertMessagesToModelMessages, generateMessageId } from "./messages.js";
13
21
  import { MiddlewareRunner } from "./middleware/compose.js";
14
22
  import { provideSandboxRuntime } from "./middleware/sandbox-runtime.js";
15
- import { CapabilityRegistry } from "./middleware/capabilities.js";
23
+ import { provideRunDisconnect } from "./middleware/run-disconnect.js";
16
24
  import { validateCapabilities } from "./middleware/validate.js";
17
25
  import { MCPManager } from "./mcp/manager.js";
18
- import { EventType } from "@ag-ui/core";
19
- const kind = "text";
20
- function createChatOptions(options) {
21
- return options;
26
+ import "./adapter.js";
27
+ import { devtoolsMiddleware } from "@tanstack/ai-event-client";
28
+ import { undoNullWidening } from "@tanstack/ai-utils";
29
+ //#region src/activities/chat/index.ts
30
+ /**
31
+ * Text Activity
32
+ *
33
+ * Handles agentic text generation, one-shot text generation, and agentic structured output.
34
+ * This is a self-contained module with implementation, types, and JSDoc.
35
+ */
36
+ /** The adapter kind this activity handles */
37
+ var kind = "text";
38
+ var interruptBindingMetadataKey = INTERRUPT_BINDING_METADATA_KEY;
39
+ function isInterruptSubmissionError(value) {
40
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
41
+ if (!("scope" in value) || !("code" in value) || !("message" in value) || !("source" in value) || !("retryable" in value) || !("threadId" in value) || !("interruptedRunId" in value) || !("generation" in value) || typeof value.code !== "string" || typeof value.message !== "string" || typeof value.retryable !== "boolean" || typeof value.threadId !== "string" || typeof value.interruptedRunId !== "string" || typeof value.generation !== "number") return false;
42
+ if (value.scope === "item") return "interruptId" in value && typeof value.interruptId === "string" && (value.source === "client" || value.source === "server");
43
+ return value.scope === "batch" && "interruptIds" in value && Array.isArray(value.interruptIds) && value.interruptIds.every((id) => typeof id === "string") && (value.source === "client" || value.source === "server" || value.source === "transport");
22
44
  }
23
- function resolveMaxToolCallsPerTurn(cap) {
24
- if (cap == null) return void 0;
25
- if (!Number.isFinite(cap) || cap < 0) {
26
- throw new Error(
27
- `maxToolCallsPerTurn must be a non-negative finite number, got ${cap}`
28
- );
29
- }
30
- return Math.floor(cap);
45
+ function structuralInterruptFailure(error) {
46
+ if (!(error instanceof Error) || error.name !== "InterruptResumeValidationError" || !("errors" in error) || !Array.isArray(error.errors) || error.errors.length === 0 || !error.errors.every(isInterruptSubmissionError)) return;
47
+ return {
48
+ error,
49
+ errors: error.errors
50
+ };
31
51
  }
32
- function combineAbortSignals(a, b) {
33
- if (!a) return b;
34
- if (!b) return a;
35
- if (a.aborted) return a;
36
- if (b.aborted) return b;
37
- const controller = new AbortController();
38
- const onAbort = (source) => () => {
39
- controller.abort(source.reason);
40
- };
41
- a.addEventListener("abort", onAbort(a), { once: true });
42
- b.addEventListener("abort", onAbort(b), { once: true });
43
- return controller.signal;
52
+ function normalizePublicInterruptBinding(value, expectedInterruptId) {
53
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return;
54
+ const binding = Object.fromEntries(Object.entries(value));
55
+ if (binding.interruptId !== expectedInterruptId || binding.v !== void 0 && binding.v !== 1 || typeof binding.interruptedRunId !== "string" || typeof binding.generation !== "number" || !Number.isInteger(binding.generation) || binding.generation < 0 || typeof binding.responseSchemaHash !== "string" || binding.expiresAt !== void 0 && typeof binding.expiresAt !== "string") return;
56
+ const base = {
57
+ v: 1,
58
+ interruptId: binding.interruptId,
59
+ interruptedRunId: binding.interruptedRunId,
60
+ generation: binding.generation,
61
+ responseSchemaHash: binding.responseSchemaHash,
62
+ ...typeof binding.expiresAt === "string" ? { expiresAt: binding.expiresAt } : {}
63
+ };
64
+ if (binding.kind === "generic") return {
65
+ kind: binding.kind,
66
+ ...base
67
+ };
68
+ if (typeof binding.toolName !== "string" || typeof binding.toolCallId !== "string") return;
69
+ if (binding.kind === "client-tool-execution" && typeof binding.outputSchemaHash === "string") return {
70
+ kind: binding.kind,
71
+ ...base,
72
+ toolName: binding.toolName,
73
+ toolCallId: binding.toolCallId,
74
+ outputSchemaHash: binding.outputSchemaHash
75
+ };
76
+ if (binding.kind === "tool-approval" && Object.prototype.hasOwnProperty.call(binding, "originalArgs") && typeof binding.inputSchemaHash === "string" && typeof binding.approvalSchemaHash === "string") return {
77
+ kind: binding.kind,
78
+ ...base,
79
+ toolName: binding.toolName,
80
+ toolCallId: binding.toolCallId,
81
+ originalArgs: binding.originalArgs,
82
+ inputSchemaHash: binding.inputSchemaHash,
83
+ approvalSchemaHash: binding.approvalSchemaHash
84
+ };
85
+ }
86
+ /**
87
+ * Create typed options for the chat() function without executing.
88
+ * This is useful for pre-defining configurations with full type inference.
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * const chatOptions = createChatOptions({
93
+ * adapter: anthropicText('claude-sonnet-4-5'),
94
+ * })
95
+ *
96
+ * const stream = chat({ ...chatOptions, messages })
97
+ * ```
98
+ */
99
+ function createChatOptions(options) {
100
+ return options;
44
101
  }
45
- class TextEngine {
46
- adapter;
47
- params;
48
- systemPrompts;
49
- tools;
50
- loopStrategy;
51
- toolCallManager;
52
- lazyToolManager;
53
- initialMessageCount;
54
- requestId;
55
- streamId;
56
- effectiveRequest;
57
- effectiveSignal;
58
- messages;
59
- iterationCount = 0;
60
- /** Cumulative tool calls counted in this run (emitted + pending resume). */
61
- toolCallCount = 0;
62
- /** Tool calls in the most recent budgeted batch (0 when none). */
63
- lastTurnToolCallCount = 0;
64
- /** Tool call IDs already counted toward `toolCallCount` (avoids double-count on resume). */
65
- countedToolCallIds = /* @__PURE__ */ new Set();
66
- lastFinishReason = null;
67
- streamStartTime = 0;
68
- totalChunkCount = 0;
69
- currentMessageId = null;
70
- accumulatedContent = "";
71
- accumulatedThinking = [];
72
- currentThinkingContent = "";
73
- currentThinkingSignature = "";
74
- eventOptions;
75
- eventToolNames;
76
- finishedEvent = null;
77
- earlyTermination = false;
78
- toolPhase = "continue";
79
- cyclePhase = "processText";
80
- maxToolCallsPerTurn;
81
- // Client state extracted from initial messages (before conversion to ModelMessage)
82
- initialApprovals;
83
- initialClientToolResults;
84
- // AG-UI protocol IDs
85
- threadId;
86
- runIdOverride;
87
- parentRunIdOverride;
88
- // Middleware support
89
- middlewareRunner;
90
- middlewareCtx;
91
- sandboxFileQueue = [];
92
- deferredPromises = [];
93
- abortReason;
94
- middlewareAbortController;
95
- // Combines the caller's signal with middleware abort() so running tools
96
- // observe both cancellation sources via ctx.abortSignal.
97
- toolAbortSignal;
98
- terminalHookCalled = false;
99
- logger;
100
- // Structured-output finalization state (populated by runStructuredFinalization)
101
- structuredOutputResult = null;
102
- // Native combined mode: tracks whether we've already emitted the synthetic
103
- // `structured-output.start` event before the schema-constrained final-turn
104
- // text begins streaming. The event must precede the first
105
- // TEXT_MESSAGE_START so the client-side StreamProcessor routes the JSON
106
- // deltas into a StructuredOutputPart instead of a plain TextPart.
107
- combinedStartEmitted = false;
108
- // Native combined mode: messageId we want the synthetic
109
- // `structured-output.start` (and any error emitted before deltas arrive)
110
- // to carry, so the client matches it to the streaming text deltas.
111
- combinedStructuredMessageId = null;
112
- // Holds the validated value when `finalStructuredOutput.validate` is provided
113
- // and succeeds. Distinct from `structuredOutputResult.data` (the normalized
114
- // but unvalidated payload from the structured-output.complete chunk).
115
- validatedStructuredOutput = void 0;
116
- hasValidatedStructuredOutput = false;
117
- finalizationError = null;
118
- finalStructuredOutput;
119
- constructor(config, logger) {
120
- this.logger = logger;
121
- this.adapter = config.adapter;
122
- this.finalStructuredOutput = config.finalStructuredOutput;
123
- this.params = config.params;
124
- this.systemPrompts = config.params.systemPrompts || [];
125
- this.loopStrategy = config.params.agentLoopStrategy || maxIterations(5);
126
- this.maxToolCallsPerTurn = resolveMaxToolCallsPerTurn(
127
- config.params.maxToolCallsPerTurn
128
- );
129
- this.initialMessageCount = config.params.messages.length;
130
- const { approvals, clientToolResults } = this.extractClientStateFromOriginalMessages(
131
- config.params.messages
132
- );
133
- this.initialApprovals = approvals;
134
- this.initialClientToolResults = clientToolResults;
135
- this.messages = convertMessagesToModelMessages(config.params.messages);
136
- this.lazyToolManager = new LazyToolManager(
137
- config.params.tools || [],
138
- this.messages,
139
- config.params.lazyToolsConfig
140
- );
141
- this.tools = this.lazyToolManager.getActiveTools();
142
- this.toolCallManager = new ToolCallManager(this.tools);
143
- this.requestId = this.createId("chat");
144
- this.streamId = this.createId("stream");
145
- this.effectiveRequest = config.params.abortController ? { signal: config.params.abortController.signal } : void 0;
146
- this.effectiveSignal = config.params.abortController?.signal;
147
- this.threadId = config.params.threadId || config.params.conversationId || this.createId("thread");
148
- this.runIdOverride = config.params.runId;
149
- this.parentRunIdOverride = config.params.parentRunId;
150
- const allMiddleware = [
151
- devtoolsMiddleware(),
152
- ...config.middleware || [],
153
- stripToSpecMiddleware()
154
- ];
155
- this.middlewareRunner = new MiddlewareRunner(allMiddleware, logger);
156
- this.middlewareAbortController = new AbortController();
157
- this.toolAbortSignal = combineAbortSignals(
158
- this.effectiveSignal,
159
- this.middlewareAbortController.signal
160
- );
161
- this.middlewareCtx = {
162
- requestId: this.requestId,
163
- streamId: this.streamId,
164
- runId: this.runIdOverride ?? this.requestId,
165
- threadId: this.threadId,
166
- // Legacy alias kept on the ctx so middleware that reads
167
- // `ctx.conversationId` keeps working. Always equals `threadId`.
168
- conversationId: this.threadId,
169
- phase: "init",
170
- iteration: 0,
171
- chunkIndex: 0,
172
- signal: this.effectiveSignal,
173
- abort: (reason) => {
174
- this.abortReason = reason;
175
- this.middlewareAbortController?.abort(reason);
176
- },
177
- context: config.context,
178
- defer: (promise) => {
179
- this.deferredPromises.push(promise);
180
- },
181
- // Provider / adapter info
182
- activity: "chat",
183
- provider: config.adapter.name,
184
- model: config.params.model,
185
- source: "server",
186
- streaming: true,
187
- // Config-derived (updated in beforeRun and applyMiddlewareConfig)
188
- systemPrompts: this.systemPrompts,
189
- toolNames: void 0,
190
- options: void 0,
191
- modelOptions: config.params.modelOptions,
192
- // Computed
193
- messageCount: this.initialMessageCount,
194
- hasTools: this.tools.length > 0,
195
- // Mutable per-iteration
196
- currentMessageId: null,
197
- accumulatedContent: "",
198
- // References
199
- messages: this.messages,
200
- createId: (prefix) => this.createId(prefix),
201
- // Capability bookkeeping for this request (populated by middleware setup)
202
- capabilities: new CapabilityRegistry(),
203
- // Convenience accessors that delegate to a capability handle's own
204
- // tuple getter/provider, keyed by this context. `getX(ctx)` and
205
- // `ctx.get(X)` are interchangeable.
206
- get: (capability) => capability[0](this.middlewareCtx),
207
- getOptional: (capability) => capability[0](this.middlewareCtx, { optional: true }),
208
- provide: (capability, value) => capability[1](this.middlewareCtx, value)
209
- };
210
- provideSandboxRuntime(this.middlewareCtx, {
211
- logger: this.logger,
212
- emit: (event) => {
213
- this.logger.sandbox(`file ${event.type} ${event.path}`, {
214
- event: {
215
- type: event.type,
216
- path: event.path,
217
- timestamp: event.timestamp
218
- }
219
- });
220
- void this.middlewareRunner.runSandboxFile(this.middlewareCtx, event).catch((err) => {
221
- this.logger.errors("sandbox file hook failed", { error: err });
222
- });
223
- this.sandboxFileQueue.push(
224
- this.createCustomEventChunk("sandbox.file", {
225
- type: event.type,
226
- path: event.path,
227
- timestamp: event.timestamp
228
- })
229
- );
230
- },
231
- emitFileDiff: (value) => {
232
- this.sandboxFileQueue.push(
233
- this.createCustomEventChunk("sandbox.file.diff", value)
234
- );
235
- }
236
- });
237
- }
238
- /** Get the accumulated content after the chat loop completes */
239
- getAccumulatedContent() {
240
- return this.accumulatedContent;
241
- }
242
- /** Get the final messages array after the chat loop completes */
243
- getMessages() {
244
- return this.messages;
245
- }
246
- /** Returns the structured-output result if finalization ran successfully. */
247
- getStructuredOutputResult() {
248
- return this.structuredOutputResult;
249
- }
250
- /**
251
- * Returns the validated structured-output value (the result of running
252
- * `finalStructuredOutput.validate` against the raw structured-output data)
253
- * wrapped in a `{ value }` object so callers can distinguish "no validation
254
- * happened" from "validation produced undefined". Returns `null` when no
255
- * validator was configured or validation hasn't been performed yet.
256
- */
257
- getValidatedStructuredOutput() {
258
- return this.hasValidatedStructuredOutput ? { value: this.validatedStructuredOutput } : null;
259
- }
260
- /** Returns the recorded finalization error, if any. */
261
- getFinalizationError() {
262
- return this.finalizationError;
263
- }
264
- async *run() {
265
- this.beforeRun();
266
- this.logger.agentLoop("run started", {
267
- threadId: this.middlewareCtx.threadId
268
- });
269
- try {
270
- await this.middlewareRunner.runSetup(this.middlewareCtx);
271
- this.middlewareCtx.phase = "init";
272
- const initialConfig = this.buildMiddlewareConfig();
273
- const transformedConfig = await this.middlewareRunner.runOnConfig(
274
- this.middlewareCtx,
275
- initialConfig
276
- );
277
- this.applyMiddlewareConfig(transformedConfig);
278
- await this.middlewareRunner.runOnStart(this.middlewareCtx);
279
- const pendingPhase = yield* this.checkForPendingToolCalls();
280
- if (pendingPhase === "wait") {
281
- return;
282
- }
283
- const skipAgentLoop = !!this.finalStructuredOutput && this.tools.length === 0 && this.finalStructuredOutput.nativeCombined !== true;
284
- if (!skipAgentLoop) {
285
- do {
286
- if (this.earlyTermination || this.isCancelled()) {
287
- return;
288
- }
289
- this.logger.agentLoop(`iteration=${this.middlewareCtx.iteration}`, {
290
- iteration: this.middlewareCtx.iteration
291
- });
292
- await this.beginCycle();
293
- if (this.cyclePhase === "processText") {
294
- this.middlewareCtx.phase = "beforeModel";
295
- this.middlewareCtx.iteration = this.iterationCount;
296
- const iterConfig = this.buildMiddlewareConfig();
297
- const iterTransformedConfig = await this.middlewareRunner.runOnConfig(
298
- this.middlewareCtx,
299
- iterConfig
300
- );
301
- this.applyMiddlewareConfig(iterTransformedConfig);
302
- yield* this.streamModelResponse();
303
- } else {
304
- yield* this.processToolCalls();
305
- }
306
- this.endCycle();
307
- } while (this.shouldContinue());
308
- }
309
- this.logger.agentLoop("run finished", {
310
- finishReason: this.lastFinishReason
311
- });
312
- if (this.finalStructuredOutput && !this.isCancelled() && !this.finalizationError) {
313
- if (this.finalStructuredOutput.nativeCombined === true) {
314
- yield* this.harvestCombinedStructuredOutput();
315
- } else {
316
- yield* this.runStructuredFinalization();
317
- }
318
- }
319
- if (!this.terminalHookCalled && this.toolPhase !== "wait" && !this.isCancelled()) {
320
- if (this.finalizationError) {
321
- this.terminalHookCalled = true;
322
- const errForHook = new Error(
323
- this.finalizationError.message,
324
- this.finalizationError.cause !== void 0 ? { cause: this.finalizationError.cause } : void 0
325
- );
326
- if (this.finalizationError.code !== void 0) {
327
- Object.defineProperty(errForHook, "code", {
328
- value: this.finalizationError.code,
329
- enumerable: true
330
- });
331
- }
332
- await this.middlewareRunner.runOnError(this.middlewareCtx, {
333
- error: errForHook,
334
- duration: Date.now() - this.streamStartTime
335
- });
336
- } else {
337
- this.terminalHookCalled = true;
338
- await this.middlewareRunner.runOnFinish(this.middlewareCtx, {
339
- finishReason: this.lastFinishReason,
340
- duration: Date.now() - this.streamStartTime,
341
- content: this.accumulatedContent,
342
- usage: this.finishedEvent?.usage
343
- });
344
- }
345
- }
346
- } catch (error) {
347
- if (!this.terminalHookCalled) {
348
- this.terminalHookCalled = true;
349
- if (error instanceof MiddlewareAbortError) {
350
- this.abortReason = error.message;
351
- await this.middlewareRunner.runOnAbort(this.middlewareCtx, {
352
- reason: error.message,
353
- duration: Date.now() - this.streamStartTime
354
- });
355
- } else {
356
- this.logger.errors("chat run failed", {
357
- error,
358
- threadId: this.middlewareCtx.threadId
359
- });
360
- await this.middlewareRunner.runOnError(this.middlewareCtx, {
361
- error,
362
- duration: Date.now() - this.streamStartTime
363
- });
364
- }
365
- }
366
- if (!(error instanceof MiddlewareAbortError)) {
367
- throw error;
368
- }
369
- } finally {
370
- if (!this.terminalHookCalled && this.isCancelled()) {
371
- this.terminalHookCalled = true;
372
- await this.middlewareRunner.runOnAbort(this.middlewareCtx, {
373
- reason: this.abortReason,
374
- duration: Date.now() - this.streamStartTime
375
- });
376
- }
377
- if (this.deferredPromises.length > 0) {
378
- await Promise.allSettled(this.deferredPromises);
379
- }
380
- }
381
- }
382
- beforeRun() {
383
- this.streamStartTime = Date.now();
384
- const { tools, metadata } = this.params;
385
- const options = {};
386
- if (metadata !== void 0) options.metadata = metadata;
387
- this.eventOptions = Object.keys(options).length > 0 ? options : void 0;
388
- this.eventToolNames = tools?.map((t) => t.name);
389
- this.middlewareCtx.options = this.eventOptions;
390
- this.middlewareCtx.toolNames = this.eventToolNames;
391
- }
392
- async beginCycle() {
393
- if (this.cyclePhase === "processText") {
394
- await this.beginIteration();
395
- }
396
- }
397
- endCycle() {
398
- if (this.cyclePhase === "processText") {
399
- this.cyclePhase = "executeToolCalls";
400
- return;
401
- }
402
- this.cyclePhase = "processText";
403
- this.iterationCount++;
404
- }
405
- async beginIteration() {
406
- this.currentMessageId = this.createId("msg");
407
- this.accumulatedContent = "";
408
- this.accumulatedThinking = [];
409
- this.currentThinkingContent = "";
410
- this.currentThinkingSignature = "";
411
- this.finishedEvent = null;
412
- this.middlewareCtx.currentMessageId = this.currentMessageId;
413
- this.middlewareCtx.accumulatedContent = "";
414
- await this.middlewareRunner.runOnIteration(this.middlewareCtx, {
415
- iteration: this.iterationCount,
416
- messageId: this.currentMessageId
417
- });
418
- }
419
- async *streamModelResponse() {
420
- const { metadata, modelOptions } = this.params;
421
- const tools = this.tools;
422
- const toolsWithJsonSchemas = tools.map((tool) => ({
423
- ...tool,
424
- inputSchema: tool.inputSchema ? convertSchemaToJsonSchema(tool.inputSchema) : void 0,
425
- outputSchema: tool.outputSchema ? convertSchemaToJsonSchema(tool.outputSchema) : void 0
426
- }));
427
- this.middlewareCtx.phase = "modelStream";
428
- const providerName = this.adapter.provider ?? this.adapter.name;
429
- this.logger.request(
430
- `activity=chat provider=${providerName} model=${this.params.model} messages=${this.messages.length} tools=${this.tools.length} stream=true`,
431
- {
432
- provider: providerName,
433
- model: this.params.model,
434
- messageCount: this.messages.length,
435
- toolCount: this.tools.length
436
- }
437
- );
438
- const combinedSchema = this.finalStructuredOutput?.nativeCombined === true ? this.finalStructuredOutput.jsonSchema : void 0;
439
- for await (const chunk of this.adapter.chatStream({
440
- model: this.params.model,
441
- messages: this.messages,
442
- tools: toolsWithJsonSchemas,
443
- metadata,
444
- request: this.effectiveRequest,
445
- modelOptions,
446
- systemPrompts: this.systemPrompts,
447
- logger: this.logger,
448
- threadId: this.threadId,
449
- runId: this.runIdOverride,
450
- parentRunId: this.parentRunIdOverride,
451
- // Expose provided capabilities (e.g. sandbox) to harness adapters.
452
- capabilities: this.middlewareCtx,
453
- // Client approval decisions, for harness interactive-approval resolution.
454
- approvals: this.initialApprovals,
455
- ...combinedSchema ? { outputSchema: combinedSchema } : {}
456
- })) {
457
- if (this.isCancelled()) {
458
- break;
459
- }
460
- this.totalChunkCount++;
461
- this.handleStreamChunk(chunk);
462
- if (this.finalStructuredOutput?.nativeCombined === true && this.finalStructuredOutput.yieldChunks && !this.combinedStartEmitted && chunk.type === EventType.TEXT_MESSAGE_START) {
463
- this.combinedStartEmitted = true;
464
- const messageId = typeof chunk.messageId === "string" && chunk.messageId !== "" ? chunk.messageId : generateMessageId();
465
- this.combinedStructuredMessageId = messageId;
466
- const synthStart = {
467
- type: EventType.CUSTOM,
468
- name: "structured-output.start",
469
- value: { messageId },
470
- model: this.params.model,
471
- timestamp: Date.now(),
472
- threadId: this.threadId,
473
- ...this.runIdOverride ? { runId: this.runIdOverride } : {}
474
- };
475
- const synthOutputs = await this.middlewareRunner.runOnChunk(
476
- this.middlewareCtx,
477
- synthStart
478
- );
479
- for (const outputChunk of synthOutputs) {
480
- yield outputChunk;
481
- this.middlewareCtx.chunkIndex++;
482
- }
483
- }
484
- const outputChunks = await this.middlewareRunner.runOnChunk(
485
- this.middlewareCtx,
486
- chunk
487
- );
488
- const suppressAgentLifecycle = !!this.finalStructuredOutput && this.finalStructuredOutput.yieldChunks && this.finalStructuredOutput.nativeCombined !== true;
489
- for (const outputChunk of outputChunks) {
490
- if (suppressAgentLifecycle && (outputChunk.type === EventType.RUN_STARTED || outputChunk.type === EventType.RUN_FINISHED)) {
491
- continue;
492
- }
493
- this.logger.output(`type=${outputChunk.type}`, { chunk: outputChunk });
494
- yield outputChunk;
495
- this.middlewareCtx.chunkIndex++;
496
- }
497
- if (chunk.type === "RUN_FINISHED" && chunk.usage) {
498
- await this.middlewareRunner.runOnUsage(this.middlewareCtx, chunk.usage);
499
- }
500
- yield* this.drainSandboxFileQueue();
501
- if (this.earlyTermination) {
502
- break;
503
- }
504
- }
505
- yield* this.drainSandboxFileQueue();
506
- }
507
- handleStreamChunk(chunk) {
508
- switch (chunk.type) {
509
- // AG-UI Events
510
- case "TEXT_MESSAGE_CONTENT":
511
- this.handleTextMessageContentEvent(chunk);
512
- break;
513
- case "TOOL_CALL_START":
514
- this.handleToolCallStartEvent(chunk);
515
- break;
516
- case "TOOL_CALL_ARGS":
517
- this.handleToolCallArgsEvent(chunk);
518
- break;
519
- case "TOOL_CALL_END":
520
- this.handleToolCallEndEvent(chunk);
521
- break;
522
- case "RUN_FINISHED":
523
- this.handleRunFinishedEvent(chunk);
524
- break;
525
- case "RUN_ERROR":
526
- this.handleRunErrorEvent(chunk);
527
- break;
528
- case "STEP_STARTED":
529
- this.handleStepStartedEvent();
530
- break;
531
- case "STEP_FINISHED":
532
- this.handleStepFinishedEvent(chunk);
533
- break;
534
- }
535
- }
536
- // ===========================
537
- // AG-UI Event Handlers
538
- // ===========================
539
- handleTextMessageContentEvent(chunk) {
540
- if (chunk.content) {
541
- this.accumulatedContent = chunk.content;
542
- } else {
543
- this.accumulatedContent += chunk.delta;
544
- }
545
- this.middlewareCtx.accumulatedContent = this.accumulatedContent;
546
- }
547
- handleToolCallStartEvent(chunk) {
548
- this.toolCallManager.addToolCallStartEvent(chunk);
549
- }
550
- handleToolCallArgsEvent(chunk) {
551
- this.toolCallManager.addToolCallArgsEvent(chunk);
552
- }
553
- handleToolCallEndEvent(chunk) {
554
- this.toolCallManager.completeToolCall(chunk);
555
- }
556
- handleRunFinishedEvent(chunk) {
557
- this.finishedEvent = chunk;
558
- this.lastFinishReason = chunk.finishReason ?? null;
559
- }
560
- handleRunErrorEvent(_chunk) {
561
- this.earlyTermination = true;
562
- }
563
- finalizeCurrentThinkingStep() {
564
- if (this.currentThinkingContent) {
565
- this.accumulatedThinking.push({
566
- content: this.currentThinkingContent,
567
- ...this.currentThinkingSignature && {
568
- signature: this.currentThinkingSignature
569
- }
570
- });
571
- this.currentThinkingContent = "";
572
- this.currentThinkingSignature = "";
573
- }
574
- }
575
- handleStepStartedEvent() {
576
- this.finalizeCurrentThinkingStep();
577
- }
578
- handleStepFinishedEvent(chunk) {
579
- if (chunk.delta) {
580
- this.currentThinkingContent += chunk.delta;
581
- }
582
- if (chunk.signature) {
583
- this.currentThinkingSignature = chunk.signature;
584
- }
585
- }
586
- /**
587
- * Tools available for execution this turn. The discovery tool is dropped
588
- * from the advertised set (`this.tools`) once every lazy tool is discovered,
589
- * but a model may still re-request discovery; this widens execution lookup
590
- * to include it so such calls don't fail with "Unknown tool". Centralised so
591
- * both execution sites (`processToolCalls` and `checkForPendingToolCalls`)
592
- * stay in sync.
593
- */
594
- resolveExecutableTools(toolCalls) {
595
- return this.lazyToolManager.getExecutableTools(
596
- this.tools,
597
- toolCalls.map((tc) => tc.function.name)
598
- );
599
- }
600
- async *checkForPendingToolCalls() {
601
- const pendingToolCalls = this.getPendingToolCallsFromMessages();
602
- if (pendingToolCalls.length === 0) {
603
- return "continue";
604
- }
605
- const finishEvent = this.createSyntheticFinishedEvent();
606
- const { toExecute: budgetedToolCalls, skippedResults } = this.applyToolCallBudget(pendingToolCalls);
607
- const undiscoveredLazyResults = [];
608
- const executablePendingCalls = budgetedToolCalls.filter((tc) => {
609
- if (this.lazyToolManager.isUndiscoveredLazyTool(tc.function.name)) {
610
- undiscoveredLazyResults.push({
611
- toolCallId: tc.id,
612
- toolName: tc.function.name,
613
- result: {
614
- error: this.lazyToolManager.getUndiscoveredToolError(
615
- tc.function.name
616
- )
617
- },
618
- state: "output-error"
619
- });
620
- return false;
621
- }
622
- return true;
623
- });
624
- const deferredErrorResults = [...undiscoveredLazyResults, ...skippedResults];
625
- const argsMap = /* @__PURE__ */ new Map();
626
- for (const tc of pendingToolCalls) {
627
- argsMap.set(tc.id, tc.function.arguments);
628
- }
629
- if (executablePendingCalls.length === 0) {
630
- if (deferredErrorResults.length > 0) {
631
- for (const chunk of this.buildToolResultChunks(
632
- deferredErrorResults,
633
- finishEvent,
634
- argsMap
635
- )) {
636
- yield* this.pipeThroughMiddleware(chunk);
637
- }
638
- }
639
- return "continue";
640
- }
641
- const { approvals, clientToolResults } = this.collectClientState();
642
- const generator = executeToolCalls(
643
- executablePendingCalls,
644
- this.resolveExecutableTools(executablePendingCalls),
645
- approvals,
646
- clientToolResults,
647
- (eventName, data) => this.createCustomEventChunk(eventName, data),
648
- {
649
- onBeforeToolCall: async (toolCall, tool, args) => {
650
- this.logger.tools(`phase=before name=${toolCall.function.name}`, {
651
- name: toolCall.function.name,
652
- args
653
- });
654
- const hookCtx = {
655
- toolCall,
656
- tool,
657
- args,
658
- toolName: toolCall.function.name,
659
- toolCallId: toolCall.id
660
- };
661
- return this.middlewareRunner.runOnBeforeToolCall(
662
- this.middlewareCtx,
663
- hookCtx
664
- );
665
- },
666
- onAfterToolCall: async (info) => {
667
- this.logger.tools(`phase=after name=${info.toolName}`, {
668
- name: info.toolName,
669
- result: info.result
670
- });
671
- await this.middlewareRunner.runOnAfterToolCall(
672
- this.middlewareCtx,
673
- info
674
- );
675
- }
676
- },
677
- this.middlewareCtx.context,
678
- this.toolAbortSignal
679
- );
680
- const executionResult = yield* this.drainToolCallGenerator(generator);
681
- if (this.isMiddlewareAborted()) {
682
- this.setToolPhase("stop");
683
- return "stop";
684
- }
685
- const allResults = [...executionResult.results, ...deferredErrorResults];
686
- await this.middlewareRunner.runOnToolPhaseComplete(this.middlewareCtx, {
687
- toolCalls: pendingToolCalls,
688
- results: allResults,
689
- needsApproval: executionResult.needsApproval,
690
- needsClientExecution: executionResult.needsClientExecution
691
- });
692
- if (executionResult.needsApproval.length > 0 || executionResult.needsClientExecution.length > 0) {
693
- if (allResults.length > 0) {
694
- for (const chunk of this.buildToolResultChunks(
695
- allResults,
696
- finishEvent,
697
- argsMap
698
- )) {
699
- yield* this.pipeThroughMiddleware(chunk);
700
- }
701
- }
702
- for (const chunk of this.buildApprovalChunks(
703
- executionResult.needsApproval,
704
- finishEvent
705
- )) {
706
- yield* this.pipeThroughMiddleware(chunk);
707
- }
708
- for (const chunk of this.buildClientToolChunks(
709
- executionResult.needsClientExecution,
710
- finishEvent
711
- )) {
712
- yield* this.pipeThroughMiddleware(chunk);
713
- }
714
- this.setToolPhase("wait");
715
- return "wait";
716
- }
717
- const toolResultChunks = this.buildToolResultChunks(
718
- allResults,
719
- finishEvent,
720
- argsMap
721
- );
722
- for (const chunk of toolResultChunks) {
723
- yield* this.pipeThroughMiddleware(chunk);
724
- }
725
- return "continue";
726
- }
727
- async *processToolCalls() {
728
- if (!this.shouldExecuteToolPhase()) {
729
- this.lastTurnToolCallCount = 0;
730
- this.setToolPhase("stop");
731
- return;
732
- }
733
- const toolCalls = this.toolCallManager.getToolCalls();
734
- const finishEvent = this.finishedEvent;
735
- if (!finishEvent || toolCalls.length === 0) {
736
- this.lastTurnToolCallCount = 0;
737
- this.setToolPhase("stop");
738
- return;
739
- }
740
- const { toExecute: budgetedToolCalls, skippedResults } = this.applyToolCallBudget(toolCalls);
741
- this.addAssistantToolCallMessage(toolCalls);
742
- const undiscoveredLazyResults = [];
743
- const executableToolCalls = budgetedToolCalls.filter((tc) => {
744
- if (this.lazyToolManager.isUndiscoveredLazyTool(tc.function.name)) {
745
- undiscoveredLazyResults.push({
746
- toolCallId: tc.id,
747
- toolName: tc.function.name,
748
- result: {
749
- error: this.lazyToolManager.getUndiscoveredToolError(
750
- tc.function.name
751
- )
752
- },
753
- state: "output-error"
754
- });
755
- return false;
756
- }
757
- return true;
758
- });
759
- const deferredErrorResults = [...undiscoveredLazyResults, ...skippedResults];
760
- if (executableToolCalls.length === 0) {
761
- if (deferredErrorResults.length > 0) {
762
- for (const chunk of this.buildToolResultChunks(
763
- deferredErrorResults,
764
- finishEvent
765
- )) {
766
- yield* this.pipeThroughMiddleware(chunk);
767
- }
768
- }
769
- this.toolCallManager.clear();
770
- this.setToolPhase("continue");
771
- return;
772
- }
773
- this.middlewareCtx.phase = "beforeTools";
774
- const { approvals, clientToolResults } = this.collectClientState();
775
- const generator = executeToolCalls(
776
- executableToolCalls,
777
- this.resolveExecutableTools(executableToolCalls),
778
- approvals,
779
- clientToolResults,
780
- (eventName, data) => this.createCustomEventChunk(eventName, data),
781
- {
782
- onBeforeToolCall: async (toolCall, tool, args) => {
783
- this.logger.tools(`phase=before name=${toolCall.function.name}`, {
784
- name: toolCall.function.name,
785
- args
786
- });
787
- const hookCtx = {
788
- toolCall,
789
- tool,
790
- args,
791
- toolName: toolCall.function.name,
792
- toolCallId: toolCall.id
793
- };
794
- return this.middlewareRunner.runOnBeforeToolCall(
795
- this.middlewareCtx,
796
- hookCtx
797
- );
798
- },
799
- onAfterToolCall: async (info) => {
800
- this.logger.tools(`phase=after name=${info.toolName}`, {
801
- name: info.toolName,
802
- result: info.result
803
- });
804
- await this.middlewareRunner.runOnAfterToolCall(
805
- this.middlewareCtx,
806
- info
807
- );
808
- }
809
- },
810
- this.middlewareCtx.context,
811
- this.toolAbortSignal
812
- );
813
- const executionResult = yield* this.drainToolCallGenerator(generator);
814
- this.middlewareCtx.phase = "afterTools";
815
- if (this.isMiddlewareAborted()) {
816
- this.setToolPhase("stop");
817
- return;
818
- }
819
- const allResults = [...executionResult.results, ...deferredErrorResults];
820
- await this.middlewareRunner.runOnToolPhaseComplete(this.middlewareCtx, {
821
- toolCalls,
822
- results: allResults,
823
- needsApproval: executionResult.needsApproval,
824
- needsClientExecution: executionResult.needsClientExecution
825
- });
826
- if (executionResult.needsApproval.length > 0 || executionResult.needsClientExecution.length > 0) {
827
- if (allResults.length > 0) {
828
- for (const chunk of this.buildToolResultChunks(
829
- allResults,
830
- finishEvent
831
- )) {
832
- yield* this.pipeThroughMiddleware(chunk);
833
- }
834
- }
835
- for (const chunk of this.buildApprovalChunks(
836
- executionResult.needsApproval,
837
- finishEvent
838
- )) {
839
- yield* this.pipeThroughMiddleware(chunk);
840
- }
841
- for (const chunk of this.buildClientToolChunks(
842
- executionResult.needsClientExecution,
843
- finishEvent
844
- )) {
845
- yield* this.pipeThroughMiddleware(chunk);
846
- }
847
- this.setToolPhase("wait");
848
- return;
849
- }
850
- const toolResultChunks = this.buildToolResultChunks(allResults, finishEvent);
851
- for (const chunk of toolResultChunks) {
852
- yield* this.pipeThroughMiddleware(chunk);
853
- }
854
- if (this.lazyToolManager.hasNewlyDiscoveredTools()) {
855
- this.tools = this.lazyToolManager.getActiveTools();
856
- this.toolCallManager = new ToolCallManager(this.tools);
857
- this.setToolPhase("continue");
858
- return;
859
- }
860
- this.toolCallManager.clear();
861
- this.setToolPhase("continue");
862
- }
863
- shouldExecuteToolPhase() {
864
- return this.finishedEvent?.finishReason === "tool_calls" && this.tools.length > 0 && this.toolCallManager.hasToolCalls();
865
- }
866
- addAssistantToolCallMessage(toolCalls) {
867
- this.finalizeCurrentThinkingStep();
868
- this.messages = [
869
- ...this.messages,
870
- {
871
- role: "assistant",
872
- content: this.accumulatedContent || null,
873
- toolCalls,
874
- ...this.accumulatedThinking.length > 0 && {
875
- thinking: this.accumulatedThinking
876
- }
877
- }
878
- ];
879
- }
880
- /**
881
- * Extract client state (approvals and client tool results) from original messages.
882
- * This is called in the constructor BEFORE converting to ModelMessage format,
883
- * because the parts array (which contains approval state) is lost during conversion.
884
- */
885
- extractClientStateFromOriginalMessages(originalMessages) {
886
- const approvals = /* @__PURE__ */ new Map();
887
- const clientToolResults = /* @__PURE__ */ new Map();
888
- for (const message of originalMessages) {
889
- if (message.role === "assistant" && message.parts) {
890
- for (const part of message.parts) {
891
- if (part.type === "tool-call") {
892
- if (part.output !== void 0 && !part.approval) {
893
- clientToolResults.set(part.id, part.output);
894
- }
895
- if (part.approval?.id && part.approval?.approved !== void 0 && part.state === "approval-responded") {
896
- approvals.set(part.approval.id, part.approval.approved);
897
- }
898
- }
899
- }
900
- }
901
- }
902
- return { approvals, clientToolResults };
903
- }
904
- collectClientState() {
905
- const approvals = new Map(this.initialApprovals);
906
- const clientToolResults = new Map(this.initialClientToolResults);
907
- for (const message of this.messages) {
908
- if (message.role === "tool" && message.toolCallId) {
909
- let output;
910
- if (Array.isArray(message.content)) {
911
- output = message.content;
912
- } else {
913
- try {
914
- output = JSON.parse(message.content);
915
- } catch {
916
- output = message.content;
917
- }
918
- }
919
- if (output && typeof output === "object" && output.pendingExecution === true) {
920
- continue;
921
- }
922
- clientToolResults.set(message.toolCallId, output);
923
- }
924
- }
925
- return { approvals, clientToolResults };
926
- }
927
- buildApprovalChunks(approvals, finishEvent) {
928
- const chunks = [];
929
- for (const approval of approvals) {
930
- chunks.push({
931
- type: "CUSTOM",
932
- timestamp: Date.now(),
933
- model: finishEvent.model,
934
- name: "approval-requested",
935
- value: {
936
- toolCallId: approval.toolCallId,
937
- toolName: approval.toolName,
938
- input: approval.input,
939
- approval: {
940
- id: approval.approvalId,
941
- needsApproval: true
942
- }
943
- }
944
- });
945
- }
946
- return chunks;
947
- }
948
- buildClientToolChunks(clientRequests, finishEvent) {
949
- const chunks = [];
950
- for (const clientTool of clientRequests) {
951
- chunks.push({
952
- type: "CUSTOM",
953
- timestamp: Date.now(),
954
- model: finishEvent.model,
955
- name: "tool-input-available",
956
- value: {
957
- toolCallId: clientTool.toolCallId,
958
- toolName: clientTool.toolName,
959
- input: clientTool.input
960
- }
961
- });
962
- }
963
- return chunks;
964
- }
965
- buildToolResultChunks(results, finishEvent, argsMap) {
966
- const chunks = [];
967
- for (const result of results) {
968
- const content = normalizeToolResult(result.result);
969
- const wireContent = typeof content === "string" ? content : JSON.stringify(content);
970
- if (argsMap) {
971
- chunks.push({
972
- type: "TOOL_CALL_START",
973
- timestamp: Date.now(),
974
- model: finishEvent.model,
975
- toolCallId: result.toolCallId,
976
- toolCallName: result.toolName,
977
- toolName: result.toolName
978
- });
979
- const args = argsMap.get(result.toolCallId) ?? "{}";
980
- chunks.push({
981
- type: "TOOL_CALL_ARGS",
982
- timestamp: Date.now(),
983
- model: finishEvent.model,
984
- toolCallId: result.toolCallId,
985
- delta: args,
986
- args
987
- });
988
- chunks.push({
989
- type: "TOOL_CALL_END",
990
- timestamp: Date.now(),
991
- model: finishEvent.model,
992
- toolCallId: result.toolCallId,
993
- toolCallName: result.toolName,
994
- toolName: result.toolName,
995
- result: wireContent,
996
- ...result.state !== void 0 && { state: result.state }
997
- });
998
- }
999
- chunks.push({
1000
- type: "TOOL_CALL_RESULT",
1001
- timestamp: Date.now(),
1002
- model: finishEvent.model,
1003
- messageId: this.createId("tool-result"),
1004
- toolCallId: result.toolCallId,
1005
- content: wireContent,
1006
- role: "tool",
1007
- ...result.state !== void 0 && { state: result.state }
1008
- });
1009
- const placeholderIdx = this.messages.findIndex((m) => {
1010
- if (m.role !== "tool" || m.toolCallId !== result.toolCallId) {
1011
- return false;
1012
- }
1013
- if (typeof m.content !== "string") return false;
1014
- try {
1015
- return JSON.parse(m.content)?.pendingExecution === true;
1016
- } catch {
1017
- return false;
1018
- }
1019
- });
1020
- const newToolMessage = {
1021
- role: "tool",
1022
- content,
1023
- toolCallId: result.toolCallId
1024
- };
1025
- if (placeholderIdx >= 0) {
1026
- this.messages = [
1027
- ...this.messages.slice(0, placeholderIdx),
1028
- newToolMessage,
1029
- ...this.messages.slice(placeholderIdx + 1)
1030
- ];
1031
- } else {
1032
- this.messages = [...this.messages, newToolMessage];
1033
- }
1034
- }
1035
- return chunks;
1036
- }
1037
- getPendingToolCallsFromMessages() {
1038
- const completedToolIds = /* @__PURE__ */ new Set();
1039
- for (const message of this.messages) {
1040
- if (message.role === "tool" && message.toolCallId) {
1041
- let hasPendingExecution = false;
1042
- if (typeof message.content === "string") {
1043
- try {
1044
- const parsed = JSON.parse(message.content);
1045
- if (parsed.pendingExecution === true) {
1046
- hasPendingExecution = true;
1047
- }
1048
- } catch {
1049
- }
1050
- }
1051
- if (!hasPendingExecution) {
1052
- completedToolIds.add(message.toolCallId);
1053
- }
1054
- }
1055
- }
1056
- const pending = [];
1057
- for (const message of this.messages) {
1058
- if (message.role === "assistant" && message.toolCalls) {
1059
- for (const toolCall of message.toolCalls) {
1060
- if (isProviderExecutedToolCall(toolCall)) {
1061
- continue;
1062
- }
1063
- if (!completedToolIds.has(toolCall.id)) {
1064
- pending.push(toolCall);
1065
- }
1066
- }
1067
- }
1068
- }
1069
- return pending;
1070
- }
1071
- createSyntheticFinishedEvent() {
1072
- return {
1073
- type: "RUN_FINISHED",
1074
- runId: this.createId("pending"),
1075
- threadId: this.threadId,
1076
- model: this.params.model,
1077
- timestamp: Date.now(),
1078
- finishReason: "tool_calls"
1079
- };
1080
- }
1081
- shouldContinue() {
1082
- if (this.cyclePhase === "executeToolCalls") {
1083
- return true;
1084
- }
1085
- return this.loopStrategy({
1086
- iterationCount: this.iterationCount,
1087
- messages: this.messages,
1088
- finishReason: this.lastFinishReason,
1089
- toolCallCount: this.toolCallCount,
1090
- lastTurnToolCallCount: this.lastTurnToolCallCount
1091
- }) && this.toolPhase === "continue";
1092
- }
1093
- /**
1094
- * Record tool calls (deduped by id) and return the subset that should be
1095
- * executed after applying `maxToolCallsPerTurn`. Excess calls get synthetic
1096
- * error results so every tool_call still has a matching result.
1097
- *
1098
- * Used for both live model turns and pending/resume batches. IDs already
1099
- * counted in this run (e.g. wait→resume after a live turn) are not
1100
- * re-added to `toolCallCount`.
1101
- */
1102
- applyToolCallBudget(toolCalls) {
1103
- this.lastTurnToolCallCount = toolCalls.length;
1104
- let newlyCounted = 0;
1105
- for (const tc of toolCalls) {
1106
- if (!this.countedToolCallIds.has(tc.id)) {
1107
- this.countedToolCallIds.add(tc.id);
1108
- newlyCounted++;
1109
- }
1110
- }
1111
- this.toolCallCount += newlyCounted;
1112
- const cap = this.maxToolCallsPerTurn;
1113
- if (cap == null || toolCalls.length <= cap) {
1114
- return { toExecute: toolCalls, skippedResults: [] };
1115
- }
1116
- this.logger.agentLoop(
1117
- `maxToolCallsPerTurn=${cap} skipped=${toolCalls.length - cap}`,
1118
- {
1119
- maxToolCallsPerTurn: cap,
1120
- emitted: toolCalls.length,
1121
- skipped: toolCalls.length - cap
1122
- }
1123
- );
1124
- const toExecute = toolCalls.slice(0, cap);
1125
- const skippedResults = toolCalls.slice(cap).map((tc) => ({
1126
- toolCallId: tc.id,
1127
- toolName: tc.function.name,
1128
- result: {
1129
- error: `Skipped: exceeded maxToolCallsPerTurn (${cap})`
1130
- },
1131
- state: "output-error"
1132
- }));
1133
- return { toExecute, skippedResults };
1134
- }
1135
- isAborted() {
1136
- return !!this.effectiveSignal?.aborted;
1137
- }
1138
- isMiddlewareAborted() {
1139
- return !!this.middlewareAbortController?.signal.aborted;
1140
- }
1141
- isCancelled() {
1142
- return this.isAborted() || this.isMiddlewareAborted();
1143
- }
1144
- /**
1145
- * Run the final structured-output adapter call through the middleware
1146
- * pipeline. Yields chunks to the caller only when
1147
- * `this.finalStructuredOutput.yieldChunks` is true; otherwise consumes
1148
- * silently while still piping through middleware.
1149
- *
1150
- * On success, populates this.structuredOutputResult.
1151
- * On failure, populates this.finalizationError.
1152
- */
1153
- async *runStructuredFinalization() {
1154
- if (!this.finalStructuredOutput) {
1155
- throw new Error(
1156
- "runStructuredFinalization called without finalStructuredOutput config"
1157
- );
1158
- }
1159
- this.middlewareCtx.phase = "structuredOutput";
1160
- const baseConfig = this.buildMiddlewareConfig();
1161
- const { tools: _omitTools, ...baseWithoutTools } = baseConfig;
1162
- let structuredConfig = {
1163
- ...baseWithoutTools,
1164
- outputSchema: this.finalStructuredOutput.jsonSchema
1165
- };
1166
- structuredConfig = await this.middlewareRunner.runOnStructuredOutputConfig(
1167
- this.middlewareCtx,
1168
- structuredConfig
1169
- );
1170
- const { outputSchema: pinnedSchema, ...chatConfigSlice } = structuredConfig;
1171
- const postOnConfig = await this.middlewareRunner.runOnConfig(
1172
- this.middlewareCtx,
1173
- { ...chatConfigSlice, tools: baseConfig.tools }
1174
- );
1175
- this.applyMiddlewareConfig(postOnConfig);
1176
- const structuredCallOptions = {
1177
- chatOptions: {
1178
- model: this.params.model,
1179
- messages: this.messages,
1180
- metadata: postOnConfig.metadata,
1181
- modelOptions: postOnConfig.modelOptions,
1182
- systemPrompts: postOnConfig.systemPrompts,
1183
- logger: this.logger,
1184
- threadId: this.threadId,
1185
- runId: this.runIdOverride,
1186
- parentRunId: this.parentRunIdOverride,
1187
- ...this.effectiveRequest ? { request: this.effectiveRequest } : {}
1188
- },
1189
- outputSchema: pinnedSchema
1190
- };
1191
- let fallbackAdapterError = void 0;
1192
- const providerStream = this.adapter.structuredOutputStream ? this.adapter.structuredOutputStream(structuredCallOptions) : fallbackStructuredOutputStream(
1193
- this.adapter,
1194
- structuredCallOptions,
1195
- (err) => {
1196
- fallbackAdapterError = err;
1197
- }
1198
- );
1199
- let startEmitted = false;
1200
- let structuredMessageId = null;
1201
- const extractMessageId = (c) => {
1202
- if (c.type === EventType.TEXT_MESSAGE_START || c.type === EventType.TEXT_MESSAGE_CONTENT || c.type === EventType.TEXT_MESSAGE_END) {
1203
- return typeof c.messageId === "string" && c.messageId !== "" ? c.messageId : null;
1204
- }
1205
- return null;
1206
- };
1207
- const buildSynthesizedStart = () => {
1208
- const idForStart = structuredMessageId ?? generateMessageId();
1209
- structuredMessageId = idForStart;
1210
- return {
1211
- type: EventType.CUSTOM,
1212
- name: "structured-output.start",
1213
- value: { messageId: idForStart },
1214
- model: this.params.model,
1215
- timestamp: Date.now(),
1216
- threadId: this.threadId,
1217
- ...this.runIdOverride ? { runId: this.runIdOverride } : {}
1218
- };
1219
- };
1220
- const pipeThroughMiddleware = async (synthChunk) => this.middlewareRunner.runOnChunk(this.middlewareCtx, synthChunk);
1221
- let runErrorYielded = false;
1222
- for await (const chunk of providerStream) {
1223
- if (this.isCancelled()) {
1224
- break;
1225
- }
1226
- if (!startEmitted && chunk.type === EventType.CUSTOM && chunk.name === "structured-output.start") {
1227
- startEmitted = true;
1228
- }
1229
- if (!structuredMessageId) {
1230
- const extracted = extractMessageId(chunk);
1231
- if (extracted) structuredMessageId = extracted;
1232
- }
1233
- if (this.finalStructuredOutput.yieldChunks) {
1234
- if (!startEmitted && (chunk.type === EventType.TEXT_MESSAGE_START || chunk.type === EventType.TEXT_MESSAGE_CONTENT || chunk.type === EventType.TEXT_MESSAGE_END)) {
1235
- startEmitted = true;
1236
- const synthStart = buildSynthesizedStart();
1237
- const synthOutputs = await pipeThroughMiddleware(synthStart);
1238
- for (const outputChunk of synthOutputs) {
1239
- yield outputChunk;
1240
- this.middlewareCtx.chunkIndex++;
1241
- }
1242
- }
1243
- if (!startEmitted && chunk.type === EventType.RUN_ERROR) {
1244
- startEmitted = true;
1245
- const synthStart = buildSynthesizedStart();
1246
- const synthOutputs = await pipeThroughMiddleware(synthStart);
1247
- for (const outputChunk of synthOutputs) {
1248
- yield outputChunk;
1249
- this.middlewareCtx.chunkIndex++;
1250
- }
1251
- }
1252
- }
1253
- let outboundChunk = chunk;
1254
- if (chunk.type === EventType.CUSTOM && chunk.name === "structured-output.complete") {
1255
- const parsed = readStructuredOutputCompleteValue(chunk.value);
1256
- if (parsed) {
1257
- const object = this.finalStructuredOutput.normalize ? this.finalStructuredOutput.normalize(parsed.object) : parsed.object;
1258
- this.structuredOutputResult = { data: object, rawText: parsed.raw };
1259
- const value = chunk.value;
1260
- if (object !== parsed.object && value && typeof value === "object") {
1261
- outboundChunk = { ...chunk, value: { ...value, object } };
1262
- }
1263
- }
1264
- }
1265
- if (chunk.type === EventType.RUN_FINISHED && chunk.usage) {
1266
- await this.middlewareRunner.runOnUsage(this.middlewareCtx, chunk.usage);
1267
- }
1268
- if (chunk.type === EventType.RUN_ERROR) {
1269
- this.finalizationError = {
1270
- message: chunk.message,
1271
- ...chunk.code ? { code: chunk.code } : {},
1272
- ...fallbackAdapterError !== void 0 ? { cause: fallbackAdapterError } : {}
1273
- };
1274
- }
1275
- const outputChunks = await this.middlewareRunner.runOnChunk(
1276
- this.middlewareCtx,
1277
- outboundChunk
1278
- );
1279
- if (this.finalStructuredOutput.yieldChunks) {
1280
- for (const outputChunk of outputChunks) {
1281
- if (outputChunk.type === EventType.RUN_ERROR) {
1282
- runErrorYielded = true;
1283
- }
1284
- yield outputChunk;
1285
- this.middlewareCtx.chunkIndex++;
1286
- }
1287
- }
1288
- if (this.finalizationError) {
1289
- break;
1290
- }
1291
- }
1292
- if (this.isCancelled()) {
1293
- return;
1294
- }
1295
- if (!this.structuredOutputResult && !this.finalizationError) {
1296
- this.finalizationError = {
1297
- message: "missing structured result",
1298
- code: "structured-output-missing-result"
1299
- };
1300
- }
1301
- if (this.structuredOutputResult && !this.finalizationError && this.finalStructuredOutput.validate) {
1302
- try {
1303
- const validated = this.finalStructuredOutput.validate(
1304
- this.structuredOutputResult.data
1305
- );
1306
- this.validatedStructuredOutput = validated;
1307
- this.hasValidatedStructuredOutput = true;
1308
- } catch (err) {
1309
- const message = err instanceof Error ? err.message : String(err);
1310
- this.finalizationError = {
1311
- message,
1312
- code: "structured-output-validation-failed",
1313
- cause: err
1314
- };
1315
- }
1316
- }
1317
- if (this.finalizationError && this.finalStructuredOutput.yieldChunks && !runErrorYielded) {
1318
- if (!startEmitted) {
1319
- const synthStart = buildSynthesizedStart();
1320
- const startOutputs = await pipeThroughMiddleware(synthStart);
1321
- for (const outputChunk of startOutputs) {
1322
- yield outputChunk;
1323
- this.middlewareCtx.chunkIndex++;
1324
- }
1325
- startEmitted = true;
1326
- }
1327
- const errChunk = {
1328
- type: EventType.RUN_ERROR,
1329
- runId: this.runIdOverride ?? this.requestId,
1330
- model: this.params.model,
1331
- timestamp: Date.now(),
1332
- threadId: this.threadId,
1333
- message: this.finalizationError.message,
1334
- ...this.finalizationError.code ? { code: this.finalizationError.code } : {},
1335
- error: {
1336
- message: this.finalizationError.message,
1337
- ...this.finalizationError.code ? { code: this.finalizationError.code } : {}
1338
- }
1339
- };
1340
- const outputChunks = await this.middlewareRunner.runOnChunk(
1341
- this.middlewareCtx,
1342
- errChunk
1343
- );
1344
- for (const outputChunk of outputChunks) {
1345
- yield outputChunk;
1346
- this.middlewareCtx.chunkIndex++;
1347
- }
1348
- }
1349
- }
1350
- /**
1351
- * Native combined mode: harvest the structured output from the agent
1352
- * loop's accumulated final-turn text (no separate provider call).
1353
- *
1354
- * The adapter wired `outputSchema` into the regular `chatStream` request,
1355
- * so the model's final-turn text is the schema-constrained JSON. We parse
1356
- * `this.accumulatedContent`, populate `this.structuredOutputResult`, emit
1357
- * a synthetic `structured-output.complete` (and a `structured-output.start`
1358
- * if one wasn't emitted earlier — only happens on the streaming path when
1359
- * the model returned no text at all), and run the validate callback when
1360
- * present. Failures populate `this.finalizationError` so the engine's
1361
- * terminal-hook chooser routes to `onError` (per spec §7.3).
1362
- *
1363
- * The `'structuredOutput'` middleware phase intentionally does NOT fire on
1364
- * this path — middleware sees the run through `beforeModel` / `modelStream`
1365
- * as usual. See PR #605 / issue #605 for the design rationale.
1366
- */
1367
- async *harvestCombinedStructuredOutput() {
1368
- if (!this.finalStructuredOutput) {
1369
- throw new Error(
1370
- "harvestCombinedStructuredOutput called without finalStructuredOutput config"
1371
- );
1372
- }
1373
- const yieldChunks = this.finalStructuredOutput.yieldChunks;
1374
- const rawText = this.accumulatedContent;
1375
- if (rawText.length === 0) {
1376
- this.finalizationError = {
1377
- message: "missing structured result",
1378
- code: "structured-output-missing-result"
1379
- };
1380
- } else {
1381
- try {
1382
- const parsed = JSON.parse(rawText);
1383
- const data = this.finalStructuredOutput.normalize ? this.finalStructuredOutput.normalize(parsed) : parsed;
1384
- this.structuredOutputResult = { data, rawText };
1385
- } catch (err) {
1386
- const detail = rawText.slice(0, 200) + (rawText.length > 200 ? "..." : "");
1387
- this.finalizationError = {
1388
- message: `Failed to parse structured output as JSON. Content: ${detail}`,
1389
- code: "structured-output-parse-failed",
1390
- cause: err
1391
- };
1392
- }
1393
- }
1394
- if (this.structuredOutputResult && !this.finalizationError && this.finalStructuredOutput.validate) {
1395
- try {
1396
- const validated = this.finalStructuredOutput.validate(
1397
- this.structuredOutputResult.data
1398
- );
1399
- this.validatedStructuredOutput = validated;
1400
- this.hasValidatedStructuredOutput = true;
1401
- } catch (err) {
1402
- const message = err instanceof Error ? err.message : String(err);
1403
- this.finalizationError = {
1404
- message,
1405
- code: "structured-output-validation-failed",
1406
- cause: err
1407
- };
1408
- }
1409
- }
1410
- if (!yieldChunks) {
1411
- return;
1412
- }
1413
- if (!this.combinedStartEmitted) {
1414
- this.combinedStartEmitted = true;
1415
- const messageId = this.combinedStructuredMessageId ?? generateMessageId();
1416
- this.combinedStructuredMessageId = messageId;
1417
- const synthStart = {
1418
- type: EventType.CUSTOM,
1419
- name: "structured-output.start",
1420
- value: { messageId },
1421
- model: this.params.model,
1422
- timestamp: Date.now(),
1423
- threadId: this.threadId,
1424
- ...this.runIdOverride ? { runId: this.runIdOverride } : {}
1425
- };
1426
- const startOutputs = await this.middlewareRunner.runOnChunk(
1427
- this.middlewareCtx,
1428
- synthStart
1429
- );
1430
- for (const outputChunk of startOutputs) {
1431
- yield outputChunk;
1432
- this.middlewareCtx.chunkIndex++;
1433
- }
1434
- }
1435
- if (this.structuredOutputResult && !this.finalizationError) {
1436
- const completeChunk = {
1437
- type: EventType.CUSTOM,
1438
- name: "structured-output.complete",
1439
- value: {
1440
- object: this.structuredOutputResult.data,
1441
- raw: this.structuredOutputResult.rawText,
1442
- ...this.combinedStructuredMessageId ? { messageId: this.combinedStructuredMessageId } : {}
1443
- },
1444
- model: this.params.model,
1445
- timestamp: Date.now(),
1446
- threadId: this.threadId,
1447
- ...this.runIdOverride ? { runId: this.runIdOverride } : {}
1448
- };
1449
- const completeOutputs = await this.middlewareRunner.runOnChunk(
1450
- this.middlewareCtx,
1451
- completeChunk
1452
- );
1453
- for (const outputChunk of completeOutputs) {
1454
- yield outputChunk;
1455
- this.middlewareCtx.chunkIndex++;
1456
- }
1457
- }
1458
- if (this.finalizationError) {
1459
- const errChunk = {
1460
- type: EventType.RUN_ERROR,
1461
- runId: this.runIdOverride ?? this.requestId,
1462
- model: this.params.model,
1463
- timestamp: Date.now(),
1464
- threadId: this.threadId,
1465
- message: this.finalizationError.message,
1466
- ...this.finalizationError.code ? { code: this.finalizationError.code } : {},
1467
- error: {
1468
- message: this.finalizationError.message,
1469
- ...this.finalizationError.code ? { code: this.finalizationError.code } : {}
1470
- }
1471
- };
1472
- const errOutputs = await this.middlewareRunner.runOnChunk(
1473
- this.middlewareCtx,
1474
- errChunk
1475
- );
1476
- for (const outputChunk of errOutputs) {
1477
- yield outputChunk;
1478
- this.middlewareCtx.chunkIndex++;
1479
- }
1480
- }
1481
- }
1482
- buildMiddlewareConfig() {
1483
- return {
1484
- messages: this.messages,
1485
- systemPrompts: [...this.systemPrompts],
1486
- tools: [...this.tools],
1487
- metadata: this.params.metadata,
1488
- modelOptions: this.params.modelOptions
1489
- };
1490
- }
1491
- applyMiddlewareConfig(config) {
1492
- this.messages = config.messages;
1493
- this.systemPrompts = config.systemPrompts;
1494
- this.tools = config.tools;
1495
- this.params = {
1496
- ...this.params,
1497
- metadata: config.metadata,
1498
- modelOptions: config.modelOptions
1499
- };
1500
- this.middlewareCtx.messages = this.messages;
1501
- this.middlewareCtx.systemPrompts = this.systemPrompts;
1502
- this.middlewareCtx.hasTools = this.tools.length > 0;
1503
- this.middlewareCtx.toolNames = this.tools.map((t) => t.name);
1504
- this.middlewareCtx.modelOptions = config.modelOptions;
1505
- }
1506
- setToolPhase(phase) {
1507
- this.toolPhase = phase;
1508
- }
1509
- /**
1510
- * Pipe a single chunk through the middleware pipeline (strip-to-spec, devtools, etc.)
1511
- * and yield all resulting output chunks.
1512
- */
1513
- async *pipeThroughMiddleware(chunk) {
1514
- const outputChunks = await this.middlewareRunner.runOnChunk(
1515
- this.middlewareCtx,
1516
- chunk
1517
- );
1518
- for (const outputChunk of outputChunks) {
1519
- yield outputChunk;
1520
- this.middlewareCtx.chunkIndex++;
1521
- }
1522
- }
1523
- /**
1524
- * Drain queued `sandbox.file` chunks (emitted via the SandboxRuntime sink)
1525
- * through the middleware pipeline and into the public stream.
1526
- */
1527
- async *drainSandboxFileQueue() {
1528
- while (this.sandboxFileQueue.length > 0) {
1529
- const chunk = this.sandboxFileQueue.shift();
1530
- if (chunk) yield* this.pipeThroughMiddleware(chunk);
1531
- }
1532
- }
1533
- /**
1534
- * Drain an executeToolCalls async generator, yielding any CustomEvent chunks
1535
- * through the middleware pipeline and returning the final ExecuteToolCallsResult.
1536
- */
1537
- async *drainToolCallGenerator(generator) {
1538
- let next = await generator.next();
1539
- while (!next.done) {
1540
- yield* this.pipeThroughMiddleware(next.value);
1541
- next = await generator.next();
1542
- }
1543
- return next.value;
1544
- }
1545
- createCustomEventChunk(eventName, value) {
1546
- return {
1547
- type: "CUSTOM",
1548
- timestamp: Date.now(),
1549
- model: this.params.model,
1550
- name: eventName,
1551
- value
1552
- };
1553
- }
1554
- createId(prefix) {
1555
- return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
1556
- }
102
+ /**
103
+ * Combine two optional AbortSignals into one that aborts when either does.
104
+ * Returns the other signal directly when one is absent or already aborted.
105
+ * (Manual implementation — `AbortSignal.any` requires Node >= 20.3.)
106
+ */
107
+ function combineAbortSignals(a, b) {
108
+ if (!a) return b;
109
+ if (!b) return a;
110
+ if (a.aborted) return a;
111
+ if (b.aborted) return b;
112
+ const controller = new AbortController();
113
+ const onAbort = (source) => () => {
114
+ controller.abort(source.reason);
115
+ };
116
+ a.addEventListener("abort", onAbort(a), { once: true });
117
+ b.addEventListener("abort", onAbort(b), { once: true });
118
+ return controller.signal;
1557
119
  }
120
+ var TextEngine = class {
121
+ adapter;
122
+ params;
123
+ systemPrompts;
124
+ tools;
125
+ loopStrategy;
126
+ toolCallManager;
127
+ lazyToolManager;
128
+ initialMessageCount;
129
+ requestId;
130
+ streamId;
131
+ effectiveRequest;
132
+ effectiveSignal;
133
+ messages;
134
+ iterationCount = 0;
135
+ /** Cumulative tool calls counted in this run (emitted + pending resume). */
136
+ toolCallCount = 0;
137
+ /** Tool calls in the most recent budgeted batch (0 when none). */
138
+ lastTurnToolCallCount = 0;
139
+ /** Tool call IDs already counted toward `toolCallCount` (avoids double-count on resume). */
140
+ countedToolCallIds = /* @__PURE__ */ new Set();
141
+ lastFinishReason = null;
142
+ streamStartTime = 0;
143
+ totalChunkCount = 0;
144
+ currentMessageId = null;
145
+ accumulatedContent = "";
146
+ accumulatedThinking = [];
147
+ currentThinkingContent = "";
148
+ currentThinkingSignature = "";
149
+ eventOptions;
150
+ eventToolNames;
151
+ finishedEvent = null;
152
+ deferredToolCallRunFinishedChunks = [];
153
+ earlyTermination = false;
154
+ toolPhase = "continue";
155
+ cyclePhase = "processText";
156
+ initialApprovals;
157
+ initialClientToolResults;
158
+ resumeApprovals = /* @__PURE__ */ new Map();
159
+ resumeClientToolResults = /* @__PURE__ */ new Map();
160
+ resumeDeniedToolResults = /* @__PURE__ */ new Map();
161
+ resumeCancelledToolCallIds = /* @__PURE__ */ new Set();
162
+ threadId;
163
+ runIdOverride;
164
+ parentRunIdOverride;
165
+ middlewareRunner;
166
+ middlewareCtx;
167
+ sandboxFileQueue = [];
168
+ deferredPromises = [];
169
+ abortReason;
170
+ middlewareAbortController;
171
+ toolAbortSignal;
172
+ terminalHookCalled = false;
173
+ /**
174
+ * Latched the first time the delivery socket closes; see `notifyDisconnected`.
175
+ * Also read by `subscribe` so a listener registered AFTER the disconnect (a
176
+ * middleware whose `setup` was still running at the time — the common case) is
177
+ * called immediately rather than never.
178
+ */
179
+ disconnected = false;
180
+ disconnectListeners = [];
181
+ logger;
182
+ structuredOutputResult = null;
183
+ combinedStartEmitted = false;
184
+ combinedStructuredMessageId = null;
185
+ validatedStructuredOutput = void 0;
186
+ hasValidatedStructuredOutput = false;
187
+ finalizationError = null;
188
+ finalStructuredOutput;
189
+ constructor(config, logger) {
190
+ this.logger = logger;
191
+ this.adapter = config.adapter;
192
+ this.finalStructuredOutput = config.finalStructuredOutput;
193
+ this.params = config.params;
194
+ this.systemPrompts = config.params.systemPrompts || [];
195
+ this.loopStrategy = config.params.agentLoopStrategy || maxIterations(5);
196
+ this.initialMessageCount = config.params.messages.length;
197
+ const { approvals, clientToolResults } = this.extractClientStateFromOriginalMessages(config.params.messages);
198
+ this.initialApprovals = approvals;
199
+ this.initialClientToolResults = clientToolResults;
200
+ this.messages = convertMessagesToModelMessages(config.params.messages);
201
+ this.lazyToolManager = new LazyToolManager(config.params.tools || [], this.messages, config.params.lazyToolsConfig);
202
+ this.tools = this.lazyToolManager.getActiveTools();
203
+ this.toolCallManager = new ToolCallManager(this.tools);
204
+ this.requestId = this.createId("chat");
205
+ this.streamId = this.createId("stream");
206
+ this.effectiveRequest = config.params.abortController ? { signal: config.params.abortController.signal } : void 0;
207
+ this.effectiveSignal = config.params.abortController?.signal;
208
+ this.threadId = config.params.threadId || config.params.conversationId || this.createId("thread");
209
+ this.runIdOverride = config.params.runId;
210
+ this.parentRunIdOverride = config.params.parentRunId;
211
+ const allMiddleware = [
212
+ devtoolsMiddleware(),
213
+ ...config.middleware || [],
214
+ stripToSpecMiddleware()
215
+ ];
216
+ this.middlewareRunner = new MiddlewareRunner(allMiddleware, logger);
217
+ this.middlewareAbortController = new AbortController();
218
+ this.toolAbortSignal = combineAbortSignals(this.effectiveSignal, this.middlewareAbortController.signal);
219
+ this.middlewareCtx = {
220
+ requestId: this.requestId,
221
+ streamId: this.streamId,
222
+ runId: this.runIdOverride ?? this.requestId,
223
+ parentRunId: this.parentRunIdOverride,
224
+ threadId: this.threadId,
225
+ conversationId: this.threadId,
226
+ phase: "init",
227
+ iteration: 0,
228
+ chunkIndex: 0,
229
+ signal: this.effectiveSignal,
230
+ abort: (reason) => {
231
+ this.abortReason = reason;
232
+ this.middlewareAbortController?.abort(reason);
233
+ },
234
+ context: config.context,
235
+ defer: (promise) => {
236
+ this.deferredPromises.push(promise);
237
+ },
238
+ activity: "chat",
239
+ provider: config.adapter.name,
240
+ model: config.params.model,
241
+ source: "server",
242
+ streaming: true,
243
+ systemPrompts: this.systemPrompts,
244
+ toolNames: void 0,
245
+ options: void 0,
246
+ modelOptions: config.params.modelOptions,
247
+ messageCount: this.initialMessageCount,
248
+ hasTools: this.tools.length > 0,
249
+ currentMessageId: null,
250
+ accumulatedContent: "",
251
+ messages: this.messages,
252
+ createId: (prefix) => this.createId(prefix),
253
+ capabilities: new CapabilityRegistry(),
254
+ get: (capability) => capability[0](this.middlewareCtx),
255
+ getOptional: (capability) => capability[0](this.middlewareCtx, { optional: true }),
256
+ provide: (capability, value) => capability[1](this.middlewareCtx, value)
257
+ };
258
+ provideRunDisconnect(this.middlewareCtx, { subscribe: (listener) => {
259
+ this.disconnectListeners.push(listener);
260
+ if (this.disconnected) this.runDisconnectListener(listener);
261
+ } });
262
+ provideSandboxRuntime(this.middlewareCtx, {
263
+ logger: this.logger,
264
+ emit: (event) => {
265
+ this.logger.sandbox(`file ${event.type} ${event.path}`, { event: {
266
+ type: event.type,
267
+ path: event.path,
268
+ timestamp: event.timestamp
269
+ } });
270
+ this.middlewareRunner.runSandboxFile(this.middlewareCtx, event).catch((err) => {
271
+ this.logger.errors("sandbox file hook failed", { error: err });
272
+ });
273
+ this.sandboxFileQueue.push(this.createCustomEventChunk("sandbox.file", {
274
+ type: event.type,
275
+ path: event.path,
276
+ timestamp: event.timestamp
277
+ }));
278
+ },
279
+ emitFileDiff: (value) => {
280
+ this.sandboxFileQueue.push(this.createCustomEventChunk("sandbox.file.diff", value));
281
+ }
282
+ });
283
+ }
284
+ /** Get the accumulated content after the chat loop completes */
285
+ getAccumulatedContent() {
286
+ return this.accumulatedContent;
287
+ }
288
+ /** Get the final messages array after the chat loop completes */
289
+ getMessages() {
290
+ return this.messages;
291
+ }
292
+ /** Returns the structured-output result if finalization ran successfully. */
293
+ getStructuredOutputResult() {
294
+ return this.structuredOutputResult;
295
+ }
296
+ /**
297
+ * Returns the validated structured-output value (the result of running
298
+ * `finalStructuredOutput.validate` against the raw structured-output data)
299
+ * wrapped in a `{ value }` object so callers can distinguish "no validation
300
+ * happened" from "validation produced undefined". Returns `null` when no
301
+ * validator was configured or validation hasn't been performed yet.
302
+ */
303
+ getValidatedStructuredOutput() {
304
+ return this.hasValidatedStructuredOutput ? { value: this.validatedStructuredOutput } : null;
305
+ }
306
+ /** Returns the recorded finalization error, if any. */
307
+ getFinalizationError() {
308
+ return this.finalizationError;
309
+ }
310
+ async *run() {
311
+ this.beforeRun();
312
+ this.logger.agentLoop("run started", { threadId: this.middlewareCtx.threadId });
313
+ try {
314
+ await this.middlewareRunner.runSetup(this.middlewareCtx);
315
+ this.middlewareCtx.phase = "init";
316
+ const initialConfig = this.buildMiddlewareConfig();
317
+ const transformedConfig = await this.middlewareRunner.runOnConfig(this.middlewareCtx, initialConfig);
318
+ this.applyMiddlewareConfig(transformedConfig);
319
+ await this.applyEphemeralInterruptResume(transformedConfig);
320
+ await this.middlewareRunner.runOnStart(this.middlewareCtx);
321
+ if ((yield* this.checkForPendingToolCalls()) === "wait") return;
322
+ if (!(!!this.finalStructuredOutput && this.tools.length === 0 && this.finalStructuredOutput.nativeCombined !== true)) do {
323
+ if (this.earlyTermination || this.isCancelled()) return;
324
+ this.logger.agentLoop(`iteration=${this.middlewareCtx.iteration}`, { iteration: this.middlewareCtx.iteration });
325
+ await this.beginCycle();
326
+ if (this.cyclePhase === "processText") {
327
+ this.middlewareCtx.phase = "beforeModel";
328
+ this.middlewareCtx.iteration = this.iterationCount;
329
+ const iterConfig = this.buildMiddlewareConfig();
330
+ const iterTransformedConfig = await this.middlewareRunner.runOnConfig(this.middlewareCtx, iterConfig);
331
+ this.applyMiddlewareConfig(iterTransformedConfig);
332
+ yield* this.streamModelResponse();
333
+ } else yield* this.processToolCalls();
334
+ this.endCycle();
335
+ } while (await this.shouldContinue());
336
+ this.logger.agentLoop("run finished", { finishReason: this.lastFinishReason });
337
+ if (this.finalStructuredOutput && this.toolPhase !== "wait" && !this.isCancelled() && !this.finalizationError) if (this.finalStructuredOutput.nativeCombined === true) yield* this.harvestCombinedStructuredOutput();
338
+ else yield* this.runStructuredFinalization();
339
+ if (!this.terminalHookCalled && this.toolPhase !== "wait" && !this.isCancelled()) if (this.finalizationError) {
340
+ this.terminalHookCalled = true;
341
+ const errForHook = new Error(this.finalizationError.message, this.finalizationError.cause !== void 0 ? { cause: this.finalizationError.cause } : void 0);
342
+ if (this.finalizationError.code !== void 0) Object.defineProperty(errForHook, "code", {
343
+ value: this.finalizationError.code,
344
+ enumerable: true
345
+ });
346
+ await this.middlewareRunner.runOnError(this.middlewareCtx, {
347
+ error: errForHook,
348
+ duration: Date.now() - this.streamStartTime
349
+ });
350
+ } else {
351
+ this.terminalHookCalled = true;
352
+ await this.middlewareRunner.runOnFinish(this.middlewareCtx, {
353
+ finishReason: this.lastFinishReason,
354
+ duration: Date.now() - this.streamStartTime,
355
+ content: this.accumulatedContent,
356
+ usage: this.finishedEvent?.usage
357
+ });
358
+ }
359
+ } catch (error) {
360
+ if (error instanceof Error && error.name === "InterruptReplaySignal" && "continuationRunId" in error && typeof error.continuationRunId === "string") {
361
+ this.terminalHookCalled = true;
362
+ yield {
363
+ type: EventType.RUN_FINISHED,
364
+ timestamp: Date.now(),
365
+ threadId: this.threadId,
366
+ runId: this.runIdOverride ?? this.requestId,
367
+ finishReason: "stop",
368
+ outcome: { type: "success" },
369
+ result: {
370
+ replayed: true,
371
+ continuationRunId: error.continuationRunId
372
+ }
373
+ };
374
+ return;
375
+ }
376
+ const interruptFailure = structuralInterruptFailure(error);
377
+ if (interruptFailure) {
378
+ this.terminalHookCalled = true;
379
+ this.logger.errors("chat interrupt resume failed", {
380
+ error,
381
+ threadId: this.middlewareCtx.threadId
382
+ });
383
+ await this.middlewareRunner.runOnError(this.middlewareCtx, {
384
+ error: interruptFailure.error,
385
+ duration: Date.now() - this.streamStartTime
386
+ });
387
+ yield this.buildInterruptRunErrorChunk(error);
388
+ return;
389
+ }
390
+ if (!this.terminalHookCalled) {
391
+ this.terminalHookCalled = true;
392
+ if (error instanceof MiddlewareAbortError) {
393
+ this.abortReason = error.message;
394
+ await this.middlewareRunner.runOnAbort(this.middlewareCtx, {
395
+ reason: error.message,
396
+ duration: Date.now() - this.streamStartTime,
397
+ cancelRequested: isCancelRequestedReason(error.message)
398
+ });
399
+ } else {
400
+ this.logger.errors("chat run failed", {
401
+ error,
402
+ threadId: this.middlewareCtx.threadId
403
+ });
404
+ await this.middlewareRunner.runOnError(this.middlewareCtx, {
405
+ error,
406
+ duration: Date.now() - this.streamStartTime
407
+ });
408
+ }
409
+ }
410
+ if (!(error instanceof MiddlewareAbortError)) throw error;
411
+ } finally {
412
+ if (!this.terminalHookCalled && this.isCancelled()) {
413
+ this.terminalHookCalled = true;
414
+ const reason = this.resolveAbortReason();
415
+ await this.middlewareRunner.runOnAbort(this.middlewareCtx, {
416
+ reason,
417
+ duration: Date.now() - this.streamStartTime,
418
+ cancelRequested: isCancelRequestedReason(reason)
419
+ });
420
+ }
421
+ if (this.deferredPromises.length > 0) await Promise.allSettled(this.deferredPromises);
422
+ }
423
+ }
424
+ beforeRun() {
425
+ this.streamStartTime = Date.now();
426
+ const { tools, metadata } = this.params;
427
+ const options = {};
428
+ if (metadata !== void 0) options.metadata = metadata;
429
+ this.eventOptions = Object.keys(options).length > 0 ? options : void 0;
430
+ this.eventToolNames = tools?.map((t) => t.name);
431
+ this.middlewareCtx.options = this.eventOptions;
432
+ this.middlewareCtx.toolNames = this.eventToolNames;
433
+ }
434
+ async beginCycle() {
435
+ if (this.cyclePhase === "processText") await this.beginIteration();
436
+ }
437
+ endCycle() {
438
+ if (this.cyclePhase === "processText") {
439
+ this.cyclePhase = "executeToolCalls";
440
+ return;
441
+ }
442
+ this.cyclePhase = "processText";
443
+ this.iterationCount++;
444
+ }
445
+ async beginIteration() {
446
+ this.currentMessageId = this.createId("msg");
447
+ this.accumulatedContent = "";
448
+ this.accumulatedThinking = [];
449
+ this.currentThinkingContent = "";
450
+ this.currentThinkingSignature = "";
451
+ this.finishedEvent = null;
452
+ this.middlewareCtx.currentMessageId = this.currentMessageId;
453
+ this.middlewareCtx.accumulatedContent = "";
454
+ await this.middlewareRunner.runOnIteration(this.middlewareCtx, {
455
+ iteration: this.iterationCount,
456
+ messageId: this.currentMessageId
457
+ });
458
+ }
459
+ async *streamModelResponse() {
460
+ const { metadata, modelOptions } = this.params;
461
+ const toolsWithJsonSchemas = this.tools.map((tool) => ({
462
+ ...tool,
463
+ inputSchema: tool.inputSchema ? convertSchemaToJsonSchema(tool.inputSchema) : void 0,
464
+ outputSchema: tool.outputSchema ? convertSchemaToJsonSchema(tool.outputSchema) : void 0
465
+ }));
466
+ this.middlewareCtx.phase = "modelStream";
467
+ const providerName = this.adapter.provider ?? this.adapter.name;
468
+ this.logger.request(`activity=chat provider=${providerName} model=${this.params.model} messages=${this.messages.length} tools=${this.tools.length} stream=true`, {
469
+ provider: providerName,
470
+ model: this.params.model,
471
+ messageCount: this.messages.length,
472
+ toolCount: this.tools.length
473
+ });
474
+ const combinedSchema = this.finalStructuredOutput?.nativeCombined === true ? this.finalStructuredOutput.jsonSchema : void 0;
475
+ const { approvals } = this.collectClientState();
476
+ const adapterApprovals = /* @__PURE__ */ new Map();
477
+ for (const [approvalId, resolution] of approvals) adapterApprovals.set(approvalId, typeof resolution === "boolean" ? resolution : resolution.approved);
478
+ for await (const chunk of this.adapter.chatStream({
479
+ model: this.params.model,
480
+ messages: this.messages,
481
+ tools: toolsWithJsonSchemas,
482
+ metadata,
483
+ request: this.effectiveRequest,
484
+ modelOptions,
485
+ systemPrompts: this.systemPrompts,
486
+ logger: this.logger,
487
+ threadId: this.threadId,
488
+ runId: this.runIdOverride,
489
+ parentRunId: this.parentRunIdOverride,
490
+ capabilities: this.middlewareCtx,
491
+ approvals: adapterApprovals,
492
+ ...combinedSchema ? { outputSchema: combinedSchema } : {}
493
+ })) {
494
+ if (this.isCancelled()) break;
495
+ this.totalChunkCount++;
496
+ this.handleStreamChunk(chunk);
497
+ if (this.finalStructuredOutput?.nativeCombined === true && this.finalStructuredOutput.yieldChunks && !this.combinedStartEmitted && chunk.type === EventType.TEXT_MESSAGE_START) {
498
+ this.combinedStartEmitted = true;
499
+ const messageId = typeof chunk.messageId === "string" && chunk.messageId !== "" ? chunk.messageId : generateMessageId();
500
+ this.combinedStructuredMessageId = messageId;
501
+ const synthStart = {
502
+ type: EventType.CUSTOM,
503
+ name: "structured-output.start",
504
+ value: { messageId },
505
+ model: this.params.model,
506
+ timestamp: Date.now(),
507
+ threadId: this.threadId,
508
+ ...this.runIdOverride ? { runId: this.runIdOverride } : {}
509
+ };
510
+ const synthOutputs = await this.middlewareRunner.runOnChunk(this.middlewareCtx, synthStart);
511
+ for (const outputChunk of synthOutputs) {
512
+ yield outputChunk;
513
+ this.middlewareCtx.chunkIndex++;
514
+ }
515
+ }
516
+ const outputChunks = await this.middlewareRunner.runOnChunk(this.middlewareCtx, chunk);
517
+ const suppressAgentLifecycle = !!this.finalStructuredOutput && this.finalStructuredOutput.yieldChunks && this.finalStructuredOutput.nativeCombined !== true;
518
+ for (const outputChunk of outputChunks) {
519
+ if (suppressAgentLifecycle && (outputChunk.type === EventType.RUN_STARTED || outputChunk.type === EventType.RUN_FINISHED)) continue;
520
+ if (this.shouldDeferToolCallRunFinished(outputChunk)) {
521
+ this.deferredToolCallRunFinishedChunks.push(outputChunk);
522
+ continue;
523
+ }
524
+ this.logger.output(`type=${outputChunk.type}`, { chunk: outputChunk });
525
+ yield outputChunk;
526
+ this.middlewareCtx.chunkIndex++;
527
+ }
528
+ if (chunk.type === "RUN_FINISHED" && chunk.usage) await this.middlewareRunner.runOnUsage(this.middlewareCtx, chunk.usage);
529
+ yield* this.drainSandboxFileQueue();
530
+ if (this.earlyTermination) break;
531
+ }
532
+ yield* this.drainSandboxFileQueue();
533
+ }
534
+ handleStreamChunk(chunk) {
535
+ switch (chunk.type) {
536
+ case "TEXT_MESSAGE_CONTENT":
537
+ this.handleTextMessageContentEvent(chunk);
538
+ break;
539
+ case "TOOL_CALL_START":
540
+ this.handleToolCallStartEvent(chunk);
541
+ break;
542
+ case "TOOL_CALL_ARGS":
543
+ this.handleToolCallArgsEvent(chunk);
544
+ break;
545
+ case "TOOL_CALL_END":
546
+ this.handleToolCallEndEvent(chunk);
547
+ break;
548
+ case "RUN_FINISHED":
549
+ this.handleRunFinishedEvent(chunk);
550
+ break;
551
+ case "RUN_ERROR":
552
+ this.handleRunErrorEvent(chunk);
553
+ break;
554
+ case "STEP_STARTED":
555
+ this.handleStepStartedEvent();
556
+ break;
557
+ case "STEP_FINISHED":
558
+ this.handleStepFinishedEvent(chunk);
559
+ break;
560
+ case "TOOL_CALL_RESULT": break;
561
+ case "REASONING_START":
562
+ case "REASONING_MESSAGE_START":
563
+ case "REASONING_MESSAGE_CONTENT":
564
+ case "REASONING_MESSAGE_END":
565
+ case "REASONING_END": break;
566
+ default: break;
567
+ }
568
+ }
569
+ handleTextMessageContentEvent(chunk) {
570
+ if (chunk.content) this.accumulatedContent = chunk.content;
571
+ else this.accumulatedContent += chunk.delta;
572
+ this.middlewareCtx.accumulatedContent = this.accumulatedContent;
573
+ }
574
+ handleToolCallStartEvent(chunk) {
575
+ this.toolCallManager.addToolCallStartEvent(chunk);
576
+ }
577
+ handleToolCallArgsEvent(chunk) {
578
+ this.toolCallManager.addToolCallArgsEvent(chunk);
579
+ }
580
+ handleToolCallEndEvent(chunk) {
581
+ this.toolCallManager.completeToolCall(chunk);
582
+ }
583
+ handleRunFinishedEvent(chunk) {
584
+ this.finishedEvent = chunk;
585
+ this.lastFinishReason = chunk.finishReason ?? null;
586
+ }
587
+ handleRunErrorEvent(_chunk) {
588
+ this.earlyTermination = true;
589
+ }
590
+ finalizeCurrentThinkingStep() {
591
+ if (this.currentThinkingContent) {
592
+ this.accumulatedThinking.push({
593
+ content: this.currentThinkingContent,
594
+ ...this.currentThinkingSignature && { signature: this.currentThinkingSignature }
595
+ });
596
+ this.currentThinkingContent = "";
597
+ this.currentThinkingSignature = "";
598
+ }
599
+ }
600
+ handleStepStartedEvent() {
601
+ this.finalizeCurrentThinkingStep();
602
+ }
603
+ handleStepFinishedEvent(chunk) {
604
+ if (chunk.delta) this.currentThinkingContent += chunk.delta;
605
+ if (chunk.signature) this.currentThinkingSignature = chunk.signature;
606
+ }
607
+ /**
608
+ * Tools available for execution this turn. The discovery tool is dropped
609
+ * from the advertised set (`this.tools`) once every lazy tool is discovered,
610
+ * but a model may still re-request discovery; this widens execution lookup
611
+ * to include it so such calls don't fail with "Unknown tool". Centralised so
612
+ * both execution sites (`processToolCalls` and `checkForPendingToolCalls`)
613
+ * stay in sync.
614
+ */
615
+ resolveExecutableTools(toolCalls) {
616
+ return this.lazyToolManager.getExecutableTools(this.tools, toolCalls.map((tc) => tc.function.name));
617
+ }
618
+ async *checkForPendingToolCalls() {
619
+ const pendingToolCalls = this.getPendingToolCallsFromMessages();
620
+ if (pendingToolCalls.length === 0) return "continue";
621
+ const finishEvent = this.createSyntheticFinishedEvent();
622
+ this.recordToolCalls(pendingToolCalls);
623
+ const undiscoveredLazyResults = [];
624
+ const executablePendingCalls = pendingToolCalls.filter((tc) => {
625
+ if (this.lazyToolManager.isUndiscoveredLazyTool(tc.function.name)) {
626
+ undiscoveredLazyResults.push({
627
+ toolCallId: tc.id,
628
+ toolName: tc.function.name,
629
+ result: { error: this.lazyToolManager.getUndiscoveredToolError(tc.function.name) },
630
+ state: "output-error"
631
+ });
632
+ return false;
633
+ }
634
+ return true;
635
+ });
636
+ const deferredErrorResults = [...undiscoveredLazyResults];
637
+ const argsMap = /* @__PURE__ */ new Map();
638
+ for (const tc of pendingToolCalls) argsMap.set(tc.id, tc.function.arguments);
639
+ if (executablePendingCalls.length === 0) {
640
+ if (deferredErrorResults.length > 0) for (const chunk of this.buildToolResultChunks(deferredErrorResults, finishEvent, argsMap)) yield* this.pipeThroughMiddleware(chunk);
641
+ return "continue";
642
+ }
643
+ const { approvals, clientToolResults } = this.collectClientState();
644
+ const generator = executeToolCalls(executablePendingCalls, this.resolveExecutableTools(executablePendingCalls), approvals, clientToolResults, (eventName, data) => this.createCustomEventChunk(eventName, data), {
645
+ onBeforeToolCall: async (toolCall, tool, args) => {
646
+ this.logger.tools(`phase=before name=${toolCall.function.name}`, {
647
+ name: toolCall.function.name,
648
+ args
649
+ });
650
+ const hookCtx = {
651
+ toolCall,
652
+ tool,
653
+ args,
654
+ toolName: toolCall.function.name,
655
+ toolCallId: toolCall.id
656
+ };
657
+ return this.middlewareRunner.runOnBeforeToolCall(this.middlewareCtx, hookCtx);
658
+ },
659
+ onAfterToolCall: async (info) => {
660
+ this.logger.tools(`phase=after name=${info.toolName}`, {
661
+ name: info.toolName,
662
+ result: info.result
663
+ });
664
+ await this.middlewareRunner.runOnAfterToolCall(this.middlewareCtx, info);
665
+ }
666
+ }, this.middlewareCtx.context, this.toolAbortSignal, {
667
+ deniedToolResults: this.resumeDeniedToolResults,
668
+ cancelledToolCallIds: this.resumeCancelledToolCallIds
669
+ });
670
+ const executionResult = yield* this.drainToolCallGenerator(generator);
671
+ if (this.isMiddlewareAborted()) {
672
+ this.setToolPhase("stop");
673
+ return "stop";
674
+ }
675
+ const allResults = [...executionResult.results, ...deferredErrorResults];
676
+ await this.middlewareRunner.runOnToolPhaseComplete(this.middlewareCtx, {
677
+ toolCalls: pendingToolCalls,
678
+ results: allResults,
679
+ needsApproval: executionResult.needsApproval,
680
+ needsClientExecution: executionResult.needsClientExecution
681
+ });
682
+ if (executionResult.needsApproval.length > 0 || executionResult.needsClientExecution.length > 0) {
683
+ this.discardDeferredToolCallRunFinishedChunks();
684
+ if (allResults.length > 0) for (const chunk of this.buildToolResultChunks(allResults, finishEvent)) yield* this.pipeThroughMiddleware(chunk);
685
+ const emitted = yield* this.emitActionableInterruptBoundary(finishEvent, executionResult.needsApproval, executionResult.needsClientExecution);
686
+ this.setToolPhase(emitted ? "wait" : "stop");
687
+ return emitted ? "wait" : "stop";
688
+ }
689
+ const toolResultChunks = this.buildToolResultChunks(allResults, finishEvent);
690
+ for (const chunk of toolResultChunks) yield* this.pipeThroughMiddleware(chunk);
691
+ return "continue";
692
+ }
693
+ async *processToolCalls() {
694
+ if (!this.shouldExecuteToolPhase()) {
695
+ this.lastTurnToolCallCount = 0;
696
+ this.setToolPhase("stop");
697
+ return;
698
+ }
699
+ const toolCalls = this.toolCallManager.getToolCalls();
700
+ const finishEvent = this.finishedEvent;
701
+ if (!finishEvent || toolCalls.length === 0) {
702
+ this.lastTurnToolCallCount = 0;
703
+ this.setToolPhase("stop");
704
+ return;
705
+ }
706
+ this.recordToolCalls(toolCalls);
707
+ this.addAssistantToolCallMessage(toolCalls);
708
+ const undiscoveredLazyResults = [];
709
+ const executableToolCalls = toolCalls.filter((tc) => {
710
+ if (this.lazyToolManager.isUndiscoveredLazyTool(tc.function.name)) {
711
+ undiscoveredLazyResults.push({
712
+ toolCallId: tc.id,
713
+ toolName: tc.function.name,
714
+ result: { error: this.lazyToolManager.getUndiscoveredToolError(tc.function.name) },
715
+ state: "output-error"
716
+ });
717
+ return false;
718
+ }
719
+ return true;
720
+ });
721
+ const deferredErrorResults = [...undiscoveredLazyResults];
722
+ if (executableToolCalls.length === 0) {
723
+ yield* this.flushDeferredToolCallRunFinishedChunks();
724
+ if (deferredErrorResults.length > 0) for (const chunk of this.buildToolResultChunks(deferredErrorResults, finishEvent)) yield* this.pipeThroughMiddleware(chunk);
725
+ this.toolCallManager.clear();
726
+ this.setToolPhase("continue");
727
+ return;
728
+ }
729
+ this.middlewareCtx.phase = "beforeTools";
730
+ const { approvals, clientToolResults } = this.collectClientState();
731
+ const generator = executeToolCalls(executableToolCalls, this.resolveExecutableTools(executableToolCalls), approvals, clientToolResults, (eventName, data) => this.createCustomEventChunk(eventName, data), {
732
+ onBeforeToolCall: async (toolCall, tool, args) => {
733
+ this.logger.tools(`phase=before name=${toolCall.function.name}`, {
734
+ name: toolCall.function.name,
735
+ args
736
+ });
737
+ const hookCtx = {
738
+ toolCall,
739
+ tool,
740
+ args,
741
+ toolName: toolCall.function.name,
742
+ toolCallId: toolCall.id
743
+ };
744
+ return this.middlewareRunner.runOnBeforeToolCall(this.middlewareCtx, hookCtx);
745
+ },
746
+ onAfterToolCall: async (info) => {
747
+ this.logger.tools(`phase=after name=${info.toolName}`, {
748
+ name: info.toolName,
749
+ result: info.result
750
+ });
751
+ await this.middlewareRunner.runOnAfterToolCall(this.middlewareCtx, info);
752
+ }
753
+ }, this.middlewareCtx.context, this.toolAbortSignal, {
754
+ deniedToolResults: this.resumeDeniedToolResults,
755
+ cancelledToolCallIds: this.resumeCancelledToolCallIds
756
+ });
757
+ const executionResult = yield* this.drainToolCallGenerator(generator);
758
+ this.middlewareCtx.phase = "afterTools";
759
+ if (this.isMiddlewareAborted()) {
760
+ this.setToolPhase("stop");
761
+ return;
762
+ }
763
+ const allResults = [...executionResult.results, ...deferredErrorResults];
764
+ await this.middlewareRunner.runOnToolPhaseComplete(this.middlewareCtx, {
765
+ toolCalls,
766
+ results: allResults,
767
+ needsApproval: executionResult.needsApproval,
768
+ needsClientExecution: executionResult.needsClientExecution
769
+ });
770
+ if (executionResult.needsApproval.length > 0 || executionResult.needsClientExecution.length > 0) {
771
+ if (allResults.length > 0) for (const chunk of this.buildToolResultChunks(allResults, finishEvent)) yield* this.pipeThroughMiddleware(chunk);
772
+ const emitted = yield* this.emitActionableInterruptBoundary(finishEvent, executionResult.needsApproval, executionResult.needsClientExecution);
773
+ this.setToolPhase(emitted ? "wait" : "stop");
774
+ return;
775
+ }
776
+ yield* this.flushDeferredToolCallRunFinishedChunks();
777
+ const toolResultChunks = this.buildToolResultChunks(allResults, finishEvent);
778
+ for (const chunk of toolResultChunks) yield* this.pipeThroughMiddleware(chunk);
779
+ if (this.lazyToolManager.hasNewlyDiscoveredTools()) {
780
+ this.tools = this.lazyToolManager.getActiveTools();
781
+ this.toolCallManager = new ToolCallManager(this.tools);
782
+ this.setToolPhase("continue");
783
+ return;
784
+ }
785
+ this.toolCallManager.clear();
786
+ this.setToolPhase("continue");
787
+ }
788
+ shouldDeferToolCallRunFinished(chunk) {
789
+ return chunk.type === EventType.RUN_FINISHED && this.finishedEvent?.finishReason === "tool_calls" && this.tools.length > 0 && this.toolCallManager.hasToolCalls();
790
+ }
791
+ *flushDeferredToolCallRunFinishedChunks() {
792
+ for (const chunk of this.deferredToolCallRunFinishedChunks) {
793
+ this.logger.output(`type=${chunk.type}`, { chunk });
794
+ yield chunk;
795
+ this.middlewareCtx.chunkIndex++;
796
+ }
797
+ this.deferredToolCallRunFinishedChunks = [];
798
+ }
799
+ discardDeferredToolCallRunFinishedChunks() {
800
+ this.deferredToolCallRunFinishedChunks = [];
801
+ }
802
+ shouldExecuteToolPhase() {
803
+ return this.finishedEvent?.finishReason === "tool_calls" && this.tools.length > 0 && this.toolCallManager.hasToolCalls();
804
+ }
805
+ addAssistantToolCallMessage(toolCalls) {
806
+ this.finalizeCurrentThinkingStep();
807
+ this.messages = [...this.messages, {
808
+ role: "assistant",
809
+ content: this.accumulatedContent || null,
810
+ toolCalls,
811
+ ...this.accumulatedThinking.length > 0 && { thinking: this.accumulatedThinking }
812
+ }];
813
+ this.middlewareCtx.messages = this.messages;
814
+ }
815
+ /**
816
+ * Extract client state (approvals and client tool results) from original messages.
817
+ * This is called in the constructor BEFORE converting to ModelMessage format,
818
+ * because the parts array (which contains approval state) is lost during conversion.
819
+ */
820
+ extractClientStateFromOriginalMessages(originalMessages) {
821
+ const approvals = /* @__PURE__ */ new Map();
822
+ const clientToolResults = /* @__PURE__ */ new Map();
823
+ for (const message of originalMessages) if (message.role === "assistant" && message.parts) {
824
+ for (const part of message.parts) if (part.type === "tool-call") {
825
+ if (part.output !== void 0 && !part.approval) clientToolResults.set(part.id, part.output);
826
+ if (part.approval?.id && part.approval?.approved !== void 0 && part.state === "approval-responded") approvals.set(part.approval.id, part.approval.approved);
827
+ }
828
+ }
829
+ return {
830
+ approvals,
831
+ clientToolResults
832
+ };
833
+ }
834
+ collectClientState() {
835
+ const approvals = new Map(this.initialApprovals);
836
+ const clientToolResults = new Map(this.initialClientToolResults);
837
+ for (const [approvalId, approved] of this.resumeApprovals) approvals.set(approvalId, approved);
838
+ for (const [toolCallId, result] of this.resumeClientToolResults) clientToolResults.set(toolCallId, result);
839
+ for (const message of this.messages) if (message.role === "tool" && message.toolCallId) {
840
+ let output;
841
+ if (Array.isArray(message.content)) output = message.content;
842
+ else try {
843
+ output = JSON.parse(message.content);
844
+ } catch {
845
+ output = message.content;
846
+ }
847
+ if (output && typeof output === "object" && output.pendingExecution === true) continue;
848
+ clientToolResults.set(message.toolCallId, output);
849
+ }
850
+ return {
851
+ approvals,
852
+ clientToolResults
853
+ };
854
+ }
855
+ buildActionableInterrupts(approvals, clientRequests) {
856
+ const interrupts = [];
857
+ for (const approval of approvals) {
858
+ const tool = this.tools.find((candidate) => candidate.name === approval.toolName);
859
+ const normalized = normalizeApprovalSchema(tool?.approvalSchema, tool?.inputSchema);
860
+ interrupts.push({
861
+ id: approval.approvalId,
862
+ reason: "tool_call",
863
+ message: `Approval required to run ${approval.toolName}`,
864
+ toolCallId: approval.toolCallId,
865
+ responseSchema: normalized.responseSchema,
866
+ metadata: {
867
+ kind: "approval",
868
+ toolName: approval.toolName,
869
+ input: approval.input,
870
+ [interruptBindingMetadataKey]: {
871
+ v: 1,
872
+ kind: "tool-approval",
873
+ interruptId: approval.approvalId,
874
+ toolName: approval.toolName,
875
+ toolCallId: approval.toolCallId,
876
+ originalArgs: approval.input,
877
+ inputSchemaHash: hashSchemaInput(tool?.inputSchema),
878
+ approvalSchemaHash: normalized.approvalSchemaHash,
879
+ responseSchemaHash: normalized.responseSchemaHash
880
+ }
881
+ }
882
+ });
883
+ }
884
+ for (const clientTool of clientRequests) {
885
+ const tool = this.tools.find((candidate) => candidate.name === clientTool.toolName);
886
+ const responseSchema = convertSchemaToJsonSchema(tool?.outputSchema) ?? {};
887
+ interrupts.push({
888
+ id: `client_tool_${clientTool.toolCallId}`,
889
+ reason: "tanstack:client_tool_execution",
890
+ message: `Client tool ${clientTool.toolName} is ready to run`,
891
+ toolCallId: clientTool.toolCallId,
892
+ responseSchema,
893
+ metadata: {
894
+ kind: "client_tool",
895
+ toolName: clientTool.toolName,
896
+ input: clientTool.input,
897
+ [interruptBindingMetadataKey]: {
898
+ v: 1,
899
+ kind: "client-tool-execution",
900
+ interruptId: `client_tool_${clientTool.toolCallId}`,
901
+ toolName: clientTool.toolName,
902
+ toolCallId: clientTool.toolCallId,
903
+ outputSchemaHash: hashSchemaInput(tool?.outputSchema),
904
+ responseSchemaHash: digestInterruptJson(canonicalInterruptJson(responseSchema))
905
+ }
906
+ }
907
+ });
908
+ }
909
+ return interrupts;
910
+ }
911
+ buildInterruptFinishedChunk(finishEvent, approvals, clientRequests) {
912
+ return {
913
+ ...finishEvent,
914
+ timestamp: Date.now(),
915
+ outcome: {
916
+ type: "interrupt",
917
+ interrupts: this.buildActionableInterrupts(approvals, clientRequests)
918
+ }
919
+ };
920
+ }
921
+ buildMessagesSnapshotChunk() {
922
+ const messages = this.messages.map((message, index) => {
923
+ const content = typeof message.content === "string" ? message.content : message.content === null ? void 0 : JSON.stringify(message.content);
924
+ return {
925
+ id: `snapshot_${this.runIdOverride ?? this.requestId}_${index}`,
926
+ role: message.role,
927
+ ...content !== void 0 ? { content } : {},
928
+ ..."toolCalls" in message && message.toolCalls ? { toolCalls: message.toolCalls } : {},
929
+ ..."toolCallId" in message && message.toolCallId ? { toolCallId: message.toolCallId } : {}
930
+ };
931
+ });
932
+ return {
933
+ type: EventType.MESSAGES_SNAPSHOT,
934
+ timestamp: Date.now(),
935
+ model: this.params.model,
936
+ messages
937
+ };
938
+ }
939
+ publicInterruptTerminal(chunk) {
940
+ if (chunk.type !== EventType.RUN_FINISHED || chunk.outcome?.type !== "interrupt") return chunk;
941
+ return {
942
+ ...chunk,
943
+ outcome: {
944
+ ...chunk.outcome,
945
+ interrupts: chunk.outcome.interrupts.map((interrupt) => {
946
+ if (!interrupt.metadata || typeof interrupt.metadata !== "object" || Array.isArray(interrupt.metadata)) return interrupt;
947
+ const metadata = { ...interrupt.metadata };
948
+ const binding = normalizePublicInterruptBinding(metadata[interruptBindingMetadataKey], interrupt.id);
949
+ if (binding) metadata[interruptBindingMetadataKey] = binding;
950
+ else delete metadata[interruptBindingMetadataKey];
951
+ return {
952
+ ...interrupt,
953
+ metadata
954
+ };
955
+ })
956
+ }
957
+ };
958
+ }
959
+ interruptFailure(error) {
960
+ const structured = structuralInterruptFailure(error);
961
+ if (structured) return {
962
+ message: structured.error.message,
963
+ code: structured.errors[0]?.code ?? "server",
964
+ errors: structured.errors
965
+ };
966
+ if (error && typeof error === "object" && "errors" in error) {
967
+ const errors = error.errors;
968
+ if (Array.isArray(errors)) {
969
+ const first = errors[0];
970
+ if (first && typeof first === "object") return {
971
+ message: "message" in first && typeof first.message === "string" ? first.message : "Interrupt persistence failed.",
972
+ code: "code" in first && typeof first.code === "string" ? first.code : "server"
973
+ };
974
+ }
975
+ }
976
+ return {
977
+ message: error instanceof Error ? error.message : "Interrupt persistence failed.",
978
+ code: "server"
979
+ };
980
+ }
981
+ buildInterruptRunErrorChunk(error) {
982
+ const failure = this.interruptFailure(error);
983
+ return {
984
+ type: EventType.RUN_ERROR,
985
+ timestamp: Date.now(),
986
+ runId: this.runIdOverride ?? this.requestId,
987
+ threadId: this.threadId,
988
+ message: failure.message,
989
+ code: failure.code,
990
+ error: {
991
+ message: failure.message,
992
+ code: failure.code
993
+ },
994
+ ...failure.errors !== void 0 ? { "tanstack:interruptErrors": failure.errors } : {}
995
+ };
996
+ }
997
+ async *emitInterruptRunError(error) {
998
+ const failure = this.interruptFailure(error);
999
+ this.finalizationError = {
1000
+ message: failure.message,
1001
+ code: failure.code,
1002
+ cause: error
1003
+ };
1004
+ yield* this.pipeThroughMiddleware(this.buildInterruptRunErrorChunk(error));
1005
+ }
1006
+ async *emitActionableInterruptBoundary(finishEvent, approvals, clientRequests) {
1007
+ const terminal = this.completeEphemeralInterruptBindings(this.buildInterruptFinishedChunk(finishEvent, approvals, clientRequests));
1008
+ let terminalOutputs;
1009
+ try {
1010
+ terminalOutputs = await this.middlewareRunner.runOnChunk(this.middlewareCtx, terminal);
1011
+ } catch (error) {
1012
+ yield* this.emitInterruptRunError(error);
1013
+ return false;
1014
+ }
1015
+ yield* this.pipeThroughMiddleware(this.buildMessagesSnapshotChunk());
1016
+ if (this.params.state !== void 0) yield* this.pipeThroughMiddleware({
1017
+ type: EventType.STATE_SNAPSHOT,
1018
+ timestamp: Date.now(),
1019
+ model: this.params.model,
1020
+ snapshot: this.params.state
1021
+ });
1022
+ for (const output of terminalOutputs) {
1023
+ yield this.publicInterruptTerminal(output);
1024
+ this.middlewareCtx.chunkIndex++;
1025
+ }
1026
+ return true;
1027
+ }
1028
+ completeEphemeralInterruptBindings(chunk) {
1029
+ if (chunk.type !== EventType.RUN_FINISHED || chunk.outcome?.type !== "interrupt") return chunk;
1030
+ const interruptedRunId = this.runIdOverride ?? this.requestId;
1031
+ return {
1032
+ ...chunk,
1033
+ outcome: {
1034
+ ...chunk.outcome,
1035
+ interrupts: chunk.outcome.interrupts.map((interrupt) => {
1036
+ if (!interrupt.metadata || typeof interrupt.metadata !== "object" || Array.isArray(interrupt.metadata)) return interrupt;
1037
+ const metadata = { ...interrupt.metadata };
1038
+ const unopened = metadata[interruptBindingMetadataKey];
1039
+ if (unopened === null || typeof unopened !== "object" || Array.isArray(unopened)) return interrupt;
1040
+ metadata[interruptBindingMetadataKey] = {
1041
+ ...unopened,
1042
+ interruptedRunId,
1043
+ generation: 0
1044
+ };
1045
+ return {
1046
+ ...interrupt,
1047
+ metadata
1048
+ };
1049
+ })
1050
+ }
1051
+ };
1052
+ }
1053
+ buildToolResultChunks(results, finishEvent, argsMap) {
1054
+ const chunks = [];
1055
+ for (const result of results) {
1056
+ const content = normalizeToolResult(result.result);
1057
+ const wireContent = typeof content === "string" ? content : JSON.stringify(content);
1058
+ if (argsMap) {
1059
+ chunks.push({
1060
+ type: "TOOL_CALL_START",
1061
+ timestamp: Date.now(),
1062
+ model: finishEvent.model,
1063
+ toolCallId: result.toolCallId,
1064
+ toolCallName: result.toolName,
1065
+ toolName: result.toolName
1066
+ });
1067
+ const args = argsMap.get(result.toolCallId) ?? "{}";
1068
+ chunks.push({
1069
+ type: "TOOL_CALL_ARGS",
1070
+ timestamp: Date.now(),
1071
+ model: finishEvent.model,
1072
+ toolCallId: result.toolCallId,
1073
+ delta: args,
1074
+ args
1075
+ });
1076
+ chunks.push({
1077
+ type: "TOOL_CALL_END",
1078
+ timestamp: Date.now(),
1079
+ model: finishEvent.model,
1080
+ toolCallId: result.toolCallId,
1081
+ toolCallName: result.toolName,
1082
+ toolName: result.toolName,
1083
+ result: wireContent,
1084
+ ...result.input !== void 0 && { input: result.input },
1085
+ ...result.output !== void 0 && { output: result.output },
1086
+ ...result.state !== void 0 && { state: result.state }
1087
+ });
1088
+ }
1089
+ chunks.push({
1090
+ type: "TOOL_CALL_RESULT",
1091
+ timestamp: Date.now(),
1092
+ model: finishEvent.model,
1093
+ messageId: this.createId("tool-result"),
1094
+ toolCallId: result.toolCallId,
1095
+ content: wireContent,
1096
+ role: "tool",
1097
+ ...result.state !== void 0 && { state: result.state }
1098
+ });
1099
+ const placeholderIdx = this.messages.findIndex((m) => {
1100
+ if (m.role !== "tool" || m.toolCallId !== result.toolCallId) return false;
1101
+ if (typeof m.content !== "string") return false;
1102
+ try {
1103
+ return JSON.parse(m.content)?.pendingExecution === true;
1104
+ } catch {
1105
+ return false;
1106
+ }
1107
+ });
1108
+ const newToolMessage = {
1109
+ role: "tool",
1110
+ content,
1111
+ toolCallId: result.toolCallId
1112
+ };
1113
+ if (placeholderIdx >= 0) this.messages = [
1114
+ ...this.messages.slice(0, placeholderIdx),
1115
+ newToolMessage,
1116
+ ...this.messages.slice(placeholderIdx + 1)
1117
+ ];
1118
+ else this.messages = [...this.messages, newToolMessage];
1119
+ this.middlewareCtx.messages = this.messages;
1120
+ }
1121
+ return chunks;
1122
+ }
1123
+ getPendingToolCallsFromMessages() {
1124
+ const completedToolIds = /* @__PURE__ */ new Set();
1125
+ for (const message of this.messages) if (message.role === "tool" && message.toolCallId) {
1126
+ let hasPendingExecution = false;
1127
+ if (typeof message.content === "string") try {
1128
+ if (JSON.parse(message.content).pendingExecution === true) hasPendingExecution = true;
1129
+ } catch {}
1130
+ if (!hasPendingExecution) completedToolIds.add(message.toolCallId);
1131
+ }
1132
+ const pending = [];
1133
+ for (const message of this.messages) if (message.role === "assistant" && message.toolCalls) for (const toolCall of message.toolCalls) {
1134
+ if (isProviderExecutedToolCall(toolCall)) continue;
1135
+ if (!completedToolIds.has(toolCall.id)) pending.push(toolCall);
1136
+ }
1137
+ return pending;
1138
+ }
1139
+ /**
1140
+ * Find a tool call by id in message history (including already-completed ones).
1141
+ * Used when the client has already attached a tool result for UI before resume.
1142
+ */
1143
+ findToolCallInMessages(toolCallId) {
1144
+ for (const message of this.messages) {
1145
+ if (message.role !== "assistant" || !message.toolCalls) continue;
1146
+ for (const toolCall of message.toolCalls) if (toolCall.id === toolCallId) return toolCall;
1147
+ }
1148
+ }
1149
+ /**
1150
+ * Tool calls that must be reconstructed as interrupt pending for ephemeral
1151
+ * resume. Includes outstanding tools plus client tools that already have
1152
+ * results in history when the resume batch still carries `client_tool_*`
1153
+ * entries (the client writes local tool results before submitting resume).
1154
+ */
1155
+ getToolCallsForEphemeralResume(resume) {
1156
+ const pending = this.getPendingToolCallsFromMessages();
1157
+ const byId = new Map(pending.map((toolCall) => [toolCall.id, toolCall]));
1158
+ for (const entry of resume ?? []) {
1159
+ let toolCallId;
1160
+ if (entry.interruptId.startsWith("client_tool_")) toolCallId = entry.interruptId.slice(12);
1161
+ else if (entry.interruptId.startsWith("approval_")) toolCallId = entry.interruptId.slice(9);
1162
+ if (toolCallId === void 0 || byId.has(toolCallId)) continue;
1163
+ const toolCall = this.findToolCallInMessages(toolCallId);
1164
+ if (toolCall && !isProviderExecutedToolCall(toolCall)) {
1165
+ pending.push(toolCall);
1166
+ byId.set(toolCallId, toolCall);
1167
+ }
1168
+ }
1169
+ return pending;
1170
+ }
1171
+ createSyntheticFinishedEvent() {
1172
+ return {
1173
+ type: "RUN_FINISHED",
1174
+ runId: this.createId("pending"),
1175
+ threadId: this.threadId,
1176
+ model: this.params.model,
1177
+ timestamp: Date.now(),
1178
+ finishReason: "tool_calls"
1179
+ };
1180
+ }
1181
+ async shouldContinue() {
1182
+ if (this.cyclePhase === "executeToolCalls") return true;
1183
+ const state = {
1184
+ iterationCount: this.iterationCount,
1185
+ messages: this.messages,
1186
+ finishReason: this.lastFinishReason,
1187
+ toolCallCount: this.toolCallCount,
1188
+ lastTurnToolCallCount: this.lastTurnToolCallCount
1189
+ };
1190
+ const strategyContinues = this.loopStrategy(state);
1191
+ const middlewareContinues = await this.middlewareRunner.runOnShouldContinue(this.middlewareCtx, state);
1192
+ return strategyContinues && middlewareContinues && this.toolPhase === "continue";
1193
+ }
1194
+ /**
1195
+ * Record tool calls (deduped by id) toward `toolCallCount` /
1196
+ * `lastTurnToolCallCount` for strategies and middleware `onShouldContinue`.
1197
+ *
1198
+ * Used for both live model turns and pending/resume batches. IDs already
1199
+ * counted in this run (e.g. wait→resume after a live turn) are not
1200
+ * re-added to `toolCallCount`. Per-turn execution caps are app middleware
1201
+ * (`onBeforeToolCall` skip), not engine policy.
1202
+ */
1203
+ recordToolCalls(toolCalls) {
1204
+ this.lastTurnToolCallCount = toolCalls.length;
1205
+ let newlyCounted = 0;
1206
+ for (const tc of toolCalls) if (!this.countedToolCallIds.has(tc.id)) {
1207
+ this.countedToolCallIds.add(tc.id);
1208
+ newlyCounted++;
1209
+ }
1210
+ this.toolCallCount += newlyCounted;
1211
+ }
1212
+ isAborted() {
1213
+ return !!this.effectiveSignal?.aborted;
1214
+ }
1215
+ isMiddlewareAborted() {
1216
+ return !!this.middlewareAbortController?.signal.aborted;
1217
+ }
1218
+ isCancelled() {
1219
+ return this.isAborted() || this.isMiddlewareAborted();
1220
+ }
1221
+ /**
1222
+ * The reason to report on `AbortInfo` for a cancelled run.
1223
+ *
1224
+ * `this.abortReason` only ever holds a *middleware*-initiated reason
1225
+ * (`ctx.abort(reason)` / `MiddlewareAbortError`). A caller that aborts its own
1226
+ * controller — `abortController.abort(RUN_CANCEL_REASON)`, the in-process
1227
+ * cancel channel — never touches that field, so the reason has to be read back
1228
+ * off the caller's signal, which is the signal `isCancelled()` consults via
1229
+ * `isAborted()`. A signal aborted with no reason carries a DOMException rather
1230
+ * than a string, so non-string reasons are reported as absent.
1231
+ */
1232
+ resolveAbortReason() {
1233
+ if (this.abortReason !== void 0) return this.abortReason;
1234
+ const signalReason = this.effectiveSignal?.reason;
1235
+ return typeof signalReason === "string" ? signalReason : void 0;
1236
+ }
1237
+ /**
1238
+ * Whether this run's teardown declared its abort a DETACH — see
1239
+ * {@link RunDetachedCapability}. Only `withSandbox`'s `onAbort` publishes it,
1240
+ * and only for a plain, intentless disconnect of a detachable run, so every
1241
+ * other exit path answers `false`.
1242
+ *
1243
+ * Surfaced on the engine (rather than the ctx being handed out) so the
1244
+ * capability read stays inside core, and so the delivery sink learns the
1245
+ * verdict through {@link publishRunDetachedSignal} instead of reaching into a
1246
+ * middleware context it has no business holding.
1247
+ *
1248
+ * @internal
1249
+ */
1250
+ wasDetached() {
1251
+ return getRunDetached(this.middlewareCtx, { optional: true }) === true;
1252
+ }
1253
+ /**
1254
+ * The delivery socket closed while this run was still going.
1255
+ *
1256
+ * Notifies every subscriber (see {@link RunDisconnectCapability}) and RETURNS
1257
+ * IMMEDIATELY. Synchronous on purpose: it is called from
1258
+ * `ReadableStream.cancel()`, which must not be made to wait on a run-store
1259
+ * write, and the caller ({@link notifyRunDisconnected}) has no consumer left to
1260
+ * report to anyway.
1261
+ *
1262
+ * Subscribers therefore run CONCURRENTLY with the still-executing run — which is
1263
+ * the entire point. The run is typically suspended inside a slow middleware
1264
+ * `setup` at this moment, so anything dispatched from the run's own unwinding
1265
+ * would be minutes late. Nothing on this path aborts the run: a durable run
1266
+ * outlives its viewer.
1267
+ *
1268
+ * Each subscriber's promise is parked on `deferredPromises`, which the run awaits
1269
+ * in its `finally`, so bookkeeping cannot be lost to a race with the run's own
1270
+ * completion even though nothing awaits it here.
1271
+ *
1272
+ * IDEMPOTENT. A second cancel, or one arriving after a terminal hook already ran,
1273
+ * is ignored: the terminal hooks own the run's outcome, and re-stamping
1274
+ * `detachedSince` on a run that has already finished would hand a completed run
1275
+ * to the reaper as reclaimable work.
1276
+ *
1277
+ * @internal
1278
+ */
1279
+ notifyDisconnected() {
1280
+ if (this.disconnected || this.terminalHookCalled) return;
1281
+ this.disconnected = true;
1282
+ for (const listener of this.disconnectListeners) this.runDisconnectListener(listener);
1283
+ }
1284
+ /**
1285
+ * Invoke one disconnect listener, isolated and with its failure SWALLOWED after
1286
+ * logging.
1287
+ *
1288
+ * There is no caller left to report to — the socket this would report on is the
1289
+ * one that just closed — and a rejection parked on `deferredPromises` would
1290
+ * surface as the run's failure, replacing a healthy outcome with a bookkeeping
1291
+ * error. Isolation matters for the usual reason too: one subscriber's failing
1292
+ * write must not skip the next one's.
1293
+ */
1294
+ runDisconnectListener(listener) {
1295
+ let result;
1296
+ try {
1297
+ result = listener();
1298
+ } catch (error) {
1299
+ this.logger.errors("run disconnect listener failed", { error });
1300
+ return;
1301
+ }
1302
+ if (result === void 0) return;
1303
+ this.deferredPromises.push(result.catch((error) => {
1304
+ this.logger.errors("run disconnect listener failed", { error });
1305
+ }));
1306
+ }
1307
+ /**
1308
+ * Run the final structured-output adapter call through the middleware
1309
+ * pipeline. Yields chunks to the caller only when
1310
+ * `this.finalStructuredOutput.yieldChunks` is true; otherwise consumes
1311
+ * silently while still piping through middleware.
1312
+ *
1313
+ * On success, populates this.structuredOutputResult.
1314
+ * On failure, populates this.finalizationError.
1315
+ */
1316
+ async *runStructuredFinalization() {
1317
+ if (!this.finalStructuredOutput) throw new Error("runStructuredFinalization called without finalStructuredOutput config");
1318
+ this.middlewareCtx.phase = "structuredOutput";
1319
+ const baseConfig = this.buildMiddlewareConfig();
1320
+ const { tools: _omitTools, ...baseWithoutTools } = baseConfig;
1321
+ let structuredConfig = {
1322
+ ...baseWithoutTools,
1323
+ outputSchema: this.finalStructuredOutput.jsonSchema
1324
+ };
1325
+ structuredConfig = await this.middlewareRunner.runOnStructuredOutputConfig(this.middlewareCtx, structuredConfig);
1326
+ const { outputSchema: pinnedSchema, ...chatConfigSlice } = structuredConfig;
1327
+ const postOnConfig = await this.middlewareRunner.runOnConfig(this.middlewareCtx, {
1328
+ ...chatConfigSlice,
1329
+ tools: baseConfig.tools
1330
+ });
1331
+ this.applyMiddlewareConfig(postOnConfig);
1332
+ const structuredCallOptions = {
1333
+ chatOptions: {
1334
+ model: this.params.model,
1335
+ messages: this.messages,
1336
+ metadata: postOnConfig.metadata,
1337
+ modelOptions: postOnConfig.modelOptions,
1338
+ systemPrompts: postOnConfig.systemPrompts,
1339
+ logger: this.logger,
1340
+ threadId: this.threadId,
1341
+ runId: this.runIdOverride,
1342
+ parentRunId: this.parentRunIdOverride,
1343
+ ...this.effectiveRequest ? { request: this.effectiveRequest } : {}
1344
+ },
1345
+ outputSchema: pinnedSchema
1346
+ };
1347
+ let fallbackAdapterError = void 0;
1348
+ const providerStream = this.adapter.structuredOutputStream ? this.adapter.structuredOutputStream(structuredCallOptions) : fallbackStructuredOutputStream(this.adapter, structuredCallOptions, (err) => {
1349
+ fallbackAdapterError = err;
1350
+ });
1351
+ let startEmitted = false;
1352
+ let structuredMessageId = null;
1353
+ const extractMessageId = (c) => {
1354
+ if (c.type === EventType.TEXT_MESSAGE_START || c.type === EventType.TEXT_MESSAGE_CONTENT || c.type === EventType.TEXT_MESSAGE_END) return typeof c.messageId === "string" && c.messageId !== "" ? c.messageId : null;
1355
+ return null;
1356
+ };
1357
+ const buildSynthesizedStart = () => {
1358
+ const idForStart = structuredMessageId ?? generateMessageId();
1359
+ structuredMessageId = idForStart;
1360
+ return {
1361
+ type: EventType.CUSTOM,
1362
+ name: "structured-output.start",
1363
+ value: { messageId: idForStart },
1364
+ model: this.params.model,
1365
+ timestamp: Date.now(),
1366
+ threadId: this.threadId,
1367
+ ...this.runIdOverride ? { runId: this.runIdOverride } : {}
1368
+ };
1369
+ };
1370
+ const pipeThroughMiddleware = async (synthChunk) => this.middlewareRunner.runOnChunk(this.middlewareCtx, synthChunk);
1371
+ let runErrorYielded = false;
1372
+ for await (const chunk of providerStream) {
1373
+ if (this.isCancelled()) break;
1374
+ if (!startEmitted && chunk.type === EventType.CUSTOM && chunk.name === "structured-output.start") startEmitted = true;
1375
+ if (!structuredMessageId) {
1376
+ const extracted = extractMessageId(chunk);
1377
+ if (extracted) structuredMessageId = extracted;
1378
+ }
1379
+ if (this.finalStructuredOutput.yieldChunks) {
1380
+ if (!startEmitted && (chunk.type === EventType.TEXT_MESSAGE_START || chunk.type === EventType.TEXT_MESSAGE_CONTENT || chunk.type === EventType.TEXT_MESSAGE_END)) {
1381
+ startEmitted = true;
1382
+ const synthOutputs = await pipeThroughMiddleware(buildSynthesizedStart());
1383
+ for (const outputChunk of synthOutputs) {
1384
+ yield outputChunk;
1385
+ this.middlewareCtx.chunkIndex++;
1386
+ }
1387
+ }
1388
+ if (!startEmitted && chunk.type === EventType.RUN_ERROR) {
1389
+ startEmitted = true;
1390
+ const synthOutputs = await pipeThroughMiddleware(buildSynthesizedStart());
1391
+ for (const outputChunk of synthOutputs) {
1392
+ yield outputChunk;
1393
+ this.middlewareCtx.chunkIndex++;
1394
+ }
1395
+ }
1396
+ }
1397
+ let outboundChunk = chunk;
1398
+ if (chunk.type === EventType.CUSTOM && chunk.name === "structured-output.complete") {
1399
+ const parsed = readStructuredOutputCompleteValue(chunk.value);
1400
+ if (parsed) {
1401
+ const object = this.finalStructuredOutput.normalize ? this.finalStructuredOutput.normalize(parsed.object) : parsed.object;
1402
+ this.structuredOutputResult = {
1403
+ data: object,
1404
+ rawText: parsed.raw
1405
+ };
1406
+ const value = chunk.value;
1407
+ if (object !== parsed.object && value && typeof value === "object") outboundChunk = {
1408
+ ...chunk,
1409
+ value: {
1410
+ ...value,
1411
+ object
1412
+ }
1413
+ };
1414
+ }
1415
+ }
1416
+ if (chunk.type === EventType.RUN_FINISHED && chunk.usage) await this.middlewareRunner.runOnUsage(this.middlewareCtx, chunk.usage);
1417
+ if (chunk.type === EventType.RUN_ERROR) this.finalizationError = {
1418
+ message: chunk.message,
1419
+ ...chunk.code ? { code: chunk.code } : {},
1420
+ ...fallbackAdapterError !== void 0 ? { cause: fallbackAdapterError } : {}
1421
+ };
1422
+ const outputChunks = await this.middlewareRunner.runOnChunk(this.middlewareCtx, outboundChunk);
1423
+ if (this.finalStructuredOutput.yieldChunks) for (const outputChunk of outputChunks) {
1424
+ if (outputChunk.type === EventType.RUN_ERROR) runErrorYielded = true;
1425
+ yield outputChunk;
1426
+ this.middlewareCtx.chunkIndex++;
1427
+ }
1428
+ if (this.finalizationError) break;
1429
+ }
1430
+ if (this.isCancelled()) return;
1431
+ if (!this.structuredOutputResult && !this.finalizationError) this.finalizationError = {
1432
+ message: "missing structured result",
1433
+ code: "structured-output-missing-result"
1434
+ };
1435
+ if (this.structuredOutputResult && !this.finalizationError && this.finalStructuredOutput.validate) try {
1436
+ const validated = this.finalStructuredOutput.validate(this.structuredOutputResult.data);
1437
+ this.validatedStructuredOutput = validated;
1438
+ this.hasValidatedStructuredOutput = true;
1439
+ } catch (err) {
1440
+ const message = err instanceof Error ? err.message : String(err);
1441
+ this.finalizationError = {
1442
+ message,
1443
+ code: "structured-output-validation-failed",
1444
+ cause: err
1445
+ };
1446
+ }
1447
+ if (this.finalizationError && this.finalStructuredOutput.yieldChunks && !runErrorYielded) {
1448
+ if (!startEmitted) {
1449
+ const startOutputs = await pipeThroughMiddleware(buildSynthesizedStart());
1450
+ for (const outputChunk of startOutputs) {
1451
+ yield outputChunk;
1452
+ this.middlewareCtx.chunkIndex++;
1453
+ }
1454
+ startEmitted = true;
1455
+ }
1456
+ const errChunk = {
1457
+ type: EventType.RUN_ERROR,
1458
+ runId: this.runIdOverride ?? this.requestId,
1459
+ model: this.params.model,
1460
+ timestamp: Date.now(),
1461
+ threadId: this.threadId,
1462
+ message: this.finalizationError.message,
1463
+ ...this.finalizationError.code ? { code: this.finalizationError.code } : {},
1464
+ error: {
1465
+ message: this.finalizationError.message,
1466
+ ...this.finalizationError.code ? { code: this.finalizationError.code } : {}
1467
+ }
1468
+ };
1469
+ const outputChunks = await this.middlewareRunner.runOnChunk(this.middlewareCtx, errChunk);
1470
+ for (const outputChunk of outputChunks) {
1471
+ yield outputChunk;
1472
+ this.middlewareCtx.chunkIndex++;
1473
+ }
1474
+ }
1475
+ }
1476
+ /**
1477
+ * Native combined mode: harvest the structured output from the agent
1478
+ * loop's accumulated final-turn text (no separate provider call).
1479
+ *
1480
+ * The adapter wired `outputSchema` into the regular `chatStream` request,
1481
+ * so the model's final-turn text is the schema-constrained JSON. We parse
1482
+ * `this.accumulatedContent`, populate `this.structuredOutputResult`, emit
1483
+ * a synthetic `structured-output.complete` (and a `structured-output.start`
1484
+ * if one wasn't emitted earlier — only happens on the streaming path when
1485
+ * the model returned no text at all), and run the validate callback when
1486
+ * present. Failures populate `this.finalizationError` so the engine's
1487
+ * terminal-hook chooser routes to `onError` (per spec §7.3).
1488
+ *
1489
+ * The `'structuredOutput'` middleware phase intentionally does NOT fire on
1490
+ * this path — middleware sees the run through `beforeModel` / `modelStream`
1491
+ * as usual. See PR #605 / issue #605 for the design rationale.
1492
+ */
1493
+ async *harvestCombinedStructuredOutput() {
1494
+ if (!this.finalStructuredOutput) throw new Error("harvestCombinedStructuredOutput called without finalStructuredOutput config");
1495
+ const yieldChunks = this.finalStructuredOutput.yieldChunks;
1496
+ const rawText = this.accumulatedContent;
1497
+ if (rawText.length === 0) this.finalizationError = {
1498
+ message: "missing structured result",
1499
+ code: "structured-output-missing-result"
1500
+ };
1501
+ else try {
1502
+ const parsed = JSON.parse(rawText);
1503
+ const data = this.finalStructuredOutput.normalize ? this.finalStructuredOutput.normalize(parsed) : parsed;
1504
+ this.structuredOutputResult = {
1505
+ data,
1506
+ rawText
1507
+ };
1508
+ } catch (err) {
1509
+ const detail = rawText.slice(0, 200) + (rawText.length > 200 ? "..." : "");
1510
+ this.finalizationError = {
1511
+ message: `Failed to parse structured output as JSON. Content: ${detail}`,
1512
+ code: "structured-output-parse-failed",
1513
+ cause: err
1514
+ };
1515
+ }
1516
+ if (this.structuredOutputResult && !this.finalizationError && this.finalStructuredOutput.validate) try {
1517
+ const validated = this.finalStructuredOutput.validate(this.structuredOutputResult.data);
1518
+ this.validatedStructuredOutput = validated;
1519
+ this.hasValidatedStructuredOutput = true;
1520
+ } catch (err) {
1521
+ const message = err instanceof Error ? err.message : String(err);
1522
+ this.finalizationError = {
1523
+ message,
1524
+ code: "structured-output-validation-failed",
1525
+ cause: err
1526
+ };
1527
+ }
1528
+ if (!yieldChunks) return;
1529
+ if (!this.combinedStartEmitted) {
1530
+ this.combinedStartEmitted = true;
1531
+ const messageId = this.combinedStructuredMessageId ?? generateMessageId();
1532
+ this.combinedStructuredMessageId = messageId;
1533
+ const synthStart = {
1534
+ type: EventType.CUSTOM,
1535
+ name: "structured-output.start",
1536
+ value: { messageId },
1537
+ model: this.params.model,
1538
+ timestamp: Date.now(),
1539
+ threadId: this.threadId,
1540
+ ...this.runIdOverride ? { runId: this.runIdOverride } : {}
1541
+ };
1542
+ const startOutputs = await this.middlewareRunner.runOnChunk(this.middlewareCtx, synthStart);
1543
+ for (const outputChunk of startOutputs) {
1544
+ yield outputChunk;
1545
+ this.middlewareCtx.chunkIndex++;
1546
+ }
1547
+ }
1548
+ if (this.structuredOutputResult && !this.finalizationError) {
1549
+ const completeChunk = {
1550
+ type: EventType.CUSTOM,
1551
+ name: "structured-output.complete",
1552
+ value: {
1553
+ object: this.structuredOutputResult.data,
1554
+ raw: this.structuredOutputResult.rawText,
1555
+ ...this.combinedStructuredMessageId ? { messageId: this.combinedStructuredMessageId } : {}
1556
+ },
1557
+ model: this.params.model,
1558
+ timestamp: Date.now(),
1559
+ threadId: this.threadId,
1560
+ ...this.runIdOverride ? { runId: this.runIdOverride } : {}
1561
+ };
1562
+ const completeOutputs = await this.middlewareRunner.runOnChunk(this.middlewareCtx, completeChunk);
1563
+ for (const outputChunk of completeOutputs) {
1564
+ yield outputChunk;
1565
+ this.middlewareCtx.chunkIndex++;
1566
+ }
1567
+ }
1568
+ if (this.finalizationError) {
1569
+ const errChunk = {
1570
+ type: EventType.RUN_ERROR,
1571
+ runId: this.runIdOverride ?? this.requestId,
1572
+ model: this.params.model,
1573
+ timestamp: Date.now(),
1574
+ threadId: this.threadId,
1575
+ message: this.finalizationError.message,
1576
+ ...this.finalizationError.code ? { code: this.finalizationError.code } : {},
1577
+ error: {
1578
+ message: this.finalizationError.message,
1579
+ ...this.finalizationError.code ? { code: this.finalizationError.code } : {}
1580
+ }
1581
+ };
1582
+ const errOutputs = await this.middlewareRunner.runOnChunk(this.middlewareCtx, errChunk);
1583
+ for (const outputChunk of errOutputs) {
1584
+ yield outputChunk;
1585
+ this.middlewareCtx.chunkIndex++;
1586
+ }
1587
+ }
1588
+ }
1589
+ buildMiddlewareConfig() {
1590
+ return {
1591
+ messages: this.messages,
1592
+ systemPrompts: [...this.systemPrompts],
1593
+ tools: [...this.tools],
1594
+ resume: this.params.resume,
1595
+ resumeToolState: {
1596
+ approvals: this.resumeApprovals,
1597
+ clientToolResults: this.resumeClientToolResults,
1598
+ deniedToolResults: this.resumeDeniedToolResults,
1599
+ cancelledToolCallIds: this.resumeCancelledToolCallIds
1600
+ },
1601
+ metadata: this.params.metadata,
1602
+ modelOptions: this.params.modelOptions
1603
+ };
1604
+ }
1605
+ async applyEphemeralInterruptResume(config) {
1606
+ if ((config.resume?.length ?? 0) === 0) return;
1607
+ const interruptedRunId = this.parentRunIdOverride;
1608
+ if (!interruptedRunId) throw new InterruptResumeValidationError([{
1609
+ scope: "batch",
1610
+ threadId: this.threadId,
1611
+ interruptedRunId: this.runIdOverride ?? this.requestId,
1612
+ generation: 0,
1613
+ interruptIds: config.resume?.map((entry) => entry.interruptId) ?? [],
1614
+ code: "stale",
1615
+ message: "Interrupt continuation requires parentRunId to identify the interrupted run.",
1616
+ source: "server",
1617
+ retryable: false
1618
+ }]);
1619
+ const approvalRequests = [];
1620
+ const clientRequests = [];
1621
+ const pendingToolCalls = this.getToolCallsForEphemeralResume(config.resume);
1622
+ const resumeInterruptIds = new Set(config.resume?.map((entry) => entry.interruptId));
1623
+ const toolInputs = /* @__PURE__ */ new Map();
1624
+ const toolsByCallId = /* @__PURE__ */ new Map();
1625
+ const clientExecutionCallIds = /* @__PURE__ */ new Set();
1626
+ for (const toolCall of pendingToolCalls) {
1627
+ const tool = this.tools.find((candidate) => candidate.name === toolCall.function.name);
1628
+ if (!tool) continue;
1629
+ toolsByCallId.set(toolCall.id, tool);
1630
+ let input = {};
1631
+ try {
1632
+ const parsed = JSON.parse(toolCall.function.arguments.trim() || "{}");
1633
+ input = parsed && typeof parsed === "object" ? parsed : {};
1634
+ } catch {
1635
+ input = {};
1636
+ }
1637
+ toolInputs.set(toolCall.id, input);
1638
+ if (!tool.execute && resumeInterruptIds.has(`client_tool_${toolCall.id}`)) clientExecutionCallIds.add(toolCall.id);
1639
+ }
1640
+ for (const toolCall of pendingToolCalls) if (toolsByCallId.get(toolCall.id)?.needsApproval && !clientExecutionCallIds.has(toolCall.id)) approvalRequests.push({
1641
+ toolCallId: toolCall.id,
1642
+ toolName: toolCall.function.name,
1643
+ input: toolInputs.get(toolCall.id) ?? {},
1644
+ approvalId: `approval_${toolCall.id}`
1645
+ });
1646
+ for (const toolCall of pendingToolCalls) {
1647
+ const tool = toolsByCallId.get(toolCall.id);
1648
+ if (tool !== void 0 && !tool.execute && (!tool.needsApproval || clientExecutionCallIds.has(toolCall.id))) clientRequests.push({
1649
+ toolCallId: toolCall.id,
1650
+ toolName: toolCall.function.name,
1651
+ input: toolInputs.get(toolCall.id) ?? {}
1652
+ });
1653
+ }
1654
+ const pending = this.buildActionableInterrupts(approvalRequests, clientRequests).flatMap((descriptor) => {
1655
+ const unopened = readUnopenedInterruptBinding(descriptor);
1656
+ return unopened ? [{
1657
+ interruptId: descriptor.id,
1658
+ payload: descriptor,
1659
+ binding: {
1660
+ ...unopened,
1661
+ interruptedRunId,
1662
+ generation: 0
1663
+ }
1664
+ }] : [];
1665
+ });
1666
+ const validated = await validateInterruptResumeBatch({
1667
+ threadId: this.threadId,
1668
+ interruptedRunId,
1669
+ generation: 0,
1670
+ pending,
1671
+ resume: config.resume,
1672
+ tools: this.tools
1673
+ });
1674
+ if (validated.errors.length > 0 || !validated.resumeToolState) throw new InterruptResumeValidationError(validated.errors);
1675
+ const approvals = new Map(validated.resumeToolState.approvals);
1676
+ for (const request of clientRequests) if (toolsByCallId.get(request.toolCallId)?.needsApproval) approvals.set(request.toolCallId, true);
1677
+ this.applyResumeToolState({
1678
+ ...validated.resumeToolState,
1679
+ approvals
1680
+ });
1681
+ }
1682
+ applyResumeToolState(state) {
1683
+ if (state?.approvals) for (const [approvalId, resolution] of state.approvals) this.resumeApprovals.set(approvalId, resolution);
1684
+ if (state?.clientToolResults) for (const [toolCallId, result] of state.clientToolResults) this.resumeClientToolResults.set(toolCallId, result);
1685
+ if (state?.deniedToolResults) for (const [toolCallId, result] of state.deniedToolResults) this.resumeDeniedToolResults.set(toolCallId, result);
1686
+ if (state?.cancelledToolCallIds) for (const toolCallId of state.cancelledToolCallIds) this.resumeCancelledToolCallIds.add(toolCallId);
1687
+ }
1688
+ applyMiddlewareConfig(config) {
1689
+ this.applyResumeToolState(config.resumeToolState);
1690
+ this.messages = config.messages;
1691
+ this.systemPrompts = config.systemPrompts;
1692
+ this.tools = config.tools;
1693
+ this.params = {
1694
+ ...this.params,
1695
+ metadata: config.metadata,
1696
+ modelOptions: config.modelOptions
1697
+ };
1698
+ this.middlewareCtx.messages = this.messages;
1699
+ this.middlewareCtx.systemPrompts = this.systemPrompts;
1700
+ this.middlewareCtx.hasTools = this.tools.length > 0;
1701
+ this.middlewareCtx.toolNames = this.tools.map((t) => t.name);
1702
+ this.middlewareCtx.modelOptions = config.modelOptions;
1703
+ }
1704
+ setToolPhase(phase) {
1705
+ this.toolPhase = phase;
1706
+ }
1707
+ /**
1708
+ * Pipe a single chunk through the middleware pipeline (strip-to-spec, devtools, etc.)
1709
+ * and yield all resulting output chunks.
1710
+ */
1711
+ async *pipeThroughMiddleware(chunk) {
1712
+ const outputChunks = await this.middlewareRunner.runOnChunk(this.middlewareCtx, chunk);
1713
+ for (const outputChunk of outputChunks) {
1714
+ yield outputChunk;
1715
+ this.middlewareCtx.chunkIndex++;
1716
+ }
1717
+ }
1718
+ /**
1719
+ * Drain queued `sandbox.file` chunks (emitted via the SandboxRuntime sink)
1720
+ * through the middleware pipeline and into the public stream.
1721
+ */
1722
+ async *drainSandboxFileQueue() {
1723
+ while (this.sandboxFileQueue.length > 0) {
1724
+ const chunk = this.sandboxFileQueue.shift();
1725
+ if (chunk) yield* this.pipeThroughMiddleware(chunk);
1726
+ }
1727
+ }
1728
+ /**
1729
+ * Drain an executeToolCalls async generator, yielding any CustomEvent chunks
1730
+ * through the middleware pipeline and returning the final ExecuteToolCallsResult.
1731
+ */
1732
+ async *drainToolCallGenerator(generator) {
1733
+ let next = await generator.next();
1734
+ while (!next.done) {
1735
+ yield* this.pipeThroughMiddleware(next.value);
1736
+ next = await generator.next();
1737
+ }
1738
+ return next.value;
1739
+ }
1740
+ createCustomEventChunk(eventName, value) {
1741
+ return {
1742
+ type: "CUSTOM",
1743
+ timestamp: Date.now(),
1744
+ model: this.params.model,
1745
+ name: eventName,
1746
+ value
1747
+ };
1748
+ }
1749
+ createId(prefix) {
1750
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
1751
+ }
1752
+ };
1753
+ /**
1754
+ * Text activity - handles agentic text generation, one-shot text generation, and agentic structured output.
1755
+ *
1756
+ * This activity supports four modes:
1757
+ * 1. **Streaming agentic text**: Stream responses with automatic tool execution
1758
+ * 2. **Streaming one-shot text**: Simple streaming request/response without tools
1759
+ * 3. **Non-streaming text**: Returns collected text as a string (stream: false)
1760
+ * 4. **Agentic structured output**: Run tools, then return structured data
1761
+ *
1762
+ * @example Full agentic text (streaming with tools)
1763
+ * ```ts
1764
+ * import { chat } from '@tanstack/ai'
1765
+ * import { openaiText } from '@tanstack/ai-openai'
1766
+ *
1767
+ * for await (const chunk of chat({
1768
+ * adapter: openaiText('gpt-5.5'),
1769
+ * messages: [{ role: 'user', content: 'What is the weather?' }],
1770
+ * tools: [weatherTool]
1771
+ * })) {
1772
+ * if (chunk.type === 'TEXT_MESSAGE_CONTENT') {
1773
+ * console.log(chunk.delta)
1774
+ * }
1775
+ * }
1776
+ * ```
1777
+ *
1778
+ * @example One-shot text (streaming without tools)
1779
+ * ```ts
1780
+ * for await (const chunk of chat({
1781
+ * adapter: openaiText('gpt-5.5'),
1782
+ * messages: [{ role: 'user', content: 'Hello!' }]
1783
+ * })) {
1784
+ * console.log(chunk)
1785
+ * }
1786
+ * ```
1787
+ *
1788
+ * @example Non-streaming text (stream: false)
1789
+ * ```ts
1790
+ * const text = await chat({
1791
+ * adapter: openaiText('gpt-5.5'),
1792
+ * messages: [{ role: 'user', content: 'Hello!' }],
1793
+ * stream: false
1794
+ * })
1795
+ * // text is a string with the full response
1796
+ * ```
1797
+ *
1798
+ * @example Agentic structured output (tools + structured response)
1799
+ * ```ts
1800
+ * import { z } from 'zod'
1801
+ *
1802
+ * const result = await chat({
1803
+ * adapter: openaiText('gpt-5.5'),
1804
+ * messages: [{ role: 'user', content: 'Research and summarize the topic' }],
1805
+ * tools: [researchTool, analyzeTool],
1806
+ * outputSchema: z.object({
1807
+ * summary: z.string(),
1808
+ * keyPoints: z.array(z.string())
1809
+ * })
1810
+ * })
1811
+ * // result is { summary: string, keyPoints: string[] }
1812
+ * ```
1813
+ */
1558
1814
  function chat(options) {
1559
- validateCapabilities(options.middleware ?? [], options.adapter);
1560
- const { outputSchema, stream } = options;
1561
- if (outputSchema && stream === true) {
1562
- return runStreamingStructuredOutput({
1563
- ...options,
1564
- outputSchema,
1565
- stream
1566
- });
1567
- }
1568
- if (outputSchema) {
1569
- return runAgenticStructuredOutput({
1570
- ...options,
1571
- outputSchema
1572
- });
1573
- }
1574
- if (stream === false) {
1575
- return runNonStreamingText({
1576
- ...options,
1577
- outputSchema: void 0,
1578
- stream
1579
- });
1580
- }
1581
- return runStreamingText({
1582
- ...options,
1583
- outputSchema: void 0,
1584
- stream
1585
- });
1815
+ validateCapabilities(options.middleware ?? [], options.adapter);
1816
+ const { outputSchema, stream } = options;
1817
+ if (outputSchema && stream === true) return runStreamingStructuredOutput({
1818
+ ...options,
1819
+ outputSchema,
1820
+ stream
1821
+ });
1822
+ if (outputSchema) return runAgenticStructuredOutput({
1823
+ ...options,
1824
+ outputSchema
1825
+ });
1826
+ if (stream === false) return runNonStreamingText({
1827
+ ...options,
1828
+ outputSchema: void 0,
1829
+ stream
1830
+ });
1831
+ return runStreamingText({
1832
+ ...options,
1833
+ outputSchema: void 0,
1834
+ stream
1835
+ });
1836
+ }
1837
+ /**
1838
+ * Publish both delivery-side seams for `stream`.
1839
+ *
1840
+ * Shared by the two streaming paths so they cannot drift apart — the
1841
+ * structured-output path having been wired for one seam and not the other is
1842
+ * exactly the bug `publishRunDetachedSignal` picked up last time (a durable
1843
+ * `chat({ outputSchema, stream: true })` could never detach).
1844
+ */
1845
+ function publishDeliverySeams(stream, engineRef) {
1846
+ publishRunDetachedSignal(stream, () => engineRef.current?.wasDetached() === true);
1847
+ publishRunDisconnectHandler(stream, () => {
1848
+ engineRef.current?.notifyDisconnected();
1849
+ });
1586
1850
  }
1587
- async function* runStreamingText(options) {
1588
- const { adapter, middleware, context, debug, mcp, ...textOptions } = options;
1589
- const model = adapter.model;
1590
- const logger = resolveDebugOption(debug);
1591
- const mcpManager = MCPManager.from(mcp);
1592
- const mcpTools = await mcpManager.discover();
1593
- if (mcpTools.length > 0) {
1594
- textOptions.tools = [...textOptions.tools ?? [], ...mcpTools];
1595
- }
1596
- const engine = new TextEngine(
1597
- {
1598
- adapter,
1599
- params: { ...textOptions, model, logger },
1600
- middleware,
1601
- context
1602
- },
1603
- logger
1604
- );
1605
- try {
1606
- for await (const chunk of engine.run()) {
1607
- yield chunk;
1608
- }
1609
- } finally {
1610
- await mcpManager.dispose();
1611
- }
1851
+ /**
1852
+ * Run streaming text (agentic or one-shot depending on tools).
1853
+ *
1854
+ * A thin, NON-generator wrapper, because the stream object is also the key the
1855
+ * durable delivery sink looks the run's detach verdict up under (see
1856
+ * `../../delivery-detach`) and delivers its disconnect notification through (see
1857
+ * `../../delivery-disconnect`). A generator function cannot reach the generator it
1858
+ * returns, so the identity has to be minted out here and the engine reached back
1859
+ * through `engineRef`, which the body fills as soon as its engine exists.
1860
+ */
1861
+ function runStreamingText(options) {
1862
+ const engineRef = {};
1863
+ const stream = streamTextChunks(options, engineRef);
1864
+ publishDeliverySeams(stream, engineRef);
1865
+ return stream;
1612
1866
  }
1867
+ async function* streamTextChunks(options, engineRef) {
1868
+ const { adapter, middleware, context, debug, mcp, ...textOptions } = options;
1869
+ const model = adapter.model;
1870
+ const logger = resolveDebugOption(debug);
1871
+ const mcpManager = MCPManager.from(mcp);
1872
+ const mcpTools = await mcpManager.discover();
1873
+ if (mcpTools.length > 0) textOptions.tools = [...textOptions.tools ?? [], ...mcpTools];
1874
+ const engine = new TextEngine({
1875
+ adapter,
1876
+ params: {
1877
+ ...textOptions,
1878
+ model,
1879
+ logger
1880
+ },
1881
+ middleware,
1882
+ context
1883
+ }, logger);
1884
+ engineRef.current = engine;
1885
+ try {
1886
+ for await (const chunk of engine.run()) yield chunk;
1887
+ } finally {
1888
+ await mcpManager.dispose();
1889
+ }
1890
+ }
1891
+ /**
1892
+ * Run non-streaming text - collects all content and returns as a string.
1893
+ * Runs the full agentic loop (if tools are provided) but returns collected text.
1894
+ */
1613
1895
  function runNonStreamingText(options) {
1614
- const stream = runStreamingText(
1615
- // eslint-disable-next-line no-restricted-syntax -- generic-stream remap: caller is non-streaming (false), but runStreamingText is invoked internally to collect text; concrete `false`→`true` literals don't structurally overlap.
1616
- options
1617
- );
1618
- return streamToText(stream);
1896
+ return streamToText(runStreamingText(options));
1619
1897
  }
1898
+ /**
1899
+ * Run agentic structured output:
1900
+ * 1. Execute the full agentic loop (with tools)
1901
+ * 2. Once complete, call adapter.structuredOutput with the conversation context
1902
+ * 3. Validate and return the structured result
1903
+ */
1620
1904
  async function runAgenticStructuredOutput(options) {
1621
- const {
1622
- adapter,
1623
- outputSchema,
1624
- middleware,
1625
- context,
1626
- debug,
1627
- mcp,
1628
- ...textOptions
1629
- } = options;
1630
- const model = adapter.model;
1631
- const logger = resolveDebugOption(debug);
1632
- if (!outputSchema) {
1633
- throw new Error("outputSchema is required for structured output");
1634
- }
1635
- const { jsonSchema, nullWideningMap } = convertSchemaForStructuredOutput(outputSchema);
1636
- if (!jsonSchema) {
1637
- throw new Error("Failed to convert output schema to JSON Schema");
1638
- }
1639
- const normalize = (data) => undoNullWidening(data, nullWideningMap);
1640
- const validate = isStandardSchema(outputSchema) ? (data) => parseWithStandardSchema(outputSchema, data) : void 0;
1641
- const nativeCombined = adapter.supportsCombinedToolsAndSchema?.(options.modelOptions) === true;
1642
- const mcpManager = MCPManager.from(mcp);
1643
- const mcpTools = await mcpManager.discover();
1644
- if (mcpTools.length > 0) {
1645
- textOptions.tools = [...textOptions.tools ?? [], ...mcpTools];
1646
- }
1647
- const engine = new TextEngine(
1648
- {
1649
- adapter,
1650
- params: { ...textOptions, model, logger },
1651
- middleware,
1652
- context,
1653
- finalStructuredOutput: {
1654
- jsonSchema,
1655
- yieldChunks: false,
1656
- normalize,
1657
- ...validate ? { validate } : {},
1658
- ...nativeCombined ? { nativeCombined: true } : {}
1659
- }
1660
- },
1661
- logger
1662
- );
1663
- try {
1664
- for await (const _chunk of engine.run()) {
1665
- }
1666
- } finally {
1667
- await mcpManager.dispose();
1668
- }
1669
- const finalizationError = engine.getFinalizationError();
1670
- if (finalizationError) {
1671
- const err = new Error(
1672
- finalizationError.message,
1673
- finalizationError.cause !== void 0 ? { cause: finalizationError.cause } : void 0
1674
- );
1675
- if (finalizationError.code !== void 0) {
1676
- Object.defineProperty(err, "code", {
1677
- value: finalizationError.code,
1678
- enumerable: true
1679
- });
1680
- }
1681
- throw err;
1682
- }
1683
- const validated = engine.getValidatedStructuredOutput();
1684
- if (validated) {
1685
- return validated.value;
1686
- }
1687
- const result = engine.getStructuredOutputResult();
1688
- if (!result) {
1689
- throw new Error("structured output finalization produced no result");
1690
- }
1691
- return result.data;
1905
+ const { adapter, outputSchema, middleware, context, debug, mcp, ...textOptions } = options;
1906
+ const model = adapter.model;
1907
+ const logger = resolveDebugOption(debug);
1908
+ if (!outputSchema) throw new Error("outputSchema is required for structured output");
1909
+ const { jsonSchema, nullWideningMap } = convertSchemaForStructuredOutput(outputSchema);
1910
+ if (!jsonSchema) throw new Error("Failed to convert output schema to JSON Schema");
1911
+ const normalize = (data) => undoNullWidening(data, nullWideningMap);
1912
+ const validate = isStandardSchema(outputSchema) ? (data) => parseWithStandardSchema(outputSchema, data) : void 0;
1913
+ const nativeCombined = adapter.supportsCombinedToolsAndSchema?.(options.modelOptions) === true;
1914
+ const mcpManager = MCPManager.from(mcp);
1915
+ const mcpTools = await mcpManager.discover();
1916
+ if (mcpTools.length > 0) textOptions.tools = [...textOptions.tools ?? [], ...mcpTools];
1917
+ const engine = new TextEngine({
1918
+ adapter,
1919
+ params: {
1920
+ ...textOptions,
1921
+ model,
1922
+ logger
1923
+ },
1924
+ middleware,
1925
+ context,
1926
+ finalStructuredOutput: {
1927
+ jsonSchema,
1928
+ yieldChunks: false,
1929
+ normalize,
1930
+ ...validate ? { validate } : {},
1931
+ ...nativeCombined ? { nativeCombined: true } : {}
1932
+ }
1933
+ }, logger);
1934
+ try {
1935
+ for await (const _chunk of engine.run());
1936
+ } finally {
1937
+ await mcpManager.dispose();
1938
+ }
1939
+ const finalizationError = engine.getFinalizationError();
1940
+ if (finalizationError) {
1941
+ const err = new Error(finalizationError.message, finalizationError.cause !== void 0 ? { cause: finalizationError.cause } : void 0);
1942
+ if (finalizationError.code !== void 0) Object.defineProperty(err, "code", {
1943
+ value: finalizationError.code,
1944
+ enumerable: true
1945
+ });
1946
+ throw err;
1947
+ }
1948
+ const validated = engine.getValidatedStructuredOutput();
1949
+ if (validated) return validated.value;
1950
+ const result = engine.getStructuredOutputResult();
1951
+ if (!result) throw new Error("structured output finalization produced no result");
1952
+ return result.data;
1692
1953
  }
1954
+ /**
1955
+ * Parse the `value` payload of a `structured-output.complete` CUSTOM event
1956
+ * into a typed shape, returning `null` if the runtime payload doesn't match.
1957
+ *
1958
+ * Uses an `unknown`-input runtime check rather than `as` casts so the engine
1959
+ * stays cast-free in its hot path.
1960
+ */
1693
1961
  function readStructuredOutputCompleteValue(value) {
1694
- if (typeof value !== "object" || value === null) return null;
1695
- if (!("object" in value) || !("raw" in value)) return null;
1696
- const raw = value.raw;
1697
- if (typeof raw !== "string") return null;
1698
- const reasoningField = value.reasoning;
1699
- const reasoning = typeof reasoningField === "string" ? reasoningField : void 0;
1700
- return {
1701
- object: value.object,
1702
- raw,
1703
- ...reasoning !== void 0 ? { reasoning } : {}
1704
- };
1962
+ if (typeof value !== "object" || value === null) return null;
1963
+ if (!("object" in value) || !("raw" in value)) return null;
1964
+ const raw = value.raw;
1965
+ if (typeof raw !== "string") return null;
1966
+ const reasoningField = value.reasoning;
1967
+ const reasoning = typeof reasoningField === "string" ? reasoningField : void 0;
1968
+ return {
1969
+ object: value.object,
1970
+ raw,
1971
+ ...reasoning !== void 0 ? { reasoning } : {}
1972
+ };
1705
1973
  }
1974
+ /**
1975
+ * Synthesize a streaming structured-output stream by wrapping a non-streaming
1976
+ * `structuredOutput` call. Used when an adapter doesn't implement
1977
+ * `structuredOutputStream` natively.
1978
+ *
1979
+ * `onAdapterError`, when provided, is invoked with the raw error from
1980
+ * `adapter.structuredOutput` before the synthesized RUN_ERROR is yielded.
1981
+ * The engine uses this to preserve the original error (stack, cause, custom
1982
+ * properties like provider `status`/`code`) as `finalizationError.cause`,
1983
+ * because the RUN_ERROR wire shape only carries `message` and `code`.
1984
+ */
1706
1985
  async function* fallbackStructuredOutputStream(adapter, options, onAdapterError) {
1707
- const { chatOptions } = options;
1708
- const fallbackRand = Math.random().toString(36).slice(2);
1709
- const runId = chatOptions.runId ?? `fallback-${Date.now()}-${fallbackRand}`;
1710
- const threadId = chatOptions.threadId ?? `fallback-${Date.now()}-${fallbackRand}`;
1711
- const messageId = `fallback-${Date.now()}-${fallbackRand}`;
1712
- const model = chatOptions.model;
1713
- const timestamp = Date.now();
1714
- yield {
1715
- type: EventType.RUN_STARTED,
1716
- runId,
1717
- threadId,
1718
- model,
1719
- timestamp
1720
- };
1721
- let result;
1722
- try {
1723
- result = await adapter.structuredOutput(options);
1724
- } catch (error) {
1725
- onAdapterError?.(error);
1726
- const message = error instanceof Error ? error.message : String(error);
1727
- yield {
1728
- type: EventType.RUN_ERROR,
1729
- runId,
1730
- threadId,
1731
- model,
1732
- timestamp,
1733
- message,
1734
- error: { message }
1735
- };
1736
- return;
1737
- }
1738
- yield {
1739
- type: EventType.TEXT_MESSAGE_START,
1740
- messageId,
1741
- role: "assistant",
1742
- model,
1743
- timestamp
1744
- };
1745
- yield {
1746
- type: EventType.TEXT_MESSAGE_CONTENT,
1747
- messageId,
1748
- delta: result.rawText,
1749
- model,
1750
- timestamp
1751
- };
1752
- yield {
1753
- type: EventType.TEXT_MESSAGE_END,
1754
- messageId,
1755
- model,
1756
- timestamp
1757
- };
1758
- yield {
1759
- type: EventType.CUSTOM,
1760
- name: "structured-output.complete",
1761
- value: { object: result.data, raw: result.rawText },
1762
- model,
1763
- timestamp
1764
- };
1765
- yield {
1766
- type: EventType.RUN_FINISHED,
1767
- runId,
1768
- threadId,
1769
- model,
1770
- timestamp,
1771
- finishReason: "stop",
1772
- // Forward adapter-reported token usage so consumers reading
1773
- // `RUN_FINISHED.usage` (and the engine's `runOnUsage` middleware hook) see
1774
- // it on the fallback path, mirroring the native streaming path. The
1775
- // conditional spread avoids emitting `usage: undefined` for adapters that
1776
- // don't report it. See #758.
1777
- ...result.usage ? { usage: result.usage } : {}
1778
- };
1986
+ const { chatOptions } = options;
1987
+ const fallbackRand = Math.random().toString(36).slice(2);
1988
+ const runId = chatOptions.runId ?? `fallback-${Date.now()}-${fallbackRand}`;
1989
+ const threadId = chatOptions.threadId ?? `fallback-${Date.now()}-${fallbackRand}`;
1990
+ const messageId = `fallback-${Date.now()}-${fallbackRand}`;
1991
+ const model = chatOptions.model;
1992
+ const timestamp = Date.now();
1993
+ yield {
1994
+ type: EventType.RUN_STARTED,
1995
+ runId,
1996
+ threadId,
1997
+ model,
1998
+ timestamp
1999
+ };
2000
+ let result;
2001
+ try {
2002
+ result = await adapter.structuredOutput(options);
2003
+ } catch (error) {
2004
+ onAdapterError?.(error);
2005
+ const message = error instanceof Error ? error.message : String(error);
2006
+ yield {
2007
+ type: EventType.RUN_ERROR,
2008
+ runId,
2009
+ threadId,
2010
+ model,
2011
+ timestamp,
2012
+ message,
2013
+ error: { message }
2014
+ };
2015
+ return;
2016
+ }
2017
+ yield {
2018
+ type: EventType.TEXT_MESSAGE_START,
2019
+ messageId,
2020
+ role: "assistant",
2021
+ model,
2022
+ timestamp
2023
+ };
2024
+ yield {
2025
+ type: EventType.TEXT_MESSAGE_CONTENT,
2026
+ messageId,
2027
+ delta: result.rawText,
2028
+ model,
2029
+ timestamp
2030
+ };
2031
+ yield {
2032
+ type: EventType.TEXT_MESSAGE_END,
2033
+ messageId,
2034
+ model,
2035
+ timestamp
2036
+ };
2037
+ yield {
2038
+ type: EventType.CUSTOM,
2039
+ name: "structured-output.complete",
2040
+ value: {
2041
+ object: result.data,
2042
+ raw: result.rawText
2043
+ },
2044
+ model,
2045
+ timestamp
2046
+ };
2047
+ yield {
2048
+ type: EventType.RUN_FINISHED,
2049
+ runId,
2050
+ threadId,
2051
+ model,
2052
+ timestamp,
2053
+ finishReason: "stop",
2054
+ ...result.usage ? { usage: result.usage } : {}
2055
+ };
1779
2056
  }
2057
+ /**
2058
+ * Run streaming structured output via the TextEngine, with the engine's
2059
+ * `finalStructuredOutput.yieldChunks: true` mode. The agent loop's
2060
+ * RUN_STARTED/RUN_FINISHED are suppressed; the structured-output finalization
2061
+ * step's pair brackets the run for the consumer.
2062
+ *
2063
+ * Standard Schema *validation* is intentionally NOT run on this path — it is
2064
+ * the consumer's responsibility. This is a deliberate asymmetry vs.
2065
+ * `runAgenticStructuredOutput` (Promise<T> path), which DOES validate inside
2066
+ * the engine and routes validation failures through `onError`. The reason:
2067
+ * streaming consumers typically render partial JSON progressively (via
2068
+ * `parsePartialJSON` or `useChat`'s `partial` slot) and validate downstream
2069
+ * after assembly. Running validation server-side would force a hard error
2070
+ * on partial-by-design payloads. See `docs/structured-outputs/overview.md`.
2071
+ *
2072
+ * Null-widening normalization, however, IS run on both paths: the
2073
+ * `structured-output.complete` CUSTOM event is forwarded with its `value.object`
2074
+ * already un-widened (synthesized strict-mode nulls dropped, genuine
2075
+ * `.nullable()` nulls kept), so a consumer validating the assembled object
2076
+ * against the original schema doesn't choke on a `null` for an `.optional()`
2077
+ * field. Same `convertSchemaForStructuredOutput` pass and same
2078
+ * `undoNullWidening` map as the Promise<T> path — the two must not diverge.
2079
+ *
2080
+ * Pre-flight validation (missing schema, unconvertible schema) throws
2081
+ * synchronously at call time rather than as a yielded RUN_ERROR mid-stream —
2082
+ * those are programmer errors, not runtime conditions.
2083
+ */
1780
2084
  function runStreamingStructuredOutput(options) {
1781
- const { outputSchema } = options;
1782
- if (!outputSchema) {
1783
- throw new Error("outputSchema is required for streaming structured output");
1784
- }
1785
- const { jsonSchema, nullWideningMap } = convertSchemaForStructuredOutput(outputSchema);
1786
- if (!jsonSchema) {
1787
- throw new Error("Failed to convert output schema to JSON Schema");
1788
- }
1789
- const normalize = (data) => undoNullWidening(data, nullWideningMap);
1790
- return runStreamingStructuredOutputImpl(
1791
- options,
1792
- jsonSchema,
1793
- normalize
1794
- );
2085
+ const { outputSchema } = options;
2086
+ if (!outputSchema) throw new Error("outputSchema is required for streaming structured output");
2087
+ const { jsonSchema, nullWideningMap } = convertSchemaForStructuredOutput(outputSchema);
2088
+ if (!jsonSchema) throw new Error("Failed to convert output schema to JSON Schema");
2089
+ const normalize = (data) => undoNullWidening(data, nullWideningMap);
2090
+ const engineRef = {};
2091
+ const stream = runStreamingStructuredOutputImpl(options, jsonSchema, normalize, engineRef);
2092
+ publishDeliverySeams(stream, engineRef);
2093
+ return stream;
1795
2094
  }
1796
- async function* runStreamingStructuredOutputImpl(options, jsonSchema, normalize) {
1797
- const {
1798
- adapter,
1799
- outputSchema,
1800
- middleware,
1801
- context,
1802
- debug,
1803
- mcp,
1804
- ...textOptions
1805
- } = options;
1806
- const model = adapter.model;
1807
- const logger = resolveDebugOption(debug);
1808
- const nativeCombined = adapter.supportsCombinedToolsAndSchema?.(options.modelOptions) === true;
1809
- const mcpManager = MCPManager.from(mcp);
1810
- const mcpTools = await mcpManager.discover();
1811
- if (mcpTools.length > 0) {
1812
- textOptions.tools = [...textOptions.tools ?? [], ...mcpTools];
1813
- }
1814
- const engine = new TextEngine(
1815
- {
1816
- adapter,
1817
- params: { ...textOptions, model, logger },
1818
- middleware,
1819
- context,
1820
- finalStructuredOutput: {
1821
- jsonSchema,
1822
- yieldChunks: true,
1823
- normalize,
1824
- ...nativeCombined ? { nativeCombined: true } : {}
1825
- }
1826
- },
1827
- logger
1828
- );
1829
- try {
1830
- for await (const chunk of engine.run()) {
1831
- yield chunk;
1832
- }
1833
- } finally {
1834
- await mcpManager.dispose();
1835
- }
2095
+ async function* runStreamingStructuredOutputImpl(options, jsonSchema, normalize, engineRef) {
2096
+ const { adapter, outputSchema, middleware, context, debug, mcp, ...textOptions } = options;
2097
+ const model = adapter.model;
2098
+ const logger = resolveDebugOption(debug);
2099
+ const nativeCombined = adapter.supportsCombinedToolsAndSchema?.(options.modelOptions) === true;
2100
+ const mcpManager = MCPManager.from(mcp);
2101
+ const mcpTools = await mcpManager.discover();
2102
+ if (mcpTools.length > 0) textOptions.tools = [...textOptions.tools ?? [], ...mcpTools];
2103
+ const engine = new TextEngine({
2104
+ adapter,
2105
+ params: {
2106
+ ...textOptions,
2107
+ model,
2108
+ logger
2109
+ },
2110
+ middleware,
2111
+ context,
2112
+ finalStructuredOutput: {
2113
+ jsonSchema,
2114
+ yieldChunks: true,
2115
+ normalize,
2116
+ ...nativeCombined ? { nativeCombined: true } : {}
2117
+ }
2118
+ }, logger);
2119
+ engineRef.current = engine;
2120
+ try {
2121
+ for await (const chunk of engine.run()) yield chunk;
2122
+ } finally {
2123
+ await mcpManager.dispose();
2124
+ }
1836
2125
  }
1837
- export {
1838
- chat,
1839
- createChatOptions,
1840
- kind
1841
- };
1842
- //# sourceMappingURL=index.js.map
2126
+ //#endregion
2127
+ export { chat, createChatOptions, kind };
2128
+
2129
+ //# sourceMappingURL=index.js.map