304 Commits

Author SHA1 Message Date
Dark-Alex-17 fa85fcfcce Sisyphus agent recreated in LangChain to figure out how it works and how to use it 2026-04-15 12:47:38 -06:00
Dark-Alex-17 c85adfd00e Merge branch 'tree-sitter-tools' into 'develop' 2026-04-09 14:48:22 -06:00
Dark-Alex-17 5b1ddf1848 feat: Automatic runtime customization using shebangs 2026-04-09 14:16:02 -06:00
Dark-Alex-17 473ec251e0 test: Updated client stream tests to use the thread_rng from rand 2026-04-09 13:53:52 -06:00
Dark-Alex-17 402c5a1ec7 build: Pulled additional features for rand dependency 2026-04-09 13:45:08 -06:00
Dark-Alex-17 4f5ead8545 fix: TypeScript function args were being passed as objects rather than direct parameters 2026-04-09 13:32:16 -06:00
Dark-Alex-17 36cced560a build: upgraded dependencies to latest 2026-04-09 13:28:19 -06:00
Dark-Alex-17 0d6efbf1f3 docs: Updated docs to talk about the new TypeScript-based tool support 2026-04-09 13:19:15 -06:00
Dark-Alex-17 bbfb489a67 feat: Created a demo TypeScript tool and a get_current_weather function in TypeScript 2026-04-09 13:18:41 -06:00
Dark-Alex-17 0f7548685c feat: Updated the Python demo tool to show all possible parameter types and variations 2026-04-09 13:18:18 -06:00
Dark-Alex-17 fab266f7b9 fix: Added in forgotten wrapper scripts for TypeScript tools 2026-04-09 13:17:53 -06:00
Dark-Alex-17 48bb2fce87 feat: Added TypeScript tool support using the refactored common ScriptedLanguage trait 2026-04-09 13:17:28 -06:00
Dark-Alex-17 ad2ab6ed49 refactor: Extracted common Python parser logic into a common.rs module 2026-04-09 13:16:35 -06:00
Dark-Alex-17 bb2cad0526 refactor: python tools now use tree-sitter queries instead of AST 2026-04-09 10:20:49 -06:00
Dark-Alex-17 0db5f634a4 fix: don't shadow variables in binary path handling for Windows 2026-04-09 07:53:18 -06:00
Dark-Alex-17 dbda5abdab build: Upgraded crossterm and reedline dependencies 2026-04-08 14:54:53 -06:00
Dark-Alex-17 3a040ae3bb fix: Tool call improvements for Windows systems 2026-04-08 12:49:43 -06:00
github-actions[bot] deb25f639f chore: bump Cargo.toml to 0.3.0 2026-04-02 20:17:47 +00:00
github-actions[bot] 10c38fa612 bump: version 0.2.0 → 0.3.0 [skip ci] 2026-04-02 20:17:45 +00:00
Dark-Alex-17 3a734e27dc feat: Added todo__clear function to the todo system and updated REPL commands to have a .clear todo as well for significant changes in agent direction 2026-04-02 13:13:44 -06:00
Dark-Alex-17 41200a71f6 fix: Clarified user text input interaction 2026-03-30 16:27:22 -06:00
Dark-Alex-17 b19655087e fix: recursion bug with similarly named Bash search functions in the explore agent 2026-03-30 13:32:13 -06:00
Dark-Alex-17 c13cb18d93 feat: Added available tools to prompts for sisyphus and code-reviewer agent families 2026-03-30 13:13:30 -06:00
Dark-Alex-17 0925acf86a feat: Added available tools to coder prompt 2026-03-30 11:11:43 -06:00
Dark-Alex-17 f8cbb1549e Merge branch 'main' of github.com:Dark-Alex-17/loki 2026-03-30 10:15:51 -06:00
Dark-Alex-17 a46f6da0d8 fix: updated the error for unauthenticated oauth to include the REPL .authenticated command 2026-03-28 11:57:01 -06:00
Dark-Alex-17 7c9fb8eb71 feat: Improved token efficiency when delegating from sisyphus -> coder 2026-03-18 15:07:29 -06:00
Dark-Alex-17 223e7ca4c5 build: Removed deprecated agent functions from the .shared/utils.sh script 2026-03-18 15:04:14 -06:00
Dark-Alex-17 0fdb1bbc42 fix: Corrected a bug in the coder agent that wasn't outputting a summary of the changes made, so the parent Sisyphus agent has no idea if the agent worked or not 2026-03-17 14:57:07 -06:00
Dark-Alex-17 16cdf47101 feat: modified sisyphus agents to use the new ddg-search MCP server for web searches instead of built-in model searches 2026-03-17 14:55:33 -06:00
Dark-Alex-17 6555ecfafc fix: Claude code system prompt injected into claude requests to make them valid once again 2026-03-17 10:44:50 -06:00
Dark-Alex-17 a586ca40e2 fix: Do not inject tools when models don't support them; detect this conflict before API calls happen 2026-03-17 09:35:51 -06:00
Dark-Alex-17 435667fac8 style: Applied formatting across new inquire files 2026-03-16 12:39:20 -06:00
Dark-Alex-17 fd3385bad8 feat: Added support for specifying a custom response to multiple-choice prompts when nothing suits the user's needs 2026-03-16 12:37:47 -06:00
Dark-Alex-17 16adae7bc3 feat: Supported theming in the inquire prompts in the REPL 2026-03-16 12:36:20 -06:00
Dark-Alex-17 639f6e2a1a build: upgraded to the most recent version of the inquire crate 2026-03-16 12:31:28 -06:00
Dark-Alex-17 cc4d2f6256 docs: Fixed a spacing issue in the example agent configuration 2026-03-13 14:19:39 -06:00
Dark-Alex-17 d8d757b060 docs: Added the file-reviewer agent to the AGENTS docs 2026-03-13 14:07:13 -06:00
Dark-Alex-17 32a9861369 docs: Updated the MCP-SERVERS docs to mention the ddg-search MCP server 2026-03-13 13:32:58 -06:00
Dark-Alex-17 922fa05b06 feat: Added the duckduckgo-search MCP server for searching the web (in addition to the built-in tools for web searches) 2026-03-13 13:29:56 -06:00
Dark-Alex-17 4cc6bccf87 Merge branch 'main' of github.com:Dark-Alex-17/loki 2026-03-12 15:17:54 -06:00
Dark-Alex-17 de3012e664 fix: Implemented the path normalization fix for the oracle and explore agents 2026-03-12 13:38:15 -06:00
Dark-Alex-17 3daac0b1cf chore: Added GPT-5.2 to models.yaml 2026-03-12 13:30:23 -06:00
Dark-Alex-17 4260088ed1 docs: Updated the docs to now explicitly mention Gemini OAuth support 2026-03-12 13:30:10 -06:00
Dark-Alex-17 cb24c7ac91 feat: Support for Gemini OAuth 2026-03-12 13:29:47 -06:00
Dark-Alex-17 5fcba4c5ab refactor: Made the oauth module more generic so it can support loopback OAuth (not just manual) 2026-03-12 13:28:09 -06:00
Dark-Alex-17 024dd5ff59 fix: Updated the atlassian MCP server endpoint to account for future deprecation 2026-03-12 12:49:26 -06:00
Dark-Alex-17 0e931a472e fix: Fixed a bug in the coder agent that was causing the agent to create absolute paths from the current directory 2026-03-12 12:39:49 -06:00
Dark-Alex-17 3ff6e3cca9 fix: The REPL .authenticate command works from within sessions, agents, and roles with pre-configured models 2026-03-12 09:08:17 -06:00
Dark-Alex-17 2e30b19479 feat: Support authenticating or refreshing OAuth for supported clients from within the REPL 2026-03-11 13:07:27 -06:00
Dark-Alex-17 208ed838e6 fix: the updated regex for secrets injection broke MCP server secrets interpolation because the regex greedily matched on new lines, replacing too much content. This fix just ignores commented out lines in YAML files by skipping commented out lines. 2026-03-11 12:55:28 -06:00
Dark-Alex-17 8380ae5d7a feat: Allow first-runs to select OAuth for supported providers 2026-03-11 12:01:17 -06:00
Dark-Alex-17 97b902441e fix: Don't try to inject secrets into commented-out lines in the config 2026-03-11 11:11:09 -06:00
Dark-Alex-17 15df5be307 feat: Support OAuth authentication flows for Claude 2026-03-11 11:10:48 -06:00
Dark-Alex-17 658f8f32dd chore: Added support for Claude 4.6 gen models 2026-03-10 14:55:30 -06:00
Dark-Alex-17 f3ee71d3f2 fix: Removed top_p parameter from some agents so they can work across model providers 2026-03-10 10:18:38 -06:00
Dark-Alex-17 719b482be9 Merge branch 'main' of github.com:Dark-Alex-17/loki 2026-03-09 14:58:23 -06:00
Dark-Alex-17 f7b589ac2b chore: Added the new gemini-3.1-pro-preview model to gemini and vertex models 2026-03-09 14:57:39 -06:00
Dark-Alex-17 cea08d804e docs: created an authorship policy and PR template that requires disclosure of AI assistance in contributions 2026-02-24 17:46:07 -07:00
Dark-Alex-17 53cc3a27fe style: Applied formatting to MCP module 2026-02-20 15:28:21 -07:00
Dark-Alex-17 0f3cf511e0 docs: Updated sisyphus README to always include the execute_command.sh tool 2026-02-20 15:06:57 -07:00
Dark-Alex-17 f8b965d801 docs: Updated the sisyphus system docs to have a pro-tip of configuring an IDE MCP server to improve performance 2026-02-20 15:01:08 -07:00
Dark-Alex-17 2cb68846b6 docs: Created README docs for the CodeRabbit-style Code reviewer agents 2026-02-20 15:00:32 -07:00
Dark-Alex-17 1e18c7a7e2 feat: Improved MCP server spinup and spindown when switching contexts or settings in the REPL: Modify existing config rather than stopping all servers always and re-initializing if unnecessary 2026-02-20 14:36:34 -07:00
Dark-Alex-17 caeed16d36 fix: Improved sub-agent stdout and stderr output for users to follow 2026-02-20 13:47:28 -07:00
Dark-Alex-17 7ab36dce90 Update models.yaml with latest OpenRouter data 2026-02-20 12:08:00 -07:00
Dark-Alex-17 f8a72f819e Add script to update models.yaml from OpenRouter 2026-02-20 12:07:59 -07:00
Dark-Alex-17 306a880257 fix: Inject agent variables into environment variables for global tool calls when invoked from agents to modify global tool behavior 2026-02-20 11:38:24 -07:00
Dark-Alex-17 59cca849a8 feat: Allow the explore agent to run search queries for understanding docs or API specs 2026-02-19 14:29:02 -07:00
Dark-Alex-17 42a1665960 feat: Allow the oracle to perform web searches for deeper research 2026-02-19 14:26:07 -07:00
Dark-Alex-17 687a4ea3bc fix: Removed the unnecessary execute_commands tool from the oracle agent 2026-02-19 14:18:16 -07:00
Dark-Alex-17 77c1c2aa6f fix: Added auto_confirm to the coder agent so sub-agent spawning doesn't freeze 2026-02-19 14:15:42 -07:00
Dark-Alex-17 3d0bbd59d1 feat: Added web search support to the main sisyphus agent to answer user queries 2026-02-19 12:29:07 -07:00
Dark-Alex-17 39f1511fea refactor: Changed the default session name for Sisyphus to temp (to require users to explicitly name sessions they wish to save) 2026-02-19 10:26:52 -07:00
Dark-Alex-17 b5b3dc5ba8 fix: Fixed a bug in the new supervisor and todo built-ins that was causing errors with OpenAI models 2026-02-18 14:52:57 -07:00
Dark-Alex-17 ad8be61a3b fix: Added condition to sisyphus to always output a summary to clearly indicate completion 2026-02-18 13:57:51 -07:00
Dark-Alex-17 4f4db10c8d fix: Updated the sisyphus prompt to explicitly tell it to delegate to the coder agent when it wants to write any code at all except for trivial changes 2026-02-18 13:51:43 -07:00
Dark-Alex-17 a430d59e9c fix: Added back in the auto_confirm variable into sisyphus 2026-02-18 13:42:39 -07:00
Dark-Alex-17 5f1734d69a fix: Removed the now unnecessary is_stale_response that was breaking auto-continuing with parallel agents 2026-02-18 13:36:25 -07:00
Dark-Alex-17 a34adc5adf style: Applied formatting to the function module 2026-02-18 13:20:18 -07:00
Dark-Alex-17 96c1d47d7f build: Upgraded to the most recent version of rmcp 2026-02-18 12:28:52 -07:00
Dark-Alex-17 7701a02b16 refactor: Updated the sisyphus agent to use the built-in user interaction tools instead of custom bash-based tools 2026-02-18 12:17:35 -07:00
Dark-Alex-17 ece9cadad5 feat: Created a CodeRabbit-style code-reviewer agent 2026-02-18 12:16:59 -07:00
Dark-Alex-17 738f39917d docs: Updated the docs to include details on the new agent spawning system and built-in user interaction tools 2026-02-18 12:16:29 -07:00
Dark-Alex-17 b95649177d fix: Bypassed enabled_tools for user interaction tools so if function calling is enabled at all, the LLM has access to the user interaction tools when in REPL mode 2026-02-18 11:25:25 -07:00
Dark-Alex-17 6f977307e6 feat: Added configuration option in agents to indicate the timeout for user input before proceeding (defaults to 5 minutes) 2026-02-18 11:24:47 -07:00
Dark-Alex-17 9a715b2fb2 feat: Added support for sub-agents to escalate user interaction requests from any depth to the parent agents for user interactions 2026-02-18 11:06:15 -07:00
Dark-Alex-17 930861d49b feat: built-in user interaction tools to remove the need for the list/confirm/etc prompts in prompt tools and to enhance user interactions in Loki 2026-02-18 11:05:43 -07:00
Dark-Alex-17 31987c9f94 fix: When parallel agents run, only write to stdout from the parent and only display the parent's throbber 2026-02-18 09:59:24 -07:00
Dark-Alex-17 fe7401c935 refactor: Cleaned up some left-over implementation stubs 2026-02-18 09:13:39 -07:00
Dark-Alex-17 543e62fe7d fix: Forgot to implement support for failing a task and keep all dependents blocked 2026-02-18 09:13:11 -07:00
Dark-Alex-17 0ec8cd4d00 fix: Clean up orphaned sub-agents when the parent agent 2026-02-18 09:12:32 -07:00
Dark-Alex-17 a469a6cf06 fix: Fixed the bash prompt utils so that they correctly show output when being run by a tool invocation 2026-02-17 17:19:42 -07:00
Dark-Alex-17 0e67e0f85a feat: Experimental update to sisyphus to use the new parallel agent spawning system 2026-02-17 16:33:08 -07:00
Dark-Alex-17 bf862d8021 fix: Forgot to automatically add the bidirectional communication back up to parent agents from sub-agents (i.e. need to be able to check inbox and send messages) 2026-02-17 16:11:35 -07:00
Dark-Alex-17 c070d151fa feat: Added an agent configuration property that allows auto-injecting sub-agent spawning instructions (when using the built-in sub-agent spawning system) 2026-02-17 15:49:40 -07:00
Dark-Alex-17 3147ad59f3 feat: Auto-dispatch support of sub-agents and support for the teammate pattern between subagents 2026-02-17 15:18:27 -07:00
Dark-Alex-17 f9d2adf33a docs: Initial documentation cleanup of parallel agent MVP 2026-02-17 14:30:28 -07:00
Dark-Alex-17 eaa224aeb9 fix: Agent delegation tools were not being passed into the {{__tools__}} placeholder so agents weren't delegating to subagents 2026-02-17 14:19:22 -07:00
Dark-Alex-17 81a81d035e feat: Full passive task queue integration for parallelization of subagents 2026-02-17 13:42:53 -07:00
Dark-Alex-17 5c5d70e4d0 feat: Implemented initial scaffolding for built-in sub-agent spawning tool call operations 2026-02-17 11:48:31 -07:00
Dark-Alex-17 ad563d4263 feat: Initial models for agent parallelization 2026-02-17 11:27:55 -07:00
Dark-Alex-17 016501ef4f docs: Fixed typos in the Sisyphus documentation 2026-02-16 14:05:51 -07:00
Dark-Alex-17 0b36d17ea0 feat: Added interactive prompting between the LLM and the user in Sisyphus using the built-in Bash utils scripts 2026-02-16 13:57:04 -07:00
github-actions[bot] faf92f9fe8 chore: bump Cargo.toml to 0.2.0 2026-02-14 01:41:41 +00:00
github-actions[bot] 2c7abace37 bump: version 0.1.3 → 0.2.0 [skip ci] 2026-02-14 01:41:29 +00:00
Dark-Alex-17 3a7128f3de feat: Simplified sisyphus prompt to improve functionality 2026-02-13 18:36:10 -07:00
Dark-Alex-17 0b8bae64d1 feat: Supported the injection of RAG sources into the prompt, not just via the .sources rag command in the REPL so models can directly reference the documents that supported their responses 2026-02-13 17:45:56 -07:00
Dark-Alex-17 9c4543ceb5 docs: updated the tools documentation to mention the new fs_read, fs_grep, and fs_glob tools 2026-02-13 16:53:00 -07:00
Dark-Alex-17 0b7bb7a816 docs: updated the default configuration example to have the new fs_read, fs_glob, fs_grep global functions 2026-02-13 16:23:49 -07:00
Dark-Alex-17 ee496e5792 docs: Updated the docs to mention the new agents 2026-02-13 15:42:28 -07:00
Dark-Alex-17 05cb8548cf feat: Created the Sisyphus agent to make Loki function like Claude Code, Gemini, Codex, etc. 2026-02-13 15:42:10 -07:00
Dark-Alex-17 57d62087f5 feat: Created the Oracle agent to handle high-level architectural decisions and design questions about a given codebase 2026-02-13 15:41:44 -07:00
Dark-Alex-17 a1f8250f58 feat: Updated the coder agent to be much more task-focused and to be delegated to by Sisyphus 2026-02-13 15:41:11 -07:00
Dark-Alex-17 194849eaab feat: Created the explore agent for exploring codebases to help answer questions 2026-02-13 15:40:46 -07:00
Dark-Alex-17 f8330523db docs: Updated todo-system docs 2026-02-13 15:13:37 -07:00
Dark-Alex-17 2a7af1531d feat: Use the official atlassian MCP server for the jira-helper agent 2026-02-13 14:56:42 -07:00
Dark-Alex-17 8f858a3d3c feat: Created fs_glob to enable more targeted file exploration utilities 2026-02-13 13:31:50 -07:00
Dark-Alex-17 51211ab1a6 feat: Created a new tool 'fs_grep' to search a given file's contents for relevant lines to reduce token usage for smaller models 2026-02-13 13:31:20 -07:00
Dark-Alex-17 4dad7d6c78 feat: Created the new fs_read tool to enable controlled reading of a file 2026-02-13 13:30:53 -07:00
Dark-Alex-17 1fa9886e7a feat: Let agent level variables be defined to bypass guard protections for tool invocations 2026-02-09 16:45:11 -07:00
Dark-Alex-17 2370525f38 fix: Improved continuation prompt to not make broad todo-items 2026-02-09 15:36:57 -07:00
Dark-Alex-17 3a131f19ee fix: Allow auto-continuation to work in agents after a session is compressed and if there's still unfinish items in the to-do list 2026-02-09 15:21:39 -07:00
Dark-Alex-17 f59286e7a7 fix: fs_ls and fs_cat outputs should always redirect to "$LLM_OUTPUT" including on errors. 2026-02-09 14:56:55 -07:00
Dark-Alex-17 79b0d044a8 feat: Implemented a built-in task management system to help smaller LLMs complete larger multistep tasks and minimize context drift 2026-02-09 12:49:06 -07:00
Dark-Alex-17 423921276d feat: Improved tool and MCP invocation error handling by returning stderr to the model when it is available 2026-02-04 12:00:21 -07:00
Dark-Alex-17 b0799e7fc6 feat: Added variable interpolation for conversation starters in agents 2026-02-04 10:51:59 -07:00
Dark-Alex-17 1b4adec4c3 build: Upgraded to the most recent version of gman to fix vault vulnerabilities 2026-02-03 09:24:53 -07:00
Dark-Alex-17 de0d8114b3 feat: Implemented retry logic for failed tool invocations so the LLM can learn from the result and try again; Also implemented chain loop detection to prevent loops 2026-02-01 17:06:16 -07:00
Dark-Alex-17 113db42ff5 fix: Claude tool calls work incorrectly when tool doesn't require any arguments or flags; would provide an empty JSON object or error on no args 2026-02-01 17:05:36 -07:00
Dark-Alex-17 3715725cbb feat: Added gemini-3-pro to the supported vertexai models 2026-01-30 19:03:41 -07:00
Dark-Alex-17 f08d91936b Fixed some typos in tool call error messages 2026-01-30 12:25:57 -07:00
Dark-Alex-17 76c2dde2aa build: Created justfile to make life easier 2026-01-27 13:49:36 -07:00
Dark-Alex-17 e9a53afc88 docs: Created a CREDITS file to document the history and origins of Loki from the original AIChat project 2026-01-27 13:15:20 -07:00
Dark-Alex-17 2d4b576977 build: Support Claude Opus 4.5 2026-01-26 12:40:06 -07:00
Dark-Alex-17 9d70569878 feat: Added an environment variable that lets users bypass guard operations in bash scripts. This is useful for agent routing 2026-01-23 14:18:52 -07:00
Dark-Alex-17 e08220f059 fix: Fixed a bug where --agent-variable values were not being passed to the agents 2026-01-23 14:15:59 -07:00
Dark-Alex-17 a9179a53cc feat: Added support for thought-signatures for Gemini 3+ models 2026-01-21 15:11:55 -07:00
Dark-Alex-17 d1c3adc565 style: Cleaned up an anyhow error 2025-12-16 14:51:35 -07:00
github-actions[bot] 10d49c86c4 bump: version 0.1.2 → 0.1.3 [skip ci] 2025-12-13 20:57:37 +00:00
Dark-Alex-17 562caeaa16 ci: Prep for 0.1.3 release 2025-12-13 13:38:09 -07:00
Dark-Alex-17 1ea5003c0c style: Improved error message for un-fully configured MCP configuration 2025-12-13 13:37:01 -07:00
github-actions[bot] 7175f86906 chore: bump Cargo.toml to 0.1.3 2025-12-13 20:28:10 +00:00
github-actions[bot] 2541f574f6 bump: version 0.1.2 → 0.1.3 [skip ci] 2025-12-13 20:27:58 +00:00
Dark-Alex-17 b3c327914a chore: Updated the models 2025-12-11 09:05:41 -07:00
Dark-Alex-17 4bc7661efa docs: Removed the warning about MCP token usage since that has been fixed 2025-12-05 12:38:15 -07:00
Dark-Alex-17 f075a6f0a3 docs: Fixed an unclosed backtick typo in the Environment Variables docs 2025-12-05 12:37:59 -07:00
Dark-Alex-17 c2e8e85b32 docs: Fixed typo in vault readme 2025-12-05 11:05:14 -07:00
Dark-Alex-17 aff14c9b88 style: Applied formatting 2025-12-03 15:06:50 -07:00
Dark-Alex-17 72e99734e6 Merge branch 'main' of github.com:Dark-Alex-17/loki 2025-12-03 14:57:03 -07:00
Dark-Alex-17 1af148e767 feat: Improved MCP implementation to minimize the tokens needed to utilize it so it doesn't quickly overwhelm the token space for a given model 2025-12-03 12:12:51 -07:00
Alex Clarke da3c766cfa ci: Updated the README to be a bit more clear in some sections 2025-11-26 15:53:54 -07:00
github-actions[bot] 181acf61d2 bump: version 0.1.1 → 0.1.2 [skip ci] 2025-11-08 23:13:34 +00:00
Dark-Alex-17 ff472c61d9 refactor: Gave the GitHub MCP server a default placeholder value that doesn't require the vault 2025-11-08 16:09:32 -07:00
github-actions[bot] 83c13f32e8 bump: version 0.1.1 → 0.1.2 [skip ci] 2025-11-08 23:02:40 +00:00
Dark-Alex-17 108203f763 bug: Removed the github MCP server and slack MCP server from mcp.json so users can just use Loki without any other setup and add more later 2025-11-08 15:59:05 -07:00
Alex Clarke 970705377a build: Removed the remaining IDE metadata directories 2025-11-07 18:21:58 -07:00
Dark-Alex-17 35018c1462 build: Added forgotten IDE configuration directories into my .gitignore 2025-11-07 18:18:32 -07:00
github-actions[bot] 0f345a5042 bump: version 0.1.0 → 0.1.1 [skip ci] 2025-11-08 00:22:06 +00:00
Dark-Alex-17 68ec599793 docs: Fixed a typo in the CI badge path 2025-11-07 17:17:57 -07:00
Dark-Alex-17 a9d5f8a4d7 docs: Fixed some confusing wording in the global configuration example file 2025-11-07 16:57:49 -07:00
github-actions[bot] c9bc9952df bump: version 0.0.1 → 0.1.0 [skip ci] 2025-11-07 23:47:37 +00:00
Dark-Alex-17 80ae76b6ec ci: Final release checks before open sourcing the repo 2025-11-07 16:43:50 -07:00
Dark-Alex-17 57da3f43e8 Merge remote-tracking branch 'origin/main' 2025-11-07 16:24:47 -07:00
Dark-Alex-17 365d4510a5 docs: Fixed a typo in the Vault documentation 2025-11-07 16:24:42 -07:00
github-actions[bot] 565b37c14a bump: version 0.0.1 → 0.1.0 [skip ci] 2025-11-07 23:19:04 +00:00
Dark-Alex-17 190c15d214 ci: Prepare for release 2025-11-07 16:18:16 -07:00
Dark-Alex-17 faf8fdb213 bump: version 0.0.1 → 0.1.0 2025-11-07 16:11:14 -07:00
Dark-Alex-17 fdc38a0b18 refactor: Updated to the most recent Rust version with 2024 syntax 2025-11-07 15:50:55 -07:00
github-actions[bot] c7d72ac22d bump: version 0.1.0 → 0.2.0 [skip ci] 2025-11-07 22:04:11 +00:00
Dark-Alex-17 c1a4d021a1 ci: Bumped the patch version 2025-11-07 15:03:31 -07:00
Dark-Alex-17 9cb4e4d1bc build: bumped the crate version 2025-11-07 14:59:41 -07:00
Dark-Alex-17 62ce4f34f8 docs: Added badges for Loki 2025-11-07 14:24:25 -07:00
Dark-Alex-17 b914e90da5 ci: Fixed typo in commit message for homebrew tap 2025-11-07 14:24:13 -07:00
Dark-Alex-17 fdc312306d build: Renamed the crate to loki-ai since loki is taken 2025-11-07 14:16:02 -07:00
Dark-Alex-17 9852245469 ci: Created the homebrew installation steps 2025-11-07 13:53:28 -07:00
Dark-Alex-17 2cb099e378 ci: Created the release pipeline 2025-11-07 13:51:53 -07:00
Dark-Alex-17 fba8e26f5b docs: Updated the README to credit the AIChat team and to offer quick links to get around the docs 2025-11-07 13:49:26 -07:00
Dark-Alex-17 60dc712bdd docs: Wrote migration documentation for users coming from AIChat 2025-11-07 13:49:02 -07:00
Dark-Alex-17 ba6d8002e1 docs: Added a simple gif to show what the models table looks like for tab completions 2025-11-07 13:48:48 -07:00
Dark-Alex-17 c9781d0062 docs: Replaced the copy gif with one that better shows that the content is copied to your clipboard 2025-11-07 13:48:30 -07:00
Dark-Alex-17 48a9f84d6c docs: Updated the continue gif to use a prompt that makes more sense 2025-11-07 13:48:09 -07:00
Dark-Alex-17 b874be4b36 docs: Updated the set gif to show the up-to-date settings names 2025-11-07 13:47:57 -07:00
Dark-Alex-17 3d34d6e273 docs: Updated the regenerate gif to use the up-to-date settings names 2025-11-07 13:47:41 -07:00
Dark-Alex-17 040ce15b55 docs: Created docs for the REPL 2025-11-07 13:47:20 -07:00
Dark-Alex-17 411812875a docs: Documented all available environment variables 2025-11-07 13:47:10 -07:00
Dark-Alex-17 06bacd47ad docs: Added back in the conversation starters gif for the agent docs 2025-11-07 13:46:53 -07:00
Dark-Alex-17 7056818808 docs: Made an example agent gif to show how they work (and variables) 2025-11-07 13:46:35 -07:00
Dark-Alex-17 67b4510d94 docs: Created documentation for agents 2025-11-07 13:46:16 -07:00
Dark-Alex-17 2e74619b03 docs: Added a screenshot of the tools overrides settings 2025-11-07 13:46:00 -07:00
Dark-Alex-17 727ff52ff7 docs: Created docs about both built-in and custom tools for function calling capabilities 2025-11-07 13:45:45 -07:00
Dark-Alex-17 33cb6aaf1f docs: Documented how to create custom tools in Python, and how custom tools are created and used 2025-11-07 13:45:23 -07:00
Dark-Alex-17 2dfab3d399 docs: Documented how to create custom Bash-based tools 2025-11-07 13:45:01 -07:00
Dark-Alex-17 bcbd0e7be1 docs: Added back in forgotten gif of a session 2025-11-07 13:44:44 -07:00
Dark-Alex-17 8ccc61e831 docs: documentation on how sessions work in Loki 2025-11-07 13:44:32 -07:00
Dark-Alex-17 a1656da7a2 docs: Created a demo gif of how to use roles in general 2025-11-07 13:44:16 -07:00
Dark-Alex-17 499d396802 docs: Created a demo gif of a temporary prompt role 2025-11-07 13:44:00 -07:00
Dark-Alex-17 bdfc9ca062 docs: Documented roles 2025-11-07 13:43:37 -07:00
Dark-Alex-17 120368178d docs: created a gif that demonstrates macro functionality 2025-11-07 13:43:26 -07:00
Dark-Alex-17 783dc76285 docs: Removed a forgotten TODO comment 2025-11-07 13:43:09 -07:00
Dark-Alex-17 cf1f5d39a1 docs: created a screenshot of the global settings overrides for MCP servers 2025-11-07 13:42:36 -07:00
Dark-Alex-17 beb4c54ea5 docs: created screenshots for both ephemeral and persistent RAG 2025-11-07 13:42:15 -07:00
Dark-Alex-17 9573c88efd docs: documented RAG 2025-11-07 13:41:50 -07:00
Dark-Alex-17 7b339e35f8 docs: Created docs that explain how to use MCP servers with Loki 2025-11-07 13:41:19 -07:00
Dark-Alex-17 ed3f4b23f8 docs: created docs for Loki's macro system 2025-11-07 13:40:48 -07:00
Dark-Alex-17 8b2c23f598 docs: documented how to use custom themes 2025-11-07 13:40:25 -07:00
Dark-Alex-17 5f227988bb docs: documented how to create custom REPL prompts 2025-11-07 13:40:10 -07:00
Dark-Alex-17 82e2bcbce4 docs: documented the now built-in bash helper script and the tools it comes with 2025-11-07 13:39:53 -07:00
Dark-Alex-17 4277226ca1 docs: created documentation for how to patch requests via configuration settings 2025-11-07 13:39:04 -07:00
Dark-Alex-17 21ccc7af86 docs: created documentation for client configurations 2025-11-07 13:38:34 -07:00
Dark-Alex-17 f1a2570d41 docs: updated the vault demo screenshots and gifs 2025-11-07 13:38:22 -07:00
Dark-Alex-17 e977864158 docs: Added screenshots for select custom themes 2025-11-07 13:37:56 -07:00
Dark-Alex-17 4b8085b142 docs: Added documentation for secret injection support into environment variables for agents 2025-11-07 12:28:11 -07:00
Dark-Alex-17 832bc419dd docs: Added an explain-shell screenshot 2025-11-07 12:26:43 -07:00
Dark-Alex-17 0ceae6a98f docs: Fixed a typo in the shell integrations documentation 2025-11-07 12:25:26 -07:00
Dark-Alex-17 922e4f4b1a docs: Created license 2025-11-07 11:48:19 -07:00
Dark-Alex-17 7b68077f7a ci: Created Loki installation scripts 2025-11-07 11:48:08 -07:00
Dark-Alex-17 7f359af72c refactor: Changed the name of the summary_prompt setting to summary_context_prompt 2025-11-07 11:13:58 -07:00
Dark-Alex-17 1330ff72ce refactor: Renamed summarize_prompt setting to summarization_prompt 2025-11-07 11:09:48 -07:00
Dark-Alex-17 288e1fa234 refactor: Renamed the compress_threshold setting to compression_threshold 2025-11-07 11:06:20 -07:00
Dark-Alex-17 71f33cb87a style: Applied formatting 2025-11-06 18:19:25 -07:00
Dark-Alex-17 afeb634b94 refactor: Migrated around the location of some of the more large documents for documentation 2025-11-06 18:02:17 -07:00
Dark-Alex-17 72ad69b401 docs: Updated the global configuration example to have a separate section for the REPL prompts 2025-11-06 16:24:20 -07:00
Dark-Alex-17 c1ac4d9032 docs: Fixed a typo in the description of the stream setting 2025-11-06 16:10:44 -07:00
Dark-Alex-17 9f9ef10da9 docs: Referenced the vault documentation in the example config 2025-11-06 16:09:21 -07:00
Dark-Alex-17 a44e58547e docs: Created a separate, dedicated section of the example configuration file for the vault 2025-11-06 16:08:20 -07:00
Dark-Alex-17 7d5e5fce76 docs: Improved the documentation for sessions and the examples in the global configuration example 2025-11-06 15:55:38 -07:00
Dark-Alex-17 610bebaae1 docs: Improved the documentation of preludes and their purpose in the example global configuration file 2025-11-06 15:48:44 -07:00
Dark-Alex-17 aa1b7d57a4 docs: Improved the documentation of the behavior-related settings of the global configuration file example 2025-11-06 15:47:30 -07:00
Dark-Alex-17 2c8daca20c docs: Improved wording in the example agent configuration 2025-11-06 13:55:44 -07:00
Dark-Alex-17 a13f771925 docs: Updated the example agent configuration to show the new global_tools and mcp_servers environment variables 2025-11-06 13:31:25 -07:00
Dark-Alex-17 0b23f1174b feat: Added the agents directory to sysinfo output 2025-11-06 13:22:13 -07:00
Dark-Alex-17 f09a06365b docs: Fixed a typo in the Vertex AI client configuration example in the example global configuration file 2025-11-06 13:07:34 -07:00
Dark-Alex-17 e9071c8b82 Added environment variables for agents for the global_tools and mcp_servers settings 2025-11-06 12:16:36 -07:00
Dark-Alex-17 5959cbd809 docs: Updated the example global configuration file with some better examples for RAG 2025-11-06 10:49:51 -07:00
Dark-Alex-17 19597735b8 docs: Created an example macro configuration file 2025-11-05 16:55:04 -07:00
Dark-Alex-17 aa171c6e6d feat: Added built-in macros 2025-11-05 16:28:56 -07:00
Dark-Alex-17 22939a53a9 bug: Removed deprecated experimentation for MCP sampling 2025-11-05 16:12:04 -07:00
Dark-Alex-17 f076373859 style: Added an import for Anyhow's Result in the macros module 2025-11-05 15:52:44 -07:00
Dark-Alex-17 528c3ae657 refactor: Factored out the macros structs from the large config module 2025-11-05 15:50:39 -07:00
Dark-Alex-17 e0e0f519fb bug: Fixed a bug with the spacing of info output now that function_calling_support is a longer name 2025-11-05 15:41:49 -07:00
Dark-Alex-17 8feb292738 feat: Updated the example role configuration file to also have the prompt field 2025-11-05 15:25:01 -07:00
Dark-Alex-17 17abfe9aa4 feat: Updated the code role 2025-11-05 15:24:45 -07:00
Dark-Alex-17 441e472328 refactor: Refactored mcp_servers and function_calling to mcp_server_support and function_calling_support to make the purpose of the fields more clear 2025-11-04 13:17:58 -07:00
Dark-Alex-17 89cf081749 refactor: Refactored the use_mcp_servers field to enabled_mcp_servers to make the purpose of the field more clear 2025-11-04 12:51:41 -07:00
Dark-Alex-17 872ac62e81 Merge branch 'main' of github.com:Dark-Alex-17/loki 2025-11-04 12:37:32 -07:00
Dark-Alex-17 9de95ca21d refactor: Refactored use_tools field to enabled_tools field to make the use of the field more clear 2025-11-04 12:37:14 -07:00
Dark-Alex-17 3af07cabe8 Refactored the use_tools field to enabled_tools to make field uses and functions more clear 2025-11-04 12:36:31 -07:00
Dark-Alex-17 5719ff2e79 docs: Updated the config.example.yaml to have an example of how to use the visible_tools array 2025-11-04 12:10:17 -07:00
Dark-Alex-17 84556cb706 refactor: Removed the use of the tools.txt file and added tool visibility declarations to the global configuration file 2025-11-04 12:07:58 -07:00
Dark-Alex-17 0983868196 refactor: Agents that depend on global tools now have all binaries compiled and stored in the agent's bin directory so multiple agents can run at once 2025-11-04 11:29:59 -07:00
Dark-Alex-17 382926243c feat: Secret injection as environment variables into agent tools 2025-11-03 15:10:34 -07:00
Dark-Alex-17 467afb6767 feat: Removed the server functionality 2025-11-03 14:25:55 -07:00
Dark-Alex-17 73356b4a76 feat: Require Vault set up for first-time setup so all passed in secrets can be encrypted right off the bat 2025-10-27 12:00:27 -06:00
Dark-Alex-17 34d4681b38 style: Re-applied formatting to make Clippy happy 2025-10-24 15:05:42 -06:00
Dark-Alex-17 aa980b0a96 refactor: Removed the git MCP server and used the newer, better mcp-server-docker for local docker integration 2025-10-24 14:38:13 -06:00
Dark-Alex-17 397db60782 docs: Added in forgotten MCP server configuration values to the example config 2025-10-24 14:16:13 -06:00
Dark-Alex-17 57c5c35c37 Created an Elvish integration script 2025-10-24 11:28:31 -06:00
Dark-Alex-17 7ab9fea439 refactor: Renamed the argument for the --completions flag to SHELL 2025-10-24 10:58:28 -06:00
Dark-Alex-17 4c0514d8e9 feat: Added static completions via a --completions flag 2025-10-24 10:56:34 -06:00
Dark-Alex-17 2fb9d2fa86 refactor: Updated the instructions for the jira-helper agent 2025-10-23 10:07:50 -06:00
Dark-Alex-17 f57a134bc0 bug: Fixed a bug when passing tools to Claude for tools that don't have any inputs 2025-10-21 10:04:38 -06:00
Dark-Alex-17 2ceb0808c8 bug: Fixed a bug that was duplicating entries of all the functions for agents between MCP and tools 2025-10-20 15:30:29 -06:00
Dark-Alex-17 c9b90e8411 ci: Updated to only include basic ARM64 and x86_64 architectures 2025-10-17 13:30:42 -06:00
Dark-Alex-17 8d03b2fc72 bug: corrected a typo for sourcing the prompt utility bash script in the built-in tools 2025-10-16 15:48:53 -06:00
Dark-Alex-17 b172fe8fbf fix: Corrected a typo for sourcing the bash utility script in some agent definitions 2025-10-16 15:47:07 -06:00
Dark-Alex-17 bdd3aaa0ab chore: update the models.yaml 2025-10-16 15:20:33 -06:00
Dark-Alex-17 64cbac0dd9 refactor: Modified the default PS1 look 2025-10-16 15:08:48 -06:00
Dark-Alex-17 50903c3d03 style: Cleaned up some linting issues for Windows 2025-10-16 13:30:30 -06:00
Dark-Alex-17 680b71e13d style: Applied formatting 2025-10-16 13:01:37 -06:00
Dark-Alex-17 a410818015 refactor: Fixed a linting issue for Windows builds 2025-10-16 12:44:50 -06:00
Dark-Alex-17 6c5bc51a0a docs: Updated outdated API links in the config example 2025-10-16 12:38:07 -06:00
Dark-Alex-17 38d114808e feat: Support for secret injection into the global config file (API keys, for example) 2025-10-16 12:30:18 -06:00
Dark-Alex-17 bb37513ef5 feat: Improved MCP handling toggle handling 2025-10-15 18:36:54 -06:00
Dark-Alex-17 85533f665e feat: Secret injection into the MCP configuration 2025-10-15 16:06:59 -06:00
Dark-Alex-17 b790041d95 feat: added REPL support for interacting with the Loki vault 2025-10-15 15:15:04 -06:00
Dark-Alex-17 bb5419967f feat: Integrated gman with Loki to create a vault and added flags to configure the Loki vault 2025-10-14 18:00:11 -06:00
Dark-Alex-17 21b00c6333 Applied formatting 2025-10-10 15:32:51 -06:00
Dark-Alex-17 4050997d7b bug: Automatically mark all extracted tools as executable 2025-10-10 15:30:58 -06:00
Dark-Alex-17 7d7477f4ec docs: Created an example role configuration 2025-10-10 15:15:11 -06:00
Dark-Alex-17 7d81b45f92 feat: Added a default session to the jira helper to make interaction more natural 2025-10-10 15:03:26 -06:00
Dark-Alex-17 83f3790d2f style: applied formatting 2025-10-10 15:01:55 -06:00
Dark-Alex-17 4ad20c380d refactor: Changed the name of agent_prelude to agent_session to make its purpose more clear 2025-10-10 15:01:44 -06:00
Dark-Alex-17 a895da9e47 style: Applied consistent formatting to agent changes 2025-10-10 14:48:10 -06:00
Dark-Alex-17 1bfd2b7370 feat: Created the repo-analyzer role 2025-10-10 14:43:18 -06:00
Dark-Alex-17 6631ff53f2 feat: Created the coder and sql agents 2025-10-10 13:38:47 -06:00
Dark-Alex-17 6244e337b0 feat: Cleaned the built-in functions to not have leftover dependencies 2025-10-10 13:38:27 -06:00
Dark-Alex-17 d199e9ebe6 feat: Created additional built-in roles for slack, repo analysis, and github 2025-10-10 13:38:03 -06:00
Dark-Alex-17 ca5bfd6e8f feat: Install built-in agents 2025-10-10 13:37:05 -06:00
Dark-Alex-17 c8984cf91a refactor: Removed leftover javascript function support; will not implement 2025-10-10 10:22:05 -06:00
Dark-Alex-17 bf4422ed0d docs: Fixed typo in Python execution docs 2025-10-10 10:05:09 -06:00
Dark-Alex-17 e41fbed9cc feat: Embedded baseline MCP config and global tools 2025-10-10 09:58:20 -06:00
Dark-Alex-17 4e6e8a845f docs: Created the code of conduct 2025-10-07 10:59:27 -06:00
Dark-Alex-17 e24c056191 docs: Added the security policy 2025-10-07 10:58:02 -06:00
Dark-Alex-17 b56fe7d3cd ci: Initialized commitizen configuration 2025-10-07 10:57:37 -06:00
Dark-Alex-17 f4c5d9f0d7 docs: Added loki contribution guidelines 2025-10-07 10:55:52 -06:00
Dark-Alex-17 f9dc61e906 Created an .actrc file to make local CI/CD testing easier 2025-10-07 10:54:16 -06:00
Dark-Alex-17 ed8dc34ff6 Removed the hestia CLI since it is no longer needed 2025-10-07 10:53:44 -06:00
Dark-Alex-17 a2b57caff5 Updated gitignore 2025-10-07 10:53:00 -06:00
Dark-Alex-17 19a680442d Create issue templates and CI/CD workflows 2025-10-07 10:51:04 -06:00
Dark-Alex-17 394f1f92a0 Baseline project 2025-10-07 10:45:42 -06:00
Dark-Alex-17 044f34b029 Created initial assets 2025-10-07 10:43:34 -06:00
Dark-Alex-17 a250fe98bb Created initial assets 2025-10-07 10:42:46 -06:00
Dark-Alex-17 a7c770120a Initial commit 2025-10-07 10:41:42 -06:00
39 changed files with 4458 additions and 766 deletions
Generated
+106 -335
View File
@@ -205,7 +205,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2",
"cpufeatures",
"cpufeatures 0.2.17",
"password-hash",
]
@@ -314,29 +314,17 @@ version = "1.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc"
dependencies = [
"aws-lc-sys 0.39.1",
"aws-lc-sys",
"zeroize",
]
[[package]]
name = "aws-lc-sys"
version = "0.37.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549"
dependencies = [
"bindgen",
"cc",
"cmake",
"dunce",
"fs_extra",
]
[[package]]
name = "aws-lc-sys"
version = "0.39.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399"
dependencies = [
"bindgen",
"cc",
"cmake",
"dunce",
@@ -859,7 +847,7 @@ dependencies = [
"proc-macro2",
"quote",
"regex",
"rustc-hash 2.1.2",
"rustc-hash",
"shlex",
"syn",
]
@@ -1038,7 +1026,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
"cpufeatures 0.2.17",
]
[[package]]
name = "chacha20"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"rand_core 0.10.0",
]
[[package]]
@@ -1048,7 +1047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
dependencies = [
"aead",
"chacha20",
"chacha20 0.9.1",
"cipher",
"poly1305",
"zeroize",
@@ -1300,6 +1299,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crc32c"
version = "0.6.8"
@@ -1355,7 +1363,6 @@ dependencies = [
"mio",
"parking_lot",
"rustix 0.38.44",
"serde",
"signal-hook",
"signal-hook-mio",
"winapi",
@@ -1374,6 +1381,7 @@ dependencies = [
"mio",
"parking_lot",
"rustix 1.1.4",
"serde",
"signal-hook",
"signal-hook-mio",
"winapi",
@@ -1388,12 +1396,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-common"
version = "0.1.7"
@@ -1518,34 +1520,13 @@ dependencies = [
"syn",
]
[[package]]
name = "derive_more"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
dependencies = [
"derive_more-impl 1.0.0",
]
[[package]]
name = "derive_more"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
dependencies = [
"derive_more-impl 2.1.1",
]
[[package]]
name = "derive_more-impl"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
dependencies = [
"proc-macro2",
"quote",
"syn",
"unicode-xid",
"derive_more-impl",
]
[[package]]
@@ -2105,15 +2086,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "getopts"
version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
dependencies = [
"unicode-width",
]
[[package]]
name = "getrandom"
version = "0.2.17"
@@ -2150,6 +2122,7 @@ dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
"rand_core 0.10.0",
"wasip2",
"wasip3",
]
@@ -2168,15 +2141,15 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "gman"
version = "0.3.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7c3a428900217107275faf709b30c00f37e1112ec2b75742987b5ca88700eaa"
checksum = "742225eb41061a0938aa0924ce8d08a1ec48875789b72ce3f0cb02eda52ab1db"
dependencies = [
"anyhow",
"argon2",
"async-trait",
"aws-config",
"aws-lc-sys 0.37.1",
"aws-lc-sys",
"aws-sdk-secretsmanager",
"azure_core",
"azure_identity",
@@ -2258,15 +2231,6 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
@@ -2822,18 +2786,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "is-macro"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d57a3e447e24c22647738e4607f1df1e0ec6f72e16182c4cd199f647cdfb0e4"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "is-terminal"
version = "0.4.17"
@@ -2870,15 +2822,6 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.13.0"
@@ -2987,12 +2930,6 @@ dependencies = [
"simple_asn1",
]
[[package]]
name = "lalrpop-util"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553"
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -3021,12 +2958,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "libm"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "libredox"
version = "0.1.15"
@@ -3135,8 +3066,10 @@ dependencies = [
"clap_complete_nushell",
"colored",
"crossterm 0.28.1",
"crossterm 0.29.0",
"dirs",
"duct",
"dunce",
"fancy-regex",
"futures-util",
"fuzzy-matcher",
@@ -3162,15 +3095,13 @@ dependencies = [
"parking_lot",
"path-absolutize",
"pretty_assertions",
"rand 0.9.2",
"rand 0.10.0",
"rayon",
"reedline",
"reqwest",
"reqwest-eventsource",
"rmcp",
"rust-embed",
"rustpython-ast",
"rustpython-parser",
"scraper",
"serde",
"serde_json",
@@ -3186,6 +3117,10 @@ dependencies = [
"tokio",
"tokio-graceful",
"tokio-stream",
"tree-sitter",
"tree-sitter-language",
"tree-sitter-python",
"tree-sitter-typescript",
"unicode-segmentation",
"unicode-width",
"url",
@@ -3231,64 +3166,6 @@ dependencies = [
"libc",
]
[[package]]
name = "malachite"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fbdf9cb251732db30a7200ebb6ae5d22fe8e11397364416617d2c2cf0c51cb5"
dependencies = [
"malachite-base",
"malachite-nz",
"malachite-q",
]
[[package]]
name = "malachite-base"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ea0ed76adf7defc1a92240b5c36d5368cfe9251640dcce5bd2d0b7c1fd87aeb"
dependencies = [
"hashbrown 0.14.5",
"itertools 0.11.0",
"libm",
"ryu",
]
[[package]]
name = "malachite-bigint"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d149aaa2965d70381709d9df4c7ee1fc0de1c614a4efc2ee356f5e43d68749f8"
dependencies = [
"derive_more 1.0.0",
"malachite",
"num-integer",
"num-traits",
"paste",
]
[[package]]
name = "malachite-nz"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34a79feebb2bc9aa7762047c8e5495269a367da6b5a90a99882a0aeeac1841f7"
dependencies = [
"itertools 0.11.0",
"libm",
"malachite-base",
]
[[package]]
name = "malachite-q"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f235d5747b1256b47620f5640c2a17a88c7569eebdf27cd9cb130e1a619191"
dependencies = [
"itertools 0.11.0",
"malachite-base",
"malachite-nz",
]
[[package]]
name = "markup5ever"
version = "0.12.1"
@@ -3943,12 +3820,6 @@ dependencies = [
"subtle",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pastey"
version = "0.2.1"
@@ -4115,7 +3986,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
dependencies = [
"cpufeatures",
"cpufeatures 0.2.17",
"opaque-debug",
"universal-hash",
]
@@ -4291,7 +4162,7 @@ dependencies = [
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash 2.1.2",
"rustc-hash",
"rustls 0.23.37",
"socket2 0.6.3",
"thiserror 2.0.18",
@@ -4311,7 +4182,7 @@ dependencies = [
"lru-slab",
"rand 0.9.2",
"ring",
"rustc-hash 2.1.2",
"rustc-hash",
"rustls 0.23.37",
"rustls-pki-types",
"slab",
@@ -4362,8 +4233,6 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
@@ -4373,18 +4242,19 @@ version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_chacha",
"rand_core 0.9.5",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
name = "rand"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
"chacha20 0.10.0",
"getrandom 0.4.2",
"rand_core 0.10.0",
]
[[package]]
@@ -4415,6 +4285,12 @@ dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rand_core"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
[[package]]
name = "rayon"
version = "1.11.0"
@@ -4457,12 +4333,12 @@ dependencies = [
[[package]]
name = "reedline"
version = "0.40.0"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5cdfab7494d13ebfb6ce64828648518205d3ce8541ef1f94a27887f29d2d50b"
checksum = "fe9e7c532bfc2759bc8a28902c04e8b993fc13ebd085ee4292eb1b230fa9beef"
dependencies = [
"chrono",
"crossterm 0.28.1",
"crossterm 0.29.0",
"fd-lock",
"itertools 0.13.0",
"nu-ansi-term",
@@ -4471,6 +4347,7 @@ dependencies = [
"strum",
"strum_macros 0.26.4",
"thiserror 2.0.18",
"unicase",
"unicode-segmentation",
"unicode-width",
]
@@ -4729,12 +4606,6 @@ version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
version = "2.1.2"
@@ -4848,63 +4719,6 @@ dependencies = [
"untrusted",
]
[[package]]
name = "rustpython-ast"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cdaf8ee5c1473b993b398c174641d3aa9da847af36e8d5eb8291930b72f31a5"
dependencies = [
"is-macro",
"malachite-bigint",
"rustpython-parser-core",
"static_assertions",
]
[[package]]
name = "rustpython-parser"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "868f724daac0caf9bd36d38caf45819905193a901e8f1c983345a68e18fb2abb"
dependencies = [
"anyhow",
"is-macro",
"itertools 0.11.0",
"lalrpop-util",
"log",
"malachite-bigint",
"num-traits",
"phf",
"phf_codegen",
"rustc-hash 1.1.0",
"rustpython-ast",
"rustpython-parser-core",
"tiny-keccak",
"unic-emoji-char",
"unic-ucd-ident",
"unicode_names2",
]
[[package]]
name = "rustpython-parser-core"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4b6c12fa273825edc7bccd9a734f0ad5ba4b8a2f4da5ff7efe946f066d0f4ad"
dependencies = [
"is-macro",
"memchr",
"rustpython-parser-vendored",
]
[[package]]
name = "rustpython-parser-vendored"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04fcea49a4630a3a5d940f4d514dc4f575ed63c14c3e3ed07146634aed7f67a6"
dependencies = [
"memchr",
"once_cell",
]
[[package]]
name = "rustversion"
version = "1.0.22"
@@ -5224,7 +5038,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@@ -5385,12 +5199,6 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "stop-words"
version = "0.9.0"
@@ -5400,6 +5208,12 @@ dependencies = [
"serde_json",
]
[[package]]
name = "streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520"
[[package]]
name = "string_cache"
version = "0.8.9"
@@ -5736,15 +5550,6 @@ dependencies = [
"time-core",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]]
name = "tinystr"
version = "0.8.3"
@@ -6050,6 +5855,46 @@ dependencies = [
"tracing-log",
]
[[package]]
name = "tree-sitter"
version = "0.26.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "887bd495d0582c5e3e0d8ece2233666169fa56a9644d172fc22ad179ab2d0538"
dependencies = [
"cc",
"regex",
"regex-syntax",
"serde_json",
"streaming-iterator",
"tree-sitter-language",
]
[[package]]
name = "tree-sitter-language"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782"
[[package]]
name = "tree-sitter-python"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bf85fd39652e740bf60f46f4cda9492c3a9ad75880575bf14960f775cb74a1c"
dependencies = [
"cc",
"tree-sitter-language",
]
[[package]]
name = "tree-sitter-typescript"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c5f76ed8d947a75cc446d5fccd8b602ebf0cde64ccf2ffa434d873d7a575eff"
dependencies = [
"cc",
"tree-sitter-language",
]
[[package]]
name = "tree_magic_mini"
version = "3.2.2"
@@ -6133,58 +5978,6 @@ dependencies = [
"syn",
]
[[package]]
name = "unic-char-property"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
dependencies = [
"unic-char-range",
]
[[package]]
name = "unic-char-range"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
[[package]]
name = "unic-common"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
[[package]]
name = "unic-emoji-char"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b07221e68897210270a38bde4babb655869637af0f69407f96053a34f76494d"
dependencies = [
"unic-char-property",
"unic-char-range",
"unic-ucd-version",
]
[[package]]
name = "unic-ucd-ident"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987"
dependencies = [
"unic-char-property",
"unic-char-range",
"unic-ucd-version",
]
[[package]]
name = "unic-ucd-version"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
dependencies = [
"unic-common",
]
[[package]]
name = "unicase"
version = "2.9.0"
@@ -6221,28 +6014,6 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unicode_names2"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1673eca9782c84de5f81b82e4109dcfb3611c8ba0d52930ec4a9478f547b2dd"
dependencies = [
"phf",
"unicode_names2_generator",
]
[[package]]
name = "unicode_names2_generator"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b91e5b84611016120197efd7dc93ef76774f4e084cd73c9fb3ea4a86c570c56e"
dependencies = [
"getopts",
"log",
"phf_codegen",
"rand 0.8.5",
]
[[package]]
name = "universal-hash"
version = "0.5.1"
+9 -6
View File
@@ -18,10 +18,11 @@ anyhow = "1.0.69"
bytes = "1.4.0"
clap = { version = "4.5.40", features = ["cargo", "derive", "wrap_help"] }
dirs = "6.0.0"
dunce = "1.0.5"
futures-util = "0.3.29"
inquire = "0.9.4"
is-terminal = "0.4.9"
reedline = "0.40.0"
reedline = "0.46.0"
serde = { version = "1.0.152", features = ["derive"] }
serde_json = { version = "1.0.93", features = ["preserve_order"] }
serde_yaml = "0.9.17"
@@ -37,7 +38,7 @@ tokio-graceful = "0.2.2"
tokio-stream = { version = "0.1.15", default-features = false, features = [
"sync",
] }
crossterm = "0.28.1"
crossterm = "0.29.0"
chrono = "0.4.23"
bincode = { version = "2.0.0", features = [
"serde",
@@ -90,14 +91,16 @@ strum_macros = "0.27.2"
indoc = "2.0.6"
rmcp = { version = "0.16.0", features = ["client", "transport-child-process"] }
num_cpus = "1.17.0"
rustpython-parser = "0.4.0"
rustpython-ast = "0.4.0"
tree-sitter = "0.26.8"
tree-sitter-language = "0.1"
tree-sitter-python = "0.25.0"
tree-sitter-typescript = "0.23"
colored = "3.0.0"
clap_complete = { version = "4.5.58", features = ["unstable-dynamic"] }
gman = "0.3.0"
gman = "0.4.1"
clap_complete_nushell = "4.5.9"
open = "5"
rand = "0.9.0"
rand = { version = "0.10.0", features = ["default"] }
url = "2.5.8"
[dependencies.reqwest]
+1
View File
@@ -28,6 +28,7 @@ Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration g
* [Function Calling](./docs/function-calling/TOOLS.md#Tools): Leverage function calling capabilities to extend Loki's functionality with custom tools
* [Creating Custom Tools](./docs/function-calling/CUSTOM-TOOLS.md): You can create your own custom tools to enhance Loki's capabilities.
* [Create Custom Python Tools](./docs/function-calling/CUSTOM-TOOLS.md#custom-python-based-tools)
* [Create Custom TypeScript Tools](./docs/function-calling/CUSTOM-TOOLS.md#custom-typescript-based-tools)
* [Create Custom Bash Tools](./docs/function-calling/CUSTOM-BASH-TOOLS.md)
* [Bash Prompt Utilities](./docs/function-calling/BASH-PROMPT-HELPERS.md)
* [First-Class MCP Server Support](./docs/function-calling/MCP-SERVERS.md): Easily connect and interact with MCP servers for advanced functionality.
+7 -1
View File
@@ -50,7 +50,13 @@ def parse_raw_data(data):
def parse_argv():
agent_func = sys.argv[1]
agent_data = sys.argv[2]
tool_data_file = os.environ.get("LLM_TOOL_DATA_FILE")
if tool_data_file and os.path.isfile(tool_data_file):
with open(tool_data_file, "r", encoding="utf-8") as f:
agent_data = f.read()
else:
agent_data = sys.argv[2]
if (not agent_data) or (not agent_func):
print("Usage: ./{agent_name}.py <agent-func> <agent-data>", file=sys.stderr)
+5 -2
View File
@@ -14,7 +14,11 @@ main() {
parse_argv() {
agent_func="$1"
agent_data="$2"
if [[ -n "$LLM_TOOL_DATA_FILE" ]] && [[ -f "$LLM_TOOL_DATA_FILE" ]]; then
agent_data="$(cat "$LLM_TOOL_DATA_FILE")"
else
agent_data="$2"
fi
if [[ -z "$agent_data" ]] || [[ -z "$agent_func" ]]; then
die "usage: ./{agent_name}.sh <agent-func> <agent-data>"
fi
@@ -57,7 +61,6 @@ run() {
if [[ "$OS" == "Windows_NT" ]]; then
set -o igncr
tools_path="$(cygpath -w "$tools_path")"
tool_data="$(echo "$tool_data" | sed 's/\\/\\\\/g')"
fi
jq_script="$(cat <<-'EOF'
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env tsx
// Usage: ./{agent_name}.ts <agent-func> <agent-data>
import { readFileSync, writeFileSync, existsSync } from "fs";
import { join } from "path";
import { pathToFileURL } from "url";
async function main(): Promise<void> {
const { agentFunc, rawData } = parseArgv();
const agentData = parseRawData(rawData);
const configDir = "{config_dir}";
setupEnv(configDir, agentFunc);
const agentToolsPath = join(configDir, "agents", "{agent_name}", "tools.ts");
await run(agentToolsPath, agentFunc, agentData);
}
function parseRawData(data: string): Record<string, unknown> {
if (!data) {
throw new Error("No JSON data");
}
try {
return JSON.parse(data);
} catch {
throw new Error("Invalid JSON data");
}
}
function parseArgv(): { agentFunc: string; rawData: string } {
const agentFunc = process.argv[2];
const toolDataFile = process.env["LLM_TOOL_DATA_FILE"];
let agentData: string;
if (toolDataFile && existsSync(toolDataFile)) {
agentData = readFileSync(toolDataFile, "utf-8");
} else {
agentData = process.argv[3];
}
if (!agentFunc || !agentData) {
process.stderr.write("Usage: ./{agent_name}.ts <agent-func> <agent-data>\n");
process.exit(1);
}
return { agentFunc, rawData: agentData };
}
function setupEnv(configDir: string, agentFunc: string): void {
loadEnv(join(configDir, ".env"));
process.env["LLM_ROOT_DIR"] = configDir;
process.env["LLM_AGENT_NAME"] = "{agent_name}";
process.env["LLM_AGENT_FUNC"] = agentFunc;
process.env["LLM_AGENT_ROOT_DIR"] = join(configDir, "agents", "{agent_name}");
process.env["LLM_AGENT_CACHE_DIR"] = join(configDir, "cache", "{agent_name}");
}
function loadEnv(filePath: string): void {
let lines: string[];
try {
lines = readFileSync(filePath, "utf-8").split("\n");
} catch {
return;
}
for (const raw of lines) {
const line = raw.trim();
if (line.startsWith("#") || !line) {
continue;
}
const eqIdx = line.indexOf("=");
if (eqIdx === -1) {
continue;
}
const key = line.slice(0, eqIdx).trim();
if (key in process.env) {
continue;
}
let value = line.slice(eqIdx + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
process.env[key] = value;
}
}
function extractParamNames(fn: Function): string[] {
const src = fn.toString();
const match = src.match(/^(?:async\s+)?function\s*\w*\s*\(([^)]*)\)/);
if (!match) {
return [];
}
return match[1]
.split(",")
.map((p) => p.trim().replace(/[:=?].*/s, "").trim())
.filter(Boolean);
}
function spreadArgs(
fn: Function,
data: Record<string, unknown>,
): unknown[] {
const names = extractParamNames(fn);
if (names.length === 0) {
return [];
}
return names.map((name) => data[name]);
}
async function run(
agentPath: string,
agentFunc: string,
agentData: Record<string, unknown>,
): Promise<void> {
const mod = await import(pathToFileURL(agentPath).href);
if (typeof mod[agentFunc] !== "function") {
throw new Error(`No module function '${agentFunc}' at '${agentPath}'`);
}
const fn = mod[agentFunc] as Function;
const args = spreadArgs(fn, agentData);
const value = await fn(...args);
returnToLlm(value);
dumpResult(`{agent_name}:${agentFunc}`);
}
function returnToLlm(value: unknown): void {
if (value === null || value === undefined) {
return;
}
const output = process.env["LLM_OUTPUT"];
const write = (s: string) => {
if (output) {
writeFileSync(output, s, "utf-8");
} else {
process.stdout.write(s);
}
};
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
write(String(value));
} else if (typeof value === "object") {
write(JSON.stringify(value, null, 2));
}
}
function dumpResult(name: string): void {
const dumpResults = process.env["LLM_DUMP_RESULTS"];
const llmOutput = process.env["LLM_OUTPUT"];
if (!dumpResults || !llmOutput || !process.stdout.isTTY) {
return;
}
try {
const pattern = new RegExp(`\\b(${dumpResults})\\b`);
if (!pattern.test(name)) {
return;
}
} catch {
return;
}
let data: string;
try {
data = readFileSync(llmOutput, "utf-8");
} catch {
return;
}
process.stdout.write(
`\x1b[2m----------------------\n${data}\n----------------------\x1b[0m\n`,
);
}
main().catch((err) => {
process.stderr.write(`${err}\n`);
process.exit(1);
});
+5
View File
@@ -49,6 +49,11 @@ def parse_raw_data(data):
def parse_argv():
tool_data_file = os.environ.get("LLM_TOOL_DATA_FILE")
if tool_data_file and os.path.isfile(tool_data_file):
with open(tool_data_file, "r", encoding="utf-8") as f:
return f.read()
argv = sys.argv[:] + [None] * max(0, 2 - len(sys.argv))
tool_data = argv[1]
+5 -2
View File
@@ -13,7 +13,11 @@ main() {
}
parse_argv() {
tool_data="$1"
if [[ -n "$LLM_TOOL_DATA_FILE" ]] && [[ -f "$LLM_TOOL_DATA_FILE" ]]; then
tool_data="$(cat "$LLM_TOOL_DATA_FILE")"
else
tool_data="$1"
fi
if [[ -z "$tool_data" ]]; then
die "usage: ./{function_name}.sh <tool-data>"
fi
@@ -54,7 +58,6 @@ run() {
if [[ "$OS" == "Windows_NT" ]]; then
set -o igncr
tool_path="$(cygpath -w "$tool_path")"
tool_data="$(echo "$tool_data" | sed 's/\\/\\\\/g')"
fi
jq_script="$(cat <<-'EOF'
+184
View File
@@ -0,0 +1,184 @@
#!/usr/bin/env tsx
// Usage: ./{function_name}.ts <tool-data>
import { readFileSync, writeFileSync, existsSync } from "fs";
import { join } from "path";
import { pathToFileURL } from "url";
async function main(): Promise<void> {
const rawData = parseArgv();
const toolData = parseRawData(rawData);
const rootDir = "{root_dir}";
setupEnv(rootDir);
const toolPath = "{tool_path}.ts";
await run(toolPath, "run", toolData);
}
function parseRawData(data: string): Record<string, unknown> {
if (!data) {
throw new Error("No JSON data");
}
try {
return JSON.parse(data);
} catch {
throw new Error("Invalid JSON data");
}
}
function parseArgv(): string {
const toolDataFile = process.env["LLM_TOOL_DATA_FILE"];
if (toolDataFile && existsSync(toolDataFile)) {
return readFileSync(toolDataFile, "utf-8");
}
const toolData = process.argv[2];
if (!toolData) {
process.stderr.write("Usage: ./{function_name}.ts <tool-data>\n");
process.exit(1);
}
return toolData;
}
function setupEnv(rootDir: string): void {
loadEnv(join(rootDir, ".env"));
process.env["LLM_ROOT_DIR"] = rootDir;
process.env["LLM_TOOL_NAME"] = "{function_name}";
process.env["LLM_TOOL_CACHE_DIR"] = join(rootDir, "cache", "{function_name}");
}
function loadEnv(filePath: string): void {
let lines: string[];
try {
lines = readFileSync(filePath, "utf-8").split("\n");
} catch {
return;
}
for (const raw of lines) {
const line = raw.trim();
if (line.startsWith("#") || !line) {
continue;
}
const eqIdx = line.indexOf("=");
if (eqIdx === -1) {
continue;
}
const key = line.slice(0, eqIdx).trim();
if (key in process.env) {
continue;
}
let value = line.slice(eqIdx + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
process.env[key] = value;
}
}
function extractParamNames(fn: Function): string[] {
const src = fn.toString();
const match = src.match(/^(?:async\s+)?function\s*\w*\s*\(([^)]*)\)/);
if (!match) {
return [];
}
return match[1]
.split(",")
.map((p) => p.trim().replace(/[:=?].*/s, "").trim())
.filter(Boolean);
}
function spreadArgs(
fn: Function,
data: Record<string, unknown>,
): unknown[] {
const names = extractParamNames(fn);
if (names.length === 0) {
return [];
}
return names.map((name) => data[name]);
}
async function run(
toolPath: string,
toolFunc: string,
toolData: Record<string, unknown>,
): Promise<void> {
const mod = await import(pathToFileURL(toolPath).href);
if (typeof mod[toolFunc] !== "function") {
throw new Error(`No module function '${toolFunc}' at '${toolPath}'`);
}
const fn = mod[toolFunc] as Function;
const args = spreadArgs(fn, toolData);
const value = await fn(...args);
returnToLlm(value);
dumpResult("{function_name}");
}
function returnToLlm(value: unknown): void {
if (value === null || value === undefined) {
return;
}
const output = process.env["LLM_OUTPUT"];
const write = (s: string) => {
if (output) {
writeFileSync(output, s, "utf-8");
} else {
process.stdout.write(s);
}
};
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
write(String(value));
} else if (typeof value === "object") {
write(JSON.stringify(value, null, 2));
}
}
function dumpResult(name: string): void {
const dumpResults = process.env["LLM_DUMP_RESULTS"];
const llmOutput = process.env["LLM_OUTPUT"];
if (!dumpResults || !llmOutput || !process.stdout.isTTY) {
return;
}
try {
const pattern = new RegExp(`\\b(${dumpResults})\\b`);
if (!pattern.test(name)) {
return;
}
} catch {
return;
}
let data: string;
try {
data = readFileSync(llmOutput, "utf-8");
} catch {
return;
}
process.stdout.write(
`\x1b[2m----------------------\n${data}\n----------------------\x1b[0m\n`,
);
}
main().catch((err) => {
process.stderr.write(`${err}\n`);
process.exit(1);
});
+23 -10
View File
@@ -1,6 +1,7 @@
import os
from typing import List, Literal, Optional
def run(
string: str,
string_enum: Literal["foo", "bar"],
@@ -9,26 +10,38 @@ def run(
number: float,
array: List[str],
string_optional: Optional[str] = None,
integer_with_default: int = 42,
boolean_with_default: bool = True,
number_with_default: float = 3.14,
string_with_default: str = "hello",
array_optional: Optional[List[str]] = None,
):
"""Demonstrates how to create a tool using Python and how to use comments.
"""Demonstrates all supported Python parameter types and variations.
Args:
string: Define a required string property
string_enum: Define a required string property with enum
boolean: Define a required boolean property
integer: Define a required integer property
number: Define a required number property
array: Define a required string array property
string_optional: Define an optional string property
array_optional: Define an optional string array property
string: A required string property
string_enum: A required string property constrained to specific values
boolean: A required boolean property
integer: A required integer property
number: A required number (float) property
array: A required string array property
string_optional: An optional string property (Optional[str] with None default)
integer_with_default: An optional integer with a non-None default value
boolean_with_default: An optional boolean with a default value
number_with_default: An optional number with a default value
string_with_default: An optional string with a default value
array_optional: An optional string array property
"""
output = f"""string: {string}
string_enum: {string_enum}
string_optional: {string_optional}
boolean: {boolean}
integer: {integer}
number: {number}
array: {array}
string_optional: {string_optional}
integer_with_default: {integer_with_default}
boolean_with_default: {boolean_with_default}
number_with_default: {number_with_default}
string_with_default: {string_with_default}
array_optional: {array_optional}"""
for key, value in os.environ.items():
+53
View File
@@ -0,0 +1,53 @@
/**
* Demonstrates all supported TypeScript parameter types and variations.
*
* @param string - A required string property
* @param string_enum - A required string property constrained to specific values
* @param boolean - A required boolean property
* @param number - A required number property
* @param array_bracket - A required string array using bracket syntax
* @param array_generic - A required string array using generic syntax
* @param string_optional - An optional string using the question mark syntax
* @param string_nullable - An optional string using the union-with-null syntax
* @param number_with_default - An optional number with a default value
* @param boolean_with_default - An optional boolean with a default value
* @param string_with_default - An optional string with a default value
* @param array_optional - An optional string array using the question mark syntax
*/
export function run(
string: string,
string_enum: "foo" | "bar",
boolean: boolean,
number: number,
array_bracket: string[],
array_generic: Array<string>,
string_optional?: string,
string_nullable: string | null = null,
number_with_default: number = 42,
boolean_with_default: boolean = true,
string_with_default: string = "hello",
array_optional?: string[],
): string {
const parts = [
`string: ${string}`,
`string_enum: ${string_enum}`,
`boolean: ${boolean}`,
`number: ${number}`,
`array_bracket: ${JSON.stringify(array_bracket)}`,
`array_generic: ${JSON.stringify(array_generic)}`,
`string_optional: ${string_optional}`,
`string_nullable: ${string_nullable}`,
`number_with_default: ${number_with_default}`,
`boolean_with_default: ${boolean_with_default}`,
`string_with_default: ${string_with_default}`,
`array_optional: ${JSON.stringify(array_optional)}`,
];
for (const [key, value] of Object.entries(process.env)) {
if (key.startsWith("LLM_")) {
parts.push(`${key}: ${value}`);
}
}
return parts.join("\n");
}
@@ -0,0 +1,24 @@
#!/usr/bin/env tsx
import { appendFileSync, mkdirSync } from "fs";
import { dirname } from "path";
/**
* Get the current weather in a given location
* @param location - The city and optionally the state or country (e.g., "London", "San Francisco, CA").
*/
export async function run(location: string): string {
const encoded = encodeURIComponent(location);
const url = `https://wttr.in/${encoded}?format=4`;
const resp = await fetch(url);
const data = await resp.text();
const dest = process.env["LLM_OUTPUT"] ?? "/dev/stdout";
if (dest !== "-" && dest !== "/dev/stdout") {
mkdirSync(dirname(dest), { recursive: true });
appendFileSync(dest, data, "utf-8");
}
return data;
}
+2
View File
@@ -46,6 +46,7 @@ enabled_tools: null # Which tools to enable by default. (e.g. 'fs,w
visible_tools: # Which tools are visible to be compiled (and are thus able to be defined in 'enabled_tools')
# - demo_py.py
# - demo_sh.sh
# - demo_ts.ts
- execute_command.sh
# - execute_py_code.py
# - execute_sql_code.sh
@@ -61,6 +62,7 @@ visible_tools: # Which tools are visible to be compiled (and a
# - fs_write.sh
- get_current_time.sh
# - get_current_weather.py
# - get_current_weather.ts
- get_current_weather.sh
- query_jira_issues.sh
# - search_arxiv.sh
+62 -9
View File
@@ -33,6 +33,7 @@ If you're looking for more example agents, refer to the [built-in agents](../ass
- [.env File Support](#env-file-support)
- [Python-Based Agent Tools](#python-based-agent-tools)
- [Bash-Based Agent Tools](#bash-based-agent-tools)
- [TypeScript-Based Agent Tools](#typescript-based-agent-tools)
- [5. Conversation Starters](#5-conversation-starters)
- [6. Todo System & Auto-Continuation](#6-todo-system--auto-continuation)
- [7. Sub-Agent Spawning System](#7-sub-agent-spawning-system)
@@ -62,10 +63,12 @@ Agent configurations often have the following directory structure:
├── tools.sh
or
├── tools.py
or
├── tools.ts
```
This means that agent configurations often are only two files: the agent configuration file (`config.yaml`), and the
tool definitions (`agents/my-agent/tools.sh` or `tools.py`).
tool definitions (`agents/my-agent/tools.sh`, `tools.py`, or `tools.ts`).
To see a full example configuration file, refer to the [example agent config file](../config.agent.example.yaml).
@@ -114,10 +117,10 @@ isolated environment, so in order for an agent to use a tool or MCP server that
explicitly state which tools and/or MCP servers the agent uses. Otherwise, it is assumed that the agent doesn't use any
tools outside its own custom defined tools.
And if you don't define a `agents/my-agent/tools.sh` or `agents/my-agent/tools.py`, then the agent is really just a
And if you don't define a `agents/my-agent/tools.sh`, `agents/my-agent/tools.py`, or `agents/my-agent/tools.ts`, then the agent is really just a
`role`.
You'll notice there's no settings for agent-specific tooling. This is because they are handled separately and
You'll notice there are no settings for agent-specific tooling. This is because they are handled separately and
automatically. See the [Building Tools for Agents](#4-building-tools-for-agents) section below for more information.
To see a full example configuration file, refer to the [example agent config file](../config.agent.example.yaml).
@@ -205,7 +208,7 @@ variables:
### Dynamic Instructions
Sometimes you may find it useful to dynamically generate instructions on startup. Whether that be via a call to Loki
itself to generate them, or by some other means. Loki supports this type of behavior using a special function defined
in your `agents/my-agent/tools.py` or `agents/my-agent/tools.sh`.
in your `agents/my-agent/tools.py`, `agents/my-agent/tools.sh`, or `agents/my-agent/tools.ts`.
**Example: Instructions for a JSON-reader agent that specializes on each JSON input it receives**
`agents/json-reader/tools.py`:
@@ -306,8 +309,8 @@ EOF
}
```
For more information on how to create custom tools for your agent and the structure of the `agent/my-agent/tools.sh` or
`agent/my-agent/tools.py` files, refer to the [Building Tools for Agents](#4-building-tools-for-agents) section below.
For more information on how to create custom tools for your agent and the structure of the `agent/my-agent/tools.sh`,
`agent/my-agent/tools.py`, or `agent/my-agent/tools.ts` files, refer to the [Building Tools for Agents](#4-building-tools-for-agents) section below.
#### Variables
All the same variable interpolations supported by static instructions is also supported by dynamic instructions. For
@@ -337,10 +340,11 @@ defining a single function that gets executed at runtime (e.g. `main` for bash t
tools define a number of *subcommands*.
### Limitations
You can only utilize either a bash-based `<loki-config-dir>/agents/my-agent/tools.sh` or a Python-based
`<loki-config-dir>/agents/my-agent/tools.py`. However, if it's easier to achieve a task in one language vs the other,
You can only utilize one of: a bash-based `<loki-config-dir>/agents/my-agent/tools.sh`, a Python-based
`<loki-config-dir>/agents/my-agent/tools.py`, or a TypeScript-based `<loki-config-dir>/agents/my-agent/tools.ts`.
However, if it's easier to achieve a task in one language vs the other,
you're free to define other scripts in your agent's configuration directory and reference them from the main
`tools.py/sh` file. **Any scripts *not* named `tools.{py,sh}` will not be picked up by Loki's compiler**, meaning they
tools file. **Any scripts *not* named `tools.{py,sh,ts}` will not be picked up by Loki's compiler**, meaning they
can be used like any other set of scripts.
It's important to keep in mind the following:
@@ -428,6 +432,55 @@ the same syntax ad formatting as is used to create custom bash tools globally.
For more information on how to write, [build and test](function-calling/CUSTOM-BASH-TOOLS.md#execute-and-test-your-bash-tools) tools in bash, refer to the
[custom bash tools documentation](function-calling/CUSTOM-BASH-TOOLS.md).
### TypeScript-Based Agent Tools
TypeScript-based agent tools work exactly the same as TypeScript global tools. Instead of a single `run` function,
you define as many exported functions as you like. Non-exported functions are private helpers and are invisible to the
LLM.
**Example:**
`agents/my-agent/tools.ts`
```typescript
/**
* Get your IP information
*/
export async function get_ip_info(): Promise<string> {
const resp = await fetch("https://httpbin.org/ip");
return await resp.text();
}
/**
* Find your public IP address using AWS
*/
export async function get_ip_address_from_aws(): Promise<string> {
const resp = await fetch("https://checkip.amazonaws.com");
return await resp.text();
}
// Non-exported helper — invisible to the LLM
function formatResponse(data: string): string {
return data.trim();
}
```
Loki automatically compiles each exported function as a separate tool for the LLM to call. Just make sure you
follow the same JSDoc and parameter conventions as you would when creating custom TypeScript tools.
TypeScript agent tools also support dynamic instructions via an exported `_instructions()` function:
```typescript
import { readFileSync } from "fs";
/**
* Generates instructions for the agent dynamically
*/
export function _instructions(): string {
const schema = readFileSync("schema.json", "utf-8");
return `You are an AI agent that works with the following schema:\n${schema}`;
}
```
For more information on how to build tools in TypeScript, refer to the [custom TypeScript tools documentation](function-calling/CUSTOM-TOOLS.md#custom-typescript-based-tools).
## 5. Conversation Starters
It's often helpful to also have some conversation starters so users know what kinds of things the agent is capable of
doing. These are available in the REPL via the `.starter` command and are selectable.
+4 -3
View File
@@ -107,6 +107,7 @@ The following variables can be used to change the log level of Loki or the locat
can also pass the `--disable-log-colors` flag as well.
## Miscellaneous Variables
| Environment Variable | Description | Default Value |
|----------------------|--------------------------------------------------------------------------------------------------|---------------|
| `AUTO_CONFIRM` | Bypass all `guard_*` checks in the bash prompt helpers; useful for agent composition and routing | |
| Environment Variable | Description | Default Value |
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|
| `AUTO_CONFIRM` | Bypass all `guard_*` checks in the bash prompt helpers; useful for agent composition and routing | |
| `LLM_TOOL_DATA_FILE` | Set automatically by Loki on Windows. Points to a temporary file containing the JSON tool call data. <br>Tool scripts (`run-tool.sh`, `run-agent.sh`, etc.) read from this file instead of command-line args <br>to avoid JSON escaping issues when data passes through `cmd.exe` → bash. **Not intended to be set by users.** | |
+174 -11
View File
@@ -10,6 +10,8 @@ into your Loki setup. This document provides a guide on how to create and use cu
- [Environment Variables](#environment-variables)
- [Custom Bash-Based Tools](#custom-bash-based-tools)
- [Custom Python-Based Tools](#custom-python-based-tools)
- [Custom TypeScript-Based Tools](#custom-typescript-based-tools)
- [Custom Runtime](#custom-runtime)
<!--toc:end-->
---
@@ -19,9 +21,10 @@ Loki supports custom tools written in the following programming languages:
* Python
* Bash
* TypeScript
## Creating a Custom Tool
All tools are created as scripts in either Python or Bash. They should be placed in the `functions/tools` directory.
All tools are created as scripts in either Python, Bash, or TypeScript. They should be placed in the `functions/tools` directory.
The location of the `functions` directory varies between systems, so you can use the following command to locate
your `functions` directory:
@@ -81,6 +84,7 @@ Loki and demonstrates how to create a Python-based tool:
import os
from typing import List, Literal, Optional
def run(
string: str,
string_enum: Literal["foo", "bar"],
@@ -89,26 +93,38 @@ def run(
number: float,
array: List[str],
string_optional: Optional[str] = None,
integer_with_default: int = 42,
boolean_with_default: bool = True,
number_with_default: float = 3.14,
string_with_default: str = "hello",
array_optional: Optional[List[str]] = None,
):
"""Demonstrates how to create a tool using Python and how to use comments.
"""Demonstrates all supported Python parameter types and variations.
Args:
string: Define a required string property
string_enum: Define a required string property with enum
boolean: Define a required boolean property
integer: Define a required integer property
number: Define a required number property
array: Define a required string array property
string_optional: Define an optional string property
array_optional: Define an optional string array property
string: A required string property
string_enum: A required string property constrained to specific values
boolean: A required boolean property
integer: A required integer property
number: A required number (float) property
array: A required string array property
string_optional: An optional string property (Optional[str] with None default)
integer_with_default: An optional integer with a non-None default value
boolean_with_default: An optional boolean with a default value
number_with_default: An optional number with a default value
string_with_default: An optional string with a default value
array_optional: An optional string array property
"""
output = f"""string: {string}
string_enum: {string_enum}
string_optional: {string_optional}
boolean: {boolean}
integer: {integer}
number: {number}
array: {array}
string_optional: {string_optional}
integer_with_default: {integer_with_default}
boolean_with_default: {boolean_with_default}
number_with_default: {number_with_default}
string_with_default: {string_with_default}
array_optional: {array_optional}"""
for key, value in os.environ.items():
@@ -117,3 +133,150 @@ array_optional: {array_optional}"""
return output
```
### Custom TypeScript-Based Tools
Loki supports tools written in TypeScript. TypeScript tools require [Node.js](https://nodejs.org/) and
[tsx](https://tsx.is/) (`npx tsx` is used as the default runtime).
Each TypeScript-based tool must follow a specific structure in order for Loki to properly compile and execute it:
* The tool must be a TypeScript file with a `.ts` file extension.
* The tool must have an `export function run(...)` that serves as the entry point for the tool.
* Non-exported functions are ignored by the compiler and can be used as private helpers.
* The `run` function must accept flat parameters that define the inputs for the tool.
* Always use type annotations to specify the data type of each parameter.
* Use `param?: type` or `type | null` to indicate optional parameters.
* Use `param: type = value` for parameters with default values.
* The `run` function must return a `string` (or `Promise<string>` for async functions).
* For TypeScript, the return value is automatically written to the `LLM_OUTPUT` environment variable, so there's
no need to explicitly write to the environment variable within the function.
* The function must have a JSDoc comment that describes the tool and its parameters.
* Each parameter should be documented using `@param name - description` tags.
* These descriptions are passed to the LLM as the tool description, letting the LLM know what the tool does and
how to use it.
* Async functions (`export async function run(...)`) are fully supported and handled transparently.
**Supported Parameter Types:**
| TypeScript Type | JSON Schema | Notes |
|-------------------|--------------------------------------------------|-----------------------------|
| `string` | `{"type": "string"}` | Required string |
| `number` | `{"type": "number"}` | Required number |
| `boolean` | `{"type": "boolean"}` | Required boolean |
| `string[]` | `{"type": "array", "items": {"type": "string"}}` | Array (bracket syntax) |
| `Array<string>` | `{"type": "array", "items": {"type": "string"}}` | Array (generic syntax) |
| `"foo" \| "bar"` | `{"type": "string", "enum": ["foo", "bar"]}` | String enum (literal union) |
| `param?: string` | `{"type": "string"}` (not required) | Optional via question mark |
| `string \| null` | `{"type": "string"}` (not required) | Optional via null union |
| `param = "value"` | `{"type": "string"}` (not required) | Optional via default value |
**Unsupported Patterns (will produce a compile error):**
* Rest parameters (`...args: string[]`)
* Destructured object parameters (`{ a, b }: { a: string, b: string }`)
* Arrow functions (`const run = (x: string) => ...`)
* Function expressions (`const run = function(x: string) { ... }`)
Only `export function` declarations are recognized. Non-exported functions are invisible to the compiler.
Below is the [`demo_ts.ts`](../../assets/functions/tools/demo_ts.ts) tool definition that comes pre-packaged with
Loki and demonstrates how to create a TypeScript-based tool:
```typescript
/**
* Demonstrates all supported TypeScript parameter types and variations.
*
* @param string - A required string property
* @param string_enum - A required string property constrained to specific values
* @param boolean - A required boolean property
* @param number - A required number property
* @param array_bracket - A required string array using bracket syntax
* @param array_generic - A required string array using generic syntax
* @param string_optional - An optional string using the question mark syntax
* @param string_nullable - An optional string using the union-with-null syntax
* @param number_with_default - An optional number with a default value
* @param boolean_with_default - An optional boolean with a default value
* @param string_with_default - An optional string with a default value
* @param array_optional - An optional string array using the question mark syntax
*/
export function run(
string: string,
string_enum: "foo" | "bar",
boolean: boolean,
number: number,
array_bracket: string[],
array_generic: Array<string>,
string_optional?: string,
string_nullable: string | null = null,
number_with_default: number = 42,
boolean_with_default: boolean = true,
string_with_default: string = "hello",
array_optional?: string[],
): string {
const parts = [
`string: ${string}`,
`string_enum: ${string_enum}`,
`boolean: ${boolean}`,
`number: ${number}`,
`array_bracket: ${JSON.stringify(array_bracket)}`,
`array_generic: ${JSON.stringify(array_generic)}`,
`string_optional: ${string_optional}`,
`string_nullable: ${string_nullable}`,
`number_with_default: ${number_with_default}`,
`boolean_with_default: ${boolean_with_default}`,
`string_with_default: ${string_with_default}`,
`array_optional: ${JSON.stringify(array_optional)}`,
];
for (const [key, value] of Object.entries(process.env)) {
if (key.startsWith("LLM_")) {
parts.push(`${key}: ${value}`);
}
}
return parts.join("\n");
}
```
## Custom Runtime
By default, Loki uses the following runtimes to execute tools:
| Language | Default Runtime | Requirement |
|------------|-----------------|--------------------------------|
| Python | `python` | Python 3 on `$PATH` |
| TypeScript | `npx tsx` | Node.js + tsx (`npm i -g tsx`) |
| Bash | `bash` | Bash on `$PATH` |
You can override the runtime for Python and TypeScript tools using a **shebang line** (`#!`) at the top of your
script. Loki reads the first line of each tool file; if it starts with `#!`, the specified interpreter is used instead
of the default.
**Examples:**
```python
#!/usr/bin/env python3.11
# This Python tool will be executed with python3.11 instead of the default `python`
def run(name: str):
"""Greet someone.
Args:
name: The name to greet
"""
return f"Hello, {name}!"
```
```typescript
#!/usr/bin/env bun
// This TypeScript tool will be executed with Bun instead of the default `npx tsx`
/**
* Greet someone.
* @param name - The name to greet
*/
export function run(name: string): string {
return `Hello, ${name}!`;
}
```
This is useful for pinning a specific Python version, using an alternative TypeScript runtime like
[Bun](https://bun.sh/) or [Deno](https://deno.com/), or working with virtual environments.
+2
View File
@@ -32,6 +32,7 @@ be enabled/disabled can be found in the [Configuration](#configuration) section
|-------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------|
| [`demo_py.py`](../../assets/functions/tools/demo_py.py) | Demonstrates how to create a tool using Python and how to use comments. | 🔴 |
| [`demo_sh.sh`](../../assets/functions/tools/demo_sh.sh) | Demonstrate how to create a tool using Bash and how to use comment tags. | 🔴 |
| [`demo_ts.ts`](../../assets/functions/tools/demo_ts.ts) | Demonstrates how to create a tool using TypeScript and how to use JSDoc comments. | 🔴 |
| [`execute_command.sh`](../../assets/functions/tools/execute_command.sh) | Execute the shell command. | 🟢 |
| [`execute_py_code.py`](../../assets/functions/tools/execute_py_code.py) | Execute the given Python code. | 🔴 |
| [`execute_sql_code.sh`](../../assets/functions/tools/execute_sql_code.sh) | Execute SQL code. | 🔴 |
@@ -49,6 +50,7 @@ be enabled/disabled can be found in the [Configuration](#configuration) section
| [`get_current_time.sh`](../../assets/functions/tools/get_current_time.sh) | Get the current time. | 🟢 |
| [`get_current_weather.py`](../../assets/functions/tools/get_current_weather.py) | Get the current weather in a given location (Python implementation) | 🔴 |
| [`get_current_weather.sh`](../../assets/functions/tools/get_current_weather.sh) | Get the current weather in a given location. | 🟢 |
| [`get_current_weather.ts`](../../assets/functions/tools/get_current_weather.ts) | Get the current weather in a given location (TypeScript implementation) | 🔴 |
| [`query_jira_issues.sh`](../../assets/functions/tools/query_jira_issues.sh) | Query for jira issues using a Jira Query Language (JQL) query. | 🟢 |
| [`search_arxiv.sh`](../../assets/functions/tools/search_arxiv.sh) | Search arXiv using the given search query and return the top papers. | 🔴 |
| [`search_wikipedia.sh`](../../assets/functions/tools/search_wikipedia.sh) | Search Wikipedia using the given search query. <br>Use it to get detailed information about a public figure, interpretation of a <br>complex scientific concept or in-depth connectivity of a significant historical <br>event, etc. | 🔴 |
+416
View File
@@ -0,0 +1,416 @@
# Sisyphus in LangChain/LangGraph
A faithful recreation of [Loki's Sisyphus agent](../../assets/agents/sisyphus/) using [LangGraph](https://docs.langchain.com/langgraph/) — LangChain's framework for stateful, multi-agent workflows.
This project exists to help you understand LangChain/LangGraph by mapping every concept to its Loki equivalent.
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────┐
│ SUPERVISOR NODE │
│ Intent classification → Routing decision → Command(goto=) │
│ │
│ Loki equivalent: sisyphus/config.yaml │
│ (agent__spawn → Command, agent__collect → graph edge) │
└──────────┬──────────────┬──────────────┬────────────────────┘
│ │ │
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ EXPLORE │ │ ORACLE │ │ CODER │
│ (research) │ │ (advise) │ │ (build) │
│ │ │ │ │ │
│ read-only │ │ read-only │ │ read+write │
│ tools │ │ tools │ │ tools │
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
│ │ │
└──────────────┼──────────────┘
back to supervisor
```
## Concept Map: Loki → LangGraph
This is the key reference. Every row maps a Loki concept to its LangGraph equivalent.
### Core Architecture
| Loki Concept | LangGraph Equivalent | Where in Code |
|---|---|---|
| Agent config (config.yaml) | Node function + system prompt | `agents/explore.py`, etc. |
| Agent instructions | System prompt string | `EXPLORE_SYSTEM_PROMPT`, etc. |
| Agent tools (tools.sh) | `@tool`-decorated Python functions | `tools/filesystem.py`, `tools/project.py` |
| Agent session (chat loop) | Graph state + message list | `state.py``SisyphusState.messages` |
| `agent__spawn --agent X` | `Command(goto="X")` | `agents/supervisor.py` |
| `agent__collect --id` | Graph edge (implicit — workers return to supervisor) | `graph.py``add_edge("explore", "supervisor")` |
| `agent__check` (non-blocking) | Not needed (graph handles scheduling) | — |
| `agent__cancel` | Not needed (graph handles lifecycle) | — |
| `can_spawn_agents: true` | Node has routing logic (supervisor) | `agents/supervisor.py` |
| `max_concurrent_agents: 4` | `Send()` API for parallel fan-out | See [Parallel Execution](#parallel-execution) |
| `max_agent_depth: 3` | `recursion_limit` in config | `cli.py``recursion_limit: 50` |
| `summarization_threshold` | Manual truncation in supervisor | `supervisor.py``_summarize_outputs()` |
### Tool System
| Loki Concept | LangGraph Equivalent | Notes |
|---|---|---|
| `tools.sh` with `@cmd` annotations | `@tool` decorator | Loki compiles bash annotations to JSON schema; LangChain generates schema from the Python function signature + docstring |
| `@option --pattern!` (required arg) | Function parameter without default | `def search_content(pattern: str)` |
| `@option --lines` (optional arg) | Parameter with default | `def read_file(path: str, limit: int = 200)` |
| `@env LLM_OUTPUT=/dev/stdout` | Return value | LangChain tools return strings; Loki tools write to `$LLM_OUTPUT` |
| `@describe` | Docstring | The tool's docstring becomes the description the LLM sees |
| Global tools (`fs_read.sh`, etc.) | Shared tool imports | Both agents import from `tools/filesystem.py` |
| Agent-specific tools | Per-node tool binding | `llm.bind_tools(EXPLORE_TOOLS)` vs `llm.bind_tools(CODER_TOOLS)` |
| `.shared/utils.sh` | `tools/project.py` | Shared project detection utilities |
| `detect_project()` heuristic | `detect_project()` in Python | Same logic: check Cargo.toml → go.mod → package.json → etc. |
| LLM fallback for unknown projects | (omitted) | The agents themselves can reason about unknown project types |
### State & Memory
| Loki Concept | LangGraph Equivalent | Notes |
|---|---|---|
| Agent session (conversation history) | `SisyphusState.messages` | `Annotated[list, add_messages]` — the reducer appends instead of replacing |
| `agent_session: temp` | `MemorySaver` checkpointer | Loki's temp sessions are ephemeral; MemorySaver is in-memory (lost on restart) |
| Per-agent isolation | Per-node system prompt + tools | In Loki agents have separate sessions; in LangGraph they share messages but have different system prompts |
| `{{project_dir}}` variable | `SisyphusState.project_dir` | Loki interpolates variables into prompts; LangGraph stores them in state |
| `{{__tools__}}` injection | `llm.bind_tools()` | Loki injects tool descriptions into the prompt; LangChain attaches them to the API call |
### Orchestration
| Loki Concept | LangGraph Equivalent | Notes |
|---|---|---|
| Intent classification table | `RoutingDecision` structured output | Loki does this in free text; LangGraph forces typed JSON |
| Oracle triggers ("How should I...") | Supervisor prompt + structured output | Same trigger phrases, enforced via system prompt |
| Coder delegation format | Supervisor builds HumanMessage | The structured prompt (Goal/Reference Files/Conventions/Constraints) |
| `agent__spawn` (parallel) | `Send()` API | Dynamic fan-out to multiple nodes |
| Todo system (`todo__init`, etc.) | `SisyphusState.todos` | State field with a merge reducer |
| `auto_continue: true` | Supervisor loop (iteration counter) | Supervisor re-routes until FINISH or max iterations |
| `max_auto_continues: 25` | `MAX_ITERATIONS = 15` | Safety valve to prevent infinite loops |
| `user__ask` / `user__confirm` | `interrupt()` API | Pauses graph, surfaces question to caller, resumes with answer |
| Escalation (child → parent → user) | `interrupt()` in any node | Any node can pause; the caller handles the interaction |
### Execution Model
| Loki Concept | LangGraph Equivalent | Notes |
|---|---|---|
| `loki --agent sisyphus` | `python -m sisyphus_langchain.cli` | CLI entry point |
| REPL mode | `cli.py``repl()` | Interactive loop with thread persistence |
| One-shot mode | `cli.py``run_query()` | Single query, print result, exit |
| Streaming output | `graph.stream()` | LangGraph supports per-node streaming |
| `inject_spawn_instructions` | (always on) | System prompts are always included |
| `inject_todo_instructions` | (always on) | Todo instructions could be added to prompts |
## How the Execution Flow Works
### 1. User sends a message
```python
graph.invoke({"messages": [HumanMessage("Add a health check endpoint")]})
```
### 2. Supervisor classifies intent
The supervisor LLM reads the message and produces a `RoutingDecision`:
```json
{
"intent": "implementation",
"next_agent": "explore",
"delegation_notes": "Find existing API endpoint patterns, route structure, and health check conventions"
}
```
### 3. Supervisor routes via Command
```python
return Command(goto="explore", update={"intent": "implementation", "iteration_count": 1})
```
### 4. Explore agent runs
- Receives the full message history (including the user's request)
- Calls read-only tools (search_content, search_files, read_file)
- Returns findings in messages
### 5. Control returns to supervisor
The graph edge `explore → supervisor` fires automatically.
### 6. Supervisor reviews and routes again
Now it has explore's findings. It routes to coder with context:
```json
{
"intent": "implementation",
"next_agent": "coder",
"delegation_notes": "Implement health check endpoint following patterns found in src/routes/"
}
```
### 7. Coder implements
- Reads explore's findings from the message history
- Writes files via `write_file` tool
- Runs `verify_build` to check compilation
### 8. Supervisor verifies and finishes
```json
{
"intent": "implementation",
"next_agent": "FINISH",
"delegation_notes": "Added /health endpoint in src/routes/health.py. Build passes."
}
```
## Key Differences from Loki
### What LangGraph does better
1. **Declarative graph** — The topology is visible and debuggable. Loki's orchestration is emergent from the LLM's tool calls.
2. **Typed state**`SisyphusState` is a TypedDict with reducers. Loki's state is implicit in the conversation.
3. **Checkpointing** — Built-in persistence. Loki manages sessions manually.
4. **Time-travel debugging** — Inspect any checkpoint. Loki has no equivalent.
5. **Structured routing**`RoutingDecision` forces valid JSON. Loki relies on the LLM calling the right tool.
### What Loki does better
1. **True parallelism**`agent__spawn` runs multiple agents concurrently in separate threads. This LangGraph implementation is sequential (see [Parallel Execution](#parallel-execution) for how to add it).
2. **Agent isolation** — Each Loki agent has its own session, tools, and config. LangGraph nodes share state.
3. **Teammate messaging** — Loki agents can send messages to siblings. LangGraph nodes communicate only through shared state.
4. **Dynamic tool compilation** — Loki compiles bash/python/typescript tools at startup. LangChain tools are statically defined.
5. **Escalation protocol** — Loki's child-to-parent escalation is sophisticated. LangGraph's `interrupt()` is simpler but less structured.
6. **Task queues with dependencies** — Loki's `agent__task_create` supports dependency DAGs. LangGraph's routing is simpler (hub-and-spoke).
## Running It
### Prerequisites
```bash
# Python 3.11+
python --version
# Set your API key
export OPENAI_API_KEY="sk-..."
```
### Install
```bash
cd examples/langchain-sisyphus
# With pip
pip install -e .
# Or with uv (recommended)
uv pip install -e .
```
### Usage
```bash
# Interactive REPL (like `loki --agent sisyphus`)
sisyphus
# One-shot query
sisyphus "Find all TODO comments in the codebase"
# With custom models (cost optimization)
sisyphus --explore-model gpt-4o-mini --coder-model gpt-4o "Add input validation to the API"
# Programmatic usage
python -c "
from sisyphus_langchain import build_graph
from langchain_core.messages import HumanMessage
graph = build_graph()
result = graph.invoke({
'messages': [HumanMessage('What patterns does this codebase use?')],
'intent': 'ambiguous',
'next_agent': '',
'iteration_count': 0,
'todos': [],
'agent_outputs': {},
'final_output': '',
'project_dir': '.',
}, config={'configurable': {'thread_id': 'demo'}, 'recursion_limit': 50})
print(result['final_output'])
"
```
### Using Anthropic Models
Replace `ChatOpenAI` with `ChatAnthropic` in the agent factories:
```python
from langchain_anthropic import ChatAnthropic
# In agents/oracle.py:
llm = ChatAnthropic(model="claude-sonnet-4-20250514", temperature=0.2).bind_tools(ORACLE_TOOLS)
```
## Deployment
### Option 1: Standalone Script (Simplest)
Just run the CLI directly. No infrastructure needed.
```bash
sisyphus "Add a health check endpoint"
```
### Option 2: FastAPI Server
```python
# server.py
from fastapi import FastAPI
from langserve import add_routes
from sisyphus_langchain import build_graph
app = FastAPI(title="Sisyphus API")
graph = build_graph()
add_routes(app, graph, path="/agent")
# Run: uvicorn server:app --host 0.0.0.0 --port 8000
# Call: POST http://localhost:8000/agent/invoke
```
### Option 3: LangGraph Platform (Production)
Create a `langgraph.json` at the project root:
```json
{
"graphs": {
"sisyphus": "./sisyphus_langchain/graph.py:build_graph"
},
"dependencies": ["./sisyphus_langchain"],
"env": ".env"
}
```
Then deploy:
```bash
pip install langgraph-cli
langgraph deploy
```
This gives you:
- Durable checkpointing (PostgreSQL)
- Background runs
- Streaming API
- Zero-downtime deployments
- Built-in observability
### Option 4: Docker
```dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install -e .
CMD ["sisyphus"]
```
```bash
docker build -t sisyphus .
docker run -it -e OPENAI_API_KEY=$OPENAI_API_KEY sisyphus
```
## Parallel Execution
This implementation routes sequentially for simplicity. To add Loki-style parallel agent execution, use LangGraph's `Send()` API:
```python
from langgraph.types import Send
def supervisor_node(state):
# Fan out to multiple explore agents in parallel
# (like Loki's agent__spawn called multiple times)
return [
Send("explore", {
**state,
"messages": state["messages"] + [
HumanMessage("Find existing API endpoint patterns")
],
}),
Send("explore", {
**state,
"messages": state["messages"] + [
HumanMessage("Find data models and database patterns")
],
}),
]
```
This is equivalent to Loki's pattern of spawning multiple explore agents:
```
agent__spawn --agent explore --prompt "Find API patterns"
agent__spawn --agent explore --prompt "Find database patterns"
agent__collect --id <id1>
agent__collect --id <id2>
```
## Adding Human-in-the-Loop
To replicate Loki's `user__ask` / `user__confirm` tools, use LangGraph's `interrupt()`:
```python
from langgraph.types import interrupt
def supervisor_node(state):
# Pause and ask the user (like Loki's user__ask)
answer = interrupt({
"question": "How should we structure the authentication?",
"options": [
"JWT with httpOnly cookies (Recommended)",
"Session-based with Redis",
"OAuth2 with external provider",
],
})
# `answer` contains the user's selection when the graph resumes
```
## Project Structure
```
examples/langchain-sisyphus/
├── pyproject.toml # Dependencies & build config
├── README.md # This file
└── sisyphus_langchain/
├── __init__.py # Package entry point
├── cli.py # CLI (REPL + one-shot mode)
├── graph.py # Graph assembly (wires nodes + edges)
├── state.py # Shared state schema (TypedDict)
├── agents/
│ ├── __init__.py
│ ├── supervisor.py # Sisyphus orchestrator (intent → routing)
│ ├── explore.py # Read-only codebase researcher
│ ├── oracle.py # Architecture/debugging advisor
│ └── coder.py # Implementation worker
└── tools/
├── __init__.py
├── filesystem.py # File read/write/search/glob tools
└── project.py # Project detection, build, test tools
```
### File-to-Loki Mapping
| This Project | Loki Equivalent |
|---|---|
| `state.py` | Session context + todo state (implicit in Loki) |
| `graph.py` | `src/supervisor/mod.rs` (runtime orchestration) |
| `cli.py` | `src/main.rs` (CLI entry point) |
| `agents/supervisor.py` | `assets/agents/sisyphus/config.yaml` |
| `agents/explore.py` | `assets/agents/explore/config.yaml` + `tools.sh` |
| `agents/oracle.py` | `assets/agents/oracle/config.yaml` + `tools.sh` |
| `agents/coder.py` | `assets/agents/coder/config.yaml` + `tools.sh` |
| `tools/filesystem.py` | `assets/functions/tools/fs_*.sh` |
| `tools/project.py` | `assets/agents/.shared/utils.sh` + `sisyphus/tools.sh` |
## Further Reading
- [LangGraph Documentation](https://docs.langchain.com/langgraph/)
- [LangGraph Multi-Agent Tutorial](https://docs.langchain.com/langgraph/how-tos/multi-agent-systems)
- [Loki Agents Documentation](../../docs/AGENTS.md)
- [Loki Sisyphus README](../../assets/agents/sisyphus/README.md)
- [LangGraph Supervisor Library](https://github.com/langchain-ai/langgraph-supervisor-py)
@@ -0,0 +1,29 @@
[project]
name = "sisyphus-langchain"
version = "0.1.0"
description = "Loki's Sisyphus multi-agent orchestrator recreated in LangChain/LangGraph"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"langgraph>=0.3.0",
"langchain>=0.3.0",
"langchain-openai>=0.3.0",
"langchain-anthropic>=0.3.0",
"langchain-core>=0.3.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"ruff>=0.8.0",
]
server = [
"langgraph-api>=0.1.0",
]
[project.scripts]
sisyphus = "sisyphus_langchain.cli:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
@@ -0,0 +1,5 @@
"""Sisyphus multi-agent orchestrator — a LangGraph recreation of Loki's Sisyphus agent."""
from sisyphus_langchain.graph import build_graph
__all__ = ["build_graph"]
@@ -0,0 +1 @@
"""Agent node definitions for the Sisyphus orchestrator."""
@@ -0,0 +1,145 @@
"""
Coder agent node — the implementation worker.
Loki equivalent: assets/agents/coder/config.yaml + tools.sh
In Loki, the coder is the ONLY agent that modifies files. It:
- Receives a structured prompt from sisyphus with code patterns to follow
- Writes files via the write_file tool (never pastes code in chat)
- Verifies builds after every change
- Signals CODER_COMPLETE or CODER_FAILED
In LangGraph, coder is a node with write-capable tools (read_file, write_file,
search_content, execute_command, verify_build). The supervisor formats a
structured delegation prompt (Goal / Reference Files / Code Patterns /
Conventions / Constraints) and routes to this node.
Key Loki→LangGraph mapping:
- Loki's "Coder Delegation Format" → the supervisor builds this as a
HumanMessage before routing to the coder node.
- Loki's auto_continue (up to 15) → the supervisor can re-route to coder
if verification fails, up to iteration_count limits.
- Loki's todo system for multi-file changes → the coder updates
state["todos"] as it completes each file.
"""
from __future__ import annotations
from langchain_core.messages import SystemMessage
from langchain_openai import ChatOpenAI
from sisyphus_langchain.state import SisyphusState
from sisyphus_langchain.tools.filesystem import (
read_file,
search_content,
search_files,
write_file,
)
from sisyphus_langchain.tools.project import (
execute_command,
run_tests,
verify_build,
)
# ---------------------------------------------------------------------------
# System prompt — faithfully mirrors coder/config.yaml
# ---------------------------------------------------------------------------
CODER_SYSTEM_PROMPT = """\
You are a senior engineer. You write code that works on the first try.
## Your Mission
Given an implementation task:
1. Check for context provided in the conversation (patterns, conventions, reference files).
2. Fill gaps only — read files NOT already covered in context.
3. Write the code using the write_file tool (NEVER output code in chat).
4. Verify it compiles/builds using verify_build.
5. Provide a summary of what you implemented.
## Using Provided Context (IMPORTANT)
Your prompt often contains prior findings from the explore agent: file paths,
code patterns, and conventions.
**If context is provided:**
1. Use it as your primary reference. Don't re-read files already summarized.
2. Follow the code patterns shown — snippets in context ARE the style guide.
3. Read referenced files ONLY IF you need more detail (full signatures, imports).
4. If context includes a "Conventions" section, follow it exactly.
**If context is NOT provided or is too vague:**
Fall back to self-exploration: search for similar files, read 1-2 examples,
match their style.
## Writing Code
CRITICAL: Write code using the write_file tool. NEVER paste code in chat.
## Pattern Matching
Before writing ANY file:
1. Find a similar existing file.
2. Match its style: imports, naming, structure.
3. Follow the same patterns exactly.
## Verification
After writing files:
1. Run verify_build to check compilation.
2. If it fails, fix the error (minimal change).
3. Don't move on until build passes.
## Rules
1. Write code via tools — never output code to chat.
2. Follow patterns — read existing files first.
3. Verify builds — don't finish without checking.
4. Minimal fixes — if build fails, fix precisely.
5. No refactoring — only implement what's asked.
"""
# Full tool set — coder gets write access and command execution
CODER_TOOLS = [
read_file,
write_file,
search_content,
search_files,
execute_command,
verify_build,
run_tests,
]
def create_coder_node(model_name: str = "gpt-4o", temperature: float = 0.1):
"""
Factory that returns a coder node function.
Coder needs a capable model because it writes production code. In Loki,
coder uses the same model as the parent by default.
Args:
model_name: Model identifier.
temperature: LLM temperature (Loki coder uses 0.1 for consistency).
"""
llm = ChatOpenAI(model=model_name, temperature=temperature).bind_tools(CODER_TOOLS)
def coder_node(state: SisyphusState) -> dict:
"""
LangGraph node: run the coder agent.
Reads conversation history (including the supervisor's structured
delegation prompt), invokes the LLM with write-capable tools,
and returns the result.
"""
response = llm.invoke(
[SystemMessage(content=CODER_SYSTEM_PROMPT)] + state["messages"]
)
return {
"messages": [response],
"agent_outputs": {
**state.get("agent_outputs", {}),
"coder": response.content,
},
}
return coder_node
@@ -0,0 +1,110 @@
"""
Explore agent node — the read-only codebase researcher.
Loki equivalent: assets/agents/explore/config.yaml + tools.sh
In Loki, the explore agent is spawned via `agent__spawn --agent explore --prompt "..."`
and runs as an isolated subprocess with its own session. It ends with
"EXPLORE_COMPLETE" so the parent knows it's finished.
In LangGraph, the explore agent is a *node* in the graph. The supervisor routes
to it via `Command(goto="explore")`. It reads the latest message (the supervisor's
delegation prompt), calls the LLM with read-only tools, and writes its findings
back to the shared message list. The graph edge then returns control to the
supervisor.
Key differences from Loki:
- No isolated session — shares the graph's message list (but has its own
system prompt and tool set, just like Loki's per-agent config).
- No "EXPLORE_COMPLETE" sentinel — the graph edge handles control flow.
- No output summarization — LangGraph's state handles context management.
"""
from __future__ import annotations
from langchain_core.messages import SystemMessage
from langchain_openai import ChatOpenAI
from sisyphus_langchain.state import SisyphusState
from sisyphus_langchain.tools.filesystem import (
list_directory,
read_file,
search_content,
search_files,
)
# ---------------------------------------------------------------------------
# System prompt — faithfully mirrors explore/config.yaml
# ---------------------------------------------------------------------------
EXPLORE_SYSTEM_PROMPT = """\
You are a codebase explorer. Your job: Search, find, report. Nothing else.
## Your Mission
Given a search task, you:
1. Search for relevant files and patterns
2. Read key files to understand structure
3. Report findings concisely
## Strategy
1. **Find first, read second** — Never read a file without knowing why.
2. **Use search_content to locate** — find exactly where things are defined.
3. **Use search_files to discover** — find files by name pattern.
4. **Read targeted sections** — use offset and limit to read only relevant lines.
5. **Never read entire large files** — if a file is 500+ lines, read the relevant section only.
## Output Format
Always end your response with a structured findings summary:
FINDINGS:
- [Key finding 1]
- [Key finding 2]
- Relevant files: [list of paths]
## Rules
1. Be fast — don't read every file, read representative ones.
2. Be focused — answer the specific question asked.
3. Be concise — report findings, not your process.
4. Never modify files — you are read-only.
5. Limit reads — max 5 file reads per exploration.
"""
# Read-only tools — mirrors explore's tool set (no write_file, no execute_command)
EXPLORE_TOOLS = [read_file, search_content, search_files, list_directory]
def create_explore_node(model_name: str = "gpt-4o-mini", temperature: float = 0.1):
"""
Factory that returns an explore node function bound to a specific model.
In Loki, the model is set per-agent in config.yaml. Here we parameterize it
so you can use a cheap model for exploration (cost optimization).
Args:
model_name: OpenAI model identifier.
temperature: LLM temperature (Loki explore uses 0.1).
"""
llm = ChatOpenAI(model=model_name, temperature=temperature).bind_tools(EXPLORE_TOOLS)
def explore_node(state: SisyphusState) -> dict:
"""
LangGraph node: run the explore agent.
Reads the conversation history, applies the explore system prompt,
invokes the LLM with read-only tools, and returns the response.
"""
response = llm.invoke(
[SystemMessage(content=EXPLORE_SYSTEM_PROMPT)] + state["messages"]
)
return {
"messages": [response],
"agent_outputs": {
**state.get("agent_outputs", {}),
"explore": response.content,
},
}
return explore_node
@@ -0,0 +1,124 @@
"""
Oracle agent node — the high-IQ architecture and debugging advisor.
Loki equivalent: assets/agents/oracle/config.yaml + tools.sh
In Loki, the oracle is a READ-ONLY advisor spawned for:
- Architecture decisions and multi-system tradeoffs
- Complex debugging (after 2+ failed fix attempts)
- Code/design review
- Risk assessment
It uses temperature 0.2 (slightly higher than explore/coder for more creative
reasoning) and ends with "ORACLE_COMPLETE".
In LangGraph, oracle is a node that receives the full message history, reasons
about the problem, and writes structured advice back. It has read-only tools
only — it never modifies files.
Key Loki→LangGraph mapping:
- Loki oracle triggers (the "MUST spawn oracle when..." rules in sisyphus)
become routing conditions in the supervisor node.
- Oracle's structured output format (Analysis/Recommendation/Reasoning/Risks)
is enforced via the system prompt, same as in Loki.
"""
from __future__ import annotations
from langchain_core.messages import SystemMessage
from langchain_openai import ChatOpenAI
from sisyphus_langchain.state import SisyphusState
from sisyphus_langchain.tools.filesystem import (
list_directory,
read_file,
search_content,
search_files,
)
# ---------------------------------------------------------------------------
# System prompt — faithfully mirrors oracle/config.yaml
# ---------------------------------------------------------------------------
ORACLE_SYSTEM_PROMPT = """\
You are Oracle — a senior architect and debugger consulted for complex decisions.
## Your Role
You are READ-ONLY. You analyze, advise, and recommend. You do NOT implement.
## When You're Consulted
1. **Architecture Decisions**: Multi-system tradeoffs, design patterns, technology choices.
2. **Complex Debugging**: After 2+ failed fix attempts, deep analysis needed.
3. **Code Review**: Evaluating proposed designs or implementations.
4. **Risk Assessment**: Security, performance, or reliability concerns.
## Your Process
1. **Understand**: Read relevant code, understand the full context.
2. **Analyze**: Consider multiple angles and tradeoffs.
3. **Recommend**: Provide clear, actionable advice.
4. **Justify**: Explain your reasoning.
## Output Format
Structure your response as:
## Analysis
[Your understanding of the situation]
## Recommendation
[Clear, specific advice]
## Reasoning
[Why this is the right approach]
## Risks/Considerations
[What to watch out for]
## Rules
1. Never modify files — you advise, others implement.
2. Be thorough — read all relevant context before advising.
3. Be specific — general advice isn't helpful.
4. Consider tradeoffs — there are rarely perfect solutions.
5. Stay focused — answer the specific question asked.
"""
# Read-only tools — same set as explore (oracle never writes)
ORACLE_TOOLS = [read_file, search_content, search_files, list_directory]
def create_oracle_node(model_name: str = "gpt-4o", temperature: float = 0.2):
"""
Factory that returns an oracle node function.
Oracle uses a more expensive model than explore because it needs deeper
reasoning. In Loki, the model is inherited from the global config unless
overridden in oracle/config.yaml.
Args:
model_name: Model identifier (use a strong reasoning model).
temperature: LLM temperature (Loki oracle uses 0.2).
"""
llm = ChatOpenAI(model=model_name, temperature=temperature).bind_tools(ORACLE_TOOLS)
def oracle_node(state: SisyphusState) -> dict:
"""
LangGraph node: run the oracle agent.
Reads conversation history, applies the oracle system prompt,
invokes the LLM, and returns structured advice.
"""
response = llm.invoke(
[SystemMessage(content=ORACLE_SYSTEM_PROMPT)] + state["messages"]
)
return {
"messages": [response],
"agent_outputs": {
**state.get("agent_outputs", {}),
"oracle": response.content,
},
}
return oracle_node
@@ -0,0 +1,227 @@
"""
Sisyphus supervisor node — the orchestrator that classifies intent and routes.
Loki equivalent: assets/agents/sisyphus/config.yaml
This is the brain of the system. In Loki, Sisyphus is the top-level agent that:
1. Classifies every incoming request (trivial / exploration / implementation /
architecture / ambiguous)
2. Routes to the appropriate sub-agent (explore, coder, oracle)
3. Manages the todo list for multi-step tasks
4. Verifies results and decides when the task is complete
In LangGraph, the supervisor is a node that returns `Command(goto="agent_name")`
to route control. This replaces Loki's `agent__spawn` + `agent__collect` pattern
with a declarative graph edge.
Key Loki→LangGraph mapping:
- agent__spawn --agent explore → Command(goto="explore")
- agent__spawn --agent coder → Command(goto="coder")
- agent__spawn --agent oracle → Command(goto="oracle")
- agent__check / agent__collect → (implicit: graph edges return to supervisor)
- todo__init / todo__add → state["todos"] updates
- user__ask / user__confirm → interrupt() for human-in-the-loop
Parallel execution note:
Loki can spawn multiple explore agents in parallel. In LangGraph, you'd use
the Send() API for dynamic fan-out. For simplicity, this implementation uses
sequential routing. See the README for how to add parallel fan-out.
"""
from __future__ import annotations
from typing import Literal
from langchain_core.messages import SystemMessage
from langchain_openai import ChatOpenAI
from langgraph.types import Command
from pydantic import BaseModel, Field
from sisyphus_langchain.state import SisyphusState
# ---------------------------------------------------------------------------
# Maximum iterations before forcing completion (safety valve)
# Mirrors Loki's max_auto_continues: 25
# ---------------------------------------------------------------------------
MAX_ITERATIONS = 15
# ---------------------------------------------------------------------------
# Structured output schema for the supervisor's routing decision.
#
# In Loki, the supervisor is an LLM that produces free-text and calls tools
# like agent__spawn. In LangGraph, we use structured output to force the
# LLM into a typed routing decision — more reliable than parsing free text.
# ---------------------------------------------------------------------------
class RoutingDecision(BaseModel):
"""The supervisor's decision about what to do next."""
intent: Literal["trivial", "exploration", "implementation", "architecture", "ambiguous"] = Field(
description="Classified intent of the user's request."
)
next_agent: Literal["explore", "oracle", "coder", "FINISH"] = Field(
description=(
"Which agent to route to. 'explore' for research/discovery, "
"'oracle' for architecture/design/debugging advice, "
"'coder' for implementation, 'FINISH' if the task is complete."
)
)
delegation_notes: str = Field(
description=(
"Brief instructions for the target agent: what to look for (explore), "
"what to analyze (oracle), or what to implement (coder). "
"For FINISH, summarize what was accomplished."
)
)
# ---------------------------------------------------------------------------
# Supervisor system prompt — faithfully mirrors sisyphus/config.yaml
# ---------------------------------------------------------------------------
SUPERVISOR_SYSTEM_PROMPT = """\
You are Sisyphus — an orchestrator that drives coding tasks to completion.
Your job: Classify → Delegate → Verify → Complete.
## Intent Classification (BEFORE every action)
| Type | Signal | Action |
|-----------------|-----------------------------------------------------|----------------------|
| trivial | Single file, known location, typo fix | Route to FINISH |
| exploration | "Find X", "Where is Y", "List all Z" | Route to explore |
| implementation | "Add feature", "Fix bug", "Write code" | Route to coder |
| architecture | See oracle triggers below | Route to oracle |
| ambiguous | Unclear scope, multiple interpretations | Route to FINISH with a clarifying question |
## Oracle Triggers (MUST route to oracle when you see these)
Route to oracle ANY time the user asks about:
- "How should I..." / "What's the best way to..." — design/approach questions
- "Why does X keep..." / "What's wrong with..." — complex debugging
- "Should I use X or Y?" — technology or pattern choices
- "How should this be structured?" — architecture
- "Review this" / "What do you think of..." — code/design review
- Tradeoff questions, multi-component questions, vague/open-ended questions
## Agent Specializations
| Agent | Use For |
|---------|-----------------------------------------------|
| explore | Find patterns, understand code, search |
| coder | Write/edit files, implement features |
| oracle | Architecture decisions, complex debugging |
## Workflow Patterns
### Implementation task: explore → coder
1. Route to explore to find existing patterns and conventions.
2. Review explore findings.
3. Route to coder with a structured prompt including the explore findings.
4. Verify the coder's output (check for CODER_COMPLETE or CODER_FAILED).
### Architecture question: explore + oracle
1. Route to explore to find relevant code.
2. Route to oracle with the explore findings for analysis.
### Simple question: oracle directly
For pure design/architecture questions, route to oracle directly.
## Rules
1. Always classify before acting.
2. You are a coordinator, not an implementer.
3. Route to oracle for ANY design/architecture question.
4. When routing to coder, include code patterns from explore findings.
5. Route to FINISH when the task is fully addressed.
## Current State
Iteration: {iteration_count}/{max_iterations}
Previous agent outputs: {agent_outputs}
"""
def create_supervisor_node(model_name: str = "gpt-4o", temperature: float = 0.1):
"""
Factory that returns a supervisor node function.
The supervisor uses a capable model for accurate routing.
Args:
model_name: Model identifier.
temperature: LLM temperature (low for consistent routing).
"""
llm = ChatOpenAI(model=model_name, temperature=temperature).with_structured_output(
RoutingDecision
)
def supervisor_node(
state: SisyphusState,
) -> Command[Literal["explore", "oracle", "coder", "__end__"]]:
"""
LangGraph node: the Sisyphus supervisor.
Classifies the user's intent, decides which agent to route to,
and returns a Command that directs graph execution.
"""
iteration = state.get("iteration_count", 0)
# Safety valve — prevent infinite loops
if iteration >= MAX_ITERATIONS:
return Command(
goto="__end__",
update={
"final_output": "Reached maximum iterations. Here's what was accomplished:\n"
+ "\n".join(
f"- {k}: {v[:200]}" for k, v in state.get("agent_outputs", {}).items()
),
},
)
# Format the system prompt with current state
prompt = SUPERVISOR_SYSTEM_PROMPT.format(
iteration_count=iteration,
max_iterations=MAX_ITERATIONS,
agent_outputs=_summarize_outputs(state.get("agent_outputs", {})),
)
# Invoke the LLM to get a structured routing decision
decision: RoutingDecision = llm.invoke(
[SystemMessage(content=prompt)] + state["messages"]
)
# Route to FINISH
if decision.next_agent == "FINISH":
return Command(
goto="__end__",
update={
"intent": decision.intent,
"next_agent": "FINISH",
"final_output": decision.delegation_notes,
},
)
# Route to a worker agent
return Command(
goto=decision.next_agent,
update={
"intent": decision.intent,
"next_agent": decision.next_agent,
"iteration_count": iteration + 1,
},
)
return supervisor_node
def _summarize_outputs(outputs: dict[str, str]) -> str:
"""Summarize agent outputs for the supervisor's context window."""
if not outputs:
return "(none yet)"
parts = []
for agent, output in outputs.items():
# Truncate long outputs to keep supervisor context manageable
# This mirrors Loki's summarization_threshold behavior
if len(output) > 2000:
output = output[:2000] + "... (truncated)"
parts.append(f"[{agent}]: {output}")
return "\n\n".join(parts)
@@ -0,0 +1,155 @@
"""
CLI entry point for the Sisyphus LangChain agent.
This mirrors Loki's `loki --agent sisyphus` entry point.
In Loki:
loki --agent sisyphus
# Starts a REPL with the sisyphus agent loaded
In this LangChain version:
python -m sisyphus_langchain.cli
# or: sisyphus (if installed via pip)
Usage:
# Interactive REPL mode
sisyphus
# One-shot query
sisyphus "Add a health check endpoint to the API"
# With custom models
sisyphus --supervisor-model gpt-4o --explore-model gpt-4o-mini "Find auth patterns"
Environment variables:
OPENAI_API_KEY — Required for OpenAI models
ANTHROPIC_API_KEY — Required if using Anthropic models
"""
from __future__ import annotations
import argparse
import sys
import uuid
from langchain_core.messages import HumanMessage
from sisyphus_langchain.graph import build_graph
def run_query(graph, query: str, thread_id: str) -> str:
"""
Run a single query through the Sisyphus graph.
Args:
graph: Compiled LangGraph.
query: User's natural language request.
thread_id: Session identifier for checkpointing.
Returns:
The final output string.
"""
result = graph.invoke(
{
"messages": [HumanMessage(content=query)],
"intent": "ambiguous",
"next_agent": "",
"iteration_count": 0,
"todos": [],
"agent_outputs": {},
"final_output": "",
"project_dir": ".",
},
config={
"configurable": {"thread_id": thread_id},
"recursion_limit": 50,
},
)
return result.get("final_output", "(no output)")
def repl(graph, thread_id: str) -> None:
"""
Interactive REPL loop — mirrors Loki's REPL mode.
Maintains conversation across turns via the thread_id (checkpointer).
"""
print("Sisyphus (LangChain) — type 'quit' to exit")
print("=" * 50)
while True:
try:
query = input("\n> ").strip()
except (EOFError, KeyboardInterrupt):
print("\nBye.")
break
if not query:
continue
if query.lower() in ("quit", "exit", "q"):
print("Bye.")
break
try:
output = run_query(graph, query, thread_id)
print(f"\n{output}")
except Exception as e:
print(f"\nError: {e}")
def main() -> None:
"""CLI entry point."""
parser = argparse.ArgumentParser(
description="Sisyphus — multi-agent coding orchestrator (LangChain edition)"
)
parser.add_argument(
"query",
nargs="?",
help="One-shot query (omit for REPL mode)",
)
parser.add_argument(
"--supervisor-model",
default="gpt-4o",
help="Model for the supervisor (default: gpt-4o)",
)
parser.add_argument(
"--explore-model",
default="gpt-4o-mini",
help="Model for the explore agent (default: gpt-4o-mini)",
)
parser.add_argument(
"--oracle-model",
default="gpt-4o",
help="Model for the oracle agent (default: gpt-4o)",
)
parser.add_argument(
"--coder-model",
default="gpt-4o",
help="Model for the coder agent (default: gpt-4o)",
)
parser.add_argument(
"--thread-id",
default=None,
help="Session thread ID for persistence (auto-generated if omitted)",
)
args = parser.parse_args()
graph = build_graph(
supervisor_model=args.supervisor_model,
explore_model=args.explore_model,
oracle_model=args.oracle_model,
coder_model=args.coder_model,
)
thread_id = args.thread_id or f"sisyphus-{uuid.uuid4().hex[:8]}"
if args.query:
output = run_query(graph, args.query, thread_id)
print(output)
else:
repl(graph, thread_id)
if __name__ == "__main__":
main()
@@ -0,0 +1,115 @@
"""
Graph assembly — wires together the supervisor and worker nodes.
This is the LangGraph equivalent of Loki's runtime agent execution engine
(src/supervisor/mod.rs + src/config/request_context.rs).
In Loki, the runtime:
1. Loads the agent config (config.yaml)
2. Compiles tools (tools.sh → binary)
3. Starts a chat loop: user → LLM → tool calls → LLM → ...
4. For orchestrators with can_spawn_agents: true, the supervisor module
manages child agent lifecycle (spawn, check, collect, cancel).
In LangGraph, all of this is declarative:
1. Define nodes (supervisor, explore, oracle, coder)
2. Define edges (workers always return to supervisor)
3. Compile the graph (with optional checkpointer for persistence)
4. Invoke with initial state
The graph topology:
┌─────────────────────────────────────────────┐
│ SUPERVISOR │
│ (classifies intent, routes to workers) │
└─────┬──────────┬──────────┬─────────────────┘
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│EXPLORE │ │ ORACLE │ │ CODER │
│(search)│ │(advise)│ │(build) │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
(back to supervisor)
Every worker returns to the supervisor. The supervisor decides what to do next:
route to another worker, or end the graph.
"""
from __future__ import annotations
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, START, StateGraph
from sisyphus_langchain.agents.coder import create_coder_node
from sisyphus_langchain.agents.explore import create_explore_node
from sisyphus_langchain.agents.oracle import create_oracle_node
from sisyphus_langchain.agents.supervisor import create_supervisor_node
from sisyphus_langchain.state import SisyphusState
def build_graph(
*,
supervisor_model: str = "gpt-4o",
explore_model: str = "gpt-4o-mini",
oracle_model: str = "gpt-4o",
coder_model: str = "gpt-4o",
use_checkpointer: bool = True,
):
"""
Build and compile the Sisyphus LangGraph.
This is the main entry point for creating the agent system. It wires
together all nodes and edges, optionally adds a checkpointer for
persistence, and returns a compiled graph ready to invoke.
Args:
supervisor_model: Model for the routing supervisor.
explore_model: Model for the explore agent (can be cheaper).
oracle_model: Model for the oracle agent (should be strong).
coder_model: Model for the coder agent.
use_checkpointer: Whether to add MemorySaver for session persistence.
Returns:
A compiled LangGraph ready to .invoke() or .stream().
Model cost optimization (mirrors Loki's per-agent model config):
- supervisor: expensive (accurate routing is critical)
- explore: cheap (just searching, not reasoning deeply)
- oracle: expensive (deep reasoning, architecture advice)
- coder: expensive (writing correct code matters)
"""
# Create the graph builder with our typed state
builder = StateGraph(SisyphusState)
# ── Register nodes ─────────────────────────────────────────────────
# Each node is a function that takes state and returns state updates.
# This mirrors Loki's agent registration (agents are discovered by
# their config.yaml in the agents/ directory).
builder.add_node("supervisor", create_supervisor_node(supervisor_model))
builder.add_node("explore", create_explore_node(explore_model))
builder.add_node("oracle", create_oracle_node(oracle_model))
builder.add_node("coder", create_coder_node(coder_model))
# ── Define edges ───────────────────────────────────────────────────
# Entry point: every invocation starts at the supervisor
builder.add_edge(START, "supervisor")
# Workers always return to supervisor (the hub-and-spoke pattern).
# In Loki, this is implicit: agent__collect returns output to the parent,
# and the parent (sisyphus) decides what to do next.
builder.add_edge("explore", "supervisor")
builder.add_edge("oracle", "supervisor")
builder.add_edge("coder", "supervisor")
# The supervisor node itself uses Command(goto=...) to route,
# so we don't need add_conditional_edges — the Command API
# handles dynamic routing internally.
# ── Compile ────────────────────────────────────────────────────────
checkpointer = MemorySaver() if use_checkpointer else None
graph = builder.compile(checkpointer=checkpointer)
return graph
@@ -0,0 +1,100 @@
"""
Shared state schema for the Sisyphus orchestrator graph.
In LangGraph, state is the single source of truth that flows through every node.
This is analogous to Loki's per-agent session context, but unified into one typed
dictionary that the entire graph shares.
Loki Concept Mapping:
- Loki session context → SisyphusState (TypedDict)
- Loki todo__init / todo__add → SisyphusState.todos list
- Loki agent__spawn outputs → SisyphusState.agent_outputs dict
- Loki intent classification → SisyphusState.intent field
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Annotated, Literal
from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages
from typing_extensions import TypedDict
# ---------------------------------------------------------------------------
# Intent types — mirrors Loki's Sisyphus classification table
# ---------------------------------------------------------------------------
IntentType = Literal[
"trivial", # Single file, known location, typo fix → handle yourself
"exploration", # "Find X", "Where is Y" → spawn explore
"implementation", # "Add feature", "Fix bug" → spawn coder
"architecture", # Design questions, oracle triggers → spawn oracle
"ambiguous", # Unclear scope → ask user
]
# ---------------------------------------------------------------------------
# Todo item — mirrors Loki's built-in todo system
# ---------------------------------------------------------------------------
@dataclass
class TodoItem:
"""A single task in the orchestrator's todo list."""
id: int
task: str
done: bool = False
def _merge_todos(existing: list[TodoItem], new: list[TodoItem]) -> list[TodoItem]:
"""
Reducer for the todos field.
LangGraph requires a reducer for any state field that can be written by
multiple nodes. This merges by id: if a todo with the same id already
exists, the incoming version wins (allows marking done).
"""
by_id = {t.id: t for t in existing}
for t in new:
by_id[t.id] = t
return list(by_id.values())
# ---------------------------------------------------------------------------
# Core graph state
# ---------------------------------------------------------------------------
class SisyphusState(TypedDict):
"""
The shared state that flows through every node in the Sisyphus graph.
Annotated fields use *reducers* — functions that merge concurrent writes.
Without reducers, parallel node outputs would overwrite each other.
"""
# Conversation history — the `add_messages` reducer appends new messages
# instead of replacing the list. This is critical: every node adds its
# response here, and downstream nodes see the full history.
#
# Loki equivalent: each agent's chat session accumulates messages the same
# way, but messages are scoped per-agent. In LangGraph the shared message
# list IS the inter-agent communication channel.
messages: Annotated[list[BaseMessage], add_messages]
# Classified intent for the current request
intent: IntentType
# Which agent the supervisor routed to last
next_agent: str
# Iteration counter — safety valve analogous to Loki's max_auto_continues
iteration_count: int
# Todo list for multi-step tracking (mirrors Loki's todo__* tools)
todos: Annotated[list[TodoItem], _merge_todos]
# Accumulated outputs from sub-agent nodes, keyed by agent name.
# The supervisor reads these to decide what to do next.
agent_outputs: dict[str, str]
# Final synthesized answer to return to the user
final_output: str
# The working directory / project path (mirrors Loki's project_dir variable)
project_dir: str
@@ -0,0 +1 @@
"""Tool definitions for Sisyphus agents."""
@@ -0,0 +1,175 @@
"""
Filesystem tools for Sisyphus agents.
These are the LangChain equivalents of Loki's global tools:
- fs_read.sh → read_file
- fs_grep.sh → search_content
- fs_glob.sh → search_files
- fs_ls.sh → list_directory
- fs_write.sh → write_file
- fs_patch.sh → (omitted — write_file covers full rewrites)
Loki Concept Mapping:
Loki tools are bash scripts with @cmd annotations that Loki's compiler
turns into function-calling declarations. In LangChain, we use the @tool
decorator which serves the same purpose: it generates the JSON schema
that the LLM sees, and wraps the Python function for execution.
"""
from __future__ import annotations
import fnmatch
import os
import re
import subprocess
from langchain_core.tools import tool
@tool
def read_file(path: str, offset: int = 1, limit: int = 200) -> str:
"""Read a file's contents with optional line range.
Args:
path: Path to the file (absolute or relative to cwd).
offset: 1-based line number to start from.
limit: Maximum number of lines to return.
"""
path = os.path.expanduser(path)
if not os.path.isfile(path):
return f"Error: file not found: {path}"
try:
with open(path, "r", encoding="utf-8", errors="replace") as f:
lines = f.readlines()
except Exception as e:
return f"Error reading {path}: {e}"
total = len(lines)
start = max(0, offset - 1)
end = min(total, start + limit)
selected = lines[start:end]
result = f"File: {path} (lines {start + 1}-{end} of {total})\n\n"
for i, line in enumerate(selected, start=start + 1):
result += f"{i}: {line}"
if end < total:
result += f"\n... truncated ({total} total lines)"
return result
@tool
def write_file(path: str, content: str) -> str:
"""Write complete contents to a file, creating parent directories as needed.
Args:
path: Path for the file.
content: Complete file contents to write.
"""
path = os.path.expanduser(path)
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
try:
with open(path, "w", encoding="utf-8") as f:
f.write(content)
return f"Wrote: {path}"
except Exception as e:
return f"Error writing {path}: {e}"
@tool
def search_content(pattern: str, directory: str = ".", file_type: str = "") -> str:
"""Search for a text/regex pattern in files under a directory.
Args:
pattern: Text or regex pattern to search for.
directory: Root directory to search in.
file_type: Optional file extension filter (e.g. "py", "rs").
"""
directory = os.path.expanduser(directory)
cmd = ["grep", "-rn"]
if file_type:
cmd += [f"--include=*.{file_type}"]
cmd += [pattern, directory]
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
lines = result.stdout.strip().splitlines()
except Exception as e:
return f"Error: {e}"
# Filter noise
noise = {"/.git/", "/node_modules/", "/target/", "/dist/", "/__pycache__/"}
filtered = [l for l in lines if not any(n in l for n in noise)][:30]
if not filtered:
return "No matches found."
return "\n".join(filtered)
@tool
def search_files(pattern: str, directory: str = ".") -> str:
"""Find files matching a glob pattern.
Args:
pattern: Glob pattern (e.g. '*.py', 'config*', '*test*').
directory: Directory to search in.
"""
directory = os.path.expanduser(directory)
noise = {".git", "node_modules", "target", "dist", "__pycache__"}
matches: list[str] = []
for root, dirs, files in os.walk(directory):
dirs[:] = [d for d in dirs if d not in noise]
for name in files:
if fnmatch.fnmatch(name, pattern):
matches.append(os.path.join(root, name))
if len(matches) >= 25:
break
if len(matches) >= 25:
break
if not matches:
return "No files found."
return "\n".join(matches)
@tool
def list_directory(path: str = ".", max_depth: int = 3) -> str:
"""List directory tree structure.
Args:
path: Directory to list.
max_depth: Maximum depth to recurse.
"""
path = os.path.expanduser(path)
if not os.path.isdir(path):
return f"Error: not a directory: {path}"
noise = {".git", "node_modules", "target", "dist", "__pycache__", ".venv", "venv"}
lines: list[str] = []
def _walk(dir_path: str, prefix: str, depth: int) -> None:
if depth > max_depth:
return
try:
entries = sorted(os.listdir(dir_path))
except PermissionError:
return
dirs = [e for e in entries if os.path.isdir(os.path.join(dir_path, e)) and e not in noise]
files = [e for e in entries if os.path.isfile(os.path.join(dir_path, e))]
for f in files[:20]:
lines.append(f"{prefix}{f}")
if len(files) > 20:
lines.append(f"{prefix}... ({len(files) - 20} more files)")
for d in dirs:
lines.append(f"{prefix}{d}/")
_walk(os.path.join(dir_path, d), prefix + " ", depth + 1)
lines.append(f"{os.path.basename(path) or path}/")
_walk(path, " ", 1)
return "\n".join(lines[:200])
@@ -0,0 +1,142 @@
"""
Project detection and build/test tools.
These mirror Loki's .shared/utils.sh detect_project() heuristic and the
sisyphus/coder tools.sh run_build / run_tests / verify_build commands.
Loki Concept Mapping:
Loki uses a heuristic cascade: check for Cargo.toml → go.mod → package.json
etc., then falls back to an LLM call for unknown projects. We replicate the
heuristic portion here. The LLM fallback is omitted since the agents
themselves can reason about unknown project types.
"""
from __future__ import annotations
import json
import os
import subprocess
from langchain_core.tools import tool
# ---------------------------------------------------------------------------
# Project detection (mirrors _detect_heuristic in utils.sh)
# ---------------------------------------------------------------------------
_HEURISTICS: list[tuple[str, dict[str, str]]] = [
("Cargo.toml", {"type": "rust", "build": "cargo build", "test": "cargo test", "check": "cargo check"}),
("go.mod", {"type": "go", "build": "go build ./...", "test": "go test ./...", "check": "go vet ./..."}),
("package.json", {"type": "nodejs", "build": "npm run build", "test": "npm test", "check": "npm run lint"}),
("pyproject.toml", {"type": "python", "build": "", "test": "pytest", "check": "ruff check ."}),
("pom.xml", {"type": "java", "build": "mvn compile", "test": "mvn test", "check": "mvn verify"}),
("Makefile", {"type": "make", "build": "make build", "test": "make test", "check": "make lint"}),
]
def detect_project(directory: str) -> dict[str, str]:
"""Detect project type and return build/test commands."""
for marker, info in _HEURISTICS:
if os.path.exists(os.path.join(directory, marker)):
return info
return {"type": "unknown", "build": "", "test": "", "check": ""}
@tool
def get_project_info(directory: str = ".") -> str:
"""Detect the project type and show structure overview.
Args:
directory: Project root directory.
"""
directory = os.path.expanduser(directory)
info = detect_project(directory)
result = f"Project: {os.path.abspath(directory)}\n"
result += f"Type: {info['type']}\n"
result += f"Build: {info['build'] or '(none)'}\n"
result += f"Test: {info['test'] or '(none)'}\n"
result += f"Check: {info['check'] or '(none)'}\n"
return result
def _run_project_command(directory: str, command_key: str) -> str:
"""Run a detected project command (build/test/check)."""
directory = os.path.expanduser(directory)
info = detect_project(directory)
cmd = info.get(command_key, "")
if not cmd:
return f"No {command_key} command detected for this project."
try:
result = subprocess.run(
cmd,
shell=True,
capture_output=True,
text=True,
cwd=directory,
timeout=300,
)
output = result.stdout + result.stderr
status = "SUCCESS" if result.returncode == 0 else f"FAILED (exit {result.returncode})"
return f"Running: {cmd}\n\n{output}\n\n{command_key.upper()}: {status}"
except subprocess.TimeoutExpired:
return f"{command_key.upper()}: TIMEOUT after 300s"
except Exception as e:
return f"{command_key.upper()}: ERROR — {e}"
@tool
def run_build(directory: str = ".") -> str:
"""Run the project's build command.
Args:
directory: Project root directory.
"""
return _run_project_command(directory, "build")
@tool
def run_tests(directory: str = ".") -> str:
"""Run the project's test suite.
Args:
directory: Project root directory.
"""
return _run_project_command(directory, "test")
@tool
def verify_build(directory: str = ".") -> str:
"""Run the project's check/lint command to verify correctness.
Args:
directory: Project root directory.
"""
return _run_project_command(directory, "check")
@tool
def execute_command(command: str, directory: str = ".") -> str:
"""Execute a shell command and return its output.
Args:
command: Shell command to execute.
directory: Working directory.
"""
directory = os.path.expanduser(directory)
try:
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
cwd=directory,
timeout=120,
)
output = (result.stdout + result.stderr).strip()
if result.returncode != 0:
return f"Command failed (exit {result.returncode}):\n{output}"
return output or "(no output)"
except subprocess.TimeoutExpired:
return "Command timed out after 120s."
except Exception as e:
return f"Error: {e}"
+3 -4
View File
@@ -342,7 +342,7 @@ mod tests {
use bytes::Bytes;
use futures_util::stream;
use rand::Rng;
use rand::random_range;
use serde_json::json;
#[test]
@@ -392,10 +392,9 @@ mod tests {
}
fn split_chunks(text: &str) -> Vec<Vec<u8>> {
let mut rng = rand::rng();
let len = text.len();
let cut1 = rng.random_range(1..len - 1);
let cut2 = rng.random_range(cut1 + 1..len);
let cut1 = random_range(1..len - 1);
let cut2 = random_range(cut1 + 1..len);
let chunk1 = text.as_bytes()[..cut1].to_vec();
let chunk2 = text.as_bytes()[cut1..cut2].to_vec();
let chunk3 = text.as_bytes()[cut2..].to_vec();
+1 -1
View File
@@ -584,7 +584,7 @@ impl Config {
}
pub fn agent_functions_file(name: &str) -> Result<PathBuf> {
let allowed = ["tools.sh", "tools.py", "tools.js"];
let allowed = ["tools.sh", "tools.py", "tools.ts", "tools.js"];
for entry in read_dir(Self::agent_data_dir(name))? {
let entry = entry?;
+145 -48
View File
@@ -12,7 +12,7 @@ use crate::mcp::{
MCP_DESCRIBE_META_FUNCTION_NAME_PREFIX, MCP_INVOKE_META_FUNCTION_NAME_PREFIX,
MCP_SEARCH_META_FUNCTION_NAME_PREFIX,
};
use crate::parsers::{bash, python};
use crate::parsers::{bash, python, typescript};
use anyhow::{Context, Result, anyhow, bail};
use indexmap::IndexMap;
use indoc::formatdoc;
@@ -53,6 +53,7 @@ enum BinaryType<'a> {
enum Language {
Bash,
Python,
TypeScript,
Unsupported,
}
@@ -61,6 +62,7 @@ impl From<&String> for Language {
match s.to_lowercase().as_str() {
"sh" => Language::Bash,
"py" => Language::Python,
"ts" => Language::TypeScript,
_ => Language::Unsupported,
}
}
@@ -72,6 +74,7 @@ impl Language {
match self {
Language::Bash => "bash",
Language::Python => "python",
Language::TypeScript => "npx tsx",
Language::Unsupported => "sh",
}
}
@@ -80,11 +83,32 @@ impl Language {
match self {
Language::Bash => "sh",
Language::Python => "py",
Language::TypeScript => "ts",
_ => "sh",
}
}
}
fn extract_shebang_runtime(path: &Path) -> Option<String> {
let file = File::open(path).ok()?;
let reader = io::BufReader::new(file);
let first_line = io::BufRead::lines(reader).next()?.ok()?;
let shebang = first_line.strip_prefix("#!")?;
let cmd = shebang.trim();
if cmd.is_empty() {
return None;
}
if let Some(after_env) = cmd.strip_prefix("/usr/bin/env ") {
let runtime = after_env.trim();
if runtime.is_empty() {
return None;
}
Some(runtime.to_string())
} else {
Some(cmd.to_string())
}
}
pub async fn eval_tool_calls(
config: &GlobalConfig,
mut calls: Vec<ToolCall>,
@@ -473,6 +497,11 @@ impl Functions {
file_name,
tools_file_path.parent(),
),
Language::TypeScript => typescript::generate_typescript_declarations(
tool_file,
file_name,
tools_file_path.parent(),
),
Language::Unsupported => {
bail!("Unsupported tool file extension: {}", language.as_ref())
}
@@ -513,7 +542,14 @@ impl Functions {
bail!("Unsupported tool file extension: {}", language.as_ref());
}
Self::build_binaries(binary_name, language, BinaryType::Tool(agent_name))?;
let tool_path = Config::global_tools_dir().join(tool);
let custom_runtime = extract_shebang_runtime(&tool_path);
Self::build_binaries(
binary_name,
language,
BinaryType::Tool(agent_name),
custom_runtime.as_deref(),
)?;
}
Ok(())
@@ -554,8 +590,9 @@ impl Functions {
}
fn build_agent_tool_binaries(name: &str) -> Result<()> {
let tools_file = Config::agent_functions_file(name)?;
let language = Language::from(
&Config::agent_functions_file(name)?
&tools_file
.extension()
.and_then(OsStr::to_str)
.map(|s| s.to_lowercase())
@@ -568,7 +605,8 @@ impl Functions {
bail!("Unsupported tool file extension: {}", language.as_ref());
}
Self::build_binaries(name, language, BinaryType::Agent)
let custom_runtime = extract_shebang_runtime(&tools_file);
Self::build_binaries(name, language, BinaryType::Agent, custom_runtime.as_deref())
}
#[cfg(windows)]
@@ -576,6 +614,7 @@ impl Functions {
binary_name: &str,
language: Language,
binary_type: BinaryType,
custom_runtime: Option<&str>,
) -> Result<()> {
use native::runtime;
let (binary_file, binary_script_file) = match binary_type {
@@ -613,6 +652,7 @@ impl Functions {
)
})?;
let content_template = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
let to_script_path = |p: &str| -> String { p.replace('\\', "/") };
let content = match binary_type {
BinaryType::Tool(None) => {
let root_dir = Config::functions_dir();
@@ -622,8 +662,8 @@ impl Functions {
);
content_template
.replace("{function_name}", binary_name)
.replace("{root_dir}", &root_dir.to_string_lossy())
.replace("{tool_path}", &tool_path)
.replace("{root_dir}", &to_script_path(&root_dir.to_string_lossy()))
.replace("{tool_path}", &to_script_path(&tool_path))
}
BinaryType::Tool(Some(agent_name)) => {
let root_dir = Config::agent_data_dir(agent_name);
@@ -633,16 +673,19 @@ impl Functions {
);
content_template
.replace("{function_name}", binary_name)
.replace("{root_dir}", &root_dir.to_string_lossy())
.replace("{tool_path}", &tool_path)
.replace("{root_dir}", &to_script_path(&root_dir.to_string_lossy()))
.replace("{tool_path}", &to_script_path(&tool_path))
}
BinaryType::Agent => content_template
.replace("{agent_name}", binary_name)
.replace("{config_dir}", &Config::config_dir().to_string_lossy()),
.replace(
"{config_dir}",
&to_script_path(&Config::config_dir().to_string_lossy()),
),
}
.replace(
"{prompt_utils_file}",
&Config::bash_prompt_utils_file().to_string_lossy(),
&to_script_path(&Config::bash_prompt_utils_file().to_string_lossy()),
);
if binary_script_file.exists() {
fs::remove_file(&binary_script_file)?;
@@ -656,40 +699,48 @@ impl Functions {
binary_file.display()
);
let run = match language {
Language::Bash => {
let shell = runtime::bash_path().ok_or_else(|| anyhow!("Shell not found"))?;
format!("{shell} --noprofile --norc")
let run = if let Some(rt) = custom_runtime {
rt.to_string()
} else {
match language {
Language::Bash => {
let shell = runtime::bash_path().ok_or_else(|| anyhow!("Shell not found"))?;
format!("{shell} --noprofile --norc")
}
Language::Python if Path::new(".venv").exists() => {
let executable_path = env::current_dir()?
.join(".venv")
.join("Scripts")
.join("activate.bat");
let canonicalized_path = dunce::canonicalize(&executable_path)?;
format!(
"call \"{}\" && {}",
canonicalized_path.to_string_lossy(),
language.to_cmd()
)
}
Language::Python => {
let executable_path = which::which("python")
.or_else(|_| which::which("python3"))
.map_err(|_| anyhow!("Python executable not found in PATH"))?;
let canonicalized_path = dunce::canonicalize(&executable_path)?;
canonicalized_path.to_string_lossy().into_owned()
}
Language::TypeScript => {
let npx_path = which::which("npx").map_err(|_| {
anyhow!("npx executable not found in PATH (required for TypeScript tools)")
})?;
let canonicalized_path = dunce::canonicalize(&npx_path)?;
format!("{} tsx", canonicalized_path.to_string_lossy())
}
_ => bail!("Unsupported language: {}", language.as_ref()),
}
Language::Python if Path::new(".venv").exists() => {
let executable_path = env::current_dir()?
.join(".venv")
.join("Scripts")
.join("activate.bat");
let canonicalized_path = fs::canonicalize(&executable_path)?;
format!(
"call \"{}\" && {}",
canonicalized_path.to_string_lossy(),
language.to_cmd()
)
}
Language::Python => {
let executable_path = which::which("python")
.or_else(|_| which::which("python3"))
.map_err(|_| anyhow!("Python executable not found in PATH"))?;
let canonicalized_path = fs::canonicalize(&executable_path)?;
canonicalized_path.to_string_lossy().into_owned()
}
_ => bail!("Unsupported language: {}", language.as_ref()),
};
let bin_dir = binary_file
.parent()
.expect("Failed to get parent directory of binary file")
.canonicalize()?
.to_string_lossy()
.into_owned();
let wrapper_binary = binary_script_file
.canonicalize()?
.expect("Failed to get parent directory of binary file");
let canonical_bin_dir = dunce::canonicalize(bin_dir)?.to_string_lossy().into_owned();
let wrapper_binary = dunce::canonicalize(&binary_script_file)?
.to_string_lossy()
.into_owned();
let content = formatdoc!(
@@ -697,7 +748,7 @@ impl Functions {
@echo off
setlocal
set "bin_dir={bin_dir}"
set "bin_dir={canonical_bin_dir}"
{run} "{wrapper_binary}" %*"#,
);
@@ -713,6 +764,7 @@ impl Functions {
binary_name: &str,
language: Language,
binary_type: BinaryType,
custom_runtime: Option<&str>,
) -> Result<()> {
use std::os::unix::prelude::PermissionsExt;
@@ -741,7 +793,7 @@ impl Functions {
)
})?;
let content_template = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
let content = match binary_type {
let mut content = match binary_type {
BinaryType::Tool(None) => {
let root_dir = Config::functions_dir();
let tool_path = format!(
@@ -772,13 +824,44 @@ impl Functions {
"{prompt_utils_file}",
&Config::bash_prompt_utils_file().to_string_lossy(),
);
if binary_file.exists() {
fs::remove_file(&binary_file)?;
}
let mut file = File::create(&binary_file)?;
file.write_all(content.as_bytes())?;
fs::set_permissions(&binary_file, fs::Permissions::from_mode(0o755))?;
if let Some(rt) = custom_runtime
&& let Some(newline_pos) = content.find('\n')
{
content = format!("#!/usr/bin/env {rt}{}", &content[newline_pos..]);
}
if language == Language::TypeScript {
let bin_dir = binary_file
.parent()
.expect("Failed to get parent directory of binary file");
let script_file = bin_dir.join(format!("run-{binary_name}.ts"));
if script_file.exists() {
fs::remove_file(&script_file)?;
}
let mut sf = File::create(&script_file)?;
sf.write_all(content.as_bytes())?;
fs::set_permissions(&script_file, fs::Permissions::from_mode(0o755))?;
let ts_runtime = custom_runtime.unwrap_or("tsx");
let wrapper = format!(
"#!/bin/sh\nexec {ts_runtime} \"{}\" \"$@\"\n",
script_file.display()
);
if binary_file.exists() {
fs::remove_file(&binary_file)?;
}
let mut wf = File::create(&binary_file)?;
wf.write_all(wrapper.as_bytes())?;
fs::set_permissions(&binary_file, fs::Permissions::from_mode(0o755))?;
} else {
if binary_file.exists() {
fs::remove_file(&binary_file)?;
}
let mut file = File::create(&binary_file)?;
file.write_all(content.as_bytes())?;
fs::set_permissions(&binary_file, fs::Permissions::from_mode(0o755))?;
}
Ok(())
}
@@ -1117,6 +1200,20 @@ pub fn run_llm_function(
#[cfg(windows)]
let cmd_name = polyfill_cmd_name(&cmd_name, &bin_dirs);
#[cfg(windows)]
let cmd_args = {
let mut args = cmd_args;
if let Some(json_data) = args.pop() {
let tool_data_file = temp_file("-tool-data-", ".json");
fs::write(&tool_data_file, &json_data)?;
envs.insert(
"LLM_TOOL_DATA_FILE".into(),
tool_data_file.display().to_string(),
);
}
args
};
envs.insert("CLICOLOR_FORCE".into(), "1".into());
envs.insert("FORCE_COLOR".into(), "1".into());
+236
View File
@@ -0,0 +1,236 @@
use crate::function::{FunctionDeclaration, JsonSchema};
use anyhow::{Context, Result, anyhow, bail};
use indexmap::IndexMap;
use serde_json::Value;
use tree_sitter::Node;
#[derive(Debug)]
pub(crate) struct Param {
pub name: String,
pub ty_hint: String,
pub required: bool,
pub default: Option<Value>,
pub doc_type: Option<String>,
pub doc_desc: Option<String>,
}
pub(crate) trait ScriptedLanguage {
fn ts_language(&self) -> tree_sitter::Language;
fn lang_name(&self) -> &str;
fn find_functions<'a>(&self, root: Node<'a>, src: &str) -> Vec<(Node<'a>, Node<'a>)>;
fn function_name<'a>(&self, func_node: Node<'a>, src: &'a str) -> Result<&'a str>;
fn extract_description(
&self,
wrapper_node: Node<'_>,
func_node: Node<'_>,
src: &str,
) -> Option<String>;
fn extract_params(
&self,
func_node: Node<'_>,
src: &str,
description: &str,
) -> Result<Vec<Param>>;
}
pub(crate) fn build_param(
name: &str,
mut ty: String,
mut required: bool,
default: Option<Value>,
) -> Param {
if ty.ends_with('?') {
ty.pop();
required = false;
}
Param {
name: name.to_string(),
ty_hint: ty,
required,
default,
doc_type: None,
doc_desc: None,
}
}
pub(crate) fn build_parameters_schema(params: &[Param], _description: &str) -> JsonSchema {
let mut props: IndexMap<String, JsonSchema> = IndexMap::new();
let mut req: Vec<String> = Vec::new();
for p in params {
let name = p.name.replace('-', "_");
let mut schema = JsonSchema::default();
let ty = if !p.ty_hint.is_empty() {
p.ty_hint.as_str()
} else if let Some(t) = &p.doc_type {
t.as_str()
} else {
"str"
};
if let Some(d) = &p.doc_desc
&& !d.is_empty()
{
schema.description = Some(d.clone());
}
apply_type_to_schema(ty, &mut schema);
if p.default.is_none() && p.required {
req.push(name.clone());
}
props.insert(name, schema);
}
JsonSchema {
type_value: Some("object".into()),
description: None,
properties: Some(props),
items: None,
any_of: None,
enum_value: None,
default: None,
required: if req.is_empty() { None } else { Some(req) },
}
}
pub(crate) fn apply_type_to_schema(ty: &str, s: &mut JsonSchema) {
let t = ty.trim_end_matches('?');
if let Some(rest) = t.strip_prefix("list[") {
s.type_value = Some("array".into());
let inner = rest.trim_end_matches(']');
let mut item = JsonSchema::default();
apply_type_to_schema(inner, &mut item);
if item.type_value.is_none() {
item.type_value = Some("string".into());
}
s.items = Some(Box::new(item));
return;
}
if let Some(rest) = t.strip_prefix("literal:") {
s.type_value = Some("string".into());
let vals = rest
.split('|')
.map(|x| x.trim().trim_matches('"').trim_matches('\'').to_string())
.collect::<Vec<_>>();
if !vals.is_empty() {
s.enum_value = Some(vals);
}
return;
}
s.type_value = Some(
match t {
"bool" => "boolean",
"int" => "integer",
"float" => "number",
"str" | "any" | "" => "string",
_ => "string",
}
.into(),
);
}
pub(crate) fn underscore(s: &str) -> String {
s.chars()
.map(|c| {
if c.is_ascii_alphanumeric() {
c.to_ascii_lowercase()
} else {
'_'
}
})
.collect::<String>()
.split('_')
.filter(|t| !t.is_empty())
.collect::<Vec<_>>()
.join("_")
}
pub(crate) fn node_text<'a>(node: Node<'_>, src: &'a str) -> Result<&'a str> {
node.utf8_text(src.as_bytes())
.map_err(|err| anyhow!("invalid utf-8 in source: {err}"))
}
pub(crate) fn named_child(node: Node<'_>, index: usize) -> Option<Node<'_>> {
let mut cursor = node.walk();
node.named_children(&mut cursor).nth(index)
}
pub(crate) fn generate_declarations<L: ScriptedLanguage>(
lang: &L,
src: &str,
file_name: &str,
is_tool: bool,
) -> Result<Vec<FunctionDeclaration>> {
let mut parser = tree_sitter::Parser::new();
let language = lang.ts_language();
parser.set_language(&language).with_context(|| {
format!(
"failed to initialize {} tree-sitter parser",
lang.lang_name()
)
})?;
let tree = parser
.parse(src.as_bytes(), None)
.ok_or_else(|| anyhow!("failed to parse {}: {file_name}", lang.lang_name()))?;
if tree.root_node().has_error() {
bail!(
"failed to parse {}: syntax error in {file_name}",
lang.lang_name()
);
}
let mut out = Vec::new();
for (wrapper, func) in lang.find_functions(tree.root_node(), src) {
let func_name = lang.function_name(func, src)?;
if func_name.starts_with('_') && func_name != "_instructions" {
continue;
}
if is_tool && func_name != "run" {
continue;
}
let description = lang
.extract_description(wrapper, func, src)
.unwrap_or_default();
let params = lang
.extract_params(func, src, &description)
.with_context(|| format!("in function '{func_name}' in {file_name}"))?;
let schema = build_parameters_schema(&params, &description);
let name = if is_tool && func_name == "run" {
underscore(file_name)
} else {
underscore(func_name)
};
let desc_trim = description.trim().to_string();
if desc_trim.is_empty() {
bail!("Missing or empty description on function: {func_name}");
}
out.push(FunctionDeclaration {
name,
description: desc_trim,
parameters: schema,
agent: !is_tool,
});
}
Ok(out)
}
+2
View File
@@ -1,2 +1,4 @@
pub(crate) mod bash;
pub(crate) mod common;
pub(crate) mod python;
pub(crate) mod typescript;
+680 -334
View File
File diff suppressed because it is too large Load Diff
+789
View File
@@ -0,0 +1,789 @@
use crate::function::FunctionDeclaration;
use crate::parsers::common::{self, Param, ScriptedLanguage};
use anyhow::{Context, Result, anyhow, bail};
use indexmap::IndexMap;
use serde_json::Value;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use tree_sitter::Node;
pub(crate) struct TypeScriptLanguage;
impl ScriptedLanguage for TypeScriptLanguage {
fn ts_language(&self) -> tree_sitter::Language {
tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()
}
fn lang_name(&self) -> &str {
"typescript"
}
fn find_functions<'a>(&self, root: Node<'a>, _src: &str) -> Vec<(Node<'a>, Node<'a>)> {
let mut cursor = root.walk();
root.named_children(&mut cursor)
.filter_map(|stmt| match stmt.kind() {
"export_statement" => unwrap_exported_function(stmt).map(|fd| (stmt, fd)),
_ => None,
})
.collect()
}
fn function_name<'a>(&self, func_node: Node<'a>, src: &'a str) -> Result<&'a str> {
let name_node = func_node
.child_by_field_name("name")
.ok_or_else(|| anyhow!("function_declaration missing name"))?;
common::node_text(name_node, src)
}
fn extract_description(
&self,
wrapper_node: Node<'_>,
func_node: Node<'_>,
src: &str,
) -> Option<String> {
let text = jsdoc_text(wrapper_node, func_node, src)?;
let lines = clean_jsdoc_lines(text);
let mut description = Vec::new();
for line in lines {
if line.starts_with('@') {
break;
}
description.push(line);
}
let description = description.join("\n").trim().to_string();
(!description.is_empty()).then_some(description)
}
fn extract_params(
&self,
func_node: Node<'_>,
src: &str,
_description: &str,
) -> Result<Vec<Param>> {
let parameters = func_node
.child_by_field_name("parameters")
.ok_or_else(|| anyhow!("function_declaration missing parameters"))?;
let mut out = Vec::new();
let mut cursor = parameters.walk();
for param in parameters.named_children(&mut cursor) {
match param.kind() {
"required_parameter" | "optional_parameter" => {
let name = parameter_name(param, src)?;
let ty = get_arg_type(param.child_by_field_name("type"), src)?;
let required = param.kind() == "required_parameter"
&& param.child_by_field_name("value").is_none();
let default = param.child_by_field_name("value").map(|_| Value::Null);
out.push(common::build_param(name, ty, required, default));
}
"rest_parameter" => {
let line = param.start_position().row + 1;
bail!("line {line}: rest parameters (...) are not supported in tool functions")
}
"object_pattern" => {
let line = param.start_position().row + 1;
bail!(
"line {line}: destructured object parameters (e.g. '{{ a, b }}: {{ a: string }}') \
are not supported in tool functions. Use flat parameters instead (e.g. 'a: string, b: string')."
)
}
other => {
let line = param.start_position().row + 1;
bail!("line {line}: unsupported parameter type: {other}")
}
}
}
let wrapper = match func_node.parent() {
Some(parent) if parent.kind() == "export_statement" => parent,
_ => func_node,
};
if let Some(doc) = jsdoc_text(wrapper, func_node, src) {
let meta = parse_jsdoc_params(doc);
for p in &mut out {
if let Some(desc) = meta.get(&p.name)
&& !desc.is_empty()
{
p.doc_desc = Some(desc.clone());
}
}
}
Ok(out)
}
}
pub fn generate_typescript_declarations(
mut tool_file: File,
file_name: &str,
parent: Option<&Path>,
) -> Result<Vec<FunctionDeclaration>> {
let mut src = String::new();
tool_file
.read_to_string(&mut src)
.with_context(|| format!("Failed to load script at '{tool_file:?}'"))?;
let is_tool = parent
.and_then(|p| p.file_name())
.is_some_and(|n| n == "tools");
common::generate_declarations(&TypeScriptLanguage, &src, file_name, is_tool)
}
fn unwrap_exported_function(node: Node<'_>) -> Option<Node<'_>> {
node.child_by_field_name("declaration")
.filter(|child| child.kind() == "function_declaration")
.or_else(|| {
let mut cursor = node.walk();
node.named_children(&mut cursor)
.find(|child| child.kind() == "function_declaration")
})
}
fn jsdoc_text<'a>(wrapper_node: Node<'_>, func_node: Node<'_>, src: &'a str) -> Option<&'a str> {
wrapper_node
.prev_named_sibling()
.or_else(|| func_node.prev_named_sibling())
.filter(|node| node.kind() == "comment")
.and_then(|node| common::node_text(node, src).ok())
.filter(|text| text.trim_start().starts_with("/**"))
}
fn clean_jsdoc_lines(doc: &str) -> Vec<String> {
let trimmed = doc.trim();
let inner = trimmed
.strip_prefix("/**")
.unwrap_or(trimmed)
.strip_suffix("*/")
.unwrap_or(trimmed);
inner
.lines()
.map(|line| {
let line = line.trim();
let line = line.strip_prefix('*').unwrap_or(line).trim_start();
line.to_string()
})
.collect()
}
fn parse_jsdoc_params(doc: &str) -> IndexMap<String, String> {
let mut out = IndexMap::new();
for line in clean_jsdoc_lines(doc) {
let Some(rest) = line.strip_prefix("@param") else {
continue;
};
let mut rest = rest.trim();
if rest.starts_with('{')
&& let Some(end) = rest.find('}')
{
rest = rest[end + 1..].trim_start();
}
if rest.is_empty() {
continue;
}
let name_end = rest.find(char::is_whitespace).unwrap_or(rest.len());
let mut name = rest[..name_end].trim();
if let Some(stripped) = name.strip_suffix('?') {
name = stripped;
}
if name.is_empty() {
continue;
}
let mut desc = rest[name_end..].trim();
if let Some(stripped) = desc.strip_prefix('-') {
desc = stripped.trim_start();
}
out.insert(name.to_string(), desc.to_string());
}
out
}
fn parameter_name<'a>(node: Node<'_>, src: &'a str) -> Result<&'a str> {
if let Some(name) = node.child_by_field_name("name") {
return match name.kind() {
"identifier" => common::node_text(name, src),
"rest_pattern" => {
let line = node.start_position().row + 1;
bail!("line {line}: rest parameters (...) are not supported in tool functions")
}
"object_pattern" | "array_pattern" => {
let line = node.start_position().row + 1;
bail!(
"line {line}: destructured parameters are not supported in tool functions. \
Use flat parameters instead (e.g. 'a: string, b: string')."
)
}
other => {
let line = node.start_position().row + 1;
bail!("line {line}: unsupported parameter type: {other}")
}
};
}
let pattern = node
.child_by_field_name("pattern")
.ok_or_else(|| anyhow!("parameter missing pattern"))?;
match pattern.kind() {
"identifier" => common::node_text(pattern, src),
"rest_pattern" => {
let line = node.start_position().row + 1;
bail!("line {line}: rest parameters (...) are not supported in tool functions")
}
"object_pattern" | "array_pattern" => {
let line = node.start_position().row + 1;
bail!(
"line {line}: destructured parameters are not supported in tool functions. \
Use flat parameters instead (e.g. 'a: string, b: string')."
)
}
other => {
let line = node.start_position().row + 1;
bail!("line {line}: unsupported parameter type: {other}")
}
}
}
fn get_arg_type(annotation: Option<Node<'_>>, src: &str) -> Result<String> {
let Some(annotation) = annotation else {
return Ok(String::new());
};
match annotation.kind() {
"type_annotation" | "type" => get_arg_type(common::named_child(annotation, 0), src),
"predefined_type" => Ok(match common::node_text(annotation, src)? {
"string" => "str",
"number" => "float",
"boolean" => "bool",
"any" | "unknown" | "void" | "undefined" => "any",
_ => "any",
}
.to_string()),
"type_identifier" | "nested_type_identifier" => Ok("any".to_string()),
"generic_type" => {
let name = annotation
.child_by_field_name("name")
.ok_or_else(|| anyhow!("generic_type missing name"))?;
let type_name = common::node_text(name, src)?;
let type_args = annotation
.child_by_field_name("type_arguments")
.ok_or_else(|| anyhow!("generic_type missing type arguments"))?;
let inner = common::named_child(type_args, 0)
.ok_or_else(|| anyhow!("generic_type missing inner type"))?;
match type_name {
"Array" => Ok(format!("list[{}]", get_arg_type(Some(inner), src)?)),
_ => Ok("any".to_string()),
}
}
"array_type" => {
let inner = common::named_child(annotation, 0)
.ok_or_else(|| anyhow!("array_type missing inner type"))?;
Ok(format!("list[{}]", get_arg_type(Some(inner), src)?))
}
"union_type" => resolve_union_type(annotation, src),
"literal_type" => resolve_literal_type(annotation, src),
"parenthesized_type" => get_arg_type(common::named_child(annotation, 0), src),
_ => Ok("any".to_string()),
}
}
fn resolve_union_type(annotation: Node<'_>, src: &str) -> Result<String> {
let members = flatten_union_members(annotation);
let has_null = members.iter().any(|member| is_nullish_type(*member, src));
let mut literal_values = Vec::new();
let mut all_string_literals = true;
for member in &members {
match string_literal_member(*member, src) {
Some(value) => literal_values.push(value),
None => {
all_string_literals = false;
break;
}
}
}
if all_string_literals && !literal_values.is_empty() {
return Ok(format!("literal:{}", literal_values.join("|")));
}
let mut first_non_null = None;
for member in members {
if is_nullish_type(member, src) {
continue;
}
first_non_null = Some(get_arg_type(Some(member), src)?);
break;
}
let mut ty = first_non_null.unwrap_or_else(|| "any".to_string());
if has_null && !ty.ends_with('?') {
ty.push('?');
}
Ok(ty)
}
fn flatten_union_members(node: Node<'_>) -> Vec<Node<'_>> {
let node = if node.kind() == "type" {
match common::named_child(node, 0) {
Some(inner) => inner,
None => return vec![],
}
} else {
node
};
if node.kind() != "union_type" {
return vec![node];
}
let mut cursor = node.walk();
let mut out = Vec::new();
for child in node.named_children(&mut cursor) {
out.extend(flatten_union_members(child));
}
out
}
fn resolve_literal_type(annotation: Node<'_>, src: &str) -> Result<String> {
let inner = common::named_child(annotation, 0)
.ok_or_else(|| anyhow!("literal_type missing inner literal"))?;
match inner.kind() {
"string" | "number" | "true" | "false" | "unary_expression" => {
Ok(format!("literal:{}", common::node_text(inner, src)?.trim()))
}
"null" | "undefined" => Ok("any".to_string()),
_ => Ok("any".to_string()),
}
}
fn string_literal_member(node: Node<'_>, src: &str) -> Option<String> {
let node = if node.kind() == "type" {
common::named_child(node, 0)?
} else {
node
};
if node.kind() != "literal_type" {
return None;
}
let inner = common::named_child(node, 0)?;
if inner.kind() != "string" {
return None;
}
Some(common::node_text(inner, src).ok()?.to_string())
}
fn is_nullish_type(node: Node<'_>, src: &str) -> bool {
let node = if node.kind() == "type" {
match common::named_child(node, 0) {
Some(inner) => inner,
None => return false,
}
} else {
node
};
match node.kind() {
"literal_type" => common::named_child(node, 0)
.is_some_and(|inner| matches!(inner.kind(), "null" | "undefined")),
"predefined_type" => common::node_text(node, src)
.map(|text| matches!(text, "undefined" | "void"))
.unwrap_or(false),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::function::JsonSchema;
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
fn parse_ts_source(
source: &str,
file_name: &str,
parent: &Path,
) -> Result<Vec<FunctionDeclaration>> {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("time")
.as_nanos();
let path = std::env::temp_dir().join(format!("loki_ts_parser_{file_name}_{unique}.ts"));
fs::write(&path, source).expect("write");
let file = File::open(&path).expect("open");
let result = generate_typescript_declarations(file, file_name, Some(parent));
let _ = fs::remove_file(&path);
result
}
fn properties(schema: &JsonSchema) -> &IndexMap<String, JsonSchema> {
schema
.properties
.as_ref()
.expect("missing schema properties")
}
fn property<'a>(schema: &'a JsonSchema, name: &str) -> &'a JsonSchema {
properties(schema)
.get(name)
.unwrap_or_else(|| panic!("missing property: {name}"))
}
#[test]
fn test_ts_tool_demo() {
let source = r#"
/**
* Demonstrates how to create a tool using TypeScript.
*
* @param query - The search query string
* @param format - Output format
* @param count - Maximum results to return
* @param verbose - Enable verbose output
* @param tags - List of tags to filter by
* @param language - Optional language filter
* @param extra_tags - Optional extra tags
*/
export function run(
query: string,
format: "json" | "csv" | "xml",
count: number,
verbose: boolean,
tags: string[],
language?: string,
extra_tags?: Array<string>,
): string {
return "result";
}
"#;
let declarations = parse_ts_source(source, "demo_ts", Path::new("tools")).unwrap();
assert_eq!(declarations.len(), 1);
let decl = &declarations[0];
assert_eq!(decl.name, "demo_ts");
assert!(!decl.agent);
let params = &decl.parameters;
assert_eq!(params.type_value.as_deref(), Some("object"));
assert_eq!(
params.required.as_ref().unwrap(),
&vec![
"query".to_string(),
"format".to_string(),
"count".to_string(),
"verbose".to_string(),
"tags".to_string(),
]
);
assert_eq!(
property(params, "query").type_value.as_deref(),
Some("string")
);
let format = property(params, "format");
assert_eq!(format.type_value.as_deref(), Some("string"));
assert_eq!(
format.enum_value.as_ref().unwrap(),
&vec!["json".to_string(), "csv".to_string(), "xml".to_string()]
);
assert_eq!(
property(params, "count").type_value.as_deref(),
Some("number")
);
assert_eq!(
property(params, "verbose").type_value.as_deref(),
Some("boolean")
);
let tags = property(params, "tags");
assert_eq!(tags.type_value.as_deref(), Some("array"));
assert_eq!(
tags.items.as_ref().unwrap().type_value.as_deref(),
Some("string")
);
let language = property(params, "language");
assert_eq!(language.type_value.as_deref(), Some("string"));
assert!(
!params
.required
.as_ref()
.unwrap()
.contains(&"language".to_string())
);
let extra_tags = property(params, "extra_tags");
assert_eq!(extra_tags.type_value.as_deref(), Some("array"));
assert_eq!(
extra_tags.items.as_ref().unwrap().type_value.as_deref(),
Some("string")
);
assert!(
!params
.required
.as_ref()
.unwrap()
.contains(&"extra_tags".to_string())
);
}
#[test]
fn test_ts_tool_simple() {
let source = r#"
/**
* Execute the given code.
*
* @param code - The code to execute
*/
export function run(code: string): string {
return eval(code);
}
"#;
let declarations = parse_ts_source(source, "execute_code", Path::new("tools")).unwrap();
assert_eq!(declarations.len(), 1);
let decl = &declarations[0];
assert_eq!(decl.name, "execute_code");
assert!(!decl.agent);
let params = &decl.parameters;
assert_eq!(params.required.as_ref().unwrap(), &vec!["code".to_string()]);
assert_eq!(
property(params, "code").type_value.as_deref(),
Some("string")
);
}
#[test]
fn test_ts_agent_tools() {
let source = r#"
/** Get user info by ID */
export function get_user(id: string): string {
return "";
}
/** List all users */
export function list_users(): string {
return "";
}
"#;
let declarations = parse_ts_source(source, "tools", Path::new("demo")).unwrap();
assert_eq!(declarations.len(), 2);
assert_eq!(declarations[0].name, "get_user");
assert_eq!(declarations[1].name, "list_users");
assert!(declarations[0].agent);
assert!(declarations[1].agent);
}
#[test]
fn test_ts_reject_rest_params() {
let source = r#"
/**
* Has rest params
*/
export function run(...args: string[]): string {
return "";
}
"#;
let err = parse_ts_source(source, "rest_params", Path::new("tools")).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("rest parameters"));
assert!(msg.contains("in function 'run'"));
}
#[test]
fn test_ts_missing_jsdoc() {
let source = r#"
export function run(x: string): string {
return x;
}
"#;
let err = parse_ts_source(source, "missing_jsdoc", Path::new("tools")).unwrap_err();
assert!(
err.to_string()
.contains("Missing or empty description on function: run")
);
}
#[test]
fn test_ts_syntax_error() {
let source = "export function run(: broken";
let err = parse_ts_source(source, "syntax_error", Path::new("tools")).unwrap_err();
assert!(err.to_string().contains("failed to parse typescript"));
}
#[test]
fn test_ts_underscore_skipped() {
let source = r#"
/** Private helper */
function _helper(): void {}
/** Public function */
export function do_stuff(): string {
return "";
}
"#;
let declarations = parse_ts_source(source, "tools", Path::new("demo")).unwrap();
assert_eq!(declarations.len(), 1);
assert_eq!(declarations[0].name, "do_stuff");
assert!(declarations[0].agent);
}
#[test]
fn test_ts_non_exported_helpers_skipped() {
let source = r#"
#!/usr/bin/env tsx
import { appendFileSync } from 'fs';
/**
* Get the current weather in a given location
* @param location - The city
*/
export function get_current_weather(location: string): string {
return fetchSync("https://example.com/" + location);
}
function fetchSync(url: string): string {
return "sunny";
}
"#;
let declarations = parse_ts_source(source, "tools", Path::new("demo")).unwrap();
assert_eq!(declarations.len(), 1);
assert_eq!(declarations[0].name, "get_current_weather");
}
#[test]
fn test_ts_instructions_not_skipped() {
let source = r#"
/** Help text for the agent */
export function _instructions(): string {
return "";
}
"#;
let declarations = parse_ts_source(source, "tools", Path::new("demo")).unwrap();
assert_eq!(declarations.len(), 1);
assert_eq!(declarations[0].name, "instructions");
assert!(declarations[0].agent);
}
#[test]
fn test_ts_optional_with_null_union() {
let source = r#"
/**
* Fetch data with optional filter
*
* @param url - The URL to fetch
* @param filter - Optional filter string
*/
export function run(url: string, filter: string | null): string {
return "";
}
"#;
let declarations = parse_ts_source(source, "fetch_data", Path::new("tools")).unwrap();
let params = &declarations[0].parameters;
assert!(
params
.required
.as_ref()
.unwrap()
.contains(&"url".to_string())
);
assert!(
!params
.required
.as_ref()
.unwrap()
.contains(&"filter".to_string())
);
assert_eq!(
property(params, "filter").type_value.as_deref(),
Some("string")
);
}
#[test]
fn test_ts_optional_with_default() {
let source = r#"
/**
* Search with limit
*
* @param query - Search query
* @param limit - Max results
*/
export function run(query: string, limit: number = 10): string {
return "";
}
"#;
let declarations =
parse_ts_source(source, "search_with_limit", Path::new("tools")).unwrap();
let params = &declarations[0].parameters;
assert!(
params
.required
.as_ref()
.unwrap()
.contains(&"query".to_string())
);
assert!(
!params
.required
.as_ref()
.unwrap()
.contains(&"limit".to_string())
);
assert_eq!(
property(params, "limit").type_value.as_deref(),
Some("number")
);
}
#[test]
fn test_ts_shebang_parses() {
let source = r#"#!/usr/bin/env tsx
/**
* Get weather
* @param location - The city
*/
export function run(location: string): string {
return location;
}
"#;
let result = parse_ts_source(source, "get_weather", Path::new("tools"));
eprintln!("shebang parse result: {result:?}");
assert!(result.is_ok(), "shebang should not cause parse failure");
let declarations = result.unwrap();
assert_eq!(declarations.len(), 1);
assert_eq!(declarations[0].name, "get_weather");
}
}
+2
View File
@@ -111,12 +111,14 @@ fn create_suggestion(value: &str, description: &str, span: Span) -> Suggestion {
Some(description.to_string())
};
Suggestion {
display_override: None,
value: value.to_string(),
description,
style: None,
extra: None,
span,
append_whitespace: false,
match_indices: None,
}
}