pi-agent 1.0.0

Agent runtime with tool calling (Rust port of @earendil-works/pi-agent-core). Streaming agent loop, permission policy, built-in tools (read/write/edit/bash/grep/glob/web_fetch/todo).
Documentation
use async_trait::async_trait;
use serde_json::{json, Value};
use tokio::fs;

use crate::types::{AgentTool, AgentToolResult};

pub struct LsTool;

#[async_trait]
impl AgentTool for LsTool {
    fn name(&self) -> &str {
        "ls"
    }
    fn description(&self) -> &str {
        "List entries in a directory. Returns name and kind (file/dir/symlink)."
    }
    fn parameters(&self) -> Value {
        json!({
            "type": "object",
            "properties": {"path": {"type": "string"}},
            "required": ["path"]
        })
    }
    async fn execute(&self, _id: &str, args: Value) -> Result<AgentToolResult, String> {
        let path = args
            .get("path")
            .and_then(|v| v.as_str())
            .ok_or("missing 'path'")?;
        let mut read = fs::read_dir(path)
            .await
            .map_err(|e| format!("ls {path}: {e}"))?;
        let mut buf = String::new();
        while let Some(entry) = read.next_entry().await.map_err(|e| e.to_string())? {
            let ft = entry.file_type().await.map_err(|e| e.to_string())?;
            let kind = if ft.is_dir() {
                "dir"
            } else if ft.is_symlink() {
                "symlink"
            } else {
                "file"
            };
            buf.push_str(&format!(
                "{}\t{}\n",
                kind,
                entry.file_name().to_string_lossy()
            ));
        }
        Ok(AgentToolResult::text(buf))
    }
}