openai-agents 0.2.8__py3-none-any.whl → 0.6.8__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.
- agents/__init__.py +105 -4
- agents/_debug.py +15 -4
- agents/_run_impl.py +1203 -96
- agents/agent.py +164 -19
- agents/apply_diff.py +329 -0
- agents/editor.py +47 -0
- agents/exceptions.py +35 -0
- agents/extensions/experimental/__init__.py +6 -0
- agents/extensions/experimental/codex/__init__.py +92 -0
- agents/extensions/experimental/codex/codex.py +89 -0
- agents/extensions/experimental/codex/codex_options.py +35 -0
- agents/extensions/experimental/codex/codex_tool.py +1142 -0
- agents/extensions/experimental/codex/events.py +162 -0
- agents/extensions/experimental/codex/exec.py +263 -0
- agents/extensions/experimental/codex/items.py +245 -0
- agents/extensions/experimental/codex/output_schema_file.py +50 -0
- agents/extensions/experimental/codex/payloads.py +31 -0
- agents/extensions/experimental/codex/thread.py +214 -0
- agents/extensions/experimental/codex/thread_options.py +54 -0
- agents/extensions/experimental/codex/turn_options.py +36 -0
- agents/extensions/handoff_filters.py +13 -1
- agents/extensions/memory/__init__.py +120 -0
- agents/extensions/memory/advanced_sqlite_session.py +1285 -0
- agents/extensions/memory/async_sqlite_session.py +239 -0
- agents/extensions/memory/dapr_session.py +423 -0
- agents/extensions/memory/encrypt_session.py +185 -0
- agents/extensions/memory/redis_session.py +261 -0
- agents/extensions/memory/sqlalchemy_session.py +334 -0
- agents/extensions/models/litellm_model.py +449 -36
- agents/extensions/models/litellm_provider.py +3 -1
- agents/function_schema.py +47 -5
- agents/guardrail.py +16 -2
- agents/{handoffs.py → handoffs/__init__.py} +89 -47
- agents/handoffs/history.py +268 -0
- agents/items.py +237 -11
- agents/lifecycle.py +75 -14
- agents/mcp/server.py +280 -37
- agents/mcp/util.py +24 -3
- agents/memory/__init__.py +22 -2
- agents/memory/openai_conversations_session.py +91 -0
- agents/memory/openai_responses_compaction_session.py +249 -0
- agents/memory/session.py +19 -261
- agents/memory/sqlite_session.py +275 -0
- agents/memory/util.py +20 -0
- agents/model_settings.py +14 -3
- agents/models/__init__.py +13 -0
- agents/models/chatcmpl_converter.py +303 -50
- agents/models/chatcmpl_helpers.py +63 -0
- agents/models/chatcmpl_stream_handler.py +290 -68
- agents/models/default_models.py +58 -0
- agents/models/interface.py +4 -0
- agents/models/openai_chatcompletions.py +103 -49
- agents/models/openai_provider.py +10 -4
- agents/models/openai_responses.py +162 -46
- agents/realtime/__init__.py +4 -0
- agents/realtime/_util.py +14 -3
- agents/realtime/agent.py +7 -0
- agents/realtime/audio_formats.py +53 -0
- agents/realtime/config.py +78 -10
- agents/realtime/events.py +18 -0
- agents/realtime/handoffs.py +2 -2
- agents/realtime/items.py +17 -1
- agents/realtime/model.py +13 -0
- agents/realtime/model_events.py +12 -0
- agents/realtime/model_inputs.py +18 -1
- agents/realtime/openai_realtime.py +696 -150
- agents/realtime/session.py +243 -23
- agents/repl.py +7 -3
- agents/result.py +197 -38
- agents/run.py +949 -168
- agents/run_context.py +13 -2
- agents/stream_events.py +1 -0
- agents/strict_schema.py +14 -0
- agents/tool.py +413 -15
- agents/tool_context.py +22 -1
- agents/tool_guardrails.py +279 -0
- agents/tracing/__init__.py +2 -0
- agents/tracing/config.py +9 -0
- agents/tracing/create.py +4 -0
- agents/tracing/processor_interface.py +84 -11
- agents/tracing/processors.py +65 -54
- agents/tracing/provider.py +64 -7
- agents/tracing/spans.py +105 -0
- agents/tracing/traces.py +116 -16
- agents/usage.py +134 -12
- agents/util/_json.py +19 -1
- agents/util/_transforms.py +12 -2
- agents/voice/input.py +5 -4
- agents/voice/models/openai_stt.py +17 -9
- agents/voice/pipeline.py +2 -0
- agents/voice/pipeline_config.py +4 -0
- {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/METADATA +44 -19
- openai_agents-0.6.8.dist-info/RECORD +134 -0
- {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/WHEEL +1 -1
- openai_agents-0.2.8.dist-info/RECORD +0 -103
- {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/licenses/LICENSE +0 -0
agents/run.py
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import contextlib
|
|
4
5
|
import inspect
|
|
6
|
+
import os
|
|
7
|
+
import warnings
|
|
5
8
|
from dataclasses import dataclass, field
|
|
6
|
-
from typing import Any, Callable, Generic, cast
|
|
9
|
+
from typing import Any, Callable, Generic, cast, get_args, get_origin
|
|
7
10
|
|
|
8
|
-
from openai.types.responses import
|
|
11
|
+
from openai.types.responses import (
|
|
12
|
+
ResponseCompletedEvent,
|
|
13
|
+
ResponseOutputItemDoneEvent,
|
|
14
|
+
)
|
|
9
15
|
from openai.types.responses.response_prompt_param import (
|
|
10
16
|
ResponsePromptParam,
|
|
11
17
|
)
|
|
18
|
+
from openai.types.responses.response_reasoning_item import ResponseReasoningItem
|
|
12
19
|
from typing_extensions import NotRequired, TypedDict, Unpack
|
|
13
20
|
|
|
14
21
|
from ._run_impl import (
|
|
@@ -39,19 +46,36 @@ from .guardrail import (
|
|
|
39
46
|
OutputGuardrail,
|
|
40
47
|
OutputGuardrailResult,
|
|
41
48
|
)
|
|
42
|
-
from .handoffs import Handoff, HandoffInputFilter, handoff
|
|
43
|
-
from .items import
|
|
44
|
-
|
|
49
|
+
from .handoffs import Handoff, HandoffHistoryMapper, HandoffInputFilter, handoff
|
|
50
|
+
from .items import (
|
|
51
|
+
HandoffCallItem,
|
|
52
|
+
HandoffOutputItem,
|
|
53
|
+
ItemHelpers,
|
|
54
|
+
ModelResponse,
|
|
55
|
+
ReasoningItem,
|
|
56
|
+
RunItem,
|
|
57
|
+
ToolCallItem,
|
|
58
|
+
ToolCallItemTypes,
|
|
59
|
+
ToolCallOutputItem,
|
|
60
|
+
TResponseInputItem,
|
|
61
|
+
)
|
|
62
|
+
from .lifecycle import AgentHooksBase, RunHooks, RunHooksBase
|
|
45
63
|
from .logger import logger
|
|
46
|
-
from .memory import Session
|
|
64
|
+
from .memory import Session, SessionInputCallback, is_openai_responses_compaction_aware_session
|
|
47
65
|
from .model_settings import ModelSettings
|
|
48
66
|
from .models.interface import Model, ModelProvider
|
|
49
67
|
from .models.multi_provider import MultiProvider
|
|
50
68
|
from .result import RunResult, RunResultStreaming
|
|
51
|
-
from .run_context import RunContextWrapper, TContext
|
|
52
|
-
from .stream_events import
|
|
53
|
-
|
|
54
|
-
|
|
69
|
+
from .run_context import AgentHookContext, RunContextWrapper, TContext
|
|
70
|
+
from .stream_events import (
|
|
71
|
+
AgentUpdatedStreamEvent,
|
|
72
|
+
RawResponsesStreamEvent,
|
|
73
|
+
RunItemStreamEvent,
|
|
74
|
+
StreamEvent,
|
|
75
|
+
)
|
|
76
|
+
from .tool import Tool, dispose_resolved_computers
|
|
77
|
+
from .tool_guardrails import ToolInputGuardrailResult, ToolOutputGuardrailResult
|
|
78
|
+
from .tracing import Span, SpanError, TracingConfig, agent_span, get_current_trace, trace
|
|
55
79
|
from .tracing.span_data import AgentSpanData
|
|
56
80
|
from .usage import Usage
|
|
57
81
|
from .util import _coro, _error_tracing
|
|
@@ -81,6 +105,12 @@ def get_default_agent_runner() -> AgentRunner:
|
|
|
81
105
|
return DEFAULT_AGENT_RUNNER
|
|
82
106
|
|
|
83
107
|
|
|
108
|
+
def _default_trace_include_sensitive_data() -> bool:
|
|
109
|
+
"""Returns the default value for trace_include_sensitive_data based on environment variable."""
|
|
110
|
+
val = os.getenv("OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA", "true")
|
|
111
|
+
return val.strip().lower() in ("1", "true", "yes", "on")
|
|
112
|
+
|
|
113
|
+
|
|
84
114
|
@dataclass
|
|
85
115
|
class ModelInputData:
|
|
86
116
|
"""Container for the data that will be sent to the model."""
|
|
@@ -98,6 +128,56 @@ class CallModelData(Generic[TContext]):
|
|
|
98
128
|
context: TContext | None
|
|
99
129
|
|
|
100
130
|
|
|
131
|
+
@dataclass
|
|
132
|
+
class _ServerConversationTracker:
|
|
133
|
+
"""Tracks server-side conversation state for either conversation_id or
|
|
134
|
+
previous_response_id modes.
|
|
135
|
+
|
|
136
|
+
Note: When auto_previous_response_id=True is used, response chaining is enabled
|
|
137
|
+
automatically for the first turn, even when there's no actual previous response ID yet.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
conversation_id: str | None = None
|
|
141
|
+
previous_response_id: str | None = None
|
|
142
|
+
auto_previous_response_id: bool = False
|
|
143
|
+
sent_items: set[int] = field(default_factory=set)
|
|
144
|
+
server_items: set[int] = field(default_factory=set)
|
|
145
|
+
|
|
146
|
+
def track_server_items(self, model_response: ModelResponse) -> None:
|
|
147
|
+
for output_item in model_response.output:
|
|
148
|
+
self.server_items.add(id(output_item))
|
|
149
|
+
|
|
150
|
+
# Update previous_response_id when using previous_response_id mode or auto mode
|
|
151
|
+
if (
|
|
152
|
+
self.conversation_id is None
|
|
153
|
+
and (self.previous_response_id is not None or self.auto_previous_response_id)
|
|
154
|
+
and model_response.response_id is not None
|
|
155
|
+
):
|
|
156
|
+
self.previous_response_id = model_response.response_id
|
|
157
|
+
|
|
158
|
+
def prepare_input(
|
|
159
|
+
self,
|
|
160
|
+
original_input: str | list[TResponseInputItem],
|
|
161
|
+
generated_items: list[RunItem],
|
|
162
|
+
) -> list[TResponseInputItem]:
|
|
163
|
+
input_items: list[TResponseInputItem] = []
|
|
164
|
+
|
|
165
|
+
# On first call (when there are no generated items yet), include the original input
|
|
166
|
+
if not generated_items:
|
|
167
|
+
input_items.extend(ItemHelpers.input_to_new_input_list(original_input))
|
|
168
|
+
|
|
169
|
+
# Process generated_items, skip items already sent or from server
|
|
170
|
+
for item in generated_items:
|
|
171
|
+
raw_item_id = id(item.raw_item)
|
|
172
|
+
|
|
173
|
+
if raw_item_id in self.sent_items or raw_item_id in self.server_items:
|
|
174
|
+
continue
|
|
175
|
+
input_items.append(item.to_input_item())
|
|
176
|
+
self.sent_items.add(raw_item_id)
|
|
177
|
+
|
|
178
|
+
return input_items
|
|
179
|
+
|
|
180
|
+
|
|
101
181
|
# Type alias for the optional input filter callback
|
|
102
182
|
CallModelInputFilter = Callable[[CallModelData[Any]], MaybeAwaitable[ModelInputData]]
|
|
103
183
|
|
|
@@ -125,6 +205,19 @@ class RunConfig:
|
|
|
125
205
|
agent. See the documentation in `Handoff.input_filter` for more details.
|
|
126
206
|
"""
|
|
127
207
|
|
|
208
|
+
nest_handoff_history: bool = True
|
|
209
|
+
"""Wrap prior run history in a single assistant message before handing off when no custom
|
|
210
|
+
input filter is set. Set to False to preserve the raw transcript behavior from previous
|
|
211
|
+
releases.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
handoff_history_mapper: HandoffHistoryMapper | None = None
|
|
215
|
+
"""Optional function that receives the normalized transcript (history + handoff items) and
|
|
216
|
+
returns the input history that should be passed to the next agent. When left as `None`, the
|
|
217
|
+
runner collapses the transcript into a single assistant message. This function only runs when
|
|
218
|
+
`nest_handoff_history` is True.
|
|
219
|
+
"""
|
|
220
|
+
|
|
128
221
|
input_guardrails: list[InputGuardrail[Any]] | None = None
|
|
129
222
|
"""A list of input guardrails to run on the initial run input."""
|
|
130
223
|
|
|
@@ -135,7 +228,12 @@ class RunConfig:
|
|
|
135
228
|
"""Whether tracing is disabled for the agent run. If disabled, we will not trace the agent run.
|
|
136
229
|
"""
|
|
137
230
|
|
|
138
|
-
|
|
231
|
+
tracing: TracingConfig | None = None
|
|
232
|
+
"""Tracing configuration for this run."""
|
|
233
|
+
|
|
234
|
+
trace_include_sensitive_data: bool = field(
|
|
235
|
+
default_factory=_default_trace_include_sensitive_data
|
|
236
|
+
)
|
|
139
237
|
"""Whether we include potentially sensitive data (for example: inputs/outputs of tool calls or
|
|
140
238
|
LLM generations) in traces. If False, we'll still create spans for these events, but the
|
|
141
239
|
sensitive data will not be included.
|
|
@@ -160,6 +258,13 @@ class RunConfig:
|
|
|
160
258
|
An optional dictionary of additional metadata to include with the trace.
|
|
161
259
|
"""
|
|
162
260
|
|
|
261
|
+
session_input_callback: SessionInputCallback | None = None
|
|
262
|
+
"""Defines how to handle session history when new input is provided.
|
|
263
|
+
- `None` (default): The new input is appended to the session history.
|
|
264
|
+
- `SessionInputCallback`: A custom function that receives the history and new input, and
|
|
265
|
+
returns the desired combined list of items.
|
|
266
|
+
"""
|
|
267
|
+
|
|
163
268
|
call_model_input_filter: CallModelInputFilter | None = None
|
|
164
269
|
"""
|
|
165
270
|
Optional callback that is invoked immediately before calling the model. It receives the current
|
|
@@ -189,6 +294,12 @@ class RunOptions(TypedDict, Generic[TContext]):
|
|
|
189
294
|
previous_response_id: NotRequired[str | None]
|
|
190
295
|
"""The ID of the previous response, if any."""
|
|
191
296
|
|
|
297
|
+
auto_previous_response_id: NotRequired[bool]
|
|
298
|
+
"""Enable automatic response chaining for the first turn."""
|
|
299
|
+
|
|
300
|
+
conversation_id: NotRequired[str | None]
|
|
301
|
+
"""The ID of the stored conversation, if any."""
|
|
302
|
+
|
|
192
303
|
session: NotRequired[Session | None]
|
|
193
304
|
"""The session for the run."""
|
|
194
305
|
|
|
@@ -205,34 +316,58 @@ class Runner:
|
|
|
205
316
|
hooks: RunHooks[TContext] | None = None,
|
|
206
317
|
run_config: RunConfig | None = None,
|
|
207
318
|
previous_response_id: str | None = None,
|
|
319
|
+
auto_previous_response_id: bool = False,
|
|
320
|
+
conversation_id: str | None = None,
|
|
208
321
|
session: Session | None = None,
|
|
209
322
|
) -> RunResult:
|
|
210
|
-
"""
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
323
|
+
"""
|
|
324
|
+
Run a workflow starting at the given agent.
|
|
325
|
+
|
|
326
|
+
The agent will run in a loop until a final output is generated. The loop runs like so:
|
|
327
|
+
|
|
328
|
+
1. The agent is invoked with the given input.
|
|
329
|
+
2. If there is a final output (i.e. the agent produces something of type
|
|
330
|
+
`agent.output_type`), the loop terminates.
|
|
331
|
+
3. If there's a handoff, we run the loop again, with the new agent.
|
|
332
|
+
4. Else, we run tool calls (if any), and re-run the loop.
|
|
333
|
+
|
|
217
334
|
In two cases, the agent may raise an exception:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
335
|
+
|
|
336
|
+
1. If the max_turns is exceeded, a MaxTurnsExceeded exception is raised.
|
|
337
|
+
2. If a guardrail tripwire is triggered, a GuardrailTripwireTriggered
|
|
338
|
+
exception is raised.
|
|
339
|
+
|
|
340
|
+
Note:
|
|
341
|
+
Only the first agent's input guardrails are run.
|
|
342
|
+
|
|
221
343
|
Args:
|
|
222
344
|
starting_agent: The starting agent to run.
|
|
223
|
-
input: The initial input to the agent. You can pass a single string for a
|
|
224
|
-
or a list of input items.
|
|
345
|
+
input: The initial input to the agent. You can pass a single string for a
|
|
346
|
+
user message, or a list of input items.
|
|
225
347
|
context: The context to run the agent with.
|
|
226
|
-
max_turns: The maximum number of turns to run the agent for. A turn is
|
|
227
|
-
AI invocation (including any tool calls that might occur).
|
|
348
|
+
max_turns: The maximum number of turns to run the agent for. A turn is
|
|
349
|
+
defined as one AI invocation (including any tool calls that might occur).
|
|
228
350
|
hooks: An object that receives callbacks on various lifecycle events.
|
|
229
351
|
run_config: Global settings for the entire agent run.
|
|
230
|
-
previous_response_id: The ID of the previous response
|
|
231
|
-
Responses API, this allows you to skip passing in input
|
|
352
|
+
previous_response_id: The ID of the previous response. If using OpenAI
|
|
353
|
+
models via the Responses API, this allows you to skip passing in input
|
|
354
|
+
from the previous turn.
|
|
355
|
+
conversation_id: The conversation ID
|
|
356
|
+
(https://platform.openai.com/docs/guides/conversation-state?api-mode=responses).
|
|
357
|
+
If provided, the conversation will be used to read and write items.
|
|
358
|
+
Every agent will have access to the conversation history so far,
|
|
359
|
+
and its output items will be written to the conversation.
|
|
360
|
+
We recommend only using this if you are exclusively using OpenAI models;
|
|
361
|
+
other model providers don't write to the Conversation object,
|
|
362
|
+
so you'll end up having partial conversations stored.
|
|
363
|
+
session: A session for automatic conversation history management.
|
|
364
|
+
|
|
232
365
|
Returns:
|
|
233
|
-
A run result containing all the inputs, guardrail results and the output of
|
|
234
|
-
agent. Agents may perform handoffs, so we don't know the specific
|
|
366
|
+
A run result containing all the inputs, guardrail results and the output of
|
|
367
|
+
the last agent. Agents may perform handoffs, so we don't know the specific
|
|
368
|
+
type of the output.
|
|
235
369
|
"""
|
|
370
|
+
|
|
236
371
|
runner = DEFAULT_AGENT_RUNNER
|
|
237
372
|
return await runner.run(
|
|
238
373
|
starting_agent,
|
|
@@ -242,6 +377,8 @@ class Runner:
|
|
|
242
377
|
hooks=hooks,
|
|
243
378
|
run_config=run_config,
|
|
244
379
|
previous_response_id=previous_response_id,
|
|
380
|
+
auto_previous_response_id=auto_previous_response_id,
|
|
381
|
+
conversation_id=conversation_id,
|
|
245
382
|
session=session,
|
|
246
383
|
)
|
|
247
384
|
|
|
@@ -256,37 +393,56 @@ class Runner:
|
|
|
256
393
|
hooks: RunHooks[TContext] | None = None,
|
|
257
394
|
run_config: RunConfig | None = None,
|
|
258
395
|
previous_response_id: str | None = None,
|
|
396
|
+
auto_previous_response_id: bool = False,
|
|
397
|
+
conversation_id: str | None = None,
|
|
259
398
|
session: Session | None = None,
|
|
260
399
|
) -> RunResult:
|
|
261
|
-
"""
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
400
|
+
"""
|
|
401
|
+
Run a workflow synchronously, starting at the given agent.
|
|
402
|
+
|
|
403
|
+
Note:
|
|
404
|
+
This just wraps the `run` method, so it will not work if there's already an
|
|
405
|
+
event loop (e.g. inside an async function, or in a Jupyter notebook or async
|
|
406
|
+
context like FastAPI). For those cases, use the `run` method instead.
|
|
407
|
+
|
|
408
|
+
The agent will run in a loop until a final output is generated. The loop runs:
|
|
409
|
+
|
|
410
|
+
1. The agent is invoked with the given input.
|
|
411
|
+
2. If there is a final output (i.e. the agent produces something of type
|
|
412
|
+
`agent.output_type`), the loop terminates.
|
|
413
|
+
3. If there's a handoff, we run the loop again, with the new agent.
|
|
414
|
+
4. Else, we run tool calls (if any), and re-run the loop.
|
|
415
|
+
|
|
271
416
|
In two cases, the agent may raise an exception:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
417
|
+
|
|
418
|
+
1. If the max_turns is exceeded, a MaxTurnsExceeded exception is raised.
|
|
419
|
+
2. If a guardrail tripwire is triggered, a GuardrailTripwireTriggered
|
|
420
|
+
exception is raised.
|
|
421
|
+
|
|
422
|
+
Note:
|
|
423
|
+
Only the first agent's input guardrails are run.
|
|
424
|
+
|
|
275
425
|
Args:
|
|
276
426
|
starting_agent: The starting agent to run.
|
|
277
|
-
input: The initial input to the agent. You can pass a single string for a
|
|
278
|
-
or a list of input items.
|
|
427
|
+
input: The initial input to the agent. You can pass a single string for a
|
|
428
|
+
user message, or a list of input items.
|
|
279
429
|
context: The context to run the agent with.
|
|
280
|
-
max_turns: The maximum number of turns to run the agent for. A turn is
|
|
281
|
-
AI invocation (including any tool calls that might occur).
|
|
430
|
+
max_turns: The maximum number of turns to run the agent for. A turn is
|
|
431
|
+
defined as one AI invocation (including any tool calls that might occur).
|
|
282
432
|
hooks: An object that receives callbacks on various lifecycle events.
|
|
283
433
|
run_config: Global settings for the entire agent run.
|
|
284
|
-
previous_response_id: The ID of the previous response, if using OpenAI
|
|
285
|
-
Responses API, this allows you to skip passing in input
|
|
434
|
+
previous_response_id: The ID of the previous response, if using OpenAI
|
|
435
|
+
models via the Responses API, this allows you to skip passing in input
|
|
436
|
+
from the previous turn.
|
|
437
|
+
conversation_id: The ID of the stored conversation, if any.
|
|
438
|
+
session: A session for automatic conversation history management.
|
|
439
|
+
|
|
286
440
|
Returns:
|
|
287
|
-
A run result containing all the inputs, guardrail results and the output of
|
|
288
|
-
agent. Agents may perform handoffs, so we don't know the specific
|
|
441
|
+
A run result containing all the inputs, guardrail results and the output of
|
|
442
|
+
the last agent. Agents may perform handoffs, so we don't know the specific
|
|
443
|
+
type of the output.
|
|
289
444
|
"""
|
|
445
|
+
|
|
290
446
|
runner = DEFAULT_AGENT_RUNNER
|
|
291
447
|
return runner.run_sync(
|
|
292
448
|
starting_agent,
|
|
@@ -296,7 +452,9 @@ class Runner:
|
|
|
296
452
|
hooks=hooks,
|
|
297
453
|
run_config=run_config,
|
|
298
454
|
previous_response_id=previous_response_id,
|
|
455
|
+
conversation_id=conversation_id,
|
|
299
456
|
session=session,
|
|
457
|
+
auto_previous_response_id=auto_previous_response_id,
|
|
300
458
|
)
|
|
301
459
|
|
|
302
460
|
@classmethod
|
|
@@ -309,34 +467,53 @@ class Runner:
|
|
|
309
467
|
hooks: RunHooks[TContext] | None = None,
|
|
310
468
|
run_config: RunConfig | None = None,
|
|
311
469
|
previous_response_id: str | None = None,
|
|
470
|
+
auto_previous_response_id: bool = False,
|
|
471
|
+
conversation_id: str | None = None,
|
|
312
472
|
session: Session | None = None,
|
|
313
473
|
) -> RunResultStreaming:
|
|
314
|
-
"""
|
|
315
|
-
|
|
474
|
+
"""
|
|
475
|
+
Run a workflow starting at the given agent in streaming mode.
|
|
476
|
+
|
|
477
|
+
The returned result object contains a method you can use to stream semantic
|
|
478
|
+
events as they are generated.
|
|
479
|
+
|
|
316
480
|
The agent will run in a loop until a final output is generated. The loop runs like so:
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
481
|
+
|
|
482
|
+
1. The agent is invoked with the given input.
|
|
483
|
+
2. If there is a final output (i.e. the agent produces something of type
|
|
484
|
+
`agent.output_type`), the loop terminates.
|
|
485
|
+
3. If there's a handoff, we run the loop again, with the new agent.
|
|
486
|
+
4. Else, we run tool calls (if any), and re-run the loop.
|
|
487
|
+
|
|
322
488
|
In two cases, the agent may raise an exception:
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
489
|
+
|
|
490
|
+
1. If the max_turns is exceeded, a MaxTurnsExceeded exception is raised.
|
|
491
|
+
2. If a guardrail tripwire is triggered, a GuardrailTripwireTriggered
|
|
492
|
+
exception is raised.
|
|
493
|
+
|
|
494
|
+
Note:
|
|
495
|
+
Only the first agent's input guardrails are run.
|
|
496
|
+
|
|
326
497
|
Args:
|
|
327
498
|
starting_agent: The starting agent to run.
|
|
328
|
-
input: The initial input to the agent. You can pass a single string for a
|
|
329
|
-
or a list of input items.
|
|
499
|
+
input: The initial input to the agent. You can pass a single string for a
|
|
500
|
+
user message, or a list of input items.
|
|
330
501
|
context: The context to run the agent with.
|
|
331
|
-
max_turns: The maximum number of turns to run the agent for. A turn is
|
|
332
|
-
AI invocation (including any tool calls that might occur).
|
|
502
|
+
max_turns: The maximum number of turns to run the agent for. A turn is
|
|
503
|
+
defined as one AI invocation (including any tool calls that might occur).
|
|
333
504
|
hooks: An object that receives callbacks on various lifecycle events.
|
|
334
505
|
run_config: Global settings for the entire agent run.
|
|
335
|
-
previous_response_id: The ID of the previous response, if using OpenAI
|
|
336
|
-
Responses API, this allows you to skip passing in input
|
|
506
|
+
previous_response_id: The ID of the previous response, if using OpenAI
|
|
507
|
+
models via the Responses API, this allows you to skip passing in input
|
|
508
|
+
from the previous turn.
|
|
509
|
+
conversation_id: The ID of the stored conversation, if any.
|
|
510
|
+
session: A session for automatic conversation history management.
|
|
511
|
+
|
|
337
512
|
Returns:
|
|
338
|
-
A result object that contains data about the run, as well as a method to
|
|
513
|
+
A result object that contains data about the run, as well as a method to
|
|
514
|
+
stream events.
|
|
339
515
|
"""
|
|
516
|
+
|
|
340
517
|
runner = DEFAULT_AGENT_RUNNER
|
|
341
518
|
return runner.run_streamed(
|
|
342
519
|
starting_agent,
|
|
@@ -346,6 +523,8 @@ class Runner:
|
|
|
346
523
|
hooks=hooks,
|
|
347
524
|
run_config=run_config,
|
|
348
525
|
previous_response_id=previous_response_id,
|
|
526
|
+
auto_previous_response_id=auto_previous_response_id,
|
|
527
|
+
conversation_id=conversation_id,
|
|
349
528
|
session=session,
|
|
350
529
|
)
|
|
351
530
|
|
|
@@ -364,17 +543,35 @@ class AgentRunner:
|
|
|
364
543
|
) -> RunResult:
|
|
365
544
|
context = kwargs.get("context")
|
|
366
545
|
max_turns = kwargs.get("max_turns", DEFAULT_MAX_TURNS)
|
|
367
|
-
hooks = kwargs.get("hooks")
|
|
546
|
+
hooks = cast(RunHooks[TContext], self._validate_run_hooks(kwargs.get("hooks")))
|
|
368
547
|
run_config = kwargs.get("run_config")
|
|
369
548
|
previous_response_id = kwargs.get("previous_response_id")
|
|
549
|
+
auto_previous_response_id = kwargs.get("auto_previous_response_id", False)
|
|
550
|
+
conversation_id = kwargs.get("conversation_id")
|
|
370
551
|
session = kwargs.get("session")
|
|
371
|
-
|
|
372
|
-
hooks = RunHooks[Any]()
|
|
552
|
+
|
|
373
553
|
if run_config is None:
|
|
374
554
|
run_config = RunConfig()
|
|
375
555
|
|
|
376
|
-
#
|
|
377
|
-
|
|
556
|
+
# Check whether to enable OpenAI server-managed conversation
|
|
557
|
+
if (
|
|
558
|
+
conversation_id is not None
|
|
559
|
+
or previous_response_id is not None
|
|
560
|
+
or auto_previous_response_id
|
|
561
|
+
):
|
|
562
|
+
server_conversation_tracker = _ServerConversationTracker(
|
|
563
|
+
conversation_id=conversation_id,
|
|
564
|
+
previous_response_id=previous_response_id,
|
|
565
|
+
auto_previous_response_id=auto_previous_response_id,
|
|
566
|
+
)
|
|
567
|
+
else:
|
|
568
|
+
server_conversation_tracker = None
|
|
569
|
+
|
|
570
|
+
# Keep original user input separate from session-prepared input
|
|
571
|
+
original_user_input = input
|
|
572
|
+
prepared_input = await self._prepare_input_with_session(
|
|
573
|
+
input, session, run_config.session_input_callback
|
|
574
|
+
)
|
|
378
575
|
|
|
379
576
|
tool_use_tracker = AgentToolUseTracker()
|
|
380
577
|
|
|
@@ -383,11 +580,13 @@ class AgentRunner:
|
|
|
383
580
|
trace_id=run_config.trace_id,
|
|
384
581
|
group_id=run_config.group_id,
|
|
385
582
|
metadata=run_config.trace_metadata,
|
|
583
|
+
tracing=run_config.tracing,
|
|
386
584
|
disabled=run_config.tracing_disabled,
|
|
387
585
|
):
|
|
388
586
|
current_turn = 0
|
|
389
587
|
original_input: str | list[TResponseInputItem] = _copy_str_or_list(prepared_input)
|
|
390
|
-
generated_items: list[RunItem] = []
|
|
588
|
+
generated_items: list[RunItem] = [] # For model input (may be filtered on handoffs)
|
|
589
|
+
session_items: list[RunItem] = [] # For observability (always unfiltered)
|
|
391
590
|
model_responses: list[ModelResponse] = []
|
|
392
591
|
|
|
393
592
|
context_wrapper: RunContextWrapper[TContext] = RunContextWrapper(
|
|
@@ -395,14 +594,22 @@ class AgentRunner:
|
|
|
395
594
|
)
|
|
396
595
|
|
|
397
596
|
input_guardrail_results: list[InputGuardrailResult] = []
|
|
597
|
+
tool_input_guardrail_results: list[ToolInputGuardrailResult] = []
|
|
598
|
+
tool_output_guardrail_results: list[ToolOutputGuardrailResult] = []
|
|
398
599
|
|
|
399
600
|
current_span: Span[AgentSpanData] | None = None
|
|
400
601
|
current_agent = starting_agent
|
|
401
602
|
should_run_agent_start_hooks = True
|
|
402
603
|
|
|
604
|
+
# save only the new user input to the session, not the combined history
|
|
605
|
+
await self._save_result_to_session(session, original_user_input, [])
|
|
606
|
+
|
|
403
607
|
try:
|
|
404
608
|
while True:
|
|
405
609
|
all_tools = await AgentRunner._get_all_tools(current_agent, context_wrapper)
|
|
610
|
+
await RunImpl.initialize_computer_tools(
|
|
611
|
+
tools=all_tools, context_wrapper=context_wrapper
|
|
612
|
+
)
|
|
406
613
|
|
|
407
614
|
# Start an agent span if we don't have one. This span is ended if the current
|
|
408
615
|
# agent changes, or if the agent loop ends.
|
|
@@ -440,11 +647,31 @@ class AgentRunner:
|
|
|
440
647
|
)
|
|
441
648
|
|
|
442
649
|
if current_turn == 1:
|
|
650
|
+
# Separate guardrails based on execution mode.
|
|
651
|
+
all_input_guardrails = starting_agent.input_guardrails + (
|
|
652
|
+
run_config.input_guardrails or []
|
|
653
|
+
)
|
|
654
|
+
sequential_guardrails = [
|
|
655
|
+
g for g in all_input_guardrails if not g.run_in_parallel
|
|
656
|
+
]
|
|
657
|
+
parallel_guardrails = [g for g in all_input_guardrails if g.run_in_parallel]
|
|
658
|
+
|
|
659
|
+
# Run blocking guardrails first, before agent starts.
|
|
660
|
+
# (will raise exception if tripwire triggered).
|
|
661
|
+
sequential_results = []
|
|
662
|
+
if sequential_guardrails:
|
|
663
|
+
sequential_results = await self._run_input_guardrails(
|
|
664
|
+
starting_agent,
|
|
665
|
+
sequential_guardrails,
|
|
666
|
+
_copy_str_or_list(prepared_input),
|
|
667
|
+
context_wrapper,
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
# Run parallel guardrails + agent together.
|
|
443
671
|
input_guardrail_results, turn_result = await asyncio.gather(
|
|
444
672
|
self._run_input_guardrails(
|
|
445
673
|
starting_agent,
|
|
446
|
-
|
|
447
|
-
+ (run_config.input_guardrails or []),
|
|
674
|
+
parallel_guardrails,
|
|
448
675
|
_copy_str_or_list(prepared_input),
|
|
449
676
|
context_wrapper,
|
|
450
677
|
),
|
|
@@ -458,9 +685,12 @@ class AgentRunner:
|
|
|
458
685
|
run_config=run_config,
|
|
459
686
|
should_run_agent_start_hooks=should_run_agent_start_hooks,
|
|
460
687
|
tool_use_tracker=tool_use_tracker,
|
|
461
|
-
|
|
688
|
+
server_conversation_tracker=server_conversation_tracker,
|
|
462
689
|
),
|
|
463
690
|
)
|
|
691
|
+
|
|
692
|
+
# Combine sequential and parallel results.
|
|
693
|
+
input_guardrail_results = sequential_results + input_guardrail_results
|
|
464
694
|
else:
|
|
465
695
|
turn_result = await self._run_single_turn(
|
|
466
696
|
agent=current_agent,
|
|
@@ -472,51 +702,111 @@ class AgentRunner:
|
|
|
472
702
|
run_config=run_config,
|
|
473
703
|
should_run_agent_start_hooks=should_run_agent_start_hooks,
|
|
474
704
|
tool_use_tracker=tool_use_tracker,
|
|
475
|
-
|
|
705
|
+
server_conversation_tracker=server_conversation_tracker,
|
|
476
706
|
)
|
|
477
707
|
should_run_agent_start_hooks = False
|
|
478
708
|
|
|
479
709
|
model_responses.append(turn_result.model_response)
|
|
480
710
|
original_input = turn_result.original_input
|
|
481
|
-
|
|
711
|
+
# For model input, use new_step_items (filtered on handoffs)
|
|
712
|
+
generated_items = turn_result.pre_step_items + turn_result.new_step_items
|
|
713
|
+
# Accumulate unfiltered items for observability
|
|
714
|
+
session_items_for_turn = (
|
|
715
|
+
turn_result.session_step_items
|
|
716
|
+
if turn_result.session_step_items is not None
|
|
717
|
+
else turn_result.new_step_items
|
|
718
|
+
)
|
|
719
|
+
session_items.extend(session_items_for_turn)
|
|
482
720
|
|
|
483
|
-
if
|
|
484
|
-
|
|
485
|
-
current_agent.output_guardrails + (run_config.output_guardrails or []),
|
|
486
|
-
current_agent,
|
|
487
|
-
turn_result.next_step.output,
|
|
488
|
-
context_wrapper,
|
|
489
|
-
)
|
|
490
|
-
result = RunResult(
|
|
491
|
-
input=original_input,
|
|
492
|
-
new_items=generated_items,
|
|
493
|
-
raw_responses=model_responses,
|
|
494
|
-
final_output=turn_result.next_step.output,
|
|
495
|
-
_last_agent=current_agent,
|
|
496
|
-
input_guardrail_results=input_guardrail_results,
|
|
497
|
-
output_guardrail_results=output_guardrail_results,
|
|
498
|
-
context_wrapper=context_wrapper,
|
|
499
|
-
)
|
|
721
|
+
if server_conversation_tracker is not None:
|
|
722
|
+
server_conversation_tracker.track_server_items(turn_result.model_response)
|
|
500
723
|
|
|
501
|
-
|
|
502
|
-
|
|
724
|
+
# Collect tool guardrail results from this turn
|
|
725
|
+
tool_input_guardrail_results.extend(turn_result.tool_input_guardrail_results)
|
|
726
|
+
tool_output_guardrail_results.extend(turn_result.tool_output_guardrail_results)
|
|
503
727
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
728
|
+
try:
|
|
729
|
+
if isinstance(turn_result.next_step, NextStepFinalOutput):
|
|
730
|
+
output_guardrail_results = await self._run_output_guardrails(
|
|
731
|
+
current_agent.output_guardrails
|
|
732
|
+
+ (run_config.output_guardrails or []),
|
|
733
|
+
current_agent,
|
|
734
|
+
turn_result.next_step.output,
|
|
735
|
+
context_wrapper,
|
|
736
|
+
)
|
|
737
|
+
result = RunResult(
|
|
738
|
+
input=original_input,
|
|
739
|
+
new_items=session_items, # Use unfiltered items for observability
|
|
740
|
+
raw_responses=model_responses,
|
|
741
|
+
final_output=turn_result.next_step.output,
|
|
742
|
+
_last_agent=current_agent,
|
|
743
|
+
input_guardrail_results=input_guardrail_results,
|
|
744
|
+
output_guardrail_results=output_guardrail_results,
|
|
745
|
+
tool_input_guardrail_results=tool_input_guardrail_results,
|
|
746
|
+
tool_output_guardrail_results=tool_output_guardrail_results,
|
|
747
|
+
context_wrapper=context_wrapper,
|
|
748
|
+
)
|
|
749
|
+
if not any(
|
|
750
|
+
guardrail_result.output.tripwire_triggered
|
|
751
|
+
for guardrail_result in input_guardrail_results
|
|
752
|
+
):
|
|
753
|
+
await self._save_result_to_session(
|
|
754
|
+
session,
|
|
755
|
+
[],
|
|
756
|
+
turn_result.session_step_items
|
|
757
|
+
if turn_result.session_step_items is not None
|
|
758
|
+
else turn_result.new_step_items,
|
|
759
|
+
turn_result.model_response.response_id,
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
return result
|
|
763
|
+
elif isinstance(turn_result.next_step, NextStepHandoff):
|
|
764
|
+
# Save the conversation to session if enabled (before handoff)
|
|
765
|
+
if session is not None:
|
|
766
|
+
if not any(
|
|
767
|
+
guardrail_result.output.tripwire_triggered
|
|
768
|
+
for guardrail_result in input_guardrail_results
|
|
769
|
+
):
|
|
770
|
+
await self._save_result_to_session(
|
|
771
|
+
session,
|
|
772
|
+
[],
|
|
773
|
+
turn_result.session_step_items
|
|
774
|
+
if turn_result.session_step_items is not None
|
|
775
|
+
else turn_result.new_step_items,
|
|
776
|
+
turn_result.model_response.response_id,
|
|
777
|
+
)
|
|
778
|
+
current_agent = cast(Agent[TContext], turn_result.next_step.new_agent)
|
|
779
|
+
current_span.finish(reset_current=True)
|
|
780
|
+
current_span = None
|
|
781
|
+
should_run_agent_start_hooks = True
|
|
782
|
+
elif isinstance(turn_result.next_step, NextStepRunAgain):
|
|
783
|
+
if not any(
|
|
784
|
+
guardrail_result.output.tripwire_triggered
|
|
785
|
+
for guardrail_result in input_guardrail_results
|
|
786
|
+
):
|
|
787
|
+
await self._save_result_to_session(
|
|
788
|
+
session,
|
|
789
|
+
[],
|
|
790
|
+
turn_result.session_step_items
|
|
791
|
+
if turn_result.session_step_items is not None
|
|
792
|
+
else turn_result.new_step_items,
|
|
793
|
+
turn_result.model_response.response_id,
|
|
794
|
+
)
|
|
795
|
+
else:
|
|
796
|
+
raise AgentsException(
|
|
797
|
+
f"Unknown next step type: {type(turn_result.next_step)}"
|
|
798
|
+
)
|
|
799
|
+
finally:
|
|
800
|
+
# RunImpl.execute_tools_and_side_effects returns a SingleStepResult that
|
|
801
|
+
# stores direct references to the `pre_step_items` and `new_step_items`
|
|
802
|
+
# lists it manages internally. Clear them here so the next turn does not
|
|
803
|
+
# hold on to items from previous turns and to avoid leaking agent refs.
|
|
804
|
+
turn_result.pre_step_items.clear()
|
|
805
|
+
turn_result.new_step_items.clear()
|
|
516
806
|
except AgentsException as exc:
|
|
517
807
|
exc.run_data = RunErrorDetails(
|
|
518
808
|
input=original_input,
|
|
519
|
-
new_items=
|
|
809
|
+
new_items=session_items, # Use unfiltered items for observability
|
|
520
810
|
raw_responses=model_responses,
|
|
521
811
|
last_agent=current_agent,
|
|
522
812
|
context_wrapper=context_wrapper,
|
|
@@ -525,6 +815,10 @@ class AgentRunner:
|
|
|
525
815
|
)
|
|
526
816
|
raise
|
|
527
817
|
finally:
|
|
818
|
+
try:
|
|
819
|
+
await dispose_resolved_computers(run_context=context_wrapper)
|
|
820
|
+
except Exception as error:
|
|
821
|
+
logger.warning("Failed to dispose computers after run: %s", error)
|
|
528
822
|
if current_span:
|
|
529
823
|
current_span.finish(reset_current=True)
|
|
530
824
|
|
|
@@ -539,9 +833,44 @@ class AgentRunner:
|
|
|
539
833
|
hooks = kwargs.get("hooks")
|
|
540
834
|
run_config = kwargs.get("run_config")
|
|
541
835
|
previous_response_id = kwargs.get("previous_response_id")
|
|
836
|
+
auto_previous_response_id = kwargs.get("auto_previous_response_id", False)
|
|
837
|
+
conversation_id = kwargs.get("conversation_id")
|
|
542
838
|
session = kwargs.get("session")
|
|
543
839
|
|
|
544
|
-
|
|
840
|
+
# Python 3.14 stopped implicitly wiring up a default event loop
|
|
841
|
+
# when synchronous code touches asyncio APIs for the first time.
|
|
842
|
+
# Several of our synchronous entry points (for example the Redis/SQLAlchemy session helpers)
|
|
843
|
+
# construct asyncio primitives like asyncio.Lock during __init__,
|
|
844
|
+
# which binds them to whatever loop happens to be the thread's default at that moment.
|
|
845
|
+
# To keep those locks usable we must ensure that run_sync reuses that same default loop
|
|
846
|
+
# instead of hopping over to a brand-new asyncio.run() loop.
|
|
847
|
+
try:
|
|
848
|
+
already_running_loop = asyncio.get_running_loop()
|
|
849
|
+
except RuntimeError:
|
|
850
|
+
already_running_loop = None
|
|
851
|
+
|
|
852
|
+
if already_running_loop is not None:
|
|
853
|
+
# This method is only expected to run when no loop is already active.
|
|
854
|
+
# (Each thread has its own default loop; concurrent sync runs should happen on
|
|
855
|
+
# different threads. In a single thread use the async API to interleave work.)
|
|
856
|
+
raise RuntimeError(
|
|
857
|
+
"AgentRunner.run_sync() cannot be called when an event loop is already running."
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
policy = asyncio.get_event_loop_policy()
|
|
861
|
+
with warnings.catch_warnings():
|
|
862
|
+
warnings.simplefilter("ignore", DeprecationWarning)
|
|
863
|
+
try:
|
|
864
|
+
default_loop = policy.get_event_loop()
|
|
865
|
+
except RuntimeError:
|
|
866
|
+
default_loop = policy.new_event_loop()
|
|
867
|
+
policy.set_event_loop(default_loop)
|
|
868
|
+
|
|
869
|
+
# We intentionally leave the default loop open even if we had to create one above. Session
|
|
870
|
+
# instances and other helpers stash loop-bound primitives between calls and expect to find
|
|
871
|
+
# the same default loop every time run_sync is invoked on this thread.
|
|
872
|
+
# Schedule the async run on the default loop so that we can manage cancellation explicitly.
|
|
873
|
+
task = default_loop.create_task(
|
|
545
874
|
self.run(
|
|
546
875
|
starting_agent,
|
|
547
876
|
input,
|
|
@@ -551,9 +880,29 @@ class AgentRunner:
|
|
|
551
880
|
hooks=hooks,
|
|
552
881
|
run_config=run_config,
|
|
553
882
|
previous_response_id=previous_response_id,
|
|
883
|
+
auto_previous_response_id=auto_previous_response_id,
|
|
884
|
+
conversation_id=conversation_id,
|
|
554
885
|
)
|
|
555
886
|
)
|
|
556
887
|
|
|
888
|
+
try:
|
|
889
|
+
# Drive the coroutine to completion, harvesting the final RunResult.
|
|
890
|
+
return default_loop.run_until_complete(task)
|
|
891
|
+
except BaseException:
|
|
892
|
+
# If the sync caller aborts (KeyboardInterrupt, etc.), make sure the scheduled task
|
|
893
|
+
# does not linger on the shared loop by cancelling it and waiting for completion.
|
|
894
|
+
if not task.done():
|
|
895
|
+
task.cancel()
|
|
896
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
897
|
+
default_loop.run_until_complete(task)
|
|
898
|
+
raise
|
|
899
|
+
finally:
|
|
900
|
+
if not default_loop.is_closed():
|
|
901
|
+
# The loop stays open for subsequent runs, but we still need to flush any pending
|
|
902
|
+
# async generators so their cleanup code executes promptly.
|
|
903
|
+
with contextlib.suppress(RuntimeError):
|
|
904
|
+
default_loop.run_until_complete(default_loop.shutdown_asyncgens())
|
|
905
|
+
|
|
557
906
|
def run_streamed(
|
|
558
907
|
self,
|
|
559
908
|
starting_agent: Agent[TContext],
|
|
@@ -562,13 +911,13 @@ class AgentRunner:
|
|
|
562
911
|
) -> RunResultStreaming:
|
|
563
912
|
context = kwargs.get("context")
|
|
564
913
|
max_turns = kwargs.get("max_turns", DEFAULT_MAX_TURNS)
|
|
565
|
-
hooks = kwargs.get("hooks")
|
|
914
|
+
hooks = cast(RunHooks[TContext], self._validate_run_hooks(kwargs.get("hooks")))
|
|
566
915
|
run_config = kwargs.get("run_config")
|
|
567
916
|
previous_response_id = kwargs.get("previous_response_id")
|
|
917
|
+
auto_previous_response_id = kwargs.get("auto_previous_response_id", False)
|
|
918
|
+
conversation_id = kwargs.get("conversation_id")
|
|
568
919
|
session = kwargs.get("session")
|
|
569
920
|
|
|
570
|
-
if hooks is None:
|
|
571
|
-
hooks = RunHooks[Any]()
|
|
572
921
|
if run_config is None:
|
|
573
922
|
run_config = RunConfig()
|
|
574
923
|
|
|
@@ -583,6 +932,7 @@ class AgentRunner:
|
|
|
583
932
|
trace_id=run_config.trace_id,
|
|
584
933
|
group_id=run_config.group_id,
|
|
585
934
|
metadata=run_config.trace_metadata,
|
|
935
|
+
tracing=run_config.tracing,
|
|
586
936
|
disabled=run_config.tracing_disabled,
|
|
587
937
|
)
|
|
588
938
|
)
|
|
@@ -603,6 +953,8 @@ class AgentRunner:
|
|
|
603
953
|
max_turns=max_turns,
|
|
604
954
|
input_guardrail_results=[],
|
|
605
955
|
output_guardrail_results=[],
|
|
956
|
+
tool_input_guardrail_results=[],
|
|
957
|
+
tool_output_guardrail_results=[],
|
|
606
958
|
_current_agent_output_schema=output_schema,
|
|
607
959
|
trace=new_trace,
|
|
608
960
|
context_wrapper=context_wrapper,
|
|
@@ -619,11 +971,30 @@ class AgentRunner:
|
|
|
619
971
|
context_wrapper=context_wrapper,
|
|
620
972
|
run_config=run_config,
|
|
621
973
|
previous_response_id=previous_response_id,
|
|
974
|
+
auto_previous_response_id=auto_previous_response_id,
|
|
975
|
+
conversation_id=conversation_id,
|
|
622
976
|
session=session,
|
|
623
977
|
)
|
|
624
978
|
)
|
|
625
979
|
return streamed_result
|
|
626
980
|
|
|
981
|
+
@staticmethod
|
|
982
|
+
def _validate_run_hooks(
|
|
983
|
+
hooks: RunHooksBase[Any, Agent[Any]] | AgentHooksBase[Any, Agent[Any]] | Any | None,
|
|
984
|
+
) -> RunHooks[Any]:
|
|
985
|
+
if hooks is None:
|
|
986
|
+
return RunHooks[Any]()
|
|
987
|
+
input_hook_type = type(hooks).__name__
|
|
988
|
+
if isinstance(hooks, AgentHooksBase):
|
|
989
|
+
raise TypeError(
|
|
990
|
+
"Run hooks must be instances of RunHooks. "
|
|
991
|
+
f"Received agent-scoped hooks ({input_hook_type}). "
|
|
992
|
+
"Attach AgentHooks to an Agent via Agent(..., hooks=...)."
|
|
993
|
+
)
|
|
994
|
+
if not isinstance(hooks, RunHooksBase):
|
|
995
|
+
raise TypeError(f"Run hooks must be instances of RunHooks. Received {input_hook_type}.")
|
|
996
|
+
return hooks
|
|
997
|
+
|
|
627
998
|
@classmethod
|
|
628
999
|
async def _maybe_filter_model_input(
|
|
629
1000
|
cls,
|
|
@@ -689,6 +1060,11 @@ class AgentRunner:
|
|
|
689
1060
|
for done in asyncio.as_completed(guardrail_tasks):
|
|
690
1061
|
result = await done
|
|
691
1062
|
if result.output.tripwire_triggered:
|
|
1063
|
+
# Cancel all remaining guardrail tasks if a tripwire is triggered.
|
|
1064
|
+
for t in guardrail_tasks:
|
|
1065
|
+
t.cancel()
|
|
1066
|
+
# Wait for cancellations to propagate by awaiting the cancelled tasks.
|
|
1067
|
+
await asyncio.gather(*guardrail_tasks, return_exceptions=True)
|
|
692
1068
|
_error_tracing.attach_error_to_span(
|
|
693
1069
|
parent_span,
|
|
694
1070
|
SpanError(
|
|
@@ -699,6 +1075,9 @@ class AgentRunner:
|
|
|
699
1075
|
},
|
|
700
1076
|
),
|
|
701
1077
|
)
|
|
1078
|
+
queue.put_nowait(result)
|
|
1079
|
+
guardrail_results.append(result)
|
|
1080
|
+
break
|
|
702
1081
|
queue.put_nowait(result)
|
|
703
1082
|
guardrail_results.append(result)
|
|
704
1083
|
except Exception:
|
|
@@ -706,7 +1085,9 @@ class AgentRunner:
|
|
|
706
1085
|
t.cancel()
|
|
707
1086
|
raise
|
|
708
1087
|
|
|
709
|
-
streamed_result.input_guardrail_results =
|
|
1088
|
+
streamed_result.input_guardrail_results = (
|
|
1089
|
+
streamed_result.input_guardrail_results + guardrail_results
|
|
1090
|
+
)
|
|
710
1091
|
|
|
711
1092
|
@classmethod
|
|
712
1093
|
async def _start_streaming(
|
|
@@ -719,6 +1100,8 @@ class AgentRunner:
|
|
|
719
1100
|
context_wrapper: RunContextWrapper[TContext],
|
|
720
1101
|
run_config: RunConfig,
|
|
721
1102
|
previous_response_id: str | None,
|
|
1103
|
+
auto_previous_response_id: bool,
|
|
1104
|
+
conversation_id: str | None,
|
|
722
1105
|
session: Session | None,
|
|
723
1106
|
):
|
|
724
1107
|
if streamed_result.trace:
|
|
@@ -730,20 +1113,47 @@ class AgentRunner:
|
|
|
730
1113
|
should_run_agent_start_hooks = True
|
|
731
1114
|
tool_use_tracker = AgentToolUseTracker()
|
|
732
1115
|
|
|
1116
|
+
# Check whether to enable OpenAI server-managed conversation
|
|
1117
|
+
if (
|
|
1118
|
+
conversation_id is not None
|
|
1119
|
+
or previous_response_id is not None
|
|
1120
|
+
or auto_previous_response_id
|
|
1121
|
+
):
|
|
1122
|
+
server_conversation_tracker = _ServerConversationTracker(
|
|
1123
|
+
conversation_id=conversation_id,
|
|
1124
|
+
previous_response_id=previous_response_id,
|
|
1125
|
+
auto_previous_response_id=auto_previous_response_id,
|
|
1126
|
+
)
|
|
1127
|
+
else:
|
|
1128
|
+
server_conversation_tracker = None
|
|
1129
|
+
|
|
733
1130
|
streamed_result._event_queue.put_nowait(AgentUpdatedStreamEvent(new_agent=current_agent))
|
|
734
1131
|
|
|
735
1132
|
try:
|
|
736
1133
|
# Prepare input with session if enabled
|
|
737
|
-
prepared_input = await AgentRunner._prepare_input_with_session(
|
|
1134
|
+
prepared_input = await AgentRunner._prepare_input_with_session(
|
|
1135
|
+
starting_input, session, run_config.session_input_callback
|
|
1136
|
+
)
|
|
738
1137
|
|
|
739
1138
|
# Update the streamed result with the prepared input
|
|
740
1139
|
streamed_result.input = prepared_input
|
|
741
1140
|
|
|
1141
|
+
await AgentRunner._save_result_to_session(session, starting_input, [])
|
|
1142
|
+
|
|
742
1143
|
while True:
|
|
1144
|
+
# Check for soft cancel before starting new turn
|
|
1145
|
+
if streamed_result._cancel_mode == "after_turn":
|
|
1146
|
+
streamed_result.is_complete = True
|
|
1147
|
+
streamed_result._event_queue.put_nowait(QueueCompleteSentinel())
|
|
1148
|
+
break
|
|
1149
|
+
|
|
743
1150
|
if streamed_result.is_complete:
|
|
744
1151
|
break
|
|
745
1152
|
|
|
746
1153
|
all_tools = await cls._get_all_tools(current_agent, context_wrapper)
|
|
1154
|
+
await RunImpl.initialize_computer_tools(
|
|
1155
|
+
tools=all_tools, context_wrapper=context_wrapper
|
|
1156
|
+
)
|
|
747
1157
|
|
|
748
1158
|
# Start an agent span if we don't have one. This span is ended if the current
|
|
749
1159
|
# agent changes, or if the agent loop ends.
|
|
@@ -780,11 +1190,36 @@ class AgentRunner:
|
|
|
780
1190
|
break
|
|
781
1191
|
|
|
782
1192
|
if current_turn == 1:
|
|
783
|
-
#
|
|
1193
|
+
# Separate guardrails based on execution mode.
|
|
1194
|
+
all_input_guardrails = starting_agent.input_guardrails + (
|
|
1195
|
+
run_config.input_guardrails or []
|
|
1196
|
+
)
|
|
1197
|
+
sequential_guardrails = [
|
|
1198
|
+
g for g in all_input_guardrails if not g.run_in_parallel
|
|
1199
|
+
]
|
|
1200
|
+
parallel_guardrails = [g for g in all_input_guardrails if g.run_in_parallel]
|
|
1201
|
+
|
|
1202
|
+
# Run sequential guardrails first.
|
|
1203
|
+
if sequential_guardrails:
|
|
1204
|
+
await cls._run_input_guardrails_with_queue(
|
|
1205
|
+
starting_agent,
|
|
1206
|
+
sequential_guardrails,
|
|
1207
|
+
ItemHelpers.input_to_new_input_list(prepared_input),
|
|
1208
|
+
context_wrapper,
|
|
1209
|
+
streamed_result,
|
|
1210
|
+
current_span,
|
|
1211
|
+
)
|
|
1212
|
+
# Check if any blocking guardrail triggered and raise before starting agent.
|
|
1213
|
+
for result in streamed_result.input_guardrail_results:
|
|
1214
|
+
if result.output.tripwire_triggered:
|
|
1215
|
+
streamed_result._event_queue.put_nowait(QueueCompleteSentinel())
|
|
1216
|
+
raise InputGuardrailTripwireTriggered(result)
|
|
1217
|
+
|
|
1218
|
+
# Run parallel guardrails in background.
|
|
784
1219
|
streamed_result._input_guardrails_task = asyncio.create_task(
|
|
785
1220
|
cls._run_input_guardrails_with_queue(
|
|
786
1221
|
starting_agent,
|
|
787
|
-
|
|
1222
|
+
parallel_guardrails,
|
|
788
1223
|
ItemHelpers.input_to_new_input_list(prepared_input),
|
|
789
1224
|
context_wrapper,
|
|
790
1225
|
streamed_result,
|
|
@@ -801,7 +1236,7 @@ class AgentRunner:
|
|
|
801
1236
|
should_run_agent_start_hooks,
|
|
802
1237
|
tool_use_tracker,
|
|
803
1238
|
all_tools,
|
|
804
|
-
|
|
1239
|
+
server_conversation_tracker,
|
|
805
1240
|
)
|
|
806
1241
|
should_run_agent_start_hooks = False
|
|
807
1242
|
|
|
@@ -809,9 +1244,40 @@ class AgentRunner:
|
|
|
809
1244
|
turn_result.model_response
|
|
810
1245
|
]
|
|
811
1246
|
streamed_result.input = turn_result.original_input
|
|
812
|
-
|
|
1247
|
+
# Keep filtered items for building model input on the next turn.
|
|
1248
|
+
streamed_result._model_input_items = (
|
|
1249
|
+
turn_result.pre_step_items + turn_result.new_step_items
|
|
1250
|
+
)
|
|
1251
|
+
# Accumulate unfiltered items for observability
|
|
1252
|
+
session_items_for_turn = (
|
|
1253
|
+
turn_result.session_step_items
|
|
1254
|
+
if turn_result.session_step_items is not None
|
|
1255
|
+
else turn_result.new_step_items
|
|
1256
|
+
)
|
|
1257
|
+
streamed_result.new_items.extend(session_items_for_turn)
|
|
1258
|
+
|
|
1259
|
+
if server_conversation_tracker is not None:
|
|
1260
|
+
server_conversation_tracker.track_server_items(turn_result.model_response)
|
|
813
1261
|
|
|
814
1262
|
if isinstance(turn_result.next_step, NextStepHandoff):
|
|
1263
|
+
# Save the conversation to session if enabled (before handoff)
|
|
1264
|
+
# Streaming needs to save for graceful cancellation support
|
|
1265
|
+
if session is not None:
|
|
1266
|
+
should_skip_session_save = (
|
|
1267
|
+
await AgentRunner._input_guardrail_tripwire_triggered_for_stream(
|
|
1268
|
+
streamed_result
|
|
1269
|
+
)
|
|
1270
|
+
)
|
|
1271
|
+
if should_skip_session_save is False:
|
|
1272
|
+
await AgentRunner._save_result_to_session(
|
|
1273
|
+
session,
|
|
1274
|
+
[],
|
|
1275
|
+
turn_result.session_step_items
|
|
1276
|
+
if turn_result.session_step_items is not None
|
|
1277
|
+
else turn_result.new_step_items,
|
|
1278
|
+
turn_result.model_response.response_id,
|
|
1279
|
+
)
|
|
1280
|
+
|
|
815
1281
|
current_agent = turn_result.next_step.new_agent
|
|
816
1282
|
current_span.finish(reset_current=True)
|
|
817
1283
|
current_span = None
|
|
@@ -819,6 +1285,12 @@ class AgentRunner:
|
|
|
819
1285
|
streamed_result._event_queue.put_nowait(
|
|
820
1286
|
AgentUpdatedStreamEvent(new_agent=current_agent)
|
|
821
1287
|
)
|
|
1288
|
+
|
|
1289
|
+
# Check for soft cancel after handoff
|
|
1290
|
+
if streamed_result._cancel_mode == "after_turn": # type: ignore[comparison-overlap]
|
|
1291
|
+
streamed_result.is_complete = True
|
|
1292
|
+
streamed_result._event_queue.put_nowait(QueueCompleteSentinel())
|
|
1293
|
+
break
|
|
822
1294
|
elif isinstance(turn_result.next_step, NextStepFinalOutput):
|
|
823
1295
|
streamed_result._output_guardrails_task = asyncio.create_task(
|
|
824
1296
|
cls._run_output_guardrails(
|
|
@@ -841,24 +1313,45 @@ class AgentRunner:
|
|
|
841
1313
|
streamed_result.is_complete = True
|
|
842
1314
|
|
|
843
1315
|
# Save the conversation to session if enabled
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1316
|
+
if session is not None:
|
|
1317
|
+
should_skip_session_save = (
|
|
1318
|
+
await AgentRunner._input_guardrail_tripwire_triggered_for_stream(
|
|
1319
|
+
streamed_result
|
|
1320
|
+
)
|
|
1321
|
+
)
|
|
1322
|
+
if should_skip_session_save is False:
|
|
1323
|
+
await AgentRunner._save_result_to_session(
|
|
1324
|
+
session,
|
|
1325
|
+
[],
|
|
1326
|
+
turn_result.session_step_items
|
|
1327
|
+
if turn_result.session_step_items is not None
|
|
1328
|
+
else turn_result.new_step_items,
|
|
1329
|
+
turn_result.model_response.response_id,
|
|
1330
|
+
)
|
|
858
1331
|
|
|
859
1332
|
streamed_result._event_queue.put_nowait(QueueCompleteSentinel())
|
|
860
1333
|
elif isinstance(turn_result.next_step, NextStepRunAgain):
|
|
861
|
-
|
|
1334
|
+
if session is not None:
|
|
1335
|
+
should_skip_session_save = (
|
|
1336
|
+
await AgentRunner._input_guardrail_tripwire_triggered_for_stream(
|
|
1337
|
+
streamed_result
|
|
1338
|
+
)
|
|
1339
|
+
)
|
|
1340
|
+
if should_skip_session_save is False:
|
|
1341
|
+
await AgentRunner._save_result_to_session(
|
|
1342
|
+
session,
|
|
1343
|
+
[],
|
|
1344
|
+
turn_result.session_step_items
|
|
1345
|
+
if turn_result.session_step_items is not None
|
|
1346
|
+
else turn_result.new_step_items,
|
|
1347
|
+
turn_result.model_response.response_id,
|
|
1348
|
+
)
|
|
1349
|
+
|
|
1350
|
+
# Check for soft cancel after turn completion
|
|
1351
|
+
if streamed_result._cancel_mode == "after_turn": # type: ignore[comparison-overlap]
|
|
1352
|
+
streamed_result.is_complete = True
|
|
1353
|
+
streamed_result._event_queue.put_nowait(QueueCompleteSentinel())
|
|
1354
|
+
break
|
|
862
1355
|
except AgentsException as exc:
|
|
863
1356
|
streamed_result.is_complete = True
|
|
864
1357
|
streamed_result._event_queue.put_nowait(QueueCompleteSentinel())
|
|
@@ -887,11 +1380,32 @@ class AgentRunner:
|
|
|
887
1380
|
|
|
888
1381
|
streamed_result.is_complete = True
|
|
889
1382
|
finally:
|
|
1383
|
+
if streamed_result._input_guardrails_task:
|
|
1384
|
+
try:
|
|
1385
|
+
await AgentRunner._input_guardrail_tripwire_triggered_for_stream(
|
|
1386
|
+
streamed_result
|
|
1387
|
+
)
|
|
1388
|
+
except Exception as e:
|
|
1389
|
+
logger.debug(
|
|
1390
|
+
f"Error in streamed_result finalize for agent {current_agent.name} - {e}"
|
|
1391
|
+
)
|
|
1392
|
+
try:
|
|
1393
|
+
await dispose_resolved_computers(run_context=context_wrapper)
|
|
1394
|
+
except Exception as error:
|
|
1395
|
+
logger.warning("Failed to dispose computers after streamed run: %s", error)
|
|
890
1396
|
if current_span:
|
|
891
1397
|
current_span.finish(reset_current=True)
|
|
892
1398
|
if streamed_result.trace:
|
|
893
1399
|
streamed_result.trace.finish(reset_current=True)
|
|
894
1400
|
|
|
1401
|
+
# Ensure QueueCompleteSentinel is always put in the queue when the stream ends,
|
|
1402
|
+
# even if an exception occurs before the inner try/except block (e.g., in
|
|
1403
|
+
# _save_result_to_session at the beginning). Without this, stream_events()
|
|
1404
|
+
# would hang forever waiting for more items.
|
|
1405
|
+
if not streamed_result.is_complete:
|
|
1406
|
+
streamed_result.is_complete = True
|
|
1407
|
+
streamed_result._event_queue.put_nowait(QueueCompleteSentinel())
|
|
1408
|
+
|
|
895
1409
|
@classmethod
|
|
896
1410
|
async def _run_single_turn_streamed(
|
|
897
1411
|
cls,
|
|
@@ -903,13 +1417,21 @@ class AgentRunner:
|
|
|
903
1417
|
should_run_agent_start_hooks: bool,
|
|
904
1418
|
tool_use_tracker: AgentToolUseTracker,
|
|
905
1419
|
all_tools: list[Tool],
|
|
906
|
-
|
|
1420
|
+
server_conversation_tracker: _ServerConversationTracker | None = None,
|
|
907
1421
|
) -> SingleStepResult:
|
|
1422
|
+
emitted_tool_call_ids: set[str] = set()
|
|
1423
|
+
emitted_reasoning_item_ids: set[str] = set()
|
|
1424
|
+
|
|
908
1425
|
if should_run_agent_start_hooks:
|
|
1426
|
+
agent_hook_context = AgentHookContext(
|
|
1427
|
+
context=context_wrapper.context,
|
|
1428
|
+
usage=context_wrapper.usage,
|
|
1429
|
+
turn_input=ItemHelpers.input_to_new_input_list(streamed_result.input),
|
|
1430
|
+
)
|
|
909
1431
|
await asyncio.gather(
|
|
910
|
-
hooks.on_agent_start(
|
|
1432
|
+
hooks.on_agent_start(agent_hook_context, agent),
|
|
911
1433
|
(
|
|
912
|
-
agent.hooks.on_start(
|
|
1434
|
+
agent.hooks.on_start(agent_hook_context, agent)
|
|
913
1435
|
if agent.hooks
|
|
914
1436
|
else _coro.noop_coroutine()
|
|
915
1437
|
),
|
|
@@ -932,9 +1454,15 @@ class AgentRunner:
|
|
|
932
1454
|
|
|
933
1455
|
final_response: ModelResponse | None = None
|
|
934
1456
|
|
|
935
|
-
|
|
936
|
-
|
|
1457
|
+
if server_conversation_tracker is not None:
|
|
1458
|
+
input = server_conversation_tracker.prepare_input(
|
|
1459
|
+
streamed_result.input, streamed_result._model_input_items
|
|
1460
|
+
)
|
|
1461
|
+
else:
|
|
1462
|
+
input = ItemHelpers.input_to_new_input_list(streamed_result.input)
|
|
1463
|
+
input.extend([item.to_input_item() for item in streamed_result._model_input_items])
|
|
937
1464
|
|
|
1465
|
+
# THIS IS THE RESOLVED CONFLICT BLOCK
|
|
938
1466
|
filtered = await cls._maybe_filter_model_input(
|
|
939
1467
|
agent=agent,
|
|
940
1468
|
run_config=run_config,
|
|
@@ -943,6 +1471,28 @@ class AgentRunner:
|
|
|
943
1471
|
system_instructions=system_prompt,
|
|
944
1472
|
)
|
|
945
1473
|
|
|
1474
|
+
# Call hook just before the model is invoked, with the correct system_prompt.
|
|
1475
|
+
await asyncio.gather(
|
|
1476
|
+
hooks.on_llm_start(context_wrapper, agent, filtered.instructions, filtered.input),
|
|
1477
|
+
(
|
|
1478
|
+
agent.hooks.on_llm_start(
|
|
1479
|
+
context_wrapper, agent, filtered.instructions, filtered.input
|
|
1480
|
+
)
|
|
1481
|
+
if agent.hooks
|
|
1482
|
+
else _coro.noop_coroutine()
|
|
1483
|
+
),
|
|
1484
|
+
)
|
|
1485
|
+
|
|
1486
|
+
previous_response_id = (
|
|
1487
|
+
server_conversation_tracker.previous_response_id
|
|
1488
|
+
if server_conversation_tracker
|
|
1489
|
+
and server_conversation_tracker.previous_response_id is not None
|
|
1490
|
+
else None
|
|
1491
|
+
)
|
|
1492
|
+
conversation_id = (
|
|
1493
|
+
server_conversation_tracker.conversation_id if server_conversation_tracker else None
|
|
1494
|
+
)
|
|
1495
|
+
|
|
946
1496
|
# 1. Stream the output events
|
|
947
1497
|
async for event in model.stream_response(
|
|
948
1498
|
filtered.instructions,
|
|
@@ -955,8 +1505,12 @@ class AgentRunner:
|
|
|
955
1505
|
run_config.tracing_disabled, run_config.trace_include_sensitive_data
|
|
956
1506
|
),
|
|
957
1507
|
previous_response_id=previous_response_id,
|
|
1508
|
+
conversation_id=conversation_id,
|
|
958
1509
|
prompt=prompt_config,
|
|
959
1510
|
):
|
|
1511
|
+
# Emit the raw event ASAP
|
|
1512
|
+
streamed_result._event_queue.put_nowait(RawResponsesStreamEvent(data=event))
|
|
1513
|
+
|
|
960
1514
|
if isinstance(event, ResponseCompletedEvent):
|
|
961
1515
|
usage = (
|
|
962
1516
|
Usage(
|
|
@@ -977,16 +1531,56 @@ class AgentRunner:
|
|
|
977
1531
|
)
|
|
978
1532
|
context_wrapper.usage.add(usage)
|
|
979
1533
|
|
|
980
|
-
|
|
1534
|
+
if isinstance(event, ResponseOutputItemDoneEvent):
|
|
1535
|
+
output_item = event.item
|
|
1536
|
+
|
|
1537
|
+
if isinstance(output_item, _TOOL_CALL_TYPES):
|
|
1538
|
+
call_id: str | None = getattr(
|
|
1539
|
+
output_item, "call_id", getattr(output_item, "id", None)
|
|
1540
|
+
)
|
|
1541
|
+
|
|
1542
|
+
if call_id and call_id not in emitted_tool_call_ids:
|
|
1543
|
+
emitted_tool_call_ids.add(call_id)
|
|
1544
|
+
|
|
1545
|
+
tool_item = ToolCallItem(
|
|
1546
|
+
raw_item=cast(ToolCallItemTypes, output_item),
|
|
1547
|
+
agent=agent,
|
|
1548
|
+
)
|
|
1549
|
+
streamed_result._event_queue.put_nowait(
|
|
1550
|
+
RunItemStreamEvent(item=tool_item, name="tool_called")
|
|
1551
|
+
)
|
|
1552
|
+
|
|
1553
|
+
elif isinstance(output_item, ResponseReasoningItem):
|
|
1554
|
+
reasoning_id: str | None = getattr(output_item, "id", None)
|
|
1555
|
+
|
|
1556
|
+
if reasoning_id and reasoning_id not in emitted_reasoning_item_ids:
|
|
1557
|
+
emitted_reasoning_item_ids.add(reasoning_id)
|
|
1558
|
+
|
|
1559
|
+
reasoning_item = ReasoningItem(raw_item=output_item, agent=agent)
|
|
1560
|
+
streamed_result._event_queue.put_nowait(
|
|
1561
|
+
RunItemStreamEvent(item=reasoning_item, name="reasoning_item_created")
|
|
1562
|
+
)
|
|
1563
|
+
|
|
1564
|
+
# Call hook just after the model response is finalized.
|
|
1565
|
+
if final_response is not None:
|
|
1566
|
+
await asyncio.gather(
|
|
1567
|
+
(
|
|
1568
|
+
agent.hooks.on_llm_end(context_wrapper, agent, final_response)
|
|
1569
|
+
if agent.hooks
|
|
1570
|
+
else _coro.noop_coroutine()
|
|
1571
|
+
),
|
|
1572
|
+
hooks.on_llm_end(context_wrapper, agent, final_response),
|
|
1573
|
+
)
|
|
981
1574
|
|
|
982
1575
|
# 2. At this point, the streaming is complete for this turn of the agent loop.
|
|
983
1576
|
if not final_response:
|
|
984
1577
|
raise ModelBehaviorError("Model did not produce a final response!")
|
|
985
1578
|
|
|
986
1579
|
# 3. Now, we can process the turn as we do in the non-streaming case
|
|
987
|
-
|
|
1580
|
+
single_step_result = await cls._get_single_step_result_from_response(
|
|
988
1581
|
agent=agent,
|
|
989
|
-
|
|
1582
|
+
original_input=streamed_result.input,
|
|
1583
|
+
pre_step_items=streamed_result._model_input_items,
|
|
990
1584
|
new_response=final_response,
|
|
991
1585
|
output_schema=output_schema,
|
|
992
1586
|
all_tools=all_tools,
|
|
@@ -995,8 +1589,59 @@ class AgentRunner:
|
|
|
995
1589
|
context_wrapper=context_wrapper,
|
|
996
1590
|
run_config=run_config,
|
|
997
1591
|
tool_use_tracker=tool_use_tracker,
|
|
1592
|
+
event_queue=streamed_result._event_queue,
|
|
998
1593
|
)
|
|
999
1594
|
|
|
1595
|
+
import dataclasses as _dc
|
|
1596
|
+
|
|
1597
|
+
# Stream session items (unfiltered) when available for observability.
|
|
1598
|
+
streaming_items = (
|
|
1599
|
+
single_step_result.session_step_items
|
|
1600
|
+
if single_step_result.session_step_items is not None
|
|
1601
|
+
else single_step_result.new_step_items
|
|
1602
|
+
)
|
|
1603
|
+
|
|
1604
|
+
# Filter out items that have already been sent to avoid duplicates.
|
|
1605
|
+
items_to_stream = streaming_items
|
|
1606
|
+
|
|
1607
|
+
if emitted_tool_call_ids:
|
|
1608
|
+
# Filter out tool call items that were already emitted during streaming
|
|
1609
|
+
items_to_stream = [
|
|
1610
|
+
item
|
|
1611
|
+
for item in items_to_stream
|
|
1612
|
+
if not (
|
|
1613
|
+
isinstance(item, ToolCallItem)
|
|
1614
|
+
and (
|
|
1615
|
+
call_id := getattr(
|
|
1616
|
+
item.raw_item, "call_id", getattr(item.raw_item, "id", None)
|
|
1617
|
+
)
|
|
1618
|
+
)
|
|
1619
|
+
and call_id in emitted_tool_call_ids
|
|
1620
|
+
)
|
|
1621
|
+
]
|
|
1622
|
+
|
|
1623
|
+
if emitted_reasoning_item_ids:
|
|
1624
|
+
# Filter out reasoning items that were already emitted during streaming
|
|
1625
|
+
items_to_stream = [
|
|
1626
|
+
item
|
|
1627
|
+
for item in items_to_stream
|
|
1628
|
+
if not (
|
|
1629
|
+
isinstance(item, ReasoningItem)
|
|
1630
|
+
and (reasoning_id := getattr(item.raw_item, "id", None))
|
|
1631
|
+
and reasoning_id in emitted_reasoning_item_ids
|
|
1632
|
+
)
|
|
1633
|
+
]
|
|
1634
|
+
|
|
1635
|
+
# Filter out HandoffCallItem to avoid duplicates (already sent earlier)
|
|
1636
|
+
items_to_stream = [
|
|
1637
|
+
item for item in items_to_stream if not isinstance(item, HandoffCallItem)
|
|
1638
|
+
]
|
|
1639
|
+
|
|
1640
|
+
# Create filtered result and send to queue
|
|
1641
|
+
filtered_result = _dc.replace(single_step_result, new_step_items=items_to_stream)
|
|
1642
|
+
RunImpl.stream_step_result_to_queue(filtered_result, streamed_result._event_queue)
|
|
1643
|
+
return single_step_result
|
|
1644
|
+
|
|
1000
1645
|
@classmethod
|
|
1001
1646
|
async def _run_single_turn(
|
|
1002
1647
|
cls,
|
|
@@ -1010,14 +1655,19 @@ class AgentRunner:
|
|
|
1010
1655
|
run_config: RunConfig,
|
|
1011
1656
|
should_run_agent_start_hooks: bool,
|
|
1012
1657
|
tool_use_tracker: AgentToolUseTracker,
|
|
1013
|
-
|
|
1658
|
+
server_conversation_tracker: _ServerConversationTracker | None = None,
|
|
1014
1659
|
) -> SingleStepResult:
|
|
1015
1660
|
# Ensure we run the hooks before anything else
|
|
1016
1661
|
if should_run_agent_start_hooks:
|
|
1662
|
+
agent_hook_context = AgentHookContext(
|
|
1663
|
+
context=context_wrapper.context,
|
|
1664
|
+
usage=context_wrapper.usage,
|
|
1665
|
+
turn_input=ItemHelpers.input_to_new_input_list(original_input),
|
|
1666
|
+
)
|
|
1017
1667
|
await asyncio.gather(
|
|
1018
|
-
hooks.on_agent_start(
|
|
1668
|
+
hooks.on_agent_start(agent_hook_context, agent),
|
|
1019
1669
|
(
|
|
1020
|
-
agent.hooks.on_start(
|
|
1670
|
+
agent.hooks.on_start(agent_hook_context, agent)
|
|
1021
1671
|
if agent.hooks
|
|
1022
1672
|
else _coro.noop_coroutine()
|
|
1023
1673
|
),
|
|
@@ -1030,8 +1680,11 @@ class AgentRunner:
|
|
|
1030
1680
|
|
|
1031
1681
|
output_schema = cls._get_output_schema(agent)
|
|
1032
1682
|
handoffs = await cls._get_handoffs(agent, context_wrapper)
|
|
1033
|
-
|
|
1034
|
-
|
|
1683
|
+
if server_conversation_tracker is not None:
|
|
1684
|
+
input = server_conversation_tracker.prepare_input(original_input, generated_items)
|
|
1685
|
+
else:
|
|
1686
|
+
input = ItemHelpers.input_to_new_input_list(original_input)
|
|
1687
|
+
input.extend([generated_item.to_input_item() for generated_item in generated_items])
|
|
1035
1688
|
|
|
1036
1689
|
new_response = await cls._get_new_response(
|
|
1037
1690
|
agent,
|
|
@@ -1040,10 +1693,11 @@ class AgentRunner:
|
|
|
1040
1693
|
output_schema,
|
|
1041
1694
|
all_tools,
|
|
1042
1695
|
handoffs,
|
|
1696
|
+
hooks,
|
|
1043
1697
|
context_wrapper,
|
|
1044
1698
|
run_config,
|
|
1045
1699
|
tool_use_tracker,
|
|
1046
|
-
|
|
1700
|
+
server_conversation_tracker,
|
|
1047
1701
|
prompt_config,
|
|
1048
1702
|
)
|
|
1049
1703
|
|
|
@@ -1076,6 +1730,7 @@ class AgentRunner:
|
|
|
1076
1730
|
context_wrapper: RunContextWrapper[TContext],
|
|
1077
1731
|
run_config: RunConfig,
|
|
1078
1732
|
tool_use_tracker: AgentToolUseTracker,
|
|
1733
|
+
event_queue: asyncio.Queue[StreamEvent | QueueCompleteSentinel] | None = None,
|
|
1079
1734
|
) -> SingleStepResult:
|
|
1080
1735
|
processed_response = RunImpl.process_model_response(
|
|
1081
1736
|
agent=agent,
|
|
@@ -1087,6 +1742,14 @@ class AgentRunner:
|
|
|
1087
1742
|
|
|
1088
1743
|
tool_use_tracker.add_tool_use(agent, processed_response.tools_used)
|
|
1089
1744
|
|
|
1745
|
+
# Send handoff items immediately for streaming, but avoid duplicates
|
|
1746
|
+
if event_queue is not None and processed_response.new_items:
|
|
1747
|
+
handoff_items = [
|
|
1748
|
+
item for item in processed_response.new_items if isinstance(item, HandoffCallItem)
|
|
1749
|
+
]
|
|
1750
|
+
if handoff_items:
|
|
1751
|
+
RunImpl.stream_step_items_to_queue(cast(list[RunItem], handoff_items), event_queue)
|
|
1752
|
+
|
|
1090
1753
|
return await RunImpl.execute_tools_and_side_effects(
|
|
1091
1754
|
agent=agent,
|
|
1092
1755
|
original_input=original_input,
|
|
@@ -1115,7 +1778,7 @@ class AgentRunner:
|
|
|
1115
1778
|
tool_use_tracker: AgentToolUseTracker,
|
|
1116
1779
|
) -> SingleStepResult:
|
|
1117
1780
|
original_input = streamed_result.input
|
|
1118
|
-
pre_step_items = streamed_result.
|
|
1781
|
+
pre_step_items = streamed_result._model_input_items
|
|
1119
1782
|
event_queue = streamed_result._event_queue
|
|
1120
1783
|
|
|
1121
1784
|
processed_response = RunImpl.process_model_response(
|
|
@@ -1140,10 +1803,15 @@ class AgentRunner:
|
|
|
1140
1803
|
context_wrapper=context_wrapper,
|
|
1141
1804
|
run_config=run_config,
|
|
1142
1805
|
)
|
|
1806
|
+
# Use session_step_items (unfiltered) if available for streaming observability,
|
|
1807
|
+
# otherwise fall back to new_step_items.
|
|
1808
|
+
streaming_items = (
|
|
1809
|
+
single_step_result.session_step_items
|
|
1810
|
+
if single_step_result.session_step_items is not None
|
|
1811
|
+
else single_step_result.new_step_items
|
|
1812
|
+
)
|
|
1143
1813
|
new_step_items = [
|
|
1144
|
-
item
|
|
1145
|
-
for item in single_step_result.new_step_items
|
|
1146
|
-
if item not in new_items_processed_response
|
|
1814
|
+
item for item in streaming_items if item not in new_items_processed_response
|
|
1147
1815
|
]
|
|
1148
1816
|
RunImpl.stream_step_items_to_queue(new_step_items, event_queue)
|
|
1149
1817
|
|
|
@@ -1175,6 +1843,8 @@ class AgentRunner:
|
|
|
1175
1843
|
# Cancel all guardrail tasks if a tripwire is triggered.
|
|
1176
1844
|
for t in guardrail_tasks:
|
|
1177
1845
|
t.cancel()
|
|
1846
|
+
# Wait for cancellations to propagate by awaiting the cancelled tasks.
|
|
1847
|
+
await asyncio.gather(*guardrail_tasks, return_exceptions=True)
|
|
1178
1848
|
_error_tracing.attach_error_to_current_span(
|
|
1179
1849
|
SpanError(
|
|
1180
1850
|
message="Guardrail tripwire triggered",
|
|
@@ -1234,10 +1904,11 @@ class AgentRunner:
|
|
|
1234
1904
|
output_schema: AgentOutputSchemaBase | None,
|
|
1235
1905
|
all_tools: list[Tool],
|
|
1236
1906
|
handoffs: list[Handoff],
|
|
1907
|
+
hooks: RunHooks[TContext],
|
|
1237
1908
|
context_wrapper: RunContextWrapper[TContext],
|
|
1238
1909
|
run_config: RunConfig,
|
|
1239
1910
|
tool_use_tracker: AgentToolUseTracker,
|
|
1240
|
-
|
|
1911
|
+
server_conversation_tracker: _ServerConversationTracker | None,
|
|
1241
1912
|
prompt_config: ResponsePromptParam | None,
|
|
1242
1913
|
) -> ModelResponse:
|
|
1243
1914
|
# Allow user to modify model input right before the call, if configured
|
|
@@ -1253,6 +1924,31 @@ class AgentRunner:
|
|
|
1253
1924
|
model_settings = agent.model_settings.resolve(run_config.model_settings)
|
|
1254
1925
|
model_settings = RunImpl.maybe_reset_tool_choice(agent, tool_use_tracker, model_settings)
|
|
1255
1926
|
|
|
1927
|
+
# If we have run hooks, or if the agent has hooks, we need to call them before the LLM call
|
|
1928
|
+
await asyncio.gather(
|
|
1929
|
+
hooks.on_llm_start(context_wrapper, agent, filtered.instructions, filtered.input),
|
|
1930
|
+
(
|
|
1931
|
+
agent.hooks.on_llm_start(
|
|
1932
|
+
context_wrapper,
|
|
1933
|
+
agent,
|
|
1934
|
+
filtered.instructions, # Use filtered instructions
|
|
1935
|
+
filtered.input, # Use filtered input
|
|
1936
|
+
)
|
|
1937
|
+
if agent.hooks
|
|
1938
|
+
else _coro.noop_coroutine()
|
|
1939
|
+
),
|
|
1940
|
+
)
|
|
1941
|
+
|
|
1942
|
+
previous_response_id = (
|
|
1943
|
+
server_conversation_tracker.previous_response_id
|
|
1944
|
+
if server_conversation_tracker
|
|
1945
|
+
and server_conversation_tracker.previous_response_id is not None
|
|
1946
|
+
else None
|
|
1947
|
+
)
|
|
1948
|
+
conversation_id = (
|
|
1949
|
+
server_conversation_tracker.conversation_id if server_conversation_tracker else None
|
|
1950
|
+
)
|
|
1951
|
+
|
|
1256
1952
|
new_response = await model.get_response(
|
|
1257
1953
|
system_instructions=filtered.instructions,
|
|
1258
1954
|
input=filtered.input,
|
|
@@ -1264,11 +1960,22 @@ class AgentRunner:
|
|
|
1264
1960
|
run_config.tracing_disabled, run_config.trace_include_sensitive_data
|
|
1265
1961
|
),
|
|
1266
1962
|
previous_response_id=previous_response_id,
|
|
1963
|
+
conversation_id=conversation_id,
|
|
1267
1964
|
prompt=prompt_config,
|
|
1268
1965
|
)
|
|
1269
1966
|
|
|
1270
1967
|
context_wrapper.usage.add(new_response.usage)
|
|
1271
1968
|
|
|
1969
|
+
# If we have run hooks, or if the agent has hooks, we need to call them after the LLM call
|
|
1970
|
+
await asyncio.gather(
|
|
1971
|
+
(
|
|
1972
|
+
agent.hooks.on_llm_end(context_wrapper, agent, new_response)
|
|
1973
|
+
if agent.hooks
|
|
1974
|
+
else _coro.noop_coroutine()
|
|
1975
|
+
),
|
|
1976
|
+
hooks.on_llm_end(context_wrapper, agent, new_response),
|
|
1977
|
+
)
|
|
1978
|
+
|
|
1272
1979
|
return new_response
|
|
1273
1980
|
|
|
1274
1981
|
@classmethod
|
|
@@ -1326,19 +2033,20 @@ class AgentRunner:
|
|
|
1326
2033
|
cls,
|
|
1327
2034
|
input: str | list[TResponseInputItem],
|
|
1328
2035
|
session: Session | None,
|
|
2036
|
+
session_input_callback: SessionInputCallback | None,
|
|
1329
2037
|
) -> str | list[TResponseInputItem]:
|
|
1330
2038
|
"""Prepare input by combining it with session history if enabled."""
|
|
1331
2039
|
if session is None:
|
|
1332
2040
|
return input
|
|
1333
2041
|
|
|
1334
|
-
#
|
|
1335
|
-
|
|
1336
|
-
if isinstance(input, list):
|
|
2042
|
+
# If the user doesn't specify an input callback and pass a list as input
|
|
2043
|
+
if isinstance(input, list) and not session_input_callback:
|
|
1337
2044
|
raise UserError(
|
|
1338
|
-
"
|
|
1339
|
-
"
|
|
1340
|
-
"
|
|
1341
|
-
"
|
|
2045
|
+
"When using session memory, list inputs require a "
|
|
2046
|
+
"`RunConfig.session_input_callback` to define how they should be merged "
|
|
2047
|
+
"with the conversation history. If you don't want to use a callback, "
|
|
2048
|
+
"provide your input as a string instead, or disable session memory "
|
|
2049
|
+
"(session=None) and pass a list to manage the history manually."
|
|
1342
2050
|
)
|
|
1343
2051
|
|
|
1344
2052
|
# Get previous conversation history
|
|
@@ -1347,19 +2055,32 @@ class AgentRunner:
|
|
|
1347
2055
|
# Convert input to list format
|
|
1348
2056
|
new_input_list = ItemHelpers.input_to_new_input_list(input)
|
|
1349
2057
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
2058
|
+
if session_input_callback is None:
|
|
2059
|
+
return history + new_input_list
|
|
2060
|
+
elif callable(session_input_callback):
|
|
2061
|
+
res = session_input_callback(history, new_input_list)
|
|
2062
|
+
if inspect.isawaitable(res):
|
|
2063
|
+
return await res
|
|
2064
|
+
return res
|
|
2065
|
+
else:
|
|
2066
|
+
raise UserError(
|
|
2067
|
+
f"Invalid `session_input_callback` value: {session_input_callback}. "
|
|
2068
|
+
"Choose between `None` or a custom callable function."
|
|
2069
|
+
)
|
|
1354
2070
|
|
|
1355
2071
|
@classmethod
|
|
1356
2072
|
async def _save_result_to_session(
|
|
1357
2073
|
cls,
|
|
1358
2074
|
session: Session | None,
|
|
1359
2075
|
original_input: str | list[TResponseInputItem],
|
|
1360
|
-
|
|
2076
|
+
new_items: list[RunItem],
|
|
2077
|
+
response_id: str | None = None,
|
|
1361
2078
|
) -> None:
|
|
1362
|
-
"""
|
|
2079
|
+
"""
|
|
2080
|
+
Save the conversation turn to session.
|
|
2081
|
+
It does not account for any filtering or modification performed by
|
|
2082
|
+
`RunConfig.session_input_callback`.
|
|
2083
|
+
"""
|
|
1363
2084
|
if session is None:
|
|
1364
2085
|
return
|
|
1365
2086
|
|
|
@@ -1367,16 +2088,76 @@ class AgentRunner:
|
|
|
1367
2088
|
input_list = ItemHelpers.input_to_new_input_list(original_input)
|
|
1368
2089
|
|
|
1369
2090
|
# Convert new items to input format
|
|
1370
|
-
new_items_as_input = [item.to_input_item() for item in
|
|
2091
|
+
new_items_as_input = [item.to_input_item() for item in new_items]
|
|
1371
2092
|
|
|
1372
2093
|
# Save all items from this turn
|
|
1373
2094
|
items_to_save = input_list + new_items_as_input
|
|
1374
2095
|
await session.add_items(items_to_save)
|
|
1375
2096
|
|
|
2097
|
+
# Run compaction if session supports it and we have a response_id
|
|
2098
|
+
if response_id and is_openai_responses_compaction_aware_session(session):
|
|
2099
|
+
has_local_tool_outputs = any(
|
|
2100
|
+
isinstance(item, (ToolCallOutputItem, HandoffOutputItem)) for item in new_items
|
|
2101
|
+
)
|
|
2102
|
+
if has_local_tool_outputs:
|
|
2103
|
+
defer_compaction = getattr(session, "_defer_compaction", None)
|
|
2104
|
+
if callable(defer_compaction):
|
|
2105
|
+
result = defer_compaction(response_id)
|
|
2106
|
+
if inspect.isawaitable(result):
|
|
2107
|
+
await result
|
|
2108
|
+
logger.debug(
|
|
2109
|
+
"skip: deferring compaction for response %s due to local tool outputs",
|
|
2110
|
+
response_id,
|
|
2111
|
+
)
|
|
2112
|
+
return
|
|
2113
|
+
deferred_response_id = None
|
|
2114
|
+
get_deferred = getattr(session, "_get_deferred_compaction_response_id", None)
|
|
2115
|
+
if callable(get_deferred):
|
|
2116
|
+
deferred_response_id = get_deferred()
|
|
2117
|
+
force_compaction = deferred_response_id is not None
|
|
2118
|
+
if force_compaction:
|
|
2119
|
+
logger.debug(
|
|
2120
|
+
"compact: forcing for response %s after deferred %s",
|
|
2121
|
+
response_id,
|
|
2122
|
+
deferred_response_id,
|
|
2123
|
+
)
|
|
2124
|
+
await session.run_compaction({"response_id": response_id, "force": force_compaction})
|
|
2125
|
+
|
|
2126
|
+
@staticmethod
|
|
2127
|
+
async def _input_guardrail_tripwire_triggered_for_stream(
|
|
2128
|
+
streamed_result: RunResultStreaming,
|
|
2129
|
+
) -> bool:
|
|
2130
|
+
"""Return True if any input guardrail triggered during a streamed run."""
|
|
2131
|
+
|
|
2132
|
+
task = streamed_result._input_guardrails_task
|
|
2133
|
+
if task is None:
|
|
2134
|
+
return False
|
|
2135
|
+
|
|
2136
|
+
if not task.done():
|
|
2137
|
+
await task
|
|
2138
|
+
|
|
2139
|
+
return any(
|
|
2140
|
+
guardrail_result.output.tripwire_triggered
|
|
2141
|
+
for guardrail_result in streamed_result.input_guardrail_results
|
|
2142
|
+
)
|
|
2143
|
+
|
|
1376
2144
|
|
|
1377
2145
|
DEFAULT_AGENT_RUNNER = AgentRunner()
|
|
1378
2146
|
|
|
1379
2147
|
|
|
2148
|
+
def _get_tool_call_types() -> tuple[type, ...]:
|
|
2149
|
+
normalized_types: list[type] = []
|
|
2150
|
+
for type_hint in get_args(ToolCallItemTypes):
|
|
2151
|
+
origin = get_origin(type_hint)
|
|
2152
|
+
candidate = origin or type_hint
|
|
2153
|
+
if isinstance(candidate, type):
|
|
2154
|
+
normalized_types.append(candidate)
|
|
2155
|
+
return tuple(normalized_types)
|
|
2156
|
+
|
|
2157
|
+
|
|
2158
|
+
_TOOL_CALL_TYPES: tuple[type, ...] = _get_tool_call_types()
|
|
2159
|
+
|
|
2160
|
+
|
|
1380
2161
|
def _copy_str_or_list(input: str | list[TResponseInputItem]) -> str | list[TResponseInputItem]:
|
|
1381
2162
|
if isinstance(input, str):
|
|
1382
2163
|
return input
|