@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.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 = "";
|
|
@@ -25693,6 +26023,19 @@ var ANTHROPIC_THINKING_BUDGETS = {
|
|
|
25693
26023
|
max: 16384
|
|
25694
26024
|
};
|
|
25695
26025
|
var DEFAULT_ANTHROPIC_THINKING_BUDGET = 2048;
|
|
26026
|
+
var ANTHROPIC_ADAPTIVE_THINKING_MIN = { major: 4, minor: 6 };
|
|
26027
|
+
function anthropicThinkingShape(model) {
|
|
26028
|
+
const id = model.toLowerCase().replace(/^anthropic\//, "");
|
|
26029
|
+
const modern = /^claude-[a-z]+-(\d+)(?:[-.](\d+))?/.exec(id);
|
|
26030
|
+
if (modern) {
|
|
26031
|
+
const major = Number(modern[1]);
|
|
26032
|
+
const minor = modern[2] === void 0 ? 0 : Number(modern[2]);
|
|
26033
|
+
const { major: minMajor, minor: minMinor } = ANTHROPIC_ADAPTIVE_THINKING_MIN;
|
|
26034
|
+
return major > minMajor || major === minMajor && minor >= minMinor ? "adaptive" : "budgeted";
|
|
26035
|
+
}
|
|
26036
|
+
if (/^claude-\d/.test(id)) return "budgeted";
|
|
26037
|
+
return "adaptive";
|
|
26038
|
+
}
|
|
25696
26039
|
var ANTHROPIC_TOP_K_MODELS = /^claude-(opus-4-(1|5|6)|sonnet-4-(5|6)|haiku-4-5)(\b|-)/;
|
|
25697
26040
|
function anthropicAcceptsTopK(model) {
|
|
25698
26041
|
return ANTHROPIC_TOP_K_MODELS.test(model);
|
|
@@ -26074,6 +26417,16 @@ var AnthropicAdapter = class {
|
|
|
26074
26417
|
}
|
|
26075
26418
|
if (req.thinking) {
|
|
26076
26419
|
if (req.thinking.mode === "off") {
|
|
26420
|
+
} else if (anthropicThinkingShape(req.model) === "adaptive") {
|
|
26421
|
+
const thinking = { type: "adaptive" };
|
|
26422
|
+
if (req.thinking.visibility === "hidden") thinking.display = "omitted";
|
|
26423
|
+
body.thinking = thinking;
|
|
26424
|
+
if (req.thinking.effort) {
|
|
26425
|
+
body.output_config = {
|
|
26426
|
+
...body.output_config ?? {},
|
|
26427
|
+
effort: req.thinking.effort
|
|
26428
|
+
};
|
|
26429
|
+
}
|
|
26077
26430
|
} else {
|
|
26078
26431
|
const budget = req.thinking.effort ? ANTHROPIC_THINKING_BUDGETS[req.thinking.effort] ?? DEFAULT_ANTHROPIC_THINKING_BUDGET : DEFAULT_ANTHROPIC_THINKING_BUDGET;
|
|
26079
26432
|
const thinking = { type: "enabled", budget_tokens: budget };
|
|
@@ -31475,6 +31828,12 @@ async function handleToolError(e, tc, hooks, runId, agentId, step, metrics, repo
|
|
|
31475
31828
|
// src/agent/loop.ts
|
|
31476
31829
|
var AgentLoop = class _AgentLoop {
|
|
31477
31830
|
id;
|
|
31831
|
+
/** Human name, surfaced as `gen_ai.agent.name` — see AgentLoopConfig.label. */
|
|
31832
|
+
label;
|
|
31833
|
+
/** Which part of the host system this agent belongs to. */
|
|
31834
|
+
source;
|
|
31835
|
+
/** Extra attributes stamped on this agent's spans. */
|
|
31836
|
+
attributes;
|
|
31478
31837
|
client;
|
|
31479
31838
|
hooks;
|
|
31480
31839
|
_system;
|
|
@@ -31548,6 +31907,9 @@ var AgentLoop = class _AgentLoop {
|
|
|
31548
31907
|
this._history = new ConversationHistory();
|
|
31549
31908
|
}
|
|
31550
31909
|
this.id = this._history.id;
|
|
31910
|
+
this.label = config.label;
|
|
31911
|
+
this.source = config.source;
|
|
31912
|
+
this.attributes = config.attributes;
|
|
31551
31913
|
writeAgentLoopSystem(this._history.registry, this._system, "agent-loop");
|
|
31552
31914
|
writeAgentLoopContext(this._history.registry, this._context, "agent-loop");
|
|
31553
31915
|
this.syncLazyProtocol();
|
|
@@ -31652,7 +32014,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31652
32014
|
}
|
|
31653
32015
|
// ─── complete (non-streaming) ───────────────────────────────────────────
|
|
31654
32016
|
async complete(input, options = {}) {
|
|
31655
|
-
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input);
|
|
32017
|
+
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input, options.ctx);
|
|
31656
32018
|
const steps = [];
|
|
31657
32019
|
const totalUsage = emptyUsage();
|
|
31658
32020
|
let totalLlmTimeMs = 0;
|
|
@@ -31704,7 +32066,23 @@ var AgentLoop = class _AgentLoop {
|
|
|
31704
32066
|
thinking: options.thinking ?? this._thinking,
|
|
31705
32067
|
cache: options.cache ?? this._cache,
|
|
31706
32068
|
tools: this.toolDefinitions(options),
|
|
31707
|
-
ctx: {
|
|
32069
|
+
ctx: {
|
|
32070
|
+
// The RUN's trace, handed down to every LLM call it makes.
|
|
32071
|
+
//
|
|
32072
|
+
// Without this the agent kept `runTrace` to itself: its own spans used it
|
|
32073
|
+
// while each `client.complete()` fell through to mint-if-absent and
|
|
32074
|
+
// invented a fresh `requestId`. Since the trace id is `sessionId:requestId`,
|
|
32075
|
+
// one conversation arrived at the backend as SEVERAL unrelated traces —
|
|
32076
|
+
// measured against a real collector: a single turn with one tool call
|
|
32077
|
+
// produced six. Correlation is the whole point of a trace id, so this is
|
|
32078
|
+
// the one thing it must not get wrong.
|
|
32079
|
+
...runTrace,
|
|
32080
|
+
conversationId: this._history.id,
|
|
32081
|
+
// A caller's explicit ctx wins over all of the above: an app that already
|
|
32082
|
+
// owns a request id or a conversation id has better information than we do,
|
|
32083
|
+
// and silently overwriting it is how its telemetry stops joining up.
|
|
32084
|
+
...options.ctx
|
|
32085
|
+
},
|
|
31708
32086
|
signal: options.signal ?? this._abortController?.signal
|
|
31709
32087
|
});
|
|
31710
32088
|
const stepLatency = performance.now() - stepStart;
|
|
@@ -31872,7 +32250,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
31872
32250
|
}
|
|
31873
32251
|
// ─── stream ─────────────────────────────────────────────────────────────
|
|
31874
32252
|
async *stream(input, options = {}) {
|
|
31875
|
-
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input);
|
|
32253
|
+
const { runId, startedAt, startPerf, userMessageText, runTrace } = await this.beginRun(input, options.ctx);
|
|
31876
32254
|
const steps = [];
|
|
31877
32255
|
const totalUsage = emptyUsage();
|
|
31878
32256
|
let totalLlmTimeMs = 0;
|
|
@@ -31928,7 +32306,23 @@ var AgentLoop = class _AgentLoop {
|
|
|
31928
32306
|
thinking: options.thinking ?? this._thinking,
|
|
31929
32307
|
cache: options.cache ?? this._cache,
|
|
31930
32308
|
tools: this.toolDefinitions(options),
|
|
31931
|
-
ctx: {
|
|
32309
|
+
ctx: {
|
|
32310
|
+
// The RUN's trace, handed down to every LLM call it makes.
|
|
32311
|
+
//
|
|
32312
|
+
// Without this the agent kept `runTrace` to itself: its own spans used it
|
|
32313
|
+
// while each `client.complete()` fell through to mint-if-absent and
|
|
32314
|
+
// invented a fresh `requestId`. Since the trace id is `sessionId:requestId`,
|
|
32315
|
+
// one conversation arrived at the backend as SEVERAL unrelated traces —
|
|
32316
|
+
// measured against a real collector: a single turn with one tool call
|
|
32317
|
+
// produced six. Correlation is the whole point of a trace id, so this is
|
|
32318
|
+
// the one thing it must not get wrong.
|
|
32319
|
+
...runTrace,
|
|
32320
|
+
conversationId: this._history.id,
|
|
32321
|
+
// A caller's explicit ctx wins over all of the above: an app that already
|
|
32322
|
+
// owns a request id or a conversation id has better information than we do,
|
|
32323
|
+
// and silently overwriting it is how its telemetry stops joining up.
|
|
32324
|
+
...options.ctx
|
|
32325
|
+
},
|
|
31932
32326
|
signal: options.signal ?? this._abortController?.signal
|
|
31933
32327
|
})) {
|
|
31934
32328
|
const toYield = accumulateStreamEvent(event, state);
|
|
@@ -32140,7 +32534,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32140
32534
|
arguments: tc.arguments,
|
|
32141
32535
|
callId: tc.id,
|
|
32142
32536
|
step,
|
|
32143
|
-
trace: {
|
|
32537
|
+
trace: { ...runTrace, callId: tc.id }
|
|
32144
32538
|
});
|
|
32145
32539
|
if (!decision.pass) {
|
|
32146
32540
|
return this.buildDeniedResult(tc, decision.reason, reports);
|
|
@@ -32160,7 +32554,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32160
32554
|
}
|
|
32161
32555
|
}
|
|
32162
32556
|
try {
|
|
32163
|
-
const baseCtx = { step, callId: tc.id, metrics, trace: {
|
|
32557
|
+
const baseCtx = { step, callId: tc.id, metrics, trace: { ...runTrace, callId: tc.id } };
|
|
32164
32558
|
const result = await executeWithTimeout(lookup.tool, tc, baseCtx, this._toolTimeout);
|
|
32165
32559
|
return await this.buildSuccessResult(tc, result, runId, step, metrics, reports, toolStart, runTrace, lookup.tool, baseCtx);
|
|
32166
32560
|
} catch (e) {
|
|
@@ -32218,7 +32612,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32218
32612
|
arguments: tc.arguments,
|
|
32219
32613
|
reason,
|
|
32220
32614
|
step,
|
|
32221
|
-
trace: {
|
|
32615
|
+
trace: { ...runTrace, callId: tc.id }
|
|
32222
32616
|
};
|
|
32223
32617
|
const pending = {
|
|
32224
32618
|
callId: tc.id,
|
|
@@ -32251,7 +32645,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32251
32645
|
return this.buildOverriddenResult(tc, decision.overrideResult, reports);
|
|
32252
32646
|
}
|
|
32253
32647
|
try {
|
|
32254
|
-
const baseCtx = { step, callId: tc.id, metrics, trace: {
|
|
32648
|
+
const baseCtx = { step, callId: tc.id, metrics, trace: { ...runTrace, callId: tc.id } };
|
|
32255
32649
|
const result = await executeWithTimeout(tool, tc, baseCtx, this._toolTimeout);
|
|
32256
32650
|
return await this.buildSuccessResult(tc, result, runId, step, metrics, reports, toolStart, runTrace, tool, baseCtx);
|
|
32257
32651
|
} catch (e) {
|
|
@@ -32347,7 +32741,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32347
32741
|
return own.length > 0 ? own : void 0;
|
|
32348
32742
|
}
|
|
32349
32743
|
// ─── Run helpers ────────────────────────────────────────────────────────
|
|
32350
|
-
async beginRun(input) {
|
|
32744
|
+
async beginRun(input, callerCtx) {
|
|
32351
32745
|
if (this._running) throw new Error("AgentLoop is already running");
|
|
32352
32746
|
this._running = true;
|
|
32353
32747
|
this._stopRequested = false;
|
|
@@ -32364,10 +32758,23 @@ var AgentLoop = class _AgentLoop {
|
|
|
32364
32758
|
const startedAt = Date.now();
|
|
32365
32759
|
const startPerf = performance.now();
|
|
32366
32760
|
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 = {
|
|
32761
|
+
const runTrace = {
|
|
32762
|
+
sessionId: callerCtx?.sessionId ?? this.id,
|
|
32763
|
+
requestId: callerCtx?.requestId ?? runId,
|
|
32764
|
+
// The caller's span travels WITH the ids, for the same reason they do: `agent.run`,
|
|
32765
|
+
// every `tool.call`, and any agent nested inside a tool reach the telemetry through
|
|
32766
|
+
// `runTrace` and nothing else. While this field was missing from it only the LLM
|
|
32767
|
+
// calls joined the app's trace — they are built from the caller's ctx directly —
|
|
32768
|
+
// and the run that made them sat in a second, unrelated one. Measured against a
|
|
32769
|
+
// live backend; the unit tests fed the hooks directly and never saw it.
|
|
32770
|
+
...callerCtx?.traceparent ? { traceparent: callerCtx.traceparent } : {}
|
|
32771
|
+
};
|
|
32368
32772
|
await this.hooks.emit("onRunStart", {
|
|
32369
32773
|
runId,
|
|
32370
32774
|
agentId: this.id,
|
|
32775
|
+
label: this.label,
|
|
32776
|
+
source: this.source,
|
|
32777
|
+
attributes: this.attributes,
|
|
32371
32778
|
userMessage: input,
|
|
32372
32779
|
model: this.client.model,
|
|
32373
32780
|
system: this._history.system,
|
|
@@ -32422,7 +32829,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32422
32829
|
if (g.kind !== "input") continue;
|
|
32423
32830
|
const decision = await g.check({
|
|
32424
32831
|
kind: "input",
|
|
32425
|
-
trace: {
|
|
32832
|
+
trace: { ...runTrace },
|
|
32426
32833
|
step,
|
|
32427
32834
|
messages,
|
|
32428
32835
|
system
|
|
@@ -32450,7 +32857,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
32450
32857
|
if (g.kind !== "output") continue;
|
|
32451
32858
|
const decision = await g.check({
|
|
32452
32859
|
kind: "output",
|
|
32453
|
-
trace: {
|
|
32860
|
+
trace: { ...runTrace },
|
|
32454
32861
|
step,
|
|
32455
32862
|
response
|
|
32456
32863
|
});
|
|
@@ -34615,6 +35022,7 @@ function createEngine(config = {}) {
|
|
|
34615
35022
|
const fetchStreamBound = (req, options) => network.fetchStream(req, options);
|
|
34616
35023
|
const connectBound = (req) => network.connect(req);
|
|
34617
35024
|
const cost = new CostCollector({ hooks, catalog });
|
|
35025
|
+
const telemetry = config.telemetry ? new TelemetryAdapter(hooks, config.telemetry) : null;
|
|
34618
35026
|
const handle = {
|
|
34619
35027
|
sessionId,
|
|
34620
35028
|
hooks,
|
|
@@ -34627,9 +35035,11 @@ function createEngine(config = {}) {
|
|
|
34627
35035
|
connect: connectBound,
|
|
34628
35036
|
catalog,
|
|
34629
35037
|
cost,
|
|
35038
|
+
telemetry,
|
|
34630
35039
|
apiKeys: config.apiKeys ?? {},
|
|
34631
35040
|
destroy() {
|
|
34632
35041
|
cost.destroy();
|
|
35042
|
+
telemetry?.destroy();
|
|
34633
35043
|
network.destroy();
|
|
34634
35044
|
}
|
|
34635
35045
|
};
|
|
@@ -38399,7 +38809,8 @@ async function complete(opts) {
|
|
|
38399
38809
|
serviceTier,
|
|
38400
38810
|
cache: opts.cache,
|
|
38401
38811
|
topK: opts.topK,
|
|
38402
|
-
seed: opts.seed
|
|
38812
|
+
seed: opts.seed,
|
|
38813
|
+
thinking: opts.thinking
|
|
38403
38814
|
});
|
|
38404
38815
|
} else {
|
|
38405
38816
|
res = await llm.complete(input, {
|
|
@@ -38413,7 +38824,8 @@ async function complete(opts) {
|
|
|
38413
38824
|
serviceTier,
|
|
38414
38825
|
cache: opts.cache,
|
|
38415
38826
|
topK: opts.topK,
|
|
38416
|
-
seed: opts.seed
|
|
38827
|
+
seed: opts.seed,
|
|
38828
|
+
thinking: opts.thinking
|
|
38417
38829
|
});
|
|
38418
38830
|
}
|
|
38419
38831
|
const result = {
|