Skip to content

Python: [Bug]: max_invocations / max_invocation_exceptions are not enforced for tools defined as methods #8904

Description

Description

FunctionTool.__get__ returns copy.copy(self) on every attribute access for method tools. Each bound copy starts from the class-level invocation_count, which is never incremented, so the limit only works if the caller keeps one bound object. Accessing instance.method again, for example building tools=[svc.ping] per run, resets the count.

Expected: a tool decorated with @tool(max_invocations=1) on a method raises ToolException on the second call for the same instance.
Actual: unlimited calls succeed, and Svc.ping.invocation_count stays at 0, so the documented "inspect/reset invocation_count" workflow does not work either.

This is different from #2329. That issue is about the counter being global on singleton tools; here the limit is not enforced at all.

Suggested fix: cache the bound copy per instance (e.g. weakref.WeakKeyDictionary on the descriptor), or move the counters into a shared mutable holder referenced by the copies.

Code Sample

import asyncio
from agent_framework import tool

class Svc:
    @tool(max_invocations=1)
    def ping(self, x: int) -> int:
        return x

async def main():
    s = Svc()
    for i in range(3):
        await s.ping.invoke(arguments={"x": i})   # all 3 succeed; 2nd should raise
    print(Svc.ping.invocation_count)              # 0

    b = s.ping                                     # reusing one bound object does enforce it
    await b.invoke(arguments={"x": 0})
    await b.invoke(arguments={"x": 1})             # ToolException

asyncio.run(main())

Error Messages / Stack Traces

Package Versions

agent-framework-core 1.19.0 (main @ 853c456)

Python Version

Python 3.11

Additional Context

Found during a code review of main @ 853c456 (2026-09-30); the repro above was run against that commit.

Activity

  1. added
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Sep 30, 2026
  2. added
    reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
    on Sep 30, 2026
  3. github-actions commented on Sep 30, 2026

    @github-actions
    Contributor

    🤖 Automated triage reproduction notes (agent-authored — trust but verify)

    Agent analysis

    FunctionTool.get in python/packages/core/agent_framework/_tools.py:722-749 creates a fresh shallow copy whenever a decorated instance method is accessed, while FunctionTool.call updates counters only on that copy. The bug occurs when callers retrieve instance.method again between calls, bypassing both max_invocations and max_invocation_exceptions. Minimal repro: decorate an instance method with either limit set to 1 and invoke it twice through separate instance.method lookups.

    • Failing test: python/packages/core/tests/core/test_tools.py
    • Files examined: python/AGENTS.md, python/packages/core/AGENTS.md, python/packages/core/agent_framework/_tools.py, python/packages/core/tests/core/test_tools.py, python/packages/core/pyproject.toml
    • Tests run: test_tool_decorator_in_class, test_async_tool_exception_limit_counts_awaited_failures, test_method_tool_max_invocations_persists_across_attribute_access, test_method_tool_max_invocation_exceptions_persists_across_attribute_access
    • Reported version: 1.19.0
    • Current version: 1.19.0
  4. added
    agentsUsage: [Issues, PRs], Target: Single agent
    and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Sep 30, 2026
  5. eavanvalkenburg commented on Nov 3, 2026

    @eavanvalkenburg
    Member

    Thanks for reporting this and for the work in #8951. We will consider persistent invocation and exception limits across repeated method-tool lookups as a core MAF capability. The exact counter lifetime, reset/copy behavior and support for different owner types still need agreement: current docs define limits per FunctionTool instance and describe a fresh binding on each lookup. This is not a promise of implementation or timing, approval of the current cache design, or a request for competing implementation. We will plan the compatibility boundary using the existing PR before marking it ready or assigning additional work.

  6. eavanvalkenburg commented on Nov 3, 2026

    @eavanvalkenburg
    Member

    /plan Establish the accepted core counter/binding lifetime contract for #8904 through existing #8951, not competing implementation. Current main documents limits per FunctionTool instance and fresh descriptor bindings; retaining one binding already preserves counters. Core consideration now accepts investigating persistence across repeated lookups, not an unrestricted per-run/global cap or blanket cache-design approval. Compare fresh binding identity with owner-scoped counter state against cached binding identity. Define class access, invocation/exception counters, same/different/equal/unhashable owners, resets, shallow/deep copies and pickle semantics. Agree the slots-only/no-dict/no-weakref boundary: the current fallback does not preserve counters across lookups. Current 00338c8 addresses prior weak-key/pickle issues and prevents cache-name overwrites by raising; do not repeat resolved data-destruction findings, but decide whether that new binding failure for previously valid owners is acceptable or requires a non-colliding strategy. Cover GC, multiple/inherited descriptors, runtime metrics serialization, concurrency and documented batch overshoot without inventing strict security/rate-limit guarantees. Rebase the currently conflicted PR and run current-main-versus-candidate regressions; distinguish contributor-reported checks from new results. Update the exact public lifetime contract and consumer guidance. Keep #8911 per-run policy distinct. Estimate executable production scope after agreement; no ready label, new assignment or blanket PR approval yet.

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

Metadata

Metadata

Labels

agentsUsage: [Issues, PRs], Target: Single agentpythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions