Skip to content

Tool parameter named agent is silently dropped from inputSchema (reserved special parameter, no warning) #3499

Description

@agent-of-mkmeral

Summary

A @tool parameter named agent is silently excluded from the generated inputSchema and never populated at call time. No error, no warning — the parameter just doesn't exist as far as the model is concerned, and the function receives its default.

FunctionToolMetadata._is_special_parameter reserves {"self", "cls", "agent"} (plus the configured context param). self/cls are unambiguous and the context param is opt-in via @tool(context=...), but agent is a perfectly ordinary word for a tool parameter — agent, as in "which agent", "agent id", "agent name" — and reserving it invisibly is a footgun.

Reproduction

import asyncio, inspect
from typing import Literal
from strands.tools.decorator import tool

@tool(name="delegate")
async def delegate(task: str, agent: Literal["reviewer", "explorer"] = "reviewer") -> dict:
    """Delegate a task.

    Args:
        task: The task.
        agent: Which agent to use.
    """
    return {"task": task, "agent": agent}

print(delegate.tool_spec["inputSchema"]["json"]["properties"].keys())
# -> dict_keys(['task'])          # `agent` is gone, silently

async def call(inp):
    out = None
    async for ev in delegate.stream({"toolUseId": "1", "name": "delegate", "input": inp}, {}):
        out = ev
    return out.tool_result

print(asyncio.run(call({"task": "x", "agent": "explorer"})))
# -> content: {"task": "x", "agent": "reviewer"}   # model's value discarded

Renaming the parameter to role makes both the schema entry and the pydantic Literal enforcement appear as expected, which is how I eventually found it.

Impact

The failure is invisible in both directions: the tool author sees a working tool with a missing parameter, and the model sees a schema that doesn't mention the option, so it can never pass it. It surfaced while prototyping a config-derived schema for a delegation tool (harness-sdk#3392 design pass) where agent was the natural name for the "which child identity" selector — the parameter simply evaporated and the tool appeared to ignore the model.

Suggested fix

One of, in preference order:

  1. Warn loudly. In FunctionToolMetadata.__init__, if a non-injected parameter collides with a reserved name, emit a logger.warning (or raise, since it's a construction-time programming error) naming the parameter and the reason. Same treatment ToolContext already gets in _validate_signature, which does raise on misuse.
  2. Make agent injection opt-in the way the context param is (@tool(context=...)), so the reservation only applies when the author asks for it. This is the real fix; self/cls can stay unconditional.
  3. At minimum, document the reserved names in the @tool docstring — it currently documents the context parameter but never mentions that agent is taken.

Happy to send a PR for (1) + (3), and for (2) if you agree it's the right direction.

Activity

  1. added
    pythonPull requests that update python code
    bugSomething isn't working
    on Jul 27, 2026
  2. ebarkhordar commented on Jul 27, 2026

    @ebarkhordar
    Contributor

    The schema omission reproduces at HEAD 58a501c7, but the call-time half reads differently under a real Agent, and the difference changes which fix is right.

    agent is not left at its default. _execute_event_loop_cycle sets invocation_state["agent"] = self unconditionally (strands-py/src/strands/agent/agent.py:1352, commented "Add Agent to invocation_state to keep backwards-compatibility"), and inject_special_parameters copies that into the validated input (decorator.py:416-417). The tool body receives the Agent object. Your repro drives delegate.stream(tool_use, {}) with an empty invocation_state, which is the one context where the declared default survives.

    Injection also lands after validation. validate_input (decorator.py:383) builds from input_model, which _create_input_model assembled without the agent field (decorator.py:205), so the Literal["reviewer", "explorer"] annotation is never applied to the injected value.

    Verified at HEAD 58a501c7, clean python:3.12-slim container, pip install . from the repo copy (installed decorator.py sha256 e9506354...66b122, byte-identical to HEAD; pip show strands-agents reports the pretend version 0.0.0, so it is the local build and not PyPI). Your exact tool, invoked once through your harness and once through Agent with the repo's own tests/fixtures/mocked_model_provider.py emitting the toolUse:

    inputSchema properties: ['task']
    
    B. delegate.stream(tool_use, {})           -> tool received {'task': 'x', 'agent': 'reviewer'}
    C. Agent(model=mocked, tools=[delegate])   -> tool received task='x' agent=<Agent>
         is it the Agent instance?                True
         did it equal the model's value 'explorer'?   False
         did it equal the declared default 'reviewer'? False
    

    So a tool that branches on agent == "explorer" silently takes the other branch, and one that calls a string method on it raises AttributeError from inside the tool body rather than at construction. That is a worse failure than falling back to the default, and it argues for your option 1 raising rather than warning: once a parameter named agent carries a non-Agent annotation, there is no execution in which both the annotation and the injection can be honoured.

    On your option 3, the reserved set is exactly {"self", "cls", "agent"} plus the opt-in context param (decorator.py:433-437), and the public tool() docstring documents context but never mentions agent.

  3. Solaris-star commented on Aug 2, 2026

    @Solaris-star

    This is a real footgun — agent is a perfectly normal business parameter name.

    Looking at FunctionToolMetadata._is_special_parameter, the fix options I see:

    1. Remove agent from the hardcoded reserved set. The agent injection is already opt-in via @tool(context=...) — if the user explicitly names their context param agent, that's their choice. self/cls are the only truly unambiguous reserved names.

    2. Warn instead of silently dropping. If a parameter name collides with the reserved set, emit a warning at tool registration time so the author knows their parameter is being excluded.

    I'd lean toward (1) + (2): remove agent from the hardcoded set, and add a warning for any future collision with the configured context param name. That way self/cls stay reserved (nobody names a tool param self), the context param collision is still caught, but agent as a normal param just works.

    Happy to implement once there's agreement on direction.

  4. Solaris-star commented on Aug 2, 2026

    @Solaris-star

    Thanks @ebarkhordar for the deeper analysis — the runtime injection is worse than I initially understood. The tool receiving an Agent object where it expects a Literal["reviewer", "explorer"] is a silent type corruption, not just a missing parameter.

    Revised position: option 1 should raise at registration time when a parameter named agent carries a non-Agent annotation, rather than silently reserving or warning. The collision is irreconcilable — there's no execution where both the annotation and the injection can be honoured, as you pointed out.

    Concretely:

    • self/cls: stay silently reserved (nobody names a tool param self)
    • agent: if the annotation is Agent or untyped, allow the injection as-is. If the annotation is anything else (str, Literal, int, etc.), raise ValueError at @tool registration with a clear message: "parameter 'agent' is reserved for Agent injection; rename your parameter or annotate it as Agent"
    • configured context param: keep current behavior (opt-in, documented)

    This turns a silent runtime corruption into a loud build-time error. Happy to implement if maintainers agree.

  5. surlymochan commented on Oct 28, 2026

    @surlymochan
    Contributor

    Opened #5622 with a fix along the lines discussed here: when a tool is defined, a parameter named agent whose annotation cannot hold an Agent (e.g. Literal[...], str) now emits a UserWarning at definition time, and the @tool docstring documents the reserved names (self, cls, agent).

    One deliberate deviation from the raise-at-registration proposal: test_stream_with_agent codifies injection into an agent: dict parameter as supported behavior, so a hard ValueError would break existing tools that rely on it. The check lives in FunctionToolMetadata._validate_agent_parameter if maintainers prefer to make it stricter later. Unannotated/Any parameters, Agent/Agent | None annotations, and a context parameter named agent are all unaffected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-devxDeveloper experience improvementsarea-toolTool behavior/apibugSomething isn't workingpythonPull requests that update python code

    Type

    Fields

    Language

    Python

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions