fix: preserve positional-argument compatibility for public runtime constructors (#2413)

This commit is contained in:
Kazuhiro Sera
2026-02-04 21:02:00 +09:00
committed by GitHub
parent 1fee0d9660
commit 210585e3c3
5 changed files with 94 additions and 13 deletions
+9
View File
@@ -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
+5 -5
View File
@@ -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."""
+1 -1
View File
@@ -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."""
+9 -7
View File
@@ -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."""
+70
View File
@@ -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)