Compare commits
305 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 139b5cc57e | |||
| c6c10b5e24 | |||
| a4e5bef1b7 | |||
| f72c7b03f9 | |||
| bd6f709374 | |||
| 00f2201157 | |||
| b3f0d66071 | |||
| 8730d413bc | |||
| 79140fda3c | |||
| 67e749ea3a | |||
| 7bcfc133ae | |||
| e3e246607e | |||
| 16104cb2c5 | |||
| 224e51c386 | |||
| b022ca089c | |||
| 0ebb761c09 | |||
| c8067828d5 | |||
| 30eedd9b8c | |||
| d701b45057 | |||
| 722c9c101e | |||
| 86aa45f0c4 | |||
| cf45dc4820 | |||
| db77034431 | |||
| abdaec11b0 | |||
| 95fb349656 | |||
| d0b6b6c324 | |||
| d74c23ccf5 | |||
| ea1cfda0d6 | |||
| 5623f47f9a | |||
| e4df9ec193 | |||
| a6306d6b76 | |||
| 64529ba5cc | |||
| cc7f963b89 | |||
| 0ce86af116 | |||
| 2cb0ed3f64 | |||
| fb61854f11 | |||
| 53ba3344b1 | |||
| e20c8be8bb | |||
| 894dcb1d3c | |||
| 9a9e890f8a | |||
| 818ea634f0 | |||
| 780460f8d8 | |||
| e19483a920 | |||
| aca93f1cae | |||
| 1371a4aad2 | |||
| db4a45c0f6 | |||
| e95b1e5f82 | |||
| 15f4008f4b | |||
| f45f81fb45 | |||
| 2220fd2542 | |||
| 564480e165 | |||
| 297c63d91a | |||
| 26e2cd3f65 | |||
| 9f899466d4 | |||
| 38393ea4cf | |||
| a4f25826e3 | |||
| 93484fb33f | |||
| c90f003f92 | |||
| 24793b9b8d | |||
| 78e772f455 | |||
| 1e0d269aad | |||
| f6b1d408fc | |||
| 442b318b6c | |||
| a7c97aedb7 | |||
| 746f9e7b24 | |||
| 0d6c61af5c | |||
| 673f31c059 | |||
| 369a4f0a89 | |||
| 8d54eae4d0 | |||
| a805d5beab | |||
| dbb2aec8b6 | |||
| 1a98b76a1f | |||
| 51d10ab2b5 | |||
| 1aad750395 | |||
| e0aab6bd02 | |||
| 6cb93132b7 | |||
| 04126b99d6 | |||
| 0794eb960d | |||
| d619ad1d48 | |||
| 5b147e07b3 | |||
| 944ce441d8 | |||
| a7dcb8519b | |||
| d912d44fb3 | |||
| 4f7254a634 | |||
| bf923cb296 | |||
| d9f737e1bf | |||
| 59690d045e | |||
| 5d95acba53 | |||
| d46225d2a9 | |||
| 3af30a0e62 | |||
| 69eca4d96d | |||
| 7b2e4a83c9 | |||
| 344b80872a | |||
| ddf828ff5f | |||
| 4e170b069b | |||
| 22c75fb578 | |||
| 11ab9eb6b8 | |||
| 29b232f407 | |||
| 53e8c920e5 | |||
| 78d19bed4d | |||
| 10f4160635 | |||
| 7622836e8b | |||
| 4d4713a9fa | |||
| 25008599f9 | |||
| c00ab074f8 | |||
| aed1f1957f | |||
| c6a959e2e1 | |||
| 02b7ed37f6 | |||
| 0d84aaabb9 | |||
| 6efdcf9610 | |||
| 4266d317d8 | |||
| 4ce7aafcbd | |||
| 35d8b69f92 | |||
| 562057e608 | |||
| b7024e5340 | |||
| 088588231b | |||
| eff117d3d9 | |||
| 968c535709 | |||
| c8b6fa7b11 | |||
| 0aa334b54e | |||
| 78a49f841d | |||
| 43b2bd937e | |||
| a4326875ba | |||
| eb31a58346 | |||
| a6b0acc35d | |||
| cc7fcd0b5b | |||
| 02fe59b913 | |||
| 6fd5f47089 | |||
| 2a2922760e | |||
| a3793460fd | |||
| e0927a04d9 | |||
| 8665604bab | |||
| d4c3c135b3 | |||
| 60bd5e493c | |||
| 0753b2d841 | |||
| 17e6fbd692 | |||
| 0710441650 | |||
| 20a76cee3e | |||
| cb64785867 | |||
| e6e26103c4 | |||
| 15529a14f1 | |||
| 86839188e0 | |||
| 39701b378b | |||
| 45ff6da737 | |||
| a260dd1503 | |||
| 57859301df | |||
| 8c968d3f53 | |||
| 0034bfbe46 | |||
| a733b9247a | |||
| e0afa349b9 | |||
| 7d0ce94907 | |||
| 9045763c35 | |||
| 29898552d7 | |||
| 9d7c2f5c2f | |||
| 5c0fa42351 | |||
| ab045b0ef3 | |||
| 41e6843db1 | |||
| 911ec3c9b9 | |||
| fc6f0a1a7b | |||
| 21873da278 | |||
| d1cd6be2c9 | |||
| 0c0ae41bca | |||
| c9ed7a904a | |||
| d200a8f554 | |||
| 3d04c8fcf1 | |||
| f53f165d91 | |||
| e5645e4064 | |||
| 95e15ca8c4 | |||
| dbf7329e87 | |||
| ed6c3ae431 | |||
| 214d2ecc67 | |||
| 29c95671de | |||
| 238f93a096 | |||
| c76877e7b3 | |||
| 12e5a9c5aa | |||
| 7f4be2ca3f | |||
| 29ffe12d8c | |||
| d34bed4f15 | |||
| aec7ea7e80 | |||
| 5938e1af29 | |||
| 60902297c5 | |||
| 12a95aa6fa | |||
| 78fc459a97 | |||
| 281565804c | |||
| 33a32fd9c8 | |||
| b64aad55e9 | |||
| 2392958114 | |||
| ec04e8e24a | |||
| 4e14ee7f50 | |||
| 7ba4ab0608 | |||
| fd816112fb | |||
| d0ee85be40 | |||
| 9448704af3 | |||
| 9dad9d6ca8 | |||
| 3f41abed7c | |||
| debcbab445 | |||
| 7fcabf1de7 | |||
| e116a1841d | |||
| cd3103ca14 | |||
| 50d07a4b13 | |||
| ed1352936e | |||
| f4b4156a0c | |||
| 5cf2cce0e3 | |||
| 249453d829 | |||
| c14939cecc | |||
| 72f516abb1 | |||
| 66478ed264 | |||
| 6b10dff41d | |||
| f8cc736482 | |||
| a0794fecfc | |||
| c68059e5b3 | |||
| 832ca6b0de | |||
| 89ee43830e | |||
| f7cf13901e | |||
| ad41fa93fb | |||
| 617b7dcd49 | |||
| 417ea032c4 | |||
| b77bb6e200 | |||
| 1fa3b4a600 | |||
| 99bd502f62 | |||
| 25a271dc95 | |||
| 5002ac7716 | |||
| d92a559460 | |||
| 3d571e1a31 | |||
| d338daa4b6 | |||
| 6f802c2a58 | |||
| a3f0168817 | |||
| 677702655f | |||
| b0bbd0c083 | |||
| 5cbf23a1f4 | |||
| 39eb9b34ec | |||
| 5da8616518 | |||
| b267fe05cd | |||
| 29f7ebe559 | |||
| bbffaca511 | |||
| 80532836c3 | |||
| 9474f4f322 | |||
| 93a09d3a9f | |||
| e3935ce699 | |||
| 58c15e7833 | |||
| fd2b7f3aa0 | |||
| 5ccbc629d1 | |||
| e98ff5e8e5 | |||
| a6fffa7b57 | |||
| 3ac153dd06 | |||
| 8db3108c94 | |||
| e25ff4ad19 | |||
| 21e76c6461 | |||
| 103aa1a432 | |||
| d2f4fefcf3 | |||
| 629527988d | |||
| 7f520f1346 | |||
| e28619b55a | |||
| f474e6130e | |||
| 4b5bcb45ac | |||
| 50565a0f17 | |||
| cf37db4fa2 | |||
| ad9b4097ef | |||
| c22c01c6c3 | |||
| 31f7f50c4a | |||
| a7f6ed4b16 | |||
| 73ada5a221 | |||
| 2f96256893 | |||
| 23d9e0775f | |||
| 72ade39144 | |||
| ec64c68777 | |||
| 80932e069f | |||
| 2f9b154b07 | |||
| 20bf911732 | |||
| 65a3dbb228 | |||
| 5844cc93ca | |||
| 4d23ce58c4 | |||
| 2bb592d5f6 | |||
| 3146b20c15 | |||
| 455cf67750 | |||
| a6d6a877b0 | |||
| a7bd54471c | |||
| fe5f803163 | |||
| 66a9b5362a | |||
| f3569cf68b | |||
| 2573f14726 | |||
| f1fb2d6abf | |||
| 4934e0ff0a | |||
| f772a80501 | |||
| 8950843be2 | |||
| 9b89e68908 | |||
| ba134ca53f | |||
| 21dbd9c057 | |||
| 40a68f8e05 | |||
| 37d861a631 | |||
| 31f3e885ce | |||
| 7ffaab2012 | |||
| 35b7946b0d | |||
| 3a05a8e712 | |||
| 294a1149ef | |||
| 8d80370014 | |||
| 1cbdef36cf | |||
| 4c8accbfc1 | |||
| c4c2d9cb93 | |||
| 7aed112326 | |||
| 216a3d53cd | |||
| e0823b343b | |||
| cb0bc65ee4 | |||
| 5b9ab6636f | |||
| 9fd77feebb |
@@ -1 +0,0 @@
|
||||
{"type":"rust","build":"cargo build","test":"cargo test","check":"cargo check","_detected_by":"heuristic","_cached_at":"2026-04-13T13:36:33-06:00"}
|
||||
@@ -89,12 +89,7 @@ duct = "1.0.0"
|
||||
argc = "1.23.0"
|
||||
strum_macros = "0.27.2"
|
||||
indoc = "2.0.6"
|
||||
rmcp = { version = "1.5.0", features = [
|
||||
"client",
|
||||
"transport-child-process",
|
||||
"transport-streamable-http-client-reqwest",
|
||||
"reqwest-native-tls",
|
||||
] }
|
||||
rmcp = { version = "0.16.0", features = ["client", "transport-child-process"] }
|
||||
num_cpus = "1.17.0"
|
||||
tree-sitter = "0.26.8"
|
||||
tree-sitter-language = "0.1"
|
||||
@@ -125,7 +120,7 @@ default-features = false
|
||||
features = ["parsing", "regex-onig", "plist-load"]
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
crossterm = { version = "0.29.0", features = ["use-dev-tty"] }
|
||||
crossterm = { version = "0.28.1", features = ["use-dev-tty"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
arboard = { version = "3.3.0", default-features = false, features = [
|
||||
@@ -137,7 +132,6 @@ arboard = { version = "3.3.0", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4.0"
|
||||
serial_test = "3"
|
||||
|
||||
[[bin]]
|
||||
name = "loki"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Loki: All-in-one, batteries-included LLM CLI Tool
|
||||
|
||||

|
||||

|
||||
[](https://crates.io/crates/loki-ai)
|
||||

|
||||

|
||||
@@ -12,36 +13,36 @@ Agents, and More.
|
||||
It is designed to include a number of useful agents, roles, macros, and more so users can get up and running with Loki
|
||||
in as little time as possible.
|
||||
|
||||

|
||||

|
||||
|
||||
Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration guide](https://github.com/Dark-Alex-17/loki/wiki/AIChat-Migration) to get started.
|
||||
Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration guide](./docs/AICHAT-MIGRATION.md) to get started.
|
||||
|
||||
## Quick Links
|
||||
* [AIChat Migration Guide](https://github.com/Dark-Alex-17/loki/wiki/AIChat-Migration): Coming from AIChat? Follow the migration guide to get started.
|
||||
* [AIChat Migration Guide](./docs/AICHAT-MIGRATION.md): Coming from AIChat? Follow the migration guide to get started.
|
||||
* [Installation](#install): Install Loki
|
||||
* [Getting Started](#getting-started): Get started with Loki by doing first-run setup steps.
|
||||
* [REPL](https://github.com/Dark-Alex-17/loki/wiki/REPL): Interactive Read-Eval-Print Loop for conversational interactions with LLMs and Loki.
|
||||
* [Custom REPL Prompt](https://github.com/Dark-Alex-17/loki/wiki/REPL-Prompt): Customize the REPL prompt to provide useful contextual information.
|
||||
* [Vault](https://github.com/Dark-Alex-17/loki/wiki/Vault): Securely store and manage sensitive information such as API keys and credentials.
|
||||
* [Shell Integrations](https://github.com/Dark-Alex-17/loki/wiki/Shell-Integrations): Seamlessly integrate Loki with your shell environment for enhanced command-line assistance.
|
||||
* [Function Calling](https://github.com/Dark-Alex-17/loki/wiki/Tools): Leverage function calling capabilities to extend Loki's functionality with custom tools
|
||||
* [Creating Custom Tools](https://github.com/Dark-Alex-17/loki/wiki/Custom-Tools): You can create your own custom tools to enhance Loki's capabilities.
|
||||
* [Create Custom Python Tools](https://github.com/Dark-Alex-17/loki/wiki/Custom-Tools#custom-python-based-tools)
|
||||
* [Create Custom TypeScript Tools](https://github.com/Dark-Alex-17/loki/wiki/Custom-Tools#custom-typescript-based-tools)
|
||||
* [Create Custom Bash Tools](https://github.com/Dark-Alex-17/loki/wiki/Custom-Bash-Tools)
|
||||
* [Bash Prompt Utilities](https://github.com/Dark-Alex-17/loki/wiki/Bash-Prompt-Helpers)
|
||||
* [First-Class MCP Server Support](https://github.com/Dark-Alex-17/loki/wiki/MCP-Servers): Easily connect and interact with MCP servers for advanced functionality.
|
||||
* [Macros](https://github.com/Dark-Alex-17/loki/wiki/Macros): Automate repetitive tasks and workflows with Loki "scripts" (macros).
|
||||
* [RAG](https://github.com/Dark-Alex-17/loki/wiki/RAG): Retrieval-Augmented Generation for enhanced information retrieval and generation.
|
||||
* [Sessions](https://github.com/Dark-Alex-17/loki/wiki/Sessions): Manage and persist conversational contexts and settings across multiple interactions.
|
||||
* [Roles](https://github.com/Dark-Alex-17/loki/wiki/Roles): Customize model behavior for specific tasks or domains.
|
||||
* [Agents](https://github.com/Dark-Alex-17/loki/wiki/Agents): Leverage AI agents to perform complex tasks and workflows, including sub-agent spawning, teammate messaging, and user interaction tools.
|
||||
* [Todo System](https://github.com/Dark-Alex-17/loki/wiki/TODO-System): Built-in task tracking for improved agent reliability with smaller models.
|
||||
* [Environment Variables](https://github.com/Dark-Alex-17/loki/wiki/Environment-Variables): Override and customize your Loki configuration at runtime with environment variables.
|
||||
* [Client Configurations](https://github.com/Dark-Alex-17/loki/wiki/Clients): Configuration instructions for various LLM providers.
|
||||
* [Authentication (API Key & OAuth)](https://github.com/Dark-Alex-17/loki/wiki/Clients#authentication): Authenticate with API keys or OAuth for subscription-based access.
|
||||
* [Patching API Requests](https://github.com/Dark-Alex-17/loki/wiki/Patches): Learn how to patch API requests for advanced customization.
|
||||
* [Custom Themes](https://github.com/Dark-Alex-17/loki/wiki/Themes): Change the look and feel of Loki to your preferences with custom themes.
|
||||
* [REPL](./docs/REPL.md): Interactive Read-Eval-Print Loop for conversational interactions with LLMs and Loki.
|
||||
* [Custom REPL Prompt](./docs/REPL-PROMPT.md): Customize the REPL prompt to provide useful contextual information.
|
||||
* [Vault](./docs/VAULT.md): Securely store and manage sensitive information such as API keys and credentials.
|
||||
* [Shell Integrations](./docs/SHELL-INTEGRATIONS.md): Seamlessly integrate Loki with your shell environment for enhanced command-line assistance.
|
||||
* [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.
|
||||
* [Macros](./docs/MACROS.md): Automate repetitive tasks and workflows with Loki "scripts" (macros).
|
||||
* [RAG](./docs/RAG.md): Retrieval-Augmented Generation for enhanced information retrieval and generation.
|
||||
* [Sessions](/docs/SESSIONS.md): Manage and persist conversational contexts and settings across multiple interactions.
|
||||
* [Roles](./docs/ROLES.md): Customize model behavior for specific tasks or domains.
|
||||
* [Agents](/docs/AGENTS.md): Leverage AI agents to perform complex tasks and workflows, including sub-agent spawning, teammate messaging, and user interaction tools.
|
||||
* [Todo System](./docs/TODO-SYSTEM.md): Built-in task tracking for improved agent reliability with smaller models.
|
||||
* [Environment Variables](./docs/ENVIRONMENT-VARIABLES.md): Override and customize your Loki configuration at runtime with environment variables.
|
||||
* [Client Configurations](./docs/clients/CLIENTS.md): Configuration instructions for various LLM providers.
|
||||
* [Authentication (API Key & OAuth)](./docs/clients/CLIENTS.md#authentication): Authenticate with API keys or OAuth for subscription-based access.
|
||||
* [Patching API Requests](./docs/clients/PATCHES.md): Learn how to patch API requests for advanced customization.
|
||||
* [Custom Themes](./docs/THEMES.md): Change the look and feel of Loki to your preferences with custom themes.
|
||||
* [History](#history): A history of how Loki came to be.
|
||||
|
||||
## Prerequisites
|
||||
@@ -153,7 +154,7 @@ loki --list-secrets
|
||||
|
||||
### Authentication
|
||||
Each client in your configuration needs authentication (with a few exceptions; e.g. ollama). Most clients use an API key
|
||||
(set via `api_key` in the config or through the [vault](https://github.com/Dark-Alex-17/loki/wiki/Vault)). For providers that support OAuth (e.g. Claude Pro/Max
|
||||
(set via `api_key` in the config or through the [vault](./docs/VAULT.md)). For providers that support OAuth (e.g. Claude Pro/Max
|
||||
subscribers, Google Gemini), you can authenticate with your existing subscription instead:
|
||||
|
||||
```yaml
|
||||
@@ -169,7 +170,7 @@ loki --authenticate my-claude-oauth
|
||||
# Or via the REPL: .authenticate
|
||||
```
|
||||
|
||||
For full details, see the [authentication documentation](https://github.com/Dark-Alex-17/loki/wiki/Clients#authentication).
|
||||
For full details, see the [authentication documentation](./docs/clients/CLIENTS.md#authentication).
|
||||
|
||||
### Tab-Completions
|
||||
You can also enable tab completions to make using Loki easier. To do so, add the following to your shell profile:
|
||||
@@ -246,7 +247,7 @@ shown below:
|
||||
|
||||
| Setting | Description |
|
||||
|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `repl_prelude` | This setting lets you specify a default `session` or `role` to use when starting Loki in [REPL](https://github.com/Dark-Alex-17/loki/wiki/REPL) mode. <br>Values can be <ul><li>`role:<name>` to define a role</li><li>`session:<name>` to define a session</li><li>`<session>:<role>` to define both a session and a role to use</li></ul> |
|
||||
| `repl_prelude` | This setting lets you specify a default `session` or `role` to use when starting Loki in [REPL](./docs/REPL.md) mode. <br>Values can be <ul><li>`role:<name>` to define a role</li><li>`session:<name>` to define a session</li><li>`<session>:<role>` to define both a session and a role to use</li></ul> |
|
||||
| `cmd_prelude` | This setting lets you specify a default `session` or `role` to use when running one-off queries in Loki via the CLI. <br>Values can be <ul><li>`role:<name>` to define a role</li><li>`session:<name>` to define a session</li><li>`<session>:<role>` to define both a session and a role to use</li></ul> |
|
||||
| `agent_session` | This setting is used to specify a default session that all agents should start into, unless otherwise specified in the agent configuration. (e.g. `temp`, `default`) |
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"type": "stdio",
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
@@ -16,17 +15,14 @@
|
||||
}
|
||||
},
|
||||
"atlassian": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-remote@0.1.13", "https://mcp.atlassian.com/v1/mcp"]
|
||||
},
|
||||
"docker": {
|
||||
"type": "stdio",
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-docker"]
|
||||
},
|
||||
"ddg-search": {
|
||||
"type": "stdio",
|
||||
"command": "uvx",
|
||||
"args": ["duckduckgo-mcp-server"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# @meta require-tools jira
|
||||
# @describe Query for jira issues using a Jira Query Language (JQL) query
|
||||
# @option --jql-query! The Jira Query Language query to execute
|
||||
# @env LLM_OUTPUT=/dev/stdout The output path
|
||||
|
||||
main() {
|
||||
jira issue ls -q "$argc_jql_query" --plain >> "$LLM_OUTPUT"
|
||||
}
|
||||
@@ -0,0 +1,775 @@
|
||||
# Agents
|
||||
|
||||
Agents in Loki follow the same style as OpenAI's GPTs. They consist of 3 parts:
|
||||
|
||||
* [Role](./ROLES.md) - Tell the LLM how to behave
|
||||
* [RAG](./RAG.md) - Pre-built knowledge bases specifically for the agent
|
||||
* [Function Calling](./function-calling/TOOLS.md#tools) ([#2](./function-calling/MCP-SERVERS.md)) - Extends the functionality of the LLM through custom functions it can call
|
||||
|
||||

|
||||
|
||||
Agent configuration files are stored in the `agents` subdirectory of your Loki configuration directory. The location of
|
||||
this directory varies between systems so you can use the following command to locate yours:
|
||||
|
||||
```shell
|
||||
loki --info | grep 'agents_dir' | awk '{print $2}'
|
||||
```
|
||||
|
||||
If you're looking for more example agents, refer to the [built-in agents](../assets/agents).
|
||||
|
||||
## Quick Links
|
||||
<!--toc:start-->
|
||||
- [Directory Structure](#directory-structure)
|
||||
- [Metadata](#1-metadata)
|
||||
- [2. Define the Instructions](#2-define-the-instructions)
|
||||
- [Static Instructions](#static-instructions)
|
||||
- [Special Variables](#special-variables)
|
||||
- [User-Defined Variables](#user-defined-variables)
|
||||
- [Dynamic Instructions](#dynamic-instructions)
|
||||
- [Variables](#variables)
|
||||
- [3. Initializing RAG](#3-initializing-rag)
|
||||
- [4. Building Tools for Agents](#4-building-tools-for-agents)
|
||||
- [Limitations](#limitations)
|
||||
- [.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)
|
||||
- [Configuration](#spawning-configuration)
|
||||
- [Spawning & Collecting Agents](#spawning--collecting-agents)
|
||||
- [Task Queue with Dependencies](#task-queue-with-dependencies)
|
||||
- [Active Task Dispatch](#active-task-dispatch)
|
||||
- [Output Summarization](#output-summarization)
|
||||
- [Teammate Messaging](#teammate-messaging)
|
||||
- [Runaway Safeguards](#runaway-safeguards)
|
||||
- [8. User Interaction Tools](#8-user-interaction-tools)
|
||||
- [Available Tools](#user-interaction-available-tools)
|
||||
- [Escalation (Sub-Agent to User)](#escalation-sub-agent-to-user)
|
||||
- [9. Auto-Injected Prompts](#9-auto-injected-prompts)
|
||||
- [Built-In Agents](#built-in-agents)
|
||||
<!--toc:end-->
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
Agent configurations often have the following directory structure:
|
||||
|
||||
```
|
||||
<loki-config-dir>/agents
|
||||
└── my-agent
|
||||
├── config.yaml
|
||||
├── 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`, `tools.py`, or `tools.ts`).
|
||||
|
||||
To see a full example configuration file, refer to the [example agent config file](../config.agent.example.yaml).
|
||||
|
||||
The best way to understand how an agent is built is to go step by step in the following manner:
|
||||
|
||||
---
|
||||
|
||||
## 1. Metadata
|
||||
Agent configurations have the following settings available to customize each agent:
|
||||
|
||||
```yaml
|
||||
# Model Configuration
|
||||
model: openai:gpt-4o # Specify the LLM to use
|
||||
temperature: null # Set default temperature parameter, range (0, 1)
|
||||
top_p: null # Set default top-p parameter, with a range of (0, 1) or (0, 2), depending on the model
|
||||
# Agent Metadata Configuration
|
||||
agent_session: null # Set a session to use when starting the agent. (e.g. temp, default); defaults to globally set agent_session
|
||||
# Agent Configuration
|
||||
name: <agent-name> # Name of the agent, used in the UI and logs
|
||||
description: <description> # Description of the agent, used in the UI
|
||||
version: 1 # Version of the agent
|
||||
# Function Calling Configuration
|
||||
mcp_servers: # Optional list of MCP servers that the agent utilizes
|
||||
- github # Corresponds to the name of an MCP server in the `<loki-config-dir>/functions/mcp.json` file
|
||||
global_tools: # Optional list of additional global tools to enable for the agent; i.e. not tools specific to the agent
|
||||
- web_search
|
||||
- fs
|
||||
- python
|
||||
# Todo System & Auto-Continuation (see "Todo System & Auto-Continuation" section below)
|
||||
auto_continue: false # Enable automatic continuation when incomplete todos remain
|
||||
max_auto_continues: 10 # Maximum continuation attempts before stopping
|
||||
inject_todo_instructions: true # Inject todo tool instructions into system prompt
|
||||
continuation_prompt: null # Custom prompt for continuations (optional)
|
||||
# Sub-Agent Spawning (see "Sub-Agent Spawning System" section below)
|
||||
can_spawn_agents: false # Enable spawning child agents
|
||||
max_concurrent_agents: 4 # Max simultaneous child agents
|
||||
max_agent_depth: 3 # Max nesting depth (prevents runaway)
|
||||
inject_spawn_instructions: true # Inject spawning instructions into system prompt
|
||||
summarization_model: null # Model for summarizing sub-agent output (e.g. 'openai:gpt-4o-mini')
|
||||
summarization_threshold: 4000 # Char count above which sub-agent output is summarized
|
||||
escalation_timeout: 300 # Seconds sub-agents wait for escalated user input (default: 5 min)
|
||||
```
|
||||
|
||||
As mentioned previously: Agents utilize function calling to extend a model's capabilities. However, agents operate in
|
||||
isolated environment, so in order for an agent to use a tool or MCP server that you have defined globally, you must
|
||||
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`, `agents/my-agent/tools.py`, or `agents/my-agent/tools.ts`, then the agent is really just a
|
||||
`role`.
|
||||
|
||||
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).
|
||||
|
||||
## 2. Define the Instructions
|
||||
At their heart, agents function similarly to roles in that they tell the model how to behave. Agent configuration files
|
||||
have the following settings for the instruction definitions:
|
||||
|
||||
```yaml
|
||||
dynamic_instructions: # Whether to use dynamically generated instructions for the agent; if false, static instructions are used. False by default.
|
||||
instructions: # Static instructions for the LLM; These are ignored if dynamic instructions are used
|
||||
variables: # An array of optional variables that the agent expects and uses
|
||||
```
|
||||
|
||||
### Static Instructions
|
||||
By default, Loki agents use statically defined instructions. Think of them as being identical to the instructions for a
|
||||
[role](./ROLES.md#instructions), because they virtually are.
|
||||
|
||||
**Example:**
|
||||
```yaml
|
||||
instructions: |
|
||||
You are an AI agent designed to demonstrate agentic capabilities
|
||||
```
|
||||
|
||||
Just like roles, agents support variable interpolation at runtime. There's two types of variables that can be
|
||||
interpolated into the instructions at runtime: special variables (like roles have), and user-defined variables. Just
|
||||
like roles, variables are interpolated into your instructions anywhere Loki sees the `{{variable}}` syntax.
|
||||
|
||||
#### Special Variables
|
||||
The following special variables are provided by Loki at runtime and can be injected into your agent's instructions:
|
||||
|
||||
| Name | Description | Example |
|
||||
|-----------------|---------------------------------------------------------------------|----------------------------|
|
||||
| `__os__` | Operating system name | `linux` |
|
||||
| `__os_family__` | Operating system family | `unix` |
|
||||
| `__arch__` | System architecture | `x86_64` |
|
||||
| `__shell__` | The current user's default shell | `bash` |
|
||||
| `__locale__` | The current user's preferred language and region settings | `en-US` |
|
||||
| `__now__` | Current timestamp in ISO 8601 format | `2025-11-07T10:15:44.268Z` |
|
||||
| `__cwd__` | The current working directory | `/tmp` |
|
||||
| `__tools__` | A list of the enabled tools (global + mcp servers + agent-specific) | |
|
||||
|
||||
#### User-Defined Variables
|
||||
Agents also support user-defined variables that can be interpolated into the instructions, and are made available to any
|
||||
agent-specific tools you define (see [Building Tools for Agents](#4-building-tools-for-agents) for more details on how to
|
||||
create agent-specific tooling).
|
||||
|
||||
The `variables` setting in an agent's config has the following fields:
|
||||
|
||||
| Field | Required | Description |
|
||||
|---------------|----------|----------------------------------------------------------------------------------------------------|
|
||||
| `name` | * | The name of the variable |
|
||||
| `description` | * | The description of the field |
|
||||
| `default` | | A default value for the field. If left undefined, the user will be prompted for a value at runtime |
|
||||
|
||||
These variables can be referenced in both the agent's instructions, and in the tool definitions via `LLM_AGENT_VAR_<name>`.
|
||||
|
||||
**Example:**
|
||||
```yaml
|
||||
instructions: |
|
||||
You are an agent who answers questions about a user's system.
|
||||
|
||||
<tools>
|
||||
{{__tools__}}
|
||||
</tools>
|
||||
|
||||
<system>
|
||||
os: {{__os__}}
|
||||
os_family: {{__os_family__}}
|
||||
arch: {{__arch__}}
|
||||
shell: {{__shell__}}
|
||||
locale: {{__locale__}}
|
||||
now: {{__now__}}
|
||||
cwd: {{__cwd__}}
|
||||
</system>
|
||||
|
||||
<user>
|
||||
username: {{username}}
|
||||
</user>
|
||||
variables:
|
||||
- name: username # Accessible from the tool definitions via the `LLM_AGENT_VAR_USERNAME` environment variable
|
||||
description: Your user name
|
||||
```
|
||||
|
||||
### 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`, `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`:
|
||||
```python
|
||||
import json
|
||||
from pathlib import Path
|
||||
from genson import SchemaBuilder
|
||||
|
||||
def _instructions():
|
||||
"""Generates instructions for the agent dynamically"""
|
||||
value = input("Enter a JSON file path OR paste raw JSON: ").strip()
|
||||
if not value:
|
||||
raise SystemExit("A file path or JSON string is required.")
|
||||
|
||||
p = Path(value)
|
||||
if p.exists() and p.is_file():
|
||||
json_file_path = str(p.resolve())
|
||||
json_text = p.read_text(encoding="utf-8")
|
||||
else:
|
||||
try:
|
||||
json.loads(value)
|
||||
except json.JSONDecodeError as e:
|
||||
raise SystemExit(f"Input is neither a file nor valid JSON.\n{e}")
|
||||
json_file_path = "<provided-inline-json>"
|
||||
json_text = value
|
||||
|
||||
try:
|
||||
data = json.loads(json_text)
|
||||
except json.JSONDecodeError as e:
|
||||
raise SystemExit(f"Provided content is not valid JSON.\n{e}")
|
||||
|
||||
builder = SchemaBuilder()
|
||||
builder.add_object(data)
|
||||
json_schema = builder.to_schema()
|
||||
return f"""
|
||||
You are an AI agent that can view and filter JSON data with jq.
|
||||
|
||||
## Context
|
||||
json_file_path: {json_file_path}
|
||||
json_schema: {json.dumps(json_schema, indent=2)}
|
||||
"""
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
`agents/json-reader/tools.sh`:
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# @meta require-tools jq,genson
|
||||
# @env LLM_OUTPUT=/dev/stdout The output path
|
||||
|
||||
# @cmd Generates instructions for the agent dynamically
|
||||
_instructions() {
|
||||
read -r -p "Enter a JSON file path OR paste raw JSON: " value
|
||||
|
||||
if [[ -z "${value}" ]]; then
|
||||
echo "A file path or JSON string is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
json_file_path=""
|
||||
inline_temp=""
|
||||
cleanup() {
|
||||
[[ -n "${inline_temp:-}" && -f "${inline_temp}" ]] && rm -f "${inline_temp}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [[ -f "${value}" ]]; then
|
||||
json_file_path="$(realpath "${value}")"
|
||||
if ! jq empty "${json_file_path}" >/dev/null 2>&1; then
|
||||
echo "Error: File does not contain valid JSON: ${json_file_path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
inline_temp="$(mktemp)"
|
||||
printf "%s" "${value}" > "${inline_temp}"
|
||||
if ! jq empty "${inline_temp}" >/dev/null 2>&1; then
|
||||
echo "Error: Input is neither a file nor valid JSON." >&2
|
||||
exit 1
|
||||
fi
|
||||
json_file_path="<provided-inline-json>"
|
||||
fi
|
||||
|
||||
source_file="${json_file_path}"
|
||||
if [[ "${json_file_path}" == "<provided-inline-json>" ]]; then
|
||||
source_file="${inline_temp}"
|
||||
fi
|
||||
|
||||
json_schema="$(genson < "${source_file}" | jq -c '.')"
|
||||
cat <<EOF >> "$LLM_OUTPUT"
|
||||
You are an AI agent that can view and filter JSON data with jq.
|
||||
|
||||
## Context
|
||||
json_file_path: ${json_file_path}
|
||||
json_schema: ${json_schema}
|
||||
EOF
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
more information on what variables are available and how to use them, refer to the [Special Variables](#special-variables)
|
||||
and [User-Defined Variables](#user-defined-variables) sections above.
|
||||
|
||||
## 3. Initializing RAG
|
||||
Each agent you create also has a dedicated knowledge base that adds additional context to your queries and helps the LLM
|
||||
answer queries effectively. The documents to load into RAG are defined in the `documents` array of your agent
|
||||
configuration file:
|
||||
|
||||
```yaml
|
||||
documents:
|
||||
- https://www.ohdsi.org/data-standardization/
|
||||
- https://github.com/OHDSI/Vocabulary-v5.0/wiki/**
|
||||
- OMOPCDM_ddl.sql # Relative path to agent (i.e. file lives at '<loki-config-dir>/agents/my-agent/OMOPCDM_ddl.sql')
|
||||
```
|
||||
|
||||
These documents use the same syntax as those you'd define when constructing RAG normally. To see all the available types
|
||||
of documents that Loki supports and how to use custom document loaders, refer to the [RAG documentation](./RAG.md#supported-document-sources).
|
||||
|
||||
Anytime your agent starts up, it will automatically be using the RAG you've defined here.
|
||||
|
||||
## 4. Building Tools for Agents
|
||||
Building tools for agents is virtually identical to building custom tools, with one slight difference: instead of
|
||||
defining a single function that gets executed at runtime (e.g. `main` for bash tools and `run` for Python tools), agent
|
||||
tools define a number of *subcommands*.
|
||||
|
||||
### Limitations
|
||||
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 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:
|
||||
|
||||
* **Do not give agents the same name as an executable**. Loki compiles the tools for each agent into a binary that it
|
||||
temporarily places on your path during execution. If you have a binary with the same name as your agent, then your
|
||||
shell may execute the existing binary instead of your agent's tools
|
||||
* **`LLM_ROOT_DIR` points to the agent's configuration directory**. This is where agents differ slightly from normal
|
||||
tools: The `LLM_ROOT_DIR` environment variable does *not* point to the `functions/tools` directory like it does in
|
||||
global tools. Instead, it points to the agent's configuration directory, making it easier to source scripts and other
|
||||
miscellaneous files
|
||||
|
||||
### .env File Support
|
||||
When Loki loads an agent, it will also search the agent's configuration directory for a `.env` file. If found, all
|
||||
environment variables defined in the file will be made available to the agent's tools.
|
||||
|
||||
### Python-Based Agent Tools
|
||||
Python-based tools are defined exactly the same as they are for custom tool definitions. The only difference is that
|
||||
instead of a single `run` function, you define as many as you like with whatever arguments you like.
|
||||
|
||||
**Example:**
|
||||
`agents/my-agent/tools.py`
|
||||
```python
|
||||
import urllib.request
|
||||
|
||||
def get_ip_info():
|
||||
"""
|
||||
Get your IP information
|
||||
"""
|
||||
with urllib.request.urlopen("https://httpbin.org/ip") as response:
|
||||
data = response.read()
|
||||
return data.decode('utf-8')
|
||||
|
||||
def get_ip_address_from_aws():
|
||||
"""
|
||||
Find your public IP address using AWS
|
||||
"""
|
||||
with urllib.request.urlopen("https://checkip.amazonaws.com") as response:
|
||||
data = response.read()
|
||||
return data.decode('utf-8')
|
||||
```
|
||||
|
||||
Loki automatically compiles these as separate functions for the LLM to call. No extra work is needed. Just make sure you
|
||||
follow all the same steps to define each function as you would when creating custom Python tools.
|
||||
|
||||
For more information on how to build tools in Python, refer to the [custom Python tools documentation](./function-calling/CUSTOM-TOOLS.md#custom-python-based-tools)
|
||||
|
||||
### Bash-Based Agent Tools
|
||||
Bash-based agent tools are virtually identical to custom bash tools, with only one difference. Instead of defining a
|
||||
single entrypoint via the `main` function, you actually define as many subcommands as you like.
|
||||
|
||||
**Example:**
|
||||
`agents/my-agent/tools.sh`
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# @env LLM_OUTPUT=/dev/stdout The output path
|
||||
# @describe Discover network information about your computer and its place in the internet
|
||||
|
||||
# Use the `@cmd` annotation to define subcommands for your script.
|
||||
# @cmd Get your IP information
|
||||
get_ip_info() {
|
||||
curl -fsSL https://httpbin.org/ip >> "$LLM_OUTPUT"
|
||||
}
|
||||
|
||||
# @cmd Find your public IP address using AWS
|
||||
get_ip_address_from_aws() {
|
||||
curl -fsSL https://checkip.amazonaws.com >> "$LLM_OUTPUT"
|
||||
}
|
||||
```
|
||||
To compile the script so it's executable and testable:
|
||||
```bash
|
||||
$ loki --build-tools
|
||||
```
|
||||
|
||||
Then you can execute your script (assuming your current working directory is `agents/my-agent`):
|
||||
```bash
|
||||
$ ./tools.sh get_ip_info
|
||||
$ ./tools.sh get_ip_address_from_aws
|
||||
```
|
||||
|
||||
All other special annotations (`@env`, `@arg`, `@option` `@flags`) apply to subcommands as well, so be sure to follow
|
||||
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.
|
||||
|
||||
They are defined using the `conversation_starters` setting in your agent's configuration file:
|
||||
|
||||
**Example:**
|
||||
`agents/my-agent/config.yaml`:
|
||||
```yaml
|
||||
conversation_starters:
|
||||
- What is my username?
|
||||
- What is my current shell?
|
||||
- What is my ip?
|
||||
- How much disk space is left on my PC??
|
||||
- How to create an agent?
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 6. Todo System & Auto-Continuation
|
||||
|
||||
Loki includes a built-in task tracking system designed to improve the reliability of agents, especially when using
|
||||
smaller language models. The Todo System helps models:
|
||||
|
||||
- Break complex tasks into manageable steps
|
||||
- Track progress through multi-step workflows
|
||||
- Automatically continue work until all tasks are complete
|
||||
|
||||
### Quick Configuration
|
||||
|
||||
```yaml
|
||||
# agents/my-agent/config.yaml
|
||||
auto_continue: true # Enable auto-continuation
|
||||
max_auto_continues: 10 # Max continuation attempts
|
||||
inject_todo_instructions: true # Include the default todo instructions into prompt
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
1. When `inject_todo_instructions` is enabled, agents receive instructions on using five built-in tools:
|
||||
- `todo__init`: Initialize a todo list with a goal
|
||||
- `todo__add`: Add a task to the list
|
||||
- `todo__done`: Mark a task complete
|
||||
- `todo__list`: View current todo state
|
||||
- `todo__clear`: Clear the entire todo list and reset the goal
|
||||
|
||||
These instructions are a reasonable default that detail how to use Loki's To-Do System. If you wish,
|
||||
you can disable the injection of the default instructions and specify your own instructions for how
|
||||
to use the To-Do System into your main `instructions` for the agent.
|
||||
|
||||
2. When `auto_continue` is enabled and the model stops with incomplete tasks, Loki automatically sends a
|
||||
continuation prompt with the current todo state, nudging the model to continue working.
|
||||
|
||||
3. This continues until all tasks are done or `max_auto_continues` is reached.
|
||||
|
||||
### When to Use
|
||||
|
||||
- Multistep tasks where the model might lose track
|
||||
- Smaller models that need more structure
|
||||
- Workflows requiring guaranteed completion of all steps
|
||||
|
||||
For complete documentation including all configuration options, tool details, and best practices, see the
|
||||
[Todo System Guide](./TODO-SYSTEM.md).
|
||||
|
||||
## 7. Sub-Agent Spawning System
|
||||
|
||||
Loki agents can spawn and manage child agents that run **in parallel** as background tasks inside the same process.
|
||||
This enables orchestrator-style agents that delegate specialized work to other agents, similar to how tools like
|
||||
Claude Code or OpenCode handle complex multi-step tasks.
|
||||
|
||||
For a working example of an orchestrator agent that uses sub-agent spawning, see the built-in
|
||||
[sisyphus](../assets/agents/sisyphus) agent. For an example of the teammate messaging pattern with parallel sub-agents,
|
||||
see the [code-reviewer](../assets/agents/code-reviewer) agent.
|
||||
|
||||
### Spawning Configuration
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
|-----------------------------|---------|---------------|--------------------------------------------------------------------------------|
|
||||
| `can_spawn_agents` | boolean | `false` | Enable this agent to spawn child agents |
|
||||
| `max_concurrent_agents` | integer | `4` | Maximum number of child agents that can run simultaneously |
|
||||
| `max_agent_depth` | integer | `3` | Maximum nesting depth for sub-agents (prevents runaway spawning chains) |
|
||||
| `inject_spawn_instructions` | boolean | `true` | Inject the default spawning instructions into the agent's system prompt |
|
||||
| `summarization_model` | string | current model | Model to use for summarizing long sub-agent output (e.g. `openai:gpt-4o-mini`) |
|
||||
| `summarization_threshold` | integer | `4000` | Character count above which sub-agent output is summarized before returning |
|
||||
| `escalation_timeout` | integer | `300` | Seconds a sub-agent waits for an escalated user interaction response |
|
||||
|
||||
**Example configuration:**
|
||||
```yaml
|
||||
# agents/my-orchestrator/config.yaml
|
||||
can_spawn_agents: true
|
||||
max_concurrent_agents: 6
|
||||
max_agent_depth: 2
|
||||
inject_spawn_instructions: true
|
||||
summarization_model: openai:gpt-4o-mini
|
||||
summarization_threshold: 3000
|
||||
escalation_timeout: 600
|
||||
```
|
||||
|
||||
### Spawning & Collecting Agents
|
||||
|
||||
When `can_spawn_agents` is enabled, the agent receives tools for spawning and managing child agents:
|
||||
|
||||
| Tool | Description |
|
||||
|------------------|-------------------------------------------------------------------------|
|
||||
| `agent__spawn` | Spawn a child agent in the background. Returns an agent ID immediately. |
|
||||
| `agent__check` | Non-blocking check: is the agent done? Returns `PENDING` or the result. |
|
||||
| `agent__collect` | Blocking wait: wait for an agent to finish, return its output. |
|
||||
| `agent__list` | List all spawned agents and their status. |
|
||||
| `agent__cancel` | Cancel a running agent by ID. |
|
||||
|
||||
The core pattern is **Spawn -> Continue -> Collect**:
|
||||
|
||||
```
|
||||
# 1. Spawn agents in parallel (returns IDs immediately)
|
||||
agent__spawn --agent explore --prompt "Find auth middleware patterns in src/"
|
||||
agent__spawn --agent explore --prompt "Find error handling patterns in src/"
|
||||
|
||||
# 2. Continue your own work while they run
|
||||
|
||||
# 3. Check if done (non-blocking)
|
||||
agent__check --id agent_explore_a1b2c3d4
|
||||
|
||||
# 4. Collect results when ready (blocking)
|
||||
agent__collect --id agent_explore_a1b2c3d4
|
||||
agent__collect --id agent_explore_e5f6g7h8
|
||||
```
|
||||
|
||||
Any agent defined in your `<loki-config-dir>/agents/` directory can be spawned as a child. Child agents:
|
||||
- Run in a fully isolated environment (separate session, config, and tools)
|
||||
- Have their output suppressed from the terminal (no spinner, no tool call logging)
|
||||
- Return their accumulated output to the parent when collected
|
||||
|
||||
### Task Queue with Dependencies
|
||||
|
||||
For complex workflows where tasks have ordering requirements, the spawning system includes a dependency-aware
|
||||
task queue:
|
||||
|
||||
| Tool | Description |
|
||||
|------------------------|-----------------------------------------------------------------------------|
|
||||
| `agent__task_create` | Create a task with optional dependencies and auto-dispatch agent. |
|
||||
| `agent__task_list` | List all tasks with their status, dependencies, and assignments. |
|
||||
| `agent__task_complete` | Mark a task done. Returns newly unblocked tasks and auto-dispatches agents. |
|
||||
| `agent__task_fail` | Mark a task as failed. Dependents remain blocked. |
|
||||
|
||||
```
|
||||
# Create tasks with dependency ordering
|
||||
agent__task_create --subject "Explore existing patterns"
|
||||
agent__task_create --subject "Implement feature" --blocked_by ["task_1"]
|
||||
agent__task_create --subject "Write tests" --blocked_by ["task_2"]
|
||||
|
||||
# Mark tasks complete to unblock dependents
|
||||
agent__task_complete --task_id task_1
|
||||
```
|
||||
|
||||
### Active Task Dispatch
|
||||
|
||||
Tasks can optionally specify an agent to auto-spawn when the task becomes runnable:
|
||||
|
||||
```
|
||||
agent__task_create \
|
||||
--subject "Implement the auth module" \
|
||||
--blocked_by ["task_1"] \
|
||||
--agent coder \
|
||||
--prompt "Implement auth module based on patterns found in task_1"
|
||||
```
|
||||
|
||||
When `task_1` completes and the dependent task becomes unblocked, an agent is automatically spawned with the
|
||||
specified prompt. No manual intervention needed. This enables fully automated multi-step pipelines.
|
||||
|
||||
### Output Summarization
|
||||
|
||||
When a child agent produces long output, it can be automatically summarized before returning to the parent.
|
||||
This keeps parent context windows manageable.
|
||||
|
||||
- If the output exceeds `summarization_threshold` characters (default: 4000), it is sent through an LLM
|
||||
summarization pass
|
||||
- The `summarization_model` setting lets you use a cheaper/faster model for summarization (e.g. `gpt-4o-mini`)
|
||||
- If `summarization_model` is not set, the parent's current model is used
|
||||
- The summarization preserves all actionable information: code snippets, file paths, error messages, and
|
||||
concrete recommendations
|
||||
|
||||
### Teammate Messaging
|
||||
|
||||
All agents (including children) automatically receive tools for **direct sibling-to-sibling messaging**:
|
||||
|
||||
| Tool | Description |
|
||||
|-----------------------|-----------------------------------------------------|
|
||||
| `agent__send_message` | Send a text message to another agent's inbox by ID. |
|
||||
| `agent__check_inbox` | Drain all pending messages from your inbox. |
|
||||
|
||||
This enables coordination patterns where child agents share cross-cutting findings:
|
||||
|
||||
```
|
||||
# Agent A discovers something relevant to Agent B
|
||||
agent__send_message --id agent_reviewer_b1c2d3e4 --message "Found a security issue in auth.rs line 42"
|
||||
|
||||
# Agent B checks inbox before finalizing
|
||||
agent__check_inbox
|
||||
```
|
||||
|
||||
Messages are routed through the parent's supervisor. A parent can message its children, and children can message
|
||||
their siblings. For a working example of the teammate pattern, see the built-in
|
||||
[code-reviewer](../assets/agents/code-reviewer) agent, which spawns file-specific reviewers that share
|
||||
cross-cutting findings with each other.
|
||||
|
||||
### Runaway Safeguards
|
||||
|
||||
The spawning system includes built-in safeguards to prevent runaway agent chains:
|
||||
|
||||
- **`max_concurrent_agents`:** Caps how many agents can run at once (default: 4). Spawn attempts beyond this
|
||||
limit return an error asking the agent to wait or cancel existing agents.
|
||||
- **`max_agent_depth`:** Caps nesting depth (default: 3). A child agent spawning its own child increments the
|
||||
depth counter. Attempts beyond the limit are rejected.
|
||||
- **`can_spawn_agents`:** Only agents with this flag set to `true` can spawn children. By default, spawning is
|
||||
disabled. This means child agents cannot spawn their own children unless you explicitly create them with
|
||||
`can_spawn_agents: true` in their config.
|
||||
|
||||
## 8. User Interaction Tools
|
||||
|
||||
Loki includes built-in tools for agents (and the REPL) to interactively prompt the user for input. These tools
|
||||
are **always available**. No configuration needed. They are automatically injected into every agent and into
|
||||
REPL mode when function calling is enabled.
|
||||
|
||||
### User Interaction Available Tools
|
||||
|
||||
| Tool | Description | Returns |
|
||||
|------------------|-----------------------------------------|----------------------------------|
|
||||
| `user__ask` | Present a single-select list of options | The selected option string |
|
||||
| `user__confirm` | Ask a yes/no question | `"yes"` or `"no"` |
|
||||
| `user__input` | Request free-form text input | The text entered by the user |
|
||||
| `user__checkbox` | Present a multi-select checkbox list | Array of selected option strings |
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `user__ask`: `--question "..." --options ["Option A", "Option B", "Option C"]`
|
||||
- `user__confirm`: `--question "..."`
|
||||
- `user__input`: `--question "..."`
|
||||
- `user__checkbox`: `--question "..." --options ["Option A", "Option B", "Option C"]`
|
||||
|
||||
At the top level (depth 0), these tools render interactive terminal prompts directly using arrow-key navigation,
|
||||
checkboxes, and text input fields.
|
||||
|
||||
### Escalation (Sub-Agent to User)
|
||||
|
||||
When a **child agent** (depth > 0) calls a `user__*` tool, it cannot prompt the terminal directly. Instead,
|
||||
the request is **automatically escalated** to the root agent:
|
||||
|
||||
1. The child agent calls `user__ask(...)` and **blocks**, waiting for a reply
|
||||
2. The root agent sees a `pending_escalations` notification in its next tool results
|
||||
3. The root agent either answers from context or prompts the user itself, then calls
|
||||
`agent__reply_escalation` to unblock the child
|
||||
4. The child receives the reply and continues
|
||||
|
||||
The escalation timeout is configurable via `escalation_timeout` in the agent's `config.yaml` (default: 300
|
||||
seconds / 5 minutes). If the timeout expires, the child receives a fallback message asking it to use its
|
||||
best judgment.
|
||||
|
||||
| Tool | Description |
|
||||
|---------------------------|--------------------------------------------------------------------------|
|
||||
| `agent__reply_escalation` | Reply to a pending child escalation, unblocking the waiting child agent. |
|
||||
|
||||
This tool is automatically available to any agent with `can_spawn_agents: true`.
|
||||
|
||||
## 9. Auto-Injected Prompts
|
||||
|
||||
Loki automatically appends usage instructions to your agent's system prompt for each enabled built-in system.
|
||||
These instructions are injected into both **static and dynamic instructions** after your own instructions,
|
||||
ensuring agents always know how to use their available tools.
|
||||
|
||||
| System | Injected When | Toggle |
|
||||
|--------------------|----------------------------------------------------------------|-----------------------------|
|
||||
| Todo tools | `auto_continue: true` AND `inject_todo_instructions: true` | `inject_todo_instructions` |
|
||||
| Spawning tools | `can_spawn_agents: true` AND `inject_spawn_instructions: true` | `inject_spawn_instructions` |
|
||||
| Teammate messaging | Always (all agents) | None (always injected) |
|
||||
| User interaction | Always (all agents) | None (always injected) |
|
||||
|
||||
If you prefer to write your own instructions for a system, set the corresponding `inject_*` flag to `false`
|
||||
and include your custom instructions in the agent's `instructions` field. The built-in tools will still be
|
||||
available; only the auto-injected prompt text is suppressed.
|
||||
|
||||
## Built-In Agents
|
||||
Loki comes packaged with some useful built-in agents:
|
||||
|
||||
* `coder`: An agent to assist you with all your coding tasks
|
||||
* `code-reviewer`: A [CodeRabbit](https://coderabbit.ai)-style code reviewer that spawns per-file reviewers using the teammate messaging pattern
|
||||
* `demo`: An example agent to use for reference when learning to create your own agents
|
||||
* `explore`: An agent designed to help you explore and understand your codebase
|
||||
* `file-reviewer`: An agent designed to perform code-review on a single file (used by the `code-reviewer` agent)
|
||||
* `jira-helper`: An agent that assists you with all your Jira-related tasks
|
||||
* `oracle`: An agent for high-level architecture, design decisions, and complex debugging
|
||||
* `sisyphus`: A powerhouse orchestrator agent for writing complex code and acting as a natural language interface for your codebase (similar to ClaudeCode, Gemini CLI, Codex, or OpenCode). Uses sub-agent spawning to delegate to `explore`, `coder`, and `oracle`.
|
||||
* `sql`: A universal SQL agent that enables you to talk to any relational database in natural language
|
||||
@@ -0,0 +1,211 @@
|
||||
# AIChat to Loki Migration Guide
|
||||
Loki originally started as a fork of AIChat but has since evolved into its own separate project with separate goals.
|
||||
|
||||
As a result, there's some changes you'll need to make to your AIChat configuration to be able to use Loki.
|
||||
|
||||
Be sure you've run `loki` at least once so that the Loki configuration directory and subdirectories exist and is
|
||||
populated with the built-in defaults.
|
||||
|
||||
## Global Configuration File
|
||||
You should be able to copy/paste your AIChat configuration file into your Loki configuration directory. Since the
|
||||
location of the Loki configuration directory varies between systems, you can use the following command to locate your
|
||||
config directory:
|
||||
|
||||
```shell
|
||||
loki --info | grep 'config_dir' | awk '{print $2}'
|
||||
```
|
||||
|
||||
Then, you'll need to make the following changes:
|
||||
|
||||
* `function_calling` -> `function_calling_support`
|
||||
* `use_tools` -> `enabled_tools`
|
||||
* `agent_prelude` -> `agent_session`
|
||||
* `compress_threshold` -> `compression_threshold`
|
||||
* `summarize_prompt` -> `summarization_prompt`
|
||||
* `summary_prompt` -> `summary_context_prompt`
|
||||
|
||||
## Roles
|
||||
Locate your `roles` directory using the following command:
|
||||
|
||||
```shell
|
||||
loki --info | grep 'roles_dir' | awk '{print $2}'
|
||||
```
|
||||
|
||||
Update any roles that have `use_tools` to `enabled_tools`.
|
||||
|
||||
## Sessions
|
||||
Locate your `sessions` directory using the following command:
|
||||
|
||||
```shell
|
||||
loki --info | grep 'sessions_dir' | awk '{print $2}'
|
||||
```
|
||||
|
||||
Update the following settings:
|
||||
* `use_tools` -> `enabled_tools`
|
||||
* `compress_threshold` -> `compression_threshold`
|
||||
* `summarize_prompt` -> `summarization_prompt`
|
||||
* `summary_prompt` -> `summary_context_prompt`
|
||||
|
||||
---
|
||||
|
||||
# LLM Functions Changes
|
||||
Probably the most significant difference between AIChat and Loki is how tools are handled. So if you cloned the
|
||||
[llm-functions](https://github.com/sigoden/llm-functions) repo, you'll need to make the following changes.
|
||||
|
||||
**Note: JavaScript functions are not supported in Loki.**
|
||||
|
||||
The following guide assumes you're using the `llm-functions` repository as your base for custom functions, and thus
|
||||
follows that directory structure.
|
||||
|
||||
## Agents
|
||||
Agents are now all handled in one place: the `agents` directory (`<loki-config-dir>/agents`):
|
||||
|
||||
```shell
|
||||
loki --info | grep 'agents_dir' | awk '{print $2}'
|
||||
```
|
||||
|
||||
And instead of separate `index.yaml` and `config.yaml` files, they're now both in a single `config.yaml` file.
|
||||
|
||||
So now for all of your agents, copy all the contents of those directories to the corresponding directory in the Loki
|
||||
`agents` directory. Then make the following changes:
|
||||
|
||||
* Copy the contents of your `<aichat-config-dir>/functions/agents` directory into `<loki-config-dir/agents`
|
||||
* Merge `index.yaml` into `config.yaml`
|
||||
* If you never created a custom `config.yaml` file, then simply rename `index.yaml` to `config.yaml`
|
||||
* If you've defined an `agent_prelude`, rename that field to `agent_session`
|
||||
* Convert all JavaScript tools to either Python or Bash
|
||||
* For Bash `tools.sh`: Remove the following line:
|
||||
```bash
|
||||
eval "$(argc --argc-eval "$0" "$@")"
|
||||
```
|
||||
* Any `tools.txt` files you have that define what global functions the agent uses is now replaced by the `global_tools`
|
||||
field in the agent's `config.yaml`. So for example: If your `tools.txt` looks like this:
|
||||
```text
|
||||
fs_mkdir.sh
|
||||
fs_ls.sh
|
||||
fs_patch.sh
|
||||
fs_cat.sh
|
||||
```
|
||||
then you need to add the following to your agent's `config.yaml`:
|
||||
```yaml
|
||||
global_tools:
|
||||
- fs_mkdir.sh
|
||||
- fs_ls.sh
|
||||
- fs_patch.sh
|
||||
- fs_cat.sh
|
||||
```
|
||||
* If you have any bash `tools.sh` that depend on the utility scripts in the `llm-functions` repository, they've been
|
||||
replaced by built-in utility scripts. So use the following to replace any matching lines in your `tools.sh` files:
|
||||
```bash
|
||||
##################
|
||||
## Scripts file ##
|
||||
##################
|
||||
ROOT_DIR="${LLM_ROOT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||
# replace with
|
||||
source "$LLM_PROMPT_UTILS_FILE"
|
||||
|
||||
#######################
|
||||
## guard_path script ##
|
||||
#######################
|
||||
"$ROOT_DIR/utils/guard_path.sh"
|
||||
# replace with
|
||||
guard_path
|
||||
|
||||
############################
|
||||
## guard_operation script ##
|
||||
############################
|
||||
"$ROOT_DIR/utils/guard_operation.sh"
|
||||
# replace with
|
||||
guard_operation
|
||||
|
||||
######################
|
||||
## patch.awk script ##
|
||||
######################
|
||||
awk -f "$ROOT_DIR/utils/patch.awk"
|
||||
# replace with
|
||||
patch_file
|
||||
```
|
||||
|
||||
When you're done with this migration, you should have the following:
|
||||
|
||||
* No more `functions/agents` directory
|
||||
* No `functions/agents.txt` file (Loki assumes that if the agent directory exists, it is loadable)
|
||||
* No `<loki-config-dir>/agents/<agent-name>/tools.txt`
|
||||
* No `<loki-config-dir>/agents/<agent-name>/index.yaml`
|
||||
|
||||
## Functions
|
||||
Loki consolidates much of the `llm-functions` repo functionality into one binary. So this means
|
||||
|
||||
* There's no need to have `argc` installed anymore
|
||||
* No separate repository to manage
|
||||
* No `tools.txt`
|
||||
* No `functions.json`
|
||||
* No `functions/mcp` directory at all
|
||||
* No `functions/scripts`
|
||||
|
||||
Here's how to migrate your functions over to Loki from the `llm-functions` repository.
|
||||
|
||||
* Copy your AIChat `<aichat-config-dir>/functions` directory into your Loki config directory
|
||||
* Delete the following files and directories from your `<loki-config-dir>/functions` directory:
|
||||
* `scripts/`
|
||||
* `agents.txt`
|
||||
* `functions.json`
|
||||
* `Argcfile.sh`
|
||||
* `README.md` (irrelevant now)
|
||||
* `LICENSE` (irrelevant now)
|
||||
* `utils/guard_operation.sh`
|
||||
* `utils/guard_path.sh`
|
||||
* `utils/patch.awk`
|
||||
* Everything in `tools.txt` now lives in the global config file under the `visible_tools` setting:
|
||||
```text
|
||||
get_current_weather.sh
|
||||
execute_command.sh
|
||||
web_search.sh
|
||||
#execute_py_code.py
|
||||
query_jira_issues.sh
|
||||
```
|
||||
becomes the following in your `<loki-config-dir>/config.yaml`
|
||||
```yaml
|
||||
visible_tools:
|
||||
- get_current_weather.sh
|
||||
- execute_command.sh
|
||||
- web_search.sh
|
||||
# - web_search.sh
|
||||
- query_jira_issues.sh
|
||||
```
|
||||
* If you've defined a `functions/mcp.json` file, you can leave it alone.
|
||||
* Similarly to agents, if you have any bash `tools.sh` that depend on the utility scripts in the `llm-functions`
|
||||
repository, they've been replaced by built-in utility scripts. So use the following to replace any matching lines in
|
||||
your `tools.sh` files:
|
||||
```bash
|
||||
##################
|
||||
## Scripts file ##
|
||||
##################
|
||||
ROOT_DIR="${LLM_ROOT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||
# replace with
|
||||
source "$LLM_PROMPT_UTILS_FILE"
|
||||
|
||||
#######################
|
||||
## guard_path script ##
|
||||
#######################
|
||||
"$ROOT_DIR/utils/guard_path.sh"
|
||||
# replace with
|
||||
guard_path
|
||||
|
||||
############################
|
||||
## guard_operation script ##
|
||||
############################
|
||||
"$ROOT_DIR/utils/guard_operation.sh"
|
||||
# replace with
|
||||
guard_operation
|
||||
|
||||
######################
|
||||
## patch.awk script ##
|
||||
######################
|
||||
awk -f "$ROOT_DIR/utils/patch.awk"
|
||||
# replace with
|
||||
patch_file
|
||||
```
|
||||
|
||||
Refer to the [custom bash tools docs](./function-calling/CUSTOM-BASH-TOOLS.md) to learn how to compile and test bash
|
||||
tools in Loki without needing to use `argc`.
|
||||
@@ -0,0 +1,113 @@
|
||||
# Environment Variables
|
||||
|
||||
Loki is designed to be highly dynamic and customizable. As a result, Loki utilizes a number of environment variables
|
||||
that can be used to modify its behavior at runtime without needing to modify the existing configuration files.
|
||||
|
||||
Loki also supports defining environment variables via a `.env` file in the Loki configuration directory. This directory
|
||||
varies between systems, so you can find the location of your configuration directory using the following command:
|
||||
|
||||
```shell
|
||||
loki --info | grep 'config_dir' | awk '{print $2}'
|
||||
```
|
||||
|
||||
## Quick Links
|
||||
<!--toc:start-->
|
||||
- [Global Configuration Related Variables](#global-configuration-related-variables)
|
||||
- [Client Related Variables](#client-related-variables)
|
||||
- [Files and Directory Related Variables](#files-and-directory-related-variables)
|
||||
- [Agent Related Variables](#agent-related-variables)
|
||||
- [Logging Related Variables](#logging-related-variables)
|
||||
- [Miscellaneous Variables](#miscellaneous-variables)
|
||||
<!--toc:end-->
|
||||
|
||||
---
|
||||
|
||||
## Global Configuration Related Variables
|
||||
All configuration items in the global config file have environment variables that can be overridden at runtime. To see
|
||||
all configuration options and more thorough descriptions, refer to the [example config file](../config.example.yaml).
|
||||
|
||||
Below are the most commonly used configuration settings and their corresponding environment variables:
|
||||
|
||||
| Setting | Environment Variable |
|
||||
|----------------------------|---------------------------------|
|
||||
| `model` | `LOKI_MODEL` |
|
||||
| `temperature` | `LOKI_TEMPERATURE` |
|
||||
| `top_p` | `LOKI_TOP_P` |
|
||||
| `stream` | `LOKI_STREAM` |
|
||||
| `save` | `LOKI_SAVE` |
|
||||
| `editor` | `LOKI_EDITOR` |
|
||||
| `wrap` | `LOKI_WRAP` |
|
||||
| `wrap_code` | `LOKI_WRAP_CODE` |
|
||||
| `save_session` | `LOKI_SAVE_SESSION` |
|
||||
| `compression_threshold` | `LOKI_COMPRESSION_THRESHOLD` |
|
||||
| `function_calling_support` | `LOKI_FUNCTION_CALLING_SUPPORT` |
|
||||
| `enabled_tools` | `LOKI_ENABLED_TOOLS` |
|
||||
| `mcp_server_support` | `LOKI_MCP_SERVER_SUPPORT` |
|
||||
| `enabled_mcp_servers` | `LOKI_ENABLED_MCP_SERVERS` |
|
||||
| `rag_embedding_model` | `LOKI_RAG_EMBEDDING_MODEL` |
|
||||
| `rag_reranker_model` | `LOKI_RAG_RERANKER_MODEL` |
|
||||
| `rag_top_k` | `LOKI_RAG_TOP_K` |
|
||||
| `rag_chunk_size` | `LOKI_RAG_CHUNK_SIZE` |
|
||||
| `rag_chunk_overlap` | `LOKI_RAG_CHUNK_OVERLAP` |
|
||||
| `highlight` | `LOKI_HIGHLIGHT` |
|
||||
| `theme` | `LOKI_THEME` |
|
||||
| `serve_addr` | `LOKI_SERVE_ADDR` |
|
||||
| `user_agent` | `LOKI_USER_AGENT` |
|
||||
| `save_shell_history` | `LOKI_SAVE_SHELL_HISTORY` |
|
||||
| `sync_models_url` | `LOKI_SYNC_MODELS_URL` |
|
||||
|
||||
|
||||
## Client Related Variables
|
||||
The following environment variables are available for clients in Loki:
|
||||
|
||||
| Environment Variable | Description |
|
||||
|----------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `{client}_API_KEY` | For clients that require an API key, you can define the keys either through environment variables or <br>using the [vault](./VAULT.md). The variables are named after the client to which they apply; <br>e.g. `OPENAI_API_KEY`, `GEMINI_API_KEY`, etc. |
|
||||
| `LOKI_PLATFORM` | Combine with `{client}_API_KEY` to run Loki without a configuration file. <br>This variable is ignored if a configuration file exists. |
|
||||
| `LOKI_PATCH_{client}_CHAT_COMPLETIONS` | Patch chat completion requests to models on the corresponding client; Can modify the URL, body, <br>or headers. |
|
||||
| `LOKI_SHELL` | Specify the shell that Loki should be using when executing commands |
|
||||
|
||||
## Files and Directory Related Variables
|
||||
You can also customize the files and directories that Loki loads its configuration files from:
|
||||
|
||||
| Environment Variable | Description | Default Value |
|
||||
|----------------------|------------------------------------------------------------------------|---------------------------------|
|
||||
| `LOKI_CONFIG_DIR` | Customize the location of the Loki configuration directory. | `<user-config-dir>/loki` |
|
||||
| `LOKI_ENV_FILE` | Customize the location of the `.env` file to load at startup. | `<loki-config-dir>/.env` |
|
||||
| `LOKI_CONFIG_FILE` | Customize the location of the global `config.yaml` configuration file. | `<loki-config-dir>/config.yaml` |
|
||||
| `LOKI_ROLES_DIR` | Customize the location of the `roles` directory. | `<loki-config-dir>/roles` |
|
||||
| `LOKI_SESSIONS_DIR` | Customize the location of the `sessions` directory. | `<loki-config-dir>/sessions` |
|
||||
| `LOKI_RAGS_DIR` | Customize the location of the `rags` directory. | `<loki-config-dir>/rags` |
|
||||
| `LOKI_FUNCTIONS_DIR` | Customize the location of the `functions` directory. | `<loki-config-dir>/functions` |
|
||||
|
||||
## Agent Related Variables
|
||||
You can also customize the location of full agent configurations using the following environment variables:
|
||||
|
||||
| Environment Variable | Description |
|
||||
|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `<AGENT_NAME>_CONFIG_FILE` | Customize the location of the agent's configuration file; e.g. `SQL_CONFIG_FILE` |
|
||||
| `<AGENT_NAME>_MODEL` | Customize the `model` used for the agent; e.g `SQL_MODEL` |
|
||||
| `<AGENT_NAME>_TEMPERATURE` | Customize the `temperature` used for the agent; e.g. `SQL_TEMPERATURE` |
|
||||
| `<AGENT_NAME>_TOP_P` | Customize the `top_p` used for the agent; e.g. `SQL_TOP_P` |
|
||||
| `<AGENT_NAME>_GLOBAL_TOOLS` | Customize the `global_tools` that are enabled for the agent (a JSON string array); e.g. `SQL_GLOBAL_TOOLS` |
|
||||
| `<AGENT_NAME>_MCP_SERVERS` | Customize the `mcp_servers` that are enabled for the agent (a JSON string array); e.g. `SQL_MCP_SERVERS` |
|
||||
| `<AGENT_NAME>_AGENT_SESSION` | Customize the `agent_session` used with the agent; e.g. `SQL_SESSION` |
|
||||
| `<AGENT_NAME>_INSTRUCTIONS` | Customize the `instructions` for the agent; e.g. `SQL_INSTRUCTIONS` |
|
||||
| `<AGENT_NAME>_VARIABLES` | Customize the `variables` used for the agent (in JSON format of `[{"key1": "value1", "key2": "value2"}]`); <br>e.g. `SQL_VARIABLES` |
|
||||
|
||||
## Logging Related Variables
|
||||
The following variables can be used to change the log level of Loki or the location of the log file:
|
||||
|
||||
| Environment Variable | Description | Default Value |
|
||||
|----------------------|---------------------------------------------|----------------------------------|
|
||||
| `LOKI_LOG_LEVEL` | Customize the log level of Loki | `INFO` |
|
||||
| `LOKI_LOG_FILE` | Customize the location of the Loki log file | `<user-cache-dir>/loki/loki.log` |
|
||||
|
||||
**Pro-Tip:** You can always tail the Loki logs using the `--tail-logs` flag. If you need to disable color output, you
|
||||
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 | |
|
||||
| `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.** | |
|
||||
@@ -0,0 +1,103 @@
|
||||
# Macros
|
||||
Macros are essentially Loki "scripts"; that is, a predefined sequence of REPL commands that automate repetitive tasks or
|
||||
workflows. Macros run in isolated environments, ensuring that the macros don't inherit any pre-existing role, session,
|
||||
RAG, or agent state, and they will not affect your current context.
|
||||
|
||||
This isolation ensures that your workspace remains clean and unaffected by macro operations.
|
||||
|
||||

|
||||
|
||||
For more information on Loki's REPL, refer to the [REPL](./REPL.md) documentation.
|
||||
|
||||
## Quick Links
|
||||
<!--toc:start-->
|
||||
- [Macro Definition](#macro-definition)
|
||||
- [Step Definitions](#step-definitions)
|
||||
- [Macro Variables](#macro-variables)
|
||||
- [Built-In Macros](#built-in-macros)
|
||||
<!--toc:end-->
|
||||
|
||||
---
|
||||
|
||||
## Macro Definition
|
||||
Macros are defined as YAML files in the `macros` subdirectory of your Loki configuration directory. The Loki configuration
|
||||
directory can vary between systems, so to find the location of your macros config directory, you can use the following
|
||||
command:
|
||||
|
||||
```shell
|
||||
loki --info | grep 'macros_dir' | awk '{print $2}'
|
||||
```
|
||||
|
||||
Macro definitions are broken into two parts: the `steps` of the macro, and an optional `variables` section that lets
|
||||
users pass in variables to alter the behavior of the macro at runtime.
|
||||
|
||||
### Step Definitions
|
||||
The step definitions for a macro are straightforward: They are simply the exact commands you would otherwise type in the
|
||||
REPL.
|
||||
|
||||
**Example: Macro to generate a git commit message**
|
||||
`macros/generate-commit-message.yaml`
|
||||
```yaml
|
||||
steps:
|
||||
- .file `git diff` -- generate git commit message
|
||||
```
|
||||
Usage:
|
||||
```shell
|
||||
$ loki --macro generate-commit-message
|
||||
>> .file `git diff` -- generate a git commit message
|
||||
Add documentation on macros
|
||||
```
|
||||
|
||||
For a full example configuration, refer to the [example macro configuration file](../config.macro.example.yaml) in the root of this project.
|
||||
|
||||
### Macro Variables
|
||||
Sometimes it's useful to be able to modify the behavior of a macro at runtime. This is achieved with the `variables`
|
||||
array of the macro definition.
|
||||
|
||||
To pass variables to a macro, since they are just Loki scripts, the syntax is the same as it is for any other scripting
|
||||
language: You just pass them alongside your invocation.
|
||||
|
||||
**Example:**
|
||||
```shell
|
||||
$ loki --macro example-variable-macro first_argument second_argument
|
||||
```
|
||||
|
||||
Each variable in the `variables` array has the following properties:
|
||||
* `name` (Required): the name of the variable, which can be referenced in the actual steps of the macro using the
|
||||
`{{name}}` syntax.
|
||||
* `default` (Optional): A default value for the variable if no value is specified. If no default value is defined, and
|
||||
no value is provided for the variable at runtime, Loki will error out.
|
||||
* `rest` (Optional, Boolean): When set to `true`, this variable will collect all remaining arguments passed to the
|
||||
macro. This behavior is only applicable when the variable is the last variable in the list. By default, this is
|
||||
`false`.
|
||||
|
||||
The `variables` array is order-dependent; that is to say that all arguments passed to the macro are positional. So be
|
||||
careful about the ordering if that is important to your macro's invocation.
|
||||
|
||||
**Example: Simple variable example to invoke an agent**
|
||||
`macros/invoke-agent.yaml`
|
||||
```yaml
|
||||
variables:
|
||||
- name: agent # No default value means this must be defined at runtime
|
||||
- name: args
|
||||
rest: true # All remaining arguments to the macro are collected into this variable
|
||||
default: What can you do? # This is used if no value is passed at runtime
|
||||
steps:
|
||||
- .agent {{agent}}
|
||||
- '{{args}}'
|
||||
```
|
||||
Usage:
|
||||
```shell
|
||||
$ loki --macro invoke-agent sql
|
||||
# or
|
||||
$ loki --macro invoke-agent sql What tables are available?
|
||||
```
|
||||
|
||||
For a full example configuration, refer to the [example macro configuration file](../config.macro.example.yaml) in the root of this project.
|
||||
|
||||
## Built-In Macros
|
||||
Loki comes packaged with some useful built-in macros. These are also good examples if you're looking for more examples
|
||||
on how to make your own macros, so be sure to check out the [built-in macro definitions](../assets/macros) if you're
|
||||
looking for more examples.
|
||||
|
||||
* `generate-commit-message` - Generate a Git commit message based on the staged changes in the current directory
|
||||
@@ -0,0 +1,307 @@
|
||||
# RAG
|
||||
Retrieval Augmented Generation (RAG) is a method of minimizing LLM hallucinations and extending the model's context
|
||||
without consuming a significant portion of the context length. It uses documents and other additional resources that you
|
||||
provide to give the model more context for all of your prompts.
|
||||
|
||||
Loki has a built-in vector database and full-text search engine to support RAG knowledge bases for your queries.
|
||||
|
||||
The generated knowledge bases are stored in the `rag` subdirectory of your Loki configuration directory. The location of
|
||||
this directory varies by system, so you can use the following command to find your RAG directory:
|
||||
|
||||
```shell
|
||||
loki --info | grep 'rags_dir' | awk '{print $2}'
|
||||
```
|
||||
|
||||
## Quick Links
|
||||
<!--toc:start-->
|
||||
- [Usage](#usage)
|
||||
- [Persistent RAG](#persistent-rag)
|
||||
- [Ephemeral RAG](#ephemeral-rag)
|
||||
- [How It Works](#how-it-works)
|
||||
- [1. Build](#1-build)
|
||||
- [2. Lookup](#2-lookup)
|
||||
- [2a. Reranking (Optional)](#2a-reranking-optional)
|
||||
- [3. Prompt](#3-prompt)
|
||||
- [Supported Document Sources](#supported-document-sources)
|
||||
- [Document Loaders](#document-loaders)
|
||||
- [Document Loader Usage](#document-loader-usage)
|
||||
- [Advanced Customizations](#advanced-customizations)
|
||||
- [Embedding Model](#embedding-model)
|
||||
- [Reranker](#reranker)
|
||||
- [Chunk Size](#chunk-size)
|
||||
- [Trade-Offs](#chunk-size-trade-offs)
|
||||
- [Chunk Overlap](#chunk-overlap)
|
||||
- [Top K](#top-k)
|
||||
- [Trade-Offs](#top-k-trade-offs)
|
||||
- [RAG Template](#rag-template)
|
||||
<!--toc:end-->
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
There's two ways to use RAG in Loki: A persistent RAG that can be loaded on-demand for queries, and an ephemeral one for
|
||||
adding RAG to a single specific query.
|
||||
|
||||
### Persistent RAG
|
||||
In the REPL, persistent RAG is initialized via the `.rag` command:
|
||||
|
||||

|
||||
|
||||
The generated RAG is then saved to the `rag` subdirectory of the Loki configuration, and can then be loaded whenever you
|
||||
want that knowledge base via either `.rag <name>` or `loki --rag <RAG>`.
|
||||
|
||||
### Ephemeral RAG
|
||||
Short-lived RAG that is only used for a single session or query is loaded using `.file`/`--file`.
|
||||
|
||||
You can use it to either execute a prompt from a file, or for temporary RAG. The difference is the usage of the `--`
|
||||
separator. If you only specify a filename and no `--` separator, Loki will know to read the file contents and pass them
|
||||
as a query to the model. Otherwise, the `--` separator is read to indicate that this is the end of the list of documents
|
||||
to load into the ephemeral RAG, and what follows is the query to pass to the model.
|
||||
|
||||
```shell
|
||||
.file prompt.md # Read the file as a prompt
|
||||
.file %% -- translate the last reply to italian
|
||||
.file `git diff` -- generate a commit message
|
||||
```
|
||||
|
||||

|
||||
|
||||
Once the session ends, this RAG will no longer be accessible and is only visible to the current session.
|
||||
|
||||
#### The `%%` Document Type
|
||||
In addition to the usual documents that can be specified for persistent RAG, ephemeral RAG has a special `%%` value.
|
||||
This value references the content of the last reply. So you can use it like this:
|
||||
|
||||
```shell
|
||||
.file %% -- translate the last reply to italian
|
||||
```
|
||||
|
||||
The `--` indicates that this is the end of your documents and the beginning of your prompt.
|
||||
|
||||
#### The `cmd` Document Type
|
||||
Loki also lets you use command outputs for ephemeral RAG input. Simply enclose the command in backticks:
|
||||
|
||||
```shell
|
||||
.file `git diff` -- generate a commit message
|
||||
```
|
||||
|
||||
The `--` indicates that this is the end of your documents and the beginning of your prompt.
|
||||
|
||||
## How It Works
|
||||
#### 1. Build
|
||||
When you define RAG, Loki will first "build" the RAG. This means that Loki will consume the documents you specified and
|
||||
generate [embeddings](https://huggingface.co/spaces/hesamation/primer-llm-embedding) for that text. This essentially just means that Loki translates the document into a language
|
||||
the LLM can understand.
|
||||
|
||||
These embeddings are then stored in an in-memory vector database.
|
||||
|
||||
#### 2. Lookup
|
||||
Loki sits between you and the model. So when you submit a prompt to the model, before Loki ever sends it, it will first
|
||||
convert your prompt into embeddings (LLM language), and look for relevant snippets of text in the vector database.
|
||||
|
||||
Loki then passes the top `n`-snippets of text that it finds in the vector database as additional context to the model
|
||||
before your prompt.
|
||||
|
||||
#### 2a. Reranking (Optional)
|
||||
The lookup for relevant snippets of texts uses embeddings to find text that is semantically similar to your prompt, and
|
||||
returns the top `n`-results. This often works fairly well, however these top results aren't always the most relevant for
|
||||
answering the specific query.
|
||||
|
||||
Reranking improves these initial results (say, the top 20-100 text snippets) and re-scores them using a more
|
||||
sophisticated model. The reranker model will rank documents by their actual usefulness for answering the query to ensure
|
||||
the most relevant context is passed to the model alongside your query.
|
||||
|
||||
This reranking model can be customized for each RAG you build in Loki. See the [Custom Reranker](#reranker) section
|
||||
below for more details on how to customize this.
|
||||
|
||||
#### 3. Prompt
|
||||
Finally, the text snippets that were looked up in RAG are passed to the model as additional context to your prompt,
|
||||
giving the model query-specific context to answer your question.
|
||||
|
||||
## Supported Document Sources
|
||||
Loki supports a number of document sources that can be used for RAG:
|
||||
|
||||
| Source | Example | Comments |
|
||||
|--------------------------|-----------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Files | `/tmp/dir1/file1;/tmp/dir1/file2` | |
|
||||
| Directory | `/tmp/dir` | Picks up all files in a directory and all its subdirectories |
|
||||
| Directory (extensions} | `/tmp/dir2/**/*.{md,txt}` | Finds all files in all subdirectories with the specified extensions |
|
||||
| Recursive Filename | `/tmp/*/LOKI.md` | The following files will be picked up: <br><ul><li>`/tmp/dir1/LOKI.md`</li><li>`/tmp/dir2/subdir1/LOKI.md`</li><li>`/tmp/dir2/subdir2/LOKI.md`</li></ul> |
|
||||
| URL | `https://www.ohdsi.org/data-standardization/` | Downloads and loads the specified webpage into the <br>knowledge base |
|
||||
| Recursive URL (Websites) | `https://github.com/OHDSI/Vocabulary-v5.0/wiki/**` | Crawls all pages under the given URL and loads them <br>into the knowledge base |
|
||||
| Document Loader (custom) | `jina:https://cloud.google.com/bigquery/docs/reference/standard-sql/` | Use a custom document loader to parse the given document |
|
||||
|
||||
## Document Loaders
|
||||
Loki only has built-in support for loading text files. But that functionality can be extended to read all kinds of files
|
||||
into your knowledge bases. These custom loaders are used by both RAG and for documents specified using the
|
||||
`.file`/`--file` flags.
|
||||
|
||||
In the global configuration file, you can specify loaders for specific document types using the `document_loaders`
|
||||
setting. Each loader is defined by specifying a name and then a command that Loki will execute to load the document.
|
||||
|
||||
The following variables are interpolated at runtime by Loki and can be used as placeholders in your command definitions:
|
||||
* `$1` (Required) - The input file
|
||||
* `$2` (Optional) - The output file. If omitted, `stdout` is used as the output destination
|
||||
|
||||
**Note:** It is your responsibility to ensure that any tools used to parse documents into text that Loki can read are
|
||||
installed on your system and are available on your `$PATH`. Loki does not have any built-in way of installing
|
||||
dependencies for document loaders for you.
|
||||
|
||||
The following are some example loaders:
|
||||
```yaml
|
||||
document_loaders:
|
||||
pdf: 'pdftotext $1 -' # Use pdftotext to convert a PDF file to text
|
||||
# (see https://poppler.freedesktop.org for details on how to install pdftotext)
|
||||
docx: 'pandoc --to plain $1' # Use pandoc to convert a .docx file to text
|
||||
# (see https://pandoc.org for details on how to install pandoc)
|
||||
jina: 'curl -fsSL https://r.jina.ai/$1 -H "Authorization: Bearer {{JINA_API_KEY}}' # Use Jina to translate a website into text;
|
||||
# Requires a Jina API key to be added to the Loki vault
|
||||
git: > # Use yek to load a git repository into the knowledgebase (https://github.com/bodo-run/yek)
|
||||
sh -c "yek $1 --json | jq 'map({ path: .filename, contents: .content })'"
|
||||
```
|
||||
|
||||
### Document Loader Usage
|
||||
Once you have your loaders defined, you can specify when Loki should use them by prefixing any RAG file/directory/URI
|
||||
with the name of the loader.
|
||||
|
||||
**Example: Load a git repo into RAG**
|
||||

|
||||
|
||||
**Example: Use pdf loader for ephemeral RAG**
|
||||
```shell
|
||||
$ loki --file pdf:some-file.pdf
|
||||
```
|
||||
|
||||
## Advanced Customizations
|
||||
For those familiar with RAG, Loki exposes a handful of advanced global settings that can be used to tweak your default
|
||||
RAG configurations.
|
||||
|
||||
### Embedding Model
|
||||
When Loki queries your RAG knowledge bases, it needs to first convert your query into embeddings. By default, Loki uses
|
||||
the same embedding model that was used to create the knowledge base in the first place.
|
||||
|
||||
This can be customized to any other embedding model available in your configured clients by setting the
|
||||
`rag_embedding_model` setting in your global Loki configuration file:
|
||||
|
||||
```yaml
|
||||
rag_embedding_model: null # Specifies the embedding model used for context retrieval
|
||||
```
|
||||
|
||||
### Reranker
|
||||
By default, Loki uses [Reciprocal Rank Fusion (RRF)](https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion) to merge vector and keyword search results.
|
||||
|
||||
You can change the default reranker model to any other reranking model in your configured clients. To change the default
|
||||
reranker model, simply change the value of the `rag_reranker_model` setting in your global configuration file:
|
||||
|
||||
```yaml
|
||||
rag_reranker_model: null # By default,
|
||||
```
|
||||
|
||||
### Chunk Size
|
||||
In the context of RAG, the chunk size is the maximum length of each text chunk (measured in characters) that is created
|
||||
when splitting documents. In Loki, this defaults to `2000` characters.
|
||||
|
||||
You can specify a different global default by setting the `rag_chunk_size` property in your global configuration file:
|
||||
|
||||
```yaml
|
||||
rag_chunk_size: null # Defines the size of chunks for document processing in characters
|
||||
```
|
||||
|
||||
#### Chunk Size Trade-Offs
|
||||
Keep in mind the following trade-offs when changing the chunk size:
|
||||
|
||||
* **Smaller chunks (e.g. 256 characters):** More precise retrieval, better semantic focus, but may lack context or split
|
||||
important information
|
||||
* **Larger chunks (e.g. 1024 characters):** More context preserved, fewer chunks to manage, but less precise matching
|
||||
and more noise in retrieved document
|
||||
|
||||
### Chunk Overlap
|
||||
Chunk overlap in RAG is the number of characters that overlap between consecutive chunks to maintain continuity.
|
||||
|
||||
---
|
||||
|
||||
**Example:** If the following sentence is cut off at the end of one chunk
|
||||
|
||||
`I was doing fine until someone brought up`
|
||||
|
||||
You'll ideally want that full sentence to be picked up at the beginning of the next chunk to make sure the full meaning
|
||||
is captured. So in this example, if your chunk overlap is 42 characters, then the start of the next chunk would look
|
||||
like this:
|
||||
|
||||
`I was doing fine until someone brought up the game. <next sentence>`
|
||||
|
||||
---
|
||||
|
||||
Often, this value is 10%-20% of the chunk size.
|
||||
|
||||
By default, in Loki, this value is 5% the chunk size. You can override this and specify the default chunk overlap (in
|
||||
characters) that Loki should use as a global default by setting the `rag_chunk_overlap` property in the global Loki
|
||||
configuration file:
|
||||
|
||||
```yaml
|
||||
rag_chunk_overlap: null # Defines the overlap between chunks
|
||||
```
|
||||
|
||||
### Top K
|
||||
In RAG, `top_k` represents the top `k`-chunks to return from the vector database query. Think of it like if you search
|
||||
something on Google and only care about the top 10 results, that's what you'll use for your context.
|
||||
|
||||
In Loki, the default value for this is `5`. You can customize this global default by setting the `rag_top_k` property in
|
||||
your global configuration file:
|
||||
|
||||
```yaml
|
||||
rag_top_k: 5 # Specifies the number of documents to retrieve for answering queries
|
||||
```
|
||||
|
||||
#### Top K Trade-Offs
|
||||
When customizing this value, keep in mind the following trade-offs so you get the best performance:
|
||||
|
||||
* **Lower top_k (e.g. 3):** Faster, more focused context, lower cost, but risks missing relevant information
|
||||
* **Higher top_k (e.g. 10):** More comprehensive coverage, but more noise, higher latency, increased token costs, and
|
||||
potential context window constraints
|
||||
|
||||
### RAG Template
|
||||
When you use RAG in Loki, after Loki performs the lookup for relevant chunks of text to add as context to your query, it
|
||||
will add the retrieved text chunks as context to your query before sending it to the model. The format of this context
|
||||
is determined by the `rag_template` setting in your global Loki configuration file.
|
||||
|
||||
This template utilizes three placeholders:
|
||||
* `__INPUT__`: The user's actual query
|
||||
* `__CONTEXT__`: The context retrieved from RAG
|
||||
* `__SOURCES__`: A numbered list of the source file paths or URLs that the retrieved context came from
|
||||
|
||||
These placeholders are replaced with the corresponding values into the template and make up what's actually passed to
|
||||
the model at query-time. The `__SOURCES__` placeholder enables the model to cite which documents its answer is based on,
|
||||
which is especially useful when building knowledge-base assistants that need to provide verifiable references.
|
||||
|
||||
The default template that Loki uses is the following:
|
||||
|
||||
```text
|
||||
Answer the query based on the context while respecting the rules. (user query, some textual context and rules, all inside xml tags)
|
||||
|
||||
<context>
|
||||
__CONTEXT__
|
||||
</context>
|
||||
|
||||
<sources>
|
||||
__SOURCES__
|
||||
</sources>
|
||||
|
||||
<rules>
|
||||
- If you don't know, just say so.
|
||||
- If you are not sure, ask for clarification.
|
||||
- Answer in the same language as the user query.
|
||||
- If the context appears unreadable or of poor quality, tell the user then answer as best as you can.
|
||||
- If the answer is not in the context but you think you know the answer, explain that to the user then answer with your own knowledge.
|
||||
- Answer directly and without using xml tags.
|
||||
- When using information from the context, cite the relevant source from the <sources> section.
|
||||
</rules>
|
||||
|
||||
<user_query>
|
||||
__INPUT__
|
||||
</user_query>
|
||||
```
|
||||
|
||||
You can customize this template by specifying the `rag_template` setting in your global Loki configuration file. Your
|
||||
template *must* include both the `__INPUT__` and `__CONTEXT__` placeholders in order for it to be valid. The
|
||||
`__SOURCES__` placeholder is optional. If it is omitted, source references will not be included in the prompt.
|
||||
@@ -0,0 +1,117 @@
|
||||
# Customize REPL Prompt
|
||||
|
||||
The prompt you see when you start the Loki REPL can be customized to your liking. This is achieved via the `left_prompt`
|
||||
and `right_prompt` settings in the global Loki configuration file:
|
||||
|
||||
```yaml
|
||||
left_prompt: '{color.red}{model}){color.green}{?session {?agent {agent}>}{session}{?role /}}{!session {?agent {agent}>}}{role}{?rag @{rag}}{color.cyan}{?session )}{!session >}{color.reset} '
|
||||
right_prompt: '{color.purple}{?session {?consume_tokens {consume_tokens}({consume_percent}%)}{!consume_tokens {consume_tokens}}}{color.reset}'
|
||||
```
|
||||
|
||||
The location of the global configuration file differs between systems, so you can use the following command to find your
|
||||
global configuration file's location:
|
||||
|
||||
```shell
|
||||
loki --info | grep 'config_file' | awk '{print $2}'
|
||||
```
|
||||
|
||||
## Quick Links
|
||||
<!--toc:start-->
|
||||
- [Syntax](#syntax)
|
||||
- [Variables](#variables)
|
||||
<!--toc:end-->
|
||||
|
||||
## Syntax
|
||||
The syntax for the prompts consists of plain text and templates contained in `{...}`. The plain text is
|
||||
printed exactly as given.
|
||||
|
||||
The syntax for the templates `{...}` is as follows:
|
||||
|
||||
* `{variable}` - Replaced with the value of `variable`
|
||||
* `{?variable <template>}` - Evaluate the `<template>` when `variable` is evaluated to `true`
|
||||
* `{!variable <template>}` - Evaluate the `<template>` when `variable` is evaluated to `false`
|
||||
|
||||
Where a `<template>` is another expression consisting of plain text and/or more special computations inside `{...}`.
|
||||
|
||||
Variables are evaluated to also be "truthy"; that is, if a variable is undefined, it is considered to be the exact same
|
||||
as if that variable's value was `false`.
|
||||
|
||||
**Example 1: Simple Boolean Usage**
|
||||
For the prompt `{?variable yay}{!variable boo}`, if `variable=true`, then the output will be
|
||||
```
|
||||
yay
|
||||
```
|
||||
|
||||
And if `variable=false`:
|
||||
```
|
||||
boo
|
||||
```
|
||||
|
||||
**Example 2: Nested Expressions**
|
||||
For the prompt `{?variable {!variable2 yay}>}`, and assuming
|
||||
* `variable=true`
|
||||
* `variable2=false`
|
||||
the output will be
|
||||
```
|
||||
yay>
|
||||
```
|
||||
|
||||
If `variable2=true`, the output will be empty.
|
||||
|
||||
If `variable=false`, the output will be empty.
|
||||
|
||||
## Variables
|
||||
The following variables and output modifiers are available to you when you're creating your prompts:
|
||||
|
||||
```yaml
|
||||
# Model Variables
|
||||
model: openai:gpt-4 # The active model's full name
|
||||
client_name: openai # The name of the client serving the active model
|
||||
model_name: gpt-4 # The aliased name of the active model
|
||||
max_input_tokens: 4096 # The maximum number of input tokens for the active model
|
||||
|
||||
# Configuration Variables
|
||||
temperature: 1.0 # The temperature for the active model
|
||||
top_p: 0.9 # The top_p for the active model
|
||||
dry_run: true # Whether the given command is flagged to be a dry run
|
||||
stream: false # Whether streaming responses are enabled
|
||||
save: true # Whether shell history is saved
|
||||
wrap: 120 # The number of characters to allow before wrapping around output to the next line
|
||||
|
||||
# Role Variables
|
||||
role: code # The active role
|
||||
|
||||
# Session Variables
|
||||
session: temp # The name of the active session
|
||||
dirty: false # Whether the session settings have been updated but not persisted
|
||||
consume_tokens: 200 # The number of tokens consumed
|
||||
consume_percent: 1% # The percentage of tokens consumed to the maximum input tokens
|
||||
user_messages_len: 0 # The total number of sent user messages
|
||||
|
||||
# RAG Variables
|
||||
rag: temp # The name of the active RAG
|
||||
|
||||
# Agent Variables
|
||||
agent: todo-sh # The name of the active agent
|
||||
|
||||
# ANSI COLORS
|
||||
color.reset:
|
||||
color.black:
|
||||
color.dark_gray:
|
||||
color.red:
|
||||
color.light_red:
|
||||
color.green:
|
||||
color.light_green:
|
||||
color.yellow:
|
||||
color.light_yellow:
|
||||
color.blue:
|
||||
color.light_blue:
|
||||
color.purple:
|
||||
color.light_purple:
|
||||
color.magenta:
|
||||
color.light_magenta:
|
||||
color.cyan:
|
||||
color.light_cyan:
|
||||
color.white:
|
||||
color.light_gray:
|
||||
```
|
||||
@@ -0,0 +1,260 @@
|
||||
# Loki REPL Guide
|
||||
In addition to being a CLI, Loki also has a built-in REPL (Read-Execute-Print-Loop). This enables users to quickly try
|
||||
out prompts, commands, configurations, and everything in between without having to modify the same command every time.
|
||||
|
||||
You can enter the REPL by simply typing `loki` without any follow-up flags or arguments.
|
||||
## Quick Links
|
||||
<!--toc:start-->
|
||||
- [Features](#features)
|
||||
- [REPL Commands](#repl-commands)
|
||||
- [`.model` - Change the current LLM](#model---change-the-current-llm)
|
||||
- [`.role` - Role management](#role---role-management)
|
||||
- [`.prompt` - Set a temporary role using a prompt](#prompt---set-a-temporary-role-using-a-prompt)
|
||||
- [`.session` - Session management](#session---session-management)
|
||||
- [`.agent` - Chat with an AI agent](#agent---chat-with-an-ai-agent)
|
||||
- [`.rag` - Chat with documents](#rag---chat-with-documents)
|
||||
- [`.macro` - Execute a macro](#macro---execute-a-macro)
|
||||
- [`.file` - Read files and use them as input](#file---read-files-and-use-them-as-input)
|
||||
- [`.vault` - Manage the Loki vault](#vault---manage-the-loki-vault)
|
||||
- [`.continue` - Continue the previous response](#continue---continue-the-previous-response)
|
||||
- [`.regenerate` - Regenerate the last response](#regenerate---regenerate-the-last-response)
|
||||
- [`.copy` - Copy the last response to your clipboard](#copy---copy-the-last-response-to-your-clipboard)
|
||||
- [`.set` - Adjust runtime settings](#set---adjust-runtime-settings)
|
||||
- [`.edit` - Modify configuration files](#edit---modify-configuration-files)
|
||||
- [`.delete` - Delete configurations from Loki](#delete---delete-configurations-from-loki)
|
||||
- [`.info` - Display information about the current mode](#info---display-information-about-the-current-mode)
|
||||
- [`.authenticate` - Authenticate the current model client via OAuth](#authenticate---authenticate-the-current-model-client-via-oauth)
|
||||
- [`.exit` - Exit an agent/role/session/rag or the Loki REPL itself](#exit---exit-an-agentrolesessionrag-or-the-loki-repl-itself)
|
||||
- [`.help` - Show the help guide](#help---show-the-help-guide)
|
||||
<!--toc:end-->
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
The REPL has features that are intended to make your Loki experience as easy and as enjoyable as possible! This includes
|
||||
things like
|
||||
|
||||
* **Tab Autocompletion:** Every command in the REPL (i.e. everything that starts with a `.`) has fuzzy search auto
|
||||
completions.
|
||||
* `.<tab>` to complete REPL commands
|
||||
* `.model <tab>` to complete chat models
|
||||
* `.set <tab>` to complete configuration keys
|
||||
* `.set key <tab>` to complete configuration values
|
||||
* **Multi-Line Prompts:** You can also type prompts that span more than one line to help organize your thoughts. This
|
||||
can be done in the following ways:
|
||||
* `Ctrl-o` to open the current input buffer in your preferred editor (either the value of `editor` or `$EDITOR`)
|
||||
* You can paste multi-line text
|
||||
* You can type `:::` to start multi-line editing, and use `:::` to finish it.
|
||||
* And finally, you can use hotkeys like `{ctrl/shift/alt}+enter` or `ctrl-j` to insert a new line directly in the
|
||||
REPL.
|
||||
* **History Search** Press `ctrl+r` to search the REPL history, and navigate it with `↑↓`
|
||||
* **Configurable Keybindings:** You can switch between `emacs` style keybindings or `vi` style keybindings
|
||||
* [**Custom REPL Prompt:**](./REPL-PROMPT.md) You can even customize the REPL prompt to display information about the
|
||||
current context in the prompt
|
||||
* **Built-in user interaction tools:** When function calling is enabled in the REPL, the `user__ask`, `user__confirm`,
|
||||
`user__input`, and `user__checkbox` tools are always available for interactive prompts. These are not injected in the
|
||||
one-shot CLI mode.
|
||||
|
||||
---
|
||||
|
||||
## REPL Commands
|
||||
All REPL commands begin with a `.` to indicate that they're not part of a prompt. The following list details the
|
||||
commands available in Loki:
|
||||
|
||||
### `.model` - Change the current LLM
|
||||
When browsing models in the REPL, use the following legend to understand the purpose of each column in the model table:
|
||||
```
|
||||
openai:gpt-4o 128000 / 4096 | 5 / 15 👁 ⚒
|
||||
| | | | | | └─ supports function calling
|
||||
| | | | | └─ support vision (multi-modal)
|
||||
| | | | └─ output price ($/1M)
|
||||
| | | └─ input price ($/1M)
|
||||
| | |
|
||||
| | └─ max output tokens
|
||||
| └─ max input tokens
|
||||
└─ model id
|
||||
```
|
||||

|
||||
|
||||
For more information about how to add models to Loki, refer to the [clients documentation](./clients/CLIENTS.md).
|
||||
|
||||
### `.role` - Role management
|
||||
Loki offers the following commands to manage your roles:
|
||||
|
||||
| Command | Description |
|
||||
|--------------|-------------------------------------------------------------------------|
|
||||
| `.role` | Create or switch to a role |
|
||||
| `.info role` | Show information about the active role |
|
||||
| `.edit role` | Open the active role's configuration file in your preferred text editor |
|
||||
| `.save role` | Save the active role and its configurations to a configuration file |
|
||||
| `.exit role` | Exit the active role |
|
||||
|
||||

|
||||
|
||||
For more information about roles in Loki and how to build them, refer to the [roles documentation](./ROLES.md).
|
||||
|
||||
### `.prompt` - Set a temporary role using a prompt
|
||||
If you need to create a temporary role that you want to discard after use, you use `.prompt`. `.prompt`-based roles
|
||||
cannot be persisted to a file and saved.
|
||||
|
||||

|
||||
|
||||
### `.session` - Session management
|
||||
Use the following commands to manage sessions in Loki:
|
||||
|
||||
| Command | Description |
|
||||
|---------------------|---------------------------------------------------------------------------------------------|
|
||||
| `.session` | Start or switch to a session |
|
||||
| `.empty session` | Clear all messages for the active session |
|
||||
| `.compress session` | Compress the session messages using the `summarization_prompt` setting in the global config |
|
||||
| `.info session` | Display information about the active session |
|
||||
| `.edit session` | Open the active session's configuration in your preferred text editor |
|
||||
| `.save session` | Save the active session to a `session` configuration file |
|
||||
| `.exit session` | Exit the active session |
|
||||
|
||||

|
||||
|
||||
For more information on sessions and how to use them in Loki, refer to the [sessions documentation](./SESSIONS.md).
|
||||
|
||||
### `.agent` - Chat with an AI agent
|
||||
Loki lets you build OpenAI GPT-style agents. The following commands let you interact with and manage your agents in
|
||||
Loki:
|
||||
|
||||
| Command | Description |
|
||||
|----------------------|-----------------------------------------------------------------------------------------------|
|
||||
| `.agent` | Use an agent |
|
||||
| `.starter` | Display and use conversation starters for the active agent |
|
||||
| `.clear todo` | Clear the todo list and stop auto-continuation (requires `auto_continue: true` on the agent) |
|
||||
| `.edit agent-config` | Open the agent configuration in your preferred text editor |
|
||||
| `.info agent` | Display information about the active agent |
|
||||
| `.exit agent` | Leave the active agent |
|
||||
|
||||

|
||||
|
||||
For more information on agents in Loki and how to create them, refer to the [agents documentation](./AGENTS.md).
|
||||
|
||||
### `.rag` - Chat with documents
|
||||
RAG (Retrieval Augmented Generation) enables you to load documents into the LLM so you can ask questions about it or
|
||||
complete tasks using the documents as additional context.
|
||||
|
||||
| Command | Description |
|
||||
|------------------|------------------------------------------------------------------------------|
|
||||
| `.rag` | Initialize or access a RAG |
|
||||
| `.edit rag-docs` | Add or remove documents from the active RAG using your preferred text editor |
|
||||
| `.rebuild rag` | Rebuild the active RAG to accommodate document changes |
|
||||
| `.sources rag` | Show a works-cited of the sources used in the last query |
|
||||
| `.info rag` | Display information about the active RAG |
|
||||
| `.exit rag` | Exit the active RAG |
|
||||
|
||||

|
||||
|
||||
For more information about RAG in Loki and how to utilize it, refer to the [rag documentation](./RAG.md).
|
||||
|
||||
### `.macro` - Execute a macro
|
||||
Macros in Loki are like "scripts" of commands that can be run in isolated environments; that means they do not use any
|
||||
active settings and use the same settings they had when written. They are created/executed using the `.macro <name>`
|
||||
command.
|
||||
|
||||

|
||||
|
||||
For more information on macros in Loki and how to create them, refer to the [macros documentation](./MACROS.md).
|
||||
|
||||
### `.file` - Read files and use them as input
|
||||
Loki lets you specify any number of documents that you can load and use as ephemeral RAG to chat with the LLM. To see
|
||||
what files or values you can pass to it, simply run the command `.file` with no arguments:
|
||||
|
||||
```shell
|
||||
openai:gpt-4o)> .file
|
||||
Usage: .file <file|dir|url|%%|cmd>... [-- <text>...]
|
||||
```
|
||||
|
||||

|
||||
|
||||
For more information about ephemeral RAG, refer to the [ephemeral RAG documentation](./RAG.md#ephemeral-rag).
|
||||
|
||||
### `.vault` - Manage the Loki vault
|
||||
The Loki vault lets users store sensitive secrets and credentials securely so that there's no plaintext secrets
|
||||
anywhere in your configurations.
|
||||
|
||||

|
||||
|
||||
For more information about the Loki vault, refer to the [vault documentation](./VAULT.md).
|
||||
|
||||
### `.continue` - Continue the previous response
|
||||
When you have a response that exceeds the context length, you can use the `.continue` command to continue the generation
|
||||
of the last response.
|
||||
|
||||

|
||||
|
||||
### `.regenerate` - Regenerate the last response
|
||||
If ever your response is interrupted, or you want to try generating it again, you can use the `.regenerate` command to do
|
||||
this without having to retype your query:
|
||||
|
||||

|
||||
|
||||
### `.copy` - Copy the last response to your clipboard
|
||||
If you're trying to copy the last response (like copying some code), you can use the `.copy` command to copy the entire
|
||||
last response to your system clipboard:
|
||||
|
||||

|
||||
|
||||
### `.set` - Adjust runtime settings
|
||||
You can use `.set` to adjust select settings at runtime. This is useful when you're experimenting with settings and want
|
||||
to know how they'll affect Loki. To persist the changes you make, be sure to update them in the global configuration
|
||||
file.
|
||||
|
||||

|
||||
|
||||
### `.edit` - Modify configuration files
|
||||
The `.edit` command lets you modify configuration files for the current mode of the REPL. It will open the selected
|
||||
configuration in your preferred text editor. It lets you modify the following configurations:
|
||||
|
||||
* `.edit config` - Modify the global configuration
|
||||
* `.edit role` - Modify the active role's configuration
|
||||
* `.edit session` - Modify the active session's configuration
|
||||
* `.edit agent-config` - Modify the active agent's configuration
|
||||
* `.edit rag-docs` - Add or remove documents from the active RAG
|
||||
|
||||
### `.delete` - Delete configurations from Loki
|
||||
The `.delete` command allows you to delete entities in Loki without having to directly run `rm -rf` on the configuration
|
||||
directory or file corresponding to the target entity. You can use it to delete the following entities:
|
||||
|
||||
* `.delete role` - Delete select roles
|
||||
* `.delete session` - Delete select sessions
|
||||
* `.delete macro` - Delete select macros
|
||||
* `.delete rag` - Delete select RAGs
|
||||
* `.delete agent-data` - Delete select agent's configurations and all tools
|
||||
|
||||
### `.info` - Display information about the current mode
|
||||
The `.info` command provides useful information about different modes that Loki may be operating in. It's helpful if you
|
||||
want a quick understanding of the system info, a role's configuration, an agent's configuration, etc.
|
||||
|
||||
The following entities are supported:
|
||||
|
||||
| Command | Description |
|
||||
|-----------------|-------------------------------------------------------------|
|
||||
| `.info` | Display system information (identical to the `--info` flag) |
|
||||
| `.info role` | Display information about the active role |
|
||||
| `.info session` | Display information about the active session |
|
||||
| `.info agent` | Display information about the active agent |
|
||||
| `.info rag` | Display information about the active RAG |
|
||||
|
||||
### `.authenticate` - Authenticate the current model client via OAuth
|
||||
The `.authenticate` command will start the OAuth flow for the current model client if
|
||||
* The client supports OAuth (See the [clients documentation](./clients/CLIENTS.md#providers-that-support-oauth) for supported clients)
|
||||
* The client is configured in your Loki configuration to use OAuth via the `auth: oauth` property
|
||||
|
||||
### `.exit` - Exit an agent/role/session/rag or the Loki REPL itself
|
||||
The `.exit` command is used to move between modes in the Loki REPL.
|
||||
|
||||
| Command | Description |
|
||||
|-----------------|-------------------------|
|
||||
| `.exit role` | Exit the active role |
|
||||
| `.exit session` | Exit the active session |
|
||||
| `.exit agent` | Exit the active agent |
|
||||
| `.exit rag` | Exit the active RAG |
|
||||
| `.exit` | Exit the Loki REPL |
|
||||
|
||||
### `.help` - Show the help guide
|
||||
Just like with any shell or REPL, you sometimes need a little help and want to know what commands are available to you.
|
||||
That's when you use the `.help` command.
|
||||
@@ -0,0 +1,266 @@
|
||||
# Roles
|
||||
When customizing the behavior or LLMs, we use roles to "constrain" the responses or behavior of the LLM to whatever
|
||||
purpose we desire.
|
||||
|
||||
Think of them kind of like a baby: That baby can grow up to do anything! Be a resume builder, teacher, engineer, etc.
|
||||
|
||||
The only difference is that with roles, we're explicitly telling the LLM what we want it to be. Also: the LLM is already
|
||||
grown up so we don't have to wait!
|
||||
|
||||

|
||||
|
||||
## Quick Links
|
||||
<!--toc:start-->
|
||||
- [Role Definition](#role-definition)
|
||||
- [Metadata Header](#metadata-header)
|
||||
- [Instructions](#instructions)
|
||||
- [Special Case: Metadata Header Only](#special-case-metadata-header-only)
|
||||
- [Prompt Types](#prompt-types)
|
||||
- [Embedded Prompts](#embedded-prompts)
|
||||
- [System Prompts](#system-prompts)
|
||||
- [Few-Shot Prompt](#few-shot-prompt)
|
||||
- [Built-In Roles](#built-in-roles)
|
||||
<!--toc:end-->
|
||||
|
||||
---
|
||||
|
||||
## Role Definition
|
||||
Roles in Loki are Markdown files that live in the `roles` directory of your Loki configuration. Loki configuration
|
||||
locations vary between systems, so you can use the following command to find the location of your roles configuration
|
||||
directory:
|
||||
|
||||
```shell
|
||||
loki --info | grep 'roles_dir' | awk '{print $2}'
|
||||
```
|
||||
|
||||
All role configuration files have two parts: The metadata header, and the instructions.
|
||||
|
||||
**Example:** An expert resume builder role that specializes in helping users build and refine their resumes.
|
||||
```markdown
|
||||
---
|
||||
# This is the metadata header
|
||||
name: resume-builder
|
||||
model: openai:gpt-4o
|
||||
temperature: 0.2
|
||||
top_p: 0
|
||||
enabled_tools: fs_ls,fs_cat
|
||||
enabled_mcp_servers: github
|
||||
---
|
||||
<!-- This is the instructions -->
|
||||
You are an expert resume builder.
|
||||
```
|
||||
|
||||
To see a full example configuration for a role, refer to the [example role configuration](../config.role.example.md)
|
||||
file in the root of the repo.
|
||||
|
||||
### Metadata Header
|
||||
The metadata header in all role configuration files is completely optional. It lets you define role-specific settings
|
||||
for each role that make the model work the way you want for your role. This includes things like forcing your role to
|
||||
always use a specific model, set of tools, and tailoring the hyperparameters of the model for your role.
|
||||
|
||||
The header consists of a YAML-formatted list of settings that let you customize the model behavior for your role. These
|
||||
settings sit between `---` separators in your role configuration so Loki knows they're not part of the instructions you
|
||||
want to feed the model.
|
||||
|
||||
The following table lists the available configuration settings and their default values (if undefined):
|
||||
|
||||
| Setting | Default | Description |
|
||||
|-----------------------|----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
|
||||
| `name` | The name of the role markdown file | The name of the role |
|
||||
| `model` | Default configured model or currently in-use model (REPL mode) | The preferred model to use with this role |
|
||||
| `temperature` | Default `temperature` for the preferred model | Controls the creativity and randomness of the model's responses |
|
||||
| `top_p` | Default `top_p` for the preferred model | Alternative way to control the model's output diversity, affecting the <br>probability distribution of tokens |
|
||||
| `enabled_tools` | Global setting for `enabled_tools` | The tools that this role utilizes |
|
||||
| `enabled_mcp_servers` | Global setting for `enabled_mcp_servers` | The MCP servers that this role utilizes |
|
||||
| `prompt` | `null` | See [Prompt Types](#prompt-types) for detailed usage |
|
||||
|
||||
### Instructions
|
||||
The instructions for a role is what you use to tell the model how you want it to behave. This typically consists of one
|
||||
or two sentences, but can be more. To see some examples, look at the [built-in roles](../assets/roles) to see how they are defined.
|
||||
|
||||
**Pro-Tip:** The struggle to create good instructions for a role (or any other kind of instructions for your model) is
|
||||
so common, that Loki comes with a role to help you write instructions for roles! Simply invoke the role to start
|
||||
creating a role with the `create-prompt` role:
|
||||
|
||||
```shell
|
||||
loki -r create-prompt
|
||||
```
|
||||
|
||||
### Special Case: Metadata Header Only
|
||||
When instructions are defined, the metadata header is optional. However sometimes we want a way to enable specific
|
||||
functions or MCP servers when prompting different models. In this situation, you need only specify the metadata header
|
||||
to just enable these settings as you like.
|
||||
|
||||
**Example: Role that enables all filesystem tools**
|
||||
`roles/filesystem-functions.md`
|
||||
```markdown
|
||||
---
|
||||
enabled_tools: fs_ls,fs_cat,fs_mkdir,fs_patch,fs_write
|
||||
---
|
||||
```
|
||||
|
||||
**Example: Role that enables the GitHub MCP server with the ollama:deepseek-r1 model**
|
||||
`roles/github.md`
|
||||
```markdown
|
||||
---
|
||||
model: ollama:deepseek-r1
|
||||
enabled_mcp_servers: github
|
||||
---
|
||||
```
|
||||
|
||||
For more examples of this special use case of roles, you can look at the role configuration files for the following
|
||||
built-in roles:
|
||||
|
||||
* [explain-shell](../assets/roles/explain-shell.md) - Explains cryptic shell commands in natural language
|
||||
* [functions](../assets/roles/functions.md) - Enables all available functions (i.e. all globally `visible_functions`)
|
||||
* [mcp-servers](../assets/roles/mcp-servers.md) - Enables all available MCP servers
|
||||
|
||||
## Special Variables
|
||||
Loki has a set of built-in special variables that it will inject into your role's instructions if it finds them in the
|
||||
`{{variable_name}}` syntax. The available special variables are listed below:
|
||||
|
||||
| Name | Description | Example |
|
||||
|-----------------|-----------------------------------------------------------|----------------------------|
|
||||
| `__os__` | Operating system name | `linux` |
|
||||
| `__os_family__` | Operating system family | `unix` |
|
||||
| `__arch__` | System architecture | `x86_64` |
|
||||
| `__shell__` | The current user's default shell | `bash` |
|
||||
| `__locale__` | The current user's preferred language and region settings | `en-US` |
|
||||
| `__now__` | Current timestamp in ISO 8601 format | `2025-11-07T10:15:44.268Z` |
|
||||
| `__cwd__` | The current working directory | `/tmp` |
|
||||
|
||||
## Prompt Types
|
||||
In Loki, you can also create roles with pre-configured prompts so you can template prompts for your use cases. This is
|
||||
the purpose of the `prompt` field in the role's metadata header.
|
||||
|
||||
There's three types of prompts you can create:
|
||||
|
||||
### Embedded Prompts
|
||||
Embedded prompts let you create templated prompts for any input given to it. They are ideal for concise, input-driven
|
||||
replies from the model. The input that users pass to Loki are injected into your prompt via a `__INPUT__` placeholder in
|
||||
your prompt.
|
||||
|
||||
**Example: Role to convert the given input to TOML**
|
||||
`roles/convert-to-toml.md`
|
||||
```markdown
|
||||
---
|
||||
prompt: convert __INPUT__ to TOML
|
||||
---
|
||||
Convert the given input to TOML format. Exclude any markdown formatting or code blocks and only output code.
|
||||
```
|
||||
Usage:
|
||||
```shell
|
||||
$ loki -r json-to-toml '{"test":"hi me"}'
|
||||
test = "hi me"
|
||||
```
|
||||
|
||||
Without the instructions (i.e. the prompt after the metadata header), this role would simply generate the following
|
||||
message for the model:
|
||||
|
||||
```json
|
||||
[
|
||||
{"role": "user", "content": "convert {\"test\":\"hi me\"} to TOML"}
|
||||
]
|
||||
```
|
||||
|
||||
### System Prompts
|
||||
System prompts let you set the general context of the LLMs behavior. This is no different than other system prompts you
|
||||
define in ChatGPT, Claude, Open WebUI, etc.
|
||||
|
||||
They are essentially Embedded Prompts without an `__INPUT__` placeholder.
|
||||
|
||||
**Example: Role to convert all input words to emoji**
|
||||
`roles/emoji.md`
|
||||
```markdown
|
||||
---
|
||||
prompt: convert my words to emojis
|
||||
---
|
||||
Convert all given input words into emojis
|
||||
```
|
||||
Usage:
|
||||
```shell
|
||||
$ loki -r emoji music joy
|
||||
🎵 😊
|
||||
```
|
||||
|
||||
Without the instructions (i.e. the prompt after the metadata header), this role would simply generate the following
|
||||
messages for the model:
|
||||
|
||||
```json
|
||||
[
|
||||
{"role": "system", "content": "convert my words to emojis"},
|
||||
{"role": "user", "content": "music joy"}
|
||||
]
|
||||
```
|
||||
|
||||
### Few-Shot Prompt
|
||||
[Few-Shot prompting](https://www.promptingguide.ai/techniques/fewshot) is a technique to enable in-context learning for LLMs by providing examples in the prompt to steer
|
||||
the model to better performance. In Loki, this is done as an extension of System Prompts.
|
||||
|
||||
**Example: Role to output code only**
|
||||
`roles/code-generator.md`
|
||||
~~~markdown
|
||||
---
|
||||
prompt: |-
|
||||
Output code only without comments or explanations.
|
||||
### INPUT:
|
||||
async sleep in js
|
||||
### OUTPUT:
|
||||
```javascript
|
||||
async function timeout(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
```
|
||||
---
|
||||
Output code only in response to the user's request
|
||||
~~~
|
||||
Usage:
|
||||
~~~shell
|
||||
$ loki -r code-generator python add two numbers
|
||||
```python
|
||||
# Function to add two numbers
|
||||
def add_numbers(num1, num2):
|
||||
return num1 + num2
|
||||
|
||||
# Example usage
|
||||
number1 = 5
|
||||
number2 = 7
|
||||
|
||||
result = add_numbers(number1, number2)
|
||||
print(f"The sum of {number1} and {number2} is {result}.")
|
||||
```
|
||||
~~~
|
||||
|
||||
Without the instructions (i.e. the prompt after the metadata header), this role would simply generate the following
|
||||
messages for the model:
|
||||
|
||||
```json
|
||||
[
|
||||
{"role": "system", "content": "Output code only without comments or explanations."},
|
||||
{"role": "user", "content": "async sleep in js"},
|
||||
{"role": "assistant", "content": "```javascript\nasync function timeout(ms) {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n```"},
|
||||
{"role": "user", "content": "python add two numbers"}
|
||||
]
|
||||
```
|
||||
|
||||
## Built-In Roles
|
||||
Loki comes packaged with some useful built-in roles. These are also good examples if you're looking for more examples on
|
||||
how to make your own roles, so be sure to check out the [built-in role definitions](../assets/roles) if you're looking
|
||||
for more examples.
|
||||
|
||||
* `code`: Generates code (used by `loki -c`)
|
||||
* `create-prompt`: Creates a prompt based on the user's input
|
||||
* `create-title`: Creates 3-6 word titles based on the user's input
|
||||
* `explain-shell`: Explains shell commands
|
||||
* `functions`: Enable all globally-visible functions
|
||||
* `github`: Interact with GitHub using natural language
|
||||
* `mcp-servers`: Enables all MCP servers
|
||||
* `repo-analyzer`: Ask questions about the code repository in the current working directory
|
||||
* `shell`: Convert natural language into shell commands (used by `loki -e`)
|
||||
* `slack`: Interact with Slack using natural language
|
||||
|
||||
## Temporary Roles
|
||||
Loki also enables you to create temporary roles that will be discarded once you're finished with them. This is done via
|
||||
the `.prompt/--prompt` command:
|
||||
|
||||

|
||||
@@ -0,0 +1,44 @@
|
||||
# Sessions
|
||||
By default, Loki does not send back all previous messages in a conversation to the model. This means that each time you
|
||||
query a model, it's essentially a one-off. However, Loki does support chat-like conversations with LLMs via its
|
||||
`sessions` mechanism.
|
||||
|
||||
Sessions in Loki enable the familiar conversational interactions with LLMs. This means you can reference previous
|
||||
answers and ask follow-up questions and the model will know what you're referring to.
|
||||
|
||||
Sessions can be temporary, or can be saved so you can continue conversations at a later time.
|
||||
|
||||
Saved sessions are stored in the `sessions` subdirectory of the Loki configuration directory. The location of the
|
||||
`sessions` directory varies by system, so you can use the following command to find the `sessions` directory if you need
|
||||
it:
|
||||
|
||||
```shell
|
||||
loki --info | grep 'sessions_dir' | awk '{print $2}'
|
||||
```
|
||||
|
||||
## Usage
|
||||
When you use a session in Loki, you can either persist it or discard it once you're done. Sessions you discard are then
|
||||
just considered 'temporary' sessions.
|
||||
|
||||

|
||||
|
||||
Sessions you persist and then load again later will inherit the same configuration as was used during the last usage of
|
||||
that session. That is to say, if you had certain tools or MCP servers enabled when you were last in that session, they
|
||||
will be available again when you continue that session.
|
||||
|
||||
## Configuration
|
||||
Session behavior can be configured from the global Loki configuration file. The location of this file varies between
|
||||
systems so you can use the following command to locate it on your system:
|
||||
|
||||
```shell
|
||||
loki --info | grep 'config_file' | awk '{print $2}'
|
||||
```
|
||||
|
||||
The following settings are available to customize the default behavior of sessions globally:
|
||||
|
||||
| Setting | Description |
|
||||
|--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `save_session` | Controls the persistence of the session. <br><ul><li>If `true`, then any time you're in a session, changes will auto-save unless explicitly defined otherwise.</li> <li>If `false`, then any time you're in a session, changes will not auto-save unless explicitly specified otherwise.</li><li>If `null`, Loki will always prompt the user for what to do.</li></ul> |
|
||||
| `compression_threshold` | Defines the token count threshold at which Loki will compress the session to save on the context length |
|
||||
| `summarization_prompt` | This is the prompt that is used to compress the session up to a given point when compression is triggered |
|
||||
| `summary_context_prompt` | This is the prompt that's used to add the summarized conversation generated by the `summarization_prompt` as context to the model |
|
||||
@@ -0,0 +1,104 @@
|
||||
# Loki Shell Integrations
|
||||
Loki supports the following integrations with a handful of shell environments to enhance user experience and streamline workflows.
|
||||
|
||||
## Tab Completions
|
||||
### Dynamic
|
||||
Dynamic tab completions are supported by Loki to assist users in quickly completing commands, options, and arguments.
|
||||
You can enable it by using the corresponding command for your shell. To enable dynamic tab completions for every
|
||||
shell session (i.e. persistently), add the corresponding command to your shell's configuration file as indicated:
|
||||
|
||||
```shell
|
||||
# Bash
|
||||
# (add to: `~/.bashrc`)
|
||||
source <(COMPLETE=bash loki)
|
||||
|
||||
# Zsh
|
||||
# (add to: `~/.zshrc`)
|
||||
source <(COMPLETE=zsh loki)
|
||||
|
||||
# Fish
|
||||
# (add to: `~/.config/fish/config.fish`)
|
||||
source <(COMPLETE=fish loki | psub)
|
||||
|
||||
# Elvish
|
||||
# (add to: `~/.elvish/rc.elv`)
|
||||
eval (E:COMPLETE=elvish loki | slurp)
|
||||
|
||||
# PowerShell
|
||||
# (add to: `$PROFILE`)
|
||||
$env:COMPLETE = "powershell"
|
||||
loki | Out-String | Invoke-Expression
|
||||
```
|
||||
|
||||
At the time of writing, `nushell` is not yet fully supported for dynamic tab completions due to limitations
|
||||
in the [`clap`](https://crates.io/crates/clap) crate. However, `nushell` support is being actively developed, and will
|
||||
be added in a future release.
|
||||
|
||||
Progress on this feature can be tracked in the following issue: [Clap Issue #5840](https://github.com/clap-rs/clap/issues/5840).
|
||||
|
||||
### Static
|
||||
Static tab completions (i.e. pre-generated completion scripts that are not context aware) can also be generated using the
|
||||
`--completions` flag. You can enable static tab completions by using the corresponding commands for your shell. These commands
|
||||
will enable them for every shell session (i.e. persistently):
|
||||
|
||||
```shell
|
||||
# Bash
|
||||
echo 'source <(loki --completions bash)' >> ~/.bashrc
|
||||
|
||||
# Zsh
|
||||
echo 'source <(loki --completions zsh)' >> ~/.zshrc
|
||||
|
||||
# Fish
|
||||
echo 'loki --completions fish | source' >> ~/.config/fish/config.fish
|
||||
|
||||
# Elvish
|
||||
echo 'eval (loki --completions elvish | slurp)' >> ~/.elvish/rc.elv
|
||||
|
||||
# Nushell
|
||||
[[ -d ~/.config/nushell/completions ]] || mkdir -p ~/.config/nushell/completions
|
||||
loki --completions nushell | save -f ~/.config/nushell/completions/loki.nu
|
||||
echo 'use ~/.config/nushell/completions/cli.nu *' >> ~/.config/nushell/config.nu
|
||||
|
||||
# PowerShell
|
||||
Add-content $PROFILE "loki --completions powershell | Out-String | Invoke-Expression"
|
||||
```
|
||||
|
||||
## Shell Assistant
|
||||
Loki has an `-e,--execute` flag that allows users to run natural language commands directly from the CLI. It accepts
|
||||
natural language input and translates it into executable shell commands.
|
||||
|
||||

|
||||
|
||||
## Intelligent Command Completions
|
||||
Loki also provides shell scripts that bind `Alt-e` to `loki -e "<current command line>"`, allowing users to generate
|
||||
commands from natural text directly without invoking the CLI.
|
||||
|
||||
For example:
|
||||
|
||||
```shell
|
||||
$ find all typescript files with more than 100 lines<Alt-e>
|
||||
# Gets replaced with
|
||||
$ find . -name '*.ts' -type f -exec awk 'NR>100{exit 1}' {} \; -print
|
||||
```
|
||||
|
||||
To use the CLI helper, add the content of the appropriate integration script for your shell to your shell configuration file:
|
||||
* [Bash Integration](../scripts/shell-integration/integration.bash) (add to: `~/.bashrc`)
|
||||
* [Zsh Integration](../scripts/shell-integration/integration.zsh) (add to: `~/.zshrc`)
|
||||
* [Elvish Integration](../scripts/shell-integration/integration.elv) (add to: `~/.elvish/rc.elv`)
|
||||
* [Fish Integration](../scripts/shell-integration/integration.fish) (add to: `~/.config/fish/config.fish`)
|
||||
* [Nushell Integration](../scripts/shell-integration/integration.nu) (add to: `~/.config/nushell/config.nu`)
|
||||
* [PowerShell Integration](../scripts/shell-integration/integration.ps1) (add to: `$PROFILE`)
|
||||
|
||||
## Explain Commands
|
||||
In addition to the Shell Assistant, Loki has a built-in role that explains shell commands to you to decipher their
|
||||
language. So if Loki generates a command that you're unsure of what it does, simply pass it to the `explain-shell` role:
|
||||
|
||||

|
||||
|
||||
## Code Generation
|
||||
Users can also directly generate code snippets from natural language prompts using the `-c,--code` flag.
|
||||
|
||||

|
||||
|
||||
**Pro Tip:** Pipe the output of the code generation directly into `tee` to ensure the generated code is properly extracted
|
||||
from any generated Markdown (i.e. remove any triple backticks).
|
||||
@@ -0,0 +1,71 @@
|
||||
# Theming Loki
|
||||
Loki supports customizing the theme via a `.tmTheme` file.
|
||||
|
||||
## Setup
|
||||
To install a custom theme, download the `.tmTheme` file to the Loki configuration directory and name it `dark.tmTheme`
|
||||
or `light.tmTheme`. The location of the Loki configuration directory varies between systems, so you can use the
|
||||
following command to locate it on your system:
|
||||
|
||||
```shell
|
||||
loki --info | grep 'config_dir' | awk '{print $2}'
|
||||
```
|
||||
|
||||
## Themes
|
||||
### 1337-Scheme
|
||||
https://raw.githubusercontent.com/MarkMichos/1337-Scheme/ca6a329cfda8307449d405b70f8fab34b8fd23b5/1337.tmTheme
|
||||

|
||||
|
||||
### Coldark
|
||||
https://raw.githubusercontent.com/ArmandPhilippot/coldark-bat/e44750b2a9629dd12d8ed3ad9fd50c77232170b9/Coldark-Dark.tmTheme
|
||||

|
||||
|
||||
### Dracula
|
||||
https://raw.githubusercontent.com/dracula/sublime/c2de0acf5af67042393cf70de68013153c043656/Dracula.tmTheme
|
||||

|
||||
|
||||
### GitHub
|
||||
https://raw.githubusercontent.com/AlexanderEkdahl/github-sublime-theme/508740b2430c3c3a9e785fc93ee1d7c6f233af53/GitHub.tmTheme
|
||||

|
||||
|
||||
### gruvbox
|
||||
#### Dark
|
||||
https://raw.githubusercontent.com/subnut/gruvbox-tmTheme/64c47250e54298b91e2cf8d401320009aba9f991/gruvbox-dark.tmTheme
|
||||

|
||||
|
||||
#### Light
|
||||
https://raw.githubusercontent.com/subnut/gruvbox-tmTheme/64c47250e54298b91e2cf8d401320009aba9f991/gruvbox-light.tmTheme
|
||||

|
||||
|
||||
### OneHalf
|
||||
#### Dark
|
||||
https://raw.githubusercontent.com/sonph/onehalf/141c775ace6b71992305f144a8ab68e9a8ca4a25/sublimetext/OneHalfDark.tmTheme
|
||||

|
||||
|
||||
#### Light
|
||||
https://raw.githubusercontent.com/sonph/onehalf/141c775ace6b71992305f144a8ab68e9a8ca4a25/sublimetext/OneHalfLight.tmTheme
|
||||

|
||||
|
||||
### Solarized
|
||||
#### Dark
|
||||
https://raw.githubusercontent.com/braver/Solarized/87e01090cggjf5fb821a234265b3138426ae84900e7/Solarized%20(dark).tmTheme
|
||||

|
||||
|
||||
#### Light
|
||||
https://raw.githubusercontent.com/braver/Solarized/87e01090cf5fb821a234265b3138426ae84900e7/Solarized%20(light).tmTheme
|
||||

|
||||
|
||||
### Sublime Snazzy
|
||||
https://raw.githubusercontent.com/greggb/sublime-snazzy/70343201f1d7539adbba3c79e2fe81c2559a0431/Sublime%20Snazzy.tmTheme
|
||||

|
||||
|
||||
### TwoDark
|
||||
https://raw.githubusercontent.com/erremauro/TwoDark/8e0f6fa5b59d196658a22288f519fd8320de4c87/TwoDark.tmTheme
|
||||

|
||||
|
||||
### Visual Studio Dark+
|
||||
https://raw.githubusercontent.com/vidann1/visual-studio-dark-plus/01ee1e8e0dc578f3b4e8c0dbb6aa0279b4a26a40/Visual%20Studio%20Dark%2B.tmTheme
|
||||

|
||||
|
||||
### Zenburn
|
||||
https://raw.githubusercontent.com/colinta/zenburn/86d4ee7a1f884851a1d21d66249687f527fced32/zenburn.tmTheme
|
||||

|
||||
@@ -0,0 +1,250 @@
|
||||
# Todo System
|
||||
|
||||
Loki's Todo System is a built-in task tracking feature designed to improve the reliability and effectiveness of LLM agents,
|
||||
especially smaller models. It provides structured task management that helps models:
|
||||
|
||||
- Break complex tasks into manageable steps
|
||||
- Track progress through multistep workflows
|
||||
- Automatically continue work until all tasks are complete
|
||||
- Avoid forgetting steps or losing context
|
||||
|
||||

|
||||
|
||||
## Quick Links
|
||||
<!--toc:start-->
|
||||
- [Why Use the Todo System?](#why-use-the-todo-system)
|
||||
- [How It Works](#how-it-works)
|
||||
- [Configuration Options](#configuration-options)
|
||||
- [Available Tools](#available-tools)
|
||||
- [Auto-Continuation](#auto-continuation)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Example Workflow](#example-workflow)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
<!--toc:end-->
|
||||
|
||||
## Why Use the Todo System?
|
||||
Smaller language models often struggle with:
|
||||
- **Context drift**: Forgetting earlier steps in a multi-step task
|
||||
- **Incomplete execution**: Stopping before all work is done
|
||||
- **Lack of structure**: Jumping between tasks without clear organization
|
||||
|
||||
The Loki Todo System addresses these issues by giving the model explicit tools to plan, track, and verify task completion.
|
||||
The system automatically prompts the model to continue when incomplete tasks remain, ensuring work gets finished.
|
||||
|
||||
## How It Works
|
||||
1. **Planning Phase**: The model initializes a todo list with a goal and adds individual tasks
|
||||
2. **Execution Phase**: The model works through tasks, marking each done immediately after completion
|
||||
3. **Continuation Phase**: If incomplete tasks remain, the system automatically prompts the model to continue
|
||||
4. **Completion**: When all tasks are marked done, the workflow ends naturally
|
||||
|
||||
The todo state is preserved across the conversation (and any compressions), and injected into continuation prompts,
|
||||
keeping the model focused on remaining work.
|
||||
|
||||
## Configuration Options
|
||||
The Todo System is configured per-agent in `<loki-config-dir>/agents/<agent-name>/config.yaml`:
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
|----------------------------|---------|-------------|---------------------------------------------------------------------------------|
|
||||
| `auto_continue` | boolean | `false` | Enable the To-Do system for automatic continuation when incomplete todos remain |
|
||||
| `max_auto_continues` | integer | `10` | Maximum number of automatic continuations before stopping |
|
||||
| `inject_todo_instructions` | boolean | `true` | Inject the default todo tool usage instructions into the agent's system prompt |
|
||||
| `continuation_prompt` | string | (see below) | Custom prompt used when auto-continuing |
|
||||
|
||||
### Example Configuration
|
||||
```yaml
|
||||
# agents/my-agent/config.yaml
|
||||
model: openai:gpt-4o
|
||||
auto_continue: true # Enable auto-continuation
|
||||
max_auto_continues: 15 # Allow up to 15 automatic continuations
|
||||
inject_todo_instructions: true # Include todo instructions in system prompt
|
||||
continuation_prompt: | # Optional: customize the continuation prompt
|
||||
[CONTINUE]
|
||||
You have unfinished tasks. Proceed with the next pending item.
|
||||
Do not explain; just execute.
|
||||
```
|
||||
|
||||
### Default Continuation Prompt
|
||||
If `continuation_prompt` is not specified, the following default is used:
|
||||
|
||||
```
|
||||
[SYSTEM REMINDER - TODO CONTINUATION]
|
||||
You have incomplete tasks in your todo list. Continue with the next pending item.
|
||||
Call tools immediately. Do not explain what you will do.
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
When `inject_todo_instructions` is enabled (the default), agents have access to four built-in todo management tools:
|
||||
|
||||
### `todo__init`
|
||||
Initialize a new todo list with a goal. Clears any existing todos.
|
||||
|
||||
**Parameters:**
|
||||
- `goal` (string, required): The overall goal to achieve when all todos are completed
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{"goal": "Refactor the authentication module"}
|
||||
```
|
||||
|
||||
### `todo__add`
|
||||
Add a new todo item to the list.
|
||||
|
||||
**Parameters:**
|
||||
- `task` (string, required): Description of the todo task
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{"task": "Extract password validation into separate function"}
|
||||
```
|
||||
|
||||
**Returns:** The assigned task ID
|
||||
|
||||
### `todo__done`
|
||||
Mark a todo item as done by its ID.
|
||||
|
||||
**Parameters:**
|
||||
- `id` (integer, required): The ID of the todo item to mark as done
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{"id": 1}
|
||||
```
|
||||
|
||||
### `todo__list`
|
||||
Display the current todo list with status of each item.
|
||||
|
||||
**Parameters:** None
|
||||
|
||||
**Returns:** The full todo list with goal, progress, and item statuses
|
||||
|
||||
### `todo__clear`
|
||||
Clear the entire todo list and reset the goal. Use when the current task has been canceled or invalidated.
|
||||
|
||||
**Parameters:** None
|
||||
|
||||
**Returns:** Confirmation that the todo list was cleared
|
||||
|
||||
### REPL Command: `.clear todo`
|
||||
You can also clear the todo list manually from the REPL by typing `.clear todo`. This is useful when:
|
||||
- You gave a custom response that changes or cancels the current task
|
||||
- The agent is stuck in auto-continuation with stale todos
|
||||
- You want to start fresh without leaving and re-entering the agent
|
||||
|
||||
**Note:** This command is only available when an agent with `auto_continue: true` is active. If the todo
|
||||
system isn't enabled for the current agent, the command will display an error message.
|
||||
|
||||
## Auto-Continuation
|
||||
When `auto_continue` is enabled, Loki automatically sends a continuation prompt if:
|
||||
|
||||
1. The agent's response completes (model stops generating)
|
||||
2. There are incomplete tasks in the todo list
|
||||
3. The continuation count hasn't exceeded `max_auto_continues`
|
||||
4. The response isn't identical to the previous continuation (prevents loops)
|
||||
|
||||
### What Gets Injected
|
||||
Each continuation prompt includes:
|
||||
- The continuation prompt text (default or custom)
|
||||
- The current todo list state showing:
|
||||
- The goal
|
||||
- Progress (e.g., "3/5 completed")
|
||||
- Each task with status (✓ done, ○ pending)
|
||||
|
||||
**Example continuation context:**
|
||||
```
|
||||
[SYSTEM REMINDER - TODO CONTINUATION]
|
||||
You have incomplete tasks in your todo list. Continue with the next pending item.
|
||||
Call tools immediately. Do not explain what you will do.
|
||||
|
||||
Goal: Refactor the authentication module
|
||||
Progress: 2/4 completed
|
||||
✓ 1. Extract password validation into separate function
|
||||
✓ 2. Add unit tests for password validation
|
||||
○ 3. Update login handler to use new validation
|
||||
○ 4. Update registration handler to use new validation
|
||||
```
|
||||
|
||||
### Visual Feedback
|
||||
During auto-continuation, you'll see a message in your terminal:
|
||||
```
|
||||
📋 Auto-continuing (3/10): 2 incomplete todo(s) remain
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### For Agent Developers
|
||||
1. **Enable for complex workflows**: Use `auto_continue: true` for agents that handle multistep tasks
|
||||
2. **Set reasonable limits**: Adjust `max_auto_continues` based on typical task complexity
|
||||
3. **Customize the prompt**: If your agent needs specific continuation behavior, set a custom `continuation_prompt`
|
||||
|
||||
### For Model Behavior
|
||||
The injected instructions tell models to:
|
||||
|
||||
1. **Always create a todo list before starting work**: This ensures planning happens upfront
|
||||
2. **Mark each task done immediately after finishing**: Don't batch completions
|
||||
3. **Add all planned steps before starting**: Complete planning before execution
|
||||
|
||||
### When to Use vs. Skip
|
||||
**Use the Todo System when:**
|
||||
- Tasks have 3+ distinct steps
|
||||
- The model might lose track of progress
|
||||
- You want guaranteed completion of all steps
|
||||
- Working with smaller/less capable models
|
||||
|
||||
**Skip the Todo System when:**
|
||||
- Single-step, simple tasks
|
||||
- Interactive Q&A sessions
|
||||
- The overhead of task tracking isn't worth it
|
||||
|
||||
## Example Workflow
|
||||
Here's how a typical todo-driven workflow looks:
|
||||
|
||||
**User prompt:** "Add input validation to the user registration form"
|
||||
|
||||
**Model actions:**
|
||||
```
|
||||
1. todo__init(goal="Add input validation to user registration form")
|
||||
2. todo__add(task="Analyze current registration form fields")
|
||||
3. todo__add(task="Create validation rules for email field")
|
||||
4. todo__add(task="Create validation rules for password field")
|
||||
5. todo__add(task="Implement client-side validation")
|
||||
6. todo__add(task="Add server-side validation")
|
||||
7. todo__add(task="Write tests for validation logic")
|
||||
```
|
||||
|
||||
**Model executes first task, then:**
|
||||
```
|
||||
8. todo__done(id=1)
|
||||
9. [Proceeds with task 2...]
|
||||
10. todo__done(id=2)
|
||||
...
|
||||
```
|
||||
|
||||
**If model stops with incomplete tasks:**
|
||||
- System automatically sends continuation prompt
|
||||
- Model sees remaining tasks and continues
|
||||
- Repeats until all tasks are done or max continuations reached
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Model Not Using Todo Tools
|
||||
- Verify `inject_todo_instructions: true` in your agent config
|
||||
- Check that the agent is properly loaded (not just a role)
|
||||
- Some models may need explicit prompting to use the tools
|
||||
|
||||
### Too Many Continuations
|
||||
- Lower `max_auto_continues` to a reasonable limit
|
||||
- Check if the model is creating new tasks without completing old ones
|
||||
- Ensure tasks are appropriately scoped (not too granular)
|
||||
|
||||
### Continuation Loop
|
||||
The system detects when a model's response is identical to its previous continuation response and stops
|
||||
automatically. If you're seeing loops:
|
||||
- The model may be stuck; check if a task is impossible to complete
|
||||
- Consider adjusting the `continuation_prompt` to be more directive
|
||||
|
||||
---
|
||||
|
||||
## Additional Docs
|
||||
- [Agents](./AGENTS.md) - Full agent configuration guide
|
||||
- [Function Calling](./function-calling/TOOLS.md) - How tools work in Loki
|
||||
- [Sessions](./SESSIONS.md) - How conversation state is managed
|
||||
@@ -0,0 +1,161 @@
|
||||
# The Loki Vault
|
||||
The Loki vault lets users store sensitive secrets and credentials securely so that there's no plaintext secrets
|
||||
anywhere in your configurations.
|
||||
|
||||
It's based on the [G-Man library](https://github.com/Dark-Alex-17/gman) (which also comes in a binary format) which
|
||||
functions as a universal secret management tool.
|
||||
|
||||

|
||||
|
||||
## Quick Links
|
||||
<!--toc:start-->
|
||||
- [Usage](#usage)
|
||||
- [CLI Usage](#cli-usage)
|
||||
- [REPL Usage](#repl-usage)
|
||||
- [Motivation](#motivation)
|
||||
- [How it works](#how-it-works)
|
||||
- [Supported Files](#supported-files)
|
||||
- [Environment Variable Secret Injection in Agents](#environment-variable-secret-injection-in-agents)
|
||||
<!--toc:end-->
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
The Loki vault can be used in one of two ways: via the CLI or via the REPL for interactive usage.
|
||||
|
||||
### CLI Usage
|
||||
The vault is utilized from the CLI with the following flags:
|
||||
|
||||
```bash
|
||||
--add-secret <SECRET_NAME> Add a secret to the Loki vault
|
||||
--get-secret <SECRET_NAME> Decrypt a secret from the Loki vault and print the plaintext
|
||||
--update-secret <SECRET_NAME> Update an existing secret in the Loki vault
|
||||
--delete-secret <SECRET_NAME> Delete a secret from the Loki vault
|
||||
--list-secrets List all secrets stored in the Loki vault
|
||||
```
|
||||
(The above is also documented in `loki --help`)
|
||||
|
||||
Loki will guide you through manipulating your secrets to make usage easier.
|
||||
|
||||
### REPL Usage
|
||||
The vault can be access from within the Loki REPL using the `.vault` commands:
|
||||
|
||||

|
||||

|
||||
|
||||
The manipulation of your vault is guided in the same way as the CLI usage, ensuring ease of use.
|
||||
|
||||
## Motivation
|
||||
Loki is intended to be highly configurable and adaptable to many different use cases. This means that users of Loki
|
||||
should be able to share configurations for agents, tools, roles, etc. with other users or even entire teams.
|
||||
|
||||
My objective is to encourage this, and to make it so that users can easily version their configurations using version
|
||||
control. Good VCS hygiene dictates that one *never* commits secrets or sensitive information to a repository.
|
||||
|
||||
Since a number of files and configurations in Loki may contain sensitive information, the vault exists to solve this problem.
|
||||
|
||||
Users can either share the vault password with a team, making it so a single configuration can be pulled from VCS and used
|
||||
by said team. Alternatively, each user can maintain their own vault password and expect other users to replace secret values
|
||||
with their user-specific secrets.
|
||||
|
||||
## How it works
|
||||
When you first start Loki, if you don't already have a vault password file, it will prompt you to create one. This file
|
||||
houses the password that is used to encrypt and decrypt secrets within Loki. This file exists so that you are not prompted
|
||||
for a password every time Loki attempts to decrypt a secret.
|
||||
|
||||
When you encrypt a secret, it uses the local provider for `gman` to securely store those secrets in the Loki vault file.
|
||||
This file is typically located at your Loki configuration directory under `vault.yml`. If you open this file, you'll see a
|
||||
bunch of gibberish. This is because all secrets are encrypted using the password you provided, meaning only you can decrypt them.
|
||||
|
||||
Secrets are specified in Loki configurations using the same variable templating as the [Jinja templating engine](https://jinja.palletsprojects.com/en/stable/):
|
||||
|
||||
```
|
||||
{{some_variable}}
|
||||
```
|
||||
|
||||
So whenever you want Loki to use a secret from the vault, you simply specify the secret name in this format in the applicable
|
||||
file.
|
||||
|
||||
**Example:**
|
||||
Suppose my vault has a secret called `GITHUB_TOKEN` in it, and I want to use that in the MCP configuration. Then, I simply replace
|
||||
the expected value in my `mcp.json` with the templated secret:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"atlassian": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-remote", "https://mcp.atlassian.com/v1/sse"]
|
||||
},
|
||||
"github": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"-e",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||
"ghcr.io/github/github-mcp-server"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "{{GITHUB_TOKEN}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
At runtime, Loki will detect the templated secret and replace it with the decrypted value from the vault before executing.
|
||||
|
||||
## Supported Files
|
||||
At the time of writing, the following files support Loki secret injection:
|
||||
|
||||
| File Type | Description | Limitations |
|
||||
|-------------------------|-----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `config.yaml` | The main Loki configuration file | Cannot use secret injection on the `vault_password_file` field |
|
||||
| `functions/mcp.json` | The MCP server configuration file | |
|
||||
| `<agent>/tools.<py/sh>` | Tool files for agents | Specific configuration and only supported for Agents, not all global tools ([see below](#environment-variable-secret-injection-in-agents)) |
|
||||
|
||||
|
||||
Note that all paths are relative to the Loki configuration directory. The directory varies by system, so you can find yours by
|
||||
running
|
||||
|
||||
```shell
|
||||
loki --info | grep config_dir | awk '{print $2}'
|
||||
```
|
||||
|
||||
## Environment Variable Secret Injection in Agents
|
||||
Secrets from the Loki vault can be injected into agent `tools.sh/tools.py` as environment variables. This is done as
|
||||
follows:
|
||||
|
||||
1. Ensure a secret named `MY_USERNAME` is in your Loki vault.
|
||||
2. Set the name of the secret as the default value for a variable
|
||||
`<agent>/config.yaml`
|
||||
```yaml
|
||||
name: Username
|
||||
description: An AI agent that demonstrates agent capabilities
|
||||
instructions: |
|
||||
You are a AI agent designed to demonstrate agent capabilities.
|
||||
variables:
|
||||
- name: username
|
||||
description: Your user name
|
||||
# Configure the secret you want to inject using the same templating mentioned above; i.e. wrap the
|
||||
# case-sensitive name in '{{}}'
|
||||
default: '{{MY_USERNAME}}'
|
||||
```
|
||||
3. Reference the variable in your `<agent>/tools.<py/sh>` file using the familiar variable injection name; that is,
|
||||
since the name of the variable is `username`, the environment variable that will be provided to the tool call will
|
||||
be named `LLM_AGENT_VAR_USERNAME`
|
||||
`tools.sh`
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# @env LLM_OUTPUT=/dev/stdout The output path
|
||||
|
||||
# @cmd Get my username
|
||||
get_my_username() {
|
||||
echo "$LLM_AGENT_VAR_USERNAME" >> "$LLM_OUTPUT"
|
||||
}
|
||||
```
|
||||
|
||||
For more information about variable usage within agents, refer to the [Variables section](./AGENTS.md#user-defined-variables) of the [Agents README](./AGENTS.md)
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
# Model Clients
|
||||
|
||||
Loki supports a large number of model providers (referred to as `clients` since Loki is a client of these providers). In
|
||||
order to use them, you must configure each one in the `clients` array in the global Loki configuration file.
|
||||
|
||||
The location of the global Loki configuration file varies between systems, so you can use the following command to
|
||||
locate your configuration file:
|
||||
|
||||
```shell
|
||||
loki --info | grep 'config_file' | awk '{print $2}'
|
||||
```
|
||||
|
||||
## Quick Links
|
||||
<!--toc:start-->
|
||||
- [Supported Clients](#supported-clients)
|
||||
- [Client Configuration](#client-configuration)
|
||||
- [Authentication](#authentication)
|
||||
- [Extra Settings](#extra-settings)
|
||||
<!--toc:end-->
|
||||
|
||||
---
|
||||
|
||||
## Supported Clients
|
||||
Loki supports the following model client types:
|
||||
|
||||
* Azure AI Foundry
|
||||
* AWS Bedrock
|
||||
* Anthropic Claude
|
||||
* Cohere
|
||||
* Google Gemini
|
||||
* OpenAI
|
||||
* OpenAI-Compatible
|
||||
* GCP Vertex AI
|
||||
|
||||
In addition to the settings detailed below, each client may have additional settings specific to the provider. Check the
|
||||
[example global configuration file](../../config.example.yaml) to verify that your client has all the necessary fields
|
||||
defined.
|
||||
|
||||
## Client Configuration
|
||||
Each client in Loki has the same configuration settings available to them, with only special authentication fields added
|
||||
for specific clients as necessary. They are each placed under the `clients` array in your global configuration file:
|
||||
|
||||
```yaml
|
||||
clients:
|
||||
- name: client1
|
||||
# ... client configuration ...
|
||||
- name: client2
|
||||
# ... client configuration ...
|
||||
```
|
||||
|
||||
### Metadata
|
||||
The client metadata uniquely identifies the client in Loki so you can reference it across your configurations. The
|
||||
available settings are listed below:
|
||||
|
||||
| Setting | Description |
|
||||
|----------|------------------------------------------------------------------------------------------------------------|
|
||||
| `name` | The name of the client (e.g. `openai`, `gemini`, etc.) |
|
||||
| `auth` | Authentication method: `oauth` for OAuth, or omit to use `api_key` (see [Authentication](#authentication)) |
|
||||
| `models` | See the [model settings](#model-settings) documentation below |
|
||||
| `patch` | See the [client patch configuration](./PATCHES.md#client-configuration-patches) documentation |
|
||||
| `extra` | See the [extra settings](#extra-settings) documentation below |
|
||||
|
||||
Be sure to also check provider-specific configurations for any extra fields that are added for authentication purposes.
|
||||
|
||||
### Model Settings
|
||||
The `models` array lists the available models from the model client. Each one has the following settings:
|
||||
|
||||
| Setting | Required | Model Type | Description |
|
||||
|-----------------------------|----------|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `name` | * | `all` | The name of the model |
|
||||
| `real_name` | | `all` | You can define model aliases via the `name` field. However, Loki still needs to know the real name <br>of the model so it can query it. For example: If you have `name: gpt-alias`, then you must <br>also define `real_name: gpt-oss:latest` |
|
||||
| `type` | * | `all` | The type of model. Loki supports only 3 types of models: <ul><li>`chat`</li><li>`embedding`</li><li>`reranker`</li></ul> |
|
||||
| `input_price` | | `all` | The cost in USD per 1M tokens for each input sequence; Loki will keep track of usage costs if this is defined |
|
||||
| `output_price` | | `all` | The cost in USD per 1M tokens of the model output; Loki will keep track of usage costs if this is defined |
|
||||
| `patch` | | `all` | See the [model-specific patch configuration](./PATCHES.md#model-specific-patches) documentation |
|
||||
| `max_input_tokens` | | `all` | The maximum number of input tokens for the model |
|
||||
| `max_output_tokens` | | `chat` | The maximum number of output tokens for the model |
|
||||
| `require_max_tokens` | | `chat` | Whether to enforce the `max_output_tokens` constraint. |
|
||||
| `supports_vision` | | `chat` | Indicates if the model supports multimodal queries that would require vision (i.e. image recognition) |
|
||||
| `supports_function_calling` | | `chat` | Indicates if the model supports function calling |
|
||||
| `no_stream` | | `chat` | Enable or disable streaming API responses |
|
||||
| `no_system_message` | | `chat` | Controls whether the model supports system messages |
|
||||
| `system_prompt_prefix` | | `chat` | An additional prefix prompt to add to all system prompts to ensure consistent behavior across all interactions |
|
||||
| `max_tokens_per_chunk` | | `embedding` | The maximum chunk size supported by the embedding model |
|
||||
| `default_chunk_size` | | `embedding` | The default chunk size to use with the given model |
|
||||
| `max_batch_size` | | `embedding` | The maximum batch size that the given embedding model supports |
|
||||
|
||||
## Authentication
|
||||
|
||||
Loki clients support two authentication methods: **API keys** and **OAuth**. Each client entry in your configuration
|
||||
must use one or the other.
|
||||
|
||||
### API Key Authentication
|
||||
|
||||
Most clients authenticate using an API key. Simply set the `api_key` field directly or inject it from the
|
||||
[Loki vault](../VAULT.md):
|
||||
|
||||
```yaml
|
||||
clients:
|
||||
- type: claude
|
||||
api_key: '{{ANTHROPIC_API_KEY}}'
|
||||
```
|
||||
|
||||
API keys can also be provided via environment variables named `{CLIENT_NAME}_API_KEY` (e.g. `OPENAI_API_KEY`,
|
||||
`GEMINI_API_KEY`). See the [environment variables documentation](../ENVIRONMENT-VARIABLES.md#client-related-variables)
|
||||
for details.
|
||||
|
||||
### OAuth Authentication
|
||||
|
||||
For [providers that support OAuth](#providers-that-support-oauth), you can authenticate using your existing subscription instead of an API key. This uses
|
||||
the OAuth 2.0 PKCE flow.
|
||||
|
||||
**Step 1: Configure the client**
|
||||
|
||||
Add a client entry with `auth: oauth` and no `api_key`:
|
||||
|
||||
```yaml
|
||||
clients:
|
||||
- type: claude
|
||||
name: my-claude-oauth
|
||||
auth: oauth
|
||||
```
|
||||
|
||||
**Step 2: Authenticate**
|
||||
|
||||
Run the `--authenticate` flag with the client name:
|
||||
|
||||
```sh
|
||||
loki --authenticate my-claude-oauth
|
||||
```
|
||||
|
||||
Or if you have only one OAuth-configured client, you can omit the name:
|
||||
|
||||
```sh
|
||||
loki --authenticate
|
||||
```
|
||||
|
||||
Alternatively, you can use the REPL command `.authenticate`.
|
||||
|
||||
This opens your browser for the OAuth authorization flow. Depending on the provider, Loki will either start a
|
||||
temporary localhost server to capture the callback automatically (e.g. Gemini) or ask you to paste the authorization
|
||||
code back into the terminal (e.g. Claude). Loki stores the tokens in `~/.cache/loki/oauth` and automatically refreshes
|
||||
them when they expire.
|
||||
|
||||
#### Gemini OAuth Note
|
||||
Loki uses the following scopes for OAuth with Gemini:
|
||||
* https://www.googleapis.com/auth/generative-language.peruserquota
|
||||
* https://www.googleapis.com/auth/userinfo.email
|
||||
* https://www.googleapis.com/auth/generative-language.retriever (Sensitive)
|
||||
|
||||
Since the `generative-language.retriever` scope is a sensitive scope, Google needs to verify Loki, which requires full
|
||||
branding (logo, official website, privacy policy, terms of service, etc.). The Loki app is open-source and is designed
|
||||
to be used as a simple CLI. As such, there's no terms of service or privacy policy associated with it, and thus Google
|
||||
cannot verify Loki.
|
||||
|
||||
So, when you kick off OAuth with Gemini, you may see a page similar to the following:
|
||||

|
||||
|
||||
Simply click the `Advanced` link and click `Go to Loki (unsafe)` to continue the OAuth flow.
|
||||
|
||||

|
||||

|
||||
|
||||
**Step 3: Use normally**
|
||||
|
||||
Once authenticated, the client works like any other. Loki uses the stored OAuth tokens automatically:
|
||||
|
||||
```sh
|
||||
loki -m my-claude-oauth:claude-sonnet-4-20250514 "Hello!"
|
||||
```
|
||||
|
||||
> **Note:** You can have multiple clients for the same provider. For example: you can have one with an API key and
|
||||
> another with OAuth. Use the `name` field to distinguish them.
|
||||
|
||||
### Providers That Support OAuth
|
||||
* Claude
|
||||
* Gemini
|
||||
|
||||
## Extra Settings
|
||||
Loki also lets you customize some extra settings for interacting with APIs:
|
||||
|
||||
| Setting | Description |
|
||||
|-------------------|-------------------------------------------------------|
|
||||
| `proxy` | Set a proxy to use |
|
||||
| `connect_timeout` | Set the timeout in seconds for connections to the API |
|
||||
@@ -0,0 +1,368 @@
|
||||
# Request Patching in Loki
|
||||
Loki provides two mechanisms for modifying API requests sent to LLM providers: **Model-Specific Patches** and
|
||||
**Client Configuration Patches**. These allow you to customize request parameters, headers, and URLs to work around
|
||||
provider quirks or add custom behavior.
|
||||
|
||||
## Quick Links
|
||||
- [Model-Specific Patches](#model-specific-patches)
|
||||
- [Client Configuration Patches](#client-configuration-patches)
|
||||
- [Comparison](#comparison)
|
||||
- [Common Use Cases](#common-use-cases)
|
||||
- [Environment Variable Patches](#environment-variable-patches)
|
||||
- [Tips](#tips)
|
||||
- [Debugging Patches](#debugging-patches)
|
||||
|
||||
---
|
||||
|
||||
## Model-Specific Patches
|
||||
|
||||
### Overview
|
||||
Model-specific patches are applied **unconditionally** to a single model. They are useful for handling model-specific
|
||||
quirks or requirements.
|
||||
|
||||
### When to Use
|
||||
- A specific model requires certain parameters to be set or removed
|
||||
- A model needs different default values than other models from the same provider
|
||||
- You need to add special configuration for one model only
|
||||
|
||||
### Structure
|
||||
|
||||
```yaml
|
||||
models:
|
||||
- name: model-name
|
||||
type: chat
|
||||
# ... other model properties ...
|
||||
patch:
|
||||
url: "https://custom-endpoint.com" # Optional: override the API endpoint
|
||||
body: # Optional: modify request body
|
||||
<parameter>: <value> # Add or modify parameters
|
||||
<parameter>: null # Remove parameters (set to null)
|
||||
headers: # Optional: modify request headers
|
||||
<header-name>: <value> # Add or modify headers
|
||||
<header-name>: null # Remove headers (set to null)
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
#### Example 1: Removing Parameters
|
||||
OpenAI's o1 models don't support `temperature`, `top_p`, or `max_tokens` parameters. The `patch` removes them:
|
||||
|
||||
```yaml
|
||||
- name: o4-mini
|
||||
type: chat
|
||||
max_input_tokens: 200000
|
||||
max_output_tokens: 100000
|
||||
supports_function_calling: true
|
||||
patch:
|
||||
body:
|
||||
max_tokens: null # Remove max_tokens from request
|
||||
temperature: null # Remove temperature from request
|
||||
top_p: null # Remove top_p from request
|
||||
```
|
||||
|
||||
#### Example 2: Setting Required Parameters
|
||||
Some models require specific parameters to be set:
|
||||
|
||||
```yaml
|
||||
- name: o4-mini-high
|
||||
type: chat
|
||||
patch:
|
||||
body:
|
||||
reasoning_effort: high # Always set reasoning_effort to "high"
|
||||
max_tokens: null
|
||||
temperature: null
|
||||
```
|
||||
|
||||
#### Example 3: Custom Endpoint
|
||||
If a model needs a different API endpoint:
|
||||
|
||||
```yaml
|
||||
- name: custom-model
|
||||
type: chat
|
||||
patch:
|
||||
url: "https://special-endpoint.example.com/v1/chat"
|
||||
```
|
||||
|
||||
#### Example 4: Adding Headers
|
||||
Add authentication or custom headers:
|
||||
|
||||
```yaml
|
||||
- name: special-model
|
||||
type: chat
|
||||
patch:
|
||||
headers:
|
||||
X-Custom-Header: "special-value"
|
||||
X-API-Version: "2024-01"
|
||||
```
|
||||
|
||||
### How It Works
|
||||
1. When you use a model, Loki loads its configuration
|
||||
2. If the model has a `patch` field, it's **always applied** to every request
|
||||
3. The patch modifies the request URL, body, or headers before sending to the API
|
||||
4. Parameters set to `null` are **removed** from the request
|
||||
|
||||
---
|
||||
|
||||
## Client Configuration Patches
|
||||
|
||||
### Overview
|
||||
Client configuration patches allow you to apply customizations to **multiple models** based on
|
||||
**regex pattern matching**. They're defined in your `config.yaml` file and can target specific API types (`chat`,
|
||||
`embeddings`, or `rerank`).
|
||||
|
||||
### When to Use
|
||||
- You want to apply the same settings to multiple models from a provider
|
||||
- You need different configurations for different groups of models
|
||||
- You want to override the default client model settings
|
||||
- You need environment-specific customizations
|
||||
|
||||
### Structure
|
||||
|
||||
```yaml
|
||||
clients:
|
||||
- type: <client> # e.g., gemini, openai, claude
|
||||
# ... client configuration ...
|
||||
patch:
|
||||
chat_completions: # For chat models
|
||||
'<regex-pattern>': # Regex to match model names
|
||||
url: "..." # Optional: override endpoint
|
||||
body: # Optional: modify request body
|
||||
<parameter>: <value>
|
||||
headers: # Optional: modify headers
|
||||
<header>: <value>
|
||||
embeddings: # For embedding models
|
||||
'<regex-pattern>':
|
||||
# ... same structure ...
|
||||
rerank: # For reranker models
|
||||
'<regex-pattern>':
|
||||
# ... same structure ...
|
||||
```
|
||||
|
||||
### Pattern Matching
|
||||
- Patterns are **regular expressions** that match against the model name
|
||||
- Use `.*` to match all models
|
||||
- Use specific patterns like `gpt-4.*` to match model families
|
||||
- Use `model1|model2` to match multiple specific models
|
||||
|
||||
### Examples
|
||||
|
||||
#### Example 1: Disable Safety Filters for Gemini Models
|
||||
Apply to all Gemini chat models:
|
||||
|
||||
```yaml
|
||||
clients:
|
||||
- type: gemini
|
||||
api_key: "{{GEMINI_API_KEY}}"
|
||||
patch:
|
||||
chat_completions:
|
||||
'.*': # Matches all Gemini models
|
||||
body:
|
||||
safetySettings:
|
||||
- category: HARM_CATEGORY_HARASSMENT
|
||||
threshold: BLOCK_NONE
|
||||
- category: HARM_CATEGORY_HATE_SPEECH
|
||||
threshold: BLOCK_NONE
|
||||
- category: HARM_CATEGORY_SEXUALLY_EXPLICIT
|
||||
threshold: BLOCK_NONE
|
||||
- category: HARM_CATEGORY_DANGEROUS_CONTENT
|
||||
threshold: BLOCK_NONE
|
||||
```
|
||||
|
||||
#### Example 2: Apply Settings to Specific Model Family
|
||||
Only apply to GPT-4 models (not GPT-3.5):
|
||||
|
||||
```yaml
|
||||
clients:
|
||||
- type: openai
|
||||
api_key: "{{OPENAI_API_KEY}}"
|
||||
patch:
|
||||
chat_completions:
|
||||
'gpt-4.*': # Matches gpt-4, gpt-4-turbo, gpt-4o, etc.
|
||||
body:
|
||||
frequency_penalty: 0.2
|
||||
presence_penalty: 0.1
|
||||
```
|
||||
|
||||
#### Example 3: Different Settings for Different Models
|
||||
Apply different patches based on model name:
|
||||
|
||||
```yaml
|
||||
clients:
|
||||
- type: openai
|
||||
api_key: "{{OPENAI_API_KEY}}"
|
||||
patch:
|
||||
chat_completions:
|
||||
'gpt-4o': # Specific model
|
||||
body:
|
||||
temperature: 0.7
|
||||
'gpt-3.5.*': # Model family
|
||||
body:
|
||||
temperature: 0.9
|
||||
max_tokens: 2000
|
||||
```
|
||||
|
||||
#### Example 4: Modify Embedding Requests
|
||||
Apply to embedding models:
|
||||
|
||||
```yaml
|
||||
clients:
|
||||
- type: openai
|
||||
api_key: "{{OPENAI_API_KEY}}"
|
||||
patch:
|
||||
embeddings:
|
||||
'text-embedding-.*': # All text-embedding models
|
||||
body:
|
||||
dimensions: 1536
|
||||
encoding_format: "float"
|
||||
```
|
||||
|
||||
#### Example 5: Custom Headers for Specific Models
|
||||
Add headers only for certain models:
|
||||
|
||||
```yaml
|
||||
clients:
|
||||
- type: openai-compatible
|
||||
api_base: "https://api.example.com/v1"
|
||||
patch:
|
||||
chat_completions:
|
||||
'custom-model-.*':
|
||||
headers:
|
||||
X-Custom-Auth: "bearer-token"
|
||||
X-Model-Version: "latest"
|
||||
```
|
||||
|
||||
#### Example 6: Override Endpoint for Specific Models
|
||||
Use different endpoints for different model groups:
|
||||
|
||||
```yaml
|
||||
clients:
|
||||
- type: openai-compatible
|
||||
api_base: "https://default-endpoint.com/v1"
|
||||
patch:
|
||||
chat_completions:
|
||||
'premium-.*': # Premium models use different endpoint
|
||||
url: "https://premium-endpoint.com/v1/chat/completions"
|
||||
```
|
||||
|
||||
### How It Works
|
||||
1. When making a request, Loki checks if the client has a `patch` configuration
|
||||
2. It looks at the appropriate API type (`chat_completions`, `embeddings`, or `rerank`)
|
||||
3. For each pattern in that section, it checks if the regex matches the model name
|
||||
4. If a match is found, that patch is applied to the request
|
||||
5. Only the **first matching pattern** is applied (patterns are processed in order)
|
||||
|
||||
---
|
||||
|
||||
## Comparison
|
||||
|
||||
| Feature | Model-Specific Patch | Client Configuration Patch |
|
||||
|-----------------------|-----------------------|-------------------------------------|
|
||||
| **Scope** | Single model only | Multiple models via regex |
|
||||
| **Matching** | Exact model name | Regular expression pattern |
|
||||
| **Application** | Always applied | Only if pattern matches |
|
||||
| **API Type** | All APIs | Separate for chat/embeddings/rerank |
|
||||
| **Override** | Cannot be overridden | Can override model patch |
|
||||
| **Use Case** | Model-specific quirks | User preferences & customization |
|
||||
| **Application Order** | Applied first | Applied second (can override) |
|
||||
|
||||
### Patch Application Order
|
||||
When both patches are present, they're applied in this order:
|
||||
|
||||
1. **Model-Specific Patch**
|
||||
2. **Client Configuration Patch**
|
||||
|
||||
This means client configuration patches can override model-specific patches if they modify the same parameters.
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### Removing Unsupported Parameters
|
||||
Some models don't support standard parameters like `temperature` or `max_tokens`:
|
||||
|
||||
**Model Patch**:
|
||||
```yaml
|
||||
patch:
|
||||
body:
|
||||
temperature: null
|
||||
max_tokens: null
|
||||
```
|
||||
|
||||
### Adding Provider-Specific Parameters
|
||||
Providers often have unique parameters:
|
||||
|
||||
**Client Patch**:
|
||||
```yaml
|
||||
patch:
|
||||
chat_completions:
|
||||
'.*':
|
||||
body:
|
||||
safetySettings: [...] # Gemini
|
||||
thinking_budget: 10000 # DeepSeek
|
||||
response_format: # OpenAI
|
||||
type: json_object
|
||||
```
|
||||
|
||||
### Changing Endpoints
|
||||
Use custom or regional endpoints:
|
||||
|
||||
**Client Patch**:
|
||||
```yaml
|
||||
patch:
|
||||
chat_completions:
|
||||
'.*':
|
||||
url: "https://eu-endpoint.example.com/v1/chat"
|
||||
```
|
||||
|
||||
### Setting Default Values
|
||||
Provide defaults for specific models or model families:
|
||||
|
||||
**Client Patch**:
|
||||
```yaml
|
||||
patch:
|
||||
chat_completions:
|
||||
'claude-3-.*':
|
||||
body:
|
||||
max_tokens: 4096
|
||||
temperature: 0.7
|
||||
```
|
||||
|
||||
### Custom Authentication
|
||||
Add special authentication headers:
|
||||
|
||||
**Client Patch**:
|
||||
```yaml
|
||||
patch:
|
||||
chat_completions:
|
||||
'.*':
|
||||
headers:
|
||||
Authorization: "Bearer {{custom_token}}"
|
||||
X-Organization-ID: "org-123"
|
||||
```
|
||||
|
||||
## Environment Variable Patches
|
||||
You can also apply patches via environment variables for temporary overrides:
|
||||
|
||||
```bash
|
||||
export LLM_PATCH_OPENAI_CHAT_COMPLETIONS='{"gpt-4.*":{"body":{"temperature":0.5}}}'
|
||||
```
|
||||
|
||||
This takes precedence over client configuration patches but not model-specific patches.
|
||||
|
||||
## Tips
|
||||
1. **Use model patches** for permanent, model-specific requirements
|
||||
2. **Use client patches** for personal preferences or environment-specific settings
|
||||
3. **Test regex patterns** carefully
|
||||
4. **Set to `null`** to remove parameters, don't just omit them
|
||||
5. **Check each model provider's docs** for available parameters and their formats
|
||||
6. **Be specific** with patterns to avoid unintended matches
|
||||
7. **Remember order matters** - first matching pattern wins for client patches
|
||||
8. **Patches merge** - both types can be applied, with client patches overriding model patches
|
||||
|
||||
## Debugging Patches
|
||||
To see what request is actually being sent, enable debug logging:
|
||||
|
||||
```bash
|
||||
export RUST_LOG=loki=debug
|
||||
loki "your prompt here"
|
||||
```
|
||||
|
||||
This will show the final request body after all patches are applied.
|
||||
@@ -0,0 +1,279 @@
|
||||
# Bash Prompt Helpers
|
||||
|
||||
When creating bash based tools, it's often helpful to prompt the user for input or confirmation.
|
||||
|
||||
Loki comes pre-packaged with a handful of prompt helpers for your bash-based tools. These helpers
|
||||
can be used to prompt the user for various types of input, such as yes/no confirmations,
|
||||
text input, and selections from a list.
|
||||
|
||||
The utility script is located at `functions/utils/prompt-utils.sh` within your Loki `functions` directory.
|
||||
|
||||
The Loki `functions` directory varies between machines, so you can find its location on your system by running the following command in your terminal:
|
||||
|
||||
```shell
|
||||
loki --info | grep functions_dir | awk '{print $2}'
|
||||
```
|
||||
|
||||
## Quick Links
|
||||
<!--toc:start-->
|
||||
- [Import The Prompt Utils Into Your Tools Script](#import-the-prompt-utils-into-your-tools-script)
|
||||
- [Included Utility Functions](#included-utility-functions)
|
||||
- [input](#input)
|
||||
- [confirm](#confirm)
|
||||
- [list](#list)
|
||||
- [checkbox](#checkbox)
|
||||
- [password](#password)
|
||||
- [editor](#editor)
|
||||
- [with_validate](#with_validate)
|
||||
- [validate_present](#validate_present)
|
||||
- [detect_os](#detect_os)
|
||||
- [get_opener](#get_opener)
|
||||
- [open_link](#open_link)
|
||||
- [guard_operation](#guard_operation)
|
||||
- [guard_path](#guard_path)
|
||||
- [patch_file](#patch_file)
|
||||
- [error](#error)
|
||||
- [warn](#warn)
|
||||
- [info](#info)
|
||||
- [debug](#debug)
|
||||
- [trace](#trace)
|
||||
- [Colored Output](#colored-output)
|
||||
<!--toc:end-->
|
||||
|
||||
---
|
||||
|
||||
## Import The Prompt Utils Into Your Tools Script
|
||||
In order to use the bash prompt helpers in your bash scripts, you need to source the provided `prompt-utils.sh` script.
|
||||
This script is pre-packaged with Loki and is located [here](../../assets/functions/utils/prompt-utils.sh).
|
||||
|
||||
When sourcing the file in your bash script, you use the `LLM_PROMPT_UTILS_FILE` environment variable that automatically
|
||||
populates the `functions/utils/prompt-utils.sh` path for you.
|
||||
|
||||
Thus, to properly source and enable all the bash prompt helpers in your Bash tools, add the following prelude to your
|
||||
scripts:
|
||||
|
||||
```bash
|
||||
source "$LLM_PROMPT_UTILS_FILE"
|
||||
```
|
||||
|
||||
## Included Utility Functions
|
||||
Below are the built-in bash prompt helpers that can be used to enhance user interaction with your tool scripts.
|
||||
|
||||
### input
|
||||
Prompt for text input
|
||||
|
||||

|
||||
|
||||
**Example With Validation:**
|
||||
```bash
|
||||
text=$(with_validation 'input "Please enter something:"' validate_present 2>/dev/tty)
|
||||
```
|
||||
|
||||
**Example Without Validation:**
|
||||
```bash
|
||||
text=$(input "Please enter something:" 2>/dev/tty)
|
||||
```
|
||||
|
||||
### confirm
|
||||
Show a confirm dialog with options for yes/no
|
||||
|
||||

|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
confirmed=$(confirm "Do the thing?" 2>/dev/tty)
|
||||
if [[ $confirmed == "0" ]]; then echo "No"; else echo "Yes"; fi
|
||||
```
|
||||
|
||||
### list
|
||||
Renders a text based list of options that can be selected by the user using up, down, and enter
|
||||
keys that then returns the chosen option.
|
||||
|
||||

|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
options=("one" "two" "three" "four")
|
||||
choice=$(list "Select an item" "${options[@]}" 2>/dev/tty)
|
||||
echo "Your choice: ${options[$choice]}"
|
||||
```
|
||||
|
||||
### checkbox
|
||||
Render a text based list of options, where multiple options can be selected by the user using down, up,
|
||||
and enter keys that then returns the chosen options.
|
||||
|
||||

|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
options=("one" "two" "three" "four")
|
||||
checked=$(checkbox "Select one or more items" "${options[@]}" 2>/dev/tty)
|
||||
echo "Your choices: ${checked}"
|
||||
```
|
||||
|
||||
### password
|
||||
Show a password prompt displaying stars for each character typed.
|
||||
|
||||

|
||||
|
||||
**Example With Validation:**
|
||||
```bash
|
||||
validate_password() {
|
||||
if [[ ${#1} -lt 10 ]]; then
|
||||
echo "Password must be at least 10 characters"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
pass=$(with_validate 'password "Enter your password"' validate_password 2>/dev/tty)
|
||||
```
|
||||
|
||||
**Example Without Validation:**
|
||||
```bash
|
||||
pass="$(password "Enter your password:" 2>/dev/tty)"
|
||||
```
|
||||
|
||||
### editor
|
||||
Open the default editor (`$EDITOR`); if none is set, default back to `vi`
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
text=$(editor "Please enter something in the editor" 2>/dev/tty)
|
||||
echo -e "You wrote:\n${text}"
|
||||
```
|
||||
|
||||
### with_validate
|
||||
Evaluate the given prompt command with validation. This prompts the user for input until the
|
||||
validation functions returns 0.
|
||||
|
||||

|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
# Using the built-in 'validate_present' validator
|
||||
text=$(with_validate 'input "Please enter something and confirm with enter"' validate_present 2>/dev/tty)
|
||||
|
||||
# Using a custom validator; e.g. for password
|
||||
validate_password() {
|
||||
if [[ ${#1} -lt 10 ]]; then
|
||||
echo "Password needs to be at least 10 characters"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
pass=$(with_validate 'password "Enter random password"' validate_password 2>/dev/tty)
|
||||
```
|
||||
|
||||
### validate_present
|
||||
Validate that the prompt returned a value.
|
||||
|
||||

|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
text=$(with_validate 'input "Please enter something and confirm with enter"' validate_present 2>/dev/tty)
|
||||
```
|
||||
|
||||
### detect_os
|
||||
Detect the current OS.
|
||||
|
||||
Returns one of the following:
|
||||
|
||||
* `solaris`
|
||||
* `macos`
|
||||
* `linux`
|
||||
* `bsd`
|
||||
* `windows`
|
||||
* `unknown`
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
detect_os
|
||||
```
|
||||
|
||||
### get_opener
|
||||
Determines the Os-specific file opening command (i.e. the command to open anything)
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
# Returns 'xdg-open'
|
||||
get_opener
|
||||
```
|
||||
|
||||
### open_link
|
||||
Opens the given link in the default browser
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
open_link https://www.google.com
|
||||
```
|
||||
|
||||
### guard_operation
|
||||
Prompt for permission to run an operation.
|
||||
|
||||
Can be disabled by setting the environment variable `AUTO_CONFIRM`.
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
guard_operation "Execute SQL?"
|
||||
_run_sql
|
||||
```
|
||||
|
||||
### guard_path
|
||||
Prompt for permission to perform path operations.
|
||||
|
||||
Can be disabled by setting the environment variable `AUTO_CONFIRM`.
|
||||
|
||||
**Example:***
|
||||
```bash
|
||||
guard_path "$target_path" "Remove '$target_path'?"
|
||||
rm -rf "$target_path"
|
||||
```
|
||||
|
||||
### patch_file
|
||||
Patch a file and show a diff using the default diff viewer. Uses git diff syntax.
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
new_contents="$(patch_file "$path" file.patch)"
|
||||
```
|
||||
|
||||
### error
|
||||
Log an error
|
||||
|
||||

|
||||
|
||||
### warn
|
||||
Log a warning
|
||||
|
||||

|
||||
|
||||
### info
|
||||
Log info
|
||||
|
||||

|
||||
|
||||
### debug
|
||||
Log a debug message
|
||||
|
||||

|
||||
|
||||
### trace
|
||||
Log a trace message
|
||||
|
||||

|
||||
|
||||
### Colored Output
|
||||
The following commands allow users to output text in specific colors.
|
||||
|
||||
* `red`
|
||||
* `green`
|
||||
* `gold`
|
||||
* `blue`
|
||||
* `magenta`
|
||||
* `cyan`
|
||||
* `white`
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
red "This will be red"
|
||||
yellow "This will be yellow"
|
||||
```
|
||||
@@ -0,0 +1,309 @@
|
||||
# Custom Bash-Based Tools
|
||||
Loki supports tools written in Bash. However, they must be written in a special format with special annotations in order
|
||||
for Loki to be able to properly parse and utilize them. This formatting ensures that each Bash script is
|
||||
self-describing, and formatted in such a way that Loki can anticipate how to execute it and what parameters to pass to
|
||||
it. This standardization also lets Loki compile the script into a JSON schema that can be used to inform the LLM about
|
||||
how to use the tool.
|
||||
|
||||
Each Bash-based tool must follow a specific structure in order for Loki to be able to properly compile and execute it:
|
||||
|
||||
* The tool must be a Bash script with a `.sh` file extension.
|
||||
* The script must have the following comments:
|
||||
* `# @describe ...` comment at the top that describes the tool.
|
||||
* `# @env LLM_OUTPUT=/dev/stdout The output path` comment to describe the `LLM_OUTPUT` environment variable. This
|
||||
syntax in particular assigns `/dev/stdout` as the default value for `LLM_OUTPUT`, so that if it's not set by Loki,
|
||||
the script will still function properly.
|
||||
* `# @option --option <value> An example option` comments to define each option that the tool accepts.
|
||||
* Use `--flag` syntax for boolean flags.
|
||||
* Use `--option <value>` syntax for options that accept a value.
|
||||
* Use `--option <value1,value2>` syntax for options that accept multiple values (i.e. arrays).
|
||||
* The script must have a `main` function
|
||||
* The `main` function must redirect the return value to the `>> "$LLM_OUTPUT"` environment variable.
|
||||
* This is necessary because Loki relies on the `$LLM_OUTPUT` environment variable to capture the output of the tool.
|
||||
|
||||
Essentially, you can think of the Bash-based tool script as just a normal Bash script that uses special comments to
|
||||
define a CLI.
|
||||
* The `# @env LLM_OUTPUT=/dev/stdout` comment to define the `$LLM_OUTPUT` environment variable (good practice)
|
||||
* A `# @describe`
|
||||
* And a `main` function that writes to `$LLM_OUTPUT`
|
||||
|
||||
The following section explains how you can add parameters to your bash functions and how to test out your scripts.
|
||||
|
||||
## Quick Links:
|
||||
<!--toc:start-->
|
||||
- [Loki Bash Tools Syntax](#loki-bash-tools-syntax)
|
||||
- [Metadata](#metadata)
|
||||
- [Environment Variables](#environment-variables)
|
||||
- [Arguments](#arguments)
|
||||
- [Flags](#flags)
|
||||
- [Options](#options)
|
||||
- [Subcommands (Agents only)](#subcommands-agents-only)
|
||||
- [Execute and Test Your Bash Tools](#execute-and-test-your-bash-tools)
|
||||
- [Example](#example)
|
||||
- [Prompt Helpers](#prompt-helpers)
|
||||
<!--toc:end-->
|
||||
|
||||
---
|
||||
|
||||
## Loki Bash Tools Syntax
|
||||
Loki Bash tools work via `@___` annotations that describe specific functionality of a script. The following reference
|
||||
explains the general syntax of these annotations and how to use them to create a CLI that Loki can recognize.
|
||||
|
||||
Refer to the [Execute and Test Your Bash Tools](#execute-and-test-your-bash-tools) section to learn how to test out your Bash tools
|
||||
without needing to go through Loki itself.
|
||||
|
||||
It's important to note that any functions prefixed with `_` are not sent to the LLM, so they will be invisible to the
|
||||
LLM at runtime.
|
||||
|
||||
### Metadata:
|
||||
You can define different metadata about your script to help Loki understand its dependencies and purpose.
|
||||
|
||||
```bash
|
||||
# Use the `@meta require-tools` annotation to specify any external tools that your script depends on.
|
||||
# @meta require-tools jq,yq
|
||||
|
||||
# Use the `@describe` annotation to describe the purpose of the script.
|
||||
# @describe A tool to interact with things
|
||||
```
|
||||
|
||||
### Environment Variables:
|
||||
```bash
|
||||
###########################
|
||||
## Environment Variables ##
|
||||
###########################
|
||||
|
||||
# Use `@env` to define environment variables that the script uses.
|
||||
# @env LLM_OUTPUT=/dev/stdout The output path, with a default value of '/dev/stdout' if not set.
|
||||
# @env OPTIONAL An optional environment variable
|
||||
# @env REQUIRED! A required environment variable
|
||||
# @env DEFAULT_VALUE=default An environment variable with a default value if unset.
|
||||
# @env DEFAULT_FROM_FN=`_default_env_fn` An environment variable with a default value calculated from a function if unset.
|
||||
# @env CHOICE[even|odd] An environment variable that, if set, must be set to either `even` or `odd`
|
||||
# @env CHOICE_WITH_DEFAULT[=even|odd] An environment variable that, if set, must be set to either `even` or `odd`, and defaults to `even` when unset
|
||||
# @env CHOICE_FROM_FN[`_choice_env_fn`] An environment variable that, if set, must be set to one of the values returned by the `_choice_fn` function.
|
||||
|
||||
# Example variable usage:
|
||||
export CHOICE=even
|
||||
# ./script.sh
|
||||
main() {
|
||||
[[ $CHOICE == "even" ]] || { echo "The value of the 'CHOICE' env var is not 'even'" >> "$LLM_OUTPUT" && exit 1 }
|
||||
}
|
||||
|
||||
# Loki does not pass functions prefixed with `_` to the LLM, so these are essentially `private` functions
|
||||
_default_env_fn() {
|
||||
echo "calculated default env value"
|
||||
}
|
||||
|
||||
# Loki does not pass functions prefixed with `_` to the LLM, so these are essentially `private` functions
|
||||
_choice_env_fn() {
|
||||
echo even
|
||||
echo odd
|
||||
}
|
||||
```
|
||||
|
||||
### Arguments:
|
||||
When referencing an argument defined via the `@arg` annotation, you can access its value using the `argc_<argument_name>` variable that
|
||||
is created at runtime.
|
||||
|
||||
```bash
|
||||
###############
|
||||
## Arguments ##
|
||||
###############
|
||||
|
||||
# Use `@arg` To define positional arguments for your script.
|
||||
# To reference an argument within your script, use the `argc_<argument_name>` variable.
|
||||
# @arg optional Optional argument
|
||||
# @arg required! Required argument
|
||||
# @arg multi_value* An argument that accepts multiple values (e.g. './script.sh one two three')
|
||||
# @arg multi_value_required+ An argument that is required and accepts multiple values
|
||||
# @arg value_notated <VALUE> An argument that explicitly specifies the name for documentation (e.g. Usage: ./script.sh [VALUE])
|
||||
# @arg default=default An argument with a default value if unset
|
||||
# @arg default_from_fn=`_default_arg_fn` An argument with a default value calculated from a function if unset
|
||||
# @arg choice[even|odd] An argument that, if set, must be set to either `even` or `odd`
|
||||
# @arg required_choice+[even|odd] An required argument that must be set to either `even` or `odd`
|
||||
# @arg default_choice[=even|odd] An argument that if unset defaults to 'even', but if set must be either `even` or `odd`
|
||||
# @arg multi_value_choice*[even|odd] An argument that, if set, must be set to either `even` or `odd`, and accepts multiple values
|
||||
# @arg choice_fn[`_choice_arg_fn`] An argument that, if set, must be set to one of the values returned by the `_choice_arg_fn` function.
|
||||
# @arg choice_fn_no_valid[?`_choice_arg_fn`] An argument that, if set, can be set to one of the values returned by the `_choice_arg_fn` function,
|
||||
# but does not validate the value.
|
||||
# @arg multi_choice_fn*[`_choice_arg_fn`] An argument that, if set, must be set to one of the values returned by the `_choice_arg_fn` function,
|
||||
# and accepts multiple values.
|
||||
# @arg multi_choice_comma_fn*,[`_choice_arg_fn`] An argument that, if set, must be set to one of the values returned by the `_choice_arg_fn` function,
|
||||
# and accepts multiple values in the form of a comma-separated list
|
||||
# @arg capture_arg~ An argument that captures all remaining args passed to the script
|
||||
|
||||
# Example usage 1: ./script.sh something_required
|
||||
main() {
|
||||
[[ $argc_required == "something_required" ]] || { echo "The value of the 'required' arg is not 'something_required'" >> "$LLM_OUTPUT" && exit 1 }
|
||||
}
|
||||
|
||||
# Example usage 2: ./script.sh this is a test
|
||||
main() {
|
||||
[[ "${argc_multi_value[*]}" == "this is a test" ]] || { echo "The value of the 'multi_value' arg is not 'this is a test'" >> "$LLM_OUTPUT" && exit 1 }
|
||||
}
|
||||
|
||||
|
||||
# Loki does not pass functions prefixed with `_` to the LLM, so these are essentially `private` functions
|
||||
_default_arg_fn() {
|
||||
echo "default arg value"
|
||||
}
|
||||
|
||||
# Loki does not pass functions prefixed with `_` to the LLM, so these are essentially `private` functions
|
||||
_choice_arg_fn() {
|
||||
echo even
|
||||
echo odd
|
||||
}
|
||||
```
|
||||
|
||||
### Flags:
|
||||
To access the value of a flag defined via the `@flag` annotation, you can check the value of the `argc_<flag_name>` variable.
|
||||
|
||||
```bash
|
||||
###########
|
||||
## Flags ##
|
||||
###########
|
||||
|
||||
# Use `@flag` to define boolean flags for your script
|
||||
# To reference a flag within your script, use the `argc_<argument_name>` variable.
|
||||
# @flag --bool A boolean flag with only a long option
|
||||
# @flag -b --bool A boolean flag with a short and long option
|
||||
# @flag -b A boolean flag with only a short option
|
||||
# @flag --multi* A boolean flag that can be used multiple times (e.g. '--multi --multi' will return '2')
|
||||
|
||||
# Example usage 1: ./script.sh --bool
|
||||
main() {
|
||||
[[ $argc_bool == "1" ]] || { echo "The value of the 'bool' flag is not '1'" >> "$LLM_OUTPUT" && exit 1 }
|
||||
}
|
||||
|
||||
# Example usage 2: ./script.sh --multi --multi
|
||||
main() {
|
||||
[[ $argc_multi == "2" ]] || { echo "The value of the 'multi' flag is not 2" >> "$LLM_OUTPUT" && exit 1 }
|
||||
}
|
||||
```
|
||||
|
||||
### Options:
|
||||
To access the value of an option defined via the `@option` annotation, you can check the value of the `argc_<option_name>` variable.
|
||||
|
||||
```bash
|
||||
#############
|
||||
## Options ##
|
||||
#############
|
||||
|
||||
# Use `@option` to define flags that accept values
|
||||
# To reference an option within your script, use the `argc_<argument_name>` variable.
|
||||
# @option --option An option that accepts a value with only a long flag
|
||||
# @option -o --option An option that accepts a value with both a short and long flag
|
||||
# @option -o An option that accepts a value with only a short flag
|
||||
# @option --required A required option that accepts a value
|
||||
# @option --multi* An option that accepts multiple values
|
||||
# @option --required-multi+ An option that accepts multiple values and is required
|
||||
# @option --multi-comma*, An option that accepts multiple values in the form of a comma-separated list
|
||||
# @option --value <VALUE> An option that explicitly specifies the name for documentation (e.g. Usage: ./script.sh --value [VALUE])
|
||||
# @option --two-args <SRC> <DEST> An option that accepts two arguments and explicitly names them for documentation
|
||||
# (e.g. Usage: ./script.sh --two-args [SRC] [DEST])
|
||||
# @option --unlimited-args <SRC> <DEST+> An option that accepts an unlimited number of arguments and explicitly names them for documentation
|
||||
# (e.g. Usage: ./script.sh --unlimited-args [SRC] [DEST ...])
|
||||
# @option --default=default An option that has a default value if unset
|
||||
# @option --default-from-fn=`_default_opt_fn` An option that has a default value calculated from a function if unset
|
||||
# @option --choice[even|odd] An option that, if set, must be set to either `even` or `odd`
|
||||
# @option --choice-default[=even|odd] An option that, if unset, defaults to `even`, but if set must be either `even` or `odd`
|
||||
# @option --choice-multi*[even|odd] An option that, if set, must be set to either `even` or `odd`, and can be specified multiple times
|
||||
# (e.g. ./script.sh --choice-multi even --choice-multi odd)
|
||||
# @option --required-choice-multi+[even|odd] A required option that, must be set to either `even` or `odd`, and can be specified multiple times
|
||||
# @option --choice-fn[`_choice_opt_fn`] An option that, if set, must be set to one of the values returned by the `_choice_opt_fn` function.`
|
||||
# @option --choice-fn-no-valid[?`_choice_opt_fn`] An option that, if set, can be set to one of the values returned by the `_choice_opt_fn` function, with no validation
|
||||
# @option --choice-multi-fn*[`_choice_opt_fn`] An option that, if set, must be set to one of the values returned by the `_choice_opt_fn` function,
|
||||
# and can be specified multiple times
|
||||
# @option --choice-multi-comma*,[`_choice_opt_fn`] An option that, if set, must be set to one of the values returned by the `_choice_opt_fn` function,
|
||||
# and is specified as a comma-separated list
|
||||
# @option --capture~ An option that captures all remaining arguments passed to the script
|
||||
|
||||
# Example usage 1: ./script.sh --option some_value
|
||||
main() {
|
||||
[[ $argc_option == "some_value" ]] || { echo "The value of the 'option' option is not 'some_value'" >> "$LLM_OUTPUT" && exit 1 }
|
||||
}
|
||||
|
||||
# Example usage 2: ./script.sh --multi value1 --multi value2
|
||||
main() {
|
||||
[[ "${argc_multi[*]}" == "value1 value2" ]] || { echo "The value of the 'multi' option is not 'value1 value2'" >> "$LLM_OUTPUT" && exit 1 }
|
||||
}
|
||||
|
||||
|
||||
# Loki does not pass functions prefixed with `_` to the LLM, so these are essentially `private` functions
|
||||
_default_opt_fn() {
|
||||
echo "calculated default option value"
|
||||
}
|
||||
|
||||
# Loki does not pass functions prefixed with `_` to the LLM, so these are essentially `private` functions
|
||||
_choice_opt_fn() {
|
||||
echo even
|
||||
echo odd
|
||||
}
|
||||
```
|
||||
|
||||
### Subcommands (Agents only):
|
||||
By default, if no `@cmd` annotations are defined, the script's `main` function is treated as the default command.
|
||||
However, for agents, there can be many functions defined in one file, and thus it is useful to create subcommands
|
||||
to organize your agent's tools.
|
||||
|
||||
```bash
|
||||
#################
|
||||
## Subcommands ##
|
||||
#################
|
||||
|
||||
# Use the `@cmd` annotation to define subcommands for your script.
|
||||
# @cmd List all files
|
||||
list() {
|
||||
ls -la >> "$LLM_OUTPUT"
|
||||
}
|
||||
|
||||
# @cmd Output the contents of the specified file
|
||||
# @arg file! The file to output
|
||||
cat() {
|
||||
cat "$argc_file" >> "$LLM_OUTPUT"
|
||||
}
|
||||
|
||||
# Example usage 1: ./script.sh cat myfile.txt
|
||||
```
|
||||
|
||||
## Execute and Test Your Bash Tools
|
||||
Your bash tools are just normal bash scripts stored in the `functions/tools` directory. So you can execute and test them
|
||||
directly by first having Loki compile them so all this syntactic sugar means something.
|
||||
|
||||
This is achieved via the `loki --build-tools` command.
|
||||
|
||||
### Example
|
||||
Suppose we want to execute the `functions/tools/get_current_time.sh` script for testing.
|
||||
|
||||
We'd first make sure the script is visible in all contexts by ensuring it's in the `visible_tools` array in your global
|
||||
`config.yaml` file. This ensures Loki builds the tool so it's ready to use in any context.
|
||||
|
||||
You can find the location of your global `config.yaml` file with the following command:
|
||||
|
||||
```shell
|
||||
loki --info | grep 'config_file' | awk '{print $2}'
|
||||
```
|
||||
|
||||
Then, we can instruct Loki to build the script so we can test it out:
|
||||
|
||||
```shell
|
||||
loki --build-tools
|
||||
```
|
||||
|
||||
This will add additional boilerplate to the top of the script so that it can be executed directly.
|
||||
|
||||
Finally, we can now execute the script:
|
||||
|
||||
```bash
|
||||
$ ./get_current_time.sh
|
||||
Fri Oct 24 05:55:04 PM MDT 2025
|
||||
```
|
||||
|
||||
## Prompt Helpers
|
||||
It's often useful to create interactive prompts for our bash tools so that our tools can get input from
|
||||
users.
|
||||
|
||||
To accommodate this, Loki provides a set of prompt helper functions that can be referenced and used within your Bash
|
||||
tools.
|
||||
|
||||
For more information, refer to the [Bash Prompt Helpers documentation](BASH-PROMPT-HELPERS.md).
|
||||
@@ -0,0 +1,282 @@
|
||||
# Custom Tools
|
||||
Loki is designed to be as flexible and as customizable as possible. One of the key
|
||||
features that enables this flexibility is the ability to create and integrate custom tools
|
||||
into your Loki setup. This document provides a guide on how to create and use custom tools within Loki.
|
||||
|
||||
## Quick Links
|
||||
<!--toc:start-->
|
||||
- [Supported Languages](#supported-languages)
|
||||
- [Creating a Custom Tool](#creating-a-custom-tool)
|
||||
- [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-->
|
||||
|
||||
---
|
||||
|
||||
## Supported Languages
|
||||
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, 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:
|
||||
|
||||
```shell
|
||||
loki --info | grep functions_dir | awk '{print $2}'
|
||||
```
|
||||
|
||||
Once you've created your custom tool, remember to add it to the `visible_tools` array in your global `config.yaml` file
|
||||
to enable it globally. See the [Tools](TOOLS.md#enablingdisabling-global-tools) documentation for more information on how Loki utilizes the
|
||||
`visible_tools` array.
|
||||
|
||||
### Environment Variables
|
||||
All tools have access to the following environment variables that provide context about the current execution environment:
|
||||
|
||||
| Variable | Description |
|
||||
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `LLM_OUTPUT` | Indicates where the output of the tool should go. <br>In certain situations, this may be set to a temporary file instead of `/dev/stdout`. |
|
||||
| `LLM_ROOT_DIR` | The root `config_dir` directory for Loki <br>(i.e. `dirname $(loki --info \| grep config_file \| awk '{print $2}')`) |
|
||||
| `LLM_TOOL_NAME` | The name of the tool being executed |
|
||||
| `LLM_TOOL_CACHE_DIR` | A directory specific to the tool for storing cache or temporary files |
|
||||
|
||||
Loki also searches the tools directory on startup for a `.env` file. If found, all tools in `functions/tools/` will have
|
||||
the environment variables defined in the `.env` file available to them.
|
||||
|
||||
### Custom Bash-Based Tools
|
||||
To create a Bash-based tool, refer to the [custom bash tools documentation](CUSTOM-BASH-TOOLS.md).
|
||||
|
||||
### Custom Python-Based Tools
|
||||
Loki supports tools written in Python.
|
||||
|
||||
Each Python-based tool must follow a specific structure in order for Loki to be able to properly compile and
|
||||
execute it:
|
||||
|
||||
* The tool must be a Python script with a `.py` file extension.
|
||||
* The tool must have a `def run` function that serves as the entry point for the tool.
|
||||
* The `run` function must accept parameters that define the inputs for the tool.
|
||||
* Always use type hints to specify the data type of each parameter.
|
||||
* Use `Optional[...]` to indicate optional parameters
|
||||
* The `run` function must return a `str`.
|
||||
* For Python, this 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 also have a docstring that describes the tool and its parameters.
|
||||
* Each parameter in the `run` function should be documented in the docstring using the `Args:` section. They should use the following format:
|
||||
* `<parameter_name>: <description>` Where
|
||||
* `<parameter_name>`: The name of the parameter
|
||||
* `<description>`: The description of the parameter
|
||||
* These are *very* important because these descriptions are what's passed to the LLM as the description of the tool,
|
||||
letting the LLM know what the tool does and how to use it.
|
||||
|
||||
It's important to note that any functions prefixed with `_` are not sent to the LLM, so they will be invisible to the LLM
|
||||
at runtime.
|
||||
|
||||
Below is the [`demo_py.py`](../../assets/functions/tools/demo_py.py) tool definition that comes pre-packaged with
|
||||
Loki and demonstrates how to create a Python-based tool:
|
||||
|
||||
```python
|
||||
import os
|
||||
from typing import List, Literal, Optional
|
||||
|
||||
|
||||
def run(
|
||||
string: str,
|
||||
string_enum: Literal["foo", "bar"],
|
||||
boolean: bool,
|
||||
integer: int,
|
||||
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 all supported Python parameter types and variations.
|
||||
Args:
|
||||
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}
|
||||
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():
|
||||
if key.startswith("LLM_"):
|
||||
output = f"{output}\n{key}: {value}"
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,120 @@
|
||||
# MCP Servers
|
||||
[MCP servers](https://modelcontextprotocol.io/docs/getting-started/intro) are essentially APIs designed specifically for LLMs that work like a remote repository of
|
||||
tools for the model to access and extend its capabilities.
|
||||
|
||||
So think of it like this: Instead of having to write all your own custom tools to interact with different
|
||||
services, those services can expose their functionality through an MCP server.
|
||||
|
||||
Loki has first-class support for MCP servers.
|
||||
|
||||
As mentioned in the [Loki Vault documentation](../VAULT.md), Loki can inject sensitive
|
||||
configuration data into your MCP configuration file to ensure that secrets are not hard-coded.
|
||||
|
||||
## Quick Links
|
||||
<!--toc:start-->
|
||||
- [Important Note](#important-note)
|
||||
- [MCP Server Configuration](#mcp-server-configuration)
|
||||
- [Secret Injection](#secret-injection)
|
||||
- [Default MCP Servers](#default-mcp-servers)
|
||||
- [Loki Configuration](#loki-configuration)
|
||||
- [Global Configuration](#global-configuration)
|
||||
- [Role Configuration](#role-configuration)
|
||||
- [Agent Configuration](#agent-configuration)
|
||||
<!--toc:end-->
|
||||
|
||||
---
|
||||
|
||||
## Important Note
|
||||
Be careful how many MCP servers you enable at one time, regardless of the context. When there is a significant
|
||||
number of configured MCP servers, enabling too many MCP servers may overwhelm the context length of a model,
|
||||
and quickly exceed token limits.
|
||||
|
||||
## MCP Server Configuration
|
||||
Loki stores the MCP server configuration file, `functions/mcp.json`, in the `functions` directory. You can find
|
||||
this directory using the following command:
|
||||
|
||||
```shell
|
||||
loki --info | grep functions_dir | awk '{print $2}'
|
||||
```
|
||||
|
||||
The syntax for the `functions/mcp.json` file is identical to the syntax for MCP server configurations for Claude Desktop.
|
||||
So any time you're looking to add a new server, look at the docs for it and find the configuration example for
|
||||
Claude desktop. You should be able to use the exact same configuration in your `functions/mcp.json` file.
|
||||
|
||||
### Secret Injection
|
||||
As mentioned in the [Loki Vault documentation](../VAULT.md), you can use Loki Vault to inject secrets into your MCP configuration file.
|
||||
|
||||
In fact, this is why you need to set up your vault before using Loki at all: the built-in MCP configuration
|
||||
requires you set up some secrets to use it.
|
||||
|
||||
For more information about how to set up your vault and inject secrets, please refer to the [Loki Vault documentation](../VAULT.md).
|
||||
|
||||
## Default MCP Servers
|
||||
Loki ships with a `functions/mcp.json` file that includes some useful MCP servers:
|
||||
|
||||
* [github](https://github.com/github/github-mcp-server) - Interact with GitHub repositories, issues, pull requests, and more.
|
||||
* [docker](https://github.com/ckreiling/mcp-server-docker) - Manage your local Docker containers with natural language
|
||||
* [slack](https://github.com/korotovsky/slack-mcp-server) - Interact with Slack
|
||||
* [ddg-search](https://github.com/nickclyde/duckduckgo-mcp-server) - Perform web searches with the DuckDuckGo search engine
|
||||
|
||||
## Loki Configuration
|
||||
MCP servers, like tools, can be used in a handful of contexts:
|
||||
* Inside a session
|
||||
* Inside a role
|
||||
* Inside an agent
|
||||
* Globally (i.e. outside a session, role, or agent)
|
||||
|
||||
Each of these has a different configuration and interaction with the global configuration.
|
||||
|
||||
***Note:** The names of each MCP server referenced in the below configuration properties directly corresponds
|
||||
to the names given in the `functions/mcp.json` configuration file. So if you change the name of an MCP server
|
||||
from `slack` to `lucem-slack`, then you need to also update your Loki configuration accordingly.
|
||||
|
||||
### Global Configuration
|
||||
The global configuration is essentially what settings you want to have on by default when
|
||||
you just invoke `loki`. (Don't worry about agents, roles, or sessions yet. We'll get to them in a bit).
|
||||
|
||||
The following settings are available in the global configuration for MCP servers:
|
||||
|
||||
```yaml
|
||||
mcp_server_support: true # Enables or disables MCP server support (globally).
|
||||
mapping_mcp_servers: # Alias for an MCP server or set of servers
|
||||
git: github,gitmcp
|
||||
enabled_mcp_servers: null # Which MCP servers to enable by default (e.g. 'github,slack')
|
||||
```
|
||||
|
||||
A special note about `enabled_mcp_servers`: a user can set this to `all` to enable all configured MCP servers in the
|
||||
`functions/mcp.json` configuration.
|
||||
|
||||
(See the [Configuration Example](../../config.example.yaml) file for an example global configuration with all options.)
|
||||
|
||||
When running in REPL-mode, the `mcp_server_support` and `enabled_mcp_servers` settings can be overridden using the
|
||||
`.set` command:
|
||||
|
||||

|
||||
|
||||
### Role Configuration
|
||||
When you create a role, you have the following MCP-related configuration options available to you:
|
||||
|
||||
```yaml
|
||||
enabled_mcp_servers: github # Which MCP servers the role uses.
|
||||
```
|
||||
|
||||
The values for `mapping_mcp_servers` are inherited from the `[global configuration](#global-configuration)`.
|
||||
|
||||
For more information about roles, refer to the [Roles](../ROLES.md) documentation.
|
||||
|
||||
### Agent Configuration
|
||||
When you create an agent, you have the following MCP-related configuration options available to you:
|
||||
|
||||
```yaml
|
||||
mcp_servers: # Which MCP servers the agent uses
|
||||
- github
|
||||
- docker
|
||||
```
|
||||
|
||||
The values for `mapping_mcp_servers` are inherited from the [global configuration](#global-configuration).
|
||||
|
||||
For more information about agents, refer to the [Agents](../AGENTS.md) documentation.
|
||||
|
||||
For a full example configuration for an agent, see the [Agent Configuration Example](../../config.agent.example.yaml) file.
|
||||
@@ -0,0 +1,192 @@
|
||||
# Tools
|
||||
Loki supports function calling with various tools built-in to enhance LLM capabilities. All built-in tools for Loki
|
||||
are located in the [`functions/tools`](../../assets/functions/tools) directory. These tools are also stored in your Loki `functions`
|
||||
directory, which is also where you'd go to add more tools.
|
||||
|
||||
**Pro Tip:** The Loki functions directory can be found by running the following command:
|
||||
```bash
|
||||
loki --info | grep functions_dir | awk '{print $2}'
|
||||
```
|
||||
|
||||
# Quick Links
|
||||
<!--toc:start-->
|
||||
- [Built-In Tools](#built-in-tools)
|
||||
- [Configuration](#configuration)
|
||||
- [Global Configuration](#global-configuration)
|
||||
- [Enabling/Disabling Global Tools](#enablingdisabling-global-tools)
|
||||
- [Role Configuration](#role-configuration)
|
||||
- [Agent Configuration](#agent-configuration)
|
||||
- [Tool Error Handling](#tool-error-handling)
|
||||
- [Native/Shell Tool Errors](#nativeshell-tool-errors)
|
||||
- [MCP Errors](#mcp-tool-errors)
|
||||
- [Why Tool Error Handling Is Important](#why-this-matters)
|
||||
<!--toc:end-->
|
||||
|
||||
---
|
||||
|
||||
## Built-In Tools
|
||||
The following tools are built-in to Loki by default, and their default enabled/disabled status is indicated. More about how tools can
|
||||
be enabled/disabled can be found in the [Configuration](#configuration) section below.
|
||||
|
||||
| Tool | Description | Enabled/Disabled |
|
||||
|-------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------|
|
||||
| [`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. | 🔴 |
|
||||
| [`fetch_url_via_curl.sh`](../../assets/functions/tools/fetch_url_via_curl.sh) | Extract the content from a given URL using cURL. | 🔴 |
|
||||
| [`fetch_url_via_jina.sh`](../../assets/functions/tools/fetch_url_via_jina.sh) | Extract the content from a given URL using Jina. | 🔴 |
|
||||
| [`fs_cat.sh`](../../assets/functions/tools/fs_cat.sh) | Read the contents of a file at the specified path. | 🟢 |
|
||||
| [`fs_read.sh`](../../assets/functions/tools/fs_read.sh) | Controlled reading of the contents of a file at the specified path with line numbers, offset, and limit to read specific sections. | 🟢 |
|
||||
| [`fs_glob.sh`](../../assets/functions/tools/fs_glob.sh) | Find files by glob pattern. Returns matching file paths sorted by modification time. | 🟢 |
|
||||
| [`fs_grep.sh`](../../assets/functions/tools/fs_grep.sh) | Search file contents using regular expressions. Returns matching file paths and lines. | 🟢 |
|
||||
| [`fs_ls.sh`](../../assets/functions/tools/fs_ls.sh) | List all files and directories at the specified path. | 🟢 |
|
||||
| [`fs_mkdir.sh`](../../assets/functions/tools/fs_mkdir.sh) | Create a new directory at the specified path. | 🔴 |
|
||||
| [`fs_patch.sh`](../../assets/functions/tools/fs_patch.sh) | Apply a patch to a file at the specified path. <br>This can be used to edit a file without having to rewrite the whole file. | 🔴 |
|
||||
| [`fs_rm.sh`](../../assets/functions/tools/fs_rm.sh) | Remove a file or directory at the specified path. | 🔴 |
|
||||
| [`fs_write.sh`](../../assets/functions/tools/fs_write.sh) | Write the full file contents to a file at the specified path. | 🟢 |
|
||||
| [`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. | 🔴 |
|
||||
| [`search_wolframalpha.sh`](../../assets/functions/tools/search_wolframalpha.sh) | Get an answer to a question using Wolfram Alpha. The input query should be <br>in English. Use it to answer user questions that require computation, detailed <br>facts, data analysis, or complex queries. | 🔴 |
|
||||
| [`send_mail.sh`](../../assets/functions/tools/send_mail.sh) | Send an email. | 🔴 |
|
||||
| [`send_twilio.sh`](../../assets/functions/tools/send_twilio.sh) | Send SMS or Twilio Messaging Channels messages using the Twilio API. | 🔴 |
|
||||
| [`web_search_loki.sh`](../../assets/functions/tools/web_search_loki.sh) | Perform a web search to get up-to-date information or additional context. <br>Use this when you need current information or feel a search could provide <br>a better answer. | 🔴 |
|
||||
| [`web_search_perplexity.sh`](../../assets/functions/tools/web_search_perplexity.sh) | Perform a web search using the Perplexity API to get up-to-date <br>information or additional context. Use this when you need current <br>information or feel a search could provide a better answer. | 🔴 |
|
||||
| [`web_search_tavily.sh`](../../assets/functions/tools/web_search_tavily.sh) | Perform a web search using the Tavily API to get up-to-date <br>information or additional context. Use this when you need current <br>information or feel a search could provide a better answer. | 🔴 |
|
||||
|
||||
Details on what configuration, if any, is necessary for each tool can be found inside the tool file definition itself.
|
||||
|
||||
## Configuration
|
||||
Tools can be used in a handful of contexts:
|
||||
* Inside a session
|
||||
* Inside a role
|
||||
* Inside an agent
|
||||
* Globally (i.e. outside a session, role, or agent)
|
||||
|
||||
Each of these has a different configuration and interaction with the global configuration.
|
||||
|
||||
**Note:** For each configuration property listed below, the functions that are mentioned *only*
|
||||
correspond to the tool scripts located in your Loki `functions/tools` directory.
|
||||
|
||||
### Global Configuration
|
||||
The global configuration is essentially what settings you want to have on by default when
|
||||
you just invoke `loki`. (Don't worry about agents, roles, or sessions yet. We'll get to them in a bit).
|
||||
|
||||
The following settings are available in the global configuration for tools:
|
||||
|
||||
```yaml
|
||||
function_calling_support: true # Enables or disables function calling in any context
|
||||
mapping_tools: # Alias for a tool or toolset
|
||||
fs: 'fs_cat,fs_ls,fs_mkdir,fs_rm,fs_write'
|
||||
enabled_tools: null # Which tools to use by default. (e.g. 'fs,web_search_loki')
|
||||
visible_tools: # Which tools are visible to be compiled (and are thus able to be defined in 'enabled_tools')
|
||||
# - demo_py.py
|
||||
- execute_command.sh
|
||||
```
|
||||
|
||||
A special not about `enabled_tools`: a user can set this to `all` to enable all available tools listed in the
|
||||
`visible_tools` section of your Loki `config.yaml` file.
|
||||
See the [Enabling/Disabling Global Tools](#enablingdisabling-global-tools) section below for more information on how tools
|
||||
are globally enabled/disabled globally.
|
||||
|
||||
(See the [Configuration Example](../../config.example.yaml) file for an example global configuration with all options.)
|
||||
|
||||
When running in REPL-mode, the `function_calling_support` and `enabled_tools` settings can be overridden using the
|
||||
`.set` command:
|
||||
|
||||

|
||||
|
||||
You'll notice that mentioned above, some tools are disabled while others are enabled. How is that determined?
|
||||
|
||||
### Enabling/Disabling Global Tools
|
||||
The configured tools are enabled/disabled by looking at the values in the `visible_tools` array in your `config.yaml`
|
||||
file. This file is located in the root of the Loki `config` directory. The location of the Loki config varies by system,
|
||||
so your config file can be found using the following command:
|
||||
|
||||
```bash
|
||||
loki --info | grep 'config_file' | awk '{print $2}'
|
||||
```
|
||||
|
||||
Each line in the `visible_tools` array lists a tool.
|
||||
|
||||
If that line is commented out, then that tool is not included in the global tool set, and cannot be used in any context;
|
||||
This means it will not be built, and even if enabled under `enabled_tools`, it still will not be available in any
|
||||
context.
|
||||
|
||||
### Role Configuration
|
||||
When you create a role, you have the following global tool-related configuration options available to you:
|
||||
|
||||
```yaml
|
||||
enabled_tools: query_jira_issues # Which tools the role uses.
|
||||
```
|
||||
|
||||
The values for `mapping_tools` are inherited from the [global configuration](#global-configuration).
|
||||
|
||||
For more information about roles, refer to the [Roles](../ROLES.md) documentation.
|
||||
|
||||
### Agent Configuration
|
||||
When you create an agent, you have the following global tool-related configuration options available to you:
|
||||
|
||||
```yaml
|
||||
global_tools: # Which global tools the agent uses
|
||||
- query_jira_issues.sh
|
||||
- fs_cat.sh
|
||||
- fs_ls.sh
|
||||
```
|
||||
|
||||
The values for `mapping_tools` are inherited from the [global configuration](#global-configuration).
|
||||
|
||||
For more information about agents, refer to the [Agents](../AGENTS.md) documentation.
|
||||
|
||||
For a full example configuration for an agent, see the [Agent Configuration Example](../../config.agent.example.yaml) file.
|
||||
|
||||
---
|
||||
|
||||
## Tool Error Handling
|
||||
When tools fail, Loki captures error information and passes it back to the model so it can diagnose issues and
|
||||
potentially retry or adjust its approach.
|
||||
|
||||
### Native/Shell Tool Errors
|
||||
When a shell-based tool exits with a non-zero exit code, the model receives:
|
||||
|
||||
```json
|
||||
{
|
||||
"tool_call_error": "Tool call 'my_tool' exited with code 1",
|
||||
"stderr": "Error: file not found: config.json"
|
||||
}
|
||||
```
|
||||
|
||||
The `stderr` field contains the actual error output from the tool, giving the model context about what went wrong.
|
||||
If the tool produces no stderr output, only the `tool_call_error` field is included.
|
||||
|
||||
**Note:** Tool stdout streams to your terminal in real-time so you can see progress. Only stderr is captured for
|
||||
error reporting.
|
||||
|
||||
### MCP Tool Errors
|
||||
When an MCP (Model Context Protocol) tool invocation fails due to connection issues, timeouts, or server errors,
|
||||
the model receives:
|
||||
|
||||
```json
|
||||
{
|
||||
"tool_call_error": "MCP tool invocation failed: connection refused"
|
||||
}
|
||||
```
|
||||
|
||||
This allows the model to understand that an external service failed and take appropriate action (retry, use an
|
||||
alternative approach, or inform the user).
|
||||
|
||||
### Why This Matters
|
||||
Without proper error propagation, models would only know that "something went wrong" without understanding *what*
|
||||
went wrong. By including stderr output and detailed error messages, models can:
|
||||
|
||||
- Diagnose the root cause of failures
|
||||
- Suggest fixes (e.g., "the file doesn't exist, should I create it?")
|
||||
- Retry with corrected parameters
|
||||
- Fall back to alternative approaches when appropriate
|
||||
|
After Width: | Height: | Size: 370 KiB |
|
After Width: | Height: | Size: 587 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 446 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 878 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 303 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 396 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
@@ -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}"
|
||||
@@ -1,7 +1,5 @@
|
||||
use crate::client::{ModelType, list_models};
|
||||
use crate::config::paths;
|
||||
use crate::config::{AppConfig, Config, list_agents, list_sessions};
|
||||
use crate::vault::Vault;
|
||||
use crate::config::{Config, list_agents};
|
||||
use clap_complete::{CompletionCandidate, Shell, generate};
|
||||
use clap_complete_nushell::Nushell;
|
||||
use std::ffi::OsStr;
|
||||
@@ -34,8 +32,8 @@ impl ShellCompletion {
|
||||
|
||||
pub(super) fn model_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
let cur = current.to_string_lossy();
|
||||
match load_app_config_for_completion() {
|
||||
Ok(app_config) => list_models(&app_config, ModelType::Chat)
|
||||
match Config::init_bare() {
|
||||
Ok(config) => list_models(&config, ModelType::Chat)
|
||||
.into_iter()
|
||||
.filter(|&m| m.id().starts_with(&*cur))
|
||||
.map(|m| CompletionCandidate::new(m.id()))
|
||||
@@ -44,23 +42,9 @@ pub(super) fn model_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
}
|
||||
}
|
||||
|
||||
fn load_app_config_for_completion() -> anyhow::Result<AppConfig> {
|
||||
let h = tokio::runtime::Handle::try_current().ok();
|
||||
let cfg = match h {
|
||||
Some(handle) => {
|
||||
tokio::task::block_in_place(|| handle.block_on(Config::load_with_interpolation(true)))?
|
||||
}
|
||||
None => {
|
||||
let rt = tokio::runtime::Runtime::new()?;
|
||||
rt.block_on(Config::load_with_interpolation(true))?
|
||||
}
|
||||
};
|
||||
AppConfig::from_config(cfg)
|
||||
}
|
||||
|
||||
pub(super) fn role_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
let cur = current.to_string_lossy();
|
||||
paths::list_roles(true)
|
||||
Config::list_roles(true)
|
||||
.into_iter()
|
||||
.filter(|r| r.starts_with(&*cur))
|
||||
.map(CompletionCandidate::new)
|
||||
@@ -78,7 +62,7 @@ pub(super) fn agent_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
|
||||
pub(super) fn rag_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
let cur = current.to_string_lossy();
|
||||
paths::list_rags()
|
||||
Config::list_rags()
|
||||
.into_iter()
|
||||
.filter(|r| r.starts_with(&*cur))
|
||||
.map(CompletionCandidate::new)
|
||||
@@ -87,7 +71,7 @@ pub(super) fn rag_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
|
||||
pub(super) fn macro_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
let cur = current.to_string_lossy();
|
||||
paths::list_macros()
|
||||
Config::list_macros()
|
||||
.into_iter()
|
||||
.filter(|m| m.starts_with(&*cur))
|
||||
.map(CompletionCandidate::new)
|
||||
@@ -96,17 +80,22 @@ pub(super) fn macro_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
|
||||
pub(super) fn session_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
let cur = current.to_string_lossy();
|
||||
list_sessions()
|
||||
.into_iter()
|
||||
.filter(|s| s.starts_with(&*cur))
|
||||
.map(CompletionCandidate::new)
|
||||
.collect()
|
||||
match Config::init_bare() {
|
||||
Ok(config) => config
|
||||
.list_sessions()
|
||||
.into_iter()
|
||||
.filter(|s| s.starts_with(&*cur))
|
||||
.map(CompletionCandidate::new)
|
||||
.collect(),
|
||||
Err(_) => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn secrets_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
let cur = current.to_string_lossy();
|
||||
match load_app_config_for_completion() {
|
||||
Ok(app_config) => Vault::init(&app_config)
|
||||
match Config::init_bare() {
|
||||
Ok(config) => config
|
||||
.vault
|
||||
.list_secrets(false)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
|
||||
@@ -176,220 +176,3 @@ impl Cli {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use clap::Parser;
|
||||
|
||||
fn parse(args: &[&str]) -> Cli {
|
||||
let mut full_args = vec!["loki"];
|
||||
full_args.extend_from_slice(args);
|
||||
Cli::try_parse_from(full_args).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_no_args_defaults() {
|
||||
let cli = parse(&[]);
|
||||
assert!(cli.model.is_none());
|
||||
assert!(cli.role.is_none());
|
||||
assert!(cli.session.is_none());
|
||||
assert!(cli.agent.is_none());
|
||||
assert!(!cli.execute);
|
||||
assert!(!cli.code);
|
||||
assert!(!cli.no_stream);
|
||||
assert!(!cli.dry_run);
|
||||
assert!(!cli.info);
|
||||
assert!(!cli.build_tools);
|
||||
assert!(cli.file.is_empty());
|
||||
assert!(cli.text.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_model_flag() {
|
||||
let cli = parse(&["--model", "gpt-4o"]);
|
||||
assert_eq!(cli.model, Some("gpt-4o".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_model_short_flag() {
|
||||
let cli = parse(&["-m", "gpt-4o"]);
|
||||
assert_eq!(cli.model, Some("gpt-4o".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_role_flag() {
|
||||
let cli = parse(&["--role", "coder"]);
|
||||
assert_eq!(cli.role, Some("coder".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_session_with_name() {
|
||||
let cli = parse(&["--session", "my-session"]);
|
||||
assert_eq!(cli.session, Some(Some("my-session".to_string())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_agent_flag() {
|
||||
let cli = parse(&["--agent", "sisyphus"]);
|
||||
assert_eq!(cli.agent, Some("sisyphus".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_agent_short_flag() {
|
||||
let cli = parse(&["-a", "sisyphus"]);
|
||||
assert_eq!(cli.agent, Some("sisyphus".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_execute_flag() {
|
||||
let cli = parse(&["-e", "list files"]);
|
||||
assert!(cli.execute);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_code_flag() {
|
||||
let cli = parse(&["-c", "hello world"]);
|
||||
assert!(cli.code);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_no_stream_flag() {
|
||||
let cli = parse(&["-S", "test"]);
|
||||
assert!(cli.no_stream);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_dry_run_flag() {
|
||||
let cli = parse(&["--dry-run", "test"]);
|
||||
assert!(cli.dry_run);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_info_flag() {
|
||||
let cli = parse(&["--info"]);
|
||||
assert!(cli.info);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_list_flags() {
|
||||
assert!(parse(&["--list-models"]).list_models);
|
||||
assert!(parse(&["--list-roles"]).list_roles);
|
||||
assert!(parse(&["--list-sessions"]).list_sessions);
|
||||
assert!(parse(&["--list-agents"]).list_agents);
|
||||
assert!(parse(&["--list-rags"]).list_rags);
|
||||
assert!(parse(&["--list-macros"]).list_macros);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_file_flag_single() {
|
||||
let cli = parse(&["-f", "file.txt", "question"]);
|
||||
assert_eq!(cli.file, vec!["file.txt"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_file_flag_multiple() {
|
||||
let cli = parse(&["-f", "a.txt", "-f", "b.txt", "question"]);
|
||||
assert_eq!(cli.file, vec!["a.txt", "b.txt"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_trailing_text() {
|
||||
let cli = parse(&["hello", "world"]);
|
||||
assert_eq!(cli.text, vec!["hello", "world"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_prompt_flag() {
|
||||
let cli = parse(&["--prompt", "be a pirate"]);
|
||||
assert_eq!(cli.prompt, Some("be a pirate".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_empty_session_flag() {
|
||||
let cli = parse(&["--session", "s", "--empty-session"]);
|
||||
assert!(cli.empty_session);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_save_session_flag() {
|
||||
let cli = parse(&["--session", "s", "--save-session"]);
|
||||
assert!(cli.save_session);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_build_tools_flag() {
|
||||
let cli = parse(&["--build-tools"]);
|
||||
assert!(cli.build_tools);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_sync_models_flag() {
|
||||
let cli = parse(&["--sync-models"]);
|
||||
assert!(cli.sync_models);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_model_with_role() {
|
||||
let cli = parse(&["-m", "gpt-4o", "-r", "coder"]);
|
||||
assert_eq!(cli.model, Some("gpt-4o".to_string()));
|
||||
assert_eq!(cli.role, Some("coder".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_agent_with_file_and_text() {
|
||||
let cli = parse(&["-a", "sisyphus", "-f", "code.rs", "explain", "this"]);
|
||||
assert_eq!(cli.agent, Some("sisyphus".to_string()));
|
||||
assert_eq!(cli.file, vec!["code.rs"]);
|
||||
assert_eq!(cli.text, vec!["explain", "this"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_role_with_session() {
|
||||
let cli = parse(&["-r", "coder", "-s", "dev-session"]);
|
||||
assert_eq!(cli.role, Some("coder".to_string()));
|
||||
assert_eq!(cli.session, Some(Some("dev-session".to_string())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_text_returns_none_when_no_text_no_stdin() {
|
||||
let cli = parse(&[]);
|
||||
assert!(cli.text().unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_text_joins_trailing_args() {
|
||||
let cli = parse(&["hello", "world"]);
|
||||
assert_eq!(cli.text().unwrap(), Some("hello world".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_add_secret_flag() {
|
||||
let cli = parse(&["--add-secret", "MY_KEY"]);
|
||||
assert_eq!(cli.add_secret, Some("MY_KEY".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_get_secret_flag() {
|
||||
let cli = parse(&["--get-secret", "MY_KEY"]);
|
||||
assert_eq!(cli.get_secret, Some("MY_KEY".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_list_secrets_flag() {
|
||||
let cli = parse(&["--list-secrets"]);
|
||||
assert!(cli.list_secrets);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rag_flag() {
|
||||
let cli = parse(&["--rag", "my-rag"]);
|
||||
assert_eq!(cli.rag, Some("my-rag".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_macro_flag() {
|
||||
let cli = parse(&["--macro", "my-macro"]);
|
||||
assert_eq!(cli.macro_name, Some("my-macro".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use super::*;
|
||||
|
||||
use crate::config::paths;
|
||||
use crate::{
|
||||
config::{AppConfig, Input, RequestContext},
|
||||
config::{Config, GlobalConfig, Input},
|
||||
function::{FunctionDeclaration, ToolCall, ToolResult, eval_tool_calls},
|
||||
render::render_stream,
|
||||
utils::*,
|
||||
@@ -25,7 +24,7 @@ use tokio::sync::mpsc::unbounded_channel;
|
||||
pub const MODELS_YAML: &str = include_str!("../../models.yaml");
|
||||
|
||||
pub static ALL_PROVIDER_MODELS: LazyLock<Vec<ProviderModels>> = LazyLock::new(|| {
|
||||
paths::local_models_override()
|
||||
Config::local_models_override()
|
||||
.ok()
|
||||
.unwrap_or_else(|| serde_yaml::from_str(MODELS_YAML).unwrap())
|
||||
});
|
||||
@@ -38,7 +37,7 @@ static ESCAPE_SLASH_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?<!\\)/
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait Client: Sync + Send {
|
||||
fn app_config(&self) -> &AppConfig;
|
||||
fn global_config(&self) -> &GlobalConfig;
|
||||
|
||||
fn extra_config(&self) -> Option<&ExtraConfig>;
|
||||
|
||||
@@ -59,7 +58,7 @@ pub trait Client: Sync + Send {
|
||||
if let Some(proxy) = extra.and_then(|v| v.proxy.as_deref()) {
|
||||
builder = set_proxy(builder, proxy)?;
|
||||
}
|
||||
if let Some(user_agent) = self.app_config().user_agent.as_ref() {
|
||||
if let Some(user_agent) = self.global_config().read().user_agent.as_ref() {
|
||||
builder = builder.user_agent(user_agent);
|
||||
}
|
||||
let client = builder
|
||||
@@ -70,7 +69,7 @@ pub trait Client: Sync + Send {
|
||||
}
|
||||
|
||||
async fn chat_completions(&self, input: Input) -> Result<ChatCompletionsOutput> {
|
||||
if self.app_config().dry_run {
|
||||
if self.global_config().read().dry_run {
|
||||
let content = input.echo_messages();
|
||||
return Ok(ChatCompletionsOutput::new(&content));
|
||||
}
|
||||
@@ -90,7 +89,7 @@ pub trait Client: Sync + Send {
|
||||
let input = input.clone();
|
||||
tokio::select! {
|
||||
ret = async {
|
||||
if self.app_config().dry_run {
|
||||
if self.global_config().read().dry_run {
|
||||
let content = input.echo_messages();
|
||||
handler.text(&content)?;
|
||||
return Ok(());
|
||||
@@ -414,10 +413,9 @@ pub async fn call_chat_completions(
|
||||
print: bool,
|
||||
extract_code: bool,
|
||||
client: &dyn Client,
|
||||
ctx: &mut RequestContext,
|
||||
abort_signal: AbortSignal,
|
||||
) -> Result<(String, Vec<ToolResult>)> {
|
||||
let is_child_agent = ctx.current_depth > 0;
|
||||
let is_child_agent = client.global_config().read().current_depth > 0;
|
||||
let spinner_message = if is_child_agent { "" } else { "Generating" };
|
||||
let ret = abortable_run_with_spinner(
|
||||
client.chat_completions(input.clone()),
|
||||
@@ -438,13 +436,15 @@ pub async fn call_chat_completions(
|
||||
text = extract_code_block(&strip_think_tag(&text)).to_string();
|
||||
}
|
||||
if print {
|
||||
ctx.app.config.print_markdown(&text)?;
|
||||
client.global_config().read().print_markdown(&text)?;
|
||||
}
|
||||
}
|
||||
let tool_results = eval_tool_calls(ctx, tool_calls).await?;
|
||||
tool_results
|
||||
.iter()
|
||||
.for_each(|res| ctx.tool_scope.tool_tracker.record_call(res.call.clone()));
|
||||
let tool_results = eval_tool_calls(client.global_config(), tool_calls).await?;
|
||||
if let Some(tracker) = client.global_config().write().tool_call_tracker.as_mut() {
|
||||
tool_results
|
||||
.iter()
|
||||
.for_each(|res| tracker.record_call(res.call.clone()));
|
||||
}
|
||||
Ok((text, tool_results))
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
@@ -454,7 +454,6 @@ pub async fn call_chat_completions(
|
||||
pub async fn call_chat_completions_streaming(
|
||||
input: &Input,
|
||||
client: &dyn Client,
|
||||
ctx: &mut RequestContext,
|
||||
abort_signal: AbortSignal,
|
||||
) -> Result<(String, Vec<ToolResult>)> {
|
||||
let (tx, rx) = unbounded_channel();
|
||||
@@ -462,7 +461,7 @@ pub async fn call_chat_completions_streaming(
|
||||
|
||||
let (send_ret, render_ret) = tokio::join!(
|
||||
client.chat_completions_streaming(input, &mut handler),
|
||||
render_stream(rx, client.app_config(), abort_signal.clone()),
|
||||
render_stream(rx, client.global_config(), abort_signal.clone()),
|
||||
);
|
||||
|
||||
if handler.abort().aborted() {
|
||||
@@ -477,10 +476,12 @@ pub async fn call_chat_completions_streaming(
|
||||
if !text.is_empty() && !text.ends_with('\n') {
|
||||
println!();
|
||||
}
|
||||
let tool_results = eval_tool_calls(ctx, tool_calls).await?;
|
||||
tool_results
|
||||
.iter()
|
||||
.for_each(|res| ctx.tool_scope.tool_tracker.record_call(res.call.clone()));
|
||||
let tool_results = eval_tool_calls(client.global_config(), tool_calls).await?;
|
||||
if let Some(tracker) = client.global_config().write().tool_call_tracker.as_mut() {
|
||||
tool_results
|
||||
.iter()
|
||||
.for_each(|res| tracker.record_call(res.call.clone()));
|
||||
}
|
||||
Ok((text, tool_results))
|
||||
}
|
||||
Err(err) => {
|
||||
|
||||
@@ -24,7 +24,7 @@ macro_rules! register_client {
|
||||
$(
|
||||
#[derive(Debug)]
|
||||
pub struct $client {
|
||||
app_config: std::sync::Arc<$crate::config::AppConfig>,
|
||||
global_config: $crate::config::GlobalConfig,
|
||||
config: $config,
|
||||
model: $crate::client::Model,
|
||||
}
|
||||
@@ -32,8 +32,8 @@ macro_rules! register_client {
|
||||
impl $client {
|
||||
pub const NAME: &'static str = $name;
|
||||
|
||||
pub fn init(app_config: &std::sync::Arc<$crate::config::AppConfig>, model: &$crate::client::Model) -> Option<Box<dyn Client>> {
|
||||
let config = app_config.clients.iter().find_map(|client_config| {
|
||||
pub fn init(global_config: &$crate::config::GlobalConfig, model: &$crate::client::Model) -> Option<Box<dyn Client>> {
|
||||
let config = global_config.read().clients.iter().find_map(|client_config| {
|
||||
if let ClientConfig::$config(c) = client_config {
|
||||
if Self::name(c) == model.client_name() {
|
||||
return Some(c.clone())
|
||||
@@ -43,7 +43,7 @@ macro_rules! register_client {
|
||||
})?;
|
||||
|
||||
Some(Box::new(Self {
|
||||
app_config: std::sync::Arc::clone(app_config),
|
||||
global_config: global_config.clone(),
|
||||
config,
|
||||
model: model.clone(),
|
||||
}))
|
||||
@@ -72,9 +72,10 @@ macro_rules! register_client {
|
||||
|
||||
)+
|
||||
|
||||
pub fn init_client(app_config: &std::sync::Arc<$crate::config::AppConfig>, model: $crate::client::Model) -> anyhow::Result<Box<dyn Client>> {
|
||||
pub fn init_client(config: &$crate::config::GlobalConfig, model: Option<$crate::client::Model>) -> anyhow::Result<Box<dyn Client>> {
|
||||
let model = model.unwrap_or_else(|| config.read().model.clone());
|
||||
None
|
||||
$(.or_else(|| $client::init(app_config, &model)))+
|
||||
$(.or_else(|| $client::init(config, &model)))+
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("Invalid model '{}'", model.id())
|
||||
})
|
||||
@@ -100,7 +101,7 @@ macro_rules! register_client {
|
||||
|
||||
static ALL_CLIENT_NAMES: std::sync::OnceLock<Vec<String>> = std::sync::OnceLock::new();
|
||||
|
||||
pub fn list_client_names(config: &$crate::config::AppConfig) -> Vec<&'static String> {
|
||||
pub fn list_client_names(config: &$crate::config::Config) -> Vec<&'static String> {
|
||||
let names = ALL_CLIENT_NAMES.get_or_init(|| {
|
||||
config
|
||||
.clients
|
||||
@@ -116,7 +117,7 @@ macro_rules! register_client {
|
||||
|
||||
static ALL_MODELS: std::sync::OnceLock<Vec<$crate::client::Model>> = std::sync::OnceLock::new();
|
||||
|
||||
pub fn list_all_models(config: &$crate::config::AppConfig) -> Vec<&'static $crate::client::Model> {
|
||||
pub fn list_all_models(config: &$crate::config::Config) -> Vec<&'static $crate::client::Model> {
|
||||
let models = ALL_MODELS.get_or_init(|| {
|
||||
config
|
||||
.clients
|
||||
@@ -130,7 +131,7 @@ macro_rules! register_client {
|
||||
models.iter().collect()
|
||||
}
|
||||
|
||||
pub fn list_models(config: &$crate::config::AppConfig, model_type: $crate::client::ModelType) -> Vec<&'static $crate::client::Model> {
|
||||
pub fn list_models(config: &$crate::config::Config, model_type: $crate::client::ModelType) -> Vec<&'static $crate::client::Model> {
|
||||
list_all_models(config).into_iter().filter(|v| v.model_type() == model_type).collect()
|
||||
}
|
||||
};
|
||||
@@ -139,8 +140,8 @@ macro_rules! register_client {
|
||||
#[macro_export]
|
||||
macro_rules! client_common_fns {
|
||||
() => {
|
||||
fn app_config(&self) -> &$crate::config::AppConfig {
|
||||
&self.app_config
|
||||
fn global_config(&self) -> &$crate::config::GlobalConfig {
|
||||
&self.global_config
|
||||
}
|
||||
|
||||
fn extra_config(&self) -> Option<&$crate::client::ExtraConfig> {
|
||||
|
||||
@@ -3,7 +3,7 @@ use super::{
|
||||
message::{Message, MessageContent, MessageContentPart},
|
||||
};
|
||||
|
||||
use crate::config::AppConfig;
|
||||
use crate::config::Config;
|
||||
use crate::utils::{estimate_token_length, strip_think_tag};
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
@@ -44,11 +44,7 @@ impl Model {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn retrieve_model(
|
||||
config: &AppConfig,
|
||||
model_id: &str,
|
||||
model_type: ModelType,
|
||||
) -> Result<Self> {
|
||||
pub fn retrieve_model(config: &Config, model_id: &str, model_type: ModelType) -> Result<Self> {
|
||||
let models = list_all_models(config);
|
||||
let (client_name, model_name) = match model_id.split_once(':') {
|
||||
Some((client_name, model_name)) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::ClientConfig;
|
||||
use super::access_token::{is_valid_access_token, set_access_token};
|
||||
use crate::config::paths;
|
||||
use crate::config::Config;
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
@@ -178,13 +178,13 @@ pub async fn run_oauth_flow(provider: &dyn OAuthProvider, client_name: &str) ->
|
||||
}
|
||||
|
||||
pub fn load_oauth_tokens(client_name: &str) -> Option<OAuthTokens> {
|
||||
let path = paths::token_file(client_name);
|
||||
let path = Config::token_file(client_name);
|
||||
let content = fs::read_to_string(path).ok()?;
|
||||
serde_json::from_str(&content).ok()
|
||||
}
|
||||
|
||||
fn save_oauth_tokens(client_name: &str, tokens: &OAuthTokens) -> Result<()> {
|
||||
let path = paths::token_file(client_name);
|
||||
let path = Config::token_file(client_name);
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
@@ -311,8 +311,10 @@ impl JsonStreamParser {
|
||||
}
|
||||
self.balances.push(ch);
|
||||
}
|
||||
'[' if self.start.is_some() => {
|
||||
self.balances.push(ch);
|
||||
'[' => {
|
||||
if self.start.is_some() {
|
||||
self.balances.push(ch);
|
||||
}
|
||||
}
|
||||
'}' => {
|
||||
self.balances.pop();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::todo::TodoList;
|
||||
use super::*;
|
||||
|
||||
use crate::{
|
||||
@@ -5,8 +6,6 @@ use crate::{
|
||||
function::{Functions, run_llm_function},
|
||||
};
|
||||
|
||||
use super::rag_cache::RagKey;
|
||||
use crate::config::paths;
|
||||
use crate::config::prompts::{
|
||||
DEFAULT_SPAWN_INSTRUCTIONS, DEFAULT_TEAMMATE_INSTRUCTIONS, DEFAULT_TODO_INSTRUCTIONS,
|
||||
DEFAULT_USER_INTERACTION_INSTRUCTIONS,
|
||||
@@ -39,13 +38,16 @@ pub struct Agent {
|
||||
rag: Option<Arc<Rag>>,
|
||||
model: Model,
|
||||
vault: GlobalVault,
|
||||
todo_list: TodoList,
|
||||
continuation_count: usize,
|
||||
last_continuation_response: Option<String>,
|
||||
}
|
||||
|
||||
impl Agent {
|
||||
pub fn install_builtin_agents() -> Result<()> {
|
||||
info!(
|
||||
"Installing built-in agents in {}",
|
||||
paths::agents_data_dir().display()
|
||||
Config::agents_data_dir().display()
|
||||
);
|
||||
|
||||
for file in AgentAssets::iter() {
|
||||
@@ -54,7 +56,7 @@ impl Agent {
|
||||
let embedded_file = AgentAssets::get(&file)
|
||||
.ok_or_else(|| anyhow!("Failed to load embedded agent file: {}", file.as_ref()))?;
|
||||
let content = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
|
||||
let file_path = paths::agents_data_dir().join(file.as_ref());
|
||||
let file_path = Config::agents_data_dir().join(file.as_ref());
|
||||
let file_extension = file_path
|
||||
.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
@@ -86,17 +88,14 @@ impl Agent {
|
||||
}
|
||||
|
||||
pub async fn init(
|
||||
app: &AppConfig,
|
||||
app_state: &AppState,
|
||||
current_model: &Model,
|
||||
info_flag: bool,
|
||||
config: &GlobalConfig,
|
||||
name: &str,
|
||||
abort_signal: AbortSignal,
|
||||
) -> Result<Self> {
|
||||
let agent_data_dir = paths::agent_data_dir(name);
|
||||
let loaders = app.document_loaders.clone();
|
||||
let rag_path = paths::agent_rag_file(name, DEFAULT_AGENT_NAME);
|
||||
let config_path = paths::agent_config_file(name);
|
||||
let agent_data_dir = Config::agent_data_dir(name);
|
||||
let loaders = config.read().document_loaders.clone();
|
||||
let rag_path = Config::agent_rag_file(name, DEFAULT_AGENT_NAME);
|
||||
let config_path = Config::agent_config_file(name);
|
||||
let mut agent_config = if config_path.exists() {
|
||||
AgentConfig::load(&config_path)?
|
||||
} else {
|
||||
@@ -104,33 +103,57 @@ impl Agent {
|
||||
};
|
||||
let mut functions = Functions::init_agent(name, &agent_config.global_tools)?;
|
||||
|
||||
agent_config.load_envs(app);
|
||||
config.write().functions.clear_mcp_meta_functions();
|
||||
let mcp_servers = if config.read().mcp_server_support {
|
||||
(!agent_config.mcp_servers.is_empty()).then(|| agent_config.mcp_servers.join(","))
|
||||
} else {
|
||||
eprintln!(
|
||||
"{}",
|
||||
formatdoc!(
|
||||
"
|
||||
This agent uses MCP servers, but MCP support is disabled.
|
||||
To enable it, exit the agent and set 'mcp_server_support: true', then try again
|
||||
"
|
||||
)
|
||||
);
|
||||
None
|
||||
};
|
||||
|
||||
let model = match agent_config.model_id.as_ref() {
|
||||
Some(model_id) => Model::retrieve_model(app, model_id, ModelType::Chat)?,
|
||||
None => {
|
||||
if agent_config.temperature.is_none() {
|
||||
agent_config.temperature = app.temperature;
|
||||
let registry = config
|
||||
.write()
|
||||
.mcp_registry
|
||||
.take()
|
||||
.with_context(|| "MCP registry should be populated")?;
|
||||
let new_mcp_registry =
|
||||
McpRegistry::reinit(registry, mcp_servers, abort_signal.clone()).await?;
|
||||
|
||||
if !new_mcp_registry.is_empty() {
|
||||
functions.append_mcp_meta_functions(new_mcp_registry.list_started_servers());
|
||||
}
|
||||
|
||||
config.write().mcp_registry = Some(new_mcp_registry);
|
||||
|
||||
agent_config.load_envs(&config.read());
|
||||
|
||||
let model = {
|
||||
let config = config.read();
|
||||
match agent_config.model_id.as_ref() {
|
||||
Some(model_id) => Model::retrieve_model(&config, model_id, ModelType::Chat)?,
|
||||
None => {
|
||||
if agent_config.temperature.is_none() {
|
||||
agent_config.temperature = config.temperature;
|
||||
}
|
||||
if agent_config.top_p.is_none() {
|
||||
agent_config.top_p = config.top_p;
|
||||
}
|
||||
config.current_model().clone()
|
||||
}
|
||||
if agent_config.top_p.is_none() {
|
||||
agent_config.top_p = app.top_p;
|
||||
}
|
||||
current_model.clone()
|
||||
}
|
||||
};
|
||||
|
||||
let rag = if rag_path.exists() {
|
||||
let key = RagKey::Agent(name.to_string());
|
||||
let app_clone = app.clone();
|
||||
let rag_path_clone = rag_path.clone();
|
||||
let rag = app_state
|
||||
.rag_cache
|
||||
.load_with(key, || async move {
|
||||
Rag::load(&app_clone, DEFAULT_AGENT_NAME, &rag_path_clone)
|
||||
})
|
||||
.await?;
|
||||
Some(rag)
|
||||
} else if !agent_config.documents.is_empty() && !info_flag {
|
||||
Some(Arc::new(Rag::load(config, DEFAULT_AGENT_NAME, &rag_path)?))
|
||||
} else if !agent_config.documents.is_empty() && !config.read().info_flag {
|
||||
let mut ans = false;
|
||||
if *IS_STDOUT_TERMINAL {
|
||||
ans = Confirm::new("The agent has documents attached, init RAG?")
|
||||
@@ -162,23 +185,9 @@ impl Agent {
|
||||
document_paths.push(path.to_string())
|
||||
}
|
||||
}
|
||||
let key = RagKey::Agent(name.to_string());
|
||||
let app_clone = app.clone();
|
||||
let rag_path_clone = rag_path.clone();
|
||||
let rag = app_state
|
||||
.rag_cache
|
||||
.load_with(key, || async move {
|
||||
Rag::init(
|
||||
&app_clone,
|
||||
"rag",
|
||||
&rag_path_clone,
|
||||
&document_paths,
|
||||
abort_signal,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await?;
|
||||
Some(rag)
|
||||
let rag =
|
||||
Rag::init(config, "rag", &rag_path, &document_paths, abort_signal).await?;
|
||||
Some(Arc::new(rag))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -209,7 +218,10 @@ impl Agent {
|
||||
functions,
|
||||
rag,
|
||||
model,
|
||||
vault: app_state.vault.clone(),
|
||||
vault: Arc::clone(&config.read().vault),
|
||||
todo_list: TodoList::default(),
|
||||
continuation_count: 0,
|
||||
last_continuation_response: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -283,11 +295,11 @@ impl Agent {
|
||||
let mut config = self.config.clone();
|
||||
config.instructions = self.interpolated_instructions();
|
||||
value["definition"] = json!(config);
|
||||
value["data_dir"] = paths::agent_data_dir(&self.name)
|
||||
value["data_dir"] = Config::agent_data_dir(&self.name)
|
||||
.display()
|
||||
.to_string()
|
||||
.into();
|
||||
value["config_file"] = paths::agent_config_file(&self.name)
|
||||
value["config_file"] = Config::agent_config_file(&self.name)
|
||||
.display()
|
||||
.to_string()
|
||||
.into();
|
||||
@@ -311,14 +323,6 @@ impl Agent {
|
||||
self.rag.clone()
|
||||
}
|
||||
|
||||
pub fn append_mcp_meta_functions(&mut self, mcp_servers: Vec<String>) {
|
||||
self.functions.append_mcp_meta_functions(mcp_servers);
|
||||
}
|
||||
|
||||
pub fn mcp_server_names(&self) -> &[String] {
|
||||
&self.config.mcp_servers
|
||||
}
|
||||
|
||||
pub fn conversation_starters(&self) -> Vec<String> {
|
||||
self.config
|
||||
.conversation_starters
|
||||
@@ -439,6 +443,44 @@ impl Agent {
|
||||
self.config.escalation_timeout
|
||||
}
|
||||
|
||||
pub fn continuation_count(&self) -> usize {
|
||||
self.continuation_count
|
||||
}
|
||||
|
||||
pub fn increment_continuation(&mut self) {
|
||||
self.continuation_count += 1;
|
||||
}
|
||||
|
||||
pub fn reset_continuation(&mut self) {
|
||||
self.continuation_count = 0;
|
||||
self.last_continuation_response = None;
|
||||
}
|
||||
|
||||
pub fn set_last_continuation_response(&mut self, response: String) {
|
||||
self.last_continuation_response = Some(response);
|
||||
}
|
||||
|
||||
pub fn todo_list(&self) -> &TodoList {
|
||||
&self.todo_list
|
||||
}
|
||||
|
||||
pub fn init_todo_list(&mut self, goal: &str) {
|
||||
self.todo_list = TodoList::new(goal);
|
||||
}
|
||||
|
||||
pub fn add_todo(&mut self, task: &str) -> usize {
|
||||
self.todo_list.add(task)
|
||||
}
|
||||
|
||||
pub fn mark_todo_done(&mut self, id: usize) -> bool {
|
||||
self.todo_list.mark_done(id)
|
||||
}
|
||||
|
||||
pub fn clear_todo_list(&mut self) {
|
||||
self.todo_list.clear();
|
||||
self.reset_continuation();
|
||||
}
|
||||
|
||||
pub fn continuation_prompt(&self) -> String {
|
||||
self.config.continuation_prompt.clone().unwrap_or_else(|| {
|
||||
formatdoc! {"
|
||||
@@ -654,12 +696,12 @@ impl AgentConfig {
|
||||
Ok(agent_config)
|
||||
}
|
||||
|
||||
fn load_envs(&mut self, app: &AppConfig) {
|
||||
fn load_envs(&mut self, config: &Config) {
|
||||
let name = &self.name;
|
||||
let with_prefix = |v: &str| normalize_env_name(&format!("{name}_{v}"));
|
||||
|
||||
if self.agent_session.is_none() {
|
||||
self.agent_session = app.agent_session.clone();
|
||||
self.agent_session = config.agent_session.clone();
|
||||
}
|
||||
|
||||
if let Some(v) = read_env_value::<String>(&with_prefix("model")) {
|
||||
@@ -751,7 +793,7 @@ pub struct AgentVariable {
|
||||
}
|
||||
|
||||
pub fn list_agents() -> Vec<String> {
|
||||
let agents_data_dir = paths::agents_data_dir();
|
||||
let agents_data_dir = Config::agents_data_dir();
|
||||
if !agents_data_dir.exists() {
|
||||
return vec![];
|
||||
}
|
||||
@@ -761,7 +803,6 @@ pub fn list_agents() -> Vec<String> {
|
||||
for entry in entries.flatten() {
|
||||
if entry.path().is_dir()
|
||||
&& let Some(name) = entry.file_name().to_str()
|
||||
&& !name.starts_with('.')
|
||||
{
|
||||
agents.push(name.to_string());
|
||||
}
|
||||
@@ -772,7 +813,7 @@ pub fn list_agents() -> Vec<String> {
|
||||
}
|
||||
|
||||
pub fn complete_agent_variables(agent_name: &str) -> Vec<(String, Option<String>)> {
|
||||
let config_path = paths::agent_config_file(agent_name);
|
||||
let config_path = Config::agent_config_file(agent_name);
|
||||
if !config_path.exists() {
|
||||
return vec![];
|
||||
}
|
||||
@@ -791,89 +832,3 @@ pub fn complete_agent_variables(agent_name: &str) -> Vec<(String, Option<String>
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn agent_config_parses_from_yaml() {
|
||||
let yaml = r#"
|
||||
name: test-agent
|
||||
description: A test agent
|
||||
instructions: You are helpful
|
||||
auto_continue: true
|
||||
max_auto_continues: 5
|
||||
can_spawn_agents: true
|
||||
max_concurrent_agents: 8
|
||||
max_agent_depth: 2
|
||||
mcp_servers:
|
||||
- github
|
||||
- jira
|
||||
global_tools:
|
||||
- execute_command.sh
|
||||
- fs_read.sh
|
||||
conversation_starters:
|
||||
- "Hello!"
|
||||
- "How are you?"
|
||||
variables:
|
||||
- name: username
|
||||
description: Your name
|
||||
"#;
|
||||
|
||||
let config: AgentConfig = serde_yaml::from_str(yaml).unwrap();
|
||||
|
||||
assert_eq!(config.name, "test-agent");
|
||||
assert_eq!(config.description, "A test agent");
|
||||
assert!(config.auto_continue);
|
||||
assert_eq!(config.max_auto_continues, 5);
|
||||
assert!(config.can_spawn_agents);
|
||||
assert_eq!(config.max_concurrent_agents, 8);
|
||||
assert_eq!(config.max_agent_depth, 2);
|
||||
assert_eq!(config.mcp_servers, vec!["github", "jira"]);
|
||||
assert_eq!(config.global_tools.len(), 2);
|
||||
assert_eq!(config.conversation_starters.len(), 2);
|
||||
assert_eq!(config.variables.len(), 1);
|
||||
assert_eq!(config.variables[0].name, "username");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_config_defaults() {
|
||||
let yaml = "name: minimal\ninstructions: hi\n";
|
||||
let config: AgentConfig = serde_yaml::from_str(yaml).unwrap();
|
||||
|
||||
assert_eq!(config.name, "minimal");
|
||||
assert!(!config.auto_continue);
|
||||
assert!(!config.can_spawn_agents);
|
||||
assert_eq!(config.max_concurrent_agents, 4);
|
||||
assert_eq!(config.max_agent_depth, 3);
|
||||
assert_eq!(config.max_auto_continues, 10);
|
||||
assert!(config.mcp_servers.is_empty());
|
||||
assert!(config.global_tools.is_empty());
|
||||
assert!(config.conversation_starters.is_empty());
|
||||
assert!(config.variables.is_empty());
|
||||
assert!(config.model_id.is_none());
|
||||
assert!(config.temperature.is_none());
|
||||
assert!(config.top_p.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_config_with_model() {
|
||||
let yaml =
|
||||
"name: test\nmodel: openai:gpt-4\ntemperature: 0.7\ntop_p: 0.9\ninstructions: hi\n";
|
||||
let config: AgentConfig = serde_yaml::from_str(yaml).unwrap();
|
||||
|
||||
assert_eq!(config.model_id, Some("openai:gpt-4".to_string()));
|
||||
assert_eq!(config.temperature, Some(0.7));
|
||||
assert_eq!(config.top_p, Some(0.9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_config_inject_defaults_true() {
|
||||
let yaml = "name: test\ninstructions: hi\n";
|
||||
let config: AgentConfig = serde_yaml::from_str(yaml).unwrap();
|
||||
|
||||
assert!(config.inject_todo_instructions);
|
||||
assert!(config.inject_spawn_instructions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,740 +0,0 @@
|
||||
use crate::client::{ClientConfig, list_models};
|
||||
use crate::render::{MarkdownRender, RenderOptions};
|
||||
use crate::utils::{IS_STDOUT_TERMINAL, NO_COLOR, decode_bin, get_env_name};
|
||||
|
||||
use super::paths;
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use indexmap::IndexMap;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use syntect::highlighting::ThemeSet;
|
||||
use terminal_colorsaurus::{ColorScheme, QueryOptions, color_scheme};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct AppConfig {
|
||||
#[serde(rename(serialize = "model", deserialize = "model"))]
|
||||
#[serde(default)]
|
||||
pub model_id: String,
|
||||
pub temperature: Option<f64>,
|
||||
pub top_p: Option<f64>,
|
||||
|
||||
pub dry_run: bool,
|
||||
pub stream: bool,
|
||||
pub save: bool,
|
||||
pub keybindings: String,
|
||||
pub editor: Option<String>,
|
||||
pub wrap: Option<String>,
|
||||
pub wrap_code: bool,
|
||||
pub(crate) vault_password_file: Option<PathBuf>,
|
||||
|
||||
pub function_calling_support: bool,
|
||||
pub mapping_tools: IndexMap<String, String>,
|
||||
pub enabled_tools: Option<String>,
|
||||
pub visible_tools: Option<Vec<String>>,
|
||||
|
||||
pub mcp_server_support: bool,
|
||||
pub mapping_mcp_servers: IndexMap<String, String>,
|
||||
pub enabled_mcp_servers: Option<String>,
|
||||
|
||||
pub repl_prelude: Option<String>,
|
||||
pub cmd_prelude: Option<String>,
|
||||
pub agent_session: Option<String>,
|
||||
|
||||
pub save_session: Option<bool>,
|
||||
pub compression_threshold: usize,
|
||||
pub summarization_prompt: Option<String>,
|
||||
pub summary_context_prompt: Option<String>,
|
||||
|
||||
pub rag_embedding_model: Option<String>,
|
||||
pub rag_reranker_model: Option<String>,
|
||||
pub rag_top_k: usize,
|
||||
pub rag_chunk_size: Option<usize>,
|
||||
pub rag_chunk_overlap: Option<usize>,
|
||||
pub rag_template: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub document_loaders: HashMap<String, String>,
|
||||
|
||||
pub highlight: bool,
|
||||
pub theme: Option<String>,
|
||||
pub left_prompt: Option<String>,
|
||||
pub right_prompt: Option<String>,
|
||||
|
||||
pub user_agent: Option<String>,
|
||||
pub save_shell_history: bool,
|
||||
pub sync_models_url: Option<String>,
|
||||
|
||||
pub clients: Vec<ClientConfig>,
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model_id: Default::default(),
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
|
||||
dry_run: false,
|
||||
stream: true,
|
||||
save: false,
|
||||
keybindings: "emacs".into(),
|
||||
editor: None,
|
||||
wrap: None,
|
||||
wrap_code: false,
|
||||
vault_password_file: None,
|
||||
|
||||
function_calling_support: true,
|
||||
mapping_tools: Default::default(),
|
||||
enabled_tools: None,
|
||||
visible_tools: None,
|
||||
|
||||
mcp_server_support: true,
|
||||
mapping_mcp_servers: Default::default(),
|
||||
enabled_mcp_servers: None,
|
||||
|
||||
repl_prelude: None,
|
||||
cmd_prelude: None,
|
||||
agent_session: None,
|
||||
|
||||
save_session: None,
|
||||
compression_threshold: 4000,
|
||||
summarization_prompt: None,
|
||||
summary_context_prompt: None,
|
||||
|
||||
rag_embedding_model: None,
|
||||
rag_reranker_model: None,
|
||||
rag_top_k: 5,
|
||||
rag_chunk_size: None,
|
||||
rag_chunk_overlap: None,
|
||||
rag_template: None,
|
||||
|
||||
document_loaders: Default::default(),
|
||||
|
||||
highlight: true,
|
||||
theme: None,
|
||||
left_prompt: None,
|
||||
right_prompt: None,
|
||||
|
||||
user_agent: None,
|
||||
save_shell_history: true,
|
||||
sync_models_url: None,
|
||||
|
||||
clients: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn from_config(config: super::Config) -> Result<Self> {
|
||||
let mut app_config = Self {
|
||||
model_id: config.model_id,
|
||||
temperature: config.temperature,
|
||||
top_p: config.top_p,
|
||||
|
||||
dry_run: config.dry_run,
|
||||
stream: config.stream,
|
||||
save: config.save,
|
||||
keybindings: config.keybindings,
|
||||
editor: config.editor,
|
||||
wrap: config.wrap,
|
||||
wrap_code: config.wrap_code,
|
||||
vault_password_file: config.vault_password_file,
|
||||
|
||||
function_calling_support: config.function_calling_support,
|
||||
mapping_tools: config.mapping_tools,
|
||||
enabled_tools: config.enabled_tools,
|
||||
visible_tools: config.visible_tools,
|
||||
|
||||
mcp_server_support: config.mcp_server_support,
|
||||
mapping_mcp_servers: config.mapping_mcp_servers,
|
||||
enabled_mcp_servers: config.enabled_mcp_servers,
|
||||
|
||||
repl_prelude: config.repl_prelude,
|
||||
cmd_prelude: config.cmd_prelude,
|
||||
agent_session: config.agent_session,
|
||||
|
||||
save_session: config.save_session,
|
||||
compression_threshold: config.compression_threshold,
|
||||
summarization_prompt: config.summarization_prompt,
|
||||
summary_context_prompt: config.summary_context_prompt,
|
||||
|
||||
rag_embedding_model: config.rag_embedding_model,
|
||||
rag_reranker_model: config.rag_reranker_model,
|
||||
rag_top_k: config.rag_top_k,
|
||||
rag_chunk_size: config.rag_chunk_size,
|
||||
rag_chunk_overlap: config.rag_chunk_overlap,
|
||||
rag_template: config.rag_template,
|
||||
|
||||
document_loaders: config.document_loaders,
|
||||
|
||||
highlight: config.highlight,
|
||||
theme: config.theme,
|
||||
left_prompt: config.left_prompt,
|
||||
right_prompt: config.right_prompt,
|
||||
|
||||
user_agent: config.user_agent,
|
||||
save_shell_history: config.save_shell_history,
|
||||
sync_models_url: config.sync_models_url,
|
||||
|
||||
clients: config.clients,
|
||||
};
|
||||
app_config.load_envs();
|
||||
if let Some(wrap) = app_config.wrap.clone() {
|
||||
app_config.set_wrap(&wrap)?;
|
||||
}
|
||||
app_config.setup_document_loaders();
|
||||
app_config.setup_user_agent();
|
||||
app_config.resolve_model()?;
|
||||
Ok(app_config)
|
||||
}
|
||||
|
||||
pub fn resolve_model(&mut self) -> Result<()> {
|
||||
if self.model_id.is_empty() {
|
||||
let models = list_models(self, crate::client::ModelType::Chat);
|
||||
if models.is_empty() {
|
||||
anyhow::bail!("No available model");
|
||||
}
|
||||
self.model_id = models[0].id();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn vault_password_file(&self) -> PathBuf {
|
||||
match &self.vault_password_file {
|
||||
Some(path) => match path.exists() {
|
||||
true => path.clone(),
|
||||
false => gman::config::Config::local_provider_password_file(),
|
||||
},
|
||||
None => gman::config::Config::local_provider_password_file(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn editor(&self) -> Result<String> {
|
||||
super::EDITOR.get_or_init(move || {
|
||||
let editor = self.editor.clone()
|
||||
.or_else(|| env::var("VISUAL").ok().or_else(|| env::var("EDITOR").ok()))
|
||||
.unwrap_or_else(|| {
|
||||
if cfg!(windows) {
|
||||
"notepad".to_string()
|
||||
} else {
|
||||
"nano".to_string()
|
||||
}
|
||||
});
|
||||
which::which(&editor).ok().map(|_| editor)
|
||||
})
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("Editor not found. Please add the `editor` configuration or set the $EDITOR or $VISUAL environment variable."))
|
||||
}
|
||||
|
||||
pub fn sync_models_url(&self) -> String {
|
||||
self.sync_models_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| super::SYNC_MODELS_URL.into())
|
||||
}
|
||||
|
||||
pub fn light_theme(&self) -> bool {
|
||||
matches!(self.theme.as_deref(), Some("light"))
|
||||
}
|
||||
|
||||
pub fn render_options(&self) -> Result<RenderOptions> {
|
||||
let theme = if self.highlight {
|
||||
let theme_mode = if self.light_theme() { "light" } else { "dark" };
|
||||
let theme_filename = format!("{theme_mode}.tmTheme");
|
||||
let theme_path = paths::local_path(&theme_filename);
|
||||
if theme_path.exists() {
|
||||
let theme = ThemeSet::get_theme(&theme_path)
|
||||
.with_context(|| format!("Invalid theme at '{}'", theme_path.display()))?;
|
||||
Some(theme)
|
||||
} else {
|
||||
let theme = if self.light_theme() {
|
||||
decode_bin(super::LIGHT_THEME).context("Invalid builtin light theme")?
|
||||
} else {
|
||||
decode_bin(super::DARK_THEME).context("Invalid builtin dark theme")?
|
||||
};
|
||||
Some(theme)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let wrap = if *IS_STDOUT_TERMINAL {
|
||||
self.wrap.clone()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let truecolor = matches!(
|
||||
env::var("COLORTERM").as_ref().map(|v| v.as_str()),
|
||||
Ok("truecolor")
|
||||
);
|
||||
Ok(RenderOptions::new(theme, wrap, self.wrap_code, truecolor))
|
||||
}
|
||||
|
||||
pub fn print_markdown(&self, text: &str) -> Result<()> {
|
||||
if *IS_STDOUT_TERMINAL {
|
||||
let render_options = self.render_options()?;
|
||||
let mut markdown_render = MarkdownRender::init(render_options)?;
|
||||
println!("{}", markdown_render.render(text));
|
||||
} else {
|
||||
println!("{text}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn set_wrap(&mut self, value: &str) -> Result<()> {
|
||||
if value == "no" {
|
||||
self.wrap = None;
|
||||
} else if value == "auto" {
|
||||
self.wrap = Some(value.into());
|
||||
} else {
|
||||
value
|
||||
.parse::<u16>()
|
||||
.map_err(|_| anyhow!("Invalid wrap value"))?;
|
||||
self.wrap = Some(value.into())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn setup_document_loaders(&mut self) {
|
||||
[("pdf", "pdftotext $1 -"), ("docx", "pandoc --to plain $1")]
|
||||
.into_iter()
|
||||
.for_each(|(k, v)| {
|
||||
let (k, v) = (k.to_string(), v.to_string());
|
||||
self.document_loaders.entry(k).or_insert(v);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn setup_user_agent(&mut self) {
|
||||
if let Some("auto") = self.user_agent.as_deref() {
|
||||
self.user_agent = Some(format!(
|
||||
"{}/{}",
|
||||
env!("CARGO_CRATE_NAME"),
|
||||
env!("CARGO_PKG_VERSION")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_envs(&mut self) {
|
||||
if let Ok(v) = env::var(get_env_name("model")) {
|
||||
self.model_id = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<f64>(&get_env_name("temperature")) {
|
||||
self.temperature = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<f64>(&get_env_name("top_p")) {
|
||||
self.top_p = v;
|
||||
}
|
||||
|
||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("dry_run")) {
|
||||
self.dry_run = v;
|
||||
}
|
||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("stream")) {
|
||||
self.stream = v;
|
||||
}
|
||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("save")) {
|
||||
self.save = v;
|
||||
}
|
||||
if let Ok(v) = env::var(get_env_name("keybindings"))
|
||||
&& v == "vi"
|
||||
{
|
||||
self.keybindings = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("editor")) {
|
||||
self.editor = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("wrap")) {
|
||||
self.wrap = v;
|
||||
}
|
||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("wrap_code")) {
|
||||
self.wrap_code = v;
|
||||
}
|
||||
|
||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("function_calling_support")) {
|
||||
self.function_calling_support = v;
|
||||
}
|
||||
if let Ok(v) = env::var(get_env_name("mapping_tools"))
|
||||
&& let Ok(v) = serde_json::from_str(&v)
|
||||
{
|
||||
self.mapping_tools = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("enabled_tools")) {
|
||||
self.enabled_tools = v;
|
||||
}
|
||||
|
||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("mcp_server_support")) {
|
||||
self.mcp_server_support = v;
|
||||
}
|
||||
if let Ok(v) = env::var(get_env_name("mapping_mcp_servers"))
|
||||
&& let Ok(v) = serde_json::from_str(&v)
|
||||
{
|
||||
self.mapping_mcp_servers = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("enabled_mcp_servers")) {
|
||||
self.enabled_mcp_servers = v;
|
||||
}
|
||||
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("repl_prelude")) {
|
||||
self.repl_prelude = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("cmd_prelude")) {
|
||||
self.cmd_prelude = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("agent_session")) {
|
||||
self.agent_session = v;
|
||||
}
|
||||
|
||||
if let Some(v) = super::read_env_bool(&get_env_name("save_session")) {
|
||||
self.save_session = v;
|
||||
}
|
||||
if let Some(Some(v)) =
|
||||
super::read_env_value::<usize>(&get_env_name("compression_threshold"))
|
||||
{
|
||||
self.compression_threshold = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("summarization_prompt")) {
|
||||
self.summarization_prompt = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("summary_context_prompt")) {
|
||||
self.summary_context_prompt = v;
|
||||
}
|
||||
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("rag_embedding_model")) {
|
||||
self.rag_embedding_model = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("rag_reranker_model")) {
|
||||
self.rag_reranker_model = v;
|
||||
}
|
||||
if let Some(Some(v)) = super::read_env_value::<usize>(&get_env_name("rag_top_k")) {
|
||||
self.rag_top_k = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<usize>(&get_env_name("rag_chunk_size")) {
|
||||
self.rag_chunk_size = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<usize>(&get_env_name("rag_chunk_overlap")) {
|
||||
self.rag_chunk_overlap = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("rag_template")) {
|
||||
self.rag_template = v;
|
||||
}
|
||||
|
||||
if let Ok(v) = env::var(get_env_name("document_loaders"))
|
||||
&& let Ok(v) = serde_json::from_str(&v)
|
||||
{
|
||||
self.document_loaders = v;
|
||||
}
|
||||
|
||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("highlight")) {
|
||||
self.highlight = v;
|
||||
}
|
||||
if *NO_COLOR {
|
||||
self.highlight = false;
|
||||
}
|
||||
if self.highlight && self.theme.is_none() {
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("theme")) {
|
||||
self.theme = v;
|
||||
} else if *IS_STDOUT_TERMINAL
|
||||
&& let Ok(color_scheme) = color_scheme(QueryOptions::default())
|
||||
{
|
||||
let theme = match color_scheme {
|
||||
ColorScheme::Dark => "dark",
|
||||
ColorScheme::Light => "light",
|
||||
};
|
||||
self.theme = Some(theme.into());
|
||||
}
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("left_prompt")) {
|
||||
self.left_prompt = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("right_prompt")) {
|
||||
self.right_prompt = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("user_agent")) {
|
||||
self.user_agent = v;
|
||||
}
|
||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("save_shell_history")) {
|
||||
self.save_shell_history = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("sync_models_url")) {
|
||||
self.sync_models_url = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
#[allow(dead_code)]
|
||||
pub fn set_temperature_default(&mut self, value: Option<f64>) {
|
||||
self.temperature = value;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn set_top_p_default(&mut self, value: Option<f64>) {
|
||||
self.top_p = value;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn set_enabled_tools_default(&mut self, value: Option<String>) {
|
||||
self.enabled_tools = value;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn set_enabled_mcp_servers_default(&mut self, value: Option<String>) {
|
||||
self.enabled_mcp_servers = value;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn set_save_session_default(&mut self, value: Option<bool>) {
|
||||
self.save_session = value;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn set_compression_threshold_default(&mut self, value: Option<usize>) {
|
||||
self.compression_threshold = value.unwrap_or_default();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn set_rag_reranker_model_default(&mut self, value: Option<String>) {
|
||||
self.rag_reranker_model = value;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn set_rag_top_k_default(&mut self, value: usize) {
|
||||
self.rag_top_k = value;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn set_model_id_default(&mut self, model_id: String) {
|
||||
self.model_id = model_id;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
|
||||
fn cached_editor() -> Option<String> {
|
||||
super::super::EDITOR.get().cloned().flatten()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_config_copies_serialized_fields() {
|
||||
let cfg = Config {
|
||||
model_id: "test-model".to_string(),
|
||||
temperature: Some(0.7),
|
||||
top_p: Some(0.9),
|
||||
dry_run: true,
|
||||
stream: false,
|
||||
save: true,
|
||||
highlight: false,
|
||||
compression_threshold: 2000,
|
||||
rag_top_k: 10,
|
||||
clients: vec![ClientConfig::default()],
|
||||
..Config::default()
|
||||
};
|
||||
|
||||
let app = AppConfig::from_config(cfg).unwrap();
|
||||
|
||||
assert_eq!(app.model_id, "test-model");
|
||||
assert_eq!(app.temperature, Some(0.7));
|
||||
assert_eq!(app.top_p, Some(0.9));
|
||||
assert!(app.dry_run);
|
||||
assert!(!app.stream);
|
||||
assert!(app.save);
|
||||
assert!(!app.highlight);
|
||||
assert_eq!(app.compression_threshold, 2000);
|
||||
assert_eq!(app.rag_top_k, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_config_copies_clients() {
|
||||
let cfg = Config {
|
||||
model_id: "test-model".to_string(),
|
||||
clients: vec![ClientConfig::default()],
|
||||
..Config::default()
|
||||
};
|
||||
let app = AppConfig::from_config(cfg).unwrap();
|
||||
|
||||
assert_eq!(app.clients.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_config_copies_mapping_fields() {
|
||||
let mut cfg = Config {
|
||||
model_id: "test-model".to_string(),
|
||||
clients: vec![ClientConfig::default()],
|
||||
..Config::default()
|
||||
};
|
||||
cfg.mapping_tools
|
||||
.insert("alias".to_string(), "real_tool".to_string());
|
||||
cfg.mapping_mcp_servers
|
||||
.insert("gh".to_string(), "github-mcp".to_string());
|
||||
|
||||
let app = AppConfig::from_config(cfg).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
app.mapping_tools.get("alias"),
|
||||
Some(&"real_tool".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
app.mapping_mcp_servers.get("gh"),
|
||||
Some(&"github-mcp".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_returns_configured_value() {
|
||||
let configured = cached_editor()
|
||||
.unwrap_or_else(|| std::env::current_exe().unwrap().display().to_string());
|
||||
let app = AppConfig {
|
||||
editor: Some(configured.clone()),
|
||||
..AppConfig::default()
|
||||
};
|
||||
|
||||
assert_eq!(app.editor().unwrap(), configured);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_falls_back_to_env() {
|
||||
if let Some(expected) = cached_editor() {
|
||||
let app = AppConfig::default();
|
||||
assert_eq!(app.editor().unwrap(), expected);
|
||||
return;
|
||||
}
|
||||
|
||||
let expected = std::env::current_exe().unwrap().display().to_string();
|
||||
unsafe {
|
||||
std::env::set_var("VISUAL", &expected);
|
||||
}
|
||||
|
||||
let app = AppConfig::default();
|
||||
let result = app.editor();
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn light_theme_default_is_false() {
|
||||
let app = AppConfig::default();
|
||||
assert!(!app.light_theme());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_models_url_has_default() {
|
||||
let app = AppConfig::default();
|
||||
let url = app.sync_models_url();
|
||||
assert!(!url.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_config_copies_serde_fields() {
|
||||
let cfg = Config {
|
||||
model_id: "provider:model-x".to_string(),
|
||||
temperature: Some(0.42),
|
||||
compression_threshold: 1234,
|
||||
..Config::default()
|
||||
};
|
||||
|
||||
let app = AppConfig::from_config(cfg).unwrap();
|
||||
|
||||
assert_eq!(app.model_id, "provider:model-x");
|
||||
assert_eq!(app.temperature, Some(0.42));
|
||||
assert_eq!(app.compression_threshold, 1234);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_config_installs_default_document_loaders() {
|
||||
let cfg = Config {
|
||||
model_id: "provider:test".to_string(),
|
||||
..Config::default()
|
||||
};
|
||||
let app = AppConfig::from_config(cfg).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
app.document_loaders.get("pdf"),
|
||||
Some(&"pdftotext $1 -".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
app.document_loaders.get("docx"),
|
||||
Some(&"pandoc --to plain $1".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_config_resolves_auto_user_agent() {
|
||||
let cfg = Config {
|
||||
model_id: "provider:test".to_string(),
|
||||
user_agent: Some("auto".to_string()),
|
||||
..Config::default()
|
||||
};
|
||||
|
||||
let app = AppConfig::from_config(cfg).unwrap();
|
||||
|
||||
let ua = app.user_agent.as_deref().unwrap();
|
||||
assert!(ua != "auto", "user_agent should have been resolved");
|
||||
assert!(ua.contains('/'), "user_agent should be '<name>/<version>'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_config_preserves_explicit_user_agent() {
|
||||
let cfg = Config {
|
||||
model_id: "provider:test".to_string(),
|
||||
user_agent: Some("custom/1.0".to_string()),
|
||||
..Config::default()
|
||||
};
|
||||
|
||||
let app = AppConfig::from_config(cfg).unwrap();
|
||||
|
||||
assert_eq!(app.user_agent.as_deref(), Some("custom/1.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_config_validates_wrap_value() {
|
||||
let cfg = Config {
|
||||
model_id: "provider:test".to_string(),
|
||||
wrap: Some("invalid".to_string()),
|
||||
..Config::default()
|
||||
};
|
||||
|
||||
let result = AppConfig::from_config(cfg);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_config_accepts_wrap_auto() {
|
||||
let cfg = Config {
|
||||
model_id: "provider:test".to_string(),
|
||||
wrap: Some("auto".to_string()),
|
||||
..Config::default()
|
||||
};
|
||||
|
||||
let app = AppConfig::from_config(cfg).unwrap();
|
||||
assert_eq!(app.wrap.as_deref(), Some("auto"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_model_errors_when_no_models_available() {
|
||||
let mut app = AppConfig {
|
||||
model_id: String::new(),
|
||||
clients: vec![],
|
||||
..AppConfig::default()
|
||||
};
|
||||
|
||||
let result = app.resolve_model();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_model_keeps_explicit_model_id() {
|
||||
let mut app = AppConfig {
|
||||
model_id: "provider:explicit".to_string(),
|
||||
..AppConfig::default()
|
||||
};
|
||||
|
||||
app.resolve_model().unwrap();
|
||||
assert_eq!(app.model_id, "provider:explicit");
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
use super::mcp_factory::{McpFactory, McpServerKey};
|
||||
use super::rag_cache::RagCache;
|
||||
use crate::config::AppConfig;
|
||||
use crate::function::Functions;
|
||||
use crate::mcp::{McpRegistry, McpServersConfig};
|
||||
use crate::utils::AbortSignal;
|
||||
use crate::vault::{GlobalVault, Vault};
|
||||
|
||||
use anyhow::Result;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub config: Arc<AppConfig>,
|
||||
pub vault: GlobalVault,
|
||||
pub mcp_factory: Arc<McpFactory>,
|
||||
pub rag_cache: Arc<RagCache>,
|
||||
pub mcp_config: Option<McpServersConfig>,
|
||||
pub mcp_log_path: Option<PathBuf>,
|
||||
pub mcp_registry: Option<Arc<McpRegistry>>,
|
||||
pub functions: Functions,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
#[cfg(test)]
|
||||
pub fn test_default() -> Self {
|
||||
Self {
|
||||
config: Arc::new(AppConfig::default()),
|
||||
vault: Arc::new(Vault::default()),
|
||||
mcp_factory: Arc::new(McpFactory::default()),
|
||||
rag_cache: Arc::new(RagCache::default()),
|
||||
mcp_config: None,
|
||||
mcp_log_path: None,
|
||||
mcp_registry: None,
|
||||
functions: Functions::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn init(
|
||||
config: Arc<AppConfig>,
|
||||
log_path: Option<PathBuf>,
|
||||
start_mcp_servers: bool,
|
||||
abort_signal: AbortSignal,
|
||||
) -> Result<Self> {
|
||||
let vault = Arc::new(Vault::init(&config));
|
||||
|
||||
let mcp_registry = McpRegistry::init(
|
||||
log_path,
|
||||
start_mcp_servers,
|
||||
config.enabled_mcp_servers.clone(),
|
||||
abort_signal,
|
||||
&config,
|
||||
&vault,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mcp_config = mcp_registry.mcp_config().cloned();
|
||||
let mcp_log_path = mcp_registry.log_path().cloned();
|
||||
|
||||
let mcp_factory = Arc::new(McpFactory::default());
|
||||
if let Some(mcp_servers_config) = &mcp_config {
|
||||
for (id, handle) in mcp_registry.running_servers() {
|
||||
if let Some(spec) = mcp_servers_config.mcp_servers.get(id) {
|
||||
let key = McpServerKey::from_spec(id, spec);
|
||||
mcp_factory.insert_active(key, handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut functions = Functions::init(config.visible_tools.as_ref().unwrap_or(&Vec::new()))?;
|
||||
if !mcp_registry.is_empty() && config.mcp_server_support {
|
||||
functions.append_mcp_meta_functions(mcp_registry.list_started_servers());
|
||||
}
|
||||
|
||||
let mcp_registry = if mcp_registry.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Arc::new(mcp_registry))
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
vault,
|
||||
mcp_factory,
|
||||
rag_cache: Arc::new(RagCache::default()),
|
||||
mcp_config,
|
||||
mcp_log_path,
|
||||
mcp_registry,
|
||||
functions,
|
||||
})
|
||||
}
|
||||
}
|
||||