experimental-a2 0.3.0 → 0.5.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 (205) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/dist/ai-CFNeCrRl.d.ts +334 -0
  3. package/dist/ai-CFNeCrRl.d.ts.map +1 -0
  4. package/dist/ai-server.browser.js +2 -0
  5. package/dist/ai-server.browser.js.map +1 -0
  6. package/dist/ai-server.d.ts +49 -9
  7. package/dist/ai-server.d.ts.map +1 -0
  8. package/dist/ai-server.js +123 -49
  9. package/dist/ai-server.js.map +1 -0
  10. package/dist/ai.d.ts +2 -303
  11. package/dist/ai.js +233 -86
  12. package/dist/ai.js.map +1 -0
  13. package/dist/cli-B3VuxoDe.js +599 -0
  14. package/dist/cli-B3VuxoDe.js.map +1 -0
  15. package/dist/cli-bin.d.ts +1 -0
  16. package/dist/cli-bin.js +7 -0
  17. package/dist/cli-bin.js.map +1 -0
  18. package/dist/cli.d.ts +20 -0
  19. package/dist/cli.d.ts.map +1 -0
  20. package/dist/cli.js +2 -0
  21. package/dist/client-BKlyLiOU.js +1008 -0
  22. package/dist/client-BKlyLiOU.js.map +1 -0
  23. package/dist/client-D7mvIXrF.d.ts +191 -0
  24. package/dist/client-D7mvIXrF.d.ts.map +1 -0
  25. package/dist/client.d.ts +2 -90
  26. package/dist/client.js +1 -409
  27. package/dist/{contract-CG_adnu_.js → contract-48bUMgcL.js} +12 -2
  28. package/dist/contract-48bUMgcL.js.map +1 -0
  29. package/dist/{contract-C_3dIIEU.d.ts → contract-jIfaR085.d.ts} +63 -8
  30. package/dist/contract-jIfaR085.d.ts.map +1 -0
  31. package/dist/devtools-J_jZ2vQf.d.ts +152 -0
  32. package/dist/devtools-J_jZ2vQf.d.ts.map +1 -0
  33. package/dist/devtools-kJJaORn-.js +340 -0
  34. package/dist/devtools-kJJaORn-.js.map +1 -0
  35. package/dist/devtools-server.browser.js +3 -1
  36. package/dist/devtools-server.browser.js.map +1 -0
  37. package/dist/devtools-server.d.ts +4 -3
  38. package/dist/devtools-server.d.ts.map +1 -0
  39. package/dist/devtools-server.js +226 -43
  40. package/dist/devtools-server.js.map +1 -0
  41. package/dist/devtools.d.ts +2 -0
  42. package/dist/devtools.js +2 -0
  43. package/dist/{errors-BJRMd-h6.js → errors-BQuJpe82.js} +6 -4
  44. package/dist/errors-BQuJpe82.js.map +1 -0
  45. package/dist/{errors-xL_JTXsY.d.ts → errors-W6nwJ-fm.d.ts} +3 -2
  46. package/dist/errors-W6nwJ-fm.d.ts.map +1 -0
  47. package/dist/http.d.ts +135 -28
  48. package/dist/http.d.ts.map +1 -0
  49. package/dist/http.js +647 -61
  50. package/dist/http.js.map +1 -0
  51. package/dist/{idempotent-replay-BMyHrP0L.js → idempotent-replay-DuqEkYA7.js} +4 -2
  52. package/dist/idempotent-replay-DuqEkYA7.js.map +1 -0
  53. package/dist/index.d.ts +5 -5
  54. package/dist/index.js +2 -2
  55. package/dist/{inspection-E7qbD0Xj.js → inspection-DaxB5jM2.js} +4 -1
  56. package/dist/inspection-DaxB5jM2.js.map +1 -0
  57. package/dist/internal-DstsI6Re.js +211 -0
  58. package/dist/internal-DstsI6Re.js.map +1 -0
  59. package/dist/otel.d.ts +3 -2
  60. package/dist/otel.d.ts.map +1 -0
  61. package/dist/otel.js +2 -0
  62. package/dist/otel.js.map +1 -0
  63. package/dist/platform-B4TnJtWu.js +34 -0
  64. package/dist/platform-B4TnJtWu.js.map +1 -0
  65. package/dist/react.d.ts +47 -15
  66. package/dist/react.d.ts.map +1 -0
  67. package/dist/react.js +30 -15
  68. package/dist/react.js.map +1 -0
  69. package/dist/retryable-lazy-DZWmHpii.js +2 -0
  70. package/dist/retryable-lazy-DZWmHpii.js.map +1 -0
  71. package/dist/scheduler-qstash.d.ts +79 -0
  72. package/dist/scheduler-qstash.d.ts.map +1 -0
  73. package/dist/scheduler-qstash.js +501 -0
  74. package/dist/scheduler-qstash.js.map +1 -0
  75. package/dist/scheduler-task-BpzhPnRS.js +56 -0
  76. package/dist/scheduler-task-BpzhPnRS.js.map +1 -0
  77. package/dist/{recovery-vercel.d.ts → scheduler-vercel.d.ts} +18 -24
  78. package/dist/scheduler-vercel.d.ts.map +1 -0
  79. package/dist/scheduler-vercel.js +228 -0
  80. package/dist/scheduler-vercel.js.map +1 -0
  81. package/dist/server-C72KOw51.js +1457 -0
  82. package/dist/server-C72KOw51.js.map +1 -0
  83. package/dist/server-DUF9pjsx.d.ts +261 -0
  84. package/dist/server-DUF9pjsx.d.ts.map +1 -0
  85. package/dist/server.browser.js +2 -0
  86. package/dist/server.browser.js.map +1 -0
  87. package/dist/server.d.ts +4 -155
  88. package/dist/server.js +2 -2
  89. package/dist/{log-ldf5g8Cx.d.ts → store-DysUkTH3.d.ts} +120 -35
  90. package/dist/store-DysUkTH3.d.ts.map +1 -0
  91. package/dist/{log-yJbXUf72.js → store-N8PXxDAS.js} +3 -1
  92. package/dist/store-N8PXxDAS.js.map +1 -0
  93. package/dist/store-codec-DTG0Ftek.js +10 -0
  94. package/dist/store-codec-DTG0Ftek.js.map +1 -0
  95. package/dist/store-memory.d.ts +12 -0
  96. package/dist/store-memory.d.ts.map +1 -0
  97. package/dist/{log-memory.js → store-memory.js} +145 -34
  98. package/dist/store-memory.js.map +1 -0
  99. package/dist/{log-polling-DZ1MiKLg.js → store-polling-dSeLxzfb.js} +7 -5
  100. package/dist/store-polling-dSeLxzfb.js.map +1 -0
  101. package/dist/{log-postgres.d.ts → store-postgres.d.ts} +7 -6
  102. package/dist/store-postgres.d.ts.map +1 -0
  103. package/dist/{log-postgres.js → store-postgres.js} +214 -24
  104. package/dist/store-postgres.js.map +1 -0
  105. package/dist/{log-redis-core-CyJ5L8yR.js → store-redis-core-BFLwz0Wj.js} +271 -66
  106. package/dist/store-redis-core-BFLwz0Wj.js.map +1 -0
  107. package/dist/{log-redis-http.d.ts → store-redis-http.d.ts} +8 -7
  108. package/dist/store-redis-http.d.ts.map +1 -0
  109. package/dist/{log-redis-http.js → store-redis-http.js} +22 -12
  110. package/dist/store-redis-http.js.map +1 -0
  111. package/dist/{log-redis.d.ts → store-redis.d.ts} +7 -6
  112. package/dist/store-redis.d.ts.map +1 -0
  113. package/dist/{log-redis.js → store-redis.js} +101 -39
  114. package/dist/store-redis.js.map +1 -0
  115. package/dist/{log-sqlite.d.ts → store-sqlite.d.ts} +7 -6
  116. package/dist/store-sqlite.d.ts.map +1 -0
  117. package/dist/{log-sqlite.js → store-sqlite.js} +118 -22
  118. package/dist/store-sqlite.js.map +1 -0
  119. package/dist/{telemetry-Cso0qyHQ.d.ts → telemetry-BjYHTfh2.d.ts} +3 -2
  120. package/dist/telemetry-BjYHTfh2.d.ts.map +1 -0
  121. package/dist/testing.browser.d.ts +1 -0
  122. package/dist/testing.browser.js +6 -0
  123. package/dist/testing.browser.js.map +1 -0
  124. package/dist/testing.d.ts +32 -0
  125. package/dist/testing.d.ts.map +1 -0
  126. package/dist/testing.js +103 -0
  127. package/dist/testing.js.map +1 -0
  128. package/dist/validate-XKT4FSNn.js +2 -0
  129. package/dist/validate-XKT4FSNn.js.map +1 -0
  130. package/dist/wire-BFQmSJ-9.js +284 -0
  131. package/dist/wire-BFQmSJ-9.js.map +1 -0
  132. package/docs/01-quickstart.mdx +3 -3
  133. package/docs/concepts/01-contracts.mdx +18 -13
  134. package/docs/concepts/02-handlers.mdx +7 -7
  135. package/docs/concepts/03-durability.mdx +21 -21
  136. package/docs/concepts/04-state.mdx +18 -21
  137. package/docs/guides/01-timers.mdx +154 -49
  138. package/docs/guides/02-cancellation.mdx +30 -4
  139. package/docs/guides/03-react.mdx +76 -57
  140. package/docs/guides/04-local-first.mdx +1 -1
  141. package/docs/guides/05-production.mdx +310 -61
  142. package/docs/guides/06-ai-agents.mdx +252 -74
  143. package/docs/guides/07-devtools.mdx +137 -12
  144. package/docs/guides/08-application-data.mdx +1 -1
  145. package/docs/guides/09-presence.mdx +263 -0
  146. package/docs/guides/10-transports.mdx +140 -0
  147. package/docs/index.mdx +18 -14
  148. package/docs/reference/01-api.mdx +839 -110
  149. package/docs/reference/02-errors.mdx +13 -6
  150. package/package.json +24 -7
  151. package/src/ai-coordinator.ts +358 -0
  152. package/src/ai-projector.ts +524 -0
  153. package/src/ai-sdk-step.ts +261 -0
  154. package/src/ai-server.browser.ts +5 -0
  155. package/src/ai-server.ts +1719 -0
  156. package/src/ai.ts +2155 -0
  157. package/src/cache-indexeddb.ts +10 -0
  158. package/src/cli-bin.ts +5 -0
  159. package/src/cli.ts +1046 -0
  160. package/src/client.ts +1826 -0
  161. package/src/contract.ts +206 -0
  162. package/src/deterministic-id.ts +72 -0
  163. package/src/devtools-app.ts +989 -0
  164. package/src/devtools-server.browser.ts +5 -0
  165. package/src/devtools-server.ts +604 -0
  166. package/src/devtools.ts +716 -0
  167. package/src/errors.ts +50 -0
  168. package/src/http.ts +394 -0
  169. package/src/idempotent-replay.ts +53 -0
  170. package/src/index.ts +37 -0
  171. package/src/inspection.ts +39 -0
  172. package/src/internal.ts +426 -0
  173. package/src/otel.ts +59 -0
  174. package/src/platform.ts +60 -0
  175. package/src/push-envelope.ts +137 -0
  176. package/src/react.ts +284 -0
  177. package/src/reducer.ts +108 -0
  178. package/src/retryable-lazy.ts +27 -0
  179. package/src/scheduler-qstash.ts +915 -0
  180. package/src/scheduler-task.ts +106 -0
  181. package/src/scheduler-vercel.ts +437 -0
  182. package/src/server.browser.ts +12 -0
  183. package/src/server.ts +2700 -0
  184. package/src/session-socket.ts +548 -0
  185. package/src/sse.ts +141 -0
  186. package/src/standard-schema.ts +77 -0
  187. package/src/store-codec.ts +10 -0
  188. package/src/store-memory.ts +788 -0
  189. package/src/store-polling.ts +102 -0
  190. package/src/store-postgres.ts +1212 -0
  191. package/src/store-redis-core.ts +1494 -0
  192. package/src/store-redis-http.ts +116 -0
  193. package/src/store-redis.ts +458 -0
  194. package/src/store-sqlite.ts +1108 -0
  195. package/src/store.ts +385 -0
  196. package/src/telemetry.ts +47 -0
  197. package/src/testing.browser.ts +5 -0
  198. package/src/testing.ts +185 -0
  199. package/src/validate.ts +39 -0
  200. package/src/wire.ts +454 -0
  201. package/dist/internal-gCd5qMry.js +0 -44
  202. package/dist/log-memory.d.ts +0 -11
  203. package/dist/recovery-vercel.js +0 -119
  204. package/dist/server-BcLa4RFL.js +0 -877
  205. package/dist/wire-BVsgR8o9.js +0 -62
@@ -36,11 +36,15 @@ export async function POST(req: Request) {
36
36
  | ------------------------- | ------------------------------------------------------------------ | --------- |
37
37
  | `INVALID_PAYLOAD` | a payload fails its schema; nothing was written, and `details` carries the issues ([Standard Schema](https://standardschema.dev) format) | no |
38
38
  | `UNKNOWN_EVENT_TYPE` | an event type isn't in the contract's `events` map | no |
39
+ | `UNKNOWN_PRESENCE_FIELD` | a presence field isn't in the contract's `presence` map | no |
40
+ | `PRESENCE_NOT_SUPPORTED` | the contract declares `presence` but the configured store has no presence capability (thrown at construction; for environment-resolved stores, deferred to the first store use) | no |
39
41
  | `PARTIAL_DUPLICATE_BATCH` | a batch mixed already-appended and fresh events | no |
40
- | `LOG_UNAVAILABLE` | the log backend failed; nothing was written, original error as `cause` | **yes** |
41
- | `LOG_NOT_CONFIGURED` | production boot with no `log` configured (during `next build` page collection, deferred to first use) | no |
42
+ | `SUPERSEDED_ATTEMPT` | a handler append from an attempt a recovery claim has replaced; nothing was written, the successor owns the event. Also the abort reason on that handler's `ctx.signal` | no |
43
+ | `CLAIM_EXPIRED` | never thrown; only the `ctx.signal` abort reason when the attempt's lease lapsed without renewal (assumed loss, decided locally, not proof) | no |
44
+ | `STORE_UNAVAILABLE` | the store backend failed; nothing was written, original error as `cause` | **yes** |
45
+ | `STORE_NOT_CONFIGURED` | production boot with no `store` configured (during `next build` page collection, deferred to first use) | no |
42
46
 
43
- Retryability is derivable from the code. `LOG_UNAVAILABLE` is the only one
47
+ Retryability is derivable from the code. `STORE_UNAVAILABLE` is the only one
44
48
  worth retrying; the rest are deterministic caller bugs, and retrying them
45
49
  is wasted work.
46
50
 
@@ -69,9 +73,12 @@ The push route serializes an `A2Error` as:
69
73
  ```
70
74
 
71
75
  with a mapped status: `INVALID_PAYLOAD`, `UNKNOWN_EVENT_TYPE`, and
72
- `PARTIAL_DUPLICATE_BATCH` are 400; `LOG_UNAVAILABLE` is 503.
76
+ `PARTIAL_DUPLICATE_BATCH` are 400; `SUPERSEDED_ATTEMPT` is 409;
77
+ `STORE_UNAVAILABLE` is 503.
73
78
 
74
79
  The client's `push` deserializes the body back into an `A2Error`, so client
75
80
  and server code branch on identical codes. `push` auto-retries only
76
- `LOG_UNAVAILABLE`. The serializer pair ships in `experimental-a2/http`, alongside
77
- `parsePushBody` and `sseResponse`.
81
+ `STORE_UNAVAILABLE`. The serializer pair, `errorResponse` and
82
+ `deserializeError`, ships in `experimental-a2/http` alongside `handle`;
83
+ a `before` hook that wants the wire's own error shapes returns
84
+ `errorResponse(new A2Error(...))`.
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "experimental-a2",
3
- "version": "0.3.0",
3
+ "version": "0.5.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",
7
+ "bin": {
8
+ "a2": "./dist/cli-bin.js"
9
+ },
7
10
  "sideEffects": false,
8
11
  "engines": {
9
12
  "node": ">=22.13"
@@ -20,6 +23,7 @@
20
23
  "files": [
21
24
  "dist",
22
25
  "docs",
26
+ "src",
23
27
  "CHANGELOG.md"
24
28
  ],
25
29
  "exports": {
@@ -36,22 +40,29 @@
36
40
  "default": "./dist/ai-server.js"
37
41
  },
38
42
  "./http": "./dist/http.js",
39
- "./log-memory": "./dist/log-memory.js",
40
- "./log-sqlite": "./dist/log-sqlite.js",
41
- "./log-postgres": "./dist/log-postgres.js",
42
- "./log-redis": "./dist/log-redis.js",
43
- "./log-redis-http": "./dist/log-redis-http.js",
44
- "./recovery-vercel": "./dist/recovery-vercel.js",
43
+ "./store-memory": "./dist/store-memory.js",
44
+ "./store-sqlite": "./dist/store-sqlite.js",
45
+ "./store-postgres": "./dist/store-postgres.js",
46
+ "./store-redis": "./dist/store-redis.js",
47
+ "./store-redis-http": "./dist/store-redis-http.js",
48
+ "./scheduler-qstash": "./dist/scheduler-qstash.js",
49
+ "./scheduler-vercel": "./dist/scheduler-vercel.js",
45
50
  "./cache-indexeddb": "./dist/cache-indexeddb.js",
46
51
  "./otel": "./dist/otel.js",
52
+ "./devtools": "./dist/devtools.js",
47
53
  "./devtools/server": {
48
54
  "browser": "./dist/devtools-server.browser.js",
49
55
  "default": "./dist/devtools-server.js"
50
56
  },
57
+ "./testing": {
58
+ "browser": "./dist/testing.browser.js",
59
+ "default": "./dist/testing.js"
60
+ },
51
61
  "./package.json": "./package.json"
52
62
  },
53
63
  "peerDependencies": {
54
64
  "@opentelemetry/api": "^1.9.0",
65
+ "@upstash/qstash": "^2.11.0",
55
66
  "@vercel/queue": "*",
56
67
  "ai": "^7.0.0",
57
68
  "ioredis": "^5.4.0",
@@ -62,6 +73,9 @@
62
73
  "@opentelemetry/api": {
63
74
  "optional": true
64
75
  },
76
+ "@upstash/qstash": {
77
+ "optional": true
78
+ },
65
79
  "@vercel/queue": {
66
80
  "optional": true
67
81
  },
@@ -80,12 +94,15 @@
80
94
  },
81
95
  "devDependencies": {
82
96
  "@electric-sql/pglite": "^0.3.14",
97
+ "@upstash/qstash": "^2.11.3",
83
98
  "ai": "^7.0.58",
84
99
  "ioredis": "^5.9.0",
85
100
  "@opentelemetry/api": "^1.9.0",
86
101
  "@types/pg": "^8.15.0",
102
+ "@types/ws": "^8.18.1",
87
103
  "@vercel/queue": "^0.2.0",
88
104
  "pg": "^8.16.0",
105
+ "ws": "^8.21.3",
89
106
  "@testing-library/react": "^16.1.0",
90
107
  "@types/react": "^19.0.0",
91
108
  "esbuild": "^0.25.0",
@@ -0,0 +1,358 @@
1
+ import type { UIMessage } from 'ai'
2
+ import type {
3
+ AIEventDefs,
4
+ ApprovalRequestedPayload,
5
+ ApprovalRespondedPayload,
6
+ GenerationCompletedPayload,
7
+ GenerationFailedPayload,
8
+ GenerationRequestedPayload,
9
+ GenerationStartedPayload,
10
+ InputRequestedPayload,
11
+ MessageCreatedPayload,
12
+ MessageInterruptedPayload,
13
+ ToolCalledPayload,
14
+ ToolResultPayload,
15
+ } from './ai.ts'
16
+ import type { Contract, ContractEvent, EventDefs } from './contract.ts'
17
+ import type { Reducer } from './reducer.ts'
18
+
19
+ export type CoordinatorCall = {
20
+ index: number
21
+ call: ToolCalledPayload
22
+ approval?: ApprovalRequestedPayload
23
+ response?: ApprovalRespondedPayload
24
+ responseIndex?: number
25
+ terminal: boolean
26
+ }
27
+
28
+ export type CoordinatorResponse = {
29
+ rootMessageId: string
30
+ responseMessageId: string
31
+ status: 'requested' | 'generating' | 'waiting' | 'failed'
32
+ activeRequestId?: string
33
+ generation?: GenerationStartedPayload
34
+ completion?: GenerationCompletedPayload
35
+ failure?: GenerationFailedPayload
36
+ calls: CoordinatorCall[]
37
+ inputs: InputRequestedPayload[]
38
+ inputResponse?: {
39
+ index: number
40
+ generationId: string
41
+ inputId: string
42
+ name: string
43
+ }
44
+ }
45
+
46
+ export type AICoordinatorState = {
47
+ closed: boolean
48
+ queued: Array<{ index: number; messageId: string; generate: boolean }>
49
+ response?: CoordinatorResponse
50
+ }
51
+
52
+ const updateResponse = (
53
+ state: AICoordinatorState,
54
+ update: (response: CoordinatorResponse) => CoordinatorResponse,
55
+ ): AICoordinatorState =>
56
+ state.response === undefined
57
+ ? state
58
+ : { ...state, response: update(state.response) }
59
+
60
+ const foldCoordinator = <M extends UIMessage, D extends EventDefs>(
61
+ state: AICoordinatorState,
62
+ event: ContractEvent<D>,
63
+ ): AICoordinatorState => {
64
+ switch (event.type) {
65
+ case 'ai.session.closed':
66
+ return { closed: true, queued: [] }
67
+ case 'ai.message.created': {
68
+ const payload = event.payload as MessageCreatedPayload<M>
69
+ const message = payload.message
70
+ if (message.role !== 'user') return state
71
+ return {
72
+ ...state,
73
+ queued: [
74
+ ...state.queued.filter((item) => item.messageId !== message.id),
75
+ {
76
+ index: event.index,
77
+ messageId: message.id,
78
+ generate: payload.generate !== false,
79
+ },
80
+ ].toSorted((left, right) => left.index - right.index),
81
+ }
82
+ }
83
+ case 'ai.generation.requested': {
84
+ const request = event.payload as GenerationRequestedPayload
85
+ if (request.reason === 'message') {
86
+ const responseMessageId =
87
+ request.responseMessageId ?? `${request.messageId}:assistant`
88
+ const requested = state.queued.find(
89
+ (item) => item.messageId === request.messageId,
90
+ )
91
+ return {
92
+ ...state,
93
+ queued:
94
+ requested === undefined
95
+ ? state.queued.filter(
96
+ (item) => item.messageId !== request.messageId,
97
+ )
98
+ : state.queued.filter((item) => item.index > requested.index),
99
+ response: {
100
+ rootMessageId: request.messageId,
101
+ responseMessageId,
102
+ status: 'requested',
103
+ activeRequestId: event.id,
104
+ calls: [],
105
+ inputs: [],
106
+ },
107
+ }
108
+ }
109
+ if (
110
+ state.response === undefined ||
111
+ request.responseMessageId !== state.response.responseMessageId
112
+ ) {
113
+ return state
114
+ }
115
+ if (
116
+ (request.reason === 'tool' &&
117
+ (!continuationReady(state) ||
118
+ event.id !==
119
+ `ai.generate:tools:${state.response.generation?.generationId}`)) ||
120
+ (request.reason === 'retry' && state.response.status !== 'failed') ||
121
+ (request.reason === 'input' &&
122
+ state.response.inputResponse === undefined)
123
+ ) {
124
+ return state
125
+ }
126
+ return updateResponse(state, (response) => ({
127
+ rootMessageId: response.rootMessageId,
128
+ responseMessageId: response.responseMessageId,
129
+ status: 'requested',
130
+ activeRequestId: event.id,
131
+ calls: [],
132
+ inputs: [],
133
+ }))
134
+ }
135
+ case 'ai.generation.started': {
136
+ const generation = event.payload as GenerationStartedPayload
137
+ if (
138
+ state.response?.responseMessageId !== generation.responseMessageId ||
139
+ state.response.activeRequestId !== generation.requestId ||
140
+ (state.response.generation?.requestId === generation.requestId &&
141
+ state.response.generation.attempt >= generation.attempt)
142
+ ) {
143
+ return state
144
+ }
145
+ return updateResponse(state, (response) => ({
146
+ rootMessageId: response.rootMessageId,
147
+ responseMessageId: response.responseMessageId,
148
+ status: 'generating',
149
+ generation,
150
+ activeRequestId: generation.requestId,
151
+ calls: [],
152
+ inputs: [],
153
+ }))
154
+ }
155
+ case 'ai.generation.completed': {
156
+ const completion = event.payload as GenerationCompletedPayload
157
+ if (
158
+ state.response?.generation?.generationId !== completion.generationId
159
+ ) {
160
+ return state
161
+ }
162
+ return updateResponse(state, (response) => ({
163
+ ...response,
164
+ status: 'waiting',
165
+ completion,
166
+ }))
167
+ }
168
+ case 'ai.generation.failed': {
169
+ const failure = event.payload as GenerationFailedPayload
170
+ const response = state.response
171
+ const ownsActiveGeneration =
172
+ response?.generation?.generationId === failure.generationId
173
+ const ownsActiveStepLimit =
174
+ failure.stepLimit === true &&
175
+ response?.activeRequestId ===
176
+ `ai.generate:tools:${failure.generationId}` &&
177
+ response.responseMessageId === failure.responseMessageId
178
+ if (!ownsActiveGeneration && !ownsActiveStepLimit) {
179
+ return state
180
+ }
181
+ return updateResponse(state, (current) => ({
182
+ ...current,
183
+ status: 'failed',
184
+ failure,
185
+ }))
186
+ }
187
+ case 'ai.message.completed': {
188
+ const messageId = (event.payload as { messageId: string }).messageId
189
+ return state.response?.responseMessageId === messageId
190
+ ? { closed: state.closed, queued: state.queued }
191
+ : state
192
+ }
193
+ case 'ai.message.interrupted': {
194
+ const interruption = event.payload as MessageInterruptedPayload
195
+ const response = state.response
196
+ return response?.responseMessageId === interruption.messageId &&
197
+ (interruption.generationId === response.generation?.generationId ||
198
+ interruption.requestId === response.activeRequestId)
199
+ ? { closed: state.closed, queued: state.queued }
200
+ : state
201
+ }
202
+ case 'ai.tool.called': {
203
+ const call = event.payload as ToolCalledPayload
204
+ if (state.response?.generation?.generationId !== call.generationId) {
205
+ return state
206
+ }
207
+ return updateResponse(state, (response) => ({
208
+ ...response,
209
+ calls: [
210
+ ...response.calls.filter(
211
+ (candidate) => candidate.call.toolCallId !== call.toolCallId,
212
+ ),
213
+ { index: event.index, call, terminal: false },
214
+ ],
215
+ }))
216
+ }
217
+ case 'ai.approval.requested': {
218
+ const approval = event.payload as ApprovalRequestedPayload
219
+ if (state.response?.generation?.generationId !== approval.generationId) {
220
+ return state
221
+ }
222
+ return updateResponse(state, (response) => ({
223
+ ...response,
224
+ calls: response.calls.map((candidate) =>
225
+ candidate.call.toolCallId === approval.toolCallId
226
+ ? { ...candidate, approval }
227
+ : candidate,
228
+ ),
229
+ }))
230
+ }
231
+ case 'ai.approval.responded': {
232
+ const approval = event.payload as ApprovalRespondedPayload
233
+ if (
234
+ state.response?.responseMessageId !== approval.messageId ||
235
+ state.response.generation?.generationId !== approval.generationId
236
+ ) {
237
+ return state
238
+ }
239
+ return updateResponse(state, (response) => ({
240
+ ...response,
241
+ calls: response.calls.map((candidate) =>
242
+ candidate.approval?.approvalId === approval.approvalId
243
+ ? { ...candidate, response: approval, responseIndex: event.index }
244
+ : candidate,
245
+ ),
246
+ }))
247
+ }
248
+ case 'ai.tool.result': {
249
+ const result = event.payload as ToolResultPayload
250
+ if (
251
+ result.preliminary === true ||
252
+ state.response?.generation?.generationId !== result.generationId
253
+ ) {
254
+ return state
255
+ }
256
+ return updateResponse(state, (response) => ({
257
+ ...response,
258
+ calls: response.calls.map((candidate) =>
259
+ candidate.call.toolCallId === result.toolCallId
260
+ ? { ...candidate, terminal: true }
261
+ : candidate,
262
+ ),
263
+ }))
264
+ }
265
+ case 'ai.input.requested': {
266
+ const input = event.payload as InputRequestedPayload
267
+ if (
268
+ state.response?.responseMessageId !== input.messageId ||
269
+ state.response.generation?.generationId !== input.generationId
270
+ ) {
271
+ return state
272
+ }
273
+ return updateResponse(state, (response) => ({
274
+ ...response,
275
+ inputs: [
276
+ ...response.inputs.filter(
277
+ (candidate) =>
278
+ candidate.messageId !== input.messageId ||
279
+ candidate.generationId !== input.generationId ||
280
+ candidate.inputId !== input.inputId,
281
+ ),
282
+ input,
283
+ ],
284
+ }))
285
+ }
286
+ case 'ai.input.responded': {
287
+ const input = event.payload as {
288
+ messageId: string
289
+ generationId: string
290
+ inputId: string
291
+ name: string
292
+ }
293
+ const requested = state.response?.inputs.find(
294
+ (candidate) =>
295
+ candidate.messageId === input.messageId &&
296
+ candidate.generationId === input.generationId &&
297
+ candidate.inputId === input.inputId &&
298
+ candidate.name === input.name,
299
+ )
300
+ if (
301
+ state.response?.responseMessageId !== input.messageId ||
302
+ state.response.generation?.generationId !== input.generationId ||
303
+ requested === undefined
304
+ ) {
305
+ return state
306
+ }
307
+ return updateResponse(state, (response) => ({
308
+ ...response,
309
+ inputs: response.inputs.filter(
310
+ (candidate) => candidate.inputId !== input.inputId,
311
+ ),
312
+ inputResponse: {
313
+ index: event.index,
314
+ generationId: input.generationId,
315
+ inputId: input.inputId,
316
+ name: requested.name,
317
+ },
318
+ }))
319
+ }
320
+ default:
321
+ return state
322
+ }
323
+ }
324
+
325
+ export const aiCoordinatorReducer = <
326
+ M extends UIMessage,
327
+ D extends AIEventDefs<M> & EventDefs,
328
+ >(
329
+ contract: Contract<D>,
330
+ ): Reducer<D, AICoordinatorState> =>
331
+ contract
332
+ .reducer({
333
+ name: 'a2.ai.coordinator.v3',
334
+ initialState: {
335
+ closed: false,
336
+ queued: [],
337
+ } as AICoordinatorState,
338
+ })
339
+ .fold((state, event) => foldCoordinator<M, D>(state, event))
340
+
341
+ export const continuationReady = (state: AICoordinatorState): boolean => {
342
+ const response = state.response
343
+ return (
344
+ !state.closed &&
345
+ response?.completion?.finishReason === 'tool-calls' &&
346
+ response.generation?.generationId === response.completion.generationId &&
347
+ response.failure === undefined &&
348
+ response.calls.length > 0 &&
349
+ response.calls.every(
350
+ (call) =>
351
+ call.terminal ||
352
+ (call.call.providerExecuted === true &&
353
+ call.call.supportsDeferredResults !== true &&
354
+ call.approval !== undefined &&
355
+ call.response !== undefined),
356
+ )
357
+ )
358
+ }