@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.
@@ -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
- var traceKey = (ids) => ids.requestId ? `${ids.sessionId ?? "?"}:${ids.requestId}` : void 0;
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.model": c.model,
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
- this.spans.push({
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.spans.push({
603
+ this.recordSpan({
370
604
  traceId,
371
- spanId: `media:${traceId}`,
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
- "agent.id": c.agentId,
388
- "agent.model": c.model
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
- "tool.name": c.toolName,
418
- "agent.id": c.agentId
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.spans.push({
449
- traceId: server,
450
- spanId: `mcp:connect:${server}`,
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.spans.push({
473
- traceId: server,
474
- spanId: `mcp:tool:${server}:${tool}:${now}`,
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
- spanId: key,
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.spans.push(span);
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
- traceId: s.traceId,
564
- spanId: s.spanId,
565
- name: s.name,
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]) => ({ key, value: { stringValue: String(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: settings.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 : contentToText(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 contentToText(content) {
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
- trace: { sessionId: ctx.sessionId, requestId: ctx.requestId, callId: ctx.callId }
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
- trace: { sessionId: ctx.sessionId, requestId: ctx.requestId, callId: ctx.callId }
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
- input_schema: t.parameters
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 (t.strict) tool.strict = true;
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
- type: "function",
28117
- function: {
28118
- name: t.name,
28119
- description: t.description,
28120
- parameters: t.parameters,
28121
- ...t.strict ? { strict: true } : {}
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: req.structured.strict ?? true
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: ensureAdditionalProperties(t.parameters),
29043
- strict: t.strict ?? true,
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: ensureAdditionalProperties(req.structured.schema),
29069
- strict: req.structured.strict ?? true
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: { ...options.ctx, conversationId: this._history.id },
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: { ...options.ctx, conversationId: this._history.id },
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: { sessionId: runTrace.sessionId, requestId: runTrace.requestId, callId: tc.id }
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: { sessionId: runTrace.sessionId, requestId: runTrace.requestId, callId: tc.id } };
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: { sessionId: runTrace.sessionId, requestId: runTrace.requestId, callId: tc.id }
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: { sessionId: runTrace.sessionId, requestId: runTrace.requestId, callId: tc.id } };
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
- /** Merge agent's tool definitions with caller-provided tools (caller wins on conflict). */
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 = { sessionId: this.id, requestId: runId };
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: { sessionId: runTrace.sessionId, requestId: runTrace.requestId },
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: { sessionId: runTrace.sessionId, requestId: runTrace.requestId },
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 !== "number" || !Number.isFinite(ttl) || ttl <= 0) return false;
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) tools.push(mcpToolToAgentTool(c, d, ns, { validateOutput: opts.validateOutput }));
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,