@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.
@@ -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 { resolveStructuredSchemaTransportSchema } from "@jskit-ai/kernel/shared/validators";
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
- if (!action || typeof action !== "object") {
247
- continue;
248
- }
249
- if (!hasAutomationChannel(action)) {
250
- continue;
251
- }
508
+ if (!action || typeof action !== "object") {
509
+ continue;
510
+ }
511
+ if (!hasAutomationChannel(action)) {
512
+ continue;
513
+ }
252
514
 
253
- const actionId = normalizeText(action.id);
254
- if (!actionId) {
255
- continue;
256
- }
515
+ const actionId = normalizeText(action.id);
516
+ if (!actionId) {
517
+ continue;
518
+ }
257
519
 
258
- let assistantExtension = null;
259
- try {
260
- assistantExtension = normalizeAssistantActionExtension(action);
261
- } catch {
262
- continue;
263
- }
520
+ let assistantExtension = null;
521
+ try {
522
+ assistantExtension = normalizeAssistantActionExtension(action);
523
+ } catch {
524
+ continue;
525
+ }
264
526
 
265
- const inputSchema = resolveStructuredSchemaTransportSchema(action.input, {
266
- context: `Action definition "${actionId}" input`,
267
- defaultMode: "patch"
268
- }) || null;
269
- const outputSchema = resolveStructuredSchemaTransportSchema(action.output, {
270
- context: `Action definition "${actionId}" output`,
271
- defaultMode: "replace"
272
- }) || null;
273
- if (!inputSchema || !outputSchema) {
274
- continue;
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
- const actionVersion = Number(action.version) || 1;
278
- const actionKey = actionId.toLowerCase();
279
- const nextEntry = Object.freeze({
280
- actionId,
281
- actionVersion,
282
- toolBaseName: actionId,
283
- description: assistantExtension.description || `Run ${actionId}.`,
284
- inputSchema,
285
- outputSchema,
286
- permission: normalizePermissionSpec(action.permission),
287
- surfaces: normalizeSurfaceList(action.surfaces)
288
- });
289
- const existing = entriesByActionId.get(actionKey);
290
- if (!existing || actionVersion >= Number(existing.actionVersion || 0)) {
291
- entriesByActionId.set(actionKey, nextEntry);
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
- { barredActionIds = [], skipActionPrefixes = [] } = {}
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 resolveToolSet(context = {}) {
369
- const tools = [];
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
- const descriptor = Object.freeze({
380
- ...entry.descriptor,
381
- parameters: stripWorkspaceSlugFromSchema(entry.descriptor.parameters, context)
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
- tools.push(descriptor);
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
- return Object.freeze({
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 resolvedToolSet = toolSet && typeof toolSet === "object" ? toolSet : resolveToolSet(context);
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
- const actionInput = parseToolPayload(argumentsText);
422
- if (actionInput && typeof actionInput === "object" && !Array.isArray(actionInput)) {
423
- const workspaceSlug = resolveWorkspaceSlug(context, actionInput);
424
- if (workspaceSlug && !Object.hasOwn(actionInput, "workspaceSlug")) {
425
- actionInput.workspaceSlug = workspaceSlug;
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 result = await actions.execute({
434
- actionId: descriptor.actionId,
435
- version: descriptor.actionVersion || null,
436
- input: actionInput,
437
- context: executionContext
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
- const status = Number(error?.status || error?.statusCode || 500);
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