@jskit-ai/assistant-runtime 0.1.193 → 0.1.194

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 (50) hide show
  1. package/fixtures/canonical-conversation/App.vue +188 -0
  2. package/fixtures/canonical-conversation/DraftConversation.vue +32 -0
  3. package/fixtures/canonical-conversation/index.html +1 -0
  4. package/fixtures/canonical-conversation/main.js +61 -0
  5. package/fixtures/canonical-conversation/socket.js +50 -0
  6. package/fixtures/canonical-conversation/vite.config.mjs +178 -0
  7. package/fixtures/conversation/vite.config.mjs +12 -2
  8. package/package.json +23 -10
  9. package/src/client/components/AssistantConversationClientElement.vue +57 -0
  10. package/src/client/composables/useAssistantConversation.js +874 -0
  11. package/src/client/index.js +2 -0
  12. package/src/client/support/assistantRuntimeState.js +1 -1
  13. package/src/client/support/subscribeAssistantConversation.js +205 -0
  14. package/src/server/AssistantProvider.js +82 -106
  15. package/src/server/actionIds.js +10 -0
  16. package/src/server/actions.js +287 -79
  17. package/src/server/createAssistantRuntime.js +138 -0
  18. package/src/server/createAssistantSettingsActions.js +89 -0
  19. package/src/server/index.js +6 -0
  20. package/src/server/inputSchemas.js +90 -0
  21. package/src/server/registerConversationSubscriptions.js +163 -0
  22. package/src/server/registerFastifyConversations.js +76 -0
  23. package/src/server/registerRoutes.js +220 -198
  24. package/src/server/registerSettingsRoutes.js +113 -0
  25. package/src/server/repositories/conversationsRepository.js +12 -5
  26. package/src/server/repositories/messagesRepository.js +12 -0
  27. package/src/server/services/chatService.js +88 -694
  28. package/src/server/services/transcriptService.js +56 -52
  29. package/src/server/sqlConversationStorage.js +192 -0
  30. package/src/server/support/assistantRouteContext.js +91 -0
  31. package/src/server/support/createSurfaceAwareToolCatalog.js +6 -2
  32. package/src/shared/assistantSurfaces.js +14 -0
  33. package/src/shared/conversationRealtime.js +9 -0
  34. package/src/shared/index.js +1 -0
  35. package/test/actionDefinitions.test.js +44 -0
  36. package/test/aiConnections.test.js +1 -1
  37. package/test/assistantConversationBinding.browser.test.js +1868 -0
  38. package/test/assistantConversationOperations.test.js +459 -0
  39. package/test/assistantConversationSubscriptions.test.js +692 -0
  40. package/test/assistantIntegrationBoundary.test.js +103 -0
  41. package/test/assistantRuntimeComposition.test.js +68 -0
  42. package/test/assistantSurfacePattern.test.js +1 -1
  43. package/test/assistantSurfaces.test.js +23 -0
  44. package/test/chatServiceLifecycle.test.js +12 -1
  45. package/test/featureRuntime.test.js +238 -0
  46. package/test/registerFastifyConversations.test.js +169 -0
  47. package/test/sqlConversationStorage.test.js +205 -0
  48. package/test/support/memoryAssistantRows.js +51 -0
  49. package/test/turnRequests.test.js +11 -2
  50. package/src/shared/assistantResponseText.js +0 -29
@@ -1,4 +1,6 @@
1
1
  export { default as AssistantSurfaceClientElement } from "./components/AssistantSurfaceClientElement.vue";
2
+ export { default as AssistantConversationClientElement } from "./components/AssistantConversationClientElement.vue";
2
3
  export { default as AssistantSettingsClientElement } from "./components/AssistantSettingsClientElement.vue";
3
4
  export { AssistantClientProvider } from "./providers/AssistantClientProvider.js";
4
5
  export { useAssistantRuntime } from "./composables/useAssistantRuntime.js";
6
+ export { configureAssistantConversations, useAssistantConversation, useAssistantConversationFactory } from "./composables/useAssistantConversation.js";
@@ -2,9 +2,9 @@ import { normalizeObject, normalizeRecordId, normalizeText } from "@jskit-ai/ker
2
2
  import {
3
3
  MAX_HISTORY_MESSAGES,
4
4
  MAX_INPUT_CHARS,
5
+ isAssistantProgressOnlyText,
5
6
  parseJsonObject
6
7
  } from "@jskit-ai/assistant-core/shared";
7
- import { isAssistantProgressOnlyText } from "../../shared/assistantResponseText.js";
8
8
  import { conversationTurnsFromMessages } from "@jskit-ai/assistant-core/shared/conversation";
9
9
 
10
10
  const CONVERSATION_MESSAGE_STATUS = Object.freeze({
@@ -0,0 +1,205 @@
1
+ import { applyConversationLogPatch, mergeConversationStream } from "@jskit-ai/assistant-core/shared/conversation";
2
+ import {
3
+ ASSISTANT_CONVERSATION_EVENT,
4
+ ASSISTANT_CONVERSATION_SUBSCRIBE,
5
+ ASSISTANT_CONVERSATION_UNSUBSCRIBE
6
+ } from "../../shared/conversationRealtime.js";
7
+
8
+ const SNAPSHOT_EVENTS = new Set(["accepted", "settled", "configuration", "phase", "error", "replaced", "goal"]);
9
+
10
+ function subscribeAssistantConversation({
11
+ socket, conversationId, targetSurfaceId, hostSurfaceId, workspaceSlug,
12
+ read, onState, onEvent = () => {}, onError = () => {}
13
+ } = {}) {
14
+ if (typeof socket?.on !== "function" || typeof socket?.off !== "function" || typeof socket?.timeout !== "function") {
15
+ throw new TypeError("Conversation subscriptions require the application's shared realtime socket.");
16
+ }
17
+ if (typeof read !== "function" || typeof onState !== "function" || typeof onEvent !== "function") {
18
+ throw new TypeError("Conversation subscriptions require a canonical read operation and state listener.");
19
+ }
20
+ const subscriptionId = crypto.randomUUID();
21
+ const input = { subscriptionId, conversationId, targetSurfaceId, hostSurfaceId,
22
+ ...(workspaceSlug === undefined ? {} : { workspaceSlug }) };
23
+ let disposed = false;
24
+ let generation = 0;
25
+ let epoch = null;
26
+ let revision = 0;
27
+ let pending = [];
28
+ let listening = false;
29
+ let state = null;
30
+ let streaming = null;
31
+ let reloadInFlight = null;
32
+ let reloadQueued = false;
33
+ const completed = new Map();
34
+ const settledTurns = new Set();
35
+ const pendingReads = new Set();
36
+
37
+ function savedMessageIds() {
38
+ return new Set((state?.conversationLog || []).flatMap(turn => turn.messages || [
39
+ turn.system, turn.user, ...(turn.thinking || []), ...(turn.commentary || []), turn.assistant
40
+ ].filter(Boolean)).map(message => message.messageId));
41
+ }
42
+ function receiveStreaming(snapshot) {
43
+ if (!Number.isSafeInteger(snapshot?.revision) || (streaming && snapshot.revision <= streaming.revision)) return false;
44
+ streaming = snapshot;
45
+ return true;
46
+ }
47
+ function publishState({ initial = false, canonical = false } = {}) {
48
+ if (!state) return;
49
+ const saved = savedMessageIds();
50
+ for (const id of completed.keys()) if (saved.has(id)) completed.delete(id);
51
+ const messages = new Map([...completed].map(([id, entry]) => [id, entry.message]));
52
+ for (const message of streaming?.messages || []) messages.set(message.messageId, message);
53
+ // Saved history owns completed identities; events only supply a display overlay.
54
+ onState({ ...state, streaming, turns: mergeConversationStream(state.conversationLog, { messages: [...messages.values()] }) }, { initial, canonical });
55
+ }
56
+ function receiveState(snapshot, options) {
57
+ state = snapshot;
58
+ receiveStreaming(snapshot.streaming);
59
+ publishState({ ...options, canonical: true });
60
+ }
61
+ function reload() {
62
+ if (reloadInFlight) {
63
+ reloadQueued = true;
64
+ return reloadInFlight;
65
+ }
66
+ const attempt = generation;
67
+ const settled = new Set(settledTurns);
68
+ settledTurns.clear();
69
+ const current = () => !disposed && listening && attempt === generation;
70
+ const pendingRead = { generation: attempt, patches: [] };
71
+ const job = Promise.resolve().then(() => {
72
+ if (!current()) return null;
73
+ pendingReads.add(pendingRead);
74
+ return read();
75
+ }).then(snapshot => {
76
+ if (!current()) return;
77
+ // Ported from the original history reader: retain only delivered updates
78
+ // received while this request was in flight before replacing its cache.
79
+ for (const patch of pendingRead.patches) {
80
+ snapshot = applyConversationLogPatch(snapshot, patch, { limit: snapshot.pagination?.limit });
81
+ }
82
+ for (const [id, entry] of completed) if (settled.has(entry.turnId)) completed.delete(id);
83
+ receiveState(snapshot);
84
+ }).catch(error => {
85
+ if (current()) onError(error);
86
+ }).finally(() => {
87
+ pendingReads.delete(pendingRead);
88
+ if (reloadInFlight !== job) return;
89
+ reloadInFlight = null;
90
+ if (reloadQueued && current()) {
91
+ reloadQueued = false;
92
+ reload();
93
+ }
94
+ });
95
+ reloadInFlight = job;
96
+ return job;
97
+ }
98
+
99
+ function deliver(payload) {
100
+ if (payload.streamEpoch !== epoch || !Number.isSafeInteger(payload.streamRevision) || payload.streamRevision <= revision) return;
101
+ revision = payload.streamRevision;
102
+ const event = payload.event;
103
+ let changedPresentation = Object.hasOwn(event, "interimReply");
104
+ if (changedPresentation && state) state = { ...state, interimReply: event.interimReply };
105
+ if (event.type === "transcript") {
106
+ const next = applyConversationLogPatch(state, event.patch, { limit: state?.pagination?.limit });
107
+ if (next) {
108
+ state = next;
109
+ for (const read of pendingReads) {
110
+ if (read.generation === generation) read.patches.push(event.patch);
111
+ }
112
+ receiveStreaming(event.streaming);
113
+ changedPresentation = true;
114
+ }
115
+ } else if (event.type === "message" || event.type === "message-complete") {
116
+ // A later initial read can already contain the result of buffered events.
117
+ const older = streaming && event.streaming?.revision < streaming.revision;
118
+ const completedWhileRunning = event.type === "message" && event.status === "complete" &&
119
+ state?.conversationLog.some(turn => turn.turnId === event.turnId && turn.metadata?.runtime?.status === "running");
120
+ if (!older || completedWhileRunning) {
121
+ const previous = streaming?.messages?.find(message => message.messageId === event.messageId);
122
+ const changed = receiveStreaming(event.streaming);
123
+ if (event.type === "message" && event.status === "complete" && event.text) {
124
+ completed.set(event.messageId, { turnId: event.turnId, message: {
125
+ messageId: event.messageId, role: event.role, text: event.text, status: "complete",
126
+ ...(event.outputId ? { outputId: event.outputId } : {}),
127
+ ...(event.turnId ? { turnId: event.turnId } : {}),
128
+ ...(event.origin ? { origin: event.origin } : {}),
129
+ ...(previous?.at ? { at: previous.at } : {})
130
+ } });
131
+ }
132
+ changedPresentation ||= changed || event.status === "complete";
133
+ }
134
+ } else if (SNAPSHOT_EVENTS.has(event.type)) {
135
+ if (event.type === "settled") settledTurns.add(event.turnId);
136
+ if (event.type === "replaced") completed.clear();
137
+ reload();
138
+ }
139
+ if (changedPresentation) publishState();
140
+ // Events are notifications. Transcript presentation comes from onState.turns.
141
+ onEvent(event);
142
+ }
143
+ function receive(payload) {
144
+ if (disposed || !listening || payload?.subscriptionId !== subscriptionId || payload.conversationId !== conversationId) return;
145
+ if (epoch === null) pending.push(payload);
146
+ else deliver(payload);
147
+ }
148
+ function subscribe() {
149
+ if (disposed || !socket.connected) return;
150
+ const attempt = ++generation;
151
+ epoch = null;
152
+ revision = 0;
153
+ pending = [];
154
+ listening = true;
155
+ state = null;
156
+ streaming = null;
157
+ completed.clear();
158
+ settledTurns.clear();
159
+ reloadInFlight = null;
160
+ reloadQueued = false;
161
+ socket.timeout(10_000).emit(ASSISTANT_CONVERSATION_SUBSCRIBE, input, (error, response) => {
162
+ if (disposed || attempt !== generation) return;
163
+ if (error || response?.ok !== true) {
164
+ listening = false;
165
+ pending = [];
166
+ if (socket.connected) socket.emit(ASSISTANT_CONVERSATION_UNSUBSCRIBE, { subscriptionId });
167
+ onError(error || Object.assign(new Error(response?.error || "Conversation subscription failed."), {
168
+ code: response?.code, statusCode: response?.status
169
+ }));
170
+ return;
171
+ }
172
+ epoch = response.streamEpoch;
173
+ receiveState(response.state, { initial: true });
174
+ for (const payload of pending.sort((left, right) => left.streamRevision - right.streamRevision)) deliver(payload);
175
+ pending = [];
176
+ });
177
+ }
178
+ function disconnected() {
179
+ generation += 1;
180
+ epoch = null;
181
+ pending = [];
182
+ listening = false;
183
+ reloadInFlight = null;
184
+ reloadQueued = false;
185
+ }
186
+ socket.on(ASSISTANT_CONVERSATION_EVENT, receive);
187
+ socket.on("connect", subscribe);
188
+ socket.on("disconnect", disconnected);
189
+ subscribe();
190
+
191
+ function release() {
192
+ if (disposed) return;
193
+ disposed = true;
194
+ generation += 1;
195
+ pending = [];
196
+ completed.clear();
197
+ socket.off(ASSISTANT_CONVERSATION_EVENT, receive);
198
+ socket.off("connect", subscribe);
199
+ socket.off("disconnect", disconnected);
200
+ if (socket.connected) socket.emit(ASSISTANT_CONVERSATION_UNSUBSCRIBE, { subscriptionId });
201
+ }
202
+ return Object.assign(release, { reload });
203
+ }
204
+
205
+ export { subscribeAssistantConversation };
@@ -1,119 +1,26 @@
1
1
  import { defineFeature } from "@jskit-ai/kernel/server/features";
2
- import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
- import { AppError } from "@jskit-ai/kernel/server/runtime";
4
- import { createAiClient, createAiConnectionClient } from "@jskit-ai/assistant-core/server";
2
+ import { actionIds } from "./actionIds.js";
5
3
  import { createAssistantActions } from "./actions.js";
6
4
  import { registerRoutes } from "./registerRoutes.js";
7
- import { createRepository as createAssistantConfigRepository } from "./repositories/assistantConfigRepository.js";
8
- import { createRepository as createConversationsRepository } from "./repositories/conversationsRepository.js";
9
- import { createRepository as createMessagesRepository } from "./repositories/messagesRepository.js";
10
- import { createRepository as createTurnRequestsRepository } from "./repositories/turnRequestsRepository.js";
11
- import { createService as createAssistantConfigService } from "./services/assistantConfigService.js";
12
- import { createChatService } from "./services/chatService.js";
13
- import { createTranscriptService } from "./services/transcriptService.js";
14
- import { resolveAssistantAiConfig, resolveAssistantServerConfig } from "./support/assistantServerConfig.js";
15
- import { createSurfaceAwareToolCatalog } from "./support/createSurfaceAwareToolCatalog.js";
5
+ import { registerConversationSubscriptions } from "./registerConversationSubscriptions.js";
16
6
 
17
- function createAssistantAiClientFactory(config = {}) {
18
- const appConfig = normalizeObject(config.appConfig);
19
- const env = normalizeObject(config.env);
20
- const cache = new Map();
21
-
22
- return Object.freeze({
23
- async resolveClient(targetSurfaceId = "", { context, integrationId } = {}) {
24
- const normalizedTargetSurfaceId = normalizeText(targetSurfaceId).toLowerCase();
25
- if (!normalizedTargetSurfaceId) {
26
- throw new Error("assistant.ai.client.factory.resolveClient requires targetSurfaceId.");
27
- }
28
-
29
- const surfaceConfig = resolveAssistantServerConfig(appConfig, normalizedTargetSurfaceId);
30
- if (surfaceConfig.aiIntegrationId) {
31
- const selected = integrationId || surfaceConfig.aiIntegrationId;
32
- if (![surfaceConfig.aiIntegrationId, ...surfaceConfig.aiIntegrationIds].includes(selected)) {
33
- throw new AppError(403, "This AI connection is not available in this assistant.");
34
- }
35
- if (!config.aiConnections?.resolve) throw new Error("The assistant requires an authorized AI connection resolver.");
36
- const connection = await config.aiConnections.resolve({ context, integrationId: selected });
37
- return createAiConnectionClient(connection, { timeoutMs: surfaceConfig.timeoutMs });
38
- }
39
- if (integrationId) throw new AppError(403, "This assistant does not allow selecting an AI connection.");
40
-
41
- if (cache.has(normalizedTargetSurfaceId)) {
42
- return cache.get(normalizedTargetSurfaceId);
43
- }
44
-
45
- const assistantAiConfig = resolveAssistantAiConfig(
46
- {
47
- appConfig,
48
- env
49
- },
50
- normalizedTargetSurfaceId
51
- );
52
- const client = createAiClient(assistantAiConfig.ai);
53
- cache.set(normalizedTargetSurfaceId, client);
54
- return client;
55
- }
56
- });
57
- }
58
-
59
- function createAssistantRuntime({
60
- actionCatalogue,
61
- config,
62
- consoleRuntime,
63
- database,
64
- env,
65
- aiConnections,
66
- attachments,
67
- workspaces
68
- } = {}) {
69
- const assistantConfigRepository = createAssistantConfigRepository(database.knex);
70
- const conversationsRepository = createConversationsRepository(database.knex);
71
- const messagesRepository = createMessagesRepository(database.knex);
72
- const turnRequests = createTurnRequestsRepository(database.knex);
73
- const workspaceScopeSupport = workspaces?.scope || null;
74
- const aiClientFactory = createAssistantAiClientFactory({ appConfig: config, env, aiConnections });
75
- const toolCatalog = createSurfaceAwareToolCatalog(actionCatalogue, { appConfig: config });
76
- const configService = createAssistantConfigService({
77
- assistantConfigRepository,
78
- consoleService: consoleRuntime?.services?.access || null,
79
- appConfig: config,
80
- workspaceScopeSupport
81
- });
82
- const transcriptService = createTranscriptService({ conversationsRepository, messagesRepository });
83
- const chatService = createChatService({
84
- turnRequests,
85
- attachments,
86
- aiClientFactory,
87
- transcriptService,
88
- serviceToolCatalog: toolCatalog,
89
- assistantConfigService: configService,
90
- appConfig: config,
91
- workspaceScopeSupport
92
- });
93
-
94
- return Object.freeze({
95
- repositories: Object.freeze({
96
- config: assistantConfigRepository,
97
- conversations: conversationsRepository,
98
- messages: messagesRepository,
99
- turnRequests
100
- }),
101
- services: Object.freeze({ chat: chatService, config: configService, transcript: transcriptService }),
102
- aiClientFactory,
103
- toolCatalog
104
- });
105
- }
7
+ const subscriptionLifecycle = new WeakMap();
106
8
 
107
9
  const AssistantFeature = defineFeature({
108
10
  id: "assistant.runtime",
109
11
  domain: "assistant",
110
12
  requires: {
111
13
  config: "runtime.config",
112
- database: "runtime.database",
113
14
  env: "runtime.env",
114
15
  http: "runtime.http"
115
16
  },
116
17
  optional: {
18
+ conversationRuntime: "assistant.conversations",
19
+ conversationAccess: "assistant.conversation.access",
20
+ assistantConfigService: "assistant.settings",
21
+ database: "runtime.database",
22
+ realtime: "runtime.realtime",
23
+ events: "runtime.events",
117
24
  aiConnections: "integrations.ai",
118
25
  attachments: "assistant.attachments",
119
26
  consoleRuntime: "console.core",
@@ -122,21 +29,90 @@ const AssistantFeature = defineFeature({
122
29
  provides: {
123
30
  assistant: "assistant.runtime"
124
31
  },
125
- setup(dependencies, { actionCatalogue }) {
126
- const assistant = createAssistantRuntime({ ...dependencies, actionCatalogue });
32
+ async setup(dependencies, { actionCatalogue }) {
33
+ const { conversationRuntime, conversationAccess, assistantConfigService } = dependencies;
34
+ if (conversationAccess != null) {
35
+ if (!conversationRuntime || typeof conversationAccess.router?.register !== "function" ||
36
+ typeof conversationAccess.wrapAction !== "function") {
37
+ throw new TypeError("assistant.conversation.access requires assistant.conversations, router.register() and wrapAction().");
38
+ }
39
+ if (!["authenticated", "host"].includes(conversationAccess.requestPolicy) ||
40
+ typeof conversationAccess.subscribeActionId !== "string" || !conversationAccess.subscribeActionId.trim() ||
41
+ (conversationAccess.requestPolicy === "host" && conversationAccess.subscribeActionId === actionIds.conversationSubscribe)) {
42
+ throw new TypeError("assistant.conversation.access requires a fixed subscribeActionId and authenticated or host requestPolicy.");
43
+ }
44
+ }
45
+ let assistant;
46
+ if (conversationRuntime) {
47
+ if (typeof conversationRuntime.open !== "function") {
48
+ throw new TypeError("assistant.conversations requires createConversationRuntime().");
49
+ }
50
+ assistant = Object.freeze({ conversationRuntime,
51
+ services: Object.freeze({ conversations: conversationRuntime, config: assistantConfigService || null }) });
52
+ } else {
53
+ if (!dependencies.database?.knex) {
54
+ throw new Error("AssistantFeature requires assistant.conversations, or runtime.database for database integration.");
55
+ }
56
+ const { createAssistantRuntime } = await import("./createAssistantRuntime.js");
57
+ const { createRepository: createAssistantConfigRepository } = await import("./repositories/assistantConfigRepository.js");
58
+ const { createRepository: createConversationsRepository } = await import("./repositories/conversationsRepository.js");
59
+ const { createRepository: createMessagesRepository } = await import("./repositories/messagesRepository.js");
60
+ const { createRepository: createTurnRequestsRepository } = await import("./repositories/turnRequestsRepository.js");
61
+ assistant = createAssistantRuntime({
62
+ ...dependencies,
63
+ actionCatalogue,
64
+ repositories: {
65
+ config: createAssistantConfigRepository(dependencies.database.knex),
66
+ conversations: createConversationsRepository(dependencies.database.knex),
67
+ messages: createMessagesRepository(dependencies.database.knex),
68
+ turnRequests: createTurnRequestsRepository(dependencies.database.knex)
69
+ }
70
+ });
71
+ }
127
72
  registerRoutes(dependencies.http.router, {
128
73
  config: dependencies.config,
74
+ conversationRuntime,
75
+ conversationAccess,
76
+ conversationDataSchema: conversationRuntime?.conversationDataSchema ?? null,
77
+ conversationConfigurationSchema: conversationRuntime?.conversationConfigurationSchema ?? null,
78
+ conversationSelectionSchema: conversationRuntime?.conversationSelectionSchema ?? null,
79
+ conversationReplacementSchema: conversationRuntime?.conversationReplacementSchema ?? null,
80
+ assistantConfigService,
129
81
  workspaceScopeSupport: dependencies.workspaces?.scope || null
130
82
  });
83
+ subscriptionLifecycle.set(assistant, { actions: actionCatalogue });
131
84
  return { assistant };
132
85
  },
133
- actions({ assistant, config }) {
86
+ actions({ assistant, conversationAccess, config, workspaces }) {
134
87
  return createAssistantActions({
135
88
  assistantConfigService: assistant.services.config,
136
89
  chatService: assistant.services.chat,
90
+ conversationRuntime: assistant.conversationRuntime,
91
+ conversationAccess,
92
+ conversationDataSchema: assistant.conversationRuntime?.conversationDataSchema ?? null,
93
+ conversationConfigurationSchema: assistant.conversationRuntime?.conversationConfigurationSchema ?? null,
94
+ conversationSelectionSchema: assistant.conversationRuntime?.conversationSelectionSchema ?? null,
95
+ conversationReplacementSchema: assistant.conversationRuntime?.conversationReplacementSchema ?? null,
96
+ workspaceScopeSupport: workspaces?.scope || null,
137
97
  config
138
98
  });
99
+ },
100
+ boot({ conversationRuntime, conversationAccess, realtime, events, config, workspaces }, { outputs }) {
101
+ if (!conversationRuntime || !realtime) return;
102
+ const state = subscriptionLifecycle.get(outputs.assistant);
103
+ state.release = registerConversationSubscriptions({
104
+ realtime, events, actions: state.actions, config, workspaceScopeSupport: workspaces?.scope || null,
105
+ ...(conversationAccess ? {
106
+ subscribeActionId: conversationAccess.subscribeActionId,
107
+ requestPolicy: conversationAccess.requestPolicy
108
+ } : {})
109
+ });
110
+ },
111
+ async shutdown(_dependencies, { outputs }) {
112
+ subscriptionLifecycle.get(outputs.assistant)?.release?.();
113
+ subscriptionLifecycle.delete(outputs.assistant);
114
+ await outputs.assistant.services.chat?.close();
139
115
  }
140
116
  });
141
117
 
142
- export { AssistantFeature, createAssistantAiClientFactory, createAssistantRuntime };
118
+ export { AssistantFeature };
@@ -1,5 +1,15 @@
1
1
  const actionIds = Object.freeze({
2
2
  chatStream: "assistant.chat.stream",
3
+ conversationRead: "assistant.conversation.read",
4
+ conversationSend: "assistant.conversation.send",
5
+ conversationCancel: "assistant.conversation.cancel",
6
+ conversationSubscribe: "assistant.conversation.subscribe",
7
+ conversationInspectDelivery: "assistant.conversation.delivery.inspect",
8
+ conversationGoalRead: "assistant.conversation.goal.read",
9
+ conversationGoalUpdate: "assistant.conversation.goal.update",
10
+ conversationConfigure: "assistant.conversation.configure",
11
+ conversationSelect: "assistant.conversation.select",
12
+ conversationReplace: "assistant.conversation.replace",
3
13
  conversationsList: "assistant.conversations.list",
4
14
  conversationMessagesList: "assistant.conversation.messages.list",
5
15
  settingsRead: "assistant.settings.read",