experimental-a2 0.3.0 → 0.5.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 (205) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/dist/ai-CFNeCrRl.d.ts +334 -0
  3. package/dist/ai-CFNeCrRl.d.ts.map +1 -0
  4. package/dist/ai-server.browser.js +2 -0
  5. package/dist/ai-server.browser.js.map +1 -0
  6. package/dist/ai-server.d.ts +49 -9
  7. package/dist/ai-server.d.ts.map +1 -0
  8. package/dist/ai-server.js +123 -49
  9. package/dist/ai-server.js.map +1 -0
  10. package/dist/ai.d.ts +2 -303
  11. package/dist/ai.js +233 -86
  12. package/dist/ai.js.map +1 -0
  13. package/dist/cli-B3VuxoDe.js +599 -0
  14. package/dist/cli-B3VuxoDe.js.map +1 -0
  15. package/dist/cli-bin.d.ts +1 -0
  16. package/dist/cli-bin.js +7 -0
  17. package/dist/cli-bin.js.map +1 -0
  18. package/dist/cli.d.ts +20 -0
  19. package/dist/cli.d.ts.map +1 -0
  20. package/dist/cli.js +2 -0
  21. package/dist/client-BKlyLiOU.js +1008 -0
  22. package/dist/client-BKlyLiOU.js.map +1 -0
  23. package/dist/client-D7mvIXrF.d.ts +191 -0
  24. package/dist/client-D7mvIXrF.d.ts.map +1 -0
  25. package/dist/client.d.ts +2 -90
  26. package/dist/client.js +1 -409
  27. package/dist/{contract-CG_adnu_.js → contract-48bUMgcL.js} +12 -2
  28. package/dist/contract-48bUMgcL.js.map +1 -0
  29. package/dist/{contract-C_3dIIEU.d.ts → contract-jIfaR085.d.ts} +63 -8
  30. package/dist/contract-jIfaR085.d.ts.map +1 -0
  31. package/dist/devtools-J_jZ2vQf.d.ts +152 -0
  32. package/dist/devtools-J_jZ2vQf.d.ts.map +1 -0
  33. package/dist/devtools-kJJaORn-.js +340 -0
  34. package/dist/devtools-kJJaORn-.js.map +1 -0
  35. package/dist/devtools-server.browser.js +3 -1
  36. package/dist/devtools-server.browser.js.map +1 -0
  37. package/dist/devtools-server.d.ts +4 -3
  38. package/dist/devtools-server.d.ts.map +1 -0
  39. package/dist/devtools-server.js +226 -43
  40. package/dist/devtools-server.js.map +1 -0
  41. package/dist/devtools.d.ts +2 -0
  42. package/dist/devtools.js +2 -0
  43. package/dist/{errors-BJRMd-h6.js → errors-BQuJpe82.js} +6 -4
  44. package/dist/errors-BQuJpe82.js.map +1 -0
  45. package/dist/{errors-xL_JTXsY.d.ts → errors-W6nwJ-fm.d.ts} +3 -2
  46. package/dist/errors-W6nwJ-fm.d.ts.map +1 -0
  47. package/dist/http.d.ts +135 -28
  48. package/dist/http.d.ts.map +1 -0
  49. package/dist/http.js +647 -61
  50. package/dist/http.js.map +1 -0
  51. package/dist/{idempotent-replay-BMyHrP0L.js → idempotent-replay-DuqEkYA7.js} +4 -2
  52. package/dist/idempotent-replay-DuqEkYA7.js.map +1 -0
  53. package/dist/index.d.ts +5 -5
  54. package/dist/index.js +2 -2
  55. package/dist/{inspection-E7qbD0Xj.js → inspection-DaxB5jM2.js} +4 -1
  56. package/dist/inspection-DaxB5jM2.js.map +1 -0
  57. package/dist/internal-DstsI6Re.js +211 -0
  58. package/dist/internal-DstsI6Re.js.map +1 -0
  59. package/dist/otel.d.ts +3 -2
  60. package/dist/otel.d.ts.map +1 -0
  61. package/dist/otel.js +2 -0
  62. package/dist/otel.js.map +1 -0
  63. package/dist/platform-B4TnJtWu.js +34 -0
  64. package/dist/platform-B4TnJtWu.js.map +1 -0
  65. package/dist/react.d.ts +47 -15
  66. package/dist/react.d.ts.map +1 -0
  67. package/dist/react.js +30 -15
  68. package/dist/react.js.map +1 -0
  69. package/dist/retryable-lazy-DZWmHpii.js +2 -0
  70. package/dist/retryable-lazy-DZWmHpii.js.map +1 -0
  71. package/dist/scheduler-qstash.d.ts +79 -0
  72. package/dist/scheduler-qstash.d.ts.map +1 -0
  73. package/dist/scheduler-qstash.js +501 -0
  74. package/dist/scheduler-qstash.js.map +1 -0
  75. package/dist/scheduler-task-BpzhPnRS.js +56 -0
  76. package/dist/scheduler-task-BpzhPnRS.js.map +1 -0
  77. package/dist/{recovery-vercel.d.ts → scheduler-vercel.d.ts} +18 -24
  78. package/dist/scheduler-vercel.d.ts.map +1 -0
  79. package/dist/scheduler-vercel.js +228 -0
  80. package/dist/scheduler-vercel.js.map +1 -0
  81. package/dist/server-C72KOw51.js +1457 -0
  82. package/dist/server-C72KOw51.js.map +1 -0
  83. package/dist/server-DUF9pjsx.d.ts +261 -0
  84. package/dist/server-DUF9pjsx.d.ts.map +1 -0
  85. package/dist/server.browser.js +2 -0
  86. package/dist/server.browser.js.map +1 -0
  87. package/dist/server.d.ts +4 -155
  88. package/dist/server.js +2 -2
  89. package/dist/{log-ldf5g8Cx.d.ts → store-DysUkTH3.d.ts} +120 -35
  90. package/dist/store-DysUkTH3.d.ts.map +1 -0
  91. package/dist/{log-yJbXUf72.js → store-N8PXxDAS.js} +3 -1
  92. package/dist/store-N8PXxDAS.js.map +1 -0
  93. package/dist/store-codec-DTG0Ftek.js +10 -0
  94. package/dist/store-codec-DTG0Ftek.js.map +1 -0
  95. package/dist/store-memory.d.ts +12 -0
  96. package/dist/store-memory.d.ts.map +1 -0
  97. package/dist/{log-memory.js → store-memory.js} +145 -34
  98. package/dist/store-memory.js.map +1 -0
  99. package/dist/{log-polling-DZ1MiKLg.js → store-polling-dSeLxzfb.js} +7 -5
  100. package/dist/store-polling-dSeLxzfb.js.map +1 -0
  101. package/dist/{log-postgres.d.ts → store-postgres.d.ts} +7 -6
  102. package/dist/store-postgres.d.ts.map +1 -0
  103. package/dist/{log-postgres.js → store-postgres.js} +214 -24
  104. package/dist/store-postgres.js.map +1 -0
  105. package/dist/{log-redis-core-CyJ5L8yR.js → store-redis-core-BFLwz0Wj.js} +271 -66
  106. package/dist/store-redis-core-BFLwz0Wj.js.map +1 -0
  107. package/dist/{log-redis-http.d.ts → store-redis-http.d.ts} +8 -7
  108. package/dist/store-redis-http.d.ts.map +1 -0
  109. package/dist/{log-redis-http.js → store-redis-http.js} +22 -12
  110. package/dist/store-redis-http.js.map +1 -0
  111. package/dist/{log-redis.d.ts → store-redis.d.ts} +7 -6
  112. package/dist/store-redis.d.ts.map +1 -0
  113. package/dist/{log-redis.js → store-redis.js} +101 -39
  114. package/dist/store-redis.js.map +1 -0
  115. package/dist/{log-sqlite.d.ts → store-sqlite.d.ts} +7 -6
  116. package/dist/store-sqlite.d.ts.map +1 -0
  117. package/dist/{log-sqlite.js → store-sqlite.js} +118 -22
  118. package/dist/store-sqlite.js.map +1 -0
  119. package/dist/{telemetry-Cso0qyHQ.d.ts → telemetry-BjYHTfh2.d.ts} +3 -2
  120. package/dist/telemetry-BjYHTfh2.d.ts.map +1 -0
  121. package/dist/testing.browser.d.ts +1 -0
  122. package/dist/testing.browser.js +6 -0
  123. package/dist/testing.browser.js.map +1 -0
  124. package/dist/testing.d.ts +32 -0
  125. package/dist/testing.d.ts.map +1 -0
  126. package/dist/testing.js +103 -0
  127. package/dist/testing.js.map +1 -0
  128. package/dist/validate-XKT4FSNn.js +2 -0
  129. package/dist/validate-XKT4FSNn.js.map +1 -0
  130. package/dist/wire-BFQmSJ-9.js +284 -0
  131. package/dist/wire-BFQmSJ-9.js.map +1 -0
  132. package/docs/01-quickstart.mdx +3 -3
  133. package/docs/concepts/01-contracts.mdx +18 -13
  134. package/docs/concepts/02-handlers.mdx +7 -7
  135. package/docs/concepts/03-durability.mdx +21 -21
  136. package/docs/concepts/04-state.mdx +18 -21
  137. package/docs/guides/01-timers.mdx +154 -49
  138. package/docs/guides/02-cancellation.mdx +30 -4
  139. package/docs/guides/03-react.mdx +76 -57
  140. package/docs/guides/04-local-first.mdx +1 -1
  141. package/docs/guides/05-production.mdx +310 -61
  142. package/docs/guides/06-ai-agents.mdx +252 -74
  143. package/docs/guides/07-devtools.mdx +137 -12
  144. package/docs/guides/08-application-data.mdx +1 -1
  145. package/docs/guides/09-presence.mdx +263 -0
  146. package/docs/guides/10-transports.mdx +140 -0
  147. package/docs/index.mdx +18 -14
  148. package/docs/reference/01-api.mdx +839 -110
  149. package/docs/reference/02-errors.mdx +13 -6
  150. package/package.json +24 -7
  151. package/src/ai-coordinator.ts +358 -0
  152. package/src/ai-projector.ts +524 -0
  153. package/src/ai-sdk-step.ts +261 -0
  154. package/src/ai-server.browser.ts +5 -0
  155. package/src/ai-server.ts +1719 -0
  156. package/src/ai.ts +2155 -0
  157. package/src/cache-indexeddb.ts +10 -0
  158. package/src/cli-bin.ts +5 -0
  159. package/src/cli.ts +1046 -0
  160. package/src/client.ts +1826 -0
  161. package/src/contract.ts +206 -0
  162. package/src/deterministic-id.ts +72 -0
  163. package/src/devtools-app.ts +989 -0
  164. package/src/devtools-server.browser.ts +5 -0
  165. package/src/devtools-server.ts +604 -0
  166. package/src/devtools.ts +716 -0
  167. package/src/errors.ts +50 -0
  168. package/src/http.ts +394 -0
  169. package/src/idempotent-replay.ts +53 -0
  170. package/src/index.ts +37 -0
  171. package/src/inspection.ts +39 -0
  172. package/src/internal.ts +426 -0
  173. package/src/otel.ts +59 -0
  174. package/src/platform.ts +60 -0
  175. package/src/push-envelope.ts +137 -0
  176. package/src/react.ts +284 -0
  177. package/src/reducer.ts +108 -0
  178. package/src/retryable-lazy.ts +27 -0
  179. package/src/scheduler-qstash.ts +915 -0
  180. package/src/scheduler-task.ts +106 -0
  181. package/src/scheduler-vercel.ts +437 -0
  182. package/src/server.browser.ts +12 -0
  183. package/src/server.ts +2700 -0
  184. package/src/session-socket.ts +548 -0
  185. package/src/sse.ts +141 -0
  186. package/src/standard-schema.ts +77 -0
  187. package/src/store-codec.ts +10 -0
  188. package/src/store-memory.ts +788 -0
  189. package/src/store-polling.ts +102 -0
  190. package/src/store-postgres.ts +1212 -0
  191. package/src/store-redis-core.ts +1494 -0
  192. package/src/store-redis-http.ts +116 -0
  193. package/src/store-redis.ts +458 -0
  194. package/src/store-sqlite.ts +1108 -0
  195. package/src/store.ts +385 -0
  196. package/src/telemetry.ts +47 -0
  197. package/src/testing.browser.ts +5 -0
  198. package/src/testing.ts +185 -0
  199. package/src/validate.ts +39 -0
  200. package/src/wire.ts +454 -0
  201. package/dist/internal-gCd5qMry.js +0 -44
  202. package/dist/log-memory.d.ts +0 -11
  203. package/dist/recovery-vercel.js +0 -119
  204. package/dist/server-BcLa4RFL.js +0 -877
  205. package/dist/wire-BVsgR8o9.js +0 -62
@@ -0,0 +1,1719 @@
1
+ /**
2
+ * experimental-a2/ai/server — the server-only implementation of an experimental-a2/ai definition.
3
+ *
4
+ * `createHandlers()` exposes the ordinary A2 handler table;
5
+ * `createAgentServer()` is its batteries-included assembly with createServer.
6
+ */
7
+
8
+ // oxlint-disable no-await-in-loop -- stream chunks and durable appends are
9
+ // ordered; parallel consumption would corrupt progress sequence and state.
10
+
11
+ import { AsyncLocalStorage } from 'node:async_hooks'
12
+ import { convertToModelMessages } from 'ai'
13
+ import type {
14
+ FinishReason,
15
+ Instructions,
16
+ LanguageModel,
17
+ LanguageModelUsage,
18
+ ModelMessage,
19
+ TextStreamPart,
20
+ ToolSet,
21
+ UIMessage,
22
+ UIMessageChunk,
23
+ } from 'ai'
24
+ import { generateAISDKStep, type AISDKStepSettings } from './ai-sdk-step.ts'
25
+ import type {
26
+ AIEventDefs,
27
+ AIState,
28
+ AgentDefinition,
29
+ AgentToolContext,
30
+ AgentToolEventDefs,
31
+ ApprovalRequestedPayload,
32
+ CompactionCompletedPayload,
33
+ GenerationCompletedPayload,
34
+ GenerationFailedPayload,
35
+ GenerationProgressPayload,
36
+ GenerationRequestedPayload,
37
+ GenerationStartedPayload,
38
+ MessageCompletedPayload,
39
+ MessageCreatedPayload,
40
+ MessageInterruptedPayload,
41
+ ToolCalledPayload,
42
+ ToolResultPayload,
43
+ } from './ai.ts'
44
+ import {
45
+ aiCoordinatorReducer,
46
+ continuationReady,
47
+ type AICoordinatorState,
48
+ } from './ai-coordinator.ts'
49
+ import type { AppendInput, ContractEvent, EventDefs } from './contract.ts'
50
+ import { A2Error } from './errors.ts'
51
+ import {
52
+ consumeSchedulerSendFailure,
53
+ installAmbientToolScopeStorage,
54
+ type AmbientToolScope,
55
+ } from './internal.ts'
56
+ import {
57
+ createServer,
58
+ type A2Server,
59
+ type HandlerContext,
60
+ type PushValidationContext,
61
+ type ServerOptions,
62
+ } from './server.ts'
63
+
64
+ export function validateAgentPush(context: PushValidationContext): void {
65
+ const rejected = context.events.find((event) => {
66
+ if (event.type !== 'ai.message.created') {
67
+ return !(
68
+ event.type === 'ai.approval.responded' ||
69
+ event.type === 'ai.input.responded' ||
70
+ event.type === 'ai.message.interrupted' ||
71
+ event.type === 'ai.retry.requested'
72
+ )
73
+ }
74
+ const payload = event.payload
75
+ return (
76
+ typeof payload !== 'object' ||
77
+ payload === null ||
78
+ !('message' in payload) ||
79
+ typeof payload.message !== 'object' ||
80
+ payload.message === null ||
81
+ !('role' in payload.message) ||
82
+ payload.message.role !== 'user'
83
+ )
84
+ })
85
+ if (rejected !== undefined) {
86
+ throw new A2Error(
87
+ 'INVALID_PAYLOAD',
88
+ `event '${rejected.type}' is server-only for an AI agent`,
89
+ )
90
+ }
91
+ }
92
+
93
+ type AgentGenerationProgress = {
94
+ type: 'progress'
95
+ chunks: UIMessageChunk[]
96
+ }
97
+
98
+ type AgentGenerationFinish = {
99
+ type: 'finish'
100
+ finishReason?: FinishReason
101
+ usage?: LanguageModelUsage
102
+ }
103
+
104
+ type AgentGenerationUpdate = AgentGenerationProgress | AgentGenerationFinish
105
+
106
+ export type AgentGenerateContext<
107
+ M extends UIMessage,
108
+ D extends AIEventDefs<M> & EventDefs,
109
+ T extends ToolSet,
110
+ > = {
111
+ request: ContractEvent<D, 'ai.generation.requested'>
112
+ requestId: string
113
+ generationId: string
114
+ responseMessageId: string
115
+ messages: M[]
116
+ state: AIState<M>
117
+ history: ContractEvent<D>[]
118
+ signal: AbortSignal
119
+ model: LanguageModel
120
+ tools: T
121
+ instructions?: Instructions
122
+ generation: AgentGenerationSettings<T>
123
+ }
124
+
125
+ export type AgentGenerate<
126
+ M extends UIMessage,
127
+ D extends AIEventDefs<M> & EventDefs,
128
+ T extends ToolSet,
129
+ > = (
130
+ context: AgentGenerateContext<M, D, T>,
131
+ ) => ReadableStream<UIMessageChunk> | Promise<ReadableStream<UIMessageChunk>>
132
+
133
+ type UIMessageMetadata<M extends UIMessage> =
134
+ M extends UIMessage<infer Metadata> ? Metadata : unknown
135
+
136
+ export type AgentMessageMetadata<
137
+ M extends UIMessage,
138
+ D extends AIEventDefs<M> & EventDefs,
139
+ T extends ToolSet,
140
+ > = (
141
+ context: AgentGenerateContext<M, D, T> & { part: TextStreamPart<T> },
142
+ ) => UIMessageMetadata<M> | undefined
143
+
144
+ export type AgentResolverContext<
145
+ M extends UIMessage,
146
+ D extends AIEventDefs<M> & EventDefs,
147
+ > = {
148
+ event: ContractEvent<D, 'ai.generation.requested'>
149
+ state: AIState<M>
150
+ history: ContractEvent<D>[]
151
+ signal: AbortSignal
152
+ }
153
+
154
+ export type Resolvable<T, C> = T | ((context: C) => T | Promise<T>)
155
+
156
+ export type CompactionPolicy<
157
+ M extends UIMessage,
158
+ D extends AIEventDefs<M> & EventDefs,
159
+ > = {
160
+ shouldCompact(
161
+ context: AgentResolverContext<M, D> & { messages: M[] },
162
+ ): boolean | Promise<boolean>
163
+ compact(
164
+ context: AgentResolverContext<M, D> & { messages: M[] },
165
+ ): M[] | Promise<M[]>
166
+ }
167
+
168
+ export type AgentGenerationSettings<T extends ToolSet> = AISDKStepSettings<T>
169
+
170
+ export type CreateHandlersOptions<
171
+ M extends UIMessage,
172
+ D extends AIEventDefs<M> & EventDefs,
173
+ T extends ToolSet = ToolSet,
174
+ > = {
175
+ agent: AgentDefinition<M, D>
176
+ /** AI SDK model string or provider model, optionally resolved per generation. */
177
+ model: Resolvable<LanguageModel, AgentResolverContext<M, D>>
178
+ tools?: T
179
+ instructions?: Resolvable<
180
+ Instructions | undefined,
181
+ AgentResolverContext<M, D>
182
+ >
183
+ /** Settings for each one-step AI SDK streamText call. */
184
+ generation?: AgentGenerationSettings<T>
185
+ /**
186
+ * Maximum model steps in one assistant response. Defaults to Infinity.
187
+ */
188
+ maxSteps?: number
189
+ /** Map default AI SDK stream parts to typed assistant message metadata. */
190
+ messageMetadata?: AgentMessageMetadata<M, D, T>
191
+ /**
192
+ * Replace model generation with an AI SDK UI message chunk stream.
193
+ * The custom stream owns its metadata chunks.
194
+ */
195
+ generate?: AgentGenerate<M, D, T>
196
+ compaction?: CompactionPolicy<M, D>
197
+ /** Progress is durably flushed at either limit, whichever is reached first. */
198
+ progress?: { maxChunks?: number; maxDelayMs?: number }
199
+ }
200
+
201
+ export type CreateAgentServerOptions<
202
+ M extends UIMessage,
203
+ D extends AIEventDefs<M> & EventDefs,
204
+ T extends ToolSet = ToolSet,
205
+ > = CreateHandlersOptions<M, D, T> &
206
+ Omit<ServerOptions<D>, 'contract' | 'handlers' | 'validatePush'> & {
207
+ handlers?: ServerOptions<D>['handlers']
208
+ }
209
+
210
+ const resolve = async <T, C>(
211
+ value: Resolvable<T, C>,
212
+ context: C,
213
+ ): Promise<T> =>
214
+ typeof value === 'function'
215
+ ? await (value as (context: C) => T | Promise<T>)(context)
216
+ : value
217
+
218
+ const modelName = (model: LanguageModel): string => {
219
+ if (typeof model === 'string') return model
220
+ if ('modelId' in model && typeof model.modelId === 'string') {
221
+ return model.modelId
222
+ }
223
+ return 'custom'
224
+ }
225
+
226
+ const errorMessage = (error: unknown): string =>
227
+ error instanceof Error ? error.message : String(error)
228
+
229
+ const toolScopeStorage = installAmbientToolScopeStorage(
230
+ () => new AsyncLocalStorage<AmbientToolScope>(),
231
+ )
232
+
233
+ /** Run `fn` with `ctx` as the ambient tool handler context, for direct tool tests. */
234
+ export function runWithHandlerContext<D extends AgentToolEventDefs, R>(
235
+ ctx: AgentToolContext<D>,
236
+ fn: () => R,
237
+ ): R {
238
+ return toolScopeStorage.run({ context: ctx }, fn)
239
+ }
240
+
241
+ const promptCacheKey = (sessionId: string, generationId: string): string =>
242
+ `${sessionId}\u001f${generationId}`
243
+
244
+ const asyncIterator = (value: unknown): AsyncIterator<unknown> | undefined => {
245
+ if (typeof value !== 'object' || value === null) return undefined
246
+ const factory = Reflect.get(value, Symbol.asyncIterator)
247
+ if (typeof factory !== 'function') return undefined
248
+ return (factory as (this: object) => AsyncIterator<unknown>).call(value)
249
+ }
250
+
251
+ const isBoundaryChunk = (chunk: UIMessageChunk | undefined): boolean =>
252
+ chunk !== undefined &&
253
+ (chunk.type === 'text-end' ||
254
+ chunk.type === 'reasoning-end' ||
255
+ chunk.type === 'tool-input-available' ||
256
+ chunk.type === 'tool-input-error' ||
257
+ chunk.type === 'tool-output-available' ||
258
+ chunk.type === 'tool-output-error' ||
259
+ chunk.type === 'tool-output-denied' ||
260
+ chunk.type === 'tool-approval-request' ||
261
+ chunk.type === 'finish' ||
262
+ chunk.type === 'abort' ||
263
+ chunk.type === 'error')
264
+
265
+ const nextOrTimer = async <T>(
266
+ next: Promise<IteratorResult<T>>,
267
+ delayMs: number,
268
+ ): Promise<{ type: 'next'; result: IteratorResult<T> } | { type: 'timer' }> => {
269
+ let timer: ReturnType<typeof setTimeout> | undefined
270
+ try {
271
+ return await Promise.race([
272
+ next.then((result) => ({ type: 'next' as const, result })),
273
+ new Promise<{ type: 'timer' }>((finishTimer) => {
274
+ timer = setTimeout(() => finishTimer({ type: 'timer' }), delayMs)
275
+ ;(timer as { unref?: () => void }).unref?.()
276
+ }),
277
+ ])
278
+ } finally {
279
+ if (timer !== undefined) clearTimeout(timer)
280
+ }
281
+ }
282
+
283
+ type GenerationCompletion = {
284
+ finishReason?: FinishReason
285
+ usage?: LanguageModelUsage
286
+ }
287
+
288
+ type GenerationSource = {
289
+ stream: ReadableStream<UIMessageChunk>
290
+ completion?: () => GenerationCompletion
291
+ }
292
+
293
+ async function generateWithAISDK<
294
+ M extends UIMessage,
295
+ D extends AIEventDefs<M> & EventDefs,
296
+ T extends ToolSet,
297
+ >(
298
+ context: AgentGenerateContext<M, D, T>,
299
+ messageMetadata: AgentMessageMetadata<M, D, T> | undefined,
300
+ ): Promise<GenerationSource> {
301
+ const step = await generateAISDKStep<M, T>({
302
+ model: context.model,
303
+ tools: context.tools,
304
+ messages: context.messages,
305
+ responseMessageId: context.responseMessageId,
306
+ abortSignal: context.signal,
307
+ ...(context.instructions === undefined
308
+ ? {}
309
+ : { instructions: context.instructions }),
310
+ settings: context.generation,
311
+ ...(messageMetadata === undefined
312
+ ? {}
313
+ : {
314
+ messageMetadata: ({ part }) => messageMetadata({ ...context, part }),
315
+ }),
316
+ })
317
+ return {
318
+ stream: step.stream as ReadableStream<UIMessageChunk>,
319
+ completion: step.completion,
320
+ }
321
+ }
322
+
323
+ async function* consumeGeneration(options: {
324
+ source: GenerationSource
325
+ progress?: { maxChunks?: number; maxDelayMs?: number }
326
+ }): AsyncGenerator<AgentGenerationUpdate> {
327
+ const pending: UIMessageChunk[] = []
328
+ let streamedFinishReason: FinishReason | undefined
329
+ let streamedError: string | undefined
330
+ const maxChunks = options.progress?.maxChunks ?? 16
331
+ const maxDelayMs = options.progress?.maxDelayMs ?? 30
332
+ let lastFlush = Date.now()
333
+ const chunks = options.source.stream[Symbol.asyncIterator]()
334
+ let pendingChunk = chunks.next()
335
+ try {
336
+ for (;;) {
337
+ let result: IteratorResult<UIMessageChunk>
338
+ if (pending.length > 0) {
339
+ const remaining = Math.max(0, maxDelayMs - (Date.now() - lastFlush))
340
+ const outcome = await nextOrTimer(pendingChunk, remaining)
341
+ if (outcome.type === 'timer') {
342
+ yield { type: 'progress', chunks: pending.splice(0) }
343
+ lastFlush = Date.now()
344
+ continue
345
+ }
346
+ result = outcome.result
347
+ } else {
348
+ result = await pendingChunk
349
+ }
350
+ if (result.done) break
351
+ const chunk = result.value
352
+ pending.push(chunk)
353
+ if (chunk.type === 'finish') streamedFinishReason = chunk.finishReason
354
+ if (chunk.type === 'error') streamedError = chunk.errorText
355
+ pendingChunk = chunks.next()
356
+
357
+ if (pending.length >= maxChunks || isBoundaryChunk(pending.at(-1))) {
358
+ yield { type: 'progress', chunks: pending.splice(0) }
359
+ lastFlush = Date.now()
360
+ }
361
+ }
362
+ } catch (error) {
363
+ if (pending.length > 0)
364
+ yield { type: 'progress', chunks: pending.splice(0) }
365
+ throw error
366
+ }
367
+
368
+ let completion: GenerationCompletion | undefined
369
+ try {
370
+ completion = options.source.completion?.()
371
+ } catch (error) {
372
+ if (pending.length > 0) {
373
+ yield { type: 'progress', chunks: pending.splice(0) }
374
+ }
375
+ throw error
376
+ }
377
+ if (pending.length > 0) {
378
+ yield { type: 'progress', chunks: pending.splice(0) }
379
+ }
380
+ if (streamedError !== undefined) throw new Error(streamedError)
381
+ const finishReason = completion?.finishReason ?? streamedFinishReason
382
+ yield {
383
+ type: 'finish',
384
+ ...(finishReason === undefined ? {} : { finishReason }),
385
+ ...(completion?.usage === undefined ? {} : { usage: completion.usage }),
386
+ }
387
+ }
388
+
389
+ const replay = <M extends UIMessage, D extends AIEventDefs<M> & EventDefs>(
390
+ definition: AgentDefinition<M, D>,
391
+ history: ContractEvent<D>[],
392
+ ): AIState<M> => {
393
+ let state = definition.reducer.initialState
394
+ for (const event of history) state = definition.reducer.fold(state, event)
395
+ return state
396
+ }
397
+
398
+ const contextMessages = <M extends UIMessage>(state: AIState<M>): M[] => {
399
+ const compaction = state.compaction
400
+ if (compaction?.status !== 'completed' || !compaction.messages) {
401
+ return state.messages
402
+ }
403
+ const boundary = state.messages.findIndex(
404
+ (message) => message.id === compaction.throughMessageId,
405
+ )
406
+ const retained = new Set(compaction.retainedMessageIds ?? [])
407
+ return boundary === -1
408
+ ? state.messages
409
+ : [
410
+ ...compaction.messages,
411
+ ...state.messages
412
+ .slice(0, boundary + 1)
413
+ .filter((message) => retained.has(message.id)),
414
+ ...state.messages.slice(boundary + 1),
415
+ ]
416
+ }
417
+
418
+ const activeContextMessages = <M extends UIMessage>(
419
+ state: AIState<M>,
420
+ coordinator: AICoordinatorState,
421
+ ): M[] => {
422
+ const queued = new Set(coordinator.queued.map((item) => item.messageId))
423
+ return contextMessages(state).filter((message) => !queued.has(message.id))
424
+ }
425
+
426
+ const generationRequestId = (generationId: string): string | undefined => {
427
+ const markerIndex = generationId.lastIndexOf(':generation:')
428
+ return markerIndex === -1 ? undefined : generationId.slice(0, markerIndex)
429
+ }
430
+
431
+ type ToolCallClassification = 'automatic' | 'approval' | 'unknown'
432
+
433
+ type PendingToolCall = {
434
+ call: ToolCalledPayload
435
+ classification: ToolCallClassification
436
+ }
437
+
438
+ const toolCalledEvent = <D extends EventDefs>(
439
+ payload: ToolCalledPayload,
440
+ ): AppendInput<D> =>
441
+ ({
442
+ type: 'ai.tool.called',
443
+ id: `${payload.generationId}:tool:${payload.toolCallId}:called`,
444
+ payload,
445
+ }) as AppendInput<D>
446
+
447
+ const approvalClassification = <T extends ToolSet>(
448
+ tools: T,
449
+ generation: AgentGenerationSettings<T>,
450
+ toolName: string,
451
+ ): ToolCallClassification => {
452
+ const configured = generation.toolApproval
453
+ if (typeof configured === 'function') return 'unknown'
454
+ const policy = configured?.[toolName]
455
+ if (typeof policy === 'function') return 'unknown'
456
+ if (
457
+ policy === 'user-approval' ||
458
+ (typeof policy === 'object' && policy?.type === 'user-approval')
459
+ ) {
460
+ return 'approval'
461
+ }
462
+ if (
463
+ policy === 'denied' ||
464
+ (typeof policy === 'object' && policy?.type === 'denied')
465
+ ) {
466
+ return 'approval'
467
+ }
468
+ const tool = tools[toolName] as { needsApproval?: unknown } | undefined
469
+ if (typeof tool?.needsApproval === 'function') return 'unknown'
470
+ return tool?.needsApproval === true ? 'approval' : 'automatic'
471
+ }
472
+
473
+ const lifecycleEvents = <D extends EventDefs, T extends ToolSet>(options: {
474
+ requestId: string
475
+ messageId: string
476
+ generationId: string
477
+ sequence: number
478
+ chunks: UIMessageChunk[]
479
+ pending: PendingToolCall[]
480
+ tools: T
481
+ generation: AgentGenerationSettings<T>
482
+ custom: boolean
483
+ }): { events: AppendInput<D>[]; pending: PendingToolCall[] } => {
484
+ const result: AppendInput<D>[] = []
485
+ const pending = [...options.pending]
486
+ const take = (toolCallId: string): PendingToolCall | undefined => {
487
+ const index = pending.findIndex(
488
+ (candidate) => candidate.call.toolCallId === toolCallId,
489
+ )
490
+ if (index === -1) return undefined
491
+ return pending.splice(index, 1)[0]
492
+ }
493
+ const flush = (toolCallId: string): void => {
494
+ const pendingCall = take(toolCallId)
495
+ if (pendingCall) result.push(toolCalledEvent<D>(pendingCall.call))
496
+ }
497
+ for (const [chunkIndex, chunk] of options.chunks.entries()) {
498
+ const resultId = `${options.generationId}:tool-result:${options.sequence}:${chunkIndex}`
499
+ if (chunk.type === 'tool-input-available') {
500
+ const payload: ToolCalledPayload = {
501
+ requestId: options.requestId,
502
+ messageId: options.messageId,
503
+ generationId: options.generationId,
504
+ toolCallId: chunk.toolCallId,
505
+ toolName: chunk.toolName,
506
+ input: chunk.input,
507
+ ...(chunk.dynamic === undefined ? {} : { dynamic: chunk.dynamic }),
508
+ ...(chunk.providerExecuted === undefined
509
+ ? {}
510
+ : { providerExecuted: chunk.providerExecuted }),
511
+ ...((
512
+ options.tools[chunk.toolName] as
513
+ { supportsDeferredResults?: boolean } | undefined
514
+ )?.supportsDeferredResults === true
515
+ ? { supportsDeferredResults: true }
516
+ : {}),
517
+ ...(chunk.providerMetadata === undefined
518
+ ? {}
519
+ : { providerMetadata: chunk.providerMetadata }),
520
+ ...(chunk.toolMetadata === undefined
521
+ ? {}
522
+ : { toolMetadata: chunk.toolMetadata }),
523
+ ...(chunk.title === undefined ? {} : { title: chunk.title }),
524
+ }
525
+ take(payload.toolCallId)
526
+ const classification = approvalClassification(
527
+ options.tools,
528
+ options.generation,
529
+ chunk.toolName,
530
+ )
531
+ if (classification === 'automatic') {
532
+ result.push(toolCalledEvent<D>(payload))
533
+ } else {
534
+ pending.push({ call: payload, classification })
535
+ }
536
+ } else if (chunk.type === 'tool-input-error') {
537
+ const payload: ToolResultPayload = {
538
+ requestId: options.requestId,
539
+ messageId: options.messageId,
540
+ generationId: options.generationId,
541
+ toolCallId: chunk.toolCallId,
542
+ toolName: chunk.toolName,
543
+ ...(chunk.dynamic ? { input: chunk.input } : { rawInput: chunk.input }),
544
+ error: chunk.errorText,
545
+ phase: 'input',
546
+ ...(chunk.dynamic === undefined ? {} : { dynamic: chunk.dynamic }),
547
+ ...(chunk.providerExecuted === undefined
548
+ ? {}
549
+ : { providerExecuted: chunk.providerExecuted }),
550
+ ...(chunk.providerMetadata === undefined
551
+ ? {}
552
+ : { providerMetadata: chunk.providerMetadata }),
553
+ ...(chunk.toolMetadata === undefined
554
+ ? {}
555
+ : { toolMetadata: chunk.toolMetadata }),
556
+ }
557
+ const called: ToolCalledPayload = {
558
+ requestId: options.requestId,
559
+ messageId: options.messageId,
560
+ generationId: options.generationId,
561
+ toolCallId: chunk.toolCallId,
562
+ toolName: chunk.toolName,
563
+ input: chunk.input,
564
+ }
565
+ take(chunk.toolCallId)
566
+ result.push(toolCalledEvent<D>(called), {
567
+ type: 'ai.tool.result',
568
+ id: resultId,
569
+ payload,
570
+ } as AppendInput<D>)
571
+ } else if (chunk.type === 'tool-output-available') {
572
+ flush(chunk.toolCallId)
573
+ const payload: ToolResultPayload = {
574
+ requestId: options.requestId,
575
+ messageId: options.messageId,
576
+ generationId: options.generationId,
577
+ toolCallId: chunk.toolCallId,
578
+ output: chunk.output,
579
+ phase: 'execution',
580
+ ...(chunk.preliminary === undefined
581
+ ? {}
582
+ : { preliminary: chunk.preliminary }),
583
+ ...(chunk.dynamic === undefined ? {} : { dynamic: chunk.dynamic }),
584
+ ...(chunk.providerExecuted === undefined
585
+ ? {}
586
+ : { providerExecuted: chunk.providerExecuted }),
587
+ ...(chunk.providerMetadata === undefined
588
+ ? {}
589
+ : { providerMetadata: chunk.providerMetadata }),
590
+ ...(chunk.toolMetadata === undefined
591
+ ? {}
592
+ : { toolMetadata: chunk.toolMetadata }),
593
+ }
594
+ result.push({
595
+ type: 'ai.tool.result',
596
+ id: resultId,
597
+ payload,
598
+ } as AppendInput<D>)
599
+ } else if (chunk.type === 'tool-output-error') {
600
+ flush(chunk.toolCallId)
601
+ const payload: ToolResultPayload = {
602
+ requestId: options.requestId,
603
+ messageId: options.messageId,
604
+ generationId: options.generationId,
605
+ toolCallId: chunk.toolCallId,
606
+ error: chunk.errorText,
607
+ phase: 'execution',
608
+ ...(chunk.dynamic === undefined ? {} : { dynamic: chunk.dynamic }),
609
+ ...(chunk.providerExecuted === undefined
610
+ ? {}
611
+ : { providerExecuted: chunk.providerExecuted }),
612
+ ...(chunk.providerMetadata === undefined
613
+ ? {}
614
+ : { providerMetadata: chunk.providerMetadata }),
615
+ ...(chunk.toolMetadata === undefined
616
+ ? {}
617
+ : { toolMetadata: chunk.toolMetadata }),
618
+ }
619
+ result.push({
620
+ type: 'ai.tool.result',
621
+ id: resultId,
622
+ payload,
623
+ } as AppendInput<D>)
624
+ } else if (chunk.type === 'tool-output-denied') {
625
+ flush(chunk.toolCallId)
626
+ const payload: ToolResultPayload = {
627
+ requestId: options.requestId,
628
+ messageId: options.messageId,
629
+ generationId: options.generationId,
630
+ toolCallId: chunk.toolCallId,
631
+ denied: true,
632
+ phase: 'execution',
633
+ }
634
+ result.push({
635
+ type: 'ai.tool.result',
636
+ id: resultId,
637
+ payload,
638
+ } as AppendInput<D>)
639
+ } else if (chunk.type === 'tool-approval-request') {
640
+ flush(chunk.toolCallId)
641
+ const payload: ApprovalRequestedPayload = {
642
+ messageId: options.messageId,
643
+ generationId: options.generationId,
644
+ approvalId: chunk.approvalId,
645
+ toolCallId: chunk.toolCallId,
646
+ ...(chunk.isAutomatic === undefined
647
+ ? {}
648
+ : { isAutomatic: chunk.isAutomatic }),
649
+ ...(chunk.signature === undefined
650
+ ? {}
651
+ : { signature: chunk.signature }),
652
+ }
653
+ result.push({
654
+ type: 'ai.approval.requested',
655
+ id: `${options.generationId}:approval:${chunk.approvalId}`,
656
+ payload,
657
+ } as AppendInput<D>)
658
+ } else if (chunk.type === 'tool-approval-response') {
659
+ result.push({
660
+ type: 'ai.approval.responded',
661
+ id: `${options.generationId}:approval:${chunk.approvalId}:automatic-response`,
662
+ payload: {
663
+ messageId: options.messageId,
664
+ generationId: options.generationId,
665
+ approvalId: chunk.approvalId,
666
+ approved: chunk.approved,
667
+ ...(chunk.reason === undefined ? {} : { reason: chunk.reason }),
668
+ },
669
+ } as AppendInput<D>)
670
+ }
671
+ }
672
+ return { events: result, pending }
673
+ }
674
+
675
+ function queuedMessagesAt<D extends EventDefs>(
676
+ history: ContractEvent<D>[],
677
+ ): AICoordinatorState['queued'] {
678
+ const queued: AICoordinatorState['queued'] = []
679
+ for (const event of history) {
680
+ if (event.type === 'ai.message.created') {
681
+ const payload = event.payload as MessageCreatedPayload<UIMessage>
682
+ if (payload.message.role !== 'user') continue
683
+ const duplicate = queued.findIndex(
684
+ (item) => item.messageId === payload.message.id,
685
+ )
686
+ if (duplicate !== -1) queued.splice(duplicate, 1)
687
+ queued.push({
688
+ index: event.index,
689
+ messageId: payload.message.id,
690
+ generate: payload.generate !== false,
691
+ })
692
+ continue
693
+ }
694
+ if (event.type !== 'ai.generation.requested') continue
695
+ const request = event.payload as GenerationRequestedPayload
696
+ if (request.reason !== 'message') continue
697
+ const requested = queued.findIndex(
698
+ (item) => item.messageId === request.messageId,
699
+ )
700
+ if (requested !== -1) queued.splice(0, requested + 1)
701
+ }
702
+ return queued
703
+ }
704
+
705
+ function withoutGenerationLifecycle<D extends EventDefs>(
706
+ history: ContractEvent<D>[],
707
+ generationIds: ReadonlySet<string>,
708
+ ): ContractEvent<D>[] {
709
+ if (generationIds.size === 0) return history
710
+ const approvalKeys = new Set<string>()
711
+ const inputKeys = new Set<string>()
712
+ for (const event of history) {
713
+ const payload = event.payload as {
714
+ generationId?: unknown
715
+ messageId?: unknown
716
+ approvalId?: unknown
717
+ inputId?: unknown
718
+ }
719
+ if (
720
+ typeof payload.generationId !== 'string' ||
721
+ !generationIds.has(payload.generationId) ||
722
+ typeof payload.messageId !== 'string'
723
+ ) {
724
+ continue
725
+ }
726
+ if (
727
+ event.type === 'ai.approval.requested' &&
728
+ typeof payload.approvalId === 'string'
729
+ ) {
730
+ approvalKeys.add(`${payload.messageId}\u001f${payload.approvalId}`)
731
+ }
732
+ if (
733
+ event.type === 'ai.input.requested' &&
734
+ typeof payload.inputId === 'string'
735
+ ) {
736
+ inputKeys.add(`${payload.messageId}\u001f${payload.inputId}`)
737
+ }
738
+ }
739
+ return history.filter((event) => {
740
+ const payload = event.payload as {
741
+ generationId?: unknown
742
+ messageId?: unknown
743
+ approvalId?: unknown
744
+ inputId?: unknown
745
+ }
746
+ if (
747
+ typeof payload.generationId === 'string' &&
748
+ generationIds.has(payload.generationId)
749
+ ) {
750
+ return false
751
+ }
752
+ if (
753
+ event.type === 'ai.approval.responded' &&
754
+ typeof payload.messageId === 'string' &&
755
+ typeof payload.approvalId === 'string'
756
+ ) {
757
+ return !approvalKeys.has(
758
+ `${payload.messageId}\u001f${payload.approvalId}`,
759
+ )
760
+ }
761
+ if (
762
+ event.type === 'ai.input.responded' &&
763
+ typeof payload.messageId === 'string' &&
764
+ typeof payload.inputId === 'string'
765
+ ) {
766
+ return !inputKeys.has(`${payload.messageId}\u001f${payload.inputId}`)
767
+ }
768
+ return true
769
+ })
770
+ }
771
+
772
+ /**
773
+ * Build the ordinary A2 handler table for the built-in agent protocol.
774
+ * Application handlers can be spread beside this table.
775
+ */
776
+ export function createHandlers<
777
+ M extends UIMessage,
778
+ D extends AIEventDefs<M> & EventDefs,
779
+ T extends ToolSet = ToolSet,
780
+ >(
781
+ options: CreateHandlersOptions<M, D, T>,
782
+ ): NonNullable<ServerOptions<D>['handlers']> {
783
+ if (options.model === undefined) {
784
+ throw new TypeError('createHandlers requires model')
785
+ }
786
+ if (
787
+ options.progress?.maxChunks !== undefined &&
788
+ (!Number.isInteger(options.progress.maxChunks) ||
789
+ options.progress.maxChunks < 1)
790
+ ) {
791
+ throw new TypeError('progress.maxChunks must be a positive integer')
792
+ }
793
+ if (
794
+ options.progress?.maxDelayMs !== undefined &&
795
+ (!Number.isFinite(options.progress.maxDelayMs) ||
796
+ options.progress.maxDelayMs < 0)
797
+ ) {
798
+ throw new TypeError('progress.maxDelayMs must be a non-negative number')
799
+ }
800
+ if (
801
+ options.maxSteps !== undefined &&
802
+ options.maxSteps !== Number.POSITIVE_INFINITY &&
803
+ (!Number.isInteger(options.maxSteps) || options.maxSteps < 1)
804
+ ) {
805
+ throw new TypeError('maxSteps must be a positive integer or Infinity')
806
+ }
807
+
808
+ const tools = options.tools ?? ({} as T)
809
+ const generation = options.generation ?? {}
810
+ const maxSteps = options.maxSteps ?? Number.POSITIVE_INFINITY
811
+ const coordinator = aiCoordinatorReducer(options.agent.contract)
812
+ const promptCache = new Map<string, Promise<ModelMessage[]>>()
813
+
814
+ const coordinatorStateAt = (
815
+ history: ContractEvent<D>[],
816
+ frontier = Number.POSITIVE_INFINITY,
817
+ ): AICoordinatorState => {
818
+ let state = coordinator.initialState
819
+ for (const event of history) {
820
+ if (event.index >= frontier) break
821
+ state = coordinator.fold(state, event)
822
+ }
823
+ return state
824
+ }
825
+
826
+ const requestForNextMessage = (
827
+ state: AICoordinatorState,
828
+ ): AppendInput<D> | undefined => {
829
+ if (state.closed || state.response !== undefined) return undefined
830
+ const next = state.queued.find((item) => item.generate !== false)
831
+ if (next === undefined) return undefined
832
+ return {
833
+ type: 'ai.generation.requested',
834
+ id: `ai.generate:message:${next.messageId}`,
835
+ payload: { messageId: next.messageId, reason: 'message' },
836
+ } as AppendInput<D>
837
+ }
838
+
839
+ const scheduleNext = async (
840
+ ctx: Pick<HandlerContext<D>, 'session'>,
841
+ ): Promise<AppendInput<D> | void> =>
842
+ requestForNextMessage((await ctx.session.state(coordinator)).state)
843
+
844
+ const continueIfReady = async (
845
+ ctx: Pick<HandlerContext<D>, 'session'>,
846
+ generationId: string,
847
+ ): Promise<void> => {
848
+ const state = (await ctx.session.state(coordinator)).state
849
+ const response = state.response
850
+ if (response?.generation?.generationId !== generationId) {
851
+ return
852
+ }
853
+ const input = response.inputResponse
854
+ const inputReady =
855
+ response.completion?.generationId === generationId &&
856
+ response.failure === undefined &&
857
+ input !== undefined &&
858
+ response.inputs.length === 0 &&
859
+ response.calls.every(
860
+ (call) =>
861
+ call.terminal ||
862
+ (call.call.providerExecuted === true &&
863
+ call.call.supportsDeferredResults !== true &&
864
+ call.approval !== undefined &&
865
+ call.response !== undefined),
866
+ )
867
+ if (inputReady) {
868
+ await ctx.session.append('continue-after-input', {
869
+ type: 'ai.generation.requested',
870
+ id: `ai.generate:input:${encodeURIComponent(response.responseMessageId)}:${encodeURIComponent(input.generationId)}:${encodeURIComponent(input.inputId)}`,
871
+ payload: {
872
+ messageId: response.responseMessageId,
873
+ responseMessageId: response.responseMessageId,
874
+ reason: 'input',
875
+ },
876
+ } as AppendInput<D>)
877
+ return
878
+ }
879
+ if (!continuationReady(state)) return
880
+ await ctx.session.append('continue-after-tools', {
881
+ type: 'ai.generation.requested',
882
+ id: `ai.generate:tools:${generationId}`,
883
+ payload: {
884
+ messageId: response.responseMessageId,
885
+ responseMessageId: response.responseMessageId,
886
+ reason: 'tool',
887
+ },
888
+ } as AppendInput<D>)
889
+ }
890
+
891
+ type RuntimeTool = {
892
+ execute?: (
893
+ input: unknown,
894
+ options: {
895
+ toolCallId: string
896
+ messages: ModelMessage[]
897
+ abortSignal?: AbortSignal
898
+ },
899
+ ) => unknown
900
+ }
901
+
902
+ type ToolHandlerContext =
903
+ | HandlerContext<D, 'ai.tool.called'>
904
+ | HandlerContext<D, 'ai.approval.responded'>
905
+
906
+ const resultEvent = (
907
+ call: ToolCalledPayload,
908
+ suffix: string,
909
+ result: {
910
+ output?: unknown
911
+ error?: string
912
+ denied?: boolean
913
+ preliminary?: boolean
914
+ },
915
+ ): AppendInput<D> =>
916
+ ({
917
+ type: 'ai.tool.result',
918
+ id: `${call.generationId}:tool:${call.toolCallId}:${suffix}`,
919
+ payload: {
920
+ requestId: call.requestId,
921
+ messageId: call.messageId,
922
+ generationId: call.generationId,
923
+ toolCallId: call.toolCallId,
924
+ toolName: call.toolName,
925
+ input: call.input,
926
+ phase: 'execution',
927
+ ...(result.output === undefined ? {} : { output: result.output }),
928
+ ...(result.error === undefined ? {} : { error: result.error }),
929
+ ...(result.denied === undefined ? {} : { denied: result.denied }),
930
+ ...(result.preliminary === undefined
931
+ ? {}
932
+ : { preliminary: result.preliminary }),
933
+ ...(call.dynamic === undefined ? {} : { dynamic: call.dynamic }),
934
+ ...(call.providerExecuted === undefined
935
+ ? {}
936
+ : { providerExecuted: call.providerExecuted }),
937
+ ...(call.toolMetadata === undefined
938
+ ? {}
939
+ : { toolMetadata: call.toolMetadata }),
940
+ },
941
+ }) as AppendInput<D>
942
+
943
+ const promptMessages = (
944
+ sessionId: string,
945
+ call: ToolCalledPayload,
946
+ readHistory: () => Promise<ContractEvent<D>[]>,
947
+ ): Promise<ModelMessage[]> => {
948
+ const key = promptCacheKey(sessionId, call.generationId)
949
+ const cached = promptCache.get(key)
950
+ if (cached) return cached
951
+ const computation = (async () => {
952
+ const history = await readHistory()
953
+ const coordinatorState = coordinatorStateAt(history)
954
+ const compaction = history.find(
955
+ (event) =>
956
+ event.type === 'ai.compaction.completed' &&
957
+ (event.payload as CompactionCompletedPayload<M>).generationId ===
958
+ call.generationId,
959
+ )
960
+ if (compaction !== undefined) {
961
+ const messages = (
962
+ compaction.payload as CompactionCompletedPayload<M>
963
+ ).messages.filter(
964
+ (message) =>
965
+ !coordinatorState.queued.some(
966
+ (queued) => queued.messageId === message.id,
967
+ ),
968
+ )
969
+ return convertToModelMessages(messages, { tools })
970
+ }
971
+ const started = history.find(
972
+ (event) =>
973
+ event.type === 'ai.generation.started' &&
974
+ (event.payload as GenerationStartedPayload).generationId ===
975
+ call.generationId,
976
+ )
977
+ const frontier = started?.index ?? Number.POSITIVE_INFINITY
978
+ const state = replay(
979
+ options.agent,
980
+ history.filter((event) => event.index < frontier),
981
+ )
982
+ return convertToModelMessages(
983
+ activeContextMessages(state, coordinatorState),
984
+ { tools },
985
+ )
986
+ })()
987
+ promptCache.set(key, computation)
988
+ void computation.catch(() => {
989
+ if (promptCache.get(key) === computation) promptCache.delete(key)
990
+ })
991
+ return computation
992
+ }
993
+
994
+ const toolExecutionFailure = (
995
+ ctx: ToolHandlerContext,
996
+ call: ToolCalledPayload,
997
+ error: unknown,
998
+ ): AppendInput<D> | void => {
999
+ const schedulerFailure = consumeSchedulerSendFailure(error)
1000
+ if (ctx.signal.aborted) return
1001
+ if (schedulerFailure === 'retryable') throw error
1002
+ return resultEvent(call, 'execution:error', {
1003
+ error: errorMessage(error),
1004
+ })
1005
+ }
1006
+
1007
+ const runToolExecution = async (
1008
+ ctx: ToolHandlerContext,
1009
+ call: ToolCalledPayload,
1010
+ execute: NonNullable<RuntimeTool['execute']>,
1011
+ messages: ModelMessage[],
1012
+ ): Promise<AppendInput<D> | void> => {
1013
+ let output: unknown
1014
+ try {
1015
+ output = await execute(call.input, {
1016
+ toolCallId: call.toolCallId,
1017
+ messages,
1018
+ abortSignal: ctx.signal,
1019
+ })
1020
+ } catch (error) {
1021
+ return toolExecutionFailure(ctx, call, error)
1022
+ }
1023
+ let iterator: AsyncIterator<unknown> | undefined
1024
+ try {
1025
+ iterator = asyncIterator(output)
1026
+ } catch (error) {
1027
+ return toolExecutionFailure(ctx, call, error)
1028
+ }
1029
+ if (iterator === undefined) {
1030
+ return resultEvent(call, 'execution:0', { output })
1031
+ }
1032
+ let last: unknown
1033
+ let sequence = 0
1034
+ for (;;) {
1035
+ let result: IteratorResult<unknown>
1036
+ try {
1037
+ result = await iterator.next()
1038
+ } catch (error) {
1039
+ return toolExecutionFailure(ctx, call, error)
1040
+ }
1041
+ if (result.done) break
1042
+ if (ctx.signal.aborted) {
1043
+ try {
1044
+ await iterator.return?.()
1045
+ } catch {
1046
+ // The aborted handler cannot record an iterator cleanup failure.
1047
+ }
1048
+ return
1049
+ }
1050
+ last = result.value
1051
+ try {
1052
+ await ctx.session.append(
1053
+ `tool:${call.toolCallId}:preliminary:${sequence}`,
1054
+ resultEvent(call, `execution:${sequence}:preliminary`, {
1055
+ output: result.value,
1056
+ preliminary: true,
1057
+ }),
1058
+ )
1059
+ } catch (error) {
1060
+ try {
1061
+ await iterator.return?.()
1062
+ } catch {
1063
+ // Preserve the append failure that makes the durable handler retry.
1064
+ }
1065
+ throw error
1066
+ }
1067
+ sequence += 1
1068
+ }
1069
+ return resultEvent(
1070
+ call,
1071
+ `execution:${sequence}:final`,
1072
+ sequence === 0 ? {} : { output: last },
1073
+ )
1074
+ }
1075
+
1076
+ const executeTool = async (
1077
+ ctx: ToolHandlerContext,
1078
+ call: ToolCalledPayload,
1079
+ ): Promise<AppendInput<D> | void> => {
1080
+ const tool = tools[call.toolName] as RuntimeTool | undefined
1081
+ const execute = tool?.execute
1082
+ if (execute === undefined) {
1083
+ return resultEvent(call, 'execution:error', {
1084
+ error: `Tool '${call.toolName}' has no server executor`,
1085
+ })
1086
+ }
1087
+ const messages = await promptMessages(
1088
+ ctx.event.sessionId,
1089
+ call,
1090
+ ctx.session.history,
1091
+ )
1092
+ const scope: AmbientToolScope = {
1093
+ contract: options.agent.contract,
1094
+ context: ctx,
1095
+ }
1096
+ // Async iteration resumes in the caller's context, so the whole
1097
+ // execution, not just the execute call, runs inside the scope.
1098
+ return toolScopeStorage.run(scope, () =>
1099
+ runToolExecution(ctx, call, execute, messages),
1100
+ )
1101
+ }
1102
+
1103
+ const handleToolCall = async (
1104
+ ctx: HandlerContext<D, 'ai.tool.called'>,
1105
+ ): Promise<AppendInput<D> | void> => {
1106
+ const state = (await ctx.session.state(coordinator)).state
1107
+ const response = state.response
1108
+ const current = response?.calls.find(
1109
+ (candidate) => candidate.index === ctx.event.index,
1110
+ )
1111
+ if (
1112
+ current === undefined ||
1113
+ response?.generation?.generationId !== ctx.event.payload.generationId ||
1114
+ response.failure !== undefined ||
1115
+ current.terminal ||
1116
+ current.approval !== undefined
1117
+ ) {
1118
+ return
1119
+ }
1120
+ if (current.call.providerExecuted === true) {
1121
+ await continueIfReady(ctx, current.call.generationId)
1122
+ return
1123
+ }
1124
+ return executeTool(ctx, current.call)
1125
+ }
1126
+
1127
+ const handleApproval = async (
1128
+ ctx: HandlerContext<D, 'ai.approval.responded'>,
1129
+ ): Promise<AppendInput<D> | void> => {
1130
+ const state = (await ctx.session.state(coordinator)).state
1131
+ const response = state.response
1132
+ const current = response?.calls.find(
1133
+ (candidate) =>
1134
+ candidate.approval?.approvalId === ctx.event.payload.approvalId &&
1135
+ candidate.approval.messageId === ctx.event.payload.messageId &&
1136
+ candidate.approval.generationId === ctx.event.payload.generationId &&
1137
+ candidate.responseIndex === ctx.event.index,
1138
+ )
1139
+ if (
1140
+ current === undefined ||
1141
+ response?.generation?.generationId !== current.call.generationId ||
1142
+ response.failure !== undefined ||
1143
+ current.terminal
1144
+ ) {
1145
+ return
1146
+ }
1147
+ if (current.call.providerExecuted === true) {
1148
+ await continueIfReady(ctx, current.call.generationId)
1149
+ return
1150
+ }
1151
+ if (!ctx.event.payload.approved) {
1152
+ return resultEvent(current.call, 'execution:denied', { denied: true })
1153
+ }
1154
+ return executeTool(ctx, current.call)
1155
+ }
1156
+
1157
+ const generationHandler = async (
1158
+ ctx: HandlerContext<D, 'ai.generation.requested'>,
1159
+ ): Promise<void | AppendInput<D> | readonly AppendInput<D>[]> => {
1160
+ const history = await ctx.session.history()
1161
+ const requestId = ctx.event.id
1162
+ const request = ctx.event.payload
1163
+ const coordinatorState = (await ctx.session.state(coordinator)).state
1164
+ if (
1165
+ coordinatorState.closed ||
1166
+ coordinatorState.response?.activeRequestId !== requestId
1167
+ ) {
1168
+ return
1169
+ }
1170
+ const terminal = history.some((event) => {
1171
+ if (event.type === 'ai.generation.completed') {
1172
+ return (
1173
+ (event.payload as GenerationCompletedPayload).requestId === requestId
1174
+ )
1175
+ }
1176
+ if (event.type !== 'ai.generation.failed') return false
1177
+ const payload = event.payload as GenerationFailedPayload
1178
+ return payload.requestId === requestId && payload.superseded !== true
1179
+ })
1180
+ if (terminal) return
1181
+
1182
+ const previousStarts = history.filter(
1183
+ (event) =>
1184
+ event.type === 'ai.generation.started' &&
1185
+ (event.payload as GenerationStartedPayload).requestId === requestId,
1186
+ )
1187
+ const priorAttempts = previousStarts.map(
1188
+ (event) => (event.payload as GenerationStartedPayload).attempt,
1189
+ )
1190
+ if (priorAttempts.some((priorAttempt) => priorAttempt >= ctx.attempt)) {
1191
+ return
1192
+ }
1193
+ const attempt = ctx.attempt
1194
+ const generationId = `${requestId}:generation:${attempt}`
1195
+ const responseMessageId =
1196
+ request.responseMessageId ??
1197
+ (request.reason === 'message'
1198
+ ? `${request.messageId}:assistant`
1199
+ : request.messageId)
1200
+
1201
+ const responseStepCount = history.filter(
1202
+ (event) =>
1203
+ event.type === 'ai.generation.completed' &&
1204
+ (event.payload as GenerationCompletedPayload).responseMessageId ===
1205
+ responseMessageId,
1206
+ ).length
1207
+
1208
+ const sourceCoordinatorState = coordinatorStateAt(history, ctx.event.index)
1209
+ if (request.reason === 'tool') {
1210
+ const prefix = 'ai.generate:tools:'
1211
+ if (!requestId.startsWith(prefix)) return
1212
+ const sourceGenerationId = requestId.slice(prefix.length)
1213
+ if (
1214
+ !continuationReady(sourceCoordinatorState) ||
1215
+ sourceCoordinatorState.response?.generation?.generationId !==
1216
+ sourceGenerationId ||
1217
+ sourceCoordinatorState.response.responseMessageId !==
1218
+ request.messageId ||
1219
+ sourceCoordinatorState.response.responseMessageId !==
1220
+ request.responseMessageId
1221
+ ) {
1222
+ return
1223
+ }
1224
+ }
1225
+
1226
+ const previous = previousStarts
1227
+ .filter(
1228
+ (event) =>
1229
+ (event.payload as GenerationStartedPayload).attempt < attempt,
1230
+ )
1231
+ .toSorted(
1232
+ (left, right) =>
1233
+ (right.payload as GenerationStartedPayload).attempt -
1234
+ (left.payload as GenerationStartedPayload).attempt,
1235
+ )[0]
1236
+
1237
+ const incompleteId = previous
1238
+ ? (previous.payload as GenerationStartedPayload).generationId
1239
+ : undefined
1240
+ const replacedGenerationIds = new Set<string>()
1241
+ if (incompleteId !== undefined) replacedGenerationIds.add(incompleteId)
1242
+ if (
1243
+ request.reason === 'retry' &&
1244
+ sourceCoordinatorState.response?.failure?.generationId !== undefined
1245
+ ) {
1246
+ replacedGenerationIds.add(
1247
+ sourceCoordinatorState.response.failure.generationId,
1248
+ )
1249
+ }
1250
+ const promptHistory = withoutGenerationLifecycle(
1251
+ history,
1252
+ replacedGenerationIds,
1253
+ )
1254
+ let state = replay(options.agent, promptHistory)
1255
+ const resolverContext: AgentResolverContext<M, D> = {
1256
+ event: ctx.event,
1257
+ state,
1258
+ history,
1259
+ signal: ctx.signal,
1260
+ }
1261
+
1262
+ const resolvedModel = await resolve(options.model, resolverContext)
1263
+ if (resolvedModel === undefined) {
1264
+ throw new TypeError('the model resolver returned undefined')
1265
+ }
1266
+ if (ctx.signal.aborted) return
1267
+
1268
+ if (request.reason === 'tool' && responseStepCount >= maxSteps) {
1269
+ const source = sourceCoordinatorState.response?.generation
1270
+ if (source === undefined) return
1271
+ return {
1272
+ type: 'ai.generation.failed',
1273
+ id: `${requestId}:step-limit`,
1274
+ payload: {
1275
+ requestId: source.requestId,
1276
+ messageId: source.messageId,
1277
+ generationId: source.generationId,
1278
+ responseMessageId,
1279
+ error: `agent exceeded the ${maxSteps}-step limit`,
1280
+ stepLimit: true,
1281
+ },
1282
+ } as AppendInput<D>
1283
+ }
1284
+
1285
+ const started: GenerationStartedPayload = {
1286
+ requestId,
1287
+ messageId: request.messageId,
1288
+ generationId,
1289
+ responseMessageId,
1290
+ attempt,
1291
+ model: modelName(resolvedModel),
1292
+ }
1293
+ const startEvents: AppendInput<D>[] = []
1294
+ if (previous) {
1295
+ const payload = previous.payload as GenerationStartedPayload
1296
+ const superseded: GenerationFailedPayload = {
1297
+ requestId,
1298
+ messageId: payload.messageId,
1299
+ generationId: payload.generationId,
1300
+ responseMessageId: payload.responseMessageId,
1301
+ error: 'generation attempt was superseded after an incomplete run',
1302
+ superseded: true,
1303
+ }
1304
+ startEvents.push({
1305
+ type: 'ai.generation.failed',
1306
+ id: `${payload.generationId}:superseded`,
1307
+ payload: superseded,
1308
+ } as AppendInput<D>)
1309
+ }
1310
+ startEvents.push({
1311
+ type: 'ai.generation.started',
1312
+ id: generationId,
1313
+ payload: started,
1314
+ } as AppendInput<D>)
1315
+ await ctx.session.append('generation-start', ...startEvents)
1316
+
1317
+ const startedCoordinatorState = (await ctx.session.state(coordinator)).state
1318
+ if (
1319
+ startedCoordinatorState.response?.activeRequestId !== requestId ||
1320
+ startedCoordinatorState.response.generation?.generationId !== generationId
1321
+ ) {
1322
+ return
1323
+ }
1324
+
1325
+ const promptCoordinatorState = {
1326
+ ...coordinatorStateAt(history),
1327
+ queued: queuedMessagesAt(history),
1328
+ }
1329
+ let messages = activeContextMessages(state, promptCoordinatorState)
1330
+ if (options.compaction) {
1331
+ const compactionContext = { ...resolverContext, messages }
1332
+ if (await options.compaction.shouldCompact(compactionContext)) {
1333
+ const throughMessageId = messages.at(-1)?.id ?? request.messageId
1334
+ await ctx.session.append('compaction-requested', {
1335
+ type: 'ai.compaction.requested',
1336
+ id: `${generationId}:compaction:requested`,
1337
+ payload: { generationId, throughMessageId },
1338
+ })
1339
+ messages = await options.compaction.compact(compactionContext)
1340
+ const completed: CompactionCompletedPayload<M> = {
1341
+ generationId,
1342
+ throughMessageId,
1343
+ messages,
1344
+ ...(promptCoordinatorState.queued.length === 0
1345
+ ? {}
1346
+ : {
1347
+ retainedMessageIds: promptCoordinatorState.queued.map(
1348
+ (item) => item.messageId,
1349
+ ),
1350
+ }),
1351
+ }
1352
+ await ctx.session.append('compaction-completed', {
1353
+ type: 'ai.compaction.completed',
1354
+ id: `${generationId}:compaction:completed`,
1355
+ payload: completed,
1356
+ })
1357
+ state = {
1358
+ ...state,
1359
+ compaction: { status: 'completed', ...completed },
1360
+ }
1361
+ }
1362
+ }
1363
+
1364
+ const currentState = (await ctx.session.state(options.agent.reducer)).state
1365
+ const currentCoordinatorState = (await ctx.session.state(coordinator)).state
1366
+ const generationMessages = activeContextMessages(
1367
+ currentState,
1368
+ currentCoordinatorState,
1369
+ )
1370
+ promptCache.set(
1371
+ promptCacheKey(ctx.event.sessionId, generationId),
1372
+ convertToModelMessages(generationMessages, { tools }),
1373
+ )
1374
+
1375
+ let pendingToolCalls: PendingToolCall[] = []
1376
+ try {
1377
+ const resolvedInstructions =
1378
+ options.instructions === undefined
1379
+ ? undefined
1380
+ : await resolve(options.instructions, resolverContext)
1381
+ const generateContext: AgentGenerateContext<M, D, T> = {
1382
+ request: ctx.event,
1383
+ requestId,
1384
+ generationId,
1385
+ responseMessageId,
1386
+ messages: generationMessages,
1387
+ state: currentState,
1388
+ history,
1389
+ signal: ctx.signal,
1390
+ model: resolvedModel,
1391
+ tools,
1392
+ ...(resolvedInstructions === undefined
1393
+ ? {}
1394
+ : { instructions: resolvedInstructions }),
1395
+ generation: options.generation ?? {},
1396
+ }
1397
+ const custom = options.generate !== undefined
1398
+ const source =
1399
+ options.generate === undefined
1400
+ ? await generateWithAISDK(generateContext, options.messageMetadata)
1401
+ : { stream: await options.generate(generateContext) }
1402
+ const updates = consumeGeneration({
1403
+ source,
1404
+ ...(options.progress === undefined
1405
+ ? {}
1406
+ : { progress: options.progress }),
1407
+ })
1408
+
1409
+ let sequence = 0
1410
+ let finish: AgentGenerationFinish | undefined
1411
+ for await (const update of updates) {
1412
+ ctx.signal.throwIfAborted()
1413
+ if (update.type === 'finish') {
1414
+ finish = update
1415
+ continue
1416
+ }
1417
+ const progress: GenerationProgressPayload = {
1418
+ requestId,
1419
+ messageId: request.messageId,
1420
+ generationId,
1421
+ responseMessageId,
1422
+ sequence,
1423
+ chunks: update.chunks,
1424
+ }
1425
+ const lifecycle = lifecycleEvents<D, T>({
1426
+ requestId,
1427
+ messageId: responseMessageId,
1428
+ generationId,
1429
+ sequence,
1430
+ chunks: update.chunks,
1431
+ pending: pendingToolCalls,
1432
+ tools,
1433
+ generation,
1434
+ custom,
1435
+ })
1436
+ pendingToolCalls = lifecycle.pending
1437
+ await ctx.session.append(
1438
+ `generation-progress:${sequence}`,
1439
+ {
1440
+ type: 'ai.generation.progress',
1441
+ id: `${generationId}:progress:${sequence}`,
1442
+ payload: progress,
1443
+ },
1444
+ ...lifecycle.events,
1445
+ )
1446
+ sequence += 1
1447
+ }
1448
+
1449
+ if (!finish) throw new Error('agent generation finished without output')
1450
+ const completed: GenerationCompletedPayload = {
1451
+ requestId,
1452
+ messageId: request.messageId,
1453
+ generationId,
1454
+ responseMessageId,
1455
+ ...(finish.finishReason === undefined
1456
+ ? {}
1457
+ : { finishReason: finish.finishReason }),
1458
+ ...(finish.usage === undefined ? {} : { usage: finish.usage }),
1459
+ }
1460
+ const completionEvent = {
1461
+ type: 'ai.generation.completed',
1462
+ id: `${generationId}:completed`,
1463
+ payload: completed,
1464
+ } as AppendInput<D>
1465
+ const unresolved = options.generate
1466
+ ? pendingToolCalls.filter(
1467
+ (pending) => pending.classification !== 'automatic',
1468
+ )
1469
+ : []
1470
+ if (unresolved.length > 0) {
1471
+ throw new Error(
1472
+ `generation ended with unresolved tool authorization for '${unresolved[0]!.call.toolName}'`,
1473
+ )
1474
+ }
1475
+ const pendingEvents = pendingToolCalls
1476
+ .filter(
1477
+ (pending) =>
1478
+ pending.classification === 'automatic' ||
1479
+ (options.generate === undefined &&
1480
+ pending.classification === 'unknown'),
1481
+ )
1482
+ .map((pending) => toolCalledEvent<D>(pending.call))
1483
+ if (finish.finishReason === 'tool-calls') {
1484
+ return [...pendingEvents, completionEvent]
1485
+ }
1486
+ return [
1487
+ ...pendingEvents,
1488
+ completionEvent,
1489
+ {
1490
+ type: 'ai.message.completed',
1491
+ id: `${generationId}:message:completed`,
1492
+ payload: {
1493
+ messageId: responseMessageId,
1494
+ } satisfies MessageCompletedPayload,
1495
+ } as AppendInput<D>,
1496
+ ]
1497
+ } catch (error) {
1498
+ if (error instanceof A2Error) throw error
1499
+ if (ctx.signal.aborted) {
1500
+ const interrupted: MessageInterruptedPayload = {
1501
+ messageId: responseMessageId,
1502
+ generationId,
1503
+ reason: 'aborted',
1504
+ }
1505
+ return {
1506
+ type: 'ai.message.interrupted',
1507
+ id: `${generationId}:interrupted`,
1508
+ payload: interrupted,
1509
+ } as AppendInput<D>
1510
+ }
1511
+ const failed: GenerationFailedPayload = {
1512
+ requestId,
1513
+ messageId: request.messageId,
1514
+ generationId,
1515
+ responseMessageId,
1516
+ error: errorMessage(error),
1517
+ }
1518
+ return {
1519
+ type: 'ai.generation.failed',
1520
+ id: `${generationId}:failed`,
1521
+ payload: failed,
1522
+ } as AppendInput<D>
1523
+ }
1524
+ }
1525
+
1526
+ const handleMessageCreated = async (
1527
+ ctx: HandlerContext<D, 'ai.message.created'>,
1528
+ ): Promise<AppendInput<D> | void> => {
1529
+ if (
1530
+ ctx.event.payload.message.role !== 'user' ||
1531
+ ctx.event.payload.generate === false
1532
+ ) {
1533
+ return
1534
+ }
1535
+ return scheduleNext(ctx)
1536
+ }
1537
+
1538
+ const handleRetry = async (
1539
+ ctx: HandlerContext<D, 'ai.retry.requested'>,
1540
+ ): Promise<AppendInput<D> | void> => {
1541
+ const state = (await ctx.session.state(coordinator)).state
1542
+ const response = state.response
1543
+ if (
1544
+ response?.status !== 'failed' ||
1545
+ response.rootMessageId !== ctx.event.payload.messageId ||
1546
+ response.responseMessageId !== ctx.event.payload.responseMessageId
1547
+ ) {
1548
+ return
1549
+ }
1550
+ return {
1551
+ type: 'ai.generation.requested',
1552
+ id: `ai.generate:retry:${ctx.event.payload.retryId}`,
1553
+ payload: {
1554
+ messageId: response.rootMessageId,
1555
+ responseMessageId: response.responseMessageId,
1556
+ reason: 'retry',
1557
+ },
1558
+ } as AppendInput<D>
1559
+ }
1560
+
1561
+ const handleInputResponse = async (
1562
+ ctx: HandlerContext<D, 'ai.input.responded'>,
1563
+ ): Promise<void> => {
1564
+ const state = (await ctx.session.state(coordinator)).state
1565
+ const response = state.response
1566
+ if (
1567
+ response?.responseMessageId !== ctx.event.payload.messageId ||
1568
+ response.inputResponse?.index !== ctx.event.index ||
1569
+ response.inputResponse.generationId !== ctx.event.payload.generationId ||
1570
+ response.inputResponse.inputId !== ctx.event.payload.inputId
1571
+ ) {
1572
+ return
1573
+ }
1574
+ await continueIfReady(ctx, ctx.event.payload.generationId)
1575
+ }
1576
+
1577
+ const clearPromptCache = (sessionId: string): void => {
1578
+ const prefix = `${sessionId}\u001f`
1579
+ for (const key of promptCache.keys()) {
1580
+ if (key.startsWith(prefix)) promptCache.delete(key)
1581
+ }
1582
+ }
1583
+
1584
+ const handleResponseEnded = async (
1585
+ ctx: HandlerContext<D, 'ai.message.completed' | 'ai.message.interrupted'>,
1586
+ ): Promise<AppendInput<D> | void> => {
1587
+ clearPromptCache(ctx.event.sessionId)
1588
+ return scheduleNext(ctx)
1589
+ }
1590
+
1591
+ return {
1592
+ 'ai.message.created': { lane: 'a2.ai.turn', handler: handleMessageCreated },
1593
+ 'ai.retry.requested': { lane: 'a2.ai.turn', handler: handleRetry },
1594
+ 'ai.input.responded': {
1595
+ lane: 'a2.ai.turn',
1596
+ handler: handleInputResponse,
1597
+ },
1598
+ 'ai.message.completed': {
1599
+ lane: 'a2.ai.turn',
1600
+ handler: handleResponseEnded,
1601
+ },
1602
+ 'ai.message.interrupted': {
1603
+ lane: 'a2.ai.turn',
1604
+ handler: handleResponseEnded,
1605
+ },
1606
+ 'ai.session.closed': {
1607
+ handler: (ctx) => {
1608
+ clearPromptCache(ctx.event.sessionId)
1609
+ return Promise.resolve()
1610
+ },
1611
+ },
1612
+ 'ai.generation.failed': {
1613
+ handler: (ctx) => {
1614
+ promptCache.delete(
1615
+ promptCacheKey(ctx.event.sessionId, ctx.event.payload.generationId),
1616
+ )
1617
+ return Promise.resolve()
1618
+ },
1619
+ },
1620
+ 'ai.generation.requested': {
1621
+ lane: 'a2.ai.turn',
1622
+ abortOn: {
1623
+ 'ai.message.interrupted': (event, trigger, context) => {
1624
+ const responseMessageId =
1625
+ trigger.payload.responseMessageId ??
1626
+ (trigger.payload.reason === 'message'
1627
+ ? `${trigger.payload.messageId}:assistant`
1628
+ : trigger.payload.messageId)
1629
+ return (
1630
+ event.payload.messageId === responseMessageId &&
1631
+ (event.payload.requestId === trigger.id ||
1632
+ event.payload.generationId ===
1633
+ `${trigger.id}:generation:${context.attempt}`)
1634
+ )
1635
+ },
1636
+ 'ai.session.closed': true,
1637
+ },
1638
+ handler: generationHandler,
1639
+ },
1640
+ 'ai.generation.completed': {
1641
+ handler: async (ctx) =>
1642
+ continueIfReady(ctx, ctx.event.payload.generationId),
1643
+ },
1644
+ 'ai.tool.called': {
1645
+ abortOn: {
1646
+ 'ai.generation.failed': (event, trigger) =>
1647
+ event.payload.generationId === trigger.payload.generationId,
1648
+ 'ai.message.interrupted': (event, trigger) =>
1649
+ event.payload.messageId === trigger.payload.messageId &&
1650
+ (event.payload.requestId === trigger.payload.requestId ||
1651
+ event.payload.generationId === trigger.payload.generationId),
1652
+ 'ai.session.closed': true,
1653
+ },
1654
+ handler: handleToolCall,
1655
+ },
1656
+ 'ai.approval.responded': {
1657
+ abortOn: {
1658
+ 'ai.generation.failed': (event, trigger) =>
1659
+ event.payload.responseMessageId === trigger.payload.messageId &&
1660
+ event.payload.generationId === trigger.payload.generationId,
1661
+ 'ai.message.interrupted': (event, trigger) =>
1662
+ event.payload.messageId === trigger.payload.messageId &&
1663
+ (event.payload.requestId ===
1664
+ generationRequestId(trigger.payload.generationId) ||
1665
+ event.payload.generationId === trigger.payload.generationId),
1666
+ 'ai.session.closed': true,
1667
+ },
1668
+ handler: handleApproval,
1669
+ },
1670
+ 'ai.tool.result': {
1671
+ handler: async (ctx) => {
1672
+ if (ctx.event.payload.preliminary === true) return
1673
+ await continueIfReady(ctx, ctx.event.payload.generationId)
1674
+ },
1675
+ },
1676
+ } as NonNullable<ServerOptions<D>['handlers']>
1677
+ }
1678
+
1679
+ /** Assemble an A2 server with the built-in agent handlers and app extensions. */
1680
+ export function createAgentServer<
1681
+ M extends UIMessage,
1682
+ D extends AIEventDefs<M> & EventDefs,
1683
+ T extends ToolSet = ToolSet,
1684
+ >(options: CreateAgentServerOptions<M, D, T>): A2Server<D> {
1685
+ const {
1686
+ agent: definition,
1687
+ model,
1688
+ tools,
1689
+ instructions,
1690
+ generation,
1691
+ maxSteps,
1692
+ messageMetadata,
1693
+ generate,
1694
+ compaction,
1695
+ progress,
1696
+ handlers,
1697
+ ...serverOptions
1698
+ } = options
1699
+ const builtIns = createHandlers({
1700
+ agent: definition,
1701
+ model,
1702
+ ...(tools === undefined ? {} : { tools }),
1703
+ ...(instructions === undefined ? {} : { instructions }),
1704
+ ...(generation === undefined ? {} : { generation }),
1705
+ ...(maxSteps === undefined ? {} : { maxSteps }),
1706
+ ...(messageMetadata === undefined ? {} : { messageMetadata }),
1707
+ ...(generate === undefined ? {} : { generate }),
1708
+ ...(compaction === undefined ? {} : { compaction }),
1709
+ ...(progress === undefined ? {} : { progress }),
1710
+ })
1711
+ return createServer({
1712
+ ...serverOptions,
1713
+ contract: definition.contract,
1714
+ handlers: { ...builtIns, ...handlers },
1715
+ validatePush: validateAgentPush,
1716
+ })
1717
+ }
1718
+
1719
+ export type { Instructions, LanguageModel, ToolSet }