@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-core",
3
- "version": "0.1.149",
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.115",
15
- "@jskit-ai/resource-crud-core": "0.1.115",
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.171",
23
- "@jskit-ai/kernel": "0.1.173",
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 = new URLSearchParams();
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 = new URLSearchParams();
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
- ? DISCOVERY_TOOL_DESCRIPTORS.slice()
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
  }
@@ -305,6 +305,7 @@ const assistantResource = defineResource({
305
305
  export {
306
306
  MAX_INPUT_CHARS,
307
307
  MAX_HISTORY_MESSAGES,
308
+ MAX_MESSAGE_PAGE_SIZE,
308
309
  assistantResource,
309
310
  assistantConversationOutputValidator
310
311
  };
@@ -28,6 +28,7 @@ export {
28
28
  export {
29
29
  MAX_INPUT_CHARS,
30
30
  MAX_HISTORY_MESSAGES,
31
+ MAX_MESSAGE_PAGE_SIZE,
31
32
  assistantResource,
32
33
  assistantConversationOutputValidator
33
34
  } from "./assistantResource.js";
@@ -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
- limit: 5
46
+ cursor: "next-page",
47
+ limit: 5,
48
+ status: "completed"
47
49
  });
48
50
  await api.getConversationMessages(99, {
49
- page: 1,
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")}`