experimental-a2 0.0.0 → 0.2.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 (55) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/ai-server.browser.js +2 -2
  3. package/dist/ai-server.d.ts +19 -7
  4. package/dist/ai-server.js +730 -96
  5. package/dist/ai.d.ts +32 -11
  6. package/dist/ai.js +253 -75
  7. package/dist/client.d.ts +1 -1
  8. package/dist/client.js +4 -4
  9. package/dist/{contract-B0kAXoaL.js → contract-CG_adnu_.js} +2 -1
  10. package/dist/{contract-DL8btVd9.d.ts → contract-C_3dIIEU.d.ts} +4 -1
  11. package/dist/devtools-server.browser.js +2 -2
  12. package/dist/devtools-server.js +1 -1
  13. package/dist/http.d.ts +1 -1
  14. package/dist/http.js +4 -3
  15. package/dist/idempotent-replay-BMyHrP0L.js +19 -0
  16. package/dist/index.d.ts +4 -4
  17. package/dist/index.js +1 -1
  18. package/dist/{internal-Dm8Ejnud.js → internal-D6wNxTck.js} +3 -3
  19. package/dist/{log-Dg1I8NRr.d.ts → log-ldf5g8Cx.d.ts} +74 -56
  20. package/dist/log-memory.d.ts +1 -1
  21. package/dist/log-memory.js +173 -96
  22. package/dist/{log-polling-RO7kclzR.js → log-polling-6COoN60V.js} +1 -1
  23. package/dist/log-postgres.d.ts +1 -1
  24. package/dist/log-postgres.js +235 -192
  25. package/dist/log-redis.d.ts +1 -1
  26. package/dist/log-redis.js +453 -263
  27. package/dist/log-sqlite.d.ts +1 -1
  28. package/dist/log-sqlite.js +216 -127
  29. package/dist/otel.d.ts +1 -1
  30. package/dist/otel.js +1 -1
  31. package/dist/react.d.ts +1 -1
  32. package/dist/react.js +1 -1
  33. package/dist/recovery-vercel.d.ts +2 -2
  34. package/dist/recovery-vercel.js +9 -10
  35. package/dist/server-DJgD2YWP.js +877 -0
  36. package/dist/server.browser.js +4 -4
  37. package/dist/server.d.ts +46 -27
  38. package/dist/server.js +1 -1
  39. package/dist/{telemetry-C78al20p.d.ts → telemetry-Cso0qyHQ.d.ts} +1 -1
  40. package/dist/{wire-2QpU1EtJ.js → wire-BVsgR8o9.js} +1 -1
  41. package/docs/01-quickstart.mdx +7 -7
  42. package/docs/concepts/01-contracts.mdx +22 -22
  43. package/docs/concepts/02-handlers.mdx +223 -89
  44. package/docs/concepts/03-durability.mdx +199 -112
  45. package/docs/concepts/04-state.mdx +27 -1
  46. package/docs/guides/01-timers.mdx +4 -4
  47. package/docs/guides/02-cancellation.mdx +32 -4
  48. package/docs/guides/05-production.mdx +61 -27
  49. package/docs/guides/06-ai-agents.mdx +151 -70
  50. package/docs/guides/07-devtools.mdx +6 -3
  51. package/docs/guides/08-application-data.mdx +5 -6
  52. package/docs/index.mdx +30 -14
  53. package/docs/reference/01-api.mdx +305 -70
  54. package/package.json +31 -31
  55. package/dist/server-DYsnKTTy.js +0 -780
package/dist/ai-server.js CHANGED
@@ -1,12 +1,286 @@
1
- import { t as createServer } from "./server-DYsnKTTy.js";
2
- import { ToolLoopAgent, createAgentUIStream } from "ai";
1
+ import { t as A2Error } from "./errors-BJRMd-h6.js";
2
+ import { t as createServer } from "./server-DJgD2YWP.js";
3
+ import { convertToModelMessages, stepCountIs, streamText, toUIMessageStream } from "ai";
4
+ //#region src/ai-sdk-step.ts
5
+ const CONTROLLED_SETTINGS = [
6
+ "_internal",
7
+ "abortSignal",
8
+ "instructions",
9
+ "messages",
10
+ "model",
11
+ "onToolExecutionEnd",
12
+ "onToolExecutionStart",
13
+ "experimental_onToolCallFinish",
14
+ "experimental_onToolCallStart",
15
+ "experimental_sandbox",
16
+ "experimental_toolCallers",
17
+ "experimental_toolApprovalSecret",
18
+ "prompt",
19
+ "prepareStep",
20
+ "stopWhen",
21
+ "system",
22
+ "tools",
23
+ "toolsContext"
24
+ ];
25
+ const safeSettings = (settings) => {
26
+ const result = { ...settings };
27
+ for (const key of CONTROLLED_SETTINGS) Reflect.deleteProperty(result, key);
28
+ if (typeof result.timeout === "object" && result.timeout !== null) {
29
+ const timeout = { ...result.timeout };
30
+ Reflect.deleteProperty(timeout, "toolMs");
31
+ Reflect.deleteProperty(timeout, "tools");
32
+ result.timeout = timeout;
33
+ }
34
+ return result;
35
+ };
36
+ const modelToolSet = (tools) => Object.fromEntries(Object.entries(tools).map(([name, tool]) => {
37
+ if (tool.type === "provider" && tool.isProviderExecuted === true) return [name, tool];
38
+ const definition = { ...tool };
39
+ Reflect.deleteProperty(definition, "execute");
40
+ return [name, definition];
41
+ }));
42
+ const errorMessage$1 = (error) => error instanceof Error ? error.message : String(error);
43
+ /** Run exactly one AI SDK model step while leaving local tool execution to A2. */
44
+ async function generateAISDKStep(input) {
45
+ const settings = safeSettings(input.settings);
46
+ const tools = modelToolSet(input.tools);
47
+ const messages = await convertToModelMessages(input.messages, { tools });
48
+ const streamOptions = {
49
+ ...settings,
50
+ model: input.model,
51
+ tools,
52
+ messages,
53
+ abortSignal: input.abortSignal,
54
+ ...input.instructions === void 0 ? {} : { instructions: input.instructions },
55
+ stopWhen: stepCountIs(1),
56
+ onError: settings.onError ?? (() => {})
57
+ };
58
+ const result = streamText(streamOptions);
59
+ let completion;
60
+ let fatalError;
61
+ const observed = result.stream.pipeThrough(new TransformStream({ transform(part, controller) {
62
+ if (part.type === "error" && fatalError === void 0) fatalError = { value: part.error };
63
+ else if (part.type === "finish") completion = {
64
+ finishReason: part.finishReason,
65
+ usage: part.totalUsage
66
+ };
67
+ controller.enqueue(part);
68
+ } }));
69
+ return {
70
+ stream: toUIMessageStream({
71
+ stream: observed,
72
+ tools,
73
+ originalMessages: input.messages,
74
+ generateMessageId: () => input.responseMessageId,
75
+ onError: errorMessage$1
76
+ }),
77
+ completion() {
78
+ if (fatalError !== void 0) throw fatalError.value;
79
+ if (completion === void 0) throw new Error("AI SDK step ended without a finish part");
80
+ if (completion.finishReason === "error") throw new Error("AI SDK step finished with an error");
81
+ return completion;
82
+ }
83
+ };
84
+ }
85
+ //#endregion
86
+ //#region src/ai-coordinator.ts
87
+ const updateResponse = (state, update) => state.response === void 0 ? state : {
88
+ ...state,
89
+ response: update(state.response)
90
+ };
91
+ const foldCoordinator = (state, event) => {
92
+ switch (event.type) {
93
+ case "ai.session.closed": return {
94
+ closed: true,
95
+ queued: []
96
+ };
97
+ case "ai.message.created": {
98
+ const message = event.payload.message;
99
+ if (message.role !== "user") return state;
100
+ return {
101
+ ...state,
102
+ queued: [...state.queued.filter((item) => item.messageId !== message.id), {
103
+ index: event.index,
104
+ messageId: message.id
105
+ }].toSorted((left, right) => left.index - right.index)
106
+ };
107
+ }
108
+ case "ai.generation.requested": {
109
+ const request = event.payload;
110
+ if (request.reason === "message") {
111
+ const responseMessageId = request.responseMessageId ?? `${request.messageId}:assistant`;
112
+ return {
113
+ ...state,
114
+ queued: state.queued.filter((item) => item.messageId !== request.messageId),
115
+ response: {
116
+ rootMessageId: request.messageId,
117
+ responseMessageId,
118
+ status: "requested",
119
+ activeRequestId: event.id,
120
+ calls: [],
121
+ inputs: []
122
+ }
123
+ };
124
+ }
125
+ if (state.response === void 0 || request.responseMessageId !== state.response.responseMessageId) return state;
126
+ if (request.reason === "tool" && (!continuationReady(state) || event.id !== `ai.generate:tools:${state.response.generation?.generationId}`) || request.reason === "retry" && state.response.status !== "failed" || request.reason === "input" && state.response.inputResponse === void 0) return state;
127
+ return updateResponse(state, (response) => ({
128
+ rootMessageId: response.rootMessageId,
129
+ responseMessageId: response.responseMessageId,
130
+ status: "requested",
131
+ activeRequestId: event.id,
132
+ calls: [],
133
+ inputs: []
134
+ }));
135
+ }
136
+ case "ai.generation.started": {
137
+ const generation = event.payload;
138
+ if (state.response?.responseMessageId !== generation.responseMessageId || state.response.activeRequestId !== generation.requestId || state.response.generation?.requestId === generation.requestId && state.response.generation.attempt >= generation.attempt) return state;
139
+ return updateResponse(state, (response) => ({
140
+ rootMessageId: response.rootMessageId,
141
+ responseMessageId: response.responseMessageId,
142
+ status: "generating",
143
+ generation,
144
+ activeRequestId: generation.requestId,
145
+ calls: [],
146
+ inputs: []
147
+ }));
148
+ }
149
+ case "ai.generation.completed": {
150
+ const completion = event.payload;
151
+ if (state.response?.generation?.generationId !== completion.generationId) return state;
152
+ return updateResponse(state, (response) => ({
153
+ ...response,
154
+ status: "waiting",
155
+ completion
156
+ }));
157
+ }
158
+ case "ai.generation.failed": {
159
+ const failure = event.payload;
160
+ const response = state.response;
161
+ const ownsActiveGeneration = response?.generation?.generationId === failure.generationId;
162
+ const ownsActiveStepLimit = failure.stepLimit === true && response?.activeRequestId === `ai.generate:tools:${failure.generationId}` && response.responseMessageId === failure.responseMessageId;
163
+ if (!ownsActiveGeneration && !ownsActiveStepLimit) return state;
164
+ return updateResponse(state, (current) => ({
165
+ ...current,
166
+ status: "failed",
167
+ failure
168
+ }));
169
+ }
170
+ case "ai.message.completed": {
171
+ const messageId = event.payload.messageId;
172
+ return state.response?.responseMessageId === messageId ? {
173
+ closed: state.closed,
174
+ queued: state.queued
175
+ } : state;
176
+ }
177
+ case "ai.message.interrupted": {
178
+ const interruption = event.payload;
179
+ const response = state.response;
180
+ return response?.responseMessageId === interruption.messageId && (interruption.generationId === void 0 || interruption.generationId === response.generation?.generationId) ? {
181
+ closed: state.closed,
182
+ queued: state.queued
183
+ } : state;
184
+ }
185
+ case "ai.tool.called": {
186
+ const call = event.payload;
187
+ if (state.response?.generation?.generationId !== call.generationId) return state;
188
+ return updateResponse(state, (response) => ({
189
+ ...response,
190
+ calls: [...response.calls.filter((candidate) => candidate.call.toolCallId !== call.toolCallId), {
191
+ index: event.index,
192
+ call,
193
+ terminal: false
194
+ }]
195
+ }));
196
+ }
197
+ case "ai.approval.requested": {
198
+ const approval = event.payload;
199
+ if (state.response?.generation?.generationId !== approval.generationId) return state;
200
+ return updateResponse(state, (response) => ({
201
+ ...response,
202
+ calls: response.calls.map((candidate) => candidate.call.toolCallId === approval.toolCallId ? {
203
+ ...candidate,
204
+ approval
205
+ } : candidate)
206
+ }));
207
+ }
208
+ case "ai.approval.responded": {
209
+ const approval = event.payload;
210
+ if (state.response?.responseMessageId !== approval.messageId || state.response.generation?.generationId !== approval.generationId) return state;
211
+ return updateResponse(state, (response) => ({
212
+ ...response,
213
+ calls: response.calls.map((candidate) => candidate.approval?.approvalId === approval.approvalId ? {
214
+ ...candidate,
215
+ response: approval,
216
+ responseIndex: event.index
217
+ } : candidate)
218
+ }));
219
+ }
220
+ case "ai.tool.result": {
221
+ const result = event.payload;
222
+ if (result.preliminary === true || state.response?.generation?.generationId !== result.generationId) return state;
223
+ return updateResponse(state, (response) => ({
224
+ ...response,
225
+ calls: response.calls.map((candidate) => candidate.call.toolCallId === result.toolCallId ? {
226
+ ...candidate,
227
+ terminal: true
228
+ } : candidate)
229
+ }));
230
+ }
231
+ case "ai.input.requested": {
232
+ const input = event.payload;
233
+ if (state.response?.responseMessageId !== input.messageId || state.response.generation?.generationId !== input.generationId) return state;
234
+ return updateResponse(state, (response) => ({
235
+ ...response,
236
+ inputs: [...response.inputs.filter((candidate) => candidate.messageId !== input.messageId || candidate.generationId !== input.generationId || candidate.inputId !== input.inputId), input]
237
+ }));
238
+ }
239
+ case "ai.input.responded": {
240
+ const input = event.payload;
241
+ const requested = state.response?.inputs.find((candidate) => candidate.messageId === input.messageId && candidate.generationId === input.generationId && candidate.inputId === input.inputId && candidate.name === input.name);
242
+ if (state.response?.responseMessageId !== input.messageId || state.response.generation?.generationId !== input.generationId || requested === void 0) return state;
243
+ return updateResponse(state, (response) => ({
244
+ ...response,
245
+ inputs: response.inputs.filter((candidate) => candidate.inputId !== input.inputId),
246
+ inputResponse: {
247
+ index: event.index,
248
+ generationId: input.generationId,
249
+ inputId: input.inputId,
250
+ name: requested.name
251
+ }
252
+ }));
253
+ }
254
+ default: return state;
255
+ }
256
+ };
257
+ const aiCoordinatorReducer = (contract) => contract.reducer({
258
+ name: "a2.ai.coordinator.v1",
259
+ initialState: {
260
+ closed: false,
261
+ queued: []
262
+ }
263
+ }).fold((state, event) => foldCoordinator(state, event));
264
+ const continuationReady = (state) => {
265
+ const response = state.response;
266
+ return !state.closed && response?.completion?.finishReason === "tool-calls" && response.generation?.generationId === response.completion.generationId && response.failure === void 0 && response.calls.length > 0 && response.calls.every((call) => call.terminal || call.call.providerExecuted === true && call.call.supportsDeferredResults !== true && call.approval !== void 0 && call.response !== void 0);
267
+ };
268
+ //#endregion
3
269
  //#region src/ai-server.ts
4
270
  /**
5
- * a2/ai/server — the server-only implementation of an a2/ai definition.
271
+ * experimental-a2/ai/server — the server-only implementation of an experimental-a2/ai definition.
6
272
  *
7
273
  * `createHandlers()` exposes the ordinary A2 handler table;
8
274
  * `createAgentServer()` is its batteries-included assembly with createServer.
9
275
  */
276
+ function validateAgentPush(context) {
277
+ const rejected = context.events.find((event) => {
278
+ if (event.type !== "ai.message.created") return !(event.type === "ai.approval.responded" || event.type === "ai.input.responded" || event.type === "ai.message.interrupted" || event.type === "ai.retry.requested");
279
+ const payload = event.payload;
280
+ return typeof payload !== "object" || payload === null || !("message" in payload) || typeof payload.message !== "object" || payload.message === null || !("role" in payload.message) || payload.message.role !== "user";
281
+ });
282
+ if (rejected !== void 0) throw new A2Error("INVALID_PAYLOAD", `event '${rejected.type}' is server-only for an AI agent`);
283
+ }
10
284
  const resolve = async (value, context) => typeof value === "function" ? await value(context) : value;
11
285
  const modelName = (model) => {
12
286
  if (typeof model === "string") return model;
@@ -14,23 +288,8 @@ const modelName = (model) => {
14
288
  return "custom";
15
289
  };
16
290
  const errorMessage = (error) => error instanceof Error ? error.message : String(error);
17
- const addTokenCounts = (left, right) => left === void 0 && right === void 0 ? void 0 : (left ?? 0) + (right ?? 0);
18
- const addUsage = (left, right) => {
19
- return {
20
- inputTokens: addTokenCounts(left?.inputTokens, right.inputTokens),
21
- inputTokenDetails: {
22
- noCacheTokens: addTokenCounts(left?.inputTokenDetails.noCacheTokens, right.inputTokenDetails.noCacheTokens),
23
- cacheReadTokens: addTokenCounts(left?.inputTokenDetails.cacheReadTokens, right.inputTokenDetails.cacheReadTokens),
24
- cacheWriteTokens: addTokenCounts(left?.inputTokenDetails.cacheWriteTokens, right.inputTokenDetails.cacheWriteTokens)
25
- },
26
- outputTokens: addTokenCounts(left?.outputTokens, right.outputTokens),
27
- outputTokenDetails: {
28
- textTokens: addTokenCounts(left?.outputTokenDetails.textTokens, right.outputTokenDetails.textTokens),
29
- reasoningTokens: addTokenCounts(left?.outputTokenDetails.reasoningTokens, right.outputTokenDetails.reasoningTokens)
30
- },
31
- totalTokens: addTokenCounts(left?.totalTokens, right.totalTokens)
32
- };
33
- };
291
+ const promptCacheKey = (sessionId, generationId) => `${sessionId}\u001f${generationId}`;
292
+ const isAsyncIterable = (value) => typeof value === "object" && value !== null && Symbol.asyncIterator in value && typeof value[Symbol.asyncIterator] === "function";
34
293
  const isBoundaryChunk = (chunk) => chunk !== void 0 && (chunk.type === "text-end" || chunk.type === "reasoning-end" || chunk.type === "tool-input-available" || chunk.type === "tool-input-error" || chunk.type === "tool-output-available" || chunk.type === "tool-output-error" || chunk.type === "tool-output-denied" || chunk.type === "tool-approval-request" || chunk.type === "finish" || chunk.type === "abort" || chunk.type === "error");
35
294
  const nextOrTimer = async (next, delayMs) => {
36
295
  let timer;
@@ -47,41 +306,24 @@ const nextOrTimer = async (next, delayMs) => {
47
306
  }
48
307
  };
49
308
  async function generateWithAISDK(context) {
50
- const { onStepEnd, onStepFinish, ...settings } = context.generation;
51
- const userOnStepEnd = onStepEnd ?? onStepFinish;
52
- const agentSettings = {
53
- ...settings,
54
- id: context.generationId,
309
+ const step = await generateAISDKStep({
55
310
  model: context.model,
56
311
  tools: context.tools,
57
- ...context.instructions === void 0 ? {} : { instructions: context.instructions }
58
- };
59
- const sdkAgent = new ToolLoopAgent(agentSettings);
60
- let usage;
61
- let finishReason;
312
+ messages: context.messages,
313
+ responseMessageId: context.responseMessageId,
314
+ abortSignal: context.signal,
315
+ ...context.instructions === void 0 ? {} : { instructions: context.instructions },
316
+ settings: context.generation
317
+ });
62
318
  return {
63
- stream: await createAgentUIStream({
64
- agent: sdkAgent,
65
- uiMessages: context.messages,
66
- generateMessageId: () => context.responseMessageId,
67
- abortSignal: context.signal,
68
- async onStepEnd(step) {
69
- usage = addUsage(usage, step.usage);
70
- await userOnStepEnd?.(step);
71
- },
72
- onEnd(event) {
73
- finishReason = event.finishReason;
74
- }
75
- }),
76
- completion: () => ({
77
- ...finishReason === void 0 ? {} : { finishReason },
78
- ...usage === void 0 ? {} : { usage }
79
- })
319
+ stream: step.stream,
320
+ completion: step.completion
80
321
  };
81
322
  }
82
323
  async function* consumeGeneration(options) {
83
324
  const pending = [];
84
325
  let streamedFinishReason;
326
+ let streamedError;
85
327
  const maxChunks = options.progress?.maxChunks ?? 16;
86
328
  const maxDelayMs = options.progress?.maxDelayMs ?? 30;
87
329
  let lastFlush = Date.now();
@@ -107,6 +349,7 @@ async function* consumeGeneration(options) {
107
349
  const chunk = result.value;
108
350
  pending.push(chunk);
109
351
  if (chunk.type === "finish") streamedFinishReason = chunk.finishReason;
352
+ if (chunk.type === "error") streamedError = chunk.errorText;
110
353
  pendingChunk = chunks.next();
111
354
  if (pending.length >= maxChunks || isBoundaryChunk(pending.at(-1))) {
112
355
  yield {
@@ -123,11 +366,21 @@ async function* consumeGeneration(options) {
123
366
  };
124
367
  throw error;
125
368
  }
126
- const completion = options.source.completion?.();
369
+ let completion;
370
+ try {
371
+ completion = options.source.completion?.();
372
+ } catch (error) {
373
+ if (pending.length > 0) yield {
374
+ type: "progress",
375
+ chunks: pending.splice(0)
376
+ };
377
+ throw error;
378
+ }
127
379
  if (pending.length > 0) yield {
128
380
  type: "progress",
129
381
  chunks: pending.splice(0)
130
382
  };
383
+ if (streamedError !== void 0) throw new Error(streamedError);
131
384
  const finishReason = completion?.finishReason ?? streamedFinishReason;
132
385
  yield {
133
386
  type: "finish",
@@ -144,10 +397,45 @@ const contextMessages = (state) => {
144
397
  const compaction = state.compaction;
145
398
  if (compaction?.status !== "completed" || !compaction.messages) return state.messages;
146
399
  const boundary = state.messages.findIndex((message) => message.id === compaction.throughMessageId);
147
- return boundary === -1 ? state.messages : [...compaction.messages, ...state.messages.slice(boundary + 1)];
400
+ const retained = new Set(compaction.retainedMessageIds ?? []);
401
+ return boundary === -1 ? state.messages : [
402
+ ...compaction.messages,
403
+ ...state.messages.slice(0, boundary + 1).filter((message) => retained.has(message.id)),
404
+ ...state.messages.slice(boundary + 1)
405
+ ];
406
+ };
407
+ const activeContextMessages = (state, coordinator) => {
408
+ const queued = new Set(coordinator.queued.map((item) => item.messageId));
409
+ return contextMessages(state).filter((message) => !queued.has(message.id));
410
+ };
411
+ const toolCalledEvent = (payload) => ({
412
+ type: "ai.tool.called",
413
+ id: `${payload.generationId}:tool:${payload.toolCallId}:called`,
414
+ payload
415
+ });
416
+ const approvalClassification = (tools, generation, toolName) => {
417
+ const configured = generation.toolApproval;
418
+ if (typeof configured === "function") return "unknown";
419
+ const policy = configured?.[toolName];
420
+ if (typeof policy === "function") return "unknown";
421
+ if (policy === "user-approval" || typeof policy === "object" && policy?.type === "user-approval") return "approval";
422
+ if (policy === "denied" || typeof policy === "object" && policy?.type === "denied") return "approval";
423
+ const tool = tools[toolName];
424
+ if (typeof tool?.needsApproval === "function") return "unknown";
425
+ return tool?.needsApproval === true ? "approval" : "automatic";
148
426
  };
149
427
  const lifecycleEvents = (options) => {
150
428
  const result = [];
429
+ const pending = [...options.pending];
430
+ const take = (toolCallId) => {
431
+ const index = pending.findIndex((candidate) => candidate.call.toolCallId === toolCallId);
432
+ if (index === -1) return void 0;
433
+ return pending.splice(index, 1)[0];
434
+ };
435
+ const flush = (toolCallId) => {
436
+ const pendingCall = take(toolCallId);
437
+ if (pendingCall) result.push(toolCalledEvent(pendingCall.call));
438
+ };
151
439
  for (const [chunkIndex, chunk] of options.chunks.entries()) {
152
440
  const resultId = `${options.generationId}:tool-result:${options.sequence}:${chunkIndex}`;
153
441
  if (chunk.type === "tool-input-available") {
@@ -160,14 +448,17 @@ const lifecycleEvents = (options) => {
160
448
  input: chunk.input,
161
449
  ...chunk.dynamic === void 0 ? {} : { dynamic: chunk.dynamic },
162
450
  ...chunk.providerExecuted === void 0 ? {} : { providerExecuted: chunk.providerExecuted },
451
+ ...options.tools[chunk.toolName]?.supportsDeferredResults === true ? { supportsDeferredResults: true } : {},
163
452
  ...chunk.providerMetadata === void 0 ? {} : { providerMetadata: chunk.providerMetadata },
164
453
  ...chunk.toolMetadata === void 0 ? {} : { toolMetadata: chunk.toolMetadata },
165
454
  ...chunk.title === void 0 ? {} : { title: chunk.title }
166
455
  };
167
- result.push({
168
- type: "ai.tool.called",
169
- id: `${options.generationId}:tool:${chunk.toolCallId}:called`,
170
- payload
456
+ take(payload.toolCallId);
457
+ const classification = approvalClassification(options.tools, options.generation, chunk.toolName);
458
+ if (classification === "automatic") result.push(toolCalledEvent(payload));
459
+ else pending.push({
460
+ call: payload,
461
+ classification
171
462
  });
172
463
  } else if (chunk.type === "tool-input-error") {
173
464
  const payload = {
@@ -184,12 +475,22 @@ const lifecycleEvents = (options) => {
184
475
  ...chunk.providerMetadata === void 0 ? {} : { providerMetadata: chunk.providerMetadata },
185
476
  ...chunk.toolMetadata === void 0 ? {} : { toolMetadata: chunk.toolMetadata }
186
477
  };
187
- result.push({
478
+ const called = {
479
+ requestId: options.requestId,
480
+ messageId: options.messageId,
481
+ generationId: options.generationId,
482
+ toolCallId: chunk.toolCallId,
483
+ toolName: chunk.toolName,
484
+ input: chunk.input
485
+ };
486
+ take(chunk.toolCallId);
487
+ result.push(toolCalledEvent(called), {
188
488
  type: "ai.tool.result",
189
489
  id: resultId,
190
490
  payload
191
491
  });
192
492
  } else if (chunk.type === "tool-output-available") {
493
+ flush(chunk.toolCallId);
193
494
  const payload = {
194
495
  requestId: options.requestId,
195
496
  messageId: options.messageId,
@@ -209,6 +510,7 @@ const lifecycleEvents = (options) => {
209
510
  payload
210
511
  });
211
512
  } else if (chunk.type === "tool-output-error") {
513
+ flush(chunk.toolCallId);
212
514
  const payload = {
213
515
  requestId: options.requestId,
214
516
  messageId: options.messageId,
@@ -227,6 +529,7 @@ const lifecycleEvents = (options) => {
227
529
  payload
228
530
  });
229
531
  } else if (chunk.type === "tool-output-denied") {
532
+ flush(chunk.toolCallId);
230
533
  const payload = {
231
534
  requestId: options.requestId,
232
535
  messageId: options.messageId,
@@ -241,6 +544,7 @@ const lifecycleEvents = (options) => {
241
544
  payload
242
545
  });
243
546
  } else if (chunk.type === "tool-approval-request") {
547
+ flush(chunk.toolCallId);
244
548
  const payload = {
245
549
  messageId: options.messageId,
246
550
  generationId: options.generationId,
@@ -254,10 +558,52 @@ const lifecycleEvents = (options) => {
254
558
  id: `${options.generationId}:approval:${chunk.approvalId}`,
255
559
  payload
256
560
  });
257
- }
561
+ } else if (chunk.type === "tool-approval-response") result.push({
562
+ type: "ai.approval.responded",
563
+ id: `${options.generationId}:approval:${chunk.approvalId}:automatic-response`,
564
+ payload: {
565
+ messageId: options.messageId,
566
+ generationId: options.generationId,
567
+ approvalId: chunk.approvalId,
568
+ approved: chunk.approved,
569
+ ...chunk.reason === void 0 ? {} : { reason: chunk.reason }
570
+ }
571
+ });
258
572
  }
259
- return result;
573
+ return {
574
+ events: result,
575
+ pending
576
+ };
260
577
  };
578
+ function queuedMessagesAt(history) {
579
+ const started = new Set(history.filter((event) => event.type === "ai.generation.requested").map((event) => event.payload).filter((request) => request.reason === "message").map((request) => request.messageId));
580
+ return history.flatMap((event) => {
581
+ if (event.type !== "ai.message.created") return [];
582
+ const message = event.payload.message;
583
+ return message.role === "user" && !started.has(message.id) ? [{
584
+ index: event.index,
585
+ messageId: message.id
586
+ }] : [];
587
+ });
588
+ }
589
+ function withoutGenerationLifecycle(history, generationIds) {
590
+ if (generationIds.size === 0) return history;
591
+ const approvalKeys = /* @__PURE__ */ new Set();
592
+ const inputKeys = /* @__PURE__ */ new Set();
593
+ for (const event of history) {
594
+ const payload = event.payload;
595
+ if (typeof payload.generationId !== "string" || !generationIds.has(payload.generationId) || typeof payload.messageId !== "string") continue;
596
+ if (event.type === "ai.approval.requested" && typeof payload.approvalId === "string") approvalKeys.add(`${payload.messageId}\u001f${payload.approvalId}`);
597
+ if (event.type === "ai.input.requested" && typeof payload.inputId === "string") inputKeys.add(`${payload.messageId}\u001f${payload.inputId}`);
598
+ }
599
+ return history.filter((event) => {
600
+ const payload = event.payload;
601
+ if (typeof payload.generationId === "string" && generationIds.has(payload.generationId)) return false;
602
+ if (event.type === "ai.approval.responded" && typeof payload.messageId === "string" && typeof payload.approvalId === "string") return !approvalKeys.has(`${payload.messageId}\u001f${payload.approvalId}`);
603
+ if (event.type === "ai.input.responded" && typeof payload.messageId === "string" && typeof payload.inputId === "string") return !inputKeys.has(`${payload.messageId}\u001f${payload.inputId}`);
604
+ return true;
605
+ });
606
+ }
261
607
  /**
262
608
  * Build the ordinary A2 handler table for the built-in agent protocol.
263
609
  * Application handlers can be spread beside this table.
@@ -266,11 +612,161 @@ function createHandlers(options) {
266
612
  if (options.model === void 0) throw new TypeError("createHandlers requires model");
267
613
  if (options.progress?.maxChunks !== void 0 && (!Number.isInteger(options.progress.maxChunks) || options.progress.maxChunks < 1)) throw new TypeError("progress.maxChunks must be a positive integer");
268
614
  if (options.progress?.maxDelayMs !== void 0 && (!Number.isFinite(options.progress.maxDelayMs) || options.progress.maxDelayMs < 0)) throw new TypeError("progress.maxDelayMs must be a non-negative number");
615
+ if (options.maxSteps !== void 0 && (!Number.isInteger(options.maxSteps) || options.maxSteps < 1)) throw new TypeError("maxSteps must be a positive integer");
616
+ const tools = options.tools ?? {};
617
+ const generation = options.generation ?? {};
618
+ const maxSteps = options.maxSteps ?? 20;
619
+ const coordinator = aiCoordinatorReducer(options.agent.contract);
620
+ const promptCache = /* @__PURE__ */ new Map();
621
+ const coordinatorStateAt = (history, frontier = Number.POSITIVE_INFINITY) => {
622
+ let state = coordinator.initialState;
623
+ for (const event of history) {
624
+ if (event.index >= frontier) break;
625
+ state = coordinator.fold(state, event);
626
+ }
627
+ return state;
628
+ };
629
+ const requestForNextMessage = (state) => {
630
+ if (state.closed || state.response !== void 0) return void 0;
631
+ const next = state.queued[0];
632
+ if (next === void 0) return void 0;
633
+ return {
634
+ type: "ai.generation.requested",
635
+ id: `ai.generate:message:${next.messageId}`,
636
+ payload: {
637
+ messageId: next.messageId,
638
+ reason: "message"
639
+ }
640
+ };
641
+ };
642
+ const scheduleNext = async (ctx) => requestForNextMessage((await ctx.session.state(coordinator)).state);
643
+ const continueIfReady = async (ctx, generationId) => {
644
+ const state = (await ctx.session.state(coordinator)).state;
645
+ const response = state.response;
646
+ if (response?.generation?.generationId !== generationId) return;
647
+ const input = response.inputResponse;
648
+ if (response.completion?.generationId === generationId && response.failure === void 0 && input !== void 0 && response.inputs.length === 0 && response.calls.every((call) => call.terminal || call.call.providerExecuted === true && call.call.supportsDeferredResults !== true && call.approval !== void 0 && call.response !== void 0)) {
649
+ await ctx.session.append("continue-after-input", {
650
+ type: "ai.generation.requested",
651
+ id: `ai.generate:input:${encodeURIComponent(response.responseMessageId)}:${encodeURIComponent(input.generationId)}:${encodeURIComponent(input.inputId)}`,
652
+ payload: {
653
+ messageId: response.responseMessageId,
654
+ responseMessageId: response.responseMessageId,
655
+ reason: "input"
656
+ }
657
+ });
658
+ return;
659
+ }
660
+ if (!continuationReady(state)) return;
661
+ await ctx.session.append("continue-after-tools", {
662
+ type: "ai.generation.requested",
663
+ id: `ai.generate:tools:${generationId}`,
664
+ payload: {
665
+ messageId: response.responseMessageId,
666
+ responseMessageId: response.responseMessageId,
667
+ reason: "tool"
668
+ }
669
+ });
670
+ };
671
+ const resultEvent = (call, suffix, result) => ({
672
+ type: "ai.tool.result",
673
+ id: `${call.generationId}:tool:${call.toolCallId}:${suffix}`,
674
+ payload: {
675
+ requestId: call.requestId,
676
+ messageId: call.messageId,
677
+ generationId: call.generationId,
678
+ toolCallId: call.toolCallId,
679
+ toolName: call.toolName,
680
+ input: call.input,
681
+ phase: "execution",
682
+ ...result.output === void 0 ? {} : { output: result.output },
683
+ ...result.error === void 0 ? {} : { error: result.error },
684
+ ...result.denied === void 0 ? {} : { denied: result.denied },
685
+ ...result.preliminary === void 0 ? {} : { preliminary: result.preliminary },
686
+ ...call.dynamic === void 0 ? {} : { dynamic: call.dynamic },
687
+ ...call.providerExecuted === void 0 ? {} : { providerExecuted: call.providerExecuted },
688
+ ...call.toolMetadata === void 0 ? {} : { toolMetadata: call.toolMetadata }
689
+ }
690
+ });
691
+ const promptMessages = (sessionId, call, readHistory) => {
692
+ const key = promptCacheKey(sessionId, call.generationId);
693
+ const cached = promptCache.get(key);
694
+ if (cached) return cached;
695
+ const computation = (async () => {
696
+ const history = await readHistory();
697
+ const coordinatorState = coordinatorStateAt(history);
698
+ const compaction = history.find((event) => event.type === "ai.compaction.completed" && event.payload.generationId === call.generationId);
699
+ if (compaction !== void 0) {
700
+ const messages = compaction.payload.messages.filter((message) => !coordinatorState.queued.some((queued) => queued.messageId === message.id));
701
+ return convertToModelMessages(messages, { tools });
702
+ }
703
+ const frontier = history.find((event) => event.type === "ai.generation.started" && event.payload.generationId === call.generationId)?.index ?? Number.POSITIVE_INFINITY;
704
+ const state = replay(options.agent, history.filter((event) => event.index < frontier));
705
+ return convertToModelMessages(activeContextMessages(state, coordinatorState), { tools });
706
+ })();
707
+ promptCache.set(key, computation);
708
+ computation.catch(() => {
709
+ if (promptCache.get(key) === computation) promptCache.delete(key);
710
+ });
711
+ return computation;
712
+ };
713
+ const executeTool = async (ctx, call) => {
714
+ const tool = tools[call.toolName];
715
+ if (tool?.execute === void 0) return resultEvent(call, "execution:error", { error: `Tool '${call.toolName}' has no server executor` });
716
+ const messages = await promptMessages(ctx.event.sessionId, call, ctx.session.history);
717
+ let output;
718
+ try {
719
+ output = await tool.execute(call.input, {
720
+ toolCallId: call.toolCallId,
721
+ messages,
722
+ abortSignal: ctx.signal,
723
+ context: void 0
724
+ });
725
+ } catch (error) {
726
+ if (ctx.signal.aborted) return;
727
+ return resultEvent(call, "execution:error", { error: errorMessage(error) });
728
+ }
729
+ if (!isAsyncIterable(output)) return resultEvent(call, "execution:0", { output });
730
+ let last;
731
+ let sequence = 0;
732
+ for await (const value of output) {
733
+ if (ctx.signal.aborted) return;
734
+ last = value;
735
+ await ctx.session.append(`tool:${call.toolCallId}:preliminary:${sequence}`, resultEvent(call, `execution:${sequence}:preliminary`, {
736
+ output: value,
737
+ preliminary: true
738
+ }));
739
+ sequence += 1;
740
+ }
741
+ return resultEvent(call, `execution:${sequence}:final`, sequence === 0 ? {} : { output: last });
742
+ };
743
+ const handleToolCall = async (ctx) => {
744
+ const response = (await ctx.session.state(coordinator)).state.response;
745
+ const current = response?.calls.find((candidate) => candidate.index === ctx.event.index);
746
+ if (current === void 0 || response?.generation?.generationId !== ctx.event.payload.generationId || response.failure !== void 0 || current.terminal || current.approval !== void 0) return;
747
+ if (current.call.providerExecuted === true) {
748
+ await continueIfReady(ctx, current.call.generationId);
749
+ return;
750
+ }
751
+ return executeTool(ctx, current.call);
752
+ };
753
+ const handleApproval = async (ctx) => {
754
+ const response = (await ctx.session.state(coordinator)).state.response;
755
+ const current = response?.calls.find((candidate) => candidate.approval?.approvalId === ctx.event.payload.approvalId && candidate.approval.messageId === ctx.event.payload.messageId && candidate.approval.generationId === ctx.event.payload.generationId && candidate.responseIndex === ctx.event.index);
756
+ if (current === void 0 || response?.generation?.generationId !== current.call.generationId || response.failure !== void 0 || current.terminal) return;
757
+ if (current.call.providerExecuted === true) {
758
+ await continueIfReady(ctx, current.call.generationId);
759
+ return;
760
+ }
761
+ if (!ctx.event.payload.approved) return resultEvent(current.call, "execution:denied", { denied: true });
762
+ return executeTool(ctx, current.call);
763
+ };
269
764
  const generationHandler = async (ctx) => {
270
- const history = await ctx.history();
765
+ const history = await ctx.session.history();
271
766
  const requestId = ctx.event.id;
272
767
  const request = ctx.event.payload;
273
- if (history.some((event) => event.type === "ai.session.closed")) return;
768
+ const coordinatorState = (await ctx.session.state(coordinator)).state;
769
+ if (coordinatorState.closed || coordinatorState.response?.activeRequestId !== requestId) return;
274
770
  if (history.some((event) => {
275
771
  if (event.type === "ai.generation.completed") return event.payload.requestId === requestId;
276
772
  if (event.type !== "ai.generation.failed") return false;
@@ -278,12 +774,23 @@ function createHandlers(options) {
278
774
  return payload.requestId === requestId && payload.superseded !== true;
279
775
  })) return;
280
776
  const previousStarts = history.filter((event) => event.type === "ai.generation.started" && event.payload.requestId === requestId);
281
- const attempt = previousStarts.length + 1;
777
+ if (previousStarts.map((event) => event.payload.attempt).some((priorAttempt) => priorAttempt >= ctx.attempt)) return;
778
+ const attempt = ctx.attempt;
282
779
  const generationId = `${requestId}:generation:${attempt}`;
283
780
  const responseMessageId = request.responseMessageId ?? (request.reason === "message" ? `${request.messageId}:assistant` : request.messageId);
284
- const previous = previousStarts.at(-1);
781
+ const responseStepCount = history.filter((event) => event.type === "ai.generation.completed" && event.payload.responseMessageId === responseMessageId).length;
782
+ const sourceCoordinatorState = coordinatorStateAt(history, ctx.event.index);
783
+ if (request.reason === "tool") {
784
+ if (!requestId.startsWith("ai.generate:tools:")) return;
785
+ const sourceGenerationId = requestId.slice(18);
786
+ if (!continuationReady(sourceCoordinatorState) || sourceCoordinatorState.response?.generation?.generationId !== sourceGenerationId || sourceCoordinatorState.response.responseMessageId !== request.messageId || sourceCoordinatorState.response.responseMessageId !== request.responseMessageId) return;
787
+ }
788
+ const previous = previousStarts.filter((event) => event.payload.attempt < attempt).toSorted((left, right) => right.payload.attempt - left.payload.attempt)[0];
285
789
  const incompleteId = previous ? previous.payload.generationId : void 0;
286
- const promptHistory = incompleteId === void 0 && request.reason !== "retry" ? history : history.filter((event) => !(event.type === "ai.generation.progress" && (incompleteId !== void 0 && event.payload.generationId === incompleteId || request.reason === "retry" && event.payload.responseMessageId === responseMessageId)));
790
+ const replacedGenerationIds = /* @__PURE__ */ new Set();
791
+ if (incompleteId !== void 0) replacedGenerationIds.add(incompleteId);
792
+ if (request.reason === "retry" && sourceCoordinatorState.response?.failure?.generationId !== void 0) replacedGenerationIds.add(sourceCoordinatorState.response.failure.generationId);
793
+ const promptHistory = withoutGenerationLifecycle(history, replacedGenerationIds);
287
794
  let state = replay(options.agent, promptHistory);
288
795
  const resolverContext = {
289
796
  event: ctx.event,
@@ -293,6 +800,23 @@ function createHandlers(options) {
293
800
  };
294
801
  const resolvedModel = await resolve(options.model, resolverContext);
295
802
  if (resolvedModel === void 0) throw new TypeError("the model resolver returned undefined");
803
+ if (ctx.signal.aborted) return;
804
+ if (request.reason === "tool" && responseStepCount >= maxSteps) {
805
+ const source = sourceCoordinatorState.response?.generation;
806
+ if (source === void 0) return;
807
+ return {
808
+ type: "ai.generation.failed",
809
+ id: `${requestId}:step-limit`,
810
+ payload: {
811
+ requestId: source.requestId,
812
+ messageId: source.messageId,
813
+ generationId: source.generationId,
814
+ responseMessageId,
815
+ error: `agent exceeded the ${maxSteps}-step limit`,
816
+ stepLimit: true
817
+ }
818
+ };
819
+ }
296
820
  const started = {
297
821
  requestId,
298
822
  messageId: request.messageId,
@@ -323,8 +847,14 @@ function createHandlers(options) {
323
847
  id: generationId,
324
848
  payload: started
325
849
  });
326
- await ctx.append(...startEvents);
327
- let messages = contextMessages(state);
850
+ await ctx.session.append("generation-start", ...startEvents);
851
+ const startedCoordinatorState = (await ctx.session.state(coordinator)).state;
852
+ if (startedCoordinatorState.response?.activeRequestId !== requestId || startedCoordinatorState.response.generation?.generationId !== generationId) return;
853
+ const promptCoordinatorState = {
854
+ ...coordinatorStateAt(history),
855
+ queued: queuedMessagesAt(history)
856
+ };
857
+ let messages = activeContextMessages(state, promptCoordinatorState);
328
858
  if (options.compaction) {
329
859
  const compactionContext = {
330
860
  ...resolverContext,
@@ -332,7 +862,7 @@ function createHandlers(options) {
332
862
  };
333
863
  if (await options.compaction.shouldCompact(compactionContext)) {
334
864
  const throughMessageId = messages.at(-1)?.id ?? request.messageId;
335
- await ctx.append({
865
+ await ctx.session.append("compaction-requested", {
336
866
  type: "ai.compaction.requested",
337
867
  id: `${generationId}:compaction:requested`,
338
868
  payload: {
@@ -344,9 +874,10 @@ function createHandlers(options) {
344
874
  const completed = {
345
875
  generationId,
346
876
  throughMessageId,
347
- messages
877
+ messages,
878
+ ...promptCoordinatorState.queued.length === 0 ? {} : { retainedMessageIds: promptCoordinatorState.queued.map((item) => item.messageId) }
348
879
  };
349
- await ctx.append({
880
+ await ctx.session.append("compaction-completed", {
350
881
  type: "ai.compaction.completed",
351
882
  id: `${generationId}:compaction:completed`,
352
883
  payload: completed
@@ -360,6 +891,11 @@ function createHandlers(options) {
360
891
  };
361
892
  }
362
893
  }
894
+ const currentState = (await ctx.session.state(options.agent.reducer)).state;
895
+ const currentCoordinatorState = (await ctx.session.state(coordinator)).state;
896
+ const generationMessages = activeContextMessages(currentState, currentCoordinatorState);
897
+ promptCache.set(promptCacheKey(ctx.event.sessionId, generationId), convertToModelMessages(generationMessages, { tools }));
898
+ let pendingToolCalls = [];
363
899
  try {
364
900
  const resolvedInstructions = options.instructions === void 0 ? void 0 : await resolve(options.instructions, resolverContext);
365
901
  const generateContext = {
@@ -367,15 +903,16 @@ function createHandlers(options) {
367
903
  requestId,
368
904
  generationId,
369
905
  responseMessageId,
370
- messages,
371
- state,
906
+ messages: generationMessages,
907
+ state: currentState,
372
908
  history,
373
909
  signal: ctx.signal,
374
910
  model: resolvedModel,
375
- tools: options.tools ?? {},
911
+ tools,
376
912
  ...resolvedInstructions === void 0 ? {} : { instructions: resolvedInstructions },
377
913
  generation: options.generation ?? {}
378
914
  };
915
+ const custom = options.generate !== void 0;
379
916
  const updates = consumeGeneration({
380
917
  source: options.generate === void 0 ? await generateWithAISDK(generateContext) : { stream: await options.generate(generateContext) },
381
918
  ...options.progress === void 0 ? {} : { progress: options.progress }
@@ -396,18 +933,23 @@ function createHandlers(options) {
396
933
  sequence,
397
934
  chunks: update.chunks
398
935
  };
399
- const extras = lifecycleEvents({
936
+ const lifecycle = lifecycleEvents({
400
937
  requestId,
401
938
  messageId: responseMessageId,
402
939
  generationId,
403
940
  sequence,
404
- chunks: update.chunks
941
+ chunks: update.chunks,
942
+ pending: pendingToolCalls,
943
+ tools,
944
+ generation,
945
+ custom
405
946
  });
406
- await ctx.append({
947
+ pendingToolCalls = lifecycle.pending;
948
+ await ctx.session.append(`generation-progress:${sequence}`, {
407
949
  type: "ai.generation.progress",
408
950
  id: `${generationId}:progress:${sequence}`,
409
951
  payload: progress
410
- }, ...extras);
952
+ }, ...lifecycle.events);
411
953
  sequence += 1;
412
954
  }
413
955
  if (!finish) throw new Error("agent generation finished without output");
@@ -419,29 +961,37 @@ function createHandlers(options) {
419
961
  ...finish.finishReason === void 0 ? {} : { finishReason: finish.finishReason },
420
962
  ...finish.usage === void 0 ? {} : { usage: finish.usage }
421
963
  };
422
- const messageCompleted = { messageId: responseMessageId };
423
- await ctx.append({
964
+ const completionEvent = {
424
965
  type: "ai.generation.completed",
425
966
  id: `${generationId}:completed`,
426
967
  payload: completed
427
- }, {
428
- type: "ai.message.completed",
429
- id: `${generationId}:message:completed`,
430
- payload: messageCompleted
431
- });
968
+ };
969
+ const unresolved = options.generate ? pendingToolCalls.filter((pending) => pending.classification !== "automatic") : [];
970
+ if (unresolved.length > 0) throw new Error(`generation ended with unresolved tool authorization for '${unresolved[0].call.toolName}'`);
971
+ const pendingEvents = pendingToolCalls.filter((pending) => pending.classification === "automatic" || options.generate === void 0 && pending.classification === "unknown").map((pending) => toolCalledEvent(pending.call));
972
+ if (finish.finishReason === "tool-calls") return [...pendingEvents, completionEvent];
973
+ return [
974
+ ...pendingEvents,
975
+ completionEvent,
976
+ {
977
+ type: "ai.message.completed",
978
+ id: `${generationId}:message:completed`,
979
+ payload: { messageId: responseMessageId }
980
+ }
981
+ ];
432
982
  } catch (error) {
983
+ if (error instanceof A2Error) throw error;
433
984
  if (ctx.signal.aborted) {
434
985
  const interrupted = {
435
986
  messageId: responseMessageId,
436
987
  generationId,
437
988
  reason: "aborted"
438
989
  };
439
- await ctx.append({
990
+ return {
440
991
  type: "ai.message.interrupted",
441
992
  id: `${generationId}:interrupted`,
442
993
  payload: interrupted
443
- });
444
- return;
994
+ };
445
995
  }
446
996
  const failed = {
447
997
  requestId,
@@ -450,33 +1000,116 @@ function createHandlers(options) {
450
1000
  responseMessageId,
451
1001
  error: errorMessage(error)
452
1002
  };
453
- await ctx.append({
1003
+ return {
454
1004
  type: "ai.generation.failed",
455
1005
  id: `${generationId}:failed`,
456
1006
  payload: failed
457
- });
1007
+ };
458
1008
  }
459
1009
  };
460
- return { "ai.generation.requested": {
461
- abortOn: {
462
- "ai.message.interrupted": (event, trigger) => {
463
- const responseMessageId = trigger.payload.responseMessageId ?? (trigger.payload.reason === "message" ? `${trigger.payload.messageId}:assistant` : trigger.payload.messageId);
464
- return event.payload.messageId === responseMessageId;
1010
+ const handleMessageCreated = async (ctx) => {
1011
+ if (ctx.event.payload.message.role !== "user") return;
1012
+ return scheduleNext(ctx);
1013
+ };
1014
+ const handleRetry = async (ctx) => {
1015
+ const response = (await ctx.session.state(coordinator)).state.response;
1016
+ if (response?.status !== "failed" || response.rootMessageId !== ctx.event.payload.messageId || response.responseMessageId !== ctx.event.payload.responseMessageId) return;
1017
+ return {
1018
+ type: "ai.generation.requested",
1019
+ id: `ai.generate:retry:${ctx.event.payload.retryId}`,
1020
+ payload: {
1021
+ messageId: response.rootMessageId,
1022
+ responseMessageId: response.responseMessageId,
1023
+ reason: "retry"
1024
+ }
1025
+ };
1026
+ };
1027
+ const handleInputResponse = async (ctx) => {
1028
+ const response = (await ctx.session.state(coordinator)).state.response;
1029
+ if (response?.responseMessageId !== ctx.event.payload.messageId || response.inputResponse?.index !== ctx.event.index || response.inputResponse.generationId !== ctx.event.payload.generationId || response.inputResponse.inputId !== ctx.event.payload.inputId) return;
1030
+ await continueIfReady(ctx, ctx.event.payload.generationId);
1031
+ };
1032
+ const clearPromptCache = (sessionId) => {
1033
+ const prefix = `${sessionId}\u001f`;
1034
+ for (const key of promptCache.keys()) if (key.startsWith(prefix)) promptCache.delete(key);
1035
+ };
1036
+ const handleResponseEnded = async (ctx) => {
1037
+ clearPromptCache(ctx.event.sessionId);
1038
+ return scheduleNext(ctx);
1039
+ };
1040
+ return {
1041
+ "ai.message.created": {
1042
+ lane: "a2.ai.turn",
1043
+ handler: handleMessageCreated
1044
+ },
1045
+ "ai.retry.requested": {
1046
+ lane: "a2.ai.turn",
1047
+ handler: handleRetry
1048
+ },
1049
+ "ai.input.responded": {
1050
+ lane: "a2.ai.turn",
1051
+ handler: handleInputResponse
1052
+ },
1053
+ "ai.message.completed": {
1054
+ lane: "a2.ai.turn",
1055
+ handler: handleResponseEnded
1056
+ },
1057
+ "ai.message.interrupted": {
1058
+ lane: "a2.ai.turn",
1059
+ handler: handleResponseEnded
1060
+ },
1061
+ "ai.session.closed": { handler: (ctx) => {
1062
+ clearPromptCache(ctx.event.sessionId);
1063
+ return Promise.resolve();
1064
+ } },
1065
+ "ai.generation.failed": { handler: (ctx) => {
1066
+ promptCache.delete(promptCacheKey(ctx.event.sessionId, ctx.event.payload.generationId));
1067
+ return Promise.resolve();
1068
+ } },
1069
+ "ai.generation.requested": {
1070
+ lane: "a2.ai.turn",
1071
+ abortOn: {
1072
+ "ai.message.interrupted": (event, trigger, context) => {
1073
+ const responseMessageId = trigger.payload.responseMessageId ?? (trigger.payload.reason === "message" ? `${trigger.payload.messageId}:assistant` : trigger.payload.messageId);
1074
+ return event.payload.messageId === responseMessageId && (event.payload.generationId === void 0 || event.payload.generationId === `${trigger.id}:generation:${context.attempt}`);
1075
+ },
1076
+ "ai.session.closed": true
465
1077
  },
466
- "ai.session.closed": true
1078
+ handler: generationHandler
467
1079
  },
468
- handler: generationHandler
469
- } };
1080
+ "ai.generation.completed": { handler: async (ctx) => continueIfReady(ctx, ctx.event.payload.generationId) },
1081
+ "ai.tool.called": {
1082
+ abortOn: {
1083
+ "ai.generation.failed": (event, trigger) => event.payload.generationId === trigger.payload.generationId,
1084
+ "ai.message.interrupted": (event, trigger) => event.payload.messageId === trigger.payload.messageId && (event.payload.generationId === void 0 || event.payload.generationId === trigger.payload.generationId),
1085
+ "ai.session.closed": true
1086
+ },
1087
+ handler: handleToolCall
1088
+ },
1089
+ "ai.approval.responded": {
1090
+ abortOn: {
1091
+ "ai.generation.failed": (event, trigger) => event.payload.responseMessageId === trigger.payload.messageId && event.payload.generationId === trigger.payload.generationId,
1092
+ "ai.message.interrupted": (event, trigger) => event.payload.messageId === trigger.payload.messageId && (event.payload.generationId === void 0 || event.payload.generationId === trigger.payload.generationId),
1093
+ "ai.session.closed": true
1094
+ },
1095
+ handler: handleApproval
1096
+ },
1097
+ "ai.tool.result": { handler: async (ctx) => {
1098
+ if (ctx.event.payload.preliminary === true) return;
1099
+ await continueIfReady(ctx, ctx.event.payload.generationId);
1100
+ } }
1101
+ };
470
1102
  }
471
1103
  /** Assemble an A2 server with the built-in agent handlers and app extensions. */
472
1104
  function createAgentServer(options) {
473
- const { agent: definition, model, tools, instructions, generation, generate, compaction, progress, handlers, ...serverOptions } = options;
1105
+ const { agent: definition, model, tools, instructions, generation, maxSteps, generate, compaction, progress, handlers, ...serverOptions } = options;
474
1106
  const builtIns = createHandlers({
475
1107
  agent: definition,
476
1108
  model,
477
1109
  ...tools === void 0 ? {} : { tools },
478
1110
  ...instructions === void 0 ? {} : { instructions },
479
1111
  ...generation === void 0 ? {} : { generation },
1112
+ ...maxSteps === void 0 ? {} : { maxSteps },
480
1113
  ...generate === void 0 ? {} : { generate },
481
1114
  ...compaction === void 0 ? {} : { compaction },
482
1115
  ...progress === void 0 ? {} : { progress }
@@ -487,8 +1120,9 @@ function createAgentServer(options) {
487
1120
  handlers: {
488
1121
  ...builtIns,
489
1122
  ...handlers
490
- }
1123
+ },
1124
+ validatePush: validateAgentPush
491
1125
  });
492
1126
  }
493
1127
  //#endregion
494
- export { createAgentServer, createHandlers };
1128
+ export { createAgentServer, createHandlers, validateAgentPush };