Compare commits
23 Commits
88a9a7709f
...
f91cf2e346
| Author | SHA1 | Date | |
|---|---|---|---|
|
f91cf2e346
|
|||
|
b6b33ab7e3
|
|||
|
c1902a69d1
|
|||
|
812a8e101c
|
|||
|
655ee2a599
|
|||
|
128a8f9a9c
|
|||
|
b1be9443e7
|
|||
|
7b12c69ebf
|
|||
|
69ad584137
|
|||
|
313058e70a
|
|||
|
ea96d9ba3d
|
|||
|
7884adc7c1
|
|||
|
948466d771
|
|||
|
3894c98b5b
|
|||
|
5e9c31595e
|
|||
|
39d9b25e47
|
|||
|
b86f76ddb9
|
|||
|
7f267a10a1
|
|||
|
cdafdff281
|
|||
|
60ad83d6d9
|
|||
|
44c03ccf4f
|
|||
|
af933bbb29
|
|||
|
1f127ee990
|
Generated
+120
-16
@@ -1459,6 +1459,16 @@ dependencies = [
|
||||
"darling_macro 0.21.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
|
||||
dependencies = [
|
||||
"darling_core 0.23.0",
|
||||
"darling_macro 0.23.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.11"
|
||||
@@ -1487,6 +1497,19 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
|
||||
dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.11"
|
||||
@@ -1509,6 +1532,17 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
||||
dependencies = [
|
||||
"darling_core 0.23.0",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.6"
|
||||
@@ -3008,9 +3042,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.182"
|
||||
version = "0.2.180"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
@@ -3512,6 +3546,18 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225e7cfe711e0ba79a68baeddb2982723e4235247aefce1482f2f16c27865b66"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
@@ -3969,6 +4015,12 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pastey"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec"
|
||||
|
||||
[[package]]
|
||||
name = "path-absolutize"
|
||||
version = "3.1.1"
|
||||
@@ -4226,16 +4278,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "process-wrap"
|
||||
version = "8.2.1"
|
||||
version = "9.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3ef4f2f0422f23a82ec9f628ea2acd12871c81a9362b02c43c1aa86acfc3ba1"
|
||||
checksum = "ccd9713fe2c91c3c85ac388b31b89de339365d2c995146e630b5e0da9d06526a"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"indexmap 2.12.1",
|
||||
"nix 0.30.1",
|
||||
"nix 0.31.1",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"windows 0.61.3",
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4610,14 +4662,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rmcp"
|
||||
version = "0.6.4"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41ab0892f4938752b34ae47cb53910b1b0921e55e77ddb6e44df666cab17939f"
|
||||
checksum = "cc4c9c94680f75470ee8083a0667988b5d7b5beb70b9f998a8e51de7c682ce60"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64",
|
||||
"chrono",
|
||||
"futures",
|
||||
"paste",
|
||||
"pastey",
|
||||
"pin-project-lite",
|
||||
"process-wrap",
|
||||
"rmcp-macros",
|
||||
@@ -4633,11 +4686,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rmcp-macros"
|
||||
version = "0.6.4"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1827cd98dab34cade0513243c6fe0351f0f0b2c9d6825460bcf45b42804bdda0"
|
||||
checksum = "90c23c8f26cae4da838fbc3eadfaecf2d549d97c04b558e7bd90526a9c28b42a"
|
||||
dependencies = [
|
||||
"darling 0.21.3",
|
||||
"darling 0.23.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_json",
|
||||
@@ -6700,11 +6753,23 @@ version = "0.61.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-collections 0.2.0",
|
||||
"windows-core 0.61.2",
|
||||
"windows-future",
|
||||
"windows-future 0.2.1",
|
||||
"windows-link 0.1.3",
|
||||
"windows-numerics",
|
||||
"windows-numerics 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections 0.3.2",
|
||||
"windows-core 0.62.2",
|
||||
"windows-future 0.3.2",
|
||||
"windows-numerics 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6716,6 +6781,15 @@ dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.2"
|
||||
@@ -6750,7 +6824,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
"windows-link 0.1.3",
|
||||
"windows-threading",
|
||||
"windows-threading 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core 0.62.2",
|
||||
"windows-link 0.2.1",
|
||||
"windows-threading 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6797,6 +6882,16 @@ dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core 0.62.2",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
@@ -6935,6 +7030,15 @@ dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ duct = "1.0.0"
|
||||
argc = "1.23.0"
|
||||
strum_macros = "0.27.2"
|
||||
indoc = "2.0.6"
|
||||
rmcp = { version = "0.6.1", features = ["client", "transport-child-process"] }
|
||||
rmcp = { version = "0.16.0", features = ["client", "transport-child-process"] }
|
||||
num_cpus = "1.17.0"
|
||||
rustpython-parser = "0.4.0"
|
||||
rustpython-ast = "0.4.0"
|
||||
|
||||
@@ -35,7 +35,7 @@ Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration g
|
||||
* [RAG](./docs/RAG.md): Retrieval-Augmented Generation for enhanced information retrieval and generation.
|
||||
* [Sessions](/docs/SESSIONS.md): Manage and persist conversational contexts and settings across multiple interactions.
|
||||
* [Roles](./docs/ROLES.md): Customize model behavior for specific tasks or domains.
|
||||
* [Agents](/docs/AGENTS.md): Leverage AI agents to perform complex tasks and workflows.
|
||||
* [Agents](/docs/AGENTS.md): Leverage AI agents to perform complex tasks and workflows, including sub-agent spawning, teammate messaging, and user interaction tools.
|
||||
* [Todo System](./docs/TODO-SYSTEM.md): Built-in task tracking for improved agent reliability with smaller models.
|
||||
* [Environment Variables](./docs/ENVIRONMENT-VARIABLES.md): Override and customize your Loki configuration at runtime with environment variables.
|
||||
* [Client Configurations](./docs/clients/CLIENTS.md): Configuration instructions for various LLM providers.
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
name: code-reviewer
|
||||
description: CodeRabbit-style code reviewer - spawns per-file reviewers, synthesizes findings
|
||||
version: 1.0.0
|
||||
temperature: 0.1
|
||||
top_p: 0.95
|
||||
|
||||
auto_continue: true
|
||||
max_auto_continues: 20
|
||||
inject_todo_instructions: true
|
||||
|
||||
can_spawn_agents: true
|
||||
max_concurrent_agents: 10
|
||||
max_agent_depth: 2
|
||||
|
||||
variables:
|
||||
- name: project_dir
|
||||
description: Project directory to review
|
||||
default: '.'
|
||||
|
||||
global_tools:
|
||||
- fs_read.sh
|
||||
- fs_grep.sh
|
||||
- fs_glob.sh
|
||||
- execute_command.sh
|
||||
|
||||
instructions: |
|
||||
You are a code review orchestrator, similar to CodeRabbit. You coordinate per-file reviews and produce a unified report.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Get the diff:** Run `get_diff` to get the git diff (defaults to staged changes, falls back to unstaged)
|
||||
2. **Parse changed files:** Extract the list of files from the diff
|
||||
3. **Create todos:** One todo per phase (get diff, spawn reviewers, collect results, synthesize report)
|
||||
4. **Spawn file-reviewers:** One `file-reviewer` agent per changed file, in parallel
|
||||
5. **Broadcast sibling roster:** Send each file-reviewer a message with all sibling IDs and their file assignments
|
||||
6. **Collect all results:** Wait for each file-reviewer to complete
|
||||
7. **Synthesize:** Combine all findings into a CodeRabbit-style report
|
||||
|
||||
## Spawning File Reviewers
|
||||
|
||||
For each changed file, spawn a file-reviewer with a prompt containing:
|
||||
- The file path
|
||||
- The relevant diff hunk(s) for that file
|
||||
- Instructions to review it
|
||||
|
||||
```
|
||||
agent__spawn --agent file-reviewer --prompt "Review the following diff for <file_path>:
|
||||
|
||||
<diff content for this file>
|
||||
|
||||
Focus on bugs, security issues, logic errors, and style. Use the severity format (🔴🟡🟢💡).
|
||||
End with REVIEW_COMPLETE."
|
||||
```
|
||||
|
||||
## Sibling Roster Broadcast
|
||||
|
||||
After spawning ALL file-reviewers (collecting their IDs), send each one a message with the roster:
|
||||
|
||||
```
|
||||
agent__send_message --to <agent_id> --message "SIBLING_ROSTER:
|
||||
- <agent_id_1>: reviewing <file_1>
|
||||
- <agent_id_2>: reviewing <file_2>
|
||||
...
|
||||
Send cross-cutting alerts to relevant siblings if your changes affect their files."
|
||||
```
|
||||
|
||||
## Diff Parsing
|
||||
|
||||
Split the diff by file. Each file's diff starts with `diff --git a/<path> b/<path>`. Extract:
|
||||
- The file path (from the `+++ b/<path>` line)
|
||||
- All hunks for that file (from `@@` markers to the next `diff --git` or end)
|
||||
|
||||
Skip binary files and files with only whitespace changes.
|
||||
|
||||
## Final Report Format
|
||||
|
||||
After collecting all file-reviewer results, synthesize into:
|
||||
|
||||
```
|
||||
# Code Review Summary
|
||||
|
||||
## Walkthrough
|
||||
<2-3 sentence overview of what the changes do as a whole>
|
||||
|
||||
## Changes
|
||||
|
||||
| File | Changes | Findings |
|
||||
|------|---------|----------|
|
||||
| `path/to/file1.rs` | <brief description> | 🔴 1 🟡 2 🟢 1 |
|
||||
| `path/to/file2.rs` | <brief description> | 🟢 2 💡 1 |
|
||||
|
||||
## Detailed Findings
|
||||
|
||||
### `path/to/file1.rs`
|
||||
<paste file-reviewer's findings here, cleaned up>
|
||||
|
||||
### `path/to/file2.rs`
|
||||
<paste file-reviewer's findings here, cleaned up>
|
||||
|
||||
## Cross-File Concerns
|
||||
<any cross-cutting issues identified by the teammate pattern>
|
||||
|
||||
---
|
||||
*Reviewed N files, found X critical, Y warnings, Z suggestions, W nitpicks*
|
||||
```
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- **Single file changed:** Still spawn one file-reviewer (for consistency), skip roster broadcast
|
||||
- **Too many files (>10):** Group small files (< 20 lines changed) and review them together
|
||||
- **No changes found:** Report "No changes to review" and exit
|
||||
- **Binary files:** Skip with a note in the summary
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Always use `get_diff` first:** Don't assume what changed
|
||||
2. **Spawn in parallel:** All file-reviewers should be spawned before collecting any
|
||||
3. **Don't review code yourself:** Delegate ALL review work to file-reviewers
|
||||
4. **Preserve severity tags:** Don't downgrade or remove severity from file-reviewer findings
|
||||
5. **Include ALL findings:** Don't summarize away specific issues
|
||||
|
||||
## Context
|
||||
- Project: {{project_dir}}
|
||||
- CWD: {{__cwd__}}
|
||||
- Shell: {{__shell__}}
|
||||
Executable
+478
@@ -0,0 +1,478 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
source "$LLM_PROMPT_UTILS_FILE"
|
||||
source "$LLM_ROOT_DIR/agents/.shared/utils.sh"
|
||||
|
||||
# @env LLM_OUTPUT=/dev/stdout
|
||||
# @env LLM_AGENT_VAR_PROJECT_DIR=.
|
||||
# @describe Code review orchestrator tools
|
||||
|
||||
_project_dir() {
|
||||
local dir="${LLM_AGENT_VAR_PROJECT_DIR:-.}"
|
||||
(cd "${dir}" 2>/dev/null && pwd) || echo "${dir}"
|
||||
}
|
||||
|
||||
# @cmd Get git diff for code review. Returns staged changes, or unstaged if nothing is staged, or HEAD~1 diff if working tree is clean.
|
||||
# @option --base Optional base ref to diff against (e.g., "main", "HEAD~3", a commit SHA)
|
||||
get_diff() {
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
# shellcheck disable=SC2154
|
||||
local base="${argc_base:-}"
|
||||
|
||||
local diff_output=""
|
||||
|
||||
if [[ -n "${base}" ]]; then
|
||||
diff_output=$(cd "${project_dir}" && git diff "${base}" 2>&1) || true
|
||||
else
|
||||
diff_output=$(cd "${project_dir}" && git diff --cached 2>&1) || true
|
||||
|
||||
if [[ -z "${diff_output}" ]]; then
|
||||
diff_output=$(cd "${project_dir}" && git diff 2>&1) || true
|
||||
fi
|
||||
|
||||
if [[ -z "${diff_output}" ]]; then
|
||||
diff_output=$(cd "${project_dir}" && git diff HEAD~1 2>&1) || true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${diff_output}" ]]; then
|
||||
warn "No changes found to review" >> "$LLM_OUTPUT"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local file_count
|
||||
file_count=$(echo "${diff_output}" | grep -c '^diff --git' || true)
|
||||
|
||||
{
|
||||
info "Diff contains changes to ${file_count} file(s)"
|
||||
echo ""
|
||||
echo "${diff_output}"
|
||||
} >> "$LLM_OUTPUT"
|
||||
}
|
||||
|
||||
# @cmd Get list of changed files with stats
|
||||
# @option --base Optional base ref to diff against
|
||||
get_changed_files() {
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
local base="${argc_base:-}"
|
||||
|
||||
local stat_output=""
|
||||
|
||||
if [[ -n "${base}" ]]; then
|
||||
stat_output=$(cd "${project_dir}" && git diff --stat "${base}" 2>&1) || true
|
||||
else
|
||||
stat_output=$(cd "${project_dir}" && git diff --cached --stat 2>&1) || true
|
||||
|
||||
if [[ -z "${stat_output}" ]]; then
|
||||
stat_output=$(cd "${project_dir}" && git diff --stat 2>&1) || true
|
||||
fi
|
||||
|
||||
if [[ -z "${stat_output}" ]]; then
|
||||
stat_output=$(cd "${project_dir}" && git diff --stat HEAD~1 2>&1) || true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${stat_output}" ]]; then
|
||||
warn "No changes found" >> "$LLM_OUTPUT"
|
||||
return 0
|
||||
fi
|
||||
|
||||
{
|
||||
info "Changed files:"
|
||||
echo ""
|
||||
echo "${stat_output}"
|
||||
} >> "$LLM_OUTPUT"
|
||||
}
|
||||
|
||||
# @cmd Get project structure and type information
|
||||
get_project_info() {
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
|
||||
local project_info
|
||||
project_info=$(detect_project "${project_dir}")
|
||||
|
||||
{
|
||||
info "Project: ${project_dir}"
|
||||
echo "Type: $(echo "${project_info}" | jq -r '.type')"
|
||||
echo ""
|
||||
get_tree "${project_dir}" 2
|
||||
} >> "$LLM_OUTPUT"
|
||||
}
|
||||
|
||||
# ARGC-BUILD {
|
||||
# This block was generated by argc (https://github.com/sigoden/argc).
|
||||
# Modifying it manually is not recommended
|
||||
|
||||
_argc_run() {
|
||||
if [[ "${1:-}" == "___internal___" ]]; then
|
||||
_argc_die "error: unsupported ___internal___ command"
|
||||
fi
|
||||
if [[ "${OS:-}" == "Windows_NT" ]] && [[ -n "${MSYSTEM:-}" ]]; then
|
||||
set -o igncr
|
||||
fi
|
||||
argc__args=("$(basename "$0" .sh)" "$@")
|
||||
argc__positionals=()
|
||||
_argc_index=1
|
||||
_argc_len="${#argc__args[@]}"
|
||||
_argc_tools=()
|
||||
_argc_parse
|
||||
if [ -n "${argc__fn:-}" ]; then
|
||||
$argc__fn "${argc__positionals[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
_argc_usage() {
|
||||
cat <<-'EOF'
|
||||
Code review orchestrator tools
|
||||
|
||||
USAGE: <COMMAND>
|
||||
|
||||
COMMANDS:
|
||||
get_diff Get git diff for code review. Returns staged changes, or unstaged if nothing is staged, or HEAD~1 diff if working tree is clean. [aliases: get-diff]
|
||||
get_changed_files Get list of changed files with stats [aliases: get-changed-files]
|
||||
get_project_info Get project structure and type information [aliases: get-project-info]
|
||||
|
||||
ENVIRONMENTS:
|
||||
LLM_OUTPUT [default: /dev/stdout]
|
||||
LLM_AGENT_VAR_PROJECT_DIR [default: .]
|
||||
EOF
|
||||
exit
|
||||
}
|
||||
|
||||
_argc_version() {
|
||||
echo 0.0.0
|
||||
exit
|
||||
}
|
||||
|
||||
_argc_parse() {
|
||||
local _argc_key _argc_action
|
||||
local _argc_subcmds="get_diff, get-diff, get_changed_files, get-changed-files, get_project_info, get-project-info"
|
||||
while [[ $_argc_index -lt $_argc_len ]]; do
|
||||
_argc_item="${argc__args[_argc_index]}"
|
||||
_argc_key="${_argc_item%%=*}"
|
||||
case "$_argc_key" in
|
||||
--help | -help | -h)
|
||||
_argc_usage
|
||||
;;
|
||||
--version | -version | -V)
|
||||
_argc_version
|
||||
;;
|
||||
--)
|
||||
_argc_dash="${#argc__positionals[@]}"
|
||||
argc__positionals+=("${argc__args[@]:$((_argc_index + 1))}")
|
||||
_argc_index=$_argc_len
|
||||
break
|
||||
;;
|
||||
get_diff | get-diff)
|
||||
_argc_index=$((_argc_index + 1))
|
||||
_argc_action=_argc_parse_get_diff
|
||||
break
|
||||
;;
|
||||
get_changed_files | get-changed-files)
|
||||
_argc_index=$((_argc_index + 1))
|
||||
_argc_action=_argc_parse_get_changed_files
|
||||
break
|
||||
;;
|
||||
get_project_info | get-project-info)
|
||||
_argc_index=$((_argc_index + 1))
|
||||
_argc_action=_argc_parse_get_project_info
|
||||
break
|
||||
;;
|
||||
help)
|
||||
local help_arg="${argc__args[$((_argc_index + 1))]:-}"
|
||||
case "$help_arg" in
|
||||
get_diff | get-diff)
|
||||
_argc_usage_get_diff
|
||||
;;
|
||||
get_changed_files | get-changed-files)
|
||||
_argc_usage_get_changed_files
|
||||
;;
|
||||
get_project_info | get-project-info)
|
||||
_argc_usage_get_project_info
|
||||
;;
|
||||
"")
|
||||
_argc_usage
|
||||
;;
|
||||
*)
|
||||
_argc_die "error: invalid value \`$help_arg\` for \`<command>\`"$'\n'" [possible values: $_argc_subcmds]"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
_argc_die "error: \`\` requires a subcommand but one was not provided"$'\n'" [subcommands: $_argc_subcmds]"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if [[ -n "${_argc_action:-}" ]]; then
|
||||
$_argc_action
|
||||
else
|
||||
_argc_usage
|
||||
fi
|
||||
}
|
||||
|
||||
_argc_usage_get_diff() {
|
||||
cat <<-'EOF'
|
||||
Get git diff for code review. Returns staged changes, or unstaged if nothing is staged, or HEAD~1 diff if working tree is clean.
|
||||
|
||||
USAGE: get_diff [OPTIONS]
|
||||
|
||||
OPTIONS:
|
||||
--base <BASE> Optional base ref to diff against (e.g., "main", "HEAD~3", a commit SHA)
|
||||
-h, --help Print help
|
||||
|
||||
ENVIRONMENTS:
|
||||
LLM_OUTPUT [default: /dev/stdout]
|
||||
LLM_AGENT_VAR_PROJECT_DIR [default: .]
|
||||
EOF
|
||||
exit
|
||||
}
|
||||
|
||||
_argc_parse_get_diff() {
|
||||
local _argc_key _argc_action
|
||||
local _argc_subcmds=""
|
||||
while [[ $_argc_index -lt $_argc_len ]]; do
|
||||
_argc_item="${argc__args[_argc_index]}"
|
||||
_argc_key="${_argc_item%%=*}"
|
||||
case "$_argc_key" in
|
||||
--help | -help | -h)
|
||||
_argc_usage_get_diff
|
||||
;;
|
||||
--)
|
||||
_argc_dash="${#argc__positionals[@]}"
|
||||
argc__positionals+=("${argc__args[@]:$((_argc_index + 1))}")
|
||||
_argc_index=$_argc_len
|
||||
break
|
||||
;;
|
||||
--base)
|
||||
_argc_take_args "--base <BASE>" 1 1 "-" ""
|
||||
_argc_index=$((_argc_index + _argc_take_args_len + 1))
|
||||
if [[ -z "${argc_base:-}" ]]; then
|
||||
argc_base="${_argc_take_args_values[0]:-}"
|
||||
else
|
||||
_argc_die "error: the argument \`--base\` cannot be used multiple times"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
if _argc_maybe_flag_option "-" "$_argc_item"; then
|
||||
_argc_die "error: unexpected argument \`$_argc_key\` found"
|
||||
fi
|
||||
argc__positionals+=("$_argc_item")
|
||||
_argc_index=$((_argc_index + 1))
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if [[ -n "${_argc_action:-}" ]]; then
|
||||
$_argc_action
|
||||
else
|
||||
argc__fn=get_diff
|
||||
if [[ "${argc__positionals[0]:-}" == "help" ]] && [[ "${#argc__positionals[@]}" -eq 1 ]]; then
|
||||
_argc_usage_get_diff
|
||||
fi
|
||||
if [[ -z "${LLM_OUTPUT:-}" ]]; then
|
||||
export LLM_OUTPUT=/dev/stdout
|
||||
fi
|
||||
if [[ -z "${LLM_AGENT_VAR_PROJECT_DIR:-}" ]]; then
|
||||
export LLM_AGENT_VAR_PROJECT_DIR=.
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
_argc_usage_get_changed_files() {
|
||||
cat <<-'EOF'
|
||||
Get list of changed files with stats
|
||||
|
||||
USAGE: get_changed_files [OPTIONS]
|
||||
|
||||
OPTIONS:
|
||||
--base <BASE> Optional base ref to diff against
|
||||
-h, --help Print help
|
||||
|
||||
ENVIRONMENTS:
|
||||
LLM_OUTPUT [default: /dev/stdout]
|
||||
LLM_AGENT_VAR_PROJECT_DIR [default: .]
|
||||
EOF
|
||||
exit
|
||||
}
|
||||
|
||||
_argc_parse_get_changed_files() {
|
||||
local _argc_key _argc_action
|
||||
local _argc_subcmds=""
|
||||
while [[ $_argc_index -lt $_argc_len ]]; do
|
||||
_argc_item="${argc__args[_argc_index]}"
|
||||
_argc_key="${_argc_item%%=*}"
|
||||
case "$_argc_key" in
|
||||
--help | -help | -h)
|
||||
_argc_usage_get_changed_files
|
||||
;;
|
||||
--)
|
||||
_argc_dash="${#argc__positionals[@]}"
|
||||
argc__positionals+=("${argc__args[@]:$((_argc_index + 1))}")
|
||||
_argc_index=$_argc_len
|
||||
break
|
||||
;;
|
||||
--base)
|
||||
_argc_take_args "--base <BASE>" 1 1 "-" ""
|
||||
_argc_index=$((_argc_index + _argc_take_args_len + 1))
|
||||
if [[ -z "${argc_base:-}" ]]; then
|
||||
argc_base="${_argc_take_args_values[0]:-}"
|
||||
else
|
||||
_argc_die "error: the argument \`--base\` cannot be used multiple times"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
if _argc_maybe_flag_option "-" "$_argc_item"; then
|
||||
_argc_die "error: unexpected argument \`$_argc_key\` found"
|
||||
fi
|
||||
argc__positionals+=("$_argc_item")
|
||||
_argc_index=$((_argc_index + 1))
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if [[ -n "${_argc_action:-}" ]]; then
|
||||
$_argc_action
|
||||
else
|
||||
argc__fn=get_changed_files
|
||||
if [[ "${argc__positionals[0]:-}" == "help" ]] && [[ "${#argc__positionals[@]}" -eq 1 ]]; then
|
||||
_argc_usage_get_changed_files
|
||||
fi
|
||||
if [[ -z "${LLM_OUTPUT:-}" ]]; then
|
||||
export LLM_OUTPUT=/dev/stdout
|
||||
fi
|
||||
if [[ -z "${LLM_AGENT_VAR_PROJECT_DIR:-}" ]]; then
|
||||
export LLM_AGENT_VAR_PROJECT_DIR=.
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
_argc_usage_get_project_info() {
|
||||
cat <<-'EOF'
|
||||
Get project structure and type information
|
||||
|
||||
USAGE: get_project_info
|
||||
|
||||
ENVIRONMENTS:
|
||||
LLM_OUTPUT [default: /dev/stdout]
|
||||
LLM_AGENT_VAR_PROJECT_DIR [default: .]
|
||||
EOF
|
||||
exit
|
||||
}
|
||||
|
||||
_argc_parse_get_project_info() {
|
||||
local _argc_key _argc_action
|
||||
local _argc_subcmds=""
|
||||
while [[ $_argc_index -lt $_argc_len ]]; do
|
||||
_argc_item="${argc__args[_argc_index]}"
|
||||
_argc_key="${_argc_item%%=*}"
|
||||
case "$_argc_key" in
|
||||
--help | -help | -h)
|
||||
_argc_usage_get_project_info
|
||||
;;
|
||||
--)
|
||||
_argc_dash="${#argc__positionals[@]}"
|
||||
argc__positionals+=("${argc__args[@]:$((_argc_index + 1))}")
|
||||
_argc_index=$_argc_len
|
||||
break
|
||||
;;
|
||||
*)
|
||||
argc__positionals+=("$_argc_item")
|
||||
_argc_index=$((_argc_index + 1))
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if [[ -n "${_argc_action:-}" ]]; then
|
||||
$_argc_action
|
||||
else
|
||||
argc__fn=get_project_info
|
||||
if [[ "${argc__positionals[0]:-}" == "help" ]] && [[ "${#argc__positionals[@]}" -eq 1 ]]; then
|
||||
_argc_usage_get_project_info
|
||||
fi
|
||||
if [[ -z "${LLM_OUTPUT:-}" ]]; then
|
||||
export LLM_OUTPUT=/dev/stdout
|
||||
fi
|
||||
if [[ -z "${LLM_AGENT_VAR_PROJECT_DIR:-}" ]]; then
|
||||
export LLM_AGENT_VAR_PROJECT_DIR=.
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
_argc_take_args() {
|
||||
_argc_take_args_values=()
|
||||
_argc_take_args_len=0
|
||||
local param="$1" min="$2" max="$3" signs="$4" delimiter="$5"
|
||||
if [[ "$min" -eq 0 ]] && [[ "$max" -eq 0 ]]; then
|
||||
return
|
||||
fi
|
||||
local _argc_take_index=$((_argc_index + 1)) _argc_take_value
|
||||
if [[ "$_argc_item" == *=* ]]; then
|
||||
_argc_take_args_values=("${_argc_item##*=}")
|
||||
else
|
||||
while [[ $_argc_take_index -lt $_argc_len ]]; do
|
||||
_argc_take_value="${argc__args[_argc_take_index]}"
|
||||
if _argc_maybe_flag_option "$signs" "$_argc_take_value"; then
|
||||
if [[ "${#_argc_take_value}" -gt 1 ]]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
_argc_take_args_values+=("$_argc_take_value")
|
||||
_argc_take_args_len=$((_argc_take_args_len + 1))
|
||||
if [[ "$_argc_take_args_len" -ge "$max" ]]; then
|
||||
break
|
||||
fi
|
||||
_argc_take_index=$((_argc_take_index + 1))
|
||||
done
|
||||
fi
|
||||
if [[ "${#_argc_take_args_values[@]}" -lt "$min" ]]; then
|
||||
_argc_die "error: incorrect number of values for \`$param\`"
|
||||
fi
|
||||
if [[ -n "$delimiter" ]] && [[ "${#_argc_take_args_values[@]}" -gt 0 ]]; then
|
||||
local item values arr=()
|
||||
for item in "${_argc_take_args_values[@]}"; do
|
||||
IFS="$delimiter" read -r -a values <<<"$item"
|
||||
arr+=("${values[@]}")
|
||||
done
|
||||
_argc_take_args_values=("${arr[@]}")
|
||||
fi
|
||||
}
|
||||
|
||||
_argc_maybe_flag_option() {
|
||||
local signs="$1" arg="$2"
|
||||
if [[ -z "$signs" ]]; then
|
||||
return 1
|
||||
fi
|
||||
local cond=false
|
||||
if [[ "$signs" == *"+"* ]]; then
|
||||
if [[ "$arg" =~ ^\+[^+].* ]]; then
|
||||
cond=true
|
||||
fi
|
||||
elif [[ "$arg" == -* ]]; then
|
||||
if (( ${#arg} < 3 )) || [[ ! "$arg" =~ ^---.* ]]; then
|
||||
cond=true
|
||||
fi
|
||||
fi
|
||||
if [[ "$cond" == "false" ]]; then
|
||||
return 1
|
||||
fi
|
||||
local value="${arg%%=*}"
|
||||
if [[ "$value" =~ [[:space:]] ]]; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
_argc_die() {
|
||||
if [[ $# -eq 0 ]]; then
|
||||
cat
|
||||
else
|
||||
echo "$*" >&2
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
_argc_run "$@"
|
||||
|
||||
# ARGC-BUILD }
|
||||
@@ -0,0 +1,111 @@
|
||||
name: file-reviewer
|
||||
description: Reviews a single file's diff for bugs, style issues, and cross-cutting concerns
|
||||
version: 1.0.0
|
||||
temperature: 0.1
|
||||
top_p: 0.95
|
||||
|
||||
variables:
|
||||
- name: project_dir
|
||||
description: Project directory for context
|
||||
default: '.'
|
||||
|
||||
global_tools:
|
||||
- fs_read.sh
|
||||
- fs_grep.sh
|
||||
- fs_glob.sh
|
||||
|
||||
instructions: |
|
||||
You are a precise code reviewer. You review ONE file's diff and produce structured findings.
|
||||
|
||||
## Your Mission
|
||||
|
||||
You receive a git diff for a single file. Your job:
|
||||
1. Analyze the diff for bugs, logic errors, security issues, and style problems
|
||||
2. Read surrounding code for context (use `fs_read` with targeted offsets)
|
||||
3. Check your inbox for cross-cutting alerts from sibling reviewers
|
||||
4. Send alerts to siblings if you spot cross-file issues
|
||||
5. Return structured findings
|
||||
|
||||
## Input
|
||||
|
||||
You receive:
|
||||
- The file path being reviewed
|
||||
- The git diff for that file
|
||||
- A sibling roster (other file-reviewers and which files they're reviewing)
|
||||
|
||||
## Cross-Cutting Alerts (Teammate Pattern)
|
||||
|
||||
After analyzing your file, check if changes might affect sibling files:
|
||||
- **Interface changes**: If a function signature changed, alert siblings reviewing callers
|
||||
- **Type changes**: If a type/struct changed, alert siblings reviewing files that use it
|
||||
- **Import changes**: If exports changed, alert siblings reviewing importers
|
||||
- **Config changes**: Alert all siblings if config format changed
|
||||
|
||||
To alert a sibling:
|
||||
```
|
||||
agent__send_message --to <sibling_agent_id> --message "ALERT: <description of cross-file concern>"
|
||||
```
|
||||
|
||||
Check your inbox periodically for alerts from siblings:
|
||||
```
|
||||
agent__check_inbox
|
||||
```
|
||||
|
||||
If you receive an alert, incorporate it into your findings under a "Cross-File Concerns" section.
|
||||
|
||||
## File Reading Strategy
|
||||
|
||||
1. **Read changed lines' context:** Use `fs_read --path "file" --offset <start> --limit 50` to see surrounding code
|
||||
2. **Grep for usage:** `fs_grep --pattern "function_name" --include "*.rs"` to find callers
|
||||
3. **Never read entire large files:** Target the changed regions only
|
||||
4. **Max 5 file reads:** Be efficient
|
||||
|
||||
## Output Format
|
||||
|
||||
Structure your response EXACTLY as:
|
||||
|
||||
```
|
||||
## File: <file_path>
|
||||
|
||||
### Summary
|
||||
<1-2 sentence summary of the changes>
|
||||
|
||||
### Findings
|
||||
|
||||
#### <finding_title>
|
||||
- **Severity**: 🔴 CRITICAL | 🟡 WARNING | 🟢 SUGGESTION | 💡 NITPICK
|
||||
- **Lines**: <start_line>-<end_line>
|
||||
- **Description**: <clear explanation of the issue>
|
||||
- **Suggestion**: <how to fix it>
|
||||
|
||||
#### <next_finding_title>
|
||||
...
|
||||
|
||||
### Cross-File Concerns
|
||||
<any issues received from siblings or that you alerted siblings about>
|
||||
<"None" if no cross-file concerns>
|
||||
|
||||
REVIEW_COMPLETE
|
||||
```
|
||||
|
||||
## Severity Guide
|
||||
|
||||
| Severity | When to use |
|
||||
|----------|------------|
|
||||
| 🔴 CRITICAL | Bugs, security vulnerabilities, data loss risks, crashes |
|
||||
| 🟡 WARNING | Logic errors, performance issues, missing error handling, race conditions |
|
||||
| 🟢 SUGGESTION | Better patterns, improved readability, missing docs for public APIs |
|
||||
| 💡 NITPICK | Style preferences, minor naming issues, formatting |
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Be specific:** Reference exact line numbers and code
|
||||
2. **Be actionable:** Every finding must have a suggestion
|
||||
3. **Don't nitpick formatting:** If a formatter/linter exists (check for .rustfmt.toml, .prettierrc, etc.)
|
||||
4. **Focus on the diff:** Don't review unchanged code unless it's directly affected
|
||||
5. **Never modify files:** You are read-only
|
||||
6. **Always end with REVIEW_COMPLETE**
|
||||
|
||||
## Context
|
||||
- Project: {{project_dir}}
|
||||
- CWD: {{__cwd__}}
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
source "$LLM_PROMPT_UTILS_FILE"
|
||||
source "$LLM_ROOT_DIR/agents/.shared/utils.sh"
|
||||
|
||||
# @env LLM_OUTPUT=/dev/stdout
|
||||
# @env LLM_AGENT_VAR_PROJECT_DIR=.
|
||||
# @describe File reviewer tools for single-file code review
|
||||
|
||||
_project_dir() {
|
||||
local dir="${LLM_AGENT_VAR_PROJECT_DIR:-.}"
|
||||
(cd "${dir}" 2>/dev/null && pwd) || echo "${dir}"
|
||||
}
|
||||
|
||||
# @cmd Get project structure to understand codebase layout
|
||||
get_structure() {
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
|
||||
info "Project structure:" >> "$LLM_OUTPUT"
|
||||
echo "" >> "$LLM_OUTPUT"
|
||||
|
||||
local project_info
|
||||
project_info=$(detect_project "${project_dir}")
|
||||
|
||||
{
|
||||
echo "Type: $(echo "${project_info}" | jq -r '.type')"
|
||||
echo ""
|
||||
get_tree "${project_dir}" 2
|
||||
} >> "$LLM_OUTPUT"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
name: sisyphus
|
||||
description: OpenCode-style orchestrator - classifies intent, delegates to specialists, tracks progress with todos
|
||||
version: 1.0.0
|
||||
version: 2.0.0
|
||||
temperature: 0.1
|
||||
top_p: 0.95
|
||||
|
||||
@@ -9,13 +9,16 @@ auto_continue: true
|
||||
max_auto_continues: 25
|
||||
inject_todo_instructions: true
|
||||
|
||||
can_spawn_agents: true
|
||||
max_concurrent_agents: 4
|
||||
max_agent_depth: 3
|
||||
inject_spawn_instructions: true
|
||||
summarization_threshold: 4000
|
||||
|
||||
variables:
|
||||
- name: project_dir
|
||||
description: Project directory to work in
|
||||
default: '.'
|
||||
- name: auto_confirm
|
||||
description: Auto-confirm command execution
|
||||
default: '1'
|
||||
|
||||
global_tools:
|
||||
- fs_read.sh
|
||||
@@ -34,14 +37,14 @@ instructions: |
|
||||
| Type | Signal | Action |
|
||||
|------|--------|--------|
|
||||
| Trivial | Single file, known location, typo fix | Do it yourself with tools |
|
||||
| Exploration | "Find X", "Where is Y", "List all Z" | Delegate to `explore` agent |
|
||||
| Implementation | "Add feature", "Fix bug", "Write code" | Delegate to `coder` agent |
|
||||
| Architecture/Design | See oracle triggers below | Delegate to `oracle` agent |
|
||||
| Ambiguous | Unclear scope, multiple interpretations | ASK the user via `ask_user` or `ask_user_input` |
|
||||
| Exploration | "Find X", "Where is Y", "List all Z" | Spawn `explore` agent |
|
||||
| Implementation | "Add feature", "Fix bug", "Write code" | Spawn `coder` agent |
|
||||
| Architecture/Design | See oracle triggers below | Spawn `oracle` agent |
|
||||
| Ambiguous | Unclear scope, multiple interpretations | ASK the user via `user__ask` or `user__input` |
|
||||
|
||||
### Oracle Triggers (MUST delegate to oracle when you see these)
|
||||
### Oracle Triggers (MUST spawn oracle when you see these)
|
||||
|
||||
Delegate to `oracle` ANY time the user asks about:
|
||||
Spawn `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 (not simple errors)
|
||||
- **"Should I use X or Y?"** -- technology or pattern choices
|
||||
@@ -55,54 +58,7 @@ instructions: |
|
||||
Even if you think you know the answer, oracle provides deeper, more thorough analysis.
|
||||
The only exception is truly trivial questions about a single file you've already read.
|
||||
|
||||
## Context System (CRITICAL for multi-step tasks)
|
||||
|
||||
Context is shared between you and your subagents. This lets subagents know what you've learned.
|
||||
|
||||
**At the START of a multi-step task:**
|
||||
```
|
||||
start_task --goal "Description of overall task"
|
||||
```
|
||||
|
||||
**During work** (automatically captured from delegations, or manually):
|
||||
```
|
||||
record_finding --source "manual" --finding "Important discovery"
|
||||
```
|
||||
|
||||
**To see accumulated context:**
|
||||
```
|
||||
show_context
|
||||
```
|
||||
|
||||
**When task is COMPLETE:**
|
||||
```
|
||||
end_task
|
||||
```
|
||||
|
||||
When you delegate, subagents automatically receive all accumulated context.
|
||||
|
||||
## Todo System (MANDATORY for multi-step tasks)
|
||||
|
||||
For ANY task with 2+ steps:
|
||||
1. Call `start_task` with the goal (initializes context)
|
||||
2. Call `todo__init` with the goal
|
||||
3. Call `todo__add` for each step BEFORE starting
|
||||
4. Work through steps, calling `todo__done` IMMEDIATELY after each
|
||||
5. The system auto-continues until all todos are done
|
||||
6. Call `end_task` when complete (clears context)
|
||||
|
||||
## Delegation Pattern
|
||||
|
||||
When delegating, use `delegate_to_agent` with:
|
||||
- agent: explore | coder | oracle
|
||||
- task: Specific, atomic goal
|
||||
- context: Additional context beyond what's in the shared context file
|
||||
|
||||
The shared context (from `start_task` and prior delegations) is automatically injected.
|
||||
|
||||
**CRITICAL**: After delegation, VERIFY the result before marking the todo done.
|
||||
|
||||
## Agent Specializations
|
||||
### Agent Specializations
|
||||
|
||||
| Agent | Use For | Characteristics |
|
||||
|-------|---------|-----------------|
|
||||
@@ -112,40 +68,42 @@ instructions: |
|
||||
|
||||
## Workflow Examples
|
||||
|
||||
### Example 1: Implementation task (explore -> coder)
|
||||
### Example 1: Implementation task (explore -> coder, parallel exploration)
|
||||
|
||||
User: "Add a new API endpoint for user profiles"
|
||||
|
||||
```
|
||||
1. start_task --goal "Add user profiles API endpoint"
|
||||
2. todo__init --goal "Add user profiles API endpoint"
|
||||
3. todo__add --task "Explore existing API patterns"
|
||||
4. todo__add --task "Implement profile endpoint"
|
||||
5. todo__add --task "Verify with build/test"
|
||||
6. delegate_to_agent --agent explore --task "Find existing API endpoint patterns and structures"
|
||||
7. todo__done --id 1
|
||||
8. delegate_to_agent --agent coder --task "Create user profiles endpoint following existing patterns"
|
||||
9. todo__done --id 2
|
||||
10. run_build
|
||||
11. run_tests
|
||||
12. todo__done --id 3
|
||||
13. end_task
|
||||
1. todo__init --goal "Add user profiles API endpoint"
|
||||
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"
|
||||
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]"
|
||||
11. agent__collect --id <coder_id>
|
||||
12. todo__done --id 2
|
||||
13. run_build
|
||||
14. run_tests
|
||||
15. todo__done --id 3
|
||||
```
|
||||
|
||||
### Example 2: Architecture/design question (explore -> oracle)
|
||||
### Example 2: Architecture/design question (explore + oracle in parallel)
|
||||
|
||||
User: "How should I structure the authentication for this app?"
|
||||
|
||||
```
|
||||
1. start_task --goal "Get architecture advice for authentication"
|
||||
2. todo__init --goal "Get architecture advice for authentication"
|
||||
3. todo__add --task "Explore current auth-related code"
|
||||
4. todo__add --task "Consult oracle for architecture recommendation"
|
||||
5. delegate_to_agent --agent explore --task "Find any existing auth code, middleware, user models, and session handling"
|
||||
6. todo__done --id 1
|
||||
7. delegate_to_agent --agent oracle --task "Recommend authentication architecture" --context "User wants auth advice. Explore found: [summarize findings]. Evaluate approaches and recommend the best one with justification."
|
||||
8. todo__done --id 2
|
||||
9. end_task
|
||||
1. todo__init --goal "Get architecture advice for authentication"
|
||||
2. todo__add --task "Explore current auth-related code"
|
||||
3. todo__add --task "Consult oracle for architecture recommendation"
|
||||
4. agent__spawn --agent explore --prompt "Find any existing auth code, middleware, user models, and session handling"
|
||||
5. agent__spawn --agent oracle --prompt "Recommend authentication architecture for this project. Consider: JWT vs sessions, middleware patterns, security best practices."
|
||||
6. agent__collect --id <explore_id>
|
||||
7. todo__done --id 1
|
||||
8. agent__collect --id <oracle_id>
|
||||
9. todo__done --id 2
|
||||
```
|
||||
|
||||
### Example 3: Vague/open-ended question (oracle directly)
|
||||
@@ -153,22 +111,21 @@ instructions: |
|
||||
User: "What do you think of this codebase structure?"
|
||||
|
||||
```
|
||||
1. delegate_to_agent --agent oracle --task "Review the project structure and provide recommendations for improvement"
|
||||
# Oracle will read files and analyze on its own
|
||||
agent__spawn --agent oracle --prompt "Review the project structure and provide recommendations for improvement"
|
||||
agent__collect --id <oracle_id>
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Always start_task first** - Initialize context before multi-step work
|
||||
2. **Always classify before acting** - Don't jump into implementation
|
||||
3. **Create todos for multi-step tasks** - Track your progress
|
||||
4. **Delegate specialized work** - You're a coordinator, not an implementer
|
||||
5. **Verify after delegation** - Don't trust blindly
|
||||
1. **Always classify before acting** - Don't jump into implementation
|
||||
2. **Create todos for multi-step tasks** - Track your progress
|
||||
3. **Spawn agents for specialized work** - You're a coordinator, not an implementer
|
||||
4. **Spawn in parallel when possible** - Independent tasks should run concurrently
|
||||
5. **Verify after collecting agent results** - Don't trust blindly
|
||||
6. **Mark todos done immediately** - Don't batch completions
|
||||
7. **Ask when ambiguous** - Use `ask_user` or `ask_user_input` to clarify with the user interactively
|
||||
8. **Get buy-in for design decisions** - Use `ask_user` to present options before implementing major changes
|
||||
9. **Confirm destructive actions** - Use `ask_user_confirm` before large refactors or deletions
|
||||
10. **Always end_task** - Clean up context when done
|
||||
7. **Ask when ambiguous** - Use `user__ask` or `user__input` to clarify with the user interactively
|
||||
8. **Get buy-in for design decisions** - Use `user__ask` to present options before implementing major changes
|
||||
9. **Confirm destructive actions** - Use `user__confirm` before large refactors or deletions
|
||||
|
||||
## When to Do It Yourself
|
||||
|
||||
@@ -187,58 +144,18 @@ instructions: |
|
||||
|
||||
## User Interaction (CRITICAL - get buy-in before major decisions)
|
||||
|
||||
You have tools to prompt the user for input. Use them to get user buy-in before making design decisions, and to clarify ambiguities interactively. **Do NOT guess when you can ask.**
|
||||
You have built-in tools to prompt the user for input. Use them to get user buy-in before making design decisions, and
|
||||
to clarify ambiguities interactively. **Do NOT guess when you can ask.**
|
||||
|
||||
### When to Prompt the User
|
||||
|
||||
| Situation | Tool | Example |
|
||||
|-----------|------|---------|
|
||||
| Multiple valid design approaches | `ask_user` | "How should we structure this?" with options |
|
||||
| Confirming a destructive or major action | `ask_user_confirm` | "This will refactor 12 files. Proceed?" |
|
||||
| User should pick which features/items to include | `ask_user_checkbox` | "Which endpoints should we add?" |
|
||||
| Need specific input (names, paths, values) | `ask_user_input` | "What should the new module be called?" |
|
||||
| Ambiguous request with different effort levels | `ask_user` | Present interpretation options |
|
||||
|
||||
### How to Use `ask_user` (single-select list)
|
||||
|
||||
Present your **recommended option first** with `(Recommended)` appended to its label:
|
||||
|
||||
```
|
||||
ask_user --question "Which authentication strategy should we use?" \
|
||||
--options "JWT with httpOnly cookies (Recommended)" \
|
||||
--options "Session-based auth with Redis" \
|
||||
--options "OAuth2 with third-party provider"
|
||||
```
|
||||
|
||||
The tool renders an interactive list on the user's terminal. They navigate with arrow keys and press Enter. The selected label is returned to you.
|
||||
|
||||
### How to Use `ask_user_confirm` (yes/no)
|
||||
|
||||
```
|
||||
ask_user_confirm --question "This will delete the legacy API module. Continue?"
|
||||
```
|
||||
|
||||
Returns "User confirmed: yes" or "User confirmed: no". **Respect the answer** — if the user says no, do NOT proceed with that action.
|
||||
|
||||
### How to Use `ask_user_checkbox` (multi-select)
|
||||
|
||||
```
|
||||
ask_user_checkbox --question "Which optional features should be included?" \
|
||||
--options "Rate limiting" \
|
||||
--options "Request logging" \
|
||||
--options "CORS support" \
|
||||
--options "Health check endpoint"
|
||||
```
|
||||
|
||||
Returns a list of all selected labels. The user selects items with Space and confirms with Enter.
|
||||
|
||||
### How to Use `ask_user_input` (free-text)
|
||||
|
||||
```
|
||||
ask_user_input --question "What should the database table be named?"
|
||||
```
|
||||
|
||||
Returns whatever text the user typed.
|
||||
| Multiple valid design approaches | `user__ask` | "How should we structure this?" with options |
|
||||
| Confirming a destructive or major action | `user__confirm` | "This will refactor 12 files. Proceed?" |
|
||||
| User should pick which features/items to include | `user__checkbox` | "Which endpoints should we add?" |
|
||||
| Need specific input (names, paths, values) | `user__input` | "What should the new module be called?" |
|
||||
| Ambiguous request with different effort levels | `user__ask` | Present interpretation options |
|
||||
|
||||
### Design Review Pattern
|
||||
|
||||
@@ -246,27 +163,23 @@ instructions: |
|
||||
|
||||
1. **Explore** the codebase to understand existing patterns
|
||||
2. **Formulate** 2-3 design options based on findings
|
||||
3. **Present options** to the user via `ask_user` with your recommendation marked `(Recommended)`
|
||||
3. **Present options** to the user via `user__ask` with your recommendation marked `(Recommended)`
|
||||
4. **Confirm** the chosen approach before delegating to `coder`
|
||||
5. Proceed with implementation
|
||||
|
||||
Example flow:
|
||||
```
|
||||
1. delegate_to_agent --agent explore --task "Find existing API patterns"
|
||||
2. ask_user --question "I found two patterns in the codebase. Which should we follow?" \
|
||||
--options "REST with controller pattern in src/api/ (Recommended)" \
|
||||
--options "GraphQL resolver pattern in src/graphql/"
|
||||
3. ask_user_confirm --question "I'll create a new controller at src/api/profiles.rs following the REST pattern. Proceed?"
|
||||
4. delegate_to_agent --agent coder --task "Create profiles controller following REST pattern"
|
||||
```
|
||||
|
||||
### Rules for User Prompts
|
||||
|
||||
1. **Always include (Recommended)** on the option you think is best in `ask_user`
|
||||
2. **Respect user choices** — never override or ignore a selection
|
||||
3. **Don't over-prompt** — trivial decisions (variable names in small functions, formatting) don't need prompts
|
||||
1. **Always include (Recommended)** on the option you think is best in `user__ask`
|
||||
2. **Respect user choices** - never override or ignore a selection
|
||||
3. **Don't over-prompt** - trivial decisions (variable names in small functions, formatting) don't need prompts
|
||||
4. **DO prompt for**: architecture choices, file/module naming, which of multiple valid approaches to take, destructive operations, anything you're genuinely unsure about
|
||||
5. **Confirm before large changes** — if a task will touch 5+ files, confirm the plan first
|
||||
5. **Confirm before large changes** - if a task will touch 5+ files, confirm the plan first
|
||||
|
||||
## Escalation Handling
|
||||
|
||||
If you see `pending_escalations` in your tool results, a child agent needs user input and is blocked.
|
||||
Reply promptly via `agent__reply_escalation` to unblock it. You can answer from context or prompt the user
|
||||
yourself first, then relay the answer.
|
||||
|
||||
## Available Tools
|
||||
{{__tools__}}
|
||||
|
||||
@@ -7,126 +7,18 @@ export AUTO_CONFIRM=true
|
||||
|
||||
# @env LLM_OUTPUT=/dev/stdout
|
||||
# @env LLM_AGENT_VAR_PROJECT_DIR=.
|
||||
# @describe Sisyphus orchestrator tools for delegating to specialized agents
|
||||
# @describe Sisyphus orchestrator tools (project info, build, test)
|
||||
|
||||
_project_dir() {
|
||||
local dir="${LLM_AGENT_VAR_PROJECT_DIR:-.}"
|
||||
(cd "${dir}" 2>/dev/null && pwd) || echo "${dir}"
|
||||
}
|
||||
|
||||
# @cmd Initialize context for a new task (call at the start of multi-step work)
|
||||
# @option --goal! Description of the overall task/goal
|
||||
start_task() {
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
|
||||
export LLM_AGENT_VAR_PROJECT_DIR="${project_dir}"
|
||||
# shellcheck disable=SC2154
|
||||
init_context "${argc_goal}"
|
||||
|
||||
cat <<-EOF >> "$LLM_OUTPUT"
|
||||
$(green "Context initialized for task: ${argc_goal}")
|
||||
Context file: ${project_dir}/.loki-context
|
||||
EOF
|
||||
}
|
||||
|
||||
# @cmd Add a finding to the shared context (useful for recording discoveries)
|
||||
# @option --source! Source of the finding (e.g., "manual", "explore", "coder")
|
||||
# @option --finding! The finding to record
|
||||
record_finding() {
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
|
||||
export LLM_AGENT_VAR_PROJECT_DIR="${project_dir}"
|
||||
# shellcheck disable=SC2154
|
||||
append_context "${argc_source}" "${argc_finding}"
|
||||
|
||||
green "Recorded finding from ${argc_source}" >> "$LLM_OUTPUT"
|
||||
}
|
||||
|
||||
# @cmd Show current accumulated context
|
||||
show_context() {
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
|
||||
export LLM_AGENT_VAR_PROJECT_DIR="${project_dir}"
|
||||
local context
|
||||
context=$(read_context)
|
||||
|
||||
if [[ -n "${context}" ]]; then
|
||||
cat <<-EOF >> "$LLM_OUTPUT"
|
||||
$(info "Current Context:")
|
||||
|
||||
${context}
|
||||
EOF
|
||||
else
|
||||
warn "No context file found. Use start_task to initialize." >> "$LLM_OUTPUT"
|
||||
fi
|
||||
}
|
||||
|
||||
# @cmd Clear the context file (call when task is complete)
|
||||
end_task() {
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
|
||||
export LLM_AGENT_VAR_PROJECT_DIR="${project_dir}"
|
||||
clear_context
|
||||
|
||||
green "Context cleared. Task complete." >> "$LLM_OUTPUT"
|
||||
}
|
||||
|
||||
# @cmd Delegate a task to a specialized agent
|
||||
# @option --agent! Agent to delegate to: explore, coder, or oracle
|
||||
# @option --task! Specific task description for the agent
|
||||
# @option --context Additional context (file paths, patterns, constraints)
|
||||
delegate_to_agent() {
|
||||
local extra_context="${argc_context:-}"
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
|
||||
# shellcheck disable=SC2154
|
||||
if [[ ! "${argc_agent}" =~ ^(explore|coder|oracle)$ ]]; then
|
||||
error "Invalid agent: ${argc_agent}. Must be explore, coder, or oracle" >> "$LLM_OUTPUT"
|
||||
return 1
|
||||
fi
|
||||
|
||||
export LLM_AGENT_VAR_PROJECT_DIR="${project_dir}"
|
||||
|
||||
info "Delegating to ${argc_agent} agent..." >> "$LLM_OUTPUT"
|
||||
echo "" >> "$LLM_OUTPUT"
|
||||
|
||||
# shellcheck disable=SC2154
|
||||
local prompt="${argc_task}"
|
||||
if [[ -n "${extra_context}" ]]; then
|
||||
prompt="$(printf "%s\n\nAdditional Context:\n%s\n" "${argc_task}" "${extra_context}")"
|
||||
fi
|
||||
|
||||
cat <<-EOF >> "$LLM_OUTPUT"
|
||||
$(cyan "------------------------------------------")
|
||||
DELEGATING TO: ${argc_agent}
|
||||
TASK: ${argc_task}
|
||||
$(cyan "------------------------------------------")
|
||||
|
||||
EOF
|
||||
|
||||
local output
|
||||
output=$(invoke_agent_with_summary "${argc_agent}" "${prompt}" \
|
||||
--agent-variable project_dir "${project_dir}" 2>&1) || true
|
||||
|
||||
cat <<-EOF >> "$LLM_OUTPUT"
|
||||
${output}
|
||||
|
||||
$(cyan "------------------------------------------")
|
||||
DELEGATION COMPLETE: ${argc_agent}
|
||||
$(cyan "------------------------------------------")
|
||||
EOF
|
||||
}
|
||||
|
||||
# @cmd Get project information and structure
|
||||
get_project_info() {
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
|
||||
|
||||
info "Project: ${project_dir}" >> "$LLM_OUTPUT"
|
||||
echo "" >> "$LLM_OUTPUT"
|
||||
|
||||
@@ -147,17 +39,17 @@ get_project_info() {
|
||||
run_build() {
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
|
||||
|
||||
local project_info
|
||||
project_info=$(detect_project "${project_dir}")
|
||||
local build_cmd
|
||||
build_cmd=$(echo "${project_info}" | jq -r '.build')
|
||||
|
||||
|
||||
if [[ -z "${build_cmd}" ]] || [[ "${build_cmd}" == "null" ]]; then
|
||||
warn "No build command detected for this project" >> "$LLM_OUTPUT"
|
||||
return 0
|
||||
fi
|
||||
|
||||
|
||||
info "Running: ${build_cmd}" >> "$LLM_OUTPUT"
|
||||
echo "" >> "$LLM_OUTPUT"
|
||||
|
||||
@@ -177,17 +69,17 @@ run_build() {
|
||||
run_tests() {
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
|
||||
|
||||
local project_info
|
||||
project_info=$(detect_project "${project_dir}")
|
||||
local test_cmd
|
||||
test_cmd=$(echo "${project_info}" | jq -r '.test')
|
||||
|
||||
|
||||
if [[ -z "${test_cmd}" ]] || [[ "${test_cmd}" == "null" ]]; then
|
||||
warn "No test command detected for this project" >> "$LLM_OUTPUT"
|
||||
return 0
|
||||
fi
|
||||
|
||||
|
||||
info "Running: ${test_cmd}" >> "$LLM_OUTPUT"
|
||||
echo "" >> "$LLM_OUTPUT"
|
||||
|
||||
@@ -203,151 +95,3 @@ run_tests() {
|
||||
fi
|
||||
}
|
||||
|
||||
# @cmd Quick file search in the project
|
||||
# @option --pattern! File name pattern to search for (e.g., "*.rs", "config*")
|
||||
search_files() {
|
||||
# shellcheck disable=SC2154
|
||||
local pattern="${argc_pattern}"
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
|
||||
info "Searching for: ${pattern}" >> "$LLM_OUTPUT"
|
||||
echo "" >> "$LLM_OUTPUT"
|
||||
|
||||
local results
|
||||
results=$(search_files "${pattern}" "${project_dir}")
|
||||
|
||||
if [[ -n "${results}" ]]; then
|
||||
echo "${results}" >> "$LLM_OUTPUT"
|
||||
else
|
||||
warn "No files found matching: ${pattern}" >> "$LLM_OUTPUT"
|
||||
fi
|
||||
}
|
||||
|
||||
# @cmd Search for content in files
|
||||
# @option --pattern! Text pattern to search for
|
||||
# @option --file-type File extension to search in (e.g., "rs", "py")
|
||||
search_content() {
|
||||
local pattern="${argc_pattern}"
|
||||
local file_type="${argc_file_type:-}"
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
|
||||
info "Searching for: ${pattern}" >> "$LLM_OUTPUT"
|
||||
echo "" >> "$LLM_OUTPUT"
|
||||
|
||||
local grep_args="-rn"
|
||||
if [[ -n "${file_type}" ]]; then
|
||||
grep_args="${grep_args} --include=*.${file_type}"
|
||||
fi
|
||||
|
||||
local results
|
||||
# shellcheck disable=SC2086
|
||||
results=$(grep ${grep_args} "${pattern}" "${project_dir}" 2>/dev/null | \
|
||||
grep -v '/target/' | grep -v '/node_modules/' | grep -v '/.git/' | \
|
||||
head -30) || true
|
||||
|
||||
if [[ -n "${results}" ]]; then
|
||||
echo "${results}" >> "$LLM_OUTPUT"
|
||||
else
|
||||
warn "No matches found for: ${pattern}" >> "$LLM_OUTPUT"
|
||||
fi
|
||||
}
|
||||
|
||||
# @cmd Ask the user to select ONE option from a list. The first option should be your recommended choice — append '(Recommended)' to its label. Returns the selected option's label text.
|
||||
# @option --question! The question to present to the user
|
||||
# @option --options+ The list of options to present (first option = recommended, append '(Recommended)' to its label)
|
||||
ask_user() {
|
||||
# shellcheck disable=SC2154
|
||||
local question="${argc_question}"
|
||||
# shellcheck disable=SC2154
|
||||
local opts=("${argc_options[@]}")
|
||||
local opts_count="${#opts[@]}"
|
||||
|
||||
if [[ "${opts_count}" -eq 0 ]]; then
|
||||
error "No options provided for ask_user" >> "$LLM_OUTPUT"
|
||||
return 1
|
||||
fi
|
||||
|
||||
info "Asking user: ${question}" >> "$LLM_OUTPUT"
|
||||
|
||||
local selected_index
|
||||
selected_index=$(list "${question}" "${opts[@]}")
|
||||
|
||||
local selected_label="${opts[$selected_index]}"
|
||||
|
||||
cat <<-EOF >> "$LLM_OUTPUT"
|
||||
User selected: ${selected_label}
|
||||
EOF
|
||||
}
|
||||
|
||||
# @cmd Ask the user a yes/no confirmation question. Returns 'yes' or 'no'.
|
||||
# @option --question! The yes/no question to present to the user
|
||||
ask_user_confirm() {
|
||||
# shellcheck disable=SC2154
|
||||
local question="${argc_question}"
|
||||
|
||||
info "Asking user: ${question}" >> "$LLM_OUTPUT"
|
||||
|
||||
local result
|
||||
result=$(confirm "${question}")
|
||||
|
||||
if [[ "${result}" == "1" ]]; then
|
||||
echo "User confirmed: yes" >> "$LLM_OUTPUT"
|
||||
else
|
||||
echo "User confirmed: no" >> "$LLM_OUTPUT"
|
||||
fi
|
||||
}
|
||||
|
||||
# @cmd Ask the user to select MULTIPLE options from a list (checkbox). Returns the labels of all selected items.
|
||||
# @option --question! The question to present to the user
|
||||
# @option --options+ The list of options the user can select from (multiple selections allowed)
|
||||
ask_user_checkbox() {
|
||||
# shellcheck disable=SC2154
|
||||
local question="${argc_question}"
|
||||
# shellcheck disable=SC2154
|
||||
local opts=("${argc_options[@]}")
|
||||
local opts_count="${#opts[@]}"
|
||||
|
||||
if [[ "${opts_count}" -eq 0 ]]; then
|
||||
error "No options provided for ask_user_checkbox" >> "$LLM_OUTPUT"
|
||||
return 1
|
||||
fi
|
||||
|
||||
info "Asking user (select multiple): ${question}" >> "$LLM_OUTPUT"
|
||||
|
||||
local checked_indices
|
||||
checked_indices=$(checkbox "${question}" "${opts[@]}")
|
||||
|
||||
local selected_labels=()
|
||||
for idx in ${checked_indices}; do
|
||||
if [[ -n "${idx}" ]] && [[ "${idx}" =~ ^[0-9]+$ ]]; then
|
||||
selected_labels+=("${opts[$idx]}")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "${#selected_labels[@]}" -eq 0 ]]; then
|
||||
echo "User selected: (none)" >> "$LLM_OUTPUT"
|
||||
else
|
||||
echo "User selected:" >> "$LLM_OUTPUT"
|
||||
for label in "${selected_labels[@]}"; do
|
||||
echo " - ${label}" >> "$LLM_OUTPUT"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
# @cmd Ask the user for free-text input. Returns whatever the user typed.
|
||||
# @option --question! The prompt/question to present to the user
|
||||
ask_user_input() {
|
||||
# shellcheck disable=SC2154
|
||||
local question="${argc_question}"
|
||||
|
||||
info "Asking user: ${question}" >> "$LLM_OUTPUT"
|
||||
|
||||
local user_text
|
||||
user_text=$(input "${question}")
|
||||
|
||||
cat <<-EOF >> "$LLM_OUTPUT"
|
||||
User input: ${user_text}
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ _cursor_blink_off() {
|
||||
}
|
||||
|
||||
_cursor_to() {
|
||||
echo -en "\033[$1;$2H" >&2
|
||||
echo -en "\033[$1;${2:-1}H" >&2
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2154
|
||||
@@ -133,7 +133,7 @@ _key_input() {
|
||||
_read_stdin -rsn2 b
|
||||
fi
|
||||
|
||||
declare input="${a}${b}"
|
||||
declare input="${a}${b:-}"
|
||||
case "$input" in
|
||||
"${ESC}[A" | "k") echo up ;;
|
||||
"${ESC}[B" | "j") echo down ;;
|
||||
|
||||
@@ -24,6 +24,16 @@ auto_continue: false # Enable automatic continuation when incomplete
|
||||
max_auto_continues: 10 # Maximum number of automatic continuations before stopping
|
||||
inject_todo_instructions: true # Inject the default todo tool usage instructions into the agent's system prompt
|
||||
continuation_prompt: null # Custom prompt used when auto-continuing (optional; uses default if null)
|
||||
# Sub-Agent Spawning System
|
||||
# Enable this agent to spawn and manage child agents in parallel.
|
||||
# See docs/AGENTS.md for detailed documentation.
|
||||
can_spawn_agents: false # Enable the agent to spawn child agents
|
||||
max_concurrent_agents: 4 # Maximum number of agents that can run simultaneously
|
||||
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
|
||||
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
|
||||
global_tools: # Optional list of additional global tools to enable for the agent; i.e. not tools specific to the agent
|
||||
|
||||
+238
-1
@@ -35,6 +35,18 @@ If you're looking for more example agents, refer to the [built-in agents](../ass
|
||||
- [Bash-Based Agent Tools](#bash-based-agent-tools)
|
||||
- [5. Conversation Starters](#5-conversation-starters)
|
||||
- [6. Todo System & Auto-Continuation](#6-todo-system--auto-continuation)
|
||||
- [7. Sub-Agent Spawning System](#7-sub-agent-spawning-system)
|
||||
- [Configuration](#spawning-configuration)
|
||||
- [Spawning & Collecting Agents](#spawning--collecting-agents)
|
||||
- [Task Queue with Dependencies](#task-queue-with-dependencies)
|
||||
- [Active Task Dispatch](#active-task-dispatch)
|
||||
- [Output Summarization](#output-summarization)
|
||||
- [Teammate Messaging](#teammate-messaging)
|
||||
- [Runaway Safeguards](#runaway-safeguards)
|
||||
- [8. User Interaction Tools](#8-user-interaction-tools)
|
||||
- [Available Tools](#user-interaction-available-tools)
|
||||
- [Escalation (Sub-Agent to User)](#escalation-sub-agent-to-user)
|
||||
- [9. Auto-Injected Prompts](#9-auto-injected-prompts)
|
||||
- [Built-In Agents](#built-in-agents)
|
||||
<!--toc:end-->
|
||||
|
||||
@@ -87,6 +99,14 @@ auto_continue: false # Enable automatic continuation when incomp
|
||||
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
|
||||
@@ -471,13 +491,230 @@ inject_todo_instructions: true # Include the default todo instructions into pr
|
||||
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
|
||||
* `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 agent for writing complex code and acting as a natural language interface for your codebase (similar to ClaudeCode, Gemini CLI, Codex, or OpenCode)
|
||||
* `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
|
||||
|
||||
+4
-1
@@ -50,6 +50,9 @@ things like
|
||||
* **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.
|
||||
|
||||
---
|
||||
|
||||
@@ -247,4 +250,4 @@ The `.exit` command is used to move between modes in 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.
|
||||
That's when you use the `.help` command.
|
||||
|
||||
@@ -66,12 +66,12 @@ Prompt for text input
|
||||
|
||||
**Example With Validation:**
|
||||
```bash
|
||||
text=$(with_validation 'input "Please enter something:"' validate_present)
|
||||
text=$(with_validation 'input "Please enter something:"' validate_present 2>/dev/tty)
|
||||
```
|
||||
|
||||
**Example Without Validation:**
|
||||
```bash
|
||||
text=$(input "Please enter something:")
|
||||
text=$(input "Please enter something:" 2>/dev/tty)
|
||||
```
|
||||
|
||||
### confirm
|
||||
@@ -81,7 +81,7 @@ Show a confirm dialog with options for yes/no
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
confirmed=$(confirm "Do the thing?")
|
||||
confirmed=$(confirm "Do the thing?" 2>/dev/tty)
|
||||
if [[ $confirmed == "0" ]]; then echo "No"; else echo "Yes"; fi
|
||||
```
|
||||
|
||||
@@ -94,7 +94,7 @@ keys that then returns the chosen option.
|
||||
**Example:**
|
||||
```bash
|
||||
options=("one" "two" "three" "four")
|
||||
choice=$(list "Select an item" "${options[@]}")
|
||||
choice=$(list "Select an item" "${options[@]}" 2>/dev/tty)
|
||||
echo "Your choice: ${options[$choice]}"
|
||||
```
|
||||
|
||||
@@ -107,7 +107,7 @@ and enter keys that then returns the chosen options.
|
||||
**Example:**
|
||||
```bash
|
||||
options=("one" "two" "three" "four")
|
||||
checked=$(checkbox "Select one or more items" "${options[@]}")
|
||||
checked=$(checkbox "Select one or more items" "${options[@]}" 2>/dev/tty)
|
||||
echo "Your choices: ${checked}"
|
||||
```
|
||||
|
||||
@@ -124,12 +124,12 @@ validate_password() {
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
pass=$(with_validate 'password "Enter your password"' validate_password)
|
||||
pass=$(with_validate 'password "Enter your password"' validate_password 2>/dev/tty)
|
||||
```
|
||||
|
||||
**Example Without Validation:**
|
||||
```bash
|
||||
pass="$(password "Enter your password:")"
|
||||
pass="$(password "Enter your password:" 2>/dev/tty)"
|
||||
```
|
||||
|
||||
### editor
|
||||
@@ -137,7 +137,7 @@ Open the default editor (`$EDITOR`); if none is set, default back to `vi`
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
text=$(editor "Please enter something in the editor")
|
||||
text=$(editor "Please enter something in the editor" 2>/dev/tty)
|
||||
echo -e "You wrote:\n${text}"
|
||||
```
|
||||
|
||||
@@ -150,7 +150,7 @@ 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)
|
||||
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() {
|
||||
@@ -159,7 +159,7 @@ validate_password() {
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
pass=$(with_validate 'password "Enter random password"' validate_password)
|
||||
pass=$(with_validate 'password "Enter random password"' validate_password 2>/dev/tty)
|
||||
```
|
||||
|
||||
### validate_present
|
||||
@@ -169,7 +169,7 @@ Validate that the prompt returned a value.
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
text=$(with_validate 'input "Please enter something and confirm with enter"' validate_present)
|
||||
text=$(with_validate 'input "Please enter something and confirm with enter"' validate_present 2>/dev/tty)
|
||||
```
|
||||
|
||||
### detect_os
|
||||
|
||||
@@ -411,9 +411,11 @@ pub async fn call_chat_completions(
|
||||
client: &dyn Client,
|
||||
abort_signal: AbortSignal,
|
||||
) -> Result<(String, Vec<ToolResult>)> {
|
||||
let is_child_agent = client.global_config().read().current_depth > 0;
|
||||
let spinner_message = if is_child_agent { "" } else { "Generating" };
|
||||
let ret = abortable_run_with_spinner(
|
||||
client.chat_completions(input.clone()),
|
||||
"Generating",
|
||||
spinner_message,
|
||||
abort_signal,
|
||||
)
|
||||
.await;
|
||||
|
||||
+74
-13
@@ -6,6 +6,10 @@ use crate::{
|
||||
function::{Functions, run_llm_function},
|
||||
};
|
||||
|
||||
use crate::config::prompts::{
|
||||
DEFAULT_SPAWN_INSTRUCTIONS, DEFAULT_TEAMMATE_INSTRUCTIONS, DEFAULT_TODO_INSTRUCTIONS,
|
||||
DEFAULT_USER_INTERACTION_INSTRUCTIONS,
|
||||
};
|
||||
use crate::vault::SECRET_RE;
|
||||
use anyhow::{Context, Result};
|
||||
use fancy_regex::Captures;
|
||||
@@ -15,18 +19,6 @@ use serde::{Deserialize, Serialize};
|
||||
use std::{ffi::OsStr, path::Path};
|
||||
|
||||
const DEFAULT_AGENT_NAME: &str = "rag";
|
||||
const DEFAULT_TODO_INSTRUCTIONS: &str = "\
|
||||
\n## Task Tracking\n\
|
||||
You have built-in task tracking tools. Use them to track your progress:\n\
|
||||
- `todo__init`: Initialize a todo list with a goal. Call this at the start of every multi-step task.\n\
|
||||
- `todo__add`: Add individual tasks. Add all planned steps before starting work.\n\
|
||||
- `todo__done`: Mark a task done by id. Call this immediately after completing each step.\n\
|
||||
- `todo__list`: Show the current todo list.\n\
|
||||
\n\
|
||||
RULES:\n\
|
||||
- Always create a todo list before starting work.\n\
|
||||
- Mark each task done as soon as you finish it; do not batch.\n\
|
||||
- If you stop with incomplete tasks, the system will automatically prompt you to continue.";
|
||||
|
||||
pub type AgentVariables = IndexMap<String, String>;
|
||||
|
||||
@@ -140,7 +132,6 @@ impl Agent {
|
||||
}
|
||||
|
||||
config.write().mcp_registry = Some(new_mcp_registry);
|
||||
agent_config.replace_tools_placeholder(&functions);
|
||||
|
||||
agent_config.load_envs(&config.read());
|
||||
|
||||
@@ -208,6 +199,15 @@ impl Agent {
|
||||
functions.append_todo_functions();
|
||||
}
|
||||
|
||||
if agent_config.can_spawn_agents {
|
||||
functions.append_supervisor_functions();
|
||||
}
|
||||
|
||||
functions.append_teammate_functions();
|
||||
functions.append_user_interaction_functions();
|
||||
|
||||
agent_config.replace_tools_placeholder(&functions);
|
||||
|
||||
Ok(Self {
|
||||
name: name.to_string(),
|
||||
config: agent_config,
|
||||
@@ -342,6 +342,13 @@ impl Agent {
|
||||
output.push_str(DEFAULT_TODO_INSTRUCTIONS);
|
||||
}
|
||||
|
||||
if self.config.can_spawn_agents && self.config.inject_spawn_instructions {
|
||||
output.push_str(DEFAULT_SPAWN_INSTRUCTIONS);
|
||||
}
|
||||
|
||||
output.push_str(DEFAULT_TEAMMATE_INSTRUCTIONS);
|
||||
output.push_str(DEFAULT_USER_INTERACTION_INSTRUCTIONS);
|
||||
|
||||
self.interpolate_text(&output)
|
||||
}
|
||||
|
||||
@@ -412,6 +419,30 @@ impl Agent {
|
||||
self.config.max_auto_continues
|
||||
}
|
||||
|
||||
pub fn can_spawn_agents(&self) -> bool {
|
||||
self.config.can_spawn_agents
|
||||
}
|
||||
|
||||
pub fn max_concurrent_agents(&self) -> usize {
|
||||
self.config.max_concurrent_agents
|
||||
}
|
||||
|
||||
pub fn max_agent_depth(&self) -> usize {
|
||||
self.config.max_agent_depth
|
||||
}
|
||||
|
||||
pub fn summarization_model(&self) -> Option<&str> {
|
||||
self.config.summarization_model.as_deref()
|
||||
}
|
||||
|
||||
pub fn summarization_threshold(&self) -> usize {
|
||||
self.config.summarization_threshold
|
||||
}
|
||||
|
||||
pub fn escalation_timeout(&self) -> u64 {
|
||||
self.config.escalation_timeout
|
||||
}
|
||||
|
||||
pub fn continuation_count(&self) -> usize {
|
||||
self.continuation_count
|
||||
}
|
||||
@@ -590,10 +621,18 @@ pub struct AgentConfig {
|
||||
pub agent_session: Option<String>,
|
||||
#[serde(default)]
|
||||
pub auto_continue: bool,
|
||||
#[serde(default)]
|
||||
pub can_spawn_agents: bool,
|
||||
#[serde(default = "default_max_concurrent_agents")]
|
||||
pub max_concurrent_agents: usize,
|
||||
#[serde(default = "default_max_agent_depth")]
|
||||
pub max_agent_depth: usize,
|
||||
#[serde(default = "default_max_auto_continues")]
|
||||
pub max_auto_continues: usize,
|
||||
#[serde(default = "default_true")]
|
||||
pub inject_todo_instructions: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub inject_spawn_instructions: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub compression_threshold: Option<usize>,
|
||||
#[serde(default)]
|
||||
@@ -616,16 +655,38 @@ pub struct AgentConfig {
|
||||
pub conversation_starters: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub documents: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub summarization_model: Option<String>,
|
||||
#[serde(default = "default_summarization_threshold")]
|
||||
pub summarization_threshold: usize,
|
||||
#[serde(default = "default_escalation_timeout")]
|
||||
pub escalation_timeout: u64,
|
||||
}
|
||||
|
||||
fn default_max_auto_continues() -> usize {
|
||||
10
|
||||
}
|
||||
|
||||
fn default_max_concurrent_agents() -> usize {
|
||||
4
|
||||
}
|
||||
|
||||
fn default_max_agent_depth() -> usize {
|
||||
3
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_summarization_threshold() -> usize {
|
||||
4000
|
||||
}
|
||||
|
||||
fn default_escalation_timeout() -> u64 {
|
||||
300
|
||||
}
|
||||
|
||||
impl AgentConfig {
|
||||
pub fn load(path: &Path) -> Result<Self> {
|
||||
let contents = read_to_string(path)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod agent;
|
||||
mod input;
|
||||
mod macros;
|
||||
mod prompts;
|
||||
mod role;
|
||||
mod session;
|
||||
pub(crate) mod todo;
|
||||
@@ -18,6 +19,7 @@ use crate::client::{
|
||||
ClientConfig, MessageContentToolCalls, Model, ModelType, OPENAI_COMPATIBLE_PROVIDERS,
|
||||
ProviderModels, create_client_config, list_client_types, list_models,
|
||||
};
|
||||
use crate::function::user_interaction::USER_FUNCTION_PREFIX;
|
||||
use crate::function::{FunctionDeclaration, Functions, ToolCallTracker, ToolResult};
|
||||
use crate::rag::Rag;
|
||||
use crate::render::{MarkdownRender, RenderOptions};
|
||||
@@ -28,6 +30,9 @@ use crate::mcp::{
|
||||
MCP_DESCRIBE_META_FUNCTION_NAME_PREFIX, MCP_INVOKE_META_FUNCTION_NAME_PREFIX,
|
||||
MCP_SEARCH_META_FUNCTION_NAME_PREFIX, McpRegistry,
|
||||
};
|
||||
use crate::supervisor::Supervisor;
|
||||
use crate::supervisor::escalation::EscalationQueue;
|
||||
use crate::supervisor::mailbox::Inbox;
|
||||
use crate::vault::{GlobalVault, Vault, create_vault_password_file, interpolate_secrets};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use fancy_regex::Regex;
|
||||
@@ -207,6 +212,18 @@ pub struct Config {
|
||||
pub agent: Option<Agent>,
|
||||
#[serde(skip)]
|
||||
pub(crate) tool_call_tracker: Option<ToolCallTracker>,
|
||||
#[serde(skip)]
|
||||
pub supervisor: Option<Arc<RwLock<Supervisor>>>,
|
||||
#[serde(skip)]
|
||||
pub parent_supervisor: Option<Arc<RwLock<Supervisor>>>,
|
||||
#[serde(skip)]
|
||||
pub self_agent_id: Option<String>,
|
||||
#[serde(skip)]
|
||||
pub current_depth: usize,
|
||||
#[serde(skip)]
|
||||
pub inbox: Option<Arc<Inbox>>,
|
||||
#[serde(skip)]
|
||||
pub root_escalation_queue: Option<Arc<EscalationQueue>>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@@ -280,6 +297,12 @@ impl Default for Config {
|
||||
rag: None,
|
||||
agent: None,
|
||||
tool_call_tracker: Some(ToolCallTracker::default()),
|
||||
supervisor: None,
|
||||
parent_supervisor: None,
|
||||
self_agent_id: None,
|
||||
current_depth: 0,
|
||||
inbox: None,
|
||||
root_escalation_queue: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1818,8 +1841,17 @@ impl Config {
|
||||
agent.agent_session().map(|v| v.to_string())
|
||||
}
|
||||
});
|
||||
let should_init_supervisor = agent.can_spawn_agents();
|
||||
let max_concurrent = agent.max_concurrent_agents();
|
||||
let max_depth = agent.max_agent_depth();
|
||||
config.write().rag = agent.rag();
|
||||
config.write().agent = Some(agent);
|
||||
if should_init_supervisor {
|
||||
config.write().supervisor = Some(Arc::new(RwLock::new(Supervisor::new(
|
||||
max_concurrent,
|
||||
max_depth,
|
||||
))));
|
||||
}
|
||||
if let Some(session) = session {
|
||||
Config::use_session_safely(config, Some(&session), abort_signal).await?;
|
||||
} else {
|
||||
@@ -1871,6 +1903,10 @@ impl Config {
|
||||
self.exit_session()?;
|
||||
self.load_functions()?;
|
||||
if self.agent.take().is_some() {
|
||||
if let Some(ref supervisor) = self.supervisor {
|
||||
supervisor.read().cancel_all();
|
||||
}
|
||||
self.supervisor.take();
|
||||
self.rag.take();
|
||||
self.discontinuous_last_message();
|
||||
}
|
||||
@@ -2029,6 +2065,20 @@ impl Config {
|
||||
.collect();
|
||||
}
|
||||
|
||||
if self.agent.is_none() {
|
||||
let existing: HashSet<String> = functions.iter().map(|f| f.name.clone()).collect();
|
||||
let builtin_functions: Vec<FunctionDeclaration> = self
|
||||
.functions
|
||||
.declarations()
|
||||
.iter()
|
||||
.filter(|v| {
|
||||
v.name.starts_with(USER_FUNCTION_PREFIX) && !existing.contains(&v.name)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
functions.extend(builtin_functions);
|
||||
}
|
||||
|
||||
if let Some(agent) = &self.agent {
|
||||
let mut agent_functions: Vec<FunctionDeclaration> = agent
|
||||
.functions()
|
||||
@@ -2886,6 +2936,9 @@ impl Config {
|
||||
|
||||
fn load_functions(&mut self) -> Result<()> {
|
||||
self.functions = Functions::init(self.visible_tools.as_ref().unwrap_or(&Vec::new()))?;
|
||||
if self.working_mode.is_repl() {
|
||||
self.functions.append_user_interaction_functions();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
use indoc::indoc;
|
||||
|
||||
pub(in crate::config) const DEFAULT_TODO_INSTRUCTIONS: &str = indoc! {"
|
||||
## Task Tracking
|
||||
You have built-in task tracking tools. Use them to track your progress:
|
||||
- `todo__init`: Initialize a todo list with a goal. Call this at the start of every multi-step task.
|
||||
- `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.
|
||||
|
||||
RULES:
|
||||
- Always create a todo list before starting work.
|
||||
- Mark each task done as soon as you finish it; do not batch.
|
||||
- If you stop with incomplete tasks, the system will automatically prompt you to continue."
|
||||
};
|
||||
|
||||
pub(in crate::config) const DEFAULT_SPAWN_INSTRUCTIONS: &str = indoc! {"
|
||||
## Agent Spawning System
|
||||
|
||||
You have built-in tools for spawning and managing subagents. These run **in parallel** as
|
||||
background tasks inside the same process; no shell overhead, true concurrency.
|
||||
|
||||
### Available Agent Tools
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|----------|
|
||||
| `agent__spawn` | Spawn a subagent in the background. Returns an `id` immediately. |
|
||||
| `agent__check` | Non-blocking check: is the agent done yet? Returns PENDING or 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. |
|
||||
| `agent__task_create` | Create a task in the dependency-aware task queue. |
|
||||
| `agent__task_list` | List all tasks and their status/dependencies. |
|
||||
| `agent__task_complete` | Mark a task done; returns any newly unblocked tasks. Auto-dispatches agents for tasks with a designated agent. |
|
||||
| `agent__task_fail` | Mark a task as failed. Dependents remain blocked. |
|
||||
|
||||
### Core Pattern: Spawn -> Continue -> Collect
|
||||
|
||||
```
|
||||
# 1. Spawn agents in parallel
|
||||
agent__spawn --agent explore --prompt \"Find auth middleware patterns in src/\"
|
||||
agent__spawn --agent explore --prompt \"Find error handling patterns in src/\"
|
||||
# Both return IDs immediately, e.g. agent_explore_a1b2c3d4, agent_explore_e5f6g7h8
|
||||
|
||||
# 2. Continue your own work while they run (or spawn more agents)
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
### Parallel Spawning (DEFAULT for multi-agent work)
|
||||
|
||||
When a task needs multiple agents, **spawn them all at once**, then collect:
|
||||
|
||||
```
|
||||
# Spawn explore and oracle simultaneously
|
||||
agent__spawn --agent explore --prompt \"Find all database query patterns\"
|
||||
agent__spawn --agent oracle --prompt \"Evaluate pros/cons of connection pooling approaches\"
|
||||
|
||||
# Collect both results
|
||||
agent__collect --id <explore_id>
|
||||
agent__collect --id <oracle_id>
|
||||
```
|
||||
|
||||
**NEVER spawn sequentially when tasks are independent.** Parallel is always better.
|
||||
|
||||
### Task Queue (for complex dependency chains)
|
||||
|
||||
When tasks have ordering requirements, use the task queue:
|
||||
|
||||
```
|
||||
# Create tasks with dependencies (optional: auto-dispatch with --agent)
|
||||
agent__task_create --subject \"Explore existing patterns\"
|
||||
agent__task_create --subject \"Implement feature\" --blocked_by [\"task_1\"] --agent coder --prompt \"Implement based on patterns found\"
|
||||
agent__task_create --subject \"Write tests\" --blocked_by [\"task_2\"]
|
||||
|
||||
# Check what's runnable
|
||||
agent__task_list
|
||||
|
||||
# After completing a task, mark it done to unblock dependents
|
||||
# If dependents have --agent set, they auto-dispatch
|
||||
agent__task_complete --task_id task_1
|
||||
```
|
||||
|
||||
### Escalation Handling
|
||||
|
||||
Child agents may need user input but cannot prompt the user directly. When this happens,
|
||||
you will see `pending_escalations` in your tool results listing blocked children and their questions.
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|----------|
|
||||
| `agent__reply_escalation` | Unblock a child agent by answering its escalated question. |
|
||||
|
||||
When you see a pending escalation:
|
||||
1. Read the child's question and options.
|
||||
2. If you can answer from context, call `agent__reply_escalation` with your answer.
|
||||
3. If you need the user's input, call the appropriate `user__*` tool yourself, then relay the answer via `agent__reply_escalation`.
|
||||
4. **Respond promptly**; the child agent is blocked and waiting (5-minute timeout).
|
||||
"};
|
||||
|
||||
pub(in crate::config) const DEFAULT_TEAMMATE_INSTRUCTIONS: &str = indoc! {"
|
||||
## Teammate Messaging
|
||||
|
||||
You have tools to communicate with other agents running alongside you:
|
||||
- `agent__send_message --id <agent_id> --message \"...\"`: Send a message to a sibling or parent agent.
|
||||
- `agent__check_inbox`: Check for messages sent to you by other agents.
|
||||
|
||||
If you are working alongside other agents (e.g. reviewing different files, exploring different areas):
|
||||
- **Check your inbox** before finalizing your work to incorporate any cross-cutting findings from teammates.
|
||||
- **Send messages** to teammates when you discover something that affects their work.
|
||||
- Messages are delivered to the agent's inbox and read on their next `check_inbox` call."
|
||||
};
|
||||
|
||||
pub(in crate::config) const DEFAULT_USER_INTERACTION_INSTRUCTIONS: &str = indoc! {"
|
||||
## User Interaction
|
||||
|
||||
You have built-in tools to interact with the user directly:
|
||||
- `user__ask --question \"...\" --options [\"A\", \"B\", \"C\"]`: Present a selection prompt. Returns the chosen option.
|
||||
- `user__confirm --question \"...\"`: Ask a yes/no question. Returns \"yes\" or \"no\".
|
||||
- `user__input --question \"...\"`: Request free-form text input from the user.
|
||||
- `user__checkbox --question \"...\" --options [\"A\", \"B\", \"C\"]`: Multi-select prompt. Returns an array of selected options.
|
||||
|
||||
Use these tools when you need user decisions, preferences, or clarification.
|
||||
If you are running as a subagent, these questions are automatically escalated to the root agent for resolution."
|
||||
};
|
||||
+65
-1
@@ -1,4 +1,6 @@
|
||||
pub(crate) mod supervisor;
|
||||
pub(crate) mod todo;
|
||||
pub(crate) mod user_interaction;
|
||||
|
||||
use crate::{
|
||||
config::{Agent, Config, GlobalConfig},
|
||||
@@ -28,7 +30,9 @@ use std::{
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
use strum_macros::AsRefStr;
|
||||
use supervisor::SUPERVISOR_FUNCTION_PREFIX;
|
||||
use todo::TODO_FUNCTION_PREFIX;
|
||||
use user_interaction::USER_FUNCTION_PREFIX;
|
||||
|
||||
#[derive(Embed)]
|
||||
#[folder = "assets/functions/"]
|
||||
@@ -119,6 +123,31 @@ pub async fn eval_tool_calls(
|
||||
if is_all_null {
|
||||
output = vec![];
|
||||
}
|
||||
|
||||
if !output.is_empty() {
|
||||
let (has_escalations, summary) = {
|
||||
let cfg = config.read();
|
||||
if cfg.current_depth == 0 && let Some(ref queue) = cfg.root_escalation_queue && queue.has_pending() {
|
||||
(true, queue.pending_summary())
|
||||
} else {
|
||||
(false, vec![])
|
||||
}
|
||||
};
|
||||
|
||||
if has_escalations {
|
||||
let notification = json!({
|
||||
"pending_escalations": summary,
|
||||
"instruction": "Child agents are BLOCKED waiting for your reply. Call agent__reply_escalation for each pending escalation to unblock them."
|
||||
});
|
||||
let synthetic_call = ToolCall::new(
|
||||
"__escalation_notification".to_string(),
|
||||
json!({}),
|
||||
Some("escalation_check".to_string()),
|
||||
);
|
||||
output.push(ToolResult::new(synthetic_call, notification));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
@@ -269,6 +298,23 @@ impl Functions {
|
||||
self.declarations.extend(todo::todo_function_declarations());
|
||||
}
|
||||
|
||||
pub fn append_supervisor_functions(&mut self) {
|
||||
self.declarations
|
||||
.extend(supervisor::supervisor_function_declarations());
|
||||
self.declarations
|
||||
.extend(supervisor::escalation_function_declarations());
|
||||
}
|
||||
|
||||
pub fn append_teammate_functions(&mut self) {
|
||||
self.declarations
|
||||
.extend(supervisor::teammate_function_declarations());
|
||||
}
|
||||
|
||||
pub fn append_user_interaction_functions(&mut self) {
|
||||
self.declarations
|
||||
.extend(user_interaction::user_interaction_function_declarations());
|
||||
}
|
||||
|
||||
pub fn clear_mcp_meta_functions(&mut self) {
|
||||
self.declarations.retain(|d| {
|
||||
!d.name.starts_with(MCP_INVOKE_META_FUNCTION_NAME_PREFIX)
|
||||
@@ -849,7 +895,7 @@ impl ToolCall {
|
||||
|
||||
let prompt = format!("Call {cmd_name} {}", cmd_args.join(" "));
|
||||
|
||||
if *IS_STDOUT_TERMINAL {
|
||||
if *IS_STDOUT_TERMINAL && config.read().current_depth == 0 {
|
||||
println!("{}", dimmed_text(&prompt));
|
||||
}
|
||||
|
||||
@@ -886,6 +932,24 @@ impl ToolCall {
|
||||
json!({"tool_call_error": error_msg})
|
||||
})
|
||||
}
|
||||
_ if cmd_name.starts_with(SUPERVISOR_FUNCTION_PREFIX) => {
|
||||
supervisor::handle_supervisor_tool(config, &cmd_name, &json_data)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
let error_msg = format!("Supervisor tool failed: {e}");
|
||||
eprintln!("{}", warning_text(&format!("⚠️ {error_msg} ⚠️")));
|
||||
json!({"tool_call_error": error_msg})
|
||||
})
|
||||
}
|
||||
_ if cmd_name.starts_with(USER_FUNCTION_PREFIX) => {
|
||||
user_interaction::handle_user_tool(config, &cmd_name, &json_data)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
let error_msg = format!("User interaction failed: {e}");
|
||||
eprintln!("{}", warning_text(&format!("⚠️ {error_msg} ⚠️")));
|
||||
json!({"tool_call_error": error_msg})
|
||||
})
|
||||
}
|
||||
_ => match run_llm_function(cmd_name, cmd_args, envs, agent_name) {
|
||||
Ok(Some(contents)) => serde_json::from_str(&contents)
|
||||
.ok()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,272 @@
|
||||
use super::{FunctionDeclaration, JsonSchema};
|
||||
use crate::config::GlobalConfig;
|
||||
use crate::supervisor::escalation::{EscalationRequest, new_escalation_id};
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use indexmap::IndexMap;
|
||||
use inquire::{Confirm, MultiSelect, Select, Text};
|
||||
use serde_json::{Value, json};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
pub const USER_FUNCTION_PREFIX: &str = "user__";
|
||||
|
||||
const DEFAULT_ESCALATION_TIMEOUT_SECS: u64 = 300;
|
||||
|
||||
pub fn user_interaction_function_declarations() -> Vec<FunctionDeclaration> {
|
||||
vec![
|
||||
FunctionDeclaration {
|
||||
name: format!("{USER_FUNCTION_PREFIX}ask"),
|
||||
description: "Ask the user to select one option from a list. Returns the selected option. Indicate the recommended choice if there is one.".to_string(),
|
||||
parameters: JsonSchema {
|
||||
type_value: Some("object".to_string()),
|
||||
properties: Some(IndexMap::from([
|
||||
(
|
||||
"question".to_string(),
|
||||
JsonSchema {
|
||||
type_value: Some("string".to_string()),
|
||||
description: Some("The question to present to the user".into()),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
"options".to_string(),
|
||||
JsonSchema {
|
||||
type_value: Some("array".to_string()),
|
||||
description: Some("List of options for the user to choose from".into()),
|
||||
items: Some(Box::new(JsonSchema {
|
||||
type_value: Some("string".to_string()),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
])),
|
||||
required: Some(vec!["question".to_string(), "options".to_string()]),
|
||||
..Default::default()
|
||||
},
|
||||
agent: false,
|
||||
},
|
||||
FunctionDeclaration {
|
||||
name: format!("{USER_FUNCTION_PREFIX}confirm"),
|
||||
description: "Ask the user a yes/no question. Returns \"yes\" or \"no\".".to_string(),
|
||||
parameters: JsonSchema {
|
||||
type_value: Some("object".to_string()),
|
||||
properties: Some(IndexMap::from([(
|
||||
"question".to_string(),
|
||||
JsonSchema {
|
||||
type_value: Some("string".to_string()),
|
||||
description: Some("The yes/no question to ask the user".into()),
|
||||
..Default::default()
|
||||
},
|
||||
)])),
|
||||
required: Some(vec!["question".to_string()]),
|
||||
..Default::default()
|
||||
},
|
||||
agent: false,
|
||||
},
|
||||
FunctionDeclaration {
|
||||
name: format!("{USER_FUNCTION_PREFIX}input"),
|
||||
description: "Ask the user for free-form text input. Returns the text entered.".to_string(),
|
||||
parameters: JsonSchema {
|
||||
type_value: Some("object".to_string()),
|
||||
properties: Some(IndexMap::from([(
|
||||
"question".to_string(),
|
||||
JsonSchema {
|
||||
type_value: Some("string".to_string()),
|
||||
description: Some("The prompt/question to display".into()),
|
||||
..Default::default()
|
||||
},
|
||||
)])),
|
||||
required: Some(vec!["question".to_string()]),
|
||||
..Default::default()
|
||||
},
|
||||
agent: false,
|
||||
},
|
||||
FunctionDeclaration {
|
||||
name: format!("{USER_FUNCTION_PREFIX}checkbox"),
|
||||
description: "Ask the user to select one or more options from a list. Returns an array of selected options.".to_string(),
|
||||
parameters: JsonSchema {
|
||||
type_value: Some("object".to_string()),
|
||||
properties: Some(IndexMap::from([
|
||||
(
|
||||
"question".to_string(),
|
||||
JsonSchema {
|
||||
type_value: Some("string".to_string()),
|
||||
description: Some("The question to present to the user".into()),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
"options".to_string(),
|
||||
JsonSchema {
|
||||
type_value: Some("array".to_string()),
|
||||
description: Some("List of options the user can select from (multiple selections allowed)".into()),
|
||||
items: Some(Box::new(JsonSchema {
|
||||
type_value: Some("string".to_string()),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
])),
|
||||
required: Some(vec!["question".to_string(), "options".to_string()]),
|
||||
..Default::default()
|
||||
},
|
||||
agent: false,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
pub async fn handle_user_tool(
|
||||
config: &GlobalConfig,
|
||||
cmd_name: &str,
|
||||
args: &Value,
|
||||
) -> Result<Value> {
|
||||
let action = cmd_name
|
||||
.strip_prefix(USER_FUNCTION_PREFIX)
|
||||
.unwrap_or(cmd_name);
|
||||
|
||||
let depth = config.read().current_depth;
|
||||
|
||||
if depth == 0 {
|
||||
handle_direct(action, args)
|
||||
} else {
|
||||
handle_escalated(config, action, args).await
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_direct(action: &str, args: &Value) -> Result<Value> {
|
||||
match action {
|
||||
"ask" => handle_direct_ask(args),
|
||||
"confirm" => handle_direct_confirm(args),
|
||||
"input" => handle_direct_input(args),
|
||||
"checkbox" => handle_direct_checkbox(args),
|
||||
_ => Err(anyhow!("Unknown user interaction: {action}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_direct_ask(args: &Value) -> Result<Value> {
|
||||
let question = args
|
||||
.get("question")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| anyhow!("'question' is required"))?;
|
||||
let options = parse_options(args)?;
|
||||
|
||||
let answer = Select::new(question, options).prompt()?;
|
||||
|
||||
Ok(json!({ "answer": answer }))
|
||||
}
|
||||
|
||||
fn handle_direct_confirm(args: &Value) -> Result<Value> {
|
||||
let question = args
|
||||
.get("question")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| anyhow!("'question' is required"))?;
|
||||
|
||||
let answer = Confirm::new(question).with_default(true).prompt()?;
|
||||
|
||||
Ok(json!({ "answer": if answer { "yes" } else { "no" } }))
|
||||
}
|
||||
|
||||
fn handle_direct_input(args: &Value) -> Result<Value> {
|
||||
let question = args
|
||||
.get("question")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| anyhow!("'question' is required"))?;
|
||||
|
||||
let answer = Text::new(question).prompt()?;
|
||||
|
||||
Ok(json!({ "answer": answer }))
|
||||
}
|
||||
|
||||
fn handle_direct_checkbox(args: &Value) -> Result<Value> {
|
||||
let question = args
|
||||
.get("question")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| anyhow!("'question' is required"))?;
|
||||
let options = parse_options(args)?;
|
||||
|
||||
let answers = MultiSelect::new(question, options).prompt()?;
|
||||
|
||||
Ok(json!({ "answers": answers }))
|
||||
}
|
||||
|
||||
async fn handle_escalated(config: &GlobalConfig, action: &str, args: &Value) -> Result<Value> {
|
||||
let question = args
|
||||
.get("question")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| anyhow!("'question' is required"))?
|
||||
.to_string();
|
||||
|
||||
let options: Option<Vec<String>> = args.get("options").and_then(Value::as_array).map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.map(String::from)
|
||||
.collect()
|
||||
});
|
||||
|
||||
let (from_agent_id, from_agent_name, root_queue, timeout_secs) = {
|
||||
let cfg = config.read();
|
||||
let agent_id = cfg
|
||||
.self_agent_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let agent_name = cfg
|
||||
.agent
|
||||
.as_ref()
|
||||
.map(|a| a.name().to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let queue = cfg
|
||||
.root_escalation_queue
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("No escalation queue available; cannot reach parent agent"))?;
|
||||
let timeout = cfg
|
||||
.agent
|
||||
.as_ref()
|
||||
.map(|a| a.escalation_timeout())
|
||||
.unwrap_or(DEFAULT_ESCALATION_TIMEOUT_SECS);
|
||||
(agent_id, agent_name, queue, timeout)
|
||||
};
|
||||
|
||||
let escalation_id = new_escalation_id();
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
let request = EscalationRequest {
|
||||
id: escalation_id.clone(),
|
||||
from_agent_id,
|
||||
from_agent_name: from_agent_name.clone(),
|
||||
question: format!("[{action}] {question}"),
|
||||
options,
|
||||
reply_tx: tx,
|
||||
};
|
||||
|
||||
root_queue.submit(request);
|
||||
|
||||
let timeout = Duration::from_secs(timeout_secs);
|
||||
match tokio::time::timeout(timeout, rx).await {
|
||||
Ok(Ok(reply)) => Ok(json!({ "answer": reply })),
|
||||
Ok(Err(_)) => Ok(json!({
|
||||
"error": "Escalation was cancelled. The parent agent dropped the request",
|
||||
"fallback": "Make your best judgment and proceed",
|
||||
})),
|
||||
Err(_) => Ok(json!({
|
||||
"error": format!(
|
||||
"Escalation timed out after {timeout_secs} seconds waiting for user response"
|
||||
),
|
||||
"fallback": "Make your best judgment and proceed",
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_options(args: &Value) -> Result<Vec<String>> {
|
||||
args.get("options")
|
||||
.and_then(Value::as_array)
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.map(String::from)
|
||||
.collect()
|
||||
})
|
||||
.ok_or_else(|| anyhow!("'options' is required and must be an array of strings"))
|
||||
}
|
||||
@@ -9,6 +9,7 @@ mod repl;
|
||||
mod utils;
|
||||
mod mcp;
|
||||
mod parsers;
|
||||
mod supervisor;
|
||||
mod vault;
|
||||
|
||||
#[macro_use]
|
||||
|
||||
+4
-2
@@ -6,7 +6,7 @@ use bm25::{Document, Language, SearchEngine, SearchEngineBuilder};
|
||||
use futures_util::future::BoxFuture;
|
||||
use futures_util::{StreamExt, TryStreamExt, stream};
|
||||
use indoc::formatdoc;
|
||||
use rmcp::model::{CallToolRequestParam, CallToolResult};
|
||||
use rmcp::model::{CallToolRequestParams, CallToolResult};
|
||||
use rmcp::service::RunningService;
|
||||
use rmcp::transport::TokioChildProcess;
|
||||
use rmcp::{RoleClient, ServiceExt};
|
||||
@@ -395,9 +395,11 @@ impl McpRegistry {
|
||||
let tool = tool.to_owned();
|
||||
Box::pin(async move {
|
||||
let server = server?;
|
||||
let call_tool_request = CallToolRequestParam {
|
||||
let call_tool_request = CallToolRequestParams {
|
||||
name: Cow::Owned(tool.to_owned()),
|
||||
arguments: arguments.as_object().cloned(),
|
||||
meta: None,
|
||||
task: None,
|
||||
};
|
||||
|
||||
let result = server.call_tool(call_tool_request).await?;
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
use fmt::{Debug, Formatter};
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use tokio::sync::oneshot;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct EscalationRequest {
|
||||
pub id: String,
|
||||
pub from_agent_id: String,
|
||||
pub from_agent_name: String,
|
||||
pub question: String,
|
||||
pub options: Option<Vec<String>>,
|
||||
pub reply_tx: oneshot::Sender<String>,
|
||||
}
|
||||
|
||||
pub struct EscalationQueue {
|
||||
pending: parking_lot::Mutex<HashMap<String, EscalationRequest>>,
|
||||
}
|
||||
|
||||
impl EscalationQueue {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
pending: parking_lot::Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn submit(&self, request: EscalationRequest) -> String {
|
||||
let id = request.id.clone();
|
||||
self.pending.lock().insert(id.clone(), request);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn take(&self, escalation_id: &str) -> Option<EscalationRequest> {
|
||||
self.pending.lock().remove(escalation_id)
|
||||
}
|
||||
|
||||
pub fn pending_summary(&self) -> Vec<Value> {
|
||||
self.pending
|
||||
.lock()
|
||||
.values()
|
||||
.map(|r| {
|
||||
let mut entry = json!({
|
||||
"escalation_id": r.id,
|
||||
"from_agent_id": r.from_agent_id,
|
||||
"from_agent_name": r.from_agent_name,
|
||||
"question": r.question,
|
||||
});
|
||||
if let Some(ref options) = r.options {
|
||||
entry["options"] = json!(options);
|
||||
}
|
||||
entry
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn has_pending(&self) -> bool {
|
||||
!self.pending.lock().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EscalationQueue {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for EscalationQueue {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let count = self.pending.lock().len();
|
||||
f.debug_struct("EscalationQueue")
|
||||
.field("pending_count", &count)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_escalation_id() -> String {
|
||||
let short = &Uuid::new_v4().to_string()[..8];
|
||||
format!("esc_{short}")
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Envelope {
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
pub payload: EnvelopePayload,
|
||||
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum EnvelopePayload {
|
||||
Text { content: String },
|
||||
TaskCompleted { task_id: String, summary: String },
|
||||
ShutdownRequest { reason: String },
|
||||
ShutdownApproved,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Inbox {
|
||||
messages: parking_lot::Mutex<Vec<Envelope>>,
|
||||
}
|
||||
|
||||
impl Inbox {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
messages: parking_lot::Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deliver(&self, envelope: Envelope) {
|
||||
self.messages.lock().push(envelope);
|
||||
}
|
||||
|
||||
pub fn drain(&self) -> Vec<Envelope> {
|
||||
let mut msgs = {
|
||||
let mut guard = self.messages.lock();
|
||||
std::mem::take(&mut *guard)
|
||||
};
|
||||
|
||||
msgs.sort_by_key(|e| match &e.payload {
|
||||
EnvelopePayload::ShutdownRequest { .. } => 0,
|
||||
EnvelopePayload::ShutdownApproved => 0,
|
||||
EnvelopePayload::TaskCompleted { .. } => 1,
|
||||
EnvelopePayload::Text { .. } => 2,
|
||||
});
|
||||
|
||||
msgs
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Inbox {
|
||||
fn clone(&self) -> Self {
|
||||
let messages = self.messages.lock().clone();
|
||||
Self {
|
||||
messages: parking_lot::Mutex::new(messages),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
pub mod escalation;
|
||||
pub mod mailbox;
|
||||
pub mod taskqueue;
|
||||
|
||||
use crate::utils::AbortSignal;
|
||||
use fmt::{Debug, Formatter};
|
||||
use mailbox::Inbox;
|
||||
use taskqueue::TaskQueue;
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AgentExitStatus {
|
||||
Completed,
|
||||
Failed(String),
|
||||
}
|
||||
|
||||
pub struct AgentResult {
|
||||
pub id: String,
|
||||
pub agent_name: String,
|
||||
pub output: String,
|
||||
pub exit_status: AgentExitStatus,
|
||||
}
|
||||
|
||||
pub struct AgentHandle {
|
||||
pub id: String,
|
||||
pub agent_name: String,
|
||||
pub depth: usize,
|
||||
pub inbox: Arc<Inbox>,
|
||||
pub abort_signal: AbortSignal,
|
||||
pub join_handle: JoinHandle<Result<AgentResult>>,
|
||||
}
|
||||
|
||||
pub struct Supervisor {
|
||||
handles: HashMap<String, AgentHandle>,
|
||||
task_queue: TaskQueue,
|
||||
max_concurrent: usize,
|
||||
max_depth: usize,
|
||||
}
|
||||
|
||||
impl Supervisor {
|
||||
pub fn new(max_concurrent: usize, max_depth: usize) -> Self {
|
||||
Self {
|
||||
handles: HashMap::new(),
|
||||
task_queue: TaskQueue::new(),
|
||||
max_concurrent,
|
||||
max_depth,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_count(&self) -> usize {
|
||||
self.handles.len()
|
||||
}
|
||||
|
||||
pub fn max_concurrent(&self) -> usize {
|
||||
self.max_concurrent
|
||||
}
|
||||
|
||||
pub fn max_depth(&self) -> usize {
|
||||
self.max_depth
|
||||
}
|
||||
|
||||
pub fn task_queue(&self) -> &TaskQueue {
|
||||
&self.task_queue
|
||||
}
|
||||
|
||||
pub fn task_queue_mut(&mut self) -> &mut TaskQueue {
|
||||
&mut self.task_queue
|
||||
}
|
||||
|
||||
pub fn register(&mut self, handle: AgentHandle) -> Result<()> {
|
||||
if self.handles.len() >= self.max_concurrent {
|
||||
bail!(
|
||||
"Cannot spawn agent: at capacity ({}/{})",
|
||||
self.handles.len(),
|
||||
self.max_concurrent
|
||||
);
|
||||
}
|
||||
if handle.depth > self.max_depth {
|
||||
bail!(
|
||||
"Cannot spawn agent: max depth exceeded ({}/{})",
|
||||
handle.depth,
|
||||
self.max_depth
|
||||
);
|
||||
}
|
||||
self.handles.insert(handle.id.clone(), handle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_finished(&self, id: &str) -> Option<bool> {
|
||||
self.handles.get(id).map(|h| h.join_handle.is_finished())
|
||||
}
|
||||
|
||||
pub fn take(&mut self, id: &str) -> Option<AgentHandle> {
|
||||
self.handles.remove(id)
|
||||
}
|
||||
|
||||
pub fn inbox(&self, id: &str) -> Option<&Arc<Inbox>> {
|
||||
self.handles.get(id).map(|h| &h.inbox)
|
||||
}
|
||||
|
||||
pub fn list_agents(&self) -> Vec<(&str, &str)> {
|
||||
self.handles
|
||||
.values()
|
||||
.map(|h| (h.id.as_str(), h.agent_name.as_str()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn cancel_all(&self) {
|
||||
for handle in self.handles.values() {
|
||||
handle.abort_signal.set_ctrlc();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Supervisor {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Supervisor")
|
||||
.field("active_agents", &self.handles.len())
|
||||
.field("max_concurrent", &self.max_concurrent)
|
||||
.field("max_depth", &self.max_depth)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TaskStatus {
|
||||
Pending,
|
||||
Blocked,
|
||||
InProgress,
|
||||
Completed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TaskNode {
|
||||
pub id: String,
|
||||
pub subject: String,
|
||||
pub description: String,
|
||||
pub status: TaskStatus,
|
||||
pub owner: Option<String>,
|
||||
pub blocked_by: HashSet<String>,
|
||||
pub blocks: HashSet<String>,
|
||||
pub dispatch_agent: Option<String>,
|
||||
pub prompt: Option<String>,
|
||||
}
|
||||
|
||||
impl TaskNode {
|
||||
pub fn new(
|
||||
id: String,
|
||||
subject: String,
|
||||
description: String,
|
||||
dispatch_agent: Option<String>,
|
||||
prompt: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
subject,
|
||||
description,
|
||||
status: TaskStatus::Pending,
|
||||
owner: None,
|
||||
blocked_by: HashSet::new(),
|
||||
blocks: HashSet::new(),
|
||||
dispatch_agent,
|
||||
prompt,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_runnable(&self) -> bool {
|
||||
self.status == TaskStatus::Pending && self.blocked_by.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TaskQueue {
|
||||
tasks: HashMap<String, TaskNode>,
|
||||
next_id: usize,
|
||||
}
|
||||
|
||||
impl TaskQueue {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tasks: HashMap::new(),
|
||||
next_id: 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(
|
||||
&mut self,
|
||||
subject: String,
|
||||
description: String,
|
||||
dispatch_agent: Option<String>,
|
||||
prompt: Option<String>,
|
||||
) -> String {
|
||||
let id = self.next_id.to_string();
|
||||
self.next_id += 1;
|
||||
let task = TaskNode::new(id.clone(), subject, description, dispatch_agent, prompt);
|
||||
self.tasks.insert(id.clone(), task);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn add_dependency(&mut self, task_id: &str, blocked_by: &str) -> Result<(), String> {
|
||||
if task_id == blocked_by {
|
||||
return Err("A task cannot depend on itself".into());
|
||||
}
|
||||
if !self.tasks.contains_key(blocked_by) {
|
||||
return Err(format!("Dependency task '{blocked_by}' does not exist"));
|
||||
}
|
||||
if !self.tasks.contains_key(task_id) {
|
||||
return Err(format!("Task '{task_id}' does not exist"));
|
||||
}
|
||||
|
||||
if self.would_create_cycle(task_id, blocked_by) {
|
||||
return Err(format!(
|
||||
"Adding dependency {task_id} -> {blocked_by} would create a cycle"
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(task) = self.tasks.get_mut(task_id) {
|
||||
task.blocked_by.insert(blocked_by.to_string());
|
||||
task.status = TaskStatus::Blocked;
|
||||
}
|
||||
if let Some(blocker) = self.tasks.get_mut(blocked_by) {
|
||||
blocker.blocks.insert(task_id.to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn complete(&mut self, task_id: &str) -> Vec<String> {
|
||||
let mut newly_runnable = Vec::new();
|
||||
|
||||
let dependents: Vec<String> = self
|
||||
.tasks
|
||||
.get(task_id)
|
||||
.map(|t| t.blocks.iter().cloned().collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(task) = self.tasks.get_mut(task_id) {
|
||||
task.status = TaskStatus::Completed;
|
||||
}
|
||||
|
||||
for dep_id in &dependents {
|
||||
if let Some(dep) = self.tasks.get_mut(dep_id) {
|
||||
dep.blocked_by.remove(task_id);
|
||||
if dep.blocked_by.is_empty() && dep.status == TaskStatus::Blocked {
|
||||
dep.status = TaskStatus::Pending;
|
||||
newly_runnable.push(dep_id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newly_runnable
|
||||
}
|
||||
|
||||
pub fn fail(&mut self, task_id: &str) {
|
||||
if let Some(task) = self.tasks.get_mut(task_id) {
|
||||
task.status = TaskStatus::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn claim(&mut self, task_id: &str, owner: &str) -> bool {
|
||||
if let Some(task) = self.tasks.get_mut(task_id)
|
||||
&& task.is_runnable()
|
||||
&& task.owner.is_none()
|
||||
{
|
||||
task.owner = Some(owner.to_string());
|
||||
task.status = TaskStatus::InProgress;
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn get(&self, task_id: &str) -> Option<&TaskNode> {
|
||||
self.tasks.get(task_id)
|
||||
}
|
||||
|
||||
pub fn list(&self) -> Vec<&TaskNode> {
|
||||
let mut tasks: Vec<&TaskNode> = self.tasks.values().collect();
|
||||
tasks.sort_by_key(|t| t.id.parse::<usize>().unwrap_or(0));
|
||||
tasks
|
||||
}
|
||||
|
||||
fn would_create_cycle(&self, task_id: &str, blocked_by: &str) -> bool {
|
||||
let mut visited = HashSet::new();
|
||||
let mut stack = vec![blocked_by.to_string()];
|
||||
|
||||
while let Some(current) = stack.pop() {
|
||||
if current == task_id {
|
||||
return true;
|
||||
}
|
||||
if visited.insert(current.clone())
|
||||
&& let Some(task) = self.tasks.get(¤t)
|
||||
{
|
||||
for dep in &task.blocked_by {
|
||||
stack.push(dep.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_and_list() {
|
||||
let mut queue = TaskQueue::new();
|
||||
let id1 = queue.create(
|
||||
"Research".into(),
|
||||
"Research auth patterns".into(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let id2 = queue.create("Implement".into(), "Write the code".into(), None, None);
|
||||
|
||||
assert_eq!(id1, "1");
|
||||
assert_eq!(id2, "2");
|
||||
assert_eq!(queue.list().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dependency_and_completion() {
|
||||
let mut queue = TaskQueue::new();
|
||||
let id1 = queue.create("Step 1".into(), "".into(), None, None);
|
||||
let id2 = queue.create("Step 2".into(), "".into(), None, None);
|
||||
|
||||
queue.add_dependency(&id2, &id1).unwrap();
|
||||
|
||||
assert!(queue.get(&id1).unwrap().is_runnable());
|
||||
assert!(!queue.get(&id2).unwrap().is_runnable());
|
||||
assert_eq!(queue.get(&id2).unwrap().status, TaskStatus::Blocked);
|
||||
|
||||
let unblocked = queue.complete(&id1);
|
||||
assert_eq!(unblocked, vec![id2.clone()]);
|
||||
assert!(queue.get(&id2).unwrap().is_runnable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fan_in_dependency() {
|
||||
let mut queue = TaskQueue::new();
|
||||
let id1 = queue.create("A".into(), "".into(), None, None);
|
||||
let id2 = queue.create("B".into(), "".into(), None, None);
|
||||
let id3 = queue.create("C (needs A and B)".into(), "".into(), None, None);
|
||||
|
||||
queue.add_dependency(&id3, &id1).unwrap();
|
||||
queue.add_dependency(&id3, &id2).unwrap();
|
||||
|
||||
assert!(!queue.get(&id3).unwrap().is_runnable());
|
||||
|
||||
let unblocked = queue.complete(&id1);
|
||||
assert!(unblocked.is_empty());
|
||||
assert!(!queue.get(&id3).unwrap().is_runnable());
|
||||
|
||||
let unblocked = queue.complete(&id2);
|
||||
assert_eq!(unblocked, vec![id3.clone()]);
|
||||
assert!(queue.get(&id3).unwrap().is_runnable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cycle_detection() {
|
||||
let mut queue = TaskQueue::new();
|
||||
let id1 = queue.create("A".into(), "".into(), None, None);
|
||||
let id2 = queue.create("B".into(), "".into(), None, None);
|
||||
|
||||
queue.add_dependency(&id2, &id1).unwrap();
|
||||
let result = queue.add_dependency(&id1, &id2);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("cycle"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_self_dependency_rejected() {
|
||||
let mut queue = TaskQueue::new();
|
||||
let id1 = queue.create("A".into(), "".into(), None, None);
|
||||
let result = queue.add_dependency(&id1, &id1);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_claim() {
|
||||
let mut queue = TaskQueue::new();
|
||||
let id1 = queue.create("Task".into(), "".into(), None, None);
|
||||
|
||||
assert!(queue.claim(&id1, "worker-1"));
|
||||
assert!(!queue.claim(&id1, "worker-2"));
|
||||
assert_eq!(queue.get(&id1).unwrap().status, TaskStatus::InProgress);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user