Remove RLM/Duo modes and restore footer scroll
This commit is contained in:
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.3.9] - 2026-02-04
|
||||
|
||||
### Removed
|
||||
- RLM mode, tools, and documentation pending a faithful implementation of the MIT RLM design
|
||||
- Duo mode tools and prompts pending a citable research spec
|
||||
|
||||
### Fixed
|
||||
- Footer context usage bar remains visible while status toasts are shown
|
||||
|
||||
### Changed
|
||||
- Updated prompts and docs to reflect the simplified mode/tool surface
|
||||
|
||||
## [0.3.8] - 2026-02-03
|
||||
|
||||
### Fixed
|
||||
- Resolve clippy warnings (CI `-D warnings`) in new tool implementations
|
||||
|
||||
## [0.3.7] - 2026-02-03
|
||||
|
||||
### Added
|
||||
|
||||
Generated
+1
-1
@@ -674,7 +674,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-tui"
|
||||
version = "0.3.7"
|
||||
version = "0.3.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deepseek-tui"
|
||||
version = "0.3.7"
|
||||
version = "0.3.9"
|
||||
edition = "2024"
|
||||
description = "Unofficial DeepSeek CLI - Just run 'deepseek' to start chatting"
|
||||
license = "MIT"
|
||||
|
||||
@@ -11,7 +11,7 @@ Unofficial terminal UI (TUI) + CLI for the [DeepSeek platform](https://platform.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **Interactive TUI** with multiple modes (Normal, Plan, Agent, YOLO, RLM, Duo)
|
||||
- **Interactive TUI** with multiple modes (Normal, Plan, Agent, YOLO)
|
||||
- **Comprehensive tool access** – File operations, shell execution, task management, and sub-agent systems
|
||||
- **File operations**: List directories, read/write/edit files, apply patches, search files with regex
|
||||
- **Shell execution**: Run commands with timeout support, background execution with task management
|
||||
@@ -100,7 +100,7 @@ See `config.example.toml` and `docs/CONFIGURATION.md` for a full reference.
|
||||
|
||||
## 🎮 Modes
|
||||
|
||||
In the TUI, press `Tab` to cycle modes: **Normal → Plan → Agent → YOLO → RLM → Duo → Normal**.
|
||||
In the TUI, press `Tab` to cycle modes: **Normal → Plan → Agent → YOLO → Normal**.
|
||||
|
||||
| Mode | Description | Approval Behavior |
|
||||
|------|-------------|-------------------|
|
||||
@@ -108,9 +108,6 @@ In the TUI, press `Tab` to cycle modes: **Normal → Plan → Agent → YOLO →
|
||||
| **Plan** | Design‑first prompting; same approvals as Normal | Manual approval for writes & shell |
|
||||
| **Agent** | Multi‑step tool use; asks before shell | Manual approval for shell, auto‑approve file writes |
|
||||
| **YOLO** | Enables shell + trust + auto‑approves all tools (dangerous) | Auto‑approve all tools |
|
||||
| **RLM** | Externalized context + REPL helpers; auto‑approves tools (best for large files) | Auto‑approve tools |
|
||||
| **Duo** | Player‑coach autocoding with iterative validation (based on g3 paper) | Depends on phase |
|
||||
|
||||
Approval behavior is mode‑dependent, but you can also override it at runtime with `/set approval_mode auto|suggest|never`.
|
||||
|
||||
## 🛠️ Tools
|
||||
@@ -163,30 +160,6 @@ DeepSeek CLI exposes a comprehensive set of tools to the model across 5 categori
|
||||
- **Skills**: Reusable workflows stored as `SKILL.md` directories. The resolved skills dir prefers workspace-local `.agents/skills`, then `./skills`, then `~/.deepseek/skills`. Use `/skills` and `/skill <name>`. Bootstrap with `deepseek setup --skills` (add `--local` for `./skills`).
|
||||
- **MCP**: Load external tool servers via `~/.deepseek/mcp.json` (supports `servers` and `mcpServers`). MCP tools currently execute without TUI approval prompts, so only enable servers you trust. See `docs/MCP.md`.
|
||||
|
||||
## 🧠 RLM (Reasoning & Large‑scale Memory)
|
||||
|
||||
RLM mode is designed for "too big for context" tasks: large files, whole‑doc sweeps, and big pasted blocks.
|
||||
|
||||
- Auto‑switch triggers: "largest file", explicit "RLM", large file requests, and large pastes.
|
||||
- Shortcut: `/rlm` (or `/aleph`) enters RLM mode directly.
|
||||
- In **RLM mode**, `/load @path` loads a file into the external context store (outside RLM mode, `/load` loads a saved chat JSON).
|
||||
- Use `/repl` to enter expression mode (e.g. `search("pattern")`, `lines(1, 80)`).
|
||||
- Power tools: `rlm_load`, `rlm_exec`, `rlm_status`, `rlm_query`.
|
||||
|
||||
`rlm_query` can be expensive: prefer batching and check `/status` if you're doing lots of sub‑queries.
|
||||
|
||||
## 👥 Duo Mode
|
||||
|
||||
> **Note:** Duo mode is experimental and may not work correctly in all cases. Use with caution.
|
||||
|
||||
Duo mode implements the player‑coach autocoding paradigm for iterative development with built‑in validation:
|
||||
|
||||
- **Player**: implements requirements (builder role)
|
||||
- **Coach**: validates implementation against requirements (critic role)
|
||||
- Tools: `duo_init`, `duo_player`, `duo_coach`, `duo_advance`, `duo_status`
|
||||
|
||||
Workflow: `init → player → coach → advance → (repeat until approved)`
|
||||
|
||||
## 📚 Examples
|
||||
|
||||
### Interactive chat
|
||||
@@ -278,7 +251,6 @@ Run `deepseek doctor` to confirm sandbox availability. On macOS, ensure
|
||||
- `docs/CONFIGURATION.md` – Complete configuration reference
|
||||
- `docs/MCP.md` – Model Context Protocol guide
|
||||
- `docs/ARCHITECTURE.md` – Project architecture
|
||||
- `docs/RLM.md` – RLM mode deep‑dive
|
||||
- `docs/MODES.md` – Mode comparison and usage
|
||||
- `CONTRIBUTING.md` – How to contribute to the project
|
||||
|
||||
|
||||
@@ -54,8 +54,6 @@ subagents = true
|
||||
web_search = true # enables web.run and web_search
|
||||
apply_patch = true
|
||||
mcp = true
|
||||
rlm = true
|
||||
duo = true
|
||||
exec_policy = true
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
@@ -78,16 +76,6 @@ exponential_base = 2.0
|
||||
# model = "deepseek-chat" # Model to use for summarization
|
||||
# cache_summary = true # Cache the summary block
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
# RLM Sandbox Configuration (PLANNED - not yet implemented)
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
# [rlm]
|
||||
# max_context_chars = 10000000 # Max characters for context (10MB)
|
||||
# max_search_results = 100 # Max search results
|
||||
# default_chunk_size = 2000 # Default chunk size
|
||||
# default_overlap = 200 # Default chunk overlap
|
||||
# session_dir = "~/.deepseek/rlm" # Directory for RLM sessions
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
# Profile Example (for multiple environments)
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -121,7 +121,6 @@ Responses API (with automatic fallback if needed).
|
||||
- **`utils.rs`** - Common utilities
|
||||
- **`logging.rs`** - Logging infrastructure
|
||||
- **`compaction.rs`** - Context compaction for long conversations
|
||||
- **`rlm.rs`** - Reflection/reasoning utilities
|
||||
- **`pricing.rs`** - Cost estimation
|
||||
- **`prompts.rs`** - System prompt templates
|
||||
- **`project_doc.rs`** - Project documentation handling
|
||||
|
||||
@@ -67,7 +67,7 @@ Common settings keys:
|
||||
- `auto_compact` (on/off)
|
||||
- `show_thinking` (on/off)
|
||||
- `show_tool_details` (on/off)
|
||||
- `default_mode` (normal, agent, plan, yolo, rlm, duo)
|
||||
- `default_mode` (normal, agent, plan, yolo)
|
||||
- `max_history` (number of input history entries)
|
||||
- `default_model` (model name override)
|
||||
|
||||
@@ -113,8 +113,6 @@ subagents = true
|
||||
web_search = true # enables web.run and web_search
|
||||
apply_patch = true
|
||||
mcp = true
|
||||
rlm = true
|
||||
duo = true
|
||||
exec_policy = true
|
||||
```
|
||||
|
||||
|
||||
+3
-4
@@ -2,18 +2,17 @@
|
||||
|
||||
DeepSeek CLI has two related concepts:
|
||||
|
||||
- **TUI mode**: what kind of interaction you’re in (Normal/Plan/Agent/YOLO/RLM).
|
||||
- **TUI mode**: what kind of interaction you’re in (Normal/Plan/Agent/YOLO).
|
||||
- **Approval mode**: how aggressively the UI asks before executing tools.
|
||||
|
||||
## TUI Modes
|
||||
|
||||
Press `Tab` to cycle: **Normal → Plan → Agent → YOLO → RLM → Normal**.
|
||||
Press `Tab` to cycle: **Normal → Plan → Agent → YOLO → Normal**.
|
||||
|
||||
- **Normal**: chat-first. Approvals for file writes, shell, and paid tools.
|
||||
- **Plan**: design-first prompting. Approvals match Normal.
|
||||
- **Agent**: multi-step tool use. Approvals for shell and paid tools (file writes are allowed without a prompt).
|
||||
- **YOLO**: enables shell + trust mode and auto-approves all tools. Use only in trusted repos.
|
||||
- **RLM**: externalized context store + REPL helpers. Tools are auto-approved (best for large files and long-context work).
|
||||
|
||||
## Approval Mode
|
||||
|
||||
@@ -26,7 +25,7 @@ You can override approval behavior at runtime:
|
||||
```
|
||||
|
||||
- `suggest` (default): uses the per-mode rules above.
|
||||
- `auto`: auto-approves all tools (similar to YOLO/RLM approval behavior, but without forcing YOLO mode).
|
||||
- `auto`: auto-approves all tools (similar to YOLO approval behavior, but without forcing YOLO mode).
|
||||
- `never`: blocks any tool that isn’t considered safe/read-only.
|
||||
|
||||
## Workspace Boundary and Trust Mode
|
||||
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
# RLM Mode
|
||||
|
||||
RLM mode (“Recursive Language Model” mode) is DeepSeek CLI’s long-context workflow: it stores large context externally (Aleph-style external memory) and provides REPL-like tools to explore and query it without stuffing everything into the model’s context window.
|
||||
|
||||
If you’re curious about the research inspiration and implementation notes, see:
|
||||
|
||||
- `docs/rlm-paper.txt`
|
||||
- `docs/rlm_gap_analysis.md`
|
||||
|
||||
## When To Use It
|
||||
|
||||
RLM mode is best for:
|
||||
|
||||
- “Analyze this large file / doc”
|
||||
- “Summarize the whole repository”
|
||||
- “Search for every occurrence of X and explain it”
|
||||
- Big pasted blocks of text
|
||||
|
||||
The UI may auto-switch to RLM for large file requests, “largest file”, explicit “RLM” requests, and large pastes.
|
||||
|
||||
## How To Use It
|
||||
|
||||
### Switch modes
|
||||
|
||||
- Press `Tab` until you reach **RLM**
|
||||
- Or type `/rlm` (or `/aleph`) to jump directly into RLM mode
|
||||
|
||||
### Load context
|
||||
|
||||
In RLM mode, `/load` loads external context (in other modes, `/load` loads a saved chat JSON):
|
||||
|
||||
```text
|
||||
/load @path/to/file.rs
|
||||
```
|
||||
|
||||
`@path` is workspace-relative.
|
||||
|
||||
### Inspect and query
|
||||
|
||||
- `/status` shows which contexts are loaded and basic usage totals.
|
||||
- `/repl` toggles expression input mode.
|
||||
|
||||
Typical REPL helpers include:
|
||||
|
||||
- `lines(1, 80)` (show a slice of the context)
|
||||
- `search("pattern")`
|
||||
- `chunk(2000)` (create fixed-size chunks for later querying)
|
||||
|
||||
Under the hood, the model uses tools like `rlm_load`, `rlm_exec`, `rlm_status`, and `rlm_query`.
|
||||
|
||||
## Cost and Safety Notes
|
||||
|
||||
- `rlm_query` can be expensive because it triggers additional model calls. Prefer batching related questions.
|
||||
- RLM mode auto-approves tools; keep `--workspace` scoped to the repo you want it to access.
|
||||
@@ -125,9 +125,6 @@ pub fn set_config(app: &mut App, args: Option<&str>) -> CommandResult {
|
||||
compaction.model = app.model.clone();
|
||||
action = Some(AppAction::UpdateCompaction(compaction));
|
||||
}
|
||||
"auto_rlm" => {
|
||||
app.auto_rlm = settings.auto_rlm;
|
||||
}
|
||||
"show_thinking" | "thinking" => {
|
||||
app.show_thinking = settings.show_thinking;
|
||||
app.mark_history_updated();
|
||||
@@ -141,8 +138,6 @@ pub fn set_config(app: &mut App, args: Option<&str>) -> CommandResult {
|
||||
"agent" | "normal" => AppMode::Agent,
|
||||
"plan" => AppMode::Plan,
|
||||
"yolo" => AppMode::Yolo,
|
||||
"rlm" => AppMode::Rlm,
|
||||
"duo" => AppMode::Duo,
|
||||
_ => AppMode::Agent,
|
||||
};
|
||||
app.set_mode(mode);
|
||||
|
||||
@@ -160,14 +160,6 @@ pub fn home_dashboard(app: &mut App) -> CommandResult {
|
||||
let _ = writeln!(stats, "Plan mode - Design before implementing");
|
||||
let _ = writeln!(stats, " Use /plan to create structured checklists");
|
||||
}
|
||||
AppMode::Rlm => {
|
||||
let _ = writeln!(stats, "RLM mode - Recursive language model sandbox");
|
||||
let _ = writeln!(stats, " Use /repl to toggle REPL input");
|
||||
}
|
||||
AppMode::Duo => {
|
||||
let _ = writeln!(stats, "Duo mode - Dialectical autocoding");
|
||||
let _ = writeln!(stats, " Player-coach loop for complex tasks");
|
||||
}
|
||||
}
|
||||
|
||||
CommandResult::message(stats)
|
||||
|
||||
+4
-93
@@ -10,11 +10,10 @@ mod init;
|
||||
mod note;
|
||||
mod queue;
|
||||
mod review;
|
||||
pub mod rlm;
|
||||
mod session;
|
||||
mod skills;
|
||||
|
||||
use crate::tui::app::{App, AppAction, AppMode};
|
||||
use crate::tui::app::{App, AppAction};
|
||||
|
||||
/// Result of executing a command
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -150,39 +149,9 @@ pub const COMMANDS: &[CommandInfo] = &[
|
||||
CommandInfo {
|
||||
name: "load",
|
||||
aliases: &[],
|
||||
description: "Load session from file (or RLM context in RLM mode)",
|
||||
description: "Load session from file",
|
||||
usage: "/load [path]",
|
||||
},
|
||||
CommandInfo {
|
||||
name: "rlm",
|
||||
aliases: &[],
|
||||
description: "Enter RLM (Aleph) mode and show quickstart",
|
||||
usage: "/rlm",
|
||||
},
|
||||
CommandInfo {
|
||||
name: "aleph",
|
||||
aliases: &[],
|
||||
description: "Alias for /rlm (external memory quickstart)",
|
||||
usage: "/aleph",
|
||||
},
|
||||
CommandInfo {
|
||||
name: "save-session",
|
||||
aliases: &["save_session"],
|
||||
description: "Save RLM session to file",
|
||||
usage: "/save-session [path]",
|
||||
},
|
||||
CommandInfo {
|
||||
name: "status",
|
||||
aliases: &[],
|
||||
description: "Show RLM context status",
|
||||
usage: "/status",
|
||||
},
|
||||
CommandInfo {
|
||||
name: "repl",
|
||||
aliases: &[],
|
||||
description: "Toggle RLM REPL mode",
|
||||
usage: "/repl",
|
||||
},
|
||||
CommandInfo {
|
||||
name: "compact",
|
||||
aliases: &[],
|
||||
@@ -320,18 +289,7 @@ pub fn execute(cmd: &str, app: &mut App) -> CommandResult {
|
||||
// Session commands
|
||||
"save" => session::save(app, arg),
|
||||
"sessions" | "resume" => session::sessions(app),
|
||||
"load" => {
|
||||
let force_rlm = arg.is_some_and(|raw| raw.trim_start().starts_with('@'));
|
||||
if app.mode == AppMode::Rlm || force_rlm {
|
||||
rlm::load(app, arg)
|
||||
} else {
|
||||
session::load(app, arg)
|
||||
}
|
||||
}
|
||||
"rlm" | "aleph" => rlm::enter(app),
|
||||
"save-session" | "save_session" => rlm::save_session(app, arg),
|
||||
"status" => rlm::status(app),
|
||||
"repl" => rlm::repl(app),
|
||||
"load" => session::load(app, arg),
|
||||
"compact" => session::compact(app),
|
||||
"export" => session::export(app, arg),
|
||||
|
||||
@@ -387,52 +345,5 @@ pub fn commands_matching(prefix: &str) -> Vec<&'static CommandInfo> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::execute;
|
||||
use crate::config::Config;
|
||||
use crate::tui::app::{App, AppMode, TuiOptions};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn make_test_app(workspace: PathBuf) -> App {
|
||||
let skills_dir = workspace.join("skills");
|
||||
let _ = fs::create_dir_all(&skills_dir);
|
||||
let options = TuiOptions {
|
||||
model: "test-model".to_string(),
|
||||
workspace,
|
||||
allow_shell: false,
|
||||
use_alt_screen: false,
|
||||
max_subagents: 1,
|
||||
skills_dir,
|
||||
memory_path: PathBuf::from("memory.md"),
|
||||
notes_path: PathBuf::from("notes.txt"),
|
||||
mcp_config_path: PathBuf::from("mcp.json"),
|
||||
use_memory: false,
|
||||
start_in_agent_mode: false,
|
||||
skip_onboarding: true,
|
||||
yolo: false,
|
||||
resume_session_id: None,
|
||||
};
|
||||
App::new(options, &Config::default())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_at_path_uses_rlm_outside_rlm_mode() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let file = tmp.path().join("example.txt");
|
||||
fs::write(&file, "hello").expect("write");
|
||||
|
||||
let mut app = make_test_app(tmp.path().to_path_buf());
|
||||
app.mode = AppMode::Normal;
|
||||
|
||||
let result = execute("/load @example.txt", &mut app);
|
||||
let message = result.message.unwrap_or_default();
|
||||
assert!(
|
||||
message.starts_with("Loaded "),
|
||||
"expected RLM load message, got: {message}"
|
||||
);
|
||||
|
||||
let session = app.rlm_session.lock().expect("lock session");
|
||||
assert!(!session.contexts.is_empty());
|
||||
}
|
||||
// No unit tests currently required for command routing.
|
||||
}
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
//! RLM commands for the TUI (load/status/repl/save-session).
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::rlm::{context_id_from_path, unique_context_id};
|
||||
use crate::tui::app::{App, AppMode};
|
||||
|
||||
use super::CommandResult;
|
||||
|
||||
const DEFAULT_CHUNK_SIZE: usize = 2000;
|
||||
const DEFAULT_CHUNK_OVERLAP: usize = 200;
|
||||
|
||||
pub fn welcome_message() -> String {
|
||||
[
|
||||
"DeepSeek RLM / Aleph Sandbox",
|
||||
"Commands: /rlm, /aleph, /load <file>, /repl, /status, /save-session",
|
||||
"Press Tab to exit RLM mode",
|
||||
"Use /repl to toggle expression mode (chat is the default)",
|
||||
"Tip: /load @path forces workspace-relative paths (e.g. @docs/rlm-paper.txt)",
|
||||
"",
|
||||
"Expressions:",
|
||||
" len(ctx)",
|
||||
" search(\"pattern\")",
|
||||
" lines(1, 20)",
|
||||
" chunk(2000, 200)",
|
||||
" chunk_sections(20000)",
|
||||
" chunk_auto(20000)",
|
||||
" vars(), get(\"name\"), set(\"name\", \"value\")",
|
||||
"",
|
||||
"Tip: rlm_query auto_chunks runs the same question over chunk_auto slices.",
|
||||
"Tip: /save-session <path> persists the current RLM session.",
|
||||
]
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
pub fn overview_message() -> String {
|
||||
[
|
||||
"RLM / Aleph Quickstart",
|
||||
"Use /rlm or /aleph to enter external-memory mode.",
|
||||
"Use /load @path to load a file into the RLM context store.",
|
||||
"Use /status to list contexts and usage totals.",
|
||||
"Use /repl to toggle expression mode (chat is default).",
|
||||
"Tip: rlm_query auto_chunks runs the same question over chunk_auto slices.",
|
||||
]
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
pub fn enter(app: &mut App) -> CommandResult {
|
||||
if app.mode != AppMode::Rlm {
|
||||
app.set_mode(AppMode::Rlm);
|
||||
}
|
||||
app.rlm_repl_active = false;
|
||||
CommandResult::message(overview_message())
|
||||
}
|
||||
|
||||
pub fn repl(app: &mut App) -> CommandResult {
|
||||
if app.mode != AppMode::Rlm {
|
||||
app.set_mode(AppMode::Rlm);
|
||||
}
|
||||
if app.rlm_repl_active {
|
||||
app.rlm_repl_active = false;
|
||||
return CommandResult::message("Exited RLM REPL mode. Chat is active.");
|
||||
}
|
||||
app.rlm_repl_active = true;
|
||||
CommandResult::message(welcome_message())
|
||||
}
|
||||
|
||||
pub fn status(app: &mut App) -> CommandResult {
|
||||
let session = match app.rlm_session.lock() {
|
||||
Ok(session) => session,
|
||||
Err(_) => return CommandResult::error("Failed to access RLM session"),
|
||||
};
|
||||
|
||||
if session.contexts.is_empty() {
|
||||
return CommandResult::message("No RLM contexts loaded. Use /load <path>.");
|
||||
}
|
||||
|
||||
let mut lines = Vec::new();
|
||||
lines.push("RLM Session".to_string());
|
||||
lines.push(format!("Active context: {}", session.active_context));
|
||||
lines.push(format!("Loaded contexts: {}", session.contexts.len()));
|
||||
lines.push(format!(
|
||||
"Queries: {} | Input tokens: {} | Output tokens: {}",
|
||||
session.usage.queries, session.usage.input_tokens, session.usage.output_tokens
|
||||
));
|
||||
|
||||
let mut ids: Vec<_> = session.contexts.keys().collect();
|
||||
ids.sort();
|
||||
for id in ids {
|
||||
if let Some(ctx) = session.contexts.get(id) {
|
||||
let source = ctx
|
||||
.source_path
|
||||
.as_ref()
|
||||
.map(|s| format!(" (source: {s})"))
|
||||
.unwrap_or_default();
|
||||
let chunk_count = ctx.chunk(DEFAULT_CHUNK_SIZE, DEFAULT_CHUNK_OVERLAP).len();
|
||||
let section_count = ctx.chunk_sections(20_000).len();
|
||||
lines.push(format!(
|
||||
"- {id}: {} lines, {} chars, {} chunks, {} sections{source}",
|
||||
ctx.line_count, ctx.char_count, chunk_count, section_count
|
||||
));
|
||||
if !ctx.variables.is_empty() {
|
||||
lines.push(format!(" variables: {}", ctx.variables.len()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CommandResult::message(lines.join("\n"))
|
||||
}
|
||||
|
||||
pub fn load(app: &mut App, path: Option<&str>) -> CommandResult {
|
||||
let Some(raw) = path else {
|
||||
return CommandResult::error("Usage: /load <path>");
|
||||
};
|
||||
|
||||
let resolved = match resolve_path(app, raw) {
|
||||
Ok(path) => path,
|
||||
Err(err) => return CommandResult::error(err),
|
||||
};
|
||||
|
||||
let mut session = match app.rlm_session.lock() {
|
||||
Ok(session) => session,
|
||||
Err(_) => return CommandResult::error("Failed to access RLM session"),
|
||||
};
|
||||
|
||||
let base_id = context_id_from_path(&resolved);
|
||||
let id = unique_context_id(&session, &base_id);
|
||||
let (line_count, char_count) = match session.load_file(&id, &resolved) {
|
||||
Ok(stats) => stats,
|
||||
Err(err) => {
|
||||
return CommandResult::error(format!("Failed to load {}: {err}", resolved.display()));
|
||||
}
|
||||
};
|
||||
|
||||
CommandResult::message(format!(
|
||||
"Loaded {} ({} lines, {} chars)",
|
||||
resolved.display(),
|
||||
line_count,
|
||||
char_count
|
||||
))
|
||||
}
|
||||
|
||||
pub fn save_session(app: &mut App, path: Option<&str>) -> CommandResult {
|
||||
let save_path = if let Some(p) = path {
|
||||
PathBuf::from(p)
|
||||
} else {
|
||||
let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
|
||||
PathBuf::from(format!("rlm_session_{timestamp}.json"))
|
||||
};
|
||||
|
||||
let parent_dir = save_path
|
||||
.parent()
|
||||
.filter(|p| !p.as_os_str().is_empty())
|
||||
.map(std::path::Path::to_path_buf);
|
||||
if let Some(dir) = parent_dir
|
||||
&& let Err(err) = fs::create_dir_all(&dir)
|
||||
{
|
||||
return CommandResult::error(format!(
|
||||
"Failed to create directory {}: {err}",
|
||||
dir.display()
|
||||
));
|
||||
}
|
||||
|
||||
let session = match app.rlm_session.lock() {
|
||||
Ok(session) => session,
|
||||
Err(_) => return CommandResult::error("Failed to access RLM session"),
|
||||
};
|
||||
let json = match serde_json::to_string_pretty(&*session) {
|
||||
Ok(json) => json,
|
||||
Err(err) => return CommandResult::error(format!("Failed to serialize session: {err}")),
|
||||
};
|
||||
|
||||
match fs::write(&save_path, json) {
|
||||
Ok(()) => CommandResult::message(format!("RLM session saved to {}", save_path.display())),
|
||||
Err(err) => CommandResult::error(format!("Failed to save session: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_path(app: &App, raw: &str) -> Result<PathBuf, String> {
|
||||
let raw = raw.trim();
|
||||
let (raw, force_workspace) = if let Some(stripped) = raw.strip_prefix('@') {
|
||||
(stripped.trim(), true)
|
||||
} else {
|
||||
(raw, false)
|
||||
};
|
||||
if raw.is_empty() {
|
||||
return Err("Usage: /load <path> (use @ for workspace-relative paths)".to_string());
|
||||
}
|
||||
|
||||
let candidate = if force_workspace {
|
||||
app.workspace.join(raw.trim_start_matches(['/', '\\']))
|
||||
} else if Path::new(raw).is_absolute() {
|
||||
PathBuf::from(raw)
|
||||
} else {
|
||||
app.workspace.join(raw)
|
||||
};
|
||||
let canonical = candidate.canonicalize().map_err(|err| {
|
||||
let mut message = format!("Failed to resolve path {}: {err}", candidate.display());
|
||||
if !force_workspace {
|
||||
message.push_str("\nTip: use /load @path to resolve relative to the workspace.");
|
||||
}
|
||||
message
|
||||
})?;
|
||||
let workspace_root = app
|
||||
.workspace
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| app.workspace.clone());
|
||||
if !app.trust_mode && !canonical.starts_with(&workspace_root) {
|
||||
return Err("Path is outside workspace. Use /trust to allow access.".to_string());
|
||||
}
|
||||
Ok(canonical)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::tui::app::{App, TuiOptions};
|
||||
use std::fs;
|
||||
|
||||
fn make_app(workspace: PathBuf) -> App {
|
||||
let options = TuiOptions {
|
||||
model: "test-model".to_string(),
|
||||
workspace,
|
||||
allow_shell: false,
|
||||
use_alt_screen: true,
|
||||
max_subagents: 1,
|
||||
skills_dir: PathBuf::from("."),
|
||||
memory_path: PathBuf::from("memory.md"),
|
||||
notes_path: PathBuf::from("notes.txt"),
|
||||
mcp_config_path: PathBuf::from("mcp.json"),
|
||||
use_memory: false,
|
||||
start_in_agent_mode: false,
|
||||
skip_onboarding: false,
|
||||
yolo: false,
|
||||
resume_session_id: None,
|
||||
};
|
||||
App::new(options, &Config::default())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_with_at_prefix_uses_workspace_root() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let docs_dir = tmp.path().join("docs");
|
||||
fs::create_dir_all(&docs_dir).expect("create docs dir");
|
||||
let file_path = docs_dir.join("rlm-paper.txt");
|
||||
fs::write(&file_path, "hello").expect("write file");
|
||||
|
||||
let app = make_app(tmp.path().to_path_buf());
|
||||
let resolved = resolve_path(&app, "@/docs/rlm-paper.txt").expect("resolve path with @");
|
||||
assert_eq!(resolved, file_path.canonicalize().expect("canonicalize"));
|
||||
}
|
||||
}
|
||||
@@ -157,8 +157,6 @@ pub fn export(app: &mut App, path: Option<&str>) -> CommandResult {
|
||||
let (role, body) = match cell {
|
||||
HistoryCell::User { content } => ("**You:**", content.clone()),
|
||||
HistoryCell::Assistant { content, .. } => ("**Assistant:**", content.clone()),
|
||||
HistoryCell::Player { content, .. } => ("**Player:**", content.clone()),
|
||||
HistoryCell::Coach { content, .. } => ("**Coach:**", content.clone()),
|
||||
HistoryCell::System { content } => ("*System:*", content.clone()),
|
||||
HistoryCell::Thinking { content, .. } => ("*Thinking:*", content.clone()),
|
||||
HistoryCell::Tool(tool) => ("**Tool:**", render_tool_cell(tool, 80)),
|
||||
|
||||
+4
-167
@@ -7,10 +7,9 @@
|
||||
//! - Proper cancellation support
|
||||
//! - Tool execution orchestration
|
||||
|
||||
use std::fmt::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::pin::pin;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -26,7 +25,6 @@ use crate::compaction::{
|
||||
};
|
||||
use crate::config::Config;
|
||||
use crate::config::DEFAULT_MAX_SUBAGENTS;
|
||||
use crate::duo::{DuoSession, SharedDuoSession, session_summary as duo_session_summary};
|
||||
use crate::features::{Feature, Features};
|
||||
use crate::llm_client::LlmClient;
|
||||
use crate::mcp::McpPool;
|
||||
@@ -34,7 +32,6 @@ use crate::models::{
|
||||
ContentBlock, ContentBlockStart, Delta, Message, MessageRequest, StreamEvent, Tool, Usage,
|
||||
};
|
||||
use crate::prompts;
|
||||
use crate::rlm::{RlmSession, SharedRlmSession, session_summary as rlm_session_summary};
|
||||
use crate::tools::plan::{SharedPlanState, new_shared_plan_state};
|
||||
use crate::tools::shell::{SharedShellManager, new_shared_shell_manager};
|
||||
use crate::tools::spec::{ApprovalRequirement, ToolError, ToolResult};
|
||||
@@ -75,10 +72,6 @@ pub struct EngineConfig {
|
||||
pub max_subagents: usize,
|
||||
/// Feature flags controlling tool availability.
|
||||
pub features: Features,
|
||||
/// Shared RLM session state.
|
||||
pub rlm_session: SharedRlmSession,
|
||||
/// Shared Duo session state.
|
||||
pub duo_session: SharedDuoSession,
|
||||
/// Auto-compaction settings for long conversations.
|
||||
pub compaction: CompactionConfig,
|
||||
/// Shared Todo list state.
|
||||
@@ -99,8 +92,6 @@ impl Default for EngineConfig {
|
||||
max_steps: 100,
|
||||
max_subagents: DEFAULT_MAX_SUBAGENTS,
|
||||
features: Features::with_defaults(),
|
||||
rlm_session: Arc::new(Mutex::new(RlmSession::default())),
|
||||
duo_session: Arc::new(Mutex::new(DuoSession::new())),
|
||||
compaction: CompactionConfig::default(),
|
||||
todos: new_shared_todo_list(),
|
||||
plan_state: new_shared_plan_state(),
|
||||
@@ -538,8 +529,6 @@ impl Engine {
|
||||
AppMode::Agent,
|
||||
&config.workspace,
|
||||
working_set_summary.as_deref(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
session.system_prompt = Some(system_prompt);
|
||||
|
||||
@@ -778,24 +767,6 @@ impl Engine {
|
||||
self.config.trust_mode = trust_mode;
|
||||
|
||||
// Update system prompt to match the current mode
|
||||
let rlm_summary = if mode == AppMode::Rlm {
|
||||
self.config
|
||||
.rlm_session
|
||||
.lock()
|
||||
.ok()
|
||||
.map(|session| rlm_session_summary(&session))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let duo_summary = if mode == AppMode::Duo {
|
||||
self.config
|
||||
.duo_session
|
||||
.lock()
|
||||
.ok()
|
||||
.map(|s| duo_session_summary(&s))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let working_set_summary = self
|
||||
.session
|
||||
.working_set
|
||||
@@ -804,8 +775,6 @@ impl Engine {
|
||||
mode,
|
||||
&self.config.workspace,
|
||||
working_set_summary.as_deref(),
|
||||
rlm_summary.as_deref(),
|
||||
duo_summary.as_deref(),
|
||||
));
|
||||
|
||||
// Build tool registry and tool list for the current mode
|
||||
@@ -822,19 +791,8 @@ impl Engine {
|
||||
.with_todo_tool(todo_list.clone())
|
||||
.with_plan_tool(plan_state.clone())
|
||||
} else {
|
||||
let rlm_opt = if self.config.features.enabled(Feature::Rlm) {
|
||||
Some(self.config.rlm_session.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
ToolRegistryBuilder::new()
|
||||
.with_agent_tools(
|
||||
self.session.allow_shell,
|
||||
rlm_opt,
|
||||
self.deepseek_client.clone(),
|
||||
self.session.model.clone(),
|
||||
)
|
||||
.with_agent_tools(self.session.allow_shell)
|
||||
.with_todo_tool(todo_list.clone())
|
||||
.with_plan_tool(plan_state.clone())
|
||||
};
|
||||
@@ -857,33 +815,9 @@ impl Engine {
|
||||
{
|
||||
builder = builder.with_shell_tools();
|
||||
}
|
||||
if mode == AppMode::Rlm {
|
||||
if self.config.features.enabled(Feature::Rlm) {
|
||||
builder = builder.with_rlm_tools(
|
||||
self.config.rlm_session.clone(),
|
||||
self.deepseek_client.clone(),
|
||||
self.session.model.clone(),
|
||||
);
|
||||
} else {
|
||||
let _ = self
|
||||
.tx_event
|
||||
.send(Event::status("RLM tools are disabled by feature flags"))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
if mode == AppMode::Duo {
|
||||
if self.config.features.enabled(Feature::Duo) {
|
||||
builder = builder.with_duo_tools(self.config.duo_session.clone());
|
||||
} else {
|
||||
let _ = self
|
||||
.tx_event
|
||||
.send(Event::status("Duo tools are disabled by feature flags"))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
let tool_registry = match mode {
|
||||
AppMode::Agent | AppMode::Yolo | AppMode::Rlm | AppMode::Duo => {
|
||||
AppMode::Agent | AppMode::Yolo => {
|
||||
if self.config.features.enabled(Feature::Subagents) {
|
||||
let runtime = if let Some(client) = self.deepseek_client.clone() {
|
||||
Some(SubAgentRuntime::new(
|
||||
@@ -947,42 +881,6 @@ impl Engine {
|
||||
.with_shell_manager(self.shell_manager.clone())
|
||||
}
|
||||
|
||||
/// Automatically offload large tool results to RLM memory if enabled.
|
||||
/// Returns either the original content or a pointer to the RLM context.
|
||||
fn offload_to_rlm_if_needed(&self, tool_name: &str, content: String) -> String {
|
||||
const OFFLOAD_THRESHOLD: usize = 15_000;
|
||||
|
||||
if !self.config.features.enabled(Feature::Rlm) || content.len() < OFFLOAD_THRESHOLD {
|
||||
return content;
|
||||
}
|
||||
|
||||
let mut session = match self.config.rlm_session.lock() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return content,
|
||||
};
|
||||
|
||||
let context_id = format!(
|
||||
"auto_{}_{}",
|
||||
tool_name,
|
||||
&uuid::Uuid::new_v4().to_string()[..8]
|
||||
);
|
||||
let char_count = content.len();
|
||||
let line_count = content.lines().count();
|
||||
|
||||
session.load_context(&context_id, content, None);
|
||||
|
||||
format!(
|
||||
"[AUTOMATIC RLM OFFLOAD]\n\
|
||||
The output of '{tool_name}' was too large ({char_count} chars, {line_count} lines) \
|
||||
and has been moved to RLM memory to preserve your context window.\n\n\
|
||||
Context ID: {context_id}\n\n\
|
||||
You can explore this data using RLM tools:\n\
|
||||
- `rlm_exec(code=\"lines(1, 100)\", context_id=\"{context_id}\")` to see the start\n\
|
||||
- `rlm_exec(code=\"search(\\\"pattern\\\")\", context_id=\"{context_id}\")` to search\n\
|
||||
- `rlm_query(query=\"...\", context_id=\"{context_id}\")` for deep analysis"
|
||||
)
|
||||
}
|
||||
|
||||
async fn ensure_mcp_pool(&mut self) -> Result<Arc<AsyncMutex<McpPool>>, ToolError> {
|
||||
if let Some(pool) = self.mcp_pool.as_ref() {
|
||||
return Ok(Arc::clone(pool));
|
||||
@@ -1322,49 +1220,6 @@ impl Engine {
|
||||
Ok(result) => {
|
||||
// Only update if we got valid messages (never corrupt state)
|
||||
if !result.messages.is_empty() || self.session.messages.is_empty() {
|
||||
// Offload removed messages to RLM history if enabled
|
||||
if self.config.features.enabled(Feature::Rlm)
|
||||
&& !result.removed_messages.is_empty()
|
||||
{
|
||||
if let Ok(mut rlm) = self.config.rlm_session.lock() {
|
||||
let mut history_text = String::new();
|
||||
for msg in &result.removed_messages {
|
||||
let role = if msg.role == "user" {
|
||||
"User"
|
||||
} else {
|
||||
"Assistant"
|
||||
};
|
||||
for block in &msg.content {
|
||||
match block {
|
||||
ContentBlock::Text { text, .. } => {
|
||||
let _ =
|
||||
writeln!(history_text, "{role}: {text}\n");
|
||||
}
|
||||
ContentBlock::ToolUse { name, input, .. } => {
|
||||
let _ = writeln!(
|
||||
history_text,
|
||||
"{role}: [Used tool: {name}] Input: {input}\n"
|
||||
);
|
||||
}
|
||||
ContentBlock::ToolResult { content, .. } => {
|
||||
let _ = writeln!(
|
||||
history_text,
|
||||
"Tool result: {content}\n"
|
||||
);
|
||||
}
|
||||
ContentBlock::Thinking { thinking } => {
|
||||
let _ = writeln!(
|
||||
history_text,
|
||||
"{role} (thinking): {thinking}\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
rlm.append_var("history", history_text);
|
||||
}
|
||||
}
|
||||
|
||||
self.session.messages = result.messages;
|
||||
self.session.system_prompt = merge_system_prompts(
|
||||
self.session.system_prompt.as_ref(),
|
||||
@@ -1970,9 +1825,7 @@ impl Engine {
|
||||
|
||||
match outcome.result {
|
||||
Ok(output) => {
|
||||
let original_content = output.content;
|
||||
let output_content =
|
||||
self.offload_to_rlm_if_needed(&outcome.name, original_content);
|
||||
let output_content = output.content;
|
||||
|
||||
tool_call.set_result(output_content.clone(), duration);
|
||||
self.session.working_set.observe_tool_call(
|
||||
@@ -2044,20 +1897,6 @@ impl Engine {
|
||||
|
||||
/// Refresh the system prompt based on current mode and context.
|
||||
fn refresh_system_prompt(&mut self, mode: AppMode) {
|
||||
let rlm_summary = self
|
||||
.config
|
||||
.rlm_session
|
||||
.lock()
|
||||
.ok()
|
||||
.map(|session| rlm_session_summary(&session));
|
||||
|
||||
let duo_summary = self
|
||||
.config
|
||||
.duo_session
|
||||
.lock()
|
||||
.ok()
|
||||
.map(|s| duo_session_summary(&s));
|
||||
|
||||
let working_set_summary = self
|
||||
.session
|
||||
.working_set
|
||||
@@ -2067,8 +1906,6 @@ impl Engine {
|
||||
mode,
|
||||
&self.config.workspace,
|
||||
working_set_summary.as_deref(),
|
||||
rlm_summary.as_deref(),
|
||||
duo_summary.as_deref(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ pub enum Event {
|
||||
/// The turn is complete (no more tool calls)
|
||||
TurnComplete { usage: Usage },
|
||||
|
||||
// === Sub-Agent Events (for RLM mode) ===
|
||||
// === Sub-Agent Events ===
|
||||
/// A sub-agent has been spawned
|
||||
AgentSpawned { id: String, prompt: String },
|
||||
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ pub enum Op {
|
||||
/// Deny a tool call that requires permission
|
||||
DenyToolCall { id: String },
|
||||
|
||||
/// Spawn a sub-agent (for RLM mode)
|
||||
/// Spawn a sub-agent
|
||||
SpawnSubAgent { prompt: String },
|
||||
|
||||
/// List current sub-agents and their status
|
||||
|
||||
-802
@@ -1,802 +0,0 @@
|
||||
//! Duo mode state machine for hegelion's autocoding (player-coach adversarial cooperation).
|
||||
//!
|
||||
//! Implements the g3 paper's coach-player paradigm where:
|
||||
//! - Player: implements requirements (builder role)
|
||||
//! - Coach: validates implementation against requirements (critic role)
|
||||
//!
|
||||
//! The loop continues until the coach approves or max turns are reached.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
// === Phase & Status Enums ===
|
||||
|
||||
/// The current phase in the autocoding loop.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DuoPhase {
|
||||
/// Session initialized, ready to start player phase
|
||||
Init,
|
||||
/// Player is implementing requirements
|
||||
Player,
|
||||
/// Coach is validating the implementation
|
||||
Coach,
|
||||
/// Coach approved the implementation
|
||||
Approved,
|
||||
/// Maximum turns reached without approval
|
||||
Timeout,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DuoPhase {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DuoPhase::Init => write!(f, "init"),
|
||||
DuoPhase::Player => write!(f, "player"),
|
||||
DuoPhase::Coach => write!(f, "coach"),
|
||||
DuoPhase::Approved => write!(f, "approved"),
|
||||
DuoPhase::Timeout => write!(f, "timeout"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The overall status of the autocoding session.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DuoStatus {
|
||||
/// Session is actively running
|
||||
Active,
|
||||
/// Coach has approved the implementation
|
||||
Approved,
|
||||
/// Coach has rejected (used for explicit rejection, not just iteration)
|
||||
Rejected,
|
||||
/// Maximum turns exhausted without approval
|
||||
Timeout,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DuoStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DuoStatus::Active => write!(f, "active"),
|
||||
DuoStatus::Approved => write!(f, "approved"),
|
||||
DuoStatus::Rejected => write!(f, "rejected"),
|
||||
DuoStatus::Timeout => write!(f, "timeout"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Turn History ===
|
||||
|
||||
/// Record of a single turn in the autocoding loop.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TurnRecord {
|
||||
/// Turn number (1-indexed)
|
||||
pub turn: u32,
|
||||
/// The phase this record is for
|
||||
pub phase: DuoPhase,
|
||||
/// Summary of what happened (player implementation or coach feedback)
|
||||
pub summary: String,
|
||||
/// Quality score from coach (0.0 to 1.0), if applicable
|
||||
pub quality_score: Option<f64>,
|
||||
/// Timestamp when this turn was recorded
|
||||
#[serde(default = "chrono::Utc::now")]
|
||||
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl TurnRecord {
|
||||
/// Create a new turn record.
|
||||
#[must_use]
|
||||
pub fn new(turn: u32, phase: DuoPhase, summary: String, quality_score: Option<f64>) -> Self {
|
||||
Self {
|
||||
turn,
|
||||
phase,
|
||||
summary,
|
||||
quality_score,
|
||||
timestamp: chrono::Utc::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Main State ===
|
||||
|
||||
/// The complete state of a Duo autocoding session.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DuoState {
|
||||
/// Unique session identifier
|
||||
pub session_id: String,
|
||||
/// Optional human-readable session name
|
||||
pub session_name: Option<String>,
|
||||
/// The requirements document (source of truth for validation)
|
||||
pub requirements: String,
|
||||
/// Current turn number (1-indexed)
|
||||
pub current_turn: u32,
|
||||
/// Maximum allowed turns before timeout
|
||||
pub max_turns: u32,
|
||||
/// Current phase in the autocoding loop
|
||||
pub phase: DuoPhase,
|
||||
/// Overall session status
|
||||
pub status: DuoStatus,
|
||||
/// History of all turns
|
||||
pub turn_history: Vec<TurnRecord>,
|
||||
/// Last feedback from the coach (used in next player prompt)
|
||||
pub last_coach_feedback: Option<String>,
|
||||
/// Quality scores from each coach review
|
||||
pub quality_scores: Vec<f64>,
|
||||
/// Threshold score needed for approval (0.0 to 1.0)
|
||||
pub approval_threshold: f64,
|
||||
/// Timestamp when session was created
|
||||
#[serde(default = "chrono::Utc::now")]
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
/// Timestamp of last update
|
||||
#[serde(default = "chrono::Utc::now")]
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl DuoState {
|
||||
/// Create a new Duo session with the given requirements.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `requirements` - The requirements document (source of truth)
|
||||
/// * `session_name` - Optional human-readable name
|
||||
/// * `max_turns` - Maximum turns before timeout (default: 10)
|
||||
/// * `approval_threshold` - Score needed for approval (default: 0.9)
|
||||
#[must_use]
|
||||
pub fn create(
|
||||
requirements: String,
|
||||
session_name: Option<String>,
|
||||
max_turns: Option<u32>,
|
||||
approval_threshold: Option<f64>,
|
||||
) -> Self {
|
||||
let now = chrono::Utc::now();
|
||||
Self {
|
||||
session_id: Uuid::new_v4().to_string(),
|
||||
session_name,
|
||||
requirements,
|
||||
current_turn: 1,
|
||||
max_turns: max_turns.unwrap_or(10),
|
||||
phase: DuoPhase::Init,
|
||||
status: DuoStatus::Active,
|
||||
turn_history: Vec::new(),
|
||||
last_coach_feedback: None,
|
||||
quality_scores: Vec::new(),
|
||||
approval_threshold: approval_threshold.unwrap_or(0.9),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// Transition from Init or Player phase to Coach phase.
|
||||
///
|
||||
/// Records the player's implementation summary in turn history.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `player_summary` - Summary of what the player implemented
|
||||
///
|
||||
/// # Returns
|
||||
/// `Ok(())` on success, `Err` if not in a valid phase for this transition
|
||||
pub fn advance_to_coach(&mut self, player_summary: String) -> Result<(), DuoError> {
|
||||
match self.phase {
|
||||
DuoPhase::Init | DuoPhase::Player => {
|
||||
// Record player turn
|
||||
let record =
|
||||
TurnRecord::new(self.current_turn, DuoPhase::Player, player_summary, None);
|
||||
self.turn_history.push(record);
|
||||
self.phase = DuoPhase::Coach;
|
||||
self.updated_at = chrono::Utc::now();
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(DuoError::InvalidPhaseTransition {
|
||||
from: self.phase,
|
||||
to: DuoPhase::Coach,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Process coach feedback and determine the next phase.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `coach_feedback` - The coach's feedback text
|
||||
/// * `approved` - Whether the coach approved the implementation
|
||||
/// * `compliance_score` - Optional compliance score (0.0 to 1.0)
|
||||
///
|
||||
/// # Returns
|
||||
/// `Ok(())` on success, `Err` if not in coach phase
|
||||
pub fn advance_turn(
|
||||
&mut self,
|
||||
coach_feedback: String,
|
||||
approved: bool,
|
||||
compliance_score: Option<f64>,
|
||||
) -> Result<(), DuoError> {
|
||||
if self.phase != DuoPhase::Coach {
|
||||
return Err(DuoError::InvalidPhaseTransition {
|
||||
from: self.phase,
|
||||
to: DuoPhase::Player,
|
||||
});
|
||||
}
|
||||
|
||||
// Record coach turn
|
||||
let record = TurnRecord::new(
|
||||
self.current_turn,
|
||||
DuoPhase::Coach,
|
||||
coach_feedback.clone(),
|
||||
compliance_score,
|
||||
);
|
||||
self.turn_history.push(record);
|
||||
|
||||
// Track quality score if provided
|
||||
if let Some(score) = compliance_score {
|
||||
self.quality_scores.push(score);
|
||||
}
|
||||
|
||||
self.last_coach_feedback = Some(coach_feedback);
|
||||
self.updated_at = chrono::Utc::now();
|
||||
|
||||
if approved {
|
||||
// Coach approved - session complete
|
||||
self.phase = DuoPhase::Approved;
|
||||
self.status = DuoStatus::Approved;
|
||||
} else if self.current_turn >= self.max_turns {
|
||||
// Max turns reached - timeout
|
||||
self.phase = DuoPhase::Timeout;
|
||||
self.status = DuoStatus::Timeout;
|
||||
} else {
|
||||
// Continue to next turn
|
||||
self.current_turn += 1;
|
||||
self.phase = DuoPhase::Player;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if the session is complete (approved or timed out).
|
||||
#[must_use]
|
||||
pub fn is_complete(&self) -> bool {
|
||||
matches!(
|
||||
self.status,
|
||||
DuoStatus::Approved | DuoStatus::Rejected | DuoStatus::Timeout
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the number of turns remaining before timeout.
|
||||
#[must_use]
|
||||
pub fn turns_remaining(&self) -> u32 {
|
||||
self.max_turns.saturating_sub(self.current_turn)
|
||||
}
|
||||
|
||||
/// Get the average quality score across all coach reviews.
|
||||
#[must_use]
|
||||
pub fn average_quality_score(&self) -> Option<f64> {
|
||||
if self.quality_scores.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let sum: f64 = self.quality_scores.iter().sum();
|
||||
Some(sum / self.quality_scores.len() as f64)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a human-readable summary of the session state.
|
||||
#[must_use]
|
||||
pub fn summary(&self) -> String {
|
||||
let name = self
|
||||
.session_name
|
||||
.as_deref()
|
||||
.unwrap_or(&self.session_id[..8]);
|
||||
|
||||
let avg_score = self
|
||||
.average_quality_score()
|
||||
.map(|s| format!("{:.1}%", s * 100.0))
|
||||
.unwrap_or_else(|| "N/A".to_string());
|
||||
|
||||
let status_icon = match self.status {
|
||||
DuoStatus::Active => "🔄",
|
||||
DuoStatus::Approved => "✅",
|
||||
DuoStatus::Rejected => "❌",
|
||||
DuoStatus::Timeout => "⏰",
|
||||
};
|
||||
|
||||
format!(
|
||||
"{status_icon} Duo Session: {name}\n\
|
||||
Phase: {} | Turn: {}/{} | Status: {}\n\
|
||||
Avg Quality: {} | Threshold: {:.0}%\n\
|
||||
History: {} records",
|
||||
self.phase,
|
||||
self.current_turn,
|
||||
self.max_turns,
|
||||
self.status,
|
||||
avg_score,
|
||||
self.approval_threshold * 100.0,
|
||||
self.turn_history.len()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// === Error Types ===
|
||||
|
||||
/// Errors that can occur during Duo session operations.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DuoError {
|
||||
/// Invalid phase transition attempted
|
||||
InvalidPhaseTransition { from: DuoPhase, to: DuoPhase },
|
||||
/// Session not found (reserved for future multi-session management)
|
||||
#[allow(dead_code)]
|
||||
SessionNotFound { session_id: String },
|
||||
/// Session already complete (reserved for future session validation)
|
||||
#[allow(dead_code)]
|
||||
SessionAlreadyComplete { session_id: String },
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DuoError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DuoError::InvalidPhaseTransition { from, to } => {
|
||||
write!(f, "Invalid phase transition from {} to {}", from, to)
|
||||
}
|
||||
DuoError::SessionNotFound { session_id } => {
|
||||
write!(f, "Session not found: {}", session_id)
|
||||
}
|
||||
DuoError::SessionAlreadyComplete { session_id } => {
|
||||
write!(f, "Session already complete: {}", session_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DuoError {}
|
||||
|
||||
// === Session Container ===
|
||||
|
||||
/// Container for managing multiple Duo sessions.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct DuoSession {
|
||||
/// The currently active session state
|
||||
pub active_state: Option<DuoState>,
|
||||
/// Saved/completed session states indexed by session_id
|
||||
pub saved_states: HashMap<String, DuoState>,
|
||||
}
|
||||
|
||||
impl DuoSession {
|
||||
/// Create a new empty session container.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
active_state: None,
|
||||
saved_states: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a new Duo session.
|
||||
pub fn start_session(
|
||||
&mut self,
|
||||
requirements: String,
|
||||
session_name: Option<String>,
|
||||
max_turns: Option<u32>,
|
||||
approval_threshold: Option<f64>,
|
||||
) -> &DuoState {
|
||||
// Save any existing active session
|
||||
if let Some(state) = self.active_state.take() {
|
||||
self.saved_states.insert(state.session_id.clone(), state);
|
||||
}
|
||||
|
||||
// Create new session
|
||||
let state = DuoState::create(requirements, session_name, max_turns, approval_threshold);
|
||||
self.active_state = Some(state);
|
||||
self.active_state.as_ref().expect("just set active_state")
|
||||
}
|
||||
|
||||
/// Get the active session state.
|
||||
#[must_use]
|
||||
pub fn get_active(&self) -> Option<&DuoState> {
|
||||
self.active_state.as_ref()
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the active session state.
|
||||
pub fn get_active_mut(&mut self) -> Option<&mut DuoState> {
|
||||
self.active_state.as_mut()
|
||||
}
|
||||
|
||||
/// Get a saved session by ID (reserved for future multi-session management).
|
||||
#[must_use]
|
||||
#[allow(dead_code)]
|
||||
pub fn get_saved(&self, session_id: &str) -> Option<&DuoState> {
|
||||
self.saved_states.get(session_id)
|
||||
}
|
||||
|
||||
/// Save the current active session and clear it (reserved for future session management).
|
||||
#[allow(dead_code)]
|
||||
pub fn save_active(&mut self) -> Option<String> {
|
||||
if let Some(state) = self.active_state.take() {
|
||||
let id = state.session_id.clone();
|
||||
self.saved_states.insert(id.clone(), state);
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore a saved session as the active session (reserved for future session management).
|
||||
#[allow(dead_code)]
|
||||
pub fn restore_session(&mut self, session_id: &str) -> Result<(), DuoError> {
|
||||
let state =
|
||||
self.saved_states
|
||||
.remove(session_id)
|
||||
.ok_or_else(|| DuoError::SessionNotFound {
|
||||
session_id: session_id.to_string(),
|
||||
})?;
|
||||
|
||||
// Save current active if any
|
||||
if let Some(current) = self.active_state.take() {
|
||||
self.saved_states
|
||||
.insert(current.session_id.clone(), current);
|
||||
}
|
||||
|
||||
self.active_state = Some(state);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all session IDs (active and saved, reserved for future session management).
|
||||
#[must_use]
|
||||
#[allow(dead_code)]
|
||||
pub fn list_sessions(&self) -> Vec<&str> {
|
||||
let mut ids: Vec<&str> = self.saved_states.keys().map(String::as_str).collect();
|
||||
if let Some(ref active) = self.active_state {
|
||||
ids.push(&active.session_id);
|
||||
}
|
||||
ids.sort();
|
||||
ids
|
||||
}
|
||||
}
|
||||
|
||||
/// Thread-safe shared Duo session.
|
||||
pub type SharedDuoSession = Arc<Mutex<DuoSession>>;
|
||||
|
||||
/// Create a new shared Duo session.
|
||||
#[must_use]
|
||||
pub fn new_shared_duo_session() -> SharedDuoSession {
|
||||
Arc::new(Mutex::new(DuoSession::new()))
|
||||
}
|
||||
|
||||
// === Prompt Generation ===
|
||||
|
||||
/// Generate the player (implementation) prompt for the current state.
|
||||
///
|
||||
/// The player focuses on implementing requirements and should NOT declare success.
|
||||
#[must_use]
|
||||
pub fn generate_player_prompt(state: &DuoState) -> String {
|
||||
let mut prompt = String::new();
|
||||
|
||||
prompt.push_str("# Player Phase - Implementation\n\n");
|
||||
prompt.push_str("You are the PLAYER in an autocoding session. Your role is to IMPLEMENT the requirements.\n\n");
|
||||
|
||||
prompt.push_str("## Requirements (Source of Truth)\n\n");
|
||||
prompt.push_str(&state.requirements);
|
||||
prompt.push_str("\n\n");
|
||||
|
||||
prompt.push_str(&format!(
|
||||
"## Session Info\n\n\
|
||||
- Turn: {}/{}\n\
|
||||
- Approval Threshold: {:.0}%\n",
|
||||
state.current_turn,
|
||||
state.max_turns,
|
||||
state.approval_threshold * 100.0
|
||||
));
|
||||
|
||||
if let Some(ref feedback) = state.last_coach_feedback {
|
||||
prompt.push_str("\n## Previous Coach Feedback\n\n");
|
||||
prompt.push_str("Address these issues from the last review:\n\n");
|
||||
prompt.push_str(feedback);
|
||||
prompt.push('\n');
|
||||
}
|
||||
|
||||
prompt.push_str("\n## Instructions\n\n");
|
||||
prompt.push_str(
|
||||
"1. Implement the requirements above using available tools\n\
|
||||
2. Focus on making incremental progress\n\
|
||||
3. DO NOT declare success or claim completion\n\
|
||||
4. DO NOT evaluate your own work\n\
|
||||
5. The Coach will verify your implementation\n\n\
|
||||
Begin implementation now.\n",
|
||||
);
|
||||
|
||||
prompt
|
||||
}
|
||||
|
||||
/// Generate the coach (validation) prompt for the current state.
|
||||
///
|
||||
/// The coach verifies the implementation against requirements and ignores player self-assessment.
|
||||
#[must_use]
|
||||
pub fn generate_coach_prompt(state: &DuoState) -> String {
|
||||
let mut prompt = String::new();
|
||||
|
||||
prompt.push_str("# Coach Phase - Validation\n\n");
|
||||
prompt.push_str("You are the COACH in an autocoding session. Your role is to VERIFY the implementation.\n\n");
|
||||
|
||||
prompt.push_str("## Requirements (Source of Truth)\n\n");
|
||||
prompt.push_str(&state.requirements);
|
||||
prompt.push_str("\n\n");
|
||||
|
||||
prompt.push_str(&format!(
|
||||
"## Session Info\n\n\
|
||||
- Turn: {}/{}\n\
|
||||
- Approval Threshold: {:.0}%\n\
|
||||
- Turns Remaining: {}\n",
|
||||
state.current_turn,
|
||||
state.max_turns,
|
||||
state.approval_threshold * 100.0,
|
||||
state.turns_remaining()
|
||||
));
|
||||
|
||||
if !state.quality_scores.is_empty() {
|
||||
let avg = state.average_quality_score().unwrap_or(0.0);
|
||||
prompt.push_str(&format!("- Average Quality: {:.1}%\n", avg * 100.0));
|
||||
}
|
||||
|
||||
prompt.push_str("\n## Instructions\n\n");
|
||||
prompt.push_str(
|
||||
"1. Review the current implementation against the requirements\n\
|
||||
2. Create a COMPLIANCE CHECKLIST:\n\
|
||||
- [ ] or [x] for each requirement item\n\
|
||||
- Note any missing or incorrect implementations\n\
|
||||
3. Calculate a COMPLIANCE SCORE (0.0 to 1.0)\n\
|
||||
4. IGNORE any player self-assessment or claims of completion\n\
|
||||
5. If score >= threshold AND all critical items pass:\n\
|
||||
- Output: COACH APPROVED\n\
|
||||
6. Otherwise, provide specific feedback:\n\
|
||||
- What is missing\n\
|
||||
- What needs to be fixed\n\
|
||||
- Actionable next steps\n\n\
|
||||
Begin validation now.\n",
|
||||
);
|
||||
|
||||
prompt
|
||||
}
|
||||
|
||||
/// Generate a summary of the session for system prompt injection.
|
||||
#[must_use]
|
||||
pub fn session_summary(session: &DuoSession) -> String {
|
||||
let mut lines = Vec::new();
|
||||
|
||||
if let Some(ref state) = session.active_state {
|
||||
lines.push(format!("Active Duo Session: {}", state.summary()));
|
||||
} else {
|
||||
lines.push("No active Duo session.".to_string());
|
||||
}
|
||||
|
||||
if !session.saved_states.is_empty() {
|
||||
lines.push(format!("Saved sessions: {}", session.saved_states.len()));
|
||||
for (id, state) in &session.saved_states {
|
||||
let name = state
|
||||
.session_name
|
||||
.as_deref()
|
||||
.unwrap_or(&id[..8.min(id.len())]);
|
||||
lines.push(format!(" - {}: {} ({})", name, state.status, state.phase));
|
||||
}
|
||||
}
|
||||
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
// === Tests ===
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn sample_requirements() -> String {
|
||||
"## Requirements\n\
|
||||
- [ ] Create a function `add(a, b)` that returns the sum\n\
|
||||
- [ ] Add unit tests for the function\n\
|
||||
- [ ] Document the function with comments"
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_session() {
|
||||
let state = DuoState::create(
|
||||
sample_requirements(),
|
||||
Some("test-session".to_string()),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(state.session_name, Some("test-session".to_string()));
|
||||
assert_eq!(state.current_turn, 1);
|
||||
assert_eq!(state.max_turns, 10);
|
||||
assert_eq!(state.phase, DuoPhase::Init);
|
||||
assert_eq!(state.status, DuoStatus::Active);
|
||||
assert!(state.turn_history.is_empty());
|
||||
assert!(state.last_coach_feedback.is_none());
|
||||
assert_eq!(state.approval_threshold, 0.9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_advance_to_coach() {
|
||||
let mut state = DuoState::create(sample_requirements(), None, None, None);
|
||||
|
||||
assert!(
|
||||
state
|
||||
.advance_to_coach("Implemented add function".to_string())
|
||||
.is_ok()
|
||||
);
|
||||
assert_eq!(state.phase, DuoPhase::Coach);
|
||||
assert_eq!(state.turn_history.len(), 1);
|
||||
assert_eq!(state.turn_history[0].phase, DuoPhase::Player);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_advance_turn_approved() {
|
||||
let mut state = DuoState::create(sample_requirements(), None, None, None);
|
||||
state
|
||||
.advance_to_coach("Implemented everything".to_string())
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
state
|
||||
.advance_turn(
|
||||
"COACH APPROVED - All requirements met".to_string(),
|
||||
true,
|
||||
Some(0.95)
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
assert_eq!(state.phase, DuoPhase::Approved);
|
||||
assert_eq!(state.status, DuoStatus::Approved);
|
||||
assert!(state.is_complete());
|
||||
assert_eq!(state.quality_scores, vec![0.95]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_advance_turn_continue() {
|
||||
let mut state = DuoState::create(sample_requirements(), None, None, None);
|
||||
state
|
||||
.advance_to_coach("Partial implementation".to_string())
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
state
|
||||
.advance_turn("Missing tests".to_string(), false, Some(0.5))
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
assert_eq!(state.phase, DuoPhase::Player);
|
||||
assert_eq!(state.status, DuoStatus::Active);
|
||||
assert_eq!(state.current_turn, 2);
|
||||
assert!(!state.is_complete());
|
||||
assert_eq!(state.last_coach_feedback, Some("Missing tests".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timeout() {
|
||||
let mut state = DuoState::create(sample_requirements(), None, Some(2), None);
|
||||
|
||||
// Turn 1
|
||||
state.advance_to_coach("Attempt 1".to_string()).unwrap();
|
||||
state
|
||||
.advance_turn("Not good enough".to_string(), false, Some(0.3))
|
||||
.unwrap();
|
||||
|
||||
// Turn 2 (max)
|
||||
state.advance_to_coach("Attempt 2".to_string()).unwrap();
|
||||
state
|
||||
.advance_turn("Still not good enough".to_string(), false, Some(0.4))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.phase, DuoPhase::Timeout);
|
||||
assert_eq!(state.status, DuoStatus::Timeout);
|
||||
assert!(state.is_complete());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_phase_transition() {
|
||||
let mut state = DuoState::create(sample_requirements(), None, None, None);
|
||||
state.phase = DuoPhase::Approved;
|
||||
|
||||
let result = state.advance_to_coach("Should fail".to_string());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_turns_remaining() {
|
||||
let state = DuoState::create(sample_requirements(), None, Some(10), None);
|
||||
assert_eq!(state.turns_remaining(), 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_average_quality_score() {
|
||||
let mut state = DuoState::create(sample_requirements(), None, None, None);
|
||||
assert!(state.average_quality_score().is_none());
|
||||
|
||||
state.quality_scores = vec![0.5, 0.7, 0.9];
|
||||
let avg = state.average_quality_score().unwrap();
|
||||
assert!((avg - 0.7).abs() < 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_container() {
|
||||
let mut session = DuoSession::new();
|
||||
|
||||
// Start first session
|
||||
session.start_session(
|
||||
sample_requirements(),
|
||||
Some("session-1".to_string()),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(session.get_active().is_some());
|
||||
|
||||
// Start second session (first gets saved)
|
||||
session.start_session(
|
||||
"Other requirements".to_string(),
|
||||
Some("session-2".to_string()),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(session.saved_states.len(), 1);
|
||||
|
||||
// Get active
|
||||
let active = session.get_active().unwrap();
|
||||
assert_eq!(active.session_name, Some("session-2".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_player_prompt() {
|
||||
let state = DuoState::create(sample_requirements(), None, None, None);
|
||||
let prompt = generate_player_prompt(&state);
|
||||
|
||||
assert!(prompt.contains("Player Phase"));
|
||||
assert!(prompt.contains("Requirements (Source of Truth)"));
|
||||
assert!(prompt.contains("Turn: 1/10"));
|
||||
assert!(prompt.contains("DO NOT declare success"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_coach_prompt() {
|
||||
let state = DuoState::create(sample_requirements(), None, None, None);
|
||||
let prompt = generate_coach_prompt(&state);
|
||||
|
||||
assert!(prompt.contains("Coach Phase"));
|
||||
assert!(prompt.contains("COMPLIANCE CHECKLIST"));
|
||||
assert!(prompt.contains("COACH APPROVED"));
|
||||
assert!(prompt.contains("IGNORE any player self-assessment"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shared_session() {
|
||||
let shared = new_shared_duo_session();
|
||||
|
||||
{
|
||||
let mut session = shared.lock().unwrap();
|
||||
session.start_session(sample_requirements(), None, None, None);
|
||||
}
|
||||
|
||||
{
|
||||
let session = shared.lock().unwrap();
|
||||
assert!(session.get_active().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_summary() {
|
||||
let state = DuoState::create(sample_requirements(), Some("test".to_string()), None, None);
|
||||
let summary = state.summary();
|
||||
|
||||
assert!(summary.contains("Duo Session: test"));
|
||||
assert!(summary.contains("Phase: init"));
|
||||
assert!(summary.contains("Turn: 1/10"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_summary() {
|
||||
let mut session = DuoSession::new();
|
||||
session.start_session(
|
||||
sample_requirements(),
|
||||
Some("active-session".to_string()),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let summary = session_summary(&session);
|
||||
assert!(summary.contains("Active Duo Session"));
|
||||
assert!(summary.contains("active-session"));
|
||||
}
|
||||
}
|
||||
@@ -28,10 +28,6 @@ pub enum Feature {
|
||||
ApplyPatch,
|
||||
/// Enable MCP tools.
|
||||
Mcp,
|
||||
/// Enable RLM tools.
|
||||
Rlm,
|
||||
/// Enable Duo tools.
|
||||
Duo,
|
||||
/// Enable execpolicy integration/tooling.
|
||||
ExecPolicy,
|
||||
}
|
||||
@@ -171,18 +167,6 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
stage: Stage::Experimental,
|
||||
default_enabled: true,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::Rlm,
|
||||
key: "rlm",
|
||||
stage: Stage::Experimental,
|
||||
default_enabled: true,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::Duo,
|
||||
key: "duo",
|
||||
stage: Stage::Experimental,
|
||||
default_enabled: true,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::ExecPolicy,
|
||||
key: "exec_policy",
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ pub enum HookCondition {
|
||||
},
|
||||
/// Only run in specific modes
|
||||
Mode {
|
||||
/// Mode: "plan", "agent", "yolo", "rlm", "duo"
|
||||
/// Mode: "plan", "agent", "yolo"
|
||||
mode: String,
|
||||
},
|
||||
/// Only run when exit code matches (for `ToolCallAfter`)
|
||||
|
||||
@@ -32,7 +32,6 @@ mod commands;
|
||||
mod compaction;
|
||||
mod config;
|
||||
mod core;
|
||||
mod duo;
|
||||
mod eval;
|
||||
mod execpolicy;
|
||||
mod features;
|
||||
@@ -48,7 +47,6 @@ mod project_context;
|
||||
mod project_doc;
|
||||
mod prompts;
|
||||
mod responses_api_proxy;
|
||||
mod rlm;
|
||||
mod sandbox;
|
||||
mod session_manager;
|
||||
mod settings;
|
||||
@@ -1823,14 +1821,10 @@ async fn run_exec_agent(
|
||||
auto_approve: bool,
|
||||
trust_mode: bool,
|
||||
) -> Result<()> {
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::compaction::CompactionConfig;
|
||||
use crate::core::engine::{EngineConfig, spawn_engine};
|
||||
use crate::core::events::Event;
|
||||
use crate::core::ops::Op;
|
||||
use crate::duo::DuoSession;
|
||||
use crate::rlm::RlmSession;
|
||||
use crate::tools::plan::new_shared_plan_state;
|
||||
use crate::tools::todo::new_shared_todo_list;
|
||||
use crate::tui::app::AppMode;
|
||||
@@ -1845,8 +1839,6 @@ async fn run_exec_agent(
|
||||
max_steps: 100,
|
||||
max_subagents,
|
||||
features: config.features(),
|
||||
rlm_session: Arc::new(Mutex::new(RlmSession::default())),
|
||||
duo_session: Arc::new(Mutex::new(DuoSession::new())),
|
||||
compaction: CompactionConfig::default(),
|
||||
todos: new_shared_todo_list(),
|
||||
plan_state: new_shared_plan_state(),
|
||||
|
||||
@@ -56,8 +56,6 @@ pub const MODE_NORMAL: Color = Color::Gray;
|
||||
pub const MODE_AGENT: Color = Color::Rgb(80, 150, 255); // Bright blue
|
||||
pub const MODE_YOLO: Color = Color::Rgb(255, 100, 100); // Warning red
|
||||
pub const MODE_PLAN: Color = Color::Rgb(255, 170, 60); // Orange
|
||||
pub const MODE_RLM: Color = Color::Rgb(180, 100, 255); // Purple (was INK!)
|
||||
pub const MODE_DUO: Color = Color::Rgb(100, 220, 180); // Teal
|
||||
|
||||
pub const SELECTION_BG: Color = Color::Rgb(26, 44, 74);
|
||||
pub const COMPOSER_BG: Color = DEEPSEEK_SLATE;
|
||||
|
||||
@@ -13,8 +13,6 @@ pub const BASE_PROMPT: &str = include_str!("prompts/base.txt");
|
||||
pub const NORMAL_PROMPT: &str = include_str!("prompts/normal.txt");
|
||||
pub const AGENT_PROMPT: &str = include_str!("prompts/agent.txt");
|
||||
pub const PLAN_PROMPT: &str = include_str!("prompts/plan.txt");
|
||||
pub const RLM_PROMPT: &str = include_str!("prompts/rlm.txt");
|
||||
pub const DUO_PROMPT: &str = include_str!("prompts/duo.txt");
|
||||
|
||||
/// Get the system prompt for a specific mode
|
||||
pub fn system_prompt_for_mode(mode: AppMode) -> SystemPrompt {
|
||||
@@ -22,8 +20,6 @@ pub fn system_prompt_for_mode(mode: AppMode) -> SystemPrompt {
|
||||
AppMode::Normal => NORMAL_PROMPT,
|
||||
AppMode::Agent | AppMode::Yolo => AGENT_PROMPT,
|
||||
AppMode::Plan => PLAN_PROMPT,
|
||||
AppMode::Rlm => RLM_PROMPT,
|
||||
AppMode::Duo => DUO_PROMPT,
|
||||
};
|
||||
SystemPrompt::Text(text.trim().to_string())
|
||||
}
|
||||
@@ -33,15 +29,11 @@ pub fn system_prompt_for_mode_with_context(
|
||||
mode: AppMode,
|
||||
workspace: &Path,
|
||||
working_set_summary: Option<&str>,
|
||||
rlm_summary: Option<&str>,
|
||||
duo_summary: Option<&str>,
|
||||
) -> SystemPrompt {
|
||||
let base_prompt = match mode {
|
||||
AppMode::Normal => NORMAL_PROMPT,
|
||||
AppMode::Agent | AppMode::Yolo => AGENT_PROMPT,
|
||||
AppMode::Plan => PLAN_PROMPT,
|
||||
AppMode::Rlm => RLM_PROMPT,
|
||||
AppMode::Duo => DUO_PROMPT,
|
||||
};
|
||||
|
||||
// Load project context from workspace
|
||||
@@ -68,16 +60,6 @@ pub fn system_prompt_for_mode_with_context(
|
||||
full_prompt = format!("{full_prompt}\n\n{summary}");
|
||||
}
|
||||
|
||||
if mode == AppMode::Rlm {
|
||||
let summary = rlm_summary.unwrap_or("No RLM contexts loaded.");
|
||||
full_prompt = format!("{full_prompt}\n\nRLM Context Summary:\n{summary}");
|
||||
}
|
||||
|
||||
if mode == AppMode::Duo {
|
||||
let summary = duo_summary.unwrap_or("No Duo contexts loaded.");
|
||||
full_prompt = format!("{full_prompt}\n\nDuo Context Summary:\n{summary}");
|
||||
}
|
||||
|
||||
SystemPrompt::Text(full_prompt)
|
||||
}
|
||||
|
||||
|
||||
@@ -40,8 +40,6 @@ Approval etiquette:
|
||||
|
||||
Tone: competent, warm, and concise. Use light humor sparingly when it fits; a rare example is "You're absolutely right! ... maybe."
|
||||
|
||||
Context Management & RLM:
|
||||
- You have a finite context window. Large tool results may be automatically moved to RLM memory.
|
||||
- If you see "[AUTOMATIC RLM OFFLOAD]", the content was stored in an RLM context ID. Use `rlm_exec` or `rlm_query` to access it.
|
||||
- Proactively use `rlm_load` for files larger than 100KB to keep your main window lean.
|
||||
- If the conversation is long, earlier messages may be summarized. You can search the full history using `rlm_exec(code="get(\"history\")")` if available.
|
||||
Context Management:
|
||||
- You have a finite context window. Keep responses concise and prefer targeted file reads or searches.
|
||||
- Long conversations may be compacted into summaries; ask for clarification if critical details are missing.
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
You are in Duo mode for requirements-driven development.
|
||||
|
||||
Workflow:
|
||||
- Start with duo_init using a clear requirements checklist and acceptance criteria.
|
||||
- Alternate duo_player (implement) and duo_coach (verify) until approved.
|
||||
- In duo_player phases, work tool-first: search, read, make targeted edits, then verify.
|
||||
- In duo_coach phases, prioritize objective verification and requirement coverage.
|
||||
|
||||
Tool selection and verification:
|
||||
- Use search/read tools to ground work in the current codebase before editing.
|
||||
- Prefer targeted diffs over broad rewrites when possible.
|
||||
- After any change, run the most relevant tests/checks before handing off to duo_coach.
|
||||
- If verification fails, report it concisely, fix it, and re-run.
|
||||
|
||||
Budgeting and hygiene:
|
||||
- Budget attempts. If 2-3 attempts fail to make progress, reassess and state the blocker.
|
||||
- Check git status early and again before finishing; do not revert unrelated changes.
|
||||
- Provide brief progress updates at phase boundaries and major checkpoints.
|
||||
@@ -1,13 +0,0 @@
|
||||
You are in RLM mode for working with large files that exceed context limits.
|
||||
|
||||
Work tool-first and chunk-aware:
|
||||
- Use rlm_* tools to load files, explore content, and run focused queries over chunks.
|
||||
- Prefer search-then-read: locate relevant sections before loading large spans.
|
||||
- Summarize the relevant chunks in your own words before editing.
|
||||
- Make targeted edits; avoid full-file rewrites unless necessary.
|
||||
|
||||
Verification, budgeting, and hygiene:
|
||||
- After any change, run the most relevant tests/checks before declaring success.
|
||||
- Check git status early and again before finishing; do not revert unrelated changes.
|
||||
- Budget attempts. If 2-3 chunking/search attempts fail to surface the needed context, pause and state the blocker or request clarification.
|
||||
- Provide concise progress updates at key checkpoints (what chunk/area you inspected, what changed, what verified).
|
||||
-1317
File diff suppressed because it is too large
Load Diff
+4
-15
@@ -15,13 +15,11 @@ pub struct Settings {
|
||||
pub theme: String,
|
||||
/// Auto-compact conversations when they get long
|
||||
pub auto_compact: bool,
|
||||
/// Auto-switch to RLM mode when large inputs are detected
|
||||
pub auto_rlm: bool,
|
||||
/// Show thinking blocks from the model
|
||||
pub show_thinking: bool,
|
||||
/// Show detailed tool output
|
||||
pub show_tool_details: bool,
|
||||
/// Default mode: "agent", "plan", "yolo", "rlm", "duo"
|
||||
/// Default mode: "agent", "plan", "yolo"
|
||||
pub default_mode: String,
|
||||
/// Sidebar width as percentage of terminal width
|
||||
pub sidebar_width_percent: u16,
|
||||
@@ -36,7 +34,6 @@ impl Default for Settings {
|
||||
Self {
|
||||
theme: "whale".to_string(),
|
||||
auto_compact: true,
|
||||
auto_rlm: true,
|
||||
show_thinking: true,
|
||||
show_tool_details: true,
|
||||
default_mode: "agent".to_string(),
|
||||
@@ -102,9 +99,6 @@ impl Settings {
|
||||
"auto_compact" | "compact" => {
|
||||
self.auto_compact = parse_bool(value)?;
|
||||
}
|
||||
"auto_rlm" => {
|
||||
self.auto_rlm = parse_bool(value)?;
|
||||
}
|
||||
"show_thinking" | "thinking" => {
|
||||
self.show_thinking = parse_bool(value)?;
|
||||
}
|
||||
@@ -113,9 +107,9 @@ impl Settings {
|
||||
}
|
||||
"default_mode" | "mode" => {
|
||||
let normalized = normalize_mode(value);
|
||||
if !["agent", "plan", "yolo", "rlm", "duo"].contains(&normalized) {
|
||||
if !["agent", "plan", "yolo"].contains(&normalized) {
|
||||
anyhow::bail!(
|
||||
"Failed to update setting: invalid mode '{value}'. Expected: agent, plan, yolo, rlm, duo."
|
||||
"Failed to update setting: invalid mode '{value}'. Expected: agent, plan, yolo."
|
||||
);
|
||||
}
|
||||
self.default_mode = normalized.to_string();
|
||||
@@ -160,7 +154,6 @@ impl Settings {
|
||||
lines.push("─────────────────────────────".to_string());
|
||||
lines.push(format!(" theme: {}", self.theme));
|
||||
lines.push(format!(" auto_compact: {}", self.auto_compact));
|
||||
lines.push(format!(" auto_rlm: {}", self.auto_rlm));
|
||||
lines.push(format!(" show_thinking: {}", self.show_thinking));
|
||||
lines.push(format!(" show_tool_details: {}", self.show_tool_details));
|
||||
lines.push(format!(" default_mode: {}", self.default_mode));
|
||||
@@ -186,13 +179,9 @@ impl Settings {
|
||||
vec![
|
||||
("theme", "Color theme: default, dark, light"),
|
||||
("auto_compact", "Auto-compact conversations: on/off"),
|
||||
(
|
||||
"auto_rlm",
|
||||
"Auto-switch to RLM mode for large inputs: on/off",
|
||||
),
|
||||
("show_thinking", "Show model thinking: on/off"),
|
||||
("show_tool_details", "Show detailed tool output: on/off"),
|
||||
("default_mode", "Default mode: agent, plan, yolo, rlm, duo"),
|
||||
("default_mode", "Default mode: agent, plan, yolo"),
|
||||
("sidebar_width", "Sidebar width percentage: 10-50"),
|
||||
("max_history", "Max input history entries"),
|
||||
("default_model", "Default model name"),
|
||||
|
||||
@@ -1,468 +0,0 @@
|
||||
//! Tools for Duo mode: Player-Coach autocoding workflow.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::duo::{
|
||||
DuoPhase, SharedDuoSession, generate_coach_prompt, generate_player_prompt, session_summary,
|
||||
};
|
||||
use crate::tools::spec::{
|
||||
ApprovalRequirement, ToolCapability, ToolContext, ToolError, ToolResult, ToolSpec,
|
||||
optional_str, required_str,
|
||||
};
|
||||
|
||||
/// Initialize an autocoding session with requirements.
|
||||
pub struct DuoInitTool {
|
||||
session: SharedDuoSession,
|
||||
}
|
||||
|
||||
impl DuoInitTool {
|
||||
#[must_use]
|
||||
pub fn new(session: SharedDuoSession) -> Self {
|
||||
Self { session }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ToolSpec for DuoInitTool {
|
||||
fn name(&self) -> &'static str {
|
||||
"duo_init"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Initialize a Duo autocoding session with requirements. Returns session summary."
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"requirements": {
|
||||
"type": "string",
|
||||
"description": "The requirements document (source of truth). Should be structured as a checklist."
|
||||
},
|
||||
"max_turns": {
|
||||
"type": "integer",
|
||||
"description": "Maximum turns before timeout (default: 10)"
|
||||
},
|
||||
"session_name": {
|
||||
"type": "string",
|
||||
"description": "Optional human-readable session name (e.g., 'auth-feature')"
|
||||
},
|
||||
"approval_threshold": {
|
||||
"type": "number",
|
||||
"description": "Minimum compliance score for approval (0-1, default: 0.9)"
|
||||
}
|
||||
},
|
||||
"required": ["requirements"]
|
||||
})
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> Vec<ToolCapability> {
|
||||
vec![ToolCapability::ReadOnly]
|
||||
}
|
||||
|
||||
fn approval_requirement(&self) -> ApprovalRequirement {
|
||||
ApprovalRequirement::Auto
|
||||
}
|
||||
|
||||
async fn execute(&self, input: Value, _context: &ToolContext) -> Result<ToolResult, ToolError> {
|
||||
let requirements = required_str(&input, "requirements")?;
|
||||
let max_turns = input
|
||||
.get("max_turns")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|v| v as u32);
|
||||
let session_name = optional_str(&input, "session_name").map(str::to_string);
|
||||
let approval_threshold = input.get("approval_threshold").and_then(|v| v.as_f64());
|
||||
|
||||
let mut session = self
|
||||
.session
|
||||
.lock()
|
||||
.map_err(|_| ToolError::execution_failed("Failed to lock Duo session"))?;
|
||||
|
||||
let state = session.start_session(
|
||||
requirements.to_string(),
|
||||
session_name,
|
||||
max_turns,
|
||||
approval_threshold,
|
||||
);
|
||||
|
||||
let summary = state.summary();
|
||||
Ok(ToolResult::success(format!(
|
||||
"Duo session initialized. Ready for player phase.\n\n{}",
|
||||
summary
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the player prompt for implementation.
|
||||
pub struct DuoPlayerTool {
|
||||
session: SharedDuoSession,
|
||||
}
|
||||
|
||||
impl DuoPlayerTool {
|
||||
#[must_use]
|
||||
pub fn new(session: SharedDuoSession) -> Self {
|
||||
Self { session }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ToolSpec for DuoPlayerTool {
|
||||
fn name(&self) -> &'static str {
|
||||
"duo_player"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Generate the player prompt for implementation. Must be in Init or Player phase. Call after implementing to advance to Coach phase."
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"implementation_summary": {
|
||||
"type": "string",
|
||||
"description": "Optional summary of implementation work done (recorded in history)"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> Vec<ToolCapability> {
|
||||
vec![ToolCapability::ReadOnly]
|
||||
}
|
||||
|
||||
fn approval_requirement(&self) -> ApprovalRequirement {
|
||||
ApprovalRequirement::Auto
|
||||
}
|
||||
|
||||
async fn execute(&self, input: Value, _context: &ToolContext) -> Result<ToolResult, ToolError> {
|
||||
let implementation_summary = optional_str(&input, "implementation_summary")
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| "Implementation in progress".to_string());
|
||||
|
||||
let mut session = self
|
||||
.session
|
||||
.lock()
|
||||
.map_err(|_| ToolError::execution_failed("Failed to lock Duo session"))?;
|
||||
|
||||
let state = session
|
||||
.get_active_mut()
|
||||
.ok_or_else(|| ToolError::invalid_input("No active session. Call duo_init first."))?;
|
||||
|
||||
// Check we're in a valid phase for player
|
||||
match state.phase {
|
||||
DuoPhase::Init | DuoPhase::Player => {
|
||||
// Generate prompt first
|
||||
let prompt = generate_player_prompt(state);
|
||||
|
||||
// Advance to Coach phase
|
||||
state
|
||||
.advance_to_coach(implementation_summary)
|
||||
.map_err(|e| ToolError::execution_failed(e.to_string()))?;
|
||||
|
||||
Ok(ToolResult::success(format!(
|
||||
"=== PLAYER PROMPT ===\n\n{}\n\n---\nAdvanced to Coach phase. Use duo_coach for verification.",
|
||||
prompt
|
||||
)))
|
||||
}
|
||||
DuoPhase::Coach => Err(ToolError::invalid_input(
|
||||
"Already in Coach phase. Use duo_coach to get verification prompt.",
|
||||
)),
|
||||
DuoPhase::Approved => Err(ToolError::invalid_input(
|
||||
"Session already approved. Start a new session with duo_init.",
|
||||
)),
|
||||
DuoPhase::Timeout => Err(ToolError::invalid_input(
|
||||
"Session timed out. Start a new session with duo_init.",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the coach prompt for validation.
|
||||
pub struct DuoCoachTool {
|
||||
session: SharedDuoSession,
|
||||
}
|
||||
|
||||
impl DuoCoachTool {
|
||||
#[must_use]
|
||||
pub fn new(session: SharedDuoSession) -> Self {
|
||||
Self { session }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ToolSpec for DuoCoachTool {
|
||||
fn name(&self) -> &'static str {
|
||||
"duo_coach"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Generate the coach prompt for validation. Must be in Coach phase. Does NOT advance state."
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
})
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> Vec<ToolCapability> {
|
||||
vec![ToolCapability::ReadOnly]
|
||||
}
|
||||
|
||||
fn approval_requirement(&self) -> ApprovalRequirement {
|
||||
ApprovalRequirement::Auto
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
_input: Value,
|
||||
_context: &ToolContext,
|
||||
) -> Result<ToolResult, ToolError> {
|
||||
let session = self
|
||||
.session
|
||||
.lock()
|
||||
.map_err(|_| ToolError::execution_failed("Failed to lock Duo session"))?;
|
||||
|
||||
let state = session
|
||||
.get_active()
|
||||
.ok_or_else(|| ToolError::invalid_input("No active session. Call duo_init first."))?;
|
||||
|
||||
if state.phase != DuoPhase::Coach {
|
||||
return Err(ToolError::invalid_input(format!(
|
||||
"Expected Coach phase, but current phase is {}. Use duo_player first.",
|
||||
state.phase
|
||||
)));
|
||||
}
|
||||
|
||||
let prompt = generate_coach_prompt(state);
|
||||
|
||||
Ok(ToolResult::success(format!(
|
||||
"=== COACH PROMPT ===\n\n{}\n\n---\nAfter verification, use duo_advance with feedback and approval status.",
|
||||
prompt
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Advance the session after coach review.
|
||||
pub struct DuoAdvanceTool {
|
||||
session: SharedDuoSession,
|
||||
}
|
||||
|
||||
impl DuoAdvanceTool {
|
||||
#[must_use]
|
||||
pub fn new(session: SharedDuoSession) -> Self {
|
||||
Self { session }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ToolSpec for DuoAdvanceTool {
|
||||
fn name(&self) -> &'static str {
|
||||
"duo_advance"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Advance the session after coach review. Updates turn count and records feedback. Returns new status."
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"feedback": {
|
||||
"type": "string",
|
||||
"description": "The coach's feedback text (compliance checklist and actions needed)"
|
||||
},
|
||||
"approved": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the coach approved the implementation (look for 'COACH APPROVED')"
|
||||
},
|
||||
"compliance_score": {
|
||||
"type": "number",
|
||||
"description": "Optional compliance score (0-1) based on checklist items satisfied"
|
||||
}
|
||||
},
|
||||
"required": ["feedback", "approved"]
|
||||
})
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> Vec<ToolCapability> {
|
||||
vec![ToolCapability::ReadOnly]
|
||||
}
|
||||
|
||||
fn approval_requirement(&self) -> ApprovalRequirement {
|
||||
ApprovalRequirement::Auto
|
||||
}
|
||||
|
||||
async fn execute(&self, input: Value, _context: &ToolContext) -> Result<ToolResult, ToolError> {
|
||||
let feedback = required_str(&input, "feedback")?;
|
||||
let approved = input
|
||||
.get("approved")
|
||||
.and_then(|v| v.as_bool())
|
||||
.ok_or_else(|| ToolError::missing_field("approved"))?;
|
||||
let compliance_score = input.get("compliance_score").and_then(|v| v.as_f64());
|
||||
|
||||
let mut session = self
|
||||
.session
|
||||
.lock()
|
||||
.map_err(|_| ToolError::execution_failed("Failed to lock Duo session"))?;
|
||||
|
||||
let state = session
|
||||
.get_active_mut()
|
||||
.ok_or_else(|| ToolError::invalid_input("No active session. Call duo_init first."))?;
|
||||
|
||||
if state.phase != DuoPhase::Coach {
|
||||
return Err(ToolError::invalid_input(format!(
|
||||
"Expected Coach phase, but current phase is {}",
|
||||
state.phase
|
||||
)));
|
||||
}
|
||||
|
||||
// Advance the turn
|
||||
state
|
||||
.advance_turn(feedback.to_string(), approved, compliance_score)
|
||||
.map_err(|e| ToolError::execution_failed(e.to_string()))?;
|
||||
|
||||
// Determine status message based on new phase
|
||||
let status_msg = match state.phase {
|
||||
DuoPhase::Approved => "🎉 APPROVED! All requirements verified.",
|
||||
DuoPhase::Timeout => "⏰ TIMEOUT. Max turns reached without approval.",
|
||||
DuoPhase::Player => "🔄 Continuing to next player turn...",
|
||||
_ => "Session updated.",
|
||||
};
|
||||
|
||||
let summary = state.summary();
|
||||
let mut result = ToolResult::success(format!("{}\n\n{}", status_msg, summary));
|
||||
result.metadata = Some(json!({
|
||||
"phase": state.phase.to_string(),
|
||||
"status": state.status.to_string(),
|
||||
"turn": state.current_turn,
|
||||
"max_turns": state.max_turns,
|
||||
"approved": approved,
|
||||
"compliance_score": compliance_score,
|
||||
"is_complete": state.is_complete(),
|
||||
}));
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Show the current session status.
|
||||
pub struct DuoStatusTool {
|
||||
session: SharedDuoSession,
|
||||
}
|
||||
|
||||
impl DuoStatusTool {
|
||||
#[must_use]
|
||||
pub fn new(session: SharedDuoSession) -> Self {
|
||||
Self { session }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ToolSpec for DuoStatusTool {
|
||||
fn name(&self) -> &'static str {
|
||||
"duo_status"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Show the current Duo session status including phase, turn count, and requirements."
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
})
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> Vec<ToolCapability> {
|
||||
vec![ToolCapability::ReadOnly]
|
||||
}
|
||||
|
||||
fn approval_requirement(&self) -> ApprovalRequirement {
|
||||
ApprovalRequirement::Auto
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
_input: Value,
|
||||
_context: &ToolContext,
|
||||
) -> Result<ToolResult, ToolError> {
|
||||
let session = self
|
||||
.session
|
||||
.lock()
|
||||
.map_err(|_| ToolError::execution_failed("Failed to lock Duo session"))?;
|
||||
|
||||
Ok(ToolResult::success(session_summary(&session)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::duo::new_shared_duo_session;
|
||||
|
||||
#[test]
|
||||
fn test_duo_init_tool_schema() {
|
||||
let session = new_shared_duo_session();
|
||||
let tool = DuoInitTool::new(session);
|
||||
|
||||
assert_eq!(tool.name(), "duo_init");
|
||||
assert_eq!(tool.approval_requirement(), ApprovalRequirement::Auto);
|
||||
|
||||
let schema = tool.input_schema();
|
||||
assert!(schema.get("properties").is_some());
|
||||
assert!(
|
||||
schema["required"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.contains(&json!("requirements"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duo_player_tool_schema() {
|
||||
let session = new_shared_duo_session();
|
||||
let tool = DuoPlayerTool::new(session);
|
||||
|
||||
assert_eq!(tool.name(), "duo_player");
|
||||
assert_eq!(tool.approval_requirement(), ApprovalRequirement::Auto);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duo_coach_tool_schema() {
|
||||
let session = new_shared_duo_session();
|
||||
let tool = DuoCoachTool::new(session);
|
||||
|
||||
assert_eq!(tool.name(), "duo_coach");
|
||||
assert_eq!(tool.approval_requirement(), ApprovalRequirement::Auto);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duo_advance_tool_schema() {
|
||||
let session = new_shared_duo_session();
|
||||
let tool = DuoAdvanceTool::new(session);
|
||||
|
||||
assert_eq!(tool.name(), "duo_advance");
|
||||
assert_eq!(tool.approval_requirement(), ApprovalRequirement::Auto);
|
||||
|
||||
let schema = tool.input_schema();
|
||||
let required = schema["required"].as_array().unwrap();
|
||||
assert!(required.contains(&json!("feedback")));
|
||||
assert!(required.contains(&json!("approved")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duo_status_tool_schema() {
|
||||
let session = new_shared_duo_session();
|
||||
let tool = DuoStatusTool::new(session);
|
||||
|
||||
assert_eq!(tool.name(), "duo_status");
|
||||
assert_eq!(tool.approval_requirement(), ApprovalRequirement::Auto);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@
|
||||
pub mod apply_patch;
|
||||
pub mod calculator;
|
||||
pub mod diagnostics;
|
||||
pub mod duo;
|
||||
pub mod file;
|
||||
pub mod file_search;
|
||||
pub mod finance;
|
||||
@@ -17,7 +16,6 @@ pub mod plan;
|
||||
pub mod project;
|
||||
pub mod registry;
|
||||
pub mod review;
|
||||
pub mod rlm;
|
||||
pub mod search;
|
||||
pub mod shell;
|
||||
pub mod spec;
|
||||
@@ -90,6 +88,3 @@ pub use parallel::MultiToolUseParallelTool;
|
||||
|
||||
// Re-export user input tool/types
|
||||
pub use user_input::{RequestUserInputTool, UserInputAnswer, UserInputRequest, UserInputResponse};
|
||||
|
||||
// Re-export RLM tools
|
||||
pub use rlm::{RlmExecTool, RlmLoadTool, RlmQueryTool, RlmStatusTool};
|
||||
|
||||
+3
-45
@@ -12,9 +12,7 @@ use std::sync::Arc;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::client::DeepSeekClient;
|
||||
use crate::duo::SharedDuoSession;
|
||||
use crate::models::Tool;
|
||||
use crate::rlm::SharedRlmSession;
|
||||
|
||||
use super::spec::{
|
||||
ApprovalRequirement, ToolCapability, ToolContext, ToolError, ToolResult, ToolSpec,
|
||||
@@ -383,14 +381,8 @@ impl ToolRegistryBuilder {
|
||||
|
||||
/// Include all agent tools (file tools + shell + note + search + patch).
|
||||
#[must_use]
|
||||
pub fn with_agent_tools(
|
||||
self,
|
||||
allow_shell: bool,
|
||||
rlm_session: Option<SharedRlmSession>,
|
||||
client: Option<DeepSeekClient>,
|
||||
model: String,
|
||||
) -> Self {
|
||||
let mut builder = self
|
||||
pub fn with_agent_tools(self, allow_shell: bool) -> Self {
|
||||
let builder = self
|
||||
.with_file_tools()
|
||||
.with_note_tool()
|
||||
.with_search_tools()
|
||||
@@ -404,10 +396,6 @@ impl ToolRegistryBuilder {
|
||||
.with_project_tools()
|
||||
.with_test_runner_tool();
|
||||
|
||||
if let Some(session) = rlm_session {
|
||||
builder = builder.with_rlm_tools(session, client, model);
|
||||
}
|
||||
|
||||
if allow_shell {
|
||||
builder.with_shell_tools()
|
||||
} else {
|
||||
@@ -439,42 +427,12 @@ impl ToolRegistryBuilder {
|
||||
allow_shell: bool,
|
||||
todo_list: super::todo::SharedTodoList,
|
||||
plan_state: super::plan::SharedPlanState,
|
||||
rlm_session: Option<SharedRlmSession>,
|
||||
client: Option<DeepSeekClient>,
|
||||
model: String,
|
||||
) -> Self {
|
||||
self.with_agent_tools(allow_shell, rlm_session, client, model)
|
||||
self.with_agent_tools(allow_shell)
|
||||
.with_todo_tool(todo_list)
|
||||
.with_plan_tool(plan_state)
|
||||
}
|
||||
|
||||
/// Include RLM tools for context execution and sub-queries.
|
||||
#[must_use]
|
||||
pub fn with_rlm_tools(
|
||||
self,
|
||||
session: SharedRlmSession,
|
||||
client: Option<DeepSeekClient>,
|
||||
model: String,
|
||||
) -> Self {
|
||||
self.with_tool(Arc::new(super::rlm::RlmExecTool::new(session.clone())))
|
||||
.with_tool(Arc::new(super::rlm::RlmLoadTool::new(session.clone())))
|
||||
.with_tool(Arc::new(super::rlm::RlmStatusTool::new(session.clone())))
|
||||
.with_tool(Arc::new(super::rlm::RlmQueryTool::new(
|
||||
session, client, model,
|
||||
)))
|
||||
}
|
||||
|
||||
/// Include Duo tools for dialectical autocoding.
|
||||
#[must_use]
|
||||
pub fn with_duo_tools(self, session: SharedDuoSession) -> Self {
|
||||
use super::duo::{DuoAdvanceTool, DuoCoachTool, DuoInitTool, DuoPlayerTool, DuoStatusTool};
|
||||
self.with_tool(Arc::new(DuoInitTool::new(session.clone())))
|
||||
.with_tool(Arc::new(DuoPlayerTool::new(session.clone())))
|
||||
.with_tool(Arc::new(DuoCoachTool::new(session.clone())))
|
||||
.with_tool(Arc::new(DuoAdvanceTool::new(session.clone())))
|
||||
.with_tool(Arc::new(DuoStatusTool::new(session)))
|
||||
}
|
||||
|
||||
/// Include sub-agent management tools.
|
||||
#[must_use]
|
||||
pub fn with_subagent_tools(
|
||||
|
||||
-1047
File diff suppressed because it is too large
Load Diff
+2
-36
@@ -10,11 +10,9 @@ use thiserror::Error;
|
||||
|
||||
use crate::compaction::CompactionConfig;
|
||||
use crate::config::{Config, has_api_key, save_api_key};
|
||||
use crate::duo::{SharedDuoSession, new_shared_duo_session};
|
||||
use crate::hooks::{HookContext, HookEvent, HookExecutor, HookResult};
|
||||
use crate::models::{Message, SystemPrompt};
|
||||
use crate::palette::{self, UiTheme};
|
||||
use crate::rlm::{RlmSession, SharedRlmSession};
|
||||
use crate::settings::Settings;
|
||||
use crate::tools::plan::{SharedPlanState, new_shared_plan_state};
|
||||
use crate::tools::subagent::SubAgentResult;
|
||||
@@ -27,7 +25,6 @@ use crate::tui::scrolling::{MouseScrollState, TranscriptScroll};
|
||||
use crate::tui::selection::TranscriptSelection;
|
||||
use crate::tui::transcript::TranscriptViewCache;
|
||||
use crate::tui::views::ViewStack;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Format a nice welcome banner similar to Kimi CLI.
|
||||
@@ -70,8 +67,6 @@ pub enum AppMode {
|
||||
Agent,
|
||||
Yolo,
|
||||
Plan,
|
||||
Rlm,
|
||||
Duo,
|
||||
}
|
||||
|
||||
fn char_count(text: &str) -> usize {
|
||||
@@ -119,8 +114,6 @@ impl AppMode {
|
||||
AppMode::Agent => "AGENT",
|
||||
AppMode::Yolo => "YOLO",
|
||||
AppMode::Plan => "PLAN",
|
||||
AppMode::Rlm => "RLM",
|
||||
AppMode::Duo => "DUO",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,8 +125,6 @@ impl AppMode {
|
||||
AppMode::Agent => "Agent mode - autonomous task execution with tools",
|
||||
AppMode::Yolo => "YOLO mode - full tool access without approvals",
|
||||
AppMode::Plan => "Plan mode - design before implementing",
|
||||
AppMode::Rlm => "RLM mode - recursive language model sandbox",
|
||||
AppMode::Duo => "Duo mode - dialectical autocoding with player-coach loop",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,7 +191,6 @@ pub struct App {
|
||||
pub input_history: Vec<String>,
|
||||
pub history_index: Option<usize>,
|
||||
pub auto_compact: bool,
|
||||
pub auto_rlm: bool,
|
||||
pub show_thinking: bool,
|
||||
pub show_tool_details: bool,
|
||||
#[allow(dead_code)]
|
||||
@@ -242,12 +232,6 @@ pub struct App {
|
||||
pub plan_prompt_pending: bool,
|
||||
/// Whether update_plan was called during the current turn
|
||||
pub plan_tool_used_in_turn: bool,
|
||||
/// RLM sandbox session state
|
||||
pub rlm_session: SharedRlmSession,
|
||||
/// Duo mode session state (player-coach autocoding loop)
|
||||
pub duo_session: SharedDuoSession,
|
||||
/// Whether RLM REPL input mode is active.
|
||||
pub rlm_repl_active: bool,
|
||||
/// Todo list for `TodoWriteTool`
|
||||
#[allow(dead_code)] // For future engine integration
|
||||
pub todos: SharedTodoList,
|
||||
@@ -314,14 +298,6 @@ impl QueuedMessage {
|
||||
self.display.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn content_with_query(&self, query: &str) -> String {
|
||||
if let Some(skill_instruction) = self.skill_instruction.as_ref() {
|
||||
format!("{skill_instruction}\n\n---\n\nUser request: {query}")
|
||||
} else {
|
||||
query.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Errors ===
|
||||
@@ -364,7 +340,6 @@ impl App {
|
||||
let needs_onboarding = !skip_onboarding && (!was_onboarded || needs_api_key);
|
||||
let settings = Settings::load().unwrap_or_else(|_| Settings::default());
|
||||
let auto_compact = settings.auto_compact;
|
||||
let auto_rlm = settings.auto_rlm;
|
||||
let show_thinking = settings.show_thinking;
|
||||
let show_tool_details = settings.show_tool_details;
|
||||
let max_input_history = settings.max_input_history;
|
||||
@@ -376,8 +351,6 @@ impl App {
|
||||
"plan" => AppMode::Plan,
|
||||
"agent" | "normal" => AppMode::Agent,
|
||||
"yolo" => AppMode::Yolo,
|
||||
"rlm" => AppMode::Rlm,
|
||||
"duo" => AppMode::Duo,
|
||||
_ => AppMode::Agent,
|
||||
};
|
||||
let initial_mode = if yolo {
|
||||
@@ -445,7 +418,6 @@ impl App {
|
||||
input_history: Vec::new(),
|
||||
history_index: None,
|
||||
auto_compact,
|
||||
auto_rlm,
|
||||
show_thinking,
|
||||
show_tool_details,
|
||||
compact_threshold: 50000,
|
||||
@@ -484,9 +456,6 @@ impl App {
|
||||
plan_state,
|
||||
plan_prompt_pending: false,
|
||||
plan_tool_used_in_turn: false,
|
||||
rlm_session: Arc::new(Mutex::new(RlmSession::default())),
|
||||
duo_session: new_shared_duo_session(),
|
||||
rlm_repl_active: false,
|
||||
todos: new_shared_todo_list(),
|
||||
tool_log: Vec::new(),
|
||||
session_cost: 0.0,
|
||||
@@ -550,7 +519,6 @@ impl App {
|
||||
} else {
|
||||
ApprovalMode::Suggest
|
||||
};
|
||||
self.rlm_repl_active = false;
|
||||
if mode != AppMode::Plan {
|
||||
self.plan_prompt_pending = false;
|
||||
self.plan_tool_used_in_turn = false;
|
||||
@@ -565,14 +533,12 @@ impl App {
|
||||
let _ = self.hooks.execute(HookEvent::ModeChange, &context);
|
||||
}
|
||||
|
||||
/// Cycle through modes: Plan → Agent → YOLO → RLM → Duo → Plan
|
||||
/// Cycle through modes: Plan → Agent → YOLO → Plan
|
||||
pub fn cycle_mode(&mut self) {
|
||||
let next = match self.mode {
|
||||
AppMode::Plan => AppMode::Agent,
|
||||
AppMode::Agent => AppMode::Yolo,
|
||||
AppMode::Yolo => AppMode::Rlm,
|
||||
AppMode::Rlm => AppMode::Duo,
|
||||
AppMode::Duo | AppMode::Normal => AppMode::Plan,
|
||||
AppMode::Yolo | AppMode::Normal => AppMode::Plan,
|
||||
};
|
||||
self.set_mode(next);
|
||||
}
|
||||
|
||||
+4
-62
@@ -27,8 +27,6 @@ const TOOL_TEXT_LIMIT: usize = 240;
|
||||
pub enum HistoryCell {
|
||||
User { content: String },
|
||||
Assistant { content: String, streaming: bool },
|
||||
Player { content: String, streaming: bool },
|
||||
Coach { content: String, streaming: bool },
|
||||
System { content: String },
|
||||
Thinking { content: String, streaming: bool },
|
||||
Tool(ToolCell),
|
||||
@@ -67,30 +65,6 @@ impl HistoryCell {
|
||||
}
|
||||
lines
|
||||
}
|
||||
HistoryCell::Player { content, streaming } => {
|
||||
let mut lines = render_message("Player", content, player_style(), width);
|
||||
if *streaming {
|
||||
if let Some(last) = lines.last_mut() {
|
||||
last.spans.push(Span::styled(
|
||||
"▋",
|
||||
Style::default().fg(palette::DEEPSEEK_SKY),
|
||||
));
|
||||
}
|
||||
}
|
||||
lines
|
||||
}
|
||||
HistoryCell::Coach { content, streaming } => {
|
||||
let mut lines = render_message("Coach", content, coach_style(), width);
|
||||
if *streaming {
|
||||
if let Some(last) = lines.last_mut() {
|
||||
last.spans.push(Span::styled(
|
||||
"▋",
|
||||
Style::default().fg(palette::DEEPSEEK_SKY),
|
||||
));
|
||||
}
|
||||
}
|
||||
lines
|
||||
}
|
||||
HistoryCell::System { content } => {
|
||||
render_message("System", content, system_style(), width)
|
||||
}
|
||||
@@ -148,15 +122,6 @@ impl HistoryCell {
|
||||
/// Convert a message into history cells for rendering.
|
||||
#[must_use]
|
||||
pub fn history_cells_from_message(msg: &Message) -> Vec<HistoryCell> {
|
||||
history_cells_from_message_with_mode(msg, None)
|
||||
}
|
||||
|
||||
/// Convert a message into history cells with optional Duo phase context.
|
||||
#[must_use]
|
||||
pub fn history_cells_from_message_with_mode(
|
||||
msg: &Message,
|
||||
duo_phase: Option<crate::duo::DuoPhase>,
|
||||
) -> Vec<HistoryCell> {
|
||||
let mut cells = Vec::new();
|
||||
let mut text_blocks = Vec::new();
|
||||
let mut thinking_blocks = Vec::new();
|
||||
@@ -174,23 +139,10 @@ pub fn history_cells_from_message_with_mode(
|
||||
match msg.role.as_str() {
|
||||
"user" => cells.push(HistoryCell::User { content }),
|
||||
"assistant" => {
|
||||
let cell = match duo_phase {
|
||||
Some(crate::duo::DuoPhase::Player) | Some(crate::duo::DuoPhase::Init) => {
|
||||
HistoryCell::Player {
|
||||
content,
|
||||
streaming: false,
|
||||
}
|
||||
}
|
||||
Some(crate::duo::DuoPhase::Coach) => HistoryCell::Coach {
|
||||
content,
|
||||
streaming: false,
|
||||
},
|
||||
_ => HistoryCell::Assistant {
|
||||
content,
|
||||
streaming: false,
|
||||
},
|
||||
};
|
||||
cells.push(cell);
|
||||
cells.push(HistoryCell::Assistant {
|
||||
content,
|
||||
streaming: false,
|
||||
});
|
||||
}
|
||||
"system" => cells.push(HistoryCell::System { content }),
|
||||
_ => {}
|
||||
@@ -1321,16 +1273,6 @@ fn assistant_style() -> Style {
|
||||
Style::default().fg(palette::DEEPSEEK_SKY)
|
||||
}
|
||||
|
||||
fn player_style() -> Style {
|
||||
Style::default().fg(palette::MODE_DUO)
|
||||
}
|
||||
|
||||
fn coach_style() -> Style {
|
||||
Style::default()
|
||||
.fg(palette::DEEPSEEK_BLUE)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
}
|
||||
|
||||
fn system_style() -> Style {
|
||||
Style::default().fg(palette::TEXT_MUTED).italic()
|
||||
}
|
||||
|
||||
@@ -55,9 +55,7 @@ pub fn tips_lines() -> Vec<ratatui::text::Line<'static>> {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)),
|
||||
Line::from(""),
|
||||
Line::from(Span::raw(
|
||||
" - Tab cycles modes (Plan → Agent → YOLO → RLM → Duo)",
|
||||
)),
|
||||
Line::from(Span::raw(" - Tab cycles modes (Plan → Agent → YOLO)")),
|
||||
Line::from(Span::raw(" - Ctrl+R opens the session picker")),
|
||||
Line::from(Span::raw(" - l opens the pager for the last message")),
|
||||
Line::from(Span::raw(" - Ctrl+C cancels or exits")),
|
||||
|
||||
+70
-807
File diff suppressed because it is too large
Load Diff
+1
-13
@@ -284,7 +284,7 @@ impl ModalView for HelpView {
|
||||
"Modes:",
|
||||
Style::default().fg(palette::DEEPSEEK_SKY).bold(),
|
||||
)]),
|
||||
Line::from(" Tab cycles modes: Plan → Agent → YOLO → RLM → Duo"),
|
||||
Line::from(" Tab cycles modes: Plan → Agent → YOLO"),
|
||||
Line::from(""),
|
||||
Line::from(vec![Span::styled(
|
||||
"Commands:",
|
||||
@@ -299,18 +299,6 @@ impl ModalView for HelpView {
|
||||
)));
|
||||
}
|
||||
|
||||
help_lines.push(Line::from(""));
|
||||
help_lines.push(Line::from(vec![Span::styled(
|
||||
"RLM / Aleph:",
|
||||
Style::default().fg(palette::DEEPSEEK_SKY).bold(),
|
||||
)]));
|
||||
help_lines.push(Line::from(" /rlm or /aleph - Enter external memory mode"));
|
||||
help_lines.push(Line::from(
|
||||
" /load @path - Load a file into RLM context",
|
||||
));
|
||||
help_lines.push(Line::from(" /repl - Toggle expression mode"));
|
||||
help_lines.push(Line::from(" /status - Show contexts and usage"));
|
||||
|
||||
help_lines.push(Line::from(""));
|
||||
help_lines.push(Line::from(vec![Span::styled(
|
||||
"Tools:",
|
||||
|
||||
@@ -60,8 +60,6 @@ impl<'a> HeaderWidget<'a> {
|
||||
AppMode::Agent => palette::MODE_AGENT,
|
||||
AppMode::Yolo => palette::MODE_YOLO,
|
||||
AppMode::Plan => palette::MODE_PLAN,
|
||||
AppMode::Rlm => palette::MODE_RLM,
|
||||
AppMode::Duo => palette::MODE_DUO,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-10
@@ -5,7 +5,7 @@ pub use header::{HeaderData, HeaderWidget};
|
||||
pub use renderable::Renderable;
|
||||
|
||||
use crate::palette;
|
||||
use crate::tui::app::{App, AppMode};
|
||||
use crate::tui::app::App;
|
||||
use crate::tui::approval::{ApprovalRequest, ElevationOption, ElevationRequest, ToolCategory};
|
||||
use crate::tui::scrolling::TranscriptScroll;
|
||||
use ratatui::{
|
||||
@@ -128,15 +128,7 @@ impl Renderable for ComposerWidget<'_> {
|
||||
|
||||
let mut lines = Vec::new();
|
||||
if self.app.input.is_empty() {
|
||||
let placeholder = if self.app.mode == AppMode::Rlm {
|
||||
if self.app.rlm_repl_active {
|
||||
"Type an RLM expression or /repl to exit..."
|
||||
} else {
|
||||
"Ask a question or /repl to enter expression mode..."
|
||||
}
|
||||
} else {
|
||||
"Type a message or /help for commands..."
|
||||
};
|
||||
let placeholder = "Type a message or /help for commands...";
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(
|
||||
self.prompt,
|
||||
|
||||
Reference in New Issue
Block a user