experimental-a2 0.15.0 → 0.16.1

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 (101) 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-Cai-lCbj.d.ts → ai-CrEf6p_W.d.ts} +73 -14
  9. package/dist/ai-CrEf6p_W.d.ts.map +1 -0
  10. package/dist/ai-DgOBltJ_.js +2451 -0
  11. package/dist/ai-DgOBltJ_.js.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 +788 -296
  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 +3 -3
  25. package/dist/index.js +1 -12
  26. package/dist/otel.d.ts +1 -1
  27. package/dist/react.d.ts +13 -13
  28. package/dist/react.d.ts.map +1 -1
  29. package/dist/react.js.map +1 -1
  30. package/dist/reducer-DEMjEY_O.js.map +1 -1
  31. package/dist/{reducer-BcS9VDKC.d.ts → reducer-otzHjuJj.d.ts} +3 -3
  32. package/dist/{reducer-BcS9VDKC.d.ts.map → reducer-otzHjuJj.d.ts.map} +1 -1
  33. package/dist/scheduler-qstash.d.ts +2 -2
  34. package/dist/scheduler-qstash.js +1 -1
  35. package/dist/scheduler-vercel.d.ts +2 -2
  36. package/dist/scheduler-vercel.js +1 -1
  37. package/dist/{server-CjJSGcF7.js → server-BD5ckxZb.js} +39 -26
  38. package/dist/server-BD5ckxZb.js.map +1 -0
  39. package/dist/{server-Bp5Nd1pF.d.ts → server-DbD7IJVK.d.ts} +5 -5
  40. package/dist/{server-Bp5Nd1pF.d.ts.map → server-DbD7IJVK.d.ts.map} +1 -1
  41. package/dist/server.d.ts +3 -3
  42. package/dist/server.js +1 -1
  43. package/dist/{store-D_yhNdPz.d.ts → store-DyZM6fS5.d.ts} +3 -2
  44. package/dist/{store-D_yhNdPz.d.ts.map → store-DyZM6fS5.d.ts.map} +1 -1
  45. package/dist/store-N8PXxDAS.js.map +1 -1
  46. package/dist/store-memory.d.ts +1 -1
  47. package/dist/store-postgres.d.ts +1 -1
  48. package/dist/store-redis-http.d.ts +1 -1
  49. package/dist/store-redis-http.js +1 -1
  50. package/dist/{store-redis-notify-D2EI6gwX.js → store-redis-notify-BVLUfI3j.js} +14 -13
  51. package/dist/store-redis-notify-BVLUfI3j.js.map +1 -0
  52. package/dist/store-redis.d.ts +1 -1
  53. package/dist/store-redis.js +1 -1
  54. package/dist/store-sqlite.d.ts +1 -1
  55. package/dist/{telemetry-CpeclqB2.d.ts → telemetry-B5jzpy6y.d.ts} +2 -2
  56. package/dist/telemetry-B5jzpy6y.d.ts.map +1 -0
  57. package/docs/concepts/04-state.mdx +9 -4
  58. package/docs/guides/06-ai-agents.mdx +149 -48
  59. package/docs/reference/01-api.mdx +110 -28
  60. package/examples/playground/app/agent/[agentId]/agent-client.tsx +20 -28
  61. package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +123 -0
  62. package/examples/playground/app/agent/[agentId]/agent-queue.tsx +37 -43
  63. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +44 -24
  64. package/examples/playground/app/agent/compaction-timeline.test.ts +47 -7
  65. package/examples/playground/app/agent/compaction-timeline.ts +13 -3
  66. package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -5
  67. package/examples/playground/app/chat/[chatId]/session.ts +17 -2
  68. package/examples/playground/app/chat/model.test.ts +4 -4
  69. package/examples/playground/app/chat/model.ts +18 -6
  70. package/examples/playground/app/chat/server.ts +6 -3
  71. package/examples/playground/package.json +3 -1
  72. package/package.json +1 -1
  73. package/src/ai-client-state.ts +36 -4
  74. package/src/ai-context-schema.ts +296 -0
  75. package/src/ai-context.ts +680 -0
  76. package/src/ai-control-server.ts +183 -135
  77. package/src/ai-control-state.ts +6 -0
  78. package/src/ai-control.ts +8 -7
  79. package/src/ai-coordinator.ts +92 -38
  80. package/src/ai-id.ts +6 -0
  81. package/src/ai-input-tokens.ts +75 -0
  82. package/src/ai-message-projection.ts +474 -0
  83. package/src/ai-server.ts +247 -336
  84. package/src/ai-stored-state.ts +484 -0
  85. package/src/ai.ts +255 -538
  86. package/src/client.ts +21 -15
  87. package/src/react.ts +25 -19
  88. package/src/reducer.ts +2 -1
  89. package/src/server.ts +73 -56
  90. package/src/store-redis-core.ts +15 -17
  91. package/src/store.ts +1 -0
  92. package/src/telemetry.ts +2 -1
  93. package/dist/ai-Cai-lCbj.d.ts.map +0 -1
  94. package/dist/ai-control-CcD4hh3y.js +0 -119
  95. package/dist/ai-control-CcD4hh3y.js.map +0 -1
  96. package/dist/ai.js.map +0 -1
  97. package/dist/client-BAEABRZB.d.ts.map +0 -1
  98. package/dist/index.js.map +0 -1
  99. package/dist/server-CjJSGcF7.js.map +0 -1
  100. package/dist/store-redis-notify-D2EI6gwX.js.map +0 -1
  101. package/dist/telemetry-CpeclqB2.d.ts.map +0 -1
@@ -0,0 +1,474 @@
1
+ import type { UIMessage, UIMessageChunk } from 'ai'
2
+ import type {
3
+ ActiveGenerationProjection,
4
+ AIState,
5
+ ApprovalRequestedPayload,
6
+ ApprovalRespondedPayload,
7
+ ToolCalledPayload,
8
+ ToolResultPayload,
9
+ ToolActivity,
10
+ } from './ai.ts'
11
+ import {
12
+ type UIMessageCursor,
13
+ projectUIMessageStream,
14
+ interruptUIMessage,
15
+ } from './ai-projector.ts'
16
+ import {
17
+ flattenProgressBatches,
18
+ appendProgressBatch,
19
+ progressBatches,
20
+ type ProgressBatches,
21
+ } from './ai-progress-batches.ts'
22
+
23
+ export type MessageProjection<M extends UIMessage> = Omit<
24
+ ActiveGenerationProjection<M>,
25
+ 'baseTools'
26
+ >
27
+
28
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
29
+ typeof value === 'object' && value !== null && !Array.isArray(value)
30
+
31
+ export const upsertMessage = <M extends UIMessage>(
32
+ messages: M[],
33
+ message: M,
34
+ ): M[] => {
35
+ const index = messages.findIndex((candidate) => candidate.id === message.id)
36
+ if (index === -1) return [...messages, message]
37
+ const next = [...messages]
38
+ next[index] = message
39
+ return next
40
+ }
41
+
42
+ export const upsertResponse = <M extends UIMessage>(options: {
43
+ messages: M[]
44
+ message: M
45
+ inputId: string
46
+ }): M[] => {
47
+ const { messages, message, inputId } = options
48
+ if (messages.some((item) => item.id === message.id))
49
+ return upsertMessage(messages, message)
50
+ const inputIndex = messages.findIndex((item) => item.id === inputId)
51
+ if (inputIndex === -1) return [...messages, message]
52
+ return [
53
+ ...messages.slice(0, inputIndex + 1),
54
+ message,
55
+ ...messages.slice(inputIndex + 1),
56
+ ]
57
+ }
58
+
59
+ export const answerApproval = <M extends UIMessage>(
60
+ messages: M[],
61
+ response: ApprovalRespondedPayload,
62
+ ): M[] =>
63
+ messages.map((message) => {
64
+ if (message.id !== response.messageId) return message
65
+ let changed = false
66
+ const parts = message.parts.map((part) => {
67
+ const candidate = part as unknown
68
+ if (
69
+ !isRecord(candidate) ||
70
+ candidate['state'] !== 'approval-requested' ||
71
+ !isRecord(candidate['approval']) ||
72
+ candidate['approval']['id'] !== response.approvalId
73
+ ) {
74
+ return part
75
+ }
76
+ changed = true
77
+ return {
78
+ ...part,
79
+ state: 'approval-responded',
80
+ approval: {
81
+ ...candidate['approval'],
82
+ approved: response.approved,
83
+ ...(response.reason === undefined ? {} : { reason: response.reason }),
84
+ },
85
+ } as typeof part
86
+ })
87
+ return changed ? ({ ...message, parts } as M) : message
88
+ })
89
+
90
+ export const approvalRequestedChunk = (
91
+ payload: ApprovalRequestedPayload,
92
+ ): UIMessageChunk => ({
93
+ type: 'tool-approval-request',
94
+ approvalId: payload.approvalId,
95
+ toolCallId: payload.toolCallId,
96
+ ...(payload.isAutomatic === undefined
97
+ ? {}
98
+ : { isAutomatic: payload.isAutomatic }),
99
+ ...(payload.signature === undefined ? {} : { signature: payload.signature }),
100
+ })
101
+
102
+ export const approvalRespondedChunk = (
103
+ payload: ApprovalRespondedPayload,
104
+ ): UIMessageChunk => ({
105
+ type: 'tool-approval-response',
106
+ approvalId: payload.approvalId,
107
+ approved: payload.approved,
108
+ ...(payload.reason === undefined ? {} : { reason: payload.reason }),
109
+ })
110
+
111
+ export const toolResultChunk = (payload: ToolResultPayload): UIMessageChunk => {
112
+ if (payload.phase === 'input') {
113
+ return {
114
+ type: 'tool-input-error',
115
+ toolCallId: payload.toolCallId,
116
+ toolName: payload.toolName ?? 'unknown',
117
+ input: payload.rawInput ?? payload.input,
118
+ errorText: payload.error ?? 'Invalid tool input',
119
+ ...(payload.dynamic === undefined ? {} : { dynamic: payload.dynamic }),
120
+ ...(payload.providerExecuted === undefined
121
+ ? {}
122
+ : { providerExecuted: payload.providerExecuted }),
123
+ ...(payload.providerMetadata === undefined
124
+ ? {}
125
+ : { providerMetadata: payload.providerMetadata }),
126
+ ...(payload.toolMetadata === undefined
127
+ ? {}
128
+ : { toolMetadata: payload.toolMetadata }),
129
+ } as UIMessageChunk
130
+ }
131
+ if (payload.denied === true) {
132
+ return {
133
+ type: 'tool-output-denied',
134
+ toolCallId: payload.toolCallId,
135
+ } as UIMessageChunk
136
+ }
137
+ if (payload.error !== undefined) {
138
+ return {
139
+ type: 'tool-output-error',
140
+ toolCallId: payload.toolCallId,
141
+ errorText: payload.error,
142
+ ...(payload.dynamic === undefined ? {} : { dynamic: payload.dynamic }),
143
+ ...(payload.providerExecuted === undefined
144
+ ? {}
145
+ : { providerExecuted: payload.providerExecuted }),
146
+ ...(payload.providerMetadata === undefined
147
+ ? {}
148
+ : { providerMetadata: payload.providerMetadata }),
149
+ ...(payload.toolMetadata === undefined
150
+ ? {}
151
+ : { toolMetadata: payload.toolMetadata }),
152
+ } as UIMessageChunk
153
+ }
154
+ return {
155
+ type: 'tool-output-available',
156
+ toolCallId: payload.toolCallId,
157
+ output: payload.output,
158
+ ...(payload.preliminary === undefined
159
+ ? {}
160
+ : { preliminary: payload.preliminary }),
161
+ ...(payload.dynamic === undefined ? {} : { dynamic: payload.dynamic }),
162
+ ...(payload.providerExecuted === undefined
163
+ ? {}
164
+ : { providerExecuted: payload.providerExecuted }),
165
+ ...(payload.providerMetadata === undefined
166
+ ? {}
167
+ : { providerMetadata: payload.providerMetadata }),
168
+ ...(payload.toolMetadata === undefined
169
+ ? {}
170
+ : { toolMetadata: payload.toolMetadata }),
171
+ } as UIMessageChunk
172
+ }
173
+
174
+ export const projectedMessageStream = <M extends UIMessage>(
175
+ projection: MessageProjection<M>,
176
+ ): { message: M; cursor: UIMessageCursor } => {
177
+ const projectionEvents = flattenProgressBatches(projection.batches)
178
+ for (const event of projection.toolEvents) {
179
+ if (event.type === 'ai.tool.result') {
180
+ projectionEvents.push({
181
+ index: event.index,
182
+ chunks: [toolResultChunk(event.payload)],
183
+ })
184
+ }
185
+ }
186
+ for (const event of projection.approvalEvents) {
187
+ projectionEvents.push({
188
+ index: event.index,
189
+ chunks: [
190
+ event.type === 'ai.approval.requested'
191
+ ? approvalRequestedChunk(event.payload)
192
+ : approvalRespondedChunk(event.payload),
193
+ ],
194
+ })
195
+ }
196
+ return projectUIMessageStream({
197
+ messageId: projection.responseMessageId,
198
+ ...(projection.baseMessage === undefined
199
+ ? {}
200
+ : { base: projection.baseMessage }),
201
+ chunks: projectionEvents
202
+ .toSorted((left, right) => left.index - right.index)
203
+ .flatMap((event) => event.chunks),
204
+ })
205
+ }
206
+
207
+ export const projectedMessage = <M extends UIMessage>(
208
+ projection: MessageProjection<M>,
209
+ ): M => projectedMessageStream(projection).message
210
+
211
+ export const reduceToolActivity = (
212
+ tools: ToolActivity[],
213
+ event:
214
+ | { type: 'ai.tool.called'; payload: ToolCalledPayload }
215
+ | { type: 'ai.tool.result'; payload: ToolResultPayload },
216
+ ): ToolActivity[] => {
217
+ if (event.type === 'ai.tool.called') {
218
+ const payload = event.payload
219
+ const { providerMetadata, ...called } = payload
220
+ const activity: ToolActivity = {
221
+ ...called,
222
+ ...(providerMetadata === undefined
223
+ ? {}
224
+ : { callProviderMetadata: providerMetadata }),
225
+ status: 'running',
226
+ }
227
+ return [
228
+ ...tools.filter(
229
+ (candidate) =>
230
+ candidate.generationId !== payload.generationId ||
231
+ candidate.toolCallId !== payload.toolCallId,
232
+ ),
233
+ activity,
234
+ ]
235
+ }
236
+
237
+ const payload = event.payload
238
+ const existing = tools.find(
239
+ (candidate) =>
240
+ candidate.generationId === payload.generationId &&
241
+ candidate.toolCallId === payload.toolCallId,
242
+ )
243
+ const toolName = payload.toolName ?? existing?.toolName
244
+ const input = payload.input === undefined ? existing?.input : payload.input
245
+ const dynamic = payload.dynamic ?? existing?.dynamic
246
+ const providerExecuted =
247
+ payload.providerExecuted ?? existing?.providerExecuted
248
+ const toolMetadata = payload.toolMetadata ?? existing?.toolMetadata
249
+ const resultProviderMetadata =
250
+ payload.providerMetadata ?? existing?.resultProviderMetadata
251
+ const activity: ToolActivity = {
252
+ requestId: payload.requestId,
253
+ messageId: payload.messageId,
254
+ generationId: payload.generationId,
255
+ toolCallId: payload.toolCallId,
256
+ ...(toolName === undefined ? {} : { toolName }),
257
+ ...(input === undefined ? {} : { input }),
258
+ ...(payload.rawInput === undefined ? {} : { rawInput: payload.rawInput }),
259
+ ...(payload.output === undefined ? {} : { output: payload.output }),
260
+ ...(payload.error === undefined ? {} : { error: payload.error }),
261
+ ...(payload.preliminary === undefined
262
+ ? {}
263
+ : { preliminary: payload.preliminary }),
264
+ ...(payload.phase === undefined ? {} : { phase: payload.phase }),
265
+ ...(dynamic === undefined ? {} : { dynamic }),
266
+ ...(providerExecuted === undefined ? {} : { providerExecuted }),
267
+ ...(existing?.callProviderMetadata === undefined
268
+ ? {}
269
+ : { callProviderMetadata: existing.callProviderMetadata }),
270
+ ...(resultProviderMetadata === undefined ? {} : { resultProviderMetadata }),
271
+ ...(toolMetadata === undefined ? {} : { toolMetadata }),
272
+ ...(existing?.title === undefined ? {} : { title: existing.title }),
273
+ status: payload.denied
274
+ ? 'denied'
275
+ : payload.error
276
+ ? 'failed'
277
+ : payload.preliminary
278
+ ? 'running'
279
+ : 'completed',
280
+ }
281
+ return [
282
+ ...tools.filter(
283
+ (candidate) =>
284
+ candidate.generationId !== payload.generationId ||
285
+ candidate.toolCallId !== payload.toolCallId,
286
+ ),
287
+ activity,
288
+ ]
289
+ }
290
+
291
+ export const advanceProjection = <M extends UIMessage>({
292
+ projection,
293
+ base,
294
+ chunks,
295
+ index,
296
+ }: {
297
+ projection: MessageProjection<M>
298
+ base: M | undefined
299
+ chunks: UIMessageChunk[]
300
+ index: number
301
+ }): {
302
+ message: M
303
+ batches: ProgressBatches
304
+ cursor: NonNullable<MessageProjection<M>['cursor']>
305
+ } => {
306
+ const ordered = index > (projection.batches.tail.at(-1)?.index ?? -1)
307
+ const batches = ordered
308
+ ? appendProgressBatch({
309
+ batches: projection.batches,
310
+ batch: { index, chunks },
311
+ })
312
+ : progressBatches(
313
+ [
314
+ ...flattenProgressBatches(projection.batches).filter(
315
+ (batch) => batch.index !== index,
316
+ ),
317
+ { index, chunks },
318
+ ].toSorted((left, right) => left.index - right.index),
319
+ )
320
+ const incremental =
321
+ base !== undefined &&
322
+ projection.cursor != null &&
323
+ index > projection.cursor.throughIndex
324
+ const projected = incremental
325
+ ? projectUIMessageStream({
326
+ messageId: projection.responseMessageId,
327
+ base,
328
+ cursor: projection.cursor!.stream,
329
+ chunks,
330
+ })
331
+ : projectedMessageStream({ ...projection, batches })
332
+ return {
333
+ message: projected.message,
334
+ batches,
335
+ cursor: {
336
+ throughIndex: incremental
337
+ ? index
338
+ : Math.max(
339
+ index,
340
+ batches.tail.at(-1)?.index ?? 0,
341
+ projection.toolEvents.reduce(
342
+ (latest, event) => Math.max(latest, event.index),
343
+ 0,
344
+ ),
345
+ projection.approvalEvents.reduce(
346
+ (latest, event) => Math.max(latest, event.index),
347
+ 0,
348
+ ),
349
+ ),
350
+ stream: projected.cursor,
351
+ },
352
+ }
353
+ }
354
+
355
+ export const projectedTools = <M extends UIMessage>(
356
+ projection: ActiveGenerationProjection<M>,
357
+ ): ToolActivity[] =>
358
+ projection.toolEvents.reduce(
359
+ (tools, event) => reduceToolActivity(tools, event),
360
+ projection.baseTools,
361
+ )
362
+
363
+ export const restoreGenerationBase = <M extends UIMessage>(
364
+ state: AIState<M>,
365
+ generationId: string,
366
+ responseMessageId: string,
367
+ reason: 'interrupted' | 'superseded' | 'failed',
368
+ ): AIState<M> => {
369
+ const projection =
370
+ state.activeProjection?.generationId === generationId
371
+ ? state.activeProjection
372
+ : null
373
+ return {
374
+ ...state,
375
+ messages:
376
+ projection?.baseMessage === undefined
377
+ ? state.messages.filter((message) => message.id !== responseMessageId)
378
+ : upsertMessage(state.messages, projection.baseMessage),
379
+ tools:
380
+ projection === null
381
+ ? state.tools.filter(
382
+ (activity) => activity.generationId !== generationId,
383
+ )
384
+ : projection.baseTools,
385
+ pendingApprovals: state.pendingApprovals.filter(
386
+ (approval) => approval.generationId !== generationId,
387
+ ),
388
+ pendingInputs: state.pendingInputs.filter(
389
+ (input) => input.generationId !== generationId,
390
+ ),
391
+ activeGeneration: null,
392
+ activeProjection: null,
393
+ terminalGenerations: {
394
+ ...state.terminalGenerations,
395
+ [generationId]: reason,
396
+ },
397
+ compaction:
398
+ projection === null
399
+ ? state.compaction?.generationId === generationId
400
+ ? null
401
+ : state.compaction
402
+ : (projection.baseCompaction ?? null),
403
+ }
404
+ }
405
+
406
+ export const stopGenerationTools = (
407
+ tools: ToolActivity[],
408
+ generationId: string | undefined,
409
+ responseMessageId: string,
410
+ error: string,
411
+ ): ToolActivity[] =>
412
+ tools.map((activity) =>
413
+ activity.status === 'running' &&
414
+ (generationId === undefined
415
+ ? activity.messageId === responseMessageId
416
+ : activity.generationId === generationId)
417
+ ? { ...activity, status: 'failed', error, preliminary: false }
418
+ : activity,
419
+ )
420
+
421
+ export const interruptGenerationProjection = <M extends UIMessage>(
422
+ state: AIState<M>,
423
+ generationId: string,
424
+ responseMessageId: string,
425
+ cutoff: number,
426
+ ): AIState<M> | null => {
427
+ const projection =
428
+ state.activeProjection?.generationId === generationId
429
+ ? state.activeProjection
430
+ : null
431
+ if (projection === null) return null
432
+ const retained: ActiveGenerationProjection<M> = {
433
+ ...projection,
434
+ batches: progressBatches(
435
+ flattenProgressBatches(projection.batches).filter(
436
+ (batch) => batch.index <= cutoff,
437
+ ),
438
+ ),
439
+ toolEvents: projection.toolEvents.filter(
440
+ (toolEvent) => toolEvent.index <= cutoff,
441
+ ),
442
+ approvalEvents: projection.approvalEvents.filter(
443
+ (approvalEvent) => approvalEvent.index <= cutoff,
444
+ ),
445
+ }
446
+ const shouldProject =
447
+ retained.baseMessage !== undefined || retained.batches.length > 0
448
+ const restored = restoreGenerationBase(
449
+ state,
450
+ generationId,
451
+ responseMessageId,
452
+ 'interrupted',
453
+ )
454
+ return {
455
+ ...restored,
456
+ messages: shouldProject
457
+ ? upsertMessage(
458
+ state.messages,
459
+ interruptUIMessage(projectedMessage(retained)),
460
+ )
461
+ : restored.messages,
462
+ tools: stopGenerationTools(
463
+ projectedTools(retained),
464
+ generationId,
465
+ responseMessageId,
466
+ 'Tool execution was interrupted.',
467
+ ),
468
+ usage:
469
+ projection.completionIndex === undefined ||
470
+ projection.completionIndex <= cutoff
471
+ ? restored.usage
472
+ : restored.usage.filter((entry) => entry.generationId !== generationId),
473
+ }
474
+ }