@jskit-ai/assistant-runtime 0.1.234 → 0.1.235

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 (31) hide show
  1. package/README.md +36 -0
  2. package/fixtures/conversation/vite.config.mjs +2 -2
  3. package/package.json +9 -10
  4. package/src/client/components/AssistantSettingsClientElement.vue +0 -3
  5. package/src/client/components/AssistantSurfaceClientElement.vue +2 -5
  6. package/src/client/composables/useAssistantConversation.js +57 -55
  7. package/src/client/composables/useAssistantRuntime.js +14 -29
  8. package/src/client/support/accessDenied.js +6 -0
  9. package/src/client/support/subscribeAssistantConversation.js +4 -5
  10. package/src/server/actions.js +3 -36
  11. package/src/server/createAssistantSettingsActions.js +0 -8
  12. package/src/server/inputSchemas.js +3 -8
  13. package/src/server/registerConversationSubscriptions.js +13 -15
  14. package/src/server/registerRoutes.js +15 -39
  15. package/src/server/repositories/conversationsRepository.js +9 -47
  16. package/src/server/repositories/messagesRepository.js +5 -22
  17. package/src/server/repositories/repositoryPersistenceUtils.js +34 -1
  18. package/src/server/services/assistantConfigService.js +4 -17
  19. package/src/server/services/chatService.js +14 -37
  20. package/src/server/services/transcriptService.js +1 -54
  21. package/src/server/support/assistantErrors.js +13 -0
  22. package/src/server/support/assistantRouteContext.js +1 -10
  23. package/src/server/support/assistantStreamWriter.js +12 -0
  24. package/src/server/support/assistantSurface.js +13 -0
  25. package/src/server/support/createSurfaceAwareToolCatalog.js +5 -6
  26. package/src/shared/assistantRuntimeConfig.js +3 -1
  27. package/src/shared/assistantSurfaces.js +18 -0
  28. package/test/actionDefinitions.test.js +0 -1
  29. package/test/assistantSurfaces.test.js +29 -0
  30. package/test/chatServiceLifecycle.test.js +12 -3
  31. package/test/turnRequests.test.js +0 -1
package/README.md CHANGED
@@ -1,3 +1,35 @@
1
+ # @jskit-ai/assistant-runtime
2
+
3
+ Hosts JSKIT's common conversation runtime (from `@jskit-ai/assistant-core`)
4
+ through authorized HTTP operations, shared realtime subscriptions and Vue/Vuetify
5
+ conversation elements. Package discovery registers `AssistantFeature` once on a
6
+ JSKIT host; plain Fastify hosts use `registerFastifyConversations`.
7
+
8
+ Two hosting modes share that runtime:
9
+
10
+ - **Application-supplied runtime.** The host supplies its own
11
+ `createConversationRuntime` (file or application storage) as the
12
+ `assistant.conversations` capability. No database is required.
13
+ - **Built-in database-backed surface.** The optional SQL integration adds
14
+ database conversations, per-surface settings and the request-streaming chat
15
+ route. It needs `npm run db:migrate` and is composed by the
16
+ `assistant/assistant-surface` pattern.
17
+
18
+ Main exports by subpath:
19
+
20
+ | Subpath | Provides |
21
+ | --- | --- |
22
+ | `@jskit-ai/assistant-runtime/server` | `AssistantFeature`, `createAssistantActions`, `registerAssistantRoutes`, `registerConversationSubscriptions`, `registerFastifyConversations` |
23
+ | `@jskit-ai/assistant-runtime/server/actionIds` | Assistant action identifiers |
24
+ | `@jskit-ai/assistant-runtime/client` | `AssistantConversationClientElement`, `AssistantSurfaceClientElement`, `AssistantSettingsClientElement`, `useAssistantConversation`, `configureAssistantConversations` |
25
+ | `@jskit-ai/assistant-runtime/shared` | Assistant surface configuration helpers |
26
+
27
+ The full guides are in the JSKIT agent docs:
28
+ `packages/agent-docs/site/guide/app-extras/assistant.md` (setup and hosting
29
+ modes) and `packages/agent-docs/site/guide/app-extras/assistant-conversation.md`
30
+ (client contract and advanced host facilities).
31
+
32
+ ## Subscription diagnostics
1
33
 
2
34
  With the existing realtime diagnostic option (`JSKIT_REALTIME_DEBUG=1`, or
3
35
  `config.realtime.debug=true`), conversation subscriptions emit one `pending`
@@ -12,6 +44,8 @@ Timers are removed on acknowledgement, failure, replacement, unsubscribe
12
44
  and connection close. Diagnostics contain no request body or conversation text,
13
45
  and do not change authorization, subscription deadlines or model work.
14
46
 
47
+ ## Subscription release
48
+
15
49
  Released subscriptions stop setup at the next completed phase. The existing
16
50
  socket owner supplies its current subscription identity to the subscribe action;
17
51
  an obsolete open cannot attach a new observer, and a late observer is detached
@@ -19,6 +53,8 @@ without starting a snapshot. Reads already in progress may finish, but cannot
19
53
  acknowledge into a replacement view. Reload performs fresh authorization, and
20
54
  releasing a view never stops its model turn.
21
55
 
56
+ ## Queued prepared submissions
57
+
22
58
  Prepared application submissions may explicitly enable `queueWhileSending` to
23
59
  reuse the binding's serial delivery queue while application preparation is still
24
60
  running. Each queued request retains its captured payload and receipt identity.
@@ -1,5 +1,6 @@
1
1
  import { createMemoryTurnRequests } from "../../test/support/memoryTurnRequests.js";
2
2
  import { createChatService } from "../../src/server/services/chatService.js";
3
+ import { ASSISTANT_STREAM_WRITER_METHODS } from "../../src/server/support/assistantStreamWriter.js";
3
4
  import { createAssistantConversationRuntime } from "../../src/server/createAssistantRuntime.js";
4
5
  import { createMemoryAssistantRows } from "../../test/support/memoryAssistantRows.js";
5
6
  import { defineConfig } from "vite";
@@ -81,7 +82,6 @@ export default defineConfig({
81
82
  } },
82
83
  transcriptService: {
83
84
  async createConversationForTurn() { return { conversation: { id: "100" } }; },
84
- async appendMessage(_surface, _id, message) { transcript.push(message); },
85
85
  async completeConversation() {}
86
86
  },
87
87
  serviceToolCatalog: { limits: { maxToolArgumentBytes: 64 * 1024 }, resolveToolSet: () => ({ tools: [] }),
@@ -96,7 +96,7 @@ export default defineConfig({
96
96
  attachments: dependencies.attachments
97
97
  });
98
98
  const service = createChatService({ ...dependencies, conversationRuntime });
99
- const streamWriter = Object.fromEntries(["sendMeta", "sendAssistantDelta", "sendAssistantMessage", "sendToolCall", "sendToolResult", "sendError", "sendDone"].map(method => [method, event => send(stream, event)]));
99
+ const streamWriter = Object.fromEntries(ASSISTANT_STREAM_WRITER_METHODS.map(method => [method, event => send(stream, event)]));
100
100
  try { await service.streamChat({ ...payload, targetSurfaceId: "admin" }, { streamWriter, abortSignal: controller.signal, context: { actor: { id: "1" } } }); }
101
101
  catch (error) { if (!res.destroyed) send(stream, { type: "error", message: error.message }); }
102
102
  finally { res.end(); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-runtime",
3
- "version": "0.1.234",
3
+ "version": "0.1.235",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1"
@@ -12,19 +12,19 @@
12
12
  "./server": "./src/server/index.js"
13
13
  },
14
14
  "dependencies": {
15
- "@jskit-ai/assistant-core": "0.1.240",
15
+ "@jskit-ai/assistant-core": "0.1.241",
16
16
  "json-rest-schema": "^1.0.17"
17
17
  },
18
18
  "peerDependencies": {
19
- "@jskit-ai/http-runtime": "0.1.243",
20
- "@jskit-ai/http-web": "0.1.92",
21
- "@jskit-ai/kernel": "0.1.245",
22
- "@jskit-ai/realtime": "0.1.244",
23
- "@jskit-ai/shell-web": "0.1.251",
19
+ "@jskit-ai/http-runtime": "0.1.244",
20
+ "@jskit-ai/http-web": "0.1.93",
21
+ "@jskit-ai/kernel": "0.1.246",
22
+ "@jskit-ai/realtime": "0.1.245",
23
+ "@jskit-ai/shell-web": "0.1.252",
24
24
  "@tanstack/vue-query": "^5.90.5",
25
25
  "vue": "^3.5.13",
26
26
  "vuetify": "^4.0.0",
27
- "@jskit-ai/database-runtime": "0.1.245"
27
+ "@jskit-ai/database-runtime": "0.1.246"
28
28
  },
29
29
  "description": "Capability-based assistant runtime with per-surface chat, transcripts, settings, and action tools.",
30
30
  "jskit": {
@@ -38,8 +38,7 @@
38
38
  "runtime.config",
39
39
  "runtime.env",
40
40
  "runtime.http",
41
- "runtime.http-web",
42
- "runtime.http-client"
41
+ "runtime.http-web"
43
42
  ]
44
43
  },
45
44
  "runtime": {
@@ -180,9 +180,6 @@ async function submit() {
180
180
  });
181
181
  if (!validation.ok) {
182
182
  fieldErrors.systemPrompt = String(validation.fieldErrors.systemPrompt || "");
183
- if (validation.globalErrors.length > 0) {
184
- reportAssistantSettingsError(validation.globalErrors[0], "assistant.settings:validation");
185
- }
186
183
  return;
187
184
  }
188
185
 
@@ -98,11 +98,8 @@ function submitMessage({ attachments = [], retryMessageId = "" }) {
98
98
  }
99
99
 
100
100
  function conversationSubtitle(entry) {
101
- const details = [runtime.meta.normalizeConversationStatus(entry.status), runtime.meta.formatConversationStartedAt(entry.startedAt), `${Number(entry.messageCount || 0)} messages`];
102
- if (state.isAdminSurface) {
103
- details.push(entry.createdByUserDisplayName || entry.createdByUserEmail || (entry.createdByUserId ? `User #${entry.createdByUserId}` : "Unknown user"));
104
- }
105
- return details.join(" · ");
101
+ return [runtime.meta.normalizeConversationStatus(entry.status), runtime.meta.formatConversationStartedAt(entry.startedAt),
102
+ `${Number(entry.messageCount || 0)} messages`].join(" · ");
106
103
  }
107
104
 
108
105
  async function selectConversation(entry) {
@@ -4,15 +4,16 @@ import { createAssistantMessageDelivery, retainAssistantConversation, unmatchedO
4
4
  import { createAssistantTextSubmission } from "@jskit-ai/assistant-core/client/conversation";
5
5
  import { useAssistantQuestions } from "@jskit-ai/assistant-core/client/conversation-questions";
6
6
  import { assistantHttpClient, createAssistantApi } from "@jskit-ai/assistant-core/client";
7
- import { buildAssistantApiPath } from "@jskit-ai/assistant-core/shared";
8
7
  import {
9
- conversationTurnsFromMessages, latestAssistantMessageAwaitingUserReply, mergeConversationLogPages, mergeConversationStream,
8
+ conversationTurnMessages, conversationTurnsFromMessages, latestAssistantMessageAwaitingUserReply, mergeConversationLogPages, mergeConversationStream,
10
9
  normalizeConversationLogPage, normalizeConversationLogPagination
11
10
  } from "@jskit-ai/assistant-core/shared/conversation";
12
11
  import { useRealtimeSocket } from "@jskit-ai/realtime/client/composables/useRealtimeEvent";
12
+ import { SHELL_WEB_PLACEMENT_RUNTIME_KEY } from "@jskit-ai/shell-web/client/placement";
13
13
  import { useSurfaceRouteContext } from "@jskit-ai/shell-web/client/navigation/useSurfaceRouteContext";
14
- import { resolveAssistantSurfaceConfig } from "../../shared/assistantSurfaces.js";
14
+ import { resolveAssistantRuntimeRoute, resolveAssistantSurfaceConfig } from "../../shared/assistantSurfaces.js";
15
15
  import { useWorkspaceWebScopeSupport } from "../support/workspaceScopeSupport.js";
16
+ import { isAccessDenied } from "../support/accessDenied.js";
16
17
  import { subscribeAssistantConversation, refreshedHistoryPages, patchLoadedHistoryPages } from "../support/subscribeAssistantConversation.js";
17
18
 
18
19
  const text = value => String(toValue(value) ?? "").trim();
@@ -40,6 +41,20 @@ function isQuestionConfiguration(value) {
40
41
  !["questions", "choices", "capture"].some(key => key in value));
41
42
  }
42
43
 
44
+ function questionsAnswered(prompts) {
45
+ return prompts.questions.every(question => String(prompts.answers[question.name] || "").trim());
46
+ }
47
+
48
+ /** The authored conversation request for a composer payload without one. */
49
+ function authoredRequest(payload, { attachmentIds, steer }) {
50
+ return {
51
+ text: String(payload.message || ""),
52
+ ...(payload.data !== undefined ? { data: payload.data } : {}),
53
+ ...(attachmentIds?.length ? { attachmentIds } : {}),
54
+ ...(steer ? { steer: true } : {})
55
+ };
56
+ }
57
+
43
58
  function draftAfterAcceptedSubmission(currentDraft = "", submittedDraft = "") {
44
59
  const current = String(currentDraft || "");
45
60
  const submitted = String(submittedDraft || "");
@@ -103,9 +118,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
103
118
  ]).conversationLog;
104
119
  // Use the original overlay against the full loaded saved span. A synthetic
105
120
  // live older turn must not replace its authored request or saved progress.
106
- const messages = (snapshot.value?.turns || []).flatMap(turn => turn.messages || [
107
- turn.system, turn.user, ...(turn.thinking || []), ...(turn.commentary || []), turn.assistant
108
- ].filter(Boolean));
121
+ const messages = (snapshot.value?.turns || []).flatMap(conversationTurnMessages);
109
122
  return mergeConversationStream(saved, { messages });
110
123
  });
111
124
  const oldestLoadedPage = computed(() => olderPages.value[0] || snapshot.value);
@@ -231,24 +244,29 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
231
244
  if (!current.value) return;
232
245
  loading.value = false;
233
246
  error.value = failure.message;
234
- accessDenied.value = [401, 403].includes(Number(failure.status || failure.statusCode));
247
+ accessDenied.value = isAccessDenied(failure);
235
248
  if (accessDenied.value) {
236
249
  deniedHistoryRevision += 1;
237
- snapshot.value = null;
238
- historyRevision += 1;
239
- olderPages.value = [];
240
- draft.value = "";
241
- draftMessageId.value = "";
242
- draftAttachments.value = [];
243
- draftRetry.value = null;
244
- delivery.reset();
245
- goalError.value = "";
246
- goalView.value = null;
250
+ clearConversationView();
247
251
  goalTargetCurrent.value = false;
248
- goalLoadError.value = "";
249
- goalReadController?.abort();
250
252
  }
251
253
  }
254
+ // Shared by access denial and actor change. Access denial keeps the open goal
255
+ // command, queued goal read and question form; an actor change clears them.
256
+ function clearConversationView() {
257
+ delivery.reset();
258
+ snapshot.value = null;
259
+ historyRevision += 1;
260
+ olderPages.value = [];
261
+ draft.value = "";
262
+ draftMessageId.value = "";
263
+ draftAttachments.value = [];
264
+ draftRetry.value = null;
265
+ goalError.value = "";
266
+ goalView.value = null;
267
+ goalLoadError.value = "";
268
+ goalReadController?.abort();
269
+ }
252
270
  function receiveEvent(event) {
253
271
  if (event.type === "transcript" && olderPages.value.some(page =>
254
272
  page.conversationLog.some(turn => turn.turnId === event.patch?.turn?.turnId))) {
@@ -330,20 +348,9 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
330
348
  watch(current, value => {
331
349
  if (value) return;
332
350
  for (const pending of pendingMessages.values()) if (!pending.dispatched) pending.controller.abort();
333
- delivery.reset();
334
- snapshot.value = null;
335
- historyRevision += 1;
336
- olderPages.value = [];
337
- draft.value = "";
338
- draftMessageId.value = "";
339
- draftAttachments.value = [];
340
- draftRetry.value = null;
341
- goalError.value = "";
351
+ clearConversationView();
342
352
  goalAction.value = "";
343
- goalView.value = null;
344
- goalLoadError.value = "";
345
353
  goalReadQueued = false;
346
- goalReadController?.abort();
347
354
  questions.reset();
348
355
  }, { flush: "sync" });
349
356
  onScopeDispose(() => {
@@ -374,12 +381,11 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
374
381
  const appAdmission = policy.admitWhileWorking && !steering;
375
382
  if ((snapshot.value.status === "working" || delivery.state.sending) && !steering && !deferred && !appAdmission) return false;
376
383
  if ((retry?.payload || payload).displayAttachments?.length && snapshot.value.capabilities?.attachments !== true) return false;
377
- const captured = retry?.payload?.request ? retry.payload : { ...(retry?.payload || payload), request: payload.request || {
378
- text: String(payload.message || ""),
379
- ...(payload.data !== undefined ? { data: payload.data } : {}),
380
- ...(payload.displayAttachments?.length ? { attachmentIds: payload.displayAttachments.map(file => file.attachmentId) } : {}),
381
- ...(steerable.value ? { steer: true } : {})
382
- } };
384
+ const captured = retry?.payload?.request ? retry.payload : { ...(retry?.payload || payload), request: payload.request ||
385
+ authoredRequest(payload, {
386
+ attachmentIds: payload.displayAttachments?.map(file => file.attachmentId),
387
+ steer: steerable.value
388
+ }) };
383
389
  return submitRequest(captured, { messageId, onAccepted, queue: queueWhileSending !== false && (steering || deferred || appAdmission), deferred, appAdmission });
384
390
  }
385
391
  function submitPrepared(payload, { messageId = crypto.randomUUID(), onAccepted, prepare } = {}) {
@@ -583,7 +589,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
583
589
  } catch (failure) {
584
590
  if (current.value) {
585
591
  loadMoreError.value = String(failure?.message || failure || "Older conversation history could not be loaded.");
586
- if ([401, 403].includes(Number(failure.status || failure.statusCode))) receiveError(failure);
592
+ if (isAccessDenied(failure)) receiveError(failure);
587
593
  }
588
594
  finish({ changed: false, loaded: false });
589
595
  return false;
@@ -665,7 +671,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
665
671
  } catch (failure) {
666
672
  if (current.value) {
667
673
  goalError.value = failure.message;
668
- if ([401, 403].includes(Number(failure.status || failure.statusCode))) receiveError(failure);
674
+ if (isAccessDenied(failure)) receiveError(failure);
669
675
  }
670
676
  return false;
671
677
  } finally {
@@ -804,7 +810,7 @@ function conversationBindingSetup({ socket, boundedTask = null } = {}) {
804
810
  if (!app) throw new Error("Conversation binding must be acquired from the application setup.");
805
811
  if (boundedTask !== null) return { app };
806
812
  const routeContext = useSurfaceRouteContext();
807
- const placement = inject("jskit.shell-web.runtime.web-placement.client", null);
813
+ const placement = inject(SHELL_WEB_PLACEMENT_RUNTIME_KEY, null);
808
814
  if (typeof placement?.getContext !== "function" || typeof placement?.subscribe !== "function") {
809
815
  throw new Error("Conversation binding requires the application's placement runtime.");
810
816
  }
@@ -856,15 +862,13 @@ function createConversationBinding({ conversationId, endpoint = "", surfaceId =
856
862
  const host = (text(hostSurfaceId) || text(routeContext.currentSurfaceId)).toLowerCase();
857
863
  const target = (text(surfaceId) || host).toLowerCase();
858
864
  const surface = resolveAssistantSurfaceConfig(config, target);
859
- const workspace = toValue(workspaceSlug) === undefined
860
- ? surface?.runtimeSurfaceRequiresWorkspace ? workspaceScope.readRouteScope(routeContext).workspaceSlug : ""
861
- : text(workspaceSlug);
865
+ const route = resolveAssistantRuntimeRoute(surface, toValue(workspaceSlug) === undefined
866
+ ? workspaceScope.readRouteScope(routeContext).workspaceSlug : text(workspaceSlug));
862
867
  const actor = toValue(actorKey);
863
868
  return Object.freeze({ conversationId: text(conversationId), targetSurfaceId: target, hostSurfaceId: host || target,
864
- workspaceSlug: workspace || "", configured: Boolean(surface) && (!surface.runtimeSurfaceRequiresWorkspace || Boolean(workspace)),
869
+ workspaceSlug: route.workspaceSlug, configured: route.configured,
865
870
  actorKey: actor === undefined ? String(routeContext.placementContext.value?.user?.id || "") : text(actor),
866
- endpoint: text(endpoint) || buildAssistantApiPath({ requiresWorkspace: Boolean(workspace), workspaceSlug: workspace,
867
- suffix: `/${target}` }) });
871
+ endpoint: text(endpoint) || route.endpoint });
868
872
  });
869
873
  // The original composer owns input before a conversation ID is available.
870
874
  // It is scoped to the real actor/surface/workspace, never a placeholder ID.
@@ -922,7 +926,7 @@ function createConversationBinding({ conversationId, endpoint = "", surfaceId =
922
926
  const prompts = questionOwner.value;
923
927
  const formatsQuestions = typeof prompts?.capture === "function";
924
928
  if (!retry && formatsQuestions && prompts.questions?.length &&
925
- !prompts.questions.every(question => String(prompts.answers[question.name] || "").trim())) return false;
929
+ !questionsAnswered(prompts)) return false;
926
930
  const answer = !retry && formatsQuestions ? prompts.capture(current.draft.value) : null;
927
931
  const captured = retry?.payload || { message: answer?.message ?? current.draft.value, displayAttachments: files,
928
932
  ...(waitForAcceptance ? { draft: current.draft.value } : {}),
@@ -948,13 +952,11 @@ function createConversationBinding({ conversationId, endpoint = "", surfaceId =
948
952
  return false;
949
953
  }
950
954
  const captured = previous?.payload?.request ? previous.payload : payload;
951
- const authored = captured.request ? captured : { ...captured, request: {
952
- text: String(captured.message || ""),
953
- ...(captured.data !== undefined ? { data: captured.data } : {}),
954
- ...(captured.attachmentIds?.length ? { attachmentIds: captured.attachmentIds }
955
- : captured.displayAttachments?.length ? { attachmentIds: captured.displayAttachments.map(file => file.attachmentId) } : {}),
956
- ...((previous ? captured.submissionKind === "steer" : working) ? { steer: true } : {})
957
- } };
955
+ const authored = captured.request ? captured : { ...captured, request: authoredRequest(captured, {
956
+ attachmentIds: captured.attachmentIds?.length ? captured.attachmentIds
957
+ : captured.displayAttachments?.map(file => file.attachmentId),
958
+ steer: previous ? captured.submissionKind === "steer" : working
959
+ }) };
958
960
  return submitCaptured(current, authored, { messageId: id, retry: previous, clearDraft,
959
961
  attachments, isCurrent, onSubmit, onAccepted, prepare });
960
962
  }
@@ -1016,7 +1018,7 @@ function createConversationBinding({ conversationId, endpoint = "", surfaceId =
1016
1018
  const editable = canDraftWhileLoading.value ? !current || current.editable.value : current?.available.value;
1017
1019
  const message = formatsQuestions ? prompts.capture(draft).message : draft;
1018
1020
  const answered = !formatsQuestions || !prompts.questions?.length ||
1019
- prompts.questions.every(question => String(prompts.answers[question.name] || "").trim());
1021
+ questionsAnswered(prompts);
1020
1022
  const canSend = current?.canSubmit.value || false;
1021
1023
  return {
1022
1024
  delivery: current?.delivery,
@@ -3,7 +3,6 @@ import { useQueryClient } from "@tanstack/vue-query";
3
3
  import { getClientAppConfig } from "@jskit-ai/kernel/client";
4
4
  import { normalizeObject, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
5
5
  import { createAssistantMessageDelivery } from "@jskit-ai/assistant-core/client/conversation-delivery";
6
- import { buildAssistantApiPath } from "@jskit-ai/assistant-core/shared";
7
6
  import {
8
7
  ASSISTANT_STREAM_EVENT_TYPES,
9
8
  MAX_INPUT_CHARS,
@@ -21,7 +20,7 @@ import {
21
20
  import { useShellWebErrorRuntime } from "@jskit-ai/shell-web/client/error";
22
21
  import { usePagedCollection } from "@jskit-ai/http-web/client/composables/usePagedCollection";
23
22
  import { useSurfaceRouteContext } from "@jskit-ai/shell-web/client/navigation/useSurfaceRouteContext";
24
- import { resolveAssistantSurfaceConfig } from "../../shared/assistantSurfaces.js";
23
+ import { resolveAssistantRuntimeRoute, resolveAssistantSurfaceConfig } from "../../shared/assistantSurfaces.js";
25
24
  import {
26
25
  buildHistory,
27
26
  buildId,
@@ -162,35 +161,22 @@ function useAssistantRuntime({ api = null, surfaceId = "", integrationId = "" }
162
161
  resolveAssistantSurfaceConfig(appConfig, toValue(surfaceId))
163
162
  );
164
163
  const routeScope = computed(() => workspaceScopeSupport.readRouteScope(routeContext));
165
- const runtimeScope = computed(() => {
166
- const workspaceSlug = assistantSurface.value?.runtimeSurfaceRequiresWorkspace
167
- ? normalizeText(routeScope.value.workspaceSlug).toLowerCase()
168
- : "";
169
-
170
- return {
171
- targetSurfaceId: normalizeText(assistantSurface.value?.targetSurfaceId).toLowerCase(),
172
- userId: normalizeRecordId(placementSnapshot.value?.user?.id, { fallback: null }),
173
- workspaceSlug
174
- };
175
- });
176
- const hasRuntimeScope = computed(() =>
177
- Boolean(assistantSurface.value && runtimeScope.value.userId) &&
178
- (assistantSurface.value?.runtimeSurfaceRequiresWorkspace ? Boolean(runtimeScope.value.workspaceSlug) : true)
179
- );
164
+ const runtimeRoute = computed(() => resolveAssistantRuntimeRoute(assistantSurface.value,
165
+ normalizeText(routeScope.value.workspaceSlug).toLowerCase()));
166
+ const runtimeScope = computed(() => ({
167
+ targetSurfaceId: normalizeText(assistantSurface.value?.targetSurfaceId).toLowerCase(),
168
+ userId: normalizeRecordId(placementSnapshot.value?.user?.id, { fallback: null }),
169
+ workspaceSlug: runtimeRoute.value.workspaceSlug
170
+ }));
171
+ const hasRuntimeScope = computed(() => runtimeRoute.value.configured && Boolean(runtimeScope.value.userId));
180
172
 
181
173
  const runtimeApi = createRuntimeApi({
182
174
  overrideApi: api,
183
- resolveBasePath: () =>
184
- buildAssistantApiPath({
185
- requiresWorkspace: assistantSurface.value?.runtimeSurfaceRequiresWorkspace === true,
186
- workspaceSlug: runtimeScope.value.workspaceSlug,
187
- suffix: `/${runtimeScope.value.targetSurfaceId}`
188
- }),
175
+ resolveBasePath: () => runtimeRoute.value.endpoint,
189
176
  resolveSurfaceId: () => normalizeText(currentSurfaceId.value).toLowerCase()
190
177
  });
191
178
 
192
179
  const activeConversationId = computed(() => normalizeRecordId(conversationId.value, { fallback: "" }));
193
- const isAdminSurface = computed(() => normalizeText(currentSurfaceId.value).toLowerCase() === "admin");
194
180
  const canSend = computed(() => {
195
181
  return Boolean(
196
182
  hasRuntimeScope.value &&
@@ -324,13 +310,14 @@ function useAssistantRuntime({ api = null, surfaceId = "", integrationId = "" }
324
310
  return messages.value.find((entry) => entry.id === messageId) || null;
325
311
  }
326
312
 
327
- async function invalidateConversationScope() {
313
+ // Invalidation refetches the active conversation list; a turn never changes the scope's settings.
314
+ async function invalidateConversations() {
328
315
  if (!hasRuntimeScope.value) {
329
316
  return;
330
317
  }
331
318
 
332
319
  await queryClient.invalidateQueries({
333
- queryKey: assistantScopeQueryKey(runtimeScope.value)
320
+ queryKey: [...assistantScopeQueryKey(runtimeScope.value), "conversations"]
334
321
  });
335
322
  }
336
323
 
@@ -634,8 +621,7 @@ function useAssistantRuntime({ api = null, surfaceId = "", integrationId = "" }
634
621
  isStreaming.value = false;
635
622
  isCanceling.value = false;
636
623
  if (accepted) {
637
- await invalidateConversationScope();
638
- await refreshConversationHistory();
624
+ await invalidateConversations();
639
625
  }
640
626
  }
641
627
  }
@@ -673,7 +659,6 @@ function useAssistantRuntime({ api = null, surfaceId = "", integrationId = "" }
673
659
  conversationHistoryLoadingMore,
674
660
  conversationHistoryHasMore,
675
661
  conversationHistoryError,
676
- isAdminSurface,
677
662
  canSend,
678
663
  canStartNewConversation
679
664
  },
@@ -0,0 +1,6 @@
1
+ /** Whether a conversation failure means the viewer lost access to it. */
2
+ function isAccessDenied(error) {
3
+ return [401, 403].includes(Number(error.status || error.statusCode));
4
+ }
5
+
6
+ export { isAccessDenied };
@@ -1,9 +1,10 @@
1
- import { applyConversationLogPatch, mergeConversationStream } from "@jskit-ai/assistant-core/shared/conversation";
1
+ import { applyConversationLogPatch, conversationTurnMessages, mergeConversationStream } from "@jskit-ai/assistant-core/shared/conversation";
2
2
  import {
3
3
  ASSISTANT_CONVERSATION_EVENT,
4
4
  ASSISTANT_CONVERSATION_SUBSCRIBE,
5
5
  ASSISTANT_CONVERSATION_UNSUBSCRIBE
6
6
  } from "../../shared/conversationRealtime.js";
7
+ import { isAccessDenied } from "./accessDenied.js";
7
8
 
8
9
  // Private to the original client history/subscription coordination. Symbols
9
10
  // survive local snapshot spreads but never enter the conversation wire format.
@@ -44,9 +45,7 @@ function subscribeAssistantConversation({
44
45
  const pendingReads = new Set();
45
46
 
46
47
  function savedMessageIds() {
47
- return new Set((state?.conversationLog || []).flatMap(turn => turn.messages || [
48
- turn.system, turn.user, ...(turn.thinking || []), ...(turn.commentary || []), turn.assistant
49
- ].filter(Boolean)).map(message => message.messageId));
48
+ return new Set((state?.conversationLog || []).flatMap(conversationTurnMessages).map(message => message.messageId));
50
49
  }
51
50
  function receiveStreaming(snapshot) {
52
51
  if (!Number.isSafeInteger(snapshot?.revision) || (streaming && snapshot.revision <= streaming.revision)) return false;
@@ -97,7 +96,7 @@ function subscribeAssistantConversation({
97
96
  receiveState(snapshot);
98
97
  }).catch(error => {
99
98
  if (current()) {
100
- if ([401, 403].includes(Number(error.status || error.statusCode))) {
99
+ if (isAccessDenied(error)) {
101
100
  if (epoch === null) {
102
101
  disconnected();
103
102
  if (socket.connected) socket.emit(ASSISTANT_CONVERSATION_UNSUBSCRIBE, { subscriptionId });
@@ -12,6 +12,7 @@ import {
12
12
  } from "@jskit-ai/assistant-core/shared";
13
13
  import { actionIds } from "./actionIds.js";
14
14
  import { createAssistantSettingsActions } from "./createAssistantSettingsActions.js";
15
+ import { requireAssistantSurface } from "./support/assistantSurface.js";
15
16
  import {
16
17
  assistantConversationIdInputValidator,
17
18
  assistantConversationGoalBodyValidator,
@@ -109,8 +110,7 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
109
110
  { mode: "patch", context: "assistant-runtime conversation send action input" }
110
111
  );
111
112
  async function openConversation(input, context) {
112
- const assistantSurface = resolveAssistantSurfaceConfig(config, input.targetSurfaceId);
113
- if (!assistantSurface) throw new AppError(404, "Assistant not found.");
113
+ const assistantSurface = requireAssistantSurface(config, input.targetSurfaceId);
114
114
  let conversationContext = context;
115
115
  if (assistantSurface.runtimeSurfaceRequiresWorkspace) {
116
116
  if (typeof workspaceScopeSupport?.resolveWorkspace !== "function") {
@@ -134,8 +134,6 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
134
134
  input: conversationReadInputValidator,
135
135
  output: null,
136
136
  idempotency: "none",
137
- audit: { actionName: actionIds.conversationRead },
138
- observability: {},
139
137
  async execute(input, context) {
140
138
  const conversation = await openConversation(input, context);
141
139
  const options = {};
@@ -155,8 +153,6 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
155
153
  input: conversationSendInputValidator,
156
154
  output: null,
157
155
  idempotency: "none",
158
- audit: { actionName: actionIds.conversationSend },
159
- observability: {},
160
156
  async execute(input, context) {
161
157
  const conversation = await openConversation(input, context);
162
158
  return conversation.send({
@@ -178,8 +174,6 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
178
174
  input: conversationInputValidator,
179
175
  output: null,
180
176
  idempotency: "none",
181
- audit: { actionName: actionIds.conversationCancel },
182
- observability: {},
183
177
  async execute(input, context) {
184
178
  const conversation = await openConversation(input, context);
185
179
  return conversation.cancel();
@@ -195,8 +189,6 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
195
189
  input: conversationInputValidator,
196
190
  output: null,
197
191
  idempotency: "none",
198
- audit: { actionName: actionIds.conversationSubscribe },
199
- observability: {},
200
192
  async execute(input, context, deps) {
201
193
  if (typeof deps?.onEvent !== "function" || typeof deps?.onRelease !== "function") {
202
194
  throw new TypeError("Conversation subscriptions require their server-owned event and cleanup callbacks.");
@@ -228,8 +220,6 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
228
220
  input: conversationInspectInputValidator,
229
221
  output: null,
230
222
  idempotency: "none",
231
- audit: { actionName: actionIds.conversationInspectDelivery },
232
- observability: {},
233
223
  async execute(input, context) {
234
224
  const conversation = await openConversation(input, context);
235
225
  return conversation.inspectDelivery({ messageId: input.messageId });
@@ -245,8 +235,6 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
245
235
  input: conversationInputValidator,
246
236
  output: null,
247
237
  idempotency: "none",
248
- audit: { actionName: actionIds.conversationGoalRead },
249
- observability: {},
250
238
  async execute(input, context) {
251
239
  return (await openConversation(input, context)).readGoal();
252
240
  }
@@ -263,8 +251,6 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
263
251
  }),
264
252
  output: null,
265
253
  idempotency: "none",
266
- audit: { actionName: actionIds.conversationGoalUpdate },
267
- observability: {},
268
254
  async execute(input, context) {
269
255
  const conversation = await openConversation(input, context);
270
256
  const goal = { action: input.action, expectedSegmentId: input.expectedSegmentId };
@@ -286,8 +272,6 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
286
272
  }),
287
273
  output: null,
288
274
  idempotency: "none",
289
- audit: { actionName: actionIds.conversationConfigure },
290
- observability: {},
291
275
  async execute(input, context) {
292
276
  return (await openConversation(input, context)).configure(input.configuration);
293
277
  }
@@ -304,8 +288,6 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
304
288
  }),
305
289
  output: null,
306
290
  idempotency: "none",
307
- audit: { actionName: actionIds.conversationSelect },
308
- observability: {},
309
291
  async execute(input, context) {
310
292
  return (await openConversation(input, context)).select(input.selection);
311
293
  }
@@ -322,8 +304,6 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
322
304
  }),
323
305
  output: null,
324
306
  idempotency: "none",
325
- audit: { actionName: actionIds.conversationReplace },
326
- observability: {},
327
307
  async execute(input, context) {
328
308
  return (await openConversation(input, context)).replace(input.replacement);
329
309
  }
@@ -364,8 +344,7 @@ function createAssistantActions({
364
344
  if (!conversationAccess) return definition;
365
345
  const selected = definition.id === actionIds.conversationSubscribe ? {
366
346
  ...definition,
367
- id: conversationAccess.subscribeActionId,
368
- audit: { ...definition.audit, actionName: conversationAccess.subscribeActionId }
347
+ id: conversationAccess.subscribeActionId
369
348
  } : definition;
370
349
  return conversationAccess.wrapAction(selected);
371
350
  }),
@@ -385,10 +364,6 @@ function createAssistantActions({
385
364
  },
386
365
  input: runtimeChatStreamInputValidator,
387
366
  idempotency: "optional",
388
- audit: {
389
- actionName: actionIds.chatStream
390
- },
391
- observability: {},
392
367
  async execute(input, context, deps) {
393
368
  return chatService.streamChat(input, {
394
369
  context,
@@ -409,10 +384,6 @@ function createAssistantActions({
409
384
  input: runtimeConversationsListInputValidator,
410
385
  output: null,
411
386
  idempotency: "none",
412
- audit: {
413
- actionName: actionIds.conversationsList
414
- },
415
- observability: {},
416
387
  async execute(input, context) {
417
388
  return returnJsonApiData(await chatService.listConversations(input.query, {
418
389
  context,
@@ -432,10 +403,6 @@ function createAssistantActions({
432
403
  input: runtimeConversationMessagesListInputValidator,
433
404
  output: null,
434
405
  idempotency: "none",
435
- audit: {
436
- actionName: actionIds.conversationMessagesList
437
- },
438
- observability: {},
439
406
  async execute(input, context) {
440
407
  return returnJsonApiData(await chatService.getConversationMessages(input.conversationId, input.query, {
441
408
  context,