@jskit-ai/assistant-runtime 0.1.192 → 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
@@ -6,12 +6,23 @@ import {
6
6
  deepFreeze
7
7
  } from "@jskit-ai/kernel/shared/support/deepFreeze";
8
8
  import { returnJsonApiData } from "@jskit-ai/http-runtime/shared";
9
+ import { AppError } from "@jskit-ai/kernel/server/runtime";
9
10
  import {
10
- assistantConfigResource,
11
11
  assistantResource
12
12
  } from "@jskit-ai/assistant-core/shared";
13
13
  import { actionIds } from "./actionIds.js";
14
- import { assistantTargetSurfaceInputValidator } from "./inputSchemas.js";
14
+ import { createAssistantSettingsActions } from "./createAssistantSettingsActions.js";
15
+ import {
16
+ assistantConversationIdInputValidator,
17
+ assistantConversationGoalBodyValidator,
18
+ assistantConversationMessageIdInputValidator,
19
+ assistantConversationReadQueryValidator,
20
+ conversationConfigureBodyValidator,
21
+ conversationSelectBodyValidator,
22
+ conversationReplaceBodyValidator,
23
+ conversationSendBodyValidator,
24
+ assistantTargetSurfaceInputValidator
25
+ } from "./inputSchemas.js";
15
26
  import {
16
27
  resolveAssistantSurfaceConfig,
17
28
  resolveAssistantSurfacesConfig
@@ -70,35 +81,266 @@ const runtimeChatStreamInputValidator = composeSchemaDefinitions(
70
81
  }
71
82
  );
72
83
 
73
- const settingsReadInputValidator = assistantTargetSurfaceInputValidator;
74
-
75
- const settingsUpdatePatchInputValidator = deepFreeze({
76
- schema: createSchema({
77
- patch: {
78
- type: "object",
79
- required: true,
80
- schema: assistantConfigResource.operations.patch.body.schema
81
- }
82
- }),
83
- mode: "patch"
84
- });
85
-
86
- const settingsUpdateInputValidator = composeSchemaDefinitions(
87
- [assistantTargetSurfaceInputValidator, settingsUpdatePatchInputValidator],
88
- {
89
- mode: "patch",
90
- context: "assistant-runtime settings update action input"
91
- }
92
- );
93
-
94
84
  function configuredAssistantSurfaces(config = {}) {
95
85
  return Object.keys(resolveAssistantSurfacesConfig(config))
96
86
  .map((surfaceId) => resolveAssistantSurfaceConfig(config, surfaceId))
97
87
  .filter(Boolean);
98
88
  }
99
89
 
100
- function createAssistantActions({ assistantConfigService, chatService, config = {} } = {}) {
101
- if (!assistantConfigService || !chatService) {
90
+ const conversationInputValidator = composeSchemaDefinitions(
91
+ [assistantTargetSurfaceInputValidator, assistantConversationIdInputValidator],
92
+ { mode: "patch", context: "assistant-runtime conversation action input" }
93
+ );
94
+
95
+ const conversationInspectInputValidator = composeSchemaDefinitions(
96
+ [conversationInputValidator, assistantConversationMessageIdInputValidator],
97
+ { mode: "patch", context: "assistant-runtime conversation delivery inspection input" }
98
+ );
99
+
100
+ const conversationReadInputValidator = composeSchemaDefinitions(
101
+ [conversationInputValidator, assistantConversationReadQueryValidator],
102
+ { mode: "patch", context: "assistant-runtime conversation read action input" }
103
+ );
104
+
105
+ function createConversationActions({ conversationRuntime, conversationDataSchema, conversationConfigurationSchema,
106
+ conversationSelectionSchema, conversationReplacementSchema, config, runtimeSurfaces, workspaceScopeSupport }) {
107
+ const conversationSendInputValidator = composeSchemaDefinitions(
108
+ [conversationInputValidator, conversationSendBodyValidator(conversationDataSchema)],
109
+ { mode: "patch", context: "assistant-runtime conversation send action input" }
110
+ );
111
+ async function openConversation(input, context) {
112
+ const assistantSurface = resolveAssistantSurfaceConfig(config, input.targetSurfaceId);
113
+ if (!assistantSurface) throw new AppError(404, "Assistant not found.");
114
+ let conversationContext = context;
115
+ if (assistantSurface.runtimeSurfaceRequiresWorkspace) {
116
+ if (typeof workspaceScopeSupport?.resolveWorkspace !== "function") {
117
+ throw new Error("Assistant conversation actions require workspace server scope support.");
118
+ }
119
+ const workspace = workspaceScopeSupport.resolveWorkspace(context, { workspaceSlug: input.workspaceSlug });
120
+ if (!workspace?.id) throw new AppError(409, "Workspace selection required.");
121
+ conversationContext = { ...context, workspace };
122
+ }
123
+ return conversationRuntime.open({ id: input.conversationId, context: conversationContext });
124
+ }
125
+
126
+ return [
127
+ {
128
+ id: actionIds.conversationRead,
129
+ version: 1,
130
+ kind: "query",
131
+ channels: ["api", "internal"],
132
+ surfaces: runtimeSurfaces,
133
+ permission: { require: "authenticated" },
134
+ input: conversationReadInputValidator,
135
+ output: null,
136
+ idempotency: "none",
137
+ audit: { actionName: actionIds.conversationRead },
138
+ observability: {},
139
+ async execute(input, context) {
140
+ const conversation = await openConversation(input, context);
141
+ const options = {};
142
+ for (const key of ["beforeTurnId", "limit"]) {
143
+ if (Object.hasOwn(input, key)) options[key] = input[key];
144
+ }
145
+ return Object.keys(options).length ? conversation.read(options) : conversation.read();
146
+ }
147
+ },
148
+ {
149
+ id: actionIds.conversationSend,
150
+ version: 1,
151
+ kind: "command",
152
+ channels: ["api", "internal"],
153
+ surfaces: runtimeSurfaces,
154
+ permission: { require: "authenticated" },
155
+ input: conversationSendInputValidator,
156
+ output: null,
157
+ idempotency: "none",
158
+ audit: { actionName: actionIds.conversationSend },
159
+ observability: {},
160
+ async execute(input, context) {
161
+ const conversation = await openConversation(input, context);
162
+ return conversation.send({
163
+ messageId: input.messageId,
164
+ text: input.text,
165
+ ...(conversationDataSchema && Object.hasOwn(input, "data") ? { data: input.data } : {}),
166
+ ...(Object.hasOwn(input, "attachmentIds") ? { attachmentIds: input.attachmentIds } : {}),
167
+ ...(Object.hasOwn(input, "steer") ? { steer: input.steer } : {})
168
+ });
169
+ }
170
+ },
171
+ {
172
+ id: actionIds.conversationCancel,
173
+ version: 1,
174
+ kind: "command",
175
+ channels: ["api", "internal"],
176
+ surfaces: runtimeSurfaces,
177
+ permission: { require: "authenticated" },
178
+ input: conversationInputValidator,
179
+ output: null,
180
+ idempotency: "none",
181
+ audit: { actionName: actionIds.conversationCancel },
182
+ observability: {},
183
+ async execute(input, context) {
184
+ const conversation = await openConversation(input, context);
185
+ return conversation.cancel();
186
+ }
187
+ },
188
+ {
189
+ id: actionIds.conversationSubscribe,
190
+ version: 1,
191
+ kind: "query",
192
+ channels: ["internal"],
193
+ surfaces: runtimeSurfaces,
194
+ permission: { require: "authenticated" },
195
+ input: conversationInputValidator,
196
+ output: null,
197
+ idempotency: "none",
198
+ audit: { actionName: actionIds.conversationSubscribe },
199
+ observability: {},
200
+ async execute(input, context, deps) {
201
+ if (typeof deps?.onEvent !== "function" || typeof deps?.onRelease !== "function") {
202
+ throw new TypeError("Conversation subscriptions require their server-owned event and cleanup callbacks.");
203
+ }
204
+ const conversation = await openConversation(input, context);
205
+ const release = await conversation.subscribe(deps.onEvent);
206
+ try {
207
+ deps.onRelease(release);
208
+ return await conversation.read();
209
+ } catch (error) {
210
+ release();
211
+ throw error;
212
+ }
213
+ }
214
+ },
215
+ {
216
+ id: actionIds.conversationInspectDelivery,
217
+ version: 1,
218
+ kind: "command",
219
+ channels: ["api", "internal"],
220
+ surfaces: runtimeSurfaces,
221
+ permission: { require: "authenticated" },
222
+ input: conversationInspectInputValidator,
223
+ output: null,
224
+ idempotency: "none",
225
+ audit: { actionName: actionIds.conversationInspectDelivery },
226
+ observability: {},
227
+ async execute(input, context) {
228
+ const conversation = await openConversation(input, context);
229
+ return conversation.inspectDelivery({ messageId: input.messageId });
230
+ }
231
+ },
232
+ {
233
+ id: actionIds.conversationGoalRead,
234
+ version: 1,
235
+ kind: "query",
236
+ channels: ["api", "internal"],
237
+ surfaces: runtimeSurfaces,
238
+ permission: { require: "authenticated" },
239
+ input: conversationInputValidator,
240
+ output: null,
241
+ idempotency: "none",
242
+ audit: { actionName: actionIds.conversationGoalRead },
243
+ observability: {},
244
+ async execute(input, context) {
245
+ return (await openConversation(input, context)).readGoal();
246
+ }
247
+ },
248
+ {
249
+ id: actionIds.conversationGoalUpdate,
250
+ version: 1,
251
+ kind: "command",
252
+ channels: ["api", "internal"],
253
+ surfaces: runtimeSurfaces,
254
+ permission: { require: "authenticated" },
255
+ input: composeSchemaDefinitions([conversationInputValidator, assistantConversationGoalBodyValidator], {
256
+ mode: "replace", context: "assistant-runtime conversation goal action input"
257
+ }),
258
+ output: null,
259
+ idempotency: "none",
260
+ audit: { actionName: actionIds.conversationGoalUpdate },
261
+ observability: {},
262
+ async execute(input, context) {
263
+ const conversation = await openConversation(input, context);
264
+ const goal = { action: input.action, expectedSegmentId: input.expectedSegmentId };
265
+ for (const key of ["expectedGoalId", "messageId", "objective", "tokenBudget", "attachmentIds"]) {
266
+ if (Object.hasOwn(input, key)) goal[key] = input[key];
267
+ }
268
+ return conversation.updateGoal(goal);
269
+ }
270
+ },
271
+ ...(conversationConfigurationSchema ? [{
272
+ id: actionIds.conversationConfigure,
273
+ version: 1,
274
+ kind: "command",
275
+ channels: ["api", "internal"],
276
+ surfaces: runtimeSurfaces,
277
+ permission: { require: "authenticated" },
278
+ input: composeSchemaDefinitions([conversationInputValidator, conversationConfigureBodyValidator(conversationConfigurationSchema)], {
279
+ mode: "replace", context: "assistant-runtime declared conversation configuration"
280
+ }),
281
+ output: null,
282
+ idempotency: "none",
283
+ audit: { actionName: actionIds.conversationConfigure },
284
+ observability: {},
285
+ async execute(input, context) {
286
+ return (await openConversation(input, context)).configure(input.configuration);
287
+ }
288
+ }] : []),
289
+ ...(conversationSelectionSchema ? [{
290
+ id: actionIds.conversationSelect,
291
+ version: 1,
292
+ kind: "command",
293
+ channels: ["api", "internal"],
294
+ surfaces: runtimeSurfaces,
295
+ permission: { require: "authenticated" },
296
+ input: composeSchemaDefinitions([conversationInputValidator, conversationSelectBodyValidator(conversationSelectionSchema)], {
297
+ mode: "replace", context: "assistant-runtime declared conversation selection"
298
+ }),
299
+ output: null,
300
+ idempotency: "none",
301
+ audit: { actionName: actionIds.conversationSelect },
302
+ observability: {},
303
+ async execute(input, context) {
304
+ return (await openConversation(input, context)).select(input.selection);
305
+ }
306
+ }] : []),
307
+ ...(conversationReplacementSchema ? [{
308
+ id: actionIds.conversationReplace,
309
+ version: 1,
310
+ kind: "command",
311
+ channels: ["api", "internal"],
312
+ surfaces: runtimeSurfaces,
313
+ permission: { require: "authenticated" },
314
+ input: composeSchemaDefinitions([conversationInputValidator, conversationReplaceBodyValidator(conversationReplacementSchema)], {
315
+ mode: "replace", context: "assistant-runtime declared conversation replacement"
316
+ }),
317
+ output: null,
318
+ idempotency: "none",
319
+ audit: { actionName: actionIds.conversationReplace },
320
+ observability: {},
321
+ async execute(input, context) {
322
+ return (await openConversation(input, context)).replace(input.replacement);
323
+ }
324
+ }] : [])
325
+ ];
326
+ }
327
+
328
+ function createAssistantActions({
329
+ assistantConfigService,
330
+ chatService,
331
+ conversationRuntime = null,
332
+ conversationAccess = null,
333
+ conversationDataSchema = null,
334
+ conversationConfigurationSchema = null,
335
+ conversationSelectionSchema = null,
336
+ conversationReplacementSchema = null,
337
+ workspaceScopeSupport = null,
338
+ config = {}
339
+ } = {}) {
340
+ if (conversationRuntime && typeof conversationRuntime.open !== "function") {
341
+ throw new TypeError("Assistant conversation actions require conversationRuntime.open().");
342
+ }
343
+ if (!conversationRuntime && (!assistantConfigService || !chatService)) {
102
344
  throw new TypeError("createAssistantActions requires assistantConfigService and chatService.");
103
345
  }
104
346
  const configuredSurfaces = configuredAssistantSurfaces(config);
@@ -106,6 +348,25 @@ function createAssistantActions({ assistantConfigService, chatService, config =
106
348
  const settingsSurfaces = [...new Set(configuredSurfaces.map((entry) => entry.settingsSurfaceId))];
107
349
  if (runtimeSurfaces.length === 0 || settingsSurfaces.length === 0) return [];
108
350
 
351
+ if (conversationRuntime) {
352
+ const conversationActions = createConversationActions({
353
+ conversationRuntime, conversationDataSchema, conversationConfigurationSchema, conversationSelectionSchema, conversationReplacementSchema,
354
+ config, runtimeSurfaces, workspaceScopeSupport
355
+ });
356
+ return Object.freeze([
357
+ ...conversationActions.map((definition) => {
358
+ if (!conversationAccess) return definition;
359
+ const selected = definition.id === actionIds.conversationSubscribe ? {
360
+ ...definition,
361
+ id: conversationAccess.subscribeActionId,
362
+ audit: { ...definition.audit, actionName: conversationAccess.subscribeActionId }
363
+ } : definition;
364
+ return conversationAccess.wrapAction(selected);
365
+ }),
366
+ ...(assistantConfigService ? createAssistantSettingsActions({ assistantConfigService, settingsSurfaces }) : [])
367
+ ]);
368
+ }
369
+
109
370
  return Object.freeze([
110
371
  {
111
372
  id: actionIds.chatStream,
@@ -176,60 +437,7 @@ function createAssistantActions({ assistantConfigService, chatService, config =
176
437
  }));
177
438
  }
178
439
  },
179
- {
180
- id: actionIds.settingsRead,
181
- version: 1,
182
- kind: "query",
183
- channels: ["api", "automation", "internal"],
184
- surfaces: settingsSurfaces,
185
- permission: {
186
- require: "authenticated"
187
- },
188
- input: settingsReadInputValidator,
189
- output: null,
190
- extensions: {
191
- assistant: {
192
- exclude: "Assistant self-configuration is disabled; use the authenticated assistant settings screen."
193
- }
194
- },
195
- idempotency: "none",
196
- audit: {
197
- actionName: actionIds.settingsRead
198
- },
199
- observability: {},
200
- async execute(input, context) {
201
- return returnJsonApiData(await assistantConfigService.getSettings(input, {
202
- context
203
- }));
204
- }
205
- },
206
- {
207
- id: actionIds.settingsUpdate,
208
- version: 1,
209
- kind: "command",
210
- channels: ["api", "automation", "internal"],
211
- surfaces: settingsSurfaces,
212
- permission: {
213
- require: "authenticated"
214
- },
215
- input: settingsUpdateInputValidator,
216
- output: null,
217
- extensions: {
218
- assistant: {
219
- exclude: "Assistant self-configuration is disabled; use the authenticated assistant settings screen."
220
- }
221
- },
222
- idempotency: "optional",
223
- audit: {
224
- actionName: actionIds.settingsUpdate
225
- },
226
- observability: {},
227
- async execute(input, context) {
228
- return returnJsonApiData(await assistantConfigService.updateSettings(input, input.patch, {
229
- context
230
- }));
231
- }
232
- }
440
+ ...createAssistantSettingsActions({ assistantConfigService, settingsSurfaces })
233
441
  ]);
234
442
  }
235
443
 
@@ -0,0 +1,138 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { normalizeObject, normalizeRecordId, 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";
5
+ import { createConversationRuntime } from "@jskit-ai/assistant-core/server/conversation";
6
+ import { createSqlConversationStorage } from "./sqlConversationStorage.js";
7
+ import { createService as createAssistantConfigService } from "./services/assistantConfigService.js";
8
+ import { createChatService } from "./services/chatService.js";
9
+ import { createTranscriptService } from "./services/transcriptService.js";
10
+ import { resolveAssistantAiConfig, resolveAssistantServerConfig } from "./support/assistantServerConfig.js";
11
+ import { createSurfaceAwareToolCatalog } from "./support/createSurfaceAwareToolCatalog.js";
12
+
13
+ function createAssistantAiClientFactory(config = {}) {
14
+ const appConfig = normalizeObject(config.appConfig);
15
+ const env = normalizeObject(config.env);
16
+ const cache = new Map();
17
+
18
+ return Object.freeze({
19
+ async resolveClient(targetSurfaceId = "", { context, integrationId } = {}) {
20
+ const normalizedTargetSurfaceId = normalizeText(targetSurfaceId).toLowerCase();
21
+ if (!normalizedTargetSurfaceId) {
22
+ throw new Error("assistant.ai.client.factory.resolveClient requires targetSurfaceId.");
23
+ }
24
+
25
+ const surfaceConfig = resolveAssistantServerConfig(appConfig, normalizedTargetSurfaceId);
26
+ if (surfaceConfig.aiIntegrationId) {
27
+ const selected = integrationId || surfaceConfig.aiIntegrationId;
28
+ if (![surfaceConfig.aiIntegrationId, ...surfaceConfig.aiIntegrationIds].includes(selected)) {
29
+ throw new AppError(403, "This AI connection is not available in this assistant.");
30
+ }
31
+ if (!config.aiConnections?.resolve) throw new Error("The assistant requires an authorized AI connection resolver.");
32
+ const connection = await config.aiConnections.resolve({ context, integrationId: selected });
33
+ return createAiConnectionClient(connection, { timeoutMs: surfaceConfig.timeoutMs });
34
+ }
35
+ if (integrationId) throw new AppError(403, "This assistant does not allow selecting an AI connection.");
36
+
37
+ if (cache.has(normalizedTargetSurfaceId)) {
38
+ return cache.get(normalizedTargetSurfaceId);
39
+ }
40
+
41
+ const assistantAiConfig = resolveAssistantAiConfig(
42
+ {
43
+ appConfig,
44
+ env
45
+ },
46
+ normalizedTargetSurfaceId
47
+ );
48
+ const client = createAiClient(assistantAiConfig.ai);
49
+ cache.set(normalizedTargetSurfaceId, client);
50
+ return client;
51
+ }
52
+ });
53
+ }
54
+
55
+ function createAssistantRuntime({
56
+ actionCatalogue,
57
+ config,
58
+ consoleRuntime,
59
+ repositories = {},
60
+ env,
61
+ aiConnections,
62
+ attachments,
63
+ workspaces
64
+ } = {}) {
65
+ const {
66
+ config: assistantConfigRepository,
67
+ conversations: conversationsRepository,
68
+ messages: messagesRepository,
69
+ turnRequests
70
+ } = repositories;
71
+ const workspaceScopeSupport = workspaces?.scope || null;
72
+ const aiClientFactory = createAssistantAiClientFactory({ appConfig: config, env, aiConnections });
73
+ const toolCatalog = createSurfaceAwareToolCatalog(actionCatalogue, { appConfig: config });
74
+ const configService = createAssistantConfigService({
75
+ assistantConfigRepository,
76
+ consoleService: consoleRuntime?.services?.access || null,
77
+ appConfig: config,
78
+ workspaceScopeSupport
79
+ });
80
+ const transcriptService = createTranscriptService({ conversationsRepository, messagesRepository });
81
+ const conversationRuntime = createAssistantConversationRuntime({
82
+ conversationsRepository, messagesRepository, aiClientFactory, toolCatalog, attachments
83
+ });
84
+ const chatService = createChatService({
85
+ conversationRuntime,
86
+ turnRequests,
87
+ attachments,
88
+ aiClientFactory,
89
+ transcriptService,
90
+ serviceToolCatalog: toolCatalog,
91
+ assistantConfigService: configService,
92
+ appConfig: config,
93
+ workspaceScopeSupport
94
+ });
95
+
96
+ return Object.freeze({
97
+ repositories: Object.freeze({
98
+ config: assistantConfigRepository,
99
+ conversations: conversationsRepository,
100
+ messages: messagesRepository,
101
+ turnRequests
102
+ }),
103
+ services: Object.freeze({ chat: chatService, config: configService, transcript: transcriptService }),
104
+ aiClientFactory,
105
+ toolCatalog
106
+ });
107
+ }
108
+
109
+ // Internal SQL composition: the existing facade supplies the authenticated action
110
+ // context and normalized request history, never another browser context channel.
111
+ function createAssistantConversationRuntime({ conversationsRepository, messagesRepository, aiClientFactory, toolCatalog, attachments }) {
112
+ return createConversationRuntime({
113
+ storage: createSqlConversationStorage({ conversationsRepository, messagesRepository }),
114
+ async authorize({ context, conversationId }) {
115
+ const request = context?.assistantRequest;
116
+ if (!request || request.conversation.id !== conversationId) return false;
117
+ return Boolean(await conversationsRepository.findByIdForActorScope(conversationId, {
118
+ actorUserId: normalizeRecordId(context.actor?.id, { fallback: null }), surfaceId: context.surface,
119
+ workspaceId: normalizeRecordId(request.workspace?.id || request.workspace, { fallback: null })
120
+ }));
121
+ },
122
+ apiClientFactory: { preserveWhitespace: false, resolveClient: ({ context, configuration }) => aiClientFactory.resolveClient(context.surface,
123
+ { context, integrationId: configuration.integrationId }) },
124
+ apiHistory: ({ context }) => context.assistantRequest.history,
125
+ toolCatalog,
126
+ ...(attachments ? { attachments: {
127
+ resolve({ attachmentIds, context }) {
128
+ const prepared = context.assistantRequest.resolvedAttachments.shift();
129
+ if (!prepared || !isDeepStrictEqual(prepared.attachmentIds, attachmentIds)) {
130
+ throw new Error("The API attachment request differs from its authorized request context.");
131
+ }
132
+ return prepared.files;
133
+ }
134
+ } } : {})
135
+ });
136
+ }
137
+
138
+ export { createAssistantAiClientFactory, createAssistantRuntime, createAssistantConversationRuntime };
@@ -0,0 +1,89 @@
1
+ import { createSchema } from "json-rest-schema";
2
+ import { composeSchemaDefinitions } from "@jskit-ai/kernel/shared/validators";
3
+ import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
4
+ import { returnJsonApiData } from "@jskit-ai/http-runtime/shared";
5
+ import { assistantConfigResource } from "@jskit-ai/assistant-core/shared";
6
+ import { actionIds } from "./actionIds.js";
7
+ import { assistantTargetSurfaceInputValidator } from "./inputSchemas.js";
8
+
9
+ const settingsReadInputValidator = assistantTargetSurfaceInputValidator;
10
+
11
+ const settingsUpdatePatchInputValidator = deepFreeze({
12
+ schema: createSchema({
13
+ patch: {
14
+ type: "object",
15
+ required: true,
16
+ schema: assistantConfigResource.operations.patch.body.schema
17
+ }
18
+ }),
19
+ mode: "patch"
20
+ });
21
+
22
+ const settingsUpdateInputValidator = composeSchemaDefinitions(
23
+ [assistantTargetSurfaceInputValidator, settingsUpdatePatchInputValidator],
24
+ {
25
+ mode: "patch",
26
+ context: "assistant-runtime settings update action input"
27
+ }
28
+ );
29
+
30
+ function createAssistantSettingsActions({ assistantConfigService, settingsSurfaces }) {
31
+ return Object.freeze([
32
+ {
33
+ id: actionIds.settingsRead,
34
+ version: 1,
35
+ kind: "query",
36
+ channels: ["api", "automation", "internal"],
37
+ surfaces: settingsSurfaces,
38
+ permission: {
39
+ require: "authenticated"
40
+ },
41
+ input: settingsReadInputValidator,
42
+ output: null,
43
+ extensions: {
44
+ assistant: {
45
+ exclude: "Assistant self-configuration is disabled; use the authenticated assistant settings screen."
46
+ }
47
+ },
48
+ idempotency: "none",
49
+ audit: {
50
+ actionName: actionIds.settingsRead
51
+ },
52
+ observability: {},
53
+ async execute(input, context) {
54
+ return returnJsonApiData(await assistantConfigService.getSettings(input, {
55
+ context
56
+ }));
57
+ }
58
+ },
59
+ {
60
+ id: actionIds.settingsUpdate,
61
+ version: 1,
62
+ kind: "command",
63
+ channels: ["api", "automation", "internal"],
64
+ surfaces: settingsSurfaces,
65
+ permission: {
66
+ require: "authenticated"
67
+ },
68
+ input: settingsUpdateInputValidator,
69
+ output: null,
70
+ extensions: {
71
+ assistant: {
72
+ exclude: "Assistant self-configuration is disabled; use the authenticated assistant settings screen."
73
+ }
74
+ },
75
+ idempotency: "optional",
76
+ audit: {
77
+ actionName: actionIds.settingsUpdate
78
+ },
79
+ observability: {},
80
+ async execute(input, context) {
81
+ return returnJsonApiData(await assistantConfigService.updateSettings(input, input.patch, {
82
+ context
83
+ }));
84
+ }
85
+ }
86
+ ]);
87
+ }
88
+
89
+ export { createAssistantSettingsActions };
@@ -0,0 +1,6 @@
1
+ export { AssistantFeature } from "./AssistantProvider.js";
2
+ export { createAssistantActions } from "./actions.js";
3
+ export { assistantConversationGoalBodyValidator } from "./inputSchemas.js";
4
+ export { registerRoutes as registerAssistantRoutes } from "./registerRoutes.js";
5
+ export { registerConversationSubscriptions } from "./registerConversationSubscriptions.js";
6
+ export { registerFastifyConversations } from "./registerFastifyConversations.js";