@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.js
CHANGED
|
@@ -114,6 +114,86 @@ var REDACTED = "***REDACTED***";
|
|
|
114
114
|
var SENSITIVE_QUERY_PARAMS = /* @__PURE__ */ new Set(["key", "api_key", "access_token", "token"]);
|
|
115
115
|
var SENSITIVE_HEADERS = /* @__PURE__ */ new Set(["authorization", "x-goog-api-key", "x-api-key", "api-key"]);
|
|
116
116
|
var MAX_ERROR_RAW_CHARS = 512;
|
|
117
|
+
var OTLP_SPAN_KIND = { internal: 1, client: 3 };
|
|
118
|
+
var OTLP_KIND_BY_SPAN = {
|
|
119
|
+
llm: OTLP_SPAN_KIND.client,
|
|
120
|
+
http: OTLP_SPAN_KIND.client,
|
|
121
|
+
mcp: OTLP_SPAN_KIND.client,
|
|
122
|
+
media: OTLP_SPAN_KIND.client,
|
|
123
|
+
agent: OTLP_SPAN_KIND.internal,
|
|
124
|
+
tool: OTLP_SPAN_KIND.internal,
|
|
125
|
+
other: OTLP_SPAN_KIND.internal
|
|
126
|
+
};
|
|
127
|
+
function fnv1a32(input, seed) {
|
|
128
|
+
let h = seed >>> 0;
|
|
129
|
+
for (let i = 0; i < input.length; i++) {
|
|
130
|
+
h ^= input.charCodeAt(i);
|
|
131
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
132
|
+
}
|
|
133
|
+
return h >>> 0;
|
|
134
|
+
}
|
|
135
|
+
var isHex = (value, chars) => value.length === chars && /^[0-9a-f]+$/.test(value);
|
|
136
|
+
function toOtlpId(input, bytes) {
|
|
137
|
+
let out = "";
|
|
138
|
+
for (let i = 0; i < bytes / 4; i++) {
|
|
139
|
+
out += fnv1a32(input, 2166136261 + i * 2654435769 >>> 0).toString(16).padStart(8, "0");
|
|
140
|
+
}
|
|
141
|
+
return /^0+$/.test(out) ? `${out.slice(0, -1)}1` : out;
|
|
142
|
+
}
|
|
143
|
+
function toOtlpValue(value) {
|
|
144
|
+
if (typeof value === "boolean") return { boolValue: value };
|
|
145
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
146
|
+
return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
|
|
147
|
+
}
|
|
148
|
+
if (typeof value === "string") return { stringValue: value };
|
|
149
|
+
if (value === null || value === void 0) return { stringValue: "" };
|
|
150
|
+
return { stringValue: typeof value === "object" ? JSON.stringify(value) : String(value) };
|
|
151
|
+
}
|
|
152
|
+
var SPAN_NAME_SUBJECT = {
|
|
153
|
+
chat: "gen_ai.request.model",
|
|
154
|
+
invoke_agent: "gen_ai.agent.name",
|
|
155
|
+
execute_tool: "gen_ai.tool.name"
|
|
156
|
+
};
|
|
157
|
+
function otlpSpanName(span) {
|
|
158
|
+
const op = span.attributes["gen_ai.operation.name"];
|
|
159
|
+
if (typeof op !== "string") return span.name;
|
|
160
|
+
const subject = SPAN_NAME_SUBJECT[op] ? span.attributes[SPAN_NAME_SUBJECT[op]] : void 0;
|
|
161
|
+
return typeof subject === "string" && subject ? `${op} ${subject}` : op;
|
|
162
|
+
}
|
|
163
|
+
function toMessageList(payload, defaultRole) {
|
|
164
|
+
if (payload == null) return [];
|
|
165
|
+
if (typeof payload === "string") {
|
|
166
|
+
return payload ? [{ role: defaultRole, content: payload }] : [];
|
|
167
|
+
}
|
|
168
|
+
if (Array.isArray(payload)) {
|
|
169
|
+
const parts2 = payload;
|
|
170
|
+
if (parts2.length > 0 && parts2[0] && "role" in parts2[0]) {
|
|
171
|
+
return parts2.map((m) => ({ role: String(m.role ?? defaultRole), content: contentToText(m.content) })).filter((m) => m.content);
|
|
172
|
+
}
|
|
173
|
+
const text2 = contentToText(parts2);
|
|
174
|
+
return text2 ? [{ role: defaultRole, content: text2 }] : [];
|
|
175
|
+
}
|
|
176
|
+
const text = contentToText(payload);
|
|
177
|
+
return text ? [{ role: defaultRole, content: text }] : [];
|
|
178
|
+
}
|
|
179
|
+
function contentToText(content) {
|
|
180
|
+
if (typeof content === "string") return content;
|
|
181
|
+
if (!Array.isArray(content)) return "";
|
|
182
|
+
return content.map((part) => {
|
|
183
|
+
const p = part;
|
|
184
|
+
return typeof p?.text === "string" ? p.text : "";
|
|
185
|
+
}).filter(Boolean).join("");
|
|
186
|
+
}
|
|
187
|
+
var EVENT_TYPE_BY_KIND = {
|
|
188
|
+
agent: "agent",
|
|
189
|
+
tool: "tool",
|
|
190
|
+
llm: "llm",
|
|
191
|
+
http: "http",
|
|
192
|
+
mcp: "mcp",
|
|
193
|
+
media: "media",
|
|
194
|
+
other: "other"
|
|
195
|
+
};
|
|
196
|
+
var SAMPLE_SEED = 2654435769;
|
|
117
197
|
var CATEGORY = {
|
|
118
198
|
// Network
|
|
119
199
|
onEnqueue: "network",
|
|
@@ -182,10 +262,29 @@ function traceIdsOf(ctx) {
|
|
|
182
262
|
const t = c?.trace ?? c?.ctx ?? c;
|
|
183
263
|
return {
|
|
184
264
|
sessionId: t?.sessionId,
|
|
185
|
-
requestId: t?.requestId
|
|
265
|
+
requestId: t?.requestId,
|
|
266
|
+
/** W3C parent context, when the app is already inside a trace of its own. */
|
|
267
|
+
traceparent: t?.traceparent,
|
|
268
|
+
// `gen_ai.conversation.id` in the semantic conventions — the thread a turn
|
|
269
|
+
// belongs to, which is what lets a backend group turns into one conversation.
|
|
270
|
+
// AgentLoop sets it from the history id; a bare client call has none.
|
|
271
|
+
conversationId: t?.conversationId
|
|
186
272
|
};
|
|
187
273
|
}
|
|
188
|
-
|
|
274
|
+
function parseTraceparent(value) {
|
|
275
|
+
if (!value) return null;
|
|
276
|
+
const m = /^[0-9a-f]{2}-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/.exec(value.trim().toLowerCase());
|
|
277
|
+
if (!m) return null;
|
|
278
|
+
const [, traceId, spanId] = m;
|
|
279
|
+
if (/^0+$/.test(traceId) || /^0+$/.test(spanId)) return null;
|
|
280
|
+
return { traceId, spanId };
|
|
281
|
+
}
|
|
282
|
+
var CONTAINER_SPANS = /* @__PURE__ */ new Set(["agent.run", "tool.call"]);
|
|
283
|
+
var traceKey = (ids) => {
|
|
284
|
+
const parent = parseTraceparent(ids.traceparent);
|
|
285
|
+
if (parent) return parent.traceId;
|
|
286
|
+
return ids.requestId ? `${ids.sessionId ?? "?"}:${ids.requestId}` : void 0;
|
|
287
|
+
};
|
|
189
288
|
var TelemetryAdapter = class {
|
|
190
289
|
events = [];
|
|
191
290
|
spans = [];
|
|
@@ -206,17 +305,138 @@ var TelemetryAdapter = class {
|
|
|
206
305
|
/** Service identity stamped on exported telemetry. */
|
|
207
306
|
resource;
|
|
208
307
|
seq = 0;
|
|
308
|
+
/** Discriminator for POINT spans (media, mcp connect/tool), whose natural keys are
|
|
309
|
+
* not unique — the same server reconnects, a run emits two images, two tool calls
|
|
310
|
+
* land in one millisecond. A duplicate span id inside a trace is invalid OTLP and
|
|
311
|
+
* the backend silently keeps only one. */
|
|
312
|
+
spanSeq = 0;
|
|
313
|
+
/** Per trace: the app's span from a `traceparent`, and the CONTAINER spans currently
|
|
314
|
+
* open on it. Together they decide what a new span hangs under — see `parentFor`.
|
|
315
|
+
* Both are cleared once a trace has nothing open, so a long-lived process does not
|
|
316
|
+
* accumulate an entry per conversation forever.
|
|
317
|
+
*
|
|
318
|
+
* A list, not a single slot: an agent nested in a tool call (C2 inside C1's tool) is a
|
|
319
|
+
* second run on the SAME trace, and with one slot it overwrote its own parent and then
|
|
320
|
+
* deleted it on close — leaving the rest of the outer run parentless. */
|
|
321
|
+
appParent = /* @__PURE__ */ new Map();
|
|
322
|
+
containers = /* @__PURE__ */ new Map();
|
|
209
323
|
latSum = 0;
|
|
210
324
|
open = /* @__PURE__ */ new Map();
|
|
211
325
|
maxEvents;
|
|
212
326
|
includeSensitiveData;
|
|
213
327
|
unsub;
|
|
328
|
+
/** Subscribers, each with its own filter. Re-parenting is computed PER SINK: two
|
|
329
|
+
* consumers asking for different types each get a tree that is correct for them. */
|
|
330
|
+
sinks = [];
|
|
331
|
+
content;
|
|
332
|
+
sampleRate;
|
|
333
|
+
/** spanId → its parent and type, for EVERY span including filtered ones — walking up
|
|
334
|
+
* past a dropped ancestor is the whole point, so the dropped ones must still be here.
|
|
335
|
+
* Bounded, because a long-lived process would otherwise remember every span it ever
|
|
336
|
+
* saw. */
|
|
337
|
+
lineage = /* @__PURE__ */ new Map();
|
|
338
|
+
maxLineage;
|
|
339
|
+
msgSeq = 0;
|
|
214
340
|
constructor(hooks, opts = {}) {
|
|
215
341
|
this.maxEvents = opts.maxEvents ?? 2e3;
|
|
216
342
|
this.includeSensitiveData = opts.includeSensitiveData ?? true;
|
|
217
343
|
this.resource = opts.resource ?? { serviceName: "unknown_service" };
|
|
344
|
+
this.content = opts.content ?? "none";
|
|
345
|
+
this.sampleRate = opts.sample ?? 1;
|
|
346
|
+
this.maxLineage = this.maxEvents * 2;
|
|
347
|
+
if (opts.onTrace) this.onTrace({ types: opts.types }, opts.onTrace);
|
|
218
348
|
this.unsub = hooks.onAny((name, ctx) => this.handle(name, ctx));
|
|
219
349
|
}
|
|
350
|
+
onTrace(filterOrHandler, maybeHandler) {
|
|
351
|
+
const handler = typeof filterOrHandler === "function" ? filterOrHandler : maybeHandler;
|
|
352
|
+
if (!handler) throw new Error("onTrace requires a handler");
|
|
353
|
+
const filter = typeof filterOrHandler === "function" ? {} : filterOrHandler;
|
|
354
|
+
const sink = { types: filter.types ? new Set(filter.types) : void 0, handler };
|
|
355
|
+
this.sinks.push(sink);
|
|
356
|
+
return () => {
|
|
357
|
+
const at = this.sinks.indexOf(sink);
|
|
358
|
+
if (at !== -1) this.sinks.splice(at, 1);
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/** Record a finished span and hand it to the subscribers. Every span reaches the store
|
|
362
|
+
* through here, so there is one place where an event can be missed rather than five. */
|
|
363
|
+
recordSpan(span) {
|
|
364
|
+
this.spans.push(span);
|
|
365
|
+
const type = EVENT_TYPE_BY_KIND[span.kind];
|
|
366
|
+
if (!this.lineage.has(span.spanId)) this.remember(span.spanId, span.parentSpanId, type);
|
|
367
|
+
this.dispatch({
|
|
368
|
+
type,
|
|
369
|
+
traceId: span.traceId,
|
|
370
|
+
spanId: span.spanId,
|
|
371
|
+
parentSpanId: span.parentSpanId,
|
|
372
|
+
name: otlpSpanName(span),
|
|
373
|
+
startTime: span.startTime,
|
|
374
|
+
endTime: span.endTime,
|
|
375
|
+
durationMs: span.durationMs,
|
|
376
|
+
status: span.status,
|
|
377
|
+
attributes: span.attributes
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
remember(spanId, parentSpanId, type) {
|
|
381
|
+
this.lineage.set(spanId, { parentSpanId, type });
|
|
382
|
+
if (this.lineage.size > this.maxLineage) {
|
|
383
|
+
const oldest = this.lineage.keys().next().value;
|
|
384
|
+
if (oldest !== void 0) this.lineage.delete(oldest);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
dispatch(event) {
|
|
388
|
+
if (this.sinks.length === 0) return;
|
|
389
|
+
if (!this.isSampled(event.traceId)) return;
|
|
390
|
+
for (const sink of this.sinks) {
|
|
391
|
+
if (sink.types && !sink.types.has(event.type)) continue;
|
|
392
|
+
const parentSpanId = sink.types ? this.survivingParent(event.parentSpanId, sink.types) : event.parentSpanId;
|
|
393
|
+
sink.handler(parentSpanId === event.parentSpanId ? event : { ...event, parentSpanId });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/** The nearest ancestor this subscriber actually receives. Without this, filtering out
|
|
397
|
+
* `http` would leave its children pointing at a span that never arrives, and a backend
|
|
398
|
+
* renders a dangling parent as a separate root. */
|
|
399
|
+
survivingParent(parentSpanId, types) {
|
|
400
|
+
let id = parentSpanId;
|
|
401
|
+
while (id) {
|
|
402
|
+
const node = this.lineage.get(id);
|
|
403
|
+
if (!node) return void 0;
|
|
404
|
+
if (types.has(node.type)) return id;
|
|
405
|
+
id = node.parentSpanId;
|
|
406
|
+
}
|
|
407
|
+
return void 0;
|
|
408
|
+
}
|
|
409
|
+
/** Hashed rather than random, so the same trace samples the same way in every process
|
|
410
|
+
* and a trace shared by two services is kept or dropped by both. */
|
|
411
|
+
isSampled(traceId) {
|
|
412
|
+
if (this.sampleRate >= 1) return true;
|
|
413
|
+
if (this.sampleRate <= 0) return false;
|
|
414
|
+
return fnv1a32(traceId, SAMPLE_SEED) / 4294967296 < this.sampleRate;
|
|
415
|
+
}
|
|
416
|
+
/** Conversation content, as its own event so it can be routed somewhere different from
|
|
417
|
+
* the spans — a debug store, not the metrics backend. */
|
|
418
|
+
emitMessage(span, direction, payload) {
|
|
419
|
+
if (this.sinks.length === 0) return;
|
|
420
|
+
const messages = toMessageList(payload, direction === "input" ? "user" : "assistant");
|
|
421
|
+
if (messages.length === 0) return;
|
|
422
|
+
const chars = messages.reduce((n, m) => n + m.content.length, 0);
|
|
423
|
+
this.dispatch({
|
|
424
|
+
type: "message",
|
|
425
|
+
traceId: span.traceId,
|
|
426
|
+
spanId: `${span.spanId}:msg${this.msgSeq++}`,
|
|
427
|
+
parentSpanId: span.spanId,
|
|
428
|
+
name: `message.${direction}`,
|
|
429
|
+
startTime: Date.now(),
|
|
430
|
+
status: "unset",
|
|
431
|
+
attributes: clean({
|
|
432
|
+
"message.direction": direction,
|
|
433
|
+
"message.count": messages.length,
|
|
434
|
+
"message.chars": chars,
|
|
435
|
+
// Opt-In in the spec, and off by default here for the same reason.
|
|
436
|
+
[`gen_ai.${direction}.messages`]: this.content === "full" ? messages : void 0
|
|
437
|
+
})
|
|
438
|
+
});
|
|
439
|
+
}
|
|
220
440
|
/** Stop tapping the bus. */
|
|
221
441
|
destroy() {
|
|
222
442
|
this.unsub();
|
|
@@ -224,6 +444,8 @@ var TelemetryAdapter = class {
|
|
|
224
444
|
handle(name, ctx) {
|
|
225
445
|
const ids = traceIdsOf(ctx);
|
|
226
446
|
const traceId = traceKey(ids);
|
|
447
|
+
const parent = parseTraceparent(ids.traceparent);
|
|
448
|
+
if (parent && traceId) this.appParent.set(traceId, parent.spanId);
|
|
227
449
|
this.events.push({
|
|
228
450
|
seq: this.seq++,
|
|
229
451
|
time: Date.now(),
|
|
@@ -246,20 +468,27 @@ var TelemetryAdapter = class {
|
|
|
246
468
|
this.metrics.outputTokens += usage.outputTokens ?? 0;
|
|
247
469
|
}
|
|
248
470
|
if (traceId) {
|
|
471
|
+
const responseModel = c.response?.model;
|
|
249
472
|
const attrs = {
|
|
250
|
-
"gen_ai.provider": c.provider,
|
|
251
|
-
"gen_ai.
|
|
473
|
+
"gen_ai.provider.name": c.provider,
|
|
474
|
+
"gen_ai.operation.name": "chat",
|
|
475
|
+
"gen_ai.request.model": c.model,
|
|
476
|
+
// The model that actually answered, which can differ from the one asked
|
|
477
|
+
// for (an alias resolving to a dated snapshot, a router picking a peer).
|
|
478
|
+
"gen_ai.response.model": responseModel,
|
|
479
|
+
"gen_ai.conversation.id": ids.conversationId,
|
|
252
480
|
"gen_ai.usage.input_tokens": usage?.inputTokens,
|
|
253
481
|
"gen_ai.usage.output_tokens": usage?.outputTokens
|
|
254
482
|
};
|
|
255
483
|
const key = `llm:${traceId}`;
|
|
484
|
+
let llmSpan;
|
|
256
485
|
if (this.open.has(key)) {
|
|
257
|
-
this.closeSpan(key, "ok", attrs);
|
|
486
|
+
llmSpan = this.closeSpan(key, "ok", attrs);
|
|
258
487
|
} else {
|
|
259
488
|
const http = [...this.spans].reverse().find((s) => s.traceId === traceId && s.kind === "http");
|
|
260
489
|
const start = http?.startTime ?? Date.now();
|
|
261
490
|
const end = Date.now();
|
|
262
|
-
|
|
491
|
+
llmSpan = {
|
|
263
492
|
traceId,
|
|
264
493
|
spanId: key,
|
|
265
494
|
name: "llm.request",
|
|
@@ -269,7 +498,12 @@ var TelemetryAdapter = class {
|
|
|
269
498
|
durationMs: end - start,
|
|
270
499
|
status: "ok",
|
|
271
500
|
attributes: clean(attrs)
|
|
272
|
-
}
|
|
501
|
+
};
|
|
502
|
+
this.recordSpan(llmSpan);
|
|
503
|
+
}
|
|
504
|
+
if (llmSpan) {
|
|
505
|
+
const response = c.response;
|
|
506
|
+
this.emitMessage(llmSpan, "output", response?.content ?? response?.text);
|
|
273
507
|
}
|
|
274
508
|
}
|
|
275
509
|
break;
|
|
@@ -315,9 +549,11 @@ var TelemetryAdapter = class {
|
|
|
315
549
|
this.metrics.mediaGenerated += c.count ?? 1;
|
|
316
550
|
if (traceId) {
|
|
317
551
|
const now = Date.now();
|
|
318
|
-
this.
|
|
552
|
+
this.recordSpan({
|
|
319
553
|
traceId,
|
|
320
|
-
|
|
554
|
+
// One run can generate several images; `media:${traceId}` would give them
|
|
555
|
+
// all the same span id, which is invalid within a trace.
|
|
556
|
+
spanId: `media:${traceId}:${this.spanSeq++}`,
|
|
321
557
|
name: "media.generate",
|
|
322
558
|
kind: "media",
|
|
323
559
|
startTime: now,
|
|
@@ -332,10 +568,21 @@ var TelemetryAdapter = class {
|
|
|
332
568
|
case "onRunStart": {
|
|
333
569
|
const runId = c.runId;
|
|
334
570
|
if (runId) {
|
|
335
|
-
this.openSpan(`agent:${runId}`, runId, "agent.run", "agent", {
|
|
336
|
-
|
|
337
|
-
|
|
571
|
+
const runSpan = this.openSpan(`agent:${runId}`, traceId ?? runId, "agent.run", "agent", {
|
|
572
|
+
// The host's own attributes go FIRST so ours win on a key collision: a stray
|
|
573
|
+
// `gen_ai.*` key in a caller's bag must not be able to rewrite the identity
|
|
574
|
+
// of the span.
|
|
575
|
+
...c.attributes,
|
|
576
|
+
"gen_ai.operation.name": "invoke_agent",
|
|
577
|
+
// Named when the agent was given a label; the exported span is then
|
|
578
|
+
// `invoke_agent {label}` rather than the bare operation.
|
|
579
|
+
"gen_ai.agent.name": c.label,
|
|
580
|
+
"gen_ai.agent.id": c.agentId,
|
|
581
|
+
"gen_ai.request.model": c.model,
|
|
582
|
+
// Ours, not a convention attribute — the GenAI spec has no term for it.
|
|
583
|
+
"agent.source": c.source
|
|
338
584
|
});
|
|
585
|
+
this.emitMessage(runSpan, "input", c.userMessage);
|
|
339
586
|
}
|
|
340
587
|
break;
|
|
341
588
|
}
|
|
@@ -362,9 +609,11 @@ var TelemetryAdapter = class {
|
|
|
362
609
|
case "onToolCallStart": {
|
|
363
610
|
const callId = c.callId;
|
|
364
611
|
if (callId) {
|
|
365
|
-
this.openSpan(`tool:${callId}`, callId, "tool.call", "tool", {
|
|
366
|
-
"
|
|
367
|
-
"
|
|
612
|
+
this.openSpan(`tool:${callId}`, traceId ?? callId, "tool.call", "tool", {
|
|
613
|
+
"gen_ai.operation.name": "execute_tool",
|
|
614
|
+
"gen_ai.tool.name": c.toolName,
|
|
615
|
+
"gen_ai.tool.call.id": callId,
|
|
616
|
+
"gen_ai.agent.id": c.agentId
|
|
368
617
|
});
|
|
369
618
|
}
|
|
370
619
|
break;
|
|
@@ -373,7 +622,7 @@ var TelemetryAdapter = class {
|
|
|
373
622
|
const callId = c.callId;
|
|
374
623
|
if (callId) {
|
|
375
624
|
this.closeSpan(`tool:${callId}`, "ok", {
|
|
376
|
-
"tool.name": c.toolName,
|
|
625
|
+
"gen_ai.tool.name": c.toolName,
|
|
377
626
|
"tool.latency_ms": c.latencyMs
|
|
378
627
|
});
|
|
379
628
|
}
|
|
@@ -383,7 +632,7 @@ var TelemetryAdapter = class {
|
|
|
383
632
|
const callId = c.callId;
|
|
384
633
|
if (callId) {
|
|
385
634
|
this.closeSpan(`tool:${callId}`, "error", {
|
|
386
|
-
"tool.name": c.toolName,
|
|
635
|
+
"gen_ai.tool.name": c.toolName,
|
|
387
636
|
"tool.error": c.error?.message
|
|
388
637
|
});
|
|
389
638
|
}
|
|
@@ -394,9 +643,17 @@ var TelemetryAdapter = class {
|
|
|
394
643
|
const server = c.server;
|
|
395
644
|
if (server) {
|
|
396
645
|
const now = Date.now();
|
|
397
|
-
this.
|
|
398
|
-
|
|
399
|
-
|
|
646
|
+
this.recordSpan({
|
|
647
|
+
// A connect usually happens at startup, outside any run, so there is often
|
|
648
|
+
// no trace to join — but keying the trace by server name merged every
|
|
649
|
+
// reconnect over the process lifetime into one trace. Falls back to a span
|
|
650
|
+
// of its own instead.
|
|
651
|
+
traceId: traceId ?? `mcp:connect:${server}:${this.spanSeq}`,
|
|
652
|
+
// `${server}` alone repeats on every reconnect, and a duplicate span id
|
|
653
|
+
// within a trace is invalid OTLP — the backend keeps one and drops the
|
|
654
|
+
// rest. The counter is monotonic where a timestamp is not: two connects
|
|
655
|
+
// inside the same millisecond would still collide.
|
|
656
|
+
spanId: `mcp:connect:${server}:${this.spanSeq++}`,
|
|
400
657
|
name: "mcp.connect",
|
|
401
658
|
kind: "mcp",
|
|
402
659
|
startTime: now,
|
|
@@ -418,9 +675,14 @@ var TelemetryAdapter = class {
|
|
|
418
675
|
if (server && tool) {
|
|
419
676
|
const now = Date.now();
|
|
420
677
|
const lat = c.latencyMs;
|
|
421
|
-
this.
|
|
422
|
-
|
|
423
|
-
|
|
678
|
+
this.recordSpan({
|
|
679
|
+
// An MCP tool call happens INSIDE a run, so it belongs to that run's trace.
|
|
680
|
+
// Keying it by server put every call to one server in a single eternal
|
|
681
|
+
// trace, and none of them with the agent that made the call.
|
|
682
|
+
traceId: traceId ?? `mcp:${server}`,
|
|
683
|
+
// A timestamp is not a unique key: two tool calls in the same millisecond
|
|
684
|
+
// share it. The counter is.
|
|
685
|
+
spanId: `mcp:tool:${server}:${tool}:${this.spanSeq++}`,
|
|
424
686
|
name: "mcp.tool_call",
|
|
425
687
|
kind: "mcp",
|
|
426
688
|
startTime: now - (lat ?? 0),
|
|
@@ -438,10 +700,33 @@ var TelemetryAdapter = class {
|
|
|
438
700
|
}
|
|
439
701
|
}
|
|
440
702
|
}
|
|
703
|
+
/** What a new span on this trace hangs under: the innermost container still open on
|
|
704
|
+
* it, else the app's span, else nothing (we are the root).
|
|
705
|
+
*
|
|
706
|
+
* A container wins over the app's span because an LLM call made during a run belongs
|
|
707
|
+
* to that run — attaching it straight to the app would flatten the very nesting the
|
|
708
|
+
* tree exists to show. A span joins the stack only after it is built, so nothing can
|
|
709
|
+
* become its own parent, and a run nested in a tool call lands under that tool call —
|
|
710
|
+
* exactly where it happened.
|
|
711
|
+
*
|
|
712
|
+
* Limit worth naming: with tools running in parallel two `tool.call` spans are open at
|
|
713
|
+
* once and "innermost" is merely the more recent one. Attributing a nested run to the
|
|
714
|
+
* right sibling needs real async context propagation, which this adapter does not
|
|
715
|
+
* have; sequential tools, the common case, are exact. */
|
|
716
|
+
parentFor(traceId) {
|
|
717
|
+
const stack = this.containers.get(traceId);
|
|
718
|
+
return stack?.[stack.length - 1] ?? this.appParent.get(traceId);
|
|
719
|
+
}
|
|
441
720
|
openSpan(key, traceId, spanName, kind, attributes) {
|
|
721
|
+
const parentSpanId = this.parentFor(traceId);
|
|
442
722
|
const span = {
|
|
443
723
|
traceId,
|
|
444
|
-
|
|
724
|
+
...parentSpanId ? { parentSpanId } : {},
|
|
725
|
+
// The KEY pairs open with close (`llm:${traceId}`); the SPAN ID must be unique.
|
|
726
|
+
// Those were the same string until a run stopped fragmenting into one trace per
|
|
727
|
+
// call — at which point every LLM call in a run produced the identical key, and
|
|
728
|
+
// the collision merged them into one span at the collector.
|
|
729
|
+
spanId: `${key}#${this.spanSeq++}`,
|
|
445
730
|
name: spanName,
|
|
446
731
|
kind,
|
|
447
732
|
startTime: Date.now(),
|
|
@@ -449,17 +734,33 @@ var TelemetryAdapter = class {
|
|
|
449
734
|
attributes
|
|
450
735
|
};
|
|
451
736
|
this.open.set(key, span);
|
|
737
|
+
this.remember(span.spanId, parentSpanId, EVENT_TYPE_BY_KIND[kind]);
|
|
738
|
+
if (CONTAINER_SPANS.has(spanName)) {
|
|
739
|
+
const stack = this.containers.get(traceId);
|
|
740
|
+
if (stack) stack.push(span.spanId);
|
|
741
|
+
else this.containers.set(traceId, [span.spanId]);
|
|
742
|
+
}
|
|
452
743
|
return span;
|
|
453
744
|
}
|
|
454
745
|
closeSpan(key, status, attributes) {
|
|
455
746
|
const span = this.open.get(key);
|
|
456
|
-
if (!span) return;
|
|
747
|
+
if (!span) return void 0;
|
|
748
|
+
const stack = this.containers.get(span.traceId);
|
|
749
|
+
if (stack) {
|
|
750
|
+
const at = stack.lastIndexOf(span.spanId);
|
|
751
|
+
if (at !== -1) stack.splice(at, 1);
|
|
752
|
+
if (stack.length === 0) {
|
|
753
|
+
this.containers.delete(span.traceId);
|
|
754
|
+
this.appParent.delete(span.traceId);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
457
757
|
span.endTime = Date.now();
|
|
458
758
|
span.durationMs = span.endTime - span.startTime;
|
|
459
759
|
span.status = status;
|
|
460
760
|
Object.assign(span.attributes, clean(attributes));
|
|
461
761
|
this.open.delete(key);
|
|
462
|
-
this.
|
|
762
|
+
this.recordSpan(span);
|
|
763
|
+
return span;
|
|
463
764
|
}
|
|
464
765
|
recordLatency(ms) {
|
|
465
766
|
if (typeof ms !== "number") return;
|
|
@@ -509,14 +810,27 @@ var TelemetryAdapter = class {
|
|
|
509
810
|
{
|
|
510
811
|
scope: { name: "combycode.telemetry" },
|
|
511
812
|
spans: this.spans.map((s) => ({
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
813
|
+
// An app-supplied trace id is ALREADY a real 32-hex id — hashing it
|
|
814
|
+
// would produce a different trace and defeat the whole point of
|
|
815
|
+
// accepting a parent.
|
|
816
|
+
traceId: isHex(s.traceId, 32) ? s.traceId : toOtlpId(s.traceId, 16),
|
|
817
|
+
// Scoped by trace: two conversations can each hold a span keyed
|
|
818
|
+
// `llm:…`, and colliding their ids would merge unrelated traces.
|
|
819
|
+
spanId: toOtlpId(`${s.traceId}|${s.spanId}`, 8),
|
|
820
|
+
// The app's own span id arrives as hex and passes through; one of ours
|
|
821
|
+
// is hashed exactly as it was when we emitted it, so the link matches.
|
|
822
|
+
...s.parentSpanId ? {
|
|
823
|
+
parentSpanId: isHex(s.parentSpanId, 16) ? s.parentSpanId : toOtlpId(`${s.traceId}|${s.parentSpanId}`, 8)
|
|
824
|
+
} : {},
|
|
825
|
+
name: otlpSpanName(s),
|
|
515
826
|
startTimeUnixNano: Math.round(s.startTime * 1e6),
|
|
516
827
|
endTimeUnixNano: Math.round((s.endTime ?? s.startTime) * 1e6),
|
|
517
|
-
kind: s.kind,
|
|
828
|
+
kind: OTLP_KIND_BY_SPAN[s.kind] ?? OTLP_SPAN_KIND.internal,
|
|
518
829
|
status: { code: s.status === "error" ? 2 : s.status === "ok" ? 1 : 0 },
|
|
519
|
-
attributes: Object.entries(s.attributes).map(([key, value]) => ({
|
|
830
|
+
attributes: Object.entries(s.attributes).map(([key, value]) => ({
|
|
831
|
+
key,
|
|
832
|
+
value: toOtlpValue(value)
|
|
833
|
+
}))
|
|
520
834
|
}))
|
|
521
835
|
}
|
|
522
836
|
]
|
|
@@ -25147,7 +25461,7 @@ function extractSystem(messages) {
|
|
|
25147
25461
|
const rest = [];
|
|
25148
25462
|
for (const m of messages) {
|
|
25149
25463
|
if (m.role === "system") {
|
|
25150
|
-
const text = typeof m.content === "string" ? m.content :
|
|
25464
|
+
const text = typeof m.content === "string" ? m.content : contentToText2(m.content);
|
|
25151
25465
|
if (text) systemTexts.push(text);
|
|
25152
25466
|
} else {
|
|
25153
25467
|
rest.push(m);
|
|
@@ -25158,7 +25472,7 @@ function extractSystem(messages) {
|
|
|
25158
25472
|
messages: rest
|
|
25159
25473
|
};
|
|
25160
25474
|
}
|
|
25161
|
-
function
|
|
25475
|
+
function contentToText2(content) {
|
|
25162
25476
|
return content.filter((p) => p.type === "text").map((p) => p.text).join("\n");
|
|
25163
25477
|
}
|
|
25164
25478
|
function parseStructured(text) {
|
|
@@ -25440,7 +25754,15 @@ var LLMClient = class {
|
|
|
25440
25754
|
signal: options.signal,
|
|
25441
25755
|
provider: this.provider,
|
|
25442
25756
|
model: this.model,
|
|
25443
|
-
|
|
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
|
+
}
|
|
25444
25766
|
};
|
|
25445
25767
|
response = await this.fetchFn(httpReq, {
|
|
25446
25768
|
queueName: this.queueName,
|
|
@@ -25568,7 +25890,15 @@ var LLMClient = class {
|
|
|
25568
25890
|
stream: true,
|
|
25569
25891
|
provider: this.provider,
|
|
25570
25892
|
model: this.model,
|
|
25571
|
-
|
|
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
|
+
}
|
|
25572
25902
|
};
|
|
25573
25903
|
const start = performance.now();
|
|
25574
25904
|
let text = "";
|
|
@@ -31475,6 +31805,12 @@ async function handleToolError(e, tc, hooks, runId, agentId, step, metrics, repo
|
|
|
31475
31805
|
// src/agent/loop.ts
|
|
31476
31806
|
var AgentLoop = class _AgentLoop {
|
|
31477
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;
|
|
31478
31814
|
client;
|
|
31479
31815
|
hooks;
|
|
31480
31816
|
_system;
|
|
@@ -31548,6 +31884,9 @@ var AgentLoop = class _AgentLoop {
|
|
|
31548
31884
|
this._history = new ConversationHistory();
|
|
31549
31885
|
}
|
|
31550
31886
|
this.id = this._history.id;
|
|
31887
|
+
this.label = config.label;
|
|
31888
|
+
this.source = config.source;
|
|
31889
|
+
this.attributes = config.attributes;
|
|
31551
31890
|
writeAgentLoopSystem(this._history.registry, this._system, "agent-loop");
|
|
31552
31891
|
writeAgentLoopContext(this._history.registry, this._context, "agent-loop");
|
|
31553
31892
|
this.syncLazyProtocol();
|
|
@@ -31652,7 +31991,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31652
31991
|
}
|
|
31653
31992
|
// ─── complete (non-streaming) ───────────────────────────────────────────
|
|
31654
31993
|
async complete(input, options = {}) {
|
|
31655
|
-
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input);
|
|
31994
|
+
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input, options.ctx);
|
|
31656
31995
|
const steps = [];
|
|
31657
31996
|
const totalUsage = emptyUsage();
|
|
31658
31997
|
let totalLlmTimeMs = 0;
|
|
@@ -31704,7 +32043,23 @@ var AgentLoop = class _AgentLoop {
|
|
|
31704
32043
|
thinking: options.thinking ?? this._thinking,
|
|
31705
32044
|
cache: options.cache ?? this._cache,
|
|
31706
32045
|
tools: this.toolDefinitions(options),
|
|
31707
|
-
ctx: {
|
|
32046
|
+
ctx: {
|
|
32047
|
+
// The RUN's trace, handed down to every LLM call it makes.
|
|
32048
|
+
//
|
|
32049
|
+
// Without this the agent kept `runTrace` to itself: its own spans used it
|
|
32050
|
+
// while each `client.complete()` fell through to mint-if-absent and
|
|
32051
|
+
// invented a fresh `requestId`. Since the trace id is `sessionId:requestId`,
|
|
32052
|
+
// one conversation arrived at the backend as SEVERAL unrelated traces —
|
|
32053
|
+
// measured against a real collector: a single turn with one tool call
|
|
32054
|
+
// produced six. Correlation is the whole point of a trace id, so this is
|
|
32055
|
+
// the one thing it must not get wrong.
|
|
32056
|
+
...runTrace,
|
|
32057
|
+
conversationId: this._history.id,
|
|
32058
|
+
// A caller's explicit ctx wins over all of the above: an app that already
|
|
32059
|
+
// owns a request id or a conversation id has better information than we do,
|
|
32060
|
+
// and silently overwriting it is how its telemetry stops joining up.
|
|
32061
|
+
...options.ctx
|
|
32062
|
+
},
|
|
31708
32063
|
signal: options.signal ?? this._abortController?.signal
|
|
31709
32064
|
});
|
|
31710
32065
|
const stepLatency = performance.now() - stepStart;
|
|
@@ -31872,7 +32227,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31872
32227
|
}
|
|
31873
32228
|
// ─── stream ─────────────────────────────────────────────────────────────
|
|
31874
32229
|
async *stream(input, options = {}) {
|
|
31875
|
-
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input);
|
|
32230
|
+
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input, options.ctx);
|
|
31876
32231
|
const steps = [];
|
|
31877
32232
|
const totalUsage = emptyUsage();
|
|
31878
32233
|
let totalLlmTimeMs = 0;
|
|
@@ -31928,7 +32283,23 @@ var AgentLoop = class _AgentLoop {
|
|
|
31928
32283
|
thinking: options.thinking ?? this._thinking,
|
|
31929
32284
|
cache: options.cache ?? this._cache,
|
|
31930
32285
|
tools: this.toolDefinitions(options),
|
|
31931
|
-
ctx: {
|
|
32286
|
+
ctx: {
|
|
32287
|
+
// The RUN's trace, handed down to every LLM call it makes.
|
|
32288
|
+
//
|
|
32289
|
+
// Without this the agent kept `runTrace` to itself: its own spans used it
|
|
32290
|
+
// while each `client.complete()` fell through to mint-if-absent and
|
|
32291
|
+
// invented a fresh `requestId`. Since the trace id is `sessionId:requestId`,
|
|
32292
|
+
// one conversation arrived at the backend as SEVERAL unrelated traces —
|
|
32293
|
+
// measured against a real collector: a single turn with one tool call
|
|
32294
|
+
// produced six. Correlation is the whole point of a trace id, so this is
|
|
32295
|
+
// the one thing it must not get wrong.
|
|
32296
|
+
...runTrace,
|
|
32297
|
+
conversationId: this._history.id,
|
|
32298
|
+
// A caller's explicit ctx wins over all of the above: an app that already
|
|
32299
|
+
// owns a request id or a conversation id has better information than we do,
|
|
32300
|
+
// and silently overwriting it is how its telemetry stops joining up.
|
|
32301
|
+
...options.ctx
|
|
32302
|
+
},
|
|
31932
32303
|
signal: options.signal ?? this._abortController?.signal
|
|
31933
32304
|
})) {
|
|
31934
32305
|
const toYield = accumulateStreamEvent(event, state);
|
|
@@ -32140,7 +32511,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32140
32511
|
arguments: tc.arguments,
|
|
32141
32512
|
callId: tc.id,
|
|
32142
32513
|
step,
|
|
32143
|
-
trace: {
|
|
32514
|
+
trace: { ...runTrace, callId: tc.id }
|
|
32144
32515
|
});
|
|
32145
32516
|
if (!decision.pass) {
|
|
32146
32517
|
return this.buildDeniedResult(tc, decision.reason, reports);
|
|
@@ -32160,7 +32531,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32160
32531
|
}
|
|
32161
32532
|
}
|
|
32162
32533
|
try {
|
|
32163
|
-
const baseCtx = { step, callId: tc.id, metrics, trace: {
|
|
32534
|
+
const baseCtx = { step, callId: tc.id, metrics, trace: { ...runTrace, callId: tc.id } };
|
|
32164
32535
|
const result = await executeWithTimeout(lookup.tool, tc, baseCtx, this._toolTimeout);
|
|
32165
32536
|
return await this.buildSuccessResult(tc, result, runId, step, metrics, reports, toolStart, runTrace, lookup.tool, baseCtx);
|
|
32166
32537
|
} catch (e) {
|
|
@@ -32218,7 +32589,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32218
32589
|
arguments: tc.arguments,
|
|
32219
32590
|
reason,
|
|
32220
32591
|
step,
|
|
32221
|
-
trace: {
|
|
32592
|
+
trace: { ...runTrace, callId: tc.id }
|
|
32222
32593
|
};
|
|
32223
32594
|
const pending = {
|
|
32224
32595
|
callId: tc.id,
|
|
@@ -32251,7 +32622,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32251
32622
|
return this.buildOverriddenResult(tc, decision.overrideResult, reports);
|
|
32252
32623
|
}
|
|
32253
32624
|
try {
|
|
32254
|
-
const baseCtx = { step, callId: tc.id, metrics, trace: {
|
|
32625
|
+
const baseCtx = { step, callId: tc.id, metrics, trace: { ...runTrace, callId: tc.id } };
|
|
32255
32626
|
const result = await executeWithTimeout(tool, tc, baseCtx, this._toolTimeout);
|
|
32256
32627
|
return await this.buildSuccessResult(tc, result, runId, step, metrics, reports, toolStart, runTrace, tool, baseCtx);
|
|
32257
32628
|
} catch (e) {
|
|
@@ -32347,7 +32718,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32347
32718
|
return own.length > 0 ? own : void 0;
|
|
32348
32719
|
}
|
|
32349
32720
|
// ─── Run helpers ────────────────────────────────────────────────────────
|
|
32350
|
-
async beginRun(input) {
|
|
32721
|
+
async beginRun(input, callerCtx) {
|
|
32351
32722
|
if (this._running) throw new Error("AgentLoop is already running");
|
|
32352
32723
|
this._running = true;
|
|
32353
32724
|
this._stopRequested = false;
|
|
@@ -32364,10 +32735,23 @@ var AgentLoop = class _AgentLoop {
|
|
|
32364
32735
|
const startedAt = Date.now();
|
|
32365
32736
|
const startPerf = performance.now();
|
|
32366
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);
|
|
32367
|
-
const runTrace = {
|
|
32738
|
+
const runTrace = {
|
|
32739
|
+
sessionId: callerCtx?.sessionId ?? this.id,
|
|
32740
|
+
requestId: callerCtx?.requestId ?? runId,
|
|
32741
|
+
// The caller's span travels WITH the ids, for the same reason they do: `agent.run`,
|
|
32742
|
+
// every `tool.call`, and any agent nested inside a tool reach the telemetry through
|
|
32743
|
+
// `runTrace` and nothing else. While this field was missing from it only the LLM
|
|
32744
|
+
// calls joined the app's trace — they are built from the caller's ctx directly —
|
|
32745
|
+
// and the run that made them sat in a second, unrelated one. Measured against a
|
|
32746
|
+
// live backend; the unit tests fed the hooks directly and never saw it.
|
|
32747
|
+
...callerCtx?.traceparent ? { traceparent: callerCtx.traceparent } : {}
|
|
32748
|
+
};
|
|
32368
32749
|
await this.hooks.emit("onRunStart", {
|
|
32369
32750
|
runId,
|
|
32370
32751
|
agentId: this.id,
|
|
32752
|
+
label: this.label,
|
|
32753
|
+
source: this.source,
|
|
32754
|
+
attributes: this.attributes,
|
|
32371
32755
|
userMessage: input,
|
|
32372
32756
|
model: this.client.model,
|
|
32373
32757
|
system: this._history.system,
|
|
@@ -32422,7 +32806,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32422
32806
|
if (g.kind !== "input") continue;
|
|
32423
32807
|
const decision = await g.check({
|
|
32424
32808
|
kind: "input",
|
|
32425
|
-
trace: {
|
|
32809
|
+
trace: { ...runTrace },
|
|
32426
32810
|
step,
|
|
32427
32811
|
messages,
|
|
32428
32812
|
system
|
|
@@ -32450,7 +32834,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32450
32834
|
if (g.kind !== "output") continue;
|
|
32451
32835
|
const decision = await g.check({
|
|
32452
32836
|
kind: "output",
|
|
32453
|
-
trace: {
|
|
32837
|
+
trace: { ...runTrace },
|
|
32454
32838
|
step,
|
|
32455
32839
|
response
|
|
32456
32840
|
});
|
|
@@ -34615,6 +34999,7 @@ function createEngine(config = {}) {
|
|
|
34615
34999
|
const fetchStreamBound = (req, options) => network.fetchStream(req, options);
|
|
34616
35000
|
const connectBound = (req) => network.connect(req);
|
|
34617
35001
|
const cost = new CostCollector({ hooks, catalog });
|
|
35002
|
+
const telemetry = config.telemetry ? new TelemetryAdapter(hooks, config.telemetry) : null;
|
|
34618
35003
|
const handle = {
|
|
34619
35004
|
sessionId,
|
|
34620
35005
|
hooks,
|
|
@@ -34627,9 +35012,11 @@ function createEngine(config = {}) {
|
|
|
34627
35012
|
connect: connectBound,
|
|
34628
35013
|
catalog,
|
|
34629
35014
|
cost,
|
|
35015
|
+
telemetry,
|
|
34630
35016
|
apiKeys: config.apiKeys ?? {},
|
|
34631
35017
|
destroy() {
|
|
34632
35018
|
cost.destroy();
|
|
35019
|
+
telemetry?.destroy();
|
|
34633
35020
|
network.destroy();
|
|
34634
35021
|
}
|
|
34635
35022
|
};
|