From 210585e3c3ec4cdbbfb6521e7cd2e2d7b070cbfe Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Wed, 4 Feb 2026 21:02:00 +0900 Subject: [PATCH] fix: preserve positional-argument compatibility for public runtime constructors (#2413) --- AGENTS.md | 9 +++ src/agents/run_config.py | 10 ++-- src/agents/run_context.py | 2 +- src/agents/tool.py | 16 +++--- tests/test_source_compat_constructors.py | 70 ++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 13 deletions(-) create mode 100644 tests/test_source_compat_constructors.py diff --git a/AGENTS.md b/AGENTS.md index ecdd6284..6228ef85 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,6 +36,15 @@ Call out potential backward compatibility or public API risks early in your plan 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. +### Public API Positional Compatibility + +Treat the parameter and dataclass field order of exported runtime APIs as a compatibility contract. + +- 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. +- When adding a new optional public field/parameter, append it to the end whenever possible and keep old fields in the same order. +- If reordering is unavoidable, add an explicit compatibility layer and regression tests that exercise the old positional call pattern. +- Prefer keyword arguments at call sites to reduce accidental breakage, but do not rely on this to justify breaking positional compatibility for public APIs. + ## Project Structure Guide ### Overview diff --git a/src/agents/run_config.py b/src/agents/run_config.py index 77343e8f..d1829058 100644 --- a/src/agents/run_config.py +++ b/src/agents/run_config.py @@ -96,11 +96,6 @@ class RunConfig: settings. """ - session_settings: SessionSettings | None = None - """Configure session settings. Any non-null values will override the session's default - settings. Used to control session behavior like the number of items to retrieve. - """ - handoff_input_filter: HandoffInputFilter | None = None """A global input filter to apply to all handoffs. If `Handoff.input_filter` is set, then that will take precedence. The input filter allows you to edit the inputs that are sent to the new @@ -183,6 +178,11 @@ class RunConfig: Returning ``None`` falls back to the SDK default message. """ + session_settings: SessionSettings | None = None + """Configure session settings. Any non-null values will override the session's default + settings. Used to control session behavior like the number of items to retrieve. + """ + class RunOptions(TypedDict, Generic[TContext]): """Arguments for ``AgentRunner`` methods.""" diff --git a/src/agents/run_context.py b/src/agents/run_context.py index 529d1ce9..d3ef96e5 100644 --- a/src/agents/run_context.py +++ b/src/agents/run_context.py @@ -45,8 +45,8 @@ class RunContextWrapper(Generic[TContext]): last chunk of the stream is processed. """ - _approvals: dict[str, _ApprovalRecord] = field(default_factory=dict) turn_input: list[TResponseInputItem] = field(default_factory=list) + _approvals: dict[str, _ApprovalRecord] = field(default_factory=dict) tool_input: Any | None = None """Structured input for the current agent tool run, when available.""" diff --git a/src/agents/tool.py b/src/agents/tool.py index 07e33303..4f70adc0 100644 --- a/src/agents/tool.py +++ b/src/agents/tool.py @@ -240,6 +240,15 @@ class FunctionTool: and returns whether the tool is enabled. You can use this to dynamically enable/disable a tool based on your context/state.""" + # Keep guardrail fields before needs_approval to preserve v0.7.0 positional + # constructor compatibility for public FunctionTool callers. + # Tool-specific guardrails. + tool_input_guardrails: list[ToolInputGuardrail[Any]] | None = None + """Optional list of input guardrails to run before invoking this tool.""" + + tool_output_guardrails: list[ToolOutputGuardrail[Any]] | None = None + """Optional list of output guardrails to run after invoking this tool.""" + needs_approval: ( bool | Callable[[RunContextWrapper[Any], dict[str, Any], str], Awaitable[bool]] ) = False @@ -249,13 +258,6 @@ class FunctionTool: function that takes (run_context, tool_parameters, call_id) and returns whether this specific call needs approval.""" - # Tool-specific guardrails - tool_input_guardrails: list[ToolInputGuardrail[Any]] | None = None - """Optional list of input guardrails to run before invoking this tool.""" - - tool_output_guardrails: list[ToolOutputGuardrail[Any]] | None = None - """Optional list of output guardrails to run after invoking this tool.""" - _is_agent_tool: bool = field(default=False, init=False, repr=False) """Internal flag indicating if this tool is an agent-as-tool.""" diff --git a/tests/test_source_compat_constructors.py b/tests/test_source_compat_constructors.py new file mode 100644 index 00000000..fe8f3ccd --- /dev/null +++ b/tests/test_source_compat_constructors.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from typing import Any + +from agents import ( + AgentHookContext, + FunctionTool, + HandoffInputData, + ItemHelpers, + MultiProvider, + RunConfig, + ToolGuardrailFunctionOutput, + ToolInputGuardrailData, + ToolOutputGuardrailData, + Usage, + tool_input_guardrail, + tool_output_guardrail, +) +from agents.tool_context import ToolContext + + +def test_run_config_positional_arguments_remain_backward_compatible() -> None: + async def keep_handoff_input(data: HandoffInputData) -> HandoffInputData: + return data + + config = RunConfig(None, MultiProvider(), None, keep_handoff_input) + + assert config.handoff_input_filter is keep_handoff_input + assert config.session_settings is None + + +def test_function_tool_positional_arguments_keep_guardrail_positions() -> None: + async def invoke(_ctx: ToolContext[Any], _args: str) -> str: + return "ok" + + @tool_input_guardrail + def allow_input(_data: ToolInputGuardrailData) -> ToolGuardrailFunctionOutput: + return ToolGuardrailFunctionOutput.allow() + + @tool_output_guardrail + def allow_output(_data: ToolOutputGuardrailData) -> ToolGuardrailFunctionOutput: + return ToolGuardrailFunctionOutput.allow() + + input_guardrails = [allow_input] + output_guardrails = [allow_output] + + tool = FunctionTool( + "tool_name", + "tool_description", + {"type": "object", "properties": {}}, + invoke, + True, + True, + input_guardrails, + output_guardrails, + ) + + assert tool.needs_approval is False + assert tool.tool_input_guardrails is not None + assert tool.tool_output_guardrails is not None + assert tool.tool_input_guardrails[0] is allow_input + assert tool.tool_output_guardrails[0] is allow_output + + +def test_agent_hook_context_third_positional_argument_is_turn_input() -> None: + turn_input = ItemHelpers.input_to_new_input_list("hello") + context = AgentHookContext(None, Usage(), turn_input) + + assert context.turn_input == turn_input + assert isinstance(context._approvals, dict)