@jskit-ai/assistant-core 0.1.141 → 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.
@@ -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,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
- if (!action || typeof action !== "object") {
247
- continue;
248
- }
249
- if (!hasAutomationChannel(action)) {
250
- continue;
251
- }
488
+ if (!action || typeof action !== "object") {
489
+ continue;
490
+ }
491
+ if (!hasAutomationChannel(action)) {
492
+ continue;
493
+ }
252
494
 
253
- const actionId = normalizeText(action.id);
254
- if (!actionId) {
255
- continue;
256
- }
495
+ const actionId = normalizeText(action.id);
496
+ if (!actionId) {
497
+ continue;
498
+ }
257
499
 
258
- let assistantExtension = null;
259
- try {
260
- assistantExtension = normalizeAssistantActionExtension(action);
261
- } catch {
262
- continue;
263
- }
500
+ let assistantExtension = null;
501
+ try {
502
+ assistantExtension = normalizeAssistantActionExtension(action);
503
+ } catch {
504
+ continue;
505
+ }
264
506
 
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
- }
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
- 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
- }
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
- { barredActionIds = [], skipActionPrefixes = [] } = {}
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 resolveToolSet(context = {}) {
369
- const tools = [];
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
- const descriptor = Object.freeze({
380
- ...entry.descriptor,
381
- parameters: stripWorkspaceSlugFromSchema(entry.descriptor.parameters, context)
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
- tools.push(descriptor);
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
- return Object.freeze({
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 resolvedToolSet = toolSet && typeof toolSet === "object" ? toolSet : resolveToolSet(context);
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
- 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;
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 result = await actions.execute({
434
- actionId: descriptor.actionId,
435
- version: descriptor.actionVersion || null,
436
- input: actionInput,
437
- context: executionContext
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
- 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
- };
883
+ return resolveToolFailure(error);
453
884
  }
454
885
  }
455
886