@jskit-ai/assistant-runtime 0.1.131-oldline.2 → 0.1.131

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 (29) hide show
  1. package/package.json +84 -8
  2. package/patterns/assistant-surface/PATTERN.md +77 -0
  3. package/patterns/assistant-surface/example/config/public.js +19 -0
  4. package/patterns/assistant-surface/example/config/server.js +9 -0
  5. package/patterns/assistant-surface/example/src/pages/admin/assistant/index.vue +7 -0
  6. package/patterns/assistant-surface/example/src/pages/admin/settings/assistant/index.vue +7 -0
  7. package/patterns/assistant-surface/example/src/placement.js +38 -0
  8. package/src/client/components/AssistantSettingsClientElement.vue +1 -1
  9. package/src/client/composables/useAssistantRuntime.js +2 -2
  10. package/src/client/providers/AssistantClientProvider.js +9 -11
  11. package/src/server/AssistantProvider.js +74 -109
  12. package/src/server/actions.js +37 -17
  13. package/src/server/registerRoutes.js +4 -9
  14. package/src/server/repositories/assistantConfigRepository.js +1 -1
  15. package/src/server/repositories/conversationsRepository.js +1 -1
  16. package/src/server/repositories/messagesRepository.js +1 -1
  17. package/src/server/repositories/repositoryPersistenceUtils.js +29 -0
  18. package/src/server/support/createSurfaceAwareToolCatalog.js +5 -5
  19. package/test/actionDefinitions.test.js +49 -142
  20. package/test/assistantSurfacePattern.test.js +30 -0
  21. package/test/createSurfaceAwareToolCatalog.test.js +4 -2
  22. package/test/featureRuntime.test.js +76 -0
  23. package/test/lazyAppConfig.test.js +16 -47
  24. package/test/packageMetadata.test.js +38 -0
  25. package/package.descriptor.mjs +0 -155
  26. package/src/server/support/workspaceScopeSupport.js +0 -47
  27. package/test/packageDescriptor.test.js +0 -38
  28. /package/{templates/migrations → migrations}/assistant_config_initial.cjs +0 -0
  29. /package/{templates/migrations → migrations}/assistant_transcripts_initial.cjs +0 -0
@@ -1,5 +1,4 @@
1
1
  import { AppError } from "@jskit-ai/kernel/server/runtime";
2
- import { resolveAppConfig } from "@jskit-ai/kernel/server/support";
3
2
  import { composeSchemaDefinitions } from "@jskit-ai/kernel/shared/validators";
4
3
  import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
5
4
  import { createJsonApiResourceRouteContract } from "@jskit-ai/http-runtime/shared/validators/jsonApiRouteTransport";
@@ -22,7 +21,6 @@ import {
22
21
  import { resolveAssistantSurfaceConfig } from "../shared/assistantSurfaces.js";
23
22
  import { actionIds } from "./actionIds.js";
24
23
  import { assistantSurfaceRouteParamsValidator } from "./inputSchemas.js";
25
- import { resolveWorkspaceServerScopeSupport } from "./support/workspaceScopeSupport.js";
26
24
 
27
25
  function requireWorkspaceAssistantRouteParams(workspaceScopeSupport = null) {
28
26
  if (!workspaceScopeSupport) {
@@ -499,14 +497,11 @@ function registerRuntimeRoutes(
499
497
  );
500
498
  }
501
499
 
502
- function registerRoutes(app) {
503
- if (!app || typeof app.make !== "function") {
504
- throw new Error("registerRoutes requires application make().");
500
+ function registerRoutes(router, { config = {}, workspaceScopeSupport = null } = {}) {
501
+ if (!router || typeof router.register !== "function") {
502
+ throw new Error("registerRoutes requires an HTTP router.");
505
503
  }
506
-
507
- const router = app.make("jskit.http.router");
508
- const resolveCurrentAppConfig = () => resolveAppConfig(app);
509
- const workspaceScopeSupport = resolveWorkspaceServerScopeSupport(app);
504
+ const resolveCurrentAppConfig = () => config;
510
505
 
511
506
  registerSettingsRoutes(router, resolveCurrentAppConfig, {
512
507
  requiresWorkspace: false
@@ -1,7 +1,7 @@
1
1
  import { normalizeDbRecordId, createWithTransaction } from "@jskit-ai/database-runtime/shared";
2
2
  import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
3
3
  import { normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
4
- import { resolveInsertedId } from "@jskit-ai/assistant-core/server";
4
+ import { resolveInsertedId } from "./repositoryPersistenceUtils.js";
5
5
  import { assistantRuntimeConfig } from "../../shared/assistantRuntimeConfig.js";
6
6
 
7
7
  function normalizeTargetSurfaceId(value = "") {
@@ -6,7 +6,7 @@ import {
6
6
  resolveInsertedId,
7
7
  stringifyJsonObject,
8
8
  toIso
9
- } from "@jskit-ai/assistant-core/server";
9
+ } from "./repositoryPersistenceUtils.js";
10
10
  import { assistantRuntimeConfig } from "../../shared/assistantRuntimeConfig.js";
11
11
 
12
12
  function normalizeWorkspaceId(value) {
@@ -5,7 +5,7 @@ import {
5
5
  resolveInsertedId,
6
6
  stringifyJsonObject,
7
7
  toIso
8
- } from "@jskit-ai/assistant-core/server";
8
+ } from "./repositoryPersistenceUtils.js";
9
9
  import { assistantRuntimeConfig } from "../../shared/assistantRuntimeConfig.js";
10
10
 
11
11
  function normalizeWorkspaceId(value) {
@@ -0,0 +1,29 @@
1
+ import { resolveInsertedRecordId } from "@jskit-ai/database-runtime/shared";
2
+ import { parseJsonObject } from "@jskit-ai/assistant-core/shared";
3
+
4
+ function stringifyJsonObject(value) {
5
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
6
+ return null;
7
+ }
8
+
9
+ return JSON.stringify(value);
10
+ }
11
+
12
+ function toIso(value) {
13
+ if (!value) {
14
+ return "";
15
+ }
16
+
17
+ const date = value instanceof Date ? value : new Date(value);
18
+ if (Number.isNaN(date.getTime())) {
19
+ return "";
20
+ }
21
+
22
+ return date.toISOString();
23
+ }
24
+
25
+ function resolveInsertedId(insertResult) {
26
+ return resolveInsertedRecordId(insertResult, { fallback: "" }) || "";
27
+ }
28
+
29
+ export { parseJsonObject, stringifyJsonObject, toIso, resolveInsertedId };
@@ -21,9 +21,9 @@ function requireContextSurfaceId(context = {}) {
21
21
  throw new Error("assistant surface-aware tool catalog requires context.surface.");
22
22
  }
23
23
 
24
- function createSurfaceAwareToolCatalog(scope, { appConfig = {}, resolveAppConfig = null, createCatalog = createServiceToolCatalog } = {}) {
25
- if (!scope) {
26
- throw new Error("createSurfaceAwareToolCatalog requires scope.");
24
+ function createSurfaceAwareToolCatalog(actions, { appConfig = {}, resolveAppConfig = null, createCatalog = createServiceToolCatalog } = {}) {
25
+ if (!actions || typeof actions.listDefinitions !== "function") {
26
+ throw new Error("createSurfaceAwareToolCatalog requires runtime.actions.");
27
27
  }
28
28
 
29
29
  const resolveCurrentAppConfig =
@@ -36,7 +36,7 @@ function createSurfaceAwareToolCatalog(scope, { appConfig = {}, resolveAppConfig
36
36
  return cache.get(surfaceId);
37
37
  }
38
38
 
39
- const nextCatalog = createCatalog(scope, buildCatalogOptions(resolveCurrentAppConfig(), surfaceId));
39
+ const nextCatalog = createCatalog(actions, buildCatalogOptions(resolveCurrentAppConfig(), surfaceId));
40
40
  cache.set(surfaceId, nextCatalog);
41
41
  return nextCatalog;
42
42
  }
@@ -48,7 +48,7 @@ function createSurfaceAwareToolCatalog(scope, { appConfig = {}, resolveAppConfig
48
48
  },
49
49
  toOpenAiToolSchema(tool) {
50
50
  if (!schemaCatalog) {
51
- schemaCatalog = createCatalog(scope, buildCatalogOptions(resolveCurrentAppConfig(), ""));
51
+ schemaCatalog = createCatalog(actions, buildCatalogOptions(resolveCurrentAppConfig(), ""));
52
52
  }
53
53
 
54
54
  return schemaCatalog.toOpenAiToolSchema(tool);
@@ -1,192 +1,99 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
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";
4
+ import { createAssistantActions } from "../src/server/actions.js";
10
5
  import { actionIds } from "../src/server/actionIds.js";
11
6
 
12
- function createSingletonApp() {
13
- const singletons = new Map();
14
- const instances = new Map();
15
- const tags = new Map();
7
+ const CONFIG = Object.freeze({
8
+ surfaceDefinitions: {
9
+ home: { enabled: true, requiresWorkspace: false },
10
+ admin: { enabled: true, requiresWorkspace: true },
11
+ console: { enabled: true, requiresWorkspace: false, accessPolicyId: "console_owner" }
12
+ },
13
+ assistantSurfaces: {
14
+ home: { settingsSurfaceId: "console", configScope: "global" },
15
+ admin: { settingsSurfaceId: "admin", configScope: "workspace" }
16
+ }
17
+ });
16
18
 
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);
19
+ function createDefinitions() {
20
+ return createAssistantActions({
21
+ config: CONFIG,
22
+ chatService: {
23
+ streamChat() { return { ok: true }; },
24
+ listConversations(query, { input }) { return { query, input }; },
25
+ getConversationMessages(conversationId, query, { input }) {
26
+ return { conversationId, query, input };
47
27
  }
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
28
  },
80
- getConversationMessages(conversationId, query, { input }) {
81
- return { conversationId, query, input };
29
+ assistantConfigService: {
30
+ getSettings(input) { return { input }; },
31
+ updateSettings(input, patch) { return { input, patch }; }
82
32
  }
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");
33
+ });
105
34
  }
106
35
 
107
36
  function findDefinition(definitions, id) {
108
37
  return definitions.find((definition) => definition.id === id);
109
38
  }
110
39
 
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
- }
40
+ test("assistant actions use configured runtime and settings surfaces without surface discovery", () => {
41
+ const definitions = createDefinitions();
42
+
43
+ assert.equal(definitions.length, 5);
44
+ assert.deepEqual(findDefinition(definitions, actionIds.chatStream).surfaces, ["home", "admin"]);
45
+ assert.deepEqual(findDefinition(definitions, actionIds.conversationsList).surfaces, ["home", "admin"]);
46
+ assert.deepEqual(findDefinition(definitions, actionIds.settingsRead).surfaces, ["console", "admin"]);
47
+ assert.ok(definitions.every((definition) => !Object.hasOwn(definition, "dependencies")));
48
+ assert.equal(createAssistantActions({
49
+ config: {},
50
+ chatService: {},
51
+ assistantConfigService: {}
52
+ }).length, 0);
130
53
  });
131
54
 
132
- test("assistant-runtime conversations list action keeps query nested under a schema definition", () => {
133
- const definition = findDefinition(createAssistantActionExecutor().listDefinitions(), actionIds.conversationsList);
55
+ test("assistant conversations list keeps query nested under one schema definition", () => {
56
+ const definition = findDefinition(createDefinitions(), actionIds.conversationsList);
134
57
  const result = definition.input.schema.patch({
135
58
  targetSurfaceId: "home",
136
59
  workspaceSlug: "example-workspace",
137
- query: {
138
- limit: 10,
139
- status: "active"
140
- }
60
+ query: { limit: 10, status: "active" }
141
61
  });
142
62
 
143
63
  assert.deepEqual(result.errors, {});
144
64
  assert.deepEqual(result.validatedObject, {
145
65
  targetSurfaceId: "home",
146
66
  workspaceSlug: "example-workspace",
147
- query: {
148
- limit: 10,
149
- status: "active"
150
- }
67
+ query: { limit: 10, status: "active" }
151
68
  });
152
69
  });
153
70
 
154
- test("assistant-runtime conversation messages list action composes params and nested query", () => {
155
- const definition = findDefinition(createAssistantActionExecutor().listDefinitions(), actionIds.conversationMessagesList);
71
+ test("assistant conversation messages composes params and nested query", () => {
72
+ const definition = findDefinition(createDefinitions(), actionIds.conversationMessagesList);
156
73
  const result = definition.input.schema.patch({
157
74
  targetSurfaceId: "home",
158
75
  conversationId: "123",
159
- query: {
160
- page: 2,
161
- pageSize: 25
162
- }
76
+ query: { page: 2, pageSize: 25 }
163
77
  });
164
78
 
165
79
  assert.deepEqual(result.errors, {});
166
80
  assert.deepEqual(result.validatedObject, {
167
81
  targetSurfaceId: "home",
168
82
  conversationId: 123,
169
- query: {
170
- page: 2,
171
- pageSize: 25
172
- }
83
+ query: { page: 2, pageSize: 25 }
173
84
  });
174
85
  });
175
86
 
176
- test("assistant-runtime settings update action keeps patch nested under a schema definition", () => {
177
- const definition = findDefinition(createAssistantActionExecutor().listDefinitions(), actionIds.settingsUpdate);
87
+ test("assistant settings update keeps patch nested under one schema definition", () => {
88
+ const definition = findDefinition(createDefinitions(), actionIds.settingsUpdate);
178
89
  const result = definition.input.schema.patch({
179
90
  targetSurfaceId: "home",
180
- patch: {
181
- systemPrompt: "Be concise."
182
- }
91
+ patch: { systemPrompt: "Be concise." }
183
92
  });
184
93
 
185
94
  assert.deepEqual(result.errors, {});
186
95
  assert.deepEqual(result.validatedObject, {
187
96
  targetSurfaceId: "home",
188
- patch: {
189
- systemPrompt: "Be concise."
190
- }
97
+ patch: { systemPrompt: "Be concise." }
191
98
  });
192
99
  });
@@ -0,0 +1,30 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFile } from "node:fs/promises";
3
+ import test from "node:test";
4
+ import { parse as parseSfc } from "@vue/compiler-sfc";
5
+
6
+ const exampleRoot = new URL("../patterns/assistant-surface/example/", import.meta.url);
7
+
8
+ test("assistant surface pattern keeps product config explicit and secrets external", async () => {
9
+ const publicConfig = (await import(new URL("config/public.js", exampleRoot))).default;
10
+ const serverConfig = (await import(new URL("config/server.js", exampleRoot))).default;
11
+ const placementSource = await readFile(new URL("src/placement.js", exampleRoot), "utf8");
12
+ const pagePaths = [
13
+ "src/pages/admin/assistant/index.vue",
14
+ "src/pages/admin/settings/assistant/index.vue"
15
+ ];
16
+
17
+ assert.deepEqual(publicConfig.assistantSurfaces.admin, {
18
+ settingsSurfaceId: "admin",
19
+ configScope: "global"
20
+ });
21
+ assert.equal(serverConfig.assistantServer.admin.aiConfigPrefix, "ADMIN_ASSISTANT");
22
+ assert.doesNotMatch(JSON.stringify({ publicConfig, serverConfig }), /api[_-]?key|secret/iu);
23
+ assert.doesNotMatch(placementSource, /generator|scaffold|receipt|provenance/u);
24
+
25
+ for (const pagePath of pagePaths) {
26
+ const source = await readFile(new URL(pagePath, exampleRoot), "utf8");
27
+ const parsed = parseSfc(source, { filename: pagePath });
28
+ assert.equal(parsed.errors.length, 0, `${pagePath}: ${parsed.errors.join("\n")}`);
29
+ }
30
+ });
@@ -4,8 +4,9 @@ import { createSurfaceAwareToolCatalog } from "../src/server/support/createSurfa
4
4
 
5
5
  test("surface-aware tool catalog applies per-surface skip and barred configuration", async () => {
6
6
  const observed = [];
7
+ const actions = { listDefinitions: () => [], async execute() {} };
7
8
  const catalog = createSurfaceAwareToolCatalog(
8
- {},
9
+ actions,
9
10
  {
10
11
  appConfig: {
11
12
  assistantServer: {
@@ -18,7 +19,8 @@ test("surface-aware tool catalog applies per-surface skip and barred configurati
18
19
  }
19
20
  }
20
21
  },
21
- createCatalog(_scope, options = {}) {
22
+ createCatalog(receivedActions, options = {}) {
23
+ assert.equal(receivedActions, actions);
22
24
  observed.push(options);
23
25
  return {
24
26
  resolveToolSet(context = {}) {
@@ -0,0 +1,76 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { createActionProvider } from "@jskit-ai/kernel/server/actions";
5
+ import { createCapabilityRuntime, defineProvider } from "@jskit-ai/kernel/shared/capabilities";
6
+ import { AssistantFeature } from "../src/server/AssistantProvider.js";
7
+
8
+ function valueProvider(id, capability, value) {
9
+ return defineProvider({
10
+ id,
11
+ provides: { value: capability },
12
+ setup() { return { value }; }
13
+ });
14
+ }
15
+
16
+ test("AssistantFeature composes chat, transcripts, settings, routes, and action tools from capabilities", async () => {
17
+ const routes = [];
18
+ let actions;
19
+ let assistant;
20
+ const config = {
21
+ surfaceDefinitions: {
22
+ home: { enabled: true, requiresWorkspace: false },
23
+ console: { enabled: true, requiresWorkspace: false, accessPolicyId: "console_owner" }
24
+ },
25
+ assistantSurfaces: {
26
+ home: { settingsSurfaceId: "console", configScope: "global" }
27
+ },
28
+ assistantServer: {
29
+ home: { aiConfigPrefix: "HOME" }
30
+ }
31
+ };
32
+ const runtime = createCapabilityRuntime({
33
+ providers: [
34
+ createActionProvider(),
35
+ valueProvider("test.config", "runtime.config", config),
36
+ valueProvider("test.database", "runtime.database", { knex: Object.assign(() => {}, {}) }),
37
+ valueProvider("test.env", "runtime.env", {}),
38
+ valueProvider("test.http", "runtime.http", {
39
+ router: {
40
+ register(method, path, contract, handler) {
41
+ routes.push({ method, path, contract, handler });
42
+ }
43
+ }
44
+ }),
45
+ AssistantFeature,
46
+ defineProvider({
47
+ id: "test.observer",
48
+ requires: {
49
+ actionCatalogue: "runtime.actions",
50
+ assistantRuntime: "assistant.runtime"
51
+ },
52
+ setup(dependencies) {
53
+ actions = dependencies.actionCatalogue;
54
+ assistant = dependencies.assistantRuntime;
55
+ return {};
56
+ }
57
+ })
58
+ ]
59
+ });
60
+
61
+ await runtime.start();
62
+
63
+ assert.deepEqual(actions.listDefinitions().map((entry) => entry.id), [
64
+ "assistant.chat.stream",
65
+ "assistant.conversations.list",
66
+ "assistant.conversation.messages.list",
67
+ "assistant.settings.read",
68
+ "assistant.settings.update"
69
+ ]);
70
+ assert.equal(typeof assistant.services.chat.streamChat, "function");
71
+ assert.equal(typeof assistant.toolCatalog.resolveToolSet, "function");
72
+ assert.equal(routes.length, 5);
73
+ assert.ok(actions.listDefinitions().every((entry) => !Object.hasOwn(entry, "dependencies")));
74
+
75
+ await runtime.shutdown();
76
+ });
@@ -51,7 +51,6 @@ function createWorkspaceServerScopeSupport() {
51
51
  }
52
52
 
53
53
  function createAssistantTestApp({
54
- resolveCurrentAppConfig = () => null,
55
54
  workspaceScopeSupport = null,
56
55
  router = null
57
56
  } = {}) {
@@ -59,29 +58,7 @@ function createAssistantTestApp({
59
58
 
60
59
  return {
61
60
  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);
83
- }
84
- }
61
+ workspaceScopeSupport
85
62
  };
86
63
  }
87
64
 
@@ -91,11 +68,13 @@ function findRoute(routes, { method, path }) {
91
68
 
92
69
  test("registerRoutes exposes JSON:API contracts for assistant settings and transcript endpoints", () => {
93
70
  const testApp = createAssistantTestApp({
94
- workspaceScopeSupport: createWorkspaceServerScopeSupport(),
95
- resolveCurrentAppConfig: () => createAssistantAppConfig()
71
+ workspaceScopeSupport: createWorkspaceServerScopeSupport()
96
72
  });
97
73
 
98
- registerRoutes(testApp.app);
74
+ registerRoutes(testApp.router, {
75
+ config: createAssistantAppConfig(),
76
+ workspaceScopeSupport: testApp.workspaceScopeSupport
77
+ });
99
78
  const routes = testApp.router.list();
100
79
  const publicSettingsReadRoute = findRoute(routes, {
101
80
  method: "GET",
@@ -125,16 +104,13 @@ test("registerRoutes exposes JSON:API contracts for assistant settings and trans
125
104
  assert.equal(publicConversationMessagesRoute?.schema?.response?.[200]?.required?.[0], "data");
126
105
  });
127
106
 
128
- test("registerRoutes resolves appConfig lazily when handlers run", async () => {
129
- let currentAppConfig = null;
107
+ test("registerRoutes builds workspace action input from explicit config and scope support", async () => {
130
108
  const workspaceScopeSupport = createWorkspaceServerScopeSupport();
131
109
  const testApp = createAssistantTestApp({
132
- workspaceScopeSupport,
133
- resolveCurrentAppConfig: () => currentAppConfig
110
+ workspaceScopeSupport
134
111
  });
135
112
 
136
- registerRoutes(testApp.app);
137
- currentAppConfig = createAssistantAppConfig();
113
+ registerRoutes(testApp.router, { config: createAssistantAppConfig(), workspaceScopeSupport });
138
114
  const routes = testApp.router.list();
139
115
 
140
116
  const route = findRoute(routes, {
@@ -194,16 +170,13 @@ test("registerRoutes resolves appConfig lazily when handlers run", async () => {
194
170
  });
195
171
 
196
172
  test("registerRoutes returns clear AppError payload for pre-stream assistant failures", async () => {
197
- let currentAppConfig = null;
198
173
  let capturedInput = null;
199
174
  const workspaceScopeSupport = createWorkspaceServerScopeSupport();
200
175
  const testApp = createAssistantTestApp({
201
- workspaceScopeSupport,
202
- resolveCurrentAppConfig: () => currentAppConfig
176
+ workspaceScopeSupport
203
177
  });
204
178
 
205
- registerRoutes(testApp.app);
206
- currentAppConfig = createAssistantAppConfig();
179
+ registerRoutes(testApp.router, { config: createAssistantAppConfig(), workspaceScopeSupport });
207
180
  const routes = testApp.router.list();
208
181
 
209
182
  const route = findRoute(routes, {
@@ -271,9 +244,7 @@ test("registerRoutes returns clear AppError payload for pre-stream assistant fai
271
244
  });
272
245
  });
273
246
 
274
- test("chat service resolves appConfig lazily when conversations are listed", async () => {
275
- let currentAppConfig = {};
276
-
247
+ test("chat service uses explicit app config when conversations are listed", async () => {
277
248
  const chatService = createChatService({
278
249
  aiClientFactory: {
279
250
  resolveClient() {
@@ -293,12 +264,10 @@ test("chat service resolves appConfig lazily when conversations are listed", asy
293
264
  },
294
265
  serviceToolCatalog: {},
295
266
  assistantConfigService: {},
296
- resolveAppConfig: () => currentAppConfig,
267
+ appConfig: createAssistantAppConfig(),
297
268
  workspaceScopeSupport: createWorkspaceServerScopeSupport()
298
269
  });
299
270
 
300
- currentAppConfig = createAssistantAppConfig();
301
-
302
271
  const response = await chatService.listConversations(
303
272
  {
304
273
  limit: 20
@@ -339,7 +308,7 @@ test("chat service rejects workspace-scoped assistant surfaces when workspace su
339
308
  },
340
309
  serviceToolCatalog: {},
341
310
  assistantConfigService: {},
342
- resolveAppConfig: () => createAssistantAppConfig()
311
+ appConfig: createAssistantAppConfig()
343
312
  });
344
313
 
345
314
  await assert.rejects(
@@ -367,10 +336,10 @@ test("chat service rejects workspace-scoped assistant surfaces when workspace su
367
336
 
368
337
  test("registerRoutes omits workspace assistant routes when workspace scope support is unavailable", () => {
369
338
  const testApp = createAssistantTestApp({
370
- resolveCurrentAppConfig: () => createAssistantAppConfig()
339
+ workspaceScopeSupport: null
371
340
  });
372
341
 
373
- registerRoutes(testApp.app);
342
+ registerRoutes(testApp.router, { config: createAssistantAppConfig() });
374
343
  const routes = testApp.router.list();
375
344
 
376
345
  assert.equal(
@@ -0,0 +1,38 @@
1
+ import assert from "node:assert/strict";
2
+ import { readdir, readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import test from "node:test";
5
+ import { fileURLToPath } from "node:url";
6
+ import packageJson from "../package.json" with { type: "json" };
7
+
8
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
+ const packageMetadata = packageJson.jskit;
10
+
11
+ test("assistant-runtime registers providers without install-time authoring machinery", async () => {
12
+ assert.equal(packageMetadata.kind, "runtime");
13
+ assert.equal(packageMetadata.capabilities?.requires?.includes("workspaces.core"), false);
14
+ assert.equal(packageMetadata.runtime?.server?.providers?.[0]?.entrypoint, "src/server/AssistantProvider.js");
15
+ assert.equal(packageMetadata.runtime?.server?.providers?.[0]?.export, "AssistantFeature");
16
+ assert.equal(
17
+ packageMetadata.runtime?.client?.providers?.[0]?.entrypoint,
18
+ "src/client/providers/AssistantClientProvider.js"
19
+ );
20
+ assert.equal(Object.hasOwn(packageMetadata, "mutations"), false);
21
+ assert.equal(Object.hasOwn(packageMetadata.metadata.apiSummary, "containerTokens"), false);
22
+ assert.deepEqual(packageMetadata.migrations, { directories: ["migrations"] });
23
+ assert.deepEqual((await readdir(path.join(PACKAGE_ROOT, "migrations"))).sort(), [
24
+ "assistant_config_initial.cjs",
25
+ "assistant_transcripts_initial.cjs"
26
+ ]);
27
+
28
+ const publicConfig = await readFile(
29
+ path.join(PACKAGE_ROOT, "patterns/assistant-surface/example/config/public.js"),
30
+ "utf8"
31
+ );
32
+ const serverConfig = await readFile(
33
+ path.join(PACKAGE_ROOT, "patterns/assistant-surface/example/config/server.js"),
34
+ "utf8"
35
+ );
36
+ assert.match(publicConfig, /assistantSurfaces/u);
37
+ assert.match(serverConfig, /assistantServer/u);
38
+ });