vibefast-cli 0.2.2 → 0.2.4
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/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite +0 -0
- package/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-shm +0 -0
- package/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-wal +0 -0
- package/.wrangler/state/v3/r2/vibefast-recipes/blobs/177b5d7279681c1bec396cafe63779a2d89eaf538109e55733147727276e2b9f0000019a81f04ba2 +0 -0
- package/.wrangler/state/v3/r2/vibefast-recipes/blobs/4fe398bba6e2d5f13b569bc1be4244e696d86caa04c323db2d9fb0b9381c508f0000019a81f0503f +0 -0
- package/.wrangler/state/v3/r2/vibefast-recipes/blobs/f68f19a655380ac7fb575eb49c0623cde74046261ed89c498ba5107b8aacde9d0000019a81f05484 +0 -0
- package/DOCS-CLEANUP-SUMMARY.md +140 -0
- package/DOCS.md +141 -0
- package/IMPLEMENTATION-COMPLETE.md +6 -5
- package/MANUAL-STEPS-GUIDE.md +385 -0
- package/MANUAL-STEPS-USER-FLOW.md +231 -0
- package/PLAN-VS-IMPLEMENTATION.md +248 -0
- package/PUBLISHED-0.2.2.md +65 -0
- package/README.md +24 -2
- package/START-HERE.md +115 -0
- package/docs/next-steps.md +12 -0
- package/package.json +10 -1
- package/recipes/audio-recorder@latest.zip +0 -0
- package/recipes/charts/apps/native/src/app/charts/index.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/app/preview.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/area-chart.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/bar-chart.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/candlestick-chart.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/chart-card.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/column-chart.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/doughnut-chart.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/index.ts +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/line-chart.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/radar-chart.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/radial-bar-chart.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/stacked-area-chart.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/components/stacked-bar-chart.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/data/mock-data.ts +3 -0
- package/recipes/charts/apps/native/src/features/charts/types/index.ts +3 -0
- package/recipes/charts/recipe.json +7 -1
- package/recipes/charts@latest.zip +0 -0
- package/recipes/chatbot/apps/native/src/app/chatbot/index.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/app/index.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-header-buttons.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-input-bar.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-markdown.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-message-bubble.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-settings-modal.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/image-preview-list.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/code-block.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/index.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/table-renderer.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/message-error-boundary.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/message-list.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/model-selector.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/report-content-modal.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/suggested-messages.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/constants/models.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/constants/report-reasons.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-attachment-cache.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-config.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-handlers.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chatbot-settings.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-conversation.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-image-picker.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-keyboard-coordinator.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-smart-scroll-manager.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/index.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/models.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/providers.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/types.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/services/file-uploader.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/services/message-handler-service.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/types/index.ts +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/utils/chat-telemetry.ts +1 -0
- package/recipes/chatbot/packages/backend/convex/agents.ts +116 -0
- package/recipes/chatbot/packages/backend/convex/chatbot/index.ts +30 -0
- package/recipes/chatbot/packages/backend/convex/chatbotAgent.ts +1085 -0
- package/recipes/chatbot/packages/backend/convex/chatbotHistory.ts +307 -0
- package/recipes/chatbot/packages/backend/convex/lib/rateLimit.ts +100 -0
- package/recipes/chatbot/packages/backend/convex/lib/telemetry.ts +29 -0
- package/recipes/chatbot/packages/backend/convex/ragKnowledge.ts +714 -0
- package/recipes/chatbot/packages/backend/convex/tools/index.ts +18 -0
- package/recipes/chatbot/packages/backend/convex/tools/knowledgeRetrieval.ts +92 -0
- package/recipes/chatbot/packages/backend/convex/tools/tavilySearch.ts +83 -0
- package/recipes/chatbot/packages/backend/convex/tools/userProfile.ts +72 -0
- package/recipes/chatbot/recipe.json +104 -1
- package/recipes/chatbot@latest.zip +0 -0
- package/recipes/image-generator/packages/backend/convex/imageGeneration/index.ts +12 -0
- package/recipes/image-generator/packages/backend/convex/imageGeneratorFunctions.ts +290 -0
- package/recipes/image-generator/recipe.json +41 -1
- package/recipes/image-generator@latest.zip +0 -0
- package/recipes/quiz@latest.zip +0 -0
- package/recipes/tracker-app@latest.zip +0 -0
- package/recipes/voice-bot/packages/backend/convex/router.ts +81 -0
- package/recipes/voice-bot/recipe.json +48 -1
- package/recipes/voice-bot@latest.zip +0 -0
- package/scripts/create-recipes.mjs +33 -1
- package/MONITORING-AND-ANNOUNCEMENT-GUIDE.md +0 -669
- package/PRE-PUBLISH-CHECKLIST.md +0 -558
- package/PUBLISHED-SUCCESS.md +0 -282
- package/READY-TO-PUBLISH.md +0 -419
- package/RECIPES-READY.md +0 -172
- package/cloudflare-worker/mini-native@latest.zip +0 -0
- package/cloudflare-worker/test-recipe/apps/native/src/app/mini/index.tsx +0 -15
- package/cloudflare-worker/test-recipe/recipe.json +0 -16
- package/text.md +0 -27
- /package/{AUTO-DETECT-DEPS.md → docs/archive/AUTO-DETECT-DEPS.md} +0 -0
- /package/{FINAL-PACKAGE-STRATEGY.md → docs/archive/FINAL-PACKAGE-STRATEGY.md} +0 -0
- /package/{FINAL-SIMPLE-PLAN.md → docs/archive/FINAL-SIMPLE-PLAN.md} +0 -0
- /package/{FINAL-STATUS.md → docs/archive/FINAL-STATUS.md} +0 -0
- /package/{FLOW-DIAGRAM.md → docs/archive/FLOW-DIAGRAM.md} +0 -0
- /package/{GOTCHAS-AND-RISKS.md → docs/archive/GOTCHAS-AND-RISKS.md} +0 -0
- /package/{IMPLEMENTATION-PLAN.md → docs/archive/IMPLEMENTATION-PLAN.md} +0 -0
- /package/{PLAN.md → docs/archive/PLAN.md} +0 -0
- /package/{PRODUCTION-READINESS.md → docs/archive/PRODUCTION-READINESS.md} +0 -0
- /package/{PRODUCTION-TEST-RESULTS.md → docs/archive/PRODUCTION-TEST-RESULTS.md} +0 -0
- /package/{SIMPLIFIED-PLAN.md → docs/archive/SIMPLIFIED-PLAN.md} +0 -0
- /package/{STATUS.md → docs/archive/STATUS.md} +0 -0
- /package/{SUCCESS.md → docs/archive/SUCCESS.md} +0 -0
- /package/{TEST-SUMMARY.md → docs/archive/TEST-SUMMARY.md} +0 -0
- /package/{TESTING-CHECKLIST.md → docs/archive/TESTING-CHECKLIST.md} +0 -0
- /package/{USER-MODIFICATIONS.md → docs/archive/USER-MODIFICATIONS.md} +0 -0
|
@@ -0,0 +1,1085 @@
|
|
|
1
|
+
import { getAuthUserId } from '@convex-dev/auth/server';
|
|
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
|
+
import { v } from 'convex/values';
|
|
12
|
+
|
|
13
|
+
import { components, internal } from './_generated/api';
|
|
14
|
+
import type { Doc, Id } from './_generated/dataModel';
|
|
15
|
+
import {
|
|
16
|
+
internalAction,
|
|
17
|
+
internalMutation,
|
|
18
|
+
internalQuery,
|
|
19
|
+
type MutationCtx,
|
|
20
|
+
mutation,
|
|
21
|
+
type QueryCtx,
|
|
22
|
+
query,
|
|
23
|
+
} from './_generated/server';
|
|
24
|
+
import { agent, DEFAULT_MODEL, resolveChatModel } from './agents';
|
|
25
|
+
import { enforceAgentRateLimit } from './lib/rateLimit';
|
|
26
|
+
import { getChatTelemetryMode, type TelemetryMode } from './lib/telemetry';
|
|
27
|
+
|
|
28
|
+
const persistentTextStreaming = new PersistentTextStreaming(
|
|
29
|
+
components.persistentTextStreaming,
|
|
30
|
+
);
|
|
31
|
+
|
|
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
|
+
export const startAgentStream = mutation({
|
|
215
|
+
args: {
|
|
216
|
+
conversationId: v.id('conversations'),
|
|
217
|
+
userMessageId: v.id('messages'),
|
|
218
|
+
preferredModel: v.optional(v.string()),
|
|
219
|
+
},
|
|
220
|
+
handler: async (ctx, args) => {
|
|
221
|
+
const telemetry = createTelemetryTracker('startAgentStream', {
|
|
222
|
+
conversationId: args.conversationId,
|
|
223
|
+
userMessageId: args.userMessageId,
|
|
224
|
+
requestedModel: args.preferredModel ?? null,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const userId = await getAuthUserId(ctx);
|
|
228
|
+
telemetry?.mark('auth.resolved', { hasUser: Boolean(userId) });
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
if (!userId) {
|
|
232
|
+
throw new Error('Not authenticated');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const conversation = await ctx.db.get(args.conversationId);
|
|
236
|
+
if (!conversation || conversation.userId !== userId) {
|
|
237
|
+
throw new Error('Conversation not found');
|
|
238
|
+
}
|
|
239
|
+
telemetry?.mark('conversation.loaded');
|
|
240
|
+
|
|
241
|
+
const message = await ctx.db.get(args.userMessageId);
|
|
242
|
+
if (!message || message.conversationId !== args.conversationId) {
|
|
243
|
+
throw new Error('User message not found for conversation');
|
|
244
|
+
}
|
|
245
|
+
telemetry?.mark('message.loaded', {
|
|
246
|
+
hasAttachments: Boolean(message.attachments?.length),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (message.authorType !== 'user') {
|
|
250
|
+
throw new Error('Only user messages can start a streaming response');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
await enforceAgentRateLimit(ctx.db, userId);
|
|
254
|
+
telemetry?.mark('rateLimit.checked');
|
|
255
|
+
|
|
256
|
+
const { session, wasCreated } = await ensureAgentSession(
|
|
257
|
+
ctx,
|
|
258
|
+
args.conversationId,
|
|
259
|
+
userId,
|
|
260
|
+
args.preferredModel ?? undefined,
|
|
261
|
+
);
|
|
262
|
+
telemetry?.mark('session.ready', { wasCreated });
|
|
263
|
+
|
|
264
|
+
const streamId = await persistentTextStreaming.createStream(ctx);
|
|
265
|
+
telemetry?.mark('stream.created');
|
|
266
|
+
|
|
267
|
+
await ctx.db.patch(session._id, {
|
|
268
|
+
activeStreamId: streamId,
|
|
269
|
+
lastUserMessageId: args.userMessageId,
|
|
270
|
+
preferredModel:
|
|
271
|
+
args.preferredModel ?? session.preferredModel ?? DEFAULT_MODEL,
|
|
272
|
+
updatedAt: Date.now(),
|
|
273
|
+
lastErrorMessage: undefined,
|
|
274
|
+
lastErrorAt: undefined,
|
|
275
|
+
currentStatus: {
|
|
276
|
+
type: 'thinking',
|
|
277
|
+
label: 'Thinking…',
|
|
278
|
+
startedAt: Date.now(),
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
telemetry?.mark('session.patched');
|
|
282
|
+
|
|
283
|
+
console.log('[chatbotAgent] startAgentStream', {
|
|
284
|
+
conversationId: args.conversationId,
|
|
285
|
+
userMessageId: args.userMessageId,
|
|
286
|
+
requestedModel: args.preferredModel ?? null,
|
|
287
|
+
persistedPreferredModel:
|
|
288
|
+
args.preferredModel ?? session.preferredModel ?? DEFAULT_MODEL,
|
|
289
|
+
streamId,
|
|
290
|
+
agentSessionId: session._id,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
await ctx.scheduler.runAfter(0, internal.chatbotAgent.runStreaming, {
|
|
294
|
+
streamId,
|
|
295
|
+
conversationId: args.conversationId,
|
|
296
|
+
agentThreadId: session.agentThreadId,
|
|
297
|
+
userMessageId: args.userMessageId,
|
|
298
|
+
preferredModel:
|
|
299
|
+
args.preferredModel ?? session.preferredModel ?? undefined,
|
|
300
|
+
agentSessionId: session._id,
|
|
301
|
+
});
|
|
302
|
+
telemetry?.mark('stream.scheduled');
|
|
303
|
+
telemetry?.finalize('ok', { streamId, agentSessionId: session._id });
|
|
304
|
+
|
|
305
|
+
return { streamId };
|
|
306
|
+
} catch (error) {
|
|
307
|
+
telemetry?.finalize('error', {
|
|
308
|
+
error:
|
|
309
|
+
error instanceof Error
|
|
310
|
+
? error.message
|
|
311
|
+
: typeof error === 'string'
|
|
312
|
+
? error
|
|
313
|
+
: 'Unknown error',
|
|
314
|
+
});
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
export const getAgentStreamBody = query({
|
|
321
|
+
args: {
|
|
322
|
+
conversationId: v.id('conversations'),
|
|
323
|
+
streamId: v.string(),
|
|
324
|
+
},
|
|
325
|
+
handler: async (ctx, args) => {
|
|
326
|
+
const userId = await getAuthUserId(ctx);
|
|
327
|
+
if (!userId) {
|
|
328
|
+
throw new Error('Not authenticated');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const conversation = await ctx.db.get(args.conversationId);
|
|
332
|
+
if (!conversation || conversation.userId !== userId) {
|
|
333
|
+
throw new Error('Conversation not found');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const session = await getSessionForConversation(ctx, args.conversationId);
|
|
337
|
+
if (!session || session.activeStreamId !== args.streamId) {
|
|
338
|
+
// Allow reading historical streams that were saved on the message metadata.
|
|
339
|
+
const messageWithStream = await ctx.db
|
|
340
|
+
.query('messages')
|
|
341
|
+
.withIndex('by_conversationId', (q) =>
|
|
342
|
+
q.eq('conversationId', args.conversationId),
|
|
343
|
+
)
|
|
344
|
+
.filter((q) => q.eq(q.field('metadata.streamId'), args.streamId))
|
|
345
|
+
.first();
|
|
346
|
+
|
|
347
|
+
if (!messageWithStream) {
|
|
348
|
+
console.warn(
|
|
349
|
+
'[chatbotAgent] getAgentStreamBody fallback returning empty body',
|
|
350
|
+
{
|
|
351
|
+
conversationId: args.conversationId,
|
|
352
|
+
streamId: args.streamId,
|
|
353
|
+
},
|
|
354
|
+
);
|
|
355
|
+
return '';
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return await persistentTextStreaming.getStreamBody(
|
|
360
|
+
ctx,
|
|
361
|
+
args.streamId as any,
|
|
362
|
+
);
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
export const getAgentSession = query({
|
|
367
|
+
args: {
|
|
368
|
+
conversationId: v.id('conversations'),
|
|
369
|
+
},
|
|
370
|
+
handler: async (ctx, args) => {
|
|
371
|
+
const userId = await getAuthUserId(ctx);
|
|
372
|
+
if (!userId) {
|
|
373
|
+
throw new Error('Not authenticated');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const conversation = await ctx.db.get(args.conversationId);
|
|
377
|
+
if (!conversation || conversation.userId !== userId) {
|
|
378
|
+
throw new Error('Conversation not found');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const session = await getSessionForConversation(ctx, args.conversationId);
|
|
382
|
+
if (!session) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
activeStreamId: session.activeStreamId ?? null,
|
|
388
|
+
preferredModel: session.preferredModel ?? DEFAULT_MODEL,
|
|
389
|
+
agentThreadId: session.agentThreadId,
|
|
390
|
+
lastErrorMessage: session.lastErrorMessage ?? null,
|
|
391
|
+
lastErrorAt: session.lastErrorAt ?? null,
|
|
392
|
+
lastUserMessageId: session.lastUserMessageId ?? null,
|
|
393
|
+
currentStatus: session.currentStatus
|
|
394
|
+
? {
|
|
395
|
+
type: session.currentStatus.type,
|
|
396
|
+
label: session.currentStatus.label ?? null,
|
|
397
|
+
toolName: session.currentStatus.toolName ?? null,
|
|
398
|
+
startedAt: session.currentStatus.startedAt,
|
|
399
|
+
}
|
|
400
|
+
: null,
|
|
401
|
+
};
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
export const runStreaming = internalAction({
|
|
406
|
+
args: {
|
|
407
|
+
streamId: v.string(),
|
|
408
|
+
conversationId: v.id('conversations'),
|
|
409
|
+
agentThreadId: v.string(),
|
|
410
|
+
userMessageId: v.id('messages'),
|
|
411
|
+
preferredModel: v.optional(v.string()),
|
|
412
|
+
agentSessionId: v.id('agentSessions'),
|
|
413
|
+
},
|
|
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
|
+
},
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
export const updateSessionStatus = internalMutation({
|
|
872
|
+
args: {
|
|
873
|
+
agentSessionId: v.id('agentSessions'),
|
|
874
|
+
status: v.union(
|
|
875
|
+
v.null(),
|
|
876
|
+
v.object({
|
|
877
|
+
type: v.union(v.literal('thinking'), v.literal('tool')),
|
|
878
|
+
label: v.optional(v.string()),
|
|
879
|
+
toolName: v.optional(v.string()),
|
|
880
|
+
}),
|
|
881
|
+
),
|
|
882
|
+
},
|
|
883
|
+
handler: async (ctx, { agentSessionId, status }) => {
|
|
884
|
+
await ctx.db.patch(agentSessionId, {
|
|
885
|
+
currentStatus: status
|
|
886
|
+
? {
|
|
887
|
+
type: status.type,
|
|
888
|
+
label: status.label ?? undefined,
|
|
889
|
+
toolName: status.toolName ?? undefined,
|
|
890
|
+
startedAt: Date.now(),
|
|
891
|
+
}
|
|
892
|
+
: undefined,
|
|
893
|
+
});
|
|
894
|
+
},
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
export const markStreamFailed = internalMutation({
|
|
898
|
+
args: {
|
|
899
|
+
agentSessionId: v.id('agentSessions'),
|
|
900
|
+
conversationId: v.id('conversations'),
|
|
901
|
+
streamId: v.string(),
|
|
902
|
+
errorMessage: v.optional(v.string()),
|
|
903
|
+
},
|
|
904
|
+
handler: async (ctx, args) => {
|
|
905
|
+
console.log('[chatbotAgent] markStreamFailed called', {
|
|
906
|
+
agentSessionId: args.agentSessionId,
|
|
907
|
+
conversationId: args.conversationId,
|
|
908
|
+
streamId: args.streamId,
|
|
909
|
+
errorMessage: args.errorMessage,
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
const session = await ctx.db.get(args.agentSessionId);
|
|
913
|
+
|
|
914
|
+
if (!session) {
|
|
915
|
+
console.error('[chatbotAgent] markStreamFailed: session not found!', {
|
|
916
|
+
agentSessionId: args.agentSessionId,
|
|
917
|
+
});
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const lastUserMessageId = session?.lastUserMessageId;
|
|
922
|
+
|
|
923
|
+
if (lastUserMessageId) {
|
|
924
|
+
try {
|
|
925
|
+
await ctx.db.delete(lastUserMessageId);
|
|
926
|
+
console.log('[chatbotAgent] Deleted failed user message', {
|
|
927
|
+
messageId: lastUserMessageId,
|
|
928
|
+
});
|
|
929
|
+
} catch (error) {
|
|
930
|
+
console.warn('[chatbotAgent] Failed to delete failed user message', {
|
|
931
|
+
messageId: lastUserMessageId,
|
|
932
|
+
error,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
await ctx.db.patch(args.agentSessionId, {
|
|
938
|
+
activeStreamId: undefined,
|
|
939
|
+
lastUserMessageId: undefined,
|
|
940
|
+
lastErrorMessage: args.errorMessage ?? 'An error occurred',
|
|
941
|
+
lastErrorAt: Date.now(),
|
|
942
|
+
updatedAt: Date.now(),
|
|
943
|
+
currentStatus: undefined,
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
console.log('[chatbotAgent] Session patched with error state', {
|
|
947
|
+
agentSessionId: args.agentSessionId,
|
|
948
|
+
lastErrorAt: Date.now(),
|
|
949
|
+
lastErrorMessage: args.errorMessage,
|
|
950
|
+
});
|
|
951
|
+
},
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
export const saveAssistantMessage = internalMutation({
|
|
955
|
+
args: {
|
|
956
|
+
agentSessionId: v.id('agentSessions'),
|
|
957
|
+
conversationId: v.id('conversations'),
|
|
958
|
+
streamId: v.string(),
|
|
959
|
+
text: v.optional(v.string()),
|
|
960
|
+
provider: v.optional(v.string()),
|
|
961
|
+
modelId: v.optional(v.string()),
|
|
962
|
+
usage: v.optional(
|
|
963
|
+
v.object({
|
|
964
|
+
promptTokens: v.optional(v.number()),
|
|
965
|
+
completionTokens: v.optional(v.number()),
|
|
966
|
+
totalTokens: v.optional(v.number()),
|
|
967
|
+
}),
|
|
968
|
+
),
|
|
969
|
+
toolCalls: v.optional(
|
|
970
|
+
v.array(
|
|
971
|
+
v.object({
|
|
972
|
+
toolName: v.string(),
|
|
973
|
+
args: v.any(),
|
|
974
|
+
result: v.optional(v.any()),
|
|
975
|
+
}),
|
|
976
|
+
),
|
|
977
|
+
),
|
|
978
|
+
},
|
|
979
|
+
handler: async (ctx, args) => {
|
|
980
|
+
const telemetry = createTelemetryTracker('saveAssistantMessage', {
|
|
981
|
+
streamId: args.streamId,
|
|
982
|
+
agentSessionId: args.agentSessionId,
|
|
983
|
+
conversationId: args.conversationId,
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
try {
|
|
987
|
+
telemetry?.mark('lookup.session');
|
|
988
|
+
const session = await ctx.db.get(args.agentSessionId);
|
|
989
|
+
telemetry?.mark('lookup.conversation');
|
|
990
|
+
const conversation = await ctx.db.get(args.conversationId);
|
|
991
|
+
|
|
992
|
+
if (!session || !conversation) {
|
|
993
|
+
telemetry?.finalize('error', {
|
|
994
|
+
reason: 'missingData',
|
|
995
|
+
sessionExists: Boolean(session),
|
|
996
|
+
conversationExists: Boolean(conversation),
|
|
997
|
+
});
|
|
998
|
+
console.warn(
|
|
999
|
+
'[chatbotAgent] Unable to save assistant message: missing data',
|
|
1000
|
+
{
|
|
1001
|
+
sessionExists: !!session,
|
|
1002
|
+
conversationExists: !!conversation,
|
|
1003
|
+
},
|
|
1004
|
+
);
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const trimmedText = args.text?.trim();
|
|
1009
|
+
|
|
1010
|
+
telemetry?.mark('message.insert.started', {
|
|
1011
|
+
hasText: Boolean(trimmedText),
|
|
1012
|
+
hasTools: Boolean(args.toolCalls?.length),
|
|
1013
|
+
});
|
|
1014
|
+
const messageId = await ctx.db.insert('messages', {
|
|
1015
|
+
conversationId: args.conversationId,
|
|
1016
|
+
userId: conversation.userId,
|
|
1017
|
+
authorType: 'bot',
|
|
1018
|
+
text: trimmedText && trimmedText.length > 0 ? trimmedText : undefined,
|
|
1019
|
+
aiProvider: args.provider,
|
|
1020
|
+
aiModel: args.modelId,
|
|
1021
|
+
toolCalls: args.toolCalls ?? undefined,
|
|
1022
|
+
metadata: {
|
|
1023
|
+
streamId: args.streamId,
|
|
1024
|
+
usage: args.usage
|
|
1025
|
+
? {
|
|
1026
|
+
promptTokens: args.usage.promptTokens ?? 0,
|
|
1027
|
+
completionTokens: args.usage.completionTokens ?? 0,
|
|
1028
|
+
totalTokens: args.usage.totalTokens ?? 0,
|
|
1029
|
+
}
|
|
1030
|
+
: undefined,
|
|
1031
|
+
status: 'complete',
|
|
1032
|
+
},
|
|
1033
|
+
});
|
|
1034
|
+
telemetry?.mark('message.insert.completed', { messageId });
|
|
1035
|
+
|
|
1036
|
+
await ctx.db.patch(args.agentSessionId, {
|
|
1037
|
+
activeStreamId: undefined,
|
|
1038
|
+
lastAssistantMessageId: messageId,
|
|
1039
|
+
lastErrorMessage: undefined,
|
|
1040
|
+
lastErrorAt: undefined,
|
|
1041
|
+
updatedAt: Date.now(),
|
|
1042
|
+
currentStatus: undefined,
|
|
1043
|
+
});
|
|
1044
|
+
telemetry?.mark('session.patched');
|
|
1045
|
+
telemetry?.finalize('ok', { messageId });
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
telemetry?.finalize('error', {
|
|
1048
|
+
error:
|
|
1049
|
+
error instanceof Error
|
|
1050
|
+
? error.message
|
|
1051
|
+
: typeof error === 'string'
|
|
1052
|
+
? error
|
|
1053
|
+
: 'Unknown error',
|
|
1054
|
+
});
|
|
1055
|
+
throw error;
|
|
1056
|
+
}
|
|
1057
|
+
},
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
export const getConversationForStreaming = internalQuery({
|
|
1061
|
+
args: {
|
|
1062
|
+
conversationId: v.id('conversations'),
|
|
1063
|
+
},
|
|
1064
|
+
handler: async (ctx, args) => {
|
|
1065
|
+
return await ctx.db.get(args.conversationId);
|
|
1066
|
+
},
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
export const getMessageForStreaming = internalQuery({
|
|
1070
|
+
args: {
|
|
1071
|
+
messageId: v.id('messages'),
|
|
1072
|
+
},
|
|
1073
|
+
handler: async (ctx, args) => {
|
|
1074
|
+
return await ctx.db.get(args.messageId);
|
|
1075
|
+
},
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
export const getAgentSessionById = internalQuery({
|
|
1079
|
+
args: {
|
|
1080
|
+
agentSessionId: v.id('agentSessions'),
|
|
1081
|
+
},
|
|
1082
|
+
handler: async (ctx, args) => {
|
|
1083
|
+
return await ctx.db.get(args.agentSessionId);
|
|
1084
|
+
},
|
|
1085
|
+
});
|