-
Notifications
You must be signed in to change notification settings - Fork 0
feat(streaming): emit ReasoningDeltaEvent for reasoning/thinking deltas (#825) #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,147 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Tests for ReasoningDeltaEvent stream event (issue #825).""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| import pytest | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| from agents import Agent, Runner | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from agents.stream_events import ReasoningDeltaEvent, RawResponsesStreamEvent | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| from openai.types.responses.response_reasoning_item import ResponseReasoningItem, Summary | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| from .fake_model import FakeModel | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from .test_responses import get_text_message | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _make_reasoning_item(text: str) -> ResponseReasoningItem: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ResponseReasoningItem( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| id="rs_test", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="reasoning", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| summary=[Summary(text=text, type="summary_text")], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @pytest.mark.asyncio | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def test_reasoning_delta_event_emitted_during_streaming() -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ReasoningDeltaEvent is emitted when the model streams a reasoning summary delta.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| model = FakeModel() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| model.set_next_output([ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| _make_reasoning_item("Let me think..."), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| get_text_message("Answer"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| agent = Agent(name="A", model=model) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = Runner.run_streamed(agent, input="hi") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| reasoning_deltas: list[ReasoningDeltaEvent] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async for event in result.stream_events(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(event, ReasoningDeltaEvent): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| reasoning_deltas.append(event) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert len(reasoning_deltas) >= 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert all(isinstance(e.delta, str) for e in reasoning_deltas) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert all(isinstance(e.snapshot, str) for e in reasoning_deltas) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert all(e.type == "reasoning_delta" for e in reasoning_deltas) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @pytest.mark.asyncio | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def test_reasoning_delta_snapshot_accumulates() -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """The snapshot field grows monotonically across delta events.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| model = FakeModel() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| model.set_next_output([ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| _make_reasoning_item("Hello world"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| get_text_message("done"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| agent = Agent(name="A", model=model) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = Runner.run_streamed(agent, input="hi") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| snapshots: list[str] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async for event in result.stream_events(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(event, ReasoningDeltaEvent): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| snapshots.append(event.snapshot) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Each snapshot must be at least as long as the previous one | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| for i in range(1, len(snapshots)): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert len(snapshots[i]) >= len(snapshots[i - 1]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Last snapshot must contain the full reasoning text | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if snapshots: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert "Hello world" in snapshots[-1] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+59
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid vacuous pass when no reasoning snapshots are emitted. Line 69 currently guards the final assertion with ✅ Minimal hardening diff snapshots: list[str] = []
async for event in result.stream_events():
if isinstance(event, ReasoningDeltaEvent):
snapshots.append(event.snapshot)
+ assert snapshots, "Expected at least one ReasoningDeltaEvent snapshot"
+
# Each snapshot must be at least as long as the previous one
for i in range(1, len(snapshots)):
assert len(snapshots[i]) >= len(snapshots[i - 1])
# Last snapshot must contain the full reasoning text
- if snapshots:
- assert "Hello world" in snapshots[-1]
+ assert "Hello world" in snapshots[-1]📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @pytest.mark.asyncio | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def test_no_reasoning_delta_event_without_reasoning() -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ReasoningDeltaEvent is not emitted when there is no reasoning in the response.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| model = FakeModel() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| model.set_next_output([get_text_message("plain text answer")]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| agent = Agent(name="A", model=model) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = Runner.run_streamed(agent, input="hi") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| async for event in result.stream_events(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert not isinstance(event, ReasoningDeltaEvent), ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Got unexpected ReasoningDeltaEvent for a plain text response" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+82
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Assert that the stream produced events in the negative-case test. Line 82–85 verifies event type but not stream liveness. A fully empty stream would incorrectly pass this test. ✅ Minimal hardening diff- async for event in result.stream_events():
+ saw_event = False
+ async for event in result.stream_events():
+ saw_event = True
assert not isinstance(event, ReasoningDeltaEvent), (
"Got unexpected ReasoningDeltaEvent for a plain text response"
)
+ assert saw_event, "Expected at least one streamed event"📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @pytest.mark.asyncio | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def test_reasoning_delta_event_type_field() -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ReasoningDeltaEvent.type is always 'reasoning_delta'.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| model = FakeModel() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| model.set_next_output([ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| _make_reasoning_item("some reasoning"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| get_text_message("answer"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| agent = Agent(name="A", model=model) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = Runner.run_streamed(agent, input="hi") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| found = False | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async for event in result.stream_events(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(event, ReasoningDeltaEvent): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert event.type == "reasoning_delta" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| found = True | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert found, "Expected at least one ReasoningDeltaEvent but none were emitted" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @pytest.mark.asyncio | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def test_raw_response_events_still_emitted_alongside_reasoning_delta() -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """RawResponsesStreamEvent is still emitted even when ReasoningDeltaEvent is also emitted.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| model = FakeModel() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| model.set_next_output([ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| _make_reasoning_item("thinking"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| get_text_message("result"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| agent = Agent(name="A", model=model) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = Runner.run_streamed(agent, input="hi") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| raw_events: list[RawResponsesStreamEvent] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| reasoning_events: list[ReasoningDeltaEvent] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| async for event in result.stream_events(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(event, RawResponsesStreamEvent): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| raw_events.append(event) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif isinstance(event, ReasoningDeltaEvent): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| reasoning_events.append(event) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Both types should be present | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert len(raw_events) > 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert len(reasoning_events) > 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @pytest.mark.asyncio | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def test_reasoning_delta_event_importable_from_agents() -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ReasoningDeltaEvent can be imported directly from the agents package.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from agents import ReasoningDeltaEvent as RDE | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert RDE is ReasoningDeltaEvent | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_reasoning_delta_event_dataclass() -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ReasoningDeltaEvent is a proper dataclass with expected fields.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| event = ReasoningDeltaEvent(delta="chunk", snapshot="full chunk") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert event.delta == "chunk" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert event.snapshot == "full chunk" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert event.type == "reasoning_delta" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.