Skip to content

feat(streaming): emit ReasoningDeltaEvent for reasoning/thinking deltas#2913

Open
adityasingh2400 wants to merge 6 commits into
openai:mainfrom
adityasingh2400:feat/reasoning-delta-stream-event-825
Open

feat(streaming): emit ReasoningDeltaEvent for reasoning/thinking deltas#2913
adityasingh2400 wants to merge 6 commits into
openai:mainfrom
adityasingh2400:feat/reasoning-delta-stream-event-825

Conversation

@adityasingh2400
Copy link
Copy Markdown
Contributor

Summary

When models like o3 or DeepSeek-R1 produce reasoning/thinking tokens during streaming, those deltas currently only surface as raw RawResponsesStreamEvent wrappers around low-level response.reasoning_summary_text.delta or response.reasoning_text.delta events. To consume them, callers have to inspect .data.type and cast the event themselves — there's no clean signal in the StreamEvent union.

This PR adds ReasoningDeltaEvent to StreamEvent and emits it alongside the existing raw event so reasoning deltas are as easy to consume as message deltas.

Closes #825

What changed

  • Added ReasoningDeltaEvent dataclass to stream_events.py with delta, snapshot, and type fields
  • Updated StreamEvent type alias to include ReasoningDeltaEvent
  • Exported from agents/__init__.py
  • In run_internal/run_loop.py, the run_single_turn_streamed loop now emits a ReasoningDeltaEvent after each ResponseReasoningSummaryTextDeltaEvent (o-series) and ResponseReasoningTextDeltaEvent (DeepSeek/LiteLLM)
  • The snapshot field accumulates the full reasoning text so far in the turn, so callers don't have to maintain their own buffer
  • Raw events are still emitted unchanged — fully backwards compatible

Usage example

from agents import Agent, Runner
from agents.stream_events import ReasoningDeltaEvent

agent = Agent(name="thinker", model="o3-mini")
result = Runner.run_streamed(agent, "prove P != NP")

async for event in result.stream_events():
    if isinstance(event, ReasoningDeltaEvent):
        print(event.delta, end="", flush=True)

print()  # reasoning complete

Tests

Added tests/test_reasoning_delta_stream_event.py covering:

  • ReasoningDeltaEvent is emitted for reasoning items
  • Snapshot grows monotonically and ends with full text
  • No event emitted for plain text responses
  • Raw events still emitted alongside
  • Importable directly from agents
  • Correct dataclass fields

Also updated tests/test_stream_events.py::test_complete_streaming_events to account for the new event in the event sequence (count goes from 27 → 28).

Summary by CodeRabbit

  • New Features

    • Added ReasoningDeltaEvent for streaming incremental reasoning updates from AI agents, featuring:
      • delta: incremental reasoning fragment
      • snapshot: accumulated reasoning text
      • Emitted during agent streaming when reasoning is available
  • Tests

    • Added comprehensive tests validating reasoning delta streaming behavior and event accumulation

@github-actions github-actions Bot added enhancement New feature or request feature:core labels Apr 16, 2026
@seratch seratch marked this pull request as draft April 17, 2026 04:51
@adityasingh2400 adityasingh2400 marked this pull request as ready for review April 26, 2026 21:01
@adityasingh2400 adityasingh2400 force-pushed the feat/reasoning-delta-stream-event-825 branch from a2d4974 to 3a823e4 Compare April 26, 2026 21:05
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

This PR is stale because it has been open for 10 days with no activity.

@github-actions github-actions Bot added the stale label May 7, 2026
@adityasingh2400 adityasingh2400 force-pushed the feat/reasoning-delta-stream-event-825 branch from 3a823e4 to 5c36515 Compare May 8, 2026 00:06
@github-actions github-actions Bot removed the stale label May 8, 2026
@adityasingh2400
Copy link
Copy Markdown
Contributor Author

@seratch friendly bump — this has been waiting on review since #825 was opened. Happy to rebase or split if it's blocked on scope.

@github-actions
Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for 10 days with no activity.

@github-actions github-actions Bot added the stale label May 22, 2026
Ubuntu and others added 4 commits May 21, 2026 22:10
…as (openai#825)

Add a new ReasoningDeltaEvent to StreamEvent so callers can react to
reasoning/thinking tokens in real time without unpacking low-level raw
response events.

The event is emitted whenever a ResponseReasoningSummaryTextDeltaEvent
(o-series extended thinking via the Responses API) or a
ResponseReasoningTextDeltaEvent (third-party models like DeepSeek-R1
via LiteLLM) passes through the stream.  The underlying
RawResponsesStreamEvent is still emitted as well, so nothing breaks for
consumers that already inspect raw events.

Fields:
  delta    - the incremental text fragment from this chunk
  snapshot - full accumulated reasoning text so far in this turn
  type     - always 'reasoning_delta'

Closes openai#825
- Import ResponseCreatedEvent and reset _reasoning_snapshot to "" when
  a ResponseCreatedEvent is received inside the retry stream loop, fixing
  the bug where snapshot text would be duplicated across retries
- In test_reasoning_delta_event_type_field: add found=False flag and
  assert found after the loop so the test properly fails when no
  ReasoningDeltaEvent is emitted
The two stream-event tests were only asserting on data conditional on a
ReasoningDeltaEvent being emitted at all, so a regression that stopped
emitting the event entirely would have passed silently.

  * test_reasoning_delta_snapshot_accumulates: assert that snapshots is
    non-empty before checking monotonic length and the "Hello world"
    inclusion (previously gated on `if snapshots:`).
  * test_no_reasoning_delta_event_without_reasoning: count yielded events
    and assert the stream produced at least one, so the negative
    not-isinstance assertion can't pass on an empty event stream.

Picked up the remaining nitpicks from the CodeRabbit review of PR #3.
@adityasingh2400 adityasingh2400 force-pushed the feat/reasoning-delta-stream-event-825 branch from 5c36515 to 5b04cf8 Compare May 22, 2026 05:10
@adityasingh2400
Copy link
Copy Markdown
Contributor Author

Bumping after the stale-bot ping. I just rebased onto current main and CI is still green across the 5-file diff. The change stays narrow, one new ReasoningDeltaEvent type, a small emit path in run_loop, and tests. Happy to split the docs/example out into a follow-up PR if that helps land the core type sooner. cc @rm-openai @seratch when you have a moment.

@seratch
Copy link
Copy Markdown
Member

seratch commented May 22, 2026

@adityasingh2400 Thanks for sharing this. This change could drastically increase the number of events that could be sent, so we hesitate changing the default behavior in this way for now.

@adityasingh2400
Copy link
Copy Markdown
Contributor Author

Thanks for the read. Totally fair point on event volume. Would you accept the change behind an opt-in flag so the default stream stays unchanged? I can gate emission behind a RunConfig flag (e.g. emit_reasoning_deltas defaulting to False) so anyone with a reasoning UI can opt in, and existing consumers see identical event counts.

If you'd prefer to close this and reopen as the gated version, happy to do that too.

@github-actions github-actions Bot removed the stale label May 24, 2026
…ning_deltas

Default the new ReasoningDeltaEvent emission to off so the streamed event
count is unchanged for existing consumers. Opt in via
RunConfig.emit_reasoning_deltas=True to receive reasoning deltas without
unwrapping the raw events. Addresses the event-volume concern raised in review.
@adityasingh2400
Copy link
Copy Markdown
Contributor Author

I went ahead and gated this behind an opt-in flag so the default behavior is unchanged. I added RunConfig.emit_reasoning_deltas, defaulting to False. When it is False the runner emits exactly the same events as before, so existing consumers see no change in event volume. Setting it to True turns on ReasoningDeltaEvent for anyone building a reasoning UI.

The two emission points in run_single_turn_streamed are now wrapped in that flag check, and I added a test asserting no ReasoningDeltaEvent is emitted by default plus updated the existing streaming tests to opt in. Locally ruff format, ruff lint, mypy on the two changed source files, and the full tests/test_stream_events.py and tests/test_reasoning_delta_stream_event.py suites all pass. Let me know if you would prefer the flag live somewhere other than RunConfig.

Adding ReasoningDeltaEvent to the StreamEvent union widened PrintableEvent,
so mypy could no longer prove the trailing match handled a RunItemStreamEvent.
Add an early return for ReasoningDeltaEvent, mirroring the existing
RawResponsesStreamEvent guard, which restores narrowing and fixes typecheck.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request feature:core

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Yield reasoning delta in the response or add hooks to handle

2 participants