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.
- agents/agent.py +18 -2
- agents/extensions/handoff_filters.py +2 -0
- agents/extensions/memory/__init__.py +42 -15
- agents/extensions/memory/encrypt_session.py +185 -0
- agents/extensions/models/litellm_model.py +42 -5
- agents/function_schema.py +45 -3
- agents/models/chatcmpl_converter.py +74 -15
- agents/models/chatcmpl_helpers.py +6 -0
- agents/models/chatcmpl_stream_handler.py +29 -1
- agents/models/openai_chatcompletions.py +9 -2
- agents/models/openai_responses.py +14 -1
- agents/realtime/__init__.py +2 -0
- agents/realtime/config.py +10 -0
- agents/realtime/model_events.py +2 -0
- agents/realtime/openai_realtime.py +11 -1
- agents/result.py +47 -20
- agents/run.py +137 -71
- agents/tracing/processor_interface.py +84 -11
- agents/tracing/spans.py +88 -0
- agents/tracing/traces.py +99 -16
- agents/util/_transforms.py +12 -2
- agents/voice/models/openai_stt.py +9 -4
- {openai_agents-0.3.0.dist-info → openai_agents-0.3.1.dist-info}/METADATA +3 -1
- {openai_agents-0.3.0.dist-info → openai_agents-0.3.1.dist-info}/RECORD +26 -25
- {openai_agents-0.3.0.dist-info → openai_agents-0.3.1.dist-info}/WHEEL +0 -0
- {openai_agents-0.3.0.dist-info → openai_agents-0.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
-
|
|
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=
|
|
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=
|
|
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:
|
agents/realtime/__init__.py
CHANGED
|
@@ -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
|
|
agents/realtime/model_events.py
CHANGED
|
@@ -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 [
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
196
|
-
|
|
196
|
+
if self.is_complete and self._event_queue.empty():
|
|
197
|
+
break
|
|
197
198
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
199
|
+
try:
|
|
200
|
+
item = await self._event_queue.get()
|
|
201
|
+
except asyncio.CancelledError:
|
|
202
|
+
break
|
|
202
203
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
+
# Check for errors, in case the queue was completed
|
|
212
|
+
# due to an exception
|
|
213
|
+
self._check_errors()
|
|
214
|
+
break
|
|
211
215
|
|
|
212
|
-
|
|
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
|
|
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
|
-
"""
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
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
|
|
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
|
|
261
|
-
Responses API, this allows you to skip passing in input
|
|
262
|
-
|
|
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
|
|
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
|
|
271
|
-
agent. Agents may perform handoffs, so we don't know the specific
|
|
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
|
-
"""
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
|
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
|
|
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
|
|
324
|
-
Responses API, this allows you to skip passing in input
|
|
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
|
|
328
|
-
agent. Agents may perform handoffs, so we don't know the specific
|
|
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
|
-
"""
|
|
357
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
|
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
|
|
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
|
|
378
|
-
Responses API, this allows you to skip passing in input
|
|
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
|
|
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
|
-
|
|
1101
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1118
|
-
|
|
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
|
-
|
|
1122
|
-
|
|
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,
|