@fevex/core 0.1.0-alpha.8 → 0.1.0-alpha.9

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/dist/index.mjs CHANGED
@@ -4,8 +4,8 @@ import { n as RunPausedError, t as FevexRunError } from "./run-error-Cr17wPGH.mj
4
4
  import { ChannelError, deliverChannelRun, handleChannelInput } from "./channels/index.mjs";
5
5
  import { InMemoryMemoryStore, createAgentsMdContextProvider, createMemoryContextProvider, defineContextProvider, defineSkill, isProgressiveSkillProvider, loadSkillFile, loadSkillFiles } from "./knowledge/index.mjs";
6
6
  import { n as teamAsWorkflow, t as defineTeam } from "./teams-Ctz676G4.mjs";
7
- import { InMemoryRunStore, createDigestContextCompactor, estimateContextTokens, isDurableRunStore, isRetainableRunStore, resolveContextBudget, validateListEventsOptions, validatePurgeRunsOptions } from "./runtime/index.mjs";
8
- import { FEVEX_JSON_SCHEMA_PROFILE_VERSION, IntegrationError, compileFevexJsonSchema, defineConnection, defineTool, toToolSpec, validateFevexJsonSchemaProfile } from "./tools/index.mjs";
7
+ import { InMemoryRunStore, createDigestContextCompactor, createModelContextCompactor, estimateContextTokens, isDurableRunStore, isRetainableRunStore, resolveContextBudget, validateListEventsOptions, validatePurgeRunsOptions } from "./runtime/index.mjs";
8
+ import { FEVEX_JSON_SCHEMA_PROFILE_VERSION, InMemoryToolEmbeddingCache, IntegrationError, buildToolEmbeddingIndex, compileFevexJsonSchema, cosineSimilarity, defineConnection, defineTool, schemaKeywords, searchToolEmbeddingIndex, toToolSpec, toolEmbeddingKey, toolEmbeddingText, validateFevexJsonSchemaProfile } from "./tools/index.mjs";
9
9
  import { _ as serializeValue, a as assertTokenBudget, c as eventUsage, d as remainingDeadlineMs, f as remainingOutputTokens, g as serializeJsonValue, h as toErrorMessage, i as assertContinuationBudget, l as isTimeoutError, m as runSignal, n as abortable, o as cancellationReason, p as resumeDeadlineSignal, r as addUsage, s as deadlineAtFromMs, u as minDeadline, v as toJsonValue } from "./run-support-CmZ6Xo6H.mjs";
10
10
  import { t as readModelStream } from "./model-stream-JQdHDDcj.mjs";
11
11
  import "./models/index.mjs";
@@ -67,6 +67,139 @@ async function validateSchema(schema, value, errorMessage) {
67
67
  return result.value;
68
68
  }
69
69
  //#endregion
70
+ //#region src/internal/tool-search.ts
71
+ function resolveToolSearchOptions(options, label, defaultCache) {
72
+ if (options !== void 0 && (typeof options !== "object" || options === null || Array.isArray(options))) throw new TypeError(`${label} must be an object`);
73
+ const mode = options?.mode ?? "auto";
74
+ if (mode !== "auto" && mode !== "on" && mode !== "off") throw new TypeError(`${label}.mode must be auto, on or off`);
75
+ const threshold = options?.threshold ?? 30;
76
+ if (!Number.isSafeInteger(threshold) || threshold < 1) throw new TypeError(`${label}.threshold must be a positive safe integer`);
77
+ const tokenThreshold = options?.tokenThreshold;
78
+ if (tokenThreshold !== void 0 && (!Number.isSafeInteger(tokenThreshold) || tokenThreshold < 1)) throw new TypeError(`${label}.tokenThreshold must be a positive safe integer`);
79
+ const limit = options?.limit ?? 5;
80
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 20) throw new TypeError(`${label}.limit must be a safe integer between 1 and 20`);
81
+ const pinned = options?.pinned ?? [];
82
+ if (!Array.isArray(pinned) || pinned.some((name) => typeof name !== "string" || !name.trim())) throw new TypeError(`${label}.pinned must contain non-empty tool names`);
83
+ const normalizedPinned = [...new Set(pinned.map((name) => name.trim()))];
84
+ if (normalizedPinned.length !== pinned.length) throw new TypeError(`${label}.pinned must contain unique tool names`);
85
+ const semantic = options?.semantic;
86
+ return {
87
+ mode,
88
+ threshold,
89
+ ...tokenThreshold === void 0 ? {} : { tokenThreshold },
90
+ limit,
91
+ pinned: normalizedPinned,
92
+ ...semantic === void 0 ? {} : { semantic: resolveSemanticOptions(semantic, `${label}.semantic`, defaultCache) }
93
+ };
94
+ }
95
+ function serializeToolSearchOptions(options) {
96
+ return {
97
+ mode: options.mode,
98
+ threshold: options.threshold,
99
+ ...options.tokenThreshold === void 0 ? {} : { tokenThreshold: options.tokenThreshold },
100
+ limit: options.limit,
101
+ pinned: [...options.pinned]
102
+ };
103
+ }
104
+ function reciprocalRankFusionCandidates(lexical, semantic, limit, rankConstant = 60) {
105
+ if (!Number.isSafeInteger(limit) || limit < 1) throw new TypeError("Tool search fusion limit must be a positive safe integer");
106
+ if (!Number.isSafeInteger(rankConstant) || rankConstant < 1) throw new TypeError("Tool search fusion rankConstant must be a positive safe integer");
107
+ const scores = /* @__PURE__ */ new Map();
108
+ const add = (tools) => {
109
+ for (let index = 0; index < tools.length; index += 1) {
110
+ const tool = tools[index];
111
+ const entry = scores.get(tool.name) ?? {
112
+ tool,
113
+ score: 0
114
+ };
115
+ entry.score += 1 / (rankConstant + index + 1);
116
+ scores.set(tool.name, entry);
117
+ }
118
+ };
119
+ add(lexical);
120
+ add(semantic);
121
+ return [...scores.values()].sort((left, right) => right.score - left.score || left.tool.name.localeCompare(right.tool.name)).slice(0, limit);
122
+ }
123
+ function resolveSemanticOptions(semantic, label, defaultCache) {
124
+ if (typeof semantic !== "object" || semantic === null || Array.isArray(semantic)) throw new TypeError(`${label} must be an object`);
125
+ const embeddings = semantic.embeddings;
126
+ if (typeof embeddings !== "object" || embeddings === null || typeof embeddings.id !== "string" || !embeddings.id.trim() || typeof embeddings.embed !== "function") throw new TypeError(`${label}.embeddings must implement EmbeddingModel`);
127
+ if (embeddings.dimensions !== void 0 && (!Number.isSafeInteger(embeddings.dimensions) || embeddings.dimensions < 1)) throw new TypeError(`${label}.embeddings.dimensions must be a positive safe integer`);
128
+ const cache = semantic.cache ?? defaultCache;
129
+ if (cache === void 0 || typeof cache.get !== "function" || typeof cache.set !== "function") throw new TypeError(`${label}.cache must implement get and set`);
130
+ const topK = semantic.topK ?? 20;
131
+ if (!Number.isSafeInteger(topK) || topK < 1) throw new TypeError(`${label}.topK must be a positive safe integer`);
132
+ const minScore = semantic.minScore ?? 0;
133
+ if (typeof minScore !== "number" || !Number.isFinite(minScore) || minScore < -1 || minScore > 1) throw new TypeError(`${label}.minScore must be a finite number between -1 and 1`);
134
+ const reranker = semantic.reranker;
135
+ if (reranker !== void 0 && (typeof reranker !== "object" || reranker === null || typeof reranker.rerank !== "function")) throw new TypeError(`${label}.reranker must implement rerank`);
136
+ return {
137
+ embeddings,
138
+ cache,
139
+ topK,
140
+ minScore,
141
+ ...reranker === void 0 ? {} : { reranker }
142
+ };
143
+ }
144
+ function shouldUseToolSearch(options, tools) {
145
+ if (options.mode === "off" || tools.length === 0) return false;
146
+ if (options.mode === "on") return true;
147
+ if (tools.length > options.threshold) return true;
148
+ if (options.tokenThreshold === void 0) return false;
149
+ return Math.ceil(JSON.stringify(tools).length / 4) > options.tokenThreshold;
150
+ }
151
+ const searchIndex = /* @__PURE__ */ new WeakMap();
152
+ function indexedTools(tools) {
153
+ const cached = searchIndex.get(tools);
154
+ if (cached !== void 0) return cached;
155
+ const index = tools.map((tool) => {
156
+ const name = tool.name.toLowerCase();
157
+ const description = (tool.description ?? "").toLowerCase();
158
+ return {
159
+ tool,
160
+ name,
161
+ nameTerms: new Set(name.split(/[^a-z0-9]+/).filter(Boolean)),
162
+ description,
163
+ descriptionTerms: new Set(description.split(/[^a-z0-9]+/).filter(Boolean)),
164
+ schema: JSON.stringify(tool.inputSchema ?? {}).toLowerCase()
165
+ };
166
+ });
167
+ searchIndex.set(tools, index);
168
+ return index;
169
+ }
170
+ function searchToolSpecs(tools, query, limit) {
171
+ const terms = [...new Set(query.toLowerCase().split(/[^a-z0-9]+/).filter((term) => term.length > 1))];
172
+ if (terms.length === 0) return [];
173
+ return indexedTools(tools).map(({ tool, name, nameTerms, description, descriptionTerms, schema }) => {
174
+ let score = 0;
175
+ for (const term of terms) {
176
+ if (name === term) score += 100;
177
+ else if (nameTerms.has(term)) score += 40;
178
+ else if (name.startsWith(term)) score += 30;
179
+ else if (name.includes(term)) score += 20;
180
+ if (descriptionTerms.has(term)) score += 8;
181
+ else if (description.includes(term)) score += 5;
182
+ if (schema.includes(term)) score += 1;
183
+ }
184
+ return {
185
+ tool,
186
+ score
187
+ };
188
+ }).filter(({ score }) => score > 0).sort((left, right) => right.score - left.score || left.tool.name.localeCompare(right.tool.name)).slice(0, limit).map(({ tool }) => tool);
189
+ }
190
+ function readToolSearchInput(input) {
191
+ if (typeof input !== "object" || input === null || Array.isArray(input)) throw new TypeError("Tool search input must be an object");
192
+ const record = input;
193
+ const query = record.query;
194
+ const limit = record.limit;
195
+ if (typeof query !== "string" || !query.trim()) throw new TypeError("Tool search query must be a non-empty string");
196
+ if (limit !== void 0 && (typeof limit !== "number" || !Number.isSafeInteger(limit) || limit < 1)) throw new TypeError("Tool search limit must be a positive safe integer");
197
+ return {
198
+ query: query.trim(),
199
+ ...limit === void 0 ? {} : { limit }
200
+ };
201
+ }
202
+ //#endregion
70
203
  //#region src/internal/configuration.ts
71
204
  const REASONING_EFFORTS = /* @__PURE__ */ new Set([
72
205
  "provider-default",
@@ -190,6 +323,7 @@ function expandConnection(connection) {
190
323
  };
191
324
  assertConfiguration(policy?.inputSchema === void 0 || isJsonObject(policy.inputSchema), "INVALID_TOOL", `Input schema for connection tool "${remoteName}" must be a JSON object`);
192
325
  assertConfiguration(policy?.outputSchema === void 0 || isJsonObject(policy.outputSchema), "INVALID_TOOL", `Output schema for connection tool "${remoteName}" must be a JSON object`);
326
+ assertConfiguration(policy?.maxResultBytes === void 0 || policy.maxResultBytes === false || Number.isSafeInteger(policy.maxResultBytes) && policy.maxResultBytes > 0, "INVALID_TOOL", `maxResultBytes for connection tool "${remoteName}" must be a positive safe integer or false`);
193
327
  return {
194
328
  name: namespacedToolName(connection.name, remoteName),
195
329
  ...policy?.label === void 0 ? {} : { label: policy.label },
@@ -201,6 +335,7 @@ function expandConnection(connection) {
201
335
  ...policy?.idempotency === void 0 ? {} : { idempotency: policy.idempotency },
202
336
  ...policy?.retry === void 0 ? {} : { retry: policy.retry },
203
337
  ...policy?.credentials === void 0 ? {} : { credentials: policy.credentials },
338
+ ...policy?.maxResultBytes === void 0 ? {} : { maxResultBytes: policy.maxResultBytes },
204
339
  source,
205
340
  async resolve(context) {
206
341
  try {
@@ -305,10 +440,18 @@ function createComposition(config) {
305
440
  } catch (error) {
306
441
  throw configurationError("INVALID_CONFIG", error.message);
307
442
  }
443
+ assertConfiguration(config.toolSearchCache === void 0 || isRecord(config.toolSearchCache) && typeof config.toolSearchCache.get === "function" && typeof config.toolSearchCache.set === "function", "INVALID_CONFIG", "Fevex config \"toolSearchCache\" must implement get and set");
444
+ const defaultToolSearchCache = config.toolSearchCache ?? new InMemoryToolEmbeddingCache();
445
+ if (config.toolSearch !== void 0) try {
446
+ resolveToolSearchOptions(config.toolSearch, "Fevex config \"toolSearch\"", defaultToolSearchCache);
447
+ } catch (error) {
448
+ throw configurationError("INVALID_CONFIG", error.message);
449
+ }
308
450
  if (config.compaction !== void 0) {
309
451
  assertConfiguration(typeof config.compaction === "object" && config.compaction !== null && !Array.isArray(config.compaction), "INVALID_CONFIG", "Fevex config \"compaction\" must be an object");
310
452
  assertConfiguration(config.compaction.compactor === void 0 || isRecord(config.compaction.compactor) && typeof config.compaction.compactor.compact === "function", "INVALID_CONFIG", "Fevex config \"compaction.compactor\" must implement compact");
311
453
  assertConfiguration(config.compaction.estimateTokens === void 0 || typeof config.compaction.estimateTokens === "function", "INVALID_CONFIG", "Fevex config \"compaction.estimateTokens\" must be a function");
454
+ assertConfiguration(config.compaction.redactSummary === void 0 || typeof config.compaction.redactSummary === "function", "INVALID_CONFIG", "Fevex config \"compaction.redactSummary\" must be a function");
312
455
  }
313
456
  assertConfiguration(config.memoryStore === void 0 || isMemoryStore(config.memoryStore), "INVALID_CONFIG", "Fevex config \"memoryStore\" must implement MemoryStore");
314
457
  assertConfiguration(config.onEvent === void 0 || typeof config.onEvent === "function", "INVALID_CONFIG", "Fevex config \"onEvent\" must be a function");
@@ -395,6 +538,7 @@ function createComposition(config) {
395
538
  assertConfiguration(definition.credentials === void 0 || Array.isArray(definition.credentials) && definition.credentials.every((value) => typeof value === "string" && value.trim()), "INVALID_TOOL", `Tool "${name}" credentials must be non-empty strings`);
396
539
  assertSandboxCapabilities(definition.sandbox, name);
397
540
  assertConfiguration(definition.sandbox === void 0 || config.sandbox !== void 0, "INVALID_TOOL", `Tool "${name}" requires Fevex config "sandbox"`);
541
+ assertConfiguration(definition.maxResultBytes === void 0 || definition.maxResultBytes === false || Number.isSafeInteger(definition.maxResultBytes) && definition.maxResultBytes > 0, "INVALID_TOOL", `Tool "${name}" maxResultBytes must be a positive safe integer or false`);
398
542
  if (definition.retry !== void 0) assertConfiguration(isRecord(definition.retry) && Number.isInteger(definition.retry.maxAttempts) && definition.retry.maxAttempts >= 1 && Number.isInteger(definition.retry.backoffMs) && definition.retry.backoffMs >= 0 && (definition.retry.maxBackoffMs === void 0 || Number.isInteger(definition.retry.maxBackoffMs) && definition.retry.maxBackoffMs >= 0), "INVALID_TOOL", `Tool "${name}" retry is invalid`);
399
543
  if (definition.inputSchema !== void 0) assertConfiguration(isStandardSchema(definition.inputSchema), "INVALID_TOOL", `Input schema for tool "${name}" must implement Standard Schema`);
400
544
  if (definition.inputSchema !== void 0) assertConfiguration(isStandardSchema(definition.inputSchema), "INVALID_AGENT", `Input schema for agent "${name}" must implement Standard Schema`);
@@ -414,6 +558,7 @@ function createComposition(config) {
414
558
  ...definition.idempotency === void 0 ? {} : { idempotency: definition.idempotency },
415
559
  ...definition.retry === void 0 ? {} : { retry: { ...definition.retry } },
416
560
  ...definition.credentials === void 0 ? {} : { credentials: [...definition.credentials] },
561
+ ...definition.maxResultBytes === void 0 ? {} : { maxResultBytes: definition.maxResultBytes },
417
562
  ...definition.sandbox === void 0 ? {} : { sandbox: copySandboxCapabilities(definition.sandbox) },
418
563
  ...definition.source === void 0 ? {} : { source: { ...definition.source } },
419
564
  ...definition.resolve === void 0 ? {} : { resolve: definition.resolve },
@@ -490,12 +635,18 @@ function createComposition(config) {
490
635
  } catch (error) {
491
636
  throw configurationError("INVALID_AGENT", error.message);
492
637
  }
638
+ if (definition.toolSearch !== void 0) try {
639
+ resolveToolSearchOptions(definition.toolSearch, `Agent "${name}" toolSearch`, defaultToolSearchCache);
640
+ } catch (error) {
641
+ throw configurationError("INVALID_AGENT", error.message);
642
+ }
493
643
  if (definition.outputSchema !== void 0) assertConfiguration(isStandardSchema(definition.outputSchema), "INVALID_AGENT", `Output schema for agent "${name}" must implement Standard Schema`);
494
644
  const rawLimits = definition.limits;
495
645
  assertLimit(rawLimits?.maxSteps, "maxSteps", 1, name);
496
646
  assertLimit(rawLimits?.maxToolCalls, "maxToolCalls", 0, name);
497
647
  assertLimit(rawLimits?.maxInputTokens, "maxInputTokens", 1, name, true);
498
648
  assertLimit(rawLimits?.maxOutputTokens, "maxOutputTokens", 1, name, true);
649
+ assertLimit(rawLimits?.maxToolResultBytes, "maxToolResultBytes", 1, name, true);
499
650
  agents.set(name, {
500
651
  name,
501
652
  instructions: definition.instructions,
@@ -514,13 +665,18 @@ function createComposition(config) {
514
665
  ...definition.elicitation === void 0 ? {} : { elicitation: definition.elicitation },
515
666
  ...definition.approvalMode === void 0 ? {} : { approvalMode: definition.approvalMode },
516
667
  ...definition.contextBudget === void 0 ? {} : { contextBudget: { ...definition.contextBudget } },
668
+ ...definition.toolSearch === void 0 ? {} : { toolSearch: {
669
+ ...definition.toolSearch,
670
+ ...definition.toolSearch.pinned === void 0 ? {} : { pinned: [...definition.toolSearch.pinned] }
671
+ } },
517
672
  ...definition.inputSchema === void 0 ? {} : { inputSchema: definition.inputSchema },
518
673
  ...definition.outputSchema === void 0 ? {} : { outputSchema: definition.outputSchema },
519
674
  ...rawLimits === void 0 ? {} : { limits: {
520
675
  ...rawLimits.maxSteps === void 0 ? {} : { maxSteps: rawLimits.maxSteps },
521
676
  ...rawLimits.maxToolCalls === void 0 ? {} : { maxToolCalls: rawLimits.maxToolCalls },
522
677
  ...rawLimits.maxInputTokens === void 0 ? {} : { maxInputTokens: rawLimits.maxInputTokens },
523
- ...rawLimits.maxOutputTokens === void 0 ? {} : { maxOutputTokens: rawLimits.maxOutputTokens }
678
+ ...rawLimits.maxOutputTokens === void 0 ? {} : { maxOutputTokens: rawLimits.maxOutputTokens },
679
+ ...rawLimits.maxToolResultBytes === void 0 ? {} : { maxToolResultBytes: rawLimits.maxToolResultBytes }
524
680
  } }
525
681
  });
526
682
  }
@@ -557,6 +713,7 @@ function createComposition(config) {
557
713
  assertLimit(rawLimits?.maxToolCalls, "maxToolCalls", 0, name, false, "Workflow", "INVALID_WORKFLOW");
558
714
  assertLimit(rawLimits?.maxInputTokens, "maxInputTokens", 1, name, true, "Workflow", "INVALID_WORKFLOW");
559
715
  assertLimit(rawLimits?.maxOutputTokens, "maxOutputTokens", 1, name, true, "Workflow", "INVALID_WORKFLOW");
716
+ assertLimit(rawLimits?.maxToolResultBytes, "maxToolResultBytes", 1, name, false, "Workflow", "INVALID_WORKFLOW");
560
717
  assertConfiguration(!workflows.has(name), "DUPLICATE_WORKFLOW", `Workflow "${name}" is duplicated`);
561
718
  workflows.set(name, {
562
719
  name,
@@ -568,7 +725,8 @@ function createComposition(config) {
568
725
  ...rawLimits.maxSteps === void 0 ? {} : { maxSteps: rawLimits.maxSteps },
569
726
  ...rawLimits.maxToolCalls === void 0 ? {} : { maxToolCalls: rawLimits.maxToolCalls },
570
727
  ...rawLimits.maxInputTokens === void 0 ? {} : { maxInputTokens: rawLimits.maxInputTokens },
571
- ...rawLimits.maxOutputTokens === void 0 ? {} : { maxOutputTokens: rawLimits.maxOutputTokens }
728
+ ...rawLimits.maxOutputTokens === void 0 ? {} : { maxOutputTokens: rawLimits.maxOutputTokens },
729
+ ...rawLimits.maxToolResultBytes === void 0 ? {} : { maxToolResultBytes: rawLimits.maxToolResultBytes }
572
730
  } },
573
731
  run: definition.run
574
732
  });
@@ -607,6 +765,7 @@ function createComposition(config) {
607
765
  assertLimit(rawLimits?.maxToolCalls, "maxToolCalls", 0, name, false, "Team", "INVALID_TEAM");
608
766
  assertLimit(rawLimits?.maxInputTokens, "maxInputTokens", 1, name, true, "Team", "INVALID_TEAM");
609
767
  assertLimit(rawLimits?.maxOutputTokens, "maxOutputTokens", 1, name, true, "Team", "INVALID_TEAM");
768
+ assertLimit(rawLimits?.maxToolResultBytes, "maxToolResultBytes", 1, name, false, "Team", "INVALID_TEAM");
610
769
  assertConfiguration(!teams.has(name), "DUPLICATE_TEAM", `Team "${name}" is duplicated`);
611
770
  assertConfiguration(!workflows.has(name), "INVALID_TEAM", `Team "${name}" conflicts with a workflow of the same name`);
612
771
  const team = {
@@ -622,7 +781,8 @@ function createComposition(config) {
622
781
  ...rawLimits.maxSteps === void 0 ? {} : { maxSteps: rawLimits.maxSteps },
623
782
  ...rawLimits.maxToolCalls === void 0 ? {} : { maxToolCalls: rawLimits.maxToolCalls },
624
783
  ...rawLimits.maxInputTokens === void 0 ? {} : { maxInputTokens: rawLimits.maxInputTokens },
625
- ...rawLimits.maxOutputTokens === void 0 ? {} : { maxOutputTokens: rawLimits.maxOutputTokens }
784
+ ...rawLimits.maxOutputTokens === void 0 ? {} : { maxOutputTokens: rawLimits.maxOutputTokens },
785
+ ...rawLimits.maxToolResultBytes === void 0 ? {} : { maxToolResultBytes: rawLimits.maxToolResultBytes }
626
786
  } },
627
787
  run: definition.run
628
788
  };
@@ -638,9 +798,15 @@ function createComposition(config) {
638
798
  tools,
639
799
  contextProviders,
640
800
  contextBudget: config.contextBudget === void 0 ? void 0 : { ...config.contextBudget },
801
+ toolSearch: config.toolSearch === void 0 ? void 0 : {
802
+ ...config.toolSearch,
803
+ ...config.toolSearch.pinned === void 0 ? {} : { pinned: [...config.toolSearch.pinned] }
804
+ },
805
+ toolSearchCache: defaultToolSearchCache,
641
806
  compaction: {
642
807
  compactor: config.compaction?.compactor ?? createDigestContextCompactor(),
643
- estimateTokens: config.compaction?.estimateTokens ?? estimateContextTokens
808
+ estimateTokens: config.compaction?.estimateTokens ?? estimateContextTokens,
809
+ ...config.compaction?.redactSummary === void 0 ? {} : { redactSummary: config.compaction.redactSummary }
644
810
  },
645
811
  memoryStore: config.memoryStore ?? (usesMemory ? new InMemoryMemoryStore() : void 0),
646
812
  runStore: config.runStore ?? new InMemoryRunStore(),
@@ -672,6 +838,10 @@ const LEASE_RENEW_MS = 1e4;
672
838
  const ELICIT_TOOL_NAME = "fevex__elicit";
673
839
  /** Reserved name of the internal progressive skill loader exposed to the model. */
674
840
  const LOAD_SKILL_TOOL_NAME = "fevex__load_skill";
841
+ /** Reserved name of the internal on-demand tool search exposed to the model. */
842
+ const SEARCH_TOOLS_TOOL_NAME = "fevex__search_tools";
843
+ /** Default safety cap for one serialized tool result. */
844
+ const DEFAULT_MAX_TOOL_RESULT_BYTES = 1048576;
675
845
  //#endregion
676
846
  //#region src/internal/run-helpers.ts
677
847
  function redact(message, secrets) {
@@ -799,9 +969,114 @@ const runErrorPayload = (error) => {
799
969
  };
800
970
  };
801
971
  //#endregion
972
+ //#region src/internal/agent-internal-tools.ts
973
+ async function executeInternalTool(input) {
974
+ if (input.call.name === "fevex__search_tools") {
975
+ if (!input.searchEnabled) return void 0;
976
+ const search = readToolSearchInput(input.input);
977
+ const limit = Math.min(search.limit ?? input.toolSearch.limit, input.toolSearch.limit);
978
+ const semantic = input.toolSearch.semantic;
979
+ const candidateLimit = Math.max(limit, semantic?.topK ?? limit);
980
+ const lexical = searchToolSpecs(input.tools, search.query, candidateLimit);
981
+ let matches = lexical;
982
+ if (semantic !== void 0) try {
983
+ if (input.semanticState.index === void 0 && !input.semanticState.attempted) {
984
+ input.semanticState.attempted = true;
985
+ input.semanticState.index = await buildToolEmbeddingIndex({
986
+ tools: input.tools,
987
+ embeddings: semantic.embeddings,
988
+ cache: semantic.cache,
989
+ ...input.signal === void 0 ? {} : { signal: input.signal }
990
+ });
991
+ }
992
+ if (input.semanticState.index !== void 0) {
993
+ const queryVector = (await semantic.embeddings.embed({
994
+ texts: [search.query],
995
+ ...input.signal === void 0 ? {} : { signal: input.signal }
996
+ }))[0];
997
+ if (!queryVector) throw new Error("Embedding model returned no query vector");
998
+ const semanticMatches = searchToolEmbeddingIndex(input.semanticState.index, queryVector, semantic.topK, semantic.minScore);
999
+ if (semanticMatches.length > 0) {
1000
+ const fusionLimit = semantic.reranker === void 0 ? limit : Math.max(limit, semantic.topK);
1001
+ const fused = reciprocalRankFusionCandidates(lexical, semanticMatches.map((match) => match.tool), fusionLimit);
1002
+ if (semantic.reranker !== void 0 && fused.length > 1) matches = (await rerankToolCandidates(semantic.reranker, search.query, fused, limit, input.signal) ?? fused).map((candidate) => candidate.tool);
1003
+ else matches = fused.map((candidate) => candidate.tool);
1004
+ }
1005
+ }
1006
+ } catch (error) {
1007
+ if (input.signal?.aborted) throw error;
1008
+ matches = lexical;
1009
+ }
1010
+ const selected = matches.slice(0, limit);
1011
+ for (const match of selected) input.loadedTools.add(match.name);
1012
+ return { content: serializeValue({
1013
+ tools: selected.map((tool) => ({
1014
+ name: tool.name,
1015
+ ...tool.description === void 0 ? {} : { description: tool.description }
1016
+ })),
1017
+ ...selected.length === 0 ? { message: "No tools matched; try a different query." } : {}
1018
+ }, "Tool search result must be serializable") };
1019
+ }
1020
+ if (input.call.name === "fevex__load_skill") {
1021
+ if (input.progressiveSkills.length === 0) return void 0;
1022
+ const skillInput = readSkillInput(input.input);
1023
+ const skill = input.progressiveSkills.find((item) => item.name === skillInput.name);
1024
+ if (!skill) throw new Error(`Skill "${skillInput.name}" is not available`);
1025
+ try {
1026
+ return { content: (await abortable(() => skill.load(input.knowledgeContext, skillInput.resource), input.signal)).filter((block) => block.content.trim()).map((block) => `[Skill: ${skill.name}/${block.id}]\n${block.content}`).join("\n\n") };
1027
+ } catch (error) {
1028
+ throw new Error(`Skill provider "${skill.name}" failed: ${toErrorMessage(error)}`, { cause: error });
1029
+ }
1030
+ }
1031
+ }
1032
+ async function rerankToolCandidates(reranker, query, candidates, limit, signal) {
1033
+ try {
1034
+ const result = await reranker.rerank({
1035
+ query,
1036
+ tools: candidates,
1037
+ limit,
1038
+ ...signal === void 0 ? {} : { signal }
1039
+ });
1040
+ if (!Array.isArray(result)) return void 0;
1041
+ const byName = new Map(candidates.map((candidate) => [candidate.tool.name, candidate]));
1042
+ const seen = /* @__PURE__ */ new Set();
1043
+ const ordered = [];
1044
+ for (const candidate of result) {
1045
+ const name = candidate?.tool?.name;
1046
+ const original = typeof name === "string" ? byName.get(name) : void 0;
1047
+ if (!original || seen.has(name)) return void 0;
1048
+ seen.add(name);
1049
+ ordered.push(original);
1050
+ }
1051
+ return ordered.slice(0, limit);
1052
+ } catch (error) {
1053
+ if (signal?.aborted) throw error;
1054
+ return;
1055
+ }
1056
+ }
1057
+ function readSkillInput(input) {
1058
+ if (typeof input !== "object" || input === null || Array.isArray(input)) throw new TypeError(`${LOAD_SKILL_TOOL_NAME} input must be an object`);
1059
+ const record = input;
1060
+ const name = record.name;
1061
+ const resource = record.resource;
1062
+ if (typeof name !== "string" || !name.trim()) throw new TypeError(`${LOAD_SKILL_TOOL_NAME} name must be a non-empty string`);
1063
+ if (resource !== void 0 && (typeof resource !== "string" || !resource.trim())) throw new TypeError(`${LOAD_SKILL_TOOL_NAME} resource must be a non-empty string`);
1064
+ return {
1065
+ name: name.trim(),
1066
+ ...resource === void 0 ? {} : { resource: resource.trim() }
1067
+ };
1068
+ }
1069
+ //#endregion
802
1070
  //#region src/internal/context-compaction.ts
803
1071
  /** Maximum summary characters copied into the context.compacted event. */
804
1072
  const MAX_COMPACTION_EVENT_SUMMARY_CHARS = 4e3;
1073
+ /** Keeps the newest summary segments instead of cutting mid-sentence. */
1074
+ function capSummary(summary, maxChars) {
1075
+ if (summary.length <= maxChars) return summary;
1076
+ const tail = summary.slice(summary.length - maxChars + 1);
1077
+ const boundary = tail.indexOf("\n\n");
1078
+ return `…${boundary === -1 ? tail : tail.slice(boundary + 2)}`;
1079
+ }
805
1080
  async function compactContext(input) {
806
1081
  const estimateWith = (messages, includeTools) => {
807
1082
  const value = input.estimateTokens({
@@ -914,6 +1189,7 @@ async function transportableTool(tool) {
914
1189
  retry: tool.retry,
915
1190
  credentials: tool.credentials ?? [],
916
1191
  sandbox: tool.sandbox,
1192
+ maxResultBytes: tool.maxResultBytes,
917
1193
  source: tool.source
918
1194
  };
919
1195
  }
@@ -983,7 +1259,8 @@ function combineLimits(first, second) {
983
1259
  ...minLimit(first.maxSteps, second.maxSteps) === void 0 ? {} : { maxSteps: minLimit(first.maxSteps, second.maxSteps) },
984
1260
  ...minLimit(first.maxToolCalls, second.maxToolCalls) === void 0 ? {} : { maxToolCalls: minLimit(first.maxToolCalls, second.maxToolCalls) },
985
1261
  ...minLimit(first.maxInputTokens, second.maxInputTokens) === void 0 ? {} : { maxInputTokens: minLimit(first.maxInputTokens, second.maxInputTokens) },
986
- ...minLimit(first.maxOutputTokens, second.maxOutputTokens) === void 0 ? {} : { maxOutputTokens: minLimit(first.maxOutputTokens, second.maxOutputTokens) }
1262
+ ...minLimit(first.maxOutputTokens, second.maxOutputTokens) === void 0 ? {} : { maxOutputTokens: minLimit(first.maxOutputTokens, second.maxOutputTokens) },
1263
+ ...minLimit(first.maxToolResultBytes, second.maxToolResultBytes) === void 0 ? {} : { maxToolResultBytes: minLimit(first.maxToolResultBytes, second.maxToolResultBytes) }
987
1264
  };
988
1265
  }
989
1266
  function subtractLimit(limit, used) {
@@ -996,7 +1273,8 @@ function remainingWorkflowLimits(limits, budget) {
996
1273
  ...limits.maxSteps === void 0 ? {} : { maxSteps: subtractLimit(limits.maxSteps, budget?.steps) },
997
1274
  ...limits.maxToolCalls === void 0 ? {} : { maxToolCalls: subtractLimit(limits.maxToolCalls, budget?.toolCalls) },
998
1275
  ...limits.maxInputTokens === void 0 ? {} : { maxInputTokens: subtractLimit(limits.maxInputTokens, budget?.usage?.inputTokens) },
999
- ...limits.maxOutputTokens === void 0 ? {} : { maxOutputTokens: subtractLimit(limits.maxOutputTokens, budget?.usage?.outputTokens) }
1276
+ ...limits.maxOutputTokens === void 0 ? {} : { maxOutputTokens: subtractLimit(limits.maxOutputTokens, budget?.usage?.outputTokens) },
1277
+ ...limits.maxToolResultBytes === void 0 ? {} : { maxToolResultBytes: limits.maxToolResultBytes }
1000
1278
  };
1001
1279
  }
1002
1280
  function assertWorkflowBudget(workflowName, limits, budget) {
@@ -1023,13 +1301,24 @@ function addWorkflowBudget(first, second) {
1023
1301
  * engine — that direction is what keeps the two separable.
1024
1302
  */
1025
1303
  function createAgentEngine(composition, core) {
1026
- const { models, agents, tools, contextProviders, contextBudget: defaultContextBudget, compaction, memoryStore, runStore, credentialStore, sandbox, artifacts, policies } = composition;
1304
+ const { models, agents, tools, contextProviders, contextBudget: defaultContextBudget, compaction, toolSearch: defaultToolSearch, toolSearchCache: defaultToolSearchCache, memoryStore, runStore, credentialStore, sandbox, artifacts, policies } = composition;
1305
+ const compactionStats = {
1306
+ compactions: 0,
1307
+ removedMessages: 0,
1308
+ estimatedTokens: 0,
1309
+ compactedTokens: 0,
1310
+ savedTokens: 0
1311
+ };
1027
1312
  const { activeRuns, activeSessions, runtimeOwner, isClosed, admitStart, markInternalRun, resolveModel, resolveCheckpointModel, notifyObserver, createEvent, emitEvent, releaseExecution, commit, startLease, cancelExecution, authorize } = core;
1028
1313
  const prepareExecutionBody = async (name, request, internal = false, correlation = {}) => {
1029
1314
  if (!internal && isClosed()) throw new FevexRunError("RUNTIME_CLOSED", "Runtime is not accepting new runs");
1030
1315
  const agent = agents.get(name);
1031
1316
  if (!agent) throw new FevexRunError("AGENT_NOT_FOUND", `Agent "${name}" is not registered`);
1032
1317
  const contextBudget = resolveContextBudget(request.contextBudget ?? agent.contextBudget ?? defaultContextBudget, `Agent "${name}" contextBudget`);
1318
+ const toolSearch = resolveToolSearchOptions(request.toolSearch ?? agent.toolSearch ?? defaultToolSearch, `Agent "${name}" toolSearch`, defaultToolSearchCache);
1319
+ const toolChoice = effectiveToolChoice(agent, request);
1320
+ const initialLoadedTools = new Set(toolSearch.pinned.filter((toolName) => agent.tools?.includes(toolName)));
1321
+ if (typeof toolChoice === "object" && agent.tools?.includes(toolChoice.name)) initialLoadedTools.add(toolChoice.name);
1033
1322
  const { modelName, model } = resolveModel(agent, request.model);
1034
1323
  const now = (/* @__PURE__ */ new Date()).toISOString();
1035
1324
  let session;
@@ -1103,6 +1392,8 @@ function createAgentEngine(composition, core) {
1103
1392
  definitionHash: await definitionHash(name, modelName, agent, tools, contextBudget),
1104
1393
  limits: combineLimits(agent.limits, request.limits),
1105
1394
  ...contextBudget === void 0 ? {} : { contextBudget },
1395
+ toolSearch: serializeToolSearchOptions(toolSearch),
1396
+ loadedTools: [...initialLoadedTools],
1106
1397
  messages,
1107
1398
  inputContent,
1108
1399
  context: request.context,
@@ -1113,7 +1404,7 @@ function createAgentEngine(composition, core) {
1113
1404
  pendingIndex: 0,
1114
1405
  effectiveElicitationMode: effectiveElicitationMode(agent, request),
1115
1406
  effectiveApprovalMode: effectiveApprovalMode(agent, request),
1116
- effectiveToolChoice: effectiveToolChoice(agent, request)
1407
+ effectiveToolChoice: toolChoice
1117
1408
  };
1118
1409
  const ownerId = `${runtimeOwner}:${crypto.randomUUID()}`;
1119
1410
  const lease = {
@@ -1161,6 +1452,7 @@ function createAgentEngine(composition, core) {
1161
1452
  const agent = agents.get(state.run.agentName);
1162
1453
  if (agent.model !== void 0 && typeof agent.model !== "string") throw new FevexRunError("RUN_NOT_RESUMABLE", "Durable continuation requires a named model", state.run.id);
1163
1454
  const contextBudget = resolveContextBudget(state.checkpoint?.contextBudget ?? state.request.contextBudget ?? agent.contextBudget ?? defaultContextBudget, `Agent "${state.run.agentName}" contextBudget`);
1455
+ const toolSearch = state.toolSearch ?? resolveToolSearchOptions(state.checkpoint?.toolSearch ?? state.request.toolSearch ?? agent.toolSearch ?? defaultToolSearch, `Agent "${state.run.agentName}" toolSearch`, defaultToolSearchCache);
1164
1456
  let serializedState;
1165
1457
  if (providerState !== void 0) {
1166
1458
  if (!model.stateCodec) throw new FevexRunError("RUN_NOT_RESUMABLE", "ModelGateway must provide stateCodec for durable continuation", state.run.id);
@@ -1174,6 +1466,8 @@ function createAgentEngine(composition, core) {
1174
1466
  definitionHash: await definitionHash(state.run.agentName, modelName, agent, tools, contextBudget),
1175
1467
  limits: state.checkpoint?.limits ?? combineLimits(agent.limits, state.request.limits),
1176
1468
  ...contextBudget === void 0 ? {} : { contextBudget },
1469
+ toolSearch: serializeToolSearchOptions(toolSearch),
1470
+ loadedTools: [...state.loadedTools ?? []],
1177
1471
  messages: structuredClone(messages),
1178
1472
  inputContent,
1179
1473
  context: state.request.context,
@@ -1233,6 +1527,14 @@ function createAgentEngine(composition, core) {
1233
1527
  const agent = agents.get(name);
1234
1528
  const { modelName, model } = resolveModel(agent, state.checkpoint?.modelName ?? request.model);
1235
1529
  const contextBudget = resolveContextBudget(state.checkpoint?.contextBudget ?? request.contextBudget ?? agent.contextBudget ?? defaultContextBudget, `Agent "${name}" contextBudget`);
1530
+ const semantic = request.toolSearch?.semantic ?? agent.toolSearch?.semantic ?? defaultToolSearch?.semantic;
1531
+ const toolSearch = resolveToolSearchOptions(state.checkpoint === void 0 ? request.toolSearch ?? agent.toolSearch ?? defaultToolSearch : state.checkpoint.toolSearch === void 0 ? {
1532
+ mode: "off",
1533
+ ...semantic === void 0 ? {} : { semantic }
1534
+ } : {
1535
+ ...state.checkpoint.toolSearch,
1536
+ ...semantic === void 0 ? {} : { semantic }
1537
+ }, `Agent "${name}" toolSearch`, defaultToolSearchCache);
1236
1538
  const events = [...state.initialEvents ?? []];
1237
1539
  const firstEvents = state.initialEvents ?? [];
1238
1540
  state.initialEvents = void 0;
@@ -1251,6 +1553,7 @@ function createAgentEngine(composition, core) {
1251
1553
  let pendingTools;
1252
1554
  let pendingIndex;
1253
1555
  let compactionApplied = false;
1556
+ const semanticState = { attempted: false };
1254
1557
  try {
1255
1558
  for (const event of firstEvents) yield event;
1256
1559
  if (state.checkpoint) {
@@ -1326,30 +1629,86 @@ function createAgentEngine(composition, core) {
1326
1629
  additionalProperties: false
1327
1630
  }
1328
1631
  };
1632
+ const searchEnabled = shouldUseToolSearch(toolSearch, agentTools);
1633
+ const searchTool = searchEnabled ? {
1634
+ name: SEARCH_TOOLS_TOOL_NAME,
1635
+ description: `Search ${agentTools.length} tools available to this agent by capability. Use it when you need a tool that is not currently exposed. Returns matching tools and makes their schemas available on the next turn.`,
1636
+ inputSchema: {
1637
+ type: "object",
1638
+ properties: {
1639
+ query: {
1640
+ type: "string",
1641
+ description: "Capability to search for, such as \"send email\" or \"query database\"."
1642
+ },
1643
+ limit: {
1644
+ type: "integer",
1645
+ minimum: 1,
1646
+ maximum: toolSearch.limit
1647
+ }
1648
+ },
1649
+ required: ["query"],
1650
+ additionalProperties: false
1651
+ }
1652
+ } : void 0;
1653
+ const elicitationTool = currentElicitationMode === "pause" ? {
1654
+ name: ELICIT_TOOL_NAME,
1655
+ description: "Request external information required to continue this run. Use prompt for explanatory text shown outside the form. Put short field labels in responseSchema property titles and optional field help in property descriptions.",
1656
+ inputSchema: {
1657
+ type: "object",
1658
+ properties: {
1659
+ prompt: {
1660
+ type: "string",
1661
+ description: "Explanatory text for the user; do not duplicate field labels here."
1662
+ },
1663
+ responseSchema: {
1664
+ type: "object",
1665
+ description: "JSON Schema for the expected response. Each property should include a short title for the form label and may include a description for field help."
1666
+ },
1667
+ expiresAt: { type: "string" }
1668
+ },
1669
+ required: ["prompt", "responseSchema"],
1670
+ additionalProperties: false
1671
+ }
1672
+ } : void 0;
1329
1673
  const allTools = [
1330
1674
  ...agentTools,
1331
1675
  ...skillTool === void 0 ? [] : [skillTool],
1332
- ...currentElicitationMode === "pause" ? [{
1333
- name: ELICIT_TOOL_NAME,
1334
- description: "Request external information required to continue this run. Use prompt for explanatory text shown outside the form. Put short field labels in responseSchema property titles and optional field help in property descriptions.",
1335
- inputSchema: {
1336
- type: "object",
1337
- properties: {
1338
- prompt: {
1339
- type: "string",
1340
- description: "Explanatory text for the user; do not duplicate field labels here."
1341
- },
1342
- responseSchema: {
1343
- type: "object",
1344
- description: "JSON Schema for the expected response. Each property should include a short title for the form label and may include a description for field help."
1345
- },
1346
- expiresAt: { type: "string" }
1347
- },
1348
- required: ["prompt", "responseSchema"],
1349
- additionalProperties: false
1350
- }
1351
- }] : []
1676
+ ...searchTool === void 0 ? [] : [searchTool],
1677
+ ...elicitationTool === void 0 ? [] : [elicitationTool]
1352
1678
  ];
1679
+ const loadedTools = new Set(state.loadedTools ?? state.checkpoint?.loadedTools ?? []);
1680
+ for (const toolName of toolSearch.pinned) if (agent.tools?.includes(toolName)) loadedTools.add(toolName);
1681
+ if (typeof currentToolChoice === "object" && agent.tools?.includes(currentToolChoice.name)) loadedTools.add(currentToolChoice.name);
1682
+ state.loadedTools = loadedTools;
1683
+ state.toolSearch = toolSearch;
1684
+ let loadedToolsVersion = 0;
1685
+ let cachedModelTools;
1686
+ let cachedModelToolsVersion = -1;
1687
+ const modelTools = () => {
1688
+ if (!searchEnabled) return allTools;
1689
+ if (cachedModelTools !== void 0 && cachedModelToolsVersion === loadedToolsVersion) return cachedModelTools;
1690
+ cachedModelTools = [
1691
+ ...agentTools.filter((tool) => loadedTools.has(tool.name)),
1692
+ ...skillTool === void 0 ? [] : [skillTool],
1693
+ ...searchTool === void 0 ? [] : [searchTool],
1694
+ ...elicitationTool === void 0 ? [] : [elicitationTool]
1695
+ ];
1696
+ cachedModelToolsVersion = loadedToolsVersion;
1697
+ return cachedModelTools;
1698
+ };
1699
+ const toolsTokenCache = /* @__PURE__ */ new WeakMap();
1700
+ const estimateContext = (input) => {
1701
+ if (!input.tools?.length) return compaction.estimateTokens(input);
1702
+ let toolTokens = toolsTokenCache.get(input.tools);
1703
+ if (toolTokens === void 0) {
1704
+ toolTokens = compaction.estimateTokens({
1705
+ messages: [],
1706
+ tools: input.tools
1707
+ });
1708
+ toolsTokenCache.set(input.tools, toolTokens);
1709
+ }
1710
+ return compaction.estimateTokens({ messages: input.messages }) + toolTokens;
1711
+ };
1353
1712
  const effectiveLimits = state.checkpoint?.limits ?? combineLimits(agent.limits, request.limits);
1354
1713
  const maxSteps = effectiveLimits?.maxSteps ?? 8;
1355
1714
  const maxToolCalls = effectiveLimits?.maxToolCalls ?? 16;
@@ -1357,16 +1716,17 @@ function createAgentEngine(composition, core) {
1357
1716
  while (step <= maxSteps) {
1358
1717
  request.signal.throwIfAborted();
1359
1718
  if (pendingTools.length === 0) {
1719
+ const currentTools = modelTools();
1360
1720
  if (contextBudget !== void 0) {
1361
1721
  const compacted = await compactContext({
1362
1722
  runId: state.run.id,
1363
1723
  agentName: name,
1364
1724
  sessionId: state.session.id,
1365
1725
  messages,
1366
- tools: allTools,
1726
+ tools: currentTools,
1367
1727
  contextBudget,
1368
1728
  compactor: compaction.compactor,
1369
- estimateTokens: compaction.estimateTokens,
1729
+ estimateTokens: estimateContext,
1370
1730
  step,
1371
1731
  ...request.context === void 0 ? {} : { context: request.context },
1372
1732
  signal: request.signal,
@@ -1375,15 +1735,22 @@ function createAgentEngine(composition, core) {
1375
1735
  if (compacted !== void 0) {
1376
1736
  messages = compacted.messages;
1377
1737
  compactionApplied = true;
1738
+ compactionStats.compactions += 1;
1739
+ compactionStats.removedMessages += compacted.removedMessages;
1740
+ compactionStats.estimatedTokens += compacted.estimatedTokens;
1741
+ compactionStats.compactedTokens += compacted.compactedTokens;
1742
+ compactionStats.savedTokens += Math.max(0, compacted.estimatedTokens - compacted.compactedTokens);
1378
1743
  const previous = state.session.compaction;
1379
1744
  const combined = previous === void 0 ? compacted.summary : `${previous.summary}\n\n${compacted.summary}`;
1380
1745
  const maxSummaryChars = Math.max(200, contextBudget.targetTokens * 4);
1381
1746
  state.session.compaction = {
1382
- summary: combined.length > maxSummaryChars ? `…${combined.slice(combined.length - maxSummaryChars + 1)}` : combined,
1747
+ summary: capSummary(combined, maxSummaryChars),
1383
1748
  coveredMessages: state.session.history.length,
1384
1749
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1385
1750
  };
1386
- const summaryTruncated = compacted.summary.length > MAX_COMPACTION_EVENT_SUMMARY_CHARS;
1751
+ const redactedSummary = compaction.redactSummary?.(compacted.summary) ?? compacted.summary;
1752
+ if (typeof redactedSummary !== "string") throw new TypeError("compaction.redactSummary must return a string");
1753
+ const summaryTruncated = redactedSummary.length > MAX_COMPACTION_EVENT_SUMMARY_CHARS;
1387
1754
  const event = createEvent(state, "context.compacted", {
1388
1755
  step,
1389
1756
  removedMessages: compacted.removedMessages,
@@ -1391,7 +1758,7 @@ function createAgentEngine(composition, core) {
1391
1758
  compactedTokens: compacted.compactedTokens,
1392
1759
  targetTokens: contextBudget.targetTokens,
1393
1760
  contextWindow: contextBudget.contextWindow,
1394
- summary: summaryTruncated ? `${compacted.summary.slice(0, MAX_COMPACTION_EVENT_SUMMARY_CHARS - 1)}…` : compacted.summary,
1761
+ summary: summaryTruncated ? capSummary(redactedSummary, MAX_COMPACTION_EVENT_SUMMARY_CHARS) : redactedSummary,
1395
1762
  ...summaryTruncated ? { summaryTruncated: true } : {}
1396
1763
  });
1397
1764
  if (isDurableRunStore(runStore)) {
@@ -1411,11 +1778,11 @@ function createAgentEngine(composition, core) {
1411
1778
  }
1412
1779
  }
1413
1780
  const hasToolBudget = step < maxSteps && toolCallCount < maxToolCalls;
1414
- const modelToolChoice = hasToolBudget && allTools.length ? currentToolChoice === "auto" ? void 0 : currentToolChoice : void 0;
1781
+ const modelToolChoice = hasToolBudget && currentTools.length ? currentToolChoice === "auto" ? void 0 : currentToolChoice : void 0;
1415
1782
  yield await emit("model.started", { step });
1416
1783
  const modelStream = readModelStream(model, {
1417
1784
  messages: [...messages],
1418
- tools: hasToolBudget && allTools.length ? allTools : void 0,
1785
+ tools: hasToolBudget && currentTools.length ? currentTools : void 0,
1419
1786
  ...modelToolChoice === void 0 ? {} : { toolChoice: modelToolChoice },
1420
1787
  reasoning: state.request.reasoning ?? agent.reasoning,
1421
1788
  modelOptions: agent.modelOptions,
@@ -1491,52 +1858,63 @@ function createAgentEngine(composition, core) {
1491
1858
  assertContinuationBudget(name, "maxInputTokens", "inputTokens", effectiveLimits?.maxInputTokens, usage);
1492
1859
  assertContinuationBudget(name, "maxOutputTokens", "outputTokens", effectiveLimits?.maxOutputTokens, usage);
1493
1860
  if (result.toolCalls.length > maxToolCalls - toolCallCount) throw new Error(`Agent "${name}" exceeded maxToolCalls limit of ${maxToolCalls}`);
1494
- const loadSkillCall = progressiveSkills.length === 0 ? void 0 : result.toolCalls.find((call) => call.name === LOAD_SKILL_TOOL_NAME);
1495
- if (loadSkillCall) {
1496
- if (result.toolCalls.length !== 1) throw new Error(`${LOAD_SKILL_TOOL_NAME} must be the only tool call in a model turn`);
1497
- if (!loadSkillCall.id?.trim()) throw new Error("Tool call id cannot be empty");
1498
- if (seenToolCallIds.has(loadSkillCall.id)) throw new Error(`Tool call id "${loadSkillCall.id}" is duplicated in run "${state.run.id}"`);
1861
+ const internalCall = result.toolCalls.find((call) => searchEnabled && call.name === "fevex__search_tools" || progressiveSkills.length > 0 && call.name === "fevex__load_skill");
1862
+ if (internalCall) {
1863
+ if (result.toolCalls.length !== 1) throw new Error(`${internalCall.name} must be the only tool call in a model turn`);
1864
+ if (!internalCall.id?.trim()) throw new Error("Tool call id cannot be empty");
1865
+ if (seenToolCallIds.has(internalCall.id)) throw new Error(`Tool call id "${internalCall.id}" is duplicated in run "${state.run.id}"`);
1499
1866
  if (toolCallCount + 1 > maxToolCalls) throw new Error(`Agent "${name}" exceeded maxToolCalls limit of ${maxToolCalls}`);
1500
- const rawInput = toJsonValue(loadSkillCall.input, `Input for tool "${LOAD_SKILL_TOOL_NAME}" must be JSON-serializable`);
1501
- const skillInput = readSkillInput(rawInput);
1502
- const skill = progressiveSkills.find((item) => item.name === skillInput.name);
1503
- if (!skill) throw new Error(`Skill "${skillInput.name}" is not available to agent "${name}"`);
1504
- seenToolCallIds.add(loadSkillCall.id);
1867
+ const rawInput = toJsonValue(internalCall.input, `Input for tool "${internalCall.name}" must be JSON-serializable`);
1868
+ const loadedBefore = loadedTools.size;
1869
+ seenToolCallIds.add(internalCall.id);
1505
1870
  messages.push({
1506
1871
  role: "assistant",
1507
1872
  content: result.output === void 0 ? "" : serializeValue(result.output, `Model output for agent "${name}" must be serializable`),
1508
1873
  toolCalls: [{
1509
- ...loadSkillCall,
1874
+ ...internalCall,
1510
1875
  input: rawInput
1511
1876
  }]
1512
1877
  });
1513
1878
  yield await emit("tool.started", {
1514
1879
  step,
1515
- toolCallId: loadSkillCall.id,
1516
- toolName: LOAD_SKILL_TOOL_NAME
1880
+ toolCallId: internalCall.id,
1881
+ toolName: internalCall.name
1517
1882
  });
1518
- let blocks;
1883
+ let execution;
1519
1884
  try {
1520
- blocks = await abortable(() => skill.load(knowledgeContext(state, inputContent), skillInput.resource), request.signal);
1885
+ execution = await executeInternalTool({
1886
+ call: internalCall,
1887
+ input: rawInput,
1888
+ tools: agentTools,
1889
+ searchEnabled,
1890
+ toolSearch,
1891
+ loadedTools,
1892
+ progressiveSkills,
1893
+ knowledgeContext: knowledgeContext(state, inputContent),
1894
+ semanticState,
1895
+ signal: request.signal
1896
+ });
1521
1897
  } catch (error) {
1522
1898
  yield await emit("tool.failed", {
1523
1899
  step,
1524
- toolCallId: loadSkillCall.id,
1525
- toolName: LOAD_SKILL_TOOL_NAME,
1900
+ toolCallId: internalCall.id,
1901
+ toolName: internalCall.name,
1526
1902
  error: toErrorMessage(error)
1527
1903
  });
1528
- throw new Error(`Skill provider "${skill.name}" failed: ${toErrorMessage(error)}`, { cause: error });
1904
+ throw error;
1529
1905
  }
1906
+ if (!execution) throw new Error(`Tool "${internalCall.name}" is not available to agent "${name}"`);
1907
+ if (loadedTools.size > loadedBefore) loadedToolsVersion += 1;
1530
1908
  messages.push({
1531
1909
  role: "tool",
1532
- name: LOAD_SKILL_TOOL_NAME,
1533
- toolCallId: loadSkillCall.id,
1534
- content: blocks.filter((block) => block.content.trim()).map((block) => `[Skill: ${skill.name}/${block.id}]\n${block.content}`).join("\n\n")
1910
+ name: internalCall.name,
1911
+ toolCallId: internalCall.id,
1912
+ content: execution.content
1535
1913
  });
1536
1914
  yield await emit("tool.completed", {
1537
1915
  step,
1538
- toolCallId: loadSkillCall.id,
1539
- toolName: LOAD_SKILL_TOOL_NAME
1916
+ toolCallId: internalCall.id,
1917
+ toolName: internalCall.name
1540
1918
  });
1541
1919
  toolCallCount += 1;
1542
1920
  step += 1;
@@ -1598,6 +1976,7 @@ function createAgentEngine(composition, core) {
1598
1976
  batch.add(call.id);
1599
1977
  const tool = agent.tools?.includes(call.name) ? tools.get(call.name) : void 0;
1600
1978
  if (!tool) throw new Error(`Tool "${call.name}" is not available to agent "${name}"`);
1979
+ if (searchEnabled && !loadedTools.has(call.name)) throw new Error(`Tool "${call.name}" is not loaded; use ${SEARCH_TOOLS_TOOL_NAME} first`);
1601
1980
  const rawInput = toJsonValue(call.input, `Input for tool "${call.name}" must be JSON-serializable`);
1602
1981
  let validated;
1603
1982
  try {
@@ -1854,7 +2233,13 @@ function createAgentEngine(composition, core) {
1854
2233
  signal: request.signal
1855
2234
  }), request.signal);
1856
2235
  for (const event of createdArtifacts.splice(0)) yield event;
1857
- output = toJsonValue(tool.outputSchema === void 0 ? raw : await validateSchema(tool.outputSchema, raw, `Output from tool "${tool.name}" does not match outputSchema`), `Output from tool "${tool.name}" must be JSON-serializable`);
2236
+ const validated = tool.outputSchema === void 0 ? raw : await validateSchema(tool.outputSchema, raw, `Output from tool "${tool.name}" does not match outputSchema`);
2237
+ const serialized = serializeValue(validated, `Output from tool "${tool.name}" must be JSON-serializable`);
2238
+ const configuredMaxResultBytes = tool.maxResultBytes !== void 0 ? tool.maxResultBytes : effectiveLimits?.maxToolResultBytes;
2239
+ const maxResultBytes = configuredMaxResultBytes === void 0 ? DEFAULT_MAX_TOOL_RESULT_BYTES : configuredMaxResultBytes;
2240
+ const resultBytes = new TextEncoder().encode(serialized).byteLength;
2241
+ if (maxResultBytes !== false && resultBytes > maxResultBytes) throw new FevexRunError("TOOL_RESULT_TOO_LARGE", `Tool "${tool.name}" returned ${resultBytes} bytes; limit is ${maxResultBytes}. Store large outputs as artifacts and return an artifact descriptor.`, state.run.id);
2242
+ output = toJsonValue(validated, `Output from tool "${tool.name}" must be JSON-serializable`);
1858
2243
  if (containsSecret(output, secrets)) throw new Error(`Output from tool "${tool.name}" contains a credential`);
1859
2244
  record = {
1860
2245
  ...record,
@@ -1891,7 +2276,7 @@ function createAgentEngine(composition, core) {
1891
2276
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1892
2277
  };
1893
2278
  if (durable) await commit(state, { toolExecution: record });
1894
- if (attempt >= maxAttempts || tool.idempotency !== "keyed") {
2279
+ if (attempt >= maxAttempts || tool.idempotency !== "keyed" || error instanceof FevexRunError && error.code === "TOOL_RESULT_TOO_LARGE") {
1895
2280
  yield await emit("tool.failed", {
1896
2281
  step,
1897
2282
  toolCallId: pending.call.id,
@@ -2288,7 +2673,10 @@ function createAgentEngine(composition, core) {
2288
2673
  nextEvent,
2289
2674
  drainExecution,
2290
2675
  resumeAgent,
2291
- cancelStoredAgent
2676
+ cancelStoredAgent,
2677
+ getCompactionStats() {
2678
+ return { ...compactionStats };
2679
+ }
2292
2680
  };
2293
2681
  }
2294
2682
  function sessionHistoryMessages(session) {
@@ -2299,18 +2687,6 @@ function sessionHistoryMessages(session) {
2299
2687
  content: `[Compacted conversation]\n${compaction.summary}`
2300
2688
  }, ...session.history.slice(compaction.coveredMessages)];
2301
2689
  }
2302
- function readSkillInput(input) {
2303
- if (typeof input !== "object" || input === null || Array.isArray(input)) throw new TypeError(`${LOAD_SKILL_TOOL_NAME} input must be an object`);
2304
- const record = input;
2305
- const name = record.name;
2306
- const resource = record.resource;
2307
- if (typeof name !== "string" || !name.trim()) throw new TypeError(`${LOAD_SKILL_TOOL_NAME} name must be a non-empty string`);
2308
- if (resource !== void 0 && (typeof resource !== "string" || !resource.trim())) throw new TypeError(`${LOAD_SKILL_TOOL_NAME} resource must be a non-empty string`);
2309
- return {
2310
- name: name.trim(),
2311
- ...resource === void 0 ? {} : { resource: resource.trim() }
2312
- };
2313
- }
2314
2690
  function scopedArtifacts(store, options) {
2315
2691
  const scopedContext = (value) => {
2316
2692
  const contextTenant = options.request.context?.attributes?.tenantId;
@@ -4415,7 +4791,7 @@ function createRuntime(composition) {
4415
4791
  const core = createRunCore(composition);
4416
4792
  const { activeRuns, activeWorkflowRuns, activeSessions, isIdle, waitForIdle, closeAdmission, isClosed, traceExports, runtimeOwner, createEvent, createCoordinatorEvent, notifyObserver, commit, startLease, cancelExecution, releaseWorkflowExecution, commitWorkflow } = core;
4417
4793
  const agentEngine = createAgentEngine(composition, core);
4418
- const { prepareExecution, executeAgent, nextEvent, drainExecution, resumeAgent, cancelStoredAgent } = agentEngine;
4794
+ const { prepareExecution, executeAgent, nextEvent, drainExecution, resumeAgent, cancelStoredAgent, getCompactionStats } = agentEngine;
4419
4795
  const { prepareWorkflowExecution, executeWorkflow, nextWorkflowEvent, drainWorkflowExecution, resumeWorkflow, cancelWorkflowExecution, cancelStoredWorkflow } = createWorkflowEngine(composition, core, agentEngine);
4420
4796
  const resumeRun = async (runId, resolution) => {
4421
4797
  if (isClosed()) throw new FevexRunError("RUNTIME_CLOSED", "Runtime is not accepting new runs", runId);
@@ -4616,6 +4992,9 @@ function createRuntime(composition) {
4616
4992
  getObservabilityStats() {
4617
4993
  return traceExports.stats();
4618
4994
  },
4995
+ getCompactionStats() {
4996
+ return getCompactionStats();
4997
+ },
4619
4998
  shutdown
4620
4999
  };
4621
5000
  }
@@ -4631,4 +5010,4 @@ function createFevex(config) {
4631
5010
  return createRuntime(createComposition(config));
4632
5011
  }
4633
5012
  //#endregion
4634
- export { ArtifactError, CatalogedArtifactStore, ChannelError, FEVEX_JSON_SCHEMA_PROFILE_VERSION, FevexConfigurationError, FevexRunError, FileSystemArtifactStore, InMemoryArtifactCatalog, InMemoryArtifactStore, InMemoryMemoryStore, InMemoryRunStore, IntegrationError, PROVIDER_REASONING_UNSUPPORTED, PROVIDER_SCHEMA_UNSUPPORTED, RunPausedError, S3ArtifactStore, SCHEMA_NOT_TRANSPORTABLE, SandboxError, artifactTenantId, assertArtifactId, compileFevexJsonSchema, createAgentsMdContextProvider, createDigestContextCompactor, createFevex, createLocalSandbox, createMemoryContextProvider, defineAgent, defineConnection, defineContextProvider, definePolicy, defineSkill, defineTeam, defineTool, defineWorkflow, deliverChannelRun, estimateContextTokens, handleChannelInput, isDurableRunStore, isProgressiveSkillProvider, isRetainableRunStore, loadSkillFile, loadSkillFiles, resolveContextBudget, sandboxResultToJson, toToolSpec, validateFevexJsonSchemaProfile, validateListEventsOptions, validatePurgeRunsOptions };
5013
+ export { ArtifactError, CatalogedArtifactStore, ChannelError, FEVEX_JSON_SCHEMA_PROFILE_VERSION, FevexConfigurationError, FevexRunError, FileSystemArtifactStore, InMemoryArtifactCatalog, InMemoryArtifactStore, InMemoryMemoryStore, InMemoryRunStore, InMemoryToolEmbeddingCache, IntegrationError, PROVIDER_REASONING_UNSUPPORTED, PROVIDER_SCHEMA_UNSUPPORTED, RunPausedError, S3ArtifactStore, SCHEMA_NOT_TRANSPORTABLE, SandboxError, artifactTenantId, assertArtifactId, buildToolEmbeddingIndex, compileFevexJsonSchema, cosineSimilarity, createAgentsMdContextProvider, createDigestContextCompactor, createFevex, createLocalSandbox, createMemoryContextProvider, createModelContextCompactor, defineAgent, defineConnection, defineContextProvider, definePolicy, defineSkill, defineTeam, defineTool, defineWorkflow, deliverChannelRun, estimateContextTokens, handleChannelInput, isDurableRunStore, isProgressiveSkillProvider, isRetainableRunStore, loadSkillFile, loadSkillFiles, resolveContextBudget, sandboxResultToJson, schemaKeywords, searchToolEmbeddingIndex, toToolSpec, toolEmbeddingKey, toolEmbeddingText, validateFevexJsonSchemaProfile, validateListEventsOptions, validatePurgeRunsOptions };