struct-sdk 0.2.13__tar.gz → 0.2.15__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.13
3
+ Version: 0.2.15
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "struct-sdk"
7
- version = "0.2.13"
7
+ version = "0.2.15"
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"
@@ -161,7 +161,8 @@ def _attach_current_span(
161
161
  """Make ``span`` the current OTel span so children — customer-created raw spans
162
162
  AND nested SDK spans — nest under it (ambient-context honoring, good-citizen
163
163
  behavior). Returns ``(token, enter_task)`` for a later task-safe detach."""
164
- token = _otel_context.attach(trace.set_span_in_context(span))
164
+ ctx = trace.set_span_in_context(span)
165
+ token = _otel_context.attach(ctx)
165
166
  return token, _current_task_or_none()
166
167
 
167
168
 
@@ -657,51 +658,34 @@ class _AgentContext:
657
658
  and enclosing_agent_span is not None
658
659
  )
659
660
 
660
- # ── Foreign-context guard (top-level agent run) ──────────────────
661
- # A top-level agent run (no enclosing STRUCT agent) must NOT inherit
662
- # whatever OTel span happens to be active. That active span may be a
663
- # FOREIGN span leaked across an async boundary — e.g. a tool span from
664
- # a PRIOR turn that a Temporal/queue context propagator carried into
665
- # this later wake-up. Inheriting it would mis-parent this brand-new
666
- # turn UNDER unrelated, long-finished work (it would show up nested in
667
- # that old tool call instead of as its own turn).
661
+ # ── Parentage: PURE NEST (Option D) ──────────────────────────────
662
+ # A non-break-out agent nests under whatever OTel span is active — the
663
+ # host's ambient context exactly like every peer LLM/agent SDK
664
+ # (Vercel, Pydantic AI, OpenLLMetry, OpenInference, Langfuse). We do NOT
665
+ # second-guess the active span or unilaterally re-root: the old
666
+ # `foreign_root` heuristic detached from ANY active span and so ripped
667
+ # agents out of legitimate host request traces. Cross-conversation
668
+ # grouping is carried by gen_ai.conversation.id, never by trace
669
+ # parentage.
668
670
  #
669
- # So: when there is no enclosing Struct agent but some other span is
670
- # active, start a FRESH ROOT trace and record that active span as a
671
- # causal OTel Link (preserving "this turn was triggered by that") —
672
- # never as the parent.
673
- #
674
- # Genuine in-run sub-agents are unaffected: they always run with an
675
- # enclosing Struct agent in scope (``enclosing_session_id`` set, because
676
- # the contextvar is live in the same task), so they fall through to the
677
- # inherit path below and stay nested in the same trace.
678
- active_span_context = trace.get_current_span().get_span_context()
679
- foreign_root = (
680
- not break_out
681
- and enclosing_session_id is None
682
- and active_span_context.is_valid
683
- )
684
-
671
+ # If a runtime propagates a stale/unwanted span across a boundary e.g.
672
+ # our own persistent_agent's long-lived Temporal workflow span it is
673
+ # cleared at the SOURCE (edgedive-server resets the OTel context at the
674
+ # Temporal activity boundary so no foreign span is active here), never
675
+ # defended against in the SDK. (A provenance-gated detach — re-root only
676
+ # a leaked Struct-OWNED span, nest under host spans is a documented
677
+ # follow-up, deferred until a real customer needs it.)
685
678
  if break_out:
686
- # Start a fresh root span: pass context=trace.Context() to create
687
- # a span with no parent (new TraceId) while keeping the current
688
- # context vars readable for the span's children.
679
+ # Explicit different session while nested under another Struct agent:
680
+ # start a fresh root trace and carry a spawned-by Link to the
681
+ # enclosing agent. This is the one deliberate re-root, and it is
682
+ # opt-in (the caller passed an explicit, differing session_id).
689
683
  assert enclosing_agent_span is not None # narrowing for mypy
690
- links = [trace.Link(enclosing_agent_span.get_span_context())]
691
- self._span = tracer.start_span(
692
- f"invoke_agent {agent_name}",
693
- kind=trace.SpanKind.INTERNAL,
694
- context=_OtelContext(), # empty context → new root trace
695
- links=links,
696
- )
697
- elif foreign_root:
698
- # New root trace; the leaked/foreign active span becomes a Link
699
- # (causal origin), NOT this turn's parent.
700
684
  self._span = tracer.start_span(
701
685
  f"invoke_agent {agent_name}",
702
686
  kind=trace.SpanKind.INTERNAL,
703
687
  context=_OtelContext(), # empty context → new root trace
704
- links=[trace.Link(active_span_context)],
688
+ links=[trace.Link(enclosing_agent_span.get_span_context())],
705
689
  )
706
690
  else:
707
691
  self._span = tracer.start_span(
@@ -104,10 +104,25 @@ def patch(sdk: StructSDK) -> None:
104
104
  *args: Any,
105
105
  **kwargs: Any,
106
106
  ) -> Any:
107
- inheritable_callbacks = _inject_handler(
108
- inheritable_callbacks, _active_handler
109
- )
110
- local_callbacks = _strip_struct(local_callbacks)
107
+ # Our handler injection/stripping must NEVER break the host's
108
+ # configure(). It runs on the invoke/stream/batch path and,
109
+ # unlike the on_* callbacks, has no ``raise_error=False`` net.
110
+ # _inject_handler / _strip_struct read ``h.name`` / ``.handlers``
111
+ # on host-owned callback objects via ``getattr(..., default)``,
112
+ # which swallows only AttributeError — a getter raising anything
113
+ # else (hostile/lazy/proxy handler) would abort a host call that
114
+ # LangChain's own configure (which never reads ``.name``) would
115
+ # have completed. Degrade ATOMICALLY: on any failure fall through
116
+ # with the ORIGINAL args (both rebind only if both succeed).
117
+ # Mirrors langchain.ts's resolveConfigureHandlerArgs guard.
118
+ try:
119
+ new_inheritable = _inject_handler(
120
+ inheritable_callbacks, _active_handler
121
+ )
122
+ new_local = _strip_struct(local_callbacks)
123
+ inheritable_callbacks, local_callbacks = new_inheritable, new_local
124
+ except Exception: # noqa: BLE001 — never break the host's configure()
125
+ pass
111
126
  return orig_func(
112
127
  cls,
113
128
  inheritable_callbacks,
File without changes
File without changes
File without changes