@struct-ai/sdk 0.3.0 → 0.4.2
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/README.md +101 -16
- package/dist/commonjs/context.d.ts +45 -0
- package/dist/commonjs/context.js +78 -1
- package/dist/commonjs/core.js +184 -29
- package/dist/commonjs/events.d.ts +17 -6
- package/dist/commonjs/events.js +82 -59
- package/dist/commonjs/genai-content.d.ts +52 -0
- package/dist/commonjs/genai-content.js +143 -0
- package/dist/commonjs/instrument.d.ts +47 -0
- package/dist/commonjs/instrument.js +158 -0
- package/dist/commonjs/integrations/anthropic-content.js +18 -6
- package/dist/commonjs/integrations/anthropic.d.ts +8 -1
- package/dist/commonjs/integrations/anthropic.js +515 -104
- package/dist/commonjs/integrations/index.js +8 -0
- package/dist/commonjs/integrations/langchain-callback.d.ts +182 -27
- package/dist/commonjs/integrations/langchain-callback.js +754 -87
- package/dist/commonjs/integrations/langchain-content.js +1 -1
- package/dist/commonjs/integrations/langchain.d.ts +3 -0
- package/dist/commonjs/integrations/langchain.js +353 -7
- package/dist/commonjs/integrations/openai-content.d.ts +34 -0
- package/dist/commonjs/integrations/openai-content.js +375 -0
- package/dist/commonjs/integrations/openai.d.ts +39 -0
- package/dist/commonjs/integrations/openai.js +305 -0
- package/dist/commonjs/semconv.d.ts +12 -0
- package/dist/commonjs/semconv.js +13 -1
- package/dist/commonjs/truncation.d.ts +29 -0
- package/dist/commonjs/truncation.js +184 -10
- package/dist/commonjs/version.d.ts +2 -0
- package/dist/commonjs/version.js +6 -0
- package/dist/esm/context.d.ts +45 -0
- package/dist/esm/context.js +74 -1
- package/dist/esm/core.js +185 -30
- package/dist/esm/events.d.ts +17 -6
- package/dist/esm/events.js +82 -61
- package/dist/esm/genai-content.d.ts +52 -0
- package/dist/esm/genai-content.js +137 -0
- package/dist/esm/instrument.d.ts +47 -0
- package/dist/esm/instrument.js +155 -0
- package/dist/esm/integrations/anthropic-content.js +19 -7
- package/dist/esm/integrations/anthropic.d.ts +8 -1
- package/dist/esm/integrations/anthropic.js +514 -107
- package/dist/esm/integrations/index.js +8 -0
- package/dist/esm/integrations/langchain-callback.d.ts +182 -27
- package/dist/esm/integrations/langchain-callback.js +756 -89
- package/dist/esm/integrations/langchain-content.js +1 -1
- package/dist/esm/integrations/langchain.d.ts +3 -0
- package/dist/esm/integrations/langchain.js +352 -7
- package/dist/esm/integrations/openai-content.d.ts +34 -0
- package/dist/esm/integrations/openai-content.js +360 -0
- package/dist/esm/integrations/openai.d.ts +39 -0
- package/dist/esm/integrations/openai.js +296 -0
- package/dist/esm/semconv.d.ts +12 -0
- package/dist/esm/semconv.js +12 -0
- package/dist/esm/truncation.d.ts +29 -0
- package/dist/esm/truncation.js +182 -10
- package/dist/esm/version.d.ts +2 -0
- package/dist/esm/version.js +3 -0
- package/package.json +11 -3
package/dist/commonjs/core.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.StructSDK = exports.firstFailureLogged = exports.DEFAULT_ENDPOINT = void 0;
|
|
4
4
|
exports.safe = safe;
|
|
5
|
-
const node_crypto_1 = require("node:crypto");
|
|
6
5
|
const api_1 = require("@opentelemetry/api");
|
|
7
6
|
const exporter_logs_otlp_proto_1 = require("@opentelemetry/exporter-logs-otlp-proto");
|
|
8
7
|
const exporter_trace_otlp_proto_1 = require("@opentelemetry/exporter-trace-otlp-proto");
|
|
@@ -14,6 +13,7 @@ const context_js_1 = require("./context.js");
|
|
|
14
13
|
const index_js_1 = require("./integrations/index.js");
|
|
15
14
|
const semconv_js_1 = require("./semconv.js");
|
|
16
15
|
const truncation_js_1 = require("./truncation.js");
|
|
16
|
+
const version_js_1 = require("./version.js");
|
|
17
17
|
exports.DEFAULT_ENDPOINT = "https://ingest.struct.ai";
|
|
18
18
|
const LOG_ENABLED = process.env.STRUCT_SDK_LOG === "1";
|
|
19
19
|
const defaultInternalLogger = {
|
|
@@ -88,12 +88,22 @@ function safe(fn, site, logger) {
|
|
|
88
88
|
fn();
|
|
89
89
|
}
|
|
90
90
|
catch (err) {
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
// The diagnostic log MUST NOT itself throw into the host: the default
|
|
92
|
+
// logger reaches `console.warn`, which a host may have replaced with a
|
|
93
|
+
// throwing implementation, and callers pass custom loggers. A throw here
|
|
94
|
+
// would escape `safe()` and defeat its whole purpose (e.g. block the host
|
|
95
|
+
// call when span creation fails). Guard the logging too.
|
|
96
|
+
try {
|
|
97
|
+
if (exports.firstFailureLogged.has(site)) {
|
|
98
|
+
logger.debug(`Struct SDK suppressed exception at ${site}`, err);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
exports.firstFailureLogged.add(site);
|
|
102
|
+
logger.warn(`Struct SDK suppressed exception at ${site}`, err);
|
|
103
|
+
}
|
|
93
104
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
logger.warn(`Struct SDK suppressed exception at ${site}`, err);
|
|
105
|
+
catch {
|
|
106
|
+
/* diagnostic logging failed — never propagate into the host path */
|
|
97
107
|
}
|
|
98
108
|
}
|
|
99
109
|
}
|
|
@@ -232,13 +242,13 @@ class StructSDK {
|
|
|
232
242
|
if (!this._tracerProvider) {
|
|
233
243
|
throw new Error("Call struct.init() before using the SDK");
|
|
234
244
|
}
|
|
235
|
-
return this._tracerProvider.getTracer(name);
|
|
245
|
+
return this._tracerProvider.getTracer(name, version_js_1.SDK_VERSION);
|
|
236
246
|
}
|
|
237
247
|
getLogger(name = "struct-sdk") {
|
|
238
248
|
if (!this._loggerProvider) {
|
|
239
249
|
throw new Error("Call struct.init() before using the SDK");
|
|
240
250
|
}
|
|
241
|
-
return this._loggerProvider.getLogger(name);
|
|
251
|
+
return this._loggerProvider.getLogger(name, version_js_1.SDK_VERSION);
|
|
242
252
|
}
|
|
243
253
|
getInternalLogger() {
|
|
244
254
|
return this._internalLogger;
|
|
@@ -306,8 +316,7 @@ class StructSDK {
|
|
|
306
316
|
return await fn();
|
|
307
317
|
}
|
|
308
318
|
const agentName = options.name;
|
|
309
|
-
const
|
|
310
|
-
const parentSessionId = (0, context_js_1.getSessionId)();
|
|
319
|
+
const explicitSessionId = options.sessionId;
|
|
311
320
|
const tracer = this.getTracer("struct-sdk");
|
|
312
321
|
// Parent the new agent span on the nearest enclosing agent span (if
|
|
313
322
|
// any) so nested `struct.agent()` / `struct.tool()` calls build a
|
|
@@ -316,16 +325,68 @@ class StructSDK {
|
|
|
316
325
|
// nothing to hang off. We do NOT rely on the global OTel context
|
|
317
326
|
// manager being installed — the SDK manages its own parent chain via
|
|
318
327
|
// StructContext ALS to stay neutral in multi-tenant OTel setups.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
328
|
+
//
|
|
329
|
+
// Captured BEFORE we resolve/overwrite anything below — mirrors
|
|
330
|
+
// python's `enclosing_session_id` / `enclosing_agent_span` capture at
|
|
331
|
+
// core.py:625-626. Used to (1) resolve this agent's own session id via
|
|
332
|
+
// ambient inheritance, (2) detect break-out, and (3) decide whether to
|
|
333
|
+
// stamp `struct.agent.parent_session_id`.
|
|
334
|
+
const enclosingSessionId = (0, context_js_1.getSessionId)();
|
|
335
|
+
const enclosingAgentSpan = (0, context_js_1.getAgentSpan)();
|
|
336
|
+
// ── Resolve sessionId (REVISION R1 grouping model) ──────────────────
|
|
337
|
+
// Resolution order: explicit caller arg > ambient (enclosing agent) >
|
|
338
|
+
// session-less. We never fabricate a uuid — a caller that omits
|
|
339
|
+
// sessionId with no enclosing agent gets a SESSION-LESS run (no
|
|
340
|
+
// `gen_ai.conversation.id` attribute at all). Ports core.py:628-644
|
|
341
|
+
// exactly (`self._session_id = ""` there is `undefined` here).
|
|
342
|
+
const sessionId = explicitSessionId !== undefined
|
|
343
|
+
? explicitSessionId
|
|
344
|
+
: enclosingSessionId !== undefined
|
|
345
|
+
? enclosingSessionId
|
|
346
|
+
: undefined;
|
|
347
|
+
// ── Break-out detection (spawned-by Link) ───────────────────────────
|
|
348
|
+
// Caller supplied an EXPLICIT session id that differs from the
|
|
349
|
+
// enclosing agent's session AND there IS an enclosing Struct agent
|
|
350
|
+
// span in scope. This agent starts a fresh ROOT trace (no OTel
|
|
351
|
+
// parent) and carries a causal Link back to the enclosing agent's
|
|
352
|
+
// span context. Ports struct_sdk.core._AgentContext._start_span's
|
|
353
|
+
// `break_out` branch (struct-sdk-python/src/struct_sdk/core.py:651-656)
|
|
354
|
+
// exactly.
|
|
355
|
+
const breakOut = explicitSessionId !== undefined &&
|
|
356
|
+
enclosingSessionId !== undefined &&
|
|
357
|
+
explicitSessionId !== enclosingSessionId &&
|
|
358
|
+
enclosingAgentSpan !== undefined;
|
|
359
|
+
// ── Parentage: PURE NEST (Option D) ─────────────────────────────────
|
|
360
|
+
// A non-break-out agent nests under whatever OTel span is active — the
|
|
361
|
+
// host's ambient context — like every peer LLM/agent SDK. We do NOT
|
|
362
|
+
// second-guess the active span or unilaterally re-root: the old
|
|
363
|
+
// `foreignRoot` heuristic detached from ANY active span and so ripped
|
|
364
|
+
// agents out of legitimate host request traces. Cross-conversation
|
|
365
|
+
// grouping is carried by gen_ai.conversation.id, never by trace
|
|
366
|
+
// parentage. A runtime that propagates a stale/unwanted span across a
|
|
367
|
+
// boundary (e.g. our own persistent_agent Temporal workflow span) clears
|
|
368
|
+
// it at the SOURCE, never here. (A provenance-gated detach — re-root only
|
|
369
|
+
// a leaked Struct-OWNED span — is a documented, deferred follow-up.)
|
|
370
|
+
let startContext;
|
|
371
|
+
let links;
|
|
372
|
+
if (breakOut) {
|
|
373
|
+
// Explicit different session under another Struct agent: fresh ROOT
|
|
374
|
+
// trace + a spawned-by Link. The one deliberate, opt-in re-root.
|
|
375
|
+
// enclosingAgentSpan is guaranteed defined by the breakOut guard above.
|
|
376
|
+
links = [{ context: enclosingAgentSpan.spanContext() }];
|
|
377
|
+
startContext = api_1.ROOT_CONTEXT;
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
startContext = enclosingAgentSpan
|
|
381
|
+
? api_1.trace.setSpan(api_1.context.active(), enclosingAgentSpan)
|
|
382
|
+
: api_1.context.active();
|
|
383
|
+
}
|
|
323
384
|
// Span creation itself can fail (custom tracer, broken context). If it
|
|
324
385
|
// does, fall through to running `fn` directly so the user's call
|
|
325
386
|
// always runs uninstrumented rather than blowing up on telemetry.
|
|
326
387
|
let span;
|
|
327
388
|
safe(() => {
|
|
328
|
-
span = tracer.startSpan(`invoke_agent ${agentName}`, { kind: api_1.SpanKind.INTERNAL },
|
|
389
|
+
span = tracer.startSpan(`invoke_agent ${agentName}`, { kind: api_1.SpanKind.INTERNAL, links }, startContext);
|
|
329
390
|
}, "agent.start_span", this._internalLogger);
|
|
330
391
|
if (span === undefined) {
|
|
331
392
|
return await fn();
|
|
@@ -333,7 +394,10 @@ class StructSDK {
|
|
|
333
394
|
const startedSpan = span;
|
|
334
395
|
safe(() => {
|
|
335
396
|
startedSpan.setAttribute(semconv_js_1.GEN_AI.OPERATION_NAME, "invoke_agent");
|
|
336
|
-
|
|
397
|
+
// gen_ai.provider.name is NOT set here: this layer is
|
|
398
|
+
// provider-agnostic, and a framework name ("struct") isn't a
|
|
399
|
+
// provider. The first child inference call stamps the real one via
|
|
400
|
+
// propagateProviderToParent (write-once, best knowledge).
|
|
337
401
|
startedSpan.setAttribute(semconv_js_1.GEN_AI.AGENT_NAME, agentName);
|
|
338
402
|
// gen_ai.agent.id is the stable identifier of the agent
|
|
339
403
|
// DEFINITION. Only set it when the caller provides one — we do
|
|
@@ -346,17 +410,24 @@ class StructSDK {
|
|
|
346
410
|
startedSpan.setAttribute(semconv_js_1.GEN_AI.AGENT_VERSION, options.version);
|
|
347
411
|
}
|
|
348
412
|
// gen_ai.conversation.id is the spec-blessed name for
|
|
349
|
-
// session/thread id.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
413
|
+
// session/thread id. Omitted when session-less (never fabricated).
|
|
414
|
+
// Ports core.py:723-725 (`if self._session_id:`) exactly.
|
|
415
|
+
if (sessionId) {
|
|
416
|
+
startedSpan.setAttribute(semconv_js_1.GEN_AI.CONVERSATION_ID, sessionId);
|
|
417
|
+
}
|
|
418
|
+
// ``struct.agent.parent_session_id`` is a SPAWNED-BY marker — set it
|
|
419
|
+
// ONLY when this agent has an enclosing session that DIFFERS from
|
|
420
|
+
// its own resolved session id. An inline nested agent (same session
|
|
421
|
+
// as the enclosing agent, whether by explicit same id or ambient
|
|
422
|
+
// inheritance) already has its parent relationship encoded by the
|
|
423
|
+
// OTel span tree (ParentSpanId) — stamping
|
|
424
|
+
// parent_session_id === own sessionId would be self-referential
|
|
425
|
+
// noise. Structure comes from the tree/Link, not this attr
|
|
426
|
+
// (Link-canonical decision). Ports core.py:736-745 exactly (both
|
|
427
|
+
// the break-out and the "legacy" differing-id-no-span branches
|
|
428
|
+
// collapse to this one condition).
|
|
429
|
+
if (enclosingSessionId !== undefined && enclosingSessionId !== sessionId) {
|
|
430
|
+
startedSpan.setAttribute(semconv_js_1.STRUCT.AGENT_PARENT_SESSION_ID, enclosingSessionId);
|
|
360
431
|
}
|
|
361
432
|
if (options.metadata) {
|
|
362
433
|
for (const [key, value] of Object.entries(options.metadata)) {
|
|
@@ -369,6 +440,7 @@ class StructSDK {
|
|
|
369
440
|
conversationId: sessionId,
|
|
370
441
|
agentSpan: startedSpan,
|
|
371
442
|
pendingToolCalls: {},
|
|
443
|
+
manualAgentSpan: startedSpan,
|
|
372
444
|
}, async () => {
|
|
373
445
|
const activeCtx = api_1.trace.setSpan(api_1.context.active(), startedSpan);
|
|
374
446
|
try {
|
|
@@ -420,7 +492,8 @@ class StructSDK {
|
|
|
420
492
|
const startedSpan = span;
|
|
421
493
|
safe(() => {
|
|
422
494
|
startedSpan.setAttribute(semconv_js_1.GEN_AI.OPERATION_NAME, "execute_tool");
|
|
423
|
-
|
|
495
|
+
// No gen_ai.provider.name: the spec's execute_tool span does not
|
|
496
|
+
// define that attribute.
|
|
424
497
|
startedSpan.setAttribute(semconv_js_1.GEN_AI.TOOL_NAME, toolName);
|
|
425
498
|
if (toolCallId) {
|
|
426
499
|
startedSpan.setAttribute(semconv_js_1.GEN_AI.TOOL_CALL_ID, toolCallId);
|
|
@@ -436,7 +509,28 @@ class StructSDK {
|
|
|
436
509
|
if (this.captureContent && result !== undefined && result !== null) {
|
|
437
510
|
safe(() => startedSpan.setAttribute(semconv_js_1.GEN_AI.TOOL_CALL_RESULT, (0, truncation_js_1.safeJsonStringify)(result).slice(0, 8192)), "tool.set_result_attr", this._internalLogger);
|
|
438
511
|
}
|
|
439
|
-
|
|
512
|
+
if (toolResultSignalsError(result)) {
|
|
513
|
+
safe(() => {
|
|
514
|
+
startedSpan.setAttribute(semconv_js_1.ERROR_TYPE, "tool_error");
|
|
515
|
+
// The status message is content placed on a SPAN — it follows
|
|
516
|
+
// the span-content routing gate (emitSpanContent), not merely
|
|
517
|
+
// "any capture on": under EventOnly (the default) content
|
|
518
|
+
// routes to log events, so span text gets the fixed literal.
|
|
519
|
+
// Deliberate boundary: gen_ai.tool.call.result above stays on
|
|
520
|
+
// captureContent — tool spans have no log-event equivalent for
|
|
521
|
+
// results, so the attribute is the sanctioned tool-content
|
|
522
|
+
// channel in every non-None mode. Mirrors python _note_result.
|
|
523
|
+
startedSpan.setStatus({
|
|
524
|
+
code: api_1.SpanStatusCode.ERROR,
|
|
525
|
+
message: this.emitSpanContent
|
|
526
|
+
? toolErrorStatusMessage(result)
|
|
527
|
+
: "tool returned is_error=true",
|
|
528
|
+
});
|
|
529
|
+
}, "tool.set_tool_error_status", this._internalLogger);
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
safe(() => startedSpan.setStatus({ code: api_1.SpanStatusCode.OK }), "tool.set_ok_status", this._internalLogger);
|
|
533
|
+
}
|
|
440
534
|
return result;
|
|
441
535
|
}
|
|
442
536
|
catch (err) {
|
|
@@ -458,4 +552,65 @@ function recordError(span, err) {
|
|
|
458
552
|
span.recordException(err);
|
|
459
553
|
}
|
|
460
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* Read one property from untrusted host data, isolating the read in its
|
|
557
|
+
* own try — a hostile getter/Proxy trap on ONE key must never throw into
|
|
558
|
+
* tool()'s try block (rejecting the host's successful call) nor mask a
|
|
559
|
+
* readable value on a SIBLING key (callers probe each supported alias
|
|
560
|
+
* independently). Mirrors python `_safe_probe` — keep in lockstep.
|
|
561
|
+
*/
|
|
562
|
+
function safeProbe(obj, key) {
|
|
563
|
+
if (typeof obj !== "object" || obj === null)
|
|
564
|
+
return undefined;
|
|
565
|
+
try {
|
|
566
|
+
return obj[key];
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Whether a tool's RETURN VALUE signals in-band failure (MCP
|
|
574
|
+
* CallToolResult.isError / Anthropic tool_result.is_error). Strictly
|
|
575
|
+
* boolean `true` on the top-level object — truthy strings/numbers and
|
|
576
|
+
* nested flags do not trigger (host data is untrusted; be conservative).
|
|
577
|
+
* Each alias is probed independently so a hostile getter on one cannot
|
|
578
|
+
* mask the other. Mirrors python `_tool_result_signals_error`.
|
|
579
|
+
*/
|
|
580
|
+
function toolResultSignalsError(result) {
|
|
581
|
+
return (safeProbe(result, "isError") === true ||
|
|
582
|
+
safeProbe(result, "is_error") === true);
|
|
583
|
+
}
|
|
584
|
+
/** Short status message from an error result's content, else a fixed one.
|
|
585
|
+
*
|
|
586
|
+
* TOTAL FUNCTION: never throws and does bounded work. It runs inside the
|
|
587
|
+
* safe() closure that also sets span status — a hostile Proxy whose
|
|
588
|
+
* `length`/index traps throw must not abort that closure (which would
|
|
589
|
+
* leave the span UNSET instead of ERROR). Two layers: per-key safeProbe
|
|
590
|
+
* isolation (one hostile item cannot mask a later readable one) INSIDE a
|
|
591
|
+
* whole-body catch (collection machinery itself is untrusted), index
|
|
592
|
+
* loop instead of for..of (iterator protocol is trappable), traversal
|
|
593
|
+
* capped at 20 items. Mirrors python `_tool_error_status_message`. */
|
|
594
|
+
function toolErrorStatusMessage(result) {
|
|
595
|
+
try {
|
|
596
|
+
const content = safeProbe(result, "content");
|
|
597
|
+
if (typeof content === "string" && content)
|
|
598
|
+
return content.slice(0, 256);
|
|
599
|
+
if (Array.isArray(content)) {
|
|
600
|
+
const len = Math.min(content.length, 20);
|
|
601
|
+
for (let i = 0; i < len; i++) {
|
|
602
|
+
const item = safeProbe(content, String(i));
|
|
603
|
+
const text = safeProbe(item, "text");
|
|
604
|
+
// Truthiness (not just typeof) matches the python twin: empty-string
|
|
605
|
+
// text items are skipped, falling through to the fixed message.
|
|
606
|
+
if (typeof text === "string" && text)
|
|
607
|
+
return text.slice(0, 256);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
// Hostile collection — fall through to the fixed message.
|
|
613
|
+
}
|
|
614
|
+
return "tool returned is_error=true";
|
|
615
|
+
}
|
|
461
616
|
//# sourceMappingURL=core.js.map
|
|
@@ -1,12 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Logger } from "@opentelemetry/api-logs";
|
|
2
|
+
import type { Span } from "@opentelemetry/api";
|
|
2
3
|
/**
|
|
3
4
|
* Emit per-message log events for an Anthropic messages.create() call.
|
|
4
|
-
*
|
|
5
|
+
* Delegates the LogRecord wiring to the shared genai-content emitters; this
|
|
6
|
+
* function is only the Anthropic message → parts mapping + ordering.
|
|
5
7
|
*/
|
|
6
|
-
export declare function emitAnthropicMessageEvents(logger: Logger, messages: unknown, system: unknown): void;
|
|
8
|
+
export declare function emitAnthropicMessageEvents(logger: Logger, messages: unknown, system: unknown, span?: Span, provider?: string): void;
|
|
9
|
+
/** Emit a gen_ai.choice LogRecord for an Anthropic response. */
|
|
10
|
+
export declare function emitAnthropicChoiceEvent(logger: Logger, contentBlocks: unknown, stopReason: string | null | undefined, span?: Span, provider?: string): void;
|
|
7
11
|
/**
|
|
8
|
-
* Emit
|
|
9
|
-
*
|
|
12
|
+
* Emit per-message log events for an OpenAI responses.create() call.
|
|
13
|
+
* `instructions` (the Responses system prompt) is emitted FIRST at index 0.
|
|
14
|
+
* Delegates LogRecord wiring to the shared emitters; only the Responses
|
|
15
|
+
* item → event mapping + ordering lives here.
|
|
10
16
|
*/
|
|
11
|
-
export declare function
|
|
17
|
+
export declare function emitOpenAIInputMessageEvents(logger: Logger, input: unknown, instructions: unknown, span?: Span, provider?: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* Emit the gen_ai.choice LogRecord from an OpenAI response.output.
|
|
20
|
+
* `finishReason` is mapped inside this emitter using mapChoiceFinishReason.
|
|
21
|
+
*/
|
|
22
|
+
export declare function emitOpenAIChoiceEvent(logger: Logger, output: unknown, finishReason: string | undefined, span?: Span, provider?: string): void;
|
|
12
23
|
//# sourceMappingURL=events.d.ts.map
|
package/dist/commonjs/events.js
CHANGED
|
@@ -2,40 +2,18 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.emitAnthropicMessageEvents = emitAnthropicMessageEvents;
|
|
4
4
|
exports.emitAnthropicChoiceEvent = emitAnthropicChoiceEvent;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
5
|
+
exports.emitOpenAIInputMessageEvents = emitOpenAIInputMessageEvents;
|
|
6
|
+
exports.emitOpenAIChoiceEvent = emitOpenAIChoiceEvent;
|
|
7
|
+
const genai_content_js_1 = require("./genai-content.js");
|
|
8
8
|
const semconv_js_1 = require("./semconv.js");
|
|
9
|
-
const truncation_js_1 = require("./truncation.js");
|
|
10
9
|
const anthropic_content_js_1 = require("./integrations/anthropic-content.js");
|
|
11
|
-
|
|
12
|
-
* Emit a LogRecord with the active span context linked.
|
|
13
|
-
*
|
|
14
|
-
* Follows the OTel logs data model convention:
|
|
15
|
-
* - `body` (log record body) = event tag string (human-readable signal)
|
|
16
|
-
* - `attributes.body` (log record attribute) = JSON-serialised payload
|
|
17
|
-
*/
|
|
18
|
-
function emitLogRecord({ logger, eventName, payload, extraAttrs = {}, }) {
|
|
19
|
-
const sessionId = (0, context_js_1.getSessionId)();
|
|
20
|
-
const attributes = {
|
|
21
|
-
[semconv_js_1.EVENT_NAME]: eventName,
|
|
22
|
-
body: payload,
|
|
23
|
-
...extraAttrs,
|
|
24
|
-
};
|
|
25
|
-
if (sessionId)
|
|
26
|
-
attributes[semconv_js_1.GEN_AI.CONVERSATION_ID] = sessionId;
|
|
27
|
-
logger.emit({
|
|
28
|
-
body: eventName,
|
|
29
|
-
severityNumber: api_logs_1.SeverityNumber.INFO,
|
|
30
|
-
attributes,
|
|
31
|
-
context: api_1.context.active(),
|
|
32
|
-
});
|
|
33
|
-
}
|
|
10
|
+
const openai_content_js_1 = require("./integrations/openai-content.js");
|
|
34
11
|
/**
|
|
35
12
|
* Emit per-message log events for an Anthropic messages.create() call.
|
|
36
|
-
*
|
|
13
|
+
* Delegates the LogRecord wiring to the shared genai-content emitters; this
|
|
14
|
+
* function is only the Anthropic message → parts mapping + ordering.
|
|
37
15
|
*/
|
|
38
|
-
function emitAnthropicMessageEvents(logger, messages, system) {
|
|
16
|
+
function emitAnthropicMessageEvents(logger, messages, system, span, provider = "anthropic") {
|
|
39
17
|
if (!Array.isArray(messages))
|
|
40
18
|
return;
|
|
41
19
|
let msgIndex = 0;
|
|
@@ -45,17 +23,14 @@ function emitAnthropicMessageEvents(logger, messages, system) {
|
|
|
45
23
|
: Array.isArray(system)
|
|
46
24
|
? (0, anthropic_content_js_1.contentToParts)(system)
|
|
47
25
|
: [{ type: "text", content: String(system) }];
|
|
48
|
-
|
|
26
|
+
(0, genai_content_js_1.emitMessageEvent)({
|
|
49
27
|
logger,
|
|
28
|
+
role: "system",
|
|
29
|
+
parts,
|
|
50
30
|
eventName: semconv_js_1.EVENT_NAMES.SYSTEM_MESSAGE,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}),
|
|
55
|
-
extraAttrs: {
|
|
56
|
-
[semconv_js_1.GEN_AI.SYSTEM]: "anthropic",
|
|
57
|
-
[semconv_js_1.GEN_AI.MESSAGE_INDEX]: msgIndex,
|
|
58
|
-
},
|
|
31
|
+
provider,
|
|
32
|
+
messageIndex: msgIndex,
|
|
33
|
+
span,
|
|
59
34
|
});
|
|
60
35
|
msgIndex++;
|
|
61
36
|
}
|
|
@@ -66,38 +41,86 @@ function emitAnthropicMessageEvents(logger, messages, system) {
|
|
|
66
41
|
const role = typeof m.role === "string" ? m.role : "user";
|
|
67
42
|
const parts = (0, anthropic_content_js_1.contentToParts)(m.content);
|
|
68
43
|
const eventName = semconv_js_1.ROLE_TO_EVENT_NAME[role] ?? `gen_ai.${role}.message`;
|
|
69
|
-
|
|
44
|
+
(0, genai_content_js_1.emitMessageEvent)({
|
|
70
45
|
logger,
|
|
46
|
+
role,
|
|
47
|
+
parts,
|
|
71
48
|
eventName,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
[semconv_js_1.GEN_AI.MESSAGE_INDEX]: msgIndex,
|
|
76
|
-
},
|
|
49
|
+
provider,
|
|
50
|
+
messageIndex: msgIndex,
|
|
51
|
+
span,
|
|
77
52
|
});
|
|
78
53
|
msgIndex++;
|
|
79
54
|
}
|
|
80
55
|
}
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
* Port of _emit_choice_event from anthropic.py.
|
|
84
|
-
*/
|
|
85
|
-
function emitAnthropicChoiceEvent(logger, contentBlocks, stopReason) {
|
|
56
|
+
/** Emit a gen_ai.choice LogRecord for an Anthropic response. */
|
|
57
|
+
function emitAnthropicChoiceEvent(logger, contentBlocks, stopReason, span, provider = "anthropic") {
|
|
86
58
|
const parts = (0, anthropic_content_js_1.contentToParts)(contentBlocks);
|
|
87
59
|
const mappedReason = (stopReason && (semconv_js_1.ANTHROPIC_FINISH_REASON_MAP[stopReason] ?? stopReason)) ||
|
|
88
60
|
"stop";
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
61
|
+
(0, genai_content_js_1.emitChoiceEvent)({
|
|
62
|
+
logger,
|
|
63
|
+
parts,
|
|
64
|
+
finishReason: mappedReason,
|
|
65
|
+
provider,
|
|
66
|
+
span,
|
|
93
67
|
});
|
|
94
|
-
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Emit per-message log events for an OpenAI responses.create() call.
|
|
71
|
+
* `instructions` (the Responses system prompt) is emitted FIRST at index 0.
|
|
72
|
+
* Delegates LogRecord wiring to the shared emitters; only the Responses
|
|
73
|
+
* item → event mapping + ordering lives here.
|
|
74
|
+
*/
|
|
75
|
+
function emitOpenAIInputMessageEvents(logger, input, instructions, span, provider = "openai") {
|
|
76
|
+
let msgIndex = 0;
|
|
77
|
+
if (instructions) {
|
|
78
|
+
const parts = typeof instructions === "string"
|
|
79
|
+
? [{ type: "text", content: instructions }]
|
|
80
|
+
: [{ type: "text", content: String(instructions) }];
|
|
81
|
+
(0, genai_content_js_1.emitMessageEvent)({
|
|
82
|
+
logger,
|
|
83
|
+
role: "system",
|
|
84
|
+
parts,
|
|
85
|
+
eventName: semconv_js_1.EVENT_NAMES.SYSTEM_MESSAGE,
|
|
86
|
+
provider,
|
|
87
|
+
messageIndex: msgIndex,
|
|
88
|
+
span,
|
|
89
|
+
});
|
|
90
|
+
msgIndex++;
|
|
91
|
+
}
|
|
92
|
+
for (const item of (0, openai_content_js_1.normalizeInput)(input)) {
|
|
93
|
+
const mapped = (0, openai_content_js_1.inputItemToEvent)(item);
|
|
94
|
+
if (!mapped)
|
|
95
|
+
continue;
|
|
96
|
+
const [eventName, role, parts] = mapped;
|
|
97
|
+
(0, genai_content_js_1.emitMessageEvent)({
|
|
98
|
+
logger,
|
|
99
|
+
role,
|
|
100
|
+
parts,
|
|
101
|
+
eventName,
|
|
102
|
+
provider,
|
|
103
|
+
messageIndex: msgIndex,
|
|
104
|
+
span,
|
|
105
|
+
});
|
|
106
|
+
msgIndex++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Emit the gen_ai.choice LogRecord from an OpenAI response.output.
|
|
111
|
+
* `finishReason` is mapped inside this emitter using mapChoiceFinishReason.
|
|
112
|
+
*/
|
|
113
|
+
function emitOpenAIChoiceEvent(logger, output, finishReason, span, provider = "openai") {
|
|
114
|
+
const parts = [];
|
|
115
|
+
for (const item of Array.isArray(output) ? output : []) {
|
|
116
|
+
parts.push(...(0, openai_content_js_1.outputItemToChoiceParts)(item));
|
|
117
|
+
}
|
|
118
|
+
(0, genai_content_js_1.emitChoiceEvent)({
|
|
95
119
|
logger,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
},
|
|
120
|
+
parts,
|
|
121
|
+
finishReason: (0, openai_content_js_1.mapChoiceFinishReason)(finishReason),
|
|
122
|
+
provider,
|
|
123
|
+
span,
|
|
101
124
|
});
|
|
102
125
|
}
|
|
103
126
|
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type Span } from "@opentelemetry/api";
|
|
2
|
+
import { type Logger } from "@opentelemetry/api-logs";
|
|
3
|
+
/** Part in the GenAI spec format (provider-agnostic, already mapped). */
|
|
4
|
+
export type Part = Record<string, unknown>;
|
|
5
|
+
/** Emit ONE per-message LogRecord from already-built spec parts. */
|
|
6
|
+
export declare function emitMessageEvent(opts: {
|
|
7
|
+
logger: Logger;
|
|
8
|
+
role: string;
|
|
9
|
+
parts: Part[];
|
|
10
|
+
eventName: string;
|
|
11
|
+
provider: string;
|
|
12
|
+
messageIndex: number;
|
|
13
|
+
span?: Span;
|
|
14
|
+
}): void;
|
|
15
|
+
/**
|
|
16
|
+
* Emit the `gen_ai.choice` LogRecord. `finishReason` is already spec-mapped by
|
|
17
|
+
* the caller (mapping is provider-specific). Omits `gen_ai.message.index`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function emitChoiceEvent(opts: {
|
|
20
|
+
logger: Logger;
|
|
21
|
+
parts: Part[];
|
|
22
|
+
finishReason: string;
|
|
23
|
+
provider: string;
|
|
24
|
+
span?: Span;
|
|
25
|
+
}): void;
|
|
26
|
+
/**
|
|
27
|
+
* Stamp the last user message on the parent invoke_agent span (write-once).
|
|
28
|
+
* Provider-agnostic: the caller extracts the last user message's spec parts
|
|
29
|
+
* (Anthropic message blocks vs Responses items differ); this only stamps them.
|
|
30
|
+
*/
|
|
31
|
+
export declare function propagateUserPromptToParent(lastUserParts: Part[] | undefined): void;
|
|
32
|
+
export type ProviderClassNameRule = readonly [ReadonlySet<string>, string];
|
|
33
|
+
export type ProviderHostRule = readonly [(host: string) => boolean, string];
|
|
34
|
+
/**
|
|
35
|
+
* Best-knowledge `gen_ai.provider.name` from a bound resource. TS twin of
|
|
36
|
+
* python `_genai_content.detect_provider_from_resource`.
|
|
37
|
+
*
|
|
38
|
+
* The platform client flavors (@anthropic-ai/bedrock-sdk, /vertex-sdk,
|
|
39
|
+
* AzureOpenAI) reuse the same resource prototypes as the first-party clients,
|
|
40
|
+
* so the platform is read at call time from the resource's owning client:
|
|
41
|
+
* EXACT constructor names up the prototype chain first (exact, not substring
|
|
42
|
+
* — a class named `NotAzureOpenAI` must not match; subclasses match via their
|
|
43
|
+
* inherited base's name), then per-platform `baseURL` host predicates matching
|
|
44
|
+
* only official endpoint shapes.
|
|
45
|
+
*
|
|
46
|
+
* Takes the RESOURCE: the `_client` property access is a host-boundary read (a
|
|
47
|
+
* proxied resource can throw from its getter), so it happens inside this
|
|
48
|
+
* function's guard. Falls back whenever routing is not positively detectable;
|
|
49
|
+
* never throws.
|
|
50
|
+
*/
|
|
51
|
+
export declare function detectProviderFromResource(resource: unknown, classNameRules: readonly ProviderClassNameRule[], hostRules: readonly ProviderHostRule[], fallback: string): string;
|
|
52
|
+
//# sourceMappingURL=genai-content.d.ts.map
|