experimental-a2 0.11.0 → 0.13.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 (46) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/{actor-D_54lz_1.d.ts → actor-DJi3RsNu.d.ts} +2 -2
  3. package/dist/{actor-D_54lz_1.d.ts.map → actor-DJi3RsNu.d.ts.map} +1 -1
  4. package/dist/actor-client.d.ts +1 -1
  5. package/dist/actor-react.d.ts +1 -1
  6. package/dist/actor.d.ts +1 -1
  7. package/dist/actor.js +1 -1
  8. package/dist/ai-server.d.ts +12 -4
  9. package/dist/ai-server.d.ts.map +1 -1
  10. package/dist/ai-server.js +477 -143
  11. package/dist/ai-server.js.map +1 -1
  12. package/dist/ai.d.ts +33 -2
  13. package/dist/ai.d.ts.map +1 -1
  14. package/dist/ai.js +39 -7
  15. package/dist/ai.js.map +1 -1
  16. package/dist/scheduler-qstash.d.ts +1 -1
  17. package/dist/scheduler-qstash.js +1 -1
  18. package/dist/scheduler-vercel.d.ts +1 -1
  19. package/dist/scheduler-vercel.js +1 -1
  20. package/dist/{server-Dkz2a84E.js → server-BeNADlCI.js} +8 -5
  21. package/dist/server-BeNADlCI.js.map +1 -0
  22. package/dist/{server-DwPrMqHB.d.ts → server-DjZZa1wr.d.ts} +8 -2
  23. package/dist/{server-DwPrMqHB.d.ts.map → server-DjZZa1wr.d.ts.map} +1 -1
  24. package/dist/server.d.ts +2 -2
  25. package/dist/server.js +2 -2
  26. package/docs/guides/06-ai-agents.mdx +100 -18
  27. package/docs/reference/01-api.mdx +105 -5
  28. package/examples/playground/app/agent/[agentId]/agent-client.tsx +31 -16
  29. package/examples/playground/app/agent/[agentId]/compaction/route.ts +14 -0
  30. package/examples/playground/app/agent/[agentId]/compaction-event.tsx +38 -0
  31. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +294 -0
  32. package/examples/playground/app/agent/compaction-settings.test.ts +128 -0
  33. package/examples/playground/app/agent/compaction-settings.ts +49 -0
  34. package/examples/playground/app/agent/compaction-timeline.test.ts +337 -0
  35. package/examples/playground/app/agent/compaction-timeline.ts +198 -0
  36. package/examples/playground/app/agent/model.ts +47 -1
  37. package/examples/playground/app/agent/server.ts +3 -2
  38. package/examples/playground/app/globals.css +154 -0
  39. package/examples/playground/package.json +1 -1
  40. package/package.json +1 -1
  41. package/src/ai-model-metadata.ts +108 -0
  42. package/src/ai-sdk-step.ts +5 -1
  43. package/src/ai-server.ts +712 -203
  44. package/src/ai.ts +99 -4
  45. package/src/server.ts +14 -3
  46. package/dist/server-Dkz2a84E.js.map +0 -1
package/dist/ai-server.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { t as A2Error } from "./errors-DCk6ch5n.js";
2
2
  import { f as consumeSchedulerSendFailure, p as installAmbientToolScopeStorage } from "./internal-Dq2qYxou.js";
3
- import { r as setServerFetchHooks, t as createServer } from "./server-Dkz2a84E.js";
3
+ import { i as setServerFetchHooks, t as createServer } from "./server-BeNADlCI.js";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
- import { convertToModelMessages, stepCountIs, streamText, toUIMessageStream } from "ai";
5
+ import { asSchema, convertToModelMessages, stepCountIs, streamText, toUIMessageStream } from "ai";
6
6
  //#region src/ai-sdk-step.ts
7
7
  const CONTROLLED_SETTINGS = [
8
8
  "_internal",
@@ -45,7 +45,7 @@ const errorMessage$1 = (error) => error instanceof Error ? error.message : Strin
45
45
  async function generateAISDKStep(input) {
46
46
  const settings = safeSettings(input.settings);
47
47
  const tools = modelToolSet(input.tools);
48
- const messages = await convertToModelMessages(input.messages, { tools });
48
+ const messages = input.modelMessages ?? await convertToModelMessages(input.messages, { tools });
49
49
  const streamOptions = {
50
50
  ...settings,
51
51
  model: input.model,
@@ -85,6 +85,66 @@ async function generateAISDKStep(input) {
85
85
  };
86
86
  }
87
87
  //#endregion
88
+ //#region src/ai-model-metadata.ts
89
+ const catalogUrl = "https://ai-gateway.vercel.sh/v1/models";
90
+ const catalogLifetimeMs = 36e5;
91
+ const catalogTimeoutMs = 5e3;
92
+ let catalog;
93
+ const gatewayModelId = (model) => {
94
+ if (typeof model === "string") return globalThis.AI_SDK_DEFAULT_PROVIDER === void 0 ? model : void 0;
95
+ return model.provider === "gateway" ? model.modelId : void 0;
96
+ };
97
+ const readCatalog = () => {
98
+ if (catalog && catalog.expiresAt > Date.now()) return catalog.promise;
99
+ const entry = {
100
+ expiresAt: Number.POSITIVE_INFINITY,
101
+ promise: (async () => {
102
+ const controller = new AbortController();
103
+ const timeout = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("model catalog request timed out")), catalogTimeoutMs);
104
+ timeout.unref?.();
105
+ try {
106
+ const response = await fetch(catalogUrl, { signal: controller.signal });
107
+ if (!response.ok) throw new Error(`model catalog request failed: HTTP ${response.status}`);
108
+ const body = await response.json();
109
+ if (typeof body !== "object" || body === null || !("data" in body) || !Array.isArray(body.data)) throw new Error("model catalog response must contain a data array");
110
+ const models = /* @__PURE__ */ new Map();
111
+ for (const row of body.data) {
112
+ if (typeof row !== "object" || row === null || !("type" in row) || !("id" in row) || !("context_window" in row) || !("max_tokens" in row) || row.type !== "language" || typeof row.id !== "string" || typeof row.context_window !== "number" || typeof row.max_tokens !== "number" || !Number.isSafeInteger(row.context_window) || row.context_window <= 0 || !Number.isSafeInteger(row.max_tokens) || row.max_tokens <= 0) continue;
113
+ models.set(row.id, {
114
+ contextWindow: row.context_window,
115
+ maxOutputTokens: row.max_tokens
116
+ });
117
+ }
118
+ return models;
119
+ } finally {
120
+ clearTimeout(timeout);
121
+ }
122
+ })()
123
+ };
124
+ catalog = entry;
125
+ entry.promise = entry.promise.then((models) => {
126
+ entry.expiresAt = Date.now() + catalogLifetimeMs;
127
+ return models;
128
+ }, (error) => {
129
+ if (catalog === entry) catalog = void 0;
130
+ throw error;
131
+ });
132
+ return entry.promise;
133
+ };
134
+ const readModelLimits = async (options) => {
135
+ options.signal.throwIfAborted();
136
+ let onAbort;
137
+ const aborted = new Promise((_resolve, reject) => {
138
+ onAbort = () => reject(options.signal.reason);
139
+ options.signal.addEventListener("abort", onAbort, { once: true });
140
+ });
141
+ try {
142
+ return (await Promise.race([readCatalog(), aborted])).get(options.modelId) ?? null;
143
+ } finally {
144
+ options.signal.removeEventListener("abort", onAbort);
145
+ }
146
+ };
147
+ //#endregion
88
148
  //#region src/ai-coordinator.ts
89
149
  const updateResponse = (state, update) => state.response === void 0 ? state : {
90
150
  ...state,
@@ -286,7 +346,23 @@ function validateAgentIngress(context) {
286
346
  });
287
347
  if (rejected !== void 0) throw new A2Error("INVALID_PAYLOAD", `event '${rejected.type}' is server-only for an AI agent`);
288
348
  }
289
- const resolve = async (value, context) => typeof value === "function" ? await value(context) : value;
349
+ const checkAbort = (signal) => {
350
+ if (!signal.aborted) return false;
351
+ const reason = signal.reason;
352
+ if (reason instanceof A2Error && (reason.code === "CLAIM_EXPIRED" || reason.code === "SUPERSEDED_ATTEMPT")) throw reason;
353
+ return true;
354
+ };
355
+ const resolve = async (value, context) => {
356
+ let result;
357
+ try {
358
+ result = typeof value === "function" ? await value(context) : value;
359
+ } catch (error) {
360
+ checkAbort(context.signal);
361
+ throw error;
362
+ }
363
+ checkAbort(context.signal);
364
+ return result;
365
+ };
290
366
  const modelName = (model) => {
291
367
  if (typeof model === "string") return model;
292
368
  if ("modelId" in model && typeof model.modelId === "string") return model.modelId;
@@ -325,6 +401,7 @@ async function generateWithAISDK(context, messageMetadata) {
325
401
  model: context.model,
326
402
  tools: context.tools,
327
403
  messages: context.messages,
404
+ modelMessages: context.modelMessages,
328
405
  responseMessageId: context.responseMessageId,
329
406
  abortSignal: context.signal,
330
407
  ...context.instructions === void 0 ? {} : { instructions: context.instructions },
@@ -346,8 +423,8 @@ async function* consumeGeneration(options) {
346
423
  const maxChunks = options.progress?.maxChunks ?? 16;
347
424
  const maxDelayMs = options.progress?.maxDelayMs ?? 30;
348
425
  let lastFlush = Date.now();
349
- const chunks = options.source.stream[Symbol.asyncIterator]();
350
- let pendingChunk = chunks.next();
426
+ const reader = options.source.stream.getReader();
427
+ let pendingChunk = reader.read();
351
428
  try {
352
429
  for (;;) {
353
430
  let result;
@@ -369,7 +446,7 @@ async function* consumeGeneration(options) {
369
446
  pending.push(chunk);
370
447
  if (chunk.type === "finish") streamedFinishReason = chunk.finishReason;
371
448
  if (chunk.type === "error") streamedError = chunk.errorText;
372
- pendingChunk = chunks.next();
449
+ pendingChunk = reader.read();
373
450
  if (pending.length >= maxChunks || isBoundaryChunk(pending.at(-1))) {
374
451
  yield {
375
452
  type: "progress",
@@ -384,6 +461,13 @@ async function* consumeGeneration(options) {
384
461
  chunks: pending.splice(0)
385
462
  };
386
463
  throw error;
464
+ } finally {
465
+ pendingChunk.catch(() => {});
466
+ try {
467
+ await reader.cancel();
468
+ } catch {} finally {
469
+ reader.releaseLock();
470
+ }
387
471
  }
388
472
  let completion;
389
473
  try {
@@ -412,20 +496,131 @@ const replay = (definition, history) => {
412
496
  for (const event of history) state = definition.reducer.fold(state, event);
413
497
  return state;
414
498
  };
415
- const contextMessages = (state) => {
499
+ const summarize = async (options) => {
500
+ const instruction = [
501
+ "Summarize this conversation so you can continue the task after the earlier context is replaced by your summary.",
502
+ "Preserve the objective, constraints, decisions, exact identifiers, completed actions and their results, unresolved questions, and next steps. Distinguish observations from assumptions.",
503
+ "Respond only with the summary as text. Do not call any tools or continue working on the task.",
504
+ options.instructions
505
+ ].filter(Boolean).join("\n");
506
+ const generation = { ...options.context.generation };
507
+ for (const key of Object.keys(generation)) if (key.startsWith("on") || key.startsWith("experimental_on") || [
508
+ "repairToolCall",
509
+ "experimental_repairToolCall",
510
+ "experimental_refineToolInput",
511
+ "experimental_transform",
512
+ "toolApproval"
513
+ ].includes(key)) Reflect.deleteProperty(generation, key);
514
+ const tools = Object.fromEntries(Object.entries(options.context.tools).map(([name, tool]) => {
515
+ const definition = {
516
+ ...tool,
517
+ needsApproval: false
518
+ };
519
+ for (const key of [
520
+ "execute",
521
+ "onInputStart",
522
+ "onInputDelta",
523
+ "onInputAvailable"
524
+ ]) Reflect.deleteProperty(definition, key);
525
+ return [name, definition];
526
+ }));
527
+ const source = await generateWithAISDK({
528
+ ...options.context,
529
+ generation,
530
+ tools,
531
+ modelMessages: [...options.context.modelMessages, {
532
+ role: "user",
533
+ content: instruction
534
+ }],
535
+ responseMessageId: `${options.context.generationId}:summary`
536
+ }, void 0);
537
+ let summary = "";
538
+ let calledTool = false;
539
+ let finish;
540
+ for await (const update of consumeGeneration({ source })) {
541
+ options.context.signal.throwIfAborted();
542
+ if (update.type === "finish") {
543
+ finish = update;
544
+ continue;
545
+ }
546
+ for (const chunk of update.chunks) {
547
+ if (chunk.type === "text-delta") summary += chunk.delta;
548
+ if (chunk.type.startsWith("tool-")) calledTool = true;
549
+ }
550
+ }
551
+ if (calledTool || summary.trim().length === 0 || finish?.finishReason !== "stop") throw new Error("compaction must produce a complete text summary without calling tools");
552
+ return {
553
+ summary,
554
+ ...finish.usage === void 0 ? {} : { usage: finish.usage }
555
+ };
556
+ };
557
+ const activeCompaction = (state) => {
416
558
  const compaction = state.compaction;
417
- if (compaction?.status !== "completed" || !compaction.messages) return state.messages;
418
- const boundary = state.messages.findIndex((message) => message.id === compaction.throughMessageId);
559
+ return compaction?.status === "completed" && compaction.messages !== void 0 && state.messages.some((message) => message.id === compaction.throughMessageId) ? compaction : null;
560
+ };
561
+ const contextMessages = (options) => {
562
+ const { agent, state, history } = options;
563
+ const compaction = activeCompaction(state);
564
+ if (compaction === null) return state.messages;
419
565
  const retained = new Set(compaction.retainedMessageIds ?? []);
420
- return boundary === -1 ? state.messages : [
566
+ const startedIndex = history.find((event) => event.type === "ai.generation.started" && event.payload.generationId === compaction.generationId)?.index;
567
+ const throughIndex = compaction.throughIndex ?? (startedIndex === void 0 ? void 0 : startedIndex - 1);
568
+ if (throughIndex !== void 0) {
569
+ const prefix = history.filter((event) => event.index <= throughIndex);
570
+ for (const queued of queuedMessagesAt(prefix)) retained.add(queued.messageId);
571
+ let context = replay(agent, prefix);
572
+ context = {
573
+ ...context,
574
+ messages: [...compaction.messages, ...context.messages.filter((message) => retained.has(message.id))],
575
+ activeProjection: null
576
+ };
577
+ for (const event of history) if (event.index > throughIndex) context = agent.reducer.fold(context, event);
578
+ return context.messages;
579
+ }
580
+ const boundary = state.messages.findIndex((message) => message.id === compaction.throughMessageId);
581
+ return [
421
582
  ...compaction.messages,
422
583
  ...state.messages.slice(0, boundary + 1).filter((message) => retained.has(message.id)),
423
584
  ...state.messages.slice(boundary + 1)
424
585
  ];
425
586
  };
426
- const activeContextMessages = (state, coordinator) => {
427
- const queued = new Set(coordinator.queued.map((item) => item.messageId));
428
- return contextMessages(state).filter((message) => !queued.has(message.id));
587
+ const activeContextMessages = (options) => {
588
+ const queued = new Set(options.coordinator.queued.map((item) => item.messageId));
589
+ return contextMessages(options).filter((message) => !queued.has(message.id));
590
+ };
591
+ const modelContext = async (options) => {
592
+ const messages = await convertToModelMessages(options.messages, { tools: options.tools });
593
+ const summary = activeCompaction(options.state)?.summary;
594
+ return summary === void 0 ? messages : [{
595
+ role: "user",
596
+ content: summary
597
+ }, ...messages];
598
+ };
599
+ const estimateInputTokens = async (options) => {
600
+ const tools = await Promise.all(Object.entries(options.tools).map(async ([name, tool]) => ({
601
+ name,
602
+ description: tool.description,
603
+ inputSchema: await asSchema(tool.inputSchema).jsonSchema
604
+ })));
605
+ return Math.ceil(new TextEncoder().encode(JSON.stringify({
606
+ messages: options.messages,
607
+ instructions: options.instructions,
608
+ tools
609
+ })).byteLength / 4);
610
+ };
611
+ const measuredInputTokens = (options) => {
612
+ for (const event of options.history.toReversed()) {
613
+ if (event.type === "ai.compaction.completed" || event.type === "ai.retry.requested") break;
614
+ if (event.type !== "ai.generation.completed") continue;
615
+ const completed = event.payload;
616
+ const started = options.history.find((candidate) => candidate.type === "ai.generation.started" && candidate.payload.generationId === completed.generationId);
617
+ const input = completed.usage?.inputTokens;
618
+ const estimate = completed.inputTokenEstimate;
619
+ if ((started?.payload)?.model !== options.model) break;
620
+ if (input === void 0 || estimate === void 0 || !Number.isFinite(input) || input < 0 || options.estimate < estimate) break;
621
+ return Math.max(options.estimate, Math.ceil(input + options.estimate - estimate));
622
+ }
623
+ return options.estimate;
429
624
  };
430
625
  const generationRequestId = (generationId) => {
431
626
  const markerIndex = generationId.lastIndexOf(":generation:");
@@ -639,6 +834,24 @@ function withoutGenerationLifecycle(history, generationIds) {
639
834
  return true;
640
835
  });
641
836
  }
837
+ const validateCompaction = (options) => {
838
+ let compaction = options.compaction;
839
+ if (compaction !== false && (typeof compaction !== "object" || compaction === null)) throw new TypeError("compaction must resolve to false or a policy object");
840
+ if (compaction !== false && "then" in compaction) {
841
+ Promise.resolve(compaction).catch(() => {});
842
+ throw new TypeError("compaction options must resolve synchronously");
843
+ }
844
+ if (compaction !== false && !("shouldCompact" in compaction)) {
845
+ if (compaction.thresholdTokens !== void 0 && (!Number.isSafeInteger(compaction.thresholdTokens) || compaction.thresholdTokens < 1)) throw new TypeError("compaction.thresholdTokens must be a positive safe integer");
846
+ const choice = options.generation?.toolChoice;
847
+ const unsupported = options.generation?.output !== void 0 ? "structured output" : choice !== void 0 && choice !== "auto" && choice !== "none" ? "forced tool choice" : Object.values(options.tools ?? {}).some((tool) => tool.type === "provider" && tool.isProviderExecuted === true) ? "provider-executed tools" : void 0;
848
+ if (unsupported !== void 0) {
849
+ if (options.explicit) throw new TypeError(`automatic compaction does not support ${unsupported}; use a custom policy or compaction: false`);
850
+ compaction = false;
851
+ }
852
+ }
853
+ return compaction;
854
+ };
642
855
  /**
643
856
  * Build the ordinary A2 handler table for the built-in agent protocol.
644
857
  * Application handlers can be spread beside this table.
@@ -648,6 +861,13 @@ function createHandlers(options) {
648
861
  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");
649
862
  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");
650
863
  if (options.maxSteps !== void 0 && options.maxSteps !== Number.POSITIVE_INFINITY && (!Number.isInteger(options.maxSteps) || options.maxSteps < 1)) throw new TypeError("maxSteps must be a positive integer or Infinity");
864
+ const configuredCompaction = options.compaction ?? (options.generate === void 0 ? {} : false);
865
+ const staticCompaction = typeof configuredCompaction === "function" ? void 0 : validateCompaction({
866
+ compaction: configuredCompaction,
867
+ explicit: options.compaction !== void 0,
868
+ generation: options.generation,
869
+ tools: options.tools
870
+ });
651
871
  const tools = options.tools ?? {};
652
872
  const generation = options.generation ?? {};
653
873
  const maxSteps = options.maxSteps ?? Number.POSITIVE_INFINITY;
@@ -729,15 +949,20 @@ function createHandlers(options) {
729
949
  if (cached) return cached;
730
950
  const computation = (async () => {
731
951
  const history = await readHistory();
732
- const coordinatorState = coordinatorStateAt(history);
733
- const compaction = history.find((event) => event.type === "ai.compaction.completed" && event.payload.generationId === call.generationId);
734
- if (compaction !== void 0) {
735
- const messages = compaction.payload.messages.filter((message) => !coordinatorState.queued.some((queued) => queued.messageId === message.id));
736
- return convertToModelMessages(messages, { tools });
737
- }
738
- const frontier = history.find((event) => event.type === "ai.generation.started" && event.payload.generationId === call.generationId)?.index ?? Number.POSITIVE_INFINITY;
739
- const state = replay(options.agent, history.filter((event) => event.index < frontier));
740
- return convertToModelMessages(activeContextMessages(state, coordinatorState), { tools });
952
+ const started = history.find((event) => event.type === "ai.generation.started" && event.payload.generationId === call.generationId);
953
+ const frontier = (started?.payload)?.promptThroughIndex ?? (started?.index ?? Number.POSITIVE_INFINITY) - 1;
954
+ const promptHistory = history.filter((event) => event.index <= frontier || (event.type === "ai.generation.started" || event.type === "ai.compaction.requested" || event.type === "ai.compaction.completed") && event.payload.generationId === call.generationId);
955
+ const state = replay(options.agent, promptHistory);
956
+ return modelContext({
957
+ messages: activeContextMessages({
958
+ agent: options.agent,
959
+ state,
960
+ history: promptHistory,
961
+ coordinator: coordinatorStateAt(promptHistory)
962
+ }),
963
+ state,
964
+ tools
965
+ });
741
966
  })();
742
967
  promptCache.set(key, computation);
743
968
  computation.catch(() => {
@@ -747,7 +972,7 @@ function createHandlers(options) {
747
972
  };
748
973
  const toolExecutionFailure = (ctx, call, error) => {
749
974
  const schedulerFailure = consumeSchedulerSendFailure(error);
750
- if (ctx.signal.aborted) return;
975
+ if (checkAbort(ctx.signal)) return;
751
976
  if (schedulerFailure === "retryable") throw error;
752
977
  return resultEvent(call, "execution:error", { error: errorMessage(error) });
753
978
  };
@@ -771,33 +996,31 @@ function createHandlers(options) {
771
996
  if (iterator === void 0) return resultEvent(call, "execution:0", { output });
772
997
  let last;
773
998
  let sequence = 0;
774
- for (;;) {
775
- let result;
776
- try {
777
- result = await iterator.next();
778
- } catch (error) {
779
- return toolExecutionFailure(ctx, call, error);
780
- }
781
- if (result.done) break;
782
- if (ctx.signal.aborted) {
999
+ let done = false;
1000
+ try {
1001
+ for (;;) {
1002
+ let result;
783
1003
  try {
784
- await iterator.return?.();
785
- } catch {}
786
- return;
787
- }
788
- last = result.value;
789
- try {
790
- await ctx.session.append(`tool:${call.toolCallId}:preliminary:${sequence}`, resultEvent(call, `execution:${sequence}:preliminary`, {
1004
+ result = await iterator.next();
1005
+ } catch (error) {
1006
+ return toolExecutionFailure(ctx, call, error);
1007
+ }
1008
+ if (checkAbort(ctx.signal)) return;
1009
+ if (result.done) {
1010
+ done = true;
1011
+ break;
1012
+ }
1013
+ last = result.value;
1014
+ await ctx.session.append(`tool:${call.toolCallId}:preliminary:${sequence}`, resultEvent(call, `execution:${ctx.attempt}:${sequence}:preliminary`, {
791
1015
  output: result.value,
792
1016
  preliminary: true
793
1017
  }));
794
- } catch (error) {
795
- try {
796
- await iterator.return?.();
797
- } catch {}
798
- throw error;
1018
+ sequence += 1;
799
1019
  }
800
- sequence += 1;
1020
+ } finally {
1021
+ if (!done) try {
1022
+ await iterator.return?.();
1023
+ } catch {}
801
1024
  }
802
1025
  return resultEvent(call, `execution:${sequence}:final`, sequence === 0 ? {} : { output: last });
803
1026
  };
@@ -805,7 +1028,7 @@ function createHandlers(options) {
805
1028
  const execute = tools[call.toolName]?.execute;
806
1029
  if (execute === void 0) return resultEvent(call, "execution:error", { error: `Tool '${call.toolName}' has no server executor` });
807
1030
  const messages = await promptMessages(ctx.event.sessionId, call, ctx.session.history);
808
- if (ctx.signal.aborted) return;
1031
+ if (checkAbort(ctx.signal)) return;
809
1032
  const scope = {
810
1033
  contract: options.agent.contract,
811
1034
  context: ctx
@@ -863,120 +1086,211 @@ function createHandlers(options) {
863
1086
  if (incompleteId !== void 0) replacedGenerationIds.add(incompleteId);
864
1087
  if (request.reason === "retry" && sourceCoordinatorState.response?.failure?.generationId !== void 0) replacedGenerationIds.add(sourceCoordinatorState.response.failure.generationId);
865
1088
  const promptHistory = withoutGenerationLifecycle(history, replacedGenerationIds);
866
- let state = replay(options.agent, promptHistory);
1089
+ const state = replay(options.agent, promptHistory);
867
1090
  const resolverContext = {
868
1091
  event: ctx.event,
869
1092
  state,
870
1093
  history,
871
1094
  signal: ctx.signal
872
1095
  };
873
- const resolvedModel = await resolve(options.model, resolverContext);
874
- if (resolvedModel === void 0) throw new TypeError("the model resolver returned undefined");
875
- if (ctx.signal.aborted) return;
876
- if (request.reason === "tool" && responseStepCount >= maxSteps) {
877
- const source = sourceCoordinatorState.response?.generation;
878
- if (source === void 0) return;
879
- return {
880
- type: "ai.generation.failed",
881
- id: `${requestId}:step-limit`,
882
- payload: {
883
- requestId: source.requestId,
884
- messageId: source.messageId,
885
- generationId: source.generationId,
886
- responseMessageId,
887
- error: `agent exceeded the ${maxSteps}-step limit`,
888
- stepLimit: true
889
- }
890
- };
891
- }
892
- const started = {
893
- requestId,
894
- messageId: request.messageId,
895
- generationId,
896
- responseMessageId,
897
- attempt,
898
- model: modelName(resolvedModel)
899
- };
900
- const startEvents = [];
901
- if (previous) {
902
- const payload = previous.payload;
903
- const superseded = {
1096
+ let generationStarted = false;
1097
+ try {
1098
+ const resolvedModel = await resolve(options.model, resolverContext);
1099
+ if (resolvedModel === void 0) throw new TypeError("the model resolver returned undefined");
1100
+ if (checkAbort(ctx.signal)) return;
1101
+ const resolvedInstructions = options.instructions === void 0 ? void 0 : await resolve(options.instructions, resolverContext);
1102
+ if (checkAbort(ctx.signal)) return;
1103
+ const compaction = typeof configuredCompaction === "function" ? validateCompaction({
1104
+ compaction: configuredCompaction(resolverContext),
1105
+ explicit: true,
1106
+ generation: options.generation,
1107
+ tools: options.tools
1108
+ }) : staticCompaction;
1109
+ if (checkAbort(ctx.signal)) return;
1110
+ if (request.reason === "tool" && responseStepCount >= maxSteps) {
1111
+ const source = sourceCoordinatorState.response?.generation;
1112
+ if (source === void 0) return;
1113
+ return {
1114
+ type: "ai.generation.failed",
1115
+ id: `${requestId}:step-limit`,
1116
+ payload: {
1117
+ requestId: source.requestId,
1118
+ messageId: source.messageId,
1119
+ generationId: source.generationId,
1120
+ responseMessageId,
1121
+ error: `agent exceeded the ${maxSteps}-step limit`,
1122
+ stepLimit: true
1123
+ }
1124
+ };
1125
+ }
1126
+ const started = {
904
1127
  requestId,
905
- messageId: payload.messageId,
906
- generationId: payload.generationId,
907
- responseMessageId: payload.responseMessageId,
908
- error: "generation attempt was superseded after an incomplete run",
909
- superseded: true
1128
+ messageId: request.messageId,
1129
+ generationId,
1130
+ responseMessageId,
1131
+ attempt,
1132
+ model: modelName(resolvedModel),
1133
+ promptThroughIndex: history.at(-1)?.index ?? ctx.event.index
910
1134
  };
1135
+ const catalogModelId = gatewayModelId(resolvedModel);
1136
+ const gatewayOptions = options.generation?.providerOptions?.["gateway"];
1137
+ const usesFallbackModels = Array.isArray(gatewayOptions?.["models"]) && gatewayOptions["models"].length > 0;
1138
+ const discoversMetadata = compaction !== false && !("shouldCompact" in compaction) && compaction.thresholdTokens === void 0 && !usesFallbackModels && catalogModelId !== void 0;
1139
+ const startEvents = [];
1140
+ if (discoversMetadata && !Object.hasOwn(state.modelMetadata, catalogModelId)) startEvents.push({
1141
+ type: "ai.model.metadata.requested",
1142
+ id: `ai.model.metadata:${encodeURIComponent(options.agent.contract.name)}:${encodeURIComponent(ctx.event.sessionId)}:${encodeURIComponent(catalogModelId)}`,
1143
+ payload: { modelId: catalogModelId }
1144
+ });
1145
+ if (previous) {
1146
+ const payload = previous.payload;
1147
+ const superseded = {
1148
+ requestId,
1149
+ messageId: payload.messageId,
1150
+ generationId: payload.generationId,
1151
+ responseMessageId: payload.responseMessageId,
1152
+ error: "generation attempt was superseded after an incomplete run",
1153
+ superseded: true
1154
+ };
1155
+ startEvents.push({
1156
+ type: "ai.generation.failed",
1157
+ id: `${payload.generationId}:superseded`,
1158
+ payload: superseded
1159
+ });
1160
+ }
911
1161
  startEvents.push({
912
- type: "ai.generation.failed",
913
- id: `${payload.generationId}:superseded`,
914
- payload: superseded
1162
+ type: "ai.generation.started",
1163
+ id: generationId,
1164
+ payload: started
915
1165
  });
916
- }
917
- startEvents.push({
918
- type: "ai.generation.started",
919
- id: generationId,
920
- payload: started
921
- });
922
- await ctx.session.append("generation-start", ...startEvents);
923
- const startedCoordinatorState = (await ctx.session.state(coordinator, { through: "latest" })).state;
924
- if (startedCoordinatorState.response?.activeRequestId !== requestId || startedCoordinatorState.response.generation?.generationId !== generationId) return;
925
- const promptCoordinatorState = {
926
- ...coordinatorStateAt(history),
927
- queued: queuedMessagesAt(history)
928
- };
929
- let messages = activeContextMessages(state, promptCoordinatorState);
930
- if (options.compaction) {
931
- const compactionContext = {
932
- ...resolverContext,
933
- messages
1166
+ const generationHistory = [...history, ...await ctx.session.append("generation-start", ...startEvents)];
1167
+ generationStarted = true;
1168
+ const startedCoordinatorState = (await ctx.session.state(coordinator, { through: "latest" })).state;
1169
+ if (startedCoordinatorState.response?.activeRequestId !== requestId || startedCoordinatorState.response.generation?.generationId !== generationId) return;
1170
+ const promptCoordinatorState = {
1171
+ ...coordinatorStateAt(history),
1172
+ queued: queuedMessagesAt(history)
934
1173
  };
935
- if (await options.compaction.shouldCompact(compactionContext)) {
936
- const throughMessageId = messages.at(-1)?.id ?? request.messageId;
937
- await ctx.session.append("compaction-requested", {
938
- type: "ai.compaction.requested",
939
- id: `${generationId}:compaction:requested`,
940
- payload: {
941
- generationId,
942
- throughMessageId
943
- }
944
- });
945
- messages = await options.compaction.compact(compactionContext);
946
- const completed = {
947
- generationId,
948
- throughMessageId,
1174
+ const messages = activeContextMessages({
1175
+ agent: options.agent,
1176
+ state,
1177
+ history: promptHistory,
1178
+ coordinator: promptCoordinatorState
1179
+ });
1180
+ let modelMessages = await modelContext({
1181
+ messages,
1182
+ state,
1183
+ tools
1184
+ });
1185
+ const baseContext = {
1186
+ request: ctx.event,
1187
+ requestId,
1188
+ generationId,
1189
+ responseMessageId,
1190
+ messages,
1191
+ modelMessages,
1192
+ state,
1193
+ history,
1194
+ signal: ctx.signal,
1195
+ model: resolvedModel,
1196
+ tools,
1197
+ ...resolvedInstructions === void 0 ? {} : { instructions: resolvedInstructions },
1198
+ generation
1199
+ };
1200
+ const policy = compaction;
1201
+ const tracksInput = policy !== false && !("shouldCompact" in policy) && (policy.thresholdTokens !== void 0 || discoversMetadata);
1202
+ const metadata = catalogModelId === void 0 ? void 0 : state.modelMetadata[catalogModelId];
1203
+ const limits = metadata?.status === "resolved" && !usesFallbackModels ? metadata.limits : void 0;
1204
+ let inputTokenEstimate;
1205
+ let compacted = false;
1206
+ const canCompact = sourceCoordinatorState.response?.calls.every((call) => call.terminal) ?? true;
1207
+ if (policy && canCompact) {
1208
+ const compactionContext = {
1209
+ ...resolverContext,
949
1210
  messages,
950
- ...promptCoordinatorState.queued.length === 0 ? {} : { retainedMessageIds: promptCoordinatorState.queued.map((item) => item.messageId) }
951
- };
952
- await ctx.session.append("compaction-completed", {
953
- type: "ai.compaction.completed",
954
- id: `${generationId}:compaction:completed`,
955
- payload: completed
956
- });
957
- state = {
958
- ...state,
959
- compaction: {
960
- status: "completed",
961
- ...completed
962
- }
1211
+ modelMessages
963
1212
  };
1213
+ let shouldCompact;
1214
+ if ("shouldCompact" in policy) shouldCompact = await policy.shouldCompact(compactionContext);
1215
+ else if (tracksInput) {
1216
+ inputTokenEstimate = await estimateInputTokens({
1217
+ messages: modelMessages,
1218
+ instructions: resolvedInstructions,
1219
+ tools
1220
+ });
1221
+ const inputTokens = measuredInputTokens({
1222
+ estimate: inputTokenEstimate,
1223
+ model: modelName(resolvedModel),
1224
+ history
1225
+ });
1226
+ const threshold = policy.thresholdTokens ?? (limits === void 0 ? void 0 : Math.floor(Math.min(limits.contextWindow * .75, limits.contextWindow - (generation.maxOutputTokens ?? 0))));
1227
+ if (limits !== void 0 && threshold !== void 0 && threshold <= 0) throw new Error(`output allowance exhausts the ${limits.contextWindow}-token context window`);
1228
+ shouldCompact = threshold !== void 0 && inputTokens >= threshold;
1229
+ } else shouldCompact = false;
1230
+ if (checkAbort(ctx.signal)) return;
1231
+ if (shouldCompact) {
1232
+ const throughMessageId = state.messages.findLast((message) => !promptCoordinatorState.queued.some((queued) => queued.messageId === message.id))?.id ?? request.messageId;
1233
+ const throughIndex = history.at(-1)?.index ?? ctx.event.index;
1234
+ generationHistory.push(...await ctx.session.append("compaction-requested", {
1235
+ type: "ai.compaction.requested",
1236
+ id: `${generationId}:compaction:requested`,
1237
+ payload: {
1238
+ generationId,
1239
+ throughMessageId,
1240
+ throughIndex
1241
+ }
1242
+ }));
1243
+ const result = !("shouldCompact" in policy) ? {
1244
+ messages: [],
1245
+ ...await summarize({
1246
+ context: baseContext,
1247
+ instructions: policy.instructions
1248
+ })
1249
+ } : { messages: await policy.compact(compactionContext) };
1250
+ if (checkAbort(ctx.signal)) return;
1251
+ const completed = {
1252
+ generationId,
1253
+ throughMessageId,
1254
+ throughIndex,
1255
+ ...result,
1256
+ ...promptCoordinatorState.queued.length === 0 ? {} : { retainedMessageIds: promptCoordinatorState.queued.map((item) => item.messageId) }
1257
+ };
1258
+ generationHistory.push(...await ctx.session.append("compaction-completed", {
1259
+ type: "ai.compaction.completed",
1260
+ id: `${generationId}:compaction:completed`,
1261
+ payload: completed
1262
+ }));
1263
+ compacted = true;
1264
+ }
964
1265
  }
965
- }
966
- const currentState = (await ctx.session.state(options.agent.reducer, { through: "latest" })).state;
967
- const currentCoordinatorState = (await ctx.session.state(coordinator, { through: "latest" })).state;
968
- const generationMessages = activeContextMessages(currentState, currentCoordinatorState);
969
- promptCache.set(promptCacheKey(ctx.event.sessionId, generationId), convertToModelMessages(generationMessages, { tools }));
970
- let pendingToolCalls = [];
971
- try {
972
- const resolvedInstructions = options.instructions === void 0 ? void 0 : await resolve(options.instructions, resolverContext);
973
- if (ctx.signal.aborted) return;
1266
+ const currentState = replay(options.agent, generationHistory);
1267
+ const currentCoordinatorState = coordinatorStateAt(generationHistory);
1268
+ const generationMessages = activeContextMessages({
1269
+ agent: options.agent,
1270
+ state: currentState,
1271
+ history: generationHistory,
1272
+ coordinator: currentCoordinatorState
1273
+ });
1274
+ modelMessages = await modelContext({
1275
+ messages: generationMessages,
1276
+ state: currentState,
1277
+ tools
1278
+ });
1279
+ promptCache.set(promptCacheKey(ctx.event.sessionId, generationId), Promise.resolve(modelMessages));
1280
+ if (tracksInput && (inputTokenEstimate === void 0 || compacted)) inputTokenEstimate = await estimateInputTokens({
1281
+ messages: modelMessages,
1282
+ instructions: resolvedInstructions,
1283
+ tools
1284
+ });
1285
+ let pendingToolCalls = [];
1286
+ if (checkAbort(ctx.signal)) return;
974
1287
  const generateContext = {
975
1288
  request: ctx.event,
976
1289
  requestId,
977
1290
  generationId,
978
1291
  responseMessageId,
979
1292
  messages: generationMessages,
1293
+ modelMessages,
980
1294
  state: currentState,
981
1295
  history,
982
1296
  signal: ctx.signal,
@@ -1031,6 +1345,7 @@ function createHandlers(options) {
1031
1345
  messageId: request.messageId,
1032
1346
  generationId,
1033
1347
  responseMessageId,
1348
+ ...inputTokenEstimate === void 0 ? {} : { inputTokenEstimate },
1034
1349
  ...finish.finishReason === void 0 ? {} : { finishReason: finish.finishReason },
1035
1350
  ...finish.usage === void 0 ? {} : { usage: finish.usage }
1036
1351
  };
@@ -1053,8 +1368,11 @@ function createHandlers(options) {
1053
1368
  }
1054
1369
  ];
1055
1370
  } catch (error) {
1056
- if (error instanceof A2Error) throw error;
1057
- if (ctx.signal.aborted) {
1371
+ if (error instanceof A2Error) {
1372
+ checkAbort(ctx.signal);
1373
+ throw error;
1374
+ }
1375
+ if (checkAbort(ctx.signal)) {
1058
1376
  const interrupted = {
1059
1377
  messageId: responseMessageId,
1060
1378
  generationId,
@@ -1066,6 +1384,7 @@ function createHandlers(options) {
1066
1384
  payload: interrupted
1067
1385
  };
1068
1386
  }
1387
+ if (!generationStarted) throw error;
1069
1388
  const failed = {
1070
1389
  requestId,
1071
1390
  messageId: request.messageId,
@@ -1111,6 +1430,21 @@ function createHandlers(options) {
1111
1430
  return scheduleNext(ctx);
1112
1431
  };
1113
1432
  return {
1433
+ "ai.model.metadata.requested": { handler: async (ctx) => {
1434
+ const limits = await readModelLimits({
1435
+ modelId: ctx.event.payload.modelId,
1436
+ signal: ctx.signal
1437
+ });
1438
+ ctx.signal.throwIfAborted();
1439
+ return {
1440
+ type: "ai.model.metadata.resolved",
1441
+ id: `${ctx.event.id}:resolved`,
1442
+ payload: {
1443
+ modelId: ctx.event.payload.modelId,
1444
+ limits
1445
+ }
1446
+ };
1447
+ } },
1114
1448
  "ai.message.created": {
1115
1449
  lane: "a2.ai.turn",
1116
1450
  handler: handleMessageCreated