veryfront 0.1.322 → 0.1.324

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.
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.1.322";
1
+ export declare const VERSION = "0.1.324";
2
2
  //# sourceMappingURL=version-constant.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
- export const VERSION = "0.1.322";
3
+ export const VERSION = "0.1.324";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.1.322",
3
+ "version": "0.1.324",
4
4
  "description": "The simplest way to build AI-powered apps",
5
5
  "keywords": [
6
6
  "react",
package/src/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.322",
3
+ "version": "0.1.324",
4
4
  "license": "Apache-2.0",
5
5
  "nodeModulesDir": "auto",
6
6
  "workspace": [
@@ -0,0 +1,596 @@
1
+ import * as dntShim from "../../_dnt.shims.js";
2
+ import type { ChatFinishReason, ChatStreamEvent } from "../chat/protocol.js";
3
+ import type {
4
+ ChatDynamicToolUiPart,
5
+ ChatUiMessage,
6
+ ChatUiMessageChunk,
7
+ MessageMetadata,
8
+ } from "../chat/types.js";
9
+ import { createAgUiRuntimeChatStreamEncoder } from "./ag-ui-runtime-chat-stream-encoder.js";
10
+ import {
11
+ mergeToolInputDelta,
12
+ parseToolInputObject,
13
+ streamDataStreamEvents,
14
+ stripLeadingEmptyObjectPlaceholder,
15
+ } from "./data-stream.js";
16
+ import {
17
+ normalizeChatMessageMetadata,
18
+ normalizeChatUiMessageStream,
19
+ } from "../chat/chat-ui-message-helpers.js";
20
+
21
+ export type ChatUiMessageStreamFinishPart = {
22
+ type: "finish";
23
+ finishReason: ChatFinishReason;
24
+ rawFinishReason: ChatFinishReason;
25
+ totalUsage: {
26
+ inputTokens: number;
27
+ outputTokens: number;
28
+ totalTokens: number;
29
+ inputTokenDetails: {
30
+ noCacheTokens?: number;
31
+ cacheReadTokens?: number;
32
+ cacheWriteTokens?: number;
33
+ };
34
+ outputTokenDetails: {
35
+ textTokens?: number;
36
+ reasoningTokens?: number;
37
+ };
38
+ };
39
+ };
40
+
41
+ export type ChatUiMessageStreamFinish<TMessageMetadata = MessageMetadata> = {
42
+ messages: Array<ChatUiMessage<TMessageMetadata>>;
43
+ isContinuation: false;
44
+ responseMessage: ChatUiMessage<TMessageMetadata>;
45
+ isAborted: false;
46
+ finishReason: ChatFinishReason;
47
+ };
48
+
49
+ export type ChatUiMessageStreamOptions<TMessageMetadata = MessageMetadata> = {
50
+ generateMessageId?: () => string;
51
+ sendReasoning?: boolean;
52
+ onError?: (error: unknown) => string;
53
+ messageMetadata?: (
54
+ input: { part: ChatUiMessageStreamFinishPart },
55
+ ) => TMessageMetadata | undefined;
56
+ onFinish?: (finish: ChatUiMessageStreamFinish<TMessageMetadata>) => void | Promise<void>;
57
+ onOrphanedToolInput?: (input: { toolCallId: string; inputText: string }) => void;
58
+ };
59
+
60
+ type OrderedTextBlock = {
61
+ id: string;
62
+ order: number;
63
+ text: string;
64
+ };
65
+
66
+ type ToolPart = {
67
+ toolCallId: string;
68
+ toolName: string;
69
+ order: number;
70
+ inputText: string;
71
+ input: Record<string, unknown>;
72
+ state: "input-available" | "output-available" | "output-error";
73
+ output?: unknown;
74
+ errorText?: string;
75
+ };
76
+
77
+ type DataPart = {
78
+ name: string;
79
+ order: number;
80
+ value: unknown;
81
+ };
82
+
83
+ type PendingToolDelta = {
84
+ inputText: string;
85
+ chunks: string[];
86
+ };
87
+
88
+ type FrameworkUiMessageState = {
89
+ textBlocks: Map<string, OrderedTextBlock>;
90
+ reasoningBlocks: Map<string, OrderedTextBlock>;
91
+ toolParts: Map<string, ToolPart>;
92
+ dataParts: DataPart[];
93
+ pendingToolDeltas: Map<string, PendingToolDelta>;
94
+ nextOrder: number;
95
+ };
96
+
97
+ function createFrameworkUiMessageState(): FrameworkUiMessageState {
98
+ return {
99
+ textBlocks: new Map(),
100
+ reasoningBlocks: new Map(),
101
+ toolParts: new Map(),
102
+ dataParts: [],
103
+ pendingToolDeltas: new Map(),
104
+ nextOrder: 0,
105
+ };
106
+ }
107
+
108
+ function isRecord(value: unknown): value is Record<string, unknown> {
109
+ return typeof value === "object" && value !== null && !Array.isArray(value);
110
+ }
111
+
112
+ function getStringField(event: ChatStreamEvent, key: string): string | undefined {
113
+ const value = event[key as keyof ChatStreamEvent];
114
+ return typeof value === "string" ? value : undefined;
115
+ }
116
+
117
+ function appendPendingToolDelta(
118
+ state: FrameworkUiMessageState,
119
+ toolCallId: string,
120
+ inputTextDelta: string,
121
+ ): void {
122
+ const existing = state.pendingToolDeltas.get(toolCallId);
123
+ if (existing) {
124
+ existing.inputText = mergeToolInputDelta(existing.inputText, inputTextDelta);
125
+ existing.chunks.push(inputTextDelta);
126
+ return;
127
+ }
128
+
129
+ state.pendingToolDeltas.set(toolCallId, {
130
+ inputText: inputTextDelta,
131
+ chunks: [inputTextDelta],
132
+ });
133
+ }
134
+
135
+ function trackPendingFrameworkToolInput(input: {
136
+ state: FrameworkUiMessageState;
137
+ materializedToolCallIds: Set<string>;
138
+ event: Record<string, unknown> & { type: string };
139
+ }): void {
140
+ const { state, materializedToolCallIds, event } = input;
141
+
142
+ if (event.type === "tool-input-delta") {
143
+ const toolCallId = typeof event.toolCallId === "string" ? event.toolCallId : null;
144
+ const inputTextDelta = typeof event.inputTextDelta === "string"
145
+ ? event.inputTextDelta
146
+ : typeof event.delta === "string"
147
+ ? event.delta
148
+ : "";
149
+ if (toolCallId && inputTextDelta.length > 0 && !materializedToolCallIds.has(toolCallId)) {
150
+ appendPendingToolDelta(state, toolCallId, inputTextDelta);
151
+ }
152
+ return;
153
+ }
154
+
155
+ if (event.type === "tool-input-start" || event.type === "tool-input-available") {
156
+ if (typeof event.toolCallId === "string") {
157
+ materializedToolCallIds.add(event.toolCallId);
158
+ state.pendingToolDeltas.delete(event.toolCallId);
159
+ }
160
+ }
161
+ }
162
+
163
+ function getParsedStreamedToolInput(inputText: string): Record<string, unknown> | null {
164
+ const normalizedInputText = stripLeadingEmptyObjectPlaceholder(inputText).trim();
165
+ if (normalizedInputText.length === 0) {
166
+ return {};
167
+ }
168
+
169
+ try {
170
+ const parsed = JSON.parse(normalizedInputText);
171
+ return isRecord(parsed) ? Object.fromEntries(Object.entries(parsed)) : {};
172
+ } catch {
173
+ return null;
174
+ }
175
+ }
176
+
177
+ function observeChatStreamEvent(input: {
178
+ event: ChatStreamEvent;
179
+ responseMessageId: string;
180
+ state: FrameworkUiMessageState;
181
+ }): void {
182
+ const { event, responseMessageId, state } = input;
183
+
184
+ switch (event.type) {
185
+ case "text-start": {
186
+ const id = event.id || responseMessageId;
187
+ if (!state.textBlocks.has(id)) {
188
+ state.textBlocks.set(id, { id, order: state.nextOrder, text: "" });
189
+ state.nextOrder += 1;
190
+ }
191
+ return;
192
+ }
193
+ case "text-delta": {
194
+ const id = event.id || responseMessageId;
195
+ const existingBlock = state.textBlocks.get(id);
196
+ if (existingBlock) {
197
+ existingBlock.text += event.delta;
198
+ return;
199
+ }
200
+ state.textBlocks.set(id, { id, order: state.nextOrder, text: event.delta });
201
+ state.nextOrder += 1;
202
+ return;
203
+ }
204
+ case "reasoning-start": {
205
+ if (!state.reasoningBlocks.has(event.id)) {
206
+ state.reasoningBlocks.set(event.id, { id: event.id, order: state.nextOrder, text: "" });
207
+ state.nextOrder += 1;
208
+ }
209
+ return;
210
+ }
211
+ case "reasoning-delta": {
212
+ const existingBlock = state.reasoningBlocks.get(event.id);
213
+ if (existingBlock) {
214
+ existingBlock.text += event.delta;
215
+ return;
216
+ }
217
+ state.reasoningBlocks.set(event.id, {
218
+ id: event.id,
219
+ order: state.nextOrder,
220
+ text: event.delta,
221
+ });
222
+ state.nextOrder += 1;
223
+ return;
224
+ }
225
+ case "tool-input-start": {
226
+ if (!state.toolParts.has(event.toolCallId)) {
227
+ state.toolParts.set(event.toolCallId, {
228
+ toolCallId: event.toolCallId,
229
+ toolName: event.toolName,
230
+ order: state.nextOrder,
231
+ inputText: "",
232
+ input: {},
233
+ state: "input-available",
234
+ });
235
+ state.nextOrder += 1;
236
+ }
237
+
238
+ const pendingToolDelta = state.pendingToolDeltas.get(event.toolCallId);
239
+ if (!pendingToolDelta) {
240
+ return;
241
+ }
242
+
243
+ const toolPart = state.toolParts.get(event.toolCallId);
244
+ if (!toolPart) {
245
+ return;
246
+ }
247
+
248
+ toolPart.inputText = pendingToolDelta.inputText;
249
+ const parsedInput = getParsedStreamedToolInput(toolPart.inputText);
250
+ if (parsedInput) {
251
+ toolPart.input = parsedInput;
252
+ }
253
+ state.pendingToolDeltas.delete(event.toolCallId);
254
+ return;
255
+ }
256
+ case "tool-input-delta": {
257
+ const toolPart = state.toolParts.get(event.toolCallId);
258
+ if (!toolPart) {
259
+ appendPendingToolDelta(state, event.toolCallId, event.inputTextDelta);
260
+ return;
261
+ }
262
+ toolPart.inputText = mergeToolInputDelta(toolPart.inputText, event.inputTextDelta);
263
+ const parsedInput = getParsedStreamedToolInput(toolPart.inputText);
264
+ if (parsedInput) {
265
+ toolPart.input = parsedInput;
266
+ }
267
+ return;
268
+ }
269
+ case "tool-input-available": {
270
+ const toolPart = state.toolParts.get(event.toolCallId);
271
+ const input = parseToolInputObject(event.input);
272
+ if (toolPart) {
273
+ toolPart.toolName = event.toolName;
274
+ toolPart.input = input;
275
+ toolPart.state = "input-available";
276
+ } else {
277
+ state.toolParts.set(event.toolCallId, {
278
+ toolCallId: event.toolCallId,
279
+ toolName: event.toolName,
280
+ order: state.nextOrder,
281
+ inputText: "",
282
+ input,
283
+ state: "input-available",
284
+ });
285
+ state.nextOrder += 1;
286
+ }
287
+ state.pendingToolDeltas.delete(event.toolCallId);
288
+ return;
289
+ }
290
+ case "tool-output-available": {
291
+ const toolPart = state.toolParts.get(event.toolCallId);
292
+ if (!toolPart) {
293
+ return;
294
+ }
295
+ toolPart.state = "output-available";
296
+ toolPart.output = event.output;
297
+ return;
298
+ }
299
+ case "tool-output-error":
300
+ case "tool-input-error": {
301
+ const toolPart = state.toolParts.get(event.toolCallId);
302
+ if (toolPart) {
303
+ toolPart.state = "output-error";
304
+ toolPart.errorText = event.errorText;
305
+ if ("input" in event && event.input !== undefined) {
306
+ toolPart.input = parseToolInputObject(event.input);
307
+ }
308
+ return;
309
+ }
310
+ state.toolParts.set(event.toolCallId, {
311
+ toolCallId: event.toolCallId,
312
+ toolName: getStringField(event, "toolName") ?? "unknown",
313
+ order: state.nextOrder,
314
+ inputText: "",
315
+ input: "input" in event && event.input !== undefined
316
+ ? parseToolInputObject(event.input)
317
+ : {},
318
+ state: "output-error",
319
+ errorText: event.errorText,
320
+ });
321
+ state.nextOrder += 1;
322
+ return;
323
+ }
324
+ default: {
325
+ if (!event.type.startsWith("data-")) {
326
+ return;
327
+ }
328
+ if (!("data" in event)) {
329
+ return;
330
+ }
331
+ state.dataParts.push({
332
+ name: event.type.slice("data-".length),
333
+ order: state.nextOrder,
334
+ value: event.data,
335
+ });
336
+ state.nextOrder += 1;
337
+ return;
338
+ }
339
+ }
340
+ }
341
+
342
+ function getOrphanedToolInput(
343
+ state: FrameworkUiMessageState,
344
+ toolCallId: string,
345
+ ): Record<string, unknown> {
346
+ const pending = state.pendingToolDeltas.get(toolCallId);
347
+ if (!pending) {
348
+ return {};
349
+ }
350
+
351
+ const parsedInput = getParsedStreamedToolInput(pending.inputText);
352
+ if (parsedInput) {
353
+ return parsedInput;
354
+ }
355
+
356
+ return {
357
+ __rawInputText: pending.inputText,
358
+ };
359
+ }
360
+
361
+ function buildOrphanedToolInputErrorText(inputText: string): string {
362
+ const normalizedInputText = inputText.trim();
363
+ const preview = normalizedInputText.length > 160
364
+ ? `${normalizedInputText.slice(0, 160)}...`
365
+ : normalizedInputText;
366
+ return preview.length > 0
367
+ ? `Tool input started streaming before the tool lifecycle was established and never materialized into an executable tool call. Buffered args: ${preview}`
368
+ : "Tool input started streaming before the tool lifecycle was established and never materialized into an executable tool call.";
369
+ }
370
+
371
+ function buildResponseMessageParts(state: FrameworkUiMessageState): ChatUiMessage["parts"] {
372
+ const orderedParts: Array<{ order: number; part: ChatUiMessage["parts"][number] }> = [];
373
+
374
+ for (const textBlock of state.textBlocks.values()) {
375
+ if (textBlock.text.length === 0) {
376
+ continue;
377
+ }
378
+
379
+ orderedParts.push({
380
+ order: textBlock.order,
381
+ part: {
382
+ type: "text",
383
+ text: textBlock.text,
384
+ },
385
+ });
386
+ }
387
+
388
+ for (const reasoningBlock of state.reasoningBlocks.values()) {
389
+ if (reasoningBlock.text.length === 0) {
390
+ continue;
391
+ }
392
+
393
+ orderedParts.push({
394
+ order: reasoningBlock.order,
395
+ part: {
396
+ type: "reasoning",
397
+ text: reasoningBlock.text,
398
+ },
399
+ });
400
+ }
401
+
402
+ for (const toolPart of state.toolParts.values()) {
403
+ const basePart: Pick<
404
+ ChatDynamicToolUiPart,
405
+ "type" | "toolName" | "toolCallId" | "input"
406
+ > = {
407
+ type: "dynamic-tool",
408
+ toolName: toolPart.toolName,
409
+ toolCallId: toolPart.toolCallId,
410
+ input: toolPart.input,
411
+ };
412
+
413
+ const part: ChatUiMessage["parts"][number] = toolPart.state === "output-available"
414
+ ? {
415
+ ...basePart,
416
+ state: "output-available",
417
+ output: toolPart.output,
418
+ }
419
+ : toolPart.state === "output-error"
420
+ ? {
421
+ ...basePart,
422
+ state: "output-error",
423
+ errorText: toolPart.errorText ?? "Tool execution failed",
424
+ }
425
+ : {
426
+ ...basePart,
427
+ state: "input-available",
428
+ };
429
+
430
+ orderedParts.push({
431
+ order: toolPart.order,
432
+ part,
433
+ });
434
+ }
435
+
436
+ for (const dataPart of state.dataParts) {
437
+ orderedParts.push({
438
+ order: dataPart.order,
439
+ part: {
440
+ type: `data-${dataPart.name}`,
441
+ data: dataPart.value,
442
+ },
443
+ });
444
+ }
445
+
446
+ return orderedParts.sort((left, right) => left.order - right.order).map((entry) => entry.part);
447
+ }
448
+
449
+ function buildFinishPart(finishReason: ChatFinishReason): ChatUiMessageStreamFinishPart {
450
+ return {
451
+ type: "finish",
452
+ finishReason,
453
+ rawFinishReason: finishReason,
454
+ totalUsage: {
455
+ inputTokens: 0,
456
+ outputTokens: 0,
457
+ totalTokens: 0,
458
+ inputTokenDetails: {
459
+ noCacheTokens: undefined,
460
+ cacheReadTokens: undefined,
461
+ cacheWriteTokens: undefined,
462
+ },
463
+ outputTokenDetails: {
464
+ textTokens: undefined,
465
+ reasoningTokens: undefined,
466
+ },
467
+ },
468
+ };
469
+ }
470
+
471
+ function toUiChunk(event: ChatStreamEvent): ChatUiMessageChunk<MessageMetadata> | null {
472
+ switch (event.type) {
473
+ case "start":
474
+ return {
475
+ type: "start",
476
+ ...(event.messageId ? { messageId: event.messageId } : {}),
477
+ ...(event.messageMetadata !== undefined
478
+ ? { messageMetadata: normalizeChatMessageMetadata(event.messageMetadata) }
479
+ : {}),
480
+ };
481
+ case "finish":
482
+ return {
483
+ type: "finish",
484
+ ...(event.finishReason ? { finishReason: event.finishReason } : {}),
485
+ };
486
+ case "message-metadata":
487
+ return {
488
+ type: "message-metadata",
489
+ messageMetadata: normalizeChatMessageMetadata(event.messageMetadata),
490
+ };
491
+ default:
492
+ return event;
493
+ }
494
+ }
495
+
496
+ export function createChatUiMessageStreamFromDataStream<TMessageMetadata = MessageMetadata>(
497
+ input: { stream: ReadableStream<Uint8Array> },
498
+ options: ChatUiMessageStreamOptions<TMessageMetadata> = {},
499
+ ): AsyncIterable<ChatUiMessageChunk<MessageMetadata>> {
500
+ const responseMessageId = options.generateMessageId?.() ?? dntShim.crypto.randomUUID();
501
+ const state = createFrameworkUiMessageState();
502
+ const chatEventEncoder = createAgUiRuntimeChatStreamEncoder({
503
+ responseMessageId,
504
+ sendReasoning: options.sendReasoning,
505
+ onError: options.onError,
506
+ });
507
+ const materializedToolCallIds = new Set<string>();
508
+ let finishReason: ChatFinishReason = "stop";
509
+
510
+ return normalizeChatUiMessageStream(
511
+ (async function* () {
512
+ const ensureStepStarted = function* (shouldStartStep: boolean) {
513
+ if (shouldStartStep) {
514
+ yield { type: "start-step" as const };
515
+ }
516
+ };
517
+
518
+ yield {
519
+ type: "start",
520
+ messageId: responseMessageId,
521
+ };
522
+
523
+ for await (const event of streamDataStreamEvents(input.stream)) {
524
+ trackPendingFrameworkToolInput({
525
+ state,
526
+ materializedToolCallIds,
527
+ event,
528
+ });
529
+ const chatEvents = chatEventEncoder.encode(event);
530
+ finishReason = chatEventEncoder.state.finishReason;
531
+ for (const chatEvent of chatEvents) {
532
+ observeChatStreamEvent({
533
+ event: chatEvent,
534
+ responseMessageId,
535
+ state,
536
+ });
537
+ const chunk = toUiChunk(chatEvent);
538
+ if (chunk) {
539
+ yield chunk;
540
+ }
541
+ }
542
+ }
543
+
544
+ for (const [toolCallId, pendingToolDelta] of state.pendingToolDeltas.entries()) {
545
+ yield* ensureStepStarted(!chatEventEncoder.state.isStepOpen);
546
+ chatEventEncoder.state.isStepOpen = true;
547
+ const toolInput = getOrphanedToolInput(state, toolCallId);
548
+ const errorText = buildOrphanedToolInputErrorText(pendingToolDelta.inputText);
549
+
550
+ options.onOrphanedToolInput?.({ toolCallId, inputText: pendingToolDelta.inputText });
551
+
552
+ state.toolParts.set(toolCallId, {
553
+ toolCallId,
554
+ toolName: "unknown",
555
+ order: state.nextOrder,
556
+ inputText: pendingToolDelta.inputText,
557
+ input: toolInput,
558
+ state: "output-error",
559
+ errorText,
560
+ });
561
+ state.nextOrder += 1;
562
+
563
+ yield {
564
+ type: "tool-input-error",
565
+ toolCallId,
566
+ toolName: "unknown",
567
+ input: toolInput,
568
+ errorText,
569
+ };
570
+ }
571
+ state.pendingToolDeltas.clear();
572
+
573
+ const finishPart = buildFinishPart(finishReason);
574
+ const messageMetadata = options.messageMetadata?.({ part: finishPart });
575
+ const responseMessage: ChatUiMessage<TMessageMetadata> = {
576
+ id: responseMessageId,
577
+ role: "assistant",
578
+ parts: buildResponseMessageParts(state),
579
+ ...(messageMetadata ? { metadata: messageMetadata } : {}),
580
+ };
581
+
582
+ await options.onFinish?.({
583
+ messages: [responseMessage],
584
+ isContinuation: false,
585
+ responseMessage,
586
+ isAborted: false,
587
+ finishReason: finishPart.finishReason,
588
+ });
589
+
590
+ yield {
591
+ type: "finish",
592
+ finishReason,
593
+ };
594
+ })(),
595
+ );
596
+ }
@@ -231,6 +231,12 @@ export {
231
231
  createAgUiRuntimeBrowserResponse,
232
232
  type CreateAgUiRuntimeBrowserResponseInput,
233
233
  } from "./ag-ui-runtime-browser-response.js";
234
+ export {
235
+ type ChatUiMessageStreamFinish,
236
+ type ChatUiMessageStreamFinishPart,
237
+ type ChatUiMessageStreamOptions,
238
+ createChatUiMessageStreamFromDataStream,
239
+ } from "./chat-ui-message-stream.js";
234
240
  export {
235
241
  createAgUiTrackedBrowserResponse,
236
242
  type CreateAgUiTrackedBrowserResponseInput,