struct-sdk 0.3.2__tar.gz → 0.4.13__tar.gz

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 (26) hide show
  1. {struct_sdk-0.3.2 → struct_sdk-0.4.13}/.gitignore +3 -0
  2. {struct_sdk-0.3.2 → struct_sdk-0.4.13}/PKG-INFO +286 -14
  3. {struct_sdk-0.3.2 → struct_sdk-0.4.13}/README.md +271 -13
  4. {struct_sdk-0.3.2 → struct_sdk-0.4.13}/pyproject.toml +82 -11
  5. {struct_sdk-0.3.2 → struct_sdk-0.4.13}/src/struct_sdk/_auto.py +52 -2
  6. {struct_sdk-0.3.2 → struct_sdk-0.4.13}/src/struct_sdk/_genai_content.py +80 -114
  7. struct_sdk-0.4.13/src/struct_sdk/_operation_details.py +293 -0
  8. struct_sdk-0.4.13/src/struct_sdk/ag2.py +803 -0
  9. {struct_sdk-0.3.2 → struct_sdk-0.4.13}/src/struct_sdk/anthropic.py +59 -170
  10. struct_sdk-0.4.13/src/struct_sdk/autogen.py +403 -0
  11. {struct_sdk-0.3.2 → struct_sdk-0.4.13}/src/struct_sdk/core.py +78 -8
  12. struct_sdk-0.4.13/src/struct_sdk/crewai.py +889 -0
  13. struct_sdk-0.4.13/src/struct_sdk/google_genai.py +593 -0
  14. {struct_sdk-0.3.2 → struct_sdk-0.4.13}/src/struct_sdk/langchain.py +216 -162
  15. struct_sdk-0.4.13/src/struct_sdk/litellm.py +612 -0
  16. struct_sdk-0.4.13/src/struct_sdk/llamaindex.py +1347 -0
  17. struct_sdk-0.4.13/src/struct_sdk/mcp.py +376 -0
  18. struct_sdk-0.4.13/src/struct_sdk/openai.py +2015 -0
  19. struct_sdk-0.4.13/src/struct_sdk/openai_agents.py +881 -0
  20. {struct_sdk-0.3.2 → struct_sdk-0.4.13}/src/struct_sdk/pydantic_ai.py +94 -17
  21. struct_sdk-0.3.2/src/struct_sdk/openai.py +0 -810
  22. {struct_sdk-0.3.2 → struct_sdk-0.4.13}/LICENSE +0 -0
  23. {struct_sdk-0.3.2 → struct_sdk-0.4.13}/src/struct_sdk/__init__.py +0 -0
  24. {struct_sdk-0.3.2 → struct_sdk-0.4.13}/src/struct_sdk/_distro.py +0 -0
  25. {struct_sdk-0.3.2 → struct_sdk-0.4.13}/src/struct_sdk/claude_agent.py +0 -0
  26. {struct_sdk-0.3.2 → struct_sdk-0.4.13}/src/struct_sdk/temporal.py +0 -0
@@ -118,3 +118,6 @@ struct-agent-key.json
118
118
 
119
119
  # Local/CI test result artifacts
120
120
  **/test-results/
121
+
122
+ # Generated agent session project ledgers (data, HTML and local helper state)
123
+ .project-ledgers/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: struct-sdk
3
- Version: 0.3.2
3
+ Version: 0.4.13
4
4
  Summary: Struct agent observability SDK — auto-instruments AI agent frameworks with OpenTelemetry
5
5
  Project-URL: Homepage, https://struct.ai
6
6
  Project-URL: Documentation, https://struct.ai/docs
@@ -24,6 +24,7 @@ Requires-Python: >=3.10
24
24
  Requires-Dist: opentelemetry-api>=1.45.1
25
25
  Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27.0
26
26
  Requires-Dist: opentelemetry-sdk>=1.27.0
27
+ Requires-Dist: wrapt>=1.16.0
27
28
  Provides-Extra: anthropic
28
29
  Requires-Dist: anthropic>=0.30.0; extra == 'anthropic'
29
30
  Provides-Extra: auto
@@ -69,6 +70,9 @@ Requires-Dist: opentelemetry-instrumentation-urllib; extra == 'auto'
69
70
  Requires-Dist: opentelemetry-instrumentation-urllib3; extra == 'auto'
70
71
  Requires-Dist: opentelemetry-instrumentation>=0.51b0; extra == 'auto'
71
72
  Requires-Dist: opentelemetry-resource-detector-containerid; extra == 'auto'
73
+ Provides-Extra: autogen
74
+ Requires-Dist: autogen-agentchat>=0.7.0; extra == 'autogen'
75
+ Requires-Dist: autogen-ext[openai]>=0.7.0; extra == 'autogen'
72
76
  Provides-Extra: azure
73
77
  Requires-Dist: azure-core-tracing-opentelemetry>=1.0.0b11; extra == 'azure'
74
78
  Provides-Extra: claude-agent-sdk
@@ -88,10 +92,20 @@ Requires-Dist: pytest>=9.0.3; extra == 'dev'
88
92
  Requires-Dist: ruff>=0.5; extra == 'dev'
89
93
  Provides-Extra: exceptions
90
94
  Requires-Dist: opentelemetry-instrumentation-exceptions; extra == 'exceptions'
95
+ Provides-Extra: google-genai
96
+ Requires-Dist: google-genai>=1.12.1; extra == 'google-genai'
91
97
  Provides-Extra: langchain
92
98
  Requires-Dist: langchain-core>=1.3.3; extra == 'langchain'
99
+ Provides-Extra: litellm
100
+ Requires-Dist: litellm[mcp]>=1.70.0; extra == 'litellm'
101
+ Provides-Extra: llamaindex
102
+ Requires-Dist: llama-index-core>=0.13.0; extra == 'llamaindex'
103
+ Provides-Extra: mcp
104
+ Requires-Dist: mcp>=1.28.1; extra == 'mcp'
93
105
  Provides-Extra: openai
94
106
  Requires-Dist: openai>=1.66.0; extra == 'openai'
107
+ Provides-Extra: openai-agents
108
+ Requires-Dist: openai-agents>=0.20.0; extra == 'openai-agents'
95
109
  Provides-Extra: pydantic-ai
96
110
  Requires-Dist: pydantic-ai-slim>=2.53.0; extra == 'pydantic-ai'
97
111
  Provides-Extra: structlog
@@ -103,9 +117,9 @@ Description-Content-Type: text/markdown
103
117
  # struct-sdk
104
118
 
105
119
  OpenTelemetry instrumentation for AI agents in Python. Captures spans,
106
- token usage, and message events from the Anthropic SDK, the OpenAI SDK
107
- (Responses API), pydantic-ai, the Claude Agent SDK, and LangChain /
108
- LangGraph, and exports them to [struct.ai](https://struct.ai) for observability.
120
+ token usage, and message content from the Anthropic SDK, the OpenAI SDK
121
+ (Responses API), google-genai (Gemini), LiteLLM, pydantic-ai, the OpenAI Agents SDK, MCP clients, the Claude Agent SDK,
122
+ LangChain / LangGraph, LlamaIndex, CrewAI, Microsoft AutoGen, and AG2, and exports them to [struct.ai](https://struct.ai) for observability.
109
123
 
110
124
  A TypeScript version is available as
111
125
  [`@struct-ai/sdk`](https://www.npmjs.com/package/@struct-ai/sdk). Both
@@ -120,8 +134,13 @@ pip install struct-sdk
120
134
  pip install anthropic
121
135
  pip install openai
122
136
  pip install pydantic-ai-slim
137
+ pip install openai-agents
138
+ pip install mcp
139
+ pip install google-genai
140
+ pip install "litellm[mcp]" # LiteLLM needs mcp for calls with tools
123
141
  pip install claude-agent-sdk
124
142
  pip install langchain-core langgraph
143
+ pip install llama-index-core llama-index-llms-openai # or the provider integration you use
125
144
  ```
126
145
 
127
146
  Requires Python 3.10+.
@@ -164,8 +183,16 @@ async with struct.agent(name="checkout"):
164
183
  | Library | Span type | Notes |
165
184
  |---|---|---|
166
185
  | `anthropic` | `chat {model}` | Cache-token accounting; streaming chats with tool-use reconstruction. Bedrock and Vertex variants supported if installed. |
167
- | `openai` (Responses API) | `chat {model}` | Non-streaming `responses.create` on sync and async clients (Azure clients included). Cached-token accounting from `usage.input_tokens_details`. Streaming and `chat.completions` calls pass through untraced. Message-content capture at parity with the Anthropic integration: per-message events (`gen_ai.system/user/assistant/tool.message`) and `gen_ai.choice`, plus `gen_ai.input.messages` / `gen_ai.output.messages` in span-content mode. Reasoning items are skipped (`encrypted_content` is never emitted). |
186
+ | `openai` (Responses and Chat Completions) | `chat {model}` | `responses.create` and `chat.completions.create` on sync and async clients, streaming or not, including the `.stream()` helpers (Azure and OpenAI-compatible clients such as vLLM, Groq, DeepSeek and xAI included). Raw `create(stream=True)` spans end exactly once on exhaustion, `close()`, `with` exit, error, cancellation or garbage collection; every loop over a stream shares one iteration (LiteLLM starts one per chunk), so a `break` ends the span when it releases the stream. Helper streams that exit early end when the helper is released. Chat Completions streamed usage is recorded when you pass `stream_options={"include_usage": True}`; Responses records terminal usage without that option. The SDK never changes your request. Cached-token accounting from the usage details. Message-content capture at parity with the Anthropic integration: in event modes, one `gen_ai.client.inference.operation.details` record per call (one output message per choice; for a stream, written when the stream ends); in span modes, `gen_ai.input.messages` / `gen_ai.output.messages` on the span. Reasoning items are skipped (`encrypted_content` is never emitted). `chat.completions.parse()` and `responses.parse()` calls are not traced. |
168
187
  | `pydantic_ai` | `invoke_agent {agent}`, `chat {model}`, `execute_tool {tool}` | pydantic-ai's own instrumentation, turned on with Struct's tracer provider. See [pydantic-ai](#pydantic-ai). |
188
+ | `agents` (OpenAI Agents SDK) | `invoke_agent {workflow}`, `invoke_agent {agent}`, `chat {model}`, `execute_tool {tool}` | A `TracingProcessor` added next to the host's: every run, agent, model call (Responses, Chat Completions and LiteLLM models, streamed or not), function tool and handoff. See [OpenAI Agents SDK](#openai-agents-sdk). |
189
+ | `mcp` (`ClientSession.call_tool`) | `tools/call {tool}` | One CLIENT span per MCP tool call (`gen_ai.operation.name = execute_tool`), with the trace context sent in `params._meta`. See [MCP](#mcp). |
190
+ | `google.genai` | `generate_content {model}` | `client.models.generate_content` and `client.aio.models.generate_content` (Gemini API → `gcp.gemini`, Vertex AI → `gcp.vertex_ai`), usage with cached and thinking tokens. Streams pass through untraced. Google ADK's own spans own its model calls. See [Gemini and Google ADK](#gemini-and-google-adk). |
191
+ | `litellm` | `chat {model}` | Non-streaming `litellm.completion` / `litellm.acompletion`, including each `Router` attempt. Model without LiteLLM's `provider/` prefix; `gen_ai.provider.name` in GenAI naming (`aws.bedrock`, `gcp.vertex_ai`, `azure.ai.openai`, …). Usage with cache reads, cache writes and reasoning tokens. Streams get no LiteLLM span (on LiteLLM's OpenAI provider the `openai` patch traces them). See [LiteLLM](#litellm). |
192
+ | `llama_index` | `invoke_agent`, `chat`, `execute_tool`, `retrieval` | Workflow agent runs (`FunctionAgent`, `ReActAgent`, `AgentWorkflow`), LLM chats (sync, async, streamed), tool calls and retrievers, through LlamaIndex's instrumentation dispatcher. The provider SDK span under a LlamaIndex chat is suppressed. See [LlamaIndex](#llamaindex). |
193
+ | `crewai` (1.15.13+) | `invoke_agent`, `chat`, `execute_tool` | Crew kickoffs and agent executions, model calls (native providers or LiteLLM) and tool calls, from CrewAI's event bus. The provider SDK span under a CrewAI model call is suppressed. See [CrewAI](#crewai). |
194
+ | `autogen_ext` (AutoGen 0.7+) | `chat {model}` | AutoGen traces its own agents and tools; Struct adds a `chat` span for each non-streaming call through the OpenAI-family clients (`OpenAIChatCompletionClient`, Azure, OpenAI-compatible endpoints). In owned full-app mode, Struct also stamps the `struct.agent()` session on AutoGen's spans. AutoGen's Anthropic client is covered by the `anthropic` integration. See [AutoGen](#autogen). |
195
+ | `autogen` (AG2 0.x) | `chat {model}`, `execute_tool {name}` | Every `OpenAIWrapper.create` model call and `ConversableAgent` tool execution. AG2's own instrumentation, when you turn it on, owns those calls instead. See [AG2](#ag2). |
169
196
  | `claude_agent_sdk` | `agent`, `chat`, `execute_tool` | Telemetry comes from Claude Code itself in the subprocess; ingest credentials are propagated to it via `ClaudeAgentOptions`. Subagents inherit the configuration. |
170
197
  | `langchain_core` `BaseChatModel` | `chat {model}` | When the underlying provider SDK is also instrumented (e.g. `ChatAnthropic` + `anthropic`), the provider's span is suppressed, so there is a single span. |
171
198
  | `langchain_core` `BaseTool` | `execute_tool {name}` | `tool_call_id` extracted from the LangChain ToolCall when present. |
@@ -227,10 +254,9 @@ calls your code makes while reading a stream keep their spans). During agent
227
254
  runs, Struct guards telemetry faults and caps content. Realtime sessions and
228
255
  direct `InstrumentedModel` exception recording are outside this guarantee.
229
256
 
230
- - **Content** goes on span attributes only (pydantic-ai has no message
231
- events): `span_only` / `span_and_event` include prompts, answers and tool
232
- arguments; `event_only` and `none` do not. Tool definitions are always
233
- recorded.
257
+ - **Content:** in event modes, each chat call's messages go to one
258
+ `operation.details` event. Span modes keep chat content on spans. Tool
259
+ arguments and results remain on `execute_tool` spans in every mode but `none`.
234
260
  - **Sessions:** on Struct's provider, a `struct.agent(session_id=...)`
235
261
  around the run wins; otherwise pydantic-ai's `conversation_id` is used.
236
262
  Instrumentation configured on your own provider keeps its conversation IDs.
@@ -240,6 +266,185 @@ direct `InstrumentedModel` exception recording are outside this guarantee.
240
266
  `Agent.instrument_all(False)` before `struct.init()` is pydantic-ai's
241
267
  initial value, so it does not keep pydantic-ai off.
242
268
 
269
+ #### OpenAI Agents SDK
270
+
271
+ ```python
272
+ from struct_sdk import struct
273
+ struct.init(ingest_key="pk-...", service_name="support-agents")
274
+
275
+ from agents import Agent, Runner, RunConfig
276
+ result = await Runner.run(triage_agent, "Refund order 42",
277
+ run_config=RunConfig(group_id=conversation_id))
278
+ ```
279
+
280
+ `struct.init()` adds a tracing processor to the Agents SDK
281
+ (`add_trace_processor`), so OpenAI's own trace upload keeps working. Each
282
+ run is one trace:
283
+
284
+ ```
285
+ invoke_agent Agent workflow # the run (RunConfig.workflow_name)
286
+ ├── invoke_agent Triage # each agent that ran
287
+ │ ├── chat gpt-5.4-mini # each model call, with usage
288
+ │ ├── execute_tool get_weather # each function tool, with its call id
289
+ │ └── execute_tool transfer_to_billing # a handoff is a tool call
290
+ └── invoke_agent Billing
291
+ └── chat gpt-5.4-mini
292
+ ```
293
+
294
+ - **Sessions.** `RunConfig(group_id=...)` becomes `gen_ai.conversation.id`.
295
+ Wrapping the run in `struct.agent(session_id=...)` makes that span the
296
+ run's root instead (no separate `invoke_agent Agent workflow`) and its
297
+ session wins.
298
+ - **One span per model call.** The processor owns `chat` spans, including
299
+ streamed runs; the OpenAI patch makes no span of its own while it does,
300
+ only recording the requested model, which the Agents SDK's Responses
301
+ spans do not carry. Non-streamed Chat Completions and LiteLLM model spans
302
+ have no response id or model (the Agents SDK records only the message).
303
+ - **Nesting.** While a tool or model call runs, its span is the current
304
+ OpenTelemetry span, so your own spans inside a tool (and HTTP client
305
+ spans in full-app mode) nest under it.
306
+ - **Content** needs both switches on: the Agents SDK's
307
+ `trace_include_sensitive_data` (on by default) and a Struct
308
+ `content_capture` mode other than `none`. Usage is always recorded.
309
+ - **Turning Agents tracing off** (`OPENAI_AGENTS_DISABLE_TRACING=1`,
310
+ `RunConfig(tracing_disabled=True)`) also removes Struct's agent and tool
311
+ spans; only the OpenAI patch's `chat` spans for non-streamed calls
312
+ remain. To stop only the upload to OpenAI, replace the processor list
313
+ and keep Struct's:
314
+
315
+ ```python
316
+ from agents import set_trace_processors
317
+ from struct_sdk.openai_agents import get_tracing_processor
318
+ set_trace_processors([get_tracing_processor()]) # after struct.init()
319
+ ```
320
+
321
+ #### Gemini and Google ADK
322
+
323
+ ```python
324
+ from struct_sdk import struct
325
+ struct.init(ingest_key="pk-...", service_name="planner")
326
+
327
+ from google import genai
328
+ client = genai.Client() # or genai.Client(vertexai=True, project=..., location=...)
329
+ response = client.models.generate_content(model="gemini-3.1-flash-lite", contents="Plan my day")
330
+ ```
331
+
332
+ Each `generate_content` call (sync or `client.aio`) is a
333
+ `generate_content {model}` span. Wrap your loop in `struct.agent()` and
334
+ your tools in `struct.tool()` as for the other LLM SDKs.
335
+
336
+ Google ADK traces itself (agents, tools and model calls) through the
337
+ global OpenTelemetry provider, which Struct owns or attaches to in the
338
+ default full-app mode, so ADK runs need no extra setup; Struct adds no
339
+ second span for a model call ADK already traces. With
340
+ `auto_instrument=False` and no host OpenTelemetry, Struct sees ADK's
341
+ model calls but not its agent and tool spans. ADK puts message content in its spans by default; set
342
+ ADK's own `ADK_CAPTURE_MESSAGE_CONTENT_IN_SPANS=false` to stop that.
343
+
344
+ If you install `opentelemetry-instrumentation-google-genai` yourself,
345
+ Struct leaves google-genai to it. Default full-app mode also leaves Datadog-owned
346
+ google-genai bindings alone; explicit AI-only mode retains Struct’s isolated
347
+ instrumentation.
348
+
349
+ #### LlamaIndex
350
+
351
+ ```python
352
+ from struct_sdk import struct
353
+ struct.init(ingest_key="pk-...", service_name="my-agent")
354
+
355
+ from llama_index.core.agent.workflow import FunctionAgent
356
+ from llama_index.llms.openai import OpenAI
357
+
358
+ llm = OpenAI(
359
+ model="gpt-4.1-mini",
360
+ # Streamed calls report token usage only when asked to.
361
+ additional_kwargs={"stream_options": {"include_usage": True}},
362
+ )
363
+ agent = FunctionAgent(name="weather", tools=[...], llm=llm)
364
+ result = await agent.run(user_msg="Weather in Paris?")
365
+ ```
366
+
367
+ Each run is an `invoke_agent` span with its model calls (`chat`), tool
368
+ calls (`execute_tool`) and retrievals (`retrieval`) under it, even though
369
+ LlamaIndex runs workflow steps on separate tasks. Wrap the run in
370
+ `struct.agent(session_id=...)` to group runs into a conversation.
371
+ Completions, embeddings and query engines are not spans of their own;
372
+ their LLM and retriever calls are.
373
+
374
+ #### CrewAI
375
+
376
+ ```python
377
+ from struct_sdk import struct
378
+ struct.init(ingest_key="pk-...", service_name="my-crew")
379
+
380
+ from crewai import Agent, Crew, Task
381
+ crew = Crew(name="weather-crew", agents=[...], tasks=[...])
382
+ with struct.agent(name="support", session_id=conversation_id):
383
+ crew.kickoff()
384
+ ```
385
+
386
+ The crew and each agent execution are `invoke_agent` spans, with the
387
+ agent's model calls (`chat`) and tool calls (`execute_tool`) under it,
388
+ across the threads CrewAI runs agents and async tasks on. Agent spans are
389
+ named by the role as configured; the resolved goal (with kickoff inputs)
390
+ is recorded only when content is captured. A cancelled `akickoff()` (or an
391
+ interrupted `kickoff()`) ends the run's spans as errors. Requires
392
+ CrewAI 1.15.13 or later (older releases are left alone); there is no
393
+ `struct-sdk[crewai]` extra. Install CrewAI separately as you already do;
394
+ the SDK activates the integration when it is present.
395
+
396
+ #### AutoGen
397
+
398
+ ```python
399
+ from struct_sdk import struct
400
+ struct.init(ingest_key="pk-...", service_name="my-agent") # full-app mode, the default
401
+
402
+ from autogen_agentchat.agents import AssistantAgent
403
+ from autogen_ext.models.openai import OpenAIChatCompletionClient
404
+
405
+ agent = AssistantAgent(name="weather", model_client=OpenAIChatCompletionClient(model="gpt-4.1-mini"), tools=[...])
406
+ async with struct.agent(name="support", session_id=conversation_id):
407
+ await agent.run(task="Weather in Paris?")
408
+ ```
409
+
410
+ AutoGen (0.7+) traces its agents and tools through the global
411
+ OpenTelemetry provider, which Struct owns or attaches to in the default
412
+ full-app mode; with `auto_instrument=False` only Struct's `chat` spans
413
+ arrive. The `struct.agent()` session reaches AutoGen's own spans only when
414
+ Struct owns the provider (owned full-app mode); attached to your provider,
415
+ only Struct's `chat` spans carry it. Streamed model calls
416
+ (`model_client_stream=True`) get no AutoGen span; the `openai` integration
417
+ traces them. This is Microsoft AutoGen (`autogen-agentchat`), not AG2.
418
+
419
+ #### AG2
420
+
421
+ ```python
422
+ from struct_sdk import struct
423
+ struct.init(ingest_key="pk-...", service_name="my-agents")
424
+
425
+ from autogen import ConversableAgent
426
+ with struct.agent(name="support", session_id=conversation_id):
427
+ user.initiate_chat(assistant, message="Weather in Paris?")
428
+ ```
429
+
430
+ AG2 0.x (`pip install "ag2[openai]<1"`, imported as `autogen`) gets a
431
+ `chat` span per model call and an `execute_tool` span per tool call;
432
+ wrap the conversation in `struct.agent()` for an agent span and a
433
+ session. The `chat` span describes the request AG2 actually sent (the
434
+ client that answered, its merged config, instantiated templates), for
435
+ streamed calls and the V2 clients too; Anthropic and OpenAI (Responses
436
+ and Chat Completions) calls that are not streamed keep the provider's
437
+ cache counts. Async chats (`a_initiate_chat`)
438
+ run model calls on an executor thread. OTel thread instrumentation carries
439
+ trace parentage; the wrapper can recover the session from a carried current
440
+ Struct span. Owned full-app mode enables that instrumentor when installed;
441
+ a host can also supply it. Without thread-context propagation, model spans
442
+ start new traces without the session. AG2's own
443
+ OpenTelemetry (`autogen.opentelemetry` in 0.x, `TelemetryMiddleware` in
444
+ 1.x) also works: pass it `trace.get_tracer_provider()` (Struct's in owned
445
+ mode, yours when Struct attaches to it) and Struct stamps the session on
446
+ its spans and adds no duplicates under them.
447
+
243
448
  #### LangChain / LangGraph (with an agent or graph)
244
449
 
245
450
  ```python
@@ -252,6 +457,18 @@ from langgraph.prebuilt import create_react_agent
252
457
  # execute_tool spans. BaseRetriever.invoke gets retrieval spans.
253
458
  ```
254
459
 
460
+ - Sub-graphs and `create_agent` sub-agents nest under their parent graph;
461
+ `configurable.thread_id` is the session, so a run resumed after an
462
+ `interrupt()` stays in the same session.
463
+ - `interrupt()` and `Command(graph=Command.PARENT)` are control flow: the
464
+ tool or graph span is not marked as an error.
465
+ - While a tool runs, its `execute_tool` span is the current span: your own
466
+ spans inside the tool, and full-app database/HTTP spans, nest under it.
467
+ Cancellation restores the caller’s context even when LangChain skips its
468
+ end/error callbacks. Plain function nodes get no Struct span; their work
469
+ inherits existing host or `struct.agent()` context. Automatic graph spans
470
+ end on interrupt; resume starts a separate trace only without an enclosing span.
471
+
255
472
  ##### Recommended pattern: wrap LangChain entry points in `struct.agent`
256
473
 
257
474
  For multi-turn HTTP-style usage (every request continues the same
@@ -343,6 +560,34 @@ result = await registry[block.name](**block.input) # arguments + result captur
343
560
  `anthropic.Anthropic`, `anthropic.AsyncAnthropic`, and the bedrock/vertex
344
561
  clients are all auto-instrumented for chat spans.
345
562
 
563
+ #### MCP
564
+
565
+ ```python
566
+ from struct_sdk import struct
567
+ struct.init(ingest_key="pk-...", service_name="support-agent")
568
+
569
+ from mcp import ClientSession
570
+ async with struct.agent(name="support", session_id=conversation_id):
571
+ result = await session.call_tool("get_weather", {"city": "Paris"})
572
+ ```
573
+
574
+ Every `ClientSession.call_tool` (mcp 1.x and 2.x, including the OpenAI
575
+ Agents SDK's MCP servers) becomes a `tools/call get_weather` CLIENT span
576
+ with `mcp.method.name`, `gen_ai.operation.name = execute_tool` and the
577
+ request's `jsonrpc.request.id`. Its trace context is added
578
+ to the request's `params._meta` (`traceparent`, `tracestate`, `baggage`),
579
+ so an instrumented MCP server continues the same trace. On mcp 1.x, trace
580
+ keys you pass in `meta` win; mcp 2.x writes the trace keys again itself on
581
+ every transport but its in-process `Client`, from its own `MCP send`
582
+ span when your global tracer provider records (that span is a child of
583
+ Struct's, so the server stays in the trace). mcp before 1.19 has no `meta`
584
+ parameter: those calls get the span without propagation.
585
+
586
+ The first MCP call inside a `struct.tool()` scope is recorded on that tool
587
+ span, including its outcome; later calls get child spans. An `isError`
588
+ result is `error.type = tool_error`; a JSON-RPC error records its code in
589
+ `error.type` and `rpc.response.status_code`.
590
+
346
591
  #### Parallel tool calls — pass `tool_call_id` explicitly
347
592
 
348
593
  When you execute an assistant turn's tool calls **sequentially** — one
@@ -396,6 +641,30 @@ When you do use `ChatAnthropic` *and* have the `anthropic` SDK installed,
396
641
  the chat span comes from the LangChain layer (single span); the Anthropic
397
642
  instrumentation skips its own to avoid duplicates.
398
643
 
644
+ #### LiteLLM
645
+
646
+ ```python
647
+ from struct_sdk import struct
648
+ struct.init(ingest_key="pk-...", service_name="my-agent")
649
+
650
+ import litellm # wrapped on import; a `completion` imported before init() is not traced
651
+
652
+ async with struct.agent(name="my-agent"):
653
+ response = await litellm.acompletion(model="anthropic/claude-sonnet-5", messages=[...])
654
+ async with struct.tool(name="search"):
655
+ ...
656
+ ```
657
+
658
+ Each call is one `chat {model}` span, whichever provider LiteLLM routes
659
+ to. A framework that owns the call (LangChain, pydantic-ai, the OpenAI
660
+ Agents SDK) keeps its own span and LiteLLM adds none. LiteLLM's own
661
+ `litellm.callbacks = ["otel"]` would add a second, LiteLLM-shaped span;
662
+ leave it off when Struct is on. `struct.init()` does not import LiteLLM
663
+ (its import fetches the model cost map over the network): it wraps
664
+ `litellm.completion` and `litellm.acompletion` when your code imports
665
+ `litellm`. `pip install "struct-sdk[litellm]"` installs LiteLLM with its
666
+ `mcp` extra, which recent LiteLLM releases need for calls that pass `tools`.
667
+
399
668
  ## Content capture
400
669
 
401
670
  The SDK supports four capture modes controlling how prompt/response
@@ -411,9 +680,10 @@ struct.init(
411
680
  )
412
681
  ```
413
682
 
414
- - **`EVENT_ONLY`**: per-message content lands on OTel log
415
- records (`gen_ai.{user,assistant,system,tool}.message`,
416
- `gen_ai.choice`). Spans carry metadata only.
683
+ - **`EVENT_ONLY`**: each model call's content lands on one OTel log record,
684
+ `gen_ai.client.inference.operation.details` (input and output messages,
685
+ system instructions, tool definitions, as structured values). Spans carry
686
+ metadata only.
417
687
  - **`SPAN_ONLY`**: content on span attributes (`gen_ai.input.messages`,
418
688
  `gen_ai.output.messages`).
419
689
  - **`SPAN_AND_EVENT`**: both.
@@ -484,13 +754,15 @@ Emits attributes per the OTel GenAI semantic conventions:
484
754
 
485
755
  The OTel conventions ask instrumentations to document the error values
486
756
  they report ("Instrumentations SHOULD document the list of errors they
487
- report"). This SDK emits exactly two kinds of value, both alongside
757
+ report"). This SDK emits exactly four kinds of value, all alongside
488
758
  span `StatusCode.ERROR`:
489
759
 
490
760
  | Value | When | Meaning |
491
761
  | --- | --- | --- |
492
762
  | exception class name (e.g. `RuntimeError`, `httpx.ConnectError`) | the instrumented call raised | We failed to execute the request. Also records an OTel exception event. |
493
- | `tool_error` | an `execute_tool` span whose result signalled failure **in band** — Anthropic `tool_result` blocks with `is_error: true`, MCP `CallToolResult.isError`, or a LangChain `ToolMessage` with `status="error"` | The tool ran and reported a failure back to the model (bad arguments, a domain "no"), so the model can self-correct. No exception object exists, so no class name is available. |
763
+ | `tool_error` | an `execute_tool` span whose result signalled failure **in band** — Anthropic `tool_result` blocks with `is_error: true`, MCP `CallToolResult.isError` (also on MCP `tools/call` spans), or a LangChain `ToolMessage` with `status="error"` | The tool ran and reported a failure back to the model (bad arguments, a domain "no"), so the model can self-correct. No exception object exists, so no class name is available. |
764
+ | JSON-RPC error code (e.g. `-32602`) | an MCP `tools/call` span whose request got a JSON-RPC error response | Also in `rpc.response.status_code`; the status carries the error's message (MCP semantic conventions). |
765
+ | `_OTHER` | an OpenAI Agents SDK span the Agents SDK marked as failed (a model call, tool, agent or the whole run), or one still open when its trace ended | The Agents SDK reports the failure as a message, not an exception, so no class name is available. `_OTHER` is the semantic conventions' fallback value. The message goes in the span status. |
494
766
 
495
767
  `tool_error` is a deliberate low-cardinality sentinel, which the
496
768
  `error.type` convention explicitly permits ("another low-cardinality