@combycode/llm-sdk 2.0.1 → 2.2.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 +289 -3
- 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 +28 -0
- package/dist/agent/loop-internals.d.ts +13 -0
- package/dist/agent/loop.d.ts +33 -1
- package/dist/agent/types.d.ts +15 -0
- package/dist/bus/hook-map.d.ts +20 -0
- package/dist/helpers/define-tool.d.ts +15 -6
- package/dist/helpers/engine.d.ts +36 -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 +850 -76
- package/dist/index.d.ts +11 -4
- package/dist/index.js +850 -76
- 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/network/types.d.ts +6 -0
- 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/dist/plugins/telemetry/telemetry.d.ts +144 -0
- package/dist/types/request-context.d.ts +11 -0
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -114,6 +114,86 @@ var REDACTED = "***REDACTED***";
|
|
|
114
114
|
var SENSITIVE_QUERY_PARAMS = /* @__PURE__ */ new Set(["key", "api_key", "access_token", "token"]);
|
|
115
115
|
var SENSITIVE_HEADERS = /* @__PURE__ */ new Set(["authorization", "x-goog-api-key", "x-api-key", "api-key"]);
|
|
116
116
|
var MAX_ERROR_RAW_CHARS = 512;
|
|
117
|
+
var OTLP_SPAN_KIND = { internal: 1, client: 3 };
|
|
118
|
+
var OTLP_KIND_BY_SPAN = {
|
|
119
|
+
llm: OTLP_SPAN_KIND.client,
|
|
120
|
+
http: OTLP_SPAN_KIND.client,
|
|
121
|
+
mcp: OTLP_SPAN_KIND.client,
|
|
122
|
+
media: OTLP_SPAN_KIND.client,
|
|
123
|
+
agent: OTLP_SPAN_KIND.internal,
|
|
124
|
+
tool: OTLP_SPAN_KIND.internal,
|
|
125
|
+
other: OTLP_SPAN_KIND.internal
|
|
126
|
+
};
|
|
127
|
+
function fnv1a32(input, seed) {
|
|
128
|
+
let h = seed >>> 0;
|
|
129
|
+
for (let i = 0; i < input.length; i++) {
|
|
130
|
+
h ^= input.charCodeAt(i);
|
|
131
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
132
|
+
}
|
|
133
|
+
return h >>> 0;
|
|
134
|
+
}
|
|
135
|
+
var isHex = (value, chars) => value.length === chars && /^[0-9a-f]+$/.test(value);
|
|
136
|
+
function toOtlpId(input, bytes) {
|
|
137
|
+
let out = "";
|
|
138
|
+
for (let i = 0; i < bytes / 4; i++) {
|
|
139
|
+
out += fnv1a32(input, 2166136261 + i * 2654435769 >>> 0).toString(16).padStart(8, "0");
|
|
140
|
+
}
|
|
141
|
+
return /^0+$/.test(out) ? `${out.slice(0, -1)}1` : out;
|
|
142
|
+
}
|
|
143
|
+
function toOtlpValue(value) {
|
|
144
|
+
if (typeof value === "boolean") return { boolValue: value };
|
|
145
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
146
|
+
return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
|
|
147
|
+
}
|
|
148
|
+
if (typeof value === "string") return { stringValue: value };
|
|
149
|
+
if (value === null || value === void 0) return { stringValue: "" };
|
|
150
|
+
return { stringValue: typeof value === "object" ? JSON.stringify(value) : String(value) };
|
|
151
|
+
}
|
|
152
|
+
var SPAN_NAME_SUBJECT = {
|
|
153
|
+
chat: "gen_ai.request.model",
|
|
154
|
+
invoke_agent: "gen_ai.agent.name",
|
|
155
|
+
execute_tool: "gen_ai.tool.name"
|
|
156
|
+
};
|
|
157
|
+
function otlpSpanName(span) {
|
|
158
|
+
const op = span.attributes["gen_ai.operation.name"];
|
|
159
|
+
if (typeof op !== "string") return span.name;
|
|
160
|
+
const subject = SPAN_NAME_SUBJECT[op] ? span.attributes[SPAN_NAME_SUBJECT[op]] : void 0;
|
|
161
|
+
return typeof subject === "string" && subject ? `${op} ${subject}` : op;
|
|
162
|
+
}
|
|
163
|
+
function toMessageList(payload, defaultRole) {
|
|
164
|
+
if (payload == null) return [];
|
|
165
|
+
if (typeof payload === "string") {
|
|
166
|
+
return payload ? [{ role: defaultRole, content: payload }] : [];
|
|
167
|
+
}
|
|
168
|
+
if (Array.isArray(payload)) {
|
|
169
|
+
const parts2 = payload;
|
|
170
|
+
if (parts2.length > 0 && parts2[0] && "role" in parts2[0]) {
|
|
171
|
+
return parts2.map((m) => ({ role: String(m.role ?? defaultRole), content: contentToText(m.content) })).filter((m) => m.content);
|
|
172
|
+
}
|
|
173
|
+
const text2 = contentToText(parts2);
|
|
174
|
+
return text2 ? [{ role: defaultRole, content: text2 }] : [];
|
|
175
|
+
}
|
|
176
|
+
const text = contentToText(payload);
|
|
177
|
+
return text ? [{ role: defaultRole, content: text }] : [];
|
|
178
|
+
}
|
|
179
|
+
function contentToText(content) {
|
|
180
|
+
if (typeof content === "string") return content;
|
|
181
|
+
if (!Array.isArray(content)) return "";
|
|
182
|
+
return content.map((part) => {
|
|
183
|
+
const p = part;
|
|
184
|
+
return typeof p?.text === "string" ? p.text : "";
|
|
185
|
+
}).filter(Boolean).join("");
|
|
186
|
+
}
|
|
187
|
+
var EVENT_TYPE_BY_KIND = {
|
|
188
|
+
agent: "agent",
|
|
189
|
+
tool: "tool",
|
|
190
|
+
llm: "llm",
|
|
191
|
+
http: "http",
|
|
192
|
+
mcp: "mcp",
|
|
193
|
+
media: "media",
|
|
194
|
+
other: "other"
|
|
195
|
+
};
|
|
196
|
+
var SAMPLE_SEED = 2654435769;
|
|
117
197
|
var CATEGORY = {
|
|
118
198
|
// Network
|
|
119
199
|
onEnqueue: "network",
|
|
@@ -182,10 +262,29 @@ function traceIdsOf(ctx) {
|
|
|
182
262
|
const t = c?.trace ?? c?.ctx ?? c;
|
|
183
263
|
return {
|
|
184
264
|
sessionId: t?.sessionId,
|
|
185
|
-
requestId: t?.requestId
|
|
265
|
+
requestId: t?.requestId,
|
|
266
|
+
/** W3C parent context, when the app is already inside a trace of its own. */
|
|
267
|
+
traceparent: t?.traceparent,
|
|
268
|
+
// `gen_ai.conversation.id` in the semantic conventions — the thread a turn
|
|
269
|
+
// belongs to, which is what lets a backend group turns into one conversation.
|
|
270
|
+
// AgentLoop sets it from the history id; a bare client call has none.
|
|
271
|
+
conversationId: t?.conversationId
|
|
186
272
|
};
|
|
187
273
|
}
|
|
188
|
-
|
|
274
|
+
function parseTraceparent(value) {
|
|
275
|
+
if (!value) return null;
|
|
276
|
+
const m = /^[0-9a-f]{2}-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/.exec(value.trim().toLowerCase());
|
|
277
|
+
if (!m) return null;
|
|
278
|
+
const [, traceId, spanId] = m;
|
|
279
|
+
if (/^0+$/.test(traceId) || /^0+$/.test(spanId)) return null;
|
|
280
|
+
return { traceId, spanId };
|
|
281
|
+
}
|
|
282
|
+
var CONTAINER_SPANS = /* @__PURE__ */ new Set(["agent.run", "tool.call"]);
|
|
283
|
+
var traceKey = (ids) => {
|
|
284
|
+
const parent = parseTraceparent(ids.traceparent);
|
|
285
|
+
if (parent) return parent.traceId;
|
|
286
|
+
return ids.requestId ? `${ids.sessionId ?? "?"}:${ids.requestId}` : void 0;
|
|
287
|
+
};
|
|
189
288
|
var TelemetryAdapter = class {
|
|
190
289
|
events = [];
|
|
191
290
|
spans = [];
|
|
@@ -206,17 +305,138 @@ var TelemetryAdapter = class {
|
|
|
206
305
|
/** Service identity stamped on exported telemetry. */
|
|
207
306
|
resource;
|
|
208
307
|
seq = 0;
|
|
308
|
+
/** Discriminator for POINT spans (media, mcp connect/tool), whose natural keys are
|
|
309
|
+
* not unique — the same server reconnects, a run emits two images, two tool calls
|
|
310
|
+
* land in one millisecond. A duplicate span id inside a trace is invalid OTLP and
|
|
311
|
+
* the backend silently keeps only one. */
|
|
312
|
+
spanSeq = 0;
|
|
313
|
+
/** Per trace: the app's span from a `traceparent`, and the CONTAINER spans currently
|
|
314
|
+
* open on it. Together they decide what a new span hangs under — see `parentFor`.
|
|
315
|
+
* Both are cleared once a trace has nothing open, so a long-lived process does not
|
|
316
|
+
* accumulate an entry per conversation forever.
|
|
317
|
+
*
|
|
318
|
+
* A list, not a single slot: an agent nested in a tool call (C2 inside C1's tool) is a
|
|
319
|
+
* second run on the SAME trace, and with one slot it overwrote its own parent and then
|
|
320
|
+
* deleted it on close — leaving the rest of the outer run parentless. */
|
|
321
|
+
appParent = /* @__PURE__ */ new Map();
|
|
322
|
+
containers = /* @__PURE__ */ new Map();
|
|
209
323
|
latSum = 0;
|
|
210
324
|
open = /* @__PURE__ */ new Map();
|
|
211
325
|
maxEvents;
|
|
212
326
|
includeSensitiveData;
|
|
213
327
|
unsub;
|
|
328
|
+
/** Subscribers, each with its own filter. Re-parenting is computed PER SINK: two
|
|
329
|
+
* consumers asking for different types each get a tree that is correct for them. */
|
|
330
|
+
sinks = [];
|
|
331
|
+
content;
|
|
332
|
+
sampleRate;
|
|
333
|
+
/** spanId → its parent and type, for EVERY span including filtered ones — walking up
|
|
334
|
+
* past a dropped ancestor is the whole point, so the dropped ones must still be here.
|
|
335
|
+
* Bounded, because a long-lived process would otherwise remember every span it ever
|
|
336
|
+
* saw. */
|
|
337
|
+
lineage = /* @__PURE__ */ new Map();
|
|
338
|
+
maxLineage;
|
|
339
|
+
msgSeq = 0;
|
|
214
340
|
constructor(hooks, opts = {}) {
|
|
215
341
|
this.maxEvents = opts.maxEvents ?? 2e3;
|
|
216
342
|
this.includeSensitiveData = opts.includeSensitiveData ?? true;
|
|
217
343
|
this.resource = opts.resource ?? { serviceName: "unknown_service" };
|
|
344
|
+
this.content = opts.content ?? "none";
|
|
345
|
+
this.sampleRate = opts.sample ?? 1;
|
|
346
|
+
this.maxLineage = this.maxEvents * 2;
|
|
347
|
+
if (opts.onTrace) this.onTrace({ types: opts.types }, opts.onTrace);
|
|
218
348
|
this.unsub = hooks.onAny((name, ctx) => this.handle(name, ctx));
|
|
219
349
|
}
|
|
350
|
+
onTrace(filterOrHandler, maybeHandler) {
|
|
351
|
+
const handler = typeof filterOrHandler === "function" ? filterOrHandler : maybeHandler;
|
|
352
|
+
if (!handler) throw new Error("onTrace requires a handler");
|
|
353
|
+
const filter = typeof filterOrHandler === "function" ? {} : filterOrHandler;
|
|
354
|
+
const sink = { types: filter.types ? new Set(filter.types) : void 0, handler };
|
|
355
|
+
this.sinks.push(sink);
|
|
356
|
+
return () => {
|
|
357
|
+
const at = this.sinks.indexOf(sink);
|
|
358
|
+
if (at !== -1) this.sinks.splice(at, 1);
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/** Record a finished span and hand it to the subscribers. Every span reaches the store
|
|
362
|
+
* through here, so there is one place where an event can be missed rather than five. */
|
|
363
|
+
recordSpan(span) {
|
|
364
|
+
this.spans.push(span);
|
|
365
|
+
const type = EVENT_TYPE_BY_KIND[span.kind];
|
|
366
|
+
if (!this.lineage.has(span.spanId)) this.remember(span.spanId, span.parentSpanId, type);
|
|
367
|
+
this.dispatch({
|
|
368
|
+
type,
|
|
369
|
+
traceId: span.traceId,
|
|
370
|
+
spanId: span.spanId,
|
|
371
|
+
parentSpanId: span.parentSpanId,
|
|
372
|
+
name: otlpSpanName(span),
|
|
373
|
+
startTime: span.startTime,
|
|
374
|
+
endTime: span.endTime,
|
|
375
|
+
durationMs: span.durationMs,
|
|
376
|
+
status: span.status,
|
|
377
|
+
attributes: span.attributes
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
remember(spanId, parentSpanId, type) {
|
|
381
|
+
this.lineage.set(spanId, { parentSpanId, type });
|
|
382
|
+
if (this.lineage.size > this.maxLineage) {
|
|
383
|
+
const oldest = this.lineage.keys().next().value;
|
|
384
|
+
if (oldest !== void 0) this.lineage.delete(oldest);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
dispatch(event) {
|
|
388
|
+
if (this.sinks.length === 0) return;
|
|
389
|
+
if (!this.isSampled(event.traceId)) return;
|
|
390
|
+
for (const sink of this.sinks) {
|
|
391
|
+
if (sink.types && !sink.types.has(event.type)) continue;
|
|
392
|
+
const parentSpanId = sink.types ? this.survivingParent(event.parentSpanId, sink.types) : event.parentSpanId;
|
|
393
|
+
sink.handler(parentSpanId === event.parentSpanId ? event : { ...event, parentSpanId });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/** The nearest ancestor this subscriber actually receives. Without this, filtering out
|
|
397
|
+
* `http` would leave its children pointing at a span that never arrives, and a backend
|
|
398
|
+
* renders a dangling parent as a separate root. */
|
|
399
|
+
survivingParent(parentSpanId, types) {
|
|
400
|
+
let id = parentSpanId;
|
|
401
|
+
while (id) {
|
|
402
|
+
const node = this.lineage.get(id);
|
|
403
|
+
if (!node) return void 0;
|
|
404
|
+
if (types.has(node.type)) return id;
|
|
405
|
+
id = node.parentSpanId;
|
|
406
|
+
}
|
|
407
|
+
return void 0;
|
|
408
|
+
}
|
|
409
|
+
/** Hashed rather than random, so the same trace samples the same way in every process
|
|
410
|
+
* and a trace shared by two services is kept or dropped by both. */
|
|
411
|
+
isSampled(traceId) {
|
|
412
|
+
if (this.sampleRate >= 1) return true;
|
|
413
|
+
if (this.sampleRate <= 0) return false;
|
|
414
|
+
return fnv1a32(traceId, SAMPLE_SEED) / 4294967296 < this.sampleRate;
|
|
415
|
+
}
|
|
416
|
+
/** Conversation content, as its own event so it can be routed somewhere different from
|
|
417
|
+
* the spans — a debug store, not the metrics backend. */
|
|
418
|
+
emitMessage(span, direction, payload) {
|
|
419
|
+
if (this.sinks.length === 0) return;
|
|
420
|
+
const messages = toMessageList(payload, direction === "input" ? "user" : "assistant");
|
|
421
|
+
if (messages.length === 0) return;
|
|
422
|
+
const chars = messages.reduce((n, m) => n + m.content.length, 0);
|
|
423
|
+
this.dispatch({
|
|
424
|
+
type: "message",
|
|
425
|
+
traceId: span.traceId,
|
|
426
|
+
spanId: `${span.spanId}:msg${this.msgSeq++}`,
|
|
427
|
+
parentSpanId: span.spanId,
|
|
428
|
+
name: `message.${direction}`,
|
|
429
|
+
startTime: Date.now(),
|
|
430
|
+
status: "unset",
|
|
431
|
+
attributes: clean({
|
|
432
|
+
"message.direction": direction,
|
|
433
|
+
"message.count": messages.length,
|
|
434
|
+
"message.chars": chars,
|
|
435
|
+
// Opt-In in the spec, and off by default here for the same reason.
|
|
436
|
+
[`gen_ai.${direction}.messages`]: this.content === "full" ? messages : void 0
|
|
437
|
+
})
|
|
438
|
+
});
|
|
439
|
+
}
|
|
220
440
|
/** Stop tapping the bus. */
|
|
221
441
|
destroy() {
|
|
222
442
|
this.unsub();
|
|
@@ -224,6 +444,8 @@ var TelemetryAdapter = class {
|
|
|
224
444
|
handle(name, ctx) {
|
|
225
445
|
const ids = traceIdsOf(ctx);
|
|
226
446
|
const traceId = traceKey(ids);
|
|
447
|
+
const parent = parseTraceparent(ids.traceparent);
|
|
448
|
+
if (parent && traceId) this.appParent.set(traceId, parent.spanId);
|
|
227
449
|
this.events.push({
|
|
228
450
|
seq: this.seq++,
|
|
229
451
|
time: Date.now(),
|
|
@@ -246,20 +468,27 @@ var TelemetryAdapter = class {
|
|
|
246
468
|
this.metrics.outputTokens += usage.outputTokens ?? 0;
|
|
247
469
|
}
|
|
248
470
|
if (traceId) {
|
|
471
|
+
const responseModel = c.response?.model;
|
|
249
472
|
const attrs = {
|
|
250
|
-
"gen_ai.provider": c.provider,
|
|
251
|
-
"gen_ai.
|
|
473
|
+
"gen_ai.provider.name": c.provider,
|
|
474
|
+
"gen_ai.operation.name": "chat",
|
|
475
|
+
"gen_ai.request.model": c.model,
|
|
476
|
+
// The model that actually answered, which can differ from the one asked
|
|
477
|
+
// for (an alias resolving to a dated snapshot, a router picking a peer).
|
|
478
|
+
"gen_ai.response.model": responseModel,
|
|
479
|
+
"gen_ai.conversation.id": ids.conversationId,
|
|
252
480
|
"gen_ai.usage.input_tokens": usage?.inputTokens,
|
|
253
481
|
"gen_ai.usage.output_tokens": usage?.outputTokens
|
|
254
482
|
};
|
|
255
483
|
const key = `llm:${traceId}`;
|
|
484
|
+
let llmSpan;
|
|
256
485
|
if (this.open.has(key)) {
|
|
257
|
-
this.closeSpan(key, "ok", attrs);
|
|
486
|
+
llmSpan = this.closeSpan(key, "ok", attrs);
|
|
258
487
|
} else {
|
|
259
488
|
const http = [...this.spans].reverse().find((s) => s.traceId === traceId && s.kind === "http");
|
|
260
489
|
const start = http?.startTime ?? Date.now();
|
|
261
490
|
const end = Date.now();
|
|
262
|
-
|
|
491
|
+
llmSpan = {
|
|
263
492
|
traceId,
|
|
264
493
|
spanId: key,
|
|
265
494
|
name: "llm.request",
|
|
@@ -269,7 +498,12 @@ var TelemetryAdapter = class {
|
|
|
269
498
|
durationMs: end - start,
|
|
270
499
|
status: "ok",
|
|
271
500
|
attributes: clean(attrs)
|
|
272
|
-
}
|
|
501
|
+
};
|
|
502
|
+
this.recordSpan(llmSpan);
|
|
503
|
+
}
|
|
504
|
+
if (llmSpan) {
|
|
505
|
+
const response = c.response;
|
|
506
|
+
this.emitMessage(llmSpan, "output", response?.content ?? response?.text);
|
|
273
507
|
}
|
|
274
508
|
}
|
|
275
509
|
break;
|
|
@@ -315,9 +549,11 @@ var TelemetryAdapter = class {
|
|
|
315
549
|
this.metrics.mediaGenerated += c.count ?? 1;
|
|
316
550
|
if (traceId) {
|
|
317
551
|
const now = Date.now();
|
|
318
|
-
this.
|
|
552
|
+
this.recordSpan({
|
|
319
553
|
traceId,
|
|
320
|
-
|
|
554
|
+
// One run can generate several images; `media:${traceId}` would give them
|
|
555
|
+
// all the same span id, which is invalid within a trace.
|
|
556
|
+
spanId: `media:${traceId}:${this.spanSeq++}`,
|
|
321
557
|
name: "media.generate",
|
|
322
558
|
kind: "media",
|
|
323
559
|
startTime: now,
|
|
@@ -332,10 +568,21 @@ var TelemetryAdapter = class {
|
|
|
332
568
|
case "onRunStart": {
|
|
333
569
|
const runId = c.runId;
|
|
334
570
|
if (runId) {
|
|
335
|
-
this.openSpan(`agent:${runId}`, runId, "agent.run", "agent", {
|
|
336
|
-
|
|
337
|
-
|
|
571
|
+
const runSpan = this.openSpan(`agent:${runId}`, traceId ?? runId, "agent.run", "agent", {
|
|
572
|
+
// The host's own attributes go FIRST so ours win on a key collision: a stray
|
|
573
|
+
// `gen_ai.*` key in a caller's bag must not be able to rewrite the identity
|
|
574
|
+
// of the span.
|
|
575
|
+
...c.attributes,
|
|
576
|
+
"gen_ai.operation.name": "invoke_agent",
|
|
577
|
+
// Named when the agent was given a label; the exported span is then
|
|
578
|
+
// `invoke_agent {label}` rather than the bare operation.
|
|
579
|
+
"gen_ai.agent.name": c.label,
|
|
580
|
+
"gen_ai.agent.id": c.agentId,
|
|
581
|
+
"gen_ai.request.model": c.model,
|
|
582
|
+
// Ours, not a convention attribute — the GenAI spec has no term for it.
|
|
583
|
+
"agent.source": c.source
|
|
338
584
|
});
|
|
585
|
+
this.emitMessage(runSpan, "input", c.userMessage);
|
|
339
586
|
}
|
|
340
587
|
break;
|
|
341
588
|
}
|
|
@@ -362,9 +609,11 @@ var TelemetryAdapter = class {
|
|
|
362
609
|
case "onToolCallStart": {
|
|
363
610
|
const callId = c.callId;
|
|
364
611
|
if (callId) {
|
|
365
|
-
this.openSpan(`tool:${callId}`, callId, "tool.call", "tool", {
|
|
366
|
-
"
|
|
367
|
-
"
|
|
612
|
+
this.openSpan(`tool:${callId}`, traceId ?? callId, "tool.call", "tool", {
|
|
613
|
+
"gen_ai.operation.name": "execute_tool",
|
|
614
|
+
"gen_ai.tool.name": c.toolName,
|
|
615
|
+
"gen_ai.tool.call.id": callId,
|
|
616
|
+
"gen_ai.agent.id": c.agentId
|
|
368
617
|
});
|
|
369
618
|
}
|
|
370
619
|
break;
|
|
@@ -373,7 +622,7 @@ var TelemetryAdapter = class {
|
|
|
373
622
|
const callId = c.callId;
|
|
374
623
|
if (callId) {
|
|
375
624
|
this.closeSpan(`tool:${callId}`, "ok", {
|
|
376
|
-
"tool.name": c.toolName,
|
|
625
|
+
"gen_ai.tool.name": c.toolName,
|
|
377
626
|
"tool.latency_ms": c.latencyMs
|
|
378
627
|
});
|
|
379
628
|
}
|
|
@@ -383,7 +632,7 @@ var TelemetryAdapter = class {
|
|
|
383
632
|
const callId = c.callId;
|
|
384
633
|
if (callId) {
|
|
385
634
|
this.closeSpan(`tool:${callId}`, "error", {
|
|
386
|
-
"tool.name": c.toolName,
|
|
635
|
+
"gen_ai.tool.name": c.toolName,
|
|
387
636
|
"tool.error": c.error?.message
|
|
388
637
|
});
|
|
389
638
|
}
|
|
@@ -394,9 +643,17 @@ var TelemetryAdapter = class {
|
|
|
394
643
|
const server = c.server;
|
|
395
644
|
if (server) {
|
|
396
645
|
const now = Date.now();
|
|
397
|
-
this.
|
|
398
|
-
|
|
399
|
-
|
|
646
|
+
this.recordSpan({
|
|
647
|
+
// A connect usually happens at startup, outside any run, so there is often
|
|
648
|
+
// no trace to join — but keying the trace by server name merged every
|
|
649
|
+
// reconnect over the process lifetime into one trace. Falls back to a span
|
|
650
|
+
// of its own instead.
|
|
651
|
+
traceId: traceId ?? `mcp:connect:${server}:${this.spanSeq}`,
|
|
652
|
+
// `${server}` alone repeats on every reconnect, and a duplicate span id
|
|
653
|
+
// within a trace is invalid OTLP — the backend keeps one and drops the
|
|
654
|
+
// rest. The counter is monotonic where a timestamp is not: two connects
|
|
655
|
+
// inside the same millisecond would still collide.
|
|
656
|
+
spanId: `mcp:connect:${server}:${this.spanSeq++}`,
|
|
400
657
|
name: "mcp.connect",
|
|
401
658
|
kind: "mcp",
|
|
402
659
|
startTime: now,
|
|
@@ -418,9 +675,14 @@ var TelemetryAdapter = class {
|
|
|
418
675
|
if (server && tool) {
|
|
419
676
|
const now = Date.now();
|
|
420
677
|
const lat = c.latencyMs;
|
|
421
|
-
this.
|
|
422
|
-
|
|
423
|
-
|
|
678
|
+
this.recordSpan({
|
|
679
|
+
// An MCP tool call happens INSIDE a run, so it belongs to that run's trace.
|
|
680
|
+
// Keying it by server put every call to one server in a single eternal
|
|
681
|
+
// trace, and none of them with the agent that made the call.
|
|
682
|
+
traceId: traceId ?? `mcp:${server}`,
|
|
683
|
+
// A timestamp is not a unique key: two tool calls in the same millisecond
|
|
684
|
+
// share it. The counter is.
|
|
685
|
+
spanId: `mcp:tool:${server}:${tool}:${this.spanSeq++}`,
|
|
424
686
|
name: "mcp.tool_call",
|
|
425
687
|
kind: "mcp",
|
|
426
688
|
startTime: now - (lat ?? 0),
|
|
@@ -438,10 +700,33 @@ var TelemetryAdapter = class {
|
|
|
438
700
|
}
|
|
439
701
|
}
|
|
440
702
|
}
|
|
703
|
+
/** What a new span on this trace hangs under: the innermost container still open on
|
|
704
|
+
* it, else the app's span, else nothing (we are the root).
|
|
705
|
+
*
|
|
706
|
+
* A container wins over the app's span because an LLM call made during a run belongs
|
|
707
|
+
* to that run — attaching it straight to the app would flatten the very nesting the
|
|
708
|
+
* tree exists to show. A span joins the stack only after it is built, so nothing can
|
|
709
|
+
* become its own parent, and a run nested in a tool call lands under that tool call —
|
|
710
|
+
* exactly where it happened.
|
|
711
|
+
*
|
|
712
|
+
* Limit worth naming: with tools running in parallel two `tool.call` spans are open at
|
|
713
|
+
* once and "innermost" is merely the more recent one. Attributing a nested run to the
|
|
714
|
+
* right sibling needs real async context propagation, which this adapter does not
|
|
715
|
+
* have; sequential tools, the common case, are exact. */
|
|
716
|
+
parentFor(traceId) {
|
|
717
|
+
const stack = this.containers.get(traceId);
|
|
718
|
+
return stack?.[stack.length - 1] ?? this.appParent.get(traceId);
|
|
719
|
+
}
|
|
441
720
|
openSpan(key, traceId, spanName, kind, attributes) {
|
|
721
|
+
const parentSpanId = this.parentFor(traceId);
|
|
442
722
|
const span = {
|
|
443
723
|
traceId,
|
|
444
|
-
|
|
724
|
+
...parentSpanId ? { parentSpanId } : {},
|
|
725
|
+
// The KEY pairs open with close (`llm:${traceId}`); the SPAN ID must be unique.
|
|
726
|
+
// Those were the same string until a run stopped fragmenting into one trace per
|
|
727
|
+
// call — at which point every LLM call in a run produced the identical key, and
|
|
728
|
+
// the collision merged them into one span at the collector.
|
|
729
|
+
spanId: `${key}#${this.spanSeq++}`,
|
|
445
730
|
name: spanName,
|
|
446
731
|
kind,
|
|
447
732
|
startTime: Date.now(),
|
|
@@ -449,17 +734,33 @@ var TelemetryAdapter = class {
|
|
|
449
734
|
attributes
|
|
450
735
|
};
|
|
451
736
|
this.open.set(key, span);
|
|
737
|
+
this.remember(span.spanId, parentSpanId, EVENT_TYPE_BY_KIND[kind]);
|
|
738
|
+
if (CONTAINER_SPANS.has(spanName)) {
|
|
739
|
+
const stack = this.containers.get(traceId);
|
|
740
|
+
if (stack) stack.push(span.spanId);
|
|
741
|
+
else this.containers.set(traceId, [span.spanId]);
|
|
742
|
+
}
|
|
452
743
|
return span;
|
|
453
744
|
}
|
|
454
745
|
closeSpan(key, status, attributes) {
|
|
455
746
|
const span = this.open.get(key);
|
|
456
|
-
if (!span) return;
|
|
747
|
+
if (!span) return void 0;
|
|
748
|
+
const stack = this.containers.get(span.traceId);
|
|
749
|
+
if (stack) {
|
|
750
|
+
const at = stack.lastIndexOf(span.spanId);
|
|
751
|
+
if (at !== -1) stack.splice(at, 1);
|
|
752
|
+
if (stack.length === 0) {
|
|
753
|
+
this.containers.delete(span.traceId);
|
|
754
|
+
this.appParent.delete(span.traceId);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
457
757
|
span.endTime = Date.now();
|
|
458
758
|
span.durationMs = span.endTime - span.startTime;
|
|
459
759
|
span.status = status;
|
|
460
760
|
Object.assign(span.attributes, clean(attributes));
|
|
461
761
|
this.open.delete(key);
|
|
462
|
-
this.
|
|
762
|
+
this.recordSpan(span);
|
|
763
|
+
return span;
|
|
463
764
|
}
|
|
464
765
|
recordLatency(ms) {
|
|
465
766
|
if (typeof ms !== "number") return;
|
|
@@ -509,14 +810,27 @@ var TelemetryAdapter = class {
|
|
|
509
810
|
{
|
|
510
811
|
scope: { name: "combycode.telemetry" },
|
|
511
812
|
spans: this.spans.map((s) => ({
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
813
|
+
// An app-supplied trace id is ALREADY a real 32-hex id — hashing it
|
|
814
|
+
// would produce a different trace and defeat the whole point of
|
|
815
|
+
// accepting a parent.
|
|
816
|
+
traceId: isHex(s.traceId, 32) ? s.traceId : toOtlpId(s.traceId, 16),
|
|
817
|
+
// Scoped by trace: two conversations can each hold a span keyed
|
|
818
|
+
// `llm:…`, and colliding their ids would merge unrelated traces.
|
|
819
|
+
spanId: toOtlpId(`${s.traceId}|${s.spanId}`, 8),
|
|
820
|
+
// The app's own span id arrives as hex and passes through; one of ours
|
|
821
|
+
// is hashed exactly as it was when we emitted it, so the link matches.
|
|
822
|
+
...s.parentSpanId ? {
|
|
823
|
+
parentSpanId: isHex(s.parentSpanId, 16) ? s.parentSpanId : toOtlpId(`${s.traceId}|${s.parentSpanId}`, 8)
|
|
824
|
+
} : {},
|
|
825
|
+
name: otlpSpanName(s),
|
|
515
826
|
startTimeUnixNano: Math.round(s.startTime * 1e6),
|
|
516
827
|
endTimeUnixNano: Math.round((s.endTime ?? s.startTime) * 1e6),
|
|
517
|
-
kind: s.kind,
|
|
828
|
+
kind: OTLP_KIND_BY_SPAN[s.kind] ?? OTLP_SPAN_KIND.internal,
|
|
518
829
|
status: { code: s.status === "error" ? 2 : s.status === "ok" ? 1 : 0 },
|
|
519
|
-
attributes: Object.entries(s.attributes).map(([key, value]) => ({
|
|
830
|
+
attributes: Object.entries(s.attributes).map(([key, value]) => ({
|
|
831
|
+
key,
|
|
832
|
+
value: toOtlpValue(value)
|
|
833
|
+
}))
|
|
520
834
|
}))
|
|
521
835
|
}
|
|
522
836
|
]
|
|
@@ -2703,12 +3017,15 @@ var NetworkEngine = class {
|
|
|
2703
3017
|
hooks;
|
|
2704
3018
|
fetchFn;
|
|
2705
3019
|
connectFn;
|
|
3020
|
+
/** Engine-wide retry policy, inherited by every queue created from here. */
|
|
3021
|
+
defaultRetry;
|
|
2706
3022
|
settings = /* @__PURE__ */ new Map();
|
|
2707
3023
|
queues = /* @__PURE__ */ new Map();
|
|
2708
3024
|
constructor(config) {
|
|
2709
3025
|
this.hooks = config?.hooks ?? new HookBus();
|
|
2710
3026
|
this.fetchFn = config?.fetch ?? globalThis.fetch.bind(globalThis);
|
|
2711
3027
|
this.connectFn = config?.connect ?? defaultConnectFn;
|
|
3028
|
+
this.defaultRetry = config?.retry;
|
|
2712
3029
|
if (config?.queues) {
|
|
2713
3030
|
for (const [name, settings] of Object.entries(config.queues)) {
|
|
2714
3031
|
this.settings.set(name, settings);
|
|
@@ -2790,12 +3107,18 @@ var NetworkEngine = class {
|
|
|
2790
3107
|
...FALLBACK_LIMITS,
|
|
2791
3108
|
...settings.limits
|
|
2792
3109
|
};
|
|
3110
|
+
const retry = this.defaultRetry || settings.retry ? {
|
|
3111
|
+
...this.defaultRetry,
|
|
3112
|
+
...settings.retry,
|
|
3113
|
+
...this.defaultRetry?.backoff || settings.retry?.backoff ? { backoff: { ...this.defaultRetry?.backoff, ...settings.retry?.backoff } } : {},
|
|
3114
|
+
...this.defaultRetry?.perKind || settings.retry?.perKind ? { perKind: { ...this.defaultRetry?.perKind, ...settings.retry?.perKind } } : {}
|
|
3115
|
+
} : void 0;
|
|
2793
3116
|
const config = {
|
|
2794
3117
|
queueName,
|
|
2795
3118
|
fetch: this.fetchFn,
|
|
2796
3119
|
hooks: this.hooks,
|
|
2797
3120
|
limits,
|
|
2798
|
-
retry
|
|
3121
|
+
retry,
|
|
2799
3122
|
queue: settings.queue
|
|
2800
3123
|
};
|
|
2801
3124
|
queue = new QueueState(config);
|
|
@@ -2872,6 +3195,62 @@ function ensureAdditionalProperties(schema) {
|
|
|
2872
3195
|
}
|
|
2873
3196
|
return result;
|
|
2874
3197
|
}
|
|
3198
|
+
var ANTHROPIC_UNSUPPORTED = /* @__PURE__ */ new Set([
|
|
3199
|
+
"minimum",
|
|
3200
|
+
"maximum",
|
|
3201
|
+
"exclusiveMinimum",
|
|
3202
|
+
"exclusiveMaximum",
|
|
3203
|
+
"multipleOf",
|
|
3204
|
+
"maxItems"
|
|
3205
|
+
]);
|
|
3206
|
+
function strictSupport(schema, dialect) {
|
|
3207
|
+
const visit = (node, path) => {
|
|
3208
|
+
if (!node || typeof node !== "object" || Array.isArray(node)) return null;
|
|
3209
|
+
const n = node;
|
|
3210
|
+
const at = path || "(root)";
|
|
3211
|
+
if (typeof n.$ref === "string") return `${at}: '$ref' cannot be verified without resolution`;
|
|
3212
|
+
if (dialect === "anthropic") {
|
|
3213
|
+
for (const key of Object.keys(n)) {
|
|
3214
|
+
if (ANTHROPIC_UNSUPPORTED.has(key)) return `${at}: '${key}' is not supported under strict`;
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
const props = n.properties;
|
|
3218
|
+
if (n.additionalProperties !== void 0 && n.additionalProperties !== false) {
|
|
3219
|
+
return `${at}: 'additionalProperties' must be false under strict`;
|
|
3220
|
+
}
|
|
3221
|
+
if (dialect === "openai") {
|
|
3222
|
+
if (n.type === "object" && props === void 0) {
|
|
3223
|
+
return `${at}: an object schema with no 'properties' cannot be strict (a free-form object is not expressible)`;
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
if (props && typeof props === "object") {
|
|
3227
|
+
if (dialect === "openai") {
|
|
3228
|
+
const required = new Set(Array.isArray(n.required) ? n.required : []);
|
|
3229
|
+
const missing = Object.keys(props).filter((k) => !required.has(k));
|
|
3230
|
+
if (missing.length > 0) return `${at}: ${missing.join(", ")} not listed in 'required'`;
|
|
3231
|
+
}
|
|
3232
|
+
for (const [key, val] of Object.entries(props)) {
|
|
3233
|
+
const r = visit(val, path ? `${path}.${key}` : key);
|
|
3234
|
+
if (r) return r;
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
for (const key of ["items", "additionalProperties"]) {
|
|
3238
|
+
const r = visit(n[key], path ? `${path}.${key}` : key);
|
|
3239
|
+
if (r) return r;
|
|
3240
|
+
}
|
|
3241
|
+
for (const key of ["anyOf", "oneOf", "allOf"]) {
|
|
3242
|
+
const branches = n[key];
|
|
3243
|
+
if (!Array.isArray(branches)) continue;
|
|
3244
|
+
for (const [i, sub] of branches.entries()) {
|
|
3245
|
+
const r = visit(sub, `${at}.${key}[${i}]`);
|
|
3246
|
+
if (r) return r;
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
return null;
|
|
3250
|
+
};
|
|
3251
|
+
const reason = visit(schema, "");
|
|
3252
|
+
return reason ? { ok: false, reason } : { ok: true };
|
|
3253
|
+
}
|
|
2875
3254
|
|
|
2876
3255
|
// src/llm/providers/anthropic/catalog.json
|
|
2877
3256
|
var catalog_default = {
|
|
@@ -25082,7 +25461,7 @@ function extractSystem(messages) {
|
|
|
25082
25461
|
const rest = [];
|
|
25083
25462
|
for (const m of messages) {
|
|
25084
25463
|
if (m.role === "system") {
|
|
25085
|
-
const text = typeof m.content === "string" ? m.content :
|
|
25464
|
+
const text = typeof m.content === "string" ? m.content : contentToText2(m.content);
|
|
25086
25465
|
if (text) systemTexts.push(text);
|
|
25087
25466
|
} else {
|
|
25088
25467
|
rest.push(m);
|
|
@@ -25093,7 +25472,7 @@ function extractSystem(messages) {
|
|
|
25093
25472
|
messages: rest
|
|
25094
25473
|
};
|
|
25095
25474
|
}
|
|
25096
|
-
function
|
|
25475
|
+
function contentToText2(content) {
|
|
25097
25476
|
return content.filter((p) => p.type === "text").map((p) => p.text).join("\n");
|
|
25098
25477
|
}
|
|
25099
25478
|
function parseStructured(text) {
|
|
@@ -25375,7 +25754,15 @@ var LLMClient = class {
|
|
|
25375
25754
|
signal: options.signal,
|
|
25376
25755
|
provider: this.provider,
|
|
25377
25756
|
model: this.model,
|
|
25378
|
-
|
|
25757
|
+
// Every trace field, not a hand-picked three: `traceparent` rides with the ids,
|
|
25758
|
+
// and picking fields here is what left the HTTP spans rooting a trace of their
|
|
25759
|
+
// own while the LLM span they belong to had joined the caller's.
|
|
25760
|
+
trace: {
|
|
25761
|
+
sessionId: ctx.sessionId,
|
|
25762
|
+
requestId: ctx.requestId,
|
|
25763
|
+
callId: ctx.callId,
|
|
25764
|
+
traceparent: ctx.traceparent
|
|
25765
|
+
}
|
|
25379
25766
|
};
|
|
25380
25767
|
response = await this.fetchFn(httpReq, {
|
|
25381
25768
|
queueName: this.queueName,
|
|
@@ -25503,7 +25890,15 @@ var LLMClient = class {
|
|
|
25503
25890
|
stream: true,
|
|
25504
25891
|
provider: this.provider,
|
|
25505
25892
|
model: this.model,
|
|
25506
|
-
|
|
25893
|
+
// Every trace field, not a hand-picked three: `traceparent` rides with the ids,
|
|
25894
|
+
// and picking fields here is what left the HTTP spans rooting a trace of their
|
|
25895
|
+
// own while the LLM span they belong to had joined the caller's.
|
|
25896
|
+
trace: {
|
|
25897
|
+
sessionId: ctx.sessionId,
|
|
25898
|
+
requestId: ctx.requestId,
|
|
25899
|
+
callId: ctx.callId,
|
|
25900
|
+
traceparent: ctx.traceparent
|
|
25901
|
+
}
|
|
25507
25902
|
};
|
|
25508
25903
|
const start = performance.now();
|
|
25509
25904
|
let text = "";
|
|
@@ -25985,12 +26380,16 @@ var AnthropicAdapter = class {
|
|
|
25985
26380
|
}
|
|
25986
26381
|
return null;
|
|
25987
26382
|
}
|
|
26383
|
+
const strict = t.strict === true;
|
|
26384
|
+
const params = ensureAdditionalProperties(t.parameters);
|
|
25988
26385
|
const tool = {
|
|
25989
26386
|
name: t.name,
|
|
25990
26387
|
description: t.description,
|
|
25991
|
-
|
|
26388
|
+
// Only the strict path was measured with `additionalProperties: false`
|
|
26389
|
+
// applied; without strict the schema goes out untouched, as before.
|
|
26390
|
+
input_schema: strict ? params : t.parameters
|
|
25992
26391
|
};
|
|
25993
|
-
if (
|
|
26392
|
+
if (strict) tool.strict = true;
|
|
25994
26393
|
if ((t.cache || shouldCacheTools) && i === req.tools.length - 1) {
|
|
25995
26394
|
tool.cache_control = { type: "ephemeral" };
|
|
25996
26395
|
}
|
|
@@ -28039,15 +28438,19 @@ var OpenAIAdapter = class {
|
|
|
28039
28438
|
};
|
|
28040
28439
|
}
|
|
28041
28440
|
if (req.tools?.length) {
|
|
28042
|
-
body.tools = req.tools.filter(isFunctionTool).map((t) =>
|
|
28043
|
-
|
|
28044
|
-
|
|
28045
|
-
|
|
28046
|
-
|
|
28047
|
-
|
|
28048
|
-
|
|
28049
|
-
|
|
28050
|
-
|
|
28441
|
+
body.tools = req.tools.filter(isFunctionTool).map((t) => {
|
|
28442
|
+
const params = ensureAdditionalProperties(t.parameters);
|
|
28443
|
+
const strict = t.strict === true;
|
|
28444
|
+
return {
|
|
28445
|
+
type: "function",
|
|
28446
|
+
function: {
|
|
28447
|
+
name: t.name,
|
|
28448
|
+
description: t.description,
|
|
28449
|
+
parameters: strict ? params : t.parameters,
|
|
28450
|
+
...strict ? { strict: true } : {}
|
|
28451
|
+
}
|
|
28452
|
+
};
|
|
28453
|
+
});
|
|
28051
28454
|
}
|
|
28052
28455
|
if (req.toolChoice) {
|
|
28053
28456
|
if (typeof req.toolChoice === "string") body.tool_choice = req.toolChoice;
|
|
@@ -28057,12 +28460,14 @@ var OpenAIAdapter = class {
|
|
|
28057
28460
|
body.reasoning = { effort: req.thinking.effort ?? "medium" };
|
|
28058
28461
|
}
|
|
28059
28462
|
if (req.structured) {
|
|
28463
|
+
const schema = ensureAdditionalProperties(req.structured.schema);
|
|
28464
|
+
const strict = req.structured.strict ?? strictSupport(schema, "openai").ok;
|
|
28060
28465
|
body.response_format = {
|
|
28061
28466
|
type: "json_schema",
|
|
28062
28467
|
json_schema: {
|
|
28063
28468
|
name: req.structured.name ?? "response",
|
|
28064
|
-
schema: req.structured.schema,
|
|
28065
|
-
strict
|
|
28469
|
+
schema: strict ? schema : req.structured.schema,
|
|
28470
|
+
strict
|
|
28066
28471
|
}
|
|
28067
28472
|
};
|
|
28068
28473
|
}
|
|
@@ -28962,12 +29367,13 @@ var OpenAIResponsesAdapter = class {
|
|
|
28962
29367
|
if (req.tools?.length) {
|
|
28963
29368
|
body.tools = req.tools.map((t) => {
|
|
28964
29369
|
if (isFunctionTool(t)) {
|
|
29370
|
+
const params = ensureAdditionalProperties(t.parameters);
|
|
28965
29371
|
return {
|
|
28966
29372
|
type: "function",
|
|
28967
29373
|
name: t.name,
|
|
28968
29374
|
description: t.description,
|
|
28969
|
-
parameters:
|
|
28970
|
-
strict: t.strict ??
|
|
29375
|
+
parameters: params,
|
|
29376
|
+
strict: t.strict ?? strictSupport(params, "openai").ok,
|
|
28971
29377
|
// Programmatic tool calling (Responses): who may call it + return schema.
|
|
28972
29378
|
...t.allowedCallers ? { allowed_callers: t.allowedCallers } : {},
|
|
28973
29379
|
...t.outputSchema ? { output_schema: t.outputSchema } : {}
|
|
@@ -28988,12 +29394,13 @@ var OpenAIResponsesAdapter = class {
|
|
|
28988
29394
|
}
|
|
28989
29395
|
}
|
|
28990
29396
|
if (req.structured) {
|
|
29397
|
+
const schema = ensureAdditionalProperties(req.structured.schema);
|
|
28991
29398
|
body.text = {
|
|
28992
29399
|
format: {
|
|
28993
29400
|
type: "json_schema",
|
|
28994
29401
|
name: req.structured.name ?? "response",
|
|
28995
|
-
schema
|
|
28996
|
-
strict: req.structured.strict ??
|
|
29402
|
+
schema,
|
|
29403
|
+
strict: req.structured.strict ?? strictSupport(schema, "openai").ok
|
|
28997
29404
|
}
|
|
28998
29405
|
};
|
|
28999
29406
|
}
|
|
@@ -30587,7 +30994,9 @@ var LAYER_MEMORY = "memory";
|
|
|
30587
30994
|
var LAYER_CHAT_FACTS = "chat.facts";
|
|
30588
30995
|
var LAYER_EXECUTOR_TOOL_EXAMPLES = "executor.tool-examples";
|
|
30589
30996
|
var LAYER_CONTEXT_GUARD_SUMMARY = "context-guard.summary";
|
|
30997
|
+
var LAYER_LAZY_TOOLS = "agentloop.lazy-tools";
|
|
30590
30998
|
var PRIORITY_AGENTLOOP_SYSTEM = 10;
|
|
30999
|
+
var PRIORITY_LAZY_TOOLS = 20;
|
|
30591
31000
|
var PRIORITY_LEGACY_SYSTEM = 50;
|
|
30592
31001
|
var PRIORITY_AGENTLOOP_CONTEXT = 100;
|
|
30593
31002
|
var PRIORITY_MEMORY = 200;
|
|
@@ -30605,6 +31014,17 @@ function writeAgentLoopSystem(registry, text, owner) {
|
|
|
30605
31014
|
owner
|
|
30606
31015
|
});
|
|
30607
31016
|
}
|
|
31017
|
+
function writeLazyToolsProtocol(registry, active, owner) {
|
|
31018
|
+
if (!active) {
|
|
31019
|
+
registry.remove(LAYER_LAZY_TOOLS);
|
|
31020
|
+
return;
|
|
31021
|
+
}
|
|
31022
|
+
registry.set(
|
|
31023
|
+
LAYER_LAZY_TOOLS,
|
|
31024
|
+
"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.",
|
|
31025
|
+
{ priority: PRIORITY_LAZY_TOOLS, tags: ["system"], owner }
|
|
31026
|
+
);
|
|
31027
|
+
}
|
|
30608
31028
|
function writeAgentLoopContext(registry, text, owner) {
|
|
30609
31029
|
if (!text) {
|
|
30610
31030
|
registry.remove(LAYER_AGENTLOOP_CONTEXT);
|
|
@@ -30956,6 +31376,175 @@ ${text}` : text;
|
|
|
30956
31376
|
}
|
|
30957
31377
|
};
|
|
30958
31378
|
|
|
31379
|
+
// src/agent/lazy-tools.ts
|
|
31380
|
+
var DEFAULT_LIMIT = 5;
|
|
31381
|
+
var MAX_LIMIT = 20;
|
|
31382
|
+
var DEFAULT_MAX_SEARCHES = 5;
|
|
31383
|
+
var LAZY_SEARCH_TOOL = "tool_search";
|
|
31384
|
+
var LAZY_CALL_TOOL = "call_tool";
|
|
31385
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
31386
|
+
"the",
|
|
31387
|
+
"a",
|
|
31388
|
+
"an",
|
|
31389
|
+
"of",
|
|
31390
|
+
"for",
|
|
31391
|
+
"to",
|
|
31392
|
+
"in",
|
|
31393
|
+
"on",
|
|
31394
|
+
"and",
|
|
31395
|
+
"or",
|
|
31396
|
+
"is",
|
|
31397
|
+
"it",
|
|
31398
|
+
"that",
|
|
31399
|
+
"this",
|
|
31400
|
+
"with",
|
|
31401
|
+
"return",
|
|
31402
|
+
"returns",
|
|
31403
|
+
"my",
|
|
31404
|
+
"me",
|
|
31405
|
+
"do",
|
|
31406
|
+
"we",
|
|
31407
|
+
"i",
|
|
31408
|
+
"how",
|
|
31409
|
+
"many",
|
|
31410
|
+
"much",
|
|
31411
|
+
"what",
|
|
31412
|
+
"when",
|
|
31413
|
+
"has",
|
|
31414
|
+
"have",
|
|
31415
|
+
"need",
|
|
31416
|
+
"any",
|
|
31417
|
+
"get",
|
|
31418
|
+
"can",
|
|
31419
|
+
"you",
|
|
31420
|
+
"are",
|
|
31421
|
+
"was",
|
|
31422
|
+
"been",
|
|
31423
|
+
"does",
|
|
31424
|
+
"did",
|
|
31425
|
+
"should",
|
|
31426
|
+
"from",
|
|
31427
|
+
"by",
|
|
31428
|
+
"at",
|
|
31429
|
+
"as",
|
|
31430
|
+
"be"
|
|
31431
|
+
]);
|
|
31432
|
+
function tokenize(s) {
|
|
31433
|
+
return s.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w));
|
|
31434
|
+
}
|
|
31435
|
+
var isFn = (t) => "name" in t;
|
|
31436
|
+
var nameOf = (t) => isFn(t.definition) ? t.definition.name : "";
|
|
31437
|
+
function rankTools(query, candidates, limit) {
|
|
31438
|
+
const q = new Set(tokenize(query));
|
|
31439
|
+
if (q.size === 0) return [];
|
|
31440
|
+
const scored = [];
|
|
31441
|
+
for (const tool of candidates) {
|
|
31442
|
+
const def = tool.definition;
|
|
31443
|
+
if (!isFn(def)) continue;
|
|
31444
|
+
const props = Object.keys(
|
|
31445
|
+
def.parameters?.properties ?? {}
|
|
31446
|
+
);
|
|
31447
|
+
let score = 0;
|
|
31448
|
+
for (const w of tokenize(`${def.name} ${def.description ?? ""} ${props.join(" ")}`)) {
|
|
31449
|
+
if (q.has(w)) score++;
|
|
31450
|
+
}
|
|
31451
|
+
for (const w of tokenize(def.name)) if (q.has(w)) score += 2;
|
|
31452
|
+
if (score > 0) scored.push({ tool, score });
|
|
31453
|
+
}
|
|
31454
|
+
return scored.sort((a, b) => b.score - a.score).slice(0, limit).map((s) => s.tool);
|
|
31455
|
+
}
|
|
31456
|
+
function createLazyTools(deps) {
|
|
31457
|
+
const limit = Math.min(deps.config.limit ?? DEFAULT_LIMIT, MAX_LIMIT);
|
|
31458
|
+
const maxSearches = deps.config.maxSearches ?? DEFAULT_MAX_SEARCHES;
|
|
31459
|
+
const search = {
|
|
31460
|
+
definition: {
|
|
31461
|
+
type: "function",
|
|
31462
|
+
name: LAZY_SEARCH_TOOL,
|
|
31463
|
+
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.",
|
|
31464
|
+
parameters: {
|
|
31465
|
+
type: "object",
|
|
31466
|
+
properties: {
|
|
31467
|
+
queries: {
|
|
31468
|
+
type: "array",
|
|
31469
|
+
items: { type: "string" },
|
|
31470
|
+
description: "One phrase per capability you need, in your own words."
|
|
31471
|
+
}
|
|
31472
|
+
},
|
|
31473
|
+
required: ["queries"]
|
|
31474
|
+
}
|
|
31475
|
+
},
|
|
31476
|
+
execute: async (args) => {
|
|
31477
|
+
deps.state.searches++;
|
|
31478
|
+
if (deps.state.searches > maxSearches) {
|
|
31479
|
+
return JSON.stringify({
|
|
31480
|
+
error: `Search budget exhausted (${maxSearches} searches per run). Use the tools you already found.`
|
|
31481
|
+
});
|
|
31482
|
+
}
|
|
31483
|
+
const raw = args.queries;
|
|
31484
|
+
const queries = (Array.isArray(raw) ? raw : [raw]).filter((q) => typeof q === "string" && q.trim().length > 0);
|
|
31485
|
+
if (queries.length === 0) {
|
|
31486
|
+
return JSON.stringify({ tools: [], error: "Pass at least one query string in `queries`." });
|
|
31487
|
+
}
|
|
31488
|
+
const candidates = deps.lazyTools();
|
|
31489
|
+
const hits = /* @__PURE__ */ new Map();
|
|
31490
|
+
const unmatched = [];
|
|
31491
|
+
for (const q of queries) {
|
|
31492
|
+
const found = rankTools(q, candidates, limit);
|
|
31493
|
+
if (found.length === 0) unmatched.push(q);
|
|
31494
|
+
for (const t of found) hits.set(nameOf(t), t);
|
|
31495
|
+
}
|
|
31496
|
+
deps.onSearch?.({ queries, matched: [...hits.keys()], unmatched });
|
|
31497
|
+
return JSON.stringify({
|
|
31498
|
+
tools: [...hits.values()].map((t) => t.definition),
|
|
31499
|
+
...unmatched.length > 0 ? {
|
|
31500
|
+
unmatched,
|
|
31501
|
+
hint: "These queries matched no tool. Search again for them using different words, or tell the user the capability is unavailable."
|
|
31502
|
+
} : {}
|
|
31503
|
+
});
|
|
31504
|
+
}
|
|
31505
|
+
};
|
|
31506
|
+
const call = {
|
|
31507
|
+
definition: {
|
|
31508
|
+
type: "function",
|
|
31509
|
+
name: LAZY_CALL_TOOL,
|
|
31510
|
+
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.",
|
|
31511
|
+
parameters: {
|
|
31512
|
+
type: "object",
|
|
31513
|
+
properties: {
|
|
31514
|
+
name: { type: "string", description: "Exact tool name from tool_search." },
|
|
31515
|
+
input: {
|
|
31516
|
+
type: "object",
|
|
31517
|
+
description: "That tool's own arguments, as an object.",
|
|
31518
|
+
additionalProperties: true
|
|
31519
|
+
}
|
|
31520
|
+
},
|
|
31521
|
+
required: ["name", "input"]
|
|
31522
|
+
}
|
|
31523
|
+
},
|
|
31524
|
+
execute: async (args, ctx) => {
|
|
31525
|
+
const name = String(args.name ?? "");
|
|
31526
|
+
const target = deps.lazyTools().find((t) => nameOf(t) === name);
|
|
31527
|
+
if (!target) {
|
|
31528
|
+
if (deps.eagerNames().includes(name)) {
|
|
31529
|
+
return `"${name}" is already available as a normal tool \u2014 call it directly, not through ${LAZY_CALL_TOOL}.`;
|
|
31530
|
+
}
|
|
31531
|
+
return `No tool named "${name}". Call ${LAZY_SEARCH_TOOL} first and use a name exactly as returned.`;
|
|
31532
|
+
}
|
|
31533
|
+
const input = args.input;
|
|
31534
|
+
if (input !== void 0 && (typeof input !== "object" || input === null || Array.isArray(input))) {
|
|
31535
|
+
return `\`input\` must be an object of ${name}'s arguments, not ${Array.isArray(input) ? "an array" : typeof input}.`;
|
|
31536
|
+
}
|
|
31537
|
+
return target.execute(input ?? {}, ctx);
|
|
31538
|
+
}
|
|
31539
|
+
};
|
|
31540
|
+
return [search, call];
|
|
31541
|
+
}
|
|
31542
|
+
function unwrapLazyCall(toolName, args) {
|
|
31543
|
+
if (toolName !== LAZY_CALL_TOOL) return null;
|
|
31544
|
+
const inner = args.name;
|
|
31545
|
+
return typeof inner === "string" && inner.length > 0 ? inner : null;
|
|
31546
|
+
}
|
|
31547
|
+
|
|
30959
31548
|
// src/agent/tool-key.ts
|
|
30960
31549
|
function toolKey(tool) {
|
|
30961
31550
|
return isFunctionTool(tool.definition) ? tool.definition.name : tool.definition.type;
|
|
@@ -31216,12 +31805,25 @@ async function handleToolError(e, tc, hooks, runId, agentId, step, metrics, repo
|
|
|
31216
31805
|
// src/agent/loop.ts
|
|
31217
31806
|
var AgentLoop = class _AgentLoop {
|
|
31218
31807
|
id;
|
|
31808
|
+
/** Human name, surfaced as `gen_ai.agent.name` — see AgentLoopConfig.label. */
|
|
31809
|
+
label;
|
|
31810
|
+
/** Which part of the host system this agent belongs to. */
|
|
31811
|
+
source;
|
|
31812
|
+
/** Extra attributes stamped on this agent's spans. */
|
|
31813
|
+
attributes;
|
|
31219
31814
|
client;
|
|
31220
31815
|
hooks;
|
|
31221
31816
|
_system;
|
|
31222
31817
|
_systemThunk = null;
|
|
31223
31818
|
_context;
|
|
31224
31819
|
_tools;
|
|
31820
|
+
_lazyConfig = {};
|
|
31821
|
+
/** Per-run search budget, reset at the start of every run. */
|
|
31822
|
+
_lazyState = { searches: 0 };
|
|
31823
|
+
/** Installed on the first `lazy` registration and never removed, so the declared tool
|
|
31824
|
+
* array stays byte-identical for the life of the conversation — which is the entire
|
|
31825
|
+
* reason the design is cheap. */
|
|
31826
|
+
_lazyInstalled = false;
|
|
31225
31827
|
_history;
|
|
31226
31828
|
_reports = [];
|
|
31227
31829
|
_metadata = {};
|
|
@@ -31269,6 +31871,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31269
31871
|
this._checkpoint = config.checkpoint ?? null;
|
|
31270
31872
|
this._collisionPolicy = config.toolNameCollisionPolicy ?? "warn";
|
|
31271
31873
|
this._reflectRetry = config.reflectAndRetry ? new ReflectAndRetryPolicy(config.reflectAndRetry) : null;
|
|
31874
|
+
this._lazyConfig = config.lazyTools ?? {};
|
|
31272
31875
|
this._tools = /* @__PURE__ */ new Map();
|
|
31273
31876
|
for (const t of config.tools ?? []) {
|
|
31274
31877
|
this.registerTool(t);
|
|
@@ -31281,8 +31884,12 @@ var AgentLoop = class _AgentLoop {
|
|
|
31281
31884
|
this._history = new ConversationHistory();
|
|
31282
31885
|
}
|
|
31283
31886
|
this.id = this._history.id;
|
|
31887
|
+
this.label = config.label;
|
|
31888
|
+
this.source = config.source;
|
|
31889
|
+
this.attributes = config.attributes;
|
|
31284
31890
|
writeAgentLoopSystem(this._history.registry, this._system, "agent-loop");
|
|
31285
31891
|
writeAgentLoopContext(this._history.registry, this._context, "agent-loop");
|
|
31892
|
+
this.syncLazyProtocol();
|
|
31286
31893
|
this.hooks.emitSync("onAgentCreate", {
|
|
31287
31894
|
agentId: this.id,
|
|
31288
31895
|
clientId: this.client.id,
|
|
@@ -31369,6 +31976,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31369
31976
|
});
|
|
31370
31977
|
}
|
|
31371
31978
|
this._tools.set(key, tool);
|
|
31979
|
+
if (tool.lazy) this.installLazyTools();
|
|
31372
31980
|
}
|
|
31373
31981
|
removeTool(name) {
|
|
31374
31982
|
this._tools.delete(name);
|
|
@@ -31383,7 +31991,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31383
31991
|
}
|
|
31384
31992
|
// ─── complete (non-streaming) ───────────────────────────────────────────
|
|
31385
31993
|
async complete(input, options = {}) {
|
|
31386
|
-
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input);
|
|
31994
|
+
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input, options.ctx);
|
|
31387
31995
|
const steps = [];
|
|
31388
31996
|
const totalUsage = emptyUsage();
|
|
31389
31997
|
let totalLlmTimeMs = 0;
|
|
@@ -31435,7 +32043,23 @@ var AgentLoop = class _AgentLoop {
|
|
|
31435
32043
|
thinking: options.thinking ?? this._thinking,
|
|
31436
32044
|
cache: options.cache ?? this._cache,
|
|
31437
32045
|
tools: this.toolDefinitions(options),
|
|
31438
|
-
ctx: {
|
|
32046
|
+
ctx: {
|
|
32047
|
+
// The RUN's trace, handed down to every LLM call it makes.
|
|
32048
|
+
//
|
|
32049
|
+
// Without this the agent kept `runTrace` to itself: its own spans used it
|
|
32050
|
+
// while each `client.complete()` fell through to mint-if-absent and
|
|
32051
|
+
// invented a fresh `requestId`. Since the trace id is `sessionId:requestId`,
|
|
32052
|
+
// one conversation arrived at the backend as SEVERAL unrelated traces —
|
|
32053
|
+
// measured against a real collector: a single turn with one tool call
|
|
32054
|
+
// produced six. Correlation is the whole point of a trace id, so this is
|
|
32055
|
+
// the one thing it must not get wrong.
|
|
32056
|
+
...runTrace,
|
|
32057
|
+
conversationId: this._history.id,
|
|
32058
|
+
// A caller's explicit ctx wins over all of the above: an app that already
|
|
32059
|
+
// owns a request id or a conversation id has better information than we do,
|
|
32060
|
+
// and silently overwriting it is how its telemetry stops joining up.
|
|
32061
|
+
...options.ctx
|
|
32062
|
+
},
|
|
31439
32063
|
signal: options.signal ?? this._abortController?.signal
|
|
31440
32064
|
});
|
|
31441
32065
|
const stepLatency = performance.now() - stepStart;
|
|
@@ -31603,7 +32227,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31603
32227
|
}
|
|
31604
32228
|
// ─── stream ─────────────────────────────────────────────────────────────
|
|
31605
32229
|
async *stream(input, options = {}) {
|
|
31606
|
-
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input);
|
|
32230
|
+
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input, options.ctx);
|
|
31607
32231
|
const steps = [];
|
|
31608
32232
|
const totalUsage = emptyUsage();
|
|
31609
32233
|
let totalLlmTimeMs = 0;
|
|
@@ -31659,7 +32283,23 @@ var AgentLoop = class _AgentLoop {
|
|
|
31659
32283
|
thinking: options.thinking ?? this._thinking,
|
|
31660
32284
|
cache: options.cache ?? this._cache,
|
|
31661
32285
|
tools: this.toolDefinitions(options),
|
|
31662
|
-
ctx: {
|
|
32286
|
+
ctx: {
|
|
32287
|
+
// The RUN's trace, handed down to every LLM call it makes.
|
|
32288
|
+
//
|
|
32289
|
+
// Without this the agent kept `runTrace` to itself: its own spans used it
|
|
32290
|
+
// while each `client.complete()` fell through to mint-if-absent and
|
|
32291
|
+
// invented a fresh `requestId`. Since the trace id is `sessionId:requestId`,
|
|
32292
|
+
// one conversation arrived at the backend as SEVERAL unrelated traces —
|
|
32293
|
+
// measured against a real collector: a single turn with one tool call
|
|
32294
|
+
// produced six. Correlation is the whole point of a trace id, so this is
|
|
32295
|
+
// the one thing it must not get wrong.
|
|
32296
|
+
...runTrace,
|
|
32297
|
+
conversationId: this._history.id,
|
|
32298
|
+
// A caller's explicit ctx wins over all of the above: an app that already
|
|
32299
|
+
// owns a request id or a conversation id has better information than we do,
|
|
32300
|
+
// and silently overwriting it is how its telemetry stops joining up.
|
|
32301
|
+
...options.ctx
|
|
32302
|
+
},
|
|
31663
32303
|
signal: options.signal ?? this._abortController?.signal
|
|
31664
32304
|
})) {
|
|
31665
32305
|
const toYield = accumulateStreamEvent(event, state);
|
|
@@ -31871,7 +32511,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31871
32511
|
arguments: tc.arguments,
|
|
31872
32512
|
callId: tc.id,
|
|
31873
32513
|
step,
|
|
31874
|
-
trace: {
|
|
32514
|
+
trace: { ...runTrace, callId: tc.id }
|
|
31875
32515
|
});
|
|
31876
32516
|
if (!decision.pass) {
|
|
31877
32517
|
return this.buildDeniedResult(tc, decision.reason, reports);
|
|
@@ -31891,7 +32531,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31891
32531
|
}
|
|
31892
32532
|
}
|
|
31893
32533
|
try {
|
|
31894
|
-
const baseCtx = { step, callId: tc.id, metrics, trace: {
|
|
32534
|
+
const baseCtx = { step, callId: tc.id, metrics, trace: { ...runTrace, callId: tc.id } };
|
|
31895
32535
|
const result = await executeWithTimeout(lookup.tool, tc, baseCtx, this._toolTimeout);
|
|
31896
32536
|
return await this.buildSuccessResult(tc, result, runId, step, metrics, reports, toolStart, runTrace, lookup.tool, baseCtx);
|
|
31897
32537
|
} catch (e) {
|
|
@@ -31949,7 +32589,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31949
32589
|
arguments: tc.arguments,
|
|
31950
32590
|
reason,
|
|
31951
32591
|
step,
|
|
31952
|
-
trace: {
|
|
32592
|
+
trace: { ...runTrace, callId: tc.id }
|
|
31953
32593
|
};
|
|
31954
32594
|
const pending = {
|
|
31955
32595
|
callId: tc.id,
|
|
@@ -31982,7 +32622,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31982
32622
|
return this.buildOverriddenResult(tc, decision.overrideResult, reports);
|
|
31983
32623
|
}
|
|
31984
32624
|
try {
|
|
31985
|
-
const baseCtx = { step, callId: tc.id, metrics, trace: {
|
|
32625
|
+
const baseCtx = { step, callId: tc.id, metrics, trace: { ...runTrace, callId: tc.id } };
|
|
31986
32626
|
const result = await executeWithTimeout(tool, tc, baseCtx, this._toolTimeout);
|
|
31987
32627
|
return await this.buildSuccessResult(tc, result, runId, step, metrics, reports, toolStart, runTrace, tool, baseCtx);
|
|
31988
32628
|
} catch (e) {
|
|
@@ -32019,31 +32659,71 @@ var AgentLoop = class _AgentLoop {
|
|
|
32019
32659
|
metrics,
|
|
32020
32660
|
trace: runTrace
|
|
32021
32661
|
});
|
|
32662
|
+
const inner = unwrapLazyCall(tc.name, tc.arguments);
|
|
32022
32663
|
reports.push({
|
|
32023
32664
|
callId: tc.id,
|
|
32024
|
-
toolName: tc.name,
|
|
32665
|
+
toolName: inner ?? tc.name,
|
|
32025
32666
|
arguments: tc.arguments,
|
|
32026
32667
|
resultSizeBytes: resultStr.length,
|
|
32027
32668
|
latencyMs,
|
|
32028
32669
|
skipped: false,
|
|
32029
32670
|
error: null,
|
|
32030
32671
|
metrics: Object.fromEntries(metrics),
|
|
32672
|
+
...inner ? { discoveredVia: "search" } : {},
|
|
32031
32673
|
...customData !== void 0 ? { customData } : {}
|
|
32032
32674
|
});
|
|
32033
32675
|
return { type: "tool_result", id: tc.id, content: resultStr };
|
|
32034
32676
|
}
|
|
32035
|
-
/**
|
|
32677
|
+
/** Declare `tool_search` + `call_tool`, once, on the first lazy registration.
|
|
32678
|
+
*
|
|
32679
|
+
* They go through `registerTool` like anything else, so the collision policy covers
|
|
32680
|
+
* them and there is no second registry to keep in sync. They are never removed: the
|
|
32681
|
+
* declared array must stay identical for the whole conversation or the cached prefix
|
|
32682
|
+
* is invalidated, which is the cost the feature exists to avoid. */
|
|
32683
|
+
installLazyTools() {
|
|
32684
|
+
if (this._lazyInstalled) return;
|
|
32685
|
+
this._lazyInstalled = true;
|
|
32686
|
+
for (const t of createLazyTools({
|
|
32687
|
+
lazyTools: () => [...this._tools.values()].filter((t2) => t2.lazy),
|
|
32688
|
+
eagerNames: () => [...this._tools.entries()].filter(([, t2]) => !t2.lazy).map(([key]) => key),
|
|
32689
|
+
state: this._lazyState,
|
|
32690
|
+
config: this._lazyConfig,
|
|
32691
|
+
onSearch: (info) => {
|
|
32692
|
+
void this.hooks.emit("onToolSearch", { agentId: this.id, ...info });
|
|
32693
|
+
}
|
|
32694
|
+
})) {
|
|
32695
|
+
this.registerTool(t);
|
|
32696
|
+
}
|
|
32697
|
+
this.syncLazyProtocol();
|
|
32698
|
+
}
|
|
32699
|
+
/** Publish (or remove) the "your tools are not all listed" layer.
|
|
32700
|
+
*
|
|
32701
|
+
* Separate from `installLazyTools` because tools are registered in the constructor
|
|
32702
|
+
* BEFORE `_history` exists, and the layer lives in the history's registry. The
|
|
32703
|
+
* constructor calls this again once history is built.
|
|
32704
|
+
*
|
|
32705
|
+
* The model has no reason to suspect a tool it cannot see, and the failure without
|
|
32706
|
+
* this is quiet — it answers from whatever it did find. Measured at 8/12 and 9/12
|
|
32707
|
+
* without the protocol, 18/18 with it, same tasks and same ranker. */
|
|
32708
|
+
syncLazyProtocol() {
|
|
32709
|
+
if (!this._history) return;
|
|
32710
|
+
writeLazyToolsProtocol(this._history.registry, this._lazyInstalled, "agent-loop");
|
|
32711
|
+
}
|
|
32712
|
+
/** Merge agent's tool definitions with caller-provided tools (caller wins on conflict).
|
|
32713
|
+
*
|
|
32714
|
+
* Lazy tools are registered but NOT declared — that filter is the whole mechanism. */
|
|
32036
32715
|
toolDefinitions(options) {
|
|
32037
|
-
const own = [...this._tools.values()].map((t) => t.definition);
|
|
32716
|
+
const own = [...this._tools.values()].filter((t) => !t.lazy).map((t) => t.definition);
|
|
32038
32717
|
if (options.tools) return [...own, ...options.tools];
|
|
32039
32718
|
return own.length > 0 ? own : void 0;
|
|
32040
32719
|
}
|
|
32041
32720
|
// ─── Run helpers ────────────────────────────────────────────────────────
|
|
32042
|
-
async beginRun(input) {
|
|
32721
|
+
async beginRun(input, callerCtx) {
|
|
32043
32722
|
if (this._running) throw new Error("AgentLoop is already running");
|
|
32044
32723
|
this._running = true;
|
|
32045
32724
|
this._stopRequested = false;
|
|
32046
32725
|
this._abortController = new AbortController();
|
|
32726
|
+
this._lazyState.searches = 0;
|
|
32047
32727
|
if (this._systemThunk) {
|
|
32048
32728
|
const next = await this._systemThunk();
|
|
32049
32729
|
if (next !== this._system) {
|
|
@@ -32055,10 +32735,23 @@ var AgentLoop = class _AgentLoop {
|
|
|
32055
32735
|
const startedAt = Date.now();
|
|
32056
32736
|
const startPerf = performance.now();
|
|
32057
32737
|
const userMessageText = typeof input === "string" ? input : Array.isArray(input) && input.length > 0 && "role" in input[0] ? contentText(input[input.length - 1].content) : contentText(input);
|
|
32058
|
-
const runTrace = {
|
|
32738
|
+
const runTrace = {
|
|
32739
|
+
sessionId: callerCtx?.sessionId ?? this.id,
|
|
32740
|
+
requestId: callerCtx?.requestId ?? runId,
|
|
32741
|
+
// The caller's span travels WITH the ids, for the same reason they do: `agent.run`,
|
|
32742
|
+
// every `tool.call`, and any agent nested inside a tool reach the telemetry through
|
|
32743
|
+
// `runTrace` and nothing else. While this field was missing from it only the LLM
|
|
32744
|
+
// calls joined the app's trace — they are built from the caller's ctx directly —
|
|
32745
|
+
// and the run that made them sat in a second, unrelated one. Measured against a
|
|
32746
|
+
// live backend; the unit tests fed the hooks directly and never saw it.
|
|
32747
|
+
...callerCtx?.traceparent ? { traceparent: callerCtx.traceparent } : {}
|
|
32748
|
+
};
|
|
32059
32749
|
await this.hooks.emit("onRunStart", {
|
|
32060
32750
|
runId,
|
|
32061
32751
|
agentId: this.id,
|
|
32752
|
+
label: this.label,
|
|
32753
|
+
source: this.source,
|
|
32754
|
+
attributes: this.attributes,
|
|
32062
32755
|
userMessage: input,
|
|
32063
32756
|
model: this.client.model,
|
|
32064
32757
|
system: this._history.system,
|
|
@@ -32113,7 +32806,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32113
32806
|
if (g.kind !== "input") continue;
|
|
32114
32807
|
const decision = await g.check({
|
|
32115
32808
|
kind: "input",
|
|
32116
|
-
trace: {
|
|
32809
|
+
trace: { ...runTrace },
|
|
32117
32810
|
step,
|
|
32118
32811
|
messages,
|
|
32119
32812
|
system
|
|
@@ -32141,7 +32834,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32141
32834
|
if (g.kind !== "output") continue;
|
|
32142
32835
|
const decision = await g.check({
|
|
32143
32836
|
kind: "output",
|
|
32144
|
-
trace: {
|
|
32837
|
+
trace: { ...runTrace },
|
|
32145
32838
|
step,
|
|
32146
32839
|
response
|
|
32147
32840
|
});
|
|
@@ -33017,9 +33710,16 @@ function summarize(entries) {
|
|
|
33017
33710
|
reasoning: 0,
|
|
33018
33711
|
total: 0,
|
|
33019
33712
|
tokens: { input: 0, output: 0, cached: 0, cacheWrite: 0, reasoning: 0 },
|
|
33020
|
-
entries: entries.length
|
|
33713
|
+
entries: entries.length,
|
|
33714
|
+
unpriced: 0,
|
|
33715
|
+
unpricedModels: []
|
|
33021
33716
|
};
|
|
33022
33717
|
for (const e of entries) {
|
|
33718
|
+
if (e.cost.source === "unknown") {
|
|
33719
|
+
s.unpriced++;
|
|
33720
|
+
const key = `${e.provider}/${e.model}`;
|
|
33721
|
+
if (!s.unpricedModels.includes(key)) s.unpricedModels.push(key);
|
|
33722
|
+
}
|
|
33023
33723
|
s.input += e.cost.input;
|
|
33024
33724
|
s.output += e.cost.output;
|
|
33025
33725
|
s.cacheRead += e.cost.cacheRead;
|
|
@@ -33045,6 +33745,8 @@ var CostCollector = class {
|
|
|
33045
33745
|
budgets = [];
|
|
33046
33746
|
triggeredThresholds = /* @__PURE__ */ new Map();
|
|
33047
33747
|
_runningTotal = 0;
|
|
33748
|
+
/** Models already reported as unpriced, so the warning fires once rather than per call. */
|
|
33749
|
+
warnedUnpriced = /* @__PURE__ */ new Set();
|
|
33048
33750
|
watchedAgents = /* @__PURE__ */ new Set();
|
|
33049
33751
|
unsub = null;
|
|
33050
33752
|
unsubMedia = null;
|
|
@@ -33177,6 +33879,7 @@ var CostCollector = class {
|
|
|
33177
33879
|
};
|
|
33178
33880
|
this.ledger.push(entry);
|
|
33179
33881
|
this._runningTotal += cost.total;
|
|
33882
|
+
this.noteIfUnpriced(entry);
|
|
33180
33883
|
this.hooks.emitSync("onCostEntry", { entry, runningTotal: this._runningTotal });
|
|
33181
33884
|
this.checkBudgets(entry);
|
|
33182
33885
|
}
|
|
@@ -33222,9 +33925,31 @@ var CostCollector = class {
|
|
|
33222
33925
|
};
|
|
33223
33926
|
this.ledger.push(entry);
|
|
33224
33927
|
this._runningTotal += cost.total;
|
|
33928
|
+
this.noteIfUnpriced(entry);
|
|
33225
33929
|
this.hooks.emitSync("onCostEntry", { entry, runningTotal: this._runningTotal });
|
|
33226
33930
|
this.checkBudgets(entry);
|
|
33227
33931
|
}
|
|
33932
|
+
/** A total of exactly 0 because the model is not in the catalog reads identically to a
|
|
33933
|
+
* total of 0 because the call was free — and it silently under-counts every budget
|
|
33934
|
+
* and report built on it. `source: 'unknown'` already records the difference per
|
|
33935
|
+
* entry, but nothing aggregated it, so a whole benchmark run once reported $0.00000
|
|
33936
|
+
* for a live provider and looked like a free arm.
|
|
33937
|
+
*
|
|
33938
|
+
* Fires once per provider/model: an unpriced model is a configuration fact, not a
|
|
33939
|
+
* per-request event, and repeating it on every call would train the reader to ignore
|
|
33940
|
+
* it. Free calls are priced 'calculated' with an explicit note, so they stay silent. */
|
|
33941
|
+
noteIfUnpriced(entry) {
|
|
33942
|
+
if (entry.cost.source !== "unknown") return;
|
|
33943
|
+
const key = `${entry.provider}/${entry.model}`;
|
|
33944
|
+
if (this.warnedUnpriced.has(key)) return;
|
|
33945
|
+
this.warnedUnpriced.add(key);
|
|
33946
|
+
this.hooks.emitSync("onWarning", {
|
|
33947
|
+
source: "cost",
|
|
33948
|
+
code: "unpriced_model",
|
|
33949
|
+
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.`,
|
|
33950
|
+
details: { provider: entry.provider, model: entry.model }
|
|
33951
|
+
});
|
|
33952
|
+
}
|
|
33228
33953
|
checkBudgets(entry) {
|
|
33229
33954
|
for (const budget of this.budgets) {
|
|
33230
33955
|
if (!matchesScope(entry, budget.scope)) continue;
|
|
@@ -34269,11 +34994,12 @@ function createEngine(config = {}) {
|
|
|
34269
34994
|
const persistence = resolvePersistence(config.persistence);
|
|
34270
34995
|
const cache2 = resolveCache(config.cache);
|
|
34271
34996
|
const catalog = resolveCatalog(config.catalog);
|
|
34272
|
-
const network = new NetworkEngine({ hooks, fetch: config.fetch });
|
|
34997
|
+
const network = new NetworkEngine({ hooks, fetch: config.fetch, retry: config.retry, queues: config.queues });
|
|
34273
34998
|
const fetchBound = (req, options) => network.fetch(req, options);
|
|
34274
34999
|
const fetchStreamBound = (req, options) => network.fetchStream(req, options);
|
|
34275
35000
|
const connectBound = (req) => network.connect(req);
|
|
34276
35001
|
const cost = new CostCollector({ hooks, catalog });
|
|
35002
|
+
const telemetry = config.telemetry ? new TelemetryAdapter(hooks, config.telemetry) : null;
|
|
34277
35003
|
const handle = {
|
|
34278
35004
|
sessionId,
|
|
34279
35005
|
hooks,
|
|
@@ -34286,9 +35012,11 @@ function createEngine(config = {}) {
|
|
|
34286
35012
|
connect: connectBound,
|
|
34287
35013
|
catalog,
|
|
34288
35014
|
cost,
|
|
35015
|
+
telemetry,
|
|
34289
35016
|
apiKeys: config.apiKeys ?? {},
|
|
34290
35017
|
destroy() {
|
|
34291
35018
|
cost.destroy();
|
|
35019
|
+
telemetry?.destroy();
|
|
34292
35020
|
network.destroy();
|
|
34293
35021
|
}
|
|
34294
35022
|
};
|
|
@@ -37562,6 +38290,7 @@ function defineTool(input) {
|
|
|
37562
38290
|
if (!optional.has(key)) required.push(key);
|
|
37563
38291
|
}
|
|
37564
38292
|
return {
|
|
38293
|
+
...input.lazy ? { lazy: true } : {},
|
|
37565
38294
|
definition: {
|
|
37566
38295
|
name: input.name,
|
|
37567
38296
|
description: input.description,
|
|
@@ -38050,7 +38779,14 @@ async function complete(opts) {
|
|
|
38050
38779
|
temperature: opts.temperature
|
|
38051
38780
|
});
|
|
38052
38781
|
res = await loop.complete(input, {
|
|
38053
|
-
structured: opts.structured
|
|
38782
|
+
structured: opts.structured,
|
|
38783
|
+
providerOptions: opts.providerOptions,
|
|
38784
|
+
audio: opts.audio,
|
|
38785
|
+
outputModalities: opts.outputModalities,
|
|
38786
|
+
serviceTier,
|
|
38787
|
+
cache: opts.cache,
|
|
38788
|
+
topK: opts.topK,
|
|
38789
|
+
seed: opts.seed
|
|
38054
38790
|
});
|
|
38055
38791
|
} else {
|
|
38056
38792
|
res = await llm.complete(input, {
|
|
@@ -38061,12 +38797,16 @@ async function complete(opts) {
|
|
|
38061
38797
|
providerOptions: opts.providerOptions,
|
|
38062
38798
|
audio: opts.audio,
|
|
38063
38799
|
outputModalities: opts.outputModalities,
|
|
38064
|
-
serviceTier
|
|
38800
|
+
serviceTier,
|
|
38801
|
+
cache: opts.cache,
|
|
38802
|
+
topK: opts.topK,
|
|
38803
|
+
seed: opts.seed
|
|
38065
38804
|
});
|
|
38066
38805
|
}
|
|
38067
38806
|
const result = {
|
|
38068
38807
|
text: res.text,
|
|
38069
38808
|
response: res,
|
|
38809
|
+
...res.error ? { error: res.error } : {},
|
|
38070
38810
|
// Bound to this call's client (same provider/model/key/engine).
|
|
38071
38811
|
retrieveFile: (file) => llm.retrieveFile(file),
|
|
38072
38812
|
streamFile: (file) => llm.streamFile(file)
|
|
@@ -38412,10 +39152,21 @@ var McpResultCache = class {
|
|
|
38412
39152
|
}
|
|
38413
39153
|
return hit.value;
|
|
38414
39154
|
}
|
|
38415
|
-
/** Store only when the server actually asked for it. Returns whether anything was stored.
|
|
39155
|
+
/** Store only when the server actually asked for it. Returns whether anything was stored.
|
|
39156
|
+
*
|
|
39157
|
+
* A non-positive `ttlMs` is an instruction, not a missing value: the server is saying *do not
|
|
39158
|
+
* reuse this*. Any entry already held under that key is dropped, so the next `get` re-fetches.
|
|
39159
|
+
* Without the eviction the hint is inert — a server that first said "cache for 60s" and then
|
|
39160
|
+
* says "stale now" would keep being answered from the stale entry for the rest of the original
|
|
39161
|
+
* TTL. Absent hints are different and must stay different: they carry no instruction, so an
|
|
39162
|
+
* existing entry is left alone and pre-2026 servers behave exactly as before. */
|
|
38416
39163
|
set(key, value, hints, now = Date.now()) {
|
|
38417
39164
|
const ttl = hints?.ttlMs;
|
|
38418
|
-
if (typeof ttl
|
|
39165
|
+
if (typeof ttl === "number" && Number.isFinite(ttl) && ttl <= 0) {
|
|
39166
|
+
this.entries.delete(key);
|
|
39167
|
+
return false;
|
|
39168
|
+
}
|
|
39169
|
+
if (typeof ttl !== "number" || !Number.isFinite(ttl)) return false;
|
|
38419
39170
|
this.entries.set(key, {
|
|
38420
39171
|
value,
|
|
38421
39172
|
expiresAt: now + ttl,
|
|
@@ -39573,11 +40324,23 @@ function mcpPromptToMessages(result) {
|
|
|
39573
40324
|
}
|
|
39574
40325
|
function mcpToolToAgentTool(client, tool, namespace, opts = {}) {
|
|
39575
40326
|
return {
|
|
40327
|
+
...opts.lazy ? { lazy: true } : {},
|
|
39576
40328
|
definition: {
|
|
39577
40329
|
type: "function",
|
|
39578
40330
|
name: `${namespace}__${tool.name}`,
|
|
39579
40331
|
description: tool.description ?? tool.title ?? tool.name,
|
|
39580
|
-
parameters: tool.inputSchema ?? { type: "object", properties: {} }
|
|
40332
|
+
parameters: tool.inputSchema ?? { type: "object", properties: {} },
|
|
40333
|
+
// MCP publishes a schema for the tool's structured output and OpenAI
|
|
40334
|
+
// Responses accepts one (`output_schema`), so the model can reason over the
|
|
40335
|
+
// shape it will get back.
|
|
40336
|
+
//
|
|
40337
|
+
// Gated on `validateOutput` because declaring it is a PROMISE, not a hint:
|
|
40338
|
+
// the provider then requires the result to be JSON matching the schema, so
|
|
40339
|
+
// the tool result changes from prose to structured data. Forwarding it
|
|
40340
|
+
// unconditionally would silently reshape every existing MCP tool result —
|
|
40341
|
+
// and did, until a live round trip through OpenAI Responses failed. Anyone
|
|
40342
|
+
// asking for output validation has already opted into that contract.
|
|
40343
|
+
...opts.validateOutput && tool.outputSchema ? { outputSchema: tool.outputSchema } : {}
|
|
39581
40344
|
},
|
|
39582
40345
|
execute: async (args, ctx) => {
|
|
39583
40346
|
const res = await client.callTool(tool.name, args, ctx.trace);
|
|
@@ -39585,6 +40348,9 @@ function mcpToolToAgentTool(client, tool, namespace, opts = {}) {
|
|
|
39585
40348
|
const errors = validateJsonSchema(tool.outputSchema, res.structuredContent);
|
|
39586
40349
|
if (errors.length > 0) return `Tool output failed schema validation: ${errors.slice(0, 5).join("; ")}`;
|
|
39587
40350
|
}
|
|
40351
|
+
if (opts.validateOutput && tool.outputSchema && res.structuredContent !== void 0 && !res.isError) {
|
|
40352
|
+
return JSON.stringify(res.structuredContent);
|
|
40353
|
+
}
|
|
39588
40354
|
return mcpContentToResult(res);
|
|
39589
40355
|
}
|
|
39590
40356
|
};
|
|
@@ -40321,7 +41087,9 @@ async function connectMcp(config, opts = {}) {
|
|
|
40321
41087
|
const refresh = async (c) => {
|
|
40322
41088
|
const defs = await c.listTools();
|
|
40323
41089
|
tools.length = 0;
|
|
40324
|
-
for (const d of defs)
|
|
41090
|
+
for (const d of defs) {
|
|
41091
|
+
tools.push(mcpToolToAgentTool(c, d, ns, { validateOutput: opts.validateOutput, lazy: opts.lazy }));
|
|
41092
|
+
}
|
|
40325
41093
|
};
|
|
40326
41094
|
const sampler = opts.sampling ? samplingHandler(opts.sampling) : null;
|
|
40327
41095
|
const capabilities = {};
|
|
@@ -42853,6 +43621,8 @@ export {
|
|
|
42853
43621
|
LAYER_EXECUTOR_TOOL_EXAMPLES,
|
|
42854
43622
|
LAYER_LEGACY_SYSTEM,
|
|
42855
43623
|
LAYER_MEMORY,
|
|
43624
|
+
LAZY_CALL_TOOL,
|
|
43625
|
+
LAZY_SEARCH_TOOL,
|
|
42856
43626
|
LLMClient,
|
|
42857
43627
|
LLMError,
|
|
42858
43628
|
LLM_DEF_KEY,
|
|
@@ -42980,6 +43750,7 @@ export {
|
|
|
42980
43750
|
defineLLMTool,
|
|
42981
43751
|
defineTool,
|
|
42982
43752
|
delegate,
|
|
43753
|
+
describeTool,
|
|
42983
43754
|
discoverMetadata,
|
|
42984
43755
|
dispatch,
|
|
42985
43756
|
embed,
|
|
@@ -43041,6 +43812,7 @@ export {
|
|
|
43041
43812
|
parseSSEStream,
|
|
43042
43813
|
parseToolId,
|
|
43043
43814
|
pcmToWav,
|
|
43815
|
+
rankTools,
|
|
43044
43816
|
readFactsLayer,
|
|
43045
43817
|
reflectionGuidance,
|
|
43046
43818
|
refreshTokens,
|
|
@@ -43058,7 +43830,9 @@ export {
|
|
|
43058
43830
|
selectVariant,
|
|
43059
43831
|
shellGlob,
|
|
43060
43832
|
sniffImageMime,
|
|
43833
|
+
strictSupport,
|
|
43061
43834
|
submitBatch,
|
|
43835
|
+
toolKey,
|
|
43062
43836
|
transcribe,
|
|
43063
43837
|
trimReplacer,
|
|
43064
43838
|
tryParseToolId,
|