Skip to content

Commit 789575f

Browse files
authored
fix(streaming): #1712 push processed_response.new_items (including HandoffCallItem) to event_queue (#1703)
1 parent 5e6269c commit 789575f

File tree

2 files changed

+94
-15
lines changed

2 files changed

+94
-15
lines changed

src/agents/run.py

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
)
4646
from .handoffs import Handoff, HandoffInputFilter, handoff
4747
from .items import (
48+
HandoffCallItem,
4849
ItemHelpers,
4950
ModelResponse,
5051
RunItem,
@@ -60,7 +61,12 @@
6061
from .models.multi_provider import MultiProvider
6162
from .result import RunResult, RunResultStreaming
6263
from .run_context import RunContextWrapper, TContext
63-
from .stream_events import AgentUpdatedStreamEvent, RawResponsesStreamEvent, RunItemStreamEvent
64+
from .stream_events import (
65+
AgentUpdatedStreamEvent,
66+
RawResponsesStreamEvent,
67+
RunItemStreamEvent,
68+
StreamEvent,
69+
)
6470
from .tool import Tool
6571
from .tracing import Span, SpanError, agent_span, get_current_trace, trace
6672
from .tracing.span_data import AgentSpanData
@@ -1095,14 +1101,19 @@ async def _run_single_turn_streamed(
10951101
context_wrapper=context_wrapper,
10961102
run_config=run_config,
10971103
tool_use_tracker=tool_use_tracker,
1104+
event_queue=streamed_result._event_queue,
10981105
)
10991106

1100-
if emitted_tool_call_ids:
1101-
import dataclasses as _dc
1107+
import dataclasses as _dc
1108+
1109+
# Filter out items that have already been sent to avoid duplicates
1110+
items_to_filter = single_step_result.new_step_items
11021111

1103-
filtered_items = [
1112+
if emitted_tool_call_ids:
1113+
# Filter out tool call items that were already emitted during streaming
1114+
items_to_filter = [
11041115
item
1105-
for item in single_step_result.new_step_items
1116+
for item in items_to_filter
11061117
if not (
11071118
isinstance(item, ToolCallItem)
11081119
and (
@@ -1114,15 +1125,17 @@ async def _run_single_turn_streamed(
11141125
)
11151126
]
11161127

1117-
single_step_result_filtered = _dc.replace(
1118-
single_step_result, new_step_items=filtered_items
1119-
)
1128+
# Filter out HandoffCallItem to avoid duplicates (already sent earlier)
1129+
items_to_filter = [
1130+
item for item in items_to_filter
1131+
if not isinstance(item, HandoffCallItem)
1132+
]
11201133

1121-
RunImpl.stream_step_result_to_queue(
1122-
single_step_result_filtered, streamed_result._event_queue
1123-
)
1124-
else:
1125-
RunImpl.stream_step_result_to_queue(single_step_result, streamed_result._event_queue)
1134+
# Create filtered result and send to queue
1135+
filtered_result = _dc.replace(
1136+
single_step_result, new_step_items=items_to_filter
1137+
)
1138+
RunImpl.stream_step_result_to_queue(filtered_result, streamed_result._event_queue)
11261139
return single_step_result
11271140

11281141
@classmethod
@@ -1207,6 +1220,7 @@ async def _get_single_step_result_from_response(
12071220
context_wrapper: RunContextWrapper[TContext],
12081221
run_config: RunConfig,
12091222
tool_use_tracker: AgentToolUseTracker,
1223+
event_queue: asyncio.Queue[StreamEvent | QueueCompleteSentinel] | None = None,
12101224
) -> SingleStepResult:
12111225
processed_response = RunImpl.process_model_response(
12121226
agent=agent,
@@ -1218,6 +1232,15 @@ async def _get_single_step_result_from_response(
12181232

12191233
tool_use_tracker.add_tool_use(agent, processed_response.tools_used)
12201234

1235+
# Send handoff items immediately for streaming, but avoid duplicates
1236+
if event_queue is not None and processed_response.new_items:
1237+
handoff_items = [
1238+
item for item in processed_response.new_items
1239+
if isinstance(item, HandoffCallItem)
1240+
]
1241+
if handoff_items:
1242+
RunImpl.stream_step_items_to_queue(cast(list[RunItem], handoff_items), event_queue)
1243+
12211244
return await RunImpl.execute_tools_and_side_effects(
12221245
agent=agent,
12231246
original_input=original_input,

tests/test_stream_events.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33

44
import pytest
55

6-
from agents import Agent, Runner, function_tool
6+
from agents import Agent, HandoffCallItem, Runner, function_tool
7+
from agents.extensions.handoff_filters import remove_all_tools
8+
from agents.handoffs import handoff
79

810
from .fake_model import FakeModel
9-
from .test_responses import get_function_tool_call, get_text_message
11+
from .test_responses import get_function_tool_call, get_handoff_tool_call, get_text_message
1012

1113

1214
@function_tool
@@ -52,3 +54,57 @@ async def test_stream_events_main():
5254
assert tool_call_start_time > 0, "tool_call_item was not observed"
5355
assert tool_call_end_time > 0, "tool_call_output_item was not observed"
5456
assert tool_call_start_time < tool_call_end_time, "Tool call ended before or equals it started?"
57+
58+
59+
@pytest.mark.asyncio
60+
async def test_stream_events_main_with_handoff():
61+
@function_tool
62+
async def foo(args: str) -> str:
63+
return f"foo_result_{args}"
64+
65+
english_agent = Agent(
66+
name="EnglishAgent",
67+
instructions="You only speak English.",
68+
model=FakeModel(),
69+
)
70+
71+
model = FakeModel()
72+
model.add_multiple_turn_outputs(
73+
[
74+
[
75+
get_text_message("Hello"),
76+
get_function_tool_call("foo", '{"args": "arg1"}'),
77+
get_handoff_tool_call(english_agent),
78+
],
79+
[get_text_message("Done")],
80+
]
81+
)
82+
83+
triage_agent = Agent(
84+
name="TriageAgent",
85+
instructions="Handoff to the appropriate agent based on the language of the request.",
86+
handoffs=[
87+
handoff(english_agent, input_filter=remove_all_tools),
88+
],
89+
tools=[foo],
90+
model=model,
91+
)
92+
93+
result = Runner.run_streamed(
94+
triage_agent,
95+
input="Start",
96+
)
97+
98+
handoff_requested_seen = False
99+
agent_switched_to_english = False
100+
101+
async for event in result.stream_events():
102+
if event.type == "run_item_stream_event":
103+
if isinstance(event.item, HandoffCallItem):
104+
handoff_requested_seen = True
105+
elif event.type == "agent_updated_stream_event":
106+
if hasattr(event, "new_agent") and event.new_agent.name == "EnglishAgent":
107+
agent_switched_to_english = True
108+
109+
assert handoff_requested_seen, "handoff_requested event not observed"
110+
assert agent_switched_to_english, "Agent did not switch to EnglishAgent"

0 commit comments

Comments
 (0)