@jskit-ai/assistant-runtime 0.1.234 → 0.1.236

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 (31) hide show
  1. package/README.md +36 -0
  2. package/fixtures/conversation/vite.config.mjs +2 -2
  3. package/package.json +9 -10
  4. package/src/client/components/AssistantSettingsClientElement.vue +0 -3
  5. package/src/client/components/AssistantSurfaceClientElement.vue +2 -5
  6. package/src/client/composables/useAssistantConversation.js +57 -55
  7. package/src/client/composables/useAssistantRuntime.js +14 -29
  8. package/src/client/support/accessDenied.js +6 -0
  9. package/src/client/support/subscribeAssistantConversation.js +4 -5
  10. package/src/server/actions.js +3 -36
  11. package/src/server/createAssistantSettingsActions.js +0 -8
  12. package/src/server/inputSchemas.js +3 -8
  13. package/src/server/registerConversationSubscriptions.js +13 -15
  14. package/src/server/registerRoutes.js +15 -39
  15. package/src/server/repositories/conversationsRepository.js +9 -47
  16. package/src/server/repositories/messagesRepository.js +5 -22
  17. package/src/server/repositories/repositoryPersistenceUtils.js +34 -1
  18. package/src/server/services/assistantConfigService.js +4 -17
  19. package/src/server/services/chatService.js +14 -37
  20. package/src/server/services/transcriptService.js +1 -54
  21. package/src/server/support/assistantErrors.js +13 -0
  22. package/src/server/support/assistantRouteContext.js +1 -10
  23. package/src/server/support/assistantStreamWriter.js +12 -0
  24. package/src/server/support/assistantSurface.js +13 -0
  25. package/src/server/support/createSurfaceAwareToolCatalog.js +5 -6
  26. package/src/shared/assistantRuntimeConfig.js +3 -1
  27. package/src/shared/assistantSurfaces.js +18 -0
  28. package/test/actionDefinitions.test.js +0 -1
  29. package/test/assistantSurfaces.test.js +29 -0
  30. package/test/chatServiceLifecycle.test.js +12 -3
  31. package/test/turnRequests.test.js +0 -1
@@ -46,10 +46,6 @@ function createAssistantSettingsActions({ assistantConfigService, settingsSurfac
46
46
  }
47
47
  },
48
48
  idempotency: "none",
49
- audit: {
50
- actionName: actionIds.settingsRead
51
- },
52
- observability: {},
53
49
  async execute(input, context) {
54
50
  return returnJsonApiData(await assistantConfigService.getSettings(input, {
55
51
  context
@@ -73,10 +69,6 @@ function createAssistantSettingsActions({ assistantConfigService, settingsSurfac
73
69
  }
74
70
  },
75
71
  idempotency: "optional",
76
- audit: {
77
- actionName: actionIds.settingsUpdate
78
- },
79
- observability: {},
80
72
  async execute(input, context) {
81
73
  return returnJsonApiData(await assistantConfigService.updateSettings(input, input.patch, {
82
74
  context
@@ -1,6 +1,7 @@
1
1
  import { createSchema } from "json-rest-schema";
2
2
  import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
3
3
  import { composeSchemaDefinitions } from "@jskit-ai/kernel/shared/validators";
4
+ import { attachmentIdsField } from "@jskit-ai/assistant-core/shared";
4
5
 
5
6
  const assistantSurfaceRouteParamsSchema = createSchema({
6
7
  surfaceId: {
@@ -65,10 +66,7 @@ const assistantConversationSendBodyValidator = deepFreeze({
65
66
  schema: createSchema({
66
67
  messageId: { type: "string", required: true, minLength: 1, maxLength: 128 },
67
68
  text: { type: "string", required: true },
68
- attachmentIds: {
69
- type: "array", required: false, maxItems: 10,
70
- items: { type: "string", minLength: 1, maxLength: 256 }
71
- },
69
+ attachmentIds: attachmentIdsField,
72
70
  steer: { type: "boolean", required: false }
73
71
  }),
74
72
  mode: "patch"
@@ -111,10 +109,7 @@ const assistantConversationGoalBodyValidator = deepFreeze({
111
109
  messageId: { type: "string", required: false, minLength: 1, maxLength: 128 },
112
110
  objective: { type: "string", required: false, minLength: 1, maxLength: 4000 },
113
111
  tokenBudget: { type: "integer", required: false, nullable: true, min: 1, max: Number.MAX_SAFE_INTEGER },
114
- attachmentIds: {
115
- type: "array", required: false, maxItems: 10,
116
- items: { type: "string", minLength: 1, maxLength: 256 }
117
- }
112
+ attachmentIds: attachmentIdsField
118
113
  }),
119
114
  mode: "replace"
120
115
  });
@@ -3,7 +3,8 @@ import { attachDirectRequestActionExecutor } from "@jskit-ai/kernel/server/http"
3
3
  import { AppError } from "@jskit-ai/kernel/server/runtime";
4
4
  import { actionIds } from "./actionIds.js";
5
5
  import { resolveRouteRequestState } from "./support/assistantRouteContext.js";
6
- import { resolveAssistantSurfaceConfig } from "../shared/assistantSurfaces.js";
6
+ import { errorStatus } from "./support/assistantErrors.js";
7
+ import { requireAssistantSurface } from "./support/assistantSurface.js";
7
8
  import {
8
9
  ASSISTANT_CONVERSATION_EVENT,
9
10
  ASSISTANT_CONVERSATION_SUBSCRIBE,
@@ -18,7 +19,7 @@ function requireSubscriptionId(input) {
18
19
  }
19
20
 
20
21
  function subscriptionError(error) {
21
- const status = Number(error?.status || error?.statusCode || 500);
22
+ const status = errorStatus(error);
22
23
  return {
23
24
  ok: false,
24
25
  error: status < 500 ? String(error.message || "Conversation subscription failed.") : "Conversation subscription failed.",
@@ -69,14 +70,16 @@ function registerConversationSubscriptions({ realtime, events, actions, config =
69
70
  let stage = "request-admission";
70
71
  let observedConversationId;
71
72
  let diagnosticTimer;
73
+ function stageRecord(stageName, fields, now) {
74
+ return { event: "assistant.conversation.subscription", subscriptionEpoch: subscription?.epoch,
75
+ ...(typeof observedConversationId === "string" ? { conversationId: observedConversationId } : {}),
76
+ stage: stageName, previousStage: stage, durationMs: now - stageStartedAt, elapsedMs: now - startedAt, ...fields };
77
+ }
72
78
  function diagnostic(stageName, fields = {}) {
73
79
  if (!diagnostics) return;
74
80
  const now = performance.now();
75
81
  try {
76
- logger?.warn?.({ event: "assistant.conversation.subscription", subscriptionEpoch: subscription?.epoch,
77
- ...(typeof observedConversationId === "string" ? { conversationId: observedConversationId } : {}),
78
- stage: stageName, previousStage: stage, durationMs: now - stageStartedAt,
79
- elapsedMs: now - startedAt, ...fields }, "Assistant conversation subscription diagnostic.");
82
+ logger?.warn?.(stageRecord(stageName, fields, now), "Assistant conversation subscription diagnostic.");
80
83
  } catch { /* Diagnostics never change admission or cleanup. */ }
81
84
  }
82
85
  function onStage(next, fields = {}) {
@@ -86,11 +89,7 @@ function registerConversationSubscriptions({ realtime, events, actions, config =
86
89
  try {
87
90
  const slowOutcome = now - startedAt >= 5_000 && ["acknowledgement", "abandoned"].includes(next);
88
91
  const level = diagnostics || next === "failed" || slowOutcome ? "warn" : "info";
89
- logger?.[level]?.({ event: "assistant.conversation.subscription", subscriptionEpoch: subscription?.epoch,
90
- ...(typeof observedConversationId === "string" ? { conversationId: observedConversationId } : {}),
91
- stage: next, previousStage: stage,
92
- durationMs: now - stageStartedAt, elapsedMs: now - startedAt, ...fields },
93
- "Assistant conversation subscription stage");
92
+ logger?.[level]?.(stageRecord(next, fields, now), "Assistant conversation subscription stage");
94
93
  } catch { /* Diagnostics must not change conversation admission or cleanup. */ }
95
94
  stage = next;
96
95
  stageStartedAt = now;
@@ -113,8 +112,7 @@ function registerConversationSubscriptions({ realtime, events, actions, config =
113
112
  const request = await (requestPolicy === "host" ? readRequest() : authenticate());
114
113
  if (!current()) { onStage("abandoned"); return; }
115
114
  onStage("action-admission");
116
- const assistantSurface = resolveAssistantSurfaceConfig(config, input.targetSurfaceId);
117
- if (!assistantSurface) throw new AppError(404, "Assistant not found.");
115
+ const assistantSurface = requireAssistantSurface(config, input.targetSurfaceId);
118
116
  const requiresWorkspace = assistantSurface.runtimeSurfaceRequiresWorkspace;
119
117
  request.headers = { ...request.headers, "x-jskit-surface": input.hostSurfaceId };
120
118
  request.input = { params: {
@@ -155,7 +153,7 @@ function registerConversationSubscriptions({ realtime, events, actions, config =
155
153
  function onEvent(event) {
156
154
  if (!current()) return;
157
155
  const revision = ++subscription.revision;
158
- // The Colleague publisher already coalesces transient full-text replies.
156
+ // Conversation runtimes publish full-text in-progress replies; only the latest per message needs delivery.
159
157
  if (event.type === "message" && event.status === "inProgress") {
160
158
  subscription.pending.set(event.messageId, { event, revision });
161
159
  subscription.timer ||= setTimeout(flush, 25);
@@ -187,7 +185,7 @@ function registerConversationSubscriptions({ realtime, events, actions, config =
187
185
  onStage("acknowledgement-sent");
188
186
  } catch (error) {
189
187
  onStage("failed", { code: String(error?.code || "assistant_subscription_failed"),
190
- status: Number(error?.status || error?.statusCode || 500) });
188
+ status: errorStatus(error) });
191
189
  if (subscriptions.get(subscriptionId) === subscription) release(subscriptionId);
192
190
  if (!closed && socket.connected) acknowledge(subscriptionError(error));
193
191
  } finally {
@@ -27,14 +27,12 @@ import {
27
27
  assistantSurfaceRouteParamsValidator
28
28
  } from "./inputSchemas.js";
29
29
  import { registerSettingsRoutes } from "./registerSettingsRoutes.js";
30
+ import { errorStatus, exposedAppErrorDetails } from "./support/assistantErrors.js";
31
+ import { ASSISTANT_STREAM_WRITER_METHODS } from "./support/assistantStreamWriter.js";
30
32
  import { requireWorkspaceAssistantRouteParams, resolveRouteRequestState } from "./support/assistantRouteContext.js";
31
33
 
32
- function shouldExposeAppErrorDetails(errorCode = "") {
33
- return String(errorCode || "").trim() !== "ACTION_PERMISSION_DENIED";
34
- }
35
-
36
34
  function sendPreStreamErrorResponse(reply, error) {
37
- const statusCode = Number(error?.status || error?.statusCode || 500);
35
+ const statusCode = errorStatus(error);
38
36
  const safeStatusCode = Number.isInteger(statusCode) && statusCode >= 400 && statusCode <= 599 ? statusCode : 500;
39
37
 
40
38
  if (error instanceof AppError) {
@@ -44,10 +42,11 @@ function sendPreStreamErrorResponse(reply, error) {
44
42
  code: appErrorCode
45
43
  };
46
44
 
47
- if (error.details && shouldExposeAppErrorDetails(appErrorCode)) {
48
- payload.details = error.details;
49
- if (error.details.fieldErrors) {
50
- payload.fieldErrors = error.details.fieldErrors;
45
+ const details = exposedAppErrorDetails(error);
46
+ if (details) {
47
+ payload.details = details;
48
+ if (details.fieldErrors) {
49
+ payload.fieldErrors = details.fieldErrors;
51
50
  }
52
51
  }
53
52
 
@@ -156,36 +155,13 @@ function registerRuntimeRoutes(
156
155
  streamStarted = true;
157
156
  }
158
157
 
159
- const streamWriter = Object.freeze({
160
- sendMeta(payload = {}) {
161
- ensureStreamStarted();
162
- writeNdjson(reply, payload);
163
- },
164
- sendAssistantDelta(payload = {}) {
165
- ensureStreamStarted();
166
- writeNdjson(reply, payload);
167
- },
168
- sendAssistantMessage(payload = {}) {
169
- ensureStreamStarted();
170
- writeNdjson(reply, payload);
171
- },
172
- sendToolCall(payload = {}) {
173
- ensureStreamStarted();
174
- writeNdjson(reply, payload);
175
- },
176
- sendToolResult(payload = {}) {
177
- ensureStreamStarted();
178
- writeNdjson(reply, payload);
179
- },
180
- sendError(payload = {}) {
181
- ensureStreamStarted();
182
- writeNdjson(reply, payload);
183
- },
184
- sendDone(payload = {}) {
185
- ensureStreamStarted();
186
- writeNdjson(reply, payload);
187
- }
188
- });
158
+ function sendStreamEvent(payload = {}) {
159
+ ensureStreamStarted();
160
+ writeNdjson(reply, payload);
161
+ }
162
+ const streamWriter = Object.freeze(
163
+ Object.fromEntries(ASSISTANT_STREAM_WRITER_METHODS.map(name => [name, sendStreamEvent]))
164
+ );
189
165
 
190
166
  try {
191
167
  request.raw.on("close", closeListener);
@@ -1,25 +1,17 @@
1
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
- import { normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
4
4
  import {
5
+ applyWorkspaceScope,
6
+ normalizeDbWorkspaceId,
7
+ normalizeInputRecordId,
8
+ normalizeWorkspaceId,
5
9
  parseJsonObject,
6
10
  resolveInsertedId,
7
11
  stringifyJsonObject,
8
12
  toIso
9
13
  } from "./repositoryPersistenceUtils.js";
10
- import { assistantRuntimeConfig } from "../../shared/assistantRuntimeConfig.js";
11
-
12
- function normalizeWorkspaceId(value) {
13
- return normalizeRecordId(value, { fallback: null });
14
- }
15
-
16
- function normalizeDbWorkspaceId(value) {
17
- return normalizeDbRecordId(value, { fallback: null });
18
- }
19
-
20
- function normalizeInputRecordId(value) {
21
- return normalizeRecordId(value, { fallback: null });
22
- }
14
+ import { DEFAULT_CONVERSATION_TITLE, assistantRuntimeConfig } from "../../shared/assistantRuntimeConfig.js";
23
15
 
24
16
  function normalizeRequiredSurfaceId(value) {
25
17
  const normalizedSurfaceId = normalizeSurfaceId(value);
@@ -30,20 +22,11 @@ function normalizeRequiredSurfaceId(value) {
30
22
  return normalizedSurfaceId;
31
23
  }
32
24
 
33
- function applyWorkspaceScope(query, columnName, workspaceId) {
34
- const normalizedWorkspaceId = normalizeWorkspaceId(workspaceId);
35
- if (normalizedWorkspaceId) {
36
- return query.where(columnName, normalizedWorkspaceId);
37
- }
38
-
39
- return query.whereNull(columnName);
40
- }
41
-
42
25
  function mapConversationRow(row = {}) {
43
26
  return {
44
27
  id: normalizeDbRecordId(row.id, { fallback: "" }),
45
28
  workspaceId: normalizeDbWorkspaceId(row.workspace_id),
46
- title: String(row.title || "New conversation"),
29
+ title: String(row.title || DEFAULT_CONVERSATION_TITLE),
47
30
  createdByUserId: row.created_by_user_id == null ? null : normalizeDbRecordId(row.created_by_user_id, { fallback: null }),
48
31
  status: String(row.status || "active"),
49
32
  provider: String(row.provider || ""),
@@ -132,7 +115,7 @@ function createRepository(knex) {
132
115
  const insertResult = await client(assistantRuntimeConfig.conversationsTable).insert({
133
116
  workspace_id: normalizeWorkspaceId(payload.workspaceId),
134
117
  created_by_user_id: normalizeInputRecordId(payload.createdByUserId),
135
- title: normalizeText(payload.title) || "New conversation",
118
+ title: normalizeText(payload.title) || DEFAULT_CONVERSATION_TITLE,
136
119
  status: normalizeText(payload.status).toLowerCase() || "active",
137
120
  provider: normalizeText(payload.provider),
138
121
  model: normalizeText(payload.model),
@@ -164,7 +147,7 @@ function createRepository(knex) {
164
147
  const updatePatch = {};
165
148
 
166
149
  if (Object.hasOwn(patch, "title")) {
167
- updatePatch.title = normalizeText(patch.title) || "New conversation";
150
+ updatePatch.title = normalizeText(patch.title) || DEFAULT_CONVERSATION_TITLE;
168
151
  }
169
152
  if (Object.hasOwn(patch, "status")) {
170
153
  updatePatch.status = normalizeText(patch.status).toLowerCase() || "active";
@@ -200,26 +183,6 @@ function createRepository(knex) {
200
183
  });
201
184
  }
202
185
 
203
- async function incrementMessageCount(conversationId, delta = 1, options = {}) {
204
- const normalizedConversationId = normalizeInputRecordId(conversationId);
205
- if (!normalizedConversationId) {
206
- return null;
207
- }
208
-
209
- const client = options?.trx || knex;
210
- const incrementBy = Number.isInteger(Number(delta)) ? Number(delta) : 1;
211
- await client(assistantRuntimeConfig.conversationsTable)
212
- .where({ id: normalizedConversationId })
213
- .update({
214
- message_count: client.raw("GREATEST(0, message_count + ?)", [incrementBy]),
215
- updated_at: new Date()
216
- });
217
-
218
- return findById(normalizedConversationId, {
219
- trx: client
220
- });
221
- }
222
-
223
186
  async function listForActorScope(
224
187
  { workspaceId = null, actorUserId = null, surfaceId = "", pagination = {}, filters = {} } = {},
225
188
  options = {}
@@ -275,7 +238,6 @@ function createRepository(knex) {
275
238
  findByIdForActorScope,
276
239
  create,
277
240
  updateById,
278
- incrementMessageCount,
279
241
  listForActorScope,
280
242
  transaction
281
243
  });
@@ -1,6 +1,10 @@
1
1
  import { createWithTransaction, normalizeDbRecordId, runInTransaction } from "@jskit-ai/database-runtime/shared/repositoryOptions";
2
- import { normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
3
  import {
4
+ applyWorkspaceScope,
5
+ normalizeDbWorkspaceId,
6
+ normalizeInputRecordId,
7
+ normalizeWorkspaceId,
4
8
  parseJsonObject,
5
9
  resolveInsertedId,
6
10
  stringifyJsonObject,
@@ -8,27 +12,6 @@ import {
8
12
  } from "./repositoryPersistenceUtils.js";
9
13
  import { assistantRuntimeConfig } from "../../shared/assistantRuntimeConfig.js";
10
14
 
11
- function normalizeWorkspaceId(value) {
12
- return normalizeRecordId(value, { fallback: null });
13
- }
14
-
15
- function normalizeDbWorkspaceId(value) {
16
- return normalizeDbRecordId(value, { fallback: null });
17
- }
18
-
19
- function normalizeInputRecordId(value) {
20
- return normalizeRecordId(value, { fallback: null });
21
- }
22
-
23
- function applyWorkspaceScope(query, columnName, workspaceId) {
24
- const normalizedWorkspaceId = normalizeWorkspaceId(workspaceId);
25
- if (normalizedWorkspaceId) {
26
- return query.where(columnName, normalizedWorkspaceId);
27
- }
28
-
29
- return query.whereNull(columnName);
30
- }
31
-
32
15
  function mapMessageRow(row = {}) {
33
16
  return {
34
17
  id: normalizeDbRecordId(row.id, { fallback: "" }),
@@ -1,6 +1,30 @@
1
1
  import { resolveInsertedRecordId } from "@jskit-ai/database-runtime/shared";
2
+ import { normalizeDbRecordId } from "@jskit-ai/database-runtime/shared/repositoryOptions";
3
+ import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
2
4
  import { parseJsonObject } from "@jskit-ai/assistant-core/shared";
3
5
 
6
+ function normalizeWorkspaceId(value) {
7
+ return normalizeRecordId(value, { fallback: null });
8
+ }
9
+
10
+ function normalizeDbWorkspaceId(value) {
11
+ return normalizeDbRecordId(value, { fallback: null });
12
+ }
13
+
14
+ function normalizeInputRecordId(value) {
15
+ return normalizeRecordId(value, { fallback: null });
16
+ }
17
+
18
+ function applyWorkspaceScope(query, columnName, workspaceId) {
19
+ const normalizedWorkspaceId = normalizeWorkspaceId(workspaceId);
20
+ if (normalizedWorkspaceId) {
21
+ return query.where(columnName, normalizedWorkspaceId);
22
+ }
23
+
24
+ return query.whereNull(columnName);
25
+ }
26
+
27
+
4
28
  function stringifyJsonObject(value) {
5
29
  if (!value || typeof value !== "object" || Array.isArray(value)) {
6
30
  return null;
@@ -26,4 +50,13 @@ function resolveInsertedId(insertResult) {
26
50
  return resolveInsertedRecordId(insertResult, { fallback: "" }) || "";
27
51
  }
28
52
 
29
- export { parseJsonObject, stringifyJsonObject, toIso, resolveInsertedId };
53
+ export {
54
+ applyWorkspaceScope,
55
+ normalizeDbWorkspaceId,
56
+ normalizeInputRecordId,
57
+ normalizeWorkspaceId,
58
+ parseJsonObject,
59
+ stringifyJsonObject,
60
+ toIso,
61
+ resolveInsertedId
62
+ };
@@ -1,12 +1,11 @@
1
1
  import { AppError, requireAuth } from "@jskit-ai/kernel/server/runtime";
2
2
  import { normalizeObject, normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
3
- import { resolveAssistantSurfaceConfig } from "../../shared/assistantSurfaces.js";
3
+ import { requireAssistantSurface } from "../support/assistantSurface.js";
4
4
 
5
5
  function createService({
6
6
  assistantConfigRepository,
7
7
  consoleService = null,
8
8
  appConfig = {},
9
- resolveAppConfig = null,
10
9
  workspaceScopeSupport = null
11
10
  } = {}) {
12
11
  if (!assistantConfigRepository || typeof assistantConfigRepository.findByScope !== "function") {
@@ -19,18 +18,6 @@ function createService({
19
18
  throw new Error("assistantConfigService requires assistantConfigRepository.createDefaultRecord().");
20
19
  }
21
20
 
22
- const resolveCurrentAppConfig =
23
- typeof resolveAppConfig === "function" ? resolveAppConfig : () => appConfig;
24
-
25
- function requireAssistantSurface(targetSurfaceId = "") {
26
- const assistantSurface = resolveAssistantSurfaceConfig(resolveCurrentAppConfig(), targetSurfaceId);
27
- if (assistantSurface) {
28
- return assistantSurface;
29
- }
30
-
31
- throw new AppError(404, "Assistant not found.");
32
- }
33
-
34
21
  async function requireSettingsAccess(assistantSurface, context = {}, { mode = "read" } = {}) {
35
22
  requireAuth({ context }, {
36
23
  require: "authenticated"
@@ -82,7 +69,7 @@ function createService({
82
69
  }
83
70
 
84
71
  async function getSettings(input = {}, options = {}) {
85
- const assistantSurface = requireAssistantSurface(input?.targetSurfaceId);
72
+ const assistantSurface = requireAssistantSurface(appConfig, input?.targetSurfaceId);
86
73
  await requireSettingsAccess(assistantSurface, options?.context, {
87
74
  mode: "read"
88
75
  });
@@ -100,7 +87,7 @@ function createService({
100
87
  }
101
88
 
102
89
  async function updateSettings(input = {}, patch = {}, options = {}) {
103
- const assistantSurface = requireAssistantSurface(input?.targetSurfaceId);
90
+ const assistantSurface = requireAssistantSurface(appConfig, input?.targetSurfaceId);
104
91
  await requireSettingsAccess(assistantSurface, options?.context, {
105
92
  mode: "write"
106
93
  });
@@ -116,7 +103,7 @@ function createService({
116
103
  }
117
104
 
118
105
  async function resolveSystemPrompt(assistantSurface, workspace = null, _options = {}, serviceOptions = {}) {
119
- const resolvedAssistantSurface = requireAssistantSurface(assistantSurface?.targetSurfaceId);
106
+ const resolvedAssistantSurface = requireAssistantSurface(appConfig, assistantSurface?.targetSurfaceId);
120
107
  const workspaceId = resolveConfigWorkspaceId(
121
108
  resolvedAssistantSurface,
122
109
  workspace,
@@ -2,11 +2,12 @@ import { isDeepStrictEqual } from "node:util";
2
2
  import { AppError } from "@jskit-ai/kernel/server/runtime";
3
3
  import { normalizeObject, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
4
4
  import { resolveWorkspaceSlug } from "@jskit-ai/assistant-core/server";
5
- import { ASSISTANT_STREAM_EVENT_TYPES, isAssistantProgressOnlyText } from "@jskit-ai/assistant-core/shared";
6
- import { resolveAssistantSurfaceConfig } from "../../shared/assistantSurfaces.js";
5
+ 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 { exposedAppErrorDetails, errorStatus } from "../support/assistantErrors.js";
8
+ import { requireAssistantSurface } from "../support/assistantSurface.js";
9
+ import { ASSISTANT_STREAM_WRITER_METHODS } from "../support/assistantStreamWriter.js";
7
10
 
8
- const MAX_HISTORY_MESSAGES = 20;
9
- const MAX_INPUT_CHARS = 8000;
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.";
11
12
 
12
13
  function normalizeConversationId(value) {
@@ -71,16 +72,7 @@ function normalizeStreamInput(payload = {}) {
71
72
  }
72
73
 
73
74
  function hasStreamWriter(streamWriter) {
74
- return Boolean(
75
- streamWriter &&
76
- typeof streamWriter.sendMeta === "function" &&
77
- typeof streamWriter.sendAssistantDelta === "function" &&
78
- typeof streamWriter.sendAssistantMessage === "function" &&
79
- typeof streamWriter.sendToolCall === "function" &&
80
- typeof streamWriter.sendToolResult === "function" &&
81
- typeof streamWriter.sendError === "function" &&
82
- typeof streamWriter.sendDone === "function"
83
- );
75
+ return ASSISTANT_STREAM_WRITER_METHODS.every(name => typeof streamWriter?.[name] === "function");
84
76
  }
85
77
 
86
78
  function isAbortError(error) {
@@ -157,15 +149,6 @@ function buildSystemPrompt({ targetSurfaceId = "", toolDescriptors = [], workspa
157
149
  return promptSegments.join(" ");
158
150
  }
159
151
 
160
- function requireAssistantSurface(appConfig = {}, targetSurfaceId = "") {
161
- const assistantSurface = resolveAssistantSurfaceConfig(appConfig, targetSurfaceId);
162
- if (assistantSurface) {
163
- return assistantSurface;
164
- }
165
-
166
- throw new AppError(404, "Assistant not found.");
167
- }
168
-
169
152
  function buildAssistantActionContext(context = {}, assistantSurface = {}) {
170
153
  return {
171
154
  ...normalizeObject(context),
@@ -182,7 +165,6 @@ function createChatService({
182
165
  serviceToolCatalog,
183
166
  assistantConfigService,
184
167
  appConfig = {},
185
- resolveAppConfig = null,
186
168
  workspaceScopeSupport = null
187
169
  } = {}) {
188
170
  if (!aiClientFactory || typeof aiClientFactory.resolveClient !== "function" || !turnRequests || !transcriptService || !serviceToolCatalog || !assistantConfigService) {
@@ -191,9 +173,6 @@ function createChatService({
191
173
  );
192
174
  }
193
175
 
194
- const resolveCurrentAppConfig =
195
- typeof resolveAppConfig === "function" ? resolveAppConfig : () => appConfig;
196
-
197
176
  function resolveRuntimeWorkspace(assistantSurface, context = {}, input = {}) {
198
177
  if (assistantSurface?.runtimeSurfaceRequiresWorkspace !== true) {
199
178
  return null;
@@ -230,7 +209,7 @@ function createChatService({
230
209
  }
231
210
 
232
211
  async function streamChat(payload = {}, options = {}) {
233
- const assistantSurface = requireAssistantSurface(resolveCurrentAppConfig(), payload?.targetSurfaceId);
212
+ const assistantSurface = requireAssistantSurface(appConfig, payload?.targetSurfaceId);
234
213
  const context = normalizeObject(options.context);
235
214
  const assistantContext = buildAssistantActionContext(context, assistantSurface);
236
215
  const workspace = resolveRuntimeWorkspace(assistantSurface, assistantContext, payload);
@@ -270,9 +249,9 @@ function createChatService({
270
249
  result = await executeTurn();
271
250
  } catch (error) {
272
251
  response.failure = { message: String(error?.message || "Assistant request failed."),
273
- status: Number(error?.status || error?.statusCode || 500),
252
+ status: errorStatus(error),
274
253
  ...(error instanceof AppError ? { code: error.code,
275
- ...(error.details && String(error.code).trim() !== "ACTION_PERMISSION_DENIED" ? { details: error.details } : {}) } : {}) };
254
+ ...(exposedAppErrorDetails(error) ? { details: error.details } : {}) } : {}) };
276
255
  await turnRequests.update(claim, response, "failed");
277
256
  throw error;
278
257
  }
@@ -307,10 +286,8 @@ function createChatService({
307
286
  const resolvedAttachments = [];
308
287
  async function prepareAttachments(attachmentIds) {
309
288
  if (attachmentIds === undefined || (Array.isArray(attachmentIds) && !attachmentIds.length)) return;
310
- if (!Array.isArray(attachmentIds) || attachmentIds.length > 10 ||
311
- attachmentIds.some(id => typeof id !== "string" || !id || id.length > 256)) {
312
- throw new AppError(400, "Invalid attachment identifiers.");
313
- }
289
+ try { conversationAttachmentIds(attachmentIds); }
290
+ catch { throw new AppError(400, "Invalid attachment identifiers."); }
314
291
  if (!attachments?.resolve || !aiClient.supportsAttachments) {
315
292
  throw new AppError(400, "File attachments are not configured for this assistant.");
316
293
  }
@@ -402,7 +379,7 @@ function createChatService({
402
379
  streamWriter.sendError({ type: ASSISTANT_STREAM_EVENT_TYPES.ERROR, messageId: source.messageId,
403
380
  code: aborted ? "assistant_stream_aborted" : String(error?.code || "assistant_stream_failed"),
404
381
  message: aborted ? "Assistant request was cancelled." : String(error?.message || "Assistant request failed."),
405
- status: aborted ? 499 : Number(error?.status || error?.statusCode || 500) });
382
+ status: aborted ? 499 : errorStatus(error) });
406
383
  streamWriter.sendDone({ type: ASSISTANT_STREAM_EVENT_TYPES.DONE, messageId: source.messageId, status });
407
384
  return { conversationId, messageId: source.messageId, status };
408
385
  } finally {
@@ -414,7 +391,7 @@ function createChatService({
414
391
  }
415
392
 
416
393
  async function listConversations(query = {}, options = {}) {
417
- const assistantSurface = requireAssistantSurface(resolveCurrentAppConfig(), options?.input?.targetSurfaceId);
394
+ const assistantSurface = requireAssistantSurface(appConfig, options?.input?.targetSurfaceId);
418
395
  const context = normalizeObject(options.context);
419
396
  const assistantContext = buildAssistantActionContext(context, assistantSurface);
420
397
  const workspace = resolveRuntimeWorkspace(assistantSurface, assistantContext, options.input || {});
@@ -424,7 +401,7 @@ function createChatService({
424
401
  }
425
402
 
426
403
  async function getConversationMessages(conversationId, query = {}, options = {}) {
427
- const assistantSurface = requireAssistantSurface(resolveCurrentAppConfig(), options?.input?.targetSurfaceId);
404
+ const assistantSurface = requireAssistantSurface(appConfig, options?.input?.targetSurfaceId);
428
405
  const context = normalizeObject(options.context);
429
406
  const assistantContext = buildAssistantActionContext(context, assistantSurface);
430
407
  const workspace = resolveRuntimeWorkspace(assistantSurface, assistantContext, options.input || {});