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
@@ -0,0 +1,484 @@
1
+ import type { UIMessage } from 'ai'
2
+ import type { AIState } from './ai.ts'
3
+ import type { MessageProjection } from './ai-message-projection.ts'
4
+ import type { EventDefs } from './contract.ts'
5
+ import { clientStateProjection, type Reducer } from './reducer.ts'
6
+ import { validateSync } from './validate.ts'
7
+
8
+ type Value =
9
+ null | boolean | number | string | Value[] | { [key: string]: Value }
10
+ type Payload = { value?: unknown }
11
+ type Undo =
12
+ | { value: Value }
13
+ | { length: number; items: Record<string, Undo> }
14
+ | { fields: Record<string, Undo>; remove: string[] }
15
+
16
+ type Projection = {
17
+ fields: Value
18
+ hasMessage: boolean
19
+ message?: Undo
20
+ tools?: Undo
21
+ }
22
+
23
+ export type AIStoredState = {
24
+ version: 2
25
+ fields: Value
26
+ projection: Projection | null
27
+ values: Payload[]
28
+ }
29
+
30
+ export type AIStoredView = {
31
+ messages: UIMessage[]
32
+ activeProjection:
33
+ (MessageProjection<UIMessage> & { baseTools?: AIState['tools'] }) | null
34
+ tools?: AIState['tools']
35
+ usage?: AIState['usage']
36
+ }
37
+
38
+ export const aiStateReducer: unique symbol = Symbol('a2.ai.stateReducer')
39
+
40
+ export type AIStoredReducer<
41
+ D extends EventDefs,
42
+ S extends AIStoredView,
43
+ > = Reducer<D, AIStoredState, Record<never, never>, S> & {
44
+ readonly [aiStateReducer]: Reducer<D, S>
45
+ view(state: AIStoredState): S
46
+ fromView(state: S): AIStoredState
47
+ }
48
+
49
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
50
+ typeof value === 'object' && value !== null && !Array.isArray(value)
51
+
52
+ const hasKeys = (
53
+ value: unknown,
54
+ { required, optional = [] }: { required: string[]; optional?: string[] },
55
+ ): value is Record<string, unknown> =>
56
+ isRecord(value) &&
57
+ required.every((key) => Object.hasOwn(value, key)) &&
58
+ Object.keys(value).every(
59
+ (key) => required.includes(key) || optional.includes(key),
60
+ )
61
+
62
+ const reference = (value: Value): number | undefined => {
63
+ if (!isRecord(value) || !Object.hasOwn(value, '$a2Value')) return undefined
64
+ const id = value['$a2Value']
65
+ if (
66
+ typeof id !== 'number' ||
67
+ !Number.isSafeInteger(id) ||
68
+ id < 0 ||
69
+ Object.keys(value).length !== 1
70
+ )
71
+ throw new TypeError('invalid AI payload reference')
72
+ return id
73
+ }
74
+
75
+ const payloadFields = new Set([
76
+ 'input',
77
+ 'output',
78
+ 'rawInput',
79
+ 'metadata',
80
+ 'providerMetadata',
81
+ 'callProviderMetadata',
82
+ 'resultProviderMetadata',
83
+ 'toolMetadata',
84
+ 'data',
85
+ ])
86
+
87
+ const keyFor = (value: unknown): string =>
88
+ value === undefined ? 'undefined' : JSON.stringify(value)
89
+
90
+ const sameValue = (left: unknown, right: unknown): boolean => {
91
+ if (Object.is(left, right)) return true
92
+ if (
93
+ typeof left !== 'object' ||
94
+ left === null ||
95
+ typeof right !== 'object' ||
96
+ right === null
97
+ )
98
+ return false
99
+ const prototype = Object.getPrototypeOf(left)
100
+ if (
101
+ prototype !== Object.getPrototypeOf(right) ||
102
+ (prototype !== Object.prototype &&
103
+ prototype !== Array.prototype &&
104
+ prototype !== null)
105
+ )
106
+ return false
107
+ if (
108
+ Array.isArray(left) &&
109
+ (!Array.isArray(right) || left.length !== right.length)
110
+ )
111
+ return false
112
+ const keys = Reflect.ownKeys(left)
113
+ return (
114
+ keys.length === Reflect.ownKeys(right).length &&
115
+ keys.every(
116
+ (key) =>
117
+ Object.hasOwn(right, key) &&
118
+ sameValue(Reflect.get(left, key), Reflect.get(right, key)),
119
+ )
120
+ )
121
+ }
122
+
123
+ const restore = (current: Value, undo: Undo | undefined): Value => {
124
+ if (undo === undefined) return current
125
+ if (!isRecord(undo)) throw new TypeError('invalid AI recovery change')
126
+ if ('value' in undo) {
127
+ if (!hasKeys(undo, { required: ['value'] }))
128
+ throw new TypeError('invalid AI replacement recovery')
129
+ return undo.value
130
+ }
131
+ if ('length' in undo) {
132
+ if (
133
+ !hasKeys(undo, { required: ['length', 'items'] }) ||
134
+ !Number.isSafeInteger(undo.length) ||
135
+ undo.length < 0 ||
136
+ undo.length > 0xffffffff ||
137
+ !isRecord(undo.items) ||
138
+ !Array.isArray(current)
139
+ )
140
+ throw new TypeError('invalid AI array recovery')
141
+ const items = Object.entries(undo.items)
142
+ let added = 0
143
+ for (const [key, patch] of items) {
144
+ const index = Number(key)
145
+ if (
146
+ !Number.isSafeInteger(index) ||
147
+ index < 0 ||
148
+ index >= undo.length ||
149
+ String(index) !== key ||
150
+ patch === undefined
151
+ )
152
+ throw new TypeError('invalid AI array recovery index')
153
+ if (index >= current.length) added += 1
154
+ }
155
+ if (added !== Math.max(0, undo.length - current.length))
156
+ throw new TypeError('incomplete AI array recovery')
157
+ const result = current.slice(0, undo.length)
158
+ result.length = undo.length
159
+ for (const [index, patch] of items)
160
+ result[Number(index)] = restore(current[Number(index)] ?? null, patch)
161
+ return result
162
+ }
163
+ if (
164
+ !hasKeys(undo, { required: ['fields', 'remove'] }) ||
165
+ !isRecord(undo.fields) ||
166
+ !Array.isArray(undo.remove) ||
167
+ Object.keys(undo.remove).length !== undo.remove.length ||
168
+ Array.from(undo.remove).some((key) => typeof key !== 'string') ||
169
+ new Set(undo.remove).size !== undo.remove.length ||
170
+ !isRecord(current) ||
171
+ reference(current) !== undefined
172
+ )
173
+ throw new TypeError('invalid AI object recovery')
174
+ const result = { ...current } as Record<string, Value>
175
+ for (const key of undo.remove) delete result[key]
176
+ for (const [key, patch] of Object.entries(undo.fields)) {
177
+ if (patch === undefined || undo.remove.includes(key))
178
+ throw new TypeError('invalid AI object recovery field')
179
+ Object.defineProperty(result, key, {
180
+ value: restore(Object.hasOwn(result, key) ? result[key]! : null, patch),
181
+ enumerable: true,
182
+ configurable: true,
183
+ writable: true,
184
+ })
185
+ }
186
+ return result
187
+ }
188
+
189
+ const difference = (current: Value, before: Value): Undo | undefined => {
190
+ if (current === before) return undefined
191
+ if (reference(current) !== undefined || reference(before) !== undefined)
192
+ return reference(current) === reference(before)
193
+ ? undefined
194
+ : { value: before }
195
+ if (Array.isArray(current) && Array.isArray(before)) {
196
+ const items: Record<string, Undo> = {}
197
+ for (const [index, value] of before.entries()) {
198
+ const patch =
199
+ index < current.length ? difference(current[index]!, value) : { value }
200
+ if (patch !== undefined) items[index] = patch
201
+ }
202
+ return current.length === before.length && Object.keys(items).length === 0
203
+ ? undefined
204
+ : { length: before.length, items }
205
+ }
206
+ if (isRecord(current) && isRecord(before)) {
207
+ const fields: Record<string, Undo> = {}
208
+ const remove = Object.keys(current).filter(
209
+ (key) => !Object.hasOwn(before, key),
210
+ )
211
+ for (const [key, value] of Object.entries(before)) {
212
+ const patch = Object.hasOwn(current, key)
213
+ ? difference(current[key] as Value, value as Value)
214
+ : { value: value as Value }
215
+ if (patch !== undefined)
216
+ Object.defineProperty(fields, key, {
217
+ value: patch,
218
+ enumerable: true,
219
+ configurable: true,
220
+ writable: true,
221
+ })
222
+ }
223
+ return remove.length === 0 && Object.keys(fields).length === 0
224
+ ? undefined
225
+ : { fields, remove }
226
+ }
227
+ return { value: before }
228
+ }
229
+
230
+ const storeState = <S extends AIStoredView>(state: S): AIStoredState => {
231
+ const packed = new WeakMap<object, Value>()
232
+ const values: Payload[] = []
233
+ const ids = new Map<unknown, number>()
234
+ const contents = new Map<string, number[]>()
235
+ const intern = (value: unknown): Value => {
236
+ let id = ids.get(value)
237
+ if (id !== undefined) return { $a2Value: id }
238
+ const key =
239
+ typeof value === 'object' && value !== null ? keyFor(value) : undefined
240
+ const candidates = key === undefined ? undefined : contents.get(key)
241
+ if (candidates !== undefined)
242
+ id = candidates.find((candidate) =>
243
+ sameValue(values[candidate]!.value, value),
244
+ )
245
+ if (id === undefined) {
246
+ id = values.length
247
+ values.push(value === undefined ? {} : { value })
248
+ }
249
+ ids.set(value, id)
250
+ if (key !== undefined && !candidates?.includes(id))
251
+ contents.set(key, [...(candidates ?? []), id])
252
+ return { $a2Value: id }
253
+ }
254
+ const pack = (value: unknown, field?: string): Value => {
255
+ if (
256
+ value === undefined ||
257
+ (typeof value === 'string' && value.length > 80) ||
258
+ (field !== undefined &&
259
+ payloadFields.has(field) &&
260
+ !(field === 'input' && isRecord(value) && 'message' in value)) ||
261
+ (isRecord(value) && Object.hasOwn(value, '$a2Value'))
262
+ )
263
+ return intern(value)
264
+ if (
265
+ value === null ||
266
+ typeof value === 'string' ||
267
+ typeof value === 'boolean' ||
268
+ typeof value === 'number'
269
+ )
270
+ return value
271
+ if (typeof value !== 'object')
272
+ throw new TypeError('AI state must contain JSON values')
273
+ const cached = packed.get(value)
274
+ if (cached !== undefined) return cached
275
+ const result: Value = Array.isArray(value)
276
+ ? value.map((item) => pack(item))
277
+ : Object.fromEntries(
278
+ Object.entries(value)
279
+ .toSorted(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
280
+ .map(([key, item]) => [key, pack(item, key)]),
281
+ )
282
+ packed.set(value, result)
283
+ return result
284
+ }
285
+ const saveProjection = (
286
+ projection: NonNullable<AIStoredView['activeProjection']>,
287
+ messages: UIMessage[],
288
+ tools: AIStoredView['tools'],
289
+ ): Projection => {
290
+ const { baseMessage, baseTools, ...fields } = projection
291
+ const current = messages.find(
292
+ (message) => message.id === projection.responseMessageId,
293
+ )
294
+ const message =
295
+ baseMessage === undefined
296
+ ? undefined
297
+ : difference(pack(current), pack(baseMessage))
298
+ const toolPatch =
299
+ baseTools === undefined
300
+ ? undefined
301
+ : difference(pack(tools), pack(baseTools))
302
+ return {
303
+ fields: pack(fields),
304
+ hasMessage: baseMessage !== undefined,
305
+ ...(message === undefined ? {} : { message }),
306
+ ...(toolPatch === undefined ? {} : { tools: toolPatch }),
307
+ }
308
+ }
309
+ const { activeProjection, ...fields } = state
310
+ const projection =
311
+ activeProjection === null
312
+ ? null
313
+ : saveProjection(activeProjection, state.messages, state.tools)
314
+ return {
315
+ version: 2,
316
+ fields: pack(fields),
317
+ projection,
318
+ values,
319
+ }
320
+ }
321
+
322
+ const loadState = <S extends AIStoredView>(stored: AIStoredState): S => {
323
+ const unpack = (value: Value): unknown => {
324
+ const id = reference(value)
325
+ if (id !== undefined) {
326
+ if (!Object.hasOwn(stored.values, id))
327
+ throw new TypeError('missing AI payload')
328
+ return stored.values[id]!.value
329
+ }
330
+ if (Array.isArray(value)) {
331
+ if (Object.keys(value).length !== value.length)
332
+ throw new TypeError('invalid AI stored array')
333
+ return Array.from(value, unpack)
334
+ }
335
+ if (isRecord(value))
336
+ return Object.fromEntries(
337
+ Object.entries(value).map(([key, item]) => [
338
+ key,
339
+ unpack(item as Value),
340
+ ]),
341
+ )
342
+ if (
343
+ value === null ||
344
+ typeof value === 'boolean' ||
345
+ typeof value === 'string' ||
346
+ (typeof value === 'number' && Number.isFinite(value))
347
+ )
348
+ return value
349
+ throw new TypeError('invalid AI stored value')
350
+ }
351
+ const fields = stored.fields as Record<string, Value>
352
+ const state = unpack(stored.fields) as S
353
+ const loadProjection = (
354
+ saved: Projection,
355
+ messages: Value,
356
+ tools: Value | undefined,
357
+ ): AIStoredView['activeProjection'] => {
358
+ if (
359
+ !hasKeys(saved, {
360
+ required: ['fields', 'hasMessage'],
361
+ optional: ['message', 'tools'],
362
+ }) ||
363
+ typeof saved.hasMessage !== 'boolean' ||
364
+ !isRecord(saved.fields) ||
365
+ ('message' in saved &&
366
+ (!saved.hasMessage || saved.message === undefined)) ||
367
+ ('tools' in saved && saved.tools === undefined)
368
+ )
369
+ throw new TypeError('invalid AI projection recovery')
370
+ const projection = unpack(saved.fields) as NonNullable<
371
+ AIStoredView['activeProjection']
372
+ >
373
+ const currentMessages = unpack(messages) as UIMessage[]
374
+ const index = currentMessages.findIndex(
375
+ (message) => message.id === projection.responseMessageId,
376
+ )
377
+ if (saved.hasMessage) {
378
+ const current = index === -1 ? null : (messages as Value[])[index]!
379
+ projection.baseMessage = unpack(
380
+ restore(current, saved.message),
381
+ ) as UIMessage
382
+ }
383
+ if (tools !== undefined)
384
+ projection.baseTools = unpack(
385
+ restore(tools, saved.tools),
386
+ ) as AIState['tools']
387
+ else if (saved.tools !== undefined)
388
+ throw new TypeError('missing AI recovery tools')
389
+ return projection
390
+ }
391
+ state.activeProjection =
392
+ stored.projection === null
393
+ ? null
394
+ : loadProjection(stored.projection, fields['messages']!, fields['tools']!)
395
+ return state
396
+ }
397
+
398
+ export const storedAIReducer = <D extends EventDefs, S extends AIStoredView>(
399
+ source: Reducer<D, S>,
400
+ ): AIStoredReducer<D, S> => {
401
+ const views = new WeakMap<AIStoredState, S>()
402
+ const view = (state: AIStoredState): S => {
403
+ let result = views.get(state)
404
+ if (result === undefined) {
405
+ result = loadState<S>(state)
406
+ views.set(state, result)
407
+ }
408
+ return result
409
+ }
410
+ const defer = (next: S): AIStoredState => {
411
+ let stored: AIStoredState | undefined
412
+ const read = (): AIStoredState => (stored ??= storeState(next))
413
+ const state: AIStoredState = {
414
+ version: 2,
415
+ get fields() {
416
+ return read().fields
417
+ },
418
+ get projection() {
419
+ return read().projection
420
+ },
421
+ get values() {
422
+ return read().values
423
+ },
424
+ }
425
+ views.set(state, next)
426
+ return state
427
+ }
428
+ return {
429
+ ...source,
430
+ name: `${source.name}:records-v2`,
431
+ initialState: storeState(source.initialState),
432
+ [aiStateReducer]: source,
433
+ view,
434
+ fromView: (state) => storeState(state),
435
+ [clientStateProjection]: (state) =>
436
+ source[clientStateProjection]?.(view(state)) ?? view(state),
437
+ stateSchema: {
438
+ '~standard': {
439
+ version: 1,
440
+ vendor: 'a2-ai',
441
+ validate(value) {
442
+ try {
443
+ if (
444
+ !hasKeys(value, {
445
+ required: ['version', 'fields', 'projection', 'values'],
446
+ }) ||
447
+ value['version'] !== 2 ||
448
+ !Array.isArray(value['values']) ||
449
+ !isRecord(value['fields']) ||
450
+ (value['projection'] !== null && !isRecord(value['projection']))
451
+ )
452
+ return { issues: [{ message: 'invalid AI stored state' }] }
453
+ const state = value as AIStoredState
454
+ if (Object.keys(state.values).length !== state.values.length)
455
+ return { issues: [{ message: 'invalid AI payload table' }] }
456
+ for (const payload of state.values) {
457
+ if (!hasKeys(payload, { required: [], optional: ['value'] }))
458
+ return { issues: [{ message: 'invalid AI payload table' }] }
459
+ }
460
+ const decoded = loadState<S>(state)
461
+ if (source.stateSchema) {
462
+ const result = validateSync(
463
+ source.stateSchema,
464
+ decoded,
465
+ 'AI view stateSchema',
466
+ )
467
+ if (result.issues) return result
468
+ return { value: storeState(result.value) }
469
+ }
470
+ return { value: state }
471
+ } catch {
472
+ return { issues: [{ message: 'invalid AI stored state' }] }
473
+ }
474
+ },
475
+ },
476
+ },
477
+ fold(state, event) {
478
+ const current = view(state)
479
+ const next = source.fold(current, event)
480
+ if (next === current) return state
481
+ return defer(next)
482
+ },
483
+ }
484
+ }