struct-sdk 0.2.0__tar.gz → 0.2.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: struct-sdk
3
- Version: 0.2.0
3
+ Version: 0.2.2
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
@@ -108,7 +108,7 @@ async with struct.agent(name="checkout"):
108
108
  | `langchain_core` `BaseChatModel` | `chat {model}` | Skipped when an underlying provider SDK is also instrumented (e.g. `ChatAnthropic` + `anthropic` → a single span). |
109
109
  | `langchain_core` `BaseTool` | `execute_tool {name}` | `tool_call_id` extracted from the LangChain ToolCall when present. |
110
110
  | `langchain_core` `BaseRetriever` | `retrieval {name}` | |
111
- | `langgraph` `Pregel` | `invoke_agent {name}` | Covers `create_react_agent` and custom graphs. `thread_id` `gen_ai.conversation.id`. |
111
+ | `langgraph` `Pregel` | `invoke_agent {name}` | Covers `create_react_agent`, `langchain.agents.create_agent`, and custom graphs. Reads conversation id from any of: `configurable.thread_id` (LangGraph canonical), or `metadata.{thread_id, session_id, conversation_id}` (LangSmith conventions). For multi-turn HTTP-style threading, wrap your entry point in [`struct.agent(session_id=conv_id)`](#recommended-pattern-wrap-langchain-entry-points-in-structagent) — that's the struct-native replacement for `ls.tracing_context(parent=run_tree)`. |
112
112
 
113
113
  ## Framework integration
114
114
 
@@ -156,6 +156,40 @@ from langgraph.prebuilt import create_react_agent
156
156
  # execute_tool spans. BaseRetriever.invoke gets retrieval spans.
157
157
  ```
158
158
 
159
+ ##### Recommended pattern: wrap LangChain entry points in `struct.agent`
160
+
161
+ For multi-turn HTTP-style usage (every request continues the same
162
+ conversation), wrap your request handler in
163
+ `struct.agent(session_id=conversation_id)`. This is the struct-native
164
+ replacement for `with ls.tracing_context(parent=run_tree):` and gives
165
+ you two things you can't get from `configurable.thread_id` alone:
166
+
167
+ 1. **Threading without per-call config plumbing.** Every nested
168
+ LangChain call inherits the conversation id via the SDK's ambient
169
+ contextvar — you don't have to ensure each `compiled_graph.ainvoke`
170
+ gets `thread_id` on its config.
171
+ 2. **One trace per request.** `struct.agent` creates a parent OTel span
172
+ so all the LangChain work for the request nests under one trace
173
+ (clean tree, "Subagents" / "Spawned by" UI links work). Without it,
174
+ each `.invoke()` becomes its own root trace, and the UI's session
175
+ list shows a non-deterministic agent name (`omni_agent`,
176
+ `LangGraph`, the first sub-agent it sees…).
177
+
178
+ Migrating from LangSmith:
179
+
180
+ ```python
181
+ # Before — LangSmith convention, fragments under struct-sdk
182
+ with ls.tracing_context(parent=run_tree):
183
+ await orchestrator.ainvoke(inputs, config=config)
184
+
185
+ # After — struct-native, threads correctly, no langsmith dep
186
+ async with struct.agent(session_id=conversation_id):
187
+ await orchestrator.ainvoke(inputs, config=config)
188
+ ```
189
+
190
+ Also works as a sync context manager (`with struct.agent(...)`) for
191
+ non-async handlers.
192
+
159
193
  ### LLM SDKs used directly — manual agent + tool scopes required
160
194
 
161
195
  When you call an LLM SDK directly (no agent framework wrapping it), only
@@ -269,8 +303,19 @@ async def run_checkout(order_id: str):
269
303
  return await charge()
270
304
  ```
271
305
 
272
- Nested agents are linked to their parent via the
273
- `struct.agent.parent_session_id` attribute on the inner span.
306
+ Sub-agents (e.g. a `create_agent` graph invoked from inside another
307
+ agent's tool body) record their parent via the
308
+ `struct.agent.parent_session_id` attribute on the inner `invoke_agent`
309
+ span. This powers the UI's "Spawned by" backlink, which works for any
310
+ nested invocation.
311
+
312
+ The parent's "Subagents" forward list — the inverse direction —
313
+ additionally requires that the nested invoke shares the outer agent's
314
+ trace. This works automatically when the outer tool is built with the
315
+ `@tool` decorator (or any callback-aware wrapping) since the nested
316
+ invoke inherits the parent's run state. Bare `Tool(...)` constructors
317
+ that bypass the callback chain can break the forward link; the
318
+ backlink still renders.
274
319
 
275
320
  ## Semantic conventions
276
321
 
@@ -62,7 +62,7 @@ async with struct.agent(name="checkout"):
62
62
  | `langchain_core` `BaseChatModel` | `chat {model}` | Skipped when an underlying provider SDK is also instrumented (e.g. `ChatAnthropic` + `anthropic` → a single span). |
63
63
  | `langchain_core` `BaseTool` | `execute_tool {name}` | `tool_call_id` extracted from the LangChain ToolCall when present. |
64
64
  | `langchain_core` `BaseRetriever` | `retrieval {name}` | |
65
- | `langgraph` `Pregel` | `invoke_agent {name}` | Covers `create_react_agent` and custom graphs. `thread_id` `gen_ai.conversation.id`. |
65
+ | `langgraph` `Pregel` | `invoke_agent {name}` | Covers `create_react_agent`, `langchain.agents.create_agent`, and custom graphs. Reads conversation id from any of: `configurable.thread_id` (LangGraph canonical), or `metadata.{thread_id, session_id, conversation_id}` (LangSmith conventions). For multi-turn HTTP-style threading, wrap your entry point in [`struct.agent(session_id=conv_id)`](#recommended-pattern-wrap-langchain-entry-points-in-structagent) — that's the struct-native replacement for `ls.tracing_context(parent=run_tree)`. |
66
66
 
67
67
  ## Framework integration
68
68
 
@@ -110,6 +110,40 @@ from langgraph.prebuilt import create_react_agent
110
110
  # execute_tool spans. BaseRetriever.invoke gets retrieval spans.
111
111
  ```
112
112
 
113
+ ##### Recommended pattern: wrap LangChain entry points in `struct.agent`
114
+
115
+ For multi-turn HTTP-style usage (every request continues the same
116
+ conversation), wrap your request handler in
117
+ `struct.agent(session_id=conversation_id)`. This is the struct-native
118
+ replacement for `with ls.tracing_context(parent=run_tree):` and gives
119
+ you two things you can't get from `configurable.thread_id` alone:
120
+
121
+ 1. **Threading without per-call config plumbing.** Every nested
122
+ LangChain call inherits the conversation id via the SDK's ambient
123
+ contextvar — you don't have to ensure each `compiled_graph.ainvoke`
124
+ gets `thread_id` on its config.
125
+ 2. **One trace per request.** `struct.agent` creates a parent OTel span
126
+ so all the LangChain work for the request nests under one trace
127
+ (clean tree, "Subagents" / "Spawned by" UI links work). Without it,
128
+ each `.invoke()` becomes its own root trace, and the UI's session
129
+ list shows a non-deterministic agent name (`omni_agent`,
130
+ `LangGraph`, the first sub-agent it sees…).
131
+
132
+ Migrating from LangSmith:
133
+
134
+ ```python
135
+ # Before — LangSmith convention, fragments under struct-sdk
136
+ with ls.tracing_context(parent=run_tree):
137
+ await orchestrator.ainvoke(inputs, config=config)
138
+
139
+ # After — struct-native, threads correctly, no langsmith dep
140
+ async with struct.agent(session_id=conversation_id):
141
+ await orchestrator.ainvoke(inputs, config=config)
142
+ ```
143
+
144
+ Also works as a sync context manager (`with struct.agent(...)`) for
145
+ non-async handlers.
146
+
113
147
  ### LLM SDKs used directly — manual agent + tool scopes required
114
148
 
115
149
  When you call an LLM SDK directly (no agent framework wrapping it), only
@@ -223,8 +257,19 @@ async def run_checkout(order_id: str):
223
257
  return await charge()
224
258
  ```
225
259
 
226
- Nested agents are linked to their parent via the
227
- `struct.agent.parent_session_id` attribute on the inner span.
260
+ Sub-agents (e.g. a `create_agent` graph invoked from inside another
261
+ agent's tool body) record their parent via the
262
+ `struct.agent.parent_session_id` attribute on the inner `invoke_agent`
263
+ span. This powers the UI's "Spawned by" backlink, which works for any
264
+ nested invocation.
265
+
266
+ The parent's "Subagents" forward list — the inverse direction —
267
+ additionally requires that the nested invoke shares the outer agent's
268
+ trace. This works automatically when the outer tool is built with the
269
+ `@tool` decorator (or any callback-aware wrapping) since the nested
270
+ invoke inherits the parent's run state. Bare `Tool(...)` constructors
271
+ that bypass the callback chain can break the forward link; the
272
+ backlink still renders.
228
273
 
229
274
  ## Semantic conventions
230
275
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "struct-sdk"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Struct agent observability SDK — auto-instruments AI agent frameworks with OpenTelemetry"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -142,8 +142,16 @@ def _create_common(
142
142
  try:
143
143
  result = yield f, args, kwargs
144
144
  except Exception as e:
145
- _safe(lambda: host_span.set_attribute("error.type", type(e).__name__),
146
- site="anthropic.create.enrich.error_type")
145
+ # Capture the type name OUTSIDE the lambda — ``except X as e``
146
+ # binds ``e`` only for the duration of the except block, but
147
+ # ``_safe`` is opaque to static analysis (ruff flags F841 +
148
+ # F821 thinking the lambda outlives the binding). Snapshotting
149
+ # to a local makes the closure capture trivially correct.
150
+ err_type = type(e).__name__
151
+ _safe(
152
+ lambda: host_span.set_attribute("error.type", err_type),
153
+ site="anthropic.create.enrich.error_type",
154
+ )
147
155
  raise
148
156
  _safe(
149
157
  lambda: _set_response_attrs(host_span, sdk, model, result, otel_logger),
@@ -201,12 +209,27 @@ def _create_common(
201
209
  try:
202
210
  result = yield f, args, kwargs
203
211
  except Exception as e:
204
- _safe(lambda: span.set_attribute("error.type", type(e).__name__),
205
- site="anthropic.create.error_type")
206
- _safe(lambda: span.set_status(StatusCode.ERROR, str(e)),
207
- site="anthropic.create.error_status")
208
- _safe(lambda: span.record_exception(e),
209
- site="anthropic.create.record_exception")
212
+ # Snapshot ``e`` outside the lambdas — ``except X as e`` unbinds
213
+ # ``e`` at end-of-block, and ruff (correctly) flags closures that
214
+ # reference it. The lambdas run synchronously inside the except
215
+ # via ``_safe``, so this is mechanically equivalent — just legible
216
+ # to static analysis. ``type(exc).__name__`` is cheap and won't
217
+ # raise; ``str(exc)`` runs inside ``_safe`` so a broken
218
+ # ``__str__`` can't mask the original exception we're re-raising.
219
+ exc = e
220
+ err_type = type(exc).__name__
221
+ _safe(
222
+ lambda: span.set_attribute("error.type", err_type),
223
+ site="anthropic.create.error_type",
224
+ )
225
+ _safe(
226
+ lambda: span.set_status(StatusCode.ERROR, str(exc)),
227
+ site="anthropic.create.error_status",
228
+ )
229
+ _safe(
230
+ lambda: span.record_exception(exc),
231
+ site="anthropic.create.record_exception",
232
+ )
210
233
  raise
211
234
  _safe(lambda: _set_response_attrs(span, sdk, model, result, otel_logger),
212
235
  site="anthropic.create.set_response_attrs")
File without changes
File without changes