diff --git a/Cargo.toml b/Cargo.toml index 4b705a094..2792e9f93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ object_store = "0.12.5" [workspace.lints.clippy] wildcard_imports = "warn" +absolute_paths = "warn" [profile.release] lto = "thin" diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 000000000..ecc515b4f --- /dev/null +++ b/clippy.toml @@ -0,0 +1,2 @@ +absolute-paths-max-segments = 2 +absolute-paths-allowed-crates = ["std", "core", "alloc"] diff --git a/lib/crates/fabro-agent/src/agent_profile.rs b/lib/crates/fabro-agent/src/agent_profile.rs index 4594e70ae..160424932 100644 --- a/lib/crates/fabro-agent/src/agent_profile.rs +++ b/lib/crates/fabro-agent/src/agent_profile.rs @@ -2,13 +2,14 @@ use crate::profiles::EnvContext; use crate::sandbox::Sandbox; use crate::skills::Skill; use crate::subagent::{ - make_close_agent_tool, make_send_input_tool, make_spawn_agent_tool, SessionFactory, - SubAgentManager, + make_close_agent_tool, make_send_input_tool, make_spawn_agent_tool, make_wait_tool, + SessionFactory, SubAgentManager, }; use crate::tool_registry::ToolRegistry; use fabro_llm::types::ToolDefinition; use fabro_model::{Catalog, Provider}; use std::sync::Arc; +use tokio::sync::Mutex; pub trait AgentProfile: Send + Sync { fn provider(&self) -> Provider; @@ -43,7 +44,7 @@ pub trait AgentProfile: Send + Sync { fn register_subagent_tools( &mut self, - manager: Arc>, + manager: Arc>, session_factory: SessionFactory, current_depth: usize, ) { @@ -55,7 +56,7 @@ pub trait AgentProfile: Send + Sync { self.tool_registry_mut() .register(make_send_input_tool(manager.clone())); self.tool_registry_mut() - .register(crate::subagent::make_wait_tool(manager.clone())); + .register(make_wait_tool(manager.clone())); self.tool_registry_mut() .register(make_close_agent_tool(manager)); } diff --git a/lib/crates/fabro-agent/src/cli.rs b/lib/crates/fabro-agent/src/cli.rs index 4945e129b..366376c4b 100644 --- a/lib/crates/fabro-agent/src/cli.rs +++ b/lib/crates/fabro-agent/src/cli.rs @@ -1,16 +1,26 @@ -use crate::config::ToolApprovalFn; +use crate::config::{ToolApprovalAdapter, ToolApprovalFn, ToolHookCallback}; +use crate::error::AbortReason; +use crate::tools::WebFetchSummarizer; +use crate::truncation; use crate::{ subagent::{SessionFactory, SubAgentManager}, AgentEvent, AgentProfile, AnthropicProfile, GeminiProfile, LocalSandbox, OpenAiProfile, - Session, SessionConfig, Turn, + Sandbox, Session, SessionConfig, Turn, }; use clap::{Args, Parser}; use fabro_llm::client::Client; +use fabro_llm::error::SdkError; +use fabro_llm::middleware::{Middleware, NextFn, NextStreamFn}; +use fabro_llm::provider::StreamEventStream; +use fabro_llm::types::{Request, Response}; +use fabro_mcp::config::McpServerConfig; use fabro_model::{Catalog, ModelRef, Provider}; use fabro_util::terminal::Styles; use std::io::{IsTerminal, Write}; use std::path::PathBuf; use std::sync::{Arc, Mutex}; +use tokio::signal; +use tokio::sync::Mutex as AsyncMutex; /// Public arguments for the agent command, usable from an external CLI. #[derive(Args)] @@ -173,12 +183,9 @@ fn summarizer_model_id(provider: Provider) -> ModelRef { } } -fn build_summarizer( - provider: Provider, - llm_client: Option, -) -> Option { +fn build_summarizer(provider: Provider, llm_client: Option) -> Option { let client = llm_client?; - Some(crate::tools::WebFetchSummarizer { + Some(WebFetchSummarizer { client, model_id: summarizer_model_id(provider), }) @@ -218,7 +225,7 @@ fn format_tool_args(args: &serde_json::Value, cwd: &str) -> String { serde_json::Value::String(s) => { let s = s.strip_prefix(&cwd_prefix).unwrap_or(s); let display = if s.len() > 80 { - format!("{}...", &s[..crate::truncation::floor_char_boundary(s, 77)]) + format!("{}...", &s[..truncation::floor_char_boundary(s, 77)]) } else { s.to_string() }; @@ -273,12 +280,8 @@ struct DebugMiddleware { } #[async_trait::async_trait] -impl fabro_llm::middleware::Middleware for DebugMiddleware { - async fn handle_complete( - &self, - request: fabro_llm::types::Request, - next: fabro_llm::middleware::NextFn, - ) -> Result { +impl Middleware for DebugMiddleware { + async fn handle_complete(&self, request: Request, next: NextFn) -> Result { let s = self.styles; eprintln!( "{}", @@ -306,9 +309,9 @@ impl fabro_llm::middleware::Middleware for DebugMiddleware { async fn handle_stream( &self, - request: fabro_llm::types::Request, - next: fabro_llm::middleware::NextStreamFn, - ) -> Result { + request: Request, + next: NextStreamFn, + ) -> Result { next(request).await } } @@ -319,12 +322,8 @@ struct VerboseMiddleware { } #[async_trait::async_trait] -impl fabro_llm::middleware::Middleware for VerboseMiddleware { - async fn handle_complete( - &self, - request: fabro_llm::types::Request, - next: fabro_llm::middleware::NextFn, - ) -> Result { +impl Middleware for VerboseMiddleware { + async fn handle_complete(&self, request: Request, next: NextFn) -> Result { let s = self.styles; eprintln!( "{}\n{}", @@ -344,16 +343,16 @@ impl fabro_llm::middleware::Middleware for VerboseMiddleware { async fn handle_stream( &self, - request: fabro_llm::types::Request, - next: fabro_llm::middleware::NextStreamFn, - ) -> Result { + request: Request, + next: NextStreamFn, + ) -> Result { next(request).await } } pub async fn run_with_args( args: AgentArgs, - mcp_servers: Vec, + mcp_servers: Vec, ) -> anyhow::Result<()> { run_with_args_and_client(args, None, mcp_servers).await } @@ -361,7 +360,7 @@ pub async fn run_with_args( pub async fn run_with_args_and_client( args: AgentArgs, llm_client: Option, - mcp_servers: Vec, + mcp_servers: Vec, ) -> anyhow::Result<()> { // Resolve color support once, leak to get 'static lifetime for use across threads let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); @@ -407,7 +406,7 @@ pub async fn run_with_args_and_client( // Build sandbox let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); let cwd_str = cwd.to_string_lossy().to_string(); - let env: Arc = Arc::new(crate::ReadBeforeWriteSandbox::new(Arc::new( + let env: Arc = Arc::new(crate::ReadBeforeWriteSandbox::new(Arc::new( LocalSandbox::new(cwd), ))); @@ -415,8 +414,7 @@ pub async fn run_with_args_and_client( let permissions = args.permissions.unwrap_or(PermissionLevel::ReadWrite); let is_interactive = std::io::stdin().is_terminal() && !args.auto_approve; let tool_approval = build_tool_approval(permissions, is_interactive, styles); - let tool_hooks: Arc = - Arc::new(crate::config::ToolApprovalAdapter(tool_approval)); + let tool_hooks: Arc = Arc::new(ToolApprovalAdapter(tool_approval)); let config = SessionConfig { tool_hooks: Some(tool_hooks.clone()), @@ -426,7 +424,7 @@ pub async fn run_with_args_and_client( }; // Register subagent tools - let manager = Arc::new(tokio::sync::Mutex::new(SubAgentManager::new( + let manager = Arc::new(AsyncMutex::new(SubAgentManager::new( config.max_subagent_depth, ))); let manager_for_callback = manager.clone(); @@ -490,11 +488,11 @@ pub async fn run_with_args_and_client( let cancel_token = session.cancel_token(); let abort_reason = session.abort_reason_handle(); tokio::spawn(async move { - tokio::signal::ctrl_c().await.ok(); + signal::ctrl_c().await.ok(); { let mut guard = abort_reason.lock().unwrap_or_else(|e| e.into_inner()); if guard.is_none() { - *guard = Some(crate::error::AbortReason::Cancelled); + *guard = Some(AbortReason::Cancelled); } } cancel_token.cancel(); @@ -562,7 +560,7 @@ pub async fn run_with_args_and_client( .. } => { let task_preview = if task.len() > 60 { - &task[..crate::truncation::floor_char_boundary(task, 60)] + &task[..truncation::floor_char_boundary(task, 60)] } else { task }; @@ -773,7 +771,7 @@ mod tests { #[test] fn build_profile_can_register_subagent_tools() { let mut profile = build_profile(Provider::Anthropic, "model", None); - let manager = Arc::new(tokio::sync::Mutex::new(SubAgentManager::new(1))); + let manager = Arc::new(AsyncMutex::new(SubAgentManager::new(1))); let factory: SessionFactory = Arc::new(|| { panic!("factory should not be called in this test"); }); diff --git a/lib/crates/fabro-agent/src/compaction.rs b/lib/crates/fabro-agent/src/compaction.rs index 886429d27..dee426486 100644 --- a/lib/crates/fabro-agent/src/compaction.rs +++ b/lib/crates/fabro-agent/src/compaction.rs @@ -3,6 +3,7 @@ use crate::error::AgentError; use crate::event::EventEmitter; use crate::file_tracker::FileTracker; use crate::history::History; +use crate::truncation; use crate::types::{AgentEvent, Turn}; use fabro_llm::client::Client; use fabro_llm::types::{Message, Request}; @@ -212,7 +213,7 @@ pub fn render_turns_for_summary(turns: &[Turn]) -> String { let truncated = if args_str.len() > 500 { format!( "{}...", - &args_str[..crate::truncation::floor_char_boundary(&args_str, 500)] + &args_str[..truncation::floor_char_boundary(&args_str, 500)] ) } else { args_str @@ -226,8 +227,7 @@ pub fn render_turns_for_summary(turns: &[Turn]) -> String { let truncated = if content_str.len() > 500 { format!( "{}...", - &content_str - [..crate::truncation::floor_char_boundary(&content_str, 500)] + &content_str[..truncation::floor_char_boundary(&content_str, 500)] ) } else { content_str diff --git a/lib/crates/fabro-agent/src/config.rs b/lib/crates/fabro-agent/src/config.rs index eae9af6f0..f474b79c3 100644 --- a/lib/crates/fabro-agent/src/config.rs +++ b/lib/crates/fabro-agent/src/config.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; +use fabro_llm::types::ReasoningEffort; use fabro_mcp::config::McpServerConfig; /// Callback invoked before each tool execution. Return `Ok(())` to allow, @@ -63,7 +64,7 @@ pub struct SessionConfig { pub max_tool_rounds_per_input: usize, pub default_command_timeout_ms: u64, pub max_command_timeout_ms: u64, - pub reasoning_effort: Option, + pub reasoning_effort: Option, pub speed: Option, pub tool_output_limits: HashMap, pub tool_line_limits: HashMap, @@ -187,14 +188,11 @@ mod tests { fn config_with_custom_values() { let config = SessionConfig { max_turns: 50, - reasoning_effort: Some(fabro_llm::types::ReasoningEffort::High), + reasoning_effort: Some(ReasoningEffort::High), ..Default::default() }; assert_eq!(config.max_turns, 50); - assert_eq!( - config.reasoning_effort, - Some(fabro_llm::types::ReasoningEffort::High) - ); + assert_eq!(config.reasoning_effort, Some(ReasoningEffort::High)); assert_eq!(config.max_tool_rounds_per_input, 0); } diff --git a/lib/crates/fabro-agent/src/session.rs b/lib/crates/fabro-agent/src/session.rs index bca746b9c..e4f8ac08c 100644 --- a/lib/crates/fabro-agent/src/session.rs +++ b/lib/crates/fabro-agent/src/session.rs @@ -1,27 +1,37 @@ use crate::agent_profile::AgentProfile; +use crate::compaction::{check_context_usage, compact_context}; use crate::config::SessionConfig; use crate::error::{AbortReason, AgentError}; use crate::event::EventEmitter; use crate::file_tracker::FileTracker; use crate::history::History; use crate::loop_detection::detect_loop; +use crate::mcp_integration; use crate::memory::discover_memory; use crate::profiles::EnvContext; use crate::sandbox::Sandbox; use crate::skills::{ - default_skill_dirs, discover_skills, expand_skill, make_use_skill_tool, Skill, + default_skill_dirs, discover_skills, expand_skill, make_use_skill_tool, ExpandedInput, Skill, }; -use crate::types::{AgentEvent, SessionState, Turn}; +use crate::subagent::{SubAgentEventCallback, SubAgentManager}; +use crate::tool_execution::execute_tool_calls; +use crate::types::{AgentEvent, SessionEvent, SessionState, Turn}; use fabro_llm::client::Client; use fabro_llm::error::{ProviderErrorKind, SdkError}; use fabro_llm::generate::StreamAccumulator; use fabro_llm::provider::StreamEventStream; -use fabro_llm::types::{Message, Request, StreamEvent, ToolChoice}; -use fabro_mcp::config::McpServerConfig; +use fabro_llm::retry; +use fabro_llm::types::{ + ContentPart, Message, ReasoningEffort, Request, RetryPolicy, StreamEvent, ToolChoice, +}; +use fabro_mcp::config::{McpServerConfig, McpTransport}; +use fabro_mcp::connection_manager::McpConnectionManager; use futures::StreamExt; -use std::collections::VecDeque; +use std::collections::{HashMap, VecDeque}; use std::sync::{Arc, Mutex}; use std::time::SystemTime; +use tokio::sync::{broadcast, Mutex as AsyncMutex}; +use tokio::time; use tokio_util::sync::CancellationToken; use tracing::{debug, info, warn}; @@ -43,8 +53,8 @@ pub struct Session { skills: Vec, system_prompt: String, file_tracker: FileTracker, - tool_env: Option>, - subagent_manager: Option>>, + tool_env: Option>, + subagent_manager: Option>>, } impl Session { @@ -54,7 +64,7 @@ impl Session { provider_profile: Arc, sandbox: Arc, config: SessionConfig, - subagent_manager: Option>>, + subagent_manager: Option>>, ) -> Self { Self { id: uuid::Uuid::new_v4().to_string(), @@ -79,7 +89,7 @@ impl Session { } } - pub fn set_tool_env(&mut self, env: std::collections::HashMap) { + pub fn set_tool_env(&mut self, env: HashMap) { self.tool_env = Some(env); } @@ -129,7 +139,7 @@ impl Session { // then rewrite the config to Http using the sandbox's preview URL. let mcp_servers = self.resolve_sandbox_mcp_servers().await; - let mut manager = fabro_mcp::connection_manager::McpConnectionManager::new(); + let mut manager = McpConnectionManager::new(); let results = manager.start_servers(&mcp_servers).await; for (server_name, result) in &results { @@ -156,7 +166,7 @@ impl Session { } let manager = Arc::new(manager); - let mcp_tools = crate::mcp_integration::make_mcp_tools(manager); + let mcp_tools = mcp_integration::make_mcp_tools(manager); if let Some(profile) = Arc::get_mut(&mut self.provider_profile) { for tool in mcp_tools { profile.tool_registry_mut().register(tool); @@ -189,7 +199,7 @@ impl Session { for config in &self.config.mcp_servers { match &config.transport { - fabro_mcp::config::McpTransport::Sandbox { command, port, env } => { + McpTransport::Sandbox { command, port, env } => { let port = *port; match self.start_sandbox_mcp_server(command, port, env).await { Ok((url, headers)) => { @@ -200,7 +210,7 @@ impl Session { ); resolved.push(McpServerConfig { name: config.name.clone(), - transport: fabro_mcp::config::McpTransport::Http { url, headers }, + transport: McpTransport::Http { url, headers }, startup_timeout_secs: config.startup_timeout_secs, tool_timeout_secs: config.tool_timeout_secs, }); @@ -347,7 +357,7 @@ impl Session { } #[must_use] - pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver { + pub fn subscribe(&self) -> broadcast::Receiver { self.event_emitter.subscribe() } @@ -410,9 +420,9 @@ impl Session { &mut self, client: &Client, request: &Request, - retry_policy: &fabro_llm::types::RetryPolicy, + retry_policy: &RetryPolicy, ) -> Result { - let stream_result = fabro_llm::retry::retry(retry_policy, || { + let stream_result = retry::retry(retry_policy, || { let client = client.clone(); let request = request.clone(); async move { client.stream(&request).await } @@ -442,7 +452,7 @@ impl Session { /// Build a callback that forwards `AgentEvent`s through this session's emitter. #[must_use] - pub fn event_callback(&self) -> crate::subagent::SubAgentEventCallback { + pub fn event_callback(&self) -> SubAgentEventCallback { let emitter = self.event_emitter.clone(); let session_id = self.id.clone(); Arc::new(move |event| { @@ -498,7 +508,7 @@ impl Session { self.transition(SessionState::Closed); } - pub fn set_reasoning_effort(&mut self, effort: Option) { + pub fn set_reasoning_effort(&mut self, effort: Option) { self.config.reasoning_effort = effort; } @@ -530,7 +540,7 @@ impl Session { let token = self.cancel_token.clone(); let reason_handle = self.abort_reason.clone(); tokio::spawn(async move { - tokio::time::sleep(duration).await; + time::sleep(duration).await; { let mut guard = reason_handle.lock().unwrap_or_else(|e| e.into_inner()); if guard.is_none() { @@ -581,7 +591,7 @@ impl Session { // Expand skill references in input let expanded = if self.skills.is_empty() { - crate::skills::ExpandedInput { + ExpandedInput { text: input.to_string(), skill_name: None, } @@ -661,7 +671,7 @@ impl Session { let retry_session_id = self.id.clone(); let retry_provider = self.provider_profile.provider().as_str().to_string(); let retry_model = self.provider_profile.model().to_string(); - let retry_policy = fabro_llm::types::RetryPolicy { + let retry_policy = RetryPolicy { max_retries: 3, on_retry: Some(std::sync::Arc::new(move |err, attempt, delay| { retry_emitter.emit( @@ -782,13 +792,7 @@ impl Session { .message .content .iter() - .filter(|p| { - matches!( - p, - fabro_llm::types::ContentPart::Other { .. } - | fabro_llm::types::ContentPart::Thinking(_) - ) - }) + .filter(|p| matches!(p, ContentPart::Other { .. } | ContentPart::Thinking(_))) .cloned() .collect(); let usage = response.usage.clone(); @@ -824,7 +828,7 @@ impl Session { round_count += 1; // Execute tool calls (parallel or sequential based on provider) - let results = crate::tool_execution::execute_tool_calls( + let results = execute_tool_calls( &tool_calls, true, self.provider_profile.tool_registry(), @@ -878,7 +882,7 @@ impl Session { } async fn compact_if_needed(&mut self) { - let over_threshold = crate::compaction::check_context_usage( + let over_threshold = check_context_usage( &self.system_prompt, &self.history, self.provider_profile.as_ref(), @@ -887,7 +891,7 @@ impl Session { &self.id, ); if over_threshold && self.config.enable_context_compaction { - if let Err(e) = crate::compaction::compact_context( + if let Err(e) = compact_context( &mut self.history, &self.llm_client, self.provider_profile.as_ref(), diff --git a/lib/crates/fabro-agent/src/subagent.rs b/lib/crates/fabro-agent/src/subagent.rs index d95f47e07..bbe8407c7 100644 --- a/lib/crates/fabro-agent/src/subagent.rs +++ b/lib/crates/fabro-agent/src/subagent.rs @@ -6,6 +6,8 @@ use crate::types::{AgentEvent, Turn}; use fabro_llm::types::ToolDefinition; use std::collections::{HashMap, VecDeque}; use std::sync::{Arc, Mutex}; +use tokio::sync::Mutex as AsyncMutex; +use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; pub type SessionFactory = Arc Session + Send + Sync>; @@ -26,7 +28,7 @@ pub enum SubAgentStatus { } pub struct SubAgent { - task: Option>>, + task: Option>>, followup_queue: Arc>>, cancel_token: CancellationToken, depth: usize, @@ -303,7 +305,7 @@ impl SubAgentManager { } pub fn make_spawn_agent_tool( - manager: Arc>, + manager: Arc>, session_factory: SessionFactory, current_depth: usize, ) -> RegisteredTool { @@ -358,7 +360,7 @@ pub fn make_spawn_agent_tool( } } -pub fn make_send_input_tool(manager: Arc>) -> RegisteredTool { +pub fn make_send_input_tool(manager: Arc>) -> RegisteredTool { RegisteredTool { definition: ToolDefinition { name: "send_input".into(), @@ -393,7 +395,7 @@ pub fn make_send_input_tool(manager: Arc>) - } } -pub fn make_wait_tool(manager: Arc>) -> RegisteredTool { +pub fn make_wait_tool(manager: Arc>) -> RegisteredTool { RegisteredTool { definition: ToolDefinition { name: "wait".into(), @@ -425,7 +427,7 @@ pub fn make_wait_tool(manager: Arc>) -> Regi } } -pub fn make_close_agent_tool(manager: Arc>) -> RegisteredTool { +pub fn make_close_agent_tool(manager: Arc>) -> RegisteredTool { RegisteredTool { definition: ToolDefinition { name: "close_agent".into(), diff --git a/lib/crates/fabro-agent/src/tool_execution.rs b/lib/crates/fabro-agent/src/tool_execution.rs index 1acb8c53e..08d0dbfb8 100644 --- a/lib/crates/fabro-agent/src/tool_execution.rs +++ b/lib/crates/fabro-agent/src/tool_execution.rs @@ -1,10 +1,11 @@ use crate::config::{SessionConfig, ToolHookCallback, ToolHookDecision}; use crate::event::EventEmitter; use crate::sandbox::Sandbox; -use crate::tool_registry::ToolRegistry; +use crate::tool_registry::{RegisteredTool, ToolContext, ToolRegistry}; use crate::truncation::truncate_tool_output; use crate::types::AgentEvent; -use fabro_llm::types::ToolResult; +use fabro_llm::types::{ToolCall, ToolResult}; +use futures::future; use std::collections::HashMap; use std::sync::Arc; use tokio_util::sync::CancellationToken; @@ -13,7 +14,7 @@ use tracing::debug; /// Execute tool calls, choosing parallel or sequential based on `parallel` flag. #[allow(clippy::too_many_arguments)] pub async fn execute_tool_calls( - tool_calls: &[fabro_llm::types::ToolCall], + tool_calls: &[ToolCall], parallel: bool, registry: &ToolRegistry, env: Arc, @@ -55,7 +56,7 @@ pub async fn execute_tool_calls( #[allow(clippy::too_many_arguments)] async fn execute_tool_calls_sequential( - tool_calls: &[fabro_llm::types::ToolCall], + tool_calls: &[ToolCall], registry: &ToolRegistry, env: Arc, tool_hooks: Option<&Arc>, @@ -91,7 +92,7 @@ async fn execute_tool_calls_sequential( #[allow(clippy::too_many_arguments)] async fn execute_tool_calls_parallel( - tool_calls: &[fabro_llm::types::ToolCall], + tool_calls: &[ToolCall], registry: &ToolRegistry, env: Arc, tool_hooks: Option<&Arc>, @@ -132,13 +133,13 @@ async fn execute_tool_calls_parallel( }) .collect(); - futures::future::join_all(futures).await + future::join_all(futures).await } /// Execute a single tool call with event emission and output truncation. #[allow(clippy::too_many_arguments)] pub async fn execute_and_emit_one_tool( - tc: &fabro_llm::types::ToolCall, + tc: &ToolCall, registry: &ToolRegistry, env: Arc, tool_hooks: Option<&Arc>, @@ -165,8 +166,8 @@ pub async fn execute_and_emit_one_tool( /// Execute a single tool call with event emission, using a pre-looked-up tool reference. #[allow(clippy::too_many_arguments)] async fn execute_and_emit_one_tool_with_lookup( - tc: &fabro_llm::types::ToolCall, - registered_tool: Option<&crate::tool_registry::RegisteredTool>, + tc: &ToolCall, + registered_tool: Option<&RegisteredTool>, env: Arc, tool_hooks: Option<&Arc>, cancel_token: CancellationToken, @@ -262,8 +263,8 @@ async fn execute_and_emit_one_tool_with_lookup( /// Execute a single tool call: argument validation and execution. async fn execute_one_tool( - tc: &fabro_llm::types::ToolCall, - registered_tool: Option<&crate::tool_registry::RegisteredTool>, + tc: &ToolCall, + registered_tool: Option<&RegisteredTool>, env: Arc, cancel_token: CancellationToken, tool_env: Option<&HashMap>, @@ -276,7 +277,7 @@ async fn execute_one_tool( return ToolResult::error(&tc.id, validation_error); } - let ctx = crate::tool_registry::ToolContext { + let ctx = ToolContext { env, cancel: cancel_token, tool_env: tool_env.cloned(), diff --git a/lib/crates/fabro-agent/src/tools.rs b/lib/crates/fabro-agent/src/tools.rs index e00ad4583..a371e2c10 100644 --- a/lib/crates/fabro-agent/src/tools.rs +++ b/lib/crates/fabro-agent/src/tools.rs @@ -1,6 +1,6 @@ use crate::config::SessionConfig; use crate::sandbox::GrepOptions; -use crate::tool_registry::RegisteredTool; +use crate::tool_registry::{RegisteredTool, ToolRegistry}; use fabro_llm::client::Client; use fabro_llm::types::{Message, Request, ToolDefinition}; use fabro_model::ModelRef; @@ -47,7 +47,7 @@ fn html_to_markdown(text: &str) -> String { /// `SessionConfig` (e.g. with a longer `default_command_timeout_ms`) for providers /// that need non-default shell behavior. pub fn register_core_tools( - registry: &mut crate::tool_registry::ToolRegistry, + registry: &mut ToolRegistry, config: &SessionConfig, summarizer: Option, ) { diff --git a/lib/crates/fabro-agent/src/types.rs b/lib/crates/fabro-agent/src/types.rs index 4b9ad64a1..63d8b695c 100644 --- a/lib/crates/fabro-agent/src/types.rs +++ b/lib/crates/fabro-agent/src/types.rs @@ -1,9 +1,12 @@ +use crate::error::AgentError; +use fabro_llm::error::SdkError; use fabro_llm::types::{ContentPart, ThinkingData, ToolCall, ToolResult, Usage}; use serde::{Deserialize, Serialize}; use std::time::SystemTime; mod system_time_iso8601 { - use chrono::{DateTime, Utc}; + use chrono::{DateTime, SecondsFormat, Utc}; + use serde::de::Error as DeError; use serde::{self, Deserialize, Deserializer, Serializer}; use std::time::SystemTime; @@ -12,7 +15,7 @@ mod system_time_iso8601 { S: Serializer, { let dt: DateTime = (*time).into(); - serializer.serialize_str(&dt.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)) + serializer.serialize_str(&dt.to_rfc3339_opts(SecondsFormat::Millis, true)) } pub fn deserialize<'de, D>(deserializer: D) -> Result @@ -20,7 +23,7 @@ mod system_time_iso8601 { D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; - let dt = DateTime::parse_from_rfc3339(&s).map_err(serde::de::Error::custom)?; + let dt = DateTime::parse_from_rfc3339(&s).map_err(DeError::custom)?; Ok(dt.with_timezone(&Utc).into()) } } @@ -128,7 +131,7 @@ pub enum AgentEvent { is_error: bool, }, Error { - error: crate::error::AgentError, + error: AgentError, }, Warning { kind: String, @@ -160,7 +163,7 @@ pub enum AgentEvent { model: String, attempt: usize, delay_secs: f64, - error: fabro_llm::error::SdkError, + error: SdkError, }, SubAgentSpawned { agent_id: String, @@ -176,7 +179,7 @@ pub enum AgentEvent { SubAgentFailed { agent_id: String, depth: usize, - error: crate::error::AgentError, + error: AgentError, }, SubAgentClosed { agent_id: String, @@ -481,7 +484,7 @@ mod tests { let event = AgentEvent::SubAgentFailed { agent_id: "sa-1".into(), depth: 0, - error: crate::error::AgentError::ToolExecution("timeout".into()), + error: AgentError::ToolExecution("timeout".into()), }; assert!(matches!(event, AgentEvent::SubAgentFailed { depth: 0, .. })); } @@ -527,7 +530,7 @@ mod tests { AgentEvent::SubAgentFailed { agent_id: "sa-1".into(), depth: 0, - error: crate::error::AgentError::ToolExecution("oops".into()), + error: AgentError::ToolExecution("oops".into()), }, AgentEvent::SubAgentClosed { agent_id: "sa-1".into(), @@ -669,7 +672,7 @@ mod tests { #[test] fn error_event_serde_roundtrip_with_agent_error() { let event = AgentEvent::Error { - error: crate::error::AgentError::Llm(fabro_llm::error::SdkError::Network { + error: AgentError::Llm(fabro_llm::error::SdkError::Network { message: "refused".into(), source: None, }), @@ -720,7 +723,7 @@ mod tests { let event = AgentEvent::SubAgentFailed { agent_id: "sa-1".into(), depth: 0, - error: crate::error::AgentError::ToolExecution("cmd failed".into()), + error: AgentError::ToolExecution("cmd failed".into()), }; let json = serde_json::to_string(&event).unwrap(); let deserialized: AgentEvent = serde_json::from_str(&json).unwrap(); @@ -735,7 +738,7 @@ mod tests { #[test] fn error_event_preserves_error_type_through_json() { let event = AgentEvent::Error { - error: crate::error::AgentError::ToolExecution("cmd failed".into()), + error: AgentError::ToolExecution("cmd failed".into()), }; let json = serde_json::to_string(&event).unwrap(); let v: serde_json::Value = serde_json::from_str(&json).unwrap(); diff --git a/lib/crates/fabro-api-types/build.rs b/lib/crates/fabro-api-types/build.rs index 2f31c5ad1..5be485f4b 100644 --- a/lib/crates/fabro-api-types/build.rs +++ b/lib/crates/fabro-api-types/build.rs @@ -1,5 +1,6 @@ use std::{env, fs, path::Path}; +use schemars::schema::Schema; use typify::{TypeSpace, TypeSpaceSettings}; fn main() { @@ -23,10 +24,10 @@ fn main() { .as_object() .expect("no components/schemas in spec"); - let named_schemas: Vec<(String, schemars::schema::Schema)> = schemas + let named_schemas: Vec<(String, Schema)> = schemas .iter() .map(|(name, value)| { - let schema: schemars::schema::Schema = serde_json::from_value(value.clone()) + let schema: Schema = serde_json::from_value(value.clone()) .unwrap_or_else(|e| panic!("failed to parse schema {name}: {e}")); (name.clone(), schema) }) diff --git a/lib/crates/fabro-api-types/src/lib.rs b/lib/crates/fabro-api-types/src/lib.rs index 58035b43d..ecb7d0772 100644 --- a/lib/crates/fabro-api-types/src/lib.rs +++ b/lib/crates/fabro-api-types/src/lib.rs @@ -1,4 +1,4 @@ -#[allow(clippy::derivable_impls)] +#[allow(clippy::absolute_paths, clippy::derivable_impls)] mod generated { include!(concat!(env!("OUT_DIR"), "/openapi_types.rs")); } diff --git a/lib/crates/fabro-api/src/github_webhooks.rs b/lib/crates/fabro-api/src/github_webhooks.rs index 27d542141..d359a2c66 100644 --- a/lib/crates/fabro-api/src/github_webhooks.rs +++ b/lib/crates/fabro-api/src/github_webhooks.rs @@ -6,6 +6,8 @@ use axum::Router; use hmac::{Hmac, Mac}; use sha2::Sha256; use tokio::net::TcpListener; +use tokio::process::Command; +use tokio::sync::oneshot; use tracing::{debug, error, info, warn}; type HmacSha256 = Hmac; @@ -108,7 +110,7 @@ fn parse_event_metadata(body: &[u8]) -> (String, String) { /// A running webhook listener that can be shut down. pub struct WebhookListener { port: u16, - shutdown_tx: tokio::sync::oneshot::Sender<()>, + shutdown_tx: oneshot::Sender<()>, } impl WebhookListener { @@ -132,7 +134,7 @@ pub async fn spawn_webhook_listener(secret: Vec) -> anyhow::Result(); + let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); tokio::spawn(async move { axum::serve(listener, router) @@ -198,7 +200,7 @@ impl WebhookManager { } async fn enable_tailscale_funnel(port: u16) -> anyhow::Result { - let output = tokio::process::Command::new("tailscale") + let output = Command::new("tailscale") .args(["funnel", &port.to_string()]) .output() .await?; @@ -209,7 +211,7 @@ async fn enable_tailscale_funnel(port: u16) -> anyhow::Result { } // Get the funnel URL from `tailscale funnel status` - let status_output = tokio::process::Command::new("tailscale") + let status_output = Command::new("tailscale") .args(["funnel", "status"]) .output() .await?; @@ -233,7 +235,7 @@ async fn enable_tailscale_funnel(port: u16) -> anyhow::Result { } async fn disable_tailscale_funnel(port: u16) { - match tokio::process::Command::new("tailscale") + match Command::new("tailscale") .args(["funnel", "off", &port.to_string()]) .output() .await diff --git a/lib/crates/fabro-api/src/jwt_auth.rs b/lib/crates/fabro-api/src/jwt_auth.rs index 32a9c49f1..1d3d7b11d 100644 --- a/lib/crates/fabro-api/src/jwt_auth.rs +++ b/lib/crates/fabro-api/src/jwt_auth.rs @@ -2,12 +2,14 @@ use std::sync::Arc; use axum::extract::FromRequestParts; use axum::http::request::Parts; +use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use jsonwebtoken::{Algorithm, DecodingKey, Validation}; use rustls_pki_types::CertificateDer; use serde::Deserialize; use tracing::warn; use crate::error::ApiError; +use fabro_config::server::ApiSettings; /// JWT claims for service-to-service authentication. #[derive(Debug, Deserialize)] @@ -57,7 +59,7 @@ pub fn decode_pem_env(name: &str, value: &str) -> String { if value.starts_with("-----") { return value.to_string(); } - let bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, value) + let bytes = base64::Engine::decode(&BASE64_STANDARD, value) .unwrap_or_else(|e| panic!("{name} is not valid PEM or base64: {e}")); String::from_utf8(bytes) .unwrap_or_else(|e| panic!("{name} base64 decoded to invalid UTF-8: {e}")) @@ -67,10 +69,7 @@ pub fn decode_pem_env(name: &str, value: &str) -> String { /// /// Call this once at startup before serving requests. Panics if the /// configuration is invalid (JWT strategy but no public key, or mTLS without TLS config). -pub fn resolve_auth_mode( - api_config: &fabro_config::server::ApiSettings, - allowed_usernames: Vec, -) -> AuthMode { +pub fn resolve_auth_mode(api_config: &ApiSettings, allowed_usernames: Vec) -> AuthMode { use fabro_config::server::ApiAuthStrategy; if api_config.authentication_strategies.is_empty() { diff --git a/lib/crates/fabro-api/src/lib.rs b/lib/crates/fabro-api/src/lib.rs index 1a2dc05bb..e2cf51524 100644 --- a/lib/crates/fabro-api/src/lib.rs +++ b/lib/crates/fabro-api/src/lib.rs @@ -1,4 +1,4 @@ -#[allow(clippy::wildcard_imports)] +#[allow(clippy::wildcard_imports, clippy::absolute_paths)] mod demo; pub mod error; pub mod github_webhooks; diff --git a/lib/crates/fabro-api/src/serve.rs b/lib/crates/fabro-api/src/serve.rs index f6613b7fb..16a510a43 100644 --- a/lib/crates/fabro-api/src/serve.rs +++ b/lib/crates/fabro-api/src/serve.rs @@ -2,18 +2,23 @@ use std::path::PathBuf; use std::sync::{Arc, RwLock}; use std::time::Duration; +use fabro_config::server::{load_server_settings, resolve_storage_dir}; use fabro_model::{Catalog, Provider}; use fabro_util::terminal::Styles; +use fabro_workflows::git::GitAuthor; use tokio::net::TcpListener; +use tokio::time::interval; use tracing::{error, info, warn}; use clap::Args; use fabro_config::FabroSettings; -use crate::jwt_auth::{AuthMode, AuthStrategy}; -use crate::server::build_router; -use crate::tls::ClientAuth; +use crate::github_webhooks::WebhookManager; +use crate::jwt_auth::{decode_pem_env, resolve_auth_mode, AuthMode, AuthStrategy}; +use crate::server::{build_router, create_app_state_with_options, spawn_scheduler}; +use crate::tls::{build_rustls_config, serve_tls, ClientAuth}; +use fabro_llm::client::Client as LlmClient; use fabro_sandbox::SandboxProvider; use fabro_workflows::pipeline::LlmSpec; @@ -62,7 +67,7 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow:: let dry_run_mode = if args.dry_run { true } else { - match fabro_llm::client::Client::from_env().await { + match LlmClient::from_env().await { Ok(c) if c.provider_names().is_empty() => { eprintln!( "{} No LLM providers configured. Running in dry-run mode.", @@ -83,8 +88,8 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow:: // Initialize data directory and SQLite database let config_path = args.config; - let server_settings = fabro_config::server::load_server_settings(config_path.as_deref())?; - let data_dir = fabro_config::server::resolve_storage_dir(&server_settings); + let server_settings = load_server_settings(config_path.as_deref())?; + let data_dir = resolve_storage_dir(&server_settings); // Shared config for live reloading let shared_config = Arc::new(RwLock::new(server_settings)); @@ -121,7 +126,7 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow:: .as_ref() .map(|w| w.auth.allowed_usernames.clone()) .unwrap_or_default(); - let auth_mode = crate::jwt_auth::resolve_auth_mode(&api, allowed_usernames); + let auth_mode = resolve_auth_mode(&api, allowed_usernames); let client_auth = api.tls.as_ref().map(|_| client_auth_from_mode(&auth_mode)); let max_concurrent_runs = args .max_concurrent_runs @@ -133,7 +138,7 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow:: let git_author = { let cfg = shared_config.read().expect("config lock poisoned"); let author = cfg.git_author(); - fabro_workflows::git::GitAuthor::from_options( + GitAuthor::from_options( author.and_then(|a| a.name.clone()), author.and_then(|a| a.email.clone()), ) @@ -142,7 +147,7 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow:: let cfg = shared_config.read().expect("config lock poisoned"); cfg.hooks.clone() }; - let state = crate::server::create_app_state_with_options( + let state = create_app_state_with_options( db, factory, dry_run_mode, @@ -150,7 +155,7 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow:: git_author, hooks, ); - crate::server::spawn_scheduler(Arc::clone(&state)); + spawn_scheduler(Arc::clone(&state)); let router = build_router(state, auth_mode); let addr = format!("{}:{}", args.host, args.port); @@ -183,13 +188,7 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow:: let private_key_pem = read_github_private_key(); match (secret, private_key_pem) { (Some(secret), Some(pem)) => { - match crate::github_webhooks::WebhookManager::start( - secret.into_bytes(), - &app_id, - &pem, - ) - .await - { + match WebhookManager::start(secret.into_bytes(), &app_id, &pem).await { Ok(manager) => Some(manager), Err(err) => { error!(error = %err, "Failed to start webhook listener"); @@ -210,11 +209,11 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow:: let config_for_poll = Arc::clone(&shared_config); let config_path_for_poll = config_path.clone(); tokio::spawn(async move { - let mut interval = tokio::time::interval(Duration::from_secs(5)); + let mut interval = interval(Duration::from_secs(5)); interval.tick().await; // skip first immediate tick loop { interval.tick().await; - match fabro_config::server::load_server_settings(config_path_for_poll.as_deref()) { + match load_server_settings(config_path_for_poll.as_deref()) { Ok(new_config) => { let changed = { let cfg = config_for_poll.read().expect("config lock poisoned"); @@ -243,12 +242,12 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow:: if let Some(ref tls_config) = tls_config { let client_auth = client_auth.unwrap(); - let rustls_config = crate::tls::build_rustls_config(tls_config, client_auth); + let rustls_config = build_rustls_config(tls_config, client_auth); let tls_acceptor = tokio_rustls::TlsAcceptor::from(rustls_config); info!("TLS enabled"); - crate::tls::serve_tls(listener, tls_acceptor, router).await?; + serve_tls(listener, tls_acceptor, router).await?; } else { axum::serve(listener, router).await?; } @@ -308,10 +307,7 @@ fn resolve_model_provider( /// Read the GitHub App private key from the environment, decoding base64 if needed. fn read_github_private_key() -> Option { let raw = std::env::var("GITHUB_APP_PRIVATE_KEY").ok()?; - Some(crate::jwt_auth::decode_pem_env( - "GITHUB_APP_PRIVATE_KEY", - &raw, - )) + Some(decode_pem_env("GITHUB_APP_PRIVATE_KEY", &raw)) } /// Derive client certificate verification mode from the resolved auth strategies. diff --git a/lib/crates/fabro-api/src/server.rs b/lib/crates/fabro-api/src/server.rs index 263ca0c4e..9cf52cfc6 100644 --- a/lib/crates/fabro-api/src/server.rs +++ b/lib/crates/fabro-api/src/server.rs @@ -3,21 +3,41 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Duration; -use axum::extract::{Path, Query, State}; +use axum::extract::{self as axum_extract, Path, Query, State}; use axum::http::StatusCode; -use axum::response::sse::{Event, Sse}; +use axum::response::sse::{Event, KeepAlive, Sse}; use axum::response::{IntoResponse, Response}; use axum::routing::{get, post}; use axum::{Json, Router}; +use fabro_config::sandbox::SandboxSettings; +use fabro_llm::client::Client as LlmClient; +use fabro_llm::generate::{generate, generate_object, GenerateParams}; +use fabro_llm::types::{ + ContentPart, FinishReason, Message as LlmMessage, Request as LlmRequest, + Response as LlmResponse, Role, StreamEvent, ToolChoice, ToolDefinition, Usage, +}; +use fabro_retro::retro::{derive_retro, extract_stage_durations, Retro}; +use fabro_util::redact::redact_jsonl_line; +use fabro_workflows::error::FabroError; +use fabro_workflows::git::GitAuthor; +use fabro_workflows::handler::HandlerRegistry; +use futures_util::stream; use tokio::sync::broadcast; +use tokio::sync::oneshot; +use tokio::sync::{Notify, OnceCell}; +use tokio::task::spawn_blocking; +use tokio::time::{sleep, timeout}; use tokio_stream::wrappers::BroadcastStream; use tokio_stream::StreamExt; -use tower::ServiceExt; +use tower::{service_fn, ServiceExt}; use tracing::{error, info}; +use crate::demo; use crate::error::ApiError; use crate::jwt_auth::{AuthMode, AuthenticatedService, AuthenticatedUser}; +use crate::sessions as sessions_mod; +use crate::sessions::{new_session_store, SessionStore}; use fabro_interview::{Answer, Interviewer, QuestionType, WebInterviewer}; use fabro_retro::RetroExt; use fabro_workflows::context::Context; @@ -75,7 +95,7 @@ struct ManagedRun { event_tx: Option>, context: Option, checkpoint: Option, - cancel_tx: Option>, + cancel_tx: Option>, cancel_token: Option>, run_dir: Option, } @@ -98,8 +118,7 @@ struct AggregateUsageTotals { } type LlmSpecFactory = dyn Fn() -> LlmSpec + Send + Sync; -type RegistryFactoryOverride = - dyn Fn(Arc) -> fabro_workflows::handler::HandlerRegistry + Send + Sync; +type RegistryFactoryOverride = dyn Fn(Arc) -> HandlerRegistry + Send + Sync; /// Shared application state for the server. pub struct AppState { @@ -110,11 +129,11 @@ pub struct AppState { pub dry_run: bool, pub db: sqlx::SqlitePool, max_concurrent_runs: usize, - scheduler_notify: tokio::sync::Notify, + scheduler_notify: Notify, pub hooks: Vec, - git_author: fabro_workflows::git::GitAuthor, - pub sessions: crate::sessions::SessionStore, - llm_client: tokio::sync::OnceCell, + git_author: GitAuthor, + pub sessions: SessionStore, + llm_client: OnceCell, } /// Build the axum Router with all run endpoints. @@ -140,7 +159,7 @@ pub fn build_router(state: Arc, auth_mode: AuthMode) -> Router { .layer(axum::Extension(auth_mode)) .with_state(state); - let dispatch = tower::service_fn(move |req: axum::extract::Request| { + let dispatch = service_fn(move |req: axum_extract::Request| { let demo = demo_router.clone(); let real = real_router.clone(); async move { @@ -157,99 +176,78 @@ pub fn build_router(state: Arc, auth_mode: AuthMode) -> Router { fn demo_routes() -> Router> { Router::new() - .route( - "/runs", - get(crate::demo::list_runs).post(crate::demo::start_run_stub), - ) - .route("/runs/{id}", get(crate::demo::get_run_status)) - .route("/runs/{id}/questions", get(crate::demo::get_questions_stub)) - .route( - "/runs/{id}/questions/{qid}/answer", - post(crate::demo::answer_stub), - ) - .route("/runs/{id}/events", get(crate::demo::run_events_stub)) - .route("/runs/{id}/checkpoint", get(crate::demo::checkpoint_stub)) - .route("/runs/{id}/context", get(crate::demo::context_stub)) - .route("/runs/{id}/cancel", post(crate::demo::cancel_stub)) - .route("/runs/{id}/pause", post(crate::demo::pause_stub)) - .route("/runs/{id}/unpause", post(crate::demo::unpause_stub)) - .route("/runs/{id}/graph", get(crate::demo::get_run_graph)) - .route("/runs/{id}/retro", get(crate::demo::get_run_retro)) - .route("/runs/{id}/stages", get(crate::demo::get_run_stages)) + .route("/runs", get(demo::list_runs).post(demo::start_run_stub)) + .route("/runs/{id}", get(demo::get_run_status)) + .route("/runs/{id}/questions", get(demo::get_questions_stub)) + .route("/runs/{id}/questions/{qid}/answer", post(demo::answer_stub)) + .route("/runs/{id}/events", get(demo::run_events_stub)) + .route("/runs/{id}/checkpoint", get(demo::checkpoint_stub)) + .route("/runs/{id}/context", get(demo::context_stub)) + .route("/runs/{id}/cancel", post(demo::cancel_stub)) + .route("/runs/{id}/pause", post(demo::pause_stub)) + .route("/runs/{id}/unpause", post(demo::unpause_stub)) + .route("/runs/{id}/graph", get(demo::get_run_graph)) + .route("/runs/{id}/retro", get(demo::get_run_retro)) + .route("/runs/{id}/stages", get(demo::get_run_stages)) .route( "/runs/{id}/stages/{stageId}/turns", - get(crate::demo::get_stage_turns), - ) - .route("/runs/{id}/files", get(crate::demo::get_run_files)) - .route("/runs/{id}/usage", get(crate::demo::get_run_usage)) - .route( - "/runs/{id}/verification", - get(crate::demo::get_run_verification), - ) - .route("/runs/{id}/settings", get(crate::demo::get_run_settings)) - .route("/runs/{id}/steer", post(crate::demo::steer_run_stub)) - .route( - "/runs/{id}/preview", - post(crate::demo::generate_preview_url_stub), - ) - .route("/workflows", get(crate::demo::list_workflows)) - .route("/workflows/{name}", get(crate::demo::get_workflow)) - .route( - "/workflows/{name}/runs", - get(crate::demo::list_workflow_runs), + get(demo::get_stage_turns), ) + .route("/runs/{id}/files", get(demo::get_run_files)) + .route("/runs/{id}/usage", get(demo::get_run_usage)) + .route("/runs/{id}/verification", get(demo::get_run_verification)) + .route("/runs/{id}/settings", get(demo::get_run_settings)) + .route("/runs/{id}/steer", post(demo::steer_run_stub)) + .route("/runs/{id}/preview", post(demo::generate_preview_url_stub)) + .route("/workflows", get(demo::list_workflows)) + .route("/workflows/{name}", get(demo::get_workflow)) + .route("/workflows/{name}/runs", get(demo::list_workflow_runs)) .route( "/verification/criteria", - get(crate::demo::list_verification_criteria), + get(demo::list_verification_criteria), ) .route( "/verification/criteria/{id}", - get(crate::demo::get_verification_criterion), + get(demo::get_verification_criterion), ) .route( "/verification/controls", - get(crate::demo::list_verification_controls), + get(demo::list_verification_controls), ) .route( "/verification/controls/{id}", - get(crate::demo::get_verification_control), + get(demo::get_verification_control), ) .route( "/verification/signoffs", - get(crate::demo::list_signoffs).post(crate::demo::create_signoff_stub), + get(demo::list_signoffs).post(demo::create_signoff_stub), ) - .route("/verification/signoffs/{id}", get(crate::demo::get_signoff)) - .route("/retros", get(crate::demo::list_retros)) + .route("/verification/signoffs/{id}", get(demo::get_signoff)) + .route("/retros", get(demo::list_retros)) .route( "/sessions", - get(crate::demo::list_sessions).post(crate::demo::create_session_stub), - ) - .route("/sessions/{id}", get(crate::demo::get_session)) - .route( - "/sessions/{id}/messages", - post(crate::demo::send_message_stub), - ) - .route( - "/sessions/{id}/events", - get(crate::demo::session_events_stub), + get(demo::list_sessions).post(demo::create_session_stub), ) + .route("/sessions/{id}", get(demo::get_session)) + .route("/sessions/{id}/messages", post(demo::send_message_stub)) + .route("/sessions/{id}/events", get(demo::session_events_stub)) .route( "/insights/queries", - get(crate::demo::list_saved_queries).post(crate::demo::save_query_stub), + get(demo::list_saved_queries).post(demo::save_query_stub), ) .route( "/insights/queries/{id}", - get(crate::demo::get_saved_query) - .put(crate::demo::update_query_stub) - .delete(crate::demo::delete_query_stub), + get(demo::get_saved_query) + .put(demo::update_query_stub) + .delete(demo::delete_query_stub), ) - .route("/insights/execute", post(crate::demo::execute_query_stub)) - .route("/insights/history", get(crate::demo::list_query_history)) - .route("/models", get(crate::demo::list_models)) + .route("/insights/execute", post(demo::execute_query_stub)) + .route("/insights/history", get(demo::list_query_history)) + .route("/models", get(demo::list_models)) .route("/models/{id}/test", post(test_model)) .route("/completions", post(create_completion)) - .route("/settings", get(crate::demo::get_server_settings)) - .route("/usage", get(crate::demo::get_aggregate_usage)) + .route("/settings", get(demo::get_server_settings)) + .route("/usage", get(demo::get_aggregate_usage)) } fn real_routes() -> Router> { @@ -289,16 +287,13 @@ fn real_routes() -> Router> { .route("/retros", get(not_implemented)) .route( "/sessions", - get(crate::sessions::list_sessions).post(crate::sessions::create_session), - ) - .route("/sessions/{id}", get(crate::sessions::retrieve_session)) - .route( - "/sessions/{id}/messages", - post(crate::sessions::send_message), + get(sessions_mod::list_sessions).post(sessions_mod::create_session), ) + .route("/sessions/{id}", get(sessions_mod::retrieve_session)) + .route("/sessions/{id}/messages", post(sessions_mod::send_message)) .route( "/sessions/{id}/events", - get(crate::sessions::stream_session_events), + get(sessions_mod::stream_session_events), ) .route( "/insights/queries", @@ -312,7 +307,7 @@ fn real_routes() -> Router> { ) .route("/insights/execute", post(not_implemented)) .route("/insights/history", get(not_implemented)) - .route("/models", get(crate::demo::list_models)) + .route("/models", get(demo::list_models)) .route("/models/{id}/test", post(test_model)) .route("/completions", post(create_completion)) .route("/settings", get(not_implemented)) @@ -393,7 +388,7 @@ pub fn create_app_state( llm_spec_factory, false, 5, - fabro_workflows::git::GitAuthor::default(), + GitAuthor::default(), Vec::new(), ) } @@ -402,10 +397,7 @@ pub fn create_app_state( pub fn create_app_state_with_registry_factory( db: sqlx::SqlitePool, llm_spec_factory: impl Fn() -> LlmSpec + Send + Sync + 'static, - registry_factory_override: impl Fn(Arc) -> fabro_workflows::handler::HandlerRegistry - + Send - + Sync - + 'static, + registry_factory_override: impl Fn(Arc) -> HandlerRegistry + Send + Sync + 'static, ) -> Arc { build_app_state( db, @@ -413,7 +405,7 @@ pub fn create_app_state_with_registry_factory( Some(Box::new(registry_factory_override)), false, 5, - fabro_workflows::git::GitAuthor::default(), + GitAuthor::default(), Vec::new(), ) } @@ -424,7 +416,7 @@ pub fn create_app_state_with_options( llm_spec_factory: impl Fn() -> LlmSpec + Send + Sync + 'static, dry_run: bool, max_concurrent_runs: usize, - git_author: fabro_workflows::git::GitAuthor, + git_author: GitAuthor, hooks: Vec, ) -> Arc { build_app_state( @@ -444,7 +436,7 @@ fn build_app_state( registry_factory_override: Option>, dry_run: bool, max_concurrent_runs: usize, - git_author: fabro_workflows::git::GitAuthor, + git_author: GitAuthor, hooks: Vec, ) -> Arc { Arc::new(AppState { @@ -455,11 +447,11 @@ fn build_app_state( dry_run, db, max_concurrent_runs, - scheduler_notify: tokio::sync::Notify::new(), + scheduler_notify: Notify::new(), hooks, git_author, - sessions: crate::sessions::new_session_store(), - llm_client: tokio::sync::OnceCell::new(), + sessions: new_session_store(), + llm_client: OnceCell::new(), }) } @@ -524,7 +516,7 @@ async fn start_run( let settings = fabro_config::FabroSettings { dry_run: Some(state.dry_run), hooks: state.hooks.clone(), - sandbox: Some(fabro_config::sandbox::SandboxSettings { + sandbox: Some(SandboxSettings { provider: Some("local".to_string()), ..Default::default() }), @@ -544,7 +536,7 @@ async fn start_run( base_branch: None, }) { Ok(created) => created, - Err(ref err @ fabro_workflows::error::FabroError::ValidationFailed { ref diagnostics }) => { + Err(ref err @ FabroError::ValidationFailed { ref diagnostics }) => { let message = if diagnostics.is_empty() { err.to_string() } else { @@ -556,7 +548,7 @@ async fn start_run( }; return ApiError::bad_request(message).into_response(); } - Err(err @ fabro_workflows::error::FabroError::Parse(_)) => { + Err(err @ FabroError::Parse(_)) => { return ApiError::bad_request(err.to_string()).into_response(); } Err(err) => { @@ -619,7 +611,7 @@ async fn execute_run(state: Arc, run_id: String) { None => return, }; - let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>(); + let (cancel_tx, cancel_rx) = oneshot::channel::<()>(); let cancel_token = Arc::new(AtomicBool::new(false)); let (event_tx, _) = broadcast::channel(256); @@ -751,7 +743,7 @@ async fn execute_run(state: Arc, run_id: String) { }, ) .await?; - Ok::<_, fabro_workflows::error::FabroError>(pipeline::execute(initialized).await) + Ok::<_, FabroError>(pipeline::execute(initialized).await) } }; @@ -778,8 +770,8 @@ async fn execute_run(state: Arc, run_id: String) { if let Some(ref cp) = checkpoint { let failed = result.is_err(); let completed_stages = fabro_workflows::build_completed_stages(cp, failed); - let stage_durations = fabro_retro::retro::extract_stage_durations(&run_options.run_dir); - let retro = fabro_retro::retro::derive_retro( + let stage_durations = extract_stage_durations(&run_options.run_dir); + let retro = derive_retro( &run_id, "workflow", "", @@ -817,7 +809,7 @@ async fn execute_run(state: Arc, run_id: String) { info!(run_id = %run_id, "Run completed"); managed_run.status = RunStatus::Completed; } - Err(fabro_workflows::error::FabroError::Cancelled) => { + Err(FabroError::Cancelled) => { info!(run_id = %run_id, "Run cancelled"); managed_run.status = RunStatus::Cancelled; } @@ -844,7 +836,7 @@ pub fn spawn_scheduler(state: Arc) { loop { tokio::select! { _ = state.scheduler_notify.notified() => {}, - _ = tokio::time::sleep(std::time::Duration::from_secs(1)) => {}, + _ = sleep(std::time::Duration::from_secs(1)) => {}, } // Promote as many queued runs as capacity allows loop { @@ -1045,7 +1037,7 @@ async fn get_events( let stream = BroadcastStream::new(rx).filter_map(|result| match result { Ok(event) => { let data = serde_json::to_string(&event).unwrap_or_default(); - let data = fabro_util::redact::redact_jsonl_line(&data); + let data = redact_jsonl_line(&data); Some(Ok::( Event::default().data(data), )) @@ -1196,16 +1188,12 @@ async fn test_model( .into_response(); } - let params = fabro_llm::generate::GenerateParams::new(&info.id) + let params = GenerateParams::new(&info.id) .provider(info.provider.as_str()) .prompt("Say OK") .max_tokens(16); - let result = tokio::time::timeout( - Duration::from_secs(30), - fabro_llm::generate::generate(params), - ) - .await; + let result = timeout(Duration::from_secs(30), generate(params)).await; match result { Ok(Ok(_)) => Json(serde_json::json!({ @@ -1228,26 +1216,26 @@ async fn test_model( } } -fn finish_reason_to_api_stop_reason(reason: &fabro_llm::types::FinishReason) -> String { +fn finish_reason_to_api_stop_reason(reason: &FinishReason) -> String { match reason { - fabro_llm::types::FinishReason::Stop => "end_turn".to_string(), - fabro_llm::types::FinishReason::Length => "max_tokens".to_string(), - fabro_llm::types::FinishReason::ToolCalls => "tool_calls".to_string(), - fabro_llm::types::FinishReason::ContentFilter => "content_filter".to_string(), - fabro_llm::types::FinishReason::Error => "error".to_string(), - fabro_llm::types::FinishReason::Other(s) => s.clone(), + FinishReason::Stop => "end_turn".to_string(), + FinishReason::Length => "max_tokens".to_string(), + FinishReason::ToolCalls => "tool_calls".to_string(), + FinishReason::ContentFilter => "content_filter".to_string(), + FinishReason::Error => "error".to_string(), + FinishReason::Other(s) => s.clone(), } } -fn convert_api_message(msg: &fabro_api_types::CompletionMessage) -> fabro_llm::types::Message { +fn convert_api_message(msg: &fabro_api_types::CompletionMessage) -> LlmMessage { let role = match msg.role { - fabro_api_types::CompletionMessageRole::System => fabro_llm::types::Role::System, - fabro_api_types::CompletionMessageRole::User => fabro_llm::types::Role::User, - fabro_api_types::CompletionMessageRole::Assistant => fabro_llm::types::Role::Assistant, - fabro_api_types::CompletionMessageRole::Tool => fabro_llm::types::Role::Tool, - fabro_api_types::CompletionMessageRole::Developer => fabro_llm::types::Role::Developer, + fabro_api_types::CompletionMessageRole::System => Role::System, + fabro_api_types::CompletionMessageRole::User => Role::User, + fabro_api_types::CompletionMessageRole::Assistant => Role::Assistant, + fabro_api_types::CompletionMessageRole::Tool => Role::Tool, + fabro_api_types::CompletionMessageRole::Developer => Role::Developer, }; - let content: Vec = msg + let content: Vec = msg .content .iter() .filter_map(|part| { @@ -1255,7 +1243,7 @@ fn convert_api_message(msg: &fabro_api_types::CompletionMessage) -> fabro_llm::t serde_json::from_value(json).ok() }) .collect(); - fabro_llm::types::Message { + LlmMessage { role, content, name: msg.name.clone(), @@ -1263,13 +1251,13 @@ fn convert_api_message(msg: &fabro_api_types::CompletionMessage) -> fabro_llm::t } } -fn convert_llm_message(msg: &fabro_llm::types::Message) -> fabro_api_types::CompletionMessage { +fn convert_llm_message(msg: &LlmMessage) -> fabro_api_types::CompletionMessage { let role = match msg.role { - fabro_llm::types::Role::System => fabro_api_types::CompletionMessageRole::System, - fabro_llm::types::Role::User => fabro_api_types::CompletionMessageRole::User, - fabro_llm::types::Role::Assistant => fabro_api_types::CompletionMessageRole::Assistant, - fabro_llm::types::Role::Tool => fabro_api_types::CompletionMessageRole::Tool, - fabro_llm::types::Role::Developer => fabro_api_types::CompletionMessageRole::Developer, + Role::System => fabro_api_types::CompletionMessageRole::System, + Role::User => fabro_api_types::CompletionMessageRole::User, + Role::Assistant => fabro_api_types::CompletionMessageRole::Assistant, + Role::Tool => fabro_api_types::CompletionMessageRole::Tool, + Role::Developer => fabro_api_types::CompletionMessageRole::Developer, }; let content: Vec = msg .content @@ -1310,22 +1298,22 @@ async fn create_completion( info!(model = %model_id, provider = ?provider_name, "Completion request received"); // Build messages list - let mut messages: Vec = Vec::new(); + let mut messages: Vec = Vec::new(); if let Some(system) = req.system { - messages.push(fabro_llm::types::Message::system(system)); + messages.push(LlmMessage::system(system)); } for msg in &req.messages { messages.push(convert_api_message(msg)); } // Convert tools - let tools: Option> = if req.tools.is_empty() { + let tools: Option> = if req.tools.is_empty() { None } else { Some( req.tools .into_iter() - .map(|t| fabro_llm::types::ToolDefinition { + .map(|t| ToolDefinition { name: t.name, description: t.description, parameters: t.parameters, @@ -1335,20 +1323,17 @@ async fn create_completion( }; // Convert tool_choice - let tool_choice: Option = - req.tool_choice.map(|tc| match tc.mode { - fabro_api_types::CompletionToolChoiceMode::Auto => fabro_llm::types::ToolChoice::Auto, - fabro_api_types::CompletionToolChoiceMode::None => fabro_llm::types::ToolChoice::None, - fabro_api_types::CompletionToolChoiceMode::Required => { - fabro_llm::types::ToolChoice::Required - } - fabro_api_types::CompletionToolChoiceMode::Named => { - fabro_llm::types::ToolChoice::named(tc.tool_name.unwrap_or_default()) - } - }); + let tool_choice: Option = req.tool_choice.map(|tc| match tc.mode { + fabro_api_types::CompletionToolChoiceMode::Auto => ToolChoice::Auto, + fabro_api_types::CompletionToolChoiceMode::None => ToolChoice::None, + fabro_api_types::CompletionToolChoiceMode::Required => ToolChoice::Required, + fabro_api_types::CompletionToolChoiceMode::Named => { + ToolChoice::named(tc.tool_name.unwrap_or_default()) + } + }); // Build the LLM request - let request = fabro_llm::types::Request { + let request = LlmRequest { model: model_id.clone(), messages, provider: provider_name, @@ -1376,23 +1361,23 @@ async fn create_completion( if state.dry_run { let msg_id = ulid::Ulid::new().to_string(); if use_stream { - let finish_event = fabro_llm::types::StreamEvent::finish( - fabro_llm::types::FinishReason::Stop, - fabro_llm::types::Usage::default(), - fabro_llm::types::Response { + let finish_event = StreamEvent::finish( + FinishReason::Stop, + Usage::default(), + LlmResponse { id: msg_id.clone(), model: model_id.clone(), provider: String::new(), - message: fabro_llm::types::Message::assistant(""), - finish_reason: fabro_llm::types::FinishReason::Stop, - usage: fabro_llm::types::Usage::default(), + message: LlmMessage::assistant(""), + finish_reason: FinishReason::Stop, + usage: Usage::default(), raw: None, warnings: vec![], rate_limit: None, }, ); let json = serde_json::to_string(&finish_event).unwrap_or_default(); - let sse_stream = futures_util::stream::iter(vec![Ok::<_, std::convert::Infallible>( + let sse_stream = stream::iter(vec![Ok::<_, std::convert::Infallible>( Event::default().event("stream_event").data(json), )]); return Sse::new(sse_stream).into_response(); @@ -1418,11 +1403,7 @@ async fn create_completion( } // Get or create LLM client (cached in AppState) - let client = match state - .llm_client - .get_or_try_init(fabro_llm::client::Client::from_env) - .await - { + let client = match state.llm_client.get_or_try_init(LlmClient::from_env).await { Ok(c) => c, Err(e) => { return ApiError::new( @@ -1469,13 +1450,11 @@ async fn create_completion( Sse::new(sse_stream) .keep_alive( - axum::response::sse::KeepAlive::new() - .interval(Duration::from_secs(15)) - .event( - Event::default() - .event("ping") - .data(serde_json::json!({"type": "ping"}).to_string()), - ), + KeepAlive::new().interval(Duration::from_secs(15)).event( + Event::default() + .event("ping") + .data(serde_json::json!({"type": "ping"}).to_string()), + ), ) .into_response() } else { @@ -1484,7 +1463,7 @@ async fn create_completion( if let Some(schema) = req.schema { // Structured output uses generate_object for JSON parsing logic - let mut params = fabro_llm::generate::GenerateParams::new(&request.model) + let mut params = GenerateParams::new(&request.model) .messages(request.messages) .client(std::sync::Arc::new(client.clone())); if let Some(ref p) = request.provider { @@ -1499,7 +1478,7 @@ async fn create_completion( if let Some(top_p) = request.top_p { params = params.top_p(top_p); } - match fabro_llm::generate::generate_object(params, schema).await { + match generate_object(params, schema).await { Ok(result) => Json(fabro_api_types::CompletionResponse { id: msg_id, model: model_id, @@ -1553,7 +1532,7 @@ async fn get_retro( return (StatusCode::OK, Json(serde_json::json!(null))).into_response(); }; - match fabro_retro::retro::Retro::load(&run_dir) { + match Retro::load(&run_dir) { Ok(retro) => (StatusCode::OK, Json(retro)).into_response(), Err(_) => (StatusCode::OK, Json(serde_json::json!(null))).into_response(), } @@ -1564,7 +1543,7 @@ pub(crate) async fn render_dot_svg(dot_source: &str) -> Response { use fabro_graphviz::render::{render_dot, GraphFormat}; let source = dot_source.to_owned(); - match tokio::task::spawn_blocking(move || render_dot(&source, GraphFormat::Svg)).await { + match spawn_blocking(move || render_dot(&source, GraphFormat::Svg)).await { Ok(Ok(bytes)) => { (StatusCode::OK, [("content-type", "image/svg+xml")], bytes).into_response() } @@ -1675,7 +1654,7 @@ mod tests { test_llm_spec, true, 5, - fabro_workflows::git::GitAuthor::default(), + GitAuthor::default(), Vec::new(), ); let app = build_router(state, AuthMode::Disabled); @@ -1702,7 +1681,7 @@ mod tests { test_llm_spec, true, 5, - fabro_workflows::git::GitAuthor::default(), + GitAuthor::default(), Vec::new(), ); let app = build_router(state, AuthMode::Disabled); @@ -2393,7 +2372,7 @@ mod tests { test_llm_spec, false, 1, - fabro_workflows::git::GitAuthor::default(), + GitAuthor::default(), Vec::new(), ); let app = test_app_with_scheduler(state); @@ -2481,7 +2460,7 @@ mod tests { test_llm_spec, true, 5, - fabro_workflows::git::GitAuthor::default(), + GitAuthor::default(), Vec::new(), ); let app = build_router(state, AuthMode::Disabled); @@ -2518,7 +2497,7 @@ mod tests { test_llm_spec, true, 5, - fabro_workflows::git::GitAuthor::default(), + GitAuthor::default(), Vec::new(), ); let app = build_router(state, AuthMode::Disabled); diff --git a/lib/crates/fabro-api/src/sessions.rs b/lib/crates/fabro-api/src/sessions.rs index cf97c388b..ecec1d903 100644 --- a/lib/crates/fabro-api/src/sessions.rs +++ b/lib/crates/fabro-api/src/sessions.rs @@ -7,11 +7,14 @@ use axum::http::StatusCode; use axum::response::sse::{Event, Sse}; use axum::response::{IntoResponse, Response}; use axum::Json; +use fabro_llm::generate::{stream as llm_stream, GenerateParams}; +use fabro_llm::types::{Message as LlmMessage, StreamEvent}; use tokio::sync::broadcast; +use tokio_stream::wrappers::BroadcastStream; use crate::error::ApiError; use crate::jwt_auth::AuthenticatedService; -use crate::server::PaginationParams; +use crate::server::{AppState, PaginationParams}; pub type SessionStore = Arc>>; @@ -72,15 +75,13 @@ fn resolve_model(model_arg: Option) -> (String, Option) { } } -fn turns_to_messages(turns: &[fabro_api_types::SessionTurn]) -> Vec { +fn turns_to_messages(turns: &[fabro_api_types::SessionTurn]) -> Vec { turns .iter() .filter_map(|turn| match turn { - fabro_api_types::SessionTurn::UserTurn(t) => { - Some(fabro_llm::types::Message::user(&t.content)) - } + fabro_api_types::SessionTurn::UserTurn(t) => Some(LlmMessage::user(&t.content)), fabro_api_types::SessionTurn::AssistantTurn(t) => { - Some(fabro_llm::types::Message::assistant(&t.content)) + Some(LlmMessage::assistant(&t.content)) } fabro_api_types::SessionTurn::ToolTurn(_) => None, }) @@ -136,7 +137,7 @@ fn spawn_generation(store: SessionStore, session_id: uuid::Uuid, dry_run: bool, return; } - let mut params = fabro_llm::generate::GenerateParams::new(&model_id) + let mut params = GenerateParams::new(&model_id) .messages(messages) .max_tokens(4096); if let Some(ref provider) = model_provider { @@ -146,7 +147,7 @@ fn spawn_generation(store: SessionStore, session_id: uuid::Uuid, dry_run: bool, params = params.system(system); } - let stream_result = match fabro_llm::generate::stream(params).await { + let stream_result = match llm_stream(params).await { Ok(s) => s, Err(e) => { let _ = event_tx.send(SessionEvent::Error { @@ -165,7 +166,7 @@ fn spawn_generation(store: SessionStore, session_id: uuid::Uuid, dry_run: bool, return; } match event { - Ok(fabro_llm::types::StreamEvent::TextDelta { delta, .. }) => { + Ok(StreamEvent::TextDelta { delta, .. }) => { full_text.push_str(&delta); let _ = event_tx.send(SessionEvent::TextDelta { delta }); } @@ -207,7 +208,7 @@ fn spawn_generation(store: SessionStore, session_id: uuid::Uuid, dry_run: bool, pub async fn create_session( _auth: AuthenticatedService, - State(state): State>, + State(state): State>, Json(req): Json, ) -> Response { let (model_id, model_provider) = resolve_model(req.model); @@ -259,7 +260,7 @@ pub async fn create_session( pub async fn retrieve_session( _auth: AuthenticatedService, - State(state): State>, + State(state): State>, Path(id): Path, ) -> Response { let store = state.sessions.read().expect("session store lock poisoned"); @@ -284,7 +285,7 @@ pub async fn retrieve_session( pub async fn send_message( _auth: AuthenticatedService, - State(state): State>, + State(state): State>, Path(id): Path, Json(req): Json, ) -> Response { @@ -318,7 +319,7 @@ pub async fn send_message( pub async fn stream_session_events( _auth: AuthenticatedService, - State(state): State>, + State(state): State>, Path(id): Path, ) -> Response { let rx = { @@ -331,46 +332,45 @@ pub async fn stream_session_events( use tokio_stream::StreamExt; - let stream = - tokio_stream::wrappers::BroadcastStream::new(rx).filter_map(|result| match result { - Ok(event) => { - let sse: Option = match event { - SessionEvent::TextDelta { delta } => Some( - Event::default() - .event("content_delta") - .data(serde_json::json!({"delta": delta}).to_string()), + let stream = BroadcastStream::new(rx).filter_map(|result| match result { + Ok(event) => { + let sse: Option = match event { + SessionEvent::TextDelta { delta } => Some( + Event::default() + .event("content_delta") + .data(serde_json::json!({"delta": delta}).to_string()), + ), + SessionEvent::AssistantTurnComplete { + content, + created_at, + } => Some( + Event::default().event("assistant_turn").data( + serde_json::json!({ + "kind": "assistant", + "content": content, + "created_at": created_at, + }) + .to_string(), ), - SessionEvent::AssistantTurnComplete { - content, - created_at, - } => Some( - Event::default().event("assistant_turn").data( - serde_json::json!({ - "kind": "assistant", - "content": content, - "created_at": created_at, - }) - .to_string(), - ), - ), - SessionEvent::Done => Some(Event::default().event("done").data("{}")), - SessionEvent::Error { message } => Some( - Event::default() - .event("error") - .data(serde_json::json!({"message": message}).to_string()), - ), - }; - sse.map(Ok::<_, std::convert::Infallible>) - } - Err(_) => None, - }); + ), + SessionEvent::Done => Some(Event::default().event("done").data("{}")), + SessionEvent::Error { message } => Some( + Event::default() + .event("error") + .data(serde_json::json!({"message": message}).to_string()), + ), + }; + sse.map(Ok::<_, std::convert::Infallible>) + } + Err(_) => None, + }); Sse::new(stream).into_response() } pub async fn list_sessions( _auth: AuthenticatedService, - State(state): State>, + State(state): State>, Query(pagination): Query, ) -> Response { let store = state.sessions.read().expect("session store lock poisoned"); diff --git a/lib/crates/fabro-api/src/tls.rs b/lib/crates/fabro-api/src/tls.rs index bb7fd32f6..226c8db29 100644 --- a/lib/crates/fabro-api/src/tls.rs +++ b/lib/crates/fabro-api/src/tls.rs @@ -65,6 +65,8 @@ pub async fn serve_tls( tls_acceptor: tokio_rustls::TlsAcceptor, router: axum::Router, ) -> anyhow::Result<()> { + use hyper::body::Incoming; + use hyper::service::service_fn; use hyper_util::rt::{TokioExecutor, TokioIo}; use hyper_util::server::conn::auto::Builder; use tower_service::Service; @@ -94,13 +96,11 @@ pub async fn serve_tls( let io = TokioIo::new(tls_stream); - let service = hyper::service::service_fn( - move |mut req: hyper::Request| { - req.extensions_mut().insert(peer_certs.clone()); - let mut router = router.clone(); - async move { router.call(req).await } - }, - ); + let service = service_fn(move |mut req: hyper::Request| { + req.extensions_mut().insert(peer_certs.clone()); + let mut router = router.clone(); + async move { router.call(req).await } + }); if let Err(e) = builder.serve_connection(io, service).await { error!(%remote_addr, "connection error: {e}"); diff --git a/lib/crates/fabro-cli/src/args.rs b/lib/crates/fabro-cli/src/args.rs index b8c29c4be..4a6a9bf1e 100644 --- a/lib/crates/fabro-cli/src/args.rs +++ b/lib/crates/fabro-cli/src/args.rs @@ -2,6 +2,9 @@ use std::fmt; use std::path::PathBuf; use clap::{Args, Subcommand, ValueEnum}; +use fabro_agent::cli::AgentArgs; +use fabro_graphviz::render::GraphFormat; +use fabro_llm::cli::{ChatArgs, ModelsCommand, PromptArgs}; #[cfg(feature = "server")] use crate::cli_config; @@ -273,7 +276,7 @@ pub(crate) enum GraphOutputFormat { Png, } -impl From for fabro_graphviz::render::GraphFormat { +impl From for GraphFormat { fn from(value: GraphOutputFormat) -> Self { match value { GraphOutputFormat::Svg => Self::Svg, @@ -726,7 +729,7 @@ pub(crate) enum Commands { Llm(LlmNamespace), /// Run an agentic coding session #[command(hide = true)] - Exec(fabro_agent::cli::AgentArgs), + Exec(AgentArgs), #[command(flatten)] RunCmd(RunCommands), /// Validate run configuration without executing @@ -745,7 +748,7 @@ pub(crate) enum Commands { /// List and test LLM models Model { #[command(subcommand)] - command: Option, + command: Option, }, /// Start the HTTP API server #[cfg(feature = "server")] @@ -825,8 +828,8 @@ impl Commands { Self::Parse(_) => "parse", Self::RunsCmd(cmd) => cmd.name(), Self::Model { command } => match command { - Some(fabro_llm::cli::ModelsCommand::List { .. }) => "model list", - Some(fabro_llm::cli::ModelsCommand::Test { .. }) => "model test", + Some(ModelsCommand::List { .. }) => "model list", + Some(ModelsCommand::Test { .. }) => "model test", None => "model", }, #[cfg(feature = "server")] @@ -1009,9 +1012,9 @@ pub(crate) struct LlmNamespace { #[derive(Subcommand)] pub(crate) enum LlmCommand { /// Execute a prompt - Prompt(fabro_llm::cli::PromptArgs), + Prompt(PromptArgs), /// Interactive multi-turn chat - Chat(fabro_llm::cli::ChatArgs), + Chat(ChatArgs), } #[derive(Args)] diff --git a/lib/crates/fabro-cli/src/cli_config.rs b/lib/crates/fabro-cli/src/cli_config.rs index 245694e62..e39efc9ea 100644 --- a/lib/crates/fabro-cli/src/cli_config.rs +++ b/lib/crates/fabro-cli/src/cli_config.rs @@ -3,13 +3,14 @@ pub use fabro_config::cli::*; use std::path::Path; +use fabro_config::cli::load_cli_config; use fabro_config::FabroSettings; #[cfg(feature = "server")] use tracing::debug; pub fn load_cli_settings(path: Option<&Path>) -> anyhow::Result { - fabro_config::cli::load_cli_config(path)?.try_into() + load_cli_config(path)?.try_into() } #[cfg(feature = "server")] diff --git a/lib/crates/fabro-cli/src/commands/asset/cp.rs b/lib/crates/fabro-cli/src/commands/asset/cp.rs index 594fddbf9..d0b982369 100644 --- a/lib/crates/fabro-cli/src/commands/asset/cp.rs +++ b/lib/crates/fabro-cli/src/commands/asset/cp.rs @@ -3,18 +3,20 @@ use std::path::{Path, PathBuf}; use anyhow::{bail, Context, Result}; use fabro_config::FabroSettingsExt; use fabro_store::RuntimeState; +use fabro_workflows::assets::{scan_assets, AssetEntry}; +use fabro_workflows::run_lookup::{resolve_run, runs_base}; use crate::args::AssetCpArgs; +use crate::cli_config::load_cli_settings; use crate::shared::split_run_path; pub fn cp_command(args: &AssetCpArgs) -> Result<()> { - let cli_config = crate::cli_config::load_cli_settings(None)?; - let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); + let cli_config = load_cli_settings(None)?; + let base = runs_base(&cli_config.storage_dir()); let (run_id, asset_path) = parse_source(&args.source); - let run = fabro_workflows::run_lookup::resolve_run(&base, run_id)?; + let run = resolve_run(&base, run_id)?; let runtime_state = RuntimeState::new(&run.path); - let entries = - fabro_workflows::assets::scan_assets(&runtime_state.assets_dir(), args.node.as_deref())?; + let entries = scan_assets(&runtime_state.assets_dir(), args.node.as_deref())?; if entries.is_empty() { bail!("No assets found for this run"); @@ -80,8 +82,7 @@ pub fn cp_command(args: &AssetCpArgs) -> Result<()> { })?; } } else { - let mut by_filename: Vec<(String, &fabro_workflows::assets::AssetEntry)> = - Vec::with_capacity(entries.len()); + let mut by_filename: Vec<(String, &AssetEntry)> = Vec::with_capacity(entries.len()); for entry in &entries { let filename = Path::new(&entry.relative_path) .file_name() diff --git a/lib/crates/fabro-cli/src/commands/asset/list.rs b/lib/crates/fabro-cli/src/commands/asset/list.rs index 70e8f55cb..64c6f13f2 100644 --- a/lib/crates/fabro-cli/src/commands/asset/list.rs +++ b/lib/crates/fabro-cli/src/commands/asset/list.rs @@ -1,17 +1,19 @@ use anyhow::Result; use fabro_config::FabroSettingsExt; use fabro_store::RuntimeState; +use fabro_workflows::assets::scan_assets; +use fabro_workflows::run_lookup::{resolve_run, runs_base}; use crate::args::AssetListArgs; +use crate::cli_config::load_cli_settings; use crate::shared::format_size; pub fn list_command(args: &AssetListArgs) -> Result<()> { - let cli_config = crate::cli_config::load_cli_settings(None)?; - let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); - let run = fabro_workflows::run_lookup::resolve_run(&base, &args.run_id)?; + let cli_config = load_cli_settings(None)?; + let base = runs_base(&cli_config.storage_dir()); + let run = resolve_run(&base, &args.run_id)?; let runtime_state = RuntimeState::new(&run.path); - let entries = - fabro_workflows::assets::scan_assets(&runtime_state.assets_dir(), args.node.as_deref())?; + let entries = scan_assets(&runtime_state.assets_dir(), args.node.as_deref())?; if args.json { println!("{}", serde_json::to_string_pretty(&entries)?); diff --git a/lib/crates/fabro-cli/src/commands/config/mod.rs b/lib/crates/fabro-cli/src/commands/config/mod.rs index d650d94e4..b72626a07 100644 --- a/lib/crates/fabro-cli/src/commands/config/mod.rs +++ b/lib/crates/fabro-cli/src/commands/config/mod.rs @@ -2,7 +2,8 @@ use std::io::Write; use std::path::Path; use crate::args::{ConfigCommand, ConfigNamespace, ConfigShowArgs}; -use fabro_config::project::ResolveSettingsInput; +use fabro_config::cli::load_cli_config; +use fabro_config::project::{discover_project_config, resolve_settings, ResolveSettingsInput}; use fabro_config::{FabroConfig, FabroSettings}; pub fn dispatch(ns: ConfigNamespace) -> anyhow::Result<()> { @@ -13,9 +14,9 @@ pub fn dispatch(ns: ConfigNamespace) -> anyhow::Result<()> { fn merged_config(workflow: Option<&Path>) -> anyhow::Result { if let Some(workflow) = workflow { - let cli_config = fabro_config::cli::load_cli_config(None)?; + let cli_config = load_cli_config(None)?; let cwd = std::env::current_dir()?; - return fabro_config::project::resolve_settings(ResolveSettingsInput { + return resolve_settings(ResolveSettingsInput { workflow_path: workflow.to_path_buf(), cwd, defaults: cli_config, @@ -25,10 +26,10 @@ fn merged_config(workflow: Option<&Path>) -> anyhow::Result { } let cwd = std::env::current_dir()?; - let project_config = fabro_config::project::discover_project_config(&cwd)? + let project_config = discover_project_config(&cwd)? .map(|(_, config)| config) .unwrap_or_default(); - let cli_config = fabro_config::cli::load_cli_config(None)?; + let cli_config = load_cli_config(None)?; FabroConfig::combine(project_config, cli_config).try_into() } diff --git a/lib/crates/fabro-cli/src/commands/doctor.rs b/lib/crates/fabro-cli/src/commands/doctor.rs index eae34fc28..c38aaaa2e 100644 --- a/lib/crates/fabro-cli/src/commands/doctor.rs +++ b/lib/crates/fabro-cli/src/commands/doctor.rs @@ -4,18 +4,25 @@ use std::process::Command; #[cfg(feature = "server")] use std::sync::LazyLock; +use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; +use base64::Engine as _; #[cfg(feature = "server")] use fabro_config::server::{ApiAuthStrategy, AuthProvider}; +use fabro_llm::client::Client as LlmClient; +use fabro_llm::types::{Message, Request}; use fabro_model::{Catalog, Provider}; pub use fabro_util::check_report::{ CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus, }; use fabro_util::terminal::Styles; +use futures::future::join_all; #[cfg(feature = "server")] use regex::Regex; #[cfg(feature = "server")] use semver::Version; +use crate::cli_config::load_cli_settings; + // --------------------------------------------------------------------------- // System dependency types and parsers (server mode only) // --------------------------------------------------------------------------- @@ -863,12 +870,12 @@ pub(crate) fn probe_model(provider: Provider) -> String { } async fn probe_llm_provider( - client: &fabro_llm::client::Client, + client: &LlmClient, provider: Provider, ) -> (Provider, Result<(), String>) { - let request = fabro_llm::types::Request { + let request = Request { model: probe_model(provider), - messages: vec![fabro_llm::types::Message::user("hi")], + messages: vec![Message::user("hi")], provider: Some(provider.as_str().to_string()), tools: None, tool_choice: None, @@ -928,7 +935,7 @@ pub async fn run_doctor(verbose: bool, live: bool) -> i32 { spinner.enable_steady_tick(std::time::Duration::from_millis(80)); // Gather state - let cli_config = crate::cli_config::load_cli_settings(None).unwrap_or_default(); + let cli_config = load_cli_settings(None).unwrap_or_default(); let config_path = dirs::home_dir().map(|h| h.join(".fabro").join("cli.toml")); let config_exists = config_path.as_ref().is_some_and(|p| p.exists()); @@ -980,7 +987,8 @@ pub async fn run_doctor(verbose: bool, live: bool) -> i32 { let pem = if raw.starts_with("-----") { Ok(raw.clone()) } else { - base64::Engine::decode(&base64::engine::general_purpose::STANDARD, raw) + BASE64_STANDARD + .decode(raw) .map_err(|e| format!("base64 decode failed: {e}")) .and_then(|bytes| { String::from_utf8(bytes) @@ -1058,7 +1066,7 @@ pub async fn run_doctor(verbose: bool, live: bool) -> i32 { let http = reqwest::Client::new(); // Build LLM client — may fail if no keys are set - let llm_client = fabro_llm::client::Client::from_env().await.ok(); + let llm_client = LlmClient::from_env().await.ok(); let configured_providers: Vec = llm_statuses .iter() @@ -1072,7 +1080,7 @@ pub async fn run_doctor(verbose: bool, live: bool) -> i32 { .iter() .map(|p| probe_llm_provider(client, *p)) .collect(); - Some(futures::future::join_all(futures).await) + Some(join_all(futures).await) } else { None } diff --git a/lib/crates/fabro-cli/src/commands/exec.rs b/lib/crates/fabro-cli/src/commands/exec.rs index 3b607e2bb..25282da98 100644 --- a/lib/crates/fabro-cli/src/commands/exec.rs +++ b/lib/crates/fabro-cli/src/commands/exec.rs @@ -1,9 +1,14 @@ use anyhow::Result; +#[cfg(feature = "server")] +use fabro_agent::cli::run_with_args_and_client; +use fabro_agent::cli::{run_with_args, AgentArgs}; +use fabro_config::mcp::McpServerEntry; +use fabro_mcp::config::McpServerConfig; use crate::args::GlobalArgs; use crate::cli_config; -pub async fn execute(mut args: fabro_agent::cli::AgentArgs, globals: &GlobalArgs) -> Result<()> { +pub async fn execute(mut args: AgentArgs, globals: &GlobalArgs) -> Result<()> { let cli_config = cli_config::load_cli_settings(None)?; #[cfg(feature = "sleep_inhibitor")] let _sleep_guard = crate::sleep_inhibitor::guard(cli_config.prevent_idle_sleep_enabled()); @@ -20,10 +25,10 @@ pub async fn execute(mut args: fabro_agent::cli::AgentArgs, globals: &GlobalArgs globals.server_url.as_deref(), &cli_config, ); - let mcp_servers: Vec = cli_config + let mcp_servers: Vec = cli_config .mcp_servers .into_iter() - .map(|(name, entry): (String, fabro_config::mcp::McpServerEntry)| entry.into_config(name)) + .map(|(name, entry): (String, McpServerEntry)| entry.into_config(name)) .collect(); #[cfg(feature = "server")] { @@ -46,11 +51,11 @@ pub async fn execute(mut args: fabro_agent::cli::AgentArgs, globals: &GlobalArgs .register_provider(adapter) .await .map_err(|e| anyhow::anyhow!("Failed to register fabro server adapter: {e}"))?; - fabro_agent::cli::run_with_args_and_client(args, Some(client), mcp_servers).await? + run_with_args_and_client(args, Some(client), mcp_servers).await? } cli_config::ExecutionMode::Standalone => { tracing::info!(mode = "standalone", "Agent session starting"); - fabro_agent::cli::run_with_args(args, mcp_servers).await? + run_with_args(args, mcp_servers).await? } } } @@ -58,7 +63,7 @@ pub async fn execute(mut args: fabro_agent::cli::AgentArgs, globals: &GlobalArgs { let _ = globals; tracing::info!(mode = "standalone", "Agent session starting"); - fabro_agent::cli::run_with_args(args, mcp_servers).await? + run_with_args(args, mcp_servers).await? } Ok(()) diff --git a/lib/crates/fabro-cli/src/commands/graph.rs b/lib/crates/fabro-cli/src/commands/graph.rs index de700fcf9..3f18a3552 100644 --- a/lib/crates/fabro-cli/src/commands/graph.rs +++ b/lib/crates/fabro-cli/src/commands/graph.rs @@ -3,9 +3,12 @@ use std::io::Write; use std::sync::LazyLock; use anyhow::bail; -use fabro_config::project::ResolveSettingsInput; +use fabro_config::cli::load_cli_config; +use fabro_config::project::{resolve_settings, resolve_workflow_path, ResolveSettingsInput}; +use fabro_graphviz::render::render_dot; use fabro_util::terminal::Styles; use fabro_validate::Severity; +use fabro_workflows::operations::{validate, ValidateInput, WorkflowInput}; use tracing::debug; use crate::args::{GraphArgs, GraphDirection}; @@ -16,22 +19,21 @@ static RANKDIR_RE: LazyLock = pub fn run(args: &GraphArgs, styles: &Styles) -> anyhow::Result<()> { let cwd = std::env::current_dir()?; - let cli_defaults = fabro_config::cli::load_cli_config(None)?; - let settings = fabro_config::project::resolve_settings(ResolveSettingsInput { + let cli_defaults = load_cli_config(None)?; + let settings = resolve_settings(ResolveSettingsInput { workflow_path: args.workflow.clone(), cwd: cwd.clone(), defaults: cli_defaults, overrides: fabro_config::FabroConfig::default(), apply_project_config: true, })?; - let resolution = fabro_config::project::resolve_workflow_path(&args.workflow, &cwd)?; - let validated = - fabro_workflows::operations::validate(fabro_workflows::operations::ValidateInput { - workflow: fabro_workflows::operations::WorkflowInput::Path(args.workflow.clone()), - settings, - cwd, - custom_transforms: Vec::new(), - })?; + let resolution = resolve_workflow_path(&args.workflow, &cwd)?; + let validated = validate(ValidateInput { + workflow: WorkflowInput::Path(args.workflow.clone()), + settings, + cwd, + custom_transforms: Vec::new(), + })?; let diagnostics = validated.diagnostics(); print_diagnostics(diagnostics, styles); @@ -42,7 +44,7 @@ pub fn run(args: &GraphArgs, styles: &Styles) -> anyhow::Result<()> { let source = read_workflow_file(&resolution.dot_path)?; let source = apply_direction(&source, args.direction); - let rendered = fabro_graphviz::render::render_dot(&source, args.format.into())?; + let rendered = render_dot(&source, args.format.into())?; if let Some(ref output_path) = args.output { std::fs::write(output_path, &rendered)?; diff --git a/lib/crates/fabro-cli/src/commands/install.rs b/lib/crates/fabro-cli/src/commands/install.rs index 36b497d07..0b8436ae7 100644 --- a/lib/crates/fabro-cli/src/commands/install.rs +++ b/lib/crates/fabro-cli/src/commands/install.rs @@ -8,12 +8,17 @@ use anyhow::{bail, Context, Result}; use axum::extract::Query; use axum::response::Html; use axum::routing::get; +use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; +use base64::Engine as _; +use dialoguer::console::Term; +use dialoguer::theme::ColorfulTheme; use dialoguer::{MultiSelect, Select}; use fabro_model::Provider; use fabro_util::terminal::Styles; use rand::Rng; use tokio::net::TcpListener; use tokio::sync::oneshot; +use tokio::task::spawn_blocking; use super::doctor; use crate::shared::provider_auth::{ @@ -224,29 +229,23 @@ fn detect_binary_on_path(binary: &str) -> bool { #[cfg(feature = "server")] fn prompt_input(prompt: &str) -> Result { - Ok( - dialoguer::Input::with_theme(&dialoguer::theme::ColorfulTheme::default()) - .with_prompt(prompt) - .interact_on(&dialoguer::console::Term::stderr())?, - ) + Ok(dialoguer::Input::with_theme(&ColorfulTheme::default()) + .with_prompt(prompt) + .interact_on(&Term::stderr())?) } fn prompt_select(prompt: &str, items: &[String]) -> Result { - Ok( - Select::with_theme(&dialoguer::theme::ColorfulTheme::default()) - .with_prompt(prompt) - .items(items) - .interact_on(&dialoguer::console::Term::stderr())?, - ) + Ok(Select::with_theme(&ColorfulTheme::default()) + .with_prompt(prompt) + .items(items) + .interact_on(&Term::stderr())?) } fn prompt_multiselect(prompt: &str, items: &[String]) -> Result> { - Ok( - MultiSelect::with_theme(&dialoguer::theme::ColorfulTheme::default()) - .with_prompt(prompt) - .items(items) - .interact_on(&dialoguer::console::Term::stderr())?, - ) + Ok(MultiSelect::with_theme(&ColorfulTheme::default()) + .with_prompt(prompt) + .items(items) + .interact_on(&Term::stderr())?) } // --------------------------------------------------------------------------- @@ -468,8 +467,7 @@ async fn setup_github_app( ); // Return secrets as env pairs - let pem_b64 = - base64::Engine::encode(&base64::engine::general_purpose::STANDARD, pem.as_bytes()); + let pem_b64 = BASE64_STANDARD.encode(pem.as_bytes()); let mut env_pairs = vec![ ("GITHUB_APP_PRIVATE_KEY".to_string(), pem_b64), @@ -524,7 +522,7 @@ pub async fn run_install(web_url: &str) -> Result<()> { let dot_idx = doctor::DEP_SPECS.iter().position(|s| s.name == "dot"); if let Some(idx) = dot_idx { if matches!(dep_outcomes[idx], doctor::ProbeOutcome::NotFound) { - let install = tokio::task::spawn_blocking(|| { + let install = spawn_blocking(|| { prompt_confirm("Graphviz (dot) not found. Install via Homebrew?", true) }) .await??; @@ -560,7 +558,7 @@ pub async fn run_install(web_url: &str) -> Result<()> { if codex_detected { tracing::debug!("Codex binary detected on PATH"); - let use_oauth = tokio::task::spawn_blocking(|| { + let use_oauth = spawn_blocking(|| { prompt_confirm( "OpenAI (Codex) detected. Set up OpenAI via browser login?", true, @@ -584,7 +582,7 @@ pub async fn run_install(web_url: &str) -> Result<()> { .map(|p| provider_display_name(*p).to_string()) .collect(); - let primary_idx: usize = tokio::task::spawn_blocking({ + let primary_idx: usize = spawn_blocking({ let labels = primary_labels.clone(); move || prompt_select("Choose your first LLM provider", &labels) }) @@ -601,8 +599,7 @@ pub async fn run_install(web_url: &str) -> Result<()> { // Additional providers eprintln!(); let add_more = - tokio::task::spawn_blocking(|| prompt_confirm("Set up additional LLM providers?", false)) - .await??; + spawn_blocking(|| prompt_confirm("Set up additional LLM providers?", false)).await??; if add_more { let remaining_labels: Vec = Provider::ALL @@ -619,7 +616,7 @@ pub async fn run_install(web_url: &str) -> Result<()> { .copied() .collect(); - let selected_indices: Vec = tokio::task::spawn_blocking({ + let selected_indices: Vec = spawn_blocking({ let labels = remaining_labels.clone(); move || prompt_multiselect("Which additional LLM providers?", &labels) }) @@ -644,10 +641,8 @@ pub async fn run_install(web_url: &str) -> Result<()> { eprintln!(); { - let setup_github = tokio::task::spawn_blocking(|| { - prompt_confirm("Set up a GitHub App? (Recommended)", true) - }) - .await??; + let setup_github = + spawn_blocking(|| prompt_confirm("Set up a GitHub App? (Recommended)", true)).await??; if setup_github { let github_env_pairs = setup_github_app(&arc_dir, &s, web_url).await?; @@ -686,7 +681,7 @@ pub async fn run_install(web_url: &str) -> Result<()> { let config_path = arc_dir.join("server.toml"); let write_config = if config_path.exists() { - tokio::task::spawn_blocking(|| { + spawn_blocking(|| { prompt_confirm("~/.fabro/server.toml already exists. Overwrite?", false) }) .await?? @@ -696,8 +691,7 @@ pub async fn run_install(web_url: &str) -> Result<()> { if write_config { let username: String = - tokio::task::spawn_blocking(|| prompt_input("GitHub username for allowed access")) - .await??; + spawn_blocking(|| prompt_input("GitHub username for allowed access")).await??; let toml_content = format_config_toml(&username); std::fs::write(&config_path, &toml_content)?; @@ -732,14 +726,8 @@ pub async fn run_install(web_url: &str) -> Result<()> { s.green.apply_to("✔") ); - let jwt_private_b64 = base64::Engine::encode( - &base64::engine::general_purpose::STANDARD, - jwt_private_pem.as_bytes(), - ); - let jwt_public_b64 = base64::Engine::encode( - &base64::engine::general_purpose::STANDARD, - jwt_public_pem.as_bytes(), - ); + let jwt_private_b64 = BASE64_STANDARD.encode(jwt_private_pem.as_bytes()); + let jwt_public_b64 = BASE64_STANDARD.encode(jwt_public_pem.as_bytes()); let server_env_pairs = vec![ ("FABRO_JWT_PRIVATE_KEY".to_string(), jwt_private_b64), @@ -759,8 +747,7 @@ pub async fn run_install(web_url: &str) -> Result<()> { // Verify setup let env_path = arc_dir.join(".env"); let run_doctor = - tokio::task::spawn_blocking(|| prompt_confirm("Run fabro doctor to verify?", true)) - .await??; + spawn_blocking(|| prompt_confirm("Run fabro doctor to verify?", true)).await??; if run_doctor { // Reload .env so doctor sees the values we just wrote diff --git a/lib/crates/fabro-cli/src/commands/llm/chat.rs b/lib/crates/fabro-cli/src/commands/llm/chat.rs index 7d690934c..d6d305077 100644 --- a/lib/crates/fabro-cli/src/commands/llm/chat.rs +++ b/lib/crates/fabro-cli/src/commands/llm/chat.rs @@ -1,10 +1,13 @@ use anyhow::Result; use fabro_config::FabroSettings; +use fabro_llm::cli::{run_chat, ChatArgs}; +#[cfg(feature = "server")] +use fabro_llm::cli::{run_chat_via_server, ServerConnection}; use crate::args::GlobalArgs; pub async fn execute( - mut args: fabro_llm::cli::ChatArgs, + mut args: ChatArgs, cli_config: &FabroSettings, globals: &GlobalArgs, ) -> Result<()> { @@ -23,14 +26,14 @@ pub async fn execute( match resolved.mode { crate::cli_config::ExecutionMode::Server => { let client = crate::cli_config::build_server_client(resolved.tls.as_ref())?; - let server = fabro_llm::cli::ServerConnection { + let server = ServerConnection { client, base_url: resolved.server_base_url, }; - fabro_llm::cli::run_chat_via_server(args, &server).await?; + run_chat_via_server(args, &server).await?; } crate::cli_config::ExecutionMode::Standalone => { - fabro_llm::cli::run_chat(args).await?; + run_chat(args).await?; } } } @@ -38,7 +41,7 @@ pub async fn execute( #[cfg(not(feature = "server"))] { let _ = globals; - fabro_llm::cli::run_chat(args).await?; + run_chat(args).await?; } Ok(()) diff --git a/lib/crates/fabro-cli/src/commands/llm/mod.rs b/lib/crates/fabro-cli/src/commands/llm/mod.rs index 76ef04d0a..e513101f7 100644 --- a/lib/crates/fabro-cli/src/commands/llm/mod.rs +++ b/lib/crates/fabro-cli/src/commands/llm/mod.rs @@ -4,9 +4,10 @@ mod prompt; use anyhow::Result; use crate::args::{GlobalArgs, LlmCommand, LlmNamespace}; +use crate::cli_config::load_cli_settings; pub async fn dispatch(ns: LlmNamespace, globals: &GlobalArgs) -> Result<()> { - let cli_config = crate::cli_config::load_cli_settings(None)?; + let cli_config = load_cli_settings(None)?; match ns.command { LlmCommand::Prompt(args) => prompt::execute(args, &cli_config, globals).await, diff --git a/lib/crates/fabro-cli/src/commands/llm/prompt.rs b/lib/crates/fabro-cli/src/commands/llm/prompt.rs index 3647e52ed..f3289715f 100644 --- a/lib/crates/fabro-cli/src/commands/llm/prompt.rs +++ b/lib/crates/fabro-cli/src/commands/llm/prompt.rs @@ -1,10 +1,13 @@ use anyhow::Result; use fabro_config::FabroSettings; +use fabro_llm::cli::{run_prompt, PromptArgs}; +#[cfg(feature = "server")] +use fabro_llm::cli::{run_prompt_via_server, ServerConnection}; use crate::args::GlobalArgs; pub async fn execute( - mut args: fabro_llm::cli::PromptArgs, + mut args: PromptArgs, cli_config: &FabroSettings, globals: &GlobalArgs, ) -> Result<()> { @@ -23,14 +26,14 @@ pub async fn execute( match resolved.mode { crate::cli_config::ExecutionMode::Server => { let client = crate::cli_config::build_server_client(resolved.tls.as_ref())?; - let server = fabro_llm::cli::ServerConnection { + let server = ServerConnection { client, base_url: resolved.server_base_url, }; - fabro_llm::cli::run_prompt_via_server(args, &server).await?; + run_prompt_via_server(args, &server).await?; } crate::cli_config::ExecutionMode::Standalone => { - fabro_llm::cli::run_prompt(args).await?; + run_prompt(args).await?; } } } @@ -38,7 +41,7 @@ pub async fn execute( #[cfg(not(feature = "server"))] { let _ = globals; - fabro_llm::cli::run_prompt(args).await?; + run_prompt(args).await?; } Ok(()) diff --git a/lib/crates/fabro-cli/src/commands/model.rs b/lib/crates/fabro-cli/src/commands/model.rs index 6c4250770..4610334a8 100644 --- a/lib/crates/fabro-cli/src/commands/model.rs +++ b/lib/crates/fabro-cli/src/commands/model.rs @@ -1,13 +1,13 @@ use anyhow::Result; +#[cfg(feature = "server")] +use fabro_llm::cli::ServerConnection; +use fabro_llm::cli::{run_models, ModelsCommand}; use crate::args::GlobalArgs; #[cfg(feature = "server")] use crate::cli_config; -pub async fn execute( - command: Option, - globals: &GlobalArgs, -) -> Result<()> { +pub async fn execute(command: Option, globals: &GlobalArgs) -> Result<()> { let server = { #[cfg(feature = "server")] { @@ -20,7 +20,7 @@ pub async fn execute( match resolved.mode { cli_config::ExecutionMode::Server => { let client = cli_config::build_server_client(resolved.tls.as_ref())?; - Some(fabro_llm::cli::ServerConnection { + Some(ServerConnection { client, base_url: resolved.server_base_url, }) @@ -35,5 +35,5 @@ pub async fn execute( } }; - fabro_llm::cli::run_models(command, server).await + run_models(command, server).await } diff --git a/lib/crates/fabro-cli/src/commands/parse.rs b/lib/crates/fabro-cli/src/commands/parse.rs index 3827f422f..9e5df9b5a 100644 --- a/lib/crates/fabro-cli/src/commands/parse.rs +++ b/lib/crates/fabro-cli/src/commands/parse.rs @@ -1,6 +1,10 @@ +use std::io::Write; + +use fabro_config::project::resolve_workflow; +use fabro_graphviz::parser::parse_ast; + use crate::args::ParseArgs; use crate::shared::read_workflow_file; -use std::io::Write; pub fn run(args: &ParseArgs) -> anyhow::Result<()> { let stdout = std::io::stdout(); @@ -8,9 +12,9 @@ pub fn run(args: &ParseArgs) -> anyhow::Result<()> { } fn run_to(args: &ParseArgs, mut out: impl Write) -> anyhow::Result<()> { - let (dot_path, _cfg) = fabro_config::project::resolve_workflow(&args.workflow)?; + let (dot_path, _cfg) = resolve_workflow(&args.workflow)?; let source = read_workflow_file(&dot_path)?; - let ast = fabro_graphviz::parser::parse_ast(&source)?; + let ast = parse_ast(&source)?; serde_json::to_writer_pretty(&mut out, &ast)?; writeln!(out)?; Ok(()) diff --git a/lib/crates/fabro-cli/src/commands/pr/close.rs b/lib/crates/fabro-cli/src/commands/pr/close.rs index 7c5f9ec2d..07a1959cf 100644 --- a/lib/crates/fabro-cli/src/commands/pr/close.rs +++ b/lib/crates/fabro-cli/src/commands/pr/close.rs @@ -2,16 +2,18 @@ use std::path::Path; use anyhow::{Context, Result}; use fabro_config::FabroSettingsExt; +use fabro_workflows::run_lookup::runs_base; use tracing::info; use crate::args::PrCloseArgs; +use crate::cli_config::load_cli_settings; pub async fn close_command( args: PrCloseArgs, github_app: Option, ) -> Result<()> { - let cli_config = crate::cli_config::load_cli_settings(None)?; - let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); + let cli_config = load_cli_settings(None)?; + let base = runs_base(&cli_config.storage_dir()); close_from(&base, args, github_app).await } diff --git a/lib/crates/fabro-cli/src/commands/pr/create.rs b/lib/crates/fabro-cli/src/commands/pr/create.rs index 94a4fe9ee..f07476b38 100644 --- a/lib/crates/fabro-cli/src/commands/pr/create.rs +++ b/lib/crates/fabro-cli/src/commands/pr/create.rs @@ -3,17 +3,24 @@ use std::path::Path; use anyhow::{bail, Context, Result}; use fabro_config::FabroSettingsExt; use fabro_model::Catalog; -use fabro_workflows::records::{ConclusionExt, RunRecordExt, StartRecordExt}; +use fabro_sandbox::daytona::detect_repo_info; +use fabro_workflows::outcome::StageStatus; +use fabro_workflows::pull_request::maybe_open_pull_request; +use fabro_workflows::records::{ + Conclusion, ConclusionExt, RunRecord, RunRecordExt, StartRecord, StartRecordExt, +}; +use fabro_workflows::run_lookup::{resolve_run, runs_base}; use tracing::info; use crate::args::PrCreateArgs; +use crate::cli_config::load_cli_settings; pub async fn create_command( args: PrCreateArgs, github_app: Option, ) -> Result<()> { - let cli_config = crate::cli_config::load_cli_settings(None)?; - let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); + let cli_config = load_cli_settings(None)?; + let base = runs_base(&cli_config.storage_dir()); create_from(&base, args, github_app).await } @@ -22,20 +29,17 @@ async fn create_from( args: PrCreateArgs, github_app: Option, ) -> Result<()> { - let run_dir = fabro_workflows::run_lookup::resolve_run(base, &args.run_id)?.path; + let run_dir = resolve_run(base, &args.run_id)?.path; - let record = - fabro_workflows::records::RunRecord::load(&run_dir).context("Failed to load run.json")?; + let record = RunRecord::load(&run_dir).context("Failed to load run.json")?; - let start = fabro_workflows::records::StartRecord::load(&run_dir) - .context("Failed to load start.json")?; + let start = StartRecord::load(&run_dir).context("Failed to load start.json")?; - let conclusion = fabro_workflows::records::Conclusion::load(&run_dir.join("conclusion.json")) + let conclusion = Conclusion::load(&run_dir.join("conclusion.json")) .context("Failed to load conclusion.json — is the run finished?")?; match conclusion.status { - fabro_workflows::outcome::StageStatus::Success - | fabro_workflows::outcome::StageStatus::PartialSuccess => {} + StageStatus::Success | StageStatus::PartialSuccess => {} status => bail!("Run status is '{status}', expected success or partial_success"), } @@ -52,7 +56,7 @@ async fn create_from( let cwd = std::env::current_dir().context("Failed to get current directory")?; let (origin_url, detected_branch) = - fabro_sandbox::daytona::detect_repo_info(&cwd).map_err(|err| anyhow::anyhow!("{err}"))?; + detect_repo_info(&cwd).map_err(|err| anyhow::anyhow!("{err}"))?; let base_branch = record .base_branch @@ -89,7 +93,7 @@ async fn create_from( .model .unwrap_or_else(|| Catalog::builtin().default_from_env().id.clone()); - let record = fabro_workflows::pull_request::maybe_open_pull_request( + let record = maybe_open_pull_request( &creds, &origin_url, base_branch, diff --git a/lib/crates/fabro-cli/src/commands/pr/list.rs b/lib/crates/fabro-cli/src/commands/pr/list.rs index a494f2451..835552177 100644 --- a/lib/crates/fabro-cli/src/commands/pr/list.rs +++ b/lib/crates/fabro-cli/src/commands/pr/list.rs @@ -2,16 +2,20 @@ use std::path::Path; use anyhow::{Context, Result}; use fabro_config::FabroSettingsExt; +use fabro_workflows::pull_request::PullRequestRecord; +use fabro_workflows::run_lookup::{runs_base, scan_runs}; +use futures::future::join_all; use tracing::info; use crate::args::PrListArgs; +use crate::cli_config::load_cli_settings; pub async fn list_command( args: PrListArgs, github_app: Option, ) -> Result<()> { - let cli_config = crate::cli_config::load_cli_settings(None)?; - let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); + let cli_config = load_cli_settings(None)?; + let base = runs_base(&cli_config.storage_dir()); list_from(&base, args, github_app).await } @@ -24,15 +28,13 @@ async fn list_from( "GitHub App credentials required — set GITHUB_APP_PRIVATE_KEY and configure app_id", )?; - let runs = fabro_workflows::run_lookup::scan_runs(base).context("Failed to scan runs")?; + let runs = scan_runs(base).context("Failed to scan runs")?; - let mut entries: Vec<(String, fabro_workflows::pull_request::PullRequestRecord)> = Vec::new(); + let mut entries: Vec<(String, PullRequestRecord)> = Vec::new(); for run in &runs { let pr_path = run.path.join("pull_request.json"); if let Ok(content) = std::fs::read_to_string(&pr_path) { - if let Ok(record) = - serde_json::from_str::(&content) - { + if let Ok(record) = serde_json::from_str::(&content) { entries.push((run.run_id.clone(), record)); } } @@ -93,7 +95,7 @@ async fn list_from( }) .collect(); - let all_rows = futures::future::join_all(futures).await; + let all_rows = join_all(futures).await; let rows: Vec<_> = if args.all { all_rows } else { diff --git a/lib/crates/fabro-cli/src/commands/pr/merge.rs b/lib/crates/fabro-cli/src/commands/pr/merge.rs index c6023a8b1..acfb6a590 100644 --- a/lib/crates/fabro-cli/src/commands/pr/merge.rs +++ b/lib/crates/fabro-cli/src/commands/pr/merge.rs @@ -4,14 +4,17 @@ use anyhow::{Context, Result}; use fabro_config::FabroSettingsExt; use tracing::info; +use fabro_workflows::run_lookup::runs_base; + use crate::args::PrMergeArgs; +use crate::cli_config::load_cli_settings; pub async fn merge_command( args: PrMergeArgs, github_app: Option, ) -> Result<()> { - let cli_config = crate::cli_config::load_cli_settings(None)?; - let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); + let cli_config = load_cli_settings(None)?; + let base = runs_base(&cli_config.storage_dir()); merge_from(&base, args, github_app).await } diff --git a/lib/crates/fabro-cli/src/commands/pr/mod.rs b/lib/crates/fabro-cli/src/commands/pr/mod.rs index 3936b05a6..e7188b70f 100644 --- a/lib/crates/fabro-cli/src/commands/pr/mod.rs +++ b/lib/crates/fabro-cli/src/commands/pr/mod.rs @@ -8,11 +8,16 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; +use fabro_workflows::pull_request::PullRequestRecord; +use fabro_workflows::run_lookup::resolve_run; + use crate::args::{PrCommand, PrNamespace}; +use crate::cli_config::load_cli_settings; +use crate::shared::github::build_github_app_credentials; pub async fn dispatch(ns: PrNamespace) -> Result<()> { - let cli_config = crate::cli_config::load_cli_settings(None)?; - let github_app = crate::shared::github::build_github_app_credentials(cli_config.app_id()); + let cli_config = load_cli_settings(None)?; + let github_app = build_github_app_credentials(cli_config.app_id()); match ns.command { PrCommand::Create(args) => create::create_command(args, github_app).await, @@ -23,11 +28,8 @@ pub async fn dispatch(ns: PrNamespace) -> Result<()> { } } -pub(crate) fn load_pr_record( - base: &Path, - run_id: &str, -) -> Result<(fabro_workflows::pull_request::PullRequestRecord, PathBuf)> { - let run_dir = fabro_workflows::run_lookup::resolve_run(base, run_id)?.path; +pub(crate) fn load_pr_record(base: &Path, run_id: &str) -> Result<(PullRequestRecord, PathBuf)> { + let run_dir = resolve_run(base, run_id)?.path; let pr_path = run_dir.join("pull_request.json"); let content = std::fs::read_to_string(&pr_path).with_context(|| { format!( @@ -35,7 +37,7 @@ pub(crate) fn load_pr_record( Create one first with: fabro pr create {run_id}" ) })?; - let record: fabro_workflows::pull_request::PullRequestRecord = + let record: PullRequestRecord = serde_json::from_str(&content).context("Failed to parse pull_request.json")?; Ok((record, run_dir)) } diff --git a/lib/crates/fabro-cli/src/commands/pr/view.rs b/lib/crates/fabro-cli/src/commands/pr/view.rs index cc7fe89ec..4971f5bd9 100644 --- a/lib/crates/fabro-cli/src/commands/pr/view.rs +++ b/lib/crates/fabro-cli/src/commands/pr/view.rs @@ -4,14 +4,17 @@ use anyhow::{Context, Result}; use fabro_config::FabroSettingsExt; use tracing::info; +use fabro_workflows::run_lookup::runs_base; + use crate::args::PrViewArgs; +use crate::cli_config::load_cli_settings; pub async fn view_command( args: PrViewArgs, github_app: Option, ) -> Result<()> { - let cli_config = crate::cli_config::load_cli_settings(None)?; - let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); + let cli_config = load_cli_settings(None)?; + let base = runs_base(&cli_config.storage_dir()); view_from(&base, args, github_app).await } diff --git a/lib/crates/fabro-cli/src/commands/preflight.rs b/lib/crates/fabro-cli/src/commands/preflight.rs index 4deaaa5e9..84ad34264 100644 --- a/lib/crates/fabro-cli/src/commands/preflight.rs +++ b/lib/crates/fabro-cli/src/commands/preflight.rs @@ -3,39 +3,47 @@ use std::sync::Arc; use anyhow::bail; use fabro_agent::{DockerSandbox, DockerSandboxConfig, LocalSandbox, Sandbox}; -use fabro_config::project::ResolveSettingsInput; +use fabro_config::cli::load_cli_config; +use fabro_config::project::{ + resolve_settings, resolve_workflow_path, resolve_working_directory, ResolveSettingsInput, +}; use fabro_config::{FabroConfig, FabroSettings}; +use fabro_graphviz::graph::{is_llm_handler_type, Graph}; +use fabro_llm::client::Client as LlmClient; use fabro_model::{Catalog, Provider}; +use fabro_sandbox::daytona::{detect_repo_info, DaytonaConfig, DaytonaSandbox}; +use fabro_sandbox::ssh::{GitCloneParams as SshGitCloneParams, SshConfig, SshSandbox}; use fabro_sandbox::SandboxProvider; use fabro_util::terminal::Styles; -use fabro_workflows::git::GitSyncStatus; +use fabro_workflows::git::{sync_status, GitSyncStatus}; +use fabro_workflows::operations::{validate, ValidateInput, WorkflowInput}; use crate::args::PreflightArgs; +use crate::shared::github::build_github_app_credentials; pub async fn execute(mut args: PreflightArgs) -> anyhow::Result<()> { let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); - let cli_defaults = fabro_config::cli::load_cli_config(None)?; + let cli_defaults = load_cli_config(None)?; let cli_config: FabroSettings = cli_defaults.clone().try_into()?; args.verbose = args.verbose || cli_config.verbose_enabled(); - let github_app = crate::shared::github::build_github_app_credentials(cli_config.app_id()); + let github_app = build_github_app_credentials(cli_config.app_id()); let cli_args_config = FabroConfig::try_from(&args)?; let cwd = std::env::current_dir()?; - let settings = fabro_config::project::resolve_settings(ResolveSettingsInput { + let settings = resolve_settings(ResolveSettingsInput { workflow_path: args.workflow.clone(), cwd: cwd.clone(), defaults: cli_defaults, overrides: cli_args_config, apply_project_config: true, })?; - let resolution = fabro_config::project::resolve_workflow_path(&args.workflow, &cwd)?; - let working_directory = fabro_config::project::resolve_working_directory(&settings, &cwd); + let resolution = resolve_workflow_path(&args.workflow, &cwd)?; + let working_directory = resolve_working_directory(&settings, &cwd); - let (origin_url, detected_base_branch) = - fabro_sandbox::daytona::detect_repo_info(&working_directory) - .map(|(url, branch)| (Some(url), branch)) - .unwrap_or((None, None)); - let git_status = fabro_workflows::git::sync_status( + let (origin_url, detected_base_branch) = detect_repo_info(&working_directory) + .map(|(url, branch)| (Some(url), branch)) + .unwrap_or((None, None)); + let git_status = sync_status( &working_directory, "origin", detected_base_branch.as_deref(), @@ -43,13 +51,12 @@ pub async fn execute(mut args: PreflightArgs) -> anyhow::Result<()> { let sandbox_provider = resolve_sandbox_provider(args.sandbox.map(Into::into), &settings)?; - let validated = - fabro_workflows::operations::validate(fabro_workflows::operations::ValidateInput { - workflow: fabro_workflows::operations::WorkflowInput::Path(args.workflow.clone()), - settings: settings.clone(), - cwd, - custom_transforms: Vec::new(), - })?; + let validated = validate(ValidateInput { + workflow: WorkflowInput::Path(args.workflow.clone()), + settings: settings.clone(), + cwd, + custom_transforms: Vec::new(), + })?; super::run::output::print_workflow_report(&validated, Some(&resolution.dot_path), styles); if validated.has_errors() { bail!("Validation failed"); @@ -74,7 +81,7 @@ fn resolve_model_provider( cli_model: Option<&str>, cli_provider: Option<&str>, settings: &FabroSettings, - graph: &fabro_graphviz::graph::Graph, + graph: &Graph, ) -> (String, Option) { let configured_model = settings.llm.as_ref().and_then(|llm| llm.model.as_deref()); let configured_provider = settings @@ -128,9 +135,7 @@ fn resolve_sandbox_provider( .unwrap_or_default()) } -fn resolve_daytona_config( - settings: &FabroSettings, -) -> Option { +fn resolve_daytona_config(settings: &FabroSettings) -> Option { settings .sandbox_settings() .and_then(|sandbox| sandbox.daytona.clone()) @@ -145,7 +150,7 @@ fn resolve_exe_config(settings: &FabroSettings) -> Option Option { - let (detected_url, branch) = match fabro_sandbox::daytona::detect_repo_info(cwd) { + let (detected_url, branch) = match detect_repo_info(cwd) { Ok(info) => info, Err(err) => { tracing::warn!("No git repo detected for exe.dev clone: {err}"); @@ -156,14 +161,14 @@ fn resolve_exe_clone_params(cwd: &Path) -> Option Option { +fn resolve_ssh_config(settings: &FabroSettings) -> Option { settings .sandbox_settings() .and_then(|sandbox| sandbox.ssh.clone()) } -fn resolve_ssh_clone_params(cwd: &Path) -> Option { - let (detected_url, branch) = match fabro_sandbox::daytona::detect_repo_info(cwd) { +fn resolve_ssh_clone_params(cwd: &Path) -> Option { + let (detected_url, branch) = match detect_repo_info(cwd) { Ok(info) => info, Err(err) => { tracing::warn!("No git repo detected for SSH clone: {err}"); @@ -171,7 +176,7 @@ fn resolve_ssh_clone_params(cwd: &Path) -> Option, cli_provider: Option<&str>, @@ -281,14 +286,7 @@ async fn run_preflight( } SandboxProvider::Daytona => { let config = daytona_config.unwrap_or_default(); - match fabro_sandbox::daytona::DaytonaSandbox::new( - config, - github_app.clone(), - None, - None, - ) - .await - { + match DaytonaSandbox::new(config, github_app.clone(), None, None).await { Ok(env) => Ok(Arc::new(env) as Arc), Err(e) => Err(format!("Daytona sandbox creation failed: {e}")), } @@ -316,7 +314,7 @@ async fn run_preflight( SandboxProvider::Ssh => match ssh_config { Some(config) => { let clone_params = resolve_ssh_clone_params(working_directory); - let env = fabro_sandbox::ssh::SshSandbox::new(config, clone_params, None, None); + let env = SshSandbox::new(config, clone_params, None, None); Ok(Arc::new(env) as Arc) } None => Err("SSH sandbox requires [sandbox.ssh] config".to_string()), @@ -367,14 +365,14 @@ async fn run_preflight( } let default_provider = provider.as_deref().unwrap_or("anthropic"); - let llm_ok = match fabro_llm::client::Client::from_env().await { + let llm_ok = match LlmClient::from_env().await { Ok(c) => { let configured: Vec = c.provider_names().iter().map(|s| s.to_string()).collect(); let mut model_providers = std::collections::BTreeSet::new(); for node in graph.nodes.values() { - if !fabro_graphviz::graph::is_llm_handler_type(node.handler_type()) { + if !is_llm_handler_type(node.handler_type()) { continue; } let node_model = node.model().unwrap_or(&model); diff --git a/lib/crates/fabro-cli/src/commands/provider/login.rs b/lib/crates/fabro-cli/src/commands/provider/login.rs index 2ffa34aa5..139b95c9e 100644 --- a/lib/crates/fabro-cli/src/commands/provider/login.rs +++ b/lib/crates/fabro-cli/src/commands/provider/login.rs @@ -1,6 +1,7 @@ use anyhow::{Context, Result}; use fabro_model::Provider; use fabro_util::terminal::Styles; +use tokio::task::spawn_blocking; use crate::args::ProviderLoginArgs; use crate::shared::provider_auth; @@ -13,10 +14,8 @@ pub async fn login_command(args: ProviderLoginArgs) -> Result<()> { std::fs::create_dir_all(&arc_dir)?; let use_oauth = args.provider == Provider::OpenAi - && tokio::task::spawn_blocking(|| { - provider_auth::prompt_confirm("Log in via browser (OAuth)?", true) - }) - .await??; + && spawn_blocking(|| provider_auth::prompt_confirm("Log in via browser (OAuth)?", true)) + .await??; let env_pairs = if use_oauth { provider_auth::run_openai_oauth_or_api_key(&s).await? diff --git a/lib/crates/fabro-cli/src/commands/repo/init.rs b/lib/crates/fabro-cli/src/commands/repo/init.rs index 9a3928f7d..20789e871 100644 --- a/lib/crates/fabro-cli/src/commands/repo/init.rs +++ b/lib/crates/fabro-cli/src/commands/repo/init.rs @@ -1,6 +1,10 @@ use std::path::PathBuf; use anyhow::{bail, Context, Result}; +use tokio::task::spawn_blocking; + +use crate::cli_config::load_cli_settings; +use crate::shared::github::build_github_app_credentials; pub(super) fn git_repo_root() -> Result { let output = std::process::Command::new("git") @@ -150,7 +154,7 @@ async fn check_github_app_installation() { }; // Load CLI config to get app_id and slug - let cli_config = match crate::cli_config::load_cli_settings(None) { + let cli_config = match load_cli_settings(None) { Ok(c) => c, Err(_) => return, }; @@ -172,7 +176,7 @@ async fn check_github_app_installation() { let slug = cli_config.slug().map(String::from); // Build GitHub App credentials - let creds = match crate::shared::github::build_github_app_credentials(Some(&app_id)) { + let creds = match build_github_app_credentials(Some(&app_id)) { Some(c) => c, None => { eprintln!( @@ -264,7 +268,7 @@ async fn check_github_app_installation() { // Only prompt if stdin is a terminal if std::io::IsTerminal::is_terminal(&std::io::stdin()) { eprintln!(" Press Enter to continue after installing..."); - let _ = tokio::task::spawn_blocking(|| { + let _ = spawn_blocking(|| { let mut buf = String::new(); let _ = std::io::stdin().read_line(&mut buf); }) diff --git a/lib/crates/fabro-cli/src/commands/run/attach.rs b/lib/crates/fabro-cli/src/commands/run/attach.rs index 94934a1cf..4fddbb5a1 100644 --- a/lib/crates/fabro-cli/src/commands/run/attach.rs +++ b/lib/crates/fabro-cli/src/commands/run/attach.rs @@ -10,8 +10,11 @@ use anyhow::{bail, Result}; use fabro_interview::{AnswerValue, ConsoleInterviewer}; use fabro_store::RuntimeState; use fabro_util::terminal::Styles; -use fabro_workflows::records::{ConclusionExt, RunRecordExt}; +use fabro_workflows::outcome::StageStatus; +use fabro_workflows::records::{Conclusion, ConclusionExt, RunRecord, RunRecordExt}; use fabro_workflows::run_status::{RunStatus, RunStatusRecord, RunStatusRecordExt}; +use tokio::signal::ctrl_c; +use tokio::time::sleep; use super::run_progress; @@ -41,7 +44,7 @@ pub async fn attach_run( let mut engine_guard = engine_child.map(EngineChildGuard::new); let is_tty = std::io::stderr().is_terminal(); - let verbose = fabro_workflows::records::RunRecord::load(run_dir) + let verbose = RunRecord::load(run_dir) .map(|record| record.settings.verbose_enabled()) .unwrap_or(false); let mut progress_ui = run_progress::ProgressUI::new(is_tty, verbose); @@ -51,7 +54,7 @@ pub async fn attach_run( { let cancelled = Arc::clone(&cancelled); tokio::spawn(async move { - let _ = tokio::signal::ctrl_c().await; + let _ = ctrl_c().await; cancelled.store(true, Ordering::Relaxed); }); } @@ -62,7 +65,7 @@ pub async fn attach_run( // engine death so we surface the real failure instead of timing out. let mut wait_count = 0; while !progress_path.exists() { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; + sleep(std::time::Duration::from_millis(100)).await; wait_count += 1; // Check if engine died before writing any progress @@ -140,7 +143,7 @@ pub async fn attach_run( { break; } - tokio::time::sleep(Duration::from_millis(100)).await; + sleep(Duration::from_millis(100)).await; } } else { if let Some(guard) = engine_guard.as_mut() { @@ -246,7 +249,7 @@ pub async fn attach_run( } } - tokio::time::sleep(Duration::from_millis(100)).await; + sleep(Duration::from_millis(100)).await; } // Finish progress bars @@ -438,11 +441,10 @@ fn write_interview_response_atomically( fn determine_exit_code(conclusion_path: &Path, status_record: Option) -> ExitCode { if conclusion_path.exists() { - if let Ok(conclusion) = fabro_workflows::records::Conclusion::load(conclusion_path) { + if let Ok(conclusion) = Conclusion::load(conclusion_path) { let success = matches!( conclusion.status, - fabro_workflows::outcome::StageStatus::Success - | fabro_workflows::outcome::StageStatus::PartialSuccess + StageStatus::Success | StageStatus::PartialSuccess ); return if success { ExitCode::from(0) diff --git a/lib/crates/fabro-cli/src/commands/run/command.rs b/lib/crates/fabro-cli/src/commands/run/command.rs index 4a0f99273..e92fb7d70 100644 --- a/lib/crates/fabro-cli/src/commands/run/command.rs +++ b/lib/crates/fabro-cli/src/commands/run/command.rs @@ -1,11 +1,12 @@ use anyhow::Result; +use fabro_config::cli::load_cli_config; +use fabro_util::terminal::Styles; use crate::args::{GlobalArgs, RunArgs}; pub async fn execute(mut args: RunArgs, _globals: &GlobalArgs) -> Result<()> { - let styles: &'static fabro_util::terminal::Styles = - Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr())); - let cli_defaults = fabro_config::cli::load_cli_config(None)?; + let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); + let cli_defaults = load_cli_config(None)?; let cli_config: fabro_config::FabroSettings = cli_defaults.clone().try_into()?; args.verbose = args.verbose || cli_config.verbose_enabled(); diff --git a/lib/crates/fabro-cli/src/commands/run/cp.rs b/lib/crates/fabro-cli/src/commands/run/cp.rs index 1604b417a..9fc0b8711 100644 --- a/lib/crates/fabro-cli/src/commands/run/cp.rs +++ b/lib/crates/fabro-cli/src/commands/run/cp.rs @@ -1,11 +1,16 @@ use std::path::{Path, PathBuf}; use anyhow::{bail, Context, Result}; +use fabro_agent::sandbox::Sandbox; use fabro_config::FabroSettingsExt; +use fabro_sandbox::reconnect::reconnect; use fabro_sandbox::SandboxRecordExt; +use fabro_workflows::run_lookup::{resolve_run, runs_base}; +use tokio::fs; use tracing::{debug, info}; use crate::args::CpArgs; +use crate::cli_config::load_cli_settings; use crate::shared::split_run_path; enum CopyDirection { @@ -23,8 +28,8 @@ enum CopyDirection { pub async fn cp_command(args: CpArgs) -> Result<()> { let direction = parse_direction(&args.src, &args.dst)?; - let cli_config = crate::cli_config::load_cli_settings(None)?; - let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); + let cli_config = load_cli_settings(None)?; + let base = runs_base(&cli_config.storage_dir()); match direction { CopyDirection::Download { @@ -90,11 +95,8 @@ fn parse_direction(src: &str, dst: &str) -> Result { } } -async fn load_sandbox( - base: &Path, - run_prefix: &str, -) -> Result> { - let run_dir = fabro_workflows::run_lookup::resolve_run(base, run_prefix)?.path; +async fn load_sandbox(base: &Path, run_prefix: &str) -> Result> { + let run_dir = resolve_run(base, run_prefix)?.path; let sandbox_json = run_dir.join("sandbox.json"); debug!(path = %sandbox_json.display(), "Loading sandbox record"); let record = fabro_sandbox::SandboxRecord::load(&sandbox_json).context( @@ -102,11 +104,11 @@ async fn load_sandbox( )?; info!(run_id = %run_prefix, provider = %record.provider, "Connecting to sandbox"); - fabro_sandbox::reconnect::reconnect(&record).await + reconnect(&record).await } async fn download_recursive( - sandbox: &dyn fabro_agent::sandbox::Sandbox, + sandbox: &dyn Sandbox, remote_path: &str, local_path: &Path, ) -> Result<()> { @@ -123,7 +125,7 @@ async fn download_recursive( let remote_file = format!("{remote_path}/{}", entry.name); let local_file = local_path.join(&entry.name); if let Some(parent) = local_file.parent() { - tokio::fs::create_dir_all(parent) + fs::create_dir_all(parent) .await .with_context(|| format!("Failed to create directory {}", parent.display()))?; } @@ -139,7 +141,7 @@ async fn download_recursive( } async fn upload_recursive( - sandbox: &dyn fabro_agent::sandbox::Sandbox, + sandbox: &dyn Sandbox, local_path: &Path, remote_path: &str, ) -> Result<()> { @@ -147,7 +149,7 @@ async fn upload_recursive( let mut stack = vec![(local_path.to_path_buf(), remote_path.to_string())]; while let Some((dir_path, dir_remote)) = stack.pop() { - let mut entries = tokio::fs::read_dir(&dir_path) + let mut entries = fs::read_dir(&dir_path) .await .with_context(|| format!("Failed to read directory {}", dir_path.display()))?; diff --git a/lib/crates/fabro-cli/src/commands/run/create.rs b/lib/crates/fabro-cli/src/commands/run/create.rs index a8449b7e4..b09da535c 100644 --- a/lib/crates/fabro-cli/src/commands/run/create.rs +++ b/lib/crates/fabro-cli/src/commands/run/create.rs @@ -1,10 +1,11 @@ use std::path::PathBuf; use crate::args::RunArgs; -use fabro_config::project::ResolveSettingsInput; +use fabro_config::project::{resolve_settings, ResolveSettingsInput}; use fabro_config::{FabroConfig, FabroSettings}; - use fabro_util::terminal::Styles; +use fabro_workflows::error::FabroError; +use fabro_workflows::operations::{create, CreateRunInput, WorkflowInput}; use super::output::{print_diagnostics_from_error, print_workflow_report_from_persisted}; @@ -23,7 +24,7 @@ pub async fn create_run( .ok_or_else(|| anyhow::anyhow!("--workflow is required"))?; let cli_args_config = FabroConfig::try_from(args)?; let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); - let settings: FabroSettings = fabro_config::project::resolve_settings(ResolveSettingsInput { + let settings: FabroSettings = resolve_settings(ResolveSettingsInput { workflow_path: workflow_path.clone(), cwd: cwd.clone(), defaults: cli_defaults, @@ -31,26 +32,25 @@ pub async fn create_run( apply_project_config: true, })?; - let created = - match fabro_workflows::operations::create(fabro_workflows::operations::CreateRunInput { - workflow: fabro_workflows::operations::WorkflowInput::Path(workflow_path.clone()), - settings, - cwd, - workflow_slug: None, - run_dir: None, - run_id: args.run_id.clone(), - base_branch: None, - host_repo_path: None, - }) { - Ok(created) => created, - Err(fabro_workflows::error::FabroError::ValidationFailed { diagnostics }) => { - if !quiet { - print_diagnostics_from_error(&diagnostics, styles); - } - anyhow::bail!("Validation failed"); + let created = match create(CreateRunInput { + workflow: WorkflowInput::Path(workflow_path.clone()), + settings, + cwd, + workflow_slug: None, + run_dir: None, + run_id: args.run_id.clone(), + base_branch: None, + host_repo_path: None, + }) { + Ok(created) => created, + Err(FabroError::ValidationFailed { diagnostics }) => { + if !quiet { + print_diagnostics_from_error(&diagnostics, styles); } - Err(err) => return Err(err.into()), - }; + anyhow::bail!("Validation failed"); + } + Err(err) => return Err(err.into()), + }; if !quiet { print_workflow_report_from_persisted( diff --git a/lib/crates/fabro-cli/src/commands/run/detached.rs b/lib/crates/fabro-cli/src/commands/run/detached.rs index c2159ccfc..abe80604d 100644 --- a/lib/crates/fabro-cli/src/commands/run/detached.rs +++ b/lib/crates/fabro-cli/src/commands/run/detached.rs @@ -5,6 +5,8 @@ use anyhow::Result; use fabro_interview::FileInterviewer; use fabro_store::RuntimeState; use fabro_workflows::event::EventEmitter; +use fabro_workflows::git::GitAuthor; +use fabro_workflows::operations::{resume as resume_run, start as start_run, StartServices}; use crate::cli_config; use crate::shared; @@ -12,7 +14,7 @@ use crate::shared; pub async fn execute(run_dir: PathBuf, launcher_path: PathBuf, resume: bool) -> Result<()> { let cli_config = cli_config::load_cli_settings(None)?; let github_app = shared::github::build_github_app_credentials(cli_config.app_id()); - let git_author = fabro_workflows::git::GitAuthor::from_options( + let git_author = GitAuthor::from_options( cli_config.git_author().and_then(|a| a.name.clone()), cli_config.git_author().and_then(|a| a.email.clone()), ); @@ -22,7 +24,7 @@ pub async fn execute(run_dir: PathBuf, launcher_path: PathBuf, resume: bool) -> }); let runtime_state = RuntimeState::new(&run_dir); - let services = fabro_workflows::operations::StartServices { + let services = StartServices { cancel_token: None, emitter: Arc::new(EventEmitter::new()), interviewer: Arc::new(FileInterviewer::new( @@ -36,9 +38,9 @@ pub async fn execute(run_dir: PathBuf, launcher_path: PathBuf, resume: bool) -> }; if resume { - let _ = fabro_workflows::operations::resume(&run_dir, services).await?; + let _ = resume_run(&run_dir, services).await?; } else { - let _ = fabro_workflows::operations::start(&run_dir, services).await?; + let _ = start_run(&run_dir, services).await?; } Ok(()) diff --git a/lib/crates/fabro-cli/src/commands/run/diff.rs b/lib/crates/fabro-cli/src/commands/run/diff.rs index 2907f8d29..9a17d575a 100644 --- a/lib/crates/fabro-cli/src/commands/run/diff.rs +++ b/lib/crates/fabro-cli/src/commands/run/diff.rs @@ -3,17 +3,21 @@ use std::path::Path; use anyhow::{bail, Context, Result}; use fabro_config::FabroSettingsExt; +use fabro_sandbox::reconnect::reconnect; use fabro_sandbox::SandboxRecordExt; -use fabro_workflows::records::StartRecordExt; +use fabro_workflows::records::{StartRecord, StartRecordExt}; +use fabro_workflows::run_lookup::{resolve_run, runs_base}; +use fabro_workflows::sandbox_git::GIT_REMOTE; use tracing::{debug, info}; use crate::args::DiffArgs; +use crate::cli_config::load_cli_settings; pub async fn run(args: DiffArgs) -> Result<()> { info!(run_id = %args.run, "Showing diff"); - let cli_config = crate::cli_config::load_cli_settings(None)?; - let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); - let run_dir = fabro_workflows::run_lookup::resolve_run(&base, &args.run)?.path; + let cli_config = load_cli_settings(None)?; + let base = runs_base(&cli_config.storage_dir()); + let run_dir = resolve_run(&base, &args.run)?.path; let patch = resolve_diff(&run_dir, &args).await?; @@ -38,8 +42,7 @@ async fn resolve_diff(run_dir: &Path, args: &DiffArgs) -> Result { }); } - let start = fabro_workflows::records::StartRecord::load(run_dir) - .context("Failed to load start.json")?; + let start = StartRecord::load(run_dir).context("Failed to load start.json")?; let base_sha = start .base_sha @@ -66,7 +69,7 @@ async fn resolve_diff(run_dir: &Path, args: &DiffArgs) -> Result { )?; info!(provider = %record.provider, "Reconnecting to sandbox for live diff"); - let sandbox = fabro_sandbox::reconnect::reconnect(&record).await?; + let sandbox = reconnect(&record).await?; let cmd = build_live_diff_cmd(base_sha, args.stat, args.shortstat); debug!(cmd, "Running git diff in sandbox"); @@ -98,8 +101,7 @@ fn build_live_diff_cmd(base_sha: &str, stat: bool, shortstat: bool) -> String { ); format!( "{} add -N . && {} diff{flags} {quoted_sha}", - fabro_workflows::sandbox_git::GIT_REMOTE, - fabro_workflows::sandbox_git::GIT_REMOTE + GIT_REMOTE, GIT_REMOTE ) } diff --git a/lib/crates/fabro-cli/src/commands/run/fork.rs b/lib/crates/fabro-cli/src/commands/run/fork.rs index 7a4a69f25..f33861283 100644 --- a/lib/crates/fabro-cli/src/commands/run/fork.rs +++ b/lib/crates/fabro-cli/src/commands/run/fork.rs @@ -2,16 +2,19 @@ use anyhow::Context; use anyhow::Result; use fabro_git_storage::gitobj::Store; use fabro_util::terminal::Styles; +use fabro_workflows::operations::{ + build_timeline, find_run_id_by_prefix, fork, ForkRunInput, RewindTarget, +}; use git2::Repository; use crate::args::ForkArgs; pub fn run(args: &ForkArgs, styles: &Styles) -> Result<()> { let repo = Repository::discover(".").context("not in a git repository")?; - let run_id = fabro_workflows::operations::find_run_id_by_prefix(&repo, &args.run_id)?; + let run_id = find_run_id_by_prefix(&repo, &args.run_id)?; let store = Store::new(repo); - let timeline = fabro_workflows::operations::build_timeline(&store, &run_id)?; + let timeline = build_timeline(&store, &run_id)?; if args.list { super::rewind::print_timeline(&timeline, styles); @@ -21,11 +24,11 @@ pub fn run(args: &ForkArgs, styles: &Styles) -> Result<()> { let target = args .target .as_deref() - .map(str::parse::) + .map(str::parse::) .transpose()?; - let new_run_id = fabro_workflows::operations::fork( + let new_run_id = fork( &store, - fabro_workflows::operations::ForkRunInput { + ForkRunInput { source_run_id: run_id.clone(), target, push: !args.no_push, diff --git a/lib/crates/fabro-cli/src/commands/run/launcher.rs b/lib/crates/fabro-cli/src/commands/run/launcher.rs index c20f3167d..b0ffc0784 100644 --- a/lib/crates/fabro-cli/src/commands/run/launcher.rs +++ b/lib/crates/fabro-cli/src/commands/run/launcher.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; use chrono::{DateTime, Utc}; use fabro_config::FabroSettingsExt; -use fabro_workflows::records::RunRecordExt; +use fabro_workflows::records::{RunRecord, RunRecordExt}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -46,7 +46,7 @@ pub(crate) fn remove_launcher_record(path: &Path) { } pub(crate) fn active_launcher_record_for_run(run_dir: &Path) -> Option { - let run_record = fabro_workflows::records::RunRecord::load(run_dir).ok()?; + let run_record = RunRecord::load(run_dir).ok()?; let path = launcher_record_path(&run_record.settings.storage_dir(), &run_record.run_id); let launcher = read_launcher_record(&path)?; if launcher_record_is_running(&launcher) { diff --git a/lib/crates/fabro-cli/src/commands/run/logs.rs b/lib/crates/fabro-cli/src/commands/run/logs.rs index 7ece228c8..680c57b8d 100644 --- a/lib/crates/fabro-cli/src/commands/run/logs.rs +++ b/lib/crates/fabro-cli/src/commands/run/logs.rs @@ -5,14 +5,16 @@ use anyhow::{bail, Context, Result}; use chrono::{DateTime, Utc}; use fabro_config::FabroSettingsExt; use fabro_util::terminal::Styles; +use fabro_workflows::run_lookup::{resolve_run, runs_base}; use tracing::{debug, info}; use crate::args::LogsArgs; +use crate::cli_config::load_cli_settings; pub fn run(args: LogsArgs, styles: &Styles) -> Result<()> { - let cli_config = crate::cli_config::load_cli_settings(None)?; - let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); - let run = fabro_workflows::run_lookup::resolve_run(&base, &args.run)?; + let cli_config = load_cli_settings(None)?; + let base = runs_base(&cli_config.storage_dir()); + let run = resolve_run(&base, &args.run)?; info!(run_id = %run.run_id, "Showing logs"); diff --git a/lib/crates/fabro-cli/src/commands/run/mod.rs b/lib/crates/fabro-cli/src/commands/run/mod.rs index 2e3397369..d33245623 100644 --- a/lib/crates/fabro-cli/src/commands/run/mod.rs +++ b/lib/crates/fabro-cli/src/commands/run/mod.rs @@ -1,7 +1,11 @@ use anyhow::Result; +use fabro_config::cli::load_cli_config; use fabro_config::FabroSettingsExt; +use fabro_util::terminal::Styles; +use fabro_workflows::run_lookup::{resolve_run, runs_base}; use crate::args::{GlobalArgs, RunCommands}; +use crate::cli_config::load_cli_settings; pub(crate) mod attach; pub(crate) mod command; @@ -26,27 +30,25 @@ pub async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<()> { match cmd { RunCommands::Run(args) => command::execute(args, globals).await, RunCommands::Create(args) => { - let styles: &'static fabro_util::terminal::Styles = - Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr())); - let cli_defaults = fabro_config::cli::load_cli_config(None)?; + let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); + let cli_defaults = load_cli_config(None)?; let (run_id, _run_dir) = create::create_run(&args, cli_defaults, styles, true).await?; println!("{run_id}"); Ok(()) } RunCommands::Start { run } => { - let cli_config = crate::cli_config::load_cli_settings(None)?; - let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); - let run_info = fabro_workflows::run_lookup::resolve_run(&base, &run)?; + let cli_config = load_cli_settings(None)?; + let base = runs_base(&cli_config.storage_dir()); + let run_info = resolve_run(&base, &run)?; let child = start::start_run(&run_info.path, false)?; eprintln!("Started engine process (PID {})", child.id()); Ok(()) } RunCommands::Attach { run } => { - let styles: &'static fabro_util::terminal::Styles = - Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr())); - let cli_config = crate::cli_config::load_cli_settings(None)?; - let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); - let run_info = fabro_workflows::run_lookup::resolve_run(&base, &run)?; + let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); + let cli_config = load_cli_settings(None)?; + let base = runs_base(&cli_config.storage_dir()); + let run_info = resolve_run(&base, &run)?; let exit_code = attach::attach_run(&run_info.path, false, styles, None).await?; if exit_code != std::process::ExitCode::SUCCESS { std::process::exit(1); @@ -63,29 +65,28 @@ pub async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<()> { RunCommands::Ssh(args) => ssh::run(args).await, RunCommands::Diff(args) => diff::run(args).await, RunCommands::Logs(args) => { - let styles = fabro_util::terminal::Styles::detect_stdout(); + let styles = Styles::detect_stdout(); logs::run(args, &styles) } RunCommands::Resume(args) => { - let styles: &'static fabro_util::terminal::Styles = - Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr())); + let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); #[cfg(feature = "sleep_inhibitor")] let _sleep_guard = { - let cli_config = crate::cli_config::load_cli_settings(None)?; + let cli_config = load_cli_settings(None)?; crate::sleep_inhibitor::guard(cli_config.prevent_idle_sleep_enabled()) }; resume::resume_command(args, styles).await } RunCommands::Rewind(args) => { - let styles = fabro_util::terminal::Styles::detect_stderr(); + let styles = Styles::detect_stderr(); rewind::run(&args, &styles) } RunCommands::Fork(args) => { - let styles = fabro_util::terminal::Styles::detect_stderr(); + let styles = Styles::detect_stderr(); fork::run(&args, &styles) } RunCommands::Wait(args) => { - let styles = fabro_util::terminal::Styles::detect_stderr(); + let styles = Styles::detect_stderr(); wait::run(args, &styles) } } diff --git a/lib/crates/fabro-cli/src/commands/run/output.rs b/lib/crates/fabro-cli/src/commands/run/output.rs index dccaac6a5..3b41cd1d5 100644 --- a/lib/crates/fabro-cli/src/commands/run/output.rs +++ b/lib/crates/fabro-cli/src/commands/run/output.rs @@ -1,17 +1,21 @@ use std::path::Path; use std::time::Duration; +use fabro_graphviz::graph::Graph; use fabro_store::RuntimeState; use fabro_util::terminal::Styles; +use fabro_util::text::strip_goal_decoration; +use fabro_workflows::asset_snapshot::collect_asset_paths; use fabro_workflows::outcome::{format_cost, StageStatus}; use fabro_workflows::pipeline::{Persisted, Validated}; -use fabro_workflows::records::{Checkpoint, CheckpointExt, ConclusionExt}; +use fabro_workflows::pull_request::PullRequestRecord; +use fabro_workflows::records::{Checkpoint, CheckpointExt, Conclusion, ConclusionExt}; use indicatif::HumanDuration; use crate::shared::{format_tokens_human, print_diagnostics, relative_path, tilde_path}; fn print_workflow_header( - graph: &fabro_graphviz::graph::Graph, + graph: &Graph, diagnostics: &[fabro_validate::Diagnostic], dot_path: Option<&Path>, styles: &Styles, @@ -37,7 +41,7 @@ fn print_workflow_header( let goal = graph.goal(); if !goal.is_empty() { - let stripped = fabro_util::text::strip_goal_decoration(goal); + let stripped = strip_goal_decoration(goal); eprintln!("{} {stripped}\n", styles.bold.apply_to("Goal:")); } @@ -69,14 +73,14 @@ pub(crate) fn print_diagnostics_from_error( pub(crate) fn print_run_summary(run_dir: &Path, run_id: &str, styles: &Styles) { let conclusion_path = run_dir.join("conclusion.json"); - let Ok(conclusion) = fabro_workflows::records::Conclusion::load(&conclusion_path) else { + let Ok(conclusion) = Conclusion::load(&conclusion_path) else { return; }; let pr_url = std::fs::read_to_string(run_dir.join("pull_request.json")) .ok() .and_then(|content| { - serde_json::from_str::(&content) + serde_json::from_str::(&content) .ok() .map(|record| record.html_url) }); @@ -94,7 +98,7 @@ pub(crate) fn print_run_summary(run_dir: &Path, run_id: &str, styles: &Styles) { } pub(crate) fn print_run_conclusion( - conclusion: &fabro_workflows::records::Conclusion, + conclusion: &Conclusion, run_id: &str, run_dir: &Path, pushed_branch: Option<&str>, @@ -201,7 +205,7 @@ pub(crate) fn print_final_output(run_dir: &Path, styles: &Styles) { pub(crate) fn print_assets(run_dir: &Path, styles: &Styles) { let runtime_state = RuntimeState::new(run_dir); - let paths = fabro_workflows::asset_snapshot::collect_asset_paths(&runtime_state.assets_dir()); + let paths = collect_asset_paths(&runtime_state.assets_dir()); if paths.is_empty() { return; } diff --git a/lib/crates/fabro-cli/src/commands/run/overrides.rs b/lib/crates/fabro-cli/src/commands/run/overrides.rs index d4f5362dc..a68ab9868 100644 --- a/lib/crates/fabro-cli/src/commands/run/overrides.rs +++ b/lib/crates/fabro-cli/src/commands/run/overrides.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use anyhow::Result; +use fabro_config::run::LlmConfig; use fabro_config::{sandbox as sandbox_config, FabroConfig}; use fabro_sandbox::SandboxProvider; @@ -23,7 +24,7 @@ impl TryFrom<&RunArgs> for FabroConfig { fn try_from(args: &RunArgs) -> Result { let llm = if args.model.is_some() || args.provider.is_some() { - Some(fabro_config::run::LlmConfig { + Some(LlmConfig { model: args.model.clone(), provider: args.provider.clone(), fallbacks: None, @@ -65,7 +66,7 @@ impl TryFrom<&PreflightArgs> for FabroConfig { fn try_from(args: &PreflightArgs) -> Result { let llm = if args.model.is_some() || args.provider.is_some() { - Some(fabro_config::run::LlmConfig { + Some(LlmConfig { model: args.model.clone(), provider: args.provider.clone(), fallbacks: None, diff --git a/lib/crates/fabro-cli/src/commands/run/preview.rs b/lib/crates/fabro-cli/src/commands/run/preview.rs index ca602a558..d28113a10 100644 --- a/lib/crates/fabro-cli/src/commands/run/preview.rs +++ b/lib/crates/fabro-cli/src/commands/run/preview.rs @@ -1,15 +1,18 @@ use anyhow::{Context, Result}; use fabro_config::FabroSettingsExt; +use fabro_sandbox::daytona::DaytonaSandbox; use fabro_sandbox::SandboxRecordExt; +use fabro_workflows::run_lookup::{resolve_run, runs_base}; use tracing::info; use crate::args::PreviewArgs; +use crate::cli_config::load_cli_settings; use crate::shared::validate_daytona_provider; pub async fn run(args: PreviewArgs) -> Result<()> { - let cli_config = crate::cli_config::load_cli_settings(None)?; - let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); - let run_dir = fabro_workflows::run_lookup::resolve_run(&base, &args.run)?.path; + let cli_config = load_cli_settings(None)?; + let base = runs_base(&cli_config.storage_dir()); + let run_dir = resolve_run(&base, &args.run)?.path; let sandbox_json = run_dir.join("sandbox.json"); let record = fabro_sandbox::SandboxRecord::load(&sandbox_json).context( "Failed to load sandbox.json — was this run started with a recent version of arc?", @@ -24,7 +27,7 @@ pub async fn run(args: PreviewArgs) -> Result<()> { info!(run_id = %args.run, provider = %record.provider, port = args.port, "Generating preview URL"); - let daytona = fabro_sandbox::daytona::DaytonaSandbox::reconnect(name) + let daytona = DaytonaSandbox::reconnect(name) .await .map_err(|e| anyhow::anyhow!("{e}"))?; diff --git a/lib/crates/fabro-cli/src/commands/run/resume.rs b/lib/crates/fabro-cli/src/commands/run/resume.rs index 294d19b03..4ec915cc2 100644 --- a/lib/crates/fabro-cli/src/commands/run/resume.rs +++ b/lib/crates/fabro-cli/src/commands/run/resume.rs @@ -2,8 +2,10 @@ use anyhow::bail; use fabro_config::FabroSettingsExt; use fabro_util::terminal::Styles; use fabro_workflows::records::{RunRecord, RunRecordExt}; +use fabro_workflows::run_lookup::{find_run_by_prefix, runs_base}; use crate::args::ResumeArgs; +use crate::cli_config::load_cli_settings; /// Resume an interrupted workflow run. /// @@ -11,9 +13,9 @@ use crate::args::ResumeArgs; /// artifacts from the previous execution, then spawns an engine subprocess /// (identical to `fabro run`'s create→start→attach flow). pub async fn resume_command(args: ResumeArgs, styles: &'static Styles) -> anyhow::Result<()> { - let cli_config = crate::cli_config::load_cli_settings(None)?; - let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); - let run_dir = fabro_workflows::run_lookup::find_run_by_prefix(&base, &args.run)?; + let cli_config = load_cli_settings(None)?; + let base = runs_base(&cli_config.storage_dir()); + let run_dir = find_run_by_prefix(&base, &args.run)?; // find_run_by_prefix can match orphan directories (no run.json). if !run_dir.join("run.json").exists() { diff --git a/lib/crates/fabro-cli/src/commands/run/rewind.rs b/lib/crates/fabro-cli/src/commands/run/rewind.rs index 680cce59d..7a735dcc9 100644 --- a/lib/crates/fabro-cli/src/commands/run/rewind.rs +++ b/lib/crates/fabro-cli/src/commands/run/rewind.rs @@ -4,6 +4,9 @@ use cli_table::format::{Border, Separator}; use cli_table::{print_stderr, Cell, CellStruct, Color, Style, Table}; use fabro_git_storage::gitobj::Store; use fabro_util::terminal::Styles; +use fabro_workflows::operations::{ + build_timeline, find_run_id_by_prefix, rewind, RewindInput, RewindTarget, RunTimeline, +}; use git2::Repository; use crate::args::RewindArgs; @@ -11,25 +14,21 @@ use crate::shared::color_if; pub fn run(args: &RewindArgs, styles: &Styles) -> Result<()> { let repo = Repository::discover(".").context("not in a git repository")?; - let run_id = fabro_workflows::operations::find_run_id_by_prefix(&repo, &args.run_id)?; + let run_id = find_run_id_by_prefix(&repo, &args.run_id)?; let store = Store::new(repo); - let timeline = fabro_workflows::operations::build_timeline(&store, &run_id)?; + let timeline = build_timeline(&store, &run_id)?; if args.list || args.target.is_none() { print_timeline(&timeline, styles); return Ok(()); } - let target = args - .target - .as_deref() - .unwrap() - .parse::()?; + let target = args.target.as_deref().unwrap().parse::()?; - fabro_workflows::operations::rewind( + rewind( &store, - fabro_workflows::operations::RewindInput { + RewindInput { run_id: run_id.clone(), target, push: !args.no_push, @@ -44,7 +43,7 @@ pub fn run(args: &RewindArgs, styles: &Styles) -> Result<()> { Ok(()) } -pub(crate) fn print_timeline(timeline: &fabro_workflows::operations::RunTimeline, styles: &Styles) { +pub(crate) fn print_timeline(timeline: &RunTimeline, styles: &Styles) { if timeline.entries.is_empty() { eprintln!("No checkpoints found."); return; diff --git a/lib/crates/fabro-cli/src/commands/run/run_progress.rs b/lib/crates/fabro-cli/src/commands/run/run_progress.rs index 5824769c6..04010804b 100644 --- a/lib/crates/fabro-cli/src/commands/run/run_progress.rs +++ b/lib/crates/fabro-cli/src/commands/run/run_progress.rs @@ -9,6 +9,7 @@ use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle}; use fabro_agent::AgentEvent; use fabro_interview::{Answer, ConsoleInterviewer, Interviewer, Question}; +use fabro_util::version::FABRO_VERSION; use fabro_workflows::event::{EventEmitter, RunNoticeLevel, WorkflowRunEvent}; use fabro_workflows::outcome::StageStatus; @@ -1227,7 +1228,7 @@ impl ProgressUI { } pub fn show_version(&mut self) { - let version = fabro_util::version::FABRO_VERSION; + let version = FABRO_VERSION; match &self.renderer { ProgressRenderer::Tty(tty) => { let bar = tty.multi.add(ProgressBar::new_spinner()); diff --git a/lib/crates/fabro-cli/src/commands/run/ssh.rs b/lib/crates/fabro-cli/src/commands/run/ssh.rs index 82fc9fc55..ed9de411c 100644 --- a/lib/crates/fabro-cli/src/commands/run/ssh.rs +++ b/lib/crates/fabro-cli/src/commands/run/ssh.rs @@ -1,15 +1,18 @@ use anyhow::{bail, Context, Result}; use fabro_config::FabroSettingsExt; +use fabro_sandbox::daytona::DaytonaSandbox; use fabro_sandbox::SandboxRecordExt; +use fabro_workflows::run_lookup::{resolve_run, runs_base}; use tracing::info; use crate::args::SshArgs; +use crate::cli_config::load_cli_settings; use crate::shared::validate_daytona_provider; pub async fn run(args: SshArgs) -> Result<()> { - let cli_config = crate::cli_config::load_cli_settings(None)?; - let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); - let run_dir = fabro_workflows::run_lookup::resolve_run(&base, &args.run)?.path; + let cli_config = load_cli_settings(None)?; + let base = runs_base(&cli_config.storage_dir()); + let run_dir = resolve_run(&base, &args.run)?.path; let sandbox_json = run_dir.join("sandbox.json"); let record = fabro_sandbox::SandboxRecord::load(&sandbox_json).context( "Failed to load sandbox.json — was this run started with a recent version of arc?", @@ -24,7 +27,7 @@ pub async fn run(args: SshArgs) -> Result<()> { info!(run_id = %args.run, ttl_minutes = args.ttl, "Creating SSH access"); - let daytona = fabro_sandbox::daytona::DaytonaSandbox::reconnect(name) + let daytona = DaytonaSandbox::reconnect(name) .await .map_err(|e| anyhow::anyhow!("{e}"))?; diff --git a/lib/crates/fabro-cli/src/commands/run/start.rs b/lib/crates/fabro-cli/src/commands/run/start.rs index 08ecc2b4e..9cd6002ac 100644 --- a/lib/crates/fabro-cli/src/commands/run/start.rs +++ b/lib/crates/fabro-cli/src/commands/run/start.rs @@ -3,7 +3,7 @@ use std::path::Path; use anyhow::{anyhow, Result}; use chrono::Utc; use fabro_config::FabroSettingsExt; -use fabro_workflows::records::RunRecordExt; +use fabro_workflows::records::{RunRecord, RunRecordExt}; use super::launcher::{ launcher_log_path, launcher_record_path, remove_launcher_record, write_launcher_record, @@ -15,7 +15,7 @@ use super::launcher::{ /// The engine process reads `run.json` from the run directory and executes the /// workflow. Returns the child process handle (use `.id()` for the PID). pub fn start_run(run_dir: &Path, resume: bool) -> Result { - let record = fabro_workflows::records::RunRecord::load(run_dir) + let record = RunRecord::load(run_dir) .map_err(|e| anyhow!("Cannot start run: failed to load run.json: {e}"))?; let storage_dir = record.settings.storage_dir(); diff --git a/lib/crates/fabro-cli/src/commands/run/wait.rs b/lib/crates/fabro-cli/src/commands/run/wait.rs index 2d7188369..71eb92a02 100644 --- a/lib/crates/fabro-cli/src/commands/run/wait.rs +++ b/lib/crates/fabro-cli/src/commands/run/wait.rs @@ -3,17 +3,19 @@ use std::io::Write; use anyhow::{bail, Result}; use fabro_config::FabroSettingsExt; use fabro_util::terminal::Styles; -use fabro_workflows::records::ConclusionExt; +use fabro_workflows::records::{Conclusion, ConclusionExt}; +use fabro_workflows::run_lookup::{resolve_run, runs_base}; use fabro_workflows::run_status::{RunStatus, RunStatusRecord, RunStatusRecordExt}; use tracing::info; use crate::args::WaitArgs; +use crate::cli_config::load_cli_settings; use crate::shared::format_duration_ms; pub fn run(args: WaitArgs, styles: &Styles) -> Result<()> { - let cli_config = crate::cli_config::load_cli_settings(None)?; - let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); - let run_info = fabro_workflows::run_lookup::resolve_run(&base, &args.run)?; + let cli_config = load_cli_settings(None)?; + let base = runs_base(&cli_config.storage_dir()); + let run_info = resolve_run(&base, &args.run)?; info!(run_id = %run_info.run_id, "Waiting for run to complete"); @@ -49,7 +51,7 @@ pub fn run(args: WaitArgs, styles: &Styles) -> Result<()> { }; let conclusion_path = run_info.path.join("conclusion.json"); - let conclusion = fabro_workflows::records::Conclusion::load(&conclusion_path).ok(); + let conclusion = Conclusion::load(&conclusion_path).ok(); if args.json { let json_value = build_json_output(final_status, &run_info.run_id, conclusion.as_ref()); @@ -70,7 +72,7 @@ pub fn run(args: WaitArgs, styles: &Styles) -> Result<()> { fn build_json_output( status: RunStatus, run_id: &str, - conclusion: Option<&fabro_workflows::records::Conclusion>, + conclusion: Option<&Conclusion>, ) -> serde_json::Value { let mut value = serde_json::json!({ "run_id": run_id, @@ -88,7 +90,7 @@ fn build_json_output( fn print_human_output( status: RunStatus, run_id: &str, - conclusion: Option<&fabro_workflows::records::Conclusion>, + conclusion: Option<&Conclusion>, styles: &Styles, ) { let (style, label) = match status { diff --git a/lib/crates/fabro-cli/src/commands/runs/inspect.rs b/lib/crates/fabro-cli/src/commands/runs/inspect.rs index d6b99efd7..9a747304c 100644 --- a/lib/crates/fabro-cli/src/commands/runs/inspect.rs +++ b/lib/crates/fabro-cli/src/commands/runs/inspect.rs @@ -6,13 +6,18 @@ use fabro_sandbox::SandboxRecordExt; use fabro_workflows::records::{CheckpointExt, ConclusionExt, RunRecordExt, StartRecordExt}; use serde::Serialize; +use fabro_workflows::records::{Checkpoint, Conclusion, RunRecord, StartRecord}; +use fabro_workflows::run_lookup::{resolve_run, runs_base}; +use fabro_workflows::run_status::RunStatus; + use crate::args::InspectArgs; +use crate::cli_config::load_cli_settings; #[derive(Debug, Serialize)] pub struct InspectOutput { pub run_id: String, pub run_dir: PathBuf, - pub status: fabro_workflows::run_status::RunStatus, + pub status: RunStatus, pub run_record: Option, pub start_record: Option, pub conclusion: Option, @@ -21,30 +26,26 @@ pub struct InspectOutput { } pub fn run(args: &InspectArgs) -> Result<()> { - let cli_config = crate::cli_config::load_cli_settings(None)?; - let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); - let run = fabro_workflows::run_lookup::resolve_run(&base, &args.run)?; + let cli_config = load_cli_settings(None)?; + let base = runs_base(&cli_config.storage_dir()); + let run = resolve_run(&base, &args.run)?; let output = inspect_run_dir(&run.run_id, &run.path, run.status)?; let json = serde_json::to_string_pretty(&[output])?; println!("{json}"); Ok(()) } -fn inspect_run_dir( - run_id: &str, - run_dir: &Path, - status: fabro_workflows::run_status::RunStatus, -) -> Result { - let run_record = fabro_workflows::records::RunRecord::load(run_dir) +fn inspect_run_dir(run_id: &str, run_dir: &Path, status: RunStatus) -> Result { + let run_record = RunRecord::load(run_dir) .ok() .and_then(|v| serde_json::to_value(v).ok()); - let start_record = fabro_workflows::records::StartRecord::load(run_dir) + let start_record = StartRecord::load(run_dir) .ok() .and_then(|v| serde_json::to_value(v).ok()); - let conclusion = fabro_workflows::records::Conclusion::load(&run_dir.join("conclusion.json")) + let conclusion = Conclusion::load(&run_dir.join("conclusion.json")) .ok() .and_then(|v| serde_json::to_value(v).ok()); - let checkpoint = fabro_workflows::records::Checkpoint::load(&run_dir.join("checkpoint.json")) + let checkpoint = Checkpoint::load(&run_dir.join("checkpoint.json")) .ok() .and_then(|v| serde_json::to_value(v).ok()); let sandbox = fabro_sandbox::SandboxRecord::load(&run_dir.join("sandbox.json")) diff --git a/lib/crates/fabro-cli/src/commands/runs/list.rs b/lib/crates/fabro-cli/src/commands/runs/list.rs index 085fc445d..8b3c0e907 100644 --- a/lib/crates/fabro-cli/src/commands/runs/list.rs +++ b/lib/crates/fabro-cli/src/commands/runs/list.rs @@ -7,26 +7,31 @@ use cli_table::{print_stdout, Cell, CellStruct, Color, Style, Table}; use fabro_config::FabroSettingsExt; use fabro_util::terminal::Styles; +use fabro_util::text::strip_goal_decoration; +use fabro_workflows::run_lookup::{filter_runs, runs_base, scan_runs, StatusFilter}; +use fabro_workflows::run_status::RunStatus; + use crate::args::RunsListArgs; +use crate::cli_config::load_cli_settings; use crate::shared::{color_if, format_duration_ms, tilde_path}; use super::short_run_id; pub fn list_command(args: &RunsListArgs, styles: &Styles) -> Result<()> { - let cli_config = crate::cli_config::load_cli_settings(None)?; - let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); - let runs = fabro_workflows::run_lookup::scan_runs(&base)?; + let cli_config = load_cli_settings(None)?; + let base = runs_base(&cli_config.storage_dir()); + let runs = scan_runs(&base)?; let label_filters = parse_label_filters(&args.filter.label); - let filtered = fabro_workflows::run_lookup::filter_runs( + let filtered = filter_runs( &runs, args.filter.before.as_deref(), args.filter.workflow.as_deref(), &label_filters, args.filter.orphans, if args.all { - fabro_workflows::run_lookup::StatusFilter::All + StatusFilter::All } else { - fabro_workflows::run_lookup::StatusFilter::RunningOnly + StatusFilter::RunningOnly }, ); @@ -110,17 +115,15 @@ pub fn list_command(args: &RunsListArgs, styles: &Styles) -> Result<()> { Ok(()) } -fn status_cell(status: fabro_workflows::run_status::RunStatus, use_color: bool) -> CellStruct { +fn status_cell(status: RunStatus, use_color: bool) -> CellStruct { let text = status.to_string(); let color = match status { - fabro_workflows::run_status::RunStatus::Succeeded => Some(Color::Green), - fabro_workflows::run_status::RunStatus::Failed => Some(Color::Red), - fabro_workflows::run_status::RunStatus::Running - | fabro_workflows::run_status::RunStatus::Starting - | fabro_workflows::run_status::RunStatus::Submitted => Some(Color::Cyan), - fabro_workflows::run_status::RunStatus::Removing => Some(Color::Yellow), - fabro_workflows::run_status::RunStatus::Paused => Some(Color::Magenta), - fabro_workflows::run_status::RunStatus::Dead => Some(Color::Ansi256(8)), + RunStatus::Succeeded => Some(Color::Green), + RunStatus::Failed => Some(Color::Red), + RunStatus::Running | RunStatus::Starting | RunStatus::Submitted => Some(Color::Cyan), + RunStatus::Removing => Some(Color::Yellow), + RunStatus::Paused => Some(Color::Magenta), + RunStatus::Dead => Some(Color::Ansi256(8)), }; text.cell() .bold(use_color && color != Some(Color::Ansi256(8))) @@ -136,7 +139,7 @@ fn parse_label_filters(label_args: &[String]) -> Vec<(String, String)> { } fn truncate_goal(goal: &str, max_len: usize) -> String { - truncate_str(fabro_util::text::strip_goal_decoration(goal), max_len) + truncate_str(strip_goal_decoration(goal), max_len) } fn truncate_str(s: &str, max_len: usize) -> String { diff --git a/lib/crates/fabro-cli/src/commands/runs/mod.rs b/lib/crates/fabro-cli/src/commands/runs/mod.rs index 2007a297c..37390c616 100644 --- a/lib/crates/fabro-cli/src/commands/runs/mod.rs +++ b/lib/crates/fabro-cli/src/commands/runs/mod.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use fabro_util::terminal::Styles; use crate::args::RunsCommands; @@ -9,7 +10,7 @@ pub(crate) mod rm; pub async fn dispatch(cmd: RunsCommands) -> Result<()> { match cmd { RunsCommands::Ps(args) => { - let styles = fabro_util::terminal::Styles::detect_stdout(); + let styles = Styles::detect_stdout(); list::list_command(&args, &styles) } RunsCommands::Rm(args) => rm::remove_command(&args).await, diff --git a/lib/crates/fabro-cli/src/commands/runs/rm.rs b/lib/crates/fabro-cli/src/commands/runs/rm.rs index 9e8ac747a..c1f202088 100644 --- a/lib/crates/fabro-cli/src/commands/runs/rm.rs +++ b/lib/crates/fabro-cli/src/commands/runs/rm.rs @@ -5,13 +5,18 @@ use fabro_config::FabroSettingsExt; use fabro_sandbox::SandboxRecordExt; use tracing::warn; +use fabro_sandbox::reconnect::reconnect as reconnect_sandbox; +use fabro_workflows::run_lookup::{resolve_run, runs_base}; +use fabro_workflows::run_status::{write_run_status, RunStatus}; + use crate::args::RunsRemoveArgs; +use crate::cli_config::load_cli_settings; use super::short_run_id; pub async fn remove_command(args: &RunsRemoveArgs) -> Result<()> { - let cli_config = crate::cli_config::load_cli_settings(None)?; - let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); + let cli_config = load_cli_settings(None)?; + let base = runs_base(&cli_config.storage_dir()); remove_from(args, &base).await } @@ -19,7 +24,7 @@ async fn remove_from(args: &RunsRemoveArgs, base: &Path) -> Result<()> { let mut had_errors = false; for identifier in &args.runs { - let run = match fabro_workflows::run_lookup::resolve_run(base, identifier) { + let run = match resolve_run(base, identifier) { Ok(run) => run, Err(err) => { eprintln!("error: {identifier}: {err}"); @@ -38,16 +43,12 @@ async fn remove_from(args: &RunsRemoveArgs, base: &Path) -> Result<()> { continue; } - fabro_workflows::run_status::write_run_status( - &run.path, - fabro_workflows::run_status::RunStatus::Removing, - None, - ); + write_run_status(&run.path, RunStatus::Removing, None); let sandbox_path = run.path.join("sandbox.json"); if let Ok(record) = fabro_sandbox::SandboxRecord::load(&sandbox_path) { if record.provider != "local" { - match fabro_sandbox::reconnect::reconnect(&record).await { + match reconnect_sandbox(&record).await { Ok(sandbox) => { if let Err(err) = sandbox.cleanup().await { warn!(run_id = %run.run_id, error = %err, "sandbox cleanup failed"); diff --git a/lib/crates/fabro-cli/src/commands/system/df.rs b/lib/crates/fabro-cli/src/commands/system/df.rs index f030e881d..c7aa804c5 100644 --- a/lib/crates/fabro-cli/src/commands/system/df.rs +++ b/lib/crates/fabro-cli/src/commands/system/df.rs @@ -6,19 +6,23 @@ use cli_table::format::{Border, Justify, Separator}; use cli_table::{print_stdout, Cell, CellStruct, Style, Table}; use fabro_config::FabroSettingsExt; +use fabro_workflows::run_lookup::{logs_base, runs_base, scan_runs}; +use fabro_workflows::run_status::RunStatus; + use crate::args::DfArgs; +use crate::cli_config::load_cli_settings; use crate::shared::format_size; pub fn df_command(args: &DfArgs) -> Result<()> { - let cli_config = crate::cli_config::load_cli_settings(None)?; + let cli_config = load_cli_settings(None)?; let data_dir = cli_config.storage_dir(); - let runs_base = fabro_workflows::run_lookup::runs_base(&data_dir); - let logs_base = fabro_workflows::run_lookup::logs_base(&data_dir); - df_from(args, &data_dir, &runs_base, &logs_base) + let runs_base_dir = runs_base(&data_dir); + let logs_base_dir = logs_base(&data_dir); + df_from(args, &data_dir, &runs_base_dir, &logs_base_dir) } fn df_from(args: &DfArgs, data_dir: &Path, runs_base: &Path, logs_base: &Path) -> Result<()> { - let runs = fabro_workflows::run_lookup::scan_runs(runs_base)?; + let runs = scan_runs(runs_base)?; let mut active_count = 0u64; let mut total_run_size = 0u64; let mut reclaimable_run_size = 0u64; @@ -26,7 +30,7 @@ fn df_from(args: &DfArgs, data_dir: &Path, runs_base: &Path, logs_base: &Path) - struct RunSizeInfo { run_id: String, workflow_name: String, - status: fabro_workflows::run_status::RunStatus, + status: RunStatus, start_time_dt: Option>, size: u64, } diff --git a/lib/crates/fabro-cli/src/commands/system/prune.rs b/lib/crates/fabro-cli/src/commands/system/prune.rs index 23e744b85..651be4cc1 100644 --- a/lib/crates/fabro-cli/src/commands/system/prune.rs +++ b/lib/crates/fabro-cli/src/commands/system/prune.rs @@ -5,12 +5,15 @@ use chrono::Utc; use fabro_config::FabroSettingsExt; use tracing::{debug, info}; +use fabro_workflows::run_lookup::{filter_runs, runs_base, scan_runs, StatusFilter}; + use crate::args::RunsPruneArgs; +use crate::cli_config::load_cli_settings; use crate::shared::format_size; pub fn prune_command(args: &RunsPruneArgs) -> Result<()> { - let cli_config = crate::cli_config::load_cli_settings(None)?; - let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); + let cli_config = load_cli_settings(None)?; + let base = runs_base(&cli_config.storage_dir()); prune_from(args, &base) } @@ -31,15 +34,15 @@ pub(crate) fn parse_duration(s: &str) -> Result { } fn prune_from(args: &RunsPruneArgs, base: &Path) -> Result<()> { - let runs = fabro_workflows::run_lookup::scan_runs(base)?; + let runs = scan_runs(base)?; let label_filters = parse_label_filters(&args.filter.label); - let mut filtered = fabro_workflows::run_lookup::filter_runs( + let mut filtered = filter_runs( &runs, args.filter.before.as_deref(), args.filter.workflow.as_deref(), &label_filters, args.filter.orphans, - fabro_workflows::run_lookup::StatusFilter::All, + StatusFilter::All, ); let has_explicit_filters = diff --git a/lib/crates/fabro-cli/src/commands/upgrade.rs b/lib/crates/fabro-cli/src/commands/upgrade.rs index 8984f2e23..8a4474d4f 100644 --- a/lib/crates/fabro-cli/src/commands/upgrade.rs +++ b/lib/crates/fabro-cli/src/commands/upgrade.rs @@ -7,6 +7,9 @@ use semver::Version; use sha2::{Digest, Sha256}; use tracing::debug; +use tokio::process::Command as TokioCommand; +use tokio::task::JoinHandle; + use crate::args::UpgradeArgs; // ── Download backend abstraction ─────────────────────────────────────────── @@ -29,7 +32,7 @@ impl Backend { async fn fetch_latest_release_tag(&self) -> Result { match self { Backend::Gh => { - let output = tokio::process::Command::new("gh") + let output = TokioCommand::new("gh") .args([ "release", "view", @@ -75,7 +78,7 @@ impl Backend { let dest = dest_dir.join(asset); match self { Backend::Gh => { - let status = tokio::process::Command::new("gh") + let status = TokioCommand::new("gh") .args([ "release", "download", @@ -117,10 +120,7 @@ impl Backend { async fn select_backend() -> Backend { // Check if gh is available - let gh_version = tokio::process::Command::new("gh") - .arg("--version") - .output() - .await; + let gh_version = TokioCommand::new("gh").arg("--version").output().await; let Ok(output) = gh_version else { debug!("gh CLI not found, using HTTP backend"); return Backend::Http(http_client().expect("failed to build HTTP client")); @@ -131,7 +131,7 @@ async fn select_backend() -> Backend { } // Check if gh is authenticated - let auth_status = tokio::process::Command::new("gh") + let auth_status = TokioCommand::new("gh") .args(["auth", "status"]) .output() .await; @@ -349,7 +349,7 @@ pub async fn run_upgrade(args: UpgradeArgs) -> Result<()> { pub fn spawn_upgrade_check( no_upgrade_check: bool, upgrade_check_enabled: bool, -) -> Option> { +) -> Option> { if no_upgrade_check || !upgrade_check_enabled { return None; } diff --git a/lib/crates/fabro-cli/src/commands/validate.rs b/lib/crates/fabro-cli/src/commands/validate.rs index 5a6b1b666..bd49182b7 100644 --- a/lib/crates/fabro-cli/src/commands/validate.rs +++ b/lib/crates/fabro-cli/src/commands/validate.rs @@ -1,29 +1,31 @@ use anyhow::bail; -use fabro_config::project::ResolveSettingsInput; +use fabro_config::cli::load_cli_config; +use fabro_config::project::{resolve_settings, resolve_workflow_path, ResolveSettingsInput}; +use fabro_config::FabroConfig; use fabro_util::terminal::Styles; use fabro_validate::Severity; +use fabro_workflows::operations::{validate, ValidateInput, WorkflowInput}; use crate::args::ValidateArgs; use crate::shared::{print_diagnostics, relative_path}; pub fn run(args: &ValidateArgs, styles: &Styles) -> anyhow::Result<()> { let cwd = std::env::current_dir()?; - let cli_defaults = fabro_config::cli::load_cli_config(None)?; - let settings = fabro_config::project::resolve_settings(ResolveSettingsInput { + let cli_defaults = load_cli_config(None)?; + let settings = resolve_settings(ResolveSettingsInput { workflow_path: args.workflow.clone(), cwd: cwd.clone(), defaults: cli_defaults, - overrides: fabro_config::FabroConfig::default(), + overrides: FabroConfig::default(), apply_project_config: true, })?; - let resolution = fabro_config::project::resolve_workflow_path(&args.workflow, &cwd)?; - let validated = - fabro_workflows::operations::validate(fabro_workflows::operations::ValidateInput { - workflow: fabro_workflows::operations::WorkflowInput::Path(args.workflow.clone()), - settings, - cwd, - custom_transforms: Vec::new(), - })?; + let resolution = resolve_workflow_path(&args.workflow, &cwd)?; + let validated = validate(ValidateInput { + workflow: WorkflowInput::Path(args.workflow.clone()), + settings, + cwd, + custom_transforms: Vec::new(), + })?; let graph = validated.graph(); let diagnostics = validated.diagnostics(); diff --git a/lib/crates/fabro-cli/src/commands/workflow/create.rs b/lib/crates/fabro-cli/src/commands/workflow/create.rs index 96c59180d..9a4f2dab3 100644 --- a/lib/crates/fabro-cli/src/commands/workflow/create.rs +++ b/lib/crates/fabro-cli/src/commands/workflow/create.rs @@ -2,13 +2,15 @@ use std::path::Path; use anyhow::{bail, Context, Result}; +use fabro_config::project::{discover_project_config, resolve_fabro_root}; + use crate::args::WorkflowCreateArgs; use crate::shared::relative_path; pub fn create_command(args: &WorkflowCreateArgs) -> Result<()> { let cwd = std::env::current_dir()?; - let (config_path, config) = match fabro_config::project::discover_project_config(&cwd)? { + let (config_path, config) = match discover_project_config(&cwd)? { Some(found) => found, None => bail!( "No fabro.toml found in {cwd} or any parent directory", @@ -16,7 +18,7 @@ pub fn create_command(args: &WorkflowCreateArgs) -> Result<()> { ), }; - let fabro_root = fabro_config::project::resolve_fabro_root(&config_path, &config); + let fabro_root = resolve_fabro_root(&config_path, &config); write_workflow_scaffold(args, &fabro_root)?; let workflows_dir = fabro_root.join("workflows").join(&args.name); diff --git a/lib/crates/fabro-cli/src/commands/workflow/list.rs b/lib/crates/fabro-cli/src/commands/workflow/list.rs index 338621cd9..c772effa1 100644 --- a/lib/crates/fabro-cli/src/commands/workflow/list.rs +++ b/lib/crates/fabro-cli/src/commands/workflow/list.rs @@ -1,6 +1,11 @@ use anyhow::{bail, Result}; use fabro_util::terminal::Styles; +use fabro_config::project::{ + discover_project_config, list_workflows_detailed, resolve_fabro_root, WorkflowInfo, + WorkflowSource, +}; + use crate::args::WorkflowListArgs; use crate::shared::relative_path; @@ -10,7 +15,7 @@ pub fn list_command(_args: &WorkflowListArgs) -> Result<()> { let styles = Styles::detect_stderr(); let cwd = std::env::current_dir()?; - let (config_path, config) = match fabro_config::project::discover_project_config(&cwd)? { + let (config_path, config) = match discover_project_config(&cwd)? { Some(found) => found, None => bail!( "No fabro.toml found in {cwd} or any parent directory", @@ -18,22 +23,19 @@ pub fn list_command(_args: &WorkflowListArgs) -> Result<()> { ), }; - let fabro_root = fabro_config::project::resolve_fabro_root(&config_path, &config); + let fabro_root = resolve_fabro_root(&config_path, &config); let project_wf_dir = fabro_root.join("workflows"); let user_wf_dir = dirs::home_dir().map(|h| h.join(".fabro").join("workflows")); - let workflows = fabro_config::project::list_workflows_detailed( - Some(&project_wf_dir), - user_wf_dir.as_deref(), - ); + let workflows = list_workflows_detailed(Some(&project_wf_dir), user_wf_dir.as_deref()); let project: Vec<_> = workflows .iter() - .filter(|w| w.source == fabro_config::project::WorkflowSource::Project) + .filter(|w| w.source == WorkflowSource::Project) .collect(); let user: Vec<_> = workflows .iter() - .filter(|w| w.source == fabro_config::project::WorkflowSource::User) + .filter(|w| w.source == WorkflowSource::User) .collect(); let name_width = workflows.iter().map(|w| w.name.len()).max().unwrap_or(0); @@ -65,7 +67,7 @@ pub fn list_command(_args: &WorkflowListArgs) -> Result<()> { fn print_section( title: &str, path: &str, - workflows: &[&fabro_config::project::WorkflowInfo], + workflows: &[&WorkflowInfo], name_width: usize, styles: &Styles, ) { diff --git a/lib/crates/fabro-cli/src/logging.rs b/lib/crates/fabro-cli/src/logging.rs index c5eab4a27..affcefdb6 100644 --- a/lib/crates/fabro-cli/src/logging.rs +++ b/lib/crates/fabro-cli/src/logging.rs @@ -1,4 +1,6 @@ use anyhow::{Context, Result}; +use fabro_util::run_log; +use tracing_appender::rolling; use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; pub fn init_tracing(debug: bool, config_log_level: Option<&str>, log_prefix: &str) -> Result<()> { @@ -20,9 +22,9 @@ pub fn init_tracing(debug: bool, config_log_level: Option<&str>, log_prefix: &st let filename = chrono::Local::now() .format(&format!("{log_prefix}-%Y-%m-%d.log")) .to_string(); - let file_appender = tracing_appender::rolling::never(&log_dir, &filename); + let file_appender = rolling::never(&log_dir, &filename); - let run_log_writer = fabro_util::run_log::init(); + let run_log_writer = run_log::init(); tracing_subscriber::registry() .with(filter) diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs index 89e36843d..eb6d26dd8 100644 --- a/lib/crates/fabro-cli/src/main.rs +++ b/lib/crates/fabro-cli/src/main.rs @@ -9,6 +9,9 @@ mod sleep_inhibitor; use anyhow::Result; use args::{Commands, GlobalArgs, RunCommands, LONG_VERSION}; use clap::Parser; +use fabro_telemetry::{git, panic as tel_panic, sanitize, sender}; +use fabro_util::terminal::Styles; +use rustls::crypto::ring::default_provider; use tracing::debug; #[derive(Parser)] @@ -23,7 +26,7 @@ struct Cli { #[tokio::main] async fn main() { - fabro_telemetry::panic::install_panic_hook(); + tel_panic::install_panic_hook(); fabro_telemetry::init_cli(); let start = std::time::Instant::now(); @@ -33,8 +36,8 @@ async fn main() { let duration_ms = start.elapsed().as_millis() as u64; let is_error = result.is_err(); - let command = fabro_telemetry::sanitize::sanitize_command(&raw_args, &command_name); - let repository = fabro_telemetry::git::repository_identifier(); + let command = sanitize::sanitize_command(&raw_args, &command_name); + let repository = git::repository_identifier(); let ci = std::env::var("CI").is_ok(); if is_error { fabro_telemetry::track!("CLI Errored", { @@ -82,7 +85,7 @@ async fn main() { } async fn main_inner() -> (String, Result<()>) { - let _ = rustls::crypto::ring::default_provider().install_default(); + let _ = default_provider().install_default(); let cli = Cli::parse(); if let Some(home) = dirs::home_dir() { @@ -107,7 +110,7 @@ async fn main_inner() -> (String, Result<()>) { Err(err) => return (command_name, Err(err)), } } else { - match crate::cli_config::load_cli_settings(None) { + match cli_config::load_cli_settings(None) { Ok(cli_config) => ( cli_config.log.as_ref().and_then(|l| l.level.clone()), cli_config.upgrade_check_enabled(), @@ -118,7 +121,7 @@ async fn main_inner() -> (String, Result<()>) { } #[cfg(not(feature = "server"))] { - match crate::cli_config::load_cli_settings(None) { + match cli_config::load_cli_settings(None) { Ok(cli_config) => ( cli_config.log.as_ref().and_then(|l| l.level.clone()), cli_config.upgrade_check_enabled(), @@ -160,11 +163,11 @@ async fn main_inner() -> (String, Result<()>) { Commands::RunCmd(cmd) => commands::run::dispatch(cmd, &globals).await?, Commands::Preflight(args) => commands::preflight::execute(args).await?, Commands::Validate(args) => { - let styles = fabro_util::terminal::Styles::detect_stderr(); + let styles = Styles::detect_stderr(); commands::validate::run(&args, &styles)?; } Commands::Graph(args) => { - let styles = fabro_util::terminal::Styles::detect_stderr(); + let styles = Styles::detect_stderr(); commands::graph::run(&args, &styles)?; } Commands::Parse(args) => { @@ -175,8 +178,7 @@ async fn main_inner() -> (String, Result<()>) { Commands::Model { command } => commands::model::execute(command, &globals).await?, #[cfg(feature = "server")] Commands::Serve(args) => { - let styles: &'static fabro_util::terminal::Styles = - Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr())); + let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr())); fabro_api::serve::serve_command(args, styles).await?; } Commands::Doctor { verbose, dry_run } => { @@ -213,12 +215,12 @@ async fn main_inner() -> (String, Result<()>) { Commands::Provider(ns) => commands::provider::dispatch(ns).await?, Commands::System(ns) => commands::system::dispatch(ns)?, Commands::SendAnalytics { path } => { - let result = fabro_telemetry::sender::upload(&path).await; + let result = sender::upload(&path).await; let _ = std::fs::remove_file(&path); result?; } Commands::SendPanic { path } => { - let result = fabro_telemetry::panic::capture(&path).await; + let result = tel_panic::capture(&path).await; let _ = std::fs::remove_file(&path); result?; } diff --git a/lib/crates/fabro-cli/src/shared/github.rs b/lib/crates/fabro-cli/src/shared/github.rs index be9fc546f..19c93c78c 100644 --- a/lib/crates/fabro-cli/src/shared/github.rs +++ b/lib/crates/fabro-cli/src/shared/github.rs @@ -1,16 +1,17 @@ -pub(crate) fn build_github_app_credentials( - app_id: Option<&str>, -) -> Option { +use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; +use base64::Engine as _; +use fabro_github::GitHubAppCredentials; + +pub(crate) fn build_github_app_credentials(app_id: Option<&str>) -> Option { let app_id = app_id?; let raw = std::env::var("GITHUB_APP_PRIVATE_KEY").ok()?; let private_key_pem = if raw.starts_with("-----") { raw } else { - let pem_bytes = - base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &raw).ok()?; + let pem_bytes = BASE64_STANDARD.decode(&raw).ok()?; String::from_utf8(pem_bytes).ok()? }; - Some(fabro_github::GitHubAppCredentials { + Some(GitHubAppCredentials { app_id: app_id.to_string(), private_key_pem, }) diff --git a/lib/crates/fabro-cli/src/shared/provider_auth.rs b/lib/crates/fabro-cli/src/shared/provider_auth.rs index 933caa345..711a64e15 100644 --- a/lib/crates/fabro-cli/src/shared/provider_auth.rs +++ b/lib/crates/fabro-cli/src/shared/provider_auth.rs @@ -1,9 +1,16 @@ use std::path::Path; use anyhow::Result; +use dialoguer::console::Term; +use dialoguer::theme::ColorfulTheme; use dialoguer::{Confirm, Password}; +use fabro_config::dotenv::{merge_env, write_env_file as write_env}; +use fabro_llm::client::Client as LlmClient; +use fabro_llm::generate::{generate, GenerateParams}; use fabro_model::Provider; use fabro_util::terminal::Styles; +use tokio::task::spawn_blocking; +use tokio::time::timeout; use crate::commands::doctor; @@ -111,20 +118,16 @@ pub(crate) async fn run_openai_oauth_or_api_key(s: &Styles) -> Result Result { - Ok( - Confirm::with_theme(&dialoguer::theme::ColorfulTheme::default()) - .with_prompt(prompt) - .default(default) - .interact_on(&dialoguer::console::Term::stderr())?, - ) + Ok(Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt(prompt) + .default(default) + .interact_on(&Term::stderr())?) } pub(crate) fn prompt_password(prompt: &str) -> Result { - Ok( - Password::with_theme(&dialoguer::theme::ColorfulTheme::default()) - .with_prompt(prompt) - .interact_on(&dialoguer::console::Term::stderr())?, - ) + Ok(Password::with_theme(&ColorfulTheme::default()) + .with_prompt(prompt) + .interact_on(&Term::stderr())?) } // --------------------------------------------------------------------------- @@ -142,8 +145,8 @@ pub(crate) fn write_env_file( .iter() .map(|(k, v)| (k.as_str(), v.as_str())) .collect(); - let merged = fabro_config::dotenv::merge_env(&existing, &refs); - fabro_config::dotenv::write_env_file(&env_path, &merged)?; + let merged = merge_env(&existing, &refs); + write_env(&env_path, &merged)?; eprintln!( " {}", s.dim.apply_to(format!("Wrote {}", env_path.display())) @@ -160,24 +163,19 @@ pub(crate) async fn validate_api_key(provider: Provider, api_key: &str) -> Resul let env_var = provider.api_key_env_vars()[0]; std::env::set_var(env_var, api_key); - let client = fabro_llm::client::Client::from_env() - .await - .map_err(|e| e.to_string())?; + let client = LlmClient::from_env().await.map_err(|e| e.to_string())?; - let params = fabro_llm::generate::GenerateParams::new(doctor::probe_model(provider)) + let params = GenerateParams::new(doctor::probe_model(provider)) .provider(provider.as_str()) .prompt("Say OK") .max_tokens(16) .client(std::sync::Arc::new(client)); - tokio::time::timeout( - std::time::Duration::from_secs(30), - fabro_llm::generate::generate(params), - ) - .await - .map_err(|_| "timeout (30s)".to_string())? - .map(|_| ()) - .map_err(|e| e.to_string()) + timeout(std::time::Duration::from_secs(30), generate(params)) + .await + .map_err(|_| "timeout (30s)".to_string())? + .map(|_| ()) + .map_err(|e| e.to_string()) } pub(crate) async fn prompt_and_validate_key( @@ -193,7 +191,7 @@ pub(crate) async fn prompt_and_validate_key( loop { let prompt = env_var.to_string(); - let key: String = tokio::task::spawn_blocking(move || prompt_password(&prompt)).await??; + let key: String = spawn_blocking(move || prompt_password(&prompt)).await??; eprintln!(" {}", s.dim.apply_to("Validating API key...")); match validate_api_key(provider, &key).await { @@ -203,10 +201,9 @@ pub(crate) async fn prompt_and_validate_key( } Err(e) => { eprintln!(" [error] API key validation failed: {e}"); - let retry = tokio::task::spawn_blocking(|| { - prompt_confirm("Try again with a different key?", true) - }) - .await??; + let retry = + spawn_blocking(|| prompt_confirm("Try again with a different key?", true)) + .await??; if !retry { return Ok((env_var.to_string(), key)); } diff --git a/lib/crates/fabro-config/src/cli.rs b/lib/crates/fabro-config/src/cli.rs index 62339c218..f8e9b55e0 100644 --- a/lib/crates/fabro-config/src/cli.rs +++ b/lib/crates/fabro-config/src/cli.rs @@ -3,6 +3,8 @@ use std::path::{Path, PathBuf}; use anyhow::anyhow; use serde::{Deserialize, Serialize}; +use crate::config::FabroConfig; + pub use fabro_types::settings::cli::{ ClientTlsSettings, ExecSettings, ExecutionMode, OutputFormat, PermissionLevel, ServerSettings, }; @@ -70,6 +72,6 @@ impl From for ExecSettings { /// Load CLI config from an explicit path or `~/.fabro/cli.toml`, returning defaults if the /// default file doesn't exist. An explicit path that doesn't exist is an error. -pub fn load_cli_config(path: Option<&Path>) -> anyhow::Result { +pub fn load_cli_config(path: Option<&Path>) -> anyhow::Result { crate::load_config_file(path, "cli.toml") } diff --git a/lib/crates/fabro-config/src/lib.rs b/lib/crates/fabro-config/src/lib.rs index 215a3ec40..03be6d1ee 100644 --- a/lib/crates/fabro-config/src/lib.rs +++ b/lib/crates/fabro-config/src/lib.rs @@ -19,13 +19,15 @@ pub use settings::{FabroSettings, FabroSettingsExt}; use std::path::Path; +use serde::de::DeserializeOwned; + /// Load a TOML config from an explicit path or `~/.fabro/{filename}`. /// /// Returns `T::default()` when no explicit path is given and the default file /// doesn't exist. An explicit path that doesn't exist is an error. pub fn load_config_file(path: Option<&Path>, filename: &str) -> anyhow::Result where - T: Default + serde::de::DeserializeOwned, + T: Default + DeserializeOwned, { if let Some(explicit) = path { tracing::debug!(path = %explicit.display(), "Loading config from explicit path"); diff --git a/lib/crates/fabro-config/src/run.rs b/lib/crates/fabro-config/src/run.rs index 027a666b4..55d4b9dec 100644 --- a/lib/crates/fabro-config/src/run.rs +++ b/lib/crates/fabro-config/src/run.rs @@ -7,6 +7,7 @@ use tracing::debug; use crate::combine::Combine; use crate::config::FabroConfig; +use crate::sandbox::DockerfileSource; pub use fabro_types::settings::run::{ AssetsSettings, CheckpointSettings, GitHubSettings, LlmSettings, MergeStrategy, PullRequestSettings, SetupSettings, @@ -179,12 +180,12 @@ fn resolve_dockerfile(config: &mut FabroConfig, config_dir: &Path) -> anyhow::Re .and_then(|d| d.snapshot.as_mut()) .and_then(|snap| snap.dockerfile.as_mut()); - if let Some(crate::sandbox::DockerfileSource::Path { path: ref rel }) = source { + if let Some(DockerfileSource::Path { path: ref rel }) = source { let path = config_dir.join(rel); let contents = std::fs::read_to_string(&path) .with_context(|| format!("Failed to read dockerfile at {}", path.display()))?; debug!(path = %path.display(), "Resolved dockerfile from path"); - *source.unwrap() = crate::sandbox::DockerfileSource::Inline(contents); + *source.unwrap() = DockerfileSource::Inline(contents); } Ok(()) diff --git a/lib/crates/fabro-core/src/executor.rs b/lib/crates/fabro-core/src/executor.rs index c78b5ab2a..0d7180047 100644 --- a/lib/crates/fabro-core/src/executor.rs +++ b/lib/crates/fabro-core/src/executor.rs @@ -5,7 +5,7 @@ use std::time::Instant; use tokio_util::sync::CancellationToken; use crate::context::Context; -use crate::error::{CoreError, Result}; +use crate::error::{CoreError, Result, VisitLimitSource}; use crate::graph::{EdgeSpec, Graph, NodeSpec}; use crate::handler::NodeHandler; use crate::lifecycle::{ @@ -14,6 +14,7 @@ use crate::lifecycle::{ }; use crate::outcome::{NodeResult, NodeResultExt, Outcome, StageStatus}; use crate::state::RunState; +use tokio::time::sleep; #[derive(Default)] pub struct ExecutorOptions { @@ -147,7 +148,7 @@ impl Executor { node_id: node.id().to_string(), visits, limit: max, - limit_source: crate::error::VisitLimitSource::Node, + limit_source: VisitLimitSource::Node, }); } } @@ -157,7 +158,7 @@ impl Executor { node_id: node.id().to_string(), visits, limit: global_max, - limit_source: crate::error::VisitLimitSource::Graph, + limit_source: VisitLimitSource::Graph, }); } } @@ -276,7 +277,7 @@ impl Executor { backoff_delay: Some(delay), }; self.lifecycle.after_attempt(&ctx, state).await?; - tokio::time::sleep(delay).await; + sleep(delay).await; } Ok(outcome) if outcome.status == StageStatus::Retry => { let final_outcome = self.handler.on_retries_exhausted(node, outcome); @@ -321,7 +322,7 @@ impl Executor { backoff_delay: Some(delay), }; self.lifecycle.after_attempt(&ctx, state).await?; - tokio::time::sleep(delay).await; + sleep(delay).await; } Err(e) => { // Convert handler error to fail outcome so routing continues @@ -1876,7 +1877,7 @@ mod tests { // Cancel stall token while "running" self.0.cancel(); // Simulate long work - tokio::time::sleep(Duration::from_secs(10)).await; + sleep(Duration::from_secs(10)).await; Ok(Outcome::success()) } } @@ -1973,7 +1974,7 @@ mod tests { _s: &RunState, ) -> Result { self.0.cancel(); - tokio::time::sleep(Duration::from_secs(10)).await; + sleep(Duration::from_secs(10)).await; Ok(NodeDecision::Continue) } } diff --git a/lib/crates/fabro-core/src/outcome.rs b/lib/crates/fabro-core/src/outcome.rs index 8aaecec8f..e635ace7f 100644 --- a/lib/crates/fabro-core/src/outcome.rs +++ b/lib/crates/fabro-core/src/outcome.rs @@ -1,25 +1,16 @@ use std::time::Duration; +use crate::error::CoreError; pub use fabro_types::outcome::{ FailureCategory, FailureDetail, NodeResult, Outcome, OutcomeMeta, StageStatus, }; pub trait NodeResultExt { - fn from_error( - error: &crate::error::CoreError, - duration: Duration, - attempts: u32, - max_attempts: u32, - ) -> Self; + fn from_error(error: &CoreError, duration: Duration, attempts: u32, max_attempts: u32) -> Self; } impl NodeResultExt for NodeResult { - fn from_error( - error: &crate::error::CoreError, - duration: Duration, - attempts: u32, - max_attempts: u32, - ) -> Self { + fn from_error(error: &CoreError, duration: Duration, attempts: u32, max_attempts: u32) -> Self { Self { outcome: error.to_fail_outcome(), duration, diff --git a/lib/crates/fabro-core/src/stall.rs b/lib/crates/fabro-core/src/stall.rs index 47a01006a..e1184497d 100644 --- a/lib/crates/fabro-core/src/stall.rs +++ b/lib/crates/fabro-core/src/stall.rs @@ -3,6 +3,8 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::Notify; +use tokio::task::JoinHandle; +use tokio::time::sleep; /// Trait for receiving stall timeout notifications. pub trait ActivityMonitor: Send + Sync { @@ -25,7 +27,7 @@ pub struct StallWatchdog { pub struct StallGuard { activity: Arc, shutdown: Arc, - handle: Option>, + handle: Option>, } impl StallWatchdog { @@ -55,7 +57,7 @@ impl StallWatchdog { let handle = tokio::spawn(async move { loop { tokio::select! { - _ = tokio::time::sleep(timeout) => { + _ = sleep(timeout) => { if shutdown.load(Ordering::Relaxed) { return; } diff --git a/lib/crates/fabro-devcontainer/src/features.rs b/lib/crates/fabro-devcontainer/src/features.rs index 5aeeb9629..d3fd1e3d2 100644 --- a/lib/crates/fabro-devcontainer/src/features.rs +++ b/lib/crates/fabro-devcontainer/src/features.rs @@ -1,6 +1,8 @@ use std::collections::{HashMap, HashSet, VecDeque}; use std::path::Path; +use tokio::fs; +use tokio::process::Command; use tracing::info; use crate::types::{FeatureMetadata, LifecycleCommand}; @@ -61,7 +63,7 @@ fn dir_name_from_id(feature_id: &str) -> String { /// Ensure `oras` CLI is available, installing it if necessary. async fn ensure_oras() -> crate::Result<()> { - let check = tokio::process::Command::new("which") + let check = Command::new("which") .arg("oras") .output() .await @@ -74,7 +76,7 @@ async fn ensure_oras() -> crate::Result<()> { info!("oras not found, attempting to install"); if cfg!(target_os = "macos") { - let status = tokio::process::Command::new("brew") + let status = Command::new("brew") .args(["install", "oras"]) .status() .await @@ -93,7 +95,7 @@ async fn ensure_oras() -> crate::Result<()> { .map_err(|_| DevcontainerError::OrasInstall("HOME not set".to_string()))?; let bin_dir = format!("{home}/.local/bin"); - tokio::fs::create_dir_all(&bin_dir).await.map_err(|e| { + fs::create_dir_all(&bin_dir).await.map_err(|e| { DevcontainerError::OrasInstall(format!("failed to create {bin_dir}: {e}")) })?; @@ -107,7 +109,7 @@ async fn ensure_oras() -> crate::Result<()> { "https://github.com/oras-project/oras/releases/download/v{version}/oras_{version}_linux_{arch}.tar.gz" ); - let status = tokio::process::Command::new("sh") + let status = Command::new("sh") .args([ "-c", &format!("curl -fsSL '{url}' | tar xzf - -C '{bin_dir}' oras"), @@ -128,7 +130,7 @@ async fn ensure_oras() -> crate::Result<()> { /// Find the first `.tgz` file in a directory. async fn find_tgz(dir: &Path) -> Option { - let mut entries = tokio::fs::read_dir(dir).await.ok()?; + let mut entries = fs::read_dir(dir).await.ok()?; while let Ok(Some(entry)) = entries.next_entry().await { if let Some(name) = entry.file_name().to_str() { if name.ends_with(".tgz") { @@ -141,7 +143,7 @@ async fn find_tgz(dir: &Path) -> Option { /// Extract a tgz archive in the given directory. async fn extract_tgz(feature_dir: &Path, tgz_name: &str, feature_id: &str) -> crate::Result<()> { - let status = tokio::process::Command::new("tar") + let status = Command::new("tar") .args(["xzf", tgz_name]) .current_dir(feature_dir) .status() @@ -159,11 +161,9 @@ async fn extract_tgz(feature_dir: &Path, tgz_name: &str, feature_id: &str) -> cr /// Read and parse devcontainer-feature.json from a feature directory. async fn read_feature_metadata(feature_dir: &Path) -> crate::Result { let metadata_path = feature_dir.join("devcontainer-feature.json"); - let metadata_str = tokio::fs::read_to_string(&metadata_path) - .await - .map_err(|e| { - DevcontainerError::Feature(format!("failed to read {}: {e}", metadata_path.display())) - })?; + let metadata_str = fs::read_to_string(&metadata_path).await.map_err(|e| { + DevcontainerError::Feature(format!("failed to read {}: {e}", metadata_path.display())) + })?; serde_json::from_str(&metadata_str).map_err(|e| { DevcontainerError::Feature(format!("failed to parse {}: {e}", metadata_path.display())) @@ -177,7 +177,7 @@ async fn create_feature_dir( ) -> crate::Result { let dir_name = dir_name_from_id(feature_id); let feature_dir = output_dir.join(&dir_name); - tokio::fs::create_dir_all(&feature_dir).await.map_err(|e| { + fs::create_dir_all(&feature_dir).await.map_err(|e| { DevcontainerError::Feature(format!( "failed to create dir {}: {e}", feature_dir.display() @@ -192,7 +192,7 @@ async fn fetch_feature_oci(feature_id: &str, output_dir: &Path) -> crate::Result info!(feature_id, "pulling feature with oras"); - let output = tokio::process::Command::new("oras") + let output = Command::new("oras") .args(["pull", feature_id, "-o"]) .arg(&feature_dir) .output() @@ -259,7 +259,7 @@ async fn fetch_feature_https( })?; let tgz_path = feature_dir.join("devcontainer-feature.tgz"); - tokio::fs::write(&tgz_path, &bytes).await.map_err(|e| { + fs::write(&tgz_path, &bytes).await.map_err(|e| { DevcontainerError::Feature(format!("failed to write {}: {e}", tgz_path.display())) })?; @@ -290,11 +290,11 @@ async fn fetch_feature_dispatch( /// Recursively copy a directory. async fn copy_dir_recursive(src: &Path, dst: &Path) -> crate::Result<()> { - tokio::fs::create_dir_all(dst).await.map_err(|e| { + fs::create_dir_all(dst).await.map_err(|e| { DevcontainerError::Feature(format!("failed to create dir {}: {e}", dst.display())) })?; - let mut entries = tokio::fs::read_dir(src).await.map_err(|e| { + let mut entries = fs::read_dir(src).await.map_err(|e| { DevcontainerError::Feature(format!("failed to read dir {}: {e}", src.display())) })?; @@ -309,15 +309,13 @@ async fn copy_dir_recursive(src: &Path, dst: &Path) -> crate::Result<()> { if entry_path.is_dir() { Box::pin(copy_dir_recursive(&entry_path, &dest_path)).await?; } else { - tokio::fs::copy(&entry_path, &dest_path) - .await - .map_err(|e| { - DevcontainerError::Feature(format!( - "failed to copy {} to {}: {e}", - entry_path.display(), - dest_path.display() - )) - })?; + fs::copy(&entry_path, &dest_path).await.map_err(|e| { + DevcontainerError::Feature(format!( + "failed to copy {} to {}: {e}", + entry_path.display(), + dest_path.display() + )) + })?; } } @@ -548,7 +546,7 @@ pub async fn resolve_features( .as_nanos() ); let tmp_dir = std::env::temp_dir().join(unique_id); - tokio::fs::create_dir_all(&tmp_dir) + fs::create_dir_all(&tmp_dir) .await .map_err(|e| DevcontainerError::Feature(format!("failed to create temp dir: {e}")))?; diff --git a/lib/crates/fabro-devcontainer/src/lib.rs b/lib/crates/fabro-devcontainer/src/lib.rs index 84c94e863..33b14f63d 100644 --- a/lib/crates/fabro-devcontainer/src/lib.rs +++ b/lib/crates/fabro-devcontainer/src/lib.rs @@ -10,6 +10,7 @@ mod variables; use std::collections::HashMap; use std::path::{Path, PathBuf}; +use fabro_util::env::SystemEnv; pub use types::DevcontainerJson; /// Lifecycle command — string, array, or object (parallel) form. @@ -174,7 +175,7 @@ impl DevcontainerResolver { .clone() .unwrap_or_else(|| format!("/workspaces/{repo_name}")); - let system_env = fabro_util::env::SystemEnv; + let system_env = SystemEnv; let preliminary_vars = variables::VariableContext { local_workspace_folder: repo_root.to_string_lossy().to_string(), local_workspace_folder_basename: repo_name.clone(), diff --git a/lib/crates/fabro-devcontainer/src/variables.rs b/lib/crates/fabro-devcontainer/src/variables.rs index fd9aee496..72b5f1533 100644 --- a/lib/crates/fabro-devcontainer/src/variables.rs +++ b/lib/crates/fabro-devcontainer/src/variables.rs @@ -1,9 +1,11 @@ +use fabro_util::env::Env; + /// Context for variable substitution. pub struct VariableContext<'a> { pub local_workspace_folder: String, pub local_workspace_folder_basename: String, pub container_workspace_folder: String, - pub env: &'a dyn fabro_util::env::Env, + pub env: &'a dyn Env, } /// Replace devcontainer variables in a string value. diff --git a/lib/crates/fabro-graphviz/src/parser/grammar.rs b/lib/crates/fabro-graphviz/src/parser/grammar.rs index 45c7f40cd..f8d16ac77 100644 --- a/lib/crates/fabro-graphviz/src/parser/grammar.rs +++ b/lib/crates/fabro-graphviz/src/parser/grammar.rs @@ -1,6 +1,8 @@ use nom::branch::alt; -use nom::character::complete::char; +use nom::bytes::complete::tag; +use nom::character::complete::{char, multispace0}; use nom::combinator::opt; +use nom::error::{Error, ParseError}; use nom::multi::{many0, separated_list0}; use nom::sequence::{delimited, preceded, tuple}; use nom::IResult; @@ -83,11 +85,11 @@ fn node_or_edge_stmt(input: &str) -> IResult<&str, Statement> { let (rest, first_id) = preceded(ws, identifier)(input)?; // Try to parse as edge: first_id (-> id)+ [attrs]? ;? - if let Ok((rest2, _)) = arrow::>(rest) { + if let Ok((rest2, _)) = arrow::>(rest) { let (rest2, second_id) = preceded(ws, identifier)(rest2)?; let mut nodes = vec![first_id.to_string(), second_id.to_string()]; let mut remaining = rest2; - while let Ok((r, _)) = arrow::>(remaining) { + while let Ok((r, _)) = arrow::>(remaining) { let (r, next_id) = preceded(ws, identifier)(r)?; nodes.push(next_id.to_string()); remaining = r; @@ -148,11 +150,8 @@ pub fn parse_dot_graph(input: &str) -> IResult<&str, DotGraph> { } // We need arrow to work with explicit error types -fn arrow<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, &'a str, E> { - preceded( - nom::character::complete::multispace0, - nom::bytes::complete::tag("->"), - )(input) +fn arrow<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, &'a str, E> { + preceded(multispace0, tag("->"))(input) } #[cfg(test)] diff --git a/lib/crates/fabro-graphviz/src/parser/lexer.rs b/lib/crates/fabro-graphviz/src/parser/lexer.rs index f2b1d80e8..fe9331dd5 100644 --- a/lib/crates/fabro-graphviz/src/parser/lexer.rs +++ b/lib/crates/fabro-graphviz/src/parser/lexer.rs @@ -56,8 +56,9 @@ pub mod combinators { use nom::bytes::complete::{tag, take_while, take_while1}; use nom::character::complete::{char, multispace0}; use nom::combinator::{map, opt, recognize}; + use nom::error::{Error, ErrorKind}; use nom::sequence::{delimited, pair, preceded}; - use nom::IResult; + use nom::{Err, IResult}; use crate::parser::ast::AstValue; @@ -85,7 +86,7 @@ pub mod combinators { let mut result = first.to_string(); let mut remaining = rest; let mut found_dot = false; - while let Ok((r, _)) = char::<&str, nom::error::Error<&str>>('.')(remaining) { + while let Ok((r, _)) = char::<&str, Error<&str>>('.')(remaining) { if let Ok((r2, segment)) = identifier(r) { result.push('.'); result.push_str(segment); @@ -98,10 +99,7 @@ pub mod combinators { if found_dot { Ok((remaining, result)) } else { - Err(nom::Err::Error(nom::error::Error::new( - input, - nom::error::ErrorKind::Tag, - ))) + Err(Err::Error(Error::new(input, ErrorKind::Tag))) } } @@ -148,10 +146,7 @@ pub mod combinators { consumed += c.len_utf8(); } None => { - return Err(nom::Err::Error(nom::error::Error::new( - input, - nom::error::ErrorKind::Char, - ))); + return Err(Err::Error(Error::new(input, ErrorKind::Char))); } } } @@ -160,10 +155,7 @@ pub mod combinators { consumed += c.len_utf8(); } None => { - return Err(nom::Err::Error(nom::error::Error::new( - input, - nom::error::ErrorKind::Char, - ))); + return Err(Err::Error(Error::new(input, ErrorKind::Char))); } } } @@ -175,10 +167,7 @@ pub mod combinators { match word { "true" => Ok((rest, true)), "false" => Ok((rest, false)), - _ => Err(nom::Err::Error(nom::error::Error::new( - input, - nom::error::ErrorKind::Tag, - ))), + _ => Err(Err::Error(Error::new(input, ErrorKind::Tag))), } } @@ -188,9 +177,9 @@ pub mod combinators { pair(opt(char('-')), take_while(|c: char| c.is_ascii_digit())), pair(char('.'), take_while1(|c: char| c.is_ascii_digit())), ))(input)?; - let val: f64 = raw.parse().map_err(|_| { - nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Float)) - })?; + let val: f64 = raw + .parse() + .map_err(|_| Err::Error(Error::new(input, ErrorKind::Float)))?; Ok((rest, val)) } @@ -201,14 +190,11 @@ pub mod combinators { take_while1(|c: char| c.is_ascii_digit()), ))(input)?; if rest.starts_with('.') { - return Err(nom::Err::Error(nom::error::Error::new( - input, - nom::error::ErrorKind::Digit, - ))); + return Err(Err::Error(Error::new(input, ErrorKind::Digit))); } - let val: i64 = raw.parse().map_err(|_| { - nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit)) - })?; + let val: i64 = raw + .parse() + .map_err(|_| Err::Error(Error::new(input, ErrorKind::Digit)))?; Ok((rest, val)) } @@ -224,10 +210,7 @@ pub mod combinators { .next() .is_some_and(|c| c.is_ascii_alphanumeric()) { - return Err(nom::Err::Error(nom::error::Error::new( - input, - nom::error::ErrorKind::Tag, - ))); + return Err(Err::Error(Error::new(input, ErrorKind::Tag))); } Ok((rest, AstValue::Str(format!("{num}{unit}")))) } @@ -243,10 +226,7 @@ pub mod combinators { take_while(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.'), ))(input)?; if !raw.contains('-') && !raw.contains('.') { - return Err(nom::Err::Error(nom::error::Error::new( - input, - nom::error::ErrorKind::Verify, - ))); + return Err(Err::Error(Error::new(input, ErrorKind::Verify))); } Ok((rest, raw.to_string())) } diff --git a/lib/crates/fabro-graphviz/src/parser/semantic.rs b/lib/crates/fabro-graphviz/src/parser/semantic.rs index abe5f8702..2756f81ea 100644 --- a/lib/crates/fabro-graphviz/src/parser/semantic.rs +++ b/lib/crates/fabro-graphviz/src/parser/semantic.rs @@ -3,7 +3,7 @@ use std::time::Duration; use crate::error::GraphvizError; use crate::graph::types::{AttrValue, Edge, Graph, Node}; -use crate::parser::ast::{AstValue, AttrBlock, DotGraph, Statement}; +use crate::parser::ast::{AstValue, AttrBlock, DotGraph, EdgeStmt, NodeStmt, Statement}; /// Convert an AST `AstValue` to a semantic `AttrValue`. fn convert_value(ast_val: &AstValue) -> AttrValue { @@ -89,11 +89,7 @@ impl SemanticState { } } - fn process_node( - &mut self, - node_stmt: &crate::parser::ast::NodeStmt, - subgraph_class: Option<&str>, - ) { + fn process_node(&mut self, node_stmt: &NodeStmt, subgraph_class: Option<&str>) { self.ensure_node(&node_stmt.id); let node = self .graph @@ -141,11 +137,7 @@ impl SemanticState { } } - fn process_edge( - &mut self, - edge_stmt: &crate::parser::ast::EdgeStmt, - subgraph_class: Option<&str>, - ) { + fn process_edge(&mut self, edge_stmt: &EdgeStmt, subgraph_class: Option<&str>) { for id in &edge_stmt.nodes { self.ensure_node(id); if let Some(cls) = subgraph_class { diff --git a/lib/crates/fabro-hooks/src/executor.rs b/lib/crates/fabro-hooks/src/executor.rs index 58e44a561..200d2000a 100644 --- a/lib/crates/fabro-hooks/src/executor.rs +++ b/lib/crates/fabro-hooks/src/executor.rs @@ -5,8 +5,15 @@ use std::sync::{Arc, LazyLock}; use std::time::Instant; use async_trait::async_trait; - +use fabro_agent::tool_registry::ToolContext; use fabro_agent::Sandbox; +use fabro_llm::client::Client as LlmClient; +use fabro_llm::generate::{generate_object, GenerateParams}; +use fabro_llm::types::{Message, Request, ToolResult}; +use fabro_util::env::{Env, SystemEnv}; +use tokio::process::Command as TokioCommand; +use tokio::time::timeout as tokio_timeout; +use tokio_util::sync::CancellationToken; use crate::config::{HookDefinition, HookType, TlsMode}; use crate::types::{HookContext, HookDecision, HookResult, PromptHookResponse}; @@ -40,11 +47,7 @@ pub trait HookExecutor: Send + Sync { /// Interpolate `$VAR` and `${VAR}` references in `value` using environment /// variables, but only when the variable name appears in `allowed_vars`. /// Unlisted or missing vars are replaced with the empty string. -pub fn interpolate_env_vars( - value: &str, - allowed_vars: &[String], - env: &dyn fabro_util::env::Env, -) -> String { +pub fn interpolate_env_vars(value: &str, allowed_vars: &[String], env: &dyn Env) -> String { let mut result = String::with_capacity(value.len()); let mut chars = value.chars().peekable(); @@ -149,7 +152,7 @@ impl HookExecutorImpl { }, } } else { - let mut cmd = tokio::process::Command::new("sh"); + let mut cmd = TokioCommand::new("sh"); cmd.arg("-c").arg(command); if let Some(wd) = work_dir { cmd.current_dir(wd); @@ -238,7 +241,7 @@ impl HookExecutorImpl { F: FnOnce() -> Fut, Fut: std::future::Future, { - match tokio::time::timeout(timeout, f()).await { + match tokio_timeout(timeout, f()).await { Ok(decision) => decision, Err(_) => { tracing::warn!("{hook_kind} hook timed out, proceeding"); @@ -258,12 +261,12 @@ impl HookExecutorImpl { let user_msg = Self::build_hook_user_message(prompt, context); Self::execute_llm_with_timeout(timeout, "prompt", || async move { - let params = fabro_llm::generate::GenerateParams::new(&resolved_model) + let params = GenerateParams::new(&resolved_model) .system(HOOK_EVALUATOR_SYSTEM_PROMPT) .prompt(user_msg) .max_tokens(1024); - match fabro_llm::generate::generate_object(params, HOOK_RESPONSE_SCHEMA.clone()).await { + match generate_object(params, HOOK_RESPONSE_SCHEMA.clone()).await { Ok(result) => match result.output { Some(obj) => match serde_json::from_value::(obj) { Ok(resp) if resp.ok => HookDecision::Proceed, @@ -306,7 +309,7 @@ impl HookExecutorImpl { let user_msg = Self::build_hook_user_message(prompt, context); Self::execute_llm_with_timeout(timeout, "agent", || async move { - let client = match fabro_llm::client::Client::from_env().await { + let client = match LlmClient::from_env().await { Ok(c) => c, Err(e) => { tracing::warn!(error = %e, "agent hook client creation failed, proceeding"); @@ -320,15 +323,15 @@ impl HookExecutorImpl { let tool_defs = registry.definitions(); let mut messages = vec![ - fabro_llm::types::Message::system(HOOK_EVALUATOR_SYSTEM_PROMPT), - fabro_llm::types::Message::user(user_msg), + Message::system(HOOK_EVALUATOR_SYSTEM_PROMPT), + Message::user(user_msg), ]; let rounds = max_tool_rounds.unwrap_or(50); - let cancel = tokio_util::sync::CancellationToken::new(); + let cancel = CancellationToken::new(); for _ in 0..rounds { - let request = fabro_llm::types::Request { + let request = Request { model: resolved_model.clone(), messages: messages.clone(), provider: None, @@ -362,25 +365,23 @@ impl HookExecutorImpl { for tc in &tool_calls { let tool = registry.get(&tc.name).cloned(); - let ctx = fabro_agent::tool_registry::ToolContext { + let ctx = ToolContext { env: sandbox.clone(), cancel: cancel.child_token(), tool_env: None, }; let result = match tool { Some(t) => match (t.executor)(tc.arguments.clone(), ctx).await { - Ok(output) => fabro_llm::types::ToolResult::success( - tc.id.clone(), - serde_json::json!(output), - ), - Err(err) => fabro_llm::types::ToolResult::error(tc.id.clone(), err), + Ok(output) => { + ToolResult::success(tc.id.clone(), serde_json::json!(output)) + } + Err(err) => ToolResult::error(tc.id.clone(), err), }, - None => fabro_llm::types::ToolResult::error( - tc.id.clone(), - format!("Unknown tool: {}", tc.name), - ), + None => { + ToolResult::error(tc.id.clone(), format!("Unknown tool: {}", tc.name)) + } }; - messages.push(fabro_llm::types::Message::tool_result( + messages.push(Message::tool_result( result.tool_call_id, result.content, result.is_error, @@ -414,7 +415,7 @@ impl HookExecutorImpl { tls: &TlsMode, context: &HookContext, timeout: std::time::Duration, - env: &dyn fabro_util::env::Env, + env: &dyn Env, ) -> HookDecision { // Enforce URL scheme based on TLS mode match tls { @@ -551,7 +552,7 @@ impl HookExecutor for HookExecutorImpl { tls, context, definition.timeout(), - &fabro_util::env::SystemEnv, + &SystemEnv, ) .await } diff --git a/lib/crates/fabro-interview/src/console.rs b/lib/crates/fabro-interview/src/console.rs index 556881d66..f202108c2 100644 --- a/lib/crates/fabro-interview/src/console.rs +++ b/lib/crates/fabro-interview/src/console.rs @@ -4,7 +4,8 @@ use async_trait::async_trait; use dialoguer::console::Term; use dialoguer::theme::ColorfulTheme; use fabro_util::terminal::Styles; -use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::io::{self, AsyncBufReadExt, BufReader}; +use tokio::task; use crate::{Answer, AnswerValue, Interviewer, Question, QuestionOption, QuestionType}; @@ -55,7 +56,7 @@ fn find_matching_option(response: &str, options: &[QuestionOption]) -> Option PromptRead { // Print the prompt to stderr so it doesn't interfere with piped stdout eprint!("{prompt}"); - let stdin = tokio::io::stdin(); + let stdin = io::stdin(); let mut reader = BufReader::new(stdin); let mut line = String::new(); match reader.read_line(&mut line).await { @@ -219,7 +220,7 @@ impl Interviewer for ConsoleInterviewer { eprint!("{rendered}"); } let q = question; - return tokio::task::spawn_blocking(move || match q.question_type { + return task::spawn_blocking(move || match q.question_type { QuestionType::MultipleChoice => ask_select_interactive(&q), QuestionType::MultiSelect => ask_multi_select_interactive(&q), QuestionType::YesNo | QuestionType::Confirmation => ask_confirm_interactive(&q), diff --git a/lib/crates/fabro-interview/src/file.rs b/lib/crates/fabro-interview/src/file.rs index e8a2f40de..f22dad9a3 100644 --- a/lib/crates/fabro-interview/src/file.rs +++ b/lib/crates/fabro-interview/src/file.rs @@ -2,6 +2,8 @@ use std::path::PathBuf; use std::time::Duration; use async_trait::async_trait; +use tokio::fs; +use tokio::time; use crate::{Answer, Interviewer, Question}; @@ -49,17 +51,17 @@ impl FileInterviewer { let json = serde_json::to_string_pretty(question).expect("Question serialization failed"); let request_path = self.request_path(); if let Some(parent) = request_path.parent() { - tokio::fs::create_dir_all(parent).await?; + fs::create_dir_all(parent).await?; } let temp_path = request_path.with_extension("json.tmp"); - tokio::fs::write(&temp_path, json).await?; - tokio::fs::rename(temp_path, request_path).await + fs::write(&temp_path, json).await?; + fs::rename(temp_path, request_path).await } async fn cleanup_ipc_files(&self) { - let _ = tokio::fs::remove_file(self.request_path()).await; - let _ = tokio::fs::remove_file(self.response_path()).await; - let _ = tokio::fs::remove_file(self.claim_path()).await; + let _ = fs::remove_file(self.request_path()).await; + let _ = fs::remove_file(self.response_path()).await; + let _ = fs::remove_file(self.claim_path()).await; } } @@ -81,9 +83,9 @@ impl Interviewer for FileInterviewer { let response_path = self.response_path(); let claim_path = self.claim_path(); let mut claim_was_seen = false; - let mut reattach_deadline: Option = None; + let mut reattach_deadline: Option = None; loop { - match tokio::fs::read_to_string(&response_path).await { + match fs::read_to_string(&response_path).await { Ok(data) => match serde_json::from_str::(&data) { Ok(answer) => { self.cleanup_ipc_files().await; @@ -107,23 +109,23 @@ impl Interviewer for FileInterviewer { claim_was_seen = true; reattach_deadline = None; } else if claim_was_seen && reattach_deadline.is_none() { - reattach_deadline = Some(tokio::time::Instant::now() + REATTACH_WINDOW); + reattach_deadline = Some(time::Instant::now() + REATTACH_WINDOW); } if let Some(deadline) = reattach_deadline { - if tokio::time::Instant::now() >= deadline { + if time::Instant::now() >= deadline { self.cleanup_ipc_files().await; return default_for_claim_timeout.unwrap_or_else(Answer::timeout); } } - tokio::time::sleep(Duration::from_millis(100)).await; + time::sleep(Duration::from_millis(100)).await; } }; if let Some(secs) = timeout_secs { let duration = std::time::Duration::from_secs_f64(secs); - match tokio::time::timeout(duration, poll).await { + match time::timeout(duration, poll).await { Ok(answer) => answer, Err(_) => { self.cleanup_ipc_files().await; diff --git a/lib/crates/fabro-interview/src/lib.rs b/lib/crates/fabro-interview/src/lib.rs index 5a058df2f..280e912aa 100644 --- a/lib/crates/fabro-interview/src/lib.rs +++ b/lib/crates/fabro-interview/src/lib.rs @@ -11,6 +11,7 @@ use std::collections::HashMap; use async_trait::async_trait; use serde::{Deserialize, Serialize}; +use tokio::time; /// The type of question being asked. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -174,7 +175,7 @@ pub async fn ask_with_timeout(interviewer: &dyn Interviewer, question: Question) if let Some(secs) = timeout_secs { let duration = std::time::Duration::from_secs_f64(secs); - match tokio::time::timeout(duration, interviewer.ask(question)).await { + match time::timeout(duration, interviewer.ask(question)).await { Ok(answer) => answer, Err(_elapsed) => default_answer.unwrap_or_else(Answer::timeout), } diff --git a/lib/crates/fabro-llm/src/cli.rs b/lib/crates/fabro-llm/src/cli.rs index b052618f0..63ee14c95 100644 --- a/lib/crates/fabro-llm/src/cli.rs +++ b/lib/crates/fabro-llm/src/cli.rs @@ -8,8 +8,10 @@ use anyhow::{bail, Context, Result}; use clap::{Args, Subcommand}; use cli_table::format::{Border, Justify, Separator}; use cli_table::{print_stdout, Cell, CellStruct, Color, Style, Table}; -use futures::StreamExt; +use futures::{stream, StreamExt}; use serde::Deserialize; +use tokio::task; +use tokio::time; use fabro_util::terminal::Styles; @@ -17,7 +19,7 @@ use fabro_model::{Catalog, Model, Provider}; use crate::generate::{self, GenerateParams}; use crate::tools::Tool; -use crate::types::{ContentPart, Message}; +use crate::types::{ContentPart, GenerateResult, Message, ReasoningEffort, StreamEvent, Usage}; pub struct ServerConnection { pub client: reqwest::Client, @@ -161,7 +163,7 @@ fn models_title() -> Vec { ] } -fn print_models_table(models: &[crate::types::Model], s: &Styles) { +fn print_models_table(models: &[Model], s: &Styles) { let use_color = s.use_color; let rows: Vec> = models.iter().map(|m| model_row(m, use_color)).collect(); let table = rows @@ -251,7 +253,7 @@ fn apply_options( Ok(params) } -fn print_usage(usage: &crate::types::Usage) { +fn print_usage(usage: &Usage) { eprintln!( "Tokens: {} input, {} output, {} total", usage.input_tokens, usage.output_tokens, usage.total_tokens @@ -278,7 +280,7 @@ pub async fn run_chat(args: ChatArgs) -> Result<()> { loop { let line = if is_tty { - let result = tokio::task::spawn_blocking(|| { + let result = task::spawn_blocking(|| { dialoguer::Input::::with_theme(&ColorfulTheme::default()) .with_prompt(">") .interact_on(&Term::stderr()) @@ -318,7 +320,7 @@ pub async fn run_chat(args: ChatArgs) -> Result<()> { let mut stream_result = generate::stream(params).await?; let mut full_text = String::new(); while let Some(event) = stream_result.next().await { - if let crate::types::StreamEvent::TextDelta { delta, .. } = event? { + if let StreamEvent::TextDelta { delta, .. } = event? { print!("{delta}"); full_text.push_str(&delta); } @@ -380,7 +382,7 @@ pub async fn run_prompt(args: PromptArgs) -> Result<()> { (false, None) => { let mut stream_result = generate::stream(params).await?; while let Some(event) = stream_result.next().await { - if let crate::types::StreamEvent::TextDelta { delta, .. } = event? { + if let StreamEvent::TextDelta { delta, .. } = event? { print!("{delta}"); } } @@ -480,22 +482,22 @@ pub async fn run_prompt_via_server(args: PromptArgs, server: &ServerConnection) } let show_usage = args.usage; - let mut output_usage: Option = None; + let mut output_usage: Option = None; parse_sse_frames(response, |event_type, data| { if event_type == "stream_event" { - if let Ok(event) = serde_json::from_str::(data) { + if let Ok(event) = serde_json::from_str::(data) { match event { - crate::types::StreamEvent::TextDelta { delta, .. } => { + StreamEvent::TextDelta { delta, .. } => { print!("{delta}"); let _ = io::stdout().flush(); } - crate::types::StreamEvent::Finish { usage, .. } => { + StreamEvent::Finish { usage, .. } => { if show_usage { output_usage = Some(usage); } } - crate::types::StreamEvent::Error { error, .. } => { + StreamEvent::Error { error, .. } => { bail!("Server error: {error}"); } _ => {} @@ -646,7 +648,7 @@ pub async fn run_chat_via_server(args: ChatArgs, server: &ServerConnection) -> R loop { let line = if is_tty { - let result = tokio::task::spawn_blocking(|| { + let result = task::spawn_blocking(|| { dialoguer::Input::::with_theme(&ColorfulTheme::default()) .with_prompt(">") .interact_on(&Term::stderr()) @@ -869,16 +871,13 @@ fn build_deep_test_params(info: &Model) -> Option { .max_tokens(1024); if info.features.reasoning { - params = params.reasoning_effort(crate::types::ReasoningEffort::High); + params = params.reasoning_effort(ReasoningEffort::High); } Some(params) } -fn validate_deep_result( - result: &crate::types::GenerateResult, - info: &Model, -) -> (cli_table::Color, String) { +fn validate_deep_result(result: &GenerateResult, info: &Model) -> (cli_table::Color, String) { // Check tool use: need at least 2 steps (tool call + follow-up) if result.steps.len() < 2 { return ( @@ -1048,7 +1047,7 @@ async fn test_one_model(info: &Model, deep: bool) -> (Color, String) { None => (Color::Yellow, "deep: skipped (no tool support)".to_string()), Some(params) => { let result = - tokio::time::timeout(Duration::from_secs(90), generate::generate(params)).await; + time::timeout(Duration::from_secs(90), generate::generate(params)).await; match result { Ok(Ok(ref gen_result)) => validate_deep_result(gen_result, info), Ok(Err(e)) => (Color::Red, format!("deep: error: {e}")), @@ -1062,8 +1061,7 @@ async fn test_one_model(info: &Model, deep: bool) -> (Color, String) { .prompt("Say OK") .max_tokens(16); - let result = - tokio::time::timeout(Duration::from_secs(30), generate::generate(params)).await; + let result = time::timeout(Duration::from_secs(30), generate::generate(params)).await; match result { Ok(Ok(_)) => (Color::Green, "ok".to_string()), Ok(Err(e)) => (Color::Red, format!("error: {e}")), @@ -1109,7 +1107,7 @@ async fn test_models( indexed.shuffle(&mut rand::thread_rng()); // Run tests concurrently, 6 at a time - let results: Vec<(usize, Color, String)> = futures::stream::iter(indexed) + let results: Vec<(usize, Color, String)> = stream::iter(indexed) .map(|(idx, info)| { let pb = pb.clone(); async move { diff --git a/lib/crates/fabro-llm/src/generate.rs b/lib/crates/fabro-llm/src/generate.rs index 44cf6eb1f..d697f9788 100644 --- a/lib/crates/fabro-llm/src/generate.rs +++ b/lib/crates/fabro-llm/src/generate.rs @@ -8,11 +8,14 @@ use crate::types::{ ResponseFormat, ResponseFormatType, RetryPolicy, StepResult, StreamEvent, TimeoutConfig, ToolCall, ToolChoice, ToolDefinition, Usage, }; -use futures::{Stream, StreamExt}; +use fabro_util::backoff::BackoffPolicy; +use futures::{future, stream, Stream, StreamExt}; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; -use tokio::sync::OnceCell; +use tokio::sync::{mpsc, OnceCell}; +use tokio::time; +use tokio_stream::wrappers::ReceiverStream; use tokio_util::sync::CancellationToken; use tracing::{debug, warn}; @@ -109,7 +112,7 @@ pub async fn generate(params: GenerateParams) -> Result Result Result Pin> + Send>> { Box::pin(self.filter_map(|result| { - futures::future::ready(match result { + future::ready(match result { Ok(StreamEvent::TextDelta { delta, .. }) => Some(Ok(delta)), Err(e) => Some(Err(e)), _ => None, @@ -661,12 +664,12 @@ async fn stream_with_tool_loop(params: GenerateParams) -> Result>(64); + let (tx, rx) = mpsc::channel::>(64); let tools = params.tools.clone(); let retry_policy = RetryPolicy { max_retries: params.max_retries, - backoff: fabro_util::backoff::BackoffPolicy { + backoff: BackoffPolicy { initial_delay: std::time::Duration::from_micros(1), jitter: false, ..Default::default() @@ -703,7 +706,7 @@ async fn stream_with_tool_loop(params: GenerateParams) -> Result Result Result Some((item, (stream, false))), - Ok(None) => None, // stream completed naturally - Err(_) => Some(( - Err(SdkError::RequestTimeout { - message: format!("Total timeout of {total_copy}s exceeded"), - source: None, - }), - (stream, true), - )), - } - }); + let timed_stream = stream::unfold((stream, false), move |(mut stream, done)| async move { + if done { + return None; + } + match time::timeout_at(deadline, stream.next()).await { + Ok(Some(item)) => Some((item, (stream, false))), + Ok(None) => None, // stream completed naturally + Err(_) => Some(( + Err(SdkError::RequestTimeout { + message: format!("Total timeout of {total_copy}s exceeded"), + source: None, + }), + (stream, true), + )), + } + }); Ok(Box::pin(timed_stream)) } else { Ok(stream) @@ -1099,7 +1098,7 @@ pub async fn stream_object( } } - futures::future::ready(Some(futures::stream::iter(events))) + future::ready(Some(stream::iter(events))) }, ); @@ -1672,7 +1671,7 @@ mod tests { .unwrap(); let events: Vec = obj_stream - .filter_map(|r| futures::future::ready(r.ok())) + .filter_map(|r| future::ready(r.ok())) .collect() .await; @@ -1710,7 +1709,7 @@ mod tests { .unwrap(); let events: Vec = obj_stream - .filter_map(|r| futures::future::ready(r.ok())) + .filter_map(|r| future::ready(r.ok())) .collect() .await; @@ -1967,7 +1966,7 @@ mod tests { let texts: Vec = result .text_stream() - .filter_map(|r| futures::future::ready(r.ok())) + .filter_map(|r| future::ready(r.ok())) .collect() .await; diff --git a/lib/crates/fabro-llm/src/providers/anthropic.rs b/lib/crates/fabro-llm/src/providers/anthropic.rs index d278009db..42694ac24 100644 --- a/lib/crates/fabro-llm/src/providers/anthropic.rs +++ b/lib/crates/fabro-llm/src/providers/anthropic.rs @@ -1,14 +1,16 @@ use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine}; +use futures::stream; -use crate::error::SdkError; -use crate::provider::{ProviderAdapter, StreamEventStream}; +use crate::error::{error_from_status_code, SdkError}; +use crate::provider::{validate_tool_choice, ProviderAdapter, StreamEventStream}; use crate::providers::common::{ - extract_system_prompt, parse_error_body, parse_rate_limit_headers, parse_retry_after, - send_and_read_response, + self as common, extract_system_prompt, parse_error_body, parse_rate_limit_headers, + parse_retry_after, send_and_read_response, }; use crate::types::{ - ContentPart, FinishReason, Message, Request, Response, ResponseFormatType, Role, StreamEvent, - ThinkingData, ToolCall, ToolChoice, ToolDefinition, Usage, + AdapterTimeout, ContentPart, FinishReason, Message, RateLimitInfo, Request, Response, + ResponseFormatType, Role, StreamEvent, ThinkingData, ToolCall, ToolChoice, ToolDefinition, + Usage, }; /// Provider adapter for the Anthropic Messages API. @@ -47,7 +49,7 @@ impl Adapter { } #[must_use] - pub fn with_timeout(self, timeout: crate::types::AdapterTimeout) -> Self { + pub fn with_timeout(self, timeout: AdapterTimeout) -> Self { Self { http: self.http.with_timeout(timeout), ..self @@ -266,8 +268,8 @@ fn content_part_to_api(part: &ContentPart) -> Option { } ContentPart::Image(img) => { if let Some(url) = &img.url { - if crate::providers::common::is_file_path(url) { - return match crate::providers::common::load_file_as_base64(url) { + if common::is_file_path(url) { + return match common::load_file_as_base64(url) { Ok((b64, mime)) => Some(serde_json::json!({ "type": "image", "source": {"type": "base64", "media_type": mime, "data": b64} @@ -286,8 +288,8 @@ fn content_part_to_api(part: &ContentPart) -> Option { } ContentPart::Document(doc) => { if let Some(url) = &doc.url { - if crate::providers::common::is_file_path(url) { - return match crate::providers::common::load_file_as_base64(url) { + if common::is_file_path(url) { + return match common::load_file_as_base64(url) { Ok((b64, mime)) => Some(serde_json::json!({ "type": "document", "source": {"type": "base64", "media_type": mime, "data": b64} @@ -669,11 +671,11 @@ struct StreamAccumulator { /// Accumulated raw JSON arguments for the current `tool_use` block. current_tool_args: String, /// Rate limit info parsed from the initial HTTP response headers. - rate_limit: Option, + rate_limit: Option, } impl StreamAccumulator { - fn new(rate_limit: Option) -> Self { + fn new(rate_limit: Option) -> Self { Self { id: String::new(), model: String::new(), @@ -974,7 +976,7 @@ struct SseReaderState { impl SseReaderState { fn new( http_resp: reqwest::Response, - rate_limit: Option, + rate_limit: Option, json_schema_mode: bool, stream_read_timeout: Option, ) -> Self { @@ -1214,7 +1216,7 @@ impl ProviderAdapter for Adapter { async fn complete(&self, request: &Request) -> Result { if let Some(tc) = &request.tool_choice { - crate::provider::validate_tool_choice(self, tc)?; + validate_tool_choice(self, tc)?; } // Non-Anthropic providers (e.g. Kimi) require stream=true even for @@ -1290,7 +1292,7 @@ impl ProviderAdapter for Adapter { async fn stream(&self, request: &Request) -> Result { if let Some(tc) = &request.tool_choice { - crate::provider::validate_tool_choice(self, tc)?; + validate_tool_choice(self, tc)?; } let (_api_request, req_builder) = build_api_request(self, request, true); @@ -1307,7 +1309,7 @@ impl ProviderAdapter for Adapter { .await .map_err(|e| SdkError::network(e.to_string(), e))?; let (msg, code, raw) = parse_error_body(&body, "type"); - return Err(crate::error::error_from_status_code( + return Err(error_from_status_code( status.as_u16(), msg, self.provider_name.clone(), @@ -1321,7 +1323,7 @@ impl ProviderAdapter for Adapter { let json_schema_mode = uses_json_schema_format(request); let stream_read_timeout = self.http.stream_read_timeout; - let stream = futures::stream::unfold( + let stream = stream::unfold( SseReaderState::new(http_resp, rate_limit, json_schema_mode, stream_read_timeout), |mut state| async move { loop { diff --git a/lib/crates/fabro-llm/src/providers/common.rs b/lib/crates/fabro-llm/src/providers/common.rs index e812dd0bf..541c68fe6 100644 --- a/lib/crates/fabro-llm/src/providers/common.rs +++ b/lib/crates/fabro-llm/src/providers/common.rs @@ -2,6 +2,8 @@ use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine}; use crate::error::{error_from_status_code, SdkError}; use crate::types::{Message, RateLimitInfo, Role}; +use reqwest::header::HeaderMap; +use tokio::time; use tracing::warn; /// Parse an error response body, extracting the message and error code. @@ -104,7 +106,7 @@ pub fn load_file_as_base64(path: &str) -> Result<(String, String), std::io::Erro /// Extract the `Retry-After` header value from an HTTP response as seconds. #[must_use] -pub fn parse_retry_after(headers: &reqwest::header::HeaderMap) -> Option { +pub fn parse_retry_after(headers: &HeaderMap) -> Option { headers .get("retry-after") .and_then(|v| v.to_str().ok()) @@ -115,15 +117,15 @@ pub fn parse_retry_after(headers: &reqwest::header::HeaderMap) -> Option { /// /// Returns `None` if no rate limit headers are present. #[must_use] -pub fn parse_rate_limit_headers(headers: &reqwest::header::HeaderMap) -> Option { - fn header_i64(headers: &reqwest::header::HeaderMap, name: &str) -> Option { +pub fn parse_rate_limit_headers(headers: &HeaderMap) -> Option { + fn header_i64(headers: &HeaderMap, name: &str) -> Option { headers .get(name) .and_then(|v| v.to_str().ok()) .and_then(|s| s.parse::().ok()) } - fn header_str(headers: &reqwest::header::HeaderMap, name: &str) -> Option { + fn header_str(headers: &HeaderMap, name: &str) -> Option { headers .get(name) .and_then(|v| v.to_str().ok()) @@ -166,7 +168,7 @@ pub async fn send_and_read_response( request: reqwest::RequestBuilder, provider: &str, error_code_field: &str, -) -> Result<(String, reqwest::header::HeaderMap), SdkError> { +) -> Result<(String, HeaderMap), SdkError> { let http_resp = request.send().await.map_err(|e| { if e.is_timeout() { warn!(provider = %provider, error = %e, "Provider request timed out"); @@ -239,7 +241,7 @@ impl LineReader { } let chunk_result = match self.stream_read_timeout { - Some(timeout) => tokio::time::timeout(timeout, self.response.chunk()).await, + Some(timeout) => time::timeout(timeout, self.response.chunk()).await, None => Ok(self.response.chunk().await), }; match chunk_result { @@ -317,7 +319,7 @@ mod tests { #[test] fn parse_rate_limit_headers_all_present() { - let mut headers = reqwest::header::HeaderMap::new(); + let mut headers = HeaderMap::new(); headers.insert("x-ratelimit-remaining-requests", "99".parse().unwrap()); headers.insert("x-ratelimit-limit-requests", "100".parse().unwrap()); headers.insert("x-ratelimit-remaining-tokens", "9000".parse().unwrap()); @@ -337,13 +339,13 @@ mod tests { #[test] fn parse_rate_limit_headers_none_present() { - let headers = reqwest::header::HeaderMap::new(); + let headers = HeaderMap::new(); assert!(parse_rate_limit_headers(&headers).is_none()); } #[test] fn parse_rate_limit_headers_partial() { - let mut headers = reqwest::header::HeaderMap::new(); + let mut headers = HeaderMap::new(); headers.insert("x-ratelimit-remaining-requests", "50".parse().unwrap()); let info = parse_rate_limit_headers(&headers).unwrap(); @@ -356,7 +358,7 @@ mod tests { #[test] fn parse_rate_limit_headers_reset_tokens_fallback() { - let mut headers = reqwest::header::HeaderMap::new(); + let mut headers = HeaderMap::new(); headers.insert("x-ratelimit-limit-tokens", "5000".parse().unwrap()); headers.insert( "x-ratelimit-reset-tokens", @@ -370,7 +372,7 @@ mod tests { #[test] fn parse_rate_limit_headers_invalid_values_ignored() { - let mut headers = reqwest::header::HeaderMap::new(); + let mut headers = HeaderMap::new(); headers.insert( "x-ratelimit-remaining-requests", "not-a-number".parse().unwrap(), @@ -499,27 +501,27 @@ mod tests { #[test] fn parse_retry_after_valid() { - let mut headers = reqwest::header::HeaderMap::new(); + let mut headers = HeaderMap::new(); headers.insert("retry-after", "2.5".parse().unwrap()); assert_eq!(parse_retry_after(&headers), Some(2.5)); } #[test] fn parse_retry_after_missing() { - let headers = reqwest::header::HeaderMap::new(); + let headers = HeaderMap::new(); assert_eq!(parse_retry_after(&headers), None); } #[test] fn parse_retry_after_invalid() { - let mut headers = reqwest::header::HeaderMap::new(); + let mut headers = HeaderMap::new(); headers.insert("retry-after", "not-a-number".parse().unwrap()); assert_eq!(parse_retry_after(&headers), None); } #[test] fn parse_retry_after_integer() { - let mut headers = reqwest::header::HeaderMap::new(); + let mut headers = HeaderMap::new(); headers.insert("retry-after", "5".parse().unwrap()); assert_eq!(parse_retry_after(&headers), Some(5.0)); } diff --git a/lib/crates/fabro-llm/src/providers/fabro_server.rs b/lib/crates/fabro-llm/src/providers/fabro_server.rs index feffba837..8e01ac144 100644 --- a/lib/crates/fabro-llm/src/providers/fabro_server.rs +++ b/lib/crates/fabro-llm/src/providers/fabro_server.rs @@ -2,6 +2,7 @@ use crate::error::{error_from_status_code, SdkError}; use crate::provider::{ProviderAdapter, StreamEventStream}; use crate::providers::common::LineReader; use crate::types::{FinishReason, Message, Request, Response, StreamEvent, Usage}; +use futures::stream; use tracing::{debug, error}; /// Provider adapter that routes LLM requests through an fabro server's @@ -161,35 +162,34 @@ impl ProviderAdapter for Adapter { let body = build_body(request, true)?; let http_resp = send_request(&self.client, &url, &body, &self.provider_name).await?; - let stream = - futures::stream::unfold(LineReader::new(http_resp, None), |mut reader| async move { - loop { - match reader.read_next_chunk("\n\n").await { - Ok(Some(block)) => { - if let Some((event_type, data)) = parse_sse_block(&block) { - if event_type == "stream_event" { - match serde_json::from_str::(&data) { - Ok(event) => return Some((Ok(event), reader)), - Err(e) => { - return Some(( - Err(SdkError::stream_error( - format!("failed to parse stream event: {e}"), - e, - )), - reader, - )); - } + let stream = stream::unfold(LineReader::new(http_resp, None), |mut reader| async move { + loop { + match reader.read_next_chunk("\n\n").await { + Ok(Some(block)) => { + if let Some((event_type, data)) = parse_sse_block(&block) { + if event_type == "stream_event" { + match serde_json::from_str::(&data) { + Ok(event) => return Some((Ok(event), reader)), + Err(e) => { + return Some(( + Err(SdkError::stream_error( + format!("failed to parse stream event: {e}"), + e, + )), + reader, + )); } } - // Skip non-stream_event SSE events } - // Empty or unparsable block — keep reading. + // Skip non-stream_event SSE events } - Ok(None) => return None, - Err(e) => return Some((Err(e), reader)), + // Empty or unparsable block — keep reading. } + Ok(None) => return None, + Err(e) => return Some((Err(e), reader)), } - }); + } + }); Ok(Box::pin(stream)) } diff --git a/lib/crates/fabro-llm/src/providers/gemini.rs b/lib/crates/fabro-llm/src/providers/gemini.rs index ad2a6f466..2fd6279db 100644 --- a/lib/crates/fabro-llm/src/providers/gemini.rs +++ b/lib/crates/fabro-llm/src/providers/gemini.rs @@ -5,14 +5,17 @@ use crate::error::{ error_from_grpc_status, error_from_status_code, ProviderErrorDetail, ProviderErrorKind, SdkError, }; -use crate::provider::{ProviderAdapter, StreamEventStream}; +use crate::provider::{validate_tool_choice, ProviderAdapter, StreamEventStream}; use crate::providers::common::{ - extract_system_prompt, parse_error_body, parse_rate_limit_headers, parse_retry_after, + self as common, extract_system_prompt, parse_error_body, parse_rate_limit_headers, + parse_retry_after, }; use crate::types::{ - ContentPart, FinishReason, Message, Request, Response, ResponseFormat, ResponseFormatType, - Role, StreamEvent, ThinkingData, ToolCall, ToolChoice, ToolDefinition, Usage, + AdapterTimeout, ContentPart, FinishReason, Message, RateLimitInfo, Request, Response, + ResponseFormat, ResponseFormatType, Role, StreamEvent, ThinkingData, ToolCall, ToolChoice, + ToolDefinition, Usage, }; +use reqwest::header::HeaderMap; const DEFAULT_BASE_URL: &str = "https://generativelanguage.googleapis.com/v1beta"; @@ -43,7 +46,7 @@ impl Adapter { } #[must_use] - pub fn with_timeout(self, timeout: crate::types::AdapterTimeout) -> Self { + pub fn with_timeout(self, timeout: AdapterTimeout) -> Self { Self { http: self.http.with_timeout(timeout), } @@ -251,8 +254,8 @@ fn translate_messages(messages: &[&Message]) -> Vec { }) }, |url| { - if crate::providers::common::is_file_path(url) { - match crate::providers::common::load_file_as_base64(url) { + if common::is_file_path(url) { + match common::load_file_as_base64(url) { Ok((b64, mime)) => Some(serde_json::json!({"inlineData": {"mimeType": mime, "data": b64}})), Err(_) => None, } @@ -273,8 +276,8 @@ fn translate_messages(messages: &[&Message]) -> Vec { }) }, |url| { - if crate::providers::common::is_file_path(url) { - match crate::providers::common::load_file_as_base64(url) { + if common::is_file_path(url) { + match common::load_file_as_base64(url) { Ok((b64, mime)) => Some(serde_json::json!({"inlineData": {"mimeType": mime, "data": b64}})), Err(_) => None, } @@ -295,8 +298,8 @@ fn translate_messages(messages: &[&Message]) -> Vec { }) }, |url| { - if crate::providers::common::is_file_path(url) { - match crate::providers::common::load_file_as_base64(url) { + if common::is_file_path(url) { + match common::load_file_as_base64(url) { Ok((b64, mime)) => Some(serde_json::json!({"inlineData": {"mimeType": mime, "data": b64}})), Err(_) => None, } @@ -505,7 +508,7 @@ fn parse_usage(metadata: Option<&UsageMetadata>) -> Usage { /// Like `send_and_read_response` but uses gRPC status code mapping when available. async fn send_gemini_response( request: reqwest::RequestBuilder, -) -> Result<(String, reqwest::header::HeaderMap), SdkError> { +) -> Result<(String, HeaderMap), SdkError> { let http_resp = request.send().await.map_err(|e| { if e.is_timeout() { SdkError::request_timeout(format!("gemini: {e}"), e) @@ -589,7 +592,7 @@ async fn send_streaming_request( fn process_sse_stream( http_resp: reqwest::Response, model: String, - rate_limit: Option, + rate_limit: Option, stream_read_timeout: Option, ) -> StreamEventStream { Box::pin(stream::unfold( @@ -698,14 +701,14 @@ struct SseStreamState { /// Whether we have emitted the `Finish` event. finished: bool, /// Rate limit info parsed from HTTP response headers. - rate_limit: Option, + rate_limit: Option, } impl SseStreamState { fn new( http_resp: reqwest::Response, model: String, - rate_limit: Option, + rate_limit: Option, stream_read_timeout: Option, ) -> Self { Self { @@ -884,7 +887,7 @@ impl ProviderAdapter for Adapter { async fn complete(&self, request: &Request) -> Result { if let Some(tc) = &request.tool_choice { - crate::provider::validate_tool_choice(self, tc)?; + validate_tool_choice(self, tc)?; } let api_body = build_api_request(request); @@ -950,7 +953,7 @@ impl ProviderAdapter for Adapter { async fn stream(&self, request: &Request) -> Result { if let Some(tc) = &request.tool_choice { - crate::provider::validate_tool_choice(self, tc)?; + validate_tool_choice(self, tc)?; } let api_body = build_api_request(request); diff --git a/lib/crates/fabro-llm/src/providers/openai.rs b/lib/crates/fabro-llm/src/providers/openai.rs index a62edab3d..9662da49e 100644 --- a/lib/crates/fabro-llm/src/providers/openai.rs +++ b/lib/crates/fabro-llm/src/providers/openai.rs @@ -1,14 +1,16 @@ use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine}; -use futures::StreamExt; +use futures::{stream, StreamExt}; -use crate::error::SdkError; -use crate::provider::{ProviderAdapter, StreamEventStream}; +use crate::error::{error_from_status_code, SdkError}; +use crate::provider::{validate_tool_choice, ProviderAdapter, StreamEventStream}; use crate::providers::common::{ - parse_error_body, parse_rate_limit_headers, parse_retry_after, send_and_read_response, + self as common, parse_error_body, parse_rate_limit_headers, parse_retry_after, + send_and_read_response, }; use crate::types::{ - ContentPart, FinishReason, Message, Request, Response, ResponseFormat, ResponseFormatType, - Role, StreamEvent, ToolCall, ToolChoice, ToolDefinition, Usage, + AdapterTimeout, ContentPart, FinishReason, Message, RateLimitInfo, Request, Response, + ResponseFormat, ResponseFormatType, Role, StreamEvent, ToolCall, ToolChoice, ToolDefinition, + Usage, }; const DEFAULT_BASE_URL: &str = "https://api.openai.com/v1"; @@ -69,7 +71,7 @@ impl Adapter { } #[must_use] - pub fn with_timeout(self, timeout: crate::types::AdapterTimeout) -> Self { + pub fn with_timeout(self, timeout: AdapterTimeout) -> Self { Self { http: self.http.with_timeout(timeout), ..self @@ -216,8 +218,8 @@ fn translate_input(messages: &[Message]) -> (Option, Vec Some(serde_json::json!({"type": "input_image", "image_url": format!("data:{mime};base64,{b64}")})), Err(_) => None, } @@ -550,7 +552,7 @@ struct SseStreamState { emitted_text_start: bool, emitted_reasoning_start: bool, raw_response: Option, - rate_limit: Option, + rate_limit: Option, } /// Parse a single SSE message block into an (`event_type`, `data`) pair. @@ -944,7 +946,7 @@ impl ProviderAdapter for Adapter { } if let Some(tc) = &request.tool_choice { - crate::provider::validate_tool_choice(self, tc)?; + validate_tool_choice(self, tc)?; } let request_body = build_request_body(request, false, false); let url = format!("{}/responses", self.http.base_url); @@ -999,7 +1001,7 @@ impl ProviderAdapter for Adapter { async fn stream(&self, request: &Request) -> Result { if let Some(tc) = &request.tool_choice { - crate::provider::validate_tool_choice(self, tc)?; + validate_tool_choice(self, tc)?; } let request_body = build_request_body(request, true, self.codex_mode); let url = format!("{}/responses", self.http.base_url); @@ -1019,7 +1021,7 @@ impl ProviderAdapter for Adapter { .await .map_err(|e| SdkError::network(e.to_string(), e))?; let (msg, code, raw) = parse_error_body(&body, "type"); - return Err(crate::error::error_from_status_code( + return Err(error_from_status_code( status.as_u16(), msg, "openai".to_string(), @@ -1051,14 +1053,14 @@ impl ProviderAdapter for Adapter { rate_limit, }; - let stream = futures::stream::unfold(state, |mut state| async move { + let stream = stream::unfold(state, |mut state| async move { let events = process_next_sse_events(&mut state).await; let items: Vec> = match events { Ok(events) if events.is_empty() => return None, Ok(events) => events.into_iter().map(Ok).collect(), Err(e) => vec![Err(e)], }; - Some((futures::stream::iter(items), state)) + Some((stream::iter(items), state)) }) .flatten(); diff --git a/lib/crates/fabro-llm/src/providers/openai_compatible.rs b/lib/crates/fabro-llm/src/providers/openai_compatible.rs index 7cb39babe..37b8c35e5 100644 --- a/lib/crates/fabro-llm/src/providers/openai_compatible.rs +++ b/lib/crates/fabro-llm/src/providers/openai_compatible.rs @@ -1,13 +1,14 @@ -use futures::StreamExt; +use futures::{stream, StreamExt}; use crate::error::{error_from_status_code, ProviderErrorDetail, ProviderErrorKind, SdkError}; -use crate::provider::{ProviderAdapter, StreamEventStream}; +use crate::provider::{validate_tool_choice, ProviderAdapter, StreamEventStream}; use crate::providers::common::{ parse_error_body, parse_rate_limit_headers, parse_retry_after, send_and_read_response, }; use crate::types::{ - ContentPart, FinishReason, Message, Request, Response, ResponseFormat, ResponseFormatType, - Role, StreamEvent, ThinkingData, ToolCall, ToolChoice, ToolDefinition, Usage, + AdapterTimeout, ContentPart, FinishReason, Message, RateLimitInfo, Request, Response, + ResponseFormat, ResponseFormatType, Role, StreamEvent, ThinkingData, ToolCall, ToolChoice, + ToolDefinition, Usage, }; /// `OpenAI`-compatible Chat Completions adapter (Section 7.10). @@ -46,7 +47,7 @@ impl Adapter { } #[must_use] - pub fn with_timeout(self, timeout: crate::types::AdapterTimeout) -> Self { + pub fn with_timeout(self, timeout: AdapterTimeout) -> Self { Self { http: self.http.with_timeout(timeout), ..self @@ -451,7 +452,7 @@ impl ProviderAdapter for Adapter { async fn complete(&self, request: &Request) -> Result { if let Some(tc) = &request.tool_choice { - crate::provider::validate_tool_choice(self, tc)?; + validate_tool_choice(self, tc)?; } let api_body = build_api_request(request, None, &self.provider_name); let url = format!("{}/chat/completions", self.http.base_url); @@ -530,7 +531,7 @@ impl ProviderAdapter for Adapter { async fn stream(&self, request: &Request) -> Result { if let Some(tc) = &request.tool_choice { - crate::provider::validate_tool_choice(self, tc)?; + validate_tool_choice(self, tc)?; } let api_body = build_api_request(request, Some(true), &self.provider_name); let url = format!("{}/chat/completions", self.http.base_url); @@ -565,7 +566,7 @@ impl ProviderAdapter for Adapter { let rate_limit = parse_rate_limit_headers(http_resp.headers()); let stream_read_timeout = self.http.stream_read_timeout; - let stream = futures::stream::unfold( + let stream = stream::unfold( StreamState::new( http_resp, provider_name, @@ -629,7 +630,7 @@ impl ProviderAdapter for Adapter { ); // Flatten batched events into individual stream events. - let flat_stream = futures::stream::unfold( + let flat_stream = stream::unfold( FlattenState { inner: Box::pin(stream), pending: Vec::new(), @@ -680,7 +681,7 @@ struct StreamState { done: bool, /// True after `finish_events()` has been called (guards against duplicates). finished: bool, - rate_limit: Option, + rate_limit: Option, } impl StreamState { @@ -688,7 +689,7 @@ impl StreamState { response: reqwest::Response, provider_name: String, model: String, - rate_limit: Option, + rate_limit: Option, stream_read_timeout: Option, ) -> Self { Self { diff --git a/lib/crates/fabro-llm/src/retry.rs b/lib/crates/fabro-llm/src/retry.rs index 09227b19c..bd063cb17 100644 --- a/lib/crates/fabro-llm/src/retry.rs +++ b/lib/crates/fabro-llm/src/retry.rs @@ -2,6 +2,7 @@ use crate::error::SdkError; use crate::types::RetryPolicy; use std::future::Future; use std::time::Duration; +use tokio::time; use tracing::warn; /// Retry a fallible async operation according to the given policy (Section 6.6). @@ -51,7 +52,7 @@ where on_retry(&err, attempt, delay); } - tokio::time::sleep(delay).await; + time::sleep(delay).await; attempt += 1; } diff --git a/lib/crates/fabro-llm/src/types.rs b/lib/crates/fabro-llm/src/types.rs index 005b1860c..0157476ee 100644 --- a/lib/crates/fabro-llm/src/types.rs +++ b/lib/crates/fabro-llm/src/types.rs @@ -1,4 +1,6 @@ use crate::error::SdkError; +use fabro_util::backoff::BackoffPolicy; +use serde::de; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; @@ -188,7 +190,7 @@ impl<'de> Deserialize<'de> for ContentPart { let kind = value .get("kind") .and_then(serde_json::Value::as_str) - .ok_or_else(|| serde::de::Error::missing_field("kind"))?; + .ok_or_else(|| de::Error::missing_field("kind"))?; let data = value .get("data") .cloned() @@ -196,31 +198,31 @@ impl<'de> Deserialize<'de> for ContentPart { match kind { "text" => serde_json::from_value(data) .map(Self::Text) - .map_err(serde::de::Error::custom), + .map_err(de::Error::custom), "image" => serde_json::from_value(data) .map(Self::Image) - .map_err(serde::de::Error::custom), + .map_err(de::Error::custom), "audio" => serde_json::from_value(data) .map(Self::Audio) - .map_err(serde::de::Error::custom), + .map_err(de::Error::custom), "document" => serde_json::from_value(data) .map(Self::Document) - .map_err(serde::de::Error::custom), + .map_err(de::Error::custom), "tool_call" => serde_json::from_value(data) .map(Self::ToolCall) - .map_err(serde::de::Error::custom), + .map_err(de::Error::custom), "tool_result" => serde_json::from_value(data) .map(Self::ToolResult) - .map_err(serde::de::Error::custom), + .map_err(de::Error::custom), "thinking" => serde_json::from_value(data) .map(Self::Thinking) - .map_err(serde::de::Error::custom), + .map_err(de::Error::custom), "redacted_thinking" => serde_json::from_value::(data) .map(|mut td| { td.redacted = true; Self::Thinking(td) }) - .map_err(serde::de::Error::custom), + .map_err(de::Error::custom), other => Ok(Self::Other { kind: other.to_string(), data, @@ -733,7 +735,7 @@ pub type OnRetryCallback = Arc, } @@ -752,7 +754,7 @@ impl Default for RetryPolicy { fn default() -> Self { Self { max_retries: 2, - backoff: fabro_util::backoff::BackoffPolicy { + backoff: BackoffPolicy { initial_delay: std::time::Duration::from_secs(1), factor: 2.0, max_delay: std::time::Duration::from_secs(60), diff --git a/lib/crates/fabro-mcp/src/client.rs b/lib/crates/fabro-mcp/src/client.rs index 33f3268fb..6ac42648a 100644 --- a/lib/crates/fabro-mcp/src/client.rs +++ b/lib/crates/fabro-mcp/src/client.rs @@ -3,13 +3,15 @@ use std::sync::Arc; use std::time::Duration; use anyhow::{anyhow, Result}; +use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use rmcp::model::{CallToolRequestParams, CallToolResult}; -use rmcp::service::{RoleClient, RunningService}; +use rmcp::service::{serve_client, RoleClient, RunningService}; use rmcp::transport::child_process::TokioChildProcess; use rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig; use rmcp::transport::StreamableHttpClientTransport; use tokio::process::Command; use tokio::sync::Mutex; +use tokio::time; use tracing::{debug, error, info, warn}; use crate::client_handler::LoggingClientHandler; @@ -65,11 +67,11 @@ impl McpClient { let mut builder = reqwest::Client::builder(); if !headers.is_empty() { - let mut header_map = reqwest::header::HeaderMap::new(); + let mut header_map = HeaderMap::new(); for (key, value) in headers { - let name = reqwest::header::HeaderName::from_bytes(key.as_bytes()) + let name = HeaderName::from_bytes(key.as_bytes()) .map_err(|e| anyhow!("invalid header name '{}': {}", key, e))?; - let val = reqwest::header::HeaderValue::from_str(value) + let val = HeaderValue::from_str(value) .map_err(|e| anyhow!("invalid header value for '{}': {}", key, e))?; header_map.insert(name, val); } @@ -122,16 +124,12 @@ impl McpClient { let handshake = async { match transport { - PendingTransport::Stdio(t) => { - rmcp::service::serve_client(handler.clone(), t).await - } - PendingTransport::Http(t) => { - rmcp::service::serve_client(handler.clone(), t).await - } + PendingTransport::Stdio(t) => serve_client(handler.clone(), t).await, + PendingTransport::Http(t) => serve_client(handler.clone(), t).await, } }; - let service = tokio::time::timeout(timeout, handshake) + let service = time::timeout(timeout, handshake) .await .map_err(|_| { error!(server = %self.server_name, timeout_secs = timeout.as_secs(), "MCP server handshake timed out"); @@ -224,7 +222,7 @@ impl McpClient { debug!(server = %self.server_name, tool = %name, "Calling MCP tool"); - let result = tokio::time::timeout(timeout, service.call_tool(params)) + let result = time::timeout(timeout, service.call_tool(params)) .await .map_err(|_| { warn!(server = %self.server_name, tool = %name, timeout_secs = timeout.as_secs(), "MCP tool call timed out"); diff --git a/lib/crates/fabro-mcp/src/client_handler.rs b/lib/crates/fabro-mcp/src/client_handler.rs index 2fb4be023..36500bab9 100644 --- a/lib/crates/fabro-mcp/src/client_handler.rs +++ b/lib/crates/fabro-mcp/src/client_handler.rs @@ -1,6 +1,7 @@ use rmcp::model::{ - CancelledNotificationParam, LoggingLevel, LoggingMessageNotificationParam, - ProgressNotificationParam, ResourceUpdatedNotificationParam, + CancelledNotificationParam, ClientCapabilities, ClientInfo, Implementation, LoggingLevel, + LoggingMessageNotificationParam, ProgressNotificationParam, ProtocolVersion, + ResourceUpdatedNotificationParam, }; use rmcp::service::NotificationContext; use rmcp::{ClientHandler, RoleClient}; @@ -11,11 +12,11 @@ use tracing::{debug, error, info, warn}; pub(crate) struct LoggingClientHandler; impl ClientHandler for LoggingClientHandler { - fn get_info(&self) -> rmcp::model::ClientInfo { - rmcp::model::ClientInfo { - protocol_version: rmcp::model::ProtocolVersion::V_2025_03_26, - capabilities: rmcp::model::ClientCapabilities::default(), - client_info: rmcp::model::Implementation { + fn get_info(&self) -> ClientInfo { + ClientInfo { + protocol_version: ProtocolVersion::V_2025_03_26, + capabilities: ClientCapabilities::default(), + client_info: Implementation { name: "fabro-mcp".into(), version: env!("CARGO_PKG_VERSION").into(), title: None, diff --git a/lib/crates/fabro-openai-oauth/src/lib.rs b/lib/crates/fabro-openai-oauth/src/lib.rs index 82a9b0a38..301de2a6f 100644 --- a/lib/crates/fabro-openai-oauth/src/lib.rs +++ b/lib/crates/fabro-openai-oauth/src/lib.rs @@ -1,7 +1,14 @@ +use axum::extract::Query; +use axum::http::StatusCode; +use axum::response::Html; +use axum::routing::get; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine; use serde::Deserialize; use sha2::{Digest, Sha256}; +use tokio::net::TcpListener; +use tokio::sync::oneshot; +use tokio::time; pub const DEFAULT_CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann"; pub const DEFAULT_ISSUER: &str = "https://auth.openai.com"; @@ -336,7 +343,7 @@ pub async fn poll_device_flow( if error == "authorization_pending" { tracing::debug!(attempt, "Device flow authorization pending"); if device.interval > 0 { - tokio::time::sleep(std::time::Duration::from_secs(device.interval)).await; + time::sleep(std::time::Duration::from_secs(device.interval)).await; } continue; } @@ -366,8 +373,8 @@ struct CallbackParams { pub async fn start_callback_server( port: u16, expected_state: String, -) -> Result<(u16, tokio::sync::oneshot::Receiver>), String> { - let listener = tokio::net::TcpListener::bind(format!("localhost:{port}")) +) -> Result<(u16, oneshot::Receiver>), String> { + let listener = TcpListener::bind(format!("localhost:{port}")) .await .map_err(|e| format!("Failed to bind callback server: {e}"))?; let actual_port = listener @@ -375,8 +382,8 @@ pub async fn start_callback_server( .map_err(|e| format!("Failed to get local address: {e}"))? .port(); - let (code_tx, code_rx) = tokio::sync::oneshot::channel::>(); - let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>(); + let (code_tx, code_rx) = oneshot::channel::>(); + let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); let code_tx = std::sync::Arc::new(std::sync::Mutex::new(Some(code_tx))); let shutdown_tx = std::sync::Arc::new(std::sync::Mutex::new(Some(shutdown_tx))); @@ -384,12 +391,12 @@ pub async fn start_callback_server( let app = axum::Router::new().route( "/auth/callback", - axum::routing::get( - move |axum::extract::Query(params): axum::extract::Query| async move { + get( + move |Query(params): Query| async move { if params.state != *expected_state { return ( - axum::http::StatusCode::BAD_REQUEST, - axum::response::Html("State mismatch".to_string()), + StatusCode::BAD_REQUEST, + Html("State mismatch".to_string()), ); } @@ -404,8 +411,8 @@ pub async fn start_callback_server( let _ = tx.send(()); } return ( - axum::http::StatusCode::BAD_REQUEST, - axum::response::Html(format!( + StatusCode::BAD_REQUEST, + Html(format!( r#" @@ -441,8 +448,8 @@ pub async fn start_callback_server( let _ = tx.send(()); } return ( - axum::http::StatusCode::BAD_REQUEST, - axum::response::Html("No authorization code received".to_string()), + StatusCode::BAD_REQUEST, + Html("No authorization code received".to_string()), ); } }; @@ -454,8 +461,8 @@ pub async fn start_callback_server( let _ = tx.send(()); } ( - axum::http::StatusCode::OK, - axum::response::Html( + StatusCode::OK, + Html( r#" diff --git a/lib/crates/fabro-retro/src/retro_agent.rs b/lib/crates/fabro-retro/src/retro_agent.rs index d5c6ed3ac..623a1c33f 100644 --- a/lib/crates/fabro-retro/src/retro_agent.rs +++ b/lib/crates/fabro-retro/src/retro_agent.rs @@ -2,6 +2,7 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::Duration; +use fabro_agent::tool_registry::RegisteredTool; use fabro_agent::{ AgentProfile, AnthropicProfile, GeminiProfile, OpenAiProfile, Sandbox, Session, SessionConfig, SessionEvent, Turn, @@ -9,9 +10,11 @@ use fabro_agent::{ use fabro_llm::client::Client; use fabro_llm::provider::Provider; use fabro_llm::types::ToolDefinition; +use fabro_util::redact::redact_jsonl_line; +use tokio::sync::broadcast::Receiver; use tokio::task::JoinHandle; -use crate::retro::RetroNarrative; +use crate::retro::{RetroNarrative, SmoothnessRating}; const RETRO_SYSTEM_PROMPT: &str = r#"You are a workflow run retrospective analyst. Your job is to analyze a completed workflow run and generate a structured retrospective. @@ -133,7 +136,7 @@ pub async fn run_retro_agent( let mut profile = build_profile(provider, model); // Register submit_retro tool - let submit_tool = fabro_agent::tool_registry::RegisteredTool { + let submit_tool = RegisteredTool { definition: ToolDefinition { name: "submit_retro".to_string(), description: "Submit the structured retrospective analysis. Call this once you have analyzed the workflow run data.".to_string(), @@ -254,7 +257,7 @@ pub async fn run_retro_agent( /// derive → apply_narrative → save path without making LLM calls. pub fn dry_run_narrative() -> RetroNarrative { RetroNarrative { - smoothness: crate::retro::SmoothnessRating::Smooth, + smoothness: SmoothnessRating::Smooth, intent: "[dry-run] No LLM analysis performed".to_string(), outcome: "[dry-run] Run completed in simulated mode".to_string(), learnings: vec![], @@ -296,15 +299,12 @@ fn write_retro_artifacts( /// Spawn a background task that reads `SessionEvent`s from the broadcast receiver /// and appends them as JSONL to the given path. -fn spawn_retro_event_writer( - mut rx: tokio::sync::broadcast::Receiver, - path: PathBuf, -) -> JoinHandle<()> { +fn spawn_retro_event_writer(mut rx: Receiver, path: PathBuf) -> JoinHandle<()> { tokio::spawn(async move { use std::io::Write; while let Ok(event) = rx.recv().await { if let Ok(line) = serde_json::to_string(&event) { - let line = fabro_util::redact::redact_jsonl_line(&line); + let line = redact_jsonl_line(&line); if let Ok(mut f) = std::fs::OpenOptions::new() .create(true) .append(true) @@ -407,7 +407,7 @@ mod tests { }); let narrative: RetroNarrative = serde_json::from_value(args).unwrap(); - assert_eq!(narrative.smoothness, crate::retro::SmoothnessRating::Smooth); + assert_eq!(narrative.smoothness, SmoothnessRating::Smooth); assert_eq!(narrative.intent, "Fix the login bug"); assert_eq!(narrative.learnings.len(), 1); assert_eq!(narrative.friction_points.len(), 1); @@ -423,10 +423,7 @@ mod tests { }); let narrative: RetroNarrative = serde_json::from_value(args).unwrap(); - assert_eq!( - narrative.smoothness, - crate::retro::SmoothnessRating::Effortless - ); + assert_eq!(narrative.smoothness, SmoothnessRating::Effortless); assert!(narrative.learnings.is_empty()); assert!(narrative.friction_points.is_empty()); assert!(narrative.open_items.is_empty()); diff --git a/lib/crates/fabro-sandbox/src/daytona/mod.rs b/lib/crates/fabro-sandbox/src/daytona/mod.rs index 3b9d16a88..7dbe3c1bb 100644 --- a/lib/crates/fabro-sandbox/src/daytona/mod.rs +++ b/lib/crates/fabro-sandbox/src/daytona/mod.rs @@ -2,14 +2,20 @@ use std::collections::HashMap; use std::path::Path; use std::time::Instant; +use crate::sandbox::resolve_path; use crate::shell_quote; use crate::{ format_lines_numbered, DirEntry, ExecResult, GrepOptions, Sandbox, SandboxEvent, SandboxEventCallback, }; use async_trait::async_trait; +use daytona_sdk::api_types::SignedPortPreviewUrl; use fabro_github::GitHubAppCredentials; use rand::Rng; +use tokio::fs; +use tokio::sync::OnceCell; +use tokio::time; +use tokio_util::sync::CancellationToken; const WORKING_DIRECTORY: &str = "/home/daytona/workspace"; const DEFAULT_SNAPSHOT: &str = "daytona-medium"; @@ -24,11 +30,11 @@ pub struct DaytonaSandbox { config: DaytonaConfig, client: daytona_sdk::Client, github_app: Option, - sandbox: tokio::sync::OnceCell, - rg_available: tokio::sync::OnceCell, + sandbox: OnceCell, + rg_available: OnceCell, event_callback: Option, /// HTTPS origin URL stored after clone so we can refresh push credentials later. - origin_url: tokio::sync::OnceCell, + origin_url: OnceCell, run_id: Option, /// Explicit branch to clone. When set, overrides the branch detected by /// `detect_repo_info` — avoids cloning a local-only worktree branch @@ -51,10 +57,10 @@ impl DaytonaSandbox { config, client, github_app, - sandbox: tokio::sync::OnceCell::new(), - rg_available: tokio::sync::OnceCell::const_new(), + sandbox: OnceCell::new(), + rg_available: OnceCell::const_new(), event_callback: None, - origin_url: tokio::sync::OnceCell::new(), + origin_url: OnceCell::new(), run_id, clone_branch, }) @@ -72,16 +78,16 @@ impl DaytonaSandbox { .get(sandbox_name) .await .map_err(|e| format!("Failed to reconnect to Daytona sandbox '{sandbox_name}': {e}"))?; - let sandbox_cell = tokio::sync::OnceCell::new(); + let sandbox_cell = OnceCell::new(); let _ = sandbox_cell.set(sdk_sandbox); Ok(Self { config: DaytonaConfig::default(), client, github_app: None, sandbox: sandbox_cell, - rg_available: tokio::sync::OnceCell::const_new(), + rg_available: OnceCell::const_new(), event_callback: None, - origin_url: tokio::sync::OnceCell::new(), + origin_url: OnceCell::new(), run_id: None, clone_branch: None, }) @@ -126,7 +132,7 @@ impl DaytonaSandbox { &self, port: u16, expires_in_seconds: Option, - ) -> Result { + ) -> Result { let sandbox = self.sandbox()?; sandbox .get_signed_preview_url(port as i32, expires_in_seconds) @@ -142,7 +148,7 @@ impl DaytonaSandbox { } fn resolve_path(&self, path: &str) -> String { - crate::sandbox::resolve_path(path, WORKING_DIRECTORY) + resolve_path(path, WORKING_DIRECTORY) } /// Get the sandbox, returning an error if not yet initialized. @@ -257,7 +263,7 @@ impl DaytonaSandbox { let deadline = Instant::now() + std::time::Duration::from_secs(600); while Instant::now() < deadline { - tokio::time::sleep(delay).await; + time::sleep(delay).await; let dto = self .client .snapshot @@ -332,11 +338,11 @@ impl Sandbox for DaytonaSandbox { .map_err(|e| format!("Failed to download file {resolved}: {e}"))?; if let Some(parent) = local_path.parent() { - tokio::fs::create_dir_all(parent) + fs::create_dir_all(parent) .await .map_err(|e| format!("Failed to create parent dirs: {e}"))?; } - tokio::fs::write(local_path, &bytes) + fs::write(local_path, &bytes) .await .map_err(|e| format!("Failed to write {}: {e}", local_path.display()))?; @@ -363,7 +369,7 @@ impl Sandbox for DaytonaSandbox { } } - let bytes = tokio::fs::read(local_path) + let bytes = fs::read(local_path) .await .map_err(|e| format!("Failed to read {}: {e}", local_path.display()))?; @@ -914,7 +920,7 @@ impl Sandbox for DaytonaSandbox { timeout_ms: u64, working_dir: Option<&str>, env_vars: Option<&HashMap>, - cancel_token: Option, + cancel_token: Option, ) -> Result { tracing::info!(command, timeout_ms, "exec_command: entered"); @@ -976,7 +982,7 @@ impl Sandbox for DaytonaSandbox { ); res.map_err(|e| format!("Failed to execute command: {e}"))? } - () = tokio::time::sleep(timeout_duration) => { + () = time::sleep(timeout_duration) => { tracing::info!( elapsed_ms = start.elapsed().as_millis() as u64, timeout_ms, @@ -1126,8 +1132,9 @@ impl Sandbox for DaytonaSandbox { /// Uses base64 encoding (matching the TypeScript/Python/Ruby Daytona SDKs) /// to avoid shell escaping issues with quotes and special characters. fn wrap_bash_command(command: &str) -> String { + use base64::engine::general_purpose::STANDARD; use base64::Engine; - let encoded = base64::engine::general_purpose::STANDARD.encode(command); + let encoded = STANDARD.encode(command); format!("sh -c \"echo '{encoded}' | base64 -d | sh\"") } diff --git a/lib/crates/fabro-sandbox/src/docker.rs b/lib/crates/fabro-sandbox/src/docker.rs index 0a35bb1b1..d23674f51 100644 --- a/lib/crates/fabro-sandbox/src/docker.rs +++ b/lib/crates/fabro-sandbox/src/docker.rs @@ -3,16 +3,21 @@ use crate::{ SandboxEventCallback, }; use async_trait::async_trait; +use bollard::container::LogOutput; use bollard::container::{ Config, CreateContainerOptions, RemoveContainerOptions, StartContainerOptions, StopContainerOptions, UploadToContainerOptions, }; use bollard::exec::{CreateExecOptions, StartExecResults}; use bollard::image::CreateImageOptions; +use bollard::models::HostConfig; use bollard::Docker; use futures::StreamExt; use std::collections::HashMap; use std::time::Instant; +use tokio::fs; +use tokio::sync::OnceCell; +use tokio::time; use tokio_util::sync::CancellationToken; /// Configuration for a Docker-based sandbox. @@ -60,10 +65,10 @@ impl Default for DockerSandboxConfig { pub struct DockerSandbox { docker: Docker, config: DockerSandboxConfig, - container_id: tokio::sync::OnceCell, + container_id: OnceCell, cached_platform: std::sync::OnceLock, cached_os_version: std::sync::OnceLock, - rg_available: tokio::sync::OnceCell, + rg_available: OnceCell, event_callback: Option, } @@ -78,10 +83,10 @@ impl DockerSandbox { Ok(Self { docker, config, - container_id: tokio::sync::OnceCell::new(), + container_id: OnceCell::new(), cached_platform: std::sync::OnceLock::new(), cached_os_version: std::sync::OnceLock::new(), - rg_available: tokio::sync::OnceCell::const_new(), + rg_available: OnceCell::const_new(), event_callback: None, }) } @@ -166,10 +171,10 @@ impl DockerSandbox { if let StartExecResults::Attached { mut output, .. } = start_result { while let Some(chunk) = output.next().await { match chunk { - Ok(bollard::container::LogOutput::StdOut { message }) => { + Ok(LogOutput::StdOut { message }) => { stdout.push_str(&String::from_utf8_lossy(&message)); } - Ok(bollard::container::LogOutput::StdErr { message }) => { + Ok(LogOutput::StdErr { message }) => { stderr.push_str(&String::from_utf8_lossy(&message)); } Ok(_) => {} @@ -228,7 +233,7 @@ impl DockerSandbox { duration_ms, }) } - () = tokio::time::sleep(timeout_duration) => { + () = time::sleep(timeout_duration) => { let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX); Ok(ExecResult { stdout: String::new(), @@ -294,11 +299,11 @@ impl Sandbox for DockerSandbox { let host_path = self.container_to_host_path(remote_path)?; if let Some(parent) = local_path.parent() { - tokio::fs::create_dir_all(parent) + fs::create_dir_all(parent) .await .map_err(|e| format!("Failed to create parent dirs: {e}"))?; } - tokio::fs::copy(&host_path, local_path).await.map_err(|e| { + fs::copy(&host_path, local_path).await.map_err(|e| { format!( "Failed to copy {} to {}: {e}", host_path.display(), @@ -316,11 +321,11 @@ impl Sandbox for DockerSandbox { let host_path = self.container_to_host_path(remote_path)?; if let Some(parent) = host_path.parent() { - tokio::fs::create_dir_all(parent) + fs::create_dir_all(parent) .await .map_err(|e| format!("Failed to create parent dirs: {e}"))?; } - tokio::fs::copy(local_path, &host_path).await.map_err(|e| { + fs::copy(local_path, &host_path).await.map_err(|e| { format!( "Failed to copy {} to {}: {e}", local_path.display(), @@ -363,7 +368,7 @@ impl Sandbox for DockerSandbox { binds.push(extra.clone()); } - let host_config = bollard::models::HostConfig { + let host_config = HostConfig { binds: Some(binds), network_mode: self.config.network_mode.clone(), memory: self.config.memory_limit, diff --git a/lib/crates/fabro-sandbox/src/local.rs b/lib/crates/fabro-sandbox/src/local.rs index e185c413f..a03f88f61 100644 --- a/lib/crates/fabro-sandbox/src/local.rs +++ b/lib/crates/fabro-sandbox/src/local.rs @@ -5,8 +5,10 @@ use crate::{ use async_trait::async_trait; use std::path::{Path, PathBuf}; use std::time::Instant; +use tokio::fs; use tokio::io::AsyncReadExt; -use tokio::process::Command; +use tokio::process::{Child, Command}; +use tokio::time; use tokio_util::sync::CancellationToken; pub struct LocalSandbox { @@ -80,7 +82,7 @@ impl Sandbox for LocalSandbox { limit: Option, ) -> Result { let full_path = self.resolve_path(path); - let content = tokio::fs::read_to_string(&full_path) + let content = fs::read_to_string(&full_path) .await .map_err(|e| format!("Failed to read {}: {e}", full_path.display()))?; @@ -90,18 +92,18 @@ impl Sandbox for LocalSandbox { async fn write_file(&self, path: &str, content: &str) -> Result<(), String> { let full_path = self.resolve_path(path); if let Some(parent) = full_path.parent() { - tokio::fs::create_dir_all(parent) + fs::create_dir_all(parent) .await .map_err(|e| format!("Failed to create parent dirs: {e}"))?; } - tokio::fs::write(&full_path, content) + fs::write(&full_path, content) .await .map_err(|e| format!("Failed to write {}: {e}", full_path.display())) } async fn delete_file(&self, path: &str) -> Result<(), String> { let full_path = self.resolve_path(path); - tokio::fs::remove_file(&full_path) + fs::remove_file(&full_path) .await .map_err(|e| format!("Failed to delete {}: {e}", full_path.display())) } @@ -237,7 +239,7 @@ impl Sandbox for LocalSandbox { let status = status_result.map_err(|e| format!("Failed to wait for process: {e}"))?; (false, status.code().unwrap_or(-1)) } - () = tokio::time::sleep(timeout_duration) => { + () = time::sleep(timeout_duration) => { sigterm_then_kill(&mut child).await; (true, -1) } @@ -366,11 +368,11 @@ impl Sandbox for LocalSandbox { ) -> Result<(), String> { let full_path = self.resolve_path(remote_path); if let Some(parent) = local_path.parent() { - tokio::fs::create_dir_all(parent) + fs::create_dir_all(parent) .await .map_err(|e| format!("Failed to create parent dirs: {e}"))?; } - tokio::fs::copy(&full_path, local_path).await.map_err(|e| { + fs::copy(&full_path, local_path).await.map_err(|e| { format!( "Failed to copy {} to {}: {e}", full_path.display(), @@ -387,11 +389,11 @@ impl Sandbox for LocalSandbox { ) -> Result<(), String> { let full_path = self.resolve_path(remote_path); if let Some(parent) = full_path.parent() { - tokio::fs::create_dir_all(parent) + fs::create_dir_all(parent) .await .map_err(|e| format!("Failed to create parent dirs: {e}"))?; } - tokio::fs::copy(local_path, &full_path).await.map_err(|e| { + fs::copy(local_path, &full_path).await.map_err(|e| { format!( "Failed to copy {} to {}: {e}", local_path.display(), @@ -406,7 +408,7 @@ impl Sandbox for LocalSandbox { provider: "local".into(), }); let start = Instant::now(); - let result = tokio::fs::create_dir_all(&self.working_directory) + let result = fs::create_dir_all(&self.working_directory) .await .map_err(|e| format!("Failed to create working directory: {e}")); let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX); @@ -477,13 +479,13 @@ impl Sandbox for LocalSandbox { } /// Send SIGTERM to the process group, wait 2s for graceful shutdown, then SIGKILL. -async fn sigterm_then_kill(child: &mut tokio::process::Child) { +async fn sigterm_then_kill(child: &mut Child) { #[cfg(unix)] if let Some(pid) = child.id() { unsafe { libc::kill(-(pid as i32), libc::SIGTERM); } - if tokio::time::timeout(std::time::Duration::from_secs(2), child.wait()) + if time::timeout(std::time::Duration::from_secs(2), child.wait()) .await .is_err() { diff --git a/lib/crates/fabro-sandbox/src/reconnect.rs b/lib/crates/fabro-sandbox/src/reconnect.rs index 8eae4a7a9..885777842 100644 --- a/lib/crates/fabro-sandbox/src/reconnect.rs +++ b/lib/crates/fabro-sandbox/src/reconnect.rs @@ -1,8 +1,16 @@ use std::path::PathBuf; +#[allow(unused_imports)] use anyhow::{bail, Context, Result}; +#[cfg(feature = "daytona")] +use crate::daytona::DaytonaSandbox; +#[cfg(feature = "docker")] +use crate::docker::{DockerSandbox, DockerSandboxConfig}; +use crate::local::LocalSandbox; use crate::sandbox_record::SandboxRecord; +#[cfg(feature = "ssh")] +use crate::ssh::{OpensshRunner, SshConfig, SshSandbox}; /// Reconnect to a sandbox from a saved record. /// @@ -11,7 +19,7 @@ pub async fn reconnect(record: &SandboxRecord) -> Result match record.provider.as_str() { #[cfg(feature = "local")] "local" => { - let sandbox = crate::local::LocalSandbox::new(PathBuf::from(&record.working_directory)); + let sandbox = LocalSandbox::new(PathBuf::from(&record.working_directory)); Ok(Box::new(sandbox)) } #[cfg(feature = "docker")] @@ -25,12 +33,12 @@ pub async fn reconnect(record: &SandboxRecord) -> Result .as_deref() .unwrap_or("/workspace"); - let config = crate::docker::DockerSandboxConfig { + let config = DockerSandboxConfig { host_working_directory: host_dir.to_string(), container_mount_point: mount_point.to_string(), - ..crate::docker::DockerSandboxConfig::default() + ..DockerSandboxConfig::default() }; - let sandbox = crate::docker::DockerSandbox::new(config) + let sandbox = DockerSandbox::new(config) .map_err(|e| anyhow::anyhow!("Failed to create Docker sandbox: {e}"))?; Ok(Box::new(sandbox)) } @@ -41,7 +49,7 @@ pub async fn reconnect(record: &SandboxRecord) -> Result .as_deref() .context("Daytona sandbox record missing identifier (sandbox name)")?; - let sandbox = crate::daytona::DaytonaSandbox::reconnect(name) + let sandbox = DaytonaSandbox::reconnect(name) .await .map_err(|e| anyhow::anyhow!("{e}"))?; Ok(Box::new(sandbox)) @@ -69,19 +77,19 @@ pub async fn reconnect(record: &SandboxRecord) -> Result .as_deref() .context("SSH sandbox record missing data_host (destination)")?; - let ssh = crate::ssh::OpensshRunner::connect(destination, None) + let ssh = OpensshRunner::connect(destination, None) .await .map_err(|e| { anyhow::anyhow!("Failed to connect to SSH sandbox '{destination}': {e}") })?; - let config = crate::ssh::SshConfig { + let config = SshConfig { destination: destination.to_string(), working_directory: record.working_directory.clone(), config_file: None, preview_url_base: None, }; - let sandbox = crate::ssh::SshSandbox::from_existing(Box::new(ssh), config); + let sandbox = SshSandbox::from_existing(Box::new(ssh), config); Ok(Box::new(sandbox)) } other => bail!("Unknown sandbox provider: {other}"), diff --git a/lib/crates/fabro-sandbox/src/ssh/mod.rs b/lib/crates/fabro-sandbox/src/ssh/mod.rs index 1e03081eb..96b5170b2 100644 --- a/lib/crates/fabro-sandbox/src/ssh/mod.rs +++ b/lib/crates/fabro-sandbox/src/ssh/mod.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use std::path::Path; use std::time::Instant; +use crate::sandbox::resolve_path; use crate::shell_quote; use crate::ssh_common; use crate::{ @@ -11,6 +12,8 @@ use crate::{ SandboxEventCallback, }; use async_trait::async_trait; +use tokio::fs; +use tokio::sync::OnceCell; use tokio_util::sync::CancellationToken; pub use crate::ssh_common::{GitCloneParams, SshOutput, SshRunner}; @@ -25,14 +28,14 @@ const PROVIDER: &str = "ssh"; /// Unlike ExeSandbox, there is no VM lifecycle management -- the host /// must already be running and accessible via SSH. pub struct SshSandbox { - ssh: tokio::sync::OnceCell>, + ssh: OnceCell>, config: SshConfig, clone_params: Option, run_id: Option, github_app: Option, - rg_available: tokio::sync::OnceCell, + rg_available: OnceCell, event_callback: Option, - origin_url: tokio::sync::OnceCell, + origin_url: OnceCell, } impl SshSandbox { @@ -44,21 +47,21 @@ impl SshSandbox { github_app: Option, ) -> Self { Self { - ssh: tokio::sync::OnceCell::new(), + ssh: OnceCell::new(), config, clone_params, run_id, github_app, - rg_available: tokio::sync::OnceCell::const_new(), + rg_available: OnceCell::const_new(), event_callback: None, - origin_url: tokio::sync::OnceCell::new(), + origin_url: OnceCell::new(), } } /// Create an `SshSandbox` from a pre-connected SSH runner. /// Used for reconnection (e.g. `fabro cp`) when the host is already known. pub fn from_existing(ssh: Box, config: SshConfig) -> Self { - let ssh_cell = tokio::sync::OnceCell::new(); + let ssh_cell = OnceCell::new(); let _ = ssh_cell.set(ssh); Self { ssh: ssh_cell, @@ -66,9 +69,9 @@ impl SshSandbox { clone_params: None, run_id: None, github_app: None, - rg_available: tokio::sync::OnceCell::const_new(), + rg_available: OnceCell::const_new(), event_callback: None, - origin_url: tokio::sync::OnceCell::new(), + origin_url: OnceCell::new(), } } @@ -111,7 +114,7 @@ impl SshSandbox { } fn resolve_path(&self, path: &str) -> String { - crate::sandbox::resolve_path(path, &self.config.working_directory) + resolve_path(path, &self.config.working_directory) } } @@ -484,11 +487,11 @@ impl Sandbox for SshSandbox { let bytes = ssh.download_file(&resolved).await?; if let Some(parent) = local_path.parent() { - tokio::fs::create_dir_all(parent) + fs::create_dir_all(parent) .await .map_err(|e| format!("Failed to create parent dirs: {e}"))?; } - tokio::fs::write(local_path, &bytes) + fs::write(local_path, &bytes) .await .map_err(|e| format!("Failed to write {}: {e}", local_path.display()))?; @@ -503,7 +506,7 @@ impl Sandbox for SshSandbox { let ssh = self.ssh()?; let resolved = self.resolve_path(remote_path); - let bytes = tokio::fs::read(local_path) + let bytes = fs::read(local_path) .await .map_err(|e| format!("Failed to read {}: {e}", local_path.display()))?; diff --git a/lib/crates/fabro-sandbox/src/ssh/openssh_runner.rs b/lib/crates/fabro-sandbox/src/ssh/openssh_runner.rs index beb460a99..0c823fb5f 100644 --- a/lib/crates/fabro-sandbox/src/ssh/openssh_runner.rs +++ b/lib/crates/fabro-sandbox/src/ssh/openssh_runner.rs @@ -1,5 +1,7 @@ use async_trait::async_trait; +use base64::engine::general_purpose::STANDARD; use openssh::{KnownHosts, SessionBuilder}; +use tokio::time; use super::{SshOutput, SshRunner}; use crate::shell_quote; @@ -52,7 +54,7 @@ impl SshRunner for OpensshRunner { let mut child = self.session.shell(command); let fut = child.output(); - match tokio::time::timeout(timeout, fut).await { + match time::timeout(timeout, fut).await { Ok(Ok(output)) => { let exit_code = output.status.code().unwrap_or(-1); Ok(SshOutput { @@ -68,7 +70,7 @@ impl SshRunner for OpensshRunner { async fn upload_file(&self, path: &str, content: &[u8]) -> Result<(), String> { use base64::Engine; - let encoded = base64::engine::general_purpose::STANDARD.encode(content); + let encoded = STANDARD.encode(content); let cmd = format!("echo '{}' | base64 -d > {}", encoded, shell_quote(path),); let output = self .session diff --git a/lib/crates/fabro-sandbox/src/ssh_common.rs b/lib/crates/fabro-sandbox/src/ssh_common.rs index ae552433a..8e71770c0 100644 --- a/lib/crates/fabro-sandbox/src/ssh_common.rs +++ b/lib/crates/fabro-sandbox/src/ssh_common.rs @@ -3,7 +3,9 @@ use std::time::Instant; use async_trait::async_trait; +use base64::engine::general_purpose::STANDARD; use base64::Engine; +use tokio::sync::OnceCell; use crate::{shell_quote, SandboxEvent}; @@ -41,7 +43,7 @@ pub struct GitCloneParams { /// Wrap a shell command in base64 encoding to avoid escaping issues. pub(crate) fn wrap_bash_command(command: &str) -> String { - let encoded = base64::engine::general_purpose::STANDARD.encode(command); + let encoded = STANDARD.encode(command); format!("echo '{encoded}' | base64 -d | sh") } @@ -68,7 +70,7 @@ pub(crate) async fn clone_repo( working_dir: &str, params: &GitCloneParams, github_app: Option<&fabro_github::GitHubAppCredentials>, - origin_url: &tokio::sync::OnceCell, + origin_url: &OnceCell, emit: &(dyn Fn(SandboxEvent) + Send + Sync), ) -> Result<(), String> { emit(SandboxEvent::GitCloneStarted { diff --git a/lib/crates/fabro-slack/src/connection.rs b/lib/crates/fabro-slack/src/connection.rs index 0190a83e7..01ebd0286 100644 --- a/lib/crates/fabro-slack/src/connection.rs +++ b/lib/crates/fabro-slack/src/connection.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use fabro_interview::WebInterviewer; use futures_util::{SinkExt, StreamExt}; +use tokio::time::sleep; use tokio_tungstenite::tungstenite::Message; use tracing::{debug, error, info, warn}; @@ -164,7 +165,7 @@ pub async fn run( } Err(e) => { error!("Failed to open Socket Mode connection: {e}"); - tokio::time::sleep(backoff).await; + sleep(backoff).await; backoff = (backoff * 2).min(max_backoff); continue; } @@ -177,7 +178,7 @@ pub async fn run( } Err(e) => { error!("Event loop error: {e}, reconnecting..."); - tokio::time::sleep(backoff).await; + sleep(backoff).await; backoff = (backoff * 2).min(max_backoff); } } diff --git a/lib/crates/fabro-store/src/slate/mod.rs b/lib/crates/fabro-store/src/slate/mod.rs index ab5786aa0..9bb10e528 100644 --- a/lib/crates/fabro-store/src/slate/mod.rs +++ b/lib/crates/fabro-store/src/slate/mod.rs @@ -9,6 +9,7 @@ use chrono::{DateTime, Utc}; use futures::TryStreamExt; use object_store::path::Path; use object_store::ObjectStore; +use slatedb::config::DbReaderOptions; use slatedb::DbReader; use tokio::sync::Mutex; @@ -53,7 +54,7 @@ impl SlateStore { db_prefix.to_string(), self.object_store.clone(), None, - slatedb::config::DbReaderOptions::default(), + DbReaderOptions::default(), ) .await?) } diff --git a/lib/crates/fabro-store/src/slate/run_store.rs b/lib/crates/fabro-store/src/slate/run_store.rs index 646eb0a93..e95ddeced 100644 --- a/lib/crates/fabro-store/src/slate/run_store.rs +++ b/lib/crates/fabro-store/src/slate/run_store.rs @@ -11,6 +11,7 @@ use serde::de::DeserializeOwned; use serde::Serialize; use slatedb::{CloseReason, DbRead, ErrorKind}; use tokio::sync::{mpsc, Mutex}; +use tokio::time; use tokio_stream::wrappers::UnboundedReceiverStream; use crate::keys; @@ -341,7 +342,7 @@ impl RunStore for SlateRunStore { match list_events_from(&db, next_seq).await { Ok(events) => { if events.is_empty() { - tokio::time::sleep(Duration::from_millis(100)).await; + time::sleep(Duration::from_millis(100)).await; continue; } for event in events { diff --git a/lib/crates/fabro-telemetry/src/panic.rs b/lib/crates/fabro-telemetry/src/panic.rs index 065c51f44..2d64c43ce 100644 --- a/lib/crates/fabro-telemetry/src/panic.rs +++ b/lib/crates/fabro-telemetry/src/panic.rs @@ -1,7 +1,9 @@ use std::panic::PanicHookInfo; use std::path::Path; -use sentry::protocol::{Event, Exception, Mechanism}; +use crate::spawn::spawn_fabro_subcommand; +use sentry::integrations::backtrace; +use sentry::protocol::{Context, Event, Exception, Mechanism, OsContext, Values}; use crate::TelemetryLevel; @@ -24,7 +26,7 @@ pub fn build_event(message: &str) -> Event<'static> { let mut event = Event::new(); event.level = sentry::Level::Fatal; - let stacktrace = sentry::integrations::backtrace::current_stacktrace(); + let stacktrace = backtrace::current_stacktrace(); let exception = Exception { ty: "panic".into(), @@ -38,14 +40,14 @@ pub fn build_event(message: &str) -> Event<'static> { ..Default::default() }; - event.exception = sentry::protocol::Values { + event.exception = Values { values: vec![exception], }; // Add OS context. event.contexts.insert( "os".to_string(), - sentry::protocol::Context::Os(Box::new(sentry::protocol::OsContext { + Context::Os(Box::new(OsContext { name: Some(std::env::consts::OS.to_string()), ..Default::default() })), @@ -102,7 +104,7 @@ fn spawn_panic_sender(event: Event<'static>) { }; let filename = format!("fabro-panic-{}.json", event.event_id); - crate::spawn::spawn_fabro_subcommand("__send_panic", &filename, &json); + spawn_fabro_subcommand("__send_panic", &filename, &json); } /// Send a serialized Sentry panic event. Called by the `__send_panic` subcommand. diff --git a/lib/crates/fabro-telemetry/src/sender.rs b/lib/crates/fabro-telemetry/src/sender.rs index 5015711c7..457b2af80 100644 --- a/lib/crates/fabro-telemetry/src/sender.rs +++ b/lib/crates/fabro-telemetry/src/sender.rs @@ -5,6 +5,8 @@ use base64::Engine; use uuid::Uuid; use crate::event::Track; +use crate::spawn::spawn_fabro_subcommand; +use reqwest::blocking::Client as BlockingClient; const SEGMENT_BASE_URL: &str = match option_env!("SEGMENT_BASE_URL") { Some(url) => url, @@ -42,7 +44,7 @@ fn spawn_sender(tracks: &[Track]) { let jsonl = lines.join("\n"); let filename = format!("fabro-events-{}.jsonl", Uuid::new_v4()); - crate::spawn::spawn_fabro_subcommand("__send_analytics", &filename, jsonl.as_bytes()); + spawn_fabro_subcommand("__send_analytics", &filename, jsonl.as_bytes()); } /// Parse JSONL content into a Segment batch payload. @@ -99,7 +101,7 @@ pub fn upload_blocking(tracks: &[Track]) -> anyhow::Result<()> { let auth = STANDARD.encode(format!("{write_key}:")); - let resp = reqwest::blocking::Client::new() + let resp = BlockingClient::new() .post(format!("{SEGMENT_BASE_URL}/v1/batch")) .header("Authorization", format!("Basic {auth}")) .json(&payload) diff --git a/lib/crates/fabro-workflows/src/asset_snapshot.rs b/lib/crates/fabro-workflows/src/asset_snapshot.rs index fe620620c..9a8c95fb6 100644 --- a/lib/crates/fabro-workflows/src/asset_snapshot.rs +++ b/lib/crates/fabro-workflows/src/asset_snapshot.rs @@ -1,6 +1,7 @@ use fabro_agent::Sandbox; use serde::{Deserialize, Serialize}; use std::path::Path; +use tokio::fs; use tracing::{debug, warn}; /// A file discovered by the find command. @@ -308,9 +309,9 @@ pub async fn collect_assets( if let Ok(json) = serde_json::to_string_pretty(&summary) { let manifest_path = stage_dir.join("manifest.json"); if let Some(parent) = manifest_path.parent() { - let _ = tokio::fs::create_dir_all(parent).await; + let _ = fs::create_dir_all(parent).await; } - let _ = tokio::fs::write(&manifest_path, json).await; + let _ = fs::write(&manifest_path, json).await; } } diff --git a/lib/crates/fabro-workflows/src/devcontainer_bridge.rs b/lib/crates/fabro-workflows/src/devcontainer_bridge.rs index 2e574e705..334a276a6 100644 --- a/lib/crates/fabro-workflows/src/devcontainer_bridge.rs +++ b/lib/crates/fabro-workflows/src/devcontainer_bridge.rs @@ -5,7 +5,9 @@ use sha2::{Digest, Sha256}; use fabro_devcontainer::DevcontainerConfig; use crate::event::{EventEmitter, WorkflowRunEvent}; +use fabro_agent::sandbox::Sandbox; use fabro_sandbox::daytona::{DaytonaSnapshotConfig, DockerfileSource}; +use futures::future::try_join_all; /// Compute a deterministic snapshot name from Dockerfile content. pub fn snapshot_name_for_dockerfile(dockerfile: &str) -> String { @@ -29,7 +31,7 @@ pub fn devcontainer_to_snapshot_config(dc: &DevcontainerConfig) -> DaytonaSnapsh /// /// Follows the same pattern as setup commands in `run.rs`. pub async fn run_devcontainer_lifecycle( - sandbox: &dyn fabro_agent::sandbox::Sandbox, + sandbox: &dyn Sandbox, emitter: &EventEmitter, phase: &str, commands: &[fabro_devcontainer::Command], @@ -122,7 +124,7 @@ pub async fn run_devcontainer_lifecycle( } }) .collect(); - futures::future::try_join_all(futs).await?; + try_join_all(futs).await?; } } } @@ -136,7 +138,7 @@ pub async fn run_devcontainer_lifecycle( } async fn run_single_lifecycle_command( - sandbox: &dyn fabro_agent::sandbox::Sandbox, + sandbox: &dyn Sandbox, emitter: &EventEmitter, phase: &str, command: &str, diff --git a/lib/crates/fabro-workflows/src/error.rs b/lib/crates/fabro-workflows/src/error.rs index de3ab97ac..bce5e246d 100644 --- a/lib/crates/fabro-workflows/src/error.rs +++ b/lib/crates/fabro-workflows/src/error.rs @@ -1,3 +1,4 @@ +use fabro_graphviz::error::GraphvizError; use fabro_llm::error::{ProviderErrorKind, SdkError}; use fabro_validate::Diagnostic; use serde::{Deserialize, Serialize}; @@ -6,6 +7,8 @@ use thiserror::Error; pub use fabro_types::failure_signature::FailureSignature; pub use fabro_types::outcome::FailureCategory; +use crate::outcome::{FailureDetail, Outcome, StageStatus}; + /// Classify an `SdkError` into a `FailureCategory` based on its structure. #[must_use] pub fn classify_sdk_error(err: &SdkError) -> FailureCategory { @@ -299,16 +302,16 @@ impl FabroError { } /// Build a fail `Outcome` with structured `FailureDetail`. - pub fn to_fail_outcome(&self) -> crate::outcome::Outcome { - let failure = crate::outcome::FailureDetail { + pub fn to_fail_outcome(&self) -> Outcome { + let failure = FailureDetail { message: self.to_string(), category: self.failure_category(), signature: self.failure_signature_hint(), }; - crate::outcome::Outcome { - status: crate::outcome::StageStatus::Fail, + Outcome { + status: StageStatus::Fail, failure: Some(failure), - ..crate::outcome::Outcome::success() + ..Outcome::success() } } } @@ -325,11 +328,11 @@ impl From for FabroError { } } -impl From for FabroError { - fn from(e: fabro_graphviz::error::GraphvizError) -> Self { +impl From for FabroError { + fn from(e: GraphvizError) -> Self { match e { - fabro_graphviz::error::GraphvizError::Parse(msg) => Self::Parse(msg), - fabro_graphviz::error::GraphvizError::Stylesheet(msg) => Self::Stylesheet(msg), + GraphvizError::Parse(msg) => Self::Parse(msg), + GraphvizError::Stylesheet(msg) => Self::Stylesheet(msg), } } } diff --git a/lib/crates/fabro-workflows/src/event.rs b/lib/crates/fabro-workflows/src/event.rs index c03f1a5af..6d4d37157 100644 --- a/lib/crates/fabro-workflows/src/event.rs +++ b/lib/crates/fabro-workflows/src/event.rs @@ -7,8 +7,11 @@ use anyhow::{Context, Result}; use chrono::{SecondsFormat, Utc}; use serde::{Deserialize, Serialize}; -use crate::outcome::StageUsage; +use crate::error::FabroError; +use crate::outcome::{FailureDetail, StageUsage}; use fabro_agent::{AgentEvent, SandboxEvent, WorktreeEvent, WorktreeEventCallback}; +use fabro_llm::types::Usage as LlmUsage; +use fabro_util::redact::redact_jsonl_line; #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] @@ -45,10 +48,10 @@ pub enum WorkflowRunEvent { #[serde(default, skip_serializing_if = "Option::is_none")] final_git_commit_sha: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - usage: Option, + usage: Option, }, WorkflowRunFailed { - error: crate::error::FabroError, + error: FabroError, duration_ms: u64, #[serde(default, skip_serializing_if = "Option::is_none")] git_commit_sha: Option, @@ -78,7 +81,7 @@ pub enum WorkflowRunEvent { suggested_next_ids: Vec, usage: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - failure: Option, + failure: Option, notes: Option, files_touched: Vec, attempt: usize, @@ -88,7 +91,7 @@ pub enum WorkflowRunEvent { node_id: String, name: String, index: usize, - failure: crate::outcome::FailureDetail, + failure: FailureDetail, will_retry: bool, }, StageRetrying { @@ -820,7 +823,7 @@ pub fn build_event_envelope(event: &WorkflowRunEvent, run_id: &str) -> serde_jso pub fn append_progress_event(run_dir: &Path, run_id: &str, event: &WorkflowRunEvent) -> Result<()> { let envelope = build_event_envelope(event, run_id); let line = serde_json::to_string(&envelope)?; - let line = fabro_util::redact::redact_jsonl_line(&line); + let line = redact_jsonl_line(&line); let mut file = std::fs::OpenOptions::new() .create(true) .append(true) @@ -834,7 +837,7 @@ pub fn append_progress_event(run_dir: &Path, run_id: &str, event: &WorkflowRunEv writeln!(file, "{line}")?; let pretty = serde_json::to_string_pretty(&envelope)?; - let pretty = fabro_util::redact::redact_jsonl_line(&pretty); + let pretty = redact_jsonl_line(&pretty); std::fs::write(run_dir.join("live.json"), pretty) .with_context(|| format!("Failed to write {}", run_dir.join("live.json").display()))?; diff --git a/lib/crates/fabro-workflows/src/git.rs b/lib/crates/fabro-workflows/src/git.rs index c684d0ea5..51a2791de 100644 --- a/lib/crates/fabro-workflows/src/git.rs +++ b/lib/crates/fabro-workflows/src/git.rs @@ -6,7 +6,9 @@ use fabro_git_storage::gitobj::Store; use git2::{Repository, Signature}; use crate::error::{FabroError, Result}; -use crate::records::Checkpoint; +use crate::records::{Checkpoint, RunRecord, StartRecord}; +use tokio::task::{spawn_blocking, JoinError}; +use tokio::time::timeout; /// Branch prefix for workflow run branches (e.g. `fabro/run/{run_id}`). pub const RUN_BRANCH_PREFIX: &str = "fabro/run/"; @@ -231,9 +233,9 @@ pub fn push_run_branches( /// Error from [`blocking_push_with_timeout`]. pub enum BlockingPushError { /// The git push itself failed. - Push(crate::error::FabroError), + Push(FabroError), /// The spawned blocking task panicked. - Panicked(tokio::task::JoinError), + Panicked(JoinError), /// The push did not complete within the timeout. TimedOut, } @@ -256,9 +258,9 @@ pub async fn blocking_push_with_timeout( where F: FnOnce() -> Result<()> + Send + 'static, { - match tokio::time::timeout( + match timeout( std::time::Duration::from_secs(timeout_secs), - tokio::task::spawn_blocking(f), + spawn_blocking(f), ) .await { @@ -508,13 +510,10 @@ impl MetadataStore { } /// Read the run record from the metadata branch. Returns `None` if not found. - pub fn read_run_record( - repo_path: &Path, - run_id: &str, - ) -> Result> { + pub fn read_run_record(repo_path: &Path, run_id: &str) -> Result> { match Self::read_file(repo_path, run_id, "run.json")? { Some(bytes) => { - let record: crate::records::RunRecord = serde_json::from_slice(&bytes) + let record: RunRecord = serde_json::from_slice(&bytes) .map_err(|e| git_error(format!("run record deserialize failed: {e}")))?; Ok(Some(record)) } @@ -523,13 +522,10 @@ impl MetadataStore { } /// Read the start record from the metadata branch. Returns `None` if not found. - pub fn read_start_record( - repo_path: &Path, - run_id: &str, - ) -> Result> { + pub fn read_start_record(repo_path: &Path, run_id: &str) -> Result> { match Self::read_file(repo_path, run_id, "start.json")? { Some(bytes) => { - let record: crate::records::StartRecord = serde_json::from_slice(&bytes) + let record: StartRecord = serde_json::from_slice(&bytes) .map_err(|e| git_error(format!("start record deserialize failed: {e}")))?; Ok(Some(record)) } diff --git a/lib/crates/fabro-workflows/src/handler/agent.rs b/lib/crates/fabro-workflows/src/handler/agent.rs index 98dd15f56..25f01c618 100644 --- a/lib/crates/fabro-workflows/src/handler/agent.rs +++ b/lib/crates/fabro-workflows/src/handler/agent.rs @@ -9,8 +9,13 @@ use crate::context::keys; use crate::context::{Context, WorkflowContext}; use crate::error::FabroError; use crate::event::EventEmitter; -use crate::outcome::{Outcome, OutcomeExt, StageUsage}; +use crate::outcome::{ + FailureCategory, FailureDetail, Outcome, OutcomeExt, StageStatus, StageUsage, +}; +use crate::run_dir::{node_dir, visit_from_context}; +use crate::vars::expand_vars; use fabro_graphviz::graph::{Graph, Node}; +use tokio::fs; use super::{EngineServices, Handler}; @@ -75,7 +80,7 @@ impl AgentHandler { /// `$gaol` at runtime. pub(crate) fn expand_variables(text: &str, graph: &Graph) -> Result { let vars = HashMap::from([("goal".to_string(), graph.goal().to_string())]); - crate::vars::expand_vars(text, &vars).map_err(|e| FabroError::Validation(e.to_string())) + expand_vars(text, &vars).map_err(|e| FabroError::Validation(e.to_string())) } /// Status fields that indicate a JSON object contains routing directives. @@ -164,14 +169,12 @@ pub(crate) fn extract_status_fields(text: &str, outcome: &mut Outcome) -> bool { } if let Some(status_str) = obj.get("outcome").and_then(|v| v.as_str()) { - if let Ok(status) = status_str.parse::() { + if let Ok(status) = status_str.parse::() { outcome.status = status; - if outcome.status == crate::outcome::StageStatus::Fail { + if outcome.status == StageStatus::Fail { if let Some(reason) = obj.get("failure_reason").and_then(|v| v.as_str()) { - outcome.failure = Some(crate::outcome::FailureDetail::new( - reason, - crate::outcome::FailureCategory::Deterministic, - )); + outcome.failure = + Some(FailureDetail::new(reason, FailureCategory::Deterministic)); } } } @@ -249,10 +252,10 @@ impl Handler for AgentHandler { }; // 2. Write prompt to logs - let visit = crate::run_dir::visit_from_context(context); - let stage_dir = crate::run_dir::node_dir(run_dir, &node.id, visit); - tokio::fs::create_dir_all(&stage_dir).await?; - tokio::fs::write(stage_dir.join("prompt.md"), &prompt).await?; + let visit = visit_from_context(context); + let stage_dir = node_dir(run_dir, &node.id, visit); + fs::create_dir_all(&stage_dir).await?; + fs::write(stage_dir.join("prompt.md"), &prompt).await?; // 3. Call LLM backend (agent loop) let thread_id = context.thread_id(); @@ -285,7 +288,7 @@ impl Handler for AgentHandler { Ok(CodergenResult::Full(outcome)) => { let status_json = serde_json::to_string_pretty(&outcome) .unwrap_or_else(|_| "{}".to_string()); - tokio::fs::write(stage_dir.join("status.json"), &status_json).await?; + fs::write(stage_dir.join("status.json"), &status_json).await?; return Ok(outcome); } Ok(CodergenResult::Text { @@ -311,7 +314,7 @@ impl Handler for AgentHandler { }; // 4. Write response to logs - tokio::fs::write(stage_dir.join("response.md"), &response_text).await?; + fs::write(stage_dir.join("response.md"), &response_text).await?; // 7. Build and write status let mut outcome = Outcome::success(); @@ -364,7 +367,7 @@ impl Handler for AgentHandler { let status_json = serde_json::to_string_pretty(&outcome).unwrap_or_else(|_| "{}".to_string()); - tokio::fs::write(stage_dir.join("status.json"), &status_json).await?; + fs::write(stage_dir.join("status.json"), &status_json).await?; Ok(outcome) } diff --git a/lib/crates/fabro-workflows/src/handler/command.rs b/lib/crates/fabro-workflows/src/handler/command.rs index 38e0dd14d..19d3c4ba5 100644 --- a/lib/crates/fabro-workflows/src/handler/command.rs +++ b/lib/crates/fabro-workflows/src/handler/command.rs @@ -6,7 +6,9 @@ use crate::context::keys; use crate::context::Context; use crate::error::FabroError; use crate::outcome::{Outcome, OutcomeExt}; +use crate::run_dir::{node_dir, visit_from_context}; use fabro_graphviz::graph::{Graph, Node}; +use tokio::fs; use super::{EngineServices, Handler}; @@ -84,16 +86,16 @@ impl Handler for CommandHandler { ))); } - let visit = crate::run_dir::visit_from_context(context); - let stage_dir = crate::run_dir::node_dir(run_dir, &node.id, visit); - tokio::fs::create_dir_all(&stage_dir).await?; + let visit = visit_from_context(context); + let stage_dir = node_dir(run_dir, &node.id, visit); + fs::create_dir_all(&stage_dir).await?; let invocation = serde_json::json!({ "command": script, "language": language, "timeout_ms": timeout_ms(node), }); - tokio::fs::write( + fs::write( stage_dir.join("script_invocation.json"), serde_json::to_string_pretty(&invocation).unwrap(), ) @@ -118,15 +120,15 @@ impl Handler for CommandHandler { .await .map_err(|e| FabroError::handler(format!("Failed to spawn script: {e}")))?; - tokio::fs::write(stage_dir.join("stdout.log"), &result.stdout).await?; - tokio::fs::write(stage_dir.join("stderr.log"), &result.stderr).await?; + fs::write(stage_dir.join("stdout.log"), &result.stdout).await?; + fs::write(stage_dir.join("stderr.log"), &result.stderr).await?; let timing = serde_json::json!({ "duration_ms": result.duration_ms, "exit_code": if result.timed_out { serde_json::Value::Null } else { serde_json::json!(result.exit_code) }, "timed_out": result.timed_out, }); - tokio::fs::write( + fs::write( stage_dir.join("script_timing.json"), serde_json::to_string_pretty(&timing).unwrap(), ) diff --git a/lib/crates/fabro-workflows/src/handler/fan_in.rs b/lib/crates/fabro-workflows/src/handler/fan_in.rs index 9ee1521ef..79cfc9a66 100644 --- a/lib/crates/fabro-workflows/src/handler/fan_in.rs +++ b/lib/crates/fabro-workflows/src/handler/fan_in.rs @@ -9,7 +9,10 @@ use crate::context::Context; use crate::error::FabroError; use crate::event::EventEmitter; use crate::outcome::{Outcome, OutcomeExt}; +use crate::run_dir::{node_dir, visit_from_context}; +use crate::sandbox_git::git_merge_ff_only; use fabro_graphviz::graph::{Graph, Node}; +use tokio::fs; use super::agent::{CodergenBackend, CodergenResult}; use super::{EngineServices, Handler}; @@ -116,7 +119,7 @@ impl Handler for FanInHandler { }; if let (Some(ref sha), Some(_)) = (&best_head_sha, services.git_state()) { - crate::sandbox_git::git_merge_ff_only(&*services.sandbox, sha).await; + git_merge_ff_only(&*services.sandbox, sha).await; } let mut outcome = Outcome::success(); @@ -231,10 +234,10 @@ async fn llm_evaluate( ); // Write prompt to logs - let visit = crate::run_dir::visit_from_context(context); - let stage_dir = crate::run_dir::node_dir(run_dir, node_id, visit); - tokio::fs::create_dir_all(&stage_dir).await?; - tokio::fs::write(stage_dir.join("prompt.md"), &full_prompt).await?; + let visit = visit_from_context(context); + let stage_dir = node_dir(run_dir, node_id, visit); + fs::create_dir_all(&stage_dir).await?; + fs::write(stage_dir.join("prompt.md"), &full_prompt).await?; // Build a synthetic node for the backend call let eval_node = Node::new("fan_in_eval"); @@ -264,7 +267,7 @@ async fn llm_evaluate( .unwrap_or_else(|| "unknown".to_string()); let response_text = serde_json::to_string_pretty(&outcome).unwrap_or_else(|_| "{}".to_string()); - tokio::fs::write(stage_dir.join("response.md"), &response_text).await?; + fs::write(stage_dir.join("response.md"), &response_text).await?; Ok(Candidate { id: best_id, status: outcome.status.to_string(), @@ -273,7 +276,7 @@ async fn llm_evaluate( } Ok(CodergenResult::Text { text, .. }) => { // Write response to logs - tokio::fs::write(stage_dir.join("response.md"), &text).await?; + fs::write(stage_dir.join("response.md"), &text).await?; // The LLM responded with text; try to find a matching candidate ID let text = text.trim().to_string(); diff --git a/lib/crates/fabro-workflows/src/handler/llm/api.rs b/lib/crates/fabro-workflows/src/handler/llm/api.rs index a66e4c5aa..0328f511a 100644 --- a/lib/crates/fabro-workflows/src/handler/llm/api.rs +++ b/lib/crates/fabro-workflows/src/handler/llm/api.rs @@ -9,13 +9,18 @@ use fabro_agent::{ SessionConfig, Turn, }; use fabro_llm::client::Client; +use fabro_llm::types::{Message, Request, Usage}; +use fabro_mcp::config::McpServerConfig; use fabro_model::FallbackTarget; use fabro_model::Provider; +use tokio::fs; +use tokio::sync::Mutex as TokioMutex; use super::super::agent::{CodergenBackend, CodergenResult}; +use crate::context::keys::Fidelity; use crate::context::{Context, WorkflowContext}; use crate::error::FabroError; -use crate::event::WorkflowRunEvent; +use crate::event::{EventEmitter, WorkflowRunEvent}; use crate::outcome::compute_stage_cost; use crate::outcome::StageUsage; use fabro_graphviz::graph::Node; @@ -85,7 +90,7 @@ fn track_file_event(event: &AgentEvent, state: &mut FileTracking) { fn spawn_event_forwarder( session: &Session, node_id: String, - emitter: Arc, + emitter: Arc, file_tracking: Arc>, ) { let mut rx = session.subscribe(); @@ -129,7 +134,7 @@ pub struct AgentApiBackend { fallback_chain: Vec, sessions: Mutex>, env: HashMap, - mcp_servers: Vec, + mcp_servers: Vec, } impl AgentApiBackend { @@ -152,7 +157,7 @@ impl AgentApiBackend { } #[must_use] - pub fn with_mcp_servers(mut self, servers: Vec) -> Self { + pub fn with_mcp_servers(mut self, servers: Vec) -> Self { self.mcp_servers = servers; self } @@ -187,7 +192,7 @@ impl AgentApiBackend { sandbox: &Arc, env: &HashMap, tool_hooks: Option>, - mcp_servers: Vec, + mcp_servers: Vec, ) -> Result { let client = Client::from_env() .await @@ -204,7 +209,7 @@ impl AgentApiBackend { ..SessionConfig::default() }; - let manager = Arc::new(tokio::sync::Mutex::new(SubAgentManager::new( + let manager = Arc::new(TokioMutex::new(SubAgentManager::new( config.max_subagent_depth, ))); let manager_for_callback = manager.clone(); @@ -291,11 +296,11 @@ impl CodergenBackend for AgentApiBackend { let mut messages = Vec::new(); if let Some(sys) = system_prompt { - messages.push(fabro_llm::types::Message::system(sys)); + messages.push(Message::system(sys)); } - messages.push(fabro_llm::types::Message::user(prompt)); + messages.push(Message::user(prompt)); - let request = fabro_llm::types::Request { + let request = Request { model: model.to_string(), messages, provider, @@ -312,9 +317,9 @@ impl CodergenBackend for AgentApiBackend { provider_options: None, }; - let _ = tokio::fs::create_dir_all(stage_dir).await; + let _ = fs::create_dir_all(stage_dir).await; if let Ok(json) = serde_json::to_string_pretty(&request) { - let _ = tokio::fs::write(stage_dir.join("api_request.json"), json).await; + let _ = fs::write(stage_dir.join("api_request.json"), json).await; } // Build per-request fallback chain: if the node overrides the provider, @@ -366,7 +371,7 @@ impl CodergenBackend for AgentApiBackend { .and_then(|m| m.limits.max_output) }); - let fallback_request = fabro_llm::types::Request { + let fallback_request = Request { model: target.model.clone(), provider: Some(target.provider.clone()), max_tokens, @@ -394,7 +399,7 @@ impl CodergenBackend for AgentApiBackend { }; if let Ok(json) = serde_json::to_string_pretty(&response) { - let _ = tokio::fs::write(stage_dir.join("api_response.json"), json).await; + let _ = fs::write(stage_dir.join("api_response.json"), json).await; } let provider_used = serde_json::json!({ @@ -403,7 +408,7 @@ impl CodergenBackend for AgentApiBackend { "model": &actual_model, }); if let Ok(json) = serde_json::to_string_pretty(&provider_used) { - let _ = tokio::fs::write(stage_dir.join("provider_used.json"), json).await; + let _ = fs::write(stage_dir.join("provider_used.json"), json).await; } let mut stage_usage = StageUsage { @@ -432,7 +437,7 @@ impl CodergenBackend for AgentApiBackend { prompt: &str, context: &Context, thread_id: Option<&str>, - emitter: &Arc, + emitter: &Arc, stage_dir: &std::path::Path, sandbox: &Arc, tool_hooks: Option>, @@ -444,7 +449,7 @@ impl CodergenBackend for AgentApiBackend { .unwrap_or(self.provider); let fidelity = context.fidelity(); - let reuse_key = if fidelity == crate::context::keys::Fidelity::Full { + let reuse_key = if fidelity == Fidelity::Full { thread_id.map(String::from) } else { None @@ -493,7 +498,7 @@ impl CodergenBackend for AgentApiBackend { ); // Emit Prompt event before processing - emitter.emit(&crate::event::WorkflowRunEvent::Prompt { + emitter.emit(&WorkflowRunEvent::Prompt { stage: node.id.clone(), text: prompt.to_string(), }); @@ -600,7 +605,7 @@ impl CodergenBackend for AgentApiBackend { result?; // Aggregate token usage only from new turns (prevents double-counting on reuse). - let mut total_usage = fabro_llm::types::Usage::default(); + let mut total_usage = Usage::default(); for turn in &session.history().turns()[turns_before..] { if let Turn::Assistant { usage, .. } = turn { total_usage = total_usage + *usage.clone(); @@ -865,7 +870,7 @@ mod tests { #[test] fn build_profile_can_register_subagent_tools() { let mut profile = build_profile("claude-opus-4-6", Provider::Anthropic); - let manager = Arc::new(tokio::sync::Mutex::new(SubAgentManager::new(1))); + let manager = Arc::new(TokioMutex::new(SubAgentManager::new(1))); let factory: SessionFactory = Arc::new(|| { panic!("factory should not be called in this test"); }); diff --git a/lib/crates/fabro-workflows/src/handler/llm/cli.rs b/lib/crates/fabro-workflows/src/handler/llm/cli.rs index 8c4089acc..bc9322333 100644 --- a/lib/crates/fabro-workflows/src/handler/llm/cli.rs +++ b/lib/crates/fabro-workflows/src/handler/llm/cli.rs @@ -6,6 +6,8 @@ use async_trait::async_trait; use fabro_agent::sandbox::ExecResult; use fabro_agent::Sandbox; use fabro_model::Provider; +use tokio::fs; +use tokio::time::sleep; use super::super::agent::{CodergenBackend, CodergenResult}; use crate::context::Context; @@ -492,7 +494,7 @@ impl CodergenBackend for AgentCliBackend { let command = cli_command_for_provider(provider, model, &prompt_path); - let _ = tokio::fs::create_dir_all(stage_dir).await; + let _ = fs::create_dir_all(stage_dir).await; let provider_used = serde_json::json!({ "mode": "cli", "provider": provider.as_str(), @@ -500,7 +502,7 @@ impl CodergenBackend for AgentCliBackend { "command": &command, }); if let Ok(json) = serde_json::to_string_pretty(&provider_used) { - let _ = tokio::fs::write(stage_dir.join("provider_used.json"), json).await; + let _ = fs::write(stage_dir.join("provider_used.json"), json).await; } // Forward provider API key and custom env vars so the CLI tool can authenticate. @@ -587,7 +589,7 @@ impl CodergenBackend for AgentCliBackend { format!("[ -f {exit_code_path} ] && cat {exit_code_path} || echo running"); let poll_interval = self.poll_interval; let exit_code: i32 = loop { - tokio::time::sleep(poll_interval).await; + sleep(poll_interval).await; emitter.touch(); // keep the stall watchdog alive while polling let poll_result = sandbox .exec_command(&poll_command, 30_000, None, None, None) @@ -605,7 +607,7 @@ impl CodergenBackend for AgentCliBackend { .await { if !r.stdout.is_empty() { - let _ = tokio::fs::write(stage_dir.join(local), &r.stdout).await; + let _ = fs::write(stage_dir.join(local), &r.stdout).await; } } } @@ -645,12 +647,12 @@ impl CodergenBackend for AgentCliBackend { "stderr_len": result.stderr.len(), "duration_ms": result.duration_ms, })) { - let _ = tokio::fs::write(stage_dir.join("cli_result_meta.json"), json).await; + let _ = fs::write(stage_dir.join("cli_result_meta.json"), json).await; } if result.exit_code != 0 { - let _ = tokio::fs::write(stage_dir.join("cli_stdout.log"), &result.stdout).await; - let _ = tokio::fs::write(stage_dir.join("cli_stderr.log"), &result.stderr).await; + let _ = fs::write(stage_dir.join("cli_stdout.log"), &result.stdout).await; + let _ = fs::write(stage_dir.join("cli_stderr.log"), &result.stderr).await; let tail = |s: &str, n: usize| -> String { s.chars() diff --git a/lib/crates/fabro-workflows/src/handler/manager_loop.rs b/lib/crates/fabro-workflows/src/handler/manager_loop.rs index 51fcba086..ae1c7c8a9 100644 --- a/lib/crates/fabro-workflows/src/handler/manager_loop.rs +++ b/lib/crates/fabro-workflows/src/handler/manager_loop.rs @@ -15,8 +15,10 @@ use crate::operations::{validate, ValidateInput, WorkflowInput}; use crate::outcome::{Outcome, OutcomeExt, StageStatus}; use crate::pipeline; use crate::pipeline::types::Initialized; +use crate::run_dir::visit_from_context; use crate::run_options::RunOptions; -use fabro_graphviz::graph::{Graph, Node}; +use fabro_graphviz::graph::{AttrValue, Graph, Node}; +use tokio::time::{sleep, timeout}; use super::{EngineServices, Handler}; @@ -116,7 +118,7 @@ impl Handler for SubWorkflowHandler { let poll_interval = node .attrs .get("manager.poll_interval") - .and_then(fabro_graphviz::graph::AttrValue::as_duration) + .and_then(AttrValue::as_duration) .unwrap_or_else(|| { let raw = node .attrs @@ -129,7 +131,7 @@ impl Handler for SubWorkflowHandler { let max_cycles = node .attrs .get("manager.max_cycles") - .and_then(fabro_graphviz::graph::AttrValue::as_i64) + .and_then(AttrValue::as_i64) .unwrap_or(1000); let max_cycles = u64::try_from(max_cycles).unwrap_or(1000).max(1); @@ -150,7 +152,7 @@ impl Handler for SubWorkflowHandler { }; // Build child RunOptions - let visit = crate::run_dir::visit_from_context(context) as u64; + let visit = visit_from_context(context) as u64; let child_logs = run_dir.join(format!("nodes/{}_{visit}/child", node.id)); let _ = std::fs::create_dir_all(&child_logs); @@ -255,14 +257,14 @@ impl Handler for SubWorkflowHandler { return Ok(outcome); } - _ = tokio::time::sleep(poll_interval) => { + _ = sleep(poll_interval) => { // Check stop condition if !stop_condition.is_empty() { let dummy_outcome = Outcome::success(); if evaluate_condition(stop_condition, &dummy_outcome, context) { child_cancel.store(true, Ordering::Relaxed); // Give child a moment to wind down - let _ = tokio::time::timeout( + let _ = timeout( Duration::from_millis(100), &mut child_handle, ).await; @@ -279,7 +281,7 @@ impl Handler for SubWorkflowHandler { // Max cycles exceeded — cancel child child_cancel.store(true, Ordering::Relaxed); - let _ = tokio::time::timeout(Duration::from_millis(100), &mut child_handle).await; + let _ = timeout(Duration::from_millis(100), &mut child_handle).await; Ok(Outcome::fail_classify(format!( "Max cycles ({max_cycles}) exceeded for manager loop node: {}", diff --git a/lib/crates/fabro-workflows/src/handler/parallel.rs b/lib/crates/fabro-workflows/src/handler/parallel.rs index b1581a42a..523675229 100644 --- a/lib/crates/fabro-workflows/src/handler/parallel.rs +++ b/lib/crates/fabro-workflows/src/handler/parallel.rs @@ -10,11 +10,15 @@ use crate::context::keys; use crate::context::{Context, WorkflowContext}; use crate::error::FabroError; use crate::event::WorkflowRunEvent; +use crate::git::sanitize_ref_component; use crate::hook_context::set_hook_node; use crate::millis_u64; -use crate::outcome::{Outcome, OutcomeExt, StageStatus}; -use fabro_graphviz::graph::{Graph, Node}; +use crate::outcome::{FailureCategory, FailureDetail, Outcome, OutcomeExt, StageStatus}; +use crate::run_dir::{node_dir, visit_from_context}; +use crate::sandbox_git::{git_checkpoint, git_merge_ff_only, git_remove_worktree, GIT_REMOTE}; +use fabro_graphviz::graph::{AttrValue, Graph, Node}; use fabro_hooks::{HookContext, HookEvent}; +use tokio::fs; use super::{EngineServices, Handler}; @@ -153,7 +157,7 @@ impl Handler for ParallelHandler { let max_parallel = node .attrs .get("max_parallel") - .and_then(fabro_graphviz::graph::AttrValue::as_i64) + .and_then(AttrValue::as_i64) .unwrap_or(4); let max_parallel = usize::try_from(max_parallel).unwrap_or(4).max(1); @@ -162,7 +166,7 @@ impl Handler for ParallelHandler { // --- Git isolation: checkpoint "parallel base" before fan-out --- let base_sha: Option = if let Some(ref gs) = git_state { - let result = crate::sandbox_git::git_checkpoint( + let result = git_checkpoint( &*services.sandbox, &gs.run_id, &node.id, @@ -205,13 +209,13 @@ impl Handler for ParallelHandler { (&git_state, &base_sha) { let branch_key = &target_id; - let visit = crate::run_dir::visit_from_context(&branch_context); + let visit = visit_from_context(&branch_context); let branch_name = format!( "fabro/run/parallel/{}/{}/pass{}/{}", gs.run_id, - crate::git::sanitize_ref_component(&node.id), + sanitize_ref_component(&node.id), visit, - crate::git::sanitize_ref_component(branch_key), + sanitize_ref_component(branch_key), ); // Compute worktree path (each sandbox type knows its own path scheme) @@ -327,7 +331,7 @@ impl Handler for ParallelHandler { let nid = &setup.target_id; let status_str = outcome.status.to_string(); // Use exec_command to commit and capture HEAD in the branch worktree - let git_r = crate::sandbox_git::GIT_REMOTE; + let git_r = GIT_REMOTE; let add_cmd = format!("{git_r} add -A"); let add_result = setup .sandbox @@ -414,7 +418,7 @@ impl Handler for ParallelHandler { for result in &results { if let Some(ref wt_path) = result.worktree_path { let wt_str = wt_path.to_string_lossy().into_owned(); - crate::sandbox_git::git_remove_worktree(&*services.sandbox, &wt_str).await; + git_remove_worktree(&*services.sandbox, &wt_str).await; services .emitter .emit(&WorkflowRunEvent::GitWorktreeRemove { path: wt_str }); @@ -431,7 +435,7 @@ impl Handler for ParallelHandler { successful.sort_by(|a, b| a.id.cmp(&b.id)); if let Some(winner) = successful.first() { let sha = winner.head_sha.as_ref().unwrap(); - crate::sandbox_git::git_merge_ff_only(&*services.sandbox, sha).await; + git_merge_ff_only(&*services.sandbox, sha).await; } } @@ -463,11 +467,11 @@ impl Handler for ParallelHandler { context.set(keys::PARALLEL_RESULTS, serde_json::json!(results_json)); context.set(keys::PARALLEL_BRANCH_COUNT, serde_json::json!(total)); - let visit = crate::run_dir::visit_from_context(context); - let node_dir = crate::run_dir::node_dir(run_dir, &node.id, visit); - let _ = tokio::fs::create_dir_all(&node_dir).await; + let visit = visit_from_context(context); + let node_dir = node_dir(run_dir, &node.id, visit); + let _ = fs::create_dir_all(&node_dir).await; if let Ok(json) = serde_json::to_string_pretty(&results_json) { - let _ = tokio::fs::write(node_dir.join("parallel_results.json"), json).await; + let _ = fs::write(node_dir.join("parallel_results.json"), json).await; } services.emitter.emit(&WorkflowRunEvent::ParallelCompleted { @@ -514,9 +518,9 @@ impl Handler for ParallelHandler { "Parallel node dispatched {total} branches ({success_count} succeeded, {fail_count} failed)" )), failure: if is_fail { - Some(crate::outcome::FailureDetail::new( + Some(FailureDetail::new( format!("Join policy not satisfied: {success_count}/{total} succeeded"), - crate::outcome::FailureCategory::Deterministic, + FailureCategory::Deterministic, )) } else { None diff --git a/lib/crates/fabro-workflows/src/handler/prompt.rs b/lib/crates/fabro-workflows/src/handler/prompt.rs index 7b2082b01..40c2da3a8 100644 --- a/lib/crates/fabro-workflows/src/handler/prompt.rs +++ b/lib/crates/fabro-workflows/src/handler/prompt.rs @@ -8,7 +8,9 @@ use crate::context::keys; use crate::context::{Context, WorkflowContext}; use crate::error::FabroError; use crate::outcome::Outcome; +use crate::run_dir::{node_dir, visit_from_context}; use fabro_graphviz::graph::{Graph, Node}; +use tokio::fs; use super::agent::{ expand_variables, extract_status_fields, truncate, CodergenBackend, CodergenResult, @@ -86,10 +88,10 @@ impl Handler for PromptHandler { }; // 2. Write prompt to logs - let visit = crate::run_dir::visit_from_context(context); - let stage_dir = crate::run_dir::node_dir(run_dir, &node.id, visit); - tokio::fs::create_dir_all(&stage_dir).await?; - tokio::fs::write(stage_dir.join("prompt.md"), &prompt).await?; + let visit = visit_from_context(context); + let stage_dir = node_dir(run_dir, &node.id, visit); + fs::create_dir_all(&stage_dir).await?; + fs::write(stage_dir.join("prompt.md"), &prompt).await?; // 3. Call LLM backend (one_shot) let (response_text, stage_usage, backend_files_touched) = @@ -101,7 +103,7 @@ impl Handler for PromptHandler { Ok(CodergenResult::Full(outcome)) => { let status_json = serde_json::to_string_pretty(&outcome) .unwrap_or_else(|_| "{}".to_string()); - tokio::fs::write(stage_dir.join("status.json"), &status_json).await?; + fs::write(stage_dir.join("status.json"), &status_json).await?; return Ok(outcome); } Ok(CodergenResult::Text { @@ -126,7 +128,7 @@ impl Handler for PromptHandler { }; // 4. Write response to logs - tokio::fs::write(stage_dir.join("response.md"), &response_text).await?; + fs::write(stage_dir.join("response.md"), &response_text).await?; // 5. Build and write status let mut outcome = Outcome::success(); @@ -149,7 +151,7 @@ impl Handler for PromptHandler { let status_json = serde_json::to_string_pretty(&outcome).unwrap_or_else(|_| "{}".to_string()); - tokio::fs::write(stage_dir.join("status.json"), &status_json).await?; + fs::write(stage_dir.join("status.json"), &status_json).await?; Ok(outcome) } diff --git a/lib/crates/fabro-workflows/src/handler/wait.rs b/lib/crates/fabro-workflows/src/handler/wait.rs index 17c822e7c..ec6003082 100644 --- a/lib/crates/fabro-workflows/src/handler/wait.rs +++ b/lib/crates/fabro-workflows/src/handler/wait.rs @@ -6,6 +6,7 @@ use crate::context::Context; use crate::error::FabroError; use crate::outcome::Outcome; use fabro_graphviz::graph::{AttrValue, Graph, Node}; +use tokio::time::sleep; use super::{EngineServices, Handler}; @@ -32,7 +33,7 @@ impl Handler for WaitHandler { node.id )) })?; - tokio::time::sleep(duration).await; + sleep(duration).await; Ok(Outcome::success()) } } diff --git a/lib/crates/fabro-workflows/src/lib.rs b/lib/crates/fabro-workflows/src/lib.rs index c19b89261..a44eecb5a 100644 --- a/lib/crates/fabro-workflows/src/lib.rs +++ b/lib/crates/fabro-workflows/src/lib.rs @@ -1,3 +1,6 @@ +use fabro_retro::retro::CompletedStage; +use serde::de::DeserializeOwned; + /// Convert a Duration's milliseconds to u64, saturating on overflow. pub(crate) fn millis_u64(d: std::time::Duration) -> u64 { u64::try_from(d.as_millis()).unwrap_or(u64::MAX) @@ -16,7 +19,7 @@ pub(crate) fn save_json( } /// Load a value from a JSON file. -pub(crate) fn load_json( +pub(crate) fn load_json( path: &std::path::Path, label: &str, ) -> error::Result { @@ -27,10 +30,7 @@ pub(crate) fn load_json( /// Build `Vec` from a `Checkpoint`, mapping workflow-engine /// types into the flat struct expected by `fabro_retro::retro::derive_retro`. -pub fn build_completed_stages( - cp: &records::Checkpoint, - run_failed: bool, -) -> Vec { +pub fn build_completed_stages(cp: &records::Checkpoint, run_failed: bool) -> Vec { use outcome::{OutcomeExt, StageStatus}; let mut stages = Vec::new(); @@ -53,7 +53,7 @@ pub fn build_completed_stages( any_stage_failed = true; } - stages.push(fabro_retro::retro::CompletedStage { + stages.push(CompletedStage { node_id: node_id.clone(), status, succeeded, @@ -71,7 +71,7 @@ pub fn build_completed_stages( if let Some(last) = stages.last_mut() { last.failed = true; } else { - stages.push(fabro_retro::retro::CompletedStage { + stages.push(CompletedStage { node_id: "unknown".to_string(), status: "fail".to_string(), succeeded: false, diff --git a/lib/crates/fabro-workflows/src/lifecycle/artifact.rs b/lib/crates/fabro-workflows/src/lifecycle/artifact.rs index dbd33d7f2..5c9ecbaaf 100644 --- a/lib/crates/fabro-workflows/src/lifecycle/artifact.rs +++ b/lib/crates/fabro-workflows/src/lifecycle/artifact.rs @@ -9,10 +9,12 @@ use fabro_core::outcome::NodeResult; use fabro_core::state::RunState; use crate::artifact::{offload_large_values, sync_artifacts_to_env, ArtifactStore}; +use crate::asset_snapshot::collect_assets; use crate::event::{EventEmitter, RunNoticeLevel, WorkflowRunEvent}; use crate::graph::WorkflowGraph; use crate::graph::WorkflowNode; use crate::outcome::StageUsage; +use fabro_core::error::Result as CoreResult; use fabro_core::lifecycle::NodeDecision; type WfRunState = RunState>; @@ -55,11 +57,7 @@ impl ArtifactLifecycle { #[async_trait] impl RunLifecycle for ArtifactLifecycle { - async fn on_run_start( - &self, - _graph: &WorkflowGraph, - _state: &WfRunState, - ) -> fabro_core::error::Result<()> { + async fn on_run_start(&self, _graph: &WorkflowGraph, _state: &WfRunState) -> CoreResult<()> { // Swap in a fresh artifact store on restart (don't call clear() — preserves files on disk) let mut store = self.artifact_store.lock().unwrap(); *store = ArtifactStore::new(self.artifact_values_dir.clone()); @@ -71,7 +69,7 @@ impl RunLifecycle for ArtifactLifecycle { &self, _ctx: &AttemptContext<'_, WorkflowGraph>, _state: &WfRunState, - ) -> fabro_core::error::Result { + ) -> CoreResult { // Record epoch seconds (floored to integer for macOS stat mtime parity) let epoch = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -85,7 +83,7 @@ impl RunLifecycle for ArtifactLifecycle { &self, ctx: &AttemptResultContext<'_, WorkflowGraph>, state: &WfRunState, - ) -> fabro_core::error::Result<()> { + ) -> CoreResult<()> { if self.asset_globs.is_empty() { return Ok(()); } @@ -103,14 +101,7 @@ impl RunLifecycle for ArtifactLifecycle { .join(format!("retry_{}", ctx.attempt)); let _ = std::fs::create_dir_all(&stage_dir); - match crate::asset_snapshot::collect_assets( - &*self.sandbox, - &stage_dir, - &self.asset_globs, - epoch, - ) - .await - { + match collect_assets(&*self.sandbox, &stage_dir, &self.asset_globs, epoch).await { Ok(summary) if summary.files_copied > 0 => { self.emitter.emit(&WorkflowRunEvent::AssetsCaptured { node_id: node_id.to_string(), @@ -137,7 +128,7 @@ impl RunLifecycle for ArtifactLifecycle { node: &WorkflowNode, result: &mut WfNodeResult, _state: &WfRunState, - ) -> fabro_core::error::Result<()> { + ) -> CoreResult<()> { let node_id = node.id(); // Offload large context_updates values to artifact store diff --git a/lib/crates/fabro-workflows/src/lifecycle/auto_status.rs b/lib/crates/fabro-workflows/src/lifecycle/auto_status.rs index 33c1be406..9f3dab762 100644 --- a/lib/crates/fabro-workflows/src/lifecycle/auto_status.rs +++ b/lib/crates/fabro-workflows/src/lifecycle/auto_status.rs @@ -1,5 +1,6 @@ use async_trait::async_trait; +use fabro_core::error::Result as CoreResult; use fabro_core::lifecycle::RunLifecycle; use fabro_core::outcome::NodeResult; use fabro_core::state::RunState; @@ -21,7 +22,7 @@ impl RunLifecycle for AutoStatusLifecycle { node: &WorkflowNode, result: &mut WfNodeResult, _state: &WfRunState, - ) -> fabro_core::error::Result<()> { + ) -> CoreResult<()> { let gv = node.inner(); let outcome = &mut result.outcome; if gv.auto_status() diff --git a/lib/crates/fabro-workflows/src/lifecycle/disk.rs b/lib/crates/fabro-workflows/src/lifecycle/disk.rs index e500a7574..838a3eca2 100644 --- a/lib/crates/fabro-workflows/src/lifecycle/disk.rs +++ b/lib/crates/fabro-workflows/src/lifecycle/disk.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use async_trait::async_trait; +use fabro_core::error::Result as CoreResult; use fabro_core::graph::NodeSpec; use fabro_core::lifecycle::RunLifecycle; use fabro_core::outcome::NodeResult; @@ -16,6 +17,8 @@ use crate::outcome::StageUsage; use crate::records::{Checkpoint, CheckpointExt}; use crate::run_dir::{write_node_status, write_start_record}; use crate::run_options::RunOptions; +use crate::run_status::{write_run_status, RunStatus}; +use fabro_graphviz::graph::types::Graph as GvGraph; type WfRunState = RunState>; type WfNodeResult = NodeResult>; @@ -24,7 +27,7 @@ type WfNodeResult = NodeResult>; pub struct DiskLifecycle { pub run_dir: PathBuf, pub run_id: String, - pub graph: Arc, + pub graph: Arc, pub run_options: Arc, pub emitter: Arc, pub circuit_breaker: Arc, @@ -33,19 +36,11 @@ pub struct DiskLifecycle { #[async_trait] impl RunLifecycle for DiskLifecycle { - async fn on_run_start( - &self, - _graph: &WorkflowGraph, - _state: &WfRunState, - ) -> fabro_core::error::Result<()> { + async fn on_run_start(&self, _graph: &WorkflowGraph, _state: &WfRunState) -> CoreResult<()> { // Write start.json write_start_record(&self.run_dir, &self.run_options); // Write run status as Running - crate::run_status::write_run_status( - &self.run_dir, - crate::run_status::RunStatus::Running, - None, - ); + write_run_status(&self.run_dir, RunStatus::Running, None); Ok(()) } @@ -54,7 +49,7 @@ impl RunLifecycle for DiskLifecycle { node: &WorkflowNode, result: &mut WfNodeResult, state: &WfRunState, - ) -> fabro_core::error::Result<()> { + ) -> CoreResult<()> { let gv = node.inner(); let visit = state.node_visits.get(gv.id.as_str()).copied().unwrap_or(1); write_node_status(&self.run_dir, &gv.id, visit, &result.outcome); @@ -67,7 +62,7 @@ impl RunLifecycle for DiskLifecycle { result: &WfNodeResult, next_node_id: Option<&str>, state: &WfRunState, - ) -> fabro_core::error::Result<()> { + ) -> CoreResult<()> { if !self.checkpoint_enabled { return Ok(()); } diff --git a/lib/crates/fabro-workflows/src/lifecycle/event.rs b/lib/crates/fabro-workflows/src/lifecycle/event.rs index 256f54dd1..3677dab6a 100644 --- a/lib/crates/fabro-workflows/src/lifecycle/event.rs +++ b/lib/crates/fabro-workflows/src/lifecycle/event.rs @@ -3,13 +3,17 @@ use std::time::Instant; use async_trait::async_trait; +use fabro_core::error::Result as CoreResult; use fabro_core::graph::NodeSpec; -use fabro_core::lifecycle::{AttemptContext, AttemptResultContext, EdgeContext, RunLifecycle}; +use fabro_core::lifecycle::{ + AttemptContext, AttemptResultContext, EdgeContext, EdgeDecision, NodeDecision, RunLifecycle, +}; use fabro_core::outcome::NodeResult; use fabro_core::state::RunState; use super::git::GitCheckpointResult; use crate::artifact::ArtifactStore; +use crate::error::FabroError; use crate::event::{EventEmitter, WorkflowRunEvent}; use crate::graph::WorkflowGraph; use crate::graph::WorkflowNode; @@ -52,11 +56,7 @@ pub struct EventLifecycle { #[async_trait] impl RunLifecycle for EventLifecycle { - async fn on_run_start( - &self, - _graph: &WorkflowGraph, - _state: &WfRunState, - ) -> fabro_core::error::Result<()> { + async fn on_run_start(&self, _graph: &WorkflowGraph, _state: &WfRunState) -> CoreResult<()> { // If restarted_from is Some, emit LoopRestart and clear it { let mut restarted = self.restarted_from.lock().unwrap(); @@ -124,7 +124,7 @@ impl RunLifecycle for EventLifecycle { &self, ctx: &AttemptContext<'_, WorkflowGraph>, state: &WfRunState, - ) -> fabro_core::error::Result>> { + ) -> CoreResult>> { let gv = ctx.node.inner(); self.emitter.emit(&WorkflowRunEvent::StageStarted { node_id: gv.id.clone(), @@ -135,14 +135,14 @@ impl RunLifecycle for EventLifecycle { attempt: ctx.attempt as usize, max_attempts: ctx.max_attempts as usize, }); - Ok(fabro_core::lifecycle::NodeDecision::Continue) + Ok(NodeDecision::Continue) } async fn after_attempt( &self, ctx: &AttemptResultContext<'_, WorkflowGraph>, state: &WfRunState, - ) -> fabro_core::error::Result<()> { + ) -> CoreResult<()> { if ctx.will_retry { let gv = ctx.node.inner(); let outcome = &ctx.result.outcome; @@ -175,7 +175,7 @@ impl RunLifecycle for EventLifecycle { node: &WorkflowNode, result: &mut WfNodeResult, state: &WfRunState, - ) -> fabro_core::error::Result<()> { + ) -> CoreResult<()> { let outcome = &result.outcome; // Skipped nodes had no StageStarted, so skip completion events (engine.rs:2080) if outcome.status == StageStatus::Skipped { @@ -219,7 +219,7 @@ impl RunLifecycle for EventLifecycle { &self, ctx: &EdgeContext<'_, WorkflowGraph>, _state: &WfRunState, - ) -> fabro_core::error::Result { + ) -> CoreResult { let outcome = ctx.outcome; let label = ctx .edge @@ -240,7 +240,7 @@ impl RunLifecycle for EventLifecycle { stage_status: outcome.status.to_string(), is_jump: ctx.is_jump, }); - Ok(fabro_core::lifecycle::EdgeDecision::Continue) + Ok(EdgeDecision::Continue) } async fn on_checkpoint( @@ -249,7 +249,7 @@ impl RunLifecycle for EventLifecycle { result: &WfNodeResult, _next_node_id: Option<&str>, _state: &WfRunState, - ) -> fabro_core::error::Result<()> { + ) -> CoreResult<()> { let status = result.outcome.status.to_string(); // Read git checkpoint result (set by GitLifecycle) @@ -323,7 +323,7 @@ impl RunLifecycle for EventLifecycle { .map(|f| f.message.clone()) .unwrap_or_else(|| "run failed".to_string()); self.emitter.emit(&WorkflowRunEvent::WorkflowRunFailed { - error: crate::error::FabroError::engine(error_msg), + error: FabroError::engine(error_msg), duration_ms, git_commit_sha: last_sha, }); diff --git a/lib/crates/fabro-workflows/src/lifecycle/fidelity.rs b/lib/crates/fabro-workflows/src/lifecycle/fidelity.rs index 0d1135560..0d725d06c 100644 --- a/lib/crates/fabro-workflows/src/lifecycle/fidelity.rs +++ b/lib/crates/fabro-workflows/src/lifecycle/fidelity.rs @@ -2,8 +2,9 @@ use std::sync::{Arc, Mutex}; use async_trait::async_trait; +use fabro_core::error::Result as CoreResult; use fabro_core::graph::NodeSpec; -use fabro_core::lifecycle::{EdgeContext, NodeDecision, RunLifecycle}; +use fabro_core::lifecycle::{EdgeContext, EdgeDecision, NodeDecision, RunLifecycle}; use fabro_core::state::RunState; use fabro_graphviz::graph::types::{Edge as GvEdge, Graph as GvGraph, Node as GvNode}; @@ -47,11 +48,7 @@ impl FidelityLifecycle { #[async_trait] impl RunLifecycle for FidelityLifecycle { - async fn on_run_start( - &self, - _graph: &WorkflowGraph, - _state: &WfRunState, - ) -> fabro_core::error::Result<()> { + async fn on_run_start(&self, _graph: &WorkflowGraph, _state: &WfRunState) -> CoreResult<()> { // Clear incoming edge data (restart target must not inherit pre-restart edge) *self.incoming_edge_data.lock().unwrap() = None; Ok(()) @@ -61,7 +58,7 @@ impl RunLifecycle for FidelityLifecycle { &self, node: &WorkflowNode, state: &WfRunState, - ) -> fabro_core::error::Result { + ) -> CoreResult { let incoming = self.incoming_edge_data.lock().unwrap().take(); let gv_node = node.inner(); @@ -142,7 +139,7 @@ impl RunLifecycle for FidelityLifecycle { &self, ctx: &EdgeContext<'_, WorkflowGraph>, _state: &WfRunState, - ) -> fabro_core::error::Result { + ) -> CoreResult { // Capture fidelity/thread from edge for next node if let Some(ref edge) = ctx.edge { let gv_edge = edge.inner(); @@ -151,7 +148,7 @@ impl RunLifecycle for FidelityLifecycle { }; *self.incoming_edge_data.lock().unwrap() = Some(edge_data); } - Ok(fabro_core::lifecycle::EdgeDecision::Continue) + Ok(EdgeDecision::Continue) } } diff --git a/lib/crates/fabro-workflows/src/lifecycle/git.rs b/lib/crates/fabro-workflows/src/lifecycle/git.rs index 85e088657..e5d0f1d9a 100644 --- a/lib/crates/fabro-workflows/src/lifecycle/git.rs +++ b/lib/crates/fabro-workflows/src/lifecycle/git.rs @@ -3,7 +3,7 @@ use std::sync::{Arc, Mutex}; use async_trait::async_trait; -use fabro_core::error::CoreError; +use fabro_core::error::{CoreError, Result as CoreResult}; use fabro_core::graph::NodeSpec; use fabro_core::lifecycle::RunLifecycle; use fabro_core::outcome::NodeResult; @@ -11,10 +11,12 @@ use fabro_core::state::RunState; use crate::artifact::ArtifactStore; use crate::event::{EventEmitter, RunNoticeLevel, WorkflowRunEvent}; +use crate::git::scan_node_files; +use crate::git::MetadataStore; use crate::graph::WorkflowGraph; use crate::graph::WorkflowNode; use crate::outcome::{Outcome, StageStatus, StageUsage}; -use crate::records::CheckpointExt; +use crate::records::{Checkpoint, CheckpointExt}; use crate::run_dir::node_dir; use crate::run_options::RunOptions; use crate::sandbox_git::{git_checkpoint, git_diff, git_push_host}; @@ -45,11 +47,7 @@ pub struct GitLifecycle { #[async_trait] impl RunLifecycle for GitLifecycle { - async fn on_run_start( - &self, - _graph: &WorkflowGraph, - _state: &WfRunState, - ) -> fabro_core::error::Result<()> { + async fn on_run_start(&self, _graph: &WorkflowGraph, _state: &WfRunState) -> CoreResult<()> { // Reset last_git_sha (diff base parity) *self.last_git_sha.lock().unwrap() = None; *self.checkpoint_git_result.lock().unwrap() = None; @@ -62,7 +60,7 @@ impl RunLifecycle for GitLifecycle { .and_then(|g| g.meta_branch.as_ref()), self.run_options.host_repo_path.as_ref(), ) { - let store = crate::git::MetadataStore::new(repo_path, &self.run_options.git_author); + let store = MetadataStore::new(repo_path, &self.run_options.git_author); let run_json = std::fs::read(self.run_dir.join("run.json")).ok(); let start_json = std::fs::read(self.run_dir.join("start.json")).ok(); let sandbox_json = std::fs::read(self.run_dir.join("sandbox.json")).ok(); @@ -94,7 +92,7 @@ impl RunLifecycle for GitLifecycle { result: &WfNodeResult, _next_node_id: Option<&str>, state: &WfRunState, - ) -> fabro_core::error::Result<()> { + ) -> CoreResult<()> { let node_id = node.id(); // Skip git checkpoint for the start node (always empty) or if git disabled @@ -111,7 +109,7 @@ impl RunLifecycle for GitLifecycle { .and_then(|g| g.meta_branch.as_ref()), self.run_options.host_repo_path.as_ref(), ) { - let store = crate::git::MetadataStore::new(repo_path, &self.run_options.git_author); + let store = MetadataStore::new(repo_path, &self.run_options.git_author); // Build checkpoint JSON for shadow branch let checkpoint_path = self.run_dir.join("checkpoint.json"); std::fs::read(&checkpoint_path).ok().and_then(|cp_json| { @@ -127,7 +125,7 @@ impl RunLifecycle for GitLifecycle { }) }) .collect(); - extra_entries.extend(crate::git::scan_node_files(&self.run_dir)); + extra_entries.extend(scan_node_files(&self.run_dir)); let extra_refs: Vec<(&str, &[u8])> = extra_entries .iter() .map(|(k, v)| (k.as_str(), v.as_slice())) @@ -173,7 +171,7 @@ impl RunLifecycle for GitLifecycle { // Re-save checkpoint.json with SHA let checkpoint_path = self.run_dir.join("checkpoint.json"); - if let Ok(mut cp) = crate::records::Checkpoint::load(&checkpoint_path) { + if let Ok(mut cp) = Checkpoint::load(&checkpoint_path) { cp.git_commit_sha = Some(sha.clone()); if let Err(e) = cp.save(&checkpoint_path) { self.emitter.emit(&WorkflowRunEvent::RunNotice { diff --git a/lib/crates/fabro-workflows/src/lifecycle/mod.rs b/lib/crates/fabro-workflows/src/lifecycle/mod.rs index a690eab96..1cbe47f58 100644 --- a/lib/crates/fabro-workflows/src/lifecycle/mod.rs +++ b/lib/crates/fabro-workflows/src/lifecycle/mod.rs @@ -26,12 +26,13 @@ use fabro_core::state::RunState; use crate::artifact::ArtifactStore; use crate::context; -use crate::error::FailureSignatureExt; +use crate::error::{FailureSignature, FailureSignatureExt}; use crate::event::EventEmitter; use crate::graph::WorkflowGraph; use crate::graph::WorkflowNode; use crate::outcome::{Outcome, StageUsage}; use crate::run_options::RunOptions; +use fabro_graphviz::graph::types::Graph as GvGraph; use fabro_hooks::HookRunner; use fabro_sandbox::Sandbox; @@ -68,7 +69,7 @@ pub struct WorkflowLifecycle { /// Gates context seeding on initial resume. is_initial_resume: AtomicBool, // Config needed for context seeding - graph: Arc, + graph: Arc, run_id: String, working_directory: Option, } @@ -79,7 +80,7 @@ impl WorkflowLifecycle { emitter: Arc, hook_runner: Option>, sandbox: Arc, - graph: Arc, + graph: Arc, run_dir: PathBuf, run_options: Arc, is_resume: bool, @@ -188,8 +189,8 @@ impl WorkflowLifecycle { /// Restore circuit breaker state from a checkpoint (for resume). pub fn restore_circuit_breaker( &self, - loop_sigs: HashMap, - restart_sigs: HashMap, + loop_sigs: HashMap, + restart_sigs: HashMap, ) { self.circuit_breaker.restore(loop_sigs, restart_sigs); } @@ -320,7 +321,7 @@ impl RunLifecycle for WorkflowLifecycle { .failure .as_ref() .and_then(|f| f.signature.as_deref()); - crate::error::FailureSignature::new( + FailureSignature::new( node.id(), category, signature_hint, diff --git a/lib/crates/fabro-workflows/src/node_handler.rs b/lib/crates/fabro-workflows/src/node_handler.rs index 07c5cc659..a840e2dd5 100644 --- a/lib/crates/fabro-workflows/src/node_handler.rs +++ b/lib/crates/fabro-workflows/src/node_handler.rs @@ -14,10 +14,12 @@ use crate::context::Context; use crate::graph::WorkflowGraph; use crate::graph::WorkflowNode; -use crate::handler::{format_panic_message, EngineServices}; +use crate::handler::{dispatch_handler, format_panic_message, EngineServices}; use crate::outcome::{Outcome, StageStatus}; use crate::retry::build_retry_policy; use crate::run_dir; +use fabro_graphviz::graph::types::Graph as GvGraph; +use tokio::time::timeout; /// Production node handler that bridges fabro-core's NodeHandler to the /// existing fabro-workflows Handler trait via EngineServices. @@ -27,7 +29,7 @@ use crate::run_dir; pub struct WorkflowNodeHandler { pub services: Arc, pub run_dir: PathBuf, - pub graph: Arc, + pub graph: Arc, } #[async_trait] @@ -50,7 +52,7 @@ impl NodeHandler for WorkflowNodeHandler { // Wrap with panic catch + timeout let run_dir = self.run_dir.clone(); - let future = crate::handler::dispatch_handler( + let future = dispatch_handler( handler, gv_node, &wf_context, @@ -61,7 +63,7 @@ impl NodeHandler for WorkflowNodeHandler { let panic_safe = AssertUnwindSafe(future).catch_unwind(); let timed_result = if let Some(duration) = node_timeout { - match tokio::time::timeout(duration, panic_safe).await { + match timeout(duration, panic_safe).await { Ok(inner) => inner, Err(_elapsed) => { return Err(CoreError::handler(HandlerErrorDetail { diff --git a/lib/crates/fabro-workflows/src/operations/create.rs b/lib/crates/fabro-workflows/src/operations/create.rs index d8f403c94..4313629ac 100644 --- a/lib/crates/fabro-workflows/src/operations/create.rs +++ b/lib/crates/fabro-workflows/src/operations/create.rs @@ -11,7 +11,10 @@ use crate::error::FabroError; use crate::pipeline::types::PersistOptions; use crate::pipeline::{self, Persisted, TransformOptions, Validated}; use crate::records::RunRecord; +use crate::run_lookup::default_runs_base; +use crate::run_status::{write_run_status, RunStatus}; use crate::transforms::{expand_vars, Transform}; +use fabro_sandbox::daytona::detect_repo_info; use super::source::{resolve_workflow, ResolveWorkflowInput, WorkflowInput}; @@ -86,7 +89,7 @@ pub fn create(request: CreateRunInput) -> Result { let host_repo_path = host_repo_path.or_else(|| Some(working_directory.to_string_lossy().to_string())); let base_branch = base_branch.or_else(|| { - fabro_sandbox::daytona::detect_repo_info(&working_directory) + detect_repo_info(&working_directory) .ok() .and_then(|(_, branch)| branch) }); @@ -111,7 +114,7 @@ pub fn create(request: CreateRunInput) -> Result { )?; write_run_config_snapshot(&run_dir, resolved.workflow_toml_path.as_deref())?; - crate::run_status::write_run_status(&run_dir, crate::run_status::RunStatus::Submitted, None); + write_run_status(&run_dir, RunStatus::Submitted, None); Ok(CreatedRun { persisted, @@ -297,7 +300,7 @@ pub(crate) fn resolve_run_settings(mut settings: FabroSettings, graph: &Graph) - } pub(crate) fn default_run_dir(run_id: &str, dry_run: bool) -> PathBuf { - make_run_dir(&crate::run_lookup::default_runs_base(), run_id, dry_run) + make_run_dir(&default_runs_base(), run_id, dry_run) } pub(crate) fn make_run_dir(runs_base: &Path, run_id: &str, dry_run: bool) -> PathBuf { diff --git a/lib/crates/fabro-workflows/src/operations/fork.rs b/lib/crates/fabro-workflows/src/operations/fork.rs index e1bef1808..65eeffa07 100644 --- a/lib/crates/fabro-workflows/src/operations/fork.rs +++ b/lib/crates/fabro-workflows/src/operations/fork.rs @@ -3,7 +3,7 @@ use fabro_git_storage::branchstore::BranchStore; use fabro_git_storage::gitobj::Store; use git2::{Oid, Signature}; -use crate::git::MetadataStore; +use crate::git::{push_run_branches, MetadataStore, RUN_BRANCH_PREFIX}; use crate::records::RunRecord; use crate::records::StartRecord; @@ -39,7 +39,7 @@ fn fork_from_entry( let new_run_id = ulid::Ulid::new().to_string(); let sig = Signature::now("Fabro", "noreply@fabro.sh")?; - let new_run_branch = format!("{}{new_run_id}", crate::git::RUN_BRANCH_PREFIX); + let new_run_branch = format!("{}{new_run_id}", RUN_BRANCH_PREFIX); match &entry.run_commit_sha { Some(sha) => { let oid = @@ -134,10 +134,10 @@ fn fork_from_entry( .map_err(|e| anyhow::anyhow!("failed to write metadata entries: {e}"))?; if push { - let source_run_branch = format!("{}{source_run_id}", crate::git::RUN_BRANCH_PREFIX); + let source_run_branch = format!("{}{source_run_id}", RUN_BRANCH_PREFIX); let run_refspec = format!("refs/heads/{new_run_branch}:refs/heads/{new_run_branch}"); let meta_refspec = format!("refs/heads/{new_meta_branch}:refs/heads/{new_meta_branch}"); - crate::git::push_run_branches( + push_run_branches( store, &source_run_branch, Some(&run_refspec), @@ -186,7 +186,7 @@ mod tests { let record = serde_json::json!({ "run_id": run_id, "start_time": "2025-01-01T00:00:00Z", - "run_branch": format!("{}{}", crate::git::RUN_BRANCH_PREFIX, run_id), + "run_branch": format!("{}{}", RUN_BRANCH_PREFIX, run_id), }); serde_json::to_vec_pretty(&record).unwrap() } @@ -194,7 +194,7 @@ mod tests { fn setup_source_run(store: &Store, run_id: &str, nodes: &[&str]) -> Vec { let sig = test_sig(); - let run_branch = format!("{}{run_id}", crate::git::RUN_BRANCH_PREFIX); + let run_branch = format!("{}{run_id}", RUN_BRANCH_PREFIX); let empty_tree = store.write_empty_tree().unwrap(); let mut run_oids = Vec::new(); let mut parent: Option = None; @@ -254,7 +254,7 @@ mod tests { ) .unwrap(); - let new_run_branch = format!("{}{new_run_id}", crate::git::RUN_BRANCH_PREFIX); + let new_run_branch = format!("{}{new_run_id}", RUN_BRANCH_PREFIX); let new_meta_branch = MetadataStore::branch_name(&new_run_id); assert!(store.resolve_ref(&new_run_branch).unwrap().is_some()); diff --git a/lib/crates/fabro-workflows/src/operations/rewind.rs b/lib/crates/fabro-workflows/src/operations/rewind.rs index a51662bbd..81663f46b 100644 --- a/lib/crates/fabro-workflows/src/operations/rewind.rs +++ b/lib/crates/fabro-workflows/src/operations/rewind.rs @@ -6,9 +6,10 @@ use fabro_git_storage::branchstore::{BranchStore, CommitInfo}; use fabro_git_storage::gitobj::Store; use git2::{Oid, Repository, Signature}; -use crate::git::MetadataStore; -use crate::records::Checkpoint; +use crate::git::{push_run_branches, MetadataStore, RUN_BRANCH_PREFIX}; +use crate::records::{Checkpoint, RunRecord}; use fabro_graphviz::graph::Graph; +use fabro_graphviz::parser; #[derive(Debug, Clone, PartialEq, Eq)] pub enum RewindTarget { @@ -164,7 +165,7 @@ fn backfill_run_shas(store: &Store, run_id: &str, timeline: &mut [TimelineEntry] return; } - let run_branch = format!("{}{run_id}", crate::git::RUN_BRANCH_PREFIX); + let run_branch = format!("{}{run_id}", RUN_BRANCH_PREFIX); let sig = match Signature::now("Fabro", "noreply@fabro.sh") { Ok(s) => s, Err(_) => return, @@ -257,7 +258,7 @@ fn rewind_to_entry(store: &Store, run_id: &str, entry: &TimelineEntry, push: boo entry.ordinal, entry.node_name ); - let run_branch = format!("{}{run_id}", crate::git::RUN_BRANCH_PREFIX); + let run_branch = format!("{}{run_id}", RUN_BRANCH_PREFIX); match &entry.run_commit_sha { Some(sha) => { let oid = @@ -267,7 +268,7 @@ fn rewind_to_entry(store: &Store, run_id: &str, entry: &TimelineEntry, push: boo .map_err(|e| anyhow::anyhow!("failed to update run branch ref: {e}"))?; eprintln!( "Rewound run branch {}{run_id} to {}", - crate::git::RUN_BRANCH_PREFIX, + RUN_BRANCH_PREFIX, &sha[..8] ); } @@ -285,7 +286,7 @@ fn rewind_to_entry(store: &Store, run_id: &str, entry: &TimelineEntry, push: boo .as_ref() .map(|_| format!("+refs/heads/{run_branch}:refs/heads/{run_branch}")); let meta_refspec = format!("+refs/heads/{meta_branch}:refs/heads/{meta_branch}"); - crate::git::push_run_branches( + push_run_branches( store, &run_branch, run_refspec.as_deref(), @@ -339,7 +340,7 @@ fn load_parallel_map(store: &Store, run_id: &str) -> HashMap { let bs = BranchStore::new(store, &branch, &sig); if let Ok(Some(run_bytes)) = bs.read_entry("run.json") { - if let Ok(record) = serde_json::from_slice::(&run_bytes) { + if let Ok(record) = serde_json::from_slice::(&run_bytes) { return detect_parallel_interior(&record.graph); } } @@ -352,7 +353,7 @@ fn load_parallel_map(store: &Store, run_id: &str) -> HashMap { }, }; let dot_source = String::from_utf8_lossy(&graph_bytes); - let graph = match fabro_graphviz::parser::parse(&dot_source) { + let graph = match parser::parse(&dot_source) { Ok(g) => g, Err(_) => return HashMap::new(), }; diff --git a/lib/crates/fabro-workflows/src/operations/source.rs b/lib/crates/fabro-workflows/src/operations/source.rs index 7a6dd5ac8..7562bada4 100644 --- a/lib/crates/fabro-workflows/src/operations/source.rs +++ b/lib/crates/fabro-workflows/src/operations/source.rs @@ -2,6 +2,7 @@ use std::path::{Path, PathBuf}; use anyhow::Context; use fabro_config::{project as project_config, FabroSettings}; +use fabro_util::path::expand_tilde; #[derive(Clone, Debug)] pub enum WorkflowInput { @@ -38,7 +39,7 @@ fn resolve_goal_file( let Some(goal_file) = goal_file else { return Ok(None); }; - let expanded = fabro_util::path::expand_tilde(goal_file); + let expanded = expand_tilde(goal_file); let goal_path = if expanded.is_absolute() { expanded } else { diff --git a/lib/crates/fabro-workflows/src/operations/start.rs b/lib/crates/fabro-workflows/src/operations/start.rs index 549b67f73..f47fad353 100644 --- a/lib/crates/fabro-workflows/src/operations/start.rs +++ b/lib/crates/fabro-workflows/src/operations/start.rs @@ -15,16 +15,26 @@ use serde::Serialize; use crate::context::Context; use crate::error::FabroError; -use crate::event::{EventEmitter, ProgressLogger, RunNoticeLevel, WorkflowRunEvent}; +use crate::event::{ + append_progress_event, EventEmitter, ProgressLogger, RunNoticeLevel, WorkflowRunEvent, +}; +use crate::git::GitAuthor; +use crate::handler::HandlerRegistry; use crate::outcome::{Outcome, StageStatus}; use crate::pipeline::{ self, build_conclusion, classify_engine_result, persist_terminal_outcome, DevcontainerSpec, FinalizeOptions, Finalized, InitOptions, LlmSpec, Persisted, PullRequestOptions, RetroOptions, SandboxEnvSpec, SandboxSpec, }; -use crate::records::{Checkpoint, Conclusion, ConclusionExt, RunRecordExt}; +use crate::records::{Checkpoint, Conclusion, ConclusionExt, RunRecord, RunRecordExt}; use crate::run_options::{GitCheckpointOptions, LifecycleOptions, RunOptions}; use crate::run_status::{self, RunStatus, RunStatusRecordExt, StatusReason}; +use fabro_config::run::PullRequestSettings; +use fabro_retro::retro::Retro; +use fabro_sandbox::daytona::detect_repo_info; +use fabro_sandbox::daytona::DaytonaConfig; +use fabro_sandbox::ssh::{GitCloneParams as SshGitCloneParams, SshConfig}; +use tokio::runtime::Handle; struct RunSession { cancel_token: Option>, @@ -37,14 +47,14 @@ struct RunSession { sandbox_env: SandboxEnvSpec, devcontainer: Option, seed_context: Option, - git_author: crate::git::GitAuthor, + git_author: GitAuthor, git: Option, github_app: Option, worktree_mode: Option, - registry_override: Option>, + registry_override: Option>, retro_enabled: bool, preserve_sandbox: bool, - pr_config: Option, + pr_config: Option, pr_github_app: Option, pr_origin_url: Option, pr_model: String, @@ -54,14 +64,14 @@ pub struct StartServices { pub cancel_token: Option>, pub emitter: Arc, pub interviewer: Arc, - pub git_author: crate::git::GitAuthor, + pub git_author: GitAuthor, pub github_app: Option, - pub registry_override: Option>, + pub registry_override: Option>, } pub struct Started { pub finalized: Finalized, - pub retro: Option, + pub retro: Option, pub retro_duration: Duration, } @@ -159,10 +169,9 @@ impl RunSession { .map_err(|err| FabroError::Precondition(err.to_string()))?; } - let (origin_url, detected_base_branch) = - fabro_sandbox::daytona::detect_repo_info(&working_directory) - .map(|(url, branch)| (Some(url), branch)) - .unwrap_or((None, None)); + let (origin_url, detected_base_branch) = detect_repo_info(&working_directory) + .map(|(url, branch)| (Some(url), branch)) + .unwrap_or((None, None)); let sandbox_provider = resolve_sandbox_provider(&settings)?; let sandbox_provider = @@ -324,9 +333,7 @@ fn resolve_worktree_mode(settings: &FabroSettings) -> sandbox_config::WorktreeMo .unwrap_or_default() } -fn resolve_daytona_config( - settings: &FabroSettings, -) -> Option { +fn resolve_daytona_config(settings: &FabroSettings) -> Option { settings .sandbox_settings() .and_then(|sandbox| sandbox.daytona.clone()) @@ -352,14 +359,14 @@ fn resolve_exe_clone_params(cwd: &Path) -> Option Option { +fn resolve_ssh_config(settings: &FabroSettings) -> Option { settings .sandbox_settings() .and_then(|sandbox| sandbox.ssh.clone()) } -fn resolve_ssh_clone_params(cwd: &Path) -> Option { - let (detected_url, branch) = match fabro_sandbox::daytona::detect_repo_info(cwd) { +fn resolve_ssh_clone_params(cwd: &Path) -> Option { + let (detected_url, branch) = match detect_repo_info(cwd) { Ok(info) => info, Err(err) => { tracing::warn!("No git repo detected for SSH clone: {err}"); @@ -367,7 +374,7 @@ fn resolve_ssh_clone_params(cwd: &Path) -> Option Option { - crate::records::RunRecord::load(run_dir) + RunRecord::load(run_dir) .ok() .map(|record| record.run_id) .filter(|run_id| !run_id.trim().is_empty()) @@ -649,7 +656,7 @@ fn persist_detached_failure( run_status::write_run_status(run_dir, RunStatus::Failed, Some(reason)); if let Some(run_id) = load_run_id(run_dir) { - crate::event::append_progress_event( + append_progress_event( run_dir, &run_id, &WorkflowRunEvent::RunNotice { diff --git a/lib/crates/fabro-workflows/src/outcome.rs b/lib/crates/fabro-workflows/src/outcome.rs index 4becd3210..8184cc7b3 100644 --- a/lib/crates/fabro-workflows/src/outcome.rs +++ b/lib/crates/fabro-workflows/src/outcome.rs @@ -2,9 +2,10 @@ pub use fabro_core::outcome::{FailureCategory, FailureDetail, OutcomeMeta, Stage pub use fabro_types::usage::StageUsage; use crate::error::classify_failure_reason; +use fabro_llm::types::Usage as LlmUsage; -pub fn stage_usage_to_llm(u: &StageUsage) -> fabro_llm::types::Usage { - fabro_llm::types::Usage { +pub fn stage_usage_to_llm(u: &StageUsage) -> LlmUsage { + LlmUsage { input_tokens: u.input_tokens, output_tokens: u.output_tokens, total_tokens: u.input_tokens + u.output_tokens, diff --git a/lib/crates/fabro-workflows/src/pipeline/execute.rs b/lib/crates/fabro-workflows/src/pipeline/execute.rs index 08a235f01..e1c89e6b6 100644 --- a/lib/crates/fabro-workflows/src/pipeline/execute.rs +++ b/lib/crates/fabro-workflows/src/pipeline/execute.rs @@ -2,21 +2,25 @@ use std::sync::Arc; use std::time::Instant; use fabro_core::executor::ExecutorBuilder; +use fabro_core::handler::NodeHandler; use fabro_core::state::RunState; +use tokio::time::sleep; use tokio_util::sync::CancellationToken; use crate::context::{self, Context}; use crate::error::FabroError; +use crate::event::WorkflowRunEvent; use crate::graph::WorkflowGraph; use crate::handler::EngineServices; use crate::lifecycle::WorkflowLifecycle; use crate::node_handler::WorkflowNodeHandler; use crate::outcome::{Outcome, StageStatus}; +use crate::records::Checkpoint; use crate::sandbox_git::GitState; use super::types::{Executed, Initialized}; -fn seed_context_from_checkpoint(checkpoint: Option<&crate::records::Checkpoint>) -> Context { +fn seed_context_from_checkpoint(checkpoint: Option<&Checkpoint>) -> Context { let context = Context::new(); if let Some(cp) = checkpoint { for (k, v) in &cp.context_values { @@ -217,7 +221,7 @@ pub async fn execute(init: Initialized) -> Executed { tokio::spawn(async move { loop { tokio::select! { - _ = tokio::time::sleep(stall_timeout) => { + _ = sleep(stall_timeout) => { if shutdown_clone.is_cancelled() { return; } @@ -243,9 +247,8 @@ pub async fn execute(init: Initialized) -> Executed { None }; - let mut builder = - ExecutorBuilder::new(handler as Arc>) - .lifecycle(Box::new(lifecycle)); + let mut builder = ExecutorBuilder::new(handler as Arc>) + .lifecycle(Box::new(lifecycle)); if let Some(ref cancel) = run_options.cancel_token { builder = builder.cancel_token(cancel.clone()); @@ -279,7 +282,7 @@ pub async fn execute(init: Initialized) -> Executed { Err(fabro_core::CoreError::StallTimeout { node_id }) => { let stall_timeout = graph.stall_timeout().unwrap_or_default(); let idle_secs = stall_timeout.as_secs(); - emitter.emit(&crate::event::WorkflowRunEvent::StallWatchdogTimeout { + emitter.emit(&WorkflowRunEvent::StallWatchdogTimeout { node: node_id.clone(), idle_seconds: idle_secs, }); diff --git a/lib/crates/fabro-workflows/src/pipeline/finalize.rs b/lib/crates/fabro-workflows/src/pipeline/finalize.rs index 0e4705a7c..ee586ab66 100644 --- a/lib/crates/fabro-workflows/src/pipeline/finalize.rs +++ b/lib/crates/fabro-workflows/src/pipeline/finalize.rs @@ -3,11 +3,14 @@ use std::sync::Arc; use crate::error::FabroError; use crate::event::{EventEmitter, RunNoticeLevel, WorkflowRunEvent}; +use crate::git::{scan_node_files, MetadataStore}; use crate::outcome::{Outcome, OutcomeExt, StageStatus}; -use crate::records::{Checkpoint, CheckpointExt, Conclusion, ConclusionExt}; +use crate::records::{Checkpoint, CheckpointExt, Conclusion, ConclusionExt, StageSummary}; use crate::run_options::RunOptions; -use crate::run_status::{RunStatus, StatusReason}; +use crate::run_status::{write_run_status, RunStatus, StatusReason}; +use crate::sandbox_git::git_push_host; use fabro_hooks::{HookContext, HookEvent, HookRunner}; +use fabro_retro::retro::extract_stage_durations; use super::types::{Concluded, FinalizeOptions, Retroed}; @@ -67,7 +70,7 @@ pub fn build_conclusion( final_git_commit_sha: Option, ) -> Conclusion { let checkpoint = Checkpoint::load(&run_dir.join("checkpoint.json")).ok(); - let stage_durations = fabro_retro::retro::extract_stage_durations(run_dir); + let stage_durations = extract_stage_durations(run_dir); let mut total_input_tokens: i64 = 0; let mut total_output_tokens: i64 = 0; @@ -105,7 +108,7 @@ pub fn build_conclusion( total_reasoning_tokens += usage.reasoning_tokens.unwrap_or(0); } - stages.push(crate::records::StageSummary { + stages.push(StageSummary { stage_id: node_id.clone(), stage_label: node_id.clone(), duration_ms: stage_durations.get(node_id).copied().unwrap_or(0), @@ -143,7 +146,7 @@ pub fn persist_terminal_outcome( status_reason: Option, ) { let _ = conclusion.save(&run_dir.join("conclusion.json")); - crate::run_status::write_run_status(run_dir, run_status, status_reason); + write_run_status(run_dir, run_status, status_reason); } /// Write a finalize commit to the shadow branch with retro.json and final node files. @@ -161,8 +164,8 @@ pub async fn write_finalize_commit(run_options: &RunOptions, run_dir: &Path) { return; }; - let store = crate::git::MetadataStore::new(repo_path, &run_options.git_author); - let mut entries = crate::git::scan_node_files(run_dir); + let store = MetadataStore::new(repo_path, &run_options.git_author); + let mut entries = scan_node_files(run_dir); if let Ok(retro_bytes) = std::fs::read(run_dir.join("retro.json")) { entries.push(("retro.json".to_string(), retro_bytes)); } @@ -176,7 +179,7 @@ pub async fn write_finalize_commit(run_options: &RunOptions, run_dir: &Path) { } let refspec = format!("refs/heads/{meta_branch}"); - crate::sandbox_git::git_push_host( + git_push_host( repo_path, &refspec, &run_options.github_app, diff --git a/lib/crates/fabro-workflows/src/pipeline/initialize.rs b/lib/crates/fabro-workflows/src/pipeline/initialize.rs index 1a325b49e..775351069 100644 --- a/lib/crates/fabro-workflows/src/pipeline/initialize.rs +++ b/lib/crates/fabro-workflows/src/pipeline/initialize.rs @@ -13,12 +13,20 @@ use fabro_sandbox::{ }; use shlex::try_quote; +use crate::devcontainer_bridge::{devcontainer_to_snapshot_config, run_devcontainer_lifecycle}; use crate::error::FabroError; -use crate::event::{RunNoticeLevel, WorkflowRunEvent}; -use crate::git::{self, GitSyncStatus}; -use crate::handler::default_registry; +use crate::event::{EventEmitter, RunNoticeLevel, WorkflowRunEvent}; +use crate::git::{self, GitSyncStatus, MetadataStore}; use crate::handler::llm::{AgentApiBackend, AgentCliBackend, BackendRouter}; -use crate::run_options::GitCheckpointOptions; +use crate::handler::{default_registry, HandlerRegistry}; +use crate::run_options::{GitCheckpointOptions, RunOptions}; +use fabro_sandbox::daytona::DaytonaSandbox; +use fabro_sandbox::docker::DockerSandboxConfig; +use fabro_sandbox::ssh::SshSandbox; +use tokio::process::Command as TokioCommand; +use tokio::runtime::Handle; +use tokio::task::spawn_blocking; +use tokio::time::timeout as tokio_timeout; use super::types::{InitOptions, Initialized, LlmSpec, Persisted, SandboxEnvSpec, SandboxSpec}; @@ -53,7 +61,7 @@ async fn run_hooks( } fn emit_run_notice( - emitter: &crate::event::EventEmitter, + emitter: &EventEmitter, level: RunNoticeLevel, code: impl Into, message: impl Into, @@ -76,10 +84,7 @@ fn sandbox_provider_name(spec: &SandboxSpec) -> &'static str { } } -fn host_repo_path_for_planning( - run_options: &crate::run_options::RunOptions, - spec: &SandboxSpec, -) -> Option { +fn host_repo_path_for_planning(run_options: &RunOptions, spec: &SandboxSpec) -> Option { run_options.host_repo_path.clone().or_else(|| match spec { SandboxSpec::Local { working_directory } => Some(working_directory.clone()), SandboxSpec::Docker { config } => Some(PathBuf::from(&config.host_working_directory)), @@ -178,11 +183,9 @@ async fn resolve_worktree_plan( GitSyncStatus::Dirty => { let repo_path = repo_path.clone(); let branch = branch.clone(); - tokio::task::spawn_blocking(move || { - git::branch_needs_push(&repo_path, "origin", &branch) - }) - .await - .unwrap_or(true) + spawn_blocking(move || git::branch_needs_push(&repo_path, "origin", &branch)) + .await + .unwrap_or(true) } }; @@ -254,7 +257,7 @@ async fn resolve_worktree_plan( fn local_sandbox_with_callback( working_directory: PathBuf, - emitter: Arc, + emitter: Arc, ) -> Arc { let mut sandbox = LocalSandbox::new(working_directory); sandbox.set_event_callback(Arc::new(move |event| { @@ -266,7 +269,7 @@ fn local_sandbox_with_callback( async fn build_sandbox( spec: &SandboxSpec, worktree_plan: Option<&WorktreePlan>, - emitter: Arc, + emitter: Arc, ) -> Result { let mut worktree_created = false; let sandbox: Arc = match spec { @@ -310,7 +313,7 @@ async fn build_sandbox( } } SandboxSpec::Docker { config } => { - let mut sandbox = DockerSandbox::new(fabro_sandbox::docker::DockerSandboxConfig { + let mut sandbox = DockerSandbox::new(DockerSandboxConfig { image: config.image.clone(), host_working_directory: config.host_working_directory.clone(), container_mount_point: config.container_mount_point.clone(), @@ -334,7 +337,7 @@ async fn build_sandbox( run_id, clone_branch, } => { - let mut sandbox = fabro_sandbox::daytona::DaytonaSandbox::new( + let mut sandbox = DaytonaSandbox::new( config.clone(), github_app.clone(), run_id.clone(), @@ -380,7 +383,7 @@ async fn build_sandbox( run_id, github_app, } => { - let mut sandbox = fabro_sandbox::ssh::SshSandbox::new( + let mut sandbox = SshSandbox::new( config.clone(), clone_params.clone(), run_id.clone(), @@ -428,7 +431,7 @@ async fn mint_github_token( async fn build_sandbox_env( spec: &SandboxEnvSpec, github_app: Option<&fabro_github::GitHubAppCredentials>, - emitter: &crate::event::EventEmitter, + emitter: &EventEmitter, ) -> Result, FabroError> { let mut env = spec.devcontainer_env.clone(); env.extend(spec.toml_env.clone()); @@ -458,8 +461,8 @@ async fn build_registry( spec: &LlmSpec, interviewer: Arc, sandbox_env: &HashMap, - emitter: &crate::event::EventEmitter, -) -> Result<(Arc, Option, bool), FabroError> { + emitter: &EventEmitter, +) -> Result<(Arc, Option, bool), FabroError> { let build_dry_run = || Arc::new(default_registry(Arc::clone(&interviewer), || None)); if spec.dry_run { @@ -531,9 +534,7 @@ async fn resolve_devcontainer(options: &mut InitOptions) -> Result<(), FabroErro config: daytona, .. } = &mut options.sandbox { - daytona.snapshot = Some(crate::devcontainer_bridge::devcontainer_to_snapshot_config( - &config, - )); + daytona.snapshot = Some(devcontainer_to_snapshot_config(&config)); } let timeout = std::time::Duration::from_millis(300_000); @@ -551,9 +552,9 @@ async fn resolve_devcontainer(options: &mut InitOptions) -> Result<(), FabroErro }; for shell_command in shell_commands { - let output = tokio::time::timeout( + let output = tokio_timeout( timeout, - tokio::process::Command::new("sh") + TokioCommand::new("sh") .arg("-c") .arg(&shell_command) .current_dir(&devcontainer.resolve_dir) @@ -664,7 +665,7 @@ pub async fn initialize( options.run_options.git = Some(GitCheckpointOptions { base_sha: Some(plan.base_sha.clone()), run_branch: Some(plan.branch_name.clone()), - meta_branch: Some(crate::git::MetadataStore::branch_name(&options.run_id)), + meta_branch: Some(MetadataStore::branch_name(&options.run_id)), }); } @@ -680,7 +681,7 @@ pub async fn initialize( let sandbox = sandbox_result.sandbox; let cleanup_guard = scopeguard::guard(Arc::clone(&sandbox), |sandbox| { - if let Ok(handle) = tokio::runtime::Handle::try_current() { + if let Ok(handle) = Handle::try_current() { handle.spawn(async move { let _ = sandbox.cleanup().await; }); @@ -759,9 +760,7 @@ pub async fn initialize( options.run_options.git = Some(GitCheckpointOptions { base_sha, run_branch: Some(info.run_branch.clone()), - meta_branch: Some(crate::git::MetadataStore::branch_name( - &options.run_options.run_id, - )), + meta_branch: Some(MetadataStore::branch_name(&options.run_options.run_id)), }); if options.run_options.base_branch.is_none() { options.run_options.base_branch = info.base_branch; @@ -828,7 +827,7 @@ pub async fn initialize( } for (phase, commands) in &options.lifecycle.devcontainer_phases { - crate::devcontainer_bridge::run_devcontainer_lifecycle( + run_devcontainer_lifecycle( sandbox.as_ref(), &options.emitter, phase, diff --git a/lib/crates/fabro-workflows/src/pipeline/parse.rs b/lib/crates/fabro-workflows/src/pipeline/parse.rs index 3d80704fc..08f18cc0c 100644 --- a/lib/crates/fabro-workflows/src/pipeline/parse.rs +++ b/lib/crates/fabro-workflows/src/pipeline/parse.rs @@ -1,3 +1,5 @@ +use fabro_graphviz::parser; + use super::types::Parsed; use crate::error::FabroError; @@ -7,7 +9,7 @@ use crate::error::FabroError; /// /// Returns `FabroError::Parse` if the DOT source is invalid. pub fn parse(dot_source: &str) -> Result { - let graph = fabro_graphviz::parser::parse(dot_source)?; + let graph = parser::parse(dot_source)?; Ok(Parsed { graph, source: dot_source.to_string(), diff --git a/lib/crates/fabro-workflows/src/pipeline/persist.rs b/lib/crates/fabro-workflows/src/pipeline/persist.rs index 75bdfc6cf..a149ecbe1 100644 --- a/lib/crates/fabro-workflows/src/pipeline/persist.rs +++ b/lib/crates/fabro-workflows/src/pipeline/persist.rs @@ -1,7 +1,7 @@ use std::path::Path; use crate::error::FabroError; -use crate::records::RunRecordExt; +use crate::records::{RunRecord, RunRecordExt}; use super::types::{PersistOptions, Persisted, Validated}; @@ -35,7 +35,7 @@ pub fn persist(validated: Validated, mut options: PersistOptions) -> Result Result { - let run_record = crate::records::RunRecord::load(run_dir)?; + let run_record = RunRecord::load(run_dir)?; let graph = run_record.graph.clone(); let source = match std::fs::read_to_string(run_dir.join(GRAPH_FILE_NAME)) { Ok(source) => source, diff --git a/lib/crates/fabro-workflows/src/pipeline/pull_request.rs b/lib/crates/fabro-workflows/src/pipeline/pull_request.rs index 8ff990ecf..a260317e2 100644 --- a/lib/crates/fabro-workflows/src/pipeline/pull_request.rs +++ b/lib/crates/fabro-workflows/src/pipeline/pull_request.rs @@ -5,12 +5,16 @@ use serde::{Deserialize, Serialize}; use tracing::{debug, info}; use fabro_github::{self as github_app, ssh_url_to_https, GitHubAppCredentials}; +use fabro_graphviz::parser; +use fabro_llm::generate::{generate, GenerateParams}; use fabro_retro::RetroExt; +use fabro_util::text::strip_goal_decoration; use crate::event::{EventEmitter, RunNoticeLevel, WorkflowRunEvent}; -use crate::outcome::StageStatus; +use crate::outcome::{format_cost as outcome_format_cost, StageStatus}; use crate::records::{Conclusion, ConclusionExt, RunRecord, RunRecordExt}; use fabro_retro::retro::Retro; +use tokio::fs::read_to_string; use super::types::{Concluded, Finalized, PullRequestOptions}; @@ -38,7 +42,7 @@ impl PullRequestRecord { /// /// Uses the first line, truncated to 120 characters for readability. fn pr_title_from_goal(goal: &str) -> String { - let stripped = fabro_util::text::strip_goal_decoration(goal); + let stripped = strip_goal_decoration(goal); if stripped.chars().count() > 120 { let truncated: String = stripped.chars().take(119).collect(); format!("{truncated}…") @@ -60,7 +64,7 @@ fn truncate_pr_body(body: &str) -> String { /// Format an optional cost as `$X.XX` or an en-dash when absent. fn format_cost(cost: Option) -> String { - cost.map(crate::outcome::format_cost) + cost.map(outcome_format_cost) .unwrap_or_else(|| "\u{2013}".to_string()) } @@ -205,7 +209,7 @@ fn format_arc_details_section( /// Parse a DOT source string to extract graph name, node count, and edge count. fn parse_dot_summary(dot: &str) -> (String, usize, usize) { - match fabro_graphviz::parser::parse(dot) { + match parser::parse(dot) { Ok(graph) => ( format!("{}.fabro", graph.name), graph.nodes.len(), @@ -362,11 +366,9 @@ pub async fn build_pr_body( format!("Goal: {goal}\n\nDiff:\n```\n{truncated_diff}\n```") }; - let params = fabro_llm::generate::GenerateParams::new(model) - .system(system) - .prompt(prompt); + let params = GenerateParams::new(model).system(system).prompt(prompt); - let result = fabro_llm::generate::generate(params) + let result = generate(params) .await .map_err(|e| format!("LLM generation failed: {e}"))?; @@ -498,7 +500,7 @@ pub async fn pull_request(concluded: Concluded, options: &PullRequestOptions) -> result.status, StageStatus::Success | StageStatus::PartialSuccess ) { - let diff = tokio::fs::read_to_string(options.run_dir.join("final.patch")) + let diff = read_to_string(options.run_dir.join("final.patch")) .await .unwrap_or_default(); if let (Some(base_branch), Some(run_branch), Some(creds), Some(origin)) = ( diff --git a/lib/crates/fabro-workflows/src/pipeline/retro.rs b/lib/crates/fabro-workflows/src/pipeline/retro.rs index 2404bb9de..e143c3cbf 100644 --- a/lib/crates/fabro-workflows/src/pipeline/retro.rs +++ b/lib/crates/fabro-workflows/src/pipeline/retro.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use fabro_agent::SessionEvent; -use fabro_retro::retro::Retro; +use fabro_retro::retro::{derive_retro, extract_stage_durations, Retro}; +use fabro_retro::retro_agent::{dry_run_narrative, run_retro_agent}; use fabro_retro::RetroExt; use crate::event::WorkflowRunEvent; @@ -25,8 +26,8 @@ pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option { }; let completed_stages = crate::build_completed_stages(&cp, options.failed); - let stage_durations = fabro_retro::retro::extract_stage_durations(&options.run_dir); - let mut retro = fabro_retro::retro::derive_retro( + let stage_durations = extract_stage_durations(&options.run_dir); + let mut retro = derive_retro( &options.run_id, &options.workflow_name, &options.goal, @@ -45,7 +46,7 @@ pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option { } let narrative_result = if dry_run { - Ok(fabro_retro::retro_agent::dry_run_narrative()) + Ok(dry_run_narrative()) } else if let Some(client) = options.llm_client.as_ref() { let emitter_clone = options.emitter.clone(); let event_callback: Option> = @@ -70,7 +71,7 @@ pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option { } }) }); - fabro_retro::retro_agent::run_retro_agent( + run_retro_agent( &options.sandbox, &options.run_dir, client, diff --git a/lib/crates/fabro-workflows/src/pipeline/types.rs b/lib/crates/fabro-workflows/src/pipeline/types.rs index 5ada5c586..1b1a47829 100644 --- a/lib/crates/fabro-workflows/src/pipeline/types.rs +++ b/lib/crates/fabro-workflows/src/pipeline/types.rs @@ -20,9 +20,14 @@ use fabro_validate::Diagnostic; use crate::context::Context; use crate::error::FabroError; use crate::event::EventEmitter; +use crate::handler::HandlerRegistry; use crate::outcome::Outcome; use crate::records::{Checkpoint, Conclusion, RunRecord}; -use crate::run_options::{LifecycleOptions, RunOptions}; +use crate::run_options::{GitCheckpointOptions, LifecycleOptions, RunOptions}; +use crate::transforms::Transform; +use fabro_config::run::PullRequestSettings; +use fabro_llm::client::Client; +use fabro_retro::retro::Retro; use fabro_validate::Severity; /// Output of the PARSE phase. @@ -262,9 +267,9 @@ pub struct InitOptions { pub hooks: fabro_hooks::HookConfig, pub sandbox_env: SandboxEnvSpec, pub devcontainer: Option, - pub git: Option, + pub git: Option, pub worktree_mode: Option, - pub registry_override: Option>, + pub registry_override: Option>, pub checkpoint: Option, pub seed_context: Option, } @@ -279,11 +284,11 @@ pub struct Initialized { pub(crate) seed_context: Option, pub emitter: Arc, pub sandbox: Arc, - pub registry: Arc, + pub registry: Arc, pub hook_runner: Option>, pub env: HashMap, pub dry_run: bool, - pub llm_client: Option, + pub llm_client: Option, pub model: String, pub provider: Provider, } @@ -299,7 +304,7 @@ pub struct Executed { pub sandbox: Arc, pub duration_ms: u64, pub final_context: Context, - pub llm_client: Option, + pub llm_client: Option, pub model: String, pub provider: Provider, } @@ -314,7 +319,7 @@ pub struct Retroed { pub emitter: Arc, pub sandbox: Arc, pub duration_ms: u64, - pub retro: Option, + pub retro: Option, } /// Output of the FINALIZE phase. @@ -342,7 +347,7 @@ pub struct Finalized { /// Options for the TRANSFORM phase. pub struct TransformOptions { pub base_dir: Option, - pub custom_transforms: Vec>, + pub custom_transforms: Vec>, } /// Options for the RETRO phase. @@ -356,7 +361,7 @@ pub struct RetroOptions { pub failed: bool, pub run_duration_ms: u64, pub enabled: bool, - pub llm_client: Option, + pub llm_client: Option, pub provider: Provider, pub model: String, } @@ -374,7 +379,7 @@ pub struct FinalizeOptions { /// Options for the PULL_REQUEST phase. pub struct PullRequestOptions { pub run_dir: PathBuf, - pub pr_config: Option, + pub pr_config: Option, pub github_app: Option, pub origin_url: Option, pub model: String, diff --git a/lib/crates/fabro-workflows/src/records/checkpoint.rs b/lib/crates/fabro-workflows/src/records/checkpoint.rs index 56b64f0f0..a79781ba5 100644 --- a/lib/crates/fabro-workflows/src/records/checkpoint.rs +++ b/lib/crates/fabro-workflows/src/records/checkpoint.rs @@ -4,7 +4,7 @@ use std::path::Path; pub use fabro_types::checkpoint::Checkpoint; use crate::context::Context; -use crate::error::FailureSignature; +use crate::error::{FailureSignature, Result as CrateResult}; use crate::outcome::Outcome; pub trait CheckpointExt { @@ -22,8 +22,8 @@ pub trait CheckpointExt { ) -> Self where Self: Sized; - fn save(&self, path: &Path) -> crate::error::Result<()>; - fn load(path: &Path) -> crate::error::Result + fn save(&self, path: &Path) -> CrateResult<()>; + fn load(path: &Path) -> CrateResult where Self: Sized; } @@ -55,12 +55,12 @@ impl CheckpointExt for Checkpoint { } } - fn save(&self, path: &Path) -> crate::error::Result<()> { + fn save(&self, path: &Path) -> CrateResult<()> { tracing::debug!(path = %path.display(), node = %self.current_node, "Saving checkpoint"); crate::save_json(self, path, "checkpoint") } - fn load(path: &Path) -> crate::error::Result { + fn load(path: &Path) -> CrateResult { tracing::debug!(path = %path.display(), "Loading checkpoint"); crate::load_json(path, "checkpoint") } diff --git a/lib/crates/fabro-workflows/src/records/conclusion.rs b/lib/crates/fabro-workflows/src/records/conclusion.rs index 98fc16d48..705c750be 100644 --- a/lib/crates/fabro-workflows/src/records/conclusion.rs +++ b/lib/crates/fabro-workflows/src/records/conclusion.rs @@ -2,19 +2,21 @@ use std::path::Path; pub use fabro_types::conclusion::{Conclusion, StageSummary}; +use crate::error::Result as CrateResult; + pub trait ConclusionExt { - fn save(&self, path: &Path) -> crate::error::Result<()>; - fn load(path: &Path) -> crate::error::Result + fn save(&self, path: &Path) -> CrateResult<()>; + fn load(path: &Path) -> CrateResult where Self: Sized; } impl ConclusionExt for Conclusion { - fn save(&self, path: &Path) -> crate::error::Result<()> { + fn save(&self, path: &Path) -> CrateResult<()> { crate::save_json(self, path, "conclusion") } - fn load(path: &Path) -> crate::error::Result { + fn load(path: &Path) -> CrateResult { crate::load_json(path, "conclusion") } } diff --git a/lib/crates/fabro-workflows/src/records/run.rs b/lib/crates/fabro-workflows/src/records/run.rs index 23bb7928a..849fd7aa5 100644 --- a/lib/crates/fabro-workflows/src/records/run.rs +++ b/lib/crates/fabro-workflows/src/records/run.rs @@ -2,14 +2,16 @@ use std::path::Path; pub use fabro_types::run::RunRecord; +use crate::error::Result as CrateResult; + const FILE_NAME: &str = "run.json"; pub trait RunRecordExt { fn file_name() -> &'static str where Self: Sized; - fn save(&self, run_dir: &Path) -> crate::error::Result<()>; - fn load(run_dir: &Path) -> crate::error::Result + fn save(&self, run_dir: &Path) -> CrateResult<()>; + fn load(run_dir: &Path) -> CrateResult where Self: Sized; fn workflow_name(&self) -> &str; @@ -23,11 +25,11 @@ impl RunRecordExt for RunRecord { FILE_NAME } - fn save(&self, run_dir: &Path) -> crate::error::Result<()> { + fn save(&self, run_dir: &Path) -> CrateResult<()> { crate::save_json(self, &run_dir.join(FILE_NAME), "run record") } - fn load(run_dir: &Path) -> crate::error::Result { + fn load(run_dir: &Path) -> CrateResult { crate::load_json(&run_dir.join(FILE_NAME), "run record") } diff --git a/lib/crates/fabro-workflows/src/records/start.rs b/lib/crates/fabro-workflows/src/records/start.rs index b2756fc8c..f27aabcb4 100644 --- a/lib/crates/fabro-workflows/src/records/start.rs +++ b/lib/crates/fabro-workflows/src/records/start.rs @@ -2,14 +2,16 @@ use std::path::Path; pub use fabro_types::start::StartRecord; +use crate::error::Result as CrateResult; + const FILE_NAME: &str = "start.json"; pub trait StartRecordExt { fn file_name() -> &'static str where Self: Sized; - fn save(&self, run_dir: &Path) -> crate::error::Result<()>; - fn load(run_dir: &Path) -> crate::error::Result + fn save(&self, run_dir: &Path) -> CrateResult<()>; + fn load(run_dir: &Path) -> CrateResult where Self: Sized; } @@ -19,11 +21,11 @@ impl StartRecordExt for StartRecord { FILE_NAME } - fn save(&self, run_dir: &Path) -> crate::error::Result<()> { + fn save(&self, run_dir: &Path) -> CrateResult<()> { crate::save_json(self, &run_dir.join(FILE_NAME), "start record") } - fn load(run_dir: &Path) -> crate::error::Result { + fn load(run_dir: &Path) -> CrateResult { crate::load_json(&run_dir.join(FILE_NAME), "start record") } } diff --git a/lib/crates/fabro-workflows/src/run_dir.rs b/lib/crates/fabro-workflows/src/run_dir.rs index 24ff6fc0c..f50ea8bd9 100644 --- a/lib/crates/fabro-workflows/src/run_dir.rs +++ b/lib/crates/fabro-workflows/src/run_dir.rs @@ -5,16 +5,13 @@ use fabro_types::NodeStatusRecord; use crate::context::Context; use crate::outcome::{Outcome, OutcomeExt}; -use crate::records::StartRecordExt; +use crate::records::{StartRecord, StartRecordExt}; use crate::run_options::RunOptions; /// Write start.json at the start of a workflow run. Returns the StartRecord. -pub(crate) fn write_start_record( - run_dir: &Path, - settings: &RunOptions, -) -> crate::records::StartRecord { +pub(crate) fn write_start_record(run_dir: &Path, settings: &RunOptions) -> StartRecord { let git_state = settings.git.as_ref(); - let record = crate::records::StartRecord { + let record = StartRecord { run_id: settings.run_id.clone(), start_time: Utc::now(), run_branch: git_state.and_then(|g| g.run_branch.clone()), diff --git a/lib/crates/fabro-workflows/src/run_lookup.rs b/lib/crates/fabro-workflows/src/run_lookup.rs index 38a5b5c50..9130ce757 100644 --- a/lib/crates/fabro-workflows/src/run_lookup.rs +++ b/lib/crates/fabro-workflows/src/run_lookup.rs @@ -5,7 +5,9 @@ use anyhow::{bail, Context, Result}; use chrono::{DateTime, Utc}; use serde::Serialize; -use crate::records::{ConclusionExt, RunRecord, RunRecordExt, StartRecord, StartRecordExt}; +use crate::records::{ + Conclusion, ConclusionExt, RunRecord, RunRecordExt, StartRecord, StartRecordExt, +}; use crate::run_status::{RunStatus, RunStatusRecord, RunStatusRecordExt, StatusReason}; #[derive(Debug, Clone, Serialize)] @@ -171,9 +173,7 @@ impl StatusInfo { fn read_status(run_dir: &Path) -> StatusInfo { if let Ok(record) = RunStatusRecord::load(&run_dir.join("status.json")) { if record.status.is_terminal() { - if let Ok(conclusion) = - crate::records::Conclusion::load(&run_dir.join("conclusion.json")) - { + if let Ok(conclusion) = Conclusion::load(&run_dir.join("conclusion.json")) { return StatusInfo { status: record.status, reason: record.reason, diff --git a/lib/crates/fabro-workflows/src/sandbox_git.rs b/lib/crates/fabro-workflows/src/sandbox_git.rs index 39de405a4..566205c45 100644 --- a/lib/crates/fabro-workflows/src/sandbox_git.rs +++ b/lib/crates/fabro-workflows/src/sandbox_git.rs @@ -4,6 +4,8 @@ use fabro_agent::Sandbox; use fabro_git_storage::trailerlink::{self, Trailer}; use crate::asset_snapshot; +use crate::git::{blocking_push_with_timeout, push_ref, GitAuthor}; +use fabro_sandbox::daytona::detect_repo_info; /// Captured git state for a workflow run, shared with handlers. #[derive(Debug, Clone)] @@ -13,7 +15,7 @@ pub struct GitState { pub run_branch: Option, pub meta_branch: Option, pub checkpoint_exclude_globs: Vec, - pub git_author: crate::git::GitAuthor, + pub git_author: GitAuthor, } pub const GIT_REMOTE: &str = "git -c maintenance.auto=0 -c gc.auto=0"; @@ -36,7 +38,7 @@ pub async fn git_checkpoint( completed_count: usize, shadow_sha: Option, exclude_globs: &[String], - author: &crate::git::GitAuthor, + author: &GitAuthor, ) -> std::result::Result { let mut all_excludes: Vec = asset_snapshot::EXCLUDE_DIRS .iter() @@ -133,7 +135,7 @@ pub async fn git_push_host( github_app: &Option, label: &str, ) -> bool { - let (origin_url, _) = match fabro_sandbox::daytona::detect_repo_info(repo_path) { + let (origin_url, _) = match detect_repo_info(repo_path) { Ok(info) => info, Err(e) => { tracing::warn!(error = %e, label, "Cannot detect origin for push"); @@ -158,10 +160,8 @@ pub async fn git_push_host( let rp = repo_path.to_path_buf(); let refspec_owned = refspec.to_string(); - let result = crate::git::blocking_push_with_timeout(60, move || { - crate::git::push_ref(&rp, &push_url, &refspec_owned) - }) - .await; + let result = + blocking_push_with_timeout(60, move || push_ref(&rp, &push_url, &refspec_owned)).await; match result { Ok(()) => { tracing::info!(label, "Pushed to origin"); diff --git a/lib/crates/fabro-workflows/src/test_support.rs b/lib/crates/fabro-workflows/src/test_support.rs index 928f92591..cb5e4ce31 100644 --- a/lib/crates/fabro-workflows/src/test_support.rs +++ b/lib/crates/fabro-workflows/src/test_support.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::sync::Arc; use fabro_agent::Sandbox; +use fabro_graphviz::graph::Graph as GvGraph; use crate::error::Result; use crate::event::EventEmitter; @@ -22,7 +23,7 @@ fn initialized( registry: HandlerRegistry, emitter: Arc, sandbox: Arc, - graph: &fabro_graphviz::graph::Graph, + graph: &GvGraph, run_options: &RunOptions, options: InitializedOptions, ) -> Initialized { @@ -49,7 +50,7 @@ pub async fn run_graph( registry: HandlerRegistry, emitter: Arc, sandbox: Arc, - graph: &fabro_graphviz::graph::Graph, + graph: &GvGraph, run_options: &RunOptions, ) -> Result { let executed = pipeline::execute(initialized( @@ -72,7 +73,7 @@ pub async fn run_graph_with_hooks( registry: HandlerRegistry, emitter: Arc, sandbox: Arc, - graph: &fabro_graphviz::graph::Graph, + graph: &GvGraph, run_options: &RunOptions, hook_runner: Arc, env: Option>, @@ -97,7 +98,7 @@ pub async fn run_graph_from_checkpoint( registry: HandlerRegistry, emitter: Arc, sandbox: Arc, - graph: &fabro_graphviz::graph::Graph, + graph: &GvGraph, run_options: &RunOptions, checkpoint: &Checkpoint, ) -> Result { @@ -137,11 +138,7 @@ impl WorkflowRunner { } } - pub async fn run( - &self, - graph: &fabro_graphviz::graph::Graph, - run_options: &RunOptions, - ) -> Result { + pub async fn run(&self, graph: &GvGraph, run_options: &RunOptions) -> Result { let registry = self .registry .lock() @@ -160,7 +157,7 @@ impl WorkflowRunner { pub async fn run_from_checkpoint( &self, - graph: &fabro_graphviz::graph::Graph, + graph: &GvGraph, run_options: &RunOptions, checkpoint: &Checkpoint, ) -> Result {