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 diff --git a/codecompanion-workspace.json b/codecompanion-workspace.json index a9f1caaff..0c10fb786 100644 --- a/codecompanion-workspace.json +++ b/codecompanion-workspace.json @@ -140,6 +140,42 @@ "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" + }, + { + "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.txt" + } + ] } ] } 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 0115e13a6..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 @@ -3380,15 +3431,74 @@ 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. +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: @@ -3400,7 +3510,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. @@ -3412,7 +3522,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 @@ -3502,7 +3612,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 @@ -3524,7 +3634,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 @@ -3552,7 +3662,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) @@ -3581,33 +3691,20 @@ For the Code Runner agent, the `system_prompt` table was: HANDLERS -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 `handlers` table consists of three methods: -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 @@ -3792,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..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 + }, + }, + }, }, }, }, @@ -154,6 +154,24 @@ 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 = { + 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/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/doc/extending/tools.md b/doc/extending/tools.md index 1ff6f20bc..61a77ad63 100644 --- a/doc/extending/tools.md +++ b/doc/extending/tools.md @@ -6,7 +6,63 @@ 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. + +## 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 @@ -14,14 +70,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 +152,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 +170,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 +195,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) @@ -167,23 +223,19 @@ You must: ### `handlers` -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. +The _handlers_ table consists of two methods: -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. +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 four 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. `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` 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/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. 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/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/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 a42e821af..7a8ece507 100644 --- a/lua/codecompanion/config.lua +++ b/lua/codecompanion/config.lua @@ -46,46 +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.tools.cmd_runner", - description = "Run shell commands initiated by the LLM", - opts = { - user_approval = true, - }, - }, - ["editor"] = { - callback = "strategies.chat.tools.editor", - description = "Update a buffer with the LLM's response", - }, - ["files"] = { - callback = "strategies.chat.tools.files", - description = "Update the file system with the LLM's response", - opts = { - user_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", }, }, - ["rag"] = { - callback = "strategies.chat.tools.rag", - description = "Supplement the LLM with real-time info from the internet", - opts = { - hide_output = 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. @@ -99,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 = { @@ -541,7 +534,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 new file mode 100644 index 000000000..0983ec46f --- /dev/null +++ b/lua/codecompanion/strategies/chat/agents/executor/cmd.lua @@ -0,0 +1,107 @@ +local Job = require("plenary.job") +local log = require("codecompanion.utils.log") + +---@class CodeCompanion.Agent.Executor.Cmd +---@field executor CodeCompanion.Agent.Executor +---@field cmds table +---@field count number +---@field index number +local CmdExecutor = {} + +---@param executor CodeCompanion.Agent.Executor +---@param cmds table +---@param index number +function CmdExecutor.new(executor, cmds, index) + return setmetatable({ + executor = executor, + cmds = cmds, + count = vim.tbl_count(cmds), + index = index, + }, { __index = CmdExecutor }) +end + +---Orchestrate the tool function +---@return nil +function CmdExecutor:orchestrate() + log:debug("CmdExecutor:orchestrate %s", self.cmds) + + for i = self.index, self.count do + self:run(self.cmds[i], i) + end +end + +---Some commands output ANSI color codes which don't render in the chat buffer +---@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 +---@param index number +---@return nil +function CmdExecutor:run(cmd, index) + 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 + + vim.schedule(function() + local ok, output = pcall(function() + 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 the on_exit handler unless it's the last command + if index == self.count then + self.executor:close() + return self.executor:setup() + end + else + return self.executor:error(cmd, string.format("Failed with code %s", code)) + end + end) + + if not ok then + return self.executor:error(cmd, string.format("Error whilst running command %s: %s", cmd, output)) + end + end) + end, + }) + + if not vim.tbl_isempty(self.executor.current_cmd_tool) then + self.executor.current_cmd_tool:and_then_wrap(job) + else + job:start() + end + + self.executor.current_cmd_tool = job +end + +return CmdExecutor diff --git a/lua/codecompanion/strategies/chat/agents/executor/func.lua b/lua/codecompanion/strategies/chat/agents/executor/func.lua new file mode 100644 index 000000000..8e79d887a --- /dev/null +++ b/lua/codecompanion/strategies/chat/agents/executor/func.lua @@ -0,0 +1,95 @@ +local log = require("codecompanion.utils.log") + +---@class CodeCompanion.Agent.Executor.Func +---@field executor CodeCompanion.Agent.Executor +---@field func fun(self: CodeCompanion.Agent, actions: table, input: any) +---@field index number +local FuncExecutor = {} + +---@param executor CodeCompanion.Agent.Executor +---@param func fun() +---@param index number +function FuncExecutor.new(executor, func, index) + return setmetatable({ + executor = executor, + func = func, + index = index, + }, { __index = FuncExecutor }) +end + +---Orchestrate the tool function +---@param input any +---@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) + + 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 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.executor:close() + return self.executor:setup(output) + end +end + +---Run the tool's function +---@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(func, action, input, callback) + log:debug("FuncExecutor:run") + local ok, output = pcall(function() + return func(self.executor.agent, action, input) + end) + if not ok then + return self.executor:error(action, output) + end + + self.executor:success(action, output) + + if callback then + callback(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 new file mode 100644 index 000000000..1251cf0c1 --- /dev/null +++ b/lua/codecompanion/strategies/chat/agents/executor/init.lua @@ -0,0 +1,182 @@ +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 log = require("codecompanion.utils.log") +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 id number The id of the agent +---@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 id number +function Executor.new(agent, id) + local self = setmetatable({ + agent = agent, + current_cmd_tool = {}, + id = id, + queue = Queue.new(), + }, { __index = Executor }) + + _G.codecompanion_cancel_tool = false + + 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(self.agent) + end + end, + on_exit = function() + if self.tool.handlers and self.tool.handlers.on_exit then + self.tool.handlers.on_exit(self.agent) + end + end, + } + + 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) + end + end, + error = function(cmd, error, output) + if self.tool.output and self.tool.output.error then + 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(self.agent, cmd, output) + end + end, + } +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 + + -- Get the next tool to run + self.tool = self.queue:pop() + + -- 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` tool", self.tool.name) + + -- Check if the tool requires approval + 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) + if prompt == nil or prompt == "" then + prompt = ("Run the %q tool?"):format(self.tool.name) + end + + 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 + 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|table|function +---@param input? any +---@return nil +function Executor:execute(cmd, input) + 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 + return CmdExecutor.new(self, self.tool.cmds, 1):orchestrate() +end + +---Handle an error from a tool +---@param action table +---@param error? string +---@return nil +function Executor:error(action, error) + log:debug("Executor:error") + self.agent.status = self.agent.constants.STATUS_ERROR + if error then + table.insert(self.agent.stderr, 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 + +---Handle a successful completion of a tool +---@param action table +---@param output? string +---@return nil +function Executor:success(action, output) + log:debug("Executor:success") + if output then + table.insert(self.agent.stdout, output) + end + self.output.success(action, self.agent.stdout) +end + +---Close the execution of the tool +---@return nil +function Executor:close() + log:debug("Executor:close") + self.handlers.on_exit() + 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 + +return Executor 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..253358982 --- /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 new file mode 100644 index 000000000..b2db455e2 --- /dev/null +++ b/lua/codecompanion/strategies/chat/agents/init.lua @@ -0,0 +1,427 @@ +---@class CodeCompanion.Agent +---@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 +---@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") +local ui = require("codecompanion.utils.ui") +local util = require("codecompanion.utils") +local xml2lua = require("codecompanion.utils.xml.xml2lua") + +local api = vim.api + +local CONSTANTS = { + PREFIX = "@", + + NS_TOOLS = "CodeCompanion-agents", + AUTOCMD_GROUP = "codecompanion.agent", + + STATUS_ERROR = "error", + STATUS_SUCCESS = "success", + + PROCESSING_MSG = "Tool processing ...", +} + +---Parse XML in a given message +---@param message string +---@return table +local function parse_xml(message) + log:trace("Trying to parse: %s", message) + + local handler = TreeHandler:new() + local parser = xml2lua.parser(handler) + -- parser.options.stripWS = nil + parser:parse(message) + + log:trace("Parsed xml: %s", handler.root) + + return handler.root.tools +end + +---@class CodeCompanion.Agent +local Agent = {} + +---@param args table +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 = {}, + tools_config = config.strategies.chat.tools, + tools_ns = api.nvim_create_namespace(CONSTANTS.NS_TOOLS), + }, { __index = Agent }) + + return self +end + +---Set the autocmds for the tool +---@return nil +function Agent:set_autocmds() + api.nvim_create_autocmd("User", { + desc = "Handle responses from an Agent", + group = self.aug, + pattern = "CodeCompanionAgent*", + callback = function(request) + if request.data.bufnr ~= self.bufnr then + return + end + + if request.match == "CodeCompanionAgentStarted" then + log:info("[Agent] Initiated") + return ui.set_virtual_text( + self.bufnr, + self.tools_ns, + CONSTANTS.PROCESSING_MSG, + { hl_group = "CodeCompanionVirtualText" } + ) + elseif request.match == "CodeCompanionAgentFinished" then + -- Handle any errors + if request.data.status == CONSTANTS.STATUS_ERROR then + local error = request.data.stderr + log:error("Tool %s finished with error(s): %s", string.upper(self.tool.name), error) + + if self.tool.output and self.tool.output.errors then + self.tool.output.errors(self, error) + end + 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.tools_config.opts.auto_submit_success then + self.chat:submit() + end + end + end + self:reset() + end, + }) +end + +---Parse a chat buffer for tools +---@param chat CodeCompanion.Chat +---@param start_range number +---@param end_range number +---@return nil +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] + + local llm = {} + for id, node in query:iter_captures(tree:root(), chat.bufnr, start_range - 1, end_range - 1) do + if query.captures[id] == "content" then + table.insert(llm, vim.treesitter.get_node_text(node, chat.bufnr)) + end + end + + if vim.tbl_isempty(llm) then + return + end + + -- NOTE: Only work with the last response from the LLM + local response = llm[#llm] + + local parser = vim.treesitter.get_string_parser(response, "markdown") + tree = parser:parse()[1] + + local tools = {} + for id, node in query:iter_captures(tree:root(), response, 0, -1) do -- NOTE: Keep this scoped to 0,-1 + if query.captures[id] == "tool" then + local tool = vim.treesitter.get_node_text(node, response) + tool = tool:gsub("^`+", ""):gsub("```$", "") + table.insert(tools, vim.trim(tool)) + end + end + + log:trace("[Tools] Detected: %s", tools) + + if not vim.tbl_isempty(tools) then + self.extracted = tools + vim.iter(tools):each(function(t) + return self:execute(chat, t) + end) + end +end + +---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:execute(chat, xml) + self.chat = chat + + local ok, schema = pcall(parse_xml, xml) + if not ok then + self:add_error_to_chat(string.format("The XML schema couldn't be processed:\n\n%s", schema)):reset() + return log:error("Error parsing XML schema: %s", schema) + 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(executor, s) + -- If an error occurred, don't run any more tools + if self.status == CONSTANTS.STATUS_ERROR then + return + end + + local name = s.tool._attr.name + local tool_config = self.tools_config[name] + + ---@type CodeCompanion.Agent.Tool|nil + local resolved_tool + ok, resolved_tool = pcall(function() + 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") + log:info("XML:\n%s", xml) + log:info("Schema:\n%s", s) + return + 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() + + if self.tool.env then + local env = type(self.tool.env) == "function" and self.tool.env(s.tool) or {} + util.replace_placeholders(self.tool.cmds, env) + end + + return executor.queue:push(self.tool) + end + + 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 + if vim.isarray(schema.tool) then + vim.iter(schema.tool):each(function(tool) + run_tool(executor, { tool = tool }) + end) + else + run_tool(executor, schema) + end + + util.fire("AgentStarted", { id = id, bufnr = self.bufnr }) + return executor:setup() +end + +---Look for tools in a given message +---@param chat CodeCompanion.Chat +---@param message table +---@return table?, table? +function Agent:find(chat, message) + if not message.content then + return nil, nil + end + + local groups = {} + local tools = {} + + local function is_found(tool) + return message.content:match("%f[%w" .. CONSTANTS.PREFIX .. "]" .. CONSTANTS.PREFIX .. tool .. "%f[%W]") + end + + -- Process groups + vim.iter(self.tools_config.groups):each(function(tool) + if is_found(tool) then + table.insert(groups, tool) + + for _, t in ipairs(self.tools_config.groups[tool].tools) do + if not vim.tbl_contains(tools, t) then + table.insert(tools, t) + end + end + end + end) + + -- Process tools + vim + .iter(self.tools_config) + :filter(function(name) + return name ~= "opts" and name ~= "groups" + end) + :each(function(tool) + if is_found(tool) and not vim.tbl_contains(tools, tool) then + table.insert(tools, tool) + end + end) + + if #tools == 0 then + return nil, nil + end + + return tools, groups +end + +---@param chat CodeCompanion.Chat +---@param message table +---@return boolean +function Agent:parse(chat, message) + local tools, groups = self:find(chat, message) + + 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.tools_config[tool]) + end + end + + if groups and not vim.tbl_isempty(groups) 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.groups[group].system_prompt, + }, { tag = "tool", visible = false }) + end + end + end + return true + end + + return false +end + +---Replace the tool tag in a given message +---@param message string +---@return string +function Agent:replace(message) + 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 group, _ in pairs(self.tools_config.groups) do + message = vim.trim(message:gsub(CONSTANTS.PREFIX .. group, "")) + end + + return message +end + +---Reset the Agent class +---@return nil +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 Agent:fold_xml() + local query = vim.treesitter.query.parse( + "markdown", + [[ +( + fenced_code_block + (info_string) @lang + (code_fence_content) @code + (#eq? @lang "xml") +) + ]] + ) + + local parser = vim.treesitter.get_parser(self.bufnr, "markdown") + local tree = parser:parse()[1] + + vim.o.foldmethod = "manual" + + for _, matches, _ in query:iter_matches(tree:root(), self.bufnr) do + local nodes = matches[2] -- The second capture is always the code block + local code_node = type(nodes) == "table" and nodes[1] or nodes + + if code_node then + local start_row, _, end_row, _ = code_node:range() + if start_row < end_row then + api.nvim_buf_call(self.bufnr, function() + vim.cmd(string.format("%d,%dfold", start_row, end_row)) + end) + end + end + end +end + +---Add an error message to the chat buffer +---@param error string +---@return CodeCompanion.Agent +function Agent:add_error_to_chat(error) + self.chat:add_message({ + role = config.constants.USER_ROLE, + content = error, + }, { visible = false }) + + --- Alert the user that the error message has been shared + self.chat:add_buf_message({ + role = config.constants.USER_ROLE, + content = "Please correct for the error message I've shared", + }) + + if self.tools_config.opts and self.tools_config.opts.auto_submit_errors then + self.chat:submit() + end + + return self +end + +---Resolve a tool from the config +---@param tool table The tool from the config +---@return CodeCompanion.Agent.Tool|nil +function Agent.resolve(tool) + local callback = tool.callback + + if type(callback) == "table" then + return callback --[[@as CodeCompanion.Agent.Tool]] + end + + local ok, module = pcall(require, "codecompanion." .. callback) + if ok then + log:debug("[Tools] %s identified", callback) + return module + end + + -- Try loading the tool from the user's config + ok, module = pcall(loadfile, callback) + if not ok then + return log:error("[Tools] %s could not be resolved", callback) + end + + if module then + log:debug("[Tools] %s identified", callback) + return module() + end +end + +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 68% rename from lua/codecompanion/strategies/chat/tools/cmd_runner.lua rename to lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua index 6bbb93887..b186f697d 100644 --- a/lua/codecompanion/strategies/chat/tools/cmd_runner.lua +++ b/lua/codecompanion/strategies/chat/agents/tools/cmd_runner.lua @@ -5,26 +5,23 @@ 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, " ") + else + cmd = 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 +31,7 @@ local function to_chat(msg, tool, opts) ]], msg, - opts.cmd + cmd ) else content = string.format( @@ -46,7 +43,7 @@ local function to_chat(msg, tool, opts) ]], msg, - opts.cmd, + cmd, opts.output ) end @@ -57,7 +54,7 @@ local function to_chat(msg, tool, opts) }) end ----@class CodeCompanion.Tool +---@class CodeCompanion.Agent.Tool return { name = "cmd_runner", cmds = { @@ -161,9 +158,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 } @@ -175,49 +172,50 @@ return { table.insert(tool.cmds, entry) end end, + }, - ---Approve the command to be run - ---@param self CodeCompanion.Tools The tool object - ---@param cmd table - ---@return boolean - approved = function(self, 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 + 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 cmds = self.cmds + if vim.tbl_count(cmds) == 1 then + return string.format("Run the command `%s`?", table.concat(cmds[1].cmd, " ")) end - log:info("[Cmd Runner Tool] Approved running the command") - return true + 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, - }, - 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 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 agent CodeCompanion.Agent + ---@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 }) end, }, } 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 8543d3b2a..c8f5808c4 100644 --- a/lua/codecompanion/strategies/chat/tools/editor.lua +++ b/lua/codecompanion/strategies/chat/agents/tools/editor.lua @@ -97,14 +97,14 @@ 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.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 85% rename from lua/codecompanion/strategies/chat/tools/files.lua rename to lua/codecompanion/strategies/chat/agents/tools/files.lua index a8f20fc24..7901c850b 100644 --- a/lua/codecompanion/strategies/chat/tools/files.lua +++ b/lua/codecompanion/strategies/chat/agents/tools/files.lua @@ -157,7 +157,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,60 +384,59 @@ Remember: ) end, handlers = { - ---Approve the command to be run - ---@param self CodeCompanion.Tools 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, - on_exit = function(self) - log:debug("[Files Tool] on_exit handler executed") - file = nil - end, - }, - output = { - success = function(self, action, output) + + ---@param agent CodeCompanion.Agent The tool object + ---@param action table + ---@param output table + ---@return nil + 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: @@ -454,9 +453,13 @@ Remember: end end, - error = function(self, action, err) + ---@param agent CodeCompanion.Agent The tool object + ---@param action table + ---@param err string + ---@return nil + 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: @@ -470,8 +473,12 @@ Remember: }) end, - rejected = function(self, action) - return self.chat:add_buf_message({ + ---The action to take if the user rejects the command + ---@param agent CodeCompanion.Agent The tool object + ---@param action table + ---@return nil + 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/init.lua b/lua/codecompanion/strategies/chat/init.lua index e7e457316..c0654f1c2 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() @@ -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 @@ -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/strategies/chat/tools/init.lua b/lua/codecompanion/strategies/chat/tools/init.lua deleted file mode 100644 index 4792da49c..000000000 --- a/lua/codecompanion/strategies/chat/tools/init.lua +++ /dev/null @@ -1,636 +0,0 @@ -local Job = require("plenary.job") -local config = require("codecompanion.config") - -local TreeHandler = require("codecompanion.utils.xml.xmlhandler.tree") -local log = require("codecompanion.utils.log") -local ui = require("codecompanion.utils.ui") -local util = require("codecompanion.utils") -local xml2lua = require("codecompanion.utils.xml.xml2lua") - -local api = vim.api - -local CONSTANTS = { - PREFIX = "@", - - NS_TOOLS = "CodeCompanion-agents", - AUTOCMD_GROUP = "codecompanion.agent", - - STATUS_ERROR = "error", - STATUS_SUCCESS = "success", - - 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 -local function parse_xml(message) - log:trace("Trying to parse: %s", message) - - local handler = TreeHandler:new() - local parser = xml2lua.parser(handler) - -- parser.options.stripWS = nil - parser:parse(message) - - log:trace("Parsed xml: %s", handler.root) - - return handler.root.tools -end - ----@class CodeCompanion.Tools -local Tools = {} - ----@param args table -function Tools.new(args) - local self = setmetatable({ - aug = api.nvim_create_augroup(CONSTANTS.AUTOCMD_GROUP .. ":" .. args.bufnr, { clear = true }), - bufnr = args.bufnr, - chat = {}, - extracted = {}, - messages = args.messages, - tool = {}, - agent_config = config.strategies.chat.agents, - tools_ns = api.nvim_create_namespace(CONSTANTS.NS_TOOLS), - }, { __index = Tools }) - - return self -end - ----Set the autocmds for the tool ----@return nil -function Tools:set_autocmds() - api.nvim_create_autocmd("User", { - desc = "Handle responses from an Agent", - group = self.aug, - pattern = "CodeCompanionAgent*", - callback = function(request) - if request.data.bufnr ~= self.bufnr then - return - end - - if request.match == "CodeCompanionAgentStarted" then - log:info("[Agent] Initiated") - return ui.set_virtual_text( - self.bufnr, - self.tools_ns, - CONSTANTS.PROCESSING_MSG, - { hl_group = "CodeCompanionVirtualText" } - ) - elseif request.match == "CodeCompanionAgentFinished" then - -- Handle any errors - if request.data.status == CONSTANTS.STATUS_ERROR then - local error = request.data.stderr - log:error("Tool %s finished with error(s): %s", string.upper(self.tool.name), error) - - 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 - 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 - self.chat:submit() - end - end - end - self:reset() - end, - }) -end - ----Parse a chat buffer for tools ----@param chat CodeCompanion.Chat ----@param start_range number ----@param end_range number ----@return nil -function Tools: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] - - local llm = {} - for id, node in query:iter_captures(tree:root(), chat.bufnr, start_range - 1, end_range - 1) do - if query.captures[id] == "content" then - table.insert(llm, vim.treesitter.get_node_text(node, chat.bufnr)) - end - end - - if vim.tbl_isempty(llm) then - return - end - - -- NOTE: Only work with the last response from the LLM - local response = llm[#llm] - - local parser = vim.treesitter.get_string_parser(response, "markdown") - tree = parser:parse()[1] - - local tools = {} - for id, node in query:iter_captures(tree:root(), response, 0, -1) do -- NOTE: Keep this scoped to 0,-1 - if query.captures[id] == "tool" then - local tool = vim.treesitter.get_node_text(node, response) - tool = tool:gsub("^`+", ""):gsub("```$", "") - table.insert(tools, vim.trim(tool)) - end - end - - log:trace("[Tools] Detected: %s", tools) - - if not vim.tbl_isempty(tools) then - self.extracted = tools - vim.iter(tools):each(function(t) - return self:setup(chat, t) - end) - end -end - ----Setup 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 Tools:setup(chat, xml) - self.chat = chat - - local ok, schema = pcall(parse_xml, xml) - if not ok then - self:add_error_to_chat(string.format("The XML schema couldn't be processed:\n\n%s", schema)):reset() - return log:error("Error parsing XML schema: %s", schema) - end - - ---Resolve and run the tool - ---@param s table The tool's schema - local function run_tool(s) - ---@type CodeCompanion.Tool|nil - local resolved_tool - ok, resolved_tool = pcall(function() - return Tools.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") - log:info("XML:\n%s", xml) - log:info("Schema:\n%s", s) - return - end - - self.tool = vim.deepcopy(resolved_tool) - self.tool.request = s.tool - self:fold_xml() - self:set_autocmds() - - if self.tool.env then - local env = type(self.tool.env) == "function" and self.tool.env(s.tool) or {} - util.replace_placeholders(self.tool.cmds, env) - end - - self:run() - end - - -- 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 }) - end) - return - end - 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 ----@return table?, table? -function Tools:find(chat, message) - if not message.content then - return nil, nil - end - - local agents = {} - 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) - - for _, tool in ipairs(self.agent_config[agent].tools) do - if not vim.tbl_contains(tools, tool) then - table.insert(tools, tool) - end - end - end - end) - - -- Process tools - vim - .iter(self.agent_config.tools) - :filter(function(name) - return name ~= "opts" - end) - :each(function(tool) - if is_found(tool) and not vim.tbl_contains(tools, tool) then - table.insert(tools, tool) - end - end) - - if #tools == 0 then - return nil, nil - end - - return tools, agents -end - ----@param chat CodeCompanion.Chat ----@param message table ----@return boolean -function Tools:parse(chat, message) - local tools, agents = self:find(chat, message) - - if tools or agents 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]) - 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 - chat:add_message({ - role = config.constants.SYSTEM_ROLE, - content = self.agent_config[agent].system_prompt, - }, { tag = "tool", visible = false }) - end - end - end - return true - end - - return false -end - ----Replace the tool tag in a given message ----@param message string ----@return string -function Tools:replace(message) - for tool, _ in pairs(self.agent_config.tools) do - message = vim.trim(message:gsub(CONSTANTS.PREFIX .. tool, tool)) - end - for agent, _ in pairs(self.agent_config) do - message = vim.trim(message:gsub(CONSTANTS.PREFIX .. agent, "")) - end - - return message -end - ----Reset the tool class ----@return nil -function Tools:reset() - api.nvim_buf_clear_namespace(self.bufnr, self.tools_ns, 0, -1) - api.nvim_clear_autocmds({ group = self.aug }) - self.extracted = {} - log:info("[Agent] Completed") -end - ----Fold any XML code blocks in the buffer ----@return nil -function Tools:fold_xml() - local query = vim.treesitter.query.parse( - "markdown", - [[ -( - fenced_code_block - (info_string) @lang - (code_fence_content) @code - (#eq? @lang "xml") -) - ]] - ) - - local parser = vim.treesitter.get_parser(self.bufnr, "markdown") - local tree = parser:parse()[1] - - vim.o.foldmethod = "manual" - - for _, matches, _ in query:iter_matches(tree:root(), self.bufnr) do - local nodes = matches[2] -- The second capture is always the code block - local code_node = type(nodes) == "table" and nodes[1] or nodes - - if code_node then - local start_row, _, end_row, _ = code_node:range() - if start_row < end_row then - api.nvim_buf_call(self.bufnr, function() - vim.cmd(string.format("%d,%dfold", start_row, end_row)) - end) - end - end - end -end - ----Add an error message to the chat buffer ----@param error string ----@return CodeCompanion.Tools -function Tools:add_error_to_chat(error) - self.chat:add_message({ - role = config.constants.USER_ROLE, - content = error, - }, { visible = false }) - - --- Alert the user that the error message has been shared - self.chat:add_buf_message({ - role = config.constants.USER_ROLE, - content = "Please correct for the error message I've shared", - }) - - if self.agent_config.opts and self.agent_config.opts.auto_submit_errors then - self.chat:submit() - end - - return self -end - ----Resolve a tool from the config ----@param tool table The tool from the config ----@return CodeCompanion.Tool|nil -function Tools.resolve(tool) - local callback = tool.callback - - if type(callback) == "table" then - return callback --[[@as CodeCompanion.Tool]] - end - - local ok, module = pcall(require, "codecompanion." .. callback) - if ok then - log:debug("[Tools] %s identified", callback) - return module - end - - -- Try loading the tool from the user's config - ok, module = pcall(loadfile, callback) - if not ok then - return log:error("[Tools] %s could not be resolved", callback) - end - - if module then - log:debug("[Tools] %s identified", callback) - return module() - end -end - -return Tools diff --git a/lua/codecompanion/strategies/chat/tools/rag.lua b/lua/codecompanion/strategies/chat/tools/rag.lua deleted file mode 100644 index f7344614a..000000000 --- a/lua/codecompanion/strategies/chat/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, - }, -} diff --git a/lua/codecompanion/types.lua b/lua/codecompanion/types.lua index 0cb8e1ef0..073c1fdb2 100644 --- a/lua/codecompanion/types.lua +++ b/lua/codecompanion/types.lua @@ -104,33 +104,23 @@ ---@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 ---@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.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 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 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 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.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 ----@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 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/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 54ebf5e70..92f8d0bf9 100644 --- a/tests/config.lua +++ b/tests/config.lua @@ -50,24 +50,83 @@ return { llm = "assistant", user = "foo", }, - agents = { - tools = { - ["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", - }, + 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 = { - system_prompt = [[My tool system prompt]], + 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", + }, + }, + }, + opts = { + system_prompt = [[My tool system prompt]], + }, }, variables = { ["buffer"] = { diff --git a/tests/helpers.lua b/tests/helpers.lua index ccbd2f535..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 @@ -48,7 +84,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 +117,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 @@ -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/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_cmd.lua b/tests/strategies/chat/agents/executor/test_cmd.lua new file mode 100644 index 000000000..b718a8381 --- /dev/null +++ b/tests/strategies/chat/agents/executor/test_cmd.lua @@ -0,0 +1,99 @@ +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 the environment in the child process + child.lua([[ + h = require('tests.helpers') + chat, agent = h.setup_chat_buffer() + + -- Reset test globals + _G._test_setup = nil + _G._test_exit = nil + _G._test_order = nil + _G._test_output = nil + ]]) + 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"]["handlers and outputs are called"] = function() + child.lua([[ + local cmd_xml = require("tests.strategies.chat.agents.tools.stubs.xml.cmd_xml") + local xml = cmd_xml.load() + agent:execute(chat, xml) + vim.wait(100) + ]]) + + -- 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(100) + ]], + tool + )) + + -- 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 + +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() + agent:execute(chat, xml) + vim.wait(100) + ]]) + + h.eq({ testing = true }, child.lua_get("agent.chat.tool_flags")) +end + +T["Agent"]["cmds"]["can run multiple commands"] = function() + child.lua([[ + local cmd_xml = require("tests.strategies.chat.agents.tools.stubs.xml.cmd_xml") + local xml = cmd_xml.load("cmd_consecutive") + agent:execute(chat, xml) + vim.wait(100) + ]]) + + -- 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" } }, 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 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..73bcfb4e9 --- /dev/null +++ b/tests/strategies/chat/agents/executor/test_func.lua @@ -0,0 +1,169 @@ +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"]["functions"] = new_set() + +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) + ]]) + + -- 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 + +T["Agent"]["functions"]["calls output.success"] = function() + h.eq(vim.NIL, child.lua_get([[_G._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.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 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.NIL, child.lua_get([[_G._test_func]])) + + local tool = "'func_consecutive'" + child.lua(string.format( + [[ + --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 + )) + + 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 + +T["Agent"]["functions"]["can run consecutively"] = function() + 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 + )) + + 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 + +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) + ]], + 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 + +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) + ]], + tool + )) + + -- Test that stderr is updated on the agent, only once + h.eq({ "Something went wrong" }, child.lua_get([[agent.stderr]])) +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) + ]]) + + h.eq( + { { data = "Data 1", status = "success" }, { data = "Data 2", status = "success" } }, + child.lua_get([[agent.stdout]]) + ) +end + +return T diff --git a/tests/strategies/chat/agents/executor/test_queue.lua b/tests/strategies/chat/agents/executor/test_queue.lua new file mode 100644 index 000000000..4673e10e5 --- /dev/null +++ b/tests/strategies/chat/agents/executor/test_queue.lua @@ -0,0 +1,55 @@ +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([[ + --require("tests.log") + 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"]["queue"] = new_set() + +T["Agent"]["queue"]["can queue functions and commands"] = function() + h.eq(vim.NIL, child.lua_get([[_G._test_order]])) + + child.lua([[ + local queue = require("tests.strategies.chat.agents.tools.stubs.xml.queue_xml") + local xml = queue.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/executor/test_user_approval.lua b/tests/strategies/chat/agents/executor/test_user_approval.lua new file mode 100644 index 000000000..8dc2bec50 --- /dev/null +++ b/tests/strategies/chat/agents/executor/test_user_approval.lua @@ -0,0 +1,48 @@ +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 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/test_tools.lua b/tests/strategies/chat/agents/test_agents.lua similarity index 62% rename from tests/strategies/chat/test_tools.lua rename to tests/strategies/chat/agents/test_agents.lua index 1ed9b5be1..49cbc34ee 100644 --- a/tests/strategies/chat/test_tools.lua +++ b/tests/strategies/chat/agents/test_agents.lua @@ -3,24 +3,27 @@ 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_once = function() + 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["Tools"]["resolve"] = new_set() +T["Agent"]["resolve"] = new_set() -T["Tools"]["resolve"]["can resolve built-in tools"] = function() - local tool = tools.resolve({ - callback = "strategies.chat.tools.editor", +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", }) @@ -29,8 +32,8 @@ T["Tools"]["resolve"]["can resolve built-in tools"] = function() h.eq(6, #tool.schema) end -T["Tools"]["resolve"]["can resolve user's tools"] = function() - local tool = tools.resolve({ +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", }) @@ -40,21 +43,21 @@ T["Tools"]["resolve"]["can resolve user's tools"] = function() 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", }) - tools:parse(chat, chat.messages[#chat.messages]) + 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["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", @@ -72,14 +75,14 @@ T["Tools"][":parse"]["a response from the LLM"] = function() ``` ]], }) - chat.tools.chat = chat - chat.tools:parse_buffer(chat, 5, 100) + 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["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", @@ -100,18 +103,18 @@ T["Tools"][":parse"]["a nested response from the LLM"] = function() ``` ]], }) - chat.tools.chat = chat - chat.tools:parse_buffer(chat, 5, 100) + 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["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") + local result = agent:replace(message, "foo") h.eq("run the foo tool", result) end 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..1fe6cf1aa --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/cmd.lua @@ -0,0 +1,29 @@ +return { + name = "cmd", + system_prompt = function(schema) + return "my cmd system prompt" + end, + cmds = { + { "echo", "Hello World" }, + }, + 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 only be called once + success = function(self, cmd, output) + _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_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/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_queue.lua b/tests/strategies/chat/agents/tools/stubs/cmd_queue.lua new file mode 100644 index 000000000..aaf2e9200 --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/cmd_queue.lua @@ -0,0 +1,28 @@ +return { + name = "cmd_queue", + 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 {} + end, + }, +} diff --git a/tests/strategies/chat/agents/tools/stubs/func.lua b/tests/strategies/chat/agents/tools/stubs/func.lua new file mode 100644 index 000000000..873fdfad2 --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/func.lua @@ -0,0 +1,37 @@ +return { + name = "func", + 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 "") .. "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 new file mode 100644 index 000000000..aa98a942c --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/func_consecutive.lua @@ -0,0 +1,41 @@ +local log = require("codecompanion.utils.log") +return { + name = "func_consecutive", + system_prompt = function(schema) + return "my func system prompt" + end, + 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 + 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 new file mode 100644 index 000000000..37f787799 --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/func_error.lua @@ -0,0 +1,34 @@ +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, + }, + 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, + }, +} diff --git a/tests/strategies/chat/agents/tools/stubs/func_queue.lua b/tests/strategies/chat/agents/tools/stubs/func_queue.lua new file mode 100644 index 000000000..72ee6de6c --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/func_queue.lua @@ -0,0 +1,37 @@ +return { + name = "func_queue", + 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_queue_2.lua b/tests/strategies/chat/agents/tools/stubs/func_queue_2.lua new file mode 100644 index 000000000..6a12aa0a5 --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/func_queue_2.lua @@ -0,0 +1,37 @@ +return { + name = "func_queue_2", + 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/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..7655b445e --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/xml/cmd_xml.lua @@ -0,0 +1,37 @@ +local M = {} + +function M.load(name) + name = name or "cmd" + return string.format( + [[ + +]], + 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 [[ + + + echo Hello World + testing + + +]] +end + +return M diff --git a/tests/strategies/chat/agents/tools/stubs/xml/editor_xml.lua b/tests/strategies/chat/agents/tools/stubs/xml/editor_xml.lua new file mode 100644 index 000000000..c08c4f464 --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/xml/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/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/stubs/xml/queue_xml.lua b/tests/strategies/chat/agents/tools/stubs/xml/queue_xml.lua new file mode 100644 index 000000000..31f39b132 --- /dev/null +++ b/tests/strategies/chat/agents/tools/stubs/xml/queue_xml.lua @@ -0,0 +1,18 @@ +local M = {} + +function M.run(name) + return string.format( + [[ + + Data 1 + + + + Data 2 + +]], + name + ) +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..d7ff42fba --- /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.xml.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.xml.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.xml.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 diff --git a/tests/strategies/chat/tools/test_files.lua b/tests/strategies/chat/agents/tools/test_files.lua similarity index 97% rename from tests/strategies/chat/tools/test_files.lua rename to tests/strategies/chat/agents/tools/test_files.lua index 749e65b3c..a8ce7e4c2 100644 --- a/tests/strategies/chat/tools/test_files.lua +++ b/tests/strategies/chat/agents/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/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)]) diff --git a/tests/stubs/queue.txt b/tests/stubs/queue.txt new file mode 100644 index 000000000..e60548ae6 --- /dev/null +++ b/tests/stubs/queue.txt @@ -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 = + } + } +}