@jskit-ai/assistant-runtime 0.2.29 → 0.2.31

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 (38) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +6 -3
  3. package/package.json +16 -10
  4. package/patterns/assistant-surface/PATTERN.md +5 -2
  5. package/src/client/components/AssistantSettingsClientElement.vue +5 -3
  6. package/src/server/AssistantProvider.js +8 -13
  7. package/src/server/AssistantStoreFeature.js +59 -0
  8. package/src/server/actions.js +5 -5
  9. package/src/server/createAssistantRuntime.js +2 -1
  10. package/src/server/createAssistantSettingsActions.js +17 -9
  11. package/src/server/index.js +1 -0
  12. package/src/server/registerConversationSubscriptions.js +5 -2
  13. package/src/server/registerRoutes.js +11 -8
  14. package/src/server/registerSettingsRoutes.js +4 -4
  15. package/src/server/repositories/assistantConfigRepository.js +52 -67
  16. package/src/server/repositories/assistantResources.js +119 -0
  17. package/src/server/repositories/conversationsRepository.js +164 -147
  18. package/src/server/repositories/messagesRepository.js +136 -87
  19. package/src/server/repositories/repositoryPersistenceUtils.js +11 -22
  20. package/src/server/repositories/turnRequestsRepository.js +50 -16
  21. package/src/server/services/assistantConfigService.js +22 -0
  22. package/src/server/services/chatService.js +15 -7
  23. package/src/server/services/transcriptService.js +3 -3
  24. package/src/server/sqlConversationStorage.js +23 -19
  25. package/src/server/types.js +1 -0
  26. package/test/assistantIntegrationBoundary.test.js +2 -0
  27. package/test/assistantSettings.browser.test.js +23 -2
  28. package/test/assistantSettingsAccess.test.js +46 -1
  29. package/test/chatServiceLifecycle.test.js +32 -1
  30. package/test/featureRuntime.test.js +17 -3
  31. package/test/repositories.mysql.test.js +474 -0
  32. package/test/repositoryContracts.test.js +13 -13
  33. package/test/sqlConversationStorage.mysql.test.js +9 -6
  34. package/test/sqlConversationStorage.test.js +27 -20
  35. package/test/support/assistantStore.js +39 -0
  36. package/test/support/memoryAssistantRows.js +11 -4
  37. package/test/turnRequests.test.js +37 -14
  38. package/test/usersForeignKeysMigration.test.js +24 -0
package/CHANGELOG.md CHANGED
@@ -2,8 +2,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - The conversations and messages repositories hand their handle calls only the caller's transaction, with their own context and settings: a caller's other settings (a format, `missing`, `returning`, `fields`) no longer ride into every read and write they make. They take `passedOn` from `@jskit-ai/resource-core/shared/callOptions`, so `@jskit-ai/assistant-runtime/server` still loads on file storage, where `@jskit-ai/resource-runtime` (an optional peer) is not installed.
6
+ - Its table resources declare the foreign keys their migrations always create (`foreignKeys`), so json-rest-api 3.1.0 can skip the visibility query a system-context read makes for a record such a key guarantees exists. `jskit test --databases` checks each against an app's test database: assistant_conversations.created_by_user_id and assistant_turn_requests.actor_user_id to users, and assistant_messages.conversation_id to assistant_conversations and actor_user_id to users. The workspace_id keys exist only where the application has a workspaces table, so none declares them. `usersForeignKeysMigration.test.js` proves the declarations match the migrations.
7
+ - A database error in an assistant turn reaches the client only as the kernel's generic answer (`hideDatabaseError`: 409 `kernel.database.concurrentWrite` when the database refused the transaction for a concurrent one, else 500). A failure after the stream started was sent with its own message, which for a database error is the statement and its values, and the same message was saved with the turn and replayed, as an `AppError`, to a client that sent the message again; the chat route's error before the stream and a conversation subscription's acknowledgement carried the driver's code. Each now sends and saves the stand-in and logs the whole error; `createChatService` takes the runtime's `logger`.
8
+ - The assistant settings pane shows **Save** only to people the server lets change the settings. It offered Save to anyone who could read them, so a person without the permission (an employee, say) saw Save and was refused with `assistant.runtime.settings.updateForbidden` when they used it. The settings read now carries `canUpdate`, answered by the new `assistant.settings` service method `canUpdateSettings(input, { context })`, which runs the same check as `updateSettings` (an app's `assistantServer.<surface>.settingsUpdatePermission`, else `workspace.settings.update`, the console owner or `assistant.settings.update`) and answers false where that check refuses; a saved change carries `canUpdate: true`. Without it, the pane shows the instructions read-only, with no Save in the app bar. Apps change nothing; an app that supplies its own `assistant.settings` service adds `canUpdateSettings`.
9
+ - A conversation write through the SQL storage issues 8 statements on SQLite where it issued 12, whatever the conversation's length: the conversation and its transcript state come from one read, new messages take the next seq after the one the load read under the conversation's lock instead of reading it again, and one UPDATE writes the transcript state with the record's changes instead of an update, a read-back and a second update. A read issues 4 where it issued 5. `assistant.repositories`' conversations replace `readTranscriptState(id)` with `findWithTranscriptState(id, options)`, which answers `{ conversation, revision, summary }` (under a row lock with `forUpdate`), and `writeTranscriptState` takes the record's `changes` beside the revision and summary.
10
+ - `AssistantFeature` with neither `assistant.conversations` nor `AssistantStoreFeature` fails at startup naming `AssistantStoreFeature`, its import path and where to register it.
11
+ - `assistant.repositories`' messages gain `findNewestAssistantByUserId(userId, { kind }, options)`: the newest assistant message (latest `createdAt`, then highest id) in any of one user's conversations, in every workspace and surface, or null; `kind` narrows it, such as `"chat"` for a reply. It costs two reads for a user with up to 1,000 conversations. The messages table resource gains `role` and `kind` filters and a `conversationIds` list filter.
5
12
  - `@jskit-ai/assistant-runtime/client/providers/AssistantClientProvider` exports the client provider by itself, so the client bootstrap loads it without the assistant's elements and what they import.
6
13
  - `subscribeAssistantConversation()` takes `noticeMs`, how long an unanswered subscription waits before the loading notice. It stays ten seconds; tests pass a shorter one, so the real-socket proof of a slow subscription no longer waits ten seconds.
14
+ - Reads its operations from assistant-core's `assistantContract` and `assistantConfigContract`.
7
15
  - The assistant settings are a settings pane: the app bar shows their title and Save (a primary bar action that submits the form, with the card's loading and disabled states), and the card no longer has a Save row at its bottom. The button reads **Save** (`jskit.assistant.runtime.settings.save`, was "Save assistant settings"), since the bar's title names the settings. Visual only: what saving does is unchanged.
8
16
  - A browser test (`test/assistantSettings.browser.test.js`, fixture `fixtures/settings/`, run in CI) renders the settings pane in the shell's `ShellLayout` at 390 and 1280 px: the bar's title is the page's one `h1`, its **Save** is a submit button for `assistant-settings-form` that sends the edited prompt and shows its loading state until the save answers, a refused save shows the prompt's field message and reports the failure, and workspace settings show their note and no Save until the address names a workspace.
9
17
  - The assistant surface pattern's icon is `$smartToy`, and the fixtures render with `@jskit-ai/material-web`.
@@ -17,6 +25,7 @@
17
25
 
18
26
  ### Upgrading
19
27
 
28
+ - Register `AssistantStoreFeature` from `@jskit-ai/assistant-runtime/server` in the server providers beside `AssistantFeature`, unless the app provides `assistant.conversations` itself: the assistant's four tables are table resources that `AssistantStoreFeature` owns, and it provides the `assistant.repositories` the database-backed assistant keeps its conversations in, so `AssistantFeature` without it refuses to start, naming it.
20
29
  - Grant `assistant.settings.update` to the people who configure the assistant, or set `settingsUpdatePermission` to a permission they already hold.
21
30
 
22
31
  ## 0.2.0 - 2026-10-29
package/README.md CHANGED
@@ -12,14 +12,17 @@ Two hosting modes share that runtime:
12
12
  `assistant.conversations` capability. No database is required.
13
13
  - **Built-in database-backed surface.** The optional SQL integration adds
14
14
  database conversations, per-surface settings and the request-streaming chat
15
- route. It needs `npm run db:migrate` and is composed by the
16
- `assistant/assistant-surface` pattern.
15
+ route. The application registers `AssistantStoreFeature` in its server
16
+ feature list and installs `@jskit-ai/resource-runtime`: the feature owns the
17
+ assistant's four tables as table resources and provides their repositories as
18
+ `assistant.repositories`, which `AssistantFeature` uses. It needs
19
+ `npm run db:migrate` and is composed by the `assistant/assistant-surface` pattern.
17
20
 
18
21
  Main exports by subpath:
19
22
 
20
23
  | Subpath | Provides |
21
24
  | --- | --- |
22
- | `@jskit-ai/assistant-runtime/server` | `AssistantFeature`, `createAssistantActions`, `registerAssistantRoutes`, `registerConversationSubscriptions`, `registerFastifyConversations`, `createSurfaceAwareToolCatalog` |
25
+ | `@jskit-ai/assistant-runtime/server` | `AssistantFeature`, `AssistantStoreFeature`, `createAssistantActions`, `registerAssistantRoutes`, `registerConversationSubscriptions`, `registerFastifyConversations`, `createSurfaceAwareToolCatalog` |
23
26
  | `@jskit-ai/assistant-runtime/server/actionIds` | Assistant action identifiers |
24
27
  | `@jskit-ai/assistant-runtime/client` | `AssistantConversationClientElement`, `AssistantSurfaceClientElement`, `AssistantSettingsClientElement`, `useAssistantConversation`, `configureAssistantConversations` |
25
28
  | `@jskit-ai/assistant-runtime/shared` | Assistant surface configuration helpers |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-runtime",
3
- "version": "0.2.29",
3
+ "version": "0.2.31",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "jskit test"
@@ -15,19 +15,21 @@
15
15
  "./locales/en.json": "./locales/en.json"
16
16
  },
17
17
  "dependencies": {
18
- "@jskit-ai/assistant-core": "0.2.27",
18
+ "@jskit-ai/assistant-core": "0.2.29",
19
+ "@jskit-ai/resource-core": "0.1.206",
19
20
  "json-rest-schema": "^1.0.17"
20
21
  },
21
22
  "peerDependencies": {
22
- "@jskit-ai/http-runtime": "0.2.13",
23
- "@jskit-ai/http-web": "0.2.21",
24
- "@jskit-ai/kernel": "0.2.13",
25
- "@jskit-ai/realtime": "0.2.19",
26
- "@jskit-ai/shell-web": "0.2.16",
23
+ "@jskit-ai/http-runtime": "0.2.15",
24
+ "@jskit-ai/http-web": "0.2.23",
25
+ "@jskit-ai/kernel": "0.2.15",
26
+ "@jskit-ai/realtime": "0.2.21",
27
+ "@jskit-ai/shell-web": "0.2.18",
27
28
  "@tanstack/vue-query": "^5.90.5",
28
29
  "vue": "^3.5.13",
29
30
  "vuetify": "^4.0.0",
30
- "@jskit-ai/database-runtime": "0.1.262"
31
+ "@jskit-ai/database-runtime": "0.1.264",
32
+ "@jskit-ai/resource-runtime": "0.2.13"
31
33
  },
32
34
  "description": "Capability-based assistant runtime with per-surface chat, transcripts, settings, and action tools.",
33
35
  "jskit": {
@@ -103,7 +105,7 @@
103
105
  },
104
106
  {
105
107
  "subpath": "./server",
106
- "summary": "Provides conversation routes, authorized shared realtime subscriptions, explicit Fastify hosting shorthand, optional database integration, the per-surface assistant tool catalogue and the person-context prompt a host composes into its conversations' instructions."
108
+ "summary": "Provides conversation routes, authorized shared realtime subscriptions, explicit Fastify hosting shorthand, optional database integration (AssistantStoreFeature owns its tables through runtime.resources and provides assistant.repositories), the per-surface assistant tool catalogue and the person-context prompt a host composes into its conversations' instructions."
107
109
  },
108
110
  {
109
111
  "subpath": "./server/types",
@@ -130,9 +132,13 @@
130
132
  },
131
133
  "@jskit-ai/database-runtime": {
132
134
  "optional": true
135
+ },
136
+ "@jskit-ai/resource-runtime": {
137
+ "optional": true
133
138
  }
134
139
  },
135
140
  "devDependencies": {
136
- "@jskit-ai/material-web": "0.1.3"
141
+ "@jskit-ai/material-web": "0.1.3",
142
+ "@jskit-ai/resource-runtime": "0.2.13"
137
143
  }
138
144
  }
@@ -48,8 +48,11 @@ in source or a questionnaire transcript.
48
48
 
49
49
  Use `AssistantSurfaceClientElement` and `AssistantSettingsClientElement` from
50
50
  `@jskit-ai/assistant-runtime/client`. Install the runtime package through the
51
- normal package-capability path. Configure `assistantSurfaces` in public config
52
- and `assistantServer` in server config.
51
+ normal package-capability path. Install `@jskit-ai/resource-runtime` and
52
+ register `AssistantStoreFeature` from `@jskit-ai/assistant-runtime/server` in
53
+ the ordinary server feature list; it owns the assistant's tables and provides the
54
+ repositories the database-backed surface uses. Configure `assistantSurfaces` in
55
+ public config and `assistantServer` in server config.
53
56
 
54
57
  ## Example files
55
58
 
@@ -41,7 +41,7 @@ import { getClientAppConfig } from "@jskit-ai/kernel/client";
41
41
  import { normalizeObject, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
42
42
  import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
43
43
  import { assistantHttpClient, createAssistantApi, AssistantSettingsFormCard } from "@jskit-ai/assistant-core/client";
44
- import { assistantConfigResource, assistantSettingsQueryKey, buildAssistantApiPath } from "@jskit-ai/assistant-core/shared";
44
+ import { assistantConfigContract, assistantSettingsQueryKey, buildAssistantApiPath } from "@jskit-ai/assistant-core/shared";
45
45
  import ShellPageAction from "@jskit-ai/shell-web/client/components/ShellPageAction";
46
46
  import ShellPageActions from "@jskit-ai/shell-web/client/components/ShellPageActions";
47
47
  import { useShellPageTitle } from "@jskit-ai/shell-web/client/composables/useShellPageTitle";
@@ -195,7 +195,7 @@ async function submit() {
195
195
  clearFieldErrors();
196
196
 
197
197
  const validation = validateOperationSection({
198
- operation: assistantConfigResource.operations.patch,
198
+ operation: assistantConfigContract.operations.patch,
199
199
  section: "body",
200
200
  value: {
201
201
  systemPrompt: form.systemPrompt
@@ -221,7 +221,9 @@ async function submit() {
221
221
 
222
222
  const addEdit = computed(() => ({
223
223
  canView: Boolean(assistantSurface.value) && hasScope.value,
224
- canSave: Boolean(assistantSurface.value) && hasScope.value,
224
+ // The server says whether this person may change the settings (it checks the same permission when they save); anyone
225
+ // else sees them read-only, without Save.
226
+ canSave: Boolean(assistantSurface.value) && hasScope.value && settingsQuery.data.value?.canUpdate === true,
225
227
  loadError: loadError.value,
226
228
  isInitialLoading: Boolean(settingsQuery.isLoading.value),
227
229
  isRefetching: Boolean(settingsQuery.isFetching.value && !settingsQuery.isLoading.value),
@@ -51,7 +51,7 @@ const AssistantFeature = defineFeature({
51
51
  conversationRuntime: "assistant.conversations",
52
52
  conversationAccess: "assistant.conversation.access",
53
53
  assistantConfigService: "assistant.settings",
54
- database: "runtime.database",
54
+ repositories: "assistant.repositories",
55
55
  realtime: "runtime.realtime",
56
56
  aiConnections: "integrations.ai",
57
57
  attachments: "assistant.attachments",
@@ -77,14 +77,14 @@ const AssistantFeature = defineFeature({
77
77
  // composes assistant.personContext into them itself, with createPersonContextPrompt.
78
78
  assistant = createHostedAssistant({ conversationRuntime, assistantConfigService: assistantConfigService || null });
79
79
  } else {
80
- if (!dependencies.database?.knex) {
81
- throw new Error("AssistantFeature requires assistant.conversations, or runtime.database for database integration.");
80
+ // The database-backed assistant keeps its rows through AssistantStoreFeature's repositories.
81
+ if (!dependencies.repositories) {
82
+ throw new Error(
83
+ "AssistantFeature keeps conversations in assistant.repositories, which no feature provides: register AssistantStoreFeature " +
84
+ "from @jskit-ai/assistant-runtime/server in your server providers, or provide assistant.conversations (createConversationRuntime())."
85
+ );
82
86
  }
83
87
  const { createAssistantRuntime } = await import("./createAssistantRuntime.js");
84
- const { createRepository: createAssistantConfigRepository } = await import("./repositories/assistantConfigRepository.js");
85
- const { createRepository: createConversationsRepository } = await import("./repositories/conversationsRepository.js");
86
- const { createRepository: createMessagesRepository } = await import("./repositories/messagesRepository.js");
87
- const { createRepository: createTurnRequestsRepository } = await import("./repositories/turnRequestsRepository.js");
88
88
  assistant = createAssistantRuntime({
89
89
  actionCatalogue,
90
90
  config: dependencies.config,
@@ -95,12 +95,7 @@ const AssistantFeature = defineFeature({
95
95
  personContext: dependencies.personContext ?? NO_PERSON_CONTEXT,
96
96
  logger: dependencies.logger,
97
97
  workspaces: dependencies.workspaces || null,
98
- repositories: {
99
- config: createAssistantConfigRepository(dependencies.database.knex),
100
- conversations: createConversationsRepository(dependencies.database.knex),
101
- messages: createMessagesRepository(dependencies.database.knex),
102
- turnRequests: createTurnRequestsRepository(dependencies.database.knex)
103
- }
98
+ repositories: dependencies.repositories
104
99
  });
105
100
  }
106
101
  registerRoutes(dependencies.http.router, {
@@ -0,0 +1,59 @@
1
+ import { defineFeature } from "@jskit-ai/kernel/server/features";
2
+ import {
3
+ assistantConfigResource,
4
+ assistantConversationsResource,
5
+ assistantMessagesResource,
6
+ assistantTurnRequestsResource
7
+ } from "./repositories/assistantResources.js";
8
+ import { createRepository as createConfigRepository } from "./repositories/assistantConfigRepository.js";
9
+ import { createRepository as createConversationsRepository } from "./repositories/conversationsRepository.js";
10
+ import { createRepository as createMessagesRepository } from "./repositories/messagesRepository.js";
11
+ import { createRepository as createTurnRequestsRepository } from "./repositories/turnRequestsRepository.js";
12
+
13
+ /**
14
+ * The repositories of the database-backed assistant over the tables this feature owns.
15
+ * @param {object} dependencies
16
+ * @param {Record<string, import("@jskit-ai/resource-runtime/server/types").ResourceHandle>} dependencies.resources
17
+ * @param {(reason: string) => import("@jskit-ai/resource-runtime/server/types").SystemContext} dependencies.systemContext
18
+ */
19
+ function createAssistantRepositories({ resources, systemContext }) {
20
+ return Object.freeze({
21
+ config: createConfigRepository({ assistantConfig: resources.assistantConfig, systemContext }),
22
+ conversations: createConversationsRepository({ conversations: resources.assistantConversations, systemContext }),
23
+ messages: createMessagesRepository({
24
+ messages: resources.assistantMessages,
25
+ conversations: resources.assistantConversations,
26
+ systemContext
27
+ }),
28
+ turnRequests: createTurnRequestsRepository({ turnRequests: resources.assistantTurnRequests, systemContext })
29
+ });
30
+ }
31
+
32
+ // The database-backed assistant's tables: settings, conversations, messages and
33
+ // request receipts. An application that keeps its assistant in the database
34
+ // registers this feature beside AssistantFeature, which takes its repositories;
35
+ // an application that supplies assistant.conversations does not.
36
+ const AssistantStoreFeature = defineFeature({
37
+ id: "assistant.store",
38
+ domain: "assistant",
39
+ owns: [
40
+ assistantConfigResource,
41
+ assistantConversationsResource,
42
+ assistantMessagesResource,
43
+ assistantTurnRequestsResource
44
+ ],
45
+ systemScope: "the assistant reads and writes its rows by the actor, workspace and surface its own access checks resolved, and keeps global settings that every workspace shares",
46
+ provides: {
47
+ repositories: "assistant.repositories"
48
+ },
49
+ /**
50
+ * @param {object} dependencies
51
+ * @param {Record<string, import("@jskit-ai/resource-runtime/server/types").ResourceHandle>} dependencies.resources
52
+ * @param {(reason: string) => import("@jskit-ai/resource-runtime/server/types").SystemContext} dependencies.systemContext
53
+ */
54
+ setup({ resources, systemContext }) {
55
+ return { repositories: createAssistantRepositories({ resources, systemContext }) };
56
+ }
57
+ });
58
+
59
+ export { AssistantStoreFeature };
@@ -7,7 +7,7 @@ import {
7
7
  } from "@jskit-ai/kernel/shared/support/deepFreeze";
8
8
  import { returnJsonApiData } from "@jskit-ai/http-runtime/shared";
9
9
  import {
10
- assistantResource
10
+ assistantContract
11
11
  } from "@jskit-ai/assistant-core/shared";
12
12
  import { actionIds } from "./actionIds.js";
13
13
  import { createAssistantSettingsActions } from "./createAssistantSettingsActions.js";
@@ -40,7 +40,7 @@ const runtimeConversationsListQueryInputValidator = deepFreeze({
40
40
  query: {
41
41
  type: "object",
42
42
  required: false,
43
- schema: assistantResource.operations.conversationsList.query.schema
43
+ schema: assistantContract.operations.conversationsList.query.schema
44
44
  }
45
45
  }),
46
46
  mode: "patch"
@@ -59,7 +59,7 @@ const runtimeConversationMessagesListQueryInputValidator = deepFreeze({
59
59
  query: {
60
60
  type: "object",
61
61
  required: false,
62
- schema: assistantResource.operations.conversationMessagesList.query.schema
62
+ schema: assistantContract.operations.conversationMessagesList.query.schema
63
63
  }
64
64
  }),
65
65
  mode: "patch"
@@ -68,7 +68,7 @@ const runtimeConversationMessagesListQueryInputValidator = deepFreeze({
68
68
  const runtimeConversationMessagesListInputValidator = composeSchemaDefinitions(
69
69
  [
70
70
  assistantTargetSurfaceInputValidator,
71
- assistantResource.operations.conversationMessagesList.params,
71
+ assistantContract.operations.conversationMessagesList.params,
72
72
  runtimeConversationMessagesListQueryInputValidator
73
73
  ],
74
74
  {
@@ -80,7 +80,7 @@ const runtimeConversationMessagesListInputValidator = composeSchemaDefinitions(
80
80
  const runtimeChatStreamInputValidator = composeSchemaDefinitions(
81
81
  [
82
82
  assistantTargetSurfaceInputValidator,
83
- assistantResource.operations.chatStream.body
83
+ assistantContract.operations.chatStream.body
84
84
  ],
85
85
  {
86
86
  mode: "create",
@@ -129,7 +129,8 @@ function createAssistantRuntime({
129
129
  assistantConfigService: configService,
130
130
  personContextPrompt: createPersonContextPrompt({ personContext, logger }),
131
131
  appConfig: config,
132
- workspaceScopeSupport
132
+ workspaceScopeSupport,
133
+ logger
133
134
  });
134
135
 
135
136
  return Object.freeze({
@@ -2,7 +2,7 @@ import { createSchema } from "json-rest-schema";
2
2
  import { composeSchemaDefinitions } from "@jskit-ai/kernel/shared/validators";
3
3
  import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
4
4
  import { returnJsonApiData } from "@jskit-ai/http-runtime/shared";
5
- import { assistantConfigResource } from "@jskit-ai/assistant-core/shared";
5
+ import { assistantConfigContract } from "@jskit-ai/assistant-core/shared";
6
6
  import { actionIds } from "./actionIds.js";
7
7
  import { assistantTargetSurfaceInputValidator } from "./inputSchemas.js";
8
8
 
@@ -15,7 +15,7 @@ const settingsUpdatePatchInputValidator = deepFreeze({
15
15
  patch: {
16
16
  type: "object",
17
17
  required: true,
18
- schema: assistantConfigResource.operations.patch.body.schema
18
+ schema: assistantConfigContract.operations.patch.body.schema
19
19
  }
20
20
  }),
21
21
  mode: "patch"
@@ -30,27 +30,35 @@ const settingsUpdateInputValidator = composeSchemaDefinitions(
30
30
  );
31
31
 
32
32
  /**
33
- * The JSON:API document of a surface's assistant settings.
33
+ * The JSON:API document of a surface's assistant settings, with whether the reader may change them (`canUpdate`).
34
34
  * @param {AssistantConfigService} assistantConfigService
35
35
  * @param {object} input
36
36
  * @param {object} context
37
37
  */
38
38
  async function readSettingsDocument(assistantConfigService, input, context) {
39
- return returnJsonApiData(await assistantConfigService.getSettings(input, {
39
+ const settings = await assistantConfigService.getSettings(input, {
40
40
  context
41
- }));
41
+ });
42
+ const canUpdate = await assistantConfigService.canUpdateSettings(input, {
43
+ context
44
+ });
45
+ return returnJsonApiData({ ...settings, canUpdate });
42
46
  }
43
47
 
44
48
  /**
45
- * The JSON:API document of a surface's assistant settings after the input's patch.
49
+ * The JSON:API document of a surface's assistant settings after the input's patch, which only someone who may change
50
+ * them gets.
46
51
  * @param {AssistantConfigService} assistantConfigService
47
52
  * @param {object} input
48
53
  * @param {object} context
49
54
  */
50
55
  async function updateSettingsDocument(assistantConfigService, input, context) {
51
- return returnJsonApiData(await assistantConfigService.updateSettings(input, input.patch, {
52
- context
53
- }));
56
+ return returnJsonApiData({
57
+ ...(await assistantConfigService.updateSettings(input, input.patch, {
58
+ context
59
+ })),
60
+ canUpdate: true
61
+ });
54
62
  }
55
63
 
56
64
  /**
@@ -1,4 +1,5 @@
1
1
  export { AssistantFeature } from "./AssistantProvider.js";
2
+ export { AssistantStoreFeature } from "./AssistantStoreFeature.js";
2
3
  export { createAssistantActions } from "./actions.js";
3
4
  export { assistantConversationGoalBodyValidator } from "./inputSchemas.js";
4
5
  export { registerRoutes as registerAssistantRoutes } from "./registerRoutes.js";
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { resolveActorId } from "@jskit-ai/kernel/server/actions/actionContributorHelpers";
3
3
  import { attachDirectRequestActionExecutor } from "@jskit-ai/kernel/server/http";
4
- import { AppError } from "@jskit-ai/kernel/server/runtime";
4
+ import { AppError, hideDatabaseError } from "@jskit-ai/kernel/server/runtime";
5
5
  import { silentLogger } from "@jskit-ai/kernel/server/runtime/logger";
6
6
  import { actionIds } from "./actionIds.js";
7
7
  import { resolveRouteRequestState } from "./support/assistantRouteContext.js";
@@ -210,7 +210,10 @@ function registerConversationSubscriptions({ realtime, events, actions, config =
210
210
  acknowledge({ ok: true, subscriptionId, streamEpoch: subscription.epoch, state });
211
211
  // Sending an acknowledgement does not prove that a timed-out client received it.
212
212
  onStage("acknowledgement-sent");
213
- } catch (error) {
213
+ } catch (thrown) {
214
+ // A database error is acknowledged as its stand-in (a generic 409 or 500), and its detail goes to the log.
215
+ const error = hideDatabaseError(thrown);
216
+ if (error !== thrown) logger.error({ err: thrown }, "Assistant conversation subscription failed on a database error.");
214
217
  onStage("failed", { code: String(error?.code || "assistant_subscription_failed"),
215
218
  status: errorStatus(error) });
216
219
  if (subscriptions.get(subscriptionId) === subscription) release(subscriptionId);
@@ -1,11 +1,11 @@
1
- import { AppError } from "@jskit-ai/kernel/server/runtime";
1
+ import { AppError, hideDatabaseError } from "@jskit-ai/kernel/server/runtime";
2
2
  import { composeSchemaDefinitions } from "@jskit-ai/kernel/shared/validators";
3
3
  import { createJsonApiResourceRouteContract } from "@jskit-ai/http-runtime/shared/validators/jsonApiRouteTransport";
4
4
  import {
5
5
  ASSISTANT_CONVERSATIONS_TRANSPORT,
6
6
  ASSISTANT_CONVERSATION_MESSAGES_TRANSPORT,
7
7
  assistantConversationOutputValidator,
8
- assistantResource,
8
+ assistantContract,
9
9
  resolveAssistantApiBasePath
10
10
  } from "@jskit-ai/assistant-core/shared";
11
11
  import {
@@ -109,7 +109,7 @@ function registerRuntimeRoutes(
109
109
  ? requireWorkspaceAssistantRouteParams(workspaceScopeSupport)
110
110
  : assistantSurfaceRouteParamsValidator;
111
111
  const conversationMessagesParams = composeSchemaDefinitions(
112
- [params, assistantResource.operations.conversationMessagesList.params],
112
+ [params, assistantContract.operations.conversationMessagesList.params],
113
113
  {
114
114
  mode: "patch",
115
115
  context: requiresWorkspace === true
@@ -129,7 +129,7 @@ function registerRuntimeRoutes(
129
129
  tags: ["assistant"],
130
130
  summary: "Stream assistant response."
131
131
  },
132
- body: assistantResource.operations.chatStream.body
132
+ body: assistantContract.operations.chatStream.body
133
133
  },
134
134
  async function assistantChatStreamRoute(request, reply) {
135
135
  const routeState = resolveRouteRequestState(request, {
@@ -188,7 +188,10 @@ function registerRuntimeRoutes(
188
188
  }
189
189
 
190
190
  reply.code(204).send();
191
- } catch (error) {
191
+ } catch (thrown) {
192
+ // A database error is answered by its stand-in (a generic 409 or 500), and its detail goes to the log.
193
+ const error = hideDatabaseError(thrown);
194
+ if (error !== thrown) request.log.error({ err: thrown }, "Assistant chat stream failed on a database error.");
192
195
  if (!streamStarted) {
193
196
  sendPreStreamErrorResponse(reply, error);
194
197
  return;
@@ -223,7 +226,7 @@ function registerRuntimeRoutes(
223
226
  },
224
227
  ...createJsonApiResourceRouteContract({
225
228
  ...ASSISTANT_CONVERSATIONS_TRANSPORT,
226
- query: assistantResource.operations.conversationsList.query,
229
+ query: assistantContract.operations.conversationsList.query,
227
230
  output: assistantConversationOutputValidator,
228
231
  outputKind: "collection"
229
232
  })
@@ -262,8 +265,8 @@ function registerRuntimeRoutes(
262
265
  },
263
266
  ...createJsonApiResourceRouteContract({
264
267
  ...ASSISTANT_CONVERSATION_MESSAGES_TRANSPORT,
265
- query: assistantResource.operations.conversationMessagesList.query,
266
- output: assistantResource.operations.conversationMessagesList.output,
268
+ query: assistantContract.operations.conversationMessagesList.query,
269
+ output: assistantContract.operations.conversationMessagesList.output,
267
270
  outputKind: "record",
268
271
  getRecordId: resolveAssistantConversationMessagesRecordId
269
272
  })
@@ -2,7 +2,7 @@ import { createJsonApiResourceRouteContract } from "@jskit-ai/http-runtime/share
2
2
  import {
3
3
  ASSISTANT_SETTINGS_UPDATE_TRANSPORT,
4
4
  ASSISTANT_SETTINGS_TRANSPORT,
5
- assistantConfigResource,
5
+ assistantConfigContract,
6
6
  resolveAssistantApiBasePath
7
7
  } from "@jskit-ai/assistant-core/shared";
8
8
  import { actionIds } from "./actionIds.js";
@@ -45,7 +45,7 @@ function registerSettingsRoutes(
45
45
  },
46
46
  ...createJsonApiResourceRouteContract({
47
47
  ...ASSISTANT_SETTINGS_TRANSPORT,
48
- output: assistantConfigResource.operations.view.output,
48
+ output: assistantConfigContract.operations.view.output,
49
49
  outputKind: "record",
50
50
  getRecordId: resolveAssistantSettingsRecordId
51
51
  })
@@ -81,8 +81,8 @@ function registerSettingsRoutes(
81
81
  },
82
82
  ...createJsonApiResourceRouteContract({
83
83
  ...ASSISTANT_SETTINGS_UPDATE_TRANSPORT,
84
- body: assistantConfigResource.operations.patch.body,
85
- output: assistantConfigResource.operations.patch.output,
84
+ body: assistantConfigContract.operations.patch.body,
85
+ output: assistantConfigContract.operations.patch.output,
86
86
  outputKind: "record",
87
87
  getRecordId: resolveAssistantSettingsRecordId,
88
88
  includeValidation400: true