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/run_context.py CHANGED
@@ -1,14 +1,17 @@
1
1
  from dataclasses import dataclass, field
2
- from typing import Any, Generic
2
+ from typing import TYPE_CHECKING, Any, Generic
3
3
 
4
4
  from typing_extensions import TypeVar
5
5
 
6
6
  from .usage import Usage
7
7
 
8
+ if TYPE_CHECKING:
9
+ from .items import TResponseInputItem
10
+
8
11
  TContext = TypeVar("TContext", default=Any)
9
12
 
10
13
 
11
- @dataclass
14
+ @dataclass(eq=False)
12
15
  class RunContextWrapper(Generic[TContext]):
13
16
  """This wraps the context object that you passed to `Runner.run()`. It also contains
14
17
  information about the usage of the agent run so far.
@@ -24,3 +27,11 @@ class RunContextWrapper(Generic[TContext]):
24
27
  """The usage of the agent run so far. For streamed responses, the usage will be stale until the
25
28
  last chunk of the stream is processed.
26
29
  """
30
+
31
+
32
+ @dataclass(eq=False)
33
+ class AgentHookContext(RunContextWrapper[TContext]):
34
+ """Context passed to agent hooks (on_start, on_end)."""
35
+
36
+ turn_input: "list[TResponseInputItem]" = field(default_factory=list)
37
+ """The input items for the current turn."""
agents/stream_events.py CHANGED
@@ -37,6 +37,7 @@ class RunItemStreamEvent:
37
37
  "tool_output",
38
38
  "reasoning_item_created",
39
39
  "mcp_approval_requested",
40
+ "mcp_approval_response",
40
41
  "mcp_list_tools",
41
42
  ]
42
43
  """The name of the event."""
agents/strict_schema.py CHANGED
@@ -87,6 +87,20 @@ def _ensure_strict_json_schema(
87
87
  for i, variant in enumerate(any_of)
88
88
  ]
89
89
 
90
+ # oneOf is not supported by OpenAI's structured outputs in nested contexts,
91
+ # so we convert it to anyOf which provides equivalent functionality for
92
+ # discriminated unions
93
+ one_of = json_schema.get("oneOf")
94
+ if is_list(one_of):
95
+ existing_any_of = json_schema.get("anyOf", [])
96
+ if not is_list(existing_any_of):
97
+ existing_any_of = []
98
+ json_schema["anyOf"] = existing_any_of + [
99
+ _ensure_strict_json_schema(variant, path=(*path, "oneOf", str(i)), root=root)
100
+ for i, variant in enumerate(one_of)
101
+ ]
102
+ json_schema.pop("oneOf")
103
+
90
104
  # intersections
91
105
  all_of = json_schema.get("allOf")
92
106
  if is_list(all_of):
agents/tool.py CHANGED
@@ -2,9 +2,21 @@ from __future__ import annotations
2
2
 
3
3
  import inspect
4
4
  import json
5
+ import weakref
5
6
  from collections.abc import Awaitable
6
- from dataclasses import dataclass
7
- from typing import TYPE_CHECKING, Any, Callable, Literal, Union, overload
7
+ from dataclasses import dataclass, field
8
+ from typing import (
9
+ TYPE_CHECKING,
10
+ Any,
11
+ Callable,
12
+ Generic,
13
+ Literal,
14
+ Protocol,
15
+ TypeVar,
16
+ Union,
17
+ cast,
18
+ overload,
19
+ )
8
20
 
9
21
  from openai.types.responses.file_search_tool_param import Filters, RankingOptions
10
22
  from openai.types.responses.response_computer_tool_call import (
@@ -13,25 +25,29 @@ from openai.types.responses.response_computer_tool_call import (
13
25
  )
14
26
  from openai.types.responses.response_output_item import LocalShellCall, McpApprovalRequest
15
27
  from openai.types.responses.tool_param import CodeInterpreter, ImageGeneration, Mcp
28
+ from openai.types.responses.web_search_tool import Filters as WebSearchToolFilters
16
29
  from openai.types.responses.web_search_tool_param import UserLocation
17
- from pydantic import ValidationError
30
+ from pydantic import BaseModel, TypeAdapter, ValidationError, model_validator
18
31
  from typing_extensions import Concatenate, NotRequired, ParamSpec, TypedDict
19
32
 
20
33
  from . import _debug
21
34
  from .computer import AsyncComputer, Computer
22
- from .exceptions import ModelBehaviorError
35
+ from .editor import ApplyPatchEditor
36
+ from .exceptions import ModelBehaviorError, UserError
23
37
  from .function_schema import DocstringStyle, function_schema
24
- from .items import RunItem
25
38
  from .logger import logger
26
39
  from .run_context import RunContextWrapper
27
40
  from .strict_schema import ensure_strict_json_schema
28
41
  from .tool_context import ToolContext
42
+ from .tool_guardrails import ToolInputGuardrail, ToolOutputGuardrail
29
43
  from .tracing import SpanError
30
44
  from .util import _error_tracing
31
45
  from .util._types import MaybeAwaitable
32
46
 
33
47
  if TYPE_CHECKING:
34
48
  from .agent import Agent, AgentBase
49
+ from .items import RunItem
50
+
35
51
 
36
52
  ToolParams = ParamSpec("ToolParams")
37
53
 
@@ -46,6 +62,123 @@ ToolFunction = Union[
46
62
  ]
47
63
 
48
64
 
65
+ class ToolOutputText(BaseModel):
66
+ """Represents a tool output that should be sent to the model as text."""
67
+
68
+ type: Literal["text"] = "text"
69
+ text: str
70
+
71
+
72
+ class ToolOutputTextDict(TypedDict, total=False):
73
+ """TypedDict variant for text tool outputs."""
74
+
75
+ type: Literal["text"]
76
+ text: str
77
+
78
+
79
+ class ToolOutputImage(BaseModel):
80
+ """Represents a tool output that should be sent to the model as an image.
81
+
82
+ You can provide either an `image_url` (URL or data URL) or a `file_id` for previously uploaded
83
+ content. The optional `detail` can control vision detail.
84
+ """
85
+
86
+ type: Literal["image"] = "image"
87
+ image_url: str | None = None
88
+ file_id: str | None = None
89
+ detail: Literal["low", "high", "auto"] | None = None
90
+
91
+ @model_validator(mode="after")
92
+ def check_at_least_one_required_field(self) -> ToolOutputImage:
93
+ """Validate that at least one of image_url or file_id is provided."""
94
+ if self.image_url is None and self.file_id is None:
95
+ raise ValueError("At least one of image_url or file_id must be provided")
96
+ return self
97
+
98
+
99
+ class ToolOutputImageDict(TypedDict, total=False):
100
+ """TypedDict variant for image tool outputs."""
101
+
102
+ type: Literal["image"]
103
+ image_url: NotRequired[str]
104
+ file_id: NotRequired[str]
105
+ detail: NotRequired[Literal["low", "high", "auto"]]
106
+
107
+
108
+ class ToolOutputFileContent(BaseModel):
109
+ """Represents a tool output that should be sent to the model as a file.
110
+
111
+ Provide one of `file_data` (base64), `file_url`, or `file_id`. You may also
112
+ provide an optional `filename` when using `file_data` to hint file name.
113
+ """
114
+
115
+ type: Literal["file"] = "file"
116
+ file_data: str | None = None
117
+ file_url: str | None = None
118
+ file_id: str | None = None
119
+ filename: str | None = None
120
+
121
+ @model_validator(mode="after")
122
+ def check_at_least_one_required_field(self) -> ToolOutputFileContent:
123
+ """Validate that at least one of file_data, file_url, or file_id is provided."""
124
+ if self.file_data is None and self.file_url is None and self.file_id is None:
125
+ raise ValueError("At least one of file_data, file_url, or file_id must be provided")
126
+ return self
127
+
128
+
129
+ class ToolOutputFileContentDict(TypedDict, total=False):
130
+ """TypedDict variant for file content tool outputs."""
131
+
132
+ type: Literal["file"]
133
+ file_data: NotRequired[str]
134
+ file_url: NotRequired[str]
135
+ file_id: NotRequired[str]
136
+ filename: NotRequired[str]
137
+
138
+
139
+ ValidToolOutputPydanticModels = Union[ToolOutputText, ToolOutputImage, ToolOutputFileContent]
140
+ ValidToolOutputPydanticModelsTypeAdapter: TypeAdapter[ValidToolOutputPydanticModels] = TypeAdapter(
141
+ ValidToolOutputPydanticModels
142
+ )
143
+
144
+ ComputerLike = Union[Computer, AsyncComputer]
145
+ ComputerT = TypeVar("ComputerT", bound=ComputerLike)
146
+ ComputerT_co = TypeVar("ComputerT_co", bound=ComputerLike, covariant=True)
147
+ ComputerT_contra = TypeVar("ComputerT_contra", bound=ComputerLike, contravariant=True)
148
+
149
+
150
+ class ComputerCreate(Protocol[ComputerT_co]):
151
+ """Initializes a computer for the current run context."""
152
+
153
+ def __call__(self, *, run_context: RunContextWrapper[Any]) -> MaybeAwaitable[ComputerT_co]: ...
154
+
155
+
156
+ class ComputerDispose(Protocol[ComputerT_contra]):
157
+ """Cleans up a computer initialized for a run context."""
158
+
159
+ def __call__(
160
+ self,
161
+ *,
162
+ run_context: RunContextWrapper[Any],
163
+ computer: ComputerT_contra,
164
+ ) -> MaybeAwaitable[None]: ...
165
+
166
+
167
+ @dataclass
168
+ class ComputerProvider(Generic[ComputerT]):
169
+ """Configures create/dispose hooks for per-run computer lifecycle management."""
170
+
171
+ create: ComputerCreate[ComputerT]
172
+ dispose: ComputerDispose[ComputerT] | None = None
173
+
174
+
175
+ ComputerConfig = Union[
176
+ ComputerT,
177
+ ComputerCreate[ComputerT],
178
+ ComputerProvider[ComputerT],
179
+ ]
180
+
181
+
49
182
  @dataclass
50
183
  class FunctionToolResult:
51
184
  tool: FunctionTool
@@ -79,7 +212,9 @@ class FunctionTool:
79
212
  1. The tool run context.
80
213
  2. The arguments from the LLM, as a JSON string.
81
214
 
82
- You must return a string representation of the tool output, or something we can call `str()` on.
215
+ You must return a one of the structured tool output types (e.g. ToolOutputText, ToolOutputImage,
216
+ ToolOutputFileContent) or a string representation of the tool output, or a list of them,
217
+ or something we can call `str()` on.
83
218
  In case of errors, you can either raise an Exception (which will cause the run to fail) or
84
219
  return a string error message (which will be sent back to the LLM).
85
220
  """
@@ -93,6 +228,13 @@ class FunctionTool:
93
228
  and returns whether the tool is enabled. You can use this to dynamically enable/disable a tool
94
229
  based on your context/state."""
95
230
 
231
+ # Tool-specific guardrails
232
+ tool_input_guardrails: list[ToolInputGuardrail[Any]] | None = None
233
+ """Optional list of input guardrails to run before invoking this tool."""
234
+
235
+ tool_output_guardrails: list[ToolOutputGuardrail[Any]] | None = None
236
+ """Optional list of output guardrails to run after invoking this tool."""
237
+
96
238
  def __post_init__(self):
97
239
  if self.strict_json_schema:
98
240
  self.params_json_schema = ensure_strict_json_schema(self.params_json_schema)
@@ -133,31 +275,169 @@ class WebSearchTool:
133
275
  user_location: UserLocation | None = None
134
276
  """Optional location for the search. Lets you customize results to be relevant to a location."""
135
277
 
278
+ filters: WebSearchToolFilters | None = None
279
+ """A filter to apply based on file attributes."""
280
+
136
281
  search_context_size: Literal["low", "medium", "high"] = "medium"
137
282
  """The amount of context to use for the search."""
138
283
 
139
284
  @property
140
285
  def name(self):
141
- return "web_search_preview"
286
+ return "web_search"
142
287
 
143
288
 
144
- @dataclass
145
- class ComputerTool:
289
+ @dataclass(eq=False)
290
+ class ComputerTool(Generic[ComputerT]):
146
291
  """A hosted tool that lets the LLM control a computer."""
147
292
 
148
- computer: Computer | AsyncComputer
149
- """The computer implementation, which describes the environment and dimensions of the computer,
150
- as well as implements the computer actions like click, screenshot, etc.
151
- """
293
+ computer: ComputerConfig[ComputerT]
294
+ """The computer implementation, or a factory that produces a computer per run."""
152
295
 
153
296
  on_safety_check: Callable[[ComputerToolSafetyCheckData], MaybeAwaitable[bool]] | None = None
154
297
  """Optional callback to acknowledge computer tool safety checks."""
155
298
 
299
+ def __post_init__(self) -> None:
300
+ _store_computer_initializer(self)
301
+
156
302
  @property
157
303
  def name(self):
158
304
  return "computer_use_preview"
159
305
 
160
306
 
307
+ @dataclass
308
+ class _ResolvedComputer:
309
+ computer: ComputerLike
310
+ dispose: ComputerDispose[ComputerLike] | None = None
311
+
312
+
313
+ _computer_cache: weakref.WeakKeyDictionary[
314
+ ComputerTool[Any],
315
+ weakref.WeakKeyDictionary[RunContextWrapper[Any], _ResolvedComputer],
316
+ ] = weakref.WeakKeyDictionary()
317
+ _computer_initializer_map: weakref.WeakKeyDictionary[ComputerTool[Any], ComputerConfig[Any]] = (
318
+ weakref.WeakKeyDictionary()
319
+ )
320
+ _computers_by_run_context: weakref.WeakKeyDictionary[
321
+ RunContextWrapper[Any], dict[ComputerTool[Any], _ResolvedComputer]
322
+ ] = weakref.WeakKeyDictionary()
323
+
324
+
325
+ def _is_computer_provider(candidate: object) -> bool:
326
+ return isinstance(candidate, ComputerProvider) or (
327
+ hasattr(candidate, "create") and callable(candidate.create)
328
+ )
329
+
330
+
331
+ def _store_computer_initializer(tool: ComputerTool[Any]) -> None:
332
+ config = tool.computer
333
+ if callable(config) or _is_computer_provider(config):
334
+ _computer_initializer_map[tool] = config
335
+
336
+
337
+ def _get_computer_initializer(tool: ComputerTool[Any]) -> ComputerConfig[Any] | None:
338
+ if tool in _computer_initializer_map:
339
+ return _computer_initializer_map[tool]
340
+
341
+ if callable(tool.computer) or _is_computer_provider(tool.computer):
342
+ return tool.computer
343
+
344
+ return None
345
+
346
+
347
+ def _track_resolved_computer(
348
+ *,
349
+ tool: ComputerTool[Any],
350
+ run_context: RunContextWrapper[Any],
351
+ resolved: _ResolvedComputer,
352
+ ) -> None:
353
+ resolved_by_run = _computers_by_run_context.get(run_context)
354
+ if resolved_by_run is None:
355
+ resolved_by_run = {}
356
+ _computers_by_run_context[run_context] = resolved_by_run
357
+ resolved_by_run[tool] = resolved
358
+
359
+
360
+ async def resolve_computer(
361
+ *, tool: ComputerTool[Any], run_context: RunContextWrapper[Any]
362
+ ) -> ComputerLike:
363
+ """Resolve a computer for a given run context, initializing it if needed."""
364
+ per_context = _computer_cache.get(tool)
365
+ if per_context is None:
366
+ per_context = weakref.WeakKeyDictionary()
367
+ _computer_cache[tool] = per_context
368
+
369
+ cached = per_context.get(run_context)
370
+ if cached is not None:
371
+ _track_resolved_computer(tool=tool, run_context=run_context, resolved=cached)
372
+ return cached.computer
373
+
374
+ initializer_config = _get_computer_initializer(tool)
375
+ lifecycle: ComputerProvider[Any] | None = (
376
+ cast(ComputerProvider[Any], initializer_config)
377
+ if _is_computer_provider(initializer_config)
378
+ else None
379
+ )
380
+ initializer: ComputerCreate[Any] | None = None
381
+ disposer: ComputerDispose[Any] | None = lifecycle.dispose if lifecycle else None
382
+
383
+ if lifecycle is not None:
384
+ initializer = lifecycle.create
385
+ elif callable(initializer_config):
386
+ initializer = initializer_config
387
+ elif _is_computer_provider(tool.computer):
388
+ lifecycle_provider = cast(ComputerProvider[Any], tool.computer)
389
+ initializer = lifecycle_provider.create
390
+ disposer = lifecycle_provider.dispose
391
+
392
+ if initializer:
393
+ computer_candidate = initializer(run_context=run_context)
394
+ computer = (
395
+ await computer_candidate
396
+ if inspect.isawaitable(computer_candidate)
397
+ else computer_candidate
398
+ )
399
+ else:
400
+ computer = cast(ComputerLike, tool.computer)
401
+
402
+ if not isinstance(computer, (Computer, AsyncComputer)):
403
+ raise UserError("The computer tool did not provide a computer instance.")
404
+
405
+ resolved = _ResolvedComputer(computer=computer, dispose=disposer)
406
+ per_context[run_context] = resolved
407
+ _track_resolved_computer(tool=tool, run_context=run_context, resolved=resolved)
408
+ tool.computer = computer
409
+ return computer
410
+
411
+
412
+ async def dispose_resolved_computers(*, run_context: RunContextWrapper[Any]) -> None:
413
+ """Dispose any computer instances created for the provided run context."""
414
+ resolved_by_tool = _computers_by_run_context.pop(run_context, None)
415
+ if not resolved_by_tool:
416
+ return
417
+
418
+ disposers: list[tuple[ComputerDispose[ComputerLike], ComputerLike]] = []
419
+
420
+ for tool, _resolved in resolved_by_tool.items():
421
+ per_context = _computer_cache.get(tool)
422
+ if per_context is not None:
423
+ per_context.pop(run_context, None)
424
+
425
+ initializer = _get_computer_initializer(tool)
426
+ if initializer is not None:
427
+ tool.computer = initializer
428
+
429
+ if _resolved.dispose is not None:
430
+ disposers.append((_resolved.dispose, _resolved.computer))
431
+
432
+ for dispose, computer in disposers:
433
+ try:
434
+ result = dispose(run_context=run_context, computer=computer)
435
+ if inspect.isawaitable(result):
436
+ await result
437
+ except Exception as exc:
438
+ logger.warning("Failed to dispose computer for run context: %s", exc)
439
+
440
+
161
441
  @dataclass
162
442
  class ComputerToolSafetyCheckData:
163
443
  """Information about a computer tool safety check."""
@@ -264,7 +544,11 @@ LocalShellExecutor = Callable[[LocalShellCommandRequest], MaybeAwaitable[str]]
264
544
 
265
545
  @dataclass
266
546
  class LocalShellTool:
267
- """A tool that allows the LLM to execute commands on a shell."""
547
+ """A tool that allows the LLM to execute commands on a shell.
548
+
549
+ For more details, see:
550
+ https://platform.openai.com/docs/guides/tools-local-shell
551
+ """
268
552
 
269
553
  executor: LocalShellExecutor
270
554
  """A function that executes a command on a shell."""
@@ -274,12 +558,109 @@ class LocalShellTool:
274
558
  return "local_shell"
275
559
 
276
560
 
561
+ @dataclass
562
+ class ShellCallOutcome:
563
+ """Describes the terminal condition of a shell command."""
564
+
565
+ type: Literal["exit", "timeout"]
566
+ exit_code: int | None = None
567
+
568
+
569
+ def _default_shell_outcome() -> ShellCallOutcome:
570
+ return ShellCallOutcome(type="exit")
571
+
572
+
573
+ @dataclass
574
+ class ShellCommandOutput:
575
+ """Structured output for a single shell command execution."""
576
+
577
+ stdout: str = ""
578
+ stderr: str = ""
579
+ outcome: ShellCallOutcome = field(default_factory=_default_shell_outcome)
580
+ command: str | None = None
581
+ provider_data: dict[str, Any] | None = None
582
+
583
+ @property
584
+ def exit_code(self) -> int | None:
585
+ return self.outcome.exit_code
586
+
587
+ @property
588
+ def status(self) -> Literal["completed", "timeout"]:
589
+ return "timeout" if self.outcome.type == "timeout" else "completed"
590
+
591
+
592
+ @dataclass
593
+ class ShellResult:
594
+ """Result returned by a shell executor."""
595
+
596
+ output: list[ShellCommandOutput]
597
+ max_output_length: int | None = None
598
+ provider_data: dict[str, Any] | None = None
599
+
600
+
601
+ @dataclass
602
+ class ShellActionRequest:
603
+ """Action payload for a next-generation shell call."""
604
+
605
+ commands: list[str]
606
+ timeout_ms: int | None = None
607
+ max_output_length: int | None = None
608
+
609
+
610
+ @dataclass
611
+ class ShellCallData:
612
+ """Normalized shell call data provided to shell executors."""
613
+
614
+ call_id: str
615
+ action: ShellActionRequest
616
+ status: Literal["in_progress", "completed"] | None = None
617
+ raw: Any | None = None
618
+
619
+
620
+ @dataclass
621
+ class ShellCommandRequest:
622
+ """A request to execute a modern shell call."""
623
+
624
+ ctx_wrapper: RunContextWrapper[Any]
625
+ data: ShellCallData
626
+
627
+
628
+ ShellExecutor = Callable[[ShellCommandRequest], MaybeAwaitable[Union[str, ShellResult]]]
629
+ """Executes a shell command sequence and returns either text or structured output."""
630
+
631
+
632
+ @dataclass
633
+ class ShellTool:
634
+ """Next-generation shell tool. LocalShellTool will be deprecated in favor of this."""
635
+
636
+ executor: ShellExecutor
637
+ name: str = "shell"
638
+
639
+ @property
640
+ def type(self) -> str:
641
+ return "shell"
642
+
643
+
644
+ @dataclass
645
+ class ApplyPatchTool:
646
+ """Hosted apply_patch tool. Lets the model request file mutations via unified diffs."""
647
+
648
+ editor: ApplyPatchEditor
649
+ name: str = "apply_patch"
650
+
651
+ @property
652
+ def type(self) -> str:
653
+ return "apply_patch"
654
+
655
+
277
656
  Tool = Union[
278
657
  FunctionTool,
279
658
  FileSearchTool,
280
659
  WebSearchTool,
281
- ComputerTool,
660
+ ComputerTool[Any],
282
661
  HostedMCPTool,
662
+ ShellTool,
663
+ ApplyPatchTool,
283
664
  LocalShellTool,
284
665
  ImageGenerationTool,
285
666
  CodeInterpreterTool,
@@ -306,6 +687,8 @@ def function_tool(
306
687
  failure_error_function: ToolErrorFunction | None = None,
307
688
  strict_mode: bool = True,
308
689
  is_enabled: bool | Callable[[RunContextWrapper[Any], AgentBase], MaybeAwaitable[bool]] = True,
690
+ tool_input_guardrails: list[ToolInputGuardrail[Any]] | None = None,
691
+ tool_output_guardrails: list[ToolOutputGuardrail[Any]] | None = None,
309
692
  ) -> FunctionTool:
310
693
  """Overload for usage as @function_tool (no parentheses)."""
311
694
  ...
@@ -321,6 +704,8 @@ def function_tool(
321
704
  failure_error_function: ToolErrorFunction | None = None,
322
705
  strict_mode: bool = True,
323
706
  is_enabled: bool | Callable[[RunContextWrapper[Any], AgentBase], MaybeAwaitable[bool]] = True,
707
+ tool_input_guardrails: list[ToolInputGuardrail[Any]] | None = None,
708
+ tool_output_guardrails: list[ToolOutputGuardrail[Any]] | None = None,
324
709
  ) -> Callable[[ToolFunction[...]], FunctionTool]:
325
710
  """Overload for usage as @function_tool(...)."""
326
711
  ...
@@ -336,6 +721,8 @@ def function_tool(
336
721
  failure_error_function: ToolErrorFunction | None = default_tool_error_function,
337
722
  strict_mode: bool = True,
338
723
  is_enabled: bool | Callable[[RunContextWrapper[Any], AgentBase], MaybeAwaitable[bool]] = True,
724
+ tool_input_guardrails: list[ToolInputGuardrail[Any]] | None = None,
725
+ tool_output_guardrails: list[ToolOutputGuardrail[Any]] | None = None,
339
726
  ) -> FunctionTool | Callable[[ToolFunction[...]], FunctionTool]:
340
727
  """
341
728
  Decorator to create a FunctionTool from a function. By default, we will:
@@ -367,6 +754,8 @@ def function_tool(
367
754
  is_enabled: Whether the tool is enabled. Can be a bool or a callable that takes the run
368
755
  context and agent and returns whether the tool is enabled. Disabled tools are hidden
369
756
  from the LLM at runtime.
757
+ tool_input_guardrails: Optional list of guardrails to run before invoking the tool.
758
+ tool_output_guardrails: Optional list of guardrails to run after the tool returns.
370
759
  """
371
760
 
372
761
  def _create_function_tool(the_func: ToolFunction[...]) -> FunctionTool:
@@ -448,6 +837,13 @@ def function_tool(
448
837
  },
449
838
  )
450
839
  )
840
+ if _debug.DONT_LOG_TOOL_DATA:
841
+ logger.debug(f"Tool {schema.name} failed")
842
+ else:
843
+ logger.error(
844
+ f"Tool {schema.name} failed: {input} {e}",
845
+ exc_info=e,
846
+ )
451
847
  return result
452
848
 
453
849
  return FunctionTool(
@@ -457,6 +853,8 @@ def function_tool(
457
853
  on_invoke_tool=_on_invoke_tool,
458
854
  strict_json_schema=strict_mode,
459
855
  is_enabled=is_enabled,
856
+ tool_input_guardrails=tool_input_guardrails,
857
+ tool_output_guardrails=tool_output_guardrails,
460
858
  )
461
859
 
462
860
  # If func is actually a callable, we were used as @function_tool with no parentheses
agents/tool_context.py CHANGED
@@ -14,6 +14,10 @@ def _assert_must_pass_tool_name() -> str:
14
14
  raise ValueError("tool_name must be passed to ToolContext")
15
15
 
16
16
 
17
+ def _assert_must_pass_tool_arguments() -> str:
18
+ raise ValueError("tool_arguments must be passed to ToolContext")
19
+
20
+
17
21
  @dataclass
18
22
  class ToolContext(RunContextWrapper[TContext]):
19
23
  """The context of a tool call."""
@@ -24,6 +28,12 @@ class ToolContext(RunContextWrapper[TContext]):
24
28
  tool_call_id: str = field(default_factory=_assert_must_pass_tool_call_id)
25
29
  """The ID of the tool call."""
26
30
 
31
+ tool_arguments: str = field(default_factory=_assert_must_pass_tool_arguments)
32
+ """The raw arguments string of the tool call."""
33
+
34
+ tool_call: Optional[ResponseFunctionToolCall] = None
35
+ """The tool call object associated with this invocation."""
36
+
27
37
  @classmethod
28
38
  def from_agent_context(
29
39
  cls,
@@ -39,4 +49,15 @@ class ToolContext(RunContextWrapper[TContext]):
39
49
  f.name: getattr(context, f.name) for f in fields(RunContextWrapper) if f.init
40
50
  }
41
51
  tool_name = tool_call.name if tool_call is not None else _assert_must_pass_tool_name()
42
- return cls(tool_name=tool_name, tool_call_id=tool_call_id, **base_values)
52
+ tool_args = (
53
+ tool_call.arguments if tool_call is not None else _assert_must_pass_tool_arguments()
54
+ )
55
+
56
+ tool_context = cls(
57
+ tool_name=tool_name,
58
+ tool_call_id=tool_call_id,
59
+ tool_arguments=tool_args,
60
+ tool_call=tool_call,
61
+ **base_values,
62
+ )
63
+ return tool_context