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.
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +28 -2
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +5 -3
- package/dist/commands/init.js.map +1 -1
- package/package.json +1 -1
- package/recipes/audio-recorder/recipe.json +1 -1
- package/recipes/audio-recorder@latest.zip +0 -0
- package/recipes/charts/apps/native/src/app/{charts → (root)/(protected)/charts}/index.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/app/preview.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/area-chart.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/bar-chart.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/candlestick-chart.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/chart-card.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/column-chart.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/doughnut-chart.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/index.ts +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/line-chart.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/radar-chart.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/radial-bar-chart.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/stacked-area-chart.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/components/stacked-bar-chart.tsx +0 -3
- package/recipes/charts/apps/native/src/features/charts/data/mock-data.ts +0 -3
- package/recipes/charts/apps/native/src/features/charts/types/index.ts +0 -3
- package/recipes/charts/recipe.json +1 -1
- package/recipes/charts@latest.zip +0 -0
- package/recipes/chatbot/apps/native/src/api-client/chatbot.ts +83 -0
- package/recipes/chatbot/apps/native/src/app/{chatbot → (root)/(protected)/chatbot}/index.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/app/index.tsx +56 -60
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-header-buttons.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-input-bar.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-markdown.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-message-bubble.tsx +3 -26
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-settings-modal.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/image-preview-list.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/code-block.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/index.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/table-renderer.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/message-error-boundary.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/message-list.tsx +10 -14
- package/recipes/chatbot/apps/native/src/features/chatbot/components/model-selector.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/report-content-modal.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/components/suggested-messages.tsx +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/constants/models.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/constants/report-reasons.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-attachment-cache.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-config.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-handlers.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chatbot-settings.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-conversation.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-image-picker.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-keyboard-coordinator.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-smart-scroll-manager.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/models/index.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/models/models.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/models/providers.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/models/types.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/services/file-uploader.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/services/message-handler-service.ts +0 -1
- package/recipes/chatbot/apps/native/src/features/chatbot/types/index.ts +5 -3
- package/recipes/chatbot/apps/native/src/features/chatbot/utils/chat-telemetry.ts +0 -1
- package/recipes/chatbot/packages/backend/convex/agents.ts +3 -4
- package/recipes/chatbot/packages/backend/convex/chatbot/content.ts +35 -0
- package/recipes/chatbot/packages/backend/convex/chatbot/sessions.ts +52 -0
- package/recipes/chatbot/packages/backend/convex/chatbot/streaming.ts +422 -0
- package/recipes/chatbot/packages/backend/convex/chatbot/telemetry.ts +56 -0
- package/recipes/chatbot/packages/backend/convex/chatbot/tools.ts +128 -0
- package/recipes/chatbot/packages/backend/convex/chatbotAgent.ts +6 -651
- package/recipes/chatbot/packages/backend/convex/ragKnowledge.ts +0 -714
- package/recipes/chatbot/packages/backend/convex/tools/knowledgeRetrieval.ts +12 -7
- package/recipes/chatbot/recipe.json +6 -1
- package/recipes/chatbot@latest.zip +0 -0
- package/recipes/image-generator/apps/native/src/api-client/image-generator.ts +34 -0
- package/recipes/image-generator/packages/backend/convex/{imageGeneratorFunctions.ts → imageGenerator.ts} +1 -1
- package/recipes/image-generator/recipe.json +5 -1
- package/recipes/image-generator@latest.zip +0 -0
- package/recipes/payments/apps/native/src/api-client/payments.ts +44 -0
- package/recipes/payments/packages/backend/convex/payments/index.ts +13 -0
- package/recipes/payments/packages/backend/convex/payments.ts +119 -0
- package/recipes/payments/recipe.json +15 -2
- package/recipes/payments@latest.zip +0 -0
- package/recipes/quiz/recipe.json +1 -1
- package/recipes/quiz@latest.zip +0 -0
- package/recipes/tracker-app/recipe.json +1 -1
- package/recipes/tracker-app@latest.zip +0 -0
- package/recipes/voice-bot/recipe.json +1 -1
- package/recipes/voice-bot@latest.zip +0 -0
- package/src/commands/add.ts +108 -70
- package/src/commands/init.ts +5 -3
- package/tmp-npm-cache/_update-notifier-last-checked +0 -0
- /package/recipes/audio-recorder/apps/native/src/app/{audio-recorder → (root)/(protected)/audio-recorder}/index.tsx +0 -0
- /package/recipes/image-generator/apps/native/src/app/{image-generator → (root)/(protected)/image-generator}/gallery.tsx +0 -0
- /package/recipes/image-generator/apps/native/src/app/{image-generator → (root)/(protected)/image-generator}/index.tsx +0 -0
- /package/recipes/quiz/apps/native/src/app/{quiz → (root)/(protected)/quiz}/index.tsx +0 -0
- /package/recipes/tracker-app/apps/native/src/app/{tracker-app → (root)/(protected)/tracker-app}/index.tsx +0 -0
- /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 {
|
|
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 {
|
|
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({
|