@jskit-ai/assistant-runtime 0.1.25 → 0.1.27

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.
@@ -1,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/assistant-runtime",
4
- version: "0.1.25",
4
+ version: "0.1.27",
5
5
  kind: "runtime",
6
6
  description: "Shared assistant runtime with per-surface assistant registration.",
7
7
  dependsOn: [
@@ -74,13 +74,13 @@ export default Object.freeze({
74
74
  mutations: {
75
75
  dependencies: {
76
76
  runtime: {
77
- "@jskit-ai/assistant-core": "0.1.30",
78
- "@jskit-ai/database-runtime": "0.1.54",
79
- "@jskit-ai/http-runtime": "0.1.53",
80
- "@jskit-ai/kernel": "0.1.54",
81
- "@jskit-ai/shell-web": "0.1.53",
82
- "@jskit-ai/users-core": "0.1.64",
83
- "@jskit-ai/users-web": "0.1.69",
77
+ "@jskit-ai/assistant-core": "0.1.32",
78
+ "@jskit-ai/database-runtime": "0.1.56",
79
+ "@jskit-ai/http-runtime": "0.1.55",
80
+ "@jskit-ai/kernel": "0.1.56",
81
+ "@jskit-ai/shell-web": "0.1.55",
82
+ "@jskit-ai/users-core": "0.1.66",
83
+ "@jskit-ai/users-web": "0.1.71",
84
84
  "@tanstack/vue-query": "^5.90.5",
85
85
  "vuetify": "^4.0.0"
86
86
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-runtime",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./client": "./src/client/index.js",
@@ -8,14 +8,18 @@
8
8
  "./server/actionIds": "./src/server/actionIds.js"
9
9
  },
10
10
  "dependencies": {
11
- "@jskit-ai/assistant-core": "0.1.30",
12
- "@jskit-ai/database-runtime": "0.1.54",
13
- "@jskit-ai/http-runtime": "0.1.53",
14
- "@jskit-ai/kernel": "0.1.54",
15
- "@jskit-ai/shell-web": "0.1.53",
16
- "@jskit-ai/users-core": "0.1.64",
17
- "@jskit-ai/users-web": "0.1.69",
11
+ "@jskit-ai/assistant-core": "0.1.32",
12
+ "@jskit-ai/database-runtime": "0.1.56",
13
+ "@jskit-ai/http-runtime": "0.1.55",
14
+ "@jskit-ai/kernel": "0.1.56",
15
+ "@jskit-ai/shell-web": "0.1.55",
16
+ "@jskit-ai/users-core": "0.1.66",
17
+ "@jskit-ai/users-web": "0.1.71",
18
+ "json-rest-schema": "1.x.x",
18
19
  "@tanstack/vue-query": "^5.90.5",
19
20
  "vuetify": "^4.0.0"
21
+ },
22
+ "peerDependencies": {
23
+ "vue": "^3.5.13"
20
24
  }
21
25
  }
@@ -173,7 +173,7 @@ async function submit() {
173
173
 
174
174
  const validation = validateOperationSection({
175
175
  operation: assistantConfigResource.operations.patch,
176
- section: "bodyValidator",
176
+ section: "body",
177
177
  value: {
178
178
  systemPrompt: form.systemPrompt
179
179
  }
@@ -1,4 +1,4 @@
1
- import { computed, ref, watch } from "vue";
1
+ import { computed, nextTick, ref, watch } from "vue";
2
2
  import { useQueryClient } from "@tanstack/vue-query";
3
3
  import { getClientAppConfig } from "@jskit-ai/kernel/client";
4
4
  import { normalizeObject, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
@@ -23,6 +23,7 @@ import { useShellWebErrorRuntime } from "@jskit-ai/shell-web/client/error";
23
23
  import { usePagedCollection } from "@jskit-ai/users-web/client/composables/usePagedCollection";
24
24
  import { useSurfaceRouteContext } from "@jskit-ai/users-web/client/composables/useSurfaceRouteContext";
25
25
  import { resolveAssistantSurfaceConfig } from "../../shared/assistantSurfaces.js";
26
+ import { insertTextAtSelection } from "../support/composerInputSupport.js";
26
27
  import { useWorkspaceWebScopeSupport } from "../support/workspaceScopeSupport.js";
27
28
 
28
29
  const DEFAULT_STREAM_TIMEOUT_MS = 120_000;
@@ -523,6 +524,23 @@ function useAssistantRuntime({ api = null, surfaceId = "" } = {}) {
523
524
  return;
524
525
  }
525
526
 
527
+ if (event?.key === "Enter" && event?.altKey === true && event?.ctrlKey !== true && event?.metaKey !== true) {
528
+ event.preventDefault();
529
+
530
+ const target = event?.target;
531
+ const nextValue = insertTextAtSelection(input.value, target?.selectionStart, target?.selectionEnd, "\n");
532
+ input.value = nextValue.value;
533
+
534
+ void nextTick(() => {
535
+ if (!target || typeof target.setSelectionRange !== "function") {
536
+ return;
537
+ }
538
+
539
+ target.setSelectionRange(nextValue.selectionStart, nextValue.selectionEnd);
540
+ });
541
+ return;
542
+ }
543
+
526
544
  if (
527
545
  event?.key === "Enter" &&
528
546
  event?.shiftKey !== true &&
@@ -0,0 +1,29 @@
1
+ function normalizeSelectionBoundary(value, fallback, max) {
2
+ const parsed = Number(value);
3
+ if (!Number.isInteger(parsed) || parsed < 0) {
4
+ return fallback;
5
+ }
6
+
7
+ if (parsed > max) {
8
+ return max;
9
+ }
10
+
11
+ return parsed;
12
+ }
13
+
14
+ function insertTextAtSelection(source = "", selectionStart, selectionEnd, text = "") {
15
+ const value = String(source || "");
16
+ const maxBoundary = value.length;
17
+ const normalizedStart = normalizeSelectionBoundary(selectionStart, maxBoundary, maxBoundary);
18
+ const normalizedEnd = normalizeSelectionBoundary(selectionEnd, normalizedStart, maxBoundary);
19
+ const insertedText = String(text ?? "");
20
+ const nextBoundary = normalizedStart + insertedText.length;
21
+
22
+ return {
23
+ value: `${value.slice(0, normalizedStart)}${insertedText}${value.slice(normalizedEnd)}`,
24
+ selectionStart: nextBoundary,
25
+ selectionEnd: nextBoundary
26
+ };
27
+ }
28
+
29
+ export { insertTextAtSelection };
@@ -1,36 +1,90 @@
1
+ import { createSchema } from "json-rest-schema";
2
+ import {
3
+ composeSchemaDefinitions
4
+ } from "@jskit-ai/kernel/shared/validators";
5
+ import {
6
+ deepFreeze
7
+ } from "@jskit-ai/kernel/shared/support/deepFreeze";
1
8
  import {
2
9
  assistantConfigResource,
3
10
  assistantResource
4
11
  } from "@jskit-ai/assistant-core/shared";
5
12
  import { actionIds } from "./actionIds.js";
6
- import { assistantTargetSurfaceInputValidator } from "./inputValidators.js";
13
+ import { assistantTargetSurfaceInputValidator } from "./inputSchemas.js";
14
+
15
+ const runtimeConversationsListQueryInputValidator = deepFreeze({
16
+ schema: createSchema({
17
+ query: {
18
+ type: "object",
19
+ required: false,
20
+ schema: assistantResource.operations.conversationsList.query.schema
21
+ }
22
+ }),
23
+ mode: "patch"
24
+ });
25
+
26
+ const runtimeConversationsListInputValidator = composeSchemaDefinitions(
27
+ [assistantTargetSurfaceInputValidator, runtimeConversationsListQueryInputValidator],
28
+ {
29
+ mode: "patch",
30
+ context: "assistant-runtime conversations list action input"
31
+ }
32
+ );
7
33
 
8
- const runtimeQueryInputValidator = [
9
- assistantTargetSurfaceInputValidator,
10
- { query: assistantResource.operations.conversationsList.queryValidator }
11
- ];
34
+ const runtimeConversationMessagesListQueryInputValidator = deepFreeze({
35
+ schema: createSchema({
36
+ query: {
37
+ type: "object",
38
+ required: false,
39
+ schema: assistantResource.operations.conversationMessagesList.query.schema
40
+ }
41
+ }),
42
+ mode: "patch"
43
+ });
12
44
 
13
- const runtimeMessagesInputValidator = [
14
- assistantTargetSurfaceInputValidator,
15
- assistantResource.operations.conversationMessagesList.paramsValidator,
45
+ const runtimeConversationMessagesListInputValidator = composeSchemaDefinitions(
46
+ [
47
+ assistantTargetSurfaceInputValidator,
48
+ assistantResource.operations.conversationMessagesList.params,
49
+ runtimeConversationMessagesListQueryInputValidator
50
+ ],
16
51
  {
17
- query: assistantResource.operations.conversationMessagesList.queryValidator
52
+ mode: "patch",
53
+ context: "assistant-runtime conversation messages action input"
18
54
  }
19
- ];
55
+ );
20
56
 
21
- const runtimeChatInputValidator = [
22
- assistantTargetSurfaceInputValidator,
23
- assistantResource.operations.chatStream.bodyValidator
24
- ];
57
+ const runtimeChatStreamInputValidator = composeSchemaDefinitions(
58
+ [
59
+ assistantTargetSurfaceInputValidator,
60
+ assistantResource.operations.chatStream.body
61
+ ],
62
+ {
63
+ mode: "patch",
64
+ context: "assistant-runtime chat stream action input"
65
+ }
66
+ );
25
67
 
26
68
  const settingsReadInputValidator = assistantTargetSurfaceInputValidator;
27
69
 
28
- const settingsUpdateInputValidator = [
29
- assistantTargetSurfaceInputValidator,
70
+ const settingsUpdatePatchInputValidator = deepFreeze({
71
+ schema: createSchema({
72
+ patch: {
73
+ type: "object",
74
+ required: true,
75
+ schema: assistantConfigResource.operations.patch.body.schema
76
+ }
77
+ }),
78
+ mode: "patch"
79
+ });
80
+
81
+ const settingsUpdateInputValidator = composeSchemaDefinitions(
82
+ [assistantTargetSurfaceInputValidator, settingsUpdatePatchInputValidator],
30
83
  {
31
- patch: assistantConfigResource.operations.patch.bodyValidator
84
+ mode: "patch",
85
+ context: "assistant-runtime settings update action input"
32
86
  }
33
- ];
87
+ );
34
88
 
35
89
  const assistantActions = Object.freeze([
36
90
  {
@@ -42,7 +96,7 @@ const assistantActions = Object.freeze([
42
96
  permission: {
43
97
  require: "authenticated"
44
98
  },
45
- inputValidator: runtimeChatInputValidator,
99
+ input: runtimeChatStreamInputValidator,
46
100
  idempotency: "optional",
47
101
  audit: {
48
102
  actionName: actionIds.chatStream
@@ -65,8 +119,8 @@ const assistantActions = Object.freeze([
65
119
  permission: {
66
120
  require: "authenticated"
67
121
  },
68
- inputValidator: runtimeQueryInputValidator,
69
- outputValidator: assistantResource.operations.conversationsList.outputValidator,
122
+ input: runtimeConversationsListInputValidator,
123
+ output: assistantResource.operations.conversationsList.output,
70
124
  idempotency: "none",
71
125
  audit: {
72
126
  actionName: actionIds.conversationsList
@@ -88,8 +142,8 @@ const assistantActions = Object.freeze([
88
142
  permission: {
89
143
  require: "authenticated"
90
144
  },
91
- inputValidator: runtimeMessagesInputValidator,
92
- outputValidator: assistantResource.operations.conversationMessagesList.outputValidator,
145
+ input: runtimeConversationMessagesListInputValidator,
146
+ output: assistantResource.operations.conversationMessagesList.output,
93
147
  idempotency: "none",
94
148
  audit: {
95
149
  actionName: actionIds.conversationMessagesList
@@ -111,8 +165,8 @@ const assistantActions = Object.freeze([
111
165
  permission: {
112
166
  require: "authenticated"
113
167
  },
114
- inputValidator: settingsReadInputValidator,
115
- outputValidator: assistantConfigResource.operations.view.outputValidator,
168
+ input: settingsReadInputValidator,
169
+ output: assistantConfigResource.operations.view.output,
116
170
  idempotency: "none",
117
171
  audit: {
118
172
  actionName: actionIds.settingsRead
@@ -133,8 +187,8 @@ const assistantActions = Object.freeze([
133
187
  permission: {
134
188
  require: "authenticated"
135
189
  },
136
- inputValidator: settingsUpdateInputValidator,
137
- outputValidator: assistantConfigResource.operations.patch.outputValidator,
190
+ input: settingsUpdateInputValidator,
191
+ output: assistantConfigResource.operations.patch.output,
138
192
  idempotency: "optional",
139
193
  audit: {
140
194
  actionName: actionIds.settingsUpdate
@@ -0,0 +1,44 @@
1
+ import { createSchema } from "json-rest-schema";
2
+ import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
3
+
4
+ const assistantSurfaceRouteParamsSchema = createSchema({
5
+ surfaceId: {
6
+ type: "string",
7
+ required: true,
8
+ lowercase: true,
9
+ minLength: 1,
10
+ maxLength: 64
11
+ }
12
+ });
13
+
14
+ const assistantTargetSurfaceInputSchema = createSchema({
15
+ targetSurfaceId: {
16
+ type: "string",
17
+ required: true,
18
+ lowercase: true,
19
+ minLength: 1,
20
+ maxLength: 64
21
+ },
22
+ workspaceSlug: {
23
+ type: "string",
24
+ required: false,
25
+ lowercase: true,
26
+ minLength: 1,
27
+ maxLength: 160
28
+ }
29
+ });
30
+
31
+ const assistantSurfaceRouteParamsValidator = deepFreeze({
32
+ schema: assistantSurfaceRouteParamsSchema,
33
+ mode: "patch"
34
+ });
35
+
36
+ const assistantTargetSurfaceInputValidator = deepFreeze({
37
+ schema: assistantTargetSurfaceInputSchema,
38
+ mode: "patch"
39
+ });
40
+
41
+ export {
42
+ assistantSurfaceRouteParamsValidator,
43
+ assistantTargetSurfaceInputValidator
44
+ };
@@ -1,5 +1,6 @@
1
1
  import { AppError } from "@jskit-ai/kernel/server/runtime";
2
2
  import { resolveAppConfig } from "@jskit-ai/kernel/server/support";
3
+ import { composeSchemaDefinitions } from "@jskit-ai/kernel/shared/validators";
3
4
  import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
4
5
  import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
5
6
  import {
@@ -15,28 +16,21 @@ import {
15
16
  } from "@jskit-ai/assistant-core/server";
16
17
  import { resolveAssistantSurfaceConfig } from "../shared/assistantSurfaces.js";
17
18
  import { actionIds } from "./actionIds.js";
18
- import { assistantSurfaceRouteParamsValidator } from "./inputValidators.js";
19
+ import { assistantSurfaceRouteParamsValidator } from "./inputSchemas.js";
19
20
  import { resolveWorkspaceServerScopeSupport } from "./support/workspaceScopeSupport.js";
20
21
 
21
- function buildRouteParamsValidator(requiresWorkspace, workspaceScopeSupport = null) {
22
- if (requiresWorkspace === true) {
23
- if (!workspaceScopeSupport) {
24
- throw new Error("Assistant workspace routes require workspace server scope support.");
25
- }
26
-
27
- return [workspaceScopeSupport.paramsValidator, assistantSurfaceRouteParamsValidator];
28
- }
29
-
30
- return assistantSurfaceRouteParamsValidator;
31
- }
32
-
33
- function buildConversationMessagesRouteParamsValidator(requiresWorkspace, workspaceScopeSupport = null) {
34
- const validators = buildRouteParamsValidator(requiresWorkspace, workspaceScopeSupport);
35
- if (Array.isArray(validators)) {
36
- return validators.concat(assistantResource.operations.conversationMessagesList.paramsValidator);
22
+ function requireWorkspaceAssistantRouteParams(workspaceScopeSupport = null) {
23
+ if (!workspaceScopeSupport) {
24
+ throw new Error("Assistant workspace routes require workspace server scope support.");
37
25
  }
38
26
 
39
- return [validators, assistantResource.operations.conversationMessagesList.paramsValidator];
27
+ return composeSchemaDefinitions(
28
+ [workspaceScopeSupport.params, assistantSurfaceRouteParamsValidator],
29
+ {
30
+ mode: "patch",
31
+ context: "assistant-runtime workspace surface route params"
32
+ }
33
+ );
40
34
  }
41
35
 
42
36
  function readWorkspaceInput(request, requiresWorkspace, workspaceScopeSupport = null) {
@@ -146,6 +140,22 @@ function resolveRouteRequestState(
146
140
  });
147
141
  }
148
142
 
143
+ function buildChatStreamActionInput(routeInput = {}, requestBody = {}) {
144
+ const actionInput = {
145
+ ...routeInput,
146
+ messageId: requestBody.messageId,
147
+ input: requestBody.input
148
+ };
149
+
150
+ for (const key of ["conversationId", "history", "clientContext"]) {
151
+ if (Object.prototype.hasOwnProperty.call(requestBody, key)) {
152
+ actionInput[key] = requestBody[key];
153
+ }
154
+ }
155
+
156
+ return actionInput;
157
+ }
158
+
149
159
  function registerSettingsRoutes(
150
160
  router,
151
161
  resolveCurrentAppConfig,
@@ -156,7 +166,9 @@ function registerSettingsRoutes(
156
166
  });
157
167
  const visibility = requiresWorkspace ? "workspace" : "public";
158
168
  const routePath = `${routeBase}/:surfaceId/settings`;
159
- const paramsValidator = buildRouteParamsValidator(requiresWorkspace, workspaceScopeSupport);
169
+ const params = requiresWorkspace === true
170
+ ? requireWorkspaceAssistantRouteParams(workspaceScopeSupport)
171
+ : assistantSurfaceRouteParamsValidator;
160
172
 
161
173
  router.register(
162
174
  "GET",
@@ -164,13 +176,13 @@ function registerSettingsRoutes(
164
176
  {
165
177
  auth: "required",
166
178
  visibility,
167
- paramsValidator,
179
+ params,
168
180
  meta: {
169
181
  tags: ["assistant", "settings"],
170
182
  summary: "Get assistant settings."
171
183
  },
172
- responseValidators: withStandardErrorResponses({
173
- 200: assistantConfigResource.operations.view.outputValidator
184
+ responses: withStandardErrorResponses({
185
+ 200: assistantConfigResource.operations.view.output
174
186
  })
175
187
  },
176
188
  async function assistantSettingsReadRoute(request, reply) {
@@ -199,15 +211,15 @@ function registerSettingsRoutes(
199
211
  {
200
212
  auth: "required",
201
213
  visibility,
202
- paramsValidator,
214
+ params,
203
215
  meta: {
204
216
  tags: ["assistant", "settings"],
205
217
  summary: "Update assistant settings."
206
218
  },
207
- bodyValidator: assistantConfigResource.operations.patch.bodyValidator,
208
- responseValidators: withStandardErrorResponses(
219
+ body: assistantConfigResource.operations.patch.body,
220
+ responses: withStandardErrorResponses(
209
221
  {
210
- 200: assistantConfigResource.operations.patch.outputValidator
222
+ 200: assistantConfigResource.operations.patch.output
211
223
  },
212
224
  {
213
225
  includeValidation400: true
@@ -248,7 +260,18 @@ function registerRuntimeRoutes(
248
260
  });
249
261
  const visibility = requiresWorkspace ? "workspace" : "public";
250
262
  const surfaceRouteBase = `${routeBase}/:surfaceId`;
251
- const paramsValidator = buildRouteParamsValidator(requiresWorkspace, workspaceScopeSupport);
263
+ const params = requiresWorkspace === true
264
+ ? requireWorkspaceAssistantRouteParams(workspaceScopeSupport)
265
+ : assistantSurfaceRouteParamsValidator;
266
+ const conversationMessagesParams = composeSchemaDefinitions(
267
+ [params, assistantResource.operations.conversationMessagesList.params],
268
+ {
269
+ mode: "patch",
270
+ context: requiresWorkspace === true
271
+ ? "assistant-runtime workspace conversation messages route params"
272
+ : "assistant-runtime conversation messages route params"
273
+ }
274
+ );
252
275
 
253
276
  router.register(
254
277
  "POST",
@@ -256,12 +279,12 @@ function registerRuntimeRoutes(
256
279
  {
257
280
  auth: "required",
258
281
  visibility,
259
- paramsValidator,
282
+ params,
260
283
  meta: {
261
284
  tags: ["assistant"],
262
285
  summary: "Stream assistant response."
263
286
  },
264
- bodyValidator: assistantResource.operations.chatStream.bodyValidator
287
+ body: assistantResource.operations.chatStream.body
265
288
  },
266
289
  async function assistantChatStreamRoute(request, reply) {
267
290
  const routeState = resolveRouteRequestState(request, {
@@ -331,14 +354,7 @@ function registerRuntimeRoutes(
331
354
  context: {
332
355
  surface: routeState.hostSurfaceId
333
356
  },
334
- input: {
335
- ...routeState.actionInput,
336
- messageId: requestBody.messageId,
337
- conversationId: requestBody.conversationId,
338
- input: requestBody.input,
339
- history: requestBody.history,
340
- clientContext: requestBody.clientContext
341
- },
357
+ input: buildChatStreamActionInput(routeState.actionInput, requestBody),
342
358
  deps: {
343
359
  streamWriter,
344
360
  abortSignal: abortController.signal
@@ -379,14 +395,14 @@ function registerRuntimeRoutes(
379
395
  {
380
396
  auth: "required",
381
397
  visibility,
382
- paramsValidator,
398
+ params,
383
399
  meta: {
384
400
  tags: ["assistant"],
385
401
  summary: "List assistant conversations."
386
402
  },
387
- queryValidator: assistantResource.operations.conversationsList.queryValidator,
388
- responseValidators: withStandardErrorResponses({
389
- 200: assistantResource.operations.conversationsList.outputValidator
403
+ query: assistantResource.operations.conversationsList.query,
404
+ responses: withStandardErrorResponses({
405
+ 200: assistantResource.operations.conversationsList.output
390
406
  })
391
407
  },
392
408
  async function assistantConversationsRoute(request, reply) {
@@ -418,14 +434,14 @@ function registerRuntimeRoutes(
418
434
  {
419
435
  auth: "required",
420
436
  visibility,
421
- paramsValidator: buildConversationMessagesRouteParamsValidator(requiresWorkspace, workspaceScopeSupport),
437
+ params: conversationMessagesParams,
422
438
  meta: {
423
439
  tags: ["assistant"],
424
440
  summary: "List assistant conversation messages."
425
441
  },
426
- queryValidator: assistantResource.operations.conversationMessagesList.queryValidator,
427
- responseValidators: withStandardErrorResponses({
428
- 200: assistantResource.operations.conversationMessagesList.outputValidator
442
+ query: assistantResource.operations.conversationMessagesList.query,
443
+ responses: withStandardErrorResponses({
444
+ 200: assistantResource.operations.conversationMessagesList.output
429
445
  })
430
446
  },
431
447
  async function assistantConversationMessagesRoute(request, reply) {
@@ -1,11 +1,23 @@
1
+ import { normalizeSchemaDefinition } from "@jskit-ai/kernel/shared/validators";
2
+
1
3
  const WORKSPACES_SERVER_SCOPE_SUPPORT_TOKEN = "workspaces.server.scope-support";
2
4
 
5
+ function hasWorkspaceRouteParamsDefinition(value) {
6
+ try {
7
+ return Boolean(normalizeSchemaDefinition(value, {
8
+ context: "assistant-runtime workspace scope support.params",
9
+ defaultMode: "patch"
10
+ }));
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
3
16
  function isWorkspaceServerScopeSupport(value) {
4
17
  return Boolean(
5
18
  value &&
6
19
  value.available === true &&
7
- value.paramsValidator &&
8
- typeof value.paramsValidator.normalize === "function" &&
20
+ hasWorkspaceRouteParamsDefinition(value.params) &&
9
21
  typeof value.buildInputFromRouteParams === "function" &&
10
22
  typeof value.resolveWorkspace === "function"
11
23
  );
@@ -0,0 +1,192 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import {
5
+ withActionDefaults
6
+ } from "@jskit-ai/kernel/shared/actions";
7
+ import { ActionRuntimeServiceProvider } from "@jskit-ai/kernel/server/actions";
8
+
9
+ import { assistantActions } from "../src/server/actions.js";
10
+ import { actionIds } from "../src/server/actionIds.js";
11
+
12
+ function createSingletonApp() {
13
+ const singletons = new Map();
14
+ const instances = new Map();
15
+ const tags = new Map();
16
+
17
+ return {
18
+ has(token) {
19
+ return singletons.has(token) || instances.has(token);
20
+ },
21
+ singleton(token, factory) {
22
+ singletons.set(token, {
23
+ factory,
24
+ resolved: false,
25
+ value: undefined
26
+ });
27
+ },
28
+ tag(token, tagName) {
29
+ if (!this.has(token)) {
30
+ throw new Error(`Cannot tag unresolved token "${String(token)}".`);
31
+ }
32
+ if (!tags.has(tagName)) {
33
+ tags.set(tagName, new Set());
34
+ }
35
+ tags.get(tagName).add(token);
36
+ },
37
+ resolveTag(tagName) {
38
+ const tagged = tags.get(tagName);
39
+ if (!tagged) {
40
+ return [];
41
+ }
42
+ return [...tagged].map((token) => this.make(token));
43
+ },
44
+ make(token) {
45
+ if (instances.has(token)) {
46
+ return instances.get(token);
47
+ }
48
+ if (!singletons.has(token)) {
49
+ throw new Error(`Token "${String(token)}" is not registered.`);
50
+ }
51
+ const entry = singletons.get(token);
52
+ if (!entry.resolved) {
53
+ entry.value = entry.factory(this);
54
+ entry.resolved = true;
55
+ instances.set(token, entry.value);
56
+ }
57
+ return entry.value;
58
+ }
59
+ };
60
+ }
61
+
62
+ function createAssistantActionExecutor() {
63
+ const app = createSingletonApp();
64
+ const provider = new ActionRuntimeServiceProvider();
65
+ provider.register(app);
66
+
67
+ app.singleton("jskit.surface.runtime", () => ({
68
+ listEnabledSurfaceIds() {
69
+ return ["home", "console"];
70
+ }
71
+ }));
72
+
73
+ app.singleton("assistant.chat.service", () => ({
74
+ streamChat() {
75
+ return { ok: true };
76
+ },
77
+ listConversations(query, { input }) {
78
+ return { query, input };
79
+ },
80
+ getConversationMessages(conversationId, query, { input }) {
81
+ return { conversationId, query, input };
82
+ }
83
+ }));
84
+
85
+ app.singleton("assistant.config.service", () => ({
86
+ getSettings(input) {
87
+ return { input };
88
+ },
89
+ updateSettings(input, patch) {
90
+ return { input, patch };
91
+ }
92
+ }));
93
+
94
+ app.actions(
95
+ withActionDefaults(assistantActions, {
96
+ domain: "assistant",
97
+ dependencies: {
98
+ chatService: "assistant.chat.service",
99
+ assistantConfigService: "assistant.config.service"
100
+ }
101
+ })
102
+ );
103
+
104
+ return app.make("actionExecutor");
105
+ }
106
+
107
+ function findDefinition(definitions, id) {
108
+ return definitions.find((definition) => definition.id === id);
109
+ }
110
+
111
+ test("assistant-runtime actions materialize through the action runtime with single schema-definition inputs", () => {
112
+ const actionExecutor = createAssistantActionExecutor();
113
+ const definitions = actionExecutor.listDefinitions();
114
+
115
+ assert.equal(definitions.length, assistantActions.length);
116
+
117
+ for (const action of assistantActions) {
118
+ assert.equal(Array.isArray(action.input), false, `${action.id} input must not be an array`);
119
+ }
120
+
121
+ for (const definition of definitions) {
122
+ assert.equal(typeof definition.input?.schema?.patch, "function", `${definition.id} input schema must normalize`);
123
+ assert.equal(
124
+ typeof definition.input?.schema?.toJsonSchema,
125
+ "function",
126
+ `${definition.id} input schema must export`
127
+ );
128
+ assert.deepEqual(definition.surfaces, ["home", "console"]);
129
+ }
130
+ });
131
+
132
+ test("assistant-runtime conversations list action keeps query nested under a schema definition", () => {
133
+ const definition = findDefinition(createAssistantActionExecutor().listDefinitions(), actionIds.conversationsList);
134
+ const result = definition.input.schema.patch({
135
+ targetSurfaceId: "home",
136
+ workspaceSlug: "example-workspace",
137
+ query: {
138
+ limit: 10,
139
+ status: "active"
140
+ }
141
+ });
142
+
143
+ assert.deepEqual(result.errors, {});
144
+ assert.deepEqual(result.validatedObject, {
145
+ targetSurfaceId: "home",
146
+ workspaceSlug: "example-workspace",
147
+ query: {
148
+ limit: 10,
149
+ status: "active"
150
+ }
151
+ });
152
+ });
153
+
154
+ test("assistant-runtime conversation messages list action composes params and nested query", () => {
155
+ const definition = findDefinition(createAssistantActionExecutor().listDefinitions(), actionIds.conversationMessagesList);
156
+ const result = definition.input.schema.patch({
157
+ targetSurfaceId: "home",
158
+ conversationId: "123",
159
+ query: {
160
+ page: 2,
161
+ pageSize: 25
162
+ }
163
+ });
164
+
165
+ assert.deepEqual(result.errors, {});
166
+ assert.deepEqual(result.validatedObject, {
167
+ targetSurfaceId: "home",
168
+ conversationId: 123,
169
+ query: {
170
+ page: 2,
171
+ pageSize: 25
172
+ }
173
+ });
174
+ });
175
+
176
+ test("assistant-runtime settings update action keeps patch nested under a schema definition", () => {
177
+ const definition = findDefinition(createAssistantActionExecutor().listDefinitions(), actionIds.settingsUpdate);
178
+ const result = definition.input.schema.patch({
179
+ targetSurfaceId: "home",
180
+ patch: {
181
+ systemPrompt: "Be concise."
182
+ }
183
+ });
184
+
185
+ assert.deepEqual(result.errors, {});
186
+ assert.deepEqual(result.validatedObject, {
187
+ targetSurfaceId: "home",
188
+ patch: {
189
+ systemPrompt: "Be concise."
190
+ }
191
+ });
192
+ });
@@ -5,7 +5,7 @@ import {
5
5
  resolveAssistantServerConfig
6
6
  } from "../src/server/support/assistantServerConfig.js";
7
7
 
8
- test("assistant server config resolves per-surface AI config without legacy global fallback", () => {
8
+ test("assistant server config resolves per-surface AI config without app-level global fallback", () => {
9
9
  const appConfig = {
10
10
  assistantServer: {
11
11
  admin: {
@@ -21,14 +21,14 @@ test("assistant server config resolves per-surface AI config without legacy glob
21
21
  },
22
22
  assistant: {
23
23
  provider: "openai",
24
- apiKey: "legacy-key"
24
+ apiKey: "global-key"
25
25
  }
26
26
  };
27
27
  const env = {
28
28
  ADMIN_ASSISTANT_AI_PROVIDER: "deepseek",
29
29
  ADMIN_ASSISTANT_AI_API_KEY: "surface-key",
30
30
  AI_PROVIDER: "openai",
31
- AI_API_KEY: "legacy-env-key"
31
+ AI_API_KEY: "global-env-key"
32
32
  };
33
33
 
34
34
  assert.deepEqual(resolveAssistantServerConfig(appConfig, "admin"), {
@@ -62,7 +62,7 @@ test("assistant server config requires explicit aiConfigPrefix for each surface"
62
62
  appConfig: {},
63
63
  env: {
64
64
  AI_PROVIDER: "anthropic",
65
- AI_API_KEY: "legacy-env-key"
65
+ AI_API_KEY: "global-env-key"
66
66
  }
67
67
  },
68
68
  "admin"
@@ -0,0 +1,23 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { insertTextAtSelection } from "../src/client/support/composerInputSupport.js";
4
+
5
+ test("insertTextAtSelection inserts a line break at the caret", () => {
6
+ const result = insertTextAtSelection("hello world", 5, 5, "\n");
7
+
8
+ assert.deepEqual(result, {
9
+ value: "hello\n world",
10
+ selectionStart: 6,
11
+ selectionEnd: 6
12
+ });
13
+ });
14
+
15
+ test("insertTextAtSelection replaces the active selection with a line break", () => {
16
+ const result = insertTextAtSelection("hello world", 5, 11, "\n");
17
+
18
+ assert.deepEqual(result, {
19
+ value: "hello\n",
20
+ selectionStart: 6,
21
+ selectionEnd: 6
22
+ });
23
+ });
@@ -1,6 +1,8 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
+ import { createSchema } from "json-rest-schema";
3
4
  import { AppError } from "@jskit-ai/kernel/server/runtime";
5
+ import { createRouter } from "../../kernel/server/http/lib/router.js";
4
6
  import { registerRoutes } from "../src/server/registerRoutes.js";
5
7
  import { createChatService } from "../src/server/services/chatService.js";
6
8
 
@@ -26,12 +28,17 @@ function createAssistantAppConfig() {
26
28
  function createWorkspaceServerScopeSupport() {
27
29
  return Object.freeze({
28
30
  available: true,
29
- paramsValidator: Object.freeze({
30
- normalize(value = {}) {
31
- return {
32
- workspaceSlug: String(value?.workspaceSlug || "").trim().toLowerCase()
33
- };
34
- }
31
+ params: Object.freeze({
32
+ schema: createSchema({
33
+ workspaceSlug: {
34
+ type: "string",
35
+ required: true,
36
+ lowercase: true,
37
+ minLength: 1,
38
+ maxLength: 160
39
+ }
40
+ }),
41
+ mode: "patch"
35
42
  }),
36
43
  buildInputFromRouteParams(params = {}) {
37
44
  const workspaceSlug = String(params?.workspaceSlug || "").trim().toLowerCase();
@@ -43,39 +50,52 @@ function createWorkspaceServerScopeSupport() {
43
50
  });
44
51
  }
45
52
 
46
- test("registerRoutes resolves appConfig lazily when handlers run", async () => {
47
- const routes = [];
48
- let currentAppConfig = null;
49
-
50
- const router = {
51
- register(method, path, options, handler) {
52
- routes.push({ method, path, options, handler });
53
- }
54
- };
53
+ function createAssistantTestApp({
54
+ resolveCurrentAppConfig = () => null,
55
+ workspaceScopeSupport = null,
56
+ router = null
57
+ } = {}) {
58
+ const resolvedRouter = router || createRouter();
55
59
 
56
- const app = {
57
- make(token) {
58
- if (token === "jskit.http.router") {
59
- return router;
60
- }
61
- if (token === "appConfig") {
62
- return currentAppConfig;
63
- }
64
- if (token === "workspaces.server.scope-support") {
65
- return createWorkspaceServerScopeSupport();
60
+ return {
61
+ router: resolvedRouter,
62
+ app: {
63
+ make(token) {
64
+ if (token === "jskit.http.router") {
65
+ return resolvedRouter;
66
+ }
67
+ if (token === "appConfig") {
68
+ return resolveCurrentAppConfig();
69
+ }
70
+ if (token === "workspaces.server.scope-support" && workspaceScopeSupport) {
71
+ return workspaceScopeSupport;
72
+ }
73
+ throw new Error(`Unexpected token: ${token}`);
74
+ },
75
+ has(token) {
76
+ if (token === "jskit.http.router") {
77
+ return true;
78
+ }
79
+ if (token === "appConfig") {
80
+ return Boolean(resolveCurrentAppConfig());
81
+ }
82
+ return token === "workspaces.server.scope-support" && Boolean(workspaceScopeSupport);
66
83
  }
67
- throw new Error(`Unexpected token: ${token}`);
68
- },
69
- has(token) {
70
- return (
71
- (token === "appConfig" ? Boolean(currentAppConfig) : token === "jskit.http.router") ||
72
- token === "workspaces.server.scope-support"
73
- );
74
84
  }
75
85
  };
86
+ }
87
+
88
+ test("registerRoutes resolves appConfig lazily when handlers run", async () => {
89
+ let currentAppConfig = null;
90
+ const workspaceScopeSupport = createWorkspaceServerScopeSupport();
91
+ const testApp = createAssistantTestApp({
92
+ workspaceScopeSupport,
93
+ resolveCurrentAppConfig: () => currentAppConfig
94
+ });
76
95
 
77
- registerRoutes(app);
96
+ registerRoutes(testApp.app);
78
97
  currentAppConfig = createAssistantAppConfig();
98
+ const routes = testApp.router.list();
79
99
 
80
100
  const route = routes.find(
81
101
  (entry) =>
@@ -135,38 +155,17 @@ test("registerRoutes resolves appConfig lazily when handlers run", async () => {
135
155
  });
136
156
 
137
157
  test("registerRoutes returns clear AppError payload for pre-stream assistant failures", async () => {
138
- const routes = [];
139
158
  let currentAppConfig = null;
159
+ let capturedInput = null;
160
+ const workspaceScopeSupport = createWorkspaceServerScopeSupport();
161
+ const testApp = createAssistantTestApp({
162
+ workspaceScopeSupport,
163
+ resolveCurrentAppConfig: () => currentAppConfig
164
+ });
140
165
 
141
- const router = {
142
- register(method, path, options, handler) {
143
- routes.push({ method, path, options, handler });
144
- }
145
- };
146
-
147
- const app = {
148
- make(token) {
149
- if (token === "jskit.http.router") {
150
- return router;
151
- }
152
- if (token === "appConfig") {
153
- return currentAppConfig;
154
- }
155
- if (token === "workspaces.server.scope-support") {
156
- return createWorkspaceServerScopeSupport();
157
- }
158
- throw new Error(`Unexpected token: ${token}`);
159
- },
160
- has(token) {
161
- return (
162
- (token === "appConfig" ? Boolean(currentAppConfig) : token === "jskit.http.router") ||
163
- token === "workspaces.server.scope-support"
164
- );
165
- }
166
- };
167
-
168
- registerRoutes(app);
166
+ registerRoutes(testApp.app);
169
167
  currentAppConfig = createAssistantAppConfig();
168
+ const routes = testApp.router.list();
170
169
 
171
170
  const route = routes.find(
172
171
  (entry) =>
@@ -212,7 +211,8 @@ test("registerRoutes returns clear AppError payload for pre-stream assistant fai
212
211
  history: []
213
212
  }
214
213
  },
215
- executeAction: async () => {
214
+ executeAction: async ({ input }) => {
215
+ capturedInput = input;
216
216
  throw new AppError(503, "Assistant provider is not configured.");
217
217
  }
218
218
  },
@@ -224,6 +224,13 @@ test("registerRoutes returns clear AppError payload for pre-stream assistant fai
224
224
  error: "Assistant provider is not configured.",
225
225
  code: "APP_ERROR"
226
226
  });
227
+ assert.deepEqual(capturedInput, {
228
+ targetSurfaceId: "admin",
229
+ workspaceSlug: "dogandgroom",
230
+ messageId: "msg_1",
231
+ input: "hello",
232
+ history: []
233
+ });
227
234
  });
228
235
 
229
236
  test("chat service resolves appConfig lazily when conversations are listed", async () => {
@@ -321,27 +328,12 @@ test("chat service rejects workspace-scoped assistant surfaces when workspace su
321
328
  });
322
329
 
323
330
  test("registerRoutes omits workspace assistant routes when workspace scope support is unavailable", () => {
324
- const routes = [];
325
- const app = {
326
- make(token) {
327
- if (token === "jskit.http.router") {
328
- return {
329
- register(method, path, options, handler) {
330
- routes.push({ method, path, options, handler });
331
- }
332
- };
333
- }
334
- if (token === "appConfig") {
335
- return createAssistantAppConfig();
336
- }
337
- throw new Error(`Unexpected token: ${token}`);
338
- },
339
- has(token) {
340
- return token === "jskit.http.router" || token === "appConfig";
341
- }
342
- };
331
+ const testApp = createAssistantTestApp({
332
+ resolveCurrentAppConfig: () => createAssistantAppConfig()
333
+ });
343
334
 
344
- registerRoutes(app);
335
+ registerRoutes(testApp.app);
336
+ const routes = testApp.router.list();
345
337
 
346
338
  assert.equal(
347
339
  routes.some((entry) => entry.path.startsWith("/api/w/:workspaceSlug/assistant/")),
@@ -1,41 +0,0 @@
1
- import { Type } from "typebox";
2
- import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
3
- import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
4
-
5
- const assistantSurfaceRouteParamsValidator = Object.freeze({
6
- schema: Type.Object(
7
- {
8
- surfaceId: Type.String({ minLength: 1, maxLength: 64 })
9
- },
10
- { additionalProperties: false }
11
- ),
12
- normalize(value = {}) {
13
- return {
14
- surfaceId: normalizeSurfaceId(value?.surfaceId)
15
- };
16
- }
17
- });
18
-
19
- const assistantTargetSurfaceInputValidator = Object.freeze({
20
- schema: Type.Object(
21
- {
22
- targetSurfaceId: Type.String({ minLength: 1, maxLength: 64 }),
23
- workspaceSlug: Type.Optional(Type.String({ minLength: 1, maxLength: 160 }))
24
- },
25
- { additionalProperties: false }
26
- ),
27
- normalize(value = {}) {
28
- const targetSurfaceId = normalizeSurfaceId(value?.targetSurfaceId);
29
- const workspaceSlug = normalizeText(value?.workspaceSlug).toLowerCase();
30
-
31
- return {
32
- targetSurfaceId,
33
- ...(workspaceSlug ? { workspaceSlug } : {})
34
- };
35
- }
36
- });
37
-
38
- export {
39
- assistantSurfaceRouteParamsValidator,
40
- assistantTargetSurfaceInputValidator
41
- };