fix(agui): eliminate concurrent toolkit pollution via per-call RuntimeContext toolkit - #2463
fix(agui): eliminate concurrent toolkit pollution via per-call RuntimeContext toolkit#2463xzxiaoshan wants to merge 1 commit into
Conversation
…ll toolkit via RuntimeContext, which now supports runtime toolkit override
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
oss-maintainer
left a comment
There was a problem hiding this comment.
Review: Approved ✅
This is a clean, well-architected fix for the concurrent toolkit pollution problem identified in #2459.
Design Assessment
The shift from borrow-return mutation (ToolInjection.close() + doFinally) to per-call immutable copy via RuntimeContext.toolkit is the correct approach:
- Shared toolkit: Before = mutated per call (register/remove), After = read-only throughout
- Concurrency safety: Before = single-flow self-consistent only, After = fully isolated per call
- Cleanup: Before = fragile
doFinally+ToolInjection.close(), After = not needed — copy is GC'd with the call - Frontend tool visibility: Before = leaked into shared toolkit between cleanup cycles, After = scoped to
activeToolkitinCallExecution
Key Changes
-
RuntimeContext.toolkit— Clean per-call override mechanism. Getter/setter + Builder support.nullsemantics (fallback to shared field) is well-documented. -
ReActAgent.CallExecution.activeToolkit— Resolved once inbeforeAgentExecution, stable for the whole call. All internal references correctly migrated fromtoolkittoactiveToolkit. -
AguiAgentAdapter—ToolInjectionpattern fully removed.buildPerCallToolkit()deep-copies the shared toolkit and layers frontend tools on the copy. The shared toolkit is never mutated. -
Tests — Updated to assert
toolkit.getTool("frontend_lookup")isnull(shared toolkit untouched) andperCallToolkit.getTool(...)carries the frontend tool. Good coverage of the invariant.
Minor Note
CI builds are still in progress — will confirm once green.
Fixes #2459.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
oss-maintainer
left a comment
There was a problem hiding this comment.
Review: fix(agui): eliminate concurrent toolkit pollution via per-call RuntimeContext toolkit
Verdict: Approved ✅
Excellent architectural improvement. This PR eliminates the root cause of cross-stream toolkit pollution by replacing the fragile borrow-restore (ToolInjection) pattern with a clean per-call copy approach.
Key changes
RuntimeContext.toolkitfield — new per-call toolkit override with getter/setter/builder support.nullmeans fall back to the agent's shared field (backward compatible).CallExecution.activeToolkit— resolved once inbeforeAgentExecution, stable for the entire call. All 5 toolkit access points inCallExecutionnow readactiveToolkitinstead of the shared field.AguiAgentAdapterrefactor —injectFrontendTools→buildPerCallToolkitoperates onToolkit.copy(), sets it intoRuntimeContext. The entireToolInjectioninner class (borrow-restore + cleanup) is removed — 39 lines of fragile state management eliminated.from()builder method — correctly copies thetoolkitfield when deriving a context.
Test quality
All 5 existing tests are properly rewritten to verify shared toolkit isolation (asserting the shared toolkit is never mutated). The new perCallToolkitOverrideUsedByCallExecution test validates the end-to-end flow with a tool that only exists on the per-call toolkit.
Why this is better than the old approach
Old (ToolInjection) |
New (per-call copy) |
|---|---|
Mutates shared toolkit, restores on doFinally |
Shared toolkit never touched |
| Race condition if two streams overlap | Fully isolated per call |
| Complex cleanup logic (reverse-order remove, restore map) | Zero cleanup needed |
GhostToolName edge cases in cleanup |
Copy handles it naturally |
CI all green. LGTM.
问题
AguiAgentAdapter在运行时直接对 Agent 的共享 toolkit 执行registerAgentTool/removeTool,再通过ToolInjection.close()+doFinally做"借-还"恢复。该机制只保证单流自洽——同一 Agent 单例并发执行时,各流的注入与恢复会相互污染共享 toolkit 状态。方案
三层改动,从根源上消除并发污染(共享 toolkit 全程只读):
toolkit属性(getter/setter/builder),支持运行时携带 per-call toolkit 覆盖activeToolkit字段,beforeAgentExecution一次性解析(优先 RuntimeContext,fallback 共享字段),内部全部 toolkit 访问切换injectFrontendTools→buildPerCallToolkit:在toolkit.copy()副本上构建,设入 RuntimeContext,移除ToolInjection借-还机制改动文件
RuntimeContext.java— 新增toolkit字段及 Builder 方法ReActAgent.java—CallExecution新增activeToolkit,5 处 toolkit 引用收口AguiAgentAdapter.java— 重构为 per-call 副本模式,删除ToolInjection内部类Test Plan
mvn test -pl agentscope-core -Dtest="ReActAgent*,Toolkit*,RuntimeContext*"— 75 passedmvn test -pl agentscope-extensions-agui -Dtest=AguiAgentAdapterTest— 45 passedperCallToolkitOverrideUsedByCallExecution验证 RuntimeContext toolkit 在 CallExecution 层生效