fix: preserve positional-argument compatibility for public runtime constructors (#2413)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
@@ -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."""
|
||||
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user