Skip to content

fix(sdk): keep the workflow span current across a streamed agent run - #5708

Open
mmabrouk wants to merge 1 commit into
mainfrom
fix/sdk-streaming-usage-span
Open

fix(sdk): keep the workflow span current across a streamed agent run#5708
mmabrouk wants to merge 1 commit into
mainfrom
fix/sdk-streaming-usage-span

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Aug 3, 2026

Copy link
Copy Markdown
Member

The symptom

An agent run on the streaming path records no tokens and no cost on its workflow span. Batch runs are fine, which is why this stayed invisible.

Measured: zero out of roughly 1,300 streaming runs across two independent stacks since 2026-07-08. The playground still showed a cost, because the playground reads the number the harness streams to the browser, not the trace.

The cause

Commit 8e99171 (2026-07-07, "Add time-based run limits to the runner") added an SSE keepalive so a proxy would not drop the connection during a silent gap, such as a tool call that runs for minutes. To race a keepalive timer against each chunk, it changed async for chunk in aiter into a hand-driven loop that wraps every pull in asyncio.ensure_future(iterator.__anext__()).

Every asyncio task gets its own copy of the context. The instrumentation attaches the OpenTelemetry context and activates the workflow span from inside the generator body, so that activation landed in the first pull's private copy. Every later pull started from a copy that had never seen it.

Chunk one recorded. Everything after it ran against a NonRecordingSpan, including the finally block where record_usage writes usage. The writes were silently discarded.

Observed directly in a test against the unfixed code:

the workflow span stopped being current mid-stream:
['workflow', 'NonRecordingSpan', 'NonRecordingSpan', 'NonRecordingSpan', 'NonRecordingSpan']

The fix

Share one context across every pull. The stream captures contextvars.copy_context() once when the generator is first driven and creates every pull task with it. That restores the property plain iteration had, where the whole upstream ran in the consumer's single context. Backpressure stays lock-step with no read-ahead, and cancellation and cleanup are untouched. It also removes the "Token created in a different Context" detach errors that show up in the pre-fix logs.

Stop depending on ambient context for usage. The handler captures the workflow span at entry and passes that reference into record_usage, so this class of bug cannot silence usage again. The existing early return on a falsy total and the truthy-cost guard are unchanged. An override that takes only the usage argument still works, so the service's own recorders are unaffected.

Verification

  • run-tests.py --layer unit: 1,905 passed, 10 xfailed.
  • The service's agent tests against the editable SDK: 100 passed, confirming the recorder seam stays backward compatible.
  • Two new test files. Both fail against the unfixed code and pass against the fix, verified by swapping the old file in and back.
    • The stream test asserts the same recording span is current on every chunk, across a keepalive gap at a 0.02s interval, with no frame dropped or duplicated, and that a disconnect cancels the outstanding pull without leaking a task.
    • The usage test drains the handler's stream with one task per pull, the adverse condition, after the span is no longer current.

Notes for the reviewer

  • The same dead-context problem would silence any span created mid-stream by future code. Only the usage write is insured here. The plain SSE and NDJSON paths in decorators/routing.py do not use a per-pull task and are unaffected.
  • _bind_workflow_span inspects the recorder's signature to stay compatible with (usage)-only overrides. The cleaner version widens the callback type, which would have meant touching services/oss callers and was left out of scope.
  • Cleanup on disconnect is unchanged: cancelling the pending pull is fire and forget, so an upstream finally that awaits may be truncated. Worth a separate look.

Related

Part of a set of three independent fixes for the same reported problem, that an agent run shows a cost in the playground and none in the trace. The other two are the API ingest fix and the runner usage fix.

Since commit 8e99171 (2026-07-07) an agent run on the streaming path has recorded
no tokens and no cost on its workflow span. Measured: zero out of roughly 1,300
streaming runs across two independent stacks since 2026-07-08. Batch runs were never
affected, which is why the bug stayed invisible.

That commit added an SSE keepalive so a proxy would not drop a connection during a
silent gap, such as a tool call that runs for minutes. To race a keepalive timer
against each chunk, it began driving the response generator with one asyncio task per
pull. Every task gets its own copy of the context. The instrumentation attaches the
OpenTelemetry context and activates the workflow span from inside the generator body,
so that activation landed in the first pull's private copy and every later pull
started from a copy that had never seen it. Chunk one recorded. Everything after it,
including the `finally` block where usage is written, ran against a non-recording
span and was silently discarded.

Two changes.

The stream now captures one context when the generator is first driven and creates
every pull task with it, so all pulls share a single context. That restores the
property the old plain iteration had, keeps the existing lock-step backpressure with
no read-ahead, and leaves cancellation and cleanup alone. It also removes the
"Token created in a different Context" detach errors visible in the pre-fix logs.

`record_usage` no longer depends on the ambient context. The handler captures the
workflow span at entry and passes that reference, so this class of bug cannot silence
usage again. The existing early return on a falsy total and the truthy-cost guard are
unchanged, and an override that takes only the usage argument still works.

Tests: 1,905 passing through the canonical runner, and 100 passing in the service's
own agent tests, which confirms the recorder seam stays backward compatible. The two
new test files fail against the unfixed code and pass against the fix. The stream test
asserts the same recording span is current on every chunk, across a keepalive gap,
and that no frame is dropped or duplicated.

Claude-Session: https://claude.ai/code/session_01RkWWQUNNzRbaB5jnCAdjYA
@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Aug 3, 2026
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview Aug 3, 2026 8:39pm

Request Review

@dosubot dosubot Bot added bug Something isn't working python Pull requests that update Python code tests labels Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 927ee299-87d9-45b5-a446-daa247048e4e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Preserved workflow tracing context during streaming responses, including keepalive events.
    • Ensured usage metrics remain attached to the correct workflow trace for both streaming and batch executions.
    • Improved stream shutdown behavior by canceling in-flight operations cleanly when a connection ends.
  • Tests

    • Added coverage for trace preservation, usage recording, SSE keepalive frames, and stream teardown.

Walkthrough

The change binds usage recording to the workflow span and preserves execution context for Vercel SSE pulls. Tests cover streaming, batch execution, keepalive frames, cancellation, span attributes, and legacy recorders.

Changes

Workflow span usage recording

Layer / File(s) Summary
Bind usage recording to workflow spans
sdks/python/agenta/sdk/agents/tracing.py, sdks/python/agenta/sdk/agents/handler.py, sdks/python/oss/tests/pytest/unit/agents/test_usage_span_binding.py
record_usage accepts an optional explicit span. The handler captures the workflow span and binds it to streaming and batch usage recording. Tests cover modern and legacy recorders.
Preserve context during SSE pulls
sdks/python/agenta/sdk/agents/adapters/vercel/sse.py, sdks/python/oss/tests/pytest/unit/agents/adapters/test_vercel_sse_context.py
Keepalive SSE pulls run in a captured execution context. Tests cover span continuity, SSE framing, disconnect cancellation, and task cleanup.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AgentHandler
  participant WorkflowSpan
  participant UsageRecorder
  participant SSEAdapter
  participant UpstreamIterator

  AgentHandler->>WorkflowSpan: capture active workflow span
  AgentHandler->>UsageRecorder: create span-bound recorder
  SSEAdapter->>WorkflowSpan: preserve execution context
  SSEAdapter->>UpstreamIterator: pull stream item
  UpstreamIterator-->>SSEAdapter: item or keepalive timeout
  AgentHandler->>UsageRecorder: record usage
  UsageRecorder->>WorkflowSpan: write usage attributes
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the primary fix for workflow span continuity during streamed agent runs.
Description check ✅ Passed The description directly explains the streaming span issue, the fix, compatibility considerations, and verification results.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sdk-streaming-usage-span

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mmabrouk

mmabrouk commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

This is one of three independent fixes for the same reported problem: an agent run shows a cost in the playground and nothing in the trace. They can be reviewed and merged separately, in any order.

Live end-to-end verification of all three together on a running stack is in progress, and I will post the result here.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

@mmabrouk I will review #5708. I will assess the SDK change independently from #5709 and #5710.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-pr-5708.up.railway.app/w
Project agenta-oss-clone-spike
Image tag pr-5708-5c5decc
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-08-03T20:50:20.251Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working python Pull requests that update Python code size:M This PR changes 30-99 lines, ignoring generated files. tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant