openai-agents 0.2.6__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 +294 -21
- 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 +238 -13
- 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 +18 -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 -48
- agents/models/openai_provider.py +10 -4
- agents/models/openai_responses.py +167 -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 +700 -151
- agents/realtime/session.py +309 -32
- agents/repl.py +7 -3
- agents/result.py +197 -38
- agents/run.py +1053 -178
- 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.6.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.6.dist-info → openai_agents-0.6.8.dist-info}/WHEEL +1 -1
- openai_agents-0.2.6.dist-info/RECORD +0 -103
- {openai_agents-0.2.6.dist-info → openai_agents-0.6.8.dist-info}/licenses/LICENSE +0 -0
agents/run.py
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
import
|
|
4
|
+
import contextlib
|
|
5
5
|
import inspect
|
|
6
|
+
import os
|
|
7
|
+
import warnings
|
|
6
8
|
from dataclasses import dataclass, field
|
|
7
|
-
from typing import Any, Generic, cast
|
|
9
|
+
from typing import Any, Callable, Generic, cast, get_args, get_origin
|
|
8
10
|
|
|
9
|
-
from openai.types.responses import
|
|
11
|
+
from openai.types.responses import (
|
|
12
|
+
ResponseCompletedEvent,
|
|
13
|
+
ResponseOutputItemDoneEvent,
|
|
14
|
+
)
|
|
10
15
|
from openai.types.responses.response_prompt_param import (
|
|
11
16
|
ResponsePromptParam,
|
|
12
17
|
)
|
|
18
|
+
from openai.types.responses.response_reasoning_item import ResponseReasoningItem
|
|
13
19
|
from typing_extensions import NotRequired, TypedDict, Unpack
|
|
14
20
|
|
|
15
21
|
from ._run_impl import (
|
|
@@ -40,22 +46,40 @@ from .guardrail import (
|
|
|
40
46
|
OutputGuardrail,
|
|
41
47
|
OutputGuardrailResult,
|
|
42
48
|
)
|
|
43
|
-
from .handoffs import Handoff, HandoffInputFilter, handoff
|
|
44
|
-
from .items import
|
|
45
|
-
|
|
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
|
|
46
63
|
from .logger import logger
|
|
47
|
-
from .memory import Session
|
|
64
|
+
from .memory import Session, SessionInputCallback, is_openai_responses_compaction_aware_session
|
|
48
65
|
from .model_settings import ModelSettings
|
|
49
66
|
from .models.interface import Model, ModelProvider
|
|
50
67
|
from .models.multi_provider import MultiProvider
|
|
51
68
|
from .result import RunResult, RunResultStreaming
|
|
52
|
-
from .run_context import RunContextWrapper, TContext
|
|
53
|
-
from .stream_events import
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
56
79
|
from .tracing.span_data import AgentSpanData
|
|
57
80
|
from .usage import Usage
|
|
58
81
|
from .util import _coro, _error_tracing
|
|
82
|
+
from .util._types import MaybeAwaitable
|
|
59
83
|
|
|
60
84
|
DEFAULT_MAX_TURNS = 10
|
|
61
85
|
|
|
@@ -81,6 +105,83 @@ 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
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class ModelInputData:
|
|
116
|
+
"""Container for the data that will be sent to the model."""
|
|
117
|
+
|
|
118
|
+
input: list[TResponseInputItem]
|
|
119
|
+
instructions: str | None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class CallModelData(Generic[TContext]):
|
|
124
|
+
"""Data passed to `RunConfig.call_model_input_filter` prior to model call."""
|
|
125
|
+
|
|
126
|
+
model_data: ModelInputData
|
|
127
|
+
agent: Agent[TContext]
|
|
128
|
+
context: TContext | None
|
|
129
|
+
|
|
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
|
+
|
|
181
|
+
# Type alias for the optional input filter callback
|
|
182
|
+
CallModelInputFilter = Callable[[CallModelData[Any]], MaybeAwaitable[ModelInputData]]
|
|
183
|
+
|
|
184
|
+
|
|
84
185
|
@dataclass
|
|
85
186
|
class RunConfig:
|
|
86
187
|
"""Configures settings for the entire agent run."""
|
|
@@ -104,6 +205,19 @@ class RunConfig:
|
|
|
104
205
|
agent. See the documentation in `Handoff.input_filter` for more details.
|
|
105
206
|
"""
|
|
106
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
|
+
|
|
107
221
|
input_guardrails: list[InputGuardrail[Any]] | None = None
|
|
108
222
|
"""A list of input guardrails to run on the initial run input."""
|
|
109
223
|
|
|
@@ -114,7 +228,12 @@ class RunConfig:
|
|
|
114
228
|
"""Whether tracing is disabled for the agent run. If disabled, we will not trace the agent run.
|
|
115
229
|
"""
|
|
116
230
|
|
|
117
|
-
|
|
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
|
+
)
|
|
118
237
|
"""Whether we include potentially sensitive data (for example: inputs/outputs of tool calls or
|
|
119
238
|
LLM generations) in traces. If False, we'll still create spans for these events, but the
|
|
120
239
|
sensitive data will not be included.
|
|
@@ -139,6 +258,23 @@ class RunConfig:
|
|
|
139
258
|
An optional dictionary of additional metadata to include with the trace.
|
|
140
259
|
"""
|
|
141
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
|
+
|
|
268
|
+
call_model_input_filter: CallModelInputFilter | None = None
|
|
269
|
+
"""
|
|
270
|
+
Optional callback that is invoked immediately before calling the model. It receives the current
|
|
271
|
+
agent, context and the model input (instructions and input items), and must return a possibly
|
|
272
|
+
modified `ModelInputData` to use for the model call.
|
|
273
|
+
|
|
274
|
+
This allows you to edit the input sent to the model e.g. to stay within a token limit.
|
|
275
|
+
For example, you can use this to add a system prompt to the input.
|
|
276
|
+
"""
|
|
277
|
+
|
|
142
278
|
|
|
143
279
|
class RunOptions(TypedDict, Generic[TContext]):
|
|
144
280
|
"""Arguments for ``AgentRunner`` methods."""
|
|
@@ -158,6 +294,12 @@ class RunOptions(TypedDict, Generic[TContext]):
|
|
|
158
294
|
previous_response_id: NotRequired[str | None]
|
|
159
295
|
"""The ID of the previous response, if any."""
|
|
160
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
|
+
|
|
161
303
|
session: NotRequired[Session | None]
|
|
162
304
|
"""The session for the run."""
|
|
163
305
|
|
|
@@ -174,34 +316,58 @@ class Runner:
|
|
|
174
316
|
hooks: RunHooks[TContext] | None = None,
|
|
175
317
|
run_config: RunConfig | None = None,
|
|
176
318
|
previous_response_id: str | None = None,
|
|
319
|
+
auto_previous_response_id: bool = False,
|
|
320
|
+
conversation_id: str | None = None,
|
|
177
321
|
session: Session | None = None,
|
|
178
322
|
) -> RunResult:
|
|
179
|
-
"""
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
+
|
|
186
334
|
In two cases, the agent may raise an exception:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
+
|
|
190
343
|
Args:
|
|
191
344
|
starting_agent: The starting agent to run.
|
|
192
|
-
input: The initial input to the agent. You can pass a single string for a
|
|
193
|
-
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.
|
|
194
347
|
context: The context to run the agent with.
|
|
195
|
-
max_turns: The maximum number of turns to run the agent for. A turn is
|
|
196
|
-
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).
|
|
197
350
|
hooks: An object that receives callbacks on various lifecycle events.
|
|
198
351
|
run_config: Global settings for the entire agent run.
|
|
199
|
-
previous_response_id: The ID of the previous response
|
|
200
|
-
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
|
+
|
|
201
365
|
Returns:
|
|
202
|
-
A run result containing all the inputs, guardrail results and the output of
|
|
203
|
-
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.
|
|
204
369
|
"""
|
|
370
|
+
|
|
205
371
|
runner = DEFAULT_AGENT_RUNNER
|
|
206
372
|
return await runner.run(
|
|
207
373
|
starting_agent,
|
|
@@ -211,6 +377,8 @@ class Runner:
|
|
|
211
377
|
hooks=hooks,
|
|
212
378
|
run_config=run_config,
|
|
213
379
|
previous_response_id=previous_response_id,
|
|
380
|
+
auto_previous_response_id=auto_previous_response_id,
|
|
381
|
+
conversation_id=conversation_id,
|
|
214
382
|
session=session,
|
|
215
383
|
)
|
|
216
384
|
|
|
@@ -225,37 +393,56 @@ class Runner:
|
|
|
225
393
|
hooks: RunHooks[TContext] | None = None,
|
|
226
394
|
run_config: RunConfig | None = None,
|
|
227
395
|
previous_response_id: str | None = None,
|
|
396
|
+
auto_previous_response_id: bool = False,
|
|
397
|
+
conversation_id: str | None = None,
|
|
228
398
|
session: Session | None = None,
|
|
229
399
|
) -> RunResult:
|
|
230
|
-
"""
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
+
|
|
240
416
|
In two cases, the agent may raise an exception:
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
+
|
|
244
425
|
Args:
|
|
245
426
|
starting_agent: The starting agent to run.
|
|
246
|
-
input: The initial input to the agent. You can pass a single string for a
|
|
247
|
-
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.
|
|
248
429
|
context: The context to run the agent with.
|
|
249
|
-
max_turns: The maximum number of turns to run the agent for. A turn is
|
|
250
|
-
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).
|
|
251
432
|
hooks: An object that receives callbacks on various lifecycle events.
|
|
252
433
|
run_config: Global settings for the entire agent run.
|
|
253
|
-
previous_response_id: The ID of the previous response, if using OpenAI
|
|
254
|
-
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
|
+
|
|
255
440
|
Returns:
|
|
256
|
-
A run result containing all the inputs, guardrail results and the output of
|
|
257
|
-
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.
|
|
258
444
|
"""
|
|
445
|
+
|
|
259
446
|
runner = DEFAULT_AGENT_RUNNER
|
|
260
447
|
return runner.run_sync(
|
|
261
448
|
starting_agent,
|
|
@@ -265,7 +452,9 @@ class Runner:
|
|
|
265
452
|
hooks=hooks,
|
|
266
453
|
run_config=run_config,
|
|
267
454
|
previous_response_id=previous_response_id,
|
|
455
|
+
conversation_id=conversation_id,
|
|
268
456
|
session=session,
|
|
457
|
+
auto_previous_response_id=auto_previous_response_id,
|
|
269
458
|
)
|
|
270
459
|
|
|
271
460
|
@classmethod
|
|
@@ -278,34 +467,53 @@ class Runner:
|
|
|
278
467
|
hooks: RunHooks[TContext] | None = None,
|
|
279
468
|
run_config: RunConfig | None = None,
|
|
280
469
|
previous_response_id: str | None = None,
|
|
470
|
+
auto_previous_response_id: bool = False,
|
|
471
|
+
conversation_id: str | None = None,
|
|
281
472
|
session: Session | None = None,
|
|
282
473
|
) -> RunResultStreaming:
|
|
283
|
-
"""
|
|
284
|
-
|
|
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
|
+
|
|
285
480
|
The agent will run in a loop until a final output is generated. The loop runs like so:
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
+
|
|
291
488
|
In two cases, the agent may raise an exception:
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
+
|
|
295
497
|
Args:
|
|
296
498
|
starting_agent: The starting agent to run.
|
|
297
|
-
input: The initial input to the agent. You can pass a single string for a
|
|
298
|
-
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.
|
|
299
501
|
context: The context to run the agent with.
|
|
300
|
-
max_turns: The maximum number of turns to run the agent for. A turn is
|
|
301
|
-
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).
|
|
302
504
|
hooks: An object that receives callbacks on various lifecycle events.
|
|
303
505
|
run_config: Global settings for the entire agent run.
|
|
304
|
-
previous_response_id: The ID of the previous response, if using OpenAI
|
|
305
|
-
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
|
+
|
|
306
512
|
Returns:
|
|
307
|
-
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.
|
|
308
515
|
"""
|
|
516
|
+
|
|
309
517
|
runner = DEFAULT_AGENT_RUNNER
|
|
310
518
|
return runner.run_streamed(
|
|
311
519
|
starting_agent,
|
|
@@ -315,6 +523,8 @@ class Runner:
|
|
|
315
523
|
hooks=hooks,
|
|
316
524
|
run_config=run_config,
|
|
317
525
|
previous_response_id=previous_response_id,
|
|
526
|
+
auto_previous_response_id=auto_previous_response_id,
|
|
527
|
+
conversation_id=conversation_id,
|
|
318
528
|
session=session,
|
|
319
529
|
)
|
|
320
530
|
|
|
@@ -333,17 +543,35 @@ class AgentRunner:
|
|
|
333
543
|
) -> RunResult:
|
|
334
544
|
context = kwargs.get("context")
|
|
335
545
|
max_turns = kwargs.get("max_turns", DEFAULT_MAX_TURNS)
|
|
336
|
-
hooks = kwargs.get("hooks")
|
|
546
|
+
hooks = cast(RunHooks[TContext], self._validate_run_hooks(kwargs.get("hooks")))
|
|
337
547
|
run_config = kwargs.get("run_config")
|
|
338
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")
|
|
339
551
|
session = kwargs.get("session")
|
|
340
|
-
|
|
341
|
-
hooks = RunHooks[Any]()
|
|
552
|
+
|
|
342
553
|
if run_config is None:
|
|
343
554
|
run_config = RunConfig()
|
|
344
555
|
|
|
345
|
-
#
|
|
346
|
-
|
|
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
|
+
)
|
|
347
575
|
|
|
348
576
|
tool_use_tracker = AgentToolUseTracker()
|
|
349
577
|
|
|
@@ -352,11 +580,13 @@ class AgentRunner:
|
|
|
352
580
|
trace_id=run_config.trace_id,
|
|
353
581
|
group_id=run_config.group_id,
|
|
354
582
|
metadata=run_config.trace_metadata,
|
|
583
|
+
tracing=run_config.tracing,
|
|
355
584
|
disabled=run_config.tracing_disabled,
|
|
356
585
|
):
|
|
357
586
|
current_turn = 0
|
|
358
|
-
original_input: str | list[TResponseInputItem] =
|
|
359
|
-
generated_items: list[RunItem] = []
|
|
587
|
+
original_input: str | list[TResponseInputItem] = _copy_str_or_list(prepared_input)
|
|
588
|
+
generated_items: list[RunItem] = [] # For model input (may be filtered on handoffs)
|
|
589
|
+
session_items: list[RunItem] = [] # For observability (always unfiltered)
|
|
360
590
|
model_responses: list[ModelResponse] = []
|
|
361
591
|
|
|
362
592
|
context_wrapper: RunContextWrapper[TContext] = RunContextWrapper(
|
|
@@ -364,14 +594,22 @@ class AgentRunner:
|
|
|
364
594
|
)
|
|
365
595
|
|
|
366
596
|
input_guardrail_results: list[InputGuardrailResult] = []
|
|
597
|
+
tool_input_guardrail_results: list[ToolInputGuardrailResult] = []
|
|
598
|
+
tool_output_guardrail_results: list[ToolOutputGuardrailResult] = []
|
|
367
599
|
|
|
368
600
|
current_span: Span[AgentSpanData] | None = None
|
|
369
601
|
current_agent = starting_agent
|
|
370
602
|
should_run_agent_start_hooks = True
|
|
371
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
|
+
|
|
372
607
|
try:
|
|
373
608
|
while True:
|
|
374
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
|
+
)
|
|
375
613
|
|
|
376
614
|
# Start an agent span if we don't have one. This span is ended if the current
|
|
377
615
|
# agent changes, or if the agent loop ends.
|
|
@@ -409,12 +647,32 @@ class AgentRunner:
|
|
|
409
647
|
)
|
|
410
648
|
|
|
411
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.
|
|
412
671
|
input_guardrail_results, turn_result = await asyncio.gather(
|
|
413
672
|
self._run_input_guardrails(
|
|
414
673
|
starting_agent,
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
copy.deepcopy(prepared_input),
|
|
674
|
+
parallel_guardrails,
|
|
675
|
+
_copy_str_or_list(prepared_input),
|
|
418
676
|
context_wrapper,
|
|
419
677
|
),
|
|
420
678
|
self._run_single_turn(
|
|
@@ -427,9 +685,12 @@ class AgentRunner:
|
|
|
427
685
|
run_config=run_config,
|
|
428
686
|
should_run_agent_start_hooks=should_run_agent_start_hooks,
|
|
429
687
|
tool_use_tracker=tool_use_tracker,
|
|
430
|
-
|
|
688
|
+
server_conversation_tracker=server_conversation_tracker,
|
|
431
689
|
),
|
|
432
690
|
)
|
|
691
|
+
|
|
692
|
+
# Combine sequential and parallel results.
|
|
693
|
+
input_guardrail_results = sequential_results + input_guardrail_results
|
|
433
694
|
else:
|
|
434
695
|
turn_result = await self._run_single_turn(
|
|
435
696
|
agent=current_agent,
|
|
@@ -441,51 +702,111 @@ class AgentRunner:
|
|
|
441
702
|
run_config=run_config,
|
|
442
703
|
should_run_agent_start_hooks=should_run_agent_start_hooks,
|
|
443
704
|
tool_use_tracker=tool_use_tracker,
|
|
444
|
-
|
|
705
|
+
server_conversation_tracker=server_conversation_tracker,
|
|
445
706
|
)
|
|
446
707
|
should_run_agent_start_hooks = False
|
|
447
708
|
|
|
448
709
|
model_responses.append(turn_result.model_response)
|
|
449
710
|
original_input = turn_result.original_input
|
|
450
|
-
|
|
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)
|
|
451
720
|
|
|
452
|
-
if
|
|
453
|
-
|
|
454
|
-
current_agent.output_guardrails + (run_config.output_guardrails or []),
|
|
455
|
-
current_agent,
|
|
456
|
-
turn_result.next_step.output,
|
|
457
|
-
context_wrapper,
|
|
458
|
-
)
|
|
459
|
-
result = RunResult(
|
|
460
|
-
input=original_input,
|
|
461
|
-
new_items=generated_items,
|
|
462
|
-
raw_responses=model_responses,
|
|
463
|
-
final_output=turn_result.next_step.output,
|
|
464
|
-
_last_agent=current_agent,
|
|
465
|
-
input_guardrail_results=input_guardrail_results,
|
|
466
|
-
output_guardrail_results=output_guardrail_results,
|
|
467
|
-
context_wrapper=context_wrapper,
|
|
468
|
-
)
|
|
721
|
+
if server_conversation_tracker is not None:
|
|
722
|
+
server_conversation_tracker.track_server_items(turn_result.model_response)
|
|
469
723
|
|
|
470
|
-
|
|
471
|
-
|
|
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)
|
|
472
727
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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()
|
|
485
806
|
except AgentsException as exc:
|
|
486
807
|
exc.run_data = RunErrorDetails(
|
|
487
808
|
input=original_input,
|
|
488
|
-
new_items=
|
|
809
|
+
new_items=session_items, # Use unfiltered items for observability
|
|
489
810
|
raw_responses=model_responses,
|
|
490
811
|
last_agent=current_agent,
|
|
491
812
|
context_wrapper=context_wrapper,
|
|
@@ -494,6 +815,10 @@ class AgentRunner:
|
|
|
494
815
|
)
|
|
495
816
|
raise
|
|
496
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)
|
|
497
822
|
if current_span:
|
|
498
823
|
current_span.finish(reset_current=True)
|
|
499
824
|
|
|
@@ -508,9 +833,44 @@ class AgentRunner:
|
|
|
508
833
|
hooks = kwargs.get("hooks")
|
|
509
834
|
run_config = kwargs.get("run_config")
|
|
510
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")
|
|
511
838
|
session = kwargs.get("session")
|
|
512
839
|
|
|
513
|
-
|
|
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(
|
|
514
874
|
self.run(
|
|
515
875
|
starting_agent,
|
|
516
876
|
input,
|
|
@@ -520,9 +880,29 @@ class AgentRunner:
|
|
|
520
880
|
hooks=hooks,
|
|
521
881
|
run_config=run_config,
|
|
522
882
|
previous_response_id=previous_response_id,
|
|
883
|
+
auto_previous_response_id=auto_previous_response_id,
|
|
884
|
+
conversation_id=conversation_id,
|
|
523
885
|
)
|
|
524
886
|
)
|
|
525
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
|
+
|
|
526
906
|
def run_streamed(
|
|
527
907
|
self,
|
|
528
908
|
starting_agent: Agent[TContext],
|
|
@@ -531,13 +911,13 @@ class AgentRunner:
|
|
|
531
911
|
) -> RunResultStreaming:
|
|
532
912
|
context = kwargs.get("context")
|
|
533
913
|
max_turns = kwargs.get("max_turns", DEFAULT_MAX_TURNS)
|
|
534
|
-
hooks = kwargs.get("hooks")
|
|
914
|
+
hooks = cast(RunHooks[TContext], self._validate_run_hooks(kwargs.get("hooks")))
|
|
535
915
|
run_config = kwargs.get("run_config")
|
|
536
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")
|
|
537
919
|
session = kwargs.get("session")
|
|
538
920
|
|
|
539
|
-
if hooks is None:
|
|
540
|
-
hooks = RunHooks[Any]()
|
|
541
921
|
if run_config is None:
|
|
542
922
|
run_config = RunConfig()
|
|
543
923
|
|
|
@@ -552,6 +932,7 @@ class AgentRunner:
|
|
|
552
932
|
trace_id=run_config.trace_id,
|
|
553
933
|
group_id=run_config.group_id,
|
|
554
934
|
metadata=run_config.trace_metadata,
|
|
935
|
+
tracing=run_config.tracing,
|
|
555
936
|
disabled=run_config.tracing_disabled,
|
|
556
937
|
)
|
|
557
938
|
)
|
|
@@ -562,7 +943,7 @@ class AgentRunner:
|
|
|
562
943
|
)
|
|
563
944
|
|
|
564
945
|
streamed_result = RunResultStreaming(
|
|
565
|
-
input=
|
|
946
|
+
input=_copy_str_or_list(input),
|
|
566
947
|
new_items=[],
|
|
567
948
|
current_agent=starting_agent,
|
|
568
949
|
raw_responses=[],
|
|
@@ -572,6 +953,8 @@ class AgentRunner:
|
|
|
572
953
|
max_turns=max_turns,
|
|
573
954
|
input_guardrail_results=[],
|
|
574
955
|
output_guardrail_results=[],
|
|
956
|
+
tool_input_guardrail_results=[],
|
|
957
|
+
tool_output_guardrail_results=[],
|
|
575
958
|
_current_agent_output_schema=output_schema,
|
|
576
959
|
trace=new_trace,
|
|
577
960
|
context_wrapper=context_wrapper,
|
|
@@ -588,11 +971,71 @@ class AgentRunner:
|
|
|
588
971
|
context_wrapper=context_wrapper,
|
|
589
972
|
run_config=run_config,
|
|
590
973
|
previous_response_id=previous_response_id,
|
|
974
|
+
auto_previous_response_id=auto_previous_response_id,
|
|
975
|
+
conversation_id=conversation_id,
|
|
591
976
|
session=session,
|
|
592
977
|
)
|
|
593
978
|
)
|
|
594
979
|
return streamed_result
|
|
595
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
|
+
|
|
998
|
+
@classmethod
|
|
999
|
+
async def _maybe_filter_model_input(
|
|
1000
|
+
cls,
|
|
1001
|
+
*,
|
|
1002
|
+
agent: Agent[TContext],
|
|
1003
|
+
run_config: RunConfig,
|
|
1004
|
+
context_wrapper: RunContextWrapper[TContext],
|
|
1005
|
+
input_items: list[TResponseInputItem],
|
|
1006
|
+
system_instructions: str | None,
|
|
1007
|
+
) -> ModelInputData:
|
|
1008
|
+
"""Apply optional call_model_input_filter to modify model input.
|
|
1009
|
+
|
|
1010
|
+
Returns a `ModelInputData` that will be sent to the model.
|
|
1011
|
+
"""
|
|
1012
|
+
effective_instructions = system_instructions
|
|
1013
|
+
effective_input: list[TResponseInputItem] = input_items
|
|
1014
|
+
|
|
1015
|
+
if run_config.call_model_input_filter is None:
|
|
1016
|
+
return ModelInputData(input=effective_input, instructions=effective_instructions)
|
|
1017
|
+
|
|
1018
|
+
try:
|
|
1019
|
+
model_input = ModelInputData(
|
|
1020
|
+
input=effective_input.copy(),
|
|
1021
|
+
instructions=effective_instructions,
|
|
1022
|
+
)
|
|
1023
|
+
filter_payload: CallModelData[TContext] = CallModelData(
|
|
1024
|
+
model_data=model_input,
|
|
1025
|
+
agent=agent,
|
|
1026
|
+
context=context_wrapper.context,
|
|
1027
|
+
)
|
|
1028
|
+
maybe_updated = run_config.call_model_input_filter(filter_payload)
|
|
1029
|
+
updated = await maybe_updated if inspect.isawaitable(maybe_updated) else maybe_updated
|
|
1030
|
+
if not isinstance(updated, ModelInputData):
|
|
1031
|
+
raise UserError("call_model_input_filter must return a ModelInputData instance")
|
|
1032
|
+
return updated
|
|
1033
|
+
except Exception as e:
|
|
1034
|
+
_error_tracing.attach_error_to_current_span(
|
|
1035
|
+
SpanError(message="Error in call_model_input_filter", data={"error": str(e)})
|
|
1036
|
+
)
|
|
1037
|
+
raise
|
|
1038
|
+
|
|
596
1039
|
@classmethod
|
|
597
1040
|
async def _run_input_guardrails_with_queue(
|
|
598
1041
|
cls,
|
|
@@ -617,6 +1060,11 @@ class AgentRunner:
|
|
|
617
1060
|
for done in asyncio.as_completed(guardrail_tasks):
|
|
618
1061
|
result = await done
|
|
619
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)
|
|
620
1068
|
_error_tracing.attach_error_to_span(
|
|
621
1069
|
parent_span,
|
|
622
1070
|
SpanError(
|
|
@@ -627,6 +1075,9 @@ class AgentRunner:
|
|
|
627
1075
|
},
|
|
628
1076
|
),
|
|
629
1077
|
)
|
|
1078
|
+
queue.put_nowait(result)
|
|
1079
|
+
guardrail_results.append(result)
|
|
1080
|
+
break
|
|
630
1081
|
queue.put_nowait(result)
|
|
631
1082
|
guardrail_results.append(result)
|
|
632
1083
|
except Exception:
|
|
@@ -634,7 +1085,9 @@ class AgentRunner:
|
|
|
634
1085
|
t.cancel()
|
|
635
1086
|
raise
|
|
636
1087
|
|
|
637
|
-
streamed_result.input_guardrail_results =
|
|
1088
|
+
streamed_result.input_guardrail_results = (
|
|
1089
|
+
streamed_result.input_guardrail_results + guardrail_results
|
|
1090
|
+
)
|
|
638
1091
|
|
|
639
1092
|
@classmethod
|
|
640
1093
|
async def _start_streaming(
|
|
@@ -647,6 +1100,8 @@ class AgentRunner:
|
|
|
647
1100
|
context_wrapper: RunContextWrapper[TContext],
|
|
648
1101
|
run_config: RunConfig,
|
|
649
1102
|
previous_response_id: str | None,
|
|
1103
|
+
auto_previous_response_id: bool,
|
|
1104
|
+
conversation_id: str | None,
|
|
650
1105
|
session: Session | None,
|
|
651
1106
|
):
|
|
652
1107
|
if streamed_result.trace:
|
|
@@ -658,20 +1113,47 @@ class AgentRunner:
|
|
|
658
1113
|
should_run_agent_start_hooks = True
|
|
659
1114
|
tool_use_tracker = AgentToolUseTracker()
|
|
660
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
|
+
|
|
661
1130
|
streamed_result._event_queue.put_nowait(AgentUpdatedStreamEvent(new_agent=current_agent))
|
|
662
1131
|
|
|
663
1132
|
try:
|
|
664
1133
|
# Prepare input with session if enabled
|
|
665
|
-
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
|
+
)
|
|
666
1137
|
|
|
667
1138
|
# Update the streamed result with the prepared input
|
|
668
1139
|
streamed_result.input = prepared_input
|
|
669
1140
|
|
|
1141
|
+
await AgentRunner._save_result_to_session(session, starting_input, [])
|
|
1142
|
+
|
|
670
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
|
+
|
|
671
1150
|
if streamed_result.is_complete:
|
|
672
1151
|
break
|
|
673
1152
|
|
|
674
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
|
+
)
|
|
675
1157
|
|
|
676
1158
|
# Start an agent span if we don't have one. This span is ended if the current
|
|
677
1159
|
# agent changes, or if the agent loop ends.
|
|
@@ -708,12 +1190,37 @@ class AgentRunner:
|
|
|
708
1190
|
break
|
|
709
1191
|
|
|
710
1192
|
if current_turn == 1:
|
|
711
|
-
#
|
|
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.
|
|
712
1219
|
streamed_result._input_guardrails_task = asyncio.create_task(
|
|
713
1220
|
cls._run_input_guardrails_with_queue(
|
|
714
1221
|
starting_agent,
|
|
715
|
-
|
|
716
|
-
|
|
1222
|
+
parallel_guardrails,
|
|
1223
|
+
ItemHelpers.input_to_new_input_list(prepared_input),
|
|
717
1224
|
context_wrapper,
|
|
718
1225
|
streamed_result,
|
|
719
1226
|
current_span,
|
|
@@ -729,7 +1236,7 @@ class AgentRunner:
|
|
|
729
1236
|
should_run_agent_start_hooks,
|
|
730
1237
|
tool_use_tracker,
|
|
731
1238
|
all_tools,
|
|
732
|
-
|
|
1239
|
+
server_conversation_tracker,
|
|
733
1240
|
)
|
|
734
1241
|
should_run_agent_start_hooks = False
|
|
735
1242
|
|
|
@@ -737,9 +1244,40 @@ class AgentRunner:
|
|
|
737
1244
|
turn_result.model_response
|
|
738
1245
|
]
|
|
739
1246
|
streamed_result.input = turn_result.original_input
|
|
740
|
-
|
|
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)
|
|
741
1261
|
|
|
742
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
|
+
|
|
743
1281
|
current_agent = turn_result.next_step.new_agent
|
|
744
1282
|
current_span.finish(reset_current=True)
|
|
745
1283
|
current_span = None
|
|
@@ -747,6 +1285,12 @@ class AgentRunner:
|
|
|
747
1285
|
streamed_result._event_queue.put_nowait(
|
|
748
1286
|
AgentUpdatedStreamEvent(new_agent=current_agent)
|
|
749
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
|
|
750
1294
|
elif isinstance(turn_result.next_step, NextStepFinalOutput):
|
|
751
1295
|
streamed_result._output_guardrails_task = asyncio.create_task(
|
|
752
1296
|
cls._run_output_guardrails(
|
|
@@ -769,24 +1313,45 @@ class AgentRunner:
|
|
|
769
1313
|
streamed_result.is_complete = True
|
|
770
1314
|
|
|
771
1315
|
# Save the conversation to session if enabled
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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
|
+
)
|
|
786
1331
|
|
|
787
1332
|
streamed_result._event_queue.put_nowait(QueueCompleteSentinel())
|
|
788
1333
|
elif isinstance(turn_result.next_step, NextStepRunAgain):
|
|
789
|
-
|
|
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
|
|
790
1355
|
except AgentsException as exc:
|
|
791
1356
|
streamed_result.is_complete = True
|
|
792
1357
|
streamed_result._event_queue.put_nowait(QueueCompleteSentinel())
|
|
@@ -815,11 +1380,32 @@ class AgentRunner:
|
|
|
815
1380
|
|
|
816
1381
|
streamed_result.is_complete = True
|
|
817
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)
|
|
818
1396
|
if current_span:
|
|
819
1397
|
current_span.finish(reset_current=True)
|
|
820
1398
|
if streamed_result.trace:
|
|
821
1399
|
streamed_result.trace.finish(reset_current=True)
|
|
822
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
|
+
|
|
823
1409
|
@classmethod
|
|
824
1410
|
async def _run_single_turn_streamed(
|
|
825
1411
|
cls,
|
|
@@ -831,13 +1417,21 @@ class AgentRunner:
|
|
|
831
1417
|
should_run_agent_start_hooks: bool,
|
|
832
1418
|
tool_use_tracker: AgentToolUseTracker,
|
|
833
1419
|
all_tools: list[Tool],
|
|
834
|
-
|
|
1420
|
+
server_conversation_tracker: _ServerConversationTracker | None = None,
|
|
835
1421
|
) -> SingleStepResult:
|
|
1422
|
+
emitted_tool_call_ids: set[str] = set()
|
|
1423
|
+
emitted_reasoning_item_ids: set[str] = set()
|
|
1424
|
+
|
|
836
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
|
+
)
|
|
837
1431
|
await asyncio.gather(
|
|
838
|
-
hooks.on_agent_start(
|
|
1432
|
+
hooks.on_agent_start(agent_hook_context, agent),
|
|
839
1433
|
(
|
|
840
|
-
agent.hooks.on_start(
|
|
1434
|
+
agent.hooks.on_start(agent_hook_context, agent)
|
|
841
1435
|
if agent.hooks
|
|
842
1436
|
else _coro.noop_coroutine()
|
|
843
1437
|
),
|
|
@@ -860,13 +1454,49 @@ class AgentRunner:
|
|
|
860
1454
|
|
|
861
1455
|
final_response: ModelResponse | None = None
|
|
862
1456
|
|
|
863
|
-
|
|
864
|
-
|
|
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])
|
|
1464
|
+
|
|
1465
|
+
# THIS IS THE RESOLVED CONFLICT BLOCK
|
|
1466
|
+
filtered = await cls._maybe_filter_model_input(
|
|
1467
|
+
agent=agent,
|
|
1468
|
+
run_config=run_config,
|
|
1469
|
+
context_wrapper=context_wrapper,
|
|
1470
|
+
input_items=input,
|
|
1471
|
+
system_instructions=system_prompt,
|
|
1472
|
+
)
|
|
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
|
+
)
|
|
865
1495
|
|
|
866
1496
|
# 1. Stream the output events
|
|
867
1497
|
async for event in model.stream_response(
|
|
868
|
-
|
|
869
|
-
input,
|
|
1498
|
+
filtered.instructions,
|
|
1499
|
+
filtered.input,
|
|
870
1500
|
model_settings,
|
|
871
1501
|
all_tools,
|
|
872
1502
|
output_schema,
|
|
@@ -875,8 +1505,12 @@ class AgentRunner:
|
|
|
875
1505
|
run_config.tracing_disabled, run_config.trace_include_sensitive_data
|
|
876
1506
|
),
|
|
877
1507
|
previous_response_id=previous_response_id,
|
|
1508
|
+
conversation_id=conversation_id,
|
|
878
1509
|
prompt=prompt_config,
|
|
879
1510
|
):
|
|
1511
|
+
# Emit the raw event ASAP
|
|
1512
|
+
streamed_result._event_queue.put_nowait(RawResponsesStreamEvent(data=event))
|
|
1513
|
+
|
|
880
1514
|
if isinstance(event, ResponseCompletedEvent):
|
|
881
1515
|
usage = (
|
|
882
1516
|
Usage(
|
|
@@ -897,16 +1531,56 @@ class AgentRunner:
|
|
|
897
1531
|
)
|
|
898
1532
|
context_wrapper.usage.add(usage)
|
|
899
1533
|
|
|
900
|
-
|
|
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
|
+
)
|
|
901
1574
|
|
|
902
1575
|
# 2. At this point, the streaming is complete for this turn of the agent loop.
|
|
903
1576
|
if not final_response:
|
|
904
1577
|
raise ModelBehaviorError("Model did not produce a final response!")
|
|
905
1578
|
|
|
906
1579
|
# 3. Now, we can process the turn as we do in the non-streaming case
|
|
907
|
-
|
|
1580
|
+
single_step_result = await cls._get_single_step_result_from_response(
|
|
908
1581
|
agent=agent,
|
|
909
|
-
|
|
1582
|
+
original_input=streamed_result.input,
|
|
1583
|
+
pre_step_items=streamed_result._model_input_items,
|
|
910
1584
|
new_response=final_response,
|
|
911
1585
|
output_schema=output_schema,
|
|
912
1586
|
all_tools=all_tools,
|
|
@@ -915,8 +1589,59 @@ class AgentRunner:
|
|
|
915
1589
|
context_wrapper=context_wrapper,
|
|
916
1590
|
run_config=run_config,
|
|
917
1591
|
tool_use_tracker=tool_use_tracker,
|
|
1592
|
+
event_queue=streamed_result._event_queue,
|
|
918
1593
|
)
|
|
919
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
|
+
|
|
920
1645
|
@classmethod
|
|
921
1646
|
async def _run_single_turn(
|
|
922
1647
|
cls,
|
|
@@ -930,14 +1655,19 @@ class AgentRunner:
|
|
|
930
1655
|
run_config: RunConfig,
|
|
931
1656
|
should_run_agent_start_hooks: bool,
|
|
932
1657
|
tool_use_tracker: AgentToolUseTracker,
|
|
933
|
-
|
|
1658
|
+
server_conversation_tracker: _ServerConversationTracker | None = None,
|
|
934
1659
|
) -> SingleStepResult:
|
|
935
1660
|
# Ensure we run the hooks before anything else
|
|
936
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
|
+
)
|
|
937
1667
|
await asyncio.gather(
|
|
938
|
-
hooks.on_agent_start(
|
|
1668
|
+
hooks.on_agent_start(agent_hook_context, agent),
|
|
939
1669
|
(
|
|
940
|
-
agent.hooks.on_start(
|
|
1670
|
+
agent.hooks.on_start(agent_hook_context, agent)
|
|
941
1671
|
if agent.hooks
|
|
942
1672
|
else _coro.noop_coroutine()
|
|
943
1673
|
),
|
|
@@ -950,8 +1680,11 @@ class AgentRunner:
|
|
|
950
1680
|
|
|
951
1681
|
output_schema = cls._get_output_schema(agent)
|
|
952
1682
|
handoffs = await cls._get_handoffs(agent, context_wrapper)
|
|
953
|
-
|
|
954
|
-
|
|
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])
|
|
955
1688
|
|
|
956
1689
|
new_response = await cls._get_new_response(
|
|
957
1690
|
agent,
|
|
@@ -960,10 +1693,11 @@ class AgentRunner:
|
|
|
960
1693
|
output_schema,
|
|
961
1694
|
all_tools,
|
|
962
1695
|
handoffs,
|
|
1696
|
+
hooks,
|
|
963
1697
|
context_wrapper,
|
|
964
1698
|
run_config,
|
|
965
1699
|
tool_use_tracker,
|
|
966
|
-
|
|
1700
|
+
server_conversation_tracker,
|
|
967
1701
|
prompt_config,
|
|
968
1702
|
)
|
|
969
1703
|
|
|
@@ -996,6 +1730,7 @@ class AgentRunner:
|
|
|
996
1730
|
context_wrapper: RunContextWrapper[TContext],
|
|
997
1731
|
run_config: RunConfig,
|
|
998
1732
|
tool_use_tracker: AgentToolUseTracker,
|
|
1733
|
+
event_queue: asyncio.Queue[StreamEvent | QueueCompleteSentinel] | None = None,
|
|
999
1734
|
) -> SingleStepResult:
|
|
1000
1735
|
processed_response = RunImpl.process_model_response(
|
|
1001
1736
|
agent=agent,
|
|
@@ -1007,6 +1742,14 @@ class AgentRunner:
|
|
|
1007
1742
|
|
|
1008
1743
|
tool_use_tracker.add_tool_use(agent, processed_response.tools_used)
|
|
1009
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
|
+
|
|
1010
1753
|
return await RunImpl.execute_tools_and_side_effects(
|
|
1011
1754
|
agent=agent,
|
|
1012
1755
|
original_input=original_input,
|
|
@@ -1034,9 +1777,8 @@ class AgentRunner:
|
|
|
1034
1777
|
run_config: RunConfig,
|
|
1035
1778
|
tool_use_tracker: AgentToolUseTracker,
|
|
1036
1779
|
) -> SingleStepResult:
|
|
1037
|
-
|
|
1038
1780
|
original_input = streamed_result.input
|
|
1039
|
-
pre_step_items = streamed_result.
|
|
1781
|
+
pre_step_items = streamed_result._model_input_items
|
|
1040
1782
|
event_queue = streamed_result._event_queue
|
|
1041
1783
|
|
|
1042
1784
|
processed_response = RunImpl.process_model_response(
|
|
@@ -1061,10 +1803,15 @@ class AgentRunner:
|
|
|
1061
1803
|
context_wrapper=context_wrapper,
|
|
1062
1804
|
run_config=run_config,
|
|
1063
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
|
+
)
|
|
1064
1813
|
new_step_items = [
|
|
1065
|
-
item
|
|
1066
|
-
for item in single_step_result.new_step_items
|
|
1067
|
-
if item not in new_items_processed_response
|
|
1814
|
+
item for item in streaming_items if item not in new_items_processed_response
|
|
1068
1815
|
]
|
|
1069
1816
|
RunImpl.stream_step_items_to_queue(new_step_items, event_queue)
|
|
1070
1817
|
|
|
@@ -1096,6 +1843,8 @@ class AgentRunner:
|
|
|
1096
1843
|
# Cancel all guardrail tasks if a tripwire is triggered.
|
|
1097
1844
|
for t in guardrail_tasks:
|
|
1098
1845
|
t.cancel()
|
|
1846
|
+
# Wait for cancellations to propagate by awaiting the cancelled tasks.
|
|
1847
|
+
await asyncio.gather(*guardrail_tasks, return_exceptions=True)
|
|
1099
1848
|
_error_tracing.attach_error_to_current_span(
|
|
1100
1849
|
SpanError(
|
|
1101
1850
|
message="Guardrail tripwire triggered",
|
|
@@ -1155,19 +1904,54 @@ class AgentRunner:
|
|
|
1155
1904
|
output_schema: AgentOutputSchemaBase | None,
|
|
1156
1905
|
all_tools: list[Tool],
|
|
1157
1906
|
handoffs: list[Handoff],
|
|
1907
|
+
hooks: RunHooks[TContext],
|
|
1158
1908
|
context_wrapper: RunContextWrapper[TContext],
|
|
1159
1909
|
run_config: RunConfig,
|
|
1160
1910
|
tool_use_tracker: AgentToolUseTracker,
|
|
1161
|
-
|
|
1911
|
+
server_conversation_tracker: _ServerConversationTracker | None,
|
|
1162
1912
|
prompt_config: ResponsePromptParam | None,
|
|
1163
1913
|
) -> ModelResponse:
|
|
1914
|
+
# Allow user to modify model input right before the call, if configured
|
|
1915
|
+
filtered = await cls._maybe_filter_model_input(
|
|
1916
|
+
agent=agent,
|
|
1917
|
+
run_config=run_config,
|
|
1918
|
+
context_wrapper=context_wrapper,
|
|
1919
|
+
input_items=input,
|
|
1920
|
+
system_instructions=system_prompt,
|
|
1921
|
+
)
|
|
1922
|
+
|
|
1164
1923
|
model = cls._get_model(agent, run_config)
|
|
1165
1924
|
model_settings = agent.model_settings.resolve(run_config.model_settings)
|
|
1166
1925
|
model_settings = RunImpl.maybe_reset_tool_choice(agent, tool_use_tracker, model_settings)
|
|
1167
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
|
+
|
|
1168
1952
|
new_response = await model.get_response(
|
|
1169
|
-
system_instructions=
|
|
1170
|
-
input=input,
|
|
1953
|
+
system_instructions=filtered.instructions,
|
|
1954
|
+
input=filtered.input,
|
|
1171
1955
|
model_settings=model_settings,
|
|
1172
1956
|
tools=all_tools,
|
|
1173
1957
|
output_schema=output_schema,
|
|
@@ -1176,11 +1960,22 @@ class AgentRunner:
|
|
|
1176
1960
|
run_config.tracing_disabled, run_config.trace_include_sensitive_data
|
|
1177
1961
|
),
|
|
1178
1962
|
previous_response_id=previous_response_id,
|
|
1963
|
+
conversation_id=conversation_id,
|
|
1179
1964
|
prompt=prompt_config,
|
|
1180
1965
|
)
|
|
1181
1966
|
|
|
1182
1967
|
context_wrapper.usage.add(new_response.usage)
|
|
1183
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
|
+
|
|
1184
1979
|
return new_response
|
|
1185
1980
|
|
|
1186
1981
|
@classmethod
|
|
@@ -1238,19 +2033,20 @@ class AgentRunner:
|
|
|
1238
2033
|
cls,
|
|
1239
2034
|
input: str | list[TResponseInputItem],
|
|
1240
2035
|
session: Session | None,
|
|
2036
|
+
session_input_callback: SessionInputCallback | None,
|
|
1241
2037
|
) -> str | list[TResponseInputItem]:
|
|
1242
2038
|
"""Prepare input by combining it with session history if enabled."""
|
|
1243
2039
|
if session is None:
|
|
1244
2040
|
return input
|
|
1245
2041
|
|
|
1246
|
-
#
|
|
1247
|
-
|
|
1248
|
-
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:
|
|
1249
2044
|
raise UserError(
|
|
1250
|
-
"
|
|
1251
|
-
"
|
|
1252
|
-
"
|
|
1253
|
-
"
|
|
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."
|
|
1254
2050
|
)
|
|
1255
2051
|
|
|
1256
2052
|
# Get previous conversation history
|
|
@@ -1259,19 +2055,32 @@ class AgentRunner:
|
|
|
1259
2055
|
# Convert input to list format
|
|
1260
2056
|
new_input_list = ItemHelpers.input_to_new_input_list(input)
|
|
1261
2057
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
+
)
|
|
1266
2070
|
|
|
1267
2071
|
@classmethod
|
|
1268
2072
|
async def _save_result_to_session(
|
|
1269
2073
|
cls,
|
|
1270
2074
|
session: Session | None,
|
|
1271
2075
|
original_input: str | list[TResponseInputItem],
|
|
1272
|
-
|
|
2076
|
+
new_items: list[RunItem],
|
|
2077
|
+
response_id: str | None = None,
|
|
1273
2078
|
) -> None:
|
|
1274
|
-
"""
|
|
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
|
+
"""
|
|
1275
2084
|
if session is None:
|
|
1276
2085
|
return
|
|
1277
2086
|
|
|
@@ -1279,11 +2088,77 @@ class AgentRunner:
|
|
|
1279
2088
|
input_list = ItemHelpers.input_to_new_input_list(original_input)
|
|
1280
2089
|
|
|
1281
2090
|
# Convert new items to input format
|
|
1282
|
-
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]
|
|
1283
2092
|
|
|
1284
2093
|
# Save all items from this turn
|
|
1285
2094
|
items_to_save = input_list + new_items_as_input
|
|
1286
2095
|
await session.add_items(items_to_save)
|
|
1287
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
|
+
|
|
1288
2144
|
|
|
1289
2145
|
DEFAULT_AGENT_RUNNER = AgentRunner()
|
|
2146
|
+
|
|
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
|
+
|
|
2161
|
+
def _copy_str_or_list(input: str | list[TResponseInputItem]) -> str | list[TResponseInputItem]:
|
|
2162
|
+
if isinstance(input, str):
|
|
2163
|
+
return input
|
|
2164
|
+
return input.copy()
|