@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
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Run lifecycle types — the neutral home for what a "run" is.
3
+ *
4
+ * Shared by `@tanstack/ai-persistence` (which exposes a `runs` store through
5
+ * `withPersistence`) and `@tanstack/ai-sandbox` (whose run driver records run
6
+ * status). Living in core is what lets one `RunRecord` per run be shared by
7
+ * both, instead of each package keeping its own and disagreeing. Same rationale
8
+ * as `LockStore` (`packages/ai/src/locks.ts`), which is likewise a
9
+ * coordination primitive that core owns so that no consumer package has to.
10
+ */
11
+ import { createCapability } from './capabilities'
12
+ import type { TokenUsage } from '../../../types'
13
+
14
+ /** A terminal run status: no further events will be appended. */
15
+ export type TerminalRunStatus = 'completed' | 'failed' | 'aborted'
16
+
17
+ /**
18
+ * Lifecycle status of one run (one agent turn within a conversation).
19
+ *
20
+ * `interrupted` is a human-in-the-loop PAUSE that interrupt-resume continues
21
+ * from — it is deliberately NOT terminal, and must never be conflated with
22
+ * `aborted` (an explicit cancellation).
23
+ *
24
+ * The two are now written by different hooks and cannot be confused:
25
+ *
26
+ * - `'interrupted'` is written ONLY by `withPersistence`'s `onInterrupt`, and
27
+ * carries NO `finishedAt` (a non-terminal status has not finished).
28
+ * - `'aborted'` is written by `withPersistence`'s `onAbort`, and only for an
29
+ * abort that is an explicit cancel or that is ending the run for good.
30
+ * - A mere client disconnect on a run with durable storage wired writes
31
+ * NEITHER: the record stays `'running'` and gains `detachedSince`, because the
32
+ * agent is still running and a later attach can take it over.
33
+ *
34
+ * Intent is never inferred from the abort itself — see `RUN_CANCEL_REASON` and
35
+ * `requestRunCancel` in `../cancel`.
36
+ */
37
+ export type RunStatus = 'running' | 'interrupted' | TerminalRunStatus
38
+
39
+ // A Record keyed by the union is exhaustiveness-checked: adding a member to
40
+ // TerminalRunStatus is a compile error here until this map is updated. A
41
+ // `Set<RunStatus>` would silently answer `false` for the new member instead.
42
+ const TERMINAL: Record<TerminalRunStatus, true> = {
43
+ completed: true,
44
+ failed: true,
45
+ aborted: true,
46
+ }
47
+
48
+ // Same exhaustiveness trick over the FULL union, for {@link isRunStatus}.
49
+ const ALL_STATUSES: Record<RunStatus, true> = {
50
+ running: true,
51
+ interrupted: true,
52
+ completed: true,
53
+ failed: true,
54
+ aborted: true,
55
+ }
56
+
57
+ /**
58
+ * Whether `value` is a {@link RunStatus} — the guard a backend validates a row
59
+ * with at DESERIALIZATION.
60
+ *
61
+ * `RunStatus` is a compile-time claim about a storage column. A row arrives as
62
+ * JSON out of D1, a Durable Object, or Postgres, and nothing in the type system
63
+ * checked what that column actually held, so a `RunStore` implementation should
64
+ * run its row's `status` through this before handing the record on. The readers
65
+ * downstream act DESTRUCTIVELY on the answer — `@tanstack/ai-sandbox`'s journal
66
+ * sweep DELETES the journal of a run it believes terminal — so a row that lies
67
+ * about its status is not a display bug.
68
+ */
69
+ export function isRunStatus(value: unknown): value is RunStatus {
70
+ return typeof value === 'string' && Object.hasOwn(ALL_STATUSES, value)
71
+ }
72
+
73
+ /**
74
+ * Whether `status` means no further events will be appended. Narrows, so a
75
+ * caller inside the guard can pass `status` where a {@link TerminalRunStatus}
76
+ * is required without a cast.
77
+ *
78
+ * `Object.hasOwn`, never `in`: `in` walks the prototype chain, so a row whose
79
+ * `status` column held `'toString'` or `'constructor'` would be reported
80
+ * terminal. `status` is TYPED `RunStatus`, but every value reaching here comes
81
+ * off a user-implemented {@link RunStore} and the type is only a claim (see
82
+ * {@link isRunStatus}). A false `true` deletes a live run's journal
83
+ * (`@tanstack/ai-sandbox`'s journal sweep), fails its attach as `'terminal-run'`
84
+ * (`attach-preflight`), and refuses to drive it (`stream-to-response.ts`).
85
+ */
86
+ export function isTerminalRunStatus(
87
+ status: RunStatus,
88
+ ): status is TerminalRunStatus {
89
+ return Object.hasOwn(TERMINAL, status)
90
+ }
91
+
92
+ /**
93
+ * Why a run failed.
94
+ *
95
+ * A bare message is an LLM provider's prose: it changes between model
96
+ * versions and cannot be branched on. `code` is what a consumer switches over
97
+ * to decide whether to retry, escalate, or surface a specific UI.
98
+ */
99
+ export interface RunError {
100
+ message: string
101
+ /** Stable, machine-branchable classification, when the provider supplies one. */
102
+ code?: string
103
+ }
104
+
105
+ /** Durable bookkeeping for a single run. */
106
+ export interface RunRecord {
107
+ runId: string
108
+ /**
109
+ * Conversation this run belongs to — the `Scope.threadId`.
110
+ *
111
+ * Generation jobs (a one-shot `generate()` with no conversation) must not
112
+ * reuse this record by faking `threadId = requestId`; they need a separate
113
+ * job store. `withGenerationPersistence` currently does exactly that and
114
+ * labels itself a stopgap — do not copy it.
115
+ */
116
+ threadId: string
117
+ status: RunStatus
118
+ startedAt: number
119
+ finishedAt?: number
120
+ error?: RunError
121
+ usage?: TokenUsage
122
+ /**
123
+ * Compound sandbox key this run was bound to, when it ran in a sandbox.
124
+ * Recorded so a future reclaimer can identify the sandbox to tear down
125
+ * without re-deriving the key. Written by `withSandbox`'s detach path
126
+ * (`onAbort` in `@tanstack/ai-sandbox`'s `middleware.ts`) at the same time as
127
+ * `detachedSince`, when a disconnect leaves the run detached rather than
128
+ * destroying the sandbox. A backend must round-trip this field — see
129
+ * `listReclaimable` below for who eventually reads it.
130
+ */
131
+ sandboxKey?: string
132
+ /**
133
+ * Epoch ms when the last viewer detached; absent while someone is attached.
134
+ * Written by `withSandbox`'s detach path (`onAbort` in `@tanstack/ai-sandbox`'s
135
+ * `middleware.ts`) alongside `sandboxKey`, when a disconnect leaves the
136
+ * agent running rather than tearing the sandbox down. A backend must
137
+ * round-trip this field: `listReclaimable` depends on it, and
138
+ * `@tanstack/ai-sandbox`'s `reapDetachedRuns` sweeps the candidates it
139
+ * surfaces (see that method's doc comment).
140
+ */
141
+ detachedSince?: number
142
+ /**
143
+ * Set by an explicit out-of-band cancel, to be distinguished from a mere
144
+ * client disconnect (the two produce an identical TCP close, so intent is not
145
+ * inferable from the disconnect).
146
+ *
147
+ * Written by `requestRunCancel` and read by `wasCancelRequested` (both in
148
+ * `../cancel`). Deliberately NOT a status: recording intent is not the same as
149
+ * the run having stopped, and only the driver knows when it has.
150
+ */
151
+ cancelRequested?: boolean
152
+ /**
153
+ * Monotonic fencing token for the run's driver. Bumped by each host that
154
+ * successfully claims the run (see `withRunClaim` in `@tanstack/ai-sandbox`),
155
+ * so a superseded host can discover it lost by comparing the stored value
156
+ * against the one it holds.
157
+ *
158
+ * A lock alone cannot provide this: it tells the winner it won, but gives a
159
+ * loser nothing to read. Absent on a run that was never claimed.
160
+ */
161
+ driverEpoch?: number
162
+ }
163
+
164
+ /**
165
+ * Durable store for run lifecycle records.
166
+ *
167
+ * REQUIRED: `createOrResume`, `update`, `get`, `findActiveRun`. Every backend
168
+ * must implement all four — they are what the persistence middleware calls
169
+ * unconditionally. `findActiveRun` is required rather than feature-detected
170
+ * because a backend that has not implemented it is indistinguishable from one
171
+ * whose answer is legitimately `null`, so reconnect would silently do nothing
172
+ * instead of failing at build time. It was optional for exactly one release
173
+ * cycle and cost precisely that.
174
+ *
175
+ * OPTIONAL: `listByThread`, `listReclaimable`. Each serves one higher-level
176
+ * feature (thread history, reclaim reaping) and callers feature-detect them,
177
+ * degrading gracefully when a backend omits them.
178
+ */
179
+ export interface RunStore {
180
+ /**
181
+ * Create a run record, or return the existing one unchanged if `runId` is
182
+ * already present.
183
+ *
184
+ * INVARIANT (idempotency): an existing record is returned **unchanged** and
185
+ * the passed `threadId`/`startedAt`/`status` are ignored. This is what makes
186
+ * resuming a run safe. `status` defaults to `'running'` on first creation.
187
+ */
188
+ createOrResume: (
189
+ input: Pick<RunRecord, 'runId' | 'threadId' | 'startedAt'> & {
190
+ status?: RunStatus
191
+ },
192
+ ) => Promise<RunRecord>
193
+ /**
194
+ * Patch a record's mutable fields.
195
+ *
196
+ * INVARIANT: updating an unknown `runId` is a **no-op** — it must not throw
197
+ * and must not create a record.
198
+ */
199
+ update: (
200
+ runId: string,
201
+ patch: Partial<
202
+ Pick<
203
+ RunRecord,
204
+ | 'status'
205
+ | 'finishedAt'
206
+ | 'error'
207
+ | 'usage'
208
+ | 'sandboxKey'
209
+ | 'detachedSince'
210
+ | 'cancelRequested'
211
+ | 'driverEpoch'
212
+ >
213
+ >,
214
+ ) => Promise<void>
215
+ /** Current record, or null when unknown. */
216
+ get: (runId: string) => Promise<RunRecord | null>
217
+ /**
218
+ * Every run in a conversation, ascending by `startedAt`. OPTIONAL: only
219
+ * needed to render a thread's past agent activity. Consumers feature-detect.
220
+ */
221
+ listByThread?: (threadId: string) => Promise<Array<RunRecord>>
222
+ /**
223
+ * Runs that may be reclaimed: ALL THREE of `status === 'running'`,
224
+ * `detachedSince` is set, and `detachedSince <= now - ttlMs`. The cutoff is
225
+ * **inclusive** — a run detached at exactly `now - ttlMs` IS reclaimable.
226
+ *
227
+ * OPTIONAL: only needed by a reaper. Consumers feature-detect.
228
+ *
229
+ * `detachedSince` is populated by `withSandbox`'s detach path (see
230
+ * {@link RunRecord.detachedSince}). The sweep over the candidates this
231
+ * surfaces is `@tanstack/ai-sandbox`'s `reapDetachedRuns`: it finalizes a run
232
+ * whose agent already finished, expires one past its TTL, and reclaims the
233
+ * sandbox. That is a function, not a scheduler — the application invokes it
234
+ * (cron, queue, `alarm()`, `waitUntil`) — and a backend that omits this
235
+ * method cannot be reaped at all.
236
+ */
237
+ listReclaimable?: (opts: {
238
+ now: number
239
+ ttlMs: number
240
+ }) => Promise<Array<RunRecord>>
241
+ /**
242
+ * The most recent `'running'` run for `threadId`, or `null` if none is active.
243
+ *
244
+ * REQUIRED. This resolves "does this thread have a live run to attach to?"
245
+ * from the STABLE thread id, which is the durable basis for reconnecting a
246
+ * client (a reload, or the same thread opened on another device) — independent
247
+ * of the ephemeral run id, which a single turn may mint several of. When more
248
+ * than one run is `'running'`, the one with the greatest `startedAt` wins.
249
+ *
250
+ * A backend that stubs this to `null` turns reconnect off silently, because
251
+ * `null` is also the correct answer for an idle thread. A backend with no run
252
+ * lifecycle at all should omit the whole `runs` store instead — capability
253
+ * tiers belong at the store level, not the method level.
254
+ */
255
+ findActiveRun: (threadId: string) => Promise<RunRecord | null>
256
+ }
257
+
258
+ /**
259
+ * Type a {@link RunStore} implementation inline: pass the object and get
260
+ * autocomplete plus contract checking with no separate annotation. Mirrors
261
+ * `defineLock` / `defineSandboxInstanceStore`.
262
+ *
263
+ * The generic return preserves the argument's own type, so an optional method
264
+ * the implementation actually provides stays known-present on the result
265
+ * instead of collapsing back to `| undefined` on the interface.
266
+ */
267
+ export function defineRunStore<const T extends RunStore>(store: T): T {
268
+ return store
269
+ }
270
+
271
+ /**
272
+ * Whether the current run can be DETACHED rather than destroyed when its client
273
+ * disconnects — `true` only when some middleware has both a {@link RunStore} and
274
+ * a durable event log wired (`withSandbox`'s `runs` + `durability.adapter`).
275
+ *
276
+ * Lives in core for the same reason `LockStore` does: it is a coordination fact
277
+ * that two consumer packages must agree on, and neither may depend on the other.
278
+ * `@tanstack/ai-sandbox` provides it; `@tanstack/ai-persistence` reads it to
279
+ * decide whether an abort is terminal (`'aborted'`) or a detach (write nothing).
280
+ * A persistence → sandbox import would be a layering inversion.
281
+ *
282
+ * Consumers read it with `{ optional: true }`: absent means "not detachable",
283
+ * which is every app that has not wired durability.
284
+ *
285
+ * Typed `true`, not `boolean`: ABSENCE is the negative, so a published `false`
286
+ * has no meaning — and a consumer that tests PRESENCE rather than the value
287
+ * would read one as "detachable". Narrowing the payload makes that
288
+ * unrepresentable instead of merely undocumented.
289
+ */
290
+ export const DetachableRunCapability =
291
+ createCapability<true>()('detachable-run')
292
+
293
+ /**
294
+ * Destructured accessors: `getDetachableRun(ctx, { optional: true })` /
295
+ * `provideDetachableRun(ctx, true)`.
296
+ */
297
+ export const [getDetachableRun, provideDetachableRun] = DetachableRunCapability
298
+
299
+ /**
300
+ * Whether this run's teardown DID detach — the disconnect was survived, the
301
+ * agent is still working, and a later attach can take the run over.
302
+ *
303
+ * The past-tense counterpart of {@link DetachableRunCapability}, and the two must
304
+ * not be confused:
305
+ *
306
+ * - **detachABLE** is published at `setup`, and only says a disconnect *may* be
307
+ * survived (a `RunStore` and a durable log are wired).
308
+ * - **detachED** is published on the ABORT path, by the middleware that actually
309
+ * makes the call — `withSandbox`'s `onAbort`, which is the only actor that has
310
+ * resolved BOTH out-of-band cancel bands (`AbortInfo.cancelRequested` and
311
+ * `wasCancelRequested` on the record) and `detachOnDisconnect`. An explicit
312
+ * cancel, a non-detachable disconnect, an error, and a normal finish all leave
313
+ * it unpublished.
314
+ *
315
+ * Its consumer is the durable DELIVERY sink in `stream-to-response.ts`: a
316
+ * detached run's log must stay OPEN and un-terminalized so the takeover can
317
+ * continue it (see `wasRunDetached` in `../../../delivery-detach`). Reading it
318
+ * is safe and race-free only because a `for await` over the chat stream awaits
319
+ * the generator's `return()` — and therefore the whole `onAbort` chain — before
320
+ * the sink's own `finally` runs.
321
+ *
322
+ * Read with `{ optional: true }`: absent means "not detached", which is every
323
+ * other exit path and every app that has not wired durability.
324
+ *
325
+ * Typed `true`, not `boolean`, for the same reason as
326
+ * {@link DetachableRunCapability}: absence is the only negative, so publishing
327
+ * `false` must not be representable.
328
+ */
329
+ export const RunDetachedCapability = createCapability<true>()('run-detached')
330
+
331
+ /**
332
+ * Destructured accessors: `getRunDetached(ctx, { optional: true })` /
333
+ * `provideRunDetached(ctx, true)`.
334
+ */
335
+ export const [getRunDetached, provideRunDetached] = RunDetachedCapability
336
+
337
+ /** In-memory {@link RunStore}. Single process only. */
338
+ export class InMemoryRunStore implements RunStore {
339
+ private readonly runs = new Map<string, RunRecord>()
340
+
341
+ createOrResume(
342
+ input: Pick<RunRecord, 'runId' | 'threadId' | 'startedAt'> & {
343
+ status?: RunStatus
344
+ },
345
+ ): Promise<RunRecord> {
346
+ const existing = this.runs.get(input.runId)
347
+ if (existing) return Promise.resolve(existing)
348
+ const record: RunRecord = {
349
+ runId: input.runId,
350
+ threadId: input.threadId,
351
+ status: input.status ?? 'running',
352
+ startedAt: input.startedAt,
353
+ }
354
+ this.runs.set(record.runId, record)
355
+ return Promise.resolve(record)
356
+ }
357
+
358
+ update(
359
+ runId: string,
360
+ patch: Partial<
361
+ Pick<
362
+ RunRecord,
363
+ | 'status'
364
+ | 'finishedAt'
365
+ | 'error'
366
+ | 'usage'
367
+ | 'sandboxKey'
368
+ | 'detachedSince'
369
+ | 'cancelRequested'
370
+ | 'driverEpoch'
371
+ >
372
+ >,
373
+ ): Promise<void> {
374
+ const existing = this.runs.get(runId)
375
+ if (existing) this.runs.set(runId, { ...existing, ...patch })
376
+ return Promise.resolve()
377
+ }
378
+
379
+ get(runId: string): Promise<RunRecord | null> {
380
+ return Promise.resolve(this.runs.get(runId) ?? null)
381
+ }
382
+
383
+ listByThread(threadId: string): Promise<Array<RunRecord>> {
384
+ const matching = [...this.runs.values()]
385
+ .filter((run) => run.threadId === threadId)
386
+ .sort((a, b) => a.startedAt - b.startedAt)
387
+ return Promise.resolve(matching)
388
+ }
389
+
390
+ listReclaimable(opts: {
391
+ now: number
392
+ ttlMs: number
393
+ }): Promise<Array<RunRecord>> {
394
+ const cutoff = opts.now - opts.ttlMs
395
+ const matching = [...this.runs.values()].filter(
396
+ (run) =>
397
+ run.status === 'running' &&
398
+ run.detachedSince !== undefined &&
399
+ run.detachedSince <= cutoff,
400
+ )
401
+ return Promise.resolve(matching)
402
+ }
403
+
404
+ findActiveRun(threadId: string): Promise<RunRecord | null> {
405
+ let active: RunRecord | null = null
406
+ for (const run of this.runs.values()) {
407
+ if (run.threadId !== threadId || run.status !== 'running') continue
408
+ if (active === null || run.startedAt > active.startedAt) active = run
409
+ }
410
+ return Promise.resolve(active)
411
+ }
412
+ }
@@ -1,12 +1,15 @@
1
1
  import type {
2
+ AgentLoopState,
2
3
  JSONSchema,
3
4
  ModelMessage,
5
+ RunAgentResumeItem,
4
6
  StreamChunk,
5
7
  TokenUsage,
6
8
  Tool,
7
9
  ToolCall,
8
10
  } from '../../../types'
9
11
  import type { SystemPrompt } from '../../../system-prompts'
12
+ import type { ToolApprovalResolution } from '../../../interrupts'
10
13
  import type {
11
14
  Capability,
12
15
  CapabilityHandle,
@@ -90,6 +93,8 @@ export interface ChatMiddlewareContext<TContext = unknown> {
90
93
  streamId: string
91
94
  /** AG-UI run identifier for correlating client and server events */
92
95
  runId: string
96
+ /** Interrupted or parent run correlated with this continuation. */
97
+ parentRunId?: string
93
98
  /**
94
99
  * AG-UI thread identifier — a stable per-conversation ID used to
95
100
  * correlate client and server devtools events. Resolves to the
@@ -133,7 +138,7 @@ export interface ChatMiddlewareContext<TContext = unknown> {
133
138
  activity: 'chat'
134
139
  /** Provider name (e.g., 'openai', 'anthropic') */
135
140
  provider: string
136
- /** Model identifier (e.g., 'gpt-4o') */
141
+ /** Model identifier (e.g., 'gpt-5.5') */
137
142
  model: string
138
143
  /** Source of the chat invocation — always 'server' for server-side chat */
139
144
  source: 'client' | 'server'
@@ -208,10 +213,31 @@ export interface ChatMiddlewareConfig {
208
213
  messages: Array<ModelMessage>
209
214
  systemPrompts: Array<SystemPrompt>
210
215
  tools: Array<Tool>
216
+ resume?: Array<RunAgentResumeItem> | undefined
217
+ resumeToolState?: ChatResumeToolState | undefined
211
218
  metadata?: Record<string, unknown> | undefined
212
219
  modelOptions?: Record<string, unknown> | undefined
213
220
  }
214
221
 
222
+ /**
223
+ * Tool decisions reconstructed by server-side middleware from validated resume
224
+ * entries. This lets empty-message interrupt resumes continue tool execution
225
+ * without relying on client message history.
226
+ */
227
+ export interface ChatResumeToolState {
228
+ approvals?: ReadonlyMap<string, ToolApprovalResolution> | undefined
229
+ clientToolResults?: ReadonlyMap<string, unknown> | undefined
230
+ genericInterrupts?:
231
+ | ReadonlyMap<string, ChatResumeGenericResolution>
232
+ | undefined
233
+ deniedToolResults?: ReadonlyMap<string, unknown> | undefined
234
+ cancelledToolCallIds?: ReadonlySet<string> | undefined
235
+ }
236
+
237
+ export type ChatResumeGenericResolution =
238
+ | { interruptId: string; status: 'resolved'; payload: unknown }
239
+ | { interruptId: string; status: 'cancelled'; payload?: never }
240
+
215
241
  /**
216
242
  * Config passed to onStructuredOutputConfig.
217
243
  *
@@ -373,6 +399,22 @@ export interface AbortInfo {
373
399
  reason?: string
374
400
  /** Duration until abort in milliseconds */
375
401
  duration: number
402
+ /**
403
+ * True only when the abort came from an explicit, out-of-band cancel (e.g. a
404
+ * cancel endpoint setting `RunRecord.cancelRequested`), never from a mere
405
+ * client disconnect.
406
+ *
407
+ * A disconnect and a user pressing "stop" are the SAME connection close on
408
+ * the wire, so consumers must not infer intent from an abort alone. Middleware
409
+ * that tears down expensive resources reads this to distinguish "the viewer
410
+ * left, keep going" from "the user wants this stopped". Populated from the
411
+ * abort reason: `true` exactly when the run was aborted with `RUN_CANCEL_REASON`
412
+ * (matched with `===`, so an arbitrary error message can never be read as a
413
+ * deliberate cancel), `false` for every other abort. The durable channel is
414
+ * separate — middleware that must also catch a cancel recorded on a different
415
+ * host reads `RunRecord.cancelRequested` in addition to this flag.
416
+ */
417
+ cancelRequested?: boolean
376
418
  }
377
419
 
378
420
  /**
@@ -506,6 +548,25 @@ export interface ChatMiddleware<TContext = unknown> {
506
548
  info: IterationInfo,
507
549
  ) => void | Promise<void>
508
550
 
551
+ /**
552
+ * Called when the engine is deciding whether to start another agent-loop
553
+ * iteration (after a tool phase or between model turns).
554
+ *
555
+ * Return `false` to stop further iterations. Return `true`, `void`, or
556
+ * `undefined` to allow continuation. Combined with AND semantics across
557
+ * middleware and with `agentLoopStrategy` — any `false` stops the loop.
558
+ *
559
+ * Does not abort the run: the stream finishes normally with the current
560
+ * messages. Use `ctx.abort()` only when you need a hard abort.
561
+ *
562
+ * Receives the same {@link AgentLoopState} passed to strategies
563
+ * (`iterationCount`, `toolCallCount`, `lastTurnToolCallCount`, etc.).
564
+ */
565
+ onShouldContinue?: (
566
+ ctx: ChatMiddlewareContext<TContext>,
567
+ state: AgentLoopState,
568
+ ) => boolean | void | Promise<boolean | void>
569
+
509
570
  /**
510
571
  * Called for every chunk yielded by chat().
511
572
  * Can observe, transform, expand, or drop chunks.