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/agent.py CHANGED
@@ -13,21 +13,38 @@ from typing_extensions import NotRequired, TypeAlias, TypedDict
13
13
  from .agent_output import AgentOutputSchemaBase
14
14
  from .guardrail import InputGuardrail, OutputGuardrail
15
15
  from .handoffs import Handoff
16
- from .items import ItemHelpers
17
16
  from .logger import logger
18
17
  from .mcp import MCPUtil
19
18
  from .model_settings import ModelSettings
19
+ from .models.default_models import (
20
+ get_default_model_settings,
21
+ gpt_5_reasoning_settings_required,
22
+ is_gpt_5_default,
23
+ )
20
24
  from .models.interface import Model
21
25
  from .prompts import DynamicPromptFunction, Prompt, PromptUtil
22
26
  from .run_context import RunContextWrapper, TContext
23
- from .tool import FunctionTool, FunctionToolResult, Tool, function_tool
27
+ from .tool import (
28
+ FunctionTool,
29
+ FunctionToolResult,
30
+ Tool,
31
+ ToolErrorFunction,
32
+ default_tool_error_function,
33
+ function_tool,
34
+ )
35
+ from .tool_context import ToolContext
24
36
  from .util import _transforms
25
37
  from .util._types import MaybeAwaitable
26
38
 
27
39
  if TYPE_CHECKING:
28
- from .lifecycle import AgentHooks
40
+ from openai.types.responses.response_function_tool_call import ResponseFunctionToolCall
41
+
42
+ from .lifecycle import AgentHooks, RunHooks
29
43
  from .mcp import MCPServer
30
- from .result import RunResult
44
+ from .memory.session import Session
45
+ from .result import RunResult, RunResultStreaming
46
+ from .run import RunConfig
47
+ from .stream_events import StreamEvent
31
48
 
32
49
 
33
50
  @dataclass
@@ -52,6 +69,19 @@ ToolsToFinalOutputFunction: TypeAlias = Callable[
52
69
  """
53
70
 
54
71
 
72
+ class AgentToolStreamEvent(TypedDict):
73
+ """Streaming event emitted when an agent is invoked as a tool."""
74
+
75
+ event: StreamEvent
76
+ """The streaming event from the nested agent run."""
77
+
78
+ agent: Agent[Any]
79
+ """The nested agent emitting the event."""
80
+
81
+ tool_call: ResponseFunctionToolCall | None
82
+ """The originating tool call, if available."""
83
+
84
+
55
85
  class StopAtTools(TypedDict):
56
86
  stop_at_tool_names: list[str]
57
87
  """A list of tool names, any of which will stop the agent from running further."""
@@ -168,10 +198,10 @@ class Agent(AgentBase, Generic[TContext]):
168
198
  """The model implementation to use when invoking the LLM.
169
199
 
170
200
  By default, if not set, the agent will use the default model configured in
171
- `openai_provider.DEFAULT_MODEL` (currently "gpt-4o").
201
+ `agents.models.get_default_model()` (currently "gpt-4.1").
172
202
  """
173
203
 
174
- model_settings: ModelSettings = field(default_factory=ModelSettings)
204
+ model_settings: ModelSettings = field(default_factory=get_default_model_settings)
175
205
  """Configures model-specific tuning parameters (e.g. temperature, top_p).
176
206
  """
177
207
 
@@ -205,8 +235,9 @@ class Agent(AgentBase, Generic[TContext]):
205
235
  This lets you configure how tool use is handled.
206
236
  - "run_llm_again": The default behavior. Tools are run, and then the LLM receives the results
207
237
  and gets to respond.
208
- - "stop_on_first_tool": The output of the first tool call is used as the final output. This
209
- means that the LLM does not process the result of the tool call.
238
+ - "stop_on_first_tool": The output from the first tool call is treated as the final result.
239
+ In other words, it isn’t sent back to the LLM for further processing but is used directly
240
+ as the final output.
210
241
  - A StopAtTools object: The agent will stop running if any of the tools listed in
211
242
  `stop_at_tool_names` is called.
212
243
  The final output will be the output of the first matching tool call.
@@ -285,6 +316,26 @@ class Agent(AgentBase, Generic[TContext]):
285
316
  f"got {type(self.model_settings).__name__}"
286
317
  )
287
318
 
319
+ if (
320
+ # The user sets a non-default model
321
+ self.model is not None
322
+ and (
323
+ # The default model is gpt-5
324
+ is_gpt_5_default() is True
325
+ # However, the specified model is not a gpt-5 model
326
+ and (
327
+ isinstance(self.model, str) is False
328
+ or gpt_5_reasoning_settings_required(self.model) is False # type: ignore
329
+ )
330
+ # The model settings are not customized for the specified model
331
+ and self.model_settings == get_default_model_settings()
332
+ )
333
+ ):
334
+ # In this scenario, we should use a generic model settings
335
+ # because non-gpt-5 models are not compatible with the default gpt-5 model settings.
336
+ # This is a best-effort attempt to make the agent work with non-gpt-5 models.
337
+ self.model_settings = ModelSettings()
338
+
288
339
  if not isinstance(self.input_guardrails, list):
289
340
  raise TypeError(
290
341
  f"Agent input_guardrails must be a list, got {type(self.input_guardrails).__name__}"
@@ -355,7 +406,19 @@ class Agent(AgentBase, Generic[TContext]):
355
406
  self,
356
407
  tool_name: str | None,
357
408
  tool_description: str | None,
358
- custom_output_extractor: Callable[[RunResult], Awaitable[str]] | None = None,
409
+ custom_output_extractor: (
410
+ Callable[[RunResult | RunResultStreaming], Awaitable[str]] | None
411
+ ) = None,
412
+ is_enabled: bool
413
+ | Callable[[RunContextWrapper[Any], AgentBase[Any]], MaybeAwaitable[bool]] = True,
414
+ on_stream: Callable[[AgentToolStreamEvent], MaybeAwaitable[None]] | None = None,
415
+ run_config: RunConfig | None = None,
416
+ max_turns: int | None = None,
417
+ hooks: RunHooks[TContext] | None = None,
418
+ previous_response_id: str | None = None,
419
+ conversation_id: str | None = None,
420
+ session: Session | None = None,
421
+ failure_error_function: ToolErrorFunction | None = default_tool_error_function,
359
422
  ) -> Tool:
360
423
  """Transform this agent into a tool, callable by other agents.
361
424
 
@@ -371,24 +434,106 @@ class Agent(AgentBase, Generic[TContext]):
371
434
  when to use it.
372
435
  custom_output_extractor: A function that extracts the output from the agent. If not
373
436
  provided, the last message from the agent will be used.
437
+ is_enabled: Whether the tool is enabled. Can be a bool or a callable that takes the run
438
+ context and agent and returns whether the tool is enabled. Disabled tools are hidden
439
+ from the LLM at runtime.
440
+ on_stream: Optional callback (sync or async) to receive streaming events from the nested
441
+ agent run. The callback receives an `AgentToolStreamEvent` containing the nested
442
+ agent, the originating tool call (when available), and each stream event. When
443
+ provided, the nested agent is executed in streaming mode.
444
+ failure_error_function: If provided, generate an error message when the tool (agent) run
445
+ fails. The message is sent to the LLM. If None, the exception is raised instead.
374
446
  """
375
447
 
376
448
  @function_tool(
377
449
  name_override=tool_name or _transforms.transform_string_function_style(self.name),
378
450
  description_override=tool_description or "",
451
+ is_enabled=is_enabled,
452
+ failure_error_function=failure_error_function,
379
453
  )
380
- async def run_agent(context: RunContextWrapper, input: str) -> str:
381
- from .run import Runner
382
-
383
- output = await Runner.run(
384
- starting_agent=self,
385
- input=input,
386
- context=context.context,
387
- )
454
+ async def run_agent(context: ToolContext, input: str) -> Any:
455
+ from .run import DEFAULT_MAX_TURNS, Runner
456
+
457
+ resolved_max_turns = max_turns if max_turns is not None else DEFAULT_MAX_TURNS
458
+ run_result: RunResult | RunResultStreaming
459
+
460
+ if on_stream is not None:
461
+ run_result = Runner.run_streamed(
462
+ starting_agent=self,
463
+ input=input,
464
+ context=context.context,
465
+ run_config=run_config,
466
+ max_turns=resolved_max_turns,
467
+ hooks=hooks,
468
+ previous_response_id=previous_response_id,
469
+ conversation_id=conversation_id,
470
+ session=session,
471
+ )
472
+ # Dispatch callbacks in the background so slow handlers do not block
473
+ # event consumption.
474
+ event_queue: asyncio.Queue[AgentToolStreamEvent | None] = asyncio.Queue()
475
+
476
+ async def _run_handler(payload: AgentToolStreamEvent) -> None:
477
+ """Execute the user callback while capturing exceptions."""
478
+ try:
479
+ maybe_result = on_stream(payload)
480
+ if inspect.isawaitable(maybe_result):
481
+ await maybe_result
482
+ except Exception:
483
+ logger.exception(
484
+ "Error while handling on_stream event for agent tool %s.",
485
+ self.name,
486
+ )
487
+
488
+ async def dispatch_stream_events() -> None:
489
+ while True:
490
+ payload = await event_queue.get()
491
+ is_sentinel = payload is None # None marks the end of the stream.
492
+ try:
493
+ if payload is not None:
494
+ await _run_handler(payload)
495
+ finally:
496
+ event_queue.task_done()
497
+
498
+ if is_sentinel:
499
+ break
500
+
501
+ dispatch_task = asyncio.create_task(dispatch_stream_events())
502
+
503
+ try:
504
+ from .stream_events import AgentUpdatedStreamEvent
505
+
506
+ current_agent = run_result.current_agent
507
+ async for event in run_result.stream_events():
508
+ if isinstance(event, AgentUpdatedStreamEvent):
509
+ current_agent = event.new_agent
510
+
511
+ payload: AgentToolStreamEvent = {
512
+ "event": event,
513
+ "agent": current_agent,
514
+ "tool_call": context.tool_call,
515
+ }
516
+ await event_queue.put(payload)
517
+ finally:
518
+ await event_queue.put(None)
519
+ await event_queue.join()
520
+ await dispatch_task
521
+ else:
522
+ run_result = await Runner.run(
523
+ starting_agent=self,
524
+ input=input,
525
+ context=context.context,
526
+ run_config=run_config,
527
+ max_turns=resolved_max_turns,
528
+ hooks=hooks,
529
+ previous_response_id=previous_response_id,
530
+ conversation_id=conversation_id,
531
+ session=session,
532
+ )
388
533
  if custom_output_extractor:
389
- return await custom_output_extractor(output)
534
+ return await custom_output_extractor(run_result)
390
535
 
391
- return ItemHelpers.text_message_outputs(output.new_items)
536
+ return run_result.final_output
392
537
 
393
538
  return run_agent
394
539
 
agents/apply_diff.py ADDED
@@ -0,0 +1,329 @@
1
+ """Utility for applying V4A diffs against text inputs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Sequence
7
+ from dataclasses import dataclass
8
+ from typing import Callable, Literal
9
+
10
+ ApplyDiffMode = Literal["default", "create"]
11
+
12
+
13
+ @dataclass
14
+ class Chunk:
15
+ orig_index: int
16
+ del_lines: list[str]
17
+ ins_lines: list[str]
18
+
19
+
20
+ @dataclass
21
+ class ParserState:
22
+ lines: list[str]
23
+ index: int = 0
24
+ fuzz: int = 0
25
+
26
+
27
+ @dataclass
28
+ class ParsedUpdateDiff:
29
+ chunks: list[Chunk]
30
+ fuzz: int
31
+
32
+
33
+ @dataclass
34
+ class ReadSectionResult:
35
+ next_context: list[str]
36
+ section_chunks: list[Chunk]
37
+ end_index: int
38
+ eof: bool
39
+
40
+
41
+ END_PATCH = "*** End Patch"
42
+ END_FILE = "*** End of File"
43
+ SECTION_TERMINATORS = [
44
+ END_PATCH,
45
+ "*** Update File:",
46
+ "*** Delete File:",
47
+ "*** Add File:",
48
+ ]
49
+ END_SECTION_MARKERS = [*SECTION_TERMINATORS, END_FILE]
50
+
51
+
52
+ def apply_diff(input: str, diff: str, mode: ApplyDiffMode = "default") -> str:
53
+ """Apply a V4A diff to the provided text.
54
+
55
+ This parser understands both the create-file syntax (only "+" prefixed
56
+ lines) and the default update syntax that includes context hunks.
57
+ """
58
+
59
+ diff_lines = _normalize_diff_lines(diff)
60
+ if mode == "create":
61
+ return _parse_create_diff(diff_lines)
62
+
63
+ parsed = _parse_update_diff(diff_lines, input)
64
+ return _apply_chunks(input, parsed.chunks)
65
+
66
+
67
+ def _normalize_diff_lines(diff: str) -> list[str]:
68
+ lines = [line.rstrip("\r") for line in re.split(r"\r?\n", diff)]
69
+ if lines and lines[-1] == "":
70
+ lines.pop()
71
+ return lines
72
+
73
+
74
+ def _is_done(state: ParserState, prefixes: Sequence[str]) -> bool:
75
+ if state.index >= len(state.lines):
76
+ return True
77
+ if any(state.lines[state.index].startswith(prefix) for prefix in prefixes):
78
+ return True
79
+ return False
80
+
81
+
82
+ def _read_str(state: ParserState, prefix: str) -> str:
83
+ if state.index >= len(state.lines):
84
+ return ""
85
+ current = state.lines[state.index]
86
+ if current.startswith(prefix):
87
+ state.index += 1
88
+ return current[len(prefix) :]
89
+ return ""
90
+
91
+
92
+ def _parse_create_diff(lines: list[str]) -> str:
93
+ parser = ParserState(lines=[*lines, END_PATCH])
94
+ output: list[str] = []
95
+
96
+ while not _is_done(parser, SECTION_TERMINATORS):
97
+ if parser.index >= len(parser.lines):
98
+ break
99
+ line = parser.lines[parser.index]
100
+ parser.index += 1
101
+ if not line.startswith("+"):
102
+ raise ValueError(f"Invalid Add File Line: {line}")
103
+ output.append(line[1:])
104
+
105
+ return "\n".join(output)
106
+
107
+
108
+ def _parse_update_diff(lines: list[str], input: str) -> ParsedUpdateDiff:
109
+ parser = ParserState(lines=[*lines, END_PATCH])
110
+ input_lines = input.split("\n")
111
+ chunks: list[Chunk] = []
112
+ cursor = 0
113
+
114
+ while not _is_done(parser, END_SECTION_MARKERS):
115
+ anchor = _read_str(parser, "@@ ")
116
+ has_bare_anchor = (
117
+ anchor == "" and parser.index < len(parser.lines) and parser.lines[parser.index] == "@@"
118
+ )
119
+ if has_bare_anchor:
120
+ parser.index += 1
121
+
122
+ if not (anchor or has_bare_anchor or cursor == 0):
123
+ current_line = parser.lines[parser.index] if parser.index < len(parser.lines) else ""
124
+ raise ValueError(f"Invalid Line:\n{current_line}")
125
+
126
+ if anchor.strip():
127
+ cursor = _advance_cursor_to_anchor(anchor, input_lines, cursor, parser)
128
+
129
+ section = _read_section(parser.lines, parser.index)
130
+ find_result = _find_context(input_lines, section.next_context, cursor, section.eof)
131
+ if find_result.new_index == -1:
132
+ ctx_text = "\n".join(section.next_context)
133
+ if section.eof:
134
+ raise ValueError(f"Invalid EOF Context {cursor}:\n{ctx_text}")
135
+ raise ValueError(f"Invalid Context {cursor}:\n{ctx_text}")
136
+
137
+ cursor = find_result.new_index + len(section.next_context)
138
+ parser.fuzz += find_result.fuzz
139
+ parser.index = section.end_index
140
+
141
+ for ch in section.section_chunks:
142
+ chunks.append(
143
+ Chunk(
144
+ orig_index=ch.orig_index + find_result.new_index,
145
+ del_lines=list(ch.del_lines),
146
+ ins_lines=list(ch.ins_lines),
147
+ )
148
+ )
149
+
150
+ return ParsedUpdateDiff(chunks=chunks, fuzz=parser.fuzz)
151
+
152
+
153
+ def _advance_cursor_to_anchor(
154
+ anchor: str,
155
+ input_lines: list[str],
156
+ cursor: int,
157
+ parser: ParserState,
158
+ ) -> int:
159
+ found = False
160
+
161
+ if not any(line == anchor for line in input_lines[:cursor]):
162
+ for i in range(cursor, len(input_lines)):
163
+ if input_lines[i] == anchor:
164
+ cursor = i + 1
165
+ found = True
166
+ break
167
+
168
+ if not found and not any(line.strip() == anchor.strip() for line in input_lines[:cursor]):
169
+ for i in range(cursor, len(input_lines)):
170
+ if input_lines[i].strip() == anchor.strip():
171
+ cursor = i + 1
172
+ parser.fuzz += 1
173
+ found = True
174
+ break
175
+
176
+ return cursor
177
+
178
+
179
+ def _read_section(lines: list[str], start_index: int) -> ReadSectionResult:
180
+ context: list[str] = []
181
+ del_lines: list[str] = []
182
+ ins_lines: list[str] = []
183
+ section_chunks: list[Chunk] = []
184
+ mode: Literal["keep", "add", "delete"] = "keep"
185
+ index = start_index
186
+ orig_index = index
187
+
188
+ while index < len(lines):
189
+ raw = lines[index]
190
+ if (
191
+ raw.startswith("@@")
192
+ or raw.startswith(END_PATCH)
193
+ or raw.startswith("*** Update File:")
194
+ or raw.startswith("*** Delete File:")
195
+ or raw.startswith("*** Add File:")
196
+ or raw.startswith(END_FILE)
197
+ ):
198
+ break
199
+ if raw == "***":
200
+ break
201
+ if raw.startswith("***"):
202
+ raise ValueError(f"Invalid Line: {raw}")
203
+
204
+ index += 1
205
+ last_mode = mode
206
+ line = raw if raw else " "
207
+ prefix = line[0]
208
+ if prefix == "+":
209
+ mode = "add"
210
+ elif prefix == "-":
211
+ mode = "delete"
212
+ elif prefix == " ":
213
+ mode = "keep"
214
+ else:
215
+ raise ValueError(f"Invalid Line: {line}")
216
+
217
+ line_content = line[1:]
218
+ switching_to_context = mode == "keep" and last_mode != mode
219
+ if switching_to_context and (del_lines or ins_lines):
220
+ section_chunks.append(
221
+ Chunk(
222
+ orig_index=len(context) - len(del_lines),
223
+ del_lines=list(del_lines),
224
+ ins_lines=list(ins_lines),
225
+ )
226
+ )
227
+ del_lines = []
228
+ ins_lines = []
229
+
230
+ if mode == "delete":
231
+ del_lines.append(line_content)
232
+ context.append(line_content)
233
+ elif mode == "add":
234
+ ins_lines.append(line_content)
235
+ else:
236
+ context.append(line_content)
237
+
238
+ if del_lines or ins_lines:
239
+ section_chunks.append(
240
+ Chunk(
241
+ orig_index=len(context) - len(del_lines),
242
+ del_lines=list(del_lines),
243
+ ins_lines=list(ins_lines),
244
+ )
245
+ )
246
+
247
+ if index < len(lines) and lines[index] == END_FILE:
248
+ return ReadSectionResult(context, section_chunks, index + 1, True)
249
+
250
+ if index == orig_index:
251
+ next_line = lines[index] if index < len(lines) else ""
252
+ raise ValueError(f"Nothing in this section - index={index} {next_line}")
253
+
254
+ return ReadSectionResult(context, section_chunks, index, False)
255
+
256
+
257
+ @dataclass
258
+ class ContextMatch:
259
+ new_index: int
260
+ fuzz: int
261
+
262
+
263
+ def _find_context(lines: list[str], context: list[str], start: int, eof: bool) -> ContextMatch:
264
+ if eof:
265
+ end_start = max(0, len(lines) - len(context))
266
+ end_match = _find_context_core(lines, context, end_start)
267
+ if end_match.new_index != -1:
268
+ return end_match
269
+ fallback = _find_context_core(lines, context, start)
270
+ return ContextMatch(new_index=fallback.new_index, fuzz=fallback.fuzz + 10000)
271
+ return _find_context_core(lines, context, start)
272
+
273
+
274
+ def _find_context_core(lines: list[str], context: list[str], start: int) -> ContextMatch:
275
+ if not context:
276
+ return ContextMatch(new_index=start, fuzz=0)
277
+
278
+ for i in range(start, len(lines)):
279
+ if _equals_slice(lines, context, i, lambda value: value):
280
+ return ContextMatch(new_index=i, fuzz=0)
281
+ for i in range(start, len(lines)):
282
+ if _equals_slice(lines, context, i, lambda value: value.rstrip()):
283
+ return ContextMatch(new_index=i, fuzz=1)
284
+ for i in range(start, len(lines)):
285
+ if _equals_slice(lines, context, i, lambda value: value.strip()):
286
+ return ContextMatch(new_index=i, fuzz=100)
287
+
288
+ return ContextMatch(new_index=-1, fuzz=0)
289
+
290
+
291
+ def _equals_slice(
292
+ source: list[str], target: list[str], start: int, map_fn: Callable[[str], str]
293
+ ) -> bool:
294
+ if start + len(target) > len(source):
295
+ return False
296
+ for offset, target_value in enumerate(target):
297
+ if map_fn(source[start + offset]) != map_fn(target_value):
298
+ return False
299
+ return True
300
+
301
+
302
+ def _apply_chunks(input: str, chunks: list[Chunk]) -> str:
303
+ orig_lines = input.split("\n")
304
+ dest_lines: list[str] = []
305
+ cursor = 0
306
+
307
+ for chunk in chunks:
308
+ if chunk.orig_index > len(orig_lines):
309
+ raise ValueError(
310
+ f"applyDiff: chunk.origIndex {chunk.orig_index} > input length {len(orig_lines)}"
311
+ )
312
+ if cursor > chunk.orig_index:
313
+ raise ValueError(
314
+ f"applyDiff: overlapping chunk at {chunk.orig_index} (cursor {cursor})"
315
+ )
316
+
317
+ dest_lines.extend(orig_lines[cursor : chunk.orig_index])
318
+ cursor = chunk.orig_index
319
+
320
+ if chunk.ins_lines:
321
+ dest_lines.extend(chunk.ins_lines)
322
+
323
+ cursor += len(chunk.del_lines)
324
+
325
+ dest_lines.extend(orig_lines[cursor:])
326
+ return "\n".join(dest_lines)
327
+
328
+
329
+ __all__ = ["apply_diff"]
agents/editor.py ADDED
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from dataclasses import dataclass
5
+ from typing import Literal, Protocol, runtime_checkable
6
+
7
+ from .run_context import RunContextWrapper
8
+ from .util._types import MaybeAwaitable
9
+
10
+ ApplyPatchOperationType = Literal["create_file", "update_file", "delete_file"]
11
+
12
+ _DATACLASS_KWARGS = {"slots": True} if sys.version_info >= (3, 10) else {}
13
+
14
+
15
+ @dataclass(**_DATACLASS_KWARGS)
16
+ class ApplyPatchOperation:
17
+ """Represents a single apply_patch editor operation requested by the model."""
18
+
19
+ type: ApplyPatchOperationType
20
+ path: str
21
+ diff: str | None = None
22
+ ctx_wrapper: RunContextWrapper | None = None
23
+
24
+
25
+ @dataclass(**_DATACLASS_KWARGS)
26
+ class ApplyPatchResult:
27
+ """Optional metadata returned by editor operations."""
28
+
29
+ status: Literal["completed", "failed"] | None = None
30
+ output: str | None = None
31
+
32
+
33
+ @runtime_checkable
34
+ class ApplyPatchEditor(Protocol):
35
+ """Host-defined editor that applies diffs on disk."""
36
+
37
+ def create_file(
38
+ self, operation: ApplyPatchOperation
39
+ ) -> MaybeAwaitable[ApplyPatchResult | str | None]: ...
40
+
41
+ def update_file(
42
+ self, operation: ApplyPatchOperation
43
+ ) -> MaybeAwaitable[ApplyPatchResult | str | None]: ...
44
+
45
+ def delete_file(
46
+ self, operation: ApplyPatchOperation
47
+ ) -> MaybeAwaitable[ApplyPatchResult | str | None]: ...
agents/exceptions.py CHANGED
@@ -8,6 +8,11 @@ if TYPE_CHECKING:
8
8
  from .guardrail import InputGuardrailResult, OutputGuardrailResult
9
9
  from .items import ModelResponse, RunItem, TResponseInputItem
10
10
  from .run_context import RunContextWrapper
11
+ from .tool_guardrails import (
12
+ ToolGuardrailFunctionOutput,
13
+ ToolInputGuardrail,
14
+ ToolOutputGuardrail,
15
+ )
11
16
 
12
17
  from .util._pretty_print import pretty_print_run_error_details
13
18
 
@@ -94,3 +99,33 @@ class OutputGuardrailTripwireTriggered(AgentsException):
94
99
  super().__init__(
95
100
  f"Guardrail {guardrail_result.guardrail.__class__.__name__} triggered tripwire"
96
101
  )
102
+
103
+
104
+ class ToolInputGuardrailTripwireTriggered(AgentsException):
105
+ """Exception raised when a tool input guardrail tripwire is triggered."""
106
+
107
+ guardrail: ToolInputGuardrail[Any]
108
+ """The guardrail that was triggered."""
109
+
110
+ output: ToolGuardrailFunctionOutput
111
+ """The output from the guardrail function."""
112
+
113
+ def __init__(self, guardrail: ToolInputGuardrail[Any], output: ToolGuardrailFunctionOutput):
114
+ self.guardrail = guardrail
115
+ self.output = output
116
+ super().__init__(f"Tool input guardrail {guardrail.__class__.__name__} triggered tripwire")
117
+
118
+
119
+ class ToolOutputGuardrailTripwireTriggered(AgentsException):
120
+ """Exception raised when a tool output guardrail tripwire is triggered."""
121
+
122
+ guardrail: ToolOutputGuardrail[Any]
123
+ """The guardrail that was triggered."""
124
+
125
+ output: ToolGuardrailFunctionOutput
126
+ """The output from the guardrail function."""
127
+
128
+ def __init__(self, guardrail: ToolOutputGuardrail[Any], output: ToolGuardrailFunctionOutput):
129
+ self.guardrail = guardrail
130
+ self.output = output
131
+ super().__init__(f"Tool output guardrail {guardrail.__class__.__name__} triggered tripwire")
@@ -0,0 +1,6 @@
1
+ # This package contains experimental extensions to the agents package.
2
+ # The interface and implementation details could be changed until being GAed.
3
+
4
+ __all__ = [
5
+ "codex",
6
+ ]