@combycode/llm-sdk 2.1.0 → 2.2.1
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 +133 -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/helpers/one-shot.d.ts +5 -1
- package/dist/index.browser.js +461 -49
- package/dist/index.d.ts +1 -1
- package/dist/index.js +461 -49
- package/dist/llm/providers/anthropic/constants.d.ts +30 -0
- 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 = "";
|
|
@@ -25766,6 +26096,19 @@ var ANTHROPIC_THINKING_BUDGETS = {
|
|
|
25766
26096
|
max: 16384
|
|
25767
26097
|
};
|
|
25768
26098
|
var DEFAULT_ANTHROPIC_THINKING_BUDGET = 2048;
|
|
26099
|
+
var ANTHROPIC_ADAPTIVE_THINKING_MIN = { major: 4, minor: 6 };
|
|
26100
|
+
function anthropicThinkingShape(model) {
|
|
26101
|
+
const id = model.toLowerCase().replace(/^anthropic\//, "");
|
|
26102
|
+
const modern = /^claude-[a-z]+-(\d+)(?:[-.](\d+))?/.exec(id);
|
|
26103
|
+
if (modern) {
|
|
26104
|
+
const major = Number(modern[1]);
|
|
26105
|
+
const minor = modern[2] === void 0 ? 0 : Number(modern[2]);
|
|
26106
|
+
const { major: minMajor, minor: minMinor } = ANTHROPIC_ADAPTIVE_THINKING_MIN;
|
|
26107
|
+
return major > minMajor || major === minMajor && minor >= minMinor ? "adaptive" : "budgeted";
|
|
26108
|
+
}
|
|
26109
|
+
if (/^claude-\d/.test(id)) return "budgeted";
|
|
26110
|
+
return "adaptive";
|
|
26111
|
+
}
|
|
25769
26112
|
var ANTHROPIC_TOP_K_MODELS = /^claude-(opus-4-(1|5|6)|sonnet-4-(5|6)|haiku-4-5)(\b|-)/;
|
|
25770
26113
|
function anthropicAcceptsTopK(model) {
|
|
25771
26114
|
return ANTHROPIC_TOP_K_MODELS.test(model);
|
|
@@ -26147,6 +26490,16 @@ var AnthropicAdapter = class {
|
|
|
26147
26490
|
}
|
|
26148
26491
|
if (req.thinking) {
|
|
26149
26492
|
if (req.thinking.mode === "off") {
|
|
26493
|
+
} else if (anthropicThinkingShape(req.model) === "adaptive") {
|
|
26494
|
+
const thinking = { type: "adaptive" };
|
|
26495
|
+
if (req.thinking.visibility === "hidden") thinking.display = "omitted";
|
|
26496
|
+
body.thinking = thinking;
|
|
26497
|
+
if (req.thinking.effort) {
|
|
26498
|
+
body.output_config = {
|
|
26499
|
+
...body.output_config ?? {},
|
|
26500
|
+
effort: req.thinking.effort
|
|
26501
|
+
};
|
|
26502
|
+
}
|
|
26150
26503
|
} else {
|
|
26151
26504
|
const budget = req.thinking.effort ? ANTHROPIC_THINKING_BUDGETS[req.thinking.effort] ?? DEFAULT_ANTHROPIC_THINKING_BUDGET : DEFAULT_ANTHROPIC_THINKING_BUDGET;
|
|
26152
26505
|
const thinking = { type: "enabled", budget_tokens: budget };
|
|
@@ -31548,6 +31901,12 @@ async function handleToolError(e, tc, hooks, runId, agentId, step, metrics, repo
|
|
|
31548
31901
|
// src/agent/loop.ts
|
|
31549
31902
|
var AgentLoop = class _AgentLoop {
|
|
31550
31903
|
id;
|
|
31904
|
+
/** Human name, surfaced as `gen_ai.agent.name` — see AgentLoopConfig.label. */
|
|
31905
|
+
label;
|
|
31906
|
+
/** Which part of the host system this agent belongs to. */
|
|
31907
|
+
source;
|
|
31908
|
+
/** Extra attributes stamped on this agent's spans. */
|
|
31909
|
+
attributes;
|
|
31551
31910
|
client;
|
|
31552
31911
|
hooks;
|
|
31553
31912
|
_system;
|
|
@@ -31621,6 +31980,9 @@ var AgentLoop = class _AgentLoop {
|
|
|
31621
31980
|
this._history = new ConversationHistory();
|
|
31622
31981
|
}
|
|
31623
31982
|
this.id = this._history.id;
|
|
31983
|
+
this.label = config.label;
|
|
31984
|
+
this.source = config.source;
|
|
31985
|
+
this.attributes = config.attributes;
|
|
31624
31986
|
writeAgentLoopSystem(this._history.registry, this._system, "agent-loop");
|
|
31625
31987
|
writeAgentLoopContext(this._history.registry, this._context, "agent-loop");
|
|
31626
31988
|
this.syncLazyProtocol();
|
|
@@ -31725,7 +32087,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31725
32087
|
}
|
|
31726
32088
|
// ─── complete (non-streaming) ───────────────────────────────────────────
|
|
31727
32089
|
async complete(input, options = {}) {
|
|
31728
|
-
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input);
|
|
32090
|
+
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input, options.ctx);
|
|
31729
32091
|
const steps = [];
|
|
31730
32092
|
const totalUsage = emptyUsage();
|
|
31731
32093
|
let totalLlmTimeMs = 0;
|
|
@@ -31777,7 +32139,23 @@ var AgentLoop = class _AgentLoop {
|
|
|
31777
32139
|
thinking: options.thinking ?? this._thinking,
|
|
31778
32140
|
cache: options.cache ?? this._cache,
|
|
31779
32141
|
tools: this.toolDefinitions(options),
|
|
31780
|
-
ctx: {
|
|
32142
|
+
ctx: {
|
|
32143
|
+
// The RUN's trace, handed down to every LLM call it makes.
|
|
32144
|
+
//
|
|
32145
|
+
// Without this the agent kept `runTrace` to itself: its own spans used it
|
|
32146
|
+
// while each `client.complete()` fell through to mint-if-absent and
|
|
32147
|
+
// invented a fresh `requestId`. Since the trace id is `sessionId:requestId`,
|
|
32148
|
+
// one conversation arrived at the backend as SEVERAL unrelated traces —
|
|
32149
|
+
// measured against a real collector: a single turn with one tool call
|
|
32150
|
+
// produced six. Correlation is the whole point of a trace id, so this is
|
|
32151
|
+
// the one thing it must not get wrong.
|
|
32152
|
+
...runTrace,
|
|
32153
|
+
conversationId: this._history.id,
|
|
32154
|
+
// A caller's explicit ctx wins over all of the above: an app that already
|
|
32155
|
+
// owns a request id or a conversation id has better information than we do,
|
|
32156
|
+
// and silently overwriting it is how its telemetry stops joining up.
|
|
32157
|
+
...options.ctx
|
|
32158
|
+
},
|
|
31781
32159
|
signal: options.signal ?? this._abortController?.signal
|
|
31782
32160
|
});
|
|
31783
32161
|
const stepLatency = performance.now() - stepStart;
|
|
@@ -31945,7 +32323,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31945
32323
|
}
|
|
31946
32324
|
// ─── stream ─────────────────────────────────────────────────────────────
|
|
31947
32325
|
async *stream(input, options = {}) {
|
|
31948
|
-
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input);
|
|
32326
|
+
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input, options.ctx);
|
|
31949
32327
|
const steps = [];
|
|
31950
32328
|
const totalUsage = emptyUsage();
|
|
31951
32329
|
let totalLlmTimeMs = 0;
|
|
@@ -32001,7 +32379,23 @@ var AgentLoop = class _AgentLoop {
|
|
|
32001
32379
|
thinking: options.thinking ?? this._thinking,
|
|
32002
32380
|
cache: options.cache ?? this._cache,
|
|
32003
32381
|
tools: this.toolDefinitions(options),
|
|
32004
|
-
ctx: {
|
|
32382
|
+
ctx: {
|
|
32383
|
+
// The RUN's trace, handed down to every LLM call it makes.
|
|
32384
|
+
//
|
|
32385
|
+
// Without this the agent kept `runTrace` to itself: its own spans used it
|
|
32386
|
+
// while each `client.complete()` fell through to mint-if-absent and
|
|
32387
|
+
// invented a fresh `requestId`. Since the trace id is `sessionId:requestId`,
|
|
32388
|
+
// one conversation arrived at the backend as SEVERAL unrelated traces —
|
|
32389
|
+
// measured against a real collector: a single turn with one tool call
|
|
32390
|
+
// produced six. Correlation is the whole point of a trace id, so this is
|
|
32391
|
+
// the one thing it must not get wrong.
|
|
32392
|
+
...runTrace,
|
|
32393
|
+
conversationId: this._history.id,
|
|
32394
|
+
// A caller's explicit ctx wins over all of the above: an app that already
|
|
32395
|
+
// owns a request id or a conversation id has better information than we do,
|
|
32396
|
+
// and silently overwriting it is how its telemetry stops joining up.
|
|
32397
|
+
...options.ctx
|
|
32398
|
+
},
|
|
32005
32399
|
signal: options.signal ?? this._abortController?.signal
|
|
32006
32400
|
})) {
|
|
32007
32401
|
const toYield = accumulateStreamEvent(event, state);
|
|
@@ -32213,7 +32607,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32213
32607
|
arguments: tc.arguments,
|
|
32214
32608
|
callId: tc.id,
|
|
32215
32609
|
step,
|
|
32216
|
-
trace: {
|
|
32610
|
+
trace: { ...runTrace, callId: tc.id }
|
|
32217
32611
|
});
|
|
32218
32612
|
if (!decision.pass) {
|
|
32219
32613
|
return this.buildDeniedResult(tc, decision.reason, reports);
|
|
@@ -32233,7 +32627,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32233
32627
|
}
|
|
32234
32628
|
}
|
|
32235
32629
|
try {
|
|
32236
|
-
const baseCtx = { step, callId: tc.id, metrics, trace: {
|
|
32630
|
+
const baseCtx = { step, callId: tc.id, metrics, trace: { ...runTrace, callId: tc.id } };
|
|
32237
32631
|
const result = await executeWithTimeout(lookup.tool, tc, baseCtx, this._toolTimeout);
|
|
32238
32632
|
return await this.buildSuccessResult(tc, result, runId, step, metrics, reports, toolStart, runTrace, lookup.tool, baseCtx);
|
|
32239
32633
|
} catch (e) {
|
|
@@ -32291,7 +32685,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32291
32685
|
arguments: tc.arguments,
|
|
32292
32686
|
reason,
|
|
32293
32687
|
step,
|
|
32294
|
-
trace: {
|
|
32688
|
+
trace: { ...runTrace, callId: tc.id }
|
|
32295
32689
|
};
|
|
32296
32690
|
const pending = {
|
|
32297
32691
|
callId: tc.id,
|
|
@@ -32324,7 +32718,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32324
32718
|
return this.buildOverriddenResult(tc, decision.overrideResult, reports);
|
|
32325
32719
|
}
|
|
32326
32720
|
try {
|
|
32327
|
-
const baseCtx = { step, callId: tc.id, metrics, trace: {
|
|
32721
|
+
const baseCtx = { step, callId: tc.id, metrics, trace: { ...runTrace, callId: tc.id } };
|
|
32328
32722
|
const result = await executeWithTimeout(tool, tc, baseCtx, this._toolTimeout);
|
|
32329
32723
|
return await this.buildSuccessResult(tc, result, runId, step, metrics, reports, toolStart, runTrace, tool, baseCtx);
|
|
32330
32724
|
} catch (e) {
|
|
@@ -32420,7 +32814,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32420
32814
|
return own.length > 0 ? own : void 0;
|
|
32421
32815
|
}
|
|
32422
32816
|
// ─── Run helpers ────────────────────────────────────────────────────────
|
|
32423
|
-
async beginRun(input) {
|
|
32817
|
+
async beginRun(input, callerCtx) {
|
|
32424
32818
|
if (this._running) throw new Error("AgentLoop is already running");
|
|
32425
32819
|
this._running = true;
|
|
32426
32820
|
this._stopRequested = false;
|
|
@@ -32437,10 +32831,23 @@ var AgentLoop = class _AgentLoop {
|
|
|
32437
32831
|
const startedAt = Date.now();
|
|
32438
32832
|
const startPerf = performance.now();
|
|
32439
32833
|
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 = {
|
|
32834
|
+
const runTrace = {
|
|
32835
|
+
sessionId: callerCtx?.sessionId ?? this.id,
|
|
32836
|
+
requestId: callerCtx?.requestId ?? runId,
|
|
32837
|
+
// The caller's span travels WITH the ids, for the same reason they do: `agent.run`,
|
|
32838
|
+
// every `tool.call`, and any agent nested inside a tool reach the telemetry through
|
|
32839
|
+
// `runTrace` and nothing else. While this field was missing from it only the LLM
|
|
32840
|
+
// calls joined the app's trace — they are built from the caller's ctx directly —
|
|
32841
|
+
// and the run that made them sat in a second, unrelated one. Measured against a
|
|
32842
|
+
// live backend; the unit tests fed the hooks directly and never saw it.
|
|
32843
|
+
...callerCtx?.traceparent ? { traceparent: callerCtx.traceparent } : {}
|
|
32844
|
+
};
|
|
32441
32845
|
await this.hooks.emit("onRunStart", {
|
|
32442
32846
|
runId,
|
|
32443
32847
|
agentId: this.id,
|
|
32848
|
+
label: this.label,
|
|
32849
|
+
source: this.source,
|
|
32850
|
+
attributes: this.attributes,
|
|
32444
32851
|
userMessage: input,
|
|
32445
32852
|
model: this.client.model,
|
|
32446
32853
|
system: this._history.system,
|
|
@@ -32495,7 +32902,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32495
32902
|
if (g.kind !== "input") continue;
|
|
32496
32903
|
const decision = await g.check({
|
|
32497
32904
|
kind: "input",
|
|
32498
|
-
trace: {
|
|
32905
|
+
trace: { ...runTrace },
|
|
32499
32906
|
step,
|
|
32500
32907
|
messages,
|
|
32501
32908
|
system
|
|
@@ -32523,7 +32930,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32523
32930
|
if (g.kind !== "output") continue;
|
|
32524
32931
|
const decision = await g.check({
|
|
32525
32932
|
kind: "output",
|
|
32526
|
-
trace: {
|
|
32933
|
+
trace: { ...runTrace },
|
|
32527
32934
|
step,
|
|
32528
32935
|
response
|
|
32529
32936
|
});
|
|
@@ -34688,6 +35095,7 @@ function createEngine(config = {}) {
|
|
|
34688
35095
|
const fetchStreamBound = (req, options) => network.fetchStream(req, options);
|
|
34689
35096
|
const connectBound = (req) => network.connect(req);
|
|
34690
35097
|
const cost = new CostCollector({ hooks, catalog });
|
|
35098
|
+
const telemetry = config.telemetry ? new TelemetryAdapter(hooks, config.telemetry) : null;
|
|
34691
35099
|
const handle = {
|
|
34692
35100
|
sessionId,
|
|
34693
35101
|
hooks,
|
|
@@ -34700,9 +35108,11 @@ function createEngine(config = {}) {
|
|
|
34700
35108
|
connect: connectBound,
|
|
34701
35109
|
catalog,
|
|
34702
35110
|
cost,
|
|
35111
|
+
telemetry,
|
|
34703
35112
|
apiKeys: config.apiKeys ?? {},
|
|
34704
35113
|
destroy() {
|
|
34705
35114
|
cost.destroy();
|
|
35115
|
+
telemetry?.destroy();
|
|
34706
35116
|
network.destroy();
|
|
34707
35117
|
}
|
|
34708
35118
|
};
|
|
@@ -38472,7 +38882,8 @@ async function complete(opts) {
|
|
|
38472
38882
|
serviceTier,
|
|
38473
38883
|
cache: opts.cache,
|
|
38474
38884
|
topK: opts.topK,
|
|
38475
|
-
seed: opts.seed
|
|
38885
|
+
seed: opts.seed,
|
|
38886
|
+
thinking: opts.thinking
|
|
38476
38887
|
});
|
|
38477
38888
|
} else {
|
|
38478
38889
|
res = await llm.complete(input, {
|
|
@@ -38486,7 +38897,8 @@ async function complete(opts) {
|
|
|
38486
38897
|
serviceTier,
|
|
38487
38898
|
cache: opts.cache,
|
|
38488
38899
|
topK: opts.topK,
|
|
38489
|
-
seed: opts.seed
|
|
38900
|
+
seed: opts.seed,
|
|
38901
|
+
thinking: opts.thinking
|
|
38490
38902
|
});
|
|
38491
38903
|
}
|
|
38492
38904
|
const result = {
|