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.
- {struct_sdk-0.3.2 → struct_sdk-0.4.13}/.gitignore +3 -0
- {struct_sdk-0.3.2 → struct_sdk-0.4.13}/PKG-INFO +286 -14
- {struct_sdk-0.3.2 → struct_sdk-0.4.13}/README.md +271 -13
- {struct_sdk-0.3.2 → struct_sdk-0.4.13}/pyproject.toml +82 -11
- {struct_sdk-0.3.2 → struct_sdk-0.4.13}/src/struct_sdk/_auto.py +52 -2
- {struct_sdk-0.3.2 → struct_sdk-0.4.13}/src/struct_sdk/_genai_content.py +80 -114
- struct_sdk-0.4.13/src/struct_sdk/_operation_details.py +293 -0
- struct_sdk-0.4.13/src/struct_sdk/ag2.py +803 -0
- {struct_sdk-0.3.2 → struct_sdk-0.4.13}/src/struct_sdk/anthropic.py +59 -170
- struct_sdk-0.4.13/src/struct_sdk/autogen.py +403 -0
- {struct_sdk-0.3.2 → struct_sdk-0.4.13}/src/struct_sdk/core.py +78 -8
- struct_sdk-0.4.13/src/struct_sdk/crewai.py +889 -0
- struct_sdk-0.4.13/src/struct_sdk/google_genai.py +593 -0
- {struct_sdk-0.3.2 → struct_sdk-0.4.13}/src/struct_sdk/langchain.py +216 -162
- struct_sdk-0.4.13/src/struct_sdk/litellm.py +612 -0
- struct_sdk-0.4.13/src/struct_sdk/llamaindex.py +1347 -0
- struct_sdk-0.4.13/src/struct_sdk/mcp.py +376 -0
- struct_sdk-0.4.13/src/struct_sdk/openai.py +2015 -0
- struct_sdk-0.4.13/src/struct_sdk/openai_agents.py +881 -0
- {struct_sdk-0.3.2 → struct_sdk-0.4.13}/src/struct_sdk/pydantic_ai.py +94 -17
- struct_sdk-0.3.2/src/struct_sdk/openai.py +0 -810
- {struct_sdk-0.3.2 → struct_sdk-0.4.13}/LICENSE +0 -0
- {struct_sdk-0.3.2 → struct_sdk-0.4.13}/src/struct_sdk/__init__.py +0 -0
- {struct_sdk-0.3.2 → struct_sdk-0.4.13}/src/struct_sdk/_distro.py +0 -0
- {struct_sdk-0.3.2 → struct_sdk-0.4.13}/src/struct_sdk/claude_agent.py +0 -0
- {struct_sdk-0.3.2 → struct_sdk-0.4.13}/src/struct_sdk/temporal.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: struct-sdk
|
|
3
|
-
Version: 0.
|
|
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
|
|
107
|
-
(Responses API), pydantic-ai, the
|
|
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
|
|
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
|
|
231
|
-
|
|
232
|
-
arguments
|
|
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`**:
|
|
415
|
-
|
|
416
|
-
|
|
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
|
|
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
|
|
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
|