@jskit-ai/assistant-core 0.2.44 → 0.2.45

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/CHANGELOG.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Each tool set `createServiceToolCatalog` resolves carries its catalogue's `limits` (`maxToolArgumentBytes` and `maxToolResultBytes`), and an application conversation's tools hold calls to that tool set's `maxToolArgumentBytes` and give engines that value as `maximumArgumentBytes`, instead of reading the catalogue's own `limits`. A catalogue that resolves each surface's tool set from a catalogue of its own, as `@jskit-ai/assistant-runtime`'s does, now has each surface's argument limit enforced; its result limit already was, by that surface's catalogue. **Breaking** for a hand-written tool catalogue passed as `toolCatalog`: its `resolveToolSet` returns `limits: { maxToolArgumentBytes }` with its tools.
5
6
  - A Claude goal's clock counts from its first start: Resume and restart recovery send the same unfinished objective again, which Claude starts anew, so the clock went back to zero after every Pause and Resume and after a server restart. The Claude history keeps that first start, also across the clear that suspends a paused goal's checker, and the goal value carries it as `startedAt` (ISO), which `AssistantGoalControl` counts its wall-clock time from (else `createdAt`). A goal finished or cancelled and then set again starts a new clock.
6
7
  - The goal menu is 360 pixels wide, or the screen's width less the menu's 12-pixel margins on a narrower phone (the menu's `width` and `max-width`), so the new-goal objective box fills it instead of shrinking to its label.
7
8
  - `assistantConversationListItemOutputValidator` (from `@jskit-ai/assistant-core/shared`) is one conversation of `assistantContract`'s `conversationsList`, the record without its `metadata`; the contract's list output is built from it, and `@jskit-ai/assistant-runtime`'s conversations list route serializes its items with it.
@@ -8,7 +8,7 @@
8
8
  "build": "vite build"
9
9
  },
10
10
  "dependencies": {
11
- "@jskit-ai/assistant-core": "0.2.44",
11
+ "@jskit-ai/assistant-core": "0.2.45",
12
12
  "@jskit-ai/http-runtime": "0.2.20",
13
13
  "@jskit-ai/kernel": "0.2.20",
14
14
  "@jskit-ai/material-web": "0.1.5",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-core",
3
- "version": "0.2.44",
3
+ "version": "0.2.45",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "jskit test"
@@ -17,7 +17,7 @@ const TRANSIENT_HOOKS = new Set([authorizeTransientCall, saveTransientCalls, ann
17
17
  * One turn's application calls. Native/API adapters only transport these calls.
18
18
  * @param {object} options
19
19
  * @param {import("../types.js").ServiceToolCatalog} options.catalog
20
- * @param {{ tools: object[] }} [options.toolSet] A tool set resolved earlier for this turn.
20
+ * @param {{ tools: object[], limits: { maxToolArgumentBytes: number } }} [options.toolSet] A tool set resolved earlier for this turn.
21
21
  * @param {object} options.context The caller context the calls run as.
22
22
  * @param {() => Promise<object>} [options.prepareContext] Maps the context again before each call.
23
23
  * @param {AbortSignal} options.signal
@@ -42,6 +42,8 @@ export function createConversationTools({ catalog, toolSet: retainedToolSet, con
42
42
  // readable-check: allow method-probe - input validation: an optional context mapper must be a function
43
43
  if (prepareContext !== undefined && typeof prepareContext !== "function") throw new TypeError("Application tool context requires a server-owned mapper.");
44
44
  const toolSet = retainedToolSet ?? catalog.resolveToolSet(context, { discoveryOnly });
45
+ // The tool set's own limit: a surface-aware catalogue resolves each surface's set with that surface's.
46
+ const maximumArgumentBytes = toolSet.limits.maxToolArgumentBytes;
45
47
  const calls = new Map(structuredClone(previousCalls).map(call => [call.id, call]));
46
48
  let pending = Promise.resolve();
47
49
  let requested = 0;
@@ -61,7 +63,7 @@ export function createConversationTools({ catalog, toolSet: retainedToolSet, con
61
63
  signal.throwIfAborted();
62
64
  if (typeof id !== "string" || !id || id.length > 256 || typeof name !== "string" || !name || name.length > 256 ||
63
65
  typeof argumentsText !== "string") throw new TypeError("An application tool requires its call id, name and JSON arguments.");
64
- if (Buffer.byteLength(argumentsText) > catalog.limits.maxToolArgumentBytes) throw Object.assign(new Error("The application tool arguments exceed their size limit."), {
66
+ if (Buffer.byteLength(argumentsText) > maximumArgumentBytes) throw Object.assign(new Error("The application tool arguments exceed their size limit."), {
65
67
  messageCode: "assistant.conversation.toolArgumentsTooLarge", messageParams: {}
66
68
  });
67
69
  const input = { id, name, arguments: argumentsText };
@@ -117,7 +119,7 @@ export function createConversationTools({ catalog, toolSet: retainedToolSet, con
117
119
  descriptors: toolSet.tools,
118
120
  schemas: toolSet.tools.map(catalog.toOpenAiToolSchema),
119
121
  execute,
120
- maximumArgumentBytes: catalog.limits.maxToolArgumentBytes,
122
+ maximumArgumentBytes,
121
123
  records
122
124
  });
123
125
  }
@@ -381,6 +381,8 @@ function createServiceToolCatalog(
381
381
  );
382
382
  const maxToolArgumentBytes = normalizePositiveInteger(rawMaxToolArgumentBytes, { fallback: DEFAULT_MAX_TOOL_ARGUMENT_BYTES });
383
383
  const maxToolResultBytes = normalizePositiveInteger(rawMaxToolResultBytes, { fallback: DEFAULT_MAX_TOOL_RESULT_BYTES });
384
+ // Every tool set carries these, so the calls made with one surface's set keep that surface's limits.
385
+ const limits = Object.freeze({ maxToolArgumentBytes, maxToolResultBytes });
384
386
  const toolSetStates = new WeakMap();
385
387
  let methodEntries = null;
386
388
 
@@ -446,7 +448,8 @@ function createServiceToolCatalog(
446
448
 
447
449
  const toolSet = Object.freeze({
448
450
  tools: Object.freeze(tools),
449
- byName
451
+ byName,
452
+ limits
450
453
  });
451
454
  const actionEntriesById = new Map();
452
455
  const directEntriesByToolName = new Map();
@@ -669,10 +672,7 @@ function createServiceToolCatalog(
669
672
  }
670
673
 
671
674
  return Object.freeze({
672
- limits: Object.freeze({
673
- maxToolArgumentBytes,
674
- maxToolResultBytes
675
- }),
675
+ limits,
676
676
  resolveToolSet,
677
677
  toOpenAiToolSchema,
678
678
  executeToolCall
@@ -360,6 +360,36 @@ test("failed reservations and oversized arguments cannot reach application execu
360
360
  assert.equal(executions, 0);
361
361
  });
362
362
 
363
+ test("the resolved tool set's argument limit is the one its calls are held to and the one engines are given", async () => {
364
+ const lengths = [];
365
+ const actions = createActionCatalogue();
366
+ actions.register({ contributorId: "test.documents", domain: "test", actions: [{
367
+ id: "documents.propose", version: 1, kind: "command", channels: ["automation"], surfaces: ["app", "home"],
368
+ permission: { require: "authenticated" },
369
+ input: { schema: createSchema({ text: { type: "string", required: true } }), mode: "replace" },
370
+ output: { schema: createSchema({ length: { type: "number", required: true } }), mode: "replace" },
371
+ execute: async ({ text }) => { lengths.push(text.length); return { length: text.length }; }
372
+ }] });
373
+ // One catalogue per surface behind one, as assistant-runtime's surface-aware catalogue is:
374
+ // the app surface allows 64 KiB of arguments, home keeps the default 32 KiB.
375
+ const surfaces = { app: createServiceToolCatalog(actions, { maxToolArgumentBytes: 64 * 1024 }), home: createServiceToolCatalog(actions) };
376
+ const catalog = {
377
+ resolveToolSet: (toolContext, options) => surfaces[toolContext.surface].resolveToolSet(toolContext, options),
378
+ toOpenAiToolSchema: surfaces.home.toOpenAiToolSchema,
379
+ executeToolCall: payload => surfaces[payload.context.surface].executeToolCall(payload)
380
+ };
381
+ const tools = surface => createConversationTools({ catalog, context: { ...context, surface }, signal: new AbortController().signal,
382
+ transient: true });
383
+ const proposal = name => ({ id: "propose", name, arguments: JSON.stringify({ text: "x".repeat(40 * 1024) }) });
384
+ const app = tools("app");
385
+ assert.equal(app.maximumArgumentBytes, 64 * 1024);
386
+ assert.deepEqual(await app.execute(proposal(app.descriptors[0].name)), { ok: true, result: { length: 40 * 1024 } });
387
+ const home = tools("home");
388
+ assert.equal(home.maximumArgumentBytes, 32 * 1024);
389
+ await assert.rejects(home.execute(proposal(home.descriptors[0].name)), { message: "The application tool arguments exceed their size limit." });
390
+ assert.deepEqual(lengths, [40 * 1024]);
391
+ });
392
+
363
393
  test("closing a native tool connection prevents queued calls while retaining an invoked result", async () => {
364
394
  const entered = Promise.withResolvers(), complete = Promise.withResolvers();
365
395
  let executions = 0;
@@ -54,6 +54,25 @@ test("the native tool connection requires its private credential and bounds inco
54
54
  assert.deepEqual(calls, [{}]);
55
55
  });
56
56
 
57
+ test("arguments up to the limit reach the application through the native tool connection with the longest call identity", async t => {
58
+ const limit = 128 * 1024;
59
+ const sizes = [];
60
+ const bridge = await createOpenCodeToolBridge({ schemas: [], maxArgumentBytes: limit,
61
+ execute: async value => { sizes.push(Buffer.byteLength(JSON.stringify(value.input))); return { ok: true }; } });
62
+ t.after(() => bridge.close());
63
+ const { url, token } = bridge.configuration;
64
+ // The plugin's request: the call's identities, each at most 256 characters, around its input.
65
+ const identity = { sessionId: "s".repeat(256), messageId: "m".repeat(256), id: "c".repeat(256), name: "n".repeat(256) };
66
+ const send = text => fetch(url, { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
67
+ body: JSON.stringify({ ...identity, input: { text } }) });
68
+ assert.deepEqual(await (await send("x".repeat(limit - 11))).json(), { ok: true });
69
+ assert.deepEqual(sizes, [limit]);
70
+ const tooLarge = await send("x".repeat(limit + 4096));
71
+ assert.equal(tooLarge.status, 400);
72
+ assert.match((await tooLarge.json()).error, /size limit/);
73
+ assert.deepEqual(sizes, [limit]);
74
+ });
75
+
57
76
  test("native plugin definitions retain exact schemas and native identity without enabling built-in tools", async t => {
58
77
  const schema = { type: "object", properties: { query: { type: "string" } }, additionalProperties: false };
59
78
  const calls = [];
@@ -631,6 +631,15 @@ test("discovery pages and persisted tool results stay within configured bounds",
631
631
  });
632
632
  });
633
633
 
634
+ test("each tool set carries its catalog's size limits for the calls made with it", () => {
635
+ const context = { actor: { id: "7" }, surface: "admin" };
636
+ const actions = createActions([action()]);
637
+ const large = createServiceToolCatalog(actions, { maxToolArgumentBytes: 64 * 1024 });
638
+ assert.deepEqual(large.resolveToolSet(context).limits, { maxToolArgumentBytes: 64 * 1024, maxToolResultBytes: 48 * 1024 });
639
+ assert.equal(large.resolveToolSet(context, { discoveryOnly: true }).limits, large.limits);
640
+ assert.equal(createServiceToolCatalog(actions).resolveToolSet(context).limits.maxToolArgumentBytes, 32 * 1024);
641
+ });
642
+
634
643
  test("assistant tools reject unknown tools and return safe action failures", async () => {
635
644
  const actions = createActions([action({
636
645
  execute: async () => {