@jskit-ai/assistant-runtime 0.2.8 → 0.2.10

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 (69) hide show
  1. package/README.md +1 -1
  2. package/fixtures/canonical-conversation/main.js +2 -1
  3. package/fixtures/canonical-conversation/vite.config.mjs +11 -0
  4. package/fixtures/conversation/main.js +2 -1
  5. package/fixtures/conversation/vite.config.mjs +4 -3
  6. package/locales/en.json +1 -0
  7. package/migrations/assistant_config_initial.cjs +1 -5
  8. package/migrations/assistant_transcripts_initial.cjs +3 -9
  9. package/migrations/assistant_transcripts_summary.cjs +28 -0
  10. package/migrations/assistant_turn_requests.cjs +2 -1
  11. package/migrations/constraints/assistant_users_foreign_keys.cjs +22 -0
  12. package/migrations/constraints/assistant_workspaces_foreign_keys.cjs +26 -0
  13. package/package.json +17 -10
  14. package/patterns/assistant-surface/example/src/ApplicationAiFeature.js +4 -2
  15. package/src/client/components/AssistantConversationClientElement.vue +10 -4
  16. package/src/client/components/AssistantSurfaceClientElement.vue +8 -4
  17. package/src/client/composables/conversationGoal.js +266 -0
  18. package/src/client/composables/conversationHistory.js +215 -0
  19. package/src/client/composables/conversationRuntime.js +624 -0
  20. package/src/client/composables/useAssistantConversation.js +208 -840
  21. package/src/client/composables/useAssistantRuntime.js +60 -45
  22. package/src/client/providers/AssistantClientProvider.js +1 -0
  23. package/src/client/support/assistantRuntimeState.js +6 -34
  24. package/src/client/support/conversationFailures.js +30 -0
  25. package/src/client/support/conversationRestoreSupport.js +13 -5
  26. package/src/client/support/subscribeAssistantConversation.js +32 -9
  27. package/src/client/support/workspaceScopeSupport.js +13 -22
  28. package/src/client/types.js +46 -0
  29. package/src/server/AssistantProvider.js +45 -17
  30. package/src/server/actions.js +162 -201
  31. package/src/server/conversationOperations.js +141 -0
  32. package/src/server/createAssistantRuntime.js +83 -44
  33. package/src/server/createAssistantSettingsActions.js +69 -47
  34. package/src/server/hostedAssistant.js +21 -0
  35. package/src/server/index.js +1 -0
  36. package/src/server/registerConversationSubscriptions.js +34 -11
  37. package/src/server/registerFastifyConversations.js +18 -6
  38. package/src/server/registerRoutes.js +5 -2
  39. package/src/server/registerSettingsRoutes.js +1 -1
  40. package/src/server/repositories/assistantConfigRepository.js +10 -20
  41. package/src/server/repositories/conversationsRepository.js +35 -10
  42. package/src/server/repositories/messagesRepository.js +14 -10
  43. package/src/server/repositories/repositoryPersistenceUtils.js +1 -6
  44. package/src/server/repositories/turnRequestsRepository.js +6 -1
  45. package/src/server/services/assistantConfigService.js +25 -23
  46. package/src/server/services/chatService.js +57 -61
  47. package/src/server/services/transcriptService.js +15 -7
  48. package/src/server/sqlConversationStorage.js +90 -18
  49. package/src/server/support/assistantServerConfig.js +1 -1
  50. package/src/server/support/assistantStreamWriter.js +7 -1
  51. package/src/server/support/createSurfaceAwareToolCatalog.js +35 -17
  52. package/src/server/types.js +38 -0
  53. package/src/shared/assistantSurfaces.js +2 -2
  54. package/test/assistantConversationBinding.browser.test.js +115 -1
  55. package/test/assistantConversationOperations.test.js +5 -5
  56. package/test/assistantRuntimeState.test.js +1 -16
  57. package/test/chatServiceLifecycle.test.js +62 -154
  58. package/test/createSurfaceAwareToolCatalog.test.js +5 -0
  59. package/test/featureRuntime.test.js +12 -6
  60. package/test/lazyAppConfig.test.js +3 -5
  61. package/test/packageMetadata.test.js +7 -1
  62. package/test/sqlConversationStorage.mysql.test.js +89 -0
  63. package/test/sqlConversationStorage.test.js +161 -16
  64. package/test/support/memoryAssistantRows.js +6 -1
  65. package/test/support/scriptedModel.js +32 -0
  66. package/test/turnRequests.test.js +4 -2
  67. package/test/usersForeignKeysMigration.test.js +46 -0
  68. package/test/workspacesForeignKeysMigration.test.js +66 -0
  69. package/src/client/support/accessDenied.js +0 -6
@@ -5,7 +5,6 @@ import {
5
5
  applyWorkspaceScope,
6
6
  normalizeDbWorkspaceId,
7
7
  normalizeInputRecordId,
8
- normalizeWorkspaceId,
9
8
  parseJsonObject,
10
9
  resolveInsertedId,
11
10
  stringifyJsonObject,
@@ -58,10 +57,8 @@ function createConversationBaseQuery(client) {
58
57
  return client(`${assistantRuntimeConfig.conversationsTable} as c`).select("c.*");
59
58
  }
60
59
 
60
+ /** @param {import("@jskit-ai/database-runtime/server/types").RuntimeKnex} knex */
61
61
  function createRepository(knex) {
62
- if (!knex || typeof knex !== "function") {
63
- throw new Error("createConversationsRepository requires knex client.");
64
- }
65
62
  const withTransaction = createWithTransaction(knex);
66
63
 
67
64
  async function findById(conversationId, options = {}) {
@@ -70,7 +67,7 @@ function createRepository(knex) {
70
67
  return null;
71
68
  }
72
69
 
73
- const client = options?.trx || knex;
70
+ const client = options.trx || knex;
74
71
  if (options.forUpdate === true) {
75
72
  if (!options.trx) throw new TypeError("Locking a conversation requires its database transaction.");
76
73
  // Acquire the write reservation before reading, including SQLite where
@@ -96,7 +93,7 @@ function createRepository(knex) {
96
93
  return null;
97
94
  }
98
95
 
99
- const client = options?.trx || knex;
96
+ const client = options.trx || knex;
100
97
  if (options.forUpdate === true) await findById(normalizedConversationId, options);
101
98
  const query = createConversationBaseQuery(client)
102
99
  .where("c.id", normalizedConversationId)
@@ -109,11 +106,11 @@ function createRepository(knex) {
109
106
  }
110
107
 
111
108
  async function create(payload = {}, options = {}) {
112
- const client = options?.trx || knex;
109
+ const client = options.trx || knex;
113
110
  const now = new Date();
114
111
  const surfaceId = normalizeRequiredSurfaceId(payload.surfaceId);
115
112
  const insertResult = await client(assistantRuntimeConfig.conversationsTable).insert({
116
- workspace_id: normalizeWorkspaceId(payload.workspaceId),
113
+ workspace_id: normalizeInputRecordId(payload.workspaceId),
117
114
  created_by_user_id: normalizeInputRecordId(payload.createdByUserId),
118
115
  title: normalizeText(payload.title) || DEFAULT_CONVERSATION_TITLE,
119
116
  status: normalizeText(payload.status).toLowerCase() || "active",
@@ -143,7 +140,7 @@ function createRepository(knex) {
143
140
  return null;
144
141
  }
145
142
 
146
- const client = options?.trx || knex;
143
+ const client = options.trx || knex;
147
144
  const updatePatch = {};
148
145
 
149
146
  if (Object.hasOwn(patch, "title")) {
@@ -183,6 +180,32 @@ function createRepository(knex) {
183
180
  });
184
181
  }
185
182
 
183
+ // The transcript's own state stays out of conversation records, which reach the browser.
184
+ async function readTranscriptState(conversationId, options = {}) {
185
+ const id = normalizeInputRecordId(conversationId);
186
+ if (!id) return null;
187
+ const client = options.trx || knex;
188
+ const row = await client(assistantRuntimeConfig.conversationsTable).where({ id })
189
+ .first("summary_text", "summary_through_turn_id", "transcript_revision");
190
+ if (!row) return null;
191
+ return {
192
+ revision: row.transcript_revision || "",
193
+ summary: row.summary_text == null ? null
194
+ : { text: String(row.summary_text), throughTurnId: String(row.summary_through_turn_id || "") }
195
+ };
196
+ }
197
+
198
+ async function writeTranscriptState(conversationId, { revision, summary }, options = {}) {
199
+ const id = normalizeInputRecordId(conversationId);
200
+ if (!id) throw new TypeError("writeTranscriptState requires conversationId.");
201
+ const client = options.trx || knex;
202
+ await client(assistantRuntimeConfig.conversationsTable).where({ id }).update({
203
+ transcript_revision: revision,
204
+ summary_text: summary ? summary.text : null,
205
+ summary_through_turn_id: summary ? summary.throughTurnId : null
206
+ });
207
+ }
208
+
186
209
  async function listForActorScope(
187
210
  { workspaceId = null, actorUserId = null, surfaceId = "", pagination = {}, filters = {} } = {},
188
211
  options = {}
@@ -196,7 +219,7 @@ function createRepository(knex) {
196
219
  };
197
220
  }
198
221
 
199
- const client = options?.trx || knex;
222
+ const client = options.trx || knex;
200
223
  const { cursor, limit } = normalizeCursorPagination(pagination);
201
224
  let query = createConversationBaseQuery(client)
202
225
  .where("c.created_by_user_id", normalizedActorUserId)
@@ -239,6 +262,8 @@ function createRepository(knex) {
239
262
  create,
240
263
  updateById,
241
264
  listForActorScope,
265
+ readTranscriptState,
266
+ writeTranscriptState,
242
267
  transaction
243
268
  });
244
269
  }
@@ -4,7 +4,6 @@ import {
4
4
  applyWorkspaceScope,
5
5
  normalizeDbWorkspaceId,
6
6
  normalizeInputRecordId,
7
- normalizeWorkspaceId,
8
7
  parseJsonObject,
9
8
  resolveInsertedId,
10
9
  stringifyJsonObject,
@@ -55,10 +54,8 @@ async function resolveNextSequence(client, conversationId) {
55
54
  return maxSeq + 1;
56
55
  }
57
56
 
57
+ /** @param {import("@jskit-ai/database-runtime/server/types").RuntimeKnex} knex */
58
58
  function createRepository(knex) {
59
- if (!knex || typeof knex !== "function") {
60
- throw new Error("createMessagesRepository requires knex client.");
61
- }
62
59
  const withTransaction = createWithTransaction(knex);
63
60
 
64
61
  async function findById(messageId, options = {}) {
@@ -67,7 +64,7 @@ function createRepository(knex) {
67
64
  return null;
68
65
  }
69
66
 
70
- const client = options?.trx || knex;
67
+ const client = options.trx || knex;
71
68
  const row = await client(assistantRuntimeConfig.messagesTable)
72
69
  .where({ id: normalizedMessageId })
73
70
  .first();
@@ -76,7 +73,7 @@ function createRepository(knex) {
76
73
  }
77
74
 
78
75
  async function create(payload = {}, options = {}) {
79
- const client = options?.trx || knex;
76
+ const client = options.trx || knex;
80
77
  const conversationId = normalizeInputRecordId(payload.conversationId);
81
78
  if (!conversationId) {
82
79
  throw new TypeError("messagesRepository.create requires conversationId.");
@@ -86,7 +83,7 @@ function createRepository(knex) {
86
83
  const seq = Number.isInteger(providedSeq) && providedSeq > 0 ? providedSeq : (await resolveNextSequence(client, conversationId));
87
84
  const insertResult = await client(assistantRuntimeConfig.messagesTable).insert({
88
85
  conversation_id: conversationId,
89
- workspace_id: normalizeWorkspaceId(payload.workspaceId),
86
+ workspace_id: normalizeInputRecordId(payload.workspaceId),
90
87
  seq,
91
88
  role: normalizeText(payload.role).toLowerCase(),
92
89
  kind: normalizeText(payload.kind).toLowerCase() || "chat",
@@ -112,7 +109,7 @@ function createRepository(knex) {
112
109
  return 0;
113
110
  }
114
111
 
115
- const client = options?.trx || knex;
112
+ const client = options.trx || knex;
116
113
  const query = client(assistantRuntimeConfig.messagesTable).where({
117
114
  conversation_id: normalizedConversationId
118
115
  });
@@ -126,7 +123,7 @@ function createRepository(knex) {
126
123
  async function updateById(messageId, patch = {}, options = {}) {
127
124
  const id = normalizeInputRecordId(messageId);
128
125
  if (!id) return null;
129
- const client = options?.trx || knex;
126
+ const client = options.trx || knex;
130
127
  const update = {};
131
128
  if (Object.hasOwn(patch, "contentText")) update.content_text = patch.contentText == null ? null : String(patch.contentText);
132
129
  if (Object.hasOwn(patch, "metadata")) update.metadata_json = stringifyJsonObject(patch.metadata);
@@ -140,7 +137,7 @@ function createRepository(knex) {
140
137
  return [];
141
138
  }
142
139
 
143
- const client = options?.trx || knex;
140
+ const client = options.trx || knex;
144
141
  const { page, pageSize } = normalizePagination(pagination);
145
142
  const offset = (page - 1) * pageSize;
146
143
 
@@ -157,6 +154,12 @@ function createRepository(knex) {
157
154
  return rows.map(mapMessageRow);
158
155
  }
159
156
 
157
+ async function lastSequence(conversationId, options = {}) {
158
+ const normalizedConversationId = normalizeInputRecordId(conversationId);
159
+ if (!normalizedConversationId) return 0;
160
+ return (await resolveNextSequence(options.trx || knex, normalizedConversationId)) - 1;
161
+ }
162
+
160
163
  async function transaction(callback) {
161
164
  return runInTransaction(knex, callback);
162
165
  }
@@ -168,6 +171,7 @@ function createRepository(knex) {
168
171
  updateById,
169
172
  countByConversationScope,
170
173
  listByConversationScope,
174
+ lastSequence,
171
175
  transaction
172
176
  });
173
177
  }
@@ -3,10 +3,6 @@ import { normalizeDbRecordId } from "@jskit-ai/database-runtime/shared/repositor
3
3
  import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
4
4
  import { parseJsonObject } from "@jskit-ai/assistant-core/shared";
5
5
 
6
- function normalizeWorkspaceId(value) {
7
- return normalizeRecordId(value, { fallback: null });
8
- }
9
-
10
6
  function normalizeDbWorkspaceId(value) {
11
7
  return normalizeDbRecordId(value, { fallback: null });
12
8
  }
@@ -16,7 +12,7 @@ function normalizeInputRecordId(value) {
16
12
  }
17
13
 
18
14
  function applyWorkspaceScope(query, columnName, workspaceId) {
19
- const normalizedWorkspaceId = normalizeWorkspaceId(workspaceId);
15
+ const normalizedWorkspaceId = normalizeInputRecordId(workspaceId);
20
16
  if (normalizedWorkspaceId) {
21
17
  return query.where(columnName, normalizedWorkspaceId);
22
18
  }
@@ -54,7 +50,6 @@ export {
54
50
  applyWorkspaceScope,
55
51
  normalizeDbWorkspaceId,
56
52
  normalizeInputRecordId,
57
- normalizeWorkspaceId,
58
53
  parseJsonObject,
59
54
  stringifyJsonObject,
60
55
  toIso,
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { assistantRuntimeConfig } from "../../shared/assistantRuntimeConfig.js";
3
3
 
4
+ /** @param {import("@jskit-ai/database-runtime/server/types").RuntimeKnex} knex */
4
5
  function createRepository(knex) {
5
6
  const table = assistantRuntimeConfig.turnRequestsTable;
6
7
 
@@ -29,7 +30,11 @@ function createRepository(knex) {
29
30
  if (!changed) throw new Error("Assistant request ownership was lost.");
30
31
  }
31
32
 
32
- return Object.freeze({ find, claim, update });
33
+ return Object.freeze({
34
+ find,
35
+ claim,
36
+ update
37
+ });
33
38
  }
34
39
 
35
40
  export { createRepository };
@@ -2,22 +2,24 @@ import { AppError, requireAuth } from "@jskit-ai/kernel/server/runtime";
2
2
  import { normalizeObject, normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
3
3
  import { requireAssistantSurface } from "../support/assistantSurface.js";
4
4
 
5
+ /** @typedef {import("../types.js").AssistantConfigRepository} AssistantConfigRepository */
6
+ /** @typedef {import("../types.js").WorkspaceScopeSupport} WorkspaceScopeSupport */
7
+
8
+ /**
9
+ * An assistant surface's saved settings: who may read and change them, and the
10
+ * system prompt they give the surface's conversations.
11
+ * @param {object} dependencies
12
+ * @param {AssistantConfigRepository} dependencies.assistantConfigRepository
13
+ * @param {import("@jskit-ai/console-core/server/types").ConsoleService | null} dependencies.consoleService The console access service, when the console is installed.
14
+ * @param {Record<string, any>} dependencies.appConfig
15
+ * @param {WorkspaceScopeSupport | null} dependencies.workspaceScopeSupport
16
+ */
5
17
  function createService({
6
18
  assistantConfigRepository,
7
- consoleService = null,
8
- appConfig = {},
9
- workspaceScopeSupport = null
10
- } = {}) {
11
- if (!assistantConfigRepository || typeof assistantConfigRepository.findByScope !== "function") {
12
- throw new Error("assistantConfigService requires assistantConfigRepository.findByScope().");
13
- }
14
- if (typeof assistantConfigRepository.upsertByScope !== "function") {
15
- throw new Error("assistantConfigService requires assistantConfigRepository.upsertByScope().");
16
- }
17
- if (typeof assistantConfigRepository.createDefaultRecord !== "function") {
18
- throw new Error("assistantConfigService requires assistantConfigRepository.createDefaultRecord().");
19
- }
20
-
19
+ consoleService,
20
+ appConfig,
21
+ workspaceScopeSupport
22
+ }) {
21
23
  async function requireSettingsAccess(assistantSurface, context = {}, { mode = "read" } = {}) {
22
24
  requireAuth({ context }, {
23
25
  require: "authenticated"
@@ -43,7 +45,7 @@ function createService({
43
45
  return;
44
46
  }
45
47
 
46
- if (!consoleService || typeof consoleService.requireConsoleOwner !== "function") {
48
+ if (!consoleService) {
47
49
  throw new Error("assistantConfigService requires consoleService.requireConsoleOwner() for console-owner settings surfaces.");
48
50
  }
49
51
 
@@ -55,7 +57,7 @@ function createService({
55
57
  return null;
56
58
  }
57
59
 
58
- if (!workspaceScopeSupport || typeof workspaceScopeSupport.resolveWorkspace !== "function") {
60
+ if (!workspaceScopeSupport) {
59
61
  throw new Error("assistant.config.service requires workspace server scope support for workspace-scoped settings.");
60
62
  }
61
63
 
@@ -69,12 +71,12 @@ function createService({
69
71
  }
70
72
 
71
73
  async function getSettings(input = {}, options = {}) {
72
- const assistantSurface = requireAssistantSurface(appConfig, input?.targetSurfaceId);
73
- await requireSettingsAccess(assistantSurface, options?.context, {
74
+ const assistantSurface = requireAssistantSurface(appConfig, input.targetSurfaceId);
75
+ await requireSettingsAccess(assistantSurface, options.context, {
74
76
  mode: "read"
75
77
  });
76
78
 
77
- const workspaceId = resolveConfigWorkspaceId(assistantSurface, null, input, options?.context);
79
+ const workspaceId = resolveConfigWorkspaceId(assistantSurface, null, input, options.context);
78
80
  const existing = await assistantConfigRepository.findByScope({
79
81
  targetSurfaceId: assistantSurface.targetSurfaceId,
80
82
  workspaceId
@@ -87,12 +89,12 @@ function createService({
87
89
  }
88
90
 
89
91
  async function updateSettings(input = {}, patch = {}, options = {}) {
90
- const assistantSurface = requireAssistantSurface(appConfig, input?.targetSurfaceId);
91
- await requireSettingsAccess(assistantSurface, options?.context, {
92
+ const assistantSurface = requireAssistantSurface(appConfig, input.targetSurfaceId);
93
+ await requireSettingsAccess(assistantSurface, options.context, {
92
94
  mode: "write"
93
95
  });
94
96
 
95
- const workspaceId = resolveConfigWorkspaceId(assistantSurface, null, input, options?.context);
97
+ const workspaceId = resolveConfigWorkspaceId(assistantSurface, null, input, options.context);
96
98
  const normalizedPatch = normalizeObject(patch);
97
99
 
98
100
  return assistantConfigRepository.upsertByScope({
@@ -102,7 +104,7 @@ function createService({
102
104
  });
103
105
  }
104
106
 
105
- async function resolveSystemPrompt(assistantSurface, workspace = null, _options = {}, serviceOptions = {}) {
107
+ async function resolveSystemPrompt(assistantSurface, workspace = null, _options = {}, serviceOptions) {
106
108
  const resolvedAssistantSurface = requireAssistantSurface(appConfig, assistantSurface?.targetSurfaceId);
107
109
  const workspaceId = resolveConfigWorkspaceId(
108
110
  resolvedAssistantSurface,
@@ -1,43 +1,14 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
+ import { resolveActor } from "@jskit-ai/kernel/server/actions/actionContributorHelpers";
2
3
  import { AppError, createValidationError } from "@jskit-ai/kernel/server/runtime";
3
4
  import { normalizeObject, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
4
5
  import { resolveWorkspaceSlug } from "@jskit-ai/assistant-core/server";
5
6
  import { conversationAttachmentIds } from "@jskit-ai/assistant-core/server/conversation";
6
- import { ASSISTANT_STREAM_EVENT_TYPES, MAX_HISTORY_MESSAGES, MAX_INPUT_CHARS, isAssistantProgressOnlyText } from "@jskit-ai/assistant-core/shared";
7
+ import { ASSISTANT_STREAM_EVENT_TYPES, MAX_INPUT_CHARS } from "@jskit-ai/assistant-core/shared";
7
8
  import { errorMessageFields, exposedAppErrorDetails, errorStatus } from "../support/assistantErrors.js";
8
9
  import { requireAssistantSurface } from "../support/assistantSurface.js";
9
- import { ASSISTANT_STREAM_WRITER_METHODS } from "../support/assistantStreamWriter.js";
10
10
 
11
- const CLOCK_INSTRUCTION = "For current or relative date and time questions, first use any available authoritative workspace clock action; never infer the current date or time from model knowledge.";
12
-
13
- function normalizeConversationId(value) {
14
- return normalizeRecordId(value, { fallback: null });
15
- }
16
-
17
- function normalizeHistory(history = []) {
18
- const source = Array.isArray(history) ? history : [];
19
- return source
20
- .slice(0, MAX_HISTORY_MESSAGES)
21
- .map((entry) => {
22
- const item = normalizeObject(entry);
23
- const role = normalizeText(item.role).toLowerCase();
24
- if (role !== "user" && role !== "assistant") {
25
- return null;
26
- }
27
-
28
- const content = normalizeText(item.content).slice(0, MAX_INPUT_CHARS);
29
- if (!content || (role === "assistant" && isAssistantProgressOnlyText(content))) {
30
- return null;
31
- }
32
-
33
- return {
34
- role,
35
- content,
36
- attachmentIds: role === "user" ? item.attachmentIds : []
37
- };
38
- })
39
- .filter(Boolean);
40
- }
11
+ const CLOCK_INSTRUCTION = "For current or relative date and time questions, use the current time in the system's state note, or an available authoritative workspace clock action for another time zone; never infer the current date or time from model knowledge.";
41
12
 
42
13
  function normalizeStreamInput(payload = {}) {
43
14
  const source = normalizeObject(payload);
@@ -56,17 +27,12 @@ function normalizeStreamInput(payload = {}) {
56
27
 
57
28
  return {
58
29
  messageId,
59
- conversationId: normalizeConversationId(source.conversationId),
30
+ conversationId: normalizeRecordId(source.conversationId, { fallback: null }),
60
31
  input,
61
- attachmentIds: source.attachmentIds,
62
- history: normalizeHistory(source.history)
32
+ attachmentIds: source.attachmentIds
63
33
  };
64
34
  }
65
35
 
66
- function hasStreamWriter(streamWriter) {
67
- return ASSISTANT_STREAM_WRITER_METHODS.every(name => typeof streamWriter?.[name] === "function");
68
- }
69
-
70
36
  // A saved failure keeps its translation fields, except where the kernel would
71
37
  // withhold them, so a replay shows the same message in the user's language.
72
38
  function savedMessageFields(error) {
@@ -110,7 +76,12 @@ function buildToolContractLine(toolDescriptor = {}) {
110
76
  return `${name}: input=${toCompactJson(inputSchema)} output=${toCompactJson(outputSchema, "null")}`;
111
77
  }
112
78
 
113
- function buildSystemPrompt({ targetSurfaceId = "", toolDescriptors = [], workspaceSlug = "", customSystemPrompt = "" } = {}) {
79
+ /**
80
+ * The system prompt of a surface's turn: the surface, its workspace, the tools it
81
+ * offers and the surface's own instructions.
82
+ * @param {{ targetSurfaceId?: string, toolDescriptors: Array<{ name: string, parameters?: unknown, outputSchema?: unknown }>, workspaceSlug?: string, customSystemPrompt?: string }} prompt
83
+ */
84
+ function buildSystemPrompt({ targetSurfaceId = "", toolDescriptors, workspaceSlug = "", customSystemPrompt = "" }) {
114
85
  const toolSummary = toolDescriptors.length > 0
115
86
  ? `Available tools: ${toolDescriptors.map((entry) => entry.name).join(", ")}.`
116
87
  : "No tools are currently available for this user/session.";
@@ -161,6 +132,29 @@ function buildAssistantActionContext(context = {}, assistantSurface = {}) {
161
132
  };
162
133
  }
163
134
 
135
+ /** @typedef {import("../types.js").AssistantAiClientFactory} AssistantAiClientFactory */
136
+ /** @typedef {import("../types.js").AssistantAttachments} AssistantAttachments */
137
+ /** @typedef {import("../types.js").AssistantConfigService} AssistantConfigService */
138
+ /** @typedef {import("../types.js").ConversationRuntime} ConversationRuntime */
139
+ /** @typedef {import("../types.js").SurfaceAwareToolCatalog} SurfaceAwareToolCatalog */
140
+ /** @typedef {import("../types.js").TranscriptService} TranscriptService */
141
+ /** @typedef {import("../types.js").TurnRequestsRepository} TurnRequestsRepository */
142
+ /** @typedef {import("../types.js").WorkspaceScopeSupport} WorkspaceScopeSupport */
143
+
144
+ /**
145
+ * The chat stream and transcripts of the package's own tables. Each chat turn is
146
+ * recorded once per message id, so a repeated request replays its saved outcome.
147
+ * @param {object} dependencies
148
+ * @param {ConversationRuntime} dependencies.conversationRuntime
149
+ * @param {AssistantAiClientFactory} dependencies.aiClientFactory
150
+ * @param {TurnRequestsRepository} dependencies.turnRequests
151
+ * @param {AssistantAttachments | null} [dependencies.attachments]
152
+ * @param {TranscriptService} dependencies.transcriptService
153
+ * @param {SurfaceAwareToolCatalog} dependencies.serviceToolCatalog
154
+ * @param {AssistantConfigService} dependencies.assistantConfigService
155
+ * @param {Record<string, any>} dependencies.appConfig
156
+ * @param {WorkspaceScopeSupport | null} [dependencies.workspaceScopeSupport]
157
+ */
164
158
  function createChatService({
165
159
  conversationRuntime,
166
160
  aiClientFactory,
@@ -169,21 +163,15 @@ function createChatService({
169
163
  transcriptService,
170
164
  serviceToolCatalog,
171
165
  assistantConfigService,
172
- appConfig = {},
173
- workspaceScopeSupport = null
174
- } = {}) {
175
- if (!aiClientFactory || typeof aiClientFactory.resolveClient !== "function" || !turnRequests || !transcriptService || !serviceToolCatalog || !assistantConfigService) {
176
- throw new Error(
177
- "createChatService requires aiClientFactory.resolveClient(), turnRequests, transcriptService, serviceToolCatalog, and assistantConfigService."
178
- );
179
- }
180
-
166
+ appConfig,
167
+ workspaceScopeSupport
168
+ }) {
181
169
  function resolveRuntimeWorkspace(assistantSurface, context = {}, input = {}) {
182
170
  if (assistantSurface?.runtimeSurfaceRequiresWorkspace !== true) {
183
171
  return null;
184
172
  }
185
173
 
186
- if (!workspaceScopeSupport || typeof workspaceScopeSupport.resolveWorkspace !== "function") {
174
+ if (!workspaceScopeSupport) {
187
175
  throw new Error("assistant.chat.service requires workspace server scope support for workspace-scoped assistant surfaces.");
188
176
  }
189
177
 
@@ -243,17 +231,23 @@ function createChatService({
243
231
  await turns.releasing;
244
232
  }
245
233
 
246
- async function streamChat(payload = {}, options = {}) {
247
- const assistantSurface = requireAssistantSurface(appConfig, payload?.targetSurfaceId);
234
+ /**
235
+ * Streams one chat turn through its writer, or replays the saved turn with the same message id.
236
+ * @param {object} payload The validated chat stream input.
237
+ * @param {{ context: object, streamWriter: import("../support/assistantStreamWriter.js").AssistantStreamWriter,
238
+ * abortSignal?: AbortSignal }} options
239
+ */
240
+ async function streamChat(payload, options) {
241
+ const assistantSurface = requireAssistantSurface(appConfig, payload.targetSurfaceId);
248
242
  const context = normalizeObject(options.context);
249
243
  const assistantContext = buildAssistantActionContext(context, assistantSurface);
250
244
  const workspace = resolveRuntimeWorkspace(assistantSurface, assistantContext, payload);
251
245
  const source = normalizeStreamInput(payload);
252
- const actor = context.actor;
246
+ const actor = resolveActor(context);
253
247
  const actorUserId = normalizeRecordId(actor?.id, { fallback: null });
254
248
  if (!actorUserId) throw new AppError(401, "Authentication required.", { code: "assistant.runtime.authenticationRequired" });
255
- if (!hasStreamWriter(options.streamWriter)) throw new Error("assistant.chat.stream requires streamWriter methods.");
256
249
  const scope = { actorUserId, surfaceId: assistantSurface.targetSurfaceId,
250
+ // readable-check: allow no-dual-shape - data: the workspace scope resolves the workspace from the action input or context, which may carry the record or its id
257
251
  workspaceId: normalizeRecordId(workspace?.id || workspace, { fallback: null }) };
258
252
  const request = { ...source, integrationId: normalizeText(payload.integrationId) };
259
253
  const existing = await turnRequests.find(scope, source.messageId);
@@ -296,7 +290,6 @@ function createChatService({
296
290
  return result;
297
291
 
298
292
  async function executeTurn() {
299
- if (typeof conversationRuntime?.open !== "function") throw new TypeError("Assistant streaming requires its common conversation runtime.");
300
293
  const conversationResult = await transcriptService.createConversationForTurn(
301
294
  assistantSurface,
302
295
  workspace,
@@ -329,13 +322,12 @@ function createChatService({
329
322
  code: "assistant.runtime.attachments.notConfigured"
330
323
  });
331
324
  }
332
- // Preserve the facade's existing pre-admission authorization order.
325
+ // Attachments are authorized before admission, as the facade orders it.
333
326
  // Only these server-resolved materials reach the common file reader.
334
327
  const files = await attachments.resolve({ attachmentIds, context: assistantContext, workspace, conversation });
335
328
  resolvedAttachments.push({ attachmentIds: [...attachmentIds], files });
336
329
  }
337
330
  await prepareAttachments(source.attachmentIds);
338
- for (const message of source.history) await prepareAttachments(message.attachmentIds);
339
331
  const toolSet = serviceToolCatalog.resolveToolSet(assistantContext);
340
332
  const customSystemPrompt = await assistantConfigService.resolveSystemPrompt(
341
333
  assistantSurface, workspace, { surface: assistantSurface.targetSurfaceId },
@@ -346,7 +338,7 @@ function createChatService({
346
338
  workspaceSlug: resolveWorkspaceSlug(assistantContext, payload), customSystemPrompt
347
339
  }), ...(request.integrationId ? { integrationId: request.integrationId } : {}) };
348
340
  const runtimeContext = { ...assistantContext, assistantRequest: {
349
- conversation, workspace, history: source.history, resolvedAttachments
341
+ conversation, workspace, resolvedAttachments
350
342
  } };
351
343
  const handle = await openConversationTurn(conversationId, async () => {
352
344
  try { return await conversationRuntime.open({ id: conversationId, context: runtimeContext, configuration }); }
@@ -438,7 +430,7 @@ function createChatService({
438
430
  }
439
431
 
440
432
  async function listConversations(query = {}, options = {}) {
441
- const assistantSurface = requireAssistantSurface(appConfig, options?.input?.targetSurfaceId);
433
+ const assistantSurface = requireAssistantSurface(appConfig, options.input?.targetSurfaceId);
442
434
  const context = normalizeObject(options.context);
443
435
  const assistantContext = buildAssistantActionContext(context, assistantSurface);
444
436
  const workspace = resolveRuntimeWorkspace(assistantSurface, assistantContext, options.input || {});
@@ -448,7 +440,7 @@ function createChatService({
448
440
  }
449
441
 
450
442
  async function getConversationMessages(conversationId, query = {}, options = {}) {
451
- const assistantSurface = requireAssistantSurface(appConfig, options?.input?.targetSurfaceId);
443
+ const assistantSurface = requireAssistantSurface(appConfig, options.input?.targetSurfaceId);
452
444
  const context = normalizeObject(options.context);
453
445
  const assistantContext = buildAssistantActionContext(context, assistantSurface);
454
446
  const workspace = resolveRuntimeWorkspace(assistantSurface, assistantContext, options.input || {});
@@ -464,8 +456,12 @@ function createChatService({
464
456
  );
465
457
  }
466
458
 
459
+ function close() {
460
+ return conversationRuntime.close();
461
+ }
462
+
467
463
  return Object.freeze({
468
- close: () => conversationRuntime?.close(),
464
+ close,
469
465
  streamChat,
470
466
  listConversations,
471
467
  getConversationMessages
@@ -1,3 +1,4 @@
1
+ import { resolveActor } from "@jskit-ai/kernel/server/actions/actionContributorHelpers";
1
2
  import { AppError, createValidationError, parsePositiveInteger } from "@jskit-ai/kernel/server/runtime";
2
3
  import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
3
4
  import { normalizeObject, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
@@ -11,6 +12,7 @@ const MAX_MESSAGES_PAGE_SIZE = 500;
11
12
  const MAX_CONVERSATION_TITLE_LENGTH = 80;
12
13
 
13
14
  function resolveWorkspaceId(workspace, { required = false } = {}) {
15
+ // readable-check: allow no-dual-shape - data: the workspace scope resolves the workspace from the action input or context, which may carry the record or its id
14
16
  const workspaceId = normalizeRecordId(workspace?.id || workspace, { fallback: null });
15
17
  if (!workspaceId && required) {
16
18
  throw new AppError(409, "Workspace selection required.", { code: "assistant.runtime.workspace.selectionRequired" });
@@ -61,7 +63,7 @@ function isDefaultConversationTitle(value) {
61
63
  return normalizeText(value).toLowerCase() === DEFAULT_CONVERSATION_TITLE.toLowerCase();
62
64
  }
63
65
 
64
- function requireAssistantSurface(assistantSurface = {}) {
66
+ function requireAssistantSurface(assistantSurface) {
65
67
  const targetSurfaceId = normalizeSurfaceId(assistantSurface?.targetSurfaceId);
66
68
  if (!targetSurfaceId) {
67
69
  throw new TypeError("assistant transcript service requires assistantSurface.targetSurfaceId.");
@@ -73,11 +75,17 @@ function requireAssistantSurface(assistantSurface = {}) {
73
75
  });
74
76
  }
75
77
 
76
- function createTranscriptService({ conversationsRepository, messagesRepository } = {}) {
77
- if (!conversationsRepository || !messagesRepository) {
78
- throw new Error("createTranscriptService requires conversationsRepository and messagesRepository.");
79
- }
80
-
78
+ /** @typedef {import("../types.js").ConversationsRepository} ConversationsRepository */
79
+ /** @typedef {import("../types.js").MessagesRepository} MessagesRepository */
80
+
81
+ /**
82
+ * A user's conversations on an assistant surface and their messages, in the
83
+ * package's own tables.
84
+ * @param {object} dependencies
85
+ * @param {ConversationsRepository} dependencies.conversationsRepository
86
+ * @param {MessagesRepository} dependencies.messagesRepository
87
+ */
88
+ function createTranscriptService({ conversationsRepository, messagesRepository }) {
81
89
  function resolveExpectedWorkspaceId(assistantSurface, workspace) {
82
90
  const resolvedAssistantSurface = requireAssistantSurface(assistantSurface);
83
91
  return resolveWorkspaceId(workspace, {
@@ -150,7 +158,7 @@ function createTranscriptService({ conversationsRepository, messagesRepository }
150
158
 
151
159
  const source = normalizeObject(payload);
152
160
  const context = normalizeObject(options.context);
153
- const actorUserId = resolveActorUserId(context.actor, {
161
+ const actorUserId = resolveActorUserId(resolveActor(context), {
154
162
  required: true
155
163
  });
156
164
  const workspaceId = resolveExpectedWorkspaceId(resolvedAssistantSurface, options.workspace || context.workspace);