fix(tools): make _last_resolved_tool_names per-context (#34442) - #34451
fix(tools): make _last_resolved_tool_names per-context (#34442)#34451Bartok9 wants to merge 1 commit into
Conversation
|
Verified: the ContextVar approach is correct. Why Module One caveat for future callers: |
|
Thanks for the careful diagnosis and the clean ContextVar implementation, @Bartok9 — your read of the global was correct. Closing as already-mitigated, after tracing the actual impact:
So the per-instance threading that shipped earlier already closed the real hole. The remaining global is vestigial — it still gets clobbered across sessions, but no production decision reads the clobbered value. If we ever want the vestigial global gone for cleanliness, it's a ~5-line change (ContextVar + the few internal get/set sites + delegate save/restore), not a 113-line module-type proxy. The PEP-562 Appreciate the contribution — the analysis was right, the fix just isn't load-bearing. |
Summary
model_tools._last_resolved_tool_namesper-context (backed by aContextVar) instead of a process-global list.Motivation
Closes #34442.
_last_resolved_tool_names(model_tools.py:213) was a module-levelList[str]. When two sessions run concurrently in the same gateway process (e.g. two Telegram/Feishu users messaging from different chats), each session'sget_tool_definitions()overwrites this global, so session A'shandle_function_call()can check against session B's tool list. The existingdelegate_tool.pysave/restore only covers the delegation chain within a single session, not concurrent top-level sessions (as the issue notes).This implements the issue's suggested Option A: a
ContextVar[List[str]], so each asyncio task / session carries its own copy.To keep the fix surgical, the module attribute
model_tools._last_resolved_tool_namesis preserved as a transparently-proxied read/write surface (PEP 562__getattr__+ aModuleTypesubclass__setattr__that route through theContextVar). That means the ~9 existing call sites indelegate_tool.py,code_execution, and the test suite that read or assign the attribute directly keep working unchanged — they simply gain per-context isolation. No public signature changes.Verification
python3 -m pytest tests/tools/test_delegate.py— 134 passed, 1 deselected (the deselectedtest_heartbeat_does_not_trip_idle_stale_while_inside_toolis a pre-existing wall-clock timing flake that also fails on cleanorigin/mainwithout this change).tests/tools/test_delegate.py::TestLastResolvedToolNamesConcurrency:test_attribute_read_write_roundtrip— attribute still behaves like a plain list (incl. the delegate save/restore pattern).test_concurrent_sessions_do_not_clobber_each_other— two concurrent asyncio tasks each retain their own tool list across anawaitboundary (fails before this change).TestToolNamePreservation(delegate save/restore) — still passes.python3 -m py_compile model_tools.py— clean.Did NOT change
enabled_toolsparameter onhandle_function_call()remains the preferred path forexecute_code; the per-context value is still only the fallback, exactly as before.delegate_tool.pysave/restore logic is untouched (it now restores into the calling context, which is the correct scope).