@combycode/llm-sdk 2.0.0 → 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.
@@ -2776,12 +2776,15 @@ var NetworkEngine = class {
2776
2776
  hooks;
2777
2777
  fetchFn;
2778
2778
  connectFn;
2779
+ /** Engine-wide retry policy, inherited by every queue created from here. */
2780
+ defaultRetry;
2779
2781
  settings = /* @__PURE__ */ new Map();
2780
2782
  queues = /* @__PURE__ */ new Map();
2781
2783
  constructor(config) {
2782
2784
  this.hooks = config?.hooks ?? new HookBus();
2783
2785
  this.fetchFn = config?.fetch ?? globalThis.fetch.bind(globalThis);
2784
2786
  this.connectFn = config?.connect ?? defaultConnectFn;
2787
+ this.defaultRetry = config?.retry;
2785
2788
  if (config?.queues) {
2786
2789
  for (const [name, settings] of Object.entries(config.queues)) {
2787
2790
  this.settings.set(name, settings);
@@ -2863,12 +2866,18 @@ var NetworkEngine = class {
2863
2866
  ...FALLBACK_LIMITS,
2864
2867
  ...settings.limits
2865
2868
  };
2869
+ const retry = this.defaultRetry || settings.retry ? {
2870
+ ...this.defaultRetry,
2871
+ ...settings.retry,
2872
+ ...this.defaultRetry?.backoff || settings.retry?.backoff ? { backoff: { ...this.defaultRetry?.backoff, ...settings.retry?.backoff } } : {},
2873
+ ...this.defaultRetry?.perKind || settings.retry?.perKind ? { perKind: { ...this.defaultRetry?.perKind, ...settings.retry?.perKind } } : {}
2874
+ } : void 0;
2866
2875
  const config = {
2867
2876
  queueName,
2868
2877
  fetch: this.fetchFn,
2869
2878
  hooks: this.hooks,
2870
2879
  limits,
2871
- retry: settings.retry,
2880
+ retry,
2872
2881
  queue: settings.queue
2873
2882
  };
2874
2883
  queue = new QueueState(config);
@@ -2945,6 +2954,62 @@ function ensureAdditionalProperties(schema) {
2945
2954
  }
2946
2955
  return result;
2947
2956
  }
2957
+ var ANTHROPIC_UNSUPPORTED = /* @__PURE__ */ new Set([
2958
+ "minimum",
2959
+ "maximum",
2960
+ "exclusiveMinimum",
2961
+ "exclusiveMaximum",
2962
+ "multipleOf",
2963
+ "maxItems"
2964
+ ]);
2965
+ function strictSupport(schema, dialect) {
2966
+ const visit = (node, path) => {
2967
+ if (!node || typeof node !== "object" || Array.isArray(node)) return null;
2968
+ const n = node;
2969
+ const at = path || "(root)";
2970
+ if (typeof n.$ref === "string") return `${at}: '$ref' cannot be verified without resolution`;
2971
+ if (dialect === "anthropic") {
2972
+ for (const key of Object.keys(n)) {
2973
+ if (ANTHROPIC_UNSUPPORTED.has(key)) return `${at}: '${key}' is not supported under strict`;
2974
+ }
2975
+ }
2976
+ const props = n.properties;
2977
+ if (n.additionalProperties !== void 0 && n.additionalProperties !== false) {
2978
+ return `${at}: 'additionalProperties' must be false under strict`;
2979
+ }
2980
+ if (dialect === "openai") {
2981
+ if (n.type === "object" && props === void 0) {
2982
+ return `${at}: an object schema with no 'properties' cannot be strict (a free-form object is not expressible)`;
2983
+ }
2984
+ }
2985
+ if (props && typeof props === "object") {
2986
+ if (dialect === "openai") {
2987
+ const required = new Set(Array.isArray(n.required) ? n.required : []);
2988
+ const missing = Object.keys(props).filter((k) => !required.has(k));
2989
+ if (missing.length > 0) return `${at}: ${missing.join(", ")} not listed in 'required'`;
2990
+ }
2991
+ for (const [key, val] of Object.entries(props)) {
2992
+ const r = visit(val, path ? `${path}.${key}` : key);
2993
+ if (r) return r;
2994
+ }
2995
+ }
2996
+ for (const key of ["items", "additionalProperties"]) {
2997
+ const r = visit(n[key], path ? `${path}.${key}` : key);
2998
+ if (r) return r;
2999
+ }
3000
+ for (const key of ["anyOf", "oneOf", "allOf"]) {
3001
+ const branches = n[key];
3002
+ if (!Array.isArray(branches)) continue;
3003
+ for (const [i, sub] of branches.entries()) {
3004
+ const r = visit(sub, `${at}.${key}[${i}]`);
3005
+ if (r) return r;
3006
+ }
3007
+ }
3008
+ return null;
3009
+ };
3010
+ const reason = visit(schema, "");
3011
+ return reason ? { ok: false, reason } : { ok: true };
3012
+ }
2948
3013
 
2949
3014
  // src/llm/providers/anthropic/catalog.json
2950
3015
  var catalog_default = {
@@ -26058,12 +26123,16 @@ var AnthropicAdapter = class {
26058
26123
  }
26059
26124
  return null;
26060
26125
  }
26126
+ const strict = t.strict === true;
26127
+ const params = ensureAdditionalProperties(t.parameters);
26061
26128
  const tool = {
26062
26129
  name: t.name,
26063
26130
  description: t.description,
26064
- input_schema: t.parameters
26131
+ // Only the strict path was measured with `additionalProperties: false`
26132
+ // applied; without strict the schema goes out untouched, as before.
26133
+ input_schema: strict ? params : t.parameters
26065
26134
  };
26066
- if (t.strict) tool.strict = true;
26135
+ if (strict) tool.strict = true;
26067
26136
  if ((t.cache || shouldCacheTools) && i === req.tools.length - 1) {
26068
26137
  tool.cache_control = { type: "ephemeral" };
26069
26138
  }
@@ -28112,15 +28181,19 @@ var OpenAIAdapter = class {
28112
28181
  };
28113
28182
  }
28114
28183
  if (req.tools?.length) {
28115
- body.tools = req.tools.filter(isFunctionTool).map((t) => ({
28116
- type: "function",
28117
- function: {
28118
- name: t.name,
28119
- description: t.description,
28120
- parameters: t.parameters,
28121
- ...t.strict ? { strict: true } : {}
28122
- }
28123
- }));
28184
+ body.tools = req.tools.filter(isFunctionTool).map((t) => {
28185
+ const params = ensureAdditionalProperties(t.parameters);
28186
+ const strict = t.strict === true;
28187
+ return {
28188
+ type: "function",
28189
+ function: {
28190
+ name: t.name,
28191
+ description: t.description,
28192
+ parameters: strict ? params : t.parameters,
28193
+ ...strict ? { strict: true } : {}
28194
+ }
28195
+ };
28196
+ });
28124
28197
  }
28125
28198
  if (req.toolChoice) {
28126
28199
  if (typeof req.toolChoice === "string") body.tool_choice = req.toolChoice;
@@ -28130,12 +28203,14 @@ var OpenAIAdapter = class {
28130
28203
  body.reasoning = { effort: req.thinking.effort ?? "medium" };
28131
28204
  }
28132
28205
  if (req.structured) {
28206
+ const schema = ensureAdditionalProperties(req.structured.schema);
28207
+ const strict = req.structured.strict ?? strictSupport(schema, "openai").ok;
28133
28208
  body.response_format = {
28134
28209
  type: "json_schema",
28135
28210
  json_schema: {
28136
28211
  name: req.structured.name ?? "response",
28137
- schema: req.structured.schema,
28138
- strict: req.structured.strict ?? true
28212
+ schema: strict ? schema : req.structured.schema,
28213
+ strict
28139
28214
  }
28140
28215
  };
28141
28216
  }
@@ -29035,12 +29110,13 @@ var OpenAIResponsesAdapter = class {
29035
29110
  if (req.tools?.length) {
29036
29111
  body.tools = req.tools.map((t) => {
29037
29112
  if (isFunctionTool(t)) {
29113
+ const params = ensureAdditionalProperties(t.parameters);
29038
29114
  return {
29039
29115
  type: "function",
29040
29116
  name: t.name,
29041
29117
  description: t.description,
29042
- parameters: ensureAdditionalProperties(t.parameters),
29043
- strict: t.strict ?? true,
29118
+ parameters: params,
29119
+ strict: t.strict ?? strictSupport(params, "openai").ok,
29044
29120
  // Programmatic tool calling (Responses): who may call it + return schema.
29045
29121
  ...t.allowedCallers ? { allowed_callers: t.allowedCallers } : {},
29046
29122
  ...t.outputSchema ? { output_schema: t.outputSchema } : {}
@@ -29061,12 +29137,13 @@ var OpenAIResponsesAdapter = class {
29061
29137
  }
29062
29138
  }
29063
29139
  if (req.structured) {
29140
+ const schema = ensureAdditionalProperties(req.structured.schema);
29064
29141
  body.text = {
29065
29142
  format: {
29066
29143
  type: "json_schema",
29067
29144
  name: req.structured.name ?? "response",
29068
- schema: ensureAdditionalProperties(req.structured.schema),
29069
- strict: req.structured.strict ?? true
29145
+ schema,
29146
+ strict: req.structured.strict ?? strictSupport(schema, "openai").ok
29070
29147
  }
29071
29148
  };
29072
29149
  }
@@ -30660,7 +30737,9 @@ var LAYER_MEMORY = "memory";
30660
30737
  var LAYER_CHAT_FACTS = "chat.facts";
30661
30738
  var LAYER_EXECUTOR_TOOL_EXAMPLES = "executor.tool-examples";
30662
30739
  var LAYER_CONTEXT_GUARD_SUMMARY = "context-guard.summary";
30740
+ var LAYER_LAZY_TOOLS = "agentloop.lazy-tools";
30663
30741
  var PRIORITY_AGENTLOOP_SYSTEM = 10;
30742
+ var PRIORITY_LAZY_TOOLS = 20;
30664
30743
  var PRIORITY_LEGACY_SYSTEM = 50;
30665
30744
  var PRIORITY_AGENTLOOP_CONTEXT = 100;
30666
30745
  var PRIORITY_MEMORY = 200;
@@ -30678,6 +30757,17 @@ function writeAgentLoopSystem(registry, text, owner) {
30678
30757
  owner
30679
30758
  });
30680
30759
  }
30760
+ function writeLazyToolsProtocol(registry, active, owner) {
30761
+ if (!active) {
30762
+ registry.remove(LAYER_LAZY_TOOLS);
30763
+ return;
30764
+ }
30765
+ registry.set(
30766
+ LAYER_LAZY_TOOLS,
30767
+ "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.",
30768
+ { priority: PRIORITY_LAZY_TOOLS, tags: ["system"], owner }
30769
+ );
30770
+ }
30681
30771
  function writeAgentLoopContext(registry, text, owner) {
30682
30772
  if (!text) {
30683
30773
  registry.remove(LAYER_AGENTLOOP_CONTEXT);
@@ -31029,6 +31119,175 @@ ${text}` : text;
31029
31119
  }
31030
31120
  };
31031
31121
 
31122
+ // src/agent/lazy-tools.ts
31123
+ var DEFAULT_LIMIT = 5;
31124
+ var MAX_LIMIT = 20;
31125
+ var DEFAULT_MAX_SEARCHES = 5;
31126
+ var LAZY_SEARCH_TOOL = "tool_search";
31127
+ var LAZY_CALL_TOOL = "call_tool";
31128
+ var STOP_WORDS = /* @__PURE__ */ new Set([
31129
+ "the",
31130
+ "a",
31131
+ "an",
31132
+ "of",
31133
+ "for",
31134
+ "to",
31135
+ "in",
31136
+ "on",
31137
+ "and",
31138
+ "or",
31139
+ "is",
31140
+ "it",
31141
+ "that",
31142
+ "this",
31143
+ "with",
31144
+ "return",
31145
+ "returns",
31146
+ "my",
31147
+ "me",
31148
+ "do",
31149
+ "we",
31150
+ "i",
31151
+ "how",
31152
+ "many",
31153
+ "much",
31154
+ "what",
31155
+ "when",
31156
+ "has",
31157
+ "have",
31158
+ "need",
31159
+ "any",
31160
+ "get",
31161
+ "can",
31162
+ "you",
31163
+ "are",
31164
+ "was",
31165
+ "been",
31166
+ "does",
31167
+ "did",
31168
+ "should",
31169
+ "from",
31170
+ "by",
31171
+ "at",
31172
+ "as",
31173
+ "be"
31174
+ ]);
31175
+ function tokenize(s) {
31176
+ return s.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w));
31177
+ }
31178
+ var isFn = (t) => "name" in t;
31179
+ var nameOf = (t) => isFn(t.definition) ? t.definition.name : "";
31180
+ function rankTools(query, candidates, limit) {
31181
+ const q = new Set(tokenize(query));
31182
+ if (q.size === 0) return [];
31183
+ const scored = [];
31184
+ for (const tool of candidates) {
31185
+ const def = tool.definition;
31186
+ if (!isFn(def)) continue;
31187
+ const props = Object.keys(
31188
+ def.parameters?.properties ?? {}
31189
+ );
31190
+ let score = 0;
31191
+ for (const w of tokenize(`${def.name} ${def.description ?? ""} ${props.join(" ")}`)) {
31192
+ if (q.has(w)) score++;
31193
+ }
31194
+ for (const w of tokenize(def.name)) if (q.has(w)) score += 2;
31195
+ if (score > 0) scored.push({ tool, score });
31196
+ }
31197
+ return scored.sort((a, b) => b.score - a.score).slice(0, limit).map((s) => s.tool);
31198
+ }
31199
+ function createLazyTools(deps) {
31200
+ const limit = Math.min(deps.config.limit ?? DEFAULT_LIMIT, MAX_LIMIT);
31201
+ const maxSearches = deps.config.maxSearches ?? DEFAULT_MAX_SEARCHES;
31202
+ const search = {
31203
+ definition: {
31204
+ type: "function",
31205
+ name: LAZY_SEARCH_TOOL,
31206
+ 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.",
31207
+ parameters: {
31208
+ type: "object",
31209
+ properties: {
31210
+ queries: {
31211
+ type: "array",
31212
+ items: { type: "string" },
31213
+ description: "One phrase per capability you need, in your own words."
31214
+ }
31215
+ },
31216
+ required: ["queries"]
31217
+ }
31218
+ },
31219
+ execute: async (args) => {
31220
+ deps.state.searches++;
31221
+ if (deps.state.searches > maxSearches) {
31222
+ return JSON.stringify({
31223
+ error: `Search budget exhausted (${maxSearches} searches per run). Use the tools you already found.`
31224
+ });
31225
+ }
31226
+ const raw = args.queries;
31227
+ const queries = (Array.isArray(raw) ? raw : [raw]).filter((q) => typeof q === "string" && q.trim().length > 0);
31228
+ if (queries.length === 0) {
31229
+ return JSON.stringify({ tools: [], error: "Pass at least one query string in `queries`." });
31230
+ }
31231
+ const candidates = deps.lazyTools();
31232
+ const hits = /* @__PURE__ */ new Map();
31233
+ const unmatched = [];
31234
+ for (const q of queries) {
31235
+ const found = rankTools(q, candidates, limit);
31236
+ if (found.length === 0) unmatched.push(q);
31237
+ for (const t of found) hits.set(nameOf(t), t);
31238
+ }
31239
+ deps.onSearch?.({ queries, matched: [...hits.keys()], unmatched });
31240
+ return JSON.stringify({
31241
+ tools: [...hits.values()].map((t) => t.definition),
31242
+ ...unmatched.length > 0 ? {
31243
+ unmatched,
31244
+ hint: "These queries matched no tool. Search again for them using different words, or tell the user the capability is unavailable."
31245
+ } : {}
31246
+ });
31247
+ }
31248
+ };
31249
+ const call = {
31250
+ definition: {
31251
+ type: "function",
31252
+ name: LAZY_CALL_TOOL,
31253
+ 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.",
31254
+ parameters: {
31255
+ type: "object",
31256
+ properties: {
31257
+ name: { type: "string", description: "Exact tool name from tool_search." },
31258
+ input: {
31259
+ type: "object",
31260
+ description: "That tool's own arguments, as an object.",
31261
+ additionalProperties: true
31262
+ }
31263
+ },
31264
+ required: ["name", "input"]
31265
+ }
31266
+ },
31267
+ execute: async (args, ctx) => {
31268
+ const name = String(args.name ?? "");
31269
+ const target = deps.lazyTools().find((t) => nameOf(t) === name);
31270
+ if (!target) {
31271
+ if (deps.eagerNames().includes(name)) {
31272
+ return `"${name}" is already available as a normal tool \u2014 call it directly, not through ${LAZY_CALL_TOOL}.`;
31273
+ }
31274
+ return `No tool named "${name}". Call ${LAZY_SEARCH_TOOL} first and use a name exactly as returned.`;
31275
+ }
31276
+ const input = args.input;
31277
+ if (input !== void 0 && (typeof input !== "object" || input === null || Array.isArray(input))) {
31278
+ return `\`input\` must be an object of ${name}'s arguments, not ${Array.isArray(input) ? "an array" : typeof input}.`;
31279
+ }
31280
+ return target.execute(input ?? {}, ctx);
31281
+ }
31282
+ };
31283
+ return [search, call];
31284
+ }
31285
+ function unwrapLazyCall(toolName, args) {
31286
+ if (toolName !== LAZY_CALL_TOOL) return null;
31287
+ const inner = args.name;
31288
+ return typeof inner === "string" && inner.length > 0 ? inner : null;
31289
+ }
31290
+
31032
31291
  // src/agent/tool-key.ts
31033
31292
  function toolKey(tool) {
31034
31293
  return isFunctionTool(tool.definition) ? tool.definition.name : tool.definition.type;
@@ -31110,7 +31369,7 @@ function accumulateStreamEvent(event, state) {
31110
31369
  case "text":
31111
31370
  if (event.phase === "commentary") state.stepCommentary += event.text;
31112
31371
  else state.stepText += event.text;
31113
- return { type: "text", text: event.text };
31372
+ return { type: "text", text: event.text, ...event.phase ? { phase: event.phase } : {} };
31114
31373
  case "thinking":
31115
31374
  state.stepThinking += event.text;
31116
31375
  return { type: "thinking", text: event.text };
@@ -31295,6 +31554,13 @@ var AgentLoop = class _AgentLoop {
31295
31554
  _systemThunk = null;
31296
31555
  _context;
31297
31556
  _tools;
31557
+ _lazyConfig = {};
31558
+ /** Per-run search budget, reset at the start of every run. */
31559
+ _lazyState = { searches: 0 };
31560
+ /** Installed on the first `lazy` registration and never removed, so the declared tool
31561
+ * array stays byte-identical for the life of the conversation — which is the entire
31562
+ * reason the design is cheap. */
31563
+ _lazyInstalled = false;
31298
31564
  _history;
31299
31565
  _reports = [];
31300
31566
  _metadata = {};
@@ -31342,6 +31608,7 @@ var AgentLoop = class _AgentLoop {
31342
31608
  this._checkpoint = config.checkpoint ?? null;
31343
31609
  this._collisionPolicy = config.toolNameCollisionPolicy ?? "warn";
31344
31610
  this._reflectRetry = config.reflectAndRetry ? new ReflectAndRetryPolicy(config.reflectAndRetry) : null;
31611
+ this._lazyConfig = config.lazyTools ?? {};
31345
31612
  this._tools = /* @__PURE__ */ new Map();
31346
31613
  for (const t of config.tools ?? []) {
31347
31614
  this.registerTool(t);
@@ -31356,6 +31623,7 @@ var AgentLoop = class _AgentLoop {
31356
31623
  this.id = this._history.id;
31357
31624
  writeAgentLoopSystem(this._history.registry, this._system, "agent-loop");
31358
31625
  writeAgentLoopContext(this._history.registry, this._context, "agent-loop");
31626
+ this.syncLazyProtocol();
31359
31627
  this.hooks.emitSync("onAgentCreate", {
31360
31628
  agentId: this.id,
31361
31629
  clientId: this.client.id,
@@ -31442,6 +31710,7 @@ var AgentLoop = class _AgentLoop {
31442
31710
  });
31443
31711
  }
31444
31712
  this._tools.set(key, tool);
31713
+ if (tool.lazy) this.installLazyTools();
31445
31714
  }
31446
31715
  removeTool(name) {
31447
31716
  this._tools.delete(name);
@@ -32092,22 +32361,61 @@ var AgentLoop = class _AgentLoop {
32092
32361
  metrics,
32093
32362
  trace: runTrace
32094
32363
  });
32364
+ const inner = unwrapLazyCall(tc.name, tc.arguments);
32095
32365
  reports.push({
32096
32366
  callId: tc.id,
32097
- toolName: tc.name,
32367
+ toolName: inner ?? tc.name,
32098
32368
  arguments: tc.arguments,
32099
32369
  resultSizeBytes: resultStr.length,
32100
32370
  latencyMs,
32101
32371
  skipped: false,
32102
32372
  error: null,
32103
32373
  metrics: Object.fromEntries(metrics),
32374
+ ...inner ? { discoveredVia: "search" } : {},
32104
32375
  ...customData !== void 0 ? { customData } : {}
32105
32376
  });
32106
32377
  return { type: "tool_result", id: tc.id, content: resultStr };
32107
32378
  }
32108
- /** Merge agent's tool definitions with caller-provided tools (caller wins on conflict). */
32379
+ /** Declare `tool_search` + `call_tool`, once, on the first lazy registration.
32380
+ *
32381
+ * They go through `registerTool` like anything else, so the collision policy covers
32382
+ * them and there is no second registry to keep in sync. They are never removed: the
32383
+ * declared array must stay identical for the whole conversation or the cached prefix
32384
+ * is invalidated, which is the cost the feature exists to avoid. */
32385
+ installLazyTools() {
32386
+ if (this._lazyInstalled) return;
32387
+ this._lazyInstalled = true;
32388
+ for (const t of createLazyTools({
32389
+ lazyTools: () => [...this._tools.values()].filter((t2) => t2.lazy),
32390
+ eagerNames: () => [...this._tools.entries()].filter(([, t2]) => !t2.lazy).map(([key]) => key),
32391
+ state: this._lazyState,
32392
+ config: this._lazyConfig,
32393
+ onSearch: (info) => {
32394
+ void this.hooks.emit("onToolSearch", { agentId: this.id, ...info });
32395
+ }
32396
+ })) {
32397
+ this.registerTool(t);
32398
+ }
32399
+ this.syncLazyProtocol();
32400
+ }
32401
+ /** Publish (or remove) the "your tools are not all listed" layer.
32402
+ *
32403
+ * Separate from `installLazyTools` because tools are registered in the constructor
32404
+ * BEFORE `_history` exists, and the layer lives in the history's registry. The
32405
+ * constructor calls this again once history is built.
32406
+ *
32407
+ * The model has no reason to suspect a tool it cannot see, and the failure without
32408
+ * this is quiet — it answers from whatever it did find. Measured at 8/12 and 9/12
32409
+ * without the protocol, 18/18 with it, same tasks and same ranker. */
32410
+ syncLazyProtocol() {
32411
+ if (!this._history) return;
32412
+ writeLazyToolsProtocol(this._history.registry, this._lazyInstalled, "agent-loop");
32413
+ }
32414
+ /** Merge agent's tool definitions with caller-provided tools (caller wins on conflict).
32415
+ *
32416
+ * Lazy tools are registered but NOT declared — that filter is the whole mechanism. */
32109
32417
  toolDefinitions(options) {
32110
- const own = [...this._tools.values()].map((t) => t.definition);
32418
+ const own = [...this._tools.values()].filter((t) => !t.lazy).map((t) => t.definition);
32111
32419
  if (options.tools) return [...own, ...options.tools];
32112
32420
  return own.length > 0 ? own : void 0;
32113
32421
  }
@@ -32117,6 +32425,7 @@ var AgentLoop = class _AgentLoop {
32117
32425
  this._running = true;
32118
32426
  this._stopRequested = false;
32119
32427
  this._abortController = new AbortController();
32428
+ this._lazyState.searches = 0;
32120
32429
  if (this._systemThunk) {
32121
32430
  const next = await this._systemThunk();
32122
32431
  if (next !== this._system) {
@@ -33090,9 +33399,16 @@ function summarize(entries) {
33090
33399
  reasoning: 0,
33091
33400
  total: 0,
33092
33401
  tokens: { input: 0, output: 0, cached: 0, cacheWrite: 0, reasoning: 0 },
33093
- entries: entries.length
33402
+ entries: entries.length,
33403
+ unpriced: 0,
33404
+ unpricedModels: []
33094
33405
  };
33095
33406
  for (const e of entries) {
33407
+ if (e.cost.source === "unknown") {
33408
+ s.unpriced++;
33409
+ const key = `${e.provider}/${e.model}`;
33410
+ if (!s.unpricedModels.includes(key)) s.unpricedModels.push(key);
33411
+ }
33096
33412
  s.input += e.cost.input;
33097
33413
  s.output += e.cost.output;
33098
33414
  s.cacheRead += e.cost.cacheRead;
@@ -33118,6 +33434,8 @@ var CostCollector = class {
33118
33434
  budgets = [];
33119
33435
  triggeredThresholds = /* @__PURE__ */ new Map();
33120
33436
  _runningTotal = 0;
33437
+ /** Models already reported as unpriced, so the warning fires once rather than per call. */
33438
+ warnedUnpriced = /* @__PURE__ */ new Set();
33121
33439
  watchedAgents = /* @__PURE__ */ new Set();
33122
33440
  unsub = null;
33123
33441
  unsubMedia = null;
@@ -33250,6 +33568,7 @@ var CostCollector = class {
33250
33568
  };
33251
33569
  this.ledger.push(entry);
33252
33570
  this._runningTotal += cost.total;
33571
+ this.noteIfUnpriced(entry);
33253
33572
  this.hooks.emitSync("onCostEntry", { entry, runningTotal: this._runningTotal });
33254
33573
  this.checkBudgets(entry);
33255
33574
  }
@@ -33295,9 +33614,31 @@ var CostCollector = class {
33295
33614
  };
33296
33615
  this.ledger.push(entry);
33297
33616
  this._runningTotal += cost.total;
33617
+ this.noteIfUnpriced(entry);
33298
33618
  this.hooks.emitSync("onCostEntry", { entry, runningTotal: this._runningTotal });
33299
33619
  this.checkBudgets(entry);
33300
33620
  }
33621
+ /** A total of exactly 0 because the model is not in the catalog reads identically to a
33622
+ * total of 0 because the call was free — and it silently under-counts every budget
33623
+ * and report built on it. `source: 'unknown'` already records the difference per
33624
+ * entry, but nothing aggregated it, so a whole benchmark run once reported $0.00000
33625
+ * for a live provider and looked like a free arm.
33626
+ *
33627
+ * Fires once per provider/model: an unpriced model is a configuration fact, not a
33628
+ * per-request event, and repeating it on every call would train the reader to ignore
33629
+ * it. Free calls are priced 'calculated' with an explicit note, so they stay silent. */
33630
+ noteIfUnpriced(entry) {
33631
+ if (entry.cost.source !== "unknown") return;
33632
+ const key = `${entry.provider}/${entry.model}`;
33633
+ if (this.warnedUnpriced.has(key)) return;
33634
+ this.warnedUnpriced.add(key);
33635
+ this.hooks.emitSync("onWarning", {
33636
+ source: "cost",
33637
+ code: "unpriced_model",
33638
+ 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.`,
33639
+ details: { provider: entry.provider, model: entry.model }
33640
+ });
33641
+ }
33301
33642
  checkBudgets(entry) {
33302
33643
  for (const budget of this.budgets) {
33303
33644
  if (!matchesScope(entry, budget.scope)) continue;
@@ -34342,7 +34683,7 @@ function createEngine(config = {}) {
34342
34683
  const persistence = resolvePersistence(config.persistence);
34343
34684
  const cache2 = resolveCache(config.cache);
34344
34685
  const catalog = resolveCatalog(config.catalog);
34345
- const network = new NetworkEngine({ hooks, fetch: config.fetch });
34686
+ const network = new NetworkEngine({ hooks, fetch: config.fetch, retry: config.retry, queues: config.queues });
34346
34687
  const fetchBound = (req, options) => network.fetch(req, options);
34347
34688
  const fetchStreamBound = (req, options) => network.fetchStream(req, options);
34348
34689
  const connectBound = (req) => network.connect(req);
@@ -37635,6 +37976,7 @@ function defineTool(input) {
37635
37976
  if (!optional.has(key)) required.push(key);
37636
37977
  }
37637
37978
  return {
37979
+ ...input.lazy ? { lazy: true } : {},
37638
37980
  definition: {
37639
37981
  name: input.name,
37640
37982
  description: input.description,
@@ -38123,7 +38465,14 @@ async function complete(opts) {
38123
38465
  temperature: opts.temperature
38124
38466
  });
38125
38467
  res = await loop.complete(input, {
38126
- structured: opts.structured
38468
+ structured: opts.structured,
38469
+ providerOptions: opts.providerOptions,
38470
+ audio: opts.audio,
38471
+ outputModalities: opts.outputModalities,
38472
+ serviceTier,
38473
+ cache: opts.cache,
38474
+ topK: opts.topK,
38475
+ seed: opts.seed
38127
38476
  });
38128
38477
  } else {
38129
38478
  res = await llm.complete(input, {
@@ -38134,12 +38483,16 @@ async function complete(opts) {
38134
38483
  providerOptions: opts.providerOptions,
38135
38484
  audio: opts.audio,
38136
38485
  outputModalities: opts.outputModalities,
38137
- serviceTier
38486
+ serviceTier,
38487
+ cache: opts.cache,
38488
+ topK: opts.topK,
38489
+ seed: opts.seed
38138
38490
  });
38139
38491
  }
38140
38492
  const result = {
38141
38493
  text: res.text,
38142
38494
  response: res,
38495
+ ...res.error ? { error: res.error } : {},
38143
38496
  // Bound to this call's client (same provider/model/key/engine).
38144
38497
  retrieveFile: (file) => llm.retrieveFile(file),
38145
38498
  streamFile: (file) => llm.streamFile(file)
@@ -38485,10 +38838,21 @@ var McpResultCache = class {
38485
38838
  }
38486
38839
  return hit.value;
38487
38840
  }
38488
- /** Store only when the server actually asked for it. Returns whether anything was stored. */
38841
+ /** Store only when the server actually asked for it. Returns whether anything was stored.
38842
+ *
38843
+ * A non-positive `ttlMs` is an instruction, not a missing value: the server is saying *do not
38844
+ * reuse this*. Any entry already held under that key is dropped, so the next `get` re-fetches.
38845
+ * Without the eviction the hint is inert — a server that first said "cache for 60s" and then
38846
+ * says "stale now" would keep being answered from the stale entry for the rest of the original
38847
+ * TTL. Absent hints are different and must stay different: they carry no instruction, so an
38848
+ * existing entry is left alone and pre-2026 servers behave exactly as before. */
38489
38849
  set(key, value, hints, now = Date.now()) {
38490
38850
  const ttl = hints?.ttlMs;
38491
- if (typeof ttl !== "number" || !Number.isFinite(ttl) || ttl <= 0) return false;
38851
+ if (typeof ttl === "number" && Number.isFinite(ttl) && ttl <= 0) {
38852
+ this.entries.delete(key);
38853
+ return false;
38854
+ }
38855
+ if (typeof ttl !== "number" || !Number.isFinite(ttl)) return false;
38492
38856
  this.entries.set(key, {
38493
38857
  value,
38494
38858
  expiresAt: now + ttl,
@@ -39646,11 +40010,23 @@ function mcpPromptToMessages(result) {
39646
40010
  }
39647
40011
  function mcpToolToAgentTool(client, tool, namespace, opts = {}) {
39648
40012
  return {
40013
+ ...opts.lazy ? { lazy: true } : {},
39649
40014
  definition: {
39650
40015
  type: "function",
39651
40016
  name: `${namespace}__${tool.name}`,
39652
40017
  description: tool.description ?? tool.title ?? tool.name,
39653
- parameters: tool.inputSchema ?? { type: "object", properties: {} }
40018
+ parameters: tool.inputSchema ?? { type: "object", properties: {} },
40019
+ // MCP publishes a schema for the tool's structured output and OpenAI
40020
+ // Responses accepts one (`output_schema`), so the model can reason over the
40021
+ // shape it will get back.
40022
+ //
40023
+ // Gated on `validateOutput` because declaring it is a PROMISE, not a hint:
40024
+ // the provider then requires the result to be JSON matching the schema, so
40025
+ // the tool result changes from prose to structured data. Forwarding it
40026
+ // unconditionally would silently reshape every existing MCP tool result —
40027
+ // and did, until a live round trip through OpenAI Responses failed. Anyone
40028
+ // asking for output validation has already opted into that contract.
40029
+ ...opts.validateOutput && tool.outputSchema ? { outputSchema: tool.outputSchema } : {}
39654
40030
  },
39655
40031
  execute: async (args, ctx) => {
39656
40032
  const res = await client.callTool(tool.name, args, ctx.trace);
@@ -39658,6 +40034,9 @@ function mcpToolToAgentTool(client, tool, namespace, opts = {}) {
39658
40034
  const errors = validateJsonSchema(tool.outputSchema, res.structuredContent);
39659
40035
  if (errors.length > 0) return `Tool output failed schema validation: ${errors.slice(0, 5).join("; ")}`;
39660
40036
  }
40037
+ if (opts.validateOutput && tool.outputSchema && res.structuredContent !== void 0 && !res.isError) {
40038
+ return JSON.stringify(res.structuredContent);
40039
+ }
39661
40040
  return mcpContentToResult(res);
39662
40041
  }
39663
40042
  };
@@ -40394,7 +40773,9 @@ async function connectMcp(config, opts = {}) {
40394
40773
  const refresh = async (c) => {
40395
40774
  const defs = await c.listTools();
40396
40775
  tools.length = 0;
40397
- for (const d of defs) tools.push(mcpToolToAgentTool(c, d, ns, { validateOutput: opts.validateOutput }));
40776
+ for (const d of defs) {
40777
+ tools.push(mcpToolToAgentTool(c, d, ns, { validateOutput: opts.validateOutput, lazy: opts.lazy }));
40778
+ }
40398
40779
  };
40399
40780
  const sampler = opts.sampling ? samplingHandler(opts.sampling) : null;
40400
40781
  const capabilities = {};
@@ -42926,6 +43307,8 @@ export {
42926
43307
  LAYER_EXECUTOR_TOOL_EXAMPLES,
42927
43308
  LAYER_LEGACY_SYSTEM,
42928
43309
  LAYER_MEMORY,
43310
+ LAZY_CALL_TOOL,
43311
+ LAZY_SEARCH_TOOL,
42929
43312
  LLMClient,
42930
43313
  LLMError,
42931
43314
  LLM_DEF_KEY,
@@ -43053,6 +43436,7 @@ export {
43053
43436
  defineLLMTool,
43054
43437
  defineTool,
43055
43438
  delegate,
43439
+ describeTool,
43056
43440
  discoverMetadata,
43057
43441
  dispatch,
43058
43442
  embed,
@@ -43114,6 +43498,7 @@ export {
43114
43498
  parseSSEStream,
43115
43499
  parseToolId,
43116
43500
  pcmToWav,
43501
+ rankTools,
43117
43502
  readFactsLayer,
43118
43503
  reflectionGuidance,
43119
43504
  refreshTokens,
@@ -43131,7 +43516,9 @@ export {
43131
43516
  selectVariant,
43132
43517
  shellGlob,
43133
43518
  sniffImageMime,
43519
+ strictSupport,
43134
43520
  submitBatch,
43521
+ toolKey,
43135
43522
  transcribe,
43136
43523
  trimReplacer,
43137
43524
  tryParseToolId,