experimental-a2 0.17.0 → 0.18.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 (211) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-DBbe5xeE.d.ts → actor-Bjce9_5U.d.ts} +5 -5
  4. package/dist/{actor-DBbe5xeE.d.ts.map → actor-Bjce9_5U.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-UrWfKeuv.js → ai-BpOGm-zB.js} +2 -2
  11. package/dist/{ai-UrWfKeuv.js.map → ai-BpOGm-zB.js.map} +1 -1
  12. package/dist/{ai-vpGtcaCB.d.ts → ai-DrUaMyRt.d.ts} +3 -3
  13. package/dist/{ai-vpGtcaCB.d.ts.map → ai-DrUaMyRt.d.ts.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 +634 -0
  22. package/dist/ai-fx.js.map +1 -0
  23. package/dist/ai-harness-BuLh_EYi.js +273 -0
  24. package/dist/ai-harness-BuLh_EYi.js.map +1 -0
  25. package/dist/ai-harness-DvsPeJeB.d.ts +26 -0
  26. package/dist/ai-harness-DvsPeJeB.d.ts.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 +904 -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 +106 -37
  39. package/dist/ai-server.js.map +1 -1
  40. package/dist/ai.d.ts +1 -1
  41. package/dist/ai.js +1 -1
  42. package/dist/bounded-cache-CPHWF-8h.js +41 -0
  43. package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
  44. package/dist/{client-COaiaDub.js → client-BReKqwZC.js} +3 -3
  45. package/dist/{client-COaiaDub.js.map → client-BReKqwZC.js.map} +1 -1
  46. package/dist/{client-qRnRUzBS.d.ts → client-h_DKFAjj.d.ts} +3 -3
  47. package/dist/{client-qRnRUzBS.d.ts.map → client-h_DKFAjj.d.ts.map} +1 -1
  48. package/dist/client.d.ts +3 -3
  49. package/dist/client.js +1 -1
  50. package/dist/index.d.ts +5 -5
  51. package/dist/{initial-snapshot-BrLso5x_.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +2 -2
  52. package/dist/{initial-snapshot-BrLso5x_.d.ts.map → initial-snapshot-Bx-c0pDP.d.ts.map} +1 -1
  53. package/dist/{internal-CbVvEwad.js → internal-B2T3U4Rq.js} +8 -2
  54. package/dist/internal-B2T3U4Rq.js.map +1 -0
  55. package/dist/otel.d.ts +1 -1
  56. package/dist/{react-BbJ-g6Uu.d.ts → react-BBXgHWFc.d.ts} +4 -4
  57. package/dist/{react-BbJ-g6Uu.d.ts.map → react-BBXgHWFc.d.ts.map} +1 -1
  58. package/dist/react.d.ts +1 -1
  59. package/dist/react.js +1 -1
  60. package/dist/reducer-BAXc8JyF.js.map +1 -1
  61. package/dist/{reducer-BYu6DT7h.d.ts → reducer-CqA59Q6U.d.ts} +2 -2
  62. package/dist/{reducer-BYu6DT7h.d.ts.map → reducer-CqA59Q6U.d.ts.map} +1 -1
  63. package/dist/scheduler-qstash.d.ts +2 -2
  64. package/dist/scheduler-qstash.js +2 -2
  65. package/dist/scheduler-vercel.d.ts +2 -2
  66. package/dist/scheduler-vercel.js +2 -2
  67. package/dist/{server-Rj64fNvX.js → server-Dw_k_7kO.js} +93 -60
  68. package/dist/server-Dw_k_7kO.js.map +1 -0
  69. package/dist/{server-CGM8WzRG.d.ts → server-En7cbi3v.d.ts} +36 -7
  70. package/dist/server-En7cbi3v.d.ts.map +1 -0
  71. package/dist/server.d.ts +4 -4
  72. package/dist/server.js +1 -1
  73. package/dist/{session-clear-Dshpuhff.d.ts → session-clear-Bo-XTFEy.d.ts} +2 -2
  74. package/dist/session-clear-Bo-XTFEy.d.ts.map +1 -0
  75. package/dist/{store-Gpkq7WBo.d.ts → store-Djdv0bnZ.d.ts} +31 -2
  76. package/dist/store-Djdv0bnZ.d.ts.map +1 -0
  77. package/dist/store-N8PXxDAS.js.map +1 -1
  78. package/dist/store-memory.d.ts +1 -1
  79. package/dist/store-memory.js +32 -6
  80. package/dist/store-memory.js.map +1 -1
  81. package/dist/{store-polling-4_y8lETm.js → store-polling-B5FcwJfo.js} +2 -2
  82. package/dist/{store-polling-4_y8lETm.js.map → store-polling-B5FcwJfo.js.map} +1 -1
  83. package/dist/store-postgres.d.ts +1 -1
  84. package/dist/store-postgres.js +54 -3
  85. package/dist/store-postgres.js.map +1 -1
  86. package/dist/{store-presence-polling-Q1C-k2dd.js → store-presence-polling-BAwS8RWn.js} +3 -3
  87. package/dist/{store-presence-polling-Q1C-k2dd.js.map → store-presence-polling-BAwS8RWn.js.map} +1 -1
  88. package/dist/store-redis-http.d.ts +1 -1
  89. package/dist/store-redis-http.js +3 -3
  90. package/dist/{store-redis-notify-tqWNnMRu.js → store-redis-notify-BRITti_o.js} +187 -68
  91. package/dist/store-redis-notify-BRITti_o.js.map +1 -0
  92. package/dist/store-redis.d.ts +1 -1
  93. package/dist/store-redis.js +1 -1
  94. package/dist/store-sqlite.d.ts +1 -1
  95. package/dist/store-sqlite.js +32 -2
  96. package/dist/store-sqlite.js.map +1 -1
  97. package/dist/{telemetry-cZH-cibj.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
  98. package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
  99. package/dist/{wire-DjfHOvdW.js → wire-rSUglYo8.js} +2 -2
  100. package/dist/{wire-DjfHOvdW.js.map → wire-rSUglYo8.js.map} +1 -1
  101. package/docs/01-quickstart.mdx +49 -83
  102. package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
  103. package/docs/actors/01-introduction.mdx +48 -86
  104. package/docs/actors/02-concurrency.mdx +43 -70
  105. package/docs/actors/03-timers.mdx +28 -42
  106. package/docs/actors/04-routes.mdx +110 -150
  107. package/docs/actors/meta.ts +1 -1
  108. package/docs/agents/01-introduction.mdx +257 -0
  109. package/docs/agents/02-tools.mdx +247 -0
  110. package/docs/agents/03-conversations.mdx +538 -0
  111. package/docs/agents/04-harnesses.mdx +136 -0
  112. package/docs/agents/meta.ts +1 -0
  113. package/docs/client/01-react.mdx +294 -0
  114. package/docs/client/02-transports.mdx +86 -0
  115. package/docs/client/03-presence.mdx +224 -0
  116. package/docs/client/meta.ts +1 -0
  117. package/docs/contract/01-events.mdx +57 -0
  118. package/docs/contract/02-state.mdx +142 -0
  119. package/docs/contract/03-sessions.mdx +175 -0
  120. package/docs/index.mdx +108 -144
  121. package/docs/reference/01-core.mdx +139 -0
  122. package/docs/reference/02-server.mdx +855 -0
  123. package/docs/reference/03-client.mdx +301 -0
  124. package/docs/reference/04-ai.mdx +722 -0
  125. package/docs/reference/05-adapters.mdx +351 -0
  126. package/docs/reference/06-errors.mdx +138 -0
  127. package/docs/reference/meta.ts +1 -1
  128. package/docs/server/01-handlers.mdx +200 -0
  129. package/docs/server/02-timers.mdx +130 -0
  130. package/docs/server/03-cancellation.mdx +141 -0
  131. package/docs/server/04-routes.mdx +243 -0
  132. package/docs/server/05-production.mdx +322 -0
  133. package/docs/server/06-guarantees.mdx +123 -0
  134. package/examples/playground/DEPLOY.md +1 -1
  135. package/examples/playground/app/a0/[appId]/page.tsx +11 -6
  136. package/examples/playground/app/a0/app-list.tsx +9 -2
  137. package/examples/playground/app/a0/app-view.tsx +9 -1
  138. package/examples/playground/app/a0/apps/[appId]/route.ts +10 -5
  139. package/examples/playground/app/a0/apps.test.ts +12 -0
  140. package/examples/playground/app/a0/apps.ts +22 -7
  141. package/examples/playground/app/a0/chat.tsx +47 -1
  142. package/examples/playground/app/a0/fx/events/route.ts +9 -0
  143. package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
  144. package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
  145. package/examples/playground/app/a0/model.test.ts +18 -1
  146. package/examples/playground/app/a0/model.ts +47 -10
  147. package/examples/playground/app/a0/no-photon.cjs +4 -0
  148. package/examples/playground/app/a0/no-quickjs.cjs +3 -0
  149. package/examples/playground/app/a0/pi/events/route.ts +9 -0
  150. package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
  151. package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
  152. package/examples/playground/app/a0/sandbox-log.ts +10 -4
  153. package/examples/playground/app/a0/sandbox-session.ts +152 -0
  154. package/examples/playground/app/a0/scheduler/route.ts +2 -2
  155. package/examples/playground/app/a0/server.ts +100 -45
  156. package/examples/playground/app/a0/session.ts +17 -5
  157. package/examples/playground/app/a0/shell.tsx +11 -3
  158. package/examples/playground/app/agent/ingress.test.ts +6 -3
  159. package/examples/playground/app/agent/server.ts +9 -6
  160. package/examples/playground/app/agent/telemetry.test.ts +71 -50
  161. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +1 -1
  162. package/examples/playground/app/chat/server.test.ts +30 -26
  163. package/examples/playground/app/chat/server.ts +46 -46
  164. package/examples/playground/next.config.ts +17 -5
  165. package/examples/playground/package.json +4 -1
  166. package/examples/playground/vercel.json +18 -0
  167. package/package.json +27 -3
  168. package/src/ai-agent.ts +261 -87
  169. package/src/ai-fx.browser.ts +5 -0
  170. package/src/ai-fx.ts +833 -0
  171. package/src/ai-harness.ts +518 -0
  172. package/src/ai-model-metadata.ts +11 -6
  173. package/src/ai-pi.browser.ts +5 -0
  174. package/src/ai-pi.ts +1263 -0
  175. package/src/ai-sdk-step.ts +2 -2
  176. package/src/ai-server.ts +3 -1
  177. package/src/internal.ts +7 -0
  178. package/src/libfx.d.ts +34 -0
  179. package/src/reducer.ts +1 -1
  180. package/src/server.ts +177 -19
  181. package/src/store-memory.ts +42 -7
  182. package/src/store-postgres.ts +80 -0
  183. package/src/store-redis-core.ts +210 -79
  184. package/src/store-redis-notify.ts +2 -0
  185. package/src/store-sqlite.ts +60 -0
  186. package/src/store.ts +33 -0
  187. package/src/telemetry.ts +1 -0
  188. package/dist/internal-CbVvEwad.js.map +0 -1
  189. package/dist/server-CGM8WzRG.d.ts.map +0 -1
  190. package/dist/server-Rj64fNvX.js.map +0 -1
  191. package/dist/session-clear-Dshpuhff.d.ts.map +0 -1
  192. package/dist/store-Gpkq7WBo.d.ts.map +0 -1
  193. package/dist/store-redis-notify-tqWNnMRu.js.map +0 -1
  194. package/dist/telemetry-cZH-cibj.d.ts.map +0 -1
  195. package/docs/concepts/01-contracts.mdx +0 -237
  196. package/docs/concepts/02-handlers.mdx +0 -284
  197. package/docs/concepts/03-durability.mdx +0 -314
  198. package/docs/concepts/04-state.mdx +0 -209
  199. package/docs/guides/01-timers.mdx +0 -193
  200. package/docs/guides/02-cancellation.mdx +0 -161
  201. package/docs/guides/03-react.mdx +0 -324
  202. package/docs/guides/04-local-first.mdx +0 -47
  203. package/docs/guides/05-production.mdx +0 -534
  204. package/docs/guides/06-ai-agents.mdx +0 -1408
  205. package/docs/guides/08-application-data.mdx +0 -113
  206. package/docs/guides/09-presence.mdx +0 -262
  207. package/docs/guides/10-transports.mdx +0 -221
  208. package/docs/reference/01-api.mdx +0 -2358
  209. package/docs/reference/02-errors.mdx +0 -150
  210. /package/docs/{concepts → contract}/meta.ts +0 -0
  211. /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)
@@ -877,7 +935,8 @@ const lifecycleEvents = async <
877
935
  pending: PendingToolCall[]
878
936
  tools: T
879
937
  generation: AgentGenerationSettings<T>
880
- custom: boolean
938
+ /** A harness's tool calls run without approval. */
939
+ approvals?: boolean
881
940
  }): Promise<{ events: AppendInput<D>[]; pending: PendingToolCall[] }> => {
882
941
  const result: AppendInput<D>[] = []
883
942
  const pending = [...options.pending]
@@ -927,11 +986,14 @@ const lifecycleEvents = async <
927
986
  ...(chunk.title === undefined ? {} : { title: chunk.title }),
928
987
  }
929
988
  take(payload.toolCallId)
930
- const classification = approvalClassification(
931
- options.tools,
932
- options.generation,
933
- chunk.toolName,
934
- )
989
+ const classification =
990
+ options.approvals === false
991
+ ? 'automatic'
992
+ : approvalClassification(
993
+ options.tools,
994
+ options.generation,
995
+ chunk.toolName,
996
+ )
935
997
  if (classification === 'automatic') {
936
998
  result.push(await toolCalledEvent<D>(payload))
937
999
  } else {
@@ -1142,6 +1204,26 @@ const validateCompaction = <
1142
1204
  return compaction
1143
1205
  }
1144
1206
 
1207
+ // Options of the AI SDK harness, which live in it, not on agent().
1208
+ const AI_SDK_HARNESS_OPTIONS = [
1209
+ 'generation',
1210
+ 'generate',
1211
+ 'compaction',
1212
+ 'messageMetadata',
1213
+ ] as const
1214
+
1215
+ const assertNoLoopOptions = (options: { harness?: { type: string } }): void => {
1216
+ const harness = options.harness?.type ?? 'ai-sdk'
1217
+ for (const key of AI_SDK_HARNESS_OPTIONS) {
1218
+ if (!Object.hasOwn(options, key)) continue
1219
+ throw new TypeError(
1220
+ harness === 'ai-sdk'
1221
+ ? `${key} belongs to the harness: agent({ harness: { type: 'ai-sdk', ${key} } })`
1222
+ : `${key} is an option of the AI SDK harness; ${harness}() runs its own loop`,
1223
+ )
1224
+ }
1225
+ }
1226
+
1145
1227
  export function createHandlers<
1146
1228
  M extends UIMessage,
1147
1229
  D extends AIEventDefs<M> & EventDefs,
@@ -1175,22 +1257,63 @@ export function createHandlers<
1175
1257
  throw new TypeError('maxSteps must be a positive integer or Infinity')
1176
1258
  }
1177
1259
 
1260
+ assertNoLoopOptions(options)
1261
+ const configured = options.harness
1262
+ const loop: Omit<AISDKHarness<M, D, T, X>, 'type'> = configured?.type ===
1263
+ 'ai-sdk'
1264
+ ? configured
1265
+ : {}
1266
+ const harness: HarnessDriver | undefined =
1267
+ configured === undefined || configured.type === 'ai-sdk'
1268
+ ? undefined
1269
+ : harnessDriver(configured)
1270
+ const states = options.harnessStates ?? harnessStates()
1271
+ if (harness !== undefined) {
1272
+ for (const [name, tool] of Object.entries(options.tools ?? {})) {
1273
+ const needsApproval = (tool as { needsApproval?: unknown }).needsApproval
1274
+ if (needsApproval !== undefined && needsApproval !== false)
1275
+ throw new TypeError(
1276
+ `a harness runs tools without approval; '${name}' sets needsApproval`,
1277
+ )
1278
+ }
1279
+ }
1280
+
1178
1281
  const configuredCompaction =
1179
- options.compaction ?? (options.generate === undefined ? {} : false)
1282
+ loop.compaction ??
1283
+ (loop.generate === undefined && harness === undefined ? {} : false)
1180
1284
  const staticCompaction =
1181
1285
  typeof configuredCompaction === 'function'
1182
1286
  ? undefined
1183
1287
  : validateCompaction({
1184
1288
  compaction: configuredCompaction,
1185
- explicit: options.compaction !== undefined,
1289
+ explicit: loop.compaction !== undefined,
1186
1290
  generation:
1187
- typeof options.generation === 'function'
1188
- ? undefined
1189
- : options.generation,
1291
+ typeof loop.generation === 'function' ? undefined : loop.generation,
1190
1292
  tools: options.tools,
1191
1293
  })
1192
1294
 
1193
- const tools = options.tools ?? ({} as T)
1295
+ // The harness's own tools come first, so an agent tool with a name replaces them.
1296
+ const tools = (
1297
+ harness === undefined
1298
+ ? (options.tools ?? {})
1299
+ : { ...harness.tools, ...options.tools }
1300
+ ) as T
1301
+ const toolRecovery: Readonly<Record<string, ToolRecovery | undefined>> =
1302
+ options.toolRecovery ?? {}
1303
+ for (const [name, recovery] of Object.entries(toolRecovery)) {
1304
+ if (!Object.hasOwn(tools, name))
1305
+ throw new TypeError(
1306
+ `toolRecovery names '${name}', which is not one of the agent's tools`,
1307
+ )
1308
+ if (typeof (tools[name] as RuntimeTool).execute !== 'function')
1309
+ throw new TypeError(
1310
+ `toolRecovery names '${name}', which has no execute function`,
1311
+ )
1312
+ if (recovery !== 'rerun' && recovery !== 'report')
1313
+ throw new TypeError(
1314
+ `toolRecovery for '${name}' must be 'rerun' or 'report'`,
1315
+ )
1316
+ }
1194
1317
  const telemetry = options.telemetry ?? CONSOLE_TELEMETRY
1195
1318
  const maxSteps = options.maxSteps ?? Number.POSITIVE_INFINITY
1196
1319
  const control = createControlRuntime({
@@ -1250,6 +1373,7 @@ export function createHandlers<
1250
1373
  toolCallId: string
1251
1374
  messages: ModelMessage[]
1252
1375
  abortSignal?: AbortSignal
1376
+ experimental_sandbox?: Experimental_SandboxSession
1253
1377
  },
1254
1378
  ) => unknown
1255
1379
  }
@@ -1366,6 +1490,7 @@ export function createHandlers<
1366
1490
  call: ToolCalledPayload,
1367
1491
  execute: NonNullable<RuntimeTool['execute']>,
1368
1492
  messages: ModelMessage[],
1493
+ sandbox: Experimental_SandboxSession | undefined,
1369
1494
  ): Promise<AppendInput<D> | void> => {
1370
1495
  let output: unknown
1371
1496
  try {
@@ -1373,6 +1498,7 @@ export function createHandlers<
1373
1498
  toolCallId: call.toolCallId,
1374
1499
  messages,
1375
1500
  abortSignal: ctx.signal,
1501
+ ...(sandbox === undefined ? {} : { experimental_sandbox: sandbox }),
1376
1502
  })
1377
1503
  } catch (error) {
1378
1504
  return toolExecutionFailure(ctx, call, error)
@@ -1449,7 +1575,25 @@ export function createHandlers<
1449
1575
  error: `Tool '${call.toolName}' has no server executor`,
1450
1576
  })
1451
1577
  }
1578
+ const report = toolRecovery[call.toolName] === 'report'
1579
+ const interrupted = () =>
1580
+ resultEvent(call, `${ctx.event.id}:execution:error`, {
1581
+ error: `Tool '${call.toolName}' was interrupted and may have partly run.`,
1582
+ })
1583
+ // A thrown failure never follows `execute` for a reported tool (see
1584
+ // below), so only a lapsed attempt may have reached it.
1585
+ if (report && ctx.attempt > 1 && !failedPreviousAttempt.has(ctx))
1586
+ return interrupted()
1452
1587
  const messages = await promptMessages(input)
1588
+ // A sandbox that fails to resolve fails the attempt, which A2 retries;
1589
+ // it is not the tool's failure to report to the model.
1590
+ const sandbox =
1591
+ harness?.sandbox === undefined
1592
+ ? undefined
1593
+ : await resolve(harness.sandbox, {
1594
+ session: { id: ctx.session.id },
1595
+ signal: ctx.signal,
1596
+ })
1453
1597
  if (checkAbort(ctx.signal)) return
1454
1598
  const scope: AmbientToolScope = {
1455
1599
  contract: options.contract,
@@ -1457,9 +1601,19 @@ export function createHandlers<
1457
1601
  }
1458
1602
  // Async iteration resumes in the caller's context, so the whole
1459
1603
  // execution, not just the execute call, runs inside the scope.
1460
- return toolScopeStorage.run(scope, () =>
1461
- runToolExecution(ctx, call, execute, messages),
1462
- )
1604
+ const run = () =>
1605
+ toolScopeStorage.run(scope, () =>
1606
+ runToolExecution(ctx, call, execute, messages, sandbox),
1607
+ )
1608
+ if (!report) return run()
1609
+ try {
1610
+ // Undefined means a user cancellation after `execute` started.
1611
+ return (await run()) ?? (await interrupted())
1612
+ } catch {
1613
+ // A lapsed claim surrenders; the next attempt reports the call.
1614
+ checkAbort(ctx.signal)
1615
+ return interrupted()
1616
+ }
1463
1617
  }
1464
1618
 
1465
1619
  const handleToolExecution = async (
@@ -1632,19 +1786,17 @@ export function createHandlers<
1632
1786
  } as AppendInput<D>)
1633
1787
  return events
1634
1788
  }
1635
- const startedPayload = (
1636
- model?: LanguageModel,
1637
- ): GenerationStartedPayload => ({
1789
+ const startedPayload = (model?: string): GenerationStartedPayload => ({
1638
1790
  requestId,
1639
1791
  messageId: request.messageId,
1640
1792
  generationId,
1641
1793
  responseMessageId,
1642
1794
  attempt,
1643
- ...(model === undefined ? {} : { model: modelName(model) }),
1795
+ ...(model === undefined ? {} : { model }),
1644
1796
  promptThroughIndex: snapshot.index,
1645
1797
  })
1646
1798
 
1647
- let resolvedModel: LanguageModel | undefined
1799
+ let resolvedModel: string | undefined
1648
1800
  let resolving = true
1649
1801
  let generationStarted = false
1650
1802
  try {
@@ -1653,15 +1805,10 @@ export function createHandlers<
1653
1805
  throw new TypeError('the model resolver returned undefined')
1654
1806
  }
1655
1807
  const settings: ModelSettings =
1656
- typeof choice === 'string' || 'specificationVersion' in choice
1657
- ? { model: choice }
1658
- : choice
1808
+ typeof choice === 'string' ? { model: choice } : choice
1659
1809
  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')
1810
+ if (typeof model !== 'string' || model === '')
1811
+ throw new TypeError('the model must be an AI Gateway model ID')
1665
1812
  resolvedModel = model
1666
1813
  if (checkAbort(ctx.signal)) return
1667
1814
  const resolvedInstructions =
@@ -1670,7 +1817,7 @@ export function createHandlers<
1670
1817
  : await resolve(options.instructions, resolverContext)
1671
1818
  if (checkAbort(ctx.signal)) return
1672
1819
  const resolvedGeneration: AgentGenerationSettings<T> = await resolve(
1673
- options.generation ?? {},
1820
+ loop.generation ?? {},
1674
1821
  resolverContext,
1675
1822
  )
1676
1823
  if (checkAbort(ctx.signal)) return
@@ -1686,13 +1833,13 @@ export function createHandlers<
1686
1833
  })
1687
1834
  const compaction =
1688
1835
  typeof configuredCompaction === 'function' ||
1689
- typeof options.generation === 'function'
1836
+ typeof loop.generation === 'function'
1690
1837
  ? validateCompaction({
1691
1838
  compaction:
1692
1839
  typeof configuredCompaction === 'function'
1693
1840
  ? await resolve(configuredCompaction, resolverContext)
1694
1841
  : staticCompaction!,
1695
- explicit: options.compaction !== undefined,
1842
+ explicit: loop.compaction !== undefined,
1696
1843
  generation,
1697
1844
  tools: options.tools,
1698
1845
  })
@@ -1802,7 +1949,7 @@ export function createHandlers<
1802
1949
  })
1803
1950
  const inputTokens = measuredInputTokens({
1804
1951
  estimate: inputTokenEstimate,
1805
- model: modelName(resolvedModel),
1952
+ model: resolvedModel,
1806
1953
  calibration: coordinatorState.calibration,
1807
1954
  })
1808
1955
  const threshold =
@@ -1988,15 +2135,40 @@ export function createHandlers<
1988
2135
  messages: generationMessages,
1989
2136
  modelMessages,
1990
2137
  }
1991
- const custom = options.generate !== undefined
1992
2138
  const source =
1993
- options.generate === undefined
1994
- ? await generateWithAISDK({
2139
+ harness !== undefined
2140
+ ? await harness.step({
1995
2141
  agentName: options.contract.name,
1996
- context: generateContext,
1997
- messageMetadata: options.messageMetadata,
2142
+ sessionId: ctx.event.sessionId,
2143
+ requestIndex: ctx.event.index,
2144
+ generationId,
2145
+ responseMessageId,
2146
+ throughIndex: snapshot.index,
2147
+ messages: generationMessages,
2148
+ model,
2149
+ ...(settings.reasoning === undefined
2150
+ ? {}
2151
+ : { reasoning: settings.reasoning }),
2152
+ ...(settings.speed === undefined
2153
+ ? {}
2154
+ : { speed: settings.speed }),
2155
+ instructions: instructionTexts(resolvedInstructions),
2156
+ tools,
2157
+ log: harnessLog({
2158
+ states,
2159
+ session: ctx.session as Parameters<
2160
+ typeof harnessLog
2161
+ >[0]['session'],
2162
+ }),
2163
+ signal: ctx.signal,
1998
2164
  })
1999
- : { stream: await options.generate(generateContext) }
2165
+ : loop.generate === undefined
2166
+ ? await generateWithAISDK({
2167
+ agentName: options.contract.name,
2168
+ context: generateContext,
2169
+ messageMetadata: loop.messageMetadata,
2170
+ })
2171
+ : { stream: await loop.generate(generateContext) }
2000
2172
  const updates = consumeGeneration({
2001
2173
  source,
2002
2174
  ...(options.progress === undefined
@@ -2025,7 +2197,7 @@ export function createHandlers<
2025
2197
  pending: pendingToolCalls,
2026
2198
  tools,
2027
2199
  generation,
2028
- custom,
2200
+ approvals: harness === undefined,
2029
2201
  })
2030
2202
  pendingToolCalls = lifecycle.pending
2031
2203
  await control.append({
@@ -2064,7 +2236,7 @@ export function createHandlers<
2064
2236
  id: await aiEventId('a2.ai.completed', generationId),
2065
2237
  payload: completed,
2066
2238
  } as AppendInput<D>
2067
- const unresolved = options.generate
2239
+ const unresolved = loop.generate
2068
2240
  ? pendingToolCalls.filter(
2069
2241
  (pending) => pending.classification !== 'automatic',
2070
2242
  )
@@ -2079,7 +2251,7 @@ export function createHandlers<
2079
2251
  .filter(
2080
2252
  (pending) =>
2081
2253
  pending.classification === 'automatic' ||
2082
- (options.generate === undefined &&
2254
+ (loop.generate === undefined &&
2083
2255
  pending.classification === 'unknown'),
2084
2256
  )
2085
2257
  .map((pending) => toolCalledEvent<D>(pending.call)),
@@ -2255,8 +2427,9 @@ export function agent<
2255
2427
  T extends ToolSet = ToolSet,
2256
2428
  M extends UIMessage = UIMessage,
2257
2429
  E extends EventDefs = Record<never, never>,
2430
+ H extends string = never,
2258
2431
  >(
2259
- options: AgentOptions<M, E, T>,
2432
+ options: AgentOptions<M, E, T, H>,
2260
2433
  ): Agent<AIEventDefs<M> & E, AIState<M>, typeof AI_STATE_REDUCER>
2261
2434
  export function agent<
2262
2435
  D extends AIEventDefs<AIMessageOf<D>> & EventDefs,
@@ -2264,22 +2437,22 @@ export function agent<
2264
2437
  T extends ToolSet = ToolSet,
2265
2438
  Name extends string = string,
2266
2439
  X extends object = Record<never, never>,
2267
- >(options: AgentFromContractOptions<D, S, T, Name, X>): Agent<D, S, Name>
2440
+ H extends string = never,
2441
+ >(options: AgentFromContractOptions<D, S, T, Name, X, H>): Agent<D, S, Name>
2268
2442
  export function agent(
2269
2443
  options:
2270
2444
  | AgentOptions<UIMessage, EventDefs, ToolSet>
2271
2445
  | AgentFromContractOptions<AIEventDefs & EventDefs, AIState, ToolSet>,
2272
2446
  ): Agent<AIEventDefs & EventDefs, AIState> {
2273
2447
  type D = AIEventDefs & EventDefs
2448
+ assertNoLoopOptions(options)
2274
2449
  const {
2275
2450
  model,
2276
2451
  tools,
2277
2452
  instructions,
2278
- generation,
2279
2453
  maxSteps,
2280
- messageMetadata,
2281
- generate,
2282
- compaction,
2454
+ harness,
2455
+ toolRecovery,
2283
2456
  progress,
2284
2457
  toolLane,
2285
2458
  handlers,
@@ -2307,22 +2480,22 @@ export function agent(
2307
2480
  })
2308
2481
  serverOptions = remaining
2309
2482
  }
2483
+ const states = harnessStates()
2310
2484
  const builtIns = createHandlers<UIMessage, D>({
2311
2485
  contract,
2312
2486
  model,
2487
+ harnessStates: states,
2313
2488
  ...(serverOptions.telemetry === undefined
2314
2489
  ? {}
2315
2490
  : { telemetry: serverOptions.telemetry }),
2316
2491
  ...(tools === undefined ? {} : { tools }),
2317
2492
  ...(instructions === undefined ? {} : { instructions }),
2318
- ...(generation === undefined ? {} : { generation }),
2319
2493
  ...(maxSteps === undefined ? {} : { maxSteps }),
2320
- ...(messageMetadata === undefined ? {} : { messageMetadata }),
2321
- ...(generate === undefined ? {} : { generate }),
2322
- ...(compaction === undefined ? {} : { compaction }),
2494
+ ...(harness === undefined ? {} : { harness }),
2495
+ ...(toolRecovery === undefined ? {} : { toolRecovery }),
2323
2496
  ...(progress === undefined ? {} : { progress }),
2324
2497
  ...(toolLane === undefined ? {} : { toolLane }),
2325
- })
2498
+ } as HandlersOptions<UIMessage, D>)
2326
2499
  for (const type of Object.keys(handlers ?? {})) {
2327
2500
  if (Object.hasOwn(builtIns, type))
2328
2501
  throw new TypeError(`agent handles '${type}' itself`)
@@ -2332,6 +2505,7 @@ export function agent(
2332
2505
  contract,
2333
2506
  handlers: { ...builtIns, ...handlers },
2334
2507
  })
2508
+ registerHarnessStates(server, states)
2335
2509
  const applicationEvents = 'contract' in rest
2336
2510
  setServerFetchHooks(server, {
2337
2511
  validateIngress: (context) =>
@@ -2340,4 +2514,4 @@ export function agent(
2340
2514
  return server
2341
2515
  }
2342
2516
 
2343
- export type { Instructions, LanguageModel, ToolSet }
2517
+ 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 {}