@jskit-ai/assistant 0.1.38 → 0.1.39

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 (29) hide show
  1. package/package.descriptor.mjs +84 -319
  2. package/package.json +2 -2
  3. package/src/server/buildTemplateContext.js +39 -14
  4. package/templates/src/pages/assistant/index.vue +2 -2
  5. package/templates/src/pages/settings/assistant/index.vue +7 -0
  6. package/test/buildTemplateContext.test.js +42 -27
  7. package/test/packageDescriptor.test.js +26 -55
  8. package/test/templateContracts.test.js +8 -6
  9. package/templates/migrations/assistant_config_initial.cjs +0 -25
  10. package/templates/migrations/assistant_transcripts_initial.cjs +0 -56
  11. package/templates/src/local-package/client/components/AssistantSettingsClientElement.vue +0 -88
  12. package/templates/src/local-package/client/components/AssistantSurfaceClientElement.vue +0 -10
  13. package/templates/src/local-package/client/composables/useAssistantRuntime.js +0 -754
  14. package/templates/src/local-package/client/index.js +0 -4
  15. package/templates/src/local-package/client/providers/AssistantClientProvider.js +0 -16
  16. package/templates/src/local-package/package.descriptor.mjs +0 -85
  17. package/templates/src/local-package/package.json +0 -11
  18. package/templates/src/local-package/server/AssistantProvider.js +0 -143
  19. package/templates/src/local-package/server/actionIds.js +0 -9
  20. package/templates/src/local-package/server/actions.js +0 -183
  21. package/templates/src/local-package/server/registerRoutes.js +0 -296
  22. package/templates/src/local-package/server/repositories/assistantConfigRepository.js +0 -141
  23. package/templates/src/local-package/server/repositories/conversationsRepository.js +0 -240
  24. package/templates/src/local-package/server/repositories/messagesRepository.js +0 -166
  25. package/templates/src/local-package/server/services/assistantConfigService.js +0 -90
  26. package/templates/src/local-package/server/services/chatService.js +0 -995
  27. package/templates/src/local-package/server/services/transcriptService.js +0 -308
  28. package/templates/src/local-package/shared/assistantRuntimeConfig.js +0 -13
  29. package/templates/src/local-package/shared/index.js +0 -1
@@ -1,754 +0,0 @@
1
- import { computed, ref, watch } from "vue";
2
- import { useQueryClient } from "@tanstack/vue-query";
3
- import { getClientAppConfig } from "@jskit-ai/kernel/client";
4
- import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
5
- import { buildAssistantApiPath } from "@jskit-ai/assistant-core/shared";
6
- import {
7
- ASSISTANT_STREAM_EVENT_TYPES,
8
- MAX_HISTORY_MESSAGES,
9
- MAX_INPUT_CHARS,
10
- assistantConversationMessagesQueryKey,
11
- assistantConversationsListQueryKey,
12
- assistantScopeQueryKey,
13
- normalizeAssistantStreamEventType,
14
- normalizeConversationStatus as normalizeAssistantConversationStatus,
15
- parseJsonObject,
16
- toPositiveInteger
17
- } from "@jskit-ai/assistant-core/shared";
18
- import {
19
- assistantHttpClient,
20
- createAssistantApi
21
- } from "@jskit-ai/assistant-core/client";
22
- import { useShellWebErrorRuntime } from "@jskit-ai/shell-web/client/error";
23
- import { usePagedCollection } from "@jskit-ai/users-web/client/composables/usePagedCollection";
24
- import { useWorkspaceRouteContext } from "@jskit-ai/users-web/client/composables/useWorkspaceRouteContext";
25
- import { assistantRuntimeConfig } from "../../shared/assistantRuntimeConfig.js";
26
-
27
- const DEFAULT_STREAM_TIMEOUT_MS = 120_000;
28
- const DEFAULT_HISTORY_PAGE_SIZE = 20;
29
- const DEFAULT_MESSAGES_PAGE_SIZE = 200;
30
- const DEFAULT_HISTORY_STALE_TIME_MS = 60_000;
31
- const RESTORE_MESSAGES_PAGE = 1;
32
-
33
- function toNonNegativeInteger(value, fallback = 0) {
34
- const parsed = Number(value);
35
- if (!Number.isInteger(parsed) || parsed < 0) {
36
- return fallback;
37
- }
38
-
39
- return parsed;
40
- }
41
-
42
- function buildScopeStorageKey(scope = {}) {
43
- const runtimeSurfaceId = normalizeText(scope?.targetSurfaceId).toLowerCase() || "assistant";
44
- const workspaceSlug = normalizeText(scope?.workspaceSlug).toLowerCase() || "global";
45
- return `assistant.activeConversationId:${runtimeSurfaceId}:${workspaceSlug}`;
46
- }
47
-
48
- function readStoredActiveConversationId(scope = {}) {
49
- if (typeof window === "undefined" || !window.sessionStorage) {
50
- return 0;
51
- }
52
-
53
- try {
54
- return toPositiveInteger(window.sessionStorage.getItem(buildScopeStorageKey(scope)), 0);
55
- } catch {
56
- return 0;
57
- }
58
- }
59
-
60
- function writeStoredActiveConversationId(scope = {}, conversationId) {
61
- if (typeof window === "undefined" || !window.sessionStorage) {
62
- return;
63
- }
64
-
65
- const normalizedConversationId = toPositiveInteger(conversationId, 0);
66
- const storageKey = buildScopeStorageKey(scope);
67
- try {
68
- if (normalizedConversationId > 0) {
69
- window.sessionStorage.setItem(storageKey, String(normalizedConversationId));
70
- return;
71
- }
72
-
73
- window.sessionStorage.removeItem(storageKey);
74
- } catch {
75
- return;
76
- }
77
- }
78
-
79
- function buildId(prefix = "id") {
80
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
81
- return `${prefix}_${crypto.randomUUID()}`;
82
- }
83
-
84
- return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
85
- }
86
-
87
- function normalizeToolName(value) {
88
- return normalizeText(value) || "tool";
89
- }
90
-
91
- function normalizeConversationStatus(value) {
92
- return normalizeAssistantConversationStatus(value, {
93
- fallback: "unknown"
94
- });
95
- }
96
-
97
- function formatConversationStartedAt(value) {
98
- const source = normalizeText(value);
99
- if (!source) {
100
- return "unknown";
101
- }
102
-
103
- const date = new Date(source);
104
- if (Number.isNaN(date.getTime())) {
105
- return "unknown";
106
- }
107
-
108
- return date.toLocaleString();
109
- }
110
-
111
- function parseToolResultPayload(value) {
112
- return parseJsonObject(value);
113
- }
114
-
115
- function buildHistory(messages) {
116
- const normalizedHistory = (Array.isArray(messages) ? messages : [])
117
- .filter((message) => {
118
- if (!message || typeof message !== "object") {
119
- return false;
120
- }
121
- if (message.kind !== "chat") {
122
- return false;
123
- }
124
- if (message.role !== "user" && message.role !== "assistant") {
125
- return false;
126
- }
127
- if (normalizeText(message.status).toLowerCase() !== "done") {
128
- return false;
129
- }
130
- return Boolean(normalizeText(message.text));
131
- })
132
- .map((message) => ({
133
- role: message.role,
134
- content: String(message.text || "").slice(0, MAX_INPUT_CHARS)
135
- }));
136
-
137
- return normalizedHistory.slice(-MAX_HISTORY_MESSAGES);
138
- }
139
-
140
- function mapTranscriptEntriesToAssistantState(entries) {
141
- const sourceEntries = Array.isArray(entries) ? entries : [];
142
- const messages = [];
143
- const toolEventsById = new Map();
144
-
145
- function ensureToolEvent(toolCallId, toolName) {
146
- const key = normalizeText(toolCallId) || buildId("tool_call");
147
- if (toolEventsById.has(key)) {
148
- return toolEventsById.get(key);
149
- }
150
-
151
- const next = {
152
- id: key,
153
- name: normalizeToolName(toolName),
154
- arguments: "",
155
- status: "pending",
156
- result: null,
157
- error: null
158
- };
159
- toolEventsById.set(key, next);
160
- return next;
161
- }
162
-
163
- for (const entry of sourceEntries) {
164
- const role = normalizeText(entry?.role).toLowerCase();
165
- const kind = normalizeText(entry?.kind).toLowerCase();
166
- const metadata = normalizeObject(entry?.metadata);
167
- const messageId = Number(entry?.id) > 0 ? `transcript_${entry.id}` : buildId("transcript");
168
-
169
- if (kind === "chat" && (role === "user" || role === "assistant")) {
170
- messages.push({
171
- id: messageId,
172
- role,
173
- kind: "chat",
174
- text: entry?.contentText == null ? "" : String(entry.contentText),
175
- status: "done"
176
- });
177
- continue;
178
- }
179
-
180
- if (kind === "tool_call") {
181
- const toolCallId = normalizeText(metadata.toolCallId) || `tool_call_${messageId}`;
182
- const toolName = normalizeToolName(metadata.tool);
183
- const toolEvent = ensureToolEvent(toolCallId, toolName);
184
- toolEvent.arguments = String(entry?.contentText || "");
185
- toolEvent.status = "pending";
186
- continue;
187
- }
188
-
189
- if (kind === "tool_result") {
190
- const parsedResult = parseToolResultPayload(entry?.contentText);
191
- const toolCallId = normalizeText(metadata.toolCallId || parsedResult.toolCallId) || `tool_result_${messageId}`;
192
- const toolName = normalizeToolName(metadata.tool || parsedResult.tool);
193
- const toolEvent = ensureToolEvent(toolCallId, toolName);
194
- const failed = parsedResult.ok === false || metadata.ok === false;
195
- toolEvent.status = failed ? "failed" : "done";
196
- toolEvent.result = failed ? null : parsedResult.result;
197
- toolEvent.error = failed ? parsedResult.error || metadata.error || null : null;
198
- }
199
- }
200
-
201
- return {
202
- messages,
203
- pendingToolEvents: [...toolEventsById.values()]
204
- };
205
- }
206
-
207
- function resolveRuntimePolicy() {
208
- const appConfig = getClientAppConfig();
209
- const assistantConfig = normalizeObject(appConfig?.assistant);
210
-
211
- return Object.freeze({
212
- timeoutMs: toPositiveInteger(assistantConfig.timeoutMs, DEFAULT_STREAM_TIMEOUT_MS),
213
- historyPageSize: toPositiveInteger(assistantConfig.historyPageSize, DEFAULT_HISTORY_PAGE_SIZE),
214
- restoreMessagesPageSize: toPositiveInteger(assistantConfig.restoreMessagesPageSize, DEFAULT_MESSAGES_PAGE_SIZE),
215
- historyStaleTimeMs: toNonNegativeInteger(assistantConfig.historyStaleTimeMs, DEFAULT_HISTORY_STALE_TIME_MS)
216
- });
217
- }
218
-
219
- function createRuntimeApi({ overrideApi = null, resolveBasePath, resolveSurfaceId = null } = {}) {
220
- if (overrideApi && typeof overrideApi.streamChat === "function") {
221
- return overrideApi;
222
- }
223
-
224
- return createAssistantApi({
225
- request: assistantHttpClient.request,
226
- requestStream: assistantHttpClient.requestStream,
227
- resolveBasePath,
228
- resolveSurfaceId
229
- });
230
- }
231
-
232
- function useAssistantRuntime({ api = null } = {}) {
233
- const runtimePolicy = resolveRuntimePolicy();
234
- const queryClient = useQueryClient();
235
- const errorRuntime = useShellWebErrorRuntime();
236
- const { placementContext, currentSurfaceId, workspaceSlugFromRoute } = useWorkspaceRouteContext();
237
-
238
- const messages = ref([]);
239
- const input = ref("");
240
- const isStreaming = ref(false);
241
- const isRestoringConversation = ref(false);
242
- const error = ref("");
243
- const pendingToolEvents = ref([]);
244
- const conversationId = ref(null);
245
- const abortController = ref(null);
246
-
247
- const placementSnapshot = computed(() => normalizeObject(placementContext.value));
248
- const runtimeScope = computed(() => {
249
- const workspaceSlug = assistantRuntimeConfig.runtimeSurfaceRequiresWorkspace
250
- ? normalizeText(workspaceSlugFromRoute.value).toLowerCase()
251
- : "";
252
-
253
- return {
254
- targetSurfaceId: assistantRuntimeConfig.runtimeSurfaceId,
255
- workspaceSlug,
256
- workspaceId: assistantRuntimeConfig.runtimeSurfaceRequiresWorkspace
257
- ? toPositiveInteger(placementSnapshot.value?.workspace?.id, 0)
258
- : 0
259
- };
260
- });
261
- const hasRuntimeScope = computed(() =>
262
- assistantRuntimeConfig.runtimeSurfaceRequiresWorkspace ? Boolean(runtimeScope.value.workspaceSlug) : true
263
- );
264
-
265
- const runtimeApi = createRuntimeApi({
266
- overrideApi: api,
267
- resolveBasePath: () =>
268
- buildAssistantApiPath({
269
- requiresWorkspace: assistantRuntimeConfig.runtimeSurfaceRequiresWorkspace,
270
- workspaceSlug: runtimeScope.value.workspaceSlug,
271
- suffix: "/"
272
- }),
273
- resolveSurfaceId: () => normalizeText(currentSurfaceId.value).toLowerCase()
274
- });
275
-
276
- const activeConversationId = computed(() => normalizeText(conversationId.value));
277
- const isAdminSurface = computed(() => normalizeText(currentSurfaceId.value).toLowerCase() === "admin");
278
- const canSend = computed(() => {
279
- return Boolean(
280
- hasRuntimeScope.value &&
281
- !isStreaming.value &&
282
- !isRestoringConversation.value &&
283
- normalizeText(input.value)
284
- );
285
- });
286
- const canStartNewConversation = computed(() => Boolean(hasRuntimeScope.value && !isStreaming.value));
287
-
288
- function setRuntimeError(message, dedupeKey = "") {
289
- const normalizedMessage = normalizeText(message);
290
- error.value = normalizedMessage;
291
- if (!normalizedMessage) {
292
- return;
293
- }
294
-
295
- errorRuntime.report({
296
- source: "assistant.runtime",
297
- message: normalizedMessage,
298
- severity: "error",
299
- channel: "banner",
300
- dedupeKey: dedupeKey || `assistant.runtime:error:${normalizedMessage}`,
301
- dedupeWindowMs: 3000
302
- });
303
- }
304
-
305
- const conversationHistoryCollection = usePagedCollection({
306
- queryKey: computed(() =>
307
- assistantConversationsListQueryKey(runtimeScope.value, {
308
- limit: runtimePolicy.historyPageSize
309
- })
310
- ),
311
- queryFn: ({ pageParam = null }) =>
312
- runtimeApi.listConversations({
313
- cursor: pageParam,
314
- limit: runtimePolicy.historyPageSize
315
- }),
316
- initialPageParam: null,
317
- dedupeBy(entry) {
318
- const conversationNumericId = toPositiveInteger(entry?.id, 0);
319
- return conversationNumericId > 0 ? String(conversationNumericId) : normalizeText(entry?.id);
320
- },
321
- enabled: computed(() => hasRuntimeScope.value),
322
- queryOptions: {
323
- staleTime: runtimePolicy.historyStaleTimeMs,
324
- refetchOnMount: false,
325
- refetchOnWindowFocus: false
326
- },
327
- fallbackLoadError: "Unable to load conversation history."
328
- });
329
-
330
- const conversationHistory = conversationHistoryCollection.items;
331
- const conversationHistoryLoading = computed(
332
- () => Boolean(conversationHistoryCollection.isLoading.value && !conversationHistoryCollection.isLoadingMore.value)
333
- );
334
- const conversationHistoryLoadingMore = conversationHistoryCollection.isLoadingMore;
335
- const conversationHistoryHasMore = conversationHistoryCollection.hasMore;
336
- const conversationHistoryError = conversationHistoryCollection.loadError;
337
-
338
- watch(conversationId, (nextConversationId, previousConversationId) => {
339
- if (!hasRuntimeScope.value) {
340
- return;
341
- }
342
-
343
- const nextConversationNumericId = toPositiveInteger(nextConversationId, 0);
344
- if (nextConversationNumericId > 0) {
345
- writeStoredActiveConversationId(runtimeScope.value, nextConversationNumericId);
346
- return;
347
- }
348
-
349
- const previousConversationNumericId = toPositiveInteger(previousConversationId, 0);
350
- if (previousConversationNumericId > 0) {
351
- writeStoredActiveConversationId(runtimeScope.value, 0);
352
- }
353
- });
354
-
355
- watch(
356
- [
357
- hasRuntimeScope,
358
- conversationHistoryLoading,
359
- runtimeScope,
360
- conversationId,
361
- conversationHistory,
362
- isRestoringConversation
363
- ],
364
- async ([
365
- nextHasRuntimeScope,
366
- nextConversationHistoryLoading,
367
- nextRuntimeScope,
368
- nextConversationId,
369
- nextConversationHistory,
370
- nextIsRestoringConversation
371
- ]) => {
372
- if (!nextHasRuntimeScope || nextConversationHistoryLoading || nextIsRestoringConversation) {
373
- return;
374
- }
375
-
376
- const activeConversationNumericId = toPositiveInteger(nextConversationId, 0);
377
- if (activeConversationNumericId > 0) {
378
- return;
379
- }
380
-
381
- const sourceEntries = Array.isArray(nextConversationHistory) ? nextConversationHistory : [];
382
- if (sourceEntries.length < 1) {
383
- return;
384
- }
385
-
386
- const storedConversationId = readStoredActiveConversationId(nextRuntimeScope);
387
- if (!storedConversationId) {
388
- return;
389
- }
390
-
391
- const hasStoredConversation = sourceEntries.some(
392
- (entry) => toPositiveInteger(entry?.id, 0) === storedConversationId
393
- );
394
- if (!hasStoredConversation) {
395
- writeStoredActiveConversationId(nextRuntimeScope, 0);
396
- return;
397
- }
398
-
399
- await selectConversationById(storedConversationId);
400
- },
401
- {
402
- immediate: true
403
- }
404
- );
405
-
406
- function appendMessage(payload) {
407
- messages.value = [...messages.value, payload];
408
- }
409
-
410
- function updateMessage(messageId, updater) {
411
- messages.value = messages.value.map((message) => {
412
- if (message.id !== messageId) {
413
- return message;
414
- }
415
-
416
- const patch = typeof updater === "function" ? updater(message) : updater;
417
- return {
418
- ...message,
419
- ...(patch && typeof patch === "object" ? patch : {})
420
- };
421
- });
422
- }
423
-
424
- function findMessage(messageId) {
425
- return messages.value.find((entry) => entry.id === messageId) || null;
426
- }
427
-
428
- async function invalidateConversationScope() {
429
- if (!hasRuntimeScope.value) {
430
- return;
431
- }
432
-
433
- await queryClient.invalidateQueries({
434
- queryKey: assistantScopeQueryKey(runtimeScope.value)
435
- });
436
- }
437
-
438
- async function refreshConversationHistory() {
439
- if (!hasRuntimeScope.value) {
440
- return;
441
- }
442
-
443
- await conversationHistoryCollection.reload();
444
- }
445
-
446
- async function loadMoreConversationHistory() {
447
- await conversationHistoryCollection.loadMore();
448
- }
449
-
450
- async function selectConversationById(nextConversationId) {
451
- const normalizedConversationId = normalizeText(nextConversationId);
452
- if (!normalizedConversationId || isStreaming.value || isRestoringConversation.value || !hasRuntimeScope.value) {
453
- return;
454
- }
455
-
456
- const parsedConversationId = toPositiveInteger(normalizedConversationId, 0);
457
- if (!parsedConversationId) {
458
- return;
459
- }
460
-
461
- const previousConversationId = conversationId.value;
462
- conversationId.value = String(parsedConversationId);
463
- isRestoringConversation.value = true;
464
- setRuntimeError("");
465
-
466
- try {
467
- const response = await queryClient.fetchQuery({
468
- queryKey: assistantConversationMessagesQueryKey(runtimeScope.value, parsedConversationId, {
469
- page: RESTORE_MESSAGES_PAGE,
470
- pageSize: runtimePolicy.restoreMessagesPageSize
471
- }),
472
- queryFn: () =>
473
- runtimeApi.getConversationMessages(parsedConversationId, {
474
- page: RESTORE_MESSAGES_PAGE,
475
- pageSize: runtimePolicy.restoreMessagesPageSize
476
- })
477
- });
478
-
479
- const restored = mapTranscriptEntriesToAssistantState(response?.entries);
480
- messages.value = restored.messages;
481
- pendingToolEvents.value = restored.pendingToolEvents;
482
- input.value = "";
483
- } catch (loadError) {
484
- conversationId.value = previousConversationId;
485
- setRuntimeError(normalizeText(loadError?.message) || "Unable to load conversation.");
486
- } finally {
487
- isRestoringConversation.value = false;
488
- }
489
- }
490
-
491
- async function selectConversation(conversation) {
492
- await selectConversationById(conversation?.id);
493
- }
494
-
495
- function startNewConversation() {
496
- if (abortController.value) {
497
- abortController.value.abort();
498
- }
499
-
500
- messages.value = [];
501
- pendingToolEvents.value = [];
502
- input.value = "";
503
- setRuntimeError("");
504
- conversationId.value = null;
505
- writeStoredActiveConversationId(runtimeScope.value, 0);
506
- isStreaming.value = false;
507
- isRestoringConversation.value = false;
508
- abortController.value = null;
509
- }
510
-
511
- function handleInputKeydown(event) {
512
- if (event?.key === "Enter" && isStreaming.value) {
513
- event.preventDefault();
514
- return;
515
- }
516
-
517
- if (
518
- event?.key === "Enter" &&
519
- event?.shiftKey !== true &&
520
- event?.ctrlKey !== true &&
521
- event?.metaKey !== true &&
522
- event?.altKey !== true
523
- ) {
524
- event.preventDefault();
525
- void sendMessage();
526
- }
527
- }
528
-
529
- function cancelStream() {
530
- if (abortController.value) {
531
- abortController.value.abort();
532
- }
533
- }
534
-
535
- async function sendMessage() {
536
- const normalizedInput = normalizeText(input.value).slice(0, MAX_INPUT_CHARS);
537
- if (!normalizedInput || isStreaming.value || isRestoringConversation.value || !hasRuntimeScope.value) {
538
- return;
539
- }
540
-
541
- const messageId = buildId("message");
542
- const assistantMessageId = buildId("assistant");
543
- const history = buildHistory(messages.value);
544
- const parsedConversationId = toPositiveInteger(conversationId.value, 0);
545
-
546
- appendMessage({
547
- id: buildId("user"),
548
- role: "user",
549
- kind: "chat",
550
- text: normalizedInput,
551
- status: "done"
552
- });
553
-
554
- appendMessage({
555
- id: assistantMessageId,
556
- role: "assistant",
557
- kind: "chat",
558
- text: "",
559
- status: "streaming"
560
- });
561
-
562
- input.value = "";
563
- setRuntimeError("");
564
- isStreaming.value = true;
565
-
566
- const streamAbortController = new AbortController();
567
- abortController.value = streamAbortController;
568
-
569
- const streamTimeout = setTimeout(() => {
570
- streamAbortController.abort();
571
- }, runtimePolicy.timeoutMs);
572
-
573
- let streamDoneStatus = "";
574
-
575
- try {
576
- await runtimeApi.streamChat(
577
- {
578
- messageId,
579
- ...(parsedConversationId > 0 ? { conversationId: parsedConversationId } : {}),
580
- input: normalizedInput,
581
- history
582
- },
583
- {
584
- signal: streamAbortController.signal,
585
- onEvent(event) {
586
- const eventType = normalizeAssistantStreamEventType(event?.type, "");
587
-
588
- if (eventType === ASSISTANT_STREAM_EVENT_TYPES.META && Object.hasOwn(event || {}, "conversationId")) {
589
- conversationId.value = event?.conversationId ? String(event.conversationId) : null;
590
- return;
591
- }
592
-
593
- if (eventType === ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_DELTA) {
594
- const delta = String(event?.delta || "");
595
- if (!delta) {
596
- return;
597
- }
598
-
599
- updateMessage(assistantMessageId, (message) => ({
600
- text: `${String(message?.text || "")}${delta}`,
601
- status: "streaming"
602
- }));
603
- return;
604
- }
605
-
606
- if (eventType === ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_MESSAGE) {
607
- const text = String(event?.text || "");
608
- updateMessage(assistantMessageId, {
609
- text,
610
- status: "done"
611
- });
612
- return;
613
- }
614
-
615
- if (eventType === ASSISTANT_STREAM_EVENT_TYPES.TOOL_CALL) {
616
- const toolCallId = normalizeText(event?.toolCallId) || buildId("tool_call");
617
- pendingToolEvents.value = [
618
- ...pendingToolEvents.value,
619
- {
620
- id: toolCallId,
621
- name: normalizeToolName(event?.name),
622
- arguments: String(event?.arguments || ""),
623
- status: "pending",
624
- result: null,
625
- error: null
626
- }
627
- ];
628
- return;
629
- }
630
-
631
- if (eventType === ASSISTANT_STREAM_EVENT_TYPES.TOOL_RESULT) {
632
- const toolCallId = normalizeText(event?.toolCallId);
633
- if (toolCallId) {
634
- pendingToolEvents.value = pendingToolEvents.value.map((toolEvent) => {
635
- if (toolEvent.id !== toolCallId) {
636
- return toolEvent;
637
- }
638
-
639
- const failed = event?.ok === false;
640
- return {
641
- ...toolEvent,
642
- status: failed ? "failed" : "done",
643
- result: failed ? null : event?.result,
644
- error: failed ? event?.error || null : null
645
- };
646
- });
647
- }
648
- return;
649
- }
650
-
651
- if (eventType === ASSISTANT_STREAM_EVENT_TYPES.ERROR) {
652
- setRuntimeError(
653
- normalizeText(event?.message) || "Assistant request failed.",
654
- "assistant.runtime:stream-event-error"
655
- );
656
- updateMessage(assistantMessageId, {
657
- status: "error"
658
- });
659
- return;
660
- }
661
-
662
- if (eventType === ASSISTANT_STREAM_EVENT_TYPES.DONE) {
663
- streamDoneStatus = normalizeText(event?.status).toLowerCase();
664
- }
665
- }
666
- }
667
- );
668
-
669
- const assistantMessage = findMessage(assistantMessageId);
670
- const assistantMessageText = normalizeText(assistantMessage?.text);
671
- if (!assistantMessageText && streamDoneStatus !== "aborted") {
672
- if (!error.value) {
673
- setRuntimeError("Assistant returned no output.", "assistant.runtime:empty-output");
674
- }
675
- updateMessage(assistantMessageId, {
676
- status: "error"
677
- });
678
- } else if (streamDoneStatus === "aborted") {
679
- updateMessage(assistantMessageId, {
680
- status: "canceled"
681
- });
682
- } else {
683
- updateMessage(assistantMessageId, (message) => ({
684
- status: message.status === "streaming" ? "done" : message.status
685
- }));
686
- }
687
- } catch (streamError) {
688
- if (String(streamError?.name || "") === "AbortError") {
689
- updateMessage(assistantMessageId, {
690
- status: "canceled"
691
- });
692
- } else {
693
- setRuntimeError(normalizeText(streamError?.message) || "Assistant request failed.");
694
- updateMessage(assistantMessageId, {
695
- status: "error"
696
- });
697
- }
698
- } finally {
699
- clearTimeout(streamTimeout);
700
- abortController.value = null;
701
- isStreaming.value = false;
702
- await invalidateConversationScope();
703
- await refreshConversationHistory();
704
- }
705
- }
706
-
707
- const viewer = computed(() => {
708
- const user = normalizeObject(placementSnapshot.value.user);
709
-
710
- return {
711
- displayName: normalizeText(user.displayName || user.name) || "You",
712
- avatarUrl: normalizeText(user.avatarUrl)
713
- };
714
- });
715
-
716
- return Object.freeze({
717
- meta: {
718
- normalizeConversationStatus,
719
- formatConversationStartedAt
720
- },
721
- state: {
722
- messages,
723
- input,
724
- isStreaming,
725
- isRestoringConversation,
726
- error,
727
- pendingToolEvents,
728
- conversationId,
729
- activeConversationId,
730
- conversationHistory,
731
- conversationHistoryLoading,
732
- conversationHistoryLoadingMore,
733
- conversationHistoryHasMore,
734
- conversationHistoryError,
735
- isAdminSurface,
736
- canSend,
737
- canStartNewConversation
738
- },
739
- actions: {
740
- sendMessage,
741
- handleInputKeydown,
742
- cancelStream,
743
- startNewConversation,
744
- clearConversation: startNewConversation,
745
- selectConversation,
746
- selectConversationById,
747
- refreshConversationHistory,
748
- loadMoreConversationHistory
749
- },
750
- viewer
751
- });
752
- }
753
-
754
- export { useAssistantRuntime };