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.
Files changed (96) hide show
  1. agents/__init__.py +105 -4
  2. agents/_debug.py +15 -4
  3. agents/_run_impl.py +1203 -96
  4. agents/agent.py +164 -19
  5. agents/apply_diff.py +329 -0
  6. agents/editor.py +47 -0
  7. agents/exceptions.py +35 -0
  8. agents/extensions/experimental/__init__.py +6 -0
  9. agents/extensions/experimental/codex/__init__.py +92 -0
  10. agents/extensions/experimental/codex/codex.py +89 -0
  11. agents/extensions/experimental/codex/codex_options.py +35 -0
  12. agents/extensions/experimental/codex/codex_tool.py +1142 -0
  13. agents/extensions/experimental/codex/events.py +162 -0
  14. agents/extensions/experimental/codex/exec.py +263 -0
  15. agents/extensions/experimental/codex/items.py +245 -0
  16. agents/extensions/experimental/codex/output_schema_file.py +50 -0
  17. agents/extensions/experimental/codex/payloads.py +31 -0
  18. agents/extensions/experimental/codex/thread.py +214 -0
  19. agents/extensions/experimental/codex/thread_options.py +54 -0
  20. agents/extensions/experimental/codex/turn_options.py +36 -0
  21. agents/extensions/handoff_filters.py +13 -1
  22. agents/extensions/memory/__init__.py +120 -0
  23. agents/extensions/memory/advanced_sqlite_session.py +1285 -0
  24. agents/extensions/memory/async_sqlite_session.py +239 -0
  25. agents/extensions/memory/dapr_session.py +423 -0
  26. agents/extensions/memory/encrypt_session.py +185 -0
  27. agents/extensions/memory/redis_session.py +261 -0
  28. agents/extensions/memory/sqlalchemy_session.py +334 -0
  29. agents/extensions/models/litellm_model.py +449 -36
  30. agents/extensions/models/litellm_provider.py +3 -1
  31. agents/function_schema.py +47 -5
  32. agents/guardrail.py +16 -2
  33. agents/{handoffs.py → handoffs/__init__.py} +89 -47
  34. agents/handoffs/history.py +268 -0
  35. agents/items.py +237 -11
  36. agents/lifecycle.py +75 -14
  37. agents/mcp/server.py +280 -37
  38. agents/mcp/util.py +24 -3
  39. agents/memory/__init__.py +22 -2
  40. agents/memory/openai_conversations_session.py +91 -0
  41. agents/memory/openai_responses_compaction_session.py +249 -0
  42. agents/memory/session.py +19 -261
  43. agents/memory/sqlite_session.py +275 -0
  44. agents/memory/util.py +20 -0
  45. agents/model_settings.py +14 -3
  46. agents/models/__init__.py +13 -0
  47. agents/models/chatcmpl_converter.py +303 -50
  48. agents/models/chatcmpl_helpers.py +63 -0
  49. agents/models/chatcmpl_stream_handler.py +290 -68
  50. agents/models/default_models.py +58 -0
  51. agents/models/interface.py +4 -0
  52. agents/models/openai_chatcompletions.py +103 -49
  53. agents/models/openai_provider.py +10 -4
  54. agents/models/openai_responses.py +162 -46
  55. agents/realtime/__init__.py +4 -0
  56. agents/realtime/_util.py +14 -3
  57. agents/realtime/agent.py +7 -0
  58. agents/realtime/audio_formats.py +53 -0
  59. agents/realtime/config.py +78 -10
  60. agents/realtime/events.py +18 -0
  61. agents/realtime/handoffs.py +2 -2
  62. agents/realtime/items.py +17 -1
  63. agents/realtime/model.py +13 -0
  64. agents/realtime/model_events.py +12 -0
  65. agents/realtime/model_inputs.py +18 -1
  66. agents/realtime/openai_realtime.py +696 -150
  67. agents/realtime/session.py +243 -23
  68. agents/repl.py +7 -3
  69. agents/result.py +197 -38
  70. agents/run.py +949 -168
  71. agents/run_context.py +13 -2
  72. agents/stream_events.py +1 -0
  73. agents/strict_schema.py +14 -0
  74. agents/tool.py +413 -15
  75. agents/tool_context.py +22 -1
  76. agents/tool_guardrails.py +279 -0
  77. agents/tracing/__init__.py +2 -0
  78. agents/tracing/config.py +9 -0
  79. agents/tracing/create.py +4 -0
  80. agents/tracing/processor_interface.py +84 -11
  81. agents/tracing/processors.py +65 -54
  82. agents/tracing/provider.py +64 -7
  83. agents/tracing/spans.py +105 -0
  84. agents/tracing/traces.py +116 -16
  85. agents/usage.py +134 -12
  86. agents/util/_json.py +19 -1
  87. agents/util/_transforms.py +12 -2
  88. agents/voice/input.py +5 -4
  89. agents/voice/models/openai_stt.py +17 -9
  90. agents/voice/pipeline.py +2 -0
  91. agents/voice/pipeline_config.py +4 -0
  92. {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/METADATA +44 -19
  93. openai_agents-0.6.8.dist-info/RECORD +134 -0
  94. {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/WHEEL +1 -1
  95. openai_agents-0.2.8.dist-info/RECORD +0 -103
  96. {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/licenses/LICENSE +0 -0
agents/result.py CHANGED
@@ -2,9 +2,10 @@ from __future__ import annotations
2
2
 
3
3
  import abc
4
4
  import asyncio
5
+ import weakref
5
6
  from collections.abc import AsyncIterator
6
7
  from dataclasses import dataclass, field
7
- from typing import TYPE_CHECKING, Any, cast
8
+ from typing import TYPE_CHECKING, Any, Literal, cast
8
9
 
9
10
  from typing_extensions import TypeVar
10
11
 
@@ -31,6 +32,7 @@ from .util._pretty_print import (
31
32
  if TYPE_CHECKING:
32
33
  from ._run_impl import QueueCompleteSentinel
33
34
  from .agent import Agent
35
+ from .tool_guardrails import ToolInputGuardrailResult, ToolOutputGuardrailResult
34
36
 
35
37
  T = TypeVar("T")
36
38
 
@@ -59,6 +61,12 @@ class RunResultBase(abc.ABC):
59
61
  output_guardrail_results: list[OutputGuardrailResult]
60
62
  """Guardrail results for the final output of the agent."""
61
63
 
64
+ tool_input_guardrail_results: list[ToolInputGuardrailResult]
65
+ """Tool input guardrail results from all tools executed during the run."""
66
+
67
+ tool_output_guardrail_results: list[ToolOutputGuardrailResult]
68
+ """Tool output guardrail results from all tools executed during the run."""
69
+
62
70
  context_wrapper: RunContextWrapper[Any]
63
71
  """The context wrapper for the agent run."""
64
72
 
@@ -67,6 +75,35 @@ class RunResultBase(abc.ABC):
67
75
  def last_agent(self) -> Agent[Any]:
68
76
  """The last agent that was run."""
69
77
 
78
+ def release_agents(self, *, release_new_items: bool = True) -> None:
79
+ """
80
+ Release strong references to agents held by this result. After calling this method,
81
+ accessing `item.agent` or `last_agent` may return `None` if the agent has been garbage
82
+ collected. Callers can use this when they are done inspecting the result and want to
83
+ eagerly drop any associated agent graph.
84
+ """
85
+ if release_new_items:
86
+ for item in self.new_items:
87
+ release = getattr(item, "release_agent", None)
88
+ if callable(release):
89
+ release()
90
+ self._release_last_agent_reference()
91
+
92
+ def __del__(self) -> None:
93
+ try:
94
+ # Fall back to releasing agents automatically in case the caller never invoked
95
+ # `release_agents()` explicitly so GC of the RunResult drops the last strong reference.
96
+ # We pass `release_new_items=False` so RunItems that the user intentionally keeps
97
+ # continue exposing their originating agent until that agent itself is collected.
98
+ self.release_agents(release_new_items=False)
99
+ except Exception:
100
+ # Avoid raising from __del__.
101
+ pass
102
+
103
+ @abc.abstractmethod
104
+ def _release_last_agent_reference(self) -> None:
105
+ """Release stored agent reference specific to the concrete result type."""
106
+
70
107
  def final_output_as(self, cls: type[T], raise_if_incorrect_type: bool = False) -> T:
71
108
  """A convenience method to cast the final output to a specific type. By default, the cast
72
109
  is only for the typechecker. If you set `raise_if_incorrect_type` to True, we'll raise a
@@ -104,11 +141,34 @@ class RunResultBase(abc.ABC):
104
141
  @dataclass
105
142
  class RunResult(RunResultBase):
106
143
  _last_agent: Agent[Any]
144
+ _last_agent_ref: weakref.ReferenceType[Agent[Any]] | None = field(
145
+ init=False,
146
+ repr=False,
147
+ default=None,
148
+ )
149
+
150
+ def __post_init__(self) -> None:
151
+ self._last_agent_ref = weakref.ref(self._last_agent)
107
152
 
108
153
  @property
109
154
  def last_agent(self) -> Agent[Any]:
110
155
  """The last agent that was run."""
111
- return self._last_agent
156
+ agent = cast("Agent[Any] | None", self.__dict__.get("_last_agent"))
157
+ if agent is not None:
158
+ return agent
159
+ if self._last_agent_ref:
160
+ agent = self._last_agent_ref()
161
+ if agent is not None:
162
+ return agent
163
+ raise AgentsException("Last agent reference is no longer available.")
164
+
165
+ def _release_last_agent_reference(self) -> None:
166
+ agent = cast("Agent[Any] | None", self.__dict__.get("_last_agent"))
167
+ if agent is None:
168
+ return
169
+ self._last_agent_ref = weakref.ref(agent)
170
+ # Preserve dataclass field so repr/asdict continue to succeed.
171
+ self.__dict__["_last_agent"] = None
112
172
 
113
173
  def __str__(self) -> str:
114
174
  return pretty_print_result(self)
@@ -143,6 +203,15 @@ class RunResultStreaming(RunResultBase):
143
203
  is_complete: bool = False
144
204
  """Whether the agent has finished running."""
145
205
 
206
+ _current_agent_ref: weakref.ReferenceType[Agent[Any]] | None = field(
207
+ init=False,
208
+ repr=False,
209
+ default=None,
210
+ )
211
+
212
+ _model_input_items: list[RunItem] = field(default_factory=list, repr=False)
213
+ """Filtered items used to build model input between streaming turns."""
214
+
146
215
  # Queues that the background run_loop writes to
147
216
  _event_queue: asyncio.Queue[StreamEvent | QueueCompleteSentinel] = field(
148
217
  default_factory=asyncio.Queue, repr=False
@@ -157,24 +226,79 @@ class RunResultStreaming(RunResultBase):
157
226
  _output_guardrails_task: asyncio.Task[Any] | None = field(default=None, repr=False)
158
227
  _stored_exception: Exception | None = field(default=None, repr=False)
159
228
 
229
+ # Soft cancel state
230
+ _cancel_mode: Literal["none", "immediate", "after_turn"] = field(default="none", repr=False)
231
+
232
+ def __post_init__(self) -> None:
233
+ self._current_agent_ref = weakref.ref(self.current_agent)
234
+
160
235
  @property
161
236
  def last_agent(self) -> Agent[Any]:
162
237
  """The last agent that was run. Updates as the agent run progresses, so the true last agent
163
238
  is only available after the agent run is complete.
164
239
  """
165
- return self.current_agent
240
+ agent = cast("Agent[Any] | None", self.__dict__.get("current_agent"))
241
+ if agent is not None:
242
+ return agent
243
+ if self._current_agent_ref:
244
+ agent = self._current_agent_ref()
245
+ if agent is not None:
246
+ return agent
247
+ raise AgentsException("Last agent reference is no longer available.")
248
+
249
+ def _release_last_agent_reference(self) -> None:
250
+ agent = cast("Agent[Any] | None", self.__dict__.get("current_agent"))
251
+ if agent is None:
252
+ return
253
+ self._current_agent_ref = weakref.ref(agent)
254
+ # Preserve dataclass field so repr/asdict continue to succeed.
255
+ self.__dict__["current_agent"] = None
256
+
257
+ def cancel(self, mode: Literal["immediate", "after_turn"] = "immediate") -> None:
258
+ """Cancel the streaming run.
166
259
 
167
- def cancel(self) -> None:
168
- """Cancels the streaming run, stopping all background tasks and marking the run as
169
- complete."""
170
- self._cleanup_tasks() # Cancel all running tasks
171
- self.is_complete = True # Mark the run as complete to stop event streaming
172
-
173
- # Optionally, clear the event queue to prevent processing stale events
174
- while not self._event_queue.empty():
175
- self._event_queue.get_nowait()
176
- while not self._input_guardrail_queue.empty():
177
- self._input_guardrail_queue.get_nowait()
260
+ Args:
261
+ mode: Cancellation strategy:
262
+ - "immediate": Stop immediately, cancel all tasks, clear queues (default)
263
+ - "after_turn": Complete current turn gracefully before stopping
264
+ * Allows LLM response to finish
265
+ * Executes pending tool calls
266
+ * Saves session state properly
267
+ * Tracks usage accurately
268
+ * Stops before next turn begins
269
+
270
+ Example:
271
+ ```python
272
+ result = Runner.run_streamed(agent, "Task", session=session)
273
+
274
+ async for event in result.stream_events():
275
+ if user_interrupted():
276
+ result.cancel(mode="after_turn") # Graceful
277
+ # result.cancel() # Immediate (default)
278
+ ```
279
+
280
+ Note: After calling cancel(), you should continue consuming stream_events()
281
+ to allow the cancellation to complete properly.
282
+ """
283
+ # Store the cancel mode for the background task to check
284
+ self._cancel_mode = mode
285
+
286
+ if mode == "immediate":
287
+ # Existing behavior - immediate shutdown
288
+ self._cleanup_tasks() # Cancel all running tasks
289
+ self.is_complete = True # Mark the run as complete to stop event streaming
290
+
291
+ # Optionally, clear the event queue to prevent processing stale events
292
+ while not self._event_queue.empty():
293
+ self._event_queue.get_nowait()
294
+ while not self._input_guardrail_queue.empty():
295
+ self._input_guardrail_queue.get_nowait()
296
+
297
+ elif mode == "after_turn":
298
+ # Soft cancel - just set the flag
299
+ # The streaming loop will check this and stop gracefully
300
+ # Don't call _cleanup_tasks() or clear queues yet
301
+ pass
178
302
 
179
303
  async def stream_events(self) -> AsyncIterator[StreamEvent]:
180
304
  """Stream deltas for new items as they are generated. We're using the types from the
@@ -185,31 +309,50 @@ class RunResultStreaming(RunResultBase):
185
309
  - A MaxTurnsExceeded exception if the agent exceeds the max_turns limit.
186
310
  - A GuardrailTripwireTriggered exception if a guardrail is tripped.
187
311
  """
188
- while True:
189
- self._check_errors()
190
- if self._stored_exception:
191
- logger.debug("Breaking due to stored exception")
192
- self.is_complete = True
193
- break
194
-
195
- if self.is_complete and self._event_queue.empty():
196
- break
197
-
198
- try:
199
- item = await self._event_queue.get()
200
- except asyncio.CancelledError:
201
- break
202
-
203
- if isinstance(item, QueueCompleteSentinel):
204
- self._event_queue.task_done()
205
- # Check for errors, in case the queue was completed due to an exception
312
+ cancelled = False
313
+ try:
314
+ while True:
206
315
  self._check_errors()
207
- break
208
-
209
- yield item
210
- self._event_queue.task_done()
211
-
212
- self._cleanup_tasks()
316
+ if self._stored_exception:
317
+ logger.debug("Breaking due to stored exception")
318
+ self.is_complete = True
319
+ break
320
+
321
+ if self.is_complete and self._event_queue.empty():
322
+ break
323
+
324
+ try:
325
+ item = await self._event_queue.get()
326
+ except asyncio.CancelledError:
327
+ cancelled = True
328
+ self.cancel()
329
+ raise
330
+
331
+ if isinstance(item, QueueCompleteSentinel):
332
+ # Await input guardrails if they are still running, so late
333
+ # exceptions are captured.
334
+ await self._await_task_safely(self._input_guardrails_task)
335
+
336
+ self._event_queue.task_done()
337
+
338
+ # Check for errors, in case the queue was completed
339
+ # due to an exception
340
+ self._check_errors()
341
+ break
342
+
343
+ yield item
344
+ self._event_queue.task_done()
345
+ finally:
346
+ if cancelled:
347
+ # Cancellation should return promptly, so avoid waiting on long-running tasks.
348
+ # Tasks have already been cancelled above.
349
+ self._cleanup_tasks()
350
+ else:
351
+ # Ensure main execution completes before cleanup to avoid race conditions
352
+ # with session operations
353
+ await self._await_task_safely(self._run_impl_task)
354
+ # Safely terminate all background tasks after main execution has finished
355
+ self._cleanup_tasks()
213
356
 
214
357
  if self._stored_exception:
215
358
  raise self._stored_exception
@@ -274,3 +417,19 @@ class RunResultStreaming(RunResultBase):
274
417
 
275
418
  def __str__(self) -> str:
276
419
  return pretty_print_run_result_streaming(self)
420
+
421
+ async def _await_task_safely(self, task: asyncio.Task[Any] | None) -> None:
422
+ """Await a task if present, ignoring cancellation and storing exceptions elsewhere.
423
+
424
+ This ensures we do not lose late guardrail exceptions while not surfacing
425
+ CancelledError to callers of stream_events.
426
+ """
427
+ if task and not task.done():
428
+ try:
429
+ await task
430
+ except asyncio.CancelledError:
431
+ # Task was cancelled (e.g., due to result.cancel()). Nothing to do here.
432
+ pass
433
+ except Exception:
434
+ # The exception will be surfaced via _check_errors() if needed.
435
+ pass