@combycode/llm-sdk 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +109 -0
- package/dist/agent/loop-config.d.ts +19 -0
- package/dist/agent/loop-internals.d.ts +13 -0
- package/dist/agent/loop.d.ts +6 -0
- package/dist/bus/hook-map.d.ts +6 -0
- package/dist/helpers/engine.d.ts +19 -0
- package/dist/index.browser.js +434 -47
- package/dist/index.d.ts +1 -1
- package/dist/index.js +434 -47
- package/dist/network/types.d.ts +6 -0
- package/dist/plugins/telemetry/telemetry.d.ts +144 -0
- package/dist/types/request-context.d.ts +11 -0
- package/package.json +1 -1
package/dist/index.browser.js
CHANGED
|
@@ -165,6 +165,86 @@ var REDACTED = "***REDACTED***";
|
|
|
165
165
|
var SENSITIVE_QUERY_PARAMS = /* @__PURE__ */ new Set(["key", "api_key", "access_token", "token"]);
|
|
166
166
|
var SENSITIVE_HEADERS = /* @__PURE__ */ new Set(["authorization", "x-goog-api-key", "x-api-key", "api-key"]);
|
|
167
167
|
var MAX_ERROR_RAW_CHARS = 512;
|
|
168
|
+
var OTLP_SPAN_KIND = { internal: 1, client: 3 };
|
|
169
|
+
var OTLP_KIND_BY_SPAN = {
|
|
170
|
+
llm: OTLP_SPAN_KIND.client,
|
|
171
|
+
http: OTLP_SPAN_KIND.client,
|
|
172
|
+
mcp: OTLP_SPAN_KIND.client,
|
|
173
|
+
media: OTLP_SPAN_KIND.client,
|
|
174
|
+
agent: OTLP_SPAN_KIND.internal,
|
|
175
|
+
tool: OTLP_SPAN_KIND.internal,
|
|
176
|
+
other: OTLP_SPAN_KIND.internal
|
|
177
|
+
};
|
|
178
|
+
function fnv1a32(input, seed) {
|
|
179
|
+
let h = seed >>> 0;
|
|
180
|
+
for (let i = 0; i < input.length; i++) {
|
|
181
|
+
h ^= input.charCodeAt(i);
|
|
182
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
183
|
+
}
|
|
184
|
+
return h >>> 0;
|
|
185
|
+
}
|
|
186
|
+
var isHex = (value, chars) => value.length === chars && /^[0-9a-f]+$/.test(value);
|
|
187
|
+
function toOtlpId(input, bytes) {
|
|
188
|
+
let out = "";
|
|
189
|
+
for (let i = 0; i < bytes / 4; i++) {
|
|
190
|
+
out += fnv1a32(input, 2166136261 + i * 2654435769 >>> 0).toString(16).padStart(8, "0");
|
|
191
|
+
}
|
|
192
|
+
return /^0+$/.test(out) ? `${out.slice(0, -1)}1` : out;
|
|
193
|
+
}
|
|
194
|
+
function toOtlpValue(value) {
|
|
195
|
+
if (typeof value === "boolean") return { boolValue: value };
|
|
196
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
197
|
+
return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
|
|
198
|
+
}
|
|
199
|
+
if (typeof value === "string") return { stringValue: value };
|
|
200
|
+
if (value === null || value === void 0) return { stringValue: "" };
|
|
201
|
+
return { stringValue: typeof value === "object" ? JSON.stringify(value) : String(value) };
|
|
202
|
+
}
|
|
203
|
+
var SPAN_NAME_SUBJECT = {
|
|
204
|
+
chat: "gen_ai.request.model",
|
|
205
|
+
invoke_agent: "gen_ai.agent.name",
|
|
206
|
+
execute_tool: "gen_ai.tool.name"
|
|
207
|
+
};
|
|
208
|
+
function otlpSpanName(span) {
|
|
209
|
+
const op = span.attributes["gen_ai.operation.name"];
|
|
210
|
+
if (typeof op !== "string") return span.name;
|
|
211
|
+
const subject = SPAN_NAME_SUBJECT[op] ? span.attributes[SPAN_NAME_SUBJECT[op]] : void 0;
|
|
212
|
+
return typeof subject === "string" && subject ? `${op} ${subject}` : op;
|
|
213
|
+
}
|
|
214
|
+
function toMessageList(payload, defaultRole) {
|
|
215
|
+
if (payload == null) return [];
|
|
216
|
+
if (typeof payload === "string") {
|
|
217
|
+
return payload ? [{ role: defaultRole, content: payload }] : [];
|
|
218
|
+
}
|
|
219
|
+
if (Array.isArray(payload)) {
|
|
220
|
+
const parts2 = payload;
|
|
221
|
+
if (parts2.length > 0 && parts2[0] && "role" in parts2[0]) {
|
|
222
|
+
return parts2.map((m) => ({ role: String(m.role ?? defaultRole), content: contentToText(m.content) })).filter((m) => m.content);
|
|
223
|
+
}
|
|
224
|
+
const text2 = contentToText(parts2);
|
|
225
|
+
return text2 ? [{ role: defaultRole, content: text2 }] : [];
|
|
226
|
+
}
|
|
227
|
+
const text = contentToText(payload);
|
|
228
|
+
return text ? [{ role: defaultRole, content: text }] : [];
|
|
229
|
+
}
|
|
230
|
+
function contentToText(content) {
|
|
231
|
+
if (typeof content === "string") return content;
|
|
232
|
+
if (!Array.isArray(content)) return "";
|
|
233
|
+
return content.map((part) => {
|
|
234
|
+
const p = part;
|
|
235
|
+
return typeof p?.text === "string" ? p.text : "";
|
|
236
|
+
}).filter(Boolean).join("");
|
|
237
|
+
}
|
|
238
|
+
var EVENT_TYPE_BY_KIND = {
|
|
239
|
+
agent: "agent",
|
|
240
|
+
tool: "tool",
|
|
241
|
+
llm: "llm",
|
|
242
|
+
http: "http",
|
|
243
|
+
mcp: "mcp",
|
|
244
|
+
media: "media",
|
|
245
|
+
other: "other"
|
|
246
|
+
};
|
|
247
|
+
var SAMPLE_SEED = 2654435769;
|
|
168
248
|
var CATEGORY = {
|
|
169
249
|
// Network
|
|
170
250
|
onEnqueue: "network",
|
|
@@ -233,10 +313,29 @@ function traceIdsOf(ctx) {
|
|
|
233
313
|
const t = c?.trace ?? c?.ctx ?? c;
|
|
234
314
|
return {
|
|
235
315
|
sessionId: t?.sessionId,
|
|
236
|
-
requestId: t?.requestId
|
|
316
|
+
requestId: t?.requestId,
|
|
317
|
+
/** W3C parent context, when the app is already inside a trace of its own. */
|
|
318
|
+
traceparent: t?.traceparent,
|
|
319
|
+
// `gen_ai.conversation.id` in the semantic conventions — the thread a turn
|
|
320
|
+
// belongs to, which is what lets a backend group turns into one conversation.
|
|
321
|
+
// AgentLoop sets it from the history id; a bare client call has none.
|
|
322
|
+
conversationId: t?.conversationId
|
|
237
323
|
};
|
|
238
324
|
}
|
|
239
|
-
|
|
325
|
+
function parseTraceparent(value) {
|
|
326
|
+
if (!value) return null;
|
|
327
|
+
const m = /^[0-9a-f]{2}-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/.exec(value.trim().toLowerCase());
|
|
328
|
+
if (!m) return null;
|
|
329
|
+
const [, traceId, spanId] = m;
|
|
330
|
+
if (/^0+$/.test(traceId) || /^0+$/.test(spanId)) return null;
|
|
331
|
+
return { traceId, spanId };
|
|
332
|
+
}
|
|
333
|
+
var CONTAINER_SPANS = /* @__PURE__ */ new Set(["agent.run", "tool.call"]);
|
|
334
|
+
var traceKey = (ids) => {
|
|
335
|
+
const parent = parseTraceparent(ids.traceparent);
|
|
336
|
+
if (parent) return parent.traceId;
|
|
337
|
+
return ids.requestId ? `${ids.sessionId ?? "?"}:${ids.requestId}` : void 0;
|
|
338
|
+
};
|
|
240
339
|
var TelemetryAdapter = class {
|
|
241
340
|
events = [];
|
|
242
341
|
spans = [];
|
|
@@ -257,17 +356,138 @@ var TelemetryAdapter = class {
|
|
|
257
356
|
/** Service identity stamped on exported telemetry. */
|
|
258
357
|
resource;
|
|
259
358
|
seq = 0;
|
|
359
|
+
/** Discriminator for POINT spans (media, mcp connect/tool), whose natural keys are
|
|
360
|
+
* not unique — the same server reconnects, a run emits two images, two tool calls
|
|
361
|
+
* land in one millisecond. A duplicate span id inside a trace is invalid OTLP and
|
|
362
|
+
* the backend silently keeps only one. */
|
|
363
|
+
spanSeq = 0;
|
|
364
|
+
/** Per trace: the app's span from a `traceparent`, and the CONTAINER spans currently
|
|
365
|
+
* open on it. Together they decide what a new span hangs under — see `parentFor`.
|
|
366
|
+
* Both are cleared once a trace has nothing open, so a long-lived process does not
|
|
367
|
+
* accumulate an entry per conversation forever.
|
|
368
|
+
*
|
|
369
|
+
* A list, not a single slot: an agent nested in a tool call (C2 inside C1's tool) is a
|
|
370
|
+
* second run on the SAME trace, and with one slot it overwrote its own parent and then
|
|
371
|
+
* deleted it on close — leaving the rest of the outer run parentless. */
|
|
372
|
+
appParent = /* @__PURE__ */ new Map();
|
|
373
|
+
containers = /* @__PURE__ */ new Map();
|
|
260
374
|
latSum = 0;
|
|
261
375
|
open = /* @__PURE__ */ new Map();
|
|
262
376
|
maxEvents;
|
|
263
377
|
includeSensitiveData;
|
|
264
378
|
unsub;
|
|
379
|
+
/** Subscribers, each with its own filter. Re-parenting is computed PER SINK: two
|
|
380
|
+
* consumers asking for different types each get a tree that is correct for them. */
|
|
381
|
+
sinks = [];
|
|
382
|
+
content;
|
|
383
|
+
sampleRate;
|
|
384
|
+
/** spanId → its parent and type, for EVERY span including filtered ones — walking up
|
|
385
|
+
* past a dropped ancestor is the whole point, so the dropped ones must still be here.
|
|
386
|
+
* Bounded, because a long-lived process would otherwise remember every span it ever
|
|
387
|
+
* saw. */
|
|
388
|
+
lineage = /* @__PURE__ */ new Map();
|
|
389
|
+
maxLineage;
|
|
390
|
+
msgSeq = 0;
|
|
265
391
|
constructor(hooks, opts = {}) {
|
|
266
392
|
this.maxEvents = opts.maxEvents ?? 2e3;
|
|
267
393
|
this.includeSensitiveData = opts.includeSensitiveData ?? true;
|
|
268
394
|
this.resource = opts.resource ?? { serviceName: "unknown_service" };
|
|
395
|
+
this.content = opts.content ?? "none";
|
|
396
|
+
this.sampleRate = opts.sample ?? 1;
|
|
397
|
+
this.maxLineage = this.maxEvents * 2;
|
|
398
|
+
if (opts.onTrace) this.onTrace({ types: opts.types }, opts.onTrace);
|
|
269
399
|
this.unsub = hooks.onAny((name, ctx) => this.handle(name, ctx));
|
|
270
400
|
}
|
|
401
|
+
onTrace(filterOrHandler, maybeHandler) {
|
|
402
|
+
const handler = typeof filterOrHandler === "function" ? filterOrHandler : maybeHandler;
|
|
403
|
+
if (!handler) throw new Error("onTrace requires a handler");
|
|
404
|
+
const filter = typeof filterOrHandler === "function" ? {} : filterOrHandler;
|
|
405
|
+
const sink = { types: filter.types ? new Set(filter.types) : void 0, handler };
|
|
406
|
+
this.sinks.push(sink);
|
|
407
|
+
return () => {
|
|
408
|
+
const at = this.sinks.indexOf(sink);
|
|
409
|
+
if (at !== -1) this.sinks.splice(at, 1);
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
/** Record a finished span and hand it to the subscribers. Every span reaches the store
|
|
413
|
+
* through here, so there is one place where an event can be missed rather than five. */
|
|
414
|
+
recordSpan(span) {
|
|
415
|
+
this.spans.push(span);
|
|
416
|
+
const type = EVENT_TYPE_BY_KIND[span.kind];
|
|
417
|
+
if (!this.lineage.has(span.spanId)) this.remember(span.spanId, span.parentSpanId, type);
|
|
418
|
+
this.dispatch({
|
|
419
|
+
type,
|
|
420
|
+
traceId: span.traceId,
|
|
421
|
+
spanId: span.spanId,
|
|
422
|
+
parentSpanId: span.parentSpanId,
|
|
423
|
+
name: otlpSpanName(span),
|
|
424
|
+
startTime: span.startTime,
|
|
425
|
+
endTime: span.endTime,
|
|
426
|
+
durationMs: span.durationMs,
|
|
427
|
+
status: span.status,
|
|
428
|
+
attributes: span.attributes
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
remember(spanId, parentSpanId, type) {
|
|
432
|
+
this.lineage.set(spanId, { parentSpanId, type });
|
|
433
|
+
if (this.lineage.size > this.maxLineage) {
|
|
434
|
+
const oldest = this.lineage.keys().next().value;
|
|
435
|
+
if (oldest !== void 0) this.lineage.delete(oldest);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
dispatch(event) {
|
|
439
|
+
if (this.sinks.length === 0) return;
|
|
440
|
+
if (!this.isSampled(event.traceId)) return;
|
|
441
|
+
for (const sink of this.sinks) {
|
|
442
|
+
if (sink.types && !sink.types.has(event.type)) continue;
|
|
443
|
+
const parentSpanId = sink.types ? this.survivingParent(event.parentSpanId, sink.types) : event.parentSpanId;
|
|
444
|
+
sink.handler(parentSpanId === event.parentSpanId ? event : { ...event, parentSpanId });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/** The nearest ancestor this subscriber actually receives. Without this, filtering out
|
|
448
|
+
* `http` would leave its children pointing at a span that never arrives, and a backend
|
|
449
|
+
* renders a dangling parent as a separate root. */
|
|
450
|
+
survivingParent(parentSpanId, types) {
|
|
451
|
+
let id = parentSpanId;
|
|
452
|
+
while (id) {
|
|
453
|
+
const node = this.lineage.get(id);
|
|
454
|
+
if (!node) return void 0;
|
|
455
|
+
if (types.has(node.type)) return id;
|
|
456
|
+
id = node.parentSpanId;
|
|
457
|
+
}
|
|
458
|
+
return void 0;
|
|
459
|
+
}
|
|
460
|
+
/** Hashed rather than random, so the same trace samples the same way in every process
|
|
461
|
+
* and a trace shared by two services is kept or dropped by both. */
|
|
462
|
+
isSampled(traceId) {
|
|
463
|
+
if (this.sampleRate >= 1) return true;
|
|
464
|
+
if (this.sampleRate <= 0) return false;
|
|
465
|
+
return fnv1a32(traceId, SAMPLE_SEED) / 4294967296 < this.sampleRate;
|
|
466
|
+
}
|
|
467
|
+
/** Conversation content, as its own event so it can be routed somewhere different from
|
|
468
|
+
* the spans — a debug store, not the metrics backend. */
|
|
469
|
+
emitMessage(span, direction, payload) {
|
|
470
|
+
if (this.sinks.length === 0) return;
|
|
471
|
+
const messages = toMessageList(payload, direction === "input" ? "user" : "assistant");
|
|
472
|
+
if (messages.length === 0) return;
|
|
473
|
+
const chars = messages.reduce((n, m) => n + m.content.length, 0);
|
|
474
|
+
this.dispatch({
|
|
475
|
+
type: "message",
|
|
476
|
+
traceId: span.traceId,
|
|
477
|
+
spanId: `${span.spanId}:msg${this.msgSeq++}`,
|
|
478
|
+
parentSpanId: span.spanId,
|
|
479
|
+
name: `message.${direction}`,
|
|
480
|
+
startTime: Date.now(),
|
|
481
|
+
status: "unset",
|
|
482
|
+
attributes: clean({
|
|
483
|
+
"message.direction": direction,
|
|
484
|
+
"message.count": messages.length,
|
|
485
|
+
"message.chars": chars,
|
|
486
|
+
// Opt-In in the spec, and off by default here for the same reason.
|
|
487
|
+
[`gen_ai.${direction}.messages`]: this.content === "full" ? messages : void 0
|
|
488
|
+
})
|
|
489
|
+
});
|
|
490
|
+
}
|
|
271
491
|
/** Stop tapping the bus. */
|
|
272
492
|
destroy() {
|
|
273
493
|
this.unsub();
|
|
@@ -275,6 +495,8 @@ var TelemetryAdapter = class {
|
|
|
275
495
|
handle(name, ctx) {
|
|
276
496
|
const ids = traceIdsOf(ctx);
|
|
277
497
|
const traceId = traceKey(ids);
|
|
498
|
+
const parent = parseTraceparent(ids.traceparent);
|
|
499
|
+
if (parent && traceId) this.appParent.set(traceId, parent.spanId);
|
|
278
500
|
this.events.push({
|
|
279
501
|
seq: this.seq++,
|
|
280
502
|
time: Date.now(),
|
|
@@ -297,20 +519,27 @@ var TelemetryAdapter = class {
|
|
|
297
519
|
this.metrics.outputTokens += usage.outputTokens ?? 0;
|
|
298
520
|
}
|
|
299
521
|
if (traceId) {
|
|
522
|
+
const responseModel = c.response?.model;
|
|
300
523
|
const attrs = {
|
|
301
|
-
"gen_ai.provider": c.provider,
|
|
302
|
-
"gen_ai.
|
|
524
|
+
"gen_ai.provider.name": c.provider,
|
|
525
|
+
"gen_ai.operation.name": "chat",
|
|
526
|
+
"gen_ai.request.model": c.model,
|
|
527
|
+
// The model that actually answered, which can differ from the one asked
|
|
528
|
+
// for (an alias resolving to a dated snapshot, a router picking a peer).
|
|
529
|
+
"gen_ai.response.model": responseModel,
|
|
530
|
+
"gen_ai.conversation.id": ids.conversationId,
|
|
303
531
|
"gen_ai.usage.input_tokens": usage?.inputTokens,
|
|
304
532
|
"gen_ai.usage.output_tokens": usage?.outputTokens
|
|
305
533
|
};
|
|
306
534
|
const key = `llm:${traceId}`;
|
|
535
|
+
let llmSpan;
|
|
307
536
|
if (this.open.has(key)) {
|
|
308
|
-
this.closeSpan(key, "ok", attrs);
|
|
537
|
+
llmSpan = this.closeSpan(key, "ok", attrs);
|
|
309
538
|
} else {
|
|
310
539
|
const http = [...this.spans].reverse().find((s) => s.traceId === traceId && s.kind === "http");
|
|
311
540
|
const start = http?.startTime ?? Date.now();
|
|
312
541
|
const end = Date.now();
|
|
313
|
-
|
|
542
|
+
llmSpan = {
|
|
314
543
|
traceId,
|
|
315
544
|
spanId: key,
|
|
316
545
|
name: "llm.request",
|
|
@@ -320,7 +549,12 @@ var TelemetryAdapter = class {
|
|
|
320
549
|
durationMs: end - start,
|
|
321
550
|
status: "ok",
|
|
322
551
|
attributes: clean(attrs)
|
|
323
|
-
}
|
|
552
|
+
};
|
|
553
|
+
this.recordSpan(llmSpan);
|
|
554
|
+
}
|
|
555
|
+
if (llmSpan) {
|
|
556
|
+
const response = c.response;
|
|
557
|
+
this.emitMessage(llmSpan, "output", response?.content ?? response?.text);
|
|
324
558
|
}
|
|
325
559
|
}
|
|
326
560
|
break;
|
|
@@ -366,9 +600,11 @@ var TelemetryAdapter = class {
|
|
|
366
600
|
this.metrics.mediaGenerated += c.count ?? 1;
|
|
367
601
|
if (traceId) {
|
|
368
602
|
const now = Date.now();
|
|
369
|
-
this.
|
|
603
|
+
this.recordSpan({
|
|
370
604
|
traceId,
|
|
371
|
-
|
|
605
|
+
// One run can generate several images; `media:${traceId}` would give them
|
|
606
|
+
// all the same span id, which is invalid within a trace.
|
|
607
|
+
spanId: `media:${traceId}:${this.spanSeq++}`,
|
|
372
608
|
name: "media.generate",
|
|
373
609
|
kind: "media",
|
|
374
610
|
startTime: now,
|
|
@@ -383,10 +619,21 @@ var TelemetryAdapter = class {
|
|
|
383
619
|
case "onRunStart": {
|
|
384
620
|
const runId = c.runId;
|
|
385
621
|
if (runId) {
|
|
386
|
-
this.openSpan(`agent:${runId}`, runId, "agent.run", "agent", {
|
|
387
|
-
|
|
388
|
-
|
|
622
|
+
const runSpan = this.openSpan(`agent:${runId}`, traceId ?? runId, "agent.run", "agent", {
|
|
623
|
+
// The host's own attributes go FIRST so ours win on a key collision: a stray
|
|
624
|
+
// `gen_ai.*` key in a caller's bag must not be able to rewrite the identity
|
|
625
|
+
// of the span.
|
|
626
|
+
...c.attributes,
|
|
627
|
+
"gen_ai.operation.name": "invoke_agent",
|
|
628
|
+
// Named when the agent was given a label; the exported span is then
|
|
629
|
+
// `invoke_agent {label}` rather than the bare operation.
|
|
630
|
+
"gen_ai.agent.name": c.label,
|
|
631
|
+
"gen_ai.agent.id": c.agentId,
|
|
632
|
+
"gen_ai.request.model": c.model,
|
|
633
|
+
// Ours, not a convention attribute — the GenAI spec has no term for it.
|
|
634
|
+
"agent.source": c.source
|
|
389
635
|
});
|
|
636
|
+
this.emitMessage(runSpan, "input", c.userMessage);
|
|
390
637
|
}
|
|
391
638
|
break;
|
|
392
639
|
}
|
|
@@ -413,9 +660,11 @@ var TelemetryAdapter = class {
|
|
|
413
660
|
case "onToolCallStart": {
|
|
414
661
|
const callId = c.callId;
|
|
415
662
|
if (callId) {
|
|
416
|
-
this.openSpan(`tool:${callId}`, callId, "tool.call", "tool", {
|
|
417
|
-
"
|
|
418
|
-
"
|
|
663
|
+
this.openSpan(`tool:${callId}`, traceId ?? callId, "tool.call", "tool", {
|
|
664
|
+
"gen_ai.operation.name": "execute_tool",
|
|
665
|
+
"gen_ai.tool.name": c.toolName,
|
|
666
|
+
"gen_ai.tool.call.id": callId,
|
|
667
|
+
"gen_ai.agent.id": c.agentId
|
|
419
668
|
});
|
|
420
669
|
}
|
|
421
670
|
break;
|
|
@@ -424,7 +673,7 @@ var TelemetryAdapter = class {
|
|
|
424
673
|
const callId = c.callId;
|
|
425
674
|
if (callId) {
|
|
426
675
|
this.closeSpan(`tool:${callId}`, "ok", {
|
|
427
|
-
"tool.name": c.toolName,
|
|
676
|
+
"gen_ai.tool.name": c.toolName,
|
|
428
677
|
"tool.latency_ms": c.latencyMs
|
|
429
678
|
});
|
|
430
679
|
}
|
|
@@ -434,7 +683,7 @@ var TelemetryAdapter = class {
|
|
|
434
683
|
const callId = c.callId;
|
|
435
684
|
if (callId) {
|
|
436
685
|
this.closeSpan(`tool:${callId}`, "error", {
|
|
437
|
-
"tool.name": c.toolName,
|
|
686
|
+
"gen_ai.tool.name": c.toolName,
|
|
438
687
|
"tool.error": c.error?.message
|
|
439
688
|
});
|
|
440
689
|
}
|
|
@@ -445,9 +694,17 @@ var TelemetryAdapter = class {
|
|
|
445
694
|
const server = c.server;
|
|
446
695
|
if (server) {
|
|
447
696
|
const now = Date.now();
|
|
448
|
-
this.
|
|
449
|
-
|
|
450
|
-
|
|
697
|
+
this.recordSpan({
|
|
698
|
+
// A connect usually happens at startup, outside any run, so there is often
|
|
699
|
+
// no trace to join — but keying the trace by server name merged every
|
|
700
|
+
// reconnect over the process lifetime into one trace. Falls back to a span
|
|
701
|
+
// of its own instead.
|
|
702
|
+
traceId: traceId ?? `mcp:connect:${server}:${this.spanSeq}`,
|
|
703
|
+
// `${server}` alone repeats on every reconnect, and a duplicate span id
|
|
704
|
+
// within a trace is invalid OTLP — the backend keeps one and drops the
|
|
705
|
+
// rest. The counter is monotonic where a timestamp is not: two connects
|
|
706
|
+
// inside the same millisecond would still collide.
|
|
707
|
+
spanId: `mcp:connect:${server}:${this.spanSeq++}`,
|
|
451
708
|
name: "mcp.connect",
|
|
452
709
|
kind: "mcp",
|
|
453
710
|
startTime: now,
|
|
@@ -469,9 +726,14 @@ var TelemetryAdapter = class {
|
|
|
469
726
|
if (server && tool) {
|
|
470
727
|
const now = Date.now();
|
|
471
728
|
const lat = c.latencyMs;
|
|
472
|
-
this.
|
|
473
|
-
|
|
474
|
-
|
|
729
|
+
this.recordSpan({
|
|
730
|
+
// An MCP tool call happens INSIDE a run, so it belongs to that run's trace.
|
|
731
|
+
// Keying it by server put every call to one server in a single eternal
|
|
732
|
+
// trace, and none of them with the agent that made the call.
|
|
733
|
+
traceId: traceId ?? `mcp:${server}`,
|
|
734
|
+
// A timestamp is not a unique key: two tool calls in the same millisecond
|
|
735
|
+
// share it. The counter is.
|
|
736
|
+
spanId: `mcp:tool:${server}:${tool}:${this.spanSeq++}`,
|
|
475
737
|
name: "mcp.tool_call",
|
|
476
738
|
kind: "mcp",
|
|
477
739
|
startTime: now - (lat ?? 0),
|
|
@@ -489,10 +751,33 @@ var TelemetryAdapter = class {
|
|
|
489
751
|
}
|
|
490
752
|
}
|
|
491
753
|
}
|
|
754
|
+
/** What a new span on this trace hangs under: the innermost container still open on
|
|
755
|
+
* it, else the app's span, else nothing (we are the root).
|
|
756
|
+
*
|
|
757
|
+
* A container wins over the app's span because an LLM call made during a run belongs
|
|
758
|
+
* to that run — attaching it straight to the app would flatten the very nesting the
|
|
759
|
+
* tree exists to show. A span joins the stack only after it is built, so nothing can
|
|
760
|
+
* become its own parent, and a run nested in a tool call lands under that tool call —
|
|
761
|
+
* exactly where it happened.
|
|
762
|
+
*
|
|
763
|
+
* Limit worth naming: with tools running in parallel two `tool.call` spans are open at
|
|
764
|
+
* once and "innermost" is merely the more recent one. Attributing a nested run to the
|
|
765
|
+
* right sibling needs real async context propagation, which this adapter does not
|
|
766
|
+
* have; sequential tools, the common case, are exact. */
|
|
767
|
+
parentFor(traceId) {
|
|
768
|
+
const stack = this.containers.get(traceId);
|
|
769
|
+
return stack?.[stack.length - 1] ?? this.appParent.get(traceId);
|
|
770
|
+
}
|
|
492
771
|
openSpan(key, traceId, spanName, kind, attributes) {
|
|
772
|
+
const parentSpanId = this.parentFor(traceId);
|
|
493
773
|
const span = {
|
|
494
774
|
traceId,
|
|
495
|
-
|
|
775
|
+
...parentSpanId ? { parentSpanId } : {},
|
|
776
|
+
// The KEY pairs open with close (`llm:${traceId}`); the SPAN ID must be unique.
|
|
777
|
+
// Those were the same string until a run stopped fragmenting into one trace per
|
|
778
|
+
// call — at which point every LLM call in a run produced the identical key, and
|
|
779
|
+
// the collision merged them into one span at the collector.
|
|
780
|
+
spanId: `${key}#${this.spanSeq++}`,
|
|
496
781
|
name: spanName,
|
|
497
782
|
kind,
|
|
498
783
|
startTime: Date.now(),
|
|
@@ -500,17 +785,33 @@ var TelemetryAdapter = class {
|
|
|
500
785
|
attributes
|
|
501
786
|
};
|
|
502
787
|
this.open.set(key, span);
|
|
788
|
+
this.remember(span.spanId, parentSpanId, EVENT_TYPE_BY_KIND[kind]);
|
|
789
|
+
if (CONTAINER_SPANS.has(spanName)) {
|
|
790
|
+
const stack = this.containers.get(traceId);
|
|
791
|
+
if (stack) stack.push(span.spanId);
|
|
792
|
+
else this.containers.set(traceId, [span.spanId]);
|
|
793
|
+
}
|
|
503
794
|
return span;
|
|
504
795
|
}
|
|
505
796
|
closeSpan(key, status, attributes) {
|
|
506
797
|
const span = this.open.get(key);
|
|
507
|
-
if (!span) return;
|
|
798
|
+
if (!span) return void 0;
|
|
799
|
+
const stack = this.containers.get(span.traceId);
|
|
800
|
+
if (stack) {
|
|
801
|
+
const at = stack.lastIndexOf(span.spanId);
|
|
802
|
+
if (at !== -1) stack.splice(at, 1);
|
|
803
|
+
if (stack.length === 0) {
|
|
804
|
+
this.containers.delete(span.traceId);
|
|
805
|
+
this.appParent.delete(span.traceId);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
508
808
|
span.endTime = Date.now();
|
|
509
809
|
span.durationMs = span.endTime - span.startTime;
|
|
510
810
|
span.status = status;
|
|
511
811
|
Object.assign(span.attributes, clean(attributes));
|
|
512
812
|
this.open.delete(key);
|
|
513
|
-
this.
|
|
813
|
+
this.recordSpan(span);
|
|
814
|
+
return span;
|
|
514
815
|
}
|
|
515
816
|
recordLatency(ms) {
|
|
516
817
|
if (typeof ms !== "number") return;
|
|
@@ -560,14 +861,27 @@ var TelemetryAdapter = class {
|
|
|
560
861
|
{
|
|
561
862
|
scope: { name: "combycode.telemetry" },
|
|
562
863
|
spans: this.spans.map((s) => ({
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
864
|
+
// An app-supplied trace id is ALREADY a real 32-hex id — hashing it
|
|
865
|
+
// would produce a different trace and defeat the whole point of
|
|
866
|
+
// accepting a parent.
|
|
867
|
+
traceId: isHex(s.traceId, 32) ? s.traceId : toOtlpId(s.traceId, 16),
|
|
868
|
+
// Scoped by trace: two conversations can each hold a span keyed
|
|
869
|
+
// `llm:…`, and colliding their ids would merge unrelated traces.
|
|
870
|
+
spanId: toOtlpId(`${s.traceId}|${s.spanId}`, 8),
|
|
871
|
+
// The app's own span id arrives as hex and passes through; one of ours
|
|
872
|
+
// is hashed exactly as it was when we emitted it, so the link matches.
|
|
873
|
+
...s.parentSpanId ? {
|
|
874
|
+
parentSpanId: isHex(s.parentSpanId, 16) ? s.parentSpanId : toOtlpId(`${s.traceId}|${s.parentSpanId}`, 8)
|
|
875
|
+
} : {},
|
|
876
|
+
name: otlpSpanName(s),
|
|
566
877
|
startTimeUnixNano: Math.round(s.startTime * 1e6),
|
|
567
878
|
endTimeUnixNano: Math.round((s.endTime ?? s.startTime) * 1e6),
|
|
568
|
-
kind: s.kind,
|
|
879
|
+
kind: OTLP_KIND_BY_SPAN[s.kind] ?? OTLP_SPAN_KIND.internal,
|
|
569
880
|
status: { code: s.status === "error" ? 2 : s.status === "ok" ? 1 : 0 },
|
|
570
|
-
attributes: Object.entries(s.attributes).map(([key, value]) => ({
|
|
881
|
+
attributes: Object.entries(s.attributes).map(([key, value]) => ({
|
|
882
|
+
key,
|
|
883
|
+
value: toOtlpValue(value)
|
|
884
|
+
}))
|
|
571
885
|
}))
|
|
572
886
|
}
|
|
573
887
|
]
|
|
@@ -25220,7 +25534,7 @@ function extractSystem(messages) {
|
|
|
25220
25534
|
const rest = [];
|
|
25221
25535
|
for (const m of messages) {
|
|
25222
25536
|
if (m.role === "system") {
|
|
25223
|
-
const text = typeof m.content === "string" ? m.content :
|
|
25537
|
+
const text = typeof m.content === "string" ? m.content : contentToText2(m.content);
|
|
25224
25538
|
if (text) systemTexts.push(text);
|
|
25225
25539
|
} else {
|
|
25226
25540
|
rest.push(m);
|
|
@@ -25231,7 +25545,7 @@ function extractSystem(messages) {
|
|
|
25231
25545
|
messages: rest
|
|
25232
25546
|
};
|
|
25233
25547
|
}
|
|
25234
|
-
function
|
|
25548
|
+
function contentToText2(content) {
|
|
25235
25549
|
return content.filter((p) => p.type === "text").map((p) => p.text).join("\n");
|
|
25236
25550
|
}
|
|
25237
25551
|
function parseStructured(text) {
|
|
@@ -25513,7 +25827,15 @@ var LLMClient = class {
|
|
|
25513
25827
|
signal: options.signal,
|
|
25514
25828
|
provider: this.provider,
|
|
25515
25829
|
model: this.model,
|
|
25516
|
-
|
|
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
|
+
}
|
|
25517
25839
|
};
|
|
25518
25840
|
response = await this.fetchFn(httpReq, {
|
|
25519
25841
|
queueName: this.queueName,
|
|
@@ -25641,7 +25963,15 @@ var LLMClient = class {
|
|
|
25641
25963
|
stream: true,
|
|
25642
25964
|
provider: this.provider,
|
|
25643
25965
|
model: this.model,
|
|
25644
|
-
|
|
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
|
+
}
|
|
25645
25975
|
};
|
|
25646
25976
|
const start = performance.now();
|
|
25647
25977
|
let text = "";
|
|
@@ -31548,6 +31878,12 @@ async function handleToolError(e, tc, hooks, runId, agentId, step, metrics, repo
|
|
|
31548
31878
|
// src/agent/loop.ts
|
|
31549
31879
|
var AgentLoop = class _AgentLoop {
|
|
31550
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;
|
|
31551
31887
|
client;
|
|
31552
31888
|
hooks;
|
|
31553
31889
|
_system;
|
|
@@ -31621,6 +31957,9 @@ var AgentLoop = class _AgentLoop {
|
|
|
31621
31957
|
this._history = new ConversationHistory();
|
|
31622
31958
|
}
|
|
31623
31959
|
this.id = this._history.id;
|
|
31960
|
+
this.label = config.label;
|
|
31961
|
+
this.source = config.source;
|
|
31962
|
+
this.attributes = config.attributes;
|
|
31624
31963
|
writeAgentLoopSystem(this._history.registry, this._system, "agent-loop");
|
|
31625
31964
|
writeAgentLoopContext(this._history.registry, this._context, "agent-loop");
|
|
31626
31965
|
this.syncLazyProtocol();
|
|
@@ -31725,7 +32064,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31725
32064
|
}
|
|
31726
32065
|
// ─── complete (non-streaming) ───────────────────────────────────────────
|
|
31727
32066
|
async complete(input, options = {}) {
|
|
31728
|
-
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input);
|
|
32067
|
+
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input, options.ctx);
|
|
31729
32068
|
const steps = [];
|
|
31730
32069
|
const totalUsage = emptyUsage();
|
|
31731
32070
|
let totalLlmTimeMs = 0;
|
|
@@ -31777,7 +32116,23 @@ var AgentLoop = class _AgentLoop {
|
|
|
31777
32116
|
thinking: options.thinking ?? this._thinking,
|
|
31778
32117
|
cache: options.cache ?? this._cache,
|
|
31779
32118
|
tools: this.toolDefinitions(options),
|
|
31780
|
-
ctx: {
|
|
32119
|
+
ctx: {
|
|
32120
|
+
// The RUN's trace, handed down to every LLM call it makes.
|
|
32121
|
+
//
|
|
32122
|
+
// Without this the agent kept `runTrace` to itself: its own spans used it
|
|
32123
|
+
// while each `client.complete()` fell through to mint-if-absent and
|
|
32124
|
+
// invented a fresh `requestId`. Since the trace id is `sessionId:requestId`,
|
|
32125
|
+
// one conversation arrived at the backend as SEVERAL unrelated traces —
|
|
32126
|
+
// measured against a real collector: a single turn with one tool call
|
|
32127
|
+
// produced six. Correlation is the whole point of a trace id, so this is
|
|
32128
|
+
// the one thing it must not get wrong.
|
|
32129
|
+
...runTrace,
|
|
32130
|
+
conversationId: this._history.id,
|
|
32131
|
+
// A caller's explicit ctx wins over all of the above: an app that already
|
|
32132
|
+
// owns a request id or a conversation id has better information than we do,
|
|
32133
|
+
// and silently overwriting it is how its telemetry stops joining up.
|
|
32134
|
+
...options.ctx
|
|
32135
|
+
},
|
|
31781
32136
|
signal: options.signal ?? this._abortController?.signal
|
|
31782
32137
|
});
|
|
31783
32138
|
const stepLatency = performance.now() - stepStart;
|
|
@@ -31945,7 +32300,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31945
32300
|
}
|
|
31946
32301
|
// ─── stream ─────────────────────────────────────────────────────────────
|
|
31947
32302
|
async *stream(input, options = {}) {
|
|
31948
|
-
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input);
|
|
32303
|
+
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input, options.ctx);
|
|
31949
32304
|
const steps = [];
|
|
31950
32305
|
const totalUsage = emptyUsage();
|
|
31951
32306
|
let totalLlmTimeMs = 0;
|
|
@@ -32001,7 +32356,23 @@ var AgentLoop = class _AgentLoop {
|
|
|
32001
32356
|
thinking: options.thinking ?? this._thinking,
|
|
32002
32357
|
cache: options.cache ?? this._cache,
|
|
32003
32358
|
tools: this.toolDefinitions(options),
|
|
32004
|
-
ctx: {
|
|
32359
|
+
ctx: {
|
|
32360
|
+
// The RUN's trace, handed down to every LLM call it makes.
|
|
32361
|
+
//
|
|
32362
|
+
// Without this the agent kept `runTrace` to itself: its own spans used it
|
|
32363
|
+
// while each `client.complete()` fell through to mint-if-absent and
|
|
32364
|
+
// invented a fresh `requestId`. Since the trace id is `sessionId:requestId`,
|
|
32365
|
+
// one conversation arrived at the backend as SEVERAL unrelated traces —
|
|
32366
|
+
// measured against a real collector: a single turn with one tool call
|
|
32367
|
+
// produced six. Correlation is the whole point of a trace id, so this is
|
|
32368
|
+
// the one thing it must not get wrong.
|
|
32369
|
+
...runTrace,
|
|
32370
|
+
conversationId: this._history.id,
|
|
32371
|
+
// A caller's explicit ctx wins over all of the above: an app that already
|
|
32372
|
+
// owns a request id or a conversation id has better information than we do,
|
|
32373
|
+
// and silently overwriting it is how its telemetry stops joining up.
|
|
32374
|
+
...options.ctx
|
|
32375
|
+
},
|
|
32005
32376
|
signal: options.signal ?? this._abortController?.signal
|
|
32006
32377
|
})) {
|
|
32007
32378
|
const toYield = accumulateStreamEvent(event, state);
|
|
@@ -32213,7 +32584,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32213
32584
|
arguments: tc.arguments,
|
|
32214
32585
|
callId: tc.id,
|
|
32215
32586
|
step,
|
|
32216
|
-
trace: {
|
|
32587
|
+
trace: { ...runTrace, callId: tc.id }
|
|
32217
32588
|
});
|
|
32218
32589
|
if (!decision.pass) {
|
|
32219
32590
|
return this.buildDeniedResult(tc, decision.reason, reports);
|
|
@@ -32233,7 +32604,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32233
32604
|
}
|
|
32234
32605
|
}
|
|
32235
32606
|
try {
|
|
32236
|
-
const baseCtx = { step, callId: tc.id, metrics, trace: {
|
|
32607
|
+
const baseCtx = { step, callId: tc.id, metrics, trace: { ...runTrace, callId: tc.id } };
|
|
32237
32608
|
const result = await executeWithTimeout(lookup.tool, tc, baseCtx, this._toolTimeout);
|
|
32238
32609
|
return await this.buildSuccessResult(tc, result, runId, step, metrics, reports, toolStart, runTrace, lookup.tool, baseCtx);
|
|
32239
32610
|
} catch (e) {
|
|
@@ -32291,7 +32662,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32291
32662
|
arguments: tc.arguments,
|
|
32292
32663
|
reason,
|
|
32293
32664
|
step,
|
|
32294
|
-
trace: {
|
|
32665
|
+
trace: { ...runTrace, callId: tc.id }
|
|
32295
32666
|
};
|
|
32296
32667
|
const pending = {
|
|
32297
32668
|
callId: tc.id,
|
|
@@ -32324,7 +32695,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32324
32695
|
return this.buildOverriddenResult(tc, decision.overrideResult, reports);
|
|
32325
32696
|
}
|
|
32326
32697
|
try {
|
|
32327
|
-
const baseCtx = { step, callId: tc.id, metrics, trace: {
|
|
32698
|
+
const baseCtx = { step, callId: tc.id, metrics, trace: { ...runTrace, callId: tc.id } };
|
|
32328
32699
|
const result = await executeWithTimeout(tool, tc, baseCtx, this._toolTimeout);
|
|
32329
32700
|
return await this.buildSuccessResult(tc, result, runId, step, metrics, reports, toolStart, runTrace, tool, baseCtx);
|
|
32330
32701
|
} catch (e) {
|
|
@@ -32420,7 +32791,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32420
32791
|
return own.length > 0 ? own : void 0;
|
|
32421
32792
|
}
|
|
32422
32793
|
// ─── Run helpers ────────────────────────────────────────────────────────
|
|
32423
|
-
async beginRun(input) {
|
|
32794
|
+
async beginRun(input, callerCtx) {
|
|
32424
32795
|
if (this._running) throw new Error("AgentLoop is already running");
|
|
32425
32796
|
this._running = true;
|
|
32426
32797
|
this._stopRequested = false;
|
|
@@ -32437,10 +32808,23 @@ var AgentLoop = class _AgentLoop {
|
|
|
32437
32808
|
const startedAt = Date.now();
|
|
32438
32809
|
const startPerf = performance.now();
|
|
32439
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);
|
|
32440
|
-
const runTrace = {
|
|
32811
|
+
const runTrace = {
|
|
32812
|
+
sessionId: callerCtx?.sessionId ?? this.id,
|
|
32813
|
+
requestId: callerCtx?.requestId ?? runId,
|
|
32814
|
+
// The caller's span travels WITH the ids, for the same reason they do: `agent.run`,
|
|
32815
|
+
// every `tool.call`, and any agent nested inside a tool reach the telemetry through
|
|
32816
|
+
// `runTrace` and nothing else. While this field was missing from it only the LLM
|
|
32817
|
+
// calls joined the app's trace — they are built from the caller's ctx directly —
|
|
32818
|
+
// and the run that made them sat in a second, unrelated one. Measured against a
|
|
32819
|
+
// live backend; the unit tests fed the hooks directly and never saw it.
|
|
32820
|
+
...callerCtx?.traceparent ? { traceparent: callerCtx.traceparent } : {}
|
|
32821
|
+
};
|
|
32441
32822
|
await this.hooks.emit("onRunStart", {
|
|
32442
32823
|
runId,
|
|
32443
32824
|
agentId: this.id,
|
|
32825
|
+
label: this.label,
|
|
32826
|
+
source: this.source,
|
|
32827
|
+
attributes: this.attributes,
|
|
32444
32828
|
userMessage: input,
|
|
32445
32829
|
model: this.client.model,
|
|
32446
32830
|
system: this._history.system,
|
|
@@ -32495,7 +32879,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32495
32879
|
if (g.kind !== "input") continue;
|
|
32496
32880
|
const decision = await g.check({
|
|
32497
32881
|
kind: "input",
|
|
32498
|
-
trace: {
|
|
32882
|
+
trace: { ...runTrace },
|
|
32499
32883
|
step,
|
|
32500
32884
|
messages,
|
|
32501
32885
|
system
|
|
@@ -32523,7 +32907,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32523
32907
|
if (g.kind !== "output") continue;
|
|
32524
32908
|
const decision = await g.check({
|
|
32525
32909
|
kind: "output",
|
|
32526
|
-
trace: {
|
|
32910
|
+
trace: { ...runTrace },
|
|
32527
32911
|
step,
|
|
32528
32912
|
response
|
|
32529
32913
|
});
|
|
@@ -34688,6 +35072,7 @@ function createEngine(config = {}) {
|
|
|
34688
35072
|
const fetchStreamBound = (req, options) => network.fetchStream(req, options);
|
|
34689
35073
|
const connectBound = (req) => network.connect(req);
|
|
34690
35074
|
const cost = new CostCollector({ hooks, catalog });
|
|
35075
|
+
const telemetry = config.telemetry ? new TelemetryAdapter(hooks, config.telemetry) : null;
|
|
34691
35076
|
const handle = {
|
|
34692
35077
|
sessionId,
|
|
34693
35078
|
hooks,
|
|
@@ -34700,9 +35085,11 @@ function createEngine(config = {}) {
|
|
|
34700
35085
|
connect: connectBound,
|
|
34701
35086
|
catalog,
|
|
34702
35087
|
cost,
|
|
35088
|
+
telemetry,
|
|
34703
35089
|
apiKeys: config.apiKeys ?? {},
|
|
34704
35090
|
destroy() {
|
|
34705
35091
|
cost.destroy();
|
|
35092
|
+
telemetry?.destroy();
|
|
34706
35093
|
network.destroy();
|
|
34707
35094
|
}
|
|
34708
35095
|
};
|