Repository navigation
fix(strands): tolerate the 1.60.0 invocation baggage detach when a stream is closed from another context - #278
Merged
sipercai merged 1 commit intoOct 22, 2026
Conversation
… close strands-agents 1.60.0 attaches an OpenTelemetry baggage context in Agent.stream_async and detaches it from the async generator's finally block. That token is only valid in the context that started the stream, so an application that closes the stream from another context - for example asyncio.create_task(stream.aclose()), which copies the current context - makes opentelemetry.context.detach log "Failed to detach context" and raise ValueError. The instrumentation now keeps the framework's module-level context_api binding behind a proxy that pairs every attach with a private context probe. When the probe shows that the token belongs to another context the impossible reset is skipped, so closing a stream no longer records that error; when the token is still usable it is reset exactly as before, and attach/detach of tokens the proxy did not hand out are delegated unchanged. Frameworks without the baggage attach (strands-agents < 1.60.0) keep the untouched context API. Verified locally with strands-agents 1.60.0 (37 passed, previously the stream cleanup test failed with the reported ValueError), 1.59.0 and 1.50.2 (35 passed, 2 skipped each).
github-actions
Bot
requested review from
123liuziming,
Cirilla-zmh and
ralf0131
October 13, 2026 09:30
13 of 16 tasks
RichardoMrMu
force-pushed
the
fix/strands-baggage-context-detach
branch
from
October 13, 2026 09:45
b21d9e0 to
e36e46c
Compare
sipercai
approved these changes
Oct 22, 2026
sipercai
left a comment
Collaborator
There was a problem hiding this comment.
Validated the exact head with before/after reproduction of cross-task stream closure, the real Strands/OpenAI SDK reasoning and tool loop (two user turns per session, concurrent streaming sessions), cancellation and clean sibling/context checks, and source-to-cloud trace readback. The detach regression is fixed without changing business output, span count or token totals. The original plugin suite also passes against Strands 1.62.0 (37 tests). The separate pre-existing reasoning/Agent-output field issues are tracked in #282. Fresh-cache tox -e precommit passes with a clean worktree.
11 of 12 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
loongsuite-instrumentation-strandsnow tolerates the framework-owned OpenTelemetry baggage detachthat Strands runs when a stream is closed from another context. This fixes the
loongsuite-instrumentation-strands-latestfailures on Python 3.10-3.13.Root cause
strands-agents1.60.0 (published 2026-10-12T19:37Z) added an invocation level baggage context toAgent.stream_async:strands/agent/agent.py:1427-1432-baggage_ctx = context_api.get_current(),set_baggage(...)per configured attribute, then
baggage_token = context_api.attach(baggage_ctx)strands/agent/agent.py:1479-1480-finally: context_api.detach(baggage_token)ContextVar.resetonly accepts a token in the context that created it, so when the applicationcloses the async generator elsewhere the framework's
finallyruns in a copied context andopentelemetry.context.detachlogsFailed to detach contextand raisesValueError: <Token ...> was created in a different Context.The plugin wraps
Agent.stream_asyncand closes the wrapped stream, so closing it withasyncio.create_task(stream.aclose())(which copies the current context) reaches thatfinallyfromanother context.
tests/test_instrumentor.py::test_generator_exit_from_early_break_cleans_state_and_next_sibling,which asserts that stream cleanup leaves no detach failure behind, therefore started to fail with
1.60.0 only.
Observed while checking this branch (base 2026-10-13T03:26:35Z):
402984cecafb),jobs 115185430508 (3.13), 115185430132 (3.12), 115185429758 (3.11), 115185429600 (3.10) - every
strands-latestjob failed,strands-oldest(1.50.2) stayed green in the same run.(strands-agents 1.59.0).
No existing issue tracks this regression: #54 is the original instrumentation feature request, and the
failure was first noticed while #275's CI ran (unrelated to that PR's changes).
Fix
New
src/opentelemetry/instrumentation/strands/_compat.py:BaggageContextCompatreplacesstrands.agent.agent.context_apiwith a proxy only when theinstalled framework accepts
baggage_attributes(so strands-agents < 1.60.0 is left untouched),and restores the original binding on
uninstrument()._TolerantContextApipairs every frameworkattachwith a privateContextVarprobe.detachresets the real token when the probe proves the token is still valid in the current context; when
it is not, the impossible reset is skipped and nothing is logged. Tokens the proxy did not hand out
are delegated unchanged.
Tolerating the exception after the fact is not enough:
opentelemetry.context.detachlogsFailed to detach contextbefore it re-raises, so the error record would still appear. The probedecides before the call and uses only a private
ContextVar, so the OTel logging path is neverentered.
_instrumentinstalls the shim fail-open (a failure logs a warning and instrumentation continues),_uninstrumentrestores the binding, and the README/CHANGELOG describe the behaviour.Verification
Local runs at the same repository revision, only
strands-agentsdiffering (-p no:cacheprovider):test_generator_exit_from_early_break_cleans_state_and_next_sibling, sameValueErroras CI)RED discriminator: with
BaggageContextCompat.installdisabled, the same test fails again on 1.60.0(
1 failed), so the shim is what makes it pass.New tests cover the cross-context detach being tolerated while the value stays attached in the
attaching context, unrelated
ValueErrorstill propagating, frameworks withoutbaggage_attributesnever being patched, the instrumentor patching and restoring
context_api, and the framework'sinvocation baggage attach still happening once per invocation without leaking afterwards.
python -m ruff check instrumentation-loongsuite/loongsuite-instrumentation-strandsandpython -m ruff format --checkare clean (ruff 0.14.1, as pinned indev-requirements.txt).Notes
ContextVar.setbinding on theframework's own binding; the plugin's own tokens keep their exact pairing.
the detach when the context changed; the shim keeps the instrumentation green until then.
baggage is current for
invoke_agentspan start, but the plugin's stream context hooks attach theirown context, so nested plugin spans and model calls do not see that baggage. That is pre-existing
behaviour and is unchanged here.