You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Python: [Bug]: max_invocations / max_invocation_exceptions are not enforced for tools defined as methods #8904
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 itawait b.invoke(arguments={"x": 0})await b.invoke(arguments={"x": 1}) # ToolException
asyncio.run(main())
🤖 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.
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.
/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.
Activity
github-actions commentedon Sep 30, 2026
🤖 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.
python/packages/core/tests/core/test_tools.py1.19.01.19.0eavanvalkenburg commentedon Nov 3, 2026
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.
eavanvalkenburg commentedon Nov 3, 2026
/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.