openai-agents 0.2.8__py3-none-any.whl → 0.6.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agents/__init__.py +105 -4
- agents/_debug.py +15 -4
- agents/_run_impl.py +1203 -96
- agents/agent.py +164 -19
- agents/apply_diff.py +329 -0
- agents/editor.py +47 -0
- agents/exceptions.py +35 -0
- agents/extensions/experimental/__init__.py +6 -0
- agents/extensions/experimental/codex/__init__.py +92 -0
- agents/extensions/experimental/codex/codex.py +89 -0
- agents/extensions/experimental/codex/codex_options.py +35 -0
- agents/extensions/experimental/codex/codex_tool.py +1142 -0
- agents/extensions/experimental/codex/events.py +162 -0
- agents/extensions/experimental/codex/exec.py +263 -0
- agents/extensions/experimental/codex/items.py +245 -0
- agents/extensions/experimental/codex/output_schema_file.py +50 -0
- agents/extensions/experimental/codex/payloads.py +31 -0
- agents/extensions/experimental/codex/thread.py +214 -0
- agents/extensions/experimental/codex/thread_options.py +54 -0
- agents/extensions/experimental/codex/turn_options.py +36 -0
- agents/extensions/handoff_filters.py +13 -1
- agents/extensions/memory/__init__.py +120 -0
- agents/extensions/memory/advanced_sqlite_session.py +1285 -0
- agents/extensions/memory/async_sqlite_session.py +239 -0
- agents/extensions/memory/dapr_session.py +423 -0
- agents/extensions/memory/encrypt_session.py +185 -0
- agents/extensions/memory/redis_session.py +261 -0
- agents/extensions/memory/sqlalchemy_session.py +334 -0
- agents/extensions/models/litellm_model.py +449 -36
- agents/extensions/models/litellm_provider.py +3 -1
- agents/function_schema.py +47 -5
- agents/guardrail.py +16 -2
- agents/{handoffs.py → handoffs/__init__.py} +89 -47
- agents/handoffs/history.py +268 -0
- agents/items.py +237 -11
- agents/lifecycle.py +75 -14
- agents/mcp/server.py +280 -37
- agents/mcp/util.py +24 -3
- agents/memory/__init__.py +22 -2
- agents/memory/openai_conversations_session.py +91 -0
- agents/memory/openai_responses_compaction_session.py +249 -0
- agents/memory/session.py +19 -261
- agents/memory/sqlite_session.py +275 -0
- agents/memory/util.py +20 -0
- agents/model_settings.py +14 -3
- agents/models/__init__.py +13 -0
- agents/models/chatcmpl_converter.py +303 -50
- agents/models/chatcmpl_helpers.py +63 -0
- agents/models/chatcmpl_stream_handler.py +290 -68
- agents/models/default_models.py +58 -0
- agents/models/interface.py +4 -0
- agents/models/openai_chatcompletions.py +103 -49
- agents/models/openai_provider.py +10 -4
- agents/models/openai_responses.py +162 -46
- agents/realtime/__init__.py +4 -0
- agents/realtime/_util.py +14 -3
- agents/realtime/agent.py +7 -0
- agents/realtime/audio_formats.py +53 -0
- agents/realtime/config.py +78 -10
- agents/realtime/events.py +18 -0
- agents/realtime/handoffs.py +2 -2
- agents/realtime/items.py +17 -1
- agents/realtime/model.py +13 -0
- agents/realtime/model_events.py +12 -0
- agents/realtime/model_inputs.py +18 -1
- agents/realtime/openai_realtime.py +696 -150
- agents/realtime/session.py +243 -23
- agents/repl.py +7 -3
- agents/result.py +197 -38
- agents/run.py +949 -168
- agents/run_context.py +13 -2
- agents/stream_events.py +1 -0
- agents/strict_schema.py +14 -0
- agents/tool.py +413 -15
- agents/tool_context.py +22 -1
- agents/tool_guardrails.py +279 -0
- agents/tracing/__init__.py +2 -0
- agents/tracing/config.py +9 -0
- agents/tracing/create.py +4 -0
- agents/tracing/processor_interface.py +84 -11
- agents/tracing/processors.py +65 -54
- agents/tracing/provider.py +64 -7
- agents/tracing/spans.py +105 -0
- agents/tracing/traces.py +116 -16
- agents/usage.py +134 -12
- agents/util/_json.py +19 -1
- agents/util/_transforms.py +12 -2
- agents/voice/input.py +5 -4
- agents/voice/models/openai_stt.py +17 -9
- agents/voice/pipeline.py +2 -0
- agents/voice/pipeline_config.py +4 -0
- {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/METADATA +44 -19
- openai_agents-0.6.8.dist-info/RECORD +134 -0
- {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/WHEEL +1 -1
- openai_agents-0.2.8.dist-info/RECORD +0 -103
- {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/licenses/LICENSE +0 -0
agents/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
|
|
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 .
|
|
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 .
|
|
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
|
-
`
|
|
201
|
+
`agents.models.get_default_model()` (currently "gpt-4.1").
|
|
172
202
|
"""
|
|
173
203
|
|
|
174
|
-
model_settings: ModelSettings = field(default_factory=
|
|
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
|
|
209
|
-
|
|
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:
|
|
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:
|
|
381
|
-
from .run import Runner
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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(
|
|
534
|
+
return await custom_output_extractor(run_result)
|
|
390
535
|
|
|
391
|
-
return
|
|
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")
|