@jskit-ai/assistant-runtime 0.2.9 → 0.2.11

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 (66) hide show
  1. package/README.md +1 -1
  2. package/fixtures/canonical-conversation/main.js +2 -1
  3. package/fixtures/conversation/main.js +2 -1
  4. package/fixtures/conversation/vite.config.mjs +4 -3
  5. package/migrations/assistant_config_initial.cjs +1 -5
  6. package/migrations/assistant_transcripts_initial.cjs +3 -9
  7. package/migrations/assistant_transcripts_summary.cjs +28 -0
  8. package/migrations/assistant_turn_requests.cjs +2 -1
  9. package/migrations/constraints/assistant_users_foreign_keys.cjs +22 -0
  10. package/migrations/constraints/assistant_workspaces_foreign_keys.cjs +26 -0
  11. package/package.json +17 -10
  12. package/patterns/assistant-surface/example/src/ApplicationAiFeature.js +4 -2
  13. package/src/client/components/AssistantConversationClientElement.vue +10 -4
  14. package/src/client/components/AssistantSurfaceClientElement.vue +8 -4
  15. package/src/client/composables/conversationGoal.js +266 -0
  16. package/src/client/composables/conversationHistory.js +215 -0
  17. package/src/client/composables/conversationRuntime.js +624 -0
  18. package/src/client/composables/useAssistantConversation.js +208 -884
  19. package/src/client/composables/useAssistantRuntime.js +60 -45
  20. package/src/client/providers/AssistantClientProvider.js +1 -0
  21. package/src/client/support/assistantRuntimeState.js +6 -34
  22. package/src/client/support/conversationFailures.js +30 -0
  23. package/src/client/support/conversationRestoreSupport.js +13 -5
  24. package/src/client/support/subscribeAssistantConversation.js +32 -9
  25. package/src/client/support/workspaceScopeSupport.js +13 -22
  26. package/src/client/types.js +46 -0
  27. package/src/server/AssistantProvider.js +45 -17
  28. package/src/server/actions.js +162 -201
  29. package/src/server/conversationOperations.js +141 -0
  30. package/src/server/createAssistantRuntime.js +83 -44
  31. package/src/server/createAssistantSettingsActions.js +69 -47
  32. package/src/server/hostedAssistant.js +21 -0
  33. package/src/server/index.js +1 -0
  34. package/src/server/registerConversationSubscriptions.js +34 -11
  35. package/src/server/registerFastifyConversations.js +18 -6
  36. package/src/server/registerRoutes.js +5 -2
  37. package/src/server/registerSettingsRoutes.js +1 -1
  38. package/src/server/repositories/assistantConfigRepository.js +10 -20
  39. package/src/server/repositories/conversationsRepository.js +35 -10
  40. package/src/server/repositories/messagesRepository.js +14 -10
  41. package/src/server/repositories/repositoryPersistenceUtils.js +1 -6
  42. package/src/server/repositories/turnRequestsRepository.js +6 -1
  43. package/src/server/services/assistantConfigService.js +25 -23
  44. package/src/server/services/chatService.js +57 -61
  45. package/src/server/services/transcriptService.js +15 -7
  46. package/src/server/sqlConversationStorage.js +90 -18
  47. package/src/server/support/assistantServerConfig.js +1 -1
  48. package/src/server/support/assistantStreamWriter.js +7 -1
  49. package/src/server/support/createSurfaceAwareToolCatalog.js +35 -17
  50. package/src/server/types.js +38 -0
  51. package/src/shared/assistantSurfaces.js +2 -2
  52. package/test/assistantConversationOperations.test.js +5 -5
  53. package/test/assistantRuntimeState.test.js +1 -16
  54. package/test/chatServiceLifecycle.test.js +62 -154
  55. package/test/createSurfaceAwareToolCatalog.test.js +5 -0
  56. package/test/featureRuntime.test.js +12 -6
  57. package/test/lazyAppConfig.test.js +3 -5
  58. package/test/packageMetadata.test.js +7 -1
  59. package/test/sqlConversationStorage.mysql.test.js +89 -0
  60. package/test/sqlConversationStorage.test.js +161 -16
  61. package/test/support/memoryAssistantRows.js +6 -1
  62. package/test/support/scriptedModel.js +32 -0
  63. package/test/turnRequests.test.js +4 -2
  64. package/test/usersForeignKeysMigration.test.js +46 -0
  65. package/test/workspacesForeignKeysMigration.test.js +66 -0
  66. package/src/client/support/accessDenied.js +0 -6
@@ -6,13 +6,12 @@ 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";
10
9
  import {
11
10
  assistantResource
12
11
  } from "@jskit-ai/assistant-core/shared";
13
12
  import { actionIds } from "./actionIds.js";
14
13
  import { createAssistantSettingsActions } from "./createAssistantSettingsActions.js";
15
- import { requireAssistantSurface } from "./support/assistantSurface.js";
14
+ import { createConversationOperations } from "./conversationOperations.js";
16
15
  import {
17
16
  assistantConversationIdInputValidator,
18
17
  assistantConversationGoalBodyValidator,
@@ -29,6 +28,13 @@ import {
29
28
  resolveAssistantSurfacesConfig
30
29
  } from "../shared/assistantSurfaces.js";
31
30
 
31
+ /** @typedef {import("./types.js").AssistantConfigService} AssistantConfigService */
32
+ /** @typedef {import("./types.js").ChatService} ChatService */
33
+ /** @typedef {import("./types.js").ConversationAccess} ConversationAccess */
34
+ /** @typedef {import("./types.js").ConversationRuntime} ConversationRuntime */
35
+ /** @typedef {import("./types.js").WorkspaceScopeSupport} WorkspaceScopeSupport */
36
+ /** @typedef {ReturnType<typeof createConversationOperations>} ConversationOperations */
37
+
32
38
  const runtimeConversationsListQueryInputValidator = deepFreeze({
33
39
  schema: createSchema({
34
40
  query: {
@@ -43,7 +49,7 @@ const runtimeConversationsListQueryInputValidator = deepFreeze({
43
49
  const runtimeConversationsListInputValidator = composeSchemaDefinitions(
44
50
  [assistantTargetSurfaceInputValidator, runtimeConversationsListQueryInputValidator],
45
51
  {
46
- mode: "patch",
52
+ mode: "create",
47
53
  context: "assistant-runtime conversations list action input"
48
54
  }
49
55
  );
@@ -66,7 +72,7 @@ const runtimeConversationMessagesListInputValidator = composeSchemaDefinitions(
66
72
  runtimeConversationMessagesListQueryInputValidator
67
73
  ],
68
74
  {
69
- mode: "patch",
75
+ mode: "create",
70
76
  context: "assistant-runtime conversation messages action input"
71
77
  }
72
78
  );
@@ -77,7 +83,7 @@ const runtimeChatStreamInputValidator = composeSchemaDefinitions(
77
83
  assistantResource.operations.chatStream.body
78
84
  ],
79
85
  {
80
- mode: "patch",
86
+ mode: "create",
81
87
  context: "assistant-runtime chat stream action input"
82
88
  }
83
89
  );
@@ -90,41 +96,63 @@ function configuredAssistantSurfaces(config = {}) {
90
96
 
91
97
  const conversationInputValidator = composeSchemaDefinitions(
92
98
  [assistantTargetSurfaceInputValidator, assistantConversationIdInputValidator],
93
- { mode: "patch", context: "assistant-runtime conversation action input" }
99
+ { mode: "create", context: "assistant-runtime conversation action input" }
94
100
  );
95
101
 
96
102
  const conversationDeliveryInputValidator = composeSchemaDefinitions(
97
103
  [conversationInputValidator, assistantConversationMessageIdInputValidator],
98
- { mode: "patch", context: "assistant-runtime conversation delivery inspection input" }
104
+ { mode: "create", context: "assistant-runtime conversation delivery inspection input" }
99
105
  );
100
106
 
101
107
  const conversationReadInputValidator = composeSchemaDefinitions(
102
108
  [conversationInputValidator, assistantConversationReadQueryValidator],
103
- { mode: "patch", context: "assistant-runtime conversation read action input" }
109
+ { mode: "create", context: "assistant-runtime conversation read action input" }
104
110
  );
105
111
 
106
- function createConversationActions({ conversationRuntime, conversationDataSchema, conversationConfigurationSchema,
107
- conversationSelectionSchema, conversationReplacementSchema, config, runtimeSurfaces, workspaceScopeSupport }) {
112
+ /**
113
+ * The JSON:API document of a surface's conversations.
114
+ * @param {ChatService} chatService
115
+ * @param {object} input
116
+ * @param {object} context
117
+ */
118
+ async function listConversationsDocument(chatService, input, context) {
119
+ return returnJsonApiData(await chatService.listConversations(input.query, {
120
+ context,
121
+ input
122
+ }));
123
+ }
124
+
125
+ /**
126
+ * The JSON:API document of one conversation's messages.
127
+ * @param {ChatService} chatService
128
+ * @param {object} input
129
+ * @param {object} context
130
+ */
131
+ async function listConversationMessagesDocument(chatService, input, context) {
132
+ return returnJsonApiData(await chatService.getConversationMessages(input.conversationId, input.query, {
133
+ context,
134
+ input
135
+ }));
136
+ }
137
+
138
+ /**
139
+ * @param {object} dependencies
140
+ * @param {ConversationOperations} dependencies.conversationOperations
141
+ * @param {object | null} dependencies.conversationDataSchema
142
+ * @param {object | null} dependencies.conversationConfigurationSchema
143
+ * @param {object | null} dependencies.conversationSelectionSchema
144
+ * @param {object | null} dependencies.conversationReplacementSchema
145
+ * @param {string[]} dependencies.runtimeSurfaces
146
+ */
147
+ function createConversationActions({ conversationOperations, conversationDataSchema, conversationConfigurationSchema,
148
+ conversationSelectionSchema, conversationReplacementSchema, runtimeSurfaces }) {
108
149
  const conversationSendInputValidator = composeSchemaDefinitions(
109
150
  [conversationInputValidator, conversationSendBodyValidator(conversationDataSchema)],
110
- { mode: "patch", context: "assistant-runtime conversation send action input" }
151
+ { mode: "create", context: "assistant-runtime conversation send action input" }
111
152
  );
112
- async function openConversation(input, context) {
113
- const assistantSurface = requireAssistantSurface(config, input.targetSurfaceId);
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.", { code: "assistant.runtime.workspace.selectionRequired" });
121
- conversationContext = { ...context, workspace };
122
- }
123
- return conversationRuntime.open({ id: input.conversationId, context: conversationContext });
124
- }
125
153
 
126
- return [
127
- {
154
+ return Object.freeze([
155
+ Object.freeze({
128
156
  id: actionIds.conversationRead,
129
157
  version: 1,
130
158
  kind: "query",
@@ -133,17 +161,9 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
133
161
  permission: { require: "authenticated" },
134
162
  input: conversationReadInputValidator,
135
163
  output: null,
136
- idempotency: "none",
137
- async execute(input, context) {
138
- const conversation = await openConversation(input, context);
139
- const options = {};
140
- for (const key of ["beforeTurnId", "limit"]) {
141
- if (Object.hasOwn(input, key)) options[key] = input[key];
142
- }
143
- return Object.keys(options).length ? conversation.read(options) : conversation.read();
144
- }
145
- },
146
- {
164
+ execute: (input, context) => conversationOperations.read(input, context)
165
+ }),
166
+ Object.freeze({
147
167
  id: actionIds.conversationSend,
148
168
  version: 1,
149
169
  kind: "command",
@@ -152,19 +172,9 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
152
172
  permission: { require: "authenticated" },
153
173
  input: conversationSendInputValidator,
154
174
  output: null,
155
- idempotency: "none",
156
- async execute(input, context) {
157
- const conversation = await openConversation(input, context);
158
- return conversation.send({
159
- messageId: input.messageId,
160
- text: input.text,
161
- ...(conversationDataSchema && Object.hasOwn(input, "data") ? { data: input.data } : {}),
162
- ...(Object.hasOwn(input, "attachmentIds") ? { attachmentIds: input.attachmentIds } : {}),
163
- ...(Object.hasOwn(input, "steer") ? { steer: input.steer } : {})
164
- });
165
- }
166
- },
167
- {
175
+ execute: (input, context) => conversationOperations.send(input, context)
176
+ }),
177
+ Object.freeze({
168
178
  id: actionIds.conversationCancel,
169
179
  version: 1,
170
180
  kind: "command",
@@ -173,13 +183,9 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
173
183
  permission: { require: "authenticated" },
174
184
  input: conversationInputValidator,
175
185
  output: null,
176
- idempotency: "none",
177
- async execute(input, context) {
178
- const conversation = await openConversation(input, context);
179
- return conversation.cancel();
180
- }
181
- },
182
- {
186
+ execute: (input, context) => conversationOperations.cancel(input, context)
187
+ }),
188
+ Object.freeze({
183
189
  id: actionIds.conversationSubscribe,
184
190
  version: 1,
185
191
  kind: "query",
@@ -188,29 +194,9 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
188
194
  permission: { require: "authenticated" },
189
195
  input: conversationInputValidator,
190
196
  output: null,
191
- idempotency: "none",
192
- async execute(input, context, deps) {
193
- if (typeof deps?.onEvent !== "function" || typeof deps?.onRelease !== "function") {
194
- throw new TypeError("Conversation subscriptions require their server-owned event and cleanup callbacks.");
195
- }
196
- if (deps.isCurrent?.() === false) return null;
197
- deps.onStage?.("conversation-open");
198
- const conversation = await openConversation(input, context);
199
- if (deps.isCurrent?.() === false) return null;
200
- deps.onStage?.("observer-attach");
201
- const release = await conversation.subscribe(deps.onEvent);
202
- try {
203
- deps.onRelease(release);
204
- if (deps.isCurrent?.() === false) return null;
205
- deps.onStage?.("snapshot-read");
206
- return await conversation.read();
207
- } catch (error) {
208
- release();
209
- throw error;
210
- }
211
- }
212
- },
213
- {
197
+ execute: (input, context, deps) => conversationOperations.subscribe(input, context, deps)
198
+ }),
199
+ Object.freeze({
214
200
  id: actionIds.conversationInspectDelivery,
215
201
  version: 1,
216
202
  kind: "command",
@@ -219,13 +205,9 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
219
205
  permission: { require: "authenticated" },
220
206
  input: conversationDeliveryInputValidator,
221
207
  output: null,
222
- idempotency: "none",
223
- async execute(input, context) {
224
- const conversation = await openConversation(input, context);
225
- return conversation.inspectDelivery({ messageId: input.messageId });
226
- }
227
- },
228
- {
208
+ execute: (input, context) => conversationOperations.inspectDelivery(input, context)
209
+ }),
210
+ Object.freeze({
229
211
  id: actionIds.conversationCancelDelivery,
230
212
  version: 1,
231
213
  kind: "command",
@@ -234,13 +216,9 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
234
216
  permission: { require: "authenticated" },
235
217
  input: conversationDeliveryInputValidator,
236
218
  output: null,
237
- idempotency: "none",
238
- async execute(input, context) {
239
- const conversation = await openConversation(input, context);
240
- return conversation.cancelDelivery({ messageId: input.messageId });
241
- }
242
- },
243
- {
219
+ execute: (input, context) => conversationOperations.cancelDelivery(input, context)
220
+ }),
221
+ Object.freeze({
244
222
  id: actionIds.conversationGoalRead,
245
223
  version: 1,
246
224
  kind: "query",
@@ -249,12 +227,9 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
249
227
  permission: { require: "authenticated" },
250
228
  input: conversationInputValidator,
251
229
  output: null,
252
- idempotency: "none",
253
- async execute(input, context) {
254
- return (await openConversation(input, context)).readGoal();
255
- }
256
- },
257
- {
230
+ execute: (input, context) => conversationOperations.readGoal(input, context)
231
+ }),
232
+ Object.freeze({
258
233
  id: actionIds.conversationGoalUpdate,
259
234
  version: 1,
260
235
  kind: "command",
@@ -265,17 +240,9 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
265
240
  mode: "replace", context: "assistant-runtime conversation goal action input"
266
241
  }),
267
242
  output: null,
268
- idempotency: "none",
269
- async execute(input, context) {
270
- const conversation = await openConversation(input, context);
271
- const goal = { action: input.action, expectedSegmentId: input.expectedSegmentId };
272
- for (const key of ["expectedGoalId", "messageId", "objective", "tokenBudget", "attachmentIds"]) {
273
- if (Object.hasOwn(input, key)) goal[key] = input[key];
274
- }
275
- return conversation.updateGoal(goal);
276
- }
277
- },
278
- ...(conversationConfigurationSchema ? [{
243
+ execute: (input, context) => conversationOperations.updateGoal(input, context)
244
+ }),
245
+ ...(conversationConfigurationSchema ? Object.freeze([Object.freeze({
279
246
  id: actionIds.conversationConfigure,
280
247
  version: 1,
281
248
  kind: "command",
@@ -286,12 +253,9 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
286
253
  mode: "replace", context: "assistant-runtime declared conversation configuration"
287
254
  }),
288
255
  output: null,
289
- idempotency: "none",
290
- async execute(input, context) {
291
- return (await openConversation(input, context)).configure(input.configuration);
292
- }
293
- }] : []),
294
- ...(conversationSelectionSchema ? [{
256
+ execute: (input, context) => conversationOperations.configure(input, context)
257
+ })]) : []),
258
+ ...(conversationSelectionSchema ? Object.freeze([Object.freeze({
295
259
  id: actionIds.conversationSelect,
296
260
  version: 1,
297
261
  kind: "command",
@@ -302,12 +266,9 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
302
266
  mode: "replace", context: "assistant-runtime declared conversation selection"
303
267
  }),
304
268
  output: null,
305
- idempotency: "none",
306
- async execute(input, context) {
307
- return (await openConversation(input, context)).select(input.selection);
308
- }
309
- }] : []),
310
- ...(conversationReplacementSchema ? [{
269
+ execute: (input, context) => conversationOperations.select(input, context)
270
+ })]) : []),
271
+ ...(conversationReplacementSchema ? Object.freeze([Object.freeze({
311
272
  id: actionIds.conversationReplace,
312
273
  version: 1,
313
274
  kind: "command",
@@ -318,41 +279,56 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
318
279
  mode: "replace", context: "assistant-runtime declared conversation replacement"
319
280
  }),
320
281
  output: null,
321
- idempotency: "none",
322
- async execute(input, context) {
323
- return (await openConversation(input, context)).replace(input.replacement);
324
- }
325
- }] : [])
326
- ];
282
+ execute: (input, context) => conversationOperations.replace(input, context)
283
+ })]) : [])
284
+ ]);
327
285
  }
328
286
 
287
+ /**
288
+ * The assistant's actions: over a conversation runtime the application supplies, its
289
+ * conversation actions (wrapped by the host's access owner when it supplies one) and
290
+ * the settings actions when it supplies a settings service; otherwise the chat stream,
291
+ * transcript and settings actions of the package's own tables. A configuration
292
+ * without a runtime and a settings surface has none.
293
+ * @param {object} dependencies
294
+ * @param {AssistantConfigService | null} dependencies.assistantConfigService
295
+ * @param {ChatService} [dependencies.chatService] The chat service of the package's own tables, without a conversation runtime.
296
+ * @param {ConversationRuntime | null} dependencies.conversationRuntime
297
+ * @param {ConversationAccess | null} dependencies.conversationAccess
298
+ * @param {object | null} dependencies.conversationDataSchema
299
+ * @param {object | null} dependencies.conversationConfigurationSchema
300
+ * @param {object | null} dependencies.conversationSelectionSchema
301
+ * @param {object | null} dependencies.conversationReplacementSchema
302
+ * @param {WorkspaceScopeSupport | null} dependencies.workspaceScopeSupport
303
+ * @param {Record<string, any>} dependencies.config
304
+ */
329
305
  function createAssistantActions({
330
306
  assistantConfigService,
331
307
  chatService,
332
- conversationRuntime = null,
333
- conversationAccess = null,
334
- conversationDataSchema = null,
335
- conversationConfigurationSchema = null,
336
- conversationSelectionSchema = null,
337
- conversationReplacementSchema = null,
338
- workspaceScopeSupport = null,
339
- config = {}
340
- } = {}) {
341
- if (conversationRuntime && typeof conversationRuntime.open !== "function") {
342
- throw new TypeError("Assistant conversation actions require conversationRuntime.open().");
343
- }
344
- if (!conversationRuntime && (!assistantConfigService || !chatService)) {
345
- throw new TypeError("createAssistantActions requires assistantConfigService and chatService.");
346
- }
308
+ conversationRuntime,
309
+ conversationAccess,
310
+ conversationDataSchema,
311
+ conversationConfigurationSchema,
312
+ conversationSelectionSchema,
313
+ conversationReplacementSchema,
314
+ workspaceScopeSupport,
315
+ config
316
+ }) {
347
317
  const configuredSurfaces = configuredAssistantSurfaces(config);
348
318
  const runtimeSurfaces = [...new Set(configuredSurfaces.map((entry) => entry.targetSurfaceId))];
349
319
  const settingsSurfaces = [...new Set(configuredSurfaces.map((entry) => entry.settingsSurfaceId))];
350
320
  if (runtimeSurfaces.length === 0 || settingsSurfaces.length === 0) return [];
351
321
 
352
322
  if (conversationRuntime) {
323
+ const conversationOperations = createConversationOperations({
324
+ conversationRuntime,
325
+ config,
326
+ workspaceScopeSupport,
327
+ conversationDataSchema
328
+ });
353
329
  const conversationActions = createConversationActions({
354
- conversationRuntime, conversationDataSchema, conversationConfigurationSchema, conversationSelectionSchema, conversationReplacementSchema,
355
- config, runtimeSurfaces, workspaceScopeSupport
330
+ conversationOperations, conversationDataSchema, conversationConfigurationSchema, conversationSelectionSchema, conversationReplacementSchema,
331
+ runtimeSurfaces
356
332
  });
357
333
  return Object.freeze([
358
334
  ...conversationActions.map((definition) => {
@@ -368,64 +344,49 @@ function createAssistantActions({
368
344
  }
369
345
 
370
346
  return Object.freeze([
371
- {
372
- id: actionIds.chatStream,
373
- version: 1,
374
- kind: "stream",
375
- channels: ["api", "internal"],
376
- surfaces: runtimeSurfaces,
377
- permission: {
378
- require: "authenticated"
379
- },
380
- input: runtimeChatStreamInputValidator,
381
- idempotency: "optional",
382
- async execute(input, context, deps) {
383
- return chatService.streamChat(input, {
347
+ Object.freeze({
348
+ id: actionIds.chatStream,
349
+ version: 1,
350
+ kind: "stream",
351
+ channels: ["api", "internal"],
352
+ surfaces: runtimeSurfaces,
353
+ permission: {
354
+ require: "authenticated"
355
+ },
356
+ input: runtimeChatStreamInputValidator,
357
+ execute: (input, context, deps) => chatService.streamChat(input, {
384
358
  context,
385
359
  streamWriter: deps.streamWriter,
386
360
  abortSignal: deps.abortSignal
387
- });
388
- }
389
- },
390
- {
391
- id: actionIds.conversationsList,
392
- version: 1,
393
- kind: "query",
394
- channels: ["api", "internal"],
395
- surfaces: runtimeSurfaces,
396
- permission: {
397
- require: "authenticated"
398
- },
399
- input: runtimeConversationsListInputValidator,
400
- output: null,
401
- idempotency: "none",
402
- async execute(input, context) {
403
- return returnJsonApiData(await chatService.listConversations(input.query, {
404
- context,
405
- input
406
- }));
407
- }
408
- },
409
- {
410
- id: actionIds.conversationMessagesList,
411
- version: 1,
412
- kind: "query",
413
- channels: ["api", "internal"],
414
- surfaces: runtimeSurfaces,
415
- permission: {
416
- require: "authenticated"
417
- },
418
- input: runtimeConversationMessagesListInputValidator,
419
- output: null,
420
- idempotency: "none",
421
- async execute(input, context) {
422
- return returnJsonApiData(await chatService.getConversationMessages(input.conversationId, input.query, {
423
- context,
424
- input
425
- }));
426
- }
427
- },
428
- ...createAssistantSettingsActions({ assistantConfigService, settingsSurfaces })
361
+ })
362
+ }),
363
+ Object.freeze({
364
+ id: actionIds.conversationsList,
365
+ version: 1,
366
+ kind: "query",
367
+ channels: ["api", "internal"],
368
+ surfaces: runtimeSurfaces,
369
+ permission: {
370
+ require: "authenticated"
371
+ },
372
+ input: runtimeConversationsListInputValidator,
373
+ output: null,
374
+ execute: (input, context) => listConversationsDocument(chatService, input, context)
375
+ }),
376
+ Object.freeze({
377
+ id: actionIds.conversationMessagesList,
378
+ version: 1,
379
+ kind: "query",
380
+ channels: ["api", "internal"],
381
+ surfaces: runtimeSurfaces,
382
+ permission: {
383
+ require: "authenticated"
384
+ },
385
+ input: runtimeConversationMessagesListInputValidator,
386
+ output: null,
387
+ execute: (input, context) => listConversationMessagesDocument(chatService, input, context)
388
+ }),
389
+ ...createAssistantSettingsActions({ assistantConfigService, settingsSurfaces })
429
390
  ]);
430
391
  }
431
392
 
@@ -0,0 +1,141 @@
1
+ import { AppError } from "@jskit-ai/kernel/server/runtime";
2
+ import { requireAssistantSurface } from "./support/assistantSurface.js";
3
+
4
+ /** @typedef {import("./types.js").ConversationRuntime} ConversationRuntime */
5
+ /** @typedef {import("./types.js").WorkspaceScopeSupport} WorkspaceScopeSupport */
6
+
7
+ /** A subscription caller that does not say whether it is still current stays current. */
8
+ function alwaysCurrent() {
9
+ return true;
10
+ }
11
+
12
+ /** A subscription caller that does not follow its stages ignores them. */
13
+ function ignoreStage() {}
14
+
15
+ /**
16
+ * What the conversation actions do: each opens the action's conversation in its
17
+ * context, on the conversation's workspace when its surface requires one, and
18
+ * calls one conversation method.
19
+ * @param {object} dependencies
20
+ * @param {ConversationRuntime} dependencies.conversationRuntime
21
+ * @param {Record<string, any>} dependencies.config
22
+ * @param {WorkspaceScopeSupport | null} dependencies.workspaceScopeSupport
23
+ * @param {object | null} dependencies.conversationDataSchema The application's declared message data, when it declares one.
24
+ */
25
+ function createConversationOperations({ conversationRuntime, config, workspaceScopeSupport, conversationDataSchema }) {
26
+ async function openConversation(input, context) {
27
+ const assistantSurface = requireAssistantSurface(config, input.targetSurfaceId);
28
+ let conversationContext = context;
29
+ if (assistantSurface.runtimeSurfaceRequiresWorkspace) {
30
+ if (!workspaceScopeSupport) {
31
+ throw new Error("Assistant conversation actions require workspace server scope support.");
32
+ }
33
+ const workspace = workspaceScopeSupport.resolveWorkspace(context, { workspaceSlug: input.workspaceSlug });
34
+ if (!workspace?.id) throw new AppError(409, "Workspace selection required.", { code: "assistant.runtime.workspace.selectionRequired" });
35
+ conversationContext = { ...context, workspace };
36
+ }
37
+ return conversationRuntime.open({ id: input.conversationId, context: conversationContext });
38
+ }
39
+
40
+ async function read(input, context) {
41
+ const conversation = await openConversation(input, context);
42
+ const options = {};
43
+ for (const key of ["beforeTurnId", "limit"]) {
44
+ if (Object.hasOwn(input, key)) options[key] = input[key];
45
+ }
46
+ return Object.keys(options).length ? conversation.read(options) : conversation.read();
47
+ }
48
+
49
+ async function send(input, context) {
50
+ const conversation = await openConversation(input, context);
51
+ return conversation.send({
52
+ messageId: input.messageId,
53
+ text: input.text,
54
+ ...(conversationDataSchema && Object.hasOwn(input, "data") ? { data: input.data } : {}),
55
+ ...(Object.hasOwn(input, "attachmentIds") ? { attachmentIds: input.attachmentIds } : {}),
56
+ ...(Object.hasOwn(input, "steer") ? { steer: input.steer } : {})
57
+ });
58
+ }
59
+
60
+ async function cancel(input, context) {
61
+ const conversation = await openConversation(input, context);
62
+ return conversation.cancel();
63
+ }
64
+
65
+ /**
66
+ * Opens the conversation, attaches the caller's observer and answers with its
67
+ * snapshot. The caller's callbacks come with each call.
68
+ * @param {object} input
69
+ * @param {object} context
70
+ * @param {{ onEvent: (event: object) => void, onRelease: (release: () => void) => void, isCurrent?: () => boolean,
71
+ * onStage?: (stage: string) => void }} deps
72
+ */
73
+ async function subscribe(input, context, { onEvent, onRelease, isCurrent = alwaysCurrent, onStage = ignoreStage }) {
74
+ if (isCurrent() === false) return null;
75
+ onStage("conversation-open");
76
+ const conversation = await openConversation(input, context);
77
+ if (isCurrent() === false) return null;
78
+ onStage("observer-attach");
79
+ const release = await conversation.subscribe(onEvent);
80
+ try {
81
+ onRelease(release);
82
+ if (isCurrent() === false) return null;
83
+ onStage("snapshot-read");
84
+ return await conversation.read();
85
+ } catch (error) {
86
+ release();
87
+ throw error;
88
+ }
89
+ }
90
+
91
+ async function inspectDelivery(input, context) {
92
+ const conversation = await openConversation(input, context);
93
+ return conversation.inspectDelivery({ messageId: input.messageId });
94
+ }
95
+
96
+ async function cancelDelivery(input, context) {
97
+ const conversation = await openConversation(input, context);
98
+ return conversation.cancelDelivery({ messageId: input.messageId });
99
+ }
100
+
101
+ async function readGoal(input, context) {
102
+ return (await openConversation(input, context)).readGoal();
103
+ }
104
+
105
+ async function updateGoal(input, context) {
106
+ const conversation = await openConversation(input, context);
107
+ const goal = { action: input.action, expectedSegmentId: input.expectedSegmentId };
108
+ for (const key of ["expectedGoalId", "messageId", "objective", "tokenBudget", "attachmentIds"]) {
109
+ if (Object.hasOwn(input, key)) goal[key] = input[key];
110
+ }
111
+ return conversation.updateGoal(goal);
112
+ }
113
+
114
+ async function configure(input, context) {
115
+ return (await openConversation(input, context)).configure(input.configuration);
116
+ }
117
+
118
+ async function select(input, context) {
119
+ return (await openConversation(input, context)).select(input.selection);
120
+ }
121
+
122
+ async function replace(input, context) {
123
+ return (await openConversation(input, context)).replace(input.replacement);
124
+ }
125
+
126
+ return Object.freeze({
127
+ read,
128
+ send,
129
+ cancel,
130
+ subscribe,
131
+ inspectDelivery,
132
+ cancelDelivery,
133
+ readGoal,
134
+ updateGoal,
135
+ configure,
136
+ select,
137
+ replace
138
+ });
139
+ }
140
+
141
+ export { createConversationOperations };