refactor(tool): add Toolkit deep copy to isolate agent state#438
refactor(tool): add Toolkit deep copy to isolate agent state#438AlbumenJ merged 4 commits intoagentscope-ai:mainfrom
Conversation
Change-Id: I33a8687ea436db43e9f0a8af763496747a5f9526
There was a problem hiding this comment.
Pull request overview
This PR adds deep copy functionality to the Toolkit class to enable agent state isolation. Each agent built from a ReActAgent.Builder now receives its own deep copy of the toolkit, preventing state interference when multiple agents share the same toolkit configuration.
- Deep copy implementation for Toolkit, ToolRegistry, and ToolGroupManager
- ReActAgent builder now creates toolkit copies per agent instance
- Builder collections changed to Sets (hooks, knowledgeBases) for deduplication
- Integration of ActingChunkEvent notification for streaming tool execution
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| Toolkit.java | Adds copy() method to create deep copies of toolkit instances for agent isolation |
| ToolRegistry.java | Implements copyTo() to transfer tool registrations between registries |
| ToolGroupManager.java | Implements copyTo() to duplicate tool group state and active group tracking |
| ReActAgent.java | Builder creates toolkit deep copy per agent; adds chunk callback for streaming; changes hooks/knowledgeBases to Sets |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| String toolName = entry.getKey(); | ||
| AgentTool tool = entry.getValue(); | ||
| RegisteredToolFunction registered = registeredTools.get(toolName); | ||
| target.registerTool(toolName, tool, registered); |
There was a problem hiding this comment.
The copyTo method performs a shallow copy by sharing the same AgentTool and RegisteredToolFunction instances between the source and target registries. This violates the deep copy intention stated in the PR.
RegisteredToolFunction has mutable state (the presetParameters field can be modified via updatePresetParameters method). When agents share the same RegisteredToolFunction instance, calling toolkit.updateToolPresetParameters on one agent's toolkit will affect other agents that share the same tool reference.
To properly isolate agent state, each RegisteredToolFunction should be deep copied with its own presetParameters map. Consider creating a copy constructor or copy method in RegisteredToolFunction that creates a new instance with copied state.
| target.registerTool(toolName, tool, registered); | |
| RegisteredToolFunction registeredCopy = registered == null ? null : registered.copy(); | |
| target.registerTool(toolName, tool, registeredCopy); |
| public Toolkit copy() { | ||
| Toolkit copy = new Toolkit(this.config); | ||
|
|
||
| // Copy all registered tools | ||
| this.toolRegistry.copyTo(copy.toolRegistry); | ||
|
|
||
| // Copy all tool groups and their states | ||
| this.groupManager.copyTo(copy.groupManager); | ||
|
|
||
| return copy; | ||
| } |
There was a problem hiding this comment.
The new copy() method lacks test coverage. Given the critical nature of this functionality for agent state isolation and the comprehensive test suite for Toolkit (ToolkitTest.java has 740 lines of tests), this method should have corresponding tests.
Suggested test scenarios:
- Verify that modifying copied toolkit's tool groups doesn't affect the original
- Verify that updating preset parameters in one toolkit doesn't affect the copy
- Verify that all tools are copied correctly
- Verify that active groups state is properly isolated
- Verify that the config is shared (not copied) as intended
| } | ||
|
|
||
| // Set chunk callback for streaming tool responses | ||
| toolkit.setChunkCallback((toolUse, chunk) -> notifyActingChunk(toolUse, chunk).subscribe()); |
There was a problem hiding this comment.
The chunk callback uses .subscribe() which creates a fire-and-forget subscription. This means:
- Any errors in notifyActingChunk will be silently swallowed
- The subscription is not managed or cancelled, potentially leading to resource leaks
- There's no backpressure handling
Consider returning the Mono without subscribing and let the caller manage the subscription, or use .subscribeOn() with proper error handling. Alternatively, if fire-and-forget is intentional, add explicit error handling with .doOnError().
| toolkit.setChunkCallback((toolUse, chunk) -> notifyActingChunk(toolUse, chunk).subscribe()); | |
| toolkit.setChunkCallback( | |
| (toolUse, chunk) -> | |
| notifyActingChunk(toolUse, chunk) | |
| .doOnError( | |
| error -> | |
| LoggerFactory.getLogger(ReActAgent.class) | |
| .error( | |
| "Error while notifying acting chunk", | |
| error)) | |
| .subscribe()); |
Change-Id: I5992bcaa9cec12061cc8a03b717828f893ebd91f
Change-Id: If8b985c041e0311492a7b871d655316cd2056ef2
Change-Id: Id4921c309801b0e4e0d82eed6af5b68888609230
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
No description provided.