@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,14 +1,10 @@
1
1
  import { AppError } from "@jskit-ai/kernel/server/runtime";
2
2
  import { composeSchemaDefinitions } from "@jskit-ai/kernel/shared/validators";
3
- import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
4
3
  import { createJsonApiResourceRouteContract } from "@jskit-ai/http-runtime/shared/validators/jsonApiRouteTransport";
5
4
  import {
6
5
  ASSISTANT_CONVERSATIONS_TRANSPORT,
7
6
  ASSISTANT_CONVERSATION_MESSAGES_TRANSPORT,
8
- ASSISTANT_SETTINGS_UPDATE_TRANSPORT,
9
- ASSISTANT_SETTINGS_TRANSPORT,
10
7
  assistantConversationOutputValidator,
11
- assistantConfigResource,
12
8
  assistantResource,
13
9
  resolveAssistantApiBasePath
14
10
  } from "@jskit-ai/assistant-core/shared";
@@ -18,56 +14,20 @@ import {
18
14
  setNdjsonHeaders,
19
15
  writeNdjson
20
16
  } from "@jskit-ai/assistant-core/server";
21
- import { resolveAssistantSurfaceConfig } from "../shared/assistantSurfaces.js";
22
17
  import { actionIds } from "./actionIds.js";
23
- import { assistantSurfaceRouteParamsValidator } from "./inputSchemas.js";
24
-
25
- function requireWorkspaceAssistantRouteParams(workspaceScopeSupport = null) {
26
- if (!workspaceScopeSupport) {
27
- throw new Error("Assistant workspace routes require workspace server scope support.");
28
- }
29
-
30
- return composeSchemaDefinitions(
31
- [workspaceScopeSupport.params, assistantSurfaceRouteParamsValidator],
32
- {
33
- mode: "patch",
34
- context: "assistant-runtime workspace surface route params"
35
- }
36
- );
37
- }
38
-
39
- function readWorkspaceInput(request, requiresWorkspace, workspaceScopeSupport = null) {
40
- if (requiresWorkspace !== true) {
41
- return {};
42
- }
43
-
44
- if (!workspaceScopeSupport) {
45
- throw new Error("Assistant workspace routes require workspace server scope support.");
46
- }
47
-
48
- return workspaceScopeSupport.buildInputFromRouteParams(request?.input?.params);
49
- }
50
-
51
- function requireAssistantSurface(appConfig = {}, targetSurfaceId = "") {
52
- const assistantSurface = resolveAssistantSurfaceConfig(appConfig, targetSurfaceId);
53
- if (assistantSurface) {
54
- return assistantSurface;
55
- }
56
-
57
- throw new AppError(404, "Assistant not found.");
58
- }
59
-
60
- function requireHostSurfaceId(request) {
61
- const headerValue = Array.isArray(request?.headers?.["x-jskit-surface"])
62
- ? request.headers["x-jskit-surface"][0]
63
- : request?.headers?.["x-jskit-surface"];
64
- const hostSurfaceId = normalizeSurfaceId(headerValue);
65
- if (hostSurfaceId) {
66
- return hostSurfaceId;
67
- }
68
-
69
- throw new AppError(400, "Assistant surface header x-jskit-surface is required.");
70
- }
18
+ import {
19
+ assistantConversationIdInputValidator,
20
+ assistantConversationGoalBodyValidator,
21
+ assistantConversationMessageIdInputValidator,
22
+ assistantConversationReadQueryValidator,
23
+ conversationConfigureBodyValidator,
24
+ conversationSelectBodyValidator,
25
+ conversationReplaceBodyValidator,
26
+ conversationSendBodyValidator,
27
+ assistantSurfaceRouteParamsValidator
28
+ } from "./inputSchemas.js";
29
+ import { registerSettingsRoutes } from "./registerSettingsRoutes.js";
30
+ import { requireWorkspaceAssistantRouteParams, resolveRouteRequestState } from "./support/assistantRouteContext.js";
71
31
 
72
32
  function shouldExposeAppErrorDetails(errorCode = "") {
73
33
  return String(errorCode || "").trim() !== "ACTION_PERMISSION_DENIED";
@@ -106,43 +66,6 @@ function sendPreStreamErrorResponse(reply, error) {
106
66
  });
107
67
  }
108
68
 
109
- function resolveRouteRequestState(
110
- request,
111
- {
112
- resolveCurrentAppConfig = () => ({}),
113
- kind = "runtime",
114
- requiresWorkspace = false,
115
- workspaceScopeSupport = null
116
- } = {}
117
- ) {
118
- const appConfig = resolveCurrentAppConfig();
119
- const targetSurfaceId = normalizeSurfaceId(request?.input?.params?.surfaceId);
120
- const assistantSurface = requireAssistantSurface(appConfig, targetSurfaceId);
121
- const hostSurfaceId = requireHostSurfaceId(request);
122
- const expectsWorkspace = kind === "settings"
123
- ? assistantSurface.settingsSurfaceRequiresWorkspace
124
- : assistantSurface.runtimeSurfaceRequiresWorkspace;
125
- const expectedHostSurfaceId = kind === "settings"
126
- ? assistantSurface.settingsSurfaceId
127
- : assistantSurface.targetSurfaceId;
128
-
129
- if (expectsWorkspace !== (requiresWorkspace === true)) {
130
- throw new AppError(404, "Assistant route not found.");
131
- }
132
- if (hostSurfaceId !== expectedHostSurfaceId) {
133
- throw new AppError(403, "Assistant route is not available on this surface.");
134
- }
135
-
136
- return Object.freeze({
137
- assistantSurface,
138
- hostSurfaceId,
139
- actionInput: Object.freeze({
140
- targetSurfaceId: assistantSurface.targetSurfaceId,
141
- ...readWorkspaceInput(request, requiresWorkspace, workspaceScopeSupport)
142
- })
143
- });
144
- }
145
-
146
69
  function buildChatStreamActionInput(routeInput = {}, requestBody = {}) {
147
70
  const actionInput = {
148
71
  ...routeInput,
@@ -159,15 +82,6 @@ function buildChatStreamActionInput(routeInput = {}, requestBody = {}) {
159
82
  return actionInput;
160
83
  }
161
84
 
162
- function resolveAssistantSettingsRecordId(record = {}) {
163
- const scopeKey = String(record?.scopeKey || "").trim();
164
- if (!scopeKey) {
165
- throw new Error("Assistant settings JSON:API response requires scopeKey.");
166
- }
167
-
168
- return scopeKey;
169
- }
170
-
171
85
  function resolveAssistantConversationMessagesRecordId(record = {}) {
172
86
  const conversationId = String(record?.conversation?.id || "").trim();
173
87
  if (!conversationId) {
@@ -177,98 +91,6 @@ function resolveAssistantConversationMessagesRecordId(record = {}) {
177
91
  return conversationId;
178
92
  }
179
93
 
180
- function registerSettingsRoutes(
181
- router,
182
- resolveCurrentAppConfig,
183
- { requiresWorkspace = false, workspaceScopeSupport = null } = {}
184
- ) {
185
- const routeBase = resolveAssistantApiBasePath({
186
- requiresWorkspace
187
- });
188
- const visibility = requiresWorkspace ? "workspace" : "public";
189
- const routePath = `${routeBase}/:surfaceId/settings`;
190
- const params = requiresWorkspace === true
191
- ? requireWorkspaceAssistantRouteParams(workspaceScopeSupport)
192
- : assistantSurfaceRouteParamsValidator;
193
-
194
- router.register(
195
- "GET",
196
- routePath,
197
- {
198
- auth: "required",
199
- visibility,
200
- params,
201
- meta: {
202
- tags: ["assistant", "settings"],
203
- summary: "Get assistant settings."
204
- },
205
- ...createJsonApiResourceRouteContract({
206
- ...ASSISTANT_SETTINGS_TRANSPORT,
207
- output: assistantConfigResource.operations.view.output,
208
- outputKind: "record",
209
- getRecordId: resolveAssistantSettingsRecordId
210
- })
211
- },
212
- async function assistantSettingsReadRoute(request, reply) {
213
- const routeState = resolveRouteRequestState(request, {
214
- resolveCurrentAppConfig,
215
- kind: "settings",
216
- requiresWorkspace,
217
- workspaceScopeSupport
218
- });
219
-
220
- const response = await request.executeAction({
221
- actionId: actionIds.settingsRead,
222
- surface: routeState.hostSurfaceId,
223
- input: routeState.actionInput
224
- });
225
-
226
- reply.code(200).send(response);
227
- }
228
- );
229
-
230
- router.register(
231
- "PATCH",
232
- routePath,
233
- {
234
- auth: "required",
235
- visibility,
236
- params,
237
- meta: {
238
- tags: ["assistant", "settings"],
239
- summary: "Update assistant settings."
240
- },
241
- ...createJsonApiResourceRouteContract({
242
- ...ASSISTANT_SETTINGS_UPDATE_TRANSPORT,
243
- body: assistantConfigResource.operations.patch.body,
244
- output: assistantConfigResource.operations.patch.output,
245
- outputKind: "record",
246
- getRecordId: resolveAssistantSettingsRecordId,
247
- includeValidation400: true
248
- })
249
- },
250
- async function assistantSettingsPatchRoute(request, reply) {
251
- const routeState = resolveRouteRequestState(request, {
252
- resolveCurrentAppConfig,
253
- kind: "settings",
254
- requiresWorkspace,
255
- workspaceScopeSupport
256
- });
257
-
258
- const response = await request.executeAction({
259
- actionId: actionIds.settingsUpdate,
260
- surface: routeState.hostSurfaceId,
261
- input: {
262
- ...routeState.actionInput,
263
- patch: request.input.body
264
- }
265
- });
266
-
267
- reply.code(200).send(response);
268
- }
269
- );
270
- }
271
-
272
94
  function registerRuntimeRoutes(
273
95
  router,
274
96
  resolveCurrentAppConfig,
@@ -487,27 +309,227 @@ function registerRuntimeRoutes(
487
309
  );
488
310
  }
489
311
 
490
- function registerRoutes(router, { config = {}, workspaceScopeSupport = null } = {}) {
312
+ function registerConversationRoutes(
313
+ router,
314
+ resolveCurrentAppConfig,
315
+ { requiresWorkspace = false, workspaceScopeSupport = null, conversationDataSchema = null,
316
+ conversationConfigurationSchema = null, conversationSelectionSchema = null, conversationReplacementSchema = null } = {}
317
+ ) {
318
+ const routeBase = `${resolveAssistantApiBasePath({ requiresWorkspace })}/:surfaceId/conversations/:conversationId`;
319
+ const visibility = requiresWorkspace ? "workspace" : "public";
320
+ const surfaceParams = requiresWorkspace
321
+ ? requireWorkspaceAssistantRouteParams(workspaceScopeSupport)
322
+ : assistantSurfaceRouteParamsValidator;
323
+ const params = composeSchemaDefinitions([surfaceParams, assistantConversationIdInputValidator], {
324
+ mode: "patch",
325
+ context: "assistant-runtime conversation route params"
326
+ });
327
+
328
+ router.register("GET", routeBase, {
329
+ auth: "required", visibility, params,
330
+ query: assistantConversationReadQueryValidator,
331
+ meta: { tags: ["assistant"], summary: "Read assistant conversation state." }
332
+ }, async function assistantConversationReadRoute(request, reply) {
333
+ const routeState = resolveRouteRequestState(request, {
334
+ resolveCurrentAppConfig, requiresWorkspace, workspaceScopeSupport
335
+ });
336
+ const input = { ...routeState.actionInput, conversationId: request.input.params.conversationId };
337
+ for (const key of ["beforeTurnId", "limit"]) {
338
+ if (Object.hasOwn(request.input.query || {}, key)) input[key] = request.input.query[key];
339
+ }
340
+ const response = await request.executeAction({
341
+ actionId: actionIds.conversationRead,
342
+ surface: routeState.hostSurfaceId,
343
+ input
344
+ });
345
+ reply.code(200).send(response);
346
+ });
347
+
348
+ router.register("POST", `${routeBase}/messages`, {
349
+ auth: "required", visibility, params,
350
+ body: conversationSendBodyValidator(conversationDataSchema),
351
+ meta: { tags: ["assistant"], summary: "Send an assistant conversation message." }
352
+ }, async function assistantConversationSendRoute(request, reply) {
353
+ const routeState = resolveRouteRequestState(request, {
354
+ resolveCurrentAppConfig, requiresWorkspace, workspaceScopeSupport
355
+ });
356
+ const body = request.input.body;
357
+ const response = await request.executeAction({
358
+ actionId: actionIds.conversationSend,
359
+ surface: routeState.hostSurfaceId,
360
+ input: {
361
+ ...routeState.actionInput,
362
+ conversationId: request.input.params.conversationId,
363
+ messageId: body.messageId,
364
+ text: body.text,
365
+ ...(conversationDataSchema && Object.hasOwn(body, "data") ? { data: body.data } : {}),
366
+ ...(Object.hasOwn(body, "attachmentIds") ? { attachmentIds: body.attachmentIds } : {}),
367
+ ...(Object.hasOwn(body, "steer") ? { steer: body.steer } : {})
368
+ }
369
+ });
370
+ reply.code(202).send(response);
371
+ });
372
+
373
+ router.register("POST", `${routeBase}/cancel`, {
374
+ auth: "required", visibility, params,
375
+ meta: { tags: ["assistant"], summary: "Cancel the active assistant conversation turn." }
376
+ }, async function assistantConversationCancelRoute(request, reply) {
377
+ const routeState = resolveRouteRequestState(request, {
378
+ resolveCurrentAppConfig, requiresWorkspace, workspaceScopeSupport
379
+ });
380
+ const response = await request.executeAction({
381
+ actionId: actionIds.conversationCancel,
382
+ surface: routeState.hostSurfaceId,
383
+ input: { ...routeState.actionInput, conversationId: request.input.params.conversationId }
384
+ });
385
+ reply.code(200).send(response);
386
+ });
387
+
388
+ router.register("POST", `${routeBase}/deliveries/:messageId/inspect`, {
389
+ auth: "required", visibility,
390
+ params: composeSchemaDefinitions([params, assistantConversationMessageIdInputValidator], {
391
+ mode: "patch", context: "assistant-runtime conversation delivery inspection route params"
392
+ }),
393
+ meta: { tags: ["assistant"], summary: "Inspect assistant message delivery without resubmitting it." }
394
+ }, async function assistantConversationInspectDeliveryRoute(request, reply) {
395
+ const routeState = resolveRouteRequestState(request, {
396
+ resolveCurrentAppConfig, requiresWorkspace, workspaceScopeSupport
397
+ });
398
+ const response = await request.executeAction({
399
+ actionId: actionIds.conversationInspectDelivery,
400
+ surface: routeState.hostSurfaceId,
401
+ input: { ...routeState.actionInput, conversationId: request.input.params.conversationId,
402
+ messageId: request.input.params.messageId }
403
+ });
404
+ reply.code(200).send(response);
405
+ });
406
+
407
+ for (const method of ["GET", "POST"]) {
408
+ router.register(method, `${routeBase}/goal`, {
409
+ auth: "required", visibility, params,
410
+ ...(method === "POST" ? { body: assistantConversationGoalBodyValidator } : {}),
411
+ meta: { tags: ["assistant"], summary: method === "GET" ? "Read the current conversation goal." : "Update the conversation goal." }
412
+ }, async function assistantConversationGoalRoute(request, reply) {
413
+ const routeState = resolveRouteRequestState(request, {
414
+ resolveCurrentAppConfig, requiresWorkspace, workspaceScopeSupport
415
+ });
416
+ const input = { ...routeState.actionInput, conversationId: request.input.params.conversationId };
417
+ if (method === "POST") {
418
+ for (const key of ["action", "expectedSegmentId", "expectedGoalId", "messageId", "objective", "tokenBudget", "attachmentIds"]) {
419
+ if (Object.hasOwn(request.input.body, key)) input[key] = request.input.body[key];
420
+ }
421
+ }
422
+ const response = await request.executeAction({
423
+ actionId: method === "GET" ? actionIds.conversationGoalRead : actionIds.conversationGoalUpdate,
424
+ surface: routeState.hostSurfaceId, input
425
+ });
426
+ reply.code(response === null ? 204 : 200).send(response);
427
+ });
428
+ }
429
+
430
+ if (conversationConfigurationSchema) {
431
+ router.register("PATCH", `${routeBase}/configuration`, {
432
+ auth: "required", visibility, params,
433
+ body: conversationConfigureBodyValidator(conversationConfigurationSchema),
434
+ meta: { tags: ["assistant"], summary: "Apply the application's declared conversation settings." }
435
+ }, async function assistantConversationConfigureRoute(request, reply) {
436
+ const routeState = resolveRouteRequestState(request, {
437
+ resolveCurrentAppConfig, requiresWorkspace, workspaceScopeSupport
438
+ });
439
+ const response = await request.executeAction({
440
+ actionId: actionIds.conversationConfigure,
441
+ surface: routeState.hostSurfaceId,
442
+ input: { ...routeState.actionInput, conversationId: request.input.params.conversationId,
443
+ configuration: request.input.body.configuration }
444
+ });
445
+ reply.code(200).send(response);
446
+ });
447
+ }
448
+
449
+ if (conversationSelectionSchema) {
450
+ router.register("POST", `${routeBase}/selection`, {
451
+ auth: "required", visibility, params,
452
+ body: conversationSelectBodyValidator(conversationSelectionSchema),
453
+ meta: { tags: ["assistant"], summary: "Apply an authorized application conversation selection." }
454
+ }, async function assistantConversationSelectRoute(request, reply) {
455
+ const routeState = resolveRouteRequestState(request, {
456
+ resolveCurrentAppConfig, requiresWorkspace, workspaceScopeSupport
457
+ });
458
+ const response = await request.executeAction({
459
+ actionId: actionIds.conversationSelect,
460
+ surface: routeState.hostSurfaceId,
461
+ input: { ...routeState.actionInput, conversationId: request.input.params.conversationId,
462
+ selection: request.input.body.selection }
463
+ });
464
+ reply.code(200).send(response);
465
+ });
466
+ }
467
+
468
+ if (conversationReplacementSchema) {
469
+ router.register("POST", `${routeBase}/replacement`, {
470
+ auth: "required", visibility, params,
471
+ body: conversationReplaceBodyValidator(conversationReplacementSchema),
472
+ meta: { tags: ["assistant"], summary: "Apply an authorized application conversation replacement." }
473
+ }, async function assistantConversationReplaceRoute(request, reply) {
474
+ const routeState = resolveRouteRequestState(request, {
475
+ resolveCurrentAppConfig, requiresWorkspace, workspaceScopeSupport
476
+ });
477
+ const response = await request.executeAction({
478
+ actionId: actionIds.conversationReplace,
479
+ surface: routeState.hostSurfaceId,
480
+ input: { ...routeState.actionInput, conversationId: request.input.params.conversationId,
481
+ replacement: request.input.body.replacement }
482
+ });
483
+ reply.code(200).send(response);
484
+ });
485
+ }
486
+ }
487
+
488
+ function registerRoutes(router, {
489
+ config = {},
490
+ workspaceScopeSupport = null,
491
+ conversationRuntime = null,
492
+ conversationAccess = null,
493
+ conversationDataSchema = null,
494
+ conversationConfigurationSchema = null,
495
+ conversationSelectionSchema = null,
496
+ conversationReplacementSchema = null,
497
+ assistantConfigService = null
498
+ } = {}) {
491
499
  if (!router || typeof router.register !== "function") {
492
500
  throw new Error("registerRoutes requires an HTTP router.");
493
501
  }
502
+ if (conversationRuntime && typeof conversationRuntime.open !== "function") {
503
+ throw new TypeError("Assistant conversation routes require conversationRuntime.open().");
504
+ }
494
505
  const resolveCurrentAppConfig = () => config;
506
+ const registerAssistantRuntimeRoutes = conversationRuntime ? registerConversationRoutes : registerRuntimeRoutes;
507
+ const runtimeRouter = conversationRuntime && conversationAccess ? conversationAccess.router : router;
508
+ const includeSettings = !conversationRuntime || Boolean(assistantConfigService);
495
509
 
496
- registerSettingsRoutes(router, resolveCurrentAppConfig, {
510
+ if (includeSettings) registerSettingsRoutes(router, resolveCurrentAppConfig, {
497
511
  requiresWorkspace: false
498
512
  });
499
- registerRuntimeRoutes(router, resolveCurrentAppConfig, {
500
- requiresWorkspace: false
513
+ registerAssistantRuntimeRoutes(runtimeRouter, resolveCurrentAppConfig, {
514
+ requiresWorkspace: false,
515
+ conversationDataSchema,
516
+ conversationConfigurationSchema,
517
+ conversationSelectionSchema,
518
+ conversationReplacementSchema
501
519
  });
502
520
 
503
521
  if (workspaceScopeSupport) {
504
- registerSettingsRoutes(router, resolveCurrentAppConfig, {
522
+ if (includeSettings) registerSettingsRoutes(router, resolveCurrentAppConfig, {
505
523
  requiresWorkspace: true,
506
524
  workspaceScopeSupport
507
525
  });
508
- registerRuntimeRoutes(router, resolveCurrentAppConfig, {
526
+ registerAssistantRuntimeRoutes(runtimeRouter, resolveCurrentAppConfig, {
509
527
  requiresWorkspace: true,
510
- workspaceScopeSupport
528
+ workspaceScopeSupport,
529
+ conversationDataSchema,
530
+ conversationConfigurationSchema,
531
+ conversationSelectionSchema,
532
+ conversationReplacementSchema
511
533
  });
512
534
  }
513
535
  }
@@ -0,0 +1,113 @@
1
+ import { createJsonApiResourceRouteContract } from "@jskit-ai/http-runtime/shared/validators/jsonApiRouteTransport";
2
+ import {
3
+ ASSISTANT_SETTINGS_UPDATE_TRANSPORT,
4
+ ASSISTANT_SETTINGS_TRANSPORT,
5
+ assistantConfigResource,
6
+ resolveAssistantApiBasePath
7
+ } from "@jskit-ai/assistant-core/shared";
8
+ import { actionIds } from "./actionIds.js";
9
+ import { assistantSurfaceRouteParamsValidator } from "./inputSchemas.js";
10
+ import { requireWorkspaceAssistantRouteParams, resolveRouteRequestState } from "./support/assistantRouteContext.js";
11
+
12
+ function resolveAssistantSettingsRecordId(record = {}) {
13
+ const scopeKey = String(record?.scopeKey || "").trim();
14
+ if (!scopeKey) {
15
+ throw new Error("Assistant settings JSON:API response requires scopeKey.");
16
+ }
17
+
18
+ return scopeKey;
19
+ }
20
+
21
+ function registerSettingsRoutes(
22
+ router,
23
+ resolveCurrentAppConfig,
24
+ { requiresWorkspace = false, workspaceScopeSupport = null } = {}
25
+ ) {
26
+ const routeBase = resolveAssistantApiBasePath({
27
+ requiresWorkspace
28
+ });
29
+ const visibility = requiresWorkspace ? "workspace" : "public";
30
+ const routePath = `${routeBase}/:surfaceId/settings`;
31
+ const params = requiresWorkspace === true
32
+ ? requireWorkspaceAssistantRouteParams(workspaceScopeSupport)
33
+ : assistantSurfaceRouteParamsValidator;
34
+
35
+ router.register(
36
+ "GET",
37
+ routePath,
38
+ {
39
+ auth: "required",
40
+ visibility,
41
+ params,
42
+ meta: {
43
+ tags: ["assistant", "settings"],
44
+ summary: "Get assistant settings."
45
+ },
46
+ ...createJsonApiResourceRouteContract({
47
+ ...ASSISTANT_SETTINGS_TRANSPORT,
48
+ output: assistantConfigResource.operations.view.output,
49
+ outputKind: "record",
50
+ getRecordId: resolveAssistantSettingsRecordId
51
+ })
52
+ },
53
+ async function assistantSettingsReadRoute(request, reply) {
54
+ const routeState = resolveRouteRequestState(request, {
55
+ resolveCurrentAppConfig,
56
+ kind: "settings",
57
+ requiresWorkspace,
58
+ workspaceScopeSupport
59
+ });
60
+
61
+ const response = await request.executeAction({
62
+ actionId: actionIds.settingsRead,
63
+ surface: routeState.hostSurfaceId,
64
+ input: routeState.actionInput
65
+ });
66
+
67
+ reply.code(200).send(response);
68
+ }
69
+ );
70
+
71
+ router.register(
72
+ "PATCH",
73
+ routePath,
74
+ {
75
+ auth: "required",
76
+ visibility,
77
+ params,
78
+ meta: {
79
+ tags: ["assistant", "settings"],
80
+ summary: "Update assistant settings."
81
+ },
82
+ ...createJsonApiResourceRouteContract({
83
+ ...ASSISTANT_SETTINGS_UPDATE_TRANSPORT,
84
+ body: assistantConfigResource.operations.patch.body,
85
+ output: assistantConfigResource.operations.patch.output,
86
+ outputKind: "record",
87
+ getRecordId: resolveAssistantSettingsRecordId,
88
+ includeValidation400: true
89
+ })
90
+ },
91
+ async function assistantSettingsPatchRoute(request, reply) {
92
+ const routeState = resolveRouteRequestState(request, {
93
+ resolveCurrentAppConfig,
94
+ kind: "settings",
95
+ requiresWorkspace,
96
+ workspaceScopeSupport
97
+ });
98
+
99
+ const response = await request.executeAction({
100
+ actionId: actionIds.settingsUpdate,
101
+ surface: routeState.hostSurfaceId,
102
+ input: {
103
+ ...routeState.actionInput,
104
+ patch: request.input.body
105
+ }
106
+ });
107
+
108
+ reply.code(200).send(response);
109
+ }
110
+ );
111
+ }
112
+
113
+ export { registerSettingsRoutes };
@@ -1,4 +1,4 @@
1
- import { createWithTransaction, normalizeDbRecordId, runInTransaction } from "@jskit-ai/database-runtime/shared/repositoryOptions";
1
+ import { applyForUpdate, createWithTransaction, normalizeDbRecordId, runInTransaction } from "@jskit-ai/database-runtime/shared/repositoryOptions";
2
2
  import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
3
3
  import { normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
4
4
  import {
@@ -88,9 +88,15 @@ function createRepository(knex) {
88
88
  }
89
89
 
90
90
  const client = options?.trx || knex;
91
- const row = await createConversationBaseQuery(client)
92
- .where("c.id", normalizedConversationId)
93
- .first();
91
+ if (options.forUpdate === true) {
92
+ if (!options.trx) throw new TypeError("Locking a conversation requires its database transaction.");
93
+ // Acquire the write reservation before reading, including SQLite where
94
+ // SELECT FOR UPDATE is not supported. This does not change row contents.
95
+ await client(assistantRuntimeConfig.conversationsTable).where({ id: normalizedConversationId })
96
+ .update({ id: normalizedConversationId });
97
+ }
98
+ const query = createConversationBaseQuery(client).where("c.id", normalizedConversationId);
99
+ const row = await applyForUpdate(query, options).first();
94
100
 
95
101
  return row ? mapConversationRow(row) : null;
96
102
  }
@@ -108,12 +114,13 @@ function createRepository(knex) {
108
114
  }
109
115
 
110
116
  const client = options?.trx || knex;
117
+ if (options.forUpdate === true) await findById(normalizedConversationId, options);
111
118
  const query = createConversationBaseQuery(client)
112
119
  .where("c.id", normalizedConversationId)
113
120
  .where("c.created_by_user_id", normalizedActorUserId)
114
121
  .where("c.surface_id", normalizedSurfaceId);
115
122
  applyWorkspaceScope(query, "c.workspace_id", workspaceId);
116
- const row = await query.first();
123
+ const row = await applyForUpdate(query, options).first();
117
124
 
118
125
  return row ? mapConversationRow(row) : null;
119
126
  }
@@ -140,6 +140,17 @@ function createRepository(knex) {
140
140
  return Number.isFinite(total) && total > 0 ? total : 0;
141
141
  }
142
142
 
143
+ async function updateById(messageId, patch = {}, options = {}) {
144
+ const id = normalizeInputRecordId(messageId);
145
+ if (!id) return null;
146
+ const client = options?.trx || knex;
147
+ const update = {};
148
+ if (Object.hasOwn(patch, "contentText")) update.content_text = patch.contentText == null ? null : String(patch.contentText);
149
+ if (Object.hasOwn(patch, "metadata")) update.metadata_json = stringifyJsonObject(patch.metadata);
150
+ if (Object.keys(update).length) await client(assistantRuntimeConfig.messagesTable).where({ id }).update(update);
151
+ return findById(id, { trx: client });
152
+ }
153
+
143
154
  async function listByConversationScope(conversationId, { workspaceId = null } = {}, pagination = {}, options = {}) {
144
155
  const normalizedConversationId = normalizeInputRecordId(conversationId);
145
156
  if (!normalizedConversationId) {
@@ -171,6 +182,7 @@ function createRepository(knex) {
171
182
  withTransaction,
172
183
  findById,
173
184
  create,
185
+ updateById,
174
186
  countByConversationScope,
175
187
  listByConversationScope,
176
188
  transaction