Skip to content

[fix] Preserve function metadata in Toolkit bound methods via functools.wraps#7570

Open
nklowns wants to merge 1 commit intoagno-agi:mainfrom
nklowns:fix/toolkit-bound-method-wraps
Open

[fix] Preserve function metadata in Toolkit bound methods via functools.wraps#7570
nklowns wants to merge 1 commit intoagno-agi:mainfrom
nklowns:fix/toolkit-bound-method-wraps

Conversation

@nklowns
Copy link
Copy Markdown

@nklowns nklowns commented Apr 17, 2026

Summary

When _register_decorated_tool wraps a @tool-decorated method into a bound closure, it manually assigned only __name__ and __doc__. This left __qualname__, __annotations__, and __wrapped__ with incorrect or missing values on the resulting bound method.

Fixes #7569

Problem

class MyTools(Toolkit):
    def __init__(self):
        super().__init__(name="my_tools", tools=[self.my_tool])

    @tool(description="A test tool")
    def my_tool(self, x: int) -> str:
        """My docstring."""
        return str(x)

t = MyTools()
fn = t.functions["my_tool"].entrypoint

print(fn.__qualname__)    # "make_bound_method.<locals>.bound"  ← wrong
print(fn.__annotations__) # {}  ← type hints dropped
print(hasattr(fn, "__wrapped__"))  # False  ← unwrap chain broken

Solution

Replace the two manual attribute assignments with functools.wraps, which is the standard Python idiom for wrapping functions and preserves all relevant metadata in a single call. Applied to both the async and sync branches of make_bound_method.

Type of change

  • Bug fix

Checklist

  • Code complies with style guidelines
  • Ran format/validation scripts (./scripts/format.sh and ./scripts/validate.sh)
  • Self-review completed
  • Tests added/updated

Duplicate and AI-Generated PR Check

…s.wraps

Replaces manual __name__/__doc__ assignment with functools.wraps so that
bound methods created for @tool-decorated Toolkit methods correctly preserve
__qualname__, __annotations__, and __wrapped__.

Fixes agno-agi#7569

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@nklowns nklowns force-pushed the fix/toolkit-bound-method-wraps branch from 5c564e9 to f623668 Compare April 22, 2026 18:47
Copy link
Copy Markdown

@VANDRANKI VANDRANKI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diagnosis and fix are correct. functools.wraps is exactly the right tool here - it copies __module__, __name__, __qualname__, __annotations__, __doc__, __dict__ and sets __wrapped__ in one call, which is precisely what the manual assignment approach was missing.

One subtlety worth noting

After @functools.wraps(func), inspect.signature(bound) will reflect the original function signature - including self as the first parameter - rather than the bound callable's actual signature (which drops self). Python's descriptor protocol drops self when accessing a method through an instance, but functools.wraps does not replicate that. If any downstream code calls inspect.signature() on the entrypoint to build tool parameter schemas, it will see self in the signature. Worth checking whether Function schema extraction introspects the entrypoint directly.

Tests

The test structure is clean - one assertion per test function makes failures easy to pinpoint. One small gap: the async branch has no test_async_bound_method_preserves_doc counterpart to the sync test_sync_bound_method_preserves_doc. Not a blocker, but easy to add for completeness.

Both branches (sync and async) are fixed consistently, __wrapped__ is set (so inspect.unwrap() works), and the fix is minimal. LGTM from my side.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[fix] Toolkit bound methods lose __qualname__, __annotations__, and __wrapped__ when wrapping @tool-decorated methods

2 participants