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