@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 +1 -0
- package/examples/conversation/package.json +1 -1
- package/package.json +1 -1
- package/src/server/conversation/tools.js +5 -3
- package/src/server/lib/serviceToolCatalog.js +5 -5
- package/test/conversationTools.test.js +30 -0
- package/test/openCodeTools.test.js +19 -0
- package/test/serviceToolCatalog.test.js +9 -0
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.
|
package/package.json
CHANGED
|
@@ -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) >
|
|
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
|
|
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
|
|
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 () => {
|