@jskit-ai/assistant-core 0.1.142 → 0.1.144
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 +531 -80
- package/test/assistantScroll.browser.test.js +139 -0
- package/test/componentContracts.test.js +11 -0
- package/test/serviceToolCatalog.test.js +318 -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,127 @@ 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
|
+
|
|
387
|
+
function resolveValidationFailureMessage(error) {
|
|
388
|
+
const baseMessage = normalizeText(error?.message, { fallback: "Validation failed." });
|
|
389
|
+
if (normalizeText(error?.code).toUpperCase() !== "ACTION_VALIDATION_FAILED") {
|
|
390
|
+
return baseMessage;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const fieldErrors = error?.details?.fieldErrors;
|
|
394
|
+
if (!fieldErrors || typeof fieldErrors !== "object" || Array.isArray(fieldErrors)) {
|
|
395
|
+
return baseMessage;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const details = Object.entries(fieldErrors)
|
|
399
|
+
.map(([field, message]) => [normalizeText(field), truncateDescription(message, 300)])
|
|
400
|
+
.filter(([field, message]) => field && message)
|
|
401
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
402
|
+
.slice(0, 8)
|
|
403
|
+
.map(([field, message]) => `${field}: ${message}`);
|
|
404
|
+
return details.length > 0 ? `${baseMessage} ${details.join(" ")}` : baseMessage;
|
|
405
|
+
}
|
|
406
|
+
|
|
145
407
|
function canInvokeMethod(permission, context) {
|
|
146
408
|
const permissionSpec = normalizePermissionSpec(permission);
|
|
147
409
|
|
|
@@ -243,53 +505,57 @@ function resolveActionBackedToolEntries(actions) {
|
|
|
243
505
|
}
|
|
244
506
|
const entriesByActionId = new Map();
|
|
245
507
|
for (const action of actions.listDefinitions()) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
508
|
+
if (!action || typeof action !== "object") {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (!hasAutomationChannel(action)) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
252
514
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
515
|
+
const actionId = normalizeText(action.id);
|
|
516
|
+
if (!actionId) {
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
257
519
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
520
|
+
let assistantExtension = null;
|
|
521
|
+
try {
|
|
522
|
+
assistantExtension = normalizeAssistantActionExtension(action);
|
|
523
|
+
} catch {
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
264
526
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
527
|
+
const outputDefinition = assistantExtension.output || action.output;
|
|
528
|
+
const inputSchema = resolveStructuredSchemaTransportSchema(action.input, {
|
|
529
|
+
context: `Action definition "${actionId}" input`,
|
|
530
|
+
defaultMode: "patch"
|
|
531
|
+
}) || null;
|
|
532
|
+
const outputSchema = resolveStructuredSchemaTransportSchema(outputDefinition, {
|
|
533
|
+
context: `Action definition "${actionId}" assistant output`,
|
|
534
|
+
defaultMode: "replace"
|
|
535
|
+
}) || null;
|
|
536
|
+
if (!inputSchema || !outputSchema) {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
276
539
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
540
|
+
const actionVersion = Number(action.version) || 1;
|
|
541
|
+
const actionKey = actionId.toLowerCase();
|
|
542
|
+
const nextEntry = Object.freeze({
|
|
543
|
+
actionId,
|
|
544
|
+
actionVersion,
|
|
545
|
+
kind: normalizeText(action.kind).toLowerCase() || "command",
|
|
546
|
+
toolBaseName: actionId,
|
|
547
|
+
description: assistantExtension.description || `Run ${actionId}.`,
|
|
548
|
+
inputSchema,
|
|
549
|
+
outputDefinition,
|
|
550
|
+
outputSchema,
|
|
551
|
+
transformResult: assistantExtension.transformResult,
|
|
552
|
+
permission: normalizePermissionSpec(action.permission),
|
|
553
|
+
surfaces: normalizeSurfaceList(action.surfaces)
|
|
554
|
+
});
|
|
555
|
+
const existing = entriesByActionId.get(actionKey);
|
|
556
|
+
if (!existing || actionVersion >= Number(existing.actionVersion || 0)) {
|
|
557
|
+
entriesByActionId.set(actionKey, nextEntry);
|
|
558
|
+
}
|
|
293
559
|
}
|
|
294
560
|
|
|
295
561
|
return entriesByActionId;
|
|
@@ -331,6 +597,9 @@ function resolveActionToolEntries(
|
|
|
331
597
|
parameters: actionEntry.inputSchema,
|
|
332
598
|
outputSchema: actionEntry.outputSchema
|
|
333
599
|
}),
|
|
600
|
+
kind: actionEntry.kind,
|
|
601
|
+
outputDefinition: actionEntry.outputDefinition,
|
|
602
|
+
transformResult: actionEntry.transformResult,
|
|
334
603
|
permission: actionEntry.permission,
|
|
335
604
|
surfaces: actionEntry.surfaces
|
|
336
605
|
})
|
|
@@ -342,7 +611,14 @@ function resolveActionToolEntries(
|
|
|
342
611
|
|
|
343
612
|
function createServiceToolCatalog(
|
|
344
613
|
actions,
|
|
345
|
-
{
|
|
614
|
+
{
|
|
615
|
+
barredActionIds = [],
|
|
616
|
+
skipActionPrefixes = [],
|
|
617
|
+
maxDirectTools: rawMaxDirectTools = DEFAULT_MAX_DIRECT_TOOLS,
|
|
618
|
+
discoveryPageSize: rawDiscoveryPageSize = DEFAULT_DISCOVERY_PAGE_SIZE,
|
|
619
|
+
maxToolArgumentBytes: rawMaxToolArgumentBytes = DEFAULT_MAX_TOOL_ARGUMENT_BYTES,
|
|
620
|
+
maxToolResultBytes: rawMaxToolResultBytes = DEFAULT_MAX_TOOL_RESULT_BYTES
|
|
621
|
+
} = {}
|
|
346
622
|
) {
|
|
347
623
|
if (!actions || typeof actions.listDefinitions !== "function" || typeof actions.execute !== "function") {
|
|
348
624
|
throw new TypeError("createServiceToolCatalog requires runtime.actions.");
|
|
@@ -351,6 +627,17 @@ function createServiceToolCatalog(
|
|
|
351
627
|
const normalizedSkipPrefixes = (Array.isArray(skipActionPrefixes) ? skipActionPrefixes : [skipActionPrefixes])
|
|
352
628
|
.map((entry) => normalizeText(entry).toLowerCase())
|
|
353
629
|
.filter(Boolean);
|
|
630
|
+
const maxDirectTools = normalizeNonNegativeInteger(rawMaxDirectTools, DEFAULT_MAX_DIRECT_TOOLS);
|
|
631
|
+
const discoveryPageSize = Math.min(
|
|
632
|
+
MAX_DISCOVERY_PAGE_SIZE,
|
|
633
|
+
normalizePositiveInteger(rawDiscoveryPageSize, DEFAULT_DISCOVERY_PAGE_SIZE)
|
|
634
|
+
);
|
|
635
|
+
const maxToolArgumentBytes = normalizePositiveInteger(
|
|
636
|
+
rawMaxToolArgumentBytes,
|
|
637
|
+
DEFAULT_MAX_TOOL_ARGUMENT_BYTES
|
|
638
|
+
);
|
|
639
|
+
const maxToolResultBytes = normalizePositiveInteger(rawMaxToolResultBytes, DEFAULT_MAX_TOOL_RESULT_BYTES);
|
|
640
|
+
const toolSetStates = new WeakMap();
|
|
354
641
|
let methodEntries = null;
|
|
355
642
|
|
|
356
643
|
function resolveOrCreateMethodEntries() {
|
|
@@ -365,9 +652,8 @@ function createServiceToolCatalog(
|
|
|
365
652
|
return methodEntries;
|
|
366
653
|
}
|
|
367
654
|
|
|
368
|
-
function
|
|
369
|
-
const
|
|
370
|
-
const byName = new Map();
|
|
655
|
+
function resolveAuthorizedEntries(context = {}) {
|
|
656
|
+
const entries = [];
|
|
371
657
|
for (const entry of resolveOrCreateMethodEntries()) {
|
|
372
658
|
if (!canUseToolOnSurface(entry, context)) {
|
|
373
659
|
continue;
|
|
@@ -376,19 +662,49 @@ function createServiceToolCatalog(
|
|
|
376
662
|
continue;
|
|
377
663
|
}
|
|
378
664
|
|
|
379
|
-
|
|
380
|
-
...entry
|
|
381
|
-
|
|
382
|
-
|
|
665
|
+
entries.push(Object.freeze({
|
|
666
|
+
...entry,
|
|
667
|
+
descriptor: Object.freeze({
|
|
668
|
+
...entry.descriptor,
|
|
669
|
+
parameters: stripWorkspaceSlugFromSchema(entry.descriptor.parameters, context)
|
|
670
|
+
})
|
|
671
|
+
}));
|
|
672
|
+
}
|
|
383
673
|
|
|
384
|
-
|
|
674
|
+
return Object.freeze(entries.sort((left, right) => left.descriptor.actionId.localeCompare(right.descriptor.actionId)));
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function resolveToolSet(context = {}) {
|
|
678
|
+
const actionEntries = resolveAuthorizedEntries(context);
|
|
679
|
+
const useDiscovery = actionEntries.length > maxDirectTools;
|
|
680
|
+
const tools = useDiscovery
|
|
681
|
+
? DISCOVERY_TOOL_DESCRIPTORS.slice()
|
|
682
|
+
: actionEntries.map((entry) => entry.descriptor);
|
|
683
|
+
const byName = new Map();
|
|
684
|
+
for (const descriptor of tools) {
|
|
385
685
|
byName.set(descriptor.name, descriptor);
|
|
386
686
|
}
|
|
387
687
|
|
|
388
|
-
|
|
688
|
+
const toolSet = Object.freeze({
|
|
389
689
|
tools: Object.freeze(tools),
|
|
390
690
|
byName
|
|
391
691
|
});
|
|
692
|
+
const actionEntriesById = new Map();
|
|
693
|
+
const directEntriesByToolName = new Map();
|
|
694
|
+
for (const entry of actionEntries) {
|
|
695
|
+
actionEntriesById.set(entry.descriptor.actionId.toLowerCase(), entry);
|
|
696
|
+
if (!useDiscovery) {
|
|
697
|
+
directEntriesByToolName.set(entry.descriptor.name, entry);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
toolSetStates.set(toolSet, {
|
|
701
|
+
mode: useDiscovery ? "discovery" : "direct",
|
|
702
|
+
actionEntries,
|
|
703
|
+
actionEntriesById,
|
|
704
|
+
directEntriesByToolName,
|
|
705
|
+
contractedActionKeys: new Set()
|
|
706
|
+
});
|
|
707
|
+
return toolSet;
|
|
392
708
|
}
|
|
393
709
|
|
|
394
710
|
function toOpenAiToolSchema(tool) {
|
|
@@ -402,9 +718,151 @@ function createServiceToolCatalog(
|
|
|
402
718
|
};
|
|
403
719
|
}
|
|
404
720
|
|
|
721
|
+
function requireActionEntry(state, payload = {}) {
|
|
722
|
+
const actionId = normalizeText(payload.actionId);
|
|
723
|
+
const entry = actionId ? state.actionEntriesById.get(actionId.toLowerCase()) : null;
|
|
724
|
+
const requestedVersion = payload.version == null ? null : Number(payload.version);
|
|
725
|
+
if (
|
|
726
|
+
!entry ||
|
|
727
|
+
(requestedVersion != null && requestedVersion !== Number(entry.descriptor.actionVersion))
|
|
728
|
+
) {
|
|
729
|
+
throw createToolError(404, "assistant_action_unknown", "Action is not available.");
|
|
730
|
+
}
|
|
731
|
+
return entry;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function searchActionEntries(state, payload = {}) {
|
|
735
|
+
const query = normalizeDiscoveryQuery(payload.query);
|
|
736
|
+
const terms = query.split(/\s+/u).filter(Boolean);
|
|
737
|
+
const matches = terms.length < 1
|
|
738
|
+
? state.actionEntries
|
|
739
|
+
: state.actionEntries.filter((entry) => {
|
|
740
|
+
const searchable = [
|
|
741
|
+
entry.descriptor.actionId,
|
|
742
|
+
entry.kind,
|
|
743
|
+
entry.descriptor.description
|
|
744
|
+
].join(" ").toLowerCase();
|
|
745
|
+
return terms.every((term) => searchable.includes(term));
|
|
746
|
+
});
|
|
747
|
+
const offset = decodeDiscoveryCursor(payload.cursor, query);
|
|
748
|
+
const requestedLimit = normalizePositiveInteger(payload.limit, discoveryPageSize);
|
|
749
|
+
const limit = Math.min(MAX_DISCOVERY_PAGE_SIZE, requestedLimit);
|
|
750
|
+
const page = matches.slice(offset, offset + limit);
|
|
751
|
+
const nextOffset = offset + page.length;
|
|
752
|
+
const result = {
|
|
753
|
+
items: page.map((entry) => ({
|
|
754
|
+
actionId: entry.descriptor.actionId,
|
|
755
|
+
version: entry.descriptor.actionVersion,
|
|
756
|
+
kind: entry.kind,
|
|
757
|
+
description: truncateDescription(entry.descriptor.description)
|
|
758
|
+
})),
|
|
759
|
+
nextCursor: nextOffset < matches.length ? encodeDiscoveryCursor(nextOffset, query) : null,
|
|
760
|
+
total: matches.length
|
|
761
|
+
};
|
|
762
|
+
ensureSerializedSize(result, maxToolResultBytes);
|
|
763
|
+
return result;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function resolveActionContract(state, payload = {}) {
|
|
767
|
+
const entry = requireActionEntry(state, payload);
|
|
768
|
+
const contract = {
|
|
769
|
+
actionId: entry.descriptor.actionId,
|
|
770
|
+
version: entry.descriptor.actionVersion,
|
|
771
|
+
kind: entry.kind,
|
|
772
|
+
description: entry.descriptor.description,
|
|
773
|
+
inputSchema: entry.descriptor.parameters,
|
|
774
|
+
outputSchema: entry.descriptor.outputSchema
|
|
775
|
+
};
|
|
776
|
+
ensureSerializedSize(contract, maxToolResultBytes, {
|
|
777
|
+
code: "assistant_tool_contract_too_large",
|
|
778
|
+
label: "Action contract"
|
|
779
|
+
});
|
|
780
|
+
state.contractedActionKeys.add(
|
|
781
|
+
normalizeActionLookupKey(entry.descriptor.actionId, entry.descriptor.actionVersion)
|
|
782
|
+
);
|
|
783
|
+
return contract;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function createActionInput(value, context = {}) {
|
|
787
|
+
const actionInput = value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
|
|
788
|
+
const trustedWorkspaceSlug = resolveWorkspaceSlug(context);
|
|
789
|
+
if (trustedWorkspaceSlug) {
|
|
790
|
+
actionInput.workspaceSlug = trustedWorkspaceSlug;
|
|
791
|
+
}
|
|
792
|
+
return actionInput;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async function executeActionEntry(entry, input = {}, context = {}) {
|
|
796
|
+
const actionInput = createActionInput(input, context);
|
|
797
|
+
const executionContext = {
|
|
798
|
+
...context,
|
|
799
|
+
channel: AUTOMATION_CHANNEL
|
|
800
|
+
};
|
|
801
|
+
const rawResult = await actions.execute({
|
|
802
|
+
actionId: entry.descriptor.actionId,
|
|
803
|
+
version: entry.descriptor.actionVersion || null,
|
|
804
|
+
input: actionInput,
|
|
805
|
+
context: executionContext
|
|
806
|
+
});
|
|
807
|
+
const transformedResult = entry.transformResult
|
|
808
|
+
? await entry.transformResult(rawResult, Object.freeze({
|
|
809
|
+
actionId: entry.descriptor.actionId,
|
|
810
|
+
version: entry.descriptor.actionVersion,
|
|
811
|
+
input: Object.freeze({ ...actionInput }),
|
|
812
|
+
context: executionContext
|
|
813
|
+
}))
|
|
814
|
+
: rawResult;
|
|
815
|
+
|
|
816
|
+
let result = transformedResult;
|
|
817
|
+
try {
|
|
818
|
+
result = validateSchemaPayload(entry.outputDefinition, transformedResult, {
|
|
819
|
+
phase: "output",
|
|
820
|
+
context: `Assistant tool "${entry.descriptor.actionId}" output`
|
|
821
|
+
});
|
|
822
|
+
} catch {
|
|
823
|
+
throw createToolError(500, "assistant_tool_output_invalid", "Assistant tool output validation failed.");
|
|
824
|
+
}
|
|
825
|
+
ensureSerializedSize(result, maxToolResultBytes);
|
|
826
|
+
return result;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
async function executeDiscoveredAction(state, payload = {}, context = {}) {
|
|
830
|
+
const entry = requireActionEntry(state, payload);
|
|
831
|
+
const actionKey = normalizeActionLookupKey(entry.descriptor.actionId, entry.descriptor.actionVersion);
|
|
832
|
+
if (!state.contractedActionKeys.has(actionKey)) {
|
|
833
|
+
throw createToolError(
|
|
834
|
+
409,
|
|
835
|
+
"assistant_action_contract_required",
|
|
836
|
+
"Load this action's exact contract before executing it."
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
const result = await executeActionEntry(entry, payload.input, context);
|
|
840
|
+
const response = {
|
|
841
|
+
actionId: entry.descriptor.actionId,
|
|
842
|
+
version: entry.descriptor.actionVersion,
|
|
843
|
+
result
|
|
844
|
+
};
|
|
845
|
+
ensureSerializedSize(response, maxToolResultBytes);
|
|
846
|
+
return response;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function resolveToolFailure(error) {
|
|
850
|
+
const status = Number(error?.status || error?.statusCode || 500);
|
|
851
|
+
return {
|
|
852
|
+
ok: false,
|
|
853
|
+
error: {
|
|
854
|
+
code: String(error?.code || "assistant_tool_failed").trim() || "assistant_tool_failed",
|
|
855
|
+
message: status >= 500 ? "Tool call failed." : resolveValidationFailureMessage(error),
|
|
856
|
+
status: Number.isInteger(status) ? status : 500
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
405
861
|
async function executeToolCall({ toolName = "", argumentsText = "", context = {}, toolSet = null } = {}) {
|
|
406
862
|
const normalizedToolName = normalizeText(toolName);
|
|
407
|
-
const
|
|
863
|
+
const suppliedState = toolSet && typeof toolSet === "object" ? toolSetStates.get(toolSet) : null;
|
|
864
|
+
const resolvedToolSet = suppliedState ? toolSet : resolveToolSet(context);
|
|
865
|
+
const state = suppliedState || toolSetStates.get(resolvedToolSet);
|
|
408
866
|
const descriptor = normalizedToolName ? resolvedToolSet.byName.get(normalizedToolName) : null;
|
|
409
867
|
|
|
410
868
|
if (!descriptor) {
|
|
@@ -418,38 +876,31 @@ function createServiceToolCatalog(
|
|
|
418
876
|
}
|
|
419
877
|
|
|
420
878
|
try {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if (
|
|
425
|
-
|
|
879
|
+
ensureToolArgumentsSize(argumentsText, maxToolArgumentBytes);
|
|
880
|
+
const payload = parseToolPayload(argumentsText);
|
|
881
|
+
if (state.mode === "discovery") {
|
|
882
|
+
if (normalizedToolName === DISCOVERY_TOOL_NAMES.search) {
|
|
883
|
+
return { ok: true, result: searchActionEntries(state, payload) };
|
|
884
|
+
}
|
|
885
|
+
if (normalizedToolName === DISCOVERY_TOOL_NAMES.contract) {
|
|
886
|
+
return { ok: true, result: resolveActionContract(state, payload) };
|
|
887
|
+
}
|
|
888
|
+
if (normalizedToolName === DISCOVERY_TOOL_NAMES.execute) {
|
|
889
|
+
return { ok: true, result: await executeDiscoveredAction(state, payload, context) };
|
|
426
890
|
}
|
|
427
891
|
}
|
|
428
|
-
const executionContext = {
|
|
429
|
-
...context,
|
|
430
|
-
channel: AUTOMATION_CHANNEL
|
|
431
|
-
};
|
|
432
892
|
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
});
|
|
893
|
+
const entry = state.directEntriesByToolName.get(normalizedToolName);
|
|
894
|
+
if (!entry) {
|
|
895
|
+
throw createToolError(404, "assistant_tool_unknown", "Unknown tool.");
|
|
896
|
+
}
|
|
897
|
+
const result = await executeActionEntry(entry, payload, context);
|
|
439
898
|
return {
|
|
440
899
|
ok: true,
|
|
441
900
|
result
|
|
442
901
|
};
|
|
443
902
|
} 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
|
-
};
|
|
903
|
+
return resolveToolFailure(error);
|
|
453
904
|
}
|
|
454
905
|
}
|
|
455
906
|
|