experimental-a2 0.14.0 → 0.15.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.
package/dist/index.d.ts CHANGED
@@ -2,4 +2,8 @@ import { a as ContractEvent, c as PresenceDefs, d as PresenceSnapshot, f as Redu
2
2
  import { i as Event } from "./store-D_yhNdPz.js";
3
3
  import { i as NonRetriableError, n as A2ErrorCode, r as A2ServerErrorCode, t as A2Error } from "./errors-DvhSXnxk.js";
4
4
  import { i as A2Telemetry, n as A2SpanHandle, r as A2SpanName, t as A2AttributeValue } from "./telemetry-CpeclqB2.js";
5
- export { type A2AttributeValue, A2Error, type A2ErrorCode, type A2ServerErrorCode, type A2SpanHandle, type A2SpanName, type A2Telemetry, type AppendInput, type Contract, type ContractEvent, type Event, type EventBatchFactory, type EventDefs, NonRetriableError, type PresenceDefs, type PresenceMap, type PresencePatch, type PresenceSnapshot, type Reducer, type ReducerBuilder, type ReducerOptions, type StandardSchemaV1, contract };
5
+ //#region src/idempotent-id.d.ts
6
+ declare function idempotentId(...parts: string[]): Promise<string>;
7
+ //#endregion
8
+ export { type A2AttributeValue, A2Error, type A2ErrorCode, type A2ServerErrorCode, type A2SpanHandle, type A2SpanName, type A2Telemetry, type AppendInput, type Contract, type ContractEvent, type Event, type EventBatchFactory, type EventDefs, NonRetriableError, type PresenceDefs, type PresenceMap, type PresencePatch, type PresenceSnapshot, type Reducer, type ReducerBuilder, type ReducerOptions, type StandardSchemaV1, contract, idempotentId };
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/idempotent-id.ts"],"mappings":";;;;;iBAAsB,gBAAgB,kBAAkB"}
package/dist/index.js CHANGED
@@ -1,3 +1,15 @@
1
1
  import { t as contract } from "./contract-CKRg_E4q.js";
2
2
  import { n as NonRetriableError, t as A2Error } from "./errors-DCk6ch5n.js";
3
- export { A2Error, NonRetriableError, contract };
3
+ //#region src/idempotent-id.ts
4
+ async function idempotentId(...parts) {
5
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(parts)));
6
+ const bytes = new Uint8Array(digest).slice(0, 16);
7
+ bytes[6] = bytes[6] & 15 | 128;
8
+ bytes[8] = bytes[8] & 63 | 128;
9
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
10
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
11
+ }
12
+ //#endregion
13
+ export { A2Error, NonRetriableError, contract, idempotentId };
14
+
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/idempotent-id.ts"],"sourcesContent":["export async function idempotentId(...parts: string[]): Promise<string> {\n const digest = await crypto.subtle.digest(\n 'SHA-256',\n new TextEncoder().encode(JSON.stringify(parts)),\n )\n const bytes = new Uint8Array(digest).slice(0, 16)\n bytes[6] = (bytes[6]! & 0x0f) | 0x80\n bytes[8] = (bytes[8]! & 0x3f) | 0x80\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`\n}\n"],"mappings":";;;AAAA,eAAsB,aAAa,GAAG,OAAkC;CACtE,MAAM,SAAS,MAAM,OAAO,OAAO,OACjC,WACA,IAAI,YAAY,CAAC,CAAC,OAAO,KAAK,UAAU,KAAK,CAAC,CAChD;CACA,MAAM,QAAQ,IAAI,WAAW,MAAM,CAAC,CAAC,MAAM,GAAG,EAAE;CAChD,MAAM,KAAM,MAAM,KAAM,KAAQ;CAChC,MAAM,KAAM,MAAM,KAAM,KAAQ;CAChC,MAAM,MAAM,MAAM,KAAK,QAAQ,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE;CAC7E,OAAO,GAAG,IAAI,MAAM,GAAG,CAAC,EAAE,GAAG,IAAI,MAAM,GAAG,EAAE,EAAE,GAAG,IAAI,MAAM,IAAI,EAAE,EAAE,GAAG,IAAI,MAAM,IAAI,EAAE,EAAE,GAAG,IAAI,MAAM,IAAI,EAAE;AAC7G"}
@@ -67,11 +67,30 @@ and continuation between steps. Responses have no step limit by default. Set
67
67
  `maxSteps` to a positive integer to limit one complete assistant response.
68
68
  Individual providers decide which model settings they support. `generation`
69
69
  therefore excludes `stopWhen`, tool execution callbacks, tool callers, sandbox
70
- execution, and the tool approval secret. Everything else, including
71
- `toolsContext`, passes through to the AI SDK untouched. A local tool reads its
72
- durable execution context later, from the handler that runs it. Model-step
70
+ execution, and the tool approval secret. A2 preserves all other supplied
71
+ settings, including `toolsContext`, when calling the AI SDK. A local tool
72
+ reads its durable execution context later, from the handler that runs it. Model-step
73
73
  timeouts are supported; tool-execution timeouts are not.
74
74
 
75
+ For AI Gateway models, the default generator sets
76
+ `generation.providerOptions.gateway.caching` to `'auto'`. A2 merges this
77
+ default with your provider options, with caller-supplied values taking
78
+ precedence. Other Gateway options and provider namespaces are preserved.
79
+ The same default applies to automatic compaction. A custom `generate`
80
+ function receives your original settings and owns its caching policy.
81
+
82
+ Gateway calls also send an `x-session-affinity` header derived from a SHA-256
83
+ hash of the agent name and session ID. The value stays stable across turns,
84
+ retries, worker restarts, and automatic compaction. No raw session identifier
85
+ is sent in this header. A caller-supplied `generation.headers` entry takes
86
+ precedence, regardless of header casing; an explicit `undefined` suppresses
87
+ A2's default header. Set overrides here, since A2's per-call default can
88
+ override headers configured on the model itself.
89
+ Affinity helps supporting providers reuse their cache; it does not select
90
+ Gateway's provider or guarantee a cache hit.
91
+ See [Gateway automatic caching](https://vercel.com/docs/ai-gateway/models-and-providers/automatic-caching)
92
+ for provider support and cache-write costs.
93
+
75
94
  ### Store assistant metadata
76
95
 
77
96
  Extend the agent's `UIMessage` type to keep generation status and usage on the
@@ -63,6 +63,31 @@ The shared base for A2 failures, discriminated by `code`. The session
63
63
  client exposes its `A2ClientError` subclass. See
64
64
  [Errors](/reference/errors).
65
65
 
66
+ ### `idempotentId(...parts)`
67
+
68
+ `idempotentId(...parts: string[]): Promise<string>` derives a stable UUIDv8
69
+ from an ordered tuple of strings. It runs on the server and in the browser.
70
+
71
+ ```ts shared/request-id.ts
72
+ import { idempotentId } from 'experimental-a2'
73
+
74
+ export const id = await idempotentId('orders', 'shop-7', 'request-42')
75
+ ```
76
+
77
+ Pass the result as an event's `id` when retries should converge on the same
78
+ event. The same tuple produces the same ID across runtimes and A2 versions.
79
+ String boundaries, order, and case matter; strings are not normalized.
80
+ Empty strings and an empty tuple are valid inputs.
81
+
82
+ Every argument has the same role. A prefix such as `'orders'` is a namespace
83
+ by convention. Include enough context to distinguish operations across
84
+ contracts and sessions, since event IDs are globally unique within a store.
85
+
86
+ The helper performs no I/O. A2 preserves explicit IDs and keeps its automatic
87
+ ID generation unchanged. Changing an existing application's ID recipe changes
88
+ retry identity, so retries of previously committed requests need to retain
89
+ their original IDs.
90
+
66
91
  ## `experimental-a2/server`
67
92
 
68
93
  ### `handler(entry)`
@@ -22,7 +22,7 @@
22
22
  "@vercel/sandbox": "^3.0.0",
23
23
  "ai": "^7.0.58",
24
24
  "codemirror": "^6.0.2",
25
- "experimental-a2": "0.14.0",
25
+ "experimental-a2": "0.15.0",
26
26
  "ioredis": "^5.9.0",
27
27
  "next": "^16.3.0",
28
28
  "pg": "^8.16.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "experimental-a2",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Durable sync and reactions for things with a lifecycle: one event log, derived state, and live client per session.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/ai-server.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  import { createControlRuntime, ControlCancelled } from './ai-control-server.ts'
12
12
  import type { ControlCommit } from './ai-control.ts'
13
13
  import { AsyncLocalStorage } from 'node:async_hooks'
14
+ import { createHash } from 'node:crypto'
14
15
  import { asSchema, convertToModelMessages } from 'ai'
15
16
  import type {
16
17
  FinishReason,
@@ -336,10 +337,38 @@ async function generateWithAISDK<
336
337
  M extends UIMessage,
337
338
  D extends AIEventDefs<M> & EventDefs,
338
339
  T extends ToolSet,
339
- >(
340
- context: AgentGenerateContext<M, D, T>,
341
- messageMetadata: AgentMessageMetadata<M, D, T> | undefined,
342
- ): Promise<GenerationSource> {
340
+ >(options: {
341
+ agentName: string
342
+ context: AgentGenerateContext<M, D, T>
343
+ messageMetadata: AgentMessageMetadata<M, D, T> | undefined
344
+ }): Promise<GenerationSource> {
345
+ const { agentName, context, messageMetadata } = options
346
+ const headers = context.generation.headers ?? {}
347
+ const generation =
348
+ gatewayModelId(context.model) === undefined
349
+ ? context.generation
350
+ : {
351
+ ...context.generation,
352
+ headers: Object.keys(headers).some(
353
+ (key) => key.toLowerCase() === 'x-session-affinity',
354
+ )
355
+ ? headers
356
+ : {
357
+ ...headers,
358
+ 'x-session-affinity': createHash('sha256')
359
+ .update(
360
+ JSON.stringify([agentName, context.request.sessionId]),
361
+ )
362
+ .digest('hex'),
363
+ },
364
+ providerOptions: {
365
+ ...context.generation.providerOptions,
366
+ gateway: {
367
+ caching: 'auto',
368
+ ...context.generation.providerOptions?.['gateway'],
369
+ },
370
+ },
371
+ }
343
372
  const step = await generateAISDKStep<M, T>({
344
373
  model: context.model,
345
374
  tools: context.tools,
@@ -350,11 +379,12 @@ async function generateWithAISDK<
350
379
  ...(context.instructions === undefined
351
380
  ? {}
352
381
  : { instructions: context.instructions }),
353
- settings: context.generation,
382
+ settings: generation,
354
383
  ...(messageMetadata === undefined
355
384
  ? {}
356
385
  : {
357
- messageMetadata: ({ part }) => messageMetadata({ ...context, part }),
386
+ messageMetadata: ({ part }) =>
387
+ messageMetadata({ ...context, generation, part }),
358
388
  }),
359
389
  })
360
390
  return {
@@ -457,6 +487,7 @@ const summarize = async <
457
487
  D extends AIEventDefs<M> & EventDefs,
458
488
  T extends ToolSet,
459
489
  >(options: {
490
+ agentName: string
460
491
  context: AgentGenerateContext<M, D, T>
461
492
  instructions: string | undefined
462
493
  }): Promise<{ summary: string; usage?: LanguageModelUsage }> => {
@@ -498,8 +529,9 @@ const summarize = async <
498
529
  return [name, definition]
499
530
  }),
500
531
  ) as T
501
- const source = await generateWithAISDK(
502
- {
532
+ const source = await generateWithAISDK({
533
+ agentName: options.agentName,
534
+ context: {
503
535
  ...options.context,
504
536
  generation,
505
537
  tools,
@@ -509,8 +541,8 @@ const summarize = async <
509
541
  ],
510
542
  responseMessageId: `${options.context.generationId}:summary`,
511
543
  },
512
- undefined,
513
- )
544
+ messageMetadata: undefined,
545
+ })
514
546
  let summary = ''
515
547
  let calledTool = false
516
548
  let finish: AgentGenerationFinish | undefined
@@ -1663,6 +1695,7 @@ export function createHandlers<
1663
1695
  ? {
1664
1696
  messages: [] as M[],
1665
1697
  ...(await summarize({
1698
+ agentName: options.agent.contract.name,
1666
1699
  context: baseContext,
1667
1700
  instructions: policy.instructions,
1668
1701
  })),
@@ -1765,7 +1798,11 @@ export function createHandlers<
1765
1798
  const custom = options.generate !== undefined
1766
1799
  const source =
1767
1800
  options.generate === undefined
1768
- ? await generateWithAISDK(generateContext, options.messageMetadata)
1801
+ ? await generateWithAISDK({
1802
+ agentName: options.agent.contract.name,
1803
+ context: generateContext,
1804
+ messageMetadata: options.messageMetadata,
1805
+ })
1769
1806
  : { stream: await options.generate(generateContext) }
1770
1807
  const updates = consumeGeneration({
1771
1808
  source,
@@ -0,0 +1,11 @@
1
+ export async function idempotentId(...parts: string[]): Promise<string> {
2
+ const digest = await crypto.subtle.digest(
3
+ 'SHA-256',
4
+ new TextEncoder().encode(JSON.stringify(parts)),
5
+ )
6
+ const bytes = new Uint8Array(digest).slice(0, 16)
7
+ bytes[6] = (bytes[6]! & 0x0f) | 0x80
8
+ bytes[8] = (bytes[8]! & 0x3f) | 0x80
9
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')
10
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`
11
+ }
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  export { contract } from './contract.ts'
11
+ export { idempotentId } from './idempotent-id.ts'
11
12
  export type {
12
13
  AppendInput,
13
14
  Contract,