experimental-a2 0.14.1 → 0.16.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 (103) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/actor-client.d.ts +1 -1
  3. package/dist/{actor-BfQSE0KC.d.ts → actor-ohPC-81x.d.ts} +5 -5
  4. package/dist/{actor-BfQSE0KC.d.ts.map → actor-ohPC-81x.d.ts.map} +1 -1
  5. package/dist/actor-react.d.ts +3 -3
  6. package/dist/actor.d.ts +1 -1
  7. package/dist/actor.js +1 -1
  8. package/dist/ai-CrLYwNEx.js +2431 -0
  9. package/dist/ai-CrLYwNEx.js.map +1 -0
  10. package/dist/{ai-Cai-lCbj.d.ts → ai-HJ9fHfYI.d.ts} +72 -13
  11. package/dist/ai-HJ9fHfYI.d.ts.map +1 -0
  12. package/dist/ai-server.d.ts +4 -5
  13. package/dist/ai-server.d.ts.map +1 -1
  14. package/dist/ai-server.js +755 -290
  15. package/dist/ai-server.js.map +1 -1
  16. package/dist/ai.d.ts +2 -2
  17. package/dist/ai.js +1 -1999
  18. package/dist/client-BYzHjkwU.js.map +1 -1
  19. package/dist/{client-BAEABRZB.d.ts → client-CzyacQpJ.d.ts} +9 -9
  20. package/dist/client-CzyacQpJ.d.ts.map +1 -0
  21. package/dist/client.d.ts +1 -1
  22. package/dist/idempotent-id-BRJVeylj.js +13 -0
  23. package/dist/idempotent-id-BRJVeylj.js.map +1 -0
  24. package/dist/index.d.ts +8 -4
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +2 -1
  27. package/dist/otel.d.ts +1 -1
  28. package/dist/react.d.ts +13 -13
  29. package/dist/react.d.ts.map +1 -1
  30. package/dist/react.js.map +1 -1
  31. package/dist/reducer-DEMjEY_O.js.map +1 -1
  32. package/dist/{reducer-BcS9VDKC.d.ts → reducer-otzHjuJj.d.ts} +3 -3
  33. package/dist/{reducer-BcS9VDKC.d.ts.map → reducer-otzHjuJj.d.ts.map} +1 -1
  34. package/dist/scheduler-qstash.d.ts +2 -2
  35. package/dist/scheduler-qstash.js +1 -1
  36. package/dist/scheduler-vercel.d.ts +2 -2
  37. package/dist/scheduler-vercel.js +1 -1
  38. package/dist/{server-CjJSGcF7.js → server-BD5ckxZb.js} +39 -26
  39. package/dist/server-BD5ckxZb.js.map +1 -0
  40. package/dist/{server-Bp5Nd1pF.d.ts → server-DbD7IJVK.d.ts} +5 -5
  41. package/dist/{server-Bp5Nd1pF.d.ts.map → server-DbD7IJVK.d.ts.map} +1 -1
  42. package/dist/server.d.ts +3 -3
  43. package/dist/server.js +1 -1
  44. package/dist/{store-D_yhNdPz.d.ts → store-DyZM6fS5.d.ts} +3 -2
  45. package/dist/{store-D_yhNdPz.d.ts.map → store-DyZM6fS5.d.ts.map} +1 -1
  46. package/dist/store-N8PXxDAS.js.map +1 -1
  47. package/dist/store-memory.d.ts +1 -1
  48. package/dist/store-postgres.d.ts +1 -1
  49. package/dist/store-redis-http.d.ts +1 -1
  50. package/dist/store-redis-http.js +1 -1
  51. package/dist/{store-redis-notify-D2EI6gwX.js → store-redis-notify-BVLUfI3j.js} +14 -13
  52. package/dist/store-redis-notify-BVLUfI3j.js.map +1 -0
  53. package/dist/store-redis.d.ts +1 -1
  54. package/dist/store-redis.js +1 -1
  55. package/dist/store-sqlite.d.ts +1 -1
  56. package/dist/{telemetry-CpeclqB2.d.ts → telemetry-B5jzpy6y.d.ts} +2 -2
  57. package/dist/telemetry-B5jzpy6y.d.ts.map +1 -0
  58. package/docs/concepts/04-state.mdx +9 -4
  59. package/docs/guides/06-ai-agents.mdx +134 -43
  60. package/docs/reference/01-api.mdx +129 -27
  61. package/examples/playground/app/agent/[agentId]/agent-client.tsx +20 -28
  62. package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +123 -0
  63. package/examples/playground/app/agent/[agentId]/agent-queue.tsx +37 -43
  64. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +44 -24
  65. package/examples/playground/app/agent/compaction-timeline.test.ts +47 -7
  66. package/examples/playground/app/agent/compaction-timeline.ts +13 -3
  67. package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -5
  68. package/examples/playground/app/chat/[chatId]/session.ts +17 -2
  69. package/examples/playground/app/chat/model.test.ts +4 -4
  70. package/examples/playground/app/chat/model.ts +18 -6
  71. package/examples/playground/app/chat/server.ts +6 -3
  72. package/examples/playground/package.json +3 -1
  73. package/package.json +1 -1
  74. package/src/ai-client-state.ts +0 -2
  75. package/src/ai-context-schema.ts +296 -0
  76. package/src/ai-context.ts +680 -0
  77. package/src/ai-control-server.ts +160 -131
  78. package/src/ai-control-state.ts +6 -0
  79. package/src/ai-control.ts +6 -7
  80. package/src/ai-coordinator.ts +92 -38
  81. package/src/ai-id.ts +6 -0
  82. package/src/ai-input-tokens.ts +75 -0
  83. package/src/ai-message-projection.ts +348 -0
  84. package/src/ai-server.ts +232 -334
  85. package/src/ai-stored-state.ts +484 -0
  86. package/src/ai.ts +252 -418
  87. package/src/client.ts +21 -15
  88. package/src/idempotent-id.ts +11 -0
  89. package/src/index.ts +1 -0
  90. package/src/react.ts +25 -19
  91. package/src/reducer.ts +2 -1
  92. package/src/server.ts +73 -56
  93. package/src/store-redis-core.ts +15 -17
  94. package/src/store.ts +1 -0
  95. package/src/telemetry.ts +2 -1
  96. package/dist/ai-Cai-lCbj.d.ts.map +0 -1
  97. package/dist/ai-control-CcD4hh3y.js +0 -119
  98. package/dist/ai-control-CcD4hh3y.js.map +0 -1
  99. package/dist/ai.js.map +0 -1
  100. package/dist/client-BAEABRZB.d.ts.map +0 -1
  101. package/dist/server-CjJSGcF7.js.map +0 -1
  102. package/dist/store-redis-notify-D2EI6gwX.js.map +0 -1
  103. package/dist/telemetry-CpeclqB2.d.ts.map +0 -1
package/src/client.ts CHANGED
@@ -164,10 +164,11 @@ export type SessionClient<
164
164
  D extends EventDefs,
165
165
  S,
166
166
  P extends PresenceDefs = Record<never, never>,
167
+ V = S,
167
168
  > = WithPresence<P, SessionClientPresence<P>> & {
168
169
  readonly sessionId: string
169
170
  subscribe(listener: () => void): () => void
170
- getSnapshot(): SessionSnapshot<D, S, P>
171
+ getSnapshot(): SessionSnapshot<D, V, P>
171
172
  /**
172
173
  * Optimistic append: validates locally against the reducer's event
173
174
  * schemas (instant `INVALID_PAYLOAD`, no flicker), applies to the
@@ -224,11 +225,12 @@ export type A2Client<
224
225
  D extends EventDefs,
225
226
  S,
226
227
  P extends PresenceDefs = Record<never, never>,
228
+ V = S,
227
229
  > = {
228
230
  session(
229
231
  sessionId: string,
230
232
  options?: SessionOptions<D, S>,
231
- ): SessionClient<D, S, P>
233
+ ): SessionClient<D, S, P, V>
232
234
  }
233
235
 
234
236
  /**
@@ -275,8 +277,9 @@ export type CreateClientOptions<
275
277
  D extends EventDefs,
276
278
  S,
277
279
  P extends PresenceDefs = Record<never, never>,
280
+ V = S,
278
281
  > = {
279
- reducer: Reducer<D, S, P>
282
+ reducer: Reducer<D, S, P, V>
280
283
  /** The route (or routes) the client speaks — see `ClientApi`. */
281
284
  api: ClientApi
282
285
  /** Delay the next connection attempt in milliseconds, or pause with false. */
@@ -1264,13 +1267,14 @@ type SessionRuntime<
1264
1267
  D extends EventDefs,
1265
1268
  S,
1266
1269
  P extends PresenceDefs,
1267
- > = SessionClient<D, S, P> & {
1270
+ V,
1271
+ > = SessionClient<D, S, P, V> & {
1268
1272
  hydrate(options?: SessionOptions<D, S>): void
1269
1273
  canEvict(): boolean
1270
1274
  }
1271
1275
 
1272
- type CachedSession<D extends EventDefs, S, P extends PresenceDefs> = {
1273
- runtime: SessionRuntime<D, S, P>
1276
+ type CachedSession<D extends EventDefs, S, P extends PresenceDefs, V> = {
1277
+ runtime: SessionRuntime<D, S, P, V>
1274
1278
  timer?: ReturnType<typeof setTimeout>
1275
1279
  }
1276
1280
 
@@ -1278,7 +1282,8 @@ export function createClient<
1278
1282
  D extends EventDefs,
1279
1283
  S,
1280
1284
  P extends PresenceDefs = Record<never, never>,
1281
- >(options: CreateClientOptions<D, S, P>): A2Client<D, S, P> {
1285
+ V = S,
1286
+ >(options: CreateClientOptions<D, S, P, V>): A2Client<D, S, P, V> {
1282
1287
  const { reducer, api } = options
1283
1288
  const presenceDefs: Readonly<PresenceDefs> = reducer.presence
1284
1289
  const declaresPresence = Object.keys(presenceDefs).length > 0
@@ -1303,7 +1308,7 @@ export function createClient<
1303
1308
  if (Number.isNaN(gcTime) || gcTime < 0) {
1304
1309
  throw new RangeError('gcTime must be a non-negative number or Infinity')
1305
1310
  }
1306
- const sessions = new Map<string, CachedSession<D, S, P>>()
1311
+ const sessions = new Map<string, CachedSession<D, S, P, V>>()
1307
1312
 
1308
1313
  const scheduleGc = (sessionId: string, delay = gcTime): void => {
1309
1314
  const entry = sessions.get(sessionId)
@@ -1331,7 +1336,7 @@ export function createClient<
1331
1336
  const makeSession = (
1332
1337
  sessionId: string,
1333
1338
  sessionOptions?: SessionOptions<D, S>,
1334
- ): SessionRuntime<D, S, P> => {
1339
+ ): SessionRuntime<D, S, P, V> => {
1335
1340
  // ── server truth ─────────────────────────────────────────────
1336
1341
  let stateIndex = sessionOptions?.initialIndex ?? 0
1337
1342
  let streamIndex =
@@ -1373,7 +1378,7 @@ export function createClient<
1373
1378
 
1374
1379
  // ── store plumbing ───────────────────────────────────────────
1375
1380
  const listeners = new Set<() => void>()
1376
- let snapshot: SessionSnapshot<D, S> | null = null
1381
+ let snapshot: SessionSnapshot<D, V, P> | null = null
1377
1382
  let status: ConnectionStatus = 'idle'
1378
1383
  let reconnects = 0
1379
1384
  let lastError: A2ClientError | null = null
@@ -1476,7 +1481,7 @@ export function createClient<
1476
1481
  }
1477
1482
  }
1478
1483
 
1479
- const buildSnapshot = (): SessionSnapshot<D, S, P> => {
1484
+ const buildSnapshot = (): SessionSnapshot<D, V, P> => {
1480
1485
  const overlay = overlayEvents()
1481
1486
  let state = foldedState
1482
1487
  for (const event of overlay) {
@@ -1496,7 +1501,8 @@ export function createClient<
1496
1501
  return true
1497
1502
  })
1498
1503
  return {
1499
- state: reducer[clientStateProjection]?.(state) ?? state,
1504
+ state:
1505
+ reducer[clientStateProjection]?.(state) ?? (state as unknown as V),
1500
1506
  events: [...serverEvents, ...displayedOverlay],
1501
1507
  index: stateIndex,
1502
1508
  history: historyState(),
@@ -1504,7 +1510,7 @@ export function createClient<
1504
1510
  ...(declaresPresence
1505
1511
  ? { presence: presenceState as PresenceMap<P> }
1506
1512
  : {}),
1507
- } as SessionSnapshot<D, S, P>
1513
+ } as SessionSnapshot<D, V, P>
1508
1514
  }
1509
1515
 
1510
1516
  // ── presence ─────────────────────────────────────────────────
@@ -2167,7 +2173,7 @@ export function createClient<
2167
2173
  previous?.abort()
2168
2174
  }
2169
2175
 
2170
- const runtime: SessionRuntime<D, S, P> = {
2176
+ const runtime: SessionRuntime<D, S, P, V> = {
2171
2177
  sessionId,
2172
2178
  // Only on presence-declaring contracts — the type surface
2173
2179
  // promises no runtime-inert members (a2-api.md §12).
@@ -2271,7 +2277,7 @@ export function createClient<
2271
2277
  presenceTimer === undefined
2272
2278
  )
2273
2279
  },
2274
- } as SessionRuntime<D, S, P>
2280
+ } as SessionRuntime<D, S, P, V>
2275
2281
 
2276
2282
  return runtime
2277
2283
  }
@@ -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,
package/src/react.ts CHANGED
@@ -153,38 +153,43 @@ export type A2React<
153
153
  D extends EventDefs,
154
154
  S,
155
155
  P extends PresenceDefs = Record<never, never>,
156
+ V = S,
156
157
  > = {
157
158
  SessionProvider: (props: SessionProviderProps<D, S>) => ReactElement
158
- useSession: () => UseSessionResult<D, S, P>
159
+ useSession: () => UseSessionResult<D, V, P>
159
160
  }
160
161
 
161
162
  export type BoundA2React<
162
163
  D extends EventDefs,
163
164
  S,
164
165
  P extends PresenceDefs = Record<never, never>,
166
+ V = S,
165
167
  > = {
166
168
  SessionProvider: (props: BoundSessionProviderProps<D, S>) => ReactElement
167
- useSession: () => UseSessionResult<D, S, P>
169
+ useSession: () => UseSessionResult<D, V, P>
168
170
  }
169
171
 
170
172
  export function createReact<
171
173
  D extends EventDefs,
172
174
  S,
173
175
  P extends PresenceDefs = Record<never, never>,
174
- >(options: { client: A2Client<D, S, P> }): BoundA2React<D, S, P>
176
+ V = S,
177
+ >(options: { client: A2Client<D, S, P, V> }): BoundA2React<D, S, P, V>
175
178
  export function createReact<
176
179
  D extends EventDefs,
177
180
  S,
178
181
  P extends PresenceDefs = Record<never, never>,
179
- >(options: { reducer: Reducer<D, S, P> }): A2React<D, S, P>
182
+ V = S,
183
+ >(options: { reducer: Reducer<D, S, P, V> }): A2React<D, S, P, V>
180
184
  export function createReact<
181
185
  D extends EventDefs,
182
186
  S,
183
187
  P extends PresenceDefs = Record<never, never>,
188
+ V = S,
184
189
  >(
185
- options: { client: A2Client<D, S, P> } | { reducer: Reducer<D, S, P> },
186
- ): BoundA2React<D, S, P> | A2React<D, S, P> {
187
- const Context = createContext<SessionClient<D, S, P> | null>(null)
190
+ options: { client: A2Client<D, S, P, V> } | { reducer: Reducer<D, S, P, V> },
191
+ ): BoundA2React<D, S, P, V> | A2React<D, S, P, V> {
192
+ const Context = createContext<SessionClient<D, S, P, V> | null>(null)
188
193
  const resolveSession: (
189
194
  sessionId: string,
190
195
  api: string | undefined,
@@ -192,7 +197,7 @@ export function createReact<
192
197
  initialIndex: number,
193
198
  initialEvents: ContractEvent<D>[] | undefined,
194
199
  participant: string | undefined,
195
- ) => SessionClient<D, S, P> =
200
+ ) => SessionClient<D, S, P, V> =
196
201
  'client' in options
197
202
  ? (
198
203
  sessionId,
@@ -209,7 +214,7 @@ export function createReact<
209
214
  ...(participant === undefined ? {} : { participant }),
210
215
  })
211
216
  : (() => {
212
- const clients = new Map<string, A2Client<D, S, P>>()
217
+ const clients = new Map<string, A2Client<D, S, P, V>>()
213
218
  return (
214
219
  sessionId: string,
215
220
  api: string | undefined,
@@ -264,7 +269,7 @@ export function createReact<
264
269
  return createElement(Context.Provider, { value: client }, props.children)
265
270
  }
266
271
 
267
- function useBoundSession(): UseSessionResult<D, S, P> {
272
+ function useBoundSession(): UseSessionResult<D, V, P> {
268
273
  const client = useContext(Context)
269
274
  if (!client) {
270
275
  throw new Error(
@@ -288,8 +293,8 @@ export function createReact<
288
293
  * stream — connecting an unhydrated session would replay the whole
289
294
  * log, and the library never picks the expensive path silently.
290
295
  */
291
- function useSessionLifecycle<D extends EventDefs, S, P extends PresenceDefs>(
292
- session: SessionClient<D, S, P>,
296
+ function useSessionLifecycle<D extends EventDefs, S, P extends PresenceDefs, V>(
297
+ session: SessionClient<D, S, P, V>,
293
298
  live: boolean,
294
299
  ): void {
295
300
  useEffect(() => {
@@ -298,10 +303,10 @@ function useSessionLifecycle<D extends EventDefs, S, P extends PresenceDefs>(
298
303
  }, [session, live])
299
304
  }
300
305
 
301
- function useSessionResult<D extends EventDefs, S, P extends PresenceDefs>(
302
- session: SessionClient<D, S, P>,
303
- ): UseSessionResult<D, S, P> {
304
- const snapshot: SessionSnapshot<D, S, P> = useSyncExternalStore(
306
+ function useSessionResult<D extends EventDefs, S, P extends PresenceDefs, V>(
307
+ session: SessionClient<D, S, P, V>,
308
+ ): UseSessionResult<D, V, P> {
309
+ const snapshot: SessionSnapshot<D, V, P> = useSyncExternalStore(
305
310
  session.subscribe,
306
311
  session.getSnapshot,
307
312
  session.getSnapshot,
@@ -322,7 +327,7 @@ function useSessionResult<D extends EventDefs, S, P extends PresenceDefs>(
322
327
  loadHistory: session.loadHistory,
323
328
  history: snapshot.history,
324
329
  ...(setPresence && presence ? { presence, setPresence } : {}),
325
- } as UseSessionResult<D, S, P>
330
+ } as UseSessionResult<D, V, P>
326
331
  }, [snapshot, session])
327
332
  }
328
333
 
@@ -340,11 +345,12 @@ export function useSession<
340
345
  D extends EventDefs,
341
346
  S,
342
347
  P extends PresenceDefs = Record<never, never>,
348
+ V = S,
343
349
  >(
344
- client: A2Client<D, S, P>,
350
+ client: A2Client<D, S, P, V>,
345
351
  sessionId: string,
346
352
  options?: UseSessionOptions<S>,
347
- ): UseSessionResult<D, S, P> {
353
+ ): UseSessionResult<D, V, P> {
348
354
  const { participant, hydrate } = options ?? {}
349
355
  const session = useMemo(
350
356
  () =>
package/src/reducer.ts CHANGED
@@ -20,6 +20,7 @@ export type Reducer<
20
20
  D extends EventDefs = EventDefs,
21
21
  S = unknown,
22
22
  P extends PresenceDefs = Record<never, never>,
23
+ V = S,
23
24
  > = {
24
25
  /** Identity and cache key. Rename when the fold's logic changes. */
25
26
  readonly name: string
@@ -34,7 +35,7 @@ export type Reducer<
34
35
  readonly fold: (state: S, event: ContractEvent<D>) => S
35
36
  readonly initialState: S
36
37
  /** Internal read-side projection. Never used as a fold input or persisted. */
37
- readonly [clientStateProjection]?: (state: S) => S
38
+ readonly [clientStateProjection]?: (state: S) => V
38
39
  /**
39
40
  * When present: `initialState` was validated against it at
40
41
  * definition, and any cached fold a2 rehydrates (server snapshots,
package/src/server.ts CHANGED
@@ -183,8 +183,8 @@ export type Session<
183
183
  append: Append
184
184
  schedule: SessionSchedule<D>
185
185
  history(options?: { gte?: number; lte?: number }): Promise<ContractEvent<D>[]>
186
- state<S>(
187
- reducer: Reducer<D, S>,
186
+ state<S, V = S>(
187
+ reducer: Reducer<D, S, Record<never, never>, V>,
188
188
  options?: StateOptions,
189
189
  ): Promise<{ state: S; index: number }>
190
190
  /**
@@ -980,10 +980,7 @@ export function createServer<
980
980
  }
981
981
  }
982
982
 
983
- // Snapshot plus tail is one adapter operation. A cache-path failure is
984
- // not load-bearing: fall back to the raw store, which still reports a
985
- // real storage failure.
986
- const readCachedState = async (
983
+ const readStoreState = async (
987
984
  store: A2Store,
988
985
  sessionId: string,
989
986
  reducerName: string,
@@ -1003,19 +1000,8 @@ export function createServer<
1003
1000
  : { snapshotThroughIndex }),
1004
1001
  },
1005
1002
  )
1006
- } catch {
1007
- try {
1008
- return {
1009
- headIndex: null,
1010
- snapshot: null,
1011
- events: await store.read(
1012
- nsId(sessionId),
1013
- throughIndex === undefined ? undefined : { throughIndex },
1014
- ),
1015
- }
1016
- } catch (err) {
1017
- throw asStoreUnavailable(err)
1018
- }
1003
+ } catch (err) {
1004
+ throw asStoreUnavailable(err)
1019
1005
  }
1020
1006
  }
1021
1007
 
@@ -1167,9 +1153,49 @@ export function createServer<
1167
1153
  }
1168
1154
  const persistence = Promise.resolve()
1169
1155
  .then(() =>
1170
- store.putSnapshots(nsId(plan.sessionId), reducer.name, snapshots),
1156
+ telemetry.span(
1157
+ 'a2.snapshot',
1158
+ {
1159
+ 'a2.contract': name,
1160
+ 'a2.session_id': plan.sessionId,
1161
+ 'a2.snapshot.reducer': reducer.name,
1162
+ 'a2.snapshot.index': snapshots.at(-1)!.index,
1163
+ 'a2.snapshot.count': snapshots.length,
1164
+ },
1165
+ async (span) => {
1166
+ try {
1167
+ await store.putSnapshots(
1168
+ nsId(plan.sessionId),
1169
+ reducer.name,
1170
+ snapshots,
1171
+ )
1172
+ } catch (error) {
1173
+ try {
1174
+ span.recordError(error)
1175
+ } catch (telemetryError) {
1176
+ // oxlint-disable-next-line preserve-caught-error -- AggregateError preserves both failures.
1177
+ throw new AggregateError(
1178
+ [error, telemetryError],
1179
+ 'Snapshot write and error reporting failed',
1180
+ { cause: telemetryError },
1181
+ )
1182
+ }
1183
+ }
1184
+ },
1185
+ ),
1171
1186
  )
1172
- .catch(() => {})
1187
+ .catch((error: unknown) => {
1188
+ // oxlint-disable-next-line no-console -- telemetry failure must not hide failed background work.
1189
+ console.error(
1190
+ '[a2] error in a2.snapshot telemetry',
1191
+ {
1192
+ sessionId: plan.sessionId,
1193
+ reducer: reducer.name,
1194
+ index: snapshots.at(-1)!.index,
1195
+ },
1196
+ error,
1197
+ )
1198
+ })
1173
1199
  track(persistence)
1174
1200
  platformWaitUntil(persistence)
1175
1201
  }
@@ -1187,7 +1213,7 @@ export function createServer<
1187
1213
  batch: PendingStateRead[],
1188
1214
  ): Promise<void> => {
1189
1215
  const plans = planStateReads(batch)
1190
- let reads: StoreStateRead[] | null
1216
+ let reads: (StoreStateRead | Promise<StoreStateRead>)[]
1191
1217
  try {
1192
1218
  reads = store.readStates
1193
1219
  ? await store.readStates(
@@ -1202,45 +1228,33 @@ export function createServer<
1202
1228
  })),
1203
1229
  reducer.name,
1204
1230
  )
1205
- : await Promise.all(
1206
- plans.map((plan) =>
1207
- readCachedState(
1208
- store,
1209
- plan.sessionId,
1210
- reducer.name,
1211
- plan.throughIndex,
1212
- plan.snapshotThroughIndex,
1213
- ),
1214
- ),
1215
- )
1216
- } catch {
1217
- reads = null
1218
- }
1219
- if (!reads || reads.length !== plans.length) {
1220
- try {
1221
- reads = await Promise.all(
1222
- plans.map((plan) =>
1223
- readCachedState(
1231
+ : plans.map((plan) =>
1232
+ readStoreState(
1224
1233
  store,
1225
1234
  plan.sessionId,
1226
1235
  reducer.name,
1227
1236
  plan.throughIndex,
1228
1237
  plan.snapshotThroughIndex,
1229
1238
  ),
1230
- ),
1239
+ )
1240
+ if (reads.length !== plans.length)
1241
+ throw new TypeError(
1242
+ 'store.readStates must return one result per request',
1231
1243
  )
1232
- } catch (err) {
1233
- for (const entry of batch) entry.reject(err)
1234
- return
1235
- }
1236
- }
1237
- for (const [position, plan] of plans.entries()) {
1238
- try {
1239
- await foldStatePlan(store, reducer, plan, reads[position]!)
1240
- } catch (err) {
1241
- for (const entry of plan.entries) entry.reject(err)
1242
- }
1244
+ } catch (error) {
1245
+ const failure = asStoreUnavailable(error)
1246
+ for (const entry of batch) entry.reject(failure)
1247
+ return
1243
1248
  }
1249
+ await Promise.all(
1250
+ plans.map(async (plan, position) => {
1251
+ try {
1252
+ await foldStatePlan(store, reducer, plan, await reads[position]!)
1253
+ } catch (error) {
1254
+ for (const entry of plan.entries) entry.reject(error)
1255
+ }
1256
+ }),
1257
+ )
1244
1258
  }
1245
1259
 
1246
1260
  const enqueueStateRead = (
@@ -1282,9 +1296,9 @@ export function createServer<
1282
1296
  })
1283
1297
  })
1284
1298
 
1285
- const readState = async <S>(
1299
+ const readState = async <S, V>(
1286
1300
  sessionId: string,
1287
- reducer: Reducer<D, S>,
1301
+ reducer: Reducer<D, S, Record<never, never>, V>,
1288
1302
  throughIndex?: number,
1289
1303
  pinEventIndex?: number,
1290
1304
  ): Promise<{ state: S; index: number }> => {
@@ -1875,7 +1889,10 @@ export function createServer<
1875
1889
  schedule: makeSchedule(id, trigger),
1876
1890
  history: async (bounds?: { gte?: number; lte?: number }) =>
1877
1891
  (await readHistory(id, bounds)).map(toPublic),
1878
- state: <S>(reducer: Reducer<D, S>, stateOptions?: StateOptions) => {
1892
+ state: <S, V>(
1893
+ reducer: Reducer<D, S, Record<never, never>, V>,
1894
+ stateOptions?: StateOptions,
1895
+ ) => {
1879
1896
  const through = stateOptions?.through ?? trigger?.index
1880
1897
  if (through !== undefined && through !== 'latest') {
1881
1898
  assertStateIndex(through)
@@ -611,20 +611,21 @@ return live
611
611
  */
612
612
  const READ_STATE_LUA = `
613
613
  local head = redis.call('GET', KEYS[1])
614
+ local head_index = head and cjson.decode(head)['index'] or nil
615
+ if head and (type(head_index) ~= 'number' or head_index < 0 or head_index > 9007199254740991 or head_index ~= math.floor(head_index)) then
616
+ return redis.error_reply('invalid snapshot head index')
617
+ end
614
618
  local cap = ARGV[1]
615
619
  local snapshot = nil
616
- if head and (cap == '' or tonumber(cjson.decode(head)['index']) <= tonumber(cap)) then
620
+ if head_index and (cap == '' or head_index <= tonumber(cap)) then
617
621
  snapshot = head
618
- end
619
- local upper = cap == '' and '+inf' or cap
620
- local historical = redis.call('ZREVRANGEBYSCORE', KEYS[2], upper, '-inf', 'LIMIT', 0, 1)
621
- if #historical > 0 then
622
- if not snapshot or tonumber(cjson.decode(historical[1])['index']) > tonumber(cjson.decode(snapshot)['index']) then
623
- snapshot = historical[1]
624
- end
622
+ else
623
+ local upper = cap == '' and '+inf' or cap
624
+ local historical = redis.call('ZREVRANGEBYSCORE', KEYS[2], upper, '-inf', 'LIMIT', 0, 1)
625
+ if #historical > 0 then snapshot = historical[1] end
625
626
  end
626
627
  local frontier = redis.call('GET', KEYS[3]) or '0'
627
- return { head or '', snapshot or '', frontier }
628
+ return { head_index or '', snapshot or '', frontier }
628
629
  `
629
630
 
630
631
  // ── wire helpers ─────────────────────────────────────────────────────
@@ -1351,13 +1352,10 @@ export function createRedisStoreCore(
1351
1352
  stateOptions?.throughIndex ??
1352
1353
  '',
1353
1354
  ],
1354
- )) as [string, string, string]
1355
+ )) as [number | string, string, string]
1355
1356
  const rawHead = reply[0]
1356
1357
  const raw = reply[1]
1357
- const head =
1358
- rawHead === ''
1359
- ? null
1360
- : (JSON.parse(rawHead) as { index: number; state: unknown })
1358
+ const headIndex = rawHead === '' ? null : Number(rawHead)
1361
1359
  const parsed =
1362
1360
  raw === ''
1363
1361
  ? null
@@ -1366,8 +1364,8 @@ export function createRedisStoreCore(
1366
1364
  if (
1367
1365
  !Number.isSafeInteger(currentFrontier) ||
1368
1366
  currentFrontier < 0 ||
1369
- (head !== null &&
1370
- (!Number.isSafeInteger(head.index) || head.index < 0)) ||
1367
+ (headIndex !== null &&
1368
+ (!Number.isSafeInteger(headIndex) || headIndex < 0)) ||
1371
1369
  (parsed !== null &&
1372
1370
  (!Number.isSafeInteger(parsed.index) || parsed.index < 0))
1373
1371
  ) {
@@ -1387,7 +1385,7 @@ export function createRedisStoreCore(
1387
1385
  frontier,
1388
1386
  )
1389
1387
  return {
1390
- headIndex: head?.index ?? null,
1388
+ headIndex,
1391
1389
  snapshot: snapshot
1392
1390
  ? { index: snapshot.index, state: snapshot.state }
1393
1391
  : null,
package/src/store.ts CHANGED
@@ -270,6 +270,7 @@ export interface A2Store {
270
270
  * operation. On a cache miss, `snapshot` is null and `events` is the full
271
271
  * log. The snapshot is untrusted; core may reject it and issue a full
272
272
  * `read()` when its state schema no longer accepts the cached value.
273
+ * Operational failures propagate without falling back to a full-log read.
273
274
  */
274
275
  readState(
275
276
  sessionId: string,
package/src/telemetry.ts CHANGED
@@ -9,7 +9,8 @@
9
9
  export type A2AttributeValue = string | number | boolean
10
10
 
11
11
  /** The spans a2 emits today. The catalogue grows with the surface. */
12
- export type A2SpanName = 'a2.append' | 'a2.drain' | 'a2.event' | 'a2.state'
12
+ export type A2SpanName =
13
+ 'a2.append' | 'a2.drain' | 'a2.event' | 'a2.state' | 'a2.snapshot'
13
14
 
14
15
  export type A2SpanHandle = {
15
16
  /** Attach or update an attribute mid-span (e.g. the outcome). */
@@ -1 +0,0 @@
1
- {"version":3,"file":"ai-Cai-lCbj.d.ts","names":[],"sources":["../src/ai-control.ts","../src/ai-client-state.ts","../src/ai-projector.ts","../src/ai-progress-batches.ts","../src/ai.ts"],"mappings":"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"}