@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.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
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
28044
|
-
|
|
28045
|
-
|
|
28046
|
-
|
|
28047
|
-
|
|
28048
|
-
|
|
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
|
|
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:
|
|
28970
|
-
strict: t.strict ??
|
|
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
|
|
28996
|
-
strict: req.structured.strict ??
|
|
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;
|
|
@@ -31037,7 +31296,7 @@ function accumulateStreamEvent(event, state) {
|
|
|
31037
31296
|
case "text":
|
|
31038
31297
|
if (event.phase === "commentary") state.stepCommentary += event.text;
|
|
31039
31298
|
else state.stepText += event.text;
|
|
31040
|
-
return { type: "text", text: event.text };
|
|
31299
|
+
return { type: "text", text: event.text, ...event.phase ? { phase: event.phase } : {} };
|
|
31041
31300
|
case "thinking":
|
|
31042
31301
|
state.stepThinking += event.text;
|
|
31043
31302
|
return { type: "thinking", text: event.text };
|
|
@@ -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
|
-
/**
|
|
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
|
|
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)
|
|
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,
|