@struct-ai/sdk 0.3.17 → 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 (54) hide show
  1. package/README.md +62 -17
  2. package/dist/commonjs/context.d.ts +19 -0
  3. package/dist/commonjs/context.js +58 -0
  4. package/dist/commonjs/core.js +104 -8
  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 +6 -1
  13. package/dist/commonjs/integrations/anthropic.js +113 -125
  14. package/dist/commonjs/integrations/index.js +8 -0
  15. package/dist/commonjs/integrations/langchain-callback.d.ts +3 -0
  16. package/dist/commonjs/integrations/langchain-callback.js +84 -6
  17. package/dist/commonjs/integrations/langchain-content.js +1 -1
  18. package/dist/commonjs/integrations/openai-content.d.ts +34 -0
  19. package/dist/commonjs/integrations/openai-content.js +375 -0
  20. package/dist/commonjs/integrations/openai.d.ts +39 -0
  21. package/dist/commonjs/integrations/openai.js +305 -0
  22. package/dist/commonjs/semconv.d.ts +1 -0
  23. package/dist/commonjs/semconv.js +1 -0
  24. package/dist/commonjs/truncation.d.ts +29 -0
  25. package/dist/commonjs/truncation.js +184 -10
  26. package/dist/commonjs/version.d.ts +1 -1
  27. package/dist/commonjs/version.js +1 -1
  28. package/dist/esm/context.d.ts +19 -0
  29. package/dist/esm/context.js +56 -0
  30. package/dist/esm/core.js +104 -8
  31. package/dist/esm/events.d.ts +17 -6
  32. package/dist/esm/events.js +82 -61
  33. package/dist/esm/genai-content.d.ts +52 -0
  34. package/dist/esm/genai-content.js +137 -0
  35. package/dist/esm/instrument.d.ts +47 -0
  36. package/dist/esm/instrument.js +155 -0
  37. package/dist/esm/integrations/anthropic-content.js +19 -7
  38. package/dist/esm/integrations/anthropic.d.ts +6 -1
  39. package/dist/esm/integrations/anthropic.js +112 -126
  40. package/dist/esm/integrations/index.js +8 -0
  41. package/dist/esm/integrations/langchain-callback.d.ts +3 -0
  42. package/dist/esm/integrations/langchain-callback.js +85 -7
  43. package/dist/esm/integrations/langchain-content.js +1 -1
  44. package/dist/esm/integrations/openai-content.d.ts +34 -0
  45. package/dist/esm/integrations/openai-content.js +360 -0
  46. package/dist/esm/integrations/openai.d.ts +39 -0
  47. package/dist/esm/integrations/openai.js +296 -0
  48. package/dist/esm/semconv.d.ts +1 -0
  49. package/dist/esm/semconv.js +1 -0
  50. package/dist/esm/truncation.d.ts +29 -0
  51. package/dist/esm/truncation.js +182 -10
  52. package/dist/esm/version.d.ts +1 -1
  53. package/dist/esm/version.js +1 -1
  54. package/package.json +8 -2
package/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # @struct-ai/sdk
2
2
 
3
3
  Struct agent observability SDK for TypeScript/Node.js. Auto-instruments AI agent
4
- frameworks — the Anthropic SDK and LangChain.js — and emits OpenTelemetry
5
- traces + logs to [struct.ai](https://struct.ai) with zero config.
4
+ frameworks and LLM SDKs — the Anthropic SDK, the OpenAI SDK (Responses API),
5
+ and LangChain.js — and emits OpenTelemetry traces + logs to
6
+ [struct.ai](https://struct.ai) with zero config.
6
7
 
7
8
  This is the TypeScript port of [`struct-sdk`](https://pypi.org/project/struct-sdk/)
8
9
  (Python). Span names, attribute keys, and log event shapes are identical across
@@ -13,7 +14,7 @@ the two SDKs so the server processes both uniformly.
13
14
  ```bash
14
15
  npm install @struct-ai/sdk
15
16
  # optional — the SDK auto-instruments these if present
16
- npm install @anthropic-ai/sdk @langchain/core @langchain/langgraph
17
+ npm install @anthropic-ai/sdk openai @langchain/core @langchain/langgraph
17
18
  ```
18
19
 
19
20
  Requires Node 18+.
@@ -57,6 +58,7 @@ await struct.agent({ name: "checkout" }, async () => {
57
58
  |---|---|---|---|
58
59
  | `@anthropic-ai/sdk` | `Messages.prototype.create`, `.stream` | `chat {model}` | Cache-token accounting, streaming with tool-use reconstruction. Defers to a LangChain `BaseChatModel` call already in progress (e.g. `ChatAnthropic`) so the two integrations don't double-emit — see ownership order below |
59
60
  | `@anthropic-ai/bedrock-sdk`, `@anthropic-ai/vertex-sdk` | `Messages.prototype.*` | `chat {model}` | Best-effort, if installed |
61
+ | `openai` | `Responses.prototype.create` | `chat {model}` | **Responses API, non-streaming calls only** — `responses.create({stream: true})` and `responses.stream()` pass through untouched (no span), and `chat.completions` is not instrumented. Cache-read/cache-write/reasoning token accounting; `function_call` → `struct.tool()` id auto-linkage. Requires an openai release with the Responses API (v4 releases without it are a graceful no-op). Defers to a LangChain call in progress, same as Anthropic |
60
62
  | `@langchain/core` `BaseChatModel` | `.invoke`, `.stream` | `chat {model}` | Always owns the `chat` span for `ChatAnthropic`/etc. calls, even when a provider-direct instrumentor (e.g. the Anthropic patch) is also active — the framework layer outranks the provider layer (single span; see [AGENTS.md](./AGENTS.md) §5) |
61
63
  | `@langchain/core` `StructuredTool` | `.invoke` | `execute_tool {name}` | Extracts `tool_call_id` from LangChain ToolCall input or pending queue |
62
64
  | `@langchain/core` `BaseRetriever` | `.invoke` | `retrieval {name}` | |
@@ -166,6 +168,37 @@ await struct.agent({ name: "checkout" }, async () => {
166
168
  `@anthropic-ai/sdk`, `@anthropic-ai/bedrock-sdk`, and `@anthropic-ai/vertex-sdk`
167
169
  are all auto-instrumented for chat spans.
168
170
 
171
+ #### OpenAI SDK (raw, Responses API)
172
+
173
+ Same rule as raw Anthropic — chat spans emit automatically; wrap your agent
174
+ loop and tools yourself:
175
+
176
+ ```ts
177
+ import { struct } from "@struct-ai/sdk";
178
+ struct.init({ ingestKey: "pk-...", serviceName: "support-agent" });
179
+
180
+ import OpenAI from "openai";
181
+ const client = new OpenAI();
182
+
183
+ await struct.agent({ name: "support" }, async () => {
184
+ const resp = await client.responses.create({
185
+ model: "gpt-5.5",
186
+ input: "triage this ticket",
187
+ });
188
+
189
+ // tool_call_id is auto-filled from the preceding response's function_call.
190
+ await struct.tool({ name: "lookup_order" }, async () => {
191
+ return await lookupOrder(resp);
192
+ });
193
+ });
194
+ ```
195
+
196
+ **Scope:** the OpenAI integration covers **non-streaming `responses.create()`**
197
+ only. Streaming calls (`stream: true` / `responses.stream()`) run unmodified and
198
+ emit no span, and `chat.completions` is not instrumented. Azure OpenAI clients
199
+ (`AzureOpenAI`) share the same `Responses` class and are covered by the same
200
+ patch.
201
+
169
202
  #### LangChain `BaseChatModel` (no agent/graph)
170
203
 
171
204
  If you call `ChatAnthropic.invoke(...)` (or any other `BaseChatModel`)
@@ -206,13 +239,13 @@ struct.init({
206
239
  ```
207
240
 
208
241
  - **`EventOnly`** (default): per-message content lands on OTel log records
209
- (`gen_ai.{user,assistant,system,tool}.message`, `gen_ai.choice`). Spans carry
210
- metadata only.
242
+ (`gen_ai.{user,assistant,system,tool}.message`, `gen_ai.choice`).
211
243
  - **`SpanOnly`**: content on span attributes (`gen_ai.input.messages`,
212
244
  `gen_ai.output.messages`).
213
245
  - **`SpanAndEvent`**: both.
214
- - **`None`**: no content captured. Token counts, tool call IDs, finish reasons,
215
- and other metadata still flow.
246
+ - **`None`**: no content captured, anywhere — no log events, no span content
247
+ attributes. Token counts, tool call IDs, finish reasons, and other metadata
248
+ still flow.
216
249
 
217
250
  Set `captureContent: false` for the legacy bool API (equivalent to
218
251
  `ContentCaptureMode.None`).
@@ -252,7 +285,7 @@ the troubleshooting section below for the workaround.
252
285
  Emits attributes per the OTel GenAI semantic conventions:
253
286
 
254
287
  - `gen_ai.operation.name` — `chat`, `execute_tool`, `invoke_agent`, `retrieval`
255
- - `gen_ai.provider.name` — `anthropic`, `openai`, `langchain`, `struct`, …
288
+ - `gen_ai.provider.name` — the GenAI provider on inference spans (`anthropic`, `openai`, …); platform-routed calls report the platform value (`aws.bedrock`, `gcp.vertex_ai`, `azure.ai.openai`) when detectable from the client. `invoke_agent` spans inherit the provider from their first child inference call (omitted when no model is reached); `execute_tool`/`retrieval` spans carry no provider.
256
289
  - `gen_ai.request.{model, max_tokens, temperature, top_p, top_k, stop_sequences}`
257
290
  - `gen_ai.response.{model, id, finish_reasons}`
258
291
  - `gen_ai.usage.{input_tokens, output_tokens, cache_read.input_tokens, cache_creation.input_tokens}`
@@ -270,21 +303,33 @@ response excludes). Matches the Python SDK.
270
303
  `test/live/**`.
271
304
  - `pnpm typecheck` — `tsc --noEmit`.
272
305
  - `pnpm build` — `tshy`, producing the dual ESM/CJS `dist/`.
273
- - `pnpm test:live` — the live real-model suite: real `@langchain/anthropic` +
274
- real Anthropic API + real `@langchain/langgraph`, verifying emitted spans
275
- in-memory (no ingester needed). Skips cleanly (exit 0, all tests skipped)
276
- when no API key is present — safe to leave in normal CI.
306
+ - `pnpm test:live` — the live real-model suite: real provider SDKs
307
+ (`@anthropic-ai/sdk`, `openai`) and frameworks (`@langchain/*`) against the
308
+ real Anthropic / OpenAI APIs, verifying the emitted spans + log events
309
+ **in-memory** (no ingester needed). Each gated block skips cleanly (exit 0)
310
+ when its API key is absent — safe to leave in normal CI.
277
311
 
278
- To actually run it, point `STRUCT_LIVE_ENV_FILE` at a file containing
279
- `ANTHROPIC_API_KEY=...` (dotenv-style `KEY=value` lines; quotes optional).
280
- The path is never committed to the repo — it's supplied per-invocation:
312
+ To run it, point `STRUCT_LIVE_ENV_FILE` at a file with the keys you want to
313
+ exercise (`ANTHROPIC_API_KEY=...` and/or `OPENAI_API_KEY=...`; dotenv-style
314
+ `KEY=value` lines, quotes optional). The path is never committed — it's
315
+ supplied per-invocation:
281
316
 
282
317
  ```bash
283
318
  STRUCT_LIVE_ENV_FILE=/path/to/your/.env pnpm test:live
284
319
  ```
285
320
 
286
- Uses the cheapest current Anthropic model alias with small `max_tokens`
287
- budgets — a handful of real API calls per run, not dozens.
321
+ Only blocks whose key is present run, so a file with both keys runs both the
322
+ Anthropic and OpenAI suites (a handful of small real calls each — cheap model
323
+ aliases, small token budgets).
324
+
325
+ **Full-pipeline e2e** (`test/live/openai-ingest-e2e.live.test.ts`)
326
+ additionally ships the emitted telemetry to a *real* ingest endpoint and
327
+ flushes it, so you can confirm a change travels the whole path
328
+ (SDK → OTLP export → your Struct instance). It's gated on `STRUCT_INGEST_URL`
329
+ + `STRUCT_INGEST_KEY` (on top of `OPENAI_API_KEY`), so it stays skipped unless
330
+ you deliberately supply real ingest credentials. It still self-asserts the
331
+ span shape in-memory; confirming the span actually *landed* means querying
332
+ your Struct instance's telemetry — see [AGENTS.md](./AGENTS.md).
288
333
  - `pnpm test:parity` — the cross-language conformance harness: drives the
289
334
  same canonical agent topology through both this SDK and `struct-sdk-python`
290
335
  and diffs their normalized span shapes, failing (non-zero exit) on any
@@ -53,4 +53,23 @@ export declare function snapshotStore(): StructContext | undefined;
53
53
  export declare function runWithStore<T>(store: StructContext | undefined, fn: () => T): T;
54
54
  /** Test-only: run `fn` inside a completely fresh context (no parent store). */
55
55
  export declare function runInFreshContext<T>(fn: () => T): T;
56
+ /**
57
+ * Write-once `gen_ai.provider.name` on an invoke_agent span.
58
+ *
59
+ * CONTRACT: `agentSpan` is always an SDK-OWNED span — created by our own
60
+ * tracer in `struct.agent()` or the LangChain handler and delivered via the
61
+ * ALS store / run map, which nothing else writes. Never a host object. So
62
+ * the industry-standard owned-object pattern applies (state lives ON the
63
+ * object — Sentry/dd-trace private span fields, OTel JS symbol markers): a
64
+ * private Symbol sentinel set after a successful write. No registries or
65
+ * lifecycle bookkeeping — those are for FOREIGN objects.
66
+ *
67
+ * Semantics: "a real child provider" — racing children with different
68
+ * providers may pick either; the sentinel is set only after a successful
69
+ * write so a transient failure can be retried. Parity: python
70
+ * `stamp_provider_once`.
71
+ */
72
+ export declare function stampProviderOnce(agentSpan: Span | undefined, provider: string | undefined): void;
73
+ /** Stamp the ambient agent span with the child call's provider (write-once). */
74
+ export declare function propagateProviderToParent(provider: string | undefined): void;
56
75
  //# sourceMappingURL=context.d.ts.map
@@ -14,6 +14,8 @@ exports.pushPendingToolCalls = pushPendingToolCalls;
14
14
  exports.snapshotStore = snapshotStore;
15
15
  exports.runWithStore = runWithStore;
16
16
  exports.runInFreshContext = runInFreshContext;
17
+ exports.stampProviderOnce = stampProviderOnce;
18
+ exports.propagateProviderToParent = propagateProviderToParent;
17
19
  const node_async_hooks_1 = require("node:async_hooks");
18
20
  const als = new node_async_hooks_1.AsyncLocalStorage();
19
21
  function getStore() {
@@ -110,4 +112,60 @@ function runWithStore(store, fn) {
110
112
  function runInFreshContext(fn) {
111
113
  return als.run({}, fn);
112
114
  }
115
+ /**
116
+ * Write-once `gen_ai.provider.name` on an invoke_agent span.
117
+ *
118
+ * The GenAI spec puts `gen_ai.provider.name` on invoke_agent spans, but the
119
+ * layer that CREATES those spans (`struct.agent()`, the LangChain handler) is
120
+ * provider-agnostic and can't know the value yet — only the child inference
121
+ * call can. The first chat call within the agent scope stamps it (best
122
+ * knowledge; first one wins); an agent that never reaches a model omits the
123
+ * attribute rather than carrying a framework name. Parity: python
124
+ * `_genai_content.stamp_provider_once`.
125
+ */
126
+ const PROVIDER_STAMPED = Symbol("struct.providerStamped");
127
+ /**
128
+ * Write-once `gen_ai.provider.name` on an invoke_agent span.
129
+ *
130
+ * CONTRACT: `agentSpan` is always an SDK-OWNED span — created by our own
131
+ * tracer in `struct.agent()` or the LangChain handler and delivered via the
132
+ * ALS store / run map, which nothing else writes. Never a host object. So
133
+ * the industry-standard owned-object pattern applies (state lives ON the
134
+ * object — Sentry/dd-trace private span fields, OTel JS symbol markers): a
135
+ * private Symbol sentinel set after a successful write. No registries or
136
+ * lifecycle bookkeeping — those are for FOREIGN objects.
137
+ *
138
+ * Semantics: "a real child provider" — racing children with different
139
+ * providers may pick either; the sentinel is set only after a successful
140
+ * write so a transient failure can be retried. Parity: python
141
+ * `stamp_provider_once`.
142
+ */
143
+ function stampProviderOnce(agentSpan, provider) {
144
+ try {
145
+ if (!agentSpan || !provider)
146
+ return;
147
+ const marked = agentSpan;
148
+ if (marked[PROVIDER_STAMPED])
149
+ return;
150
+ agentSpan.setAttribute("gen_ai.provider.name", provider);
151
+ try {
152
+ marked[PROVIDER_STAMPED] = true;
153
+ }
154
+ catch {
155
+ /* frozen span — worst case a later child re-stamps a real provider */
156
+ }
157
+ }
158
+ catch {
159
+ /* never fail the application for telemetry */
160
+ }
161
+ }
162
+ /** Stamp the ambient agent span with the child call's provider (write-once). */
163
+ function propagateProviderToParent(provider) {
164
+ try {
165
+ stampProviderOnce(getAgentSpan(), provider);
166
+ }
167
+ catch {
168
+ /* never fail the application for telemetry */
169
+ }
170
+ }
113
171
  //# sourceMappingURL=context.js.map
@@ -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
  }
@@ -384,7 +394,10 @@ class StructSDK {
384
394
  const startedSpan = span;
385
395
  safe(() => {
386
396
  startedSpan.setAttribute(semconv_js_1.GEN_AI.OPERATION_NAME, "invoke_agent");
387
- 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).
388
401
  startedSpan.setAttribute(semconv_js_1.GEN_AI.AGENT_NAME, agentName);
389
402
  // gen_ai.agent.id is the stable identifier of the agent
390
403
  // DEFINITION. Only set it when the caller provides one — we do
@@ -479,7 +492,8 @@ class StructSDK {
479
492
  const startedSpan = span;
480
493
  safe(() => {
481
494
  startedSpan.setAttribute(semconv_js_1.GEN_AI.OPERATION_NAME, "execute_tool");
482
- 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.
483
497
  startedSpan.setAttribute(semconv_js_1.GEN_AI.TOOL_NAME, toolName);
484
498
  if (toolCallId) {
485
499
  startedSpan.setAttribute(semconv_js_1.GEN_AI.TOOL_CALL_ID, toolCallId);
@@ -495,7 +509,28 @@ class StructSDK {
495
509
  if (this.captureContent && result !== undefined && result !== null) {
496
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);
497
511
  }
498
- 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
+ }
499
534
  return result;
500
535
  }
501
536
  catch (err) {
@@ -517,4 +552,65 @@ function recordError(span, err) {
517
552
  span.recordException(err);
518
553
  }
519
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
+ }
520
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.PROVIDER_NAME]: "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.PROVIDER_NAME]: "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.PROVIDER_NAME]: "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