From 6837babb8122540639ae5ff3eed67595d6dc4898 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Thu, 20 Feb 2025 19:00:00 +0000 Subject: [PATCH 01/38] wip: start to split out tools --- .../strategies/chat/tools/editor.lua | 4 +- .../strategies/chat/tools/executor/cmd.lua | 0 .../strategies/chat/tools/executor/func.lua | 48 ++++ .../strategies/chat/tools/executor/init.lua | 129 +++++++++ .../strategies/chat/tools/init.lua | 245 +----------------- lua/codecompanion/types.lua | 18 +- tests/config.lua | 8 + tests/strategies/chat/test_tools.lua | 230 +++++++++------- .../chat/tools/tools/stubs/func.lua | 15 ++ .../tools/tools/stubs/func_consecutive.lua | 16 ++ 10 files changed, 370 insertions(+), 343 deletions(-) create mode 100644 lua/codecompanion/strategies/chat/tools/executor/cmd.lua create mode 100644 lua/codecompanion/strategies/chat/tools/executor/func.lua create mode 100644 lua/codecompanion/strategies/chat/tools/executor/init.lua create mode 100644 tests/strategies/chat/tools/tools/stubs/func.lua create mode 100644 tests/strategies/chat/tools/tools/stubs/func_consecutive.lua diff --git a/lua/codecompanion/strategies/chat/tools/editor.lua b/lua/codecompanion/strategies/chat/tools/editor.lua index 8543d3b2a..311c6e8a6 100644 --- a/lua/codecompanion/strategies/chat/tools/editor.lua +++ b/lua/codecompanion/strategies/chat/tools/editor.lua @@ -97,12 +97,12 @@ local function add(bufnr, action) add_delta(bufnr, start_line, tonumber(#lines)) end ----@class CodeCompanion.Tool +---@class CodeCompanion.Agent.Tool return { name = "editor", cmds = { ---Ensure the final function returns the status and the output - ---@param self CodeCompanion.Tools The Tools object + ---@param self CodeCompanion.Agent The Tools object ---@param actions table The action object ---@param input any The output from the previous function call ---@return { status: string, msg: string } diff --git a/lua/codecompanion/strategies/chat/tools/executor/cmd.lua b/lua/codecompanion/strategies/chat/tools/executor/cmd.lua new file mode 100644 index 000000000..e69de29bb diff --git a/lua/codecompanion/strategies/chat/tools/executor/func.lua b/lua/codecompanion/strategies/chat/tools/executor/func.lua new file mode 100644 index 000000000..7c44350db --- /dev/null +++ b/lua/codecompanion/strategies/chat/tools/executor/func.lua @@ -0,0 +1,48 @@ +---@class CodeCompanion.Agent.Executor.Func +---@field executor CodeCompanion.Agent.Executor +---@field cmd fun(self: CodeCompanion.Agent, actions: table, input: any) +---@field index number +local FuncExecutor = {} + +---@param executor CodeCompanion.Agent.Executor +---@param cmd fun() +---@param index number +function FuncExecutor.new(executor, cmd, index) + return setmetatable({ + executor = executor, + cmd = cmd, + index = index, + }, { __index = FuncExecutor }) +end + +---Orchestrate the tool function +---@param input any +---@return nil +function FuncExecutor:orchestrate(input) + local action = self.executor.tool.request.action + -- Allow the cmds table to have multiple functions + if type(action) == "table" and type(action[1]) == "table" then + local output + for _, a in ipairs(action) do + self:run(self.cmd, a, input) + end + else + self:run(self.cmd, action, input) + end +end + +---Run the tool function +---@param cmd fun(self: CodeCompanion.Agent, actions: table, input: any) +---@param action string +---@param input? any +function FuncExecutor:run(cmd, action, input) + local ok, output = pcall(function() + return cmd(self.executor.agent, action, input) + end) + if not ok then + -- do something + end + return self.executor:execute(self.index + 1, output) +end + +return FuncExecutor diff --git a/lua/codecompanion/strategies/chat/tools/executor/init.lua b/lua/codecompanion/strategies/chat/tools/executor/init.lua new file mode 100644 index 000000000..46a66af8b --- /dev/null +++ b/lua/codecompanion/strategies/chat/tools/executor/init.lua @@ -0,0 +1,129 @@ +local CmdExecutor = require("codecompanion.strategies.chat.tools.executor.cmd") +local FuncExecutor = require("codecompanion.strategies.chat.tools.executor.func") +local config = require("codecompanion.config") +local log = require("codecompanion.utils.log") +local util = require("codecompanion.utils") + +---@class CodeCompanion.Agent.Executor +---@field agent CodeCompanion.Agent +---@field handlers table +---@field index number The index of the current command +---@field output table +---@field tool CodeCompanion.Agent.Tool +---@field status string +local Executor = {} + +local CONSTANTS = { + STATUS_ERROR = "error", + STATUS_SUCCESS = "success", +} + +---@param agent CodeCompanion.Agent +---@param tool CodeCompanion.Agent.Tool +function Executor.new(agent, tool) + log:debug("Creating new Executor for tool: %s", tool.name) + local self = setmetatable({ + agent = agent, + tool = tool, + status = CONSTANTS.STATUS_SUCCESS, + stderr = {}, + stdout = {}, + }, { __index = Executor }) + + self.handlers = { + setup = function() + vim.g.codecompanion_current_tool = self.tool.name + if self.tool.handlers and self.tool.handlers.setup then + self.tool.handlers.setup(agent) + end + end, + approved = function(cmd) + if self.tool.handlers and self.tool.handlers.approved then + return self.tool.handlers.approved(agent, cmd) + end + return true + end, + on_exit = function() + if self.tool.handlers and self.tool.handlers.on_exit then + self.tool.handlers.on_exit(agent) + end + end, + } + + self.output = { + rejected = function(cmd) + if self.tool.output and self.tool.output.rejected then + self.tool.output.rejected(agent, cmd) + end + end, + error = function(cmd, error) + if self.tool.output and self.tool.output.error then + self.tool.output.error(agent, cmd, error) + end + end, + success = function(cmd, output) + if self.tool.output and self.tool.output.success then + self.tool.output.success(agent, cmd, output) + end + end, + } + + _G.codecompanion_cancel_tool = false + util.fire("AgentStarted", { tool = tool.name, bufnr = agent.bufnr }) + self.handlers.setup() + + return self +end + +---Execute the tool command +---@param index? number The index of the command to execute +---@param input? any +---@return nil +function Executor:execute(index, input) + index = index or 1 + if index > vim.tbl_count(self.tool.cmds) or self.status == CONSTANTS.STATUS_ERROR then + return self:close() + end + + local cmd = self.tool.cmds[index] + if type(cmd) == "function" then + return FuncExecutor.new(self, cmd, index):orchestrate(input) + end + return CmdExecutor.new(self, cmd):execute() +end + +---Does the tool require approval before it can be executed? +---@return boolean +function Executor:requires_approval() + return config.strategies.chat.agents.tools[self.tool.name].opts + and config.strategies.chat.agents.tools[self.tool.name].opts.user_approval + or false +end + +---Close the execution of the tool +---@return nil +function Executor:close() + self.handlers.on_exit() + + util.fire("AgentFinished", { + name = self.tool.name, + bufnr = self.agent.bufnr, + status = self.status, + stderr = self.stderr, + stdout = self.stdout, + }) + self:reset() + + self.agent.chat.subscribers:process(self.agent.chat) + vim.g.codecompanion_current_tool = nil +end + +---Reset the executor class +---@return nil +function Executor:reset() + self.status = CONSTANTS.STATUS_SUCCESS + self.stderr = {} + self.stdout = {} +end + +return Executor diff --git a/lua/codecompanion/strategies/chat/tools/init.lua b/lua/codecompanion/strategies/chat/tools/init.lua index 4792da49c..99813d2c6 100644 --- a/lua/codecompanion/strategies/chat/tools/init.lua +++ b/lua/codecompanion/strategies/chat/tools/init.lua @@ -1,7 +1,6 @@ -local Job = require("plenary.job") -local config = require("codecompanion.config") - +local Executor = require("codecompanion.strategies.chat.tools.executor") local TreeHandler = require("codecompanion.utils.xml.xmlhandler.tree") +local config = require("codecompanion.config") local log = require("codecompanion.utils.log") local ui = require("codecompanion.utils.ui") local util = require("codecompanion.utils") @@ -47,7 +46,7 @@ local function parse_xml(message) return handler.root.tools end ----@class CodeCompanion.Tools +---@class CodeCompanion.Agent local Tools = {} ---@param args table @@ -195,7 +194,7 @@ function Tools:setup(chat, xml) util.replace_placeholders(self.tool.cmds, env) end - self:run() + return Executor.new(self, self.tool):execute() end -- This allows us to run multiple tools in a single response whether they're in @@ -209,240 +208,6 @@ function Tools:setup(chat, xml) return run_tool(schema) end ----Run the tool ----@return nil -function Tools:run() - local stderr = {} - local stdout = {} - local status = CONSTANTS.STATUS_SUCCESS - _G.codecompanion_cancel_tool = false - - local requires_approval = ( - config.strategies.chat.agents.tools[self.tool.name].opts - and config.strategies.chat.agents.tools[self.tool.name].opts.user_approval - or false - ) - - local handlers = { - setup = function() - vim.g.codecompanion_current_tool = self.tool.name - if self.tool.handlers and self.tool.handlers.setup then - self.tool.handlers.setup(self) - end - end, - approved = function(cmd) - if self.tool.handlers and self.tool.handlers.approved then - return self.tool.handlers.approved(self, cmd) - end - return true - end, - on_exit = function() - if self.tool.handlers and self.tool.handlers.on_exit then - self.tool.handlers.on_exit(self) - end - end, - } - - local output = { - rejected = function(cmd) - if self.tool.output and self.tool.output.rejected then - self.tool.output.rejected(self, cmd) - end - end, - error = function(cmd, error) - if self.tool.output and self.tool.output.error then - self.tool.output.error(self, cmd, error) - end - end, - success = function(cmd, output) - if self.tool.output and self.tool.output.success then - self.tool.output.success(self, cmd, output) - end - end, - } - - ---Action to take when closing the job - local function close() - handlers.on_exit() - - util.fire( - "AgentFinished", - { name = self.tool.name, bufnr = self.bufnr, status = status, stderr = stderr, stdout = stdout } - ) - - status = CONSTANTS.STATUS_SUCCESS - stderr = {} - stdout = {} - - self.chat.subscribers:process(self.chat) - vim.g.codecompanion_current_tool = nil - end - - ---Run the commands in the tool - ---@param index number - ---@param ... any - local function run(index, ...) - local function continue() - if not self.tool.cmds then - return false - end - if index >= vim.tbl_count(self.tool.cmds) or status == CONSTANTS.STATUS_ERROR then - return false - end - return true - end - - local cmd = self.tool.cmds[index] - log:debug("[Tools] Running cmd: %s", self.tool.name) - - ---Execute a function tool - local function execute_func(action, ...) - if requires_approval and not handlers.approved(action) then - output.rejected(action) - if not continue() then - return close() - end - end - - local ok, data = pcall(function(...) - return cmd(self, action, ...) - end) - if not ok then - status = CONSTANTS.STATUS_ERROR - table.insert(stderr, data) - log:error("Error calling function in %s: %s", self.tool.name, data) - output.error(action, data) - return close() - end - - if data.status == CONSTANTS.STATUS_ERROR then - status = CONSTANTS.STATUS_ERROR - table.insert(stderr, data.msg) - output.error(action, data.msg) - else - table.insert(stdout, data.msg) - output.success(action, data.msg) - end - - if not continue() then - return close() - end - - run(index + 1, output) - end - - -- Tools that are setup as Lua functions - if type(cmd) == "function" then - local action = self.tool.request.action - if type(action) == "table" and type(action[1]) == "table" then - for _, a in ipairs(action) do - execute_func(a, ...) - end - else - execute_func(action, ...) - end - end - - -- Tools that are setup as shell commands - if type(cmd) == "table" then - if requires_approval and not handlers.approved(cmd) then - output.rejected(cmd) - if not continue() then - return close() - end - end - - local new_job = Job:new({ - command = vim.fn.has("win32") == 1 and "cmd.exe" or "sh", - args = { vim.fn.has("win32") == 1 and "/c" or "-c", table.concat(cmd.cmd or cmd, " ") }, - enable_recording = true, - cwd = vim.fn.getcwd(), - on_stdout = function(_, data) - vim.schedule(function() - table.insert(strip_ansi(stdout), data) - end) - end, - on_stderr = function(err, data) - vim.schedule(function() - table.insert(strip_ansi(stderr), data) - end) - - if err then - vim.schedule(function() - stderr = strip_ansi(err) - status = CONSTANTS.STATUS_ERROR - log:error("Error running tool %s: %s", self.tool.name, err) - return close() - end) - end - end, - on_exit = function(data, code) - self.chat.current_tool = nil - - -- Handle the LLM setting any flags - if cmd.flag then - self.chat.tool_flags = self.chat.tool_flags or {} - self.chat.tool_flags[cmd.flag] = (code == 0) - end - - log:debug("[Tools] %s finished with code %s", self.tool.name, code) - - vim.schedule(function() - if _G.codecompanion_cancel_tool then - stdout = strip_ansi(stdout) - stderr = strip_ansi(stderr) - return close() - end - - if vim.tbl_isempty(stdout) and vim.tbl_isempty(stderr) then - if code == 0 then - output.success(cmd, "Tool finished successfully but with no output") - else - output.error(cmd, "Tool failed with code " .. code .. " and no output") - end - elseif not vim.tbl_isempty(stderr) then - output.error(cmd, strip_ansi(stderr)) - log:debug("[Tools] %s finished with stderr: %s", self.tool.name, stderr) - stderr = {} - if code == 0 then - output.success(cmd, strip_ansi(stdout)) - log:trace("[Tools] %s finished with output: %s", self.tool.name, stdout) - stdout = {} - end - elseif not vim.tbl_isempty(stdout) then - output.success(cmd, strip_ansi(stdout)) - log:trace("[Tools] %s finished with output: %s", self.tool.name, stdout) - stdout = {} - end - - if not continue() then - return close() - end - - run(index + 1, data) - end) - end, - }) - - if self.chat.current_tool then - -- Chain to the previous job if it exists - self.chat.current_tool:and_then_wrap(new_job) - else - -- Start first job directly - new_job:start() - end - - -- Update current_tool reference - self.chat.current_tool = new_job - end - end - - util.fire("AgentStarted", { tool = self.tool.name, bufnr = self.bufnr }) - - handlers.setup() - return run(1) -end - ---Look for tools in a given message ---@param chat CodeCompanion.Chat ---@param message table @@ -585,7 +350,7 @@ end ---Add an error message to the chat buffer ---@param error string ----@return CodeCompanion.Tools +---@return CodeCompanion.Agent function Tools:add_error_to_chat(error) self.chat:add_message({ role = config.constants.USER_ROLE, diff --git a/lua/codecompanion/types.lua b/lua/codecompanion/types.lua index 0cb8e1ef0..55d2df2db 100644 --- a/lua/codecompanion/types.lua +++ b/lua/codecompanion/types.lua @@ -104,7 +104,7 @@ ---@field settings table ---@field tokens number ----@class CodeCompanion.Tool +---@class CodeCompanion.Agent.Tool ---@field name string The name of the tool ---@field cmds table The commands to execute ---@field schema table The schema that the LLM must use in its response to execute a tool @@ -112,22 +112,22 @@ ---@field opts? table The options for the tool ---@field env? fun(schema: table): table|nil Any environment variables that can be used in the *_cmd fields. Receives the parsed schema from the LLM ---@field handlers table Functions which can be called during the execution of the tool ----@field handlers.setup? fun(self: CodeCompanion.Tools): any Function used to setup the tool. Called before any commands ----@field handlers.approved? fun(self: CodeCompanion.Tools): boolean Function to call if an approval is needed before running a command ----@field handlers.on_exit? fun(self: CodeCompanion.Tools): any Function to call at the end of all of the commands +---@field handlers.setup? fun(self: CodeCompanion.Agent): any Function used to setup the tool. Called before any commands +---@field handlers.approved? fun(self: CodeCompanion.Agent): boolean Function to call if an approval is needed before running a command +---@field handlers.on_exit? fun(self: CodeCompanion.Agent): any Function to call at the end of all of the commands ---@field output? table Functions which can be called after the command finishes ----@field output.rejected? fun(self: CodeCompanion.Tools, cmd: table): any Function to call if the user rejects running a command ----@field output.error? fun(self: CodeCompanion.Tools, cmd: table, error: table|string): any Function to call if the tool is unsuccessful ----@field output.success? fun(self: CodeCompanion.Tools, cmd: table, output: table|string): any Function to call if the tool is successful +---@field output.rejected? fun(self: CodeCompanion.Agent, cmd: table): any Function to call if the user rejects running a command +---@field output.error? fun(self: CodeCompanion.Agent, cmd: table, error: table|string): any Function to call if the tool is unsuccessful +---@field output.success? fun(self: CodeCompanion.Agent, cmd: table, output: table|string): any Function to call if the tool is successful ---@field request table The request from the LLM to use the Tool ----@class CodeCompanion.Tools +---@class CodeCompanion.Agent ---@field aug number The augroup for the tool ---@field bufnr number The buffer of the chat buffer ---@field chat CodeCompanion.Chat The chat buffer that initiated the tool ---@field extracted table The extracted tools from the LLM's response ---@field messages table The messages in the chat buffer ----@field tool CodeCompanion.Tool The current tool that's being run +---@field tool CodeCompanion.Agent.Tool The current tool that's being run ---@field agent_config table The agent strategy from the config ---@field tools_ns integer The namespace for the virtual text that appears in the header diff --git a/tests/config.lua b/tests/config.lua index 54ebf5e70..bf5e7ba77 100644 --- a/tests/config.lua +++ b/tests/config.lua @@ -64,6 +64,14 @@ return { callback = "utils.bar_again", description = "Some bar_again function", }, + ["func"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/tools/tools/stubs/func.lua", + description = "Some function tool to test", + }, + ["func_consecutive"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/tools/tools/stubs/func_consecutive.lua", + description = "Consecutive function tool to test", + }, opts = { system_prompt = [[My tool system prompt]], }, diff --git a/tests/strategies/chat/test_tools.lua b/tests/strategies/chat/test_tools.lua index 1ed9b5be1..e70098388 100644 --- a/tests/strategies/chat/test_tools.lua +++ b/tests/strategies/chat/test_tools.lua @@ -10,109 +10,155 @@ T["Tools"] = new_set({ pre_case = function() chat, tools = h.setup_chat_buffer() end, - post_once = function() + post_case = function() h.teardown_chat_buffer() end, }, }) -T["Tools"]["resolve"] = new_set() - -T["Tools"]["resolve"]["can resolve built-in tools"] = function() - local tool = tools.resolve({ - callback = "strategies.chat.tools.editor", - description = "Update a buffer with the LLM's response", - }) - - h.eq(type(tool), "table") - h.eq("editor", tool.name) - h.eq(6, #tool.schema) -end - -T["Tools"]["resolve"]["can resolve user's tools"] = function() - local tool = tools.resolve({ - callback = vim.fn.getcwd() .. "/tests/stubs/foo.lua", - description = "Some foo function", - }) - - h.eq(type(tool), "table") - h.eq("foo", tool.name) - h.eq("This is the Foo tool", tool.cmds[1]()) -end - -T["Tools"][":parse"] = new_set() - -T["Tools"][":parse"]["a message with a tool"] = function() - table.insert(chat.messages, { - role = "user", - content = "@foo do some stuff", - }) - tools:parse(chat, chat.messages[#chat.messages]) - local messages = chat.messages - - h.eq("My tool system prompt", messages[#messages - 1].content) - h.eq("my foo system prompt", messages[#messages].content) -end - -T["Tools"][":parse"]["a response from the LLM"] = function() - chat:add_buf_message({ - role = "user", - content = "@foo do some stuff", - }) - chat:add_buf_message({ - role = "llm", - content = [[Sure. Let's do this. - -```xml - - - Some foo function +-- T["Tools"]["resolve"] = new_set() +-- +-- T["Tools"]["resolve"]["can resolve built-in tools"] = function() +-- local tool = tools.resolve({ +-- callback = "strategies.chat.tools.editor", +-- description = "Update a buffer with the LLM's response", +-- }) +-- +-- h.eq(type(tool), "table") +-- h.eq("editor", tool.name) +-- h.eq(6, #tool.schema) +-- end +-- +-- T["Tools"]["resolve"]["can resolve user's tools"] = function() +-- local tool = tools.resolve({ +-- callback = vim.fn.getcwd() .. "/tests/stubs/foo.lua", +-- description = "Some foo function", +-- }) +-- +-- h.eq(type(tool), "table") +-- h.eq("foo", tool.name) +-- h.eq("This is the Foo tool", tool.cmds[1]()) +-- end +-- +-- T["Tools"][":parse"] = new_set() +-- +-- T["Tools"][":parse"]["a message with a tool"] = function() +-- table.insert(chat.messages, { +-- role = "user", +-- content = "@foo do some stuff", +-- }) +-- tools:parse(chat, chat.messages[#chat.messages]) +-- local messages = chat.messages +-- +-- h.eq("My tool system prompt", messages[#messages - 1].content) +-- h.eq("my foo system prompt", messages[#messages].content) +-- end +-- +-- T["Tools"][":parse"]["a response from the LLM"] = function() +-- chat:add_buf_message({ +-- role = "user", +-- content = "@foo do some stuff", +-- }) +-- chat:add_buf_message({ +-- role = "llm", +-- content = [[Sure. Let's do this. +-- +-- ```xml +-- +-- +-- Some foo function +-- +-- +-- ``` +-- ]], +-- }) +-- chat.tools.chat = chat +-- chat.tools:parse_buffer(chat, 5, 100) +-- +-- local lines = h.get_buf_lines(chat.bufnr) +-- h.eq("This is from the foo tool", lines[#lines]) +-- end +-- +-- T["Tools"][":parse"]["a nested response from the LLM"] = function() +-- chat:add_buf_message({ +-- role = "user", +-- content = "@foo @bar do some stuff", +-- }) +-- chat:add_buf_message({ +-- role = "llm", +-- content = [[Sure. Let's do this. +-- +-- ```xml +-- +-- +-- Some foo function +-- +-- +-- Some bar function +-- +-- +-- ``` +-- ]], +-- }) +-- chat.tools.chat = chat +-- chat.tools:parse_buffer(chat, 5, 100) +-- +-- local lines = h.get_buf_lines(chat.bufnr) +-- h.eq("This is from the foo toolThis is from the bar tool", lines[#lines]) +-- end +-- +-- T["Tools"][":replace"] = new_set() +-- +-- T["Tools"][":replace"]["should replace the tool in the message"] = function() +-- local message = "run the @foo tool" +-- local result = tools:replace(message, "foo") +-- h.eq("run the foo tool", result) +-- end + +T["Tools"][":setup"] = new_set() + +T["Tools"][":setup"]["can run functions"] = function() + h.eq(vim.g.codecompanion_test, nil) + tools:setup( + chat, + [[ + + Data 1 + Data 2 - -``` -]], - }) - chat.tools.chat = chat - chat.tools:parse_buffer(chat, 5, 100) - - local lines = h.get_buf_lines(chat.bufnr) - h.eq("This is from the foo tool", lines[#lines]) +]] + ) + h.eq("Data 1 Data 2", vim.g.codecompanion_test) + vim.g.codecompanion_test = nil end -T["Tools"][":parse"]["a nested response from the LLM"] = function() - chat:add_buf_message({ - role = "user", - content = "@foo @bar do some stuff", - }) - chat:add_buf_message({ - role = "llm", - content = [[Sure. Let's do this. - -```xml - - - Some foo function +T["Tools"][":setup"]["can run consecutive functions"] = function() + h.eq(vim.g.codecompanion_test, nil) + tools:setup( + chat, + [[ + + Data 1 - - Some bar function - - -``` -]], - }) - chat.tools.chat = chat - chat.tools:parse_buffer(chat, 5, 100) - - local lines = h.get_buf_lines(chat.bufnr) - h.eq("This is from the foo toolThis is from the bar tool", lines[#lines]) +]] + ) + h.eq("Data 1 Data 1", vim.g.codecompanion_test) + vim.g.codecompanion_test = nil end -T["Tools"][":replace"] = new_set() - -T["Tools"][":replace"]["should replace the tool in the message"] = function() - local message = "run the @foo tool" - local result = tools:replace(message, "foo") - h.eq("run the foo tool", result) +T["Tools"][":setup"]["can run multiple, consecutive functions"] = function() + h.eq(vim.g.codecompanion_test, nil) + tools:setup( + chat, + [[ + + Data 1 + Data 2 + +]] + ) + h.eq("Data 2 Data 2", vim.g.codecompanion_test) + vim.g.codecompanion_test = nil end return T diff --git a/tests/strategies/chat/tools/tools/stubs/func.lua b/tests/strategies/chat/tools/tools/stubs/func.lua new file mode 100644 index 000000000..825d9ac1f --- /dev/null +++ b/tests/strategies/chat/tools/tools/stubs/func.lua @@ -0,0 +1,15 @@ +return { + name = "func", + system_prompt = function(schema) + return "my func system prompt" + end, + cmds = { + function(self, actions, input) + local spacer = "" + if vim.g.codecompanion_test then + spacer = " " + end + vim.g.codecompanion_test = (vim.g.codecompanion_test or "") .. spacer .. actions.data + end, + }, +} diff --git a/tests/strategies/chat/tools/tools/stubs/func_consecutive.lua b/tests/strategies/chat/tools/tools/stubs/func_consecutive.lua new file mode 100644 index 000000000..cc7e5a706 --- /dev/null +++ b/tests/strategies/chat/tools/tools/stubs/func_consecutive.lua @@ -0,0 +1,16 @@ +return { + name = "func_consecutive", + system_prompt = function(schema) + return "my func system prompt" + end, + cmds = { + function(self, actions, input) + return (input and (input .. " ") or "") .. actions.data + end, + function(self, actions, input) + local output = input .. " " .. actions.data + vim.g.codecompanion_test = output + return output + end, + }, +} From 5b7d1636fcb924c069d3340a1016137f34f8e661 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Thu, 20 Feb 2025 22:06:45 +0000 Subject: [PATCH 02/38] wip: rename `tools.init` to `agents.init` --- lua/codecompanion/config.lua | 8 +-- .../chat/{tools => agents}/executor/cmd.lua | 0 .../chat/{tools => agents}/executor/func.lua | 6 +- .../chat/{tools => agents}/executor/init.lua | 51 ++++++++------- .../chat/{tools => agents}/init.lua | 63 +++++++++++++------ .../chat/{ => agents}/tools/cmd_runner.lua | 0 .../chat/{ => agents}/tools/editor.lua | 4 +- .../chat/{ => agents}/tools/files.lua | 4 +- .../chat/{ => agents}/tools/rag.lua | 0 lua/codecompanion/strategies/chat/init.lua | 12 ++-- lua/codecompanion/types.lua | 10 --- tests/config.lua | 4 ++ tests/helpers.lua | 4 +- .../chat/{test_tools.lua => test_agent.lua} | 60 ++++++++++++------ tests/strategies/chat/tools/test_files.lua | 2 +- .../chat/tools/tools/stubs/func.lua | 5 ++ .../chat/tools/tools/stubs/func_error.lua | 16 +++++ 17 files changed, 159 insertions(+), 90 deletions(-) rename lua/codecompanion/strategies/chat/{tools => agents}/executor/cmd.lua (100%) rename lua/codecompanion/strategies/chat/{tools => agents}/executor/func.lua (91%) rename lua/codecompanion/strategies/chat/{tools => agents}/executor/init.lua (77%) rename lua/codecompanion/strategies/chat/{tools => agents}/init.lua (86%) rename lua/codecompanion/strategies/chat/{ => agents}/tools/cmd_runner.lua (100%) rename lua/codecompanion/strategies/chat/{ => agents}/tools/editor.lua (98%) rename lua/codecompanion/strategies/chat/{ => agents}/tools/files.lua (99%) rename lua/codecompanion/strategies/chat/{ => agents}/tools/rag.lua (100%) rename tests/strategies/chat/{test_tools.lua => test_agent.lua} (69%) create mode 100644 tests/strategies/chat/tools/tools/stubs/func_error.lua diff --git a/lua/codecompanion/config.lua b/lua/codecompanion/config.lua index a42e821af..52a033ead 100644 --- a/lua/codecompanion/config.lua +++ b/lua/codecompanion/config.lua @@ -58,25 +58,25 @@ local defaults = { }, tools = { ["cmd_runner"] = { - callback = "strategies.chat.tools.cmd_runner", + callback = "strategies.chat.agents.tools.cmd_runner", description = "Run shell commands initiated by the LLM", opts = { user_approval = true, }, }, ["editor"] = { - callback = "strategies.chat.tools.editor", + callback = "strategies.chat.agents.tools.editor", description = "Update a buffer with the LLM's response", }, ["files"] = { - callback = "strategies.chat.tools.files", + callback = "strategies.chat.agents.tools.files", description = "Update the file system with the LLM's response", opts = { user_approval = true, }, }, ["rag"] = { - callback = "strategies.chat.tools.rag", + callback = "strategies.chat.agents.tools.rag", description = "Supplement the LLM with real-time info from the internet", opts = { hide_output = true, diff --git a/lua/codecompanion/strategies/chat/tools/executor/cmd.lua b/lua/codecompanion/strategies/chat/agents/executor/cmd.lua similarity index 100% rename from lua/codecompanion/strategies/chat/tools/executor/cmd.lua rename to lua/codecompanion/strategies/chat/agents/executor/cmd.lua diff --git a/lua/codecompanion/strategies/chat/tools/executor/func.lua b/lua/codecompanion/strategies/chat/agents/executor/func.lua similarity index 91% rename from lua/codecompanion/strategies/chat/tools/executor/func.lua rename to lua/codecompanion/strategies/chat/agents/executor/func.lua index 7c44350db..cbaa35715 100644 --- a/lua/codecompanion/strategies/chat/tools/executor/func.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/func.lua @@ -22,7 +22,6 @@ function FuncExecutor:orchestrate(input) local action = self.executor.tool.request.action -- Allow the cmds table to have multiple functions if type(action) == "table" and type(action[1]) == "table" then - local output for _, a in ipairs(action) do self:run(self.cmd, a, input) end @@ -33,15 +32,16 @@ end ---Run the tool function ---@param cmd fun(self: CodeCompanion.Agent, actions: table, input: any) ----@param action string +---@param action table ---@param input? any function FuncExecutor:run(cmd, action, input) local ok, output = pcall(function() return cmd(self.executor.agent, action, input) end) if not ok then - -- do something + return self.executor:error(action, output) end + self.executor:success(action, output) return self.executor:execute(self.index + 1, output) end diff --git a/lua/codecompanion/strategies/chat/tools/executor/init.lua b/lua/codecompanion/strategies/chat/agents/executor/init.lua similarity index 77% rename from lua/codecompanion/strategies/chat/tools/executor/init.lua rename to lua/codecompanion/strategies/chat/agents/executor/init.lua index 46a66af8b..d00f49780 100644 --- a/lua/codecompanion/strategies/chat/tools/executor/init.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/init.lua @@ -1,5 +1,5 @@ -local CmdExecutor = require("codecompanion.strategies.chat.tools.executor.cmd") -local FuncExecutor = require("codecompanion.strategies.chat.tools.executor.func") +local CmdExecutor = require("codecompanion.strategies.chat.agents.executor.cmd") +local FuncExecutor = require("codecompanion.strategies.chat.agents.executor.func") local config = require("codecompanion.config") local log = require("codecompanion.utils.log") local util = require("codecompanion.utils") @@ -13,11 +13,6 @@ local util = require("codecompanion.utils") ---@field status string local Executor = {} -local CONSTANTS = { - STATUS_ERROR = "error", - STATUS_SUCCESS = "success", -} - ---@param agent CodeCompanion.Agent ---@param tool CodeCompanion.Agent.Tool function Executor.new(agent, tool) @@ -25,9 +20,6 @@ function Executor.new(agent, tool) local self = setmetatable({ agent = agent, tool = tool, - status = CONSTANTS.STATUS_SUCCESS, - stderr = {}, - stdout = {}, }, { __index = Executor }) self.handlers = { @@ -81,7 +73,11 @@ end ---@return nil function Executor:execute(index, input) index = index or 1 - if index > vim.tbl_count(self.tool.cmds) or self.status == CONSTANTS.STATUS_ERROR then + if + not self.tool.cmds + or index > vim.tbl_count(self.tool.cmds) + or self.agent.status == self.agent.constants.STATUS_ERROR + then return self:close() end @@ -100,6 +96,27 @@ function Executor:requires_approval() or false end +---Handle an error from a tool +---@param action table +---@param error string +---@return nil +function Executor:error(action, error) + self.agent.status = self.agent.constants.STATUS_ERROR + table.insert(self.agent.stderr, error) + self.output.error(action, error) + log:error("Error calling function in %s: %s", self.tool.name, error) + self:close() +end + +---Handle a successful completion of a tool +---@param action table +---@param output string +---@return nil +function Executor:success(action, output) + table.insert(self.agent.stdout, output) + self.output.success(action, output) +end + ---Close the execution of the tool ---@return nil function Executor:close() @@ -108,22 +125,10 @@ function Executor:close() util.fire("AgentFinished", { name = self.tool.name, bufnr = self.agent.bufnr, - status = self.status, - stderr = self.stderr, - stdout = self.stdout, }) - self:reset() self.agent.chat.subscribers:process(self.agent.chat) vim.g.codecompanion_current_tool = nil end ----Reset the executor class ----@return nil -function Executor:reset() - self.status = CONSTANTS.STATUS_SUCCESS - self.stderr = {} - self.stdout = {} -end - return Executor diff --git a/lua/codecompanion/strategies/chat/tools/init.lua b/lua/codecompanion/strategies/chat/agents/init.lua similarity index 86% rename from lua/codecompanion/strategies/chat/tools/init.lua rename to lua/codecompanion/strategies/chat/agents/init.lua index 99813d2c6..1a8809c01 100644 --- a/lua/codecompanion/strategies/chat/tools/init.lua +++ b/lua/codecompanion/strategies/chat/agents/init.lua @@ -1,4 +1,18 @@ -local Executor = require("codecompanion.strategies.chat.tools.executor") +---@class CodeCompanion.Agent +---@field agent_config table The agent strategy from the config +---@field aug number The augroup for the tool +---@field bufnr number The buffer of the chat buffer +---@field constants table The constants for the tool +---@field chat CodeCompanion.Chat The chat buffer that initiated the tool +---@field extracted table The extracted tools from the LLM's response +---@field messages table The messages in the chat buffer +---@field status string The status of the tool +---@field stdout table The stdout of the tool +---@field stderr table The stderr of the tool +---@field tool CodeCompanion.Agent.Tool The current tool that's being run +---@field tools_ns integer The namespace for the virtual text that appears in the header + +local Executor = require("codecompanion.strategies.chat.agents.executor") local TreeHandler = require("codecompanion.utils.xml.xmlhandler.tree") local config = require("codecompanion.config") local log = require("codecompanion.utils.log") @@ -47,27 +61,30 @@ local function parse_xml(message) end ---@class CodeCompanion.Agent -local Tools = {} +local Agent = {} ---@param args table -function Tools.new(args) +function Agent.new(args) local self = setmetatable({ aug = api.nvim_create_augroup(CONSTANTS.AUTOCMD_GROUP .. ":" .. args.bufnr, { clear = true }), bufnr = args.bufnr, chat = {}, + constants = CONSTANTS, extracted = {}, messages = args.messages, + stdout = {}, + stderr = {}, tool = {}, agent_config = config.strategies.chat.agents, tools_ns = api.nvim_create_namespace(CONSTANTS.NS_TOOLS), - }, { __index = Tools }) + }, { __index = Agent }) return self end ---Set the autocmds for the tool ---@return nil -function Tools:set_autocmds() +function Agent:set_autocmds() api.nvim_create_autocmd("User", { desc = "Handle responses from an Agent", group = self.aug, @@ -116,7 +133,7 @@ end ---@param start_range number ---@param end_range number ---@return nil -function Tools:parse_buffer(chat, start_range, end_range) +function Agent:parse_buffer(chat, start_range, end_range) local query = vim.treesitter.query.get("markdown", "tools") local tree = chat.parser:parse({ start_range - 1, end_range - 1 })[1] @@ -160,7 +177,7 @@ end ---@param chat CodeCompanion.Chat ---@param xml string The XML schema from the LLM's response ---@return nil -function Tools:setup(chat, xml) +function Agent:setup(chat, xml) self.chat = chat local ok, schema = pcall(parse_xml, xml) @@ -172,10 +189,15 @@ function Tools:setup(chat, xml) ---Resolve and run the tool ---@param s table The tool's schema local function run_tool(s) + -- If an error occurred, don't run any more tools + if self.status == CONSTANTS.STATUS_ERROR then + return + end + ---@type CodeCompanion.Tool|nil local resolved_tool ok, resolved_tool = pcall(function() - return Tools.resolve(self.agent_config.tools[s.tool._attr.name]) + return Agent.resolve(self.agent_config.tools[s.tool._attr.name]) end) if not ok or not resolved_tool then log:error("Couldn't resolve the tool(s) from the LLM's response") @@ -203,16 +225,16 @@ function Tools:setup(chat, xml) vim.iter(schema.tool):each(function(tool) run_tool({ tool = tool }) end) - return + else + return run_tool(schema) end - return run_tool(schema) end ---Look for tools in a given message ---@param chat CodeCompanion.Chat ---@param message table ---@return table?, table? -function Tools:find(chat, message) +function Agent:find(chat, message) if not message.content then return nil, nil end @@ -264,7 +286,7 @@ end ---@param chat CodeCompanion.Chat ---@param message table ---@return boolean -function Tools:parse(chat, message) +function Agent:parse(chat, message) local tools, agents = self:find(chat, message) if tools or agents then @@ -293,7 +315,7 @@ end ---Replace the tool tag in a given message ---@param message string ---@return string -function Tools:replace(message) +function Agent:replace(message) for tool, _ in pairs(self.agent_config.tools) do message = vim.trim(message:gsub(CONSTANTS.PREFIX .. tool, tool)) end @@ -306,16 +328,21 @@ end ---Reset the tool class ---@return nil -function Tools:reset() +function Agent:reset() api.nvim_buf_clear_namespace(self.bufnr, self.tools_ns, 0, -1) api.nvim_clear_autocmds({ group = self.aug }) + self.extracted = {} + self.status = CONSTANTS.STATUS_SUCCESS + self.stderr = {} + self.stdout = {} + log:info("[Agent] Completed") end ---Fold any XML code blocks in the buffer ---@return nil -function Tools:fold_xml() +function Agent:fold_xml() local query = vim.treesitter.query.parse( "markdown", [[ @@ -351,7 +378,7 @@ end ---Add an error message to the chat buffer ---@param error string ---@return CodeCompanion.Agent -function Tools:add_error_to_chat(error) +function Agent:add_error_to_chat(error) self.chat:add_message({ role = config.constants.USER_ROLE, content = error, @@ -373,7 +400,7 @@ end ---Resolve a tool from the config ---@param tool table The tool from the config ---@return CodeCompanion.Tool|nil -function Tools.resolve(tool) +function Agent.resolve(tool) local callback = tool.callback if type(callback) == "table" then @@ -398,4 +425,4 @@ function Tools.resolve(tool) end end -return Tools +return Agent diff --git a/lua/codecompanion/strategies/chat/tools/cmd_runner.lua b/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua similarity index 100% rename from lua/codecompanion/strategies/chat/tools/cmd_runner.lua rename to lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua diff --git a/lua/codecompanion/strategies/chat/tools/editor.lua b/lua/codecompanion/strategies/chat/agents/tools/editor.lua similarity index 98% rename from lua/codecompanion/strategies/chat/tools/editor.lua rename to lua/codecompanion/strategies/chat/agents/tools/editor.lua index 311c6e8a6..c8f5808c4 100644 --- a/lua/codecompanion/strategies/chat/tools/editor.lua +++ b/lua/codecompanion/strategies/chat/agents/tools/editor.lua @@ -102,9 +102,9 @@ return { name = "editor", cmds = { ---Ensure the final function returns the status and the output - ---@param self CodeCompanion.Agent The Tools object + ---@param self CodeCompanion.Agent.Tool The Tools object ---@param actions table The action object - ---@param input any The output from the previous function call + ---@param input? any The output from the previous function call ---@return { status: string, msg: string } function(self, actions, input) ---Run the action diff --git a/lua/codecompanion/strategies/chat/tools/files.lua b/lua/codecompanion/strategies/chat/agents/tools/files.lua similarity index 99% rename from lua/codecompanion/strategies/chat/tools/files.lua rename to lua/codecompanion/strategies/chat/agents/tools/files.lua index 3d3f0d472..179998738 100644 --- a/lua/codecompanion/strategies/chat/tools/files.lua +++ b/lua/codecompanion/strategies/chat/agents/tools/files.lua @@ -156,7 +156,7 @@ return { actions = actions, cmds = { ---Execute the file commands - ---@param self CodeCompanion.Tools The Tools object + ---@param self CodeCompanion.Agent.Tool The Tools object ---@param action table The action object ---@param input any The output from the previous function call ---@return { status: string, msg: string } @@ -384,7 +384,7 @@ Remember: end, handlers = { ---Approve the command to be run - ---@param self CodeCompanion.Tools The tool object + ---@param self CodeCompanion.Agent The tool object ---@param action table ---@return boolean approved = function(self, action) diff --git a/lua/codecompanion/strategies/chat/tools/rag.lua b/lua/codecompanion/strategies/chat/agents/tools/rag.lua similarity index 100% rename from lua/codecompanion/strategies/chat/tools/rag.lua rename to lua/codecompanion/strategies/chat/agents/tools/rag.lua diff --git a/lua/codecompanion/strategies/chat/init.lua b/lua/codecompanion/strategies/chat/init.lua index 4ee6e64dd..f504ffcf8 100644 --- a/lua/codecompanion/strategies/chat/init.lua +++ b/lua/codecompanion/strategies/chat/init.lua @@ -4,6 +4,7 @@ The Chat Buffer - This is where all of the logic for conversing with an LLM sits ---@class CodeCompanion.Chat ---@field adapter CodeCompanion.Adapter The adapter to use for the chat +---@field agents CodeCompanion.Agent The agent that calls tools available to the user ---@field aug number The ID for the autocmd group ---@field bufnr integer The buffer number of the chat ---@field context table The context of the buffer that the chat was initiated from @@ -23,7 +24,6 @@ The Chat Buffer - This is where all of the logic for conversing with an LLM sits ---@field subscribers table The subscribers to the chat buffer ---@field tokens? nil|number The number of tokens in the chat ---@field tool_flags table Flags that external functions can update and subscribers can interact with ----@field tools? CodeCompanion.Tools The tools available to the user ---@field tools_in_use? nil|table The tools that are currently being used in the chat ---@field ui CodeCompanion.Chat.UI The UI of the chat buffer ---@field variables? CodeCompanion.Variables The variables available to the user @@ -252,7 +252,7 @@ function Chat.new(args) self.references = require("codecompanion.strategies.chat.references").new({ chat = self }) self.subscribers = require("codecompanion.strategies.chat.subscribers").new() - self.tools = require("codecompanion.strategies.chat.tools").new({ bufnr = self.bufnr, messages = self.messages }) + self.agents = require("codecompanion.strategies.chat.agents").new({ bufnr = self.bufnr, messages = self.messages }) self.watchers = require("codecompanion.strategies.chat.watchers").new() self.variables = require("codecompanion.strategies.chat.variables").new() @@ -629,7 +629,7 @@ function Chat:add_tool(tool, tool_config) self.tools_in_use[tool] = true - local resolved = self.tools.resolve(tool_config) + local resolved = self.agents.resolve(tool_config) if resolved then self:add_message( { role = config.constants.SYSTEM_ROLE, content = resolved.system_prompt(resolved.schema) }, @@ -672,8 +672,8 @@ end ---@param message table ---@return nil function Chat:apply_tools_and_variables(message) - if self.tools:parse(self, message) then - message.content = self.tools:replace(message.content) + if self.agents:parse(self, message) then + message.content = self.agents:replace(message.content) end if self.variables:parse(self, message) then message.content = self.variables:replace(message.content, self.context.bufnr) @@ -832,7 +832,7 @@ function Chat:done(output) -- If we're running any tooling, let them handle the subscriptions instead if self.status == CONSTANTS.STATUS_SUCCESS and self:has_tools() then - self.tools:parse_buffer(self, assistant_range, self.header_line - 1) + self.agents:parse_buffer(self, assistant_range, self.header_line - 1) else self.subscribers:process(self) end diff --git a/lua/codecompanion/types.lua b/lua/codecompanion/types.lua index 55d2df2db..f2c5fdce5 100644 --- a/lua/codecompanion/types.lua +++ b/lua/codecompanion/types.lua @@ -121,16 +121,6 @@ ---@field output.success? fun(self: CodeCompanion.Agent, cmd: table, output: table|string): any Function to call if the tool is successful ---@field request table The request from the LLM to use the Tool ----@class CodeCompanion.Agent ----@field aug number The augroup for the tool ----@field bufnr number The buffer of the chat buffer ----@field chat CodeCompanion.Chat The chat buffer that initiated the tool ----@field extracted table The extracted tools from the LLM's response ----@field messages table The messages in the chat buffer ----@field tool CodeCompanion.Agent.Tool The current tool that's being run ----@field agent_config table The agent strategy from the config ----@field tools_ns integer The namespace for the virtual text that appears in the header - ---@class CodeCompanion.SlashCommand.Provider ---@field output function The function to call when a selection is made ---@field provider table The path to the provider diff --git a/tests/config.lua b/tests/config.lua index bf5e7ba77..8fe8a4cdc 100644 --- a/tests/config.lua +++ b/tests/config.lua @@ -72,6 +72,10 @@ return { callback = vim.fn.getcwd() .. "/tests/strategies/chat/tools/tools/stubs/func_consecutive.lua", description = "Consecutive function tool to test", }, + ["func_error"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/tools/tools/stubs/func_error.lua", + description = "Error function tool to test", + }, opts = { system_prompt = [[My tool system prompt]], }, diff --git a/tests/helpers.lua b/tests/helpers.lua index ccbd2f535..9d8efb0da 100644 --- a/tests/helpers.lua +++ b/tests/helpers.lua @@ -48,7 +48,7 @@ Helpers.setup_chat_buffer = function(config, adapter) description = "foo", }, } - local tools = require("codecompanion.strategies.chat.tools").new({ bufnr = 1 }) + local agent = require("codecompanion.strategies.chat.agents").new({ bufnr = 1 }) local vars = require("codecompanion.strategies.chat.variables").new() package.loaded["codecompanion.utils.foo"] = { @@ -81,7 +81,7 @@ Helpers.setup_chat_buffer = function(config, adapter) end, } - return chat, tools, vars + return chat, agent, vars end ---Mock the sending of a chat buffer to an LLM diff --git a/tests/strategies/chat/test_tools.lua b/tests/strategies/chat/test_agent.lua similarity index 69% rename from tests/strategies/chat/test_tools.lua rename to tests/strategies/chat/test_agent.lua index e70098388..c55fec749 100644 --- a/tests/strategies/chat/test_tools.lua +++ b/tests/strategies/chat/test_agent.lua @@ -3,12 +3,12 @@ local h = require("tests.helpers") local new_set = MiniTest.new_set local T = new_set() -local chat, tools +local chat, agent -T["Tools"] = new_set({ +T["Agent"] = new_set({ hooks = { pre_case = function() - chat, tools = h.setup_chat_buffer() + chat, agent = h.setup_chat_buffer() end, post_case = function() h.teardown_chat_buffer() @@ -16,9 +16,9 @@ T["Tools"] = new_set({ }, }) --- T["Tools"]["resolve"] = new_set() +-- T["Agent"]["resolve"] = new_set() -- --- T["Tools"]["resolve"]["can resolve built-in tools"] = function() +-- T["Agent"]["resolve"]["can resolve built-in tools"] = function() -- local tool = tools.resolve({ -- callback = "strategies.chat.tools.editor", -- description = "Update a buffer with the LLM's response", @@ -29,7 +29,7 @@ T["Tools"] = new_set({ -- h.eq(6, #tool.schema) -- end -- --- T["Tools"]["resolve"]["can resolve user's tools"] = function() +-- T["Agent"]["resolve"]["can resolve user's tools"] = function() -- local tool = tools.resolve({ -- callback = vim.fn.getcwd() .. "/tests/stubs/foo.lua", -- description = "Some foo function", @@ -40,9 +40,9 @@ T["Tools"] = new_set({ -- h.eq("This is the Foo tool", tool.cmds[1]()) -- end -- --- T["Tools"][":parse"] = new_set() +-- T["Agent"][":parse"] = new_set() -- --- T["Tools"][":parse"]["a message with a tool"] = function() +-- T["Agent"][":parse"]["a message with a tool"] = function() -- table.insert(chat.messages, { -- role = "user", -- content = "@foo do some stuff", @@ -54,7 +54,7 @@ T["Tools"] = new_set({ -- h.eq("my foo system prompt", messages[#messages].content) -- end -- --- T["Tools"][":parse"]["a response from the LLM"] = function() +-- T["Agent"][":parse"]["a response from the LLM"] = function() -- chat:add_buf_message({ -- role = "user", -- content = "@foo do some stuff", @@ -79,7 +79,7 @@ T["Tools"] = new_set({ -- h.eq("This is from the foo tool", lines[#lines]) -- end -- --- T["Tools"][":parse"]["a nested response from the LLM"] = function() +-- T["Agent"][":parse"]["a nested response from the LLM"] = function() -- chat:add_buf_message({ -- role = "user", -- content = "@foo @bar do some stuff", @@ -107,19 +107,19 @@ T["Tools"] = new_set({ -- h.eq("This is from the foo toolThis is from the bar tool", lines[#lines]) -- end -- --- T["Tools"][":replace"] = new_set() +-- T["Agent"][":replace"] = new_set() -- --- T["Tools"][":replace"]["should replace the tool in the message"] = function() +-- T["Agent"][":replace"]["should replace the tool in the message"] = function() -- local message = "run the @foo tool" -- local result = tools:replace(message, "foo") -- h.eq("run the foo tool", result) -- end -T["Tools"][":setup"] = new_set() +T["Agent"][":setup"] = new_set() -T["Tools"][":setup"]["can run functions"] = function() +T["Agent"][":setup"]["can run functions"] = function() h.eq(vim.g.codecompanion_test, nil) - tools:setup( + agent:setup( chat, [[ @@ -132,9 +132,9 @@ T["Tools"][":setup"]["can run functions"] = function() vim.g.codecompanion_test = nil end -T["Tools"][":setup"]["can run consecutive functions"] = function() +T["Agent"][":setup"]["can run consecutive functions and pass input"] = function() h.eq(vim.g.codecompanion_test, nil) - tools:setup( + agent:setup( chat, [[ @@ -144,11 +144,13 @@ T["Tools"][":setup"]["can run consecutive functions"] = function() ) h.eq("Data 1 Data 1", vim.g.codecompanion_test) vim.g.codecompanion_test = nil + h.eq("Ran with success", vim.g.codecompanion_test_output) + vim.g.codecompanion_test_output = nil end -T["Tools"][":setup"]["can run multiple, consecutive functions"] = function() +T["Agent"][":setup"]["can run multiple, consecutive functions"] = function() h.eq(vim.g.codecompanion_test, nil) - tools:setup( + agent:setup( chat, [[ @@ -161,4 +163,24 @@ T["Tools"][":setup"]["can run multiple, consecutive functions"] = function() vim.g.codecompanion_test = nil end +T["Agent"][":setup"]["can handle errors in functions"] = function() + -- Stop this from clearing out stderr + function agent:reset() + return nil + end + + agent:setup( + chat, + [[ + + Data 1 + +]] + ) + + h.eq({ "Something went wrong" }, agent.stderr) + h.eq("Something went wrong", vim.g.codecompanion_test_output) + vim.g.codecompanion_test_output = nil +end + return T diff --git a/tests/strategies/chat/tools/test_files.lua b/tests/strategies/chat/tools/test_files.lua index 09d9156ee..ed3cd3697 100644 --- a/tests/strategies/chat/tools/test_files.lua +++ b/tests/strategies/chat/tools/test_files.lua @@ -1,4 +1,4 @@ -local files = require("codecompanion.strategies.chat.tools.files") +local files = require("codecompanion.strategies.chat.agents.tools.files") local h = require("tests.helpers") diff --git a/tests/strategies/chat/tools/tools/stubs/func.lua b/tests/strategies/chat/tools/tools/stubs/func.lua index 825d9ac1f..1ebfe215a 100644 --- a/tests/strategies/chat/tools/tools/stubs/func.lua +++ b/tests/strategies/chat/tools/tools/stubs/func.lua @@ -12,4 +12,9 @@ return { vim.g.codecompanion_test = (vim.g.codecompanion_test or "") .. spacer .. actions.data end, }, + output = { + success = function(self, cmd, output) + vim.g.codecompanion_test_output = "Ran with success" + end, + }, } diff --git a/tests/strategies/chat/tools/tools/stubs/func_error.lua b/tests/strategies/chat/tools/tools/stubs/func_error.lua new file mode 100644 index 000000000..3cf7531bb --- /dev/null +++ b/tests/strategies/chat/tools/tools/stubs/func_error.lua @@ -0,0 +1,16 @@ +return { + name = "func_error", + system_prompt = function(schema) + return "my func system prompt" + end, + cmds = { + function(self, actions, input) + return error("Something went wrong") + end, + }, + output = { + error = function(self, cmd, error) + vim.g.codecompanion_test_output = "" .. error .. "" + end, + }, +} From 26af6f6e10e327d3de573814dfcb41d9ccae6d9f Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Thu, 20 Feb 2025 22:17:10 +0000 Subject: [PATCH 03/38] wip: add back other agent tests --- tests/strategies/chat/test_agent.lua | 196 +++++++++++++-------------- 1 file changed, 98 insertions(+), 98 deletions(-) diff --git a/tests/strategies/chat/test_agent.lua b/tests/strategies/chat/test_agent.lua index c55fec749..feacd49ab 100644 --- a/tests/strategies/chat/test_agent.lua +++ b/tests/strategies/chat/test_agent.lua @@ -16,104 +16,104 @@ T["Agent"] = new_set({ }, }) --- T["Agent"]["resolve"] = new_set() --- --- T["Agent"]["resolve"]["can resolve built-in tools"] = function() --- local tool = tools.resolve({ --- callback = "strategies.chat.tools.editor", --- description = "Update a buffer with the LLM's response", --- }) --- --- h.eq(type(tool), "table") --- h.eq("editor", tool.name) --- h.eq(6, #tool.schema) --- end --- --- T["Agent"]["resolve"]["can resolve user's tools"] = function() --- local tool = tools.resolve({ --- callback = vim.fn.getcwd() .. "/tests/stubs/foo.lua", --- description = "Some foo function", --- }) --- --- h.eq(type(tool), "table") --- h.eq("foo", tool.name) --- h.eq("This is the Foo tool", tool.cmds[1]()) --- end --- --- T["Agent"][":parse"] = new_set() --- --- T["Agent"][":parse"]["a message with a tool"] = function() --- table.insert(chat.messages, { --- role = "user", --- content = "@foo do some stuff", --- }) --- tools:parse(chat, chat.messages[#chat.messages]) --- local messages = chat.messages --- --- h.eq("My tool system prompt", messages[#messages - 1].content) --- h.eq("my foo system prompt", messages[#messages].content) --- end --- --- T["Agent"][":parse"]["a response from the LLM"] = function() --- chat:add_buf_message({ --- role = "user", --- content = "@foo do some stuff", --- }) --- chat:add_buf_message({ --- role = "llm", --- content = [[Sure. Let's do this. --- --- ```xml --- --- --- Some foo function --- --- --- ``` --- ]], --- }) --- chat.tools.chat = chat --- chat.tools:parse_buffer(chat, 5, 100) --- --- local lines = h.get_buf_lines(chat.bufnr) --- h.eq("This is from the foo tool", lines[#lines]) --- end --- --- T["Agent"][":parse"]["a nested response from the LLM"] = function() --- chat:add_buf_message({ --- role = "user", --- content = "@foo @bar do some stuff", --- }) --- chat:add_buf_message({ --- role = "llm", --- content = [[Sure. Let's do this. --- --- ```xml --- --- --- Some foo function --- --- --- Some bar function --- --- --- ``` --- ]], --- }) --- chat.tools.chat = chat --- chat.tools:parse_buffer(chat, 5, 100) --- --- local lines = h.get_buf_lines(chat.bufnr) --- h.eq("This is from the foo toolThis is from the bar tool", lines[#lines]) --- end --- --- T["Agent"][":replace"] = new_set() --- --- T["Agent"][":replace"]["should replace the tool in the message"] = function() --- local message = "run the @foo tool" --- local result = tools:replace(message, "foo") --- h.eq("run the foo tool", result) --- end +T["Agent"]["resolve"] = new_set() + +T["Agent"]["resolve"]["can resolve built-in tools"] = function() + local tool = agent.resolve({ + callback = "strategies.chat.agents.tools.editor", + description = "Update a buffer with the LLM's response", + }) + + h.eq(type(tool), "table") + h.eq("editor", tool.name) + h.eq(6, #tool.schema) +end + +T["Agent"]["resolve"]["can resolve user's tools"] = function() + local tool = agent.resolve({ + callback = vim.fn.getcwd() .. "/tests/stubs/foo.lua", + description = "Some foo function", + }) + + h.eq(type(tool), "table") + h.eq("foo", tool.name) + h.eq("This is the Foo tool", tool.cmds[1]()) +end + +T["Agent"][":parse"] = new_set() + +T["Agent"][":parse"]["a message with a tool"] = function() + table.insert(chat.messages, { + role = "user", + content = "@foo do some stuff", + }) + agent:parse(chat, chat.messages[#chat.messages]) + local messages = chat.messages + + h.eq("My tool system prompt", messages[#messages - 1].content) + h.eq("my foo system prompt", messages[#messages].content) +end + +T["Agent"][":parse"]["a response from the LLM"] = function() + chat:add_buf_message({ + role = "user", + content = "@foo do some stuff", + }) + chat:add_buf_message({ + role = "llm", + content = [[Sure. Let's do this. + +```xml + + + Some foo function + + +``` +]], + }) + chat.agents.chat = chat + chat.agents:parse_buffer(chat, 5, 100) + + local lines = h.get_buf_lines(chat.bufnr) + h.eq("This is from the foo tool", lines[#lines]) +end + +T["Agent"][":parse"]["a nested response from the LLM"] = function() + chat:add_buf_message({ + role = "user", + content = "@foo @bar do some stuff", + }) + chat:add_buf_message({ + role = "llm", + content = [[Sure. Let's do this. + +```xml + + + Some foo function + + + Some bar function + + +``` +]], + }) + chat.agents.chat = chat + chat.agents:parse_buffer(chat, 5, 100) + + local lines = h.get_buf_lines(chat.bufnr) + h.eq("This is from the foo toolThis is from the bar tool", lines[#lines]) +end + +T["Agent"][":replace"] = new_set() + +T["Agent"][":replace"]["should replace the tool in the message"] = function() + local message = "run the @foo tool" + local result = agent:replace(message, "foo") + h.eq("run the foo tool", result) +end T["Agent"][":setup"] = new_set() From b4e9d7ecc2fa4d25121b6760193664a9d0b59f9b Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Thu, 20 Feb 2025 22:20:31 +0000 Subject: [PATCH 04/38] wip: move test files --- tests/config.lua | 6 +++--- .../chat/{test_agent.lua => agents/test_agents.lua} | 0 tests/strategies/chat/{tools => agents}/test_files.lua | 0 .../strategies/chat/{tools => agents}/tools/stubs/func.lua | 0 .../chat/{tools => agents}/tools/stubs/func_consecutive.lua | 0 .../chat/{tools => agents}/tools/stubs/func_error.lua | 0 6 files changed, 3 insertions(+), 3 deletions(-) rename tests/strategies/chat/{test_agent.lua => agents/test_agents.lua} (100%) rename tests/strategies/chat/{tools => agents}/test_files.lua (100%) rename tests/strategies/chat/{tools => agents}/tools/stubs/func.lua (100%) rename tests/strategies/chat/{tools => agents}/tools/stubs/func_consecutive.lua (100%) rename tests/strategies/chat/{tools => agents}/tools/stubs/func_error.lua (100%) diff --git a/tests/config.lua b/tests/config.lua index 8fe8a4cdc..5d7d8af75 100644 --- a/tests/config.lua +++ b/tests/config.lua @@ -65,15 +65,15 @@ return { description = "Some bar_again function", }, ["func"] = { - callback = vim.fn.getcwd() .. "/tests/strategies/chat/tools/tools/stubs/func.lua", + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func.lua", description = "Some function tool to test", }, ["func_consecutive"] = { - callback = vim.fn.getcwd() .. "/tests/strategies/chat/tools/tools/stubs/func_consecutive.lua", + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func_consecutive.lua", description = "Consecutive function tool to test", }, ["func_error"] = { - callback = vim.fn.getcwd() .. "/tests/strategies/chat/tools/tools/stubs/func_error.lua", + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func_error.lua", description = "Error function tool to test", }, opts = { diff --git a/tests/strategies/chat/test_agent.lua b/tests/strategies/chat/agents/test_agents.lua similarity index 100% rename from tests/strategies/chat/test_agent.lua rename to tests/strategies/chat/agents/test_agents.lua diff --git a/tests/strategies/chat/tools/test_files.lua b/tests/strategies/chat/agents/test_files.lua similarity index 100% rename from tests/strategies/chat/tools/test_files.lua rename to tests/strategies/chat/agents/test_files.lua diff --git a/tests/strategies/chat/tools/tools/stubs/func.lua b/tests/strategies/chat/agents/tools/stubs/func.lua similarity index 100% rename from tests/strategies/chat/tools/tools/stubs/func.lua rename to tests/strategies/chat/agents/tools/stubs/func.lua diff --git a/tests/strategies/chat/tools/tools/stubs/func_consecutive.lua b/tests/strategies/chat/agents/tools/stubs/func_consecutive.lua similarity index 100% rename from tests/strategies/chat/tools/tools/stubs/func_consecutive.lua rename to tests/strategies/chat/agents/tools/stubs/func_consecutive.lua diff --git a/tests/strategies/chat/tools/tools/stubs/func_error.lua b/tests/strategies/chat/agents/tools/stubs/func_error.lua similarity index 100% rename from tests/strategies/chat/tools/tools/stubs/func_error.lua rename to tests/strategies/chat/agents/tools/stubs/func_error.lua From a989a5b7dae69df730f2162921385c0d3a77ec93 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Thu, 20 Feb 2025 22:32:56 +0000 Subject: [PATCH 05/38] wip: update tests --- tests/strategies/chat/agents/test_agents.lua | 27 ++++++++++++++----- .../chat/agents/tools/stubs/func.lua | 5 ++++ .../chat/agents/{ => tools}/test_files.lua | 0 3 files changed, 25 insertions(+), 7 deletions(-) rename tests/strategies/chat/agents/{ => tools}/test_files.lua (100%) diff --git a/tests/strategies/chat/agents/test_agents.lua b/tests/strategies/chat/agents/test_agents.lua index feacd49ab..28c27c64c 100644 --- a/tests/strategies/chat/agents/test_agents.lua +++ b/tests/strategies/chat/agents/test_agents.lua @@ -12,6 +12,9 @@ T["Agent"] = new_set({ end, post_case = function() h.teardown_chat_buffer() + vim.g.codecompanion_test = nil + vim.g.codecompanion_test_exit = nil + vim.g.codecompanion_test_output = nil end, }, }) @@ -118,6 +121,7 @@ end T["Agent"][":setup"] = new_set() T["Agent"][":setup"]["can run functions"] = function() + h.eq(vim.g.codecompanion_test_exit, nil) h.eq(vim.g.codecompanion_test, nil) agent:setup( chat, @@ -128,8 +132,15 @@ T["Agent"][":setup"]["can run functions"] = function() ]] ) + + -- Test that the function was called h.eq("Data 1 Data 2", vim.g.codecompanion_test) - vim.g.codecompanion_test = nil + + -- Test that the on_exit handler was called + h.eq(vim.g.codecompanion_test_exit, "Exited") + + -- Test `output.success` handler + h.eq("Ran with success", vim.g.codecompanion_test_output) end T["Agent"][":setup"]["can run consecutive functions and pass input"] = function() @@ -142,10 +153,9 @@ T["Agent"][":setup"]["can run consecutive functions and pass input"] = function( ]] ) + + -- Test that the function was called h.eq("Data 1 Data 1", vim.g.codecompanion_test) - vim.g.codecompanion_test = nil - h.eq("Ran with success", vim.g.codecompanion_test_output) - vim.g.codecompanion_test_output = nil end T["Agent"][":setup"]["can run multiple, consecutive functions"] = function() @@ -159,12 +169,13 @@ T["Agent"][":setup"]["can run multiple, consecutive functions"] = function() ]] ) + + -- Test that the function was called, overwriting the global variable h.eq("Data 2 Data 2", vim.g.codecompanion_test) - vim.g.codecompanion_test = nil end T["Agent"][":setup"]["can handle errors in functions"] = function() - -- Stop this from clearing out stderr + -- Prevent stderr from being cleared out function agent:reset() return nil end @@ -178,9 +189,11 @@ T["Agent"][":setup"]["can handle errors in functions"] = function() ]] ) + -- Test that stderr is updated on the agent h.eq({ "Something went wrong" }, agent.stderr) + + -- Test that the `output.error` handler was called h.eq("Something went wrong", vim.g.codecompanion_test_output) - vim.g.codecompanion_test_output = nil end return T diff --git a/tests/strategies/chat/agents/tools/stubs/func.lua b/tests/strategies/chat/agents/tools/stubs/func.lua index 1ebfe215a..44a1e37c4 100644 --- a/tests/strategies/chat/agents/tools/stubs/func.lua +++ b/tests/strategies/chat/agents/tools/stubs/func.lua @@ -12,6 +12,11 @@ return { vim.g.codecompanion_test = (vim.g.codecompanion_test or "") .. spacer .. actions.data end, }, + handlers = { + on_exit = function(self) + vim.g.codecompanion_test_exit = "Exited" + end, + }, output = { success = function(self, cmd, output) vim.g.codecompanion_test_output = "Ran with success" diff --git a/tests/strategies/chat/agents/test_files.lua b/tests/strategies/chat/agents/tools/test_files.lua similarity index 100% rename from tests/strategies/chat/agents/test_files.lua rename to tests/strategies/chat/agents/tools/test_files.lua From 48c67bb2c3d0764756e10836704f03b8658401c1 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Fri, 21 Feb 2025 18:32:49 +0000 Subject: [PATCH 06/38] wip: on_exit only called once + more test cases --- .../strategies/chat/agents/executor/func.lua | 37 ++++- .../strategies/chat/agents/executor/init.lua | 6 +- .../strategies/chat/agents/init.lua | 6 +- tests/log.lua | 11 ++ .../chat/agents/executor/test_func.lua | 137 ++++++++++++++++++ tests/strategies/chat/agents/test_agents.lua | 78 ---------- .../chat/agents/tools/stubs/func.lua | 2 +- 7 files changed, 188 insertions(+), 89 deletions(-) create mode 100644 tests/log.lua create mode 100644 tests/strategies/chat/agents/executor/test_func.lua diff --git a/lua/codecompanion/strategies/chat/agents/executor/func.lua b/lua/codecompanion/strategies/chat/agents/executor/func.lua index cbaa35715..bba8f8d9f 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/func.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/func.lua @@ -1,3 +1,5 @@ +local log = require("codecompanion.utils.log") + ---@class CodeCompanion.Agent.Executor.Func ---@field executor CodeCompanion.Agent.Executor ---@field cmd fun(self: CodeCompanion.Agent, actions: table, input: any) @@ -19,30 +21,53 @@ end ---@param input any ---@return nil function FuncExecutor:orchestrate(input) + log:debug("FuncExecutor:orchestrate %s", self.index) local action = self.executor.tool.request.action - -- Allow the cmds table to have multiple functions if type(action) == "table" and type(action[1]) == "table" then - for _, a in ipairs(action) do - self:run(self.cmd, a, input) + ---Process all actions in sequence without creating new execution chains + ---@param idx number The index + ---@param prev_input? any + ---@return nil + local function process_actions(idx, prev_input) + if idx > #action then + -- All actions processed, continue to next command + return self.executor:execute(self.index + 1, prev_input) + end + + -- Allow the action to call the next action directly, without calling `Executor:execute` + self:run(self.cmd, action[idx], prev_input, function(output) + process_actions(idx + 1, output) + end) end + + process_actions(1, input) else self:run(self.cmd, action, input) end end ----Run the tool function +---Run the tool's function ---@param cmd fun(self: CodeCompanion.Agent, actions: table, input: any) ---@param action table ---@param input? any -function FuncExecutor:run(cmd, action, input) +---@param callback? fun(output: any) +---@return nil +function FuncExecutor:run(cmd, action, input, callback) + log:debug("FuncExecutor:run") local ok, output = pcall(function() return cmd(self.executor.agent, action, input) end) if not ok then return self.executor:error(action, output) end + self.executor:success(action, output) - return self.executor:execute(self.index + 1, output) + + if callback then + callback(output) + else + return self.executor:execute(self.index + 1, output) + end end return FuncExecutor diff --git a/lua/codecompanion/strategies/chat/agents/executor/init.lua b/lua/codecompanion/strategies/chat/agents/executor/init.lua index d00f49780..a3bc6daec 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/init.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/init.lua @@ -16,7 +16,7 @@ local Executor = {} ---@param agent CodeCompanion.Agent ---@param tool CodeCompanion.Agent.Tool function Executor.new(agent, tool) - log:debug("Creating new Executor for tool: %s", tool.name) + log:debug("Executor.new: %s", tool.name) local self = setmetatable({ agent = agent, tool = tool, @@ -73,6 +73,7 @@ end ---@return nil function Executor:execute(index, input) index = index or 1 + log:debug("Executor:execute %s", index) if not self.tool.cmds or index > vim.tbl_count(self.tool.cmds) @@ -101,6 +102,7 @@ end ---@param error string ---@return nil function Executor:error(action, error) + log:debug("Executor:error") self.agent.status = self.agent.constants.STATUS_ERROR table.insert(self.agent.stderr, error) self.output.error(action, error) @@ -113,6 +115,7 @@ end ---@param output string ---@return nil function Executor:success(action, output) + log:debug("Executor:success") table.insert(self.agent.stdout, output) self.output.success(action, output) end @@ -120,6 +123,7 @@ end ---Close the execution of the tool ---@return nil function Executor:close() + log:debug("Executor:close") self.handlers.on_exit() util.fire("AgentFinished", { diff --git a/lua/codecompanion/strategies/chat/agents/init.lua b/lua/codecompanion/strategies/chat/agents/init.lua index 1a8809c01..b62aeec4b 100644 --- a/lua/codecompanion/strategies/chat/agents/init.lua +++ b/lua/codecompanion/strategies/chat/agents/init.lua @@ -168,16 +168,16 @@ function Agent:parse_buffer(chat, start_range, end_range) if not vim.tbl_isempty(tools) then self.extracted = tools vim.iter(tools):each(function(t) - return self:setup(chat, t) + return self:execute(chat, t) end) end end ----Setup the tool in the chat buffer based on the LLM's response +---Execute the tool in the chat buffer based on the LLM's response ---@param chat CodeCompanion.Chat ---@param xml string The XML schema from the LLM's response ---@return nil -function Agent:setup(chat, xml) +function Agent:execute(chat, xml) self.chat = chat local ok, schema = pcall(parse_xml, xml) diff --git a/tests/log.lua b/tests/log.lua new file mode 100644 index 000000000..837aca302 --- /dev/null +++ b/tests/log.lua @@ -0,0 +1,11 @@ +local log = require("codecompanion.utils.log") + +return log.set_root(log.new({ + handlers = { + { + type = "file", + filename = "codecompanion_test.log", + level = vim.log.levels["DEBUG"], + }, + }, +})) diff --git a/tests/strategies/chat/agents/executor/test_func.lua b/tests/strategies/chat/agents/executor/test_func.lua new file mode 100644 index 000000000..a79c92780 --- /dev/null +++ b/tests/strategies/chat/agents/executor/test_func.lua @@ -0,0 +1,137 @@ +require("tests.log") +local h = require("tests.helpers") + +local new_set = MiniTest.new_set +local T = new_set() + +local chat, agent + +T["Agent"] = new_set({ + hooks = { + pre_case = function() + chat, agent = h.setup_chat_buffer() + end, + post_case = function() + h.teardown_chat_buffer() + vim.g.codecompanion_test = nil + vim.g.codecompanion_test_exit = nil + vim.g.codecompanion_test_output = nil + end, + }, +}) + +T["Agent"]["functions"] = new_set() + +T["Agent"]["functions"]["can run functions"] = function() + h.eq(vim.g.codecompanion_test, nil) + agent:execute( + chat, + [[ + + Data 1 + Data 2 + +]] + ) + + -- Test that the function was called + h.eq("Data 1 Data 2", vim.g.codecompanion_test) +end + +T["Agent"]["functions"]["calls output.success when running functions"] = function() + h.eq(vim.g.codecompanion_test_output, nil) + agent:execute( + chat, + [[ + + Data 1 + Data 2 + +]] + ) + + -- Test `output.success` handler + h.eq("Ran with success", vim.g.codecompanion_test_output) +end + +T["Agent"]["functions"]["can call on_exit only once for functions"] = function() + h.eq(vim.g.codecompanion_test_exit, nil) + agent:execute( + chat, + [[ + + Data 1 + Data 2 + +]] + ) + + -- Test that the on_exit handler was called, once + h.eq(vim.g.codecompanion_test_exit, "Exited") +end + +T["Agent"]["functions"]["can run consecutive functions and pass input"] = function() + h.eq(vim.g.codecompanion_test, nil) + agent:execute( + chat, + [[ + + Data 1 + +]] + ) + + -- Test that the function was called + h.eq("Data 1 Data 1", vim.g.codecompanion_test) +end + +T["Agent"]["functions"]["can run multiple, consecutive functions"] = function() + h.eq(vim.g.codecompanion_test, nil) + agent:execute( + chat, + [[ + + Data 1 + Data 2 + +]] + ) + + -- Test that the function was called, overwriting the global variable + h.eq("Data 1 Data 2 Data 1 Data 2", vim.g.codecompanion_test) +end + +T["Agent"]["functions"]["can handle errors in functions"] = function() + agent:execute( + chat, + [[ + + Data 1 + +]] + ) + + -- Test that the `output.error` handler was called + h.eq("Something went wrong", vim.g.codecompanion_test_output) +end + +T["Agent"]["functions"]["can populate stderr in functions"] = function() + -- Prevent stderr from being cleared out + function agent:reset() + return nil + end + + agent:execute( + chat, + [[ + + Data 1 + +]] + ) + + -- Test that stderr is updated on the agent + h.eq({ "Something went wrong" }, agent.stderr) +end + +return T diff --git a/tests/strategies/chat/agents/test_agents.lua b/tests/strategies/chat/agents/test_agents.lua index 28c27c64c..49cbc34ee 100644 --- a/tests/strategies/chat/agents/test_agents.lua +++ b/tests/strategies/chat/agents/test_agents.lua @@ -118,82 +118,4 @@ T["Agent"][":replace"]["should replace the tool in the message"] = function() h.eq("run the foo tool", result) end -T["Agent"][":setup"] = new_set() - -T["Agent"][":setup"]["can run functions"] = function() - h.eq(vim.g.codecompanion_test_exit, nil) - h.eq(vim.g.codecompanion_test, nil) - agent:setup( - chat, - [[ - - Data 1 - Data 2 - -]] - ) - - -- Test that the function was called - h.eq("Data 1 Data 2", vim.g.codecompanion_test) - - -- Test that the on_exit handler was called - h.eq(vim.g.codecompanion_test_exit, "Exited") - - -- Test `output.success` handler - h.eq("Ran with success", vim.g.codecompanion_test_output) -end - -T["Agent"][":setup"]["can run consecutive functions and pass input"] = function() - h.eq(vim.g.codecompanion_test, nil) - agent:setup( - chat, - [[ - - Data 1 - -]] - ) - - -- Test that the function was called - h.eq("Data 1 Data 1", vim.g.codecompanion_test) -end - -T["Agent"][":setup"]["can run multiple, consecutive functions"] = function() - h.eq(vim.g.codecompanion_test, nil) - agent:setup( - chat, - [[ - - Data 1 - Data 2 - -]] - ) - - -- Test that the function was called, overwriting the global variable - h.eq("Data 2 Data 2", vim.g.codecompanion_test) -end - -T["Agent"][":setup"]["can handle errors in functions"] = function() - -- Prevent stderr from being cleared out - function agent:reset() - return nil - end - - agent:setup( - chat, - [[ - - Data 1 - -]] - ) - - -- Test that stderr is updated on the agent - h.eq({ "Something went wrong" }, agent.stderr) - - -- Test that the `output.error` handler was called - h.eq("Something went wrong", vim.g.codecompanion_test_output) -end - return T diff --git a/tests/strategies/chat/agents/tools/stubs/func.lua b/tests/strategies/chat/agents/tools/stubs/func.lua index 44a1e37c4..5c63ead25 100644 --- a/tests/strategies/chat/agents/tools/stubs/func.lua +++ b/tests/strategies/chat/agents/tools/stubs/func.lua @@ -14,7 +14,7 @@ return { }, handlers = { on_exit = function(self) - vim.g.codecompanion_test_exit = "Exited" + vim.g.codecompanion_test_exit = (vim.g.codecompanion_test_exit or "") .. "Exited" end, }, output = { From 3a265558352fe4a453aabb2ff0ee313030a86f18 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Fri, 21 Feb 2025 20:36:16 +0000 Subject: [PATCH 07/38] wip: add test for stdout --- .../chat/agents/executor/test_func.lua | 35 +++++++++++++++---- .../chat/agents/tools/stubs/func.lua | 3 ++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/tests/strategies/chat/agents/executor/test_func.lua b/tests/strategies/chat/agents/executor/test_func.lua index a79c92780..8735c38e7 100644 --- a/tests/strategies/chat/agents/executor/test_func.lua +++ b/tests/strategies/chat/agents/executor/test_func.lua @@ -22,7 +22,7 @@ T["Agent"] = new_set({ T["Agent"]["functions"] = new_set() -T["Agent"]["functions"]["can run functions"] = function() +T["Agent"]["functions"]["can run"] = function() h.eq(vim.g.codecompanion_test, nil) agent:execute( chat, @@ -38,7 +38,7 @@ T["Agent"]["functions"]["can run functions"] = function() h.eq("Data 1 Data 2", vim.g.codecompanion_test) end -T["Agent"]["functions"]["calls output.success when running functions"] = function() +T["Agent"]["functions"]["calls output.success"] = function() h.eq(vim.g.codecompanion_test_output, nil) agent:execute( chat, @@ -54,7 +54,7 @@ T["Agent"]["functions"]["calls output.success when running functions"] = functio h.eq("Ran with success", vim.g.codecompanion_test_output) end -T["Agent"]["functions"]["can call on_exit only once for functions"] = function() +T["Agent"]["functions"]["calls on_exit only once"] = function() h.eq(vim.g.codecompanion_test_exit, nil) agent:execute( chat, @@ -70,7 +70,7 @@ T["Agent"]["functions"]["can call on_exit only once for functions"] = function() h.eq(vim.g.codecompanion_test_exit, "Exited") end -T["Agent"]["functions"]["can run consecutive functions and pass input"] = function() +T["Agent"]["functions"]["can run consecutively and pass input"] = function() h.eq(vim.g.codecompanion_test, nil) agent:execute( chat, @@ -85,7 +85,7 @@ T["Agent"]["functions"]["can run consecutive functions and pass input"] = functi h.eq("Data 1 Data 1", vim.g.codecompanion_test) end -T["Agent"]["functions"]["can run multiple, consecutive functions"] = function() +T["Agent"]["functions"]["can run consecutively"] = function() h.eq(vim.g.codecompanion_test, nil) agent:execute( chat, @@ -101,7 +101,7 @@ T["Agent"]["functions"]["can run multiple, consecutive functions"] = function() h.eq("Data 1 Data 2 Data 1 Data 2", vim.g.codecompanion_test) end -T["Agent"]["functions"]["can handle errors in functions"] = function() +T["Agent"]["functions"]["can handle errors"] = function() agent:execute( chat, [[ @@ -115,7 +115,7 @@ T["Agent"]["functions"]["can handle errors in functions"] = function() h.eq("Something went wrong", vim.g.codecompanion_test_output) end -T["Agent"]["functions"]["can populate stderr in functions"] = function() +T["Agent"]["functions"]["can populate stderr"] = function() -- Prevent stderr from being cleared out function agent:reset() return nil @@ -134,4 +134,25 @@ T["Agent"]["functions"]["can populate stderr in functions"] = function() h.eq({ "Something went wrong" }, agent.stderr) end +T["Agent"]["functions"]["can populate stdout"] = function() + -- Prevent stderr from being cleared out + function agent:reset() + return nil + end + + agent:execute( + chat, + [[ + + Data 1 + +]] + ) + + h.eq({ { + msg = "Ran with success", + status = "success", + } }, agent.stdout) +end + return T diff --git a/tests/strategies/chat/agents/tools/stubs/func.lua b/tests/strategies/chat/agents/tools/stubs/func.lua index 5c63ead25..da9fe7fed 100644 --- a/tests/strategies/chat/agents/tools/stubs/func.lua +++ b/tests/strategies/chat/agents/tools/stubs/func.lua @@ -4,12 +4,14 @@ return { return "my func system prompt" end, cmds = { + ---@return { status: string, msg: string } function(self, actions, input) local spacer = "" if vim.g.codecompanion_test then spacer = " " end vim.g.codecompanion_test = (vim.g.codecompanion_test or "") .. spacer .. actions.data + return { status = "success", msg = "Ran with success" } end, }, handlers = { @@ -20,6 +22,7 @@ return { output = { success = function(self, cmd, output) vim.g.codecompanion_test_output = "Ran with success" + return "stdout is populated!" end, }, } From df7f43b2a2aab9bc5fb7987ad550579d7a739879 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Fri, 21 Feb 2025 21:11:05 +0000 Subject: [PATCH 08/38] wip: update tests --- lua/codecompanion/actions/init.lua | 3 +- lua/codecompanion/types.lua | 10 +-- .../chat/agents/executor/test_func.lua | 89 ++++++++++--------- .../chat/agents/tools/stubs/func.lua | 8 +- .../agents/tools/stubs/func_consecutive.lua | 1 + 5 files changed, 58 insertions(+), 53 deletions(-) diff --git a/lua/codecompanion/actions/init.lua b/lua/codecompanion/actions/init.lua index 516829db8..141994155 100644 --- a/lua/codecompanion/actions/init.lua +++ b/lua/codecompanion/actions/init.lua @@ -1,9 +1,8 @@ local Strategy = require("codecompanion.strategies") local config = require("codecompanion.config") +local log = require("codecompanion.utils.log") local prompt_library = require("codecompanion.actions.prompt_library") local static_actions = require("codecompanion.actions.static") - -local log = require("codecompanion.utils.log") local util = require("codecompanion.utils") ---@class CodeCompanion.Actions diff --git a/lua/codecompanion/types.lua b/lua/codecompanion/types.lua index f2c5fdce5..073c1fdb2 100644 --- a/lua/codecompanion/types.lua +++ b/lua/codecompanion/types.lua @@ -111,13 +111,13 @@ ---@field system_prompt fun(schema: table): string The system prompt to the LLM explaining the tool and the schema ---@field opts? table The options for the tool ---@field env? fun(schema: table): table|nil Any environment variables that can be used in the *_cmd fields. Receives the parsed schema from the LLM ----@field handlers table Functions which can be called during the execution of the tool ----@field handlers.setup? fun(self: CodeCompanion.Agent): any Function used to setup the tool. Called before any commands +---@field handlers table Functions which handle the execution of a tool ---@field handlers.approved? fun(self: CodeCompanion.Agent): boolean Function to call if an approval is needed before running a command ----@field handlers.on_exit? fun(self: CodeCompanion.Agent): any Function to call at the end of all of the commands ----@field output? table Functions which can be called after the command finishes +---@field handlers.on_exit? fun(self: CodeCompanion.Agent): any Function to call at the end of a group of commands or functions +---@field handlers.setup? fun(self: CodeCompanion.Agent): any Function used to setup the tool. Called before any commands +---@field output? table Functions which handle the output after every execution of a tool +---@field output.error? fun(self: CodeCompanion.Agent, cmd: table, error: table|string): any Function called if a tool execution fails ---@field output.rejected? fun(self: CodeCompanion.Agent, cmd: table): any Function to call if the user rejects running a command ----@field output.error? fun(self: CodeCompanion.Agent, cmd: table, error: table|string): any Function to call if the tool is unsuccessful ---@field output.success? fun(self: CodeCompanion.Agent, cmd: table, output: table|string): any Function to call if the tool is successful ---@field request table The request from the LLM to use the Tool diff --git a/tests/strategies/chat/agents/executor/test_func.lua b/tests/strategies/chat/agents/executor/test_func.lua index 8735c38e7..309338004 100644 --- a/tests/strategies/chat/agents/executor/test_func.lua +++ b/tests/strategies/chat/agents/executor/test_func.lua @@ -1,5 +1,6 @@ require("tests.log") local h = require("tests.helpers") +local log = require("codecompanion.utils.log") local new_set = MiniTest.new_set local T = new_set() @@ -22,24 +23,26 @@ T["Agent"] = new_set({ T["Agent"]["functions"] = new_set() -T["Agent"]["functions"]["can run"] = function() - h.eq(vim.g.codecompanion_test, nil) - agent:execute( - chat, - [[ - - Data 1 - Data 2 - -]] - ) - - -- Test that the function was called - h.eq("Data 1 Data 2", vim.g.codecompanion_test) -end +-- T["Agent"]["functions"]["can run"] = function() +-- h.eq(vim.g.codecompanion_test, nil) +-- agent:execute( +-- chat, +-- [[ +-- +-- Data 1 +-- Data 2 +-- +-- ]] +-- ) +-- +-- -- Test that the function was called +-- h.eq("Data 1 Data 2", vim.g.codecompanion_test) +-- end T["Agent"]["functions"]["calls output.success"] = function() h.eq(vim.g.codecompanion_test_output, nil) + + log:debug("=== TEST: Can call output.success ===") agent:execute( chat, [[ @@ -49,9 +52,10 @@ T["Agent"]["functions"]["calls output.success"] = function() ]] ) + log:debug("=== TEST END ===") -- Test `output.success` handler - h.eq("Ran with success", vim.g.codecompanion_test_output) + h.eq("Ran with successRan with success", vim.g.codecompanion_test_output) end T["Agent"]["functions"]["calls on_exit only once"] = function() @@ -75,10 +79,10 @@ T["Agent"]["functions"]["can run consecutively and pass input"] = function() agent:execute( chat, [[ - - Data 1 - -]] + + Data 1 + + ]] ) -- Test that the function was called @@ -90,11 +94,11 @@ T["Agent"]["functions"]["can run consecutively"] = function() agent:execute( chat, [[ - - Data 1 - Data 2 - -]] + + Data 1 + Data 2 + + ]] ) -- Test that the function was called, overwriting the global variable @@ -105,17 +109,17 @@ T["Agent"]["functions"]["can handle errors"] = function() agent:execute( chat, [[ - - Data 1 - -]] + + Data 1 + + ]] ) -- Test that the `output.error` handler was called h.eq("Something went wrong", vim.g.codecompanion_test_output) end -T["Agent"]["functions"]["can populate stderr"] = function() +T["Agent"]["functions"]["can populate stderr and halt execution"] = function() -- Prevent stderr from being cleared out function agent:reset() return nil @@ -124,13 +128,14 @@ T["Agent"]["functions"]["can populate stderr"] = function() agent:execute( chat, [[ - - Data 1 - -]] + + Data 1 + Data 2 + + ]] ) - -- Test that stderr is updated on the agent + -- Test that stderr is updated on the agent, only once h.eq({ "Something went wrong" }, agent.stderr) end @@ -143,16 +148,14 @@ T["Agent"]["functions"]["can populate stdout"] = function() agent:execute( chat, [[ - - Data 1 - -]] + + Data 1 + Data 2 + + ]] ) - h.eq({ { - msg = "Ran with success", - status = "success", - } }, agent.stdout) + h.eq({ { data = "Data 1", status = "success" }, { data = "Data 2", status = "success" } }, agent.stdout) end return T diff --git a/tests/strategies/chat/agents/tools/stubs/func.lua b/tests/strategies/chat/agents/tools/stubs/func.lua index da9fe7fed..868522ccd 100644 --- a/tests/strategies/chat/agents/tools/stubs/func.lua +++ b/tests/strategies/chat/agents/tools/stubs/func.lua @@ -4,24 +4,26 @@ return { return "my func system prompt" end, cmds = { - ---@return { status: string, msg: string } + ---@return { status: string, data: any } function(self, actions, input) local spacer = "" if vim.g.codecompanion_test then spacer = " " end vim.g.codecompanion_test = (vim.g.codecompanion_test or "") .. spacer .. actions.data - return { status = "success", msg = "Ran with success" } + return { status = "success", data = actions.data } end, }, handlers = { + -- Should only be called once on_exit = function(self) vim.g.codecompanion_test_exit = (vim.g.codecompanion_test_exit or "") .. "Exited" end, }, output = { + -- Should be called multiple times success = function(self, cmd, output) - vim.g.codecompanion_test_output = "Ran with success" + vim.g.codecompanion_test_output = (vim.g.codecompanion_test_output or "") .. "Ran with success" return "stdout is populated!" end, }, diff --git a/tests/strategies/chat/agents/tools/stubs/func_consecutive.lua b/tests/strategies/chat/agents/tools/stubs/func_consecutive.lua index cc7e5a706..2650492c1 100644 --- a/tests/strategies/chat/agents/tools/stubs/func_consecutive.lua +++ b/tests/strategies/chat/agents/tools/stubs/func_consecutive.lua @@ -4,6 +4,7 @@ return { return "my func system prompt" end, cmds = { + ---In production, we should be outputting as { status: string, data: any } function(self, actions, input) return (input and (input .. " ") or "") .. actions.data end, From ebb418af30e6969e1bfda6169070386da9e34490 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Fri, 21 Feb 2025 21:28:21 +0000 Subject: [PATCH 09/38] wip: add `handlers.setup` test --- .../chat/agents/executor/test_func.lua | 17 +++++++++++++++++ .../strategies/chat/agents/tools/stubs/func.lua | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/tests/strategies/chat/agents/executor/test_func.lua b/tests/strategies/chat/agents/executor/test_func.lua index 309338004..4ec406fda 100644 --- a/tests/strategies/chat/agents/executor/test_func.lua +++ b/tests/strategies/chat/agents/executor/test_func.lua @@ -16,6 +16,7 @@ T["Agent"] = new_set({ h.teardown_chat_buffer() vim.g.codecompanion_test = nil vim.g.codecompanion_test_exit = nil + vim.g.codecompanion_test_setup = nil vim.g.codecompanion_test_output = nil end, }, @@ -158,4 +159,20 @@ T["Agent"]["functions"]["can populate stdout"] = function() h.eq({ { data = "Data 1", status = "success" }, { data = "Data 2", status = "success" } }, agent.stdout) end +T["Agent"]["functions"]["calls handlers.setup once"] = function() + h.eq(nil, vim.g.codecompanion_test_setup) + + agent:execute( + chat, + [[ + + Data 1 + Data 2 + + ]] + ) + + h.eq("Setup", vim.g.codecompanion_test_setup) +end + return T diff --git a/tests/strategies/chat/agents/tools/stubs/func.lua b/tests/strategies/chat/agents/tools/stubs/func.lua index 868522ccd..b8745e9fc 100644 --- a/tests/strategies/chat/agents/tools/stubs/func.lua +++ b/tests/strategies/chat/agents/tools/stubs/func.lua @@ -15,6 +15,10 @@ return { end, }, handlers = { + -- Should only be called once + setup = function(self) + vim.g.codecompanion_test_setup = (vim.g.codecompanion_test_setup or "") .. "Setup" + end, -- Should only be called once on_exit = function(self) vim.g.codecompanion_test_exit = (vim.g.codecompanion_test_exit or "") .. "Exited" From f22993c5616a4005af6ccc2cfc68c9841f1b9b87 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Fri, 21 Feb 2025 21:38:12 +0000 Subject: [PATCH 10/38] wip: update docs --- doc/extending/tools.md | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/doc/extending/tools.md b/doc/extending/tools.md index 1ff6f20bc..50fce171f 100644 --- a/doc/extending/tools.md +++ b/doc/extending/tools.md @@ -167,23 +167,19 @@ You must: ### `handlers` -The `handlers` table consists of three methods. +The _handlers_ table consists of three methods: -The `setup` method is called before any of the `cmds` are called. This is useful if you wish to set the `cmds` dynamically on the tool itself, like in the `cmd_runner` tool. - -The `approved` method, which must return a boolean, contains logic to prompt the user for their approval prior to a command being executed. This is used in both the `files` and `cmd_runner` tool to allow the user to validate the actions the LLM is proposing to take. - -Finally, the `on_exit` method is called after all of the `cmds` have been executed. +1. `setup` - Is called before any of the commands/functions are. This is useful if you wish to set the cmds dynamically on the tool itself, like in the _@cmd_runner_ tool. +2. `approved` - Must return a boolean and contains logic to prompt the user for their approval prior to a command/function being executed. This is used in both the _@files and @cmd_runner tool to allow the user to validate the actions the LLM is proposing to take. +3. `on_exit` - Is called after all of the commands/function have executed. ### `output` -The `output` table consists of three methods. - -The `rejected` method is called when a user rejects to approve the running of a command. This method is useful of informing the LLM of the rejection. - -The `error` method is called to notify the LLM of an error when executing a command. +The _output_ table consists of three methods: -And finally, the `success` method is called to notify the LLM of a successful execution of a command. +1. `rejected` - Is called when a user rejects the approval to run a command/function. This method is used to inform the LLM of the rejection. +2. `error` - Is called when an error occurs whilst executing a command/function. It will only ever be called once as the whole execution for the group of commands/function is halted. This is a useful handler to use to notify the LLM of the failure. +3. `success` - Is called after _every_ successful execution of a command/function. This can be a useful handler to use to notfiy the LLM of the success. ### `request` From 3dfee685320709517e0d7d6f31d5831d9f1e0d92 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Fri, 21 Feb 2025 21:42:15 +0000 Subject: [PATCH 11/38] wip: update doc --- doc/extending/tools.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/doc/extending/tools.md b/doc/extending/tools.md index 50fce171f..1f463e747 100644 --- a/doc/extending/tools.md +++ b/doc/extending/tools.md @@ -6,7 +6,7 @@ In CodeCompanion, tools offer pre-defined ways for LLMs to execute actions and a In the plugin, tools work by sharing a system prompt with an LLM. This instructs them how to produce an XML markdown code block which can, in turn, be interpreted by the plugin to execute a command or function. -The plugin has a tools class `CodeCompanion.Tools` which will call individual `CodeCompanion.Tool` such as the [cmd_runner](https://github.com/olimorris/codecompanion.nvim/blob/main/lua/codecompanion/strategies/chat/tools/cmd_runner.lua) or the [editor](https://github.com/olimorris/codecompanion.nvim/blob/main/lua/codecompanion/strategies/chat/tools/editor.lua). The calling of tools is orchestrated by the `CodeCompanion.Chat` class which parses an LLM's response and looks to identify any XML code blocks. +The plugin has a tools class `CodeCompanion.Agent.Tools` which will call tools such as the [@cmd_runner](https://github.com/olimorris/codecompanion.nvim/blob/main/lua/codecompanion/strategies/chat/tools/cmd_runner.lua) or the [@editor](https://github.com/olimorris/codecompanion.nvim/blob/main/lua/codecompanion/strategies/chat/tools/editor.lua). The calling of tools is orchestrated by the `CodeCompanion.Chat` class which parses an LLM's response and looks to identify any XML code blocks. ## Tool Types @@ -14,14 +14,14 @@ There are two types of tools within the plugin: 1. **Command-based**: These tools can execute a series of commands in the background using a [plenary.job](https://github.com/nvim-lua/plenary.nvim/blob/master/lua/plenary/job.lua). They're non-blocking, meaning you can carry out other activities in Neovim whilst they run. Useful for heavy/time-consuming tasks. -2. **Function-based**: These tools, like the [editor](https://github.com/olimorris/codecompanion.nvim/blob/main/lua/codecompanion/strategies/chat/tools/editor.lua) one, execute Lua functions directly in Neovim within the main process. +2. **Function-based**: These tools, like the [@editor](https://github.com/olimorris/codecompanion.nvim/blob/main/lua/codecompanion/strategies/chat/tools/editor.lua) one, execute Lua functions directly in Neovim within the main process. ## The Interface Tools must implement the following interface: ```lua ----@class CodeCompanion.Tool +---@class CodeCompanion.Agent.Tool ---@field name string The name of the tool ---@field cmds table The commands to execute ---@field schema table The schema that the LLM must use in its response to execute a tool @@ -96,7 +96,7 @@ In this example, the first function will be called by the `CodeCompanion.Tools` The schema represents the structure of the response that the LLM must follow in order to call the tool. -In the `code_runner` tool, the schema is defined as a Lua table and then converted into XML in the chat buffer: +In the _@coderunner_ tool, the schema was defined as a Lua table and then converted into XML in the chat buffer: ```lua schema = { @@ -114,7 +114,7 @@ schema = { You can setup environment variables that other functions can access in the `env` function. This function receives the parsed schema which is requested by the LLM when it follows the schema's structure. -For the Code Runner agent, the environment was setup as: +For the _@coderunner_ agent, the environment was setup as: ```lua ---@param schema table @@ -139,7 +139,7 @@ Note that a table has been returned that can then be used in other functions. In the plugin, LLMs are given knowledge about a tool via a system prompt. This gives the LLM knowledge of the tool alongside the instructions (via the schema) required to execute it. -For the Code Runner agent, the `system_prompt` table was: +For the now archived _@coderunner_ tool, the `system_prompt` table was: ````lua system_prompt = function(schema) @@ -170,16 +170,17 @@ You must: The _handlers_ table consists of three methods: 1. `setup` - Is called before any of the commands/functions are. This is useful if you wish to set the cmds dynamically on the tool itself, like in the _@cmd_runner_ tool. -2. `approved` - Must return a boolean and contains logic to prompt the user for their approval prior to a command/function being executed. This is used in both the _@files and @cmd_runner tool to allow the user to validate the actions the LLM is proposing to take. +2. `approved` - Must return a boolean and contains logic to prompt the user for their approval prior to a command/function being executed. This is used in both the _@files_ and _@cmd_runner_ tool to allow the user to validate the actions the LLM is proposing to take. 3. `on_exit` - Is called after all of the commands/function have executed. ### `output` The _output_ table consists of three methods: -1. `rejected` - Is called when a user rejects the approval to run a command/function. This method is used to inform the LLM of the rejection. +1. `success` - Is called after _every_ successful execution of a command/function. This can be a useful handler to use to notfiy the LLM of the success. 2. `error` - Is called when an error occurs whilst executing a command/function. It will only ever be called once as the whole execution for the group of commands/function is halted. This is a useful handler to use to notify the LLM of the failure. -3. `success` - Is called after _every_ successful execution of a command/function. This can be a useful handler to use to notfiy the LLM of the success. +3. `rejected` - Is called when a user rejects the approval to run a command/function. This method is used to inform the LLM of the rejection. + ### `request` From 02fbee1623d0efcadd73e27e05385bf0820c80fd Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Fri, 21 Feb 2025 22:36:16 +0000 Subject: [PATCH 12/38] wip: re-enable tests --- .../chat/agents/executor/test_func.lua | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/strategies/chat/agents/executor/test_func.lua b/tests/strategies/chat/agents/executor/test_func.lua index 4ec406fda..cfa501671 100644 --- a/tests/strategies/chat/agents/executor/test_func.lua +++ b/tests/strategies/chat/agents/executor/test_func.lua @@ -24,21 +24,21 @@ T["Agent"] = new_set({ T["Agent"]["functions"] = new_set() --- T["Agent"]["functions"]["can run"] = function() --- h.eq(vim.g.codecompanion_test, nil) --- agent:execute( --- chat, --- [[ --- --- Data 1 --- Data 2 --- --- ]] --- ) --- --- -- Test that the function was called --- h.eq("Data 1 Data 2", vim.g.codecompanion_test) --- end +T["Agent"]["functions"]["can run"] = function() + h.eq(vim.g.codecompanion_test, nil) + agent:execute( + chat, + [[ + + Data 1 + Data 2 + +]] + ) + + -- Test that the function was called + h.eq("Data 1 Data 2", vim.g.codecompanion_test) +end T["Agent"]["functions"]["calls output.success"] = function() h.eq(vim.g.codecompanion_test_output, nil) From 3cdbe962f8b9ce2b19249e62e2cdb0183b26272b Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Sat, 22 Feb 2025 17:27:41 +0000 Subject: [PATCH 13/38] wip: add tests for editor --- tests/config.lua | 4 + .../chat/agents/tools/stubs/editor_xml.lua | 59 ++++++++++ .../chat/agents/tools/test_editor.lua | 103 ++++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 tests/strategies/chat/agents/tools/stubs/editor_xml.lua create mode 100644 tests/strategies/chat/agents/tools/test_editor.lua diff --git a/tests/config.lua b/tests/config.lua index 5d7d8af75..14b38ce38 100644 --- a/tests/config.lua +++ b/tests/config.lua @@ -52,6 +52,10 @@ return { }, agents = { tools = { + ["editor"] = { + callback = "strategies.chat.agents.tools.editor", + description = "Update a buffer with the LLM's response", + }, ["foo"] = { callback = "utils.foo", description = "Some foo function", diff --git a/tests/strategies/chat/agents/tools/stubs/editor_xml.lua b/tests/strategies/chat/agents/tools/stubs/editor_xml.lua new file mode 100644 index 000000000..c08c4f464 --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/editor_xml.lua @@ -0,0 +1,59 @@ +local M = {} + +function M.update(bufnr) + return string.format( + [[ + + + + 2 + 2 + %s + %s + + + +]], + bufnr, + '' + ) +end + +function M.add(bufnr) + return string.format( + [[ + + + + 4 + %s + %s + + + +]], + bufnr, + [[function hello_world() + return "hello_world" +end]] + ) +end + +function M.delete(bufnr) + return string.format( + [[ + + + + 1 + 4 + %s + + + +]], + bufnr + ) +end + +return M diff --git a/tests/strategies/chat/agents/tools/test_editor.lua b/tests/strategies/chat/agents/tools/test_editor.lua new file mode 100644 index 000000000..d8384c94f --- /dev/null +++ b/tests/strategies/chat/agents/tools/test_editor.lua @@ -0,0 +1,103 @@ +local h = require("tests.helpers") + +local new_set = MiniTest.new_set + +local bufnr + +local child = MiniTest.new_child_neovim() +local T = new_set({ + hooks = { + pre_case = function() + child.restart({ "-u", "scripts/minimal_init.lua" }) + child.lua([[vim.g.codecompanion_auto_tool_mode = true]]) + child.lua([[_G.chat, _G.agent = require("tests.helpers").setup_chat_buffer()]]) + + -- Setup the buffer + bufnr = child.lua([[ + local bufnr = vim.api.nvim_create_buf(false, true) + vim.bo[bufnr].readonly = false + + local lines = { + "function foo()", + ' return "foo"', + "end", + "", + "function bar()", + ' return "bar"', + "end", + "", + "function baz()", + ' return "baz"', + "end", + } + vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines) + + return bufnr + ]]) + end, + post_case = function() + _G.xml = nil + end, + post_once = child.stop, + }, +}) + +T["Agent @editor can update a buffer"] = function() + child.lua( + string.format([[ _G.xml = require("tests.strategies.chat.agents.tools.stubs.editor_xml").update(%s)]], bufnr) + ) + child.lua([[ + _G.agent:execute( + _G.chat, + _G.xml + ) + ]]) + + local lines = child.api.nvim_buf_get_lines(bufnr, 0, -1, false) + + h.eq([[ return "foobar"]], lines[2]) +end + +T["Agent @editor can add to a buffer"] = function() + child.lua( + string.format([[ _G.xml = require("tests.strategies.chat.agents.tools.stubs.editor_xml").add(%s)]], bufnr) + ) + child.lua([[ + _G.agent:execute( + _G.chat, + _G.xml + ) + ]]) + + local lines = child.api.nvim_buf_get_lines(bufnr, 0, -1, false) + + h.eq([[function hello_world()]], lines[4]) + h.eq([[ return "hello_world"]], lines[5]) + h.eq([[end]], lines[6]) +end + +T["Agent @editor can delete from a buffer"] = function() + child.lua( + string.format([[ _G.xml = require("tests.strategies.chat.agents.tools.stubs.editor_xml").delete(%s)]], bufnr) + ) + + local lines = child.api.nvim_buf_get_lines(bufnr, 0, -1, false) + h.eq([[function foo()]], lines[1]) + h.eq([[ return "foo"]], lines[2]) + h.eq([[end]], lines[3]) + + child.lua([[ + _G.agent:execute( + _G.chat, + _G.xml + ) + ]]) + + lines = child.api.nvim_buf_get_lines(bufnr, 0, -1, false) + + h.eq([[function bar()]], lines[1]) + h.eq([[ return "bar"]], lines[2]) + h.eq([[end]], lines[3]) +end + +return T From 80a72f3230922a27f399382efdb6721043b1eca8 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Sat, 22 Feb 2025 18:31:46 +0000 Subject: [PATCH 14/38] wip: update func --- .../strategies/chat/agents/executor/func.lua | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lua/codecompanion/strategies/chat/agents/executor/func.lua b/lua/codecompanion/strategies/chat/agents/executor/func.lua index bba8f8d9f..e7b617222 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/func.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/func.lua @@ -2,17 +2,17 @@ local log = require("codecompanion.utils.log") ---@class CodeCompanion.Agent.Executor.Func ---@field executor CodeCompanion.Agent.Executor ----@field cmd fun(self: CodeCompanion.Agent, actions: table, input: any) +---@field func fun(self: CodeCompanion.Agent, actions: table, input: any) ---@field index number local FuncExecutor = {} ---@param executor CodeCompanion.Agent.Executor ----@param cmd fun() +---@param func fun() ---@param index number -function FuncExecutor.new(executor, cmd, index) +function FuncExecutor.new(executor, func, index) return setmetatable({ executor = executor, - cmd = cmd, + func = func, index = index, }, { __index = FuncExecutor }) end @@ -35,27 +35,27 @@ function FuncExecutor:orchestrate(input) end -- Allow the action to call the next action directly, without calling `Executor:execute` - self:run(self.cmd, action[idx], prev_input, function(output) + self:run(self.func, action[idx], prev_input, function(output) process_actions(idx + 1, output) end) end process_actions(1, input) else - self:run(self.cmd, action, input) + self:run(self.func, action, input) end end ---Run the tool's function ----@param cmd fun(self: CodeCompanion.Agent, actions: table, input: any) +---@param func fun(self: CodeCompanion.Agent, actions: table, input: any) ---@param action table ---@param input? any ---@param callback? fun(output: any) ---@return nil -function FuncExecutor:run(cmd, action, input, callback) +function FuncExecutor:run(func, action, input, callback) log:debug("FuncExecutor:run") local ok, output = pcall(function() - return cmd(self.executor.agent, action, input) + return func(self.executor.agent, action, input) end) if not ok then return self.executor:error(action, output) From 52f786c449249921d3cbee821b2e79e5cd03910e Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Sun, 23 Feb 2025 14:38:10 +0000 Subject: [PATCH 15/38] wip: first pass at new commands --- doc/codecompanion.txt | 47 +++----- doc/extending/workflows.md | 2 +- lua/codecompanion/config.lua | 2 +- .../strategies/chat/agents/executor/cmd.lua | 101 ++++++++++++++++++ .../strategies/chat/agents/executor/init.lua | 27 +++-- .../chat/agents/tools/cmd_runner.lua | 63 +++++------ .../strategies/chat/agents/tools/files.lua | 14 +++ tests/config.lua | 4 + tests/strategies/chat/agents/executor/cmd.lua | 43 ++++++++ .../chat/agents/tools/stubs/cmd.lua | 26 +++++ .../chat/agents/tools/stubs/func_error.lua | 8 +- tests/strategies/chat/test_workflows.lua | 10 +- 12 files changed, 268 insertions(+), 79 deletions(-) create mode 100644 tests/strategies/chat/agents/executor/cmd.lua create mode 100644 tests/strategies/chat/agents/tools/stubs/cmd.lua diff --git a/doc/codecompanion.txt b/doc/codecompanion.txt index e64e371aa..4e5d4601e 100644 --- a/doc/codecompanion.txt +++ b/doc/codecompanion.txt @@ -1,4 +1,4 @@ -*codecompanion.txt* For NVIM v0.10.0 Last change: 2025 February 20 +*codecompanion.txt* For NVIM v0.10.0 Last change: 2025 February 23 ============================================================================== Table of Contents *codecompanion-table-of-contents* @@ -3368,10 +3368,10 @@ In the plugin, tools work by sharing a system prompt with an LLM. This instructs them how to produce an XML markdown code block which can, in turn, be interpreted by the plugin to execute a command or function. -The plugin has a tools class `CodeCompanion.Tools` which will call individual -`CodeCompanion.Tool` such as the cmd_runner +The plugin has a tools class `CodeCompanion.Agent.Tools` which will call tools +such as the @cmd_runner -or the editor +or the @editor . The calling of tools is orchestrated by the `CodeCompanion.Chat` class which parses an LLM’s response and looks to identify any XML code blocks. @@ -3388,7 +3388,7 @@ They’re non-blocking, meaning you can carry out other activities in Neovim whilst they run. Useful for heavy/time-consuming tasks. -2. **Function-based**: These tools, like the editor +2. **Function-based**: These tools, like the @editor one, execute Lua functions directly in Neovim within the main process. @@ -3400,7 +3400,7 @@ THE INTERFACE ~ Tools must implement the following interface: >lua - ---@class CodeCompanion.Tool + ---@class CodeCompanion.Agent.Tool ---@field name string The name of the tool ---@field cmds table The commands to execute ---@field schema table The schema that the LLM must use in its response to execute a tool @@ -3490,7 +3490,7 @@ SCHEMA The schema represents the structure of the response that the LLM must follow in order to call the tool. -In the `code_runner` tool, the schema is defined as a Lua table and then +In the `@coderunner` tool, the schema was defined as a Lua table and then converted into XML in the chat buffer: >lua @@ -3512,7 +3512,7 @@ You can setup environment variables that other functions can access in the `env` function. This function receives the parsed schema which is requested by the LLM when it follows the schema’s structure. -For the Code Runner agent, the environment was setup as: +For the `@coderunner` agent, the environment was setup as: >lua ---@param schema table @@ -3540,7 +3540,7 @@ In the plugin, LLMs are given knowledge about a tool via a system prompt. This gives the LLM knowledge of the tool alongside the instructions (via the schema) required to execute it. -For the Code Runner agent, the `system_prompt` table was: +For the now archived `@coderunner` tool, the `system_prompt` table was: >lua system_prompt = function(schema) @@ -3569,33 +3569,20 @@ For the Code Runner agent, the `system_prompt` table was: HANDLERS -The `handlers` table consists of three methods. +The `handlers` table consists of three methods: -The `setup` method is called before any of the `cmds` are called. This is -useful if you wish to set the `cmds` dynamically on the tool itself, like in -the `cmd_runner` tool. - -The `approved` method, which must return a boolean, contains logic to prompt -the user for their approval prior to a command being executed. This is used in -both the `files` and `cmd_runner` tool to allow the user to validate the -actions the LLM is proposing to take. - -Finally, the `on_exit` method is called after all of the `cmds` have been -executed. +1. `setup` - Is called before any of the commands/functions are. This is useful if you wish to set the cmds dynamically on the tool itself, like in the `@cmd_runner` tool. +2. `approved` - Must return a boolean and contains logic to prompt the user for their approval prior to a command/function being executed. This is used in both the `@files` and `@cmd_runner` tool to allow the user to validate the actions the LLM is proposing to take. +3. `on_exit` - Is called after all of the commands/function have executed. OUTPUT -The `output` table consists of three methods. - -The `rejected` method is called when a user rejects to approve the running of a -command. This method is useful of informing the LLM of the rejection. - -The `error` method is called to notify the LLM of an error when executing a -command. +The `output` table consists of three methods: -And finally, the `success` method is called to notify the LLM of a successful -execution of a command. +1. `success` - Is called after `every` successful execution of a command/function. This can be a useful handler to use to notfiy the LLM of the success. +2. `error` - Is called when an error occurs whilst executing a command/function. It will only ever be called once as the whole execution for the group of commands/function is halted. This is a useful handler to use to notify the LLM of the failure. +3. `rejected` - Is called when a user rejects the approval to run a command/function. This method is used to inform the LLM of the rejection. REQUEST diff --git a/doc/extending/workflows.md b/doc/extending/workflows.md index fe6ad8fea..516a75ffe 100644 --- a/doc/extending/workflows.md +++ b/doc/extending/workflows.md @@ -133,7 +133,7 @@ Now let's look at how we trigger the automated reflection prompts: opts = { auto_submit = true }, -- Scope this prompt to only run when the cmd_runner tool is active condition = function() - return vim.g.codecompanion_current_tool == "cmd_runner" + return _G.codecompanion_current_tool == "cmd_runner" end, -- Repeat until the tests pass, as indicated by the testing flag repeat_until = function(chat) diff --git a/lua/codecompanion/config.lua b/lua/codecompanion/config.lua index 52a033ead..ee810f93a 100644 --- a/lua/codecompanion/config.lua +++ b/lua/codecompanion/config.lua @@ -541,7 +541,7 @@ We'll repeat this cycle until the tests pass. Ensure no deviations from these st opts = { auto_submit = true }, -- Scope this prompt to the cmd_runner tool condition = function() - return vim.g.codecompanion_current_tool == "cmd_runner" + return _G.codecompanion_current_tool == "cmd_runner" end, -- Repeat until the tests pass, as indicated by the testing flag -- which the cmd_runner tool sets on the chat buffer diff --git a/lua/codecompanion/strategies/chat/agents/executor/cmd.lua b/lua/codecompanion/strategies/chat/agents/executor/cmd.lua index e69de29bb..e2d6442ba 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/cmd.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/cmd.lua @@ -0,0 +1,101 @@ +local Job = require("plenary.job") +local log = require("codecompanion.utils.log") + +---@class CodeCompanion.Agent.Executor.Cmd +---@field executor CodeCompanion.Agent.Executor +---@field cmd table +---@field index number +local CmdExecutor = {} + +---@param executor CodeCompanion.Agent.Executor +---@param cmd table +---@param index number +function CmdExecutor.new(executor, cmd, index) + return setmetatable({ + executor = executor, + cmd = cmd, + index = index, + }, { __index = CmdExecutor }) +end + +---Orchestrate the tool function +---@return nil +function CmdExecutor:orchestrate() + log:debug("CmdExecutor:orchestrate %s", self.cmd) + self:run(self.cmd) +end + +---Some commands output ANSI color codes so we need to strip them +---@param tbl table +---@return table +local function strip_ansi(tbl) + for i, v in ipairs(tbl) do + tbl[i] = v:gsub("\027%[[0-9;]*%a", "") + end + return tbl +end + +---Run the tool's function +---@param cmd table +---@return nil +function CmdExecutor:run(cmd) + log:debug("CmdExecutor:run %s", cmd) + + local job = Job:new({ + command = vim.fn.has("win32") == 1 and "cmd.exe" or "sh", + args = { vim.fn.has("win32") == 1 and "/c" or "-c", table.concat(cmd.cmd or cmd, " ") }, + enable_recording = true, + cwd = vim.fn.getcwd(), + on_exit = function(data, code) + -- log:debug("CmdExecutor:run - on_exit") + + self.executor.current_cmd_tool = nil + + -- Flags can be inserted into the chat buffer to be picked up later + if cmd.flag then + self.executor.agent.chat.tool_flags = self.executor.agent.chat.tool_flags or {} + self.executor.agent.chat.tool_flags[cmd.flag] = (code == 0) + end + + -- log:debug("[Tools] %s finished with code %s", self.cmd, code) + + vim.schedule(function() + -- We need to ensure we handle any errors that occur within the job + -- Otherwise, we'll end up with jobs which fail to shutdown + local ok, _ = pcall(function() + if _G.codecompanion_cancel_tool then + return self.executor:close() + end + if data then + if data._stderr_results then + table.insert(self.executor.agent.stderr, strip_ansi(data._stderr_results)) + end + if data._stdout_results then + table.insert(self.executor.agent.stdout, strip_ansi(data._stdout_results)) + end + end + if code == 0 then + self.executor:success(cmd) + return self.executor:close() + else + return self.executor:error(cmd, string.format("Command failed with code %s", code)) + end + end) + + if not ok then + log:error("Error running command: %s", cmd) + end + end) + end, + }) + + if not vim.tbl_isempty(self.executor.current_cmd_tool) then + self.executor.current_cmd_tool:and_then(job) + else + job:start() + end + + self.executor.current_cmd_tool = job +end + +return CmdExecutor diff --git a/lua/codecompanion/strategies/chat/agents/executor/init.lua b/lua/codecompanion/strategies/chat/agents/executor/init.lua index a3bc6daec..c2d5ac692 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/init.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/init.lua @@ -6,6 +6,7 @@ local util = require("codecompanion.utils") ---@class CodeCompanion.Agent.Executor ---@field agent CodeCompanion.Agent +---@field current_cmd_tool table The current cmd tool that's being executed ---@field handlers table ---@field index number The index of the current command ---@field output table @@ -17,8 +18,10 @@ local Executor = {} ---@param tool CodeCompanion.Agent.Tool function Executor.new(agent, tool) log:debug("Executor.new: %s", tool.name) + local self = setmetatable({ agent = agent, + current_cmd_tool = {}, tool = tool, }, { __index = Executor }) @@ -48,9 +51,9 @@ function Executor.new(agent, tool) self.tool.output.rejected(agent, cmd) end end, - error = function(cmd, error) + error = function(cmd, error, output) if self.tool.output and self.tool.output.error then - self.tool.output.error(agent, cmd, error) + self.tool.output.error(agent, cmd, error, output) end end, success = function(cmd, output) @@ -86,7 +89,7 @@ function Executor:execute(index, input) if type(cmd) == "function" then return FuncExecutor.new(self, cmd, index):orchestrate(input) end - return CmdExecutor.new(self, cmd):execute() + return CmdExecutor.new(self, cmd):orchestrate() end ---Does the tool require approval before it can be executed? @@ -99,25 +102,29 @@ end ---Handle an error from a tool ---@param action table ----@param error string +---@param error? string ---@return nil function Executor:error(action, error) log:debug("Executor:error") self.agent.status = self.agent.constants.STATUS_ERROR - table.insert(self.agent.stderr, error) - self.output.error(action, error) - log:error("Error calling function in %s: %s", self.tool.name, error) + if error then + table.insert(self.agent.stderr, error) + log:error("Error running %s: %s", self.tool.name, error) + end + self.output.error(action, self.agent.stderr, self.agent.stdout) self:close() end ---Handle a successful completion of a tool ---@param action table ----@param output string +---@param output? string ---@return nil function Executor:success(action, output) log:debug("Executor:success") - table.insert(self.agent.stdout, output) - self.output.success(action, output) + if output then + table.insert(self.agent.stdout, output) + end + self.output.success(action, self.agent.stdout) end ---Close the execution of the tool diff --git a/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua b/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua index 6bbb93887..34659cc83 100644 --- a/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua +++ b/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua @@ -5,26 +5,21 @@ commands in the same XML block. All commands must be approved by you. --]] local config = require("codecompanion.config") - local log = require("codecompanion.utils.log") local util = require("codecompanion.utils") local xml2lua = require("codecompanion.utils.xml.xml2lua") ----@class CmdRunner.ChatOpts ----@field cmd table|string The command that was executed ----@field output table|string The output of the command ----@field message? string An optional message - ---Outputs a message to the chat buffer that initiated the tool ---@param msg string The message to output ----@param tool CodeCompanion.Tools The tools object ----@param opts CmdRunner.ChatOpts +---@param tool CodeCompanion.Agent The tools object +---@param opts {cmd: table, output: table|string, message?: string} local function to_chat(msg, tool, opts) - if type(opts.cmd) == "table" then - opts.cmd = table.concat(opts.cmd, " ") + local cmd + if opts and type(opts.cmd) == "table" then + cmd = table.concat(opts.cmd, " ") end - if type(opts.output) == "table" then - opts.output = table.concat(opts.output, "\n") + if opts and type(opts.output) == "table" then + opts.output = vim.iter(opts.output):flatten():join("\n") end local content @@ -34,7 +29,7 @@ local function to_chat(msg, tool, opts) ]], msg, - opts.cmd + cmd ) else content = string.format( @@ -57,7 +52,7 @@ local function to_chat(msg, tool, opts) }) end ----@class CodeCompanion.Tool +---@class CodeCompanion.Agent.Tool return { name = "cmd_runner", cmds = { @@ -161,9 +156,9 @@ return { ) end, handlers = { - ---@param self CodeCompanion.Tools The tool object - setup = function(self) - local tool = self.tool --[[@type CodeCompanion.Tool]] + ---@param agent CodeCompanion.Agent The tool object + setup = function(agent) + local tool = agent.tool --[[@type CodeCompanion.Agent.Tool]] local action = tool.request.action local actions = vim.isarray(action) and action or { action } @@ -177,10 +172,10 @@ return { end, ---Approve the command to be run - ---@param self CodeCompanion.Tools The tool object + ---@param agent CodeCompanion.Agent ---@param cmd table ---@return boolean - approved = function(self, cmd) + approved = function(agent, cmd) if vim.g.codecompanion_auto_tool_mode then log:info("[Cmd Runner Tool] Auto-approved running the command") return true @@ -202,22 +197,30 @@ return { output = { ---Rejection message back to the LLM - rejected = function(self, cmd) - to_chat("I chose not to run", self, { cmd = cmd.cmd or cmd, output = "" }) + ---@param agent CodeCompanion.Agent + ---@param cmd table + ---@return nil + rejected = function(agent, cmd) + to_chat("I chose not to run", agent, { cmd = cmd.cmd or cmd, output = "" }) end, - ---@param self CodeCompanion.Tools The tools object - ---@param cmd table|string The command that was executed - ---@param stderr table|string - error = function(self, cmd, stderr) - to_chat("There was an error from", self, { cmd = cmd.cmd or cmd, output = stderr }) + ---@param agent CodeCompanion.Agent + ---@param cmd table + ---@param stderr table + ---@param stdout? table + error = function(agent, cmd, stderr, stdout) + to_chat("There was an error from", agent, { cmd = cmd.cmd or cmd, output = stderr }) + + if stdout and not vim.tbl_isempty(stdout) then + to_chat("There was also some output from", agent, { cmd = cmd.cmd or cmd, output = stdout }) + end end, - ---@param self CodeCompanion.Tools The tools object + ---@param agent CodeCompanion.Agent ---@param cmd table|string The command that was executed - ---@param stdout table|string - success = function(self, cmd, stdout) - to_chat("The output from", self, { cmd = cmd.cmd or cmd, output = stdout }) + ---@param stdout table + success = function(agent, cmd, stdout) + to_chat("The output from", agent, { cmd = cmd.cmd or cmd, output = stdout }) end, }, } diff --git a/lua/codecompanion/strategies/chat/agents/tools/files.lua b/lua/codecompanion/strategies/chat/agents/tools/files.lua index 80e2e32e8..7ef0cc517 100644 --- a/lua/codecompanion/strategies/chat/agents/tools/files.lua +++ b/lua/codecompanion/strategies/chat/agents/tools/files.lua @@ -424,12 +424,19 @@ Remember: log:info("[Files Tool] Approved the %s action", string.upper(action._attr.type)) return true end, + + ---@param self CodeCompanion.Agent The tool object + ---@return nil on_exit = function(self) log:debug("[Files Tool] on_exit handler executed") file = nil end, }, output = { + ---@param self CodeCompanion.Agent The tool object + ---@param action table + ---@param output table + ---@return nil success = function(self, action, output) local type = action._attr.type local path = action.path @@ -454,6 +461,10 @@ Remember: end end, + ---@param self CodeCompanion.Agent The tool object + ---@param action table + ---@param err string + ---@return nil error = function(self, action, err) log:debug("[Files Tool] error callback executed") return self.chat:add_buf_message({ @@ -470,6 +481,9 @@ Remember: }) end, + ---@param self CodeCompanion.Agent The tool object + ---@param action table + ---@return nil rejected = function(self, action) return self.chat:add_buf_message({ role = config.constants.USER_ROLE, diff --git a/tests/config.lua b/tests/config.lua index 14b38ce38..5fa56e95a 100644 --- a/tests/config.lua +++ b/tests/config.lua @@ -80,6 +80,10 @@ return { callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func_error.lua", description = "Error function tool to test", }, + ["cmd"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/cmd.lua", + description = "Cmd tool", + }, opts = { system_prompt = [[My tool system prompt]], }, diff --git a/tests/strategies/chat/agents/executor/cmd.lua b/tests/strategies/chat/agents/executor/cmd.lua new file mode 100644 index 000000000..a2ed03b61 --- /dev/null +++ b/tests/strategies/chat/agents/executor/cmd.lua @@ -0,0 +1,43 @@ +local h = require("tests.helpers") +local log = require("codecompanion.utils.log") + +local new_set = MiniTest.new_set +local T = new_set() + +local chat, agent + +T["Agent"] = new_set({ + hooks = { + pre_case = function() + chat, agent = h.setup_chat_buffer() + -- Reset test globals + vim.g.codecompanion_test_setup = nil + vim.g.codecompanion_test_exit = nil + vim.g.codecompanion_test_output = nil + end, + post_case = function() + h.teardown_chat_buffer() + end, + }, +}) + +T["Agent"]["cmds"] = new_set() + +T["Agent"]["cmds"]["can run"] = function() + log:debug("=== TEST: Can run cmds ===") + agent:execute( + chat, + [[ + +]] + ) + vim.cmd("redraw!") + log:debug("=== TEST END ===") + + -- Test that the cmd ran + h.eq(vim.g.codecompanion_test_setup, "Setup") + h.eq(vim.g.codecompanion_test_exit, "Exited") + h.eq(vim.g.codecompanion_test_output, "Ran with success") +end + +return T diff --git a/tests/strategies/chat/agents/tools/stubs/cmd.lua b/tests/strategies/chat/agents/tools/stubs/cmd.lua new file mode 100644 index 000000000..ea43f1813 --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/cmd.lua @@ -0,0 +1,26 @@ +return { + name = "cmd", + system_prompt = function(schema) + return "my cmd system prompt" + end, + cmds = { + { [[make test]] }, + }, + handlers = { + -- Should only be called once + setup = function(self) + vim.g.codecompanion_test_setup = (vim.g.codecompanion_test_setup or "") .. "Setup" + end, + -- Should only be called once + on_exit = function(self) + vim.g.codecompanion_test_exit = (vim.g.codecompanion_test_exit or "") .. "Exited" + end, + }, + output = { + -- Should be called multiple times + success = function(self, cmd, output) + vim.g.codecompanion_test_output = (vim.g.codecompanion_test_output or "") .. "Ran with success" + return "stdout is populated!" + end, + }, +} diff --git a/tests/strategies/chat/agents/tools/stubs/func_error.lua b/tests/strategies/chat/agents/tools/stubs/func_error.lua index 3cf7531bb..5756974f6 100644 --- a/tests/strategies/chat/agents/tools/stubs/func_error.lua +++ b/tests/strategies/chat/agents/tools/stubs/func_error.lua @@ -9,8 +9,12 @@ return { end, }, output = { - error = function(self, cmd, error) - vim.g.codecompanion_test_output = "" .. error .. "" + ---@param self CodeCompanion.Agent + ---@param cmd string + ---@param stderr table + ---@param stdout table + error = function(self, cmd, stderr, stdout) + vim.g.codecompanion_test_output = "" .. table.concat(stderr, " ") .. "" end, }, } diff --git a/tests/strategies/chat/test_workflows.lua b/tests/strategies/chat/test_workflows.lua index f34995e8f..00e9c9e73 100644 --- a/tests/strategies/chat/test_workflows.lua +++ b/tests/strategies/chat/test_workflows.lua @@ -66,7 +66,7 @@ T["Workflows"] = new_set({ opts = { auto_submit = false }, -- Scope this prompt to the cmd_runner tool condition = function() - return vim.g.codecompanion_current_tool == "cmd_runner" + return _G.codecompanion_current_tool == "cmd_runner" end, -- Repeat until the tests pass, as indicated by the testing flag -- which the cmd_runner tool sets on the chat buffer @@ -82,7 +82,7 @@ T["Workflows"] = new_set({ role = "user", opts = { auto_submit = false }, condition = function() - return not vim.g.codecompanion_current_tool + return not _G.codecompanion_current_tool end, content = "Tests passed!", }, @@ -103,19 +103,19 @@ T["Workflows"]["prompts are sequentially added to the chat buffer"] = function() h.eq("First prompt", h.get_buf_lines(chat.bufnr)[#h.get_buf_lines(chat.bufnr)]) -- Let's mock a failing tool test - vim.g.codecompanion_current_tool = "cmd_runner" + _G.codecompanion_current_tool = "cmd_runner" h.send_to_llm(chat, "Calling a tool...") h.eq("The tests have failed", h.get_buf_lines(chat.bufnr)[#h.get_buf_lines(chat.bufnr)]) -- And again - vim.g.codecompanion_current_tool = "cmd_runner" + _G.codecompanion_current_tool = "cmd_runner" h.send_to_llm(chat, "Calling a tool...") h.eq("The tests have failed", h.get_buf_lines(chat.bufnr)[#h.get_buf_lines(chat.bufnr)]) -- Now let's mock a passing test chat.tool_flags.testing = true h.send_to_llm(chat, "Calling a tool...", function() - vim.g.codecompanion_current_tool = nil + _G.codecompanion_current_tool = nil end) h.eq("Tests passed!", h.get_buf_lines(chat.bufnr)[#h.get_buf_lines(chat.bufnr)]) From 067af018b0feedf78ebff28111121b29b05493a3 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Sun, 23 Feb 2025 14:55:12 +0000 Subject: [PATCH 16/38] wip: cleanup comments --- lua/codecompanion/strategies/chat/agents/executor/cmd.lua | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lua/codecompanion/strategies/chat/agents/executor/cmd.lua b/lua/codecompanion/strategies/chat/agents/executor/cmd.lua index e2d6442ba..9b71b1160 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/cmd.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/cmd.lua @@ -47,7 +47,7 @@ function CmdExecutor:run(cmd) enable_recording = true, cwd = vim.fn.getcwd(), on_exit = function(data, code) - -- log:debug("CmdExecutor:run - on_exit") + log:debug("CmdExecutor:run - on_exit") self.executor.current_cmd_tool = nil @@ -57,11 +57,7 @@ function CmdExecutor:run(cmd) self.executor.agent.chat.tool_flags[cmd.flag] = (code == 0) end - -- log:debug("[Tools] %s finished with code %s", self.cmd, code) - vim.schedule(function() - -- We need to ensure we handle any errors that occur within the job - -- Otherwise, we'll end up with jobs which fail to shutdown local ok, _ = pcall(function() if _G.codecompanion_cancel_tool then return self.executor:close() @@ -83,7 +79,7 @@ function CmdExecutor:run(cmd) end) if not ok then - log:error("Error running command: %s", cmd) + log:error("Internal error running command: %s", cmd) end end) end, From 92075b18da20b4ae50089ecc79b30389c0adaef6 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Sun, 23 Feb 2025 23:06:08 +0000 Subject: [PATCH 17/38] wip: add tests for commands --- .../strategies/chat/agents/executor/cmd.lua | 43 ++--------- .../chat/agents/executor/cmd_handlers.lua | 75 +++++++++++++++++++ .../strategies/chat/agents/executor/init.lua | 3 +- .../chat/agents/tools/cmd_runner.lua | 6 +- tests/helpers.lua | 55 ++++++++++++-- tests/mocks/job.lua | 47 ++++++++++++ tests/strategies/chat/agents/executor/cmd.lua | 43 ----------- .../chat/agents/executor/test_cmd.lua | 69 +++++++++++++++++ .../chat/agents/tools/stubs/cmd.lua | 3 +- .../chat/agents/tools/stubs/cmd_xml.lua | 7 ++ 10 files changed, 258 insertions(+), 93 deletions(-) create mode 100644 lua/codecompanion/strategies/chat/agents/executor/cmd_handlers.lua create mode 100644 tests/mocks/job.lua delete mode 100644 tests/strategies/chat/agents/executor/cmd.lua create mode 100644 tests/strategies/chat/agents/executor/test_cmd.lua create mode 100644 tests/strategies/chat/agents/tools/stubs/cmd_xml.lua diff --git a/lua/codecompanion/strategies/chat/agents/executor/cmd.lua b/lua/codecompanion/strategies/chat/agents/executor/cmd.lua index 9b71b1160..8f0a4078d 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/cmd.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/cmd.lua @@ -1,4 +1,5 @@ local Job = require("plenary.job") +local handlers = require("codecompanion.strategies.chat.agents.executor.cmd_handlers") local log = require("codecompanion.utils.log") ---@class CodeCompanion.Agent.Executor.Cmd @@ -42,51 +43,17 @@ function CmdExecutor:run(cmd) log:debug("CmdExecutor:run %s", cmd) local job = Job:new({ - command = vim.fn.has("win32") == 1 and "cmd.exe" or "sh", - args = { vim.fn.has("win32") == 1 and "/c" or "-c", table.concat(cmd.cmd or cmd, " ") }, + command = handlers.command(), + args = handlers.args(cmd), enable_recording = true, cwd = vim.fn.getcwd(), on_exit = function(data, code) - log:debug("CmdExecutor:run - on_exit") - - self.executor.current_cmd_tool = nil - - -- Flags can be inserted into the chat buffer to be picked up later - if cmd.flag then - self.executor.agent.chat.tool_flags = self.executor.agent.chat.tool_flags or {} - self.executor.agent.chat.tool_flags[cmd.flag] = (code == 0) - end - - vim.schedule(function() - local ok, _ = pcall(function() - if _G.codecompanion_cancel_tool then - return self.executor:close() - end - if data then - if data._stderr_results then - table.insert(self.executor.agent.stderr, strip_ansi(data._stderr_results)) - end - if data._stdout_results then - table.insert(self.executor.agent.stdout, strip_ansi(data._stdout_results)) - end - end - if code == 0 then - self.executor:success(cmd) - return self.executor:close() - else - return self.executor:error(cmd, string.format("Command failed with code %s", code)) - end - end) - - if not ok then - log:error("Internal error running command: %s", cmd) - end - end) + handlers.on_exit(self, cmd, data, code) end, }) if not vim.tbl_isempty(self.executor.current_cmd_tool) then - self.executor.current_cmd_tool:and_then(job) + self.executor.current_cmd_tool:and_then_wrap(job) else job:start() end diff --git a/lua/codecompanion/strategies/chat/agents/executor/cmd_handlers.lua b/lua/codecompanion/strategies/chat/agents/executor/cmd_handlers.lua new file mode 100644 index 000000000..3e4642d96 --- /dev/null +++ b/lua/codecompanion/strategies/chat/agents/executor/cmd_handlers.lua @@ -0,0 +1,75 @@ +--[[ +-- The purpose of this file is to abstract away any command logic from Plenary's +-- Job API. This makes it soooooo much easier to test as we decouple it from +-- the cmd executor file. +--]] + +local log = require("codecompanion.utils.log") + +---@class CodeCompanion.Agent.Executor.CmdHandlers +local CmdHandlers = {} + +---Strip ANSI color codes from output +---@param tbl table +---@return table +local function strip_ansi(tbl) + for i, v in ipairs(tbl) do + tbl[i] = v:gsub("\027%[[0-9;]*%a", "") + end + return tbl +end + +---The command to run +---@return string +function CmdHandlers.command() + return vim.fn.has("win32") == 1 and "cmd.exe" or "sh" +end + +---The command arguments +---@param cmd table +---@return table +function CmdHandlers.args(cmd) + return { vim.fn.has("win32") == 1 and "/c" or "-c", table.concat(cmd.cmd or cmd, " ") } +end + +---Handle job exit +---@param executor CodeCompanion.Agent.Executor.Cmd +---@param cmd table Command being executed +---@param data table Job data containing stdout/stderr results +---@param code number Exit code +function CmdHandlers.on_exit(executor, cmd, data, code) + log:debug("CmdExecutor:run - on_exit") + executor.executor.current_cmd_tool = nil + + -- Flags can be inserted into the chat buffer to be picked up later + if cmd.flag then + executor.executor.agent.chat.tool_flags = executor.executor.agent.chat.tool_flags or {} + executor.executor.agent.chat.tool_flags[cmd.flag] = (code == 0) + end + + vim.schedule(function() + local ok, _ = pcall(function() + if _G.codecompanion_cancel_tool then + return executor.executor:close() + end + if data and data._stderr_results then + table.insert(executor.executor.agent.stderr, strip_ansi(data._stderr_results)) + end + if data and data._stdout_results then + table.insert(executor.executor.agent.stdout, strip_ansi(data._stdout_results)) + end + if code == 0 then + executor.executor:success(cmd) + return executor.executor:close() + else + return executor.executor:error(cmd, string.format("Failed with code %s", code)) + end + end) + + if not ok then + log:error("Internal error running command: %s", cmd) + end + end) +end + +return CmdHandlers diff --git a/lua/codecompanion/strategies/chat/agents/executor/init.lua b/lua/codecompanion/strategies/chat/agents/executor/init.lua index c2d5ac692..694a25193 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/init.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/init.lua @@ -109,7 +109,7 @@ function Executor:error(action, error) self.agent.status = self.agent.constants.STATUS_ERROR if error then table.insert(self.agent.stderr, error) - log:error("Error running %s: %s", self.tool.name, error) + log:warn("Error with %s: %s", self.tool.name, error) end self.output.error(action, self.agent.stderr, self.agent.stdout) self:close() @@ -131,6 +131,7 @@ end ---@return nil function Executor:close() log:debug("Executor:close") + self.handlers.on_exit() util.fire("AgentFinished", { diff --git a/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua b/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua index 34659cc83..fd1e18bdf 100644 --- a/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua +++ b/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua @@ -17,6 +17,8 @@ local function to_chat(msg, tool, opts) local cmd if opts and type(opts.cmd) == "table" then cmd = table.concat(opts.cmd, " ") + else + cmd = opts.cmd end if opts and type(opts.output) == "table" then opts.output = vim.iter(opts.output):flatten():join("\n") @@ -41,7 +43,7 @@ local function to_chat(msg, tool, opts) ]], msg, - opts.cmd, + cmd, opts.output ) end @@ -217,7 +219,7 @@ return { end, ---@param agent CodeCompanion.Agent - ---@param cmd table|string The command that was executed + ---@param cmd table The command that was executed ---@param stdout table success = function(agent, cmd, stdout) to_chat("The output from", agent, { cmd = cmd.cmd or cmd, output = stdout }) diff --git a/tests/helpers.lua b/tests/helpers.lua index 9d8efb0da..d7215dab6 100644 --- a/tests/helpers.lua +++ b/tests/helpers.lua @@ -1,5 +1,8 @@ local Helpers = {} +-- Store original modules +Helpers._original_modules = {} + Helpers.expect = MiniTest.expect --[[@type function]] Helpers.eq = MiniTest.expect.equality --[[@type function]] Helpers.not_eq = MiniTest.expect.no_equality --[[@type function]] @@ -16,8 +19,37 @@ Helpers.expect_starts_with = MiniTest.new_expectation( --[[@type function]] end ) -local function make_config() - -- Overwrite the config with the test config +---Setup mock for a module +---@param module_name string +---@param mock_implementation table +function Helpers.mock_module(module_name, mock_implementation) + Helpers._original_modules[module_name] = package.loaded[module_name] + package.loaded[module_name] = mock_implementation +end + +---Restore original module +---@param module_name string +function Helpers.restore_module(module_name) + package.loaded[module_name] = Helpers._original_modules[module_name] + Helpers._original_modules[module_name] = nil +end + +---Mock plenary.job specifically +---@return nil +function Helpers.mock_job() + local MockJob = require("tests.mocks.job") + Helpers.mock_module("plenary.job", MockJob) +end + +---Restore plenary.job +---@return nil +function Helpers.restore_job() + Helpers.restore_module("plenary.job") +end + +---Mock the plugin config +---@return table +local function mock_config() local config_module = require("codecompanion.config") config_module.setup = function(args) config_module.config = args or {} @@ -28,9 +60,13 @@ local function make_config() return config_module end +---Setup and mock a chat buffer +---@param config? table +---@param adapter? table +---@return CodeCompanion.Chat, CodeCompanion.Agent, CodeCompanion.Variables Helpers.setup_chat_buffer = function(config, adapter) local test_config = vim.deepcopy(require("tests.config")) - local config_module = make_config() + local config_module = mock_config() config_module.setup(vim.tbl_deep_extend("force", test_config, config or {})) -- Extend the adapters @@ -101,21 +137,26 @@ Helpers.send_to_llm = function(chat, message, callback) end ---Clean down the chat buffer if required +---@return nil Helpers.teardown_chat_buffer = function() - -- package.loaded["codecompanion.utils.foo"] = nil - -- package.loaded["codecompanion.utils.bar"] = nil - -- package.loaded["codecompanion.utils.bar_again"] = nil + package.loaded["codecompanion.utils.foo"] = nil + package.loaded["codecompanion.utils.bar"] = nil + package.loaded["codecompanion.utils.bar_again"] = nil end ---Get the lines of a buffer +---@param bufnr number +---@return table Helpers.get_buf_lines = function(bufnr) return vim.api.nvim_buf_get_lines(bufnr, 0, -1, true) end ---Setup the inline buffer +---@param config table +---@return CodeCompanion.Inline Helpers.setup_inline = function(config) local test_config = vim.deepcopy(require("tests.config")) - local config_module = make_config() + local config_module = mock_config() config_module.setup(vim.tbl_deep_extend("force", test_config, config or {})) return require("codecompanion.strategies.inline").new({ diff --git a/tests/mocks/job.lua b/tests/mocks/job.lua new file mode 100644 index 000000000..1ddb78ccd --- /dev/null +++ b/tests/mocks/job.lua @@ -0,0 +1,47 @@ +---@class MockJob +---@field is_shutdown boolean +---@field _opts table +---@field _stdout_results table +---@field _stderr_results table +---@field _on_exit function +local MockJob = {} +MockJob.__index = MockJob + +---Create a new mock job +---@param opts table +---@return MockJob +function MockJob:new(opts) + ---@type MockJob + local job = setmetatable({ + is_shutdown = false, + _opts = opts, + _stdout_results = { "mocked stdout" }, + _stderr_results = {}, + _on_exit = opts.on_exit, + }, self) + + return job +end + +---Mock start function +---@return MockJob +function MockJob:start() + -- Execute immediately instead of scheduling + if self._on_exit then + self._on_exit(self, 0) + end + return self +end + +---Mock and_then_wrap function +---@param next_job MockJob +function MockJob:and_then_wrap(next_job) + next_job:start() +end + +---Mock shutdown function +function MockJob:shutdown() + self.is_shutdown = true +end + +return MockJob diff --git a/tests/strategies/chat/agents/executor/cmd.lua b/tests/strategies/chat/agents/executor/cmd.lua deleted file mode 100644 index a2ed03b61..000000000 --- a/tests/strategies/chat/agents/executor/cmd.lua +++ /dev/null @@ -1,43 +0,0 @@ -local h = require("tests.helpers") -local log = require("codecompanion.utils.log") - -local new_set = MiniTest.new_set -local T = new_set() - -local chat, agent - -T["Agent"] = new_set({ - hooks = { - pre_case = function() - chat, agent = h.setup_chat_buffer() - -- Reset test globals - vim.g.codecompanion_test_setup = nil - vim.g.codecompanion_test_exit = nil - vim.g.codecompanion_test_output = nil - end, - post_case = function() - h.teardown_chat_buffer() - end, - }, -}) - -T["Agent"]["cmds"] = new_set() - -T["Agent"]["cmds"]["can run"] = function() - log:debug("=== TEST: Can run cmds ===") - agent:execute( - chat, - [[ - -]] - ) - vim.cmd("redraw!") - log:debug("=== TEST END ===") - - -- Test that the cmd ran - h.eq(vim.g.codecompanion_test_setup, "Setup") - h.eq(vim.g.codecompanion_test_exit, "Exited") - h.eq(vim.g.codecompanion_test_output, "Ran with success") -end - -return T diff --git a/tests/strategies/chat/agents/executor/test_cmd.lua b/tests/strategies/chat/agents/executor/test_cmd.lua new file mode 100644 index 000000000..9edcd2fb3 --- /dev/null +++ b/tests/strategies/chat/agents/executor/test_cmd.lua @@ -0,0 +1,69 @@ +local h = require("tests.helpers") + +local new_set = MiniTest.new_set + +local child = MiniTest.new_child_neovim() +local T = new_set({ + hooks = { + pre_case = function() + child.restart({ "-u", "scripts/minimal_init.lua" }) + + -- Load helpers and set up environment in child process + child.lua([[ + h = require('tests.helpers') + chat, agent = h.setup_chat_buffer() + + -- Reset test globals + vim.g.codecompanion_test_setup = nil + vim.g.codecompanion_test_exit = nil + vim.g.codecompanion_test_output = nil + + -- Set up mocks + h.mock_job() + + -- Mock vim.schedule + vim.schedule = function(cb) + cb() + end + ]]) + end, + post_case = function() + child.lua([[h.teardown_chat_buffer()]]) + end, + post_once = child.stop, + }, +}) + +T["Agent"] = new_set() +T["Agent"]["cmds"] = new_set() + +T["Agent"]["cmds"]["setup and on_exit handlers are called once"] = function() + local tool = "'cmd'" + child.lua(string.format( + [[ + local cmd_xml = require("tests.strategies.chat.agents.tools.stubs.cmd_xml") + local xml = cmd_xml.tool(%s) + agent:execute(chat, xml) + vim.wait(10) + ]], + tool + )) + + local setup = child.lua_get("vim.g.codecompanion_test_setup") + local exit = child.lua_get("vim.g.codecompanion_test_exit") + + h.eq(setup, "Setup") + h.eq(exit, "Exited") +end + +-- T["Agent"]["cmds"]["output.success is called"] = function() +-- agent:execute( +-- chat, +-- [[ +-- +-- ]] +-- ) +-- h.eq("Ran with success", vim.g.codecompanion_test_output) +-- end + +return T diff --git a/tests/strategies/chat/agents/tools/stubs/cmd.lua b/tests/strategies/chat/agents/tools/stubs/cmd.lua index ea43f1813..0ee175182 100644 --- a/tests/strategies/chat/agents/tools/stubs/cmd.lua +++ b/tests/strategies/chat/agents/tools/stubs/cmd.lua @@ -4,7 +4,7 @@ return { return "my cmd system prompt" end, cmds = { - { [[make test]] }, + { "echo", "Hello World" }, }, handlers = { -- Should only be called once @@ -20,7 +20,6 @@ return { -- Should be called multiple times success = function(self, cmd, output) vim.g.codecompanion_test_output = (vim.g.codecompanion_test_output or "") .. "Ran with success" - return "stdout is populated!" end, }, } diff --git a/tests/strategies/chat/agents/tools/stubs/cmd_xml.lua b/tests/strategies/chat/agents/tools/stubs/cmd_xml.lua new file mode 100644 index 000000000..edca3129f --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/cmd_xml.lua @@ -0,0 +1,7 @@ +local M = {} + +function M.tool(name) + return string.format([[]], name) +end + +return M From fac277ff24d8e2a2d85d3ca05717f92dede391ad Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Mon, 24 Feb 2025 20:00:40 +0000 Subject: [PATCH 18/38] wip: add more tests and clean up flow --- .../strategies/chat/agents/executor/cmd.lua | 40 ++- .../chat/agents/executor/cmd_handlers.lua | 75 ------ .../strategies/chat/agents/executor/init.lua | 1 + .../strategies/chat/agents/init.lua | 6 +- tests/config.lua | 8 + .../chat/agents/executor/test_cmd.lua | 78 +++--- .../chat/agents/executor/test_func.lua | 236 +++++++++--------- .../chat/agents/tools/stubs/cmd.lua | 12 +- .../chat/agents/tools/stubs/cmd_error.lua | 23 ++ .../chat/agents/tools/stubs/cmd_xml.lua | 7 - .../chat/agents/tools/stubs/func.lua | 10 +- .../agents/tools/stubs/func_consecutive.lua | 2 +- .../chat/agents/tools/stubs/func_error.lua | 2 +- .../agents/tools/stubs/mock_cmd_runner.lua | 36 +++ .../chat/agents/tools/stubs/xml/cmd_xml.lua | 23 ++ .../tools/stubs/{ => xml}/editor_xml.lua | 0 .../chat/agents/tools/stubs/xml/func_xml.lua | 30 +++ .../chat/agents/tools/test_editor.lua | 6 +- 18 files changed, 341 insertions(+), 254 deletions(-) delete mode 100644 lua/codecompanion/strategies/chat/agents/executor/cmd_handlers.lua create mode 100644 tests/strategies/chat/agents/tools/stubs/cmd_error.lua delete mode 100644 tests/strategies/chat/agents/tools/stubs/cmd_xml.lua create mode 100644 tests/strategies/chat/agents/tools/stubs/mock_cmd_runner.lua create mode 100644 tests/strategies/chat/agents/tools/stubs/xml/cmd_xml.lua rename tests/strategies/chat/agents/tools/stubs/{ => xml}/editor_xml.lua (100%) create mode 100644 tests/strategies/chat/agents/tools/stubs/xml/func_xml.lua diff --git a/lua/codecompanion/strategies/chat/agents/executor/cmd.lua b/lua/codecompanion/strategies/chat/agents/executor/cmd.lua index 8f0a4078d..0d08cdaae 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/cmd.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/cmd.lua @@ -1,5 +1,4 @@ local Job = require("plenary.job") -local handlers = require("codecompanion.strategies.chat.agents.executor.cmd_handlers") local log = require("codecompanion.utils.log") ---@class CodeCompanion.Agent.Executor.Cmd @@ -23,6 +22,7 @@ end ---@return nil function CmdExecutor:orchestrate() log:debug("CmdExecutor:orchestrate %s", self.cmd) + self:run(self.cmd) end @@ -43,12 +43,44 @@ function CmdExecutor:run(cmd) log:debug("CmdExecutor:run %s", cmd) local job = Job:new({ - command = handlers.command(), - args = handlers.args(cmd), + command = (vim.fn.has("win32") == 1 and "cmd.exe" or "sh"), + args = { vim.fn.has("win32") == 1 and "/c" or "-c", table.concat(cmd.cmd or cmd, " ") }, enable_recording = true, cwd = vim.fn.getcwd(), on_exit = function(data, code) - handlers.on_exit(self, cmd, data, code) + log:debug("CmdExecutor:run - on_exit") + self.executor.current_cmd_tool = nil + + -- Flags can be inserted into the chat buffer to be picked up later + if cmd.flag then + self.executor.agent.chat.tool_flags = self.executor.agent.chat.tool_flags or {} + self.executor.agent.chat.tool_flags[cmd.flag] = (code == 0) + end + + vim.schedule(function() + local ok, _ = pcall(function() + if _G.codecompanion_cancel_tool then + return self.executor:close() + end + if data and data._stderr_results then + table.insert(self.executor.agent.stderr, strip_ansi(data._stderr_results)) + end + if data and data._stdout_results then + table.insert(self.executor.agent.stdout, strip_ansi(data._stdout_results)) + end + if code == 0 then + self.executor:success(cmd) + return self.executor:close() + else + return self.executor:error(cmd, string.format("Failed with code %s", code)) + end + end) + + if not ok then + log:error("Internal error running command: %s", cmd) + return self.executor:error(cmd, "Internal error") + end + end) end, }) diff --git a/lua/codecompanion/strategies/chat/agents/executor/cmd_handlers.lua b/lua/codecompanion/strategies/chat/agents/executor/cmd_handlers.lua deleted file mode 100644 index 3e4642d96..000000000 --- a/lua/codecompanion/strategies/chat/agents/executor/cmd_handlers.lua +++ /dev/null @@ -1,75 +0,0 @@ ---[[ --- The purpose of this file is to abstract away any command logic from Plenary's --- Job API. This makes it soooooo much easier to test as we decouple it from --- the cmd executor file. ---]] - -local log = require("codecompanion.utils.log") - ----@class CodeCompanion.Agent.Executor.CmdHandlers -local CmdHandlers = {} - ----Strip ANSI color codes from output ----@param tbl table ----@return table -local function strip_ansi(tbl) - for i, v in ipairs(tbl) do - tbl[i] = v:gsub("\027%[[0-9;]*%a", "") - end - return tbl -end - ----The command to run ----@return string -function CmdHandlers.command() - return vim.fn.has("win32") == 1 and "cmd.exe" or "sh" -end - ----The command arguments ----@param cmd table ----@return table -function CmdHandlers.args(cmd) - return { vim.fn.has("win32") == 1 and "/c" or "-c", table.concat(cmd.cmd or cmd, " ") } -end - ----Handle job exit ----@param executor CodeCompanion.Agent.Executor.Cmd ----@param cmd table Command being executed ----@param data table Job data containing stdout/stderr results ----@param code number Exit code -function CmdHandlers.on_exit(executor, cmd, data, code) - log:debug("CmdExecutor:run - on_exit") - executor.executor.current_cmd_tool = nil - - -- Flags can be inserted into the chat buffer to be picked up later - if cmd.flag then - executor.executor.agent.chat.tool_flags = executor.executor.agent.chat.tool_flags or {} - executor.executor.agent.chat.tool_flags[cmd.flag] = (code == 0) - end - - vim.schedule(function() - local ok, _ = pcall(function() - if _G.codecompanion_cancel_tool then - return executor.executor:close() - end - if data and data._stderr_results then - table.insert(executor.executor.agent.stderr, strip_ansi(data._stderr_results)) - end - if data and data._stdout_results then - table.insert(executor.executor.agent.stdout, strip_ansi(data._stdout_results)) - end - if code == 0 then - executor.executor:success(cmd) - return executor.executor:close() - else - return executor.executor:error(cmd, string.format("Failed with code %s", code)) - end - end) - - if not ok then - log:error("Internal error running command: %s", cmd) - end - end) -end - -return CmdHandlers diff --git a/lua/codecompanion/strategies/chat/agents/executor/init.lua b/lua/codecompanion/strategies/chat/agents/executor/init.lua index 694a25193..abf6481ef 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/init.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/init.lua @@ -89,6 +89,7 @@ function Executor:execute(index, input) if type(cmd) == "function" then return FuncExecutor.new(self, cmd, index):orchestrate(input) end + return CmdExecutor.new(self, cmd):orchestrate() end diff --git a/lua/codecompanion/strategies/chat/agents/init.lua b/lua/codecompanion/strategies/chat/agents/init.lua index b62aeec4b..cf8ec202c 100644 --- a/lua/codecompanion/strategies/chat/agents/init.lua +++ b/lua/codecompanion/strategies/chat/agents/init.lua @@ -194,7 +194,7 @@ function Agent:execute(chat, xml) return end - ---@type CodeCompanion.Tool|nil + ---@type CodeCompanion.Agent.Tool|nil local resolved_tool ok, resolved_tool = pcall(function() return Agent.resolve(self.agent_config.tools[s.tool._attr.name]) @@ -399,12 +399,12 @@ end ---Resolve a tool from the config ---@param tool table The tool from the config ----@return CodeCompanion.Tool|nil +---@return CodeCompanion.Agent.Tool|nil function Agent.resolve(tool) local callback = tool.callback if type(callback) == "table" then - return callback --[[@as CodeCompanion.Tool]] + return callback --[[@as CodeCompanion.Agent.Tool]] end local ok, module = pcall(require, "codecompanion." .. callback) diff --git a/tests/config.lua b/tests/config.lua index 5fa56e95a..2ba319630 100644 --- a/tests/config.lua +++ b/tests/config.lua @@ -84,6 +84,14 @@ return { callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/cmd.lua", description = "Cmd tool", }, + ["cmd_error"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/cmd_error.lua", + description = "Cmd tool", + }, + ["mock_cmd_runner"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/mock_cmd_runner.lua", + description = "Cmd tool", + }, opts = { system_prompt = [[My tool system prompt]], }, diff --git a/tests/strategies/chat/agents/executor/test_cmd.lua b/tests/strategies/chat/agents/executor/test_cmd.lua index 9edcd2fb3..47a150937 100644 --- a/tests/strategies/chat/agents/executor/test_cmd.lua +++ b/tests/strategies/chat/agents/executor/test_cmd.lua @@ -8,23 +8,16 @@ local T = new_set({ pre_case = function() child.restart({ "-u", "scripts/minimal_init.lua" }) - -- Load helpers and set up environment in child process + -- Load helpers and set up the environment in the child process child.lua([[ h = require('tests.helpers') chat, agent = h.setup_chat_buffer() -- Reset test globals - vim.g.codecompanion_test_setup = nil - vim.g.codecompanion_test_exit = nil - vim.g.codecompanion_test_output = nil - - -- Set up mocks - h.mock_job() - - -- Mock vim.schedule - vim.schedule = function(cb) - cb() - end + _G._test_setup = nil + _G._test_exit = nil + _G._test_order = nil + _G._test_output = nil ]]) end, post_case = function() @@ -37,33 +30,60 @@ local T = new_set({ T["Agent"] = new_set() T["Agent"]["cmds"] = new_set() -T["Agent"]["cmds"]["setup and on_exit handlers are called once"] = function() +T["Agent"]["cmds"]["handlers and outputs are called"] = function() local tool = "'cmd'" child.lua(string.format( [[ - local cmd_xml = require("tests.strategies.chat.agents.tools.stubs.cmd_xml") - local xml = cmd_xml.tool(%s) + local cmd_xml = require("tests.strategies.chat.agents.tools.stubs.xml.cmd_xml") + local xml = cmd_xml.load(%s) + agent:execute(chat, xml) + vim.wait(100) + ]], + tool + )) + + -- handlers.setup + h.eq("Setup", child.lua_get("_G._test_setup")) + -- output.success + h.eq("Hello World", child.lua_get("_G._test_output[1][1][1]")) + -- handlers.on_exit + h.eq("Exited", child.lua_get("_G._test_exit")) + + -- Order of execution + h.eq("Setup->Success->Exit", child.lua_get("_G._test_order")) +end + +T["Agent"]["cmds"]["output.errors is called"] = function() + local tool = "'cmd_error'" + child.lua(string.format( + [[ + local cmd_xml = require("tests.strategies.chat.agents.tools.stubs.xml.cmd_xml") + local xml = cmd_xml.load(%s) agent:execute(chat, xml) - vim.wait(10) + vim.wait(100) ]], tool )) - local setup = child.lua_get("vim.g.codecompanion_test_setup") - local exit = child.lua_get("vim.g.codecompanion_test_exit") + -- output.error + h.eq("Error", child.lua_get("_G._test_output")) + + -- Order of execution + h.eq("Error->Exit", child.lua_get("_G._test_order")) +end + +-- Test that flags get inserted into a chat buffer +T["Agent"]["cmds"]["can set test flags"] = function() + child.lua([[ + local cmd_xml = require("tests.strategies.chat.agents.tools.stubs.xml.cmd_xml") + local xml = cmd_xml.test_flag() + agent:execute(chat, xml) + vim.wait(100) + ]]) - h.eq(setup, "Setup") - h.eq(exit, "Exited") + h.eq({ testing = true }, child.lua_get("agent.chat.tool_flags")) end --- T["Agent"]["cmds"]["output.success is called"] = function() --- agent:execute( --- chat, --- [[ --- --- ]] --- ) --- h.eq("Ran with success", vim.g.codecompanion_test_output) --- end +-- Test multiple commands return T diff --git a/tests/strategies/chat/agents/executor/test_func.lua b/tests/strategies/chat/agents/executor/test_func.lua index cfa501671..2aae55c10 100644 --- a/tests/strategies/chat/agents/executor/test_func.lua +++ b/tests/strategies/chat/agents/executor/test_func.lua @@ -1,178 +1,170 @@ -require("tests.log") local h = require("tests.helpers") -local log = require("codecompanion.utils.log") local new_set = MiniTest.new_set -local T = new_set() -local chat, agent - -T["Agent"] = new_set({ +local child = MiniTest.new_child_neovim() +T = new_set({ hooks = { pre_case = function() - chat, agent = h.setup_chat_buffer() + child.restart({ "-u", "scripts/minimal_init.lua" }) + + -- Load helpers and set up the environment in the child process + child.lua([[ + h = require('tests.helpers') + chat, agent = h.setup_chat_buffer() + + -- Reset test globals + _G._test_func = nil + _G._test_exit = nil + _G._test_order = nil + _G._test_output = nil + _G._test_setup = nil + ]]) end, post_case = function() - h.teardown_chat_buffer() - vim.g.codecompanion_test = nil - vim.g.codecompanion_test_exit = nil - vim.g.codecompanion_test_setup = nil - vim.g.codecompanion_test_output = nil + child.lua([[h.teardown_chat_buffer()]]) end, + post_once = child.stop, }, }) +T["Agent"] = new_set() T["Agent"]["functions"] = new_set() T["Agent"]["functions"]["can run"] = function() - h.eq(vim.g.codecompanion_test, nil) - agent:execute( - chat, - [[ - - Data 1 - Data 2 - -]] - ) + h.eq(vim.NIL, child.lua_get([[_G._test_func]])) + + child.lua([[ + local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") + local xml = func_xml.two_data_points() + agent:execute(chat, xml) + ]]) -- Test that the function was called - h.eq("Data 1 Data 2", vim.g.codecompanion_test) + h.eq("Data 1 Data 2", child.lua_get([[_G._test_func]])) end T["Agent"]["functions"]["calls output.success"] = function() - h.eq(vim.g.codecompanion_test_output, nil) - - log:debug("=== TEST: Can call output.success ===") - agent:execute( - chat, - [[ - - Data 1 - Data 2 - -]] - ) - log:debug("=== TEST END ===") + h.eq(vim.NIL, child.lua_get([[_G._test_output]])) - -- Test `output.success` handler - h.eq("Ran with successRan with success", vim.g.codecompanion_test_output) + child.lua([[ + local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") + local xml = func_xml.two_data_points() + agent:execute(chat, xml) + ]]) + + -- Test that the function was called + h.eq("Ran with successRan with success", child.lua_get([[_G._test_output]])) end T["Agent"]["functions"]["calls on_exit only once"] = function() - h.eq(vim.g.codecompanion_test_exit, nil) - agent:execute( - chat, - [[ - - Data 1 - Data 2 - -]] - ) + h.eq(vim.NIL, child.lua_get([[_G._test_exit]])) + + child.lua([[ + local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") + local xml = func_xml.two_data_points() + agent:execute(chat, xml) + ]]) - -- Test that the on_exit handler was called, once - h.eq(vim.g.codecompanion_test_exit, "Exited") + -- Test that the function was called + h.eq("Exited", child.lua_get([[_G._test_exit]])) end T["Agent"]["functions"]["can run consecutively and pass input"] = function() - h.eq(vim.g.codecompanion_test, nil) - agent:execute( - chat, - [[ - - Data 1 - - ]] - ) + h.eq(vim.NIL, child.lua_get([[_G._test_func]])) + + local tool = "'func_consecutive'" + child.lua(string.format( + [[ + local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") + local xml = func_xml.one_data_point(%s) + agent:execute(chat, xml) + ]], + tool + )) -- Test that the function was called - h.eq("Data 1 Data 1", vim.g.codecompanion_test) + h.eq("Data 1 Data 1", child.lua_get([[_G._test_func]])) end T["Agent"]["functions"]["can run consecutively"] = function() - h.eq(vim.g.codecompanion_test, nil) - agent:execute( - chat, - [[ - - Data 1 - Data 2 - - ]] - ) + h.eq(vim.NIL, child.lua_get([[_G._test_func]])) + + local tool = "'func_consecutive'" + child.lua(string.format( + [[ + local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") + local xml = func_xml.two_data_points(%s) + agent:execute(chat, xml) + ]], + tool + )) -- Test that the function was called, overwriting the global variable - h.eq("Data 1 Data 2 Data 1 Data 2", vim.g.codecompanion_test) + h.eq("Data 1 Data 2 Data 1 Data 2", child.lua_get([[_G._test_func]])) end T["Agent"]["functions"]["can handle errors"] = function() - agent:execute( - chat, - [[ - - Data 1 - - ]] - ) + local tool = "'func_error'" + child.lua(string.format( + [[ + local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") + local xml = func_xml.two_data_points(%s) + agent:execute(chat, xml) + ]], + tool + )) -- Test that the `output.error` handler was called - h.eq("Something went wrong", vim.g.codecompanion_test_output) + h.eq("Something went wrong", child.lua_get([[_G._test_output]])) end T["Agent"]["functions"]["can populate stderr and halt execution"] = function() - -- Prevent stderr from being cleared out - function agent:reset() - return nil - end - - agent:execute( - chat, - [[ - - Data 1 - Data 2 - - ]] - ) + local tool = "'func_error'" + child.lua(string.format( + [[ + -- Prevent stderr from being cleared out + function agent:reset() + return nil + end + local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") + local xml = func_xml.two_data_points(%s) + agent:execute(chat, xml) + ]], + tool + )) -- Test that stderr is updated on the agent, only once - h.eq({ "Something went wrong" }, agent.stderr) + h.eq({ "Something went wrong" }, child.lua_get([[agent.stderr]])) end T["Agent"]["functions"]["can populate stdout"] = function() - -- Prevent stderr from being cleared out - function agent:reset() - return nil - end - - agent:execute( - chat, - [[ - - Data 1 - Data 2 - - ]] + child.lua([[ + -- Prevent stdout from being cleared out + function agent:reset() + return nil + end + local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") + local xml = func_xml.two_data_points() + agent:execute(chat, xml) + ]]) + + h.eq( + { { data = "Data 1", status = "success" }, { data = "Data 2", status = "success" } }, + child.lua_get([[agent.stdout]]) ) - - h.eq({ { data = "Data 1", status = "success" }, { data = "Data 2", status = "success" } }, agent.stdout) end T["Agent"]["functions"]["calls handlers.setup once"] = function() - h.eq(nil, vim.g.codecompanion_test_setup) - - agent:execute( - chat, - [[ - - Data 1 - Data 2 - - ]] - ) + h.eq(vim.NIL, child.lua_get([[_G._test_setup]])) + + child.lua([[ + local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") + local xml = func_xml.two_data_points() + agent:execute(chat, xml) + ]]) - h.eq("Setup", vim.g.codecompanion_test_setup) + h.eq("Setup", child.lua_get([[_G._test_setup]])) end return T diff --git a/tests/strategies/chat/agents/tools/stubs/cmd.lua b/tests/strategies/chat/agents/tools/stubs/cmd.lua index 0ee175182..1fe6cf1aa 100644 --- a/tests/strategies/chat/agents/tools/stubs/cmd.lua +++ b/tests/strategies/chat/agents/tools/stubs/cmd.lua @@ -9,17 +9,21 @@ return { handlers = { -- Should only be called once setup = function(self) - vim.g.codecompanion_test_setup = (vim.g.codecompanion_test_setup or "") .. "Setup" + _G._test_order = (_G._test_order or "") .. "Setup" + _G._test_setup = (_G._test_setup or "") .. "Setup" end, -- Should only be called once on_exit = function(self) - vim.g.codecompanion_test_exit = (vim.g.codecompanion_test_exit or "") .. "Exited" + _G._test_order = (_G._test_order or "") .. "->Exit" + _G._test_exit = (_G._test_exit or "") .. "Exited" end, }, output = { - -- Should be called multiple times + -- Should only be called once success = function(self, cmd, output) - vim.g.codecompanion_test_output = (vim.g.codecompanion_test_output or "") .. "Ran with success" + _G._test_order = (_G._test_order or "") .. "->Success" + _G._test_output = _G._test_output or {} + table.insert(_G._test_output, output) end, }, } diff --git a/tests/strategies/chat/agents/tools/stubs/cmd_error.lua b/tests/strategies/chat/agents/tools/stubs/cmd_error.lua new file mode 100644 index 000000000..061d1a683 --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/cmd_error.lua @@ -0,0 +1,23 @@ +return { + name = "cmd_error", + system_prompt = function(schema) + return "my cmd system prompt" + end, + cmds = { + { "echofdsfds", "Hello World" }, + }, + handlers = { + -- Should only be called once + on_exit = function(self) + _G._test_order = (_G._test_order or "") .. "->Exit" + _G._test_exit = (_G._test_exit or "") .. "Exited" + end, + }, + output = { + -- Should only be called once + error = function(self, cmd, stderr, stdout) + _G._test_output = (_G._test_output or "") .. "Error" + _G._test_order = (_G._test_order or "") .. "Error" + end, + }, +} diff --git a/tests/strategies/chat/agents/tools/stubs/cmd_xml.lua b/tests/strategies/chat/agents/tools/stubs/cmd_xml.lua deleted file mode 100644 index edca3129f..000000000 --- a/tests/strategies/chat/agents/tools/stubs/cmd_xml.lua +++ /dev/null @@ -1,7 +0,0 @@ -local M = {} - -function M.tool(name) - return string.format([[]], name) -end - -return M diff --git a/tests/strategies/chat/agents/tools/stubs/func.lua b/tests/strategies/chat/agents/tools/stubs/func.lua index b8745e9fc..eea01129f 100644 --- a/tests/strategies/chat/agents/tools/stubs/func.lua +++ b/tests/strategies/chat/agents/tools/stubs/func.lua @@ -7,27 +7,27 @@ return { ---@return { status: string, data: any } function(self, actions, input) local spacer = "" - if vim.g.codecompanion_test then + if _G._test_func then spacer = " " end - vim.g.codecompanion_test = (vim.g.codecompanion_test or "") .. spacer .. actions.data + _G._test_func = (_G._test_func or "") .. spacer .. actions.data return { status = "success", data = actions.data } end, }, handlers = { -- Should only be called once setup = function(self) - vim.g.codecompanion_test_setup = (vim.g.codecompanion_test_setup or "") .. "Setup" + _G._test_setup = (_G._test_setup or "") .. "Setup" end, -- Should only be called once on_exit = function(self) - vim.g.codecompanion_test_exit = (vim.g.codecompanion_test_exit or "") .. "Exited" + _G._test_exit = (_G._test_exit or "") .. "Exited" end, }, output = { -- Should be called multiple times success = function(self, cmd, output) - vim.g.codecompanion_test_output = (vim.g.codecompanion_test_output or "") .. "Ran with success" + _G._test_output = (_G._test_output or "") .. "Ran with success" return "stdout is populated!" end, }, diff --git a/tests/strategies/chat/agents/tools/stubs/func_consecutive.lua b/tests/strategies/chat/agents/tools/stubs/func_consecutive.lua index 2650492c1..b10ee72ef 100644 --- a/tests/strategies/chat/agents/tools/stubs/func_consecutive.lua +++ b/tests/strategies/chat/agents/tools/stubs/func_consecutive.lua @@ -10,7 +10,7 @@ return { end, function(self, actions, input) local output = input .. " " .. actions.data - vim.g.codecompanion_test = output + _G._test_func = output return output end, }, diff --git a/tests/strategies/chat/agents/tools/stubs/func_error.lua b/tests/strategies/chat/agents/tools/stubs/func_error.lua index 5756974f6..ee2c214b5 100644 --- a/tests/strategies/chat/agents/tools/stubs/func_error.lua +++ b/tests/strategies/chat/agents/tools/stubs/func_error.lua @@ -14,7 +14,7 @@ return { ---@param stderr table ---@param stdout table error = function(self, cmd, stderr, stdout) - vim.g.codecompanion_test_output = "" .. table.concat(stderr, " ") .. "" + _G._test_output = "" .. table.concat(stderr, " ") .. "" end, }, } diff --git a/tests/strategies/chat/agents/tools/stubs/mock_cmd_runner.lua b/tests/strategies/chat/agents/tools/stubs/mock_cmd_runner.lua new file mode 100644 index 000000000..c9bbd15e8 --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/mock_cmd_runner.lua @@ -0,0 +1,36 @@ +return { + name = "mock_cmd_runner", + system_prompt = function(schema) + return "my cmd system prompt" + end, + cmds = {}, + handlers = { + ---@param agent CodeCompanion.Agent The tool object + setup = function(agent) + local tool = agent.tool --[[@type CodeCompanion.Agent.Tool]] + local action = tool.request.action + local actions = vim.isarray(action) and action or { action } + + for _, act in ipairs(actions) do + local entry = { cmd = vim.split(act.command, " ") } + if act.flag then + entry.flag = act.flag + end + table.insert(tool.cmds, entry) + end + end, + + -- Should only be called once + on_exit = function(self) + _G._test_order = (_G._test_order or "") .. "->Exit" + _G._test_exit = (_G._test_exit or "") .. "Exited" + end, + }, + output = { + -- Should only be called once + error = function(self, cmd, stderr, stdout) + _G._test_output = (_G._test_output or "") .. "Error" + _G._test_order = (_G._test_order or "") .. "Error" + end, + }, +} diff --git a/tests/strategies/chat/agents/tools/stubs/xml/cmd_xml.lua b/tests/strategies/chat/agents/tools/stubs/xml/cmd_xml.lua new file mode 100644 index 000000000..d13a3ae80 --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/xml/cmd_xml.lua @@ -0,0 +1,23 @@ +local M = {} + +function M.load(name) + return string.format( + [[ + +]], + name + ) +end + +function M.test_flag() + return [[ + + + echo Hello World + testing + + +]] +end + +return M diff --git a/tests/strategies/chat/agents/tools/stubs/editor_xml.lua b/tests/strategies/chat/agents/tools/stubs/xml/editor_xml.lua similarity index 100% rename from tests/strategies/chat/agents/tools/stubs/editor_xml.lua rename to tests/strategies/chat/agents/tools/stubs/xml/editor_xml.lua diff --git a/tests/strategies/chat/agents/tools/stubs/xml/func_xml.lua b/tests/strategies/chat/agents/tools/stubs/xml/func_xml.lua new file mode 100644 index 000000000..92b18b876 --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/xml/func_xml.lua @@ -0,0 +1,30 @@ +local M = {} + +function M.two_data_points(name) + name = name or "func" + + return string.format( + [[ + + Data 1 + Data 2 + +]], + name + ) +end + +function M.one_data_point(name) + name = name or "func" + + return string.format( + [[ + + Data 1 + +]], + name + ) +end + +return M diff --git a/tests/strategies/chat/agents/tools/test_editor.lua b/tests/strategies/chat/agents/tools/test_editor.lua index d8384c94f..d7ff42fba 100644 --- a/tests/strategies/chat/agents/tools/test_editor.lua +++ b/tests/strategies/chat/agents/tools/test_editor.lua @@ -44,7 +44,7 @@ local T = new_set({ T["Agent @editor can update a buffer"] = function() child.lua( - string.format([[ _G.xml = require("tests.strategies.chat.agents.tools.stubs.editor_xml").update(%s)]], bufnr) + string.format([[ _G.xml = require("tests.strategies.chat.agents.tools.stubs.xml.editor_xml").update(%s)]], bufnr) ) child.lua([[ _G.agent:execute( @@ -60,7 +60,7 @@ end T["Agent @editor can add to a buffer"] = function() child.lua( - string.format([[ _G.xml = require("tests.strategies.chat.agents.tools.stubs.editor_xml").add(%s)]], bufnr) + string.format([[ _G.xml = require("tests.strategies.chat.agents.tools.stubs.xml.editor_xml").add(%s)]], bufnr) ) child.lua([[ _G.agent:execute( @@ -78,7 +78,7 @@ end T["Agent @editor can delete from a buffer"] = function() child.lua( - string.format([[ _G.xml = require("tests.strategies.chat.agents.tools.stubs.editor_xml").delete(%s)]], bufnr) + string.format([[ _G.xml = require("tests.strategies.chat.agents.tools.stubs.xml.editor_xml").delete(%s)]], bufnr) ) local lines = child.api.nvim_buf_get_lines(bufnr, 0, -1, false) From 8d8dde057655f9ab5727d6173361d70520b49313 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Mon, 24 Feb 2025 21:50:38 +0000 Subject: [PATCH 19/38] wip: can call multiple commands --- .../strategies/chat/agents/executor/cmd.lua | 24 ++++++++++----- .../strategies/chat/agents/executor/init.lua | 2 +- tests/config.lua | 4 +++ .../chat/agents/executor/test_cmd.lua | 26 +++++++++++----- .../agents/tools/stubs/cmd_consecutive.lua | 30 +++++++++++++++++++ .../chat/agents/tools/stubs/xml/cmd_xml.lua | 14 +++++++++ 6 files changed, 83 insertions(+), 17 deletions(-) create mode 100644 tests/strategies/chat/agents/tools/stubs/cmd_consecutive.lua diff --git a/lua/codecompanion/strategies/chat/agents/executor/cmd.lua b/lua/codecompanion/strategies/chat/agents/executor/cmd.lua index 0d08cdaae..1f3ccda11 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/cmd.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/cmd.lua @@ -3,17 +3,19 @@ local log = require("codecompanion.utils.log") ---@class CodeCompanion.Agent.Executor.Cmd ---@field executor CodeCompanion.Agent.Executor ----@field cmd table +---@field cmds table +---@field count number ---@field index number local CmdExecutor = {} ---@param executor CodeCompanion.Agent.Executor ----@param cmd table +---@param cmds table ---@param index number -function CmdExecutor.new(executor, cmd, index) +function CmdExecutor.new(executor, cmds, index) return setmetatable({ executor = executor, - cmd = cmd, + cmds = cmds, + count = vim.tbl_count(cmds), index = index, }, { __index = CmdExecutor }) end @@ -21,9 +23,11 @@ end ---Orchestrate the tool function ---@return nil function CmdExecutor:orchestrate() - log:debug("CmdExecutor:orchestrate %s", self.cmd) + log:debug("CmdExecutor:orchestrate %s", self.cmds) - self:run(self.cmd) + for i = self.index, self.count do + self:run(self.cmds[i], i) + end end ---Some commands output ANSI color codes so we need to strip them @@ -38,8 +42,9 @@ end ---Run the tool's function ---@param cmd table +---@param index number ---@return nil -function CmdExecutor:run(cmd) +function CmdExecutor:run(cmd, index) log:debug("CmdExecutor:run %s", cmd) local job = Job:new({ @@ -70,7 +75,10 @@ function CmdExecutor:run(cmd) end if code == 0 then self.executor:success(cmd) - return self.executor:close() + -- Don't trigger on_exit unless it's the last command + if index == self.count then + return self.executor:close() + end else return self.executor:error(cmd, string.format("Failed with code %s", code)) end diff --git a/lua/codecompanion/strategies/chat/agents/executor/init.lua b/lua/codecompanion/strategies/chat/agents/executor/init.lua index abf6481ef..a24267a67 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/init.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/init.lua @@ -90,7 +90,7 @@ function Executor:execute(index, input) return FuncExecutor.new(self, cmd, index):orchestrate(input) end - return CmdExecutor.new(self, cmd):orchestrate() + return CmdExecutor.new(self, self.tool.cmds, index):orchestrate() end ---Does the tool require approval before it can be executed? diff --git a/tests/config.lua b/tests/config.lua index 2ba319630..9afa32421 100644 --- a/tests/config.lua +++ b/tests/config.lua @@ -84,6 +84,10 @@ return { callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/cmd.lua", description = "Cmd tool", }, + ["cmd_consecutive"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/cmd_consecutive.lua", + description = "Cmd tool", + }, ["cmd_error"] = { callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/cmd_error.lua", description = "Cmd tool", diff --git a/tests/strategies/chat/agents/executor/test_cmd.lua b/tests/strategies/chat/agents/executor/test_cmd.lua index 47a150937..cf9a5b46d 100644 --- a/tests/strategies/chat/agents/executor/test_cmd.lua +++ b/tests/strategies/chat/agents/executor/test_cmd.lua @@ -31,16 +31,12 @@ T["Agent"] = new_set() T["Agent"]["cmds"] = new_set() T["Agent"]["cmds"]["handlers and outputs are called"] = function() - local tool = "'cmd'" - child.lua(string.format( - [[ + child.lua([[ local cmd_xml = require("tests.strategies.chat.agents.tools.stubs.xml.cmd_xml") - local xml = cmd_xml.load(%s) + local xml = cmd_xml.load() agent:execute(chat, xml) vim.wait(100) - ]], - tool - )) + ]]) -- handlers.setup h.eq("Setup", child.lua_get("_G._test_setup")) @@ -72,7 +68,6 @@ T["Agent"]["cmds"]["output.errors is called"] = function() h.eq("Error->Exit", child.lua_get("_G._test_order")) end --- Test that flags get inserted into a chat buffer T["Agent"]["cmds"]["can set test flags"] = function() child.lua([[ local cmd_xml = require("tests.strategies.chat.agents.tools.stubs.xml.cmd_xml") @@ -85,5 +80,20 @@ T["Agent"]["cmds"]["can set test flags"] = function() end -- Test multiple commands +T["Agent"]["cmds"]["can run multiple commands"] = function() + local tool = "'cmd_consecutive'" + child.lua(string.format( + [[ + local cmd_xml = require("tests.strategies.chat.agents.tools.stubs.xml.cmd_xml") + local xml = cmd_xml.load(%s) + agent:execute(chat, xml) + vim.wait(100) + ]], + tool + )) + + h.eq("Setup->Success->Success->Exit", child.lua_get("_G._test_order")) + h.eq({ { "Hello World" }, { "Hello CodeCompanion" } }, child.lua_get("_G._test_output[1]")) +end return T diff --git a/tests/strategies/chat/agents/tools/stubs/cmd_consecutive.lua b/tests/strategies/chat/agents/tools/stubs/cmd_consecutive.lua new file mode 100644 index 000000000..c252c0b43 --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/cmd_consecutive.lua @@ -0,0 +1,30 @@ +return { + name = "cmd consecutive", + system_prompt = function(schema) + return "my cmd system prompt" + end, + cmds = { + { "echo", "Hello World" }, + { "echo", "Hello CodeCompanion" }, + }, + handlers = { + -- Should only be called once + setup = function(self) + _G._test_order = (_G._test_order or "") .. "Setup" + _G._test_setup = (_G._test_setup or "") .. "Setup" + _G._test_output = {} + end, + -- Should only be called once + on_exit = function(self) + _G._test_order = (_G._test_order or "") .. "->Exit" + _G._test_exit = (_G._test_exit or "") .. "Exited" + end, + }, + output = { + -- Should only be called once + success = function(self, cmd, output) + _G._test_order = (_G._test_order or "") .. "->Success" + table.insert(_G._test_output, output) + end, + }, +} diff --git a/tests/strategies/chat/agents/tools/stubs/xml/cmd_xml.lua b/tests/strategies/chat/agents/tools/stubs/xml/cmd_xml.lua index d13a3ae80..7655b445e 100644 --- a/tests/strategies/chat/agents/tools/stubs/xml/cmd_xml.lua +++ b/tests/strategies/chat/agents/tools/stubs/xml/cmd_xml.lua @@ -1,6 +1,7 @@ local M = {} function M.load(name) + name = name or "cmd" return string.format( [[ @@ -9,6 +10,19 @@ function M.load(name) ) end +function M.multiple(tool1, tool2) + tool1 = tool1 or "cmd" + tool2 = tool2 or "cmd" + return string.format( + [[ + + +]], + tool1, + tool2 + ) +end + function M.test_flag() return [[ From 5a9f56382f0c5701d1b14183fcf3c21b3c72e43f Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Mon, 24 Feb 2025 23:23:35 +0000 Subject: [PATCH 20/38] wip: add more test coverage --- .../chat/agents/executor/test_cmd.lua | 8 +++---- .../chat/agents/executor/test_func.lua | 21 ++++++++----------- .../chat/agents/tools/stubs/func.lua | 3 +++ .../agents/tools/stubs/func_consecutive.lua | 21 +++++++++++++++++++ .../chat/agents/tools/stubs/func_error.lua | 14 +++++++++++++ 5 files changed, 51 insertions(+), 16 deletions(-) diff --git a/tests/strategies/chat/agents/executor/test_cmd.lua b/tests/strategies/chat/agents/executor/test_cmd.lua index cf9a5b46d..79e7bc927 100644 --- a/tests/strategies/chat/agents/executor/test_cmd.lua +++ b/tests/strategies/chat/agents/executor/test_cmd.lua @@ -68,7 +68,7 @@ T["Agent"]["cmds"]["output.errors is called"] = function() h.eq("Error->Exit", child.lua_get("_G._test_order")) end -T["Agent"]["cmds"]["can set test flags"] = function() +T["Agent"]["cmds"]["can set test flags on the chat object"] = function() child.lua([[ local cmd_xml = require("tests.strategies.chat.agents.tools.stubs.xml.cmd_xml") local xml = cmd_xml.test_flag() @@ -79,9 +79,7 @@ T["Agent"]["cmds"]["can set test flags"] = function() h.eq({ testing = true }, child.lua_get("agent.chat.tool_flags")) end --- Test multiple commands T["Agent"]["cmds"]["can run multiple commands"] = function() - local tool = "'cmd_consecutive'" child.lua(string.format( [[ local cmd_xml = require("tests.strategies.chat.agents.tools.stubs.xml.cmd_xml") @@ -89,10 +87,12 @@ T["Agent"]["cmds"]["can run multiple commands"] = function() agent:execute(chat, xml) vim.wait(100) ]], - tool + "'cmd_consecutive'" )) + -- on_exit should only be called at the end h.eq("Setup->Success->Success->Exit", child.lua_get("_G._test_order")) + -- output.success should be called for each command h.eq({ { "Hello World" }, { "Hello CodeCompanion" } }, child.lua_get("_G._test_output[1]")) end diff --git a/tests/strategies/chat/agents/executor/test_func.lua b/tests/strategies/chat/agents/executor/test_func.lua index 2aae55c10..d94914e2e 100644 --- a/tests/strategies/chat/agents/executor/test_func.lua +++ b/tests/strategies/chat/agents/executor/test_func.lua @@ -40,6 +40,9 @@ T["Agent"]["functions"]["can run"] = function() agent:execute(chat, xml) ]]) + -- Test order + h.eq("Setup->Success->Success->Exit", child.lua_get([[_G._test_order]])) + -- Test that the function was called h.eq("Data 1 Data 2", child.lua_get([[_G._test_func]])) end @@ -83,6 +86,8 @@ T["Agent"]["functions"]["can run consecutively and pass input"] = function() tool )) + h.eq("Setup->Success->Success->Exit", child.lua_get([[_G._test_order]])) + -- Test that the function was called h.eq("Data 1 Data 1", child.lua_get([[_G._test_func]])) end @@ -100,6 +105,8 @@ T["Agent"]["functions"]["can run consecutively"] = function() tool )) + h.eq("Setup->Success->Success->Success->Success->Exit", child.lua_get([[_G._test_order]])) + -- Test that the function was called, overwriting the global variable h.eq("Data 1 Data 2 Data 1 Data 2", child.lua_get([[_G._test_func]])) end @@ -115,6 +122,8 @@ T["Agent"]["functions"]["can handle errors"] = function() tool )) + h.eq("Setup->Error->Exit", child.lua_get([[_G._test_order]])) + -- Test that the `output.error` handler was called h.eq("Something went wrong", child.lua_get([[_G._test_output]])) end @@ -155,16 +164,4 @@ T["Agent"]["functions"]["can populate stdout"] = function() ) end -T["Agent"]["functions"]["calls handlers.setup once"] = function() - h.eq(vim.NIL, child.lua_get([[_G._test_setup]])) - - child.lua([[ - local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") - local xml = func_xml.two_data_points() - agent:execute(chat, xml) - ]]) - - h.eq("Setup", child.lua_get([[_G._test_setup]])) -end - return T diff --git a/tests/strategies/chat/agents/tools/stubs/func.lua b/tests/strategies/chat/agents/tools/stubs/func.lua index eea01129f..873fdfad2 100644 --- a/tests/strategies/chat/agents/tools/stubs/func.lua +++ b/tests/strategies/chat/agents/tools/stubs/func.lua @@ -17,16 +17,19 @@ return { handlers = { -- Should only be called once setup = function(self) + _G._test_order = (_G._test_order or "") .. "Setup" _G._test_setup = (_G._test_setup or "") .. "Setup" end, -- Should only be called once on_exit = function(self) + _G._test_order = (_G._test_order or "") .. "->Exit" _G._test_exit = (_G._test_exit or "") .. "Exited" end, }, output = { -- Should be called multiple times success = function(self, cmd, output) + _G._test_order = (_G._test_order or "") .. "->Success" _G._test_output = (_G._test_output or "") .. "Ran with success" return "stdout is populated!" end, diff --git a/tests/strategies/chat/agents/tools/stubs/func_consecutive.lua b/tests/strategies/chat/agents/tools/stubs/func_consecutive.lua index b10ee72ef..04fe7a542 100644 --- a/tests/strategies/chat/agents/tools/stubs/func_consecutive.lua +++ b/tests/strategies/chat/agents/tools/stubs/func_consecutive.lua @@ -14,4 +14,25 @@ return { return output end, }, + handlers = { + -- Should only be called once + setup = function(self) + _G._test_order = (_G._test_order or "") .. "Setup" + _G._test_setup = (_G._test_setup or "") .. "Setup" + end, + -- Should only be called once + on_exit = function(self) + _G._test_order = (_G._test_order or "") .. "->Exit" + _G._test_exit = (_G._test_exit or "") .. "Exited" + end, + }, + + output = { + -- Should be called multiple times + success = function(self, cmd, output) + _G._test_order = (_G._test_order or "") .. "->Success" + _G._test_output = (_G._test_output or "") .. "Ran with success" + return "stdout is populated!" + end, + }, } diff --git a/tests/strategies/chat/agents/tools/stubs/func_error.lua b/tests/strategies/chat/agents/tools/stubs/func_error.lua index ee2c214b5..37f787799 100644 --- a/tests/strategies/chat/agents/tools/stubs/func_error.lua +++ b/tests/strategies/chat/agents/tools/stubs/func_error.lua @@ -8,12 +8,26 @@ return { return error("Something went wrong") end, }, + handlers = { + -- Should only be called once + setup = function(self) + _G._test_order = (_G._test_order or "") .. "Setup" + _G._test_setup = (_G._test_setup or "") .. "Setup" + end, + -- Should only be called once + on_exit = function(self) + _G._test_order = (_G._test_order or "") .. "->Exit" + _G._test_exit = (_G._test_exit or "") .. "Exited" + end, + }, + output = { ---@param self CodeCompanion.Agent ---@param cmd string ---@param stderr table ---@param stdout table error = function(self, cmd, stderr, stdout) + _G._test_order = (_G._test_order or "") .. "->Error" _G._test_output = "" .. table.concat(stderr, " ") .. "" end, }, From 905eee39ead7b496f4063f396944c24e1184c6e3 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Tue, 25 Feb 2025 08:29:46 +0000 Subject: [PATCH 21/38] wip: remove unused method --- lua/codecompanion/strategies/chat/agents/init.lua | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lua/codecompanion/strategies/chat/agents/init.lua b/lua/codecompanion/strategies/chat/agents/init.lua index cf8ec202c..7486771fe 100644 --- a/lua/codecompanion/strategies/chat/agents/init.lua +++ b/lua/codecompanion/strategies/chat/agents/init.lua @@ -34,16 +34,6 @@ local CONSTANTS = { PROCESSING_MSG = "Tool processing ...", } ----Some commands output ANSI color codes so we need to strip them ----@param tbl table ----@return table -local function strip_ansi(tbl) - for i, v in ipairs(tbl) do - tbl[i] = v:gsub("\027%[[0-9;]*%a", "") - end - return tbl -end - ---Parse XML in a given message ---@param message string ---@return table From 919592ae455c553a8a11be3757d10070007ec787 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Tue, 25 Feb 2025 21:32:00 +0000 Subject: [PATCH 22/38] wip: add integration test --- tests/config.lua | 12 +++++ .../chat/agents/executor/test_integration.lua | 54 +++++++++++++++++++ .../agents/tools/stubs/cmd_integration.lua | 29 ++++++++++ .../agents/tools/stubs/func_integration.lua | 37 +++++++++++++ .../agents/tools/stubs/func_integration_2.lua | 37 +++++++++++++ .../tools/stubs/xml/integration_xml.lua | 18 +++++++ 6 files changed, 187 insertions(+) create mode 100644 tests/strategies/chat/agents/executor/test_integration.lua create mode 100644 tests/strategies/chat/agents/tools/stubs/cmd_integration.lua create mode 100644 tests/strategies/chat/agents/tools/stubs/func_integration.lua create mode 100644 tests/strategies/chat/agents/tools/stubs/func_integration_2.lua create mode 100644 tests/strategies/chat/agents/tools/stubs/xml/integration_xml.lua diff --git a/tests/config.lua b/tests/config.lua index 9afa32421..c30fb0dfa 100644 --- a/tests/config.lua +++ b/tests/config.lua @@ -80,6 +80,14 @@ return { callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func_error.lua", description = "Error function tool to test", }, + ["func_integration"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func_integration.lua", + description = "Some function tool to test", + }, + ["func_integration_2"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func_integration_2.lua", + description = "Some function tool to test", + }, ["cmd"] = { callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/cmd.lua", description = "Cmd tool", @@ -92,6 +100,10 @@ return { callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/cmd_error.lua", description = "Cmd tool", }, + ["cmd_integration"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/cmd_integration.lua", + description = "Cmd tool", + }, ["mock_cmd_runner"] = { callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/mock_cmd_runner.lua", description = "Cmd tool", diff --git a/tests/strategies/chat/agents/executor/test_integration.lua b/tests/strategies/chat/agents/executor/test_integration.lua new file mode 100644 index 000000000..2fbd44715 --- /dev/null +++ b/tests/strategies/chat/agents/executor/test_integration.lua @@ -0,0 +1,54 @@ +local h = require("tests.helpers") + +local new_set = MiniTest.new_set + +local child = MiniTest.new_child_neovim() +T = new_set({ + hooks = { + pre_case = function() + child.restart({ "-u", "scripts/minimal_init.lua" }) + + -- Load helpers and set up the environment in the child process + child.lua([[ + h = require('tests.helpers') + chat, agent = h.setup_chat_buffer() + + -- Reset test globals + _G._test_func = nil + _G._test_exit = nil + _G._test_order = nil + _G._test_output = nil + _G._test_setup = nil + ]]) + end, + post_case = function() + child.lua([[h.teardown_chat_buffer()]]) + end, + post_once = child.stop, + }, +}) + +T["Agent"] = new_set() +T["Agent"]["integration"] = new_set() + +T["Agent"]["integration"]["can run func->cmd->func"] = function() + h.eq(vim.NIL, child.lua_get([[_G._test_order]])) + + child.lua([[ + local integration_test = require("tests.strategies.chat.agents.tools.stubs.xml.integration_xml") + local xml = integration_test.run() + agent:execute(chat, xml) + vim.wait(1000) + ]]) + + -- Test order + h.eq( + "Func[Setup]->Func[Success]->Func[Exit]->Cmd[Setup]->Cmd[Success]->Cmd[Exit]->Func2[Setup]->Func2[Success]->Func2[Exit]", + child.lua_get([[_G._test_order]]) + ) + + -- Test that the function was called + -- h.eq("Data 1 Data 2", child.lua_get([[_G._test_func]])) +end + +return T diff --git a/tests/strategies/chat/agents/tools/stubs/cmd_integration.lua b/tests/strategies/chat/agents/tools/stubs/cmd_integration.lua new file mode 100644 index 000000000..84a2504d7 --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/cmd_integration.lua @@ -0,0 +1,29 @@ +return { + name = "cmd_integrate", + system_prompt = function(schema) + return "my cmd system prompt" + end, + cmds = { + { "sleep", "0.5" }, + }, + handlers = { + -- Should only be called once + setup = function(self) + _G._test_order = (_G._test_order or "") .. "->Cmd[Setup]" + _G._test_setup = (_G._test_setup or "") .. "Setup" + end, + -- Should only be called once + on_exit = function(self) + _G._test_order = (_G._test_order or "") .. "->Cmd[Exit]" + _G._test_exit = (_G._test_exit or "") .. "Exited" + end, + }, + output = { + -- Should only be called once + success = function(self, cmd, output) + _G._test_order = (_G._test_order or "") .. "->Cmd[Success]" + _G._test_output = _G._test_output or {} + table.insert(_G._test_output, output) + end, + }, +} diff --git a/tests/strategies/chat/agents/tools/stubs/func_integration.lua b/tests/strategies/chat/agents/tools/stubs/func_integration.lua new file mode 100644 index 000000000..a1bc2bdd2 --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/func_integration.lua @@ -0,0 +1,37 @@ +return { + name = "func_integrate", + system_prompt = function(schema) + return "my func system prompt" + end, + cmds = { + ---@return { status: string, data: any } + function(self, actions, input) + local spacer = "" + if _G._test_func then + spacer = " " + end + _G._test_func = (_G._test_func or "") .. spacer .. actions.data + return { status = "success", data = actions.data } + end, + }, + handlers = { + -- Should only be called once + setup = function(self) + _G._test_order = (_G._test_order or "") .. "Func[Setup]" + _G._test_setup = (_G._test_setup or "") .. "Setup" + end, + -- Should only be called once + on_exit = function(self) + _G._test_order = (_G._test_order or "") .. "->Func[Exit]" + _G._test_exit = (_G._test_exit or "") .. "Exited" + end, + }, + output = { + -- Should be called multiple times + success = function(self, cmd, output) + _G._test_order = (_G._test_order or "") .. "->Func[Success]" + _G._test_output = (_G._test_output or "") .. "Ran with success" + return "stdout is populated!" + end, + }, +} diff --git a/tests/strategies/chat/agents/tools/stubs/func_integration_2.lua b/tests/strategies/chat/agents/tools/stubs/func_integration_2.lua new file mode 100644 index 000000000..6698cd6c5 --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/func_integration_2.lua @@ -0,0 +1,37 @@ +return { + name = "func_integrate2", + system_prompt = function(schema) + return "my func system prompt" + end, + cmds = { + ---@return { status: string, data: any } + function(self, actions, input) + local spacer = "" + if _G._test_func then + spacer = " " + end + _G._test_func = (_G._test_func or "") .. spacer .. actions.data + return { status = "success", data = actions.data } + end, + }, + handlers = { + -- Should only be called once + setup = function(self) + _G._test_order = (_G._test_order or "") .. "->Func2[Setup]" + _G._test_setup = (_G._test_setup or "") .. "Setup" + end, + -- Should only be called once + on_exit = function(self) + _G._test_order = (_G._test_order or "") .. "->Func2[Exit]" + _G._test_exit = (_G._test_exit or "") .. "Exited" + end, + }, + output = { + -- Should be called multiple times + success = function(self, cmd, output) + _G._test_order = (_G._test_order or "") .. "->Func2[Success]" + _G._test_output = (_G._test_output or "") .. "Ran with success" + return "stdout is populated!" + end, + }, +} diff --git a/tests/strategies/chat/agents/tools/stubs/xml/integration_xml.lua b/tests/strategies/chat/agents/tools/stubs/xml/integration_xml.lua new file mode 100644 index 000000000..ac4ea3e3c --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/xml/integration_xml.lua @@ -0,0 +1,18 @@ +local M = {} + +function M.run(name) + return string.format( + [[ + + Data 1 + + + + Data 2 + +]], + name + ) +end + +return M From c246689e3388c987d940c58c95fe57252e5ae12f Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Tue, 25 Feb 2025 22:20:02 +0000 Subject: [PATCH 23/38] wip: update test names --- .../executor/{test_integration.lua => test_queue.lua} | 8 ++++---- .../tools/stubs/{cmd_integration.lua => cmd_queue.lua} | 2 +- .../tools/stubs/{func_integration.lua => func_queue.lua} | 2 +- .../stubs/{func_integration_2.lua => func_queue_2.lua} | 2 +- .../stubs/xml/{integration_xml.lua => queue_xml.lua} | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) rename tests/strategies/chat/agents/executor/{test_integration.lua => test_queue.lua} (82%) rename tests/strategies/chat/agents/tools/stubs/{cmd_integration.lua => cmd_queue.lua} (96%) rename tests/strategies/chat/agents/tools/stubs/{func_integration.lua => func_queue.lua} (97%) rename tests/strategies/chat/agents/tools/stubs/{func_integration_2.lua => func_queue_2.lua} (97%) rename tests/strategies/chat/agents/tools/stubs/xml/{integration_xml.lua => queue_xml.lua} (69%) diff --git a/tests/strategies/chat/agents/executor/test_integration.lua b/tests/strategies/chat/agents/executor/test_queue.lua similarity index 82% rename from tests/strategies/chat/agents/executor/test_integration.lua rename to tests/strategies/chat/agents/executor/test_queue.lua index 2fbd44715..6b77415e4 100644 --- a/tests/strategies/chat/agents/executor/test_integration.lua +++ b/tests/strategies/chat/agents/executor/test_queue.lua @@ -29,14 +29,14 @@ T = new_set({ }) T["Agent"] = new_set() -T["Agent"]["integration"] = new_set() +T["Agent"]["queue"] = new_set() -T["Agent"]["integration"]["can run func->cmd->func"] = function() +T["Agent"]["queue"]["can queue functions and commands"] = function() h.eq(vim.NIL, child.lua_get([[_G._test_order]])) child.lua([[ - local integration_test = require("tests.strategies.chat.agents.tools.stubs.xml.integration_xml") - local xml = integration_test.run() + local queue = require("tests.strategies.chat.agents.tools.stubs.xml.queue_xml") + local xml = queue.run() agent:execute(chat, xml) vim.wait(1000) ]]) diff --git a/tests/strategies/chat/agents/tools/stubs/cmd_integration.lua b/tests/strategies/chat/agents/tools/stubs/cmd_queue.lua similarity index 96% rename from tests/strategies/chat/agents/tools/stubs/cmd_integration.lua rename to tests/strategies/chat/agents/tools/stubs/cmd_queue.lua index 84a2504d7..782b8564e 100644 --- a/tests/strategies/chat/agents/tools/stubs/cmd_integration.lua +++ b/tests/strategies/chat/agents/tools/stubs/cmd_queue.lua @@ -1,5 +1,5 @@ return { - name = "cmd_integrate", + name = "cmd_queue", system_prompt = function(schema) return "my cmd system prompt" end, diff --git a/tests/strategies/chat/agents/tools/stubs/func_integration.lua b/tests/strategies/chat/agents/tools/stubs/func_queue.lua similarity index 97% rename from tests/strategies/chat/agents/tools/stubs/func_integration.lua rename to tests/strategies/chat/agents/tools/stubs/func_queue.lua index a1bc2bdd2..72ee6de6c 100644 --- a/tests/strategies/chat/agents/tools/stubs/func_integration.lua +++ b/tests/strategies/chat/agents/tools/stubs/func_queue.lua @@ -1,5 +1,5 @@ return { - name = "func_integrate", + name = "func_queue", system_prompt = function(schema) return "my func system prompt" end, diff --git a/tests/strategies/chat/agents/tools/stubs/func_integration_2.lua b/tests/strategies/chat/agents/tools/stubs/func_queue_2.lua similarity index 97% rename from tests/strategies/chat/agents/tools/stubs/func_integration_2.lua rename to tests/strategies/chat/agents/tools/stubs/func_queue_2.lua index 6698cd6c5..6a12aa0a5 100644 --- a/tests/strategies/chat/agents/tools/stubs/func_integration_2.lua +++ b/tests/strategies/chat/agents/tools/stubs/func_queue_2.lua @@ -1,5 +1,5 @@ return { - name = "func_integrate2", + name = "func_queue_2", system_prompt = function(schema) return "my func system prompt" end, diff --git a/tests/strategies/chat/agents/tools/stubs/xml/integration_xml.lua b/tests/strategies/chat/agents/tools/stubs/xml/queue_xml.lua similarity index 69% rename from tests/strategies/chat/agents/tools/stubs/xml/integration_xml.lua rename to tests/strategies/chat/agents/tools/stubs/xml/queue_xml.lua index ac4ea3e3c..31f39b132 100644 --- a/tests/strategies/chat/agents/tools/stubs/xml/integration_xml.lua +++ b/tests/strategies/chat/agents/tools/stubs/xml/queue_xml.lua @@ -3,11 +3,11 @@ local M = {} function M.run(name) return string.format( [[ - + Data 1 - - + + Data 2 ]], From ed04276e84840045589ba62cf1aeb884f3d878f8 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Tue, 25 Feb 2025 22:25:36 +0000 Subject: [PATCH 24/38] wip: update test names --- tests/config.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/config.lua b/tests/config.lua index c30fb0dfa..e167a85a0 100644 --- a/tests/config.lua +++ b/tests/config.lua @@ -80,12 +80,12 @@ return { callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func_error.lua", description = "Error function tool to test", }, - ["func_integration"] = { - callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func_integration.lua", + ["func_queue"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func_queue.lua", description = "Some function tool to test", }, - ["func_integration_2"] = { - callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func_integration_2.lua", + ["func_queue_2"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func_queue_2.lua", description = "Some function tool to test", }, ["cmd"] = { @@ -100,8 +100,8 @@ return { callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/cmd_error.lua", description = "Cmd tool", }, - ["cmd_integration"] = { - callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/cmd_integration.lua", + ["cmd_queue"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/cmd_queue.lua", description = "Cmd tool", }, ["mock_cmd_runner"] = { From c65f88fbc84ebace441372e56764e1755c8dabb6 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Sat, 1 Mar 2025 13:04:01 +0000 Subject: [PATCH 25/38] wip: finalize queue implementation --- codecompanion-workspace.json | 32 ++++++++ .../strategies/chat/agents/executor/cmd.lua | 7 +- .../strategies/chat/agents/executor/func.lua | 57 ++++++++----- .../strategies/chat/agents/executor/init.lua | 68 +++++++++------- .../strategies/chat/agents/executor/queue.lua | 80 +++++++++++++++++++ .../strategies/chat/agents/init.lua | 13 ++- .../chat/agents/executor/test_func.lua | 66 +++++++-------- .../chat/agents/executor/test_queue.lua | 3 +- .../chat/agents/tools/stubs/cmd_queue.lua | 1 - .../agents/tools/stubs/func_consecutive.lua | 3 + 10 files changed, 240 insertions(+), 90 deletions(-) create mode 100644 lua/codecompanion/strategies/chat/agents/executor/queue.lua diff --git a/codecompanion-workspace.json b/codecompanion-workspace.json index a9f1caaff..345e71555 100644 --- a/codecompanion-workspace.json +++ b/codecompanion-workspace.json @@ -140,6 +140,38 @@ "url": "https://raw.githubusercontent.com/olimorris/codecompanion.nvim/refs/heads/main/lua/codecompanion/strategies/inline/init.lua" } ] + }, + { + "name": "Tools", + "system_prompt": "In the CodeCompanion plugin, tools can be leveraged by an LLM to execute lua functions or shell commands on the users machine. By responding with XML, CodeCompanion will pass the response, call the corresponding tool. This feature has been implemented via the agent/init.lua file, which passes all of the tools and adds them to a queue. Then those tools are run consecutively by the executor/init.lua file.", + "opts": { + "remove_config_system_prompt": true + }, + "vars": { + "base_dir": "lua/codecompanion/strategies/chat/agents" + }, + "files": [ + { + "description": "This is the entry point for the agent. If XML is detected in an LLM's response then this file is triggered which in turns add tools to a queue before calling the executor", + "path": "${base_dir}/init.lua" + }, + { + "description": "The executor file then runs the tools in the queue, whether they're functions or commands:", + "path": "${base_dir}/executor/init.lua" + }, + { + "description": "This is how function tools are run:", + "path": "${base_dir}/executor/func.lua" + }, + { + "description": "This is how command tools are run:", + "path": "${base_dir}/executor/cmd.lua" + }, + { + "description": "This is the queue implementation", + "path": "${base_dir}/executor/queue.lua" + } + ] } ] } diff --git a/lua/codecompanion/strategies/chat/agents/executor/cmd.lua b/lua/codecompanion/strategies/chat/agents/executor/cmd.lua index 1f3ccda11..8a409f70f 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/cmd.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/cmd.lua @@ -63,7 +63,7 @@ function CmdExecutor:run(cmd, index) end vim.schedule(function() - local ok, _ = pcall(function() + local ok, output = pcall(function() if _G.codecompanion_cancel_tool then return self.executor:close() end @@ -77,7 +77,8 @@ function CmdExecutor:run(cmd, index) self.executor:success(cmd) -- Don't trigger on_exit unless it's the last command if index == self.count then - return self.executor:close() + self.executor:close() + return self.executor:execute() end else return self.executor:error(cmd, string.format("Failed with code %s", code)) @@ -85,7 +86,7 @@ function CmdExecutor:run(cmd, index) end) if not ok then - log:error("Internal error running command: %s", cmd) + log:error("Internal error running command %s: %s", cmd, output) return self.executor:error(cmd, "Internal error") end end) diff --git a/lua/codecompanion/strategies/chat/agents/executor/func.lua b/lua/codecompanion/strategies/chat/agents/executor/func.lua index e7b617222..3966cc835 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/func.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/func.lua @@ -23,26 +23,49 @@ end function FuncExecutor:orchestrate(input) log:debug("FuncExecutor:orchestrate %s", self.index) local action = self.executor.tool.request.action - if type(action) == "table" and type(action[1]) == "table" then - ---Process all actions in sequence without creating new execution chains - ---@param idx number The index - ---@param prev_input? any - ---@return nil - local function process_actions(idx, prev_input) - if idx > #action then - -- All actions processed, continue to next command - return self.executor:execute(self.index + 1, prev_input) - end + log:debug("Action: %s", action) - -- Allow the action to call the next action directly, without calling `Executor:execute` - self:run(self.func, action[idx], prev_input, function(output) - process_actions(idx + 1, output) - end) + if type(action) == "table" and vim.isarray(action) and action[1] ~= nil then + -- Handle multiple functions in the cmds array + self:process_action_array(action, input) + else + self:run(self.func, action, input, function(output) + self:proceed_to_next(output) + end) + end +end + +---Process an array of actions sequentially +---@param actions table Array of actions +---@param input any Input for the first action +---@return nil +function FuncExecutor:process_action_array(actions, input) + local function process_actions(idx, prev_input) + if idx > #actions then + -- All actions processed, continue to next command + return self:proceed_to_next(prev_input) end - process_actions(1, input) + -- Process each action and chain them together + self:run(self.func, actions[idx], prev_input, function(output) + process_actions(idx + 1, output) + end) + end + + process_actions(1, input) +end + +---Move to the next function in the command chain or finish execution +---@param output any The output from the previous function +---@return nil +function FuncExecutor:proceed_to_next(output) + if self.index < #self.executor.tool.cmds then + local next_func = self.executor.tool.cmds[self.index + 1] + local next_executor = FuncExecutor.new(self.executor, next_func, self.index + 1) + return next_executor:orchestrate(output) else - self:run(self.func, action, input) + self.executor:close() + return self.executor:execute(output) end end @@ -65,8 +88,6 @@ function FuncExecutor:run(func, action, input, callback) if callback then callback(output) - else - return self.executor:execute(self.index + 1, output) end end diff --git a/lua/codecompanion/strategies/chat/agents/executor/init.lua b/lua/codecompanion/strategies/chat/agents/executor/init.lua index a24267a67..b610ff4d8 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/init.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/init.lua @@ -1,5 +1,6 @@ local CmdExecutor = require("codecompanion.strategies.chat.agents.executor.cmd") local FuncExecutor = require("codecompanion.strategies.chat.agents.executor.func") +local Queue = require("codecompanion.strategies.chat.agents.executor.queue") local config = require("codecompanion.config") local log = require("codecompanion.utils.log") local util = require("codecompanion.utils") @@ -11,36 +12,45 @@ local util = require("codecompanion.utils") ---@field index number The index of the current command ---@field output table ---@field tool CodeCompanion.Agent.Tool +---@field queue CodeCompanion.Agent.Executor.Queue ---@field status string local Executor = {} ---@param agent CodeCompanion.Agent ----@param tool CodeCompanion.Agent.Tool -function Executor.new(agent, tool) - log:debug("Executor.new: %s", tool.name) - +function Executor.new(agent) local self = setmetatable({ agent = agent, current_cmd_tool = {}, - tool = tool, + id = math.random(10000000), + queue = Queue.new(), }, { __index = Executor }) + _G.codecompanion_cancel_tool = false + -- util.fire("AgentStarted", { tool = tool.name, bufnr = agent.bufnr }) + -- self.handlers.setup() + + return self +end + +---Add the tool's handlers to the executor +---@return nil +function Executor:setup_handlers() self.handlers = { setup = function() vim.g.codecompanion_current_tool = self.tool.name if self.tool.handlers and self.tool.handlers.setup then - self.tool.handlers.setup(agent) + self.tool.handlers.setup(self.agent) end end, approved = function(cmd) if self.tool.handlers and self.tool.handlers.approved then - return self.tool.handlers.approved(agent, cmd) + return self.tool.handlers.approved(self.agent, cmd) end return true end, on_exit = function() if self.tool.handlers and self.tool.handlers.on_exit then - self.tool.handlers.on_exit(agent) + self.tool.handlers.on_exit(self.agent) end end, } @@ -48,49 +58,45 @@ function Executor.new(agent, tool) self.output = { rejected = function(cmd) if self.tool.output and self.tool.output.rejected then - self.tool.output.rejected(agent, cmd) + self.tool.output.rejected(self.agent, cmd) end end, error = function(cmd, error, output) if self.tool.output and self.tool.output.error then - self.tool.output.error(agent, cmd, error, output) + self.tool.output.error(self.agent, cmd, error, output) end end, success = function(cmd, output) if self.tool.output and self.tool.output.success then - self.tool.output.success(agent, cmd, output) + self.tool.output.success(self.agent, cmd, output) end end, } - - _G.codecompanion_cancel_tool = false - util.fire("AgentStarted", { tool = tool.name, bufnr = agent.bufnr }) - self.handlers.setup() - - return self end ---Execute the tool command ----@param index? number The index of the command to execute ---@param input? any ---@return nil -function Executor:execute(index, input) - index = index or 1 - log:debug("Executor:execute %s", index) - if - not self.tool.cmds - or index > vim.tbl_count(self.tool.cmds) - or self.agent.status == self.agent.constants.STATUS_ERROR - then - return self:close() +function Executor:execute(input) + if self.queue:is_empty() or self.agent.status == self.agent.constants.STATUS_ERROR then + log:debug("Executor:execute - Queue empty or error") + return end - local cmd = self.tool.cmds[index] + -- Get the next tool to run + self.tool = self.queue:pop() + + -- Setup its handlers + self:setup_handlers() + + local cmd = self.tool.cmds[1] + log:debug("Executor:execute - `%s` tool", self.tool.name) + self.handlers.setup() + if type(cmd) == "function" then - return FuncExecutor.new(self, cmd, index):orchestrate(input) + return FuncExecutor.new(self, cmd, 1):orchestrate(input) end - - return CmdExecutor.new(self, self.tool.cmds, index):orchestrate() + return CmdExecutor.new(self, self.tool.cmds, 1):orchestrate() end ---Does the tool require approval before it can be executed? diff --git a/lua/codecompanion/strategies/chat/agents/executor/queue.lua b/lua/codecompanion/strategies/chat/agents/executor/queue.lua new file mode 100644 index 000000000..323b295dc --- /dev/null +++ b/lua/codecompanion/strategies/chat/agents/executor/queue.lua @@ -0,0 +1,80 @@ +-- Simple Queue implementation +-- Based on deque by Pierre 'catwell' Chapuis +-- Ref: https://github.com/catwell/cw-lua/blob/master/deque/deque.lua + +---Add an item to the back of the queue +---@param self table +---@param x any +---@return nil +local push = function(self, x) + assert(x ~= nil) + self.tail = self.tail + 1 + self[self.tail] = x +end + +---Remove and return an item from the front of the queue +---@param self table +---@return any|nil The removed item or nil if queue is empty +local pop = function(self) + if self:is_empty() then + return nil + end + local r = self[self.head + 1] + self.head = self.head + 1 + local r = self[self.head] + self[self.head] = nil + return r +end + +---Get the number of items in the queue +---@param self table +---@return number Number of items in the queue +local count = function(self) + return self.tail - self.head +end + +---Check if the queue is empty +---@param self table +---@return boolean +local is_empty = function(self) + return self:count() == 0 +end + +---Get all items in the queue as a table +---@param self table +---@return table All queue items in order +local contents = function(self) + local r = {} + for i = self.head + 1, self.tail do + r[i - self.head] = self[i] + end + return r +end + +local methods = { + push = push, + pop = pop, + count = count, + is_empty = is_empty, + contents = contents, +} + +---Create a new queue +---@return table A new empty queue instance +local new = function() + local r = { head = 0, tail = 0 } + return setmetatable(r, { __index = methods }) +end + +---@class CodeCompanion.Agent.Executor.Queue +---@field head number Internal head pointer +---@field tail number Internal tail pointer +---@field push fun(self: CodeCompanion.Agent.Executor.Queue, x: any): nil Add an item to the back of the queue +---@field pop fun(self: CodeCompanion.Agent.Executor.Queue): any|nil Remove and return an item from the front of the queue +---@field count fun(self: CodeCompanion.Agent.Executor.Queue): number Get the number of items in the queue +---@field is_empty fun(self: CodeCompanion.Agent.Executor.Queue): boolean Check if the queue is empty +---@field contents fun(self: CodeCompanion.Agent.Executor.Queue): table Get all items in the queue as a table + +return { + new = new, +} diff --git a/lua/codecompanion/strategies/chat/agents/init.lua b/lua/codecompanion/strategies/chat/agents/init.lua index 7486771fe..8a4b101c9 100644 --- a/lua/codecompanion/strategies/chat/agents/init.lua +++ b/lua/codecompanion/strategies/chat/agents/init.lua @@ -177,8 +177,9 @@ function Agent:execute(chat, xml) end ---Resolve and run the tool + ---@param executor CodeCompanion.Agent.Executor The executor instance ---@param s table The tool's schema - local function run_tool(s) + local function run_tool(executor, s) -- If an error occurred, don't run any more tools if self.status == CONSTANTS.STATUS_ERROR then return @@ -206,18 +207,22 @@ function Agent:execute(chat, xml) util.replace_placeholders(self.tool.cmds, env) end - return Executor.new(self, self.tool):execute() + return executor.queue:push(self.tool) end + local executor = Executor.new(self) + -- This allows us to run multiple tools in a single response whether they're in -- their own XML block or they're in an array within the tag if vim.isarray(schema.tool) then vim.iter(schema.tool):each(function(tool) - run_tool({ tool = tool }) + run_tool(executor, { tool = tool }) end) else - return run_tool(schema) + run_tool(executor, schema) end + + return executor:execute() end ---Look for tools in a given message diff --git a/tests/strategies/chat/agents/executor/test_func.lua b/tests/strategies/chat/agents/executor/test_func.lua index d94914e2e..73bcfb4e9 100644 --- a/tests/strategies/chat/agents/executor/test_func.lua +++ b/tests/strategies/chat/agents/executor/test_func.lua @@ -35,6 +35,7 @@ T["Agent"]["functions"]["can run"] = function() h.eq(vim.NIL, child.lua_get([[_G._test_func]])) child.lua([[ + --require("tests.log") local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") local xml = func_xml.two_data_points() agent:execute(chat, xml) @@ -64,10 +65,10 @@ T["Agent"]["functions"]["calls on_exit only once"] = function() h.eq(vim.NIL, child.lua_get([[_G._test_exit]])) child.lua([[ - local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") - local xml = func_xml.two_data_points() - agent:execute(chat, xml) - ]]) + local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") + local xml = func_xml.two_data_points() + agent:execute(chat, xml) + ]]) -- Test that the function was called h.eq("Exited", child.lua_get([[_G._test_exit]])) @@ -79,10 +80,11 @@ T["Agent"]["functions"]["can run consecutively and pass input"] = function() local tool = "'func_consecutive'" child.lua(string.format( [[ - local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") - local xml = func_xml.one_data_point(%s) - agent:execute(chat, xml) - ]], + --require("tests.log") + local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") + local xml = func_xml.one_data_point(%s) + agent:execute(chat, xml) + ]], tool )) @@ -98,10 +100,10 @@ T["Agent"]["functions"]["can run consecutively"] = function() local tool = "'func_consecutive'" child.lua(string.format( [[ - local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") - local xml = func_xml.two_data_points(%s) - agent:execute(chat, xml) - ]], + local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") + local xml = func_xml.two_data_points(%s) + agent:execute(chat, xml) + ]], tool )) @@ -115,10 +117,10 @@ T["Agent"]["functions"]["can handle errors"] = function() local tool = "'func_error'" child.lua(string.format( [[ - local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") - local xml = func_xml.two_data_points(%s) - agent:execute(chat, xml) - ]], + local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") + local xml = func_xml.two_data_points(%s) + agent:execute(chat, xml) + ]], tool )) @@ -132,14 +134,14 @@ T["Agent"]["functions"]["can populate stderr and halt execution"] = function() local tool = "'func_error'" child.lua(string.format( [[ - -- Prevent stderr from being cleared out - function agent:reset() - return nil - end - local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") - local xml = func_xml.two_data_points(%s) - agent:execute(chat, xml) - ]], + -- Prevent stderr from being cleared out + function agent:reset() + return nil + end + local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") + local xml = func_xml.two_data_points(%s) + agent:execute(chat, xml) + ]], tool )) @@ -149,14 +151,14 @@ end T["Agent"]["functions"]["can populate stdout"] = function() child.lua([[ - -- Prevent stdout from being cleared out - function agent:reset() - return nil - end - local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") - local xml = func_xml.two_data_points() - agent:execute(chat, xml) - ]]) + -- Prevent stdout from being cleared out + function agent:reset() + return nil + end + local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") + local xml = func_xml.two_data_points() + agent:execute(chat, xml) + ]]) h.eq( { { data = "Data 1", status = "success" }, { data = "Data 2", status = "success" } }, diff --git a/tests/strategies/chat/agents/executor/test_queue.lua b/tests/strategies/chat/agents/executor/test_queue.lua index 6b77415e4..4673e10e5 100644 --- a/tests/strategies/chat/agents/executor/test_queue.lua +++ b/tests/strategies/chat/agents/executor/test_queue.lua @@ -10,6 +10,7 @@ T = new_set({ -- Load helpers and set up the environment in the child process child.lua([[ + --require("tests.log") h = require('tests.helpers') chat, agent = h.setup_chat_buffer() @@ -48,7 +49,7 @@ T["Agent"]["queue"]["can queue functions and commands"] = function() ) -- Test that the function was called - -- h.eq("Data 1 Data 2", child.lua_get([[_G._test_func]])) + h.eq("Data 1 Data 2", child.lua_get([[_G._test_func]])) end return T diff --git a/tests/strategies/chat/agents/tools/stubs/cmd_queue.lua b/tests/strategies/chat/agents/tools/stubs/cmd_queue.lua index 782b8564e..aaf2e9200 100644 --- a/tests/strategies/chat/agents/tools/stubs/cmd_queue.lua +++ b/tests/strategies/chat/agents/tools/stubs/cmd_queue.lua @@ -23,7 +23,6 @@ return { success = function(self, cmd, output) _G._test_order = (_G._test_order or "") .. "->Cmd[Success]" _G._test_output = _G._test_output or {} - table.insert(_G._test_output, output) end, }, } diff --git a/tests/strategies/chat/agents/tools/stubs/func_consecutive.lua b/tests/strategies/chat/agents/tools/stubs/func_consecutive.lua index 04fe7a542..aa98a942c 100644 --- a/tests/strategies/chat/agents/tools/stubs/func_consecutive.lua +++ b/tests/strategies/chat/agents/tools/stubs/func_consecutive.lua @@ -1,3 +1,4 @@ +local log = require("codecompanion.utils.log") return { name = "func_consecutive", system_prompt = function(schema) @@ -6,9 +7,11 @@ return { cmds = { ---In production, we should be outputting as { status: string, data: any } function(self, actions, input) + log:debug("FIRST ACTION") return (input and (input .. " ") or "") .. actions.data end, function(self, actions, input) + log:debug("SECOND ACTION") local output = input .. " " .. actions.data _G._test_func = output return output From 72640d78e177912c7a54fa91b207d79b1aac16d7 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Sun, 2 Mar 2025 10:53:41 +0000 Subject: [PATCH 26/38] wip: start adding back user approvals --- codecompanion-workspace.json | 4 + tests/config.lua | 7 ++ .../agents/executor/test_user_approval.lua | 51 +++++++++++++ tests/stubs/queue.lua | 73 +++++++++++++++++++ 4 files changed, 135 insertions(+) create mode 100644 tests/strategies/chat/agents/executor/test_user_approval.lua create mode 100644 tests/stubs/queue.lua diff --git a/codecompanion-workspace.json b/codecompanion-workspace.json index 345e71555..b73b56183 100644 --- a/codecompanion-workspace.json +++ b/codecompanion-workspace.json @@ -170,6 +170,10 @@ { "description": "This is the queue implementation", "path": "${base_dir}/executor/queue.lua" + }, + { + "description": "This is how the queue object can look. This is an example of a function tool, a command tool, followed by a function tool:", + "path": "tests/stubs/queue.lua" } ] } diff --git a/tests/config.lua b/tests/config.lua index e167a85a0..73e38b67b 100644 --- a/tests/config.lua +++ b/tests/config.lua @@ -88,6 +88,13 @@ return { callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func_queue_2.lua", description = "Some function tool to test", }, + ["func_approval"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func.lua", + description = "Some function tool to test but with approval", + opts = { + user_approval = true, + }, + }, ["cmd"] = { callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/cmd.lua", description = "Cmd tool", diff --git a/tests/strategies/chat/agents/executor/test_user_approval.lua b/tests/strategies/chat/agents/executor/test_user_approval.lua new file mode 100644 index 000000000..efb05d320 --- /dev/null +++ b/tests/strategies/chat/agents/executor/test_user_approval.lua @@ -0,0 +1,51 @@ +local h = require("tests.helpers") + +local new_set = MiniTest.new_set + +local child = MiniTest.new_child_neovim() +T = new_set({ + hooks = { + pre_case = function() + child.restart({ "-u", "scripts/minimal_init.lua" }) + + -- Load helpers and set up the environment in the child process + child.lua([[ + h = require('tests.helpers') + chat, agent = h.setup_chat_buffer() + + -- Reset test globals + _G._test_func = nil + _G._test_exit = nil + _G._test_order = nil + _G._test_output = nil + _G._test_setup = nil + ]]) + end, + post_case = function() + child.lua([[h.teardown_chat_buffer()]]) + end, + post_once = child.stop, + }, +}) + +T["Agent"] = new_set() +T["Agent"]["user approval"] = new_set() + +T["Agent"]["user approval"]["is triggered"] = function() + h.eq(vim.NIL, child.lua_get([[_G._test_func]])) + + child.lua([[ + --require("tests.log") + local func_xml = require("tests.strategies.chat.agents.tools.stubs.xml.func_xml") + local xml = func_xml.two_data_points("func_approval") + agent:execute(chat, xml) + ]]) + + -- Test order + h.eq("Setup->Success->Success->Exit", child.lua_get([[_G._test_order]])) + + -- Test that the function was called + h.eq("Data 1 Data 2", child.lua_get([[_G._test_func]])) +end + +return T diff --git a/tests/stubs/queue.lua b/tests/stubs/queue.lua new file mode 100644 index 000000000..e60548ae6 --- /dev/null +++ b/tests/stubs/queue.lua @@ -0,0 +1,73 @@ +local queue = { { + cmds = { , }, + handlers = { + on_exit = , + setup = + }, + name = "func_queue", + output = { + success = + }, + request = { + _attr = { + name = "func_queue" + }, + action = { + _attr = { + type = "type1" + }, + data = "Data 1" + } + }, + system_prompt = + }, { + cmds = { { "sleep", "0.5" } }, + handlers = { + on_exit = , + setup = + }, + name = "cmd_queue", + output = { + success = + }, + request = { + _attr = { + name = "cmd_queue" + } + }, + system_prompt = + }, { + cmds = { }, + handlers = { + on_exit = , + setup = + }, + name = "func_queue_2", + output = { + success = + }, + request = { + _attr = { + name = "func_queue_2" + }, + action = { + _attr = { + type = "type1" + }, + data = "Data 2" + } + }, + system_prompt = + }, + head = 0, + tail = 3, + = { + __index = { + contents = , + count = , + is_empty = , + pop = , + push = + } + } +} From bead0fd3778f4d32090bc3f065fca5693047701c Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Sun, 2 Mar 2025 21:30:54 +0000 Subject: [PATCH 27/38] wip: add back human approval --- lua/codecompanion/config.lua | 4 +- .../strategies/chat/agents/executor/cmd.lua | 5 +-- .../strategies/chat/agents/executor/func.lua | 2 +- .../strategies/chat/agents/executor/init.lua | 42 +++++++++++++++---- .../strategies/chat/agents/init.lua | 9 +++- .../chat/agents/tools/cmd_runner.lua | 23 ---------- tests/config.lua | 2 +- .../agents/executor/test_user_approval.lua | 3 -- 8 files changed, 48 insertions(+), 42 deletions(-) diff --git a/lua/codecompanion/config.lua b/lua/codecompanion/config.lua index ee810f93a..088d06f09 100644 --- a/lua/codecompanion/config.lua +++ b/lua/codecompanion/config.lua @@ -61,7 +61,7 @@ local defaults = { callback = "strategies.chat.agents.tools.cmd_runner", description = "Run shell commands initiated by the LLM", opts = { - user_approval = true, + requires_approval = true, }, }, ["editor"] = { @@ -72,7 +72,7 @@ local defaults = { callback = "strategies.chat.agents.tools.files", description = "Update the file system with the LLM's response", opts = { - user_approval = true, + requires_approval = true, }, }, ["rag"] = { diff --git a/lua/codecompanion/strategies/chat/agents/executor/cmd.lua b/lua/codecompanion/strategies/chat/agents/executor/cmd.lua index 8a409f70f..aa331e1c4 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/cmd.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/cmd.lua @@ -78,7 +78,7 @@ function CmdExecutor:run(cmd, index) -- Don't trigger on_exit unless it's the last command if index == self.count then self.executor:close() - return self.executor:execute() + return self.executor:setup() end else return self.executor:error(cmd, string.format("Failed with code %s", code)) @@ -86,8 +86,7 @@ function CmdExecutor:run(cmd, index) end) if not ok then - log:error("Internal error running command %s: %s", cmd, output) - return self.executor:error(cmd, "Internal error") + return self.executor:error(cmd, string.format("Error whilst running command %s: %s", cmd, output)) end end) end, diff --git a/lua/codecompanion/strategies/chat/agents/executor/func.lua b/lua/codecompanion/strategies/chat/agents/executor/func.lua index 3966cc835..dad3b6fd4 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/func.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/func.lua @@ -65,7 +65,7 @@ function FuncExecutor:proceed_to_next(output) return next_executor:orchestrate(output) else self.executor:close() - return self.executor:execute(output) + return self.executor:setup(output) end end diff --git a/lua/codecompanion/strategies/chat/agents/executor/init.lua b/lua/codecompanion/strategies/chat/agents/executor/init.lua index b610ff4d8..5839a01fd 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/init.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/init.lua @@ -74,25 +74,53 @@ function Executor:setup_handlers() } end ----Execute the tool command +---Setup the tool to be executed ---@param input? any ---@return nil -function Executor:execute(input) +function Executor:setup(input) if self.queue:is_empty() or self.agent.status == self.agent.constants.STATUS_ERROR then - log:debug("Executor:execute - Queue empty or error") - return + return log:debug("Executor:execute - Queue empty or error") end -- Get the next tool to run self.tool = self.queue:pop() - -- Setup its handlers + -- Setup the handlers self:setup_handlers() + self.handlers.setup() -- Call this early as cmd_runner needs to setup its cmds dynamically + -- Get the first command to run local cmd = self.tool.cmds[1] + log:debug("Executor:execute - %s", self.tool) log:debug("Executor:execute - `%s` tool", self.tool.name) - self.handlers.setup() + -- Check if the tool requires approval + if self.tool.opts and self.tool.opts.requires_approval then + log:debug("Executor:execute - Asking for approval") + local ok, choice = pcall(vim.fn.confirm, ("Run the tool %q?"):format(self.tool.name), "&Yes\n&No\n&Cancel") + if not ok or choice == 0 or choice == 3 then -- Esc or Cancel + log:debug("Executor:execute - Tool cancelled") + return self:close() + end + if choice == 1 then -- Yes + log:debug("Executor:execute - Tool approved") + self:execute(cmd, input) + end + if choice == 2 then -- No + log:debug("Executor:execute - Tool rejected") + self.output.rejected(cmd) + return self:setup() + end + else + self:execute(cmd, input) + end +end + +---Execute the tool command +---@param cmd string|function +---@param input? any +---@return nil +function Executor:execute(cmd, input) if type(cmd) == "function" then return FuncExecutor.new(self, cmd, 1):orchestrate(input) end @@ -116,7 +144,7 @@ function Executor:error(action, error) self.agent.status = self.agent.constants.STATUS_ERROR if error then table.insert(self.agent.stderr, error) - log:warn("Error with %s: %s", self.tool.name, error) + log:warn("Tool %s: %s", self.tool.name, error) end self.output.error(action, self.agent.stderr, self.agent.stdout) self:close() diff --git a/lua/codecompanion/strategies/chat/agents/init.lua b/lua/codecompanion/strategies/chat/agents/init.lua index 8a4b101c9..bebbaec72 100644 --- a/lua/codecompanion/strategies/chat/agents/init.lua +++ b/lua/codecompanion/strategies/chat/agents/init.lua @@ -185,10 +185,13 @@ function Agent:execute(chat, xml) return end + local name = s.tool._attr.name + local tool_config = self.agent_config.tools[name] + ---@type CodeCompanion.Agent.Tool|nil local resolved_tool ok, resolved_tool = pcall(function() - return Agent.resolve(self.agent_config.tools[s.tool._attr.name]) + return Agent.resolve(tool_config) end) if not ok or not resolved_tool then log:error("Couldn't resolve the tool(s) from the LLM's response") @@ -198,6 +201,8 @@ function Agent:execute(chat, xml) end self.tool = vim.deepcopy(resolved_tool) + self.tool.name = name + self.tool.opts = tool_config.opts and tool_config.opts or {} self.tool.request = s.tool self:fold_xml() self:set_autocmds() @@ -222,7 +227,7 @@ function Agent:execute(chat, xml) run_tool(executor, schema) end - return executor:execute() + return executor:setup() end ---Look for tools in a given message diff --git a/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua b/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua index fd1e18bdf..76fb65b2d 100644 --- a/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua +++ b/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua @@ -172,29 +172,6 @@ return { table.insert(tool.cmds, entry) end end, - - ---Approve the command to be run - ---@param agent CodeCompanion.Agent - ---@param cmd table - ---@return boolean - approved = function(agent, cmd) - if vim.g.codecompanion_auto_tool_mode then - log:info("[Cmd Runner Tool] Auto-approved running the command") - return true - end - - local cmd_concat = table.concat(cmd.cmd or cmd, " ") - - local msg = "Run command: `" .. cmd_concat .. "`?" - local ok, choice = pcall(vim.fn.confirm, msg, "No\nYes") - if not ok or choice ~= 2 then - log:info("[Cmd Runner Tool] Rejected running the command") - return false - end - - log:info("[Cmd Runner Tool] Approved running the command") - return true - end, }, output = { diff --git a/tests/config.lua b/tests/config.lua index 73e38b67b..bc2f9d5fc 100644 --- a/tests/config.lua +++ b/tests/config.lua @@ -92,7 +92,7 @@ return { callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func.lua", description = "Some function tool to test but with approval", opts = { - user_approval = true, + requires_approval = true, }, }, ["cmd"] = { diff --git a/tests/strategies/chat/agents/executor/test_user_approval.lua b/tests/strategies/chat/agents/executor/test_user_approval.lua index efb05d320..8dc2bec50 100644 --- a/tests/strategies/chat/agents/executor/test_user_approval.lua +++ b/tests/strategies/chat/agents/executor/test_user_approval.lua @@ -41,9 +41,6 @@ T["Agent"]["user approval"]["is triggered"] = function() agent:execute(chat, xml) ]]) - -- Test order - h.eq("Setup->Success->Success->Exit", child.lua_get([[_G._test_order]])) - -- Test that the function was called h.eq("Data 1 Data 2", child.lua_get([[_G._test_func]])) end From e0b127cd8e6f4389e6959f019be9f0e413c999bf Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Sun, 2 Mar 2025 21:46:43 +0000 Subject: [PATCH 28/38] wip: fix stylua --- codecompanion-workspace.json | 2 +- tests/stubs/{queue.lua => queue.txt} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/stubs/{queue.lua => queue.txt} (100%) diff --git a/codecompanion-workspace.json b/codecompanion-workspace.json index b73b56183..0c10fb786 100644 --- a/codecompanion-workspace.json +++ b/codecompanion-workspace.json @@ -173,7 +173,7 @@ }, { "description": "This is how the queue object can look. This is an example of a function tool, a command tool, followed by a function tool:", - "path": "tests/stubs/queue.lua" + "path": "tests/stubs/queue.txt" } ] } diff --git a/tests/stubs/queue.lua b/tests/stubs/queue.txt similarity index 100% rename from tests/stubs/queue.lua rename to tests/stubs/queue.txt From b6a0b47a6ad2d7fa5d1eeac923d784bbc3823932 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Sun, 2 Mar 2025 22:04:42 +0000 Subject: [PATCH 29/38] wip: update readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b9737d2ef..0c86c1bb8 100644 --- a/README.md +++ b/README.md @@ -111,5 +111,7 @@ buffer - [Aerial.nvim](https://github.com/stevearc/aerial.nvim) for the Tree-sitter parsing which inspired the symbols Slash Command - [Saghen](https://github.com/Saghen) for the fantastic docs inspiration from [blink.cmp](https://github.com/Saghen/blink.cmp) +- [Catwell](https://github.com/catwell) for the [queue](https://github.com/catwell/cw-lua/blob/master/deque/deque.lua) +inspiration that I use to stack agents and tools From 0de1aedf8a81fc3365a39a5979fbc27c88f44f86 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Sun, 2 Mar 2025 22:04:54 +0000 Subject: [PATCH 30/38] wip: add back autocmds --- .../strategies/chat/agents/executor/init.lua | 20 +++++++++---------- .../strategies/chat/agents/init.lua | 2 +- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lua/codecompanion/strategies/chat/agents/executor/init.lua b/lua/codecompanion/strategies/chat/agents/executor/init.lua index 5839a01fd..a0991d971 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/init.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/init.lua @@ -26,8 +26,6 @@ function Executor.new(agent) }, { __index = Executor }) _G.codecompanion_cancel_tool = false - -- util.fire("AgentStarted", { tool = tool.name, bufnr = agent.bufnr }) - -- self.handlers.setup() return self end @@ -97,7 +95,12 @@ function Executor:setup(input) -- Check if the tool requires approval if self.tool.opts and self.tool.opts.requires_approval then log:debug("Executor:execute - Asking for approval") - local ok, choice = pcall(vim.fn.confirm, ("Run the tool %q?"):format(self.tool.name), "&Yes\n&No\n&Cancel") + local cmd_name = "" + if type(cmd) == "table" then + cmd_name = " with the cmd `" .. table.concat(cmd.cmd, " ") .. "`" + end + local prompt = ("Run the %q tool%s?"):format(self.tool.name, cmd_name) + local ok, choice = pcall(vim.fn.confirm, prompt, "&Yes\n&No\n&Cancel") if not ok or choice == 0 or choice == 3 then -- Esc or Cancel log:debug("Executor:execute - Tool cancelled") return self:close() @@ -117,10 +120,11 @@ function Executor:setup(input) end ---Execute the tool command ----@param cmd string|function +---@param cmd string|table|function ---@param input? any ---@return nil function Executor:execute(cmd, input) + util.fire("AgentStarted", { tool = self.tool.name, bufnr = self.agent.bufnr }) if type(cmd) == "function" then return FuncExecutor.new(self, cmd, 1):orchestrate(input) end @@ -166,14 +170,8 @@ end ---@return nil function Executor:close() log:debug("Executor:close") - self.handlers.on_exit() - - util.fire("AgentFinished", { - name = self.tool.name, - bufnr = self.agent.bufnr, - }) - + util.fire("AgentFinished", { name = self.tool.name, bufnr = self.agent.bufnr }) self.agent.chat.subscribers:process(self.agent.chat) vim.g.codecompanion_current_tool = nil end diff --git a/lua/codecompanion/strategies/chat/agents/init.lua b/lua/codecompanion/strategies/chat/agents/init.lua index bebbaec72..99a64d174 100644 --- a/lua/codecompanion/strategies/chat/agents/init.lua +++ b/lua/codecompanion/strategies/chat/agents/init.lua @@ -326,7 +326,7 @@ function Agent:replace(message) return message end ----Reset the tool class +---Reset the Agent class ---@return nil function Agent:reset() api.nvim_buf_clear_namespace(self.bufnr, self.tools_ns, 0, -1) From 93894fa3172f66fb440ecbb782edb167ef29f22b Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Mon, 3 Mar 2025 09:22:01 +0000 Subject: [PATCH 31/38] wip: approval prompt updates --- .../strategies/chat/agents/executor/cmd.lua | 7 +++- .../strategies/chat/agents/executor/func.lua | 1 + .../strategies/chat/agents/executor/init.lua | 38 ++++++++----------- .../strategies/chat/agents/executor/queue.lua | 6 +-- .../chat/agents/tools/cmd_runner.lua | 15 ++++++++ .../chat/agents/executor/test_cmd.lua | 14 +++---- 6 files changed, 47 insertions(+), 34 deletions(-) diff --git a/lua/codecompanion/strategies/chat/agents/executor/cmd.lua b/lua/codecompanion/strategies/chat/agents/executor/cmd.lua index aa331e1c4..0983ec46f 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/cmd.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/cmd.lua @@ -30,7 +30,7 @@ function CmdExecutor:orchestrate() end end ----Some commands output ANSI color codes so we need to strip them +---Some commands output ANSI color codes which don't render in the chat buffer ---@param tbl table ---@return table local function strip_ansi(tbl) @@ -67,15 +67,18 @@ function CmdExecutor:run(cmd, index) if _G.codecompanion_cancel_tool then return self.executor:close() end + if data and data._stderr_results then + self.executor.agent.stderr = {} table.insert(self.executor.agent.stderr, strip_ansi(data._stderr_results)) end if data and data._stdout_results then + self.executor.agent.stdout = {} table.insert(self.executor.agent.stdout, strip_ansi(data._stdout_results)) end if code == 0 then self.executor:success(cmd) - -- Don't trigger on_exit unless it's the last command + -- Don't trigger the on_exit handler unless it's the last command if index == self.count then self.executor:close() return self.executor:setup() diff --git a/lua/codecompanion/strategies/chat/agents/executor/func.lua b/lua/codecompanion/strategies/chat/agents/executor/func.lua index dad3b6fd4..8e79d887a 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/func.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/func.lua @@ -22,6 +22,7 @@ end ---@return nil function FuncExecutor:orchestrate(input) log:debug("FuncExecutor:orchestrate %s", self.index) + local action = self.executor.tool.request.action log:debug("Action: %s", action) diff --git a/lua/codecompanion/strategies/chat/agents/executor/init.lua b/lua/codecompanion/strategies/chat/agents/executor/init.lua index a0991d971..7ef6728c9 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/init.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/init.lua @@ -40,12 +40,6 @@ function Executor:setup_handlers() self.tool.handlers.setup(self.agent) end end, - approved = function(cmd) - if self.tool.handlers and self.tool.handlers.approved then - return self.tool.handlers.approved(self.agent, cmd) - end - return true - end, on_exit = function() if self.tool.handlers and self.tool.handlers.on_exit then self.tool.handlers.on_exit(self.agent) @@ -54,6 +48,11 @@ function Executor:setup_handlers() } self.output = { + prompt = function(cmds) + if self.tool.output and self.tool.output.prompt then + return self.tool.output.prompt(self.agent, cmds) + end + end, rejected = function(cmd) if self.tool.output and self.tool.output.rejected then self.tool.output.rejected(self.agent, cmd) @@ -76,8 +75,11 @@ end ---@param input? any ---@return nil function Executor:setup(input) - if self.queue:is_empty() or self.agent.status == self.agent.constants.STATUS_ERROR then - return log:debug("Executor:execute - Queue empty or error") + if self.queue:is_empty() then + return log:debug("Executor:execute - Queue empty") + end + if self.agent.status == self.agent.constants.STATUS_ERROR then + return log:debug("Executor:execute - Error") end -- Get the next tool to run @@ -89,17 +91,17 @@ function Executor:setup(input) -- Get the first command to run local cmd = self.tool.cmds[1] - log:debug("Executor:execute - %s", self.tool) log:debug("Executor:execute - `%s` tool", self.tool.name) -- Check if the tool requires approval - if self.tool.opts and self.tool.opts.requires_approval then + if self.tool.opts and self.tool.opts.requires_approval and not vim.g.codecompanion_auto_tool_mode then log:debug("Executor:execute - Asking for approval") - local cmd_name = "" - if type(cmd) == "table" then - cmd_name = " with the cmd `" .. table.concat(cmd.cmd, " ") .. "`" + + local prompt = self.output.prompt(self.tool.cmds) + if prompt == nil or prompt == "" then + prompt = ("Run the %q tool?"):format(self.tool.name) end - local prompt = ("Run the %q tool%s?"):format(self.tool.name, cmd_name) + local ok, choice = pcall(vim.fn.confirm, prompt, "&Yes\n&No\n&Cancel") if not ok or choice == 0 or choice == 3 then -- Esc or Cancel log:debug("Executor:execute - Tool cancelled") @@ -131,14 +133,6 @@ function Executor:execute(cmd, input) return CmdExecutor.new(self, self.tool.cmds, 1):orchestrate() end ----Does the tool require approval before it can be executed? ----@return boolean -function Executor:requires_approval() - return config.strategies.chat.agents.tools[self.tool.name].opts - and config.strategies.chat.agents.tools[self.tool.name].opts.user_approval - or false -end - ---Handle an error from a tool ---@param action table ---@param error? string diff --git a/lua/codecompanion/strategies/chat/agents/executor/queue.lua b/lua/codecompanion/strategies/chat/agents/executor/queue.lua index 323b295dc..253358982 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/queue.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/queue.lua @@ -1,6 +1,6 @@ --- Simple Queue implementation --- Based on deque by Pierre 'catwell' Chapuis --- Ref: https://github.com/catwell/cw-lua/blob/master/deque/deque.lua +---Simple queue implementation +---Based on deque by Pierre 'catwell' Chapuis +---Ref: https://github.com/catwell/cw-lua/blob/master/deque/deque.lua ---Add an item to the back of the queue ---@param self table diff --git a/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua b/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua index 76fb65b2d..4c97789ec 100644 --- a/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua +++ b/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua @@ -175,6 +175,21 @@ return { }, output = { + ---The message to prompt the user for approval + ---@param agent CodeCompanion.Agent + ---@param cmds table + ---@return string + prompt = function(agent, cmds) + if vim.tbl_count(cmds) == 1 then + return string.format("Run the command `%s`?", table.concat(cmds[1].cmd, " ")) + end + + local individual_cmds = vim.tbl_map(function(c) + return table.concat(c.cmd, " ") + end, cmds) + return string.format("Run the following commands?\n\n%s", table.concat(individual_cmds, "\n")) + end, + ---Rejection message back to the LLM ---@param agent CodeCompanion.Agent ---@param cmd table diff --git a/tests/strategies/chat/agents/executor/test_cmd.lua b/tests/strategies/chat/agents/executor/test_cmd.lua index 79e7bc927..b718a8381 100644 --- a/tests/strategies/chat/agents/executor/test_cmd.lua +++ b/tests/strategies/chat/agents/executor/test_cmd.lua @@ -80,20 +80,20 @@ T["Agent"]["cmds"]["can set test flags on the chat object"] = function() end T["Agent"]["cmds"]["can run multiple commands"] = function() - child.lua(string.format( - [[ + child.lua([[ local cmd_xml = require("tests.strategies.chat.agents.tools.stubs.xml.cmd_xml") - local xml = cmd_xml.load(%s) + local xml = cmd_xml.load("cmd_consecutive") agent:execute(chat, xml) vim.wait(100) - ]], - "'cmd_consecutive'" - )) + ]]) -- on_exit should only be called at the end h.eq("Setup->Success->Success->Exit", child.lua_get("_G._test_order")) + -- output.success should be called for each command - h.eq({ { "Hello World" }, { "Hello CodeCompanion" } }, child.lua_get("_G._test_output[1]")) + h.eq({ { "Hello World" } }, child.lua_get("_G._test_output[1]")) + h.eq({ { "Hello CodeCompanion" } }, child.lua_get("_G._test_output[2]")) + h.eq(vim.NIL, child.lua_get("_G._test_output[3]")) end return T From 7355ff1e68c6f270ec40dac7dea9596df56273aa Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Mon, 3 Mar 2025 21:37:56 +0000 Subject: [PATCH 32/38] wip: add custom prompts --- lua/codecompanion/config.lua | 7 - .../strategies/chat/agents/executor/init.lua | 3 +- .../chat/agents/tools/cmd_runner.lua | 7 +- .../strategies/chat/agents/tools/files.lua | 95 +++++------ .../strategies/chat/agents/tools/rag.lua | 154 ------------------ 5 files changed, 49 insertions(+), 217 deletions(-) delete mode 100644 lua/codecompanion/strategies/chat/agents/tools/rag.lua diff --git a/lua/codecompanion/config.lua b/lua/codecompanion/config.lua index 088d06f09..31497fc43 100644 --- a/lua/codecompanion/config.lua +++ b/lua/codecompanion/config.lua @@ -75,13 +75,6 @@ local defaults = { requires_approval = true, }, }, - ["rag"] = { - callback = "strategies.chat.agents.tools.rag", - description = "Supplement the LLM with real-time info from the internet", - opts = { - hide_output = true, - }, - }, opts = { auto_submit_errors = false, -- Send any errors to the LLM automatically? auto_submit_success = false, -- Send any successful output to the LLM automatically? diff --git a/lua/codecompanion/strategies/chat/agents/executor/init.lua b/lua/codecompanion/strategies/chat/agents/executor/init.lua index 7ef6728c9..6a6901ff1 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/init.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/init.lua @@ -1,7 +1,6 @@ local CmdExecutor = require("codecompanion.strategies.chat.agents.executor.cmd") local FuncExecutor = require("codecompanion.strategies.chat.agents.executor.func") local Queue = require("codecompanion.strategies.chat.agents.executor.queue") -local config = require("codecompanion.config") local log = require("codecompanion.utils.log") local util = require("codecompanion.utils") @@ -97,7 +96,7 @@ function Executor:setup(input) if self.tool.opts and self.tool.opts.requires_approval and not vim.g.codecompanion_auto_tool_mode then log:debug("Executor:execute - Asking for approval") - local prompt = self.output.prompt(self.tool.cmds) + local prompt = self.output.prompt(self.tool) if prompt == nil or prompt == "" then prompt = ("Run the %q tool?"):format(self.tool.name) end diff --git a/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua b/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua index 4c97789ec..b186f697d 100644 --- a/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua +++ b/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua @@ -175,11 +175,12 @@ return { }, output = { - ---The message to prompt the user for approval + ---The message which is shared with the user when asking for their approval ---@param agent CodeCompanion.Agent - ---@param cmds table + ---@param self CodeCompanion.Agent.Tool ---@return string - prompt = function(agent, cmds) + prompt = function(agent, self) + local cmds = self.cmds if vim.tbl_count(cmds) == 1 then return string.format("Run the command `%s`?", table.concat(cmds[1].cmd, " ")) end diff --git a/lua/codecompanion/strategies/chat/agents/tools/files.lua b/lua/codecompanion/strategies/chat/agents/tools/files.lua index 7ef0cc517..7901c850b 100644 --- a/lua/codecompanion/strategies/chat/agents/tools/files.lua +++ b/lua/codecompanion/strategies/chat/agents/tools/files.lua @@ -384,67 +384,59 @@ Remember: ) end, handlers = { - ---Approve the command to be run - ---@param self CodeCompanion.Agent The tool object - ---@param action table - ---@return boolean - approved = function(self, action) - if vim.g.codecompanion_auto_tool_mode then - log:info("[Files Tool] Auto-approved running the command") - return true - end - - log:info("[Files Tool] Prompting for: %s", string.upper(action._attr.type)) - - local prompts = { - base = function(a) - return fmt("%s the file at `%s`?", string.upper(a._attr.type), vim.fn.fnamemodify(a.path, ":.")) - end, - move = function(a) - return fmt( - "%s file from `%s` to `%s`?", - string.upper(a._attr.type), - vim.fn.fnamemodify(a.path, ":."), - vim.fn.fnamemodify(a.new_path, ":.") - ) - end, + ---@param agent CodeCompanion.Agent The tool object + ---@return nil + on_exit = function(agent) + log:debug("[Files Tool] on_exit handler executed") + file = nil + end, + }, + output = { + ---The message which is shared with the user when asking for their approval + ---@param agent CodeCompanion.Agent + ---@param self CodeCompanion.Agent.Tool + ---@return string + prompt = function(agent, self) + local prompts = {} + + local responses = { + create = "Create a file at %s?", + read = "Read %s?", + read_lines = "Read specific lines in %s?", + edit = "Edit %s?", + delete = "Delete %s?", + copy = "Copy %s?", + rename = "Rename %s to %s?", + move = "Move %s to %s?", } - local prompt = prompts.base(action) - if action.new_path then - prompt = prompts.move(action) - end + for _, action in ipairs(self.request.action) do + local path = vim.fn.fnamemodify(action.path, ":.") + local new_path = vim.fn.fnamemodify(action.new_path, ":.") + local type = string.lower(action._attr.type) - local ok, choice = pcall(vim.fn.confirm, prompt, "No\nYes") - if not ok or choice ~= 2 then - log:info("[Files Tool] Rejected the %s action", string.upper(action._attr.type)) - return false + if type == "rename" or type == "move" then + table.insert(prompts, fmt(responses[type], path, new_path)) + else + table.insert(prompts, fmt(responses[type], path)) + end end - log:info("[Files Tool] Approved the %s action", string.upper(action._attr.type)) - return true + return table.concat(prompts, "\n") end, - ---@param self CodeCompanion.Agent The tool object - ---@return nil - on_exit = function(self) - log:debug("[Files Tool] on_exit handler executed") - file = nil - end, - }, - output = { - ---@param self CodeCompanion.Agent The tool object + ---@param agent CodeCompanion.Agent The tool object ---@param action table ---@param output table ---@return nil - success = function(self, action, output) + success = function(agent, action, output) local type = action._attr.type local path = action.path log:debug("[Files Tool] success callback executed") util.notify(fmt("The files tool executed successfully for the `%s` file", vim.fn.fnamemodify(path, ":t"))) if file then - self.chat:add_message({ + agent.chat:add_message({ role = config.constants.USER_ROLE, content = fmt( [[The output from the %s action for file `%s` is: @@ -461,13 +453,13 @@ Remember: end end, - ---@param self CodeCompanion.Agent The tool object + ---@param agent CodeCompanion.Agent The tool object ---@param action table ---@param err string ---@return nil - error = function(self, action, err) + error = function(agent, action, err) log:debug("[Files Tool] error callback executed") - return self.chat:add_buf_message({ + return agent.chat:add_buf_message({ role = config.constants.USER_ROLE, content = fmt( [[There was an error running the %s action: @@ -481,11 +473,12 @@ Remember: }) end, - ---@param self CodeCompanion.Agent The tool object + ---The action to take if the user rejects the command + ---@param agent CodeCompanion.Agent The tool object ---@param action table ---@return nil - rejected = function(self, action) - return self.chat:add_buf_message({ + rejected = function(agent, action) + return agent.chat:add_buf_message({ role = config.constants.USER_ROLE, content = fmt("I rejected the %s action.\n\n", string.upper(action._attr.type)), }) diff --git a/lua/codecompanion/strategies/chat/agents/tools/rag.lua b/lua/codecompanion/strategies/chat/agents/tools/rag.lua deleted file mode 100644 index f7344614a..000000000 --- a/lua/codecompanion/strategies/chat/agents/tools/rag.lua +++ /dev/null @@ -1,154 +0,0 @@ ---[[ -*RAG Tool* -This tool can be used to search the internet or navigate directly to a specific URL. ---]] - -local config = require("codecompanion.config") - -local xml2lua = require("codecompanion.utils.xml.xml2lua") - ----@class CodeCompanion.Tool -return { - name = "rag", - env = function(tool) - local url - local key - local value - - local action = tool.action._attr.type - if action == "search" then - url = "https://s.jina.ai" - key = "q" - value = tool.action.query - elseif action == "navigate" then - url = "https://r.jina.ai" - key = "url" - value = tool.action.url - end - - return { - url = url, - key = key, - value = value, - } - end, - cmds = { - { - "curl", - "-X", - "POST", - "${url}/", - "-H", - "Content-Type: application/json", - "-H", - "X-Return-Format: text", - "-d", - '{"${key}": "${value}"}', - }, - }, - schema = { - { - tool = { - _attr = { name = "rag" }, - action = { - _attr = { type = "search" }, - query = "", - }, - }, - }, - { - tool = { - _attr = { name = "rag" }, - action = { - _attr = { type = "navigate" }, - url = "", - }, - }, - }, - }, - system_prompt = function(schema) - return string.format( - [[### Retrieval Augmented Generated (RAG) Tool (`rag`) - -1. **Purpose**: This gives you the ability to access the internet to find information that you may not know. - -2. **Usage**: Return an XML markdown code block for to search the internet or navigate to a specific URL. - -3. **Key Points**: - - **Use at your discretion** when you feel you don't have access to the latest information in order to answer the user's question - - This tool is expensive so you may wish to ask the user before using it - - Ensure XML is **valid and follows the schema** - - **Don't escape** special characters - - **Wrap queries and URLs in a CDATA block**, the text could contain characters reserved by XML - - Make sure the tools xml block is **surrounded by ```xml** - -4. **Actions**: - -a) **Search the internet**: - -```xml -%s -``` - -b) **Navigate to a URL**: - -```xml -%s -``` - -Remember: -- Minimize explanations unless prompted. Focus on generating correct XML.]], - xml2lua.toXml({ tools = { schema[1] } }), - xml2lua.toXml({ tools = { schema[2] } }) - ) - end, - output = { - error = function(self, cmd, stderr) - if type(stderr) == "table" then - stderr = table.concat(stderr, "\n") - end - - self.chat:add_message({ - role = config.constants.USER_ROLE, - content = string.format( - [[After the RAG tool completed, there was an error: - - -%s - -]], - stderr - ), - }, { visible = false }) - - self.chat:add_buf_message({ - role = config.constants.USER_ROLE, - content = "I've shared the error message from the RAG tool with you.\n", - }) - end, - - success = function(self, cmd, stdout) - if type(stdout) == "table" then - stdout = table.concat(stdout, "\n") - end - - self.chat:add_message({ - role = config.constants.USER_ROLE, - content = string.format( - [[Here is the content the RAG tool retrieved: - - -%s - -]], - stdout - ), - }, { visible = false }) - - self.chat:add_buf_message({ - role = config.constants.USER_ROLE, - content = "I've shared the content from the RAG tool with you.\n", - }) - end, - }, -} From 215667a8961cae75458528c6ddb9e11c1f08e8cd Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Mon, 3 Mar 2025 21:38:03 +0000 Subject: [PATCH 33/38] wip: update docs --- doc/.vitepress/config.mjs | 199 ++-- doc/.vitepress/theme/vaporwave.css | 78 ++ doc/codecompanion.txt | 164 ++- doc/configuration/chat-buffer.md | 20 + doc/extending/tools.md | 56 + doc/getting-started.md | 1 - doc/package-lock.json | 1607 +++++++++++++++++++++++++++- doc/package.json | 4 +- doc/usage/chat-buffer/agents.md | 44 +- 9 files changed, 2003 insertions(+), 170 deletions(-) diff --git a/doc/.vitepress/config.mjs b/doc/.vitepress/config.mjs index d26a45326..c946af810 100644 --- a/doc/.vitepress/config.mjs +++ b/doc/.vitepress/config.mjs @@ -1,5 +1,6 @@ import { defineConfig } from "vitepress"; import { execSync } from "node:child_process"; +import { withMermaid } from "vitepress-plugin-mermaid"; const inProd = process.env.NODE_ENV === "production"; @@ -28,103 +29,113 @@ const headers = inProd ? [baseHeaders, umamiScript] : baseHeaders; const siteUrl = "https://codecompanion.olimorris.dev"; // https://vitepress.dev/reference/site-config -export default defineConfig({ - title: "CodeCompanion", - description: "AI-powered coding, seamlessly in Neovim", - head: headers, - sitemap: { hostname: siteUrl }, - themeConfig: { - logo: "https://github.com/user-attachments/assets/825fc040-9bc8-4743-be2a-71e257f8a7be", - nav: [ - { - text: `${version}`, - items: [ - { - text: "Changelog", - link: "https://github.com/olimorris/codecompanion.nvim/blob/main/CHANGELOG.md", - }, - { - text: "Contributing", - link: "https://github.com/olimorris/codecompanion.nvim/blob/main/.github/contributing.md", - }, - ], - }, - ], +export default withMermaid( + defineConfig({ + mermaid: { + securityLevel: "loose", // Allows more flexibility + theme: "base", // Use base theme to allow CSS variables to take effect + }, + // optionally set additional config for plugin itself with MermaidPluginConfig + title: "CodeCompanion", + description: "AI-powered coding, seamlessly in Neovim", + head: headers, + sitemap: { hostname: siteUrl }, + themeConfig: { + logo: "https://github.com/user-attachments/assets/825fc040-9bc8-4743-be2a-71e257f8a7be", + nav: [ + { + text: `${version}`, + items: [ + { + text: "Changelog", + link: "https://github.com/olimorris/codecompanion.nvim/blob/main/CHANGELOG.md", + }, + { + text: "Contributing", + link: "https://github.com/olimorris/codecompanion.nvim/blob/main/.github/contributing.md", + }, + ], + }, + ], - sidebar: [ - { text: "Introduction", link: "/" }, - { text: "Installation", link: "/installation" }, - { text: "Getting Started", link: "/getting-started" }, - { - text: "Configuration", - collapsed: true, - items: [ - { text: "Introduction", link: "/configuration/introduction" }, - { text: "Action Palette", link: "/configuration/action-palette" }, - { text: "Adapters", link: "/configuration/adapters" }, - { text: "Chat Buffer", link: "/configuration/chat-buffer" }, - { text: "Inline Assistant", link: "/configuration/inline-assistant" }, - { text: "Prompt Library", link: "/configuration/prompt-library" }, - { text: "System Prompt", link: "/configuration/system-prompt" }, - { text: "Others", link: "/configuration/others" }, - ], - }, - { - text: "Usage", - collapsed: false, - items: [ - { text: "Introduction", link: "/usage/introduction" }, - { text: "Action Palette", link: "/usage/action-palette" }, - { - text: "Chat Buffer", - link: "/usage/chat-buffer/", - collapsed: true, - items: [ - { text: "Agents/Tools", link: "/usage/chat-buffer/agents" }, - { - text: "Slash Commands", - link: "/usage/chat-buffer/slash-commands", - }, - { text: "Variables", link: "/usage/chat-buffer/variables" }, - ], - }, - { text: "Events", link: "/usage/events" }, - { text: "Inline Assistant", link: "/usage/inline-assistant" }, - { text: "User Interface", link: "/usage/ui" }, - { text: "Workflows", link: "/usage/workflows" }, - ], + sidebar: [ + { text: "Introduction", link: "/" }, + { text: "Installation", link: "/installation" }, + { text: "Getting Started", link: "/getting-started" }, + { + text: "Configuration", + collapsed: true, + items: [ + { text: "Introduction", link: "/configuration/introduction" }, + { text: "Action Palette", link: "/configuration/action-palette" }, + { text: "Adapters", link: "/configuration/adapters" }, + { text: "Chat Buffer", link: "/configuration/chat-buffer" }, + { + text: "Inline Assistant", + link: "/configuration/inline-assistant", + }, + { text: "Prompt Library", link: "/configuration/prompt-library" }, + { text: "System Prompt", link: "/configuration/system-prompt" }, + { text: "Others", link: "/configuration/others" }, + ], + }, + { + text: "Usage", + collapsed: false, + items: [ + { text: "Introduction", link: "/usage/introduction" }, + { text: "Action Palette", link: "/usage/action-palette" }, + { + text: "Chat Buffer", + link: "/usage/chat-buffer/", + collapsed: true, + items: [ + { text: "Agents/Tools", link: "/usage/chat-buffer/agents" }, + { + text: "Slash Commands", + link: "/usage/chat-buffer/slash-commands", + }, + { text: "Variables", link: "/usage/chat-buffer/variables" }, + ], + }, + { text: "Events", link: "/usage/events" }, + { text: "Inline Assistant", link: "/usage/inline-assistant" }, + { text: "User Interface", link: "/usage/ui" }, + { text: "Workflows", link: "/usage/workflows" }, + ], + }, + { + text: "Extending the Plugin", + collapsed: false, + items: [ + { text: "Creating Adapters", link: "/extending/adapters" }, + { text: "Creating Prompts", link: "/extending/prompts" }, + { text: "Creating Tools", link: "/extending/tools" }, + { text: "Creating Workflows", link: "/extending/workflows" }, + { text: "Creating Workspaces", link: "/extending/workspace" }, + ], + }, + ], + + editLink: { + pattern: + "https://github.com/olimorris/codecompanion.nvim/edit/main/doc/:path", + text: "Edit this page on GitHub", }, - { - text: "Extending the Plugin", - collapsed: false, - items: [ - { text: "Creating Adapters", link: "/extending/adapters" }, - { text: "Creating Prompts", link: "/extending/prompts" }, - { text: "Creating Tools", link: "/extending/tools" }, - { text: "Creating Workflows", link: "/extending/workflows" }, - { text: "Creating Workspaces", link: "/extending/workspace" }, - ], + + footer: { + message: "Released under the MIT License.", + copyright: "Copyright © 2024-present Oli Morris", }, - ], - editLink: { - pattern: - "https://github.com/olimorris/codecompanion.nvim/edit/main/doc/:path", - text: "Edit this page on GitHub", - }, + socialLinks: [ + { + icon: "github", + link: "https://github.com/olimorris/codecompanion.nvim", + }, + ], - footer: { - message: "Released under the MIT License.", - copyright: "Copyright © 2024-present Oli Morris", + search: { provider: "local" }, }, - - socialLinks: [ - { - icon: "github", - link: "https://github.com/olimorris/codecompanion.nvim", - }, - ], - - search: { provider: "local" }, - }, -}); + }), +); diff --git a/doc/.vitepress/theme/vaporwave.css b/doc/.vitepress/theme/vaporwave.css index 325185b18..49db5bb97 100644 --- a/doc/.vitepress/theme/vaporwave.css +++ b/doc/.vitepress/theme/vaporwave.css @@ -91,3 +91,81 @@ /* Override base background for dark mode */ --vw-base-bg-mixer: 20%; } + +/* Mermaid */ +.mermaid * { + font-family: var(--vp-font-family-base) !important; + font-weight: var(--vp-font-weight-regular, 400) !important; +} + +.mermaid .noteText, +.mermaid .loopText { + font-size: 0.9em !important; +} + +.mermaid .note { + fill: color-mix(in srgb, var(--vw-purple) 40%, var(--vw-base-bg)) !important; + stroke: var(--vw-purple) !important; +} + +.mermaid .actor { + fill: color-mix(in srgb, var(--vw-blue) 20%, var(--vw-base-bg)) !important; + stroke: var(--vw-blue) !important; + font-weight: var(--vp-font-weight-medium, 500) !important; +} + +.mermaid text.actor > tspan { + fill: var(--vw-base-fg) !important; + stroke: none !important; + font-weight: var(--vp-font-weight-medium, 500) !important; +} + +.mermaid .messageText { + fill: var(--vw-base-fg) !important; + stroke: none !important; +} + +.mermaid .messageLine0, +.mermaid .messageLine1 { + stroke: var(--vw-green) !important; +} + +.mermaid .sequenceNumber { + fill: var(--vw-base-bg) !important; +} + +.mermaid .loopLine { + stroke: var(--vw-yellow) !important; +} + +.mermaid .loopText > tspan { + fill: var(--vw-base-fg) !important; + stroke: none !important; +} + +.mermaid .labelBox { + fill: color-mix(in srgb, var(--vw-yellow) 20%, var(--vw-base-bg)) !important; + stroke: var(--vw-yellow) !important; +} + +.mermaid .labelText { + fill: var(--vw-base-fg) !important; +} + +.mermaid line.divider { + stroke: var(--vw-purple) !important; +} + +.mermaid .noteText > tspan { + fill: var(--vw-base-fg) !important; + stroke: none !important; + font-weight: var(--vp-font-weight-regular, 400) !important; +} + +/* Adds styling for the activation boxes */ +.mermaid .activation0, +.mermaid .activation1, +.mermaid .activation2 { + fill: color-mix(in srgb, var(--vw-green) 20%, var(--vw-base-bg)) !important; + stroke: var(--vw-green) !important; +} diff --git a/doc/codecompanion.txt b/doc/codecompanion.txt index 23832c172..96f8bf685 100644 --- a/doc/codecompanion.txt +++ b/doc/codecompanion.txt @@ -1,4 +1,4 @@ -*codecompanion.txt* For NVIM v0.10.0 Last change: 2025 February 25 +*codecompanion.txt* For NVIM v0.10.0 Last change: 2025 March 03 ============================================================================== Table of Contents *codecompanion-table-of-contents* @@ -320,7 +320,6 @@ actions: - `@cmd_runner` - The LLM will run shell commands (subject to approval) - `@editor` - The LLM will edit code in a Neovim buffer - `@files` - The LLM will can work with files on the file system (subject to approval) -- `@rag` - The LLM will browse and search the internet for real-time information to supplement its response Tools can also be grouped together to form `Agents`, which are also accessed via `@` in the chat buffer: @@ -644,10 +643,11 @@ Many adapters allow model selection via the `schema.model.default` property: USER CONTRIBUTED ADAPTERS ~ -Thanks to the community for building and supporting the following adapters: +Thanks to the community for building the following adapters: - Venice.ai - Fireworks.ai +- OpenRouter The section of the discussion forums which is dedicated to user created adapters can be found here @@ -993,6 +993,29 @@ The `callback` option for a tool can also be a |codecompanion-extending-tools| object, which is a table with specific keys that defines the interface and workflow of the tool. +Some tools, such as the +|codecompanion-usage-chat-buffer-agents.html-cmd-runner|, require the user to +approve any commands before they’re executed. This can be changed by altering +the config for each tool: + +>lua + require("codecompanion").setup({ + strategies = { + chat = { + agents = { + tools = { + ["cmd_runner"] = { + opts = { + requires_approval = false, + }, + }, + } + } + } + } + }) +< + LAYOUT ~ @@ -1481,7 +1504,7 @@ The |codecompanion-usage-chat-buffer-agents-cmd-runner| tool enables an LLM to execute commands on your machine. This can be useful if you wish the LLM to run a test suite on your behalf and give insight on failing cases. Simply tag the `@cmd_runner` in the chat buffer and ask it run your tests with a suitable -command e.g. `pytest`. +command e.g.� `pytest`. NAVIGATING BETWEEN RESPONSES IN THE CHAT BUFFER ~ @@ -1674,11 +1697,9 @@ examples such as web searching or code execution that have obvious benefits when using LLMs. In the plugin, tools are simply context and actions that are shared with an LLM -via a `system` prompt. The LLM and the chat buffer act as an agent by -orchestrating their use within Neovim. Tools give LLM’s knowledge and a -defined schema which can be included in the response for the plugin to parse, -execute and feedback on. Agents and tools can be added as a participant to the -chat buffer by using the `@` key. +via a `system` prompt. The LLM can act as an agent by requesting tools via the +chat buffer which in turn orchestrates their use within Neovim. Agents and +tools can be added as a participant to the chat buffer by using the `@` key. [!IMPORTANT] The agentic use of some tools in the plugin results in you, the @@ -1687,19 +1708,39 @@ chat buffer by using the `@` key. HOW TOOLS WORK ~ -LLMs are instructured by the plugin to return a structured XML block which has -been defined for each tool. The chat buffer parses the LLMs response and -detects any tool use before calling the appropriate tool. The chat buffer will -then be updated with the outcome. Depending on the tool, flags may be inserted -on the chat buffer for later processing. +When a tool is added to the chat buffer, the LLM is instructured by the plugin +to return a structured XML block which has been defined for each tool. The chat +buffer parses the LLMs response and detects any tool use before triggering the +`agent/init.lua` file. The agent triggers off a series of events, which sees +tool’s added to a queue and sequentially worked with their putput being +shared back to the LLM via the chat buffer. Depending on the tool, flags may be +inserted on the chat buffer for later processing. + +An outline of the architecture can be seen +|codecompanion-extending-tools-architecture|. + + +APPROVALS ~ + +Some tools, such as the `@cmd_runner`, require the user to approve any actions +before they can be executed. If the tool requires this a `vim.fn.confirm` +dialog will prompt you for a response. @CMD_RUNNER ~ The `@cmd_runner` tool enables an LLM to execute commands on your machine, -subject to your authorization. A common example can be asking the LLM to run -your test suite and provide feedback on any failures. Some commands do not -write any data to stdout +subject to your authorization. For example: + +>md + Can you use the @cmd_runner tool to run my test suite with `pytest`? +< + +>md + Use the @cmd_runner tool to install any missing libraries in my project +< + +Some commands do not write any data to stdout which means the plugin can’t pass the output of the execution to the LLM. When this occurs, the tool will instead share the exit code. @@ -1728,7 +1769,15 @@ An example of the XML that an LLM may generate for the tool: The `@editor` tool enables an LLM to modify the code in a Neovim buffer. If a buffer’s content has been shared with the LLM then the tool can be used to add, edit or delete specific lines. Consider pinning or watching a buffer to -avoid manually re-sending a buffer’s content to the LLM. +avoid manually re-sending a buffer’s content to the LLM: + +>md + Use the @editor tool refactor the code in #buffer{watch} +< + +>md + Can you apply the suggested changes to the buffer with the @editor tool? +< An example of the XML that an LLM may generate for the tool: @@ -1810,13 +1859,6 @@ An example of the XML that an LLM may generate for the tool: < -@RAG ~ - -The `@rag` tool uses jina.ai to parse a given URL’s content -and convert it into plain text before sharing with the LLM. It also gives the -LLM the ability to search the internet for information. - - @FULL_STACK_DEV ~ The `@full_stack_dev` agent is a combination of the `@cmd_runner`, `@editor` @@ -1826,6 +1868,15 @@ and `@files` tools. USEFUL TIPS ~ +COMBINING TOOLS + +Consider combining tools for complex tasks: + +>md + @full_stack_dev I want to play Snake. Can you create the game for me in Python and install any packages you need. Let's save it to ~/Code/Snake. When you've finished writing it, can you open it so I can play? +< + + AUTOMATIC TOOL MODE The plugin allows you to run tools on autopilot. This automatically approves @@ -2512,7 +2563,7 @@ OpenAI adapter. as a great reference to understand how they’re working with the output of the API -OPENAI’S API OUTPUT +OPENAI€�S API OUTPUT If we reference the OpenAI documentation @@ -3389,6 +3440,65 @@ The calling of tools is orchestrated by the `CodeCompanion.Chat` class which parses an LLM’s response and looks to identify any XML code blocks. +ARCHITECTURE ~ + +In order to create tools, you do not need to understand the underlying +architecture. However, for those who are curious about the implementation, +please see the diagram below: + +>mermaid + sequenceDiagram + participant C as Chat Buffer + participant L as LLM + participant A as Agent + participant E as Tool Executor + participant T as Tool + + C->>L: Prompt + L->>C: Response with Tool(s) request + + C->>A: Parse response + + loop For each detected tool + A<<->>T: Resolve Tool config + A->>A: Add Tool to queue + end + + A->>E: Begin executing Tools + + loop While queue not empty + E<<->>T: Fetch Tool implementation + + E->>E: Setup handlers and output functions + T<<->>E: handlers.setup() + + alt + Note over C,E: Some Tools require human approvals + E->>C: Prompt for approval + C->>E: User decision + end + + + alt + Note over E,T: If Tool runs with success + E<<->>T: output.success() + T-->>C: Update chat buffer + else + Note over E,T: If Tool runs with errors + E<<->>T: output.error() + T-->>C: Update chat buffer + end + + Note over E,T: When Tool completes + E<<->>T: handlers.on_exit() + end + + E-->>A: Fire autocmd + + A->>A: reset() +< + + TOOL TYPES ~ There are two types of tools within the plugin: @@ -3779,7 +3889,7 @@ Now let’s look at how we trigger the automated reflection prompts: opts = { auto_submit = true }, -- Scope this prompt to only run when the cmd_runner tool is active condition = function() - return vim.g.codecompanion_current_tool == "cmd_runner" + return _G.codecompanion_current_tool == "cmd_runner" end, -- Repeat until the tests pass, as indicated by the testing flag repeat_until = function(chat) diff --git a/doc/configuration/chat-buffer.md b/doc/configuration/chat-buffer.md index 5ff1458e2..ff0480700 100644 --- a/doc/configuration/chat-buffer.md +++ b/doc/configuration/chat-buffer.md @@ -154,6 +154,26 @@ When users introduce the agent `@my_agent` in the chat buffer, it can call the t The `callback` option for a tool can also be a [`CodeCompanion.Tool`](/extending/tools) object, which is a table with specific keys that defines the interface and workflow of the tool. +Some tools, such as the [@cmd_runner](/usage/chat-buffer/agents.html#cmd-runner), require the user to approve any commands before they're executed. This can be changed by altering the config for each tool: + +```lua +require("codecompanion").setup({ + strategies = { + chat = { + agents = { + tools = { + ["cmd_runner"] = { + opts = { + requires_approval = false, + }, + }, + } + } + } + } +}) +``` + ## Layout You can change the appearance of the chat buffer by changing the `display.chat.window` table in your configuration: diff --git a/doc/extending/tools.md b/doc/extending/tools.md index 1f463e747..632479591 100644 --- a/doc/extending/tools.md +++ b/doc/extending/tools.md @@ -8,6 +8,62 @@ In the plugin, tools work by sharing a system prompt with an LLM. This instructs The plugin has a tools class `CodeCompanion.Agent.Tools` which will call tools such as the [@cmd_runner](https://github.com/olimorris/codecompanion.nvim/blob/main/lua/codecompanion/strategies/chat/tools/cmd_runner.lua) or the [@editor](https://github.com/olimorris/codecompanion.nvim/blob/main/lua/codecompanion/strategies/chat/tools/editor.lua). The calling of tools is orchestrated by the `CodeCompanion.Chat` class which parses an LLM's response and looks to identify any XML code blocks. +## Architecture + +In order to create tools, you do not need to understand the underlying architecture. However, for those who are curious about the implementation, please see the diagram below: + +```mermaid +sequenceDiagram + participant C as Chat Buffer + participant L as LLM + participant A as Agent + participant E as Tool Executor + participant T as Tool + + C->>L: Prompt + L->>C: Response with Tool(s) request + + C->>A: Parse response + + loop For each detected tool + A<<->>T: Resolve Tool config + A->>A: Add Tool to queue + end + + A->>E: Begin executing Tools + + loop While queue not empty + E<<->>T: Fetch Tool implementation + + E->>E: Setup handlers and output functions + T<<->>E: handlers.setup() + + alt + Note over C,E: Some Tools require human approvals + E->>C: Prompt for approval + C->>E: User decision + end + + + alt + Note over E,T: If Tool runs with success + E<<->>T: output.success() + T-->>C: Update chat buffer + else + Note over E,T: If Tool runs with errors + E<<->>T: output.error() + T-->>C: Update chat buffer + end + + Note over E,T: When Tool completes + E<<->>T: handlers.on_exit() + end + + E-->>A: Fire autocmd + + A->>A: reset() +``` + ## Tool Types There are two types of tools within the plugin: diff --git a/doc/getting-started.md b/doc/getting-started.md index f0f677ba1..143bf287c 100644 --- a/doc/getting-started.md +++ b/doc/getting-started.md @@ -103,7 +103,6 @@ _Tools_, accessed via `@`, allow the LLM to function as an agent and carry out a - `@cmd_runner` - The LLM will run shell commands (subject to approval) - `@editor` - The LLM will edit code in a Neovim buffer - `@files` - The LLM will can work with files on the file system (subject to approval) -- `@rag` - The LLM will browse and search the internet for real-time information to supplement its response Tools can also be grouped together to form _Agents_, which are also accessed via `@` in the chat buffer: diff --git a/doc/package-lock.json b/doc/package-lock.json index e0fdb976c..75a4b2450 100644 --- a/doc/package-lock.json +++ b/doc/package-lock.json @@ -6,7 +6,9 @@ "": { "devDependencies": { "@types/node": "^22.10.5", - "vitepress": "^1.5.0" + "mermaid": "^11.4.1", + "vitepress": "^1.5.0", + "vitepress-plugin-mermaid": "^2.0.17" } }, "node_modules/@algolia/autocomplete-core": { @@ -251,6 +253,30 @@ "node": ">= 14.0.0" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.0.0.tgz", + "integrity": "sha512-xvX6P/lo1B3ej0OsaErAjqgFYzYVcJpamjLAFLYh9vRJngBrMoUG7aVnrGTeqM7yxbyTD5p3F2+0/QUEh8Vzhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-manager-detector": "^0.2.8", + "tinyexec": "^0.3.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@antfu/utils": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz", + "integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", @@ -301,6 +327,57 @@ "node": ">=6.9.0" } }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", + "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@docsearch/css": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", @@ -760,6 +837,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@iconify/utils": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz", + "integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.0.0", + "@antfu/utils": "^8.1.0", + "@iconify/types": "^2.0.0", + "debug": "^4.4.0", + "globals": "^15.14.0", + "kolorist": "^1.8.0", + "local-pkg": "^1.0.0", + "mlly": "^1.7.4" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -767,6 +861,41 @@ "dev": true, "license": "MIT" }, + "node_modules/@mermaid-js/mermaid-mindmap": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/mermaid-mindmap/-/mermaid-mindmap-9.3.0.tgz", + "integrity": "sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@braintree/sanitize-url": "^6.0.0", + "cytoscape": "^3.23.0", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.1.0", + "d3": "^7.0.0", + "khroma": "^2.0.0", + "non-layered-tidy-tree-layout": "^2.0.2" + } + }, + "node_modules/@mermaid-js/mermaid-mindmap/node_modules/@braintree/sanitize-url": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", + "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@mermaid-js/parser": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.3.0.tgz", + "integrity": "sha512-HsvL6zgE5sUPGgkIDlmAWR1HTNHz2Iy11BAWPTa4Jjabkpguy4Ze2gzfLrg6pdRuBvFwgUYyxiaNqZwrEEXepA==", + "dev": true, + "license": "MIT", + "dependencies": { + "langium": "3.0.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.32.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.0.tgz", @@ -1120,6 +1249,290 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -1127,6 +1540,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -1182,6 +1602,14 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -1468,6 +1896,19 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/algoliasearch": { "version": "5.20.0", "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.20.0.tgz", @@ -1536,6 +1977,34 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -1547,6 +2016,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/confbox": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.1.tgz", + "integrity": "sha512-hkT3yDPFbs95mNCy1+7qNKC6Pro+/ibzYxtM2iqEigpf0sVw+bg4Zh9/snjsBcf990vfIsg5+1U7VyiyBb3etg==", + "dev": true, + "license": "MIT" + }, "node_modules/copy-anything": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", @@ -1563,6 +2049,16 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "dev": true, + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -1570,64 +2066,650 @@ "dev": true, "license": "MIT" }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "node_modules/cytoscape": { + "version": "3.31.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.31.1.tgz", + "integrity": "sha512-Hx5Mtb1+hnmAKaZZ/7zL1Y5HTFYOjdDswZy/jD+1WINRU8KVi1B7+vlHdsTwY+VCFucTreoyu1RDzQJ9u0d2Hw==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=0.10" } }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", "dev": true, "license": "MIT", "dependencies": { - "dequal": "^2.0.0" + "cose-base": "^1.0.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "peerDependencies": { + "cytoscape": "^3.2.0" } }, - "node_modules/emoji-regex-xs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", - "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "peerDependencies": { + "cytoscape": "^3.2.0" } }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "dev": true, + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" }, "engines": { "node": ">=12" - }, - "optionalDependencies": { + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dev": true, + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "dev": true, + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz", + "integrity": "sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==", + "dev": true, + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dev": true, + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dompurify": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", + "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==", + "dev": true, + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", @@ -1660,6 +2742,13 @@ "dev": true, "license": "MIT" }, + "node_modules/exsolve": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.1.tgz", + "integrity": "sha512-Smf0iQtkQVJLaph8r/qS8C8SWfQkaq9Q/dFcD44MLbJj6DNhlWefVuaS21SjfqOsBbjVlKtbCj6L9ekXK6EZUg==", + "dev": true, + "license": "MIT" + }, "node_modules/focus-trap": { "version": "7.6.4", "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz", @@ -1685,6 +2774,26 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "dev": true, + "license": "MIT" + }, "node_modules/hast-util-to-html": { "version": "9.0.4", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.4.tgz", @@ -1741,6 +2850,29 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-what": { "version": "4.1.16", "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", @@ -1754,6 +2886,95 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/katex": { + "version": "0.16.21", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", + "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", + "dev": true, + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==", + "dev": true + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/langium": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/langium/-/langium-3.0.0.tgz", + "integrity": "sha512-+Ez9EoiByeoTu/2BXmEaZ06iPNXM6thWJp02KfBO/raSMyCJ4jw7AkWWa+zBCTm0+Tw1Fj9FOxdqSskyN5nAwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chevrotain": "~11.0.3", + "chevrotain-allstar": "~0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.0.8" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "dev": true, + "license": "MIT" + }, + "node_modules/local-pkg": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz", + "integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.0.1", + "quansync": "^0.2.8" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true, + "license": "MIT" + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -1771,6 +2992,19 @@ "dev": true, "license": "MIT" }, + "node_modules/marked": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-13.0.3.tgz", + "integrity": "sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mdast-util-to-hast": { "version": "13.2.0", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", @@ -1793,6 +3027,35 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mermaid": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.4.1.tgz", + "integrity": "sha512-Mb01JT/x6CKDWaxigwfZYuYmDZ6xtrNwNlidKZwkSrDaY9n90tdrJTV5Umk+wP1fZscGptmKFXHsXMDEVZ+Q6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.0.1", + "@iconify/utils": "^2.1.32", + "@mermaid-js/parser": "^0.3.0", + "@types/d3": "^7.4.3", + "cytoscape": "^3.29.2", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.11", + "dayjs": "^1.11.10", + "dompurify": "^3.2.1", + "katex": "^0.16.9", + "khroma": "^2.1.0", + "lodash-es": "^4.17.21", + "marked": "^13.0.2", + "roughjs": "^4.6.6", + "stylis": "^4.3.1", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.1" + } + }, "node_modules/micromark-util-character": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", @@ -1901,6 +3164,45 @@ "dev": true, "license": "MIT" }, + "node_modules/mlly": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", + "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "pathe": "^2.0.1", + "pkg-types": "^1.3.0", + "ufo": "^1.5.4" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -1920,6 +3222,14 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/non-layered-tidy-tree-layout": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/oniguruma-to-es": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz", @@ -1932,6 +3242,30 @@ "regex-recursion": "^5.1.1" } }, + "node_modules/package-manager-detector": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz", + "integrity": "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quansync": "^0.2.7" + } + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -1946,6 +3280,36 @@ "dev": true, "license": "ISC" }, + "node_modules/pkg-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz", + "integrity": "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.1", + "exsolve": "^1.0.1", + "pathe": "^2.0.3" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "dev": true, + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/postcss": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", @@ -1997,6 +3361,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/quansync": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.8.tgz", + "integrity": "sha512-4+saucphJMazjt7iOM27mbFCk+D9dd/zmgMDCzRZ8MEoBfYp7lAvoN38et/phRQF6wOPMy/OROBGgoWeSKyluA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, "node_modules/regex": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/regex/-/regex-5.1.1.tgz", @@ -2032,6 +3413,13 @@ "dev": true, "license": "MIT" }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "dev": true, + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.32.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.0.tgz", @@ -2071,6 +3459,33 @@ "fsevents": "~2.3.2" } }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/search-insights": { "version": "2.17.3", "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", @@ -2142,6 +3557,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "dev": true, + "license": "MIT" + }, "node_modules/superjson": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", @@ -2162,6 +3584,13 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -2173,6 +3602,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/ufo": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "dev": true, + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", @@ -2253,6 +3699,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -2385,6 +3845,75 @@ } } }, + "node_modules/vitepress-plugin-mermaid": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/vitepress-plugin-mermaid/-/vitepress-plugin-mermaid-2.0.17.tgz", + "integrity": "sha512-IUzYpwf61GC6k0XzfmAmNrLvMi9TRrVRMsUyCA8KNXhg/mQ1VqWnO0/tBVPiX5UoKF1mDUwqn5QV4qAJl6JnUg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "@mermaid-js/mermaid-mindmap": "^9.3.0" + }, + "peerDependencies": { + "mermaid": "10 || 11", + "vitepress": "^1.0.0 || ^1.0.0-alpha" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true, + "license": "MIT" + }, "node_modules/vue": { "version": "3.5.13", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", diff --git a/doc/package.json b/doc/package.json index 70129724b..b45a7e5a0 100644 --- a/doc/package.json +++ b/doc/package.json @@ -1,7 +1,9 @@ { "devDependencies": { "@types/node": "^22.10.5", - "vitepress": "^1.5.0" + "mermaid": "^11.4.1", + "vitepress": "^1.5.0", + "vitepress-plugin-mermaid": "^2.0.17" }, "scripts": { "dev": "vitepress dev", diff --git a/doc/usage/chat-buffer/agents.md b/doc/usage/chat-buffer/agents.md index f63e2f07f..770a03225 100644 --- a/doc/usage/chat-buffer/agents.md +++ b/doc/usage/chat-buffer/agents.md @@ -9,7 +9,7 @@ As outlined by Andrew Ng in [Agentic Design Patterns Part 3, Tool Use](https://www.deeplearning.ai/the-batch/agentic-design-patterns-part-3-tool-use), LLMs can act as agents by leveraging external tools. Andrew notes some common examples such as web searching or code execution that have obvious benefits when using LLMs. -In the plugin, tools are simply context and actions that are shared with an LLM via a `system` prompt. The LLM and the chat buffer act as an agent by orchestrating their use within Neovim. Tools give LLM's knowledge and a defined schema which can be included in the response for the plugin to parse, execute and feedback on. Agents and tools can be added as a participant to the chat buffer by using the `@` key. +In the plugin, tools are simply context and actions that are shared with an LLM via a `system` prompt. The LLM can act as an agent by requesting tools via the chat buffer which in turn orchestrates their use within Neovim. Agents and tools can be added as a participant to the chat buffer by using the `@` key. > [!IMPORTANT] > The agentic use of some tools in the plugin results in you, the developer, acting as the human-in-the-loop and @@ -17,11 +17,27 @@ In the plugin, tools are simply context and actions that are shared with an LLM ## How Tools Work -LLMs are instructured by the plugin to return a structured XML block which has been defined for each tool. The chat buffer parses the LLMs response and detects any tool use before calling the appropriate tool. The chat buffer will then be updated with the outcome. Depending on the tool, flags may be inserted on the chat buffer for later processing. +When a tool is added to the chat buffer, the LLM is instructured by the plugin to return a structured XML block which has been defined for each tool. The chat buffer parses the LLMs response and detects any tool use before triggering the _agent/init.lua_ file. The agent triggers off a series of events, which sees tool's added to a queue and sequentially worked with their putput being shared back to the LLM via the chat buffer. Depending on the tool, flags may be inserted on the chat buffer for later processing. + +An outline of the architecture can be seen [here](/extending/tools#architecture). + +## Approvals + +Some tools, such as the _@cmd_runner_, require the user to approve any actions before they can be executed. If the tool requires this a `vim.fn.confirm` dialog will prompt you for a response. ## @cmd_runner -The _@cmd_runner_ tool enables an LLM to execute commands on your machine, subject to your authorization. A common example can be asking the LLM to run your test suite and provide feedback on any failures. Some commands do not write any data to [stdout](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)) which means the plugin can't pass the output of the execution to the LLM. When this occurs, the tool will instead share the exit code. +The _@cmd_runner_ tool enables an LLM to execute commands on your machine, subject to your authorization. For example: + +```md +Can you use the @cmd_runner tool to run my test suite with `pytest`? +``` + +```md +Use the @cmd_runner tool to install any missing libraries in my project +``` + +Some commands do not write any data to [stdout](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)) which means the plugin can't pass the output of the execution to the LLM. When this occurs, the tool will instead share the exit code. The LLM is specifically instructed to detect if you're running a test suite, and if so, to insert a flag in its XML request. This is then detected and the outcome of the test is stored in the corresponding flag on the chat buffer. This makes it ideal for [workflows](/extending/workflows) to hook into. @@ -40,7 +56,15 @@ An example of the XML that an LLM may generate for the tool: ## @editor -The _@editor_ tool enables an LLM to modify the code in a Neovim buffer. If a buffer's content has been shared with the LLM then the tool can be used to add, edit or delete specific lines. Consider pinning or watching a buffer to avoid manually re-sending a buffer's content to the LLM. +The _@editor_ tool enables an LLM to modify the code in a Neovim buffer. If a buffer's content has been shared with the LLM then the tool can be used to add, edit or delete specific lines. Consider pinning or watching a buffer to avoid manually re-sending a buffer's content to the LLM: + +```md +Use the @editor tool refactor the code in #buffer{watch} +``` + +```md +Can you apply the suggested changes to the buffer with the @editor tool? +``` An example of the XML that an LLM may generate for the tool: @@ -117,16 +141,20 @@ An example of the XML that an LLM may generate for the tool: ``` -## @rag - -The _@rag_ tool uses [jina.ai](https://jina.ai) to parse a given URL's content and convert it into plain text before sharing with the LLM. It also gives the LLM the ability to search the internet for information. - ## @full_stack_dev The _@full_stack_dev_ agent is a combination of the _@cmd_runner_, _@editor_ and _@files_ tools. ## Useful Tips +### Combining Tools + +Consider combining tools for complex tasks: + +```md +@full_stack_dev I want to play Snake. Can you create the game for me in Python and install any packages you need. Let's save it to ~/Code/Snake. When you've finished writing it, can you open it so I can play? +``` + ### Automatic Tool Mode The plugin allows you to run tools on autopilot. This automatically approves any tool use instead of prompting the user, disables any diffs, and automatically saves any buffers that the agent has edited. Simply set the global variable `vim.g.codecompanion_auto_tool_mode` to enable this or set it to `nil` to undo this. Alternatively, the keymap `gta` will toggle the feature whist from the chat buffer. From 17ab6fb4efafee245f6fb63871309411b7f5a91f Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Mon, 3 Mar 2025 22:15:32 +0000 Subject: [PATCH 34/38] wip: breaking config changes --- doc/configuration/chat-buffer.md | 36 +++-- doc/configuration/others.md | 3 +- lua/codecompanion/completion.lua | 8 +- lua/codecompanion/config.lua | 60 ++++---- .../strategies/chat/agents/init.lua | 67 ++++----- lua/codecompanion/strategies/chat/init.lua | 2 +- plugin/codecompanion.lua | 14 +- tests/config.lua | 138 +++++++++--------- 8 files changed, 165 insertions(+), 163 deletions(-) diff --git a/doc/configuration/chat-buffer.md b/doc/configuration/chat-buffer.md index ff0480700..4254f3770 100644 --- a/doc/configuration/chat-buffer.md +++ b/doc/configuration/chat-buffer.md @@ -119,23 +119,12 @@ Credit to [@lazymaniac](https://github.com/lazymaniac) for the [inspiration](htt ## Agents and Tools -Tools perform specific tasks (e.g., running shell commands, editing buffers, etc.) when invoked by an LLM. You can group them into an Agent and both can be referenced with `@` when in the chat buffer: +Tools perform specific tasks (e.g., running shell commands, editing buffers, etc.) when invoked by an LLM. Multiple tools can be grouped together. Both can be referenced with `@` when in the chat buffer: ```lua require("codecompanion").setup({ strategies = { chat = { - agents = { - ["my_agent"] = { - description = "A custom agent combining tools", - system_prompt = "Describe what the agent should do", - tools = { - "cmd_runner", - "editor", - -- Add your own tools or reuse existing ones - }, - }, - }, tools = { ["my_tool"] = { description = "Run a custom task", @@ -144,6 +133,17 @@ require("codecompanion").setup({ return "Tool result" end, }, + groups = { + ["my_group"] = { + description = "A custom agent combining tools", + system_prompt = "Describe what the agent should do", + tools = { + "cmd_runner", + "editor", + -- Add your own tools or reuse existing ones + }, + }, + }, }, }, }, @@ -160,14 +160,12 @@ Some tools, such as the [@cmd_runner](/usage/chat-buffer/agents.html#cmd-runner) require("codecompanion").setup({ strategies = { chat = { - agents = { - tools = { - ["cmd_runner"] = { - opts = { - requires_approval = false, - }, + tools = { + ["cmd_runner"] = { + opts = { + requires_approval = false, }, - } + }, } } } diff --git a/doc/configuration/others.md b/doc/configuration/others.md index 9df3cfc05..6e1964f87 100644 --- a/doc/configuration/others.md +++ b/doc/configuration/others.md @@ -50,9 +50,8 @@ The plugin sets the following highlight groups during setup: - `CodeCompanionChatHeader` - The headers in the chat buffer - `CodeCompanionChatSeparator` - Separator between headings in the chat buffer - `CodeCompanionChatTokens` - Virtual text in the chat buffer showing the token count -- `CodeCompanionChatAgent` - Agents in the chat buffer - `CodeCompanionChatTool` - Tools in the chat buffer +- `CodeCompanionChatToolGroups` - Tool groups in the chat buffer - `CodeCompanionChatVariable` - Variables in the chat buffer - `CodeCompanionVirtualText` - All other virtual text in the plugin - diff --git a/lua/codecompanion/completion.lua b/lua/codecompanion/completion.lua index 613022b22..fc1e3abdb 100644 --- a/lua/codecompanion/completion.lua +++ b/lua/codecompanion/completion.lua @@ -68,9 +68,9 @@ end ---Return the tools to be used for completion ---@return table function M.tools() - -- Add agents + -- Add groups local items = vim - .iter(config.strategies.chat.agents) + .iter(config.strategies.chat.tools.groups) :filter(function(label) return label ~= "tools" end) @@ -87,9 +87,9 @@ function M.tools() -- Add tools vim - .iter(config.strategies.chat.agents.tools) + .iter(config.strategies.chat.tools) :filter(function(label) - return label ~= "opts" + return label ~= "opts" and label ~= "groups" end) :each(function(label, v) table.insert(items, { diff --git a/lua/codecompanion/config.lua b/lua/codecompanion/config.lua index 31497fc43..7a8ece507 100644 --- a/lua/codecompanion/config.lua +++ b/lua/codecompanion/config.lua @@ -46,39 +46,40 @@ local defaults = { ---@type string user = "Me", }, - agents = { - ["full_stack_dev"] = { - description = "Full Stack Developer - Can run code, edit code and modify files", - system_prompt = "**DO NOT** make any assumptions about the dependencies that a user has installed. If you need to install any dependencies to fulfil the user's request, do so via the Command Runner tool. If the user doesn't specify a path, use their current working directory.", - tools = { - "cmd_runner", - "editor", - "files", - }, - }, - tools = { - ["cmd_runner"] = { - callback = "strategies.chat.agents.tools.cmd_runner", - description = "Run shell commands initiated by the LLM", - opts = { - requires_approval = true, + tools = { + groups = { + ["full_stack_dev"] = { + description = "Full Stack Developer - Can run code, edit code and modify files", + system_prompt = "**DO NOT** make any assumptions about the dependencies that a user has installed. If you need to install any dependencies to fulfil the user's request, do so via the Command Runner tool. If the user doesn't specify a path, use their current working directory.", + tools = { + "cmd_runner", + "editor", + "files", }, }, - ["editor"] = { - callback = "strategies.chat.agents.tools.editor", - description = "Update a buffer with the LLM's response", - }, - ["files"] = { - callback = "strategies.chat.agents.tools.files", - description = "Update the file system with the LLM's response", - opts = { - requires_approval = true, - }, + }, + ["cmd_runner"] = { + callback = "strategies.chat.agents.tools.cmd_runner", + description = "Run shell commands initiated by the LLM", + opts = { + requires_approval = true, }, + }, + ["editor"] = { + callback = "strategies.chat.agents.tools.editor", + description = "Update a buffer with the LLM's response", + }, + ["files"] = { + callback = "strategies.chat.agents.tools.files", + description = "Update the file system with the LLM's response", opts = { - auto_submit_errors = false, -- Send any errors to the LLM automatically? - auto_submit_success = false, -- Send any successful output to the LLM automatically? - system_prompt = [[## Tools Access and Execution Guidelines + requires_approval = true, + }, + }, + opts = { + auto_submit_errors = false, -- Send any errors to the LLM automatically? + auto_submit_success = false, -- Send any successful output to the LLM automatically? + system_prompt = [[## Tools Access and Execution Guidelines ### Overview You now have access to specialized tools that empower you to assist users with specific tasks. These tools are available only when explicitly requested by the user. @@ -92,7 +93,6 @@ You now have access to specialized tools that empower you to assist users with s - If issuing commands of the same type, combine them within one `` XML block with separate `` entries. - If issuing commands for different tools, ensure they're wrapped in `` tags within the `` block. - **No Side Effects:** Tool invocations should not alter your core tasks or the general conversation structure.]], - }, }, }, variables = { diff --git a/lua/codecompanion/strategies/chat/agents/init.lua b/lua/codecompanion/strategies/chat/agents/init.lua index 99a64d174..96174a01a 100644 --- a/lua/codecompanion/strategies/chat/agents/init.lua +++ b/lua/codecompanion/strategies/chat/agents/init.lua @@ -1,5 +1,5 @@ ---@class CodeCompanion.Agent ----@field agent_config table The agent strategy from the config +---@field tools_config table The agent strategy from the config ---@field aug number The augroup for the tool ---@field bufnr number The buffer of the chat buffer ---@field constants table The constants for the tool @@ -65,7 +65,7 @@ function Agent.new(args) stdout = {}, stderr = {}, tool = {}, - agent_config = config.strategies.chat.agents, + tools_config = config.strategies.chat.tools, tools_ns = api.nvim_create_namespace(CONSTANTS.NS_TOOLS), }, { __index = Agent }) @@ -101,14 +101,14 @@ function Agent:set_autocmds() if self.tool.output and self.tool.output.errors then self.tool.output.errors(self, error) end - if self.agent_config.tools.opts.auto_submit_errors then + if self.tools_config.opts.auto_submit_errors then self.chat:submit() end end -- Handle any success if request.data.status == CONSTANTS.STATUS_SUCCESS then - if self.agent_config.tools.opts.auto_submit_success then + if self.tools_config.opts.auto_submit_success then self.chat:submit() end end @@ -186,7 +186,7 @@ function Agent:execute(chat, xml) end local name = s.tool._attr.name - local tool_config = self.agent_config.tools[name] + local tool_config = self.tools_config[name] ---@type CodeCompanion.Agent.Tool|nil local resolved_tool @@ -239,36 +239,31 @@ function Agent:find(chat, message) return nil, nil end - local agents = {} + local groups = {} local tools = {} local function is_found(tool) return message.content:match("%f[%w" .. CONSTANTS.PREFIX .. "]" .. CONSTANTS.PREFIX .. tool .. "%f[%W]") end - -- Process agents - vim - .iter(self.agent_config) - :filter(function(name) - return name ~= "tools" - end) - :each(function(agent) - if is_found(agent) then - table.insert(agents, agent) + -- Process groups + vim.iter(self.tools_config.groups):each(function(tool) + if is_found(tool) then + table.insert(groups, tool) - for _, tool in ipairs(self.agent_config[agent].tools) do - if not vim.tbl_contains(tools, tool) then - table.insert(tools, tool) - end + for _, t in ipairs(self.tools_config.groups[tool]) do + if not vim.tbl_contains(tools, t) then + table.insert(tools, t) end end - end) + end + end) -- Process tools vim - .iter(self.agent_config.tools) + .iter(self.tools_config) :filter(function(name) - return name ~= "opts" + return name ~= "opts" and name ~= "groups" end) :each(function(tool) if is_found(tool) and not vim.tbl_contains(tools, tool) then @@ -280,28 +275,28 @@ function Agent:find(chat, message) return nil, nil end - return tools, agents + return tools, groups end ---@param chat CodeCompanion.Chat ---@param message table ---@return boolean function Agent:parse(chat, message) - local tools, agents = self:find(chat, message) + local tools, groups = self:find(chat, message) - if tools or agents then + if tools or groups then if tools and not vim.tbl_isempty(tools) then for _, tool in ipairs(tools) do - chat:add_tool(tool, self.agent_config.tools[tool]) + chat:add_tool(tool, self.tools_config[tool]) end end - if agents and not vim.tbl_isempty(agents) then - for _, agent in ipairs(agents) do - if self.agent_config[agent].system_prompt then + if groups and not vim.tbl_isempty(groups) then + for _, tool in ipairs(groups) do + if self.tools_config[tool].system_prompt then chat:add_message({ role = config.constants.SYSTEM_ROLE, - content = self.agent_config[agent].system_prompt, + content = self.tools_config[tool].system_prompt, }, { tag = "tool", visible = false }) end end @@ -316,11 +311,13 @@ end ---@param message string ---@return string function Agent:replace(message) - for tool, _ in pairs(self.agent_config.tools) do - message = vim.trim(message:gsub(CONSTANTS.PREFIX .. tool, tool)) + for tool, _ in pairs(self.tools_config) do + if tool ~= "opts" and tool ~= "groups" then + message = vim.trim(message:gsub(CONSTANTS.PREFIX .. tool, tool)) + end end - for agent, _ in pairs(self.agent_config) do - message = vim.trim(message:gsub(CONSTANTS.PREFIX .. agent, "")) + for group, _ in pairs(self.tools_config.groups) do + message = vim.trim(message:gsub(CONSTANTS.PREFIX .. group, "")) end return message @@ -390,7 +387,7 @@ function Agent:add_error_to_chat(error) content = "Please correct for the error message I've shared", }) - if self.agent_config.opts and self.agent_config.opts.auto_submit_errors then + if self.tools_config.opts and self.tools_config.opts.auto_submit_errors then self.chat:submit() end diff --git a/lua/codecompanion/strategies/chat/init.lua b/lua/codecompanion/strategies/chat/init.lua index 345e04eb6..c0654f1c2 100644 --- a/lua/codecompanion/strategies/chat/init.lua +++ b/lua/codecompanion/strategies/chat/init.lua @@ -616,7 +616,7 @@ function Chat:add_tool(tool, tool_config) if not self:has_tools() then self:add_message({ role = config.constants.SYSTEM_ROLE, - content = config.strategies.chat.agents.tools.opts.system_prompt, + content = config.strategies.chat.tools.opts.system_prompt, }, { visible = false, reference = "tool_system_prompt", tag = "tool" }) end diff --git a/plugin/codecompanion.lua b/plugin/codecompanion.lua index ad86168a5..7173f3c78 100644 --- a/plugin/codecompanion.lua +++ b/plugin/codecompanion.lua @@ -14,8 +14,8 @@ local api = vim.api api.nvim_set_hl(0, "CodeCompanionChatHeader", { link = "@markup.heading.2.markdown", default = true }) api.nvim_set_hl(0, "CodeCompanionChatSeparator", { link = "@punctuation.special.markdown", default = true }) api.nvim_set_hl(0, "CodeCompanionChatTokens", { link = "Comment", default = true }) -api.nvim_set_hl(0, "CodeCompanionChatAgent", { link = "Constant", default = true }) api.nvim_set_hl(0, "CodeCompanionChatTool", { link = "Special", default = true }) +api.nvim_set_hl(0, "CodeCompanionChatToolGroup", { link = "Constant", default = true }) api.nvim_set_hl(0, "CodeCompanionChatVariable", { link = "Identifier", default = true }) api.nvim_set_hl(0, "CodeCompanionVirtualText", { link = "Comment", default = true }) @@ -32,17 +32,17 @@ api.nvim_create_autocmd("FileType", { vim.cmd.syntax('match CodeCompanionChatVariable "#' .. name .. '{[^}]*}"') end end) - vim.iter(config.strategies.chat.agents.tools):each(function(name, _) - vim.cmd.syntax('match CodeCompanionChatTool "@' .. name .. '"') - end) vim - .iter(config.strategies.chat.agents) + .iter(config.strategies.chat.tools) :filter(function(name) - return name ~= "tools" + return name ~= "groups" and name ~= "opts" end) :each(function(name, _) - vim.cmd.syntax('match CodeCompanionChatAgent "@' .. name .. '"') + vim.cmd.syntax('match CodeCompanionChatTool "@' .. name .. '"') end) + vim.iter(config.strategies.chat.tools.groups):each(function(name, _) + vim.cmd.syntax('match CodeCompanionChatToolGroup "@' .. name .. '"') + end) end), }) diff --git a/tests/config.lua b/tests/config.lua index bc2f9d5fc..92f8d0bf9 100644 --- a/tests/config.lua +++ b/tests/config.lua @@ -50,74 +50,82 @@ return { llm = "assistant", user = "foo", }, - agents = { - tools = { - ["editor"] = { - callback = "strategies.chat.agents.tools.editor", - description = "Update a buffer with the LLM's response", - }, - ["foo"] = { - callback = "utils.foo", - description = "Some foo function", - }, - ["bar"] = { - callback = "utils.bar", - description = "Some bar function", - }, - ["bar_again"] = { - callback = "utils.bar_again", - description = "Some bar_again function", - }, - ["func"] = { - callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func.lua", - description = "Some function tool to test", - }, - ["func_consecutive"] = { - callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func_consecutive.lua", - description = "Consecutive function tool to test", - }, - ["func_error"] = { - callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func_error.lua", - description = "Error function tool to test", - }, - ["func_queue"] = { - callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func_queue.lua", - description = "Some function tool to test", - }, - ["func_queue_2"] = { - callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func_queue_2.lua", - description = "Some function tool to test", + tools = { + ["editor"] = { + callback = "strategies.chat.agents.tools.editor", + description = "Update a buffer with the LLM's response", + }, + ["foo"] = { + callback = "utils.foo", + description = "Some foo function", + }, + ["bar"] = { + callback = "utils.bar", + description = "Some bar function", + }, + ["bar_again"] = { + callback = "utils.bar_again", + description = "Some bar_again function", + }, + ["func"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func.lua", + description = "Some function tool to test", + }, + ["func_consecutive"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func_consecutive.lua", + description = "Consecutive function tool to test", + }, + ["func_error"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func_error.lua", + description = "Error function tool to test", + }, + ["func_queue"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func_queue.lua", + description = "Some function tool to test", + }, + ["func_queue_2"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func_queue_2.lua", + description = "Some function tool to test", + }, + ["func_approval"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func.lua", + description = "Some function tool to test but with approval", + opts = { + requires_approval = true, }, - ["func_approval"] = { - callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/func.lua", - description = "Some function tool to test but with approval", - opts = { - requires_approval = true, + }, + ["cmd"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/cmd.lua", + description = "Cmd tool", + }, + ["cmd_consecutive"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/cmd_consecutive.lua", + description = "Cmd tool", + }, + ["cmd_error"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/cmd_error.lua", + description = "Cmd tool", + }, + ["cmd_queue"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/cmd_queue.lua", + description = "Cmd tool", + }, + ["mock_cmd_runner"] = { + callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/mock_cmd_runner.lua", + description = "Cmd tool", + }, + groups = { + ["tool_group"] = { + description = "Tool Group", + system_prompt = "My tool group system prompt", + tools = { + "func", + "cmd", }, }, - ["cmd"] = { - callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/cmd.lua", - description = "Cmd tool", - }, - ["cmd_consecutive"] = { - callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/cmd_consecutive.lua", - description = "Cmd tool", - }, - ["cmd_error"] = { - callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/cmd_error.lua", - description = "Cmd tool", - }, - ["cmd_queue"] = { - callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/cmd_queue.lua", - description = "Cmd tool", - }, - ["mock_cmd_runner"] = { - callback = vim.fn.getcwd() .. "/tests/strategies/chat/agents/tools/stubs/mock_cmd_runner.lua", - description = "Cmd tool", - }, - opts = { - system_prompt = [[My tool system prompt]], - }, + }, + opts = { + system_prompt = [[My tool system prompt]], }, }, variables = { From cb4b7f4f93082cff2384c1d5b01bfaf0a8c4ff5f Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Mon, 3 Mar 2025 22:26:41 +0000 Subject: [PATCH 35/38] update docs --- doc/extending/tools.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/extending/tools.md b/doc/extending/tools.md index 632479591..61a77ad63 100644 --- a/doc/extending/tools.md +++ b/doc/extending/tools.md @@ -223,21 +223,20 @@ You must: ### `handlers` -The _handlers_ table consists of three methods: +The _handlers_ table consists of two methods: 1. `setup` - Is called before any of the commands/functions are. This is useful if you wish to set the cmds dynamically on the tool itself, like in the _@cmd_runner_ tool. -2. `approved` - Must return a boolean and contains logic to prompt the user for their approval prior to a command/function being executed. This is used in both the _@files_ and _@cmd_runner_ tool to allow the user to validate the actions the LLM is proposing to take. 3. `on_exit` - Is called after all of the commands/function have executed. ### `output` -The _output_ table consists of three methods: +The _output_ table consists of four methods: 1. `success` - Is called after _every_ successful execution of a command/function. This can be a useful handler to use to notfiy the LLM of the success. 2. `error` - Is called when an error occurs whilst executing a command/function. It will only ever be called once as the whole execution for the group of commands/function is halted. This is a useful handler to use to notify the LLM of the failure. +3. `prompt` - Is called when user approval is required. It forms the message prompt which the user is asked to confirm or reject. 3. `rejected` - Is called when a user rejects the approval to run a command/function. This method is used to inform the LLM of the rejection. - ### `request` The request table is populated at runtime and contains the parsed XML that the LLM has requested to run. From f06940e1513947fc7724fe8dd198f736dc7721d3 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Mon, 3 Mar 2025 22:29:49 +0000 Subject: [PATCH 36/38] wip: remove mocks --- tests/mocks/job.lua | 47 --------------------------------------------- 1 file changed, 47 deletions(-) delete mode 100644 tests/mocks/job.lua diff --git a/tests/mocks/job.lua b/tests/mocks/job.lua deleted file mode 100644 index 1ddb78ccd..000000000 --- a/tests/mocks/job.lua +++ /dev/null @@ -1,47 +0,0 @@ ----@class MockJob ----@field is_shutdown boolean ----@field _opts table ----@field _stdout_results table ----@field _stderr_results table ----@field _on_exit function -local MockJob = {} -MockJob.__index = MockJob - ----Create a new mock job ----@param opts table ----@return MockJob -function MockJob:new(opts) - ---@type MockJob - local job = setmetatable({ - is_shutdown = false, - _opts = opts, - _stdout_results = { "mocked stdout" }, - _stderr_results = {}, - _on_exit = opts.on_exit, - }, self) - - return job -end - ----Mock start function ----@return MockJob -function MockJob:start() - -- Execute immediately instead of scheduling - if self._on_exit then - self._on_exit(self, 0) - end - return self -end - ----Mock and_then_wrap function ----@param next_job MockJob -function MockJob:and_then_wrap(next_job) - next_job:start() -end - ----Mock shutdown function -function MockJob:shutdown() - self.is_shutdown = true -end - -return MockJob From efbcdd1f7e8faeb9f857aef5012d2a6f52c2575d Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Tue, 4 Mar 2025 09:21:31 +0000 Subject: [PATCH 37/38] wip: add events for agent --- doc/usage/events.md | 6 ++++-- .../strategies/chat/agents/executor/init.lua | 18 ++++++++++++++---- .../strategies/chat/agents/init.lua | 4 +++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/doc/usage/events.md b/doc/usage/events.md index bfcf2f76f..ea9382e22 100644 --- a/doc/usage/events.md +++ b/doc/usage/events.md @@ -14,9 +14,11 @@ The events that you can access are: - `CodeCompanionChatAdapter` - Fired after the adapter has been set in the chat - `CodeCompanionChatModel` - Fired after the model has been set in the chat - `CodeCompanionChatPin` - Fired after a pinned reference has been updated in the messages table +- `CodeCompanionAgentStarted` - Fired when an agent has been initiated to run tools +- `CodeCompanionAgentFinished` - Fired when an agent has finished running all tools - `CodeCompanionToolAdded` - Fired when a tool has been added to a chat -- `CodeCompanionAgentStarted` - Fired when an agent has been initiated in the chat -- `CodeCompanionAgentFinished` - Fired when an agent has finished all tool executions +- `CodeCompanionToolStarted` - Fired when a tool has started executing +- `CodeCompanionToolFinished` - Fired when a tool has finished executing - `CodeCompanionInlineStarted` - Fired at the start of the Inline strategy - `CodeCompanionInlineFinished` - Fired at the end of the Inline strategy - `CodeCompanionRequestStarted` - Fired at the start of any API request diff --git a/lua/codecompanion/strategies/chat/agents/executor/init.lua b/lua/codecompanion/strategies/chat/agents/executor/init.lua index 6a6901ff1..1251cf0c1 100644 --- a/lua/codecompanion/strategies/chat/agents/executor/init.lua +++ b/lua/codecompanion/strategies/chat/agents/executor/init.lua @@ -8,6 +8,7 @@ local util = require("codecompanion.utils") ---@field agent CodeCompanion.Agent ---@field current_cmd_tool table The current cmd tool that's being executed ---@field handlers table +---@field id number The id of the agent ---@field index number The index of the current command ---@field output table ---@field tool CodeCompanion.Agent.Tool @@ -16,11 +17,12 @@ local util = require("codecompanion.utils") local Executor = {} ---@param agent CodeCompanion.Agent -function Executor.new(agent) +---@param id number +function Executor.new(agent, id) local self = setmetatable({ agent = agent, current_cmd_tool = {}, - id = math.random(10000000), + id = id, queue = Queue.new(), }, { __index = Executor }) @@ -70,14 +72,20 @@ function Executor:setup_handlers() } end +local function finalize_agent(self) + return util.fire("AgentFinished", { id = self.id, bufnr = self.agent.bufnr }) +end + ---Setup the tool to be executed ---@param input? any ---@return nil function Executor:setup(input) if self.queue:is_empty() then + finalize_agent(self) return log:debug("Executor:execute - Queue empty") end if self.agent.status == self.agent.constants.STATUS_ERROR then + finalize_agent(self) return log:debug("Executor:execute - Error") end @@ -104,6 +112,7 @@ function Executor:setup(input) local ok, choice = pcall(vim.fn.confirm, prompt, "&Yes\n&No\n&Cancel") if not ok or choice == 0 or choice == 3 then -- Esc or Cancel log:debug("Executor:execute - Tool cancelled") + finalize_agent(self) return self:close() end if choice == 1 then -- Yes @@ -125,7 +134,7 @@ end ---@param input? any ---@return nil function Executor:execute(cmd, input) - util.fire("AgentStarted", { tool = self.tool.name, bufnr = self.agent.bufnr }) + util.fire("ToolStarted", { id = self.id, tool = self.tool.name, bufnr = self.agent.bufnr }) if type(cmd) == "function" then return FuncExecutor.new(self, cmd, 1):orchestrate(input) end @@ -144,6 +153,7 @@ function Executor:error(action, error) log:warn("Tool %s: %s", self.tool.name, error) end self.output.error(action, self.agent.stderr, self.agent.stdout) + finalize_agent(self) self:close() end @@ -164,7 +174,7 @@ end function Executor:close() log:debug("Executor:close") self.handlers.on_exit() - util.fire("AgentFinished", { name = self.tool.name, bufnr = self.agent.bufnr }) + util.fire("ToolFinished", { id = self.id, name = self.tool.name, bufnr = self.agent.bufnr }) self.agent.chat.subscribers:process(self.agent.chat) vim.g.codecompanion_current_tool = nil end diff --git a/lua/codecompanion/strategies/chat/agents/init.lua b/lua/codecompanion/strategies/chat/agents/init.lua index 96174a01a..5dbc68a2e 100644 --- a/lua/codecompanion/strategies/chat/agents/init.lua +++ b/lua/codecompanion/strategies/chat/agents/init.lua @@ -215,7 +215,8 @@ function Agent:execute(chat, xml) return executor.queue:push(self.tool) end - local executor = Executor.new(self) + local id = math.random(10000000) + local executor = Executor.new(self, id) -- This allows us to run multiple tools in a single response whether they're in -- their own XML block or they're in an array within the tag @@ -227,6 +228,7 @@ function Agent:execute(chat, xml) run_tool(executor, schema) end + util.fire("AgentStarted", { id = id, bufnr = self.bufnr }) return executor:setup() end From 0394b724e7a8ce9e53ebbef69280e9b5f1302702 Mon Sep 17 00:00:00 2001 From: Oli Morris Date: Tue, 4 Mar 2025 09:44:22 +0000 Subject: [PATCH 38/38] wip: fix groups --- lua/codecompanion/strategies/chat/agents/init.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lua/codecompanion/strategies/chat/agents/init.lua b/lua/codecompanion/strategies/chat/agents/init.lua index 5dbc68a2e..b2db455e2 100644 --- a/lua/codecompanion/strategies/chat/agents/init.lua +++ b/lua/codecompanion/strategies/chat/agents/init.lua @@ -253,7 +253,7 @@ function Agent:find(chat, message) if is_found(tool) then table.insert(groups, tool) - for _, t in ipairs(self.tools_config.groups[tool]) do + for _, t in ipairs(self.tools_config.groups[tool].tools) do if not vim.tbl_contains(tools, t) then table.insert(tools, t) end @@ -294,11 +294,11 @@ function Agent:parse(chat, message) end if groups and not vim.tbl_isempty(groups) then - for _, tool in ipairs(groups) do - if self.tools_config[tool].system_prompt then + for _, group in ipairs(groups) do + if self.tools_config.groups[group].system_prompt then chat:add_message({ role = config.constants.SYSTEM_ROLE, - content = self.tools_config[tool].system_prompt, + content = self.tools_config.groups[group].system_prompt, }, { tag = "tool", visible = false }) end end