Compare commits
340 Commits
v0.3.0
..
cf1c06e632
| Author | SHA1 | Date | |
|---|---|---|---|
|
cf1c06e632
|
|||
|
49f2932b30
|
|||
|
5fd786dd3d
|
|||
|
f5967c7771
|
|||
|
eee0e86131
|
|||
|
51dfd2a655
|
|||
|
d9cf0c4b08
|
|||
|
b4c65f7a19
|
|||
|
1c0e836a92
|
|||
|
2da196c091
|
|||
|
69648afe27
|
|||
|
454f5c03f3
|
|||
|
406642723e
|
|||
|
2469b713c7
|
|||
|
b6ad7a575d
|
|||
|
f3b410d146
|
|||
|
095d0f3d8a
|
|||
|
5f445e046f
|
|||
|
96ab2bdc1b
|
|||
|
cb175e3b51
|
|||
|
7965b970d9
|
|||
|
0a21f10b04
|
|||
|
49aa9fad41
|
|||
|
8f7d3bd13c
|
|||
|
f7fb249d43
|
|||
|
d9498ffb21
|
|||
|
0177fa6906
|
|||
|
c3f6cb8f46
|
|||
|
7facdce6b6
|
|||
|
c11eb352fe
|
|||
|
0e427dc4ba
|
|||
|
f1914f6bd4
|
|||
|
dba6304f51
|
|||
|
e40a8bba72
|
|||
|
c057249e52
|
|||
|
d906713d7d
|
|||
| ff3419a714 | |||
|
a5899da4fb
|
|||
|
dedcef8ac5
|
|||
|
d658f1d2fe
|
|||
|
6b4a45874f
|
|||
|
7839e1dbd9
|
|||
|
78c3932f36
|
|||
|
11334149b0
|
|||
|
4caa035528
|
|||
|
f30e81af08
|
|||
|
4c75655f58
|
|||
|
f865892c28
|
|||
|
ebeb9c9b7d
|
|||
|
ab2b927fcb
|
|||
|
7e5ff2ba1f
|
|||
|
ed59051f3d
|
|||
| e98bf56a2b | |||
| fb510b1a4f | |||
|
6c17462040
|
|||
|
1536cf384c
|
|||
|
d6842d7e29
|
|||
|
fbc0acda2a
|
|||
|
0327d041b6
|
|||
|
6a01fd4fbd
|
|||
| d822180205 | |||
|
89d0fdce26
|
|||
|
b3ecdce979
|
|||
|
3873821a31
|
|||
|
9c2801b643
|
|||
|
d78820dcd4
|
|||
|
d43c4232a2
|
|||
|
f41c85b703
|
|||
|
9e056bdcf0
|
|||
|
d6022b9f98
|
|||
|
6fc1abf94a
|
|||
|
92ea0f624e
|
|||
|
c3fd8fbc1c
|
|||
|
7fd3f7761c
|
|||
|
05e19098b2
|
|||
|
60067ae757
|
|||
|
c72003b0b6
|
|||
|
7c9d500116
|
|||
|
6b2c87b562
|
|||
|
b2dbdfb4b1
|
|||
|
063e198f96
|
|||
|
73cbe16ec1
|
|||
|
bdea854a9f
|
|||
|
9b4c800597
|
|||
|
eb4d1c02f4
|
|||
|
c428990900
|
|||
|
03b9cc70b9
|
|||
|
3fa0eb832c
|
|||
|
83f66e1061
|
|||
|
741b9c364c
|
|||
|
b6f6f456db
|
|||
|
00a6cf74d7
|
|||
|
d35ca352ca
|
|||
| 57dc1cb252 | |||
|
101a9cdd6e
|
|||
|
c5f52e1efb
|
|||
|
470149b606
|
|||
|
02062c5a50
|
|||
|
e6e99b6926
|
|||
|
15a293204f
|
|||
|
ecf3780aed
|
|||
|
e798747135
|
|||
|
60493728a0
|
|||
|
25d6370b20
|
|||
|
d67f845af5
|
|||
|
920a14cabe
|
|||
|
58bdd2e584
|
|||
|
ce6f53ad05
|
|||
|
96f8007d53
|
|||
|
32a55652fe
|
|||
|
2b92e6c98b
|
|||
|
cfa654bcd8
|
|||
|
d0f5ae39e2
|
|||
|
2bb8cf5f73
|
|||
|
fbac446859
|
|||
|
f91cf2e346
|
|||
|
b6b33ab7e3
|
|||
|
c1902a69d1
|
|||
|
812a8e101c
|
|||
|
655ee2a599
|
|||
|
128a8f9a9c
|
|||
|
b1be9443e7
|
|||
|
7b12c69ebf
|
|||
|
69ad584137
|
|||
|
313058e70a
|
|||
|
ea96d9ba3d
|
|||
|
7884adc7c1
|
|||
|
948466d771
|
|||
|
3894c98b5b
|
|||
|
5e9c31595e
|
|||
|
39d9b25e47
|
|||
|
b86f76ddb9
|
|||
|
7f267a10a1
|
|||
|
cdafdff281
|
|||
|
60ad83d6d9
|
|||
|
44c03ccf4f
|
|||
|
af933bbb29
|
|||
|
1f127ee990
|
|||
|
88a9a7709f
|
|||
| e8d92d1b01 | |||
| ddbfd03e75 | |||
|
d1c7f09015
|
|||
|
d2f8f995f0
|
|||
|
5ef9a397ca
|
|||
|
325ab1f45e
|
|||
|
4cfaa2dc77
|
|||
|
6abe2c5536
|
|||
|
03cfd59962
|
|||
|
4d7d5e5e53
|
|||
|
3779b940ae
|
|||
|
d2e541c5c0
|
|||
|
621c90427c
|
|||
|
486001ee85
|
|||
|
c7a2ec084f
|
|||
|
d4e0d48198
|
|||
|
07f23bab5e
|
|||
|
b11797ea1c
|
|||
|
70c2d411ae
|
|||
|
f82c9aff40
|
|||
|
a935add2a7
|
|||
|
8a37a88ffd
|
|||
|
8f66cac680
|
|||
|
0a40ddd2e4
|
|||
|
d5e0728532
|
|||
|
25c0885dcc
|
|||
|
f56ed7d005
|
|||
|
d79e4b9dff
|
|||
|
cdd829199f
|
|||
|
e3c644b8ca
|
|||
|
5cb8070da1
|
|||
|
66801b5d07
|
|||
|
f2de196e22
|
|||
|
2eba530895
|
|||
| 3baa3102a3 | |||
| 2d4fad596c | |||
| 7259e59d2a | |||
| cec04c4597 | |||
| a7f5677195 | |||
| 6075f0a190 | |||
| 15310a9e2c | |||
| f7df54f2f7 | |||
| 212d4bace4 | |||
| f4b3267c89 | |||
| 9eeeb11871 | |||
| b8db3f689d | |||
| 3b21ce2aa5 | |||
| 9bf4fcd943 | |||
| c1f5cfbbda | |||
| 46517a4e15 | |||
| efbe76e1fc | |||
| 245c567d30 | |||
| cbb3d2c34a | |||
| bddec85fa5 | |||
| 96acbc6bf0 | |||
| 0735a31190 | |||
| 986c64ff13 | |||
| 831426d418 | |||
| b99e3fc030 | |||
| 012734f70a | |||
| f591a9635e | |||
| 7c099bf589 | |||
| 32d3cee907 | |||
| 86539c4bb8 | |||
| 14549afd52 | |||
| 667c843fc0 | |||
| 680a52982c | |||
| 52efb1a775 | |||
| c88931d318 | |||
| 2183ed62d1 | |||
| cc8bd040b9 | |||
| a2a464151f | |||
| c9a3f247e7 | |||
| d167502b7b | |||
| 0d9927bb99 | |||
| c9858ce615 | |||
| cccaa1dbe7 | |||
| acd951e981 | |||
| 10d80d58fd | |||
| f196c375d6 | |||
| cc62c89b05 | |||
| 3266cdeb08 | |||
| 6605c62015 | |||
| 704fdbd145 | |||
| 93e76a65a1 | |||
| b3ca7ebddb | |||
| 091fc0b7b7 | |||
| 874f5ba08e | |||
| 5fdfe94b88 | |||
| c02b168749 | |||
| 6ababd919d | |||
| 86b2b2d772 | |||
| 2aa2c3ccee | |||
| 70645a8431 | |||
| ca4b2f2637 | |||
| 7fce8f9b23 | |||
| e5b3b332f6 | |||
| 3e59762443 | |||
| 2ea8a48f28 | |||
| 3c07471620 | |||
| 23e2c1144f | |||
| 313f5e2dda | |||
| 26c35e55d8 | |||
| 878adc0eb7 | |||
| d353767b2c | |||
| 33baeaa62d | |||
| 591b7a5bf1 | |||
| 0bc993532b | |||
| 09379e7231 | |||
| 1a45ce9dc1 | |||
| 95df054dfb | |||
| 5b49553c6d | |||
| 6508940d11 | |||
| 71d89eaaba | |||
| 9619b7908f | |||
| 304129d793 | |||
| 5df435c21a | |||
| 2719c7320a | |||
| a84bae189c | |||
| d82c7c2535 | |||
| 2bc832ed95 | |||
| b5a0f0635b | |||
| 7426aa4bcb | |||
| ba9649382e | |||
| 9c64e97d8b | |||
| 4b1cd3cf44 | |||
| 4a0f002503 | |||
| c4f8c6e102 | |||
| 421308423f | |||
| 0550de2093 | |||
| dddf72e1da | |||
| e23820adf2 | |||
| fea4411aa6 | |||
| b814a38c59 | |||
| 1a3476e4fb | |||
| ecd4d6587c | |||
| 0938119e99 | |||
| 9f15f01871 | |||
| f09cbd2b32 | |||
| 77c1a06277 | |||
| 600f5d1484 | |||
| 7f71317acd | |||
| 865ef5827b | |||
| e5d5bf6c53 | |||
| 7b08d1ef96 | |||
| 9d363b38c7 | |||
| 2f3586cbbf | |||
| 843abe0621 | |||
| 474c5bc76f | |||
| b49a27f886 | |||
| 6f77b3f46e | |||
| a835012673 | |||
| 3f1e8003f8 | |||
| 8475707e75 | |||
| 8a240b1c3f | |||
| 59a3e3012b | |||
| c13142f971 | |||
| a468ee1154 | |||
| 1b504e211a | |||
| 29536f6291 | |||
| 4ef483126d | |||
| 8d2961f3ee | |||
| f1146bb2b9 | |||
| 2daa014c99 | |||
| ebe642f44a | |||
| 25ad254e84 | |||
| 947a7871c2 | |||
| 6421a677eb | |||
| 950893f4a2 | |||
| a10948614d | |||
| 39fc863e22 | |||
| df8b326d89 | |||
| 591f204b67 | |||
| 316ebd6d25 | |||
| 4e707ae08e | |||
| 1ef554c759 | |||
| 367e7d90fd | |||
| 6e7a89763c | |||
| 9dd3836802 | |||
| f822546971 | |||
| 4bf338f91a | |||
| 16577ddc5e | |||
| 384ae73c80 | |||
| d4c932b8ac | |||
| 743e42d4f8 | |||
| 6be2651106 | |||
| 2a2d20a25c | |||
| 882942385b | |||
| 0aa908c8d3 | |||
| 4c179c9269 | |||
| a4fe91ffda | |||
| dc500207ef | |||
| c1e3c3699b | |||
| 52e9f5fc70 | |||
| c85cddb5b4 | |||
| 477b53124d | |||
| 650dbd92e0 | |||
| 88288a98b6 | |||
| 377ab91af7 | |||
| acfc7685f4 | |||
| 5636010e1e |
@@ -0,0 +1 @@
|
||||
{"type":"rust","build":"cargo build","test":"cargo test","check":"cargo check","_detected_by":"heuristic","_cached_at":"2026-04-13T13:36:33-06:00"}
|
||||
@@ -18,10 +18,11 @@ anyhow = "1.0.69"
|
||||
bytes = "1.4.0"
|
||||
clap = { version = "4.5.40", features = ["cargo", "derive", "wrap_help"] }
|
||||
dirs = "6.0.0"
|
||||
dunce = "1.0.5"
|
||||
futures-util = "0.3.29"
|
||||
inquire = "0.9.4"
|
||||
is-terminal = "0.4.9"
|
||||
reedline = "0.40.0"
|
||||
reedline = "0.46.0"
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
serde_json = { version = "1.0.93", features = ["preserve_order"] }
|
||||
serde_yaml = "0.9.17"
|
||||
@@ -37,7 +38,7 @@ tokio-graceful = "0.2.2"
|
||||
tokio-stream = { version = "0.1.15", default-features = false, features = [
|
||||
"sync",
|
||||
] }
|
||||
crossterm = "0.28.1"
|
||||
crossterm = "0.29.0"
|
||||
chrono = "0.4.23"
|
||||
bincode = { version = "2.0.0", features = [
|
||||
"serde",
|
||||
@@ -88,16 +89,23 @@ duct = "1.0.0"
|
||||
argc = "1.23.0"
|
||||
strum_macros = "0.27.2"
|
||||
indoc = "2.0.6"
|
||||
rmcp = { version = "0.16.0", features = ["client", "transport-child-process"] }
|
||||
rmcp = { version = "1.5.0", features = [
|
||||
"client",
|
||||
"transport-child-process",
|
||||
"transport-streamable-http-client-reqwest",
|
||||
"reqwest-native-tls",
|
||||
] }
|
||||
num_cpus = "1.17.0"
|
||||
rustpython-parser = "0.4.0"
|
||||
rustpython-ast = "0.4.0"
|
||||
tree-sitter = "0.26.8"
|
||||
tree-sitter-language = "0.1"
|
||||
tree-sitter-python = "0.25.0"
|
||||
tree-sitter-typescript = "0.23"
|
||||
colored = "3.0.0"
|
||||
clap_complete = { version = "4.5.58", features = ["unstable-dynamic"] }
|
||||
gman = "0.3.0"
|
||||
gman = "0.4.1"
|
||||
clap_complete_nushell = "4.5.9"
|
||||
open = "5"
|
||||
rand = "0.9.0"
|
||||
rand = { version = "0.10.0", features = ["default"] }
|
||||
url = "2.5.8"
|
||||
|
||||
[dependencies.reqwest]
|
||||
@@ -117,7 +125,7 @@ default-features = false
|
||||
features = ["parsing", "regex-onig", "plist-load"]
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
crossterm = { version = "0.28.1", features = ["use-dev-tty"] }
|
||||
crossterm = { version = "0.29.0", features = ["use-dev-tty"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
arboard = { version = "3.3.0", default-features = false, features = [
|
||||
@@ -129,6 +137,7 @@ arboard = { version = "3.3.0", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4.0"
|
||||
serial_test = "3"
|
||||
|
||||
[[bin]]
|
||||
name = "loki"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Loki: All-in-one, batteries-included LLM CLI Tool
|
||||
|
||||

|
||||

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

|
||||

|
||||
@@ -13,35 +12,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](./docs/AICHAT-MIGRATION.md) to get started.
|
||||
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.
|
||||
|
||||
## Quick Links
|
||||
* [AIChat Migration Guide](./docs/AICHAT-MIGRATION.md): Coming from AIChat? Follow the migration guide to get started.
|
||||
* [AIChat Migration Guide](https://github.com/Dark-Alex-17/loki/wiki/AIChat-Migration): 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](./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 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.
|
||||
* [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.
|
||||
* [History](#history): A history of how Loki came to be.
|
||||
|
||||
## Prerequisites
|
||||
@@ -153,7 +153,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](./docs/VAULT.md)). For providers that support OAuth (e.g. Claude Pro/Max
|
||||
(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
|
||||
subscribers, Google Gemini), you can authenticate with your existing subscription instead:
|
||||
|
||||
```yaml
|
||||
@@ -169,7 +169,7 @@ loki --authenticate my-claude-oauth
|
||||
# Or via the REPL: .authenticate
|
||||
```
|
||||
|
||||
For full details, see the [authentication documentation](./docs/clients/CLIENTS.md#authentication).
|
||||
For full details, see the [authentication documentation](https://github.com/Dark-Alex-17/loki/wiki/Clients#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 +246,7 @@ shown below:
|
||||
|
||||
| Setting | Description |
|
||||
|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `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> |
|
||||
| `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> |
|
||||
| `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,6 +1,7 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"type": "stdio",
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
@@ -15,14 +16,17 @@
|
||||
}
|
||||
},
|
||||
"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"]
|
||||
}
|
||||
|
||||
@@ -50,7 +50,13 @@ def parse_raw_data(data):
|
||||
|
||||
def parse_argv():
|
||||
agent_func = sys.argv[1]
|
||||
agent_data = sys.argv[2]
|
||||
|
||||
tool_data_file = os.environ.get("LLM_TOOL_DATA_FILE")
|
||||
if tool_data_file and os.path.isfile(tool_data_file):
|
||||
with open(tool_data_file, "r", encoding="utf-8") as f:
|
||||
agent_data = f.read()
|
||||
else:
|
||||
agent_data = sys.argv[2]
|
||||
|
||||
if (not agent_data) or (not agent_func):
|
||||
print("Usage: ./{agent_name}.py <agent-func> <agent-data>", file=sys.stderr)
|
||||
|
||||
@@ -14,7 +14,11 @@ main() {
|
||||
|
||||
parse_argv() {
|
||||
agent_func="$1"
|
||||
agent_data="$2"
|
||||
if [[ -n "$LLM_TOOL_DATA_FILE" ]] && [[ -f "$LLM_TOOL_DATA_FILE" ]]; then
|
||||
agent_data="$(cat "$LLM_TOOL_DATA_FILE")"
|
||||
else
|
||||
agent_data="$2"
|
||||
fi
|
||||
if [[ -z "$agent_data" ]] || [[ -z "$agent_func" ]]; then
|
||||
die "usage: ./{agent_name}.sh <agent-func> <agent-data>"
|
||||
fi
|
||||
@@ -57,7 +61,6 @@ run() {
|
||||
if [[ "$OS" == "Windows_NT" ]]; then
|
||||
set -o igncr
|
||||
tools_path="$(cygpath -w "$tools_path")"
|
||||
tool_data="$(echo "$tool_data" | sed 's/\\/\\\\/g')"
|
||||
fi
|
||||
|
||||
jq_script="$(cat <<-'EOF'
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
// Usage: ./{agent_name}.ts <agent-func> <agent-data>
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { pathToFileURL } from "url";
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const { agentFunc, rawData } = parseArgv();
|
||||
const agentData = parseRawData(rawData);
|
||||
|
||||
const configDir = "{config_dir}";
|
||||
setupEnv(configDir, agentFunc);
|
||||
|
||||
const agentToolsPath = join(configDir, "agents", "{agent_name}", "tools.ts");
|
||||
await run(agentToolsPath, agentFunc, agentData);
|
||||
}
|
||||
|
||||
function parseRawData(data: string): Record<string, unknown> {
|
||||
if (!data) {
|
||||
throw new Error("No JSON data");
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
throw new Error("Invalid JSON data");
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgv(): { agentFunc: string; rawData: string } {
|
||||
const agentFunc = process.argv[2];
|
||||
|
||||
const toolDataFile = process.env["LLM_TOOL_DATA_FILE"];
|
||||
let agentData: string;
|
||||
if (toolDataFile && existsSync(toolDataFile)) {
|
||||
agentData = readFileSync(toolDataFile, "utf-8");
|
||||
} else {
|
||||
agentData = process.argv[3];
|
||||
}
|
||||
|
||||
if (!agentFunc || !agentData) {
|
||||
process.stderr.write("Usage: ./{agent_name}.ts <agent-func> <agent-data>\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return { agentFunc, rawData: agentData };
|
||||
}
|
||||
|
||||
function setupEnv(configDir: string, agentFunc: string): void {
|
||||
loadEnv(join(configDir, ".env"));
|
||||
process.env["LLM_ROOT_DIR"] = configDir;
|
||||
process.env["LLM_AGENT_NAME"] = "{agent_name}";
|
||||
process.env["LLM_AGENT_FUNC"] = agentFunc;
|
||||
process.env["LLM_AGENT_ROOT_DIR"] = join(configDir, "agents", "{agent_name}");
|
||||
process.env["LLM_AGENT_CACHE_DIR"] = join(configDir, "cache", "{agent_name}");
|
||||
}
|
||||
|
||||
function loadEnv(filePath: string): void {
|
||||
let lines: string[];
|
||||
try {
|
||||
lines = readFileSync(filePath, "utf-8").split("\n");
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const raw of lines) {
|
||||
const line = raw.trim();
|
||||
if (line.startsWith("#") || !line) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const eqIdx = line.indexOf("=");
|
||||
if (eqIdx === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = line.slice(0, eqIdx).trim();
|
||||
if (key in process.env) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = line.slice(eqIdx + 1).trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
function extractParamNames(fn: Function): string[] {
|
||||
const src = fn.toString();
|
||||
const match = src.match(/^(?:async\s+)?function\s*\w*\s*\(([^)]*)\)/);
|
||||
if (!match) {
|
||||
return [];
|
||||
}
|
||||
return match[1]
|
||||
.split(",")
|
||||
.map((p) => p.trim().replace(/[:=?].*/s, "").trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function spreadArgs(
|
||||
fn: Function,
|
||||
data: Record<string, unknown>,
|
||||
): unknown[] {
|
||||
const names = extractParamNames(fn);
|
||||
if (names.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return names.map((name) => data[name]);
|
||||
}
|
||||
|
||||
async function run(
|
||||
agentPath: string,
|
||||
agentFunc: string,
|
||||
agentData: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const mod = await import(pathToFileURL(agentPath).href);
|
||||
|
||||
if (typeof mod[agentFunc] !== "function") {
|
||||
throw new Error(`No module function '${agentFunc}' at '${agentPath}'`);
|
||||
}
|
||||
|
||||
const fn = mod[agentFunc] as Function;
|
||||
const args = spreadArgs(fn, agentData);
|
||||
const value = await fn(...args);
|
||||
returnToLlm(value);
|
||||
dumpResult(`{agent_name}:${agentFunc}`);
|
||||
}
|
||||
|
||||
function returnToLlm(value: unknown): void {
|
||||
if (value === null || value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const output = process.env["LLM_OUTPUT"];
|
||||
const write = (s: string) => {
|
||||
if (output) {
|
||||
writeFileSync(output, s, "utf-8");
|
||||
} else {
|
||||
process.stdout.write(s);
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
||||
write(String(value));
|
||||
} else if (typeof value === "object") {
|
||||
write(JSON.stringify(value, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
function dumpResult(name: string): void {
|
||||
const dumpResults = process.env["LLM_DUMP_RESULTS"];
|
||||
const llmOutput = process.env["LLM_OUTPUT"];
|
||||
|
||||
if (!dumpResults || !llmOutput || !process.stdout.isTTY) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const pattern = new RegExp(`\\b(${dumpResults})\\b`);
|
||||
if (!pattern.test(name)) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
let data: string;
|
||||
try {
|
||||
data = readFileSync(llmOutput, "utf-8");
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
`\x1b[2m----------------------\n${data}\n----------------------\x1b[0m\n`,
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
process.stderr.write(`${err}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -49,6 +49,11 @@ def parse_raw_data(data):
|
||||
|
||||
|
||||
def parse_argv():
|
||||
tool_data_file = os.environ.get("LLM_TOOL_DATA_FILE")
|
||||
if tool_data_file and os.path.isfile(tool_data_file):
|
||||
with open(tool_data_file, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
argv = sys.argv[:] + [None] * max(0, 2 - len(sys.argv))
|
||||
|
||||
tool_data = argv[1]
|
||||
|
||||
@@ -13,7 +13,11 @@ main() {
|
||||
}
|
||||
|
||||
parse_argv() {
|
||||
tool_data="$1"
|
||||
if [[ -n "$LLM_TOOL_DATA_FILE" ]] && [[ -f "$LLM_TOOL_DATA_FILE" ]]; then
|
||||
tool_data="$(cat "$LLM_TOOL_DATA_FILE")"
|
||||
else
|
||||
tool_data="$1"
|
||||
fi
|
||||
if [[ -z "$tool_data" ]]; then
|
||||
die "usage: ./{function_name}.sh <tool-data>"
|
||||
fi
|
||||
@@ -54,7 +58,6 @@ run() {
|
||||
if [[ "$OS" == "Windows_NT" ]]; then
|
||||
set -o igncr
|
||||
tool_path="$(cygpath -w "$tool_path")"
|
||||
tool_data="$(echo "$tool_data" | sed 's/\\/\\\\/g')"
|
||||
fi
|
||||
|
||||
jq_script="$(cat <<-'EOF'
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
// Usage: ./{function_name}.ts <tool-data>
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { pathToFileURL } from "url";
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const rawData = parseArgv();
|
||||
const toolData = parseRawData(rawData);
|
||||
|
||||
const rootDir = "{root_dir}";
|
||||
setupEnv(rootDir);
|
||||
|
||||
const toolPath = "{tool_path}.ts";
|
||||
await run(toolPath, "run", toolData);
|
||||
}
|
||||
|
||||
function parseRawData(data: string): Record<string, unknown> {
|
||||
if (!data) {
|
||||
throw new Error("No JSON data");
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
throw new Error("Invalid JSON data");
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgv(): string {
|
||||
const toolDataFile = process.env["LLM_TOOL_DATA_FILE"];
|
||||
if (toolDataFile && existsSync(toolDataFile)) {
|
||||
return readFileSync(toolDataFile, "utf-8");
|
||||
}
|
||||
|
||||
const toolData = process.argv[2];
|
||||
|
||||
if (!toolData) {
|
||||
process.stderr.write("Usage: ./{function_name}.ts <tool-data>\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return toolData;
|
||||
}
|
||||
|
||||
function setupEnv(rootDir: string): void {
|
||||
loadEnv(join(rootDir, ".env"));
|
||||
process.env["LLM_ROOT_DIR"] = rootDir;
|
||||
process.env["LLM_TOOL_NAME"] = "{function_name}";
|
||||
process.env["LLM_TOOL_CACHE_DIR"] = join(rootDir, "cache", "{function_name}");
|
||||
}
|
||||
|
||||
function loadEnv(filePath: string): void {
|
||||
let lines: string[];
|
||||
try {
|
||||
lines = readFileSync(filePath, "utf-8").split("\n");
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const raw of lines) {
|
||||
const line = raw.trim();
|
||||
if (line.startsWith("#") || !line) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const eqIdx = line.indexOf("=");
|
||||
if (eqIdx === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = line.slice(0, eqIdx).trim();
|
||||
if (key in process.env) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = line.slice(eqIdx + 1).trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
function extractParamNames(fn: Function): string[] {
|
||||
const src = fn.toString();
|
||||
const match = src.match(/^(?:async\s+)?function\s*\w*\s*\(([^)]*)\)/);
|
||||
if (!match) {
|
||||
return [];
|
||||
}
|
||||
return match[1]
|
||||
.split(",")
|
||||
.map((p) => p.trim().replace(/[:=?].*/s, "").trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function spreadArgs(
|
||||
fn: Function,
|
||||
data: Record<string, unknown>,
|
||||
): unknown[] {
|
||||
const names = extractParamNames(fn);
|
||||
if (names.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return names.map((name) => data[name]);
|
||||
}
|
||||
|
||||
async function run(
|
||||
toolPath: string,
|
||||
toolFunc: string,
|
||||
toolData: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const mod = await import(pathToFileURL(toolPath).href);
|
||||
|
||||
if (typeof mod[toolFunc] !== "function") {
|
||||
throw new Error(`No module function '${toolFunc}' at '${toolPath}'`);
|
||||
}
|
||||
|
||||
const fn = mod[toolFunc] as Function;
|
||||
const args = spreadArgs(fn, toolData);
|
||||
const value = await fn(...args);
|
||||
returnToLlm(value);
|
||||
dumpResult("{function_name}");
|
||||
}
|
||||
|
||||
function returnToLlm(value: unknown): void {
|
||||
if (value === null || value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const output = process.env["LLM_OUTPUT"];
|
||||
const write = (s: string) => {
|
||||
if (output) {
|
||||
writeFileSync(output, s, "utf-8");
|
||||
} else {
|
||||
process.stdout.write(s);
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
||||
write(String(value));
|
||||
} else if (typeof value === "object") {
|
||||
write(JSON.stringify(value, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
function dumpResult(name: string): void {
|
||||
const dumpResults = process.env["LLM_DUMP_RESULTS"];
|
||||
const llmOutput = process.env["LLM_OUTPUT"];
|
||||
|
||||
if (!dumpResults || !llmOutput || !process.stdout.isTTY) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const pattern = new RegExp(`\\b(${dumpResults})\\b`);
|
||||
if (!pattern.test(name)) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
let data: string;
|
||||
try {
|
||||
data = readFileSync(llmOutput, "utf-8");
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
`\x1b[2m----------------------\n${data}\n----------------------\x1b[0m\n`,
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
process.stderr.write(`${err}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
from typing import List, Literal, Optional
|
||||
|
||||
|
||||
def run(
|
||||
string: str,
|
||||
string_enum: Literal["foo", "bar"],
|
||||
@@ -9,26 +10,38 @@ def run(
|
||||
number: float,
|
||||
array: List[str],
|
||||
string_optional: Optional[str] = None,
|
||||
integer_with_default: int = 42,
|
||||
boolean_with_default: bool = True,
|
||||
number_with_default: float = 3.14,
|
||||
string_with_default: str = "hello",
|
||||
array_optional: Optional[List[str]] = None,
|
||||
):
|
||||
"""Demonstrates how to create a tool using Python and how to use comments.
|
||||
"""Demonstrates all supported Python parameter types and variations.
|
||||
Args:
|
||||
string: Define a required string property
|
||||
string_enum: Define a required string property with enum
|
||||
boolean: Define a required boolean property
|
||||
integer: Define a required integer property
|
||||
number: Define a required number property
|
||||
array: Define a required string array property
|
||||
string_optional: Define an optional string property
|
||||
array_optional: Define an optional string array property
|
||||
string: A required string property
|
||||
string_enum: A required string property constrained to specific values
|
||||
boolean: A required boolean property
|
||||
integer: A required integer property
|
||||
number: A required number (float) property
|
||||
array: A required string array property
|
||||
string_optional: An optional string property (Optional[str] with None default)
|
||||
integer_with_default: An optional integer with a non-None default value
|
||||
boolean_with_default: An optional boolean with a default value
|
||||
number_with_default: An optional number with a default value
|
||||
string_with_default: An optional string with a default value
|
||||
array_optional: An optional string array property
|
||||
"""
|
||||
output = f"""string: {string}
|
||||
string_enum: {string_enum}
|
||||
string_optional: {string_optional}
|
||||
boolean: {boolean}
|
||||
integer: {integer}
|
||||
number: {number}
|
||||
array: {array}
|
||||
string_optional: {string_optional}
|
||||
integer_with_default: {integer_with_default}
|
||||
boolean_with_default: {boolean_with_default}
|
||||
number_with_default: {number_with_default}
|
||||
string_with_default: {string_with_default}
|
||||
array_optional: {array_optional}"""
|
||||
|
||||
for key, value in os.environ.items():
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Demonstrates all supported TypeScript parameter types and variations.
|
||||
*
|
||||
* @param string - A required string property
|
||||
* @param string_enum - A required string property constrained to specific values
|
||||
* @param boolean - A required boolean property
|
||||
* @param number - A required number property
|
||||
* @param array_bracket - A required string array using bracket syntax
|
||||
* @param array_generic - A required string array using generic syntax
|
||||
* @param string_optional - An optional string using the question mark syntax
|
||||
* @param string_nullable - An optional string using the union-with-null syntax
|
||||
* @param number_with_default - An optional number with a default value
|
||||
* @param boolean_with_default - An optional boolean with a default value
|
||||
* @param string_with_default - An optional string with a default value
|
||||
* @param array_optional - An optional string array using the question mark syntax
|
||||
*/
|
||||
export function run(
|
||||
string: string,
|
||||
string_enum: "foo" | "bar",
|
||||
boolean: boolean,
|
||||
number: number,
|
||||
array_bracket: string[],
|
||||
array_generic: Array<string>,
|
||||
string_optional?: string,
|
||||
string_nullable: string | null = null,
|
||||
number_with_default: number = 42,
|
||||
boolean_with_default: boolean = true,
|
||||
string_with_default: string = "hello",
|
||||
array_optional?: string[],
|
||||
): string {
|
||||
const parts = [
|
||||
`string: ${string}`,
|
||||
`string_enum: ${string_enum}`,
|
||||
`boolean: ${boolean}`,
|
||||
`number: ${number}`,
|
||||
`array_bracket: ${JSON.stringify(array_bracket)}`,
|
||||
`array_generic: ${JSON.stringify(array_generic)}`,
|
||||
`string_optional: ${string_optional}`,
|
||||
`string_nullable: ${string_nullable}`,
|
||||
`number_with_default: ${number_with_default}`,
|
||||
`boolean_with_default: ${boolean_with_default}`,
|
||||
`string_with_default: ${string_with_default}`,
|
||||
`array_optional: ${JSON.stringify(array_optional)}`,
|
||||
];
|
||||
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (key.startsWith("LLM_")) {
|
||||
parts.push(`${key}: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join("\n");
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import { appendFileSync, mkdirSync } from "fs";
|
||||
import { dirname } from "path";
|
||||
|
||||
/**
|
||||
* Get the current weather in a given location
|
||||
* @param location - The city and optionally the state or country (e.g., "London", "San Francisco, CA").
|
||||
*/
|
||||
export async function run(location: string): string {
|
||||
const encoded = encodeURIComponent(location);
|
||||
const url = `https://wttr.in/${encoded}?format=4`;
|
||||
|
||||
const resp = await fetch(url);
|
||||
const data = await resp.text();
|
||||
|
||||
const dest = process.env["LLM_OUTPUT"] ?? "/dev/stdout";
|
||||
if (dest !== "-" && dest !== "/dev/stdout") {
|
||||
mkdirSync(dirname(dest), { recursive: true });
|
||||
appendFileSync(dest, data, "utf-8");
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/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"
|
||||
}
|
||||
@@ -46,6 +46,7 @@ enabled_tools: null # Which tools to enable by default. (e.g. 'fs,w
|
||||
visible_tools: # Which tools are visible to be compiled (and are thus able to be defined in 'enabled_tools')
|
||||
# - demo_py.py
|
||||
# - demo_sh.sh
|
||||
# - demo_ts.ts
|
||||
- execute_command.sh
|
||||
# - execute_py_code.py
|
||||
# - execute_sql_code.sh
|
||||
@@ -61,6 +62,7 @@ visible_tools: # Which tools are visible to be compiled (and a
|
||||
# - fs_write.sh
|
||||
- get_current_time.sh
|
||||
# - get_current_weather.py
|
||||
# - get_current_weather.ts
|
||||
- get_current_weather.sh
|
||||
- query_jira_issues.sh
|
||||
# - search_arxiv.sh
|
||||
|
||||
@@ -1,722 +0,0 @@
|
||||
# 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)
|
||||
- [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
|
||||
```
|
||||
|
||||
This means that agent configurations often are only two files: the agent configuration file (`config.yaml`), and the
|
||||
tool definitions (`agents/my-agent/tools.sh` or `tools.py`).
|
||||
|
||||
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` or `agents/my-agent/tools.py`, then the agent is really just a
|
||||
`role`.
|
||||
|
||||
You'll notice there's no settings for agent-specific tooling. This is because they are handled separately and
|
||||
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` or `agents/my-agent/tools.sh`.
|
||||
|
||||
**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` or
|
||||
`agent/my-agent/tools.py` 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 either a bash-based `<loki-config-dir>/agents/my-agent/tools.sh` or a Python-based
|
||||
`<loki-config-dir>/agents/my-agent/tools.py`. However, if it's easier to achieve a task in one language vs the other,
|
||||
you're free to define other scripts in your agent's configuration directory and reference them from the main
|
||||
`tools.py/sh` file. **Any scripts *not* named `tools.{py,sh}` will not be picked up by Loki's compiler**, meaning they
|
||||
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).
|
||||
|
||||
## 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
|
||||
@@ -1,211 +0,0 @@
|
||||
# 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`.
|
||||
@@ -1,112 +0,0 @@
|
||||
# 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 | |
|
||||
@@ -1,103 +0,0 @@
|
||||
# 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
|
||||
@@ -1,307 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,117 +0,0 @@
|
||||
# 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:
|
||||
```
|
||||
@@ -1,260 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,266 +0,0 @@
|
||||
# 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:
|
||||
|
||||

|
||||
@@ -1,44 +0,0 @@
|
||||
# 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 |
|
||||
@@ -1,104 +0,0 @@
|
||||
# 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).
|
||||
@@ -1,71 +0,0 @@
|
||||
# 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
|
||||

|
||||
@@ -1,250 +0,0 @@
|
||||
# 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
|
||||
@@ -1,161 +0,0 @@
|
||||
# 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)
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
# 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 |
|
||||
@@ -1,368 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,279 +0,0 @@
|
||||
# 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"
|
||||
```
|
||||
@@ -1,309 +0,0 @@
|
||||
# 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).
|
||||
@@ -1,119 +0,0 @@
|
||||
# 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)
|
||||
<!--toc:end-->
|
||||
|
||||
---
|
||||
|
||||
## Supported Languages
|
||||
Loki supports custom tools written in the following programming languages:
|
||||
|
||||
* Python
|
||||
* Bash
|
||||
|
||||
## Creating a Custom Tool
|
||||
All tools are created as scripts in either Python or Bash. 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,
|
||||
array_optional: Optional[List[str]] = None,
|
||||
):
|
||||
"""Demonstrates how to create a tool using Python and how to use comments.
|
||||
Args:
|
||||
string: Define a required string property
|
||||
string_enum: Define a required string property with enum
|
||||
boolean: Define a required boolean property
|
||||
integer: Define a required integer property
|
||||
number: Define a required number property
|
||||
array: Define a required string array property
|
||||
string_optional: Define an optional string property
|
||||
array_optional: Define an optional string array property
|
||||
"""
|
||||
output = f"""string: {string}
|
||||
string_enum: {string_enum}
|
||||
string_optional: {string_optional}
|
||||
boolean: {boolean}
|
||||
integer: {integer}
|
||||
number: {number}
|
||||
array: {array}
|
||||
array_optional: {array_optional}"""
|
||||
|
||||
for key, value in os.environ.items():
|
||||
if key.startswith("LLM_"):
|
||||
output = f"{output}\n{key}: {value}"
|
||||
|
||||
return output
|
||||
```
|
||||
@@ -1,120 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,190 +0,0 @@
|
||||
# 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. | 🔴 |
|
||||
| [`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. | 🟢 |
|
||||
| [`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
|
||||
|
Before Width: | Height: | Size: 370 KiB |
|
Before Width: | Height: | Size: 587 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 446 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 878 KiB |
|
Before Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 303 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 396 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,5 +1,7 @@
|
||||
use crate::client::{ModelType, list_models};
|
||||
use crate::config::{Config, list_agents};
|
||||
use crate::config::paths;
|
||||
use crate::config::{AppConfig, Config, list_agents, list_sessions};
|
||||
use crate::vault::Vault;
|
||||
use clap_complete::{CompletionCandidate, Shell, generate};
|
||||
use clap_complete_nushell::Nushell;
|
||||
use std::ffi::OsStr;
|
||||
@@ -32,8 +34,8 @@ impl ShellCompletion {
|
||||
|
||||
pub(super) fn model_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
let cur = current.to_string_lossy();
|
||||
match Config::init_bare() {
|
||||
Ok(config) => list_models(&config, ModelType::Chat)
|
||||
match load_app_config_for_completion() {
|
||||
Ok(app_config) => list_models(&app_config, ModelType::Chat)
|
||||
.into_iter()
|
||||
.filter(|&m| m.id().starts_with(&*cur))
|
||||
.map(|m| CompletionCandidate::new(m.id()))
|
||||
@@ -42,9 +44,23 @@ 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();
|
||||
Config::list_roles(true)
|
||||
paths::list_roles(true)
|
||||
.into_iter()
|
||||
.filter(|r| r.starts_with(&*cur))
|
||||
.map(CompletionCandidate::new)
|
||||
@@ -62,7 +78,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();
|
||||
Config::list_rags()
|
||||
paths::list_rags()
|
||||
.into_iter()
|
||||
.filter(|r| r.starts_with(&*cur))
|
||||
.map(CompletionCandidate::new)
|
||||
@@ -71,7 +87,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();
|
||||
Config::list_macros()
|
||||
paths::list_macros()
|
||||
.into_iter()
|
||||
.filter(|m| m.starts_with(&*cur))
|
||||
.map(CompletionCandidate::new)
|
||||
@@ -80,22 +96,17 @@ pub(super) fn macro_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
|
||||
pub(super) fn session_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
let cur = current.to_string_lossy();
|
||||
match Config::init_bare() {
|
||||
Ok(config) => config
|
||||
.list_sessions()
|
||||
.into_iter()
|
||||
.filter(|s| s.starts_with(&*cur))
|
||||
.map(CompletionCandidate::new)
|
||||
.collect(),
|
||||
Err(_) => vec![],
|
||||
}
|
||||
list_sessions()
|
||||
.into_iter()
|
||||
.filter(|s| s.starts_with(&*cur))
|
||||
.map(CompletionCandidate::new)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn secrets_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
let cur = current.to_string_lossy();
|
||||
match Config::init_bare() {
|
||||
Ok(config) => config
|
||||
.vault
|
||||
match load_app_config_for_completion() {
|
||||
Ok(app_config) => Vault::init(&app_config)
|
||||
.list_secrets(false)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
|
||||
@@ -176,3 +176,220 @@ 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,7 +1,8 @@
|
||||
use super::*;
|
||||
|
||||
use crate::config::paths;
|
||||
use crate::{
|
||||
config::{Config, GlobalConfig, Input},
|
||||
config::{AppConfig, Input, RequestContext},
|
||||
function::{FunctionDeclaration, ToolCall, ToolResult, eval_tool_calls},
|
||||
render::render_stream,
|
||||
utils::*,
|
||||
@@ -24,7 +25,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(|| {
|
||||
Config::local_models_override()
|
||||
paths::local_models_override()
|
||||
.ok()
|
||||
.unwrap_or_else(|| serde_yaml::from_str(MODELS_YAML).unwrap())
|
||||
});
|
||||
@@ -37,7 +38,7 @@ static ESCAPE_SLASH_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?<!\\)/
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait Client: Sync + Send {
|
||||
fn global_config(&self) -> &GlobalConfig;
|
||||
fn app_config(&self) -> &AppConfig;
|
||||
|
||||
fn extra_config(&self) -> Option<&ExtraConfig>;
|
||||
|
||||
@@ -58,7 +59,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.global_config().read().user_agent.as_ref() {
|
||||
if let Some(user_agent) = self.app_config().user_agent.as_ref() {
|
||||
builder = builder.user_agent(user_agent);
|
||||
}
|
||||
let client = builder
|
||||
@@ -69,7 +70,7 @@ pub trait Client: Sync + Send {
|
||||
}
|
||||
|
||||
async fn chat_completions(&self, input: Input) -> Result<ChatCompletionsOutput> {
|
||||
if self.global_config().read().dry_run {
|
||||
if self.app_config().dry_run {
|
||||
let content = input.echo_messages();
|
||||
return Ok(ChatCompletionsOutput::new(&content));
|
||||
}
|
||||
@@ -89,7 +90,7 @@ pub trait Client: Sync + Send {
|
||||
let input = input.clone();
|
||||
tokio::select! {
|
||||
ret = async {
|
||||
if self.global_config().read().dry_run {
|
||||
if self.app_config().dry_run {
|
||||
let content = input.echo_messages();
|
||||
handler.text(&content)?;
|
||||
return Ok(());
|
||||
@@ -413,9 +414,10 @@ 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 = client.global_config().read().current_depth > 0;
|
||||
let is_child_agent = ctx.current_depth > 0;
|
||||
let spinner_message = if is_child_agent { "" } else { "Generating" };
|
||||
let ret = abortable_run_with_spinner(
|
||||
client.chat_completions(input.clone()),
|
||||
@@ -436,15 +438,13 @@ pub async fn call_chat_completions(
|
||||
text = extract_code_block(&strip_think_tag(&text)).to_string();
|
||||
}
|
||||
if print {
|
||||
client.global_config().read().print_markdown(&text)?;
|
||||
ctx.app.config.print_markdown(&text)?;
|
||||
}
|
||||
}
|
||||
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()));
|
||||
}
|
||||
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()));
|
||||
Ok((text, tool_results))
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
@@ -454,6 +454,7 @@ 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();
|
||||
@@ -461,7 +462,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.global_config(), abort_signal.clone()),
|
||||
render_stream(rx, client.app_config(), abort_signal.clone()),
|
||||
);
|
||||
|
||||
if handler.abort().aborted() {
|
||||
@@ -476,12 +477,10 @@ pub async fn call_chat_completions_streaming(
|
||||
if !text.is_empty() && !text.ends_with('\n') {
|
||||
println!();
|
||||
}
|
||||
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()));
|
||||
}
|
||||
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()));
|
||||
Ok((text, tool_results))
|
||||
}
|
||||
Err(err) => {
|
||||
|
||||
@@ -24,7 +24,7 @@ macro_rules! register_client {
|
||||
$(
|
||||
#[derive(Debug)]
|
||||
pub struct $client {
|
||||
global_config: $crate::config::GlobalConfig,
|
||||
app_config: std::sync::Arc<$crate::config::AppConfig>,
|
||||
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(global_config: &$crate::config::GlobalConfig, model: &$crate::client::Model) -> Option<Box<dyn Client>> {
|
||||
let config = global_config.read().clients.iter().find_map(|client_config| {
|
||||
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| {
|
||||
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 {
|
||||
global_config: global_config.clone(),
|
||||
app_config: std::sync::Arc::clone(app_config),
|
||||
config,
|
||||
model: model.clone(),
|
||||
}))
|
||||
@@ -72,10 +72,9 @@ macro_rules! register_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());
|
||||
pub fn init_client(app_config: &std::sync::Arc<$crate::config::AppConfig>, model: $crate::client::Model) -> anyhow::Result<Box<dyn Client>> {
|
||||
None
|
||||
$(.or_else(|| $client::init(config, &model)))+
|
||||
$(.or_else(|| $client::init(app_config, &model)))+
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("Invalid model '{}'", model.id())
|
||||
})
|
||||
@@ -101,7 +100,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::Config) -> Vec<&'static String> {
|
||||
pub fn list_client_names(config: &$crate::config::AppConfig) -> Vec<&'static String> {
|
||||
let names = ALL_CLIENT_NAMES.get_or_init(|| {
|
||||
config
|
||||
.clients
|
||||
@@ -117,7 +116,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::Config) -> Vec<&'static $crate::client::Model> {
|
||||
pub fn list_all_models(config: &$crate::config::AppConfig) -> Vec<&'static $crate::client::Model> {
|
||||
let models = ALL_MODELS.get_or_init(|| {
|
||||
config
|
||||
.clients
|
||||
@@ -131,7 +130,7 @@ macro_rules! register_client {
|
||||
models.iter().collect()
|
||||
}
|
||||
|
||||
pub fn list_models(config: &$crate::config::Config, model_type: $crate::client::ModelType) -> Vec<&'static $crate::client::Model> {
|
||||
pub fn list_models(config: &$crate::config::AppConfig, model_type: $crate::client::ModelType) -> Vec<&'static $crate::client::Model> {
|
||||
list_all_models(config).into_iter().filter(|v| v.model_type() == model_type).collect()
|
||||
}
|
||||
};
|
||||
@@ -140,8 +139,8 @@ macro_rules! register_client {
|
||||
#[macro_export]
|
||||
macro_rules! client_common_fns {
|
||||
() => {
|
||||
fn global_config(&self) -> &$crate::config::GlobalConfig {
|
||||
&self.global_config
|
||||
fn app_config(&self) -> &$crate::config::AppConfig {
|
||||
&self.app_config
|
||||
}
|
||||
|
||||
fn extra_config(&self) -> Option<&$crate::client::ExtraConfig> {
|
||||
|
||||
@@ -3,7 +3,7 @@ use super::{
|
||||
message::{Message, MessageContent, MessageContentPart},
|
||||
};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::config::AppConfig;
|
||||
use crate::utils::{estimate_token_length, strip_think_tag};
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
@@ -44,7 +44,11 @@ impl Model {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn retrieve_model(config: &Config, model_id: &str, model_type: ModelType) -> Result<Self> {
|
||||
pub fn retrieve_model(
|
||||
config: &AppConfig,
|
||||
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::Config;
|
||||
use crate::config::paths;
|
||||
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 = Config::token_file(client_name);
|
||||
let path = paths::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 = Config::token_file(client_name);
|
||||
let path = paths::token_file(client_name);
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
@@ -311,10 +311,8 @@ 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();
|
||||
@@ -342,7 +340,7 @@ mod tests {
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures_util::stream;
|
||||
use rand::Rng;
|
||||
use rand::random_range;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
@@ -392,10 +390,9 @@ mod tests {
|
||||
}
|
||||
|
||||
fn split_chunks(text: &str) -> Vec<Vec<u8>> {
|
||||
let mut rng = rand::rng();
|
||||
let len = text.len();
|
||||
let cut1 = rng.random_range(1..len - 1);
|
||||
let cut2 = rng.random_range(cut1 + 1..len);
|
||||
let cut1 = random_range(1..len - 1);
|
||||
let cut2 = random_range(cut1 + 1..len);
|
||||
let chunk1 = text.as_bytes()[..cut1].to_vec();
|
||||
let chunk2 = text.as_bytes()[cut1..cut2].to_vec();
|
||||
let chunk3 = text.as_bytes()[cut2..].to_vec();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use super::todo::TodoList;
|
||||
use super::*;
|
||||
|
||||
use crate::{
|
||||
@@ -6,6 +5,8 @@ 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,
|
||||
@@ -38,16 +39,13 @@ 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 {}",
|
||||
Config::agents_data_dir().display()
|
||||
paths::agents_data_dir().display()
|
||||
);
|
||||
|
||||
for file in AgentAssets::iter() {
|
||||
@@ -56,7 +54,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 = Config::agents_data_dir().join(file.as_ref());
|
||||
let file_path = paths::agents_data_dir().join(file.as_ref());
|
||||
let file_extension = file_path
|
||||
.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
@@ -88,14 +86,17 @@ impl Agent {
|
||||
}
|
||||
|
||||
pub async fn init(
|
||||
config: &GlobalConfig,
|
||||
app: &AppConfig,
|
||||
app_state: &AppState,
|
||||
current_model: &Model,
|
||||
info_flag: bool,
|
||||
name: &str,
|
||||
abort_signal: AbortSignal,
|
||||
) -> Result<Self> {
|
||||
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 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 mut agent_config = if config_path.exists() {
|
||||
AgentConfig::load(&config_path)?
|
||||
} else {
|
||||
@@ -103,57 +104,33 @@ impl Agent {
|
||||
};
|
||||
let mut functions = Functions::init_agent(name, &agent_config.global_tools)?;
|
||||
|
||||
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
|
||||
};
|
||||
agent_config.load_envs(app);
|
||||
|
||||
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()
|
||||
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;
|
||||
}
|
||||
if agent_config.top_p.is_none() {
|
||||
agent_config.top_p = app.top_p;
|
||||
}
|
||||
current_model.clone()
|
||||
}
|
||||
};
|
||||
|
||||
let rag = if rag_path.exists() {
|
||||
Some(Arc::new(Rag::load(config, DEFAULT_AGENT_NAME, &rag_path)?))
|
||||
} else if !agent_config.documents.is_empty() && !config.read().info_flag {
|
||||
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 {
|
||||
let mut ans = false;
|
||||
if *IS_STDOUT_TERMINAL {
|
||||
ans = Confirm::new("The agent has documents attached, init RAG?")
|
||||
@@ -185,9 +162,23 @@ impl Agent {
|
||||
document_paths.push(path.to_string())
|
||||
}
|
||||
}
|
||||
let rag =
|
||||
Rag::init(config, "rag", &rag_path, &document_paths, abort_signal).await?;
|
||||
Some(Arc::new(rag))
|
||||
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)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -218,10 +209,7 @@ impl Agent {
|
||||
functions,
|
||||
rag,
|
||||
model,
|
||||
vault: Arc::clone(&config.read().vault),
|
||||
todo_list: TodoList::default(),
|
||||
continuation_count: 0,
|
||||
last_continuation_response: None,
|
||||
vault: app_state.vault.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -295,11 +283,11 @@ impl Agent {
|
||||
let mut config = self.config.clone();
|
||||
config.instructions = self.interpolated_instructions();
|
||||
value["definition"] = json!(config);
|
||||
value["data_dir"] = Config::agent_data_dir(&self.name)
|
||||
value["data_dir"] = paths::agent_data_dir(&self.name)
|
||||
.display()
|
||||
.to_string()
|
||||
.into();
|
||||
value["config_file"] = Config::agent_config_file(&self.name)
|
||||
value["config_file"] = paths::agent_config_file(&self.name)
|
||||
.display()
|
||||
.to_string()
|
||||
.into();
|
||||
@@ -323,6 +311,14 @@ 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
|
||||
@@ -443,44 +439,6 @@ 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! {"
|
||||
@@ -696,12 +654,12 @@ impl AgentConfig {
|
||||
Ok(agent_config)
|
||||
}
|
||||
|
||||
fn load_envs(&mut self, config: &Config) {
|
||||
fn load_envs(&mut self, app: &AppConfig) {
|
||||
let name = &self.name;
|
||||
let with_prefix = |v: &str| normalize_env_name(&format!("{name}_{v}"));
|
||||
|
||||
if self.agent_session.is_none() {
|
||||
self.agent_session = config.agent_session.clone();
|
||||
self.agent_session = app.agent_session.clone();
|
||||
}
|
||||
|
||||
if let Some(v) = read_env_value::<String>(&with_prefix("model")) {
|
||||
@@ -793,7 +751,7 @@ pub struct AgentVariable {
|
||||
}
|
||||
|
||||
pub fn list_agents() -> Vec<String> {
|
||||
let agents_data_dir = Config::agents_data_dir();
|
||||
let agents_data_dir = paths::agents_data_dir();
|
||||
if !agents_data_dir.exists() {
|
||||
return vec![];
|
||||
}
|
||||
@@ -803,6 +761,7 @@ 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());
|
||||
}
|
||||
@@ -813,7 +772,7 @@ pub fn list_agents() -> Vec<String> {
|
||||
}
|
||||
|
||||
pub fn complete_agent_variables(agent_name: &str) -> Vec<(String, Option<String>)> {
|
||||
let config_path = Config::agent_config_file(agent_name);
|
||||
let config_path = paths::agent_config_file(agent_name);
|
||||
if !config_path.exists() {
|
||||
return vec![];
|
||||
}
|
||||
@@ -832,3 +791,89 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,740 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ use crate::utils::{AbortSignal, base64_encode, is_loader_protocol, sha256};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use indexmap::IndexSet;
|
||||
use std::{collections::HashMap, fs::File, io::Read};
|
||||
use std::{collections::HashMap, fs::File, io::Read, sync::Arc};
|
||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||
|
||||
const IMAGE_EXTS: [&str; 5] = ["png", "jpeg", "jpg", "webp", "gif"];
|
||||
@@ -17,7 +17,11 @@ const SUMMARY_MAX_WIDTH: usize = 80;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Input {
|
||||
config: GlobalConfig,
|
||||
app_config: Arc<AppConfig>,
|
||||
stream_enabled: bool,
|
||||
session: Option<Session>,
|
||||
rag: Option<Arc<Rag>>,
|
||||
functions: Option<Vec<FunctionDeclaration>>,
|
||||
text: String,
|
||||
raw: (String, Vec<String>),
|
||||
patched_text: Option<String>,
|
||||
@@ -34,10 +38,15 @@ pub struct Input {
|
||||
}
|
||||
|
||||
impl Input {
|
||||
pub fn from_str(config: &GlobalConfig, text: &str, role: Option<Role>) -> Self {
|
||||
let (role, with_session, with_agent) = resolve_role(&config.read(), role);
|
||||
pub fn from_str(ctx: &RequestContext, text: &str, role: Option<Role>) -> Self {
|
||||
let (role, with_session, with_agent) = resolve_role(ctx, role);
|
||||
let captured = capture_input_config(ctx, &role);
|
||||
Self {
|
||||
config: config.clone(),
|
||||
app_config: Arc::clone(&ctx.app.config),
|
||||
stream_enabled: captured.stream_enabled,
|
||||
session: captured.session,
|
||||
rag: captured.rag,
|
||||
functions: captured.functions,
|
||||
text: text.to_string(),
|
||||
raw: (text.to_string(), vec![]),
|
||||
patched_text: None,
|
||||
@@ -55,12 +64,12 @@ impl Input {
|
||||
}
|
||||
|
||||
pub async fn from_files(
|
||||
config: &GlobalConfig,
|
||||
ctx: &RequestContext,
|
||||
raw_text: &str,
|
||||
paths: Vec<String>,
|
||||
role: Option<Role>,
|
||||
) -> Result<Self> {
|
||||
let loaders = config.read().document_loaders.clone();
|
||||
let loaders = ctx.app.config.document_loaders.clone();
|
||||
let (raw_paths, local_paths, remote_urls, external_cmds, protocol_paths, with_last_reply) =
|
||||
resolve_paths(&loaders, paths)?;
|
||||
let mut last_reply = None;
|
||||
@@ -78,7 +87,7 @@ impl Input {
|
||||
texts.push(raw_text.to_string());
|
||||
};
|
||||
if with_last_reply {
|
||||
if let Some(LastMessage { input, output, .. }) = config.read().last_message.as_ref() {
|
||||
if let Some(LastMessage { input, output, .. }) = ctx.last_message.as_ref() {
|
||||
if !output.is_empty() {
|
||||
last_reply = Some(output.clone())
|
||||
} else if let Some(v) = input.last_reply.as_ref() {
|
||||
@@ -102,9 +111,14 @@ impl Input {
|
||||
));
|
||||
}
|
||||
}
|
||||
let (role, with_session, with_agent) = resolve_role(&config.read(), role);
|
||||
let (role, with_session, with_agent) = resolve_role(ctx, role);
|
||||
let captured = capture_input_config(ctx, &role);
|
||||
Ok(Self {
|
||||
config: config.clone(),
|
||||
app_config: Arc::clone(&ctx.app.config),
|
||||
stream_enabled: captured.stream_enabled,
|
||||
session: captured.session,
|
||||
rag: captured.rag,
|
||||
functions: captured.functions,
|
||||
text: texts.join("\n"),
|
||||
raw: (raw_text.to_string(), raw_paths),
|
||||
patched_text: None,
|
||||
@@ -122,14 +136,14 @@ impl Input {
|
||||
}
|
||||
|
||||
pub async fn from_files_with_spinner(
|
||||
config: &GlobalConfig,
|
||||
ctx: &RequestContext,
|
||||
raw_text: &str,
|
||||
paths: Vec<String>,
|
||||
role: Option<Role>,
|
||||
abort_signal: AbortSignal,
|
||||
) -> Result<Self> {
|
||||
abortable_run_with_spinner(
|
||||
Input::from_files(config, raw_text, paths, role),
|
||||
Input::from_files(ctx, raw_text, paths, role),
|
||||
"Loading files",
|
||||
abort_signal,
|
||||
)
|
||||
@@ -164,7 +178,7 @@ impl Input {
|
||||
}
|
||||
|
||||
pub fn stream(&self) -> bool {
|
||||
self.config.read().stream && !self.role().model().no_stream()
|
||||
self.stream_enabled && !self.role().model().no_stream()
|
||||
}
|
||||
|
||||
pub fn continue_output(&self) -> Option<&str> {
|
||||
@@ -183,10 +197,9 @@ impl Input {
|
||||
self.regenerate
|
||||
}
|
||||
|
||||
pub fn set_regenerate(&mut self) {
|
||||
let role = self.config.read().extract_role();
|
||||
if role.name() == self.role().name() {
|
||||
self.role = role;
|
||||
pub fn set_regenerate(&mut self, current_role: Role) {
|
||||
if current_role.name() == self.role().name() {
|
||||
self.role = current_role;
|
||||
}
|
||||
self.regenerate = true;
|
||||
self.tool_calls = None;
|
||||
@@ -196,9 +209,10 @@ impl Input {
|
||||
if self.text.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let rag = self.config.read().rag.clone();
|
||||
if let Some(rag) = rag {
|
||||
let result = Config::search_rag(&self.config, &rag, &self.text, abort_signal).await?;
|
||||
if let Some(rag) = &self.rag {
|
||||
let result = rag
|
||||
.search_with_template(&self.app_config, &self.text, abort_signal)
|
||||
.await?;
|
||||
self.patched_text = Some(result);
|
||||
self.rag_name = Some(rag.name().to_string());
|
||||
}
|
||||
@@ -220,7 +234,7 @@ impl Input {
|
||||
}
|
||||
|
||||
pub fn create_client(&self) -> Result<Box<dyn Client>> {
|
||||
init_client(&self.config, Some(self.role().model().clone()))
|
||||
init_client(&self.app_config, self.role().model().clone())
|
||||
}
|
||||
|
||||
pub async fn fetch_chat_text(&self) -> Result<String> {
|
||||
@@ -240,7 +254,7 @@ impl Input {
|
||||
model.guard_max_input_tokens(&messages)?;
|
||||
let (temperature, top_p) = (self.role().temperature(), self.role().top_p());
|
||||
let functions = if model.supports_function_calling() {
|
||||
let fns = self.config.read().select_functions(self.role());
|
||||
let fns = self.functions.clone();
|
||||
if let Some(vec) = &fns {
|
||||
for def in vec {
|
||||
debug!("Function definition: {:?}", def.name);
|
||||
@@ -260,7 +274,7 @@ impl Input {
|
||||
}
|
||||
|
||||
pub fn build_messages(&self) -> Result<Vec<Message>> {
|
||||
let mut messages = if let Some(session) = self.session(&self.config.read().session) {
|
||||
let mut messages = if let Some(session) = self.session(&self.session) {
|
||||
session.build_messages(self)
|
||||
} else {
|
||||
self.role().build_messages(self)
|
||||
@@ -275,7 +289,7 @@ impl Input {
|
||||
}
|
||||
|
||||
pub fn echo_messages(&self) -> String {
|
||||
if let Some(session) = self.session(&self.config.read().session) {
|
||||
if let Some(session) = self.session(&self.session) {
|
||||
session.echo_messages(self)
|
||||
} else {
|
||||
self.role().echo_messages(self)
|
||||
@@ -384,17 +398,33 @@ impl Input {
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_role(config: &Config, role: Option<Role>) -> (Role, bool, bool) {
|
||||
fn resolve_role(ctx: &RequestContext, role: Option<Role>) -> (Role, bool, bool) {
|
||||
match role {
|
||||
Some(v) => (v, false, false),
|
||||
None => (
|
||||
config.extract_role(),
|
||||
config.session.is_some(),
|
||||
config.agent.is_some(),
|
||||
ctx.extract_role(ctx.app.config.as_ref()),
|
||||
ctx.session.is_some(),
|
||||
ctx.agent.is_some(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
struct CapturedInputConfig {
|
||||
stream_enabled: bool,
|
||||
session: Option<Session>,
|
||||
rag: Option<Arc<Rag>>,
|
||||
functions: Option<Vec<FunctionDeclaration>>,
|
||||
}
|
||||
|
||||
fn capture_input_config(ctx: &RequestContext, role: &Role) -> CapturedInputConfig {
|
||||
CapturedInputConfig {
|
||||
stream_enabled: ctx.app.config.stream,
|
||||
session: ctx.session.clone(),
|
||||
rag: ctx.rag.clone(),
|
||||
functions: ctx.select_functions(role),
|
||||
}
|
||||
}
|
||||
|
||||
type ResolvePathsOutput = (
|
||||
Vec<String>,
|
||||
Vec<String>,
|
||||
@@ -548,3 +578,390 @@ fn read_media_to_data_url(image_path: &str) -> Result<String> {
|
||||
|
||||
Ok(data_url)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::request_context::RequestContext;
|
||||
use crate::config::{AppState, WorkingMode};
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
|
||||
fn default_app_state() -> Arc<AppState> {
|
||||
Arc::new(AppState::test_default())
|
||||
}
|
||||
|
||||
fn create_test_ctx() -> RequestContext {
|
||||
RequestContext::new(default_app_state(), WorkingMode::Cmd)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_role_with_explicit_role() {
|
||||
let ctx = create_test_ctx();
|
||||
let role = Role::new("custom", "be helpful");
|
||||
let (resolved, with_session, with_agent) = resolve_role(&ctx, Some(role));
|
||||
assert_eq!(resolved.name(), "custom");
|
||||
assert!(!with_session);
|
||||
assert!(!with_agent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_role_without_role_no_session_no_agent() {
|
||||
let ctx = create_test_ctx();
|
||||
let (resolved, with_session, with_agent) = resolve_role(&ctx, None);
|
||||
assert_eq!(resolved.name(), "");
|
||||
assert!(!with_session);
|
||||
assert!(!with_agent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_role_without_role_with_session() {
|
||||
let mut ctx = create_test_ctx();
|
||||
ctx.session = Some(Session::default());
|
||||
let (_resolved, with_session, with_agent) = resolve_role(&ctx, None);
|
||||
assert!(with_session);
|
||||
assert!(!with_agent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_role_explicit_role_overrides_session_flag() {
|
||||
let mut ctx = create_test_ctx();
|
||||
ctx.session = Some(Session::default());
|
||||
let role = Role::new("explicit", "prompt");
|
||||
let (_resolved, with_session, _with_agent) = resolve_role(&ctx, Some(role));
|
||||
assert!(!with_session);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_paths_detects_last_reply_syntax() {
|
||||
let loaders = HashMap::new();
|
||||
let (_, _, _, _, _, with_last_reply) =
|
||||
resolve_paths(&loaders, vec!["%%".to_string()]).unwrap();
|
||||
assert!(with_last_reply);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_paths_detects_url() {
|
||||
let loaders = HashMap::new();
|
||||
let (_, local, remote, _, _, _) =
|
||||
resolve_paths(&loaders, vec!["https://example.com".to_string()]).unwrap();
|
||||
assert!(local.is_empty());
|
||||
assert_eq!(remote, vec!["https://example.com"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_paths_detects_external_command() {
|
||||
let loaders = HashMap::new();
|
||||
let (_, _, _, external, _, _) =
|
||||
resolve_paths(&loaders, vec!["`echo hello`".to_string()]).unwrap();
|
||||
assert_eq!(external, vec!["echo hello"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_paths_empty_input() {
|
||||
let loaders = HashMap::new();
|
||||
let (raw, local, remote, external, protocol, with_last) =
|
||||
resolve_paths(&loaders, vec![]).unwrap();
|
||||
assert!(raw.is_empty());
|
||||
assert!(local.is_empty());
|
||||
assert!(remote.is_empty());
|
||||
assert!(external.is_empty());
|
||||
assert!(protocol.is_empty());
|
||||
assert!(!with_last);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_paths_rejects_url_with_glob_suffix() {
|
||||
let loaders = HashMap::new();
|
||||
let result = resolve_paths(&loaders, vec!["https://example.com**".to_string()]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_paths_mixed_inputs() {
|
||||
let loaders = HashMap::new();
|
||||
let paths = vec![
|
||||
"%%".to_string(),
|
||||
"https://example.com".to_string(),
|
||||
"`ls`".to_string(),
|
||||
];
|
||||
let (_, _, remote, external, _, with_last) = resolve_paths(&loaders, paths).unwrap();
|
||||
assert!(with_last);
|
||||
assert_eq!(remote.len(), 1);
|
||||
assert_eq!(external.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_from_str_captures_text() {
|
||||
let ctx = create_test_ctx();
|
||||
let input = Input::from_str(&ctx, "hello world", None);
|
||||
assert_eq!(input.text(), "hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_from_str_with_explicit_role() {
|
||||
let ctx = create_test_ctx();
|
||||
let role = Role::new("pirate", "you are a pirate");
|
||||
let input = Input::from_str(&ctx, "ahoy", Some(role));
|
||||
assert_eq!(input.role().name(), "pirate");
|
||||
assert!(!input.with_agent());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_from_str_captures_stream_from_config() {
|
||||
let mut state = AppState::test_default();
|
||||
let mut config = (*state.config).clone();
|
||||
config.stream = false;
|
||||
state.config = Arc::new(config);
|
||||
let ctx = RequestContext::new(Arc::new(state), WorkingMode::Cmd);
|
||||
let input = Input::from_str(&ctx, "test", None);
|
||||
assert!(!input.stream_enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_is_empty_with_no_text_and_no_medias() {
|
||||
let ctx = create_test_ctx();
|
||||
let input = Input::from_str(&ctx, "", None);
|
||||
assert!(input.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_is_not_empty_with_text() {
|
||||
let ctx = create_test_ctx();
|
||||
let input = Input::from_str(&ctx, "hello", None);
|
||||
assert!(!input.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_set_text_changes_text() {
|
||||
let ctx = create_test_ctx();
|
||||
let mut input = Input::from_str(&ctx, "original", None);
|
||||
input.set_text("modified".to_string());
|
||||
assert_eq!(input.text(), "modified");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_text_returns_patched_when_set() {
|
||||
let ctx = create_test_ctx();
|
||||
let mut input = Input::from_str(&ctx, "original", None);
|
||||
input.patched_text = Some("patched".to_string());
|
||||
assert_eq!(input.text(), "patched");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_clear_patch_restores_original() {
|
||||
let ctx = create_test_ctx();
|
||||
let mut input = Input::from_str(&ctx, "original", None);
|
||||
input.patched_text = Some("patched".to_string());
|
||||
input.clear_patch();
|
||||
assert_eq!(input.text(), "original");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_set_continue_output_accumulates() {
|
||||
let ctx = create_test_ctx();
|
||||
let mut input = Input::from_str(&ctx, "test", None);
|
||||
assert!(input.continue_output().is_none());
|
||||
input.set_continue_output("first ");
|
||||
assert_eq!(input.continue_output(), Some("first "));
|
||||
input.set_continue_output("second");
|
||||
assert_eq!(input.continue_output(), Some("first second"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_set_regenerate_sets_flag_and_clears_tool_calls() {
|
||||
let ctx = create_test_ctx();
|
||||
let mut input = Input::from_str(&ctx, "test", None);
|
||||
let role = input.role().clone();
|
||||
assert!(!input.regenerate());
|
||||
input.set_regenerate(role);
|
||||
assert!(input.regenerate());
|
||||
assert!(input.tool_calls().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_summary_truncates_long_text() {
|
||||
let ctx = create_test_ctx();
|
||||
let long_text = "a".repeat(200);
|
||||
let input = Input::from_str(&ctx, &long_text, None);
|
||||
let summary = input.summary();
|
||||
assert!(summary.len() < 200);
|
||||
assert!(summary.ends_with("..."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_summary_preserves_short_text() {
|
||||
let ctx = create_test_ctx();
|
||||
let input = Input::from_str(&ctx, "short", None);
|
||||
assert_eq!(input.summary(), "short");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_raw_with_no_files() {
|
||||
let ctx = create_test_ctx();
|
||||
let input = Input::from_str(&ctx, "hello", None);
|
||||
assert_eq!(input.raw(), "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_render_with_no_medias() {
|
||||
let ctx = create_test_ctx();
|
||||
let input = Input::from_str(&ctx, "hello", None);
|
||||
assert_eq!(input.render(), "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_with_agent_false_when_no_agent() {
|
||||
let ctx = create_test_ctx();
|
||||
let input = Input::from_str(&ctx, "test", None);
|
||||
assert!(!input.with_agent());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_session_returns_none_when_with_session_false() {
|
||||
let ctx = create_test_ctx();
|
||||
let input = Input::from_str(&ctx, "test", Some(Role::new("r", "p")));
|
||||
let session = Some(Session::default());
|
||||
assert!(input.session(&session).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_session_returns_some_when_with_session_true() {
|
||||
let mut ctx = create_test_ctx();
|
||||
ctx.session = Some(Session::default());
|
||||
let input = Input::from_str(&ctx, "test", None);
|
||||
let session = Some(Session::default());
|
||||
assert!(input.session(&session).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_image_recognizes_image_extensions() {
|
||||
assert!(is_image("photo.png"));
|
||||
assert!(is_image("photo.jpeg"));
|
||||
assert!(is_image("photo.jpg"));
|
||||
assert!(is_image("photo.webp"));
|
||||
assert!(is_image("photo.gif"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_image_rejects_non_image_extensions() {
|
||||
assert!(!is_image("file.txt"));
|
||||
assert!(!is_image("file.rs"));
|
||||
assert!(!is_image("file.pdf"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_data_url_returns_path_for_known_hash() {
|
||||
let mut data_urls = HashMap::new();
|
||||
let data_url = "data:image/png;base64,abc123";
|
||||
let hash = sha256(data_url);
|
||||
data_urls.insert(hash, "/path/to/image.png".to_string());
|
||||
let result = resolve_data_url(&data_urls, data_url.to_string());
|
||||
assert_eq!(result, "/path/to/image.png");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_data_url_returns_original_for_non_data_url() {
|
||||
let data_urls = HashMap::new();
|
||||
let result = resolve_data_url(&data_urls, "https://example.com/image.png".to_string());
|
||||
assert_eq!(result, "https://example.com/image.png");
|
||||
}
|
||||
|
||||
fn run_async<F: Future>(f: F) -> F::Output {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(f)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_files_loads_single_text_file() {
|
||||
let dir = env::temp_dir().join(format!(
|
||||
"loki-input-test-{}",
|
||||
SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
));
|
||||
create_dir_all(&dir).unwrap();
|
||||
let file_path = dir.join("test.txt");
|
||||
fs::write(&file_path, "file content here").unwrap();
|
||||
|
||||
let ctx = create_test_ctx();
|
||||
let input = run_async(Input::from_files(
|
||||
&ctx,
|
||||
"question",
|
||||
vec![file_path.to_string_lossy().to_string()],
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
assert!(input.text().contains("file content here"));
|
||||
assert!(input.text().contains("question"));
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_files_loads_multiple_files() {
|
||||
let dir = env::temp_dir().join(format!(
|
||||
"loki-input-test-multi-{}",
|
||||
SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
));
|
||||
create_dir_all(&dir).unwrap();
|
||||
fs::write(dir.join("a.txt"), "content A").unwrap();
|
||||
fs::write(dir.join("b.txt"), "content B").unwrap();
|
||||
|
||||
let ctx = create_test_ctx();
|
||||
let input = run_async(Input::from_files(
|
||||
&ctx,
|
||||
"question",
|
||||
vec![
|
||||
dir.join("a.txt").to_string_lossy().to_string(),
|
||||
dir.join("b.txt").to_string_lossy().to_string(),
|
||||
],
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
assert!(input.text().contains("content A"));
|
||||
assert!(input.text().contains("content B"));
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_files_with_no_paths_just_text() {
|
||||
let ctx = create_test_ctx();
|
||||
let input = run_async(Input::from_files(&ctx, "just text", vec![], None)).unwrap();
|
||||
assert_eq!(input.text(), "just text");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_files_with_external_command() {
|
||||
let ctx = create_test_ctx();
|
||||
let input = run_async(Input::from_files(
|
||||
&ctx,
|
||||
"question",
|
||||
vec!["`echo hello from cmd`".to_string()],
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
assert!(input.text().contains("hello from cmd"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_files_nonexistent_file_errors() {
|
||||
let ctx = create_test_ctx();
|
||||
let result = run_async(Input::from_files(
|
||||
&ctx,
|
||||
"question",
|
||||
vec!["/nonexistent/path/xyz.txt".to_string()],
|
||||
None,
|
||||
));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::config::{Config, GlobalConfig, RoleLike, ensure_parent_exists};
|
||||
use crate::config::paths;
|
||||
use crate::config::{RequestContext, RoleLike, ensure_parent_exists};
|
||||
use crate::repl::{run_repl_command, split_args_text};
|
||||
use crate::utils::{AbortSignal, multiline_text};
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use indexmap::IndexMap;
|
||||
use parking_lot::RwLock;
|
||||
use rust_embed::Embed;
|
||||
use serde::Deserialize;
|
||||
use std::fs::File;
|
||||
use std::fs::{File, read_to_string};
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -16,12 +16,12 @@ struct MacroAssets;
|
||||
|
||||
#[async_recursion::async_recursion]
|
||||
pub async fn macro_execute(
|
||||
config: &GlobalConfig,
|
||||
ctx: &mut RequestContext,
|
||||
name: &str,
|
||||
args: Option<&str>,
|
||||
abort_signal: AbortSignal,
|
||||
) -> Result<()> {
|
||||
let macro_value = Config::load_macro(name)?;
|
||||
let macro_value = Macro::load(name)?;
|
||||
let (mut new_args, text) = split_args_text(args.unwrap_or_default(), cfg!(windows));
|
||||
if !text.is_empty() {
|
||||
new_args.push(text.to_string());
|
||||
@@ -29,25 +29,42 @@ pub async fn macro_execute(
|
||||
let variables = macro_value
|
||||
.resolve_variables(&new_args)
|
||||
.map_err(|err| anyhow!("{err}. Usage: {}", macro_value.usage(name)))?;
|
||||
let role = config.read().extract_role();
|
||||
let mut config = config.read().clone();
|
||||
config.temperature = role.temperature();
|
||||
config.top_p = role.top_p();
|
||||
config.enabled_tools = role.enabled_tools().clone();
|
||||
config.enabled_mcp_servers = role.enabled_mcp_servers().clone();
|
||||
config.macro_flag = true;
|
||||
config.model = role.model().clone();
|
||||
config.role = None;
|
||||
config.session = None;
|
||||
config.rag = None;
|
||||
config.agent = None;
|
||||
config.discontinuous_last_message();
|
||||
let config = Arc::new(RwLock::new(config));
|
||||
config.write().macro_flag = true;
|
||||
let role = ctx.extract_role(ctx.app.config.as_ref());
|
||||
let mut app_config = (*ctx.app.config).clone();
|
||||
app_config.temperature = role.temperature();
|
||||
app_config.top_p = role.top_p();
|
||||
app_config.enabled_tools = role.enabled_tools().clone();
|
||||
app_config.enabled_mcp_servers = role.enabled_mcp_servers().clone();
|
||||
|
||||
let mut app_state = (*ctx.app).clone();
|
||||
app_state.config = Arc::new(app_config);
|
||||
|
||||
let mut macro_ctx = RequestContext::new(Arc::new(app_state), ctx.working_mode);
|
||||
macro_ctx.macro_flag = true;
|
||||
macro_ctx.info_flag = ctx.info_flag;
|
||||
macro_ctx.model = role.model().clone();
|
||||
macro_ctx.agent_variables = ctx.agent_variables.clone();
|
||||
macro_ctx.last_message = ctx.last_message.clone();
|
||||
macro_ctx.supervisor = ctx.supervisor.clone();
|
||||
macro_ctx.parent_supervisor = ctx.parent_supervisor.clone();
|
||||
macro_ctx.self_agent_id = ctx.self_agent_id.clone();
|
||||
macro_ctx.inbox = ctx.inbox.clone();
|
||||
macro_ctx.escalation_queue = ctx.escalation_queue.clone();
|
||||
macro_ctx.current_depth = ctx.current_depth;
|
||||
macro_ctx.auto_continue_count = ctx.auto_continue_count;
|
||||
macro_ctx.todo_list = ctx.todo_list.clone();
|
||||
macro_ctx.tool_scope.tool_tracker = ctx.tool_scope.tool_tracker.clone();
|
||||
macro_ctx.discontinuous_last_message();
|
||||
|
||||
let app = macro_ctx.app.config.clone();
|
||||
macro_ctx
|
||||
.bootstrap_tools(app.as_ref(), true, abort_signal.clone())
|
||||
.await?;
|
||||
|
||||
for step in ¯o_value.steps {
|
||||
let command = Macro::interpolate_command(step, &variables);
|
||||
println!(">> {}", multiline_text(&command));
|
||||
run_repl_command(&config, abort_signal.clone(), &command).await?;
|
||||
run_repl_command(&mut macro_ctx, abort_signal.clone(), &command).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -60,10 +77,18 @@ pub struct Macro {
|
||||
}
|
||||
|
||||
impl Macro {
|
||||
pub fn load(name: &str) -> Result<Macro> {
|
||||
let path = paths::macro_file(name);
|
||||
let err = || format!("Failed to load macro '{name}' at '{}'", path.display());
|
||||
let content = read_to_string(&path).with_context(err)?;
|
||||
let value: Macro = serde_yaml::from_str(&content).with_context(err)?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub fn install_macros() -> Result<()> {
|
||||
info!(
|
||||
"Installing built-in macros in {}",
|
||||
Config::macros_dir().display()
|
||||
paths::macros_dir().display()
|
||||
);
|
||||
|
||||
for file in MacroAssets::iter() {
|
||||
@@ -71,7 +96,7 @@ impl Macro {
|
||||
let embedded_file = MacroAssets::get(&file)
|
||||
.ok_or_else(|| anyhow!("Failed to load embedded macro file: {}", file.as_ref()))?;
|
||||
let content = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
|
||||
let file_path = Config::macros_dir().join(file.as_ref());
|
||||
let file_path = paths::macros_dir().join(file.as_ref());
|
||||
|
||||
if file_path.exists() {
|
||||
debug!(
|
||||
@@ -144,3 +169,205 @@ pub struct MacroVariable {
|
||||
pub rest: bool,
|
||||
pub default: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn var(name: &str, rest: bool, default: Option<&str>) -> MacroVariable {
|
||||
MacroVariable {
|
||||
name: name.to_string(),
|
||||
rest,
|
||||
default: default.map(String::from),
|
||||
}
|
||||
}
|
||||
|
||||
fn macro_with_vars(vars: Vec<MacroVariable>) -> Macro {
|
||||
Macro {
|
||||
variables: vars,
|
||||
steps: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_no_variables() {
|
||||
let m = macro_with_vars(vec![]);
|
||||
let result = m.resolve_variables(&[]).unwrap();
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_required_variable_provided() {
|
||||
let m = macro_with_vars(vec![var("name", false, None)]);
|
||||
let result = m.resolve_variables(&["Alice".into()]).unwrap();
|
||||
assert_eq!(result["name"], "Alice");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_required_variable_missing_errors() {
|
||||
let m = macro_with_vars(vec![var("name", false, None)]);
|
||||
let result = m.resolve_variables(&[]);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("name"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_default_variable_uses_default() {
|
||||
let m = macro_with_vars(vec![var("color", false, Some("blue"))]);
|
||||
let result = m.resolve_variables(&[]).unwrap();
|
||||
assert_eq!(result["color"], "blue");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_default_variable_overridden() {
|
||||
let m = macro_with_vars(vec![var("color", false, Some("blue"))]);
|
||||
let result = m.resolve_variables(&["red".into()]).unwrap();
|
||||
assert_eq!(result["color"], "red");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_rest_variable_captures_all_remaining() {
|
||||
let m = macro_with_vars(vec![var("first", false, None), var("rest", true, None)]);
|
||||
let result = m
|
||||
.resolve_variables(&["a".into(), "b".into(), "c".into()])
|
||||
.unwrap();
|
||||
assert_eq!(result["first"], "a");
|
||||
assert_eq!(result["rest"], "b c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_rest_variable_with_default() {
|
||||
let m = macro_with_vars(vec![var("args", true, Some("default text"))]);
|
||||
let result = m.resolve_variables(&[]).unwrap();
|
||||
assert_eq!(result["args"], "default text");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_multiple_variables() {
|
||||
let m = macro_with_vars(vec![
|
||||
var("a", false, None),
|
||||
var("b", false, None),
|
||||
var("c", false, Some("default_c")),
|
||||
]);
|
||||
let result = m.resolve_variables(&["x".into(), "y".into()]).unwrap();
|
||||
assert_eq!(result["a"], "x");
|
||||
assert_eq!(result["b"], "y");
|
||||
assert_eq!(result["c"], "default_c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usage_no_variables() {
|
||||
let m = macro_with_vars(vec![]);
|
||||
assert_eq!(m.usage("my-macro"), "my-macro");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usage_required_variable() {
|
||||
let m = macro_with_vars(vec![var("name", false, None)]);
|
||||
assert_eq!(m.usage("greet"), "greet <name>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usage_optional_variable() {
|
||||
let m = macro_with_vars(vec![var("color", false, Some("blue"))]);
|
||||
assert_eq!(m.usage("paint"), "paint [color]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usage_rest_variable() {
|
||||
let m = macro_with_vars(vec![var("args", true, None)]);
|
||||
assert_eq!(m.usage("run"), "run <args>...");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usage_rest_with_default() {
|
||||
let m = macro_with_vars(vec![var("args", true, Some("default"))]);
|
||||
assert_eq!(m.usage("run"), "run [args]...");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usage_mixed_variables() {
|
||||
let m = macro_with_vars(vec![
|
||||
var("target", false, None),
|
||||
var("flags", true, Some("")),
|
||||
]);
|
||||
assert_eq!(m.usage("build"), "build <target> [flags]...");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interpolate_replaces_variables() {
|
||||
let vars = IndexMap::from([("name".to_string(), "world".to_string())]);
|
||||
let result = Macro::interpolate_command("hello {{name}}", &vars);
|
||||
assert_eq!(result, "hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interpolate_multiple_variables() {
|
||||
let vars = IndexMap::from([
|
||||
("a".to_string(), "1".to_string()),
|
||||
("b".to_string(), "2".to_string()),
|
||||
]);
|
||||
let result = Macro::interpolate_command("{{a}} + {{b}}", &vars);
|
||||
assert_eq!(result, "1 + 2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interpolate_no_variables_passthrough() {
|
||||
let vars = IndexMap::new();
|
||||
let result = Macro::interpolate_command("no vars here", &vars);
|
||||
assert_eq!(result, "no vars here");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interpolate_variable_not_found_left_as_is() {
|
||||
let vars = IndexMap::new();
|
||||
let result = Macro::interpolate_command("hello {{missing}}", &vars);
|
||||
assert_eq!(result, "hello {{missing}}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_macro_from_yaml() {
|
||||
let yaml = r#"
|
||||
steps:
|
||||
- ".role coder"
|
||||
- "write code for {{task}}"
|
||||
variables:
|
||||
- name: task
|
||||
"#;
|
||||
let m: Macro = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(m.steps.len(), 2);
|
||||
assert_eq!(m.variables.len(), 1);
|
||||
assert_eq!(m.variables[0].name, "task");
|
||||
assert!(!m.variables[0].rest);
|
||||
assert!(m.variables[0].default.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_macro_with_defaults() {
|
||||
let yaml = r#"
|
||||
steps:
|
||||
- "test"
|
||||
variables:
|
||||
- name: mode
|
||||
default: "fast"
|
||||
- name: args
|
||||
rest: true
|
||||
default: "none"
|
||||
"#;
|
||||
let m: Macro = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(m.variables[0].default, Some("fast".to_string()));
|
||||
assert!(m.variables[1].rest);
|
||||
assert_eq!(m.variables[1].default, Some("none".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_macro_no_variables() {
|
||||
let yaml = r#"
|
||||
steps:
|
||||
- ".help"
|
||||
"#;
|
||||
let m: Macro = serde_yaml::from_str(yaml).unwrap();
|
||||
assert!(m.variables.is_empty());
|
||||
assert_eq!(m.steps.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
use crate::mcp::{ConnectedServer, JsonField, McpServer, McpTransportType, spawn_mcp_server};
|
||||
|
||||
use anyhow::Result;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct McpServerKey {
|
||||
pub name: String,
|
||||
pub transport: McpTransportKey,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum McpTransportKey {
|
||||
Stdio {
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
env: Vec<(String, String)>,
|
||||
},
|
||||
Remote {
|
||||
transport_type: McpTransportType,
|
||||
url: String,
|
||||
headers: Vec<(String, String)>,
|
||||
},
|
||||
}
|
||||
|
||||
impl McpServerKey {
|
||||
pub fn from_spec(name: &str, spec: &McpServer) -> Self {
|
||||
let transport = if spec.is_remote() {
|
||||
let url = spec.url.clone().unwrap_or_default();
|
||||
let mut headers: Vec<(String, String)> = spec
|
||||
.headers
|
||||
.as_ref()
|
||||
.map(|h| h.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
|
||||
.unwrap_or_default();
|
||||
headers.sort();
|
||||
McpTransportKey::Remote {
|
||||
transport_type: spec.transport_type.clone(),
|
||||
url,
|
||||
headers,
|
||||
}
|
||||
} else {
|
||||
let command = spec.command.clone().unwrap_or_default();
|
||||
let mut args = spec.args.clone().unwrap_or_default();
|
||||
args.sort();
|
||||
let mut env: Vec<(String, String)> = spec
|
||||
.env
|
||||
.as_ref()
|
||||
.map(|e| {
|
||||
e.iter()
|
||||
.map(|(k, v)| {
|
||||
let v_str = match v {
|
||||
JsonField::Str(s) => s.clone(),
|
||||
JsonField::Bool(b) => b.to_string(),
|
||||
JsonField::Int(i) => i.to_string(),
|
||||
};
|
||||
(k.clone(), v_str)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
env.sort();
|
||||
McpTransportKey::Stdio { command, args, env }
|
||||
};
|
||||
Self {
|
||||
name: name.into(),
|
||||
transport,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct McpFactory {
|
||||
active: Mutex<HashMap<McpServerKey, Weak<ConnectedServer>>>,
|
||||
}
|
||||
|
||||
impl McpFactory {
|
||||
pub fn try_get_active(&self, key: &McpServerKey) -> Option<Arc<ConnectedServer>> {
|
||||
let map = self.active.lock();
|
||||
map.get(key).and_then(|weak| weak.upgrade())
|
||||
}
|
||||
|
||||
pub fn insert_active(&self, key: McpServerKey, handle: &Arc<ConnectedServer>) {
|
||||
let mut map = self.active.lock();
|
||||
map.insert(key, Arc::downgrade(handle));
|
||||
}
|
||||
|
||||
pub async fn acquire(
|
||||
&self,
|
||||
name: &str,
|
||||
spec: &McpServer,
|
||||
log_path: Option<&Path>,
|
||||
) -> Result<Arc<ConnectedServer>> {
|
||||
let key = McpServerKey::from_spec(name, spec);
|
||||
|
||||
if let Some(existing) = self.try_get_active(&key) {
|
||||
return Ok(existing);
|
||||
}
|
||||
|
||||
let handle = spawn_mcp_server(spec, log_path).await?;
|
||||
self.insert_active(key, &handle);
|
||||
Ok(handle)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mcp::{JsonField, McpServer, McpTransportType};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn stdio_spec(
|
||||
command: &str,
|
||||
args: Option<Vec<String>>,
|
||||
env: Option<HashMap<String, JsonField>>,
|
||||
) -> McpServer {
|
||||
McpServer {
|
||||
transport_type: McpTransportType::Stdio,
|
||||
command: Some(command.to_string()),
|
||||
args,
|
||||
env,
|
||||
cwd: None,
|
||||
url: None,
|
||||
headers: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_spec(
|
||||
transport: McpTransportType,
|
||||
url: &str,
|
||||
headers: Option<HashMap<String, String>>,
|
||||
) -> McpServer {
|
||||
McpServer {
|
||||
transport_type: transport,
|
||||
command: None,
|
||||
args: None,
|
||||
env: None,
|
||||
cwd: None,
|
||||
url: Some(url.to_string()),
|
||||
headers,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_from_stdio_spec_captures_command_args_env() {
|
||||
let mut env = HashMap::new();
|
||||
env.insert("TOKEN".into(), JsonField::Str("abc".into()));
|
||||
let spec = stdio_spec("npx", Some(vec!["-y".into(), "server".into()]), Some(env));
|
||||
let key = McpServerKey::from_spec("my-server", &spec);
|
||||
|
||||
assert_eq!(key.name, "my-server");
|
||||
match &key.transport {
|
||||
McpTransportKey::Stdio { command, args, env } => {
|
||||
assert_eq!(command, "npx");
|
||||
assert_eq!(args, &["-y", "server"]);
|
||||
assert_eq!(env, &[("TOKEN".to_string(), "abc".to_string())]);
|
||||
}
|
||||
_ => panic!("expected Stdio transport key"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_from_stdio_spec_sorts_args_and_env() {
|
||||
let mut env = HashMap::new();
|
||||
env.insert("Z_VAR".into(), JsonField::Str("z".into()));
|
||||
env.insert("A_VAR".into(), JsonField::Int(42));
|
||||
let spec = stdio_spec(
|
||||
"cmd",
|
||||
Some(vec!["charlie".into(), "alpha".into(), "bravo".into()]),
|
||||
Some(env),
|
||||
);
|
||||
let key = McpServerKey::from_spec("s", &spec);
|
||||
|
||||
match &key.transport {
|
||||
McpTransportKey::Stdio { args, env, .. } => {
|
||||
assert_eq!(args, &["alpha", "bravo", "charlie"]);
|
||||
assert_eq!(env[0].0, "A_VAR");
|
||||
assert_eq!(env[0].1, "42");
|
||||
assert_eq!(env[1].0, "Z_VAR");
|
||||
assert_eq!(env[1].1, "z");
|
||||
}
|
||||
_ => panic!("expected Stdio"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_from_stdio_spec_defaults_empty_when_none() {
|
||||
let spec = stdio_spec("echo", None, None);
|
||||
let key = McpServerKey::from_spec("bare", &spec);
|
||||
|
||||
match &key.transport {
|
||||
McpTransportKey::Stdio { command, args, env } => {
|
||||
assert_eq!(command, "echo");
|
||||
assert!(args.is_empty());
|
||||
assert!(env.is_empty());
|
||||
}
|
||||
_ => panic!("expected Stdio"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_from_remote_http_spec() {
|
||||
let spec = remote_spec(McpTransportType::Http, "http://localhost:8080", None);
|
||||
let key = McpServerKey::from_spec("http-srv", &spec);
|
||||
|
||||
assert_eq!(key.name, "http-srv");
|
||||
match &key.transport {
|
||||
McpTransportKey::Remote {
|
||||
transport_type,
|
||||
url,
|
||||
headers,
|
||||
} => {
|
||||
assert_eq!(*transport_type, McpTransportType::Http);
|
||||
assert_eq!(url, "http://localhost:8080");
|
||||
assert!(headers.is_empty());
|
||||
}
|
||||
_ => panic!("expected Remote"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_from_remote_sse_spec_with_sorted_headers() {
|
||||
let mut hdrs = HashMap::new();
|
||||
hdrs.insert("Z-Key".into(), "z-val".into());
|
||||
hdrs.insert("A-Key".into(), "a-val".into());
|
||||
let spec = remote_spec(McpTransportType::Sse, "http://sse.example.com", Some(hdrs));
|
||||
let key = McpServerKey::from_spec("sse-srv", &spec);
|
||||
|
||||
match &key.transport {
|
||||
McpTransportKey::Remote { headers, .. } => {
|
||||
assert_eq!(headers[0], ("A-Key".to_string(), "a-val".to_string()));
|
||||
assert_eq!(headers[1], ("Z-Key".to_string(), "z-val".to_string()));
|
||||
}
|
||||
_ => panic!("expected Remote"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_equality_same_spec_produces_equal_keys() {
|
||||
let spec = stdio_spec("npx", Some(vec!["a".into()]), None);
|
||||
let k1 = McpServerKey::from_spec("s", &spec);
|
||||
let k2 = McpServerKey::from_spec("s", &spec);
|
||||
assert_eq!(k1, k2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_inequality_different_names() {
|
||||
let spec = stdio_spec("npx", None, None);
|
||||
let k1 = McpServerKey::from_spec("a", &spec);
|
||||
let k2 = McpServerKey::from_spec("b", &spec);
|
||||
assert_ne!(k1, k2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_inequality_different_commands() {
|
||||
let s1 = stdio_spec("npx", None, None);
|
||||
let s2 = stdio_spec("node", None, None);
|
||||
let k1 = McpServerKey::from_spec("s", &s1);
|
||||
let k2 = McpServerKey::from_spec("s", &s2);
|
||||
assert_ne!(k1, k2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_env_bool_and_int_coerce_to_string() {
|
||||
let mut env = HashMap::new();
|
||||
env.insert("FLAG".into(), JsonField::Bool(true));
|
||||
env.insert("PORT".into(), JsonField::Int(3000));
|
||||
let spec = stdio_spec("cmd", None, Some(env));
|
||||
let key = McpServerKey::from_spec("s", &spec);
|
||||
|
||||
match &key.transport {
|
||||
McpTransportKey::Stdio { env, .. } => {
|
||||
let map: HashMap<&str, &str> =
|
||||
env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
assert_eq!(map["FLAG"], "true");
|
||||
assert_eq!(map["PORT"], "3000");
|
||||
}
|
||||
_ => panic!("expected Stdio"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_try_get_active_returns_none_when_empty() {
|
||||
let factory = McpFactory::default();
|
||||
let spec = stdio_spec("cmd", None, None);
|
||||
let key = McpServerKey::from_spec("s", &spec);
|
||||
assert!(factory.try_get_active(&key).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_try_get_active_returns_none_for_unknown_key() {
|
||||
let factory = McpFactory::default();
|
||||
let spec = stdio_spec("cmd", None, None);
|
||||
let key = McpServerKey::from_spec("s", &spec);
|
||||
assert!(factory.try_get_active(&key).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_default_has_empty_active_map() {
|
||||
let factory = McpFactory::default();
|
||||
let map = factory.active.lock();
|
||||
assert!(map.is_empty());
|
||||
}
|
||||
}
|
||||