@jskit-ai/assistant-core 0.1.149 → 0.1.151
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 +5 -5
- package/src/client/lib/assistantApi.js +16 -7
- package/src/server/lib/serviceToolCatalog.js +28 -2
- package/src/shared/assistantResource.js +1 -0
- package/src/shared/index.js +1 -0
- package/test/assistantApiSurfaceHeader.test.js +16 -2
- package/test/serviceToolCatalog.test.js +70 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/assistant-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.151",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -11,16 +11,16 @@
|
|
|
11
11
|
"./shared": "./src/shared/index.js"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@jskit-ai/resource-core": "0.1.
|
|
15
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
14
|
+
"@jskit-ai/resource-core": "0.1.117",
|
|
15
|
+
"@jskit-ai/resource-crud-core": "0.1.117",
|
|
16
16
|
"dompurify": "^3.4.13",
|
|
17
17
|
"json-rest-schema": "^1.0.17",
|
|
18
18
|
"marked": "^17.0.4",
|
|
19
19
|
"openai": "^6.22.0"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
23
|
-
"@jskit-ai/kernel": "0.1.
|
|
22
|
+
"@jskit-ai/http-runtime": "0.1.173",
|
|
23
|
+
"@jskit-ai/kernel": "0.1.175",
|
|
24
24
|
"vue": "^3.5.13",
|
|
25
25
|
"vuetify": "^4.0.0"
|
|
26
26
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { appendQueryString } from "@jskit-ai/kernel/shared/support";
|
|
2
|
+
import { encodeJsonApiResourceQueryObject } from "@jskit-ai/http-runtime/shared";
|
|
2
3
|
import {
|
|
3
4
|
ASSISTANT_CONVERSATIONS_TRANSPORT,
|
|
4
5
|
ASSISTANT_CONVERSATION_MESSAGES_TRANSPORT,
|
|
@@ -30,6 +31,19 @@ function appendQueryParam(params, key, value) {
|
|
|
30
31
|
params.set(key, normalized);
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
function createAssistantQueryParams(query = {}, transport = null) {
|
|
35
|
+
const params = new URLSearchParams();
|
|
36
|
+
const encodedQuery = encodeJsonApiResourceQueryObject(query, {
|
|
37
|
+
responseType: transport?.responseType
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
for (const [key, value] of Object.entries(encodedQuery)) {
|
|
41
|
+
appendQueryParam(params, key, value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return params;
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
function normalizeSurfaceHeaderValue(value) {
|
|
34
48
|
return String(value || "").trim().toLowerCase();
|
|
35
49
|
}
|
|
@@ -110,10 +124,7 @@ function createAssistantApi({ request, requestStream, resolveBasePath, resolveSu
|
|
|
110
124
|
|
|
111
125
|
listConversations(query = {}) {
|
|
112
126
|
const basePath = resolveRequiredBasePath(resolveBasePath);
|
|
113
|
-
const params =
|
|
114
|
-
appendQueryParam(params, "cursor", query.cursor);
|
|
115
|
-
appendQueryParam(params, "limit", query.limit);
|
|
116
|
-
appendQueryParam(params, "status", query.status);
|
|
127
|
+
const params = createAssistantQueryParams(query, ASSISTANT_CONVERSATIONS_TRANSPORT);
|
|
117
128
|
const requestHeaders = resolveAssistantRequestHeaders(resolveSurfaceId);
|
|
118
129
|
|
|
119
130
|
return request(
|
|
@@ -128,9 +139,7 @@ function createAssistantApi({ request, requestStream, resolveBasePath, resolveSu
|
|
|
128
139
|
getConversationMessages(conversationId, query = {}) {
|
|
129
140
|
const basePath = resolveRequiredBasePath(resolveBasePath);
|
|
130
141
|
const encodedConversationId = encodeURIComponent(String(conversationId || "").trim());
|
|
131
|
-
const params =
|
|
132
|
-
appendQueryParam(params, "page", query.page);
|
|
133
|
-
appendQueryParam(params, "pageSize", query.pageSize);
|
|
142
|
+
const params = createAssistantQueryParams(query, ASSISTANT_CONVERSATION_MESSAGES_TRANSPORT);
|
|
134
143
|
const requestHeaders = resolveAssistantRequestHeaders(resolveSurfaceId);
|
|
135
144
|
|
|
136
145
|
return request(
|
|
@@ -9,6 +9,8 @@ import { resolveWorkspaceSlug } from "./resolveWorkspaceSlug.js";
|
|
|
9
9
|
|
|
10
10
|
const AUTOMATION_CHANNEL = "automation";
|
|
11
11
|
const DEFAULT_MAX_DIRECT_TOOLS = 32;
|
|
12
|
+
const MAX_ALWAYS_AVAILABLE_TOOLS = 8;
|
|
13
|
+
const MAX_PREFLIGHT_INTENTS_PER_TOOL = 8;
|
|
12
14
|
const DEFAULT_DISCOVERY_PAGE_SIZE = 10;
|
|
13
15
|
const MAX_DISCOVERY_PAGE_SIZE = 20;
|
|
14
16
|
const DEFAULT_MAX_TOOL_ARGUMENT_BYTES = 32 * 1024;
|
|
@@ -141,6 +143,14 @@ const DISCOVERY_TOOL_DESCRIPTORS = Object.freeze([
|
|
|
141
143
|
})
|
|
142
144
|
]);
|
|
143
145
|
|
|
146
|
+
function normalizePreflightIntents(value) {
|
|
147
|
+
const source = Array.isArray(value) ? value : [value];
|
|
148
|
+
const normalized = source
|
|
149
|
+
.map((entry) => normalizeText(entry).toLowerCase())
|
|
150
|
+
.filter(Boolean);
|
|
151
|
+
return Object.freeze([...new Set(normalized)].slice(0, MAX_PREFLIGHT_INTENTS_PER_TOOL));
|
|
152
|
+
}
|
|
153
|
+
|
|
144
154
|
function normalizeAssistantExtension(value) {
|
|
145
155
|
const source = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
146
156
|
if (source.transformResult != null && typeof source.transformResult !== "function") {
|
|
@@ -149,6 +159,8 @@ function normalizeAssistantExtension(value) {
|
|
|
149
159
|
|
|
150
160
|
return Object.freeze({
|
|
151
161
|
description: normalizeText(source.description),
|
|
162
|
+
alwaysAvailable: source.alwaysAvailable === true,
|
|
163
|
+
preflight: normalizePreflightIntents(source.preflight),
|
|
152
164
|
output: Object.hasOwn(source, "output") ? source.output : null,
|
|
153
165
|
transformResult: typeof source.transformResult === "function" ? source.transformResult : null
|
|
154
166
|
});
|
|
@@ -545,6 +557,8 @@ function resolveActionBackedToolEntries(actions) {
|
|
|
545
557
|
kind: normalizeText(action.kind).toLowerCase() || "command",
|
|
546
558
|
toolBaseName: actionId,
|
|
547
559
|
description: assistantExtension.description || `Run ${actionId}.`,
|
|
560
|
+
alwaysAvailable: assistantExtension.alwaysAvailable,
|
|
561
|
+
preflight: assistantExtension.preflight,
|
|
548
562
|
inputSchema,
|
|
549
563
|
outputDefinition,
|
|
550
564
|
outputSchema,
|
|
@@ -594,6 +608,8 @@ function resolveActionToolEntries(
|
|
|
594
608
|
actionId,
|
|
595
609
|
actionVersion: Number(actionEntry.actionVersion) || null,
|
|
596
610
|
description: normalizeText(actionEntry.description) || `Run ${actionId}.`,
|
|
611
|
+
alwaysAvailable: actionEntry.alwaysAvailable === true,
|
|
612
|
+
preflight: actionEntry.preflight,
|
|
597
613
|
parameters: actionEntry.inputSchema,
|
|
598
614
|
outputSchema: actionEntry.outputSchema
|
|
599
615
|
}),
|
|
@@ -677,8 +693,18 @@ function createServiceToolCatalog(
|
|
|
677
693
|
function resolveToolSet(context = {}) {
|
|
678
694
|
const actionEntries = resolveAuthorizedEntries(context);
|
|
679
695
|
const useDiscovery = actionEntries.length > maxDirectTools;
|
|
696
|
+
const alwaysAvailableEntries = useDiscovery
|
|
697
|
+
? actionEntries
|
|
698
|
+
.filter((entry) => entry.descriptor.alwaysAvailable === true)
|
|
699
|
+
.filter((entry) => !Object.values(DISCOVERY_TOOL_NAMES).includes(entry.descriptor.name))
|
|
700
|
+
.slice(0, MAX_ALWAYS_AVAILABLE_TOOLS)
|
|
701
|
+
: [];
|
|
702
|
+
const alwaysAvailableEntrySet = new Set(alwaysAvailableEntries);
|
|
680
703
|
const tools = useDiscovery
|
|
681
|
-
?
|
|
704
|
+
? [
|
|
705
|
+
...DISCOVERY_TOOL_DESCRIPTORS,
|
|
706
|
+
...alwaysAvailableEntries.map((entry) => entry.descriptor)
|
|
707
|
+
]
|
|
682
708
|
: actionEntries.map((entry) => entry.descriptor);
|
|
683
709
|
const byName = new Map();
|
|
684
710
|
for (const descriptor of tools) {
|
|
@@ -693,7 +719,7 @@ function createServiceToolCatalog(
|
|
|
693
719
|
const directEntriesByToolName = new Map();
|
|
694
720
|
for (const entry of actionEntries) {
|
|
695
721
|
actionEntriesById.set(entry.descriptor.actionId.toLowerCase(), entry);
|
|
696
|
-
if (!useDiscovery) {
|
|
722
|
+
if (!useDiscovery || alwaysAvailableEntrySet.has(entry)) {
|
|
697
723
|
directEntriesByToolName.set(entry.descriptor.name, entry);
|
|
698
724
|
}
|
|
699
725
|
}
|
package/src/shared/index.js
CHANGED
|
@@ -43,10 +43,12 @@ test("assistant api forwards normalized surface header on requests", async () =>
|
|
|
43
43
|
input: "Hello"
|
|
44
44
|
});
|
|
45
45
|
await api.listConversations({
|
|
46
|
-
|
|
46
|
+
cursor: "next-page",
|
|
47
|
+
limit: 5,
|
|
48
|
+
status: "completed"
|
|
47
49
|
});
|
|
48
50
|
await api.getConversationMessages(99, {
|
|
49
|
-
page:
|
|
51
|
+
page: 2,
|
|
50
52
|
pageSize: 5
|
|
51
53
|
});
|
|
52
54
|
await api.getSettings();
|
|
@@ -59,6 +61,18 @@ test("assistant api forwards normalized surface header on requests", async () =>
|
|
|
59
61
|
assert.equal(observed.messages?.options?.headers?.["x-jskit-surface"], "admin");
|
|
60
62
|
assert.equal(observed.settingsRead?.options?.headers?.["x-jskit-surface"], "admin");
|
|
61
63
|
assert.equal(observed.settingsUpdate?.options?.headers?.["x-jskit-surface"], "admin");
|
|
64
|
+
const conversationsUrl = new URL(observed.list?.url, "https://assistant.test");
|
|
65
|
+
assert.equal(conversationsUrl.searchParams.get("page[cursor]"), "next-page");
|
|
66
|
+
assert.equal(conversationsUrl.searchParams.get("page[limit]"), "5");
|
|
67
|
+
assert.equal(conversationsUrl.searchParams.get("filter[status]"), "completed");
|
|
68
|
+
assert.equal(conversationsUrl.searchParams.has("cursor"), false);
|
|
69
|
+
assert.equal(conversationsUrl.searchParams.has("limit"), false);
|
|
70
|
+
assert.equal(conversationsUrl.searchParams.has("status"), false);
|
|
71
|
+
const messagesUrl = new URL(observed.messages?.url, "https://assistant.test");
|
|
72
|
+
assert.equal(messagesUrl.searchParams.get("filter[page]"), "2");
|
|
73
|
+
assert.equal(messagesUrl.searchParams.get("filter[pageSize]"), "5");
|
|
74
|
+
assert.equal(messagesUrl.searchParams.has("page"), false);
|
|
75
|
+
assert.equal(messagesUrl.searchParams.has("pageSize"), false);
|
|
62
76
|
assert.deepEqual(observed.list?.options?.transport, ASSISTANT_CONVERSATIONS_TRANSPORT);
|
|
63
77
|
assert.deepEqual(observed.messages?.options?.transport, ASSISTANT_CONVERSATION_MESSAGES_TRANSPORT);
|
|
64
78
|
assert.deepEqual(observed.settingsRead?.options?.transport, ASSISTANT_SETTINGS_TRANSPORT);
|
|
@@ -249,11 +249,26 @@ test("assistant tools expose safe field-level input guidance to the model", asyn
|
|
|
249
249
|
|
|
250
250
|
test("large authorized catalogs use compact paged discovery, exact contracts, and gated execution", async () => {
|
|
251
251
|
const executions = [];
|
|
252
|
+
let clockExecution = null;
|
|
252
253
|
const workspaceInput = schema({
|
|
253
254
|
workspaceSlug: { type: "string", required: true },
|
|
254
255
|
q: { type: "string", required: false }
|
|
255
256
|
});
|
|
256
257
|
const definitions = [
|
|
258
|
+
action({
|
|
259
|
+
id: "demo.clock.read",
|
|
260
|
+
input: workspaceInput,
|
|
261
|
+
extensions: {
|
|
262
|
+
assistant: {
|
|
263
|
+
alwaysAvailable: true,
|
|
264
|
+
preflight: ["CURRENT-TIME", "current-time"]
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
execute: async (input, context) => {
|
|
268
|
+
clockExecution = { input, context };
|
|
269
|
+
return { ok: true };
|
|
270
|
+
}
|
|
271
|
+
}),
|
|
257
272
|
action({
|
|
258
273
|
id: "demo.books.list",
|
|
259
274
|
input: workspaceInput,
|
|
@@ -284,8 +299,24 @@ test("large authorized catalogs use compact paged discovery, exact contracts, an
|
|
|
284
299
|
assert.deepEqual(toolSet.tools.map((entry) => entry.name), [
|
|
285
300
|
"assistant_action_search",
|
|
286
301
|
"assistant_action_contract",
|
|
287
|
-
"assistant_action_execute"
|
|
302
|
+
"assistant_action_execute",
|
|
303
|
+
"demo_clock_read"
|
|
288
304
|
]);
|
|
305
|
+
const clockTool = toolSet.tools.find((entry) => entry.actionId === "demo.clock.read");
|
|
306
|
+
assert.deepEqual(clockTool.preflight, ["current-time"]);
|
|
307
|
+
assert.equal(clockTool.alwaysAvailable, true);
|
|
308
|
+
assert.equal(clockTool.parameters.required?.includes("workspaceSlug") || false, false);
|
|
309
|
+
assert.deepEqual(await catalog.executeToolCall({
|
|
310
|
+
toolName: clockTool.name,
|
|
311
|
+
argumentsText: "{}",
|
|
312
|
+
context,
|
|
313
|
+
toolSet
|
|
314
|
+
}), {
|
|
315
|
+
ok: true,
|
|
316
|
+
result: { ok: true }
|
|
317
|
+
});
|
|
318
|
+
assert.deepEqual(clockExecution.input, { workspaceSlug: "library" });
|
|
319
|
+
assert.equal(clockExecution.context.channel, "automation");
|
|
289
320
|
|
|
290
321
|
const firstPage = await catalog.executeToolCall({
|
|
291
322
|
toolName: "assistant_action_search",
|
|
@@ -379,6 +410,44 @@ test("large authorized catalogs use compact paged discovery, exact contracts, an
|
|
|
379
410
|
assert.equal(executions[0].context.channel, "automation");
|
|
380
411
|
});
|
|
381
412
|
|
|
413
|
+
test("discovery mode bounds authorized always-available tools", () => {
|
|
414
|
+
const definitions = Array.from({ length: 10 }, (_, index) => action({
|
|
415
|
+
id: `demo.clock-${String(index + 1).padStart(2, "0")}.read`,
|
|
416
|
+
extensions: {
|
|
417
|
+
assistant: {
|
|
418
|
+
alwaysAvailable: true,
|
|
419
|
+
preflight: ["current-time"]
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}));
|
|
423
|
+
definitions.push(action({
|
|
424
|
+
id: "demo.clock-denied.read",
|
|
425
|
+
permission: { require: "all", permissions: ["clock.read"] },
|
|
426
|
+
extensions: {
|
|
427
|
+
assistant: {
|
|
428
|
+
alwaysAvailable: true,
|
|
429
|
+
preflight: ["current-time"]
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}));
|
|
433
|
+
|
|
434
|
+
const toolSet = createServiceToolCatalog(createActions(definitions), {
|
|
435
|
+
maxDirectTools: 0
|
|
436
|
+
}).resolveToolSet({ actor: { id: "7" }, surface: "admin" });
|
|
437
|
+
|
|
438
|
+
assert.deepEqual(toolSet.tools.slice(0, 3).map((entry) => entry.name), [
|
|
439
|
+
"assistant_action_search",
|
|
440
|
+
"assistant_action_contract",
|
|
441
|
+
"assistant_action_execute"
|
|
442
|
+
]);
|
|
443
|
+
assert.equal(toolSet.tools.length, 11);
|
|
444
|
+
assert.deepEqual(
|
|
445
|
+
toolSet.tools.slice(3).map((entry) => entry.actionId),
|
|
446
|
+
definitions.slice(0, 8).map((entry) => entry.id)
|
|
447
|
+
);
|
|
448
|
+
assert.equal(toolSet.tools.some((entry) => entry.actionId === "demo.clock-denied.read"), false);
|
|
449
|
+
});
|
|
450
|
+
|
|
382
451
|
test("discovery pages and persisted tool results stay within configured bounds", async () => {
|
|
383
452
|
const definitions = Array.from({ length: 25 }, (_, index) => action({
|
|
384
453
|
id: `demo.items.action-${String(index + 1).padStart(2, "0")}`
|