Skip to content

Commit 210585e

Browse files
authored
fix: preserve positional-argument compatibility for public runtime constructors (#2413)
1 parent 1fee0d9 commit 210585e

5 files changed

Lines changed: 94 additions & 13 deletions

File tree

AGENTS.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ Call out potential backward compatibility or public API risks early in your plan
3636

3737
Use an ExecPlan when work is multi-step, spans several files, involves new features or refactors, or is likely to take more than about an hour. Start with the template and rules in `PLANS.md`, keep milestones and living sections (Progress, Surprises & Discoveries, Decision Log, Outcomes & Retrospective) up to date as you execute, and rewrite the plan if scope shifts. If you intentionally skip an ExecPlan for a complex task, note why in your response so reviewers understand the choice.
3838

39+
### Public API Positional Compatibility
40+
41+
Treat the parameter and dataclass field order of exported runtime APIs as a compatibility contract.
42+
43+
- For public constructors (for example `RunConfig`, `FunctionTool`, `AgentHookContext`), preserve existing positional argument meaning. Do not insert new constructor parameters or dataclass fields in the middle of existing public order.
44+
- When adding a new optional public field/parameter, append it to the end whenever possible and keep old fields in the same order.
45+
- If reordering is unavoidable, add an explicit compatibility layer and regression tests that exercise the old positional call pattern.
46+
- Prefer keyword arguments at call sites to reduce accidental breakage, but do not rely on this to justify breaking positional compatibility for public APIs.
47+
3948
## Project Structure Guide
4049

4150
### Overview

src/agents/run_config.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,6 @@ class RunConfig:
9696
settings.
9797
"""
9898

99-
session_settings: SessionSettings | None = None
100-
"""Configure session settings. Any non-null values will override the session's default
101-
settings. Used to control session behavior like the number of items to retrieve.
102-
"""
103-
10499
handoff_input_filter: HandoffInputFilter | None = None
105100
"""A global input filter to apply to all handoffs. If `Handoff.input_filter` is set, then that
106101
will take precedence. The input filter allows you to edit the inputs that are sent to the new
@@ -183,6 +178,11 @@ class RunConfig:
183178
Returning ``None`` falls back to the SDK default message.
184179
"""
185180

181+
session_settings: SessionSettings | None = None
182+
"""Configure session settings. Any non-null values will override the session's default
183+
settings. Used to control session behavior like the number of items to retrieve.
184+
"""
185+
186186

187187
class RunOptions(TypedDict, Generic[TContext]):
188188
"""Arguments for ``AgentRunner`` methods."""

src/agents/run_context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ class RunContextWrapper(Generic[TContext]):
4545
last chunk of the stream is processed.
4646
"""
4747

48-
_approvals: dict[str, _ApprovalRecord] = field(default_factory=dict)
4948
turn_input: list[TResponseInputItem] = field(default_factory=list)
49+
_approvals: dict[str, _ApprovalRecord] = field(default_factory=dict)
5050
tool_input: Any | None = None
5151
"""Structured input for the current agent tool run, when available."""
5252

src/agents/tool.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,15 @@ class FunctionTool:
240240
and returns whether the tool is enabled. You can use this to dynamically enable/disable a tool
241241
based on your context/state."""
242242

243+
# Keep guardrail fields before needs_approval to preserve v0.7.0 positional
244+
# constructor compatibility for public FunctionTool callers.
245+
# Tool-specific guardrails.
246+
tool_input_guardrails: list[ToolInputGuardrail[Any]] | None = None
247+
"""Optional list of input guardrails to run before invoking this tool."""
248+
249+
tool_output_guardrails: list[ToolOutputGuardrail[Any]] | None = None
250+
"""Optional list of output guardrails to run after invoking this tool."""
251+
243252
needs_approval: (
244253
bool | Callable[[RunContextWrapper[Any], dict[str, Any], str], Awaitable[bool]]
245254
) = False
@@ -249,13 +258,6 @@ class FunctionTool:
249258
function that takes (run_context, tool_parameters, call_id) and returns whether this
250259
specific call needs approval."""
251260

252-
# Tool-specific guardrails
253-
tool_input_guardrails: list[ToolInputGuardrail[Any]] | None = None
254-
"""Optional list of input guardrails to run before invoking this tool."""
255-
256-
tool_output_guardrails: list[ToolOutputGuardrail[Any]] | None = None
257-
"""Optional list of output guardrails to run after invoking this tool."""
258-
259261
_is_agent_tool: bool = field(default=False, init=False, repr=False)
260262
"""Internal flag indicating if this tool is an agent-as-tool."""
261263

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
from agents import (
6+
AgentHookContext,
7+
FunctionTool,
8+
HandoffInputData,
9+
ItemHelpers,
10+
MultiProvider,
11+
RunConfig,
12+
ToolGuardrailFunctionOutput,
13+
ToolInputGuardrailData,
14+
ToolOutputGuardrailData,
15+
Usage,
16+
tool_input_guardrail,
17+
tool_output_guardrail,
18+
)
19+
from agents.tool_context import ToolContext
20+
21+
22+
def test_run_config_positional_arguments_remain_backward_compatible() -> None:
23+
async def keep_handoff_input(data: HandoffInputData) -> HandoffInputData:
24+
return data
25+
26+
config = RunConfig(None, MultiProvider(), None, keep_handoff_input)
27+
28+
assert config.handoff_input_filter is keep_handoff_input
29+
assert config.session_settings is None
30+
31+
32+
def test_function_tool_positional_arguments_keep_guardrail_positions() -> None:
33+
async def invoke(_ctx: ToolContext[Any], _args: str) -> str:
34+
return "ok"
35+
36+
@tool_input_guardrail
37+
def allow_input(_data: ToolInputGuardrailData) -> ToolGuardrailFunctionOutput:
38+
return ToolGuardrailFunctionOutput.allow()
39+
40+
@tool_output_guardrail
41+
def allow_output(_data: ToolOutputGuardrailData) -> ToolGuardrailFunctionOutput:
42+
return ToolGuardrailFunctionOutput.allow()
43+
44+
input_guardrails = [allow_input]
45+
output_guardrails = [allow_output]
46+
47+
tool = FunctionTool(
48+
"tool_name",
49+
"tool_description",
50+
{"type": "object", "properties": {}},
51+
invoke,
52+
True,
53+
True,
54+
input_guardrails,
55+
output_guardrails,
56+
)
57+
58+
assert tool.needs_approval is False
59+
assert tool.tool_input_guardrails is not None
60+
assert tool.tool_output_guardrails is not None
61+
assert tool.tool_input_guardrails[0] is allow_input
62+
assert tool.tool_output_guardrails[0] is allow_output
63+
64+
65+
def test_agent_hook_context_third_positional_argument_is_turn_input() -> None:
66+
turn_input = ItemHelpers.input_to_new_input_list("hello")
67+
context = AgentHookContext(None, Usage(), turn_input)
68+
69+
assert context.turn_input == turn_input
70+
assert isinstance(context._approvals, dict)

0 commit comments

Comments
 (0)