style: format seluruh workspace dengan cargo fmt

Menyeragamkan format kode sesuai rustfmt (126 file). Sebelumnya
lefthook pre-commit 'cargo fmt --check' akan gagal pada commit apa pun.
This commit is contained in:
asepharyana
2026-08-27 22:10:28 +07:00
parent 884b19ccb5
commit 7b0b53671f
127 changed files with 1271 additions and 1156 deletions
+1 -4
View File
@@ -16,10 +16,7 @@ pub trait ToolExecutor: Send + Sync {
/// Service for running agent turns asynchronously. /// Service for running agent turns asynchronously.
pub trait AgentTurnService: Send + Sync { pub trait AgentTurnService: Send + Sync {
/// Run a full agent turn loop asynchronously. /// Run a full agent turn loop asynchronously.
fn run_turn( fn run_turn(&self, params: AgentTurnParams) -> impl Future<Output = Result<()>> + Send;
&self,
params: AgentTurnParams,
) -> impl Future<Output = Result<()>> + Send;
} }
pub mod explore; pub mod explore;
+18 -23
View File
@@ -7,8 +7,8 @@ use zesdex_domain::agent::{AgentTurnParams, TurnEvent};
use zesdex_domain::core::{ChatMessage, StreamEvent, ToolDef}; use zesdex_domain::core::{ChatMessage, StreamEvent, ToolDef};
use zesdex_domain::main_agent_prompt; use zesdex_domain::main_agent_prompt;
use crate::ports::ProviderService;
use super::{ExploreService, ToolExecutor}; use super::{ExploreService, ToolExecutor};
use crate::ports::ProviderService;
/// Maximum tool-call iterations per agent turn before forcing termination. /// Maximum tool-call iterations per agent turn before forcing termination.
const MAX_TURN_ITERATIONS: u32 = 50; const MAX_TURN_ITERATIONS: u32 = 50;
@@ -124,11 +124,7 @@ pub struct AgentTurnServiceImpl<P: ProviderService, T: ToolExecutor> {
} }
impl<P: ProviderService, T: ToolExecutor> AgentTurnServiceImpl<P, T> { impl<P: ProviderService, T: ToolExecutor> AgentTurnServiceImpl<P, T> {
pub fn new( pub fn new(provider: Arc<P>, tool_executor: Arc<T>, tool_defs: Vec<ToolDef>) -> Self {
provider: Arc<P>,
tool_executor: Arc<T>,
tool_defs: Vec<ToolDef>,
) -> Self {
Self { Self {
provider, provider,
tool_executor, tool_executor,
@@ -203,7 +199,10 @@ impl<P: ProviderService, T: ToolExecutor> super::AgentTurnService for AgentTurnS
}, },
); );
match explorer.explore(&user_query, &workspace_root, &params.turn_events).await { match explorer
.explore(&user_query, &workspace_root, &params.turn_events)
.await
{
Ok(output) => { Ok(output) => {
// Insert each context message as a system message. // Insert each context message as a system message.
// They go at index 0 and are removed after the turn // They go at index 0 and are removed after the turn
@@ -236,7 +235,9 @@ impl<P: ProviderService, T: ToolExecutor> super::AgentTurnService for AgentTurnS
// entire turn, avoiding per-iteration clones of the full message list. // entire turn, avoiding per-iteration clones of the full message list.
// It is removed before emitting the Compacted event so persistence // It is removed before emitting the Compacted event so persistence
// does not store the prompt redundantly. // does not store the prompt redundantly.
params.messages.insert(0, ChatMessage::system(main_agent_prompt())); params
.messages
.insert(0, ChatMessage::system(main_agent_prompt()));
let original_count = params.messages.len(); let original_count = params.messages.len();
for iteration in 0..MAX_TURN_ITERATIONS { for iteration in 0..MAX_TURN_ITERATIONS {
@@ -287,7 +288,8 @@ impl<P: ProviderService, T: ToolExecutor> super::AgentTurnService for AgentTurnS
// ── Execute each tool call ────────────────────────── // ── Execute each tool call ──────────────────────────
for tc in &tool_calls { for tc in &tool_calls {
let output = let output =
execute_tool_call(self.tool_executor.as_ref(), &params.turn_events, tc).await; execute_tool_call(self.tool_executor.as_ref(), &params.turn_events, tc)
.await;
params params
.messages .messages
.push(ChatMessage::tool(tc.id.clone(), output)); .push(ChatMessage::tool(tc.id.clone(), output));
@@ -295,10 +297,7 @@ impl<P: ProviderService, T: ToolExecutor> super::AgentTurnService for AgentTurnS
} }
Err(e) => { Err(e) => {
warn!("{e}"); warn!("{e}");
push_event( push_event(&params.turn_events, TurnEvent::Error(e));
&params.turn_events,
TurnEvent::Error(e),
);
break; break;
} }
} }
@@ -307,10 +306,7 @@ impl<P: ProviderService, T: ToolExecutor> super::AgentTurnService for AgentTurnS
// Remove the synthetic sys_msg before shipping events to the TUI // Remove the synthetic sys_msg before shipping events to the TUI
// so the transcript shows only the actual user/assistant/tool exchange. // so the transcript shows only the actual user/assistant/tool exchange.
let compacted: Vec<ChatMessage> = params.messages.drain(original_count - 1..).collect(); let compacted: Vec<ChatMessage> = params.messages.drain(original_count - 1..).collect();
push_event( push_event(&params.turn_events, TurnEvent::Compacted(compacted));
&params.turn_events,
TurnEvent::Compacted(compacted),
);
push_event(&params.turn_events, TurnEvent::Done); push_event(&params.turn_events, TurnEvent::Done);
params.in_flight.store(false, Ordering::SeqCst); params.in_flight.store(false, Ordering::SeqCst);
@@ -341,15 +337,16 @@ pub async fn compact_messages_with_ai<P: ProviderService>(
let split_idx = messages.len() - COMPACT_KEEP_TAIL; let split_idx = messages.len() - COMPACT_KEEP_TAIL;
let evicted: Vec<_> = messages.drain(..split_idx).collect(); let evicted: Vec<_> = messages.drain(..split_idx).collect();
let mut summary_prompt = vec![ let mut summary_prompt = vec![ChatMessage::system(zesdex_domain::compaction_prompt())];
ChatMessage::system(zesdex_domain::compaction_prompt()),
];
summary_prompt.extend(evicted); summary_prompt.extend(evicted);
summary_prompt.push(ChatMessage::user( summary_prompt.push(ChatMessage::user(
"Please summarise our previous conversation above for context continuity.".to_string(), "Please summarise our previous conversation above for context continuity.".to_string(),
)); ));
match provider.chat(&summary_prompt, None, Some(1024), Some(0.3)).await { match provider
.chat(&summary_prompt, None, Some(1024), Some(0.3))
.await
{
Ok((summary_msg, _)) => { Ok((summary_msg, _)) => {
let summary_text = summary_msg let summary_text = summary_msg
.content .content
@@ -373,5 +370,3 @@ pub async fn compact_messages_with_ai<P: ProviderService>(
} }
} }
} }
+7 -20
View File
@@ -45,11 +45,7 @@ use sha2::{Digest, Sha256};
/// and clear them after a successful (or failed) flow completion. /// and clear them after a successful (or failed) flow completion.
pub trait OAuthFlowStore: Send + Sync { pub trait OAuthFlowStore: Send + Sync {
/// Persist the PKCE code verifier and CSRF state token. /// Persist the PKCE code verifier and CSRF state token.
fn save_flow_state( fn save_flow_state(&self, verifier: &str, state: &str) -> Result<(), ServiceError>;
&self,
verifier: &str,
state: &str,
) -> Result<(), ServiceError>;
/// Load the stored PKCE code verifier. /// Load the stored PKCE code verifier.
fn load_verifier(&self) -> Result<String, ServiceError>; fn load_verifier(&self) -> Result<String, ServiceError>;
@@ -141,12 +137,7 @@ pub struct OAuthUseCase<R, S, E> {
impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger> OAuthUseCase<R, S, E> { impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger> OAuthUseCase<R, S, E> {
/// Create a new OAuth use-case. /// Create a new OAuth use-case.
pub fn new( pub fn new(token_repo: R, flow_store: S, token_exchanger: E, token_path: PathBuf) -> Self {
token_repo: R,
flow_store: S,
token_exchanger: E,
token_path: PathBuf,
) -> Self {
OAuthUseCase { OAuthUseCase {
token_repo, token_repo,
flow_store, flow_store,
@@ -156,8 +147,8 @@ impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger> OAuthUseCase<R, S
} }
} }
impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger> impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger> zesdex_domain::auth::OAuthService
zesdex_domain::auth::OAuthService for OAuthUseCase<R, S, E> for OAuthUseCase<R, S, E>
{ {
fn start_flow( fn start_flow(
&self, &self,
@@ -183,13 +174,9 @@ impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger>
"starting OAuth flow", "starting OAuth flow",
); );
let mut url = url::Url::parse(&config.auth_url) let mut url = url::Url::parse(&config.auth_url).map_err(|e| {
.map_err(|e| { ServiceError::InvalidConfig(format!("invalid auth_url '{}': {e}", config.auth_url))
ServiceError::InvalidConfig(format!( })?;
"invalid auth_url '{}': {e}",
config.auth_url
))
})?;
url.query_pairs_mut() url.query_pairs_mut()
.append_pair("response_type", "code") .append_pair("response_type", "code")
+6 -11
View File
@@ -46,12 +46,11 @@ impl<R: SessionRepository, L: SessionLockRepository> SessionServiceImpl<R, L> {
} }
} }
impl<R: SessionRepository, L: SessionLockRepository> impl<R: SessionRepository, L: SessionLockRepository> zesdex_domain::auth::SessionService
zesdex_domain::auth::SessionService for SessionServiceImpl<R, L> for SessionServiceImpl<R, L>
{ {
fn create_session(&self, title: &str) -> Result<Session, ServiceError> { fn create_session(&self, title: &str) -> Result<Session, ServiceError> {
let id = SessionId::new(&Uuid::new_v4().to_string()) let id = SessionId::new(&Uuid::new_v4().to_string()).map_err(ServiceError::Other)?;
.map_err(ServiceError::Other)?;
let title_owned = if title.is_empty() { let title_owned = if title.is_empty() {
"New Session".to_string() "New Session".to_string()
} else { } else {
@@ -59,8 +58,7 @@ impl<R: SessionRepository, L: SessionLockRepository>
}; };
let session = Session::new(id.into_string(), title_owned); let session = Session::new(id.into_string(), title_owned);
tracing::debug!(session_id = %session.id, title = %session.title, "creating new session"); tracing::debug!(session_id = %session.id, title = %session.title, "creating new session");
self.session_repo self.session_repo.save_session(&self.base_dir, &session)?;
.save_session(&self.base_dir, &session)?;
Ok(session) Ok(session)
} }
@@ -73,17 +71,14 @@ impl<R: SessionRepository, L: SessionLockRepository>
fn archive_session(&self, id: SessionId) -> Result<(), ServiceError> { fn archive_session(&self, id: SessionId) -> Result<(), ServiceError> {
tracing::debug!(session_id = %id, "archiving session"); tracing::debug!(session_id = %id, "archiving session");
let mut session = self let mut session = self.session_repo.load_session(&self.base_dir, &id)?;
.session_repo
.load_session(&self.base_dir, &id)?;
session.archived = true; session.archived = true;
let millis = std::time::SystemTime::now() let millis = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default() .unwrap_or_default()
.as_millis(); .as_millis();
session.updated_at = i64::try_from(millis).unwrap_or(i64::MAX); session.updated_at = i64::try_from(millis).unwrap_or(i64::MAX);
self.session_repo self.session_repo.save_session(&self.base_dir, &session)?;
.save_session(&self.base_dir, &session)?;
Ok(()) Ok(())
} }
} }
@@ -58,11 +58,7 @@ impl<R: ConversationRepository> zesdex_domain::cms::ConversationService
Ok(()) Ok(())
} }
fn add_message( fn add_message(&self, conv: &mut Conversation, msg: ChatMessage) -> Result<(), ServiceError> {
&self,
conv: &mut Conversation,
msg: ChatMessage,
) -> Result<(), ServiceError> {
tracing::debug!("adding message to session {}", conv.session_id); tracing::debug!("adding message to session {}", conv.session_id);
conv.push(msg); conv.push(msg);
let dir = self.session_dir(&conv.session_id); let dir = self.session_dir(&conv.session_id);
+5 -14
View File
@@ -14,8 +14,7 @@ use std::path::PathBuf;
use tracing; use tracing;
use zesdex_domain::cms::{ use zesdex_domain::cms::{
AppConfig, AppConfigRepository, ProviderConfig, ServiceError, Settings, AppConfig, AppConfigRepository, ProviderConfig, ServiceError, Settings, SettingsRepository,
SettingsRepository,
}; };
/// Service implementation for settings and app-config operations. /// Service implementation for settings and app-config operations.
@@ -30,11 +29,7 @@ pub struct SettingsServiceImpl<S, C> {
impl<S: SettingsRepository, C: AppConfigRepository> SettingsServiceImpl<S, C> { impl<S: SettingsRepository, C: AppConfigRepository> SettingsServiceImpl<S, C> {
/// Create a new service with the given repositories and base directory. /// Create a new service with the given repositories and base directory.
pub fn new( pub fn new(settings_repo: S, app_config_repo: C, base_dir: impl Into<PathBuf>) -> Self {
settings_repo: S,
app_config_repo: C,
base_dir: impl Into<PathBuf>,
) -> Self {
tracing::debug!("creating SettingsServiceImpl"); tracing::debug!("creating SettingsServiceImpl");
Self { Self {
settings_repo, settings_repo,
@@ -44,8 +39,8 @@ impl<S: SettingsRepository, C: AppConfigRepository> SettingsServiceImpl<S, C> {
} }
} }
impl<S: SettingsRepository, C: AppConfigRepository> impl<S: SettingsRepository, C: AppConfigRepository> zesdex_domain::cms::SettingsService
zesdex_domain::cms::SettingsService for SettingsServiceImpl<S, C> for SettingsServiceImpl<S, C>
{ {
fn load_settings(&self) -> Result<Settings, ServiceError> { fn load_settings(&self) -> Result<Settings, ServiceError> {
tracing::debug!("loading settings"); tracing::debug!("loading settings");
@@ -60,11 +55,7 @@ impl<S: SettingsRepository, C: AppConfigRepository>
Ok(()) Ok(())
} }
fn update_provider( fn update_provider(&self, name: &str, config: &ProviderConfig) -> Result<(), ServiceError> {
&self,
name: &str,
config: &ProviderConfig,
) -> Result<(), ServiceError> {
tracing::debug!("updating provider '{name}'"); tracing::debug!("updating provider '{name}'");
let mut app_config: AppConfig = self.app_config_repo.load(&self.base_dir)?; let mut app_config: AppConfig = self.app_config_repo.load(&self.base_dir)?;
app_config app_config
+3 -4
View File
@@ -30,10 +30,10 @@
//! the use-case logic independent of any specific persistence or infrastructure //! the use-case logic independent of any specific persistence or infrastructure
//! technology. //! technology.
pub mod agent;
pub mod auth; pub mod auth;
pub mod cms; pub mod cms;
pub mod ports; pub mod ports;
pub mod agent;
// Re-export port traits for ergonomic access. // Re-export port traits for ergonomic access.
pub use ports::*; pub use ports::*;
@@ -46,12 +46,11 @@ pub use auth::{
// Re-export CMS use-cases. // Re-export CMS use-cases.
pub use cms::{ pub use cms::{
conversation_service::ConversationServiceImpl, conversation_service::ConversationServiceImpl, memory_service::MemoryServiceImpl,
memory_service::MemoryServiceImpl,
settings_service::SettingsServiceImpl, settings_service::SettingsServiceImpl,
}; };
pub use agent::{ pub use agent::{
turn_service::{compact_messages_with_ai, AgentTurnServiceImpl},
AgentTurnService, ExploreOutput, ExploreService, ToolExecutor, AgentTurnService, ExploreOutput, ExploreService, ToolExecutor,
turn_service::{AgentTurnServiceImpl, compact_messages_with_ai},
}; };
+2 -5
View File
@@ -22,11 +22,8 @@ pub trait AuthService: Send + Sync {
/// Authenticate a user by verifying a password against a stored hash. /// Authenticate a user by verifying a password against a stored hash.
/// ///
/// Returns `true` if the password matches, `false` otherwise. /// Returns `true` if the password matches, `false` otherwise.
fn authenticate( fn authenticate(&self, password: &str, hash: &str)
&self, -> impl Future<Output = Result<bool>> + Send;
password: &str,
hash: &str,
) -> impl Future<Output = Result<bool>> + Send;
/// Issue a new access + refresh token pair for the given subject. /// Issue a new access + refresh token pair for the given subject.
/// ///
+1 -1
View File
@@ -7,8 +7,8 @@ use std::path::PathBuf;
use crate::core::{ChatMessage, ToolCallResult, UsageStats}; use crate::core::{ChatMessage, ToolCallResult, UsageStats};
pub mod defaults; pub mod defaults;
pub mod prompt;
pub mod progress; pub mod progress;
pub mod prompt;
/// Which kind of caller (main agent vs. subagent vs. reviewer) is /// Which kind of caller (main agent vs. subagent vs. reviewer) is
/// invoking a tool, used to scope permissions and tag log/output paths. /// invoking a tool, used to scope permissions and tag log/output paths.
+1 -1
View File
@@ -11,5 +11,5 @@
//! - `ChatMessage` — a single message with role, content, and tool metadata //! - `ChatMessage` — a single message with role, content, and tool metadata
//! - `Role` — message role enum (User, Assistant, System, Tool) //! - `Role` — message role enum (User, Assistant, System, Tool)
pub use crate::core::message::{ChatMessage, Role};
pub use crate::core::conversation::Conversation; pub use crate::core::conversation::Conversation;
pub use crate::core::message::{ChatMessage, Role};
+1 -1
View File
@@ -32,6 +32,7 @@ pub mod settings;
pub use app_config::AppConfig; pub use app_config::AppConfig;
pub use app_config::ModelRole; pub use app_config::ModelRole;
pub use app_config::ProviderConfig; pub use app_config::ProviderConfig;
pub use commands::{NewMemory, SettingsPatch};
pub use conversation::Conversation; pub use conversation::Conversation;
pub use edit_log::EditLog; pub use edit_log::EditLog;
pub use edit_log::EditLogEntry; pub use edit_log::EditLogEntry;
@@ -46,7 +47,6 @@ pub use repository::SettingsRepository;
pub use service::ConversationService; pub use service::ConversationService;
pub use service::MemoryService; pub use service::MemoryService;
pub use service::SettingsService; pub use service::SettingsService;
pub use commands::{NewMemory, SettingsPatch};
pub use settings::InternetMode; pub use settings::InternetMode;
pub use settings::Settings; pub use settings::Settings;
pub use settings::SettingsFlags; pub use settings::SettingsFlags;
+1 -5
View File
@@ -56,11 +56,7 @@ pub trait ConversationRepository {
fn load(&self, session_dir: &Path) -> Result<Conversation, RepositoryError>; fn load(&self, session_dir: &Path) -> Result<Conversation, RepositoryError>;
/// Persist a `Conversation` to the given session directory. /// Persist a `Conversation` to the given session directory.
fn save( fn save(&self, session_dir: &Path, conversation: &Conversation) -> Result<(), RepositoryError>;
&self,
session_dir: &Path,
conversation: &Conversation,
) -> Result<(), RepositoryError>;
} }
/// Persistence contract for `Memory` (long-term agent memory entries). /// Persistence contract for `Memory` (long-term agent memory entries).
+1 -5
View File
@@ -44,11 +44,7 @@ pub trait ConversationService {
fn save_conversation(&self, conv: &Conversation) -> Result<(), ServiceError>; fn save_conversation(&self, conv: &Conversation) -> Result<(), ServiceError>;
/// Append a single `ChatMessage` to the conversation and persist. /// Append a single `ChatMessage` to the conversation and persist.
fn add_message( fn add_message(&self, conv: &mut Conversation, msg: ChatMessage) -> Result<(), ServiceError>;
&self,
conv: &mut Conversation,
msg: ChatMessage,
) -> Result<(), ServiceError>;
} }
/// Use-cases for long-term memory management. /// Use-cases for long-term memory management.
+10 -11
View File
@@ -335,16 +335,16 @@ impl SseParser {
{ {
const MAX_TOOL_CALLS: usize = 64; const MAX_TOOL_CALLS: usize = 64;
for tc in tool_calls { for tc in tool_calls {
let raw_index = let raw_index = tc
tc.get("index").and_then(Value::as_u64).unwrap_or_else( .get("index")
|| { .and_then(Value::as_u64)
tracing::warn!( .unwrap_or_else(|| {
"[stream] tool call delta missing index, \ tracing::warn!(
"[stream] tool call delta missing index, \
defaulting to 0" defaulting to 0"
); );
0 0
}, });
);
// Clamp index to prevent out-of-bounds / memory exhaustion // Clamp index to prevent out-of-bounds / memory exhaustion
let index = usize::try_from(raw_index) let index = usize::try_from(raw_index)
.unwrap_or(0) .unwrap_or(0)
@@ -395,8 +395,7 @@ impl SseParser {
if let Some(content) = delta.get("text").and_then(|c| c.as_str()) { if let Some(content) = delta.get("text").and_then(|c| c.as_str()) {
d_events.push(StreamEvent::Token(content.to_string())); d_events.push(StreamEvent::Token(content.to_string()));
} }
if let Some(reasoning) = if let Some(reasoning) = delta.get("reasoning_content").and_then(|r| r.as_str())
delta.get("reasoning_content").and_then(|r| r.as_str())
{ {
d_events.push(StreamEvent::Reasoning(reasoning.to_string())); d_events.push(StreamEvent::Reasoning(reasoning.to_string()));
} }
+5 -3
View File
@@ -40,9 +40,11 @@ impl Store {
/// ///
/// Why: paths are computed, not created — call `ensure_dirs` before use. /// Why: paths are computed, not created — call `ensure_dirs` before use.
pub fn new() -> Self { pub fn new() -> Self {
let base = if let Some(data_dir) = std::env::var("XDG_DATA_HOME").ok() let base = if let Some(data_dir) = std::env::var("XDG_DATA_HOME").ok().or_else(|| {
.or_else(|| std::env::var("HOME").ok().map(|h| format!("{h}/.local/share"))) std::env::var("HOME")
{ .ok()
.map(|h| format!("{h}/.local/share"))
}) {
PathBuf::from(data_dir).join("zesdex") PathBuf::from(data_dir).join("zesdex")
} else { } else {
PathBuf::from(".local/share/zesdex") PathBuf::from(".local/share/zesdex")
+14 -15
View File
@@ -24,33 +24,32 @@
//! no framework imports, no side effects. All persistence is expressed //! no framework imports, no side effects. All persistence is expressed
//! through repository traits that infrastructure adapters implement. //! through repository traits that infrastructure adapters implement.
pub mod agent;
pub mod auth; pub mod auth;
pub mod cms; pub mod cms;
pub mod core; pub mod core;
pub mod error; pub mod error;
pub mod agent;
pub mod workflow;
pub mod subagent; pub mod subagent;
pub mod workflow;
// Re-export all public items from each module for ergonomic imports. // Re-export all public items from each module for ergonomic imports.
// Consumers can do `use zesdex_domain::*` for common types. // Consumers can do `use zesdex_domain::*` for common types.
pub use auth::{ pub use auth::{
IamSession, NewSession, OAuthConfig, OAuthToken, OAuthRepository, OAuthService, IamSession, NewSession, OAuthConfig, OAuthRepository, OAuthService, OAuthToken,
RepositoryError as AuthRepositoryError, ServiceError as AuthServiceError, Session, RepositoryError as AuthRepositoryError, ServiceError as AuthServiceError, Session, SessionId,
SessionId, SessionLock, SessionLockRepository, SessionRepository, SessionService, SessionLock, SessionLockRepository, SessionRepository, SessionService,
}; };
pub use cms::{ pub use cms::{
AppConfig, AppConfigRepository, Conversation as CmsConversation, AppConfig, AppConfigRepository, Conversation as CmsConversation, ConversationRepository,
ConversationRepository, ConversationService, EditLog, EditLogEntry, ConversationService, EditLog, EditLogEntry, EditLogRepository, InternetMode, Memory,
EditLogRepository, InternetMode, Memory, MemoryRepository, MemoryService, MemoryRepository, MemoryService, ModelRole, NewMemory, ProviderConfig,
ModelRole, NewMemory, ProviderConfig, RepositoryError as CmsRepositoryError, RepositoryError as CmsRepositoryError, ServiceError as CmsServiceError, Settings,
ServiceError as CmsServiceError, Settings, SettingsFlags, SettingsPatch, SettingsFlags, SettingsPatch, SettingsRepository, SettingsService,
SettingsRepository, SettingsService,
}; };
pub use core::{ pub use core::{
ChatMessage, ChatRequest, ChatResponse, Choice, Conversation, Delta, Role, ChatMessage, ChatRequest, ChatResponse, Choice, Conversation, Delta, Role, SseParser, Store,
SseParser, StreamEvent, StreamOptions, Store, TokenUsage, ToolCall, StreamEvent, StreamOptions, TokenUsage, ToolCall, ToolCallResult, ToolDef, ToolFunction,
ToolCallResult, ToolDef, ToolFunction, ToolFunctionDef, UsageStats, ToolFunctionDef, UsageStats,
}; };
pub use error::DomainError; pub use error::DomainError;
@@ -60,5 +59,5 @@ pub use agent::*;
pub use agent::defaults::*; pub use agent::defaults::*;
pub use agent::progress::AgentProgress; pub use agent::progress::AgentProgress;
pub use agent::prompt::{compaction_prompt, main_agent_prompt, subagent_directive}; pub use agent::prompt::{compaction_prompt, main_agent_prompt, subagent_directive};
pub use workflow::*;
pub use subagent::*; pub use subagent::*;
pub use workflow::*;
+3 -6
View File
@@ -4,7 +4,6 @@
//! configuration files plus a seed session for development/testing. //! configuration files plus a seed session for development/testing.
//! Invoked as `cargo run --bin seed`. //! Invoked as `cargo run --bin seed`.
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
let store = zesdex_domain::core::Store::new(); let store = zesdex_domain::core::Store::new();
store.ensure_dirs()?; store.ensure_dirs()?;
@@ -46,13 +45,11 @@ fn main() -> anyhow::Result<()> {
// Create a seed session // Create a seed session
let session_id = uuid::Uuid::new_v4().to_string(); let session_id = uuid::Uuid::new_v4().to_string();
let session = zesdex_domain::auth::Session::new( let session = zesdex_domain::auth::Session::new(session_id.clone(), "Seed Session".to_string());
session_id.clone(),
"Seed Session".to_string(),
);
// Persist via the session repository // Persist via the session repository
use zesdex_domain::SessionRepository; use zesdex_domain::SessionRepository;
let repo = zesdex_infrastructure::persistence::iam::session_repo::FileSystemSessionRepository::new(); let repo =
zesdex_infrastructure::persistence::iam::session_repo::FileSystemSessionRepository::new();
repo.save_session(&store.base_dir, &session)?; repo.save_session(&store.base_dir, &session)?;
tracing::info!("Seed session created: id={session_id}"); tracing::info!("Seed session created: id={session_id}");
+2 -2
View File
@@ -5,12 +5,12 @@ fn main() {
let base_dir = dirs::home_dir().unwrap().join(".local/share/zesdex"); let base_dir = dirs::home_dir().unwrap().join(".local/share/zesdex");
let repo = JsonAppConfigRepository::new(); let repo = JsonAppConfigRepository::new();
let config = repo.load(&base_dir).unwrap(); let config = repo.load(&base_dir).unwrap();
println!("Providers:"); println!("Providers:");
for (k, v) in &config.providers { for (k, v) in &config.providers {
println!(" - {} (default model: {:?})", k, v.default_model); println!(" - {} (default model: {:?})", k, v.default_model);
} }
println!("Default provider: {}", config.default_provider); println!("Default provider: {}", config.default_provider);
println!("Default model: {}", config.default_model); println!("Default model: {}", config.default_model);
println!("Model roles:"); println!("Model roles:");
+4 -1
View File
@@ -16,7 +16,10 @@ struct ClaudeSettings {
} }
fn main() { fn main() {
let path = dirs::home_dir().unwrap().join(".claude").join("settings.json"); let path = dirs::home_dir()
.unwrap()
.join(".claude")
.join("settings.json");
println!("Path: {:?}", path); println!("Path: {:?}", path);
match std::fs::read_to_string(&path) { match std::fs::read_to_string(&path) {
Ok(content) => { Ok(content) => {
+5 -1
View File
@@ -12,7 +12,11 @@ use clap::Parser;
/// Zesdex — autonomous AI coding agent. /// Zesdex — autonomous AI coding agent.
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(name = "zesdex", version, about = "Autonomous AI coding agent with TUI")] #[command(
name = "zesdex",
version,
about = "Autonomous AI coding agent with TUI"
)]
struct Cli { struct Cli {
/// Run as background daemon with IPC socket /// Run as background daemon with IPC socket
#[arg(long)] #[arg(long)]
+6 -10
View File
@@ -21,20 +21,13 @@ impl LoopbackServer {
format!("http://127.0.0.1:{}/callback", self.port) format!("http://127.0.0.1:{}/callback", self.port)
} }
pub fn wait_for_code( pub fn wait_for_code(&self, timeout_ms: u64, expected_state: &str) -> std::io::Result<String> {
&self,
timeout_ms: u64,
expected_state: &str,
) -> std::io::Result<String> {
let (mut stream, _) = self.listener.accept()?; let (mut stream, _) = self.listener.accept()?;
stream.set_read_timeout(Some(std::time::Duration::from_millis(timeout_ms)))?; stream.set_read_timeout(Some(std::time::Duration::from_millis(timeout_ms)))?;
Self::read_callback(&mut stream, expected_state) Self::read_callback(&mut stream, expected_state)
} }
fn read_callback( fn read_callback(stream: &mut TcpStream, expected_state: &str) -> std::io::Result<String> {
stream: &mut TcpStream,
expected_state: &str,
) -> std::io::Result<String> {
let mut buf = [0u8; 4096]; let mut buf = [0u8; 4096];
let n = stream.read(&mut buf)?; let n = stream.read(&mut buf)?;
let request = String::from_utf8_lossy(&buf[..n]); let request = String::from_utf8_lossy(&buf[..n]);
@@ -68,7 +61,10 @@ impl LoopbackServer {
)); ));
} }
code.ok_or_else(|| { code.ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::InvalidData, "code not found in callback") std::io::Error::new(
std::io::ErrorKind::InvalidData,
"code not found in callback",
)
}) })
} }
@@ -59,17 +59,25 @@ pub struct AuditReport {
impl AuditReport { impl AuditReport {
/// True if any ERROR-level violations exist. /// True if any ERROR-level violations exist.
pub fn has_errors(&self) -> bool { pub fn has_errors(&self) -> bool {
self.violations.iter().any(|v| v.severity == Severity::Error) self.violations
.iter()
.any(|v| v.severity == Severity::Error)
} }
/// Number of errors. /// Number of errors.
pub fn error_count(&self) -> usize { pub fn error_count(&self) -> usize {
self.violations.iter().filter(|v| v.severity == Severity::Error).count() self.violations
.iter()
.filter(|v| v.severity == Severity::Error)
.count()
} }
/// Number of warnings. /// Number of warnings.
pub fn warning_count(&self) -> usize { pub fn warning_count(&self) -> usize {
self.violations.iter().filter(|v| v.severity == Severity::Warning).count() self.violations
.iter()
.filter(|v| v.severity == Severity::Warning)
.count()
} }
} }
@@ -152,11 +160,7 @@ fn forbidden_imports(layer: &str) -> &'static [&'static str] {
} }
/// Scan a single Rust source file for forbidden imports. /// Scan a single Rust source file for forbidden imports.
fn scan_file( fn scan_file(file_path: &Path, layer: &'static str, root: &Path) -> Vec<Violation> {
file_path: &Path,
layer: &'static str,
root: &Path,
) -> Vec<Violation> {
let mut violations = Vec::new(); let mut violations = Vec::new();
let content = match std::fs::read_to_string(file_path) { let content = match std::fs::read_to_string(file_path) {
Ok(c) => c, Ok(c) => c,
@@ -185,10 +189,12 @@ fn scan_file(
// `use crate::` in domain could reference domain-only items — skip. // `use crate::` in domain could reference domain-only items — skip.
continue; continue;
} }
if trimmed.starts_with(&pattern) || trimmed.starts_with(&format!("use {forbidden}::")) { if trimmed.starts_with(&pattern)
|| trimmed.starts_with(&format!("use {forbidden}::"))
{
// Skip test code — test modules commonly import outer layers. // Skip test code — test modules commonly import outer layers.
let is_test = content[..content.len().saturating_sub(1)] let is_test =
.contains("#[cfg(test)]"); content[..content.len().saturating_sub(1)].contains("#[cfg(test)]");
if is_test { if is_test {
continue; continue;
} }
@@ -243,10 +249,7 @@ pub fn audit_layering(root: &Path) -> Result<AuditReport> {
} }
// Determine which crate this file belongs to by walking up. // Determine which crate this file belongs to by walking up.
let layer = path let layer = path.ancestors().skip(1).find_map(|p| classify_layer(p));
.ancestors()
.skip(1)
.find_map(|p| classify_layer(p));
if let Some(layer) = layer { if let Some(layer) = layer {
files_scanned += 1; files_scanned += 1;
@@ -371,7 +374,10 @@ mod tests {
fn function_metrics_short_function_ok() { fn function_metrics_short_function_ok() {
let content = "fn ok() {\n let x = 1;\n}\n"; let content = "fn ok() {\n let x = 1;\n}\n";
let violations = check_function_metrics(content); let violations = check_function_metrics(content);
let long: Vec<_> = violations.iter().filter(|v| v.message.contains("Function too long")).collect(); let long: Vec<_> = violations
.iter()
.filter(|v| v.message.contains("Function too long"))
.collect();
assert!(long.is_empty(), "short function should not trigger"); assert!(long.is_empty(), "short function should not trigger");
} }
@@ -383,7 +389,10 @@ mod tests {
} }
lines.push_str("}\n"); lines.push_str("}\n");
let violations = check_function_metrics(&lines); let violations = check_function_metrics(&lines);
let long: Vec<_> = violations.iter().filter(|v| v.message.contains("Function too long")).collect(); let long: Vec<_> = violations
.iter()
.filter(|v| v.message.contains("Function too long"))
.collect();
assert!(!long.is_empty(), "long function should trigger warning"); assert!(!long.is_empty(), "long function should trigger warning");
} }
} }
@@ -36,7 +36,9 @@ pub struct CodeQualityReport {
impl CodeQualityReport { impl CodeQualityReport {
pub fn has_errors(&self) -> bool { pub fn has_errors(&self) -> bool {
self.findings.iter().any(|f| f.severity == super::arch_audit::Severity::Error) self.findings
.iter()
.any(|f| f.severity == super::arch_audit::Severity::Error)
} }
pub fn count_by_rule(&self) -> Vec<(&'static str, usize)> { pub fn count_by_rule(&self) -> Vec<(&'static str, usize)> {
let mut counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new(); let mut counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
@@ -147,7 +149,8 @@ pub fn scan_quality_file(file_path: &Path, root: &Path) -> Vec<Finding> {
// ── Rule: Missing doc comments on pub items ──────────────────── // ── Rule: Missing doc comments on pub items ────────────────────
if (trimmed.starts_with("pub ") || trimmed.starts_with("pub(")) if (trimmed.starts_with("pub ") || trimmed.starts_with("pub("))
&& !prev_line_doc && !prev_line_empty && !prev_line_doc
&& !prev_line_empty
{ {
// Check it's a struct/enum/fn/trait/type/const/mod // Check it's a struct/enum/fn/trait/type/const/mod
let is_item = trimmed.starts_with("pub fn ") let is_item = trimmed.starts_with("pub fn ")
@@ -246,7 +249,10 @@ mod tests {
std::fs::write(&file, "fn x() { let y = foo.unwrap(); }\n").unwrap(); std::fs::write(&file, "fn x() { let y = foo.unwrap(); }\n").unwrap();
let findings = scan_quality_file(&file, &dir); let findings = scan_quality_file(&file, &dir);
let unwrap_findings: Vec<_> = findings.iter().filter(|f| f.rule == "unwrap-in-production").collect(); let unwrap_findings: Vec<_> = findings
.iter()
.filter(|f| f.rule == "unwrap-in-production")
.collect();
assert!(!unwrap_findings.is_empty(), "should detect unwrap"); assert!(!unwrap_findings.is_empty(), "should detect unwrap");
} }
@@ -262,8 +268,14 @@ mod tests {
.unwrap(); .unwrap();
let findings = scan_quality_file(&file, &dir); let findings = scan_quality_file(&file, &dir);
let unwrap_findings: Vec<_> = findings.iter().filter(|f| f.rule == "unwrap-in-production").collect(); let unwrap_findings: Vec<_> = findings
assert!(unwrap_findings.is_empty(), "should skip unwrap in test blocks"); .iter()
.filter(|f| f.rule == "unwrap-in-production")
.collect();
assert!(
unwrap_findings.is_empty(),
"should skip unwrap in test blocks"
);
} }
#[test] #[test]
@@ -274,7 +286,13 @@ mod tests {
std::fs::write(&file, "#[allow(clippy::too_many_arguments)]\nfn x() {}\n").unwrap(); std::fs::write(&file, "#[allow(clippy::too_many_arguments)]\nfn x() {}\n").unwrap();
let findings = scan_quality_file(&file, &dir); let findings = scan_quality_file(&file, &dir);
let bypass_findings: Vec<_> = findings.iter().filter(|f| f.rule == "compiler-bypass").collect(); let bypass_findings: Vec<_> = findings
assert!(!bypass_findings.is_empty(), "should detect allow attributes"); .iter()
.filter(|f| f.rule == "compiler-bypass")
.collect();
assert!(
!bypass_findings.is_empty(),
"should detect allow attributes"
);
} }
} }
+25 -18
View File
@@ -50,10 +50,9 @@ pub fn validate_commit_message(msg: &str) -> Result<(), Vec<String>> {
} }
// Parse: `type(scope): description` or `type!: description` or `type: description` // Parse: `type(scope): description` or `type!: description` or `type: description`
let re = Regex::new( let re =
r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s+(?P<desc>.+)$", Regex::new(r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s+(?P<desc>.+)$")
) .expect("valid regex for commit parsing");
.expect("valid regex for commit parsing");
match re.captures(subject) { match re.captures(subject) {
None => { None => {
@@ -94,21 +93,19 @@ pub fn validate_commit_message(msg: &str) -> Result<(), Vec<String>> {
} else { } else {
let first_char = desc.chars().next().unwrap_or(' '); let first_char = desc.chars().next().unwrap_or(' ');
if first_char.is_uppercase() { if first_char.is_uppercase() {
errors.push(format!( errors.push(format!("Description must start with lowercase: '{desc}'"));
"Description must start with lowercase: '{desc}'"
));
} }
if desc.ends_with('.') { if desc.ends_with('.') {
errors.push(format!( errors.push(format!("Description must not end with a period: '{desc}'"));
"Description must not end with a period: '{desc}'"
));
} }
} }
// Type-specific rules. // Type-specific rules.
match type_ { match type_ {
"chore" | "docs" | "refactor" | "test" | "style" | "perf" | "ci" | "build" "chore" | "docs" | "refactor" | "test" | "style" | "perf" | "ci" | "build"
| "revert" if scope.is_some() => { | "revert"
if scope.is_some() =>
{
errors.push(format!( errors.push(format!(
"'{type_}' commits should not use a scope. \ "'{type_}' commits should not use a scope. \
Only 'feat' and 'fix' require scopes." Only 'feat' and 'fix' require scopes."
@@ -140,10 +137,9 @@ pub fn parse_commit_message(msg: &str) -> Option<CommitInfo> {
let msg = msg.trim(); let msg = msg.trim();
let subject = msg.lines().next()?; let subject = msg.lines().next()?;
let re = Regex::new( let re =
r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s+(?P<desc>.+)$", Regex::new(r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s+(?P<desc>.+)$")
) .expect("valid regex");
.expect("valid regex");
let caps = re.captures(subject)?; let caps = re.captures(subject)?;
@@ -155,10 +151,18 @@ pub fn parse_commit_message(msg: &str) -> Option<CommitInfo> {
}; };
Some(CommitInfo { Some(CommitInfo {
type_: caps.name("type").map(|m| m.as_str()).unwrap_or("").to_string(), type_: caps
.name("type")
.map(|m| m.as_str())
.unwrap_or("")
.to_string(),
scope: caps.name("scope").map(|m| m.as_str().to_string()), scope: caps.name("scope").map(|m| m.as_str().to_string()),
breaking: caps.name("breaking").is_some(), breaking: caps.name("breaking").is_some(),
description: caps.name("desc").map(|m| m.as_str()).unwrap_or("").to_string(), description: caps
.name("desc")
.map(|m| m.as_str())
.unwrap_or("")
.to_string(),
body, body,
}) })
} }
@@ -232,7 +236,10 @@ mod tests {
#[test] #[test]
fn parses_valid_commit() { fn parses_valid_commit() {
let parsed = parse_commit_message("feat(agent): add parallel execution\n\nWith cycle orchestration.").unwrap(); let parsed = parse_commit_message(
"feat(agent): add parallel execution\n\nWith cycle orchestration.",
)
.unwrap();
assert_eq!(parsed.type_, "feat"); assert_eq!(parsed.type_, "feat");
assert_eq!(parsed.scope, Some("agent".to_string())); assert_eq!(parsed.scope, Some("agent".to_string()));
assert!(!parsed.breaking); assert!(!parsed.breaking);
@@ -51,7 +51,6 @@ const EXPLORE_DIRECTIVES: [&str; 3] = [
4. Identify main entry points (main.rs, main.py, index.ts, etc.).\n\ 4. Identify main entry points (main.rs, main.py, index.ts, etc.).\n\
5. Count files by extension type.\n\ 5. Count files by extension type.\n\
Use the ls_dir, read, grep, and glob tools. Be concise.", Use the ls_dir, read, grep, and glob tools. Be concise.",
// Agent 1: Symbol Index // Agent 1: Symbol Index
"You are a symbol index explorer.\n\ "You are a symbol index explorer.\n\
1. Call the 'rebuild_index' tool to rebuild the symbol index.\n\ 1. Call the 'rebuild_index' tool to rebuild the symbol index.\n\
@@ -59,7 +58,6 @@ const EXPLORE_DIRECTIVES: [&str; 3] = [
3. Identify public APIs, entry points, and key types.\n\ 3. Identify public APIs, entry points, and key types.\n\
4. Group symbols by language and kind.\n\ 4. Group symbols by language and kind.\n\
Be concise. Report what symbols exist and where they live.", Be concise. Report what symbols exist and where they live.",
// Agent 2: Semantic Context // Agent 2: Semantic Context
"You are a semantic context explorer.\n\ "You are a semantic context explorer.\n\
1. Call the 'rebuild_index' tool to ensure the index is fresh.\n\ 1. Call the 'rebuild_index' tool to ensure the index is fresh.\n\
@@ -231,8 +229,8 @@ async fn run_explore_phase(
let tc = tool_ctx.clone(); let tc = tool_ctx.clone();
let handle = thread::spawn(move || { let handle = thread::spawn(move || {
let rt = tokio::runtime::Runtime::new() let rt =
.context("create explore subagent tokio runtime")?; tokio::runtime::Runtime::new().context("create explore subagent tokio runtime")?;
rt.block_on(run_agent(ctx, &directive, AccessTier::Read, tc)) rt.block_on(run_agent(ctx, &directive, AccessTier::Read, tc))
}); });
@@ -252,12 +250,22 @@ async fn run_explore_phase(
} }
Ok(Err(e)) => { Ok(Err(e)) => {
warn!(agent = i, error = %e, "explore subagent failed"); warn!(agent = i, error = %e, "explore subagent failed");
emit_failed(&turn_events_clone, EXPLORE_IDS[i], EXPLORE_LABELS[i], &e.to_string()); emit_failed(
&turn_events_clone,
EXPLORE_IDS[i],
EXPLORE_LABELS[i],
&e.to_string(),
);
(i, format!("Error: {e}"), false) (i, format!("Error: {e}"), false)
} }
Err(e) => { Err(e) => {
warn!(agent = i, error = ?e, "explore subagent panicked"); warn!(agent = i, error = ?e, "explore subagent panicked");
emit_failed(&turn_events_clone, EXPLORE_IDS[i], EXPLORE_LABELS[i], "thread panicked"); emit_failed(
&turn_events_clone,
EXPLORE_IDS[i],
EXPLORE_LABELS[i],
"thread panicked",
);
(i, format!("Thread panic: {e:?}"), false) (i, format!("Thread panic: {e:?}"), false)
} }
}; };
@@ -281,9 +289,7 @@ fn build_explore_context(results: &[(usize, String, bool)]) -> String {
let success_count = results.iter().filter(|r| r.2).count(); let success_count = results.iter().filter(|r| r.2).count();
let total = results.len(); let total = results.len();
let mut msg = format!( let mut msg = format!("[Explore Phase — {success_count}/{total} agents succeeded]\n\n");
"[Explore Phase — {success_count}/{total} agents succeeded]\n\n"
);
for (i, output, success) in results { for (i, output, success) in results {
let label = EXPLORE_LABELS.get(*i).unwrap_or(&"❓ Unknown"); let label = EXPLORE_LABELS.get(*i).unwrap_or(&"❓ Unknown");
+1 -4
View File
@@ -107,10 +107,7 @@ impl BestPracticeEngine {
let layering = self.audit_layering(workspace_root)?; let layering = self.audit_layering(workspace_root)?;
let quality = self.scan_quality(workspace_root)?; let quality = self.scan_quality(workspace_root)?;
Ok(CombinedAuditReport { Ok(CombinedAuditReport { layering, quality })
layering,
quality,
})
} }
} }
+3 -1
View File
@@ -89,6 +89,8 @@ impl BashJob {
let Ok(mut guard) = self.process.lock() else { let Ok(mut guard) = self.process.lock() else {
return false; return false;
}; };
guard.as_mut().is_some_and(|c| matches!(c.try_wait(), Ok(None))) guard
.as_mut()
.is_some_and(|c| matches!(c.try_wait(), Ok(None)))
} }
} }
+8 -10
View File
@@ -7,24 +7,22 @@
pub fn check_dangerous_pattern(tool_name: &str, args: &serde_json::Value) -> Option<String> { pub fn check_dangerous_pattern(tool_name: &str, args: &serde_json::Value) -> Option<String> {
match tool_name { match tool_name {
"bash" => { "bash" => {
let cmd = args let cmd = args.get("command").and_then(|v| v.as_str()).unwrap_or("");
.get("command")
.and_then(|v| v.as_str())
.unwrap_or("");
// Detect git push with --force // Detect git push with --force
if cmd.contains("git push") && cmd.contains("--force") { if cmd.contains("git push") && cmd.contains("--force") {
return Some("Force-pushing to git is destructive and may lose history".to_string()); return Some(
"Force-pushing to git is destructive and may lose history".to_string(),
);
} }
// Detect rm -rf / // Detect rm -rf /
if cmd.contains("rm -rf /") || cmd.contains("rm -rf /*") { if cmd.contains("rm -rf /") || cmd.contains("rm -rf /*") {
return Some("Recursive deletion of the root filesystem is never allowed".to_string()); return Some(
"Recursive deletion of the root filesystem is never allowed".to_string(),
);
} }
} }
"delete" => { "delete" => {
let path = args let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("");
.get("path")
.and_then(|v| v.as_str())
.unwrap_or("");
if path == "/" || path.starts_with("/etc") { if path == "/" || path.starts_with("/etc") {
return Some(format!("Deleting '{}' is too dangerous", path)); return Some(format!("Deleting '{}' is too dangerous", path));
} }
+1 -4
View File
@@ -76,10 +76,7 @@ pub struct StatePayload {
pub enum DaemonFrame { pub enum DaemonFrame {
StateUpdate(Box<StatePayload>), StateUpdate(Box<StatePayload>),
StreamToken(String), StreamToken(String),
SystemNote { SystemNote { kind: String, message: String },
kind: String,
message: String,
},
ClipboardCopy(String), ClipboardCopy(String),
Closed, Closed,
} }
+1 -2
View File
@@ -67,8 +67,7 @@ use std::sync::Arc;
pub use tools::{GraduatedCheck, Tool, ToolCtx, ToolCtxBuilder}; pub use tools::{GraduatedCheck, Tool, ToolCtx, ToolCtxBuilder};
// Re-export commonly needed types at the crate root // Re-export commonly needed types at the crate root
pub use zesdex_domain::core::{ChatMessage, Role, Store, UsageStats, ToolCallResult}; pub use zesdex_domain::core::{ChatMessage, Role, Store, ToolCallResult, UsageStats};
/// A shared, async-writable cache of directory entries, used to avoid /// A shared, async-writable cache of directory entries, used to avoid
/// re-reading a directory every render frame. /// re-reading a directory every render frame.
+16 -23
View File
@@ -4,10 +4,10 @@
use rand_core::RngCore; use rand_core::RngCore;
use std::time::Duration; use std::time::Duration;
use zesdex_application::ports::ProviderService;
use zesdex_domain::core::{ use zesdex_domain::core::{
ChatMessage, ChatRequest, ChatResponse, SseParser, StreamEvent, StreamOptions, ToolDef, ChatMessage, ChatRequest, ChatResponse, SseParser, StreamEvent, StreamOptions, ToolDef,
}; };
use zesdex_application::ports::ProviderService;
use zesdex_domain::agent::defaults::{DEFAULT_API_BASE, DEFAULT_MODEL}; use zesdex_domain::agent::defaults::{DEFAULT_API_BASE, DEFAULT_MODEL};
const DEFAULT_BASE_URL: &str = DEFAULT_API_BASE; const DEFAULT_BASE_URL: &str = DEFAULT_API_BASE;
@@ -82,10 +82,7 @@ impl LlmClient {
retrying without connect timeout", retrying without connect timeout",
e, e,
); );
match reqwest::Client::builder() match reqwest::Client::builder().timeout(REQUEST_TIMEOUT).build() {
.timeout(REQUEST_TIMEOUT)
.build()
{
Ok(c) => c, Ok(c) => c,
Err(e2) => { Err(e2) => {
tracing::warn!("also failed: {e2}. using default client"); tracing::warn!("also failed: {e2}. using default client");
@@ -115,8 +112,7 @@ impl LlmClient {
.post(url) .post(url)
.header("Content-Type", "application/json"); .header("Content-Type", "application/json");
if !self.api_key.is_empty() { if !self.api_key.is_empty() {
http_req = http_req = http_req.header("Authorization", format!("Bearer {}", self.api_key));
http_req.header("Authorization", format!("Bearer {}", self.api_key));
} }
let mut resp = http_req.json(req).send().await.map_err(|e| { let mut resp = http_req.json(req).send().await.map_err(|e| {
@@ -182,7 +178,7 @@ impl LlmClient {
} }
let tc = &mut self.tool_calls[index]; let tc = &mut self.tool_calls[index];
if let Some(ref id_val) = id { if let Some(ref id_val) = id {
tc.id = id_val.clone(); tc.id = id_val.clone();
} }
@@ -237,8 +233,7 @@ impl LlmClient {
n n
} }
}; };
let text = let text = String::from_utf8_lossy(&byte_buf[..valid_len]).into_owned();
String::from_utf8_lossy(&byte_buf[..valid_len]).into_owned();
byte_buf.drain(..valid_len); byte_buf.drain(..valid_len);
for event in parser.feed(&text) { for event in parser.feed(&text) {
@@ -306,8 +301,7 @@ impl ProviderService for LlmClient {
.header("Content-Type", "application/json"); .header("Content-Type", "application/json");
if !self.api_key.is_empty() { if !self.api_key.is_empty() {
http_req = http_req = http_req.header("Authorization", format!("Bearer {}", self.api_key));
http_req.header("Authorization", format!("Bearer {}", self.api_key));
} }
let result = async { let result = async {
@@ -335,9 +329,9 @@ impl ProviderService for LlmClient {
} }
let data: ChatResponse = resp.json().await?; let data: ChatResponse = resp.json().await?;
let usage = data.usage.map(|u| { let usage = data
(u64::from(u.prompt_tokens), u64::from(u.completion_tokens)) .usage
}); .map(|u| (u64::from(u.prompt_tokens), u64::from(u.completion_tokens)));
let message = data let message = data
.choices .choices
.into_iter() .into_iter()
@@ -345,7 +339,8 @@ impl ProviderService for LlmClient {
.and_then(|c| c.message) .and_then(|c| c.message)
.ok_or_else(|| anyhow::anyhow!("API response had no choices"))?; .ok_or_else(|| anyhow::anyhow!("API response had no choices"))?;
Ok((message, usage)) Ok((message, usage))
}.await; }
.await;
match result { match result {
Ok((msg, usage)) => return Ok((msg, usage)), Ok((msg, usage)) => return Ok((msg, usage)),
@@ -393,14 +388,16 @@ impl ProviderService for LlmClient {
let mut captured_content = false; let mut captured_content = false;
let mut wrapped = |event: &StreamEvent| -> bool { let mut wrapped = |event: &StreamEvent| -> bool {
match event { match event {
StreamEvent::Token(_) | StreamEvent::Reasoning(_) | StreamEvent::ToolCallDelta { .. } => { StreamEvent::Token(_)
| StreamEvent::Reasoning(_)
| StreamEvent::ToolCallDelta { .. } => {
captured_content = true; captured_content = true;
} }
_ => {} _ => {}
} }
on_event(event) on_event(event)
}; };
match self.try_stream_once(&req, &url, &mut wrapped).await { match self.try_stream_once(&req, &url, &mut wrapped).await {
Ok(result) => return Ok(result), Ok(result) => return Ok(result),
Err(e) => { Err(e) => {
@@ -425,11 +422,7 @@ pub fn resolve_api_key(
) -> String { ) -> String {
let provider = &settings.provider; let provider = &settings.provider;
let mut api_key = settings let mut api_key = settings.api_keys.get(provider).cloned().unwrap_or_default();
.api_keys
.get(provider)
.cloned()
.unwrap_or_default();
if api_key.is_empty() { if api_key.is_empty() {
if let Some(provider_cfg) = app_config.providers.get(provider) { if let Some(provider_cfg) = app_config.providers.get(provider) {
+10 -2
View File
@@ -32,8 +32,16 @@ impl LspClient {
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.spawn()?; .spawn()?;
let stdin = child.stdin.take().ok_or_else(|| anyhow::anyhow!("no stdin on LSP process"))?; let stdin = child
let stdout = BufReader::new(child.stdout.take().ok_or_else(|| anyhow::anyhow!("no stdout on LSP process"))?); .stdin
.take()
.ok_or_else(|| anyhow::anyhow!("no stdin on LSP process"))?;
let stdout = BufReader::new(
child
.stdout
.take()
.ok_or_else(|| anyhow::anyhow!("no stdout on LSP process"))?,
);
info!("LSP client spawned: {command}"); info!("LSP client spawned: {command}");
Ok(LspClient { Ok(LspClient {
-1
View File
@@ -28,7 +28,6 @@ impl Default for LspManager {
} }
impl LspManager { impl LspManager {
pub fn start(&mut self, language: &str, command: &str, args: &[String]) -> anyhow::Result<()> { pub fn start(&mut self, language: &str, command: &str, args: &[String]) -> anyhow::Result<()> {
let client = LspClient::start(command, args)?; let client = LspClient::start(command, args)?;
self.clients.insert(language.to_string(), client); self.clients.insert(language.to_string(), client);
@@ -9,8 +9,14 @@ fn known_configs() -> HashMap<&'static str, (&'static str, Vec<&'static str>)> {
let mut m = HashMap::new(); let mut m = HashMap::new();
m.insert("rust", ("rust-analyzer", vec![])); m.insert("rust", ("rust-analyzer", vec![]));
m.insert("python", ("pyright-langserver", vec!["--stdio"])); m.insert("python", ("pyright-langserver", vec!["--stdio"]));
m.insert("typescript", ("typescript-language-server", vec!["--stdio"])); m.insert(
m.insert("javascript", ("typescript-language-server", vec!["--stdio"])); "typescript",
("typescript-language-server", vec!["--stdio"]),
);
m.insert(
"javascript",
("typescript-language-server", vec!["--stdio"]),
);
m.insert("go", ("gopls", vec![])); m.insert("go", ("gopls", vec![]));
m m
} }
@@ -14,7 +14,10 @@ pub fn install_language_server(language: &str) -> anyhow::Result<String> {
if output.status.success() { if output.status.success() {
Ok("rust-analyzer installed via rustup".to_string()) Ok("rust-analyzer installed via rustup".to_string())
} else { } else {
anyhow::bail!("failed to install rust-analyzer: {}", String::from_utf8_lossy(&output.stderr)) anyhow::bail!(
"failed to install rust-analyzer: {}",
String::from_utf8_lossy(&output.stderr)
)
} }
} }
"python" => { "python" => {
@@ -24,7 +27,10 @@ pub fn install_language_server(language: &str) -> anyhow::Result<String> {
if output.status.success() { if output.status.success() {
Ok("pyright installed via npm".to_string()) Ok("pyright installed via npm".to_string())
} else { } else {
anyhow::bail!("failed to install pyright: {}", String::from_utf8_lossy(&output.stderr)) anyhow::bail!(
"failed to install pyright: {}",
String::from_utf8_lossy(&output.stderr)
)
} }
} }
lang => anyhow::bail!("no install method known for language '{lang}'"), lang => anyhow::bail!("no install method known for language '{lang}'"),
@@ -1,21 +1,21 @@
//! High-level manager that discovers, installs (if needed), and starts //! High-level manager that discovers, installs (if needed), and starts
//! LSP servers. //! LSP servers.
use crate::lsp::manager::LspManager;
use super::discovery::discover_installed; use super::discovery::discover_installed;
use super::install::install_language_server; use super::install::install_language_server;
use crate::lsp::manager::LspManager;
/// Auto-provision language servers for the given list of languages. /// Auto-provision language servers for the given list of languages.
/// ///
/// Flow: discover already-installed servers → for each requested language /// Flow: discover already-installed servers → for each requested language
/// not yet available, attempt auto-install → start each server. /// not yet available, attempt auto-install → start each server.
pub fn auto_provision( pub fn auto_provision(lsp_manager: &mut LspManager, languages: &[String]) -> Vec<String> {
lsp_manager: &mut LspManager,
languages: &[String],
) -> Vec<String> {
let mut started = Vec::new(); let mut started = Vec::new();
let installed = discover_installed(); let installed = discover_installed();
let mut installed_map: std::collections::HashMap<&str, &crate::lsp::provisioner::config::LspProvisionerConfig> = std::collections::HashMap::new(); let mut installed_map: std::collections::HashMap<
&str,
&crate::lsp::provisioner::config::LspProvisionerConfig,
> = std::collections::HashMap::new();
for cfg in &installed { for cfg in &installed {
installed_map.insert(cfg.language.as_str(), cfg); installed_map.insert(cfg.language.as_str(), cfg);
} }
-1
View File
@@ -31,7 +31,6 @@ impl Default for McpManager {
} }
impl McpManager { impl McpManager {
/// Register an MCP server by name and transport string. /// Register an MCP server by name and transport string.
/// ///
/// Returns an error if a server with the same name is already registered. /// Returns an error if a server with the same name is already registered.
+5 -1
View File
@@ -114,7 +114,11 @@ where
} }
Box::pin(async move { Box::pin(async move {
Ok((StatusCode::UNAUTHORIZED, "missing or invalid X-Session-Id header").into_response()) Ok((
StatusCode::UNAUTHORIZED,
"missing or invalid X-Session-Id header",
)
.into_response())
}) })
} }
} }
@@ -1,26 +0,0 @@
//! CORS layer factory for the daemon HTTP server.
use tower_http::cors::{AllowHeaders, AllowOrigin, CorsLayer};
/// Return a permissive CorsLayer for local daemon IPC.
///
/// All method and header names are static strings guaranteed to be valid
/// HTTP tokens — `.parse()` is infallible here.
pub fn default_cors_layer() -> CorsLayer {
CorsLayer::new()
.allow_origin(AllowOrigin::any())
.allow_methods([
"GET".parse().expect("static HTTP method"),
"POST".parse().expect("static HTTP method"),
"PUT".parse().expect("static HTTP method"),
"DELETE".parse().expect("static HTTP method"),
"PATCH".parse().expect("static HTTP method"),
"OPTIONS".parse().expect("static HTTP method"),
])
.allow_headers(AllowHeaders::any())
.expose_headers([
"Content-Type".parse().expect("static HTTP header"),
"X-Session-Id".parse().expect("static HTTP header"),
"X-Request-Id".parse().expect("static HTTP header"),
])
}
@@ -1,5 +1,4 @@
//! Axum middleware tower for the HTTP API layer. //! Axum middleware tower for the HTTP API layer.
pub mod auth; pub mod auth;
pub mod cors;
pub mod rate_limit; pub mod rate_limit;
@@ -28,11 +28,14 @@ impl RateLimiter {
.as_secs() as i64; .as_secs() as i64;
let cutoff = now.saturating_sub(window_secs as i64); let cutoff = now.saturating_sub(window_secs as i64);
let mut windows = self.windows.lock().map_err(|e| { let mut windows = self
anyhow::anyhow!("rate limiter lock poisoned: {e}") .windows
})?; .lock()
.map_err(|e| anyhow::anyhow!("rate limiter lock poisoned: {e}"))?;
let timestamps = windows.entry(client_id.to_string()).or_insert_with(Vec::new); let timestamps = windows
.entry(client_id.to_string())
.or_insert_with(Vec::new);
timestamps.retain(|&ts| ts >= cutoff); timestamps.retain(|&ts| ts >= cutoff);
if timestamps.len() >= max_requests as usize { if timestamps.len() >= max_requests as usize {
@@ -3,7 +3,9 @@
use std::path::Path; use std::path::Path;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use zesdex_domain::cms::{AppConfig, AppConfigRepository, ModelRole, ProviderConfig, RepositoryError}; use zesdex_domain::cms::{
AppConfig, AppConfigRepository, ModelRole, ProviderConfig, RepositoryError,
};
use crate::utils::write_json_atomic; use crate::utils::write_json_atomic;
@@ -40,12 +42,15 @@ fn claude_settings_from_file() -> Option<ClaudeSettings> {
fn detect_claude_settings_provider() -> Option<(ProviderConfig, Option<String>)> { fn detect_claude_settings_provider() -> Option<(ProviderConfig, Option<String>)> {
let settings = claude_settings_from_file(); let settings = claude_settings_from_file();
let file_creds = settings.as_ref().and_then(|s| { let file_creds = settings.as_ref().and_then(|s| {
let env = s.env.as_ref()?; let env = s.env.as_ref()?;
Some((env.anthropic_base_url.clone()?, env.anthropic_api_key.clone()?)) Some((
env.anthropic_base_url.clone()?,
env.anthropic_api_key.clone()?,
))
}); });
let env_creds = || -> Option<(String, String)> { let env_creds = || -> Option<(String, String)> {
let base_url = std::env::var("ANTHROPIC_BASE_URL").ok()?; let base_url = std::env::var("ANTHROPIC_BASE_URL").ok()?;
let key = std::env::var("ANTHROPIC_API_KEY").ok()?; let key = std::env::var("ANTHROPIC_API_KEY").ok()?;
@@ -55,7 +60,7 @@ fn detect_claude_settings_provider() -> Option<(ProviderConfig, Option<String>)>
let custom_model = settings.and_then(|s| s.custom_model); let custom_model = settings.and_then(|s| s.custom_model);
let (base_url, key) = file_creds.or_else(env_creds)?; let (base_url, key) = file_creds.or_else(env_creds)?;
Some(( Some((
ProviderConfig { ProviderConfig {
api_base: base_url, api_base: base_url,
@@ -63,7 +68,7 @@ fn detect_claude_settings_provider() -> Option<(ProviderConfig, Option<String>)>
default_model: custom_model.clone(), default_model: custom_model.clone(),
default_api_key: Some(key), default_api_key: Some(key),
}, },
custom_model custom_model,
)) ))
} }
@@ -72,9 +77,7 @@ impl AppConfigRepository for JsonAppConfigRepository {
let path = base_dir.join("app_config.json"); let path = base_dir.join("app_config.json");
let mut cfg: AppConfig = match std::fs::read_to_string(&path) { let mut cfg: AppConfig = match std::fs::read_to_string(&path) {
Ok(s) => serde_json::from_str(&s)?, Ok(s) => serde_json::from_str(&s)?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => { Err(e) if e.kind() == std::io::ErrorKind::NotFound => AppConfig::default(),
AppConfig::default()
}
Err(e) => return Err(RepositoryError::Io(e)), Err(e) => return Err(RepositoryError::Io(e)),
}; };
@@ -106,15 +109,13 @@ impl AppConfigRepository for JsonAppConfigRepository {
} }
if let Some(custom) = &custom_model { if let Some(custom) = &custom_model {
cfg.model_roles cfg.model_roles.entry(custom.clone()).or_insert(ModelRole {
.entry(custom.clone()) provider: "claude".to_string(),
.or_insert(ModelRole { model: custom.clone(),
provider: "claude".to_string(), max_tokens: Some(8192),
model: custom.clone(), context_window: Some(200_000),
max_tokens: Some(8192), temperature: Some(0.7),
context_window: Some(200_000), });
temperature: Some(0.7),
});
} }
if cfg.default_provider == defaults.default_provider { if cfg.default_provider == defaults.default_provider {
@@ -77,10 +77,7 @@ impl MarkdownMemoryRepository {
.lines() .lines()
.filter_map(|l| { .filter_map(|l| {
let mut it = l.splitn(2, ':'); let mut it = l.splitn(2, ':');
Some(( Some((it.next()?.trim().to_string(), it.next()?.trim().to_string()))
it.next()?.trim().to_string(),
it.next()?.trim().to_string(),
))
}) })
.collect() .collect()
} }
@@ -175,7 +172,9 @@ impl MemoryRepository for MarkdownMemoryRepository {
fn save(&self, memory_dir: &Path, memory: &Memory) -> Result<(), RepositoryError> { fn save(&self, memory_dir: &Path, memory: &Memory) -> Result<(), RepositoryError> {
let path = Memory::path(memory_dir, &memory.name); let path = Memory::path(memory_dir, &memory.name);
let parent = path.parent().expect("memory path always has a parent directory"); let parent = path
.parent()
.expect("memory path always has a parent directory");
std::fs::create_dir_all(parent)?; std::fs::create_dir_all(parent)?;
let frontmatter = Self::build_frontmatter(memory); let frontmatter = Self::build_frontmatter(memory);
@@ -21,16 +21,16 @@ impl SettingsRepository for JsonSettingsRepository {
fn load(&self, base_dir: &Path) -> Result<Settings, RepositoryError> { fn load(&self, base_dir: &Path) -> Result<Settings, RepositoryError> {
let path = base_dir.join("settings.json"); let path = base_dir.join("settings.json");
match std::fs::read_to_string(&path) { match std::fs::read_to_string(&path) {
Ok(s) => match serde_json::from_str(&s) { Ok(s) => {
Ok(settings) => Ok(settings), match serde_json::from_str(&s) {
Err(e) => { Ok(settings) => Ok(settings),
tracing::warn!("settings.json at '{:?}' failed to parse ({e}); falling back to defaults", path); Err(e) => {
Ok(Settings::default()) tracing::warn!("settings.json at '{:?}' failed to parse ({e}); falling back to defaults", path);
Ok(Settings::default())
}
} }
},
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
Ok(Settings::default())
} }
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Settings::default()),
Err(e) => Err(RepositoryError::Io(e)), Err(e) => Err(RepositoryError::Io(e)),
} }
} }
@@ -50,9 +50,7 @@ impl SessionLockRepository for FileSystemSessionLockRepository {
.write(true) .write(true)
.open(&tmp) .open(&tmp)
.map_err(|_| { .map_err(|_| {
RepositoryError::Other( RepositoryError::Other("another process is replacing the lock".to_string())
"another process is replacing the lock".to_string(),
)
})?; })?;
write!(tmp_file, "{pid}")?; write!(tmp_file, "{pid}")?;
tmp_file.sync_all()?; tmp_file.sync_all()?;
+6 -10
View File
@@ -5,16 +5,12 @@ pub mod cms;
pub mod iam; pub mod iam;
pub mod sqlite; pub mod sqlite;
pub use cms::{
app_config_repo::JsonAppConfigRepository, conversation_repo::JsonConversationRepository,
edit_log_repo::JsonlEditLogRepository, memory_repo::MarkdownMemoryRepository,
rewind_blob_repo::FileRewindBlobRepository, settings_repo::JsonSettingsRepository,
};
pub use iam::{ pub use iam::{
oauth_repo::FileSystemOAuthRepository, oauth_repo::FileSystemOAuthRepository, session_lock_repo::FileSystemSessionLockRepository,
session_lock_repo::FileSystemSessionLockRepository,
session_repo::FileSystemSessionRepository, session_repo::FileSystemSessionRepository,
}; };
pub use cms::{
app_config_repo::JsonAppConfigRepository,
conversation_repo::JsonConversationRepository,
edit_log_repo::JsonlEditLogRepository,
memory_repo::MarkdownMemoryRepository,
rewind_blob_repo::FileRewindBlobRepository,
settings_repo::JsonSettingsRepository,
};
@@ -160,7 +160,8 @@ pub fn spawn_background_review(
SEVERITY: HIGH|MEDIUM|LOW\n\ SEVERITY: HIGH|MEDIUM|LOW\n\
OLD: <exact text to replace>\n\ OLD: <exact text to replace>\n\
NEW: <replacement text>\n\ NEW: <replacement text>\n\
---".to_string(), ---"
.to_string(),
); );
let user_msg = ChatMessage::user(format!( let user_msg = ChatMessage::user(format!(
@@ -210,7 +211,11 @@ pub fn spawn_background_review(
&turn_events, &turn_events,
TurnEvent::SystemNote { TurnEvent::SystemNote {
kind: "review_finding".into(), kind: "review_finding".into(),
message: format!("📋 Auto-review complete ({} fix(es) applied).\n{}", fix_count, response_text.trim()), message: format!(
"📋 Auto-review complete ({} fix(es) applied).\n{}",
fix_count,
response_text.trim()
),
}, },
); );
info!(fix_count, "auto-review: completed with fixes"); info!(fix_count, "auto-review: completed with fixes");
@@ -237,11 +242,7 @@ async fn run_llm_review(client: &LlmClient, messages: &[ChatMessage]) -> Result<
} }
/// Parse the LLM response for structured fix commands and apply them. /// Parse the LLM response for structured fix commands and apply them.
fn apply_fixes_from_response( fn apply_fixes_from_response(response: &str, tools: &[Box<dyn Tool>], tool_ctx: &ToolCtx) -> usize {
response: &str,
tools: &[Box<dyn Tool>],
tool_ctx: &ToolCtx,
) -> usize {
let mut fix_count = 0; let mut fix_count = 0;
// Parse structured fix blocks // Parse structured fix blocks
+12 -14
View File
@@ -98,10 +98,7 @@ pub async fn run_agent(
// If no tool calls, we're done — return content // If no tool calls, we're done — return content
if tool_calls.is_empty() { if tool_calls.is_empty() {
info!("Subagent completed after {iteration} iterations"); info!("Subagent completed after {iteration} iterations");
report_progress( report_progress(&tool_ctx, AgentProgress::completed("subagent", directive));
&tool_ctx,
AgentProgress::completed("subagent", directive),
);
return Ok(content); return Ok(content);
} }
@@ -121,15 +118,14 @@ pub async fn run_agent(
), ),
); );
let result = let result = if let Some(tool) = tools.iter().find(|t| t.name() == tool_name) {
if let Some(tool) = tools.iter().find(|t| t.name() == tool_name) { match tool.run(&tool_ctx, &args) {
match tool.run(&tool_ctx, &args) { Ok(output) => output,
Ok(output) => output, Err(e) => format!("Error: {e}"),
Err(e) => format!("Error: {e}"), }
} } else {
} else { format!("Unknown tool: {tool_name}")
format!("Unknown tool: {tool_name}") };
};
messages.push(ChatMessage::tool(tc.id.clone(), result)); messages.push(ChatMessage::tool(tc.id.clone(), result));
} }
@@ -149,5 +145,7 @@ pub async fn run_agent(
format!("iteration limit ({MAX_ITERATIONS})"), format!("iteration limit ({MAX_ITERATIONS})"),
), ),
); );
Ok(format!("Subagent reached iteration limit ({MAX_ITERATIONS})")) Ok(format!(
"Subagent reached iteration limit ({MAX_ITERATIONS})"
))
} }
+1 -3
View File
@@ -40,9 +40,7 @@ impl SubagentProvider {
messages: &[ChatMessage], messages: &[ChatMessage],
) -> Result<(ChatMessage, Option<(u64, u64)>)> { ) -> Result<(ChatMessage, Option<(u64, u64)>)> {
use zesdex_application::ports::ProviderService; use zesdex_application::ports::ProviderService;
self.client self.client.chat(messages, None, Some(4096), None).await
.chat(messages, None, Some(4096), None)
.await
} }
/// Send messages with available tool definitions. /// Send messages with available tool definitions.
+22 -6
View File
@@ -131,7 +131,8 @@ impl Tool for BestPractice {
} }
// Suggest a template. // Suggest a template.
if let Some(parsed) = eng.parse_commit(&msg) { if let Some(parsed) = eng.parse_commit(&msg) {
let tpl = eng.suggest_commit_template(&parsed.type_, parsed.scope.as_deref()); let tpl =
eng.suggest_commit_template(&parsed.type_, parsed.scope.as_deref());
out.push_str(&format!("\nTemplate: {tpl}\n")); out.push_str(&format!("\nTemplate: {tpl}\n"));
} }
Ok(out) Ok(out)
@@ -309,8 +310,14 @@ mod tests {
let tool = BestPractice; let tool = BestPractice;
let args = json!({"action": "list_skills"}); let args = json!({"action": "list_skills"});
let result = tool.run(&test_ctx(), &args).unwrap(); let result = tool.run(&test_ctx(), &args).unwrap();
assert!(result.contains("clean-code"), "should list clean-code: {result}"); assert!(
assert!(result.contains("commit-convention"), "should list commit-convention: {result}"); result.contains("clean-code"),
"should list clean-code: {result}"
);
assert!(
result.contains("commit-convention"),
"should list commit-convention: {result}"
);
} }
#[test] #[test]
@@ -318,7 +325,10 @@ mod tests {
let tool = BestPractice; let tool = BestPractice;
let args = json!({"action": "get_skill", "skill_name": "clean-code"}); let args = json!({"action": "get_skill", "skill_name": "clean-code"});
let result = tool.run(&test_ctx(), &args).unwrap(); let result = tool.run(&test_ctx(), &args).unwrap();
assert!(result.contains("Clean Code"), "should contain skill content: {result}"); assert!(
result.contains("Clean Code"),
"should contain skill content: {result}"
);
} }
#[test] #[test]
@@ -326,7 +336,10 @@ mod tests {
let tool = CommitConvention; let tool = CommitConvention;
let args = json!({"message": "feat(tool): add best practice audit"}); let args = json!({"message": "feat(tool): add best practice audit"});
let result = tool.run(&test_ctx(), &args).unwrap(); let result = tool.run(&test_ctx(), &args).unwrap();
assert!(result.contains(""), "valid commit should succeed: {result}"); assert!(
result.contains(""),
"valid commit should succeed: {result}"
);
} }
#[test] #[test]
@@ -334,6 +347,9 @@ mod tests {
let tool = CommitConvention; let tool = CommitConvention;
let args = json!({"message": "Add new feature"}); let args = json!({"message": "Add new feature"});
let result = tool.run(&test_ctx(), &args).unwrap(); let result = tool.run(&test_ctx(), &args).unwrap();
assert!(result.contains(""), "invalid commit should fail: {result}"); assert!(
result.contains(""),
"invalid commit should fail: {result}"
);
} }
} }

Some files were not shown because too many files have changed in this diff Show More