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
@@ -0,0 +1,422 @@
1
+ import type { CoreMessage, FilePart, ImagePart, TextPart } from 'ai';
2
+
3
+ import { components, internal } from '../_generated/api';
4
+ import type { Doc, Id } from '../_generated/dataModel';
5
+ import type { ActionCtx } from '../_generated/server';
6
+ import { agent, resolveChatModel } from '../agents';
7
+ import { buildUserContent } from './content';
8
+ import { createTelemetryTracker } from './telemetry';
9
+ import {
10
+ extractToolCallId,
11
+ extractToolName,
12
+ isTextDelta,
13
+ isToolCall,
14
+ isToolCallStreamingStart,
15
+ isToolResult,
16
+ MAX_PENDING_CHARS,
17
+ mergeToolResults,
18
+ SENTENCE_DELIMITER,
19
+ toolLabel,
20
+ type ToolCall,
21
+ type ToolResult,
22
+ type ToolCallPart,
23
+ type ToolCallStreamingStartPart,
24
+ type ToolResultPart,
25
+ type ToolStreamPart,
26
+ } from './tools';
27
+
28
+ type AgentTools = NonNullable<(typeof agent)['options']['tools']>;
29
+
30
+ type StatusRecord =
31
+ | null
32
+ | { type: 'thinking'; label: string | null }
33
+ | {
34
+ type: 'tool';
35
+ label: string | null;
36
+ toolName: string | null;
37
+ toolCallId: string | null;
38
+ };
39
+
40
+ export type RunStreamingArgs = {
41
+ streamId: string;
42
+ conversationId: Id<'conversations'>;
43
+ agentThreadId: string;
44
+ userMessageId: Id<'messages'>;
45
+ preferredModel?: string;
46
+ agentSessionId: Id<'agentSessions'>;
47
+ };
48
+
49
+ const STREAM_TIMEOUT_MS = 120_000;
50
+
51
+ function getToolCallId(part: ToolCallStreamingStartPart | ToolCallPart) {
52
+ return extractToolCallId(part) ?? extractToolName(part) ?? 'tool';
53
+ }
54
+
55
+ async function applyStatus(
56
+ ctx: ActionCtx,
57
+ agentSessionId: Id<'agentSessions'>,
58
+ status:
59
+ | { type: 'thinking'; label?: string }
60
+ | {
61
+ type: 'tool';
62
+ toolName: string;
63
+ label?: string;
64
+ toolCallId?: string;
65
+ }
66
+ | null,
67
+ lastStatus: StatusRecord,
68
+ setLastStatus: (s: StatusRecord) => void,
69
+ ) {
70
+ const normalized: StatusRecord = status
71
+ ? status.type === 'tool'
72
+ ? {
73
+ type: 'tool',
74
+ label: status.label ?? null,
75
+ toolName: status.toolName,
76
+ toolCallId: status.toolCallId ?? null,
77
+ }
78
+ : {
79
+ type: 'thinking',
80
+ label: status.label ?? null,
81
+ }
82
+ : null;
83
+
84
+ if (
85
+ normalized &&
86
+ lastStatus &&
87
+ normalized.type === lastStatus.type &&
88
+ normalized.label === lastStatus.label &&
89
+ (normalized.type !== 'tool' ||
90
+ (lastStatus.type === 'tool' &&
91
+ normalized.toolName === lastStatus.toolName &&
92
+ normalized.toolCallId === lastStatus.toolCallId))
93
+ ) {
94
+ return;
95
+ }
96
+
97
+ if (normalized === null && lastStatus === null) {
98
+ return;
99
+ }
100
+
101
+ setLastStatus(normalized);
102
+
103
+ const payload = normalized
104
+ ? {
105
+ type: normalized.type,
106
+ label: normalized.label ?? undefined,
107
+ toolName:
108
+ normalized.type === 'tool'
109
+ ? (normalized.toolName ?? undefined)
110
+ : undefined,
111
+ }
112
+ : null;
113
+
114
+ await ctx.runMutation(internal.chatbotAgent.updateSessionStatus, {
115
+ agentSessionId,
116
+ status: payload,
117
+ });
118
+ }
119
+
120
+ export async function handleRunStreaming(ctx: ActionCtx, args: RunStreamingArgs) {
121
+ const telemetry = createTelemetryTracker('runStreaming', {
122
+ streamId: args.streamId,
123
+ conversationId: args.conversationId,
124
+ agentSessionId: args.agentSessionId,
125
+ });
126
+
127
+ let telemetryStatus: 'ok' | 'error' = 'ok';
128
+ const telemetryExtra: Record<string, unknown> = {};
129
+ let failureHandled = false;
130
+
131
+ const inferErrorMessage = (raw: unknown): string | undefined => {
132
+ if (!raw) return undefined;
133
+ if (typeof raw === 'string') {
134
+ try {
135
+ const parsed = JSON.parse(raw);
136
+ if (parsed && typeof parsed === 'object') {
137
+ if (typeof parsed.error === 'string') {
138
+ return parsed.error;
139
+ }
140
+ if (typeof parsed.message === 'string') {
141
+ return parsed.message;
142
+ }
143
+ }
144
+ } catch {
145
+ // ignore
146
+ }
147
+ const trimmed = raw.trim();
148
+ if (trimmed.length > 0 && trimmed.length <= 300) {
149
+ return trimmed;
150
+ }
151
+ return undefined;
152
+ }
153
+ if (raw instanceof Error) {
154
+ return raw.message;
155
+ }
156
+ return undefined;
157
+ };
158
+
159
+ const markFailure = async (
160
+ maybeMessage: unknown,
161
+ extra?: Record<string, unknown>,
162
+ ) => {
163
+ telemetryStatus = 'error';
164
+ if (extra) {
165
+ Object.assign(telemetryExtra, extra);
166
+ }
167
+ failureHandled = true;
168
+ const message =
169
+ inferErrorMessage(maybeMessage) ??
170
+ 'Sorry, I ran into a technical issue while replying. Please try again.';
171
+
172
+ try {
173
+ await ctx.runMutation(
174
+ components.persistentTextStreaming.lib.setStreamStatus,
175
+ {
176
+ streamId: args.streamId,
177
+ status: 'error',
178
+ },
179
+ );
180
+ } catch (statusError) {
181
+ console.error('[chatbotAgent] Failed to update stream status to error', {
182
+ streamId: args.streamId,
183
+ statusError,
184
+ });
185
+ }
186
+
187
+ await ctx.runMutation(internal.chatbotAgent.markStreamFailed, {
188
+ agentSessionId: args.agentSessionId,
189
+ conversationId: args.conversationId,
190
+ streamId: args.streamId,
191
+ errorMessage: message,
192
+ });
193
+ };
194
+
195
+ try {
196
+ telemetry?.mark('conversation.lookup');
197
+ const conversation = await ctx.runQuery(
198
+ internal.chatbotAgent.getConversationForStreaming,
199
+ { conversationId: args.conversationId },
200
+ );
201
+ if (!conversation) {
202
+ await markFailure('Conversation not found.', {
203
+ reason: 'conversationMissing',
204
+ });
205
+ return;
206
+ }
207
+
208
+ telemetry?.mark('session.lookup');
209
+ const session = await ctx.runQuery(
210
+ internal.chatbotAgent.getAgentSessionById,
211
+ { agentSessionId: args.agentSessionId },
212
+ );
213
+ if (!session) {
214
+ await markFailure('Agent session missing.', {
215
+ reason: 'sessionMissing',
216
+ });
217
+ return;
218
+ }
219
+
220
+ telemetry?.mark('message.lookup');
221
+ const message = await ctx.runQuery(
222
+ internal.chatbotAgent.getMessageForStreaming,
223
+ { messageId: args.userMessageId },
224
+ );
225
+ if (!message || message.conversationId !== args.conversationId) {
226
+ await markFailure('User message missing.', {
227
+ reason: 'userMessageMissing',
228
+ });
229
+ return;
230
+ }
231
+
232
+ const { model, provider, modelId } = resolveChatModel(
233
+ args.preferredModel ?? session.preferredModel ?? undefined,
234
+ );
235
+ telemetryExtra.provider = provider;
236
+ telemetryExtra.modelId = modelId;
237
+ telemetry?.mark('model.resolved', { provider, modelId });
238
+
239
+ const userContent = await buildUserContent(ctx, message as Doc<'messages'>);
240
+ telemetry?.mark('content.resolved', {
241
+ hasAttachments: Boolean(message.attachments?.length),
242
+ });
243
+
244
+ const { thread } = await agent.continueThread(ctx, {
245
+ threadId: args.agentThreadId,
246
+ userId: conversation.userId,
247
+ });
248
+ telemetry?.mark('agent.threadReady');
249
+
250
+ const messages: CoreMessage[] = [
251
+ {
252
+ role: 'user',
253
+ content: userContent as string | (TextPart | ImagePart | FilePart)[],
254
+ },
255
+ ];
256
+
257
+ let streamResult;
258
+ try {
259
+ streamResult = await thread.streamText(
260
+ {
261
+ model,
262
+ messages,
263
+ },
264
+ {
265
+ storageOptions: {
266
+ saveMessages: 'promptAndOutput',
267
+ },
268
+ },
269
+ );
270
+ } catch (streamInitError) {
271
+ console.error('[chatbotAgent] Failed to initialize stream', {
272
+ streamInitError,
273
+ provider,
274
+ modelId,
275
+ });
276
+ await markFailure(streamInitError, {
277
+ reason: 'streamInitFailed',
278
+ provider,
279
+ modelId,
280
+ });
281
+ return;
282
+ }
283
+
284
+ telemetry?.mark('agent.stream.started');
285
+
286
+ let lastStatus: StatusRecord = { type: 'thinking', label: 'Thinking…' };
287
+ const setLastStatus = (status: StatusRecord) => {
288
+ lastStatus = status;
289
+ };
290
+
291
+ let finalResponse = '';
292
+ let pendingChunk = '';
293
+ let wroteChunk = false;
294
+
295
+ const flushChunk = async (final: boolean) => {
296
+ const textToWrite = pendingChunk;
297
+ pendingChunk = '';
298
+ if (!final && textToWrite.length === 0) {
299
+ return;
300
+ }
301
+ await ctx.runMutation(components.persistentTextStreaming.lib.addChunk, {
302
+ streamId: args.streamId,
303
+ text: textToWrite,
304
+ final,
305
+ });
306
+ wroteChunk = true;
307
+ };
308
+
309
+ const streamPromise = (async () => {
310
+ // AI SDK v5 exposes normalized textStream chunks; consume those directly.
311
+ for await (const delta of streamResult.textStream) {
312
+ if (!delta) {
313
+ continue;
314
+ }
315
+ finalResponse += delta;
316
+ pendingChunk += delta;
317
+
318
+ if (lastStatus !== null) {
319
+ await applyStatus(ctx, args.agentSessionId, null, lastStatus, setLastStatus);
320
+ }
321
+
322
+ if (
323
+ SENTENCE_DELIMITER.test(delta) ||
324
+ pendingChunk.length >= MAX_PENDING_CHARS
325
+ ) {
326
+ await flushChunk(false);
327
+ }
328
+ }
329
+ })();
330
+
331
+ const timeoutPromise = new Promise((_, reject) => {
332
+ setTimeout(() => {
333
+ reject(new Error('Stream timed out after 2 minutes'));
334
+ }, STREAM_TIMEOUT_MS);
335
+ });
336
+
337
+ try {
338
+ await Promise.race([streamPromise, timeoutPromise]);
339
+ } catch (streamError) {
340
+ console.error('[chatbotAgent] Stream failed or timed out', {
341
+ streamError,
342
+ streamId: args.streamId,
343
+ });
344
+ telemetryExtra.streamError =
345
+ streamError instanceof Error
346
+ ? streamError.message
347
+ : String(streamError);
348
+ await markFailure(streamError, {
349
+ reason: 'streamReadFailed',
350
+ });
351
+ return;
352
+ }
353
+
354
+ if (pendingChunk.length > 0) {
355
+ await flushChunk(true);
356
+ } else {
357
+ await ctx.runMutation(components.persistentTextStreaming.lib.setStreamStatus, {
358
+ streamId: args.streamId,
359
+ status: wroteChunk ? 'done' : 'done',
360
+ });
361
+ }
362
+
363
+ await applyStatus(ctx, args.agentSessionId, null, lastStatus, setLastStatus);
364
+
365
+ telemetry?.mark('agent.stream.completed', {
366
+ responseLength: finalResponse.length,
367
+ });
368
+
369
+ const [usage, toolCalls, toolResults] = await Promise.all([
370
+ streamResult.usage.catch(() => undefined),
371
+ streamResult.toolCalls.catch(() => [] as ToolCall[]),
372
+ streamResult.toolResults.catch(() => [] as ToolResult[]),
373
+ ]);
374
+
375
+ const serializedTools = mergeToolResults(
376
+ (toolCalls ?? []) as ToolCall[],
377
+ (toolResults ?? []) as ToolResult[],
378
+ );
379
+
380
+ await ctx.runMutation(internal.chatbotAgent.saveAssistantMessage, {
381
+ agentSessionId: args.agentSessionId,
382
+ conversationId: args.conversationId,
383
+ streamId: args.streamId,
384
+ text: finalResponse,
385
+ provider,
386
+ modelId,
387
+ usage: usage
388
+ ? {
389
+ promptTokens: usage.promptTokens ?? undefined,
390
+ completionTokens: usage.completionTokens ?? undefined,
391
+ totalTokens: usage.totalTokens ?? undefined,
392
+ }
393
+ : undefined,
394
+ toolCalls: serializedTools,
395
+ });
396
+
397
+ telemetryExtra.responseLength = finalResponse.length;
398
+ telemetryStatus = 'ok';
399
+ } catch (error) {
400
+ console.error('[chatbotAgent] runStreaming caught error', {
401
+ error,
402
+ failureHandled,
403
+ streamId: args.streamId,
404
+ conversationId: args.conversationId,
405
+ });
406
+
407
+ if (!failureHandled) {
408
+ try {
409
+ await markFailure(error);
410
+ console.log('[chatbotAgent] markFailure completed successfully');
411
+ } catch (markFailureError) {
412
+ console.error('[chatbotAgent] markFailure itself failed!', {
413
+ markFailureError,
414
+ originalError: error,
415
+ });
416
+ }
417
+ }
418
+ telemetryExtra.error = error instanceof Error ? error.message : String(error);
419
+ } finally {
420
+ telemetry?.finalize(telemetryStatus, telemetryExtra);
421
+ }
422
+ }
@@ -0,0 +1,56 @@
1
+ import { getChatTelemetryMode, type TelemetryMode } from '../lib/telemetry';
2
+
3
+ const DEFAULT_TELEMETRY_SAMPLE = 0.1;
4
+
5
+ type TelemetryCheckpoint = {
6
+ label: string;
7
+ at: number;
8
+ data?: Record<string, unknown>;
9
+ };
10
+
11
+ export type TelemetryTracker = {
12
+ mark: (label: string, data?: Record<string, unknown>) => void;
13
+ finalize: (status: 'ok' | 'error', extra?: Record<string, unknown>) => void;
14
+ };
15
+
16
+ export function createTelemetryTracker(
17
+ event: string,
18
+ baseContext: Record<string, unknown>,
19
+ sampleProbability = DEFAULT_TELEMETRY_SAMPLE,
20
+ ): TelemetryTracker | null {
21
+ const mode: TelemetryMode = getChatTelemetryMode();
22
+ const shouldTrack =
23
+ mode === 'debug' ||
24
+ (mode === 'sample' && Math.random() < sampleProbability);
25
+
26
+ if (!shouldTrack) {
27
+ return null;
28
+ }
29
+
30
+ const start = Date.now();
31
+ const checkpoints: TelemetryCheckpoint[] = [{ label: 'start', at: start }];
32
+
33
+ return {
34
+ mark(label, data) {
35
+ checkpoints.push({ label, at: Date.now(), data });
36
+ },
37
+ finalize(status, extra) {
38
+ const finishedAt = Date.now();
39
+ const timeline = checkpoints.map((checkpoint) => ({
40
+ label: checkpoint.label,
41
+ msFromStart: checkpoint.at - start,
42
+ ...(checkpoint.data ?? {}),
43
+ }));
44
+
45
+ console.log('[chatbotAgent][telemetry]', {
46
+ event,
47
+ mode,
48
+ status,
49
+ totalMs: finishedAt - start,
50
+ ...baseContext,
51
+ ...(extra ?? {}),
52
+ timeline,
53
+ });
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,128 @@
1
+ export const SENTENCE_DELIMITER = /[.!?]/;
2
+ export const MAX_PENDING_CHARS = 180;
3
+
4
+ export type ToolCall = {
5
+ toolName: string;
6
+ args?: unknown;
7
+ toolCallId?: string;
8
+ id?: string;
9
+ };
10
+
11
+ export type ToolResult = {
12
+ toolCallId?: string;
13
+ id?: string;
14
+ result?: unknown;
15
+ };
16
+
17
+ const TOOL_STATUS_LABELS: Record<string, string> = {
18
+ knowledgeRetrieval: 'Looking through knowledge…',
19
+ tavilySearch: 'Searching the web…',
20
+ userProfile: 'Checking your profile…',
21
+ };
22
+
23
+ export function toolLabel(toolName: string) {
24
+ return TOOL_STATUS_LABELS[toolName] ?? `Using ${toolName}`;
25
+ }
26
+
27
+ export function mergeToolResults(
28
+ toolCalls: ToolCall[],
29
+ toolResults: ToolResult[],
30
+ ) {
31
+ if (!toolCalls?.length) {
32
+ return [];
33
+ }
34
+
35
+ const resultByCallId = new Map<string, unknown>();
36
+
37
+ for (const result of toolResults ?? []) {
38
+ const key =
39
+ (result as { toolCallId?: string }).toolCallId ??
40
+ (result as { id?: string }).id;
41
+ if (key) {
42
+ resultByCallId.set(key, (result as { result?: unknown }).result);
43
+ }
44
+ }
45
+
46
+ return toolCalls.map((call) => {
47
+ const callId =
48
+ (call as { id?: string }).id ??
49
+ (call as { toolCallId?: string }).toolCallId ??
50
+ call.toolName;
51
+
52
+ return {
53
+ toolName: call.toolName ?? callId ?? 'tool',
54
+ args: (call as { args?: unknown }).args ?? null,
55
+ result: callId ? resultByCallId.get(callId) : undefined,
56
+ };
57
+ });
58
+ }
59
+
60
+ export type TextDeltaPart = { type: 'text-delta'; textDelta?: string };
61
+ export type ToolCallStreamingStartPart = {
62
+ type: 'tool-call-streaming-start';
63
+ toolCallId: string;
64
+ toolName: string;
65
+ };
66
+ export type ToolCallPart = {
67
+ type: 'tool-call';
68
+ toolCallId?: string;
69
+ id?: string;
70
+ toolName: string;
71
+ };
72
+ export type ToolResultPart = {
73
+ type: 'tool-result';
74
+ toolCallId?: string;
75
+ id?: string;
76
+ toolName?: string;
77
+ };
78
+
79
+ export type ToolStreamPart =
80
+ | TextDeltaPart
81
+ | ToolCallStreamingStartPart
82
+ | ToolCallPart
83
+ | ToolResultPart
84
+ | { type: string; [key: string]: unknown };
85
+
86
+ export function isTextDelta(part: ToolStreamPart): part is TextDeltaPart {
87
+ return part.type === 'text-delta';
88
+ }
89
+
90
+ export function isToolCallStreamingStart(
91
+ part: ToolStreamPart,
92
+ ): part is ToolCallStreamingStartPart {
93
+ const candidate = part as Partial<ToolCallStreamingStartPart>;
94
+ return (
95
+ part.type === 'tool-call-streaming-start' &&
96
+ typeof candidate.toolCallId === 'string' &&
97
+ typeof candidate.toolName === 'string'
98
+ );
99
+ }
100
+
101
+ export function isToolCall(part: ToolStreamPart): part is ToolCallPart {
102
+ return part.type === 'tool-call' && typeof (part as ToolCallPart).toolName === 'string';
103
+ }
104
+
105
+ export function isToolResult(part: ToolStreamPart): part is ToolResultPart {
106
+ return part.type === 'tool-result';
107
+ }
108
+
109
+ export function extractToolCallId(
110
+ part: ToolCallPart | ToolResultPart | ToolCallStreamingStartPart,
111
+ ): string | null {
112
+ if ('toolCallId' in part && typeof part.toolCallId === 'string') {
113
+ return part.toolCallId;
114
+ }
115
+ if ('id' in part && typeof part.id === 'string') {
116
+ return part.id;
117
+ }
118
+ return null;
119
+ }
120
+
121
+ export function extractToolName(
122
+ part: ToolCallPart | ToolCallStreamingStartPart,
123
+ ): string | null {
124
+ if ('toolName' in part && typeof part.toolName === 'string') {
125
+ return part.toolName;
126
+ }
127
+ return null;
128
+ }