experimental-a2 0.21.1 → 0.22.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 (139) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/{actor-HnGmrOZZ.d.ts → actor-CqibLgjQ.d.ts} +13 -5
  3. package/dist/actor-CqibLgjQ.d.ts.map +1 -0
  4. package/dist/actor-client.d.ts +1 -1
  5. package/dist/actor-react.d.ts +1 -1
  6. package/dist/actor-react.js +1 -1
  7. package/dist/actor-shared-DFET5q16.js.map +1 -1
  8. package/dist/actor.d.ts +1 -1
  9. package/dist/actor.js +3 -3
  10. package/dist/{ai-CvEnqVfo.d.ts → ai-DAu77CDr.d.ts} +2 -2
  11. package/dist/{ai-CvEnqVfo.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
  12. package/dist/{ai-CzE672Q3.js → ai-EEfKyc-z.js} +2 -2
  13. package/dist/{ai-CzE672Q3.js.map → ai-EEfKyc-z.js.map} +1 -1
  14. package/dist/ai-client.d.ts +1 -1
  15. package/dist/ai-client.js +2 -2
  16. package/dist/ai-fx.d.ts +4 -2
  17. package/dist/ai-fx.d.ts.map +1 -1
  18. package/dist/ai-fx.js +211 -61
  19. package/dist/ai-fx.js.map +1 -1
  20. package/dist/{ai-harness-Bfk-Lycn.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
  21. package/dist/{ai-harness-Bfk-Lycn.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
  22. package/dist/ai-harness-dQeJaR1t.js.map +1 -1
  23. package/dist/ai-pi.d.ts +1 -1
  24. package/dist/ai-pi.js +1 -1
  25. package/dist/ai-react.d.ts +1 -1
  26. package/dist/ai-react.js +2 -2
  27. package/dist/ai-server.d.ts +3 -3
  28. package/dist/ai-server.d.ts.map +1 -1
  29. package/dist/ai-server.js +10 -4
  30. package/dist/ai-server.js.map +1 -1
  31. package/dist/ai.d.ts +1 -1
  32. package/dist/ai.js +1 -1
  33. package/dist/{client-9IeTsiAs.js → client-BQS4i_V9.js} +27 -4
  34. package/dist/client-BQS4i_V9.js.map +1 -0
  35. package/dist/client-CUMbS_38.d.ts.map +1 -1
  36. package/dist/client.js +1 -1
  37. package/dist/index.d.ts +2 -2
  38. package/dist/{internal-Bc4PvfHk.js → internal-CvsrprnU.js} +32 -15
  39. package/dist/{internal-Bc4PvfHk.js.map → internal-CvsrprnU.js.map} +1 -1
  40. package/dist/react.js +1 -1
  41. package/dist/{runtime-consumer-BWmht60z.js → runtime-consumer-CIeL33wj.js} +2 -2
  42. package/dist/{runtime-consumer-BWmht60z.js.map → runtime-consumer-CIeL33wj.js.map} +1 -1
  43. package/dist/scheduler-qstash.d.ts +2 -2
  44. package/dist/scheduler-qstash.js +2 -2
  45. package/dist/scheduler-vercel.d.ts +2 -2
  46. package/dist/scheduler-vercel.js +2 -2
  47. package/dist/{server-DvIB8kj5.js → server-DtFrWxi3.js} +30 -8
  48. package/dist/server-DtFrWxi3.js.map +1 -0
  49. package/dist/{server-BshFZ0eL.d.ts → server-qgez0Xny.d.ts} +3 -3
  50. package/dist/{server-BshFZ0eL.d.ts.map → server-qgez0Xny.d.ts.map} +1 -1
  51. package/dist/server.d.ts +2 -2
  52. package/dist/server.js +1 -1
  53. package/dist/{session-clear-Bk_29eEw.d.ts → session-clear-D1PFe7xl.d.ts} +2 -2
  54. package/dist/session-clear-D1PFe7xl.d.ts.map +1 -0
  55. package/dist/{store-Z2qjw50n.d.ts → store-D0MhBvg6.d.ts} +15 -6
  56. package/dist/store-D0MhBvg6.d.ts.map +1 -0
  57. package/dist/store-N8PXxDAS.js.map +1 -1
  58. package/dist/store-memory.d.ts +1 -1
  59. package/dist/store-memory.d.ts.map +1 -1
  60. package/dist/store-memory.js +15 -17
  61. package/dist/store-memory.js.map +1 -1
  62. package/dist/{store-polling-DYNyxCev.js → store-polling-BZt2Rf3D.js} +2 -2
  63. package/dist/{store-polling-DYNyxCev.js.map → store-polling-BZt2Rf3D.js.map} +1 -1
  64. package/dist/store-postgres.d.ts +1 -1
  65. package/dist/store-postgres.d.ts.map +1 -1
  66. package/dist/store-postgres.js +31 -40
  67. package/dist/store-postgres.js.map +1 -1
  68. package/dist/store-presence-polling-JuAy2Ck2.js +111 -0
  69. package/dist/store-presence-polling-JuAy2Ck2.js.map +1 -0
  70. package/dist/store-redis-http.d.ts +1 -1
  71. package/dist/store-redis-http.js +3 -3
  72. package/dist/{store-redis-notify-IiVdlYHc.js → store-redis-notify-CsJw_hfD.js} +538 -397
  73. package/dist/store-redis-notify-CsJw_hfD.js.map +1 -0
  74. package/dist/store-redis.d.ts +1 -1
  75. package/dist/store-redis.d.ts.map +1 -1
  76. package/dist/store-redis.js +3 -4
  77. package/dist/store-redis.js.map +1 -1
  78. package/dist/store-sqlite.d.ts +1 -1
  79. package/dist/store-sqlite.d.ts.map +1 -1
  80. package/dist/store-sqlite.js +16 -18
  81. package/dist/store-sqlite.js.map +1 -1
  82. package/dist/stream-buffer-DrTyiIuo.js +57 -0
  83. package/dist/stream-buffer-DrTyiIuo.js.map +1 -0
  84. package/dist/{wire-CZGqNZRx.js → wire-CV-Z7wz-.js} +3 -3
  85. package/dist/{wire-CZGqNZRx.js.map → wire-CV-Z7wz-.js.map} +1 -1
  86. package/docs/actors/01-introduction.mdx +5 -2
  87. package/docs/agents/03-conversations.mdx +9 -8
  88. package/docs/agents/04-harnesses.mdx +7 -4
  89. package/docs/reference/01-core.mdx +3 -2
  90. package/docs/reference/02-server.mdx +5 -1
  91. package/docs/reference/04-ai.mdx +13 -4
  92. package/docs/reference/06-errors.mdx +1 -1
  93. package/docs/server/01-consumers.mdx +4 -1
  94. package/docs/server/06-guarantees.mdx +7 -4
  95. package/docs/stores/02-redis.mdx +16 -10
  96. package/docs/stores/03-redis-http.mdx +6 -4
  97. package/docs/stores/04-postgres.mdx +2 -2
  98. package/docs/upgrades/01-overview.mdx +1 -0
  99. package/docs/upgrades/78-0-22.mdx +115 -0
  100. package/examples/playground/app/a0/sandbox.ts +58 -13
  101. package/examples/playground/app/a2slack/db/a2.ts +7 -4
  102. package/examples/playground/app/a2slack/db/migrations/0004_a2_session_event_ids.sql +2 -0
  103. package/examples/playground/app/a2slack/db/migrations/meta/0004_snapshot.json +1165 -0
  104. package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +8 -1
  105. package/examples/playground/app/chat/server.ts +2 -2
  106. package/examples/playground/lib/store.ts +1 -1
  107. package/examples/playground/package.json +1 -1
  108. package/package.json +1 -1
  109. package/sql/postgres/drizzle-schema.ts +7 -4
  110. package/sql/postgres/schema.sql +24 -3
  111. package/src/actor-shared.ts +10 -2
  112. package/src/ai-agent.ts +13 -0
  113. package/src/ai-fx.ts +321 -77
  114. package/src/ai-harness.ts +7 -0
  115. package/src/client.ts +19 -2
  116. package/src/deterministic-id.ts +1 -1
  117. package/src/internal.ts +42 -11
  118. package/src/presence-recovery.ts +18 -11
  119. package/src/presence-tombstones.ts +52 -0
  120. package/src/push-envelope.ts +2 -2
  121. package/src/server.ts +49 -15
  122. package/src/store-memory.ts +24 -32
  123. package/src/store-postgres.ts +43 -53
  124. package/src/store-presence-polling.ts +59 -34
  125. package/src/store-redis-core.ts +595 -439
  126. package/src/store-redis-notify.ts +74 -26
  127. package/src/store-redis.ts +2 -3
  128. package/src/store-sqlite.ts +34 -40
  129. package/src/store.ts +14 -5
  130. package/dist/actor-HnGmrOZZ.d.ts.map +0 -1
  131. package/dist/client-9IeTsiAs.js.map +0 -1
  132. package/dist/server-DvIB8kj5.js.map +0 -1
  133. package/dist/session-clear-Bk_29eEw.d.ts.map +0 -1
  134. package/dist/store-Z2qjw50n.d.ts.map +0 -1
  135. package/dist/store-presence-polling-CzQ0wdtM.js +0 -94
  136. package/dist/store-presence-polling-CzQ0wdtM.js.map +0 -1
  137. package/dist/store-redis-notify-IiVdlYHc.js.map +0 -1
  138. package/dist/stream-buffer-Cx7AAPGq.js +0 -23
  139. package/dist/stream-buffer-Cx7AAPGq.js.map +0 -1
@@ -29,6 +29,13 @@
29
29
  "when": 1792282388330,
30
30
  "tag": "0003_a2_batch",
31
31
  "breakpoints": true
32
+ },
33
+ {
34
+ "idx": 4,
35
+ "version": "7",
36
+ "when": 1792693132589,
37
+ "tag": "0004_a2_session_event_ids",
38
+ "breakpoints": true
32
39
  }
33
40
  ]
34
- }
41
+ }
@@ -136,8 +136,8 @@ const forwardToAgent = consumer({
136
136
  ? 'mention'
137
137
  : 'passive'
138
138
  const message: ChatAgentMessage = {
139
- // Event ids are unique across a store's sessions, so the forward
140
- // derives its own; a retried forward converges on the same id.
139
+ // Derived from the room event's id, so a retried forward converges on
140
+ // the same message in the agent's session.
141
141
  id: await idempotentId('chat.message', context.event.id),
142
142
  role: 'user',
143
143
  metadata: {
@@ -9,7 +9,7 @@ const postgresUrl = process.env.DATABASE_URL?.replace(
9
9
  )
10
10
 
11
11
  export const store: RedisStore | PostgresStore | undefined = redisUrl
12
- ? redis({ url: redisUrl })
12
+ ? redis({ url: redisUrl, keyPrefix: 'a2-playground-tagged' })
13
13
  : postgresUrl
14
14
  ? postgres({ connectionString: postgresUrl })
15
15
  : undefined
@@ -39,7 +39,7 @@
39
39
  "ai": "^7.0.58",
40
40
  "codemirror": "^6.0.2",
41
41
  "drizzle-orm": "^0.45.3",
42
- "experimental-a2": "0.21.1",
42
+ "experimental-a2": "0.22.0",
43
43
  "geist": "^1.7.2",
44
44
  "ioredis": "^5.9.0",
45
45
  "libfx": "0.0.12",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "experimental-a2",
3
- "version": "0.21.1",
3
+ "version": "0.22.0",
4
4
  "description": "Syncs your immutable event log, from server to browser: one contract, handlers, derived state, and a live client per session.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -7,9 +7,9 @@
7
7
  * your schema.
8
8
  *
9
9
  * Whatever creates them, A2 relies on the table and column names, the column
10
- * types, the primary keys, and the unique index on a2_events.event_id. The
11
- * other indexes keep its queries fast. Every name is spelled out, so Drizzle's
12
- * `casing` setting changes none of them.
10
+ * types, the primary keys, and the unique index on a2_events (session_id,
11
+ * event_id). The other indexes keep its queries fast. Every name is spelled
12
+ * out, so Drizzle's `casing` setting changes none of them.
13
13
  *
14
14
  * A2's tables change only when a release's changelog says so; compare your
15
15
  * copy with that release's file. Drizzle can't declare functions or
@@ -48,7 +48,10 @@ export const a2Events = pgTable(
48
48
  name: 'a2_events_pkey',
49
49
  columns: [table.sessionId, table.idx],
50
50
  }),
51
- uniqueIndex('a2_events_event_id').on(table.eventId),
51
+ uniqueIndex('a2_events_session_event_id').on(
52
+ table.sessionId,
53
+ table.eventId,
54
+ ),
52
55
  index('a2_events_types').on(table.sessionId, table.eventType, table.idx),
53
56
  ],
54
57
  )
@@ -254,9 +254,6 @@ end $$;
254
254
  --> statement-breakpoint
255
255
 
256
256
  do $$ begin
257
- if to_regclass('a2_events_event_id') is null then
258
- create unique index if not exists a2_events_event_id on a2_events (event_id);
259
- end if;
260
257
  if to_regclass('a2_events_types') is null then
261
258
  create index if not exists a2_events_types
262
259
  on a2_events (session_id, event_type, idx);
@@ -274,3 +271,27 @@ do $$ begin
274
271
  on a2_snapshot_pins (session_id, reducer_name, up_to_index);
275
272
  end if;
276
273
  end $$;
274
+ --> statement-breakpoint
275
+
276
+ -- Event ids are unique per session, through the index on (session_id,
277
+ -- event_id). It replaces a2_events_event_id, on event_id alone. The swap
278
+ -- locks the table before it builds, so it waits for running queries instead
279
+ -- of deadlocking with a writer when the whole file runs as one transaction.
280
+ -- The old index goes only once the new one is valid, so a session's ids
281
+ -- always have a unique index.
282
+ do $$ begin
283
+ if to_regclass('a2_events_session_event_id') is null then
284
+ if to_regclass('a2_events_event_id') is not null then
285
+ lock table a2_events in access exclusive mode;
286
+ end if;
287
+ create unique index if not exists a2_events_session_event_id
288
+ on a2_events (session_id, event_id);
289
+ end if;
290
+ if to_regclass('a2_events_event_id') is not null and exists (
291
+ select 1 from pg_index
292
+ where indexrelid = to_regclass('a2_events_session_event_id')
293
+ and indisvalid and indisunique
294
+ ) then
295
+ drop index a2_events_event_id;
296
+ end if;
297
+ end $$;
@@ -131,7 +131,11 @@ export type ActorScheduleSend<E> = {
131
131
  export type ActorContext<D extends ActorProtocol> = {
132
132
  /** Mutable draft, committed atomically with the handler's completion. */
133
133
  state: D['state']
134
- /** The message id — stable across re-runs; the idempotency key for external I/O. */
134
+ /**
135
+ * The message id — stable across re-runs and unique within the instance;
136
+ * the idempotency key for external I/O. An explicit send id that repeats
137
+ * in another instance repeats here too.
138
+ */
135
139
  id: string
136
140
  /** Durable 1-based dispatch ordinal of this message. */
137
141
  attempt: number
@@ -150,7 +154,11 @@ export type ActorContext<D extends ActorProtocol> = {
150
154
  * whose serial handlers decide against fresh state.
151
155
  */
152
156
  export type ActorConcurrentContext<D extends ActorProtocol> = {
153
- /** The message id — stable across re-runs; the idempotency key for external I/O. */
157
+ /**
158
+ * The message id — stable across re-runs and unique within the instance;
159
+ * the idempotency key for external I/O. An explicit send id that repeats
160
+ * in another instance repeats here too.
161
+ */
154
162
  id: string
155
163
  /** Durable 1-based dispatch ordinal of this message. */
156
164
  attempt: number
package/src/ai-agent.ts CHANGED
@@ -75,6 +75,7 @@ import type {
75
75
  import { A2Error } from './errors.ts'
76
76
  import {
77
77
  consumeSchedulerSendFailure,
78
+ drainSettledHooks,
78
79
  failedPreviousAttempt,
79
80
  installAmbientToolScopeStorage,
80
81
  type AmbientToolScope,
@@ -2148,6 +2149,7 @@ export function createConsumers<
2148
2149
  const source =
2149
2150
  harness !== undefined
2150
2151
  ? await harness.step({
2152
+ scope: states,
2151
2153
  agentName: options.contract.name,
2152
2154
  sessionId: ctx.event.sessionId,
2153
2155
  requestIndex: ctx.event.index,
@@ -2556,6 +2558,17 @@ export function agent(
2556
2558
  consumers: [...builtIns, ...(serverOptions.consumers ?? [])],
2557
2559
  })
2558
2560
  registerHarnessStates(server, states)
2561
+ const driver =
2562
+ harness === undefined || harness.type === 'ai-sdk'
2563
+ ? undefined
2564
+ : harnessDriver(harness)
2565
+ const release = driver?.release
2566
+ // What a harness kept for a session's next step goes once nothing is left
2567
+ // to run for the session here.
2568
+ if (release !== undefined)
2569
+ drainSettledHooks.set(server, (sessionId) =>
2570
+ release({ scope: states, sessionId }),
2571
+ )
2559
2572
  const applicationEvents = 'contract' in rest
2560
2573
  setServerFetchHooks(server, {
2561
2574
  validateIngress: (context) =>