Skip to content

Commit 3a5f895

Browse files
authored
fix(chat): tools can now be dynamically added (olimorris#1693)
1 parent 3a39f15 commit 3a5f895

File tree

5 files changed

+117
-4
lines changed

5 files changed

+117
-4
lines changed

lua/codecompanion/strategies/chat/agents/init.lua

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ function Agent.new(args)
5858
return self
5959
end
6060

61+
---Refresh the tools configuration to pick up any dynamically added tools
62+
---@return CodeCompanion.Agent
63+
function Agent:refresh_tools()
64+
self.tools_config = ToolFilter.filter_enabled_tools(config.strategies.chat.tools)
65+
return self
66+
end
67+
6168
---Set the autocmds for the tool
6269
---@return nil
6370
function Agent:set_autocmds()

lua/codecompanion/strategies/chat/agents/tool_filter.lua

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,46 @@
1+
local hash = require("codecompanion.utils.hash")
12
local log = require("codecompanion.utils.log")
23

34
---@class CodeCompanion.Agent.ToolFilter
45
local ToolFilter = {}
56

67
local _enabled_cache = {}
78
local _cache_timestamp = 0
9+
local _config_hash = nil
810
local CACHE_TTL = 30000
911

1012
---Clear the enabled tools cache
1113
---@return nil
1214
local function clear_cache()
1315
_enabled_cache = {}
1416
_cache_timestamp = 0
17+
_config_hash = nil
1518
log:trace("[Tool Filter] Cache cleared")
1619
end
1720

18-
---Check if the cache is valid
21+
---Check if the cache is valid (time + config unchanged)
22+
---@param tools_config_hash integer The hash of the tools config
1923
---@return boolean
20-
local function is_cache_valid()
21-
return vim.loop.now() - _cache_timestamp < CACHE_TTL
24+
local function is_cache_valid(tools_config_hash)
25+
local time_valid = vim.loop.now() - _cache_timestamp < CACHE_TTL
26+
local config_unchanged = _config_hash == tools_config_hash
27+
return time_valid and config_unchanged
2228
end
2329

2430
---Get enabled tools from the cache or compute them
2531
---@param tools_config table The tools configuration
2632
---@return table<string, boolean> Map of tool names to enabled status
2733
local function get_enabled_tools(tools_config)
28-
if is_cache_valid() and next(_enabled_cache) then
34+
local current_hash = hash.hash(tools_config)
35+
if is_cache_valid(current_hash) and next(_enabled_cache) then
2936
log:trace("[Tool Filter] Using cached enabled tools")
3037
return _enabled_cache
3138
end
3239

3340
log:trace("[Tool Filter] Computing enabled tools")
3441
_enabled_cache = {}
3542
_cache_timestamp = vim.loop.now()
43+
_config_hash = current_hash
3644

3745
for tool_name, tool_config in pairs(tools_config) do
3846
-- Skip special keys

lua/codecompanion/strategies/chat/init.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,9 @@ function Chat:submit(opts)
857857
opts.callback()
858858
end
859859

860+
-- Refresh agent tools before submitting to pick up any dynamically added tools
861+
self.agents:refresh_tools()
862+
860863
local bufnr = self.bufnr
861864

862865
if opts.auto_submit then

tests/strategies/chat/agents/test_agents.lua

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,34 @@ T["Agent"][":find"]["should not find a group when tool name starts with group na
8383
h.eq({}, result.groups)
8484
end
8585

86+
T["Agent"][":find"]["should find tools added after a chat is initialized"] = function()
87+
child.lua([[
88+
--- DO NOT DELETE THIS ---
89+
--- WE ARE USING THE `codecompanion.config` instead of `tests.config` AS PER #1693 ---
90+
local config = require("codecompanion.config")
91+
92+
-- Add a dynamic tool after chat is already created
93+
config.strategies.chat.tools.dynamic_test_tool = {
94+
callback = "",
95+
description = "Dynamic tool",
96+
enabled = true,
97+
}
98+
99+
-- Submit a message with the dynamic tool - this should trigger refresh
100+
_G.chat:add_buf_message({
101+
role = "user",
102+
content = "Use @{dynamic_test_tool} please",
103+
})
104+
_G.chat:submit()
105+
106+
_G.found_dynamic_tool = _G.chat.tools.in_use["dynamic_test_tool"]
107+
-- Clean up
108+
config.strategies.chat.tools.dynamic_test_tool = nil
109+
]])
110+
111+
h.eq(true, child.lua_get("_G.found_dynamic_tool"))
112+
end
113+
86114
T["Agent"][":parse"] = new_set()
87115
T["Agent"][":parse"]["add a tool's system prompt to chat buffer"] = function()
88116
child.lua([[

tests/strategies/chat/agents/test_tool_filter.lua

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,71 @@ T["filters disabled tools in groups"] = function()
6666
h.eq(filtered.groups.empty_group, nil) -- Empty group should be removed
6767
end
6868

69+
T["cache invalidation"] = new_set()
70+
71+
T["cache invalidation"]["detects config changes when tools are added"] = function()
72+
local initial_config = {
73+
tool1 = { callback = "test", enabled = true },
74+
opts = {},
75+
}
76+
77+
-- First call - should cache
78+
local filtered1 = ToolFilter.filter_enabled_tools(initial_config)
79+
h.eq(filtered1.tool1 ~= nil, true)
80+
h.eq(filtered1.tool2, nil)
81+
82+
-- Add a new tool to the config
83+
initial_config.tool2 = { callback = "test", enabled = true }
84+
85+
-- Second call - should detect config change and return new tool
86+
local filtered2 = ToolFilter.filter_enabled_tools(initial_config)
87+
h.eq(filtered2.tool1 ~= nil, true)
88+
h.eq(filtered2.tool2 ~= nil, true)
89+
end
90+
91+
T["cache invalidation"]["detects config changes when tools are removed"] = function()
92+
local config = {
93+
tool1 = { callback = "test", enabled = true },
94+
tool2 = { callback = "test", enabled = true },
95+
opts = {},
96+
}
97+
98+
-- First call - should cache both tools
99+
local filtered1 = ToolFilter.filter_enabled_tools(config)
100+
h.eq(filtered1.tool1 ~= nil, true)
101+
h.eq(filtered1.tool2 ~= nil, true)
102+
103+
-- Remove a tool from the config
104+
config.tool2 = nil
105+
106+
-- Second call - should detect config change and return only remaining tool
107+
local filtered2 = ToolFilter.filter_enabled_tools(config)
108+
h.eq(filtered2.tool1 ~= nil, true)
109+
h.eq(filtered2.tool2, nil)
110+
end
111+
112+
T["cache invalidation"]["detects changes in groups"] = function()
113+
local config = {
114+
tool1 = { callback = "test", enabled = true },
115+
tool2 = { callback = "test", enabled = true },
116+
groups = {
117+
test_group = {
118+
tools = { "tool1" },
119+
},
120+
},
121+
opts = {},
122+
}
123+
124+
-- First call - should cache
125+
local filtered1 = ToolFilter.filter_enabled_tools(config)
126+
h.eq(#filtered1.groups.test_group.tools, 1)
127+
128+
-- Add tool to group
129+
config.groups.test_group.tools = { "tool1", "tool2" }
130+
131+
-- Second call - should detect config change
132+
local filtered2 = ToolFilter.filter_enabled_tools(config)
133+
h.eq(#filtered2.groups.test_group.tools, 2)
134+
end
135+
69136
return T

0 commit comments

Comments
 (0)