@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.browser.js
CHANGED
|
@@ -165,6 +165,86 @@ var REDACTED = "***REDACTED***";
|
|
|
165
165
|
var SENSITIVE_QUERY_PARAMS = /* @__PURE__ */ new Set(["key", "api_key", "access_token", "token"]);
|
|
166
166
|
var SENSITIVE_HEADERS = /* @__PURE__ */ new Set(["authorization", "x-goog-api-key", "x-api-key", "api-key"]);
|
|
167
167
|
var MAX_ERROR_RAW_CHARS = 512;
|
|
168
|
+
var OTLP_SPAN_KIND = { internal: 1, client: 3 };
|
|
169
|
+
var OTLP_KIND_BY_SPAN = {
|
|
170
|
+
llm: OTLP_SPAN_KIND.client,
|
|
171
|
+
http: OTLP_SPAN_KIND.client,
|
|
172
|
+
mcp: OTLP_SPAN_KIND.client,
|
|
173
|
+
media: OTLP_SPAN_KIND.client,
|
|
174
|
+
agent: OTLP_SPAN_KIND.internal,
|
|
175
|
+
tool: OTLP_SPAN_KIND.internal,
|
|
176
|
+
other: OTLP_SPAN_KIND.internal
|
|
177
|
+
};
|
|
178
|
+
function fnv1a32(input, seed) {
|
|
179
|
+
let h = seed >>> 0;
|
|
180
|
+
for (let i = 0; i < input.length; i++) {
|
|
181
|
+
h ^= input.charCodeAt(i);
|
|
182
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
183
|
+
}
|
|
184
|
+
return h >>> 0;
|
|
185
|
+
}
|
|
186
|
+
var isHex = (value, chars) => value.length === chars && /^[0-9a-f]+$/.test(value);
|
|
187
|
+
function toOtlpId(input, bytes) {
|
|
188
|
+
let out = "";
|
|
189
|
+
for (let i = 0; i < bytes / 4; i++) {
|
|
190
|
+
out += fnv1a32(input, 2166136261 + i * 2654435769 >>> 0).toString(16).padStart(8, "0");
|
|
191
|
+
}
|
|
192
|
+
return /^0+$/.test(out) ? `${out.slice(0, -1)}1` : out;
|
|
193
|
+
}
|
|
194
|
+
function toOtlpValue(value) {
|
|
195
|
+
if (typeof value === "boolean") return { boolValue: value };
|
|
196
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
197
|
+
return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
|
|
198
|
+
}
|
|
199
|
+
if (typeof value === "string") return { stringValue: value };
|
|
200
|
+
if (value === null || value === void 0) return { stringValue: "" };
|
|
201
|
+
return { stringValue: typeof value === "object" ? JSON.stringify(value) : String(value) };
|
|
202
|
+
}
|
|
203
|
+
var SPAN_NAME_SUBJECT = {
|
|
204
|
+
chat: "gen_ai.request.model",
|
|
205
|
+
invoke_agent: "gen_ai.agent.name",
|
|
206
|
+
execute_tool: "gen_ai.tool.name"
|
|
207
|
+
};
|
|
208
|
+
function otlpSpanName(span) {
|
|
209
|
+
const op = span.attributes["gen_ai.operation.name"];
|
|
210
|
+
if (typeof op !== "string") return span.name;
|
|
211
|
+
const subject = SPAN_NAME_SUBJECT[op] ? span.attributes[SPAN_NAME_SUBJECT[op]] : void 0;
|
|
212
|
+
return typeof subject === "string" && subject ? `${op} ${subject}` : op;
|
|
213
|
+
}
|
|
214
|
+
function toMessageList(payload, defaultRole) {
|
|
215
|
+
if (payload == null) return [];
|
|
216
|
+
if (typeof payload === "string") {
|
|
217
|
+
return payload ? [{ role: defaultRole, content: payload }] : [];
|
|
218
|
+
}
|
|
219
|
+
if (Array.isArray(payload)) {
|
|
220
|
+
const parts2 = payload;
|
|
221
|
+
if (parts2.length > 0 && parts2[0] && "role" in parts2[0]) {
|
|
222
|
+
return parts2.map((m) => ({ role: String(m.role ?? defaultRole), content: contentToText(m.content) })).filter((m) => m.content);
|
|
223
|
+
}
|
|
224
|
+
const text2 = contentToText(parts2);
|
|
225
|
+
return text2 ? [{ role: defaultRole, content: text2 }] : [];
|
|
226
|
+
}
|
|
227
|
+
const text = contentToText(payload);
|
|
228
|
+
return text ? [{ role: defaultRole, content: text }] : [];
|
|
229
|
+
}
|
|
230
|
+
function contentToText(content) {
|
|
231
|
+
if (typeof content === "string") return content;
|
|
232
|
+
if (!Array.isArray(content)) return "";
|
|
233
|
+
return content.map((part) => {
|
|
234
|
+
const p = part;
|
|
235
|
+
return typeof p?.text === "string" ? p.text : "";
|
|
236
|
+
}).filter(Boolean).join("");
|
|
237
|
+
}
|
|
238
|
+
var EVENT_TYPE_BY_KIND = {
|
|
239
|
+
agent: "agent",
|
|
240
|
+
tool: "tool",
|
|
241
|
+
llm: "llm",
|
|
242
|
+
http: "http",
|
|
243
|
+
mcp: "mcp",
|
|
244
|
+
media: "media",
|
|
245
|
+
other: "other"
|
|
246
|
+
};
|
|
247
|
+
var SAMPLE_SEED = 2654435769;
|
|
168
248
|
var CATEGORY = {
|
|
169
249
|
// Network
|
|
170
250
|
onEnqueue: "network",
|
|
@@ -233,10 +313,29 @@ function traceIdsOf(ctx) {
|
|
|
233
313
|
const t = c?.trace ?? c?.ctx ?? c;
|
|
234
314
|
return {
|
|
235
315
|
sessionId: t?.sessionId,
|
|
236
|
-
requestId: t?.requestId
|
|
316
|
+
requestId: t?.requestId,
|
|
317
|
+
/** W3C parent context, when the app is already inside a trace of its own. */
|
|
318
|
+
traceparent: t?.traceparent,
|
|
319
|
+
// `gen_ai.conversation.id` in the semantic conventions — the thread a turn
|
|
320
|
+
// belongs to, which is what lets a backend group turns into one conversation.
|
|
321
|
+
// AgentLoop sets it from the history id; a bare client call has none.
|
|
322
|
+
conversationId: t?.conversationId
|
|
237
323
|
};
|
|
238
324
|
}
|
|
239
|
-
|
|
325
|
+
function parseTraceparent(value) {
|
|
326
|
+
if (!value) return null;
|
|
327
|
+
const m = /^[0-9a-f]{2}-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/.exec(value.trim().toLowerCase());
|
|
328
|
+
if (!m) return null;
|
|
329
|
+
const [, traceId, spanId] = m;
|
|
330
|
+
if (/^0+$/.test(traceId) || /^0+$/.test(spanId)) return null;
|
|
331
|
+
return { traceId, spanId };
|
|
332
|
+
}
|
|
333
|
+
var CONTAINER_SPANS = /* @__PURE__ */ new Set(["agent.run", "tool.call"]);
|
|
334
|
+
var traceKey = (ids) => {
|
|
335
|
+
const parent = parseTraceparent(ids.traceparent);
|
|
336
|
+
if (parent) return parent.traceId;
|
|
337
|
+
return ids.requestId ? `${ids.sessionId ?? "?"}:${ids.requestId}` : void 0;
|
|
338
|
+
};
|
|
240
339
|
var TelemetryAdapter = class {
|
|
241
340
|
events = [];
|
|
242
341
|
spans = [];
|
|
@@ -257,17 +356,138 @@ var TelemetryAdapter = class {
|
|
|
257
356
|
/** Service identity stamped on exported telemetry. */
|
|
258
357
|
resource;
|
|
259
358
|
seq = 0;
|
|
359
|
+
/** Discriminator for POINT spans (media, mcp connect/tool), whose natural keys are
|
|
360
|
+
* not unique — the same server reconnects, a run emits two images, two tool calls
|
|
361
|
+
* land in one millisecond. A duplicate span id inside a trace is invalid OTLP and
|
|
362
|
+
* the backend silently keeps only one. */
|
|
363
|
+
spanSeq = 0;
|
|
364
|
+
/** Per trace: the app's span from a `traceparent`, and the CONTAINER spans currently
|
|
365
|
+
* open on it. Together they decide what a new span hangs under — see `parentFor`.
|
|
366
|
+
* Both are cleared once a trace has nothing open, so a long-lived process does not
|
|
367
|
+
* accumulate an entry per conversation forever.
|
|
368
|
+
*
|
|
369
|
+
* A list, not a single slot: an agent nested in a tool call (C2 inside C1's tool) is a
|
|
370
|
+
* second run on the SAME trace, and with one slot it overwrote its own parent and then
|
|
371
|
+
* deleted it on close — leaving the rest of the outer run parentless. */
|
|
372
|
+
appParent = /* @__PURE__ */ new Map();
|
|
373
|
+
containers = /* @__PURE__ */ new Map();
|
|
260
374
|
latSum = 0;
|
|
261
375
|
open = /* @__PURE__ */ new Map();
|
|
262
376
|
maxEvents;
|
|
263
377
|
includeSensitiveData;
|
|
264
378
|
unsub;
|
|
379
|
+
/** Subscribers, each with its own filter. Re-parenting is computed PER SINK: two
|
|
380
|
+
* consumers asking for different types each get a tree that is correct for them. */
|
|
381
|
+
sinks = [];
|
|
382
|
+
content;
|
|
383
|
+
sampleRate;
|
|
384
|
+
/** spanId → its parent and type, for EVERY span including filtered ones — walking up
|
|
385
|
+
* past a dropped ancestor is the whole point, so the dropped ones must still be here.
|
|
386
|
+
* Bounded, because a long-lived process would otherwise remember every span it ever
|
|
387
|
+
* saw. */
|
|
388
|
+
lineage = /* @__PURE__ */ new Map();
|
|
389
|
+
maxLineage;
|
|
390
|
+
msgSeq = 0;
|
|
265
391
|
constructor(hooks, opts = {}) {
|
|
266
392
|
this.maxEvents = opts.maxEvents ?? 2e3;
|
|
267
393
|
this.includeSensitiveData = opts.includeSensitiveData ?? true;
|
|
268
394
|
this.resource = opts.resource ?? { serviceName: "unknown_service" };
|
|
395
|
+
this.content = opts.content ?? "none";
|
|
396
|
+
this.sampleRate = opts.sample ?? 1;
|
|
397
|
+
this.maxLineage = this.maxEvents * 2;
|
|
398
|
+
if (opts.onTrace) this.onTrace({ types: opts.types }, opts.onTrace);
|
|
269
399
|
this.unsub = hooks.onAny((name, ctx) => this.handle(name, ctx));
|
|
270
400
|
}
|
|
401
|
+
onTrace(filterOrHandler, maybeHandler) {
|
|
402
|
+
const handler = typeof filterOrHandler === "function" ? filterOrHandler : maybeHandler;
|
|
403
|
+
if (!handler) throw new Error("onTrace requires a handler");
|
|
404
|
+
const filter = typeof filterOrHandler === "function" ? {} : filterOrHandler;
|
|
405
|
+
const sink = { types: filter.types ? new Set(filter.types) : void 0, handler };
|
|
406
|
+
this.sinks.push(sink);
|
|
407
|
+
return () => {
|
|
408
|
+
const at = this.sinks.indexOf(sink);
|
|
409
|
+
if (at !== -1) this.sinks.splice(at, 1);
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
/** Record a finished span and hand it to the subscribers. Every span reaches the store
|
|
413
|
+
* through here, so there is one place where an event can be missed rather than five. */
|
|
414
|
+
recordSpan(span) {
|
|
415
|
+
this.spans.push(span);
|
|
416
|
+
const type = EVENT_TYPE_BY_KIND[span.kind];
|
|
417
|
+
if (!this.lineage.has(span.spanId)) this.remember(span.spanId, span.parentSpanId, type);
|
|
418
|
+
this.dispatch({
|
|
419
|
+
type,
|
|
420
|
+
traceId: span.traceId,
|
|
421
|
+
spanId: span.spanId,
|
|
422
|
+
parentSpanId: span.parentSpanId,
|
|
423
|
+
name: otlpSpanName(span),
|
|
424
|
+
startTime: span.startTime,
|
|
425
|
+
endTime: span.endTime,
|
|
426
|
+
durationMs: span.durationMs,
|
|
427
|
+
status: span.status,
|
|
428
|
+
attributes: span.attributes
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
remember(spanId, parentSpanId, type) {
|
|
432
|
+
this.lineage.set(spanId, { parentSpanId, type });
|
|
433
|
+
if (this.lineage.size > this.maxLineage) {
|
|
434
|
+
const oldest = this.lineage.keys().next().value;
|
|
435
|
+
if (oldest !== void 0) this.lineage.delete(oldest);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
dispatch(event) {
|
|
439
|
+
if (this.sinks.length === 0) return;
|
|
440
|
+
if (!this.isSampled(event.traceId)) return;
|
|
441
|
+
for (const sink of this.sinks) {
|
|
442
|
+
if (sink.types && !sink.types.has(event.type)) continue;
|
|
443
|
+
const parentSpanId = sink.types ? this.survivingParent(event.parentSpanId, sink.types) : event.parentSpanId;
|
|
444
|
+
sink.handler(parentSpanId === event.parentSpanId ? event : { ...event, parentSpanId });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/** The nearest ancestor this subscriber actually receives. Without this, filtering out
|
|
448
|
+
* `http` would leave its children pointing at a span that never arrives, and a backend
|
|
449
|
+
* renders a dangling parent as a separate root. */
|
|
450
|
+
survivingParent(parentSpanId, types) {
|
|
451
|
+
let id = parentSpanId;
|
|
452
|
+
while (id) {
|
|
453
|
+
const node = this.lineage.get(id);
|
|
454
|
+
if (!node) return void 0;
|
|
455
|
+
if (types.has(node.type)) return id;
|
|
456
|
+
id = node.parentSpanId;
|
|
457
|
+
}
|
|
458
|
+
return void 0;
|
|
459
|
+
}
|
|
460
|
+
/** Hashed rather than random, so the same trace samples the same way in every process
|
|
461
|
+
* and a trace shared by two services is kept or dropped by both. */
|
|
462
|
+
isSampled(traceId) {
|
|
463
|
+
if (this.sampleRate >= 1) return true;
|
|
464
|
+
if (this.sampleRate <= 0) return false;
|
|
465
|
+
return fnv1a32(traceId, SAMPLE_SEED) / 4294967296 < this.sampleRate;
|
|
466
|
+
}
|
|
467
|
+
/** Conversation content, as its own event so it can be routed somewhere different from
|
|
468
|
+
* the spans — a debug store, not the metrics backend. */
|
|
469
|
+
emitMessage(span, direction, payload) {
|
|
470
|
+
if (this.sinks.length === 0) return;
|
|
471
|
+
const messages = toMessageList(payload, direction === "input" ? "user" : "assistant");
|
|
472
|
+
if (messages.length === 0) return;
|
|
473
|
+
const chars = messages.reduce((n, m) => n + m.content.length, 0);
|
|
474
|
+
this.dispatch({
|
|
475
|
+
type: "message",
|
|
476
|
+
traceId: span.traceId,
|
|
477
|
+
spanId: `${span.spanId}:msg${this.msgSeq++}`,
|
|
478
|
+
parentSpanId: span.spanId,
|
|
479
|
+
name: `message.${direction}`,
|
|
480
|
+
startTime: Date.now(),
|
|
481
|
+
status: "unset",
|
|
482
|
+
attributes: clean({
|
|
483
|
+
"message.direction": direction,
|
|
484
|
+
"message.count": messages.length,
|
|
485
|
+
"message.chars": chars,
|
|
486
|
+
// Opt-In in the spec, and off by default here for the same reason.
|
|
487
|
+
[`gen_ai.${direction}.messages`]: this.content === "full" ? messages : void 0
|
|
488
|
+
})
|
|
489
|
+
});
|
|
490
|
+
}
|
|
271
491
|
/** Stop tapping the bus. */
|
|
272
492
|
destroy() {
|
|
273
493
|
this.unsub();
|
|
@@ -275,6 +495,8 @@ var TelemetryAdapter = class {
|
|
|
275
495
|
handle(name, ctx) {
|
|
276
496
|
const ids = traceIdsOf(ctx);
|
|
277
497
|
const traceId = traceKey(ids);
|
|
498
|
+
const parent = parseTraceparent(ids.traceparent);
|
|
499
|
+
if (parent && traceId) this.appParent.set(traceId, parent.spanId);
|
|
278
500
|
this.events.push({
|
|
279
501
|
seq: this.seq++,
|
|
280
502
|
time: Date.now(),
|
|
@@ -297,20 +519,27 @@ var TelemetryAdapter = class {
|
|
|
297
519
|
this.metrics.outputTokens += usage.outputTokens ?? 0;
|
|
298
520
|
}
|
|
299
521
|
if (traceId) {
|
|
522
|
+
const responseModel = c.response?.model;
|
|
300
523
|
const attrs = {
|
|
301
|
-
"gen_ai.provider": c.provider,
|
|
302
|
-
"gen_ai.
|
|
524
|
+
"gen_ai.provider.name": c.provider,
|
|
525
|
+
"gen_ai.operation.name": "chat",
|
|
526
|
+
"gen_ai.request.model": c.model,
|
|
527
|
+
// The model that actually answered, which can differ from the one asked
|
|
528
|
+
// for (an alias resolving to a dated snapshot, a router picking a peer).
|
|
529
|
+
"gen_ai.response.model": responseModel,
|
|
530
|
+
"gen_ai.conversation.id": ids.conversationId,
|
|
303
531
|
"gen_ai.usage.input_tokens": usage?.inputTokens,
|
|
304
532
|
"gen_ai.usage.output_tokens": usage?.outputTokens
|
|
305
533
|
};
|
|
306
534
|
const key = `llm:${traceId}`;
|
|
535
|
+
let llmSpan;
|
|
307
536
|
if (this.open.has(key)) {
|
|
308
|
-
this.closeSpan(key, "ok", attrs);
|
|
537
|
+
llmSpan = this.closeSpan(key, "ok", attrs);
|
|
309
538
|
} else {
|
|
310
539
|
const http = [...this.spans].reverse().find((s) => s.traceId === traceId && s.kind === "http");
|
|
311
540
|
const start = http?.startTime ?? Date.now();
|
|
312
541
|
const end = Date.now();
|
|
313
|
-
|
|
542
|
+
llmSpan = {
|
|
314
543
|
traceId,
|
|
315
544
|
spanId: key,
|
|
316
545
|
name: "llm.request",
|
|
@@ -320,7 +549,12 @@ var TelemetryAdapter = class {
|
|
|
320
549
|
durationMs: end - start,
|
|
321
550
|
status: "ok",
|
|
322
551
|
attributes: clean(attrs)
|
|
323
|
-
}
|
|
552
|
+
};
|
|
553
|
+
this.recordSpan(llmSpan);
|
|
554
|
+
}
|
|
555
|
+
if (llmSpan) {
|
|
556
|
+
const response = c.response;
|
|
557
|
+
this.emitMessage(llmSpan, "output", response?.content ?? response?.text);
|
|
324
558
|
}
|
|
325
559
|
}
|
|
326
560
|
break;
|
|
@@ -366,9 +600,11 @@ var TelemetryAdapter = class {
|
|
|
366
600
|
this.metrics.mediaGenerated += c.count ?? 1;
|
|
367
601
|
if (traceId) {
|
|
368
602
|
const now = Date.now();
|
|
369
|
-
this.
|
|
603
|
+
this.recordSpan({
|
|
370
604
|
traceId,
|
|
371
|
-
|
|
605
|
+
// One run can generate several images; `media:${traceId}` would give them
|
|
606
|
+
// all the same span id, which is invalid within a trace.
|
|
607
|
+
spanId: `media:${traceId}:${this.spanSeq++}`,
|
|
372
608
|
name: "media.generate",
|
|
373
609
|
kind: "media",
|
|
374
610
|
startTime: now,
|
|
@@ -383,10 +619,21 @@ var TelemetryAdapter = class {
|
|
|
383
619
|
case "onRunStart": {
|
|
384
620
|
const runId = c.runId;
|
|
385
621
|
if (runId) {
|
|
386
|
-
this.openSpan(`agent:${runId}`, runId, "agent.run", "agent", {
|
|
387
|
-
|
|
388
|
-
|
|
622
|
+
const runSpan = this.openSpan(`agent:${runId}`, traceId ?? runId, "agent.run", "agent", {
|
|
623
|
+
// The host's own attributes go FIRST so ours win on a key collision: a stray
|
|
624
|
+
// `gen_ai.*` key in a caller's bag must not be able to rewrite the identity
|
|
625
|
+
// of the span.
|
|
626
|
+
...c.attributes,
|
|
627
|
+
"gen_ai.operation.name": "invoke_agent",
|
|
628
|
+
// Named when the agent was given a label; the exported span is then
|
|
629
|
+
// `invoke_agent {label}` rather than the bare operation.
|
|
630
|
+
"gen_ai.agent.name": c.label,
|
|
631
|
+
"gen_ai.agent.id": c.agentId,
|
|
632
|
+
"gen_ai.request.model": c.model,
|
|
633
|
+
// Ours, not a convention attribute — the GenAI spec has no term for it.
|
|
634
|
+
"agent.source": c.source
|
|
389
635
|
});
|
|
636
|
+
this.emitMessage(runSpan, "input", c.userMessage);
|
|
390
637
|
}
|
|
391
638
|
break;
|
|
392
639
|
}
|
|
@@ -413,9 +660,11 @@ var TelemetryAdapter = class {
|
|
|
413
660
|
case "onToolCallStart": {
|
|
414
661
|
const callId = c.callId;
|
|
415
662
|
if (callId) {
|
|
416
|
-
this.openSpan(`tool:${callId}`, callId, "tool.call", "tool", {
|
|
417
|
-
"
|
|
418
|
-
"
|
|
663
|
+
this.openSpan(`tool:${callId}`, traceId ?? callId, "tool.call", "tool", {
|
|
664
|
+
"gen_ai.operation.name": "execute_tool",
|
|
665
|
+
"gen_ai.tool.name": c.toolName,
|
|
666
|
+
"gen_ai.tool.call.id": callId,
|
|
667
|
+
"gen_ai.agent.id": c.agentId
|
|
419
668
|
});
|
|
420
669
|
}
|
|
421
670
|
break;
|
|
@@ -424,7 +673,7 @@ var TelemetryAdapter = class {
|
|
|
424
673
|
const callId = c.callId;
|
|
425
674
|
if (callId) {
|
|
426
675
|
this.closeSpan(`tool:${callId}`, "ok", {
|
|
427
|
-
"tool.name": c.toolName,
|
|
676
|
+
"gen_ai.tool.name": c.toolName,
|
|
428
677
|
"tool.latency_ms": c.latencyMs
|
|
429
678
|
});
|
|
430
679
|
}
|
|
@@ -434,7 +683,7 @@ var TelemetryAdapter = class {
|
|
|
434
683
|
const callId = c.callId;
|
|
435
684
|
if (callId) {
|
|
436
685
|
this.closeSpan(`tool:${callId}`, "error", {
|
|
437
|
-
"tool.name": c.toolName,
|
|
686
|
+
"gen_ai.tool.name": c.toolName,
|
|
438
687
|
"tool.error": c.error?.message
|
|
439
688
|
});
|
|
440
689
|
}
|
|
@@ -445,9 +694,17 @@ var TelemetryAdapter = class {
|
|
|
445
694
|
const server = c.server;
|
|
446
695
|
if (server) {
|
|
447
696
|
const now = Date.now();
|
|
448
|
-
this.
|
|
449
|
-
|
|
450
|
-
|
|
697
|
+
this.recordSpan({
|
|
698
|
+
// A connect usually happens at startup, outside any run, so there is often
|
|
699
|
+
// no trace to join — but keying the trace by server name merged every
|
|
700
|
+
// reconnect over the process lifetime into one trace. Falls back to a span
|
|
701
|
+
// of its own instead.
|
|
702
|
+
traceId: traceId ?? `mcp:connect:${server}:${this.spanSeq}`,
|
|
703
|
+
// `${server}` alone repeats on every reconnect, and a duplicate span id
|
|
704
|
+
// within a trace is invalid OTLP — the backend keeps one and drops the
|
|
705
|
+
// rest. The counter is monotonic where a timestamp is not: two connects
|
|
706
|
+
// inside the same millisecond would still collide.
|
|
707
|
+
spanId: `mcp:connect:${server}:${this.spanSeq++}`,
|
|
451
708
|
name: "mcp.connect",
|
|
452
709
|
kind: "mcp",
|
|
453
710
|
startTime: now,
|
|
@@ -469,9 +726,14 @@ var TelemetryAdapter = class {
|
|
|
469
726
|
if (server && tool) {
|
|
470
727
|
const now = Date.now();
|
|
471
728
|
const lat = c.latencyMs;
|
|
472
|
-
this.
|
|
473
|
-
|
|
474
|
-
|
|
729
|
+
this.recordSpan({
|
|
730
|
+
// An MCP tool call happens INSIDE a run, so it belongs to that run's trace.
|
|
731
|
+
// Keying it by server put every call to one server in a single eternal
|
|
732
|
+
// trace, and none of them with the agent that made the call.
|
|
733
|
+
traceId: traceId ?? `mcp:${server}`,
|
|
734
|
+
// A timestamp is not a unique key: two tool calls in the same millisecond
|
|
735
|
+
// share it. The counter is.
|
|
736
|
+
spanId: `mcp:tool:${server}:${tool}:${this.spanSeq++}`,
|
|
475
737
|
name: "mcp.tool_call",
|
|
476
738
|
kind: "mcp",
|
|
477
739
|
startTime: now - (lat ?? 0),
|
|
@@ -489,10 +751,33 @@ var TelemetryAdapter = class {
|
|
|
489
751
|
}
|
|
490
752
|
}
|
|
491
753
|
}
|
|
754
|
+
/** What a new span on this trace hangs under: the innermost container still open on
|
|
755
|
+
* it, else the app's span, else nothing (we are the root).
|
|
756
|
+
*
|
|
757
|
+
* A container wins over the app's span because an LLM call made during a run belongs
|
|
758
|
+
* to that run — attaching it straight to the app would flatten the very nesting the
|
|
759
|
+
* tree exists to show. A span joins the stack only after it is built, so nothing can
|
|
760
|
+
* become its own parent, and a run nested in a tool call lands under that tool call —
|
|
761
|
+
* exactly where it happened.
|
|
762
|
+
*
|
|
763
|
+
* Limit worth naming: with tools running in parallel two `tool.call` spans are open at
|
|
764
|
+
* once and "innermost" is merely the more recent one. Attributing a nested run to the
|
|
765
|
+
* right sibling needs real async context propagation, which this adapter does not
|
|
766
|
+
* have; sequential tools, the common case, are exact. */
|
|
767
|
+
parentFor(traceId) {
|
|
768
|
+
const stack = this.containers.get(traceId);
|
|
769
|
+
return stack?.[stack.length - 1] ?? this.appParent.get(traceId);
|
|
770
|
+
}
|
|
492
771
|
openSpan(key, traceId, spanName, kind, attributes) {
|
|
772
|
+
const parentSpanId = this.parentFor(traceId);
|
|
493
773
|
const span = {
|
|
494
774
|
traceId,
|
|
495
|
-
|
|
775
|
+
...parentSpanId ? { parentSpanId } : {},
|
|
776
|
+
// The KEY pairs open with close (`llm:${traceId}`); the SPAN ID must be unique.
|
|
777
|
+
// Those were the same string until a run stopped fragmenting into one trace per
|
|
778
|
+
// call — at which point every LLM call in a run produced the identical key, and
|
|
779
|
+
// the collision merged them into one span at the collector.
|
|
780
|
+
spanId: `${key}#${this.spanSeq++}`,
|
|
496
781
|
name: spanName,
|
|
497
782
|
kind,
|
|
498
783
|
startTime: Date.now(),
|
|
@@ -500,17 +785,33 @@ var TelemetryAdapter = class {
|
|
|
500
785
|
attributes
|
|
501
786
|
};
|
|
502
787
|
this.open.set(key, span);
|
|
788
|
+
this.remember(span.spanId, parentSpanId, EVENT_TYPE_BY_KIND[kind]);
|
|
789
|
+
if (CONTAINER_SPANS.has(spanName)) {
|
|
790
|
+
const stack = this.containers.get(traceId);
|
|
791
|
+
if (stack) stack.push(span.spanId);
|
|
792
|
+
else this.containers.set(traceId, [span.spanId]);
|
|
793
|
+
}
|
|
503
794
|
return span;
|
|
504
795
|
}
|
|
505
796
|
closeSpan(key, status, attributes) {
|
|
506
797
|
const span = this.open.get(key);
|
|
507
|
-
if (!span) return;
|
|
798
|
+
if (!span) return void 0;
|
|
799
|
+
const stack = this.containers.get(span.traceId);
|
|
800
|
+
if (stack) {
|
|
801
|
+
const at = stack.lastIndexOf(span.spanId);
|
|
802
|
+
if (at !== -1) stack.splice(at, 1);
|
|
803
|
+
if (stack.length === 0) {
|
|
804
|
+
this.containers.delete(span.traceId);
|
|
805
|
+
this.appParent.delete(span.traceId);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
508
808
|
span.endTime = Date.now();
|
|
509
809
|
span.durationMs = span.endTime - span.startTime;
|
|
510
810
|
span.status = status;
|
|
511
811
|
Object.assign(span.attributes, clean(attributes));
|
|
512
812
|
this.open.delete(key);
|
|
513
|
-
this.
|
|
813
|
+
this.recordSpan(span);
|
|
814
|
+
return span;
|
|
514
815
|
}
|
|
515
816
|
recordLatency(ms) {
|
|
516
817
|
if (typeof ms !== "number") return;
|
|
@@ -560,14 +861,27 @@ var TelemetryAdapter = class {
|
|
|
560
861
|
{
|
|
561
862
|
scope: { name: "combycode.telemetry" },
|
|
562
863
|
spans: this.spans.map((s) => ({
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
864
|
+
// An app-supplied trace id is ALREADY a real 32-hex id — hashing it
|
|
865
|
+
// would produce a different trace and defeat the whole point of
|
|
866
|
+
// accepting a parent.
|
|
867
|
+
traceId: isHex(s.traceId, 32) ? s.traceId : toOtlpId(s.traceId, 16),
|
|
868
|
+
// Scoped by trace: two conversations can each hold a span keyed
|
|
869
|
+
// `llm:…`, and colliding their ids would merge unrelated traces.
|
|
870
|
+
spanId: toOtlpId(`${s.traceId}|${s.spanId}`, 8),
|
|
871
|
+
// The app's own span id arrives as hex and passes through; one of ours
|
|
872
|
+
// is hashed exactly as it was when we emitted it, so the link matches.
|
|
873
|
+
...s.parentSpanId ? {
|
|
874
|
+
parentSpanId: isHex(s.parentSpanId, 16) ? s.parentSpanId : toOtlpId(`${s.traceId}|${s.parentSpanId}`, 8)
|
|
875
|
+
} : {},
|
|
876
|
+
name: otlpSpanName(s),
|
|
566
877
|
startTimeUnixNano: Math.round(s.startTime * 1e6),
|
|
567
878
|
endTimeUnixNano: Math.round((s.endTime ?? s.startTime) * 1e6),
|
|
568
|
-
kind: s.kind,
|
|
879
|
+
kind: OTLP_KIND_BY_SPAN[s.kind] ?? OTLP_SPAN_KIND.internal,
|
|
569
880
|
status: { code: s.status === "error" ? 2 : s.status === "ok" ? 1 : 0 },
|
|
570
|
-
attributes: Object.entries(s.attributes).map(([key, value]) => ({
|
|
881
|
+
attributes: Object.entries(s.attributes).map(([key, value]) => ({
|
|
882
|
+
key,
|
|
883
|
+
value: toOtlpValue(value)
|
|
884
|
+
}))
|
|
571
885
|
}))
|
|
572
886
|
}
|
|
573
887
|
]
|
|
@@ -2776,12 +3090,15 @@ var NetworkEngine = class {
|
|
|
2776
3090
|
hooks;
|
|
2777
3091
|
fetchFn;
|
|
2778
3092
|
connectFn;
|
|
3093
|
+
/** Engine-wide retry policy, inherited by every queue created from here. */
|
|
3094
|
+
defaultRetry;
|
|
2779
3095
|
settings = /* @__PURE__ */ new Map();
|
|
2780
3096
|
queues = /* @__PURE__ */ new Map();
|
|
2781
3097
|
constructor(config) {
|
|
2782
3098
|
this.hooks = config?.hooks ?? new HookBus();
|
|
2783
3099
|
this.fetchFn = config?.fetch ?? globalThis.fetch.bind(globalThis);
|
|
2784
3100
|
this.connectFn = config?.connect ?? defaultConnectFn;
|
|
3101
|
+
this.defaultRetry = config?.retry;
|
|
2785
3102
|
if (config?.queues) {
|
|
2786
3103
|
for (const [name, settings] of Object.entries(config.queues)) {
|
|
2787
3104
|
this.settings.set(name, settings);
|
|
@@ -2863,12 +3180,18 @@ var NetworkEngine = class {
|
|
|
2863
3180
|
...FALLBACK_LIMITS,
|
|
2864
3181
|
...settings.limits
|
|
2865
3182
|
};
|
|
3183
|
+
const retry = this.defaultRetry || settings.retry ? {
|
|
3184
|
+
...this.defaultRetry,
|
|
3185
|
+
...settings.retry,
|
|
3186
|
+
...this.defaultRetry?.backoff || settings.retry?.backoff ? { backoff: { ...this.defaultRetry?.backoff, ...settings.retry?.backoff } } : {},
|
|
3187
|
+
...this.defaultRetry?.perKind || settings.retry?.perKind ? { perKind: { ...this.defaultRetry?.perKind, ...settings.retry?.perKind } } : {}
|
|
3188
|
+
} : void 0;
|
|
2866
3189
|
const config = {
|
|
2867
3190
|
queueName,
|
|
2868
3191
|
fetch: this.fetchFn,
|
|
2869
3192
|
hooks: this.hooks,
|
|
2870
3193
|
limits,
|
|
2871
|
-
retry
|
|
3194
|
+
retry,
|
|
2872
3195
|
queue: settings.queue
|
|
2873
3196
|
};
|
|
2874
3197
|
queue = new QueueState(config);
|
|
@@ -2945,6 +3268,62 @@ function ensureAdditionalProperties(schema) {
|
|
|
2945
3268
|
}
|
|
2946
3269
|
return result;
|
|
2947
3270
|
}
|
|
3271
|
+
var ANTHROPIC_UNSUPPORTED = /* @__PURE__ */ new Set([
|
|
3272
|
+
"minimum",
|
|
3273
|
+
"maximum",
|
|
3274
|
+
"exclusiveMinimum",
|
|
3275
|
+
"exclusiveMaximum",
|
|
3276
|
+
"multipleOf",
|
|
3277
|
+
"maxItems"
|
|
3278
|
+
]);
|
|
3279
|
+
function strictSupport(schema, dialect) {
|
|
3280
|
+
const visit = (node, path) => {
|
|
3281
|
+
if (!node || typeof node !== "object" || Array.isArray(node)) return null;
|
|
3282
|
+
const n = node;
|
|
3283
|
+
const at = path || "(root)";
|
|
3284
|
+
if (typeof n.$ref === "string") return `${at}: '$ref' cannot be verified without resolution`;
|
|
3285
|
+
if (dialect === "anthropic") {
|
|
3286
|
+
for (const key of Object.keys(n)) {
|
|
3287
|
+
if (ANTHROPIC_UNSUPPORTED.has(key)) return `${at}: '${key}' is not supported under strict`;
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
const props = n.properties;
|
|
3291
|
+
if (n.additionalProperties !== void 0 && n.additionalProperties !== false) {
|
|
3292
|
+
return `${at}: 'additionalProperties' must be false under strict`;
|
|
3293
|
+
}
|
|
3294
|
+
if (dialect === "openai") {
|
|
3295
|
+
if (n.type === "object" && props === void 0) {
|
|
3296
|
+
return `${at}: an object schema with no 'properties' cannot be strict (a free-form object is not expressible)`;
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
if (props && typeof props === "object") {
|
|
3300
|
+
if (dialect === "openai") {
|
|
3301
|
+
const required = new Set(Array.isArray(n.required) ? n.required : []);
|
|
3302
|
+
const missing = Object.keys(props).filter((k) => !required.has(k));
|
|
3303
|
+
if (missing.length > 0) return `${at}: ${missing.join(", ")} not listed in 'required'`;
|
|
3304
|
+
}
|
|
3305
|
+
for (const [key, val] of Object.entries(props)) {
|
|
3306
|
+
const r = visit(val, path ? `${path}.${key}` : key);
|
|
3307
|
+
if (r) return r;
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
for (const key of ["items", "additionalProperties"]) {
|
|
3311
|
+
const r = visit(n[key], path ? `${path}.${key}` : key);
|
|
3312
|
+
if (r) return r;
|
|
3313
|
+
}
|
|
3314
|
+
for (const key of ["anyOf", "oneOf", "allOf"]) {
|
|
3315
|
+
const branches = n[key];
|
|
3316
|
+
if (!Array.isArray(branches)) continue;
|
|
3317
|
+
for (const [i, sub] of branches.entries()) {
|
|
3318
|
+
const r = visit(sub, `${at}.${key}[${i}]`);
|
|
3319
|
+
if (r) return r;
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
return null;
|
|
3323
|
+
};
|
|
3324
|
+
const reason = visit(schema, "");
|
|
3325
|
+
return reason ? { ok: false, reason } : { ok: true };
|
|
3326
|
+
}
|
|
2948
3327
|
|
|
2949
3328
|
// src/llm/providers/anthropic/catalog.json
|
|
2950
3329
|
var catalog_default = {
|
|
@@ -25155,7 +25534,7 @@ function extractSystem(messages) {
|
|
|
25155
25534
|
const rest = [];
|
|
25156
25535
|
for (const m of messages) {
|
|
25157
25536
|
if (m.role === "system") {
|
|
25158
|
-
const text = typeof m.content === "string" ? m.content :
|
|
25537
|
+
const text = typeof m.content === "string" ? m.content : contentToText2(m.content);
|
|
25159
25538
|
if (text) systemTexts.push(text);
|
|
25160
25539
|
} else {
|
|
25161
25540
|
rest.push(m);
|
|
@@ -25166,7 +25545,7 @@ function extractSystem(messages) {
|
|
|
25166
25545
|
messages: rest
|
|
25167
25546
|
};
|
|
25168
25547
|
}
|
|
25169
|
-
function
|
|
25548
|
+
function contentToText2(content) {
|
|
25170
25549
|
return content.filter((p) => p.type === "text").map((p) => p.text).join("\n");
|
|
25171
25550
|
}
|
|
25172
25551
|
function parseStructured(text) {
|
|
@@ -25448,7 +25827,15 @@ var LLMClient = class {
|
|
|
25448
25827
|
signal: options.signal,
|
|
25449
25828
|
provider: this.provider,
|
|
25450
25829
|
model: this.model,
|
|
25451
|
-
|
|
25830
|
+
// Every trace field, not a hand-picked three: `traceparent` rides with the ids,
|
|
25831
|
+
// and picking fields here is what left the HTTP spans rooting a trace of their
|
|
25832
|
+
// own while the LLM span they belong to had joined the caller's.
|
|
25833
|
+
trace: {
|
|
25834
|
+
sessionId: ctx.sessionId,
|
|
25835
|
+
requestId: ctx.requestId,
|
|
25836
|
+
callId: ctx.callId,
|
|
25837
|
+
traceparent: ctx.traceparent
|
|
25838
|
+
}
|
|
25452
25839
|
};
|
|
25453
25840
|
response = await this.fetchFn(httpReq, {
|
|
25454
25841
|
queueName: this.queueName,
|
|
@@ -25576,7 +25963,15 @@ var LLMClient = class {
|
|
|
25576
25963
|
stream: true,
|
|
25577
25964
|
provider: this.provider,
|
|
25578
25965
|
model: this.model,
|
|
25579
|
-
|
|
25966
|
+
// Every trace field, not a hand-picked three: `traceparent` rides with the ids,
|
|
25967
|
+
// and picking fields here is what left the HTTP spans rooting a trace of their
|
|
25968
|
+
// own while the LLM span they belong to had joined the caller's.
|
|
25969
|
+
trace: {
|
|
25970
|
+
sessionId: ctx.sessionId,
|
|
25971
|
+
requestId: ctx.requestId,
|
|
25972
|
+
callId: ctx.callId,
|
|
25973
|
+
traceparent: ctx.traceparent
|
|
25974
|
+
}
|
|
25580
25975
|
};
|
|
25581
25976
|
const start = performance.now();
|
|
25582
25977
|
let text = "";
|
|
@@ -26058,12 +26453,16 @@ var AnthropicAdapter = class {
|
|
|
26058
26453
|
}
|
|
26059
26454
|
return null;
|
|
26060
26455
|
}
|
|
26456
|
+
const strict = t.strict === true;
|
|
26457
|
+
const params = ensureAdditionalProperties(t.parameters);
|
|
26061
26458
|
const tool = {
|
|
26062
26459
|
name: t.name,
|
|
26063
26460
|
description: t.description,
|
|
26064
|
-
|
|
26461
|
+
// Only the strict path was measured with `additionalProperties: false`
|
|
26462
|
+
// applied; without strict the schema goes out untouched, as before.
|
|
26463
|
+
input_schema: strict ? params : t.parameters
|
|
26065
26464
|
};
|
|
26066
|
-
if (
|
|
26465
|
+
if (strict) tool.strict = true;
|
|
26067
26466
|
if ((t.cache || shouldCacheTools) && i === req.tools.length - 1) {
|
|
26068
26467
|
tool.cache_control = { type: "ephemeral" };
|
|
26069
26468
|
}
|
|
@@ -28112,15 +28511,19 @@ var OpenAIAdapter = class {
|
|
|
28112
28511
|
};
|
|
28113
28512
|
}
|
|
28114
28513
|
if (req.tools?.length) {
|
|
28115
|
-
body.tools = req.tools.filter(isFunctionTool).map((t) =>
|
|
28116
|
-
|
|
28117
|
-
|
|
28118
|
-
|
|
28119
|
-
|
|
28120
|
-
|
|
28121
|
-
|
|
28122
|
-
|
|
28123
|
-
|
|
28514
|
+
body.tools = req.tools.filter(isFunctionTool).map((t) => {
|
|
28515
|
+
const params = ensureAdditionalProperties(t.parameters);
|
|
28516
|
+
const strict = t.strict === true;
|
|
28517
|
+
return {
|
|
28518
|
+
type: "function",
|
|
28519
|
+
function: {
|
|
28520
|
+
name: t.name,
|
|
28521
|
+
description: t.description,
|
|
28522
|
+
parameters: strict ? params : t.parameters,
|
|
28523
|
+
...strict ? { strict: true } : {}
|
|
28524
|
+
}
|
|
28525
|
+
};
|
|
28526
|
+
});
|
|
28124
28527
|
}
|
|
28125
28528
|
if (req.toolChoice) {
|
|
28126
28529
|
if (typeof req.toolChoice === "string") body.tool_choice = req.toolChoice;
|
|
@@ -28130,12 +28533,14 @@ var OpenAIAdapter = class {
|
|
|
28130
28533
|
body.reasoning = { effort: req.thinking.effort ?? "medium" };
|
|
28131
28534
|
}
|
|
28132
28535
|
if (req.structured) {
|
|
28536
|
+
const schema = ensureAdditionalProperties(req.structured.schema);
|
|
28537
|
+
const strict = req.structured.strict ?? strictSupport(schema, "openai").ok;
|
|
28133
28538
|
body.response_format = {
|
|
28134
28539
|
type: "json_schema",
|
|
28135
28540
|
json_schema: {
|
|
28136
28541
|
name: req.structured.name ?? "response",
|
|
28137
|
-
schema: req.structured.schema,
|
|
28138
|
-
strict
|
|
28542
|
+
schema: strict ? schema : req.structured.schema,
|
|
28543
|
+
strict
|
|
28139
28544
|
}
|
|
28140
28545
|
};
|
|
28141
28546
|
}
|
|
@@ -29035,12 +29440,13 @@ var OpenAIResponsesAdapter = class {
|
|
|
29035
29440
|
if (req.tools?.length) {
|
|
29036
29441
|
body.tools = req.tools.map((t) => {
|
|
29037
29442
|
if (isFunctionTool(t)) {
|
|
29443
|
+
const params = ensureAdditionalProperties(t.parameters);
|
|
29038
29444
|
return {
|
|
29039
29445
|
type: "function",
|
|
29040
29446
|
name: t.name,
|
|
29041
29447
|
description: t.description,
|
|
29042
|
-
parameters:
|
|
29043
|
-
strict: t.strict ??
|
|
29448
|
+
parameters: params,
|
|
29449
|
+
strict: t.strict ?? strictSupport(params, "openai").ok,
|
|
29044
29450
|
// Programmatic tool calling (Responses): who may call it + return schema.
|
|
29045
29451
|
...t.allowedCallers ? { allowed_callers: t.allowedCallers } : {},
|
|
29046
29452
|
...t.outputSchema ? { output_schema: t.outputSchema } : {}
|
|
@@ -29061,12 +29467,13 @@ var OpenAIResponsesAdapter = class {
|
|
|
29061
29467
|
}
|
|
29062
29468
|
}
|
|
29063
29469
|
if (req.structured) {
|
|
29470
|
+
const schema = ensureAdditionalProperties(req.structured.schema);
|
|
29064
29471
|
body.text = {
|
|
29065
29472
|
format: {
|
|
29066
29473
|
type: "json_schema",
|
|
29067
29474
|
name: req.structured.name ?? "response",
|
|
29068
|
-
schema
|
|
29069
|
-
strict: req.structured.strict ??
|
|
29475
|
+
schema,
|
|
29476
|
+
strict: req.structured.strict ?? strictSupport(schema, "openai").ok
|
|
29070
29477
|
}
|
|
29071
29478
|
};
|
|
29072
29479
|
}
|
|
@@ -30660,7 +31067,9 @@ var LAYER_MEMORY = "memory";
|
|
|
30660
31067
|
var LAYER_CHAT_FACTS = "chat.facts";
|
|
30661
31068
|
var LAYER_EXECUTOR_TOOL_EXAMPLES = "executor.tool-examples";
|
|
30662
31069
|
var LAYER_CONTEXT_GUARD_SUMMARY = "context-guard.summary";
|
|
31070
|
+
var LAYER_LAZY_TOOLS = "agentloop.lazy-tools";
|
|
30663
31071
|
var PRIORITY_AGENTLOOP_SYSTEM = 10;
|
|
31072
|
+
var PRIORITY_LAZY_TOOLS = 20;
|
|
30664
31073
|
var PRIORITY_LEGACY_SYSTEM = 50;
|
|
30665
31074
|
var PRIORITY_AGENTLOOP_CONTEXT = 100;
|
|
30666
31075
|
var PRIORITY_MEMORY = 200;
|
|
@@ -30678,6 +31087,17 @@ function writeAgentLoopSystem(registry, text, owner) {
|
|
|
30678
31087
|
owner
|
|
30679
31088
|
});
|
|
30680
31089
|
}
|
|
31090
|
+
function writeLazyToolsProtocol(registry, active, owner) {
|
|
31091
|
+
if (!active) {
|
|
31092
|
+
registry.remove(LAYER_LAZY_TOOLS);
|
|
31093
|
+
return;
|
|
31094
|
+
}
|
|
31095
|
+
registry.set(
|
|
31096
|
+
LAYER_LAZY_TOOLS,
|
|
31097
|
+
"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.",
|
|
31098
|
+
{ priority: PRIORITY_LAZY_TOOLS, tags: ["system"], owner }
|
|
31099
|
+
);
|
|
31100
|
+
}
|
|
30681
31101
|
function writeAgentLoopContext(registry, text, owner) {
|
|
30682
31102
|
if (!text) {
|
|
30683
31103
|
registry.remove(LAYER_AGENTLOOP_CONTEXT);
|
|
@@ -31029,6 +31449,175 @@ ${text}` : text;
|
|
|
31029
31449
|
}
|
|
31030
31450
|
};
|
|
31031
31451
|
|
|
31452
|
+
// src/agent/lazy-tools.ts
|
|
31453
|
+
var DEFAULT_LIMIT = 5;
|
|
31454
|
+
var MAX_LIMIT = 20;
|
|
31455
|
+
var DEFAULT_MAX_SEARCHES = 5;
|
|
31456
|
+
var LAZY_SEARCH_TOOL = "tool_search";
|
|
31457
|
+
var LAZY_CALL_TOOL = "call_tool";
|
|
31458
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
31459
|
+
"the",
|
|
31460
|
+
"a",
|
|
31461
|
+
"an",
|
|
31462
|
+
"of",
|
|
31463
|
+
"for",
|
|
31464
|
+
"to",
|
|
31465
|
+
"in",
|
|
31466
|
+
"on",
|
|
31467
|
+
"and",
|
|
31468
|
+
"or",
|
|
31469
|
+
"is",
|
|
31470
|
+
"it",
|
|
31471
|
+
"that",
|
|
31472
|
+
"this",
|
|
31473
|
+
"with",
|
|
31474
|
+
"return",
|
|
31475
|
+
"returns",
|
|
31476
|
+
"my",
|
|
31477
|
+
"me",
|
|
31478
|
+
"do",
|
|
31479
|
+
"we",
|
|
31480
|
+
"i",
|
|
31481
|
+
"how",
|
|
31482
|
+
"many",
|
|
31483
|
+
"much",
|
|
31484
|
+
"what",
|
|
31485
|
+
"when",
|
|
31486
|
+
"has",
|
|
31487
|
+
"have",
|
|
31488
|
+
"need",
|
|
31489
|
+
"any",
|
|
31490
|
+
"get",
|
|
31491
|
+
"can",
|
|
31492
|
+
"you",
|
|
31493
|
+
"are",
|
|
31494
|
+
"was",
|
|
31495
|
+
"been",
|
|
31496
|
+
"does",
|
|
31497
|
+
"did",
|
|
31498
|
+
"should",
|
|
31499
|
+
"from",
|
|
31500
|
+
"by",
|
|
31501
|
+
"at",
|
|
31502
|
+
"as",
|
|
31503
|
+
"be"
|
|
31504
|
+
]);
|
|
31505
|
+
function tokenize(s) {
|
|
31506
|
+
return s.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w));
|
|
31507
|
+
}
|
|
31508
|
+
var isFn = (t) => "name" in t;
|
|
31509
|
+
var nameOf = (t) => isFn(t.definition) ? t.definition.name : "";
|
|
31510
|
+
function rankTools(query, candidates, limit) {
|
|
31511
|
+
const q = new Set(tokenize(query));
|
|
31512
|
+
if (q.size === 0) return [];
|
|
31513
|
+
const scored = [];
|
|
31514
|
+
for (const tool of candidates) {
|
|
31515
|
+
const def = tool.definition;
|
|
31516
|
+
if (!isFn(def)) continue;
|
|
31517
|
+
const props = Object.keys(
|
|
31518
|
+
def.parameters?.properties ?? {}
|
|
31519
|
+
);
|
|
31520
|
+
let score = 0;
|
|
31521
|
+
for (const w of tokenize(`${def.name} ${def.description ?? ""} ${props.join(" ")}`)) {
|
|
31522
|
+
if (q.has(w)) score++;
|
|
31523
|
+
}
|
|
31524
|
+
for (const w of tokenize(def.name)) if (q.has(w)) score += 2;
|
|
31525
|
+
if (score > 0) scored.push({ tool, score });
|
|
31526
|
+
}
|
|
31527
|
+
return scored.sort((a, b) => b.score - a.score).slice(0, limit).map((s) => s.tool);
|
|
31528
|
+
}
|
|
31529
|
+
function createLazyTools(deps) {
|
|
31530
|
+
const limit = Math.min(deps.config.limit ?? DEFAULT_LIMIT, MAX_LIMIT);
|
|
31531
|
+
const maxSearches = deps.config.maxSearches ?? DEFAULT_MAX_SEARCHES;
|
|
31532
|
+
const search = {
|
|
31533
|
+
definition: {
|
|
31534
|
+
type: "function",
|
|
31535
|
+
name: LAZY_SEARCH_TOOL,
|
|
31536
|
+
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.",
|
|
31537
|
+
parameters: {
|
|
31538
|
+
type: "object",
|
|
31539
|
+
properties: {
|
|
31540
|
+
queries: {
|
|
31541
|
+
type: "array",
|
|
31542
|
+
items: { type: "string" },
|
|
31543
|
+
description: "One phrase per capability you need, in your own words."
|
|
31544
|
+
}
|
|
31545
|
+
},
|
|
31546
|
+
required: ["queries"]
|
|
31547
|
+
}
|
|
31548
|
+
},
|
|
31549
|
+
execute: async (args) => {
|
|
31550
|
+
deps.state.searches++;
|
|
31551
|
+
if (deps.state.searches > maxSearches) {
|
|
31552
|
+
return JSON.stringify({
|
|
31553
|
+
error: `Search budget exhausted (${maxSearches} searches per run). Use the tools you already found.`
|
|
31554
|
+
});
|
|
31555
|
+
}
|
|
31556
|
+
const raw = args.queries;
|
|
31557
|
+
const queries = (Array.isArray(raw) ? raw : [raw]).filter((q) => typeof q === "string" && q.trim().length > 0);
|
|
31558
|
+
if (queries.length === 0) {
|
|
31559
|
+
return JSON.stringify({ tools: [], error: "Pass at least one query string in `queries`." });
|
|
31560
|
+
}
|
|
31561
|
+
const candidates = deps.lazyTools();
|
|
31562
|
+
const hits = /* @__PURE__ */ new Map();
|
|
31563
|
+
const unmatched = [];
|
|
31564
|
+
for (const q of queries) {
|
|
31565
|
+
const found = rankTools(q, candidates, limit);
|
|
31566
|
+
if (found.length === 0) unmatched.push(q);
|
|
31567
|
+
for (const t of found) hits.set(nameOf(t), t);
|
|
31568
|
+
}
|
|
31569
|
+
deps.onSearch?.({ queries, matched: [...hits.keys()], unmatched });
|
|
31570
|
+
return JSON.stringify({
|
|
31571
|
+
tools: [...hits.values()].map((t) => t.definition),
|
|
31572
|
+
...unmatched.length > 0 ? {
|
|
31573
|
+
unmatched,
|
|
31574
|
+
hint: "These queries matched no tool. Search again for them using different words, or tell the user the capability is unavailable."
|
|
31575
|
+
} : {}
|
|
31576
|
+
});
|
|
31577
|
+
}
|
|
31578
|
+
};
|
|
31579
|
+
const call = {
|
|
31580
|
+
definition: {
|
|
31581
|
+
type: "function",
|
|
31582
|
+
name: LAZY_CALL_TOOL,
|
|
31583
|
+
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.",
|
|
31584
|
+
parameters: {
|
|
31585
|
+
type: "object",
|
|
31586
|
+
properties: {
|
|
31587
|
+
name: { type: "string", description: "Exact tool name from tool_search." },
|
|
31588
|
+
input: {
|
|
31589
|
+
type: "object",
|
|
31590
|
+
description: "That tool's own arguments, as an object.",
|
|
31591
|
+
additionalProperties: true
|
|
31592
|
+
}
|
|
31593
|
+
},
|
|
31594
|
+
required: ["name", "input"]
|
|
31595
|
+
}
|
|
31596
|
+
},
|
|
31597
|
+
execute: async (args, ctx) => {
|
|
31598
|
+
const name = String(args.name ?? "");
|
|
31599
|
+
const target = deps.lazyTools().find((t) => nameOf(t) === name);
|
|
31600
|
+
if (!target) {
|
|
31601
|
+
if (deps.eagerNames().includes(name)) {
|
|
31602
|
+
return `"${name}" is already available as a normal tool \u2014 call it directly, not through ${LAZY_CALL_TOOL}.`;
|
|
31603
|
+
}
|
|
31604
|
+
return `No tool named "${name}". Call ${LAZY_SEARCH_TOOL} first and use a name exactly as returned.`;
|
|
31605
|
+
}
|
|
31606
|
+
const input = args.input;
|
|
31607
|
+
if (input !== void 0 && (typeof input !== "object" || input === null || Array.isArray(input))) {
|
|
31608
|
+
return `\`input\` must be an object of ${name}'s arguments, not ${Array.isArray(input) ? "an array" : typeof input}.`;
|
|
31609
|
+
}
|
|
31610
|
+
return target.execute(input ?? {}, ctx);
|
|
31611
|
+
}
|
|
31612
|
+
};
|
|
31613
|
+
return [search, call];
|
|
31614
|
+
}
|
|
31615
|
+
function unwrapLazyCall(toolName, args) {
|
|
31616
|
+
if (toolName !== LAZY_CALL_TOOL) return null;
|
|
31617
|
+
const inner = args.name;
|
|
31618
|
+
return typeof inner === "string" && inner.length > 0 ? inner : null;
|
|
31619
|
+
}
|
|
31620
|
+
|
|
31032
31621
|
// src/agent/tool-key.ts
|
|
31033
31622
|
function toolKey(tool) {
|
|
31034
31623
|
return isFunctionTool(tool.definition) ? tool.definition.name : tool.definition.type;
|
|
@@ -31289,12 +31878,25 @@ async function handleToolError(e, tc, hooks, runId, agentId, step, metrics, repo
|
|
|
31289
31878
|
// src/agent/loop.ts
|
|
31290
31879
|
var AgentLoop = class _AgentLoop {
|
|
31291
31880
|
id;
|
|
31881
|
+
/** Human name, surfaced as `gen_ai.agent.name` — see AgentLoopConfig.label. */
|
|
31882
|
+
label;
|
|
31883
|
+
/** Which part of the host system this agent belongs to. */
|
|
31884
|
+
source;
|
|
31885
|
+
/** Extra attributes stamped on this agent's spans. */
|
|
31886
|
+
attributes;
|
|
31292
31887
|
client;
|
|
31293
31888
|
hooks;
|
|
31294
31889
|
_system;
|
|
31295
31890
|
_systemThunk = null;
|
|
31296
31891
|
_context;
|
|
31297
31892
|
_tools;
|
|
31893
|
+
_lazyConfig = {};
|
|
31894
|
+
/** Per-run search budget, reset at the start of every run. */
|
|
31895
|
+
_lazyState = { searches: 0 };
|
|
31896
|
+
/** Installed on the first `lazy` registration and never removed, so the declared tool
|
|
31897
|
+
* array stays byte-identical for the life of the conversation — which is the entire
|
|
31898
|
+
* reason the design is cheap. */
|
|
31899
|
+
_lazyInstalled = false;
|
|
31298
31900
|
_history;
|
|
31299
31901
|
_reports = [];
|
|
31300
31902
|
_metadata = {};
|
|
@@ -31342,6 +31944,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31342
31944
|
this._checkpoint = config.checkpoint ?? null;
|
|
31343
31945
|
this._collisionPolicy = config.toolNameCollisionPolicy ?? "warn";
|
|
31344
31946
|
this._reflectRetry = config.reflectAndRetry ? new ReflectAndRetryPolicy(config.reflectAndRetry) : null;
|
|
31947
|
+
this._lazyConfig = config.lazyTools ?? {};
|
|
31345
31948
|
this._tools = /* @__PURE__ */ new Map();
|
|
31346
31949
|
for (const t of config.tools ?? []) {
|
|
31347
31950
|
this.registerTool(t);
|
|
@@ -31354,8 +31957,12 @@ var AgentLoop = class _AgentLoop {
|
|
|
31354
31957
|
this._history = new ConversationHistory();
|
|
31355
31958
|
}
|
|
31356
31959
|
this.id = this._history.id;
|
|
31960
|
+
this.label = config.label;
|
|
31961
|
+
this.source = config.source;
|
|
31962
|
+
this.attributes = config.attributes;
|
|
31357
31963
|
writeAgentLoopSystem(this._history.registry, this._system, "agent-loop");
|
|
31358
31964
|
writeAgentLoopContext(this._history.registry, this._context, "agent-loop");
|
|
31965
|
+
this.syncLazyProtocol();
|
|
31359
31966
|
this.hooks.emitSync("onAgentCreate", {
|
|
31360
31967
|
agentId: this.id,
|
|
31361
31968
|
clientId: this.client.id,
|
|
@@ -31442,6 +32049,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31442
32049
|
});
|
|
31443
32050
|
}
|
|
31444
32051
|
this._tools.set(key, tool);
|
|
32052
|
+
if (tool.lazy) this.installLazyTools();
|
|
31445
32053
|
}
|
|
31446
32054
|
removeTool(name) {
|
|
31447
32055
|
this._tools.delete(name);
|
|
@@ -31456,7 +32064,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31456
32064
|
}
|
|
31457
32065
|
// ─── complete (non-streaming) ───────────────────────────────────────────
|
|
31458
32066
|
async complete(input, options = {}) {
|
|
31459
|
-
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input);
|
|
32067
|
+
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input, options.ctx);
|
|
31460
32068
|
const steps = [];
|
|
31461
32069
|
const totalUsage = emptyUsage();
|
|
31462
32070
|
let totalLlmTimeMs = 0;
|
|
@@ -31508,7 +32116,23 @@ var AgentLoop = class _AgentLoop {
|
|
|
31508
32116
|
thinking: options.thinking ?? this._thinking,
|
|
31509
32117
|
cache: options.cache ?? this._cache,
|
|
31510
32118
|
tools: this.toolDefinitions(options),
|
|
31511
|
-
ctx: {
|
|
32119
|
+
ctx: {
|
|
32120
|
+
// The RUN's trace, handed down to every LLM call it makes.
|
|
32121
|
+
//
|
|
32122
|
+
// Without this the agent kept `runTrace` to itself: its own spans used it
|
|
32123
|
+
// while each `client.complete()` fell through to mint-if-absent and
|
|
32124
|
+
// invented a fresh `requestId`. Since the trace id is `sessionId:requestId`,
|
|
32125
|
+
// one conversation arrived at the backend as SEVERAL unrelated traces —
|
|
32126
|
+
// measured against a real collector: a single turn with one tool call
|
|
32127
|
+
// produced six. Correlation is the whole point of a trace id, so this is
|
|
32128
|
+
// the one thing it must not get wrong.
|
|
32129
|
+
...runTrace,
|
|
32130
|
+
conversationId: this._history.id,
|
|
32131
|
+
// A caller's explicit ctx wins over all of the above: an app that already
|
|
32132
|
+
// owns a request id or a conversation id has better information than we do,
|
|
32133
|
+
// and silently overwriting it is how its telemetry stops joining up.
|
|
32134
|
+
...options.ctx
|
|
32135
|
+
},
|
|
31512
32136
|
signal: options.signal ?? this._abortController?.signal
|
|
31513
32137
|
});
|
|
31514
32138
|
const stepLatency = performance.now() - stepStart;
|
|
@@ -31676,7 +32300,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31676
32300
|
}
|
|
31677
32301
|
// ─── stream ─────────────────────────────────────────────────────────────
|
|
31678
32302
|
async *stream(input, options = {}) {
|
|
31679
|
-
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input);
|
|
32303
|
+
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input, options.ctx);
|
|
31680
32304
|
const steps = [];
|
|
31681
32305
|
const totalUsage = emptyUsage();
|
|
31682
32306
|
let totalLlmTimeMs = 0;
|
|
@@ -31732,7 +32356,23 @@ var AgentLoop = class _AgentLoop {
|
|
|
31732
32356
|
thinking: options.thinking ?? this._thinking,
|
|
31733
32357
|
cache: options.cache ?? this._cache,
|
|
31734
32358
|
tools: this.toolDefinitions(options),
|
|
31735
|
-
ctx: {
|
|
32359
|
+
ctx: {
|
|
32360
|
+
// The RUN's trace, handed down to every LLM call it makes.
|
|
32361
|
+
//
|
|
32362
|
+
// Without this the agent kept `runTrace` to itself: its own spans used it
|
|
32363
|
+
// while each `client.complete()` fell through to mint-if-absent and
|
|
32364
|
+
// invented a fresh `requestId`. Since the trace id is `sessionId:requestId`,
|
|
32365
|
+
// one conversation arrived at the backend as SEVERAL unrelated traces —
|
|
32366
|
+
// measured against a real collector: a single turn with one tool call
|
|
32367
|
+
// produced six. Correlation is the whole point of a trace id, so this is
|
|
32368
|
+
// the one thing it must not get wrong.
|
|
32369
|
+
...runTrace,
|
|
32370
|
+
conversationId: this._history.id,
|
|
32371
|
+
// A caller's explicit ctx wins over all of the above: an app that already
|
|
32372
|
+
// owns a request id or a conversation id has better information than we do,
|
|
32373
|
+
// and silently overwriting it is how its telemetry stops joining up.
|
|
32374
|
+
...options.ctx
|
|
32375
|
+
},
|
|
31736
32376
|
signal: options.signal ?? this._abortController?.signal
|
|
31737
32377
|
})) {
|
|
31738
32378
|
const toYield = accumulateStreamEvent(event, state);
|
|
@@ -31944,7 +32584,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31944
32584
|
arguments: tc.arguments,
|
|
31945
32585
|
callId: tc.id,
|
|
31946
32586
|
step,
|
|
31947
|
-
trace: {
|
|
32587
|
+
trace: { ...runTrace, callId: tc.id }
|
|
31948
32588
|
});
|
|
31949
32589
|
if (!decision.pass) {
|
|
31950
32590
|
return this.buildDeniedResult(tc, decision.reason, reports);
|
|
@@ -31964,7 +32604,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31964
32604
|
}
|
|
31965
32605
|
}
|
|
31966
32606
|
try {
|
|
31967
|
-
const baseCtx = { step, callId: tc.id, metrics, trace: {
|
|
32607
|
+
const baseCtx = { step, callId: tc.id, metrics, trace: { ...runTrace, callId: tc.id } };
|
|
31968
32608
|
const result = await executeWithTimeout(lookup.tool, tc, baseCtx, this._toolTimeout);
|
|
31969
32609
|
return await this.buildSuccessResult(tc, result, runId, step, metrics, reports, toolStart, runTrace, lookup.tool, baseCtx);
|
|
31970
32610
|
} catch (e) {
|
|
@@ -32022,7 +32662,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32022
32662
|
arguments: tc.arguments,
|
|
32023
32663
|
reason,
|
|
32024
32664
|
step,
|
|
32025
|
-
trace: {
|
|
32665
|
+
trace: { ...runTrace, callId: tc.id }
|
|
32026
32666
|
};
|
|
32027
32667
|
const pending = {
|
|
32028
32668
|
callId: tc.id,
|
|
@@ -32055,7 +32695,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32055
32695
|
return this.buildOverriddenResult(tc, decision.overrideResult, reports);
|
|
32056
32696
|
}
|
|
32057
32697
|
try {
|
|
32058
|
-
const baseCtx = { step, callId: tc.id, metrics, trace: {
|
|
32698
|
+
const baseCtx = { step, callId: tc.id, metrics, trace: { ...runTrace, callId: tc.id } };
|
|
32059
32699
|
const result = await executeWithTimeout(tool, tc, baseCtx, this._toolTimeout);
|
|
32060
32700
|
return await this.buildSuccessResult(tc, result, runId, step, metrics, reports, toolStart, runTrace, tool, baseCtx);
|
|
32061
32701
|
} catch (e) {
|
|
@@ -32092,31 +32732,71 @@ var AgentLoop = class _AgentLoop {
|
|
|
32092
32732
|
metrics,
|
|
32093
32733
|
trace: runTrace
|
|
32094
32734
|
});
|
|
32735
|
+
const inner = unwrapLazyCall(tc.name, tc.arguments);
|
|
32095
32736
|
reports.push({
|
|
32096
32737
|
callId: tc.id,
|
|
32097
|
-
toolName: tc.name,
|
|
32738
|
+
toolName: inner ?? tc.name,
|
|
32098
32739
|
arguments: tc.arguments,
|
|
32099
32740
|
resultSizeBytes: resultStr.length,
|
|
32100
32741
|
latencyMs,
|
|
32101
32742
|
skipped: false,
|
|
32102
32743
|
error: null,
|
|
32103
32744
|
metrics: Object.fromEntries(metrics),
|
|
32745
|
+
...inner ? { discoveredVia: "search" } : {},
|
|
32104
32746
|
...customData !== void 0 ? { customData } : {}
|
|
32105
32747
|
});
|
|
32106
32748
|
return { type: "tool_result", id: tc.id, content: resultStr };
|
|
32107
32749
|
}
|
|
32108
|
-
/**
|
|
32750
|
+
/** Declare `tool_search` + `call_tool`, once, on the first lazy registration.
|
|
32751
|
+
*
|
|
32752
|
+
* They go through `registerTool` like anything else, so the collision policy covers
|
|
32753
|
+
* them and there is no second registry to keep in sync. They are never removed: the
|
|
32754
|
+
* declared array must stay identical for the whole conversation or the cached prefix
|
|
32755
|
+
* is invalidated, which is the cost the feature exists to avoid. */
|
|
32756
|
+
installLazyTools() {
|
|
32757
|
+
if (this._lazyInstalled) return;
|
|
32758
|
+
this._lazyInstalled = true;
|
|
32759
|
+
for (const t of createLazyTools({
|
|
32760
|
+
lazyTools: () => [...this._tools.values()].filter((t2) => t2.lazy),
|
|
32761
|
+
eagerNames: () => [...this._tools.entries()].filter(([, t2]) => !t2.lazy).map(([key]) => key),
|
|
32762
|
+
state: this._lazyState,
|
|
32763
|
+
config: this._lazyConfig,
|
|
32764
|
+
onSearch: (info) => {
|
|
32765
|
+
void this.hooks.emit("onToolSearch", { agentId: this.id, ...info });
|
|
32766
|
+
}
|
|
32767
|
+
})) {
|
|
32768
|
+
this.registerTool(t);
|
|
32769
|
+
}
|
|
32770
|
+
this.syncLazyProtocol();
|
|
32771
|
+
}
|
|
32772
|
+
/** Publish (or remove) the "your tools are not all listed" layer.
|
|
32773
|
+
*
|
|
32774
|
+
* Separate from `installLazyTools` because tools are registered in the constructor
|
|
32775
|
+
* BEFORE `_history` exists, and the layer lives in the history's registry. The
|
|
32776
|
+
* constructor calls this again once history is built.
|
|
32777
|
+
*
|
|
32778
|
+
* The model has no reason to suspect a tool it cannot see, and the failure without
|
|
32779
|
+
* this is quiet — it answers from whatever it did find. Measured at 8/12 and 9/12
|
|
32780
|
+
* without the protocol, 18/18 with it, same tasks and same ranker. */
|
|
32781
|
+
syncLazyProtocol() {
|
|
32782
|
+
if (!this._history) return;
|
|
32783
|
+
writeLazyToolsProtocol(this._history.registry, this._lazyInstalled, "agent-loop");
|
|
32784
|
+
}
|
|
32785
|
+
/** Merge agent's tool definitions with caller-provided tools (caller wins on conflict).
|
|
32786
|
+
*
|
|
32787
|
+
* Lazy tools are registered but NOT declared — that filter is the whole mechanism. */
|
|
32109
32788
|
toolDefinitions(options) {
|
|
32110
|
-
const own = [...this._tools.values()].map((t) => t.definition);
|
|
32789
|
+
const own = [...this._tools.values()].filter((t) => !t.lazy).map((t) => t.definition);
|
|
32111
32790
|
if (options.tools) return [...own, ...options.tools];
|
|
32112
32791
|
return own.length > 0 ? own : void 0;
|
|
32113
32792
|
}
|
|
32114
32793
|
// ─── Run helpers ────────────────────────────────────────────────────────
|
|
32115
|
-
async beginRun(input) {
|
|
32794
|
+
async beginRun(input, callerCtx) {
|
|
32116
32795
|
if (this._running) throw new Error("AgentLoop is already running");
|
|
32117
32796
|
this._running = true;
|
|
32118
32797
|
this._stopRequested = false;
|
|
32119
32798
|
this._abortController = new AbortController();
|
|
32799
|
+
this._lazyState.searches = 0;
|
|
32120
32800
|
if (this._systemThunk) {
|
|
32121
32801
|
const next = await this._systemThunk();
|
|
32122
32802
|
if (next !== this._system) {
|
|
@@ -32128,10 +32808,23 @@ var AgentLoop = class _AgentLoop {
|
|
|
32128
32808
|
const startedAt = Date.now();
|
|
32129
32809
|
const startPerf = performance.now();
|
|
32130
32810
|
const userMessageText = typeof input === "string" ? input : Array.isArray(input) && input.length > 0 && "role" in input[0] ? contentText(input[input.length - 1].content) : contentText(input);
|
|
32131
|
-
const runTrace = {
|
|
32811
|
+
const runTrace = {
|
|
32812
|
+
sessionId: callerCtx?.sessionId ?? this.id,
|
|
32813
|
+
requestId: callerCtx?.requestId ?? runId,
|
|
32814
|
+
// The caller's span travels WITH the ids, for the same reason they do: `agent.run`,
|
|
32815
|
+
// every `tool.call`, and any agent nested inside a tool reach the telemetry through
|
|
32816
|
+
// `runTrace` and nothing else. While this field was missing from it only the LLM
|
|
32817
|
+
// calls joined the app's trace — they are built from the caller's ctx directly —
|
|
32818
|
+
// and the run that made them sat in a second, unrelated one. Measured against a
|
|
32819
|
+
// live backend; the unit tests fed the hooks directly and never saw it.
|
|
32820
|
+
...callerCtx?.traceparent ? { traceparent: callerCtx.traceparent } : {}
|
|
32821
|
+
};
|
|
32132
32822
|
await this.hooks.emit("onRunStart", {
|
|
32133
32823
|
runId,
|
|
32134
32824
|
agentId: this.id,
|
|
32825
|
+
label: this.label,
|
|
32826
|
+
source: this.source,
|
|
32827
|
+
attributes: this.attributes,
|
|
32135
32828
|
userMessage: input,
|
|
32136
32829
|
model: this.client.model,
|
|
32137
32830
|
system: this._history.system,
|
|
@@ -32186,7 +32879,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32186
32879
|
if (g.kind !== "input") continue;
|
|
32187
32880
|
const decision = await g.check({
|
|
32188
32881
|
kind: "input",
|
|
32189
|
-
trace: {
|
|
32882
|
+
trace: { ...runTrace },
|
|
32190
32883
|
step,
|
|
32191
32884
|
messages,
|
|
32192
32885
|
system
|
|
@@ -32214,7 +32907,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32214
32907
|
if (g.kind !== "output") continue;
|
|
32215
32908
|
const decision = await g.check({
|
|
32216
32909
|
kind: "output",
|
|
32217
|
-
trace: {
|
|
32910
|
+
trace: { ...runTrace },
|
|
32218
32911
|
step,
|
|
32219
32912
|
response
|
|
32220
32913
|
});
|
|
@@ -33090,9 +33783,16 @@ function summarize(entries) {
|
|
|
33090
33783
|
reasoning: 0,
|
|
33091
33784
|
total: 0,
|
|
33092
33785
|
tokens: { input: 0, output: 0, cached: 0, cacheWrite: 0, reasoning: 0 },
|
|
33093
|
-
entries: entries.length
|
|
33786
|
+
entries: entries.length,
|
|
33787
|
+
unpriced: 0,
|
|
33788
|
+
unpricedModels: []
|
|
33094
33789
|
};
|
|
33095
33790
|
for (const e of entries) {
|
|
33791
|
+
if (e.cost.source === "unknown") {
|
|
33792
|
+
s.unpriced++;
|
|
33793
|
+
const key = `${e.provider}/${e.model}`;
|
|
33794
|
+
if (!s.unpricedModels.includes(key)) s.unpricedModels.push(key);
|
|
33795
|
+
}
|
|
33096
33796
|
s.input += e.cost.input;
|
|
33097
33797
|
s.output += e.cost.output;
|
|
33098
33798
|
s.cacheRead += e.cost.cacheRead;
|
|
@@ -33118,6 +33818,8 @@ var CostCollector = class {
|
|
|
33118
33818
|
budgets = [];
|
|
33119
33819
|
triggeredThresholds = /* @__PURE__ */ new Map();
|
|
33120
33820
|
_runningTotal = 0;
|
|
33821
|
+
/** Models already reported as unpriced, so the warning fires once rather than per call. */
|
|
33822
|
+
warnedUnpriced = /* @__PURE__ */ new Set();
|
|
33121
33823
|
watchedAgents = /* @__PURE__ */ new Set();
|
|
33122
33824
|
unsub = null;
|
|
33123
33825
|
unsubMedia = null;
|
|
@@ -33250,6 +33952,7 @@ var CostCollector = class {
|
|
|
33250
33952
|
};
|
|
33251
33953
|
this.ledger.push(entry);
|
|
33252
33954
|
this._runningTotal += cost.total;
|
|
33955
|
+
this.noteIfUnpriced(entry);
|
|
33253
33956
|
this.hooks.emitSync("onCostEntry", { entry, runningTotal: this._runningTotal });
|
|
33254
33957
|
this.checkBudgets(entry);
|
|
33255
33958
|
}
|
|
@@ -33295,9 +33998,31 @@ var CostCollector = class {
|
|
|
33295
33998
|
};
|
|
33296
33999
|
this.ledger.push(entry);
|
|
33297
34000
|
this._runningTotal += cost.total;
|
|
34001
|
+
this.noteIfUnpriced(entry);
|
|
33298
34002
|
this.hooks.emitSync("onCostEntry", { entry, runningTotal: this._runningTotal });
|
|
33299
34003
|
this.checkBudgets(entry);
|
|
33300
34004
|
}
|
|
34005
|
+
/** A total of exactly 0 because the model is not in the catalog reads identically to a
|
|
34006
|
+
* total of 0 because the call was free — and it silently under-counts every budget
|
|
34007
|
+
* and report built on it. `source: 'unknown'` already records the difference per
|
|
34008
|
+
* entry, but nothing aggregated it, so a whole benchmark run once reported $0.00000
|
|
34009
|
+
* for a live provider and looked like a free arm.
|
|
34010
|
+
*
|
|
34011
|
+
* Fires once per provider/model: an unpriced model is a configuration fact, not a
|
|
34012
|
+
* per-request event, and repeating it on every call would train the reader to ignore
|
|
34013
|
+
* it. Free calls are priced 'calculated' with an explicit note, so they stay silent. */
|
|
34014
|
+
noteIfUnpriced(entry) {
|
|
34015
|
+
if (entry.cost.source !== "unknown") return;
|
|
34016
|
+
const key = `${entry.provider}/${entry.model}`;
|
|
34017
|
+
if (this.warnedUnpriced.has(key)) return;
|
|
34018
|
+
this.warnedUnpriced.add(key);
|
|
34019
|
+
this.hooks.emitSync("onWarning", {
|
|
34020
|
+
source: "cost",
|
|
34021
|
+
code: "unpriced_model",
|
|
34022
|
+
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.`,
|
|
34023
|
+
details: { provider: entry.provider, model: entry.model }
|
|
34024
|
+
});
|
|
34025
|
+
}
|
|
33301
34026
|
checkBudgets(entry) {
|
|
33302
34027
|
for (const budget of this.budgets) {
|
|
33303
34028
|
if (!matchesScope(entry, budget.scope)) continue;
|
|
@@ -34342,11 +35067,12 @@ function createEngine(config = {}) {
|
|
|
34342
35067
|
const persistence = resolvePersistence(config.persistence);
|
|
34343
35068
|
const cache2 = resolveCache(config.cache);
|
|
34344
35069
|
const catalog = resolveCatalog(config.catalog);
|
|
34345
|
-
const network = new NetworkEngine({ hooks, fetch: config.fetch });
|
|
35070
|
+
const network = new NetworkEngine({ hooks, fetch: config.fetch, retry: config.retry, queues: config.queues });
|
|
34346
35071
|
const fetchBound = (req, options) => network.fetch(req, options);
|
|
34347
35072
|
const fetchStreamBound = (req, options) => network.fetchStream(req, options);
|
|
34348
35073
|
const connectBound = (req) => network.connect(req);
|
|
34349
35074
|
const cost = new CostCollector({ hooks, catalog });
|
|
35075
|
+
const telemetry = config.telemetry ? new TelemetryAdapter(hooks, config.telemetry) : null;
|
|
34350
35076
|
const handle = {
|
|
34351
35077
|
sessionId,
|
|
34352
35078
|
hooks,
|
|
@@ -34359,9 +35085,11 @@ function createEngine(config = {}) {
|
|
|
34359
35085
|
connect: connectBound,
|
|
34360
35086
|
catalog,
|
|
34361
35087
|
cost,
|
|
35088
|
+
telemetry,
|
|
34362
35089
|
apiKeys: config.apiKeys ?? {},
|
|
34363
35090
|
destroy() {
|
|
34364
35091
|
cost.destroy();
|
|
35092
|
+
telemetry?.destroy();
|
|
34365
35093
|
network.destroy();
|
|
34366
35094
|
}
|
|
34367
35095
|
};
|
|
@@ -37635,6 +38363,7 @@ function defineTool(input) {
|
|
|
37635
38363
|
if (!optional.has(key)) required.push(key);
|
|
37636
38364
|
}
|
|
37637
38365
|
return {
|
|
38366
|
+
...input.lazy ? { lazy: true } : {},
|
|
37638
38367
|
definition: {
|
|
37639
38368
|
name: input.name,
|
|
37640
38369
|
description: input.description,
|
|
@@ -38123,7 +38852,14 @@ async function complete(opts) {
|
|
|
38123
38852
|
temperature: opts.temperature
|
|
38124
38853
|
});
|
|
38125
38854
|
res = await loop.complete(input, {
|
|
38126
|
-
structured: opts.structured
|
|
38855
|
+
structured: opts.structured,
|
|
38856
|
+
providerOptions: opts.providerOptions,
|
|
38857
|
+
audio: opts.audio,
|
|
38858
|
+
outputModalities: opts.outputModalities,
|
|
38859
|
+
serviceTier,
|
|
38860
|
+
cache: opts.cache,
|
|
38861
|
+
topK: opts.topK,
|
|
38862
|
+
seed: opts.seed
|
|
38127
38863
|
});
|
|
38128
38864
|
} else {
|
|
38129
38865
|
res = await llm.complete(input, {
|
|
@@ -38134,12 +38870,16 @@ async function complete(opts) {
|
|
|
38134
38870
|
providerOptions: opts.providerOptions,
|
|
38135
38871
|
audio: opts.audio,
|
|
38136
38872
|
outputModalities: opts.outputModalities,
|
|
38137
|
-
serviceTier
|
|
38873
|
+
serviceTier,
|
|
38874
|
+
cache: opts.cache,
|
|
38875
|
+
topK: opts.topK,
|
|
38876
|
+
seed: opts.seed
|
|
38138
38877
|
});
|
|
38139
38878
|
}
|
|
38140
38879
|
const result = {
|
|
38141
38880
|
text: res.text,
|
|
38142
38881
|
response: res,
|
|
38882
|
+
...res.error ? { error: res.error } : {},
|
|
38143
38883
|
// Bound to this call's client (same provider/model/key/engine).
|
|
38144
38884
|
retrieveFile: (file) => llm.retrieveFile(file),
|
|
38145
38885
|
streamFile: (file) => llm.streamFile(file)
|
|
@@ -38485,10 +39225,21 @@ var McpResultCache = class {
|
|
|
38485
39225
|
}
|
|
38486
39226
|
return hit.value;
|
|
38487
39227
|
}
|
|
38488
|
-
/** Store only when the server actually asked for it. Returns whether anything was stored.
|
|
39228
|
+
/** Store only when the server actually asked for it. Returns whether anything was stored.
|
|
39229
|
+
*
|
|
39230
|
+
* A non-positive `ttlMs` is an instruction, not a missing value: the server is saying *do not
|
|
39231
|
+
* reuse this*. Any entry already held under that key is dropped, so the next `get` re-fetches.
|
|
39232
|
+
* Without the eviction the hint is inert — a server that first said "cache for 60s" and then
|
|
39233
|
+
* says "stale now" would keep being answered from the stale entry for the rest of the original
|
|
39234
|
+
* TTL. Absent hints are different and must stay different: they carry no instruction, so an
|
|
39235
|
+
* existing entry is left alone and pre-2026 servers behave exactly as before. */
|
|
38489
39236
|
set(key, value, hints, now = Date.now()) {
|
|
38490
39237
|
const ttl = hints?.ttlMs;
|
|
38491
|
-
if (typeof ttl
|
|
39238
|
+
if (typeof ttl === "number" && Number.isFinite(ttl) && ttl <= 0) {
|
|
39239
|
+
this.entries.delete(key);
|
|
39240
|
+
return false;
|
|
39241
|
+
}
|
|
39242
|
+
if (typeof ttl !== "number" || !Number.isFinite(ttl)) return false;
|
|
38492
39243
|
this.entries.set(key, {
|
|
38493
39244
|
value,
|
|
38494
39245
|
expiresAt: now + ttl,
|
|
@@ -39646,11 +40397,23 @@ function mcpPromptToMessages(result) {
|
|
|
39646
40397
|
}
|
|
39647
40398
|
function mcpToolToAgentTool(client, tool, namespace, opts = {}) {
|
|
39648
40399
|
return {
|
|
40400
|
+
...opts.lazy ? { lazy: true } : {},
|
|
39649
40401
|
definition: {
|
|
39650
40402
|
type: "function",
|
|
39651
40403
|
name: `${namespace}__${tool.name}`,
|
|
39652
40404
|
description: tool.description ?? tool.title ?? tool.name,
|
|
39653
|
-
parameters: tool.inputSchema ?? { type: "object", properties: {} }
|
|
40405
|
+
parameters: tool.inputSchema ?? { type: "object", properties: {} },
|
|
40406
|
+
// MCP publishes a schema for the tool's structured output and OpenAI
|
|
40407
|
+
// Responses accepts one (`output_schema`), so the model can reason over the
|
|
40408
|
+
// shape it will get back.
|
|
40409
|
+
//
|
|
40410
|
+
// Gated on `validateOutput` because declaring it is a PROMISE, not a hint:
|
|
40411
|
+
// the provider then requires the result to be JSON matching the schema, so
|
|
40412
|
+
// the tool result changes from prose to structured data. Forwarding it
|
|
40413
|
+
// unconditionally would silently reshape every existing MCP tool result —
|
|
40414
|
+
// and did, until a live round trip through OpenAI Responses failed. Anyone
|
|
40415
|
+
// asking for output validation has already opted into that contract.
|
|
40416
|
+
...opts.validateOutput && tool.outputSchema ? { outputSchema: tool.outputSchema } : {}
|
|
39654
40417
|
},
|
|
39655
40418
|
execute: async (args, ctx) => {
|
|
39656
40419
|
const res = await client.callTool(tool.name, args, ctx.trace);
|
|
@@ -39658,6 +40421,9 @@ function mcpToolToAgentTool(client, tool, namespace, opts = {}) {
|
|
|
39658
40421
|
const errors = validateJsonSchema(tool.outputSchema, res.structuredContent);
|
|
39659
40422
|
if (errors.length > 0) return `Tool output failed schema validation: ${errors.slice(0, 5).join("; ")}`;
|
|
39660
40423
|
}
|
|
40424
|
+
if (opts.validateOutput && tool.outputSchema && res.structuredContent !== void 0 && !res.isError) {
|
|
40425
|
+
return JSON.stringify(res.structuredContent);
|
|
40426
|
+
}
|
|
39661
40427
|
return mcpContentToResult(res);
|
|
39662
40428
|
}
|
|
39663
40429
|
};
|
|
@@ -40394,7 +41160,9 @@ async function connectMcp(config, opts = {}) {
|
|
|
40394
41160
|
const refresh = async (c) => {
|
|
40395
41161
|
const defs = await c.listTools();
|
|
40396
41162
|
tools.length = 0;
|
|
40397
|
-
for (const d of defs)
|
|
41163
|
+
for (const d of defs) {
|
|
41164
|
+
tools.push(mcpToolToAgentTool(c, d, ns, { validateOutput: opts.validateOutput, lazy: opts.lazy }));
|
|
41165
|
+
}
|
|
40398
41166
|
};
|
|
40399
41167
|
const sampler = opts.sampling ? samplingHandler(opts.sampling) : null;
|
|
40400
41168
|
const capabilities = {};
|
|
@@ -42926,6 +43694,8 @@ export {
|
|
|
42926
43694
|
LAYER_EXECUTOR_TOOL_EXAMPLES,
|
|
42927
43695
|
LAYER_LEGACY_SYSTEM,
|
|
42928
43696
|
LAYER_MEMORY,
|
|
43697
|
+
LAZY_CALL_TOOL,
|
|
43698
|
+
LAZY_SEARCH_TOOL,
|
|
42929
43699
|
LLMClient,
|
|
42930
43700
|
LLMError,
|
|
42931
43701
|
LLM_DEF_KEY,
|
|
@@ -43053,6 +43823,7 @@ export {
|
|
|
43053
43823
|
defineLLMTool,
|
|
43054
43824
|
defineTool,
|
|
43055
43825
|
delegate,
|
|
43826
|
+
describeTool,
|
|
43056
43827
|
discoverMetadata,
|
|
43057
43828
|
dispatch,
|
|
43058
43829
|
embed,
|
|
@@ -43114,6 +43885,7 @@ export {
|
|
|
43114
43885
|
parseSSEStream,
|
|
43115
43886
|
parseToolId,
|
|
43116
43887
|
pcmToWav,
|
|
43888
|
+
rankTools,
|
|
43117
43889
|
readFactsLayer,
|
|
43118
43890
|
reflectionGuidance,
|
|
43119
43891
|
refreshTokens,
|
|
@@ -43131,7 +43903,9 @@ export {
|
|
|
43131
43903
|
selectVariant,
|
|
43132
43904
|
shellGlob,
|
|
43133
43905
|
sniffImageMime,
|
|
43906
|
+
strictSupport,
|
|
43134
43907
|
submitBatch,
|
|
43908
|
+
toolKey,
|
|
43135
43909
|
transcribe,
|
|
43136
43910
|
trimReplacer,
|
|
43137
43911
|
tryParseToolId,
|