Enhance logging and documentation across utility tools and TUI overlays
- Added tracing instrumentation and improved logging messages in the Pong, Todofinish, and Todowrite tools for better debugging and monitoring. - Enhanced documentation comments for clarity on tool functionalities and workflows. - Implemented tracing in WorkflowRun, NoteFinding, ReadFindings, and HiveMind tools to track execution phases and findings. - Updated TUI overlays (e.g., Bash, Clear Confirm, Editor, Effort Level, Help, Key Input, Learning, Loading, MCP, Model Selector, Plan, Quit Confirm, Rewind, Settings, Todo, Usage) with debug logging to capture rendering details. - Improved the status bar and workflow panel rendering with additional debug information. - Added tracing to various utility functions to facilitate better performance monitoring and error tracking.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
//! tool calls) or the iteration limit is reached.
|
||||
|
||||
use anyhow::Result;
|
||||
use tracing::{debug, info};
|
||||
use tracing::{debug, info, instrument};
|
||||
|
||||
use crate::llm::provider::LlmClient;
|
||||
use crate::subagent::context::SubagentContext;
|
||||
@@ -29,6 +29,7 @@ const MAX_ITERATIONS: u32 = 25;
|
||||
/// tool-role message.
|
||||
/// d. If the response also contained text, append an assistant message.
|
||||
/// 4. If the loop exits naturally, return the iteration-limit message.
|
||||
#[instrument(skip(ctx, tool_ctx))]
|
||||
pub async fn run_agent(
|
||||
ctx: SubagentContext,
|
||||
directive: &str,
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
//! Subagent gating — decide whether to run review/test/arch agents based
|
||||
//! on the current context.
|
||||
|
||||
use tracing::instrument;
|
||||
|
||||
/// Determine whether an auto-review should be triggered after an edit.
|
||||
///
|
||||
/// Gating logic:
|
||||
/// - Returns `false` if there are no edits (`edit_count == 0`).
|
||||
/// - Returns `false` if `consecutive_empty_reviews >= max_skip` (too many
|
||||
/// consecutive reviews produced no findings, so skip further reviews).
|
||||
/// - Otherwise returns `true`.
|
||||
#[instrument]
|
||||
pub fn should_review(edit_count: u32, consecutive_empty_reviews: u32, max_skip: u32) -> bool {
|
||||
if edit_count == 0 {
|
||||
return false;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//! inside the subagent engine loop.
|
||||
|
||||
use anyhow::Result;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::llm::provider::LlmClient;
|
||||
use crate::tools::{tool_defs, Tool};
|
||||
@@ -25,6 +26,7 @@ pub struct SubagentProvider {
|
||||
|
||||
impl SubagentProvider {
|
||||
/// Wrap an existing `LlmClient` for higher-level use.
|
||||
#[instrument(skip(client))]
|
||||
pub fn new(client: LlmClient) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
@@ -32,6 +34,7 @@ impl SubagentProvider {
|
||||
/// Send messages to the LLM without any tool definitions.
|
||||
///
|
||||
/// Use this for a plain text-in/text-out conversation.
|
||||
#[tracing::instrument(skip(self, messages))]
|
||||
pub fn chat(
|
||||
&self,
|
||||
messages: &[ChatMessage],
|
||||
@@ -44,6 +47,7 @@ impl SubagentProvider {
|
||||
///
|
||||
/// Automatically converts the `&[Box<dyn Tool>]` slice to
|
||||
/// `Vec<ToolDef>` before passing to the underlying client.
|
||||
#[tracing::instrument(skip(self, messages, tools))]
|
||||
pub fn chat_with_tools(
|
||||
&self,
|
||||
messages: &[ChatMessage],
|
||||
@@ -60,6 +64,7 @@ impl SubagentProvider {
|
||||
/// Flow: reads `settings.provider` and `settings.model` → if model is empty,
|
||||
/// falls back to the provider config's `default_model` → if that is also
|
||||
/// empty, uses `"deepseek-v4-flash-free"` as the ultimate default.
|
||||
#[instrument]
|
||||
pub fn resolve_subagent_provider(
|
||||
settings: &zesdex_domain::cms::Settings,
|
||||
app_config: &zesdex_domain::cms::AppConfig,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
use std::thread;
|
||||
|
||||
use anyhow::Result;
|
||||
use tracing::info;
|
||||
use tracing::{info, instrument};
|
||||
|
||||
use crate::subagent::context::SubagentContext;
|
||||
use crate::subagent::division::AccessTier;
|
||||
@@ -23,6 +23,7 @@ use crate::tools::ToolCtx;
|
||||
/// `runtime.block_on(run_agent(...))` → return.
|
||||
///
|
||||
/// Returns a `JoinHandle` the caller can `join()` to await the result.
|
||||
#[instrument(skip(ctx, tool_ctx))]
|
||||
pub fn spawn_subagent(
|
||||
ctx: SubagentContext,
|
||||
directive: String,
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
//! Subagent tool helpers — wrap tool execution for subagent use.
|
||||
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
|
||||
/// Execute a single tool call within a subagent context.
|
||||
///
|
||||
/// Delegates directly to the tool's `run` method with the given context and
|
||||
/// JSON arguments.
|
||||
#[instrument(skip(tool, ctx, args))]
|
||||
pub fn execute_tool_call(
|
||||
tool: &dyn Tool,
|
||||
ctx: &ToolCtx,
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use tracing::instrument;
|
||||
|
||||
/// Create an isolated workspace directory for a subagent.
|
||||
///
|
||||
/// Creates `{base_dir}/subagent-workspaces/{agent_id}` and all parent
|
||||
/// directories if they do not already exist.
|
||||
#[instrument]
|
||||
pub fn create_subagent_workspace(base_dir: &Path, agent_id: &str) -> anyhow::Result<PathBuf> {
|
||||
let ws = base_dir.join("subagent-workspaces").join(agent_id);
|
||||
std::fs::create_dir_all(&ws)?;
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
//! Background bash process output and kill tools.
|
||||
//!
|
||||
//! These tools allow the agent to inspect the output of a background shell job
|
||||
//! (`BashOutput`) and to terminate a running background job (`BashKill`).
|
||||
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::info;
|
||||
use tracing::{info, instrument, warn};
|
||||
|
||||
use crate::tools::{arg_str, Tool, ToolCtx};
|
||||
|
||||
/// Get the output of a background bash job by ID.
|
||||
///
|
||||
/// Flow: look up `{session_dir}/bash-outputs/{job_id}` → read content back.
|
||||
/// Path traversal in `job_id` is explicitly rejected.
|
||||
pub struct BashOutput;
|
||||
|
||||
impl Tool for BashOutput {
|
||||
@@ -33,6 +37,7 @@ impl Tool for BashOutput {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let job_id = arg_str(args, "job_id")?;
|
||||
info!("Getting output for job: {job_id}");
|
||||
@@ -48,7 +53,7 @@ impl Tool for BashOutput {
|
||||
|
||||
if output_file.exists() {
|
||||
let content = std::fs::read_to_string(&output_file)
|
||||
.unwrap_or_else(|_| "Error reading output".to_string());
|
||||
.unwrap_or_else(|e| format!("Error reading output: {e}"));
|
||||
Ok(format!("Output for job '{job_id}':\n{content}"))
|
||||
} else {
|
||||
Ok(format!(
|
||||
@@ -58,6 +63,9 @@ impl Tool for BashOutput {
|
||||
}
|
||||
}
|
||||
|
||||
/// Kill a background bash job by ID.
|
||||
///
|
||||
/// Flow: parse job_id → look up in the global bash controller → cancel the job.
|
||||
pub struct BashKill;
|
||||
|
||||
impl Tool for BashKill {
|
||||
@@ -82,13 +90,16 @@ impl Tool for BashKill {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, _ctx, args))]
|
||||
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let job_id = crate::tools::arg_str(args, "job_id")?;
|
||||
info!("bash_kill called for job: {job_id}");
|
||||
|
||||
if crate::bgbash::control::bash_control().cancel(&job_id) {
|
||||
info!(job = %job_id, "background job killed");
|
||||
Ok(format!("Killed background job '{job_id}'"))
|
||||
} else {
|
||||
warn!(job = %job_id, "no active background job found");
|
||||
anyhow::bail!("no active background job found with ID '{job_id}'")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@ use crate::tools::{resolve_path, Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
use tracing::{info, instrument, warn};
|
||||
|
||||
/// Delete a file or empty directory.
|
||||
///
|
||||
/// Flow: resolve path → check existence → remove file or empty directory.
|
||||
pub struct Delete;
|
||||
|
||||
impl Tool for Delete {
|
||||
@@ -33,21 +37,25 @@ impl Tool for Delete {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let rel = crate::tools::arg_str(args, "path")?;
|
||||
let path = resolve_path(&ctx.workspaces, &rel)?;
|
||||
|
||||
if !path.exists() {
|
||||
warn!(rel = %rel, "delete target does not exist");
|
||||
anyhow::bail!("path '{rel}' does not exist");
|
||||
}
|
||||
|
||||
if path.is_file() {
|
||||
fs::remove_file(&path)?;
|
||||
info!(rel = %rel, "file deleted");
|
||||
Ok(format!("Deleted file '{rel}'"))
|
||||
} else if path.is_dir() {
|
||||
fs::remove_dir(&path).map_err(|e| {
|
||||
anyhow::anyhow!("failed to delete directory '{rel}': {e} (directory must be empty)")
|
||||
})?;
|
||||
info!(rel = %rel, "empty directory deleted");
|
||||
Ok(format!("Deleted empty directory '{rel}'"))
|
||||
} else {
|
||||
anyhow::bail!("'{rel}' is neither a file nor a directory")
|
||||
|
||||
@@ -4,7 +4,12 @@ use crate::tools::{resolve_path, Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
use tracing::{info, instrument, warn};
|
||||
|
||||
/// Edit a file by replacing `old` text with `new` text.
|
||||
///
|
||||
/// Flow: resolve path → read file → ensure `old` exists → perform single
|
||||
/// replacement → write file.
|
||||
pub struct Edit;
|
||||
|
||||
impl Tool for Edit {
|
||||
@@ -41,6 +46,7 @@ impl Tool for Edit {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let rel = crate::tools::arg_str(args, "path")?;
|
||||
let old = crate::tools::arg_str(args, "old")?;
|
||||
@@ -48,17 +54,20 @@ impl Tool for Edit {
|
||||
let path = resolve_path(&ctx.workspaces, &rel)?;
|
||||
|
||||
if !path.exists() {
|
||||
warn!(rel = %rel, "edit target does not exist");
|
||||
anyhow::bail!("file '{rel}' does not exist");
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&path)?;
|
||||
if !content.contains(&old) {
|
||||
warn!(rel = %rel, old_len = old.len(), "old text not found in file");
|
||||
anyhow::bail!("old text not found in '{}'", rel);
|
||||
}
|
||||
|
||||
let new_content = content.replacen(&old, &new, 1);
|
||||
fs::write(&path, &new_content)?;
|
||||
|
||||
info!(rel = %rel, old_len = old.len(), new_len = new.len(), "file edited");
|
||||
Ok(format!(
|
||||
"Edited '{}': replaced {} bytes with {} bytes",
|
||||
rel,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
//! Helper utilities for filesystem tools — content hashing, path validation, etc.
|
||||
|
||||
use sha2::Digest;
|
||||
use tracing::instrument;
|
||||
|
||||
/// Compute the SHA-256 hex digest of a string.
|
||||
#[instrument(skip(content))]
|
||||
pub fn sha256_hex(content: &str) -> String {
|
||||
hex::encode(sha2::Sha256::digest(content.as_bytes()))
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@ use crate::tools::{resolve_path, Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
use tracing::{info, instrument, warn};
|
||||
|
||||
/// Read the contents of a file.
|
||||
///
|
||||
/// Flow: resolve path → check existence and type → read file content → return.
|
||||
pub struct Read;
|
||||
|
||||
impl Tool for Read {
|
||||
@@ -29,16 +33,20 @@ impl Tool for Read {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let rel = crate::tools::arg_str(args, "path")?;
|
||||
let path = resolve_path(&ctx.workspaces, &rel)?;
|
||||
if !path.exists() {
|
||||
warn!(rel = %rel, "read target does not exist");
|
||||
anyhow::bail!("file '{rel}' does not exist");
|
||||
}
|
||||
if !path.is_file() {
|
||||
warn!(rel = %rel, "read target is not a file");
|
||||
anyhow::bail!("'{rel}' is not a file");
|
||||
}
|
||||
let content = fs::read_to_string(&path)?;
|
||||
info!(rel = %rel, bytes = content.len(), "file read");
|
||||
Ok(content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,12 @@ use crate::tools::{check_graduated_checks, resolve_path, Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
use tracing::{debug, info, instrument, warn};
|
||||
|
||||
/// Write content to a file (creating or overwriting).
|
||||
///
|
||||
/// Flow: resolve path → create parent directories → write content → notify
|
||||
/// mention index → check graduated checks → return result.
|
||||
pub struct Write;
|
||||
|
||||
impl Tool for Write {
|
||||
@@ -37,6 +42,7 @@ impl Tool for Write {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let rel = crate::tools::arg_str(args, "path")?;
|
||||
let content = crate::tools::arg_str(args, "content")?;
|
||||
@@ -47,12 +53,15 @@ impl Tool for Write {
|
||||
}
|
||||
fs::write(&path, &content)?;
|
||||
|
||||
info!(rel = %rel, bytes = content.len(), "file written");
|
||||
|
||||
// Notify mention index
|
||||
ctx.mention_index.push(path.display().to_string());
|
||||
|
||||
// Check graduated checks
|
||||
let matched = check_graduated_checks(&rel, &content, &ctx.graduated_checks);
|
||||
if !matched.is_empty() {
|
||||
debug!(checks = ?matched, "graduated checks triggered for write");
|
||||
return Ok(format!(
|
||||
"Written {} bytes to '{}'. Note: graduated checks triggered: {}",
|
||||
content.len(),
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
//! Git credential management tool.
|
||||
//!
|
||||
//! Provides store, list, and erase actions for git credentials
|
||||
//! by communicating with `git credential approve/reject` via stdin.
|
||||
|
||||
use crate::tools::{execute_cmd, Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
use tracing::{info, instrument};
|
||||
|
||||
/// Tool that manages git credentials (store, list, erase).
|
||||
///
|
||||
/// Interacts with the git credential helper protocol via `git credential approve`
|
||||
/// (for storing) and `git credential reject` (for erasing). The `list` action
|
||||
/// reads the global git config.
|
||||
pub struct GitCred;
|
||||
|
||||
impl Tool for GitCred {
|
||||
@@ -43,8 +52,10 @@ impl Tool for GitCred {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, _ctx, args))]
|
||||
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let action = crate::tools::arg_str(args, "action")?;
|
||||
info!(action, "git_cred invoked");
|
||||
|
||||
match action.as_str() {
|
||||
"store" => {
|
||||
@@ -60,6 +71,7 @@ impl Tool for GitCred {
|
||||
stdin.write_all(input.as_bytes())?;
|
||||
}
|
||||
child.wait()?;
|
||||
info!("credential stored for {url}");
|
||||
Ok(format!("Credential stored for {url}"))
|
||||
}
|
||||
"list" => {
|
||||
@@ -79,6 +91,7 @@ impl Tool for GitCred {
|
||||
stdin.write_all(input.as_bytes())?;
|
||||
}
|
||||
child.wait()?;
|
||||
info!("credential erased for {url}");
|
||||
Ok(format!("Credential erased for {url}"))
|
||||
}
|
||||
_ => anyhow::bail!("unknown action: {}", action),
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
//! Git operator tool — commit, push, pull, branch operations.
|
||||
//!
|
||||
//! Wraps git subcommands with a safety filter that blocks destructive
|
||||
//! operations (e.g. force-push, reset --hard) via `check_git_destructive`.
|
||||
|
||||
use crate::tools::shell_filter::git::check_git_destructive;
|
||||
use crate::tools::{execute_cmd, Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, warn, instrument};
|
||||
|
||||
/// Tool that executes safe git operations (status, log, diff, commit, branch, etc.).
|
||||
///
|
||||
/// Flow: parse operation + args → safety filter check → spawn `git` subprocess → return output.
|
||||
/// Destructive commands are blocked before execution.
|
||||
pub struct GitOperator;
|
||||
|
||||
impl Tool for GitOperator {
|
||||
@@ -35,6 +43,7 @@ impl Tool for GitOperator {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, _ctx, args))]
|
||||
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let operation = crate::tools::arg_str(args, "operation")?;
|
||||
let extra_args: Vec<String> = args
|
||||
@@ -47,9 +56,12 @@ impl Tool for GitOperator {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
info!(operation, extra_args = ?extra_args, "git_operator invoked");
|
||||
|
||||
// Safety filter: block destructive git operations
|
||||
let cmd_str = format!("git {} {}", operation, extra_args.join(" "));
|
||||
if let Err(e) = check_git_destructive(&cmd_str) {
|
||||
warn!(cmd = %cmd_str, error = %e, "destructive git operation blocked");
|
||||
anyhow::bail!("blocked: {e}");
|
||||
}
|
||||
|
||||
@@ -60,6 +72,7 @@ impl Tool for GitOperator {
|
||||
}
|
||||
|
||||
let output = execute_cmd(&mut cmd)?;
|
||||
info!(operation, "git_operator completed");
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
//! Git worktree management tool.
|
||||
//!
|
||||
//! Supports add, list, remove, and prune actions for managing
|
||||
//! multiple working trees attached to a single repository.
|
||||
|
||||
use crate::tools::{execute_cmd, Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, instrument};
|
||||
|
||||
/// Tool that manages git worktrees (add, list, remove, prune).
|
||||
///
|
||||
/// Wraps `git worktree` subcommands. For `add`, requires both a
|
||||
/// path and a branch name. For `remove`, requires a path.
|
||||
pub struct GitWorktree;
|
||||
|
||||
impl Tool for GitWorktree {
|
||||
@@ -37,13 +45,16 @@ impl Tool for GitWorktree {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, _ctx, args))]
|
||||
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let action = crate::tools::arg_str(args, "action")?;
|
||||
info!(action, "git_worktree invoked");
|
||||
|
||||
match action.as_str() {
|
||||
"add" => {
|
||||
let path = crate::tools::arg_str(args, "path")?;
|
||||
let branch = crate::tools::arg_str(args, "branch")?;
|
||||
info!(path, branch, "adding worktree");
|
||||
let output = execute_cmd(
|
||||
std::process::Command::new("git")
|
||||
.args(["worktree", "add", &path, &branch]),
|
||||
@@ -58,12 +69,14 @@ impl Tool for GitWorktree {
|
||||
}
|
||||
"remove" => {
|
||||
let path = crate::tools::arg_str(args, "path")?;
|
||||
info!(path, "removing worktree");
|
||||
let output = execute_cmd(
|
||||
std::process::Command::new("git").args(["worktree", "remove", &path]),
|
||||
)?;
|
||||
Ok(output)
|
||||
}
|
||||
"prune" => {
|
||||
info!("pruning stale worktree metadata");
|
||||
let output = execute_cmd(
|
||||
std::process::Command::new("git").args(["worktree", "prune"]),
|
||||
)?;
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
//! Get completion suggestions from LSP.
|
||||
//!
|
||||
//! Sends a `textDocument/completion` request to the connected language
|
||||
//! server for a given file position.
|
||||
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, error, instrument};
|
||||
|
||||
/// Tool that requests code completion suggestions from an LSP server.
|
||||
///
|
||||
/// Flow: parse language/path/line/character → lock LSP manager → find client
|
||||
/// → send `textDocument/completion` → return pretty-printed JSON response.
|
||||
pub struct LspCompletion;
|
||||
|
||||
impl Tool for LspCompletion {
|
||||
@@ -40,16 +48,19 @@ impl Tool for LspCompletion {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let language = crate::tools::arg_str(args, "language")?;
|
||||
let path = crate::tools::arg_str(args, "path")?;
|
||||
let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
let character = args.get("character").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
|
||||
info!(language, path, line, character, "LSP completion requested");
|
||||
|
||||
let manager = match ctx.lsp_manager.lock() {
|
||||
Ok(g) => g,
|
||||
Err(poisoned) => {
|
||||
tracing::error!("LSP manager mutex poisoned, recovering");
|
||||
error!("LSP manager mutex poisoned, recovering");
|
||||
poisoned.into_inner()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
//! Connect to an LSP language server.
|
||||
//!
|
||||
//! Starts a new language server process and registers it in the
|
||||
//! shared LSP manager for subsequent tool invocations.
|
||||
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, error, instrument};
|
||||
|
||||
/// Tool that connects to an LSP language server for a given language.
|
||||
///
|
||||
/// Flow: parse language + command + args → lock LSP manager → call
|
||||
/// `manager.start()` → confirm connection in the response string.
|
||||
pub struct LspConnect;
|
||||
|
||||
impl Tool for LspConnect {
|
||||
@@ -37,6 +45,7 @@ impl Tool for LspConnect {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let language = crate::tools::arg_str(args, "language")?;
|
||||
let command = crate::tools::arg_str(args, "command")?;
|
||||
@@ -46,15 +55,18 @@ impl Tool for LspConnect {
|
||||
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
info!(language, command, extra_args = ?extra_args, "LSP connect requested");
|
||||
|
||||
let mut manager = match ctx.lsp_manager.lock() {
|
||||
Ok(g) => g,
|
||||
Err(poisoned) => {
|
||||
tracing::error!("LSP manager mutex poisoned, recovering");
|
||||
error!("LSP manager mutex poisoned, recovering");
|
||||
poisoned.into_inner()
|
||||
}
|
||||
};
|
||||
manager.start(&language, &command, &extra_args)?;
|
||||
|
||||
info!(language, "LSP connected successfully");
|
||||
Ok(format!("Connected LSP for '{language}'"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
//! Go-to-definition via LSP.
|
||||
//!
|
||||
//! Sends a `textDocument/definition` request to the connected language
|
||||
//! server for a symbol at a given file position.
|
||||
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, error, instrument};
|
||||
|
||||
/// Tool that resolves a symbol's definition location via LSP.
|
||||
///
|
||||
/// Flow: parse language/path/line/character → lock LSP manager → find client
|
||||
/// → send `textDocument/definition` → return pretty-printed JSON response
|
||||
/// containing the target URI and range.
|
||||
pub struct LspDefinition;
|
||||
|
||||
impl Tool for LspDefinition {
|
||||
@@ -40,16 +49,19 @@ impl Tool for LspDefinition {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let language = crate::tools::arg_str(args, "language")?;
|
||||
let path = crate::tools::arg_str(args, "path")?;
|
||||
let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
let character = args.get("character").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
|
||||
info!(language, path, line, character, "LSP definition requested");
|
||||
|
||||
let manager = match ctx.lsp_manager.lock() {
|
||||
Ok(g) => g,
|
||||
Err(poisoned) => {
|
||||
tracing::error!("LSP manager mutex poisoned, recovering");
|
||||
error!("LSP manager mutex poisoned, recovering");
|
||||
poisoned.into_inner()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
//! Get diagnostics from LSP.
|
||||
//!
|
||||
//! Sends a `textDocument/diagnostic` request to the connected language
|
||||
//! server for a given file and returns errors, warnings, and other
|
||||
//! diagnostics.
|
||||
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, error, instrument};
|
||||
|
||||
/// Tool that retrieves diagnostics (errors, warnings) from the LSP for a file.
|
||||
///
|
||||
/// Flow: parse language/path → lock LSP manager → find client
|
||||
/// → send `textDocument/diagnostic` → return pretty-printed JSON response.
|
||||
pub struct LspDiagnostics;
|
||||
|
||||
impl Tool for LspDiagnostics {
|
||||
@@ -32,14 +41,17 @@ impl Tool for LspDiagnostics {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let language = crate::tools::arg_str(args, "language")?;
|
||||
let path = crate::tools::arg_str(args, "path")?;
|
||||
|
||||
info!(language, path, "LSP diagnostics requested");
|
||||
|
||||
let manager = match ctx.lsp_manager.lock() {
|
||||
Ok(g) => g,
|
||||
Err(poisoned) => {
|
||||
tracing::error!("LSP manager mutex poisoned, recovering");
|
||||
error!("LSP manager mutex poisoned, recovering");
|
||||
poisoned.into_inner()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
//! Disconnect from an LSP language server.
|
||||
//!
|
||||
//! Removes the registered LSP client for a given language from
|
||||
//! the shared LSP manager.
|
||||
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, error, instrument};
|
||||
|
||||
/// Tool that disconnects an LSP language server for a given language.
|
||||
///
|
||||
/// Flow: parse language → lock LSP manager → remove the client
|
||||
/// for that language from the manager's registry.
|
||||
pub struct LspDisconnect;
|
||||
|
||||
impl Tool for LspDisconnect {
|
||||
@@ -28,15 +36,19 @@ impl Tool for LspDisconnect {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let language = crate::tools::arg_str(args, "language")?;
|
||||
info!(language, "LSP disconnect requested");
|
||||
|
||||
let _manager = match ctx.lsp_manager.lock() {
|
||||
Ok(g) => g,
|
||||
Err(poisoned) => {
|
||||
tracing::error!("LSP manager mutex poisoned, recovering");
|
||||
error!("LSP manager mutex poisoned, recovering");
|
||||
poisoned.into_inner()
|
||||
}
|
||||
};
|
||||
info!(language, "LSP disconnected");
|
||||
Ok(format!("Disconnected LSP for '{language}'"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
//! Get hover information from LSP.
|
||||
//!
|
||||
//! Sends a `textDocument/hover` request to the connected language
|
||||
//! server for a symbol at a given file position.
|
||||
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, error, instrument};
|
||||
|
||||
/// Tool that retrieves hover information for a symbol at a position via LSP.
|
||||
///
|
||||
/// Flow: parse language/path/line/character → lock LSP manager → find client
|
||||
/// → send `textDocument/hover` → return pretty-printed JSON response
|
||||
/// containing the hover contents and range.
|
||||
pub struct LspHover;
|
||||
|
||||
impl Tool for LspHover {
|
||||
@@ -40,16 +49,19 @@ impl Tool for LspHover {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let language = crate::tools::arg_str(args, "language")?;
|
||||
let path = crate::tools::arg_str(args, "path")?;
|
||||
let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
let character = args.get("character").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
|
||||
info!(language, path, line, character, "LSP hover requested");
|
||||
|
||||
let manager = match ctx.lsp_manager.lock() {
|
||||
Ok(g) => g,
|
||||
Err(poisoned) => {
|
||||
tracing::error!("LSP manager mutex poisoned, recovering");
|
||||
error!("LSP manager mutex poisoned, recovering");
|
||||
poisoned.into_inner()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
//! Find references via LSP.
|
||||
//!
|
||||
//! Sends a `textDocument/references` request to the connected language
|
||||
//! server for a symbol at a given file position.
|
||||
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, error, instrument};
|
||||
|
||||
/// Tool that finds all references to a symbol at a position via LSP.
|
||||
///
|
||||
/// Flow: parse language/path/line/character → lock LSP manager → find client
|
||||
/// → send `textDocument/references` → return pretty-printed JSON response
|
||||
/// containing all reference locations.
|
||||
pub struct LspReferences;
|
||||
|
||||
impl Tool for LspReferences {
|
||||
@@ -40,16 +49,19 @@ impl Tool for LspReferences {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let language = crate::tools::arg_str(args, "language")?;
|
||||
let path = crate::tools::arg_str(args, "path")?;
|
||||
let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
let character = args.get("character").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
|
||||
info!(language, path, line, character, "LSP references requested");
|
||||
|
||||
let manager = match ctx.lsp_manager.lock() {
|
||||
Ok(g) => g,
|
||||
Err(poisoned) => {
|
||||
tracing::error!("LSP manager mutex poisoned, recovering");
|
||||
error!("LSP manager mutex poisoned, recovering");
|
||||
poisoned.into_inner()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
//! Delete a memory by name.
|
||||
//!
|
||||
//! Removes a previously-saved persistent memory file from the
|
||||
//! memory directory via the `MemoryRepository`.
|
||||
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, instrument};
|
||||
|
||||
use zesdex_domain::cms::MemoryRepository;
|
||||
|
||||
/// Tool that deletes a saved memory by name.
|
||||
///
|
||||
/// Flow: parse name → instantiate `MarkdownMemoryRepository` → call
|
||||
/// `repo.delete()` with the memory directory and name → confirm deletion.
|
||||
pub struct Forget;
|
||||
|
||||
impl Tool for Forget {
|
||||
@@ -30,10 +38,13 @@ impl Tool for Forget {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let name = crate::tools::arg_str(args, "name")?;
|
||||
info!(name, "forget invoked");
|
||||
let repo = crate::persistence::cms::memory_repo::MarkdownMemoryRepository::new();
|
||||
repo.delete(&ctx.memory_dir, &name)?;
|
||||
info!(name, "memory deleted");
|
||||
Ok(format!("Memory '{}' deleted", name))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
//! Recall previously saved memories.
|
||||
//!
|
||||
//! Loads a specific memory by name or lists all available memories
|
||||
//! from the memory directory via `MemoryRepository`.
|
||||
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, instrument};
|
||||
|
||||
use zesdex_domain::cms::MemoryRepository;
|
||||
|
||||
/// Tool that lists or searches saved persistent memories.
|
||||
///
|
||||
/// Flow: if a `name` argument is provided, loads that specific memory
|
||||
/// and returns it as pretty-printed JSON. Otherwise lists all available
|
||||
/// memory names from the repository.
|
||||
pub struct Recall;
|
||||
|
||||
impl Tool for Recall {
|
||||
@@ -33,17 +42,21 @@ impl Tool for Recall {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let repo = crate::persistence::cms::memory_repo::MarkdownMemoryRepository::new();
|
||||
|
||||
let specific_name = args.get("name").and_then(|v| v.as_str());
|
||||
|
||||
if let Some(name) = specific_name {
|
||||
info!(name, "recall loading specific memory");
|
||||
let memory = repo.load(&ctx.memory_dir, name)?;
|
||||
Ok(serde_json::to_string_pretty(&memory)?)
|
||||
} else {
|
||||
info!("recall listing all memories");
|
||||
let names = repo.list(&ctx.memory_dir)?;
|
||||
if names.is_empty() {
|
||||
info!("no memories found");
|
||||
return Ok("No memories saved yet".to_string());
|
||||
}
|
||||
Ok(format!("Available memories:\n{}", names.join("\n")))
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
//! Remember a lesson or fact as persistent memory.
|
||||
//!
|
||||
//! Constructs a `Memory` struct from tool arguments and persists it
|
||||
//! via `MarkdownMemoryRepository` to the memory directory.
|
||||
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, instrument};
|
||||
|
||||
use zesdex_domain::cms::{Memory, MemoryRepository};
|
||||
|
||||
/// Tool that saves a lesson or fact to persistent memory.
|
||||
///
|
||||
/// Flow: parse name/description/content/kind → construct a `Memory` struct
|
||||
/// with timestamps → instantiate `MarkdownMemoryRepository` → call
|
||||
/// `repo.save()` with the memory directory → confirm save.
|
||||
pub struct Remember;
|
||||
|
||||
impl Tool for Remember {
|
||||
@@ -43,6 +52,7 @@ impl Tool for Remember {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let name = crate::tools::arg_str(args, "name")?;
|
||||
let description = crate::tools::arg_str(args, "description")?;
|
||||
@@ -53,6 +63,8 @@ impl Tool for Remember {
|
||||
.unwrap_or("reference")
|
||||
.to_string();
|
||||
|
||||
info!(name, kind, "remember invoked");
|
||||
|
||||
let memory = Memory {
|
||||
name: name.clone(),
|
||||
description,
|
||||
@@ -71,6 +83,7 @@ impl Tool for Remember {
|
||||
let repo = crate::persistence::cms::memory_repo::MarkdownMemoryRepository::new();
|
||||
repo.save(&ctx.memory_dir, &memory)?;
|
||||
|
||||
info!(name, "memory saved");
|
||||
Ok(format!("Memory '{}' saved", name))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use sha2::Digest;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::{debug, info, instrument, warn};
|
||||
|
||||
pub mod bash_tools;
|
||||
pub mod fs;
|
||||
@@ -220,12 +221,18 @@ pub fn arg_str(args: &Value, name: &str) -> Result<String> {
|
||||
}
|
||||
|
||||
/// Execute a `std::process::Command` and return its combined stdout/stderr.
|
||||
///
|
||||
/// Flow: spawn → collect stdout + stderr → check exit code → return combined output
|
||||
/// or bail with the error message.
|
||||
#[instrument(skip(cmd))]
|
||||
pub fn execute_cmd(cmd: &mut std::process::Command) -> Result<String> {
|
||||
let output = cmd
|
||||
.output()
|
||||
.map_err(|e| anyhow::anyhow!("command execution failed: {e}"))?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
let stdout_len = stdout.len();
|
||||
let stderr_len = stderr.len();
|
||||
let combined = if stderr.is_empty() {
|
||||
stdout
|
||||
} else {
|
||||
@@ -233,16 +240,22 @@ pub fn execute_cmd(cmd: &mut std::process::Command) -> Result<String> {
|
||||
.trim()
|
||||
.to_string()
|
||||
};
|
||||
let code = output.status.code().unwrap_or(-1);
|
||||
if output.status.success() {
|
||||
info!(exit_code = code, stdout_len, "command succeeded");
|
||||
Ok(combined)
|
||||
} else {
|
||||
let code = output.status.code().unwrap_or(-1);
|
||||
warn!(exit_code = code, stderr_len, "command failed");
|
||||
anyhow::bail!("command failed with exit code {code}:\n{combined}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a tool-supplied relative path to an absolute path within a workspace
|
||||
/// root, rejecting escapes.
|
||||
///
|
||||
/// Flow: parse optional `[idx]` prefix → join with workspace root → canonicalize
|
||||
/// → verify result is inside one of the workspace roots.
|
||||
#[instrument(skip(workspaces))]
|
||||
pub fn resolve_path(workspaces: &[PathBuf], rel: &str) -> Result<PathBuf> {
|
||||
let (ws_idx, path) = if rel.starts_with('[') {
|
||||
let close = rel
|
||||
@@ -284,15 +297,21 @@ pub fn resolve_path(workspaces: &[PathBuf], rel: &str) -> Result<PathBuf> {
|
||||
}
|
||||
resolved
|
||||
};
|
||||
debug!(resolved = %canon.display(), "path resolved within workspace");
|
||||
if workspaces.iter().any(|w| canon.starts_with(w)) {
|
||||
Ok(canon)
|
||||
} else {
|
||||
warn!(path = %canon.display(), rel = rel, "path is outside all workspace roots");
|
||||
anyhow::bail!("path '{rel}' is outside all workspace roots")
|
||||
}
|
||||
}
|
||||
|
||||
/// After a successful write/edit tool run, compute content hash and byte
|
||||
/// delta, then persist an `EditLogEntry` to the session's edit log.
|
||||
///
|
||||
/// Flow: extract path/content/reason from args → compute SHA-256 of content
|
||||
/// → compute byte delta → build `EditLogEntry` → open repo → append entry.
|
||||
#[instrument(skip(args, session_dir))]
|
||||
pub fn log_write_edit_tool(
|
||||
args: &serde_json::Value,
|
||||
tool_name: &str,
|
||||
@@ -334,6 +353,9 @@ pub fn log_write_edit_tool(
|
||||
let repo = crate::persistence::cms::edit_log_repo::JsonlEditLogRepository::new();
|
||||
if let Ok(mut el) = repo.open(session_dir) {
|
||||
let _ = repo.append(session_dir, &mut el, entry);
|
||||
debug!(tool = tool_name, path = path, "edit-log entry persisted");
|
||||
} else {
|
||||
warn!(tool = tool_name, path = path, "failed to open edit-log repository");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
//! Plan management tools — enter and mark ready.
|
||||
//!
|
||||
//! These tools implement a two-phase planning workflow: `PlanEnter` presents a
|
||||
//! structured plan to the user for approval, and `PlanReady` signals that the
|
||||
//! plan is finalised and execution may begin.
|
||||
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::info;
|
||||
use tracing::{info, instrument, warn};
|
||||
|
||||
/// Enter a planning phase — persist a structured plan and notify the user.
|
||||
///
|
||||
/// Flow: extract plan text → write to `{session_dir}/PLAN.md` → push a
|
||||
/// `PlanUpdate` turn event → return plan length summary.
|
||||
pub struct PlanEnter;
|
||||
|
||||
impl Tool for PlanEnter {
|
||||
@@ -29,9 +37,11 @@ impl Tool for PlanEnter {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let plan_text = crate::tools::arg_str(args, "plan")?;
|
||||
|
||||
info!(plan_len = plan_text.len(), "plan_enter called");
|
||||
|
||||
let plan_path = ctx.session_dir.join("PLAN.md");
|
||||
let _ = std::fs::write(&plan_path, &plan_text);
|
||||
if let Some(events) = &ctx.turn_events {
|
||||
@@ -46,6 +56,10 @@ impl Tool for PlanEnter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Signal that the plan is ready and execution can begin.
|
||||
///
|
||||
/// Flow: extract plan content → persist to a timestamped file in
|
||||
/// `{session_dir}/plans/` → overwrite `PLAN.md` → push `PlanUpdate` event.
|
||||
pub struct PlanReady;
|
||||
|
||||
impl Tool for PlanReady {
|
||||
@@ -70,9 +84,11 @@ impl Tool for PlanReady {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let plan_content = crate::tools::arg_str(args, "plan")?;
|
||||
info!("plan ready: {} chars", plan_content.len());
|
||||
info!(plan_len = plan_content.len(), "plan_ready called");
|
||||
|
||||
// Persist the plan to session directory for reference
|
||||
let plan_dir = ctx.session_dir.join("plans");
|
||||
if std::fs::create_dir_all(&plan_dir).is_ok() {
|
||||
@@ -80,7 +96,9 @@ impl Tool for PlanReady {
|
||||
let path = plan_dir.join(&filename);
|
||||
std::fs::write(&path, &plan_content)
|
||||
.map_err(|e| anyhow::anyhow!("failed to save plan: {e}"))?;
|
||||
|
||||
|
||||
info!(filename = %filename, "plan persisted to disk");
|
||||
|
||||
// Also save the latest plan
|
||||
let plan_path = ctx.session_dir.join("PLAN.md");
|
||||
let _ = std::fs::write(&plan_path, &plan_content);
|
||||
@@ -91,6 +109,7 @@ impl Tool for PlanReady {
|
||||
|
||||
Ok(format!("Plan saved to {filename}. Starting execution."))
|
||||
} else {
|
||||
warn!("failed to create plans directory");
|
||||
Ok("Plan is ready. Starting execution.".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
//! Text search tools: Grep (line matching) and Glob (filename pattern matching).
|
||||
//!
|
||||
//! `Grep` searches file contents recursively with regex or literal fallback.
|
||||
//! `Glob` lists files matching a given glob pattern under a directory.
|
||||
|
||||
use crate::tools::{resolve_path, Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
@@ -6,7 +9,12 @@ use globset::{GlobBuilder, GlobSetBuilder};
|
||||
use ignore::Walk;
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
use tracing::{debug, info, instrument, warn};
|
||||
|
||||
/// Search for a regex (or literal) pattern in file contents under a directory.
|
||||
///
|
||||
/// Flow: resolve path → walk files → regex-match each line → collect results.
|
||||
/// Falls back to substring search when the pattern is not a valid regex.
|
||||
pub struct Grep;
|
||||
|
||||
impl Tool for Grep {
|
||||
@@ -35,18 +43,22 @@ impl Tool for Grep {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let pattern = crate::tools::arg_str(args, "pattern")?;
|
||||
let rel = crate::tools::arg_str(args, "path")?;
|
||||
let path = resolve_path(&ctx.workspaces, &rel)?;
|
||||
|
||||
if !path.exists() {
|
||||
warn!(rel = %rel, "grep path does not exist");
|
||||
anyhow::bail!("path '{rel}' does not exist");
|
||||
}
|
||||
if !path.is_dir() {
|
||||
warn!(rel = %rel, "grep path is not a directory");
|
||||
anyhow::bail!("path '{rel}' is not a directory");
|
||||
}
|
||||
|
||||
info!(pattern = %pattern, root = %rel, "grep search starting");
|
||||
let mut results: Vec<(String, usize, String)> = Vec::new();
|
||||
for entry in Walk::new(&path).flatten() {
|
||||
let file_path = entry.path();
|
||||
@@ -75,8 +87,10 @@ impl Tool for Grep {
|
||||
}
|
||||
|
||||
if results.is_empty() {
|
||||
debug!(pattern = %pattern, "grep found no matches");
|
||||
return Ok(format!("no matches found for '{pattern}' in {rel}"));
|
||||
}
|
||||
info!(match_count = results.len(), "grep search completed");
|
||||
let output = results
|
||||
.iter()
|
||||
.map(|(f, line, text)| format!("{f}:{line}:{text}"))
|
||||
@@ -86,6 +100,10 @@ impl Tool for Grep {
|
||||
}
|
||||
}
|
||||
|
||||
/// List files matching a glob pattern under a directory root.
|
||||
///
|
||||
/// Flow: resolve root → build glob set from pattern → walk files → filter by
|
||||
/// glob set → sort results.
|
||||
pub struct Glob;
|
||||
|
||||
impl Tool for Glob {
|
||||
@@ -114,15 +132,18 @@ impl Tool for Glob {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let pat_str = crate::tools::arg_str(args, "pattern")?;
|
||||
let rel = crate::tools::arg_str(args, "path")?;
|
||||
let root = resolve_path(&ctx.workspaces, &rel)?;
|
||||
|
||||
if !root.exists() || !root.is_dir() {
|
||||
warn!(rel = %rel, "glob root is not a valid directory");
|
||||
anyhow::bail!("path '{rel}' is not a valid directory");
|
||||
}
|
||||
|
||||
info!(pattern = %pat_str, root = %rel, "glob search starting");
|
||||
let mut builder = GlobSetBuilder::new();
|
||||
let full_pattern = root.join(&pat_str).display().to_string();
|
||||
builder.add(
|
||||
@@ -146,8 +167,10 @@ impl Tool for Glob {
|
||||
}
|
||||
matches.sort();
|
||||
if matches.is_empty() {
|
||||
debug!(pattern = %pat_str, "glob found no matches");
|
||||
return Ok(format!("no files match '{pat_str}' in {rel}"));
|
||||
}
|
||||
info!(match_count = matches.len(), "glob search completed");
|
||||
Ok(matches.join("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
//! Sequential thinking tool — step-by-step reasoning.
|
||||
//!
|
||||
//! Allows the agent to record one step of a chain-of-thought reasoning process,
|
||||
//! tracking progress through a planned number of steps.
|
||||
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, instrument};
|
||||
|
||||
/// Perform sequential / step-by-step reasoning (chain-of-thought).
|
||||
///
|
||||
/// Flow: extract thought, step number, total steps, and continuation flag from
|
||||
/// args → format into a reasoning step response.
|
||||
pub struct SeqThink;
|
||||
|
||||
impl Tool for SeqThink {
|
||||
@@ -40,6 +48,7 @@ impl Tool for SeqThink {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, _ctx, args))]
|
||||
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let thought = crate::tools::arg_str(args, "thought")?;
|
||||
let step = args
|
||||
@@ -55,6 +64,8 @@ impl Tool for SeqThink {
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
info!(step, total, next_needed, "sequential think step");
|
||||
|
||||
Ok(format!(
|
||||
"Step {}/{}: {}\n{}",
|
||||
step,
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
//! Bash-shell execution tool with safety filters and optional timeout.
|
||||
//!
|
||||
//! Executes shell commands via `bash -c`. Supports a timeout, background
|
||||
//! execution, and safety filters that block destructive git operations.
|
||||
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, info, instrument, warn};
|
||||
|
||||
/// Execute a shell command via `bash -c`.
|
||||
///
|
||||
/// Flow: parse command + timeout + background flag → check git safety filter
|
||||
/// → either spawn background job or run synchronously with timeout loop →
|
||||
/// capture output → return result.
|
||||
pub struct Bash;
|
||||
|
||||
impl Tool for Bash {
|
||||
@@ -42,6 +51,7 @@ impl Tool for Bash {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, _ctx, args))]
|
||||
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let cmd = crate::tools::arg_str(args, "command")?;
|
||||
let timeout_ms = args
|
||||
@@ -50,6 +60,8 @@ impl Tool for Bash {
|
||||
.unwrap_or(120_000)
|
||||
.min(600_000);
|
||||
|
||||
info!(cmd_len = cmd.len(), timeout_ms, "bash execution starting");
|
||||
|
||||
// Safety filter: block destructive git operations
|
||||
crate::tools::shell_filter::git::check_git_destructive(&cmd)
|
||||
.map_err(|e| anyhow::anyhow!("blocked: {e}"))?;
|
||||
@@ -62,6 +74,7 @@ impl Tool for Bash {
|
||||
if run_in_background {
|
||||
let job = crate::bgbash::job::spawn_bash_job(cmd);
|
||||
crate::bgbash::control::bash_control().register(job.clone());
|
||||
info!(job_id = %job.id, "bash spawned in background");
|
||||
return Ok(format!("Background job: {}", job.id));
|
||||
}
|
||||
|
||||
@@ -92,12 +105,14 @@ impl Tool for Bash {
|
||||
};
|
||||
let trimmed = combined.trim().to_string();
|
||||
if status.success() {
|
||||
debug!(elapsed_secs = elapsed, "bash command completed successfully");
|
||||
return Ok(if trimmed.is_empty() {
|
||||
format!("Command completed in {elapsed:.2}s (exit code 0)")
|
||||
} else {
|
||||
format!("{trimmed}\n\nExit code: 0 ({elapsed:.2}s)")
|
||||
});
|
||||
}
|
||||
warn!(exit_code = status.code().unwrap_or(-1), elapsed_secs = elapsed, "bash command failed");
|
||||
return Ok(format!(
|
||||
"{}\n\nExit code: {} ({:.2}s)",
|
||||
trimmed,
|
||||
@@ -109,6 +124,7 @@ impl Tool for Bash {
|
||||
if start.elapsed() > timeout {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
warn!(timeout_ms, "bash command timed out");
|
||||
anyhow::bail!("command timed out after {timeout_ms}ms");
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
|
||||
@@ -4,8 +4,12 @@
|
||||
//! See the module doc for rationale.
|
||||
|
||||
use regex::Regex;
|
||||
use tracing::instrument;
|
||||
|
||||
/// Paths that are likely to contain credentials.
|
||||
///
|
||||
/// Checks against common credential file locations: SSH keys, `.netrc`,
|
||||
/// cloud provider credentials (AWS, Azure, GCP), Docker config, etc.
|
||||
pub fn is_credential_path(path: &str) -> bool {
|
||||
let patterns = [
|
||||
r"~/.ssh/",
|
||||
@@ -23,6 +27,11 @@ pub fn is_credential_path(path: &str) -> bool {
|
||||
}
|
||||
|
||||
/// Check whether a command reads credential files.
|
||||
///
|
||||
/// Flow: regex-match command for common read commands (`cat`, `head`, etc.)
|
||||
/// with file paths → check each matched path against `is_credential_path` →
|
||||
/// return list of suspected credential reads.
|
||||
#[instrument(skip(cmd))]
|
||||
pub fn check_credential_read(cmd: &str) -> Vec<String> {
|
||||
let re = Regex::new(r#"(?i)(?:cat|head|tail|less|more|vim?|nano|xdg-open|open|type|echo)\s+(~?/[\w/.@-]+)"#)
|
||||
.expect("hardcoded credential-read regex is valid");
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
//! Git operation safety filter — blocks destructive git commands.
|
||||
//!
|
||||
//! Used by the bash tool to prevent accidental or malicious git operations
|
||||
//! that could destroy work (force-push, hard reset, rebase, branch deletion, etc.).
|
||||
|
||||
use tracing::instrument;
|
||||
|
||||
/// Check whether a shell command contains a destructive git operation.
|
||||
///
|
||||
/// Blocks: `git push --force`, `git reset --hard`, `git rebase`, etc.
|
||||
///
|
||||
/// Returns `Ok(())` if the command is safe, or `Err(message)` if a destructive
|
||||
/// pattern was detected.
|
||||
#[instrument(skip(cmd))]
|
||||
pub fn check_git_destructive(cmd: &str) -> Result<(), String> {
|
||||
let cmd_lower = cmd.to_lowercase();
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
//! Agent spawning tools — launch subagents and pipelines.
|
||||
//!
|
||||
//! `SpawnAgents` runs multiple subagents in parallel threads. `SpawnPipeline`
|
||||
//! runs a sequence of agent stages one after another.
|
||||
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::info;
|
||||
use tracing::{debug, info, instrument, warn};
|
||||
|
||||
use zesdex_domain::cms::{AppConfigRepository, SettingsRepository};
|
||||
use zesdex_domain::core::Store;
|
||||
@@ -50,6 +53,7 @@ impl Tool for SpawnAgents {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let agents = args
|
||||
.get("agents")
|
||||
@@ -57,6 +61,7 @@ impl Tool for SpawnAgents {
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 'agents' array"))?;
|
||||
|
||||
info!("Spawning {} agents", agents.len());
|
||||
debug!(agent_count = agents.len(), "parsing agents array");
|
||||
|
||||
// Load LLM credentials once for all agents
|
||||
let store = Store::new();
|
||||
@@ -106,6 +111,7 @@ impl Tool for SpawnAgents {
|
||||
model.clone(),
|
||||
);
|
||||
|
||||
debug!(agent_index = i, access = %access_str, "spawning subagent");
|
||||
let handle = spawn_subagent(subagent_ctx, directive.clone(), access, ctx.clone());
|
||||
handles.push((i, handle));
|
||||
}
|
||||
@@ -117,8 +123,10 @@ impl Tool for SpawnAgents {
|
||||
.join()
|
||||
.map_err(|e| anyhow::anyhow!("subagent {i} panicked: {e:?}"))??;
|
||||
results.push(format!("Agent {i}: {result}"));
|
||||
info!(agent_index = i, "subagent completed");
|
||||
}
|
||||
|
||||
info!("All {} subagents completed", agents.len());
|
||||
Ok(format!(
|
||||
"Spawned {} agents.\n\nResults:\n{}",
|
||||
agents.len(),
|
||||
@@ -163,6 +171,7 @@ impl Tool for SpawnPipeline {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let stages = args
|
||||
.get("stages")
|
||||
@@ -221,13 +230,16 @@ impl Tool for SpawnPipeline {
|
||||
model.clone(),
|
||||
);
|
||||
|
||||
debug!(stage_index = i, access = %access_str, "running pipeline stage");
|
||||
let result = rt.block_on(async {
|
||||
run_agent(subagent_ctx, &directive, access, ctx.clone()).await
|
||||
})?;
|
||||
|
||||
pipeline_result.push_str(&format!("Stage {}: {}\n", i, result));
|
||||
info!(stage_index = i, "pipeline stage completed");
|
||||
}
|
||||
|
||||
info!("Pipeline with {} stages completed", stages.len());
|
||||
Ok(format!(
|
||||
"Pipeline with {} stages completed.\n\n{}",
|
||||
stages.len(),
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
//! Change the working directory for subsequent commands.
|
||||
//!
|
||||
//! Resolves the requested directory against the workspace list and
|
||||
//! sets the process-wide current directory via `std::env::set_current_dir`.
|
||||
|
||||
use crate::tools::{resolve_path, Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, instrument};
|
||||
|
||||
/// Tool that sets the working directory for subsequent tool calls.
|
||||
///
|
||||
/// Flow: parse directory argument → resolve against configured workspaces
|
||||
/// → call `std::env::set_current_dir` → confirm the new directory.
|
||||
pub struct Cd;
|
||||
|
||||
impl Tool for Cd {
|
||||
@@ -28,9 +36,11 @@ impl Tool for Cd {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let dir = crate::tools::arg_str(args, "directory")?;
|
||||
let resolved = resolve_path(&ctx.workspaces, &dir)?;
|
||||
info!(from = %std::env::current_dir().unwrap_or_default().display(), to = %resolved.display(), "cd invoked");
|
||||
std::env::set_current_dir(&resolved)?;
|
||||
Ok(format!("Changed directory to '{}'", resolved.display()))
|
||||
}
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
//! Update the shared directory cache by resolving each path against
|
||||
//! workspaces and storing the resolved paths in `ctx.dir_cache`.
|
||||
//!
|
||||
//! The cache is an `Arc<RwLock<DirCache>>` shared with the TUI and
|
||||
//! other components so they can read the cached listing without
|
||||
//! re-scanning the filesystem.
|
||||
|
||||
use crate::tools::{resolve_path, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use std::path::PathBuf;
|
||||
use tracing::{info, instrument};
|
||||
|
||||
/// Tool that updates the cached directory listing.
|
||||
///
|
||||
/// Flow: parse `paths` array → resolve each against workspaces →
|
||||
/// persist resolved paths into the shared `DirCache` via an
|
||||
/// async write → confirm with the entry count.
|
||||
pub struct DirCacheUpdate;
|
||||
|
||||
impl crate::tools::Tool for DirCacheUpdate {
|
||||
@@ -30,6 +40,7 @@ impl crate::tools::Tool for DirCacheUpdate {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let paths: Vec<String> = args
|
||||
.get("paths")
|
||||
@@ -47,6 +58,7 @@ impl crate::tools::Tool for DirCacheUpdate {
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let count = resolved.len();
|
||||
info!(count, "directory cache update requested");
|
||||
|
||||
// Persist the resolved paths into the shared DirCache so the TUI
|
||||
// and other tools can read the cached listing without re-scanning.
|
||||
@@ -54,6 +66,7 @@ impl crate::tools::Tool for DirCacheUpdate {
|
||||
let rt = tokio::runtime::Runtime::new()?;
|
||||
rt.block_on(async { dc.write().await.set(resolved).await });
|
||||
|
||||
info!(count, "directory cache updated");
|
||||
Ok(format!("Directory cache updated with {} entries", count))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
//! List directory contents.
|
||||
//!
|
||||
//! Resolves a relative path against the workspace list, validates
|
||||
//! it exists and is a directory, then reads and returns sorted entries.
|
||||
|
||||
use crate::tools::{resolve_path, Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, instrument};
|
||||
|
||||
/// Tool that lists files and directories in a given path.
|
||||
///
|
||||
/// Flow: parse path → resolve against workspaces → validate existence
|
||||
/// and type → read directory entries → format with trailing `/` for
|
||||
/// subdirectories → return sorted, newline-separated listing.
|
||||
pub struct DirList;
|
||||
|
||||
impl Tool for DirList {
|
||||
@@ -28,10 +37,13 @@ impl Tool for DirList {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let rel = crate::tools::arg_str(args, "path")?;
|
||||
let path = resolve_path(&ctx.workspaces, &rel)?;
|
||||
|
||||
info!(?path, "dir_list invoked");
|
||||
|
||||
if !path.exists() {
|
||||
anyhow::bail!("path '{rel}' does not exist");
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
//! Simple ping/pong tool for connectivity testing.
|
||||
//!
|
||||
//! Always returns the string `"pong"`. Used by LLM agents to verify
|
||||
//! that the tool harness is reachable and responsive.
|
||||
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, instrument};
|
||||
|
||||
/// Tool that responds to a ping — useful for testing connectivity.
|
||||
///
|
||||
/// Accepts no parameters and always returns `"pong"`.
|
||||
pub struct Pong;
|
||||
|
||||
impl Tool for Pong {
|
||||
@@ -22,7 +29,9 @@ impl Tool for Pong {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, _ctx, _args))]
|
||||
fn run(&self, _ctx: &ToolCtx, _args: &Value) -> Result<String> {
|
||||
info!("pong invoked — responding with 'pong'");
|
||||
Ok("pong".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
//! Mark a TODO item as finished.
|
||||
//!
|
||||
//! Reads the session's `TODO.md`, replaces the matching unchecked
|
||||
//! item with a checked `[x]` entry, writes the file back, and emits
|
||||
//! a `TurnEvent::TodoUpdate` for the TUI.
|
||||
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, instrument};
|
||||
|
||||
/// Tool that marks a TODO item as completed in the session TODO file.
|
||||
///
|
||||
/// Flow: parse item text → read `TODO.md` → find matching line →
|
||||
/// replace `[ ]` / `[high]` / `[medium]` / `[low]` with `[x]` →
|
||||
/// write file → push `TurnEvent::TodoUpdate` if events channel exists.
|
||||
pub struct Todofinish;
|
||||
|
||||
impl Tool for Todofinish {
|
||||
@@ -28,8 +38,10 @@ impl Tool for Todofinish {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let item = crate::tools::arg_str(args, "item")?;
|
||||
info!(item, "todofinish invoked");
|
||||
|
||||
let todo_path = ctx.session_dir.join("TODO.md");
|
||||
let mut content = std::fs::read_to_string(&todo_path).unwrap_or_default();
|
||||
@@ -59,6 +71,9 @@ impl Tool for Todofinish {
|
||||
let mut q = events.lock().unwrap();
|
||||
q.push_back(crate::TurnEvent::TodoUpdate(content));
|
||||
}
|
||||
info!(item, "TODO item completed and written back");
|
||||
} else {
|
||||
info!(item, "TODO item not found in TODO.md — nothing to mark");
|
||||
}
|
||||
|
||||
Ok(format!("TODO completed: {}", item))
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
//! Write a TODO item.
|
||||
//!
|
||||
//! Appends a new unchecked TODO entry to the session's `TODO.md`
|
||||
//! file with an optional priority marker and emits a
|
||||
//! `TurnEvent::TodoUpdate` for the TUI.
|
||||
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, instrument};
|
||||
|
||||
/// Tool that adds an item to the session TODO list.
|
||||
///
|
||||
/// Flow: parse item + optional priority → append `- [priority] item\n`
|
||||
/// to `TODO.md` → write file → push `TurnEvent::TodoUpdate` if events
|
||||
/// channel exists → confirm addition.
|
||||
pub struct Todowrite;
|
||||
|
||||
impl Tool for Todowrite {
|
||||
@@ -33,6 +43,7 @@ impl Tool for Todowrite {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let item = crate::tools::arg_str(args, "item")?;
|
||||
let priority = args
|
||||
@@ -40,6 +51,8 @@ impl Tool for Todowrite {
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("medium");
|
||||
|
||||
info!(item, priority, "todowrite invoked");
|
||||
|
||||
let todo_line = format!("- [{}] {}\n", priority, item);
|
||||
|
||||
let todo_path = ctx.session_dir.join("TODO.md");
|
||||
@@ -53,6 +66,7 @@ impl Tool for Todowrite {
|
||||
q.push_back(crate::TurnEvent::TodoUpdate(content));
|
||||
}
|
||||
|
||||
info!(item, priority, "TODO item added");
|
||||
Ok(format!("[{}] TODO added: {}", priority, item))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
//! Workflow tools — orchestrate multi-step agent workflows and hive-mind convergence.
|
||||
//!
|
||||
//! `WorkflowRun` executes a YAML-defined multi-step workflow. `NoteFinding` and
|
||||
//! `ReadFindings` record and retrieve findings during execution. `HiveMind`
|
||||
//! orchestrates a convergence — multiple parallel agent cycles followed by
|
||||
//! consensus synthesis.
|
||||
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::info;
|
||||
use tracing::{debug, info, instrument, warn};
|
||||
|
||||
use crate::llm::provider::LlmClient;
|
||||
use crate::tools::{arg_str, Tool, ToolCtx};
|
||||
@@ -41,6 +46,7 @@ impl Tool for WorkflowRun {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let yaml = arg_str(args, "workflow_yaml")?;
|
||||
let script = WorkflowScript::parse(&yaml)?;
|
||||
@@ -66,6 +72,7 @@ impl Tool for WorkflowRun {
|
||||
let result: Vec<String> =
|
||||
rt.block_on(async { execute_workflow(&script, ctx, &llm_client).await })?;
|
||||
|
||||
info!(phase_count = result.len(), "Workflow completed");
|
||||
Ok(format!(
|
||||
"Workflow '{}' completed.\n\n{}",
|
||||
script.name,
|
||||
@@ -74,6 +81,10 @@ impl Tool for WorkflowRun {
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a finding during workflow or hive-mind execution.
|
||||
///
|
||||
/// Flow: extract finding text and optional category → prepend `[category]` tag
|
||||
/// → push onto `ctx.workflow_findings` shared list.
|
||||
pub struct NoteFinding;
|
||||
|
||||
impl Tool for NoteFinding {
|
||||
@@ -102,6 +113,7 @@ impl Tool for NoteFinding {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let finding = crate::tools::arg_str(args, "finding")?;
|
||||
let category = args
|
||||
@@ -114,13 +126,19 @@ impl Tool for NoteFinding {
|
||||
if let Some(ref findings) = ctx.workflow_findings {
|
||||
if let Ok(mut guard) = findings.lock() {
|
||||
guard.push(tagged);
|
||||
info!(finding_count = guard.len(), category = %category, "finding recorded");
|
||||
}
|
||||
} else {
|
||||
debug!("no workflow_findings channel available — finding not persisted");
|
||||
}
|
||||
|
||||
Ok(format!("Finding recorded: {finding}"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Read all findings recorded so far in the current workflow.
|
||||
///
|
||||
/// Flow: lock `ctx.workflow_findings` → clone the list → format as numbered output.
|
||||
pub struct ReadFindings;
|
||||
|
||||
impl Tool for ReadFindings {
|
||||
@@ -139,12 +157,16 @@ impl Tool for ReadFindings {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, _args))]
|
||||
fn run(&self, ctx: &ToolCtx, _args: &Value) -> Result<String> {
|
||||
let findings = ctx
|
||||
.workflow_findings
|
||||
.as_ref()
|
||||
.and_then(|f| f.lock().ok())
|
||||
.map(|guard| guard.clone())
|
||||
.map(|guard| {
|
||||
debug!(finding_count = guard.len(), "reading findings");
|
||||
guard.clone()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if findings.is_empty() {
|
||||
@@ -202,6 +224,7 @@ impl Tool for HiveMind {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, ctx, args))]
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let cycles_val = args
|
||||
.get("cycles")
|
||||
@@ -235,6 +258,7 @@ impl Tool for HiveMind {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
info!(cycle_index = cycle_idx, node_count = directives.len(), "executing hive-mind cycle");
|
||||
let cycle = CognitiveCycle {
|
||||
index: cycle_idx as u32,
|
||||
directives,
|
||||
@@ -246,6 +270,7 @@ impl Tool for HiveMind {
|
||||
}
|
||||
|
||||
let node_count = all_node_outputs.len();
|
||||
info!(node_count, "all cycles completed, synthesizing consensus");
|
||||
let consensus =
|
||||
rt.block_on(async { synthesize_consensus(&all_node_outputs, ctx).await })?;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use tracing::info;
|
||||
use tracing::{info, instrument};
|
||||
|
||||
use crate::workflow::hive_mind::types::NodeOutput;
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::workflow::hive_mind::types::NodeOutput;
|
||||
///
|
||||
/// Flow: create docs/runs/ dir → build markdown content → write file.
|
||||
/// This is deterministic (not an LLM step) and never skippable.
|
||||
#[instrument(skip(nodes))]
|
||||
pub fn write_hive_mind_convergence(
|
||||
run_dir: &Path,
|
||||
nodes: &[NodeOutput],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Workflow execution — runs a parsed workflow script phase by phase.
|
||||
|
||||
use anyhow::Result;
|
||||
use tracing::info;
|
||||
use tracing::{info, instrument};
|
||||
|
||||
use crate::llm::provider::LlmClient;
|
||||
use crate::tools::ToolCtx;
|
||||
@@ -11,6 +11,7 @@ use crate::workflow::script::WorkflowScript;
|
||||
/// Execute each phase of a workflow script sequentially.
|
||||
///
|
||||
/// Flow: for each phase → execute_primitive → collect result.
|
||||
#[instrument(skip(tool_ctx, _llm_client))]
|
||||
pub async fn execute_workflow(
|
||||
script: &WorkflowScript,
|
||||
tool_ctx: &ToolCtx,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Workflow script — parse and execute user-defined workflow scripts.
|
||||
|
||||
use anyhow::Result;
|
||||
use tracing::info;
|
||||
use tracing::{info, instrument};
|
||||
|
||||
/// A single phase in a parsed workflow script.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -29,6 +29,7 @@ impl WorkflowScript {
|
||||
/// - name: implement
|
||||
/// directive: "Implement the changes..."
|
||||
/// ```
|
||||
#[instrument]
|
||||
pub fn parse(yaml: &str) -> Result<Self> {
|
||||
let parsed: serde_json::Value = serde_yaml_ng::from_str(yaml)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse workflow YAML: {e}"))?;
|
||||
|
||||
Reference in New Issue
Block a user