@jskit-ai/assistant-core 0.1.142 → 0.1.143
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/fixtures/responsive-assistant/App.vue +106 -0
- package/fixtures/responsive-assistant/index.html +12 -0
- package/fixtures/responsive-assistant/main.js +24 -0
- package/fixtures/responsive-assistant/vite.config.mjs +18 -0
- package/package.json +5 -5
- package/src/client/components/AssistantClientElement.vue +30 -20
- package/src/server/lib/serviceToolCatalog.js +511 -80
- package/test/assistantScroll.browser.test.js +139 -0
- package/test/componentContracts.test.js +11 -0
- package/test/serviceToolCatalog.test.js +287 -9
|
@@ -1,15 +1,156 @@
|
|
|
1
1
|
import { requireAuth } from "@jskit-ai/kernel/server/runtime";
|
|
2
2
|
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
3
3
|
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
resolveStructuredSchemaTransportSchema,
|
|
6
|
+
validateSchemaPayload
|
|
7
|
+
} from "@jskit-ai/kernel/shared/validators";
|
|
5
8
|
import { resolveWorkspaceSlug } from "./resolveWorkspaceSlug.js";
|
|
6
9
|
|
|
7
10
|
const AUTOMATION_CHANNEL = "automation";
|
|
11
|
+
const DEFAULT_MAX_DIRECT_TOOLS = 32;
|
|
12
|
+
const DEFAULT_DISCOVERY_PAGE_SIZE = 10;
|
|
13
|
+
const MAX_DISCOVERY_PAGE_SIZE = 20;
|
|
14
|
+
const DEFAULT_MAX_TOOL_ARGUMENT_BYTES = 32 * 1024;
|
|
15
|
+
const DEFAULT_MAX_TOOL_RESULT_BYTES = 48 * 1024;
|
|
16
|
+
const DISCOVERY_TOOL_NAMES = Object.freeze({
|
|
17
|
+
search: "assistant_action_search",
|
|
18
|
+
contract: "assistant_action_contract",
|
|
19
|
+
execute: "assistant_action_execute"
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const OPEN_OBJECT_SCHEMA = Object.freeze({
|
|
23
|
+
type: "object",
|
|
24
|
+
additionalProperties: true
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const ACTION_SEARCH_PARAMETERS = Object.freeze({
|
|
28
|
+
type: "object",
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
properties: {
|
|
31
|
+
query: {
|
|
32
|
+
type: "string",
|
|
33
|
+
maxLength: 200,
|
|
34
|
+
description: "Optional words or action-id fragments to match."
|
|
35
|
+
},
|
|
36
|
+
cursor: {
|
|
37
|
+
type: "string",
|
|
38
|
+
maxLength: 1000,
|
|
39
|
+
description: "Opaque cursor returned by the previous search page."
|
|
40
|
+
},
|
|
41
|
+
limit: {
|
|
42
|
+
type: "integer",
|
|
43
|
+
minimum: 1,
|
|
44
|
+
maximum: MAX_DISCOVERY_PAGE_SIZE,
|
|
45
|
+
description: "Maximum compact matches to return."
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const ACTION_SEARCH_OUTPUT_SCHEMA = Object.freeze({
|
|
51
|
+
type: "object",
|
|
52
|
+
additionalProperties: false,
|
|
53
|
+
required: ["items", "nextCursor", "total"],
|
|
54
|
+
properties: {
|
|
55
|
+
items: {
|
|
56
|
+
type: "array",
|
|
57
|
+
maxItems: MAX_DISCOVERY_PAGE_SIZE,
|
|
58
|
+
items: {
|
|
59
|
+
type: "object",
|
|
60
|
+
additionalProperties: false,
|
|
61
|
+
required: ["actionId", "version", "kind", "description"],
|
|
62
|
+
properties: {
|
|
63
|
+
actionId: { type: "string" },
|
|
64
|
+
version: { type: "integer", minimum: 1 },
|
|
65
|
+
kind: { type: "string" },
|
|
66
|
+
description: { type: "string" }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
nextCursor: {
|
|
71
|
+
anyOf: [{ type: "string" }, { type: "null" }]
|
|
72
|
+
},
|
|
73
|
+
total: { type: "integer", minimum: 0 }
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const ACTION_CONTRACT_PARAMETERS = Object.freeze({
|
|
78
|
+
type: "object",
|
|
79
|
+
additionalProperties: false,
|
|
80
|
+
required: ["actionId"],
|
|
81
|
+
properties: {
|
|
82
|
+
actionId: { type: "string", minLength: 1, maxLength: 300 },
|
|
83
|
+
version: { type: "integer", minimum: 1 }
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const ACTION_CONTRACT_OUTPUT_SCHEMA = Object.freeze({
|
|
88
|
+
type: "object",
|
|
89
|
+
additionalProperties: false,
|
|
90
|
+
required: ["actionId", "version", "kind", "description", "inputSchema", "outputSchema"],
|
|
91
|
+
properties: {
|
|
92
|
+
actionId: { type: "string" },
|
|
93
|
+
version: { type: "integer", minimum: 1 },
|
|
94
|
+
kind: { type: "string" },
|
|
95
|
+
description: { type: "string" },
|
|
96
|
+
inputSchema: OPEN_OBJECT_SCHEMA,
|
|
97
|
+
outputSchema: OPEN_OBJECT_SCHEMA
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const ACTION_EXECUTE_PARAMETERS = Object.freeze({
|
|
102
|
+
type: "object",
|
|
103
|
+
additionalProperties: false,
|
|
104
|
+
required: ["actionId", "input"],
|
|
105
|
+
properties: {
|
|
106
|
+
actionId: { type: "string", minLength: 1, maxLength: 300 },
|
|
107
|
+
version: { type: "integer", minimum: 1 },
|
|
108
|
+
input: OPEN_OBJECT_SCHEMA
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const ACTION_EXECUTE_OUTPUT_SCHEMA = Object.freeze({
|
|
113
|
+
type: "object",
|
|
114
|
+
additionalProperties: false,
|
|
115
|
+
required: ["actionId", "version", "result"],
|
|
116
|
+
properties: {
|
|
117
|
+
actionId: { type: "string" },
|
|
118
|
+
version: { type: "integer", minimum: 1 },
|
|
119
|
+
result: {}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const DISCOVERY_TOOL_DESCRIPTORS = Object.freeze([
|
|
124
|
+
Object.freeze({
|
|
125
|
+
name: DISCOVERY_TOOL_NAMES.search,
|
|
126
|
+
description: "Search the actions available to the current user and surface. Returns compact paged matches without schemas.",
|
|
127
|
+
parameters: ACTION_SEARCH_PARAMETERS,
|
|
128
|
+
outputSchema: ACTION_SEARCH_OUTPUT_SCHEMA
|
|
129
|
+
}),
|
|
130
|
+
Object.freeze({
|
|
131
|
+
name: DISCOVERY_TOOL_NAMES.contract,
|
|
132
|
+
description: "Load the exact input and output contract for one available action before executing it.",
|
|
133
|
+
parameters: ACTION_CONTRACT_PARAMETERS,
|
|
134
|
+
outputSchema: ACTION_CONTRACT_OUTPUT_SCHEMA
|
|
135
|
+
}),
|
|
136
|
+
Object.freeze({
|
|
137
|
+
name: DISCOVERY_TOOL_NAMES.execute,
|
|
138
|
+
description: "Execute one available action after loading its exact contract in this turn. The action result is returned in result.",
|
|
139
|
+
parameters: ACTION_EXECUTE_PARAMETERS,
|
|
140
|
+
outputSchema: ACTION_EXECUTE_OUTPUT_SCHEMA
|
|
141
|
+
})
|
|
142
|
+
]);
|
|
8
143
|
|
|
9
144
|
function normalizeAssistantExtension(value) {
|
|
10
145
|
const source = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
146
|
+
if (source.transformResult != null && typeof source.transformResult !== "function") {
|
|
147
|
+
throw new TypeError("extensions.assistant.transformResult must be a function when provided.");
|
|
148
|
+
}
|
|
149
|
+
|
|
11
150
|
return Object.freeze({
|
|
12
|
-
description: normalizeText(source.description)
|
|
151
|
+
description: normalizeText(source.description),
|
|
152
|
+
output: Object.hasOwn(source, "output") ? source.output : null,
|
|
153
|
+
transformResult: typeof source.transformResult === "function" ? source.transformResult : null
|
|
13
154
|
});
|
|
14
155
|
}
|
|
15
156
|
|
|
@@ -142,6 +283,107 @@ function parseToolPayload(argumentsText) {
|
|
|
142
283
|
}
|
|
143
284
|
}
|
|
144
285
|
|
|
286
|
+
function normalizeNonNegativeInteger(value, fallback) {
|
|
287
|
+
const parsed = Number(value);
|
|
288
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function normalizePositiveInteger(value, fallback) {
|
|
292
|
+
const parsed = Number(value);
|
|
293
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function serializedByteLength(value) {
|
|
297
|
+
const serialized = JSON.stringify(value);
|
|
298
|
+
if (serialized === undefined) {
|
|
299
|
+
return Buffer.byteLength("null", "utf8");
|
|
300
|
+
}
|
|
301
|
+
return Buffer.byteLength(serialized, "utf8");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function createToolError(status, code, message) {
|
|
305
|
+
const error = new Error(message);
|
|
306
|
+
error.status = status;
|
|
307
|
+
error.statusCode = status;
|
|
308
|
+
error.code = code;
|
|
309
|
+
return error;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function ensureSerializedSize(value, maxBytes, { code = "assistant_tool_result_too_large", label = "Tool result" } = {}) {
|
|
313
|
+
let byteLength = 0;
|
|
314
|
+
try {
|
|
315
|
+
byteLength = serializedByteLength(value);
|
|
316
|
+
} catch {
|
|
317
|
+
throw createToolError(500, "assistant_tool_result_unserializable", "Tool result could not be serialized.");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (byteLength > maxBytes) {
|
|
321
|
+
throw createToolError(
|
|
322
|
+
413,
|
|
323
|
+
code,
|
|
324
|
+
`${label} exceeds the assistant size limit. Narrow the request and try again.`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function ensureToolArgumentsSize(argumentsText, maxBytes) {
|
|
330
|
+
const source = String(argumentsText || "");
|
|
331
|
+
if (Buffer.byteLength(source, "utf8") > maxBytes) {
|
|
332
|
+
throw createToolError(
|
|
333
|
+
413,
|
|
334
|
+
"assistant_tool_arguments_too_large",
|
|
335
|
+
"Tool arguments exceed the assistant size limit. Narrow the request and try again."
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function normalizeActionLookupKey(actionId, version) {
|
|
341
|
+
const normalizedActionId = normalizeText(actionId).toLowerCase();
|
|
342
|
+
const normalizedVersion = Number(version);
|
|
343
|
+
if (!normalizedActionId || !Number.isInteger(normalizedVersion) || normalizedVersion < 1) {
|
|
344
|
+
return "";
|
|
345
|
+
}
|
|
346
|
+
return `${normalizedActionId}@${normalizedVersion}`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function normalizeDiscoveryQuery(value) {
|
|
350
|
+
return normalizeText(value).toLowerCase().slice(0, 200);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function encodeDiscoveryCursor(offset, query) {
|
|
354
|
+
return Buffer.from(JSON.stringify({ v: 1, offset, query }), "utf8").toString("base64url");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function decodeDiscoveryCursor(value, query) {
|
|
358
|
+
const normalizedCursor = normalizeText(value);
|
|
359
|
+
if (!normalizedCursor) {
|
|
360
|
+
return 0;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const parsed = JSON.parse(Buffer.from(normalizedCursor, "base64url").toString("utf8"));
|
|
365
|
+
if (
|
|
366
|
+
parsed?.v !== 1 ||
|
|
367
|
+
normalizeDiscoveryQuery(parsed?.query) !== query ||
|
|
368
|
+
!Number.isInteger(parsed?.offset) ||
|
|
369
|
+
parsed.offset < 0
|
|
370
|
+
) {
|
|
371
|
+
throw new Error("invalid cursor");
|
|
372
|
+
}
|
|
373
|
+
return parsed.offset;
|
|
374
|
+
} catch {
|
|
375
|
+
throw createToolError(400, "assistant_action_cursor_invalid", "Action search cursor is invalid for this query.");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function truncateDescription(value, maxLength = 240) {
|
|
380
|
+
const description = normalizeText(value);
|
|
381
|
+
if (description.length <= maxLength) {
|
|
382
|
+
return description;
|
|
383
|
+
}
|
|
384
|
+
return `${description.slice(0, Math.max(1, maxLength - 1))}…`;
|
|
385
|
+
}
|
|
386
|
+
|
|
145
387
|
function canInvokeMethod(permission, context) {
|
|
146
388
|
const permissionSpec = normalizePermissionSpec(permission);
|
|
147
389
|
|
|
@@ -243,53 +485,57 @@ function resolveActionBackedToolEntries(actions) {
|
|
|
243
485
|
}
|
|
244
486
|
const entriesByActionId = new Map();
|
|
245
487
|
for (const action of actions.listDefinitions()) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
488
|
+
if (!action || typeof action !== "object") {
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (!hasAutomationChannel(action)) {
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
252
494
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
495
|
+
const actionId = normalizeText(action.id);
|
|
496
|
+
if (!actionId) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
257
499
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
500
|
+
let assistantExtension = null;
|
|
501
|
+
try {
|
|
502
|
+
assistantExtension = normalizeAssistantActionExtension(action);
|
|
503
|
+
} catch {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
264
506
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
507
|
+
const outputDefinition = assistantExtension.output || action.output;
|
|
508
|
+
const inputSchema = resolveStructuredSchemaTransportSchema(action.input, {
|
|
509
|
+
context: `Action definition "${actionId}" input`,
|
|
510
|
+
defaultMode: "patch"
|
|
511
|
+
}) || null;
|
|
512
|
+
const outputSchema = resolveStructuredSchemaTransportSchema(outputDefinition, {
|
|
513
|
+
context: `Action definition "${actionId}" assistant output`,
|
|
514
|
+
defaultMode: "replace"
|
|
515
|
+
}) || null;
|
|
516
|
+
if (!inputSchema || !outputSchema) {
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
276
519
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
520
|
+
const actionVersion = Number(action.version) || 1;
|
|
521
|
+
const actionKey = actionId.toLowerCase();
|
|
522
|
+
const nextEntry = Object.freeze({
|
|
523
|
+
actionId,
|
|
524
|
+
actionVersion,
|
|
525
|
+
kind: normalizeText(action.kind).toLowerCase() || "command",
|
|
526
|
+
toolBaseName: actionId,
|
|
527
|
+
description: assistantExtension.description || `Run ${actionId}.`,
|
|
528
|
+
inputSchema,
|
|
529
|
+
outputDefinition,
|
|
530
|
+
outputSchema,
|
|
531
|
+
transformResult: assistantExtension.transformResult,
|
|
532
|
+
permission: normalizePermissionSpec(action.permission),
|
|
533
|
+
surfaces: normalizeSurfaceList(action.surfaces)
|
|
534
|
+
});
|
|
535
|
+
const existing = entriesByActionId.get(actionKey);
|
|
536
|
+
if (!existing || actionVersion >= Number(existing.actionVersion || 0)) {
|
|
537
|
+
entriesByActionId.set(actionKey, nextEntry);
|
|
538
|
+
}
|
|
293
539
|
}
|
|
294
540
|
|
|
295
541
|
return entriesByActionId;
|
|
@@ -331,6 +577,9 @@ function resolveActionToolEntries(
|
|
|
331
577
|
parameters: actionEntry.inputSchema,
|
|
332
578
|
outputSchema: actionEntry.outputSchema
|
|
333
579
|
}),
|
|
580
|
+
kind: actionEntry.kind,
|
|
581
|
+
outputDefinition: actionEntry.outputDefinition,
|
|
582
|
+
transformResult: actionEntry.transformResult,
|
|
334
583
|
permission: actionEntry.permission,
|
|
335
584
|
surfaces: actionEntry.surfaces
|
|
336
585
|
})
|
|
@@ -342,7 +591,14 @@ function resolveActionToolEntries(
|
|
|
342
591
|
|
|
343
592
|
function createServiceToolCatalog(
|
|
344
593
|
actions,
|
|
345
|
-
{
|
|
594
|
+
{
|
|
595
|
+
barredActionIds = [],
|
|
596
|
+
skipActionPrefixes = [],
|
|
597
|
+
maxDirectTools: rawMaxDirectTools = DEFAULT_MAX_DIRECT_TOOLS,
|
|
598
|
+
discoveryPageSize: rawDiscoveryPageSize = DEFAULT_DISCOVERY_PAGE_SIZE,
|
|
599
|
+
maxToolArgumentBytes: rawMaxToolArgumentBytes = DEFAULT_MAX_TOOL_ARGUMENT_BYTES,
|
|
600
|
+
maxToolResultBytes: rawMaxToolResultBytes = DEFAULT_MAX_TOOL_RESULT_BYTES
|
|
601
|
+
} = {}
|
|
346
602
|
) {
|
|
347
603
|
if (!actions || typeof actions.listDefinitions !== "function" || typeof actions.execute !== "function") {
|
|
348
604
|
throw new TypeError("createServiceToolCatalog requires runtime.actions.");
|
|
@@ -351,6 +607,17 @@ function createServiceToolCatalog(
|
|
|
351
607
|
const normalizedSkipPrefixes = (Array.isArray(skipActionPrefixes) ? skipActionPrefixes : [skipActionPrefixes])
|
|
352
608
|
.map((entry) => normalizeText(entry).toLowerCase())
|
|
353
609
|
.filter(Boolean);
|
|
610
|
+
const maxDirectTools = normalizeNonNegativeInteger(rawMaxDirectTools, DEFAULT_MAX_DIRECT_TOOLS);
|
|
611
|
+
const discoveryPageSize = Math.min(
|
|
612
|
+
MAX_DISCOVERY_PAGE_SIZE,
|
|
613
|
+
normalizePositiveInteger(rawDiscoveryPageSize, DEFAULT_DISCOVERY_PAGE_SIZE)
|
|
614
|
+
);
|
|
615
|
+
const maxToolArgumentBytes = normalizePositiveInteger(
|
|
616
|
+
rawMaxToolArgumentBytes,
|
|
617
|
+
DEFAULT_MAX_TOOL_ARGUMENT_BYTES
|
|
618
|
+
);
|
|
619
|
+
const maxToolResultBytes = normalizePositiveInteger(rawMaxToolResultBytes, DEFAULT_MAX_TOOL_RESULT_BYTES);
|
|
620
|
+
const toolSetStates = new WeakMap();
|
|
354
621
|
let methodEntries = null;
|
|
355
622
|
|
|
356
623
|
function resolveOrCreateMethodEntries() {
|
|
@@ -365,9 +632,8 @@ function createServiceToolCatalog(
|
|
|
365
632
|
return methodEntries;
|
|
366
633
|
}
|
|
367
634
|
|
|
368
|
-
function
|
|
369
|
-
const
|
|
370
|
-
const byName = new Map();
|
|
635
|
+
function resolveAuthorizedEntries(context = {}) {
|
|
636
|
+
const entries = [];
|
|
371
637
|
for (const entry of resolveOrCreateMethodEntries()) {
|
|
372
638
|
if (!canUseToolOnSurface(entry, context)) {
|
|
373
639
|
continue;
|
|
@@ -376,19 +642,49 @@ function createServiceToolCatalog(
|
|
|
376
642
|
continue;
|
|
377
643
|
}
|
|
378
644
|
|
|
379
|
-
|
|
380
|
-
...entry
|
|
381
|
-
|
|
382
|
-
|
|
645
|
+
entries.push(Object.freeze({
|
|
646
|
+
...entry,
|
|
647
|
+
descriptor: Object.freeze({
|
|
648
|
+
...entry.descriptor,
|
|
649
|
+
parameters: stripWorkspaceSlugFromSchema(entry.descriptor.parameters, context)
|
|
650
|
+
})
|
|
651
|
+
}));
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return Object.freeze(entries.sort((left, right) => left.descriptor.actionId.localeCompare(right.descriptor.actionId)));
|
|
655
|
+
}
|
|
383
656
|
|
|
384
|
-
|
|
657
|
+
function resolveToolSet(context = {}) {
|
|
658
|
+
const actionEntries = resolveAuthorizedEntries(context);
|
|
659
|
+
const useDiscovery = actionEntries.length > maxDirectTools;
|
|
660
|
+
const tools = useDiscovery
|
|
661
|
+
? DISCOVERY_TOOL_DESCRIPTORS.slice()
|
|
662
|
+
: actionEntries.map((entry) => entry.descriptor);
|
|
663
|
+
const byName = new Map();
|
|
664
|
+
for (const descriptor of tools) {
|
|
385
665
|
byName.set(descriptor.name, descriptor);
|
|
386
666
|
}
|
|
387
667
|
|
|
388
|
-
|
|
668
|
+
const toolSet = Object.freeze({
|
|
389
669
|
tools: Object.freeze(tools),
|
|
390
670
|
byName
|
|
391
671
|
});
|
|
672
|
+
const actionEntriesById = new Map();
|
|
673
|
+
const directEntriesByToolName = new Map();
|
|
674
|
+
for (const entry of actionEntries) {
|
|
675
|
+
actionEntriesById.set(entry.descriptor.actionId.toLowerCase(), entry);
|
|
676
|
+
if (!useDiscovery) {
|
|
677
|
+
directEntriesByToolName.set(entry.descriptor.name, entry);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
toolSetStates.set(toolSet, {
|
|
681
|
+
mode: useDiscovery ? "discovery" : "direct",
|
|
682
|
+
actionEntries,
|
|
683
|
+
actionEntriesById,
|
|
684
|
+
directEntriesByToolName,
|
|
685
|
+
contractedActionKeys: new Set()
|
|
686
|
+
});
|
|
687
|
+
return toolSet;
|
|
392
688
|
}
|
|
393
689
|
|
|
394
690
|
function toOpenAiToolSchema(tool) {
|
|
@@ -402,9 +698,151 @@ function createServiceToolCatalog(
|
|
|
402
698
|
};
|
|
403
699
|
}
|
|
404
700
|
|
|
701
|
+
function requireActionEntry(state, payload = {}) {
|
|
702
|
+
const actionId = normalizeText(payload.actionId);
|
|
703
|
+
const entry = actionId ? state.actionEntriesById.get(actionId.toLowerCase()) : null;
|
|
704
|
+
const requestedVersion = payload.version == null ? null : Number(payload.version);
|
|
705
|
+
if (
|
|
706
|
+
!entry ||
|
|
707
|
+
(requestedVersion != null && requestedVersion !== Number(entry.descriptor.actionVersion))
|
|
708
|
+
) {
|
|
709
|
+
throw createToolError(404, "assistant_action_unknown", "Action is not available.");
|
|
710
|
+
}
|
|
711
|
+
return entry;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function searchActionEntries(state, payload = {}) {
|
|
715
|
+
const query = normalizeDiscoveryQuery(payload.query);
|
|
716
|
+
const terms = query.split(/\s+/u).filter(Boolean);
|
|
717
|
+
const matches = terms.length < 1
|
|
718
|
+
? state.actionEntries
|
|
719
|
+
: state.actionEntries.filter((entry) => {
|
|
720
|
+
const searchable = [
|
|
721
|
+
entry.descriptor.actionId,
|
|
722
|
+
entry.kind,
|
|
723
|
+
entry.descriptor.description
|
|
724
|
+
].join(" ").toLowerCase();
|
|
725
|
+
return terms.every((term) => searchable.includes(term));
|
|
726
|
+
});
|
|
727
|
+
const offset = decodeDiscoveryCursor(payload.cursor, query);
|
|
728
|
+
const requestedLimit = normalizePositiveInteger(payload.limit, discoveryPageSize);
|
|
729
|
+
const limit = Math.min(MAX_DISCOVERY_PAGE_SIZE, requestedLimit);
|
|
730
|
+
const page = matches.slice(offset, offset + limit);
|
|
731
|
+
const nextOffset = offset + page.length;
|
|
732
|
+
const result = {
|
|
733
|
+
items: page.map((entry) => ({
|
|
734
|
+
actionId: entry.descriptor.actionId,
|
|
735
|
+
version: entry.descriptor.actionVersion,
|
|
736
|
+
kind: entry.kind,
|
|
737
|
+
description: truncateDescription(entry.descriptor.description)
|
|
738
|
+
})),
|
|
739
|
+
nextCursor: nextOffset < matches.length ? encodeDiscoveryCursor(nextOffset, query) : null,
|
|
740
|
+
total: matches.length
|
|
741
|
+
};
|
|
742
|
+
ensureSerializedSize(result, maxToolResultBytes);
|
|
743
|
+
return result;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function resolveActionContract(state, payload = {}) {
|
|
747
|
+
const entry = requireActionEntry(state, payload);
|
|
748
|
+
const contract = {
|
|
749
|
+
actionId: entry.descriptor.actionId,
|
|
750
|
+
version: entry.descriptor.actionVersion,
|
|
751
|
+
kind: entry.kind,
|
|
752
|
+
description: entry.descriptor.description,
|
|
753
|
+
inputSchema: entry.descriptor.parameters,
|
|
754
|
+
outputSchema: entry.descriptor.outputSchema
|
|
755
|
+
};
|
|
756
|
+
ensureSerializedSize(contract, maxToolResultBytes, {
|
|
757
|
+
code: "assistant_tool_contract_too_large",
|
|
758
|
+
label: "Action contract"
|
|
759
|
+
});
|
|
760
|
+
state.contractedActionKeys.add(
|
|
761
|
+
normalizeActionLookupKey(entry.descriptor.actionId, entry.descriptor.actionVersion)
|
|
762
|
+
);
|
|
763
|
+
return contract;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function createActionInput(value, context = {}) {
|
|
767
|
+
const actionInput = value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
|
|
768
|
+
const trustedWorkspaceSlug = resolveWorkspaceSlug(context);
|
|
769
|
+
if (trustedWorkspaceSlug) {
|
|
770
|
+
actionInput.workspaceSlug = trustedWorkspaceSlug;
|
|
771
|
+
}
|
|
772
|
+
return actionInput;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
async function executeActionEntry(entry, input = {}, context = {}) {
|
|
776
|
+
const actionInput = createActionInput(input, context);
|
|
777
|
+
const executionContext = {
|
|
778
|
+
...context,
|
|
779
|
+
channel: AUTOMATION_CHANNEL
|
|
780
|
+
};
|
|
781
|
+
const rawResult = await actions.execute({
|
|
782
|
+
actionId: entry.descriptor.actionId,
|
|
783
|
+
version: entry.descriptor.actionVersion || null,
|
|
784
|
+
input: actionInput,
|
|
785
|
+
context: executionContext
|
|
786
|
+
});
|
|
787
|
+
const transformedResult = entry.transformResult
|
|
788
|
+
? await entry.transformResult(rawResult, Object.freeze({
|
|
789
|
+
actionId: entry.descriptor.actionId,
|
|
790
|
+
version: entry.descriptor.actionVersion,
|
|
791
|
+
input: Object.freeze({ ...actionInput }),
|
|
792
|
+
context: executionContext
|
|
793
|
+
}))
|
|
794
|
+
: rawResult;
|
|
795
|
+
|
|
796
|
+
let result = transformedResult;
|
|
797
|
+
try {
|
|
798
|
+
result = validateSchemaPayload(entry.outputDefinition, transformedResult, {
|
|
799
|
+
phase: "output",
|
|
800
|
+
context: `Assistant tool "${entry.descriptor.actionId}" output`
|
|
801
|
+
});
|
|
802
|
+
} catch {
|
|
803
|
+
throw createToolError(500, "assistant_tool_output_invalid", "Assistant tool output validation failed.");
|
|
804
|
+
}
|
|
805
|
+
ensureSerializedSize(result, maxToolResultBytes);
|
|
806
|
+
return result;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
async function executeDiscoveredAction(state, payload = {}, context = {}) {
|
|
810
|
+
const entry = requireActionEntry(state, payload);
|
|
811
|
+
const actionKey = normalizeActionLookupKey(entry.descriptor.actionId, entry.descriptor.actionVersion);
|
|
812
|
+
if (!state.contractedActionKeys.has(actionKey)) {
|
|
813
|
+
throw createToolError(
|
|
814
|
+
409,
|
|
815
|
+
"assistant_action_contract_required",
|
|
816
|
+
"Load this action's exact contract before executing it."
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
const result = await executeActionEntry(entry, payload.input, context);
|
|
820
|
+
const response = {
|
|
821
|
+
actionId: entry.descriptor.actionId,
|
|
822
|
+
version: entry.descriptor.actionVersion,
|
|
823
|
+
result
|
|
824
|
+
};
|
|
825
|
+
ensureSerializedSize(response, maxToolResultBytes);
|
|
826
|
+
return response;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function resolveToolFailure(error) {
|
|
830
|
+
const status = Number(error?.status || error?.statusCode || 500);
|
|
831
|
+
return {
|
|
832
|
+
ok: false,
|
|
833
|
+
error: {
|
|
834
|
+
code: String(error?.code || "assistant_tool_failed").trim() || "assistant_tool_failed",
|
|
835
|
+
message: status >= 500 ? "Tool call failed." : String(error?.message || "Tool call failed."),
|
|
836
|
+
status: Number.isInteger(status) ? status : 500
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
405
841
|
async function executeToolCall({ toolName = "", argumentsText = "", context = {}, toolSet = null } = {}) {
|
|
406
842
|
const normalizedToolName = normalizeText(toolName);
|
|
407
|
-
const
|
|
843
|
+
const suppliedState = toolSet && typeof toolSet === "object" ? toolSetStates.get(toolSet) : null;
|
|
844
|
+
const resolvedToolSet = suppliedState ? toolSet : resolveToolSet(context);
|
|
845
|
+
const state = suppliedState || toolSetStates.get(resolvedToolSet);
|
|
408
846
|
const descriptor = normalizedToolName ? resolvedToolSet.byName.get(normalizedToolName) : null;
|
|
409
847
|
|
|
410
848
|
if (!descriptor) {
|
|
@@ -418,38 +856,31 @@ function createServiceToolCatalog(
|
|
|
418
856
|
}
|
|
419
857
|
|
|
420
858
|
try {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if (
|
|
425
|
-
|
|
859
|
+
ensureToolArgumentsSize(argumentsText, maxToolArgumentBytes);
|
|
860
|
+
const payload = parseToolPayload(argumentsText);
|
|
861
|
+
if (state.mode === "discovery") {
|
|
862
|
+
if (normalizedToolName === DISCOVERY_TOOL_NAMES.search) {
|
|
863
|
+
return { ok: true, result: searchActionEntries(state, payload) };
|
|
864
|
+
}
|
|
865
|
+
if (normalizedToolName === DISCOVERY_TOOL_NAMES.contract) {
|
|
866
|
+
return { ok: true, result: resolveActionContract(state, payload) };
|
|
867
|
+
}
|
|
868
|
+
if (normalizedToolName === DISCOVERY_TOOL_NAMES.execute) {
|
|
869
|
+
return { ok: true, result: await executeDiscoveredAction(state, payload, context) };
|
|
426
870
|
}
|
|
427
871
|
}
|
|
428
|
-
const executionContext = {
|
|
429
|
-
...context,
|
|
430
|
-
channel: AUTOMATION_CHANNEL
|
|
431
|
-
};
|
|
432
872
|
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
});
|
|
873
|
+
const entry = state.directEntriesByToolName.get(normalizedToolName);
|
|
874
|
+
if (!entry) {
|
|
875
|
+
throw createToolError(404, "assistant_tool_unknown", "Unknown tool.");
|
|
876
|
+
}
|
|
877
|
+
const result = await executeActionEntry(entry, payload, context);
|
|
439
878
|
return {
|
|
440
879
|
ok: true,
|
|
441
880
|
result
|
|
442
881
|
};
|
|
443
882
|
} catch (error) {
|
|
444
|
-
|
|
445
|
-
return {
|
|
446
|
-
ok: false,
|
|
447
|
-
error: {
|
|
448
|
-
code: String(error?.code || "assistant_tool_failed").trim() || "assistant_tool_failed",
|
|
449
|
-
message: status >= 500 ? "Tool call failed." : String(error?.message || "Tool call failed."),
|
|
450
|
-
status: Number.isInteger(status) ? status : 500
|
|
451
|
-
}
|
|
452
|
-
};
|
|
883
|
+
return resolveToolFailure(error);
|
|
453
884
|
}
|
|
454
885
|
}
|
|
455
886
|
|