diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b143382..eabd4564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,33 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +## [0.9.6] - 2026-02-15 + ### Changed - **BREAKING**: `ToolDef` schema field replaced `Vec` with `schemars::Schema` auto-derived from Rust structs via `#[derive(JsonSchema)]` - **BREAKING**: `ParamDef` and `ParamType` removed from `zeph-tools` public API - **BREAKING**: `ToolRegistry::new()` replaced with `ToolRegistry::from_definitions()`; registry no longer hardcodes built-in tools — each executor owns its definitions via `tool_definitions()` +- **BREAKING**: `Channel` trait now requires `ChannelError` enum with typed error handling replacing `anyhow::Result` +- **BREAKING**: `Agent::new()` signature changed to accept new field grouping; agent struct refactored into 5 inner structs for improved organization +- **BREAKING**: `AgentError` enum introduced with 7 typed variants replacing scattered `anyhow::Error` handling - `ToolDef` now includes `InvocationHint` (FencedBlock/ToolCall) so LLM prompt shows exact invocation format per tool - `web_scrape` tool definition includes all parameters (`url`, `select`, `extract`, `limit`) auto-derived from `ScrapeInstruction` - `ShellExecutor` and `WebScrapeExecutor` now implement `tool_definitions()` for single source of truth +- Replaced `tokio` "full" feature with granular features in zeph-core (async-io, macros, rt, sync, time) +- Removed `anyhow` dependency from zeph-channels +- Message persistence now uses `MessageKind` enum instead of `is_summary` bool for qdrant storage + +### Added +- `ChannelError` enum with typed variants for channel operation failures +- `AgentError` enum with 7 typed variants for agent operation failures (streaming, persistence, configuration, etc.) +- Workspace-level `qdrant` feature flag for optional semantic memory support +- Type aliases consolidated into zeph-llm: `EmbedFuture` and `EmbedFn` with typed `LlmError` +- `streaming.rs` and `persistence.rs` modules extracted from agent module for improved code organization +- `MessageKind` enum for distinguishing summary and regular messages in storage + +### Removed +- `anyhow::Result` from Channel trait (replaced with `ChannelError`) +- Direct `anyhow::Error` usage in agent module (replaced with `AgentError`) ## [0.9.5] - 2026-02-14 @@ -699,3 +719,26 @@ let agent = Agent::new(provider, channel, &skills_prompt, executor); - Agent::run() uses channel.recv()/send() instead of direct I/O - Agent calls channel.send_typing() before each LLM request - Agent::run() uses tokio::select! to race channel messages against shutdown signal + +[Unreleased]: https://github.com/bug-ops/zeph/compare/v0.9.6...HEAD +[0.9.6]: https://github.com/bug-ops/zeph/compare/v0.9.5...v0.9.6 +[0.9.5]: https://github.com/bug-ops/zeph/compare/v0.9.4...v0.9.5 +[0.9.4]: https://github.com/bug-ops/zeph/compare/v0.9.3...v0.9.4 +[0.9.3]: https://github.com/bug-ops/zeph/compare/v0.9.2...v0.9.3 +[0.9.2]: https://github.com/bug-ops/zeph/compare/v0.9.1...v0.9.2 +[0.9.1]: https://github.com/bug-ops/zeph/compare/v0.9.0...v0.9.1 +[0.9.0]: https://github.com/bug-ops/zeph/compare/v0.8.2...v0.9.0 +[0.8.2]: https://github.com/bug-ops/zeph/compare/v0.8.1...v0.8.2 +[0.8.1]: https://github.com/bug-ops/zeph/compare/v0.8.0...v0.8.1 +[0.8.0]: https://github.com/bug-ops/zeph/compare/v0.7.1...v0.8.0 +[0.7.1]: https://github.com/bug-ops/zeph/compare/v0.7.0...v0.7.1 +[0.7.0]: https://github.com/bug-ops/zeph/compare/v0.6.0...v0.7.0 +[0.6.0]: https://github.com/bug-ops/zeph/compare/v0.5.0...v0.6.0 +[0.5.0]: https://github.com/bug-ops/zeph/compare/v0.4.3...v0.5.0 +[0.4.3]: https://github.com/bug-ops/zeph/compare/v0.4.2...v0.4.3 +[0.4.2]: https://github.com/bug-ops/zeph/compare/v0.4.1...v0.4.2 +[0.4.1]: https://github.com/bug-ops/zeph/compare/v0.4.0...v0.4.1 +[0.4.0]: https://github.com/bug-ops/zeph/compare/v0.3.0...v0.4.0 +[0.3.0]: https://github.com/bug-ops/zeph/compare/v0.2.0...v0.3.0 +[0.2.0]: https://github.com/bug-ops/zeph/compare/v0.1.0...v0.2.0 +[0.1.0]: https://github.com/bug-ops/zeph/releases/tag/v0.1.0 diff --git a/Cargo.lock b/Cargo.lock index db2038d9..e4286841 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8136,7 +8136,7 @@ dependencies = [ [[package]] name = "zeph" -version = "0.9.5" +version = "0.9.6" dependencies = [ "anyhow", "tempfile", @@ -8158,7 +8158,7 @@ dependencies = [ [[package]] name = "zeph-a2a" -version = "0.9.5" +version = "0.9.6" dependencies = [ "axum 0.8.8", "eventsource-stream", @@ -8181,7 +8181,7 @@ dependencies = [ [[package]] name = "zeph-channels" -version = "0.9.5" +version = "0.9.6" dependencies = [ "criterion", "pulldown-cmark", @@ -8194,7 +8194,7 @@ dependencies = [ [[package]] name = "zeph-core" -version = "0.9.5" +version = "0.9.6" dependencies = [ "age", "anyhow", @@ -8219,7 +8219,7 @@ dependencies = [ [[package]] name = "zeph-index" -version = "0.9.5" +version = "0.9.6" dependencies = [ "blake3", "ignore", @@ -8251,7 +8251,7 @@ dependencies = [ [[package]] name = "zeph-llm" -version = "0.9.5" +version = "0.9.6" dependencies = [ "anyhow", "candle-core", @@ -8273,7 +8273,7 @@ dependencies = [ [[package]] name = "zeph-mcp" -version = "0.9.5" +version = "0.9.6" dependencies = [ "blake3", "qdrant-client", @@ -8290,7 +8290,7 @@ dependencies = [ [[package]] name = "zeph-memory" -version = "0.9.5" +version = "0.9.6" dependencies = [ "anyhow", "qdrant-client", @@ -8307,7 +8307,7 @@ dependencies = [ [[package]] name = "zeph-skills" -version = "0.9.5" +version = "0.9.6" dependencies = [ "anyhow", "blake3", @@ -8325,7 +8325,7 @@ dependencies = [ [[package]] name = "zeph-tools" -version = "0.9.5" +version = "0.9.6" dependencies = [ "dirs", "filetime", @@ -8347,7 +8347,7 @@ dependencies = [ [[package]] name = "zeph-tui" -version = "0.9.5" +version = "0.9.6" dependencies = [ "anyhow", "crossterm", diff --git a/Cargo.toml b/Cargo.toml index c9ba293c..e216ce57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "3" [workspace.package] edition = "2024" rust-version = "1.88" -version = "0.9.5" +version = "0.9.6" authors = ["bug-ops"] license = "MIT" repository = "https://github.com/bug-ops/zeph" @@ -59,16 +59,16 @@ tree-sitter = "0.26" unicode-width = "0.2" url = "2.5" uuid = "1.20" -zeph-a2a = { path = "crates/zeph-a2a", version = "0.9.4" } -zeph-channels = { path = "crates/zeph-channels", version = "0.9.4" } -zeph-core = { path = "crates/zeph-core", version = "0.9.4" } -zeph-index = { path = "crates/zeph-index", version = "0.9.4" } -zeph-llm = { path = "crates/zeph-llm", version = "0.9.4" } -zeph-mcp = { path = "crates/zeph-mcp", version = "0.9.4" } -zeph-memory = { path = "crates/zeph-memory", version = "0.9.4" } -zeph-skills = { path = "crates/zeph-skills", version = "0.9.4" } -zeph-tools = { path = "crates/zeph-tools", version = "0.9.4" } -zeph-tui = { path = "crates/zeph-tui", version = "0.9.4" } +zeph-a2a = { path = "crates/zeph-a2a", version = "0.9.6" } +zeph-channels = { path = "crates/zeph-channels", version = "0.9.6" } +zeph-core = { path = "crates/zeph-core", version = "0.9.6" } +zeph-index = { path = "crates/zeph-index", version = "0.9.6" } +zeph-llm = { path = "crates/zeph-llm", version = "0.9.6" } +zeph-mcp = { path = "crates/zeph-mcp", version = "0.9.6" } +zeph-memory = { path = "crates/zeph-memory", version = "0.9.6" } +zeph-skills = { path = "crates/zeph-skills", version = "0.9.6" } +zeph-tools = { path = "crates/zeph-tools", version = "0.9.6" } +zeph-tui = { path = "crates/zeph-tui", version = "0.9.6" } [workspace.lints.clippy] all = "warn" diff --git a/README.md b/README.md index 34f78f07..e9b73507 100644 --- a/README.md +++ b/README.md @@ -105,11 +105,13 @@ cargo build --release --features tui ## Architecture ``` -zeph (binary) -├── zeph-core — agent loop, config, config hot-reload, context builder, metrics -├── zeph-llm — LlmProvider: Ollama, Claude, OpenAI, Candle, orchestrator -├── zeph-skills — SKILL.md parser, embedding matcher, hot-reload, self-learning -├── zeph-memory — SQLite + Qdrant, semantic recall, summarization +zeph (binary) — bootstrap, AnyChannel dispatch, vault resolution (anyhow for top-level errors) +├── zeph-core — Agent split into 7 submodules (context, streaming, persistence, +│ learning, mcp, index), typed AgentError/ChannelError, config hot-reload +├── zeph-llm — LlmProvider: Ollama, Claude, OpenAI, Candle, orchestrator, +│ typed LlmError, EmbedFuture/EmbedFn type aliases +├── zeph-skills — SKILL.md parser, embedding matcher, hot-reload, self-learning, typed SkillError +├── zeph-memory — SQLite + Qdrant, semantic recall, summarization, typed MemoryError ├── zeph-index — AST-based code indexing, semantic retrieval, repo map (optional) ├── zeph-channels — Telegram adapter (teloxide) with streaming ├── zeph-tools — schemars-driven tool registry (shell, file ops, web scrape), composite dispatch @@ -118,6 +120,12 @@ zeph (binary) └── zeph-tui — ratatui TUI dashboard with live agent metrics (optional) ``` +**Error handling:** Typed errors throughout all library crates -- `AgentError` (7 variants), `ChannelError` (4 variants), `LlmError`, `MemoryError`, `SkillError`. `anyhow` is used only in `main.rs` for top-level orchestration. + +**Agent decomposition:** The agent module in `zeph-core` is split into 7 submodules (`mod.rs`, `context.rs`, `streaming.rs`, `persistence.rs`, `learning.rs`, `mcp.rs`, `index.rs`) with 5 inner field-grouping structs (`MemoryState`, `SkillState`, `ContextState`, `McpState`, `IndexState`). + +**MessageKind enum:** Replaces the previous `is_summary` boolean with an explicit variant-based message classification. + > [!IMPORTANT] > Requires Rust 1.88+ (Edition 2024). Native async traits — no `async-trait` crate dependency. @@ -132,9 +140,10 @@ Deep dive: [Architecture overview](https://bug-ops.github.io/zeph/architecture/o | `mcp` | On | MCP client for external tool servers | | `candle` | On | Local HuggingFace inference (GGUF) | | `orchestrator` | On | Multi-model routing with fallback | +| `qdrant` | On | Qdrant vector search for skills and MCP tools (opt-out) | | `self-learning` | On | Skill evolution system | | `vault-age` | On | Age-encrypted secret storage | -| `index` | Off | AST-based code indexing and semantic retrieval | +| `index` | On | AST-based code indexing and semantic retrieval | | `metal` | Off | Metal GPU acceleration (macOS) | | `tui` | Off | ratatui TUI dashboard with real-time metrics | | `cuda` | Off | CUDA GPU acceleration (Linux) | diff --git a/crates/zeph-core/src/agent/context.rs b/crates/zeph-core/src/agent/context.rs index 46d05bd1..0bd5e84c 100644 --- a/crates/zeph-core/src/agent/context.rs +++ b/crates/zeph-core/src/agent/context.rs @@ -36,7 +36,7 @@ impl Agent anyhow::Result<()> { + pub(super) async fn compact_context(&mut self) -> Result<(), super::error::AgentError> { let preserve_tail = self.context_state.compaction_preserve_tail; if self.messages.len() <= preserve_tail + 1 { @@ -182,7 +182,7 @@ impl Agent anyhow::Result<()> { + pub(super) async fn maybe_compact(&mut self) -> Result<(), super::error::AgentError> { if !self.should_compact() { return Ok(()); } @@ -236,7 +236,7 @@ impl Agent anyhow::Result<()> { + ) -> Result<(), super::error::AgentError> { self.remove_recall_messages(); let Some(memory) = &self.memory_state.memory else { @@ -333,7 +333,7 @@ impl Agent anyhow::Result<()> { + ) -> Result<(), super::error::AgentError> { self.remove_cross_session_messages(); let (Some(memory), Some(cid)) = @@ -380,7 +380,10 @@ impl Agent anyhow::Result<()> { + async fn inject_summaries( + &mut self, + token_budget: usize, + ) -> Result<(), super::error::AgentError> { self.remove_summary_messages(); let (Some(memory), Some(cid)) = @@ -465,7 +468,10 @@ impl Agent anyhow::Result<()> { + pub(super) async fn prepare_context( + &mut self, + query: &str, + ) -> Result<(), super::error::AgentError> { let Some(ref budget) = self.context_state.budget else { return Ok(()); }; diff --git a/crates/zeph-core/src/agent/error.rs b/crates/zeph-core/src/agent/error.rs new file mode 100644 index 00000000..8bbab882 --- /dev/null +++ b/crates/zeph-core/src/agent/error.rs @@ -0,0 +1,23 @@ +#[derive(Debug, thiserror::Error)] +pub enum AgentError { + #[error(transparent)] + Llm(#[from] zeph_llm::LlmError), + + #[error(transparent)] + Channel(#[from] crate::channel::ChannelError), + + #[error(transparent)] + Memory(#[from] zeph_memory::MemoryError), + + #[error(transparent)] + Skill(#[from] zeph_skills::SkillError), + + #[error(transparent)] + Tool(#[from] zeph_tools::executor::ToolError), + + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + + #[error("{0}")] + Other(String), +} diff --git a/crates/zeph-core/src/agent/index.rs b/crates/zeph-core/src/agent/index.rs index db571dc4..e4928614 100644 --- a/crates/zeph-core/src/agent/index.rs +++ b/crates/zeph-core/src/agent/index.rs @@ -5,7 +5,7 @@ impl Agent anyhow::Result<()> { + ) -> Result<(), super::error::AgentError> { let Some(retriever) = &self.index.retriever else { return Ok(()); }; @@ -13,7 +13,10 @@ impl Agent Agent anyhow::Result { + ) -> Result { if self.reflection_used || !self.is_learning_enabled() { return Ok(false); } @@ -113,7 +113,7 @@ impl Agent, - ) -> anyhow::Result<()> { + ) -> Result<(), super::error::AgentError> { if !self.is_learning_enabled() { return Ok(()); } @@ -176,7 +176,7 @@ impl Agent, - ) -> anyhow::Result { + ) -> Result { if let Some(last_time) = memory.sqlite().last_improvement_time(skill_name).await? && let Ok(last) = chrono_parse_sqlite(&last_time) { @@ -221,7 +221,7 @@ impl Agent, - ) -> anyhow::Result { + ) -> Result { let prompt = zeph_skills::evolution::build_improvement_prompt( skill_name, original_body, @@ -257,7 +257,7 @@ impl Agent anyhow::Result<()> { + ) -> Result<(), super::error::AgentError> { let active = memory.sqlite().active_skill_version(skill_name).await?; let predecessor_id = active.as_ref().map(|v| v.id); @@ -364,7 +364,10 @@ impl Agent anyhow::Result<()> { + pub(super) async fn handle_skill_command( + &mut self, + args: &str, + ) -> Result<(), super::error::AgentError> { let parts: Vec<&str> = args.split_whitespace().collect(); match parts.first().copied() { Some("stats") => self.handle_skill_stats().await, @@ -385,7 +388,10 @@ impl Agent anyhow::Result<()> { + pub(super) async fn handle_skill_command( + &mut self, + _args: &str, + ) -> Result<(), super::error::AgentError> { self.channel .send("Self-learning feature is not enabled.") .await?; @@ -393,7 +399,7 @@ impl Agent anyhow::Result<()> { + async fn handle_skill_stats(&mut self) -> Result<(), super::error::AgentError> { use std::fmt::Write; let Some(memory) = &self.memory_state.memory else { @@ -427,7 +433,10 @@ impl Agent) -> anyhow::Result<()> { + async fn handle_skill_versions( + &mut self, + name: Option<&str>, + ) -> Result<(), super::error::AgentError> { use std::fmt::Write; let Some(name) = name else { @@ -466,7 +475,7 @@ impl Agent, version_str: Option<&str>, - ) -> anyhow::Result<()> { + ) -> Result<(), super::error::AgentError> { let (Some(name), Some(ver_str)) = (name, version_str) else { self.channel .send("Usage: /skill activate ") @@ -510,7 +519,10 @@ impl Agent) -> anyhow::Result<()> { + async fn handle_skill_approve( + &mut self, + name: Option<&str>, + ) -> Result<(), super::error::AgentError> { let Some(name) = name else { self.channel.send("Usage: /skill approve ").await?; return Ok(()); @@ -555,7 +567,10 @@ impl Agent) -> anyhow::Result<()> { + async fn handle_skill_reset( + &mut self, + name: Option<&str>, + ) -> Result<(), super::error::AgentError> { let Some(name) = name else { self.channel.send("Usage: /skill reset ").await?; return Ok(()); @@ -596,9 +611,11 @@ pub(super) async fn write_skill_file( skill_name: &str, description: &str, body: &str, -) -> anyhow::Result<()> { +) -> Result<(), super::error::AgentError> { if skill_name.contains('/') || skill_name.contains('\\') || skill_name.contains("..") { - anyhow::bail!("invalid skill name: {skill_name}"); + return Err(super::error::AgentError::Other(format!( + "invalid skill name: {skill_name}" + ))); } for base in skill_paths { let skill_dir = base.join(skill_name); @@ -610,7 +627,9 @@ pub(super) async fn write_skill_file( return Ok(()); } } - anyhow::bail!("skill directory not found for {skill_name}") + Err(super::error::AgentError::Other(format!( + "skill directory not found for {skill_name}" + ))) } /// Naive parser for `SQLite` datetime strings (e.g. "2024-01-15 10:30:00") to Unix seconds. diff --git a/crates/zeph-core/src/agent/mcp.rs b/crates/zeph-core/src/agent/mcp.rs index df626d37..3cea7be2 100644 --- a/crates/zeph-core/src/agent/mcp.rs +++ b/crates/zeph-core/src/agent/mcp.rs @@ -1,7 +1,10 @@ use super::{Agent, Channel, LlmProvider, ToolExecutor}; impl Agent { - pub(super) async fn handle_mcp_command(&mut self, args: &str) -> anyhow::Result<()> { + pub(super) async fn handle_mcp_command( + &mut self, + args: &str, + ) -> Result<(), super::error::AgentError> { let parts: Vec<&str> = args.split_whitespace().collect(); match parts.first().copied() { Some("add") => self.handle_mcp_add(&parts[1..]).await, @@ -17,7 +20,7 @@ impl Agent anyhow::Result<()> { + async fn handle_mcp_add(&mut self, args: &[&str]) -> Result<(), super::error::AgentError> { if args.len() < 2 { self.channel .send("Usage: /mcp add [args...] | /mcp add ") @@ -112,7 +115,7 @@ impl Agent anyhow::Result<()> { + async fn handle_mcp_list(&mut self) -> Result<(), super::error::AgentError> { use std::fmt::Write; let Some(ref manager) = self.mcp.manager else { @@ -139,7 +142,10 @@ impl Agent) -> anyhow::Result<()> { + async fn handle_mcp_tools( + &mut self, + server_id: Option<&str>, + ) -> Result<(), super::error::AgentError> { use std::fmt::Write; let Some(server_id) = server_id else { @@ -173,7 +179,10 @@ impl Agent) -> anyhow::Result<()> { + async fn handle_mcp_remove( + &mut self, + server_id: Option<&str>, + ) -> Result<(), super::error::AgentError> { let Some(server_id) = server_id else { self.channel.send("Usage: /mcp remove ").await?; return Ok(()); diff --git a/crates/zeph-core/src/agent/mod.rs b/crates/zeph-core/src/agent/mod.rs index 42a669d3..471c7fa0 100644 --- a/crates/zeph-core/src/agent/mod.rs +++ b/crates/zeph-core/src/agent/mod.rs @@ -1,4 +1,5 @@ mod context; +pub mod error; #[cfg(feature = "index")] mod index; mod learning; @@ -536,7 +537,7 @@ impl Agent anyhow::Result<()> { + async fn process_user_message(&mut self, text: String) -> Result<(), error::AgentError> { let trimmed = text.trim(); if trimmed == "/skills" { @@ -593,7 +594,7 @@ impl Agent anyhow::Result<()> { + async fn handle_skills_command(&mut self) -> Result<(), error::AgentError> { use std::fmt::Write; let mut output = String::from("Available skills:\n\n"); @@ -623,7 +624,7 @@ impl Agent anyhow::Result<()> { + async fn handle_feedback(&mut self, input: &str) -> Result<(), error::AgentError> { #[cfg(feature = "self-learning")] { let Some((name, rest)) = input.split_once(' ') else { diff --git a/crates/zeph-core/src/agent/persistence.rs b/crates/zeph-core/src/agent/persistence.rs index 020d69d5..a81cbc52 100644 --- a/crates/zeph-core/src/agent/persistence.rs +++ b/crates/zeph-core/src/agent/persistence.rs @@ -12,7 +12,7 @@ impl Agent anyhow::Result<()> { + pub async fn load_history(&mut self) -> Result<(), super::error::AgentError> { let (Some(memory), Some(cid)) = (&self.memory_state.memory, self.memory_state.conversation_id) else { diff --git a/crates/zeph-core/src/agent/streaming.rs b/crates/zeph-core/src/agent/streaming.rs index be26f04e..323707ef 100644 --- a/crates/zeph-core/src/agent/streaming.rs +++ b/crates/zeph-core/src/agent/streaming.rs @@ -9,7 +9,7 @@ use zeph_memory::semantic::estimate_tokens; use super::{Agent, DOOM_LOOP_WINDOW, format_tool_output}; impl Agent { - pub(crate) async fn process_response(&mut self) -> anyhow::Result<()> { + pub(crate) async fn process_response(&mut self) -> Result<(), super::error::AgentError> { self.doom_loop_history.clear(); for iteration in 0..self.max_tool_iterations { @@ -95,7 +95,9 @@ impl Agent anyhow::Result> { + pub(crate) async fn call_llm_with_timeout( + &mut self, + ) -> Result, super::error::AgentError> { let llm_timeout = std::time::Duration::from_secs(self.timeouts.llm_seconds); let start = std::time::Instant::now(); let prompt_estimate: u64 = self @@ -213,7 +215,7 @@ impl Agent, ToolError>, - ) -> anyhow::Result { + ) -> Result { match result { Ok(Some(output)) => { if output.summary.trim().is_empty() { @@ -314,7 +316,9 @@ impl Agent anyhow::Result { + pub(crate) async fn process_response_streaming( + &mut self, + ) -> Result { let mut stream = self.provider.chat_stream(&self.messages).await?; let mut response = String::with_capacity(2048); diff --git a/crates/zeph-core/src/lib.rs b/crates/zeph-core/src/lib.rs index b51335aa..1d592533 100644 --- a/crates/zeph-core/src/lib.rs +++ b/crates/zeph-core/src/lib.rs @@ -11,5 +11,6 @@ pub mod redact; pub mod vault; pub use agent::Agent; +pub use agent::error::AgentError; pub use channel::{Channel, ChannelError, ChannelMessage}; pub use config::Config; diff --git a/docs/src/architecture/crates.md b/docs/src/architecture/crates.md index 91eae6aa..c6225b5e 100644 --- a/docs/src/architecture/crates.md +++ b/docs/src/architecture/crates.md @@ -6,9 +6,10 @@ Each workspace crate has a focused responsibility. All leaf crates are independe Agent loop, configuration loading, and context builder. -- `Agent` — main agent loop with streaming support, message queue drain, configurable `max_tool_iterations` (default 10), doom-loop detection, and context budget check (stops at 80% threshold) +- `Agent` — main agent loop with streaming support, message queue drain, configurable `max_tool_iterations` (default 10), doom-loop detection, and context budget check (stops at 80% threshold). Internal state is grouped into five domain structs (`MemoryState`, `SkillState`, `ContextState`, `McpState`, `IndexState`); logic is decomposed into `streaming.rs` and `persistence.rs` submodules +- `AgentError` — typed error enum covering LLM, memory, channel, tool, context, and I/O failures (replaces prior `anyhow` usage) - `Config` — TOML config loading with env var overrides -- `Channel` trait — abstraction for I/O (CLI, Telegram, TUI) with `recv()`, `try_recv()`, `send_queue_count()` for queue management +- `Channel` trait — abstraction for I/O (CLI, Telegram, TUI) with `recv()`, `try_recv()`, `send_queue_count()` for queue management. Returns `Result<_, ChannelError>` with typed variants (`Io`, `ChannelClosed`, `ConfirmationCancelled`) - Context builder — assembles system prompt from skills, memory, summaries, environment, and project config - Context engineering — proportional budget allocation, semantic recall injection, message trimming, runtime compaction - `EnvironmentContext` — runtime gathering of cwd, git branch, OS, model name @@ -21,6 +22,7 @@ Agent loop, configuration loading, and context builder. LLM provider abstraction and backend implementations. - `LlmProvider` trait — `chat()`, `chat_stream()`, `embed()`, `supports_streaming()`, `supports_embeddings()` +- `EmbedFuture` / `EmbedFn` — canonical type aliases for embedding closures, re-exported by downstream crates (`zeph-skills`, `zeph-mcp`) - `OllamaProvider` — local inference via ollama-rs - `ClaudeProvider` — Anthropic Messages API with SSE streaming - `OpenAiProvider` — OpenAI + compatible APIs (raw reqwest) @@ -46,7 +48,7 @@ SKILL.md loader, skill registry, and prompt formatter. SQLite-backed conversation persistence with Qdrant vector search. - `SqliteStore` — conversations, messages, summaries, skill usage, skill versions -- `QdrantStore` — vector storage and cosine similarity search +- `QdrantStore` — vector storage and cosine similarity search with `MessageKind` enum (`Regular` | `Summary`) for payload classification - `SemanticMemory

` — orchestrator coordinating SQLite + Qdrant + LlmProvider - Automatic collection creation, graceful degradation without Qdrant @@ -54,6 +56,7 @@ SQLite-backed conversation persistence with Qdrant vector search. Channel implementations for the Zeph agent. +- `ChannelError` — typed error enum (`Telegram`, `NoActiveChat`) replacing prior `anyhow` usage - `CliChannel` — stdin/stdout with immediate streaming output, blocking recv (queue always empty) - `TelegramChannel` — teloxide adapter with MarkdownV2 rendering, streaming via edit-in-place, user whitelisting, inline confirmation keyboards, mpsc-backed message queue with 500ms merge window diff --git a/docs/src/architecture/overview.md b/docs/src/architecture/overview.md index 6532ecce..ee2c5fe3 100644 --- a/docs/src/architecture/overview.md +++ b/docs/src/architecture/overview.md @@ -52,9 +52,9 @@ Queued messages are processed sequentially with full context rebuilding between ## Key Design Decisions -- **Generic Agent:** `Agent` — fully generic over provider, channel, and tool executor +- **Generic Agent:** `Agent` — fully generic over provider, channel, and tool executor. Internal state is grouped into five domain structs (`MemoryState`, `SkillState`, `ContextState`, `McpState`, `IndexState`) with logic decomposed into `streaming.rs` and `persistence.rs` submodules - **TLS:** rustls everywhere (no openssl-sys) -- **Errors:** `thiserror` for library crates, `anyhow` for application code (`zeph-core`, `main.rs`) +- **Errors:** `thiserror` for all crates with typed error enums (`ChannelError`, `AgentError`, `LlmError`, etc.); `anyhow` only for top-level orchestration in `main.rs` - **Lints:** workspace-level `clippy::all` + `clippy::pedantic` + `clippy::nursery`; `unsafe_code = "deny"` - **Dependencies:** versions only in root `[workspace.dependencies]`; crates inherit via `workspace = true` - **Feature gates:** optional crates (`zeph-index`, `zeph-mcp`, `zeph-a2a`, `zeph-tui`) are feature-gated in the binary diff --git a/docs/src/feature-flags.md b/docs/src/feature-flags.md index 776d65e7..793e33a3 100644 --- a/docs/src/feature-flags.md +++ b/docs/src/feature-flags.md @@ -10,6 +10,7 @@ Zeph uses Cargo feature flags to control optional functionality. Default feature | `candle` | Enabled | Local HuggingFace model inference via [candle](https://github.com/huggingface/candle) (GGUF quantized models) | | `orchestrator` | Enabled | Multi-model routing with task-based classification and fallback chains | | `self-learning` | Enabled | Skill evolution via failure detection, self-reflection, and LLM-generated improvements | +| `qdrant` | Enabled | Qdrant-backed vector storage for skill matching (`zeph-skills`) and MCP tool registry (`zeph-mcp`) | | `vault-age` | Enabled | Age-encrypted vault backend for file-based secret storage ([age](https://age-encryption.org/)) | | `index` | Enabled | AST-based code indexing and semantic retrieval via tree-sitter ([guide](guide/code-indexing.md)) | | `tui` | Disabled | ratatui-based TUI dashboard with real-time agent metrics | diff --git a/docs/src/guide/channels.md b/docs/src/guide/channels.md index 6aa94b6d..f25b8b4a 100644 --- a/docs/src/guide/channels.md +++ b/docs/src/guide/channels.md @@ -1,6 +1,6 @@ # Channels -Zeph supports multiple I/O channels for interacting with the agent. Each channel implements the `Channel` trait and can be selected at runtime based on configuration or CLI flags. +Zeph supports multiple I/O channels for interacting with the agent. Each channel implements the `Channel` trait (returning `Result<_, ChannelError>` with typed variants for I/O, closed-channel, and cancellation errors) and can be selected at runtime based on configuration or CLI flags. ## Available Channels