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.
- package/CHANGELOG.md +81 -0
- package/dist/actor-client.d.ts +1 -1
- package/dist/{actor-BfQSE0KC.d.ts → actor-ohPC-81x.d.ts} +5 -5
- package/dist/{actor-BfQSE0KC.d.ts.map → actor-ohPC-81x.d.ts.map} +1 -1
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +1 -1
- package/dist/ai-CrLYwNEx.js +2431 -0
- package/dist/ai-CrLYwNEx.js.map +1 -0
- package/dist/{ai-Cai-lCbj.d.ts → ai-HJ9fHfYI.d.ts} +72 -13
- package/dist/ai-HJ9fHfYI.d.ts.map +1 -0
- package/dist/ai-server.d.ts +4 -5
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +755 -290
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +1 -1999
- package/dist/client-BYzHjkwU.js.map +1 -1
- package/dist/{client-BAEABRZB.d.ts → client-CzyacQpJ.d.ts} +9 -9
- package/dist/client-CzyacQpJ.d.ts.map +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/idempotent-id-BRJVeylj.js +13 -0
- package/dist/idempotent-id-BRJVeylj.js.map +1 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -1
- package/dist/otel.d.ts +1 -1
- package/dist/react.d.ts +13 -13
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js.map +1 -1
- package/dist/reducer-DEMjEY_O.js.map +1 -1
- package/dist/{reducer-BcS9VDKC.d.ts → reducer-otzHjuJj.d.ts} +3 -3
- package/dist/{reducer-BcS9VDKC.d.ts.map → reducer-otzHjuJj.d.ts.map} +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +1 -1
- package/dist/{server-CjJSGcF7.js → server-BD5ckxZb.js} +39 -26
- package/dist/server-BD5ckxZb.js.map +1 -0
- package/dist/{server-Bp5Nd1pF.d.ts → server-DbD7IJVK.d.ts} +5 -5
- package/dist/{server-Bp5Nd1pF.d.ts.map → server-DbD7IJVK.d.ts.map} +1 -1
- package/dist/server.d.ts +3 -3
- package/dist/server.js +1 -1
- package/dist/{store-D_yhNdPz.d.ts → store-DyZM6fS5.d.ts} +3 -2
- package/dist/{store-D_yhNdPz.d.ts.map → store-DyZM6fS5.d.ts.map} +1 -1
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +1 -1
- package/dist/{store-redis-notify-D2EI6gwX.js → store-redis-notify-BVLUfI3j.js} +14 -13
- package/dist/store-redis-notify-BVLUfI3j.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/{telemetry-CpeclqB2.d.ts → telemetry-B5jzpy6y.d.ts} +2 -2
- package/dist/telemetry-B5jzpy6y.d.ts.map +1 -0
- package/docs/concepts/04-state.mdx +9 -4
- package/docs/guides/06-ai-agents.mdx +134 -43
- package/docs/reference/01-api.mdx +129 -27
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +20 -28
- package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +123 -0
- package/examples/playground/app/agent/[agentId]/agent-queue.tsx +37 -43
- package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +44 -24
- package/examples/playground/app/agent/compaction-timeline.test.ts +47 -7
- package/examples/playground/app/agent/compaction-timeline.ts +13 -3
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -5
- package/examples/playground/app/chat/[chatId]/session.ts +17 -2
- package/examples/playground/app/chat/model.test.ts +4 -4
- package/examples/playground/app/chat/model.ts +18 -6
- package/examples/playground/app/chat/server.ts +6 -3
- package/examples/playground/package.json +3 -1
- package/package.json +1 -1
- package/src/ai-client-state.ts +0 -2
- package/src/ai-context-schema.ts +296 -0
- package/src/ai-context.ts +680 -0
- package/src/ai-control-server.ts +160 -131
- package/src/ai-control-state.ts +6 -0
- package/src/ai-control.ts +6 -7
- package/src/ai-coordinator.ts +92 -38
- package/src/ai-id.ts +6 -0
- package/src/ai-input-tokens.ts +75 -0
- package/src/ai-message-projection.ts +348 -0
- package/src/ai-server.ts +232 -334
- package/src/ai-stored-state.ts +484 -0
- package/src/ai.ts +252 -418
- package/src/client.ts +21 -15
- package/src/idempotent-id.ts +11 -0
- package/src/index.ts +1 -0
- package/src/react.ts +25 -19
- package/src/reducer.ts +2 -1
- package/src/server.ts +73 -56
- package/src/store-redis-core.ts +15 -17
- package/src/store.ts +1 -0
- package/src/telemetry.ts +2 -1
- package/dist/ai-Cai-lCbj.d.ts.map +0 -1
- package/dist/ai-control-CcD4hh3y.js +0 -119
- package/dist/ai-control-CcD4hh3y.js.map +0 -1
- package/dist/ai.js.map +0 -1
- package/dist/client-BAEABRZB.d.ts.map +0 -1
- package/dist/server-CjJSGcF7.js.map +0 -1
- package/dist/store-redis-notify-D2EI6gwX.js.map +0 -1
- 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
|
+
}
|