@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.
Files changed (58) hide show
  1. package/README.md +101 -16
  2. package/dist/commonjs/context.d.ts +45 -0
  3. package/dist/commonjs/context.js +78 -1
  4. package/dist/commonjs/core.js +184 -29
  5. package/dist/commonjs/events.d.ts +17 -6
  6. package/dist/commonjs/events.js +82 -59
  7. package/dist/commonjs/genai-content.d.ts +52 -0
  8. package/dist/commonjs/genai-content.js +143 -0
  9. package/dist/commonjs/instrument.d.ts +47 -0
  10. package/dist/commonjs/instrument.js +158 -0
  11. package/dist/commonjs/integrations/anthropic-content.js +18 -6
  12. package/dist/commonjs/integrations/anthropic.d.ts +8 -1
  13. package/dist/commonjs/integrations/anthropic.js +515 -104
  14. package/dist/commonjs/integrations/index.js +8 -0
  15. package/dist/commonjs/integrations/langchain-callback.d.ts +182 -27
  16. package/dist/commonjs/integrations/langchain-callback.js +754 -87
  17. package/dist/commonjs/integrations/langchain-content.js +1 -1
  18. package/dist/commonjs/integrations/langchain.d.ts +3 -0
  19. package/dist/commonjs/integrations/langchain.js +353 -7
  20. package/dist/commonjs/integrations/openai-content.d.ts +34 -0
  21. package/dist/commonjs/integrations/openai-content.js +375 -0
  22. package/dist/commonjs/integrations/openai.d.ts +39 -0
  23. package/dist/commonjs/integrations/openai.js +305 -0
  24. package/dist/commonjs/semconv.d.ts +12 -0
  25. package/dist/commonjs/semconv.js +13 -1
  26. package/dist/commonjs/truncation.d.ts +29 -0
  27. package/dist/commonjs/truncation.js +184 -10
  28. package/dist/commonjs/version.d.ts +2 -0
  29. package/dist/commonjs/version.js +6 -0
  30. package/dist/esm/context.d.ts +45 -0
  31. package/dist/esm/context.js +74 -1
  32. package/dist/esm/core.js +185 -30
  33. package/dist/esm/events.d.ts +17 -6
  34. package/dist/esm/events.js +82 -61
  35. package/dist/esm/genai-content.d.ts +52 -0
  36. package/dist/esm/genai-content.js +137 -0
  37. package/dist/esm/instrument.d.ts +47 -0
  38. package/dist/esm/instrument.js +155 -0
  39. package/dist/esm/integrations/anthropic-content.js +19 -7
  40. package/dist/esm/integrations/anthropic.d.ts +8 -1
  41. package/dist/esm/integrations/anthropic.js +514 -107
  42. package/dist/esm/integrations/index.js +8 -0
  43. package/dist/esm/integrations/langchain-callback.d.ts +182 -27
  44. package/dist/esm/integrations/langchain-callback.js +756 -89
  45. package/dist/esm/integrations/langchain-content.js +1 -1
  46. package/dist/esm/integrations/langchain.d.ts +3 -0
  47. package/dist/esm/integrations/langchain.js +352 -7
  48. package/dist/esm/integrations/openai-content.d.ts +34 -0
  49. package/dist/esm/integrations/openai-content.js +360 -0
  50. package/dist/esm/integrations/openai.d.ts +39 -0
  51. package/dist/esm/integrations/openai.js +296 -0
  52. package/dist/esm/semconv.d.ts +12 -0
  53. package/dist/esm/semconv.js +12 -0
  54. package/dist/esm/truncation.d.ts +29 -0
  55. package/dist/esm/truncation.js +182 -10
  56. package/dist/esm/version.d.ts +2 -0
  57. package/dist/esm/version.js +3 -0
  58. package/package.json +11 -3
@@ -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
- if (exports.firstFailureLogged.has(site)) {
92
- logger.debug(`Struct SDK suppressed exception at ${site}`, err);
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
- else {
95
- exports.firstFailureLogged.add(site);
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 sessionId = options.sessionId ?? (0, node_crypto_1.randomUUID)();
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
- const parentSpan = (0, context_js_1.getAgentSpan)();
320
- const parentCtx = parentSpan
321
- ? api_1.trace.setSpan(api_1.context.active(), parentSpan)
322
- : api_1.context.active();
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 }, parentCtx);
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
- startedSpan.setAttribute(semconv_js_1.GEN_AI.PROVIDER_NAME, "struct");
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
- startedSpan.setAttribute(semconv_js_1.GEN_AI.CONVERSATION_ID, sessionId);
351
- // Always set parent_session_id when there's a parent agent — even
352
- // when the value matches sessionId (which happens when a nested
353
- // ``struct.agent()`` inherits ambient session). The attribute is
354
- // structural ("this agent has a parent agent"), not a uniqueness
355
- // marker. The UI uses it to render an inline subagent expansion
356
- // under the triggering call (vs the drill-in flow used when
357
- // sessionIds differ).
358
- if (parentSessionId) {
359
- startedSpan.setAttribute(semconv_js_1.STRUCT.AGENT_PARENT_SESSION_ID, parentSessionId);
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
- startedSpan.setAttribute(semconv_js_1.GEN_AI.PROVIDER_NAME, "struct");
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
- safe(() => startedSpan.setStatus({ code: api_1.SpanStatusCode.OK }), "tool.set_ok_status", this._internalLogger);
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 { type Logger } from "@opentelemetry/api-logs";
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
- * Port of _emit_message_events from anthropic.py.
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 a gen_ai.choice LogRecord for an Anthropic response.
9
- * Port of _emit_choice_event from anthropic.py.
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 emitAnthropicChoiceEvent(logger: Logger, contentBlocks: unknown, stopReason: string | null | undefined): void;
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
@@ -2,40 +2,18 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.emitAnthropicMessageEvents = emitAnthropicMessageEvents;
4
4
  exports.emitAnthropicChoiceEvent = emitAnthropicChoiceEvent;
5
- const api_1 = require("@opentelemetry/api");
6
- const api_logs_1 = require("@opentelemetry/api-logs");
7
- const context_js_1 = require("./context.js");
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
- * Port of _emit_message_events from anthropic.py.
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
- emitLogRecord({
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
- payload: (0, truncation_js_1.safeJsonStringify)({
52
- role: "system",
53
- parts: (0, truncation_js_1.truncateParts)(parts),
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
- emitLogRecord({
44
+ (0, genai_content_js_1.emitMessageEvent)({
70
45
  logger,
46
+ role,
47
+ parts,
71
48
  eventName,
72
- payload: (0, truncation_js_1.safeJsonStringify)({ role, parts: (0, truncation_js_1.truncateParts)(parts) }),
73
- extraAttrs: {
74
- [semconv_js_1.GEN_AI.SYSTEM]: "anthropic",
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
- * Emit a gen_ai.choice LogRecord for an Anthropic response.
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
- const payload = (0, truncation_js_1.safeJsonStringify)({
90
- index: 0,
91
- finish_reason: mappedReason,
92
- message: { role: "assistant", parts: (0, truncation_js_1.truncateParts)(parts) },
61
+ (0, genai_content_js_1.emitChoiceEvent)({
62
+ logger,
63
+ parts,
64
+ finishReason: mappedReason,
65
+ provider,
66
+ span,
93
67
  });
94
- emitLogRecord({
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
- eventName: semconv_js_1.EVENT_NAMES.CHOICE,
97
- payload,
98
- extraAttrs: {
99
- [semconv_js_1.GEN_AI.SYSTEM]: "anthropic",
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