@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.
- package/package.json +84 -8
- package/patterns/assistant-surface/PATTERN.md +77 -0
- package/patterns/assistant-surface/example/config/public.js +19 -0
- package/patterns/assistant-surface/example/config/server.js +9 -0
- package/patterns/assistant-surface/example/src/pages/admin/assistant/index.vue +7 -0
- package/patterns/assistant-surface/example/src/pages/admin/settings/assistant/index.vue +7 -0
- package/patterns/assistant-surface/example/src/placement.js +38 -0
- package/src/client/components/AssistantSettingsClientElement.vue +1 -1
- package/src/client/composables/useAssistantRuntime.js +2 -2
- package/src/client/providers/AssistantClientProvider.js +9 -11
- package/src/server/AssistantProvider.js +74 -109
- package/src/server/actions.js +37 -17
- package/src/server/registerRoutes.js +4 -9
- package/src/server/repositories/assistantConfigRepository.js +1 -1
- package/src/server/repositories/conversationsRepository.js +1 -1
- package/src/server/repositories/messagesRepository.js +1 -1
- package/src/server/repositories/repositoryPersistenceUtils.js +29 -0
- package/src/server/support/createSurfaceAwareToolCatalog.js +5 -5
- package/test/actionDefinitions.test.js +49 -142
- package/test/assistantSurfacePattern.test.js +30 -0
- package/test/createSurfaceAwareToolCatalog.test.js +4 -2
- package/test/featureRuntime.test.js +76 -0
- package/test/lazyAppConfig.test.js +16 -47
- package/test/packageMetadata.test.js +38 -0
- package/package.descriptor.mjs +0 -155
- package/src/server/support/workspaceScopeSupport.js +0 -47
- package/test/packageDescriptor.test.js +0 -38
- /package/{templates/migrations → migrations}/assistant_config_initial.cjs +0 -0
- /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(
|
|
503
|
-
if (!
|
|
504
|
-
throw new Error("registerRoutes requires
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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(
|
|
25
|
-
if (!
|
|
26
|
-
throw new Error("createSurfaceAwareToolCatalog requires
|
|
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(
|
|
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(
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
assert.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
133
|
-
const definition = findDefinition(
|
|
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
|
|
155
|
-
const definition = findDefinition(
|
|
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
|
|
177
|
-
const definition = findDefinition(
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
339
|
+
workspaceScopeSupport: null
|
|
371
340
|
});
|
|
372
341
|
|
|
373
|
-
registerRoutes(testApp.
|
|
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
|
+
});
|