30 Commits

Author SHA1 Message Date
6c17462040 feat: Added todo__clear function to the todo system and updated REPL commands to have a .clear todo as well for significant changes in agent direction
CI / All (ubuntu-latest) (push) Failing after 24s
CI / All (macos-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-04-02 13:13:44 -06:00
1536cf384c fix: Clarified user text input interaction
CI / All (ubuntu-latest) (push) Failing after 23s
CI / All (macos-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-03-30 16:27:22 -06:00
d6842d7e29 fix: recursion bug with similarly named Bash search functions in the explore agent
CI / All (ubuntu-latest) (push) Failing after 24s
CI / All (macos-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-03-30 13:32:13 -06:00
fbc0acda2a feat: Added available tools to prompts for sisyphus and code-reviewer agent families 2026-03-30 13:13:30 -06:00
0327d041b6 feat: Added available tools to coder prompt 2026-03-30 11:11:43 -06:00
6a01fd4fbd Merge branch 'main' of github.com:Dark-Alex-17/loki 2026-03-30 10:15:51 -06:00
d822180205 fix: updated the error for unauthenticated oauth to include the REPL .authenticated command 2026-03-28 11:57:01 -06:00
89d0fdce26 feat: Improved token efficiency when delegating from sisyphus -> coder 2026-03-18 15:07:29 -06:00
b3ecdce979 build: Removed deprecated agent functions from the .shared/utils.sh script 2026-03-18 15:04:14 -06:00
3873821a31 fix: Corrected a bug in the coder agent that wasn't outputting a summary of the changes made, so the parent Sisyphus agent has no idea if the agent worked or not
CI / All (macos-latest) (push) Has been cancelled
CI / All (ubuntu-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-03-17 14:57:07 -06:00
9c2801b643 feat: modified sisyphus agents to use the new ddg-search MCP server for web searches instead of built-in model searches 2026-03-17 14:55:33 -06:00
d78820dcd4 fix: Claude code system prompt injected into claude requests to make them valid once again
CI / All (macos-latest) (push) Has been cancelled
CI / All (ubuntu-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-03-17 10:44:50 -06:00
d43c4232a2 fix: Do not inject tools when models don't support them; detect this conflict before API calls happen 2026-03-17 09:35:51 -06:00
f41c85b703 style: Applied formatting across new inquire files
CI / All (macos-latest) (push) Has been cancelled
CI / All (ubuntu-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-03-16 12:39:20 -06:00
9e056bdcf0 feat: Added support for specifying a custom response to multiple-choice prompts when nothing suits the user's needs 2026-03-16 12:37:47 -06:00
d6022b9f98 feat: Supported theming in the inquire prompts in the REPL 2026-03-16 12:36:20 -06:00
6fc1abf94a build: upgraded to the most recent version of the inquire crate 2026-03-16 12:31:28 -06:00
92ea0f624e docs: Fixed a spacing issue in the example agent configuration
CI / All (macos-latest) (push) Has been cancelled
CI / All (ubuntu-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-03-13 14:19:39 -06:00
c3fd8fbc1c docs: Added the file-reviewer agent to the AGENTS docs
CI / All (macos-latest) (push) Has been cancelled
CI / All (ubuntu-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-03-13 14:07:13 -06:00
7fd3f7761c docs: Updated the MCP-SERVERS docs to mention the ddg-search MCP server
CI / All (macos-latest) (push) Has been cancelled
CI / All (ubuntu-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-03-13 13:32:58 -06:00
05e19098b2 feat: Added the duckduckgo-search MCP server for searching the web (in addition to the built-in tools for web searches)
CI / All (macos-latest) (push) Has been cancelled
CI / All (ubuntu-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-03-13 13:29:56 -06:00
60067ae757 Merge branch 'main' of github.com:Dark-Alex-17/loki
CI / All (macos-latest) (push) Has been cancelled
CI / All (ubuntu-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-03-12 15:17:54 -06:00
c72003b0b6 fix: Implemented the path normalization fix for the oracle and explore agents 2026-03-12 13:38:15 -06:00
7c9d500116 chore: Added GPT-5.2 to models.yaml 2026-03-12 13:30:23 -06:00
6b2c87b562 docs: Updated the docs to now explicitly mention Gemini OAuth support 2026-03-12 13:30:10 -06:00
b2dbdfb4b1 feat: Support for Gemini OAuth 2026-03-12 13:29:47 -06:00
063e198f96 refactor: Made the oauth module more generic so it can support loopback OAuth (not just manual) 2026-03-12 13:28:09 -06:00
73cbe16ec1 fix: Updated the atlassian MCP server endpoint to account for future deprecation 2026-03-12 12:49:26 -06:00
bdea854a9f fix: Fixed a bug in the coder agent that was causing the agent to create absolute paths from the current directory 2026-03-12 12:39:49 -06:00
9b4c800597 fix: The REPL .authenticate command works from within sessions, agents, and roles with pre-configured models
CI / All (macos-latest) (push) Has been cancelled
CI / All (ubuntu-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled
2026-03-12 09:08:17 -06:00
37 changed files with 474 additions and 281 deletions
Generated
+14 -70
View File
@@ -1238,7 +1238,7 @@ dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width 0.2.2",
"unicode-width",
"windows-sys 0.61.2",
]
@@ -1338,22 +1338,6 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crossterm"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
dependencies = [
"bitflags 1.3.2",
"crossterm_winapi",
"libc",
"mio 0.8.11",
"parking_lot",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm"
version = "0.28.1"
@@ -1363,7 +1347,7 @@ dependencies = [
"bitflags 2.11.0",
"crossterm_winapi",
"filedescriptor",
"mio 1.1.1",
"mio",
"parking_lot",
"rustix 0.38.44",
"serde",
@@ -1382,7 +1366,7 @@ dependencies = [
"crossterm_winapi",
"derive_more 2.1.1",
"document-features",
"mio 1.1.1",
"mio",
"parking_lot",
"rustix 1.1.3",
"signal-hook",
@@ -2164,7 +2148,7 @@ version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
dependencies = [
"unicode-width 0.2.2",
"unicode-width",
]
[[package]]
@@ -2838,19 +2822,16 @@ dependencies = [
[[package]]
name = "inquire"
version = "0.7.5"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a"
checksum = "6654738b8024300cf062d04a1c13c10c8e2cea598ec1c47dc9b6641159429756"
dependencies = [
"bitflags 2.11.0",
"crossterm 0.25.0",
"crossterm 0.29.0",
"dyn-clone",
"fuzzy-matcher",
"fxhash",
"newline-converter",
"once_cell",
"unicode-segmentation",
"unicode-width 0.1.14",
"unicode-width",
]
[[package]]
@@ -3242,7 +3223,7 @@ dependencies = [
"tokio-graceful",
"tokio-stream",
"unicode-segmentation",
"unicode-width 0.2.2",
"unicode-width",
"url",
"urlencoding",
"uuid",
@@ -3457,18 +3438,6 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "mio"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.48.0",
]
[[package]]
name = "mio"
version = "1.1.1"
@@ -3533,15 +3502,6 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "newline-converter"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "nix"
version = "0.26.4"
@@ -4555,7 +4515,7 @@ dependencies = [
"strum_macros 0.26.4",
"thiserror 2.0.18",
"unicode-segmentation",
"unicode-width 0.2.2",
"unicode-width",
]
[[package]]
@@ -5377,8 +5337,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
dependencies = [
"libc",
"mio 0.8.11",
"mio 1.1.1",
"mio",
"signal-hook",
]
@@ -5682,7 +5641,7 @@ dependencies = [
"cfg-if",
"libc",
"memchr",
"mio 1.1.1",
"mio",
"terminal-trx",
"windows-sys 0.59.0",
"xterm-color",
@@ -5717,7 +5676,7 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width 0.2.2",
"unicode-width",
]
[[package]]
@@ -5862,7 +5821,7 @@ checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
dependencies = [
"bytes",
"libc",
"mio 1.1.1",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
@@ -6293,12 +6252,6 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-width"
version = "0.2.2"
@@ -6966,15 +6919,6 @@ dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
+1 -1
View File
@@ -19,7 +19,7 @@ bytes = "1.4.0"
clap = { version = "4.5.40", features = ["cargo", "derive", "wrap_help"] }
dirs = "6.0.0"
futures-util = "0.3.29"
inquire = "0.7.0"
inquire = "0.9.4"
is-terminal = "0.4.9"
reedline = "0.40.0"
serde = { version = "1.0.152", features = ["derive"] }
+3 -131
View File
@@ -2,68 +2,6 @@
# Shared Agent Utilities - Minimal, focused helper functions
set -euo pipefail
#############################
## CONTEXT FILE MANAGEMENT ##
#############################
get_context_file() {
local project_dir="${LLM_AGENT_VAR_PROJECT_DIR:-.}"
echo "${project_dir}/.loki-context"
}
# Initialize context file for a new task
# Usage: init_context "Task description"
init_context() {
local task="$1"
local project_dir="${LLM_AGENT_VAR_PROJECT_DIR:-.}"
local context_file
context_file=$(get_context_file)
cat > "${context_file}" <<EOF
## Project: ${project_dir}
## Task: ${task}
## Started: $(date -Iseconds)
### Prior Findings
EOF
}
# Append findings to the context file
# Usage: append_context "agent_name" "finding summary
append_context() {
local agent="$1"
local finding="$2"
local context_file
context_file=$(get_context_file)
if [[ -f "${context_file}" ]]; then
{
echo ""
echo "[${agent}]:"
echo "${finding}"
} >> "${context_file}"
fi
}
# Read the current context (returns empty string if no context)
# Usage: context=$(read_context)
read_context() {
local context_file
context_file=$(get_context_file)
if [[ -f "${context_file}" ]]; then
cat "${context_file}"
fi
}
# Clear the context file
clear_context() {
local context_file
context_file=$(get_context_file)
rm -f "${context_file}"
}
#######################
## PROJECT DETECTION ##
#######################
@@ -279,9 +217,9 @@ _detect_with_llm() {
evidence=$(_gather_project_evidence "${dir}")
local prompt
prompt=$(cat <<-EOF
Analyze this project directory and determine the project type, primary language, and the correct shell commands to build, test, and check (lint/typecheck) it.
EOF
)
prompt+=$'\n'"${evidence}"$'\n'
@@ -348,77 +286,11 @@ detect_project() {
echo '{"type":"unknown","build":"","test":"","check":""}'
}
######################
## AGENT INVOCATION ##
######################
# Invoke a subagent with optional context injection
# Usage: invoke_agent <agent_name> <prompt> [extra_args...]
invoke_agent() {
local agent="$1"
local prompt="$2"
shift 2
local context
context=$(read_context)
local full_prompt
if [[ -n "${context}" ]]; then
full_prompt="## Orchestrator Context
The orchestrator (sisyphus) has gathered this context from prior work:
<context>
${context}
</context>
## Your Task
${prompt}"
else
full_prompt="${prompt}"
fi
env AUTO_CONFIRM=true loki --agent "${agent}" "$@" "${full_prompt}" 2>&1
}
# Invoke a subagent and capture a summary of its findings
# Usage: result=$(invoke_agent_with_summary "explore" "find auth patterns")
invoke_agent_with_summary() {
local agent="$1"
local prompt="$2"
shift 2
local output
output=$(invoke_agent "${agent}" "${prompt}" "$@")
local summary=""
if echo "${output}" | grep -q "FINDINGS:"; then
summary=$(echo "${output}" | sed -n '/FINDINGS:/,/^[A-Z_]*COMPLETE/p' | grep "^- " | sed 's/^- / - /')
elif echo "${output}" | grep -q "CODER_COMPLETE:"; then
summary=$(echo "${output}" | grep "CODER_COMPLETE:" | sed 's/CODER_COMPLETE: *//')
elif echo "${output}" | grep -q "ORACLE_COMPLETE"; then
summary=$(echo "${output}" | sed -n '/^## Recommendation/,/^## /{/^## Recommendation/d;/^## /d;p}' | sed '/^$/d' | head -10)
fi
# Failsafe: extract up to 5 meaningful lines if no markers found
if [[ -z "${summary}" ]]; then
summary=$(echo "${output}" | grep -v "^$" | grep -v "^#" | grep -v "^\-\-\-" | tail -10 | head -5)
fi
if [[ -n "${summary}" ]]; then
append_context "${agent}" "${summary}"
fi
echo "${output}"
}
###########################
## FILE SEARCH UTILITIES ##
###########################
search_files() {
_search_files() {
local pattern="$1"
local dir="${2:-.}"
+3
View File
@@ -122,3 +122,6 @@ instructions: |
- Project: {{project_dir}}
- CWD: {{__cwd__}}
- Shell: {{__shell__}}
## Available Tools:
{{__tools__}}
+29 -7
View File
@@ -29,11 +29,30 @@ instructions: |
## Your Mission
Given an implementation task:
1. Understand what to build (from context provided)
2. Study existing patterns (read 1-2 similar files)
1. Check for orchestrator context first (see below)
2. Fill gaps only. Read files NOT already covered in context
3. Write the code (using tools, NOT chat output)
4. Verify it compiles/builds
5. Signal completion
5. Signal completion with a summary
## Using Orchestrator Context (IMPORTANT)
When spawned by sisyphus, your prompt will often contain a `<context>` block
with prior findings: file paths, code patterns, and conventions discovered by
explore agents.
**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 the referenced files ONLY IF you need more detail (e.g. full function
signature, import list, or adjacent code not included in the snippet)
4. If context includes a "Conventions" section, follow it exactly
**If context is NOT provided or is too vague to act on:**
Fall back to self-exploration: grep for similar files, read 1-2 examples,
match their style.
**Never ignore provided context.** It represents work already done upstream.
## Todo System
@@ -82,12 +101,13 @@ instructions: |
## Completion Signal
End with:
When done, end your response with a summary so the parent agent knows what happened:
```
CODER_COMPLETE: [summary of what was implemented]
CODER_COMPLETE: [summary of what was implemented, which files were created/modified, and build status]
```
Or if failed:
Or if something went wrong:
```
CODER_FAILED: [what went wrong]
```
@@ -104,4 +124,6 @@ instructions: |
- Project: {{project_dir}}
- CWD: {{__cwd__}}
- Shell: {{__shell__}}
## Available tools:
{{__tools__}}
+26 -6
View File
@@ -14,11 +14,28 @@ _project_dir() {
(cd "${dir}" 2>/dev/null && pwd) || echo "${dir}"
}
# Normalize a path to be relative to project root.
# Strips the project_dir prefix if the LLM passes an absolute path.
# Usage: local rel_path; rel_path=$(_normalize_path "/abs/or/rel/path")
_normalize_path() {
local input_path="$1"
local project_dir
project_dir=$(_project_dir)
if [[ "${input_path}" == /* ]]; then
input_path="${input_path#"${project_dir}"/}"
fi
input_path="${input_path#./}"
echo "${input_path}"
}
# @cmd Read a file's contents before modifying
# @option --path! Path to the file (relative to project root)
read_file() {
local file_path
# shellcheck disable=SC2154
local file_path="${argc_path}"
file_path=$(_normalize_path "${argc_path}")
local project_dir
project_dir=$(_project_dir)
local full_path="${project_dir}/${file_path}"
@@ -39,7 +56,8 @@ read_file() {
# @option --path! Path for the file (relative to project root)
# @option --content! Complete file contents to write
write_file() {
local file_path="${argc_path}"
local file_path
file_path=$(_normalize_path "${argc_path}")
# shellcheck disable=SC2154
local content="${argc_content}"
local project_dir
@@ -47,7 +65,7 @@ write_file() {
local full_path="${project_dir}/${file_path}"
mkdir -p "$(dirname "${full_path}")"
echo "${content}" > "${full_path}"
printf '%s' "${content}" > "${full_path}"
green "Wrote: ${file_path}" >> "$LLM_OUTPUT"
}
@@ -55,7 +73,8 @@ write_file() {
# @cmd Find files similar to a given path (for pattern matching)
# @option --path! Path to find similar files for
find_similar_files() {
local file_path="${argc_path}"
local file_path
file_path=$(_normalize_path "${argc_path}")
local project_dir
project_dir=$(_project_dir)
@@ -71,14 +90,14 @@ find_similar_files() {
! -name "$(basename "${file_path}")" \
! -name "*test*" \
! -name "*spec*" \
2>/dev/null | head -3)
2>/dev/null | sed "s|^${project_dir}/||" | head -3)
if [[ -z "${results}" ]]; then
results=$(find "${project_dir}/src" -type f -name "*.${ext}" \
! -name "*test*" \
! -name "*spec*" \
-not -path '*/target/*' \
2>/dev/null | head -3)
2>/dev/null | sed "s|^${project_dir}/||" | head -3)
fi
if [[ -n "${results}" ]]; then
@@ -186,6 +205,7 @@ search_code() {
grep -v '/target/' | \
grep -v '/node_modules/' | \
grep -v '/.git/' | \
sed "s|^${project_dir}/||" | \
head -20) || true
if [[ -n "${results}" ]]; then
+5 -1
View File
@@ -8,12 +8,13 @@ variables:
description: Project directory to explore
default: '.'
mcp_servers:
- ddg-search
global_tools:
- fs_read.sh
- fs_grep.sh
- fs_glob.sh
- fs_ls.sh
- web_search_loki.sh
instructions: |
You are a codebase explorer. Your job: Search, find, report. Nothing else.
@@ -67,6 +68,9 @@ instructions: |
## Context
- Project: {{project_dir}}
- CWD: {{__cwd__}}
## Available Tools:
{{__tools__}}
conversation_starters:
- 'Find how authentication is implemented'
+23 -5
View File
@@ -14,6 +14,21 @@ _project_dir() {
(cd "${dir}" 2>/dev/null && pwd) || echo "${dir}"
}
# Normalize a path to be relative to project root.
# Strips the project_dir prefix if the LLM passes an absolute path.
_normalize_path() {
local input_path="$1"
local project_dir
project_dir=$(_project_dir)
if [[ "${input_path}" == /* ]]; then
input_path="${input_path#"${project_dir}"/}"
fi
input_path="${input_path#./}"
echo "${input_path}"
}
# @cmd Get project structure and layout
get_structure() {
local project_dir
@@ -45,7 +60,7 @@ search_files() {
echo "" >> "$LLM_OUTPUT"
local results
results=$(search_files "${pattern}" "${project_dir}")
results=$(_search_files "${pattern}" "${project_dir}")
if [[ -n "${results}" ]]; then
echo "${results}" >> "$LLM_OUTPUT"
@@ -78,6 +93,7 @@ search_content() {
grep -v '/node_modules/' | \
grep -v '/.git/' | \
grep -v '/dist/' | \
sed "s|^${project_dir}/||" | \
head -30) || true
if [[ -n "${results}" ]]; then
@@ -91,8 +107,9 @@ search_content() {
# @option --path! Path to the file (relative to project root)
# @option --lines Maximum lines to read (default: 200)
read_file() {
local file_path
# shellcheck disable=SC2154
local file_path="${argc_path}"
file_path=$(_normalize_path "${argc_path}")
local max_lines="${argc_lines:-200}"
local project_dir
project_dir=$(_project_dir)
@@ -122,7 +139,8 @@ read_file() {
# @cmd Find similar files to a given file (for pattern matching)
# @option --path! Path to the reference file
find_similar() {
local file_path="${argc_path}"
local file_path
file_path=$(_normalize_path "${argc_path}")
local project_dir
project_dir=$(_project_dir)
@@ -138,7 +156,7 @@ find_similar() {
! -name "$(basename "${file_path}")" \
! -name "*test*" \
! -name "*spec*" \
2>/dev/null | head -5)
2>/dev/null | sed "s|^${project_dir}/||" | head -5)
if [[ -n "${results}" ]]; then
echo "${results}" >> "$LLM_OUTPUT"
@@ -147,7 +165,7 @@ find_similar() {
! -name "$(basename "${file_path}")" \
! -name "*test*" \
-not -path '*/target/*' \
2>/dev/null | head -5)
2>/dev/null | sed "s|^${project_dir}/||" | head -5)
if [[ -n "${results}" ]]; then
echo "${results}" >> "$LLM_OUTPUT"
else
+3
View File
@@ -108,3 +108,6 @@ instructions: |
## Context
- Project: {{project_dir}}
- CWD: {{__cwd__}}
## Available Tools:
{{__tools__}}
+5 -1
View File
@@ -8,12 +8,13 @@ variables:
description: Project directory for context
default: '.'
mcp_servers:
- ddg-search
global_tools:
- fs_read.sh
- fs_grep.sh
- fs_glob.sh
- fs_ls.sh
- web_search_loki.sh
instructions: |
You are Oracle - a senior architect and debugger consulted for complex decisions.
@@ -74,6 +75,9 @@ instructions: |
## Context
- Project: {{project_dir}}
- CWD: {{__cwd__}}
## Available Tools:
{{__tools__}}
conversation_starters:
- 'Review this architecture design'
+23 -4
View File
@@ -14,21 +14,38 @@ _project_dir() {
(cd "${dir}" 2>/dev/null && pwd) || echo "${dir}"
}
# Normalize a path to be relative to project root.
# Strips the project_dir prefix if the LLM passes an absolute path.
_normalize_path() {
local input_path="$1"
local project_dir
project_dir=$(_project_dir)
if [[ "${input_path}" == /* ]]; then
input_path="${input_path#"${project_dir}"/}"
fi
input_path="${input_path#./}"
echo "${input_path}"
}
# @cmd Read a file for analysis
# @option --path! Path to the file (relative to project root)
read_file() {
local project_dir
project_dir=$(_project_dir)
local file_path
# shellcheck disable=SC2154
local full_path="${project_dir}/${argc_path}"
file_path=$(_normalize_path "${argc_path}")
local full_path="${project_dir}/${file_path}"
if [[ ! -f "${full_path}" ]]; then
error "File not found: ${argc_path}" >> "$LLM_OUTPUT"
error "File not found: ${file_path}" >> "$LLM_OUTPUT"
return 1
fi
{
info "Reading: ${argc_path}"
info "Reading: ${file_path}"
echo ""
cat "${full_path}"
} >> "$LLM_OUTPUT"
@@ -80,6 +97,7 @@ search_code() {
grep -v '/target/' | \
grep -v '/node_modules/' | \
grep -v '/.git/' | \
sed "s|^${project_dir}/||" | \
head -30) || true
if [[ -n "${results}" ]]; then
@@ -113,7 +131,8 @@ analyze_with_command() {
# @cmd List directory contents
# @option --path Path to list (default: project root)
list_directory() {
local dir_path="${argc_path:-.}"
local dir_path
dir_path=$(_normalize_path "${argc_path:-.}")
local project_dir
project_dir=$(_project_dir)
local full_path="${project_dir}/${dir_path}"
+45 -6
View File
@@ -12,7 +12,7 @@ can_spawn_agents: true
max_concurrent_agents: 4
max_agent_depth: 3
inject_spawn_instructions: true
summarization_threshold: 4000
summarization_threshold: 8000
variables:
- name: project_dir
@@ -22,12 +22,13 @@ variables:
description: Auto-confirm command execution
default: '1'
mcp_servers:
- ddg-search
global_tools:
- fs_read.sh
- fs_grep.sh
- fs_glob.sh
- fs_ls.sh
- web_search_loki.sh
- execute_command.sh
instructions: |
@@ -69,6 +70,45 @@ instructions: |
| coder | Write/edit files, implement features | Creates/modifies files, runs builds |
| oracle | Architecture decisions, complex debugging | Advisory, high-quality reasoning |
## Coder Delegation Format (MANDATORY)
When spawning the `coder` agent, your prompt MUST include these sections.
The coder has NOT seen the codebase. Your prompt IS its entire context.
### Template:
```
## Goal
[1-2 sentences: what to build/modify and where]
## Reference Files
[Files that explore found, with what each demonstrates]
- `path/to/file.ext` - what pattern this file shows
- `path/to/other.ext` - what convention this file shows
## Code Patterns to Follow
[Paste ACTUAL code snippets from explore results, not descriptions]
<code>
// From path/to/file.ext - this is the pattern to follow:
[actual code explore found, 5-20 lines]
</code>
## Conventions
[Naming, imports, error handling, file organization]
- Convention 1
- Convention 2
## Constraints
[What NOT to do, scope boundaries]
- Do NOT modify X
- Only touch files in Y/
```
**CRITICAL**: Include actual code snippets, not just file paths.
If explore returned code patterns, paste them into the coder prompt.
Vague prompts like "follow existing patterns" waste coder's tokens on
re-exploration that you already did.
## Workflow Examples
### Example 1: Implementation task (explore -> coder, parallel exploration)
@@ -80,12 +120,12 @@ instructions: |
2. todo__add --task "Explore existing API patterns"
3. todo__add --task "Implement profile endpoint"
4. todo__add --task "Verify with build/test"
5. agent__spawn --agent explore --prompt "Find existing API endpoint patterns, route structures, and controller conventions"
6. agent__spawn --agent explore --prompt "Find existing data models and database query patterns"
5. agent__spawn --agent explore --prompt "Find existing API endpoint patterns, route structures, and controller conventions. Include code snippets."
6. agent__spawn --agent explore --prompt "Find existing data models and database query patterns. Include code snippets."
7. agent__collect --id <id1>
8. agent__collect --id <id2>
9. todo__done --id 1
10. agent__spawn --agent coder --prompt "Create user profiles endpoint following existing patterns. [Include context from explore results]"
10. agent__spawn --agent coder --prompt "<structured prompt using Coder Delegation Format above, including code snippets from explore results>"
11. agent__collect --id <coder_id>
12. todo__done --id 2
13. run_build
@@ -134,7 +174,6 @@ instructions: |
## When to Do It Yourself
- Single-file reads/writes
- Simple command execution
- Trivial changes (typos, renames)
- Quick file searches
+5 -1
View File
@@ -16,11 +16,15 @@
},
"atlassian": {
"command": "npx",
"args": ["-y", "mcp-remote@0.1.13", "https://mcp.atlassian.com/v1/sse"]
"args": ["-y", "mcp-remote@0.1.13", "https://mcp.atlassian.com/v1/mcp"]
},
"docker": {
"command": "uvx",
"args": ["mcp-server-docker"]
},
"ddg-search": {
"command": "uvx",
"args": ["duckduckgo-mcp-server"]
}
}
}
+1 -1
View File
@@ -32,7 +32,7 @@ max_concurrent_agents: 4 # Maximum number of agents that can run simulta
max_agent_depth: 3 # Maximum nesting depth for sub-agents (prevents runaway spawning)
inject_spawn_instructions: true # Inject the default agent spawning instructions into the agent's system prompt
summarization_model: null # Model to use for summarizing sub-agent output (e.g. 'openai:gpt-4o-mini'); defaults to current model
summarization_threshold: 4000 # Character threshold above which sub-agent output is summarized before returning to parent
summarization_threshold: 4000 # Character threshold above which sub-agent output is summarized before returning to parent
escalation_timeout: 300 # Seconds a sub-agent waits for a user interaction response before timing out (default: 5 minutes)
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
+1 -1
View File
@@ -77,7 +77,7 @@ visible_tools: # Which tools are visible to be compiled (and a
mcp_server_support: true # Enables or disables MCP servers (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')
enabled_mcp_servers: null # Which MCP servers to enable by default (e.g. 'github,slack,ddg-search')
# ---- Session ----
# See the [Session documentation](./docs/SESSIONS.md) for more information
+3 -1
View File
@@ -467,11 +467,12 @@ inject_todo_instructions: true # Include the default todo instructions into pr
### How It Works
1. When `inject_todo_instructions` is enabled, agents receive instructions on using four built-in tools:
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
@@ -714,6 +715,7 @@ Loki comes packaged with some useful built-in agents:
* `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`.
+8 -7
View File
@@ -120,13 +120,14 @@ For more information on sessions and how to use them in Loki, refer to the [sess
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 |
| `.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 |
| 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 |
![agent](./images/agents/sql.gif)
+16
View File
@@ -117,6 +117,22 @@ Display the current todo list with status of each item.
**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:
+19
View File
@@ -142,6 +142,25 @@ temporary localhost server to capture the callback automatically (e.g. Gemini) o
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:
![](../images/clients/gemini-oauth-page.png)
Simply click the `Advanced` link and click `Go to Loki (unsafe)` to continue the OAuth flow.
![](../images/clients/gemini-oauth-unverified.png)
![](../images/clients/gemini-oauth-unverified-allow.png)
**Step 3: Use normally**
Once authenticated, the client works like any other. Loki uses the stored OAuth tokens automatically:
+1
View File
@@ -55,6 +55,7 @@ Loki ships with a `functions/mcp.json` file that includes some useful MCP server
* [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:
+7
View File
@@ -3,6 +3,13 @@
# - https://platform.openai.com/docs/api-reference/chat
- provider: openai
models:
- name: gpt-5.2
max_input_tokens: 400000
max_output_tokens: 128000
input_price: 1.75
output_price: 14
supports_vision: true
supports_function_calling: true
- name: gpt-5.1
max_input_tokens: 400000
max_output_tokens: 128000
+40 -1
View File
@@ -11,6 +11,7 @@ use serde::Deserialize;
use serde_json::{Value, json};
const API_BASE: &str = "https://api.anthropic.com/v1";
const CLAUDE_CODE_PREFIX: &str = "You are Claude Code, Anthropic's official CLI for Claude.";
#[derive(Debug, Clone, Deserialize)]
pub struct ClaudeConfig {
@@ -84,7 +85,7 @@ async fn prepare_chat_completions(
let ready = oauth::prepare_oauth_access_token(client, &provider, self_.name()).await?;
if !ready {
bail!(
"OAuth configured but no tokens found for '{}'. Run: loki --authenticate {}",
"OAuth configured but no tokens found for '{}'. Run: 'loki --authenticate {}' or '.authenticate' in the REPL",
self_.name(),
self_.name()
);
@@ -94,6 +95,7 @@ async fn prepare_chat_completions(
for (key, value) in provider.extra_request_headers() {
request_data.header(key, value);
}
inject_oauth_system_prompt(&mut request_data.body);
} else if let Ok(api_key) = self_.get_api_key() {
request_data.header("x-api-key", api_key);
} else {
@@ -107,6 +109,43 @@ async fn prepare_chat_completions(
Ok(request_data)
}
/// Anthropic requires OAuth-authenticated requests to include a Claude Code
/// system prompt prefix in order to consider a request body as "valid".
///
/// This behavior was discovered 2026-03-17.
///
/// So this function injects the Claude Code system prompt into the request
/// body to make it a valid request.
fn inject_oauth_system_prompt(body: &mut Value) {
let prefix_block = json!({
"type": "text",
"text": CLAUDE_CODE_PREFIX,
});
match body.get("system") {
Some(Value::String(existing)) => {
let existing_block = json!({
"type": "text",
"text": existing,
});
body["system"] = json!([prefix_block, existing_block]);
}
Some(Value::Array(_)) => {
if let Some(arr) = body["system"].as_array_mut() {
let already_injected = arr
.iter()
.any(|block| block["text"].as_str() == Some(CLAUDE_CODE_PREFIX));
if !already_injected {
arr.insert(0, prefix_block);
}
}
}
_ => {
body["system"] = json!([prefix_block]);
}
}
}
pub async fn claude_chat_completions(
builder: RequestBuilder,
_model: &Model,
+2 -2
View File
@@ -111,7 +111,7 @@ async fn prepare_chat_completions(
let ready = oauth::prepare_oauth_access_token(client, &provider, self_.name()).await?;
if !ready {
bail!(
"OAuth configured but no tokens found for '{}'. Run: loki --authenticate {}",
"OAuth configured but no tokens found for '{}'. Run: 'loki --authenticate {}' or '.authenticate' in the REPL",
self_.name(),
self_.name()
);
@@ -181,7 +181,7 @@ async fn prepare_embeddings(
let ready = oauth::prepare_oauth_access_token(client, &provider, self_.name()).await?;
if !ready {
bail!(
"OAuth configured but no tokens found for '{}'. Run: loki --authenticate {}",
"OAuth configured but no tokens found for '{}'. Run: 'loki --authenticate {}' or '.authenticate' in the REPL",
self_.name(),
self_.name()
);
+1 -2
View File
@@ -2,7 +2,6 @@ use super::oauth::{OAuthProvider, TokenRequestFormat};
pub struct GeminiOAuthProvider;
// TODO: Replace with real credentials after registering Loki with Google Cloud Console
const GEMINI_CLIENT_ID: &str =
"50826443741-upqcebrs4gctqht1f08ku46qlbirkdsj.apps.googleusercontent.com";
const GEMINI_CLIENT_SECRET: &str = "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";
@@ -29,7 +28,7 @@ impl OAuthProvider for GeminiOAuthProvider {
}
fn scopes(&self) -> &str {
"https://www.googleapis.com/auth/cloud-platform.readonly https://www.googleapis.com/auth/userinfo.email"
"https://www.googleapis.com/auth/generative-language.peruserquota https://www.googleapis.com/auth/generative-language.retriever https://www.googleapis.com/auth/userinfo.email"
}
fn client_secret(&self) -> Option<&str> {
+4
View File
@@ -177,6 +177,10 @@ impl Model {
self.data.max_output_tokens
}
pub fn supports_function_calling(&self) -> bool {
self.data.supports_function_calling
}
pub fn no_stream(&self) -> bool {
self.data.no_stream
}
+20 -21
View File
@@ -1,12 +1,12 @@
use super::ClientConfig;
use super::access_token::{is_valid_access_token, set_access_token};
use crate::config::Config;
use anyhow::{Result, bail};
use anyhow::{Result, anyhow, bail};
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use chrono::Utc;
use inquire::Text;
use reqwest::Client as ReqwestClient;
use reqwest::{Client as ReqwestClient, RequestBuilder};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
@@ -76,7 +76,6 @@ pub async fn run_oauth_flow(provider: &dyn OAuthProvider, client_name: &str) ->
let listener = TcpListener::bind("127.0.0.1:0")?;
let port = listener.local_addr()?.port();
let uri = format!("http://127.0.0.1:{port}/callback");
// Drop the listener so run_oauth_flow can re-bind below
drop(listener);
uri
} else {
@@ -149,15 +148,15 @@ pub async fn run_oauth_flow(provider: &dyn OAuthProvider, client_name: &str) ->
let access_token = response["access_token"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing access_token in response: {response}"))?
.ok_or_else(|| anyhow!("Missing access_token in response: {response}"))?
.to_string();
let refresh_token = response["refresh_token"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing refresh_token in response: {response}"))?
.ok_or_else(|| anyhow!("Missing refresh_token in response: {response}"))?
.to_string();
let expires_in = response["expires_in"]
.as_i64()
.ok_or_else(|| anyhow::anyhow!("Missing expires_in in response: {response}"))?;
.ok_or_else(|| anyhow!("Missing expires_in in response: {response}"))?;
let expires_at = Utc::now().timestamp() + expires_in;
@@ -214,7 +213,7 @@ pub async fn refresh_oauth_token(
let access_token = response["access_token"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing access_token in refresh response: {response}"))?
.ok_or_else(|| anyhow!("Missing access_token in refresh response: {response}"))?
.to_string();
let refresh_token = response["refresh_token"]
.as_str()
@@ -222,7 +221,7 @@ pub async fn refresh_oauth_token(
.unwrap_or_else(|| tokens.refresh_token.clone());
let expires_in = response["expires_in"]
.as_i64()
.ok_or_else(|| anyhow::anyhow!("Missing expires_in in refresh response: {response}"))?;
.ok_or_else(|| anyhow!("Missing expires_in in refresh response: {response}"))?;
let expires_at = Utc::now().timestamp() + expires_in;
@@ -266,7 +265,7 @@ fn build_token_request(
client: &ReqwestClient,
provider: &(impl OAuthProvider + ?Sized),
params: &[(&str, &str)],
) -> reqwest::RequestBuilder {
) -> RequestBuilder {
let mut request = match provider.token_request_format() {
TokenRequestFormat::Json => {
let body: serde_json::Map<String, Value> = params
@@ -308,7 +307,7 @@ fn listen_for_oauth_callback(redirect_uri: &str) -> Result<(String, String)> {
let host = url.host_str().unwrap_or("127.0.0.1");
let port = url
.port()
.ok_or_else(|| anyhow::anyhow!("No port in redirect URI"))?;
.ok_or_else(|| anyhow!("No port in redirect URI"))?;
let path = url.path();
println!("Waiting for OAuth callback on {redirect_uri} ...\n");
@@ -323,19 +322,11 @@ fn listen_for_oauth_callback(redirect_uri: &str) -> Result<(String, String)> {
let request_path = request_line
.split_whitespace()
.nth(1)
.ok_or_else(|| anyhow::anyhow!("Malformed HTTP request from OAuth callback"))?;
.ok_or_else(|| anyhow!("Malformed HTTP request from OAuth callback"))?;
let full_url = format!("http://{host}:{port}{request_path}");
let parsed: Url = full_url.parse()?;
let response_body = "<html><body><h2>Authentication successful!</h2><p>You can close this tab and return to your terminal.</p></body></html>";
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
response_body.len(),
response_body
);
stream.write_all(response.as_bytes())?;
if !parsed.path().starts_with(path) {
bail!("Unexpected callback path: {}", parsed.path());
}
@@ -350,14 +341,22 @@ fn listen_for_oauth_callback(redirect_uri: &str) -> Result<(String, String)> {
.find(|(k, _)| k == "error")
.map(|(_, v)| v.to_string())
.unwrap_or_else(|| "unknown".to_string());
anyhow::anyhow!("OAuth callback returned error: {error}")
anyhow!("OAuth callback returned error: {error}")
})?;
let returned_state = parsed
.query_pairs()
.find(|(k, _)| k == "state")
.map(|(_, v)| v.to_string())
.ok_or_else(|| anyhow::anyhow!("Missing state parameter in OAuth callback"))?;
.ok_or_else(|| anyhow!("Missing state parameter in OAuth callback"))?;
let response_body = "<html><body><h2>Authentication successful!</h2><p>You can close this tab and return to your terminal.</p></body></html>";
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
response_body.len(),
response_body
);
stream.write_all(response.as_bytes())?;
Ok((code, returned_state))
}
+5
View File
@@ -476,6 +476,11 @@ impl Agent {
self.todo_list.mark_done(id)
}
pub fn clear_todo_list(&mut self) {
self.todo_list.clear();
self.reset_continuation();
}
pub fn continuation_prompt(&self) -> String {
self.config.continuation_prompt.clone().unwrap_or_else(|| {
formatdoc! {"
+10 -5
View File
@@ -239,12 +239,17 @@ impl Input {
patch_messages(&mut messages, model);
model.guard_max_input_tokens(&messages)?;
let (temperature, top_p) = (self.role().temperature(), self.role().top_p());
let functions = self.config.read().select_functions(self.role());
if let Some(vec) = &functions {
for def in vec {
debug!("Function definition: {:?}", def.name);
let functions = if model.supports_function_calling() {
let fns = self.config.read().select_functions(self.role());
if let Some(vec) = &fns {
for def in vec {
debug!("Function definition: {:?}", def.name);
}
}
}
fns
} else {
None
};
Ok(ChatCompletionsData {
messages,
temperature,
+6
View File
@@ -1842,6 +1842,12 @@ impl Config {
bail!("Already in an agent, please run '.exit agent' first to exit the current agent.");
}
let agent = Agent::init(config, agent_name, abort_signal.clone()).await?;
if !agent.model().supports_function_calling() {
eprintln!(
"Warning: The model '{}' does not support function calling. Agent tools (including todo, spawning, and user interaction) will not be available.",
agent.model().id()
);
}
let session = session_name.map(|v| v.to_string()).or_else(|| {
if config.read().macro_flag {
None
+2
View File
@@ -7,10 +7,12 @@ pub(in crate::config) const DEFAULT_TODO_INSTRUCTIONS: &str = indoc! {"
- `todo__add`: Add individual tasks. Add all planned steps before starting work.
- `todo__done`: Mark a task done by id. Call this immediately after completing each step.
- `todo__list`: Show the current todo list.
- `todo__clear`: Clear the entire todo list and reset the goal. Use when the user cancels or changes direction.
RULES:
- Always create a todo list before starting work.
- Mark each task done as soon as you finish it; do not batch.
- If the user cancels the current task or changes direction, call `todo__clear` immediately.
- If you stop with incomplete tasks, the system will automatically prompt you to continue."
};
+20
View File
@@ -67,6 +67,11 @@ impl TodoList {
self.todos.is_empty()
}
pub fn clear(&mut self) {
self.goal.clear();
self.todos.clear();
}
pub fn render_for_model(&self) -> String {
let mut lines = Vec::new();
if !self.goal.is_empty() {
@@ -149,6 +154,21 @@ mod tests {
assert!(rendered.contains("○ 2. Map"));
}
#[test]
fn test_clear() {
let mut list = TodoList::new("Some goal");
list.add("Task 1");
list.add("Task 2");
list.mark_done(1);
assert!(!list.is_empty());
list.clear();
assert!(list.is_empty());
assert!(list.goal.is_empty());
assert_eq!(list.todos.len(), 0);
assert!(!list.has_incomplete());
}
#[test]
fn test_serialization_roundtrip() {
let mut list = TodoList::new("Roundtrip");
+21
View File
@@ -76,6 +76,16 @@ pub fn todo_function_declarations() -> Vec<FunctionDeclaration> {
},
agent: false,
},
FunctionDeclaration {
name: format!("{TODO_FUNCTION_PREFIX}clear"),
description: "Clear the entire todo list and reset the goal. Use when the current task has been canceled or invalidated.".to_string(),
parameters: JsonSchema {
type_value: Some("object".to_string()),
properties: Some(IndexMap::new()),
..Default::default()
},
agent: false,
},
]
}
@@ -156,6 +166,17 @@ pub fn handle_todo_tool(config: &GlobalConfig, cmd_name: &str, args: &Value) ->
None => bail!("No active agent"),
}
}
"clear" => {
let mut cfg = config.write();
let agent = cfg.agent.as_mut();
match agent {
Some(agent) => {
agent.clear_todo_list();
Ok(json!({"status": "ok", "message": "Todo list cleared"}))
}
None => bail!("No active agent"),
}
}
_ => bail!("Unknown todo action: {action}"),
}
}
+9 -3
View File
@@ -12,6 +12,7 @@ use tokio::sync::oneshot;
pub const USER_FUNCTION_PREFIX: &str = "user__";
const DEFAULT_ESCALATION_TIMEOUT_SECS: u64 = 300;
const CUSTOM_MULTI_CHOICE_ANSWER_OPTION: &str = "Other (custom)";
pub fn user_interaction_function_declarations() -> Vec<FunctionDeclaration> {
vec![
@@ -151,9 +152,14 @@ fn handle_direct_ask(args: &Value) -> Result<Value> {
.get("question")
.and_then(Value::as_str)
.ok_or_else(|| anyhow!("'question' is required"))?;
let options = parse_options(args)?;
let mut options = parse_options(args)?;
options.push(CUSTOM_MULTI_CHOICE_ANSWER_OPTION.to_string());
let answer = Select::new(question, options).prompt()?;
let mut answer = Select::new(question, options).prompt()?;
if answer == CUSTOM_MULTI_CHOICE_ANSWER_OPTION {
answer = Text::new("Custom response:").prompt()?
}
Ok(json!({ "answer": answer }))
}
@@ -175,7 +181,7 @@ fn handle_direct_input(args: &Value) -> Result<Value> {
.and_then(Value::as_str)
.ok_or_else(|| anyhow!("'question' is required"))?;
let answer = Text::new(question).prompt()?;
let answer = Text::new(&format!("{question}\nYour answer: ")).prompt()?;
Ok(json!({ "answer": answer }))
}
+10 -2
View File
@@ -23,7 +23,7 @@ use crate::config::{
TEMP_SESSION_NAME, WorkingMode, ensure_parent_exists, list_agents, load_env_file,
macro_execute,
};
use crate::render::render_error;
use crate::render::{prompt_theme, render_error};
use crate::repl::Repl;
use crate::utils::*;
@@ -33,7 +33,7 @@ use anyhow::{Result, anyhow, bail};
use clap::{CommandFactory, Parser};
use clap_complete::CompleteEnv;
use client::ClientConfig;
use inquire::{Select, Text};
use inquire::{Select, Text, set_global_render_config};
use log::LevelFilter;
use log4rs::append::console::ConsoleAppender;
use log4rs::append::file::FileAppender;
@@ -106,6 +106,14 @@ async fn main() -> Result<()> {
)
.await?,
));
{
let cfg = config.read();
if cfg.highlight {
set_global_render_config(prompt_theme(cfg.render_options()?)?)
}
}
if let Err(err) = run(config, cli, text, abort_signal).await {
render_error(err);
process::exit(1);
+53
View File
@@ -0,0 +1,53 @@
use crate::render::RenderOptions;
use anyhow::Result;
use inquire::ui::{Attributes, Color, RenderConfig, StyleSheet};
use syntect::highlighting::{Highlighter, Theme};
use syntect::parsing::Scope;
const DEFAULT_INQUIRE_PROMPT_THEME: Color = Color::DarkYellow;
pub fn prompt_theme<'a>(render_options: RenderOptions) -> Result<RenderConfig<'a>> {
let theme = render_options.theme.as_ref();
let mut render_config = RenderConfig::default();
if let Some(theme_ref) = theme {
let prompt_color = resolve_foreground(theme_ref, "markup.heading")?
.unwrap_or(DEFAULT_INQUIRE_PROMPT_THEME);
render_config.prompt = StyleSheet::new()
.with_fg(prompt_color)
.with_attr(Attributes::BOLD);
render_config.selected_option = Some(
render_config
.selected_option
.unwrap_or(render_config.option)
.with_attr(
render_config
.selected_option
.unwrap_or(render_config.option)
.att
| Attributes::BOLD,
),
);
render_config.selected_checkbox = render_config
.selected_checkbox
.with_attr(render_config.selected_checkbox.style.att | Attributes::BOLD);
render_config.option = render_config
.option
.with_attr(render_config.option.att | Attributes::BOLD);
}
Ok(render_config)
}
fn resolve_foreground(theme: &Theme, scope_str: &str) -> Result<Option<Color>> {
let scope = Scope::new(scope_str)?;
let style_mod = Highlighter::new(theme).style_mod_for_stack(&[scope]);
let fg = style_mod.foreground.or(theme.settings.foreground);
Ok(fg.map(|c| Color::Rgb {
r: c.r,
g: c.g,
b: c.b,
}))
}
+3
View File
@@ -1,6 +1,9 @@
mod inquire;
mod markdown;
mod stream;
pub use inquire::prompt_theme;
pub use self::markdown::{MarkdownRender, RenderOptions};
use self::stream::{markdown_stream, raw_stream};
+27 -2
View File
@@ -33,7 +33,7 @@ use std::{env, mem, process};
const MENU_NAME: &str = "completion_menu";
static REPL_COMMANDS: LazyLock<[ReplCommand; 38]> = LazyLock::new(|| {
static REPL_COMMANDS: LazyLock<[ReplCommand; 39]> = LazyLock::new(|| {
[
ReplCommand::new(".help", "Show this help guide", AssertState::pass()),
ReplCommand::new(".info", "Show system info", AssertState::pass()),
@@ -137,6 +137,11 @@ static REPL_COMMANDS: LazyLock<[ReplCommand; 38]> = LazyLock::new(|| {
"Leave agent",
AssertState::True(StateFlags::AGENT),
),
ReplCommand::new(
".clear todo",
"Clear the todo list and stop auto-continuation",
AssertState::True(StateFlags::AGENT),
),
ReplCommand::new(
".rag",
"Initialize or access RAG",
@@ -428,7 +433,8 @@ pub async fn run_repl_command(
None => println!("Usage: .model <name>"),
},
".authenticate" => {
let client = init_client(config, None)?;
let current_model = config.read().current_model().clone();
let client = init_client(config, Some(current_model))?;
if !client.supports_oauth() {
bail!(
"Client '{}' doesn't either support OAuth or isn't configured to use it (i.e. uses an API key instead)",
@@ -803,6 +809,25 @@ pub async fn run_repl_command(
Some("messages") => {
bail!("Use '.empty session' instead");
}
Some("todo") => {
let mut cfg = config.write();
match cfg.agent.as_mut() {
Some(agent) => {
if !agent.auto_continue_enabled() {
bail!(
"The todo system is not enabled for this agent. Set 'auto_continue: true' in the agent's config.yaml to enable it."
);
}
if agent.todo_list().is_empty() {
println!("Todo list is already empty.");
} else {
agent.clear_todo_list();
println!("Todo list cleared.");
}
}
None => bail!("No active agent"),
}
}
_ => unknown_command()?,
},
".vault" => match split_first_arg(args) {