openai-agents 0.3.0__py3-none-any.whl → 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of openai-agents might be problematic. Click here for more details.

@@ -62,6 +62,9 @@ class StreamingState:
62
62
  # Fields for real-time function call streaming
63
63
  function_call_streaming: dict[int, bool] = field(default_factory=dict)
64
64
  function_call_output_idx: dict[int, int] = field(default_factory=dict)
65
+ # Store accumulated thinking text and signature for Anthropic compatibility
66
+ thinking_text: str = ""
67
+ thinking_signature: str | None = None
65
68
 
66
69
 
67
70
  class SequenceNumber:
@@ -101,6 +104,19 @@ class ChatCmplStreamHandler:
101
104
 
102
105
  delta = chunk.choices[0].delta
103
106
 
107
+ # Handle thinking blocks from Anthropic (for preserving signatures)
108
+ if hasattr(delta, "thinking_blocks") and delta.thinking_blocks:
109
+ for block in delta.thinking_blocks:
110
+ if isinstance(block, dict):
111
+ # Accumulate thinking text
112
+ thinking_text = block.get("thinking", "")
113
+ if thinking_text:
114
+ state.thinking_text += thinking_text
115
+ # Store signature if present
116
+ signature = block.get("signature")
117
+ if signature:
118
+ state.thinking_signature = signature
119
+
104
120
  # Handle reasoning content for reasoning summaries
105
121
  if hasattr(delta, "reasoning_content"):
106
122
  reasoning_content = delta.reasoning_content
@@ -527,7 +543,19 @@ class ChatCmplStreamHandler:
527
543
 
528
544
  # include Reasoning item if it exists
529
545
  if state.reasoning_content_index_and_output:
530
- outputs.append(state.reasoning_content_index_and_output[1])
546
+ reasoning_item = state.reasoning_content_index_and_output[1]
547
+ # Store thinking text in content and signature in encrypted_content
548
+ if state.thinking_text:
549
+ # Add thinking text as a Content object
550
+ if not reasoning_item.content:
551
+ reasoning_item.content = []
552
+ reasoning_item.content.append(
553
+ Content(text=state.thinking_text, type="reasoning_text")
554
+ )
555
+ # Store signature in encrypted_content
556
+ if state.thinking_signature:
557
+ reasoning_item.encrypted_content = state.thinking_signature
558
+ outputs.append(reasoning_item)
531
559
 
532
560
  # include text or refusal content if they exist
533
561
  if state.text_content_index_and_output or state.refusal_content_index_and_output:
@@ -25,7 +25,7 @@ from ..tracing.spans import Span
25
25
  from ..usage import Usage
26
26
  from ..util._json import _to_dump_compatible
27
27
  from .chatcmpl_converter import Converter
28
- from .chatcmpl_helpers import HEADERS, ChatCmplHelpers
28
+ from .chatcmpl_helpers import HEADERS, USER_AGENT_OVERRIDE, ChatCmplHelpers
29
29
  from .chatcmpl_stream_handler import ChatCmplStreamHandler
30
30
  from .fake_id import FAKE_RESPONSES_ID
31
31
  from .interface import Model, ModelTracing
@@ -306,7 +306,7 @@ class OpenAIChatCompletionsModel(Model):
306
306
  reasoning_effort=self._non_null_or_not_given(reasoning_effort),
307
307
  verbosity=self._non_null_or_not_given(model_settings.verbosity),
308
308
  top_logprobs=self._non_null_or_not_given(model_settings.top_logprobs),
309
- extra_headers={**HEADERS, **(model_settings.extra_headers or {})},
309
+ extra_headers=self._merge_headers(model_settings),
310
310
  extra_query=model_settings.extra_query,
311
311
  extra_body=model_settings.extra_body,
312
312
  metadata=self._non_null_or_not_given(model_settings.metadata),
@@ -349,3 +349,10 @@ class OpenAIChatCompletionsModel(Model):
349
349
  if self._client is None:
350
350
  self._client = AsyncOpenAI()
351
351
  return self._client
352
+
353
+ def _merge_headers(self, model_settings: ModelSettings):
354
+ merged = {**HEADERS, **(model_settings.extra_headers or {})}
355
+ ua_ctx = USER_AGENT_OVERRIDE.get()
356
+ if ua_ctx is not None:
357
+ merged["User-Agent"] = ua_ctx
358
+ return merged
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  from collections.abc import AsyncIterator
5
+ from contextvars import ContextVar
5
6
  from dataclasses import dataclass
6
7
  from typing import TYPE_CHECKING, Any, Literal, cast, overload
7
8
 
@@ -49,6 +50,11 @@ if TYPE_CHECKING:
49
50
  _USER_AGENT = f"Agents/Python {__version__}"
50
51
  _HEADERS = {"User-Agent": _USER_AGENT}
51
52
 
53
+ # Override for the User-Agent header used by the Responses API.
54
+ _USER_AGENT_OVERRIDE: ContextVar[str | None] = ContextVar(
55
+ "openai_responses_user_agent_override", default=None
56
+ )
57
+
52
58
 
53
59
  class OpenAIResponsesModel(Model):
54
60
  """
@@ -312,7 +318,7 @@ class OpenAIResponsesModel(Model):
312
318
  tool_choice=tool_choice,
313
319
  parallel_tool_calls=parallel_tool_calls,
314
320
  stream=stream,
315
- extra_headers={**_HEADERS, **(model_settings.extra_headers or {})},
321
+ extra_headers=self._merge_headers(model_settings),
316
322
  extra_query=model_settings.extra_query,
317
323
  extra_body=model_settings.extra_body,
318
324
  text=response_format,
@@ -327,6 +333,13 @@ class OpenAIResponsesModel(Model):
327
333
  self._client = AsyncOpenAI()
328
334
  return self._client
329
335
 
336
+ def _merge_headers(self, model_settings: ModelSettings):
337
+ merged = {**_HEADERS, **(model_settings.extra_headers or {})}
338
+ ua_ctx = _USER_AGENT_OVERRIDE.get()
339
+ if ua_ctx is not None:
340
+ merged["User-Agent"] = ua_ctx
341
+ return merged
342
+
330
343
 
331
344
  @dataclass
332
345
  class ConvertedTools:
@@ -3,6 +3,7 @@ from .config import (
3
3
  RealtimeAudioFormat,
4
4
  RealtimeClientMessage,
5
5
  RealtimeGuardrailsSettings,
6
+ RealtimeInputAudioNoiseReductionConfig,
6
7
  RealtimeInputAudioTranscriptionConfig,
7
8
  RealtimeModelName,
8
9
  RealtimeModelTracingConfig,
@@ -101,6 +102,7 @@ __all__ = [
101
102
  "RealtimeAudioFormat",
102
103
  "RealtimeClientMessage",
103
104
  "RealtimeGuardrailsSettings",
105
+ "RealtimeInputAudioNoiseReductionConfig",
104
106
  "RealtimeInputAudioTranscriptionConfig",
105
107
  "RealtimeModelName",
106
108
  "RealtimeModelTracingConfig",
agents/realtime/config.py CHANGED
@@ -61,6 +61,13 @@ class RealtimeInputAudioTranscriptionConfig(TypedDict):
61
61
  """An optional prompt to guide transcription."""
62
62
 
63
63
 
64
+ class RealtimeInputAudioNoiseReductionConfig(TypedDict):
65
+ """Noise reduction configuration for input audio."""
66
+
67
+ type: NotRequired[Literal["near_field", "far_field"]]
68
+ """Noise reduction mode to apply to input audio."""
69
+
70
+
64
71
  class RealtimeTurnDetectionConfig(TypedDict):
65
72
  """Turn detection config. Allows extra vendor keys if needed."""
66
73
 
@@ -119,6 +126,9 @@ class RealtimeSessionModelSettings(TypedDict):
119
126
  input_audio_transcription: NotRequired[RealtimeInputAudioTranscriptionConfig]
120
127
  """Configuration for transcribing input audio."""
121
128
 
129
+ input_audio_noise_reduction: NotRequired[RealtimeInputAudioNoiseReductionConfig | None]
130
+ """Noise reduction configuration for input audio."""
131
+
122
132
  turn_detection: NotRequired[RealtimeTurnDetectionConfig]
123
133
  """Configuration for detecting conversation turns."""
124
134
 
@@ -84,6 +84,7 @@ class RealtimeModelInputAudioTranscriptionCompletedEvent:
84
84
 
85
85
  type: Literal["input_audio_transcription_completed"] = "input_audio_transcription_completed"
86
86
 
87
+
87
88
  @dataclass
88
89
  class RealtimeModelInputAudioTimeoutTriggeredEvent:
89
90
  """Input audio timeout triggered."""
@@ -94,6 +95,7 @@ class RealtimeModelInputAudioTimeoutTriggeredEvent:
94
95
 
95
96
  type: Literal["input_audio_timeout_triggered"] = "input_audio_timeout_triggered"
96
97
 
98
+
97
99
  @dataclass
98
100
  class RealtimeModelTranscriptDeltaEvent:
99
101
  """Partial transcript update."""
@@ -825,14 +825,24 @@ class OpenAIRealtimeWebSocketModel(RealtimeModel):
825
825
  "output_audio_format",
826
826
  DEFAULT_MODEL_SETTINGS.get("output_audio_format"),
827
827
  )
828
+ input_audio_noise_reduction = model_settings.get(
829
+ "input_audio_noise_reduction",
830
+ DEFAULT_MODEL_SETTINGS.get("input_audio_noise_reduction"),
831
+ )
828
832
 
829
833
  input_audio_config = None
830
834
  if any(
831
835
  value is not None
832
- for value in [input_audio_format, input_audio_transcription, turn_detection]
836
+ for value in [
837
+ input_audio_format,
838
+ input_audio_noise_reduction,
839
+ input_audio_transcription,
840
+ turn_detection,
841
+ ]
833
842
  ):
834
843
  input_audio_config = OpenAIRealtimeAudioInput(
835
844
  format=to_realtime_audio_format(input_audio_format),
845
+ noise_reduction=cast(Any, input_audio_noise_reduction),
836
846
  transcription=cast(Any, input_audio_transcription),
837
847
  turn_detection=cast(Any, turn_detection),
838
848
  )
agents/result.py CHANGED
@@ -185,31 +185,42 @@ class RunResultStreaming(RunResultBase):
185
185
  - A MaxTurnsExceeded exception if the agent exceeds the max_turns limit.
186
186
  - A GuardrailTripwireTriggered exception if a guardrail is tripped.
187
187
  """
188
- while True:
189
- self._check_errors()
190
- if self._stored_exception:
191
- logger.debug("Breaking due to stored exception")
192
- self.is_complete = True
193
- break
188
+ try:
189
+ while True:
190
+ self._check_errors()
191
+ if self._stored_exception:
192
+ logger.debug("Breaking due to stored exception")
193
+ self.is_complete = True
194
+ break
194
195
 
195
- if self.is_complete and self._event_queue.empty():
196
- break
196
+ if self.is_complete and self._event_queue.empty():
197
+ break
197
198
 
198
- try:
199
- item = await self._event_queue.get()
200
- except asyncio.CancelledError:
201
- break
199
+ try:
200
+ item = await self._event_queue.get()
201
+ except asyncio.CancelledError:
202
+ break
202
203
 
203
- if isinstance(item, QueueCompleteSentinel):
204
- self._event_queue.task_done()
205
- # Check for errors, in case the queue was completed due to an exception
206
- self._check_errors()
207
- break
204
+ if isinstance(item, QueueCompleteSentinel):
205
+ # Await input guardrails if they are still running, so late
206
+ # exceptions are captured.
207
+ await self._await_task_safely(self._input_guardrails_task)
208
+
209
+ self._event_queue.task_done()
208
210
 
209
- yield item
210
- self._event_queue.task_done()
211
+ # Check for errors, in case the queue was completed
212
+ # due to an exception
213
+ self._check_errors()
214
+ break
211
215
 
212
- self._cleanup_tasks()
216
+ yield item
217
+ self._event_queue.task_done()
218
+ finally:
219
+ # Ensure main execution completes before cleanup to avoid race conditions
220
+ # with session operations
221
+ await self._await_task_safely(self._run_impl_task)
222
+ # Safely terminate all background tasks after main execution has finished
223
+ self._cleanup_tasks()
213
224
 
214
225
  if self._stored_exception:
215
226
  raise self._stored_exception
@@ -274,3 +285,19 @@ class RunResultStreaming(RunResultBase):
274
285
 
275
286
  def __str__(self) -> str:
276
287
  return pretty_print_run_result_streaming(self)
288
+
289
+ async def _await_task_safely(self, task: asyncio.Task[Any] | None) -> None:
290
+ """Await a task if present, ignoring cancellation and storing exceptions elsewhere.
291
+
292
+ This ensures we do not lose late guardrail exceptions while not surfacing
293
+ CancelledError to callers of stream_events.
294
+ """
295
+ if task and not task.done():
296
+ try:
297
+ await task
298
+ except asyncio.CancelledError:
299
+ # Task was cancelled (e.g., due to result.cancel()). Nothing to do here.
300
+ pass
301
+ except Exception:
302
+ # The exception will be surfaced via _check_errors() if needed.
303
+ pass
agents/run.py CHANGED
@@ -45,6 +45,7 @@ from .guardrail import (
45
45
  )
46
46
  from .handoffs import Handoff, HandoffInputFilter, handoff
47
47
  from .items import (
48
+ HandoffCallItem,
48
49
  ItemHelpers,
49
50
  ModelResponse,
50
51
  RunItem,
@@ -60,7 +61,12 @@ from .models.interface import Model, ModelProvider
60
61
  from .models.multi_provider import MultiProvider
61
62
  from .result import RunResult, RunResultStreaming
62
63
  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
+ )
64
70
  from .tool import Tool
65
71
  from .tracing import Span, SpanError, agent_span, get_current_trace, trace
66
72
  from .tracing.span_data import AgentSpanData
@@ -237,39 +243,54 @@ class Runner:
237
243
  conversation_id: str | None = None,
238
244
  session: Session | None = None,
239
245
  ) -> RunResult:
240
- """Run a workflow starting at the given agent. The agent will run in a loop until a final
241
- output is generated. The loop runs like so:
242
- 1. The agent is invoked with the given input.
243
- 2. If there is a final output (i.e. the agent produces something of type
244
- `agent.output_type`, the loop terminates.
245
- 3. If there's a handoff, we run the loop again, with the new agent.
246
- 4. Else, we run tool calls (if any), and re-run the loop.
246
+ """
247
+ Run a workflow starting at the given agent.
248
+
249
+ The agent will run in a loop until a final output is generated. The loop runs like so:
250
+
251
+ 1. The agent is invoked with the given input.
252
+ 2. If there is a final output (i.e. the agent produces something of type
253
+ `agent.output_type`), the loop terminates.
254
+ 3. If there's a handoff, we run the loop again, with the new agent.
255
+ 4. Else, we run tool calls (if any), and re-run the loop.
256
+
247
257
  In two cases, the agent may raise an exception:
248
- 1. If the max_turns is exceeded, a MaxTurnsExceeded exception is raised.
249
- 2. If a guardrail tripwire is triggered, a GuardrailTripwireTriggered exception is raised.
250
- Note that only the first agent's input guardrails are run.
258
+
259
+ 1. If the max_turns is exceeded, a MaxTurnsExceeded exception is raised.
260
+ 2. If a guardrail tripwire is triggered, a GuardrailTripwireTriggered
261
+ exception is raised.
262
+
263
+ Note:
264
+ Only the first agent's input guardrails are run.
265
+
251
266
  Args:
252
267
  starting_agent: The starting agent to run.
253
- input: The initial input to the agent. You can pass a single string for a user message,
254
- or a list of input items.
268
+ input: The initial input to the agent. You can pass a single string for a
269
+ user message, or a list of input items.
255
270
  context: The context to run the agent with.
256
- max_turns: The maximum number of turns to run the agent for. A turn is defined as one
257
- AI invocation (including any tool calls that might occur).
271
+ max_turns: The maximum number of turns to run the agent for. A turn is
272
+ defined as one AI invocation (including any tool calls that might occur).
258
273
  hooks: An object that receives callbacks on various lifecycle events.
259
274
  run_config: Global settings for the entire agent run.
260
- previous_response_id: The ID of the previous response, if using OpenAI models via the
261
- Responses API, this allows you to skip passing in input from the previous turn.
262
- conversation_id: The conversation ID (https://platform.openai.com/docs/guides/conversation-state?api-mode=responses).
275
+ previous_response_id: The ID of the previous response. If using OpenAI
276
+ models via the Responses API, this allows you to skip passing in input
277
+ from the previous turn.
278
+ conversation_id: The conversation ID
279
+ (https://platform.openai.com/docs/guides/conversation-state?api-mode=responses).
263
280
  If provided, the conversation will be used to read and write items.
264
281
  Every agent will have access to the conversation history so far,
265
- and it's output items will be written to the conversation.
282
+ and its output items will be written to the conversation.
266
283
  We recommend only using this if you are exclusively using OpenAI models;
267
284
  other model providers don't write to the Conversation object,
268
285
  so you'll end up having partial conversations stored.
286
+ session: A session for automatic conversation history management.
287
+
269
288
  Returns:
270
- A run result containing all the inputs, guardrail results and the output of the last
271
- agent. Agents may perform handoffs, so we don't know the specific type of the output.
289
+ A run result containing all the inputs, guardrail results and the output of
290
+ the last agent. Agents may perform handoffs, so we don't know the specific
291
+ type of the output.
272
292
  """
293
+
273
294
  runner = DEFAULT_AGENT_RUNNER
274
295
  return await runner.run(
275
296
  starting_agent,
@@ -297,36 +318,52 @@ class Runner:
297
318
  conversation_id: str | None = None,
298
319
  session: Session | None = None,
299
320
  ) -> RunResult:
300
- """Run a workflow synchronously, starting at the given agent. Note that this just wraps the
301
- `run` method, so it will not work if there's already an event loop (e.g. inside an async
302
- function, or in a Jupyter notebook or async context like FastAPI). For those cases, use
303
- the `run` method instead.
304
- The agent will run in a loop until a final output is generated. The loop runs like so:
305
- 1. The agent is invoked with the given input.
306
- 2. If there is a final output (i.e. the agent produces something of type
307
- `agent.output_type`, the loop terminates.
308
- 3. If there's a handoff, we run the loop again, with the new agent.
309
- 4. Else, we run tool calls (if any), and re-run the loop.
321
+ """
322
+ Run a workflow synchronously, starting at the given agent.
323
+
324
+ Note:
325
+ This just wraps the `run` method, so it will not work if there's already an
326
+ event loop (e.g. inside an async function, or in a Jupyter notebook or async
327
+ context like FastAPI). For those cases, use the `run` method instead.
328
+
329
+ The agent will run in a loop until a final output is generated. The loop runs:
330
+
331
+ 1. The agent is invoked with the given input.
332
+ 2. If there is a final output (i.e. the agent produces something of type
333
+ `agent.output_type`), the loop terminates.
334
+ 3. If there's a handoff, we run the loop again, with the new agent.
335
+ 4. Else, we run tool calls (if any), and re-run the loop.
336
+
310
337
  In two cases, the agent may raise an exception:
311
- 1. If the max_turns is exceeded, a MaxTurnsExceeded exception is raised.
312
- 2. If a guardrail tripwire is triggered, a GuardrailTripwireTriggered exception is raised.
313
- Note that only the first agent's input guardrails are run.
338
+
339
+ 1. If the max_turns is exceeded, a MaxTurnsExceeded exception is raised.
340
+ 2. If a guardrail tripwire is triggered, a GuardrailTripwireTriggered
341
+ exception is raised.
342
+
343
+ Note:
344
+ Only the first agent's input guardrails are run.
345
+
314
346
  Args:
315
347
  starting_agent: The starting agent to run.
316
- input: The initial input to the agent. You can pass a single string for a user message,
317
- or a list of input items.
348
+ input: The initial input to the agent. You can pass a single string for a
349
+ user message, or a list of input items.
318
350
  context: The context to run the agent with.
319
- max_turns: The maximum number of turns to run the agent for. A turn is defined as one
320
- AI invocation (including any tool calls that might occur).
351
+ max_turns: The maximum number of turns to run the agent for. A turn is
352
+ defined as one AI invocation (including any tool calls that might occur).
321
353
  hooks: An object that receives callbacks on various lifecycle events.
322
354
  run_config: Global settings for the entire agent run.
323
- previous_response_id: The ID of the previous response, if using OpenAI models via the
324
- Responses API, this allows you to skip passing in input from the previous turn.
355
+ previous_response_id: The ID of the previous response, if using OpenAI
356
+ models via the Responses API, this allows you to skip passing in input
357
+ from the previous turn.
325
358
  conversation_id: The ID of the stored conversation, if any.
359
+ session: A session for automatic conversation history management.
360
+
326
361
  Returns:
327
- A run result containing all the inputs, guardrail results and the output of the last
328
- agent. Agents may perform handoffs, so we don't know the specific type of the output.
362
+ A run result containing all the inputs, guardrail results and the output of
363
+ the last agent. Agents may perform handoffs, so we don't know the specific
364
+ type of the output.
329
365
  """
366
+
330
367
  runner = DEFAULT_AGENT_RUNNER
331
368
  return runner.run_sync(
332
369
  starting_agent,
@@ -353,33 +390,49 @@ class Runner:
353
390
  conversation_id: str | None = None,
354
391
  session: Session | None = None,
355
392
  ) -> RunResultStreaming:
356
- """Run a workflow starting at the given agent in streaming mode. The returned result object
357
- contains a method you can use to stream semantic events as they are generated.
393
+ """
394
+ Run a workflow starting at the given agent in streaming mode.
395
+
396
+ The returned result object contains a method you can use to stream semantic
397
+ events as they are generated.
398
+
358
399
  The agent will run in a loop until a final output is generated. The loop runs like so:
359
- 1. The agent is invoked with the given input.
360
- 2. If there is a final output (i.e. the agent produces something of type
361
- `agent.output_type`, the loop terminates.
362
- 3. If there's a handoff, we run the loop again, with the new agent.
363
- 4. Else, we run tool calls (if any), and re-run the loop.
400
+
401
+ 1. The agent is invoked with the given input.
402
+ 2. If there is a final output (i.e. the agent produces something of type
403
+ `agent.output_type`), the loop terminates.
404
+ 3. If there's a handoff, we run the loop again, with the new agent.
405
+ 4. Else, we run tool calls (if any), and re-run the loop.
406
+
364
407
  In two cases, the agent may raise an exception:
365
- 1. If the max_turns is exceeded, a MaxTurnsExceeded exception is raised.
366
- 2. If a guardrail tripwire is triggered, a GuardrailTripwireTriggered exception is raised.
367
- Note that only the first agent's input guardrails are run.
408
+
409
+ 1. If the max_turns is exceeded, a MaxTurnsExceeded exception is raised.
410
+ 2. If a guardrail tripwire is triggered, a GuardrailTripwireTriggered
411
+ exception is raised.
412
+
413
+ Note:
414
+ Only the first agent's input guardrails are run.
415
+
368
416
  Args:
369
417
  starting_agent: The starting agent to run.
370
- input: The initial input to the agent. You can pass a single string for a user message,
371
- or a list of input items.
418
+ input: The initial input to the agent. You can pass a single string for a
419
+ user message, or a list of input items.
372
420
  context: The context to run the agent with.
373
- max_turns: The maximum number of turns to run the agent for. A turn is defined as one
374
- AI invocation (including any tool calls that might occur).
421
+ max_turns: The maximum number of turns to run the agent for. A turn is
422
+ defined as one AI invocation (including any tool calls that might occur).
375
423
  hooks: An object that receives callbacks on various lifecycle events.
376
424
  run_config: Global settings for the entire agent run.
377
- previous_response_id: The ID of the previous response, if using OpenAI models via the
378
- Responses API, this allows you to skip passing in input from the previous turn.
425
+ previous_response_id: The ID of the previous response, if using OpenAI
426
+ models via the Responses API, this allows you to skip passing in input
427
+ from the previous turn.
379
428
  conversation_id: The ID of the stored conversation, if any.
429
+ session: A session for automatic conversation history management.
430
+
380
431
  Returns:
381
- A result object that contains data about the run, as well as a method to stream events.
432
+ A result object that contains data about the run, as well as a method to
433
+ stream events.
382
434
  """
435
+
383
436
  runner = DEFAULT_AGENT_RUNNER
384
437
  return runner.run_streamed(
385
438
  starting_agent,
@@ -1095,14 +1148,19 @@ class AgentRunner:
1095
1148
  context_wrapper=context_wrapper,
1096
1149
  run_config=run_config,
1097
1150
  tool_use_tracker=tool_use_tracker,
1151
+ event_queue=streamed_result._event_queue,
1098
1152
  )
1099
1153
 
1100
- if emitted_tool_call_ids:
1101
- import dataclasses as _dc
1154
+ import dataclasses as _dc
1155
+
1156
+ # Filter out items that have already been sent to avoid duplicates
1157
+ items_to_filter = single_step_result.new_step_items
1102
1158
 
1103
- filtered_items = [
1159
+ if emitted_tool_call_ids:
1160
+ # Filter out tool call items that were already emitted during streaming
1161
+ items_to_filter = [
1104
1162
  item
1105
- for item in single_step_result.new_step_items
1163
+ for item in items_to_filter
1106
1164
  if not (
1107
1165
  isinstance(item, ToolCallItem)
1108
1166
  and (
@@ -1114,15 +1172,14 @@ class AgentRunner:
1114
1172
  )
1115
1173
  ]
1116
1174
 
1117
- single_step_result_filtered = _dc.replace(
1118
- single_step_result, new_step_items=filtered_items
1119
- )
1175
+ # Filter out HandoffCallItem to avoid duplicates (already sent earlier)
1176
+ items_to_filter = [
1177
+ item for item in items_to_filter if not isinstance(item, HandoffCallItem)
1178
+ ]
1120
1179
 
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)
1180
+ # Create filtered result and send to queue
1181
+ filtered_result = _dc.replace(single_step_result, new_step_items=items_to_filter)
1182
+ RunImpl.stream_step_result_to_queue(filtered_result, streamed_result._event_queue)
1126
1183
  return single_step_result
1127
1184
 
1128
1185
  @classmethod
@@ -1207,6 +1264,7 @@ class AgentRunner:
1207
1264
  context_wrapper: RunContextWrapper[TContext],
1208
1265
  run_config: RunConfig,
1209
1266
  tool_use_tracker: AgentToolUseTracker,
1267
+ event_queue: asyncio.Queue[StreamEvent | QueueCompleteSentinel] | None = None,
1210
1268
  ) -> SingleStepResult:
1211
1269
  processed_response = RunImpl.process_model_response(
1212
1270
  agent=agent,
@@ -1218,6 +1276,14 @@ class AgentRunner:
1218
1276
 
1219
1277
  tool_use_tracker.add_tool_use(agent, processed_response.tools_used)
1220
1278
 
1279
+ # Send handoff items immediately for streaming, but avoid duplicates
1280
+ if event_queue is not None and processed_response.new_items:
1281
+ handoff_items = [
1282
+ item for item in processed_response.new_items if isinstance(item, HandoffCallItem)
1283
+ ]
1284
+ if handoff_items:
1285
+ RunImpl.stream_step_items_to_queue(cast(list[RunItem], handoff_items), event_queue)
1286
+
1221
1287
  return await RunImpl.execute_tools_and_side_effects(
1222
1288
  agent=agent,
1223
1289
  original_input=original_input,