experimental-a2 0.17.0 → 0.18.1

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 (221) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-DBbe5xeE.d.ts → actor-C7b-1arn.d.ts} +5 -5
  4. package/dist/{actor-DBbe5xeE.d.ts.map → actor-C7b-1arn.d.ts.map} +1 -1
  5. package/dist/actor-client.d.ts +1 -1
  6. package/dist/actor-react.d.ts +3 -3
  7. package/dist/actor-react.js +1 -1
  8. package/dist/actor.d.ts +1 -1
  9. package/dist/actor.js +2 -2
  10. package/dist/{ai-vpGtcaCB.d.ts → ai-BMLlI2Ff.d.ts} +3 -3
  11. package/dist/{ai-vpGtcaCB.d.ts.map → ai-BMLlI2Ff.d.ts.map} +1 -1
  12. package/dist/{ai-UrWfKeuv.js → ai-D6mGlO8j.js} +2 -2
  13. package/dist/{ai-UrWfKeuv.js.map → ai-D6mGlO8j.js.map} +1 -1
  14. package/dist/ai-client.d.ts +2 -2
  15. package/dist/ai-client.js +2 -2
  16. package/dist/ai-fx.browser.d.ts +1 -0
  17. package/dist/ai-fx.browser.js +6 -0
  18. package/dist/ai-fx.browser.js.map +1 -0
  19. package/dist/ai-fx.d.ts +10 -0
  20. package/dist/ai-fx.d.ts.map +1 -0
  21. package/dist/ai-fx.js +692 -0
  22. package/dist/ai-fx.js.map +1 -0
  23. package/dist/ai-harness-B2suP2Qh.d.ts +26 -0
  24. package/dist/ai-harness-B2suP2Qh.d.ts.map +1 -0
  25. package/dist/ai-harness-BuLh_EYi.js +273 -0
  26. package/dist/ai-harness-BuLh_EYi.js.map +1 -0
  27. package/dist/ai-pi.browser.d.ts +1 -0
  28. package/dist/ai-pi.browser.js +6 -0
  29. package/dist/ai-pi.browser.js.map +1 -0
  30. package/dist/ai-pi.d.ts +17 -0
  31. package/dist/ai-pi.d.ts.map +1 -0
  32. package/dist/ai-pi.js +950 -0
  33. package/dist/ai-pi.js.map +1 -0
  34. package/dist/ai-react.d.ts +5 -5
  35. package/dist/ai-react.js +2 -2
  36. package/dist/ai-server.d.ts +47 -25
  37. package/dist/ai-server.d.ts.map +1 -1
  38. package/dist/ai-server.js +113 -40
  39. package/dist/ai-server.js.map +1 -1
  40. package/dist/ai-tool-input-DmSqMUQi.js +40 -0
  41. package/dist/ai-tool-input-DmSqMUQi.js.map +1 -0
  42. package/dist/ai.d.ts +1 -1
  43. package/dist/ai.js +1 -1
  44. package/dist/bounded-cache-CPHWF-8h.js +41 -0
  45. package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
  46. package/dist/{client-COaiaDub.js → client-BhHLoNKF.js} +3 -3
  47. package/dist/{client-COaiaDub.js.map → client-BhHLoNKF.js.map} +1 -1
  48. package/dist/{client-qRnRUzBS.d.ts → client-h_DKFAjj.d.ts} +3 -3
  49. package/dist/{client-qRnRUzBS.d.ts.map → client-h_DKFAjj.d.ts.map} +1 -1
  50. package/dist/client.d.ts +3 -3
  51. package/dist/client.js +1 -1
  52. package/dist/index.d.ts +5 -5
  53. package/dist/{initial-snapshot-BrLso5x_.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +2 -2
  54. package/dist/{initial-snapshot-BrLso5x_.d.ts.map → initial-snapshot-Bx-c0pDP.d.ts.map} +1 -1
  55. package/dist/{internal-CbVvEwad.js → internal-CscSY7K_.js} +11 -3
  56. package/dist/internal-CscSY7K_.js.map +1 -0
  57. package/dist/otel.d.ts +1 -1
  58. package/dist/{react-BbJ-g6Uu.d.ts → react-BBXgHWFc.d.ts} +4 -4
  59. package/dist/{react-BbJ-g6Uu.d.ts.map → react-BBXgHWFc.d.ts.map} +1 -1
  60. package/dist/react.d.ts +1 -1
  61. package/dist/react.js +1 -1
  62. package/dist/reducer-BAXc8JyF.js.map +1 -1
  63. package/dist/{reducer-BYu6DT7h.d.ts → reducer-CqA59Q6U.d.ts} +2 -2
  64. package/dist/{reducer-BYu6DT7h.d.ts.map → reducer-CqA59Q6U.d.ts.map} +1 -1
  65. package/dist/scheduler-qstash.d.ts +2 -2
  66. package/dist/scheduler-qstash.js +3 -3
  67. package/dist/scheduler-qstash.js.map +1 -1
  68. package/dist/scheduler-vercel.d.ts +7 -7
  69. package/dist/scheduler-vercel.d.ts.map +1 -1
  70. package/dist/scheduler-vercel.js +9 -10
  71. package/dist/scheduler-vercel.js.map +1 -1
  72. package/dist/{server-Rj64fNvX.js → server-D-Y4pqwu.js} +126 -67
  73. package/dist/server-D-Y4pqwu.js.map +1 -0
  74. package/dist/{server-CGM8WzRG.d.ts → server-DYAATFuT.d.ts} +42 -7
  75. package/dist/server-DYAATFuT.d.ts.map +1 -0
  76. package/dist/server.d.ts +4 -4
  77. package/dist/server.js +1 -1
  78. package/dist/{session-clear-Dshpuhff.d.ts → session-clear-C-QwTXV1.d.ts} +2 -2
  79. package/dist/session-clear-C-QwTXV1.d.ts.map +1 -0
  80. package/dist/{store-Gpkq7WBo.d.ts → store-CyLUy86v.d.ts} +53 -5
  81. package/dist/store-CyLUy86v.d.ts.map +1 -0
  82. package/dist/store-N8PXxDAS.js.map +1 -1
  83. package/dist/store-memory.d.ts +1 -1
  84. package/dist/store-memory.js +56 -11
  85. package/dist/store-memory.js.map +1 -1
  86. package/dist/{store-polling-4_y8lETm.js → store-polling-7WJV0YWP.js} +2 -2
  87. package/dist/{store-polling-4_y8lETm.js.map → store-polling-7WJV0YWP.js.map} +1 -1
  88. package/dist/store-postgres.d.ts +1 -1
  89. package/dist/store-postgres.d.ts.map +1 -1
  90. package/dist/store-postgres.js +106 -25
  91. package/dist/store-postgres.js.map +1 -1
  92. package/dist/{store-presence-polling-Q1C-k2dd.js → store-presence-polling-B1ixeqIS.js} +3 -3
  93. package/dist/{store-presence-polling-Q1C-k2dd.js.map → store-presence-polling-B1ixeqIS.js.map} +1 -1
  94. package/dist/store-redis-http.d.ts +1 -1
  95. package/dist/store-redis-http.js +3 -3
  96. package/dist/{store-redis-notify-tqWNnMRu.js → store-redis-notify-CM0AfLY0.js} +271 -101
  97. package/dist/store-redis-notify-CM0AfLY0.js.map +1 -0
  98. package/dist/store-redis.d.ts +1 -1
  99. package/dist/store-redis.js +1 -1
  100. package/dist/store-sqlite.d.ts +1 -1
  101. package/dist/store-sqlite.js +66 -21
  102. package/dist/store-sqlite.js.map +1 -1
  103. package/dist/{telemetry-cZH-cibj.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
  104. package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
  105. package/dist/{wire-DjfHOvdW.js → wire-BKxH1vbP.js} +2 -2
  106. package/dist/{wire-DjfHOvdW.js.map → wire-BKxH1vbP.js.map} +1 -1
  107. package/docs/01-quickstart.mdx +49 -83
  108. package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
  109. package/docs/actors/01-introduction.mdx +48 -86
  110. package/docs/actors/02-concurrency.mdx +43 -70
  111. package/docs/actors/03-timers.mdx +28 -42
  112. package/docs/actors/04-routes.mdx +110 -150
  113. package/docs/actors/meta.ts +1 -1
  114. package/docs/agents/01-introduction.mdx +257 -0
  115. package/docs/agents/02-tools.mdx +247 -0
  116. package/docs/agents/03-conversations.mdx +538 -0
  117. package/docs/agents/04-harnesses.mdx +136 -0
  118. package/docs/agents/meta.ts +1 -0
  119. package/docs/client/01-react.mdx +294 -0
  120. package/docs/client/02-transports.mdx +86 -0
  121. package/docs/client/03-presence.mdx +224 -0
  122. package/docs/client/meta.ts +1 -0
  123. package/docs/contract/01-events.mdx +57 -0
  124. package/docs/contract/02-state.mdx +142 -0
  125. package/docs/contract/03-sessions.mdx +175 -0
  126. package/docs/index.mdx +109 -145
  127. package/docs/reference/01-core.mdx +139 -0
  128. package/docs/reference/02-server.mdx +860 -0
  129. package/docs/reference/03-client.mdx +301 -0
  130. package/docs/reference/04-ai.mdx +731 -0
  131. package/docs/reference/05-adapters.mdx +351 -0
  132. package/docs/reference/06-errors.mdx +138 -0
  133. package/docs/reference/meta.ts +1 -1
  134. package/docs/server/01-handlers.mdx +200 -0
  135. package/docs/server/02-timers.mdx +130 -0
  136. package/docs/server/03-cancellation.mdx +142 -0
  137. package/docs/server/04-routes.mdx +243 -0
  138. package/docs/server/05-production.mdx +323 -0
  139. package/docs/server/06-guarantees.mdx +132 -0
  140. package/examples/playground/DEPLOY.md +3 -5
  141. package/examples/playground/app/a0/[appId]/page.tsx +12 -13
  142. package/examples/playground/app/a0/app-list.tsx +9 -2
  143. package/examples/playground/app/a0/app-view.tsx +9 -1
  144. package/examples/playground/app/a0/apps/[appId]/route.ts +10 -5
  145. package/examples/playground/app/a0/apps.test.ts +12 -0
  146. package/examples/playground/app/a0/apps.ts +22 -7
  147. package/examples/playground/app/a0/chat.tsx +47 -1
  148. package/examples/playground/app/a0/devtools.tsx +2 -8
  149. package/examples/playground/app/a0/fx/events/route.ts +9 -0
  150. package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
  151. package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
  152. package/examples/playground/app/a0/model.test.ts +18 -1
  153. package/examples/playground/app/a0/model.ts +47 -10
  154. package/examples/playground/app/a0/no-photon.cjs +4 -0
  155. package/examples/playground/app/a0/no-quickjs.cjs +3 -0
  156. package/examples/playground/app/a0/pi/events/route.ts +9 -0
  157. package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
  158. package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
  159. package/examples/playground/app/a0/sandbox-log.ts +10 -4
  160. package/examples/playground/app/a0/sandbox-session.ts +152 -0
  161. package/examples/playground/app/a0/scheduler/route.ts +2 -2
  162. package/examples/playground/app/a0/server.ts +98 -54
  163. package/examples/playground/app/a0/session.ts +17 -5
  164. package/examples/playground/app/a0/shell.tsx +11 -3
  165. package/examples/playground/app/agent/ingress.test.ts +6 -3
  166. package/examples/playground/app/agent/server.ts +9 -6
  167. package/examples/playground/app/agent/telemetry.test.ts +71 -50
  168. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +1 -1
  169. package/examples/playground/app/chat/server.test.ts +30 -26
  170. package/examples/playground/app/chat/server.ts +46 -46
  171. package/examples/playground/next.config.ts +17 -5
  172. package/examples/playground/package.json +4 -1
  173. package/examples/playground/vercel.json +18 -0
  174. package/package.json +27 -3
  175. package/src/ai-agent.ts +271 -90
  176. package/src/ai-fx.browser.ts +5 -0
  177. package/src/ai-fx.ts +912 -0
  178. package/src/ai-harness.ts +518 -0
  179. package/src/ai-model-metadata.ts +11 -6
  180. package/src/ai-pi.browser.ts +5 -0
  181. package/src/ai-pi.ts +1322 -0
  182. package/src/ai-sdk-step.ts +2 -2
  183. package/src/ai-server.ts +3 -1
  184. package/src/ai-tool-input.ts +45 -0
  185. package/src/internal.ts +13 -0
  186. package/src/libfx.d.ts +34 -0
  187. package/src/reducer.ts +1 -1
  188. package/src/scheduler-qstash.ts +5 -5
  189. package/src/scheduler-vercel.ts +14 -15
  190. package/src/server.ts +233 -29
  191. package/src/store-memory.ts +92 -16
  192. package/src/store-postgres.ts +156 -25
  193. package/src/store-redis-core.ts +322 -110
  194. package/src/store-redis-notify.ts +2 -0
  195. package/src/store-sqlite.ts +121 -28
  196. package/src/store.ts +55 -3
  197. package/src/telemetry.ts +1 -0
  198. package/dist/internal-CbVvEwad.js.map +0 -1
  199. package/dist/server-CGM8WzRG.d.ts.map +0 -1
  200. package/dist/server-Rj64fNvX.js.map +0 -1
  201. package/dist/session-clear-Dshpuhff.d.ts.map +0 -1
  202. package/dist/store-Gpkq7WBo.d.ts.map +0 -1
  203. package/dist/store-redis-notify-tqWNnMRu.js.map +0 -1
  204. package/dist/telemetry-cZH-cibj.d.ts.map +0 -1
  205. package/docs/concepts/01-contracts.mdx +0 -237
  206. package/docs/concepts/02-handlers.mdx +0 -284
  207. package/docs/concepts/03-durability.mdx +0 -314
  208. package/docs/concepts/04-state.mdx +0 -209
  209. package/docs/guides/01-timers.mdx +0 -193
  210. package/docs/guides/02-cancellation.mdx +0 -161
  211. package/docs/guides/03-react.mdx +0 -324
  212. package/docs/guides/04-local-first.mdx +0 -47
  213. package/docs/guides/05-production.mdx +0 -534
  214. package/docs/guides/06-ai-agents.mdx +0 -1408
  215. package/docs/guides/08-application-data.mdx +0 -113
  216. package/docs/guides/09-presence.mdx +0 -262
  217. package/docs/guides/10-transports.mdx +0 -221
  218. package/docs/reference/01-api.mdx +0 -2358
  219. package/docs/reference/02-errors.mdx +0 -150
  220. /package/docs/{concepts → contract}/meta.ts +0 -0
  221. /package/docs/{guides → server}/meta.ts +0 -0
package/src/ai-agent.ts CHANGED
@@ -23,9 +23,9 @@ import { AsyncLocalStorage } from 'node:async_hooks'
23
23
  import { createHash } from 'node:crypto'
24
24
  import { convertToModelMessages, isToolUIPart } from 'ai'
25
25
  import type {
26
+ Experimental_SandboxSession,
26
27
  FinishReason,
27
28
  Instructions,
28
- LanguageModel,
29
29
  LanguageModelUsage,
30
30
  ModelMessage,
31
31
  TextStreamPart,
@@ -34,6 +34,15 @@ import type {
34
34
  UIMessageChunk,
35
35
  } from 'ai'
36
36
  import { generateAISDKStep, type AISDKStepSettings } from './ai-sdk-step.ts'
37
+ import {
38
+ harnessDriver,
39
+ harnessLog,
40
+ harnessStates,
41
+ registerHarnessStates,
42
+ type Harness,
43
+ type HarnessDriver,
44
+ type HarnessStates,
45
+ } from './ai-harness.ts'
37
46
  import { gatewayModelId, readModelLimits } from './ai-model-metadata.ts'
38
47
  import { estimateInputTokens, measuredInputTokens } from './ai-input-tokens.ts'
39
48
  import { AI_STATE_REDUCER, agentContract, events as aiEvents } from './ai.ts'
@@ -66,6 +75,7 @@ import type {
66
75
  import { A2Error } from './errors.ts'
67
76
  import {
68
77
  consumeSchedulerSendFailure,
78
+ failedPreviousAttempt,
69
79
  installAmbientToolScopeStorage,
70
80
  type AmbientToolScope,
71
81
  } from './internal.ts'
@@ -141,7 +151,8 @@ export type AgentGenerateContext<
141
151
  > = AgentResolverContext<M, D, X> & {
142
152
  generationId: string
143
153
  modelMessages: ModelMessage[]
144
- model: LanguageModel
154
+ /** The resolved AI Gateway model ID. */
155
+ model: string
145
156
  tools: T
146
157
  instructions?: Instructions
147
158
  generation: AgentGenerationSettings<T>
@@ -188,7 +199,8 @@ export type Resolvable<T, C> = T | ((context: C) => T | Promise<T>)
188
199
 
189
200
  /** A model with its reasoning level and speed, chosen together. */
190
201
  export type ModelSettings = {
191
- model: LanguageModel
202
+ /** An AI Gateway model ID, such as `'anthropic/claude-sonnet-5.5'`. */
203
+ model: string
192
204
  /** Overrides `generation.reasoning`. */
193
205
  reasoning?: ReasoningLevel
194
206
  /** `fast` prefers AI Gateway's faster tier when the model offers one. */
@@ -235,36 +247,29 @@ export type HandlersOptions<
235
247
  > = AgentModelOptions<M, D, T, X> & {
236
248
  contract: Contract<D, Record<never, never>, unknown>
237
249
  telemetry?: A2Telemetry
250
+ /** Where harness steps keep their saved states; one per agent. */
251
+ harnessStates?: HarnessStates
238
252
  }
239
253
 
240
- export type AgentModelOptions<
241
- M extends UIMessage,
242
- D extends AIEventDefs<M> & EventDefs,
254
+ /** What a tool call does when a crash or a cancellation may have cut it short. */
255
+ export type ToolRecovery = 'rerun' | 'report'
256
+
257
+ /**
258
+ * The AI SDK harness, the default: one AI SDK `streamText()` step per
259
+ * durable generation, with A2 running tools, approvals, and compaction.
260
+ */
261
+ export type AISDKHarness<
262
+ M extends UIMessage = UIMessage,
263
+ D extends AIEventDefs<M> & EventDefs = AIEventDefs<M> & EventDefs,
243
264
  T extends ToolSet = ToolSet,
244
265
  X extends object = Record<never, never>,
245
266
  > = {
246
- /**
247
- * AI SDK model string or provider model, or `ModelSettings` to choose its
248
- * reasoning level and speed with it, optionally resolved per generation.
249
- */
250
- model: Resolvable<
251
- LanguageModel | ModelSettings,
252
- AgentResolverContext<M, D, X>
253
- >
254
- tools?: T
255
- instructions?: Resolvable<
256
- Instructions | undefined,
257
- AgentResolverContext<M, D, X>
258
- >
267
+ type: 'ai-sdk'
259
268
  /** Settings for each one-step AI SDK streamText call, resolved per generation. */
260
269
  generation?: Resolvable<
261
270
  AgentGenerationSettings<T>,
262
271
  AgentResolverContext<M, D, X>
263
272
  >
264
- /**
265
- * Maximum model steps in one assistant response. Defaults to Infinity.
266
- */
267
- maxSteps?: number
268
273
  /** Map default AI SDK stream parts to typed assistant message metadata. */
269
274
  messageMetadata?: AgentMessageMetadata<M, D, T, X>
270
275
  /**
@@ -276,6 +281,53 @@ export type AgentModelOptions<
276
281
  CompactionOptions<M, D, X>,
277
282
  AgentResolverContext<M, D, X>
278
283
  >
284
+ }
285
+
286
+ type AgentLoopOptions<
287
+ M extends UIMessage,
288
+ D extends AIEventDefs<M> & EventDefs,
289
+ T extends ToolSet,
290
+ X extends object,
291
+ H extends string,
292
+ > =
293
+ | {
294
+ /** The agent's loop: the AI SDK's (the default), or `fx()` or `pi()`. */
295
+ harness?: AISDKHarness<M, D, T, X>
296
+ /** Per tool name: rerun an interrupted call (the default) or report it to the model. */
297
+ toolRecovery?: Readonly<
298
+ Partial<Record<NoInfer<keyof T & string>, ToolRecovery>>
299
+ >
300
+ }
301
+ | {
302
+ harness: Harness<'fx' | 'pi', H>
303
+ /** Per tool name, the harness's own tools included: rerun an interrupted call (the default) or report it. */
304
+ toolRecovery?: Readonly<
305
+ Partial<Record<NoInfer<(keyof T & string) | H>, ToolRecovery>>
306
+ >
307
+ }
308
+
309
+ export type AgentModelOptions<
310
+ M extends UIMessage,
311
+ D extends AIEventDefs<M> & EventDefs,
312
+ T extends ToolSet = ToolSet,
313
+ X extends object = Record<never, never>,
314
+ /** The tool names a harness from `fx()` or `pi()` brings. */
315
+ H extends string = string,
316
+ > = {
317
+ /**
318
+ * An AI Gateway model ID, or `ModelSettings` to choose its reasoning level
319
+ * and speed with it, optionally resolved per generation.
320
+ */
321
+ model: Resolvable<string | ModelSettings, AgentResolverContext<M, D, X>>
322
+ tools?: T
323
+ instructions?: Resolvable<
324
+ Instructions | undefined,
325
+ AgentResolverContext<M, D, X>
326
+ >
327
+ /**
328
+ * Maximum model steps in one assistant response. Defaults to Infinity.
329
+ */
330
+ maxSteps?: number
279
331
  /** Progress is durably flushed at either limit, whichever is reached first. */
280
332
  progress?: { maxChunks?: number; maxDelayMs?: number }
281
333
  /**
@@ -283,7 +335,7 @@ export type AgentModelOptions<
283
335
  * independent tools run concurrently.
284
336
  */
285
337
  toolLane?: string
286
- }
338
+ } & AgentLoopOptions<M, D, T, X, H>
287
339
 
288
340
  type AgentServerOptions<
289
341
  M extends UIMessage,
@@ -291,7 +343,8 @@ type AgentServerOptions<
291
343
  S,
292
344
  T extends ToolSet,
293
345
  X extends object = Record<never, never>,
294
- > = AgentModelOptions<M, D, T, X> &
346
+ H extends string = string,
347
+ > = AgentModelOptions<M, D, T, X, H> &
295
348
  Omit<
296
349
  ServerOptions<D, Record<never, never>, Reducer<D, S>>,
297
350
  'contract' | 'handlers'
@@ -304,11 +357,14 @@ export type AgentOptions<
304
357
  M extends UIMessage,
305
358
  E extends EventDefs,
306
359
  T extends ToolSet,
360
+ H extends string = string,
307
361
  > = AgentServerOptions<
308
362
  NoInfer<M>,
309
363
  AIEventDefs<NoInfer<M>> & E,
310
364
  AIState<NoInfer<M>>,
311
- T
365
+ T,
366
+ Record<never, never>,
367
+ H
312
368
  > & {
313
369
  /** Identity: prefixes storage keys and queue messages. */
314
370
  name: string
@@ -327,7 +383,8 @@ export type AgentFromContractOptions<
327
383
  T extends ToolSet,
328
384
  Name extends string = string,
329
385
  X extends object = Record<never, never>,
330
- > = AgentServerOptions<AIMessageOf<D>, D, S, T, X> & {
386
+ H extends string = string,
387
+ > = AgentServerOptions<AIMessageOf<D>, D, S, T, X, H> & {
331
388
  /**
332
389
  * A shared `agentContract()`. Its application state reaches every resolver
333
390
  * as `state`; a fully custom contract is core A2, served by `createServer()`.
@@ -407,13 +464,14 @@ const withModelSettings = <T extends ToolSet>(options: {
407
464
  }
408
465
  }
409
466
 
410
- const modelName = (model: LanguageModel): string => {
411
- if (typeof model === 'string') return model
412
- if ('modelId' in model && typeof model.modelId === 'string') {
413
- return model.modelId
414
- }
415
- return 'custom'
416
- }
467
+ const instructionTexts = (instructions: Instructions | undefined): string[] =>
468
+ instructions === undefined
469
+ ? []
470
+ : typeof instructions === 'string'
471
+ ? [instructions]
472
+ : (Array.isArray(instructions) ? instructions : [instructions]).map(
473
+ (message) => message.content,
474
+ )
417
475
 
418
476
  const errorMessage = (error: unknown): string =>
419
477
  error instanceof Error ? error.message : String(error)
@@ -622,7 +680,15 @@ async function* consumeGeneration(options: {
622
680
  chunks: mergeDeltas(pending.splice(0)),
623
681
  })
624
682
  const reader = options.source.stream.getReader()
625
- let pendingChunk = reader.read()
683
+ // The next read starts before a batch is yielded, and the consumer may
684
+ // take a while to ask again: a stream that fails meanwhile must not leave
685
+ // that read rejected with no handler, which ends the process.
686
+ const readAhead = (): Promise<StreamRead<UIMessageChunk>> => {
687
+ const read = reader.read()
688
+ read.catch(() => {})
689
+ return read
690
+ }
691
+ let pendingChunk = readAhead()
626
692
  try {
627
693
  for (;;) {
628
694
  let result: StreamRead<UIMessageChunk>
@@ -643,7 +709,7 @@ async function* consumeGeneration(options: {
643
709
  pending.push(chunk)
644
710
  if (chunk.type === 'finish') streamedFinishReason = chunk.finishReason
645
711
  if (chunk.type === 'error') streamedError = chunk.errorText
646
- pendingChunk = reader.read()
712
+ pendingChunk = readAhead()
647
713
 
648
714
  if (pending.length >= maxChunks || isBoundaryChunk(pending.at(-1))) {
649
715
  yield batch()
@@ -654,7 +720,6 @@ async function* consumeGeneration(options: {
654
720
  if (pending.length > 0) yield batch()
655
721
  throw error
656
722
  } finally {
657
- void pendingChunk.catch(() => {})
658
723
  try {
659
724
  await reader.cancel()
660
725
  } catch {
@@ -877,7 +942,8 @@ const lifecycleEvents = async <
877
942
  pending: PendingToolCall[]
878
943
  tools: T
879
944
  generation: AgentGenerationSettings<T>
880
- custom: boolean
945
+ /** A harness's tool calls run without approval. */
946
+ approvals?: boolean
881
947
  }): Promise<{ events: AppendInput<D>[]; pending: PendingToolCall[] }> => {
882
948
  const result: AppendInput<D>[] = []
883
949
  const pending = [...options.pending]
@@ -927,11 +993,14 @@ const lifecycleEvents = async <
927
993
  ...(chunk.title === undefined ? {} : { title: chunk.title }),
928
994
  }
929
995
  take(payload.toolCallId)
930
- const classification = approvalClassification(
931
- options.tools,
932
- options.generation,
933
- chunk.toolName,
934
- )
996
+ const classification =
997
+ options.approvals === false
998
+ ? 'automatic'
999
+ : approvalClassification(
1000
+ options.tools,
1001
+ options.generation,
1002
+ chunk.toolName,
1003
+ )
935
1004
  if (classification === 'automatic') {
936
1005
  result.push(await toolCalledEvent<D>(payload))
937
1006
  } else {
@@ -1142,6 +1211,26 @@ const validateCompaction = <
1142
1211
  return compaction
1143
1212
  }
1144
1213
 
1214
+ // Options of the AI SDK harness, which live in it, not on agent().
1215
+ const AI_SDK_HARNESS_OPTIONS = [
1216
+ 'generation',
1217
+ 'generate',
1218
+ 'compaction',
1219
+ 'messageMetadata',
1220
+ ] as const
1221
+
1222
+ const assertNoLoopOptions = (options: { harness?: { type: string } }): void => {
1223
+ const harness = options.harness?.type ?? 'ai-sdk'
1224
+ for (const key of AI_SDK_HARNESS_OPTIONS) {
1225
+ if (!Object.hasOwn(options, key)) continue
1226
+ throw new TypeError(
1227
+ harness === 'ai-sdk'
1228
+ ? `${key} belongs to the harness: agent({ harness: { type: 'ai-sdk', ${key} } })`
1229
+ : `${key} is an option of the AI SDK harness; ${harness}() runs its own loop`,
1230
+ )
1231
+ }
1232
+ }
1233
+
1145
1234
  export function createHandlers<
1146
1235
  M extends UIMessage,
1147
1236
  D extends AIEventDefs<M> & EventDefs,
@@ -1175,22 +1264,63 @@ export function createHandlers<
1175
1264
  throw new TypeError('maxSteps must be a positive integer or Infinity')
1176
1265
  }
1177
1266
 
1267
+ assertNoLoopOptions(options)
1268
+ const configured = options.harness
1269
+ const loop: Omit<AISDKHarness<M, D, T, X>, 'type'> = configured?.type ===
1270
+ 'ai-sdk'
1271
+ ? configured
1272
+ : {}
1273
+ const harness: HarnessDriver | undefined =
1274
+ configured === undefined || configured.type === 'ai-sdk'
1275
+ ? undefined
1276
+ : harnessDriver(configured)
1277
+ const states = options.harnessStates ?? harnessStates()
1278
+ if (harness !== undefined) {
1279
+ for (const [name, tool] of Object.entries(options.tools ?? {})) {
1280
+ const needsApproval = (tool as { needsApproval?: unknown }).needsApproval
1281
+ if (needsApproval !== undefined && needsApproval !== false)
1282
+ throw new TypeError(
1283
+ `a harness runs tools without approval; '${name}' sets needsApproval`,
1284
+ )
1285
+ }
1286
+ }
1287
+
1178
1288
  const configuredCompaction =
1179
- options.compaction ?? (options.generate === undefined ? {} : false)
1289
+ loop.compaction ??
1290
+ (loop.generate === undefined && harness === undefined ? {} : false)
1180
1291
  const staticCompaction =
1181
1292
  typeof configuredCompaction === 'function'
1182
1293
  ? undefined
1183
1294
  : validateCompaction({
1184
1295
  compaction: configuredCompaction,
1185
- explicit: options.compaction !== undefined,
1296
+ explicit: loop.compaction !== undefined,
1186
1297
  generation:
1187
- typeof options.generation === 'function'
1188
- ? undefined
1189
- : options.generation,
1298
+ typeof loop.generation === 'function' ? undefined : loop.generation,
1190
1299
  tools: options.tools,
1191
1300
  })
1192
1301
 
1193
- const tools = options.tools ?? ({} as T)
1302
+ // The harness's own tools come first, so an agent tool with a name replaces them.
1303
+ const tools = (
1304
+ harness === undefined
1305
+ ? (options.tools ?? {})
1306
+ : { ...harness.tools, ...options.tools }
1307
+ ) as T
1308
+ const toolRecovery: Readonly<Record<string, ToolRecovery | undefined>> =
1309
+ options.toolRecovery ?? {}
1310
+ for (const [name, recovery] of Object.entries(toolRecovery)) {
1311
+ if (!Object.hasOwn(tools, name))
1312
+ throw new TypeError(
1313
+ `toolRecovery names '${name}', which is not one of the agent's tools`,
1314
+ )
1315
+ if (typeof (tools[name] as RuntimeTool).execute !== 'function')
1316
+ throw new TypeError(
1317
+ `toolRecovery names '${name}', which has no execute function`,
1318
+ )
1319
+ if (recovery !== 'rerun' && recovery !== 'report')
1320
+ throw new TypeError(
1321
+ `toolRecovery for '${name}' must be 'rerun' or 'report'`,
1322
+ )
1323
+ }
1194
1324
  const telemetry = options.telemetry ?? CONSOLE_TELEMETRY
1195
1325
  const maxSteps = options.maxSteps ?? Number.POSITIVE_INFINITY
1196
1326
  const control = createControlRuntime({
@@ -1250,6 +1380,7 @@ export function createHandlers<
1250
1380
  toolCallId: string
1251
1381
  messages: ModelMessage[]
1252
1382
  abortSignal?: AbortSignal
1383
+ experimental_sandbox?: Experimental_SandboxSession
1253
1384
  },
1254
1385
  ) => unknown
1255
1386
  }
@@ -1366,6 +1497,7 @@ export function createHandlers<
1366
1497
  call: ToolCalledPayload,
1367
1498
  execute: NonNullable<RuntimeTool['execute']>,
1368
1499
  messages: ModelMessage[],
1500
+ sandbox: Experimental_SandboxSession | undefined,
1369
1501
  ): Promise<AppendInput<D> | void> => {
1370
1502
  let output: unknown
1371
1503
  try {
@@ -1373,6 +1505,7 @@ export function createHandlers<
1373
1505
  toolCallId: call.toolCallId,
1374
1506
  messages,
1375
1507
  abortSignal: ctx.signal,
1508
+ ...(sandbox === undefined ? {} : { experimental_sandbox: sandbox }),
1376
1509
  })
1377
1510
  } catch (error) {
1378
1511
  return toolExecutionFailure(ctx, call, error)
@@ -1449,7 +1582,25 @@ export function createHandlers<
1449
1582
  error: `Tool '${call.toolName}' has no server executor`,
1450
1583
  })
1451
1584
  }
1585
+ const report = toolRecovery[call.toolName] === 'report'
1586
+ const interrupted = () =>
1587
+ resultEvent(call, `${ctx.event.id}:execution:error`, {
1588
+ error: `Tool '${call.toolName}' was interrupted and may have partly run.`,
1589
+ })
1590
+ // A thrown failure never follows `execute` for a reported tool (see
1591
+ // below), so only a lapsed attempt may have reached it.
1592
+ if (report && ctx.attempt > 1 && !failedPreviousAttempt.has(ctx))
1593
+ return interrupted()
1452
1594
  const messages = await promptMessages(input)
1595
+ // A sandbox that fails to resolve fails the attempt, which A2 retries;
1596
+ // it is not the tool's failure to report to the model.
1597
+ const sandbox =
1598
+ harness?.sandbox === undefined
1599
+ ? undefined
1600
+ : await resolve(harness.sandbox, {
1601
+ session: { id: ctx.session.id },
1602
+ signal: ctx.signal,
1603
+ })
1453
1604
  if (checkAbort(ctx.signal)) return
1454
1605
  const scope: AmbientToolScope = {
1455
1606
  contract: options.contract,
@@ -1457,9 +1608,19 @@ export function createHandlers<
1457
1608
  }
1458
1609
  // Async iteration resumes in the caller's context, so the whole
1459
1610
  // execution, not just the execute call, runs inside the scope.
1460
- return toolScopeStorage.run(scope, () =>
1461
- runToolExecution(ctx, call, execute, messages),
1462
- )
1611
+ const run = () =>
1612
+ toolScopeStorage.run(scope, () =>
1613
+ runToolExecution(ctx, call, execute, messages, sandbox),
1614
+ )
1615
+ if (!report) return run()
1616
+ try {
1617
+ // Undefined means a user cancellation after `execute` started.
1618
+ return (await run()) ?? (await interrupted())
1619
+ } catch {
1620
+ // A lapsed claim surrenders; the next attempt reports the call.
1621
+ checkAbort(ctx.signal)
1622
+ return interrupted()
1623
+ }
1463
1624
  }
1464
1625
 
1465
1626
  const handleToolExecution = async (
@@ -1632,19 +1793,17 @@ export function createHandlers<
1632
1793
  } as AppendInput<D>)
1633
1794
  return events
1634
1795
  }
1635
- const startedPayload = (
1636
- model?: LanguageModel,
1637
- ): GenerationStartedPayload => ({
1796
+ const startedPayload = (model?: string): GenerationStartedPayload => ({
1638
1797
  requestId,
1639
1798
  messageId: request.messageId,
1640
1799
  generationId,
1641
1800
  responseMessageId,
1642
1801
  attempt,
1643
- ...(model === undefined ? {} : { model: modelName(model) }),
1802
+ ...(model === undefined ? {} : { model }),
1644
1803
  promptThroughIndex: snapshot.index,
1645
1804
  })
1646
1805
 
1647
- let resolvedModel: LanguageModel | undefined
1806
+ let resolvedModel: string | undefined
1648
1807
  let resolving = true
1649
1808
  let generationStarted = false
1650
1809
  try {
@@ -1653,15 +1812,10 @@ export function createHandlers<
1653
1812
  throw new TypeError('the model resolver returned undefined')
1654
1813
  }
1655
1814
  const settings: ModelSettings =
1656
- typeof choice === 'string' || 'specificationVersion' in choice
1657
- ? { model: choice }
1658
- : choice
1815
+ typeof choice === 'string' ? { model: choice } : choice
1659
1816
  const model = settings.model
1660
- if (
1661
- typeof model !== 'string' &&
1662
- (typeof model !== 'object' || model === null)
1663
- )
1664
- throw new TypeError('the model resolver must return a model')
1817
+ if (typeof model !== 'string' || model === '')
1818
+ throw new TypeError('the model must be an AI Gateway model ID')
1665
1819
  resolvedModel = model
1666
1820
  if (checkAbort(ctx.signal)) return
1667
1821
  const resolvedInstructions =
@@ -1670,7 +1824,7 @@ export function createHandlers<
1670
1824
  : await resolve(options.instructions, resolverContext)
1671
1825
  if (checkAbort(ctx.signal)) return
1672
1826
  const resolvedGeneration: AgentGenerationSettings<T> = await resolve(
1673
- options.generation ?? {},
1827
+ loop.generation ?? {},
1674
1828
  resolverContext,
1675
1829
  )
1676
1830
  if (checkAbort(ctx.signal)) return
@@ -1686,13 +1840,13 @@ export function createHandlers<
1686
1840
  })
1687
1841
  const compaction =
1688
1842
  typeof configuredCompaction === 'function' ||
1689
- typeof options.generation === 'function'
1843
+ typeof loop.generation === 'function'
1690
1844
  ? validateCompaction({
1691
1845
  compaction:
1692
1846
  typeof configuredCompaction === 'function'
1693
1847
  ? await resolve(configuredCompaction, resolverContext)
1694
1848
  : staticCompaction!,
1695
- explicit: options.compaction !== undefined,
1849
+ explicit: loop.compaction !== undefined,
1696
1850
  generation,
1697
1851
  tools: options.tools,
1698
1852
  })
@@ -1802,7 +1956,7 @@ export function createHandlers<
1802
1956
  })
1803
1957
  const inputTokens = measuredInputTokens({
1804
1958
  estimate: inputTokenEstimate,
1805
- model: modelName(resolvedModel),
1959
+ model: resolvedModel,
1806
1960
  calibration: coordinatorState.calibration,
1807
1961
  })
1808
1962
  const threshold =
@@ -1988,15 +2142,40 @@ export function createHandlers<
1988
2142
  messages: generationMessages,
1989
2143
  modelMessages,
1990
2144
  }
1991
- const custom = options.generate !== undefined
1992
2145
  const source =
1993
- options.generate === undefined
1994
- ? await generateWithAISDK({
2146
+ harness !== undefined
2147
+ ? await harness.step({
1995
2148
  agentName: options.contract.name,
1996
- context: generateContext,
1997
- messageMetadata: options.messageMetadata,
2149
+ sessionId: ctx.event.sessionId,
2150
+ requestIndex: ctx.event.index,
2151
+ generationId,
2152
+ responseMessageId,
2153
+ throughIndex: snapshot.index,
2154
+ messages: generationMessages,
2155
+ model,
2156
+ ...(settings.reasoning === undefined
2157
+ ? {}
2158
+ : { reasoning: settings.reasoning }),
2159
+ ...(settings.speed === undefined
2160
+ ? {}
2161
+ : { speed: settings.speed }),
2162
+ instructions: instructionTexts(resolvedInstructions),
2163
+ tools,
2164
+ log: harnessLog({
2165
+ states,
2166
+ session: ctx.session as Parameters<
2167
+ typeof harnessLog
2168
+ >[0]['session'],
2169
+ }),
2170
+ signal: ctx.signal,
1998
2171
  })
1999
- : { stream: await options.generate(generateContext) }
2172
+ : loop.generate === undefined
2173
+ ? await generateWithAISDK({
2174
+ agentName: options.contract.name,
2175
+ context: generateContext,
2176
+ messageMetadata: loop.messageMetadata,
2177
+ })
2178
+ : { stream: await loop.generate(generateContext) }
2000
2179
  const updates = consumeGeneration({
2001
2180
  source,
2002
2181
  ...(options.progress === undefined
@@ -2025,7 +2204,7 @@ export function createHandlers<
2025
2204
  pending: pendingToolCalls,
2026
2205
  tools,
2027
2206
  generation,
2028
- custom,
2207
+ approvals: harness === undefined,
2029
2208
  })
2030
2209
  pendingToolCalls = lifecycle.pending
2031
2210
  await control.append({
@@ -2064,7 +2243,7 @@ export function createHandlers<
2064
2243
  id: await aiEventId('a2.ai.completed', generationId),
2065
2244
  payload: completed,
2066
2245
  } as AppendInput<D>
2067
- const unresolved = options.generate
2246
+ const unresolved = loop.generate
2068
2247
  ? pendingToolCalls.filter(
2069
2248
  (pending) => pending.classification !== 'automatic',
2070
2249
  )
@@ -2079,7 +2258,7 @@ export function createHandlers<
2079
2258
  .filter(
2080
2259
  (pending) =>
2081
2260
  pending.classification === 'automatic' ||
2082
- (options.generate === undefined &&
2261
+ (loop.generate === undefined &&
2083
2262
  pending.classification === 'unknown'),
2084
2263
  )
2085
2264
  .map((pending) => toolCalledEvent<D>(pending.call)),
@@ -2255,8 +2434,9 @@ export function agent<
2255
2434
  T extends ToolSet = ToolSet,
2256
2435
  M extends UIMessage = UIMessage,
2257
2436
  E extends EventDefs = Record<never, never>,
2437
+ H extends string = never,
2258
2438
  >(
2259
- options: AgentOptions<M, E, T>,
2439
+ options: AgentOptions<M, E, T, H>,
2260
2440
  ): Agent<AIEventDefs<M> & E, AIState<M>, typeof AI_STATE_REDUCER>
2261
2441
  export function agent<
2262
2442
  D extends AIEventDefs<AIMessageOf<D>> & EventDefs,
@@ -2264,22 +2444,22 @@ export function agent<
2264
2444
  T extends ToolSet = ToolSet,
2265
2445
  Name extends string = string,
2266
2446
  X extends object = Record<never, never>,
2267
- >(options: AgentFromContractOptions<D, S, T, Name, X>): Agent<D, S, Name>
2447
+ H extends string = never,
2448
+ >(options: AgentFromContractOptions<D, S, T, Name, X, H>): Agent<D, S, Name>
2268
2449
  export function agent(
2269
2450
  options:
2270
2451
  | AgentOptions<UIMessage, EventDefs, ToolSet>
2271
2452
  | AgentFromContractOptions<AIEventDefs & EventDefs, AIState, ToolSet>,
2272
2453
  ): Agent<AIEventDefs & EventDefs, AIState> {
2273
2454
  type D = AIEventDefs & EventDefs
2455
+ assertNoLoopOptions(options)
2274
2456
  const {
2275
2457
  model,
2276
2458
  tools,
2277
2459
  instructions,
2278
- generation,
2279
2460
  maxSteps,
2280
- messageMetadata,
2281
- generate,
2282
- compaction,
2461
+ harness,
2462
+ toolRecovery,
2283
2463
  progress,
2284
2464
  toolLane,
2285
2465
  handlers,
@@ -2307,22 +2487,22 @@ export function agent(
2307
2487
  })
2308
2488
  serverOptions = remaining
2309
2489
  }
2490
+ const states = harnessStates()
2310
2491
  const builtIns = createHandlers<UIMessage, D>({
2311
2492
  contract,
2312
2493
  model,
2494
+ harnessStates: states,
2313
2495
  ...(serverOptions.telemetry === undefined
2314
2496
  ? {}
2315
2497
  : { telemetry: serverOptions.telemetry }),
2316
2498
  ...(tools === undefined ? {} : { tools }),
2317
2499
  ...(instructions === undefined ? {} : { instructions }),
2318
- ...(generation === undefined ? {} : { generation }),
2319
2500
  ...(maxSteps === undefined ? {} : { maxSteps }),
2320
- ...(messageMetadata === undefined ? {} : { messageMetadata }),
2321
- ...(generate === undefined ? {} : { generate }),
2322
- ...(compaction === undefined ? {} : { compaction }),
2501
+ ...(harness === undefined ? {} : { harness }),
2502
+ ...(toolRecovery === undefined ? {} : { toolRecovery }),
2323
2503
  ...(progress === undefined ? {} : { progress }),
2324
2504
  ...(toolLane === undefined ? {} : { toolLane }),
2325
- })
2505
+ } as HandlersOptions<UIMessage, D>)
2326
2506
  for (const type of Object.keys(handlers ?? {})) {
2327
2507
  if (Object.hasOwn(builtIns, type))
2328
2508
  throw new TypeError(`agent handles '${type}' itself`)
@@ -2332,6 +2512,7 @@ export function agent(
2332
2512
  contract,
2333
2513
  handlers: { ...builtIns, ...handlers },
2334
2514
  })
2515
+ registerHarnessStates(server, states)
2335
2516
  const applicationEvents = 'contract' in rest
2336
2517
  setServerFetchHooks(server, {
2337
2518
  validateIngress: (context) =>
@@ -2340,4 +2521,4 @@ export function agent(
2340
2521
  return server
2341
2522
  }
2342
2523
 
2343
- export type { Instructions, LanguageModel, ToolSet }
2524
+ export type { Instructions, ToolSet }
@@ -0,0 +1,5 @@
1
+ /** experimental-a2/ai/fx is server-only; browsers reach an agent through its route. */
2
+ throw new Error(
3
+ 'experimental-a2/ai/fx is server-only — a client bundle imported it. Keep agent({ harness: fx() }) in server code.',
4
+ )
5
+ export {}