@combycode/llm-sdk 2.0.1 → 2.1.0

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.js CHANGED
@@ -2703,12 +2703,15 @@ var NetworkEngine = class {
2703
2703
  hooks;
2704
2704
  fetchFn;
2705
2705
  connectFn;
2706
+ /** Engine-wide retry policy, inherited by every queue created from here. */
2707
+ defaultRetry;
2706
2708
  settings = /* @__PURE__ */ new Map();
2707
2709
  queues = /* @__PURE__ */ new Map();
2708
2710
  constructor(config) {
2709
2711
  this.hooks = config?.hooks ?? new HookBus();
2710
2712
  this.fetchFn = config?.fetch ?? globalThis.fetch.bind(globalThis);
2711
2713
  this.connectFn = config?.connect ?? defaultConnectFn;
2714
+ this.defaultRetry = config?.retry;
2712
2715
  if (config?.queues) {
2713
2716
  for (const [name, settings] of Object.entries(config.queues)) {
2714
2717
  this.settings.set(name, settings);
@@ -2790,12 +2793,18 @@ var NetworkEngine = class {
2790
2793
  ...FALLBACK_LIMITS,
2791
2794
  ...settings.limits
2792
2795
  };
2796
+ const retry = this.defaultRetry || settings.retry ? {
2797
+ ...this.defaultRetry,
2798
+ ...settings.retry,
2799
+ ...this.defaultRetry?.backoff || settings.retry?.backoff ? { backoff: { ...this.defaultRetry?.backoff, ...settings.retry?.backoff } } : {},
2800
+ ...this.defaultRetry?.perKind || settings.retry?.perKind ? { perKind: { ...this.defaultRetry?.perKind, ...settings.retry?.perKind } } : {}
2801
+ } : void 0;
2793
2802
  const config = {
2794
2803
  queueName,
2795
2804
  fetch: this.fetchFn,
2796
2805
  hooks: this.hooks,
2797
2806
  limits,
2798
- retry: settings.retry,
2807
+ retry,
2799
2808
  queue: settings.queue
2800
2809
  };
2801
2810
  queue = new QueueState(config);
@@ -2872,6 +2881,62 @@ function ensureAdditionalProperties(schema) {
2872
2881
  }
2873
2882
  return result;
2874
2883
  }
2884
+ var ANTHROPIC_UNSUPPORTED = /* @__PURE__ */ new Set([
2885
+ "minimum",
2886
+ "maximum",
2887
+ "exclusiveMinimum",
2888
+ "exclusiveMaximum",
2889
+ "multipleOf",
2890
+ "maxItems"
2891
+ ]);
2892
+ function strictSupport(schema, dialect) {
2893
+ const visit = (node, path) => {
2894
+ if (!node || typeof node !== "object" || Array.isArray(node)) return null;
2895
+ const n = node;
2896
+ const at = path || "(root)";
2897
+ if (typeof n.$ref === "string") return `${at}: '$ref' cannot be verified without resolution`;
2898
+ if (dialect === "anthropic") {
2899
+ for (const key of Object.keys(n)) {
2900
+ if (ANTHROPIC_UNSUPPORTED.has(key)) return `${at}: '${key}' is not supported under strict`;
2901
+ }
2902
+ }
2903
+ const props = n.properties;
2904
+ if (n.additionalProperties !== void 0 && n.additionalProperties !== false) {
2905
+ return `${at}: 'additionalProperties' must be false under strict`;
2906
+ }
2907
+ if (dialect === "openai") {
2908
+ if (n.type === "object" && props === void 0) {
2909
+ return `${at}: an object schema with no 'properties' cannot be strict (a free-form object is not expressible)`;
2910
+ }
2911
+ }
2912
+ if (props && typeof props === "object") {
2913
+ if (dialect === "openai") {
2914
+ const required = new Set(Array.isArray(n.required) ? n.required : []);
2915
+ const missing = Object.keys(props).filter((k) => !required.has(k));
2916
+ if (missing.length > 0) return `${at}: ${missing.join(", ")} not listed in 'required'`;
2917
+ }
2918
+ for (const [key, val] of Object.entries(props)) {
2919
+ const r = visit(val, path ? `${path}.${key}` : key);
2920
+ if (r) return r;
2921
+ }
2922
+ }
2923
+ for (const key of ["items", "additionalProperties"]) {
2924
+ const r = visit(n[key], path ? `${path}.${key}` : key);
2925
+ if (r) return r;
2926
+ }
2927
+ for (const key of ["anyOf", "oneOf", "allOf"]) {
2928
+ const branches = n[key];
2929
+ if (!Array.isArray(branches)) continue;
2930
+ for (const [i, sub] of branches.entries()) {
2931
+ const r = visit(sub, `${at}.${key}[${i}]`);
2932
+ if (r) return r;
2933
+ }
2934
+ }
2935
+ return null;
2936
+ };
2937
+ const reason = visit(schema, "");
2938
+ return reason ? { ok: false, reason } : { ok: true };
2939
+ }
2875
2940
 
2876
2941
  // src/llm/providers/anthropic/catalog.json
2877
2942
  var catalog_default = {
@@ -25985,12 +26050,16 @@ var AnthropicAdapter = class {
25985
26050
  }
25986
26051
  return null;
25987
26052
  }
26053
+ const strict = t.strict === true;
26054
+ const params = ensureAdditionalProperties(t.parameters);
25988
26055
  const tool = {
25989
26056
  name: t.name,
25990
26057
  description: t.description,
25991
- input_schema: t.parameters
26058
+ // Only the strict path was measured with `additionalProperties: false`
26059
+ // applied; without strict the schema goes out untouched, as before.
26060
+ input_schema: strict ? params : t.parameters
25992
26061
  };
25993
- if (t.strict) tool.strict = true;
26062
+ if (strict) tool.strict = true;
25994
26063
  if ((t.cache || shouldCacheTools) && i === req.tools.length - 1) {
25995
26064
  tool.cache_control = { type: "ephemeral" };
25996
26065
  }
@@ -28039,15 +28108,19 @@ var OpenAIAdapter = class {
28039
28108
  };
28040
28109
  }
28041
28110
  if (req.tools?.length) {
28042
- body.tools = req.tools.filter(isFunctionTool).map((t) => ({
28043
- type: "function",
28044
- function: {
28045
- name: t.name,
28046
- description: t.description,
28047
- parameters: t.parameters,
28048
- ...t.strict ? { strict: true } : {}
28049
- }
28050
- }));
28111
+ body.tools = req.tools.filter(isFunctionTool).map((t) => {
28112
+ const params = ensureAdditionalProperties(t.parameters);
28113
+ const strict = t.strict === true;
28114
+ return {
28115
+ type: "function",
28116
+ function: {
28117
+ name: t.name,
28118
+ description: t.description,
28119
+ parameters: strict ? params : t.parameters,
28120
+ ...strict ? { strict: true } : {}
28121
+ }
28122
+ };
28123
+ });
28051
28124
  }
28052
28125
  if (req.toolChoice) {
28053
28126
  if (typeof req.toolChoice === "string") body.tool_choice = req.toolChoice;
@@ -28057,12 +28130,14 @@ var OpenAIAdapter = class {
28057
28130
  body.reasoning = { effort: req.thinking.effort ?? "medium" };
28058
28131
  }
28059
28132
  if (req.structured) {
28133
+ const schema = ensureAdditionalProperties(req.structured.schema);
28134
+ const strict = req.structured.strict ?? strictSupport(schema, "openai").ok;
28060
28135
  body.response_format = {
28061
28136
  type: "json_schema",
28062
28137
  json_schema: {
28063
28138
  name: req.structured.name ?? "response",
28064
- schema: req.structured.schema,
28065
- strict: req.structured.strict ?? true
28139
+ schema: strict ? schema : req.structured.schema,
28140
+ strict
28066
28141
  }
28067
28142
  };
28068
28143
  }
@@ -28962,12 +29037,13 @@ var OpenAIResponsesAdapter = class {
28962
29037
  if (req.tools?.length) {
28963
29038
  body.tools = req.tools.map((t) => {
28964
29039
  if (isFunctionTool(t)) {
29040
+ const params = ensureAdditionalProperties(t.parameters);
28965
29041
  return {
28966
29042
  type: "function",
28967
29043
  name: t.name,
28968
29044
  description: t.description,
28969
- parameters: ensureAdditionalProperties(t.parameters),
28970
- strict: t.strict ?? true,
29045
+ parameters: params,
29046
+ strict: t.strict ?? strictSupport(params, "openai").ok,
28971
29047
  // Programmatic tool calling (Responses): who may call it + return schema.
28972
29048
  ...t.allowedCallers ? { allowed_callers: t.allowedCallers } : {},
28973
29049
  ...t.outputSchema ? { output_schema: t.outputSchema } : {}
@@ -28988,12 +29064,13 @@ var OpenAIResponsesAdapter = class {
28988
29064
  }
28989
29065
  }
28990
29066
  if (req.structured) {
29067
+ const schema = ensureAdditionalProperties(req.structured.schema);
28991
29068
  body.text = {
28992
29069
  format: {
28993
29070
  type: "json_schema",
28994
29071
  name: req.structured.name ?? "response",
28995
- schema: ensureAdditionalProperties(req.structured.schema),
28996
- strict: req.structured.strict ?? true
29072
+ schema,
29073
+ strict: req.structured.strict ?? strictSupport(schema, "openai").ok
28997
29074
  }
28998
29075
  };
28999
29076
  }
@@ -30587,7 +30664,9 @@ var LAYER_MEMORY = "memory";
30587
30664
  var LAYER_CHAT_FACTS = "chat.facts";
30588
30665
  var LAYER_EXECUTOR_TOOL_EXAMPLES = "executor.tool-examples";
30589
30666
  var LAYER_CONTEXT_GUARD_SUMMARY = "context-guard.summary";
30667
+ var LAYER_LAZY_TOOLS = "agentloop.lazy-tools";
30590
30668
  var PRIORITY_AGENTLOOP_SYSTEM = 10;
30669
+ var PRIORITY_LAZY_TOOLS = 20;
30591
30670
  var PRIORITY_LEGACY_SYSTEM = 50;
30592
30671
  var PRIORITY_AGENTLOOP_CONTEXT = 100;
30593
30672
  var PRIORITY_MEMORY = 200;
@@ -30605,6 +30684,17 @@ function writeAgentLoopSystem(registry, text, owner) {
30605
30684
  owner
30606
30685
  });
30607
30686
  }
30687
+ function writeLazyToolsProtocol(registry, active, owner) {
30688
+ if (!active) {
30689
+ registry.remove(LAYER_LAZY_TOOLS);
30690
+ return;
30691
+ }
30692
+ registry.set(
30693
+ LAYER_LAZY_TOOLS,
30694
+ "Not all of your tools are listed. Use `tool_search` to find the ones you need \u2014 it returns their exact names and full argument schemas \u2014 then run them with `call_tool`. Search for every capability the request needs in ONE call, passing several queries. If the result reports a query as unmatched, search again with different words before answering; never answer as if a capability you could not find does not matter.",
30695
+ { priority: PRIORITY_LAZY_TOOLS, tags: ["system"], owner }
30696
+ );
30697
+ }
30608
30698
  function writeAgentLoopContext(registry, text, owner) {
30609
30699
  if (!text) {
30610
30700
  registry.remove(LAYER_AGENTLOOP_CONTEXT);
@@ -30956,6 +31046,175 @@ ${text}` : text;
30956
31046
  }
30957
31047
  };
30958
31048
 
31049
+ // src/agent/lazy-tools.ts
31050
+ var DEFAULT_LIMIT = 5;
31051
+ var MAX_LIMIT = 20;
31052
+ var DEFAULT_MAX_SEARCHES = 5;
31053
+ var LAZY_SEARCH_TOOL = "tool_search";
31054
+ var LAZY_CALL_TOOL = "call_tool";
31055
+ var STOP_WORDS = /* @__PURE__ */ new Set([
31056
+ "the",
31057
+ "a",
31058
+ "an",
31059
+ "of",
31060
+ "for",
31061
+ "to",
31062
+ "in",
31063
+ "on",
31064
+ "and",
31065
+ "or",
31066
+ "is",
31067
+ "it",
31068
+ "that",
31069
+ "this",
31070
+ "with",
31071
+ "return",
31072
+ "returns",
31073
+ "my",
31074
+ "me",
31075
+ "do",
31076
+ "we",
31077
+ "i",
31078
+ "how",
31079
+ "many",
31080
+ "much",
31081
+ "what",
31082
+ "when",
31083
+ "has",
31084
+ "have",
31085
+ "need",
31086
+ "any",
31087
+ "get",
31088
+ "can",
31089
+ "you",
31090
+ "are",
31091
+ "was",
31092
+ "been",
31093
+ "does",
31094
+ "did",
31095
+ "should",
31096
+ "from",
31097
+ "by",
31098
+ "at",
31099
+ "as",
31100
+ "be"
31101
+ ]);
31102
+ function tokenize(s) {
31103
+ return s.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w));
31104
+ }
31105
+ var isFn = (t) => "name" in t;
31106
+ var nameOf = (t) => isFn(t.definition) ? t.definition.name : "";
31107
+ function rankTools(query, candidates, limit) {
31108
+ const q = new Set(tokenize(query));
31109
+ if (q.size === 0) return [];
31110
+ const scored = [];
31111
+ for (const tool of candidates) {
31112
+ const def = tool.definition;
31113
+ if (!isFn(def)) continue;
31114
+ const props = Object.keys(
31115
+ def.parameters?.properties ?? {}
31116
+ );
31117
+ let score = 0;
31118
+ for (const w of tokenize(`${def.name} ${def.description ?? ""} ${props.join(" ")}`)) {
31119
+ if (q.has(w)) score++;
31120
+ }
31121
+ for (const w of tokenize(def.name)) if (q.has(w)) score += 2;
31122
+ if (score > 0) scored.push({ tool, score });
31123
+ }
31124
+ return scored.sort((a, b) => b.score - a.score).slice(0, limit).map((s) => s.tool);
31125
+ }
31126
+ function createLazyTools(deps) {
31127
+ const limit = Math.min(deps.config.limit ?? DEFAULT_LIMIT, MAX_LIMIT);
31128
+ const maxSearches = deps.config.maxSearches ?? DEFAULT_MAX_SEARCHES;
31129
+ const search = {
31130
+ definition: {
31131
+ type: "function",
31132
+ name: LAZY_SEARCH_TOOL,
31133
+ description: "Find the tools you need. Returns their exact names and full argument schemas. Pass every capability you need as a separate query in one call.",
31134
+ parameters: {
31135
+ type: "object",
31136
+ properties: {
31137
+ queries: {
31138
+ type: "array",
31139
+ items: { type: "string" },
31140
+ description: "One phrase per capability you need, in your own words."
31141
+ }
31142
+ },
31143
+ required: ["queries"]
31144
+ }
31145
+ },
31146
+ execute: async (args) => {
31147
+ deps.state.searches++;
31148
+ if (deps.state.searches > maxSearches) {
31149
+ return JSON.stringify({
31150
+ error: `Search budget exhausted (${maxSearches} searches per run). Use the tools you already found.`
31151
+ });
31152
+ }
31153
+ const raw = args.queries;
31154
+ const queries = (Array.isArray(raw) ? raw : [raw]).filter((q) => typeof q === "string" && q.trim().length > 0);
31155
+ if (queries.length === 0) {
31156
+ return JSON.stringify({ tools: [], error: "Pass at least one query string in `queries`." });
31157
+ }
31158
+ const candidates = deps.lazyTools();
31159
+ const hits = /* @__PURE__ */ new Map();
31160
+ const unmatched = [];
31161
+ for (const q of queries) {
31162
+ const found = rankTools(q, candidates, limit);
31163
+ if (found.length === 0) unmatched.push(q);
31164
+ for (const t of found) hits.set(nameOf(t), t);
31165
+ }
31166
+ deps.onSearch?.({ queries, matched: [...hits.keys()], unmatched });
31167
+ return JSON.stringify({
31168
+ tools: [...hits.values()].map((t) => t.definition),
31169
+ ...unmatched.length > 0 ? {
31170
+ unmatched,
31171
+ hint: "These queries matched no tool. Search again for them using different words, or tell the user the capability is unavailable."
31172
+ } : {}
31173
+ });
31174
+ }
31175
+ };
31176
+ const call = {
31177
+ definition: {
31178
+ type: "function",
31179
+ name: LAZY_CALL_TOOL,
31180
+ description: "Call one tool returned by tool_search. Pass its exact name and its own arguments as `input`. To use several tools, call this several times in the same turn.",
31181
+ parameters: {
31182
+ type: "object",
31183
+ properties: {
31184
+ name: { type: "string", description: "Exact tool name from tool_search." },
31185
+ input: {
31186
+ type: "object",
31187
+ description: "That tool's own arguments, as an object.",
31188
+ additionalProperties: true
31189
+ }
31190
+ },
31191
+ required: ["name", "input"]
31192
+ }
31193
+ },
31194
+ execute: async (args, ctx) => {
31195
+ const name = String(args.name ?? "");
31196
+ const target = deps.lazyTools().find((t) => nameOf(t) === name);
31197
+ if (!target) {
31198
+ if (deps.eagerNames().includes(name)) {
31199
+ return `"${name}" is already available as a normal tool \u2014 call it directly, not through ${LAZY_CALL_TOOL}.`;
31200
+ }
31201
+ return `No tool named "${name}". Call ${LAZY_SEARCH_TOOL} first and use a name exactly as returned.`;
31202
+ }
31203
+ const input = args.input;
31204
+ if (input !== void 0 && (typeof input !== "object" || input === null || Array.isArray(input))) {
31205
+ return `\`input\` must be an object of ${name}'s arguments, not ${Array.isArray(input) ? "an array" : typeof input}.`;
31206
+ }
31207
+ return target.execute(input ?? {}, ctx);
31208
+ }
31209
+ };
31210
+ return [search, call];
31211
+ }
31212
+ function unwrapLazyCall(toolName, args) {
31213
+ if (toolName !== LAZY_CALL_TOOL) return null;
31214
+ const inner = args.name;
31215
+ return typeof inner === "string" && inner.length > 0 ? inner : null;
31216
+ }
31217
+
30959
31218
  // src/agent/tool-key.ts
30960
31219
  function toolKey(tool) {
30961
31220
  return isFunctionTool(tool.definition) ? tool.definition.name : tool.definition.type;
@@ -31222,6 +31481,13 @@ var AgentLoop = class _AgentLoop {
31222
31481
  _systemThunk = null;
31223
31482
  _context;
31224
31483
  _tools;
31484
+ _lazyConfig = {};
31485
+ /** Per-run search budget, reset at the start of every run. */
31486
+ _lazyState = { searches: 0 };
31487
+ /** Installed on the first `lazy` registration and never removed, so the declared tool
31488
+ * array stays byte-identical for the life of the conversation — which is the entire
31489
+ * reason the design is cheap. */
31490
+ _lazyInstalled = false;
31225
31491
  _history;
31226
31492
  _reports = [];
31227
31493
  _metadata = {};
@@ -31269,6 +31535,7 @@ var AgentLoop = class _AgentLoop {
31269
31535
  this._checkpoint = config.checkpoint ?? null;
31270
31536
  this._collisionPolicy = config.toolNameCollisionPolicy ?? "warn";
31271
31537
  this._reflectRetry = config.reflectAndRetry ? new ReflectAndRetryPolicy(config.reflectAndRetry) : null;
31538
+ this._lazyConfig = config.lazyTools ?? {};
31272
31539
  this._tools = /* @__PURE__ */ new Map();
31273
31540
  for (const t of config.tools ?? []) {
31274
31541
  this.registerTool(t);
@@ -31283,6 +31550,7 @@ var AgentLoop = class _AgentLoop {
31283
31550
  this.id = this._history.id;
31284
31551
  writeAgentLoopSystem(this._history.registry, this._system, "agent-loop");
31285
31552
  writeAgentLoopContext(this._history.registry, this._context, "agent-loop");
31553
+ this.syncLazyProtocol();
31286
31554
  this.hooks.emitSync("onAgentCreate", {
31287
31555
  agentId: this.id,
31288
31556
  clientId: this.client.id,
@@ -31369,6 +31637,7 @@ var AgentLoop = class _AgentLoop {
31369
31637
  });
31370
31638
  }
31371
31639
  this._tools.set(key, tool);
31640
+ if (tool.lazy) this.installLazyTools();
31372
31641
  }
31373
31642
  removeTool(name) {
31374
31643
  this._tools.delete(name);
@@ -32019,22 +32288,61 @@ var AgentLoop = class _AgentLoop {
32019
32288
  metrics,
32020
32289
  trace: runTrace
32021
32290
  });
32291
+ const inner = unwrapLazyCall(tc.name, tc.arguments);
32022
32292
  reports.push({
32023
32293
  callId: tc.id,
32024
- toolName: tc.name,
32294
+ toolName: inner ?? tc.name,
32025
32295
  arguments: tc.arguments,
32026
32296
  resultSizeBytes: resultStr.length,
32027
32297
  latencyMs,
32028
32298
  skipped: false,
32029
32299
  error: null,
32030
32300
  metrics: Object.fromEntries(metrics),
32301
+ ...inner ? { discoveredVia: "search" } : {},
32031
32302
  ...customData !== void 0 ? { customData } : {}
32032
32303
  });
32033
32304
  return { type: "tool_result", id: tc.id, content: resultStr };
32034
32305
  }
32035
- /** Merge agent's tool definitions with caller-provided tools (caller wins on conflict). */
32306
+ /** Declare `tool_search` + `call_tool`, once, on the first lazy registration.
32307
+ *
32308
+ * They go through `registerTool` like anything else, so the collision policy covers
32309
+ * them and there is no second registry to keep in sync. They are never removed: the
32310
+ * declared array must stay identical for the whole conversation or the cached prefix
32311
+ * is invalidated, which is the cost the feature exists to avoid. */
32312
+ installLazyTools() {
32313
+ if (this._lazyInstalled) return;
32314
+ this._lazyInstalled = true;
32315
+ for (const t of createLazyTools({
32316
+ lazyTools: () => [...this._tools.values()].filter((t2) => t2.lazy),
32317
+ eagerNames: () => [...this._tools.entries()].filter(([, t2]) => !t2.lazy).map(([key]) => key),
32318
+ state: this._lazyState,
32319
+ config: this._lazyConfig,
32320
+ onSearch: (info) => {
32321
+ void this.hooks.emit("onToolSearch", { agentId: this.id, ...info });
32322
+ }
32323
+ })) {
32324
+ this.registerTool(t);
32325
+ }
32326
+ this.syncLazyProtocol();
32327
+ }
32328
+ /** Publish (or remove) the "your tools are not all listed" layer.
32329
+ *
32330
+ * Separate from `installLazyTools` because tools are registered in the constructor
32331
+ * BEFORE `_history` exists, and the layer lives in the history's registry. The
32332
+ * constructor calls this again once history is built.
32333
+ *
32334
+ * The model has no reason to suspect a tool it cannot see, and the failure without
32335
+ * this is quiet — it answers from whatever it did find. Measured at 8/12 and 9/12
32336
+ * without the protocol, 18/18 with it, same tasks and same ranker. */
32337
+ syncLazyProtocol() {
32338
+ if (!this._history) return;
32339
+ writeLazyToolsProtocol(this._history.registry, this._lazyInstalled, "agent-loop");
32340
+ }
32341
+ /** Merge agent's tool definitions with caller-provided tools (caller wins on conflict).
32342
+ *
32343
+ * Lazy tools are registered but NOT declared — that filter is the whole mechanism. */
32036
32344
  toolDefinitions(options) {
32037
- const own = [...this._tools.values()].map((t) => t.definition);
32345
+ const own = [...this._tools.values()].filter((t) => !t.lazy).map((t) => t.definition);
32038
32346
  if (options.tools) return [...own, ...options.tools];
32039
32347
  return own.length > 0 ? own : void 0;
32040
32348
  }
@@ -32044,6 +32352,7 @@ var AgentLoop = class _AgentLoop {
32044
32352
  this._running = true;
32045
32353
  this._stopRequested = false;
32046
32354
  this._abortController = new AbortController();
32355
+ this._lazyState.searches = 0;
32047
32356
  if (this._systemThunk) {
32048
32357
  const next = await this._systemThunk();
32049
32358
  if (next !== this._system) {
@@ -33017,9 +33326,16 @@ function summarize(entries) {
33017
33326
  reasoning: 0,
33018
33327
  total: 0,
33019
33328
  tokens: { input: 0, output: 0, cached: 0, cacheWrite: 0, reasoning: 0 },
33020
- entries: entries.length
33329
+ entries: entries.length,
33330
+ unpriced: 0,
33331
+ unpricedModels: []
33021
33332
  };
33022
33333
  for (const e of entries) {
33334
+ if (e.cost.source === "unknown") {
33335
+ s.unpriced++;
33336
+ const key = `${e.provider}/${e.model}`;
33337
+ if (!s.unpricedModels.includes(key)) s.unpricedModels.push(key);
33338
+ }
33023
33339
  s.input += e.cost.input;
33024
33340
  s.output += e.cost.output;
33025
33341
  s.cacheRead += e.cost.cacheRead;
@@ -33045,6 +33361,8 @@ var CostCollector = class {
33045
33361
  budgets = [];
33046
33362
  triggeredThresholds = /* @__PURE__ */ new Map();
33047
33363
  _runningTotal = 0;
33364
+ /** Models already reported as unpriced, so the warning fires once rather than per call. */
33365
+ warnedUnpriced = /* @__PURE__ */ new Set();
33048
33366
  watchedAgents = /* @__PURE__ */ new Set();
33049
33367
  unsub = null;
33050
33368
  unsubMedia = null;
@@ -33177,6 +33495,7 @@ var CostCollector = class {
33177
33495
  };
33178
33496
  this.ledger.push(entry);
33179
33497
  this._runningTotal += cost.total;
33498
+ this.noteIfUnpriced(entry);
33180
33499
  this.hooks.emitSync("onCostEntry", { entry, runningTotal: this._runningTotal });
33181
33500
  this.checkBudgets(entry);
33182
33501
  }
@@ -33222,9 +33541,31 @@ var CostCollector = class {
33222
33541
  };
33223
33542
  this.ledger.push(entry);
33224
33543
  this._runningTotal += cost.total;
33544
+ this.noteIfUnpriced(entry);
33225
33545
  this.hooks.emitSync("onCostEntry", { entry, runningTotal: this._runningTotal });
33226
33546
  this.checkBudgets(entry);
33227
33547
  }
33548
+ /** A total of exactly 0 because the model is not in the catalog reads identically to a
33549
+ * total of 0 because the call was free — and it silently under-counts every budget
33550
+ * and report built on it. `source: 'unknown'` already records the difference per
33551
+ * entry, but nothing aggregated it, so a whole benchmark run once reported $0.00000
33552
+ * for a live provider and looked like a free arm.
33553
+ *
33554
+ * Fires once per provider/model: an unpriced model is a configuration fact, not a
33555
+ * per-request event, and repeating it on every call would train the reader to ignore
33556
+ * it. Free calls are priced 'calculated' with an explicit note, so they stay silent. */
33557
+ noteIfUnpriced(entry) {
33558
+ if (entry.cost.source !== "unknown") return;
33559
+ const key = `${entry.provider}/${entry.model}`;
33560
+ if (this.warnedUnpriced.has(key)) return;
33561
+ this.warnedUnpriced.add(key);
33562
+ this.hooks.emitSync("onWarning", {
33563
+ source: "cost",
33564
+ code: "unpriced_model",
33565
+ message: `No catalog pricing for ${key} \u2014 its cost is reported as 0, which is not the same as free. Check the model id against the catalog, or add pricing for it.`,
33566
+ details: { provider: entry.provider, model: entry.model }
33567
+ });
33568
+ }
33228
33569
  checkBudgets(entry) {
33229
33570
  for (const budget of this.budgets) {
33230
33571
  if (!matchesScope(entry, budget.scope)) continue;
@@ -34269,7 +34610,7 @@ function createEngine(config = {}) {
34269
34610
  const persistence = resolvePersistence(config.persistence);
34270
34611
  const cache2 = resolveCache(config.cache);
34271
34612
  const catalog = resolveCatalog(config.catalog);
34272
- const network = new NetworkEngine({ hooks, fetch: config.fetch });
34613
+ const network = new NetworkEngine({ hooks, fetch: config.fetch, retry: config.retry, queues: config.queues });
34273
34614
  const fetchBound = (req, options) => network.fetch(req, options);
34274
34615
  const fetchStreamBound = (req, options) => network.fetchStream(req, options);
34275
34616
  const connectBound = (req) => network.connect(req);
@@ -37562,6 +37903,7 @@ function defineTool(input) {
37562
37903
  if (!optional.has(key)) required.push(key);
37563
37904
  }
37564
37905
  return {
37906
+ ...input.lazy ? { lazy: true } : {},
37565
37907
  definition: {
37566
37908
  name: input.name,
37567
37909
  description: input.description,
@@ -38050,7 +38392,14 @@ async function complete(opts) {
38050
38392
  temperature: opts.temperature
38051
38393
  });
38052
38394
  res = await loop.complete(input, {
38053
- structured: opts.structured
38395
+ structured: opts.structured,
38396
+ providerOptions: opts.providerOptions,
38397
+ audio: opts.audio,
38398
+ outputModalities: opts.outputModalities,
38399
+ serviceTier,
38400
+ cache: opts.cache,
38401
+ topK: opts.topK,
38402
+ seed: opts.seed
38054
38403
  });
38055
38404
  } else {
38056
38405
  res = await llm.complete(input, {
@@ -38061,12 +38410,16 @@ async function complete(opts) {
38061
38410
  providerOptions: opts.providerOptions,
38062
38411
  audio: opts.audio,
38063
38412
  outputModalities: opts.outputModalities,
38064
- serviceTier
38413
+ serviceTier,
38414
+ cache: opts.cache,
38415
+ topK: opts.topK,
38416
+ seed: opts.seed
38065
38417
  });
38066
38418
  }
38067
38419
  const result = {
38068
38420
  text: res.text,
38069
38421
  response: res,
38422
+ ...res.error ? { error: res.error } : {},
38070
38423
  // Bound to this call's client (same provider/model/key/engine).
38071
38424
  retrieveFile: (file) => llm.retrieveFile(file),
38072
38425
  streamFile: (file) => llm.streamFile(file)
@@ -38412,10 +38765,21 @@ var McpResultCache = class {
38412
38765
  }
38413
38766
  return hit.value;
38414
38767
  }
38415
- /** Store only when the server actually asked for it. Returns whether anything was stored. */
38768
+ /** Store only when the server actually asked for it. Returns whether anything was stored.
38769
+ *
38770
+ * A non-positive `ttlMs` is an instruction, not a missing value: the server is saying *do not
38771
+ * reuse this*. Any entry already held under that key is dropped, so the next `get` re-fetches.
38772
+ * Without the eviction the hint is inert — a server that first said "cache for 60s" and then
38773
+ * says "stale now" would keep being answered from the stale entry for the rest of the original
38774
+ * TTL. Absent hints are different and must stay different: they carry no instruction, so an
38775
+ * existing entry is left alone and pre-2026 servers behave exactly as before. */
38416
38776
  set(key, value, hints, now = Date.now()) {
38417
38777
  const ttl = hints?.ttlMs;
38418
- if (typeof ttl !== "number" || !Number.isFinite(ttl) || ttl <= 0) return false;
38778
+ if (typeof ttl === "number" && Number.isFinite(ttl) && ttl <= 0) {
38779
+ this.entries.delete(key);
38780
+ return false;
38781
+ }
38782
+ if (typeof ttl !== "number" || !Number.isFinite(ttl)) return false;
38419
38783
  this.entries.set(key, {
38420
38784
  value,
38421
38785
  expiresAt: now + ttl,
@@ -39573,11 +39937,23 @@ function mcpPromptToMessages(result) {
39573
39937
  }
39574
39938
  function mcpToolToAgentTool(client, tool, namespace, opts = {}) {
39575
39939
  return {
39940
+ ...opts.lazy ? { lazy: true } : {},
39576
39941
  definition: {
39577
39942
  type: "function",
39578
39943
  name: `${namespace}__${tool.name}`,
39579
39944
  description: tool.description ?? tool.title ?? tool.name,
39580
- parameters: tool.inputSchema ?? { type: "object", properties: {} }
39945
+ parameters: tool.inputSchema ?? { type: "object", properties: {} },
39946
+ // MCP publishes a schema for the tool's structured output and OpenAI
39947
+ // Responses accepts one (`output_schema`), so the model can reason over the
39948
+ // shape it will get back.
39949
+ //
39950
+ // Gated on `validateOutput` because declaring it is a PROMISE, not a hint:
39951
+ // the provider then requires the result to be JSON matching the schema, so
39952
+ // the tool result changes from prose to structured data. Forwarding it
39953
+ // unconditionally would silently reshape every existing MCP tool result —
39954
+ // and did, until a live round trip through OpenAI Responses failed. Anyone
39955
+ // asking for output validation has already opted into that contract.
39956
+ ...opts.validateOutput && tool.outputSchema ? { outputSchema: tool.outputSchema } : {}
39581
39957
  },
39582
39958
  execute: async (args, ctx) => {
39583
39959
  const res = await client.callTool(tool.name, args, ctx.trace);
@@ -39585,6 +39961,9 @@ function mcpToolToAgentTool(client, tool, namespace, opts = {}) {
39585
39961
  const errors = validateJsonSchema(tool.outputSchema, res.structuredContent);
39586
39962
  if (errors.length > 0) return `Tool output failed schema validation: ${errors.slice(0, 5).join("; ")}`;
39587
39963
  }
39964
+ if (opts.validateOutput && tool.outputSchema && res.structuredContent !== void 0 && !res.isError) {
39965
+ return JSON.stringify(res.structuredContent);
39966
+ }
39588
39967
  return mcpContentToResult(res);
39589
39968
  }
39590
39969
  };
@@ -40321,7 +40700,9 @@ async function connectMcp(config, opts = {}) {
40321
40700
  const refresh = async (c) => {
40322
40701
  const defs = await c.listTools();
40323
40702
  tools.length = 0;
40324
- for (const d of defs) tools.push(mcpToolToAgentTool(c, d, ns, { validateOutput: opts.validateOutput }));
40703
+ for (const d of defs) {
40704
+ tools.push(mcpToolToAgentTool(c, d, ns, { validateOutput: opts.validateOutput, lazy: opts.lazy }));
40705
+ }
40325
40706
  };
40326
40707
  const sampler = opts.sampling ? samplingHandler(opts.sampling) : null;
40327
40708
  const capabilities = {};
@@ -42853,6 +43234,8 @@ export {
42853
43234
  LAYER_EXECUTOR_TOOL_EXAMPLES,
42854
43235
  LAYER_LEGACY_SYSTEM,
42855
43236
  LAYER_MEMORY,
43237
+ LAZY_CALL_TOOL,
43238
+ LAZY_SEARCH_TOOL,
42856
43239
  LLMClient,
42857
43240
  LLMError,
42858
43241
  LLM_DEF_KEY,
@@ -42980,6 +43363,7 @@ export {
42980
43363
  defineLLMTool,
42981
43364
  defineTool,
42982
43365
  delegate,
43366
+ describeTool,
42983
43367
  discoverMetadata,
42984
43368
  dispatch,
42985
43369
  embed,
@@ -43041,6 +43425,7 @@ export {
43041
43425
  parseSSEStream,
43042
43426
  parseToolId,
43043
43427
  pcmToWav,
43428
+ rankTools,
43044
43429
  readFactsLayer,
43045
43430
  reflectionGuidance,
43046
43431
  refreshTokens,
@@ -43058,7 +43443,9 @@ export {
43058
43443
  selectVariant,
43059
43444
  shellGlob,
43060
43445
  sniffImageMime,
43446
+ strictSupport,
43061
43447
  submitBatch,
43448
+ toolKey,
43062
43449
  transcribe,
43063
43450
  trimReplacer,
43064
43451
  tryParseToolId,