Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 139b5cc57e |
@@ -1 +0,0 @@
|
|||||||
{"type":"rust","build":"cargo build","test":"cargo test","check":"cargo check","_detected_by":"heuristic","_cached_at":"2026-04-13T13:36:33-06:00"}
|
|
||||||
@@ -89,12 +89,7 @@ duct = "1.0.0"
|
|||||||
argc = "1.23.0"
|
argc = "1.23.0"
|
||||||
strum_macros = "0.27.2"
|
strum_macros = "0.27.2"
|
||||||
indoc = "2.0.6"
|
indoc = "2.0.6"
|
||||||
rmcp = { version = "1.5.0", features = [
|
rmcp = { version = "0.16.0", features = ["client", "transport-child-process"] }
|
||||||
"client",
|
|
||||||
"transport-child-process",
|
|
||||||
"transport-streamable-http-client-reqwest",
|
|
||||||
"reqwest-native-tls",
|
|
||||||
] }
|
|
||||||
num_cpus = "1.17.0"
|
num_cpus = "1.17.0"
|
||||||
tree-sitter = "0.26.8"
|
tree-sitter = "0.26.8"
|
||||||
tree-sitter-language = "0.1"
|
tree-sitter-language = "0.1"
|
||||||
@@ -125,7 +120,7 @@ default-features = false
|
|||||||
features = ["parsing", "regex-onig", "plist-load"]
|
features = ["parsing", "regex-onig", "plist-load"]
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
crossterm = { version = "0.29.0", features = ["use-dev-tty"] }
|
crossterm = { version = "0.28.1", features = ["use-dev-tty"] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
arboard = { version = "3.3.0", default-features = false, features = [
|
arboard = { version = "3.3.0", default-features = false, features = [
|
||||||
@@ -137,7 +132,6 @@ arboard = { version = "3.3.0", default-features = false }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = "1.4.0"
|
pretty_assertions = "1.4.0"
|
||||||
serial_test = "3"
|
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "loki"
|
name = "loki"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Loki: All-in-one, batteries-included LLM CLI Tool
|
# Loki: All-in-one, batteries-included LLM CLI Tool
|
||||||
|
|
||||||

|

|
||||||
|

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

|

|
||||||

|

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

|

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

|
||||||
|

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

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

|
||||||
|

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

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

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

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

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

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

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

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

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

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

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

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

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

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

|
||||||
|
|
||||||
|
You'll notice that mentioned above, some tools are disabled while others are enabled. How is that determined?
|
||||||
|
|
||||||
|
### Enabling/Disabling Global Tools
|
||||||
|
The configured tools are enabled/disabled by looking at the values in the `visible_tools` array in your `config.yaml`
|
||||||
|
file. This file is located in the root of the Loki `config` directory. The location of the Loki config varies by system,
|
||||||
|
so your config file can be found using the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
loki --info | grep 'config_file' | awk '{print $2}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Each line in the `visible_tools` array lists a tool.
|
||||||
|
|
||||||
|
If that line is commented out, then that tool is not included in the global tool set, and cannot be used in any context;
|
||||||
|
This means it will not be built, and even if enabled under `enabled_tools`, it still will not be available in any
|
||||||
|
context.
|
||||||
|
|
||||||
|
### Role Configuration
|
||||||
|
When you create a role, you have the following global tool-related configuration options available to you:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
enabled_tools: query_jira_issues # Which tools the role uses.
|
||||||
|
```
|
||||||
|
|
||||||
|
The values for `mapping_tools` are inherited from the [global configuration](#global-configuration).
|
||||||
|
|
||||||
|
For more information about roles, refer to the [Roles](../ROLES.md) documentation.
|
||||||
|
|
||||||
|
### Agent Configuration
|
||||||
|
When you create an agent, you have the following global tool-related configuration options available to you:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
global_tools: # Which global tools the agent uses
|
||||||
|
- query_jira_issues.sh
|
||||||
|
- fs_cat.sh
|
||||||
|
- fs_ls.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The values for `mapping_tools` are inherited from the [global configuration](#global-configuration).
|
||||||
|
|
||||||
|
For more information about agents, refer to the [Agents](../AGENTS.md) documentation.
|
||||||
|
|
||||||
|
For a full example configuration for an agent, see the [Agent Configuration Example](../../config.agent.example.yaml) file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tool Error Handling
|
||||||
|
When tools fail, Loki captures error information and passes it back to the model so it can diagnose issues and
|
||||||
|
potentially retry or adjust its approach.
|
||||||
|
|
||||||
|
### Native/Shell Tool Errors
|
||||||
|
When a shell-based tool exits with a non-zero exit code, the model receives:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool_call_error": "Tool call 'my_tool' exited with code 1",
|
||||||
|
"stderr": "Error: file not found: config.json"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `stderr` field contains the actual error output from the tool, giving the model context about what went wrong.
|
||||||
|
If the tool produces no stderr output, only the `tool_call_error` field is included.
|
||||||
|
|
||||||
|
**Note:** Tool stdout streams to your terminal in real-time so you can see progress. Only stderr is captured for
|
||||||
|
error reporting.
|
||||||
|
|
||||||
|
### MCP Tool Errors
|
||||||
|
When an MCP (Model Context Protocol) tool invocation fails due to connection issues, timeouts, or server errors,
|
||||||
|
the model receives:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool_call_error": "MCP tool invocation failed: connection refused"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows the model to understand that an external service failed and take appropriate action (retry, use an
|
||||||
|
alternative approach, or inform the user).
|
||||||
|
|
||||||
|
### Why This Matters
|
||||||
|
Without proper error propagation, models would only know that "something went wrong" without understanding *what*
|
||||||
|
went wrong. By including stderr output and detailed error messages, models can:
|
||||||
|
|
||||||
|
- Diagnose the root cause of failures
|
||||||
|
- Suggest fixes (e.g., "the file doesn't exist, should I create it?")
|
||||||
|
- Retry with corrected parameters
|
||||||
|
- Fall back to alternative approaches when appropriate
|
||||||
|
After Width: | Height: | Size: 370 KiB |
|
After Width: | Height: | Size: 587 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 446 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 878 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 303 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 396 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,416 @@
|
|||||||
|
# Sisyphus in LangChain/LangGraph
|
||||||
|
|
||||||
|
A faithful recreation of [Loki's Sisyphus agent](../../assets/agents/sisyphus/) using [LangGraph](https://docs.langchain.com/langgraph/) — LangChain's framework for stateful, multi-agent workflows.
|
||||||
|
|
||||||
|
This project exists to help you understand LangChain/LangGraph by mapping every concept to its Loki equivalent.
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ SUPERVISOR NODE │
|
||||||
|
│ Intent classification → Routing decision → Command(goto=) │
|
||||||
|
│ │
|
||||||
|
│ Loki equivalent: sisyphus/config.yaml │
|
||||||
|
│ (agent__spawn → Command, agent__collect → graph edge) │
|
||||||
|
└──────────┬──────────────┬──────────────┬────────────────────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌────────────┐ ┌────────────┐ ┌────────────┐
|
||||||
|
│ EXPLORE │ │ ORACLE │ │ CODER │
|
||||||
|
│ (research) │ │ (advise) │ │ (build) │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ read-only │ │ read-only │ │ read+write │
|
||||||
|
│ tools │ │ tools │ │ tools │
|
||||||
|
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
|
||||||
|
│ │ │
|
||||||
|
└──────────────┼──────────────┘
|
||||||
|
│
|
||||||
|
back to supervisor
|
||||||
|
```
|
||||||
|
|
||||||
|
## Concept Map: Loki → LangGraph
|
||||||
|
|
||||||
|
This is the key reference. Every row maps a Loki concept to its LangGraph equivalent.
|
||||||
|
|
||||||
|
### Core Architecture
|
||||||
|
|
||||||
|
| Loki Concept | LangGraph Equivalent | Where in Code |
|
||||||
|
|---|---|---|
|
||||||
|
| Agent config (config.yaml) | Node function + system prompt | `agents/explore.py`, etc. |
|
||||||
|
| Agent instructions | System prompt string | `EXPLORE_SYSTEM_PROMPT`, etc. |
|
||||||
|
| Agent tools (tools.sh) | `@tool`-decorated Python functions | `tools/filesystem.py`, `tools/project.py` |
|
||||||
|
| Agent session (chat loop) | Graph state + message list | `state.py` → `SisyphusState.messages` |
|
||||||
|
| `agent__spawn --agent X` | `Command(goto="X")` | `agents/supervisor.py` |
|
||||||
|
| `agent__collect --id` | Graph edge (implicit — workers return to supervisor) | `graph.py` → `add_edge("explore", "supervisor")` |
|
||||||
|
| `agent__check` (non-blocking) | Not needed (graph handles scheduling) | — |
|
||||||
|
| `agent__cancel` | Not needed (graph handles lifecycle) | — |
|
||||||
|
| `can_spawn_agents: true` | Node has routing logic (supervisor) | `agents/supervisor.py` |
|
||||||
|
| `max_concurrent_agents: 4` | `Send()` API for parallel fan-out | See [Parallel Execution](#parallel-execution) |
|
||||||
|
| `max_agent_depth: 3` | `recursion_limit` in config | `cli.py` → `recursion_limit: 50` |
|
||||||
|
| `summarization_threshold` | Manual truncation in supervisor | `supervisor.py` → `_summarize_outputs()` |
|
||||||
|
|
||||||
|
### Tool System
|
||||||
|
|
||||||
|
| Loki Concept | LangGraph Equivalent | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `tools.sh` with `@cmd` annotations | `@tool` decorator | Loki compiles bash annotations to JSON schema; LangChain generates schema from the Python function signature + docstring |
|
||||||
|
| `@option --pattern!` (required arg) | Function parameter without default | `def search_content(pattern: str)` |
|
||||||
|
| `@option --lines` (optional arg) | Parameter with default | `def read_file(path: str, limit: int = 200)` |
|
||||||
|
| `@env LLM_OUTPUT=/dev/stdout` | Return value | LangChain tools return strings; Loki tools write to `$LLM_OUTPUT` |
|
||||||
|
| `@describe` | Docstring | The tool's docstring becomes the description the LLM sees |
|
||||||
|
| Global tools (`fs_read.sh`, etc.) | Shared tool imports | Both agents import from `tools/filesystem.py` |
|
||||||
|
| Agent-specific tools | Per-node tool binding | `llm.bind_tools(EXPLORE_TOOLS)` vs `llm.bind_tools(CODER_TOOLS)` |
|
||||||
|
| `.shared/utils.sh` | `tools/project.py` | Shared project detection utilities |
|
||||||
|
| `detect_project()` heuristic | `detect_project()` in Python | Same logic: check Cargo.toml → go.mod → package.json → etc. |
|
||||||
|
| LLM fallback for unknown projects | (omitted) | The agents themselves can reason about unknown project types |
|
||||||
|
|
||||||
|
### State & Memory
|
||||||
|
|
||||||
|
| Loki Concept | LangGraph Equivalent | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Agent session (conversation history) | `SisyphusState.messages` | `Annotated[list, add_messages]` — the reducer appends instead of replacing |
|
||||||
|
| `agent_session: temp` | `MemorySaver` checkpointer | Loki's temp sessions are ephemeral; MemorySaver is in-memory (lost on restart) |
|
||||||
|
| Per-agent isolation | Per-node system prompt + tools | In Loki agents have separate sessions; in LangGraph they share messages but have different system prompts |
|
||||||
|
| `{{project_dir}}` variable | `SisyphusState.project_dir` | Loki interpolates variables into prompts; LangGraph stores them in state |
|
||||||
|
| `{{__tools__}}` injection | `llm.bind_tools()` | Loki injects tool descriptions into the prompt; LangChain attaches them to the API call |
|
||||||
|
|
||||||
|
### Orchestration
|
||||||
|
|
||||||
|
| Loki Concept | LangGraph Equivalent | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Intent classification table | `RoutingDecision` structured output | Loki does this in free text; LangGraph forces typed JSON |
|
||||||
|
| Oracle triggers ("How should I...") | Supervisor prompt + structured output | Same trigger phrases, enforced via system prompt |
|
||||||
|
| Coder delegation format | Supervisor builds HumanMessage | The structured prompt (Goal/Reference Files/Conventions/Constraints) |
|
||||||
|
| `agent__spawn` (parallel) | `Send()` API | Dynamic fan-out to multiple nodes |
|
||||||
|
| Todo system (`todo__init`, etc.) | `SisyphusState.todos` | State field with a merge reducer |
|
||||||
|
| `auto_continue: true` | Supervisor loop (iteration counter) | Supervisor re-routes until FINISH or max iterations |
|
||||||
|
| `max_auto_continues: 25` | `MAX_ITERATIONS = 15` | Safety valve to prevent infinite loops |
|
||||||
|
| `user__ask` / `user__confirm` | `interrupt()` API | Pauses graph, surfaces question to caller, resumes with answer |
|
||||||
|
| Escalation (child → parent → user) | `interrupt()` in any node | Any node can pause; the caller handles the interaction |
|
||||||
|
|
||||||
|
### Execution Model
|
||||||
|
|
||||||
|
| Loki Concept | LangGraph Equivalent | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `loki --agent sisyphus` | `python -m sisyphus_langchain.cli` | CLI entry point |
|
||||||
|
| REPL mode | `cli.py` → `repl()` | Interactive loop with thread persistence |
|
||||||
|
| One-shot mode | `cli.py` → `run_query()` | Single query, print result, exit |
|
||||||
|
| Streaming output | `graph.stream()` | LangGraph supports per-node streaming |
|
||||||
|
| `inject_spawn_instructions` | (always on) | System prompts are always included |
|
||||||
|
| `inject_todo_instructions` | (always on) | Todo instructions could be added to prompts |
|
||||||
|
|
||||||
|
## How the Execution Flow Works
|
||||||
|
|
||||||
|
### 1. User sends a message
|
||||||
|
|
||||||
|
```python
|
||||||
|
graph.invoke({"messages": [HumanMessage("Add a health check endpoint")]})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Supervisor classifies intent
|
||||||
|
|
||||||
|
The supervisor LLM reads the message and produces a `RoutingDecision`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"intent": "implementation",
|
||||||
|
"next_agent": "explore",
|
||||||
|
"delegation_notes": "Find existing API endpoint patterns, route structure, and health check conventions"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Supervisor routes via Command
|
||||||
|
|
||||||
|
```python
|
||||||
|
return Command(goto="explore", update={"intent": "implementation", "iteration_count": 1})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Explore agent runs
|
||||||
|
|
||||||
|
- Receives the full message history (including the user's request)
|
||||||
|
- Calls read-only tools (search_content, search_files, read_file)
|
||||||
|
- Returns findings in messages
|
||||||
|
|
||||||
|
### 5. Control returns to supervisor
|
||||||
|
|
||||||
|
The graph edge `explore → supervisor` fires automatically.
|
||||||
|
|
||||||
|
### 6. Supervisor reviews and routes again
|
||||||
|
|
||||||
|
Now it has explore's findings. It routes to coder with context:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"intent": "implementation",
|
||||||
|
"next_agent": "coder",
|
||||||
|
"delegation_notes": "Implement health check endpoint following patterns found in src/routes/"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Coder implements
|
||||||
|
|
||||||
|
- Reads explore's findings from the message history
|
||||||
|
- Writes files via `write_file` tool
|
||||||
|
- Runs `verify_build` to check compilation
|
||||||
|
|
||||||
|
### 8. Supervisor verifies and finishes
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"intent": "implementation",
|
||||||
|
"next_agent": "FINISH",
|
||||||
|
"delegation_notes": "Added /health endpoint in src/routes/health.py. Build passes."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Differences from Loki
|
||||||
|
|
||||||
|
### What LangGraph does better
|
||||||
|
|
||||||
|
1. **Declarative graph** — The topology is visible and debuggable. Loki's orchestration is emergent from the LLM's tool calls.
|
||||||
|
2. **Typed state** — `SisyphusState` is a TypedDict with reducers. Loki's state is implicit in the conversation.
|
||||||
|
3. **Checkpointing** — Built-in persistence. Loki manages sessions manually.
|
||||||
|
4. **Time-travel debugging** — Inspect any checkpoint. Loki has no equivalent.
|
||||||
|
5. **Structured routing** — `RoutingDecision` forces valid JSON. Loki relies on the LLM calling the right tool.
|
||||||
|
|
||||||
|
### What Loki does better
|
||||||
|
|
||||||
|
1. **True parallelism** — `agent__spawn` runs multiple agents concurrently in separate threads. This LangGraph implementation is sequential (see [Parallel Execution](#parallel-execution) for how to add it).
|
||||||
|
2. **Agent isolation** — Each Loki agent has its own session, tools, and config. LangGraph nodes share state.
|
||||||
|
3. **Teammate messaging** — Loki agents can send messages to siblings. LangGraph nodes communicate only through shared state.
|
||||||
|
4. **Dynamic tool compilation** — Loki compiles bash/python/typescript tools at startup. LangChain tools are statically defined.
|
||||||
|
5. **Escalation protocol** — Loki's child-to-parent escalation is sophisticated. LangGraph's `interrupt()` is simpler but less structured.
|
||||||
|
6. **Task queues with dependencies** — Loki's `agent__task_create` supports dependency DAGs. LangGraph's routing is simpler (hub-and-spoke).
|
||||||
|
|
||||||
|
## Running It
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Python 3.11+
|
||||||
|
python --version
|
||||||
|
|
||||||
|
# Set your API key
|
||||||
|
export OPENAI_API_KEY="sk-..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd examples/langchain-sisyphus
|
||||||
|
|
||||||
|
# With pip
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
# Or with uv (recommended)
|
||||||
|
uv pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive REPL (like `loki --agent sisyphus`)
|
||||||
|
sisyphus
|
||||||
|
|
||||||
|
# One-shot query
|
||||||
|
sisyphus "Find all TODO comments in the codebase"
|
||||||
|
|
||||||
|
# With custom models (cost optimization)
|
||||||
|
sisyphus --explore-model gpt-4o-mini --coder-model gpt-4o "Add input validation to the API"
|
||||||
|
|
||||||
|
# Programmatic usage
|
||||||
|
python -c "
|
||||||
|
from sisyphus_langchain import build_graph
|
||||||
|
from langchain_core.messages import HumanMessage
|
||||||
|
|
||||||
|
graph = build_graph()
|
||||||
|
result = graph.invoke({
|
||||||
|
'messages': [HumanMessage('What patterns does this codebase use?')],
|
||||||
|
'intent': 'ambiguous',
|
||||||
|
'next_agent': '',
|
||||||
|
'iteration_count': 0,
|
||||||
|
'todos': [],
|
||||||
|
'agent_outputs': {},
|
||||||
|
'final_output': '',
|
||||||
|
'project_dir': '.',
|
||||||
|
}, config={'configurable': {'thread_id': 'demo'}, 'recursion_limit': 50})
|
||||||
|
print(result['final_output'])
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Anthropic Models
|
||||||
|
|
||||||
|
Replace `ChatOpenAI` with `ChatAnthropic` in the agent factories:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from langchain_anthropic import ChatAnthropic
|
||||||
|
|
||||||
|
# In agents/oracle.py:
|
||||||
|
llm = ChatAnthropic(model="claude-sonnet-4-20250514", temperature=0.2).bind_tools(ORACLE_TOOLS)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Option 1: Standalone Script (Simplest)
|
||||||
|
|
||||||
|
Just run the CLI directly. No infrastructure needed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sisyphus "Add a health check endpoint"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: FastAPI Server
|
||||||
|
|
||||||
|
```python
|
||||||
|
# server.py
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from langserve import add_routes
|
||||||
|
from sisyphus_langchain import build_graph
|
||||||
|
|
||||||
|
app = FastAPI(title="Sisyphus API")
|
||||||
|
graph = build_graph()
|
||||||
|
add_routes(app, graph, path="/agent")
|
||||||
|
|
||||||
|
# Run: uvicorn server:app --host 0.0.0.0 --port 8000
|
||||||
|
# Call: POST http://localhost:8000/agent/invoke
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: LangGraph Platform (Production)
|
||||||
|
|
||||||
|
Create a `langgraph.json` at the project root:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"graphs": {
|
||||||
|
"sisyphus": "./sisyphus_langchain/graph.py:build_graph"
|
||||||
|
},
|
||||||
|
"dependencies": ["./sisyphus_langchain"],
|
||||||
|
"env": ".env"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then deploy:
|
||||||
|
```bash
|
||||||
|
pip install langgraph-cli
|
||||||
|
langgraph deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
This gives you:
|
||||||
|
- Durable checkpointing (PostgreSQL)
|
||||||
|
- Background runs
|
||||||
|
- Streaming API
|
||||||
|
- Zero-downtime deployments
|
||||||
|
- Built-in observability
|
||||||
|
|
||||||
|
### Option 4: Docker
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM python:3.12-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
RUN pip install -e .
|
||||||
|
CMD ["sisyphus"]
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t sisyphus .
|
||||||
|
docker run -it -e OPENAI_API_KEY=$OPENAI_API_KEY sisyphus
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parallel Execution
|
||||||
|
|
||||||
|
This implementation routes sequentially for simplicity. To add Loki-style parallel agent execution, use LangGraph's `Send()` API:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from langgraph.types import Send
|
||||||
|
|
||||||
|
def supervisor_node(state):
|
||||||
|
# Fan out to multiple explore agents in parallel
|
||||||
|
# (like Loki's agent__spawn called multiple times)
|
||||||
|
return [
|
||||||
|
Send("explore", {
|
||||||
|
**state,
|
||||||
|
"messages": state["messages"] + [
|
||||||
|
HumanMessage("Find existing API endpoint patterns")
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
Send("explore", {
|
||||||
|
**state,
|
||||||
|
"messages": state["messages"] + [
|
||||||
|
HumanMessage("Find data models and database patterns")
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
This is equivalent to Loki's pattern of spawning multiple explore agents:
|
||||||
|
```
|
||||||
|
agent__spawn --agent explore --prompt "Find API patterns"
|
||||||
|
agent__spawn --agent explore --prompt "Find database patterns"
|
||||||
|
agent__collect --id <id1>
|
||||||
|
agent__collect --id <id2>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding Human-in-the-Loop
|
||||||
|
|
||||||
|
To replicate Loki's `user__ask` / `user__confirm` tools, use LangGraph's `interrupt()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from langgraph.types import interrupt
|
||||||
|
|
||||||
|
def supervisor_node(state):
|
||||||
|
# Pause and ask the user (like Loki's user__ask)
|
||||||
|
answer = interrupt({
|
||||||
|
"question": "How should we structure the authentication?",
|
||||||
|
"options": [
|
||||||
|
"JWT with httpOnly cookies (Recommended)",
|
||||||
|
"Session-based with Redis",
|
||||||
|
"OAuth2 with external provider",
|
||||||
|
],
|
||||||
|
})
|
||||||
|
# `answer` contains the user's selection when the graph resumes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
examples/langchain-sisyphus/
|
||||||
|
├── pyproject.toml # Dependencies & build config
|
||||||
|
├── README.md # This file
|
||||||
|
└── sisyphus_langchain/
|
||||||
|
├── __init__.py # Package entry point
|
||||||
|
├── cli.py # CLI (REPL + one-shot mode)
|
||||||
|
├── graph.py # Graph assembly (wires nodes + edges)
|
||||||
|
├── state.py # Shared state schema (TypedDict)
|
||||||
|
├── agents/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── supervisor.py # Sisyphus orchestrator (intent → routing)
|
||||||
|
│ ├── explore.py # Read-only codebase researcher
|
||||||
|
│ ├── oracle.py # Architecture/debugging advisor
|
||||||
|
│ └── coder.py # Implementation worker
|
||||||
|
└── tools/
|
||||||
|
├── __init__.py
|
||||||
|
├── filesystem.py # File read/write/search/glob tools
|
||||||
|
└── project.py # Project detection, build, test tools
|
||||||
|
```
|
||||||
|
|
||||||
|
### File-to-Loki Mapping
|
||||||
|
|
||||||
|
| This Project | Loki Equivalent |
|
||||||
|
|---|---|
|
||||||
|
| `state.py` | Session context + todo state (implicit in Loki) |
|
||||||
|
| `graph.py` | `src/supervisor/mod.rs` (runtime orchestration) |
|
||||||
|
| `cli.py` | `src/main.rs` (CLI entry point) |
|
||||||
|
| `agents/supervisor.py` | `assets/agents/sisyphus/config.yaml` |
|
||||||
|
| `agents/explore.py` | `assets/agents/explore/config.yaml` + `tools.sh` |
|
||||||
|
| `agents/oracle.py` | `assets/agents/oracle/config.yaml` + `tools.sh` |
|
||||||
|
| `agents/coder.py` | `assets/agents/coder/config.yaml` + `tools.sh` |
|
||||||
|
| `tools/filesystem.py` | `assets/functions/tools/fs_*.sh` |
|
||||||
|
| `tools/project.py` | `assets/agents/.shared/utils.sh` + `sisyphus/tools.sh` |
|
||||||
|
|
||||||
|
## Further Reading
|
||||||
|
|
||||||
|
- [LangGraph Documentation](https://docs.langchain.com/langgraph/)
|
||||||
|
- [LangGraph Multi-Agent Tutorial](https://docs.langchain.com/langgraph/how-tos/multi-agent-systems)
|
||||||
|
- [Loki Agents Documentation](../../docs/AGENTS.md)
|
||||||
|
- [Loki Sisyphus README](../../assets/agents/sisyphus/README.md)
|
||||||
|
- [LangGraph Supervisor Library](https://github.com/langchain-ai/langgraph-supervisor-py)
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
[project]
|
||||||
|
name = "sisyphus-langchain"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Loki's Sisyphus multi-agent orchestrator recreated in LangChain/LangGraph"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
|
"langgraph>=0.3.0",
|
||||||
|
"langchain>=0.3.0",
|
||||||
|
"langchain-openai>=0.3.0",
|
||||||
|
"langchain-anthropic>=0.3.0",
|
||||||
|
"langchain-core>=0.3.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.0",
|
||||||
|
"ruff>=0.8.0",
|
||||||
|
]
|
||||||
|
server = [
|
||||||
|
"langgraph-api>=0.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
sisyphus = "sisyphus_langchain.cli:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""Sisyphus multi-agent orchestrator — a LangGraph recreation of Loki's Sisyphus agent."""
|
||||||
|
|
||||||
|
from sisyphus_langchain.graph import build_graph
|
||||||
|
|
||||||
|
__all__ = ["build_graph"]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Agent node definitions for the Sisyphus orchestrator."""
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
"""
|
||||||
|
Coder agent node — the implementation worker.
|
||||||
|
|
||||||
|
Loki equivalent: assets/agents/coder/config.yaml + tools.sh
|
||||||
|
|
||||||
|
In Loki, the coder is the ONLY agent that modifies files. It:
|
||||||
|
- Receives a structured prompt from sisyphus with code patterns to follow
|
||||||
|
- Writes files via the write_file tool (never pastes code in chat)
|
||||||
|
- Verifies builds after every change
|
||||||
|
- Signals CODER_COMPLETE or CODER_FAILED
|
||||||
|
|
||||||
|
In LangGraph, coder is a node with write-capable tools (read_file, write_file,
|
||||||
|
search_content, execute_command, verify_build). The supervisor formats a
|
||||||
|
structured delegation prompt (Goal / Reference Files / Code Patterns /
|
||||||
|
Conventions / Constraints) and routes to this node.
|
||||||
|
|
||||||
|
Key Loki→LangGraph mapping:
|
||||||
|
- Loki's "Coder Delegation Format" → the supervisor builds this as a
|
||||||
|
HumanMessage before routing to the coder node.
|
||||||
|
- Loki's auto_continue (up to 15) → the supervisor can re-route to coder
|
||||||
|
if verification fails, up to iteration_count limits.
|
||||||
|
- Loki's todo system for multi-file changes → the coder updates
|
||||||
|
state["todos"] as it completes each file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from langchain_core.messages import SystemMessage
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
|
||||||
|
from sisyphus_langchain.state import SisyphusState
|
||||||
|
from sisyphus_langchain.tools.filesystem import (
|
||||||
|
read_file,
|
||||||
|
search_content,
|
||||||
|
search_files,
|
||||||
|
write_file,
|
||||||
|
)
|
||||||
|
from sisyphus_langchain.tools.project import (
|
||||||
|
execute_command,
|
||||||
|
run_tests,
|
||||||
|
verify_build,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# System prompt — faithfully mirrors coder/config.yaml
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
CODER_SYSTEM_PROMPT = """\
|
||||||
|
You are a senior engineer. You write code that works on the first try.
|
||||||
|
|
||||||
|
## Your Mission
|
||||||
|
|
||||||
|
Given an implementation task:
|
||||||
|
1. Check for context provided in the conversation (patterns, conventions, reference files).
|
||||||
|
2. Fill gaps only — read files NOT already covered in context.
|
||||||
|
3. Write the code using the write_file tool (NEVER output code in chat).
|
||||||
|
4. Verify it compiles/builds using verify_build.
|
||||||
|
5. Provide a summary of what you implemented.
|
||||||
|
|
||||||
|
## Using Provided Context (IMPORTANT)
|
||||||
|
|
||||||
|
Your prompt often contains prior findings from the explore agent: file paths,
|
||||||
|
code patterns, and conventions.
|
||||||
|
|
||||||
|
**If context is provided:**
|
||||||
|
1. Use it as your primary reference. Don't re-read files already summarized.
|
||||||
|
2. Follow the code patterns shown — snippets in context ARE the style guide.
|
||||||
|
3. Read referenced files ONLY IF you need more detail (full signatures, imports).
|
||||||
|
4. If context includes a "Conventions" section, follow it exactly.
|
||||||
|
|
||||||
|
**If context is NOT provided or is too vague:**
|
||||||
|
Fall back to self-exploration: search for similar files, read 1-2 examples,
|
||||||
|
match their style.
|
||||||
|
|
||||||
|
## Writing Code
|
||||||
|
|
||||||
|
CRITICAL: Write code using the write_file tool. NEVER paste code in chat.
|
||||||
|
|
||||||
|
## Pattern Matching
|
||||||
|
|
||||||
|
Before writing ANY file:
|
||||||
|
1. Find a similar existing file.
|
||||||
|
2. Match its style: imports, naming, structure.
|
||||||
|
3. Follow the same patterns exactly.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
After writing files:
|
||||||
|
1. Run verify_build to check compilation.
|
||||||
|
2. If it fails, fix the error (minimal change).
|
||||||
|
3. Don't move on until build passes.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. Write code via tools — never output code to chat.
|
||||||
|
2. Follow patterns — read existing files first.
|
||||||
|
3. Verify builds — don't finish without checking.
|
||||||
|
4. Minimal fixes — if build fails, fix precisely.
|
||||||
|
5. No refactoring — only implement what's asked.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Full tool set — coder gets write access and command execution
|
||||||
|
CODER_TOOLS = [
|
||||||
|
read_file,
|
||||||
|
write_file,
|
||||||
|
search_content,
|
||||||
|
search_files,
|
||||||
|
execute_command,
|
||||||
|
verify_build,
|
||||||
|
run_tests,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def create_coder_node(model_name: str = "gpt-4o", temperature: float = 0.1):
|
||||||
|
"""
|
||||||
|
Factory that returns a coder node function.
|
||||||
|
|
||||||
|
Coder needs a capable model because it writes production code. In Loki,
|
||||||
|
coder uses the same model as the parent by default.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_name: Model identifier.
|
||||||
|
temperature: LLM temperature (Loki coder uses 0.1 for consistency).
|
||||||
|
"""
|
||||||
|
llm = ChatOpenAI(model=model_name, temperature=temperature).bind_tools(CODER_TOOLS)
|
||||||
|
|
||||||
|
def coder_node(state: SisyphusState) -> dict:
|
||||||
|
"""
|
||||||
|
LangGraph node: run the coder agent.
|
||||||
|
|
||||||
|
Reads conversation history (including the supervisor's structured
|
||||||
|
delegation prompt), invokes the LLM with write-capable tools,
|
||||||
|
and returns the result.
|
||||||
|
"""
|
||||||
|
response = llm.invoke(
|
||||||
|
[SystemMessage(content=CODER_SYSTEM_PROMPT)] + state["messages"]
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"messages": [response],
|
||||||
|
"agent_outputs": {
|
||||||
|
**state.get("agent_outputs", {}),
|
||||||
|
"coder": response.content,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return coder_node
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
"""
|
||||||
|
Explore agent node — the read-only codebase researcher.
|
||||||
|
|
||||||
|
Loki equivalent: assets/agents/explore/config.yaml + tools.sh
|
||||||
|
|
||||||
|
In Loki, the explore agent is spawned via `agent__spawn --agent explore --prompt "..."`
|
||||||
|
and runs as an isolated subprocess with its own session. It ends with
|
||||||
|
"EXPLORE_COMPLETE" so the parent knows it's finished.
|
||||||
|
|
||||||
|
In LangGraph, the explore agent is a *node* in the graph. The supervisor routes
|
||||||
|
to it via `Command(goto="explore")`. It reads the latest message (the supervisor's
|
||||||
|
delegation prompt), calls the LLM with read-only tools, and writes its findings
|
||||||
|
back to the shared message list. The graph edge then returns control to the
|
||||||
|
supervisor.
|
||||||
|
|
||||||
|
Key differences from Loki:
|
||||||
|
- No isolated session — shares the graph's message list (but has its own
|
||||||
|
system prompt and tool set, just like Loki's per-agent config).
|
||||||
|
- No "EXPLORE_COMPLETE" sentinel — the graph edge handles control flow.
|
||||||
|
- No output summarization — LangGraph's state handles context management.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from langchain_core.messages import SystemMessage
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
|
||||||
|
from sisyphus_langchain.state import SisyphusState
|
||||||
|
from sisyphus_langchain.tools.filesystem import (
|
||||||
|
list_directory,
|
||||||
|
read_file,
|
||||||
|
search_content,
|
||||||
|
search_files,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# System prompt — faithfully mirrors explore/config.yaml
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
EXPLORE_SYSTEM_PROMPT = """\
|
||||||
|
You are a codebase explorer. Your job: Search, find, report. Nothing else.
|
||||||
|
|
||||||
|
## Your Mission
|
||||||
|
|
||||||
|
Given a search task, you:
|
||||||
|
1. Search for relevant files and patterns
|
||||||
|
2. Read key files to understand structure
|
||||||
|
3. Report findings concisely
|
||||||
|
|
||||||
|
## Strategy
|
||||||
|
|
||||||
|
1. **Find first, read second** — Never read a file without knowing why.
|
||||||
|
2. **Use search_content to locate** — find exactly where things are defined.
|
||||||
|
3. **Use search_files to discover** — find files by name pattern.
|
||||||
|
4. **Read targeted sections** — use offset and limit to read only relevant lines.
|
||||||
|
5. **Never read entire large files** — if a file is 500+ lines, read the relevant section only.
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
Always end your response with a structured findings summary:
|
||||||
|
|
||||||
|
FINDINGS:
|
||||||
|
- [Key finding 1]
|
||||||
|
- [Key finding 2]
|
||||||
|
- Relevant files: [list of paths]
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. Be fast — don't read every file, read representative ones.
|
||||||
|
2. Be focused — answer the specific question asked.
|
||||||
|
3. Be concise — report findings, not your process.
|
||||||
|
4. Never modify files — you are read-only.
|
||||||
|
5. Limit reads — max 5 file reads per exploration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Read-only tools — mirrors explore's tool set (no write_file, no execute_command)
|
||||||
|
EXPLORE_TOOLS = [read_file, search_content, search_files, list_directory]
|
||||||
|
|
||||||
|
|
||||||
|
def create_explore_node(model_name: str = "gpt-4o-mini", temperature: float = 0.1):
|
||||||
|
"""
|
||||||
|
Factory that returns an explore node function bound to a specific model.
|
||||||
|
|
||||||
|
In Loki, the model is set per-agent in config.yaml. Here we parameterize it
|
||||||
|
so you can use a cheap model for exploration (cost optimization).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_name: OpenAI model identifier.
|
||||||
|
temperature: LLM temperature (Loki explore uses 0.1).
|
||||||
|
"""
|
||||||
|
llm = ChatOpenAI(model=model_name, temperature=temperature).bind_tools(EXPLORE_TOOLS)
|
||||||
|
|
||||||
|
def explore_node(state: SisyphusState) -> dict:
|
||||||
|
"""
|
||||||
|
LangGraph node: run the explore agent.
|
||||||
|
|
||||||
|
Reads the conversation history, applies the explore system prompt,
|
||||||
|
invokes the LLM with read-only tools, and returns the response.
|
||||||
|
"""
|
||||||
|
response = llm.invoke(
|
||||||
|
[SystemMessage(content=EXPLORE_SYSTEM_PROMPT)] + state["messages"]
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"messages": [response],
|
||||||
|
"agent_outputs": {
|
||||||
|
**state.get("agent_outputs", {}),
|
||||||
|
"explore": response.content,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return explore_node
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
"""
|
||||||
|
Oracle agent node — the high-IQ architecture and debugging advisor.
|
||||||
|
|
||||||
|
Loki equivalent: assets/agents/oracle/config.yaml + tools.sh
|
||||||
|
|
||||||
|
In Loki, the oracle is a READ-ONLY advisor spawned for:
|
||||||
|
- Architecture decisions and multi-system tradeoffs
|
||||||
|
- Complex debugging (after 2+ failed fix attempts)
|
||||||
|
- Code/design review
|
||||||
|
- Risk assessment
|
||||||
|
|
||||||
|
It uses temperature 0.2 (slightly higher than explore/coder for more creative
|
||||||
|
reasoning) and ends with "ORACLE_COMPLETE".
|
||||||
|
|
||||||
|
In LangGraph, oracle is a node that receives the full message history, reasons
|
||||||
|
about the problem, and writes structured advice back. It has read-only tools
|
||||||
|
only — it never modifies files.
|
||||||
|
|
||||||
|
Key Loki→LangGraph mapping:
|
||||||
|
- Loki oracle triggers (the "MUST spawn oracle when..." rules in sisyphus)
|
||||||
|
become routing conditions in the supervisor node.
|
||||||
|
- Oracle's structured output format (Analysis/Recommendation/Reasoning/Risks)
|
||||||
|
is enforced via the system prompt, same as in Loki.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from langchain_core.messages import SystemMessage
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
|
||||||
|
from sisyphus_langchain.state import SisyphusState
|
||||||
|
from sisyphus_langchain.tools.filesystem import (
|
||||||
|
list_directory,
|
||||||
|
read_file,
|
||||||
|
search_content,
|
||||||
|
search_files,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# System prompt — faithfully mirrors oracle/config.yaml
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
ORACLE_SYSTEM_PROMPT = """\
|
||||||
|
You are Oracle — a senior architect and debugger consulted for complex decisions.
|
||||||
|
|
||||||
|
## Your Role
|
||||||
|
|
||||||
|
You are READ-ONLY. You analyze, advise, and recommend. You do NOT implement.
|
||||||
|
|
||||||
|
## When You're Consulted
|
||||||
|
|
||||||
|
1. **Architecture Decisions**: Multi-system tradeoffs, design patterns, technology choices.
|
||||||
|
2. **Complex Debugging**: After 2+ failed fix attempts, deep analysis needed.
|
||||||
|
3. **Code Review**: Evaluating proposed designs or implementations.
|
||||||
|
4. **Risk Assessment**: Security, performance, or reliability concerns.
|
||||||
|
|
||||||
|
## Your Process
|
||||||
|
|
||||||
|
1. **Understand**: Read relevant code, understand the full context.
|
||||||
|
2. **Analyze**: Consider multiple angles and tradeoffs.
|
||||||
|
3. **Recommend**: Provide clear, actionable advice.
|
||||||
|
4. **Justify**: Explain your reasoning.
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
Structure your response as:
|
||||||
|
|
||||||
|
## Analysis
|
||||||
|
[Your understanding of the situation]
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
[Clear, specific advice]
|
||||||
|
|
||||||
|
## Reasoning
|
||||||
|
[Why this is the right approach]
|
||||||
|
|
||||||
|
## Risks/Considerations
|
||||||
|
[What to watch out for]
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. Never modify files — you advise, others implement.
|
||||||
|
2. Be thorough — read all relevant context before advising.
|
||||||
|
3. Be specific — general advice isn't helpful.
|
||||||
|
4. Consider tradeoffs — there are rarely perfect solutions.
|
||||||
|
5. Stay focused — answer the specific question asked.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Read-only tools — same set as explore (oracle never writes)
|
||||||
|
ORACLE_TOOLS = [read_file, search_content, search_files, list_directory]
|
||||||
|
|
||||||
|
|
||||||
|
def create_oracle_node(model_name: str = "gpt-4o", temperature: float = 0.2):
|
||||||
|
"""
|
||||||
|
Factory that returns an oracle node function.
|
||||||
|
|
||||||
|
Oracle uses a more expensive model than explore because it needs deeper
|
||||||
|
reasoning. In Loki, the model is inherited from the global config unless
|
||||||
|
overridden in oracle/config.yaml.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_name: Model identifier (use a strong reasoning model).
|
||||||
|
temperature: LLM temperature (Loki oracle uses 0.2).
|
||||||
|
"""
|
||||||
|
llm = ChatOpenAI(model=model_name, temperature=temperature).bind_tools(ORACLE_TOOLS)
|
||||||
|
|
||||||
|
def oracle_node(state: SisyphusState) -> dict:
|
||||||
|
"""
|
||||||
|
LangGraph node: run the oracle agent.
|
||||||
|
|
||||||
|
Reads conversation history, applies the oracle system prompt,
|
||||||
|
invokes the LLM, and returns structured advice.
|
||||||
|
"""
|
||||||
|
response = llm.invoke(
|
||||||
|
[SystemMessage(content=ORACLE_SYSTEM_PROMPT)] + state["messages"]
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"messages": [response],
|
||||||
|
"agent_outputs": {
|
||||||
|
**state.get("agent_outputs", {}),
|
||||||
|
"oracle": response.content,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return oracle_node
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
"""
|
||||||
|
Sisyphus supervisor node — the orchestrator that classifies intent and routes.
|
||||||
|
|
||||||
|
Loki equivalent: assets/agents/sisyphus/config.yaml
|
||||||
|
|
||||||
|
This is the brain of the system. In Loki, Sisyphus is the top-level agent that:
|
||||||
|
1. Classifies every incoming request (trivial / exploration / implementation /
|
||||||
|
architecture / ambiguous)
|
||||||
|
2. Routes to the appropriate sub-agent (explore, coder, oracle)
|
||||||
|
3. Manages the todo list for multi-step tasks
|
||||||
|
4. Verifies results and decides when the task is complete
|
||||||
|
|
||||||
|
In LangGraph, the supervisor is a node that returns `Command(goto="agent_name")`
|
||||||
|
to route control. This replaces Loki's `agent__spawn` + `agent__collect` pattern
|
||||||
|
with a declarative graph edge.
|
||||||
|
|
||||||
|
Key Loki→LangGraph mapping:
|
||||||
|
- agent__spawn --agent explore → Command(goto="explore")
|
||||||
|
- agent__spawn --agent coder → Command(goto="coder")
|
||||||
|
- agent__spawn --agent oracle → Command(goto="oracle")
|
||||||
|
- agent__check / agent__collect → (implicit: graph edges return to supervisor)
|
||||||
|
- todo__init / todo__add → state["todos"] updates
|
||||||
|
- user__ask / user__confirm → interrupt() for human-in-the-loop
|
||||||
|
|
||||||
|
Parallel execution note:
|
||||||
|
Loki can spawn multiple explore agents in parallel. In LangGraph, you'd use
|
||||||
|
the Send() API for dynamic fan-out. For simplicity, this implementation uses
|
||||||
|
sequential routing. See the README for how to add parallel fan-out.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from langchain_core.messages import SystemMessage
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
from langgraph.types import Command
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from sisyphus_langchain.state import SisyphusState
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Maximum iterations before forcing completion (safety valve)
|
||||||
|
# Mirrors Loki's max_auto_continues: 25
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
MAX_ITERATIONS = 15
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Structured output schema for the supervisor's routing decision.
|
||||||
|
#
|
||||||
|
# In Loki, the supervisor is an LLM that produces free-text and calls tools
|
||||||
|
# like agent__spawn. In LangGraph, we use structured output to force the
|
||||||
|
# LLM into a typed routing decision — more reliable than parsing free text.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class RoutingDecision(BaseModel):
|
||||||
|
"""The supervisor's decision about what to do next."""
|
||||||
|
|
||||||
|
intent: Literal["trivial", "exploration", "implementation", "architecture", "ambiguous"] = Field(
|
||||||
|
description="Classified intent of the user's request."
|
||||||
|
)
|
||||||
|
next_agent: Literal["explore", "oracle", "coder", "FINISH"] = Field(
|
||||||
|
description=(
|
||||||
|
"Which agent to route to. 'explore' for research/discovery, "
|
||||||
|
"'oracle' for architecture/design/debugging advice, "
|
||||||
|
"'coder' for implementation, 'FINISH' if the task is complete."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
delegation_notes: str = Field(
|
||||||
|
description=(
|
||||||
|
"Brief instructions for the target agent: what to look for (explore), "
|
||||||
|
"what to analyze (oracle), or what to implement (coder). "
|
||||||
|
"For FINISH, summarize what was accomplished."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Supervisor system prompt — faithfully mirrors sisyphus/config.yaml
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
SUPERVISOR_SYSTEM_PROMPT = """\
|
||||||
|
You are Sisyphus — an orchestrator that drives coding tasks to completion.
|
||||||
|
|
||||||
|
Your job: Classify → Delegate → Verify → Complete.
|
||||||
|
|
||||||
|
## Intent Classification (BEFORE every action)
|
||||||
|
|
||||||
|
| Type | Signal | Action |
|
||||||
|
|-----------------|-----------------------------------------------------|----------------------|
|
||||||
|
| trivial | Single file, known location, typo fix | Route to FINISH |
|
||||||
|
| exploration | "Find X", "Where is Y", "List all Z" | Route to explore |
|
||||||
|
| implementation | "Add feature", "Fix bug", "Write code" | Route to coder |
|
||||||
|
| architecture | See oracle triggers below | Route to oracle |
|
||||||
|
| ambiguous | Unclear scope, multiple interpretations | Route to FINISH with a clarifying question |
|
||||||
|
|
||||||
|
## Oracle Triggers (MUST route to oracle when you see these)
|
||||||
|
|
||||||
|
Route to oracle ANY time the user asks about:
|
||||||
|
- "How should I..." / "What's the best way to..." — design/approach questions
|
||||||
|
- "Why does X keep..." / "What's wrong with..." — complex debugging
|
||||||
|
- "Should I use X or Y?" — technology or pattern choices
|
||||||
|
- "How should this be structured?" — architecture
|
||||||
|
- "Review this" / "What do you think of..." — code/design review
|
||||||
|
- Tradeoff questions, multi-component questions, vague/open-ended questions
|
||||||
|
|
||||||
|
## Agent Specializations
|
||||||
|
|
||||||
|
| Agent | Use For |
|
||||||
|
|---------|-----------------------------------------------|
|
||||||
|
| explore | Find patterns, understand code, search |
|
||||||
|
| coder | Write/edit files, implement features |
|
||||||
|
| oracle | Architecture decisions, complex debugging |
|
||||||
|
|
||||||
|
## Workflow Patterns
|
||||||
|
|
||||||
|
### Implementation task: explore → coder
|
||||||
|
1. Route to explore to find existing patterns and conventions.
|
||||||
|
2. Review explore findings.
|
||||||
|
3. Route to coder with a structured prompt including the explore findings.
|
||||||
|
4. Verify the coder's output (check for CODER_COMPLETE or CODER_FAILED).
|
||||||
|
|
||||||
|
### Architecture question: explore + oracle
|
||||||
|
1. Route to explore to find relevant code.
|
||||||
|
2. Route to oracle with the explore findings for analysis.
|
||||||
|
|
||||||
|
### Simple question: oracle directly
|
||||||
|
For pure design/architecture questions, route to oracle directly.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. Always classify before acting.
|
||||||
|
2. You are a coordinator, not an implementer.
|
||||||
|
3. Route to oracle for ANY design/architecture question.
|
||||||
|
4. When routing to coder, include code patterns from explore findings.
|
||||||
|
5. Route to FINISH when the task is fully addressed.
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
Iteration: {iteration_count}/{max_iterations}
|
||||||
|
Previous agent outputs: {agent_outputs}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def create_supervisor_node(model_name: str = "gpt-4o", temperature: float = 0.1):
|
||||||
|
"""
|
||||||
|
Factory that returns a supervisor node function.
|
||||||
|
|
||||||
|
The supervisor uses a capable model for accurate routing.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_name: Model identifier.
|
||||||
|
temperature: LLM temperature (low for consistent routing).
|
||||||
|
"""
|
||||||
|
llm = ChatOpenAI(model=model_name, temperature=temperature).with_structured_output(
|
||||||
|
RoutingDecision
|
||||||
|
)
|
||||||
|
|
||||||
|
def supervisor_node(
|
||||||
|
state: SisyphusState,
|
||||||
|
) -> Command[Literal["explore", "oracle", "coder", "__end__"]]:
|
||||||
|
"""
|
||||||
|
LangGraph node: the Sisyphus supervisor.
|
||||||
|
|
||||||
|
Classifies the user's intent, decides which agent to route to,
|
||||||
|
and returns a Command that directs graph execution.
|
||||||
|
"""
|
||||||
|
iteration = state.get("iteration_count", 0)
|
||||||
|
|
||||||
|
# Safety valve — prevent infinite loops
|
||||||
|
if iteration >= MAX_ITERATIONS:
|
||||||
|
return Command(
|
||||||
|
goto="__end__",
|
||||||
|
update={
|
||||||
|
"final_output": "Reached maximum iterations. Here's what was accomplished:\n"
|
||||||
|
+ "\n".join(
|
||||||
|
f"- {k}: {v[:200]}" for k, v in state.get("agent_outputs", {}).items()
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Format the system prompt with current state
|
||||||
|
prompt = SUPERVISOR_SYSTEM_PROMPT.format(
|
||||||
|
iteration_count=iteration,
|
||||||
|
max_iterations=MAX_ITERATIONS,
|
||||||
|
agent_outputs=_summarize_outputs(state.get("agent_outputs", {})),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Invoke the LLM to get a structured routing decision
|
||||||
|
decision: RoutingDecision = llm.invoke(
|
||||||
|
[SystemMessage(content=prompt)] + state["messages"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Route to FINISH
|
||||||
|
if decision.next_agent == "FINISH":
|
||||||
|
return Command(
|
||||||
|
goto="__end__",
|
||||||
|
update={
|
||||||
|
"intent": decision.intent,
|
||||||
|
"next_agent": "FINISH",
|
||||||
|
"final_output": decision.delegation_notes,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Route to a worker agent
|
||||||
|
return Command(
|
||||||
|
goto=decision.next_agent,
|
||||||
|
update={
|
||||||
|
"intent": decision.intent,
|
||||||
|
"next_agent": decision.next_agent,
|
||||||
|
"iteration_count": iteration + 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return supervisor_node
|
||||||
|
|
||||||
|
|
||||||
|
def _summarize_outputs(outputs: dict[str, str]) -> str:
|
||||||
|
"""Summarize agent outputs for the supervisor's context window."""
|
||||||
|
if not outputs:
|
||||||
|
return "(none yet)"
|
||||||
|
parts = []
|
||||||
|
for agent, output in outputs.items():
|
||||||
|
# Truncate long outputs to keep supervisor context manageable
|
||||||
|
# This mirrors Loki's summarization_threshold behavior
|
||||||
|
if len(output) > 2000:
|
||||||
|
output = output[:2000] + "... (truncated)"
|
||||||
|
parts.append(f"[{agent}]: {output}")
|
||||||
|
return "\n\n".join(parts)
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
"""
|
||||||
|
CLI entry point for the Sisyphus LangChain agent.
|
||||||
|
|
||||||
|
This mirrors Loki's `loki --agent sisyphus` entry point.
|
||||||
|
|
||||||
|
In Loki:
|
||||||
|
loki --agent sisyphus
|
||||||
|
# Starts a REPL with the sisyphus agent loaded
|
||||||
|
|
||||||
|
In this LangChain version:
|
||||||
|
python -m sisyphus_langchain.cli
|
||||||
|
# or: sisyphus (if installed via pip)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
# Interactive REPL mode
|
||||||
|
sisyphus
|
||||||
|
|
||||||
|
# One-shot query
|
||||||
|
sisyphus "Add a health check endpoint to the API"
|
||||||
|
|
||||||
|
# With custom models
|
||||||
|
sisyphus --supervisor-model gpt-4o --explore-model gpt-4o-mini "Find auth patterns"
|
||||||
|
|
||||||
|
Environment variables:
|
||||||
|
OPENAI_API_KEY — Required for OpenAI models
|
||||||
|
ANTHROPIC_API_KEY — Required if using Anthropic models
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from langchain_core.messages import HumanMessage
|
||||||
|
|
||||||
|
from sisyphus_langchain.graph import build_graph
|
||||||
|
|
||||||
|
|
||||||
|
def run_query(graph, query: str, thread_id: str) -> str:
|
||||||
|
"""
|
||||||
|
Run a single query through the Sisyphus graph.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
graph: Compiled LangGraph.
|
||||||
|
query: User's natural language request.
|
||||||
|
thread_id: Session identifier for checkpointing.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The final output string.
|
||||||
|
"""
|
||||||
|
result = graph.invoke(
|
||||||
|
{
|
||||||
|
"messages": [HumanMessage(content=query)],
|
||||||
|
"intent": "ambiguous",
|
||||||
|
"next_agent": "",
|
||||||
|
"iteration_count": 0,
|
||||||
|
"todos": [],
|
||||||
|
"agent_outputs": {},
|
||||||
|
"final_output": "",
|
||||||
|
"project_dir": ".",
|
||||||
|
},
|
||||||
|
config={
|
||||||
|
"configurable": {"thread_id": thread_id},
|
||||||
|
"recursion_limit": 50,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return result.get("final_output", "(no output)")
|
||||||
|
|
||||||
|
|
||||||
|
def repl(graph, thread_id: str) -> None:
|
||||||
|
"""
|
||||||
|
Interactive REPL loop — mirrors Loki's REPL mode.
|
||||||
|
|
||||||
|
Maintains conversation across turns via the thread_id (checkpointer).
|
||||||
|
"""
|
||||||
|
print("Sisyphus (LangChain) — type 'quit' to exit")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
query = input("\n> ").strip()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print("\nBye.")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not query:
|
||||||
|
continue
|
||||||
|
if query.lower() in ("quit", "exit", "q"):
|
||||||
|
print("Bye.")
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
output = run_query(graph, query, thread_id)
|
||||||
|
print(f"\n{output}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nError: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""CLI entry point."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Sisyphus — multi-agent coding orchestrator (LangChain edition)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"query",
|
||||||
|
nargs="?",
|
||||||
|
help="One-shot query (omit for REPL mode)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--supervisor-model",
|
||||||
|
default="gpt-4o",
|
||||||
|
help="Model for the supervisor (default: gpt-4o)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--explore-model",
|
||||||
|
default="gpt-4o-mini",
|
||||||
|
help="Model for the explore agent (default: gpt-4o-mini)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--oracle-model",
|
||||||
|
default="gpt-4o",
|
||||||
|
help="Model for the oracle agent (default: gpt-4o)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--coder-model",
|
||||||
|
default="gpt-4o",
|
||||||
|
help="Model for the coder agent (default: gpt-4o)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--thread-id",
|
||||||
|
default=None,
|
||||||
|
help="Session thread ID for persistence (auto-generated if omitted)",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
graph = build_graph(
|
||||||
|
supervisor_model=args.supervisor_model,
|
||||||
|
explore_model=args.explore_model,
|
||||||
|
oracle_model=args.oracle_model,
|
||||||
|
coder_model=args.coder_model,
|
||||||
|
)
|
||||||
|
|
||||||
|
thread_id = args.thread_id or f"sisyphus-{uuid.uuid4().hex[:8]}"
|
||||||
|
|
||||||
|
if args.query:
|
||||||
|
output = run_query(graph, args.query, thread_id)
|
||||||
|
print(output)
|
||||||
|
else:
|
||||||
|
repl(graph, thread_id)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
"""
|
||||||
|
Graph assembly — wires together the supervisor and worker nodes.
|
||||||
|
|
||||||
|
This is the LangGraph equivalent of Loki's runtime agent execution engine
|
||||||
|
(src/supervisor/mod.rs + src/config/request_context.rs).
|
||||||
|
|
||||||
|
In Loki, the runtime:
|
||||||
|
1. Loads the agent config (config.yaml)
|
||||||
|
2. Compiles tools (tools.sh → binary)
|
||||||
|
3. Starts a chat loop: user → LLM → tool calls → LLM → ...
|
||||||
|
4. For orchestrators with can_spawn_agents: true, the supervisor module
|
||||||
|
manages child agent lifecycle (spawn, check, collect, cancel).
|
||||||
|
|
||||||
|
In LangGraph, all of this is declarative:
|
||||||
|
1. Define nodes (supervisor, explore, oracle, coder)
|
||||||
|
2. Define edges (workers always return to supervisor)
|
||||||
|
3. Compile the graph (with optional checkpointer for persistence)
|
||||||
|
4. Invoke with initial state
|
||||||
|
|
||||||
|
The graph topology:
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ SUPERVISOR │
|
||||||
|
│ (classifies intent, routes to workers) │
|
||||||
|
└─────┬──────────┬──────────┬─────────────────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌────────┐ ┌────────┐ ┌────────┐
|
||||||
|
│EXPLORE │ │ ORACLE │ │ CODER │
|
||||||
|
│(search)│ │(advise)│ │(build) │
|
||||||
|
└───┬────┘ └───┬────┘ └───┬────┘
|
||||||
|
│ │ │
|
||||||
|
└──────────┼──────────┘
|
||||||
|
│
|
||||||
|
(back to supervisor)
|
||||||
|
|
||||||
|
Every worker returns to the supervisor. The supervisor decides what to do next:
|
||||||
|
route to another worker, or end the graph.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from langgraph.checkpoint.memory import MemorySaver
|
||||||
|
from langgraph.graph import END, START, StateGraph
|
||||||
|
|
||||||
|
from sisyphus_langchain.agents.coder import create_coder_node
|
||||||
|
from sisyphus_langchain.agents.explore import create_explore_node
|
||||||
|
from sisyphus_langchain.agents.oracle import create_oracle_node
|
||||||
|
from sisyphus_langchain.agents.supervisor import create_supervisor_node
|
||||||
|
from sisyphus_langchain.state import SisyphusState
|
||||||
|
|
||||||
|
|
||||||
|
def build_graph(
|
||||||
|
*,
|
||||||
|
supervisor_model: str = "gpt-4o",
|
||||||
|
explore_model: str = "gpt-4o-mini",
|
||||||
|
oracle_model: str = "gpt-4o",
|
||||||
|
coder_model: str = "gpt-4o",
|
||||||
|
use_checkpointer: bool = True,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Build and compile the Sisyphus LangGraph.
|
||||||
|
|
||||||
|
This is the main entry point for creating the agent system. It wires
|
||||||
|
together all nodes and edges, optionally adds a checkpointer for
|
||||||
|
persistence, and returns a compiled graph ready to invoke.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
supervisor_model: Model for the routing supervisor.
|
||||||
|
explore_model: Model for the explore agent (can be cheaper).
|
||||||
|
oracle_model: Model for the oracle agent (should be strong).
|
||||||
|
coder_model: Model for the coder agent.
|
||||||
|
use_checkpointer: Whether to add MemorySaver for session persistence.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A compiled LangGraph ready to .invoke() or .stream().
|
||||||
|
|
||||||
|
Model cost optimization (mirrors Loki's per-agent model config):
|
||||||
|
- supervisor: expensive (accurate routing is critical)
|
||||||
|
- explore: cheap (just searching, not reasoning deeply)
|
||||||
|
- oracle: expensive (deep reasoning, architecture advice)
|
||||||
|
- coder: expensive (writing correct code matters)
|
||||||
|
"""
|
||||||
|
# Create the graph builder with our typed state
|
||||||
|
builder = StateGraph(SisyphusState)
|
||||||
|
|
||||||
|
# ── Register nodes ─────────────────────────────────────────────────
|
||||||
|
# Each node is a function that takes state and returns state updates.
|
||||||
|
# This mirrors Loki's agent registration (agents are discovered by
|
||||||
|
# their config.yaml in the agents/ directory).
|
||||||
|
builder.add_node("supervisor", create_supervisor_node(supervisor_model))
|
||||||
|
builder.add_node("explore", create_explore_node(explore_model))
|
||||||
|
builder.add_node("oracle", create_oracle_node(oracle_model))
|
||||||
|
builder.add_node("coder", create_coder_node(coder_model))
|
||||||
|
|
||||||
|
# ── Define edges ───────────────────────────────────────────────────
|
||||||
|
# Entry point: every invocation starts at the supervisor
|
||||||
|
builder.add_edge(START, "supervisor")
|
||||||
|
|
||||||
|
# Workers always return to supervisor (the hub-and-spoke pattern).
|
||||||
|
# In Loki, this is implicit: agent__collect returns output to the parent,
|
||||||
|
# and the parent (sisyphus) decides what to do next.
|
||||||
|
builder.add_edge("explore", "supervisor")
|
||||||
|
builder.add_edge("oracle", "supervisor")
|
||||||
|
builder.add_edge("coder", "supervisor")
|
||||||
|
|
||||||
|
# The supervisor node itself uses Command(goto=...) to route,
|
||||||
|
# so we don't need add_conditional_edges — the Command API
|
||||||
|
# handles dynamic routing internally.
|
||||||
|
|
||||||
|
# ── Compile ────────────────────────────────────────────────────────
|
||||||
|
checkpointer = MemorySaver() if use_checkpointer else None
|
||||||
|
graph = builder.compile(checkpointer=checkpointer)
|
||||||
|
|
||||||
|
return graph
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
"""
|
||||||
|
Shared state schema for the Sisyphus orchestrator graph.
|
||||||
|
|
||||||
|
In LangGraph, state is the single source of truth that flows through every node.
|
||||||
|
This is analogous to Loki's per-agent session context, but unified into one typed
|
||||||
|
dictionary that the entire graph shares.
|
||||||
|
|
||||||
|
Loki Concept Mapping:
|
||||||
|
- Loki session context → SisyphusState (TypedDict)
|
||||||
|
- Loki todo__init / todo__add → SisyphusState.todos list
|
||||||
|
- Loki agent__spawn outputs → SisyphusState.agent_outputs dict
|
||||||
|
- Loki intent classification → SisyphusState.intent field
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Annotated, Literal
|
||||||
|
|
||||||
|
from langchain_core.messages import BaseMessage
|
||||||
|
from langgraph.graph.message import add_messages
|
||||||
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Intent types — mirrors Loki's Sisyphus classification table
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
IntentType = Literal[
|
||||||
|
"trivial", # Single file, known location, typo fix → handle yourself
|
||||||
|
"exploration", # "Find X", "Where is Y" → spawn explore
|
||||||
|
"implementation", # "Add feature", "Fix bug" → spawn coder
|
||||||
|
"architecture", # Design questions, oracle triggers → spawn oracle
|
||||||
|
"ambiguous", # Unclear scope → ask user
|
||||||
|
]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Todo item — mirrors Loki's built-in todo system
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@dataclass
|
||||||
|
class TodoItem:
|
||||||
|
"""A single task in the orchestrator's todo list."""
|
||||||
|
id: int
|
||||||
|
task: str
|
||||||
|
done: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_todos(existing: list[TodoItem], new: list[TodoItem]) -> list[TodoItem]:
|
||||||
|
"""
|
||||||
|
Reducer for the todos field.
|
||||||
|
|
||||||
|
LangGraph requires a reducer for any state field that can be written by
|
||||||
|
multiple nodes. This merges by id: if a todo with the same id already
|
||||||
|
exists, the incoming version wins (allows marking done).
|
||||||
|
"""
|
||||||
|
by_id = {t.id: t for t in existing}
|
||||||
|
for t in new:
|
||||||
|
by_id[t.id] = t
|
||||||
|
return list(by_id.values())
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Core graph state
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class SisyphusState(TypedDict):
|
||||||
|
"""
|
||||||
|
The shared state that flows through every node in the Sisyphus graph.
|
||||||
|
|
||||||
|
Annotated fields use *reducers* — functions that merge concurrent writes.
|
||||||
|
Without reducers, parallel node outputs would overwrite each other.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Conversation history — the `add_messages` reducer appends new messages
|
||||||
|
# instead of replacing the list. This is critical: every node adds its
|
||||||
|
# response here, and downstream nodes see the full history.
|
||||||
|
#
|
||||||
|
# Loki equivalent: each agent's chat session accumulates messages the same
|
||||||
|
# way, but messages are scoped per-agent. In LangGraph the shared message
|
||||||
|
# list IS the inter-agent communication channel.
|
||||||
|
messages: Annotated[list[BaseMessage], add_messages]
|
||||||
|
|
||||||
|
# Classified intent for the current request
|
||||||
|
intent: IntentType
|
||||||
|
|
||||||
|
# Which agent the supervisor routed to last
|
||||||
|
next_agent: str
|
||||||
|
|
||||||
|
# Iteration counter — safety valve analogous to Loki's max_auto_continues
|
||||||
|
iteration_count: int
|
||||||
|
|
||||||
|
# Todo list for multi-step tracking (mirrors Loki's todo__* tools)
|
||||||
|
todos: Annotated[list[TodoItem], _merge_todos]
|
||||||
|
|
||||||
|
# Accumulated outputs from sub-agent nodes, keyed by agent name.
|
||||||
|
# The supervisor reads these to decide what to do next.
|
||||||
|
agent_outputs: dict[str, str]
|
||||||
|
|
||||||
|
# Final synthesized answer to return to the user
|
||||||
|
final_output: str
|
||||||
|
|
||||||
|
# The working directory / project path (mirrors Loki's project_dir variable)
|
||||||
|
project_dir: str
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Tool definitions for Sisyphus agents."""
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
"""
|
||||||
|
Filesystem tools for Sisyphus agents.
|
||||||
|
|
||||||
|
These are the LangChain equivalents of Loki's global tools:
|
||||||
|
- fs_read.sh → read_file
|
||||||
|
- fs_grep.sh → search_content
|
||||||
|
- fs_glob.sh → search_files
|
||||||
|
- fs_ls.sh → list_directory
|
||||||
|
- fs_write.sh → write_file
|
||||||
|
- fs_patch.sh → (omitted — write_file covers full rewrites)
|
||||||
|
|
||||||
|
Loki Concept Mapping:
|
||||||
|
Loki tools are bash scripts with @cmd annotations that Loki's compiler
|
||||||
|
turns into function-calling declarations. In LangChain, we use the @tool
|
||||||
|
decorator which serves the same purpose: it generates the JSON schema
|
||||||
|
that the LLM sees, and wraps the Python function for execution.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import fnmatch
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from langchain_core.tools import tool
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def read_file(path: str, offset: int = 1, limit: int = 200) -> str:
|
||||||
|
"""Read a file's contents with optional line range.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Path to the file (absolute or relative to cwd).
|
||||||
|
offset: 1-based line number to start from.
|
||||||
|
limit: Maximum number of lines to return.
|
||||||
|
"""
|
||||||
|
path = os.path.expanduser(path)
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
return f"Error: file not found: {path}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error reading {path}: {e}"
|
||||||
|
|
||||||
|
total = len(lines)
|
||||||
|
start = max(0, offset - 1)
|
||||||
|
end = min(total, start + limit)
|
||||||
|
selected = lines[start:end]
|
||||||
|
|
||||||
|
result = f"File: {path} (lines {start + 1}-{end} of {total})\n\n"
|
||||||
|
for i, line in enumerate(selected, start=start + 1):
|
||||||
|
result += f"{i}: {line}"
|
||||||
|
|
||||||
|
if end < total:
|
||||||
|
result += f"\n... truncated ({total} total lines)"
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def write_file(path: str, content: str) -> str:
|
||||||
|
"""Write complete contents to a file, creating parent directories as needed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Path for the file.
|
||||||
|
content: Complete file contents to write.
|
||||||
|
"""
|
||||||
|
path = os.path.expanduser(path)
|
||||||
|
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
||||||
|
try:
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
return f"Wrote: {path}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error writing {path}: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def search_content(pattern: str, directory: str = ".", file_type: str = "") -> str:
|
||||||
|
"""Search for a text/regex pattern in files under a directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pattern: Text or regex pattern to search for.
|
||||||
|
directory: Root directory to search in.
|
||||||
|
file_type: Optional file extension filter (e.g. "py", "rs").
|
||||||
|
"""
|
||||||
|
directory = os.path.expanduser(directory)
|
||||||
|
cmd = ["grep", "-rn"]
|
||||||
|
if file_type:
|
||||||
|
cmd += [f"--include=*.{file_type}"]
|
||||||
|
cmd += [pattern, directory]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||||
|
lines = result.stdout.strip().splitlines()
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error: {e}"
|
||||||
|
|
||||||
|
# Filter noise
|
||||||
|
noise = {"/.git/", "/node_modules/", "/target/", "/dist/", "/__pycache__/"}
|
||||||
|
filtered = [l for l in lines if not any(n in l for n in noise)][:30]
|
||||||
|
|
||||||
|
if not filtered:
|
||||||
|
return "No matches found."
|
||||||
|
return "\n".join(filtered)
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def search_files(pattern: str, directory: str = ".") -> str:
|
||||||
|
"""Find files matching a glob pattern.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pattern: Glob pattern (e.g. '*.py', 'config*', '*test*').
|
||||||
|
directory: Directory to search in.
|
||||||
|
"""
|
||||||
|
directory = os.path.expanduser(directory)
|
||||||
|
noise = {".git", "node_modules", "target", "dist", "__pycache__"}
|
||||||
|
matches: list[str] = []
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(directory):
|
||||||
|
dirs[:] = [d for d in dirs if d not in noise]
|
||||||
|
for name in files:
|
||||||
|
if fnmatch.fnmatch(name, pattern):
|
||||||
|
matches.append(os.path.join(root, name))
|
||||||
|
if len(matches) >= 25:
|
||||||
|
break
|
||||||
|
if len(matches) >= 25:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not matches:
|
||||||
|
return "No files found."
|
||||||
|
return "\n".join(matches)
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def list_directory(path: str = ".", max_depth: int = 3) -> str:
|
||||||
|
"""List directory tree structure.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Directory to list.
|
||||||
|
max_depth: Maximum depth to recurse.
|
||||||
|
"""
|
||||||
|
path = os.path.expanduser(path)
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
return f"Error: not a directory: {path}"
|
||||||
|
|
||||||
|
noise = {".git", "node_modules", "target", "dist", "__pycache__", ".venv", "venv"}
|
||||||
|
lines: list[str] = []
|
||||||
|
|
||||||
|
def _walk(dir_path: str, prefix: str, depth: int) -> None:
|
||||||
|
if depth > max_depth:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
entries = sorted(os.listdir(dir_path))
|
||||||
|
except PermissionError:
|
||||||
|
return
|
||||||
|
|
||||||
|
dirs = [e for e in entries if os.path.isdir(os.path.join(dir_path, e)) and e not in noise]
|
||||||
|
files = [e for e in entries if os.path.isfile(os.path.join(dir_path, e))]
|
||||||
|
|
||||||
|
for f in files[:20]:
|
||||||
|
lines.append(f"{prefix}{f}")
|
||||||
|
if len(files) > 20:
|
||||||
|
lines.append(f"{prefix}... ({len(files) - 20} more files)")
|
||||||
|
|
||||||
|
for d in dirs:
|
||||||
|
lines.append(f"{prefix}{d}/")
|
||||||
|
_walk(os.path.join(dir_path, d), prefix + " ", depth + 1)
|
||||||
|
|
||||||
|
lines.append(f"{os.path.basename(path) or path}/")
|
||||||
|
_walk(path, " ", 1)
|
||||||
|
return "\n".join(lines[:200])
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
"""
|
||||||
|
Project detection and build/test tools.
|
||||||
|
|
||||||
|
These mirror Loki's .shared/utils.sh detect_project() heuristic and the
|
||||||
|
sisyphus/coder tools.sh run_build / run_tests / verify_build commands.
|
||||||
|
|
||||||
|
Loki Concept Mapping:
|
||||||
|
Loki uses a heuristic cascade: check for Cargo.toml → go.mod → package.json
|
||||||
|
etc., then falls back to an LLM call for unknown projects. We replicate the
|
||||||
|
heuristic portion here. The LLM fallback is omitted since the agents
|
||||||
|
themselves can reason about unknown project types.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from langchain_core.tools import tool
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Project detection (mirrors _detect_heuristic in utils.sh)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
_HEURISTICS: list[tuple[str, dict[str, str]]] = [
|
||||||
|
("Cargo.toml", {"type": "rust", "build": "cargo build", "test": "cargo test", "check": "cargo check"}),
|
||||||
|
("go.mod", {"type": "go", "build": "go build ./...", "test": "go test ./...", "check": "go vet ./..."}),
|
||||||
|
("package.json", {"type": "nodejs", "build": "npm run build", "test": "npm test", "check": "npm run lint"}),
|
||||||
|
("pyproject.toml", {"type": "python", "build": "", "test": "pytest", "check": "ruff check ."}),
|
||||||
|
("pom.xml", {"type": "java", "build": "mvn compile", "test": "mvn test", "check": "mvn verify"}),
|
||||||
|
("Makefile", {"type": "make", "build": "make build", "test": "make test", "check": "make lint"}),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def detect_project(directory: str) -> dict[str, str]:
|
||||||
|
"""Detect project type and return build/test commands."""
|
||||||
|
for marker, info in _HEURISTICS:
|
||||||
|
if os.path.exists(os.path.join(directory, marker)):
|
||||||
|
return info
|
||||||
|
return {"type": "unknown", "build": "", "test": "", "check": ""}
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def get_project_info(directory: str = ".") -> str:
|
||||||
|
"""Detect the project type and show structure overview.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
directory: Project root directory.
|
||||||
|
"""
|
||||||
|
directory = os.path.expanduser(directory)
|
||||||
|
info = detect_project(directory)
|
||||||
|
result = f"Project: {os.path.abspath(directory)}\n"
|
||||||
|
result += f"Type: {info['type']}\n"
|
||||||
|
result += f"Build: {info['build'] or '(none)'}\n"
|
||||||
|
result += f"Test: {info['test'] or '(none)'}\n"
|
||||||
|
result += f"Check: {info['check'] or '(none)'}\n"
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _run_project_command(directory: str, command_key: str) -> str:
|
||||||
|
"""Run a detected project command (build/test/check)."""
|
||||||
|
directory = os.path.expanduser(directory)
|
||||||
|
info = detect_project(directory)
|
||||||
|
cmd = info.get(command_key, "")
|
||||||
|
|
||||||
|
if not cmd:
|
||||||
|
return f"No {command_key} command detected for this project."
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
shell=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
cwd=directory,
|
||||||
|
timeout=300,
|
||||||
|
)
|
||||||
|
output = result.stdout + result.stderr
|
||||||
|
status = "SUCCESS" if result.returncode == 0 else f"FAILED (exit {result.returncode})"
|
||||||
|
return f"Running: {cmd}\n\n{output}\n\n{command_key.upper()}: {status}"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return f"{command_key.upper()}: TIMEOUT after 300s"
|
||||||
|
except Exception as e:
|
||||||
|
return f"{command_key.upper()}: ERROR — {e}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def run_build(directory: str = ".") -> str:
|
||||||
|
"""Run the project's build command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
directory: Project root directory.
|
||||||
|
"""
|
||||||
|
return _run_project_command(directory, "build")
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def run_tests(directory: str = ".") -> str:
|
||||||
|
"""Run the project's test suite.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
directory: Project root directory.
|
||||||
|
"""
|
||||||
|
return _run_project_command(directory, "test")
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def verify_build(directory: str = ".") -> str:
|
||||||
|
"""Run the project's check/lint command to verify correctness.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
directory: Project root directory.
|
||||||
|
"""
|
||||||
|
return _run_project_command(directory, "check")
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def execute_command(command: str, directory: str = ".") -> str:
|
||||||
|
"""Execute a shell command and return its output.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: Shell command to execute.
|
||||||
|
directory: Working directory.
|
||||||
|
"""
|
||||||
|
directory = os.path.expanduser(directory)
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
command,
|
||||||
|
shell=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
cwd=directory,
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
output = (result.stdout + result.stderr).strip()
|
||||||
|
if result.returncode != 0:
|
||||||
|
return f"Command failed (exit {result.returncode}):\n{output}"
|
||||||
|
return output or "(no output)"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return "Command timed out after 120s."
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error: {e}"
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
use crate::client::{ModelType, list_models};
|
use crate::client::{ModelType, list_models};
|
||||||
use crate::config::paths;
|
use crate::config::{Config, list_agents};
|
||||||
use crate::config::{AppConfig, Config, list_agents, list_sessions};
|
|
||||||
use crate::vault::Vault;
|
|
||||||
use clap_complete::{CompletionCandidate, Shell, generate};
|
use clap_complete::{CompletionCandidate, Shell, generate};
|
||||||
use clap_complete_nushell::Nushell;
|
use clap_complete_nushell::Nushell;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
@@ -34,8 +32,8 @@ impl ShellCompletion {
|
|||||||
|
|
||||||
pub(super) fn model_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
pub(super) fn model_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||||
let cur = current.to_string_lossy();
|
let cur = current.to_string_lossy();
|
||||||
match load_app_config_for_completion() {
|
match Config::init_bare() {
|
||||||
Ok(app_config) => list_models(&app_config, ModelType::Chat)
|
Ok(config) => list_models(&config, ModelType::Chat)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|&m| m.id().starts_with(&*cur))
|
.filter(|&m| m.id().starts_with(&*cur))
|
||||||
.map(|m| CompletionCandidate::new(m.id()))
|
.map(|m| CompletionCandidate::new(m.id()))
|
||||||
@@ -44,23 +42,9 @@ pub(super) fn model_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_app_config_for_completion() -> anyhow::Result<AppConfig> {
|
|
||||||
let h = tokio::runtime::Handle::try_current().ok();
|
|
||||||
let cfg = match h {
|
|
||||||
Some(handle) => {
|
|
||||||
tokio::task::block_in_place(|| handle.block_on(Config::load_with_interpolation(true)))?
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let rt = tokio::runtime::Runtime::new()?;
|
|
||||||
rt.block_on(Config::load_with_interpolation(true))?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
AppConfig::from_config(cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn role_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
pub(super) fn role_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||||
let cur = current.to_string_lossy();
|
let cur = current.to_string_lossy();
|
||||||
paths::list_roles(true)
|
Config::list_roles(true)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|r| r.starts_with(&*cur))
|
.filter(|r| r.starts_with(&*cur))
|
||||||
.map(CompletionCandidate::new)
|
.map(CompletionCandidate::new)
|
||||||
@@ -78,7 +62,7 @@ pub(super) fn agent_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
|||||||
|
|
||||||
pub(super) fn rag_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
pub(super) fn rag_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||||
let cur = current.to_string_lossy();
|
let cur = current.to_string_lossy();
|
||||||
paths::list_rags()
|
Config::list_rags()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|r| r.starts_with(&*cur))
|
.filter(|r| r.starts_with(&*cur))
|
||||||
.map(CompletionCandidate::new)
|
.map(CompletionCandidate::new)
|
||||||
@@ -87,7 +71,7 @@ pub(super) fn rag_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
|||||||
|
|
||||||
pub(super) fn macro_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
pub(super) fn macro_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||||
let cur = current.to_string_lossy();
|
let cur = current.to_string_lossy();
|
||||||
paths::list_macros()
|
Config::list_macros()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|m| m.starts_with(&*cur))
|
.filter(|m| m.starts_with(&*cur))
|
||||||
.map(CompletionCandidate::new)
|
.map(CompletionCandidate::new)
|
||||||
@@ -96,17 +80,22 @@ pub(super) fn macro_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
|||||||
|
|
||||||
pub(super) fn session_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
pub(super) fn session_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||||
let cur = current.to_string_lossy();
|
let cur = current.to_string_lossy();
|
||||||
list_sessions()
|
match Config::init_bare() {
|
||||||
|
Ok(config) => config
|
||||||
|
.list_sessions()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|s| s.starts_with(&*cur))
|
.filter(|s| s.starts_with(&*cur))
|
||||||
.map(CompletionCandidate::new)
|
.map(CompletionCandidate::new)
|
||||||
.collect()
|
.collect(),
|
||||||
|
Err(_) => vec![],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn secrets_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
pub(super) fn secrets_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||||
let cur = current.to_string_lossy();
|
let cur = current.to_string_lossy();
|
||||||
match load_app_config_for_completion() {
|
match Config::init_bare() {
|
||||||
Ok(app_config) => Vault::init(&app_config)
|
Ok(config) => config
|
||||||
|
.vault
|
||||||
.list_secrets(false)
|
.list_secrets(false)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -176,220 +176,3 @@ impl Cli {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use clap::Parser;
|
|
||||||
|
|
||||||
fn parse(args: &[&str]) -> Cli {
|
|
||||||
let mut full_args = vec!["loki"];
|
|
||||||
full_args.extend_from_slice(args);
|
|
||||||
Cli::try_parse_from(full_args).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_no_args_defaults() {
|
|
||||||
let cli = parse(&[]);
|
|
||||||
assert!(cli.model.is_none());
|
|
||||||
assert!(cli.role.is_none());
|
|
||||||
assert!(cli.session.is_none());
|
|
||||||
assert!(cli.agent.is_none());
|
|
||||||
assert!(!cli.execute);
|
|
||||||
assert!(!cli.code);
|
|
||||||
assert!(!cli.no_stream);
|
|
||||||
assert!(!cli.dry_run);
|
|
||||||
assert!(!cli.info);
|
|
||||||
assert!(!cli.build_tools);
|
|
||||||
assert!(cli.file.is_empty());
|
|
||||||
assert!(cli.text.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_model_flag() {
|
|
||||||
let cli = parse(&["--model", "gpt-4o"]);
|
|
||||||
assert_eq!(cli.model, Some("gpt-4o".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_model_short_flag() {
|
|
||||||
let cli = parse(&["-m", "gpt-4o"]);
|
|
||||||
assert_eq!(cli.model, Some("gpt-4o".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_role_flag() {
|
|
||||||
let cli = parse(&["--role", "coder"]);
|
|
||||||
assert_eq!(cli.role, Some("coder".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_session_with_name() {
|
|
||||||
let cli = parse(&["--session", "my-session"]);
|
|
||||||
assert_eq!(cli.session, Some(Some("my-session".to_string())));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_agent_flag() {
|
|
||||||
let cli = parse(&["--agent", "sisyphus"]);
|
|
||||||
assert_eq!(cli.agent, Some("sisyphus".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_agent_short_flag() {
|
|
||||||
let cli = parse(&["-a", "sisyphus"]);
|
|
||||||
assert_eq!(cli.agent, Some("sisyphus".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_execute_flag() {
|
|
||||||
let cli = parse(&["-e", "list files"]);
|
|
||||||
assert!(cli.execute);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_code_flag() {
|
|
||||||
let cli = parse(&["-c", "hello world"]);
|
|
||||||
assert!(cli.code);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_no_stream_flag() {
|
|
||||||
let cli = parse(&["-S", "test"]);
|
|
||||||
assert!(cli.no_stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_dry_run_flag() {
|
|
||||||
let cli = parse(&["--dry-run", "test"]);
|
|
||||||
assert!(cli.dry_run);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_info_flag() {
|
|
||||||
let cli = parse(&["--info"]);
|
|
||||||
assert!(cli.info);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_list_flags() {
|
|
||||||
assert!(parse(&["--list-models"]).list_models);
|
|
||||||
assert!(parse(&["--list-roles"]).list_roles);
|
|
||||||
assert!(parse(&["--list-sessions"]).list_sessions);
|
|
||||||
assert!(parse(&["--list-agents"]).list_agents);
|
|
||||||
assert!(parse(&["--list-rags"]).list_rags);
|
|
||||||
assert!(parse(&["--list-macros"]).list_macros);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_file_flag_single() {
|
|
||||||
let cli = parse(&["-f", "file.txt", "question"]);
|
|
||||||
assert_eq!(cli.file, vec!["file.txt"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_file_flag_multiple() {
|
|
||||||
let cli = parse(&["-f", "a.txt", "-f", "b.txt", "question"]);
|
|
||||||
assert_eq!(cli.file, vec!["a.txt", "b.txt"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_trailing_text() {
|
|
||||||
let cli = parse(&["hello", "world"]);
|
|
||||||
assert_eq!(cli.text, vec!["hello", "world"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_prompt_flag() {
|
|
||||||
let cli = parse(&["--prompt", "be a pirate"]);
|
|
||||||
assert_eq!(cli.prompt, Some("be a pirate".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_empty_session_flag() {
|
|
||||||
let cli = parse(&["--session", "s", "--empty-session"]);
|
|
||||||
assert!(cli.empty_session);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_save_session_flag() {
|
|
||||||
let cli = parse(&["--session", "s", "--save-session"]);
|
|
||||||
assert!(cli.save_session);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_build_tools_flag() {
|
|
||||||
let cli = parse(&["--build-tools"]);
|
|
||||||
assert!(cli.build_tools);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_sync_models_flag() {
|
|
||||||
let cli = parse(&["--sync-models"]);
|
|
||||||
assert!(cli.sync_models);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_model_with_role() {
|
|
||||||
let cli = parse(&["-m", "gpt-4o", "-r", "coder"]);
|
|
||||||
assert_eq!(cli.model, Some("gpt-4o".to_string()));
|
|
||||||
assert_eq!(cli.role, Some("coder".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_agent_with_file_and_text() {
|
|
||||||
let cli = parse(&["-a", "sisyphus", "-f", "code.rs", "explain", "this"]);
|
|
||||||
assert_eq!(cli.agent, Some("sisyphus".to_string()));
|
|
||||||
assert_eq!(cli.file, vec!["code.rs"]);
|
|
||||||
assert_eq!(cli.text, vec!["explain", "this"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_role_with_session() {
|
|
||||||
let cli = parse(&["-r", "coder", "-s", "dev-session"]);
|
|
||||||
assert_eq!(cli.role, Some("coder".to_string()));
|
|
||||||
assert_eq!(cli.session, Some(Some("dev-session".to_string())));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cli_text_returns_none_when_no_text_no_stdin() {
|
|
||||||
let cli = parse(&[]);
|
|
||||||
assert!(cli.text().unwrap().is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cli_text_joins_trailing_args() {
|
|
||||||
let cli = parse(&["hello", "world"]);
|
|
||||||
assert_eq!(cli.text().unwrap(), Some("hello world".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_add_secret_flag() {
|
|
||||||
let cli = parse(&["--add-secret", "MY_KEY"]);
|
|
||||||
assert_eq!(cli.add_secret, Some("MY_KEY".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_get_secret_flag() {
|
|
||||||
let cli = parse(&["--get-secret", "MY_KEY"]);
|
|
||||||
assert_eq!(cli.get_secret, Some("MY_KEY".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_list_secrets_flag() {
|
|
||||||
let cli = parse(&["--list-secrets"]);
|
|
||||||
assert!(cli.list_secrets);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_rag_flag() {
|
|
||||||
let cli = parse(&["--rag", "my-rag"]);
|
|
||||||
assert_eq!(cli.rag, Some("my-rag".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_macro_flag() {
|
|
||||||
let cli = parse(&["--macro", "my-macro"]);
|
|
||||||
assert_eq!(cli.macro_name, Some("my-macro".to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::config::paths;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{AppConfig, Input, RequestContext},
|
config::{Config, GlobalConfig, Input},
|
||||||
function::{FunctionDeclaration, ToolCall, ToolResult, eval_tool_calls},
|
function::{FunctionDeclaration, ToolCall, ToolResult, eval_tool_calls},
|
||||||
render::render_stream,
|
render::render_stream,
|
||||||
utils::*,
|
utils::*,
|
||||||
@@ -25,7 +24,7 @@ use tokio::sync::mpsc::unbounded_channel;
|
|||||||
pub const MODELS_YAML: &str = include_str!("../../models.yaml");
|
pub const MODELS_YAML: &str = include_str!("../../models.yaml");
|
||||||
|
|
||||||
pub static ALL_PROVIDER_MODELS: LazyLock<Vec<ProviderModels>> = LazyLock::new(|| {
|
pub static ALL_PROVIDER_MODELS: LazyLock<Vec<ProviderModels>> = LazyLock::new(|| {
|
||||||
paths::local_models_override()
|
Config::local_models_override()
|
||||||
.ok()
|
.ok()
|
||||||
.unwrap_or_else(|| serde_yaml::from_str(MODELS_YAML).unwrap())
|
.unwrap_or_else(|| serde_yaml::from_str(MODELS_YAML).unwrap())
|
||||||
});
|
});
|
||||||
@@ -38,7 +37,7 @@ static ESCAPE_SLASH_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?<!\\)/
|
|||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait Client: Sync + Send {
|
pub trait Client: Sync + Send {
|
||||||
fn app_config(&self) -> &AppConfig;
|
fn global_config(&self) -> &GlobalConfig;
|
||||||
|
|
||||||
fn extra_config(&self) -> Option<&ExtraConfig>;
|
fn extra_config(&self) -> Option<&ExtraConfig>;
|
||||||
|
|
||||||
@@ -59,7 +58,7 @@ pub trait Client: Sync + Send {
|
|||||||
if let Some(proxy) = extra.and_then(|v| v.proxy.as_deref()) {
|
if let Some(proxy) = extra.and_then(|v| v.proxy.as_deref()) {
|
||||||
builder = set_proxy(builder, proxy)?;
|
builder = set_proxy(builder, proxy)?;
|
||||||
}
|
}
|
||||||
if let Some(user_agent) = self.app_config().user_agent.as_ref() {
|
if let Some(user_agent) = self.global_config().read().user_agent.as_ref() {
|
||||||
builder = builder.user_agent(user_agent);
|
builder = builder.user_agent(user_agent);
|
||||||
}
|
}
|
||||||
let client = builder
|
let client = builder
|
||||||
@@ -70,7 +69,7 @@ pub trait Client: Sync + Send {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn chat_completions(&self, input: Input) -> Result<ChatCompletionsOutput> {
|
async fn chat_completions(&self, input: Input) -> Result<ChatCompletionsOutput> {
|
||||||
if self.app_config().dry_run {
|
if self.global_config().read().dry_run {
|
||||||
let content = input.echo_messages();
|
let content = input.echo_messages();
|
||||||
return Ok(ChatCompletionsOutput::new(&content));
|
return Ok(ChatCompletionsOutput::new(&content));
|
||||||
}
|
}
|
||||||
@@ -90,7 +89,7 @@ pub trait Client: Sync + Send {
|
|||||||
let input = input.clone();
|
let input = input.clone();
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
ret = async {
|
ret = async {
|
||||||
if self.app_config().dry_run {
|
if self.global_config().read().dry_run {
|
||||||
let content = input.echo_messages();
|
let content = input.echo_messages();
|
||||||
handler.text(&content)?;
|
handler.text(&content)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -414,10 +413,9 @@ pub async fn call_chat_completions(
|
|||||||
print: bool,
|
print: bool,
|
||||||
extract_code: bool,
|
extract_code: bool,
|
||||||
client: &dyn Client,
|
client: &dyn Client,
|
||||||
ctx: &mut RequestContext,
|
|
||||||
abort_signal: AbortSignal,
|
abort_signal: AbortSignal,
|
||||||
) -> Result<(String, Vec<ToolResult>)> {
|
) -> Result<(String, Vec<ToolResult>)> {
|
||||||
let is_child_agent = ctx.current_depth > 0;
|
let is_child_agent = client.global_config().read().current_depth > 0;
|
||||||
let spinner_message = if is_child_agent { "" } else { "Generating" };
|
let spinner_message = if is_child_agent { "" } else { "Generating" };
|
||||||
let ret = abortable_run_with_spinner(
|
let ret = abortable_run_with_spinner(
|
||||||
client.chat_completions(input.clone()),
|
client.chat_completions(input.clone()),
|
||||||
@@ -438,13 +436,15 @@ pub async fn call_chat_completions(
|
|||||||
text = extract_code_block(&strip_think_tag(&text)).to_string();
|
text = extract_code_block(&strip_think_tag(&text)).to_string();
|
||||||
}
|
}
|
||||||
if print {
|
if print {
|
||||||
ctx.app.config.print_markdown(&text)?;
|
client.global_config().read().print_markdown(&text)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let tool_results = eval_tool_calls(ctx, tool_calls).await?;
|
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
|
tool_results
|
||||||
.iter()
|
.iter()
|
||||||
.for_each(|res| ctx.tool_scope.tool_tracker.record_call(res.call.clone()));
|
.for_each(|res| tracker.record_call(res.call.clone()));
|
||||||
|
}
|
||||||
Ok((text, tool_results))
|
Ok((text, tool_results))
|
||||||
}
|
}
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
@@ -454,7 +454,6 @@ pub async fn call_chat_completions(
|
|||||||
pub async fn call_chat_completions_streaming(
|
pub async fn call_chat_completions_streaming(
|
||||||
input: &Input,
|
input: &Input,
|
||||||
client: &dyn Client,
|
client: &dyn Client,
|
||||||
ctx: &mut RequestContext,
|
|
||||||
abort_signal: AbortSignal,
|
abort_signal: AbortSignal,
|
||||||
) -> Result<(String, Vec<ToolResult>)> {
|
) -> Result<(String, Vec<ToolResult>)> {
|
||||||
let (tx, rx) = unbounded_channel();
|
let (tx, rx) = unbounded_channel();
|
||||||
@@ -462,7 +461,7 @@ pub async fn call_chat_completions_streaming(
|
|||||||
|
|
||||||
let (send_ret, render_ret) = tokio::join!(
|
let (send_ret, render_ret) = tokio::join!(
|
||||||
client.chat_completions_streaming(input, &mut handler),
|
client.chat_completions_streaming(input, &mut handler),
|
||||||
render_stream(rx, client.app_config(), abort_signal.clone()),
|
render_stream(rx, client.global_config(), abort_signal.clone()),
|
||||||
);
|
);
|
||||||
|
|
||||||
if handler.abort().aborted() {
|
if handler.abort().aborted() {
|
||||||
@@ -477,10 +476,12 @@ pub async fn call_chat_completions_streaming(
|
|||||||
if !text.is_empty() && !text.ends_with('\n') {
|
if !text.is_empty() && !text.ends_with('\n') {
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
let tool_results = eval_tool_calls(ctx, tool_calls).await?;
|
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
|
tool_results
|
||||||
.iter()
|
.iter()
|
||||||
.for_each(|res| ctx.tool_scope.tool_tracker.record_call(res.call.clone()));
|
.for_each(|res| tracker.record_call(res.call.clone()));
|
||||||
|
}
|
||||||
Ok((text, tool_results))
|
Ok((text, tool_results))
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ macro_rules! register_client {
|
|||||||
$(
|
$(
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct $client {
|
pub struct $client {
|
||||||
app_config: std::sync::Arc<$crate::config::AppConfig>,
|
global_config: $crate::config::GlobalConfig,
|
||||||
config: $config,
|
config: $config,
|
||||||
model: $crate::client::Model,
|
model: $crate::client::Model,
|
||||||
}
|
}
|
||||||
@@ -32,8 +32,8 @@ macro_rules! register_client {
|
|||||||
impl $client {
|
impl $client {
|
||||||
pub const NAME: &'static str = $name;
|
pub const NAME: &'static str = $name;
|
||||||
|
|
||||||
pub fn init(app_config: &std::sync::Arc<$crate::config::AppConfig>, model: &$crate::client::Model) -> Option<Box<dyn Client>> {
|
pub fn init(global_config: &$crate::config::GlobalConfig, model: &$crate::client::Model) -> Option<Box<dyn Client>> {
|
||||||
let config = app_config.clients.iter().find_map(|client_config| {
|
let config = global_config.read().clients.iter().find_map(|client_config| {
|
||||||
if let ClientConfig::$config(c) = client_config {
|
if let ClientConfig::$config(c) = client_config {
|
||||||
if Self::name(c) == model.client_name() {
|
if Self::name(c) == model.client_name() {
|
||||||
return Some(c.clone())
|
return Some(c.clone())
|
||||||
@@ -43,7 +43,7 @@ macro_rules! register_client {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
Some(Box::new(Self {
|
Some(Box::new(Self {
|
||||||
app_config: std::sync::Arc::clone(app_config),
|
global_config: global_config.clone(),
|
||||||
config,
|
config,
|
||||||
model: model.clone(),
|
model: model.clone(),
|
||||||
}))
|
}))
|
||||||
@@ -72,9 +72,10 @@ macro_rules! register_client {
|
|||||||
|
|
||||||
)+
|
)+
|
||||||
|
|
||||||
pub fn init_client(app_config: &std::sync::Arc<$crate::config::AppConfig>, model: $crate::client::Model) -> anyhow::Result<Box<dyn Client>> {
|
pub fn init_client(config: &$crate::config::GlobalConfig, model: Option<$crate::client::Model>) -> anyhow::Result<Box<dyn Client>> {
|
||||||
|
let model = model.unwrap_or_else(|| config.read().model.clone());
|
||||||
None
|
None
|
||||||
$(.or_else(|| $client::init(app_config, &model)))+
|
$(.or_else(|| $client::init(config, &model)))+
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
anyhow::anyhow!("Invalid model '{}'", model.id())
|
anyhow::anyhow!("Invalid model '{}'", model.id())
|
||||||
})
|
})
|
||||||
@@ -100,7 +101,7 @@ macro_rules! register_client {
|
|||||||
|
|
||||||
static ALL_CLIENT_NAMES: std::sync::OnceLock<Vec<String>> = std::sync::OnceLock::new();
|
static ALL_CLIENT_NAMES: std::sync::OnceLock<Vec<String>> = std::sync::OnceLock::new();
|
||||||
|
|
||||||
pub fn list_client_names(config: &$crate::config::AppConfig) -> Vec<&'static String> {
|
pub fn list_client_names(config: &$crate::config::Config) -> Vec<&'static String> {
|
||||||
let names = ALL_CLIENT_NAMES.get_or_init(|| {
|
let names = ALL_CLIENT_NAMES.get_or_init(|| {
|
||||||
config
|
config
|
||||||
.clients
|
.clients
|
||||||
@@ -116,7 +117,7 @@ macro_rules! register_client {
|
|||||||
|
|
||||||
static ALL_MODELS: std::sync::OnceLock<Vec<$crate::client::Model>> = std::sync::OnceLock::new();
|
static ALL_MODELS: std::sync::OnceLock<Vec<$crate::client::Model>> = std::sync::OnceLock::new();
|
||||||
|
|
||||||
pub fn list_all_models(config: &$crate::config::AppConfig) -> Vec<&'static $crate::client::Model> {
|
pub fn list_all_models(config: &$crate::config::Config) -> Vec<&'static $crate::client::Model> {
|
||||||
let models = ALL_MODELS.get_or_init(|| {
|
let models = ALL_MODELS.get_or_init(|| {
|
||||||
config
|
config
|
||||||
.clients
|
.clients
|
||||||
@@ -130,7 +131,7 @@ macro_rules! register_client {
|
|||||||
models.iter().collect()
|
models.iter().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_models(config: &$crate::config::AppConfig, model_type: $crate::client::ModelType) -> Vec<&'static $crate::client::Model> {
|
pub fn list_models(config: &$crate::config::Config, model_type: $crate::client::ModelType) -> Vec<&'static $crate::client::Model> {
|
||||||
list_all_models(config).into_iter().filter(|v| v.model_type() == model_type).collect()
|
list_all_models(config).into_iter().filter(|v| v.model_type() == model_type).collect()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -139,8 +140,8 @@ macro_rules! register_client {
|
|||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! client_common_fns {
|
macro_rules! client_common_fns {
|
||||||
() => {
|
() => {
|
||||||
fn app_config(&self) -> &$crate::config::AppConfig {
|
fn global_config(&self) -> &$crate::config::GlobalConfig {
|
||||||
&self.app_config
|
&self.global_config
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extra_config(&self) -> Option<&$crate::client::ExtraConfig> {
|
fn extra_config(&self) -> Option<&$crate::client::ExtraConfig> {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use super::{
|
|||||||
message::{Message, MessageContent, MessageContentPart},
|
message::{Message, MessageContent, MessageContentPart},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::config::AppConfig;
|
use crate::config::Config;
|
||||||
use crate::utils::{estimate_token_length, strip_think_tag};
|
use crate::utils::{estimate_token_length, strip_think_tag};
|
||||||
|
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
@@ -44,11 +44,7 @@ impl Model {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn retrieve_model(
|
pub fn retrieve_model(config: &Config, model_id: &str, model_type: ModelType) -> Result<Self> {
|
||||||
config: &AppConfig,
|
|
||||||
model_id: &str,
|
|
||||||
model_type: ModelType,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let models = list_all_models(config);
|
let models = list_all_models(config);
|
||||||
let (client_name, model_name) = match model_id.split_once(':') {
|
let (client_name, model_name) = match model_id.split_once(':') {
|
||||||
Some((client_name, model_name)) => {
|
Some((client_name, model_name)) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::ClientConfig;
|
use super::ClientConfig;
|
||||||
use super::access_token::{is_valid_access_token, set_access_token};
|
use super::access_token::{is_valid_access_token, set_access_token};
|
||||||
use crate::config::paths;
|
use crate::config::Config;
|
||||||
use anyhow::{Result, anyhow, bail};
|
use anyhow::{Result, anyhow, bail};
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
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> {
|
pub fn load_oauth_tokens(client_name: &str) -> Option<OAuthTokens> {
|
||||||
let path = paths::token_file(client_name);
|
let path = Config::token_file(client_name);
|
||||||
let content = fs::read_to_string(path).ok()?;
|
let content = fs::read_to_string(path).ok()?;
|
||||||
serde_json::from_str(&content).ok()
|
serde_json::from_str(&content).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_oauth_tokens(client_name: &str, tokens: &OAuthTokens) -> Result<()> {
|
fn save_oauth_tokens(client_name: &str, tokens: &OAuthTokens) -> Result<()> {
|
||||||
let path = paths::token_file(client_name);
|
let path = Config::token_file(client_name);
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
fs::create_dir_all(parent)?;
|
fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -311,9 +311,11 @@ impl JsonStreamParser {
|
|||||||
}
|
}
|
||||||
self.balances.push(ch);
|
self.balances.push(ch);
|
||||||
}
|
}
|
||||||
'[' if self.start.is_some() => {
|
'[' => {
|
||||||
|
if self.start.is_some() {
|
||||||
self.balances.push(ch);
|
self.balances.push(ch);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
'}' => {
|
'}' => {
|
||||||
self.balances.pop();
|
self.balances.pop();
|
||||||
if self.balances.is_empty()
|
if self.balances.is_empty()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use super::todo::TodoList;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -5,8 +6,6 @@ use crate::{
|
|||||||
function::{Functions, run_llm_function},
|
function::{Functions, run_llm_function},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::rag_cache::RagKey;
|
|
||||||
use crate::config::paths;
|
|
||||||
use crate::config::prompts::{
|
use crate::config::prompts::{
|
||||||
DEFAULT_SPAWN_INSTRUCTIONS, DEFAULT_TEAMMATE_INSTRUCTIONS, DEFAULT_TODO_INSTRUCTIONS,
|
DEFAULT_SPAWN_INSTRUCTIONS, DEFAULT_TEAMMATE_INSTRUCTIONS, DEFAULT_TODO_INSTRUCTIONS,
|
||||||
DEFAULT_USER_INTERACTION_INSTRUCTIONS,
|
DEFAULT_USER_INTERACTION_INSTRUCTIONS,
|
||||||
@@ -39,13 +38,16 @@ pub struct Agent {
|
|||||||
rag: Option<Arc<Rag>>,
|
rag: Option<Arc<Rag>>,
|
||||||
model: Model,
|
model: Model,
|
||||||
vault: GlobalVault,
|
vault: GlobalVault,
|
||||||
|
todo_list: TodoList,
|
||||||
|
continuation_count: usize,
|
||||||
|
last_continuation_response: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Agent {
|
impl Agent {
|
||||||
pub fn install_builtin_agents() -> Result<()> {
|
pub fn install_builtin_agents() -> Result<()> {
|
||||||
info!(
|
info!(
|
||||||
"Installing built-in agents in {}",
|
"Installing built-in agents in {}",
|
||||||
paths::agents_data_dir().display()
|
Config::agents_data_dir().display()
|
||||||
);
|
);
|
||||||
|
|
||||||
for file in AgentAssets::iter() {
|
for file in AgentAssets::iter() {
|
||||||
@@ -54,7 +56,7 @@ impl Agent {
|
|||||||
let embedded_file = AgentAssets::get(&file)
|
let embedded_file = AgentAssets::get(&file)
|
||||||
.ok_or_else(|| anyhow!("Failed to load embedded agent file: {}", file.as_ref()))?;
|
.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 content = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
|
||||||
let file_path = paths::agents_data_dir().join(file.as_ref());
|
let file_path = Config::agents_data_dir().join(file.as_ref());
|
||||||
let file_extension = file_path
|
let file_extension = file_path
|
||||||
.extension()
|
.extension()
|
||||||
.and_then(OsStr::to_str)
|
.and_then(OsStr::to_str)
|
||||||
@@ -86,17 +88,14 @@ impl Agent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn init(
|
pub async fn init(
|
||||||
app: &AppConfig,
|
config: &GlobalConfig,
|
||||||
app_state: &AppState,
|
|
||||||
current_model: &Model,
|
|
||||||
info_flag: bool,
|
|
||||||
name: &str,
|
name: &str,
|
||||||
abort_signal: AbortSignal,
|
abort_signal: AbortSignal,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let agent_data_dir = paths::agent_data_dir(name);
|
let agent_data_dir = Config::agent_data_dir(name);
|
||||||
let loaders = app.document_loaders.clone();
|
let loaders = config.read().document_loaders.clone();
|
||||||
let rag_path = paths::agent_rag_file(name, DEFAULT_AGENT_NAME);
|
let rag_path = Config::agent_rag_file(name, DEFAULT_AGENT_NAME);
|
||||||
let config_path = paths::agent_config_file(name);
|
let config_path = Config::agent_config_file(name);
|
||||||
let mut agent_config = if config_path.exists() {
|
let mut agent_config = if config_path.exists() {
|
||||||
AgentConfig::load(&config_path)?
|
AgentConfig::load(&config_path)?
|
||||||
} else {
|
} else {
|
||||||
@@ -104,33 +103,57 @@ impl Agent {
|
|||||||
};
|
};
|
||||||
let mut functions = Functions::init_agent(name, &agent_config.global_tools)?;
|
let mut functions = Functions::init_agent(name, &agent_config.global_tools)?;
|
||||||
|
|
||||||
agent_config.load_envs(app);
|
config.write().functions.clear_mcp_meta_functions();
|
||||||
|
let mcp_servers = if config.read().mcp_server_support {
|
||||||
|
(!agent_config.mcp_servers.is_empty()).then(|| agent_config.mcp_servers.join(","))
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"{}",
|
||||||
|
formatdoc!(
|
||||||
|
"
|
||||||
|
This agent uses MCP servers, but MCP support is disabled.
|
||||||
|
To enable it, exit the agent and set 'mcp_server_support: true', then try again
|
||||||
|
"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let model = match agent_config.model_id.as_ref() {
|
let registry = config
|
||||||
Some(model_id) => Model::retrieve_model(app, model_id, ModelType::Chat)?,
|
.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 => {
|
None => {
|
||||||
if agent_config.temperature.is_none() {
|
if agent_config.temperature.is_none() {
|
||||||
agent_config.temperature = app.temperature;
|
agent_config.temperature = config.temperature;
|
||||||
}
|
}
|
||||||
if agent_config.top_p.is_none() {
|
if agent_config.top_p.is_none() {
|
||||||
agent_config.top_p = app.top_p;
|
agent_config.top_p = config.top_p;
|
||||||
|
}
|
||||||
|
config.current_model().clone()
|
||||||
}
|
}
|
||||||
current_model.clone()
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let rag = if rag_path.exists() {
|
let rag = if rag_path.exists() {
|
||||||
let key = RagKey::Agent(name.to_string());
|
Some(Arc::new(Rag::load(config, DEFAULT_AGENT_NAME, &rag_path)?))
|
||||||
let app_clone = app.clone();
|
} else if !agent_config.documents.is_empty() && !config.read().info_flag {
|
||||||
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;
|
let mut ans = false;
|
||||||
if *IS_STDOUT_TERMINAL {
|
if *IS_STDOUT_TERMINAL {
|
||||||
ans = Confirm::new("The agent has documents attached, init RAG?")
|
ans = Confirm::new("The agent has documents attached, init RAG?")
|
||||||
@@ -162,23 +185,9 @@ impl Agent {
|
|||||||
document_paths.push(path.to_string())
|
document_paths.push(path.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let key = RagKey::Agent(name.to_string());
|
let rag =
|
||||||
let app_clone = app.clone();
|
Rag::init(config, "rag", &rag_path, &document_paths, abort_signal).await?;
|
||||||
let rag_path_clone = rag_path.clone();
|
Some(Arc::new(rag))
|
||||||
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 {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -209,7 +218,10 @@ impl Agent {
|
|||||||
functions,
|
functions,
|
||||||
rag,
|
rag,
|
||||||
model,
|
model,
|
||||||
vault: app_state.vault.clone(),
|
vault: Arc::clone(&config.read().vault),
|
||||||
|
todo_list: TodoList::default(),
|
||||||
|
continuation_count: 0,
|
||||||
|
last_continuation_response: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,11 +295,11 @@ impl Agent {
|
|||||||
let mut config = self.config.clone();
|
let mut config = self.config.clone();
|
||||||
config.instructions = self.interpolated_instructions();
|
config.instructions = self.interpolated_instructions();
|
||||||
value["definition"] = json!(config);
|
value["definition"] = json!(config);
|
||||||
value["data_dir"] = paths::agent_data_dir(&self.name)
|
value["data_dir"] = Config::agent_data_dir(&self.name)
|
||||||
.display()
|
.display()
|
||||||
.to_string()
|
.to_string()
|
||||||
.into();
|
.into();
|
||||||
value["config_file"] = paths::agent_config_file(&self.name)
|
value["config_file"] = Config::agent_config_file(&self.name)
|
||||||
.display()
|
.display()
|
||||||
.to_string()
|
.to_string()
|
||||||
.into();
|
.into();
|
||||||
@@ -311,14 +323,6 @@ impl Agent {
|
|||||||
self.rag.clone()
|
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> {
|
pub fn conversation_starters(&self) -> Vec<String> {
|
||||||
self.config
|
self.config
|
||||||
.conversation_starters
|
.conversation_starters
|
||||||
@@ -439,6 +443,44 @@ impl Agent {
|
|||||||
self.config.escalation_timeout
|
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 {
|
pub fn continuation_prompt(&self) -> String {
|
||||||
self.config.continuation_prompt.clone().unwrap_or_else(|| {
|
self.config.continuation_prompt.clone().unwrap_or_else(|| {
|
||||||
formatdoc! {"
|
formatdoc! {"
|
||||||
@@ -654,12 +696,12 @@ impl AgentConfig {
|
|||||||
Ok(agent_config)
|
Ok(agent_config)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_envs(&mut self, app: &AppConfig) {
|
fn load_envs(&mut self, config: &Config) {
|
||||||
let name = &self.name;
|
let name = &self.name;
|
||||||
let with_prefix = |v: &str| normalize_env_name(&format!("{name}_{v}"));
|
let with_prefix = |v: &str| normalize_env_name(&format!("{name}_{v}"));
|
||||||
|
|
||||||
if self.agent_session.is_none() {
|
if self.agent_session.is_none() {
|
||||||
self.agent_session = app.agent_session.clone();
|
self.agent_session = config.agent_session.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(v) = read_env_value::<String>(&with_prefix("model")) {
|
if let Some(v) = read_env_value::<String>(&with_prefix("model")) {
|
||||||
@@ -751,7 +793,7 @@ pub struct AgentVariable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_agents() -> Vec<String> {
|
pub fn list_agents() -> Vec<String> {
|
||||||
let agents_data_dir = paths::agents_data_dir();
|
let agents_data_dir = Config::agents_data_dir();
|
||||||
if !agents_data_dir.exists() {
|
if !agents_data_dir.exists() {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
@@ -761,7 +803,6 @@ pub fn list_agents() -> Vec<String> {
|
|||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
if entry.path().is_dir()
|
if entry.path().is_dir()
|
||||||
&& let Some(name) = entry.file_name().to_str()
|
&& let Some(name) = entry.file_name().to_str()
|
||||||
&& !name.starts_with('.')
|
|
||||||
{
|
{
|
||||||
agents.push(name.to_string());
|
agents.push(name.to_string());
|
||||||
}
|
}
|
||||||
@@ -772,7 +813,7 @@ pub fn list_agents() -> Vec<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn complete_agent_variables(agent_name: &str) -> Vec<(String, Option<String>)> {
|
pub fn complete_agent_variables(agent_name: &str) -> Vec<(String, Option<String>)> {
|
||||||
let config_path = paths::agent_config_file(agent_name);
|
let config_path = Config::agent_config_file(agent_name);
|
||||||
if !config_path.exists() {
|
if !config_path.exists() {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
@@ -791,89 +832,3 @@ pub fn complete_agent_variables(agent_name: &str) -> Vec<(String, Option<String>
|
|||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn agent_config_parses_from_yaml() {
|
|
||||||
let yaml = r#"
|
|
||||||
name: test-agent
|
|
||||||
description: A test agent
|
|
||||||
instructions: You are helpful
|
|
||||||
auto_continue: true
|
|
||||||
max_auto_continues: 5
|
|
||||||
can_spawn_agents: true
|
|
||||||
max_concurrent_agents: 8
|
|
||||||
max_agent_depth: 2
|
|
||||||
mcp_servers:
|
|
||||||
- github
|
|
||||||
- jira
|
|
||||||
global_tools:
|
|
||||||
- execute_command.sh
|
|
||||||
- fs_read.sh
|
|
||||||
conversation_starters:
|
|
||||||
- "Hello!"
|
|
||||||
- "How are you?"
|
|
||||||
variables:
|
|
||||||
- name: username
|
|
||||||
description: Your name
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let config: AgentConfig = serde_yaml::from_str(yaml).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(config.name, "test-agent");
|
|
||||||
assert_eq!(config.description, "A test agent");
|
|
||||||
assert!(config.auto_continue);
|
|
||||||
assert_eq!(config.max_auto_continues, 5);
|
|
||||||
assert!(config.can_spawn_agents);
|
|
||||||
assert_eq!(config.max_concurrent_agents, 8);
|
|
||||||
assert_eq!(config.max_agent_depth, 2);
|
|
||||||
assert_eq!(config.mcp_servers, vec!["github", "jira"]);
|
|
||||||
assert_eq!(config.global_tools.len(), 2);
|
|
||||||
assert_eq!(config.conversation_starters.len(), 2);
|
|
||||||
assert_eq!(config.variables.len(), 1);
|
|
||||||
assert_eq!(config.variables[0].name, "username");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn agent_config_defaults() {
|
|
||||||
let yaml = "name: minimal\ninstructions: hi\n";
|
|
||||||
let config: AgentConfig = serde_yaml::from_str(yaml).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(config.name, "minimal");
|
|
||||||
assert!(!config.auto_continue);
|
|
||||||
assert!(!config.can_spawn_agents);
|
|
||||||
assert_eq!(config.max_concurrent_agents, 4);
|
|
||||||
assert_eq!(config.max_agent_depth, 3);
|
|
||||||
assert_eq!(config.max_auto_continues, 10);
|
|
||||||
assert!(config.mcp_servers.is_empty());
|
|
||||||
assert!(config.global_tools.is_empty());
|
|
||||||
assert!(config.conversation_starters.is_empty());
|
|
||||||
assert!(config.variables.is_empty());
|
|
||||||
assert!(config.model_id.is_none());
|
|
||||||
assert!(config.temperature.is_none());
|
|
||||||
assert!(config.top_p.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn agent_config_with_model() {
|
|
||||||
let yaml =
|
|
||||||
"name: test\nmodel: openai:gpt-4\ntemperature: 0.7\ntop_p: 0.9\ninstructions: hi\n";
|
|
||||||
let config: AgentConfig = serde_yaml::from_str(yaml).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(config.model_id, Some("openai:gpt-4".to_string()));
|
|
||||||
assert_eq!(config.temperature, Some(0.7));
|
|
||||||
assert_eq!(config.top_p, Some(0.9));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn agent_config_inject_defaults_true() {
|
|
||||||
let yaml = "name: test\ninstructions: hi\n";
|
|
||||||
let config: AgentConfig = serde_yaml::from_str(yaml).unwrap();
|
|
||||||
|
|
||||||
assert!(config.inject_todo_instructions);
|
|
||||||
assert!(config.inject_spawn_instructions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,740 +0,0 @@
|
|||||||
use crate::client::{ClientConfig, list_models};
|
|
||||||
use crate::render::{MarkdownRender, RenderOptions};
|
|
||||||
use crate::utils::{IS_STDOUT_TERMINAL, NO_COLOR, decode_bin, get_env_name};
|
|
||||||
|
|
||||||
use super::paths;
|
|
||||||
use anyhow::{Context, Result, anyhow};
|
|
||||||
use indexmap::IndexMap;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::env;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use syntect::highlighting::ThemeSet;
|
|
||||||
use terminal_colorsaurus::{ColorScheme, QueryOptions, color_scheme};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
#[serde(default)]
|
|
||||||
pub struct AppConfig {
|
|
||||||
#[serde(rename(serialize = "model", deserialize = "model"))]
|
|
||||||
#[serde(default)]
|
|
||||||
pub model_id: String,
|
|
||||||
pub temperature: Option<f64>,
|
|
||||||
pub top_p: Option<f64>,
|
|
||||||
|
|
||||||
pub dry_run: bool,
|
|
||||||
pub stream: bool,
|
|
||||||
pub save: bool,
|
|
||||||
pub keybindings: String,
|
|
||||||
pub editor: Option<String>,
|
|
||||||
pub wrap: Option<String>,
|
|
||||||
pub wrap_code: bool,
|
|
||||||
pub(crate) vault_password_file: Option<PathBuf>,
|
|
||||||
|
|
||||||
pub function_calling_support: bool,
|
|
||||||
pub mapping_tools: IndexMap<String, String>,
|
|
||||||
pub enabled_tools: Option<String>,
|
|
||||||
pub visible_tools: Option<Vec<String>>,
|
|
||||||
|
|
||||||
pub mcp_server_support: bool,
|
|
||||||
pub mapping_mcp_servers: IndexMap<String, String>,
|
|
||||||
pub enabled_mcp_servers: Option<String>,
|
|
||||||
|
|
||||||
pub repl_prelude: Option<String>,
|
|
||||||
pub cmd_prelude: Option<String>,
|
|
||||||
pub agent_session: Option<String>,
|
|
||||||
|
|
||||||
pub save_session: Option<bool>,
|
|
||||||
pub compression_threshold: usize,
|
|
||||||
pub summarization_prompt: Option<String>,
|
|
||||||
pub summary_context_prompt: Option<String>,
|
|
||||||
|
|
||||||
pub rag_embedding_model: Option<String>,
|
|
||||||
pub rag_reranker_model: Option<String>,
|
|
||||||
pub rag_top_k: usize,
|
|
||||||
pub rag_chunk_size: Option<usize>,
|
|
||||||
pub rag_chunk_overlap: Option<usize>,
|
|
||||||
pub rag_template: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub document_loaders: HashMap<String, String>,
|
|
||||||
|
|
||||||
pub highlight: bool,
|
|
||||||
pub theme: Option<String>,
|
|
||||||
pub left_prompt: Option<String>,
|
|
||||||
pub right_prompt: Option<String>,
|
|
||||||
|
|
||||||
pub user_agent: Option<String>,
|
|
||||||
pub save_shell_history: bool,
|
|
||||||
pub sync_models_url: Option<String>,
|
|
||||||
|
|
||||||
pub clients: Vec<ClientConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AppConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
model_id: Default::default(),
|
|
||||||
temperature: None,
|
|
||||||
top_p: None,
|
|
||||||
|
|
||||||
dry_run: false,
|
|
||||||
stream: true,
|
|
||||||
save: false,
|
|
||||||
keybindings: "emacs".into(),
|
|
||||||
editor: None,
|
|
||||||
wrap: None,
|
|
||||||
wrap_code: false,
|
|
||||||
vault_password_file: None,
|
|
||||||
|
|
||||||
function_calling_support: true,
|
|
||||||
mapping_tools: Default::default(),
|
|
||||||
enabled_tools: None,
|
|
||||||
visible_tools: None,
|
|
||||||
|
|
||||||
mcp_server_support: true,
|
|
||||||
mapping_mcp_servers: Default::default(),
|
|
||||||
enabled_mcp_servers: None,
|
|
||||||
|
|
||||||
repl_prelude: None,
|
|
||||||
cmd_prelude: None,
|
|
||||||
agent_session: None,
|
|
||||||
|
|
||||||
save_session: None,
|
|
||||||
compression_threshold: 4000,
|
|
||||||
summarization_prompt: None,
|
|
||||||
summary_context_prompt: None,
|
|
||||||
|
|
||||||
rag_embedding_model: None,
|
|
||||||
rag_reranker_model: None,
|
|
||||||
rag_top_k: 5,
|
|
||||||
rag_chunk_size: None,
|
|
||||||
rag_chunk_overlap: None,
|
|
||||||
rag_template: None,
|
|
||||||
|
|
||||||
document_loaders: Default::default(),
|
|
||||||
|
|
||||||
highlight: true,
|
|
||||||
theme: None,
|
|
||||||
left_prompt: None,
|
|
||||||
right_prompt: None,
|
|
||||||
|
|
||||||
user_agent: None,
|
|
||||||
save_shell_history: true,
|
|
||||||
sync_models_url: None,
|
|
||||||
|
|
||||||
clients: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppConfig {
|
|
||||||
pub fn from_config(config: super::Config) -> Result<Self> {
|
|
||||||
let mut app_config = Self {
|
|
||||||
model_id: config.model_id,
|
|
||||||
temperature: config.temperature,
|
|
||||||
top_p: config.top_p,
|
|
||||||
|
|
||||||
dry_run: config.dry_run,
|
|
||||||
stream: config.stream,
|
|
||||||
save: config.save,
|
|
||||||
keybindings: config.keybindings,
|
|
||||||
editor: config.editor,
|
|
||||||
wrap: config.wrap,
|
|
||||||
wrap_code: config.wrap_code,
|
|
||||||
vault_password_file: config.vault_password_file,
|
|
||||||
|
|
||||||
function_calling_support: config.function_calling_support,
|
|
||||||
mapping_tools: config.mapping_tools,
|
|
||||||
enabled_tools: config.enabled_tools,
|
|
||||||
visible_tools: config.visible_tools,
|
|
||||||
|
|
||||||
mcp_server_support: config.mcp_server_support,
|
|
||||||
mapping_mcp_servers: config.mapping_mcp_servers,
|
|
||||||
enabled_mcp_servers: config.enabled_mcp_servers,
|
|
||||||
|
|
||||||
repl_prelude: config.repl_prelude,
|
|
||||||
cmd_prelude: config.cmd_prelude,
|
|
||||||
agent_session: config.agent_session,
|
|
||||||
|
|
||||||
save_session: config.save_session,
|
|
||||||
compression_threshold: config.compression_threshold,
|
|
||||||
summarization_prompt: config.summarization_prompt,
|
|
||||||
summary_context_prompt: config.summary_context_prompt,
|
|
||||||
|
|
||||||
rag_embedding_model: config.rag_embedding_model,
|
|
||||||
rag_reranker_model: config.rag_reranker_model,
|
|
||||||
rag_top_k: config.rag_top_k,
|
|
||||||
rag_chunk_size: config.rag_chunk_size,
|
|
||||||
rag_chunk_overlap: config.rag_chunk_overlap,
|
|
||||||
rag_template: config.rag_template,
|
|
||||||
|
|
||||||
document_loaders: config.document_loaders,
|
|
||||||
|
|
||||||
highlight: config.highlight,
|
|
||||||
theme: config.theme,
|
|
||||||
left_prompt: config.left_prompt,
|
|
||||||
right_prompt: config.right_prompt,
|
|
||||||
|
|
||||||
user_agent: config.user_agent,
|
|
||||||
save_shell_history: config.save_shell_history,
|
|
||||||
sync_models_url: config.sync_models_url,
|
|
||||||
|
|
||||||
clients: config.clients,
|
|
||||||
};
|
|
||||||
app_config.load_envs();
|
|
||||||
if let Some(wrap) = app_config.wrap.clone() {
|
|
||||||
app_config.set_wrap(&wrap)?;
|
|
||||||
}
|
|
||||||
app_config.setup_document_loaders();
|
|
||||||
app_config.setup_user_agent();
|
|
||||||
app_config.resolve_model()?;
|
|
||||||
Ok(app_config)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resolve_model(&mut self) -> Result<()> {
|
|
||||||
if self.model_id.is_empty() {
|
|
||||||
let models = list_models(self, crate::client::ModelType::Chat);
|
|
||||||
if models.is_empty() {
|
|
||||||
anyhow::bail!("No available model");
|
|
||||||
}
|
|
||||||
self.model_id = models[0].id();
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn vault_password_file(&self) -> PathBuf {
|
|
||||||
match &self.vault_password_file {
|
|
||||||
Some(path) => match path.exists() {
|
|
||||||
true => path.clone(),
|
|
||||||
false => gman::config::Config::local_provider_password_file(),
|
|
||||||
},
|
|
||||||
None => gman::config::Config::local_provider_password_file(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn editor(&self) -> Result<String> {
|
|
||||||
super::EDITOR.get_or_init(move || {
|
|
||||||
let editor = self.editor.clone()
|
|
||||||
.or_else(|| env::var("VISUAL").ok().or_else(|| env::var("EDITOR").ok()))
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
if cfg!(windows) {
|
|
||||||
"notepad".to_string()
|
|
||||||
} else {
|
|
||||||
"nano".to_string()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
which::which(&editor).ok().map(|_| editor)
|
|
||||||
})
|
|
||||||
.clone()
|
|
||||||
.ok_or_else(|| anyhow!("Editor not found. Please add the `editor` configuration or set the $EDITOR or $VISUAL environment variable."))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sync_models_url(&self) -> String {
|
|
||||||
self.sync_models_url
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| super::SYNC_MODELS_URL.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn light_theme(&self) -> bool {
|
|
||||||
matches!(self.theme.as_deref(), Some("light"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_options(&self) -> Result<RenderOptions> {
|
|
||||||
let theme = if self.highlight {
|
|
||||||
let theme_mode = if self.light_theme() { "light" } else { "dark" };
|
|
||||||
let theme_filename = format!("{theme_mode}.tmTheme");
|
|
||||||
let theme_path = paths::local_path(&theme_filename);
|
|
||||||
if theme_path.exists() {
|
|
||||||
let theme = ThemeSet::get_theme(&theme_path)
|
|
||||||
.with_context(|| format!("Invalid theme at '{}'", theme_path.display()))?;
|
|
||||||
Some(theme)
|
|
||||||
} else {
|
|
||||||
let theme = if self.light_theme() {
|
|
||||||
decode_bin(super::LIGHT_THEME).context("Invalid builtin light theme")?
|
|
||||||
} else {
|
|
||||||
decode_bin(super::DARK_THEME).context("Invalid builtin dark theme")?
|
|
||||||
};
|
|
||||||
Some(theme)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let wrap = if *IS_STDOUT_TERMINAL {
|
|
||||||
self.wrap.clone()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let truecolor = matches!(
|
|
||||||
env::var("COLORTERM").as_ref().map(|v| v.as_str()),
|
|
||||||
Ok("truecolor")
|
|
||||||
);
|
|
||||||
Ok(RenderOptions::new(theme, wrap, self.wrap_code, truecolor))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn print_markdown(&self, text: &str) -> Result<()> {
|
|
||||||
if *IS_STDOUT_TERMINAL {
|
|
||||||
let render_options = self.render_options()?;
|
|
||||||
let mut markdown_render = MarkdownRender::init(render_options)?;
|
|
||||||
println!("{}", markdown_render.render(text));
|
|
||||||
} else {
|
|
||||||
println!("{text}");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppConfig {
|
|
||||||
pub fn set_wrap(&mut self, value: &str) -> Result<()> {
|
|
||||||
if value == "no" {
|
|
||||||
self.wrap = None;
|
|
||||||
} else if value == "auto" {
|
|
||||||
self.wrap = Some(value.into());
|
|
||||||
} else {
|
|
||||||
value
|
|
||||||
.parse::<u16>()
|
|
||||||
.map_err(|_| anyhow!("Invalid wrap value"))?;
|
|
||||||
self.wrap = Some(value.into())
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setup_document_loaders(&mut self) {
|
|
||||||
[("pdf", "pdftotext $1 -"), ("docx", "pandoc --to plain $1")]
|
|
||||||
.into_iter()
|
|
||||||
.for_each(|(k, v)| {
|
|
||||||
let (k, v) = (k.to_string(), v.to_string());
|
|
||||||
self.document_loaders.entry(k).or_insert(v);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setup_user_agent(&mut self) {
|
|
||||||
if let Some("auto") = self.user_agent.as_deref() {
|
|
||||||
self.user_agent = Some(format!(
|
|
||||||
"{}/{}",
|
|
||||||
env!("CARGO_CRATE_NAME"),
|
|
||||||
env!("CARGO_PKG_VERSION")
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_envs(&mut self) {
|
|
||||||
if let Ok(v) = env::var(get_env_name("model")) {
|
|
||||||
self.model_id = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = super::read_env_value::<f64>(&get_env_name("temperature")) {
|
|
||||||
self.temperature = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = super::read_env_value::<f64>(&get_env_name("top_p")) {
|
|
||||||
self.top_p = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("dry_run")) {
|
|
||||||
self.dry_run = v;
|
|
||||||
}
|
|
||||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("stream")) {
|
|
||||||
self.stream = v;
|
|
||||||
}
|
|
||||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("save")) {
|
|
||||||
self.save = v;
|
|
||||||
}
|
|
||||||
if let Ok(v) = env::var(get_env_name("keybindings"))
|
|
||||||
&& v == "vi"
|
|
||||||
{
|
|
||||||
self.keybindings = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("editor")) {
|
|
||||||
self.editor = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("wrap")) {
|
|
||||||
self.wrap = v;
|
|
||||||
}
|
|
||||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("wrap_code")) {
|
|
||||||
self.wrap_code = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("function_calling_support")) {
|
|
||||||
self.function_calling_support = v;
|
|
||||||
}
|
|
||||||
if let Ok(v) = env::var(get_env_name("mapping_tools"))
|
|
||||||
&& let Ok(v) = serde_json::from_str(&v)
|
|
||||||
{
|
|
||||||
self.mapping_tools = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("enabled_tools")) {
|
|
||||||
self.enabled_tools = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("mcp_server_support")) {
|
|
||||||
self.mcp_server_support = v;
|
|
||||||
}
|
|
||||||
if let Ok(v) = env::var(get_env_name("mapping_mcp_servers"))
|
|
||||||
&& let Ok(v) = serde_json::from_str(&v)
|
|
||||||
{
|
|
||||||
self.mapping_mcp_servers = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("enabled_mcp_servers")) {
|
|
||||||
self.enabled_mcp_servers = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("repl_prelude")) {
|
|
||||||
self.repl_prelude = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("cmd_prelude")) {
|
|
||||||
self.cmd_prelude = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("agent_session")) {
|
|
||||||
self.agent_session = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = super::read_env_bool(&get_env_name("save_session")) {
|
|
||||||
self.save_session = v;
|
|
||||||
}
|
|
||||||
if let Some(Some(v)) =
|
|
||||||
super::read_env_value::<usize>(&get_env_name("compression_threshold"))
|
|
||||||
{
|
|
||||||
self.compression_threshold = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("summarization_prompt")) {
|
|
||||||
self.summarization_prompt = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("summary_context_prompt")) {
|
|
||||||
self.summary_context_prompt = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("rag_embedding_model")) {
|
|
||||||
self.rag_embedding_model = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("rag_reranker_model")) {
|
|
||||||
self.rag_reranker_model = v;
|
|
||||||
}
|
|
||||||
if let Some(Some(v)) = super::read_env_value::<usize>(&get_env_name("rag_top_k")) {
|
|
||||||
self.rag_top_k = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = super::read_env_value::<usize>(&get_env_name("rag_chunk_size")) {
|
|
||||||
self.rag_chunk_size = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = super::read_env_value::<usize>(&get_env_name("rag_chunk_overlap")) {
|
|
||||||
self.rag_chunk_overlap = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("rag_template")) {
|
|
||||||
self.rag_template = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(v) = env::var(get_env_name("document_loaders"))
|
|
||||||
&& let Ok(v) = serde_json::from_str(&v)
|
|
||||||
{
|
|
||||||
self.document_loaders = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("highlight")) {
|
|
||||||
self.highlight = v;
|
|
||||||
}
|
|
||||||
if *NO_COLOR {
|
|
||||||
self.highlight = false;
|
|
||||||
}
|
|
||||||
if self.highlight && self.theme.is_none() {
|
|
||||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("theme")) {
|
|
||||||
self.theme = v;
|
|
||||||
} else if *IS_STDOUT_TERMINAL
|
|
||||||
&& let Ok(color_scheme) = color_scheme(QueryOptions::default())
|
|
||||||
{
|
|
||||||
let theme = match color_scheme {
|
|
||||||
ColorScheme::Dark => "dark",
|
|
||||||
ColorScheme::Light => "light",
|
|
||||||
};
|
|
||||||
self.theme = Some(theme.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("left_prompt")) {
|
|
||||||
self.left_prompt = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("right_prompt")) {
|
|
||||||
self.right_prompt = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("user_agent")) {
|
|
||||||
self.user_agent = v;
|
|
||||||
}
|
|
||||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("save_shell_history")) {
|
|
||||||
self.save_shell_history = v;
|
|
||||||
}
|
|
||||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("sync_models_url")) {
|
|
||||||
self.sync_models_url = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppConfig {
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn set_temperature_default(&mut self, value: Option<f64>) {
|
|
||||||
self.temperature = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn set_top_p_default(&mut self, value: Option<f64>) {
|
|
||||||
self.top_p = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn set_enabled_tools_default(&mut self, value: Option<String>) {
|
|
||||||
self.enabled_tools = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn set_enabled_mcp_servers_default(&mut self, value: Option<String>) {
|
|
||||||
self.enabled_mcp_servers = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn set_save_session_default(&mut self, value: Option<bool>) {
|
|
||||||
self.save_session = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn set_compression_threshold_default(&mut self, value: Option<usize>) {
|
|
||||||
self.compression_threshold = value.unwrap_or_default();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn set_rag_reranker_model_default(&mut self, value: Option<String>) {
|
|
||||||
self.rag_reranker_model = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn set_rag_top_k_default(&mut self, value: usize) {
|
|
||||||
self.rag_top_k = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn set_model_id_default(&mut self, model_id: String) {
|
|
||||||
self.model_id = model_id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::config::Config;
|
|
||||||
|
|
||||||
fn cached_editor() -> Option<String> {
|
|
||||||
super::super::EDITOR.get().cloned().flatten()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_config_copies_serialized_fields() {
|
|
||||||
let cfg = Config {
|
|
||||||
model_id: "test-model".to_string(),
|
|
||||||
temperature: Some(0.7),
|
|
||||||
top_p: Some(0.9),
|
|
||||||
dry_run: true,
|
|
||||||
stream: false,
|
|
||||||
save: true,
|
|
||||||
highlight: false,
|
|
||||||
compression_threshold: 2000,
|
|
||||||
rag_top_k: 10,
|
|
||||||
clients: vec![ClientConfig::default()],
|
|
||||||
..Config::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let app = AppConfig::from_config(cfg).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(app.model_id, "test-model");
|
|
||||||
assert_eq!(app.temperature, Some(0.7));
|
|
||||||
assert_eq!(app.top_p, Some(0.9));
|
|
||||||
assert!(app.dry_run);
|
|
||||||
assert!(!app.stream);
|
|
||||||
assert!(app.save);
|
|
||||||
assert!(!app.highlight);
|
|
||||||
assert_eq!(app.compression_threshold, 2000);
|
|
||||||
assert_eq!(app.rag_top_k, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_config_copies_clients() {
|
|
||||||
let cfg = Config {
|
|
||||||
model_id: "test-model".to_string(),
|
|
||||||
clients: vec![ClientConfig::default()],
|
|
||||||
..Config::default()
|
|
||||||
};
|
|
||||||
let app = AppConfig::from_config(cfg).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(app.clients.len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_config_copies_mapping_fields() {
|
|
||||||
let mut cfg = Config {
|
|
||||||
model_id: "test-model".to_string(),
|
|
||||||
clients: vec![ClientConfig::default()],
|
|
||||||
..Config::default()
|
|
||||||
};
|
|
||||||
cfg.mapping_tools
|
|
||||||
.insert("alias".to_string(), "real_tool".to_string());
|
|
||||||
cfg.mapping_mcp_servers
|
|
||||||
.insert("gh".to_string(), "github-mcp".to_string());
|
|
||||||
|
|
||||||
let app = AppConfig::from_config(cfg).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
app.mapping_tools.get("alias"),
|
|
||||||
Some(&"real_tool".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
app.mapping_mcp_servers.get("gh"),
|
|
||||||
Some(&"github-mcp".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn editor_returns_configured_value() {
|
|
||||||
let configured = cached_editor()
|
|
||||||
.unwrap_or_else(|| std::env::current_exe().unwrap().display().to_string());
|
|
||||||
let app = AppConfig {
|
|
||||||
editor: Some(configured.clone()),
|
|
||||||
..AppConfig::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(app.editor().unwrap(), configured);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn editor_falls_back_to_env() {
|
|
||||||
if let Some(expected) = cached_editor() {
|
|
||||||
let app = AppConfig::default();
|
|
||||||
assert_eq!(app.editor().unwrap(), expected);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let expected = std::env::current_exe().unwrap().display().to_string();
|
|
||||||
unsafe {
|
|
||||||
std::env::set_var("VISUAL", &expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
let app = AppConfig::default();
|
|
||||||
let result = app.editor();
|
|
||||||
|
|
||||||
assert!(result.is_ok());
|
|
||||||
assert_eq!(result.unwrap(), expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn light_theme_default_is_false() {
|
|
||||||
let app = AppConfig::default();
|
|
||||||
assert!(!app.light_theme());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sync_models_url_has_default() {
|
|
||||||
let app = AppConfig::default();
|
|
||||||
let url = app.sync_models_url();
|
|
||||||
assert!(!url.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_config_copies_serde_fields() {
|
|
||||||
let cfg = Config {
|
|
||||||
model_id: "provider:model-x".to_string(),
|
|
||||||
temperature: Some(0.42),
|
|
||||||
compression_threshold: 1234,
|
|
||||||
..Config::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let app = AppConfig::from_config(cfg).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(app.model_id, "provider:model-x");
|
|
||||||
assert_eq!(app.temperature, Some(0.42));
|
|
||||||
assert_eq!(app.compression_threshold, 1234);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_config_installs_default_document_loaders() {
|
|
||||||
let cfg = Config {
|
|
||||||
model_id: "provider:test".to_string(),
|
|
||||||
..Config::default()
|
|
||||||
};
|
|
||||||
let app = AppConfig::from_config(cfg).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
app.document_loaders.get("pdf"),
|
|
||||||
Some(&"pdftotext $1 -".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
app.document_loaders.get("docx"),
|
|
||||||
Some(&"pandoc --to plain $1".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_config_resolves_auto_user_agent() {
|
|
||||||
let cfg = Config {
|
|
||||||
model_id: "provider:test".to_string(),
|
|
||||||
user_agent: Some("auto".to_string()),
|
|
||||||
..Config::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let app = AppConfig::from_config(cfg).unwrap();
|
|
||||||
|
|
||||||
let ua = app.user_agent.as_deref().unwrap();
|
|
||||||
assert!(ua != "auto", "user_agent should have been resolved");
|
|
||||||
assert!(ua.contains('/'), "user_agent should be '<name>/<version>'");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_config_preserves_explicit_user_agent() {
|
|
||||||
let cfg = Config {
|
|
||||||
model_id: "provider:test".to_string(),
|
|
||||||
user_agent: Some("custom/1.0".to_string()),
|
|
||||||
..Config::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let app = AppConfig::from_config(cfg).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(app.user_agent.as_deref(), Some("custom/1.0"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_config_validates_wrap_value() {
|
|
||||||
let cfg = Config {
|
|
||||||
model_id: "provider:test".to_string(),
|
|
||||||
wrap: Some("invalid".to_string()),
|
|
||||||
..Config::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = AppConfig::from_config(cfg);
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_config_accepts_wrap_auto() {
|
|
||||||
let cfg = Config {
|
|
||||||
model_id: "provider:test".to_string(),
|
|
||||||
wrap: Some("auto".to_string()),
|
|
||||||
..Config::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let app = AppConfig::from_config(cfg).unwrap();
|
|
||||||
assert_eq!(app.wrap.as_deref(), Some("auto"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_model_errors_when_no_models_available() {
|
|
||||||
let mut app = AppConfig {
|
|
||||||
model_id: String::new(),
|
|
||||||
clients: vec![],
|
|
||||||
..AppConfig::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = app.resolve_model();
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_model_keeps_explicit_model_id() {
|
|
||||||
let mut app = AppConfig {
|
|
||||||
model_id: "provider:explicit".to_string(),
|
|
||||||
..AppConfig::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
app.resolve_model().unwrap();
|
|
||||||
assert_eq!(app.model_id, "provider:explicit");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
use super::mcp_factory::{McpFactory, McpServerKey};
|
|
||||||
use super::rag_cache::RagCache;
|
|
||||||
use crate::config::AppConfig;
|
|
||||||
use crate::function::Functions;
|
|
||||||
use crate::mcp::{McpRegistry, McpServersConfig};
|
|
||||||
use crate::utils::AbortSignal;
|
|
||||||
use crate::vault::{GlobalVault, Vault};
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct AppState {
|
|
||||||
pub config: Arc<AppConfig>,
|
|
||||||
pub vault: GlobalVault,
|
|
||||||
pub mcp_factory: Arc<McpFactory>,
|
|
||||||
pub rag_cache: Arc<RagCache>,
|
|
||||||
pub mcp_config: Option<McpServersConfig>,
|
|
||||||
pub mcp_log_path: Option<PathBuf>,
|
|
||||||
pub mcp_registry: Option<Arc<McpRegistry>>,
|
|
||||||
pub functions: Functions,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppState {
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn test_default() -> Self {
|
|
||||||
Self {
|
|
||||||
config: Arc::new(AppConfig::default()),
|
|
||||||
vault: Arc::new(Vault::default()),
|
|
||||||
mcp_factory: Arc::new(McpFactory::default()),
|
|
||||||
rag_cache: Arc::new(RagCache::default()),
|
|
||||||
mcp_config: None,
|
|
||||||
mcp_log_path: None,
|
|
||||||
mcp_registry: None,
|
|
||||||
functions: Functions::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn init(
|
|
||||||
config: Arc<AppConfig>,
|
|
||||||
log_path: Option<PathBuf>,
|
|
||||||
start_mcp_servers: bool,
|
|
||||||
abort_signal: AbortSignal,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let vault = Arc::new(Vault::init(&config));
|
|
||||||
|
|
||||||
let mcp_registry = McpRegistry::init(
|
|
||||||
log_path,
|
|
||||||
start_mcp_servers,
|
|
||||||
config.enabled_mcp_servers.clone(),
|
|
||||||
abort_signal,
|
|
||||||
&config,
|
|
||||||
&vault,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mcp_config = mcp_registry.mcp_config().cloned();
|
|
||||||
let mcp_log_path = mcp_registry.log_path().cloned();
|
|
||||||
|
|
||||||
let mcp_factory = Arc::new(McpFactory::default());
|
|
||||||
if let Some(mcp_servers_config) = &mcp_config {
|
|
||||||
for (id, handle) in mcp_registry.running_servers() {
|
|
||||||
if let Some(spec) = mcp_servers_config.mcp_servers.get(id) {
|
|
||||||
let key = McpServerKey::from_spec(id, spec);
|
|
||||||
mcp_factory.insert_active(key, handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut functions = Functions::init(config.visible_tools.as_ref().unwrap_or(&Vec::new()))?;
|
|
||||||
if !mcp_registry.is_empty() && config.mcp_server_support {
|
|
||||||
functions.append_mcp_meta_functions(mcp_registry.list_started_servers());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mcp_registry = if mcp_registry.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(Arc::new(mcp_registry))
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
config,
|
|
||||||
vault,
|
|
||||||
mcp_factory,
|
|
||||||
rag_cache: Arc::new(RagCache::default()),
|
|
||||||
mcp_config,
|
|
||||||
mcp_log_path,
|
|
||||||
mcp_registry,
|
|
||||||
functions,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||