@jskit-ai/assistant-runtime 0.1.1

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 (30) hide show
  1. package/package.descriptor.mjs +136 -0
  2. package/package.json +21 -0
  3. package/src/client/components/AssistantSettingsClientElement.vue +204 -0
  4. package/src/client/components/AssistantSurfaceClientElement.vue +19 -0
  5. package/src/client/composables/useAssistantRuntime.js +759 -0
  6. package/src/client/index.js +4 -0
  7. package/src/client/providers/AssistantClientProvider.js +16 -0
  8. package/src/server/AssistantProvider.js +152 -0
  9. package/src/server/actionIds.js +9 -0
  10. package/src/server/actions.js +151 -0
  11. package/src/server/inputValidators.js +41 -0
  12. package/src/server/registerRoutes.js +450 -0
  13. package/src/server/repositories/assistantConfigRepository.js +148 -0
  14. package/src/server/repositories/conversationsRepository.js +263 -0
  15. package/src/server/repositories/messagesRepository.js +166 -0
  16. package/src/server/services/assistantConfigService.js +132 -0
  17. package/src/server/services/chatService.js +1048 -0
  18. package/src/server/services/transcriptService.js +331 -0
  19. package/src/server/support/assistantServerConfig.js +106 -0
  20. package/src/server/support/createSurfaceAwareToolCatalog.js +64 -0
  21. package/src/shared/assistantRuntimeConfig.js +7 -0
  22. package/src/shared/assistantSurfaces.js +97 -0
  23. package/src/shared/index.js +7 -0
  24. package/templates/migrations/assistant_config_initial.cjs +27 -0
  25. package/templates/migrations/assistant_transcripts_initial.cjs +58 -0
  26. package/test/assistantServerConfig.test.js +72 -0
  27. package/test/assistantSurfaces.test.js +50 -0
  28. package/test/createSurfaceAwareToolCatalog.test.js +77 -0
  29. package/test/lazyAppConfig.test.js +248 -0
  30. package/test/packageDescriptor.test.js +34 -0
@@ -0,0 +1,331 @@
1
+ import { AppError, parsePositiveInteger } from "@jskit-ai/kernel/server/runtime";
2
+ import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
3
+ import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
4
+ import { normalizeConversationStatus } from "@jskit-ai/assistant-core/shared";
5
+
6
+ const DEFAULT_PAGE_SIZE = 20;
7
+ const MAX_PAGE_SIZE = 200;
8
+ const DEFAULT_MESSAGES_PAGE_SIZE = 200;
9
+ const MAX_MESSAGES_PAGE_SIZE = 500;
10
+ const DEFAULT_CONVERSATION_TITLE = "New conversation";
11
+ const MAX_CONVERSATION_TITLE_LENGTH = 80;
12
+
13
+ function resolveWorkspaceId(workspace, { required = false } = {}) {
14
+ const workspaceId = parsePositiveInteger(workspace?.id || workspace);
15
+ if (!workspaceId && required) {
16
+ throw new AppError(409, "Workspace selection required.");
17
+ }
18
+
19
+ return workspaceId || null;
20
+ }
21
+
22
+ function resolveActorUserId(user, { required = false } = {}) {
23
+ const actorUserId = parsePositiveInteger(user?.id);
24
+ if (!actorUserId && required) {
25
+ throw new AppError(401, "Authentication required.");
26
+ }
27
+
28
+ return actorUserId || null;
29
+ }
30
+
31
+ function normalizePagination(pagination = {}, { defaultPageSize = DEFAULT_PAGE_SIZE, maxPageSize = MAX_PAGE_SIZE } = {}) {
32
+ const page = Math.max(1, parsePositiveInteger(pagination.page) || 1);
33
+ const pageSize = Math.max(1, Math.min(maxPageSize, parsePositiveInteger(pagination.pageSize) || defaultPageSize));
34
+
35
+ return {
36
+ page,
37
+ pageSize
38
+ };
39
+ }
40
+
41
+ function normalizeCursorPagination(query = {}, { defaultLimit = DEFAULT_PAGE_SIZE, maxLimit = MAX_PAGE_SIZE } = {}) {
42
+ const cursor = parsePositiveInteger(query.cursor) || 0;
43
+ const limit = Math.max(1, Math.min(maxLimit, parsePositiveInteger(query.limit) || defaultLimit));
44
+
45
+ return {
46
+ cursor,
47
+ limit
48
+ };
49
+ }
50
+
51
+ function deriveConversationTitleFromMessage(contentText) {
52
+ const normalized = String(contentText == null ? "" : contentText).replace(/\s+/g, " ").trim();
53
+ if (!normalized) {
54
+ return "";
55
+ }
56
+
57
+ return normalized.slice(0, MAX_CONVERSATION_TITLE_LENGTH).trim();
58
+ }
59
+
60
+ function isDefaultConversationTitle(value) {
61
+ return normalizeText(value).toLowerCase() === DEFAULT_CONVERSATION_TITLE.toLowerCase();
62
+ }
63
+
64
+ function requireAssistantSurface(assistantSurface = {}) {
65
+ const targetSurfaceId = normalizeSurfaceId(assistantSurface?.targetSurfaceId);
66
+ if (!targetSurfaceId) {
67
+ throw new TypeError("assistant transcript service requires assistantSurface.targetSurfaceId.");
68
+ }
69
+
70
+ return Object.freeze({
71
+ targetSurfaceId,
72
+ runtimeSurfaceRequiresWorkspace: assistantSurface?.runtimeSurfaceRequiresWorkspace === true
73
+ });
74
+ }
75
+
76
+ function createTranscriptService({ conversationsRepository, messagesRepository } = {}) {
77
+ if (!conversationsRepository || !messagesRepository) {
78
+ throw new Error("createTranscriptService requires conversationsRepository and messagesRepository.");
79
+ }
80
+
81
+ function resolveExpectedWorkspaceId(assistantSurface, workspace) {
82
+ const resolvedAssistantSurface = requireAssistantSurface(assistantSurface);
83
+ return resolveWorkspaceId(workspace, {
84
+ required: resolvedAssistantSurface.runtimeSurfaceRequiresWorkspace === true
85
+ });
86
+ }
87
+
88
+ async function createConversationForTurn(assistantSurface, workspace, user, options = {}) {
89
+ const resolvedAssistantSurface = requireAssistantSurface(assistantSurface);
90
+ const workspaceId = resolveExpectedWorkspaceId(resolvedAssistantSurface, workspace);
91
+ const actorUserId = resolveActorUserId(user, {
92
+ required: true
93
+ });
94
+ const source = normalizeObject(options);
95
+ const conversationId = parsePositiveInteger(source.conversationId);
96
+
97
+ if (conversationId) {
98
+ const existing = await conversationsRepository.findByIdForActorScope(conversationId, {
99
+ workspaceId,
100
+ actorUserId,
101
+ surfaceId: resolvedAssistantSurface.targetSurfaceId
102
+ });
103
+ if (!existing) {
104
+ throw new AppError(404, "Conversation not found.");
105
+ }
106
+
107
+ if (existing.status !== "active") {
108
+ const reopened = await conversationsRepository.updateById(existing.id, {
109
+ status: "active",
110
+ endedAt: null
111
+ });
112
+
113
+ return {
114
+ conversation: reopened,
115
+ created: false
116
+ };
117
+ }
118
+
119
+ return {
120
+ conversation: existing,
121
+ created: false
122
+ };
123
+ }
124
+
125
+ const createdConversation = await conversationsRepository.create({
126
+ workspaceId,
127
+ createdByUserId: actorUserId,
128
+ title: normalizeText(source.title) || DEFAULT_CONVERSATION_TITLE,
129
+ status: "active",
130
+ provider: normalizeText(source.provider),
131
+ model: normalizeText(source.model),
132
+ surfaceId: normalizeSurfaceId(source.surfaceId) || resolvedAssistantSurface.targetSurfaceId,
133
+ metadata: {
134
+ firstMessageId: normalizeText(source.messageId)
135
+ }
136
+ });
137
+
138
+ return {
139
+ conversation: createdConversation,
140
+ created: true
141
+ };
142
+ }
143
+
144
+ async function appendMessage(assistantSurface, conversationId, payload = {}, options = {}) {
145
+ const resolvedAssistantSurface = requireAssistantSurface(assistantSurface);
146
+ const numericConversationId = parsePositiveInteger(conversationId);
147
+ if (!numericConversationId) {
148
+ throw new TypeError("appendMessage requires conversationId.");
149
+ }
150
+
151
+ const source = normalizeObject(payload);
152
+ const context = normalizeObject(options.context);
153
+ const actorUserId = parsePositiveInteger(source.actorUserId) || resolveActorUserId(context.actor);
154
+ const workspaceId = resolveExpectedWorkspaceId(resolvedAssistantSurface, options.workspace || context.workspace);
155
+ const conversation = await conversationsRepository.findByIdForActorScope(numericConversationId, {
156
+ workspaceId,
157
+ actorUserId,
158
+ surfaceId: resolvedAssistantSurface.targetSurfaceId
159
+ });
160
+ if (!conversation) {
161
+ throw new AppError(404, "Conversation not found.");
162
+ }
163
+
164
+ const createdMessage = await messagesRepository.create({
165
+ conversationId: numericConversationId,
166
+ workspaceId: conversation.workspaceId,
167
+ role: normalizeText(source.role).toLowerCase(),
168
+ kind: normalizeText(source.kind).toLowerCase() || "chat",
169
+ clientMessageSid: normalizeText(source.clientMessageSid),
170
+ actorUserId,
171
+ contentText: source.contentText == null ? null : String(source.contentText),
172
+ metadata: normalizeObject(source.metadata)
173
+ });
174
+
175
+ await conversationsRepository.incrementMessageCount(numericConversationId, 1);
176
+
177
+ const messageRole = normalizeText(source.role).toLowerCase();
178
+ const messageKind = normalizeText(source.kind).toLowerCase() || "chat";
179
+ if (messageRole === "user" && messageKind === "chat" && isDefaultConversationTitle(conversation.title)) {
180
+ const derivedTitle = deriveConversationTitleFromMessage(source.contentText);
181
+ if (derivedTitle) {
182
+ await conversationsRepository.updateById(numericConversationId, {
183
+ title: derivedTitle
184
+ });
185
+ }
186
+ }
187
+
188
+ return {
189
+ conversationId: numericConversationId,
190
+ message: createdMessage
191
+ };
192
+ }
193
+
194
+ async function completeConversation(assistantSurface, conversationId, payload = {}, options = {}) {
195
+ const resolvedAssistantSurface = requireAssistantSurface(assistantSurface);
196
+ const numericConversationId = parsePositiveInteger(conversationId);
197
+ if (!numericConversationId) {
198
+ throw new TypeError("completeConversation requires conversationId.");
199
+ }
200
+
201
+ const source = normalizeObject(payload);
202
+ const context = normalizeObject(options.context);
203
+ const actorUserId = resolveActorUserId(context.actor, {
204
+ required: true
205
+ });
206
+ const workspaceId = resolveExpectedWorkspaceId(resolvedAssistantSurface, options.workspace || context.workspace);
207
+ const existing = await conversationsRepository.findByIdForActorScope(numericConversationId, {
208
+ workspaceId,
209
+ actorUserId,
210
+ surfaceId: resolvedAssistantSurface.targetSurfaceId
211
+ });
212
+ if (!existing) {
213
+ throw new AppError(404, "Conversation not found.");
214
+ }
215
+
216
+ return conversationsRepository.updateById(numericConversationId, {
217
+ status: normalizeConversationStatus(source.status, {
218
+ fallback: "completed"
219
+ }),
220
+ endedAt: source.endedAt || new Date(),
221
+ metadata: {
222
+ ...normalizeObject(existing.metadata),
223
+ ...normalizeObject(source.metadata)
224
+ }
225
+ });
226
+ }
227
+
228
+ async function listConversationsForUser(assistantSurface, workspace, user, query = {}) {
229
+ const resolvedAssistantSurface = requireAssistantSurface(assistantSurface);
230
+ const workspaceId = resolveExpectedWorkspaceId(resolvedAssistantSurface, workspace);
231
+ const actorUserId = resolveActorUserId(user, {
232
+ required: true
233
+ });
234
+ const pagination = normalizeCursorPagination(query, {
235
+ defaultLimit: DEFAULT_PAGE_SIZE,
236
+ maxLimit: MAX_PAGE_SIZE
237
+ });
238
+
239
+ const status = normalizeConversationStatus(query.status, {
240
+ fallback: ""
241
+ });
242
+ const filters = {
243
+ ...(status ? { status } : {})
244
+ };
245
+
246
+ return conversationsRepository.listForActorScope(
247
+ {
248
+ workspaceId,
249
+ actorUserId,
250
+ surfaceId: resolvedAssistantSurface.targetSurfaceId,
251
+ pagination: {
252
+ cursor: pagination.cursor,
253
+ limit: pagination.limit
254
+ },
255
+ filters
256
+ }
257
+ );
258
+ }
259
+
260
+ async function getConversationMessagesForUser(assistantSurface, workspace, user, conversationId, query = {}) {
261
+ const resolvedAssistantSurface = requireAssistantSurface(assistantSurface);
262
+ const workspaceId = resolveExpectedWorkspaceId(resolvedAssistantSurface, workspace);
263
+ const actorUserId = resolveActorUserId(user, {
264
+ required: true
265
+ });
266
+ const numericConversationId = parsePositiveInteger(conversationId);
267
+ if (!numericConversationId) {
268
+ throw new AppError(400, "Validation failed.", {
269
+ details: {
270
+ fieldErrors: {
271
+ conversationId: "conversationId must be a positive integer."
272
+ }
273
+ }
274
+ });
275
+ }
276
+
277
+ const conversation = await conversationsRepository.findByIdForActorScope(
278
+ numericConversationId,
279
+ {
280
+ workspaceId,
281
+ actorUserId,
282
+ surfaceId: resolvedAssistantSurface.targetSurfaceId
283
+ }
284
+ );
285
+ if (!conversation) {
286
+ throw new AppError(404, "Conversation not found.");
287
+ }
288
+
289
+ const pagination = normalizePagination(query, {
290
+ defaultPageSize: DEFAULT_MESSAGES_PAGE_SIZE,
291
+ maxPageSize: MAX_MESSAGES_PAGE_SIZE
292
+ });
293
+ const total = await messagesRepository.countByConversationScope(
294
+ numericConversationId,
295
+ {
296
+ workspaceId
297
+ }
298
+ );
299
+ const totalPages = Math.max(1, Math.ceil(total / pagination.pageSize));
300
+ const page = Math.min(pagination.page, totalPages);
301
+ const entries = await messagesRepository.listByConversationScope(
302
+ numericConversationId,
303
+ {
304
+ workspaceId
305
+ },
306
+ {
307
+ page,
308
+ pageSize: pagination.pageSize
309
+ }
310
+ );
311
+
312
+ return {
313
+ conversation,
314
+ entries,
315
+ page,
316
+ pageSize: pagination.pageSize,
317
+ total,
318
+ totalPages
319
+ };
320
+ }
321
+
322
+ return Object.freeze({
323
+ createConversationForTurn,
324
+ appendMessage,
325
+ completeConversation,
326
+ listConversationsForUser,
327
+ getConversationMessagesForUser
328
+ });
329
+ }
330
+
331
+ export { createTranscriptService };
@@ -0,0 +1,106 @@
1
+ import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
2
+ import { normalizeObject, normalizeText, normalizeUniqueTextList } from "@jskit-ai/kernel/shared/support/normalize";
3
+ import {
4
+ DEFAULT_AI_TIMEOUT_MS,
5
+ normalizeOptionalHttpUrl,
6
+ normalizeTimeoutMs
7
+ } from "@jskit-ai/assistant-core/server";
8
+
9
+ function resolveAssistantServerConfigMap(appConfig = {}) {
10
+ const source = normalizeObject(appConfig?.assistantServer);
11
+ const resolved = {};
12
+
13
+ for (const [rawSurfaceId, rawEntry] of Object.entries(source)) {
14
+ const targetSurfaceId = normalizeSurfaceId(rawSurfaceId);
15
+ const entry = normalizeObject(rawEntry);
16
+ if (!targetSurfaceId) {
17
+ continue;
18
+ }
19
+
20
+ resolved[targetSurfaceId] = Object.freeze({
21
+ aiConfigPrefix: normalizeText(entry.aiConfigPrefix),
22
+ provider: normalizeText(entry.provider).toLowerCase(),
23
+ apiKey: normalizeText(entry.apiKey),
24
+ baseUrl: normalizeText(entry.baseUrl),
25
+ model: normalizeText(entry.model),
26
+ timeoutMs: entry.timeoutMs,
27
+ barredActionIds: Object.freeze(normalizeUniqueTextList(entry.barredActionIds, { acceptSingle: true })),
28
+ toolSkipActionPrefixes: Object.freeze(normalizeUniqueTextList(entry.toolSkipActionPrefixes, { acceptSingle: true }))
29
+ });
30
+ }
31
+
32
+ return Object.freeze(resolved);
33
+ }
34
+
35
+ function resolveAssistantServerConfig(appConfig = {}, targetSurfaceId = "") {
36
+ const normalizedTargetSurfaceId = normalizeSurfaceId(targetSurfaceId);
37
+ if (!normalizedTargetSurfaceId) {
38
+ return Object.freeze({});
39
+ }
40
+
41
+ const serverConfigMap = resolveAssistantServerConfigMap(appConfig);
42
+ return serverConfigMap[normalizedTargetSurfaceId] || Object.freeze({});
43
+ }
44
+
45
+ function readPrefixedEnvValue(env = {}, prefix = "", key = "") {
46
+ const normalizedPrefix = normalizeText(prefix);
47
+ const normalizedKey = normalizeText(key);
48
+ if (!normalizedPrefix || !normalizedKey) {
49
+ return "";
50
+ }
51
+
52
+ return normalizeText(env[`${normalizedPrefix}_${normalizedKey}`]);
53
+ }
54
+
55
+ function resolveAssistantAiConfig({ appConfig = {}, env = {} } = {}, targetSurfaceId = "") {
56
+ const surfaceServerConfig = resolveAssistantServerConfig(appConfig, targetSurfaceId);
57
+ const normalizedTargetSurfaceId = normalizeSurfaceId(targetSurfaceId);
58
+ if (!normalizedTargetSurfaceId) {
59
+ throw new Error("assistant AI config requires targetSurfaceId.");
60
+ }
61
+
62
+ const aiConfigPrefix = normalizeText(surfaceServerConfig.aiConfigPrefix);
63
+ if (!aiConfigPrefix) {
64
+ throw new Error(
65
+ `assistant server config for surface "${normalizedTargetSurfaceId}" requires assistantServer.${normalizedTargetSurfaceId}.aiConfigPrefix.`
66
+ );
67
+ }
68
+
69
+ const provider = normalizeText(
70
+ readPrefixedEnvValue(env, aiConfigPrefix, "AI_PROVIDER") || surfaceServerConfig.provider
71
+ ).toLowerCase() || "openai";
72
+ const apiKey = normalizeText(
73
+ readPrefixedEnvValue(env, aiConfigPrefix, "AI_API_KEY") || surfaceServerConfig.apiKey
74
+ );
75
+ const baseUrl = normalizeOptionalHttpUrl(
76
+ readPrefixedEnvValue(env, aiConfigPrefix, "AI_BASE_URL") || surfaceServerConfig.baseUrl,
77
+ {
78
+ context: "assistant AI_BASE_URL"
79
+ }
80
+ );
81
+ const model = normalizeText(
82
+ readPrefixedEnvValue(env, aiConfigPrefix, "AI_MODEL") || surfaceServerConfig.model
83
+ );
84
+ const timeoutMs = normalizeTimeoutMs(
85
+ readPrefixedEnvValue(env, aiConfigPrefix, "AI_TIMEOUT_MS") || surfaceServerConfig.timeoutMs,
86
+ DEFAULT_AI_TIMEOUT_MS
87
+ );
88
+
89
+ return Object.freeze({
90
+ aiConfigPrefix,
91
+ ai: Object.freeze({
92
+ enabled: Boolean(apiKey),
93
+ provider,
94
+ apiKey,
95
+ baseUrl,
96
+ model,
97
+ timeoutMs
98
+ })
99
+ });
100
+ }
101
+
102
+ export {
103
+ resolveAssistantServerConfigMap,
104
+ resolveAssistantServerConfig,
105
+ resolveAssistantAiConfig
106
+ };
@@ -0,0 +1,64 @@
1
+ import { createServiceToolCatalog } from "@jskit-ai/assistant-core/server";
2
+ import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
3
+ import { normalizeObject } from "@jskit-ai/kernel/shared/support/normalize";
4
+ import { resolveAssistantServerConfig } from "./assistantServerConfig.js";
5
+
6
+ function buildCatalogOptions(appConfig = {}, surfaceId = "") {
7
+ const surfaceConfig = resolveAssistantServerConfig(appConfig, surfaceId);
8
+
9
+ return Object.freeze({
10
+ barredActionIds: surfaceConfig.barredActionIds || [],
11
+ skipActionPrefixes: Object.freeze(["assistant.", ...(surfaceConfig.toolSkipActionPrefixes || [])])
12
+ });
13
+ }
14
+
15
+ function requireContextSurfaceId(context = {}) {
16
+ const surfaceId = normalizeSurfaceId(context?.surface);
17
+ if (surfaceId) {
18
+ return surfaceId;
19
+ }
20
+
21
+ throw new Error("assistant surface-aware tool catalog requires context.surface.");
22
+ }
23
+
24
+ function createSurfaceAwareToolCatalog(scope, { appConfig = {}, resolveAppConfig = null, createCatalog = createServiceToolCatalog } = {}) {
25
+ if (!scope) {
26
+ throw new Error("createSurfaceAwareToolCatalog requires scope.");
27
+ }
28
+
29
+ const resolveCurrentAppConfig =
30
+ typeof resolveAppConfig === "function" ? () => normalizeObject(resolveAppConfig()) : () => normalizeObject(appConfig);
31
+ const cache = new Map();
32
+ let schemaCatalog = null;
33
+
34
+ function resolveCatalog(surfaceId = "") {
35
+ if (cache.has(surfaceId)) {
36
+ return cache.get(surfaceId);
37
+ }
38
+
39
+ const nextCatalog = createCatalog(scope, buildCatalogOptions(resolveCurrentAppConfig(), surfaceId));
40
+ cache.set(surfaceId, nextCatalog);
41
+ return nextCatalog;
42
+ }
43
+
44
+ return Object.freeze({
45
+ resolveToolSet(context = {}) {
46
+ const surfaceId = requireContextSurfaceId(context);
47
+ return resolveCatalog(surfaceId).resolveToolSet(context);
48
+ },
49
+ toOpenAiToolSchema(tool) {
50
+ if (!schemaCatalog) {
51
+ schemaCatalog = createCatalog(scope, buildCatalogOptions(resolveCurrentAppConfig(), ""));
52
+ }
53
+
54
+ return schemaCatalog.toOpenAiToolSchema(tool);
55
+ },
56
+ executeToolCall(payload = {}) {
57
+ const context = payload?.context && typeof payload.context === "object" ? payload.context : {};
58
+ const surfaceId = requireContextSurfaceId(context);
59
+ return resolveCatalog(surfaceId).executeToolCall(payload);
60
+ }
61
+ });
62
+ }
63
+
64
+ export { createSurfaceAwareToolCatalog };
@@ -0,0 +1,7 @@
1
+ const assistantRuntimeConfig = Object.freeze({
2
+ configTable: "assistant_config",
3
+ conversationsTable: "assistant_conversations",
4
+ messagesTable: "assistant_messages"
5
+ });
6
+
7
+ export { assistantRuntimeConfig };
@@ -0,0 +1,97 @@
1
+ import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
2
+ import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
+
4
+ const CONSOLE_OWNER_ACCESS_POLICY_ID = "console_owner";
5
+
6
+ function resolveSurfaceDefinitions(appConfig = {}) {
7
+ const source = normalizeObject(appConfig?.surfaceDefinitions);
8
+ const resolved = {};
9
+
10
+ for (const [rawSurfaceId, rawDefinition] of Object.entries(source)) {
11
+ const surfaceId = normalizeSurfaceId(rawSurfaceId);
12
+ const definition = normalizeObject(rawDefinition);
13
+ if (!surfaceId || definition.enabled === false) {
14
+ continue;
15
+ }
16
+
17
+ resolved[surfaceId] = Object.freeze({
18
+ id: surfaceId,
19
+ requiresWorkspace: definition.requiresWorkspace === true,
20
+ accessPolicyId: normalizeText(definition.accessPolicyId).toLowerCase()
21
+ });
22
+ }
23
+
24
+ return Object.freeze(resolved);
25
+ }
26
+
27
+ function resolveAssistantSurfacesConfig(appConfig = {}) {
28
+ const source = normalizeObject(appConfig?.assistantSurfaces);
29
+ const resolved = {};
30
+
31
+ for (const [rawSurfaceId, rawEntry] of Object.entries(source)) {
32
+ const targetSurfaceId = normalizeSurfaceId(rawSurfaceId);
33
+ const entry = normalizeObject(rawEntry);
34
+ if (!targetSurfaceId) {
35
+ continue;
36
+ }
37
+
38
+ resolved[targetSurfaceId] = Object.freeze({
39
+ settingsSurfaceId: normalizeSurfaceId(entry.settingsSurfaceId),
40
+ configScope: normalizeAssistantConfigScope(entry.configScope)
41
+ });
42
+ }
43
+
44
+ return Object.freeze(resolved);
45
+ }
46
+
47
+ function normalizeAssistantConfigScope(value = "") {
48
+ const normalized = normalizeText(value).toLowerCase();
49
+ if (normalized === "workspace") {
50
+ return "workspace";
51
+ }
52
+
53
+ return "global";
54
+ }
55
+
56
+ function resolveAssistantSurfaceConfig(appConfig = {}, targetSurfaceId = "") {
57
+ const normalizedTargetSurfaceId = normalizeSurfaceId(targetSurfaceId);
58
+ if (!normalizedTargetSurfaceId) {
59
+ return null;
60
+ }
61
+
62
+ const assistantSurfaces = resolveAssistantSurfacesConfig(appConfig);
63
+ const surfaceConfig = assistantSurfaces[normalizedTargetSurfaceId];
64
+ if (!surfaceConfig) {
65
+ return null;
66
+ }
67
+
68
+ const surfaceDefinitions = resolveSurfaceDefinitions(appConfig);
69
+ const runtimeSurface = surfaceDefinitions[normalizedTargetSurfaceId];
70
+ const settingsSurface = surfaceDefinitions[surfaceConfig.settingsSurfaceId];
71
+ if (!runtimeSurface || !settingsSurface) {
72
+ return null;
73
+ }
74
+
75
+ if (
76
+ surfaceConfig.configScope === "workspace" &&
77
+ (runtimeSurface.requiresWorkspace !== true || settingsSurface.requiresWorkspace !== true)
78
+ ) {
79
+ return null;
80
+ }
81
+
82
+ return Object.freeze({
83
+ targetSurfaceId: normalizedTargetSurfaceId,
84
+ settingsSurfaceId: settingsSurface.id,
85
+ configScope: surfaceConfig.configScope,
86
+ runtimeSurfaceRequiresWorkspace: runtimeSurface.requiresWorkspace === true,
87
+ settingsSurfaceRequiresWorkspace: settingsSurface.requiresWorkspace === true,
88
+ settingsSurfaceRequiresConsoleOwner: settingsSurface.accessPolicyId === CONSOLE_OWNER_ACCESS_POLICY_ID
89
+ });
90
+ }
91
+
92
+ export {
93
+ normalizeAssistantConfigScope,
94
+ resolveAssistantSurfaceConfig,
95
+ resolveAssistantSurfacesConfig,
96
+ resolveSurfaceDefinitions
97
+ };
@@ -0,0 +1,7 @@
1
+ export { assistantRuntimeConfig } from "./assistantRuntimeConfig.js";
2
+ export {
3
+ normalizeAssistantConfigScope,
4
+ resolveAssistantSurfaceConfig,
5
+ resolveAssistantSurfacesConfig,
6
+ resolveSurfaceDefinitions
7
+ } from "./assistantSurfaces.js";
@@ -0,0 +1,27 @@
1
+ exports.up = async function up(knex) {
2
+ const hasTable = await knex.schema.hasTable("assistant_config");
3
+ if (!hasTable) {
4
+ const hasWorkspacesTable = await knex.schema.hasTable("workspaces");
5
+
6
+ await knex.schema.createTable("assistant_config", (table) => {
7
+ table.increments("id").unsigned().primary();
8
+ table.string("target_surface_id", 64).notNullable();
9
+ table.string("scope_key", 160).notNullable();
10
+ table.integer("workspace_id").unsigned().nullable();
11
+ if (hasWorkspacesTable) {
12
+ table.foreign("workspace_id").references("id").inTable("workspaces").onDelete("CASCADE");
13
+ }
14
+ table.text("system_prompt").notNullable().defaultTo("");
15
+ table.timestamp("created_at").notNullable().defaultTo(knex.fn.now());
16
+ table.timestamp("updated_at").notNullable().defaultTo(knex.fn.now());
17
+
18
+ table.unique(["target_surface_id", "scope_key"], "uq_assistant_config_target_surface_scope");
19
+ table.index(["target_surface_id"], "idx_assistant_config_target_surface");
20
+ table.index(["workspace_id"], "idx_assistant_config_workspace");
21
+ });
22
+ }
23
+ };
24
+
25
+ exports.down = async function down(knex) {
26
+ await knex.schema.dropTableIfExists("assistant_config");
27
+ };