@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.
- package/CHANGELOG.md +214 -5
- package/dist/agent/context-registry/layers.d.ts +19 -0
- package/dist/agent/lazy-tools.d.ts +85 -0
- package/dist/agent/loop-config.d.ts +9 -0
- package/dist/agent/loop.d.ts +27 -1
- package/dist/agent/types.d.ts +23 -2
- package/dist/bus/hook-map.d.ts +14 -0
- package/dist/helpers/define-tool.d.ts +15 -6
- package/dist/helpers/engine.d.ts +17 -1
- package/dist/helpers/mcp.d.ts +9 -0
- package/dist/helpers/moderate.d.ts +13 -1
- package/dist/helpers/one-shot.d.ts +19 -0
- package/dist/index.browser.js +417 -30
- package/dist/index.d.ts +10 -3
- package/dist/index.js +417 -30
- package/dist/llm/types/schema-utils.d.ts +19 -0
- package/dist/network/engine.d.ts +10 -2
- package/dist/network/queue-state-config.d.ts +10 -2
- package/dist/plugins/cost-collector/collector.d.ts +12 -0
- package/dist/plugins/cost-collector/cost-collector-types.d.ts +8 -0
- package/dist/plugins/mcp/result-cache.d.ts +8 -1
- package/dist/plugins/mcp/tools.d.ts +2 -0
- package/dist/plugins/mcp/types.d.ts +43 -0
- package/package.json +5 -2
package/dist/index.browser.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
28117
|
-
|
|
28118
|
-
|
|
28119
|
-
|
|
28120
|
-
|
|
28121
|
-
|
|
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
|
|
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:
|
|
29043
|
-
strict: t.strict ??
|
|
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
|
|
29069
|
-
strict: req.structured.strict ??
|
|
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
|
-
/**
|
|
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
|
|
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)
|
|
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,
|