vibefast-cli 0.7.12 → 0.7.14

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 (97) hide show
  1. package/dist/commands/add.d.ts.map +1 -1
  2. package/dist/commands/add.js +28 -2
  3. package/dist/commands/add.js.map +1 -1
  4. package/dist/commands/init.d.ts.map +1 -1
  5. package/dist/commands/init.js +5 -3
  6. package/dist/commands/init.js.map +1 -1
  7. package/package.json +1 -1
  8. package/recipes/audio-recorder/recipe.json +1 -1
  9. package/recipes/audio-recorder@latest.zip +0 -0
  10. package/recipes/charts/apps/native/src/app/{charts → (root)/(protected)/charts}/index.tsx +0 -3
  11. package/recipes/charts/apps/native/src/features/charts/app/preview.tsx +0 -3
  12. package/recipes/charts/apps/native/src/features/charts/components/area-chart.tsx +0 -3
  13. package/recipes/charts/apps/native/src/features/charts/components/bar-chart.tsx +0 -3
  14. package/recipes/charts/apps/native/src/features/charts/components/candlestick-chart.tsx +0 -3
  15. package/recipes/charts/apps/native/src/features/charts/components/chart-card.tsx +0 -3
  16. package/recipes/charts/apps/native/src/features/charts/components/column-chart.tsx +0 -3
  17. package/recipes/charts/apps/native/src/features/charts/components/doughnut-chart.tsx +0 -3
  18. package/recipes/charts/apps/native/src/features/charts/components/index.ts +0 -3
  19. package/recipes/charts/apps/native/src/features/charts/components/line-chart.tsx +0 -3
  20. package/recipes/charts/apps/native/src/features/charts/components/radar-chart.tsx +0 -3
  21. package/recipes/charts/apps/native/src/features/charts/components/radial-bar-chart.tsx +0 -3
  22. package/recipes/charts/apps/native/src/features/charts/components/stacked-area-chart.tsx +0 -3
  23. package/recipes/charts/apps/native/src/features/charts/components/stacked-bar-chart.tsx +0 -3
  24. package/recipes/charts/apps/native/src/features/charts/data/mock-data.ts +0 -3
  25. package/recipes/charts/apps/native/src/features/charts/types/index.ts +0 -3
  26. package/recipes/charts/recipe.json +1 -1
  27. package/recipes/charts@latest.zip +0 -0
  28. package/recipes/chatbot/apps/native/src/api-client/chatbot.ts +83 -0
  29. package/recipes/chatbot/apps/native/src/app/{chatbot → (root)/(protected)/chatbot}/index.tsx +0 -1
  30. package/recipes/chatbot/apps/native/src/features/chatbot/app/index.tsx +56 -60
  31. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-header-buttons.tsx +0 -1
  32. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-input-bar.tsx +0 -1
  33. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-markdown.tsx +0 -1
  34. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-message-bubble.tsx +3 -26
  35. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-settings-modal.tsx +0 -1
  36. package/recipes/chatbot/apps/native/src/features/chatbot/components/image-preview-list.tsx +0 -1
  37. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/code-block.tsx +0 -1
  38. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/index.ts +0 -1
  39. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/table-renderer.tsx +0 -1
  40. package/recipes/chatbot/apps/native/src/features/chatbot/components/message-error-boundary.tsx +0 -1
  41. package/recipes/chatbot/apps/native/src/features/chatbot/components/message-list.tsx +10 -14
  42. package/recipes/chatbot/apps/native/src/features/chatbot/components/model-selector.tsx +0 -1
  43. package/recipes/chatbot/apps/native/src/features/chatbot/components/report-content-modal.tsx +0 -1
  44. package/recipes/chatbot/apps/native/src/features/chatbot/components/suggested-messages.tsx +0 -1
  45. package/recipes/chatbot/apps/native/src/features/chatbot/constants/models.ts +0 -1
  46. package/recipes/chatbot/apps/native/src/features/chatbot/constants/report-reasons.ts +0 -1
  47. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-attachment-cache.ts +0 -1
  48. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-config.ts +0 -1
  49. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-handlers.ts +0 -1
  50. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chatbot-settings.ts +0 -1
  51. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-conversation.ts +0 -1
  52. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-image-picker.ts +0 -1
  53. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-keyboard-coordinator.ts +0 -1
  54. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-smart-scroll-manager.ts +0 -1
  55. package/recipes/chatbot/apps/native/src/features/chatbot/models/index.ts +0 -1
  56. package/recipes/chatbot/apps/native/src/features/chatbot/models/models.ts +0 -1
  57. package/recipes/chatbot/apps/native/src/features/chatbot/models/providers.ts +0 -1
  58. package/recipes/chatbot/apps/native/src/features/chatbot/models/types.ts +0 -1
  59. package/recipes/chatbot/apps/native/src/features/chatbot/services/file-uploader.ts +0 -1
  60. package/recipes/chatbot/apps/native/src/features/chatbot/services/message-handler-service.ts +0 -1
  61. package/recipes/chatbot/apps/native/src/features/chatbot/types/index.ts +5 -3
  62. package/recipes/chatbot/apps/native/src/features/chatbot/utils/chat-telemetry.ts +0 -1
  63. package/recipes/chatbot/packages/backend/convex/agents.ts +3 -4
  64. package/recipes/chatbot/packages/backend/convex/chatbot/content.ts +35 -0
  65. package/recipes/chatbot/packages/backend/convex/chatbot/sessions.ts +52 -0
  66. package/recipes/chatbot/packages/backend/convex/chatbot/streaming.ts +422 -0
  67. package/recipes/chatbot/packages/backend/convex/chatbot/telemetry.ts +56 -0
  68. package/recipes/chatbot/packages/backend/convex/chatbot/tools.ts +128 -0
  69. package/recipes/chatbot/packages/backend/convex/chatbotAgent.ts +6 -651
  70. package/recipes/chatbot/packages/backend/convex/ragKnowledge.ts +0 -714
  71. package/recipes/chatbot/packages/backend/convex/tools/knowledgeRetrieval.ts +12 -7
  72. package/recipes/chatbot/recipe.json +6 -1
  73. package/recipes/chatbot@latest.zip +0 -0
  74. package/recipes/image-generator/apps/native/src/api-client/image-generator.ts +34 -0
  75. package/recipes/image-generator/packages/backend/convex/{imageGeneratorFunctions.ts → imageGenerator.ts} +1 -1
  76. package/recipes/image-generator/recipe.json +5 -1
  77. package/recipes/image-generator@latest.zip +0 -0
  78. package/recipes/payments/apps/native/src/api-client/payments.ts +44 -0
  79. package/recipes/payments/packages/backend/convex/payments/index.ts +13 -0
  80. package/recipes/payments/packages/backend/convex/payments.ts +119 -0
  81. package/recipes/payments/recipe.json +15 -2
  82. package/recipes/payments@latest.zip +0 -0
  83. package/recipes/quiz/recipe.json +1 -1
  84. package/recipes/quiz@latest.zip +0 -0
  85. package/recipes/tracker-app/recipe.json +1 -1
  86. package/recipes/tracker-app@latest.zip +0 -0
  87. package/recipes/voice-bot/recipe.json +1 -1
  88. package/recipes/voice-bot@latest.zip +0 -0
  89. package/src/commands/add.ts +108 -70
  90. package/src/commands/init.ts +5 -3
  91. package/tmp-npm-cache/_update-notifier-last-checked +0 -0
  92. /package/recipes/audio-recorder/apps/native/src/app/{audio-recorder → (root)/(protected)/audio-recorder}/index.tsx +0 -0
  93. /package/recipes/image-generator/apps/native/src/app/{image-generator → (root)/(protected)/image-generator}/gallery.tsx +0 -0
  94. /package/recipes/image-generator/apps/native/src/app/{image-generator → (root)/(protected)/image-generator}/index.tsx +0 -0
  95. /package/recipes/quiz/apps/native/src/app/{quiz → (root)/(protected)/quiz}/index.tsx +0 -0
  96. /package/recipes/tracker-app/apps/native/src/app/{tracker-app → (root)/(protected)/tracker-app}/index.tsx +0 -0
  97. /package/recipes/voice-bot/apps/native/src/app/{voice-bot → (root)/(protected)/voice-bot}/index.tsx +0 -0
@@ -1,216 +1,25 @@
1
1
  import { getAuthUserId } from '@convex-dev/auth/server';
2
2
  import { PersistentTextStreaming } from '@convex-dev/persistent-text-streaming';
3
- import type {
4
- CoreMessage,
5
- FilePart,
6
- ImagePart,
7
- TextPart,
8
- ToolCallUnion,
9
- ToolResultUnion,
10
- } from 'ai';
11
3
  import { v } from 'convex/values';
12
4
 
5
+ import { handleRunStreaming } from './chatbot/streaming';
6
+ import { ensureAgentSession, getSessionForConversation } from './chatbot/sessions';
7
+ import { createTelemetryTracker } from './chatbot/telemetry';
13
8
  import { components, internal } from './_generated/api';
14
- import type { Doc, Id } from './_generated/dataModel';
9
+ import type { Id } from './_generated/dataModel';
15
10
  import {
16
11
  internalAction,
17
12
  internalMutation,
18
13
  internalQuery,
19
- type MutationCtx,
20
14
  mutation,
21
- type QueryCtx,
22
15
  query,
23
16
  } from './_generated/server';
24
- import { agent, DEFAULT_MODEL, resolveChatModel } from './agents';
17
+ import { DEFAULT_MODEL } from './agents';
25
18
  import { enforceAgentRateLimit } from './lib/rateLimit';
26
- import { getChatTelemetryMode, type TelemetryMode } from './lib/telemetry';
27
-
28
19
  const persistentTextStreaming = new PersistentTextStreaming(
29
20
  components.persistentTextStreaming,
30
21
  );
31
22
 
32
- type AgentSessionDoc = Doc<'agentSessions'>;
33
- type AgentTools = NonNullable<(typeof agent)['options']['tools']>;
34
-
35
- const SENTENCE_DELIMITER = /[.!?]/;
36
- const MAX_PENDING_CHARS = 180;
37
-
38
- const TOOL_STATUS_LABELS: Record<string, string> = {
39
- knowledgeRetrieval: 'Looking through knowledge…',
40
- tavilySearch: 'Searching the web…',
41
- userProfile: 'Checking your profile…',
42
- };
43
-
44
- function toolLabel(toolName: string) {
45
- return TOOL_STATUS_LABELS[toolName] ?? `Using ${toolName}`;
46
- }
47
-
48
- function mergeToolResults(
49
- toolCalls: ToolCallUnion<AgentTools>[],
50
- toolResults: ToolResultUnion<AgentTools>[],
51
- ) {
52
- if (!toolCalls?.length) {
53
- return [];
54
- }
55
-
56
- const resultByCallId = new Map<string, unknown>();
57
-
58
- for (const result of toolResults ?? []) {
59
- const key =
60
- (result as { toolCallId?: string }).toolCallId ??
61
- (result as { id?: string }).id;
62
- if (key) {
63
- resultByCallId.set(key, (result as { result?: unknown }).result);
64
- }
65
- }
66
-
67
- return toolCalls.map((call) => {
68
- const callId =
69
- (call as { id?: string }).id ??
70
- (call as { toolCallId?: string }).toolCallId ??
71
- call.toolName;
72
-
73
- return {
74
- toolName: call.toolName ?? callId ?? 'tool',
75
- args: (call as { args?: unknown }).args ?? null,
76
- result: callId ? resultByCallId.get(callId) : undefined,
77
- };
78
- });
79
- }
80
-
81
- async function buildUserContent(
82
- ctx: any,
83
- message: Doc<'messages'>,
84
- ): Promise<string | (TextPart | ImagePart | FilePart)[]> {
85
- const attachments = message.attachments ?? [];
86
- if (attachments.length === 0) {
87
- return message.text ?? '';
88
- }
89
-
90
- const parts: (TextPart | ImagePart)[] = [];
91
- if (message.text) {
92
- parts.push({ type: 'text', text: message.text });
93
- }
94
-
95
- for (const attachment of attachments) {
96
- try {
97
- const url = await ctx.storage.getUrl(attachment.storageId);
98
- if (url) {
99
- parts.push({ type: 'image', image: new URL(url) });
100
- }
101
- } catch (error) {
102
- console.error('[chatbotAgent] Failed to resolve attachment URL', {
103
- storageId: attachment.storageId,
104
- error,
105
- });
106
- }
107
- }
108
-
109
- return parts.length > 0 ? parts : (message.text ?? '');
110
- }
111
-
112
- const DEFAULT_TELEMETRY_SAMPLE = 0.1;
113
-
114
- type TelemetryCheckpoint = {
115
- label: string;
116
- at: number;
117
- data?: Record<string, unknown>;
118
- };
119
-
120
- type TelemetryTracker = {
121
- mark: (label: string, data?: Record<string, unknown>) => void;
122
- finalize: (status: 'ok' | 'error', extra?: Record<string, unknown>) => void;
123
- };
124
-
125
- function createTelemetryTracker(
126
- event: string,
127
- baseContext: Record<string, unknown>,
128
- sampleProbability = DEFAULT_TELEMETRY_SAMPLE,
129
- ): TelemetryTracker | null {
130
- const mode: TelemetryMode = getChatTelemetryMode();
131
- const shouldTrack =
132
- mode === 'debug' ||
133
- (mode === 'sample' && Math.random() < sampleProbability);
134
-
135
- if (!shouldTrack) {
136
- return null;
137
- }
138
-
139
- const start = Date.now();
140
- const checkpoints: TelemetryCheckpoint[] = [{ label: 'start', at: start }];
141
-
142
- return {
143
- mark(label, data) {
144
- checkpoints.push({ label, at: Date.now(), data });
145
- },
146
- finalize(status, extra) {
147
- const finishedAt = Date.now();
148
- const timeline = checkpoints.map((checkpoint) => ({
149
- label: checkpoint.label,
150
- msFromStart: checkpoint.at - start,
151
- ...(checkpoint.data ?? {}),
152
- }));
153
-
154
- console.log('[chatbotAgent][telemetry]', {
155
- event,
156
- mode,
157
- status,
158
- totalMs: finishedAt - start,
159
- ...baseContext,
160
- ...(extra ?? {}),
161
- timeline,
162
- });
163
- },
164
- };
165
- }
166
-
167
- async function getSessionForConversation(
168
- ctx: Pick<MutationCtx, 'db'> | Pick<QueryCtx, 'db'>,
169
- conversationId: Id<'conversations'>,
170
- ) {
171
- return await ctx.db
172
- .query('agentSessions')
173
- .withIndex('by_conversationId', (q) =>
174
- q.eq('conversationId', conversationId),
175
- )
176
- .first();
177
- }
178
-
179
- async function ensureAgentSession(
180
- ctx: MutationCtx,
181
- conversationId: Id<'conversations'>,
182
- userId: Id<'users'>,
183
- preferredModel?: string,
184
- ): Promise<{ session: AgentSessionDoc; wasCreated: boolean }> {
185
- const existing = await getSessionForConversation(ctx, conversationId);
186
- if (existing) {
187
- return { session: existing, wasCreated: false };
188
- }
189
-
190
- const { threadId } = await agent.createThread(ctx, {
191
- userId,
192
- });
193
-
194
- const sessionId = await ctx.db.insert('agentSessions', {
195
- conversationId,
196
- userId,
197
- agentThreadId: threadId,
198
- activeStreamId: undefined,
199
- lastUserMessageId: undefined,
200
- lastAssistantMessageId: undefined,
201
- preferredModel: preferredModel ?? DEFAULT_MODEL,
202
- updatedAt: Date.now(),
203
- lastErrorMessage: undefined,
204
- lastErrorAt: undefined,
205
- });
206
-
207
- const session = await ctx.db.get(sessionId);
208
- if (!session) {
209
- throw new Error('Failed to create agent session');
210
- }
211
- return { session, wasCreated: true };
212
- }
213
-
214
23
  export const startAgentStream = mutation({
215
24
  args: {
216
25
  conversationId: v.id('conversations'),
@@ -411,461 +220,7 @@ export const runStreaming = internalAction({
411
220
  preferredModel: v.optional(v.string()),
412
221
  agentSessionId: v.id('agentSessions'),
413
222
  },
414
- handler: async (ctx, args) => {
415
- const telemetry = createTelemetryTracker('runStreaming', {
416
- streamId: args.streamId,
417
- conversationId: args.conversationId,
418
- agentSessionId: args.agentSessionId,
419
- });
420
-
421
- let telemetryStatus: 'ok' | 'error' = 'ok';
422
- const telemetryExtra: Record<string, unknown> = {};
423
- let failureHandled = false;
424
-
425
- const inferErrorMessage = (raw: unknown): string | undefined => {
426
- if (!raw) return undefined;
427
- if (typeof raw === 'string') {
428
- try {
429
- const parsed = JSON.parse(raw);
430
- if (parsed && typeof parsed === 'object') {
431
- if (typeof parsed.error === 'string') {
432
- return parsed.error;
433
- }
434
- if (typeof parsed.message === 'string') {
435
- return parsed.message;
436
- }
437
- }
438
- } catch {
439
- // ignore JSON parse failures, fall back to raw string if concise
440
- }
441
- const trimmed = raw.trim();
442
- if (trimmed.length > 0 && trimmed.length <= 300) {
443
- return trimmed;
444
- }
445
- return undefined;
446
- }
447
- if (raw instanceof Error) {
448
- return raw.message;
449
- }
450
- return undefined;
451
- };
452
-
453
- const markFailure = async (
454
- maybeMessage: unknown,
455
- extra?: Record<string, unknown>,
456
- ) => {
457
- telemetryStatus = 'error';
458
- if (extra) {
459
- Object.assign(telemetryExtra, extra);
460
- }
461
- failureHandled = true;
462
- const message =
463
- inferErrorMessage(maybeMessage) ??
464
- 'Sorry, I ran into a technical issue while replying. Please try again.';
465
-
466
- try {
467
- await ctx.runMutation(
468
- components.persistentTextStreaming.lib.setStreamStatus,
469
- {
470
- streamId: args.streamId,
471
- status: 'error',
472
- },
473
- );
474
- } catch (statusError) {
475
- console.error(
476
- '[chatbotAgent] Failed to update stream status to error',
477
- {
478
- streamId: args.streamId,
479
- statusError,
480
- },
481
- );
482
- }
483
-
484
- await ctx.runMutation(internal.chatbotAgent.markStreamFailed, {
485
- agentSessionId: args.agentSessionId,
486
- conversationId: args.conversationId,
487
- streamId: args.streamId,
488
- errorMessage: message,
489
- });
490
- };
491
-
492
- try {
493
- telemetry?.mark('conversation.lookup');
494
- const conversation = await ctx.runQuery(
495
- internal.chatbotAgent.getConversationForStreaming,
496
- { conversationId: args.conversationId },
497
- );
498
- if (!conversation) {
499
- await markFailure('Conversation not found.', {
500
- reason: 'conversationMissing',
501
- });
502
- return;
503
- }
504
-
505
- telemetry?.mark('session.lookup');
506
- const session = await ctx.runQuery(
507
- internal.chatbotAgent.getAgentSessionById,
508
- { agentSessionId: args.agentSessionId },
509
- );
510
- if (!session) {
511
- await markFailure('Agent session missing.', {
512
- reason: 'sessionMissing',
513
- });
514
- return;
515
- }
516
-
517
- telemetry?.mark('message.lookup');
518
- const message = await ctx.runQuery(
519
- internal.chatbotAgent.getMessageForStreaming,
520
- { messageId: args.userMessageId },
521
- );
522
- if (!message || message.conversationId !== args.conversationId) {
523
- await markFailure('User message missing.', {
524
- reason: 'userMessageMissing',
525
- });
526
- return;
527
- }
528
-
529
- const { model, provider, modelId } = resolveChatModel(
530
- args.preferredModel ?? session.preferredModel ?? undefined,
531
- );
532
- telemetryExtra.provider = provider;
533
- telemetryExtra.modelId = modelId;
534
- telemetry?.mark('model.resolved', { provider, modelId });
535
-
536
- const userContent = await buildUserContent(ctx, message);
537
- telemetry?.mark('content.resolved', {
538
- hasAttachments: Boolean(message.attachments?.length),
539
- });
540
-
541
- const { thread } = await agent.continueThread(ctx, {
542
- threadId: args.agentThreadId,
543
- userId: conversation.userId,
544
- });
545
- telemetry?.mark('agent.threadReady');
546
-
547
- const messages: CoreMessage[] = [
548
- {
549
- role: 'user',
550
- content: userContent as string | (TextPart | ImagePart | FilePart)[],
551
- },
552
- ];
553
-
554
- let streamResult;
555
- try {
556
- streamResult = await thread.streamText(
557
- {
558
- model,
559
- messages,
560
- },
561
- {
562
- storageOptions: {
563
- saveMessages: 'promptAndOutput',
564
- },
565
- },
566
- );
567
- } catch (streamInitError) {
568
- console.error('[chatbotAgent] Failed to initialize stream', {
569
- streamInitError,
570
- provider,
571
- modelId,
572
- });
573
- await markFailure(streamInitError, {
574
- reason: 'streamInitFailed',
575
- provider,
576
- modelId,
577
- });
578
- return;
579
- }
580
-
581
- telemetry?.mark('agent.stream.started');
582
-
583
- const activeToolCalls = new Map<
584
- string,
585
- { toolName: string; label: string }
586
- >();
587
-
588
- type StatusRecord =
589
- | null
590
- | { type: 'thinking'; label: string | null }
591
- | {
592
- type: 'tool';
593
- label: string | null;
594
- toolName: string | null;
595
- toolCallId: string | null;
596
- };
597
-
598
- let lastStatus: StatusRecord = { type: 'thinking', label: 'Thinking…' };
599
-
600
- const applyStatus = async (
601
- status:
602
- | { type: 'thinking'; label?: string }
603
- | {
604
- type: 'tool';
605
- toolName: string;
606
- label?: string;
607
- toolCallId?: string;
608
- }
609
- | null,
610
- ) => {
611
- const normalized: StatusRecord = status
612
- ? status.type === 'tool'
613
- ? {
614
- type: 'tool',
615
- label: status.label ?? null,
616
- toolName: status.toolName,
617
- toolCallId: status.toolCallId ?? null,
618
- }
619
- : {
620
- type: 'thinking',
621
- label: status.label ?? null,
622
- }
623
- : null;
624
-
625
- if (normalized === null && lastStatus === null) {
626
- return;
627
- }
628
-
629
- if (
630
- normalized &&
631
- lastStatus &&
632
- normalized.type === lastStatus.type &&
633
- normalized.label === lastStatus.label &&
634
- (normalized.type !== 'tool' ||
635
- (lastStatus.type === 'tool' &&
636
- normalized.toolName === lastStatus.toolName &&
637
- normalized.toolCallId === lastStatus.toolCallId))
638
- ) {
639
- return;
640
- }
641
-
642
- lastStatus = normalized;
643
-
644
- const payload = normalized
645
- ? {
646
- type: normalized.type,
647
- label: normalized.label ?? undefined,
648
- toolName:
649
- normalized.type === 'tool'
650
- ? (normalized.toolName ?? undefined)
651
- : undefined,
652
- }
653
- : null;
654
-
655
- await ctx.runMutation(internal.chatbotAgent.updateSessionStatus, {
656
- agentSessionId: args.agentSessionId,
657
- status: payload,
658
- });
659
- };
660
-
661
- let finalResponse = '';
662
- let pendingChunk = '';
663
- let wroteChunk = false;
664
-
665
- const flushChunk = async (final: boolean) => {
666
- const textToWrite = pendingChunk;
667
- pendingChunk = '';
668
- if (!final && textToWrite.length === 0) {
669
- return;
670
- }
671
- await ctx.runMutation(components.persistentTextStreaming.lib.addChunk, {
672
- streamId: args.streamId,
673
- text: textToWrite,
674
- final,
675
- });
676
- wroteChunk = true;
677
- };
678
-
679
- const STREAM_TIMEOUT = 120000;
680
- const streamPromise = (async () => {
681
- try {
682
- for await (const part of streamResult.fullStream) {
683
- switch (part.type) {
684
- case 'text-delta': {
685
- const delta = part.textDelta;
686
- if (!delta) {
687
- break;
688
- }
689
- finalResponse += delta;
690
- pendingChunk += delta;
691
-
692
- if (activeToolCalls.size === 0 && lastStatus !== null) {
693
- await applyStatus(null);
694
- }
695
-
696
- if (
697
- SENTENCE_DELIMITER.test(delta) ||
698
- pendingChunk.length >= MAX_PENDING_CHARS
699
- ) {
700
- await flushChunk(false);
701
- }
702
- break;
703
- }
704
- case 'tool-call-streaming-start': {
705
- activeToolCalls.set(part.toolCallId, {
706
- toolName: part.toolName,
707
- label: toolLabel(part.toolName),
708
- });
709
- await applyStatus({
710
- type: 'tool',
711
- toolName: part.toolName,
712
- label: toolLabel(part.toolName),
713
- toolCallId: part.toolCallId,
714
- });
715
- break;
716
- }
717
- case 'tool-call': {
718
- const callId =
719
- (part as { toolCallId?: string }).toolCallId ??
720
- (part as { id?: string }).id ??
721
- part.toolName;
722
- activeToolCalls.set(callId, {
723
- toolName: part.toolName,
724
- label: toolLabel(part.toolName),
725
- });
726
- await applyStatus({
727
- type: 'tool',
728
- toolName: part.toolName,
729
- label: toolLabel(part.toolName),
730
- toolCallId: callId,
731
- });
732
- break;
733
- }
734
- case 'tool-result': {
735
- const callId =
736
- (part as { toolCallId?: string }).toolCallId ??
737
- (part as { id?: string }).id ??
738
- null;
739
- if (callId) {
740
- activeToolCalls.delete(callId);
741
- } else {
742
- activeToolCalls.clear();
743
- }
744
-
745
- if (activeToolCalls.size > 0) {
746
- const [nextId, next] = activeToolCalls.entries().next()
747
- .value as [string, { toolName: string; label: string }];
748
- await applyStatus({
749
- type: 'tool',
750
- toolName: next.toolName,
751
- label: next.label,
752
- toolCallId: nextId,
753
- });
754
- } else {
755
- await applyStatus({
756
- type: 'thinking',
757
- label: 'Thinking…',
758
- });
759
- }
760
- break;
761
- }
762
- default:
763
- break;
764
- }
765
- }
766
- } catch (streamError) {
767
- throw streamError;
768
- }
769
- })();
770
-
771
- const timeoutPromise = new Promise((_, reject) => {
772
- setTimeout(() => {
773
- reject(new Error('Stream timed out after 2 minutes'));
774
- }, STREAM_TIMEOUT);
775
- });
776
-
777
- try {
778
- await Promise.race([streamPromise, timeoutPromise]);
779
- } catch (streamError) {
780
- console.error('[chatbotAgent] Stream failed or timed out', {
781
- streamError,
782
- streamId: args.streamId,
783
- });
784
- telemetryExtra.streamError =
785
- streamError instanceof Error
786
- ? streamError.message
787
- : String(streamError);
788
- await markFailure(streamError, {
789
- reason: 'streamReadFailed',
790
- });
791
- return;
792
- }
793
-
794
- if (pendingChunk.length > 0) {
795
- await flushChunk(true);
796
- } else {
797
- await ctx.runMutation(
798
- components.persistentTextStreaming.lib.setStreamStatus,
799
- {
800
- streamId: args.streamId,
801
- status: wroteChunk ? 'done' : 'done',
802
- },
803
- );
804
- }
805
-
806
- await applyStatus(null);
807
-
808
- telemetry?.mark('agent.stream.completed', {
809
- responseLength: finalResponse.length,
810
- });
811
-
812
- const [usage, toolCalls, toolResults] = await Promise.all([
813
- streamResult.usage.catch(() => undefined),
814
- streamResult.toolCalls.catch(() => [] as ToolCallUnion<AgentTools>[]),
815
- streamResult.toolResults.catch(
816
- () => [] as ToolResultUnion<AgentTools>[],
817
- ),
818
- ]);
819
-
820
- const serializedTools = mergeToolResults(
821
- toolCalls ?? [],
822
- toolResults ?? [],
823
- );
824
-
825
- await ctx.runMutation(internal.chatbotAgent.saveAssistantMessage, {
826
- agentSessionId: args.agentSessionId,
827
- conversationId: args.conversationId,
828
- streamId: args.streamId,
829
- text: finalResponse,
830
- provider,
831
- modelId,
832
- usage: usage
833
- ? {
834
- promptTokens: usage.promptTokens ?? undefined,
835
- completionTokens: usage.completionTokens ?? undefined,
836
- totalTokens: usage.totalTokens ?? undefined,
837
- }
838
- : undefined,
839
- toolCalls: serializedTools,
840
- });
841
-
842
- telemetryExtra.responseLength = finalResponse.length;
843
- telemetryStatus = 'ok';
844
- } catch (error) {
845
- console.error('[chatbotAgent] runStreaming caught error', {
846
- error,
847
- failureHandled,
848
- streamId: args.streamId,
849
- conversationId: args.conversationId,
850
- });
851
-
852
- if (!failureHandled) {
853
- try {
854
- await markFailure(error);
855
- console.log('[chatbotAgent] markFailure completed successfully');
856
- } catch (markFailureError) {
857
- console.error('[chatbotAgent] markFailure itself failed!', {
858
- markFailureError,
859
- originalError: error,
860
- });
861
- }
862
- }
863
- telemetryExtra.error =
864
- error instanceof Error ? error.message : String(error);
865
- } finally {
866
- telemetry?.finalize(telemetryStatus, telemetryExtra);
867
- }
868
- },
223
+ handler: async (ctx, args) => handleRunStreaming(ctx, args),
869
224
  });
870
225
 
871
226
  export const updateSessionStatus = internalMutation({