diff --git a/lib/crates/fabro-agent/src/cli.rs b/lib/crates/fabro-agent/src/cli.rs index e134ce799..bb43dfe05 100644 --- a/lib/crates/fabro-agent/src/cli.rs +++ b/lib/crates/fabro-agent/src/cli.rs @@ -3,8 +3,8 @@ use std::path::PathBuf; use std::sync::{Arc, Mutex}; use clap::{Args, Parser}; +use fabro_llm::Error as LlmError; 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}; @@ -303,7 +303,7 @@ struct DebugMiddleware { #[async_trait::async_trait] impl Middleware for DebugMiddleware { #[allow(clippy::print_stderr)] - async fn handle_complete(&self, request: Request, next: NextFn) -> Result { + async fn handle_complete(&self, request: Request, next: NextFn) -> Result { let s = self.styles; eprintln!( "{}", @@ -333,7 +333,7 @@ impl Middleware for DebugMiddleware { &self, request: Request, next: NextStreamFn, - ) -> Result { + ) -> Result { next(request).await } } @@ -346,7 +346,7 @@ struct VerboseMiddleware { #[async_trait::async_trait] impl Middleware for VerboseMiddleware { #[allow(clippy::print_stderr)] - async fn handle_complete(&self, request: Request, next: NextFn) -> Result { + async fn handle_complete(&self, request: Request, next: NextFn) -> Result { let s = self.styles; eprintln!( "{}\n{}", @@ -368,7 +368,7 @@ impl Middleware for VerboseMiddleware { &self, request: Request, next: NextStreamFn, - ) -> Result { + ) -> Result { next(request).await } } diff --git a/lib/crates/fabro-agent/src/compaction.rs b/lib/crates/fabro-agent/src/compaction.rs index 4b9a6de35..3f245266a 100644 --- a/lib/crates/fabro-agent/src/compaction.rs +++ b/lib/crates/fabro-agent/src/compaction.rs @@ -5,7 +5,7 @@ use fabro_llm::types::{Message, Request}; use tracing::debug; use crate::agent_profile::AgentProfile; -use crate::error::AgentError; +use crate::error::Error; use crate::event::Emitter; use crate::file_tracker::FileTracker; use crate::history::History; @@ -28,18 +28,21 @@ pub fn check_context_usage( let threshold = context_window * threshold_percent / 100; if estimated_tokens > threshold { - emitter.emit(session_id.to_owned(), AgentEvent::Warning { - kind: "context_window".into(), - message: format!( - "Context window usage: {}%", - estimated_tokens * 100 / context_window - ), - details: serde_json::json!({ - "estimated_tokens": estimated_tokens, - "context_window_size": context_window, - "usage_percent": estimated_tokens * 100 / context_window, - }), - }); + emitter.emit( + session_id.to_owned(), + AgentEvent::Warning { + kind: "context_window".into(), + message: format!( + "Context window usage: {}%", + estimated_tokens * 100 / context_window + ), + details: serde_json::json!({ + "estimated_tokens": estimated_tokens, + "context_window_size": context_window, + "usage_percent": estimated_tokens * 100 / context_window, + }), + }, + ); true } else { false @@ -58,15 +61,18 @@ pub async fn compact_context( preserve_count: usize, emitter: &Emitter, session_id: &str, -) -> Result<(), AgentError> { +) -> Result<(), Error> { let estimated_tokens = estimate_token_count(system_prompt, history); let context_window = provider_profile.context_window_size(); let original_turn_count = history.turns().len(); - emitter.emit(session_id.to_owned(), AgentEvent::CompactionStarted { - estimated_tokens, - context_window_size: context_window, - }); + emitter.emit( + session_id.to_owned(), + AgentEvent::CompactionStarted { + estimated_tokens, + context_window_size: context_window, + }, + ); // Determine turns to summarize if original_turn_count <= preserve_count { @@ -102,31 +108,31 @@ function names, error messages, and exact values. Omit pleasantries and conversa ); let summary_request = Request { - model: provider_profile.model().to_string(), - messages: vec![ + model: provider_profile.model().to_string(), + messages: vec![ Message::system(summarization_prompt), Message::user(format!( "Here is the conversation to summarize:\n\n{rendered}" )), ], - provider: Some(provider_profile.provider().as_str().to_string()), - tools: None, - tool_choice: None, - response_format: None, - temperature: Some(0.0), - top_p: None, - max_tokens: Some(4096), - stop_sequences: None, + provider: Some(provider_profile.provider().as_str().to_string()), + tools: None, + tool_choice: None, + response_format: None, + temperature: Some(0.0), + top_p: None, + max_tokens: Some(4096), + stop_sequences: None, reasoning_effort: None, - speed: None, - metadata: None, + speed: None, + metadata: None, provider_options: None, }; let response = llm_client .complete(&summary_request) .await - .map_err(AgentError::Llm)?; + .map_err(Error::Llm)?; let summary_text = response.text(); debug!( @@ -141,12 +147,15 @@ Build on their progress — do not repeat completed steps.\n\n{summary_text}" history.compact(preserve_count, summary_content); - emitter.emit(session_id.to_owned(), AgentEvent::CompactionCompleted { - original_turn_count, - preserved_turn_count: preserve_count, - summary_token_estimate, - tracked_file_count: file_tracker.file_count(), - }); + emitter.emit( + session_id.to_owned(), + AgentEvent::CompactionCompleted { + original_turn_count, + preserved_turn_count: preserve_count, + summary_token_estimate, + tracked_file_count: file_tracker.file_count(), + }, + ); Ok(()) } @@ -259,27 +268,27 @@ mod tests { fn render_turns_produces_labeled_text() { let turns = vec![ Turn::User { - content: "Hello".into(), + content: "Hello".into(), timestamp: SystemTime::now(), }, Turn::Assistant { - content: "Let me check".into(), - tool_calls: vec![ToolCall::new( + content: "Let me check".into(), + tool_calls: vec![ToolCall::new( "c1", "read_file", serde_json::json!({"path": "foo.rs"}), )], provider_parts: vec![], - usage: Box::new(TokenCounts::default()), - response_id: "resp_1".into(), - timestamp: SystemTime::now(), + usage: Box::new(TokenCounts::default()), + response_id: "resp_1".into(), + timestamp: SystemTime::now(), }, Turn::ToolResults { - results: vec![ToolResult { - tool_call_id: "c1".into(), - content: serde_json::json!("file contents here"), - is_error: false, - image_data: None, + results: vec![ToolResult { + tool_call_id: "c1".into(), + content: serde_json::json!("file contents here"), + is_error: false, + image_data: None, image_media_type: None, }], timestamp: SystemTime::now(), @@ -298,11 +307,11 @@ mod tests { fn render_turns_truncates_long_tool_output() { let long_output = "x".repeat(1000); let turns = vec![Turn::ToolResults { - results: vec![ToolResult { - tool_call_id: "c1".into(), - content: serde_json::json!(long_output), - is_error: false, - image_data: None, + results: vec![ToolResult { + tool_call_id: "c1".into(), + content: serde_json::json!(long_output), + is_error: false, + image_data: None, image_media_type: None, }], timestamp: SystemTime::now(), @@ -317,7 +326,7 @@ mod tests { fn estimate_token_count_basic() { let mut history = History::default(); history.push(Turn::User { - content: "Hello world".into(), // 11 chars + content: "Hello world".into(), // 11 chars timestamp: SystemTime::now(), }); // system_prompt = "test" (4 chars) + 11 chars = 15 chars / 4 = 3 tokens @@ -339,7 +348,7 @@ mod tests { let mut history = History::default(); // Push enough content to exceed a tiny context window history.push(Turn::User { - content: "x".repeat(1000), + content: "x".repeat(1000), timestamp: SystemTime::now(), }); let emitter = Emitter::new(); diff --git a/lib/crates/fabro-agent/src/config.rs b/lib/crates/fabro-agent/src/config.rs index 6e2f626b6..344694361 100644 --- a/lib/crates/fabro-agent/src/config.rs +++ b/lib/crates/fabro-agent/src/config.rs @@ -216,9 +216,12 @@ mod tests { let approval: ToolApprovalFn = Arc::new(|_name, _args| Err("denied".to_string())); let adapter = ToolApprovalAdapter(approval); let decision = adapter.pre_tool_use("shell", &serde_json::json!({})).await; - assert_eq!(decision, ToolHookDecision::Block { - reason: "denied".to_string(), - }); + assert_eq!( + decision, + ToolHookDecision::Block { + reason: "denied".to_string(), + } + ); } #[tokio::test] diff --git a/lib/crates/fabro-agent/src/error.rs b/lib/crates/fabro-agent/src/error.rs index 892b451f1..13ce6aec9 100644 --- a/lib/crates/fabro-agent/src/error.rs +++ b/lib/crates/fabro-agent/src/error.rs @@ -37,7 +37,6 @@ pub enum Error { } pub type Result = std::result::Result; -pub type AgentError = Error; #[cfg(test)] mod tests { @@ -49,7 +48,7 @@ mod tests { fn agent_error_from_sdk_error() { let sdk_err = LlmError::Network { message: "connection refused".into(), - source: None, + source: None, }; let agent_err = Error::from(sdk_err); assert!(matches!(agent_err, Error::Llm(_))); @@ -92,7 +91,7 @@ mod tests { fn serde_roundtrip_llm_network() { let err = Error::Llm(LlmError::Network { message: "connection refused".into(), - source: None, + source: None, }); let json = serde_json::to_string(&err).unwrap(); let deserialized: Error = serde_json::from_str(&json).unwrap(); @@ -102,14 +101,14 @@ mod tests { #[test] fn serde_roundtrip_llm_provider() { let err = Error::Llm(LlmError::Provider { - kind: ProviderErrorKind::RateLimit, + kind: ProviderErrorKind::RateLimit, detail: Box::new(ProviderErrorDetail { - message: "too fast".into(), - provider: "openai".into(), + message: "too fast".into(), + provider: "openai".into(), status_code: Some(429), - error_code: None, + error_code: None, retry_after: Some(2.0), - raw: None, + raw: None, }), }); let json = serde_json::to_string(&err).unwrap(); @@ -156,7 +155,7 @@ mod tests { let errors: Vec = vec![ Error::Llm(LlmError::Network { message: "refused".into(), - source: None, + source: None, }), Error::SessionClosed, Error::InvalidState("reason".into()), @@ -174,7 +173,7 @@ mod tests { fn serde_tag_format_llm() { let err = Error::Llm(LlmError::Network { message: "refused".into(), - source: None, + source: None, }); let json = serde_json::to_string(&err).unwrap(); let v: serde_json::Value = serde_json::from_str(&json).unwrap(); diff --git a/lib/crates/fabro-agent/src/event.rs b/lib/crates/fabro-agent/src/event.rs index ae6febfec..a7a0f14c6 100644 --- a/lib/crates/fabro-agent/src/event.rs +++ b/lib/crates/fabro-agent/src/event.rs @@ -47,23 +47,29 @@ impl Default for Emitter { #[cfg(test)] mod tests { use super::*; - use crate::error::AgentError; + use crate::error::Error; #[tokio::test] async fn emit_and_receive_event() { let emitter = Emitter::new(); let mut receiver = emitter.subscribe(); - emitter.emit("sess-1".into(), AgentEvent::SessionStarted { - provider: Some("anthropic".into()), - model: Some("claude-opus".into()), - }); + emitter.emit( + "sess-1".into(), + AgentEvent::SessionStarted { + provider: Some("anthropic".into()), + model: Some("claude-opus".into()), + }, + ); let event = receiver.recv().await.unwrap(); - assert!(matches!(event.event, AgentEvent::SessionStarted { - provider: Some(_), - model: Some(_), - })); + assert!(matches!( + event.event, + AgentEvent::SessionStarted { + provider: Some(_), + model: Some(_), + } + )); assert_eq!(event.session_id, "sess-1"); assert_eq!(event.parent_session_id, None); } @@ -73,9 +79,12 @@ mod tests { let emitter = Emitter::new(); let mut receiver = emitter.subscribe(); - emitter.emit("sess-2".into(), AgentEvent::Error { - error: AgentError::ToolExecution("something went wrong".into()), - }); + emitter.emit( + "sess-2".into(), + AgentEvent::Error { + error: Error::ToolExecution("something went wrong".into()), + }, + ); let event = receiver.recv().await.unwrap(); assert!( @@ -105,9 +114,12 @@ mod tests { #[test] fn emit_without_subscribers_does_not_panic() { let emitter = Emitter::new(); - emitter.emit("sess-4".into(), AgentEvent::Error { - error: AgentError::ToolExecution("test".into()), - }); + emitter.emit( + "sess-4".into(), + AgentEvent::Error { + error: Error::ToolExecution("test".into()), + }, + ); } #[test] @@ -122,21 +134,24 @@ mod tests { let mut receiver = emitter.subscribe(); emitter.forward(SessionEvent { - event: AgentEvent::SessionStarted { + event: AgentEvent::SessionStarted { provider: Some("anthropic".into()), - model: Some("claude-opus".into()), + model: Some("claude-opus".into()), }, - timestamp: SystemTime::now(), - session_id: "child".into(), + timestamp: SystemTime::now(), + session_id: "child".into(), parent_session_id: Some("parent".into()), }); let event = receiver.recv().await.unwrap(); assert_eq!(event.session_id, "child"); assert_eq!(event.parent_session_id.as_deref(), Some("parent")); - assert!(matches!(event.event, AgentEvent::SessionStarted { - provider: Some(_), - model: Some(_), - })); + assert!(matches!( + event.event, + AgentEvent::SessionStarted { + provider: Some(_), + model: Some(_), + } + )); } } diff --git a/lib/crates/fabro-agent/src/file_tracker.rs b/lib/crates/fabro-agent/src/file_tracker.rs index 10d6e274b..ed8cc04fd 100644 --- a/lib/crates/fabro-agent/src/file_tracker.rs +++ b/lib/crates/fabro-agent/src/file_tracker.rs @@ -5,9 +5,9 @@ use fabro_llm::types::{ToolCall, ToolResult}; #[derive(Debug, Clone, Copy, Default)] struct FileOps { - read: bool, + read: bool, written: bool, - edited: bool, + edited: bool, } #[derive(Debug, Default)] diff --git a/lib/crates/fabro-agent/src/history.rs b/lib/crates/fabro-agent/src/history.rs index 74593dbc2..cf439e20c 100644 --- a/lib/crates/fabro-agent/src/history.rs +++ b/lib/crates/fabro-agent/src/history.rs @@ -26,7 +26,7 @@ impl History { let extracted_user_messages = extract_recent_user_messages(discarded, COMPACTION_USER_MESSAGE_TOKEN_BUDGET); self.turns.push(Turn::System { - content: summary, + content: summary, timestamp: std::time::SystemTime::now(), }); self.turns.extend(extracted_user_messages); @@ -72,9 +72,9 @@ impl History { parts.push(ContentPart::ToolCall(tc.clone())); } Message { - role: Role::Assistant, - content: parts, - name: None, + role: Role::Assistant, + content: parts, + name: None, tool_call_id: None, } } @@ -94,9 +94,9 @@ impl History { } Turn::System { content, .. } => Message::system(content), Turn::Steering { content, .. } => Message { - role: Role::User, - content: vec![ContentPart::text(content)], - name: None, + role: Role::User, + content: vec![ContentPart::text(content)], + name: None, tool_call_id: None, }, }) @@ -149,7 +149,7 @@ mod tests { let mut history = History::default(); for i in 0..8 { history.push(Turn::User { - content: format!("msg {i}"), + content: format!("msg {i}"), timestamp: SystemTime::now(), }); } @@ -163,7 +163,7 @@ mod tests { let mut history = History::default(); for i in 0..3 { history.push(Turn::User { - content: format!("msg {i}"), + content: format!("msg {i}"), timestamp: SystemTime::now(), }); } @@ -176,7 +176,7 @@ mod tests { let mut history = History::default(); for i in 0..8 { history.push(Turn::User { - content: format!("msg {i}"), + content: format!("msg {i}"), timestamp: SystemTime::now(), }); } @@ -199,7 +199,7 @@ mod tests { let mut history = History::default(); for i in 0..6 { history.push(Turn::User { - content: format!("msg {i}"), + content: format!("msg {i}"), timestamp: SystemTime::now(), }); } @@ -220,7 +220,7 @@ mod tests { fn user_turn_maps_to_user_message() { let mut history = History::default(); history.push(Turn::User { - content: "Hello".into(), + content: "Hello".into(), timestamp: SystemTime::now(), }); let messages = history.convert_to_messages(); @@ -233,12 +233,12 @@ mod tests { fn assistant_turn_maps_to_assistant_message() { let mut history = History::default(); history.push(Turn::Assistant { - content: "Hi there".into(), - tool_calls: vec![], + content: "Hi there".into(), + tool_calls: vec![], provider_parts: vec![], - usage: Box::new(TokenCounts::default()), - response_id: "resp_1".into(), - timestamp: SystemTime::now(), + usage: Box::new(TokenCounts::default()), + response_id: "resp_1".into(), + timestamp: SystemTime::now(), }); let messages = history.convert_to_messages(); assert_eq!(messages.len(), 1); @@ -251,12 +251,12 @@ mod tests { let mut history = History::default(); let tc = ToolCall::new("call_1", "read_file", serde_json::json!({"path": "foo.rs"})); history.push(Turn::Assistant { - content: "Let me read that".into(), - tool_calls: vec![tc], + content: "Let me read that".into(), + tool_calls: vec![tc], provider_parts: vec![], - usage: Box::new(TokenCounts::default()), - response_id: "resp_2".into(), - timestamp: SystemTime::now(), + usage: Box::new(TokenCounts::default()), + response_id: "resp_2".into(), + timestamp: SystemTime::now(), }); let messages = history.convert_to_messages(); assert_eq!(messages[0].role, Role::Assistant); @@ -272,17 +272,17 @@ mod tests { fn assistant_turn_with_reasoning_in_provider_parts() { let mut history = History::default(); let thinking = ContentPart::Thinking(ThinkingData { - text: "Let me think about this...".into(), + text: "Let me think about this...".into(), signature: None, - redacted: false, + redacted: false, }); history.push(Turn::Assistant { - content: "The answer is 42".into(), - tool_calls: vec![], + content: "The answer is 42".into(), + tool_calls: vec![], provider_parts: vec![thinking], - usage: Box::new(TokenCounts::default()), - response_id: "resp_3".into(), - timestamp: SystemTime::now(), + usage: Box::new(TokenCounts::default()), + response_id: "resp_3".into(), + timestamp: SystemTime::now(), }); let messages = history.convert_to_messages(); let thinking_parts: Vec<_> = messages[0] @@ -297,17 +297,17 @@ mod tests { fn thinking_with_signature_preserved_via_provider_parts() { let mut history = History::default(); let thinking = ContentPart::Thinking(ThinkingData { - text: "Let me think...".into(), + text: "Let me think...".into(), signature: Some("sig_abc123".into()), - redacted: false, + redacted: false, }); history.push(Turn::Assistant { - content: "The answer".into(), - tool_calls: vec![], + content: "The answer".into(), + tool_calls: vec![], provider_parts: vec![thinking], - usage: Box::new(TokenCounts::default()), - response_id: "resp_4".into(), - timestamp: SystemTime::now(), + usage: Box::new(TokenCounts::default()), + response_id: "resp_4".into(), + timestamp: SystemTime::now(), }); let messages = history.convert_to_messages(); let thinking_parts: Vec<_> = messages[0] @@ -333,12 +333,12 @@ mod tests { }; let tc = ToolCall::new("call_1", "search", serde_json::json!({})); history.push(Turn::Assistant { - content: String::new(), - tool_calls: vec![tc], + content: String::new(), + tool_calls: vec![tc], provider_parts: vec![reasoning_item], - usage: Box::new(TokenCounts::default()), - response_id: "resp_1".into(), - timestamp: SystemTime::now(), + usage: Box::new(TokenCounts::default()), + response_id: "resp_1".into(), + timestamp: SystemTime::now(), }); let messages = history.convert_to_messages(); assert_eq!(messages.len(), 1); @@ -354,7 +354,7 @@ mod tests { let mut history = History::default(); let result = ToolResult::success("call_1", serde_json::json!("file contents here")); history.push(Turn::ToolResults { - results: vec![result], + results: vec![result], timestamp: SystemTime::now(), }); let messages = history.convert_to_messages(); @@ -367,7 +367,7 @@ mod tests { fn system_turn_maps_to_system_message() { let mut history = History::default(); history.push(Turn::System { - content: "You are a coding assistant".into(), + content: "You are a coding assistant".into(), timestamp: SystemTime::now(), }); let messages = history.convert_to_messages(); @@ -380,7 +380,7 @@ mod tests { fn steering_turn_maps_to_user_message() { let mut history = History::default(); history.push(Turn::Steering { - content: "Focus on the main task".into(), + content: "Focus on the main task".into(), timestamp: SystemTime::now(), }); let messages = history.convert_to_messages(); @@ -394,17 +394,17 @@ mod tests { let mut history = History::default(); assert_eq!(history.turns().len(), 0); history.push(Turn::User { - content: "First".into(), + content: "First".into(), timestamp: SystemTime::now(), }); assert_eq!(history.turns().len(), 1); history.push(Turn::Assistant { - content: "Second".into(), - tool_calls: vec![], + content: "Second".into(), + tool_calls: vec![], provider_parts: vec![], - usage: Box::new(TokenCounts::default()), - response_id: "resp_1".into(), - timestamp: SystemTime::now(), + usage: Box::new(TokenCounts::default()), + response_id: "resp_1".into(), + timestamp: SystemTime::now(), }); assert_eq!(history.turns().len(), 2); } @@ -413,31 +413,31 @@ mod tests { fn round_trip_preserves_content() { let mut history = History::default(); history.push(Turn::User { - content: "Hello".into(), + content: "Hello".into(), timestamp: SystemTime::now(), }); history.push(Turn::Assistant { - content: "Hi".into(), - tool_calls: vec![ToolCall::new( + content: "Hi".into(), + tool_calls: vec![ToolCall::new( "c1", "shell", serde_json::json!({"cmd": "ls"}), )], provider_parts: vec![ContentPart::Thinking(ThinkingData { - text: "thinking...".into(), + text: "thinking...".into(), signature: None, - redacted: false, + redacted: false, })], - usage: Box::new(TokenCounts { + usage: Box::new(TokenCounts { input_tokens: 10, output_tokens: 5, ..Default::default() }), - response_id: "resp_1".into(), - timestamp: SystemTime::now(), + response_id: "resp_1".into(), + timestamp: SystemTime::now(), }); history.push(Turn::ToolResults { - results: vec![ToolResult::success( + results: vec![ToolResult::success( "c1", serde_json::json!("file1.rs\nfile2.rs"), )], @@ -455,11 +455,11 @@ mod tests { fn compact_strips_openai_reasoning_from_preserved_turns() { let mut history = History::default(); history.push(Turn::User { - content: "old msg".into(), + content: "old msg".into(), timestamp: SystemTime::now(), }); history.push(Turn::User { - content: "recent msg".into(), + content: "recent msg".into(), timestamp: SystemTime::now(), }); let reasoning = ContentPart::Other { @@ -468,12 +468,12 @@ mod tests { }; let tc = ToolCall::new("call_1", "search", serde_json::json!({})); history.push(Turn::Assistant { - content: "response".into(), - tool_calls: vec![tc], + content: "response".into(), + tool_calls: vec![tc], provider_parts: vec![reasoning], - usage: Box::new(TokenCounts::default()), - response_id: "resp_1".into(), - timestamp: SystemTime::now(), + usage: Box::new(TokenCounts::default()), + response_id: "resp_1".into(), + timestamp: SystemTime::now(), }); history.compact(2, "Summary".into()); @@ -503,25 +503,25 @@ mod tests { fn compact_preserves_anthropic_thinking_blocks() { let mut history = History::default(); history.push(Turn::User { - content: "old msg".into(), + content: "old msg".into(), timestamp: SystemTime::now(), }); history.push(Turn::User { - content: "recent msg".into(), + content: "recent msg".into(), timestamp: SystemTime::now(), }); let thinking = ContentPart::Thinking(ThinkingData { - text: "deep thought".into(), + text: "deep thought".into(), signature: Some("sig_xyz".into()), - redacted: false, + redacted: false, }); history.push(Turn::Assistant { - content: "answer".into(), - tool_calls: vec![], + content: "answer".into(), + tool_calls: vec![], provider_parts: vec![thinking], - usage: Box::new(TokenCounts::default()), - response_id: "resp_1".into(), - timestamp: SystemTime::now(), + usage: Box::new(TokenCounts::default()), + response_id: "resp_1".into(), + timestamp: SystemTime::now(), }); history.compact(2, "Summary".into()); @@ -545,21 +545,21 @@ mod tests { fn compact_strips_reasoning_from_all_preserved_assistant_turns() { let mut history = History::default(); history.push(Turn::User { - content: "old msg".into(), + content: "old msg".into(), timestamp: SystemTime::now(), }); // Two assistant turns that will both be preserved for i in 0..2 { history.push(Turn::Assistant { - content: format!("response {i}"), - tool_calls: vec![], + content: format!("response {i}"), + tool_calls: vec![], provider_parts: vec![ContentPart::Other { kind: ContentPart::OPENAI_REASONING.into(), data: serde_json::json!({"type": "reasoning", "id": format!("rs_{i}")}), }], - usage: Box::new(TokenCounts::default()), - response_id: format!("resp_{i}"), - timestamp: SystemTime::now(), + usage: Box::new(TokenCounts::default()), + response_id: format!("resp_{i}"), + timestamp: SystemTime::now(), }); } @@ -579,19 +579,19 @@ mod tests { fn extract_recent_user_messages_collects_in_chronological_order() { let turns = vec![ Turn::User { - content: "first".into(), + content: "first".into(), timestamp: SystemTime::now(), }, Turn::Assistant { - content: "reply".into(), - tool_calls: vec![], + content: "reply".into(), + tool_calls: vec![], provider_parts: vec![], - usage: Box::new(TokenCounts::default()), - response_id: "r1".into(), - timestamp: SystemTime::now(), + usage: Box::new(TokenCounts::default()), + response_id: "r1".into(), + timestamp: SystemTime::now(), }, Turn::User { - content: "second".into(), + content: "second".into(), timestamp: SystemTime::now(), }, ]; @@ -605,11 +605,11 @@ mod tests { fn extract_recent_user_messages_respects_token_budget() { let turns = vec![ Turn::User { - content: "a".repeat(100), + content: "a".repeat(100), timestamp: SystemTime::now(), }, Turn::User { - content: "b".repeat(100), + content: "b".repeat(100), timestamp: SystemTime::now(), }, ]; @@ -624,19 +624,19 @@ mod tests { fn compact_extracts_only_user_turns_from_discarded() { let mut history = History::default(); history.push(Turn::User { - content: "user msg".into(), + content: "user msg".into(), timestamp: SystemTime::now(), }); history.push(Turn::Assistant { - content: "assistant msg".into(), - tool_calls: vec![], + content: "assistant msg".into(), + tool_calls: vec![], provider_parts: vec![], - usage: Box::new(TokenCounts::default()), - response_id: "r1".into(), - timestamp: SystemTime::now(), + usage: Box::new(TokenCounts::default()), + response_id: "r1".into(), + timestamp: SystemTime::now(), }); history.push(Turn::User { - content: "preserved".into(), + content: "preserved".into(), timestamp: SystemTime::now(), }); diff --git a/lib/crates/fabro-agent/src/lib.rs b/lib/crates/fabro-agent/src/lib.rs index 3608c5137..68839f033 100644 --- a/lib/crates/fabro-agent/src/lib.rs +++ b/lib/crates/fabro-agent/src/lib.rs @@ -30,7 +30,7 @@ pub use agent_profile::AgentProfile; pub use config::{SessionOptions, ToolApprovalAdapter, ToolHookCallback, ToolHookDecision}; #[cfg(feature = "docker")] pub use docker_sandbox::{DockerSandbox, DockerSandboxOptions}; -pub use error::{AgentError, Error, InterruptReason, Result}; +pub use error::{Error, InterruptReason, Result}; pub use event::Emitter; pub use fabro_mcp::config::McpServerSettings; pub use history::History; diff --git a/lib/crates/fabro-agent/src/loop_detection.rs b/lib/crates/fabro-agent/src/loop_detection.rs index 926bd2a3c..2bb0d6554 100644 --- a/lib/crates/fabro-agent/src/loop_detection.rs +++ b/lib/crates/fabro-agent/src/loop_detection.rs @@ -103,12 +103,12 @@ mod tests { fn assistant_with_tool(name: &str, args: serde_json::Value) -> Turn { Turn::Assistant { - content: String::new(), - tool_calls: vec![ToolCall::new("call_1", name, args)], + content: String::new(), + tool_calls: vec![ToolCall::new("call_1", name, args)], provider_parts: vec![], - usage: Box::new(TokenCounts::default()), - response_id: "resp".into(), - timestamp: SystemTime::now(), + usage: Box::new(TokenCounts::default()), + response_id: "resp".into(), + timestamp: SystemTime::now(), } } @@ -266,15 +266,15 @@ mod tests { fn user_turns_are_ignored() { let mut history = History::default(); history.push(Turn::User { - content: "hello".into(), + content: "hello".into(), timestamp: SystemTime::now(), }); history.push(Turn::User { - content: "hello".into(), + content: "hello".into(), timestamp: SystemTime::now(), }); history.push(Turn::User { - content: "hello".into(), + content: "hello".into(), timestamp: SystemTime::now(), }); assert!(!detect_loop(&history, 10)); diff --git a/lib/crates/fabro-agent/src/mcp_integration.rs b/lib/crates/fabro-agent/src/mcp_integration.rs index 65d8a3fef..10daffc8a 100644 --- a/lib/crates/fabro-agent/src/mcp_integration.rs +++ b/lib/crates/fabro-agent/src/mcp_integration.rs @@ -18,11 +18,11 @@ pub fn make_mcp_tools(manager: &Arc) -> Vec, - pub is_git_repo: bool, - pub current_date: String, - pub model: String, - pub knowledge_cutoff: String, - pub git_status_short: Option, + pub git_branch: Option, + pub is_git_repo: bool, + pub current_date: String, + pub model: String, + pub knowledge_cutoff: String, + pub git_status_short: Option, pub git_recent_commits: Option, } @@ -133,12 +133,12 @@ mod tests { fn env_context_block_with_extra_context() { let env = MockSandbox::linux(); let ctx = EnvContext { - git_branch: Some("main".into()), - is_git_repo: true, - current_date: "2026-02-20".into(), - model: "claude-opus-4-6".into(), - knowledge_cutoff: "May 2025".into(), - git_status_short: None, + git_branch: Some("main".into()), + is_git_repo: true, + current_date: "2026-02-20".into(), + model: "claude-opus-4-6".into(), + knowledge_cutoff: "May 2025".into(), + git_status_short: None, git_recent_commits: None, }; let block = build_env_context_block_with(&env, &ctx); diff --git a/lib/crates/fabro-agent/src/session.rs b/lib/crates/fabro-agent/src/session.rs index dc35a51bd..4995ebe04 100644 --- a/lib/crates/fabro-agent/src/session.rs +++ b/lib/crates/fabro-agent/src/session.rs @@ -3,13 +3,13 @@ use std::sync::{Arc, Mutex}; use std::time::SystemTime; use fabro_llm::client::Client; -use fabro_llm::error::{ProviderErrorKind, SdkError}; +use fabro_llm::error::ProviderErrorKind; use fabro_llm::generate::StreamAccumulator; use fabro_llm::provider::StreamEventStream; -use fabro_llm::retry; use fabro_llm::types::{ ContentPart, Message, ReasoningEffort, Request, RetryPolicy, StreamEvent, ToolChoice, }; +use fabro_llm::{Error as LlmError, retry}; use fabro_mcp::config::{McpServerSettings, McpTransport}; use fabro_mcp::connection_manager::McpConnectionManager; use futures::StreamExt; @@ -21,7 +21,7 @@ use tracing::{debug, info, warn}; use crate::agent_profile::AgentProfile; use crate::compaction::{check_context_usage, compact_context}; use crate::config::SessionOptions; -use crate::error::{AgentError, InterruptReason}; +use crate::error::{Error, InterruptReason}; use crate::event::Emitter; use crate::file_tracker::FileTracker; use crate::history::History; @@ -38,24 +38,24 @@ use crate::tool_execution::execute_tool_calls; use crate::types::{AgentEvent, SessionEvent, SessionState, Turn}; pub struct Session { - id: String, - config: SessionOptions, - history: History, - event_emitter: Emitter, - state: SessionState, - llm_client: Client, + id: String, + config: SessionOptions, + history: History, + event_emitter: Emitter, + state: SessionState, + llm_client: Client, provider_profile: Arc, - sandbox: Arc, - steering_queue: Arc>>, - followup_queue: Arc>>, - cancel_token: CancellationToken, + sandbox: Arc, + steering_queue: Arc>>, + followup_queue: Arc>>, + cancel_token: CancellationToken, interrupt_reason: Arc>>, - memory: Vec, - env_context: EnvContext, - skills: Vec, - system_prompt: String, - file_tracker: FileTracker, - tool_env: Option>, + memory: Vec, + env_context: EnvContext, + skills: Vec, + system_prompt: String, + file_tracker: FileTracker, + tool_env: Option>, subagent_manager: Option>>, } @@ -103,11 +103,13 @@ impl Session { /// Initialize session by discovering project docs and capturing environment /// context. Call before `process_input`. pub async fn initialize(&mut self) { - self.event_emitter - .emit(self.id.clone(), AgentEvent::SessionStarted { + self.event_emitter.emit( + self.id.clone(), + AgentEvent::SessionStarted { provider: Some(self.provider_profile.provider().to_string()), - model: Some(self.provider_profile.model().to_string()), - }); + model: Some(self.provider_profile.model().to_string()), + }, + ); let doc_root = self .config @@ -155,18 +157,22 @@ impl Session { for (server_name, result) in &results { match result { Ok(tool_count) => { - self.event_emitter - .emit(self.id.clone(), AgentEvent::McpServerReady { + self.event_emitter.emit( + self.id.clone(), + AgentEvent::McpServerReady { server_name: server_name.clone(), - tool_count: *tool_count, - }); + tool_count: *tool_count, + }, + ); } Err(e) => { - self.event_emitter - .emit(self.id.clone(), AgentEvent::McpServerFailed { + self.event_emitter.emit( + self.id.clone(), + AgentEvent::McpServerFailed { server_name: server_name.clone(), - error: e.to_string(), - }); + error: e.to_string(), + }, + ); } } } @@ -216,10 +222,10 @@ impl Session { "Sandbox MCP server started, connecting via HTTP" ); resolved.push(McpServerSettings { - name: config.name.clone(), - transport: McpTransport::Http { url, headers }, + name: config.name.clone(), + transport: McpTransport::Http { url, headers }, startup_timeout_secs: config.startup_timeout_secs, - tool_timeout_secs: config.tool_timeout_secs, + tool_timeout_secs: config.tool_timeout_secs, }); } Err(e) => { @@ -228,11 +234,13 @@ impl Session { error = %e, "Failed to start sandbox MCP server" ); - self.event_emitter - .emit(self.id.clone(), AgentEvent::McpServerFailed { + self.event_emitter.emit( + self.id.clone(), + AgentEvent::McpServerFailed { server_name: config.name.clone(), - error: e, - }); + error: e, + }, + ); } } } @@ -404,24 +412,27 @@ impl Session { } } - fn interrupted_error(&self) -> AgentError { + fn interrupted_error(&self) -> Error { let reason = self .interrupt_reason .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone() .unwrap_or(InterruptReason::Cancelled); - AgentError::Interrupted(reason) + Error::Interrupted(reason) } - fn emit_llm_error(&mut self, err: SdkError) -> AgentError { - self.event_emitter.emit(self.id.clone(), AgentEvent::Error { - error: AgentError::Llm(err.clone()), - }); + fn emit_llm_error(&mut self, err: LlmError) -> Error { + self.event_emitter.emit( + self.id.clone(), + AgentEvent::Error { + error: Error::Llm(err.clone()), + }, + ); if is_auth_error(&err) { self.transition(SessionState::Closed); } - AgentError::Llm(err) + Error::Llm(err) } async fn open_stream_with_retry( @@ -429,7 +440,7 @@ impl Session { client: &Client, request: &Request, retry_policy: &RetryPolicy, - ) -> Result { + ) -> Result { let stream_result = retry::retry(retry_policy, || { let client = client.clone(); let request = request.clone(); @@ -556,9 +567,9 @@ impl Session { &self.file_tracker } - pub async fn process_input(&mut self, input: &str) -> Result<(), AgentError> { + pub async fn process_input(&mut self, input: &str) -> Result<(), Error> { if self.state == SessionState::Closed { - return Err(AgentError::SessionClosed); + return Err(Error::SessionClosed); } // Spawn wall-clock timeout task if configured @@ -610,11 +621,11 @@ impl Session { result } - async fn run_single_input(&mut self, input: &str) -> Result<(), AgentError> { + async fn run_single_input(&mut self, input: &str) -> Result<(), Error> { const STREAM_CONSUME_RETRIES: usize = 3; if self.state == SessionState::Closed { - return Err(AgentError::SessionClosed); + return Err(Error::SessionClosed); } self.transition(SessionState::Thinking); @@ -622,29 +633,33 @@ impl Session { // Expand skill references in input let expanded = if self.skills.is_empty() { ExpandedInput { - text: input.to_string(), + text: input.to_string(), skill_name: None, } } else { - expand_skill(&self.skills, input).map_err(AgentError::InvalidState)? + expand_skill(&self.skills, input).map_err(Error::InvalidState)? }; if let Some(ref name) = expanded.skill_name { - self.event_emitter - .emit(self.id.clone(), AgentEvent::SkillExpanded { + self.event_emitter.emit( + self.id.clone(), + AgentEvent::SkillExpanded { skill_name: name.clone(), - }); + }, + ); } let expanded_input = expanded.text; // Append user turn and emit event self.history.push(Turn::User { - content: expanded_input.clone(), + content: expanded_input.clone(), timestamp: SystemTime::now(), }); - self.event_emitter - .emit(self.id.clone(), AgentEvent::UserInput { + self.event_emitter.emit( + self.id.clone(), + AgentEvent::UserInput { text: expanded_input.clone(), - }); + }, + ); // Drain steering queue before first LLM call self.drain_steering(); @@ -656,19 +671,23 @@ impl Session { if self.config.max_tool_rounds_per_input > 0 && round_count >= self.config.max_tool_rounds_per_input { - self.event_emitter - .emit(self.id.clone(), AgentEvent::TurnLimitReached { + self.event_emitter.emit( + self.id.clone(), + AgentEvent::TurnLimitReached { max_turns: self.config.max_tool_rounds_per_input, - }); + }, + ); break; } // Check max_turns if self.config.max_turns > 0 && self.history.turns().len() >= self.config.max_turns { - self.event_emitter - .emit(self.id.clone(), AgentEvent::TurnLimitReached { + self.event_emitter.emit( + self.id.clone(), + AgentEvent::TurnLimitReached { max_turns: self.config.max_turns, - }); + }, + ); break; } @@ -696,13 +715,16 @@ impl Session { let retry_policy = RetryPolicy { max_retries: 3, on_retry: Some(std::sync::Arc::new(move |err, attempt, delay| { - retry_emitter.emit(retry_session_id.clone(), AgentEvent::LlmRetry { - provider: retry_provider.clone(), - model: retry_model.clone(), - attempt: attempt as usize, - delay_secs: delay.as_secs_f64(), - error: err.clone(), - }); + retry_emitter.emit( + retry_session_id.clone(), + AgentEvent::LlmRetry { + provider: retry_provider.clone(), + model: retry_model.clone(), + attempt: attempt as usize, + delay_secs: delay.as_secs_f64(), + error: err.clone(), + }, + ); })), ..Default::default() }; @@ -782,7 +804,7 @@ impl Session { self.event_emitter.emit( self.id.clone(), AgentEvent::AssistantOutputReplace { - text: String::new(), + text: String::new(), reasoning: None, }, ); @@ -794,9 +816,9 @@ impl Session { } let Some(response) = response else { - return Err(self.emit_llm_error(SdkError::Stream { + return Err(self.emit_llm_error(LlmError::Stream { message: "Stream ended without a Finish event (after retries)".into(), - source: None, + source: None, })); }; @@ -822,13 +844,15 @@ impl Session { }); // Emit AssistantMessage with enriched data from the response - self.event_emitter - .emit(self.id.clone(), AgentEvent::AssistantMessage { - text: text.clone(), - model: response.model.clone(), - usage: response.usage.clone(), + self.event_emitter.emit( + self.id.clone(), + AgentEvent::AssistantMessage { + text: text.clone(), + model: response.model.clone(), + usage: response.usage.clone(), tool_call_count: tool_calls.len(), - }); + }, + ); // Post-response compaction: trim context after appending assistant turn self.compact_if_needed().await; @@ -918,9 +942,12 @@ impl Session { ) .await { - self.event_emitter.emit(self.id.clone(), AgentEvent::Error { - error: AgentError::InvalidState(format!("Context compaction failed: {e}")), - }); + self.event_emitter.emit( + self.id.clone(), + AgentEvent::Error { + error: Error::InvalidState(format!("Context compaction failed: {e}")), + }, + ); } } } @@ -935,7 +962,7 @@ impl Session { for msg in messages { let text = msg.clone(); self.history.push(Turn::Steering { - content: msg, + content: msg, timestamp: SystemTime::now(), }); self.event_emitter @@ -980,7 +1007,7 @@ impl Session { } } -const fn is_auth_error(err: &SdkError) -> bool { +const fn is_auth_error(err: &LlmError) -> bool { matches!( err.provider_kind(), Some(ProviderErrorKind::Authentication | ProviderErrorKind::AccessDenied) @@ -1008,12 +1035,12 @@ mod tests { #[derive(Clone)] enum ScriptedStreamCall { Response(Box), - Events(Vec>), - Error(SdkError), + Events(Vec>), + Error(LlmError), } struct ScriptedStreamProvider { - calls: Vec, + calls: Vec, call_index: AtomicUsize, } @@ -1029,7 +1056,7 @@ mod tests { } } - fn events_for_response(response: Response) -> Vec> { + fn events_for_response(response: Response) -> Vec> { let mut events = Vec::new(); let text = response.text(); if !text.is_empty() { @@ -1059,14 +1086,14 @@ mod tests { "mock" } - async fn complete(&self, _request: &Request) -> Result { - Err(SdkError::Configuration { + async fn complete(&self, _request: &Request) -> Result { + Err(LlmError::Configuration { message: "ScriptedStreamProvider does not implement complete()".into(), - source: None, + source: None, }) } - async fn stream(&self, _request: &Request) -> Result { + async fn stream(&self, _request: &Request) -> Result { let idx = self.call_index.fetch_add(1, Ordering::SeqCst); let scripted = if idx < self.calls.len() { self.calls[idx].clone() @@ -1427,7 +1454,7 @@ mod tests { let result = session.process_input("Do something").await; // Should return Interrupted error and transition to Closed - assert!(matches!(result, Err(AgentError::Interrupted(_)))); + assert!(matches!(result, Err(Error::Interrupted(_)))); assert_eq!(session.state(), SessionState::Closed); // Should have stopped immediately: User turn only, no LLM call @@ -1444,11 +1471,11 @@ mod tests { // Tool that cancels the token when executed let abort_tool = RegisteredTool { definition: ToolDefinition { - name: "set_abort".into(), + name: "set_abort".into(), description: "Sets interrupt flag".into(), - parameters: serde_json::json!({"type": "object"}), + parameters: serde_json::json!({"type": "object"}), }, - executor: Arc::new(move |_args, _ctx| { + executor: Arc::new(move |_args, _ctx| { let token = cancel_token_for_tool.clone(); Box::pin(async move { token.cancel(); @@ -1481,7 +1508,7 @@ mod tests { let result = session.process_input("Do something").await; // Should return Interrupted error and transition to Closed - assert!(matches!(result, Err(AgentError::Interrupted(_)))); + assert!(matches!(result, Err(Error::Interrupted(_)))); assert_eq!(session.state(), SessionState::Closed); // Should have processed: User + Assistant(tool_call) + ToolResults = 3 turns @@ -1496,8 +1523,8 @@ mod tests { #[tokio::test] async fn auth_error_closes_session() { let error_provider = Arc::new(MockErrorProvider { - error: SdkError::Provider { - kind: ProviderErrorKind::Authentication, + error: LlmError::Provider { + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail::new("invalid api key", "mock")), }, }); @@ -1508,7 +1535,7 @@ mod tests { let result = session.process_input("Hello").await; assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), AgentError::Llm(_))); + assert!(matches!(result.unwrap_err(), Error::Llm(_))); assert_eq!(session.state(), SessionState::Closed); } @@ -1540,7 +1567,7 @@ mod tests { let result = session.process_input("Hello").await; assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), AgentError::SessionClosed)); + assert!(matches!(result.unwrap_err(), Error::SessionClosed)); } #[tokio::test] @@ -1550,7 +1577,7 @@ mod tests { let mut rx = session.subscribe(); let result = session.process_input("Hello").await; - assert!(matches!(result, Err(AgentError::SessionClosed))); + assert!(matches!(result, Err(Error::SessionClosed))); // No SessionStarted event should have been emitted let mut events = Vec::new(); @@ -1698,9 +1725,9 @@ mod tests { let mut registry = ToolRegistry::new(); registry.register(RegisteredTool { definition: ToolDefinition { - name: "strict_tool".into(), + name: "strict_tool".into(), description: "Tool with required params".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "text": {"type": "string"} @@ -1708,7 +1735,7 @@ mod tests { "required": ["text"] }), }, - executor: Arc::new(|_args, _ctx| { + executor: Arc::new(|_args, _ctx| { Box::pin(async move { Ok("should not reach".to_string()) }) }), }); @@ -1739,9 +1766,9 @@ mod tests { let mut registry = ToolRegistry::new(); registry.register(RegisteredTool { definition: ToolDefinition { - name: "strict_tool".into(), + name: "strict_tool".into(), description: "Tool with required params".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "text": {"type": "string"} @@ -1749,7 +1776,7 @@ mod tests { "required": ["text"] }), }, - executor: Arc::new(|_args, _ctx| { + executor: Arc::new(|_args, _ctx| { Box::pin(async move { Ok("tool executed".to_string()) }) }), }); @@ -2061,9 +2088,9 @@ mod tests { async fn stream_mid_stream_error() { let provider = Arc::new(MockMidStreamErrorProvider { partial_text: "partial".into(), - error: SdkError::Stream { + error: LlmError::Stream { message: "connection reset".into(), - source: None, + source: None, }, }); let client = make_client(provider as Arc).await; @@ -2072,10 +2099,7 @@ mod tests { let mut session = Session::new(client, profile, env, SessionOptions::default(), None); let result = session.process_input("Hello").await; - assert!(matches!( - result, - Err(AgentError::Llm(SdkError::Stream { .. })) - )); + assert!(matches!(result, Err(Error::Llm(LlmError::Stream { .. })))); } #[tokio::test] @@ -2149,19 +2173,22 @@ mod tests { } } - assert_eq!(observed, vec![ - "start".to_string(), - "delta:Hel".to_string(), - "replace::None".to_string(), - "delta:Hello".to_string(), - "message:Hello".to_string(), - ]); + assert_eq!( + observed, + vec![ + "start".to_string(), + "delta:Hel".to_string(), + "replace::None".to_string(), + "delta:Hello".to_string(), + "message:Hello".to_string(), + ] + ); } #[tokio::test] async fn retry_open_auth_error_emits_error_and_closes_session() { - let auth_error = SdkError::Provider { - kind: ProviderErrorKind::Authentication, + let auth_error = LlmError::Provider { + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail { status_code: Some(401), ..ProviderErrorDetail::new("bad key", "mock") @@ -2177,7 +2204,7 @@ mod tests { let result = session.process_input("Hello").await; assert!(matches!( result, - Err(AgentError::Llm(SdkError::Provider { + Err(Error::Llm(LlmError::Provider { kind: ProviderErrorKind::Authentication, .. })) @@ -2199,7 +2226,7 @@ mod tests { observed.push("error".to_string()); found_auth_error_event = matches!( error, - AgentError::Llm(SdkError::Provider { + Error::Llm(LlmError::Provider { kind: ProviderErrorKind::Authentication, .. }) @@ -2210,12 +2237,15 @@ mod tests { } } - assert_eq!(observed, vec![ - "start".to_string(), - "delta:Hel".to_string(), - "replace::None".to_string(), - "error".to_string(), - ]); + assert_eq!( + observed, + vec![ + "start".to_string(), + "delta:Hel".to_string(), + "replace::None".to_string(), + "error".to_string(), + ] + ); assert!(found_auth_error_event, "expected auth error event"); } @@ -2308,7 +2338,7 @@ mod tests { // provider that errors on complete() but succeeds on stream(). struct StreamOnlyProvider { - responses: Vec, + responses: Vec, call_index: AtomicUsize, } @@ -2318,14 +2348,14 @@ mod tests { "mock" } - async fn complete(&self, _request: &Request) -> Result { - Err(SdkError::Stream { + async fn complete(&self, _request: &Request) -> Result { + Err(LlmError::Stream { message: "summarization failed".into(), - source: None, + source: None, }) } - async fn stream(&self, _request: &Request) -> Result { + async fn stream(&self, _request: &Request) -> Result { let idx = self.call_index.fetch_add(1, Ordering::SeqCst); let response = if idx < self.responses.len() { self.responses[idx].clone() @@ -2333,7 +2363,7 @@ mod tests { self.responses[self.responses.len() - 1].clone() }; // Reuse response_to_stream helper from test_support - let mut events: Vec> = Vec::new(); + let mut events: Vec> = Vec::new(); let text = response.text(); if !text.is_empty() { events.push(Ok(StreamEvent::text_delta(text, None))); @@ -2402,8 +2432,8 @@ mod tests { // Provider that captures complete() requests (compaction) while returning // canned responses for stream() calls. struct CompactionCapturingProvider { - stream_responses: Vec, - stream_index: AtomicUsize, + stream_responses: Vec, + stream_index: AtomicUsize, captured_complete: Mutex>, } @@ -2413,12 +2443,12 @@ mod tests { "mock" } - async fn complete(&self, request: &Request) -> Result { + async fn complete(&self, request: &Request) -> Result { *self.captured_complete.lock().unwrap() = Some(request.clone()); Ok(text_response("## Goal\nSummary goes here.")) } - async fn stream(&self, _request: &Request) -> Result { + async fn stream(&self, _request: &Request) -> Result { let idx = self.stream_index.fetch_add(1, Ordering::SeqCst); let response = if idx < self.stream_responses.len() { self.stream_responses[idx].clone() @@ -2432,11 +2462,11 @@ mod tests { // read_file tool that always succeeds let read_tool = RegisteredTool { definition: ToolDefinition { - name: "read_file".into(), + name: "read_file".into(), description: "Read a file".into(), - parameters: serde_json::json!({"type": "object", "properties": {"file_path": {"type": "string"}}}), + parameters: serde_json::json!({"type": "object", "properties": {"file_path": {"type": "string"}}}), }, - executor: Arc::new(|_args, _ctx| { + executor: Arc::new(|_args, _ctx| { Box::pin(async move { Ok("file contents".to_string()) }) }), }; @@ -2545,13 +2575,13 @@ mod tests { ); let config = SessionOptions { mcp_servers: vec![McpServerSettings { - name: "test-echo".into(), - transport: McpTransport::Stdio { + name: "test-echo".into(), + transport: McpTransport::Stdio { command: vec!["python3".into(), test_server], - env: HashMap::new(), + env: HashMap::new(), }, startup_timeout_secs: 10, - tool_timeout_secs: 30, + tool_timeout_secs: 30, }], enable_loop_detection: false, ..Default::default() @@ -2657,11 +2687,11 @@ mod tests { // Register a tool that loops until the cancel token fires let slow_tool = RegisteredTool { definition: ToolDefinition { - name: "slow_tool".into(), + name: "slow_tool".into(), description: "Waits until cancelled".into(), - parameters: serde_json::json!({"type": "object"}), + parameters: serde_json::json!({"type": "object"}), }, - executor: Arc::new(|_args, ctx| { + executor: Arc::new(|_args, ctx| { Box::pin(async move { ctx.cancel.cancelled().await; Ok("cancelled".to_string()) @@ -2689,7 +2719,7 @@ mod tests { assert!( matches!( result, - Err(AgentError::Interrupted(InterruptReason::WallClockTimeout)) + Err(Error::Interrupted(InterruptReason::WallClockTimeout)) ), "expected Interrupted(WallClockTimeout), got {result:?}" ); diff --git a/lib/crates/fabro-agent/src/skills.rs b/lib/crates/fabro-agent/src/skills.rs index a4d00226e..6c48f38d9 100644 --- a/lib/crates/fabro-agent/src/skills.rs +++ b/lib/crates/fabro-agent/src/skills.rs @@ -8,9 +8,9 @@ use crate::tools::required_str; #[derive(Debug, Clone)] pub struct Skill { - pub name: String, + pub name: String, pub description: String, - pub template: String, + pub template: String, } pub fn parse_skill(content: &str) -> Result { @@ -51,11 +51,11 @@ pub fn parse_skill(content: &str) -> Result { /// A detected skill reference in user input: the name and byte range of the /// `/name` token. struct SkillMatch { - name: String, + name: String, /// Byte offset of the `/` character start: usize, /// Byte offset just past the skill name - end: usize, + end: usize, } fn is_skill_name_char(c: char) -> bool { @@ -97,9 +97,9 @@ fn find_skill_references(input: &str) -> Vec { let followed_by_boundary = j >= len || bytes[j].is_ascii_whitespace(); if followed_by_boundary { results.push(SkillMatch { - name: input[name_start..j].to_string(), + name: input[name_start..j].to_string(), start: i, - end: j, + end: j, }); } @@ -114,7 +114,7 @@ fn find_skill_references(input: &str) -> Vec { #[derive(Debug)] pub struct ExpandedInput { - pub text: String, + pub text: String, pub skill_name: Option, } @@ -123,7 +123,7 @@ pub fn expand_skill(skills: &[Skill], input: &str) -> Result Result>) -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "use_skill".into(), + name: "use_skill".into(), description: "Load a skill's instructions by name. Call this when the user's \ request matches an available skill." .into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "skill_name": { @@ -174,7 +174,7 @@ pub fn make_use_skill_tool(skills: Arc>) -> RegisteredTool { "required": ["skill_name"] }), }, - executor: Arc::new(move |args, _ctx| { + executor: Arc::new(move |args, _ctx| { let skills = skills.clone(); Box::pin(async move { let name = required_str(&args, "skill_name")?; @@ -343,14 +343,14 @@ name: trimmed fn test_skills() -> Vec { vec![ Skill { - name: "commit".into(), + name: "commit".into(), description: "Create a commit".into(), - template: "Review changes and commit.\n\n{{user_input}}".into(), + template: "Review changes and commit.\n\n{{user_input}}".into(), }, Skill { - name: "test".into(), + name: "test".into(), description: "Run tests".into(), - template: "Run the test suite.".into(), + template: "Run the test suite.".into(), }, ] } @@ -524,11 +524,14 @@ name: trimmed #[test] fn default_dirs_with_git_root() { let dirs = default_skill_dirs(Some("/home/user/.fabro/skills"), Some("/repo")); - assert_eq!(dirs, vec![ - "/home/user/.fabro/skills", - "/repo/.fabro/skills", - "/repo/skills", - ]); + assert_eq!( + dirs, + vec![ + "/home/user/.fabro/skills", + "/repo/.fabro/skills", + "/repo/skills", + ] + ); } #[test] diff --git a/lib/crates/fabro-agent/src/subagent.rs b/lib/crates/fabro-agent/src/subagent.rs index d082b6c52..0031422cf 100644 --- a/lib/crates/fabro-agent/src/subagent.rs +++ b/lib/crates/fabro-agent/src/subagent.rs @@ -6,7 +6,7 @@ use tokio::sync::Mutex as AsyncMutex; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; -use crate::error::AgentError; +use crate::error::Error; use crate::session::Session; use crate::tool_registry::RegisteredTool; use crate::tools::required_str; @@ -24,29 +24,29 @@ pub type SubAgentEventCallback = Arc), + Finished(Result), Closed, } pub struct SubAgent { - task: Option>>, + task: Option>>, followup_queue: Arc>>, - cancel_token: CancellationToken, - depth: usize, - status: SubAgentStatus, + cancel_token: CancellationToken, + depth: usize, + status: SubAgentStatus, } pub struct SubAgentManager { - agents: HashMap, - max_depth: usize, + agents: HashMap, + max_depth: usize, event_callback: Option, } @@ -75,9 +75,9 @@ impl SubAgentManager { mut session: Session, task_prompt: String, depth: usize, - ) -> Result { + ) -> Result { if depth >= self.max_depth { - return Err(AgentError::InvalidState(format!( + return Err(Error::InvalidState(format!( "Maximum subagent depth ({}) reached", self.max_depth ))); @@ -119,32 +119,35 @@ impl SubAgentManager { _ => None, }); Ok(SubAgentResult { - output: last_text.unwrap_or_default(), - success: true, + output: last_text.unwrap_or_default(), + success: true, turns_used: turns.len(), }) }); - self.agents.insert(agent_id.clone(), SubAgent { - task: Some(task), - followup_queue, - cancel_token, - depth: depth + 1, - status: SubAgentStatus::Running, - }); + self.agents.insert( + agent_id.clone(), + SubAgent { + task: Some(task), + followup_queue, + cancel_token, + depth: depth + 1, + status: SubAgentStatus::Running, + }, + ); self.emit_event(AgentEvent::SubAgentSpawned { agent_id: agent_id.clone(), - depth: depth + 1, - task: task_prompt, + depth: depth + 1, + task: task_prompt, }); Ok(agent_id) } - pub fn send_input(&self, agent_id: &str, message: &str) -> Result<(), AgentError> { + pub fn send_input(&self, agent_id: &str, message: &str) -> Result<(), Error> { let agent = self.agents.get(agent_id).ok_or_else(|| { - AgentError::InvalidState(format!( + Error::InvalidState(format!( "No agent found with id: {agent_id} (it was never spawned)" )) })?; @@ -152,7 +155,7 @@ impl SubAgentManager { match agent.status { SubAgentStatus::Running => {} _ => { - return Err(AgentError::InvalidState(format!( + return Err(Error::InvalidState(format!( "Agent {agent_id} is not running" ))); } @@ -167,12 +170,12 @@ impl SubAgentManager { Ok(()) } - pub async fn wait(&mut self, agent_id: &str) -> Result { + pub async fn wait(&mut self, agent_id: &str) -> Result { // Phase 1: Check existence and current status let agent = self.agents.get(agent_id); let depth = match agent { None => { - return Err(AgentError::InvalidState(format!( + return Err(Error::InvalidState(format!( "No agent found with id: {agent_id} (it was never spawned)" ))); } @@ -181,7 +184,7 @@ impl SubAgentManager { match &self.agents[agent_id].status { SubAgentStatus::Closed => { - return Err(AgentError::InvalidState(format!( + return Err(Error::InvalidState(format!( "Agent {agent_id} has been closed" ))); } @@ -198,16 +201,12 @@ impl SubAgentManager { .unwrap() .task .take() - .ok_or_else(|| { - AgentError::InvalidState(format!("Agent {agent_id} has no running task")) - })?; + .ok_or_else(|| Error::InvalidState(format!("Agent {agent_id} has no running task")))?; // Phase 3: Await the task (no borrow held) let task_result = match join_handle.await { Ok(result) => result, - Err(e) => Err(AgentError::InvalidState(format!( - "Agent task panicked: {e}" - ))), + Err(e) => Err(Error::InvalidState(format!("Agent task panicked: {e}"))), }; // Phase 4: Emit event @@ -239,16 +238,16 @@ impl SubAgentManager { } } - pub fn close(&mut self, agent_id: &str) -> Result<(), AgentError> { + pub fn close(&mut self, agent_id: &str) -> Result<(), Error> { let agent = self.agents.get_mut(agent_id).ok_or_else(|| { - AgentError::InvalidState(format!( + Error::InvalidState(format!( "No agent found with id: {agent_id} (it was never spawned)" )) })?; match agent.status { SubAgentStatus::Closed => { - return Err(AgentError::InvalidState(format!( + return Err(Error::InvalidState(format!( "Agent {agent_id} is already closed" ))); } @@ -307,9 +306,9 @@ pub fn make_spawn_agent_tool( ) -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "spawn_agent".into(), + name: "spawn_agent".into(), description: "Spawn a subagent to work on a delegated task".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "task": { @@ -332,7 +331,7 @@ pub fn make_spawn_agent_tool( "required": ["task"] }), }, - executor: Arc::new(move |args, _ctx| { + executor: Arc::new(move |args, _ctx| { let manager = manager.clone(); let session_factory = session_factory.clone(); Box::pin(async move { @@ -360,9 +359,9 @@ pub fn make_spawn_agent_tool( pub fn make_send_input_tool(manager: Arc>) -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "send_input".into(), + name: "send_input".into(), description: "Send a follow-up message to a running subagent".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "agent_id": { @@ -377,7 +376,7 @@ pub fn make_send_input_tool(manager: Arc>) -> Regist "required": ["agent_id", "message"] }), }, - executor: Arc::new(move |args, _ctx| { + executor: Arc::new(move |args, _ctx| { let manager = manager.clone(); Box::pin(async move { let agent_id = required_str(&args, "agent_id")?; @@ -395,9 +394,9 @@ pub fn make_send_input_tool(manager: Arc>) -> Regist pub fn make_wait_tool(manager: Arc>) -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "wait".into(), + name: "wait".into(), description: "Wait for a subagent to complete and return its result".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "agent_id": { @@ -408,7 +407,7 @@ pub fn make_wait_tool(manager: Arc>) -> RegisteredTo "required": ["agent_id"] }), }, - executor: Arc::new(move |args, _ctx| { + executor: Arc::new(move |args, _ctx| { let manager = manager.clone(); Box::pin(async move { let agent_id = required_str(&args, "agent_id")?; @@ -427,9 +426,9 @@ pub fn make_wait_tool(manager: Arc>) -> RegisteredTo pub fn make_close_agent_tool(manager: Arc>) -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "close_agent".into(), + name: "close_agent".into(), description: "Close a running subagent".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "agent_id": { @@ -440,7 +439,7 @@ pub fn make_close_agent_tool(manager: Arc>) -> Regis "required": ["agent_id"] }), }, - executor: Arc::new(move |args, _ctx| { + executor: Arc::new(move |args, _ctx| { let manager = manager.clone(); Box::pin(async move { let agent_id = required_str(&args, "agent_id")?; @@ -719,21 +718,21 @@ mod tests { let mut rx = parent.subscribe(); callback(SubAgentCallbackEvent::Forwarded(SessionEvent { - event: AgentEvent::SessionStarted { + event: AgentEvent::SessionStarted { provider: Some("anthropic".into()), - model: Some("claude-opus".into()), + model: Some("claude-opus".into()), }, - timestamp: std::time::SystemTime::now(), - session_id: "child".into(), + timestamp: std::time::SystemTime::now(), + session_id: "child".into(), parent_session_id: None, })); callback(SubAgentCallbackEvent::Forwarded(SessionEvent { - event: AgentEvent::SessionStarted { + event: AgentEvent::SessionStarted { provider: Some("anthropic".into()), - model: Some("claude-opus".into()), + model: Some("claude-opus".into()), }, - timestamp: std::time::SystemTime::now(), - session_id: "grandchild".into(), + timestamp: std::time::SystemTime::now(), + session_id: "grandchild".into(), parent_session_id: Some("child".into()), })); @@ -751,7 +750,7 @@ mod tests { let manager = SubAgentManager::new(3); manager.emit_event(AgentEvent::SubAgentClosed { agent_id: "x".into(), - depth: 0, + depth: 0, }); } diff --git a/lib/crates/fabro-agent/src/test_support.rs b/lib/crates/fabro-agent/src/test_support.rs index bf209df15..13f663f10 100644 --- a/lib/crates/fabro-agent/src/test_support.rs +++ b/lib/crates/fabro-agent/src/test_support.rs @@ -3,8 +3,8 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use async_trait::async_trait; +use fabro_llm::Error as LlmError; use fabro_llm::client::Client; -use fabro_llm::error::SdkError; use fabro_llm::provider::{ProviderAdapter, StreamEventStream}; use fabro_llm::types::{ ContentPart, FinishReason, Message, Request, Response, StreamEvent, TokenCounts, @@ -24,14 +24,14 @@ use crate::tool_registry::{RegisteredTool, ToolRegistry}; // --- TestProfile --- pub struct TestProfile { - pub registry: ToolRegistry, + pub registry: ToolRegistry, pub context_window: usize, } impl TestProfile { pub fn new() -> Self { Self { - registry: ToolRegistry::new(), + registry: ToolRegistry::new(), context_window: 200_000, } } @@ -98,7 +98,7 @@ impl AgentProfile for TestProfile { // --- MockLlmProvider --- pub struct MockLlmProvider { - pub responses: Vec, + pub responses: Vec, pub call_index: AtomicUsize, } @@ -117,7 +117,7 @@ impl ProviderAdapter for MockLlmProvider { "mock" } - async fn complete(&self, _request: &Request) -> Result { + async fn complete(&self, _request: &Request) -> Result { let idx = self.call_index.fetch_add(1, Ordering::SeqCst); if idx < self.responses.len() { Ok(self.responses[idx].clone()) @@ -126,7 +126,7 @@ impl ProviderAdapter for MockLlmProvider { } } - async fn stream(&self, _request: &Request) -> Result { + async fn stream(&self, _request: &Request) -> Result { let idx = self.call_index.fetch_add(1, Ordering::SeqCst); let response = if idx < self.responses.len() { self.responses[idx].clone() @@ -139,7 +139,7 @@ impl ProviderAdapter for MockLlmProvider { /// Convert a canned `Response` into a `StreamEventStream` for mock streaming. pub fn response_to_stream(response: Response) -> StreamEventStream { - let mut events: Vec> = Vec::new(); + let mut events: Vec> = Vec::new(); // Emit text deltas for text content let text = response.text(); @@ -170,19 +170,19 @@ pub fn response_to_stream(response: Response) -> StreamEventStream { pub fn text_response(text: &str) -> Response { Response { - id: format!("resp_{text}"), - model: "mock-model".into(), - provider: "mock".into(), - message: Message::assistant(text), + id: format!("resp_{text}"), + model: "mock-model".into(), + provider: "mock".into(), + message: Message::assistant(text), finish_reason: FinishReason::Stop, - usage: TokenCounts { + usage: TokenCounts { input_tokens: 10, output_tokens: 5, ..Default::default() }, - raw: None, - warnings: vec![], - rate_limit: None, + raw: None, + warnings: vec![], + rate_limit: None, } } @@ -238,27 +238,27 @@ pub fn tool_call_response( ) -> Response { use fabro_llm::types::{ContentPart, Role, ToolCall}; Response { - id: format!("resp_{tool_call_id}"), - model: "mock-model".into(), - provider: "mock".into(), - message: Message { - role: Role::Assistant, - content: vec![ + id: format!("resp_{tool_call_id}"), + model: "mock-model".into(), + provider: "mock".into(), + message: Message { + role: Role::Assistant, + content: vec![ ContentPart::text("Let me use a tool."), ContentPart::ToolCall(ToolCall::new(tool_call_id, tool_name, args)), ], - name: None, + name: None, tool_call_id: None, }, finish_reason: FinishReason::ToolCalls, - usage: TokenCounts { + usage: TokenCounts { input_tokens: 10, output_tokens: 5, ..Default::default() }, - raw: None, - warnings: vec![], - rate_limit: None, + raw: None, + warnings: vec![], + rate_limit: None, } } @@ -266,11 +266,11 @@ pub fn make_echo_tool() -> RegisteredTool { use fabro_llm::types::ToolDefinition; RegisteredTool { definition: ToolDefinition { - name: "echo".into(), + name: "echo".into(), description: "Echoes the input".into(), - parameters: serde_json::json!({"type": "object", "properties": {"text": {"type": "string"}}}), + parameters: serde_json::json!({"type": "object", "properties": {"text": {"type": "string"}}}), }, - executor: Arc::new(|args, _ctx| { + executor: Arc::new(|args, _ctx| { Box::pin(async move { let text = args .get("text") @@ -286,11 +286,11 @@ pub fn make_error_tool() -> RegisteredTool { use fabro_llm::types::ToolDefinition; RegisteredTool { definition: ToolDefinition { - name: "fail_tool".into(), + name: "fail_tool".into(), description: "Always fails".into(), - parameters: serde_json::json!({"type": "object"}), + parameters: serde_json::json!({"type": "object"}), }, - executor: Arc::new(|_args, _ctx| { + executor: Arc::new(|_args, _ctx| { Box::pin(async move { Err("tool execution failed".to_string()) }) }), } @@ -299,7 +299,7 @@ pub fn make_error_tool() -> RegisteredTool { // --- MockErrorProvider --- pub struct MockErrorProvider { - pub error: SdkError, + pub error: LlmError, } #[async_trait] @@ -308,11 +308,11 @@ impl ProviderAdapter for MockErrorProvider { "mock" } - async fn complete(&self, _request: &Request) -> Result { + async fn complete(&self, _request: &Request) -> Result { Err(self.error.clone()) } - async fn stream(&self, _request: &Request) -> Result { + async fn stream(&self, _request: &Request) -> Result { Err(self.error.clone()) } } @@ -338,7 +338,7 @@ impl ProviderAdapter for CapturingLlmProvider { "mock" } - async fn complete(&self, request: &Request) -> Result { + async fn complete(&self, request: &Request) -> Result { *self .captured_request .lock() @@ -346,7 +346,7 @@ impl ProviderAdapter for CapturingLlmProvider { Ok(text_response("captured")) } - async fn stream(&self, request: &Request) -> Result { + async fn stream(&self, request: &Request) -> Result { *self .captured_request .lock() @@ -360,7 +360,7 @@ impl ProviderAdapter for CapturingLlmProvider { /// A mock provider that yields some text deltas then an error mid-stream. pub struct MockMidStreamErrorProvider { pub partial_text: String, - pub error: SdkError, + pub error: LlmError, } #[async_trait] @@ -369,12 +369,12 @@ impl ProviderAdapter for MockMidStreamErrorProvider { "mock" } - async fn complete(&self, _request: &Request) -> Result { + async fn complete(&self, _request: &Request) -> Result { Err(self.error.clone()) } - async fn stream(&self, _request: &Request) -> Result { - let events: Vec> = vec![ + async fn stream(&self, _request: &Request) -> Result { + let events: Vec> = vec![ Ok(StreamEvent::text_delta(self.partial_text.clone(), None)), Err(self.error.clone()), ]; @@ -393,23 +393,23 @@ pub fn multi_tool_call_response(calls: Vec<(&str, &str, serde_json::Value)>) -> ))); } Response { - id: "resp_multi".into(), - model: "mock-model".into(), - provider: "mock".into(), - message: Message { + id: "resp_multi".into(), + model: "mock-model".into(), + provider: "mock".into(), + message: Message { role: Role::Assistant, content, name: None, tool_call_id: None, }, finish_reason: FinishReason::ToolCalls, - usage: TokenCounts { + usage: TokenCounts { input_tokens: 10, output_tokens: 5, ..Default::default() }, - raw: None, - warnings: vec![], - rate_limit: None, + raw: None, + warnings: vec![], + rate_limit: None, } } diff --git a/lib/crates/fabro-agent/src/tool_execution.rs b/lib/crates/fabro-agent/src/tool_execution.rs index 31bcfbf1a..11ce189fa 100644 --- a/lib/crates/fabro-agent/src/tool_execution.rs +++ b/lib/crates/fabro-agent/src/tool_execution.rs @@ -180,11 +180,14 @@ async fn execute_and_emit_one_tool_with_lookup( session_id: &str, tool_env: Option<&HashMap>, ) -> ToolResult { - emitter.emit(session_id.to_owned(), AgentEvent::ToolCallStarted { - tool_name: tc.name.clone(), - tool_call_id: tc.id.clone(), - arguments: tc.arguments.clone(), - }); + emitter.emit( + session_id.to_owned(), + AgentEvent::ToolCallStarted { + tool_name: tc.name.clone(), + tool_call_id: tc.id.clone(), + arguments: tc.arguments.clone(), + }, + ); // Pre-tool-use hook if let Some(hooks) = tool_hooks { @@ -197,15 +200,21 @@ async fn execute_and_emit_one_tool_with_lookup( if let ToolHookDecision::Block { reason } = decision { let result = ToolResult::error(&tc.id, &reason); - emitter.emit(session_id.to_owned(), AgentEvent::ToolCallOutputDelta { - delta: result.content.to_string(), - }); - emitter.emit(session_id.to_owned(), AgentEvent::ToolCallCompleted { - tool_name: tc.name.clone(), - tool_call_id: tc.id.clone(), - output: result.content.clone(), - is_error: true, - }); + emitter.emit( + session_id.to_owned(), + AgentEvent::ToolCallOutputDelta { + delta: result.content.to_string(), + }, + ); + emitter.emit( + session_id.to_owned(), + AgentEvent::ToolCallCompleted { + tool_name: tc.name.clone(), + tool_call_id: tc.id.clone(), + output: result.content.clone(), + is_error: true, + }, + ); return truncate_tool_result(&result, &tc.name, config); } @@ -213,16 +222,22 @@ async fn execute_and_emit_one_tool_with_lookup( let result = execute_one_tool(tc, registered_tool, env, cancel_token, tool_env).await; - emitter.emit(session_id.to_owned(), AgentEvent::ToolCallOutputDelta { - delta: result.content.to_string(), - }); + emitter.emit( + session_id.to_owned(), + AgentEvent::ToolCallOutputDelta { + delta: result.content.to_string(), + }, + ); - emitter.emit(session_id.to_owned(), AgentEvent::ToolCallCompleted { - tool_name: tc.name.clone(), - tool_call_id: tc.id.clone(), - output: result.content.clone(), - is_error: result.is_error, - }); + emitter.emit( + session_id.to_owned(), + AgentEvent::ToolCallCompleted { + tool_name: tc.name.clone(), + tool_call_id: tc.id.clone(), + output: result.content.clone(), + is_error: result.is_error, + }, + ); // Post-tool-use hooks if let Some(hooks) = tool_hooks { @@ -293,10 +308,10 @@ fn truncate_tool_result( }; ToolResult { - tool_call_id: result.tool_call_id.clone(), - content: truncated_content, - is_error: result.is_error, - image_data: result.image_data.clone(), + tool_call_id: result.tool_call_id.clone(), + content: truncated_content, + is_error: result.is_error, + image_data: result.image_data.clone(), image_media_type: result.image_media_type.clone(), } } @@ -350,9 +365,9 @@ mod tests { fn make_echo_tool() -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "echo".to_string(), + name: "echo".to_string(), description: "Echo input".to_string(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "text": {"type": "string"} @@ -360,7 +375,7 @@ mod tests { "required": ["text"] }), }, - executor: Arc::new(|args: serde_json::Value, _ctx: ToolContext| { + executor: Arc::new(|args: serde_json::Value, _ctx: ToolContext| { Box::pin(async move { let text = args["text"].as_str().unwrap_or("").to_string(); Ok(format!("echo: {text}")) @@ -372,11 +387,11 @@ mod tests { fn make_fail_tool() -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "fail_tool".to_string(), + name: "fail_tool".to_string(), description: "Always fails".to_string(), - parameters: serde_json::json!({}), + parameters: serde_json::json!({}), }, - executor: Arc::new(|_args: serde_json::Value, _ctx: ToolContext| { + executor: Arc::new(|_args: serde_json::Value, _ctx: ToolContext| { Box::pin(async move { Err("tool failed".to_string()) }) }), } @@ -384,26 +399,26 @@ mod tests { fn make_tool_call(name: &str, id: &str, args: serde_json::Value) -> ToolCall { ToolCall { - id: id.to_string(), - name: name.to_string(), - tool_type: "function".to_string(), - arguments: args, - raw_arguments: None, + id: id.to_string(), + name: name.to_string(), + tool_type: "function".to_string(), + arguments: args, + raw_arguments: None, provider_metadata: None, } } struct MockHookCallback { - pre_decision: ToolHookDecision, - post_calls: Arc>>, + pre_decision: ToolHookDecision, + post_calls: Arc>>, post_failure_calls: Arc>>, } impl MockHookCallback { fn new(decision: ToolHookDecision) -> Self { Self { - pre_decision: decision, - post_calls: Arc::new(Mutex::new(Vec::new())), + pre_decision: decision, + post_calls: Arc::new(Mutex::new(Vec::new())), post_failure_calls: Arc::new(Mutex::new(Vec::new())), } } diff --git a/lib/crates/fabro-agent/src/tool_registry.rs b/lib/crates/fabro-agent/src/tool_registry.rs index cc0984249..997808943 100644 --- a/lib/crates/fabro-agent/src/tool_registry.rs +++ b/lib/crates/fabro-agent/src/tool_registry.rs @@ -9,8 +9,8 @@ use tokio_util::sync::CancellationToken; use crate::sandbox::Sandbox; pub struct ToolContext { - pub env: Arc, - pub cancel: CancellationToken, + pub env: Arc, + pub cancel: CancellationToken, pub tool_env: Option>, } @@ -26,7 +26,7 @@ pub type ToolExecutor = Arc< #[derive(Clone)] pub struct RegisteredTool { pub definition: ToolDefinition, - pub executor: ToolExecutor, + pub executor: ToolExecutor, } pub struct ToolRegistry { @@ -80,11 +80,11 @@ mod tests { fn make_tool(name: &str) -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: name.into(), + name: name.into(), description: format!("Tool {name}"), - parameters: serde_json::json!({"type": "object"}), + parameters: serde_json::json!({"type": "object"}), }, - executor: Arc::new(|_args, _ctx| Box::pin(async { Ok("ok".into()) })), + executor: Arc::new(|_args, _ctx| Box::pin(async { Ok("ok".into()) })), } } @@ -124,19 +124,19 @@ mod tests { let mut registry = ToolRegistry::new(); registry.register(RegisteredTool { definition: ToolDefinition { - name: "tool_a".into(), + name: "tool_a".into(), description: "version 1".into(), - parameters: serde_json::json!({}), + parameters: serde_json::json!({}), }, - executor: Arc::new(|_args, _ctx| Box::pin(async { Ok("v1".into()) })), + executor: Arc::new(|_args, _ctx| Box::pin(async { Ok("v1".into()) })), }); registry.register(RegisteredTool { definition: ToolDefinition { - name: "tool_a".into(), + name: "tool_a".into(), description: "version 2".into(), - parameters: serde_json::json!({}), + parameters: serde_json::json!({}), }, - executor: Arc::new(|_args, _ctx| Box::pin(async { Ok("v2".into()) })), + executor: Arc::new(|_args, _ctx| Box::pin(async { Ok("v2".into()) })), }); let tool = registry.get("tool_a").unwrap(); diff --git a/lib/crates/fabro-agent/src/tools.rs b/lib/crates/fabro-agent/src/tools.rs index 049f64b43..d67b55ea6 100644 --- a/lib/crates/fabro-agent/src/tools.rs +++ b/lib/crates/fabro-agent/src/tools.rs @@ -15,7 +15,7 @@ const MAX_WEB_FETCH_BYTES: usize = 100 * 1024; /// Configuration for the optional LLM-based summarizer used by `web_fetch`. #[derive(Clone)] pub struct WebFetchSummarizer { - pub client: Client, + pub client: Client, pub model_id: ModelHandle, } @@ -72,9 +72,9 @@ pub(crate) fn required_str<'a>(args: &'a serde_json::Value, key: &str) -> Result pub fn make_read_file_tool() -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "read_file".into(), + name: "read_file".into(), description: "Read the contents of a file".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "file_path": {"type": "string", "description": "Absolute path to the file"}, @@ -84,7 +84,7 @@ pub fn make_read_file_tool() -> RegisteredTool { "required": ["file_path"] }), }, - executor: Arc::new(|args, ctx| { + executor: Arc::new(|args, ctx| { Box::pin(async move { let file_path = required_str(&args, "file_path")?; let offset = args.get("offset").and_then(serde_json::Value::as_u64); @@ -108,9 +108,9 @@ pub fn make_read_file_tool() -> RegisteredTool { pub fn make_write_file_tool() -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "write_file".into(), + name: "write_file".into(), description: "Write content to a file".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "file_path": {"type": "string", "description": "Absolute path to the file"}, @@ -119,7 +119,7 @@ pub fn make_write_file_tool() -> RegisteredTool { "required": ["file_path", "content"] }), }, - executor: Arc::new(|args, ctx| { + executor: Arc::new(|args, ctx| { Box::pin(async move { let file_path = required_str(&args, "file_path")?; let content = required_str(&args, "content")?; @@ -135,9 +135,9 @@ pub fn make_write_file_tool() -> RegisteredTool { pub fn make_edit_file_tool() -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "edit_file".into(), + name: "edit_file".into(), description: "Edit a file by replacing a string".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "file_path": {"type": "string", "description": "Absolute path to the file"}, @@ -148,7 +148,7 @@ pub fn make_edit_file_tool() -> RegisteredTool { "required": ["file_path", "old_string", "new_string"] }), }, - executor: Arc::new(|args, ctx| { + executor: Arc::new(|args, ctx| { Box::pin(async move { let file_path = required_str(&args, "file_path")?; let old_string = required_str(&args, "old_string")?; @@ -201,9 +201,9 @@ pub fn make_shell_tool_with_config(config: &SessionOptions) -> RegisteredTool { let max_timeout = config.max_command_timeout_ms; RegisteredTool { definition: ToolDefinition { - name: "shell".into(), + name: "shell".into(), description: "Execute a shell command".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "command": {"type": "string", "description": "The shell command to execute"}, @@ -213,7 +213,7 @@ pub fn make_shell_tool_with_config(config: &SessionOptions) -> RegisteredTool { "required": ["command"] }), }, - executor: Arc::new(move |args, ctx| { + executor: Arc::new(move |args, ctx| { Box::pin(async move { let command = required_str(&args, "command")?; let timeout_ms = args @@ -259,9 +259,9 @@ pub fn make_shell_tool_with_config(config: &SessionOptions) -> RegisteredTool { pub fn make_grep_tool() -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "grep".into(), + name: "grep".into(), description: "Search file contents with a regex pattern".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "pattern": {"type": "string", "description": "Regex pattern to search for"}, @@ -273,7 +273,7 @@ pub fn make_grep_tool() -> RegisteredTool { "required": ["pattern"] }), }, - executor: Arc::new(|args, ctx| { + executor: Arc::new(|args, ctx| { Box::pin(async move { let pattern = required_str(&args, "pattern")?; let path = args @@ -316,9 +316,9 @@ pub fn make_grep_tool() -> RegisteredTool { pub fn make_glob_tool() -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "glob".into(), + name: "glob".into(), description: "Find files matching a glob pattern".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "pattern": {"type": "string", "description": "Glob pattern to match files"}, @@ -327,7 +327,7 @@ pub fn make_glob_tool() -> RegisteredTool { "required": ["pattern"] }), }, - executor: Arc::new(|args, ctx| { + executor: Arc::new(|args, ctx| { Box::pin(async move { let pattern = required_str(&args, "pattern")?; let path = args.get("path").and_then(serde_json::Value::as_str); @@ -343,9 +343,9 @@ pub fn make_glob_tool() -> RegisteredTool { pub(crate) fn make_read_many_files_tool() -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "read_many_files".into(), + name: "read_many_files".into(), description: "Read multiple files at once".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "paths": { @@ -357,7 +357,7 @@ pub(crate) fn make_read_many_files_tool() -> RegisteredTool { "required": ["paths"] }), }, - executor: Arc::new(|args, ctx| { + executor: Arc::new(|args, ctx| { Box::pin(async move { let paths = args["paths"] .as_array() @@ -388,9 +388,9 @@ pub(crate) fn make_read_many_files_tool() -> RegisteredTool { pub(crate) fn make_list_dir_tool() -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "list_dir".into(), + name: "list_dir".into(), description: "List directory contents with depth control".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "path": {"type": "string", "description": "Directory path to list"}, @@ -399,7 +399,7 @@ pub(crate) fn make_list_dir_tool() -> RegisteredTool { "required": ["path"] }), }, - executor: Arc::new(|args, ctx| { + executor: Arc::new(|args, ctx| { Box::pin(async move { let path = required_str(&args, "path")?; let depth = args @@ -471,9 +471,9 @@ fn make_web_search_tool_with_api_key(api_key: Option) -> RegisteredTool RegisteredTool { definition: ToolDefinition { - name: "web_search".into(), + name: "web_search".into(), description: "Search the web using Brave Search".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "query": {"type": "string", "description": "Search query"}, @@ -482,7 +482,7 @@ fn make_web_search_tool_with_api_key(api_key: Option) -> RegisteredTool "required": ["query"] }), }, - executor: Arc::new(move |args, _ctx| { + executor: Arc::new(move |args, _ctx| { let api_key = api_key.clone(); Box::pin(async move { let api_key = api_key.ok_or_else(|| { @@ -638,11 +638,14 @@ mod tests { apply_read_offset_limit: true, ..Default::default() }); - let result = (tool.executor)(serde_json::json!({"file_path": "/test.txt"}), ToolContext { - env, - cancel: CancellationToken::new(), - tool_env: None, - }) + let result = (tool.executor)( + serde_json::json!({"file_path": "/test.txt"}), + ToolContext { + env, + cancel: CancellationToken::new(), + tool_env: None, + }, + ) .await; assert_eq!(result.unwrap(), " 1 | hello\n 2 | world"); } @@ -680,8 +683,8 @@ mod tests { let result = (tool.executor)( serde_json::json!({"file_path": "/out.txt", "content": "hello"}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), + env: env_clone, + cancel: CancellationToken::new(), tool_env: None, }, ) @@ -710,8 +713,8 @@ mod tests { "new_string": "goodbye" }), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), + env: env_clone, + cancel: CancellationToken::new(), tool_env: None, }, ) @@ -792,8 +795,8 @@ mod tests { "replace_all": true }), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), + env: env_clone, + cancel: CancellationToken::new(), tool_env: None, }, ) @@ -809,19 +812,22 @@ mod tests { let tool = make_shell_tool(); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: "hello".into(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: "hello".into(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 10, }, ..Default::default() }); - let result = (tool.executor)(serde_json::json!({"command": "echo hello"}), ToolContext { - env, - cancel: CancellationToken::new(), - tool_env: None, - }) + let result = (tool.executor)( + serde_json::json!({"command": "echo hello"}), + ToolContext { + env, + cancel: CancellationToken::new(), + tool_env: None, + }, + ) .await; let output = result.unwrap(); assert!(output.contains("Exit code: 0")); @@ -836,8 +842,8 @@ mod tests { let _result = (tool.executor)( serde_json::json!({"command": "sleep 1", "timeout_ms": 5000}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), + env: env_clone, + cancel: CancellationToken::new(), tool_env: None, }, ) @@ -850,19 +856,22 @@ mod tests { let tool = make_shell_tool(); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: String::new(), - stderr: "error".into(), - exit_code: 1, - timed_out: false, + stdout: String::new(), + stderr: "error".into(), + exit_code: 1, + timed_out: false, duration_ms: 10, }, ..Default::default() }); - let result = (tool.executor)(serde_json::json!({"command": "false"}), ToolContext { - env, - cancel: CancellationToken::new(), - tool_env: None, - }) + let result = (tool.executor)( + serde_json::json!({"command": "false"}), + ToolContext { + env, + cancel: CancellationToken::new(), + tool_env: None, + }, + ) .await; let output = result.unwrap(); assert!(output.contains("Exit code: 1")); @@ -874,19 +883,22 @@ mod tests { let tool = make_shell_tool(); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: String::new(), - stderr: String::new(), - exit_code: -1, - timed_out: true, + stdout: String::new(), + stderr: String::new(), + exit_code: -1, + timed_out: true, duration_ms: 10000, }, ..Default::default() }); - let result = (tool.executor)(serde_json::json!({"command": "sleep 100"}), ToolContext { - env, - cancel: CancellationToken::new(), - tool_env: None, - }) + let result = (tool.executor)( + serde_json::json!({"command": "sleep 100"}), + ToolContext { + env, + cancel: CancellationToken::new(), + tool_env: None, + }, + ) .await; let output = result.unwrap(); assert!(output.starts_with("Command timed out.\n")); @@ -902,8 +914,8 @@ mod tests { let _result = (tool.executor)( serde_json::json!({"command": "echo $MY_KEY"}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), + env: env_clone, + cancel: CancellationToken::new(), tool_env: Some(tool_env.clone()), }, ) @@ -917,11 +929,14 @@ mod tests { let tool = make_shell_tool(); let env = Arc::new(MockSandbox::default()); let env_clone: Arc = env.clone(); - let _result = (tool.executor)(serde_json::json!({"command": "echo hello"}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env: None, - }) + let _result = (tool.executor)( + serde_json::json!({"command": "echo hello"}), + ToolContext { + env: env_clone, + cancel: CancellationToken::new(), + tool_env: None, + }, + ) .await; let captured = env.captured_env_vars.lock().unwrap().clone(); assert_eq!(captured, None); @@ -932,10 +947,10 @@ mod tests { let tool = make_web_fetch_tool(None); let env = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: "fetched content".into(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: "fetched content".into(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 100, }, ..Default::default() @@ -946,8 +961,8 @@ mod tests { let _result = (tool.executor)( serde_json::json!({"url": "https://example.com"}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), + env: env_clone, + cancel: CancellationToken::new(), tool_env: Some(tool_env.clone()), }, ) @@ -966,11 +981,14 @@ mod tests { ], ..Default::default() }); - let result = (tool.executor)(serde_json::json!({"pattern": "fn"}), ToolContext { - env, - cancel: CancellationToken::new(), - tool_env: None, - }) + let result = (tool.executor)( + serde_json::json!({"pattern": "fn"}), + ToolContext { + env, + cancel: CancellationToken::new(), + tool_env: None, + }, + ) .await; let output = result.unwrap(); assert!(output.contains("src/main.rs:10:fn main()")); @@ -984,11 +1002,14 @@ mod tests { glob_results: vec!["src/main.rs".into(), "src/lib.rs".into()], ..Default::default() }); - let result = (tool.executor)(serde_json::json!({"pattern": "src/**/*.rs"}), ToolContext { - env, - cancel: CancellationToken::new(), - tool_env: None, - }) + let result = (tool.executor)( + serde_json::json!({"pattern": "src/**/*.rs"}), + ToolContext { + env, + cancel: CancellationToken::new(), + tool_env: None, + }, + ) .await; let output = result.unwrap(); assert!(output.contains("src/main.rs")); @@ -999,11 +1020,14 @@ mod tests { async fn web_search_missing_api_key_returns_error() { let tool = make_web_search_tool_with_api_key(None); let env: Arc = Arc::new(MockSandbox::default()); - let result = (tool.executor)(serde_json::json!({"query": "test"}), ToolContext { - env, - cancel: CancellationToken::new(), - tool_env: None, - }) + let result = (tool.executor)( + serde_json::json!({"query": "test"}), + ToolContext { + env, + cancel: CancellationToken::new(), + tool_env: None, + }, + ) .await; let err = result.unwrap_err(); assert!( @@ -1016,11 +1040,14 @@ mod tests { async fn web_search_missing_query_returns_error() { let tool = make_web_search_tool_with_api_key(Some("fake-key".into())); let env: Arc = Arc::new(MockSandbox::default()); - let result = (tool.executor)(serde_json::json!({}), ToolContext { - env, - cancel: CancellationToken::new(), - tool_env: None, - }) + let result = (tool.executor)( + serde_json::json!({}), + ToolContext { + env, + cancel: CancellationToken::new(), + tool_env: None, + }, + ) .await; let err = result.unwrap_err(); assert!( @@ -1057,10 +1084,10 @@ mod tests { let tool = make_web_fetch_tool(None); let env = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: "

hello

".into(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: "

hello

".into(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 100, }, ..Default::default() @@ -1069,8 +1096,8 @@ mod tests { let result = (tool.executor)( serde_json::json!({"url": "https://example.com"}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), + env: env_clone, + cancel: CancellationToken::new(), tool_env: None, }, ) @@ -1127,8 +1154,8 @@ mod tests { let _result = (tool.executor)( serde_json::json!({"url": "https://example.com", "timeout_ms": 15000}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), + env: env_clone, + cancel: CancellationToken::new(), tool_env: None, }, ) @@ -1149,8 +1176,8 @@ mod tests { let _result = (tool.executor)( serde_json::json!({"url": "https://example.com", "timeout_ms": 120_000}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), + env: env_clone, + cancel: CancellationToken::new(), tool_env: None, }, ) @@ -1169,10 +1196,10 @@ mod tests { let tool = make_web_fetch_tool(None); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: large_content, - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: large_content, + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 100, }, ..Default::default() @@ -1196,10 +1223,10 @@ mod tests { let tool = make_web_fetch_tool(None); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: String::new(), - stderr: "curl: (6) Could not resolve host".into(), - exit_code: 6, - timed_out: false, + stdout: String::new(), + stderr: "curl: (6) Could not resolve host".into(), + exit_code: 6, + timed_out: false, duration_ms: 100, }, ..Default::default() @@ -1236,18 +1263,17 @@ mod tests { client, model_id: ModelHandle::ByName { provider: fabro_model::Provider::Anthropic, - model: "mock-model".to_string(), + model: "mock-model".to_string(), }, }; let tool = make_web_fetch_tool(Some(summarizer)); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: "

Lots of content about Rust...

" - .into(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: "

Lots of content about Rust...

".into(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 100, }, ..Default::default() @@ -1273,12 +1299,11 @@ mod tests { let tool = make_web_fetch_tool(None); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: - "

Rust is a systems programming language.

" - .into(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: "

Rust is a systems programming language.

" + .into(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 100, }, ..Default::default() @@ -1305,14 +1330,15 @@ mod tests { #[tokio::test] async fn web_fetch_summarizer_routes_to_specified_provider() { - use fabro_llm::error::{ProviderErrorDetail, ProviderErrorKind, SdkError}; + use fabro_llm::Error as LlmError; + use fabro_llm::error::{ProviderErrorDetail, ProviderErrorKind}; use crate::test_support::{MockErrorProvider, MockLlmProvider, text_response}; // "other_provider" is the default — it rejects all requests. let default_provider: Arc = Arc::new(MockErrorProvider { - error: SdkError::Provider { - kind: ProviderErrorKind::NotFound, + error: LlmError::Provider { + kind: ProviderErrorKind::NotFound, detail: Box::new(ProviderErrorDetail::new( "model not found", "other_provider", @@ -1336,17 +1362,17 @@ mod tests { client, model_id: ModelHandle::ByName { provider: fabro_model::Provider::Anthropic, - model: "target-model".to_string(), + model: "target-model".to_string(), }, }; let tool = make_web_fetch_tool(Some(summarizer)); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: "

Page content

".into(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: "

Page content

".into(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 100, }, ..Default::default() @@ -1429,11 +1455,14 @@ mod tests { // read_file tool should mark the file as agent-read let tool = make_read_file_tool(); - (tool.executor)(serde_json::json!({"file_path": "a.ts"}), ToolContext { - env: Arc::clone(&env), - cancel: CancellationToken::new(), - tool_env: None, - }) + (tool.executor)( + serde_json::json!({"file_path": "a.ts"}), + ToolContext { + env: Arc::clone(&env), + cancel: CancellationToken::new(), + tool_env: None, + }, + ) .await .unwrap(); @@ -1458,11 +1487,14 @@ mod tests { // grep tool should mark matched files as agent-read let tool = make_grep_tool(); - (tool.executor)(serde_json::json!({"pattern": "content"}), ToolContext { - env: Arc::clone(&env), - cancel: CancellationToken::new(), - tool_env: None, - }) + (tool.executor)( + serde_json::json!({"pattern": "content"}), + ToolContext { + env: Arc::clone(&env), + cancel: CancellationToken::new(), + tool_env: None, + }, + ) .await .unwrap(); diff --git a/lib/crates/fabro-agent/src/types.rs b/lib/crates/fabro-agent/src/types.rs index 40391d0b4..6d4532db1 100644 --- a/lib/crates/fabro-agent/src/types.rs +++ b/lib/crates/fabro-agent/src/types.rs @@ -1,10 +1,10 @@ use std::time::SystemTime; -use fabro_llm::error::SdkError; +use fabro_llm::Error as LlmError; use fabro_llm::types::{ContentPart, ThinkingData, TokenCounts, ToolCall, ToolResult}; use serde::{Deserialize, Serialize}; -use crate::error::AgentError; +use crate::error::Error; mod system_time_iso8601 { use std::time::SystemTime; @@ -34,36 +34,36 @@ mod system_time_iso8601 { #[derive(Debug, Clone)] pub enum Turn { User { - content: String, + content: String, timestamp: SystemTime, }, Assistant { - content: String, - tool_calls: Vec, + content: String, + tool_calls: Vec, /// Provider-specific content parts (e.g. `OpenAI` reasoning items, /// `Anthropic` thinking blocks with signatures) preserved for /// round-tripping. Reasoning/thinking text is stored here as /// `ContentPart::Thinking`. provider_parts: Vec, - usage: Box, - response_id: String, - timestamp: SystemTime, + usage: Box, + response_id: String, + timestamp: SystemTime, }, ToolResults { - results: Vec, + results: Vec, timestamp: SystemTime, }, /// Injected content sent as a system-role message to the LLM (maps to /// `Role::System`). System { - content: String, + content: String, timestamp: SystemTime, }, /// Injected steering content sent as a user-role message to the LLM (maps /// to `Role::User`). Used to guide the assistant's behavior /// mid-conversation without appearing as actual user input. Steering { - content: String, + content: String, timestamp: SystemTime, }, } @@ -101,7 +101,7 @@ pub enum AgentEvent { #[serde(default, skip_serializing_if = "Option::is_none")] provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - model: Option, + model: Option, }, SessionEnded, ProcessingEnd, @@ -111,14 +111,14 @@ pub enum AgentEvent { AssistantTextStart, /// Replaces the current in-progress assistant output buffers. AssistantOutputReplace { - text: String, + text: String, #[serde(default, skip_serializing_if = "Option::is_none")] reasoning: Option, }, AssistantMessage { - text: String, - model: String, - usage: TokenCounts, + text: String, + model: String, + usage: TokenCounts, tool_call_count: usize, }, TextDelta { @@ -128,24 +128,24 @@ pub enum AgentEvent { delta: String, }, ToolCallStarted { - tool_name: String, + tool_name: String, tool_call_id: String, - arguments: serde_json::Value, + arguments: serde_json::Value, }, ToolCallOutputDelta { delta: String, }, ToolCallCompleted { - tool_name: String, + tool_name: String, tool_call_id: String, - output: serde_json::Value, - is_error: bool, + output: serde_json::Value, + is_error: bool, }, Error { - error: AgentError, + error: Error, }, Warning { - kind: String, + kind: String, message: String, details: serde_json::Value, }, @@ -160,49 +160,49 @@ pub enum AgentEvent { text: String, }, CompactionStarted { - estimated_tokens: usize, + estimated_tokens: usize, context_window_size: usize, }, CompactionCompleted { - original_turn_count: usize, - preserved_turn_count: usize, + original_turn_count: usize, + preserved_turn_count: usize, summary_token_estimate: usize, - tracked_file_count: usize, + tracked_file_count: usize, }, LlmRetry { - provider: String, - model: String, - attempt: usize, + provider: String, + model: String, + attempt: usize, delay_secs: f64, - error: SdkError, + error: LlmError, }, SubAgentSpawned { agent_id: String, - depth: usize, - task: String, + depth: usize, + task: String, }, SubAgentCompleted { - agent_id: String, - depth: usize, - success: bool, + agent_id: String, + depth: usize, + success: bool, turns_used: usize, }, SubAgentFailed { agent_id: String, - depth: usize, - error: AgentError, + depth: usize, + error: Error, }, SubAgentClosed { agent_id: String, - depth: usize, + depth: usize, }, McpServerReady { server_name: String, - tool_count: usize, + tool_count: usize, }, McpServerFailed { server_name: String, - error: String, + error: String, }, } @@ -412,10 +412,10 @@ impl AgentEvent { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SessionEvent { - pub event: AgentEvent, + pub event: AgentEvent, #[serde(with = "system_time_iso8601")] - pub timestamp: SystemTime, - pub session_id: String, + pub timestamp: SystemTime, + pub session_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub parent_session_id: Option, } @@ -427,18 +427,21 @@ mod tests { #[test] fn session_event_construction() { let event = SessionEvent { - event: AgentEvent::SessionStarted { + event: AgentEvent::SessionStarted { provider: Some("anthropic".into()), - model: Some("claude-opus".into()), + model: Some("claude-opus".into()), }, - timestamp: SystemTime::now(), - session_id: "sess_1".into(), + timestamp: SystemTime::now(), + session_id: "sess_1".into(), parent_session_id: None, }; - assert!(matches!(event.event, AgentEvent::SessionStarted { - provider: Some(_), - model: Some(_), - })); + assert!(matches!( + event.event, + AgentEvent::SessionStarted { + provider: Some(_), + model: Some(_), + } + )); assert_eq!(event.session_id, "sess_1"); assert_eq!(event.parent_session_id, None); } @@ -446,24 +449,30 @@ mod tests { #[test] fn compaction_events_constructible() { let started = AgentEvent::CompactionStarted { - estimated_tokens: 5000, + estimated_tokens: 5000, context_window_size: 8000, }; - assert!(matches!(started, AgentEvent::CompactionStarted { - estimated_tokens: 5000, - .. - })); + assert!(matches!( + started, + AgentEvent::CompactionStarted { + estimated_tokens: 5000, + .. + } + )); let completed = AgentEvent::CompactionCompleted { - original_turn_count: 20, - preserved_turn_count: 6, - summary_token_estimate: 500, - tracked_file_count: 3, - }; - assert!(matches!(completed, AgentEvent::CompactionCompleted { original_turn_count: 20, - .. - })); + preserved_turn_count: 6, + summary_token_estimate: 500, + tracked_file_count: 3, + }; + assert!(matches!( + completed, + AgentEvent::CompactionCompleted { + original_turn_count: 20, + .. + } + )); } #[test] @@ -480,36 +489,39 @@ mod tests { fn subagent_spawned_constructible() { let event = AgentEvent::SubAgentSpawned { agent_id: "sa-1".into(), - depth: 1, - task: "list files".into(), - }; - assert!(matches!(event, AgentEvent::SubAgentSpawned { depth: 1, - .. - })); + task: "list files".into(), + }; + assert!(matches!( + event, + AgentEvent::SubAgentSpawned { depth: 1, .. } + )); } #[test] fn subagent_completed_constructible() { let event = AgentEvent::SubAgentCompleted { - agent_id: "sa-1".into(), - depth: 1, - success: true, - turns_used: 5, - }; - assert!(matches!(event, AgentEvent::SubAgentCompleted { + agent_id: "sa-1".into(), + depth: 1, success: true, turns_used: 5, - .. - })); + }; + assert!(matches!( + event, + AgentEvent::SubAgentCompleted { + success: true, + turns_used: 5, + .. + } + )); } #[test] fn subagent_failed_constructible() { let event = AgentEvent::SubAgentFailed { agent_id: "sa-1".into(), - depth: 0, - error: AgentError::ToolExecution("timeout".into()), + depth: 0, + error: Error::ToolExecution("timeout".into()), }; assert!(matches!(event, AgentEvent::SubAgentFailed { depth: 0, .. })); } @@ -518,7 +530,7 @@ mod tests { fn subagent_closed_constructible() { let event = AgentEvent::SubAgentClosed { agent_id: "sa-1".into(), - depth: 2, + depth: 2, }; assert!(matches!(event, AgentEvent::SubAgentClosed { depth: 2, .. })); } @@ -528,23 +540,23 @@ mod tests { let events = vec![ AgentEvent::SubAgentSpawned { agent_id: "sa-1".into(), - depth: 0, - task: "test".into(), + depth: 0, + task: "test".into(), }, AgentEvent::SubAgentCompleted { - agent_id: "sa-1".into(), - depth: 0, - success: true, + agent_id: "sa-1".into(), + depth: 0, + success: true, turns_used: 3, }, AgentEvent::SubAgentFailed { agent_id: "sa-1".into(), - depth: 0, - error: AgentError::ToolExecution("oops".into()), + depth: 0, + error: Error::ToolExecution("oops".into()), }, AgentEvent::SubAgentClosed { agent_id: "sa-1".into(), - depth: 0, + depth: 0, }, ]; let json = serde_json::to_string(&events).unwrap(); @@ -555,12 +567,12 @@ mod tests { #[test] fn session_event_serde_round_trip_without_parent_session_id() { let event = SessionEvent { - event: AgentEvent::SessionStarted { + event: AgentEvent::SessionStarted { provider: Some("anthropic".into()), - model: Some("claude-opus".into()), + model: Some("claude-opus".into()), }, - timestamp: SystemTime::now(), - session_id: "sess_42".into(), + timestamp: SystemTime::now(), + session_id: "sess_42".into(), parent_session_id: None, }; let json = serde_json::to_string(&event).unwrap(); @@ -573,21 +585,24 @@ mod tests { let deserialized: SessionEvent = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.session_id, "sess_42"); assert_eq!(deserialized.parent_session_id, None); - assert!(matches!(deserialized.event, AgentEvent::SessionStarted { - provider: Some(_), - model: Some(_), - })); + assert!(matches!( + deserialized.event, + AgentEvent::SessionStarted { + provider: Some(_), + model: Some(_), + } + )); } #[test] fn session_event_serde_round_trip_with_parent_session_id() { let event = SessionEvent { - event: AgentEvent::SessionStarted { + event: AgentEvent::SessionStarted { provider: Some("openai".into()), - model: Some("gpt-5.4".into()), + model: Some("gpt-5.4".into()), }, - timestamp: SystemTime::now(), - session_id: "sess_child".into(), + timestamp: SystemTime::now(), + session_id: "sess_child".into(), parent_session_id: Some("sess_parent".into()), }; let json = serde_json::to_string(&event).unwrap(); @@ -606,19 +621,19 @@ mod tests { fn mcp_server_ready_constructible() { let event = AgentEvent::McpServerReady { server_name: "filesystem".into(), - tool_count: 3, - }; - assert!(matches!(event, AgentEvent::McpServerReady { tool_count: 3, - .. - })); + }; + assert!(matches!( + event, + AgentEvent::McpServerReady { tool_count: 3, .. } + )); } #[test] fn mcp_server_failed_constructible() { let event = AgentEvent::McpServerFailed { server_name: "broken".into(), - error: "connection refused".into(), + error: "connection refused".into(), }; assert!( matches!(event, AgentEvent::McpServerFailed { server_name, .. } if server_name == "broken") @@ -630,20 +645,20 @@ mod tests { let events = vec![ AgentEvent::McpServerReady { server_name: "fs".into(), - tool_count: 5, + tool_count: 5, }, AgentEvent::McpServerFailed { server_name: "bad".into(), - error: "timeout".into(), + error: "timeout".into(), }, ]; let json = serde_json::to_string(&events).unwrap(); let deserialized: Vec = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.len(), 2); - assert!(matches!(&deserialized[0], AgentEvent::McpServerReady { - tool_count: 5, - .. - })); + assert!(matches!( + &deserialized[0], + AgentEvent::McpServerReady { tool_count: 5, .. } + )); assert!(matches!( &deserialized[1], AgentEvent::McpServerFailed { .. } @@ -653,16 +668,16 @@ mod tests { #[test] fn agent_event_assistant_message() { let usage = TokenCounts { - input_tokens: 100, - output_tokens: 50, - cache_read_tokens: 80, + input_tokens: 100, + output_tokens: 50, + cache_read_tokens: 80, cache_write_tokens: 10, - reasoning_tokens: 20, + reasoning_tokens: 20, }; let event = AgentEvent::AssistantMessage { - text: "Hello".into(), - model: "test-model".into(), - usage: usage.clone(), + text: "Hello".into(), + model: "test-model".into(), + usage: usage.clone(), tool_call_count: 2, }; match &event { @@ -683,7 +698,7 @@ mod tests { #[test] fn agent_event_assistant_output_replace_roundtrip() { let event = AgentEvent::AssistantOutputReplace { - text: "Hello again".into(), + text: "Hello again".into(), reasoning: Some("Retrying from scratch".into()), }; let json = serde_json::to_string(&event).unwrap(); @@ -702,9 +717,9 @@ mod tests { #[test] fn error_event_serde_roundtrip_with_agent_error() { let event = AgentEvent::Error { - error: AgentError::Llm(SdkError::Network { + error: Error::Llm(LlmError::Network { message: "refused".into(), - source: None, + source: None, }), }; let json = serde_json::to_string(&event).unwrap(); @@ -721,19 +736,19 @@ mod tests { fn llm_retry_event_carries_sdk_error() { use fabro_llm::error::{ProviderErrorDetail, ProviderErrorKind}; let event = AgentEvent::LlmRetry { - provider: "openai".into(), - model: "gpt-4".into(), - attempt: 1, + provider: "openai".into(), + model: "gpt-4".into(), + attempt: 1, delay_secs: 2.0, - error: SdkError::Provider { - kind: ProviderErrorKind::RateLimit, + error: LlmError::Provider { + kind: ProviderErrorKind::RateLimit, detail: Box::new(ProviderErrorDetail { - message: "too fast".into(), - provider: "openai".into(), + message: "too fast".into(), + provider: "openai".into(), status_code: Some(429), - error_code: None, + error_code: None, retry_after: Some(2.0), - raw: None, + raw: None, }), }, }; @@ -752,8 +767,8 @@ mod tests { fn subagent_failed_carries_agent_error() { let event = AgentEvent::SubAgentFailed { agent_id: "sa-1".into(), - depth: 0, - error: AgentError::ToolExecution("cmd failed".into()), + depth: 0, + error: Error::ToolExecution("cmd failed".into()), }; let json = serde_json::to_string(&event).unwrap(); let deserialized: AgentEvent = serde_json::from_str(&json).unwrap(); @@ -768,11 +783,11 @@ mod tests { #[test] fn error_event_preserves_error_type_through_json() { let event = AgentEvent::Error { - error: AgentError::ToolExecution("cmd failed".into()), + error: Error::ToolExecution("cmd failed".into()), }; let json = serde_json::to_string(&event).unwrap(); let v: serde_json::Value = serde_json::from_str(&json).unwrap(); - // The error field should contain the AgentError's tagged type + // The error field should contain the Error's tagged type assert_eq!(v["Error"]["error"]["type"], "tool_execution"); } @@ -780,7 +795,7 @@ mod tests { fn mcp_server_failed_still_string() { let event = AgentEvent::McpServerFailed { server_name: "broken".into(), - error: "connection refused".into(), + error: "connection refused".into(), }; let json = serde_json::to_string(&event).unwrap(); let deserialized: AgentEvent = serde_json::from_str(&json).unwrap(); diff --git a/lib/crates/fabro-agent/src/v4a_patch.rs b/lib/crates/fabro-agent/src/v4a_patch.rs index eb45971b1..eb63aa9db 100644 --- a/lib/crates/fabro-agent/src/v4a_patch.rs +++ b/lib/crates/fabro-agent/src/v4a_patch.rs @@ -16,23 +16,23 @@ pub enum Change { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Hunk { pub context_line: String, - pub changes: Vec, - pub end_of_file: bool, + pub changes: Vec, + pub end_of_file: bool, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum PatchOperation { Add { - path: String, + path: String, content: String, }, Delete { path: String, }, Update { - path: String, + path: String, new_path: Option, - hunks: Vec, + hunks: Vec, }, } @@ -402,9 +402,9 @@ fn format_patch_error(error: &str, path: &str, content: &str) -> String { pub fn make_apply_patch_tool() -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "apply_patch".into(), + name: "apply_patch".into(), description: "Apply a v4a format patch to modify files".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "patch": { @@ -415,7 +415,7 @@ pub fn make_apply_patch_tool() -> RegisteredTool { "required": ["patch"] }), }, - executor: Arc::new(|args, ctx| { + executor: Arc::new(|args, ctx| { Box::pin(async move { let patch_text = args .get("patch") @@ -448,10 +448,13 @@ mod tests { let ops = parse_v4a_patch(patch).unwrap(); assert_eq!(ops.len(), 1); - assert_eq!(ops[0], PatchOperation::Add { - path: "src/new_file.rs".into(), - content: "fn main() {\n println!(\"hello\");\n}".into(), - }); + assert_eq!( + ops[0], + PatchOperation::Add { + path: "src/new_file.rs".into(), + content: "fn main() {\n println!(\"hello\");\n}".into(), + } + ); } #[test] @@ -463,9 +466,12 @@ mod tests { let ops = parse_v4a_patch(patch).unwrap(); assert_eq!(ops.len(), 1); - assert_eq!(ops[0], PatchOperation::Delete { - path: "src/old_file.rs".into(), - }); + assert_eq!( + ops[0], + PatchOperation::Delete { + path: "src/old_file.rs".into(), + } + ); } #[test] @@ -582,21 +588,21 @@ mod tests { let env = MutableMockSandbox::new(files); let ops = vec![PatchOperation::Update { - path: "src/game.py".into(), + path: "src/game.py".into(), new_path: None, - hunks: vec![ + hunks: vec![ Hunk { context_line: String::new(), - end_of_file: false, - changes: vec![ + end_of_file: false, + changes: vec![ Change::Remove("from src.cards import Suit".into()), Change::Add("from src.cards import Card, Suit".into()), ], }, Hunk { context_line: String::new(), - end_of_file: false, - changes: vec![ + end_of_file: false, + changes: vec![ Change::Remove(" stock: list = field(default_factory=list)".into()), Change::Remove(" waste: list = field(default_factory=list)".into()), Change::Add(" stock: list[Card] = field(default_factory=list)".into()), @@ -715,12 +721,12 @@ mod tests { let env = MutableMockSandbox::new(files); let ops = vec![PatchOperation::Update { - path: "src/lib.rs".into(), + path: "src/lib.rs".into(), new_path: None, - hunks: vec![Hunk { + hunks: vec![Hunk { context_line: String::new(), - end_of_file: false, - changes: vec![ + end_of_file: false, + changes: vec![ Change::Context("fn unchanged() {".into()), Change::Remove(" old_line();".into()), Change::Add(" new_line();".into()), @@ -746,21 +752,21 @@ mod tests { let env = MutableMockSandbox::new(files); let ops = vec![PatchOperation::Update { - path: "src/lib.rs".into(), + path: "src/lib.rs".into(), new_path: None, - hunks: vec![ + hunks: vec![ Hunk { context_line: "def setup():".into(), - end_of_file: false, - changes: vec![ + end_of_file: false, + changes: vec![ Change::Remove(" old_setup()".into()), Change::Add(" new_setup()".into()), ], }, Hunk { context_line: String::new(), - end_of_file: false, - changes: vec![ + end_of_file: false, + changes: vec![ Change::Remove(" old_teardown()".into()), Change::Add(" new_teardown()".into()), ], @@ -782,7 +788,7 @@ mod tests { async fn apply_patch_add_file() { let env = MutableMockSandbox::new(HashMap::new()); let ops = vec![PatchOperation::Add { - path: "src/new.rs".into(), + path: "src/new.rs".into(), content: "fn new() {}".into(), }]; @@ -803,12 +809,12 @@ mod tests { let env = MutableMockSandbox::new(files); let ops = vec![PatchOperation::Update { - path: "src/lib.rs".into(), + path: "src/lib.rs".into(), new_path: None, - hunks: vec![Hunk { + hunks: vec![Hunk { context_line: "fn hello() {".into(), - end_of_file: false, - changes: vec![ + end_of_file: false, + changes: vec![ Change::Remove(" println!(\"old\");".into()), Change::Add(" println!(\"new\");".into()), ], @@ -856,12 +862,12 @@ mod tests { let env = MutableMockSandbox::new(files); let ops = vec![PatchOperation::Update { - path: "src/game.py".into(), + path: "src/game.py".into(), new_path: None, - hunks: vec![Hunk { + hunks: vec![Hunk { context_line: "def nonexistent():".into(), - end_of_file: false, - changes: vec![ + end_of_file: false, + changes: vec![ Change::Remove(" old_body()".into()), Change::Add(" new_body()".into()), ], @@ -882,16 +888,16 @@ mod tests { let hunks = vec![ Hunk { context_line: String::new(), - end_of_file: false, - changes: vec![ + end_of_file: false, + changes: vec![ Change::Remove(" pass".into()), Change::Add(" return 1".into()), ], }, Hunk { context_line: String::new(), - end_of_file: false, - changes: vec![ + end_of_file: false, + changes: vec![ Change::Remove(" pass".into()), Change::Add(" return 2".into()), ], @@ -976,8 +982,8 @@ mod tests { let content = "def foo():\n pass\n\ndef bar():\n pass"; let hunks = vec![Hunk { context_line: String::new(), - end_of_file: true, - changes: vec![ + end_of_file: true, + changes: vec![ Change::Remove(" pass".into()), Change::Add(" return 99".into()), ], @@ -1025,12 +1031,12 @@ mod tests { let env = MutableMockSandbox::new(files); let ops = vec![PatchOperation::Update { - path: "src/old.py".into(), + path: "src/old.py".into(), new_path: Some("src/new.py".into()), - hunks: vec![Hunk { + hunks: vec![Hunk { context_line: "def hello():".into(), - end_of_file: false, - changes: vec![ + end_of_file: false, + changes: vec![ Change::Remove(" pass".into()), Change::Add(" return 1".into()), ], @@ -1057,8 +1063,8 @@ mod tests { let content = " indented\nindented"; let hunks = vec![Hunk { context_line: "indented".into(), - end_of_file: false, - changes: vec![Change::Add("extra".into())], + end_of_file: false, + changes: vec![Change::Add("extra".into())], }]; let result = apply_hunks(content, &hunks).unwrap(); // Should match line 1 (exact), so "extra" inserted after "indented" (line 1) @@ -1070,8 +1076,8 @@ mod tests { let content = "print(\u{201C}hello\u{201D})"; let hunks = vec![Hunk { context_line: "print(\"hello\")".into(), - end_of_file: false, - changes: vec![Change::Add("print(\"world\")".into())], + end_of_file: false, + changes: vec![Change::Add("print(\"world\")".into())], }]; let result = apply_hunks(content, &hunks).unwrap(); // Original line preserved, new line added after diff --git a/lib/crates/fabro-agent/tests/it/parity_matrix.rs b/lib/crates/fabro-agent/tests/it/parity_matrix.rs index 51d3b7311..69668efec 100644 --- a/lib/crates/fabro-agent/tests/it/parity_matrix.rs +++ b/lib/crates/fabro-agent/tests/it/parity_matrix.rs @@ -18,7 +18,7 @@ use tokio::sync::Mutex as AsyncMutex; #[derive(Clone)] struct OpenAiTwinOptions { base_url: String, - api_key: String, + api_key: String, } fn summarizer_model_id(provider: Provider) -> ModelHandle { @@ -30,22 +30,22 @@ fn summarizer_model_id(provider: Provider) -> ModelHandle { | Provider::Inception | Provider::OpenAiCompatible => ModelHandle::ByName { provider: Provider::OpenAi, - model: "gpt-5.4-mini".to_string(), + model: "gpt-5.4-mini".to_string(), }, Provider::Gemini => ModelHandle::ByName { provider: Provider::Gemini, - model: "gemini-3-flash-preview".to_string(), + model: "gemini-3-flash-preview".to_string(), }, Provider::Anthropic => ModelHandle::ByName { provider: Provider::Anthropic, - model: "claude-haiku-4-5".to_string(), + model: "claude-haiku-4-5".to_string(), }, } } fn build_summarizer(provider: Provider, client: &Client) -> WebFetchSummarizer { WebFetchSummarizer { - client: client.clone(), + client: client.clone(), model_id: summarizer_model_id(provider), } } diff --git a/lib/crates/fabro-checkpoint/src/author.rs b/lib/crates/fabro-checkpoint/src/author.rs index d8e364c69..9cd815406 100644 --- a/lib/crates/fabro-checkpoint/src/author.rs +++ b/lib/crates/fabro-checkpoint/src/author.rs @@ -6,14 +6,14 @@ use fabro_types::settings::run::{GitAuthorLayer, GitAuthorSettings}; /// Resolved git author identity for checkpoint commits. #[derive(Debug, Clone, PartialEq)] pub struct GitAuthor { - pub name: String, + pub name: String, pub email: String, } impl Default for GitAuthor { fn default() -> Self { Self { - name: "Fabro".into(), + name: "Fabro".into(), email: "noreply@fabro.sh".into(), } } @@ -24,7 +24,7 @@ impl GitAuthor { pub fn from_options(name: Option, email: Option) -> Self { let defaults = Self::default(); Self { - name: name.unwrap_or(defaults.name), + name: name.unwrap_or(defaults.name), email: email.unwrap_or(defaults.email), } } diff --git a/lib/crates/fabro-checkpoint/src/branch.rs b/lib/crates/fabro-checkpoint/src/branch.rs index 52958ee87..f275222f0 100644 --- a/lib/crates/fabro-checkpoint/src/branch.rs +++ b/lib/crates/fabro-checkpoint/src/branch.rs @@ -7,11 +7,11 @@ use crate::git::{FileMode, Store, TreeEntries}; /// Metadata about a commit, returned by `log`. #[derive(Debug)] pub struct CommitInfo { - pub oid: Oid, - pub message: String, - pub author_name: String, + pub oid: Oid, + pub message: String, + pub author_name: String, pub author_email: String, - pub time: git2::Time, + pub time: git2::Time, } /// Key-value storage on a single git branch. Each write creates one commit. @@ -19,8 +19,8 @@ pub struct CommitInfo { /// the previous. pub struct BranchStore<'a> { objects: &'a Store, - branch: String, - author: Signature<'static>, + branch: String, + author: Signature<'static>, } impl<'a> BranchStore<'a> { diff --git a/lib/crates/fabro-checkpoint/src/error.rs b/lib/crates/fabro-checkpoint/src/error.rs index 6d59481c7..fa5df4467 100644 --- a/lib/crates/fabro-checkpoint/src/error.rs +++ b/lib/crates/fabro-checkpoint/src/error.rs @@ -9,7 +9,7 @@ pub enum Error { #[error("reading file {path}: {source}")] ReadFile { - path: PathBuf, + path: PathBuf, source: std::io::Error, }, diff --git a/lib/crates/fabro-checkpoint/src/git.rs b/lib/crates/fabro-checkpoint/src/git.rs index 30f4eca69..a60d9aed2 100644 --- a/lib/crates/fabro-checkpoint/src/git.rs +++ b/lib/crates/fabro-checkpoint/src/git.rs @@ -34,7 +34,7 @@ impl FileMode { /// A single entry in a flat tree map. #[derive(Debug, Clone)] pub struct TreeEntry { - pub oid: Oid, + pub oid: Oid, pub filemode: FileMode, } @@ -125,7 +125,7 @@ impl Store { /// unix. pub fn write_blob_from_file(&self, path: &Path) -> Result<(Oid, FileMode)> { let content = std::fs::read(path).map_err(|e| Error::ReadFile { - path: path.to_path_buf(), + path: path.to_path_buf(), source: e, })?; let mode = detect_filemode(path); @@ -250,14 +250,14 @@ fn read_tree_recursive( /// Intermediate structure for building nested git trees from flat paths. struct DirNode { files: BTreeMap, - dirs: BTreeMap, + dirs: BTreeMap, } impl DirNode { fn new() -> Self { Self { files: BTreeMap::new(), - dirs: BTreeMap::new(), + dirs: BTreeMap::new(), } } } diff --git a/lib/crates/fabro-checkpoint/src/metadata.rs b/lib/crates/fabro-checkpoint/src/metadata.rs index d0afc7d15..28e864772 100644 --- a/lib/crates/fabro-checkpoint/src/metadata.rs +++ b/lib/crates/fabro-checkpoint/src/metadata.rs @@ -15,14 +15,14 @@ use crate::git::Store; /// (`fabro/meta/{run_id}`) so that runs can be resumed from git alone. pub struct MetadataStore { repo_path: PathBuf, - author: GitAuthor, + author: GitAuthor, } impl MetadataStore { pub fn new(repo_path: impl Into, author: &GitAuthor) -> Self { Self { repo_path: repo_path.into(), - author: author.clone(), + author: author.clone(), } } @@ -364,10 +364,11 @@ mod tests { let checkpoint_json = serde_json::to_vec_pretty(&test_checkpoint("node_a", Vec::new(), None)).unwrap(); store - .write_checkpoint(&run_id, &checkpoint_json, &[( - "artifacts/response.plan.json", - artifact_data.as_slice(), - )]) + .write_checkpoint( + &run_id, + &checkpoint_json, + &[("artifacts/response.plan.json", artifact_data.as_slice())], + ) .unwrap(); let read_back = MetadataStore::read_artifact(dir.path(), &run_id, "response.plan") @@ -428,10 +429,10 @@ mod tests { let run_id = fixtures::RUN_6.to_string(); let store = MetadataStore::new(dir.path(), &GitAuthor::default()); let start_record = StartRecord { - run_id: fixtures::RUN_6, + run_id: fixtures::RUN_6, start_time: Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).single().unwrap(), run_branch: Some("fabro/run/test".to_string()), - base_sha: None, + base_sha: None, }; let bytes = serde_json::to_vec_pretty(&start_record).unwrap(); store.init_run(&run_id, &[("start.json", &bytes)]).unwrap(); diff --git a/lib/crates/fabro-checkpoint/src/trailer.rs b/lib/crates/fabro-checkpoint/src/trailer.rs index cd97085af..a94dff43c 100644 --- a/lib/crates/fabro-checkpoint/src/trailer.rs +++ b/lib/crates/fabro-checkpoint/src/trailer.rs @@ -2,7 +2,7 @@ use std::fmt::Write; /// A git commit message trailer (key-value pair). pub struct Trailer<'a> { - pub key: &'a str, + pub key: &'a str, pub value: &'a str, } @@ -92,20 +92,26 @@ mod tests { #[test] fn append_to_simple_message() { - let result = append("Initial commit", &Trailer { - key: "My-Checkpoint", - value: "abc123", - }); + let result = append( + "Initial commit", + &Trailer { + key: "My-Checkpoint", + value: "abc123", + }, + ); assert_eq!(result, "Initial commit\n\nMy-Checkpoint: abc123\n"); } #[test] fn append_to_message_with_existing_trailer() { let msg = "Initial commit\n\nSigned-off-by: Alice \n"; - let result = append(msg, &Trailer { - key: "My-Checkpoint", - value: "abc123", - }); + let result = append( + msg, + &Trailer { + key: "My-Checkpoint", + value: "abc123", + }, + ); assert_eq!( result, "Initial commit\n\nSigned-off-by: Alice \nMy-Checkpoint: abc123\n" @@ -115,10 +121,13 @@ mod tests { #[test] fn append_to_message_with_body_no_trailer() { let msg = "Initial commit\n\nThis is a longer description of the change.\n"; - let result = append(msg, &Trailer { - key: "My-Checkpoint", - value: "abc123", - }); + let result = append( + msg, + &Trailer { + key: "My-Checkpoint", + value: "abc123", + }, + ); assert_eq!( result, "Initial commit\n\nThis is a longer description of the change.\n\nMy-Checkpoint: abc123\n" @@ -167,16 +176,20 @@ mod tests { #[test] fn format_message_with_trailers() { - let result = format_message("Initial commit", "", &[ - Trailer { - key: "Signed-off-by", - value: "Alice", - }, - Trailer { - key: "My-Checkpoint", - value: "abc123", - }, - ]); + let result = format_message( + "Initial commit", + "", + &[ + Trailer { + key: "Signed-off-by", + value: "Alice", + }, + Trailer { + key: "My-Checkpoint", + value: "abc123", + }, + ], + ); assert_eq!( result, "Initial commit\n\nSigned-off-by: Alice\nMy-Checkpoint: abc123\n" @@ -185,10 +198,14 @@ mod tests { #[test] fn format_message_with_body_and_trailers() { - let result = format_message("Initial commit", "Description here", &[Trailer { - key: "My-Checkpoint", - value: "abc123", - }]); + let result = format_message( + "Initial commit", + "Description here", + &[Trailer { + key: "My-Checkpoint", + value: "abc123", + }], + ); assert_eq!( result, "Initial commit\n\nDescription here\n\nMy-Checkpoint: abc123\n" diff --git a/lib/crates/fabro-cli/src/args.rs b/lib/crates/fabro-cli/src/args.rs index 216d0e532..be975cf26 100644 --- a/lib/crates/fabro-cli/src/args.rs +++ b/lib/crates/fabro-cli/src/args.rs @@ -261,17 +261,17 @@ pub(crate) struct LogsArgs { pub(crate) server: ServerTargetArgs, /// Run ID prefix or workflow name (most recent run) - pub(crate) run: String, + pub(crate) run: String, /// Follow log output #[arg(short, long)] pub(crate) follow: bool, /// Logs since timestamp or relative (e.g. "42m", "2h", /// "2026-01-02T13:00:00Z") #[arg(long)] - pub(crate) since: Option, + pub(crate) since: Option, /// Lines from end (default: all) #[arg(short = 'n', long)] - pub(crate) tail: Option, + pub(crate) tail: Option, /// Formatted colored output with rendered assistant text #[arg(short = 'p', long)] pub(crate) pretty: bool, @@ -403,9 +403,9 @@ pub(crate) struct CpArgs { pub(crate) server: ServerTargetArgs, /// Source: : or local path - pub(crate) src: String, + pub(crate) src: String, /// Destination: : or local path - pub(crate) dst: String, + pub(crate) dst: String, /// Recurse into directories #[arg(short, long)] pub(crate) recursive: bool, @@ -417,18 +417,18 @@ pub(crate) struct PreviewArgs { pub(crate) server: ServerTargetArgs, /// Run ID or prefix - pub(crate) run: String, + pub(crate) run: String, /// Port number - pub(crate) port: u16, + pub(crate) port: u16, /// Generate a signed URL (embeds auth token, no headers needed) #[arg(long)] pub(crate) signed: bool, /// Signed URL expiry in seconds (default 3600, requires --signed) #[arg(long, default_value = "3600", requires = "signed")] - pub(crate) ttl: i32, + pub(crate) ttl: i32, /// Open URL in browser (implies --signed) #[arg(long)] - pub(crate) open: bool, + pub(crate) open: bool, } #[derive(Args)] @@ -437,10 +437,10 @@ pub(crate) struct SshArgs { pub(crate) server: ServerTargetArgs, /// Run ID or prefix - pub(crate) run: String, + pub(crate) run: String, /// SSH access expiry in minutes (default 60) #[arg(long, default_value = "60")] - pub(crate) ttl: f64, + pub(crate) ttl: f64, /// Print the SSH command instead of connecting #[arg(long)] pub(crate) print: bool, @@ -452,7 +452,7 @@ pub(crate) struct DiffArgs { pub(crate) server: ServerTargetArgs, /// Run ID or prefix - pub(crate) run: String, + pub(crate) run: String, /// Show diff for a specific node #[arg(long)] pub(crate) node: Option, @@ -492,7 +492,7 @@ pub(crate) struct SecretRmArgs { #[derive(Args)] pub(crate) struct SecretSetArgs { /// Name of the secret - pub(crate) key: String, + pub(crate) key: String, /// Value to store pub(crate) value: String, } @@ -661,7 +661,7 @@ pub(crate) struct PrCreateArgs { pub(crate) run_id: String, /// LLM model for generating PR description #[arg(long)] - pub(crate) model: Option, + pub(crate) model: Option, } #[derive(Args)] diff --git a/lib/crates/fabro-cli/src/command_context.rs b/lib/crates/fabro-cli/src/command_context.rs index 465984c95..f93bb9d8c 100644 --- a/lib/crates/fabro-cli/src/command_context.rs +++ b/lib/crates/fabro-cli/src/command_context.rs @@ -16,18 +16,18 @@ pub(crate) enum ServerMode { target_override: Option, }, ByStorageDir { - target_override: Option, + target_override: Option, storage_dir_override: Option, }, } pub(crate) struct CommandContext { - cwd: PathBuf, + cwd: PathBuf, base_config_path: PathBuf, machine_settings: SettingsLayer, - cli_settings: CliSettings, - server_mode: ServerMode, - server: OnceCell>, + cli_settings: CliSettings, + server_mode: ServerMode, + server: OnceCell>, } impl CommandContext { @@ -43,7 +43,7 @@ impl CommandContext { pub(crate) fn for_connection(args: &ServerConnectionArgs) -> Result { Self::new(ServerMode::ByStorageDir { - target_override: args.target.server.clone(), + target_override: args.target.server.clone(), storage_dir_override: args.storage_dir.clone_path(), }) } diff --git a/lib/crates/fabro-cli/src/commands/artifact/cp.rs b/lib/crates/fabro-cli/src/commands/artifact/cp.rs index c120f5a26..4cb33537e 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/cp.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/cp.rs @@ -180,11 +180,11 @@ mod tests { #[test] fn format_candidate_includes_retry() { let entry = super::super::ArtifactEntry { - node_slug: "retry_assets".to_string(), - retry: 2, - stage_id: fabro_types::StageId::new("retry_assets", 2), + node_slug: "retry_assets".to_string(), + retry: 2, + stage_id: fabro_types::StageId::new("retry_assets", 2), relative_path: "assets/retry/report.txt".to_string(), - size: 6, + size: 6, }; assert_eq!(format_candidate(&entry), "retry_assets:retry_2"); diff --git a/lib/crates/fabro-cli/src/commands/artifact/mod.rs b/lib/crates/fabro-cli/src/commands/artifact/mod.rs index f01c8aadf..0395903f8 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/mod.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/mod.rs @@ -12,11 +12,11 @@ use crate::server_runs::ServerSummaryLookup; #[derive(Clone, Debug, serde::Serialize)] pub(super) struct ArtifactEntry { #[serde(skip_serializing)] - pub(super) stage_id: StageId, - pub(super) node_slug: String, - pub(super) retry: u32, + pub(super) stage_id: StageId, + pub(super) node_slug: String, + pub(super) retry: u32, pub(super) relative_path: String, - pub(super) size: u64, + pub(super) size: u64, } pub(super) async fn resolve_artifacts( diff --git a/lib/crates/fabro-cli/src/commands/doctor.rs b/lib/crates/fabro-cli/src/commands/doctor.rs index 989a94f94..ac74284ba 100644 --- a/lib/crates/fabro-cli/src/commands/doctor.rs +++ b/lib/crates/fabro-cli/src/commands/doctor.rs @@ -22,11 +22,11 @@ use crate::command_context::CommandContext; use crate::shared::print_json_pretty; pub(crate) struct DepSpec { - pub name: &'static str, - command: &'static [&'static str], - pub required: bool, + pub name: &'static str, + command: &'static [&'static str], + pub required: bool, pub min_version: Version, - pattern: &'static LazyLock, + pattern: &'static LazyLock, } #[derive(Debug, Clone, PartialEq)] @@ -43,18 +43,18 @@ static DOT_RE: LazyLock = pub(crate) const DEP_SPECS: &[DepSpec] = &[ DepSpec { - name: "openssl", - command: &["openssl", "version"], - required: true, + name: "openssl", + command: &["openssl", "version"], + required: true, min_version: Version::new(3, 0, 0), - pattern: &OPENSSL_RE, + pattern: &OPENSSL_RE, }, DepSpec { - name: "dot", - command: &["dot", "-V"], - required: false, + name: "dot", + command: &["dot", "-V"], + required: false, min_version: Version::new(2, 0, 0), - pattern: &DOT_RE, + pattern: &DOT_RE, }, ]; @@ -156,31 +156,28 @@ pub(crate) fn check_config( ) -> CheckResult { match (settings_path, legacy_paths.is_empty()) { (Some(path), true) => CheckResult { - name: "Configuration".to_string(), - status: CheckStatus::Pass, - summary: path.display().to_string(), - details: vec![CheckDetail::new(format!("Loaded from {}", path.display()))], + name: "Configuration".to_string(), + status: CheckStatus::Pass, + summary: path.display().to_string(), + details: vec![CheckDetail::new(format!("Loaded from {}", path.display()))], remediation: None, }, (Some(path), false) => CheckResult { - name: "Configuration".to_string(), - status: CheckStatus::Warning, - summary: path.display().to_string(), - details: std::iter::once(CheckDetail::new(format!( - "Loaded from {}", - path.display() - ))) - .chain(legacy_paths.iter().map(|legacy| { - CheckDetail::new(format!("Ignoring legacy config file {}", legacy.display())) - })) - .collect(), + name: "Configuration".to_string(), + status: CheckStatus::Warning, + summary: path.display().to_string(), + details: std::iter::once(CheckDetail::new(format!("Loaded from {}", path.display()))) + .chain(legacy_paths.iter().map(|legacy| { + CheckDetail::new(format!("Ignoring legacy config file {}", legacy.display())) + })) + .collect(), remediation: Some("Delete or rename legacy config files".to_string()), }, (None, false) => CheckResult { - name: "Configuration".to_string(), - status: CheckStatus::Warning, - summary: "legacy config files ignored".to_string(), - details: legacy_paths + name: "Configuration".to_string(), + status: CheckStatus::Warning, + summary: "legacy config files ignored".to_string(), + details: legacy_paths .iter() .map(|legacy| { CheckDetail::new(format!("Found legacy config file {}", legacy.display())) @@ -193,10 +190,10 @@ pub(crate) fn check_config( remediation: Some("Create ~/.fabro/settings.toml".to_string()), }, (None, true) => CheckResult { - name: "Configuration".to_string(), - status: CheckStatus::Warning, - summary: "no settings config file found".to_string(), - details: vec![CheckDetail::new( + name: "Configuration".to_string(), + status: CheckStatus::Warning, + summary: "no settings config file found".to_string(), + details: vec![CheckDetail::new( "Create ~/.fabro/settings.toml to configure Fabro".to_string(), )], remediation: Some("Create ~/.fabro/settings.toml".to_string()), @@ -207,10 +204,10 @@ pub(crate) fn check_config( fn check_legacy_env(path: Option) -> CheckResult { match path { Some(path) => CheckResult { - name: "Legacy .env".to_string(), - status: CheckStatus::Warning, - summary: "legacy secrets file detected".to_string(), - details: vec![CheckDetail::new(format!( + name: "Legacy .env".to_string(), + status: CheckStatus::Warning, + summary: "legacy secrets file detected".to_string(), + details: vec![CheckDetail::new(format!( "{} is no longer read by fabro", path.display() ))], @@ -220,10 +217,10 @@ fn check_legacy_env(path: Option) -> CheckResult { ), }, None => CheckResult { - name: "Legacy .env".to_string(), - status: CheckStatus::Pass, - summary: "not present".to_string(), - details: Vec::new(), + name: "Legacy .env".to_string(), + status: CheckStatus::Pass, + summary: "not present".to_string(), + details: Vec::new(), remediation: None, }, } @@ -233,20 +230,20 @@ fn check_version_parity(server_version: &str) -> CheckResult { let cli_version = FABRO_VERSION; if server_version == cli_version { CheckResult { - name: "Version parity".to_string(), - status: CheckStatus::Pass, - summary: cli_version.to_string(), - details: vec![CheckDetail::new(format!( + name: "Version parity".to_string(), + status: CheckStatus::Pass, + summary: cli_version.to_string(), + details: vec![CheckDetail::new(format!( "CLI and server are both {cli_version}" ))], remediation: None, } } else { CheckResult { - name: "Version parity".to_string(), - status: CheckStatus::Warning, - summary: format!("CLI {cli_version}, server {server_version}"), - details: vec![CheckDetail::new(format!( + name: "Version parity".to_string(), + status: CheckStatus::Warning, + summary: format!("CLI {cli_version}, server {server_version}"), + details: vec![CheckDetail::new(format!( "CLI version {cli_version} does not match server version {server_version}" ))], remediation: Some( @@ -269,15 +266,15 @@ fn convert_diagnostics_sections(sections: Vec) -> sections .into_iter() .map(|section| CheckSection { - title: section.title, + title: section.title, checks: section .checks .into_iter() .map(|check| CheckResult { - name: check.name, - status: convert_diagnostics_status(check.status), - summary: check.summary, - details: check + name: check.name, + status: convert_diagnostics_status(check.status), + summary: check.summary, + details: check .details .into_iter() .map(|detail| CheckDetail { @@ -345,9 +342,9 @@ pub(crate) async fn run_doctor( }; let mut report = CheckReport { - title: "Fabro Doctor".to_string(), + title: "Fabro Doctor".to_string(), sections: vec![CheckSection { - title: "Local".to_string(), + title: "Local".to_string(), checks: vec![ check_config( settings_config_path @@ -364,12 +361,12 @@ pub(crate) async fn run_doctor( Ok(ctx) => ctx, Err(err) => { report.sections.push(CheckSection { - title: "Server".to_string(), + title: "Server".to_string(), checks: vec![CheckResult { - name: "Fabro server".to_string(), - status: CheckStatus::Error, - summary: "settings resolution failed".to_string(), - details: vec![CheckDetail::new(err.to_string())], + name: "Fabro server".to_string(), + status: CheckStatus::Error, + summary: "settings resolution failed".to_string(), + details: vec![CheckDetail::new(err.to_string())], remediation: Some( "Fix the local CLI settings or provide `--server`, then run doctor again." .to_string(), @@ -394,12 +391,12 @@ pub(crate) async fn run_doctor( Ok(server) => server, Err(err) => { report.sections.push(CheckSection { - title: "Server".to_string(), + title: "Server".to_string(), checks: vec![CheckResult { - name: "Fabro server".to_string(), - status: CheckStatus::Error, - summary: "unreachable".to_string(), - details: vec![CheckDetail::new(err.to_string())], + name: "Fabro server".to_string(), + status: CheckStatus::Error, + summary: "unreachable".to_string(), + details: vec![CheckDetail::new(err.to_string())], remediation: Some( "Start or connect to the server with `--server` and run doctor again." .to_string(), @@ -424,12 +421,12 @@ pub(crate) async fn run_doctor( Ok(response) => response.into_inner(), Err(err) => { report.sections.push(CheckSection { - title: "Server".to_string(), + title: "Server".to_string(), checks: vec![CheckResult { - name: "Fabro server".to_string(), - status: CheckStatus::Error, - summary: "health check failed".to_string(), - details: vec![CheckDetail::new(err.to_string())], + name: "Fabro server".to_string(), + status: CheckStatus::Error, + summary: "health check failed".to_string(), + details: vec![CheckDetail::new(err.to_string())], remediation: Some( "Check that the server is reachable and responding to /health.".to_string(), ), @@ -462,12 +459,12 @@ pub(crate) async fn run_doctor( } Err(err) => { report.sections.push(CheckSection { - title: "Server".to_string(), + title: "Server".to_string(), checks: vec![CheckResult { - name: "Diagnostics".to_string(), - status: CheckStatus::Error, - summary: "probe failed".to_string(), - details: vec![CheckDetail::new(err.to_string())], + name: "Diagnostics".to_string(), + status: CheckStatus::Error, + summary: "probe failed".to_string(), + details: vec![CheckDetail::new(err.to_string())], remediation: Some( "Fix the server diagnostics failure and run `fabro doctor` again." .to_string(), @@ -553,14 +550,14 @@ mod tests { #[test] fn render_report_text_without_color_has_no_ansi() { let report = CheckReport { - title: "Fabro Doctor".to_string(), + title: "Fabro Doctor".to_string(), sections: vec![CheckSection { - title: "Local".to_string(), + title: "Local".to_string(), checks: vec![CheckResult { - name: "Configuration".to_string(), - status: CheckStatus::Pass, - summary: "loaded".to_string(), - details: vec![CheckDetail::new( + name: "Configuration".to_string(), + status: CheckStatus::Pass, + summary: "loaded".to_string(), + details: vec![CheckDetail::new( "Loaded from ~/.fabro/settings.toml".into(), )], remediation: None, diff --git a/lib/crates/fabro-cli/src/commands/exec.rs b/lib/crates/fabro-cli/src/commands/exec.rs index 0e73b6a62..7772ef774 100644 --- a/lib/crates/fabro-cli/src/commands/exec.rs +++ b/lib/crates/fabro-cli/src/commands/exec.rs @@ -38,7 +38,7 @@ fn runtime_mcp_server(name: &str, entry: &McpEntryLayer) -> McpServerSettings { } } McpEntryLayer::Http { url, headers, .. } => McpTransport::Http { - url: url.as_source(), + url: url.as_source(), headers: headers .iter() .map(|(key, value)| (key.clone(), value.as_source())) @@ -146,10 +146,10 @@ pub(crate) async fn execute(mut args: ExecArgs, globals: &GlobalArgs) -> Result< .mcps .values() .map(|server| McpServerSettings { - name: server.name.clone(), - transport: server.transport.clone(), + name: server.name.clone(), + transport: server.transport.clone(), startup_timeout_secs: server.startup_timeout_secs, - tool_timeout_secs: server.tool_timeout_secs, + tool_timeout_secs: server.tool_timeout_secs, }) .collect() } else if let Some(mcps) = cli_settings @@ -171,10 +171,10 @@ pub(crate) async fn execute(mut args: ExecArgs, globals: &GlobalArgs) -> Result< .mcps .values() .map(|server| McpServerSettings { - name: server.name.clone(), - transport: server.transport.clone(), + name: server.name.clone(), + transport: server.transport.clone(), startup_timeout_secs: server.startup_timeout_secs, - tool_timeout_secs: server.tool_timeout_secs, + tool_timeout_secs: server.tool_timeout_secs, }) .collect() }) diff --git a/lib/crates/fabro-cli/src/commands/graph.rs b/lib/crates/fabro-cli/src/commands/graph.rs index d7bc13a03..eeef38768 100644 --- a/lib/crates/fabro-cli/src/commands/graph.rs +++ b/lib/crates/fabro-cli/src/commands/graph.rs @@ -25,12 +25,12 @@ pub(crate) async fn run( let ctx = CommandContext::for_target(&args.target)?; let built = build_run_manifest(ManifestBuildInput { - workflow: args.workflow.clone(), - cwd: ctx.cwd().to_path_buf(), - args_layer: SettingsLayer::default(), - args: None, - run_id: None, - user_layer: load_settings_user()?, + workflow: args.workflow.clone(), + cwd: ctx.cwd().to_path_buf(), + args_layer: SettingsLayer::default(), + args: None, + run_id: None, + user_layer: load_settings_user()?, user_settings_path: Some(active_settings_path(None)), })?; let client = ctx.server().await?; @@ -47,8 +47,8 @@ pub(crate) async fn run( let rendered = client .render_workflow_graph(types::RenderWorkflowGraphRequest { - manifest: built.manifest, - format: Some(match args.format { + manifest: built.manifest, + format: Some(match args.format { GraphOutputFormat::Svg => types::RenderWorkflowGraphFormat::Svg, GraphOutputFormat::Png => types::RenderWorkflowGraphFormat::Png, }), diff --git a/lib/crates/fabro-cli/src/commands/install.rs b/lib/crates/fabro-cli/src/commands/install.rs index b415601dd..997b480f4 100644 --- a/lib/crates/fabro-cli/src/commands/install.rs +++ b/lib/crates/fabro-cli/src/commands/install.rs @@ -952,7 +952,7 @@ pub(crate) async fn run_install(args: &InstallArgs, globals: &GlobalArgs) -> Res if run_doctor { eprintln!(); let doctor_args = DoctorArgs { - target: ServerTargetArgs::default(), + target: ServerTargetArgs::default(), verbose: true, }; let _ = doctor::run_doctor(&doctor_args, true, globals).await?; diff --git a/lib/crates/fabro-cli/src/commands/model.rs b/lib/crates/fabro-cli/src/commands/model.rs index 17816c297..3a9c2de64 100644 --- a/lib/crates/fabro-cli/src/commands/model.rs +++ b/lib/crates/fabro-cli/src/commands/model.rs @@ -21,19 +21,19 @@ enum ModelTestResultKind { #[derive(Serialize)] struct ModelTestRow { - model: String, + model: String, provider: Provider, - result: ModelTestResultKind, + result: ModelTestResultKind, #[serde(skip_serializing_if = "Option::is_none")] - detail: Option, + detail: Option, #[serde(skip_serializing_if = "Option::is_none")] - error: Option, + error: Option, } #[derive(Serialize)] struct ModelTestOutput { - results: Vec, - total: usize, + results: Vec, + total: usize, failures: u32, } @@ -145,25 +145,25 @@ fn model_test_row_from_status(model: &Model, status: &str, result_color: Color) let trimmed = status.trim(); match result_color { Color::Green => ModelTestRow { - model: model.id.clone(), + model: model.id.clone(), provider: model.provider, - result: ModelTestResultKind::Pass, - detail: None, - error: None, + result: ModelTestResultKind::Pass, + detail: None, + error: None, }, Color::Yellow => ModelTestRow { - model: model.id.clone(), + model: model.id.clone(), provider: model.provider, - result: ModelTestResultKind::Skip, - detail: Some(trimmed.to_string()), - error: None, + result: ModelTestResultKind::Skip, + detail: Some(trimmed.to_string()), + error: None, }, _ => ModelTestRow { - model: model.id.clone(), + model: model.id.clone(), provider: model.provider, - result: ModelTestResultKind::Fail, - detail: None, - error: Some( + result: ModelTestResultKind::Fail, + detail: None, + error: Some( trimmed .strip_prefix("error: ") .unwrap_or(trimmed) @@ -435,19 +435,19 @@ mod tests { display_name: format!("{id} display"), limits: ModelLimits { context_window: 128_000, - max_output: Some(4096), + max_output: Some(4096), }, training: None, knowledge_cutoff: None, features: ModelFeatures { - tools: true, - vision: false, + tools: true, + vision: false, reasoning: false, - effort: false, + effort: false, }, costs: ModelCosts { - input_cost_per_mtok: Some(1.0), - output_cost_per_mtok: Some(2.0), + input_cost_per_mtok: Some(1.0), + output_cost_per_mtok: Some(2.0), cache_input_cost_per_mtok: None, }, estimated_output_tps: Some(100.0), diff --git a/lib/crates/fabro-cli/src/commands/pr/list.rs b/lib/crates/fabro-cli/src/commands/pr/list.rs index 3d885807a..2f7d15d0f 100644 --- a/lib/crates/fabro-cli/src/commands/pr/list.rs +++ b/lib/crates/fabro-cli/src/commands/pr/list.rs @@ -12,9 +12,9 @@ use crate::shared::print_json_pretty; struct PrRow { run_id: String, number: u64, - state: String, - title: String, - url: String, + state: String, + title: String, + url: String, } pub(super) async fn list_command( diff --git a/lib/crates/fabro-cli/src/commands/preflight.rs b/lib/crates/fabro-cli/src/commands/preflight.rs index 82e80c82a..045c08cbb 100644 --- a/lib/crates/fabro-cli/src/commands/preflight.rs +++ b/lib/crates/fabro-cli/src/commands/preflight.rs @@ -19,12 +19,12 @@ pub(crate) async fn execute(mut args: PreflightArgs, globals: &GlobalArgs) -> an args.verbose = args.verbose || ctx.cli_settings().output.verbosity == OutputVerbosity::Verbose; let manifest = build_run_manifest(ManifestBuildInput { - workflow: args.workflow.clone(), - cwd: ctx.cwd().to_path_buf(), - args_layer: preflight_args_layer(&args)?, - args: preflight_manifest_args(&args), - run_id: None, - user_layer: load_settings_user()?, + workflow: args.workflow.clone(), + cwd: ctx.cwd().to_path_buf(), + args_layer: preflight_args_layer(&args)?, + args: preflight_manifest_args(&args), + run_id: None, + user_layer: load_settings_user()?, user_settings_path: Some(active_settings_path(None)), })?; let client = ctx.server().await?; diff --git a/lib/crates/fabro-cli/src/commands/run/attach.rs b/lib/crates/fabro-cli/src/commands/run/attach.rs index 372b80600..98e11849f 100644 --- a/lib/crates/fabro-cli/src/commands/run/attach.rs +++ b/lib/crates/fabro-cli/src/commands/run/attach.rs @@ -108,10 +108,10 @@ pub(crate) async fn attach_run_with_client( } struct AttachOptions { - auto_approve: bool, - verbose: bool, + auto_approve: bool, + verbose: bool, kill_on_detach: bool, - json_output: bool, + json_output: bool, } fn replay_run_with_client( @@ -276,7 +276,7 @@ fn api_question_to_question(question: &types::ApiQuestion) -> Question { .options .iter() .map(|option| QuestionOption { - key: option.key.clone(), + key: option.key.clone(), label: option.label.clone(), }) .collect(); @@ -552,14 +552,14 @@ mod tests { #[test] fn answer_requires_reattach_for_interrupted_and_skipped_answers() { let interrupted = Answer { - value: AnswerValue::Interrupted, + value: AnswerValue::Interrupted, selected_option: None, - text: None, + text: None, }; let skipped = Answer { - value: AnswerValue::Skipped, + value: AnswerValue::Skipped, selected_option: None, - text: None, + text: None, }; let answered = Answer::yes(); diff --git a/lib/crates/fabro-cli/src/commands/run/cp.rs b/lib/crates/fabro-cli/src/commands/run/cp.rs index 52245830d..99a64f311 100644 --- a/lib/crates/fabro-cli/src/commands/run/cp.rs +++ b/lib/crates/fabro-cli/src/commands/run/cp.rs @@ -13,13 +13,13 @@ use crate::shared::{print_json_pretty, split_run_path}; #[derive(Debug)] enum CopyDirection { Download { - run_prefix: String, + run_prefix: String, remote_path: String, - local_path: PathBuf, + local_path: PathBuf, }, Upload { - local_path: PathBuf, - run_prefix: String, + local_path: PathBuf, + run_prefix: String, remote_path: String, }, } @@ -98,13 +98,13 @@ fn parse_direction(src: &str, dst: &str) -> Result { match (src_parts, dst_parts) { (Some((run_prefix, remote_path)), None) => Ok(CopyDirection::Download { - run_prefix: run_prefix.to_string(), + run_prefix: run_prefix.to_string(), remote_path: remote_path.to_string(), - local_path: PathBuf::from(dst), + local_path: PathBuf::from(dst), }), (None, Some((run_prefix, remote_path))) => Ok(CopyDirection::Upload { - local_path: PathBuf::from(src), - run_prefix: run_prefix.to_string(), + local_path: PathBuf::from(src), + run_prefix: run_prefix.to_string(), remote_path: remote_path.to_string(), }), (Some(_), Some(_)) => { diff --git a/lib/crates/fabro-cli/src/commands/run/create.rs b/lib/crates/fabro-cli/src/commands/run/create.rs index 603b4a647..4398dc290 100644 --- a/lib/crates/fabro-cli/src/commands/run/create.rs +++ b/lib/crates/fabro-cli/src/commands/run/create.rs @@ -15,7 +15,7 @@ use crate::manifest_builder::{ManifestBuildInput, build_run_manifest, run_manife use crate::user_config::{self, ServerTarget}; pub(crate) struct CreatedRun { - pub(crate) run_id: RunId, + pub(crate) run_id: RunId, pub(crate) local_run_dir: Option, } diff --git a/lib/crates/fabro-cli/src/commands/run/fork.rs b/lib/crates/fabro-cli/src/commands/run/fork.rs index 155ae55f7..ca8f6c72a 100644 --- a/lib/crates/fabro-cli/src/commands/run/fork.rs +++ b/lib/crates/fabro-cli/src/commands/run/fork.rs @@ -40,11 +40,14 @@ pub(crate) async fn run(args: &ForkArgs, styles: &Styles, globals: &GlobalArgs) .as_deref() .map(str::parse::) .transpose()?; - let new_run_id = fork(&store, &ForkRunInput { - source_run_id: run_id, - target, - push: !args.no_push, - })?; + let new_run_id = fork( + &store, + &ForkRunInput { + source_run_id: run_id, + target, + push: !args.no_push, + }, + )?; let run_id_string = run_id.to_string(); let new_run_id_string = new_run_id.to_string(); diff --git a/lib/crates/fabro-cli/src/commands/run/output.rs b/lib/crates/fabro-cli/src/commands/run/output.rs index 8eb06a944..43ac31d10 100644 --- a/lib/crates/fabro-cli/src/commands/run/output.rs +++ b/lib/crates/fabro-cli/src/commands/run/output.rs @@ -67,19 +67,19 @@ pub(crate) fn print_preflight_workflow_summary( fn api_diagnostic_to_local(diagnostic: &types::WorkflowDiagnostic) -> fabro_validate::Diagnostic { fabro_validate::Diagnostic { - rule: diagnostic.rule.clone(), + rule: diagnostic.rule.clone(), severity: match diagnostic.severity { types::WorkflowDiagnosticSeverity::Error => fabro_validate::Severity::Error, types::WorkflowDiagnosticSeverity::Warning => fabro_validate::Severity::Warning, types::WorkflowDiagnosticSeverity::Info => fabro_validate::Severity::Info, }, - message: diagnostic.message.clone(), - node_id: diagnostic.node_id.clone(), - edge: diagnostic + message: diagnostic.message.clone(), + node_id: diagnostic.node_id.clone(), + edge: diagnostic .edge .as_ref() .map(|edge| (edge[0].clone(), edge[1].clone())), - fix: diagnostic.fix.clone(), + fix: diagnostic.fix.clone(), } } @@ -91,24 +91,24 @@ pub(crate) fn api_diagnostics_to_local( pub(crate) fn api_check_report_to_local(report: &types::PreflightCheckReport) -> CheckReport { CheckReport { - title: report.title.clone(), + title: report.title.clone(), sections: report .sections .iter() .map(|section| CheckSection { - title: section.title.clone(), + title: section.title.clone(), checks: section .checks .iter() .map(|check| CheckResult { - name: check.name.clone(), - status: match check.status { + name: check.name.clone(), + status: match check.status { types::PreflightCheckResultStatus::Pass => CheckStatus::Pass, types::PreflightCheckResultStatus::Warning => CheckStatus::Warning, types::PreflightCheckResultStatus::Error => CheckStatus::Error, }, - summary: check.summary.clone(), - details: check + summary: check.summary.clone(), + details: check .details .iter() .map(|detail| CheckDetail { diff --git a/lib/crates/fabro-cli/src/commands/run/overrides.rs b/lib/crates/fabro-cli/src/commands/run/overrides.rs index 9a8a813b4..36daa69b2 100644 --- a/lib/crates/fabro-cli/src/commands/run/overrides.rs +++ b/lib/crates/fabro-cli/src/commands/run/overrides.rs @@ -30,8 +30,8 @@ fn model_from_args(model: Option<&str>, provider: Option<&str>) -> Option, } @@ -54,11 +54,14 @@ pub(crate) async fn run(args: &RewindArgs, styles: &Styles, globals: &GlobalArgs let target = args.target.as_deref().unwrap().parse::()?; - rewind(&store, &RewindInput { - run_id, - target: target.clone(), - push: !args.no_push, - })?; + rewind( + &store, + &RewindInput { + run_id, + target: target.clone(), + push: !args.no_push, + }, + )?; let entry = timeline.resolve(&target)?; reset_rewound_run_state(lookup.client(), &store, &run_id, entry).await?; @@ -84,9 +87,9 @@ pub(crate) fn timeline_entries_json(timeline: &RunTimeline) -> Vec, + pub(super) cost: Option, } impl ProgressUsage { pub(super) fn from_stage_usage(usage: &BilledModelUsage) -> Option { let tokens = usage.tokens(); Some(Self { - input_tokens: u64::try_from(tokens.input_tokens).ok()?, + input_tokens: u64::try_from(tokens.input_tokens).ok()?, output_tokens: u64::try_from(tokens.billable_output_tokens()).ok()?, - cost: usage.total_usd_micros.map(|cost| cost as f64 / 1_000_000.0), + cost: usage.total_usd_micros.map(|cost| cost as f64 / 1_000_000.0), }) } @@ -35,8 +35,8 @@ impl ProgressUsage { pub(super) enum ProgressEvent { WorkflowStarted { worktree_dir: Option, - base_branch: Option, - base_sha: Option, + base_branch: Option, + base_sha: Option, }, WorkingDirectorySet { working_directory: String, @@ -45,12 +45,12 @@ pub(super) enum ProgressEvent { provider: String, }, SandboxReady { - provider: String, + provider: String, duration_ms: u64, - name: Option, - cpu: Option, - memory: Option, - url: Option, + name: Option, + cpu: Option, + memory: Option, + url: Option, }, SshAccessReady { ssh_command: String, @@ -62,98 +62,98 @@ pub(super) enum ProgressEvent { duration_ms: u64, }, SetupCommandCompleted { - command: String, + command: String, command_index: u64, - exit_code: i64, - duration_ms: u64, + exit_code: i64, + duration_ms: u64, }, CliEnsureStarted { cli_name: String, }, CliEnsureCompleted { - cli_name: String, + cli_name: String, already_installed: bool, - duration_ms: u64, + duration_ms: u64, }, CliEnsureFailed { cli_name: String, }, DevcontainerResolved { - dockerfile_lines: u64, - environment_count: u64, + dockerfile_lines: u64, + environment_count: u64, lifecycle_command_count: u64, - workspace_folder: String, + workspace_folder: String, }, DevcontainerLifecycleStarted { - phase: String, + phase: String, command_count: u64, }, DevcontainerLifecycleCompleted { - phase: String, + phase: String, duration_ms: u64, }, DevcontainerLifecycleFailed { - phase: String, - command: String, + phase: String, + command: String, exit_code: i64, - stderr: String, + stderr: String, }, DevcontainerLifecycleCommandCompleted { - command: String, + command: String, command_index: u64, - exit_code: i64, - duration_ms: u64, + exit_code: i64, + duration_ms: u64, }, StageStarted { node_id: String, - name: String, - script: Option, + name: String, + script: Option, }, StageCompleted { - node_id: String, - name: String, + node_id: String, + name: String, duration_ms: u64, - status: String, - usage: Option, + status: String, + usage: Option, }, StageFailed { node_id: String, - name: String, - error: String, + name: String, + error: String, }, StageRetrying { - name: String, - attempt: u64, + name: String, + attempt: u64, max_attempts: u64, - delay_ms: u64, + delay_ms: u64, }, ParallelStarted, ParallelBranchStarted { branch: String, }, ParallelBranchCompleted { - branch: String, + branch: String, duration_ms: u64, - status: String, + status: String, }, ParallelCompleted, AssistantMessage { stage_node_id: String, - model: String, + model: String, }, ToolCallStarted { stage_node_id: String, - tool_name: String, - tool_call_id: String, - arguments: Value, - timestamp: Option>, + tool_name: String, + tool_call_id: String, + arguments: Value, + timestamp: Option>, }, ToolCallCompleted { stage_node_id: String, - tool_call_id: String, - is_error: bool, - duration_ms: Option, - timestamp: Option>, + tool_call_id: String, + is_error: bool, + duration_ms: Option, + timestamp: Option>, }, ContextWindowWarning { stage_node_id: String, @@ -163,38 +163,38 @@ pub(super) enum ProgressEvent { stage_node_id: String, }, CompactionCompleted { - stage_node_id: String, - original_turn_count: u64, + stage_node_id: String, + original_turn_count: u64, preserved_turn_count: u64, - tracked_file_count: u64, + tracked_file_count: u64, }, LlmRetry { stage_node_id: String, - model: String, - attempt: u64, - delay_ms: u64, - error: String, + model: String, + attempt: u64, + delay_ms: u64, + error: String, }, SubagentSpawned { stage_node_id: String, - agent_id: String, - task: String, + agent_id: String, + task: String, }, SubagentCompleted { stage_node_id: String, - agent_id: String, - success: bool, - turns_used: u64, + agent_id: String, + success: bool, + turns_used: u64, }, EdgeSelected { from_node: String, - to_node: String, - label: Option, + to_node: String, + label: Option, condition: Option, }, LoopRestart { from_node: String, - to_node: String, + to_node: String, }, RetroStarted, RetroCompleted { @@ -204,13 +204,13 @@ pub(super) enum ProgressEvent { duration_ms: u64, }, RunNotice { - level: RunNoticeLevel, - code: String, + level: RunNoticeLevel, + code: String, message: String, }, PullRequestCreated { pr_url: String, - draft: bool, + draft: bool, }, PullRequestFailed { error: String, @@ -224,8 +224,8 @@ pub(super) fn from_run_event(stored: &RunEvent) -> Option { match &stored.body { EventBody::RunStarted(props) => Some(ProgressEvent::WorkflowStarted { worktree_dir: props.worktree_dir.clone(), - base_branch: props.base_branch.clone(), - base_sha: props.base_sha.clone(), + base_branch: props.base_branch.clone(), + base_sha: props.base_sha.clone(), }), EventBody::SandboxInitialized(props) => Some(ProgressEvent::WorkingDirectorySet { working_directory: props.working_directory.clone(), @@ -234,12 +234,12 @@ pub(super) fn from_run_event(stored: &RunEvent) -> Option { provider: props.provider.clone(), }), EventBody::SandboxReady(props) => Some(ProgressEvent::SandboxReady { - provider: props.provider.clone(), + provider: props.provider.clone(), duration_ms: props.duration_ms, - name: props.name.clone(), - cpu: props.cpu, - memory: props.memory, - url: props.url.clone(), + name: props.name.clone(), + cpu: props.cpu, + memory: props.memory, + url: props.url.clone(), }), EventBody::SshAccessReady(props) => Some(ProgressEvent::SshAccessReady { ssh_command: props.ssh_command.clone(), @@ -251,54 +251,54 @@ pub(super) fn from_run_event(stored: &RunEvent) -> Option { duration_ms: props.duration_ms, }), EventBody::SetupCommandCompleted(props) => Some(ProgressEvent::SetupCommandCompleted { - command: props.command.clone(), + command: props.command.clone(), command_index: props.index as u64, - exit_code: i64::from(props.exit_code), - duration_ms: props.duration_ms, + exit_code: i64::from(props.exit_code), + duration_ms: props.duration_ms, }), EventBody::CliEnsureStarted(props) => Some(ProgressEvent::CliEnsureStarted { cli_name: props.cli_name.clone(), }), EventBody::CliEnsureCompleted(props) => Some(ProgressEvent::CliEnsureCompleted { - cli_name: props.cli_name.clone(), + cli_name: props.cli_name.clone(), already_installed: props.already_installed, - duration_ms: props.duration_ms, + duration_ms: props.duration_ms, }), EventBody::CliEnsureFailed(props) => Some(ProgressEvent::CliEnsureFailed { cli_name: props.cli_name.clone(), }), EventBody::DevcontainerResolved(props) => Some(ProgressEvent::DevcontainerResolved { - dockerfile_lines: props.dockerfile_lines as u64, - environment_count: props.environment_count as u64, + dockerfile_lines: props.dockerfile_lines as u64, + environment_count: props.environment_count as u64, lifecycle_command_count: props.lifecycle_command_count as u64, - workspace_folder: props.workspace_folder.clone(), + workspace_folder: props.workspace_folder.clone(), }), EventBody::DevcontainerLifecycleStarted(props) => { Some(ProgressEvent::DevcontainerLifecycleStarted { - phase: props.phase.clone(), + phase: props.phase.clone(), command_count: props.command_count as u64, }) } EventBody::DevcontainerLifecycleCompleted(props) => { Some(ProgressEvent::DevcontainerLifecycleCompleted { - phase: props.phase.clone(), + phase: props.phase.clone(), duration_ms: props.duration_ms, }) } EventBody::DevcontainerLifecycleFailed(props) => { Some(ProgressEvent::DevcontainerLifecycleFailed { - phase: props.phase.clone(), - command: props.command.clone(), + phase: props.phase.clone(), + command: props.command.clone(), exit_code: i64::from(props.exit_code), - stderr: props.stderr.clone(), + stderr: props.stderr.clone(), }) } EventBody::DevcontainerLifecycleCommandCompleted(props) => { Some(ProgressEvent::DevcontainerLifecycleCommandCompleted { - command: props.command.clone(), + command: props.command.clone(), command_index: props.index as u64, - exit_code: i64::from(props.exit_code), - duration_ms: props.duration_ms, + exit_code: i64::from(props.exit_code), + duration_ms: props.duration_ms, }) } EventBody::StageStarted(_) => Some(ProgressEvent::StageStarted { @@ -325,38 +325,38 @@ pub(super) fn from_run_event(stored: &RunEvent) -> Option { ), }), EventBody::StageRetrying(props) => Some(ProgressEvent::StageRetrying { - name: node_label, - attempt: props.attempt as u64, + name: node_label, + attempt: props.attempt as u64, max_attempts: props.max_attempts as u64, - delay_ms: props.delay_ms, + delay_ms: props.delay_ms, }), EventBody::ParallelStarted(_) => Some(ProgressEvent::ParallelStarted), EventBody::ParallelBranchStarted(_) => { Some(ProgressEvent::ParallelBranchStarted { branch: node_id }) } EventBody::ParallelBranchCompleted(props) => Some(ProgressEvent::ParallelBranchCompleted { - branch: node_id, + branch: node_id, duration_ms: props.duration_ms, - status: props.status.clone(), + status: props.status.clone(), }), EventBody::ParallelCompleted(_) => Some(ProgressEvent::ParallelCompleted), EventBody::AgentMessage(props) => Some(ProgressEvent::AssistantMessage { stage_node_id: node_id, - model: props.model.clone(), + model: props.model.clone(), }), EventBody::AgentToolStarted(props) => Some(ProgressEvent::ToolCallStarted { stage_node_id: node_id, - tool_name: props.tool_name.clone(), - tool_call_id: props.tool_call_id.clone(), - arguments: props.arguments.clone(), - timestamp: Some(stored.ts), + tool_name: props.tool_name.clone(), + tool_call_id: props.tool_call_id.clone(), + arguments: props.arguments.clone(), + timestamp: Some(stored.ts), }), EventBody::AgentToolCompleted(props) => Some(ProgressEvent::ToolCallCompleted { stage_node_id: node_id, - tool_call_id: props.tool_call_id.clone(), - is_error: props.is_error, - duration_ms: None, - timestamp: Some(stored.ts), + tool_call_id: props.tool_call_id.clone(), + is_error: props.is_error, + duration_ms: None, + timestamp: Some(stored.ts), }), EventBody::AgentWarning(props) if props.kind == "context_window" => { let usage_percent = props @@ -374,10 +374,10 @@ pub(super) fn from_run_event(stored: &RunEvent) -> Option { stage_node_id: node_id, }), EventBody::AgentCompactionCompleted(props) => Some(ProgressEvent::CompactionCompleted { - stage_node_id: node_id, - original_turn_count: props.original_turn_count as u64, + stage_node_id: node_id, + original_turn_count: props.original_turn_count as u64, preserved_turn_count: props.preserved_turn_count as u64, - tracked_file_count: props.tracked_file_count as u64, + tracked_file_count: props.tracked_file_count as u64, }), EventBody::AgentLlmRetry(props) => { #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] @@ -392,24 +392,24 @@ pub(super) fn from_run_event(stored: &RunEvent) -> Option { } EventBody::AgentSubSpawned(props) => Some(ProgressEvent::SubagentSpawned { stage_node_id: node_id, - agent_id: props.agent_id.clone(), - task: props.task.clone(), + agent_id: props.agent_id.clone(), + task: props.task.clone(), }), EventBody::AgentSubCompleted(props) => Some(ProgressEvent::SubagentCompleted { stage_node_id: node_id, - agent_id: props.agent_id.clone(), - success: props.success, - turns_used: props.turns_used as u64, + agent_id: props.agent_id.clone(), + success: props.success, + turns_used: props.turns_used as u64, }), EventBody::EdgeSelected(props) => Some(ProgressEvent::EdgeSelected { from_node: props.from_node.clone(), - to_node: props.to_node.clone(), - label: props.label.clone(), + to_node: props.to_node.clone(), + label: props.label.clone(), condition: props.condition.clone(), }), EventBody::LoopRestart(props) => Some(ProgressEvent::LoopRestart { from_node: props.from_node.clone(), - to_node: props.to_node.clone(), + to_node: props.to_node.clone(), }), EventBody::RetroStarted(_) => Some(ProgressEvent::RetroStarted), EventBody::RetroCompleted(props) => Some(ProgressEvent::RetroCompleted { @@ -419,13 +419,13 @@ pub(super) fn from_run_event(stored: &RunEvent) -> Option { duration_ms: props.duration_ms, }), EventBody::RunNotice(props) => Some(ProgressEvent::RunNotice { - level: props.level, - code: props.code.clone(), + level: props.level, + code: props.code.clone(), message: props.message.clone(), }), EventBody::PullRequestCreated(props) => Some(ProgressEvent::PullRequestCreated { pr_url: props.pr_url.clone(), - draft: props.draft, + draft: props.draft, }), EventBody::PullRequestFailed(props) => Some(ProgressEvent::PullRequestFailed { error: props.error.clone(), @@ -477,17 +477,20 @@ mod tests { #[test] fn parse_edge_selected() { - let stored = to_run_event(&fixtures::RUN_1, &Event::EdgeSelected { - from_node: "a".into(), - to_node: "b".into(), - label: Some("yes".into()), - condition: None, - reason: "condition".into(), - preferred_label: None, - suggested_next_ids: Vec::new(), - stage_status: "success".into(), - is_jump: false, - }); + let stored = to_run_event( + &fixtures::RUN_1, + &Event::EdgeSelected { + from_node: "a".into(), + to_node: "b".into(), + label: Some("yes".into()), + condition: None, + reason: "condition".into(), + preferred_label: None, + suggested_next_ids: Vec::new(), + stage_status: "success".into(), + is_jump: false, + }, + ); let event = from_run_event(&stored).unwrap(); assert!(matches!( @@ -542,14 +545,14 @@ mod tests { #[test] fn round_trip_agent_tool_call() { let event = Event::Agent { - stage: "code".into(), - visit: 1, - event: AgentEvent::ToolCallStarted { - tool_name: "read_file".into(), + stage: "code".into(), + visit: 1, + event: AgentEvent::ToolCallStarted { + tool_name: "read_file".into(), tool_call_id: "tc1".into(), - arguments: serde_json::json!({"path": "src/main.rs"}), + arguments: serde_json::json!({"path": "src/main.rs"}), }, - session_id: None, + session_id: None, parent_session_id: None, }; @@ -631,12 +634,12 @@ mod tests { fn round_trip_sandbox_ready() { let event = Event::Sandbox { event: fabro_agent::SandboxEvent::Ready { - provider: "daytona".into(), + provider: "daytona".into(), duration_ms: 2500, - name: Some("sandbox-1".into()), - cpu: Some(4.0), - memory: Some(8.0), - url: Some("https://example.test".into()), + name: Some("sandbox-1".into()), + cpu: Some(4.0), + memory: Some(8.0), + url: Some("https://example.test".into()), }, }; @@ -656,8 +659,8 @@ mod tests { #[test] fn round_trip_run_notice() { let event = Event::RunNotice { - level: RunNoticeLevel::Warn, - code: "sandbox_cleanup_failed".into(), + level: RunNoticeLevel::Warn, + code: "sandbox_cleanup_failed".into(), message: "sandbox cleanup failed".into(), }; diff --git a/lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs b/lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs index 8325e50d2..6670eed8e 100644 --- a/lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs +++ b/lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs @@ -15,9 +15,9 @@ use stage_display::StageDisplay; pub(crate) struct ProgressUI { renderer: ProgressRenderer, - stage: StageDisplay, - setup: SetupDisplay, - info: InfoDisplay, + stage: StageDisplay, + setup: SetupDisplay, + info: InfoDisplay, } impl ProgressUI { @@ -484,22 +484,25 @@ mod tests { fn stage_started(node_id: &str, name: &str) -> Event { Event::StageStarted { - node_id: node_id.into(), - name: name.into(), - index: 0, + node_id: node_id.into(), + name: name.into(), + index: 0, handler_type: String::new(), - attempt: 1, + attempt: 1, max_attempts: 1, } } fn assistant_message(stage: &str, model: &str) -> Event { - agent_event(stage, AgentEvent::AssistantMessage { - text: "done".into(), - model: model.into(), - usage: TokenCounts::default(), - tool_call_count: 0, - }) + agent_event( + stage, + AgentEvent::AssistantMessage { + text: "done".into(), + model: model.into(), + usage: TokenCounts::default(), + tool_call_count: 0, + }, + ) } fn stage_completed(node_id: &str, name: &str) -> Event { @@ -544,20 +547,26 @@ mod tests { assert!(ui.stage.active_stages.contains_key("fork1")); assert!(ui.stage.parallel_parent.is_none()); - emit(&mut ui, Event::ParallelStarted { - node_id: "fork1".into(), - visit: 1, - branch_count: 2, - join_policy: "wait_all".into(), - }); + emit( + &mut ui, + Event::ParallelStarted { + node_id: "fork1".into(), + visit: 1, + branch_count: 2, + join_policy: "wait_all".into(), + }, + ); assert_eq!(ui.stage.parallel_parent.as_deref(), Some("fork1")); - emit(&mut ui, Event::ParallelBranchStarted { - parallel_group_id: StageId::new("fork1", 1), - parallel_branch_id: ParallelBranchId::new(StageId::new("fork1", 1), 0), - branch: "security".into(), - index: 0, - }); + emit( + &mut ui, + Event::ParallelBranchStarted { + parallel_group_id: StageId::new("fork1", 1), + parallel_branch_id: ParallelBranchId::new(StageId::new("fork1", 1), 0), + branch: "security".into(), + index: 0, + }, + ); let stage = &ui.stage.active_stages["fork1"]; assert_eq!(stage.tool_calls.len(), 1); assert_eq!(stage.tool_calls[0].tool_call_id, "security"); @@ -566,15 +575,18 @@ mod tests { ToolCallStatus::Running )); - emit(&mut ui, Event::ParallelBranchCompleted { - parallel_group_id: StageId::new("fork1", 1), - parallel_branch_id: ParallelBranchId::new(StageId::new("fork1", 1), 0), - branch: "security".into(), - index: 0, - duration_ms: 2000, - status: "success".into(), - head_sha: None, - }); + emit( + &mut ui, + Event::ParallelBranchCompleted { + parallel_group_id: StageId::new("fork1", 1), + parallel_branch_id: ParallelBranchId::new(StageId::new("fork1", 1), 0), + branch: "security".into(), + index: 0, + duration_ms: 2000, + status: "success".into(), + head_sha: None, + }, + ); let stage = &ui.stage.active_stages["fork1"]; assert!(matches!( stage.tool_calls[0].status, @@ -587,18 +599,24 @@ mod tests { let mut ui = ProgressUI::new(true, false); emit(&mut ui, stage_started("fork1", "Fork")); - emit(&mut ui, Event::ParallelStarted { - node_id: "fork1".into(), - visit: 1, - branch_count: 1, - join_policy: "wait_all".into(), - }); - emit(&mut ui, Event::ParallelBranchStarted { - parallel_group_id: StageId::new("fork1", 1), - parallel_branch_id: ParallelBranchId::new(StageId::new("fork1", 1), 0), - branch: "security".into(), - index: 0, - }); + emit( + &mut ui, + Event::ParallelStarted { + node_id: "fork1".into(), + visit: 1, + branch_count: 1, + join_policy: "wait_all".into(), + }, + ); + emit( + &mut ui, + Event::ParallelBranchStarted { + parallel_group_id: StageId::new("fork1", 1), + parallel_branch_id: ParallelBranchId::new(StageId::new("fork1", 1), 0), + branch: "security".into(), + index: 0, + }, + ); let stage = &ui.stage.active_stages["fork1"]; let message = stage.tool_calls[0].bar.message(); @@ -617,21 +635,27 @@ mod tests { emit( &mut ui, - agent_event("s1", AgentEvent::CompactionStarted { - estimated_tokens: 5000, - context_window_size: 8000, - }), + agent_event( + "s1", + AgentEvent::CompactionStarted { + estimated_tokens: 5000, + context_window_size: 8000, + }, + ), ); assert!(ui.stage.active_stages["s1"].compaction_bar.is_some()); emit( &mut ui, - agent_event("s1", AgentEvent::CompactionCompleted { - original_turn_count: 20, - preserved_turn_count: 6, - summary_token_estimate: 500, - tracked_file_count: 3, - }), + agent_event( + "s1", + AgentEvent::CompactionCompleted { + original_turn_count: 20, + preserved_turn_count: 6, + summary_token_estimate: 500, + tracked_file_count: 3, + }, + ), ); assert!(ui.stage.active_stages["s1"].compaction_bar.is_none()); } @@ -650,86 +674,101 @@ mod tests { let events = vec![ stage_started("code", "Code"), Event::SandboxInitialized { - working_directory: "/home/daytona/workspace".into(), - provider: "daytona".into(), - identifier: None, + working_directory: "/home/daytona/workspace".into(), + provider: "daytona".into(), + identifier: None, host_working_directory: None, - container_mount_point: None, + container_mount_point: None, }, - agent_event("code", AgentEvent::ToolCallStarted { - tool_name: "read_file".into(), - tool_call_id: "tc1".into(), - arguments: serde_json::json!({ - "file_path": "/home/daytona/workspace/src/main.rs" - }), - }), + agent_event( + "code", + AgentEvent::ToolCallStarted { + tool_name: "read_file".into(), + tool_call_id: "tc1".into(), + arguments: serde_json::json!({ + "file_path": "/home/daytona/workspace/src/main.rs" + }), + }, + ), assistant_message("code", "gpt-5-mini"), Event::EdgeSelected { - from_node: "code".into(), - to_node: "review".into(), - label: Some("ship".into()), - condition: None, - reason: "condition".into(), - preferred_label: None, + from_node: "code".into(), + to_node: "review".into(), + label: Some("ship".into()), + condition: None, + reason: "condition".into(), + preferred_label: None, suggested_next_ids: Vec::new(), - stage_status: "success".into(), - is_jump: false, + stage_status: "success".into(), + is_jump: false, }, Event::StageRetrying { - node_id: "code".into(), - name: "Code".into(), - index: 0, - attempt: 2, + node_id: "code".into(), + name: "Code".into(), + index: 0, + attempt: 2, max_attempts: 3, - delay_ms: 1500, + delay_ms: 1500, }, - agent_event("code", AgentEvent::Warning { - kind: "context_window".into(), - message: "high usage".into(), - details: serde_json::json!({"usage_percent": 92}), - }), - agent_event("code", AgentEvent::LlmRetry { - provider: "openai".into(), - model: "gpt-5-mini".into(), - attempt: 2, - delay_secs: 1.5, - error: fabro_llm::error::SdkError::Configuration { - message: "busy".into(), - source: None, + agent_event( + "code", + AgentEvent::Warning { + kind: "context_window".into(), + message: "high usage".into(), + details: serde_json::json!({"usage_percent": 92}), }, - }), - agent_event("code", AgentEvent::SubAgentSpawned { - agent_id: "a1".into(), - depth: 1, - task: "review recent changes".into(), - }), - agent_event("code", AgentEvent::SubAgentCompleted { - agent_id: "a1".into(), - depth: 1, - success: true, - turns_used: 3, - }), + ), + agent_event( + "code", + AgentEvent::LlmRetry { + provider: "openai".into(), + model: "gpt-5-mini".into(), + attempt: 2, + delay_secs: 1.5, + error: fabro_llm::Error::Configuration { + message: "busy".into(), + source: None, + }, + }, + ), + agent_event( + "code", + AgentEvent::SubAgentSpawned { + agent_id: "a1".into(), + depth: 1, + task: "review recent changes".into(), + }, + ), + agent_event( + "code", + AgentEvent::SubAgentCompleted { + agent_id: "a1".into(), + depth: 1, + success: true, + turns_used: 3, + }, + ), Event::SetupStarted { command_count: 1 }, Event::SetupCommandCompleted { - command: "bun install".into(), - index: 0, - exit_code: 0, + command: "bun install".into(), + index: 0, + exit_code: 0, duration_ms: 2200, }, Event::SetupCompleted { duration_ms: 2200 }, Event::DevcontainerLifecycleStarted { - phase: "postCreate".into(), + phase: "postCreate".into(), command_count: 1, }, Event::DevcontainerLifecycleCommandCompleted { - phase: "postCreate".into(), - command: "npm run setup".into(), - index: 0, - exit_code: 0, + phase: "postCreate".into(), + command: "npm run setup".into(), + index: 0, + exit_code: 0, duration_ms: 1400, }, Event::DevcontainerLifecycleCompleted { - phase: "postCreate".into(), + phase: "postCreate".into(), duration_ms: 1400, }, ]; @@ -756,20 +795,26 @@ mod tests { emit(&mut ui, assistant_message("plan", "gpt-5-mini")); emit( &mut ui, - agent_event("plan", AgentEvent::ToolCallStarted { - tool_name: "read_file".into(), - tool_call_id: "tc1".into(), - arguments: serde_json::json!({"path": "src/main.rs"}), - }), + agent_event( + "plan", + AgentEvent::ToolCallStarted { + tool_name: "read_file".into(), + tool_call_id: "tc1".into(), + arguments: serde_json::json!({"path": "src/main.rs"}), + }, + ), ); emit( &mut ui, - agent_event("plan", AgentEvent::ToolCallCompleted { - tool_name: "read_file".into(), - tool_call_id: "tc1".into(), - output: serde_json::json!({"ok": true}), - is_error: false, - }), + agent_event( + "plan", + AgentEvent::ToolCallCompleted { + tool_name: "read_file".into(), + tool_call_id: "tc1".into(), + output: serde_json::json!({"ok": true}), + is_error: false, + }, + ), ); emit(&mut ui, stage_completed("plan", "Plan")); @@ -780,47 +825,68 @@ mod tests { fn plain_default_setup_snapshot() { let (mut ui, buffer) = capture_ui(false); - emit(&mut ui, Event::Sandbox { - event: SandboxEvent::Initializing { - provider: "daytona".into(), + emit( + &mut ui, + Event::Sandbox { + event: SandboxEvent::Initializing { + provider: "daytona".into(), + }, }, - }); - emit(&mut ui, Event::Sandbox { - event: SandboxEvent::Ready { - provider: "daytona".into(), - duration_ms: 2500, - name: Some("sandbox-1".into()), - cpu: Some(4.0), - memory: Some(8.0), - url: None, + ); + emit( + &mut ui, + Event::Sandbox { + event: SandboxEvent::Ready { + provider: "daytona".into(), + duration_ms: 2500, + name: Some("sandbox-1".into()), + cpu: Some(4.0), + memory: Some(8.0), + url: None, + }, }, - }); - emit(&mut ui, Event::SshAccessReady { - ssh_command: "ssh daytona@example".into(), - }); + ); + emit( + &mut ui, + Event::SshAccessReady { + ssh_command: "ssh daytona@example".into(), + }, + ); emit(&mut ui, Event::SetupStarted { command_count: 2 }); emit(&mut ui, Event::SetupCompleted { duration_ms: 8200 }); - emit(&mut ui, Event::CliEnsureCompleted { - cli_name: "gh".into(), - provider: "github".into(), - already_installed: false, - node_installed: false, - duration_ms: 600, - }); - emit(&mut ui, Event::DevcontainerResolved { - dockerfile_lines: 24, - environment_count: 3, - lifecycle_command_count: 2, - workspace_folder: "/workspace".into(), - }); - emit(&mut ui, Event::DevcontainerLifecycleStarted { - phase: "postCreate".into(), - command_count: 2, - }); - emit(&mut ui, Event::DevcontainerLifecycleCompleted { - phase: "postCreate".into(), - duration_ms: 1800, - }); + emit( + &mut ui, + Event::CliEnsureCompleted { + cli_name: "gh".into(), + provider: "github".into(), + already_installed: false, + node_installed: false, + duration_ms: 600, + }, + ); + emit( + &mut ui, + Event::DevcontainerResolved { + dockerfile_lines: 24, + environment_count: 3, + lifecycle_command_count: 2, + workspace_folder: "/workspace".into(), + }, + ); + emit( + &mut ui, + Event::DevcontainerLifecycleStarted { + phase: "postCreate".into(), + command_count: 2, + }, + ); + emit( + &mut ui, + Event::DevcontainerLifecycleCompleted { + phase: "postCreate".into(), + duration_ms: 1800, + }, + ); insta::assert_snapshot!(rendered(&buffer), @r" Sandbox: daytona (ready in 2s) @@ -840,104 +906,140 @@ mod tests { let (mut ui, buffer) = capture_ui(true); emit(&mut ui, stage_started("code", "Code")); - emit(&mut ui, Event::SandboxInitialized { - working_directory: "/home/daytona/workspace".into(), - provider: "daytona".into(), - identifier: None, - host_working_directory: None, - container_mount_point: None, - }); emit( &mut ui, - agent_event("code", AgentEvent::ToolCallStarted { - tool_name: "read_file".into(), - tool_call_id: "tc1".into(), - arguments: serde_json::json!({ - "file_path": "/home/daytona/workspace/src/main.rs" - }), - }), + Event::SandboxInitialized { + working_directory: "/home/daytona/workspace".into(), + provider: "daytona".into(), + identifier: None, + host_working_directory: None, + container_mount_point: None, + }, + ); + emit( + &mut ui, + agent_event( + "code", + AgentEvent::ToolCallStarted { + tool_name: "read_file".into(), + tool_call_id: "tc1".into(), + arguments: serde_json::json!({ + "file_path": "/home/daytona/workspace/src/main.rs" + }), + }, + ), ); emit(&mut ui, assistant_message("code", "gpt-5-mini")); - emit(&mut ui, Event::EdgeSelected { - from_node: "code".into(), - to_node: "review".into(), - label: Some("ship".into()), - condition: None, - reason: "condition".into(), - preferred_label: None, - suggested_next_ids: Vec::new(), - stage_status: "success".into(), - is_jump: false, - }); - emit(&mut ui, Event::StageRetrying { - node_id: "code".into(), - name: "Code".into(), - index: 0, - attempt: 2, - max_attempts: 3, - delay_ms: 1500, - }); emit( &mut ui, - agent_event("code", AgentEvent::Warning { - kind: "context_window".into(), - message: "high usage".into(), - details: serde_json::json!({"usage_percent": 92}), - }), + Event::EdgeSelected { + from_node: "code".into(), + to_node: "review".into(), + label: Some("ship".into()), + condition: None, + reason: "condition".into(), + preferred_label: None, + suggested_next_ids: Vec::new(), + stage_status: "success".into(), + is_jump: false, + }, ); emit( &mut ui, - agent_event("code", AgentEvent::LlmRetry { - provider: "openai".into(), - model: "gpt-5-mini".into(), - attempt: 2, - delay_secs: 1.5, - error: fabro_llm::error::SdkError::Configuration { - message: "busy".into(), - source: None, + Event::StageRetrying { + node_id: "code".into(), + name: "Code".into(), + index: 0, + attempt: 2, + max_attempts: 3, + delay_ms: 1500, + }, + ); + emit( + &mut ui, + agent_event( + "code", + AgentEvent::Warning { + kind: "context_window".into(), + message: "high usage".into(), + details: serde_json::json!({"usage_percent": 92}), }, - }), + ), ); emit( &mut ui, - agent_event("code", AgentEvent::SubAgentSpawned { - agent_id: "a1".into(), - depth: 1, - task: "review recent changes".into(), - }), + agent_event( + "code", + AgentEvent::LlmRetry { + provider: "openai".into(), + model: "gpt-5-mini".into(), + attempt: 2, + delay_secs: 1.5, + error: fabro_llm::Error::Configuration { + message: "busy".into(), + source: None, + }, + }, + ), ); emit( &mut ui, - agent_event("code", AgentEvent::SubAgentCompleted { - agent_id: "a1".into(), - depth: 1, - success: true, - turns_used: 3, - }), + agent_event( + "code", + AgentEvent::SubAgentSpawned { + agent_id: "a1".into(), + depth: 1, + task: "review recent changes".into(), + }, + ), + ); + emit( + &mut ui, + agent_event( + "code", + AgentEvent::SubAgentCompleted { + agent_id: "a1".into(), + depth: 1, + success: true, + turns_used: 3, + }, + ), ); emit(&mut ui, Event::SetupStarted { command_count: 1 }); - emit(&mut ui, Event::SetupCommandCompleted { - command: "bun install".into(), - index: 0, - exit_code: 0, - duration_ms: 2200, - }); + emit( + &mut ui, + Event::SetupCommandCompleted { + command: "bun install".into(), + index: 0, + exit_code: 0, + duration_ms: 2200, + }, + ); emit(&mut ui, Event::SetupCompleted { duration_ms: 2200 }); - emit(&mut ui, Event::DevcontainerLifecycleStarted { - phase: "postCreate".into(), - command_count: 1, - }); - emit(&mut ui, Event::DevcontainerLifecycleCommandCompleted { - phase: "postCreate".into(), - command: "npm run setup".into(), - index: 0, - exit_code: 0, - duration_ms: 1400, - }); - emit(&mut ui, Event::DevcontainerLifecycleCompleted { - phase: "postCreate".into(), - duration_ms: 1400, - }); + emit( + &mut ui, + Event::DevcontainerLifecycleStarted { + phase: "postCreate".into(), + command_count: 1, + }, + ); + emit( + &mut ui, + Event::DevcontainerLifecycleCommandCompleted { + phase: "postCreate".into(), + command: "npm run setup".into(), + index: 0, + exit_code: 0, + duration_ms: 1400, + }, + ); + emit( + &mut ui, + Event::DevcontainerLifecycleCompleted { + phase: "postCreate".into(), + duration_ms: 1400, + }, + ); emit(&mut ui, stage_completed("code", "Code")); insta::assert_snapshot!(rendered(&buffer), @r#" @@ -960,24 +1062,33 @@ mod tests { fn plain_notice_snapshot() { let (mut ui, buffer) = capture_ui(false); - emit(&mut ui, Event::RunNotice { - level: RunNoticeLevel::Warn, - code: "sandbox_cleanup_failed".into(), - message: "sandbox cleanup failed".into(), - }); - emit(&mut ui, Event::PullRequestCreated { - pr_url: "https://github.com/fabro-sh/fabro/pull/42".into(), - pr_number: 42, - owner: "fabro-sh".into(), - repo: "fabro".into(), - base_branch: "main".into(), - head_branch: "fabro/run/42".into(), - title: "Ship the change".into(), - draft: true, - }); - emit(&mut ui, Event::PullRequestFailed { - error: "auth token expired".into(), - }); + emit( + &mut ui, + Event::RunNotice { + level: RunNoticeLevel::Warn, + code: "sandbox_cleanup_failed".into(), + message: "sandbox cleanup failed".into(), + }, + ); + emit( + &mut ui, + Event::PullRequestCreated { + pr_url: "https://github.com/fabro-sh/fabro/pull/42".into(), + pr_number: 42, + owner: "fabro-sh".into(), + repo: "fabro".into(), + base_branch: "main".into(), + head_branch: "fabro/run/42".into(), + title: "Ship the change".into(), + draft: true, + }, + ); + emit( + &mut ui, + Event::PullRequestFailed { + error: "auth token expired".into(), + }, + ); insta::assert_snapshot!(rendered(&buffer), @r" Warning: sandbox cleanup failed [sandbox_cleanup_failed] @@ -991,27 +1102,36 @@ mod tests { let mut ui = ProgressUI::new(true, false); emit(&mut ui, stage_started("fork1", "Fork")); - emit(&mut ui, Event::ParallelStarted { - node_id: "fork1".into(), - visit: 1, - branch_count: 1, - join_policy: "wait_all".into(), - }); - emit(&mut ui, Event::ParallelBranchStarted { - parallel_group_id: StageId::new("fork1", 1), - parallel_branch_id: ParallelBranchId::new(StageId::new("fork1", 1), 0), - branch: "security".into(), - index: 0, - }); - emit(&mut ui, Event::ParallelBranchCompleted { - parallel_group_id: StageId::new("fork1", 1), - parallel_branch_id: ParallelBranchId::new(StageId::new("fork1", 1), 0), - branch: "security".into(), - index: 0, - duration_ms: 500, - status: "success".into(), - head_sha: None, - }); + emit( + &mut ui, + Event::ParallelStarted { + node_id: "fork1".into(), + visit: 1, + branch_count: 1, + join_policy: "wait_all".into(), + }, + ); + emit( + &mut ui, + Event::ParallelBranchStarted { + parallel_group_id: StageId::new("fork1", 1), + parallel_branch_id: ParallelBranchId::new(StageId::new("fork1", 1), 0), + branch: "security".into(), + index: 0, + }, + ); + emit( + &mut ui, + Event::ParallelBranchCompleted { + parallel_group_id: StageId::new("fork1", 1), + parallel_branch_id: ParallelBranchId::new(StageId::new("fork1", 1), 0), + branch: "security".into(), + index: 0, + duration_ms: 500, + status: "success".into(), + head_sha: None, + }, + ); let stage = &ui.stage.active_stages["fork1"]; assert_eq!(stage.tool_calls[0].bar.prefix(), "500ms"); @@ -1031,11 +1151,11 @@ mod tests { let stage_started = serde_json::to_string(&to_run_event_at( &fixtures::RUN_1, &Event::StageStarted { - node_id: "code".into(), - name: "Code".into(), - index: 0, + node_id: "code".into(), + name: "Code".into(), + index: 0, handler_type: "agent".into(), - attempt: 1, + attempt: 1, max_attempts: 1, }, started_ts, @@ -1044,23 +1164,29 @@ mod tests { .unwrap(); let tool_started = serde_json::to_string(&to_run_event_at( &fixtures::RUN_1, - &agent_event("code", AgentEvent::ToolCallStarted { - tool_name: "read_file".into(), - tool_call_id: "tc1".into(), - arguments: serde_json::json!({"path": "src/main.rs"}), - }), + &agent_event( + "code", + AgentEvent::ToolCallStarted { + tool_name: "read_file".into(), + tool_call_id: "tc1".into(), + arguments: serde_json::json!({"path": "src/main.rs"}), + }, + ), started_ts, None, )) .unwrap(); let tool_completed = serde_json::to_string(&to_run_event_at( &fixtures::RUN_1, - &agent_event("code", AgentEvent::ToolCallCompleted { - tool_name: "read_file".into(), - tool_call_id: "tc1".into(), - output: serde_json::json!({"ok": true}), - is_error: false, - }), + &agent_event( + "code", + AgentEvent::ToolCallCompleted { + tool_name: "read_file".into(), + tool_call_id: "tc1".into(), + output: serde_json::json!({"ok": true}), + is_error: false, + }, + ), completed_ts, None, )) diff --git a/lib/crates/fabro-cli/src/commands/run/run_progress/renderer.rs b/lib/crates/fabro-cli/src/commands/run/run_progress/renderer.rs index c522efac2..0f25fbf09 100644 --- a/lib/crates/fabro-cli/src/commands/run/run_progress/renderer.rs +++ b/lib/crates/fabro-cli/src/commands/run/run_progress/renderer.rs @@ -12,14 +12,14 @@ enum RendererInner { } pub(super) struct ProgressRenderer { - inner: RendererInner, + inner: RendererInner, styles: Styles, } impl ProgressRenderer { pub(super) fn new_tty() -> Self { Self { - inner: RendererInner::Tty { + inner: RendererInner::Tty { multi: MultiProgress::new(), }, styles: Styles::new(console::colors_enabled_stderr()), @@ -28,7 +28,7 @@ impl ProgressRenderer { pub(super) fn new_plain(out: Box, colors: bool) -> Self { Self { - inner: RendererInner::Plain { + inner: RendererInner::Plain { out: Mutex::new(out), }, styles: Styles::new(colors), diff --git a/lib/crates/fabro-cli/src/commands/run/run_progress/stage_display.rs b/lib/crates/fabro-cli/src/commands/run/run_progress/stage_display.rs index e02124787..0fbb7eb56 100644 --- a/lib/crates/fabro-cli/src/commands/run/run_progress/stage_display.rs +++ b/lib/crates/fabro-cli/src/commands/run/run_progress/stage_display.rs @@ -24,18 +24,18 @@ pub(super) enum ToolCallStatus { pub(super) struct ToolCallEntry { pub(super) display_name: String, pub(super) tool_call_id: String, - pub(super) status: ToolCallStatus, - pub(super) bar: ProgressBar, - pub(super) is_branch: bool, - pub(super) started_at: Option>, + pub(super) status: ToolCallStatus, + pub(super) bar: ProgressBar, + pub(super) is_branch: bool, + pub(super) started_at: Option>, } #[derive(Debug)] pub(super) struct ActiveStage { - pub(super) display_name: String, - pub(super) has_model: bool, - pub(super) spinner: ProgressBar, - pub(super) tool_calls: VecDeque, + pub(super) display_name: String, + pub(super) has_model: bool, + pub(super) spinner: ProgressBar, + pub(super) tool_calls: VecDeque, pub(super) compaction_bar: Option, } @@ -48,12 +48,12 @@ impl ActiveStage { } pub(super) struct StageDisplay { - verbose: bool, - pub(super) active_stages: HashMap, - pub(super) stage_counts: HashMap, + verbose: bool, + pub(super) active_stages: HashMap, + pub(super) stage_counts: HashMap, pub(super) parallel_parent: Option, - any_stage_started: bool, - working_directory: Option, + any_stage_started: bool, + working_directory: Option, } impl StageDisplay { @@ -117,13 +117,16 @@ impl StageDisplay { if renderer.is_tty() { bar.enable_steady_tick(Duration::from_millis(100)); } - self.active_stages.insert(node_id.to_string(), ActiveStage { - display_name, - has_model: false, - spinner: bar, - tool_calls: VecDeque::new(), - compaction_bar: None, - }); + self.active_stages.insert( + node_id.to_string(), + ActiveStage { + display_name, + has_model: false, + spinner: bar, + tool_calls: VecDeque::new(), + compaction_bar: None, + }, + ); } pub(super) fn on_stage_completed( diff --git a/lib/crates/fabro-cli/src/commands/run/runner.rs b/lib/crates/fabro-cli/src/commands/run/runner.rs index 735c28737..bcbb28644 100644 --- a/lib/crates/fabro-cli/src/commands/run/runner.rs +++ b/lib/crates/fabro-cli/src/commands/run/runner.rs @@ -216,8 +216,8 @@ fn build_artifact_uploader( } struct HttpArtifactUploader { - run_id: RunId, - client: server_client::ServerStoreClient, + run_id: RunId, + client: server_client::ServerStoreClient, bearer_token: String, } @@ -282,7 +282,7 @@ impl StageArtifactUploader for MissingArtifactUploadTokenUploader { struct HttpRunStore { run_id: RunId, client: server_client::ServerStoreClient, - state: Arc>, + state: Arc>, events: Arc>>>, } @@ -593,12 +593,12 @@ mod tests { ); assert_eq!( worker_title_phase_for_event(&EventBody::InterviewStarted(InterviewStartedProps { - question_id: "q-1".to_string(), - question: "Approve?".to_string(), - stage: "gate".to_string(), - question_type: "yes_no".to_string(), - options: Vec::new(), - allow_freeform: false, + question_id: "q-1".to_string(), + question: "Approve?".to_string(), + stage: "gate".to_string(), + question_type: "yes_no".to_string(), + options: Vec::new(), + allow_freeform: false, timeout_seconds: None, context_display: None, })), @@ -607,39 +607,39 @@ mod tests { assert_eq!( worker_title_phase_for_event(&EventBody::InterviewCompleted(InterviewCompletedProps { question_id: "q-1".to_string(), - question: "Approve?".to_string(), - answer: "yes".to_string(), + question: "Approve?".to_string(), + answer: "yes".to_string(), duration_ms: 10, })), Some(WorkerTitlePhase::Running) ); assert_eq!( worker_title_phase_for_event(&EventBody::RunCompleted(RunCompletedProps { - duration_ms: 10, - artifact_count: 0, - status: "success".to_string(), - reason: None, - total_usd_micros: None, + duration_ms: 10, + artifact_count: 0, + status: "success".to_string(), + reason: None, + total_usd_micros: None, final_git_commit_sha: None, - final_patch: None, - billing: None, + final_patch: None, + billing: None, })), Some(WorkerTitlePhase::Succeeded) ); assert_eq!( worker_title_phase_for_event(&EventBody::RunFailed(RunFailedProps { - error: "cancelled".to_string(), - duration_ms: 10, - reason: Some(StatusReason::Cancelled), + error: "cancelled".to_string(), + duration_ms: 10, + reason: Some(StatusReason::Cancelled), git_commit_sha: None, })), Some(WorkerTitlePhase::Cancelled) ); assert_eq!( worker_title_phase_for_event(&EventBody::RunFailed(RunFailedProps { - error: "boom".to_string(), - duration_ms: 10, - reason: Some(StatusReason::Terminated), + error: "boom".to_string(), + duration_ms: 10, + reason: Some(StatusReason::Terminated), git_commit_sha: None, })), Some(WorkerTitlePhase::Failed) diff --git a/lib/crates/fabro-cli/src/commands/run/wait.rs b/lib/crates/fabro-cli/src/commands/run/wait.rs index f655645f9..60ba6fe2a 100644 --- a/lib/crates/fabro-cli/src/commands/run/wait.rs +++ b/lib/crates/fabro-cli/src/commands/run/wait.rs @@ -158,22 +158,22 @@ mod tests { fn json_output_succeeded_with_conclusion() { let run_id = fixtures::RUN_1; let conclusion = Conclusion { - timestamp: chrono::Utc::now(), - status: StageStatus::Success, - duration_ms: 12345, - failure_reason: None, + timestamp: chrono::Utc::now(), + status: StageStatus::Success, + duration_ms: 12345, + failure_reason: None, final_git_commit_sha: None, - stages: vec![], - billing: Some(BilledTokenCounts { - input_tokens: 0, - output_tokens: 0, - total_tokens: 0, - reasoning_tokens: 0, - cache_read_tokens: 0, + stages: vec![], + billing: Some(BilledTokenCounts { + input_tokens: 0, + output_tokens: 0, + total_tokens: 0, + reasoning_tokens: 0, + cache_read_tokens: 0, cache_write_tokens: 0, - total_usd_micros: Some(420_000), + total_usd_micros: Some(420_000), }), - total_retries: 0, + total_retries: 0, }; let json = build_json_output(RunStatus::Succeeded, &run_id, Some(&conclusion)); assert_eq!(json["run_id"], run_id.to_string()); @@ -202,14 +202,14 @@ mod tests { fn json_output_no_cost_when_none() { let run_id = fixtures::RUN_4; let conclusion = Conclusion { - timestamp: chrono::Utc::now(), - status: StageStatus::Fail, - duration_ms: 500, - failure_reason: Some("error".into()), + timestamp: chrono::Utc::now(), + status: StageStatus::Fail, + duration_ms: 500, + failure_reason: Some("error".into()), final_git_commit_sha: None, - stages: vec![], - billing: None, - total_retries: 0, + stages: vec![], + billing: None, + total_retries: 0, }; let json = build_json_output(RunStatus::Failed, &run_id, Some(&conclusion)); assert!(json.get("total_usd_micros").is_none()); @@ -221,22 +221,22 @@ mod tests { let styles = no_color_styles(); let run_id = fixtures::RUN_5; let conclusion = Conclusion { - timestamp: chrono::Utc::now(), - status: StageStatus::Success, - duration_ms: 8000, - failure_reason: None, + timestamp: chrono::Utc::now(), + status: StageStatus::Success, + duration_ms: 8000, + failure_reason: None, final_git_commit_sha: None, - stages: vec![], - billing: Some(BilledTokenCounts { - input_tokens: 0, - output_tokens: 0, - total_tokens: 0, - reasoning_tokens: 0, - cache_read_tokens: 0, + stages: vec![], + billing: Some(BilledTokenCounts { + input_tokens: 0, + output_tokens: 0, + total_tokens: 0, + reasoning_tokens: 0, + cache_read_tokens: 0, cache_write_tokens: 0, - total_usd_micros: Some(150_000), + total_usd_micros: Some(150_000), }), - total_retries: 0, + total_retries: 0, }; // Just verify no panic; actual stderr output is hard to capture print_human_output(RunStatus::Succeeded, &run_id, Some(&conclusion), &styles); diff --git a/lib/crates/fabro-cli/src/commands/runs/inspect.rs b/lib/crates/fabro-cli/src/commands/runs/inspect.rs index 3933c3fed..1f22e693b 100644 --- a/lib/crates/fabro-cli/src/commands/runs/inspect.rs +++ b/lib/crates/fabro-cli/src/commands/runs/inspect.rs @@ -9,13 +9,13 @@ use crate::server_runs::{ServerRunSummaryInfo, ServerSummaryLookup}; #[derive(Debug, Serialize)] pub(crate) struct InspectOutput { - pub run_id: String, - pub status: RunStatus, - pub run_record: Option, + pub run_id: String, + pub status: RunStatus, + pub run_record: Option, pub start_record: Option, - pub conclusion: Option, - pub checkpoint: Option, - pub sandbox: Option, + pub conclusion: Option, + pub checkpoint: Option, + pub sandbox: Option, } pub(crate) async fn run(args: &InspectArgs, _globals: &GlobalArgs) -> Result<()> { @@ -32,24 +32,24 @@ pub(crate) async fn run(args: &InspectArgs, _globals: &GlobalArgs) -> Result<()> fn inspect_run_state(run: &ServerRunSummaryInfo, state: RunProjection) -> InspectOutput { InspectOutput { - run_id: run.run_id().to_string(), - status: state + run_id: run.run_id().to_string(), + status: state .status .as_ref() .map_or(run.status(), |record| record.status), - run_record: state + run_record: state .run .and_then(|record| serde_json::to_value(record).ok()), start_record: state .start .and_then(|record| serde_json::to_value(record).ok()), - conclusion: state + conclusion: state .conclusion .and_then(|record| serde_json::to_value(record).ok()), - checkpoint: state + checkpoint: state .checkpoint .and_then(|record| serde_json::to_value(record).ok()), - sandbox: state + sandbox: state .sandbox .and_then(|record| serde_json::to_value(record).ok()), } diff --git a/lib/crates/fabro-cli/src/commands/server/foreground.rs b/lib/crates/fabro-cli/src/commands/server/foreground.rs index ba469d061..a429a084c 100644 --- a/lib/crates/fabro-cli/src/commands/server/foreground.rs +++ b/lib/crates/fabro-cli/src/commands/server/foreground.rs @@ -48,12 +48,15 @@ pub(crate) async fn execute( styles, storage_dir, move |resolved_bind| { - record::write_server_record(&record_path, &record::ServerRecord { - pid, - bind: resolved_bind.clone(), - log_path: log_path.clone(), - started_at: Utc::now(), - }) + record::write_server_record( + &record_path, + &record::ServerRecord { + pid, + bind: resolved_bind.clone(), + log_path: log_path.clone(), + started_at: Utc::now(), + }, + ) }, )) .await diff --git a/lib/crates/fabro-cli/src/commands/server/record.rs b/lib/crates/fabro-cli/src/commands/server/record.rs index 27129208a..629b7083a 100644 --- a/lib/crates/fabro-cli/src/commands/server/record.rs +++ b/lib/crates/fabro-cli/src/commands/server/record.rs @@ -9,15 +9,15 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct ServerRecord { - pub pid: u32, - pub bind: Bind, - pub log_path: PathBuf, + pub pid: u32, + pub bind: Bind, + pub log_path: PathBuf, pub started_at: DateTime, } #[derive(Debug, Clone)] pub(crate) struct ActiveServerRecord { - pub record: ServerRecord, + pub record: ServerRecord, pub record_path: PathBuf, } diff --git a/lib/crates/fabro-cli/src/commands/server/start.rs b/lib/crates/fabro-cli/src/commands/server/start.rs index c15680923..00b40ac8d 100644 --- a/lib/crates/fabro-cli/src/commands/server/start.rs +++ b/lib/crates/fabro-cli/src/commands/server/start.rs @@ -69,15 +69,15 @@ fn ensure_server_running_with_bind( } let serve_args = ServeArgs { - bind: None, - web: false, - no_web: false, - model: None, - provider: None, - dry_run: false, - sandbox: None, + bind: None, + web: false, + no_web: false, + model: None, + provider: None, + dry_run: false, + sandbox: None, max_concurrent_runs: server_max_concurrent_runs_override(), - config: Some(config_path.to_path_buf()), + config: Some(config_path.to_path_buf()), }; let bind_request = match &bind { @@ -148,12 +148,15 @@ async fn execute_foreground( styles, Some(storage_dir), move |resolved_bind| { - record::write_server_record(&record_path, &record::ServerRecord { - pid, - bind: resolved_bind.clone(), - log_path: log_path.clone(), - started_at: Utc::now(), - }) + record::write_server_record( + &record_path, + &record::ServerRecord { + pid, + bind: resolved_bind.clone(), + log_path: log_path.clone(), + started_at: Utc::now(), + }, + ) }, )) .await diff --git a/lib/crates/fabro-cli/src/commands/store/dump.rs b/lib/crates/fabro-cli/src/commands/store/dump.rs index 16b4020c9..ae12fafa4 100644 --- a/lib/crates/fabro-cli/src/commands/store/dump.rs +++ b/lib/crates/fabro-cli/src/commands/store/dump.rs @@ -82,9 +82,9 @@ fn finalize_export( } struct DumpArtifact { - stage_id: StageId, + stage_id: StageId, relative_path: String, - data: Vec, + data: Vec, } trait DumpDataSource { @@ -97,9 +97,9 @@ trait DumpDataSource { #[cfg(test)] struct LocalDumpSource<'a> { - run_store: &'a RunDatabase, + run_store: &'a RunDatabase, artifact_store: &'a ArtifactStore, - run_id: RunId, + run_id: RunId, } #[cfg(test)] @@ -140,9 +140,9 @@ impl DumpDataSource for LocalDumpSource<'_> { ) })?; artifacts.push(DumpArtifact { - stage_id: asset.node, + stage_id: asset.node, relative_path: asset.filename, - data: data.to_vec(), + data: data.to_vec(), }); } Ok(artifacts) @@ -362,52 +362,49 @@ mod tests { fn sample_status() -> RunStatusRecord { RunStatusRecord { - status: RunStatus::Running, - reason: Some(StatusReason::SandboxInitializing), + status: RunStatus::Running, + reason: Some(StatusReason::SandboxInitializing), updated_at: dt("2026-03-27T12:05:00Z"), } } fn sample_checkpoint(current_node: &str, visit: u32) -> Checkpoint { Checkpoint { - timestamp: dt("2026-03-27T12:10:00Z"), - current_node: current_node.to_string(), - completed_nodes: vec!["plan".to_string()], - node_retries: HashMap::from([( - current_node.to_string(), - visit.saturating_sub(1), - )]), - context_values: HashMap::from([( + timestamp: dt("2026-03-27T12:10:00Z"), + current_node: current_node.to_string(), + completed_nodes: vec!["plan".to_string()], + node_retries: HashMap::from([(current_node.to_string(), visit.saturating_sub(1))]), + context_values: HashMap::from([( "artifact".to_string(), serde_json::json!({"kind": "summary"}), )]), - node_outcomes: HashMap::new(), - next_node_id: Some("review".to_string()), - git_commit_sha: Some("def456".to_string()), - loop_failure_signatures: HashMap::new(), + node_outcomes: HashMap::new(), + next_node_id: Some("review".to_string()), + git_commit_sha: Some("def456".to_string()), + loop_failure_signatures: HashMap::new(), restart_failure_signatures: HashMap::new(), - node_visits: HashMap::from([(current_node.to_string(), visit as usize)]), + node_visits: HashMap::from([(current_node.to_string(), visit as usize)]), } } fn sample_conclusion() -> Conclusion { Conclusion { - timestamp: dt("2026-03-27T12:15:00Z"), - status: StageStatus::Success, - duration_ms: 3210, - failure_reason: None, + timestamp: dt("2026-03-27T12:15:00Z"), + status: StageStatus::Success, + duration_ms: 3210, + failure_reason: None, final_git_commit_sha: Some("feedbeef".to_string()), - stages: Vec::new(), - billing: Some(BilledTokenCounts { - input_tokens: 10, - output_tokens: 20, - total_tokens: 150, - reasoning_tokens: 50, - cache_read_tokens: 30, + stages: Vec::new(), + billing: Some(BilledTokenCounts { + input_tokens: 10, + output_tokens: 20, + total_tokens: 150, + reasoning_tokens: 50, + cache_read_tokens: 30, cache_write_tokens: 40, - total_usd_micros: Some(1_250_000), + total_usd_micros: Some(1_250_000), }), - total_retries: 2, + total_retries: 2, } } @@ -420,12 +417,12 @@ mod tests { smoothness: None, stages: Vec::new(), stats: AggregateStats { - total_duration_ms: 3210, + total_duration_ms: 3210, total_billing_usd_micros: Some(1_250_000), - total_retries: 2, - files_touched: vec!["src/lib.rs".to_string()], - stages_completed: 3, - stages_failed: 0, + total_retries: 2, + files_touched: vec!["src/lib.rs".to_string()], + stages_completed: 3, + stages_failed: 0, }, intent: Some("ship the fix".to_string()), outcome: Some("done".to_string()), @@ -437,11 +434,11 @@ mod tests { fn sample_sandbox() -> SandboxRecord { SandboxRecord { - provider: "local".to_string(), - working_directory: "/tmp/night-sky".to_string(), - identifier: Some("sandbox-1".to_string()), + provider: "local".to_string(), + working_directory: "/tmp/night-sky".to_string(), + identifier: Some("sandbox-1".to_string()), host_working_directory: Some("/tmp/night-sky".to_string()), - container_mount_point: None, + container_mount_point: None, } } @@ -478,171 +475,223 @@ mod tests { ); let node = StageId::new("code", 2); - append_event(&run, &run_id, &Event::RunCreated { - run_id, - settings: serde_json::to_value(&run_record.settings).unwrap(), - graph: serde_json::to_value(&run_record.graph).unwrap(), - workflow_source: Some("digraph night_sky {}".to_string()), - workflow_config: None, - labels: run_record.labels.clone().into_iter().collect(), - run_dir: "/tmp/night-sky-run".to_string(), - working_directory: run_record.working_directory.display().to_string(), - host_repo_path: run_record.host_repo_path.clone(), - repo_origin_url: run_record.repo_origin_url.clone(), - base_branch: run_record.base_branch.clone(), - workflow_slug: run_record.workflow_slug.clone(), - db_prefix: None, - provenance: run_record.provenance.clone(), - manifest_blob: None, - }) + append_event( + &run, + &run_id, + &Event::RunCreated { + run_id, + settings: serde_json::to_value(&run_record.settings).unwrap(), + graph: serde_json::to_value(&run_record.graph).unwrap(), + workflow_source: Some("digraph night_sky {}".to_string()), + workflow_config: None, + labels: run_record.labels.clone().into_iter().collect(), + run_dir: "/tmp/night-sky-run".to_string(), + working_directory: run_record.working_directory.display().to_string(), + host_repo_path: run_record.host_repo_path.clone(), + repo_origin_url: run_record.repo_origin_url.clone(), + base_branch: run_record.base_branch.clone(), + workflow_slug: run_record.workflow_slug.clone(), + db_prefix: None, + provenance: run_record.provenance.clone(), + manifest_blob: None, + }, + ) .await .unwrap(); - append_event(&run, &run_id, &Event::WorkflowRunStarted { - name: "night-sky".to_string(), - run_id, - base_branch: run_record.base_branch.clone(), - base_sha: start_record.base_sha.clone(), - run_branch: start_record.run_branch.clone(), - worktree_dir: None, - goal: Some("map the constellations".to_string()), - }) + append_event( + &run, + &run_id, + &Event::WorkflowRunStarted { + name: "night-sky".to_string(), + run_id, + base_branch: run_record.base_branch.clone(), + base_sha: start_record.base_sha.clone(), + run_branch: start_record.run_branch.clone(), + worktree_dir: None, + goal: Some("map the constellations".to_string()), + }, + ) .await .unwrap(); - append_event(&run, &run_id, &Event::RunRunning { - reason: status_record.reason, - }) + append_event( + &run, + &run_id, + &Event::RunRunning { + reason: status_record.reason, + }, + ) .await .unwrap(); for checkpoint in [&first_checkpoint, &second_checkpoint] { - append_event(&run, &run_id, &Event::CheckpointCompleted { - node_id: checkpoint.current_node.clone(), - status: "success".to_string(), - current_node: checkpoint.current_node.clone(), - completed_nodes: checkpoint.completed_nodes.clone(), - node_retries: checkpoint.node_retries.clone().into_iter().collect(), - context_values: checkpoint.context_values.clone().into_iter().collect(), - node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(), - next_node_id: checkpoint.next_node_id.clone(), - git_commit_sha: checkpoint.git_commit_sha.clone(), - loop_failure_signatures: checkpoint - .loop_failure_signatures - .clone() - .into_iter() - .map(|(signature, count)| (signature.to_string(), count)) - .collect(), - restart_failure_signatures: checkpoint - .restart_failure_signatures - .clone() - .into_iter() - .map(|(signature, count)| (signature.to_string(), count)) - .collect(), - node_visits: checkpoint.node_visits.clone().into_iter().collect(), - diff: None, - }) + append_event( + &run, + &run_id, + &Event::CheckpointCompleted { + node_id: checkpoint.current_node.clone(), + status: "success".to_string(), + current_node: checkpoint.current_node.clone(), + completed_nodes: checkpoint.completed_nodes.clone(), + node_retries: checkpoint.node_retries.clone().into_iter().collect(), + context_values: checkpoint.context_values.clone().into_iter().collect(), + node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(), + next_node_id: checkpoint.next_node_id.clone(), + git_commit_sha: checkpoint.git_commit_sha.clone(), + loop_failure_signatures: checkpoint + .loop_failure_signatures + .clone() + .into_iter() + .map(|(signature, count)| (signature.to_string(), count)) + .collect(), + restart_failure_signatures: checkpoint + .restart_failure_signatures + .clone() + .into_iter() + .map(|(signature, count)| (signature.to_string(), count)) + .collect(), + node_visits: checkpoint.node_visits.clone().into_iter().collect(), + diff: None, + }, + ) .await .unwrap(); } - append_event(&run, &run_id, &Event::SandboxInitialized { - working_directory: sandbox.working_directory.clone(), - provider: sandbox.provider.clone(), - identifier: sandbox.identifier.clone(), - host_working_directory: sandbox.host_working_directory.clone(), - container_mount_point: sandbox.container_mount_point.clone(), - }) + append_event( + &run, + &run_id, + &Event::SandboxInitialized { + working_directory: sandbox.working_directory.clone(), + provider: sandbox.provider.clone(), + identifier: sandbox.identifier.clone(), + host_working_directory: sandbox.host_working_directory.clone(), + container_mount_point: sandbox.container_mount_point.clone(), + }, + ) .await .unwrap(); - append_event(&run, &run_id, &Event::Prompt { - stage: "code".to_string(), - visit: 2, - text: "Plan the fix".to_string(), - mode: None, - provider: None, - model: None, - }) + append_event( + &run, + &run_id, + &Event::Prompt { + stage: "code".to_string(), + visit: 2, + text: "Plan the fix".to_string(), + mode: None, + provider: None, + model: None, + }, + ) .await .unwrap(); - append_event(&run, &run_id, &Event::PromptCompleted { - node_id: "code".to_string(), - response: "Implemented".to_string(), - model: "gpt-5".to_string(), - provider: "openai".to_string(), - billing: None, - }) + append_event( + &run, + &run_id, + &Event::PromptCompleted { + node_id: "code".to_string(), + response: "Implemented".to_string(), + model: "gpt-5".to_string(), + provider: "openai".to_string(), + billing: None, + }, + ) .await .unwrap(); - append_event(&run, &run_id, &Event::StageCompleted { - node_id: "code".to_string(), - name: "Code".to_string(), - index: 1, - duration_ms: 250, - status: "partial_success".to_string(), - preferred_label: None, - suggested_next_ids: Vec::new(), - billing: None, - failure: None, - notes: Some("captured output".to_string()), - files_touched: Vec::new(), - context_updates: None, - jump_to_node: None, - context_values: None, - node_visits: Some(std::collections::BTreeMap::from([( - "code".to_string(), - 2usize, - )])), - loop_failure_signatures: None, - restart_failure_signatures: None, - response: Some("Implemented".to_string()), - attempt: 1, - max_attempts: 1, - }) + append_event( + &run, + &run_id, + &Event::StageCompleted { + node_id: "code".to_string(), + name: "Code".to_string(), + index: 1, + duration_ms: 250, + status: "partial_success".to_string(), + preferred_label: None, + suggested_next_ids: Vec::new(), + billing: None, + failure: None, + notes: Some("captured output".to_string()), + files_touched: Vec::new(), + context_updates: None, + jump_to_node: None, + context_values: None, + node_visits: Some(std::collections::BTreeMap::from([( + "code".to_string(), + 2usize, + )])), + loop_failure_signatures: None, + restart_failure_signatures: None, + response: Some("Implemented".to_string()), + attempt: 1, + max_attempts: 1, + }, + ) .await .unwrap(); - append_event(&run, &run_id, &Event::CommandStarted { - node_id: "code".to_string(), - script: "echo hi".to_string(), - command: "echo hi".to_string(), - language: "sh".to_string(), - timeout_ms: None, - }) + append_event( + &run, + &run_id, + &Event::CommandStarted { + node_id: "code".to_string(), + script: "echo hi".to_string(), + command: "echo hi".to_string(), + language: "sh".to_string(), + timeout_ms: None, + }, + ) .await .unwrap(); - append_event(&run, &run_id, &Event::CommandCompleted { - node_id: "code".to_string(), - stdout: "stdout line".to_string(), - stderr: String::new(), - exit_code: Some(0), - duration_ms: 100, - timed_out: false, - }) + append_event( + &run, + &run_id, + &Event::CommandCompleted { + node_id: "code".to_string(), + stdout: "stdout line".to_string(), + stderr: String::new(), + exit_code: Some(0), + duration_ms: 100, + timed_out: false, + }, + ) .await .unwrap(); - append_event(&run, &run_id, &Event::RetroStarted { - prompt: Some("How did it go?".to_string()), - provider: None, - model: None, - }) + append_event( + &run, + &run_id, + &Event::RetroStarted { + prompt: Some("How did it go?".to_string()), + provider: None, + model: None, + }, + ) .await .unwrap(); - append_event(&run, &run_id, &Event::RetroCompleted { - duration_ms: 50, - response: Some("Smooth enough".to_string()), - retro: Some(serde_json::to_value(&retro).unwrap()), - }) + append_event( + &run, + &run_id, + &Event::RetroCompleted { + duration_ms: 50, + response: Some("Smooth enough".to_string()), + retro: Some(serde_json::to_value(&retro).unwrap()), + }, + ) .await .unwrap(); - append_event(&run, &run_id, &Event::WorkflowRunCompleted { - duration_ms: conclusion.duration_ms, - artifact_count: 0, - status: "success".to_string(), - reason: None, - total_usd_micros: conclusion - .billing - .as_ref() - .and_then(|billing| billing.total_usd_micros), - final_git_commit_sha: conclusion.final_git_commit_sha.clone(), - final_patch: None, - billing: conclusion.billing.clone(), - }) + append_event( + &run, + &run_id, + &Event::WorkflowRunCompleted { + duration_ms: conclusion.duration_ms, + artifact_count: 0, + status: "success".to_string(), + reason: None, + total_usd_micros: conclusion + .billing + .as_ref() + .and_then(|billing| billing.total_usd_micros), + final_git_commit_sha: conclusion.final_git_commit_sha.clone(), + final_patch: None, + billing: conclusion.billing.clone(), + }, + ) .await .unwrap(); run.append_event( diff --git a/lib/crates/fabro-cli/src/commands/system/prune.rs b/lib/crates/fabro-cli/src/commands/system/prune.rs index df5c7e1c7..375962c18 100644 --- a/lib/crates/fabro-cli/src/commands/system/prune.rs +++ b/lib/crates/fabro-cli/src/commands/system/prune.rs @@ -16,12 +16,12 @@ pub(super) async fn prune_command(args: &RunsPruneArgs, globals: &GlobalArgs) -> .api() .prune_runs() .body(types::PruneRunsRequest { - before: args.filter.before.clone(), - dry_run: !args.yes, - labels: parse_label_filters(&args.filter.label), + before: args.filter.before.clone(), + dry_run: !args.yes, + labels: parse_label_filters(&args.filter.label), older_than: args.older_than.map(format_duration), - orphans: args.filter.orphans, - workflow: args.filter.workflow.clone(), + orphans: args.filter.orphans, + workflow: args.filter.workflow.clone(), }) .send() .await diff --git a/lib/crates/fabro-cli/src/commands/uninstall.rs b/lib/crates/fabro-cli/src/commands/uninstall.rs index 17cfd0b13..6cceb5bb1 100644 --- a/lib/crates/fabro-cli/src/commands/uninstall.rs +++ b/lib/crates/fabro-cli/src/commands/uninstall.rs @@ -15,13 +15,13 @@ use crate::user_config; #[derive(Debug, Serialize)] struct Inventory { - home_root: PathBuf, - storage_dir: PathBuf, - home_exists: bool, - home_size: u64, - server_running: bool, - shell_configs: Vec, - binary_path: Option, + home_root: PathBuf, + storage_dir: PathBuf, + home_exists: bool, + home_size: u64, + server_running: bool, + shell_configs: Vec, + binary_path: Option, binary_is_managed: bool, } @@ -203,12 +203,12 @@ fn print_preview(inventory: &Inventory) { #[derive(Debug, Serialize)] struct UninstallResult { - status: &'static str, - home_removed: bool, - server_stopped: bool, + status: &'static str, + home_removed: bool, + server_stopped: bool, shell_configs_cleaned: Vec, - binary_removed: bool, - binary_hint: Option, + binary_removed: bool, + binary_hint: Option, } fn execute_uninstall(inventory: &Inventory, json: bool) -> Result<()> { @@ -217,12 +217,12 @@ fn execute_uninstall(inventory: &Inventory, json: bool) -> Result<()> { let bold = console::Style::new().bold(); let mut critical_failure = false; let mut result = UninstallResult { - status: "completed", - home_removed: false, - server_stopped: false, + status: "completed", + home_removed: false, + server_stopped: false, shell_configs_cleaned: Vec::new(), - binary_removed: false, - binary_hint: None, + binary_removed: false, + binary_hint: None, }; // Unit 3a: Server stop diff --git a/lib/crates/fabro-cli/src/commands/upgrade.rs b/lib/crates/fabro-cli/src/commands/upgrade.rs index ee88accd8..43755de70 100644 --- a/lib/crates/fabro-cli/src/commands/upgrade.rs +++ b/lib/crates/fabro-cli/src/commands/upgrade.rs @@ -193,7 +193,7 @@ const LAST_CHECK_FILE: &str = "last_upgrade_check.json"; #[derive(serde::Serialize, serde::Deserialize)] struct UpgradeCheckState { - checked_at: u64, + checked_at: u64, latest_version: String, } @@ -410,7 +410,7 @@ async fn check_and_print_notice() -> Result<()> { .unwrap_or_default() .as_secs(); let state = UpgradeCheckState { - checked_at: now, + checked_at: now, latest_version: latest.to_string(), }; let _ = state.save(&state_path); @@ -503,7 +503,7 @@ mod tests { #[test] fn upgrade_check_state_roundtrip() { let state = UpgradeCheckState { - checked_at: 1_710_000_000, + checked_at: 1_710_000_000, latest_version: "0.5.0".to_string(), }; let json = serde_json::to_string(&state).unwrap(); @@ -515,7 +515,7 @@ mod tests { #[test] fn upgrade_check_state_stale() { let old = UpgradeCheckState { - checked_at: 0, // epoch — definitely stale + checked_at: 0, // epoch — definitely stale latest_version: "0.1.0".to_string(), }; assert!(old.is_stale()); @@ -528,7 +528,7 @@ mod tests { .unwrap() .as_secs(); let fresh = UpgradeCheckState { - checked_at: now, + checked_at: now, latest_version: "0.5.0".to_string(), }; assert!(!fresh.is_stale()); @@ -539,7 +539,7 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("state.json"); let state = UpgradeCheckState { - checked_at: 1_710_000_000, + checked_at: 1_710_000_000, latest_version: "0.5.0".to_string(), }; state.save(&path).unwrap(); diff --git a/lib/crates/fabro-cli/src/commands/validate.rs b/lib/crates/fabro-cli/src/commands/validate.rs index f8a86fbaa..f35666cfb 100644 --- a/lib/crates/fabro-cli/src/commands/validate.rs +++ b/lib/crates/fabro-cli/src/commands/validate.rs @@ -17,12 +17,12 @@ pub(crate) async fn run( ) -> anyhow::Result<()> { let ctx = CommandContext::for_target(&args.target)?; let built = build_run_manifest(ManifestBuildInput { - workflow: args.workflow.clone(), - cwd: ctx.cwd().to_path_buf(), - args_layer: SettingsLayer::default(), - args: None, - run_id: None, - user_layer: load_settings_user()?, + workflow: args.workflow.clone(), + cwd: ctx.cwd().to_path_buf(), + args_layer: SettingsLayer::default(), + args: None, + run_id: None, + user_layer: load_settings_user()?, user_settings_path: Some(active_settings_path(None)), })?; let client = ctx.server().await?; diff --git a/lib/crates/fabro-cli/src/server_client.rs b/lib/crates/fabro-cli/src/server_client.rs index 9cc115c46..132a30a9c 100644 --- a/lib/crates/fabro-cli/src/server_client.rs +++ b/lib/crates/fabro-cli/src/server_client.rs @@ -27,20 +27,20 @@ use crate::{sse, user_config}; #[derive(Clone)] pub(crate) struct ServerStoreClient { - client: fabro_api::Client, + client: fabro_api::Client, http_client: reqwest::Client, - base_url: String, + base_url: String, } #[derive(Debug, Clone)] struct LocalServerRuntime { active_config_path: PathBuf, - storage_dir: PathBuf, + storage_dir: PathBuf, } pub(crate) struct RunAttachEventStream { - stream: progenitor_client::ByteStream, - pending_bytes: Vec, + stream: progenitor_client::ByteStream, + pending_bytes: Vec, buffered_events: VecDeque, } @@ -105,7 +105,7 @@ pub(crate) async fn connect_server_with_settings( let target = user_config::resolve_server_target(args, settings)?; let runtime = LocalServerRuntime { active_config_path: base_config_path.to_path_buf(), - storage_dir: user_config::storage_dir(settings)?, + storage_dir: user_config::storage_dir(settings)?, }; connect_target_api_client_bundle(&target, &runtime).await } @@ -236,14 +236,14 @@ struct ArtifactBatchUploadManifest { #[derive(Debug, Serialize)] struct ArtifactBatchUploadEntry { - part: String, - path: String, + part: String, + path: String, #[serde(skip_serializing_if = "Option::is_none")] - sha256: Option, + sha256: Option, #[serde(skip_serializing_if = "Option::is_none")] expected_bytes: Option, #[serde(skip_serializing_if = "Option::is_none")] - content_type: Option, + content_type: Option, } impl ServerStoreClient { @@ -660,11 +660,11 @@ impl ServerStoreClient { .len(); manifest_entries.push(ArtifactBatchUploadEntry { - part: part_name.clone(), - path: artifact.path.clone(), - sha256: Some(artifact.content_sha256.clone()), + part: part_name.clone(), + path: artifact.path.clone(), + sha256: Some(artifact.content_sha256.clone()), expected_bytes: Some(artifact.bytes), - content_type: Some(artifact.mime.clone()), + content_type: Some(artifact.mime.clone()), }); file_parts.push(( diff --git a/lib/crates/fabro-cli/src/server_runs.rs b/lib/crates/fabro-cli/src/server_runs.rs index 8020f7882..f3ef05b79 100644 --- a/lib/crates/fabro-cli/src/server_runs.rs +++ b/lib/crates/fabro-cli/src/server_runs.rs @@ -11,9 +11,9 @@ use fabro_workflow::run_lookup::{RunInfo, resolve_run_from_summaries, scratch_ba use crate::server_client::{self, ServerStoreClient}; pub(crate) struct ServerRunLookup { - client: ServerStoreClient, + client: ServerStoreClient, scratch_base: PathBuf, - summaries: Vec, + summaries: Vec, } impl ServerRunLookup { @@ -105,7 +105,7 @@ impl ServerRunSummaryInfo { pub(crate) struct ServerSummaryLookup { client: Arc, - runs: Vec, + runs: Vec, } impl ServerSummaryLookup { diff --git a/lib/crates/fabro-cli/src/shared/openai_jwt.rs b/lib/crates/fabro-cli/src/shared/openai_jwt.rs index e15665550..712fbf7fd 100644 --- a/lib/crates/fabro-cli/src/shared/openai_jwt.rs +++ b/lib/crates/fabro-cli/src/shared/openai_jwt.rs @@ -11,9 +11,9 @@ struct JwtPayload { #[serde(default)] chatgpt_account_id: Option, #[serde(default, rename = "https://api.openai.com/auth")] - auth_claim: Option, + auth_claim: Option, #[serde(default)] - organizations: Option>, + organizations: Option>, } #[derive(Deserialize)] diff --git a/lib/crates/fabro-cli/src/user_config.rs b/lib/crates/fabro-cli/src/user_config.rs index 317a9b332..533ad95fa 100644 --- a/lib/crates/fabro-cli/src/user_config.rs +++ b/lib/crates/fabro-cli/src/user_config.rs @@ -12,8 +12,8 @@ use tracing::debug; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub(crate) struct ClientTlsSettings { pub cert: PathBuf, - pub key: PathBuf, - pub ca: PathBuf, + pub key: PathBuf, + pub ca: PathBuf, } use crate::args::ServerTargetArgs; @@ -85,7 +85,7 @@ pub(crate) fn apply_storage_dir_override( pub(crate) enum ServerTarget { HttpUrl { api_url: String, - tls: Option, + tls: Option, }, UnixSocket(PathBuf), } @@ -99,8 +99,8 @@ fn cli_target_from_settings(settings: &CliSettings) -> Option<(String, Option { let tls_settings = tls.as_ref().map(|tls| ClientTlsSettings { cert: PathBuf::from(tls.cert.as_source()), - key: PathBuf::from(tls.key.as_source()), - ca: PathBuf::from(tls.ca.as_source()), + key: PathBuf::from(tls.key.as_source()), + ca: PathBuf::from(tls.ca.as_source()), }); Some((url.as_source(), tls_settings)) } @@ -265,7 +265,7 @@ mod tests { .unwrap(), Some(ServerTarget::HttpUrl { api_url: "https://cli.example.com".to_string(), - tls: None, + tls: None, }) ); } @@ -311,7 +311,7 @@ url = "https://config.example.com" resolve_server_target(&server_target_args(None), &settings).unwrap(), ServerTarget::HttpUrl { api_url: "https://config.example.com".to_string(), - tls: None, + tls: None, } ); } @@ -335,7 +335,7 @@ url = "https://config.example.com" .unwrap(), ServerTarget::HttpUrl { api_url: "https://cli.example.com".to_string(), - tls: None, + tls: None, } ); } @@ -368,7 +368,7 @@ url = "https://config.example.com" .unwrap(), ServerTarget::HttpUrl { api_url: "https://cli.example.com".to_string(), - tls: None, + tls: None, } ); } @@ -377,8 +377,8 @@ url = "https://config.example.com" fn remote_target_uses_tls_from_config() { let expected_tls = ClientTlsSettings { cert: PathBuf::from("cert.pem"), - key: PathBuf::from("key.pem"), - ca: PathBuf::from("ca.pem"), + key: PathBuf::from("key.pem"), + ca: PathBuf::from("ca.pem"), }; let settings = parse_v2( r#" @@ -402,7 +402,7 @@ ca = "ca.pem" .unwrap(), Some(ServerTarget::HttpUrl { api_url: "https://cli.example.com".to_string(), - tls: Some(expected_tls), + tls: Some(expected_tls), }) ); } diff --git a/lib/crates/fabro-cli/tests/it/cmd/config.rs b/lib/crates/fabro-cli/tests/it/cmd/config.rs index 22e89b974..ea6c7fe8e 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/config.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/config.rs @@ -435,10 +435,10 @@ fn settings_local_workflow_name_applies_run_overlay_and_deep_merges() { // checkpoint.exclude_globs is a security/policy list: replace by default. let checkpoint = run_checkpoint(&cfg); - assert_eq!(checkpoint.exclude_globs, vec![ - "run-only".to_string(), - "shared".to_string() - ]); + assert_eq!( + checkpoint.exclude_globs, + vec!["run-only".to_string(), "shared".to_string()] + ); // Hooks: id-based replacement. The "shared" hook appears in both cli and // workflow layers and resolves to the workflow entry; project and run-only @@ -528,9 +528,10 @@ fn settings_local_explicit_workflow_path_uses_workflow_project_layers() { assert!(auto_approve_enabled(&cfg)); // v2 R30: run.prepare.steps replaces the whole ordered list across layers. // The highest-precedence layer (workflow) wins. - assert_eq!(run_prepare_commands(&cfg), vec![ - "workflow-setup".to_string() - ]); + assert_eq!( + run_prepare_commands(&cfg), + vec!["workflow-setup".to_string()] + ); assert_eq!(run_sandbox(&cfg).preserve, Some(true)); } diff --git a/lib/crates/fabro-cli/tests/it/cmd/fork.rs b/lib/crates/fabro-cli/tests/it/cmd/fork.rs index 162bdf6a5..cfea00b85 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/fork.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/fork.rs @@ -84,10 +84,10 @@ fn fork_latest_prints_new_run_and_resume_hint() { ); let new_run_id = &new_run_ids[0]; - let new_head = git_stdout(&setup.repo_dir, &[ - "rev-parse", - &format!("fabro/run/{new_run_id}"), - ]); + let new_head = git_stdout( + &setup.repo_dir, + &["rev-parse", &format!("fabro/run/{new_run_id}")], + ); let expected_head = run_branch_commits(&setup.repo_dir, &setup.run.run_id) .into_iter() .last() @@ -128,10 +128,10 @@ fn fork_from_earlier_checkpoint_uses_expected_sha() { ); let new_run_id = &new_run_ids[0]; - let new_head = git_stdout(&setup.repo_dir, &[ - "rev-parse", - &format!("fabro/run/{new_run_id}"), - ]); + let new_head = git_stdout( + &setup.repo_dir, + &["rev-parse", &format!("fabro/run/{new_run_id}")], + ); assert_eq!(new_head.trim(), expected_head); let checkpoint = git_show_json( diff --git a/lib/crates/fabro-cli/tests/it/cmd/logs.rs b/lib/crates/fabro-cli/tests/it/cmd/logs.rs index 0ec7bfe45..08749487e 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/logs.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/logs.rs @@ -91,14 +91,17 @@ fn logs_completed_run_outputs_raw_ndjson() { let events = parse_ndjson(&output.stdout); assert_events_belong_to_run(&events, &run.run_id); - assert_event_sequence_contains(&events, &[ - "run.created", - "run.running", - "stage.started", - "stage.completed", - "run.completed", - "sandbox.cleanup.completed", - ]); + assert_event_sequence_contains( + &events, + &[ + "run.created", + "run.running", + "stage.started", + "stage.completed", + "run.completed", + "sandbox.cleanup.completed", + ], + ); } #[test] @@ -224,12 +227,15 @@ fn logs_follow_detached_run_streams_until_completion() { let events = parse_ndjson(&output.stdout); assert_events_belong_to_run(&events, &run.run_id); - assert_event_sequence_contains(&events, &[ - "run.created", - "run.running", - "stage.started", - "stage.completed", - "run.completed", - "sandbox.cleanup.completed", - ]); + assert_event_sequence_contains( + &events, + &[ + "run.created", + "run.running", + "stage.started", + "stage.completed", + "run.completed", + "sandbox.cleanup.completed", + ], + ); } diff --git a/lib/crates/fabro-cli/tests/it/cmd/pr_view.rs b/lib/crates/fabro-cli/tests/it/cmd/pr_view.rs index 6a6e56753..49fe73cf4 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/pr_view.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/pr_view.rs @@ -98,14 +98,14 @@ fn pr_view_reads_pull_request_from_store_without_pull_request_json() { tool_call_id: None, actor: None, body: EventBody::PullRequestCreated(PullRequestCreatedProps { - pr_url: "https://github.com/fabro-sh/fabro/pull/123".to_string(), - pr_number: 123, - owner: "fabro-sh".to_string(), - repo: "fabro".to_string(), + pr_url: "https://github.com/fabro-sh/fabro/pull/123".to_string(), + pr_number: 123, + owner: "fabro-sh".to_string(), + repo: "fabro".to_string(), base_branch: "main".to_string(), head_branch: "fabro/run/demo".to_string(), - title: "Map the constellations".to_string(), - draft: false, + title: "Map the constellations".to_string(), + draft: false, }), }; client diff --git a/lib/crates/fabro-cli/tests/it/cmd/resume.rs b/lib/crates/fabro-cli/tests/it/cmd/resume.rs index 5a1a940dc..aad477a14 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/resume.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/resume.rs @@ -69,10 +69,10 @@ fn resume_rewound_run_succeeds() { String::from_utf8_lossy(&rewind.stdout), output_stderr(&rewind) ); - let rewound_head = git_stdout(&setup.repo_dir, &[ - "rev-parse", - &format!("fabro/run/{}", setup.run.run_id), - ]); + let rewound_head = git_stdout( + &setup.repo_dir, + &["rev-parse", &format!("fabro/run/{}", setup.run.run_id)], + ); let mut resume_cmd = context.command(); resume_cmd.current_dir(&setup.repo_dir); @@ -109,10 +109,10 @@ fn resume_rewound_run_succeeds() { std::fs::read_to_string(setup.repo_dir.join("story.txt")).unwrap(), "line 1\n" ); - let resumed_head = git_stdout(&setup.repo_dir, &[ - "rev-parse", - &format!("fabro/run/{}", setup.run.run_id), - ]); + let resumed_head = git_stdout( + &setup.repo_dir, + &["rev-parse", &format!("fabro/run/{}", setup.run.run_id)], + ); assert_ne!(resumed_head.trim(), rewound_head.trim()); } diff --git a/lib/crates/fabro-cli/tests/it/cmd/rewind.rs b/lib/crates/fabro-cli/tests/it/cmd/rewind.rs index 3122c81e7..b5052f165 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/rewind.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/rewind.rs @@ -99,10 +99,10 @@ fn rewind_target_updates_metadata_and_resume_hint() { "); assert!(output.status.success(), "rewind should succeed"); - let run_head = git_stdout(&setup.repo_dir, &[ - "rev-parse", - &format!("fabro/run/{}", setup.run.run_id), - ]); + let run_head = git_stdout( + &setup.repo_dir, + &["rev-parse", &format!("fabro/run/{}", setup.run.run_id)], + ); assert_eq!(run_head.trim(), expected_run_head); let mut list_cmd = context.command(); diff --git a/lib/crates/fabro-cli/tests/it/cmd/support.rs b/lib/crates/fabro-cli/tests/it/cmd/support.rs index 5c78df473..71f702430 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/support.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs @@ -31,23 +31,23 @@ struct RunSummaryRecord { } pub(crate) struct RunSetup { - pub(crate) run_id: String, + pub(crate) run_id: String, pub(crate) run_dir: PathBuf, } pub(crate) struct GitRunSetup { - pub(crate) run: RunSetup, + pub(crate) run: RunSetup, pub(crate) repo_dir: PathBuf, pub(crate) base_sha: String, } pub(crate) struct ProjectFixture { pub(crate) project_dir: PathBuf, - pub(crate) fabro_root: PathBuf, + pub(crate) fabro_root: PathBuf, } pub(crate) struct WorkspaceRunSetup { - pub(crate) run: RunSetup, + pub(crate) run: RunSetup, pub(crate) workspace_dir: PathBuf, } @@ -148,10 +148,10 @@ fn run_completed_dry_run(context: &TestContext, workflow: &Path) -> RunSetup { run_dir: context.find_run_dir(&run_id), run_id, }; - wait_for_event_names(&run_setup.run_dir, &[ - "run.completed", - "sandbox.cleanup.completed", - ]); + wait_for_event_names( + &run_setup.run_dir, + &["run.completed", "sandbox.cleanup.completed"], + ); run_setup } @@ -719,11 +719,10 @@ pub(crate) fn metadata_run_ids(repo_dir: &Path) -> BTreeSet { } pub(crate) fn run_branch_commits(repo_dir: &Path, run_id: &str) -> Vec { - git_stdout(repo_dir, &[ - "rev-list", - "--reverse", - &format!("fabro/run/{run_id}"), - ]) + git_stdout( + repo_dir, + &["rev-list", "--reverse", &format!("fabro/run/{run_id}")], + ) .lines() .map(str::trim) .filter(|line| !line.is_empty()) @@ -736,11 +735,14 @@ pub(crate) fn run_branch_commits_since_base( run_id: &str, base_sha: &str, ) -> Vec { - git_stdout(repo_dir, &[ - "rev-list", - "--reverse", - &format!("{base_sha}..fabro/run/{run_id}"), - ]) + git_stdout( + repo_dir, + &[ + "rev-list", + "--reverse", + &format!("{base_sha}..fabro/run/{run_id}"), + ], + ) .lines() .map(str::trim) .filter(|line| !line.is_empty()) @@ -928,9 +930,11 @@ fn setup_git_backed_run(context: &TestContext, workflow: GitWorkflowKind) -> Git git_success(&repo_dir, &["config", "user.email", "test@example.com"]); write_text_file(&repo_dir.join("story.txt"), "line 1\n"); - write_text_file(&repo_dir.join("flow.fabro"), match workflow { - GitWorkflowKind::Changed => { - r#"digraph Flow { + write_text_file( + &repo_dir.join("flow.fabro"), + match workflow { + GitWorkflowKind::Changed => { + r#"digraph Flow { graph [goal="Edit a tracked file"]; start [shape=Mdiamond]; exit [shape=Msquare]; @@ -939,9 +943,9 @@ fn setup_git_backed_run(context: &TestContext, workflow: GitWorkflowKind) -> Git start -> step_one -> step_two -> exit; } "# - } - GitWorkflowKind::Noop => { - r#"digraph Flow { + } + GitWorkflowKind::Noop => { + r#"digraph Flow { graph [goal="Leave tracked files unchanged"]; start [shape=Mdiamond]; exit [shape=Msquare]; @@ -949,8 +953,9 @@ fn setup_git_backed_run(context: &TestContext, workflow: GitWorkflowKind) -> Git start -> check -> exit; } "# - } - }); + } + }, + ); git_success(&repo_dir, &["add", "story.txt", "flow.fabro"]); git_success(&repo_dir, &["commit", "-qm", "init"]); diff --git a/lib/crates/fabro-config/src/effective_settings.rs b/lib/crates/fabro-config/src/effective_settings.rs index 46fd13ee9..aed8cefb4 100644 --- a/lib/crates/fabro-config/src/effective_settings.rs +++ b/lib/crates/fabro-config/src/effective_settings.rs @@ -25,10 +25,10 @@ pub enum EffectiveSettingsMode { #[derive(Clone, Debug, Default)] pub struct EffectiveSettingsLayers { - pub args: SettingsLayer, + pub args: SettingsLayer, pub workflow: SettingsLayer, - pub project: SettingsLayer, - pub user: SettingsLayer, + pub project: SettingsLayer, + pub user: SettingsLayer, } impl EffectiveSettingsLayers { diff --git a/lib/crates/fabro-config/src/error.rs b/lib/crates/fabro-config/src/error.rs index ead3042ad..6061c8d62 100644 --- a/lib/crates/fabro-config/src/error.rs +++ b/lib/crates/fabro-config/src/error.rs @@ -24,7 +24,7 @@ fn format_path_suffix(path: Option<&PathBuf>) -> String { pub enum Error { #[error("reading config file {path}: {source}")] ReadFile { - path: PathBuf, + path: PathBuf, #[source] source: std::io::Error, }, @@ -32,14 +32,14 @@ pub enum Error { #[error("{context}{}: {source}", format_path_suffix(.path.as_ref()))] ParseSettings { context: &'static str, - path: Option, + path: Option, #[source] - source: ParseError, + source: ParseError, }, #[error("parsing TOML config at {path}: {source}")] TomlParse { - path: PathBuf, + path: PathBuf, #[source] source: TomlError, }, @@ -47,13 +47,13 @@ pub enum Error { #[error("{context}:\n{}", format_resolve_errors(.errors))] Resolve { context: &'static str, - errors: Vec, + errors: Vec, }, #[error("missing required environment variable {var} for {field}")] MissingEnvVar { - field: String, - var: String, + field: String, + var: String, #[source] source: std::env::VarError, }, diff --git a/lib/crates/fabro-config/src/merge.rs b/lib/crates/fabro-config/src/merge.rs index 9ff89feef..cc8a4e11f 100644 --- a/lib/crates/fabro-config/src/merge.rs +++ b/lib/crates/fabro-config/src/merge.rs @@ -31,12 +31,12 @@ use fabro_types::settings::workflow::WorkflowLayer; #[must_use] pub fn combine_files(lower: SettingsLayer, higher: SettingsLayer) -> SettingsLayer { SettingsLayer { - version: higher.version.or(lower.version), - project: merge_option(lower.project, higher.project, combine_project), + version: higher.version.or(lower.version), + project: merge_option(lower.project, higher.project, combine_project), workflow: merge_option(lower.workflow, higher.workflow, combine_workflow), - run: merge_option(lower.run, higher.run, combine_run), - cli: merge_option(lower.cli, higher.cli, combine_cli), - server: merge_option(lower.server, higher.server, combine_server), + run: merge_option(lower.run, higher.run, combine_run), + cli: merge_option(lower.cli, higher.cli, combine_cli), + server: merge_option(lower.server, higher.server, combine_server), features: replace_if_some(lower.features, higher.features), } } @@ -75,10 +75,10 @@ fn merge_string_map_sticky( fn combine_project(lower: ProjectLayer, higher: ProjectLayer) -> ProjectLayer { ProjectLayer { - name: higher.name.or(lower.name), + name: higher.name.or(lower.name), description: higher.description.or(lower.description), - directory: higher.directory.or(lower.directory), - metadata: merge_string_map_replace(lower.metadata, higher.metadata), + directory: higher.directory.or(lower.directory), + metadata: merge_string_map_replace(lower.metadata, higher.metadata), } } @@ -86,10 +86,10 @@ fn combine_project(lower: ProjectLayer, higher: ProjectLayer) -> ProjectLayer { fn combine_workflow(lower: WorkflowLayer, higher: WorkflowLayer) -> WorkflowLayer { WorkflowLayer { - name: higher.name.or(lower.name), + name: higher.name.or(lower.name), description: higher.description.or(lower.description), - graph: higher.graph.or(lower.graph), - metadata: merge_string_map_replace(lower.metadata, higher.metadata), + graph: higher.graph.or(lower.graph), + metadata: merge_string_map_replace(lower.metadata, higher.metadata), } } @@ -97,30 +97,30 @@ fn combine_workflow(lower: WorkflowLayer, higher: WorkflowLayer) -> WorkflowLaye fn combine_run(lower: RunLayer, higher: RunLayer) -> RunLayer { RunLayer { - goal: higher.goal.or(lower.goal), - working_dir: higher.working_dir.or(lower.working_dir), - metadata: merge_string_map_replace(lower.metadata, higher.metadata), - inputs: higher.inputs.or(lower.inputs), - model: merge_option(lower.model, higher.model, combine_run_model), - git: merge_option(lower.git, higher.git, combine_run_git), - prepare: merge_option(lower.prepare, higher.prepare, combine_run_prepare), - execution: merge_option(lower.execution, higher.execution, combine_run_execution), - checkpoint: merge_option(lower.checkpoint, higher.checkpoint, combine_run_checkpoint), - sandbox: merge_option(lower.sandbox, higher.sandbox, combine_run_sandbox), + goal: higher.goal.or(lower.goal), + working_dir: higher.working_dir.or(lower.working_dir), + metadata: merge_string_map_replace(lower.metadata, higher.metadata), + inputs: higher.inputs.or(lower.inputs), + model: merge_option(lower.model, higher.model, combine_run_model), + git: merge_option(lower.git, higher.git, combine_run_git), + prepare: merge_option(lower.prepare, higher.prepare, combine_run_prepare), + execution: merge_option(lower.execution, higher.execution, combine_run_execution), + checkpoint: merge_option(lower.checkpoint, higher.checkpoint, combine_run_checkpoint), + sandbox: merge_option(lower.sandbox, higher.sandbox, combine_run_sandbox), notifications: combine_notifications(lower.notifications, higher.notifications), - interviews: merge_option(lower.interviews, higher.interviews, combine_interviews), - agent: merge_option(lower.agent, higher.agent, combine_run_agent), - hooks: combine_hooks(lower.hooks, higher.hooks), - scm: merge_option(lower.scm, higher.scm, combine_run_scm), - pull_request: merge_option(lower.pull_request, higher.pull_request, combine_run_pr), - artifacts: replace_if_some(lower.artifacts, higher.artifacts), + interviews: merge_option(lower.interviews, higher.interviews, combine_interviews), + agent: merge_option(lower.agent, higher.agent, combine_run_agent), + hooks: combine_hooks(lower.hooks, higher.hooks), + scm: merge_option(lower.scm, higher.scm, combine_run_scm), + pull_request: merge_option(lower.pull_request, higher.pull_request, combine_run_pr), + artifacts: replace_if_some(lower.artifacts, higher.artifacts), } } fn combine_run_model(lower: RunModelLayer, higher: RunModelLayer) -> RunModelLayer { RunModelLayer { - provider: higher.provider.or(lower.provider), - name: higher.name.or(lower.name), + provider: higher.provider.or(lower.provider), + name: higher.name.or(lower.name), fallbacks: splice_model_fallbacks(lower.fallbacks, higher.fallbacks), } } @@ -162,7 +162,7 @@ fn combine_run_git(lower: RunGitLayer, higher: RunGitLayer) -> RunGitLayer { fn combine_git_author(lower: GitAuthorLayer, higher: GitAuthorLayer) -> GitAuthorLayer { GitAuthorLayer { - name: higher.name.or(lower.name), + name: higher.name.or(lower.name), email: higher.email.or(lower.email), } } @@ -174,9 +174,9 @@ fn combine_run_prepare(_lower: RunPrepareLayer, higher: RunPrepareLayer) -> RunP fn combine_run_execution(lower: RunExecutionLayer, higher: RunExecutionLayer) -> RunExecutionLayer { RunExecutionLayer { - mode: higher.mode.or(lower.mode), + mode: higher.mode.or(lower.mode), approval: higher.approval.or(lower.approval), - retros: higher.retros.or(lower.retros), + retros: higher.retros.or(lower.retros), } } @@ -194,13 +194,13 @@ fn combine_run_checkpoint( fn combine_run_sandbox(lower: RunSandboxLayer, higher: RunSandboxLayer) -> RunSandboxLayer { RunSandboxLayer { - provider: higher.provider.or(lower.provider), - preserve: higher.preserve.or(lower.preserve), + provider: higher.provider.or(lower.provider), + preserve: higher.preserve.or(lower.preserve), devcontainer: higher.devcontainer.or(lower.devcontainer), // Sticky merge-by-key for run.sandbox.env per R71. - env: merge_string_map_sticky(lower.env, higher.env), - local: higher.local.or(lower.local), - daytona: merge_option(lower.daytona, higher.daytona, combine_daytona), + env: merge_string_map_sticky(lower.env, higher.env), + local: higher.local.or(lower.local), + daytona: merge_option(lower.daytona, higher.daytona, combine_daytona), } } @@ -208,10 +208,10 @@ fn combine_daytona(lower: DaytonaSandboxLayer, higher: DaytonaSandboxLayer) -> D DaytonaSandboxLayer { auto_stop_interval: higher.auto_stop_interval.or(lower.auto_stop_interval), // Sticky merge-by-key for provider-native labels per R71. - labels: merge_string_map_sticky(lower.labels, higher.labels), - snapshot: higher.snapshot.or(lower.snapshot), - network: higher.network.or(lower.network), - skip_clone: higher.skip_clone.or(lower.skip_clone), + labels: merge_string_map_sticky(lower.labels, higher.labels), + snapshot: higher.snapshot.or(lower.snapshot), + network: higher.network.or(lower.network), + skip_clone: higher.skip_clone.or(lower.skip_clone), } } @@ -237,12 +237,12 @@ fn combine_notification_route( higher: NotificationRouteLayer, ) -> NotificationRouteLayer { NotificationRouteLayer { - enabled: higher.enabled.or(lower.enabled), + enabled: higher.enabled.or(lower.enabled), provider: higher.provider.or(lower.provider), - events: splice_events(lower.events, higher.events), - slack: higher.slack.or(lower.slack), - discord: higher.discord.or(lower.discord), - teams: higher.teams.or(lower.teams), + events: splice_events(lower.events, higher.events), + slack: higher.slack.or(lower.slack), + discord: higher.discord.or(lower.discord), + teams: higher.teams.or(lower.teams), } } @@ -275,9 +275,9 @@ fn splice_events(lower: Vec, higher: Vec) -> Vec fn combine_interviews(lower: InterviewsLayer, higher: InterviewsLayer) -> InterviewsLayer { InterviewsLayer { provider: higher.provider.or(lower.provider), - slack: higher.slack.or(lower.slack), - discord: higher.discord.or(lower.discord), - teams: higher.teams.or(lower.teams), + slack: higher.slack.or(lower.slack), + discord: higher.discord.or(lower.discord), + teams: higher.teams.or(lower.teams), } } @@ -285,7 +285,7 @@ fn combine_run_agent(lower: RunAgentLayer, higher: RunAgentLayer) -> RunAgentLay RunAgentLayer { permissions: higher.permissions.or(lower.permissions), // MCP entries: field-merge per key. Higher replaces lower for same keys. - mcps: merge_string_map_sticky(lower.mcps, higher.mcps), + mcps: merge_string_map_sticky(lower.mcps, higher.mcps), } } @@ -320,18 +320,18 @@ fn combine_hooks(lower: Vec, higher: Vec) -> Vec RunScmLayer { RunScmLayer { - provider: higher.provider.or(lower.provider), - owner: higher.owner.or(lower.owner), + provider: higher.provider.or(lower.provider), + owner: higher.owner.or(lower.owner), repository: higher.repository.or(lower.repository), - github: higher.github.or(lower.github), + github: higher.github.or(lower.github), } } fn combine_run_pr(lower: RunPullRequestLayer, higher: RunPullRequestLayer) -> RunPullRequestLayer { RunPullRequestLayer { - enabled: higher.enabled.or(lower.enabled), - draft: higher.draft.or(lower.draft), - auto_merge: higher.auto_merge.or(lower.auto_merge), + enabled: higher.enabled.or(lower.enabled), + draft: higher.draft.or(lower.draft), + auto_merge: higher.auto_merge.or(lower.auto_merge), merge_strategy: higher.merge_strategy.or(lower.merge_strategy), } } @@ -340,10 +340,10 @@ fn combine_run_pr(lower: RunPullRequestLayer, higher: RunPullRequestLayer) -> Ru fn combine_cli(lower: CliLayer, higher: CliLayer) -> CliLayer { CliLayer { - target: merge_option(lower.target, higher.target, combine_cli_target), - auth: higher.auth.or(lower.auth), - exec: merge_option(lower.exec, higher.exec, combine_cli_exec), - output: higher.output.or(lower.output), + target: merge_option(lower.target, higher.target, combine_cli_target), + auth: higher.auth.or(lower.auth), + exec: merge_option(lower.exec, higher.exec, combine_cli_exec), + output: higher.output.or(lower.output), updates: higher.updates.or(lower.updates), logging: higher.logging.or(lower.logging), } @@ -357,8 +357,8 @@ fn combine_cli_target(_lower: CliTargetLayer, higher: CliTargetLayer) -> CliTarg fn combine_cli_exec(lower: CliExecLayer, higher: CliExecLayer) -> CliExecLayer { CliExecLayer { prevent_idle_sleep: higher.prevent_idle_sleep.or(lower.prevent_idle_sleep), - model: merge_option(lower.model, higher.model, combine_cli_exec_model), - agent: merge_option(lower.agent, higher.agent, combine_cli_exec_agent), + model: merge_option(lower.model, higher.model, combine_cli_exec_model), + agent: merge_option(lower.agent, higher.agent, combine_cli_exec_agent), } } @@ -368,7 +368,7 @@ fn combine_cli_exec_model( ) -> CliExecModelLayer { CliExecModelLayer { provider: higher.provider.or(lower.provider), - name: higher.name.or(lower.name), + name: higher.name.or(lower.name), } } @@ -378,7 +378,7 @@ fn combine_cli_exec_agent( ) -> CliExecAgentLayer { CliExecAgentLayer { permissions: higher.permissions.or(lower.permissions), - mcps: merge_string_map_sticky(lower.mcps, higher.mcps), + mcps: merge_string_map_sticky(lower.mcps, higher.mcps), } } @@ -386,15 +386,15 @@ fn combine_cli_exec_agent( fn combine_server(lower: ServerLayer, higher: ServerLayer) -> ServerLayer { ServerLayer { - listen: merge_option(lower.listen, higher.listen, combine_listen), - api: higher.api.or(lower.api), - web: merge_option(lower.web, higher.web, combine_server_web), - auth: merge_option(lower.auth, higher.auth, combine_server_auth), - storage: merge_option(lower.storage, higher.storage, combine_server_storage), - artifacts: merge_option(lower.artifacts, higher.artifacts, combine_server_artifacts), - slatedb: merge_option(lower.slatedb, higher.slatedb, combine_server_slatedb), - scheduler: merge_option(lower.scheduler, higher.scheduler, combine_server_scheduler), - logging: higher.logging.or(lower.logging), + listen: merge_option(lower.listen, higher.listen, combine_listen), + api: higher.api.or(lower.api), + web: merge_option(lower.web, higher.web, combine_server_web), + auth: merge_option(lower.auth, higher.auth, combine_server_auth), + storage: merge_option(lower.storage, higher.storage, combine_server_storage), + artifacts: merge_option(lower.artifacts, higher.artifacts, combine_server_artifacts), + slatedb: merge_option(lower.slatedb, higher.slatedb, combine_server_slatedb), + scheduler: merge_option(lower.scheduler, higher.scheduler, combine_server_scheduler), + logging: higher.logging.or(lower.logging), integrations: merge_option( lower.integrations, higher.integrations, @@ -411,7 +411,7 @@ fn combine_listen(_lower: ServerListenLayer, higher: ServerListenLayer) -> Serve fn combine_server_web(lower: ServerWebLayer, higher: ServerWebLayer) -> ServerWebLayer { ServerWebLayer { enabled: higher.enabled.or(lower.enabled), - url: higher.url.or(lower.url), + url: higher.url.or(lower.url), } } @@ -437,9 +437,9 @@ fn combine_server_artifacts( ) -> ServerArtifactsLayer { ServerArtifactsLayer { provider: higher.provider.or(lower.provider), - prefix: higher.prefix.or(lower.prefix), - local: higher.local.or(lower.local), - s3: higher.s3.or(lower.s3), + prefix: higher.prefix.or(lower.prefix), + local: higher.local.or(lower.local), + s3: higher.s3.or(lower.s3), } } @@ -448,11 +448,11 @@ fn combine_server_slatedb( higher: ServerSlateDbLayer, ) -> ServerSlateDbLayer { ServerSlateDbLayer { - provider: higher.provider.or(lower.provider), - prefix: higher.prefix.or(lower.prefix), + provider: higher.provider.or(lower.provider), + prefix: higher.prefix.or(lower.prefix), flush_interval: higher.flush_interval.or(lower.flush_interval), - local: higher.local.or(lower.local), - s3: higher.s3.or(lower.s3), + local: higher.local.or(lower.local), + s3: higher.s3.or(lower.s3), } } @@ -470,10 +470,10 @@ fn combine_server_integrations( higher: ServerIntegrationsLayer, ) -> ServerIntegrationsLayer { ServerIntegrationsLayer { - github: higher.github.or(lower.github), - slack: higher.slack.or(lower.slack), + github: higher.github.or(lower.github), + slack: higher.slack.or(lower.slack), discord: higher.discord.or(lower.discord), - teams: higher.teams.or(lower.teams), + teams: higher.teams.or(lower.teams), } } diff --git a/lib/crates/fabro-config/src/parse.rs b/lib/crates/fabro-config/src/parse.rs index 219c04939..ea975708b 100644 --- a/lib/crates/fabro-config/src/parse.rs +++ b/lib/crates/fabro-config/src/parse.rs @@ -66,7 +66,7 @@ pub fn parse_settings_layer(input: &str) -> Result { for key in table.keys() { if !ALLOWED_TOP_LEVEL_KEYS.contains(&key.as_str()) { return Err(ParseError::UnknownTopLevelKey { - key: key.clone(), + key: key.clone(), hint: rename_hint(key), }); } diff --git a/lib/crates/fabro-config/src/project.rs b/lib/crates/fabro-config/src/project.rs index 087fd619e..862d8ad3c 100644 --- a/lib/crates/fabro-config/src/project.rs +++ b/lib/crates/fabro-config/src/project.rs @@ -21,10 +21,10 @@ const CONFIG_FILENAME: &str = ".fabro/project.toml"; #[derive(Clone, Debug)] pub struct WorkflowPathResolution { pub resolved_workflow_path: PathBuf, - pub dot_path: PathBuf, - pub workflow_config: Option, - pub workflow_toml_path: Option, - pub workflow_slug: Option, + pub dot_path: PathBuf, + pub workflow_config: Option, + pub workflow_toml_path: Option, + pub workflow_slug: Option, } /// Parse a project config from a TOML string. @@ -222,8 +222,8 @@ fn user_workflows_dir() -> PathBuf { /// Metadata about a discovered workflow. #[derive(Clone, Debug, Serialize)] pub struct WorkflowInfo { - pub name: String, - pub goal: Option, + pub name: String, + pub goal: Option, pub source: WorkflowSource, } diff --git a/lib/crates/fabro-config/src/resolve/cli.rs b/lib/crates/fabro-config/src/resolve/cli.rs index f588c4fb3..b2ba0decf 100644 --- a/lib/crates/fabro-config/src/resolve/cli.rs +++ b/lib/crates/fabro-config/src/resolve/cli.rs @@ -8,13 +8,13 @@ use super::{ResolveError, require_interp}; pub fn resolve_cli(layer: &CliLayer, errors: &mut Vec) -> CliSettings { CliSettings { - target: resolve_target(layer.target.as_ref(), errors), - auth: CliAuthSettings { + target: resolve_target(layer.target.as_ref(), errors), + auth: CliAuthSettings { strategy: layer.auth.as_ref().and_then(|auth| auth.strategy), }, - exec: resolve_exec(layer.exec.as_ref()), - output: CliOutputSettings { - format: layer + exec: resolve_exec(layer.exec.as_ref()), + output: CliOutputSettings { + format: layer .output .as_ref() .and_then(|output| output.format) @@ -50,8 +50,8 @@ fn resolve_target( url: require_interp(url.as_ref(), "cli.target.url", errors), tls: tls.as_ref().map(|tls| CliTargetTlsSettings { cert: require_interp(tls.cert.as_ref(), "cli.target.tls.cert", errors), - key: require_interp(tls.key.as_ref(), "cli.target.tls.key", errors), - ca: require_interp(tls.ca.as_ref(), "cli.target.tls.ca", errors), + key: require_interp(tls.key.as_ref(), "cli.target.tls.key", errors), + ca: require_interp(tls.ca.as_ref(), "cli.target.tls.ca", errors), }), }), Some(CliTargetLayer::Unix { path }) => Some(CliTargetSettings::Unix { @@ -68,13 +68,13 @@ fn resolve_exec(exec: Option<&CliExecLayer>) -> CliExecSettings { CliExecSettings { prevent_idle_sleep: exec.prevent_idle_sleep.unwrap_or(false), - model: CliExecModelSettings { + model: CliExecModelSettings { provider: exec.model.as_ref().and_then(|model| model.provider.clone()), - name: exec.model.as_ref().and_then(|model| model.name.clone()), + name: exec.model.as_ref().and_then(|model| model.name.clone()), }, - agent: CliExecAgentSettings { + agent: CliExecAgentSettings { permissions: exec.agent.as_ref().and_then(|agent| agent.permissions), - mcps: exec + mcps: exec .agent .as_ref() .map(|agent| { diff --git a/lib/crates/fabro-config/src/resolve/mod.rs b/lib/crates/fabro-config/src/resolve/mod.rs index 2698134b5..6f27cbe23 100644 --- a/lib/crates/fabro-config/src/resolve/mod.rs +++ b/lib/crates/fabro-config/src/resolve/mod.rs @@ -28,11 +28,11 @@ pub fn resolve(file: &SettingsLayer) -> Result> { let features_layer = file.features.clone().unwrap_or_default(); let settings = Settings { - project: resolve_project(&project_layer, &mut errors), + project: resolve_project(&project_layer, &mut errors), workflow: resolve_workflow(&workflow_layer, &mut errors), - run: resolve_run(&run_layer, &mut errors), - cli: resolve_cli(&cli_layer, &mut errors), - server: resolve_server(&server_layer, &mut errors), + run: resolve_run(&run_layer, &mut errors), + cli: resolve_cli(&cli_layer, &mut errors), + server: resolve_server(&server_layer, &mut errors), features: resolve_features(&features_layer, &mut errors), }; @@ -96,7 +96,7 @@ pub(crate) fn parse_socket_addr( Ok(address) => address, Err(err) => { errors.push(ResolveError::ParseFailure { - path: path.to_string(), + path: path.to_string(), reason: err.to_string(), }); std::net::SocketAddr::from(([127, 0, 0, 1], 0)) diff --git a/lib/crates/fabro-config/src/resolve/project.rs b/lib/crates/fabro-config/src/resolve/project.rs index 16d5db288..7c4f563bd 100644 --- a/lib/crates/fabro-config/src/resolve/project.rs +++ b/lib/crates/fabro-config/src/resolve/project.rs @@ -6,12 +6,12 @@ const DEFAULT_PROJECT_DIRECTORY: &str = "."; pub fn resolve_project(layer: &ProjectLayer, _errors: &mut Vec) -> ProjectSettings { ProjectSettings { - name: layer.name.clone(), + name: layer.name.clone(), description: layer.description.clone(), - directory: layer + directory: layer .directory .clone() .unwrap_or_else(|| DEFAULT_PROJECT_DIRECTORY.to_string()), - metadata: layer.metadata.clone(), + metadata: layer.metadata.clone(), } } diff --git a/lib/crates/fabro-config/src/resolve/run.rs b/lib/crates/fabro-config/src/resolve/run.rs index ed1c06265..d228b69fe 100644 --- a/lib/crates/fabro-config/src/resolve/run.rs +++ b/lib/crates/fabro-config/src/resolve/run.rs @@ -17,32 +17,32 @@ use super::ResolveError; pub fn resolve_run(layer: &RunLayer, errors: &mut Vec) -> RunSettings { RunSettings { - goal: resolve_goal(layer.goal.as_ref()), - working_dir: layer.working_dir.clone(), - metadata: layer.metadata.clone(), - inputs: layer.inputs.clone().unwrap_or_default(), - model: resolve_model(layer.model.as_ref()), - git: resolve_git(layer.git.as_ref()), - prepare: resolve_prepare(layer.prepare.as_ref(), errors), - execution: resolve_execution(layer.execution.as_ref()), - checkpoint: resolve_checkpoint(layer.checkpoint.as_ref()), - sandbox: resolve_sandbox(layer.sandbox.as_ref(), errors), + goal: resolve_goal(layer.goal.as_ref()), + working_dir: layer.working_dir.clone(), + metadata: layer.metadata.clone(), + inputs: layer.inputs.clone().unwrap_or_default(), + model: resolve_model(layer.model.as_ref()), + git: resolve_git(layer.git.as_ref()), + prepare: resolve_prepare(layer.prepare.as_ref(), errors), + execution: resolve_execution(layer.execution.as_ref()), + checkpoint: resolve_checkpoint(layer.checkpoint.as_ref()), + sandbox: resolve_sandbox(layer.sandbox.as_ref(), errors), notifications: layer .notifications .iter() .map(|(name, route)| (name.clone(), resolve_notification_route(route))) .collect(), - interviews: resolve_interviews(layer.interviews.as_ref()), - agent: resolve_agent(layer.agent.as_ref()), - hooks: layer + interviews: resolve_interviews(layer.interviews.as_ref()), + agent: resolve_agent(layer.agent.as_ref()), + hooks: layer .hooks .iter() .enumerate() .map(|(index, hook)| resolve_hook(hook, index, errors)) .collect(), - scm: resolve_scm(layer.scm.as_ref()), - pull_request: resolve_pull_request(layer.pull_request.as_ref()), - artifacts: resolve_artifacts(layer.artifacts.as_ref()), + scm: resolve_scm(layer.scm.as_ref()), + pull_request: resolve_pull_request(layer.pull_request.as_ref()), + artifacts: resolve_artifacts(layer.artifacts.as_ref()), } } @@ -59,8 +59,8 @@ fn resolve_model(model: Option<&RunModelLayer>) -> RunModelSettings { }; RunModelSettings { - provider: model.provider.clone(), - name: model.name.clone(), + provider: model.provider.clone(), + name: model.name.clone(), fallbacks: model .fallbacks .iter() @@ -76,7 +76,7 @@ fn resolve_git(git: Option<&RunGitLayer>) -> RunGitSettings { RunGitSettings { author: git.and_then(|git| { git.author.as_ref().map(|author| GitAuthorSettings { - name: author.name.clone(), + name: author.name.clone(), email: author.email.clone(), }) }), @@ -102,7 +102,7 @@ fn resolve_prepare( .join(" "), ), (Some(_), Some(_)) | (None, None) => errors.push(ResolveError::Invalid { - path: format!("run.prepare.steps[{index}]"), + path: format!("run.prepare.steps[{index}]"), reason: "exactly one of script or command must be set".to_string(), }), } @@ -122,9 +122,9 @@ fn resolve_execution(execution: Option<&RunExecutionLayer>) -> RunExecutionSetti }; RunExecutionSettings { - mode: execution.mode.unwrap_or(RunMode::Normal), + mode: execution.mode.unwrap_or(RunMode::Normal), approval: execution.approval.unwrap_or(ApprovalMode::Prompt), - retros: execution.retros.unwrap_or(true), + retros: execution.retros.unwrap_or(true), } } @@ -151,7 +151,7 @@ fn resolve_sandbox( match provider.as_str() { "local" | "docker" | "daytona" => {} other => errors.push(ResolveError::Invalid { - path: "run.sandbox.provider".to_string(), + path: "run.sandbox.provider".to_string(), reason: format!("unknown sandbox provider: {other}"), }), } @@ -179,13 +179,13 @@ fn resolve_local_sandbox(sandbox: &RunSandboxLayer) -> LocalSandboxSettings { fn resolve_daytona(daytona: &DaytonaSandboxLayer) -> DaytonaSettings { DaytonaSettings { auto_stop_interval: daytona.auto_stop_interval, - labels: daytona.labels.clone(), - snapshot: daytona.snapshot.as_ref().and_then(|snapshot| { + labels: daytona.labels.clone(), + snapshot: daytona.snapshot.as_ref().and_then(|snapshot| { snapshot.name.as_ref().map(|name| DaytonaSnapshotSettings { - name: name.clone(), - cpu: snapshot.cpu, - memory_gb: snapshot.memory.map(|size| size_to_gb_i32(size.as_bytes())), - disk_gb: snapshot.disk.map(|size| size_to_gb_i32(size.as_bytes())), + name: name.clone(), + cpu: snapshot.cpu, + memory_gb: snapshot.memory.map(|size| size_to_gb_i32(size.as_bytes())), + disk_gb: snapshot.disk.map(|size| size_to_gb_i32(size.as_bytes())), dockerfile: snapshot .dockerfile .as_ref() @@ -199,16 +199,16 @@ fn resolve_daytona(daytona: &DaytonaSandboxLayer) -> DaytonaSettings { }), }) }), - network: daytona.network.clone(), - skip_clone: daytona.skip_clone.unwrap_or(false), + network: daytona.network.clone(), + skip_clone: daytona.skip_clone.unwrap_or(false), } } fn resolve_notification_route(route: &NotificationRouteLayer) -> NotificationRouteSettings { NotificationRouteSettings { - enabled: route.enabled.unwrap_or(false), + enabled: route.enabled.unwrap_or(false), provider: route.provider.clone(), - events: route + events: route .events .iter() .filter_map(|event| match event { @@ -216,9 +216,9 @@ fn resolve_notification_route(route: &NotificationRouteLayer) -> NotificationRou StringOrSplice::Splice => None, }) .collect(), - slack: route.slack.as_ref().map(resolve_notification_provider), - discord: route.discord.as_ref().map(resolve_notification_provider), - teams: route.teams.as_ref().map(resolve_notification_provider), + slack: route.slack.as_ref().map(resolve_notification_provider), + discord: route.discord.as_ref().map(resolve_notification_provider), + teams: route.teams.as_ref().map(resolve_notification_provider), } } @@ -237,9 +237,9 @@ fn resolve_interviews(interviews: Option<&InterviewsLayer>) -> RunInterviewsSett RunInterviewsSettings { provider: interviews.provider.clone(), - slack: interviews.slack.as_ref().map(resolve_interview_provider), - discord: interviews.discord.as_ref().map(resolve_interview_provider), - teams: interviews.teams.as_ref().map(resolve_interview_provider), + slack: interviews.slack.as_ref().map(resolve_interview_provider), + discord: interviews.discord.as_ref().map(resolve_interview_provider), + teams: interviews.teams.as_ref().map(resolve_interview_provider), } } @@ -256,7 +256,7 @@ fn resolve_agent(agent: Option<&RunAgentLayer>) -> RunAgentSettings { RunAgentSettings { permissions: agent.permissions, - mcps: agent + mcps: agent .mcps .iter() .map(|(name, entry)| (name.clone(), resolve_mcp_entry(name, entry))) @@ -273,13 +273,13 @@ pub(crate) fn resolve_mcp_entry(name: &str, entry: &McpEntryLayer) -> McpServerS .. } => McpTransport::Stdio { command: resolve_mcp_command(script.as_ref(), command.as_ref()), - env: env + env: env .iter() .map(|(key, value)| (key.clone(), value.as_source())) .collect(), }, McpEntryLayer::Http { url, headers, .. } => McpTransport::Http { - url: url.as_source(), + url: url.as_source(), headers: headers .iter() .map(|(key, value)| (key.clone(), value.as_source())) @@ -293,8 +293,8 @@ pub(crate) fn resolve_mcp_entry(name: &str, entry: &McpEntryLayer) -> McpServerS .. } => McpTransport::Sandbox { command: resolve_mcp_command(script.as_ref(), command.as_ref()), - port: *port, - env: env + port: *port, + env: env .iter() .map(|(key, value)| (key.clone(), value.as_source())) .collect(), @@ -355,7 +355,7 @@ fn resolve_hook(hook: &HookEntry, index: usize, errors: &mut Vec) if variants != 1 { errors.push(ResolveError::Invalid { - path: format!("run.hooks[{index}]"), + path: format!("run.hooks[{index}]"), reason: "exactly one hook transport must be configured".to_string(), }); } @@ -419,19 +419,19 @@ fn resolve_hook_type(hook: &HookEntry) -> Option { if hook.agent == Some(HookAgentMarker::Enabled) { return Some(HookType::Agent { - prompt: hook + prompt: hook .prompt .as_ref() .map(InterpString::as_source) .unwrap_or_default(), - model: hook.model.as_ref().map(InterpString::as_source), + model: hook.model.as_ref().map(InterpString::as_source), max_tool_rounds: hook.max_tool_rounds, }); } hook.prompt.as_ref().map(|prompt| HookType::Prompt { prompt: prompt.as_source(), - model: hook.model.as_ref().map(InterpString::as_source), + model: hook.model.as_ref().map(InterpString::as_source), }) } @@ -441,10 +441,10 @@ fn resolve_scm(scm: Option<&RunScmLayer>) -> RunScmSettings { }; RunScmSettings { - provider: scm.provider.clone(), - owner: scm.owner.clone(), + provider: scm.provider.clone(), + owner: scm.owner.clone(), repository: scm.repository.clone(), - github: scm.github.as_ref().map(|_| ScmGitHubSettings), + github: scm.github.as_ref().map(|_| ScmGitHubSettings), } } @@ -455,9 +455,9 @@ fn resolve_pull_request(pull_request: Option<&RunPullRequestLayer>) -> Option, layer: Option<&ServerWebLayer>) -> ServerWebSettings { ServerWebSettings { enabled: layer.and_then(|web| web.enabled).unwrap_or(true), - url: layer + url: layer .and_then(|web| web.url.clone()) .unwrap_or_else(|| InterpString::parse("http://localhost:3000")), } @@ -125,20 +125,20 @@ fn resolve_auth( let jwt = api.and_then(|api| { api.jwt.as_ref().map(|jwt| ServerAuthApiJwtSettings { - enabled: jwt.enabled.unwrap_or(true), - issuer: jwt.issuer.clone(), + enabled: jwt.enabled.unwrap_or(true), + issuer: jwt.issuer.clone(), audience: jwt.audience.clone(), }) }); let mtls = api.and_then(|api| { api.mtls.as_ref().map(|mtls| ServerAuthApiMtlsSettings { enabled: mtls.enabled.unwrap_or(true), - ca: mtls.ca.clone(), + ca: mtls.ca.clone(), }) }); if mtls.as_ref().is_some_and(|mtls| mtls.enabled) && !valid_tls { errors.push(ResolveError::Invalid { - path: "server.auth.api.mtls".to_string(), + path: "server.auth.api.mtls".to_string(), reason: "requires tcp listen with tls cert, key, and ca configured".to_string(), }); } @@ -149,7 +149,7 @@ fn resolve_auth( allowed_usernames: web .map(|web| web.allowed_usernames.clone()) .unwrap_or_default(), - providers: ServerAuthWebProvidersSettings { + providers: ServerAuthWebProvidersSettings { github: web .and_then(|web| web.providers.as_ref()) .and_then(|providers| providers.github.as_ref()) @@ -161,8 +161,8 @@ fn resolve_auth( fn resolve_web_github(layer: &ServerAuthWebGithubLayer) -> GithubOauthSettings { GithubOauthSettings { - enabled: layer.enabled.unwrap_or(true), - client_id: layer.client_id.clone(), + enabled: layer.enabled.unwrap_or(true), + client_id: layer.client_id.clone(), client_secret: layer.client_secret.clone(), } } @@ -180,7 +180,7 @@ fn resolve_artifacts( prefix: layer .and_then(|artifacts| artifacts.prefix.clone()) .unwrap_or_else(|| InterpString::parse("artifacts")), - store: resolve_object_store( + store: resolve_object_store( provider, layer.and_then(|artifacts| artifacts.local.as_ref()), layer.and_then(|artifacts| artifacts.s3.as_ref()), @@ -201,10 +201,10 @@ fn resolve_slatedb( .unwrap_or(ObjectStoreProvider::Local); ServerSlateDbSettings { - prefix: layer + prefix: layer .and_then(|slatedb| slatedb.prefix.clone()) .unwrap_or_else(|| InterpString::parse("")), - store: resolve_object_store( + store: resolve_object_store( provider, layer.and_then(|slatedb| slatedb.local.as_ref()), layer.and_then(|slatedb| slatedb.s3.as_ref()), @@ -255,15 +255,15 @@ fn resolve_object_store( fn resolve_integrations(layer: Option<&ServerIntegrationsLayer>) -> ServerIntegrationsSettings { ServerIntegrationsSettings { - github: layer + github: layer .and_then(|integrations| integrations.github.as_ref()) .map(|github| GithubIntegrationSettings { - enabled: github.enabled.unwrap_or(true), - app_id: github.app_id.clone(), - client_id: github.client_id.clone(), - slug: github.slug.clone(), + enabled: github.enabled.unwrap_or(true), + app_id: github.app_id.clone(), + client_id: github.client_id.clone(), + slug: github.slug.clone(), permissions: github.permissions.clone(), - webhooks: github + webhooks: github .webhooks .as_ref() .map(|webhooks| IntegrationWebhooksSettings { @@ -271,10 +271,10 @@ fn resolve_integrations(layer: Option<&ServerIntegrationsLayer>) -> ServerIntegr }), }) .unwrap_or_default(), - slack: layer + slack: layer .and_then(|integrations| integrations.slack.as_ref()) .map(|slack| SlackIntegrationSettings { - enabled: slack.enabled.unwrap_or(true), + enabled: slack.enabled.unwrap_or(true), default_channel: slack.default_channel.clone(), }) .unwrap_or_default(), @@ -284,7 +284,7 @@ fn resolve_integrations(layer: Option<&ServerIntegrationsLayer>) -> ServerIntegr enabled: discord.enabled.unwrap_or(true), }) .unwrap_or_default(), - teams: layer + teams: layer .and_then(|integrations| integrations.teams.as_ref()) .map(|teams| TeamsIntegrationSettings { enabled: teams.enabled.unwrap_or(true), diff --git a/lib/crates/fabro-config/src/resolve/workflow.rs b/lib/crates/fabro-config/src/resolve/workflow.rs index 770c9a385..18e0603b7 100644 --- a/lib/crates/fabro-config/src/resolve/workflow.rs +++ b/lib/crates/fabro-config/src/resolve/workflow.rs @@ -9,12 +9,12 @@ pub fn resolve_workflow( _errors: &mut Vec, ) -> WorkflowSettings { WorkflowSettings { - name: layer.name.clone(), + name: layer.name.clone(), description: layer.description.clone(), - graph: layer + graph: layer .graph .clone() .unwrap_or_else(|| DEFAULT_WORKFLOW_GRAPH.to_string()), - metadata: layer.metadata.clone(), + metadata: layer.metadata.clone(), } } diff --git a/lib/crates/fabro-config/src/run.rs b/lib/crates/fabro-config/src/run.rs index e4e536913..d6a6d93af 100644 --- a/lib/crates/fabro-config/src/run.rs +++ b/lib/crates/fabro-config/src/run.rs @@ -43,7 +43,7 @@ pub enum ResolveRunGoalError { var: String, }, Io { - path: PathBuf, + path: PathBuf, source: std::io::Error, }, } @@ -81,7 +81,7 @@ pub fn resolve_run_goal( match goal { RunGoalLayer::Inline(text) => Ok(Some(ResolvedRunGoal { - text: text.as_source(), + text: text.as_source(), source: ResolvedGoalSource::Inline, })), RunGoalLayer::File { file } => { diff --git a/lib/crates/fabro-core/src/error.rs b/lib/crates/fabro-core/src/error.rs index 75f973f64..f1877a10e 100644 --- a/lib/crates/fabro-core/src/error.rs +++ b/lib/crates/fabro-core/src/error.rs @@ -22,9 +22,9 @@ impl fmt::Display for VisitLimitSource { /// to_fail_outcome(). #[derive(Debug, Clone)] pub struct HandlerErrorDetail { - pub message: String, + pub message: String, pub retryable: bool, - pub category: Option, + pub category: Option, pub signature: Option, } @@ -48,9 +48,9 @@ pub enum Error { "node \"{node_id}\" visited {visits} times ({limit_source} limit {limit}); run is stuck in a cycle" )] VisitLimitExceeded { - node_id: String, - visits: usize, - limit: usize, + node_id: String, + visits: usize, + limit: usize, limit_source: VisitLimitSource, }, #[error("stall timeout on node \"{node_id}\"")] @@ -81,8 +81,8 @@ impl Error { Self::Handler { detail } => Outcome { status: StageStatus::Fail, failure: Some(FailureDetail { - message: detail.message.clone(), - category: detail.category.unwrap_or(FailureCategory::Deterministic), + message: detail.message.clone(), + category: detail.category.unwrap_or(FailureCategory::Deterministic), signature: detail.signature.clone(), }), ..Outcome::default() @@ -93,7 +93,6 @@ impl Error { } pub type Result = std::result::Result; -pub type CoreError = Error; #[cfg(test)] mod tests { @@ -119,9 +118,9 @@ mod tests { ); assert_eq!( Error::VisitLimitExceeded { - node_id: "n1".into(), - visits: 5, - limit: 3, + node_id: "n1".into(), + visits: 5, + limit: 3, limit_source: VisitLimitSource::Node, } .to_string(), @@ -143,17 +142,17 @@ mod tests { #[test] fn core_error_handler_is_retryable() { let retryable = Error::handler(HandlerErrorDetail { - message: "timeout".into(), + message: "timeout".into(), retryable: true, - category: None, + category: None, signature: None, }); assert!(retryable.is_retryable()); let not_retryable = Error::handler(HandlerErrorDetail { - message: "bad input".into(), + message: "bad input".into(), retryable: false, - category: None, + category: None, signature: None, }); assert!(!not_retryable.is_retryable()); @@ -163,9 +162,9 @@ mod tests { fn core_error_handler_to_fail_outcome() { use crate::outcome::FailureCategory; let err = Error::handler(HandlerErrorDetail { - message: "api down".into(), + message: "api down".into(), retryable: true, - category: Some(FailureCategory::TransientInfra), + category: Some(FailureCategory::TransientInfra), signature: Some("sig123".into()), }); let outcome: Outcome = err.to_fail_outcome(); diff --git a/lib/crates/fabro-core/src/executor.rs b/lib/crates/fabro-core/src/executor.rs index 53e736525..13148ce21 100644 --- a/lib/crates/fabro-core/src/executor.rs +++ b/lib/crates/fabro-core/src/executor.rs @@ -6,7 +6,7 @@ use tokio::time::sleep; use tokio_util::sync::CancellationToken; use crate::context::Context; -use crate::error::{CoreError, Result, VisitLimitSource}; +use crate::error::{Error, Result, VisitLimitSource}; use crate::graph::{EdgeSpec, Graph, NodeSpec}; use crate::handler::NodeHandler; use crate::lifecycle::{ @@ -18,15 +18,15 @@ use crate::state::ExecutionState; #[derive(Default)] pub struct ExecutorOptions { - pub cancel_token: Option>, - pub stall_token: Option, + pub cancel_token: Option>, + pub stall_token: Option, pub max_node_visits: Option, } pub struct Executor { - handler: Arc>, + handler: Arc>, lifecycle: Box>, - options: ExecutorOptions, + options: ExecutorOptions, } enum NextStep { @@ -37,9 +37,9 @@ enum NextStep { } pub struct ExecutorBuilder { - handler: Arc>, + handler: Arc>, lifecycle: Option>>, - options: ExecutorOptions, + options: ExecutorOptions, } impl ExecutorBuilder { @@ -77,9 +77,9 @@ impl ExecutorBuilder { pub fn build(self) -> Executor { Executor { - handler: self.handler, + handler: self.handler, lifecycle: self.lifecycle.unwrap_or_else(|| Box::new(NoopLifecycle)), - options: self.options, + options: self.options, } } } @@ -99,13 +99,13 @@ impl Executor { state.cancelled = true; let outcome = Outcome::fail("run cancelled"); self.lifecycle.on_run_end(&outcome, &state).await; - return Err(CoreError::Cancelled); + return Err(Error::Cancelled); } } let node = state .current_node(graph) - .ok_or_else(|| CoreError::NodeNotFound { + .ok_or_else(|| Error::NodeNotFound { id: state.current_node_id.clone(), })?; @@ -154,7 +154,7 @@ impl Executor { let visits = state.increment_visits(node.id()); if let Some(max) = node.max_visits() { if visits >= max { - return Err(CoreError::VisitLimitExceeded { + return Err(Error::VisitLimitExceeded { node_id: node.id().to_string(), visits, limit: max, @@ -164,7 +164,7 @@ impl Executor { } if let Some(global_max) = self.options.max_node_visits { if visits >= global_max { - return Err(CoreError::VisitLimitExceeded { + return Err(Error::VisitLimitExceeded { node_id: node.id().to_string(), visits, limit: global_max, @@ -183,7 +183,7 @@ impl Executor { result } NodeDecision::Block(msg) => { - return Err(CoreError::blocked(msg)); + return Err(Error::blocked(msg)); } NodeDecision::Continue => { // Execute with retry, racing against stall token @@ -191,7 +191,7 @@ impl Executor { tokio::select! { r = self.execute_with_retry(&node, &state, graph) => r, () = stall.cancelled() => { - return Err(CoreError::StallTimeout { + return Err(Error::StallTimeout { node_id: node.id().to_string(), }); } @@ -201,11 +201,11 @@ impl Executor { }; let mut result = match execution_result { Ok(result) => result, - Err(CoreError::Cancelled) => { + Err(Error::Cancelled) => { state.cancelled = true; let outcome = Outcome::fail("run cancelled"); self.lifecycle.on_run_end(&outcome, &state).await; - return Err(CoreError::Cancelled); + return Err(Error::Cancelled); } Err(err) => return Err(err), }; @@ -278,7 +278,7 @@ impl Executor { }; match self.lifecycle.before_attempt(&attempt_ctx, state).await? { NodeDecision::Skip(o) => return Ok(NodeResult::from_skip(*o)), - NodeDecision::Block(msg) => return Err(CoreError::blocked(msg)), + NodeDecision::Block(msg) => return Err(Error::blocked(msg)), NodeDecision::Continue => {} } @@ -344,7 +344,7 @@ impl Executor { self.lifecycle.after_attempt(&ctx, state).await?; sleep(delay).await; } - Err(e @ CoreError::Handler { .. }) => { + Err(e @ Error::Handler { .. }) => { // Convert handler failures to fail outcomes so routing continues. let outcome = e.to_fail_outcome(); let result = @@ -385,7 +385,7 @@ impl Executor { match self.lifecycle.on_edge_selected(&ctx, state).await? { EdgeDecision::Continue => return Ok(NextStep::Jump(target.clone())), EdgeDecision::Override(new_target) => return Ok(NextStep::Edge(new_target)), - EdgeDecision::Block(msg) => return Err(CoreError::blocked(msg)), + EdgeDecision::Block(msg) => return Err(Error::blocked(msg)), } } @@ -411,7 +411,7 @@ impl Executor { } } EdgeDecision::Override(new_target) => Ok(NextStep::Edge(new_target)), - EdgeDecision::Block(msg) => Err(CoreError::blocked(msg)), + EdgeDecision::Block(msg) => Err(Error::blocked(msg)), } } else { // No edge found @@ -500,7 +500,7 @@ mod tests { .cancel_token(token) .build(); let result = executor.run(&g, state).await; - assert!(matches!(result, Err(CoreError::Cancelled))); + assert!(matches!(result, Err(Error::Cancelled))); } // ---- Step 9: Terminal nodes, goal gates, visit limits ---- @@ -676,7 +676,7 @@ mod tests { ExecutorBuilder::new(Arc::new(AlwaysSucceedHandler) as Arc>) .build(); let result = executor.run(&g, state).await; - assert!(matches!(result, Err(CoreError::VisitLimitExceeded { .. }))); + assert!(matches!(result, Err(Error::VisitLimitExceeded { .. }))); } #[tokio::test] @@ -696,7 +696,7 @@ mod tests { .max_node_visits(3) .build(); let result = executor.run(&g, state).await; - assert!(matches!(result, Err(CoreError::VisitLimitExceeded { .. }))); + assert!(matches!(result, Err(Error::VisitLimitExceeded { .. }))); } // ---- Step 10: Edge selection, jumps, loop restarts ---- @@ -922,19 +922,19 @@ mod tests { .cancel_token(token) .build(); let result = executor.run(&g, state).await; - assert!(matches!(result, Err(CoreError::Cancelled))); + assert!(matches!(result, Err(Error::Cancelled))); } #[tokio::test] async fn executor_preserves_handler_returned_cancellation() { - let handler = Arc::new(CountingHandler::new(vec![Err(CoreError::Cancelled)])); + let handler = Arc::new(CountingHandler::new(vec![Err(Error::Cancelled)])); let g = linear_graph(&["start", "end"]); let state = ExecutionState::new(&g).unwrap(); let executor = ExecutorBuilder::new(handler as Arc>).build(); let result = executor.run(&g, state).await; - assert!(matches!(result, Err(CoreError::Cancelled))); + assert!(matches!(result, Err(Error::Cancelled))); } #[tokio::test] @@ -950,7 +950,7 @@ mod tests { } } - let handler = Arc::new(CountingHandler::new(vec![Err(CoreError::Cancelled)])); + let handler = Arc::new(CountingHandler::new(vec![Err(Error::Cancelled)])); let g = linear_graph(&["start", "end"]); let state = ExecutionState::new(&g).unwrap(); let executor = ExecutorBuilder::new(handler as Arc>) @@ -959,7 +959,7 @@ mod tests { let result = executor.run(&g, state).await; - assert!(matches!(result, Err(CoreError::Cancelled))); + assert!(matches!(result, Err(Error::Cancelled))); assert_eq!(log.lock().unwrap().as_slice(), &[true]); } @@ -969,27 +969,27 @@ mod tests { async fn executor_retry_on_retryable_error() { let handler = Arc::new( CountingHandler::new(vec![ - Err(CoreError::handler(HandlerErrorDetail { - message: "fail1".into(), + Err(Error::handler(HandlerErrorDetail { + message: "fail1".into(), retryable: true, - category: None, + category: None, signature: None, })), - Err(CoreError::handler(HandlerErrorDetail { - message: "fail2".into(), + Err(Error::handler(HandlerErrorDetail { + message: "fail2".into(), retryable: true, - category: None, + category: None, signature: None, })), Ok(Outcome::success()), ]) .with_retry_policy(RetryPolicy { max_attempts: 3, - backoff: BackoffPolicy { + backoff: BackoffPolicy { initial_delay: Duration::from_millis(1), - factor: 1.0, - max_delay: Duration::from_millis(1), - jitter: false, + factor: 1.0, + max_delay: Duration::from_millis(1), + jitter: false, }, }), ); @@ -1019,11 +1019,11 @@ mod tests { ]) .with_retry_policy(RetryPolicy { max_attempts: 3, - backoff: BackoffPolicy { + backoff: BackoffPolicy { initial_delay: Duration::from_millis(1), - factor: 1.0, - max_delay: Duration::from_millis(1), - jitter: false, + factor: 1.0, + max_delay: Duration::from_millis(1), + jitter: false, }, }), ); @@ -1040,10 +1040,10 @@ mod tests { #[tokio::test] async fn executor_retry_non_retryable_error_no_retry() { let handler = Arc::new( - CountingHandler::new(vec![Err(CoreError::handler(HandlerErrorDetail { - message: "fatal".into(), + CountingHandler::new(vec![Err(Error::handler(HandlerErrorDetail { + message: "fatal".into(), retryable: false, - category: None, + category: None, signature: None, }))]) .with_retry_policy(RetryPolicy::with_max_attempts(3)), @@ -1062,11 +1062,11 @@ mod tests { #[tokio::test] async fn executor_retry_no_retry_by_default() { // Default policy is RetryPolicy::none() (max_attempts=1) - let handler = Arc::new(CountingHandler::new(vec![Err(CoreError::handler( + let handler = Arc::new(CountingHandler::new(vec![Err(Error::handler( HandlerErrorDetail { - message: "fail".into(), + message: "fail".into(), retryable: true, - category: None, + category: None, signature: None, }, ))])); @@ -1099,11 +1099,11 @@ mod tests { fn retry_policy(&self, _n: &TestNode, _g: &TestGraph) -> RetryPolicy { RetryPolicy { max_attempts: 2, - backoff: BackoffPolicy { + backoff: BackoffPolicy { initial_delay: Duration::from_millis(1), - factor: 1.0, - max_delay: Duration::from_millis(1), - jitter: false, + factor: 1.0, + max_delay: Duration::from_millis(1), + jitter: false, }, } } @@ -1142,21 +1142,21 @@ mod tests { } let handler = Arc::new( CountingHandler::new(vec![ - Err(CoreError::handler(HandlerErrorDetail { - message: "r".into(), + Err(Error::handler(HandlerErrorDetail { + message: "r".into(), retryable: true, - category: None, + category: None, signature: None, })), Ok(Outcome::success()), ]) .with_retry_policy(RetryPolicy { max_attempts: 3, - backoff: BackoffPolicy { + backoff: BackoffPolicy { initial_delay: Duration::from_millis(1), - factor: 1.0, - max_delay: Duration::from_millis(1), - jitter: false, + factor: 1.0, + max_delay: Duration::from_millis(1), + jitter: false, }, }), ); @@ -1186,21 +1186,21 @@ mod tests { } let handler = Arc::new( CountingHandler::new(vec![ - Err(CoreError::handler(HandlerErrorDetail { - message: "r".into(), + Err(Error::handler(HandlerErrorDetail { + message: "r".into(), retryable: true, - category: None, + category: None, signature: None, })), Ok(Outcome::success()), ]) .with_retry_policy(RetryPolicy { max_attempts: 3, - backoff: BackoffPolicy { + backoff: BackoffPolicy { initial_delay: Duration::from_millis(1), - factor: 1.0, - max_delay: Duration::from_millis(1), - jitter: false, + factor: 1.0, + max_delay: Duration::from_millis(1), + jitter: false, }, }), ); @@ -1236,21 +1236,21 @@ mod tests { } let handler = Arc::new( CountingHandler::new(vec![ - Err(CoreError::handler(HandlerErrorDetail { - message: "r".into(), + Err(Error::handler(HandlerErrorDetail { + message: "r".into(), retryable: true, - category: None, + category: None, signature: None, })), Ok(Outcome::success()), // should not be reached ]) .with_retry_policy(RetryPolicy { max_attempts: 3, - backoff: BackoffPolicy { + backoff: BackoffPolicy { initial_delay: Duration::from_millis(1), - factor: 1.0, - max_delay: Duration::from_millis(1), - jitter: false, + factor: 1.0, + max_delay: Duration::from_millis(1), + jitter: false, }, }), ); @@ -1278,11 +1278,11 @@ mod tests { ]) .with_retry_policy(RetryPolicy { max_attempts: 3, - backoff: BackoffPolicy { + backoff: BackoffPolicy { initial_delay: Duration::from_secs(5), - factor: 2.0, - max_delay: Duration::from_secs(60), - jitter: false, + factor: 2.0, + max_delay: Duration::from_secs(60), + jitter: false, }, }), ); @@ -1350,7 +1350,7 @@ mod tests { .lifecycle(Box::new(Blocker)) .build(); let result = executor.run(&g, state).await; - assert!(matches!(result, Err(CoreError::Blocked { .. }))); + assert!(matches!(result, Err(Error::Blocked { .. }))); } #[tokio::test] @@ -1429,7 +1429,7 @@ mod tests { .lifecycle(Box::new(EdgeBlocker)) .build(); let result = executor.run(&g, state).await; - assert!(matches!(result, Err(CoreError::Blocked { .. }))); + assert!(matches!(result, Err(Error::Blocked { .. }))); } #[tokio::test] @@ -1619,10 +1619,13 @@ mod tests { executor.run(&g, state).await.unwrap(); let checkpoints = log.lock().unwrap().clone(); // "start" checkpoints with next="work", "work" checkpoints with next="end" - assert_eq!(checkpoints, vec![ - ("start".to_string(), Some("work".to_string())), - ("work".to_string(), Some("end".to_string())), - ]); + assert_eq!( + checkpoints, + vec![ + ("start".to_string(), Some("work".to_string())), + ("work".to_string(), Some("end".to_string())), + ] + ); } #[tokio::test] @@ -1692,10 +1695,13 @@ mod tests { executor.run(&g, state).await.unwrap(); - assert_eq!(*log.lock().unwrap(), vec![ - "after_record:start:start:hello".to_string(), - "on_edge_selected:start:hello".to_string(), - ]); + assert_eq!( + *log.lock().unwrap(), + vec![ + "after_record:start:start:hello".to_string(), + "on_edge_selected:start:hello".to_string(), + ] + ); } #[tokio::test] @@ -1744,10 +1750,10 @@ mod tests { .lifecycle(Box::new(GateTracker(log2.clone()))) .build(); executor2.run(&g2, state2).await.unwrap(); - assert_eq!(log2.lock().unwrap().clone(), vec![( - "end".to_string(), - false - )]); + assert_eq!( + log2.lock().unwrap().clone(), + vec![("end".to_string(), false)] + ); } #[tokio::test] @@ -2006,7 +2012,7 @@ mod tests { .build(); let result = executor.run(&g, state).await; match result { - Err(CoreError::StallTimeout { ref node_id }) => { + Err(Error::StallTimeout { ref node_id }) => { assert_eq!(node_id, "start"); } other => panic!("expected StallTimeout, got {other:?}"), @@ -2035,10 +2041,10 @@ mod tests { if c == 0 { // First call: fail with retryable, then cancel stall during backoff self.stall.cancel(); - Err(CoreError::handler(HandlerErrorDetail { - message: "transient".into(), + Err(Error::handler(HandlerErrorDetail { + message: "transient".into(), retryable: true, - category: None, + category: None, signature: None, })) } else { @@ -2048,11 +2054,11 @@ mod tests { fn retry_policy(&self, _n: &TestNode, _g: &TestGraph) -> RetryPolicy { RetryPolicy { max_attempts: 3, - backoff: BackoffPolicy { + backoff: BackoffPolicy { initial_delay: Duration::from_secs(60), - factor: 1.0, - max_delay: Duration::from_secs(60), - jitter: false, + factor: 1.0, + max_delay: Duration::from_secs(60), + jitter: false, }, } } @@ -2068,7 +2074,7 @@ mod tests { .build(); let result = executor.run(&g, state).await; assert!( - matches!(result, Err(CoreError::StallTimeout { .. })), + matches!(result, Err(Error::StallTimeout { .. })), "expected StallTimeout, got {result:?}" ); } @@ -2102,7 +2108,7 @@ mod tests { .build(); let result = executor.run(&g, state).await; assert!( - matches!(result, Err(CoreError::StallTimeout { .. })), + matches!(result, Err(Error::StallTimeout { .. })), "expected StallTimeout, got {result:?}" ); } diff --git a/lib/crates/fabro-core/src/graph.rs b/lib/crates/fabro-core/src/graph.rs index 304e266a8..0cb1744be 100644 --- a/lib/crates/fabro-core/src/graph.rs +++ b/lib/crates/fabro-core/src/graph.rs @@ -17,7 +17,7 @@ pub trait EdgeSpec: Send + Sync + Clone { } pub struct EdgeSelection { - pub edge: G::Edge, + pub edge: G::Edge, pub reason: &'static str, } diff --git a/lib/crates/fabro-core/src/lib.rs b/lib/crates/fabro-core/src/lib.rs index 8bdb0433a..ea6f8bb90 100644 --- a/lib/crates/fabro-core/src/lib.rs +++ b/lib/crates/fabro-core/src/lib.rs @@ -13,7 +13,7 @@ pub mod state; pub mod test_fixtures; pub use context::Context; -pub use error::{CoreError, Error, HandlerErrorDetail, Result, VisitLimitSource}; +pub use error::{Error, HandlerErrorDetail, Result, VisitLimitSource}; pub use executor::{Executor, ExecutorBuilder, ExecutorOptions}; pub use graph::{EdgeSelection, EdgeSpec, Graph, NodeSpec}; pub use handler::NodeHandler; diff --git a/lib/crates/fabro-core/src/lifecycle.rs b/lib/crates/fabro-core/src/lifecycle.rs index 16d7ecf4f..836925e37 100644 --- a/lib/crates/fabro-core/src/lifecycle.rs +++ b/lib/crates/fabro-core/src/lifecycle.rs @@ -22,26 +22,26 @@ pub enum EdgeDecision { } pub struct AttemptContext<'a, G: Graph> { - pub node: &'a G::Node, - pub attempt: u32, + pub node: &'a G::Node, + pub attempt: u32, pub max_attempts: u32, } pub struct AttemptResultContext<'a, G: Graph> { - pub node: &'a G::Node, - pub result: &'a NodeResult, - pub attempt: u32, - pub will_retry: bool, + pub node: &'a G::Node, + pub result: &'a NodeResult, + pub attempt: u32, + pub will_retry: bool, pub backoff_delay: Option, } pub struct EdgeContext<'a, G: Graph> { - pub from: &'a str, - pub to: &'a str, - pub edge: Option, + pub from: &'a str, + pub to: &'a str, + pub edge: Option, pub is_jump: bool, pub outcome: &'a Outcome, - pub reason: &'a str, + pub reason: &'a str, } #[async_trait] @@ -272,11 +272,11 @@ mod tests { /// A lifecycle that records which callbacks were called. struct RecordingLifecycle { - name: String, - log: Arc>>, - before_node_decision: Mutex>, + name: String, + log: Arc>>, + before_node_decision: Mutex>, before_attempt_decision: Mutex>, - edge_decision: Mutex>, + edge_decision: Mutex>, } impl RecordingLifecycle { @@ -525,8 +525,8 @@ mod tests { let state = ExecutionState::new(&g).unwrap(); let node = g.get_node("start").unwrap(); let ctx = AttemptContext { - node: &node, - attempt: 1, + node: &node, + attempt: 1, max_attempts: 1, }; let decision = lc.before_attempt(&ctx, &state).await.unwrap(); @@ -549,8 +549,8 @@ mod tests { let state = ExecutionState::new(&g).unwrap(); let node = g.get_node("start").unwrap(); let ctx = AttemptContext { - node: &node, - attempt: 1, + node: &node, + attempt: 1, max_attempts: 1, }; let decision = lc.before_attempt(&ctx, &state).await.unwrap(); @@ -569,10 +569,10 @@ mod tests { let node = g.get_node("start").unwrap(); let result = NodeResult::new(Outcome::success(), Duration::ZERO, 1, 1); let ctx = AttemptResultContext { - node: &node, - result: &result, - attempt: 1, - will_retry: false, + node: &node, + result: &result, + attempt: 1, + will_retry: false, backoff_delay: None, }; lc.after_attempt(&ctx, &state).await.unwrap(); @@ -595,12 +595,12 @@ mod tests { let outcome = Outcome::success(); let edge = g.outgoing_edges("start").into_iter().next().unwrap(); let ctx = EdgeContext { - from: "start", - to: "end", - edge: Some(edge), + from: "start", + to: "end", + edge: Some(edge), is_jump: false, outcome: &outcome, - reason: "unconditional", + reason: "unconditional", }; let decision = lc.on_edge_selected(&ctx, &state).await.unwrap(); assert!(matches!(decision, EdgeDecision::Override(ref t) if t == "other")); @@ -622,12 +622,12 @@ mod tests { let state = ExecutionState::new(&g).unwrap(); let outcome = Outcome::success(); let ctx = EdgeContext { - from: "start", - to: "end", - edge: None, + from: "start", + to: "end", + edge: None, is_jump: false, outcome: &outcome, - reason: "unconditional", + reason: "unconditional", }; let decision = lc.on_edge_selected(&ctx, &state).await.unwrap(); assert!(matches!(decision, EdgeDecision::Block(_))); @@ -641,12 +641,12 @@ mod tests { let state = ExecutionState::new(&g).unwrap(); let outcome = Outcome::success(); let ctx = EdgeContext:: { - from: "start", - to: "target", - edge: None, + from: "start", + to: "target", + edge: None, is_jump: true, outcome: &outcome, - reason: "jump", + reason: "jump", }; let decision = lc.on_edge_selected(&ctx, &state).await.unwrap(); assert!(matches!(decision, EdgeDecision::Continue)); @@ -692,8 +692,8 @@ mod tests { let counter = Arc::new(AtomicU32::new(0)); struct OrderedLifecycle { - name: String, - log: Arc>>, + name: String, + log: Arc>>, counter: Arc, } @@ -711,18 +711,18 @@ mod tests { let lc = CompositeLifecycle::new(vec![ Box::new(OrderedLifecycle { - name: "first".into(), - log: log.clone(), + name: "first".into(), + log: log.clone(), counter: counter.clone(), }), Box::new(OrderedLifecycle { - name: "second".into(), - log: log.clone(), + name: "second".into(), + log: log.clone(), counter: counter.clone(), }), Box::new(OrderedLifecycle { - name: "third".into(), - log: log.clone(), + name: "third".into(), + log: log.clone(), counter: counter.clone(), }), ]); diff --git a/lib/crates/fabro-core/src/outcome.rs b/lib/crates/fabro-core/src/outcome.rs index 8664b40e0..605bfc7b7 100644 --- a/lib/crates/fabro-core/src/outcome.rs +++ b/lib/crates/fabro-core/src/outcome.rs @@ -4,14 +4,14 @@ pub use fabro_types::outcome::{ FailureCategory, FailureDetail, NodeResult, Outcome, OutcomeMeta, StageStatus, }; -use crate::error::CoreError; +use crate::error::Error; pub trait NodeResultExt { - fn from_error(error: &CoreError, duration: Duration, attempts: u32, max_attempts: u32) -> Self; + fn from_error(error: &Error, duration: Duration, attempts: u32, max_attempts: u32) -> Self; } impl NodeResultExt for NodeResult { - fn from_error(error: &CoreError, duration: Duration, attempts: u32, max_attempts: u32) -> Self { + fn from_error(error: &Error, duration: Duration, attempts: u32, max_attempts: u32) -> Self { Self { outcome: error.to_fail_outcome(), duration, diff --git a/lib/crates/fabro-core/src/retry.rs b/lib/crates/fabro-core/src/retry.rs index e16b413cf..a13cf0082 100644 --- a/lib/crates/fabro-core/src/retry.rs +++ b/lib/crates/fabro-core/src/retry.rs @@ -3,14 +3,14 @@ pub use fabro_util::backoff::BackoffPolicy; #[derive(Debug, Clone)] pub struct RetryPolicy { pub max_attempts: u32, - pub backoff: BackoffPolicy, + pub backoff: BackoffPolicy, } impl RetryPolicy { pub fn none() -> Self { Self { max_attempts: 1, - backoff: BackoffPolicy::default(), + backoff: BackoffPolicy::default(), } } diff --git a/lib/crates/fabro-core/src/stall.rs b/lib/crates/fabro-core/src/stall.rs index 8068900bc..10c7e8fe9 100644 --- a/lib/crates/fabro-core/src/stall.rs +++ b/lib/crates/fabro-core/src/stall.rs @@ -16,18 +16,18 @@ pub trait ActivityMonitor: Send + Sync { /// Watches for inactivity and fires a stall timeout if no activity is /// reported within the configured duration. pub struct StallWatchdog { - timeout: Duration, + timeout: Duration, cancel_token: Arc, - activity: Arc, - shutdown: Arc, - monitor: Arc, + activity: Arc, + shutdown: Arc, + monitor: Arc, } /// Guard that resets the stall timer on activity. Drop to stop watching. pub struct StallGuard { activity: Arc, shutdown: Arc, - handle: Option>, + handle: Option>, } impl StallWatchdog { @@ -82,7 +82,7 @@ impl StallWatchdog { StallGuard { activity: self.activity, shutdown: self.shutdown, - handle: Some(handle), + handle: Some(handle), } } } diff --git a/lib/crates/fabro-core/src/state.rs b/lib/crates/fabro-core/src/state.rs index 24dd1a0dd..10617ceb5 100644 --- a/lib/crates/fabro-core/src/state.rs +++ b/lib/crates/fabro-core/src/state.rs @@ -17,30 +17,30 @@ impl std::fmt::Debug for ExecutionState { } pub struct ExecutionState { - pub context: Context, - pub current_node_id: String, - pub completed_nodes: Vec, - pub node_outcomes: HashMap>, - pub node_retries: HashMap, - pub node_visits: HashMap, - pub stage_index: usize, + pub context: Context, + pub current_node_id: String, + pub completed_nodes: Vec, + pub node_outcomes: HashMap>, + pub node_retries: HashMap, + pub node_visits: HashMap, + pub stage_index: usize, pub previous_node_id: Option, - pub cancelled: bool, + pub cancelled: bool, } impl ExecutionState { pub fn new(graph: &G) -> Result { let start = graph.find_start_node()?; Ok(Self { - context: Context::new(), - current_node_id: start.id().to_string(), - completed_nodes: Vec::new(), - node_outcomes: HashMap::new(), - node_retries: HashMap::new(), - node_visits: HashMap::new(), - stage_index: 0, + context: Context::new(), + current_node_id: start.id().to_string(), + completed_nodes: Vec::new(), + node_outcomes: HashMap::new(), + node_retries: HashMap::new(), + node_visits: HashMap::new(), + stage_index: 0, previous_node_id: None, - cancelled: false, + cancelled: false, }) } diff --git a/lib/crates/fabro-core/src/test_fixtures.rs b/lib/crates/fabro-core/src/test_fixtures.rs index e78bb2337..c653af921 100644 --- a/lib/crates/fabro-core/src/test_fixtures.rs +++ b/lib/crates/fabro-core/src/test_fixtures.rs @@ -5,7 +5,7 @@ use std::sync::atomic::{AtomicU32, Ordering}; use async_trait::async_trait; use crate::context::Context; -use crate::error::{CoreError, HandlerErrorDetail, Result}; +use crate::error::{Error, HandlerErrorDetail, Result}; use crate::graph::{EdgeSelection, EdgeSpec, Graph, NodeSpec}; use crate::handler::NodeHandler; use crate::outcome::{Outcome, StageStatus}; @@ -15,28 +15,28 @@ use crate::retry::RetryPolicy; #[derive(Debug, Clone)] pub struct TestNode { - pub id: String, - pub terminal: bool, + pub id: String, + pub terminal: bool, pub max_visits: Option, - pub goal_gate: Option<(String, StageStatus)>, + pub goal_gate: Option<(String, StageStatus)>, } impl TestNode { pub fn new(id: &str) -> Self { Self { - id: id.to_string(), - terminal: false, + id: id.to_string(), + terminal: false, max_visits: None, - goal_gate: None, + goal_gate: None, } } pub fn terminal(id: &str) -> Self { Self { - id: id.to_string(), - terminal: true, + id: id.to_string(), + terminal: true, max_visits: None, - goal_gate: None, + goal_gate: None, } } @@ -71,18 +71,18 @@ impl NodeSpec for TestNode { #[derive(Debug, Clone)] pub struct TestEdge { - pub from: String, - pub to: String, - pub label: Option, + pub from: String, + pub to: String, + pub label: Option, pub loop_restart: bool, } impl TestEdge { pub fn new(from: &str, to: &str) -> Self { Self { - from: from.to_string(), - to: to.to_string(), - label: None, + from: from.to_string(), + to: to.to_string(), + label: None, loop_restart: false, } } @@ -118,8 +118,8 @@ impl EdgeSpec for TestEdge { #[derive(Debug, Clone)] pub struct TestGraph { - pub nodes: Vec, - pub edges: Vec, + pub nodes: Vec, + pub edges: Vec, pub start_node_id: String, pub retry_targets: HashMap, } @@ -151,8 +151,7 @@ impl Graph for TestGraph { } fn find_start_node(&self) -> Result { - self.get_node(&self.start_node_id) - .ok_or(CoreError::NoStartNode) + self.get_node(&self.start_node_id).ok_or(Error::NoStartNode) } fn outgoing_edges(&self, node_id: &str) -> Vec { @@ -181,7 +180,7 @@ impl Graph for TestGraph { .find(|e| e.label.as_deref() == Some(label.as_str())) { return Some(EdgeSelection { - edge: e.clone(), + edge: e.clone(), reason: "preferred_label", }); } @@ -194,7 +193,7 @@ impl Graph for TestGraph { .find(|e| e.label.as_deref() == Some(status_label.as_str())) { return Some(EdgeSelection { - edge: e.clone(), + edge: e.clone(), reason: "condition", }); } @@ -203,7 +202,7 @@ impl Graph for TestGraph { for suggested in &outcome.suggested_next_ids { if let Some(e) = edges.iter().find(|e| e.to == *suggested) { return Some(EdgeSelection { - edge: e.clone(), + edge: e.clone(), reason: "suggested_next", }); } @@ -212,7 +211,7 @@ impl Graph for TestGraph { // Fourth: unconditional (no label) if let Some(e) = edges.iter().find(|e| e.label.is_none()) { return Some(EdgeSelection { - edge: e.clone(), + edge: e.clone(), reason: "unconditional", }); } @@ -287,16 +286,16 @@ impl NodeHandler for AlwaysFailHandler { } pub struct CountingHandler { - pub call_count: AtomicU32, - pub outcomes: std::sync::Mutex>>, + pub call_count: AtomicU32, + pub outcomes: std::sync::Mutex>>, pub retry_policy: RetryPolicy, } impl CountingHandler { - pub fn new(outcomes: Vec>) -> Self { + pub fn new(outcomes: Vec>) -> Self { Self { - call_count: AtomicU32::new(0), - outcomes: std::sync::Mutex::new(outcomes), + call_count: AtomicU32::new(0), + outcomes: std::sync::Mutex::new(outcomes), retry_policy: RetryPolicy::none(), } } @@ -337,7 +336,7 @@ impl NodeHandler for CountingHandler { /// A handler that dispatches based on node ID. pub struct DispatchHandler { handlers: HashMap>>, - default: Arc>, + default: Arc>, } impl DispatchHandler { @@ -378,20 +377,20 @@ impl NodeHandler for DispatchHandler { } } -/// A handler that returns Err(CoreError::Handler) with configurable +/// A handler that returns Err(Error::Handler) with configurable /// retryability. pub struct ErrorHandler { - pub detail: HandlerErrorDetail, + pub detail: HandlerErrorDetail, pub retry_policy: RetryPolicy, } impl ErrorHandler { pub fn retryable(message: &str, policy: RetryPolicy) -> Self { Self { - detail: HandlerErrorDetail { - message: message.to_string(), + detail: HandlerErrorDetail { + message: message.to_string(), retryable: true, - category: None, + category: None, signature: None, }, retry_policy: policy, @@ -400,10 +399,10 @@ impl ErrorHandler { pub fn non_retryable(message: &str) -> Self { Self { - detail: HandlerErrorDetail { - message: message.to_string(), + detail: HandlerErrorDetail { + message: message.to_string(), retryable: false, - category: None, + category: None, signature: None, }, retry_policy: RetryPolicy::none(), @@ -419,7 +418,7 @@ impl NodeHandler for ErrorHandler { _context: &Context, _graph: &TestGraph, ) -> Result { - Err(CoreError::handler(self.detail.clone())) + Err(Error::handler(self.detail.clone())) } fn retry_policy(&self, _node: &TestNode, _graph: &TestGraph) -> RetryPolicy { diff --git a/lib/crates/fabro-devcontainer/src/compose.rs b/lib/crates/fabro-devcontainer/src/compose.rs index 6a29213a4..065604233 100644 --- a/lib/crates/fabro-devcontainer/src/compose.rs +++ b/lib/crates/fabro-devcontainer/src/compose.rs @@ -4,17 +4,17 @@ use std::path::{Path, PathBuf}; /// Extracted configuration from a Docker Compose service. #[derive(Debug, Clone, Default)] pub(crate) struct ComposeServiceSpec { - pub image: Option, - pub build: Option, - pub ports: Vec, + pub image: Option, + pub build: Option, + pub ports: Vec, pub environment: HashMap, - pub user: Option, + pub user: Option, } /// Build configuration from a Docker Compose service. #[derive(Debug, Clone)] pub(crate) struct ComposeBuild { - pub context: String, + pub context: String, pub dockerfile: Option, } @@ -62,7 +62,7 @@ fn parse_build(service: &serde_yaml::Value) -> Option { if let Some(context) = build_val.as_str() { return Some(ComposeBuild { - context: context.to_string(), + context: context.to_string(), dockerfile: None, }); } diff --git a/lib/crates/fabro-devcontainer/src/dockerfile.rs b/lib/crates/fabro-devcontainer/src/dockerfile.rs index 24e11268b..5f7a5725b 100644 --- a/lib/crates/fabro-devcontainer/src/dockerfile.rs +++ b/lib/crates/fabro-devcontainer/src/dockerfile.rs @@ -43,8 +43,8 @@ mod tests { fn make_layer(id: &str, dir_name: &str, snippet: &str) -> FeatureLayer { FeatureLayer { - id: id.to_string(), - dir_name: dir_name.to_string(), + id: id.to_string(), + dir_name: dir_name.to_string(), dockerfile_snippet: snippet.to_string(), } } diff --git a/lib/crates/fabro-devcontainer/src/features.rs b/lib/crates/fabro-devcontainer/src/features.rs index 6d56ce254..e6a3e1421 100644 --- a/lib/crates/fabro-devcontainer/src/features.rs +++ b/lib/crates/fabro-devcontainer/src/features.rs @@ -13,9 +13,9 @@ use crate::types::{FeatureMetadata, LifecycleCommand}; #[derive(Debug, Clone)] pub(crate) struct FeatureLayer { /// Feature identifier (e.g. "ghcr.io/devcontainers/features/node:1") - pub id: String, + pub id: String, /// Directory name for COPY - pub dir_name: String, + pub dir_name: String, /// Dockerfile snippet for this feature pub dockerfile_snippet: String, } @@ -23,11 +23,11 @@ pub(crate) struct FeatureLayer { /// All resolved feature data: layers, environment, and lifecycle hooks. #[derive(Debug, Clone, Default)] pub(crate) struct ResolvedFeatures { - pub layers: Vec, - pub container_env: HashMap, - pub on_create_commands: Vec, + pub layers: Vec, + pub container_env: HashMap, + pub on_create_commands: Vec, pub post_create_commands: Vec, - pub post_start_commands: Vec, + pub post_start_commands: Vec, } /// Extract the directory name from a feature ID. @@ -628,16 +628,16 @@ pub(crate) async fn resolve_features( .get(id) .cloned() .unwrap_or_else(|| FeatureMetadata { - id: None, - name: None, - version: None, - options: HashMap::new(), - installs_after: Vec::new(), - depends_on: HashMap::new(), - container_env: HashMap::new(), - on_create_command: None, + id: None, + name: None, + version: None, + options: HashMap::new(), + installs_after: Vec::new(), + depends_on: HashMap::new(), + container_env: HashMap::new(), + on_create_command: None, post_create_command: None, - post_start_command: None, + post_start_command: None, }); // Collect feature containerEnv (later features override earlier) @@ -754,18 +754,21 @@ mod tests { let metadata: HashMap = ids .iter() .map(|id| { - (id.clone(), FeatureMetadata { - id: Some(id.clone()), - name: None, - version: None, - options: HashMap::new(), - installs_after: Vec::new(), - depends_on: HashMap::new(), - container_env: HashMap::new(), - on_create_command: None, - post_create_command: None, - post_start_command: None, - }) + ( + id.clone(), + FeatureMetadata { + id: Some(id.clone()), + name: None, + version: None, + options: HashMap::new(), + installs_after: Vec::new(), + depends_on: HashMap::new(), + container_env: HashMap::new(), + on_create_command: None, + post_create_command: None, + post_start_command: None, + }, + ) }) .collect(); @@ -778,30 +781,36 @@ mod tests { // A depends on B (A installs after B), so B should come first let ids = vec!["a".to_string(), "b".to_string()]; let mut metadata: HashMap = HashMap::new(); - metadata.insert("a".to_string(), FeatureMetadata { - id: Some("a".to_string()), - name: None, - version: None, - options: HashMap::new(), - installs_after: vec!["b".to_string()], - depends_on: HashMap::new(), - container_env: HashMap::new(), - on_create_command: None, - post_create_command: None, - post_start_command: None, - }); - metadata.insert("b".to_string(), FeatureMetadata { - id: Some("b".to_string()), - name: None, - version: None, - options: HashMap::new(), - installs_after: Vec::new(), - depends_on: HashMap::new(), - container_env: HashMap::new(), - on_create_command: None, - post_create_command: None, - post_start_command: None, - }); + metadata.insert( + "a".to_string(), + FeatureMetadata { + id: Some("a".to_string()), + name: None, + version: None, + options: HashMap::new(), + installs_after: vec!["b".to_string()], + depends_on: HashMap::new(), + container_env: HashMap::new(), + on_create_command: None, + post_create_command: None, + post_start_command: None, + }, + ); + metadata.insert( + "b".to_string(), + FeatureMetadata { + id: Some("b".to_string()), + name: None, + version: None, + options: HashMap::new(), + installs_after: Vec::new(), + depends_on: HashMap::new(), + container_env: HashMap::new(), + on_create_command: None, + post_create_command: None, + post_start_command: None, + }, + ); let sorted = topo_sort(&ids, &metadata); assert_eq!(sorted, vec!["b", "a"]); @@ -819,54 +828,66 @@ mod tests { "a".to_string(), ]; let mut metadata: HashMap = HashMap::new(); - metadata.insert("a".to_string(), FeatureMetadata { - id: Some("a".to_string()), - name: None, - version: None, - options: HashMap::new(), - installs_after: Vec::new(), - depends_on: HashMap::new(), - container_env: HashMap::new(), - on_create_command: None, - post_create_command: None, - post_start_command: None, - }); - metadata.insert("b".to_string(), FeatureMetadata { - id: Some("b".to_string()), - name: None, - version: None, - options: HashMap::new(), - installs_after: vec!["a".to_string()], - depends_on: HashMap::new(), - container_env: HashMap::new(), - on_create_command: None, - post_create_command: None, - post_start_command: None, - }); - metadata.insert("c".to_string(), FeatureMetadata { - id: Some("c".to_string()), - name: None, - version: None, - options: HashMap::new(), - installs_after: vec!["a".to_string()], - depends_on: HashMap::new(), - container_env: HashMap::new(), - on_create_command: None, - post_create_command: None, - post_start_command: None, - }); - metadata.insert("d".to_string(), FeatureMetadata { - id: Some("d".to_string()), - name: None, - version: None, - options: HashMap::new(), - installs_after: vec!["b".to_string(), "c".to_string()], - depends_on: HashMap::new(), - container_env: HashMap::new(), - on_create_command: None, - post_create_command: None, - post_start_command: None, - }); + metadata.insert( + "a".to_string(), + FeatureMetadata { + id: Some("a".to_string()), + name: None, + version: None, + options: HashMap::new(), + installs_after: Vec::new(), + depends_on: HashMap::new(), + container_env: HashMap::new(), + on_create_command: None, + post_create_command: None, + post_start_command: None, + }, + ); + metadata.insert( + "b".to_string(), + FeatureMetadata { + id: Some("b".to_string()), + name: None, + version: None, + options: HashMap::new(), + installs_after: vec!["a".to_string()], + depends_on: HashMap::new(), + container_env: HashMap::new(), + on_create_command: None, + post_create_command: None, + post_start_command: None, + }, + ); + metadata.insert( + "c".to_string(), + FeatureMetadata { + id: Some("c".to_string()), + name: None, + version: None, + options: HashMap::new(), + installs_after: vec!["a".to_string()], + depends_on: HashMap::new(), + container_env: HashMap::new(), + on_create_command: None, + post_create_command: None, + post_start_command: None, + }, + ); + metadata.insert( + "d".to_string(), + FeatureMetadata { + id: Some("d".to_string()), + name: None, + version: None, + options: HashMap::new(), + installs_after: vec!["b".to_string(), "c".to_string()], + depends_on: HashMap::new(), + container_env: HashMap::new(), + on_create_command: None, + post_create_command: None, + post_start_command: None, + }, + ); let sorted = topo_sort(&ids, &metadata); // A must come before B and C; B and C must come before D @@ -884,22 +905,25 @@ mod tests { fn generate_layer_with_options() { let options = serde_json::json!({"version": "20"}); let mut meta_options = HashMap::new(); - meta_options.insert("version".to_string(), FeatureOption { - option_type: Some("string".to_string()), - default: Some(serde_json::Value::String("lts".to_string())), - description: Some("Node.js version".to_string()), - }); + meta_options.insert( + "version".to_string(), + FeatureOption { + option_type: Some("string".to_string()), + default: Some(serde_json::Value::String("lts".to_string())), + description: Some("Node.js version".to_string()), + }, + ); let metadata = FeatureMetadata { - id: Some("node".to_string()), - name: Some("Node.js".to_string()), - version: Some("1.0.0".to_string()), - options: meta_options, - installs_after: Vec::new(), - depends_on: HashMap::new(), - container_env: HashMap::new(), - on_create_command: None, + id: Some("node".to_string()), + name: Some("Node.js".to_string()), + version: Some("1.0.0".to_string()), + options: meta_options, + installs_after: Vec::new(), + depends_on: HashMap::new(), + container_env: HashMap::new(), + on_create_command: None, post_create_command: None, - post_start_command: None, + post_start_command: None, }; let snippet = generate_layer( @@ -928,22 +952,25 @@ mod tests { fn generate_layer_with_defaults() { let options = serde_json::json!({}); let mut meta_options = HashMap::new(); - meta_options.insert("version".to_string(), FeatureOption { - option_type: Some("string".to_string()), - default: Some(serde_json::Value::String("lts".to_string())), - description: Some("Node.js version".to_string()), - }); + meta_options.insert( + "version".to_string(), + FeatureOption { + option_type: Some("string".to_string()), + default: Some(serde_json::Value::String("lts".to_string())), + description: Some("Node.js version".to_string()), + }, + ); let metadata = FeatureMetadata { - id: Some("node".to_string()), - name: None, - version: None, - options: meta_options, - installs_after: Vec::new(), - depends_on: HashMap::new(), - container_env: HashMap::new(), - on_create_command: None, + id: Some("node".to_string()), + name: None, + version: None, + options: meta_options, + installs_after: Vec::new(), + depends_on: HashMap::new(), + container_env: HashMap::new(), + on_create_command: None, post_create_command: None, - post_start_command: None, + post_start_command: None, }; let snippet = generate_layer( @@ -972,16 +999,16 @@ mod tests { fn generate_layer_no_options() { let options = serde_json::json!({}); let metadata = FeatureMetadata { - id: Some("common-utils".to_string()), - name: None, - version: None, - options: HashMap::new(), - installs_after: Vec::new(), - depends_on: HashMap::new(), - container_env: HashMap::new(), - on_create_command: None, + id: Some("common-utils".to_string()), + name: None, + version: None, + options: HashMap::new(), + installs_after: Vec::new(), + depends_on: HashMap::new(), + container_env: HashMap::new(), + on_create_command: None, post_create_command: None, - post_start_command: None, + post_start_command: None, }; let snippet = generate_layer( @@ -1012,30 +1039,36 @@ mod tests { let mut metadata: HashMap = HashMap::new(); let mut depends = HashMap::new(); depends.insert("b".to_string(), serde_json::json!({})); - metadata.insert("a".to_string(), FeatureMetadata { - id: Some("a".to_string()), - name: None, - version: None, - options: HashMap::new(), - installs_after: Vec::new(), - depends_on: depends, - container_env: HashMap::new(), - on_create_command: None, - post_create_command: None, - post_start_command: None, - }); - metadata.insert("b".to_string(), FeatureMetadata { - id: Some("b".to_string()), - name: None, - version: None, - options: HashMap::new(), - installs_after: Vec::new(), - depends_on: HashMap::new(), - container_env: HashMap::new(), - on_create_command: None, - post_create_command: None, - post_start_command: None, - }); + metadata.insert( + "a".to_string(), + FeatureMetadata { + id: Some("a".to_string()), + name: None, + version: None, + options: HashMap::new(), + installs_after: Vec::new(), + depends_on: depends, + container_env: HashMap::new(), + on_create_command: None, + post_create_command: None, + post_start_command: None, + }, + ); + metadata.insert( + "b".to_string(), + FeatureMetadata { + id: Some("b".to_string()), + name: None, + version: None, + options: HashMap::new(), + installs_after: Vec::new(), + depends_on: HashMap::new(), + container_env: HashMap::new(), + on_create_command: None, + post_create_command: None, + post_start_command: None, + }, + ); let sorted = topo_sort(&ids, &metadata); assert_eq!(sorted, vec!["b", "a"]); @@ -1048,30 +1081,36 @@ mod tests { let mut metadata: HashMap = HashMap::new(); let mut depends = HashMap::new(); depends.insert("b".to_string(), serde_json::json!({})); - metadata.insert("a".to_string(), FeatureMetadata { - id: Some("a".to_string()), - name: None, - version: None, - options: HashMap::new(), - installs_after: vec!["b".to_string()], - depends_on: depends, - container_env: HashMap::new(), - on_create_command: None, - post_create_command: None, - post_start_command: None, - }); - metadata.insert("b".to_string(), FeatureMetadata { - id: Some("b".to_string()), - name: None, - version: None, - options: HashMap::new(), - installs_after: Vec::new(), - depends_on: HashMap::new(), - container_env: HashMap::new(), - on_create_command: None, - post_create_command: None, - post_start_command: None, - }); + metadata.insert( + "a".to_string(), + FeatureMetadata { + id: Some("a".to_string()), + name: None, + version: None, + options: HashMap::new(), + installs_after: vec!["b".to_string()], + depends_on: depends, + container_env: HashMap::new(), + on_create_command: None, + post_create_command: None, + post_start_command: None, + }, + ); + metadata.insert( + "b".to_string(), + FeatureMetadata { + id: Some("b".to_string()), + name: None, + version: None, + options: HashMap::new(), + installs_after: Vec::new(), + depends_on: HashMap::new(), + container_env: HashMap::new(), + on_create_command: None, + post_create_command: None, + post_start_command: None, + }, + ); let sorted = topo_sort(&ids, &metadata); assert_eq!(sorted, vec!["b", "a"]); @@ -1118,37 +1157,37 @@ mod tests { let mut resolved = ResolvedFeatures::default(); let meta_a = FeatureMetadata { - id: Some("a".to_string()), - name: None, - version: None, - options: HashMap::new(), - installs_after: Vec::new(), - depends_on: HashMap::new(), - container_env: { + id: Some("a".to_string()), + name: None, + version: None, + options: HashMap::new(), + installs_after: Vec::new(), + depends_on: HashMap::new(), + container_env: { let mut env = HashMap::new(); env.insert("FOO".to_string(), "from_a".to_string()); env.insert("BAR".to_string(), "from_a".to_string()); env }, - on_create_command: None, + on_create_command: None, post_create_command: None, - post_start_command: None, + post_start_command: None, }; let meta_b = FeatureMetadata { - id: Some("b".to_string()), - name: None, - version: None, - options: HashMap::new(), - installs_after: Vec::new(), - depends_on: HashMap::new(), - container_env: { + id: Some("b".to_string()), + name: None, + version: None, + options: HashMap::new(), + installs_after: Vec::new(), + depends_on: HashMap::new(), + container_env: { let mut env = HashMap::new(); env.insert("FOO".to_string(), "from_b".to_string()); env }, - on_create_command: None, + on_create_command: None, post_create_command: None, - post_start_command: None, + post_start_command: None, }; // A is sorted first, then B — B's FOO overrides A's @@ -1212,22 +1251,25 @@ mod tests { fn generate_layer_shorthand_version() { let options = serde_json::json!("20"); let mut meta_options = HashMap::new(); - meta_options.insert("version".to_string(), FeatureOption { - option_type: Some("string".to_string()), - default: Some(serde_json::Value::String("lts".to_string())), - description: Some("Node.js version".to_string()), - }); + meta_options.insert( + "version".to_string(), + FeatureOption { + option_type: Some("string".to_string()), + default: Some(serde_json::Value::String("lts".to_string())), + description: Some("Node.js version".to_string()), + }, + ); let metadata = FeatureMetadata { - id: Some("node".to_string()), - name: None, - version: None, - options: meta_options, - installs_after: Vec::new(), - depends_on: HashMap::new(), - container_env: HashMap::new(), - on_create_command: None, + id: Some("node".to_string()), + name: None, + version: None, + options: meta_options, + installs_after: Vec::new(), + depends_on: HashMap::new(), + container_env: HashMap::new(), + on_create_command: None, post_create_command: None, - post_start_command: None, + post_start_command: None, }; let snippet = generate_layer( @@ -1245,16 +1287,16 @@ mod tests { fn generate_layer_install_env_vars() { let options = serde_json::json!({}); let metadata = FeatureMetadata { - id: Some("node".to_string()), - name: None, - version: None, - options: HashMap::new(), - installs_after: Vec::new(), - depends_on: HashMap::new(), - container_env: HashMap::new(), - on_create_command: None, + id: Some("node".to_string()), + name: None, + version: None, + options: HashMap::new(), + installs_after: Vec::new(), + depends_on: HashMap::new(), + container_env: HashMap::new(), + on_create_command: None, post_create_command: None, - post_start_command: None, + post_start_command: None, }; let snippet = generate_layer( diff --git a/lib/crates/fabro-devcontainer/src/lib.rs b/lib/crates/fabro-devcontainer/src/lib.rs index 036eeea06..832e197c6 100644 --- a/lib/crates/fabro-devcontainer/src/lib.rs +++ b/lib/crates/fabro-devcontainer/src/lib.rs @@ -26,33 +26,33 @@ pub enum Command { #[derive(Debug, Clone)] pub struct DevcontainerSpec { /// Generated Dockerfile content - pub dockerfile: String, + pub dockerfile: String, /// Directory for docker build context - pub build_context: PathBuf, + pub build_context: PathBuf, /// Build arguments (docker build --build-arg) - pub build_args: HashMap, + pub build_args: HashMap, /// Multi-stage build target (docker build --target) - pub build_target: Option, + pub build_target: Option, /// Run on host before build - pub initialize_commands: Vec, + pub initialize_commands: Vec, /// Run in container after first creation (before updateContentCommand) - pub on_create_commands: Vec, + pub on_create_commands: Vec, /// Run in container after creation pub post_create_commands: Vec, /// Run in container on each start - pub post_start_commands: Vec, + pub post_start_commands: Vec, /// remoteEnv merged - pub environment: HashMap, + pub environment: HashMap, /// containerEnv — baked into Dockerfile as ENV directives - pub container_env: HashMap, - pub remote_user: Option, + pub container_env: HashMap, + pub remote_user: Option, /// default: /workspaces/{repo-name} - pub workspace_folder: String, + pub workspace_folder: String, /// first = default preview port - pub forwarded_ports: Vec, + pub forwarded_ports: Vec, /// Compose file paths (empty if not in compose mode) - pub compose_files: Vec, - pub compose_service: Option, + pub compose_files: Vec, + pub compose_service: Option, } #[derive(Debug, thiserror::Error)] @@ -65,7 +65,7 @@ pub enum DevcontainerError { #[error("reading file {path}: {source}")] ReadFile { - path: PathBuf, + path: PathBuf, source: std::io::Error, }, diff --git a/lib/crates/fabro-devcontainer/src/types.rs b/lib/crates/fabro-devcontainer/src/types.rs index 2e97d4e43..a881bdad2 100644 --- a/lib/crates/fabro-devcontainer/src/types.rs +++ b/lib/crates/fabro-devcontainer/src/types.rs @@ -110,8 +110,8 @@ pub enum LifecycleCommand { #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct FeatureMetadata { - pub id: Option, - pub name: Option, + pub id: Option, + pub name: Option, pub version: Option, #[serde(default)] @@ -131,9 +131,9 @@ pub(crate) struct FeatureMetadata { pub container_env: HashMap, /// Lifecycle hooks contributed by this feature - pub on_create_command: Option, + pub on_create_command: Option, pub post_create_command: Option, - pub post_start_command: Option, + pub post_start_command: Option, } /// A single option for a devcontainer feature. @@ -141,7 +141,7 @@ pub(crate) struct FeatureMetadata { pub(crate) struct FeatureOption { #[serde(rename = "type")] pub option_type: Option, - pub default: Option, + pub default: Option, pub description: Option, } @@ -228,9 +228,10 @@ mod tests { "workspaceFolder": "/workspace" }"#; let config: DevcontainerJson = serde_json::from_str(json).unwrap(); - assert_eq!(config.docker_compose_file.as_ref().unwrap().paths(), vec![ - "docker-compose.yml" - ]); + assert_eq!( + config.docker_compose_file.as_ref().unwrap().paths(), + vec!["docker-compose.yml"] + ); assert_eq!(config.service.as_deref(), Some("app")); assert_eq!(config.workspace_folder.as_deref(), Some("/workspace")); } @@ -242,10 +243,10 @@ mod tests { "service": "app" }"#; let config: DevcontainerJson = serde_json::from_str(json).unwrap(); - assert_eq!(config.docker_compose_file.as_ref().unwrap().paths(), vec![ - "docker-compose.yml", - "docker-compose.override.yml" - ]); + assert_eq!( + config.docker_compose_file.as_ref().unwrap().paths(), + vec!["docker-compose.yml", "docker-compose.override.yml"] + ); } #[test] diff --git a/lib/crates/fabro-github/src/lib.rs b/lib/crates/fabro-github/src/lib.rs index 9b47fbfe6..abb0dbd68 100644 --- a/lib/crates/fabro-github/src/lib.rs +++ b/lib/crates/fabro-github/src/lib.rs @@ -13,21 +13,21 @@ pub fn github_api_base_url() -> String { /// Detailed information about a pull request from the GitHub API. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct PullRequestDetail { - pub number: u64, - pub title: String, - pub body: Option, - pub state: String, - pub draft: bool, - pub mergeable: Option, - pub additions: u64, - pub deletions: u64, + pub number: u64, + pub title: String, + pub body: Option, + pub state: String, + pub draft: bool, + pub mergeable: Option, + pub additions: u64, + pub deletions: u64, pub changed_files: u64, - pub html_url: String, - pub user: PullRequestUser, - pub head: PullRequestRef, - pub base: PullRequestRef, - pub created_at: String, - pub updated_at: String, + pub html_url: String, + pub user: PullRequestUser, + pub head: PullRequestRef, + pub base: PullRequestRef, + pub created_at: String, + pub updated_at: String, } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -50,14 +50,14 @@ pub struct AppOwner { /// Information about a GitHub App from the authenticated `/app` endpoint. #[derive(Debug, Clone, Deserialize)] pub struct AppInfo { - pub slug: String, + pub slug: String, pub owner: AppOwner, } /// Credentials for authenticating as a GitHub App. #[derive(Clone, Debug)] pub struct GitHubAppCredentials { - pub app_id: String, + pub app_id: String, pub private_key_pem: String, } @@ -106,7 +106,7 @@ pub enum HttpMethod { /// A minimal HTTP response for testability. pub struct HttpResponse { pub status: u16, - body: String, + body: String, } impl HttpResponse { @@ -386,8 +386,8 @@ pub async fn create_installation_access_token_for_pr( /// Result of a successful pull request creation. pub struct CreatedPullRequest { pub html_url: String, - pub number: u64, - pub node_id: String, + pub number: u64, + pub node_id: String, } /// Create a pull request on GitHub. @@ -409,8 +409,8 @@ pub async fn create_pull_request( #[derive(Deserialize)] struct PullRequestResponse { html_url: String, - number: u64, - node_id: String, + number: u64, + node_id: String, } let jwt = sign_app_jwt(&creds.app_id, &creds.private_key_pem)?; @@ -470,8 +470,8 @@ pub async fn create_pull_request( Ok(CreatedPullRequest { html_url: pr.html_url, - number: pr.number, - node_id: pr.node_id, + number: pr.number, + node_id: pr.node_id, }) } @@ -1219,11 +1219,11 @@ mod tests { // ----------------------------------------------------------------------- struct MockRoute { - method: HttpMethod, - path: String, - status: u16, - response_body: String, - assert_header: Option<(String, MockHeaderCheck)>, + method: HttpMethod, + path: String, + status: u16, + response_body: String, + assert_header: Option<(String, MockHeaderCheck)>, assert_body_json: Option, } @@ -1470,7 +1470,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }; let result = @@ -1502,7 +1502,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }; let result = @@ -1534,7 +1534,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }; let result = branch_exists_with_client(&mock, &creds, "owner", "repo", "broken", "").await; @@ -1702,7 +1702,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }; let detail = get_pull_request_with_client(&mock, &creds, "owner", "repo", 42, "") @@ -1739,7 +1739,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }; let err = get_pull_request_with_client(&mock, &creds, "owner", "repo", 999, "") @@ -1777,7 +1777,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }; merge_pull_request_with_client(&mock, &creds, "owner", "repo", 42, "squash", "") @@ -1804,7 +1804,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }; let err = merge_pull_request_with_client(&mock, &creds, "owner", "repo", 42, "squash", "") @@ -1832,7 +1832,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }; let err = merge_pull_request_with_client(&mock, &creds, "owner", "repo", 42, "squash", "") @@ -1869,7 +1869,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }; close_pull_request_with_client(&mock, &creds, "owner", "repo", 42, "") @@ -1896,7 +1896,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }; let err = close_pull_request_with_client(&mock, &creds, "owner", "repo", 999, "") diff --git a/lib/crates/fabro-github/tests/integration.rs b/lib/crates/fabro-github/tests/integration.rs index 47fa65e54..8410cde49 100644 --- a/lib/crates/fabro-github/tests/integration.rs +++ b/lib/crates/fabro-github/tests/integration.rs @@ -9,7 +9,7 @@ const TEST_RSA_KEY: &str = include_str!("../src/testdata/rsa_private.pem"); fn github_credentials() -> GitHubAppCredentials { GitHubAppCredentials { - app_id: "42".to_string(), + app_id: "42".to_string(), private_key_pem: TEST_RSA_KEY.to_string(), } } @@ -17,12 +17,12 @@ fn github_credentials() -> GitHubAppCredentials { fn standard_app_state() -> GitHubAppState { let mut state = GitHubAppState::new(); state.register_app(GitHubAppOptions { - app_id: "42".into(), - slug: "test-app".into(), - owner_login: "acme".into(), - public: true, + app_id: "42".into(), + slug: "test-app".into(), + owner_login: "acme".into(), + public: true, private_key_pem: TEST_RSA_KEY.into(), - webhook_secret: None, + webhook_secret: None, }); state.add_installation("42", "acme", vec!["widgets".into()], false); state.add_repository( diff --git a/lib/crates/fabro-graphviz/src/condition.rs b/lib/crates/fabro-graphviz/src/condition.rs index 3532f7507..ca3ab63f0 100644 --- a/lib/crates/fabro-graphviz/src/condition.rs +++ b/lib/crates/fabro-graphviz/src/condition.rs @@ -12,7 +12,7 @@ /// Literal ::= String | Integer | Boolean | BareLiteral /// BareLiteral ::= [A-Za-z_][A-Za-z0-9_.:-]* /// ``` -use crate::error::GraphvizError; +use crate::error::Error; // --------------------------------------------------------------------------- // AST @@ -28,8 +28,8 @@ pub enum ConditionExpr { #[derive(Debug, Clone, PartialEq)] pub struct Clause { - pub key: String, - pub op: Op, + pub key: String, + pub op: Op, pub value: String, } @@ -66,7 +66,7 @@ enum Token { Matches, // matches } -fn tokenize(input: &str) -> Result, GraphvizError> { +fn tokenize(input: &str) -> Result, Error> { let input = input.trim(); if input.is_empty() { return Ok(Vec::new()); @@ -166,7 +166,7 @@ fn tokenize(input: &str) -> Result, GraphvizError> { i += 1; } if i == start { - return Err(GraphvizError::Parse(format!( + return Err(Error::Parse(format!( "unexpected character '{}' in condition expression", chars[i] ))); @@ -207,7 +207,7 @@ fn is_word_operator_context(tokens: &[Token]) -> bool { struct Parser { tokens: Vec, - pos: usize, + pos: usize, } impl Parser { @@ -227,11 +227,11 @@ impl Parser { tok } - fn parse_expr(&mut self) -> Result { + fn parse_expr(&mut self) -> Result { self.parse_or() } - fn parse_or(&mut self) -> Result { + fn parse_or(&mut self) -> Result { let mut children = vec![self.parse_and()?]; while self.peek() == Some(&Token::Or) { self.advance(); @@ -244,7 +244,7 @@ impl Parser { } } - fn parse_and(&mut self) -> Result { + fn parse_and(&mut self) -> Result { let mut children = vec![self.parse_unary()?]; while self.peek() == Some(&Token::And) { self.advance(); @@ -257,7 +257,7 @@ impl Parser { } } - fn parse_unary(&mut self) -> Result { + fn parse_unary(&mut self) -> Result { if self.peek() == Some(&Token::Not) { self.advance(); let inner = self.parse_unary()?; @@ -266,16 +266,16 @@ impl Parser { self.parse_clause() } - fn parse_clause(&mut self) -> Result { + fn parse_clause(&mut self) -> Result { let key = match self.advance() { Some(Token::Word(w)) => w, Some(other) => { - return Err(GraphvizError::Parse(format!( + return Err(Error::Parse(format!( "expected key, got {other:?} in condition expression" ))); } None => { - return Err(GraphvizError::Parse( + return Err(Error::Parse( "unexpected end of condition expression".to_string(), )); } @@ -309,7 +309,7 @@ impl Parser { let value = match self.advance() { Some(Token::Word(w)) => parse_literal(&w), Some(other) => { - return Err(GraphvizError::Parse(format!( + return Err(Error::Parse(format!( "expected value after operator, got {other:?}" ))); } @@ -318,7 +318,7 @@ impl Parser { if op == Op::Eq || op == Op::NotEq { String::new() } else { - return Err(GraphvizError::Parse( + return Err(Error::Parse( "expected value after operator".to_string(), )); } @@ -328,7 +328,7 @@ impl Parser { // Validate regex at parse time if op == Op::Matches { regex::Regex::new(&value).map_err(|e| { - GraphvizError::Parse(format!("invalid regex pattern '{value}': {e}")) + Error::Parse(format!("invalid regex pattern '{value}': {e}")) })?; } @@ -349,7 +349,7 @@ fn parse_literal(raw: &str) -> String { } } -fn parse_expression(expr: &str) -> Result { +fn parse_expression(expr: &str) -> Result { let tokens = tokenize(expr)?; if tokens.is_empty() { return Ok(ConditionExpr::And(Vec::new())); @@ -357,7 +357,7 @@ fn parse_expression(expr: &str) -> Result { let mut parser = Parser::new(tokens); let result = parser.parse_expr()?; if parser.pos < parser.tokens.len() { - return Err(GraphvizError::Parse(format!( + return Err(Error::Parse(format!( "unexpected token {:?} in condition expression", parser.tokens[parser.pos] ))); @@ -370,7 +370,7 @@ fn parse_expression(expr: &str) -> Result { /// # Errors /// /// Returns an error if the expression contains invalid syntax. -pub fn parse_condition(expr: &str) -> Result<(), GraphvizError> { +pub fn parse_condition(expr: &str) -> Result<(), Error> { parse_expression(expr)?; Ok(()) } @@ -380,7 +380,7 @@ pub fn parse_condition(expr: &str) -> Result<(), GraphvizError> { /// # Errors /// /// Returns an error if the expression contains invalid syntax. -pub fn parse_condition_expr(expr: &str) -> Result { +pub fn parse_condition_expr(expr: &str) -> Result { parse_expression(expr) } @@ -406,8 +406,8 @@ mod tests { assert_eq!( expr, ConditionExpr::Clause(Clause { - key: "outcome".to_string(), - op: Op::Eq, + key: "outcome".to_string(), + op: Op::Eq, value: "success".to_string(), }) ); @@ -420,13 +420,13 @@ mod tests { expr, ConditionExpr::And(vec![ ConditionExpr::Clause(Clause { - key: "a".to_string(), - op: Op::Eq, + key: "a".to_string(), + op: Op::Eq, value: "1".to_string(), }), ConditionExpr::Clause(Clause { - key: "b".to_string(), - op: Op::Eq, + key: "b".to_string(), + op: Op::Eq, value: "2".to_string(), }), ]) @@ -439,8 +439,8 @@ mod tests { assert_eq!( expr, ConditionExpr::Clause(Clause { - key: "some_flag".to_string(), - op: Op::Truthy, + key: "some_flag".to_string(), + op: Op::Truthy, value: String::new(), }) ); @@ -452,8 +452,8 @@ mod tests { assert_eq!( expr, ConditionExpr::Clause(Clause { - key: "outcome".to_string(), - op: Op::NotEq, + key: "outcome".to_string(), + op: Op::NotEq, value: "fail".to_string(), }) ); @@ -532,8 +532,8 @@ mod tests { assert_eq!( expr, ConditionExpr::Clause(Clause { - key: "outcome".to_string(), - op: Op::Eq, + key: "outcome".to_string(), + op: Op::Eq, value: "success".to_string(), }) ); @@ -552,8 +552,8 @@ mod tests { assert_eq!( expr, ConditionExpr::Clause(Clause { - key: "outcome".to_string(), - op: Op::NotEq, + key: "outcome".to_string(), + op: Op::NotEq, value: "fail".to_string(), }) ); @@ -565,8 +565,8 @@ mod tests { assert_eq!( expr, ConditionExpr::Clause(Clause { - key: "context.msg".to_string(), - op: Op::Eq, + key: "context.msg".to_string(), + op: Op::Eq, value: "hello world".to_string(), }) ); diff --git a/lib/crates/fabro-graphviz/src/error.rs b/lib/crates/fabro-graphviz/src/error.rs index 16024d804..b710c1dc7 100644 --- a/lib/crates/fabro-graphviz/src/error.rs +++ b/lib/crates/fabro-graphviz/src/error.rs @@ -10,4 +10,3 @@ pub enum Error { } pub type Result = std::result::Result; -pub type GraphvizError = Error; diff --git a/lib/crates/fabro-graphviz/src/lib.rs b/lib/crates/fabro-graphviz/src/lib.rs index 7d5d92e01..ac4a2562f 100644 --- a/lib/crates/fabro-graphviz/src/lib.rs +++ b/lib/crates/fabro-graphviz/src/lib.rs @@ -6,5 +6,5 @@ pub mod parser; pub mod render; pub mod stylesheet; -pub use error::{Error, GraphvizError, Result}; +pub use error::{Error, Result}; pub use fidelity::Fidelity; diff --git a/lib/crates/fabro-graphviz/src/parser/ast.rs b/lib/crates/fabro-graphviz/src/parser/ast.rs index 8aa50e449..8ba32cda7 100644 --- a/lib/crates/fabro-graphviz/src/parser/ast.rs +++ b/lib/crates/fabro-graphviz/src/parser/ast.rs @@ -18,7 +18,7 @@ pub type AttrBlock = Vec<(String, AstValue)>; /// A node statement: `id [attrs]?`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct NodeStmt { - pub id: String, + pub id: String, pub attrs: Option, } @@ -33,7 +33,7 @@ pub struct EdgeStmt { /// A subgraph statement: `subgraph name? { stmts }`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SubgraphStmt { - pub name: Option, + pub name: Option, pub statements: Vec, } @@ -59,7 +59,7 @@ pub enum Statement { /// The top-level parsed DOT graph. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct DotGraph { - pub name: String, + pub name: String, pub statements: Vec, } @@ -85,11 +85,11 @@ mod tests { #[test] fn dot_graph_construction() { let graph = DotGraph { - name: "test".into(), + name: "test".into(), statements: vec![ Statement::GraphAttrDecl("rankdir".into(), AstValue::Ident("LR".into())), Statement::Node(NodeStmt { - id: "start".into(), + id: "start".into(), attrs: Some(vec![("shape".into(), AstValue::Ident("Mdiamond".into()))]), }), ], @@ -110,7 +110,7 @@ mod tests { #[test] fn subgraph_stmt() { let sub = SubgraphStmt { - name: Some("cluster_loop".into()), + name: Some("cluster_loop".into()), statements: vec![Statement::NodeDefaults(vec![( "timeout".into(), AstValue::Str("900s".into()), diff --git a/lib/crates/fabro-graphviz/src/parser/grammar.rs b/lib/crates/fabro-graphviz/src/parser/grammar.rs index 8cd4d1b87..cd37b1825 100644 --- a/lib/crates/fabro-graphviz/src/parser/grammar.rs +++ b/lib/crates/fabro-graphviz/src/parser/grammar.rs @@ -73,7 +73,7 @@ fn subgraph_stmt(input: &str) -> IResult<&str, Statement> { Ok(( rest, Statement::Subgraph(SubgraphStmt { - name: name.map(String::from), + name: name.map(String::from), statements: stmts, }), )) @@ -141,10 +141,13 @@ pub fn parse_dot_graph(input: &str) -> IResult<&str, DotGraph> { let (rest, _) = preceded(ws, char('{'))(rest)?; let (rest, stmts) = many0(statement)(rest)?; let (rest, _) = preceded(ws, char('}'))(rest)?; - Ok((rest, DotGraph { - name: name.to_string(), - statements: stmts, - })) + Ok(( + rest, + DotGraph { + name: name.to_string(), + statements: stmts, + }, + )) } // We need arrow to work with explicit error types diff --git a/lib/crates/fabro-graphviz/src/parser/mod.rs b/lib/crates/fabro-graphviz/src/parser/mod.rs index d7ad10e30..4f05b724d 100644 --- a/lib/crates/fabro-graphviz/src/parser/mod.rs +++ b/lib/crates/fabro-graphviz/src/parser/mod.rs @@ -4,7 +4,7 @@ pub mod lexer; pub mod semantic; use self::ast::DotGraph; -use crate::error::GraphvizError; +use crate::error::Error; use crate::graph::types::Graph; /// Parse a DOT source string into a raw `DotGraph` AST. @@ -16,14 +16,14 @@ use crate::graph::types::Graph; /// /// Returns an error if the input is not valid DOT syntax or contains /// trailing content after the graph definition. -pub fn parse_ast(input: &str) -> Result { +pub fn parse_ast(input: &str) -> Result { let stripped = lexer::strip_comments(input); let (rest, dot_graph) = grammar::parse_dot_graph(&stripped) - .map_err(|e| GraphvizError::Parse(format!("grammar error: {e}")))?; + .map_err(|e| Error::Parse(format!("grammar error: {e}")))?; let remaining = rest.trim(); if !remaining.is_empty() { - return Err(GraphvizError::Parse(format!( + return Err(Error::Parse(format!( "unexpected trailing content: {:?}", &remaining[..remaining.len().min(50)] ))); @@ -41,7 +41,7 @@ pub fn parse_ast(input: &str) -> Result { /// /// Returns an error if the input is not valid DOT syntax or contains /// trailing content after the graph definition. -pub fn parse(input: &str) -> Result { +pub fn parse(input: &str) -> Result { let dot_graph = parse_ast(input)?; semantic::ast_to_graph(&dot_graph) } diff --git a/lib/crates/fabro-graphviz/src/parser/semantic.rs b/lib/crates/fabro-graphviz/src/parser/semantic.rs index db94d1a1e..eb82fa16b 100644 --- a/lib/crates/fabro-graphviz/src/parser/semantic.rs +++ b/lib/crates/fabro-graphviz/src/parser/semantic.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::time::Duration; -use crate::error::GraphvizError; +use crate::error::Error; use crate::graph::types::{AttrValue, Edge, Graph, Node}; use crate::parser::ast::{AstValue, AttrBlock, DotGraph, EdgeStmt, NodeStmt, Statement}; @@ -58,7 +58,7 @@ fn derive_class_from_label(label: &str) -> String { } struct SemanticState { - graph: Graph, + graph: Graph, node_defaults: HashMap, edge_defaults: HashMap, } @@ -66,7 +66,7 @@ struct SemanticState { impl SemanticState { fn new(name: String) -> Self { Self { - graph: Graph::new(name), + graph: Graph::new(name), node_defaults: HashMap::new(), edge_defaults: HashMap::new(), } @@ -259,7 +259,7 @@ impl SemanticState { /// # Errors /// /// Returns an error if the AST cannot be converted to a valid graph. -pub fn ast_to_graph(dot: &DotGraph) -> Result { +pub fn ast_to_graph(dot: &DotGraph) -> Result { let mut state = SemanticState::new(dot.name.clone()); let empty = HashMap::new(); state.process_statements(&dot.statements, None, &empty, &empty); @@ -342,26 +342,26 @@ mod tests { #[test] fn ast_to_graph_simple_linear() { let dot = DotGraph { - name: "Simple".into(), + name: "Simple".into(), statements: vec![ Statement::GraphAttr(vec![("goal".into(), AstValue::Str("Run tests".into()))]), Statement::GraphAttrDecl("rankdir".into(), AstValue::Ident("LR".into())), Statement::Node(NodeStmt { - id: "start".into(), + id: "start".into(), attrs: Some(vec![ ("shape".into(), AstValue::Ident("Mdiamond".into())), ("label".into(), AstValue::Str("Start".into())), ]), }), Statement::Node(NodeStmt { - id: "exit".into(), + id: "exit".into(), attrs: Some(vec![ ("shape".into(), AstValue::Ident("Msquare".into())), ("label".into(), AstValue::Str("Exit".into())), ]), }), Statement::Node(NodeStmt { - id: "run_tests".into(), + id: "run_tests".into(), attrs: Some(vec![("label".into(), AstValue::Str("Run Tests".into()))]), }), Statement::Edge(EdgeStmt { @@ -385,18 +385,18 @@ mod tests { #[test] fn ast_to_graph_node_defaults_applied() { let dot = DotGraph { - name: "Defaults".into(), + name: "Defaults".into(), statements: vec![ Statement::NodeDefaults(vec![ ("shape".into(), AstValue::Ident("box".into())), ("timeout".into(), AstValue::Str("900s".into())), ]), Statement::Node(NodeStmt { - id: "plan".into(), + id: "plan".into(), attrs: Some(vec![("label".into(), AstValue::Str("Plan".into()))]), }), Statement::Node(NodeStmt { - id: "implement".into(), + id: "implement".into(), attrs: Some(vec![ ("label".into(), AstValue::Str("Implement".into())), ("timeout".into(), AstValue::Str("1800s".into())), @@ -429,13 +429,13 @@ mod tests { #[test] fn ast_to_graph_subgraph_class_derivation() { let dot = DotGraph { - name: "SubgraphTest".into(), + name: "SubgraphTest".into(), statements: vec![Statement::Subgraph(SubgraphStmt { - name: Some("cluster_loop".into()), + name: Some("cluster_loop".into()), statements: vec![ Statement::GraphAttrDecl("label".into(), AstValue::Str("Loop A".into())), Statement::Node(NodeStmt { - id: "plan".into(), + id: "plan".into(), attrs: None, }), ], @@ -450,16 +450,16 @@ mod tests { #[test] fn ast_to_graph_subgraph_class_from_graph_attr_block() { let dot = DotGraph { - name: "SubgraphAttrBlock".into(), + name: "SubgraphAttrBlock".into(), statements: vec![Statement::Subgraph(SubgraphStmt { - name: Some("cluster_review".into()), + name: Some("cluster_review".into()), statements: vec![ Statement::GraphAttr(vec![( "label".into(), AstValue::Str("Code Review".into()), )]), Statement::Node(NodeStmt { - id: "reviewer".into(), + id: "reviewer".into(), attrs: None, }), ], @@ -474,7 +474,7 @@ mod tests { #[test] fn ast_to_graph_edge_defaults_applied() { let dot = DotGraph { - name: "EdgeDefaults".into(), + name: "EdgeDefaults".into(), statements: vec![ Statement::EdgeDefaults(vec![("weight".into(), AstValue::Int(5))]), Statement::Edge(EdgeStmt { @@ -491,7 +491,7 @@ mod tests { #[test] fn ast_to_graph_chained_edges_with_attrs() { let dot = DotGraph { - name: "Chained".into(), + name: "Chained".into(), statements: vec![Statement::Edge(EdgeStmt { nodes: vec!["a".into(), "b".into(), "c".into()], attrs: Some(vec![("label".into(), AstValue::Str("next".into()))]), @@ -507,9 +507,9 @@ mod tests { #[test] fn ast_to_graph_class_attr_parsed() { let dot = DotGraph { - name: "ClassTest".into(), + name: "ClassTest".into(), statements: vec![Statement::Node(NodeStmt { - id: "review".into(), + id: "review".into(), attrs: Some(vec![( "class".into(), AstValue::Str("code,critical".into()), @@ -526,7 +526,7 @@ mod tests { #[test] fn ast_to_graph_implicit_nodes_from_edges() { let dot = DotGraph { - name: "Implicit".into(), + name: "Implicit".into(), statements: vec![Statement::Edge(EdgeStmt { nodes: vec!["a".into(), "b".into()], attrs: None, @@ -541,17 +541,17 @@ mod tests { #[test] fn codergen_mode_legacy_translates_to_type() { let dot = DotGraph { - name: "Legacy".into(), + name: "Legacy".into(), statements: vec![ Statement::Node(NodeStmt { - id: "classify".into(), + id: "classify".into(), attrs: Some(vec![( "codergen_mode".into(), AstValue::Str("one_shot".into()), )]), }), Statement::Node(NodeStmt { - id: "work".into(), + id: "work".into(), attrs: Some(vec![( "codergen_mode".into(), AstValue::Str("agent_loop".into()), @@ -580,9 +580,9 @@ mod tests { #[test] fn codergen_mode_does_not_override_explicit_type() { let dot = DotGraph { - name: "ExplicitType".into(), + name: "ExplicitType".into(), statements: vec![Statement::Node(NodeStmt { - id: "gate".into(), + id: "gate".into(), attrs: Some(vec![ ("type".into(), AstValue::Str("human".into())), ("codergen_mode".into(), AstValue::Str("one_shot".into())), diff --git a/lib/crates/fabro-graphviz/src/stylesheet.rs b/lib/crates/fabro-graphviz/src/stylesheet.rs index 79a8eb2a8..2b9a444e9 100644 --- a/lib/crates/fabro-graphviz/src/stylesheet.rs +++ b/lib/crates/fabro-graphviz/src/stylesheet.rs @@ -1,4 +1,4 @@ -use crate::error::GraphvizError; +use crate::error::Error; /// A parsed stylesheet selector. #[derive(Debug, Clone, PartialEq, Eq)] @@ -29,13 +29,13 @@ impl Selector { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Declaration { pub property: String, - pub value: String, + pub value: String, } /// A stylesheet rule: selector + declarations. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Rule { - pub selector: Selector, + pub selector: Selector, pub declarations: Vec, } @@ -50,7 +50,7 @@ pub struct Stylesheet { /// # Errors /// /// Returns an error if the input contains invalid stylesheet syntax. -pub fn parse_stylesheet(input: &str) -> Result { +pub fn parse_stylesheet(input: &str) -> Result { let input = input.trim(); if input.is_empty() { return Ok(Stylesheet { rules: Vec::new() }); @@ -64,7 +64,7 @@ pub fn parse_stylesheet(input: &str) -> Result { let selector = parse_selector(&mut remaining)?; if !remaining.starts_with('{') { - return Err(GraphvizError::Stylesheet(format!( + return Err(Error::Stylesheet(format!( "expected '{{' after selector, got: {:?}", &remaining[..remaining.len().min(20)] ))); @@ -83,7 +83,7 @@ pub fn parse_stylesheet(input: &str) -> Result { Ok(Stylesheet { rules }) } -fn parse_selector(remaining: &mut &str) -> Result { +fn parse_selector(remaining: &mut &str) -> Result { if remaining.starts_with('*') { *remaining = remaining[1..].trim(); Ok(Selector::Universal) @@ -93,7 +93,7 @@ fn parse_selector(remaining: &mut &str) -> Result { .find(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != '-') .unwrap_or(remaining.len()); if end == 0 { - return Err(GraphvizError::Stylesheet( + return Err(Error::Stylesheet( "expected identifier after '#'".into(), )); } @@ -106,7 +106,7 @@ fn parse_selector(remaining: &mut &str) -> Result { .find(|c: char| !c.is_ascii_lowercase() && !c.is_ascii_digit() && c != '-') .unwrap_or(remaining.len()); if end == 0 { - return Err(GraphvizError::Stylesheet( + return Err(Error::Stylesheet( "expected class name after '.'".into(), )); } @@ -119,7 +119,7 @@ fn parse_selector(remaining: &mut &str) -> Result { .find(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != '-') .unwrap_or(remaining.len()); if end == 0 { - return Err(GraphvizError::Stylesheet(format!( + return Err(Error::Stylesheet(format!( "expected selector ('*', '#id', '.class', or shape name), got: {:?}", &remaining[..remaining.len().min(20)] ))); @@ -130,11 +130,11 @@ fn parse_selector(remaining: &mut &str) -> Result { } } -fn parse_declarations(remaining: &mut &str) -> Result, GraphvizError> { +fn parse_declarations(remaining: &mut &str) -> Result, Error> { let mut declarations = Vec::new(); while !remaining.starts_with('}') { if remaining.is_empty() { - return Err(GraphvizError::Stylesheet( + return Err(Error::Stylesheet( "unexpected end of stylesheet, expected '}'".into(), )); } @@ -150,7 +150,7 @@ fn parse_declarations(remaining: &mut &str) -> Result, Graphviz *remaining = remaining[prop_end..].trim(); if !remaining.starts_with(':') { - return Err(GraphvizError::Stylesheet(format!( + return Err(Error::Stylesheet(format!( "expected ':' after property name '{property}'" ))); } @@ -161,7 +161,7 @@ fn parse_declarations(remaining: &mut &str) -> Result, Graphviz *remaining = remaining[val_end..].trim(); if value.is_empty() { - return Err(GraphvizError::Stylesheet(format!( + return Err(Error::Stylesheet(format!( "empty value for property '{property}'" ))); } diff --git a/lib/crates/fabro-hooks/src/bridge.rs b/lib/crates/fabro-hooks/src/bridge.rs index 0c51172a5..08765a764 100644 --- a/lib/crates/fabro-hooks/src/bridge.rs +++ b/lib/crates/fabro-hooks/src/bridge.rs @@ -12,12 +12,12 @@ use crate::types::{HookContext, HookDecision, HookEvent}; /// Created per-node in the workflow engine, capturing the `HookRunner` and /// context needed to build `HookContext` for tool-level events. pub struct WorkflowToolHookCallback { - pub hook_runner: Arc, - pub sandbox: Arc, - pub run_id: RunId, + pub hook_runner: Arc, + pub sandbox: Arc, + pub run_id: RunId, pub workflow_name: String, - pub work_dir: Option, - pub node_id: String, + pub work_dir: Option, + pub node_id: String, } impl WorkflowToolHookCallback { @@ -84,7 +84,7 @@ mod tests { struct CapturingExecutor { captured_contexts: Arc>>, - decision: HookDecision, + decision: HookDecision, } #[async_trait::async_trait] @@ -98,8 +98,8 @@ mod tests { ) -> HookResult { self.captured_contexts.lock().unwrap().push(context.clone()); HookResult { - hook_name: None, - decision: self.decision.clone(), + hook_name: None, + decision: self.decision.clone(), duration_ms: 1, } } @@ -143,7 +143,7 @@ mod tests { let captured = Arc::new(Mutex::new(Vec::new())); let executor = Arc::new(CapturingExecutor { captured_contexts: captured.clone(), - decision: HookDecision::Proceed, + decision: HookDecision::Proceed, }); let config = HookSettings { hooks: vec![make_hook(HookEvent::PreToolUse)], @@ -172,7 +172,7 @@ mod tests { async fn pre_tool_use_maps_block_decision() { let executor = Arc::new(CapturingExecutor { captured_contexts: Arc::new(Mutex::new(Vec::new())), - decision: HookDecision::Block { + decision: HookDecision::Block { reason: Some("forbidden".into()), }, }); @@ -184,16 +184,19 @@ mod tests { let bridge = make_bridge(runner, sandbox); let decision = bridge.pre_tool_use("shell", &serde_json::json!({})).await; - assert_eq!(decision, ToolHookDecision::Block { - reason: "forbidden".to_string(), - }); + assert_eq!( + decision, + ToolHookDecision::Block { + reason: "forbidden".to_string(), + } + ); } #[tokio::test] async fn pre_tool_use_maps_proceed() { let executor = Arc::new(CapturingExecutor { captured_contexts: Arc::new(Mutex::new(Vec::new())), - decision: HookDecision::Proceed, + decision: HookDecision::Proceed, }); let config = HookSettings { hooks: vec![make_hook(HookEvent::PreToolUse)], @@ -211,7 +214,7 @@ mod tests { let captured = Arc::new(Mutex::new(Vec::new())); let executor = Arc::new(CapturingExecutor { captured_contexts: captured.clone(), - decision: HookDecision::Proceed, + decision: HookDecision::Proceed, }); let config = HookSettings { hooks: vec![make_hook(HookEvent::PostToolUse)], @@ -240,7 +243,7 @@ mod tests { let captured = Arc::new(Mutex::new(Vec::new())); let executor = Arc::new(CapturingExecutor { captured_contexts: captured.clone(), - decision: HookDecision::Proceed, + decision: HookDecision::Proceed, }); let config = HookSettings { hooks: vec![make_hook(HookEvent::PostToolUseFailure)], diff --git a/lib/crates/fabro-hooks/src/config.rs b/lib/crates/fabro-hooks/src/config.rs index a6205c967..d20ef3bd8 100644 --- a/lib/crates/fabro-hooks/src/config.rs +++ b/lib/crates/fabro-hooks/src/config.rs @@ -94,20 +94,20 @@ pub enum HookType { command: String, }, Http { - url: String, - headers: Option>, + url: String, + headers: Option>, #[serde(default)] allowed_env_vars: Vec, #[serde(default)] - tls: TlsMode, + tls: TlsMode, }, Prompt { prompt: String, - model: Option, + model: Option, }, Agent { - prompt: String, - model: Option, + prompt: String, + model: Option, max_tool_rounds: Option, }, } @@ -115,23 +115,23 @@ pub enum HookType { /// A single hook definition. #[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] pub struct HookDefinition { - pub name: Option, - pub event: HookEvent, + pub name: Option, + pub event: HookEvent, /// Inline command shorthand — if set, implies `type = "command"`. #[serde(default)] - pub command: Option, + pub command: Option, /// Explicit hook type (command or http). If omitted and `command` is set, /// defaults to `Command`. #[serde(flatten)] - pub hook_type: Option, + pub hook_type: Option, /// Regex matched against node_id, handler_type, or event-specific fields. - pub matcher: Option, + pub matcher: Option, /// Override the event's default blocking behavior. - pub blocking: Option, + pub blocking: Option, /// Timeout in milliseconds (default: 60_000). pub timeout_ms: Option, /// Run inside the sandbox (true, default) or on the host (false). - pub sandbox: Option, + pub sandbox: Option, } impl HookDefinition { diff --git a/lib/crates/fabro-hooks/src/executor.rs b/lib/crates/fabro-hooks/src/executor.rs index 807d69ef7..5b84a7d5e 100644 --- a/lib/crates/fabro-hooks/src/executor.rs +++ b/lib/crates/fabro-hooks/src/executor.rs @@ -374,19 +374,19 @@ impl HookExecutorImpl { for _ in 0..rounds { let request = Request { - model: resolved_model.clone(), - messages: messages.clone(), - provider: None, - tools: Some(tool_defs.clone()), - tool_choice: None, - response_format: None, - temperature: None, - top_p: None, - max_tokens: None, - stop_sequences: None, + model: resolved_model.clone(), + messages: messages.clone(), + provider: None, + tools: Some(tool_defs.clone()), + tool_choice: None, + response_format: None, + temperature: None, + top_p: None, + max_tokens: None, + stop_sequences: None, reasoning_effort: None, - speed: None, - metadata: None, + speed: None, + metadata: None, provider_options: None, }; @@ -408,8 +408,8 @@ impl HookExecutorImpl { for tc in &tool_calls { let tool = registry.get(&tc.name).cloned(); let ctx = ToolContext { - env: sandbox.clone(), - cancel: cancel.child_token(), + env: sandbox.clone(), + cancel: cancel.child_token(), tool_env: None, }; let result = match tool { @@ -560,17 +560,17 @@ impl HookExecutorImpl { /// Cached HTTP clients keyed by TLS mode. struct HttpClientCache { - verify: reqwest::Client, + verify: reqwest::Client, no_verify: reqwest::Client, - off: reqwest::Client, + off: reqwest::Client, } impl HttpClientCache { fn new() -> Self { Self { - verify: HookExecutorImpl::build_http_client(TlsMode::Verify), + verify: HookExecutorImpl::build_http_client(TlsMode::Verify), no_verify: HookExecutorImpl::build_http_client(TlsMode::NoVerify), - off: HookExecutorImpl::build_http_client(TlsMode::Off), + off: HookExecutorImpl::build_http_client(TlsMode::Off), } } @@ -710,14 +710,14 @@ mod tests { fn make_definition(command: &str) -> HookDefinition { HookDefinition { - name: Some("test-hook".into()), - event: HookEvent::StageStart, - command: Some(command.into()), - hook_type: None, - matcher: None, - blocking: None, + name: Some("test-hook".into()), + event: HookEvent::StageStart, + command: Some(command.into()), + hook_type: None, + matcher: None, + blocking: None, timeout_ms: Some(5000), - sandbox: Some(false), // host execution for tests + sandbox: Some(false), // host execution for tests } } @@ -816,9 +816,12 @@ mod tests { let ctx = make_context(); let sandbox = make_sandbox(); let result = executor.execute(&def, &ctx, sandbox, None).await; - assert_eq!(result.decision, HookDecision::Skip { - reason: Some("test skip".into()), - }); + assert_eq!( + result.decision, + HookDecision::Skip { + reason: Some("test skip".into()), + } + ); } #[tokio::test] @@ -837,14 +840,14 @@ mod tests { async fn no_hook_type_blocks() { let executor = HookExecutorImpl; let def = HookDefinition { - name: None, - event: HookEvent::StageStart, - command: None, - hook_type: None, - matcher: None, - blocking: None, + name: None, + event: HookEvent::StageStart, + command: None, + hook_type: None, + matcher: None, + blocking: None, timeout_ms: None, - sandbox: Some(false), + sandbox: Some(false), }; let ctx = make_context(); let sandbox = make_sandbox(); @@ -1006,9 +1009,12 @@ mod tests { .await; mock.assert_async().await; - assert_eq!(decision, HookDecision::Skip { - reason: Some("not needed".into()), - }); + assert_eq!( + decision, + HookDecision::Skip { + reason: Some("not needed".into()), + } + ); } #[tokio::test] @@ -1224,19 +1230,19 @@ mod tests { let executor = HookExecutorImpl; let def = HookDefinition { - name: Some("http-test".into()), - event: HookEvent::StageStart, - command: None, - hook_type: Some(HookType::Http { - url: server.url("/hook"), - headers: None, + name: Some("http-test".into()), + event: HookEvent::StageStart, + command: None, + hook_type: Some(HookType::Http { + url: server.url("/hook"), + headers: None, allowed_env_vars: vec![], - tls: TlsMode::Off, + tls: TlsMode::Off, }), - matcher: None, - blocking: None, + matcher: None, + blocking: None, timeout_ms: Some(5000), - sandbox: Some(false), + sandbox: Some(false), }; let ctx = make_context(); let sandbox = make_sandbox(); diff --git a/lib/crates/fabro-hooks/src/runner.rs b/lib/crates/fabro-hooks/src/runner.rs index 5c617dcab..3059c3f77 100644 --- a/lib/crates/fabro-hooks/src/runner.rs +++ b/lib/crates/fabro-hooks/src/runner.rs @@ -11,8 +11,8 @@ use crate::types::{HookContext, HookDecision}; /// Central orchestrator: filters matching hooks, executes them, merges /// decisions. pub struct HookRunner { - config: HookSettings, - executor: Arc, + config: HookSettings, + executor: Arc, /// Pre-compiled regexes keyed by matcher pattern string. compiled_matchers: HashMap, } @@ -231,8 +231,8 @@ mod tests { _work_dir: Option<&Path>, ) -> HookResult { HookResult { - hook_name: definition.name.clone(), - decision: self.decision.clone(), + hook_name: definition.name.clone(), + decision: self.decision.clone(), duration_ms: 1, } } diff --git a/lib/crates/fabro-hooks/src/types.rs b/lib/crates/fabro-hooks/src/types.rs index 28def4e70..5c7c8cfbb 100644 --- a/lib/crates/fabro-hooks/src/types.rs +++ b/lib/crates/fabro-hooks/src/types.rs @@ -6,41 +6,41 @@ pub use crate::config::HookEvent; /// Rich JSON payload sent to hooks. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HookContext { - pub event: HookEvent, - pub run_id: RunId, - pub workflow_name: String, + pub event: HookEvent, + pub run_id: RunId, + pub workflow_name: String, #[serde(default, skip_serializing_if = "Option::is_none")] - pub cwd: Option, + pub cwd: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub node_id: Option, + pub node_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub node_label: Option, + pub node_label: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub handler_type: Option, + pub handler_type: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub status: Option, + pub status: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub edge_from: Option, + pub edge_from: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub edge_to: Option, + pub edge_to: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub edge_label: Option, + pub edge_label: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub failure_reason: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub attempt: Option, + pub attempt: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_attempts: Option, + pub max_attempts: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_name: Option, + pub tool_name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_input: Option, + pub tool_input: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_call_id: Option, + pub tool_call_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_output: Option, + pub tool_output: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub error_message: Option, + pub error_message: Option, } impl HookContext { @@ -73,7 +73,7 @@ impl HookContext { /// Response returned by prompt/agent hooks from the LLM. #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct PromptHookResponse { - pub ok: bool, + pub ok: bool, #[serde(default)] pub reason: Option, } @@ -119,8 +119,8 @@ impl HookDecision { /// Result from executing a single hook. #[derive(Debug, Clone)] pub struct HookResult { - pub hook_name: Option, - pub decision: HookDecision, + pub hook_name: Option, + pub decision: HookDecision, pub duration_ms: u64, } @@ -133,25 +133,25 @@ mod tests { #[test] fn hook_context_serde_round_trip() { let ctx = HookContext { - event: HookEvent::StageStart, - run_id: fixtures::RUN_1, - workflow_name: "test-wf".into(), - cwd: Some("/tmp".into()), - node_id: Some("plan".into()), - node_label: Some("Plan".into()), - handler_type: Some("agent".into()), - status: None, - edge_from: None, - edge_to: None, - edge_label: None, + event: HookEvent::StageStart, + run_id: fixtures::RUN_1, + workflow_name: "test-wf".into(), + cwd: Some("/tmp".into()), + node_id: Some("plan".into()), + node_label: Some("Plan".into()), + handler_type: Some("agent".into()), + status: None, + edge_from: None, + edge_to: None, + edge_label: None, failure_reason: None, - attempt: Some(1), - max_attempts: Some(3), - tool_name: None, - tool_input: None, - tool_call_id: None, - tool_output: None, - error_message: None, + attempt: Some(1), + max_attempts: Some(3), + tool_name: None, + tool_input: None, + tool_call_id: None, + tool_output: None, + error_message: None, }; let json = serde_json::to_string(&ctx).unwrap(); let back: HookContext = serde_json::from_str(&json).unwrap(); diff --git a/lib/crates/fabro-interview/src/auto_approve.rs b/lib/crates/fabro-interview/src/auto_approve.rs index 8efb620ff..947e77ab5 100644 --- a/lib/crates/fabro-interview/src/auto_approve.rs +++ b/lib/crates/fabro-interview/src/auto_approve.rs @@ -15,9 +15,9 @@ impl Interviewer for AutoApproveInterviewer { question.options.first().map_or_else( || Answer::text("auto-approved"), |first| Answer { - value: AnswerValue::Selected(first.key.clone()), + value: AnswerValue::Selected(first.key.clone()), selected_option: Some(first.clone()), - text: None, + text: None, }, ) } @@ -53,11 +53,11 @@ mod tests { let mut q = Question::new("Choose:", QuestionType::MultipleChoice); q.options = vec![ QuestionOption { - key: "A".to_string(), + key: "A".to_string(), label: "Alpha".to_string(), }, QuestionOption { - key: "B".to_string(), + key: "B".to_string(), label: "Beta".to_string(), }, ]; @@ -66,7 +66,7 @@ mod tests { assert_eq!( answer.selected_option, Some(QuestionOption { - key: "A".to_string(), + key: "A".to_string(), label: "Alpha".to_string(), }) ); diff --git a/lib/crates/fabro-interview/src/console.rs b/lib/crates/fabro-interview/src/console.rs index e3217c3b5..dddb40492 100644 --- a/lib/crates/fabro-interview/src/console.rs +++ b/lib/crates/fabro-interview/src/console.rs @@ -34,9 +34,9 @@ fn find_matching_option(response: &str, options: &[QuestionOption]) -> Option Option= 1 && idx <= options.len() { let opt = &options[idx - 1]; return Some(Answer { - value: AnswerValue::Selected(opt.key.clone()), + value: AnswerValue::Selected(opt.key.clone()), selected_option: Some(opt.clone()), - text: None, + text: None, }); } } @@ -146,9 +146,9 @@ fn ask_select_interactive(question: &Question) -> Answer { Ok(Some(idx)) if idx < question.options.len() => { let opt = &question.options[idx]; Answer { - value: AnswerValue::Selected(opt.key.clone()), + value: AnswerValue::Selected(opt.key.clone()), selected_option: Some(opt.clone()), - text: None, + text: None, } } _ => Answer::interrupted(), @@ -277,11 +277,11 @@ mod tests { fn find_matching_option_by_key() { let options = vec![ crate::QuestionOption { - key: "A".to_string(), + key: "A".to_string(), label: "Approve".to_string(), }, crate::QuestionOption { - key: "R".to_string(), + key: "R".to_string(), label: "Reject".to_string(), }, ]; @@ -294,7 +294,7 @@ mod tests { #[test] fn find_matching_option_by_key_case_insensitive() { let options = vec![crate::QuestionOption { - key: "Y".to_string(), + key: "Y".to_string(), label: "Yes".to_string(), }]; let result = find_matching_option("y", &options); @@ -305,11 +305,11 @@ mod tests { fn find_matching_option_by_index() { let options = vec![ crate::QuestionOption { - key: "A".to_string(), + key: "A".to_string(), label: "Alpha".to_string(), }, crate::QuestionOption { - key: "B".to_string(), + key: "B".to_string(), label: "Beta".to_string(), }, ]; @@ -322,7 +322,7 @@ mod tests { #[test] fn find_matching_option_no_match() { let options = vec![crate::QuestionOption { - key: "A".to_string(), + key: "A".to_string(), label: "Alpha".to_string(), }]; let result = find_matching_option("zzz", &options); @@ -332,7 +332,7 @@ mod tests { #[test] fn find_matching_option_index_out_of_range() { let options = vec![crate::QuestionOption { - key: "A".to_string(), + key: "A".to_string(), label: "Alpha".to_string(), }]; let result = find_matching_option("5", &options); @@ -343,7 +343,7 @@ mod tests { fn non_tty_multiple_choice_eof_returns_interrupted() { let mut question = Question::new("Approve?", QuestionType::MultipleChoice); question.options = vec![crate::QuestionOption { - key: "A".to_string(), + key: "A".to_string(), label: "Approve".to_string(), }]; diff --git a/lib/crates/fabro-interview/src/control.rs b/lib/crates/fabro-interview/src/control.rs index 602c17d2b..418e1dfeb 100644 --- a/lib/crates/fabro-interview/src/control.rs +++ b/lib/crates/fabro-interview/src/control.rs @@ -12,8 +12,8 @@ pub enum SubmitError { #[derive(Default)] struct ControlInterviewerState { - pending: HashMap>, - queued: HashMap, + pending: HashMap>, + queued: HashMap, terminal_answer: Option, } diff --git a/lib/crates/fabro-interview/src/control_protocol.rs b/lib/crates/fabro-interview/src/control_protocol.rs index 56284cb70..f62783c33 100644 --- a/lib/crates/fabro-interview/src/control_protocol.rs +++ b/lib/crates/fabro-interview/src/control_protocol.rs @@ -6,7 +6,7 @@ pub const WORKER_CONTROL_PROTOCOL_VERSION: u8 = 1; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct WorkerControlEnvelope { - pub v: u8, + pub v: u8, #[serde(flatten)] pub message: WorkerControlMessage, } @@ -15,9 +15,9 @@ impl WorkerControlEnvelope { #[must_use] pub fn interview_answer(qid: impl Into, answer: Answer) -> Self { Self { - v: WORKER_CONTROL_PROTOCOL_VERSION, + v: WORKER_CONTROL_PROTOCOL_VERSION, message: WorkerControlMessage::InterviewAnswer { - qid: qid.into(), + qid: qid.into(), answer: answer.into(), }, } @@ -26,7 +26,7 @@ impl WorkerControlEnvelope { #[must_use] pub fn cancel_run() -> Self { Self { - v: WORKER_CONTROL_PROTOCOL_VERSION, + v: WORKER_CONTROL_PROTOCOL_VERSION, message: WorkerControlMessage::RunCancel, } } @@ -37,7 +37,7 @@ impl WorkerControlEnvelope { pub enum WorkerControlMessage { #[serde(rename = "interview.answer")] InterviewAnswer { - qid: String, + qid: String, answer: WorkerControlAnswer, }, #[serde(rename = "run.cancel")] @@ -84,9 +84,9 @@ impl From for Answer { WorkerControlAnswer::Skipped => Self::skipped(), WorkerControlAnswer::Timeout => Self::timeout(), WorkerControlAnswer::Selected { key } => Self { - value: AnswerValue::Selected(key), + value: AnswerValue::Selected(key), selected_option: None, - text: None, + text: None, }, WorkerControlAnswer::MultiSelected { keys } => Self::multi_selected(keys), WorkerControlAnswer::Text { text } => Self::text(text), diff --git a/lib/crates/fabro-interview/src/lib.rs b/lib/crates/fabro-interview/src/lib.rs index c057b2fc4..92965ce8d 100644 --- a/lib/crates/fabro-interview/src/lib.rs +++ b/lib/crates/fabro-interview/src/lib.rs @@ -38,7 +38,7 @@ impl std::fmt::Display for QuestionType { /// An option presented to the user for multiple-choice questions. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct QuestionOption { - pub key: String, + pub key: String, pub label: String, } @@ -46,15 +46,15 @@ pub struct QuestionOption { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Question { #[serde(default)] - pub id: String, - pub text: String, - pub question_type: QuestionType, - pub options: Vec, - pub allow_freeform: bool, - pub default: Option, + pub id: String, + pub text: String, + pub question_type: QuestionType, + pub options: Vec, + pub allow_freeform: bool, + pub default: Option, pub timeout_seconds: Option, - pub stage: String, - pub metadata: HashMap, + pub stage: String, + pub metadata: HashMap, #[serde(default)] pub context_display: Option, } @@ -93,89 +93,89 @@ pub enum AnswerValue { /// An answer from the user. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Answer { - pub value: AnswerValue, + pub value: AnswerValue, pub selected_option: Option, - pub text: Option, + pub text: Option, } impl Answer { #[must_use] pub fn yes() -> Self { Self { - value: AnswerValue::Yes, + value: AnswerValue::Yes, selected_option: None, - text: None, + text: None, } } #[must_use] pub fn no() -> Self { Self { - value: AnswerValue::No, + value: AnswerValue::No, selected_option: None, - text: None, + text: None, } } #[must_use] pub fn cancelled() -> Self { Self { - value: AnswerValue::Cancelled, + value: AnswerValue::Cancelled, selected_option: None, - text: None, + text: None, } } #[must_use] pub fn interrupted() -> Self { Self { - value: AnswerValue::Interrupted, + value: AnswerValue::Interrupted, selected_option: None, - text: None, + text: None, } } #[must_use] pub fn skipped() -> Self { Self { - value: AnswerValue::Skipped, + value: AnswerValue::Skipped, selected_option: None, - text: None, + text: None, } } #[must_use] pub fn timeout() -> Self { Self { - value: AnswerValue::Timeout, + value: AnswerValue::Timeout, selected_option: None, - text: None, + text: None, } } pub fn selected(key: impl Into, option: QuestionOption) -> Self { let key = key.into(); Self { - value: AnswerValue::Selected(key), + value: AnswerValue::Selected(key), selected_option: Some(option), - text: None, + text: None, } } pub fn multi_selected(keys: Vec) -> Self { Self { - value: AnswerValue::MultiSelected(keys), + value: AnswerValue::MultiSelected(keys), selected_option: None, - text: None, + text: None, } } pub fn text(text: impl Into) -> Self { let t = text.into(); Self { - value: AnswerValue::Text(t.clone()), + value: AnswerValue::Text(t.clone()), selected_option: None, - text: Some(t), + text: Some(t), } } } @@ -301,7 +301,7 @@ mod tests { #[test] fn answer_selected() { let opt = QuestionOption { - key: "A".to_string(), + key: "A".to_string(), label: "Approve".to_string(), }; let a = Answer::selected("A", opt.clone()); @@ -319,11 +319,11 @@ mod tests { #[test] fn question_option_eq() { let a = QuestionOption { - key: "Y".to_string(), + key: "Y".to_string(), label: "Yes".to_string(), }; let b = QuestionOption { - key: "Y".to_string(), + key: "Y".to_string(), label: "Yes".to_string(), }; assert_eq!(a, b); diff --git a/lib/crates/fabro-interview/src/recording.rs b/lib/crates/fabro-interview/src/recording.rs index 8c7ef8f2b..18f1e366d 100644 --- a/lib/crates/fabro-interview/src/recording.rs +++ b/lib/crates/fabro-interview/src/recording.rs @@ -7,7 +7,7 @@ use crate::{Answer, Interviewer, Question}; /// Wraps another interviewer and records all question-answer pairs. pub struct RecordingInterviewer { - inner: Box, + inner: Box, recordings: Mutex>, } diff --git a/lib/crates/fabro-llm/src/client.rs b/lib/crates/fabro-llm/src/client.rs index d6edcda4e..a0d4a6e2d 100644 --- a/lib/crates/fabro-llm/src/client.rs +++ b/lib/crates/fabro-llm/src/client.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use tracing::debug; -use crate::error::SdkError; +use crate::error::Error; use crate::middleware::{Middleware, NextFn, NextStreamFn}; use crate::provider::{ProviderAdapter, StreamEventStream}; use crate::providers; @@ -12,9 +12,9 @@ use crate::types::{Request, Response}; /// The core client that routes requests to provider adapters (Section 2.2, 3). #[derive(Clone)] pub struct Client { - providers: HashMap>, + providers: HashMap>, default_provider: Option, - middleware: Vec>, + middleware: Vec>, } impl Client { @@ -38,8 +38,8 @@ impl Client { /// /// # Errors /// - /// Returns `SdkError` if any provider adapter fails to initialize. - pub async fn from_env() -> Result { + /// Returns `Error` if any provider adapter fails to initialize. + pub async fn from_env() -> Result { Self::from_lookup(|name| std::env::var(name).ok()).await } @@ -48,14 +48,14 @@ impl Client { /// This is useful when credentials come from a source other than process /// environment variables, while still preserving the env-style provider /// configuration surface. - pub async fn from_lookup(lookup: F) -> Result + pub async fn from_lookup(lookup: F) -> Result where F: Fn(&str) -> Option, { let mut client = Self { - providers: HashMap::new(), + providers: HashMap::new(), default_provider: None, - middleware: Vec::new(), + middleware: Vec::new(), }; // Register providers whose API keys are present in the environment. @@ -134,11 +134,11 @@ impl Client { /// /// # Errors /// - /// Returns `SdkError` if the adapter's `initialize()` method fails. + /// Returns `Error` if the adapter's `initialize()` method fails. pub async fn register_provider( &mut self, adapter: Arc, - ) -> Result<(), SdkError> { + ) -> Result<(), Error> { adapter.initialize().await?; let name = adapter.name().to_string(); if self.default_provider.is_none() { @@ -155,7 +155,7 @@ impl Client { } /// Resolve the provider for a request. - fn resolve_provider(&self, request: &Request) -> Result, SdkError> { + fn resolve_provider(&self, request: &Request) -> Result, Error> { let catalog_provider = fabro_model::Catalog::builtin() .get(&request.model) .map(|info| info.provider.to_string()); @@ -165,17 +165,17 @@ impl Client { .as_deref() .or(catalog_provider.as_deref()) .or(self.default_provider.as_deref()) - .ok_or_else(|| SdkError::Configuration { + .ok_or_else(|| Error::Configuration { message: "No provider specified and no default provider set".into(), - source: None, + source: None, })?; self.providers .get(provider_name) .cloned() - .ok_or_else(|| SdkError::Configuration { + .ok_or_else(|| Error::Configuration { message: format!("Provider '{provider_name}' not registered"), - source: None, + source: None, }) } @@ -183,10 +183,10 @@ impl Client { /// /// # Errors /// - /// Returns `SdkError::Configuration` if no provider is specified or + /// Returns `Error::Configuration` if no provider is specified or /// registered, or any provider/middleware error encountered during the /// request. - pub async fn complete(&self, request: &Request) -> Result { + pub async fn complete(&self, request: &Request) -> Result { let provider = self.resolve_provider(request)?; if self.middleware.is_empty() { @@ -216,10 +216,10 @@ impl Client { /// /// # Errors /// - /// Returns `SdkError::Configuration` if no provider is specified or + /// Returns `Error::Configuration` if no provider is specified or /// registered, or any provider/middleware error encountered during the /// request. - pub async fn stream(&self, request: &Request) -> Result { + pub async fn stream(&self, request: &Request) -> Result { let provider = self.resolve_provider(request)?; if self.middleware.is_empty() { @@ -250,7 +250,7 @@ impl Client { /// # Errors /// /// Returns any error from a provider adapter's `close()` method. - pub async fn close(&self) -> Result<(), SdkError> { + pub async fn close(&self) -> Result<(), Error> { for provider in self.providers.values() { provider.close().await?; } @@ -301,25 +301,25 @@ mod tests { &self.provider_name } - async fn complete(&self, _request: &Request) -> Result { + async fn complete(&self, _request: &Request) -> Result { Ok(Response { - id: "resp_mock".into(), - model: "mock-model".into(), - provider: self.provider_name.clone(), - message: Message::assistant(&self.response_text), + id: "resp_mock".into(), + model: "mock-model".into(), + provider: self.provider_name.clone(), + message: Message::assistant(&self.response_text), finish_reason: FinishReason::Stop, - usage: TokenCounts { + usage: TokenCounts { input_tokens: 10, output_tokens: 20, ..Default::default() }, - raw: None, - warnings: vec![], - rate_limit: None, + raw: None, + warnings: vec![], + rate_limit: None, }) } - async fn stream(&self, _request: &Request) -> Result { + async fn stream(&self, _request: &Request) -> Result { let text = self.response_text.clone(); let provider = self.provider_name.clone(); let events = vec![ @@ -346,19 +346,19 @@ mod tests { fn test_request() -> Request { Request { - model: "mock-model".into(), - messages: vec![Message::user("Hello")], - provider: None, - tools: None, - tool_choice: None, - response_format: None, - temperature: None, - top_p: None, - max_tokens: None, - stop_sequences: None, + model: "mock-model".into(), + messages: vec![Message::user("Hello")], + provider: None, + tools: None, + tool_choice: None, + response_format: None, + temperature: None, + top_p: None, + max_tokens: None, + stop_sequences: None, reasoning_effort: None, - speed: None, - metadata: None, + speed: None, + metadata: None, provider_options: None, } } @@ -399,10 +399,7 @@ mod tests { let client = Client::new(HashMap::new(), None, vec![]); let result = client.complete(&test_request()).await; assert!(result.is_err()); - assert!(matches!( - result.unwrap_err(), - SdkError::Configuration { .. } - )); + assert!(matches!(result.unwrap_err(), Error::Configuration { .. })); } #[tokio::test] @@ -417,10 +414,7 @@ mod tests { req.provider = Some("nonexistent".into()); let result = client.complete(&req).await; assert!(result.is_err()); - assert!(matches!( - result.unwrap_err(), - SdkError::Configuration { .. } - )); + assert!(matches!(result.unwrap_err(), Error::Configuration { .. })); } #[tokio::test] @@ -480,11 +474,7 @@ mod tests { #[async_trait::async_trait] impl Middleware for UppercaseMiddleware { - async fn handle_complete( - &self, - request: Request, - next: NextFn, - ) -> Result { + async fn handle_complete(&self, request: Request, next: NextFn) -> Result { let mut response = next(request).await?; let text = response.text().to_uppercase(); response.message = Message::assistant(text); @@ -495,7 +485,7 @@ mod tests { &self, request: Request, next: NextStreamFn, - ) -> Result { + ) -> Result { next(request).await } } diff --git a/lib/crates/fabro-llm/src/error.rs b/lib/crates/fabro-llm/src/error.rs index c846f251c..876fca13c 100644 --- a/lib/crates/fabro-llm/src/error.rs +++ b/lib/crates/fabro-llm/src/error.rs @@ -30,23 +30,23 @@ impl std::fmt::Display for ProviderErrorKind { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ProviderErrorDetail { - pub message: String, - pub provider: String, + pub message: String, + pub provider: String, pub status_code: Option, - pub error_code: Option, + pub error_code: Option, pub retry_after: Option, - pub raw: Option, + pub raw: Option, } impl ProviderErrorDetail { pub fn new(message: impl Into, provider: impl Into) -> Self { Self { - message: message.into(), - provider: provider.into(), + message: message.into(), + provider: provider.into(), status_code: None, - error_code: None, + error_code: None, retry_after: None, - raw: None, + raw: None, } } } @@ -58,7 +58,7 @@ use std::sync::Arc; pub enum Error { #[error("{kind} {}: {}", .detail.provider, .detail.message)] Provider { - kind: ProviderErrorKind, + kind: ProviderErrorKind, detail: Box, }, @@ -67,7 +67,7 @@ pub enum Error { message: String, #[source] #[serde(skip)] - source: Option>, + source: Option>, }, #[error("Request interrupted: {message}")] @@ -78,7 +78,7 @@ pub enum Error { message: String, #[source] #[serde(skip)] - source: Option>, + source: Option>, }, #[error("Stream error: {message}")] @@ -86,7 +86,7 @@ pub enum Error { message: String, #[source] #[serde(skip)] - source: Option>, + source: Option>, }, #[error("Invalid tool call: {message}")] @@ -100,7 +100,7 @@ pub enum Error { message: String, #[source] #[serde(skip)] - source: Option>, + source: Option>, }, #[error("Unsupported tool choice: {message}")] @@ -114,7 +114,7 @@ impl Error { ) -> Self { Self::Network { message: message.into(), - source: Some(Arc::new(source)), + source: Some(Arc::new(source)), } } @@ -124,7 +124,7 @@ impl Error { ) -> Self { Self::RequestTimeout { message: message.into(), - source: Some(Arc::new(source)), + source: Some(Arc::new(source)), } } @@ -134,7 +134,7 @@ impl Error { ) -> Self { Self::Stream { message: message.into(), - source: Some(Arc::new(source)), + source: Some(Arc::new(source)), } } @@ -144,7 +144,7 @@ impl Error { ) -> Self { Self::Configuration { message: message.into(), - source: Some(Arc::new(source)), + source: Some(Arc::new(source)), } } @@ -272,7 +272,7 @@ pub fn error_from_status_code( error_code: Option, raw: Option, retry_after: Option, -) -> SdkError { +) -> Error { let detail = ProviderErrorDetail { message, provider, @@ -291,7 +291,7 @@ pub fn error_from_status_code( 408 => { return Error::RequestTimeout { message: detail.message, - source: None, + source: None, }; } 413 => ProviderErrorKind::ContextLength, @@ -330,7 +330,7 @@ pub fn error_from_grpc_status( error_code: Option, raw: Option, retry_after: Option, -) -> SdkError { +) -> Error { let detail = ProviderErrorDetail { message, provider, @@ -349,7 +349,7 @@ pub fn error_from_grpc_status( "DEADLINE_EXCEEDED" => { return Error::RequestTimeout { message: detail.message, - source: None, + source: None, }; } _ => ProviderErrorKind::Server, @@ -362,7 +362,6 @@ pub fn error_from_grpc_status( } pub type Result = std::result::Result; -pub type SdkError = Error; #[cfg(test)] mod tests { @@ -372,8 +371,8 @@ mod tests { #[test] fn retryable_classification() { - let auth_err = SdkError::Provider { - kind: ProviderErrorKind::Authentication, + let auth_err = Error::Provider { + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail { status_code: Some(401), ..ProviderErrorDetail::new("bad key", "openai") @@ -381,8 +380,8 @@ mod tests { }; assert!(!auth_err.retryable()); - let rate_err = SdkError::Provider { - kind: ProviderErrorKind::RateLimit, + let rate_err = Error::Provider { + kind: ProviderErrorKind::RateLimit, detail: Box::new(ProviderErrorDetail { status_code: Some(429), retry_after: Some(2.0), @@ -392,8 +391,8 @@ mod tests { assert!(rate_err.retryable()); assert_eq!(rate_err.retry_after(), Some(2.0)); - let server_err = SdkError::Provider { - kind: ProviderErrorKind::Server, + let server_err = Error::Provider { + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail { status_code: Some(500), ..ProviderErrorDetail::new("internal error", "anthropic") @@ -401,21 +400,21 @@ mod tests { }; assert!(server_err.retryable()); - let timeout = SdkError::RequestTimeout { + let timeout = Error::RequestTimeout { message: "timed out".into(), - source: None, + source: None, }; assert!(!timeout.retryable()); - let network = SdkError::Network { + let network = Error::Network { message: "connection refused".into(), - source: None, + source: None, }; assert!(network.retryable()); - let config = SdkError::Configuration { + let config = Error::Configuration { message: "missing provider".into(), - source: None, + source: None, }; assert!(!config.retryable()); } @@ -424,38 +423,38 @@ mod tests { fn non_retryable_provider_errors() { let detail = || Box::new(ProviderErrorDetail::new("error", "openai")); - let access_denied = SdkError::Provider { - kind: ProviderErrorKind::AccessDenied, + let access_denied = Error::Provider { + kind: ProviderErrorKind::AccessDenied, detail: detail(), }; assert!(!access_denied.retryable()); - let not_found = SdkError::Provider { - kind: ProviderErrorKind::NotFound, + let not_found = Error::Provider { + kind: ProviderErrorKind::NotFound, detail: detail(), }; assert!(!not_found.retryable()); - let invalid_req = SdkError::Provider { - kind: ProviderErrorKind::InvalidRequest, + let invalid_req = Error::Provider { + kind: ProviderErrorKind::InvalidRequest, detail: detail(), }; assert!(!invalid_req.retryable()); - let ctx_length = SdkError::Provider { - kind: ProviderErrorKind::ContextLength, + let ctx_length = Error::Provider { + kind: ProviderErrorKind::ContextLength, detail: detail(), }; assert!(!ctx_length.retryable()); - let quota = SdkError::Provider { - kind: ProviderErrorKind::QuotaExceeded, + let quota = Error::Provider { + kind: ProviderErrorKind::QuotaExceeded, detail: detail(), }; assert!(!quota.retryable()); - let content_filter = SdkError::Provider { - kind: ProviderErrorKind::ContentFilter, + let content_filter = Error::Provider { + kind: ProviderErrorKind::ContentFilter, detail: detail(), }; assert!(!content_filter.retryable()); @@ -463,17 +462,17 @@ mod tests { #[test] fn non_retryable_sdk_errors() { - let invalid_tool = SdkError::InvalidToolCall { + let invalid_tool = Error::InvalidToolCall { message: "bad tool".into(), }; assert!(!invalid_tool.retryable()); - let no_object = SdkError::NoObjectGenerated { + let no_object = Error::NoObjectGenerated { message: "no output".into(), }; assert!(!no_object.retryable()); - let interrupt = SdkError::Interrupt { + let interrupt = Error::Interrupt { message: "interrupted".into(), }; assert!(!interrupt.retryable()); @@ -489,32 +488,44 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::Authentication, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::Authentication, + .. + } + )); assert!(!err.retryable()); let err = error_from_status_code(403, "forbidden".into(), "openai".into(), None, None, None); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::AccessDenied, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::AccessDenied, + .. + } + )); let err = error_from_status_code(404, "not found".into(), "openai".into(), None, None, None); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::NotFound, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::NotFound, + .. + } + )); let err = error_from_status_code(400, "bad request".into(), "openai".into(), None, None, None); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::InvalidRequest, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::InvalidRequest, + .. + } + )); let err = error_from_status_code( 422, @@ -524,20 +535,26 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::InvalidRequest, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::InvalidRequest, + .. + } + )); let err = error_from_status_code(408, "timeout".into(), "openai".into(), None, None, None); - assert!(matches!(err, SdkError::RequestTimeout { .. })); + assert!(matches!(err, Error::RequestTimeout { .. })); let err = error_from_status_code(413, "too large".into(), "openai".into(), None, None, None); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::ContextLength, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::ContextLength, + .. + } + )); let err = error_from_status_code( 429, @@ -547,26 +564,35 @@ mod tests { None, Some(5.0), ); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::RateLimit, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::RateLimit, + .. + } + )); assert!(err.retryable()); assert_eq!(err.retry_after(), Some(5.0)); let err = error_from_status_code(500, "internal".into(), "openai".into(), None, None, None); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::Server, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::Server, + .. + } + )); assert!(err.retryable()); let err = error_from_status_code(502, "bad gateway".into(), "openai".into(), None, None, None); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::Server, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::Server, + .. + } + )); let err = error_from_status_code( 529, @@ -576,10 +602,13 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::Server, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::Server, + .. + } + )); assert!(err.retryable()); } @@ -593,10 +622,13 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::ContextLength, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::ContextLength, + .. + } + )); } #[test] @@ -609,10 +641,13 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::ContextLength, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::ContextLength, + .. + } + )); } #[test] @@ -625,10 +660,13 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::ContentFilter, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::ContentFilter, + .. + } + )); } #[test] @@ -641,10 +679,13 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::ContentFilter, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::ContentFilter, + .. + } + )); } #[test] @@ -657,10 +698,13 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::NotFound, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::NotFound, + .. + } + )); } #[test] @@ -673,10 +717,13 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::NotFound, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::NotFound, + .. + } + )); } #[test] @@ -689,10 +736,13 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::Authentication, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::Authentication, + .. + } + )); } #[test] @@ -705,10 +755,13 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::Authentication, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::Authentication, + .. + } + )); } #[test] @@ -721,10 +774,13 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::NotFound, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::NotFound, + .. + } + )); let err = error_from_grpc_status( "RESOURCE_EXHAUSTED", @@ -734,10 +790,13 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::RateLimit, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::RateLimit, + .. + } + )); assert!(err.retryable()); let err = error_from_grpc_status( @@ -748,10 +807,13 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::Authentication, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::Authentication, + .. + } + )); let err = error_from_grpc_status( "DEADLINE_EXCEEDED", @@ -761,7 +823,7 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::RequestTimeout { .. })); + assert!(matches!(err, Error::RequestTimeout { .. })); let err = error_from_grpc_status( "UNKNOWN_CODE", @@ -771,16 +833,19 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::Server, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::Server, + .. + } + )); } #[test] fn error_display_messages() { - let err = SdkError::Provider { - kind: ProviderErrorKind::Authentication, + let err = Error::Provider { + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail { status_code: Some(401), ..ProviderErrorDetail::new("invalid api key", "openai") @@ -791,17 +856,17 @@ mod tests { "Authentication error for openai: invalid api key" ); - let err = SdkError::Configuration { + let err = Error::Configuration { message: "no provider".into(), - source: None, + source: None, }; assert_eq!(err.to_string(), "Configuration error: no provider"); } #[test] fn status_code_accessor() { - let err = SdkError::Provider { - kind: ProviderErrorKind::Server, + let err = Error::Provider { + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail { status_code: Some(503), ..ProviderErrorDetail::new("error", "openai") @@ -809,17 +874,17 @@ mod tests { }; assert_eq!(err.status_code(), Some(503)); - let err = SdkError::Network { + let err = Error::Network { message: "refused".into(), - source: None, + source: None, }; assert_eq!(err.status_code(), None); } #[test] fn provider_name_from_provider_variant() { - let err = SdkError::Provider { - kind: ProviderErrorKind::Authentication, + let err = Error::Provider { + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), }; assert_eq!(err.provider_name(), "openai"); @@ -827,9 +892,9 @@ mod tests { #[test] fn provider_name_defaults_to_unknown() { - let err = SdkError::Network { + let err = Error::Network { message: "refused".into(), - source: None, + source: None, }; assert_eq!(err.provider_name(), "unknown"); } @@ -839,24 +904,24 @@ mod tests { let detail = || Box::new(ProviderErrorDetail::new("error", "openai")); assert!( - SdkError::Provider { - kind: ProviderErrorKind::RateLimit, + Error::Provider { + kind: ProviderErrorKind::RateLimit, detail: detail(), } .failover_eligible() ); assert!( - SdkError::Provider { - kind: ProviderErrorKind::Server, + Error::Provider { + kind: ProviderErrorKind::Server, detail: detail(), } .failover_eligible() ); assert!( - SdkError::Provider { - kind: ProviderErrorKind::QuotaExceeded, + Error::Provider { + kind: ProviderErrorKind::QuotaExceeded, detail: detail(), } .failover_eligible() @@ -866,25 +931,25 @@ mod tests { #[test] fn failover_eligible_transient_non_provider_errors() { assert!( - SdkError::RequestTimeout { + Error::RequestTimeout { message: "timed out".into(), - source: None, + source: None, } .failover_eligible() ); assert!( - SdkError::Network { + Error::Network { message: "refused".into(), - source: None, + source: None, } .failover_eligible() ); assert!( - SdkError::Stream { + Error::Stream { message: "broken".into(), - source: None, + source: None, } .failover_eligible() ); @@ -895,32 +960,32 @@ mod tests { let detail = || Box::new(ProviderErrorDetail::new("error", "openai")); assert!( - !SdkError::Provider { - kind: ProviderErrorKind::Authentication, + !Error::Provider { + kind: ProviderErrorKind::Authentication, detail: detail(), } .failover_eligible() ); assert!( - !SdkError::Provider { - kind: ProviderErrorKind::InvalidRequest, + !Error::Provider { + kind: ProviderErrorKind::InvalidRequest, detail: detail(), } .failover_eligible() ); assert!( - !SdkError::Provider { - kind: ProviderErrorKind::ContextLength, + !Error::Provider { + kind: ProviderErrorKind::ContextLength, detail: detail(), } .failover_eligible() ); assert!( - !SdkError::Provider { - kind: ProviderErrorKind::ContentFilter, + !Error::Provider { + kind: ProviderErrorKind::ContentFilter, detail: detail(), } .failover_eligible() @@ -930,36 +995,36 @@ mod tests { #[test] fn failover_not_eligible_non_provider_errors() { assert!( - !SdkError::Configuration { + !Error::Configuration { message: "bad".into(), - source: None, + source: None, } .failover_eligible() ); assert!( - !SdkError::Interrupt { + !Error::Interrupt { message: "cancelled".into(), } .failover_eligible() ); assert!( - !SdkError::InvalidToolCall { + !Error::InvalidToolCall { message: "bad".into(), } .failover_eligible() ); assert!( - !SdkError::NoObjectGenerated { + !Error::NoObjectGenerated { message: "none".into(), } .failover_eligible() ); assert!( - !SdkError::UnsupportedToolChoice { + !Error::UnsupportedToolChoice { message: "nope".into(), } .failover_eligible() @@ -968,8 +1033,8 @@ mod tests { #[test] fn failure_signature_hint_provider_transient() { - let err = SdkError::Provider { - kind: ProviderErrorKind::RateLimit, + let err = Error::Provider { + kind: ProviderErrorKind::RateLimit, detail: Box::new(ProviderErrorDetail::new("too fast", "openai")), }; assert_eq!( @@ -977,8 +1042,8 @@ mod tests { "api_transient|openai|rate_limited" ); - let err = SdkError::Provider { - kind: ProviderErrorKind::Server, + let err = Error::Provider { + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail::new("500", "anthropic")), }; assert_eq!( @@ -989,8 +1054,8 @@ mod tests { #[test] fn failure_signature_hint_provider_deterministic() { - let err = SdkError::Provider { - kind: ProviderErrorKind::Authentication, + let err = Error::Provider { + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), }; assert_eq!( @@ -998,8 +1063,8 @@ mod tests { "api_deterministic|openai|authentication" ); - let err = SdkError::Provider { - kind: ProviderErrorKind::AccessDenied, + let err = Error::Provider { + kind: ProviderErrorKind::AccessDenied, detail: Box::new(ProviderErrorDetail::new("denied", "anthropic")), }; assert_eq!( @@ -1007,8 +1072,8 @@ mod tests { "api_deterministic|anthropic|access_denied" ); - let err = SdkError::Provider { - kind: ProviderErrorKind::NotFound, + let err = Error::Provider { + kind: ProviderErrorKind::NotFound, detail: Box::new(ProviderErrorDetail::new("missing", "openai")), }; assert_eq!( @@ -1016,8 +1081,8 @@ mod tests { "api_deterministic|openai|not_found" ); - let err = SdkError::Provider { - kind: ProviderErrorKind::InvalidRequest, + let err = Error::Provider { + kind: ProviderErrorKind::InvalidRequest, detail: Box::new(ProviderErrorDetail::new("bad", "openai")), }; assert_eq!( @@ -1025,8 +1090,8 @@ mod tests { "api_deterministic|openai|invalid_request" ); - let err = SdkError::Provider { - kind: ProviderErrorKind::ContentFilter, + let err = Error::Provider { + kind: ProviderErrorKind::ContentFilter, detail: Box::new(ProviderErrorDetail::new("blocked", "openai")), }; assert_eq!( @@ -1034,8 +1099,8 @@ mod tests { "api_deterministic|openai|content_filter" ); - let err = SdkError::Provider { - kind: ProviderErrorKind::ContextLength, + let err = Error::Provider { + kind: ProviderErrorKind::ContextLength, detail: Box::new(ProviderErrorDetail::new("too long", "openai")), }; assert_eq!( @@ -1043,8 +1108,8 @@ mod tests { "api_deterministic|openai|context_length" ); - let err = SdkError::Provider { - kind: ProviderErrorKind::QuotaExceeded, + let err = Error::Provider { + kind: ProviderErrorKind::QuotaExceeded, detail: Box::new(ProviderErrorDetail::new("out of quota", "openai")), }; assert_eq!( @@ -1056,60 +1121,60 @@ mod tests { #[test] fn failure_signature_hint_non_provider_variants() { assert_eq!( - SdkError::RequestTimeout { + Error::RequestTimeout { message: "timed out".into(), - source: None, + source: None, } .failure_signature_hint(), "api_transient|unknown|timeout" ); assert_eq!( - SdkError::Network { + Error::Network { message: "refused".into(), - source: None, + source: None, } .failure_signature_hint(), "api_transient|unknown|network" ); assert_eq!( - SdkError::Stream { + Error::Stream { message: "broken".into(), - source: None, + source: None, } .failure_signature_hint(), "api_transient|unknown|stream" ); assert_eq!( - SdkError::Interrupt { + Error::Interrupt { message: "cancelled".into(), } .failure_signature_hint(), "api_canceled|unknown|interrupt" ); assert_eq!( - SdkError::Configuration { + Error::Configuration { message: "bad".into(), - source: None, + source: None, } .failure_signature_hint(), "api_deterministic|unknown|configuration" ); assert_eq!( - SdkError::InvalidToolCall { + Error::InvalidToolCall { message: "bad".into(), } .failure_signature_hint(), "api_deterministic|unknown|invalid_tool_call" ); assert_eq!( - SdkError::NoObjectGenerated { + Error::NoObjectGenerated { message: "none".into(), } .failure_signature_hint(), "api_deterministic|unknown|no_object" ); assert_eq!( - SdkError::UnsupportedToolChoice { + Error::UnsupportedToolChoice { message: "nope".into(), } .failure_signature_hint(), @@ -1120,14 +1185,14 @@ mod tests { #[test] fn sdk_error_source_chaining() { let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused"); - let err = SdkError::network("connection failed", io_err); + let err = Error::network("connection failed", io_err); assert!(err.source().is_some()); } #[test] fn sdk_error_source_chain_walkable() { let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused"); - let err = SdkError::network("connection failed", io_err); + let err = Error::network("connection failed", io_err); // The source chain is walkable — the Arc wrapper preserves the inner error's // display let source = err.source().unwrap(); @@ -1137,9 +1202,9 @@ mod tests { #[test] fn sdk_error_serde_roundtrip_without_source() { let io_err = std::io::Error::other("boom"); - let err = SdkError::network("network failed", io_err); + let err = Error::network("network failed", io_err); let json = serde_json::to_string(&err).unwrap(); - let deserialized: SdkError = serde_json::from_str(&json).unwrap(); + let deserialized: Error = serde_json::from_str(&json).unwrap(); // source is lost through serde, message is preserved assert!(deserialized.source().is_none()); assert_eq!(deserialized.to_string(), "Network error: network failed"); diff --git a/lib/crates/fabro-llm/src/generate.rs b/lib/crates/fabro-llm/src/generate.rs index d28c6670c..16f6ce2d3 100644 --- a/lib/crates/fabro-llm/src/generate.rs +++ b/lib/crates/fabro-llm/src/generate.rs @@ -11,7 +11,7 @@ use tokio_util::sync::CancellationToken; use tracing::{debug, warn}; use crate::client::Client; -use crate::error::SdkError; +use crate::error::Error; use crate::provider::StreamEventStream; use crate::retry::retry; use crate::tools::{RepairToolCallFn, Tool, execute_all_tools_with_repair}; @@ -30,7 +30,7 @@ pub fn set_default_client(client: Client) { } /// Get the default client, lazily initialized from env. -async fn get_default_client() -> Result, SdkError> { +async fn get_default_client() -> Result, Error> { if let Some(client) = DEFAULT_CLIENT.get() { return Ok(client.clone()); } @@ -39,16 +39,16 @@ async fn get_default_client() -> Result, SdkError> { Ok(client) } -fn build_initial_messages(params: &GenerateParams) -> Result, SdkError> { +fn build_initial_messages(params: &GenerateParams) -> Result, Error> { let mut messages = Vec::new(); if let Some(system) = ¶ms.system { messages.push(Message::system(system)); } if let Some(ref prompt) = params.prompt { if params.messages.is_some() { - return Err(SdkError::Configuration { + return Err(Error::Configuration { message: "Cannot specify both 'prompt' and 'messages'".into(), - source: None, + source: None, }); } messages.push(Message::user(prompt)); @@ -64,19 +64,19 @@ fn build_request( tool_definitions: Option<&[ToolDefinition]>, ) -> Request { Request { - model: params.model.clone(), - messages: messages.to_vec(), - provider: params.provider.clone(), - tools: tool_definitions.map(<[ToolDefinition]>::to_vec), - tool_choice: params.tool_choice.clone(), - response_format: params.response_format.clone(), - temperature: params.temperature, - top_p: params.top_p, - max_tokens: params.max_tokens, - stop_sequences: params.stop_sequences.clone(), + model: params.model.clone(), + messages: messages.to_vec(), + provider: params.provider.clone(), + tools: tool_definitions.map(<[ToolDefinition]>::to_vec), + tool_choice: params.tool_choice.clone(), + response_format: params.response_format.clone(), + temperature: params.temperature, + top_p: params.top_p, + max_tokens: params.max_tokens, + stop_sequences: params.stop_sequences.clone(), reasoning_effort: params.reasoning_effort, - speed: params.speed.clone(), - metadata: params.metadata.clone(), + speed: params.speed.clone(), + metadata: params.metadata.clone(), provider_options: params.provider_options.clone(), } } @@ -101,14 +101,14 @@ fn build_generate_result(steps: Vec, total_usage: TokenCounts) -> Ge /// /// # Errors /// -/// Returns `SdkError::Configuration` if both `prompt` and `messages` are set, +/// Returns `Error::Configuration` if both `prompt` and `messages` are set, /// or any provider error encountered during generation or tool execution. /// /// # Panics /// /// Panics if a tool's `execute` handler is `None` when matched during tool /// execution. -pub async fn generate(params: GenerateParams) -> Result { +pub async fn generate(params: GenerateParams) -> Result { let client = match params.client.clone() { Some(c) => c, None => get_default_client().await?, @@ -142,7 +142,7 @@ pub async fn generate(params: GenerateParams) -> Result Result Result Result bool + Send + Sync>; /// Parameters for `generate()` (Section 4.3). #[derive(Clone)] pub struct GenerateParams { - pub model: String, - pub prompt: Option, - pub messages: Option>, - pub system: Option, - pub tools: Option>>, - pub tool_choice: Option, - pub max_tool_rounds: u32, - pub response_format: Option, - pub temperature: Option, - pub top_p: Option, - pub max_tokens: Option, - pub stop_sequences: Option>, + pub model: String, + pub prompt: Option, + pub messages: Option>, + pub system: Option, + pub tools: Option>>, + pub tool_choice: Option, + pub max_tool_rounds: u32, + pub response_format: Option, + pub temperature: Option, + pub top_p: Option, + pub max_tokens: Option, + pub stop_sequences: Option>, pub reasoning_effort: Option, - pub speed: Option, - pub provider: Option, + pub speed: Option, + pub provider: Option, pub provider_options: Option, - pub metadata: Option>, - pub max_retries: u32, - pub timeout: Option, - pub client: Option>, + pub metadata: Option>, + pub max_retries: u32, + pub timeout: Option, + pub client: Option>, /// Cancellation token to interrupt generation (Section 4.8). - pub abort_signal: Option, + pub abort_signal: Option, /// Custom stop condition checked after each tool round (Section 4.3). - pub stop_when: Option, + pub stop_when: Option, /// Callback to repair invalid tool call arguments (Section 5.8). pub repair_tool_call: Option, } @@ -319,28 +319,28 @@ pub struct GenerateParams { impl GenerateParams { pub fn new(model: impl Into) -> Self { Self { - model: model.into(), - prompt: None, - messages: None, - system: None, - tools: None, - tool_choice: None, - max_tool_rounds: 1, - response_format: None, - temperature: None, - top_p: None, - max_tokens: None, - stop_sequences: None, + model: model.into(), + prompt: None, + messages: None, + system: None, + tools: None, + tool_choice: None, + max_tool_rounds: 1, + response_format: None, + temperature: None, + top_p: None, + max_tokens: None, + stop_sequences: None, reasoning_effort: None, - speed: None, - provider: None, + speed: None, + provider: None, provider_options: None, - metadata: None, - max_retries: 2, - timeout: None, - client: None, - abort_signal: None, - stop_when: None, + metadata: None, + max_retries: 2, + timeout: None, + client: None, + abort_signal: None, + stop_when: None, repair_tool_call: None, } } @@ -479,24 +479,24 @@ impl GenerateParams { /// `StreamAccumulator` collects stream events into a complete Response (Section /// 4.4). pub struct StreamAccumulator { - text_parts: Vec, + text_parts: Vec, reasoning_parts: Vec, - tool_calls: Vec, - finish_reason: Option, - usage: Option, - response: Option, + tool_calls: Vec, + finish_reason: Option, + usage: Option, + response: Option, } impl StreamAccumulator { #[must_use] pub const fn new() -> Self { Self { - text_parts: Vec::new(), + text_parts: Vec::new(), reasoning_parts: Vec::new(), - tool_calls: Vec::new(), - finish_reason: None, - usage: None, - response: None, + tool_calls: Vec::new(), + finish_reason: None, + usage: None, + response: None, } } @@ -559,11 +559,11 @@ impl Default for StreamAccumulator { /// Wraps a streaming response with an internal `StreamAccumulator` and /// convenience methods. /// -/// Implements `Stream>` so it can be used +/// Implements `Stream>` so it can be used /// as a drop-in replacement for `StreamEventStream`. Also supports multi-step /// tool loops when active tools are provided. pub struct StreamResult { - inner: StreamEventStream, + inner: StreamEventStream, accumulator: StreamAccumulator, } @@ -589,7 +589,7 @@ impl StreamResult { /// Returns a stream that yields only text delta strings. #[must_use] - pub fn text_stream(self) -> Pin> + Send>> { + pub fn text_stream(self) -> Pin> + Send>> { Box::pin(self.filter_map(|result| { future::ready(match result { Ok(StreamEvent::TextDelta { delta, .. }) => Some(Ok(delta)), @@ -601,7 +601,7 @@ impl StreamResult { } impl Stream for StreamResult { - type Item = Result; + type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let inner = self.inner.as_mut(); @@ -621,9 +621,9 @@ impl Stream for StreamResult { /// /// # Errors /// -/// Returns `SdkError::Configuration` if both `prompt` and `messages` are set, +/// Returns `Error::Configuration` if both `prompt` and `messages` are set, /// or any provider error encountered during streaming setup. -pub async fn stream(params: GenerateParams) -> Result { +pub async fn stream(params: GenerateParams) -> Result { let inner = stream_with_tool_loop(params).await?; Ok(StreamResult::new(inner)) } @@ -639,9 +639,9 @@ pub async fn stream(params: GenerateParams) -> Result { /// /// # Errors /// -/// Returns `SdkError::Configuration` if both `prompt` and `messages` are set, +/// Returns `Error::Configuration` if both `prompt` and `messages` are set, /// or any provider error encountered during streaming setup. -async fn stream_with_tool_loop(params: GenerateParams) -> Result { +async fn stream_with_tool_loop(params: GenerateParams) -> Result { let client = match params.client.clone() { Some(c) => c, None => get_default_client().await?, @@ -669,7 +669,7 @@ 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 { @@ -691,7 +691,7 @@ async fn stream_with_tool_loop(params: GenerateParams) -> Result Result Result Result Result, -) -> Result { +) -> Result { let request = build_request(params, messages, tool_definitions); // Apply per_step timeout to the initial connection (Section 4.7) @@ -870,9 +870,9 @@ async fn stream_generate_raw( let duration = std::time::Duration::from_secs_f64(per_step); time::timeout(duration, client.stream(&request)) .await - .map_err(|_| SdkError::RequestTimeout { + .map_err(|_| Error::RequestTimeout { message: format!("Per-step timeout of {per_step}s exceeded"), - source: None, + source: None, })?? } else { client.stream(&request).await? @@ -883,7 +883,7 @@ async fn stream_generate_raw( let token = token.clone(); let mapped = inner_stream.map(move |item| { if token.is_cancelled() { - return Err(SdkError::Interrupt { + return Err(Error::Interrupt { message: "Stream interrupted by cancellation token".into(), }); } @@ -907,9 +907,9 @@ async fn stream_generate_raw( Ok(Some(item)) => Some((item, (stream, false))), Ok(None) => None, // stream completed naturally Err(_) => Some(( - Err(SdkError::RequestTimeout { + Err(Error::RequestTimeout { message: format!("Total timeout of {total_copy}s exceeded"), - source: None, + source: None, }), (stream, true), )), @@ -928,9 +928,9 @@ async fn stream_generate_raw( /// /// # Errors /// -/// Returns `SdkError::Configuration` if both `prompt` and `messages` are set, +/// Returns `Error::Configuration` if both `prompt` and `messages` are set, /// or any provider error encountered during streaming setup. -pub async fn stream_generate(params: GenerateParams) -> Result { +pub async fn stream_generate(params: GenerateParams) -> Result { let client = match params.client.clone() { Some(c) => c, None => get_default_client().await?, @@ -948,17 +948,17 @@ pub async fn stream_generate(params: GenerateParams) -> Result Result { +) -> Result { let params = GenerateParams { response_format: Some(ResponseFormat { - kind: ResponseFormatType::JsonSchema, + kind: ResponseFormatType::JsonSchema, json_schema: Some(schema), - strict: true, + strict: true, }), ..params }; @@ -971,7 +971,7 @@ pub async fn generate_object( result.output = Some(parsed); Ok(result) } - Err(e) => Err(SdkError::NoObjectGenerated { + Err(e) => Err(Error::NoObjectGenerated { message: format!("Failed to parse response as JSON: {e}"), }), } @@ -979,16 +979,16 @@ pub async fn generate_object( /// Stream type for `stream_object()`. pub type ObjectStream = - Pin> + Send>>; + Pin> + Send>>; /// Wraps an `ObjectStream` with an `object()` accessor for the final parsed /// value. /// -/// Implements `Stream>` so it can be +/// Implements `Stream>` so it can be /// used as a drop-in replacement for `ObjectStream`. Tracks the last `Complete` /// event's object internally so callers can retrieve it after the stream ends. pub struct ObjectStreamResult { - inner: ObjectStream, + inner: ObjectStream, object: Option, } @@ -1009,7 +1009,7 @@ impl ObjectStreamResult { } impl Stream for ObjectStreamResult { - type Item = Result; + type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let inner = self.inner.as_mut(); @@ -1036,18 +1036,18 @@ impl Stream for ObjectStreamResult { /// /// # Errors /// -/// Returns `SdkError::Configuration` if both `prompt` and `messages` are set, -/// `SdkError::NoObjectGenerated` if the final accumulated text is not valid +/// Returns `Error::Configuration` if both `prompt` and `messages` are set, +/// `Error::NoObjectGenerated` if the final accumulated text is not valid /// JSON, or any provider error encountered during streaming. pub async fn stream_object( params: GenerateParams, schema: serde_json::Value, -) -> Result { +) -> Result { let params = GenerateParams { response_format: Some(ResponseFormat { - kind: ResponseFormatType::JsonSchema, + kind: ResponseFormatType::JsonSchema, json_schema: Some(schema), - strict: true, + strict: true, }), ..params }; @@ -1057,7 +1057,7 @@ pub async fn stream_object( let mapped = inner_stream.scan( (String::new(), Option::::None), |(accumulated_text, last_parsed), event| { - let mut events: Vec> = Vec::new(); + let mut events: Vec> = Vec::new(); match &event { Ok(stream_event) => { @@ -1081,12 +1081,12 @@ pub async fn stream_object( match serde_json::from_str::(accumulated_text) { Ok(final_object) => { events.push(Ok(ObjectStreamEvent::Complete { - object: final_object, + object: final_object, response: response.clone(), })); } Err(e) => { - events.push(Err(SdkError::NoObjectGenerated { + events.push(Err(Error::NoObjectGenerated { message: format!("Failed to parse final response as JSON: {e}"), })); } @@ -1099,9 +1099,9 @@ pub async fn stream_object( } } Err(e) => { - events.push(Err(SdkError::Stream { + events.push(Err(Error::Stream { message: format!("{e}"), - source: None, + source: None, })); } } @@ -1146,25 +1146,25 @@ mod tests { "mock" } - async fn complete(&self, _request: &Request) -> Result { + async fn complete(&self, _request: &Request) -> Result { Ok(Response { - id: "resp_1".into(), - model: "mock-model".into(), - provider: "mock".into(), - message: Message::assistant(&self.response_text), + id: "resp_1".into(), + model: "mock-model".into(), + provider: "mock".into(), + message: Message::assistant(&self.response_text), finish_reason: FinishReason::Stop, - usage: TokenCounts { + usage: TokenCounts { input_tokens: 10, output_tokens: 20, ..Default::default() }, - raw: None, - warnings: vec![], - rate_limit: None, + raw: None, + warnings: vec![], + rate_limit: None, }) } - async fn stream(&self, _request: &Request) -> Result { + async fn stream(&self, _request: &Request) -> Result { let text = self.response_text.clone(); let events = vec![ Ok(StreamEvent::text_delta(&text, Some("t1".into()))), @@ -1176,19 +1176,19 @@ mod tests { ..Default::default() }, Response { - id: "resp_1".into(), - model: "mock-model".into(), - provider: "mock".into(), - message: Message::assistant(&text), + id: "resp_1".into(), + model: "mock-model".into(), + provider: "mock".into(), + message: Message::assistant(&text), finish_reason: FinishReason::Stop, - usage: TokenCounts { + usage: TokenCounts { input_tokens: 10, output_tokens: 20, ..Default::default() }, - raw: None, - warnings: vec![], - rate_limit: None, + raw: None, + warnings: vec![], + rate_limit: None, }, )), ]; @@ -1261,10 +1261,7 @@ mod tests { .await; assert!(result.is_err()); - assert!(matches!( - result.unwrap_err(), - SdkError::Configuration { .. } - )); + assert!(matches!(result.unwrap_err(), Error::Configuration { .. })); } /// Mock provider that returns tool calls then text @@ -1278,56 +1275,56 @@ mod tests { "mock" } - async fn complete(&self, _request: &Request) -> Result { + async fn complete(&self, _request: &Request) -> Result { let count = self.call_count.fetch_add(1, Ordering::SeqCst); if count == 0 { // First call: return tool call Ok(Response { - id: "resp_1".into(), - model: "mock-model".into(), - provider: "mock".into(), - message: Message { - role: Role::Assistant, - content: vec![ContentPart::ToolCall(ToolCall::new( + id: "resp_1".into(), + model: "mock-model".into(), + provider: "mock".into(), + message: Message { + role: Role::Assistant, + content: vec![ContentPart::ToolCall(ToolCall::new( "call_1", "get_weather", serde_json::json!({"city": "SF"}), ))], - name: None, + name: None, tool_call_id: None, }, finish_reason: FinishReason::ToolCalls, - usage: TokenCounts { + usage: TokenCounts { input_tokens: 10, output_tokens: 5, ..Default::default() }, - raw: None, - warnings: vec![], - rate_limit: None, + raw: None, + warnings: vec![], + rate_limit: None, }) } else { // Second call: return text Ok(Response { - id: "resp_2".into(), - model: "mock-model".into(), - provider: "mock".into(), - message: Message::assistant("The weather in SF is 72F"), + id: "resp_2".into(), + model: "mock-model".into(), + provider: "mock".into(), + message: Message::assistant("The weather in SF is 72F"), finish_reason: FinishReason::Stop, - usage: TokenCounts { + usage: TokenCounts { input_tokens: 20, output_tokens: 10, ..Default::default() }, - raw: None, - warnings: vec![], - rate_limit: None, + raw: None, + warnings: vec![], + rate_limit: None, }) } } - async fn stream(&self, _request: &Request) -> Result { + async fn stream(&self, _request: &Request) -> Result { Ok(Box::pin(stream::empty())) } } @@ -1380,19 +1377,19 @@ mod tests { acc.process(&StreamEvent::text_delta(" world", Some("t1".into()))); let resp = Response { - id: "r1".into(), - model: "m".into(), - provider: "p".into(), - message: Message::assistant("Hello world"), + id: "r1".into(), + model: "m".into(), + provider: "p".into(), + message: Message::assistant("Hello world"), finish_reason: FinishReason::Stop, - usage: TokenCounts { + usage: TokenCounts { input_tokens: 5, output_tokens: 2, ..Default::default() }, - raw: None, - warnings: vec![], - rate_limit: None, + raw: None, + warnings: vec![], + rate_limit: None, }; acc.process(&StreamEvent::finish( @@ -1483,7 +1480,7 @@ mod tests { assert!(result.is_err()); assert!(matches!( result.unwrap_err(), - SdkError::NoObjectGenerated { .. } + Error::NoObjectGenerated { .. } )); } @@ -1537,9 +1534,9 @@ mod tests { .max_retries(5) .tool_choice(ToolChoice::Required) .response_format(ResponseFormat { - kind: ResponseFormatType::JsonObject, + kind: ResponseFormatType::JsonObject, json_schema: None, - strict: false, + strict: false, }) .max_tool_rounds(3); @@ -1562,7 +1559,7 @@ mod tests { #[test] fn generate_params_timeout_builder() { let params = GenerateParams::new("test-model").timeout(TimeoutOptions { - total: Some(30.0), + total: Some(30.0), per_step: Some(10.0), }); assert!(params.timeout.is_some()); @@ -1573,7 +1570,7 @@ mod tests { /// Mock provider that streams JSON tokens incrementally. struct StreamingJsonMockProvider { - deltas: Vec, + deltas: Vec, full_text: String, } @@ -1593,22 +1590,22 @@ mod tests { "mock" } - async fn complete(&self, _request: &Request) -> Result { + async fn complete(&self, _request: &Request) -> Result { Ok(Response { - id: "resp_1".into(), - model: "mock-model".into(), - provider: "mock".into(), - message: Message::assistant(&self.full_text), + id: "resp_1".into(), + model: "mock-model".into(), + provider: "mock".into(), + message: Message::assistant(&self.full_text), finish_reason: FinishReason::Stop, - usage: TokenCounts::default(), - raw: None, - warnings: vec![], - rate_limit: None, + usage: TokenCounts::default(), + raw: None, + warnings: vec![], + rate_limit: None, }) } - async fn stream(&self, _request: &Request) -> Result { - let mut events: Vec> = self + async fn stream(&self, _request: &Request) -> Result { + let mut events: Vec> = self .deltas .iter() .map(|d| Ok(StreamEvent::text_delta(d.as_str(), Some("t1".into())))) @@ -1622,19 +1619,19 @@ mod tests { ..Default::default() }, Response { - id: "resp_1".into(), - model: "mock-model".into(), - provider: "mock".into(), - message: Message::assistant(&self.full_text), + id: "resp_1".into(), + model: "mock-model".into(), + provider: "mock".into(), + message: Message::assistant(&self.full_text), finish_reason: FinishReason::Stop, - usage: TokenCounts { + usage: TokenCounts { input_tokens: 10, output_tokens: 20, ..Default::default() }, - raw: None, - warnings: vec![], - rate_limit: None, + raw: None, + warnings: vec![], + rate_limit: None, }, ))); @@ -1759,7 +1756,7 @@ mod tests { .await .unwrap(); - let results: Vec> = obj_stream.collect().await; + let results: Vec> = obj_stream.collect().await; let has_error = results.iter().any(std::result::Result::is_err); assert!(has_error, "Expected an error for invalid final JSON"); @@ -1779,14 +1776,14 @@ mod tests { .await; assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), SdkError::Interrupt { .. })); + assert!(matches!(result.unwrap_err(), Error::Interrupt { .. })); } #[tokio::test] async fn generate_abort_signal_between_tool_rounds() { // Provider that always returns tool calls struct AlwaysToolCallProvider { - call_count: Arc, + call_count: Arc, cancel_token: CancellationToken, } @@ -1796,35 +1793,35 @@ mod tests { "mock" } - async fn complete(&self, _request: &Request) -> Result { + async fn complete(&self, _request: &Request) -> Result { let count = self.call_count.fetch_add(1, Ordering::SeqCst); // Cancel after first call completes if count == 0 { self.cancel_token.cancel(); } Ok(Response { - id: format!("resp_{count}"), - model: "mock-model".into(), - provider: "mock".into(), - message: Message { - role: Role::Assistant, - content: vec![ContentPart::ToolCall(ToolCall::new( + id: format!("resp_{count}"), + model: "mock-model".into(), + provider: "mock".into(), + message: Message { + role: Role::Assistant, + content: vec![ContentPart::ToolCall(ToolCall::new( format!("call_{count}"), "get_weather", serde_json::json!({"city": "SF"}), ))], - name: None, + name: None, tool_call_id: None, }, finish_reason: FinishReason::ToolCalls, - usage: TokenCounts::default(), - raw: None, - warnings: vec![], - rate_limit: None, + usage: TokenCounts::default(), + raw: None, + warnings: vec![], + rate_limit: None, }) } - async fn stream(&self, _request: &Request) -> Result { + async fn stream(&self, _request: &Request) -> Result { Ok(Box::pin(stream::empty())) } } @@ -1834,7 +1831,7 @@ mod tests { let token_clone = token.clone(); let provider: Arc = Arc::new(AlwaysToolCallProvider { - call_count: call_count.clone(), + call_count: call_count.clone(), cancel_token: token_clone, }); let mut providers: HashMap> = HashMap::new(); @@ -1857,7 +1854,7 @@ mod tests { .await; assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), SdkError::Interrupt { .. })); + assert!(matches!(result.unwrap_err(), Error::Interrupt { .. })); // Should have only made 1 call before aborting assert_eq!(call_count.load(Ordering::SeqCst), 1); } @@ -1882,7 +1879,7 @@ mod tests { let first = stream_result.next().await.unwrap(); assert!(first.is_err()); - assert!(matches!(first.unwrap_err(), SdkError::Interrupt { .. })); + assert!(matches!(first.unwrap_err(), Error::Interrupt { .. })); } #[tokio::test] @@ -1987,21 +1984,21 @@ mod tests { "mock" } - async fn complete(&self, _request: &Request) -> Result { + async fn complete(&self, _request: &Request) -> Result { Ok(Response { - id: "resp_1".into(), - model: "mock-model".into(), - provider: "mock".into(), - message: Message::assistant("fallback"), + id: "resp_1".into(), + model: "mock-model".into(), + provider: "mock".into(), + message: Message::assistant("fallback"), finish_reason: FinishReason::Stop, - usage: TokenCounts::default(), - raw: None, - warnings: vec![], - rate_limit: None, + usage: TokenCounts::default(), + raw: None, + warnings: vec![], + rate_limit: None, }) } - async fn stream(&self, _request: &Request) -> Result { + async fn stream(&self, _request: &Request) -> Result { let count = self.call_count.fetch_add(1, Ordering::SeqCst); if count == 0 { @@ -2009,24 +2006,24 @@ mod tests { let tool_call = ToolCall::new("call_1", "get_weather", serde_json::json!({"city": "SF"})); let response = Response { - id: "resp_1".into(), - model: "mock-model".into(), - provider: "mock".into(), - message: Message { - role: Role::Assistant, - content: vec![ContentPart::ToolCall(tool_call.clone())], - name: None, + id: "resp_1".into(), + model: "mock-model".into(), + provider: "mock".into(), + message: Message { + role: Role::Assistant, + content: vec![ContentPart::ToolCall(tool_call.clone())], + name: None, tool_call_id: None, }, finish_reason: FinishReason::ToolCalls, - usage: TokenCounts { + usage: TokenCounts { input_tokens: 10, output_tokens: 5, ..Default::default() }, - raw: None, - warnings: vec![], - rate_limit: None, + raw: None, + warnings: vec![], + rate_limit: None, }; let events = vec![ Ok(StreamEvent::ToolCallEnd { tool_call }), @@ -2041,19 +2038,19 @@ mod tests { // Second stream: return text let text = "The weather in SF is 72F"; let response = Response { - id: "resp_2".into(), - model: "mock-model".into(), - provider: "mock".into(), - message: Message::assistant(text), + id: "resp_2".into(), + model: "mock-model".into(), + provider: "mock".into(), + message: Message::assistant(text), finish_reason: FinishReason::Stop, - usage: TokenCounts { + usage: TokenCounts { input_tokens: 20, output_tokens: 10, ..Default::default() }, - raw: None, - warnings: vec![], - rate_limit: None, + raw: None, + warnings: vec![], + rate_limit: None, }; let events = vec![ Ok(StreamEvent::text_delta(text, Some("t1".into()))), @@ -2159,19 +2156,19 @@ mod tests { let mut acc = StreamAccumulator::new(); let response = Response { - id: "resp_1".into(), - model: "mock-model".into(), - provider: "mock".into(), - message: Message::assistant("tool step"), + id: "resp_1".into(), + model: "mock-model".into(), + provider: "mock".into(), + message: Message::assistant("tool step"), finish_reason: FinishReason::ToolCalls, - usage: TokenCounts { + usage: TokenCounts { input_tokens: 10, output_tokens: 5, ..Default::default() }, - raw: None, - warnings: vec![], - rate_limit: None, + raw: None, + warnings: vec![], + rate_limit: None, }; let tool_calls = vec![ToolCall::new( @@ -2317,7 +2314,7 @@ mod tests { /// Mock provider that fails on stream N times then succeeds struct FailThenStreamProvider { call_count: Arc, - failures: u32, + failures: u32, } #[async_trait::async_trait] @@ -2326,26 +2323,26 @@ mod tests { "mock" } - async fn complete(&self, _request: &Request) -> Result { + async fn complete(&self, _request: &Request) -> Result { Ok(Response { - id: "resp_1".into(), - model: "mock-model".into(), - provider: "mock".into(), - message: Message::assistant("fallback"), + id: "resp_1".into(), + model: "mock-model".into(), + provider: "mock".into(), + message: Message::assistant("fallback"), finish_reason: FinishReason::Stop, - usage: TokenCounts::default(), - raw: None, - warnings: vec![], - rate_limit: None, + usage: TokenCounts::default(), + raw: None, + warnings: vec![], + rate_limit: None, }) } - async fn stream(&self, _request: &Request) -> Result { + async fn stream(&self, _request: &Request) -> Result { let count = self.call_count.fetch_add(1, Ordering::SeqCst); if count < self.failures { - return Err(SdkError::Provider { - kind: ProviderErrorKind::Server, + return Err(Error::Provider { + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail { status_code: Some(500), ..ProviderErrorDetail::new("server error", "mock") @@ -2355,19 +2352,19 @@ mod tests { let text = "Hello after retry"; let response = Response { - id: "resp_1".into(), - model: "mock-model".into(), - provider: "mock".into(), - message: Message::assistant(text), + id: "resp_1".into(), + model: "mock-model".into(), + provider: "mock".into(), + message: Message::assistant(text), finish_reason: FinishReason::Stop, - usage: TokenCounts { + usage: TokenCounts { input_tokens: 10, output_tokens: 20, ..Default::default() }, - raw: None, - warnings: vec![], - rate_limit: None, + raw: None, + warnings: vec![], + rate_limit: None, }; let events = vec![ Ok(StreamEvent::text_delta(text, Some("t1".into()))), @@ -2386,7 +2383,7 @@ mod tests { let call_count = Arc::new(AtomicU32::new(0)); let provider: Arc = Arc::new(FailThenStreamProvider { call_count: call_count.clone(), - failures: 2, // fail twice, succeed on third + failures: 2, // fail twice, succeed on third }); let mut providers: HashMap> = HashMap::new(); @@ -2440,33 +2437,33 @@ mod tests { "mock" } - async fn complete(&self, _request: &Request) -> Result { + async fn complete(&self, _request: &Request) -> Result { Ok(Response { - id: "resp_1".into(), - model: "mock-model".into(), - provider: "mock".into(), - message: Message::assistant("fallback"), + id: "resp_1".into(), + model: "mock-model".into(), + provider: "mock".into(), + message: Message::assistant("fallback"), finish_reason: FinishReason::Stop, - usage: TokenCounts::default(), - raw: None, - warnings: vec![], - rate_limit: None, + usage: TokenCounts::default(), + raw: None, + warnings: vec![], + rate_limit: None, }) } - async fn stream(&self, _request: &Request) -> Result { + async fn stream(&self, _request: &Request) -> Result { sleep(self.delay).await; let text = "Slow response"; let response = Response { - id: "resp_1".into(), - model: "mock-model".into(), - provider: "mock".into(), - message: Message::assistant(text), + id: "resp_1".into(), + model: "mock-model".into(), + provider: "mock".into(), + message: Message::assistant(text), finish_reason: FinishReason::Stop, - usage: TokenCounts::default(), - raw: None, - warnings: vec![], - rate_limit: None, + usage: TokenCounts::default(), + raw: None, + warnings: vec![], + rate_limit: None, }; let events = vec![ Ok(StreamEvent::text_delta(text, Some("t1".into()))), @@ -2519,7 +2516,7 @@ mod tests { // Should have received a timeout error let has_timeout = events .iter() - .any(|e| matches!(e, Err(SdkError::RequestTimeout { .. }))); + .any(|e| matches!(e, Err(Error::RequestTimeout { .. }))); assert!(has_timeout, "Expected a RequestTimeout error"); } @@ -2539,21 +2536,21 @@ mod tests { "mock" } - async fn complete(&self, _request: &Request) -> Result { + async fn complete(&self, _request: &Request) -> Result { Ok(Response { - id: "resp_1".into(), - model: "mock-model".into(), - provider: "mock".into(), - message: Message::assistant("fallback"), + id: "resp_1".into(), + model: "mock-model".into(), + provider: "mock".into(), + message: Message::assistant("fallback"), finish_reason: FinishReason::Stop, - usage: TokenCounts::default(), - raw: None, - warnings: vec![], - rate_limit: None, + usage: TokenCounts::default(), + raw: None, + warnings: vec![], + rate_limit: None, }) } - async fn stream(&self, _request: &Request) -> Result { + async fn stream(&self, _request: &Request) -> Result { let count = self.call_count.fetch_add(1, Ordering::SeqCst); if count == 0 { @@ -2561,20 +2558,20 @@ mod tests { let tool_call = ToolCall::new("call_1", "get_weather", serde_json::json!({"city": "SF"})); let response = Response { - id: "resp_1".into(), - model: "mock-model".into(), - provider: "mock".into(), - message: Message { - role: Role::Assistant, - content: vec![ContentPart::ToolCall(tool_call.clone())], - name: None, + id: "resp_1".into(), + model: "mock-model".into(), + provider: "mock".into(), + message: Message { + role: Role::Assistant, + content: vec![ContentPart::ToolCall(tool_call.clone())], + name: None, tool_call_id: None, }, finish_reason: FinishReason::ToolCalls, - usage: TokenCounts::default(), - raw: None, - warnings: vec![], - rate_limit: None, + usage: TokenCounts::default(), + raw: None, + warnings: vec![], + rate_limit: None, }; let events = vec![ Ok(StreamEvent::ToolCallEnd { tool_call }), @@ -2590,15 +2587,15 @@ mod tests { sleep(std::time::Duration::from_secs(5)).await; let text = "Should not arrive"; let response = Response { - id: "resp_2".into(), - model: "mock-model".into(), - provider: "mock".into(), - message: Message::assistant(text), + id: "resp_2".into(), + model: "mock-model".into(), + provider: "mock".into(), + message: Message::assistant(text), finish_reason: FinishReason::Stop, - usage: TokenCounts::default(), - raw: None, - warnings: vec![], - rate_limit: None, + usage: TokenCounts::default(), + raw: None, + warnings: vec![], + rate_limit: None, }; let events = vec![ Ok(StreamEvent::text_delta(text, Some("t1".into()))), @@ -2651,7 +2648,7 @@ mod tests { // Should have received a total timeout error let has_timeout = events .iter() - .any(|e| matches!(e, Err(SdkError::RequestTimeout { .. }))); + .any(|e| matches!(e, Err(Error::RequestTimeout { .. }))); assert!( has_timeout, "Expected a RequestTimeout error from total timeout" diff --git a/lib/crates/fabro-llm/src/lib.rs b/lib/crates/fabro-llm/src/lib.rs index 680229d4b..323d5abe6 100644 --- a/lib/crates/fabro-llm/src/lib.rs +++ b/lib/crates/fabro-llm/src/lib.rs @@ -10,6 +10,6 @@ pub mod tools; pub mod types; // Re-export module-level default client helpers (Section 2.5). -pub use error::{Error, ProviderErrorDetail, ProviderErrorKind, Result, SdkError}; +pub use error::{Error, ProviderErrorDetail, ProviderErrorKind, Result}; pub use fabro_model::{ModelHandle, Provider}; pub use generate::set_default_client; diff --git a/lib/crates/fabro-llm/src/middleware.rs b/lib/crates/fabro-llm/src/middleware.rs index eb4757705..ba8fef42d 100644 --- a/lib/crates/fabro-llm/src/middleware.rs +++ b/lib/crates/fabro-llm/src/middleware.rs @@ -2,20 +2,18 @@ use std::future::Future; use std::pin::Pin; use std::sync::Arc; -use crate::error::SdkError; +use crate::error::Error; use crate::provider::StreamEventStream; use crate::types::{Request, Response}; /// The next handler in the middleware chain. pub type NextFn = Arc< - dyn Fn(Request) -> Pin> + Send>> - + Send - + Sync, + dyn Fn(Request) -> Pin> + Send>> + Send + Sync, >; /// The next handler for streaming. pub type NextStreamFn = Arc< - dyn Fn(Request) -> Pin> + Send>> + dyn Fn(Request) -> Pin> + Send>> + Send + Sync, >; @@ -23,11 +21,11 @@ pub type NextStreamFn = Arc< /// Middleware for intercepting `complete()` and streaming calls (Section 2.3). #[async_trait::async_trait] pub trait Middleware: Send + Sync { - async fn handle_complete(&self, request: Request, next: NextFn) -> Result; + async fn handle_complete(&self, request: Request, next: NextFn) -> Result; async fn handle_stream( &self, request: Request, next: NextStreamFn, - ) -> Result; + ) -> Result; } diff --git a/lib/crates/fabro-llm/src/model_test.rs b/lib/crates/fabro-llm/src/model_test.rs index edf6e8155..4fe1efced 100644 --- a/lib/crates/fabro-llm/src/model_test.rs +++ b/lib/crates/fabro-llm/src/model_test.rs @@ -65,7 +65,7 @@ impl ModelTestStatus { #[derive(Debug, Clone, PartialEq, Eq)] pub struct ModelTestOutcome { - pub status: ModelTestStatus, + pub status: ModelTestStatus, pub error_message: Option, } @@ -73,7 +73,7 @@ impl ModelTestOutcome { #[must_use] pub fn ok() -> Self { Self { - status: ModelTestStatus::Ok, + status: ModelTestStatus::Ok, error_message: None, } } @@ -81,7 +81,7 @@ impl ModelTestOutcome { #[must_use] pub fn error(message: impl Into) -> Self { Self { - status: ModelTestStatus::Error, + status: ModelTestStatus::Error, error_message: Some(message.into()), } } @@ -234,14 +234,14 @@ mod tests { display_name: "Test Model".to_string(), limits: ModelLimits { context_window: 200_000, - max_output: Some(8_000), + max_output: Some(8_000), }, training: None, knowledge_cutoff: None, features, costs: ModelCosts { - input_cost_per_mtok: None, - output_cost_per_mtok: None, + input_cost_per_mtok: None, + output_cost_per_mtok: None, cache_input_cost_per_mtok: None, }, estimated_output_tps: None, @@ -252,25 +252,25 @@ mod tests { fn response_with_text(text: &str) -> Response { Response { - id: "resp_1".to_string(), - model: "test-model".to_string(), - provider: "anthropic".to_string(), - message: Message::assistant(text), + id: "resp_1".to_string(), + model: "test-model".to_string(), + provider: "anthropic".to_string(), + message: Message::assistant(text), finish_reason: FinishReason::Stop, - usage: TokenCounts::default(), - raw: None, - warnings: vec![], - rate_limit: None, + usage: TokenCounts::default(), + raw: None, + warnings: vec![], + rate_limit: None, } } #[tokio::test] async fn run_model_test_deep_errors_when_model_lacks_tools() { let info = test_model_with(ModelFeatures { - tools: false, - vision: false, + tools: false, + vision: false, reasoning: true, - effort: true, + effort: true, }); let outcome = run_model_test(&info, ModelTestMode::Deep).await; @@ -286,11 +286,11 @@ mod tests { fn validate_deep_result_does_not_fail_only_for_missing_reasoning() { let tool_results = vec![ToolResult::success("call_1", serde_json::json!(42))]; let first_step = StepResult { - response: response_with_text("tool step"), + response: response_with_text("tool step"), tool_results: tool_results.clone(), }; let second_step = StepResult { - response: response_with_text("84 is even"), + response: response_with_text("84 is even"), tool_results: vec![], }; let result = GenerateResult { diff --git a/lib/crates/fabro-llm/src/provider.rs b/lib/crates/fabro-llm/src/provider.rs index 0dc0762ab..57bd38df8 100644 --- a/lib/crates/fabro-llm/src/provider.rs +++ b/lib/crates/fabro-llm/src/provider.rs @@ -3,7 +3,7 @@ use std::pin::Pin; pub use fabro_model::{ModelHandle, Provider}; use futures::Stream; -use crate::error::SdkError; +use crate::error::Error; use crate::types::{Request, Response, StreamEvent, ToolChoice}; // --------------------------------------------------------------------------- @@ -11,7 +11,7 @@ use crate::types::{Request, Response, StreamEvent, ToolChoice}; // --------------------------------------------------------------------------- /// Async stream of `StreamEvents` returned by streaming providers. -pub type StreamEventStream = Pin> + Send>>; +pub type StreamEventStream = Pin> + Send>>; /// The contract that every provider adapter must implement (Section 2.4). #[async_trait::async_trait] @@ -20,18 +20,18 @@ pub trait ProviderAdapter: Send + Sync { fn name(&self) -> &str; /// Send a request and block until the model finishes (Section 4.1). - async fn complete(&self, request: &Request) -> Result; + async fn complete(&self, request: &Request) -> Result; /// Send a request and return an async stream of events (Section 4.2). - async fn stream(&self, request: &Request) -> Result; + async fn stream(&self, request: &Request) -> Result; /// Release resources. Called by `Client::close()`. - async fn close(&self) -> Result<(), SdkError> { + async fn close(&self) -> Result<(), Error> { Ok(()) } /// Validate configuration on startup. Called by Client on registration. - async fn initialize(&self) -> Result<(), SdkError> { + async fn initialize(&self) -> Result<(), Error> { Ok(()) } @@ -43,20 +43,20 @@ pub trait ProviderAdapter: Send + Sync { /// Validate that the adapter supports the requested tool choice mode. /// -/// Returns `Err(SdkError::UnsupportedToolChoice)` if the adapter does not +/// Returns `Err(Error::UnsupportedToolChoice)` if the adapter does not /// support the given mode. /// /// # Errors /// -/// Returns `SdkError::UnsupportedToolChoice` when the adapter does not +/// Returns `Error::UnsupportedToolChoice` when the adapter does not /// support the requested tool choice mode. pub fn validate_tool_choice( adapter: &dyn ProviderAdapter, tool_choice: &ToolChoice, -) -> Result<(), SdkError> { +) -> Result<(), Error> { let mode = tool_choice.mode_str(); if !adapter.supports_tool_choice(mode) { - return Err(SdkError::UnsupportedToolChoice { + return Err(Error::UnsupportedToolChoice { message: format!( "provider '{}' does not support tool_choice mode '{mode}'", adapter.name() @@ -78,10 +78,10 @@ mod tests { fn name(&self) -> &'static str { "mock" } - async fn complete(&self, _request: &Request) -> Result { + async fn complete(&self, _request: &Request) -> Result { unimplemented!() } - async fn stream(&self, _request: &Request) -> Result { + async fn stream(&self, _request: &Request) -> Result { unimplemented!() } } @@ -94,10 +94,10 @@ mod tests { fn name(&self) -> &'static str { "restricted" } - async fn complete(&self, _request: &Request) -> Result { + async fn complete(&self, _request: &Request) -> Result { unimplemented!() } - async fn stream(&self, _request: &Request) -> Result { + async fn stream(&self, _request: &Request) -> Result { unimplemented!() } fn supports_tool_choice(&self, mode: &str) -> bool { @@ -125,7 +125,7 @@ mod tests { let result = validate_tool_choice(&RestrictedAdapter, &ToolChoice::named("my_tool")); assert!(result.is_err()); match result.unwrap_err() { - SdkError::UnsupportedToolChoice { message } => { + Error::UnsupportedToolChoice { message } => { assert!(message.contains("restricted")); assert!(message.contains("named")); } diff --git a/lib/crates/fabro-llm/src/providers/anthropic.rs b/lib/crates/fabro-llm/src/providers/anthropic.rs index 5d9f699d6..4ce868cf9 100644 --- a/lib/crates/fabro-llm/src/providers/anthropic.rs +++ b/lib/crates/fabro-llm/src/providers/anthropic.rs @@ -2,7 +2,7 @@ use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use futures::stream; -use crate::error::{SdkError, error_from_status_code}; +use crate::error::{Error, error_from_status_code}; use crate::provider::{ProviderAdapter, StreamEventStream, validate_tool_choice}; use crate::providers::common::{ self as common, extract_system_prompt, parse_error_body, parse_rate_limit_headers, @@ -17,14 +17,14 @@ use crate::types::{ /// Provider adapter for the Anthropic Messages API. pub struct Adapter { pub(crate) http: super::http_api::HttpApi, - provider_name: String, + provider_name: String, } impl Adapter { #[must_use] pub fn new(api_key: impl Into) -> Self { Self { - http: super::http_api::HttpApi::new(api_key, DEFAULT_BASE_URL), + http: super::http_api::HttpApi::new(api_key, DEFAULT_BASE_URL), provider_name: "anthropic".to_string(), } } @@ -64,7 +64,7 @@ impl Adapter { /// Collect a streaming response into a single [`Response`]. /// /// Used by non-Anthropic providers (e.g. Kimi) that require `stream=true`. - async fn complete_via_stream(&self, request: &Request) -> Result { + async fn complete_via_stream(&self, request: &Request) -> Result { use futures::StreamExt; let mut stream = self.stream(request).await?; @@ -76,9 +76,9 @@ impl Adapter { } } - response.ok_or_else(|| SdkError::Stream { + response.ok_or_else(|| Error::Stream { message: "complete_via_stream: stream ended without a Finish event".to_string(), - source: None, + source: None, }) } } @@ -89,51 +89,51 @@ const DEFAULT_BASE_URL: &str = "https://api.anthropic.com/v1"; #[derive(serde::Serialize)] struct ApiRequest { - model: String, - messages: Vec, - max_tokens: i64, + model: String, + messages: Vec, + max_tokens: i64, /// System prompt: either a plain string or an array of content blocks /// (with optional `cache_control` annotations for prompt caching). #[serde(skip_serializing_if = "Option::is_none")] - system: Option, + system: Option, #[serde(skip_serializing_if = "Option::is_none")] - temperature: Option, + temperature: Option, #[serde(skip_serializing_if = "Option::is_none")] - top_p: Option, + top_p: Option, #[serde(skip_serializing_if = "Option::is_none")] stop_sequences: Option>, #[serde(skip_serializing_if = "Option::is_none")] - tools: Option>, + tools: Option>, #[serde(skip_serializing_if = "Option::is_none")] - tool_choice: Option, + tool_choice: Option, /// Extended thinking configuration (e.g. `{"type": "enabled", /// "budget_tokens": 10000}`). Passed through from /// `provider_options.anthropic.thinking`. #[serde(skip_serializing_if = "Option::is_none")] - thinking: Option, + thinking: Option, #[serde(skip_serializing_if = "Option::is_none")] - output_config: Option, + output_config: Option, #[serde(skip_serializing_if = "Option::is_none")] - speed: Option, + speed: Option, #[serde(skip_serializing_if = "Option::is_none")] - metadata: Option>, + metadata: Option>, #[serde(skip_serializing_if = "std::ops::Not::not")] - stream: bool, + stream: bool, } /// Anthropic messages use structured content blocks, not plain strings. #[derive(serde::Serialize)] struct ApiMessage { - role: String, + role: String, content: Vec, } /// Anthropic tool definition format. #[derive(serde::Serialize)] struct ApiToolDef { - name: String, - description: String, - input_schema: serde_json::Value, + name: String, + description: String, + input_schema: serde_json::Value, #[serde(skip_serializing_if = "Option::is_none")] cache_control: Option, } @@ -157,20 +157,20 @@ impl CacheControl { #[derive(serde::Deserialize)] struct ApiResponse { - id: String, - model: String, - content: Vec, + id: String, + model: String, + content: Vec, stop_reason: Option, - usage: ApiUsage, + usage: ApiUsage, } #[derive(serde::Deserialize)] #[allow(clippy::struct_field_names)] // Match Anthropic's API usage payload field names. struct ApiUsage { - input_tokens: i64, - output_tokens: i64, + input_tokens: i64, + output_tokens: i64, #[serde(default)] - cache_read_input_tokens: Option, + cache_read_input_tokens: Option, #[serde(default)] cache_creation_input_tokens: Option, } @@ -211,21 +211,21 @@ fn parse_content_block(block: &serde_json::Value) -> Option { block.get("input")?.clone(), ))), "thinking" => Some(ContentPart::Thinking(ThinkingData { - text: block.get("thinking")?.as_str()?.to_string(), + text: block.get("thinking")?.as_str()?.to_string(), signature: block .get("signature") .and_then(serde_json::Value::as_str) .map(String::from), - redacted: false, + redacted: false, })), "redacted_thinking" => Some(ContentPart::Thinking(ThinkingData { - text: block + text: block .get("data") .and_then(serde_json::Value::as_str) .unwrap_or("") .to_string(), signature: None, - redacted: true, + redacted: true, })), _ => None, } @@ -360,9 +360,9 @@ fn translate_tools(tools: &[ToolDefinition]) -> Vec { tools .iter() .map(|t| ApiToolDef { - name: t.name.clone(), - description: t.description.clone(), - input_schema: t.parameters.clone(), + name: t.name.clone(), + description: t.description.clone(), + input_schema: t.parameters.clone(), cache_control: None, }) .collect() @@ -409,9 +409,9 @@ fn apply_response_format( .clone() .unwrap_or_else(|| serde_json::json!({"type": "object"})); let synthetic_tool = ApiToolDef { - name: SYNTHETIC_TOOL_NAME.to_string(), - description: "Output the requested structured data".to_string(), - input_schema: schema, + name: SYNTHETIC_TOOL_NAME.to_string(), + description: "Output the requested structured data".to_string(), + input_schema: schema, cache_control: None, }; match api_tools { @@ -665,21 +665,21 @@ enum ContentBlockKind { /// Accumulated state across SSE events during streaming. struct StreamAccumulator { - id: String, - model: String, - content_parts: Vec, - usage: TokenCounts, - finish_reason: FinishReason, + id: String, + model: String, + content_parts: Vec, + usage: TokenCounts, + finish_reason: FinishReason, /// The kind of the current content block, set by `content_block_start`. - current_block: Option, + current_block: Option, /// Accumulated text for the current text block. - current_text: String, + current_text: String, /// Accumulated thinking text for the current thinking block. - current_thinking: String, + current_thinking: String, /// 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 { @@ -703,20 +703,20 @@ impl StreamAccumulator { fn take_response(&mut self) -> Response { let content_parts = std::mem::take(&mut self.content_parts); Response { - id: self.id.clone(), - model: self.model.clone(), - provider: "anthropic".to_string(), - message: Message { - role: Role::Assistant, - content: content_parts, - name: None, + id: self.id.clone(), + model: self.model.clone(), + provider: "anthropic".to_string(), + message: Message { + role: Role::Assistant, + content: content_parts, + name: None, tool_call_id: None, }, finish_reason: self.finish_reason.clone(), - usage: self.usage.clone(), - raw: None, - warnings: vec![], - rate_limit: self.rate_limit.clone(), + usage: self.usage.clone(), + raw: None, + warnings: vec![], + rate_limit: self.rate_limit.clone(), } } } @@ -780,7 +780,7 @@ impl StreamAccumulator { .unwrap_or("") .to_string(); self.current_block = Some(ContentBlockKind::ToolUse { - id: id.clone(), + id: id.clone(), name: name.clone(), }); self.current_tool_args.clear(); @@ -823,7 +823,7 @@ impl StreamAccumulator { .unwrap_or(0); vec![StreamEvent::TextDelta { - delta: text.to_string(), + delta: text.to_string(), text_id: Some(format!("block_{index}")), }] } @@ -909,9 +909,9 @@ impl StreamAccumulator { .and_then(serde_json::Value::as_str) .map(String::from); self.content_parts.push(ContentPart::Thinking(ThinkingData { - text: thinking_text, + text: thinking_text, signature: stop_signature.or(signature), - redacted: false, + redacted: false, })); vec![StreamEvent::ReasoningEnd] } @@ -939,8 +939,8 @@ impl StreamAccumulator { let response = self.take_response(); vec![StreamEvent::Finish { finish_reason: response.finish_reason.clone(), - usage: response.usage.clone(), - response: Box::new(response), + usage: response.usage.clone(), + response: Box::new(response), }] } } @@ -968,18 +968,15 @@ fn process_sse_event( // --- SSE reader --- enum SseResult { - Event { - event_type: String, - data: String, - }, + Event { event_type: String, data: String }, Done, - Error(SdkError), + Error(Error), } struct SseReaderState { - line_reader: super::common::LineReader, - accumulator: StreamAccumulator, - pending_events: std::collections::VecDeque, + line_reader: super::common::LineReader, + accumulator: StreamAccumulator, + pending_events: std::collections::VecDeque, /// When true, `tool_use` events for the synthetic tool are converted to /// text events. json_schema_mode: bool, @@ -1230,7 +1227,7 @@ impl ProviderAdapter for Adapter { &self.provider_name } - async fn complete(&self, request: &Request) -> Result { + async fn complete(&self, request: &Request) -> Result { if let Some(tc) = &request.tool_choice { validate_tool_choice(self, tc)?; } @@ -1250,7 +1247,7 @@ impl ProviderAdapter for Adapter { let (body, headers) = send_and_read_response(req, &self.provider_name, "type").await?; let api_resp: ApiResponse = serde_json::from_str(&body).map_err(|e| { - SdkError::network( + Error::network( format!("failed to parse {} response: {e}", self.provider_name), e, ) @@ -1283,9 +1280,9 @@ impl ProviderAdapter for Adapter { model: api_resp.model, provider: self.provider_name.clone(), message: Message { - role: Role::Assistant, - content: content_parts, - name: None, + role: Role::Assistant, + content: content_parts, + name: None, tool_call_id: None, }, finish_reason, @@ -1305,7 +1302,7 @@ impl ProviderAdapter for Adapter { }) } - async fn stream(&self, request: &Request) -> Result { + async fn stream(&self, request: &Request) -> Result { if let Some(tc) = &request.tool_choice { validate_tool_choice(self, tc)?; } @@ -1314,7 +1311,7 @@ impl ProviderAdapter for Adapter { let http_resp = req_builder .send() .await - .map_err(|e| SdkError::network(e.to_string(), e))?; + .map_err(|e| Error::network(e.to_string(), e))?; let status = http_resp.status(); if !status.is_success() { @@ -1322,7 +1319,7 @@ impl ProviderAdapter for Adapter { let body = http_resp .text() .await - .map_err(|e| SdkError::network(e.to_string(), e))?; + .map_err(|e| Error::network(e.to_string(), e))?; let (msg, code, raw) = parse_error_body(&body, "type"); return Err(error_from_status_code( status.as_u16(), @@ -1359,7 +1356,7 @@ impl ProviderAdapter for Adapter { Ok(v) => v, Err(e) => { return Some(( - Err(SdkError::stream_error( + Err(Error::stream_error( format!("failed to parse SSE data: {e}"), e, )), @@ -1447,15 +1444,15 @@ mod tests { fn tool_cache_control_applied_to_last_tool() { let mut tools = vec![ ApiToolDef { - name: "tool_a".to_string(), - description: "first".to_string(), - input_schema: serde_json::json!({}), + name: "tool_a".to_string(), + description: "first".to_string(), + input_schema: serde_json::json!({}), cache_control: None, }, ApiToolDef { - name: "tool_b".to_string(), - description: "second".to_string(), - input_schema: serde_json::json!({}), + name: "tool_b".to_string(), + description: "second".to_string(), + input_schema: serde_json::json!({}), cache_control: None, }, ]; @@ -1476,9 +1473,9 @@ mod tests { #[test] fn tool_cache_control_single_tool() { let mut tools = vec![ApiToolDef { - name: "only_tool".to_string(), - description: "the one".to_string(), - input_schema: serde_json::json!({}), + name: "only_tool".to_string(), + description: "the one".to_string(), + input_schema: serde_json::json!({}), cache_control: None, }]; apply_cache_control_to_last_tool(&mut tools); @@ -1489,15 +1486,15 @@ mod tests { fn conversation_prefix_cache_control_with_two_user_messages() { let mut messages = vec![ ApiMessage { - role: "user".to_string(), + role: "user".to_string(), content: vec![serde_json::json!({"type": "text", "text": "Hello"})], }, ApiMessage { - role: "assistant".to_string(), + role: "assistant".to_string(), content: vec![serde_json::json!({"type": "text", "text": "Hi there"})], }, ApiMessage { - role: "user".to_string(), + role: "user".to_string(), content: vec![serde_json::json!({"type": "text", "text": "How are you?"})], }, ]; @@ -1516,18 +1513,18 @@ mod tests { fn conversation_prefix_cache_control_with_multiple_content_blocks() { let mut messages = vec![ ApiMessage { - role: "user".to_string(), + role: "user".to_string(), content: vec![ serde_json::json!({"type": "text", "text": "Part 1"}), serde_json::json!({"type": "text", "text": "Part 2"}), ], }, ApiMessage { - role: "assistant".to_string(), + role: "assistant".to_string(), content: vec![serde_json::json!({"type": "text", "text": "Reply"})], }, ApiMessage { - role: "user".to_string(), + role: "user".to_string(), content: vec![serde_json::json!({"type": "text", "text": "Follow up"})], }, ]; @@ -1543,7 +1540,7 @@ mod tests { #[test] fn conversation_prefix_cache_control_single_user_message() { let mut messages = vec![ApiMessage { - role: "user".to_string(), + role: "user".to_string(), content: vec![serde_json::json!({"type": "text", "text": "Hello"})], }]; @@ -1564,23 +1561,23 @@ mod tests { fn conversation_prefix_cache_control_three_user_messages() { let mut messages = vec![ ApiMessage { - role: "user".to_string(), + role: "user".to_string(), content: vec![serde_json::json!({"type": "text", "text": "First"})], }, ApiMessage { - role: "assistant".to_string(), + role: "assistant".to_string(), content: vec![serde_json::json!({"type": "text", "text": "Reply 1"})], }, ApiMessage { - role: "user".to_string(), + role: "user".to_string(), content: vec![serde_json::json!({"type": "text", "text": "Second"})], }, ApiMessage { - role: "assistant".to_string(), + role: "assistant".to_string(), content: vec![serde_json::json!({"type": "text", "text": "Reply 2"})], }, ApiMessage { - role: "user".to_string(), + role: "user".to_string(), content: vec![serde_json::json!({"type": "text", "text": "Third"})], }, ]; @@ -1647,9 +1644,9 @@ mod tests { #[test] fn tool_serialization_includes_cache_control() { let tool = ApiToolDef { - name: "test_tool".to_string(), - description: "A test tool".to_string(), - input_schema: serde_json::json!({"type": "object"}), + name: "test_tool".to_string(), + description: "A test tool".to_string(), + input_schema: serde_json::json!({"type": "object"}), cache_control: Some(CacheControl::ephemeral()), }; let json = serde_json::to_value(&tool).expect("should serialize"); @@ -1659,9 +1656,9 @@ mod tests { #[test] fn tool_serialization_omits_cache_control_when_none() { let tool = ApiToolDef { - name: "test_tool".to_string(), - description: "A test tool".to_string(), - input_schema: serde_json::json!({"type": "object"}), + name: "test_tool".to_string(), + description: "A test tool".to_string(), + input_schema: serde_json::json!({"type": "object"}), cache_control: None, }; let json = serde_json::to_value(&tool).expect("should serialize"); @@ -1678,23 +1675,23 @@ mod tests { #[test] fn api_request_serialization_with_cached_system() { let api_request = ApiRequest { - model: "claude-sonnet-4-20250514".to_string(), - messages: vec![ApiMessage { - role: "user".to_string(), + model: "claude-sonnet-4-20250514".to_string(), + messages: vec![ApiMessage { + role: "user".to_string(), content: vec![serde_json::json!({"type": "text", "text": "Hello"})], }], - max_tokens: 4096, - system: Some(system_with_cache_control("You are helpful.")), - temperature: None, - top_p: None, + max_tokens: 4096, + system: Some(system_with_cache_control("You are helpful.")), + temperature: None, + top_p: None, stop_sequences: None, - tools: None, - tool_choice: None, - thinking: None, - output_config: None, - speed: None, - metadata: None, - stream: false, + tools: None, + tool_choice: None, + thinking: None, + output_config: None, + speed: None, + metadata: None, + stream: false, }; let json = serde_json::to_value(&api_request).expect("should serialize"); @@ -1721,19 +1718,19 @@ mod tests { fn make_base_request() -> Request { Request { - model: "claude-sonnet-4-20250514".to_string(), - messages: vec![Message::user("Hello")], - provider: Some("anthropic".to_string()), - tools: None, - tool_choice: None, - response_format: None, - temperature: None, - top_p: None, - max_tokens: Some(128), - stop_sequences: None, + model: "claude-sonnet-4-20250514".to_string(), + messages: vec![Message::user("Hello")], + provider: Some("anthropic".to_string()), + tools: None, + tool_choice: None, + response_format: None, + temperature: None, + top_p: None, + max_tokens: Some(128), + stop_sequences: None, reasoning_effort: None, - speed: None, - metadata: None, + speed: None, + metadata: None, provider_options: None, } } @@ -1755,9 +1752,9 @@ mod tests { "required": ["name"] }); let request = make_request_with_format(ResponseFormat { - kind: ResponseFormatType::JsonSchema, + kind: ResponseFormatType::JsonSchema, json_schema: Some(schema.clone()), - strict: false, + strict: false, }); let mut tools: Option> = None; @@ -1783,14 +1780,14 @@ mod tests { fn response_format_json_schema_appends_to_existing_tools() { let schema = serde_json::json!({"type": "object"}); let mut request = make_request_with_format(ResponseFormat { - kind: ResponseFormatType::JsonSchema, + kind: ResponseFormatType::JsonSchema, json_schema: Some(schema), - strict: false, + strict: false, }); request.tools = Some(vec![ToolDefinition { - name: "existing_tool".to_string(), + name: "existing_tool".to_string(), description: "An existing tool".to_string(), - parameters: serde_json::json!({}), + parameters: serde_json::json!({}), }]); let mut tools: Option> = @@ -1809,9 +1806,9 @@ mod tests { #[test] fn response_format_json_object_appends_to_string_system() { let request = make_request_with_format(ResponseFormat { - kind: ResponseFormatType::JsonObject, + kind: ResponseFormatType::JsonObject, json_schema: None, - strict: false, + strict: false, }); let mut tools: Option> = None; @@ -1833,9 +1830,9 @@ mod tests { #[test] fn response_format_json_object_sets_system_when_none() { let request = make_request_with_format(ResponseFormat { - kind: ResponseFormatType::JsonObject, + kind: ResponseFormatType::JsonObject, json_schema: None, - strict: false, + strict: false, }); let mut tools: Option> = None; @@ -1852,9 +1849,9 @@ mod tests { #[test] fn response_format_json_object_appends_to_array_system() { let request = make_request_with_format(ResponseFormat { - kind: ResponseFormatType::JsonObject, + kind: ResponseFormatType::JsonObject, json_schema: None, - strict: false, + strict: false, }); let mut tools: Option> = None; @@ -1873,9 +1870,9 @@ mod tests { #[test] fn response_format_text_is_noop() { let request = make_request_with_format(ResponseFormat { - kind: ResponseFormatType::Text, + kind: ResponseFormatType::Text, json_schema: None, - strict: false, + strict: false, }); let mut tools: Option> = None; @@ -1958,24 +1955,24 @@ mod tests { #[test] fn convert_stream_event_converts_finish_reason() { let response = Box::new(Response { - id: "test".to_string(), - model: "claude".to_string(), - provider: "anthropic".to_string(), - message: Message { - role: Role::Assistant, - content: vec![ContentPart::ToolCall(ToolCall::new( + id: "test".to_string(), + model: "claude".to_string(), + provider: "anthropic".to_string(), + message: Message { + role: Role::Assistant, + content: vec![ContentPart::ToolCall(ToolCall::new( "id1", SYNTHETIC_TOOL_NAME, serde_json::json!({"data": "value"}), ))], - name: None, + name: None, tool_call_id: None, }, finish_reason: FinishReason::ToolCalls, - usage: TokenCounts::default(), - raw: None, - warnings: vec![], - rate_limit: None, + usage: TokenCounts::default(), + raw: None, + warnings: vec![], + rate_limit: None, }); let event = StreamEvent::Finish { finish_reason: FinishReason::ToolCalls, @@ -2001,10 +1998,10 @@ mod tests { #[test] fn document_url_translates_to_url_source() { let part = ContentPart::Document(DocumentData { - url: Some("https://example.com/doc.pdf".to_string()), - data: None, + url: Some("https://example.com/doc.pdf".to_string()), + data: None, media_type: None, - file_name: None, + file_name: None, }); let result = content_part_to_api(&part).expect("should produce JSON"); assert_eq!(result["type"], "document"); @@ -2015,10 +2012,10 @@ mod tests { #[test] fn document_base64_data_translates_to_base64_source() { let part = ContentPart::Document(DocumentData { - url: None, - data: Some(vec![0x25, 0x50, 0x44, 0x46]), + url: None, + data: Some(vec![0x25, 0x50, 0x44, 0x46]), media_type: Some("application/pdf".to_string()), - file_name: Some("test.pdf".to_string()), + file_name: Some("test.pdf".to_string()), }); let result = content_part_to_api(&part).expect("should produce JSON"); assert_eq!(result["type"], "document"); @@ -2030,10 +2027,10 @@ mod tests { #[test] fn document_base64_defaults_to_pdf_mime() { let part = ContentPart::Document(DocumentData { - url: None, - data: Some(vec![1, 2, 3]), + url: None, + data: Some(vec![1, 2, 3]), media_type: None, - file_name: None, + file_name: None, }); let result = content_part_to_api(&part).expect("should produce JSON"); assert_eq!(result["source"]["media_type"], "application/pdf"); @@ -2075,23 +2072,23 @@ mod tests { #[test] fn merge_provider_options_passes_through_unknown_keys() { let api_request = ApiRequest { - model: "claude-sonnet-4-20250514".to_string(), - messages: vec![ApiMessage { - role: "user".to_string(), + model: "claude-sonnet-4-20250514".to_string(), + messages: vec![ApiMessage { + role: "user".to_string(), content: vec![serde_json::json!({"type": "text", "text": "Hello"})], }], - max_tokens: 4096, - system: None, - temperature: None, - top_p: None, + max_tokens: 4096, + system: None, + temperature: None, + top_p: None, stop_sequences: None, - tools: None, - tool_choice: None, - thinking: None, - output_config: None, - speed: None, - metadata: None, - stream: false, + tools: None, + tool_choice: None, + thinking: None, + output_config: None, + speed: None, + metadata: None, + stream: false, }; let opts = serde_json::json!({ @@ -2108,23 +2105,23 @@ mod tests { #[test] fn merge_provider_options_skips_known_keys() { let api_request = ApiRequest { - model: "claude-sonnet-4-20250514".to_string(), - messages: vec![ApiMessage { - role: "user".to_string(), + model: "claude-sonnet-4-20250514".to_string(), + messages: vec![ApiMessage { + role: "user".to_string(), content: vec![serde_json::json!({"type": "text", "text": "Hello"})], }], - max_tokens: 4096, - system: None, - temperature: None, - top_p: None, + max_tokens: 4096, + system: None, + temperature: None, + top_p: None, stop_sequences: None, - tools: None, - tool_choice: None, - thinking: None, - output_config: None, - speed: None, - metadata: None, - stream: false, + tools: None, + tool_choice: None, + thinking: None, + output_config: None, + speed: None, + metadata: None, + stream: false, }; let opts = serde_json::json!({ @@ -2149,8 +2146,8 @@ mod tests { #[test] fn audio_produces_text_fallback() { let part = ContentPart::Audio(AudioData { - url: Some("https://example.com/audio.wav".to_string()), - data: None, + url: Some("https://example.com/audio.wav".to_string()), + data: None, media_type: None, }); let result = content_part_to_api(&part).expect("should produce JSON"); diff --git a/lib/crates/fabro-llm/src/providers/common.rs b/lib/crates/fabro-llm/src/providers/common.rs index b4822dae5..57007c48e 100644 --- a/lib/crates/fabro-llm/src/providers/common.rs +++ b/lib/crates/fabro-llm/src/providers/common.rs @@ -4,7 +4,7 @@ use reqwest::header::HeaderMap; use tokio::time; use tracing::warn; -use crate::error::{SdkError, error_from_status_code}; +use crate::error::{Error, error_from_status_code}; use crate::types::{Message, RateLimitInfo, Role}; /// Parse an error response body, extracting the message and error code. @@ -166,20 +166,20 @@ pub fn parse_rate_limit_headers(headers: &HeaderMap) -> Option { /// /// # Errors /// -/// Returns `SdkError::Network` on connection failure or `SdkError::Provider` on +/// Returns `Error::Network` on connection failure or `Error::Provider` on /// non-success status. pub async fn send_and_read_response( request: reqwest::RequestBuilder, provider: &str, error_code_field: &str, -) -> Result<(String, HeaderMap), SdkError> { +) -> Result<(String, HeaderMap), Error> { let http_resp = request.send().await.map_err(|e| { if e.is_timeout() { warn!(provider = %provider, error = %e, "Provider request timed out"); - SdkError::request_timeout(format!("{provider}: {e}"), e) + Error::request_timeout(format!("{provider}: {e}"), e) } else { warn!(provider = %provider, error = %e, "Provider network error"); - SdkError::network(e.to_string(), e) + Error::network(e.to_string(), e) } })?; @@ -189,7 +189,7 @@ pub async fn send_and_read_response( let body = http_resp .text() .await - .map_err(|e| SdkError::network(e.to_string(), e))?; + .map_err(|e| Error::network(e.to_string(), e))?; if !status.is_success() { warn!(provider = %provider, status = status.as_u16(), "Provider returned error"); @@ -213,8 +213,8 @@ pub async fn send_and_read_response( /// delimiter (e.g. `"\n"` for Gemini/OpenAI-compatible, `"\n\n"` for /// Anthropic/OpenAI SSE event blocks). pub struct LineReader { - response: reqwest::Response, - buffer: String, + response: reqwest::Response, + buffer: String, stream_read_timeout: Option, } @@ -236,7 +236,7 @@ impl LineReader { /// the stream is exhausted, or `Err` on I/O or timeout errors. When the /// stream ends with data remaining in the buffer, the leftover is returned /// as a final segment. - pub async fn read_next_chunk(&mut self, delimiter: &str) -> Result, SdkError> { + pub async fn read_next_chunk(&mut self, delimiter: &str) -> Result, Error> { loop { if let Some(pos) = self.buffer.find(delimiter) { let segment = self.buffer[..pos].to_string(); @@ -261,13 +261,13 @@ impl LineReader { return Ok(Some(remaining)); } Ok(Err(e)) => { - return Err(SdkError::stream_error(e.to_string(), e)); + return Err(Error::stream_error(e.to_string(), e)); } Err(_) => { warn!("Stream read timed out waiting for next event"); - return Err(SdkError::Stream { + return Err(Error::Stream { message: "stream read timed out waiting for next event".to_string(), - source: None, + source: None, }); } } @@ -467,9 +467,9 @@ mod tests { #[test] fn extract_system_prompt_developer_role() { let dev = Message { - role: Role::Developer, - content: vec![ContentPart::text("dev instructions")], - name: None, + role: Role::Developer, + content: vec![ContentPart::text("dev instructions")], + name: None, tool_call_id: None, }; let msgs = vec![dev, Message::user("hi")]; @@ -481,9 +481,9 @@ mod tests { #[test] fn extract_system_prompt_ignores_whitespace_system_and_developer() { let dev = Message { - role: Role::Developer, - content: vec![ContentPart::text(" \n\t ")], - name: None, + role: Role::Developer, + content: vec![ContentPart::text(" \n\t ")], + name: None, tool_call_id: None, }; let msgs = vec![Message::system(" "), dev, Message::user("hi")]; diff --git a/lib/crates/fabro-llm/src/providers/fabro_server.rs b/lib/crates/fabro-llm/src/providers/fabro_server.rs index 21477aab4..35180a801 100644 --- a/lib/crates/fabro-llm/src/providers/fabro_server.rs +++ b/lib/crates/fabro-llm/src/providers/fabro_server.rs @@ -1,7 +1,7 @@ use futures::stream; use tracing::{debug, error}; -use crate::error::{SdkError, error_from_status_code}; +use crate::error::{Error, error_from_status_code}; use crate::provider::{ProviderAdapter, StreamEventStream}; use crate::providers::common::LineReader; use crate::types::{FinishReason, Message, Request, Response, StreamEvent, TokenCounts}; @@ -10,8 +10,8 @@ use crate::types::{FinishReason, Message, Request, Response, StreamEvent, TokenC /// `/completions` endpoint, delegating to whatever real provider the server /// is configured with. pub struct Adapter { - client: reqwest::Client, - base_url: String, + client: reqwest::Client, + base_url: String, provider_name: String, } @@ -35,16 +35,16 @@ impl Adapter { #[derive(serde::Deserialize)] struct ServerCompletionResponse { - id: String, - model: String, - message: Message, + id: String, + model: String, + message: Message, stop_reason: String, - usage: ServerUsage, + usage: ServerUsage, } #[derive(serde::Deserialize)] struct ServerUsage { - input_tokens: i64, + input_tokens: i64, output_tokens: i64, } @@ -63,10 +63,9 @@ fn map_stop_reason(reason: &str) -> FinishReason { /// Build the JSON request body by serializing the `Request` and injecting /// the `stream` flag. -fn build_body(request: &Request, stream: bool) -> Result { - let mut body = serde_json::to_value(request).map_err(|e| { - SdkError::configuration_error(format!("failed to serialize request: {e}"), e) - })?; +fn build_body(request: &Request, stream: bool) -> Result { + let mut body = serde_json::to_value(request) + .map_err(|e| Error::configuration_error(format!("failed to serialize request: {e}"), e))?; body["stream"] = serde_json::Value::Bool(stream); Ok(body) } @@ -79,12 +78,12 @@ async fn send_request( url: &str, body: &serde_json::Value, provider: &str, -) -> Result { +) -> Result { let http_resp = client.post(url).json(body).send().await.map_err(|e| { if e.is_timeout() { - SdkError::request_timeout(e.to_string(), e) + Error::request_timeout(e.to_string(), e) } else { - SdkError::network(e.to_string(), e) + Error::network(e.to_string(), e) } })?; @@ -118,7 +117,7 @@ impl ProviderAdapter for Adapter { &self.provider_name } - async fn complete(&self, request: &Request) -> Result { + async fn complete(&self, request: &Request) -> Result { let url = format!("{}/completions", self.base_url); debug!(base_url = %url, provider = %self.provider_name, "Sending completion to fabro server"); @@ -128,11 +127,11 @@ impl ProviderAdapter for Adapter { let resp_body = http_resp .text() .await - .map_err(|e| SdkError::network(e.to_string(), e))?; + .map_err(|e| Error::network(e.to_string(), e))?; let server_resp: ServerCompletionResponse = serde_json::from_str(&resp_body).map_err(|e| { - SdkError::stream_error(format!("failed to parse completion response: {e}"), e) + Error::stream_error(format!("failed to parse completion response: {e}"), e) })?; let finish_reason = map_stop_reason(&server_resp.stop_reason); @@ -153,7 +152,7 @@ impl ProviderAdapter for Adapter { }) } - async fn stream(&self, request: &Request) -> Result { + async fn stream(&self, request: &Request) -> Result { let url = format!("{}/completions", self.base_url); debug!(base_url = %url, provider = %self.provider_name, "Sending completion to fabro server"); @@ -170,7 +169,7 @@ impl ProviderAdapter for Adapter { Ok(event) => return Some((Ok(event), reader)), Err(e) => { return Some(( - Err(SdkError::stream_error( + Err(Error::stream_error( format!("failed to parse stream event: {e}"), e, )), @@ -235,19 +234,19 @@ mod tests { fn make_request() -> Request { Request { - model: "test-model".to_string(), - messages: vec![Message::user("Hello")], - provider: None, - tools: None, - tool_choice: None, - response_format: None, - temperature: None, - top_p: None, - max_tokens: None, - stop_sequences: None, + model: "test-model".to_string(), + messages: vec![Message::user("Hello")], + provider: None, + tools: None, + tool_choice: None, + response_format: None, + temperature: None, + top_p: None, + max_tokens: None, + stop_sequences: None, reasoning_effort: None, - speed: None, - metadata: None, + speed: None, + metadata: None, provider_options: None, } } @@ -354,7 +353,7 @@ data: {\"type\":\"text_delta\",\"delta\":\" world\",\"text_id\":null}\n\ let err = adapter.complete(&make_request()).await.unwrap_err(); match &err { - SdkError::Provider { kind, detail } => { + Error::Provider { kind, detail } => { assert_eq!(*kind, ProviderErrorKind::Server); assert_eq!(detail.status_code, Some(502)); } @@ -378,7 +377,7 @@ data: {\"type\":\"text_delta\",\"delta\":\" world\",\"text_id\":null}\n\ panic!("expected error"); }; match &err { - SdkError::Provider { kind, detail } => { + Error::Provider { kind, detail } => { assert_eq!(*kind, ProviderErrorKind::Server); assert_eq!(detail.status_code, Some(502)); } diff --git a/lib/crates/fabro-llm/src/providers/gemini.rs b/lib/crates/fabro-llm/src/providers/gemini.rs index 0422b313e..931481644 100644 --- a/lib/crates/fabro-llm/src/providers/gemini.rs +++ b/lib/crates/fabro-llm/src/providers/gemini.rs @@ -4,8 +4,7 @@ use futures::stream; use reqwest::header::HeaderMap; use crate::error::{ - ProviderErrorDetail, ProviderErrorKind, SdkError, error_from_grpc_status, - error_from_status_code, + Error, ProviderErrorDetail, ProviderErrorKind, error_from_grpc_status, error_from_status_code, }; use crate::provider::{ProviderAdapter, StreamEventStream, validate_tool_choice}; use crate::providers::common::{ @@ -59,20 +58,20 @@ impl Adapter { #[derive(serde::Serialize)] #[serde(rename_all = "camelCase")] struct ApiRequest { - contents: Vec, + contents: Vec, #[serde(skip_serializing_if = "Option::is_none")] system_instruction: Option, #[serde(skip_serializing_if = "Option::is_none")] - generation_config: Option, + generation_config: Option, #[serde(skip_serializing_if = "Option::is_none")] - tools: Option>, + tools: Option>, #[serde(skip_serializing_if = "Option::is_none")] - tool_config: Option, + tool_config: Option, } #[derive(serde::Serialize)] struct Content { - role: String, + role: String, parts: Vec, } @@ -85,17 +84,17 @@ struct SystemInstruction { #[serde(rename_all = "camelCase")] struct GenerationOptions { #[serde(skip_serializing_if = "Option::is_none")] - temperature: Option, + temperature: Option, #[serde(skip_serializing_if = "Option::is_none")] - max_output_tokens: Option, + max_output_tokens: Option, #[serde(skip_serializing_if = "Option::is_none")] - top_p: Option, + top_p: Option, #[serde(skip_serializing_if = "Option::is_none")] - stop_sequences: Option>, + stop_sequences: Option>, #[serde(skip_serializing_if = "Option::is_none")] response_mime_type: Option, #[serde(skip_serializing_if = "Option::is_none")] - response_schema: Option, + response_schema: Option, } /// Gemini groups function declarations under a `tools` array. @@ -107,9 +106,9 @@ struct GeminiToolGroup { #[derive(serde::Serialize)] struct GeminiFunctionDecl { - name: String, + name: String, description: String, - parameters: serde_json::Value, + parameters: serde_json::Value, } // --- Response types --- @@ -117,14 +116,14 @@ struct GeminiFunctionDecl { #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct ApiResponse { - candidates: Option>, + candidates: Option>, usage_metadata: Option, } #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct Candidate { - content: Option, + content: Option, finish_reason: Option, } @@ -137,9 +136,9 @@ struct CandidateContent { #[serde(rename_all = "camelCase")] #[allow(clippy::struct_field_names)] struct UsageMetadata { - prompt_token_count: Option, - candidates_token_count: Option, - thoughts_token_count: Option, + prompt_token_count: Option, + candidates_token_count: Option, + thoughts_token_count: Option, cached_content_token_count: Option, } @@ -164,9 +163,9 @@ fn parse_part(part: &serde_json::Value) -> Option { .unwrap_or(false); if is_thought { return Some(ContentPart::Thinking(ThinkingData { - text: text.to_string(), + text: text.to_string(), signature: None, - redacted: false, + redacted: false, })); } return Some(ContentPart::text(text)); @@ -360,9 +359,9 @@ fn translate_tools(tools: &[ToolDefinition]) -> Vec { function_declarations: tools .iter() .map(|t| GeminiFunctionDecl { - name: t.name.clone(), + name: t.name.clone(), description: t.description.clone(), - parameters: t.parameters.clone(), + parameters: t.parameters.clone(), }) .collect(), }] @@ -514,12 +513,12 @@ fn parse_usage(metadata: Option<&UsageMetadata>) -> TokenCounts { /// available. async fn send_gemini_response( request: reqwest::RequestBuilder, -) -> Result<(String, HeaderMap), SdkError> { +) -> Result<(String, HeaderMap), Error> { let http_resp = request.send().await.map_err(|e| { if e.is_timeout() { - SdkError::request_timeout(format!("gemini: {e}"), e) + Error::request_timeout(format!("gemini: {e}"), e) } else { - SdkError::network(e.to_string(), e) + Error::network(e.to_string(), e) } })?; @@ -529,7 +528,7 @@ async fn send_gemini_response( let body = http_resp .text() .await - .map_err(|e| SdkError::network(e.to_string(), e))?; + .map_err(|e| Error::network(e.to_string(), e))?; if !status.is_success() { let (msg, code, raw) = parse_error_body(&body, "status"); @@ -547,7 +546,7 @@ fn gemini_error( grpc_status: Option, raw: Option, retry_after: Option, -) -> SdkError { +) -> Error { match grpc_status { Some(grpc_code) => error_from_grpc_status( &grpc_code, @@ -571,14 +570,14 @@ fn gemini_error( /// Send an HTTP request for streaming and return the `reqwest::Response`. /// /// Checks for HTTP errors before returning. On error, reads the body and -/// maps it to `SdkError` using gRPC status code mapping when available. +/// maps it to `Error` using gRPC status code mapping when available. async fn send_streaming_request( request: reqwest::RequestBuilder, -) -> Result { +) -> Result { let http_resp = request .send() .await - .map_err(|e| SdkError::network(e.to_string(), e))?; + .map_err(|e| Error::network(e.to_string(), e))?; let status = http_resp.status(); if !status.is_success() { @@ -586,7 +585,7 @@ async fn send_streaming_request( let body = http_resp .text() .await - .map_err(|e| SdkError::network(e.to_string(), e))?; + .map_err(|e| Error::network(e.to_string(), e))?; let (msg, code, raw) = parse_error_body(&body, "status"); return Err(gemini_error(status.as_u16(), msg, code, raw, retry_after)); } @@ -644,7 +643,7 @@ fn process_sse_stream( Ok(v) => v, Err(e) => { return Some(( - Err(SdkError::stream_error( + Err(Error::stream_error( format!("failed to parse Gemini SSE chunk: {e}"), e, )), @@ -683,32 +682,32 @@ fn process_sse_stream( /// Internal state for the SSE stream processor. struct SseStreamState { - line_reader: super::common::LineReader, - model: String, + line_reader: super::common::LineReader, + model: String, /// Events extracted from a chunk but not yet yielded. - pending_events: std::collections::VecDeque, + pending_events: std::collections::VecDeque, /// Whether we have emitted a `StreamStart` event. - stream_started: bool, + stream_started: bool, /// Whether we have emitted a `TextStart` event. - text_started: bool, + text_started: bool, /// Whether we are currently inside a reasoning (thought) segment. - reasoning_started: bool, + reasoning_started: bool, /// Accumulated thinking text across all chunks. - accumulated_thinking: String, + accumulated_thinking: String, /// Accumulated text across all chunks. - accumulated_text: String, + accumulated_text: String, /// Accumulated tool calls across all chunks. accumulated_tool_calls: Vec, /// The `text_id` used for `TextStart`/`TextDelta`/`TextEnd`. - text_id: String, + text_id: String, /// Latest usage metadata (updated per chunk; final chunk has totals). - usage: TokenCounts, + usage: TokenCounts, /// The finish reason string from the candidate, if received. - finish_reason_str: Option, + finish_reason_str: Option, /// Whether we have emitted the `Finish` event. - finished: bool, + finished: bool, /// Rate limit info parsed from HTTP response headers. - rate_limit: Option, + rate_limit: Option, } impl SseStreamState { @@ -739,7 +738,7 @@ impl SseStreamState { /// Read the next complete line from the HTTP byte stream. /// /// Returns `Ok(None)` when the stream is exhausted. - async fn read_line(&mut self) -> Result, SdkError> { + async fn read_line(&mut self) -> Result, Error> { self.line_reader .read_next_chunk("\n") .await @@ -853,9 +852,9 @@ impl SseStreamState { let mut content_parts: Vec = Vec::new(); if !self.accumulated_thinking.is_empty() { content_parts.push(ContentPart::Thinking(ThinkingData { - text: self.accumulated_thinking.clone(), + text: self.accumulated_thinking.clone(), signature: None, - redacted: false, + redacted: false, })); } if !self.accumulated_text.is_empty() { @@ -866,20 +865,20 @@ impl SseStreamState { } let response = Response { - id: uuid::Uuid::new_v4().to_string(), - model: self.model.clone(), - provider: "gemini".to_string(), - message: Message { - role: Role::Assistant, - content: content_parts, - name: None, + id: uuid::Uuid::new_v4().to_string(), + model: self.model.clone(), + provider: "gemini".to_string(), + message: Message { + role: Role::Assistant, + content: content_parts, + name: None, tool_call_id: None, }, finish_reason: finish_reason.clone(), - usage: self.usage.clone(), - raw: None, - warnings: vec![], - rate_limit: self.rate_limit.clone(), + usage: self.usage.clone(), + raw: None, + warnings: vec![], + rate_limit: self.rate_limit.clone(), }; StreamEvent::finish(finish_reason, self.usage.clone(), response) @@ -892,7 +891,7 @@ impl ProviderAdapter for Adapter { "gemini" } - async fn complete(&self, request: &Request) -> Result { + async fn complete(&self, request: &Request) -> Result { if let Some(tc) = &request.tool_choice { validate_tool_choice(self, tc)?; } @@ -914,14 +913,14 @@ impl ProviderAdapter for Adapter { let (body, headers) = send_gemini_response(gemini_req).await?; let api_resp: ApiResponse = serde_json::from_str(&body) - .map_err(|e| SdkError::network(format!("failed to parse Gemini response: {e}"), e))?; + .map_err(|e| Error::network(format!("failed to parse Gemini response: {e}"), e))?; let candidate = api_resp .candidates .as_ref() .and_then(|c| c.first()) - .ok_or_else(|| SdkError::Provider { - kind: ProviderErrorKind::Server, + .ok_or_else(|| Error::Provider { + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail::new( "no candidates in Gemini response", "gemini", @@ -945,9 +944,9 @@ impl ProviderAdapter for Adapter { model: request.model.clone(), provider: "gemini".to_string(), message: Message { - role: Role::Assistant, - content: content_parts, - name: None, + role: Role::Assistant, + content: content_parts, + name: None, tool_call_id: None, }, finish_reason, @@ -958,7 +957,7 @@ impl ProviderAdapter for Adapter { }) } - async fn stream(&self, request: &Request) -> Result { + async fn stream(&self, request: &Request) -> Result { if let Some(tc) = &request.tool_choice { validate_tool_choice(self, tc)?; } @@ -992,19 +991,19 @@ mod tests { fn minimal_request() -> Request { Request { - model: "gemini-2.0-flash".to_string(), - messages: vec![Message::user("Hello")], - provider: None, - tools: None, - tool_choice: None, - response_format: None, - temperature: None, - top_p: None, - max_tokens: None, - stop_sequences: None, + model: "gemini-2.0-flash".to_string(), + messages: vec![Message::user("Hello")], + provider: None, + tools: None, + tool_choice: None, + response_format: None, + temperature: None, + top_p: None, + max_tokens: None, + stop_sequences: None, reasoning_effort: None, - speed: None, - metadata: None, + speed: None, + metadata: None, provider_options: None, } } @@ -1137,13 +1136,13 @@ mod tests { #[test] fn audio_url_translates_to_file_data() { let msg = Message { - role: Role::User, - content: vec![ContentPart::Audio(AudioData { - url: Some("https://example.com/audio.wav".to_string()), - data: None, + role: Role::User, + content: vec![ContentPart::Audio(AudioData { + url: Some("https://example.com/audio.wav".to_string()), + data: None, media_type: Some("audio/wav".to_string()), })], - name: None, + name: None, tool_call_id: None, }; let contents = translate_messages(&[&msg]); @@ -1156,13 +1155,13 @@ mod tests { #[test] fn audio_base64_translates_to_inline_data() { let msg = Message { - role: Role::User, - content: vec![ContentPart::Audio(AudioData { - url: None, - data: Some(vec![0xFF, 0xFB, 0x90]), + role: Role::User, + content: vec![ContentPart::Audio(AudioData { + url: None, + data: Some(vec![0xFF, 0xFB, 0x90]), media_type: None, })], - name: None, + name: None, tool_call_id: None, }; let contents = translate_messages(&[&msg]); @@ -1174,14 +1173,14 @@ mod tests { #[test] fn document_url_translates_to_file_data() { let msg = Message { - role: Role::User, - content: vec![ContentPart::Document(DocumentData { - url: Some("https://example.com/doc.pdf".to_string()), - data: None, + role: Role::User, + content: vec![ContentPart::Document(DocumentData { + url: Some("https://example.com/doc.pdf".to_string()), + data: None, media_type: Some("application/pdf".to_string()), - file_name: Some("doc.pdf".to_string()), + file_name: Some("doc.pdf".to_string()), })], - name: None, + name: None, tool_call_id: None, }; let contents = translate_messages(&[&msg]); @@ -1193,14 +1192,14 @@ mod tests { #[test] fn document_base64_translates_to_inline_data() { let msg = Message { - role: Role::User, - content: vec![ContentPart::Document(DocumentData { - url: None, - data: Some(vec![0x25, 0x50, 0x44, 0x46]), + role: Role::User, + content: vec![ContentPart::Document(DocumentData { + url: None, + data: Some(vec![0x25, 0x50, 0x44, 0x46]), media_type: None, - file_name: None, + file_name: None, })], - name: None, + name: None, tool_call_id: None, }; let contents = translate_messages(&[&msg]); @@ -1220,10 +1219,13 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::NotFound, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::NotFound, + .. + } + )); let err = gemini_error( 400, @@ -1232,10 +1234,13 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::InvalidRequest, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::InvalidRequest, + .. + } + )); let err = gemini_error( 429, @@ -1244,10 +1249,13 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::RateLimit, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::RateLimit, + .. + } + )); let err = gemini_error( 401, @@ -1256,10 +1264,13 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::Authentication, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::Authentication, + .. + } + )); let err = gemini_error( 403, @@ -1268,10 +1279,13 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::AccessDenied, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::AccessDenied, + .. + } + )); let err = gemini_error( 504, @@ -1280,7 +1294,7 @@ mod tests { None, None, ); - assert!(matches!(err, SdkError::RequestTimeout { .. })); + assert!(matches!(err, Error::RequestTimeout { .. })); } #[test] @@ -1288,16 +1302,22 @@ mod tests { use crate::error::ProviderErrorKind; let err = gemini_error(429, "rate limited".into(), None, None, None); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::RateLimit, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::RateLimit, + .. + } + )); let err = gemini_error(500, "internal".into(), None, None, None); - assert!(matches!(err, SdkError::Provider { - kind: ProviderErrorKind::Server, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::Server, + .. + } + )); } #[test] @@ -1375,9 +1395,9 @@ mod tests { tc.provider_metadata = Some(serde_json::json!({"thoughtSignature": "sig456"})); let msg = Message { - role: Role::Assistant, - content: vec![ContentPart::ToolCall(tc)], - name: None, + role: Role::Assistant, + content: vec![ContentPart::ToolCall(tc)], + name: None, tool_call_id: None, }; let contents = translate_messages(&[&msg]); @@ -1397,9 +1417,9 @@ mod tests { ); let msg = Message { - role: Role::Assistant, - content: vec![ContentPart::ToolCall(tc)], - name: None, + role: Role::Assistant, + content: vec![ContentPart::ToolCall(tc)], + name: None, tool_call_id: None, }; let contents = translate_messages(&[&msg]); diff --git a/lib/crates/fabro-llm/src/providers/http_api.rs b/lib/crates/fabro-llm/src/providers/http_api.rs index 539c554e8..30cfe0d81 100644 --- a/lib/crates/fabro-llm/src/providers/http_api.rs +++ b/lib/crates/fabro-llm/src/providers/http_api.rs @@ -9,11 +9,11 @@ use crate::types::AdapterTimeout; /// configuration that every provider needs. Provider-specific fields live on /// the adapter struct itself. pub struct HttpApi { - pub(crate) api_key: String, - pub(crate) base_url: String, - pub(crate) default_headers: HashMap, - pub(crate) client: reqwest::Client, - pub(crate) request_timeout: Option, + pub(crate) api_key: String, + pub(crate) base_url: String, + pub(crate) default_headers: HashMap, + pub(crate) client: reqwest::Client, + pub(crate) request_timeout: Option, pub(crate) stream_read_timeout: Option, } diff --git a/lib/crates/fabro-llm/src/providers/openai.rs b/lib/crates/fabro-llm/src/providers/openai.rs index 88220a2c3..49a34788f 100644 --- a/lib/crates/fabro-llm/src/providers/openai.rs +++ b/lib/crates/fabro-llm/src/providers/openai.rs @@ -2,7 +2,7 @@ use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use futures::{StreamExt, stream}; -use crate::error::{SdkError, error_from_status_code}; +use crate::error::{Error, error_from_status_code}; use crate::provider::{ProviderAdapter, StreamEventStream, validate_tool_choice}; use crate::providers::common::{ self as common, parse_error_body, parse_rate_limit_headers, parse_retry_after, @@ -23,18 +23,18 @@ const DEFAULT_BASE_URL: &str = "https://api.openai.com/v1"; /// server-side state. pub struct Adapter { pub(crate) http: super::http_api::HttpApi, - org_id: Option, - project_id: Option, + org_id: Option, + project_id: Option, /// When true, always use streaming (required by the Codex endpoint). - codex_mode: bool, + codex_mode: bool, } impl Adapter { #[must_use] pub fn new(api_key: impl Into) -> Self { Self { - http: super::http_api::HttpApi::new(api_key, DEFAULT_BASE_URL), - org_id: None, + http: super::http_api::HttpApi::new(api_key, DEFAULT_BASE_URL), + org_id: None, project_id: None, codex_mode: false, } @@ -100,7 +100,7 @@ impl Adapter { /// Complete a request by streaming and collecting the final response. /// Used for the Codex endpoint which requires `stream: true`. - async fn complete_via_stream(&self, request: &Request) -> Result { + async fn complete_via_stream(&self, request: &Request) -> Result { use futures::StreamExt; let mut event_stream = self.stream(request).await?; let mut last_response: Option = None; @@ -110,9 +110,9 @@ impl Adapter { break; } } - last_response.ok_or_else(|| SdkError::Network { + last_response.ok_or_else(|| Error::Network { message: "Stream ended without a finish event".into(), - source: None, + source: None, }) } } @@ -121,52 +121,52 @@ impl Adapter { #[derive(serde::Serialize)] struct ApiRequest { - model: String, - input: Vec, + model: String, + input: Vec, #[serde(skip_serializing_if = "Option::is_none")] - instructions: Option, + instructions: Option, #[serde(skip_serializing_if = "Option::is_none")] - temperature: Option, + temperature: Option, #[serde(skip_serializing_if = "Option::is_none")] max_output_tokens: Option, #[serde(skip_serializing_if = "Option::is_none")] - top_p: Option, + top_p: Option, #[serde(skip_serializing_if = "Option::is_none")] - tools: Option>, + tools: Option>, #[serde(skip_serializing_if = "Option::is_none")] - tool_choice: Option, + tool_choice: Option, #[serde(skip_serializing_if = "Option::is_none")] - reasoning: Option, + reasoning: Option, #[serde(skip_serializing_if = "Option::is_none")] - text: Option, + text: Option, #[serde(skip_serializing_if = "Option::is_none")] - stop: Option>, + stop: Option>, #[serde(skip_serializing_if = "Option::is_none")] - metadata: Option>, - store: bool, + metadata: Option>, + store: bool, #[serde(skip_serializing_if = "Vec::is_empty")] - include: Vec, + include: Vec, #[serde(skip_serializing_if = "std::ops::Not::not")] - stream: bool, + stream: bool, } // --- Response types (Responses API format) --- #[derive(serde::Deserialize)] struct ApiResponse { - id: String, - model: Option, + id: String, + model: Option, output: Vec, status: Option, - usage: Option, + usage: Option, } #[derive(serde::Deserialize)] struct ApiUsage { - input_tokens: i64, - output_tokens: i64, + input_tokens: i64, + output_tokens: i64, output_tokens_details: Option, - input_tokens_details: Option, + input_tokens_details: Option, } #[derive(serde::Deserialize)] @@ -537,23 +537,23 @@ fn parse_output(output: &[serde_json::Value]) -> (Vec, bool) { /// Mutable state carried through SSE stream processing. struct SseStreamState { - line_reader: super::common::LineReader, - model: String, - response_id: String, - response_model: String, - accumulated_text: String, - tool_calls: Vec, + line_reader: super::common::LineReader, + model: String, + response_id: String, + response_model: String, + accumulated_text: String, + tool_calls: Vec, /// Raw reasoning output items to preserve for round-tripping. - reasoning_items: Vec, + reasoning_items: Vec, /// Raw message output items to preserve for round-tripping. - message_items: Vec, - usage: TokenCounts, - finish_reason: FinishReason, - emitted_start: bool, - emitted_text_start: bool, + message_items: Vec, + usage: TokenCounts, + finish_reason: FinishReason, + emitted_start: bool, + emitted_text_start: bool, emitted_reasoning_start: bool, - raw_response: Option, - rate_limit: Option, + raw_response: Option, + rate_limit: Option, } /// Parse a single SSE message block into an (`event_type`, `data`) pair. @@ -590,7 +590,7 @@ fn parse_sse_message(message_block: &str) -> Option<(Option, String)> { } /// Process the next chunk(s) from the byte stream and return `StreamEvent`s. -async fn process_next_sse_events(state: &mut SseStreamState) -> Result, SdkError> { +async fn process_next_sse_events(state: &mut SseStreamState) -> Result, Error> { loop { match state.line_reader.read_next_chunk("\n\n").await? { Some(message_block) => { @@ -918,9 +918,9 @@ fn handle_response_completed( model, provider: "openai".to_string(), message: Message { - role: Role::Assistant, - content: content_parts, - name: None, + role: Role::Assistant, + content: content_parts, + name: None, tool_call_id: None, }, finish_reason: state.finish_reason.clone(), @@ -943,7 +943,7 @@ impl ProviderAdapter for Adapter { "openai" } - async fn complete(&self, request: &Request) -> Result { + async fn complete(&self, request: &Request) -> Result { // Codex endpoint requires streaming; collect the stream into a response. if self.codex_mode { return self.complete_via_stream(request).await; @@ -962,7 +962,7 @@ impl ProviderAdapter for Adapter { let (body, headers) = send_and_read_response(req, "openai", "type").await?; let api_resp: ApiResponse = serde_json::from_str(&body) - .map_err(|e| SdkError::network(format!("failed to parse OpenAI response: {e}"), e))?; + .map_err(|e| Error::network(format!("failed to parse OpenAI response: {e}"), e))?; let (content_parts, has_tool_calls) = parse_output(&api_resp.output); let finish_reason = map_finish_reason(api_resp.status.as_deref(), has_tool_calls); @@ -994,9 +994,9 @@ impl ProviderAdapter for Adapter { model: api_resp.model.unwrap_or_else(|| request.model.clone()), provider: "openai".to_string(), message: Message { - role: Role::Assistant, - content: content_parts, - name: None, + role: Role::Assistant, + content: content_parts, + name: None, tool_call_id: None, }, finish_reason, @@ -1007,7 +1007,7 @@ impl ProviderAdapter for Adapter { }) } - async fn stream(&self, request: &Request) -> Result { + async fn stream(&self, request: &Request) -> Result { if let Some(tc) = &request.tool_choice { validate_tool_choice(self, tc)?; } @@ -1019,7 +1019,7 @@ impl ProviderAdapter for Adapter { .json(&request_body) .send() .await - .map_err(|e| SdkError::network(e.to_string(), e))?; + .map_err(|e| Error::network(e.to_string(), e))?; let status = http_resp.status(); if !status.is_success() { @@ -1027,7 +1027,7 @@ impl ProviderAdapter for Adapter { let body = http_resp .text() .await - .map_err(|e| SdkError::network(e.to_string(), e))?; + .map_err(|e| Error::network(e.to_string(), e))?; let (msg, code, raw) = parse_error_body(&body, "type"); return Err(error_from_status_code( status.as_u16(), @@ -1063,7 +1063,7 @@ impl ProviderAdapter for Adapter { let stream = stream::unfold(state, |mut state| async move { let events = process_next_sse_events(&mut state).await; - let items: Vec> = match events { + 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)], @@ -1086,19 +1086,19 @@ mod tests { fn minimal_request() -> Request { Request { - model: "gpt-4o".to_string(), - messages: vec![Message::user("Hello")], - provider: None, - tools: None, - tool_choice: None, - response_format: None, - temperature: None, - top_p: None, - max_tokens: None, - stop_sequences: None, + model: "gpt-4o".to_string(), + messages: vec![Message::user("Hello")], + provider: None, + tools: None, + tool_choice: None, + response_format: None, + temperature: None, + top_p: None, + max_tokens: None, + stop_sequences: None, reasoning_effort: None, - speed: None, - metadata: None, + speed: None, + metadata: None, provider_options: None, } } @@ -1240,13 +1240,13 @@ mod tests { #[test] fn audio_content_produces_text_fallback() { let msg = Message { - role: Role::User, - content: vec![ContentPart::Audio(AudioData { - url: Some("https://example.com/audio.wav".to_string()), - data: None, + role: Role::User, + content: vec![ContentPart::Audio(AudioData { + url: Some("https://example.com/audio.wav".to_string()), + data: None, media_type: None, })], - name: None, + name: None, tool_call_id: None, }; let (_, input) = translate_input(&[msg]); @@ -1263,14 +1263,14 @@ mod tests { #[test] fn document_content_produces_text_fallback_with_filename() { let msg = Message { - role: Role::User, - content: vec![ContentPart::Document(DocumentData { - url: Some("https://example.com/doc.pdf".to_string()), - data: None, + role: Role::User, + content: vec![ContentPart::Document(DocumentData { + url: Some("https://example.com/doc.pdf".to_string()), + data: None, media_type: None, - file_name: Some("report.pdf".to_string()), + file_name: Some("report.pdf".to_string()), })], - name: None, + name: None, tool_call_id: None, }; let (_, input) = translate_input(&[msg]); @@ -1287,14 +1287,14 @@ mod tests { #[test] fn document_content_produces_text_fallback_without_filename() { let msg = Message { - role: Role::User, - content: vec![ContentPart::Document(DocumentData { - url: None, - data: Some(vec![1, 2, 3]), + role: Role::User, + content: vec![ContentPart::Document(DocumentData { + url: None, + data: Some(vec![1, 2, 3]), media_type: None, - file_name: None, + file_name: None, })], - name: None, + name: None, tool_call_id: None, }; let (_, input) = translate_input(&[msg]); @@ -1345,9 +1345,9 @@ mod tests { tc.provider_metadata = Some(serde_json::json!({"id": "fc_abc123"})); let msg = Message { - role: Role::Assistant, - content: vec![ContentPart::ToolCall(tc)], - name: None, + role: Role::Assistant, + content: vec![ContentPart::ToolCall(tc)], + name: None, tool_call_id: None, }; let (_, input) = translate_input(&[msg]); @@ -1364,9 +1364,9 @@ mod tests { let tc = ToolCall::new("call_xyz789", "get_weather", serde_json::json!({})); let msg = Message { - role: Role::Assistant, - content: vec![ContentPart::ToolCall(tc)], - name: None, + role: Role::Assistant, + content: vec![ContentPart::ToolCall(tc)], + name: None, tool_call_id: None, }; let (_, input) = translate_input(&[msg]); @@ -1456,15 +1456,15 @@ mod tests { tc.provider_metadata = Some(serde_json::json!({"id": "fc_def456"})); let msg = Message { - role: Role::Assistant, - content: vec![ + role: Role::Assistant, + content: vec![ ContentPart::Other { kind: ContentPart::OPENAI_REASONING.to_string(), data: reasoning, }, ContentPart::ToolCall(tc), ], - name: None, + name: None, tool_call_id: None, }; let (_, input) = translate_input(&[msg]); @@ -1500,8 +1500,8 @@ mod tests { tc.provider_metadata = Some(serde_json::json!({"id": "fc_def456"})); let msg = Message { - role: Role::Assistant, - content: vec![ + role: Role::Assistant, + content: vec![ ContentPart::Other { kind: ContentPart::OPENAI_REASONING.to_string(), data: reasoning, @@ -1513,7 +1513,7 @@ mod tests { ContentPart::text("Checking now."), ContentPart::ToolCall(tc), ], - name: None, + name: None, tool_call_id: None, }; let (_, input) = translate_input(&[msg]); @@ -1535,9 +1535,9 @@ mod tests { // For non-OpenAI turns or turns without preserved message items, // Text parts should still produce a constructed message. let msg = Message { - role: Role::Assistant, - content: vec![ContentPart::text("Hello")], - name: None, + role: Role::Assistant, + content: vec![ContentPart::text("Hello")], + name: None, tool_call_id: None, }; let (_, input) = translate_input(&[msg]); @@ -1562,9 +1562,9 @@ mod tests { // Now translate back to input format let msg = Message { - role: Role::Assistant, - content: parts, - name: None, + role: Role::Assistant, + content: parts, + name: None, tool_call_id: None, }; let (_, input) = translate_input(&[msg]); @@ -1599,21 +1599,21 @@ mod tests { let http_resp = http::Response::builder().status(200).body("").unwrap(); let response = reqwest::Response::from(http_resp); SseStreamState { - line_reader: LineReader::new(response, None), - model: String::new(), - response_id: String::new(), - response_model: String::new(), - accumulated_text: String::new(), - tool_calls: Vec::new(), - reasoning_items: Vec::new(), - message_items: Vec::new(), - usage: TokenCounts::default(), - finish_reason: FinishReason::Stop, - emitted_start: true, - emitted_text_start: false, + line_reader: LineReader::new(response, None), + model: String::new(), + response_id: String::new(), + response_model: String::new(), + accumulated_text: String::new(), + tool_calls: Vec::new(), + reasoning_items: Vec::new(), + message_items: Vec::new(), + usage: TokenCounts::default(), + finish_reason: FinishReason::Stop, + emitted_start: true, + emitted_text_start: false, emitted_reasoning_start: false, - raw_response: None, - rate_limit: None, + raw_response: None, + rate_limit: None, } } diff --git a/lib/crates/fabro-llm/src/providers/openai_compatible.rs b/lib/crates/fabro-llm/src/providers/openai_compatible.rs index 795ba1825..392b18dbe 100644 --- a/lib/crates/fabro-llm/src/providers/openai_compatible.rs +++ b/lib/crates/fabro-llm/src/providers/openai_compatible.rs @@ -1,6 +1,6 @@ use futures::{StreamExt, stream}; -use crate::error::{ProviderErrorDetail, ProviderErrorKind, SdkError, error_from_status_code}; +use crate::error::{Error, ProviderErrorDetail, ProviderErrorKind, error_from_status_code}; use crate::provider::{ProviderAdapter, StreamEventStream, validate_tool_choice}; use crate::providers::common::{ parse_error_body, parse_rate_limit_headers, parse_retry_after, send_and_read_response, @@ -20,14 +20,14 @@ use crate::types::{ /// features. Use the primary `OpenAiAdapter` for `OpenAI`'s own API. pub struct Adapter { pub(crate) http: super::http_api::HttpApi, - provider_name: String, + provider_name: String, } impl Adapter { #[must_use] pub fn new(api_key: impl Into, base_url: impl Into) -> Self { Self { - http: super::http_api::HttpApi::new(api_key, base_url), + http: super::http_api::HttpApi::new(api_key, base_url), provider_name: "openai-compatible".to_string(), } } @@ -69,52 +69,52 @@ impl Adapter { #[derive(serde::Serialize)] struct ApiRequest { - model: String, - messages: Vec, + model: String, + messages: Vec, #[serde(skip_serializing_if = "Option::is_none")] - temperature: Option, + temperature: Option, #[serde(skip_serializing_if = "Option::is_none")] - max_tokens: Option, + max_tokens: Option, #[serde(skip_serializing_if = "Option::is_none")] - top_p: Option, + top_p: Option, #[serde(skip_serializing_if = "Option::is_none")] - stop: Option>, + stop: Option>, #[serde(skip_serializing_if = "Option::is_none")] - tools: Option>, + tools: Option>, #[serde(skip_serializing_if = "Option::is_none")] - tool_choice: Option, + tool_choice: Option, #[serde(skip_serializing_if = "Option::is_none")] response_format: Option, #[serde(skip_serializing_if = "Option::is_none")] - stream: Option, + stream: Option, } #[derive(serde::Serialize)] struct ChatMessage { - role: String, + role: String, #[serde(skip_serializing_if = "Option::is_none")] - content: Option, + content: Option, /// Reasoning/thinking content echoed back for providers that require it /// (Kimi). #[serde(skip_serializing_if = "Option::is_none")] reasoning_content: Option, #[serde(skip_serializing_if = "Option::is_none")] - tool_call_id: Option, + tool_call_id: Option, #[serde(skip_serializing_if = "Option::is_none")] - tool_calls: Option>, + tool_calls: Option>, } #[derive(serde::Serialize)] struct ChatToolCall { - id: String, + id: String, #[serde(rename = "type")] - kind: String, + kind: String, function: ChatFunction, } #[derive(serde::Serialize)] struct ChatFunction { - name: String, + name: String, arguments: String, } @@ -122,41 +122,41 @@ struct ChatFunction { #[derive(serde::Deserialize)] struct ApiResponse { - id: String, - model: String, + id: String, + model: String, choices: Vec, - usage: Option, + usage: Option, } #[derive(serde::Deserialize)] struct ApiChoice { - message: ApiChoiceMessage, + message: ApiChoiceMessage, finish_reason: Option, } #[derive(serde::Deserialize)] struct ApiChoiceMessage { - content: Option, + content: Option, reasoning_content: Option, - tool_calls: Option>, + tool_calls: Option>, } #[derive(serde::Deserialize)] struct ApiToolCall { - id: String, + id: String, function: ApiFunction, } #[derive(serde::Deserialize)] struct ApiFunction { - name: String, + name: String, arguments: String, } #[derive(serde::Deserialize)] #[allow(clippy::struct_field_names)] struct ApiUsage { - prompt_tokens: i64, + prompt_tokens: i64, completion_tokens: i64, } @@ -164,46 +164,46 @@ struct ApiUsage { #[derive(serde::Deserialize)] struct StreamChunk { - id: Option, - model: Option, + id: Option, + model: Option, choices: Option>, - usage: Option, + usage: Option, } #[derive(serde::Deserialize)] struct StreamChoice { - delta: Option, + delta: Option, finish_reason: Option, } #[derive(serde::Deserialize)] struct StreamDelta { - content: Option, + content: Option, /// Reasoning/thinking content (used by Kimi and other reasoning models). reasoning_content: Option, - tool_calls: Option>, + tool_calls: Option>, } #[derive(serde::Deserialize)] struct StreamToolCall { - index: usize, - id: Option, + index: usize, + id: Option, function: Option, } #[derive(serde::Deserialize)] struct StreamFunction { - name: Option, + name: Option, arguments: Option, } // --- Accumulated tool call state for streaming --- struct AccumulatedToolCall { - id: String, - name: String, + id: String, + name: String, arguments: String, - started: bool, + started: bool, } fn map_finish_reason(reason: Option<&str>) -> FinishReason { @@ -259,11 +259,11 @@ fn translate_messages(messages: &[Message]) -> Vec { .as_str() .map_or_else(|| tr.content.to_string(), str::to_string); Some(ChatMessage { - role: "tool".to_string(), - content: Some(output), + role: "tool".to_string(), + content: Some(output), reasoning_content: None, - tool_call_id: Some(tr.tool_call_id.clone()), - tool_calls: None, + tool_call_id: Some(tr.tool_call_id.clone()), + tool_calls: None, }) } else { None @@ -288,8 +288,8 @@ fn translate_messages(messages: &[Message]) -> Vec { .clone() .unwrap_or_else(|| tc.arguments.to_string()); tool_calls.push(ChatToolCall { - id: tc.id.clone(), - kind: "function".to_string(), + id: tc.id.clone(), + kind: "function".to_string(), function: ChatFunction { name: tc.name.clone(), arguments, @@ -453,7 +453,7 @@ impl ProviderAdapter for Adapter { &self.provider_name } - async fn complete(&self, request: &Request) -> Result { + async fn complete(&self, request: &Request) -> Result { if let Some(tc) = &request.tool_choice { validate_tool_choice(self, tc)?; } @@ -467,10 +467,10 @@ impl ProviderAdapter for Adapter { let (body, headers) = send_and_read_response(req, &self.provider_name, "type").await?; let api_resp: ApiResponse = serde_json::from_str(&body) - .map_err(|e| SdkError::network(format!("failed to parse response: {e}"), e))?; + .map_err(|e| Error::network(format!("failed to parse response: {e}"), e))?; - let choice = api_resp.choices.first().ok_or_else(|| SdkError::Provider { - kind: ProviderErrorKind::Server, + let choice = api_resp.choices.first().ok_or_else(|| Error::Provider { + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail::new( "no choices in response", &self.provider_name, @@ -481,9 +481,9 @@ impl ProviderAdapter for Adapter { if let Some(reasoning) = &choice.message.reasoning_content { if !reasoning.is_empty() { content_parts.push(ContentPart::Thinking(ThinkingData { - text: reasoning.clone(), + text: reasoning.clone(), signature: None, - redacted: false, + redacted: false, })); } } @@ -518,9 +518,9 @@ impl ProviderAdapter for Adapter { model: api_resp.model, provider: self.provider_name.clone(), message: Message { - role: Role::Assistant, - content: content_parts, - name: None, + role: Role::Assistant, + content: content_parts, + name: None, tool_call_id: None, }, finish_reason, @@ -531,7 +531,7 @@ impl ProviderAdapter for Adapter { }) } - async fn stream(&self, request: &Request) -> Result { + async fn stream(&self, request: &Request) -> Result { if let Some(tc) = &request.tool_choice { validate_tool_choice(self, tc)?; } @@ -543,7 +543,7 @@ impl ProviderAdapter for Adapter { .json(&api_body) .send() .await - .map_err(|e| SdkError::network(e.to_string(), e))?; + .map_err(|e| Error::network(e.to_string(), e))?; let status = http_resp.status(); if !status.is_success() { @@ -551,7 +551,7 @@ impl ProviderAdapter for Adapter { let body = http_resp .text() .await - .map_err(|e| SdkError::network(e.to_string(), e))?; + .map_err(|e| Error::network(e.to_string(), e))?; let (msg, code, raw) = parse_error_body(&body, "type"); return Err(error_from_status_code( status.as_u16(), @@ -615,7 +615,7 @@ impl ProviderAdapter for Adapter { Ok(c) => c, Err(e) => { return Some(( - Err(SdkError::stream_error( + Err(Error::stream_error( format!("failed to parse SSE chunk: {e}"), e, )), @@ -634,7 +634,7 @@ impl ProviderAdapter for Adapter { // Flatten batched events into individual stream events. let flat_stream = stream::unfold( FlattenState { - inner: Box::pin(stream), + inner: Box::pin(stream), pending: Vec::new(), }, |mut flatten_state| async { @@ -662,29 +662,28 @@ impl ProviderAdapter for Adapter { /// State for flattening batched events into individual stream events. struct FlattenState { - inner: - std::pin::Pin, SdkError>> + Send>>, + inner: std::pin::Pin, Error>> + Send>>, pending: Vec, } /// Accumulated state while processing the SSE stream. struct StreamState { - line_reader: super::common::LineReader, - provider_name: String, - model: String, - response_id: String, - response_model: String, - accumulated_text: String, + line_reader: super::common::LineReader, + provider_name: String, + model: String, + response_id: String, + response_model: String, + accumulated_text: String, accumulated_reasoning: String, - tool_calls: Vec, - usage: TokenCounts, - finish_reason: FinishReason, - text_started: bool, - done: bool, + tool_calls: Vec, + usage: TokenCounts, + finish_reason: FinishReason, + text_started: bool, + done: bool, /// True after `finish_events()` has been called (guards against /// duplicates). - finished: bool, - rate_limit: Option, + finished: bool, + rate_limit: Option, } impl StreamState { @@ -714,7 +713,7 @@ impl StreamState { } /// Read the next complete line from the SSE byte stream. - async fn next_line(&mut self) -> Result, SdkError> { + async fn next_line(&mut self) -> Result, Error> { if self.done { return Ok(None); } @@ -788,10 +787,10 @@ impl StreamState { // Grow the accumulated tool calls vector if needed. while self.tool_calls.len() <= index { self.tool_calls.push(AccumulatedToolCall { - id: String::new(), - name: String::new(), + id: String::new(), + name: String::new(), arguments: String::new(), - started: false, + started: false, }); } @@ -849,9 +848,9 @@ impl StreamState { // Include reasoning/thinking content if present (Kimi, etc.). if !self.accumulated_reasoning.is_empty() { content_parts.push(ContentPart::Thinking(ThinkingData { - text: std::mem::take(&mut self.accumulated_reasoning), + text: std::mem::take(&mut self.accumulated_reasoning), signature: None, - redacted: false, + redacted: false, })); } @@ -883,20 +882,20 @@ impl StreamState { }; let response = Response { - id: self.response_id.clone(), - model: response_model, - provider: self.provider_name.clone(), - message: Message { - role: Role::Assistant, - content: content_parts, - name: None, + id: self.response_id.clone(), + model: response_model, + provider: self.provider_name.clone(), + message: Message { + role: Role::Assistant, + content: content_parts, + name: None, tool_call_id: None, }, finish_reason: self.finish_reason.clone(), - usage: self.usage.clone(), - raw: None, - warnings: vec![], - rate_limit: self.rate_limit.clone(), + usage: self.usage.clone(), + raw: None, + warnings: vec![], + rate_limit: self.rate_limit.clone(), }; events.push(StreamEvent::finish( @@ -1086,10 +1085,10 @@ mod tests { ); state.response_id = "resp-1".into(); state.tool_calls.push(AccumulatedToolCall { - id: "call_1".into(), - name: "get_weather".into(), + id: "call_1".into(), + name: "get_weather".into(), arguments: r#"{"city":"SF"}"#.into(), - started: true, + started: true, }); let events = state.finish_events(); @@ -1142,32 +1141,32 @@ mod tests { #[test] fn api_request_stream_field_serialization() { let req = ApiRequest { - model: "test".into(), - messages: vec![], - temperature: None, - max_tokens: None, - top_p: None, - stop: None, - tools: None, - tool_choice: None, + model: "test".into(), + messages: vec![], + temperature: None, + max_tokens: None, + top_p: None, + stop: None, + tools: None, + tool_choice: None, response_format: None, - stream: Some(true), + stream: Some(true), }; let json = serde_json::to_value(&req).unwrap(); assert_eq!(json["stream"], true); // When stream is None, it should be omitted. let req_no_stream = ApiRequest { - model: "test".into(), - messages: vec![], - temperature: None, - max_tokens: None, - top_p: None, - stop: None, - tools: None, - tool_choice: None, + model: "test".into(), + messages: vec![], + temperature: None, + max_tokens: None, + top_p: None, + stop: None, + tools: None, + tool_choice: None, response_format: None, - stream: None, + stream: None, }; let json_no_stream = serde_json::to_value(&req_no_stream).unwrap(); assert!(json_no_stream.get("stream").is_none()); @@ -1176,13 +1175,13 @@ mod tests { #[test] fn translate_assistant_message_with_tool_calls_only() { let msg = Message { - role: Role::Assistant, - content: vec![ContentPart::ToolCall(ToolCall::new( + role: Role::Assistant, + content: vec![ContentPart::ToolCall(ToolCall::new( "call_1", "get_weather", serde_json::json!({"city": "SF"}), ))], - name: None, + name: None, tool_call_id: None, }; let translated = translate_messages(&[msg]); @@ -1200,8 +1199,8 @@ mod tests { #[test] fn translate_assistant_message_with_text_and_tool_calls() { let msg = Message { - role: Role::Assistant, - content: vec![ + role: Role::Assistant, + content: vec![ ContentPart::text("Let me check the weather"), ContentPart::ToolCall(ToolCall::new( "call_2", @@ -1209,7 +1208,7 @@ mod tests { serde_json::json!({"city": "NYC"}), )), ], - name: None, + name: None, tool_call_id: None, }; let translated = translate_messages(&[msg]); @@ -1227,9 +1226,9 @@ mod tests { let mut tc = ToolCall::new("call_3", "search", serde_json::json!({"q": "rust"})); tc.raw_arguments = Some(r#"{"q": "rust"}"#.to_string()); let msg = Message { - role: Role::Assistant, - content: vec![ContentPart::ToolCall(tc)], - name: None, + role: Role::Assistant, + content: vec![ContentPart::ToolCall(tc)], + name: None, tool_call_id: None, }; let translated = translate_messages(&[msg]); @@ -1263,13 +1262,13 @@ mod tests { #[test] fn assistant_tool_calls_serialize_correctly() { let msg = Message { - role: Role::Assistant, - content: vec![ContentPart::ToolCall(ToolCall::new( + role: Role::Assistant, + content: vec![ContentPart::ToolCall(ToolCall::new( "call_1", "get_weather", serde_json::json!({"city": "SF"}), ))], - name: None, + name: None, tool_call_id: None, }; let translated = translate_messages(&[msg]); @@ -1285,19 +1284,19 @@ mod tests { fn minimal_request() -> Request { Request { - model: "llama-3.1-70b".to_string(), - messages: vec![Message::user("Hello")], - provider: None, - tools: None, - tool_choice: None, - response_format: None, - temperature: None, - top_p: None, - max_tokens: None, - stop_sequences: None, + model: "llama-3.1-70b".to_string(), + messages: vec![Message::user("Hello")], + provider: None, + tools: None, + tool_choice: None, + response_format: None, + temperature: None, + top_p: None, + max_tokens: None, + stop_sequences: None, reasoning_effort: None, - speed: None, - metadata: None, + speed: None, + metadata: None, provider_options: None, } } @@ -1394,13 +1393,13 @@ mod tests { #[test] fn audio_content_produces_text_fallback() { let msg = Message { - role: Role::User, - content: vec![ContentPart::Audio(AudioData { - url: Some("https://example.com/audio.wav".to_string()), - data: None, + role: Role::User, + content: vec![ContentPart::Audio(AudioData { + url: Some("https://example.com/audio.wav".to_string()), + data: None, media_type: None, })], - name: None, + name: None, tool_call_id: None, }; let translated = translate_messages(&[msg]); @@ -1413,14 +1412,14 @@ mod tests { #[test] fn document_content_produces_text_fallback_with_filename() { let msg = Message { - role: Role::User, - content: vec![ContentPart::Document(DocumentData { - url: Some("https://example.com/doc.pdf".to_string()), - data: None, + role: Role::User, + content: vec![ContentPart::Document(DocumentData { + url: Some("https://example.com/doc.pdf".to_string()), + data: None, media_type: None, - file_name: Some("report.pdf".to_string()), + file_name: Some("report.pdf".to_string()), })], - name: None, + name: None, tool_call_id: None, }; let translated = translate_messages(&[msg]); @@ -1433,14 +1432,14 @@ mod tests { #[test] fn document_content_produces_text_fallback_without_filename() { let msg = Message { - role: Role::User, - content: vec![ContentPart::Document(DocumentData { - url: None, - data: Some(vec![1, 2, 3]), + role: Role::User, + content: vec![ContentPart::Document(DocumentData { + url: None, + data: Some(vec![1, 2, 3]), media_type: None, - file_name: None, + file_name: None, })], - name: None, + name: None, tool_call_id: None, }; let translated = translate_messages(&[msg]); @@ -1453,16 +1452,16 @@ mod tests { #[test] fn mixed_text_and_audio_content_concatenates() { let msg = Message { - role: Role::User, - content: vec![ + role: Role::User, + content: vec![ ContentPart::text("Check this: "), ContentPart::Audio(AudioData { - url: None, - data: Some(vec![1, 2]), + url: None, + data: Some(vec![1, 2]), media_type: None, }), ], - name: None, + name: None, tool_call_id: None, }; let translated = translate_messages(&[msg]); diff --git a/lib/crates/fabro-llm/src/retry.rs b/lib/crates/fabro-llm/src/retry.rs index f937ccf5a..09ea45c85 100644 --- a/lib/crates/fabro-llm/src/retry.rs +++ b/lib/crates/fabro-llm/src/retry.rs @@ -4,7 +4,7 @@ use std::time::Duration; use tokio::time; use tracing::warn; -use crate::error::SdkError; +use crate::error::Error; use crate::types::RetryPolicy; /// Retry a fallible async operation according to the given policy (Section @@ -16,12 +16,12 @@ use crate::types::RetryPolicy; /// /// # Errors /// -/// Returns the last `SdkError` if all retries are exhausted or the error is +/// Returns the last `Error` if all retries are exhausted or the error is /// non-retryable. -pub async fn retry(policy: &RetryPolicy, mut operation: F) -> Result +pub async fn retry(policy: &RetryPolicy, mut operation: F) -> Result where F: FnMut() -> Fut, - Fut: Future>, + Fut: Future>, { let mut attempt = 0u32; @@ -79,9 +79,9 @@ mod tests { fn fast_backoff() -> BackoffPolicy { BackoffPolicy { initial_delay: Duration::from_micros(1), - factor: 2.0, - max_delay: Duration::from_secs(60), - jitter: false, + factor: 2.0, + max_delay: Duration::from_secs(60), + jitter: false, } } @@ -103,7 +103,7 @@ mod tests { let cc = cc.clone(); async move { cc.fetch_add(1, Ordering::SeqCst); - Ok::<_, SdkError>(42) + Ok::<_, Error>(42) } }) .await; @@ -128,8 +128,8 @@ mod tests { async move { let count = cc.fetch_add(1, Ordering::SeqCst); if count < 2 { - Err(SdkError::Provider { - kind: ProviderErrorKind::Server, + Err(Error::Provider { + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail { status_code: Some(500), ..ProviderErrorDetail::new("error", "test") @@ -161,8 +161,8 @@ mod tests { let cc = cc.clone(); async move { cc.fetch_add(1, Ordering::SeqCst); - Err::(SdkError::Provider { - kind: ProviderErrorKind::Server, + Err::(Error::Provider { + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail { status_code: Some(500), ..ProviderErrorDetail::new("error", "test") @@ -191,8 +191,8 @@ mod tests { let cc = cc.clone(); async move { cc.fetch_add(1, Ordering::SeqCst); - Err::(SdkError::Provider { - kind: ProviderErrorKind::Authentication, + Err::(Error::Provider { + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail { status_code: Some(401), ..ProviderErrorDetail::new("bad key", "test") @@ -212,9 +212,9 @@ mod tests { max_retries: 3, backoff: BackoffPolicy { initial_delay: Duration::from_micros(1), - factor: 2.0, - max_delay: Duration::from_secs(5), - jitter: false, + factor: 2.0, + max_delay: Duration::from_secs(5), + jitter: false, }, ..Default::default() }; @@ -226,8 +226,8 @@ mod tests { let cc = cc.clone(); async move { cc.fetch_add(1, Ordering::SeqCst); - Err::(SdkError::Provider { - kind: ProviderErrorKind::RateLimit, + Err::(Error::Provider { + kind: ProviderErrorKind::RateLimit, detail: Box::new(ProviderErrorDetail { status_code: Some(429), retry_after: Some(100.0), // Way beyond max_delay @@ -248,9 +248,9 @@ mod tests { max_retries: 1, backoff: BackoffPolicy { initial_delay: Duration::from_secs(10), // high, but retry_after is low - factor: 2.0, - max_delay: Duration::from_secs(60), - jitter: false, + factor: 2.0, + max_delay: Duration::from_secs(60), + jitter: false, }, ..Default::default() }; @@ -264,8 +264,8 @@ mod tests { async move { let count = cc.fetch_add(1, Ordering::SeqCst); if count < 1 { - Err(SdkError::Provider { - kind: ProviderErrorKind::RateLimit, + Err(Error::Provider { + kind: ProviderErrorKind::RateLimit, detail: Box::new(ProviderErrorDetail { status_code: Some(429), retry_after: Some(0.01), @@ -293,8 +293,8 @@ mod tests { let policy = RetryPolicy { max_retries: 2, - backoff: fast_backoff(), - on_retry: Some(Arc::new(move |_err, _attempt, _delay| { + backoff: fast_backoff(), + on_retry: Some(Arc::new(move |_err, _attempt, _delay| { retry_attempts_clone.fetch_add(1, Ordering::SeqCst); })), }; @@ -307,8 +307,8 @@ mod tests { async move { let count = cc.fetch_add(1, Ordering::SeqCst); if count < 2 { - Err(SdkError::Provider { - kind: ProviderErrorKind::Server, + Err(Error::Provider { + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail { status_code: Some(500), ..ProviderErrorDetail::new("error", "test") diff --git a/lib/crates/fabro-llm/src/tools.rs b/lib/crates/fabro-llm/src/tools.rs index fc7b04c5e..e15267afb 100644 --- a/lib/crates/fabro-llm/src/tools.rs +++ b/lib/crates/fabro-llm/src/tools.rs @@ -11,7 +11,7 @@ use crate::types::{Message, ToolCall, ToolDefinition, ToolResult}; #[derive(Clone)] pub struct ToolContext { pub tool_call_id: String, - pub messages: Vec, + pub messages: Vec, pub abort_signal: Option, } @@ -30,7 +30,7 @@ pub type ExecuteHandler = Arc< /// "Passive" tools have no handler and are returned to the caller. pub struct Tool { pub definition: ToolDefinition, - pub execute: Option, + pub execute: Option, } impl Tool { @@ -50,7 +50,7 @@ impl Tool { description: description.to_string(), parameters, }, - execute: None, + execute: None, } } @@ -78,7 +78,7 @@ impl Tool { description: description.to_string(), parameters, }, - execute: Some(Arc::new(move |args, ctx| Box::pin(handler(args, ctx)))), + execute: Some(Arc::new(move |args, ctx| Box::pin(handler(args, ctx)))), } } diff --git a/lib/crates/fabro-llm/src/types.rs b/lib/crates/fabro-llm/src/types.rs index 698ac5033..f6abe5f76 100644 --- a/lib/crates/fabro-llm/src/types.rs +++ b/lib/crates/fabro-llm/src/types.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use fabro_util::backoff::BackoffPolicy; use serde::{Deserialize, Serialize, de}; -use crate::error::SdkError; +use crate::error::Error; // --- 3.2 Role --- @@ -22,32 +22,32 @@ pub enum Role { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ImageData { - pub url: Option, - pub data: Option>, + pub url: Option, + pub data: Option>, pub media_type: Option, - pub detail: Option, + pub detail: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct AudioData { - pub url: Option, - pub data: Option>, + pub url: Option, + pub data: Option>, pub media_type: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct DocumentData { - pub url: Option, - pub data: Option>, + pub url: Option, + pub data: Option>, pub media_type: Option, - pub file_name: Option, + pub file_name: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ThinkingData { - pub text: String, + pub text: String, pub signature: Option, - pub redacted: bool, + pub redacted: bool, } // --- 5.4 ToolCall / ToolResult --- @@ -58,12 +58,12 @@ fn default_tool_type() -> String { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ToolCall { - pub id: String, - pub name: String, + pub id: String, + pub name: String, #[serde(rename = "type", default = "default_tool_type")] - pub tool_type: String, - pub arguments: serde_json::Value, - pub raw_arguments: Option, + pub tool_type: String, + pub arguments: serde_json::Value, + pub raw_arguments: Option, /// Opaque provider-specific metadata (e.g. Gemini `thought_signature`). /// Preserved across round-trips so the provider can include it when /// sending conversation history back to the API. @@ -90,11 +90,11 @@ impl ToolCall { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ToolResult { - pub tool_call_id: String, - pub content: serde_json::Value, - pub is_error: bool, + pub tool_call_id: String, + pub content: serde_json::Value, + pub is_error: bool, #[serde(skip_serializing_if = "Option::is_none")] - pub image_data: Option>, + pub image_data: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub image_media_type: Option, } @@ -112,10 +112,10 @@ impl ToolResult { pub fn error(id: impl Into, message: impl Into) -> Self { Self { - tool_call_id: id.into(), - content: serde_json::Value::String(message.into()), - is_error: true, - image_data: None, + tool_call_id: id.into(), + content: serde_json::Value::String(message.into()), + is_error: true, + image_data: None, image_media_type: None, } } @@ -253,36 +253,36 @@ impl ContentPart { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Message { - pub role: Role, - pub content: Vec, - pub name: Option, + pub role: Role, + pub content: Vec, + pub name: Option, pub tool_call_id: Option, } impl Message { pub fn system(text: impl Into) -> Self { Self { - role: Role::System, - content: vec![ContentPart::text(text)], - name: None, + role: Role::System, + content: vec![ContentPart::text(text)], + name: None, tool_call_id: None, } } pub fn user(text: impl Into) -> Self { Self { - role: Role::User, - content: vec![ContentPart::text(text)], - name: None, + role: Role::User, + content: vec![ContentPart::text(text)], + name: None, tool_call_id: None, } } pub fn assistant(text: impl Into) -> Self { Self { - role: Role::Assistant, - content: vec![ContentPart::text(text)], - name: None, + role: Role::Assistant, + content: vec![ContentPart::text(text)], + name: None, tool_call_id: None, } } @@ -294,15 +294,15 @@ impl Message { ) -> Self { let id = tool_call_id.into(); Self { - role: Role::Tool, - content: vec![ContentPart::ToolResult(ToolResult { + role: Role::Tool, + content: vec![ContentPart::ToolResult(ToolResult { tool_call_id: id.clone(), content, is_error, image_data: None, image_media_type: None, })], - name: None, + name: None, tool_call_id: Some(id), } } @@ -384,10 +384,10 @@ pub enum ResponseFormatType { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ResponseFormat { #[serde(rename = "type")] - pub kind: ResponseFormatType, + pub kind: ResponseFormatType, pub json_schema: Option, #[serde(default)] - pub strict: bool, + pub strict: bool, } // --- 3.11 Warning --- @@ -395,7 +395,7 @@ pub struct ResponseFormat { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Warning { pub message: String, - pub code: Option, + pub code: Option, } // --- 3.12 RateLimitInfo --- @@ -403,10 +403,10 @@ pub struct Warning { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RateLimitInfo { pub requests_remaining: Option, - pub requests_limit: Option, - pub tokens_remaining: Option, - pub tokens_limit: Option, - pub reset_at: Option, + pub requests_limit: Option, + pub tokens_remaining: Option, + pub tokens_limit: Option, + pub reset_at: Option, } // --- 3.8 ReasoningEffort --- @@ -453,19 +453,19 @@ impl std::str::FromStr for ReasoningEffort { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Request { - pub model: String, - pub messages: Vec, - pub provider: Option, - pub tools: Option>, - pub tool_choice: Option, - pub response_format: Option, - pub temperature: Option, - pub top_p: Option, - pub max_tokens: Option, - pub stop_sequences: Option>, + pub model: String, + pub messages: Vec, + pub provider: Option, + pub tools: Option>, + pub tool_choice: Option, + pub response_format: Option, + pub temperature: Option, + pub top_p: Option, + pub max_tokens: Option, + pub stop_sequences: Option>, pub reasoning_effort: Option, - pub speed: Option, - pub metadata: Option>, + pub speed: Option, + pub metadata: Option>, pub provider_options: Option, } @@ -473,9 +473,9 @@ pub struct Request { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolDefinition { - pub name: String, + pub name: String, pub description: String, - pub parameters: serde_json::Value, + pub parameters: serde_json::Value, } // --- 5.3 ToolChoice --- @@ -512,15 +512,15 @@ impl ToolChoice { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Response { - pub id: String, - pub model: String, - pub provider: String, - pub message: Message, + pub id: String, + pub model: String, + pub provider: String, + pub message: Message, pub finish_reason: FinishReason, - pub usage: TokenCounts, - pub raw: Option, - pub warnings: Vec, - pub rate_limit: Option, + pub usage: TokenCounts, + pub raw: Option, + pub warnings: Vec, + pub rate_limit: Option, } impl Response { @@ -571,7 +571,7 @@ pub enum StreamEvent { text_id: Option, }, TextDelta { - delta: String, + delta: String, text_id: Option, }, TextEnd { @@ -593,19 +593,19 @@ pub enum StreamEvent { }, StepFinish { finish_reason: FinishReason, - usage: TokenCounts, - response: Box, - tool_calls: Vec, - tool_results: Vec, + usage: TokenCounts, + response: Box, + tool_calls: Vec, + tool_results: Vec, }, Finish { finish_reason: FinishReason, - usage: TokenCounts, - response: Box, + usage: TokenCounts, + response: Box, }, Error { - error: SdkError, - raw: Option, + error: Error, + raw: Option, }, } @@ -644,7 +644,7 @@ impl StreamEvent { } #[must_use] - pub const fn error(error: SdkError) -> Self { + pub const fn error(error: Error) -> Self { Self::Error { error, raw: None } } } @@ -657,14 +657,14 @@ pub use fabro_model::{Model, ModelCosts, ModelFeatures, ModelLimits}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct TimeoutOptions { - pub total: Option, + pub total: Option, pub per_step: Option, } impl From for TimeoutOptions { fn from(total: f64) -> Self { Self { - total: Some(total), + total: Some(total), per_step: None, } } @@ -672,16 +672,16 @@ impl From for TimeoutOptions { #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct AdapterTimeout { - pub connect: f64, - pub request: Option, + pub connect: f64, + pub request: Option, pub stream_read: Option, } impl Default for AdapterTimeout { fn default() -> Self { Self { - connect: 30.0, - request: None, + connect: 30.0, + request: None, stream_read: Some(300.0), } } @@ -691,14 +691,14 @@ impl Default for AdapterTimeout { /// Callback invoked before each retry attempt with (error, attempt, delay as /// Duration). -pub type OnRetryCallback = Arc; +pub type OnRetryCallback = Arc; #[derive(Clone)] pub struct RetryPolicy { pub max_retries: u32, - pub backoff: BackoffPolicy, + pub backoff: BackoffPolicy, /// Called before each retry with (error, attempt number, delay). - pub on_retry: Option, + pub on_retry: Option, } impl std::fmt::Debug for RetryPolicy { @@ -715,13 +715,13 @@ impl Default for RetryPolicy { fn default() -> Self { Self { max_retries: 2, - backoff: BackoffPolicy { + backoff: BackoffPolicy { initial_delay: std::time::Duration::from_secs(1), - factor: 2.0, - max_delay: std::time::Duration::from_secs(60), - jitter: true, + factor: 2.0, + max_delay: std::time::Duration::from_secs(60), + jitter: true, }, - on_retry: None, + on_retry: None, } } } @@ -737,7 +737,7 @@ pub enum ObjectStreamEvent { Delta { event: StreamEvent }, /// The stream completed with a fully parsed object and response. Complete { - object: serde_json::Value, + object: serde_json::Value, response: Box, }, } @@ -746,11 +746,11 @@ pub enum ObjectStreamEvent { #[derive(Debug, Clone)] pub struct GenerateResult { - pub response: Response, + pub response: Response, pub tool_results: Vec, - pub total_usage: TokenCounts, - pub steps: Vec, - pub output: Option, + pub total_usage: TokenCounts, + pub steps: Vec, + pub output: Option, } impl std::ops::Deref for GenerateResult { @@ -762,7 +762,7 @@ impl std::ops::Deref for GenerateResult { #[derive(Debug, Clone)] pub struct StepResult { - pub response: Response, + pub response: Response, pub tool_results: Vec, } @@ -821,13 +821,13 @@ mod tests { #[test] fn message_text_concatenates_text_parts() { let msg = Message { - role: Role::Assistant, - content: vec![ + role: Role::Assistant, + content: vec![ ContentPart::text("Hello "), ContentPart::ToolCall(ToolCall::new("c1", "test", serde_json::json!({}))), ContentPart::text("world"), ], - name: None, + name: None, tool_call_id: None, }; assert_eq!(msg.text(), "Hello world"); @@ -836,13 +836,13 @@ mod tests { #[test] fn message_text_returns_empty_for_no_text_parts() { let msg = Message { - role: Role::Assistant, - content: vec![ContentPart::ToolCall(ToolCall::new( + role: Role::Assistant, + content: vec![ContentPart::ToolCall(ToolCall::new( "c1", "test", serde_json::json!({}), ))], - name: None, + name: None, tool_call_id: None, }; assert_eq!(msg.text(), ""); @@ -897,10 +897,10 @@ mod tests { #[test] fn usage_serialization_includes_present_optional_fields() { let usage = TokenCounts { - input_tokens: 100, - output_tokens: 30, - reasoning_tokens: 20, - cache_read_tokens: 80, + input_tokens: 100, + output_tokens: 30, + reasoning_tokens: 20, + cache_read_tokens: 80, cache_write_tokens: 10, }; insta::assert_snapshot!(serde_json::to_string_pretty(&usage).unwrap(), @r#" @@ -927,17 +927,17 @@ mod tests { #[test] fn usage_addition_both_filled() { let a = TokenCounts { - input_tokens: 10, - output_tokens: 15, - reasoning_tokens: 5, - cache_read_tokens: 3, + input_tokens: 10, + output_tokens: 15, + reasoning_tokens: 5, + cache_read_tokens: 3, cache_write_tokens: 1, }; let b = TokenCounts { - input_tokens: 15, - output_tokens: 15, - reasoning_tokens: 10, - cache_read_tokens: 7, + input_tokens: 15, + output_tokens: 15, + reasoning_tokens: 10, + cache_read_tokens: 7, cache_write_tokens: 2, }; let sum = a + b; @@ -975,23 +975,26 @@ mod tests { assert_eq!(ToolChoice::None, ToolChoice::None); assert_eq!(ToolChoice::Required, ToolChoice::Required); let named = ToolChoice::named("get_weather"); - assert_eq!(named, ToolChoice::Named { - tool_name: "get_weather".to_string(), - }); + assert_eq!( + named, + ToolChoice::Named { + tool_name: "get_weather".to_string(), + } + ); } #[test] fn response_text_accessor() { let response = Response { - id: "resp_1".into(), - model: "test-model".into(), - provider: "test".into(), - message: Message::assistant("Hello world"), + id: "resp_1".into(), + model: "test-model".into(), + provider: "test".into(), + message: Message::assistant("Hello world"), finish_reason: FinishReason::Stop, - usage: TokenCounts::default(), - raw: None, - warnings: vec![], - rate_limit: None, + usage: TokenCounts::default(), + raw: None, + warnings: vec![], + rate_limit: None, }; assert_eq!(response.text(), "Hello world"); } @@ -999,12 +1002,12 @@ mod tests { #[test] fn response_tool_calls_accessor() { let response = Response { - id: "resp_1".into(), - model: "test-model".into(), - provider: "test".into(), - message: Message { - role: Role::Assistant, - content: vec![ + id: "resp_1".into(), + model: "test-model".into(), + provider: "test".into(), + message: Message { + role: Role::Assistant, + content: vec![ ContentPart::text("Let me check"), ContentPart::ToolCall(ToolCall::new( "call_1", @@ -1012,14 +1015,14 @@ mod tests { serde_json::json!({"city": "SF"}), )), ], - name: None, + name: None, tool_call_id: None, }, finish_reason: FinishReason::ToolCalls, - usage: TokenCounts::default(), - raw: None, - warnings: vec![], - rate_limit: None, + usage: TokenCounts::default(), + raw: None, + warnings: vec![], + rate_limit: None, }; let calls = response.tool_calls(); assert_eq!(calls.len(), 1); @@ -1030,27 +1033,27 @@ mod tests { #[test] fn response_reasoning_accessor() { let response = Response { - id: "resp_1".into(), - model: "test-model".into(), - provider: "test".into(), - message: Message { - role: Role::Assistant, - content: vec![ + id: "resp_1".into(), + model: "test-model".into(), + provider: "test".into(), + message: Message { + role: Role::Assistant, + content: vec![ ContentPart::Thinking(ThinkingData { - text: "Let me think...".into(), + text: "Let me think...".into(), signature: Some("sig_123".into()), - redacted: false, + redacted: false, }), ContentPart::text("The answer is 42."), ], - name: None, + name: None, tool_call_id: None, }, finish_reason: FinishReason::Stop, - usage: TokenCounts::default(), - raw: None, - warnings: vec![], - rate_limit: None, + usage: TokenCounts::default(), + raw: None, + warnings: vec![], + rate_limit: None, }; assert_eq!(response.reasoning(), Some("Let me think...".to_string())); assert_eq!(response.text(), "The answer is 42."); @@ -1059,15 +1062,15 @@ mod tests { #[test] fn response_reasoning_returns_none_when_absent() { let response = Response { - id: "resp_1".into(), - model: "test-model".into(), - provider: "test".into(), - message: Message::assistant("Hello"), + id: "resp_1".into(), + model: "test-model".into(), + provider: "test".into(), + message: Message::assistant("Hello"), finish_reason: FinishReason::Stop, - usage: TokenCounts::default(), - raw: None, - warnings: vec![], - rate_limit: None, + usage: TokenCounts::default(), + raw: None, + warnings: vec![], + rate_limit: None, }; assert_eq!(response.reasoning(), None); } @@ -1086,9 +1089,9 @@ mod tests { #[test] fn stream_event_error() { - let event = StreamEvent::error(SdkError::Stream { + let event = StreamEvent::error(Error::Stream { message: "something went wrong".into(), - source: None, + source: None, }); match &event { StreamEvent::Error { error, .. } => { @@ -1105,9 +1108,9 @@ mod tests { max_retries: 3, backoff: BackoffPolicy { initial_delay: Duration::from_secs(1), - factor: 2.0, - max_delay: Duration::from_secs(60), - jitter: false, + factor: 2.0, + max_delay: Duration::from_secs(60), + jitter: false, }, ..Default::default() }; @@ -1125,9 +1128,9 @@ mod tests { max_retries: 10, backoff: BackoffPolicy { initial_delay: Duration::from_secs(1), - factor: 2.0, - max_delay: Duration::from_secs(5), - jitter: false, + factor: 2.0, + max_delay: Duration::from_secs(5), + jitter: false, }, ..Default::default() }; @@ -1141,9 +1144,9 @@ mod tests { max_retries: 3, backoff: BackoffPolicy { initial_delay: Duration::from_secs(1), - factor: 2.0, - max_delay: Duration::from_secs(60), - jitter: true, + factor: 2.0, + max_delay: Duration::from_secs(60), + jitter: true, }, ..Default::default() }; @@ -1170,10 +1173,10 @@ mod tests { #[test] fn content_part_image_constructor() { let part = ContentPart::Image(ImageData { - url: Some("https://example.com/img.png".into()), - data: None, + url: Some("https://example.com/img.png".into()), + data: None, media_type: None, - detail: None, + detail: None, }); assert!(matches!(part, ContentPart::Image(_))); } @@ -1189,10 +1192,10 @@ mod tests { #[test] fn tool_result_with_image_data() { let result = ToolResult { - tool_call_id: "call_1".into(), - content: serde_json::json!("screenshot taken"), - is_error: false, - image_data: Some(vec![0x89, 0x50, 0x4E, 0x47]), + tool_call_id: "call_1".into(), + content: serde_json::json!("screenshot taken"), + is_error: false, + image_data: Some(vec![0x89, 0x50, 0x4E, 0x47]), image_media_type: Some("image/png".into()), }; assert!(result.image_data.is_some()); @@ -1225,19 +1228,19 @@ mod tests { #[test] fn stream_event_step_finish_constructor() { let response = Response { - id: "resp_1".into(), - model: "test-model".into(), - provider: "test".into(), - message: Message::assistant("tool response"), + id: "resp_1".into(), + model: "test-model".into(), + provider: "test".into(), + message: Message::assistant("tool response"), finish_reason: FinishReason::ToolCalls, - usage: TokenCounts { + usage: TokenCounts { input_tokens: 10, output_tokens: 5, ..Default::default() }, - raw: None, - warnings: vec![], - rate_limit: None, + raw: None, + warnings: vec![], + rate_limit: None, }; let tool_calls = vec![ToolCall::new( "call_1", diff --git a/lib/crates/fabro-llm/tests/integration.rs b/lib/crates/fabro-llm/tests/integration.rs index 28de85a83..36324633e 100644 --- a/lib/crates/fabro-llm/tests/integration.rs +++ b/lib/crates/fabro-llm/tests/integration.rs @@ -5,19 +5,19 @@ use fabro_llm::types::{FinishReason, Message, Request}; fn make_request(model: &str) -> Request { Request { - model: model.to_string(), - messages: vec![Message::user("Say hello in exactly one word")], - provider: None, - tools: None, - tool_choice: None, - response_format: None, - temperature: Some(0.0), - top_p: None, - max_tokens: Some(50), - stop_sequences: None, + model: model.to_string(), + messages: vec![Message::user("Say hello in exactly one word")], + provider: None, + tools: None, + tool_choice: None, + response_format: None, + temperature: Some(0.0), + top_p: None, + max_tokens: Some(50), + stop_sequences: None, reasoning_effort: None, - speed: None, - metadata: None, + speed: None, + metadata: None, provider_options: None, } } @@ -157,19 +157,19 @@ async fn run_multi_turn_cache_test( for turn in 0..6 { let request = Request { - model: model.to_string(), - messages: messages.clone(), - provider: None, - tools: None, - tool_choice: None, - response_format: None, - temperature: Some(0.0), - top_p: None, - max_tokens: Some(100), - stop_sequences: None, + model: model.to_string(), + messages: messages.clone(), + provider: None, + tools: None, + tool_choice: None, + response_format: None, + temperature: Some(0.0), + top_p: None, + max_tokens: Some(100), + stop_sequences: None, reasoning_effort: None, - speed: None, - metadata: None, + speed: None, + metadata: None, provider_options: None, }; diff --git a/lib/crates/fabro-mcp/src/client.rs b/lib/crates/fabro-mcp/src/client.rs index 81bbeb2b9..8fdfba7b2 100644 --- a/lib/crates/fabro-mcp/src/client.rs +++ b/lib/crates/fabro-mcp/src/client.rs @@ -32,7 +32,7 @@ enum PendingTransport { /// MCP client wrapping the rmcp SDK. Handles stdio and HTTP transports. pub struct McpClient { server_name: String, - state: Mutex, + state: Mutex, } impl McpClient { @@ -101,7 +101,7 @@ impl McpClient { Ok(Self { server_name: config.name.clone(), - state: Mutex::new(ClientState::Connecting(Some(transport))), + state: Mutex::new(ClientState::Connecting(Some(transport))), }) } diff --git a/lib/crates/fabro-mcp/src/connection_manager.rs b/lib/crates/fabro-mcp/src/connection_manager.rs index 59e12f6cc..b436e6ce1 100644 --- a/lib/crates/fabro-mcp/src/connection_manager.rs +++ b/lib/crates/fabro-mcp/src/connection_manager.rs @@ -78,16 +78,16 @@ pub fn call_result_to_string(result: &CallToolResult) -> Result /// Tool info stored per-tool in the connection manager. #[derive(Debug, Clone)] pub struct ToolInfo { - pub server_name: String, + pub server_name: String, pub original_tool_name: String, - pub description: String, - pub input_schema: serde_json::Value, + pub description: String, + pub input_schema: serde_json::Value, } /// Manages connections to multiple MCP servers and their tools. pub struct McpConnectionManager { clients: HashMap>, - tools: HashMap, + tools: HashMap, } impl McpConnectionManager { @@ -95,7 +95,7 @@ impl McpConnectionManager { pub fn new() -> Self { Self { clients: HashMap::new(), - tools: HashMap::new(), + tools: HashMap::new(), } } @@ -132,12 +132,15 @@ impl McpConnectionManager { for (name, description, input_schema) in tools { let qualified = qualified_tool_name(&config.name, &name); - self.tools.insert(qualified, ToolInfo { - server_name: config.name.clone(), - original_tool_name: name, - description, - input_schema, - }); + self.tools.insert( + qualified, + ToolInfo { + server_name: config.name.clone(), + original_tool_name: name, + description, + input_schema, + }, + ); } self.clients.insert(config.name.clone(), Arc::new(client)); diff --git a/lib/crates/fabro-mcp/tests/stdio_integration.rs b/lib/crates/fabro-mcp/tests/stdio_integration.rs index 8da5c24d6..b07d0f1a5 100644 --- a/lib/crates/fabro-mcp/tests/stdio_integration.rs +++ b/lib/crates/fabro-mcp/tests/stdio_integration.rs @@ -8,13 +8,13 @@ use fabro_mcp::connection_manager::{McpConnectionManager, call_result_to_string} fn test_server_config() -> McpServerSettings { let test_server = format!("{}/tests/test_mcp_server.py", env!("CARGO_MANIFEST_DIR")); McpServerSettings { - name: "test-echo".into(), - transport: McpTransport::Stdio { + name: "test-echo".into(), + transport: McpTransport::Stdio { command: vec!["python3".into(), test_server], - env: HashMap::new(), + env: HashMap::new(), }, startup_timeout_secs: 10, - tool_timeout_secs: 30, + tool_timeout_secs: 30, } } diff --git a/lib/crates/fabro-model/src/billing.rs b/lib/crates/fabro-model/src/billing.rs index 2e16353d9..b3bbb4f31 100644 --- a/lib/crates/fabro-model/src/billing.rs +++ b/lib/crates/fabro-model/src/billing.rs @@ -132,17 +132,17 @@ pub struct ModelRef { pub provider: Provider, pub model_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] - pub speed: Option, + pub speed: Option, } #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct TokenCounts { - pub input_tokens: i64, - pub output_tokens: i64, + pub input_tokens: i64, + pub output_tokens: i64, #[serde(default)] - pub reasoning_tokens: i64, + pub reasoning_tokens: i64, #[serde(default)] - pub cache_read_tokens: i64, + pub cache_read_tokens: i64, #[serde(default)] pub cache_write_tokens: i64, } @@ -167,10 +167,10 @@ impl std::ops::Add for TokenCounts { fn add(self, rhs: Self) -> Self::Output { Self { - input_tokens: self.input_tokens + rhs.input_tokens, - output_tokens: self.output_tokens + rhs.output_tokens, - reasoning_tokens: self.reasoning_tokens + rhs.reasoning_tokens, - cache_read_tokens: self.cache_read_tokens + rhs.cache_read_tokens, + input_tokens: self.input_tokens + rhs.input_tokens, + output_tokens: self.output_tokens + rhs.output_tokens, + reasoning_tokens: self.reasoning_tokens + rhs.reasoning_tokens, + cache_read_tokens: self.cache_read_tokens + rhs.cache_read_tokens, cache_write_tokens: self.cache_write_tokens + rhs.cache_write_tokens, } } @@ -188,30 +188,30 @@ impl std::ops::AddAssign for TokenCounts { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ModelUsage { - pub model: ModelRef, + pub model: ModelRef, pub tokens: TokenCounts, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct OpenAiModelPricing { - pub input: PricePerMTok, + pub input: PricePerMTok, pub cached_input: Option, - pub output: PricePerMTok, + pub output: PricePerMTok, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct AnthropicModelPricing { - pub input: PricePerMTok, - pub cache_read: Option, + pub input: PricePerMTok, + pub cache_read: Option, pub cache_write_5m: Option, pub cache_write_1h: Option, - pub output: PricePerMTok, + pub output: PricePerMTok, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct GeminiStorageSegment { pub cached_tokens: i64, - pub ttl_seconds: i64, + pub ttl_seconds: i64, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -221,11 +221,11 @@ pub struct GeminiStoragePricing { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct GeminiModelPricing { - pub input: PricePerMTok, - pub output: PricePerMTok, + pub input: PricePerMTok, + pub output: PricePerMTok, pub cached_input: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub storage: Option, + pub storage: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -243,7 +243,7 @@ pub enum ModelPricingPolicy { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ModelPricing { - pub model: ModelRef, + pub model: ModelRef, pub policy: ModelPricingPolicy, } @@ -302,7 +302,7 @@ pub struct ModelBillingInput { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BilledModelUsage { - pub input: ModelBillingInput, + pub input: ModelBillingInput, #[serde(default, skip_serializing_if = "Option::is_none")] pub total_usd_micros: Option, } @@ -326,17 +326,17 @@ impl BilledModelUsage { #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct BilledTokenCounts { - pub input_tokens: i64, - pub output_tokens: i64, - pub total_tokens: i64, + pub input_tokens: i64, + pub output_tokens: i64, + pub total_tokens: i64, #[serde(default)] - pub reasoning_tokens: i64, + pub reasoning_tokens: i64, #[serde(default)] - pub cache_read_tokens: i64, + pub cache_read_tokens: i64, #[serde(default)] pub cache_write_tokens: i64, #[serde(default, skip_serializing_if = "Option::is_none")] - pub total_usd_micros: Option, + pub total_usd_micros: Option, } impl BilledTokenCounts { @@ -355,13 +355,13 @@ impl BilledTokenCounts { } Self { - input_tokens: tokens.input_tokens, - output_tokens: tokens.output_tokens, - total_tokens: tokens.total_tokens(), - reasoning_tokens: tokens.reasoning_tokens, - cache_read_tokens: tokens.cache_read_tokens, + input_tokens: tokens.input_tokens, + output_tokens: tokens.output_tokens, + total_tokens: tokens.total_tokens(), + reasoning_tokens: tokens.reasoning_tokens, + cache_read_tokens: tokens.cache_read_tokens, cache_write_tokens: tokens.cache_write_tokens, - total_usd_micros: has_total.then_some(total_usd_micros), + total_usd_micros: has_total.then_some(total_usd_micros), } } } @@ -583,31 +583,31 @@ mod tests { #[test] fn openai_pricing_bills_cached_input_and_reasoning_output() { let pricing = ModelPricing { - model: ModelRef { + model: ModelRef { provider: Provider::OpenAi, model_id: "gpt-5.4".to_string(), - speed: None, + speed: None, }, policy: ModelPricingPolicy::OpenAi(OpenAiModelPricing { - input: PricePerMTok { + input: PricePerMTok { usd_micros: 1_250_000, }, cached_input: Some(PricePerMTok { usd_micros: 125_000, }), - output: PricePerMTok { + output: PricePerMTok { usd_micros: 10_000_000, }, }), }; let input = ModelBillingInput { usage: ModelUsage { - model: pricing.model.clone(), + model: pricing.model.clone(), tokens: TokenCounts { - input_tokens: 500_000, - output_tokens: 125_000, - reasoning_tokens: 25_000, - cache_read_tokens: 250_000, + input_tokens: 500_000, + output_tokens: 125_000, + reasoning_tokens: 25_000, + cache_read_tokens: 250_000, cache_write_tokens: 0, }, }, @@ -636,16 +636,16 @@ mod tests { #[test] fn anthropic_billing_supports_distinct_cache_write_buckets() { let pricing = ModelPricing { - model: ModelRef { + model: ModelRef { provider: Provider::Anthropic, model_id: "claude-opus-4-6".to_string(), - speed: Some(Speed::Fast), + speed: Some(Speed::Fast), }, policy: ModelPricingPolicy::Anthropic(AnthropicModelPricing { - input: PricePerMTok { + input: PricePerMTok { usd_micros: 30_000_000, }, - cache_read: Some(PricePerMTok { + cache_read: Some(PricePerMTok { usd_micros: 3_000_000, }), cache_write_5m: Some(PricePerMTok { @@ -654,19 +654,19 @@ mod tests { cache_write_1h: Some(PricePerMTok { usd_micros: 60_000_000, }), - output: PricePerMTok { + output: PricePerMTok { usd_micros: 150_000_000, }, }), }; let input = ModelBillingInput { usage: ModelUsage { - model: pricing.model.clone(), + model: pricing.model.clone(), tokens: TokenCounts { - input_tokens: 100_000, - output_tokens: 10_000, - reasoning_tokens: 5_000, - cache_read_tokens: 20_000, + input_tokens: 100_000, + output_tokens: 10_000, + reasoning_tokens: 5_000, + cache_read_tokens: 20_000, cache_write_tokens: 0, }, }, @@ -682,37 +682,37 @@ mod tests { #[test] fn gemini_billing_requires_storage_pricing_when_storage_facts_exist() { let pricing = ModelPricing { - model: ModelRef { + model: ModelRef { provider: Provider::Gemini, model_id: "gemini-3.1-pro-preview".to_string(), - speed: None, + speed: None, }, policy: ModelPricingPolicy::Gemini(GeminiModelPricing { - input: PricePerMTok { + input: PricePerMTok { usd_micros: 1_250_000, }, - output: PricePerMTok { + output: PricePerMTok { usd_micros: 10_000_000, }, cached_input: None, - storage: None, + storage: None, }), }; let input = ModelBillingInput { usage: ModelUsage { - model: pricing.model.clone(), + model: pricing.model.clone(), tokens: TokenCounts { - input_tokens: 100_000, - output_tokens: 10_000, - reasoning_tokens: 0, - cache_read_tokens: 0, + input_tokens: 100_000, + output_tokens: 10_000, + reasoning_tokens: 0, + cache_read_tokens: 0, cache_write_tokens: 0, }, }, facts: ModelBillingFacts::Gemini(GeminiBillingFacts { storage_segments: vec![GeminiStorageSegment { cached_tokens: 100_000, - ttl_seconds: 60, + ttl_seconds: 60, }], }), }; diff --git a/lib/crates/fabro-model/src/catalog.rs b/lib/crates/fabro-model/src/catalog.rs index 09b544555..fdf0ecea4 100644 --- a/lib/crates/fabro-model/src/catalog.rs +++ b/lib/crates/fabro-model/src/catalog.rs @@ -15,7 +15,7 @@ static GLOBAL_CATALOG: LazyLock = LazyLock::new(|| { #[derive(Debug, Clone, PartialEq, Eq)] pub struct FallbackTarget { pub provider: String, - pub model: String, + pub model: String, } /// Typed model catalog backed by a `Vec`. @@ -153,7 +153,7 @@ impl Catalog { let provider = provider_str.parse::().ok()?; self.closest(provider, reference).map(|m| FallbackTarget { provider: provider_str.clone(), - model: m.id.clone(), + model: m.id.clone(), }) }) .collect() @@ -274,10 +274,10 @@ mod tests { #[test] fn builtin_build_fallback_chain() { - let fallbacks = HashMap::from([("anthropic".to_string(), vec![ - "gemini".to_string(), - "openai".to_string(), - ])]); + let fallbacks = HashMap::from([( + "anthropic".to_string(), + vec!["gemini".to_string(), "openai".to_string()], + )]); let chain = Catalog::builtin().build_fallback_chain( Provider::Anthropic, "claude-opus-4-6", @@ -311,10 +311,10 @@ mod tests { #[test] fn builtin_build_fallback_chain_skips_no_capability_match() { - let fallbacks = HashMap::from([("anthropic".to_string(), vec![ - "openai".to_string(), - "kimi".to_string(), - ])]); + let fallbacks = HashMap::from([( + "anthropic".to_string(), + vec!["openai".to_string(), "kimi".to_string()], + )]); let chain = Catalog::builtin().build_fallback_chain( Provider::Anthropic, "claude-haiku-4-5", @@ -341,30 +341,30 @@ mod tests { use crate::types::{Model, ModelCosts, ModelFeatures, ModelLimits}; let models = vec![Model { - id: "test-model".to_string(), - provider: Provider::Anthropic, - family: "test".to_string(), - display_name: "Test Model".to_string(), - limits: ModelLimits { + id: "test-model".to_string(), + provider: Provider::Anthropic, + family: "test".to_string(), + display_name: "Test Model".to_string(), + limits: ModelLimits { context_window: 100_000, - max_output: Some(4096), + max_output: Some(4096), }, - training: None, - knowledge_cutoff: None, - features: ModelFeatures { - tools: true, - vision: false, + training: None, + knowledge_cutoff: None, + features: ModelFeatures { + tools: true, + vision: false, reasoning: false, - effort: false, + effort: false, }, - costs: ModelCosts { - input_cost_per_mtok: Some(1.0), - output_cost_per_mtok: Some(5.0), + costs: ModelCosts { + input_cost_per_mtok: Some(1.0), + output_cost_per_mtok: Some(5.0), cache_input_cost_per_mtok: None, }, estimated_output_tps: None, - aliases: vec!["test".to_string()], - default: true, + aliases: vec!["test".to_string()], + default: true, }]; let catalog = Catalog::from_models(models); diff --git a/lib/crates/fabro-model/src/model_ref.rs b/lib/crates/fabro-model/src/model_ref.rs index 96cbced9b..38ad13d6b 100644 --- a/lib/crates/fabro-model/src/model_ref.rs +++ b/lib/crates/fabro-model/src/model_ref.rs @@ -11,10 +11,7 @@ pub enum ModelHandle { /// A model whose metadata has been resolved from the catalog. Resolved(Arc), /// An unresolved provider:model pair (e.g. from CLI input or config). - ByName { - provider: Provider, - model: String, - }, + ByName { provider: Provider, model: String }, } impl ModelHandle { @@ -65,7 +62,7 @@ mod tests { fn by_name_display() { let r = ModelHandle::ByName { provider: Provider::Anthropic, - model: "claude-opus-4-6".to_string(), + model: "claude-opus-4-6".to_string(), }; assert_eq!(r.to_string(), "anthropic:claude-opus-4-6"); } @@ -74,7 +71,7 @@ mod tests { fn by_name_accessors() { let r = ModelHandle::ByName { provider: Provider::OpenAi, - model: "gpt-5.4".to_string(), + model: "gpt-5.4".to_string(), }; assert_eq!(r.model_id(), "gpt-5.4"); assert_eq!(r.provider(), Provider::OpenAi); @@ -99,7 +96,7 @@ mod tests { fn debug_format() { let r = ModelHandle::ByName { provider: Provider::Gemini, - model: "gemini-3.1-pro-preview".to_string(), + model: "gemini-3.1-pro-preview".to_string(), }; let debug = format!("{r:?}"); assert!(debug.contains("ByName")); diff --git a/lib/crates/fabro-model/src/provider.rs b/lib/crates/fabro-model/src/provider.rs index e5666850a..1f33fb5ec 100644 --- a/lib/crates/fabro-model/src/provider.rs +++ b/lib/crates/fabro-model/src/provider.rs @@ -217,9 +217,10 @@ mod tests { #[test] fn api_key_env_vars_anthropic() { - assert_eq!(Provider::Anthropic.api_key_env_vars(), &[ - "ANTHROPIC_API_KEY" - ]); + assert_eq!( + Provider::Anthropic.api_key_env_vars(), + &["ANTHROPIC_API_KEY"] + ); } #[test] @@ -251,9 +252,10 @@ mod tests { #[test] fn api_key_env_vars_inception() { - assert_eq!(Provider::Inception.api_key_env_vars(), &[ - "INCEPTION_API_KEY" - ]); + assert_eq!( + Provider::Inception.api_key_env_vars(), + &["INCEPTION_API_KEY"] + ); } #[test] diff --git a/lib/crates/fabro-model/src/types.rs b/lib/crates/fabro-model/src/types.rs index 57d89b545..bff08dfa1 100644 --- a/lib/crates/fabro-model/src/types.rs +++ b/lib/crates/fabro-model/src/types.rs @@ -7,13 +7,13 @@ use crate::provider::Provider; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ModelLimits { pub context_window: i64, - pub max_output: Option, + pub max_output: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ModelFeatures { - pub tools: bool, - pub vision: bool, + pub tools: bool, + pub vision: bool, pub reasoning: bool, /// Whether the model supports the `reasoning_effort` / `effort` parameter /// directly (e.g. Anthropic `output_config.effort`, OpenAI @@ -21,31 +21,31 @@ pub struct ModelFeatures { /// (e.g. claude-sonnet-4-5) need the older `thinking` API with /// `budget_tokens` instead. #[serde(default)] - pub effort: bool, + pub effort: bool, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ModelCosts { - pub input_cost_per_mtok: Option, - pub output_cost_per_mtok: Option, + pub input_cost_per_mtok: Option, + pub output_cost_per_mtok: Option, pub cache_input_cost_per_mtok: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Model { - pub id: String, - pub provider: Provider, - pub family: String, - pub display_name: String, - pub limits: ModelLimits, - pub training: Option, - pub knowledge_cutoff: Option, - pub features: ModelFeatures, - pub costs: ModelCosts, + pub id: String, + pub provider: Provider, + pub family: String, + pub display_name: String, + pub limits: ModelLimits, + pub training: Option, + pub knowledge_cutoff: Option, + pub features: ModelFeatures, + pub costs: ModelCosts, pub estimated_output_tps: Option, - pub aliases: Vec, + pub aliases: Vec, #[serde(default)] - pub default: bool, + pub default: bool, } impl Model { diff --git a/lib/crates/fabro-oauth/src/lib.rs b/lib/crates/fabro-oauth/src/lib.rs index e91ee0d10..255858ee8 100644 --- a/lib/crates/fabro-oauth/src/lib.rs +++ b/lib/crates/fabro-oauth/src/lib.rs @@ -16,7 +16,7 @@ use tokio::sync::oneshot; // --------------------------------------------------------------------------- pub struct PkceCodes { - pub verifier: String, + pub verifier: String, pub challenge: String, } @@ -96,10 +96,10 @@ pub fn build_authorize_url( #[derive(Debug, Deserialize)] pub struct TokenResponse { - pub id_token: Option, - pub access_token: String, + pub id_token: Option, + pub access_token: String, pub refresh_token: Option, - pub expires_in: Option, + pub expires_in: Option, } // --------------------------------------------------------------------------- @@ -198,9 +198,9 @@ pub async fn refresh_access_token( #[derive(Deserialize)] struct CallbackParams { - code: Option, - state: String, - error: Option, + code: Option, + state: String, + error: Option, error_description: Option, } diff --git a/lib/crates/fabro-proc/src/title.rs b/lib/crates/fabro-proc/src/title.rs index f1f087636..e9dafa81f 100644 --- a/lib/crates/fabro-proc/src/title.rs +++ b/lib/crates/fabro-proc/src/title.rs @@ -2,7 +2,7 @@ use std::sync::{Mutex, OnceLock}; struct Buffer { start: *mut u8, - len: usize, + len: usize, } // Safety: after init() we treat the captured argv region as exclusively diff --git a/lib/crates/fabro-retro/src/retro.rs b/lib/crates/fabro-retro/src/retro.rs index 13011a934..3e1e112de 100644 --- a/lib/crates/fabro-retro/src/retro.rs +++ b/lib/crates/fabro-retro/src/retro.rs @@ -8,15 +8,15 @@ pub use fabro_types::retro::{ #[derive(Debug, Clone)] pub struct CompletedStage { - pub node_id: String, - pub status: String, - pub succeeded: bool, - pub failed: bool, - pub retries: u32, + pub node_id: String, + pub status: String, + pub succeeded: bool, + pub failed: bool, + pub retries: u32, pub billing_usd_micros: Option, - pub notes: Option, - pub failure_reason: Option, - pub files_touched: Vec, + pub notes: Option, + pub failure_reason: Option, + pub files_touched: Vec, } pub fn derive_retro( @@ -51,15 +51,15 @@ pub fn derive_retro( let dur = stage_durations.get(&cs.node_id).copied().unwrap_or(0); stages.push(StageRetro { - stage_label: cs.node_id.clone(), - duration_ms: dur, - retries: cs.retries, + stage_label: cs.node_id.clone(), + duration_ms: dur, + retries: cs.retries, billing_usd_micros: cs.billing_usd_micros, - stage_id: cs.node_id, - status: cs.status, - notes: cs.notes, - failure_reason: cs.failure_reason, - files_touched: cs.files_touched, + stage_id: cs.node_id, + status: cs.status, + notes: cs.notes, + failure_reason: cs.failure_reason, + files_touched: cs.files_touched, }); all_files.extend( diff --git a/lib/crates/fabro-retro/src/retro_agent.rs b/lib/crates/fabro-retro/src/retro_agent.rs index e0a08e27b..b5c0643e8 100644 --- a/lib/crates/fabro-retro/src/retro_agent.rs +++ b/lib/crates/fabro-retro/src/retro_agent.rs @@ -116,7 +116,7 @@ pub const RETRO_DATA_DIR: &str = "/tmp/retro_data"; pub struct RetroAgentResult { pub narrative: RetroNarrative, - pub response: String, + pub response: String, } #[must_use] @@ -256,12 +256,12 @@ pub async fn run_retro_agent( /// derive → apply_narrative → save path without making LLM calls. pub fn dry_run_narrative() -> RetroNarrative { RetroNarrative { - smoothness: SmoothnessRating::Smooth, - intent: "[dry-run] No LLM analysis performed".to_string(), - outcome: "[dry-run] Run completed in simulated mode".to_string(), - learnings: vec![], + smoothness: SmoothnessRating::Smooth, + intent: "[dry-run] No LLM analysis performed".to_string(), + outcome: "[dry-run] Run completed in simulated mode".to_string(), + learnings: vec![], friction_points: vec![], - open_items: vec![], + open_items: vec![], } } diff --git a/lib/crates/fabro-sandbox/src/config.rs b/lib/crates/fabro-sandbox/src/config.rs index 4f5ed268c..f57074887 100644 --- a/lib/crates/fabro-sandbox/src/config.rs +++ b/lib/crates/fabro-sandbox/src/config.rs @@ -16,11 +16,11 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] pub struct DaytonaSettings { pub auto_stop_interval: Option, - pub labels: Option>, - pub snapshot: Option, - pub network: Option, + pub labels: Option>, + pub snapshot: Option, + pub network: Option, #[serde(default)] - pub skip_clone: bool, + pub skip_clone: bool, } #[derive(Clone, Debug, PartialEq)] @@ -117,10 +117,10 @@ pub enum DockerfileSource { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct DaytonaSnapshotSettings { - pub name: String, - pub cpu: Option, - pub memory: Option, - pub disk: Option, + pub name: String, + pub cpu: Option, + pub memory: Option, + pub disk: Option, pub dockerfile: Option, } diff --git a/lib/crates/fabro-sandbox/src/daytona/mod.rs b/lib/crates/fabro-sandbox/src/daytona/mod.rs index 557cf8dc0..671ee24d9 100644 --- a/lib/crates/fabro-sandbox/src/daytona/mod.rs +++ b/lib/crates/fabro-sandbox/src/daytona/mod.rs @@ -28,20 +28,20 @@ pub use crate::config::{ /// Sandbox that runs all operations inside a Daytona cloud sandbox. pub struct DaytonaSandbox { - config: DaytonaConfig, - client: daytona_sdk::Client, - github_app: Option, - sandbox: OnceCell, - rg_available: OnceCell, + config: DaytonaConfig, + client: daytona_sdk::Client, + github_app: Option, + sandbox: OnceCell, + rg_available: OnceCell, event_callback: Option, /// HTTPS origin URL stored after clone so we can refresh push credentials /// later. - origin_url: OnceCell, - run_id: Option, + 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 /// (e.g. `fabro/run/...`) that was never pushed to origin. - clone_branch: Option, + clone_branch: Option, } impl DaytonaSandbox { @@ -231,11 +231,11 @@ impl DaytonaSandbox { }; let params = daytona_sdk::CreateSnapshotParams { - name: snap_cfg.name.clone(), - image: daytona_sdk::ImageSource::Custom( + name: snap_cfg.name.clone(), + image: daytona_sdk::ImageSource::Custom( daytona_sdk::DockerImage::from_dockerfile(dockerfile), ), - resources: Some(daytona_sdk::Resources { + resources: Some(daytona_sdk::Resources { cpu: snap_cfg.cpu, memory: snap_cfg.memory, disk: snap_cfg.disk, @@ -302,7 +302,7 @@ use fabro_github::ssh_url_to_https; #[derive(Clone, Debug)] pub struct GitCloneParams { /// Clean HTTPS URL (no embedded credentials). - pub url: String, + pub url: String, /// Branch to clone. If None, uses the remote's default. pub branch: Option, } @@ -426,7 +426,7 @@ impl Sandbox for DaytonaSandbox { let snap_start = Instant::now(); if let Err(e) = self.ensure_snapshot(snap_cfg).await { self.emit(SandboxEvent::SnapshotFailed { - name: snap_cfg.name.clone(), + name: snap_cfg.name.clone(), error: e.clone(), }); let duration_ms = @@ -440,17 +440,17 @@ impl Sandbox for DaytonaSandbox { } let snap_duration = u64::try_from(snap_start.elapsed().as_millis()).unwrap_or(u64::MAX); self.emit(SandboxEvent::SnapshotReady { - name: snap_cfg.name.clone(), + name: snap_cfg.name.clone(), duration_ms: snap_duration, }); daytona_sdk::CreateParams::Snapshot(daytona_sdk::SnapshotParams { - base: self.base_params(), + base: self.base_params(), snapshot: snap_cfg.name.clone(), }) } else { daytona_sdk::CreateParams::Snapshot(daytona_sdk::SnapshotParams { - base: self.base_params(), + base: self.base_params(), snapshot: DEFAULT_SNAPSHOT.to_string(), }) }; @@ -492,7 +492,7 @@ impl Sandbox for DaytonaSandbox { // Daytona clones over HTTPS with token auth, so rewrite SSH URLs. let url = ssh_url_to_https(&detected_url); self.emit(SandboxEvent::GitCloneStarted { - url: url.clone(), + url: url.clone(), branch: branch.clone(), }); let clone_start = Instant::now(); @@ -504,7 +504,7 @@ impl Sandbox for DaytonaSandbox { .map_err(|e| { let err = format!("Failed to parse GitHub URL for clone: {e}"); self.emit(SandboxEvent::GitCloneFailed { - url: url.clone(), + url: url.clone(), error: err.clone(), }); err @@ -520,7 +520,7 @@ impl Sandbox for DaytonaSandbox { let err = format!("Failed to get GitHub App credentials for clone: {e}"); self.emit(SandboxEvent::GitCloneFailed { - url: url.clone(), + url: url.clone(), error: err.clone(), }); let duration_ms = u64::try_from(init_start.elapsed().as_millis()) @@ -544,7 +544,7 @@ impl Sandbox for DaytonaSandbox { Ok(g) => g, Err(e) => { self.emit(SandboxEvent::GitCloneFailed { - url: url.clone(), + url: url.clone(), error: e.clone(), }); let duration_ms = @@ -560,12 +560,16 @@ impl Sandbox for DaytonaSandbox { let clone_token = password.clone(); let clone_result = git_svc - .clone(&url, WORKING_DIRECTORY, daytona_sdk::GitCloneOptions { - branch, - username, - password, - ..Default::default() - }) + .clone( + &url, + WORKING_DIRECTORY, + daytona_sdk::GitCloneOptions { + branch, + username, + password, + ..Default::default() + }, + ) .await; match clone_result { @@ -573,7 +577,7 @@ impl Sandbox for DaytonaSandbox { let clone_duration = u64::try_from(clone_start.elapsed().as_millis()) .unwrap_or(u64::MAX); self.emit(SandboxEvent::GitCloneCompleted { - url: url.clone(), + url: url.clone(), duration_ms: clone_duration, }); @@ -669,12 +673,12 @@ impl Sandbox for DaytonaSandbox { let init_duration = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); self.emit(SandboxEvent::Ready { - provider: "daytona".into(), + provider: "daytona".into(), duration_ms: init_duration, - name: Some(sandbox_name), - cpu: Some(sandbox_cpu), - memory: Some(sandbox_memory), - url: Some("https://app.daytona.io/dashboard/sandboxes".into()), + name: Some(sandbox_name), + cpu: Some(sandbox_cpu), + memory: Some(sandbox_memory), + url: Some("https://app.daytona.io/dashboard/sandboxes".into()), }); Ok(()) @@ -691,7 +695,7 @@ impl Sandbox for DaytonaSandbox { let err = format!("Failed to delete Daytona sandbox: {e}"); self.emit(SandboxEvent::CleanupFailed { provider: "daytona".into(), - error: err.clone(), + error: err.clone(), }); return Err(err); } @@ -929,9 +933,9 @@ impl Sandbox for DaytonaSandbox { Ok(files .into_iter() .map(|f| DirEntry { - name: f.name, + name: f.name, is_dir: f.is_dir, - size: if f.size > 0 { + size: if f.size > 0 { Some(u64::try_from(f.size).unwrap()) } else { None @@ -967,8 +971,8 @@ impl Sandbox for DaytonaSandbox { ); let options = daytona_sdk::ExecuteCommandOptions { - cwd: Some(cwd), - env: env_vars.cloned(), + cwd: Some(cwd), + env: env_vars.cloned(), timeout: Some(std::time::Duration::from_millis(timeout_ms)), }; diff --git a/lib/crates/fabro-sandbox/src/docker.rs b/lib/crates/fabro-sandbox/src/docker.rs index c61730ec8..ccea98232 100644 --- a/lib/crates/fabro-sandbox/src/docker.rs +++ b/lib/crates/fabro-sandbox/src/docker.rs @@ -23,37 +23,37 @@ use crate::{ /// Configuration for a Docker-based sandbox. pub struct DockerSandboxOptions { /// Docker image to use. Default: `"fabro-agent:latest"`. - pub image: String, + pub image: String, /// Host directory to bind-mount into the container. pub host_working_directory: String, /// Mount point inside the container. Default: `"/workspace"`. - pub container_mount_point: String, + pub container_mount_point: String, /// Docker network mode. Default: `Some("bridge")`. - pub network_mode: Option, + pub network_mode: Option, /// Additional `"host_path:container_path"` bind mounts. - pub extra_mounts: Vec, + pub extra_mounts: Vec, /// Memory limit in bytes. `None` = unlimited. - pub memory_limit: Option, + pub memory_limit: Option, /// CPU quota (microseconds per 100ms period). `None` = unlimited. - pub cpu_quota: Option, + pub cpu_quota: Option, /// Whether to pull the image if not found locally. Default: `true`. - pub auto_pull: bool, + pub auto_pull: bool, /// Additional `KEY=VALUE` environment variables for the container. - pub env_vars: Vec, + pub env_vars: Vec, } impl Default for DockerSandboxOptions { fn default() -> Self { Self { - image: "fabro-agent:latest".to_string(), + image: "fabro-agent:latest".to_string(), host_working_directory: String::new(), - container_mount_point: "/workspace".to_string(), - network_mode: Some("bridge".to_string()), - extra_mounts: Vec::new(), - memory_limit: None, - cpu_quota: None, - auto_pull: true, - env_vars: Vec::new(), + container_mount_point: "/workspace".to_string(), + network_mode: Some("bridge".to_string()), + extra_mounts: Vec::new(), + memory_limit: None, + cpu_quota: None, + auto_pull: true, + env_vars: Vec::new(), } } } @@ -64,13 +64,13 @@ impl Default for DockerSandboxOptions { /// file operations, commands, grep, and glob execute inside the container via /// `docker exec`. pub struct DockerSandbox { - docker: Docker, - config: DockerSandboxOptions, - container_id: OnceCell, - cached_platform: std::sync::OnceLock, + docker: Docker, + config: DockerSandboxOptions, + container_id: OnceCell, + cached_platform: std::sync::OnceLock, cached_os_version: std::sync::OnceLock, - rg_available: OnceCell, - event_callback: Option, + rg_available: OnceCell, + event_callback: Option, } impl DockerSandbox { @@ -361,7 +361,7 @@ impl Sandbox for DockerSandbox { } let pull_duration = u64::try_from(pull_start.elapsed().as_millis()).unwrap_or(u64::MAX); self.emit(SandboxEvent::SnapshotPulled { - name: self.config.image.clone(), + name: self.config.image.clone(), duration_ms: pull_duration, }); @@ -432,12 +432,12 @@ impl Sandbox for DockerSandbox { let init_duration = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); self.emit(SandboxEvent::Ready { - provider: "docker".into(), + provider: "docker".into(), duration_ms: init_duration, - name: None, - cpu: None, - memory: None, - url: None, + name: None, + cpu: None, + memory: None, + url: None, }); Ok(()) diff --git a/lib/crates/fabro-sandbox/src/local.rs b/lib/crates/fabro-sandbox/src/local.rs index 209e9647b..6ded36382 100644 --- a/lib/crates/fabro-sandbox/src/local.rs +++ b/lib/crates/fabro-sandbox/src/local.rs @@ -14,8 +14,8 @@ use crate::{ pub struct LocalSandbox { working_directory: PathBuf, - event_callback: Option, - rg_available: std::sync::OnceLock, + event_callback: Option, + rg_available: std::sync::OnceLock, } impl LocalSandbox { @@ -795,10 +795,14 @@ mod tests { let env = LocalSandbox::new(dir.clone()); let results = env - .grep("hello", "test.txt", &GrepOptions { - case_insensitive: true, - ..Default::default() - }) + .grep( + "hello", + "test.txt", + &GrepOptions { + case_insensitive: true, + ..Default::default() + }, + ) .await .unwrap(); @@ -813,10 +817,14 @@ mod tests { let env = LocalSandbox::new(dir.clone()); let results = env - .grep("match", "test.txt", &GrepOptions { - max_results: Some(2), - ..Default::default() - }) + .grep( + "match", + "test.txt", + &GrepOptions { + max_results: Some(2), + ..Default::default() + }, + ) .await .unwrap(); diff --git a/lib/crates/fabro-sandbox/src/read_guard.rs b/lib/crates/fabro-sandbox/src/read_guard.rs index 19b65404a..910eec845 100644 --- a/lib/crates/fabro-sandbox/src/read_guard.rs +++ b/lib/crates/fabro-sandbox/src/read_guard.rs @@ -13,7 +13,7 @@ use crate::Sandbox; /// `write_file` or `delete_file` targets an existing file that hasn't been /// read. Writing to new (non-existent) files is always allowed. pub struct ReadBeforeWriteSandbox { - inner: Arc, + inner: Arc, read_set: Mutex>, } diff --git a/lib/crates/fabro-sandbox/src/sandbox.rs b/lib/crates/fabro-sandbox/src/sandbox.rs index 8b8284fc5..50bce5456 100644 --- a/lib/crates/fabro-sandbox/src/sandbox.rs +++ b/lib/crates/fabro-sandbox/src/sandbox.rs @@ -12,8 +12,8 @@ const GIT: &str = "git -c maintenance.auto=0 -c gc.auto=0"; /// Information returned when a sandbox sets up git for a workflow run. pub struct GitRunInfo { - pub base_sha: String, - pub run_branch: String, + pub base_sha: String, + pub run_branch: String, pub base_branch: Option, } @@ -188,28 +188,28 @@ pub enum SandboxEvent { provider: String, }, Ready { - provider: String, + provider: String, duration_ms: u64, - name: Option, - cpu: Option, - memory: Option, - url: Option, + name: Option, + cpu: Option, + memory: Option, + url: Option, }, InitializeFailed { - provider: String, - error: String, + provider: String, + error: String, duration_ms: u64, }, CleanupStarted { provider: String, }, CleanupCompleted { - provider: String, + provider: String, duration_ms: u64, }, CleanupFailed { provider: String, - error: String, + error: String, }, // -- Docker -- @@ -217,7 +217,7 @@ pub enum SandboxEvent { name: String, }, SnapshotPulled { - name: String, + name: String, duration_ms: u64, }, @@ -229,25 +229,25 @@ pub enum SandboxEvent { name: String, }, SnapshotReady { - name: String, + name: String, duration_ms: u64, }, SnapshotFailed { - name: String, + name: String, error: String, }, // -- Daytona git -- GitCloneStarted { - url: String, + url: String, branch: Option, }, GitCloneCompleted { - url: String, + url: String, duration_ms: u64, }, GitCloneFailed { - url: String, + url: String, error: String, }, } @@ -344,25 +344,25 @@ pub fn format_lines_numbered(content: &str, offset: Option, limit: Option #[derive(Debug, Clone)] pub struct ExecResult { - pub stdout: String, - pub stderr: String, - pub exit_code: i32, - pub timed_out: bool, + pub stdout: String, + pub stderr: String, + pub exit_code: i32, + pub timed_out: bool, pub duration_ms: u64, } #[derive(Debug, Clone)] pub struct DirEntry { - pub name: String, + pub name: String, pub is_dir: bool, - pub size: Option, + pub size: Option, } #[derive(Debug, Clone, Default)] pub struct GrepOptions { - pub glob_filter: Option, + pub glob_filter: Option, pub case_insensitive: bool, - pub max_results: Option, + pub max_results: Option, } #[async_trait] @@ -612,10 +612,10 @@ mod tests { #[test] fn exec_result_fields() { let result = ExecResult { - stdout: "out".into(), - stderr: "err".into(), - exit_code: 1, - timed_out: true, + stdout: "out".into(), + stderr: "err".into(), + exit_code: 1, + timed_out: true, duration_ms: 5000, }; assert_eq!(result.exit_code, 1); @@ -626,9 +626,9 @@ mod tests { #[test] fn dir_entry_fields() { let entry = DirEntry { - name: "src".into(), + name: "src".into(), is_dir: true, - size: None, + size: None, }; assert_eq!(entry.name, "src"); assert!(entry.is_dir); @@ -650,34 +650,34 @@ mod tests { provider: "local".into(), }, SandboxEvent::Ready { - provider: "local".into(), + provider: "local".into(), duration_ms: 50, - name: None, - cpu: None, - memory: None, - url: None, + name: None, + cpu: None, + memory: None, + url: None, }, SandboxEvent::InitializeFailed { - provider: "docker".into(), - error: "no daemon".into(), + provider: "docker".into(), + error: "no daemon".into(), duration_ms: 100, }, SandboxEvent::CleanupStarted { provider: "daytona".into(), }, SandboxEvent::CleanupCompleted { - provider: "daytona".into(), + provider: "daytona".into(), duration_ms: 200, }, SandboxEvent::CleanupFailed { provider: "docker".into(), - error: "container gone".into(), + error: "container gone".into(), }, SandboxEvent::SnapshotPulling { name: "ubuntu:22.04".into(), }, SandboxEvent::SnapshotPulled { - name: "ubuntu:22.04".into(), + name: "ubuntu:22.04".into(), duration_ms: 5000, }, SandboxEvent::SnapshotEnsuring { @@ -687,23 +687,23 @@ mod tests { name: "my-snap".into(), }, SandboxEvent::SnapshotReady { - name: "my-snap".into(), + name: "my-snap".into(), duration_ms: 30000, }, SandboxEvent::SnapshotFailed { - name: "my-snap".into(), + name: "my-snap".into(), error: "build failed".into(), }, SandboxEvent::GitCloneStarted { - url: "https://github.com/org/repo.git".into(), + url: "https://github.com/org/repo.git".into(), branch: Some("main".into()), }, SandboxEvent::GitCloneCompleted { - url: "https://github.com/org/repo.git".into(), + url: "https://github.com/org/repo.git".into(), duration_ms: 8000, }, SandboxEvent::GitCloneFailed { - url: "https://github.com/org/repo.git".into(), + url: "https://github.com/org/repo.git".into(), error: "auth failed".into(), }, ]; diff --git a/lib/crates/fabro-sandbox/src/sandbox_spec.rs b/lib/crates/fabro-sandbox/src/sandbox_spec.rs index a2031dd81..6023c2c5d 100644 --- a/lib/crates/fabro-sandbox/src/sandbox_spec.rs +++ b/lib/crates/fabro-sandbox/src/sandbox_spec.rs @@ -27,9 +27,9 @@ pub enum SandboxSpec { }, #[cfg(feature = "daytona")] Daytona { - config: DaytonaConfig, - github_app: Option, - run_id: Option, + config: DaytonaConfig, + github_app: Option, + run_id: Option, clone_branch: Option, }, } @@ -156,15 +156,15 @@ impl SandboxSpec { #[cfg(feature = "docker")] Self::Docker { config } => { let mut sandbox = DockerSandbox::new(DockerSandboxOptions { - image: config.image.clone(), + image: config.image.clone(), host_working_directory: config.host_working_directory.clone(), - container_mount_point: config.container_mount_point.clone(), - network_mode: config.network_mode.clone(), - extra_mounts: config.extra_mounts.clone(), - memory_limit: config.memory_limit, - cpu_quota: config.cpu_quota, - auto_pull: config.auto_pull, - env_vars: config.env_vars.clone(), + container_mount_point: config.container_mount_point.clone(), + network_mode: config.network_mode.clone(), + extra_mounts: config.extra_mounts.clone(), + memory_limit: config.memory_limit, + cpu_quota: config.cpu_quota, + auto_pull: config.auto_pull, + env_vars: config.env_vars.clone(), }) .map_err(|e| anyhow!("Failed to create Docker sandbox: {e}"))?; if let Some(callback) = event_callback { diff --git a/lib/crates/fabro-sandbox/src/test_support.rs b/lib/crates/fabro-sandbox/src/test_support.rs index d9fe1423c..a166daaf6 100644 --- a/lib/crates/fabro-sandbox/src/test_support.rs +++ b/lib/crates/fabro-sandbox/src/test_support.rs @@ -10,28 +10,28 @@ use crate::{DirEntry, ExecResult, GrepOptions, Sandbox, SandboxEvent, SandboxEve // --- MockSandbox --- pub struct MockSandbox { - pub files: HashMap, - pub exec_result: ExecResult, - pub grep_results: Vec, - pub glob_results: Vec, - pub working_dir: &'static str, - pub platform_str: &'static str, - pub os_version_str: String, + pub files: HashMap, + pub exec_result: ExecResult, + pub grep_results: Vec, + pub glob_results: Vec, + pub working_dir: &'static str, + pub platform_str: &'static str, + pub os_version_str: String, /// When true, `read_file` applies offset/limit by splitting on lines. pub apply_read_offset_limit: bool, /// Captures (path, content) pairs from `write_file` calls. - pub written_files: Mutex>, + pub written_files: Mutex>, /// Captures the `timeout_ms` argument from `exec_command` calls. - pub captured_timeout: Mutex>, + pub captured_timeout: Mutex>, /// Captures the `command` argument from `exec_command` calls (last only). - pub captured_command: Mutex>, + pub captured_command: Mutex>, /// Captures all `command` arguments from `exec_command` calls in order. - pub captured_commands: Mutex>, + pub captured_commands: Mutex>, /// Captures all `working_dir` arguments from `exec_command` calls in order. - pub captured_working_dirs: Mutex>>, + pub captured_working_dirs: Mutex>>, /// Captures the `env_vars` argument from `exec_command` calls. - pub captured_env_vars: Mutex>>, - pub event_callback: Option, + pub captured_env_vars: Mutex>>, + pub event_callback: Option, } impl MockSandbox { @@ -57,27 +57,27 @@ impl MockSandbox { impl Default for MockSandbox { fn default() -> Self { Self { - files: HashMap::new(), - exec_result: ExecResult { - stdout: "mock output".into(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + files: HashMap::new(), + exec_result: ExecResult { + stdout: "mock output".into(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 10, }, - grep_results: vec![], - glob_results: vec![], - working_dir: "/work", - platform_str: "darwin", - os_version_str: "Darwin 24.0.0".into(), + grep_results: vec![], + glob_results: vec![], + working_dir: "/work", + platform_str: "darwin", + os_version_str: "Darwin 24.0.0".into(), apply_read_offset_limit: false, - written_files: Mutex::new(Vec::new()), - captured_timeout: Mutex::new(None), - captured_command: Mutex::new(None), - captured_commands: Mutex::new(Vec::new()), - captured_working_dirs: Mutex::new(Vec::new()), - captured_env_vars: Mutex::new(None), - event_callback: None, + written_files: Mutex::new(Vec::new()), + captured_timeout: Mutex::new(None), + captured_command: Mutex::new(None), + captured_commands: Mutex::new(Vec::new()), + captured_working_dirs: Mutex::new(Vec::new()), + captured_env_vars: Mutex::new(None), + event_callback: None, } } } @@ -211,12 +211,12 @@ impl Sandbox for MockSandbox { provider: "mock".into(), }); self.emit(SandboxEvent::Ready { - provider: "mock".into(), + provider: "mock".into(), duration_ms: 0, - name: None, - cpu: None, - memory: None, - url: None, + name: None, + cpu: None, + memory: None, + url: None, }); Ok(()) } @@ -226,7 +226,7 @@ impl Sandbox for MockSandbox { provider: "mock".into(), }); self.emit(SandboxEvent::CleanupCompleted { - provider: "mock".into(), + provider: "mock".into(), duration_ms: 0, }); Ok(()) @@ -316,10 +316,10 @@ impl Sandbox for MutableMockSandbox { _cancel_token: Option, ) -> Result { Ok(ExecResult { - stdout: String::new(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: String::new(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 0, }) } diff --git a/lib/crates/fabro-sandbox/src/worktree.rs b/lib/crates/fabro-sandbox/src/worktree.rs index da4d3c3e3..75e09b86a 100644 --- a/lib/crates/fabro-sandbox/src/worktree.rs +++ b/lib/crates/fabro-sandbox/src/worktree.rs @@ -26,9 +26,9 @@ pub type WorktreeEventCallback = Arc; /// Configuration for a `WorktreeSandbox`. pub struct WorktreeOptions { - pub branch_name: String, - pub base_sha: String, - pub worktree_path: String, + pub branch_name: String, + pub base_sha: String, + pub worktree_path: String, /// Skip branch creation and hard reset (for resume, where branch already /// exists). pub skip_branch_creation: bool, @@ -41,10 +41,10 @@ pub struct WorktreeOptions { /// `initialize()` and `cleanup()` do NOT call the inner sandbox's lifecycle /// methods. The inner sandbox's lifecycle is managed separately by the caller. pub struct WorktreeSandbox { - inner: Arc, - config: WorktreeOptions, + inner: Arc, + config: WorktreeOptions, event_callback: Option, - initialized: std::sync::atomic::AtomicBool, + initialized: std::sync::atomic::AtomicBool, } impl WorktreeSandbox { @@ -153,7 +153,7 @@ impl Sandbox for WorktreeSandbox { } self.emit(WorktreeEvent::BranchCreated { branch: self.config.branch_name.clone(), - sha: self.config.base_sha.clone(), + sha: self.config.base_sha.clone(), }); } @@ -178,7 +178,7 @@ impl Sandbox for WorktreeSandbox { )); } self.emit(WorktreeEvent::WorktreeAdded { - path: self.config.worktree_path.clone(), + path: self.config.worktree_path.clone(), branch: self.config.branch_name.clone(), }); @@ -367,18 +367,18 @@ mod tests { fn make_config(wt_path: &str) -> WorktreeOptions { WorktreeOptions { - branch_name: "fabro/run/test-branch".to_string(), - base_sha: "abc123def456".to_string(), - worktree_path: wt_path.to_string(), + branch_name: "fabro/run/test-branch".to_string(), + base_sha: "abc123def456".to_string(), + worktree_path: wt_path.to_string(), skip_branch_creation: false, } } fn make_config_skip(wt_path: &str) -> WorktreeOptions { WorktreeOptions { - branch_name: "fabro/run/test-branch".to_string(), - base_sha: "abc123def456".to_string(), - worktree_path: wt_path.to_string(), + branch_name: "fabro/run/test-branch".to_string(), + base_sha: "abc123def456".to_string(), + worktree_path: wt_path.to_string(), skip_branch_creation: true, } } @@ -442,9 +442,9 @@ mod tests { async fn initialize_uses_shell_quoted_values_in_commands() { let (inner, mock) = make_mock(); let config = WorktreeOptions { - branch_name: "fabro/run/my-branch".to_string(), - base_sha: "deadbeef".to_string(), - worktree_path: "/tmp/my worktree".to_string(), // path with space + branch_name: "fabro/run/my-branch".to_string(), + base_sha: "deadbeef".to_string(), + worktree_path: "/tmp/my worktree".to_string(), // path with space skip_branch_creation: false, }; let wt = WorktreeSandbox::new(inner, config); @@ -514,10 +514,10 @@ mod tests { async fn initialize_propagates_error_on_nonzero_exit() { let inner: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: String::new(), - stderr: "fatal: not a git repo".to_string(), - exit_code: 128, - timed_out: false, + stdout: String::new(), + stderr: "fatal: not a git repo".to_string(), + exit_code: 128, + timed_out: false, duration_ms: 5, }, ..MockSandbox::linux() @@ -662,9 +662,9 @@ mod tests { let inner: Arc = Arc::new(LocalSandbox::new(original.clone())); let config = WorktreeOptions { - branch_name: "test-branch".into(), - base_sha: "abc123".into(), - worktree_path: worktree.to_string_lossy().to_string(), + branch_name: "test-branch".into(), + base_sha: "abc123".into(), + worktree_path: worktree.to_string_lossy().to_string(), skip_branch_creation: false, }; let wt = WorktreeSandbox::new(inner, config); @@ -703,9 +703,9 @@ mod tests { let inner: Arc = Arc::new(LocalSandbox::new(original.clone())); let config = WorktreeOptions { - branch_name: "test-branch".into(), - base_sha: "abc123".into(), - worktree_path: worktree.to_string_lossy().to_string(), + branch_name: "test-branch".into(), + base_sha: "abc123".into(), + worktree_path: worktree.to_string_lossy().to_string(), skip_branch_creation: false, }; let wt = WorktreeSandbox::new(inner, config); @@ -737,9 +737,9 @@ mod tests { let inner: Arc = Arc::new(LocalSandbox::new(original.clone())); let config = WorktreeOptions { - branch_name: "test-branch".into(), - base_sha: "abc123".into(), - worktree_path: worktree.to_string_lossy().to_string(), + branch_name: "test-branch".into(), + base_sha: "abc123".into(), + worktree_path: worktree.to_string_lossy().to_string(), skip_branch_creation: false, }; let wt = WorktreeSandbox::new(inner, config); @@ -763,9 +763,9 @@ mod tests { fn accessors_return_config_values() { let (inner, _mock) = make_mock(); let config = WorktreeOptions { - branch_name: "my-branch".to_string(), - base_sha: "sha123".to_string(), - worktree_path: "/path/to/wt".to_string(), + branch_name: "my-branch".to_string(), + base_sha: "sha123".to_string(), + worktree_path: "/path/to/wt".to_string(), skip_branch_creation: false, }; let wt = WorktreeSandbox::new(inner, config); diff --git a/lib/crates/fabro-server/src/demo/mod.rs b/lib/crates/fabro-server/src/demo/mod.rs index 92e5d7ebe..7226cfbee 100644 --- a/lib/crates/fabro-server/src/demo/mod.rs +++ b/lib/crates/fabro-server/src/demo/mod.rs @@ -646,454 +646,445 @@ mod runs { pub(super) fn list_items() -> Vec { vec![ RunListItem { - id: "run-1".into(), - repository: RepositoryReference { + id: "run-1".into(), + repository: RepositoryReference { name: "api-server".into(), }, - title: "Add rate limiting to auth endpoints".into(), - workflow: WorkflowReference { + title: "Add rate limiting to auth endpoints".into(), + workflow: WorkflowReference { slug: "implement".into(), }, - status: BoardColumn::Working, + status: BoardColumn::Working, pull_request: None, - timings: Some(RunTimings { - elapsed_secs: 420.0, + timings: Some(RunTimings { + elapsed_secs: 420.0, elapsed_warning: Some(false), }), - sandbox: Some(RunSandbox { - id: "sb-a1b2c3d4".into(), - resources: Some(SandboxResources { - cpu: 4, - memory: 8, - }), + sandbox: Some(RunSandbox { + id: "sb-a1b2c3d4".into(), + resources: Some(SandboxResources { cpu: 4, memory: 8 }), }), - question: None, - created_at: ts("2026-03-06T14:30:00Z"), + question: None, + created_at: ts("2026-03-06T14:30:00Z"), }, RunListItem { - id: "run-2".into(), - repository: RepositoryReference { + id: "run-2".into(), + repository: RepositoryReference { name: "web-dashboard".into(), }, - title: "Migrate to React Router v7".into(), - workflow: WorkflowReference { + title: "Migrate to React Router v7".into(), + workflow: WorkflowReference { slug: "implement".into(), }, - status: BoardColumn::Working, + status: BoardColumn::Working, pull_request: None, - timings: Some(RunTimings { - elapsed_secs: 8100.0, + timings: Some(RunTimings { + elapsed_secs: 8100.0, elapsed_warning: Some(false), }), - sandbox: Some(RunSandbox { - id: "sb-e5f6g7h8".into(), - resources: Some(SandboxResources { - cpu: 8, - memory: 16, - }), + sandbox: Some(RunSandbox { + id: "sb-e5f6g7h8".into(), + resources: Some(SandboxResources { cpu: 8, memory: 16 }), }), - question: None, - created_at: ts("2026-03-06T12:00:00Z"), + question: None, + created_at: ts("2026-03-06T12:00:00Z"), }, RunListItem { - id: "run-3".into(), - repository: RepositoryReference { + id: "run-3".into(), + repository: RepositoryReference { name: "cli-tools".into(), }, - title: "Fix config parsing for nested values".into(), - workflow: WorkflowReference { + title: "Fix config parsing for nested values".into(), + workflow: WorkflowReference { slug: "fix_build".into(), }, - status: BoardColumn::Working, + status: BoardColumn::Working, pull_request: None, - timings: Some(RunTimings { - elapsed_secs: 2700.0, + timings: Some(RunTimings { + elapsed_secs: 2700.0, elapsed_warning: Some(false), }), - sandbox: Some(RunSandbox { - id: "sb-i9j0k1l2".into(), - resources: Some(SandboxResources { - cpu: 2, - memory: 4, - }), + sandbox: Some(RunSandbox { + id: "sb-i9j0k1l2".into(), + resources: Some(SandboxResources { cpu: 2, memory: 4 }), }), - question: None, - created_at: ts("2026-03-05T09:20:00Z"), + question: None, + created_at: ts("2026-03-05T09:20:00Z"), }, RunListItem { - id: "run-4".into(), - repository: RepositoryReference { + id: "run-4".into(), + repository: RepositoryReference { name: "api-server".into(), }, - title: "Update OpenAPI spec for v3".into(), - workflow: WorkflowReference { + title: "Update OpenAPI spec for v3".into(), + workflow: WorkflowReference { slug: "expand".into(), }, - status: BoardColumn::Pending, + status: BoardColumn::Pending, pull_request: Some(RunPullRequest { - number: 0, + number: 0, additions: Some(567), deletions: Some(234), - comments: Some(0), - checks: vec![], + comments: Some(0), + checks: vec![], }), - timings: Some(RunTimings { - elapsed_secs: 4320.0, + timings: Some(RunTimings { + elapsed_secs: 4320.0, elapsed_warning: Some(false), }), - sandbox: Some(RunSandbox { - id: "sb-q7r8s9t0".into(), + sandbox: Some(RunSandbox { + id: "sb-q7r8s9t0".into(), resources: None, }), - question: Some(RunQuestion { + question: Some(RunQuestion { text: "Accept or push for another round?".into(), }), - created_at: ts("2026-03-04T15:00:00Z"), + created_at: ts("2026-03-04T15:00:00Z"), }, RunListItem { - id: "run-5".into(), - repository: RepositoryReference { + id: "run-5".into(), + repository: RepositoryReference { name: "shared-types".into(), }, - title: "Add pipeline event types".into(), - workflow: WorkflowReference { + title: "Add pipeline event types".into(), + workflow: WorkflowReference { slug: "implement".into(), }, - status: BoardColumn::Pending, + status: BoardColumn::Pending, pull_request: Some(RunPullRequest { - number: 0, + number: 0, additions: Some(145), deletions: Some(23), - comments: Some(0), - checks: vec![], + comments: Some(0), + checks: vec![], }), - timings: Some(RunTimings { - elapsed_secs: 1680.0, + timings: Some(RunTimings { + elapsed_secs: 1680.0, elapsed_warning: Some(false), }), - sandbox: Some(RunSandbox { - id: "sb-u1v2w3x4".into(), + sandbox: Some(RunSandbox { + id: "sb-u1v2w3x4".into(), resources: None, }), - question: Some(RunQuestion { + question: Some(RunQuestion { text: "Proceed from investigation to fix?".into(), }), - created_at: ts("2026-03-04T10:00:00Z"), + created_at: ts("2026-03-04T10:00:00Z"), }, RunListItem { - id: "run-6".into(), - repository: RepositoryReference { + id: "run-6".into(), + repository: RepositoryReference { name: "web-dashboard".into(), }, - title: "Add dark mode toggle".into(), - workflow: WorkflowReference { + title: "Add dark mode toggle".into(), + workflow: WorkflowReference { slug: "implement".into(), }, - status: BoardColumn::Review, + status: BoardColumn::Review, pull_request: Some(RunPullRequest { - number: 889, + number: 889, additions: Some(234), deletions: Some(67), - comments: Some(4), - checks: vec![ + comments: Some(4), + checks: vec![ CheckRun { - name: "lint".into(), - status: CheckRunStatus::Success, + name: "lint".into(), + status: CheckRunStatus::Success, duration_secs: Some(23.0), }, CheckRun { - name: "typecheck".into(), - status: CheckRunStatus::Success, + name: "typecheck".into(), + status: CheckRunStatus::Success, duration_secs: Some(72.0), }, CheckRun { - name: "unit-tests".into(), - status: CheckRunStatus::Success, + name: "unit-tests".into(), + status: CheckRunStatus::Success, duration_secs: Some(154.0), }, CheckRun { - name: "integration-tests".into(), - status: CheckRunStatus::Failure, + name: "integration-tests".into(), + status: CheckRunStatus::Failure, duration_secs: Some(296.0), }, CheckRun { - name: "e2e / chrome".into(), - status: CheckRunStatus::Failure, + name: "e2e / chrome".into(), + status: CheckRunStatus::Failure, duration_secs: Some(182.0), }, CheckRun { - name: "build".into(), - status: CheckRunStatus::Success, + name: "build".into(), + status: CheckRunStatus::Success, duration_secs: Some(105.0), }, CheckRun { - name: "coverage".into(), - status: CheckRunStatus::Skipped, + name: "coverage".into(), + status: CheckRunStatus::Skipped, duration_secs: None, }, ], }), - timings: Some(RunTimings { - elapsed_secs: 2100.0, + timings: Some(RunTimings { + elapsed_secs: 2100.0, elapsed_warning: Some(false), }), - sandbox: Some(RunSandbox { - id: "sb-m3n4o5p6".into(), + sandbox: Some(RunSandbox { + id: "sb-m3n4o5p6".into(), resources: None, }), - question: None, - created_at: ts("2026-03-03T16:45:00Z"), + question: None, + created_at: ts("2026-03-03T16:45:00Z"), }, RunListItem { - id: "run-7".into(), - repository: RepositoryReference { + id: "run-7".into(), + repository: RepositoryReference { name: "infrastructure".into(), }, - title: "Terraform module for Redis cluster".into(), - workflow: WorkflowReference { + title: "Terraform module for Redis cluster".into(), + workflow: WorkflowReference { slug: "implement".into(), }, - status: BoardColumn::Review, + status: BoardColumn::Review, pull_request: Some(RunPullRequest { - number: 156, + number: 156, additions: Some(412), deletions: Some(0), - comments: Some(1), - checks: vec![ + comments: Some(1), + checks: vec![ CheckRun { - name: "lint".into(), - status: CheckRunStatus::Success, + name: "lint".into(), + status: CheckRunStatus::Success, duration_secs: Some(18.0), }, CheckRun { - name: "typecheck".into(), - status: CheckRunStatus::Success, + name: "typecheck".into(), + status: CheckRunStatus::Success, duration_secs: Some(56.0), }, CheckRun { - name: "unit-tests".into(), - status: CheckRunStatus::Pending, + name: "unit-tests".into(), + status: CheckRunStatus::Pending, duration_secs: None, }, CheckRun { - name: "integration-tests".into(), - status: CheckRunStatus::Queued, + name: "integration-tests".into(), + status: CheckRunStatus::Queued, duration_secs: None, }, CheckRun { - name: "build".into(), - status: CheckRunStatus::Pending, + name: "build".into(), + status: CheckRunStatus::Pending, duration_secs: None, }, ], }), - timings: Some(RunTimings { - elapsed_secs: 720.0, + timings: Some(RunTimings { + elapsed_secs: 720.0, elapsed_warning: Some(false), }), - sandbox: Some(RunSandbox { - id: "sb-y5z6a7b8".into(), + sandbox: Some(RunSandbox { + id: "sb-y5z6a7b8".into(), resources: None, }), - question: None, - created_at: ts("2026-03-03T11:00:00Z"), + question: None, + created_at: ts("2026-03-03T11:00:00Z"), }, RunListItem { - id: "run-8".into(), - repository: RepositoryReference { + id: "run-8".into(), + repository: RepositoryReference { name: "api-server".into(), }, - title: "Implement webhook retry logic".into(), - workflow: WorkflowReference { + title: "Implement webhook retry logic".into(), + workflow: WorkflowReference { slug: "implement".into(), }, - status: BoardColumn::Merge, + status: BoardColumn::Merge, pull_request: Some(RunPullRequest { - number: 1249, + number: 1249, additions: Some(189), deletions: Some(45), - comments: Some(7), - checks: vec![ + comments: Some(7), + checks: vec![ CheckRun { - name: "lint".into(), - status: CheckRunStatus::Success, + name: "lint".into(), + status: CheckRunStatus::Success, duration_secs: Some(21.0), }, CheckRun { - name: "typecheck".into(), - status: CheckRunStatus::Success, + name: "typecheck".into(), + status: CheckRunStatus::Success, duration_secs: Some(68.0), }, CheckRun { - name: "unit-tests".into(), - status: CheckRunStatus::Success, + name: "unit-tests".into(), + status: CheckRunStatus::Success, duration_secs: Some(192.0), }, CheckRun { - name: "integration-tests".into(), - status: CheckRunStatus::Success, + name: "integration-tests".into(), + status: CheckRunStatus::Success, duration_secs: Some(334.0), }, CheckRun { - name: "e2e / chrome".into(), - status: CheckRunStatus::Success, + name: "e2e / chrome".into(), + status: CheckRunStatus::Success, duration_secs: Some(262.0), }, CheckRun { - name: "e2e / firefox".into(), - status: CheckRunStatus::Success, + name: "e2e / firefox".into(), + status: CheckRunStatus::Success, duration_secs: Some(285.0), }, CheckRun { - name: "build".into(), - status: CheckRunStatus::Success, + name: "build".into(), + status: CheckRunStatus::Success, duration_secs: Some(121.0), }, CheckRun { - name: "deploy-preview".into(), - status: CheckRunStatus::Success, + name: "deploy-preview".into(), + status: CheckRunStatus::Success, duration_secs: Some(93.0), }, CheckRun { - name: "security-scan".into(), - status: CheckRunStatus::Skipped, + name: "security-scan".into(), + status: CheckRunStatus::Skipped, duration_secs: None, }, CheckRun { - name: "performance".into(), - status: CheckRunStatus::Success, + name: "performance".into(), + status: CheckRunStatus::Success, duration_secs: Some(138.0), }, CheckRun { - name: "bundle-size".into(), - status: CheckRunStatus::Success, + name: "bundle-size".into(), + status: CheckRunStatus::Success, duration_secs: Some(34.0), }, CheckRun { - name: "accessibility".into(), - status: CheckRunStatus::Success, + name: "accessibility".into(), + status: CheckRunStatus::Success, duration_secs: Some(72.0), }, ], }), - timings: Some(RunTimings { - elapsed_secs: 259200.0, + timings: Some(RunTimings { + elapsed_secs: 259200.0, elapsed_warning: Some(true), }), - sandbox: Some(RunSandbox { - id: "sb-c9d0e1f2".into(), + sandbox: Some(RunSandbox { + id: "sb-c9d0e1f2".into(), resources: None, }), - question: None, - created_at: ts("2026-02-28T14:00:00Z"), + question: None, + created_at: ts("2026-02-28T14:00:00Z"), }, RunListItem { - id: "run-9".into(), - repository: RepositoryReference { + id: "run-9".into(), + repository: RepositoryReference { name: "cli-tools".into(), }, - title: "Add --verbose flag to run command".into(), - workflow: WorkflowReference { + title: "Add --verbose flag to run command".into(), + workflow: WorkflowReference { slug: "expand".into(), }, - status: BoardColumn::Merge, + status: BoardColumn::Merge, pull_request: Some(RunPullRequest { - number: 430, + number: 430, additions: Some(56), deletions: Some(12), - comments: Some(2), - checks: vec![ + comments: Some(2), + checks: vec![ CheckRun { - name: "lint".into(), - status: CheckRunStatus::Success, + name: "lint".into(), + status: CheckRunStatus::Success, duration_secs: Some(15.0), }, CheckRun { - name: "typecheck".into(), - status: CheckRunStatus::Success, + name: "typecheck".into(), + status: CheckRunStatus::Success, duration_secs: Some(48.0), }, CheckRun { - name: "unit-tests".into(), - status: CheckRunStatus::Success, + name: "unit-tests".into(), + status: CheckRunStatus::Success, duration_secs: Some(116.0), }, CheckRun { - name: "build".into(), - status: CheckRunStatus::Success, + name: "build".into(), + status: CheckRunStatus::Success, duration_secs: Some(82.0), }, CheckRun { - name: "coverage".into(), - status: CheckRunStatus::Success, + name: "coverage".into(), + status: CheckRunStatus::Success, duration_secs: Some(124.0), }, CheckRun { - name: "bundle-size".into(), - status: CheckRunStatus::Skipped, + name: "bundle-size".into(), + status: CheckRunStatus::Skipped, duration_secs: None, }, ], }), - timings: Some(RunTimings { - elapsed_secs: 3900.0, + timings: Some(RunTimings { + elapsed_secs: 3900.0, elapsed_warning: Some(false), }), - sandbox: Some(RunSandbox { - id: "sb-g3h4i5j6".into(), + sandbox: Some(RunSandbox { + id: "sb-g3h4i5j6".into(), resources: None, }), - question: None, - created_at: ts("2026-02-27T09:00:00Z"), + question: None, + created_at: ts("2026-02-27T09:00:00Z"), }, RunListItem { - id: "run-10".into(), - repository: RepositoryReference { + id: "run-10".into(), + repository: RepositoryReference { name: "shared-types".into(), }, - title: "Export utility type helpers".into(), - workflow: WorkflowReference { + title: "Export utility type helpers".into(), + workflow: WorkflowReference { slug: "sync_drift".into(), }, - status: BoardColumn::Merge, + status: BoardColumn::Merge, pull_request: Some(RunPullRequest { - number: 76, + number: 76, additions: Some(34), deletions: Some(8), - comments: Some(0), - checks: vec![ + comments: Some(0), + checks: vec![ CheckRun { - name: "lint".into(), - status: CheckRunStatus::Success, + name: "lint".into(), + status: CheckRunStatus::Success, duration_secs: Some(12.0), }, CheckRun { - name: "typecheck".into(), - status: CheckRunStatus::Success, + name: "typecheck".into(), + status: CheckRunStatus::Success, duration_secs: Some(34.0), }, CheckRun { - name: "unit-tests".into(), - status: CheckRunStatus::Success, + name: "unit-tests".into(), + status: CheckRunStatus::Success, duration_secs: Some(75.0), }, CheckRun { - name: "build".into(), - status: CheckRunStatus::Success, + name: "build".into(), + status: CheckRunStatus::Success, duration_secs: Some(58.0), }, ], }), - timings: Some(RunTimings { - elapsed_secs: 2880.0, + timings: Some(RunTimings { + elapsed_secs: 2880.0, elapsed_warning: Some(false), }), - sandbox: Some(RunSandbox { - id: "sb-k7l8m9n0".into(), + sandbox: Some(RunSandbox { + id: "sb-k7l8m9n0".into(), resources: None, }), - question: None, - created_at: ts("2026-02-26T08:00:00Z"), + question: None, + created_at: ts("2026-02-26T08:00:00Z"), }, ] } @@ -1101,32 +1092,32 @@ mod runs { pub(super) fn stages() -> Vec { vec![ RunStage { - id: "detect-drift".into(), - name: "Detect Drift".into(), - status: StageStatus::Completed, + id: "detect-drift".into(), + name: "Detect Drift".into(), + status: StageStatus::Completed, duration_secs: Some(72.0), - dot_id: Some("detect".into()), + dot_id: Some("detect".into()), }, RunStage { - id: "propose-changes".into(), - name: "Propose Changes".into(), - status: StageStatus::Completed, + id: "propose-changes".into(), + name: "Propose Changes".into(), + status: StageStatus::Completed, duration_secs: Some(154.0), - dot_id: Some("propose".into()), + dot_id: Some("propose".into()), }, RunStage { - id: "review-changes".into(), - name: "Review Changes".into(), - status: StageStatus::Completed, + id: "review-changes".into(), + name: "Review Changes".into(), + status: StageStatus::Completed, duration_secs: Some(45.0), - dot_id: Some("review".into()), + dot_id: Some("review".into()), }, RunStage { - id: "apply-changes".into(), - name: "Apply Changes".into(), - status: StageStatus::Running, + id: "apply-changes".into(), + name: "Apply Changes".into(), + status: StageStatus::Running, duration_secs: Some(118.0), - dot_id: Some("apply".into()), + dot_id: Some("apply".into()), }, ] } @@ -1148,139 +1139,139 @@ mod runs { pub(super) fn billing() -> RunBilling { RunBilling { - stages: vec![ + stages: vec![ RunBillingStage { - stage: BillingStageRef { - id: "detect-drift".into(), + stage: BillingStageRef { + id: "detect-drift".into(), name: "Detect Drift".into(), }, - model: ModelReference { + model: ModelReference { id: "Opus 4.6".into(), }, - billing: BilledTokenCounts { - cache_read_tokens: None, + billing: BilledTokenCounts { + cache_read_tokens: None, cache_write_tokens: None, - input_tokens: 12480, - output_tokens: 3210, - reasoning_tokens: None, - total_tokens: 15690, - total_usd_micros: Some(480_000), + input_tokens: 12480, + output_tokens: 3210, + reasoning_tokens: None, + total_tokens: 15690, + total_usd_micros: Some(480_000), }, runtime_secs: 72.0, }, RunBillingStage { - stage: BillingStageRef { - id: "propose-changes".into(), + stage: BillingStageRef { + id: "propose-changes".into(), name: "Propose Changes".into(), }, - model: ModelReference { + model: ModelReference { id: "Gemini 3.1".into(), }, - billing: BilledTokenCounts { - cache_read_tokens: None, + billing: BilledTokenCounts { + cache_read_tokens: None, cache_write_tokens: None, - input_tokens: 28640, - output_tokens: 8750, - reasoning_tokens: None, - total_tokens: 37390, - total_usd_micros: Some(720_000), + input_tokens: 28640, + output_tokens: 8750, + reasoning_tokens: None, + total_tokens: 37390, + total_usd_micros: Some(720_000), }, runtime_secs: 154.0, }, RunBillingStage { - stage: BillingStageRef { - id: "review-changes".into(), + stage: BillingStageRef { + id: "review-changes".into(), name: "Review Changes".into(), }, - model: ModelReference { + model: ModelReference { id: "Codex 5.3".into(), }, - billing: BilledTokenCounts { - cache_read_tokens: None, + billing: BilledTokenCounts { + cache_read_tokens: None, cache_write_tokens: None, - input_tokens: 9120, - output_tokens: 2640, - reasoning_tokens: None, - total_tokens: 11760, - total_usd_micros: Some(190_000), + input_tokens: 9120, + output_tokens: 2640, + reasoning_tokens: None, + total_tokens: 11760, + total_usd_micros: Some(190_000), }, runtime_secs: 45.0, }, RunBillingStage { - stage: BillingStageRef { - id: "apply-changes".into(), + stage: BillingStageRef { + id: "apply-changes".into(), name: "Apply Changes".into(), }, - model: ModelReference { + model: ModelReference { id: "Opus 4.6".into(), }, - billing: BilledTokenCounts { - cache_read_tokens: None, + billing: BilledTokenCounts { + cache_read_tokens: None, cache_write_tokens: None, - input_tokens: 21300, - output_tokens: 6480, - reasoning_tokens: None, - total_tokens: 27780, - total_usd_micros: Some(870_000), + input_tokens: 21300, + output_tokens: 6480, + reasoning_tokens: None, + total_tokens: 27780, + total_usd_micros: Some(870_000), }, runtime_secs: 118.0, }, ], - totals: RunBillingTotals { - cache_read_tokens: None, + totals: RunBillingTotals { + cache_read_tokens: None, cache_write_tokens: None, - runtime_secs: 389.0, - input_tokens: 71540, - output_tokens: 21080, - reasoning_tokens: None, - total_tokens: 92620, - total_usd_micros: Some(2_260_000), + runtime_secs: 389.0, + input_tokens: 71540, + output_tokens: 21080, + reasoning_tokens: None, + total_tokens: 92620, + total_usd_micros: Some(2_260_000), }, by_model: vec![ BillingByModel { billing: BilledTokenCounts { - cache_read_tokens: None, + cache_read_tokens: None, cache_write_tokens: None, - input_tokens: 33780, - output_tokens: 9690, - reasoning_tokens: None, - total_tokens: 43470, - total_usd_micros: Some(1_350_000), + input_tokens: 33780, + output_tokens: 9690, + reasoning_tokens: None, + total_tokens: 43470, + total_usd_micros: Some(1_350_000), }, - model: ModelReference { + model: ModelReference { id: "Opus 4.6".into(), }, - stages: 2, + stages: 2, }, BillingByModel { billing: BilledTokenCounts { - cache_read_tokens: None, + cache_read_tokens: None, cache_write_tokens: None, - input_tokens: 28640, - output_tokens: 8750, - reasoning_tokens: None, - total_tokens: 37390, - total_usd_micros: Some(720_000), + input_tokens: 28640, + output_tokens: 8750, + reasoning_tokens: None, + total_tokens: 37390, + total_usd_micros: Some(720_000), }, - model: ModelReference { + model: ModelReference { id: "Gemini 3.1".into(), }, - stages: 1, + stages: 1, }, BillingByModel { billing: BilledTokenCounts { - cache_read_tokens: None, + cache_read_tokens: None, cache_write_tokens: None, - input_tokens: 9120, - output_tokens: 2640, - reasoning_tokens: None, - total_tokens: 11760, - total_usd_micros: Some(190_000), + input_tokens: 9120, + output_tokens: 2640, + reasoning_tokens: None, + total_tokens: 11760, + total_usd_micros: Some(190_000), }, - model: ModelReference { + model: ModelReference { id: "Codex 5.3".into(), }, - stages: 1, + stages: 1, }, ], } @@ -1289,40 +1280,40 @@ mod runs { pub(super) fn questions() -> Vec { vec![ ApiQuestion { - id: "q-001".into(), - text: "Should we proceed with the proposed changes?".into(), - stage: "review".into(), - question_type: QuestionType::YesNo, - options: vec![ + id: "q-001".into(), + text: "Should we proceed with the proposed changes?".into(), + stage: "review".into(), + question_type: QuestionType::YesNo, + options: vec![ ApiQuestionOption { - key: "yes".into(), + key: "yes".into(), label: "Yes".into(), }, ApiQuestionOption { - key: "no".into(), + key: "no".into(), label: "No".into(), }, ], - allow_freeform: false, + allow_freeform: false, timeout_seconds: None, context_display: None, }, ApiQuestion { - id: "q-002".into(), - text: "Which approach do you prefer for the migration?".into(), - stage: "migration".into(), - question_type: QuestionType::MultipleChoice, - options: vec![ + id: "q-002".into(), + text: "Which approach do you prefer for the migration?".into(), + stage: "migration".into(), + question_type: QuestionType::MultipleChoice, + options: vec![ ApiQuestionOption { - key: "incremental".into(), + key: "incremental".into(), label: "Incremental migration".into(), }, ApiQuestionOption { - key: "big_bang".into(), + key: "big_bang".into(), label: "Big-bang rewrite".into(), }, ], - allow_freeform: true, + allow_freeform: true, timeout_seconds: None, context_display: None, }, @@ -1371,62 +1362,62 @@ mod billing { pub(super) fn aggregate() -> AggregateBilling { AggregateBilling { - totals: AggregateBillingTotals { - cache_read_tokens: None, + totals: AggregateBillingTotals { + cache_read_tokens: None, cache_write_tokens: None, - runs: 9, - input_tokens: 643_860, - output_tokens: 189_720, - reasoning_tokens: None, - runtime_secs: 3_501.0, - total_tokens: 833_580, - total_usd_micros: Some(20_340_000), + runs: 9, + input_tokens: 643_860, + output_tokens: 189_720, + reasoning_tokens: None, + runtime_secs: 3_501.0, + total_tokens: 833_580, + total_usd_micros: Some(20_340_000), }, by_model: vec![ BillingByModel { billing: BilledTokenCounts { - cache_read_tokens: None, + cache_read_tokens: None, cache_write_tokens: None, - input_tokens: 304_020, - output_tokens: 87_210, - reasoning_tokens: None, - total_tokens: 391_230, - total_usd_micros: Some(12_150_000), + input_tokens: 304_020, + output_tokens: 87_210, + reasoning_tokens: None, + total_tokens: 391_230, + total_usd_micros: Some(12_150_000), }, - model: ModelReference { + model: ModelReference { id: "Opus 4.6".into(), }, - stages: 18, + stages: 18, }, BillingByModel { billing: BilledTokenCounts { - cache_read_tokens: None, + cache_read_tokens: None, cache_write_tokens: None, - input_tokens: 257_760, - output_tokens: 78_750, - reasoning_tokens: None, - total_tokens: 336_510, - total_usd_micros: Some(6_480_000), + input_tokens: 257_760, + output_tokens: 78_750, + reasoning_tokens: None, + total_tokens: 336_510, + total_usd_micros: Some(6_480_000), }, - model: ModelReference { + model: ModelReference { id: "Gemini 3.1".into(), }, - stages: 9, + stages: 9, }, BillingByModel { billing: BilledTokenCounts { - cache_read_tokens: None, + cache_read_tokens: None, cache_write_tokens: None, - input_tokens: 82_080, - output_tokens: 23_760, - reasoning_tokens: None, - total_tokens: 105_840, - total_usd_micros: Some(1_710_000), + input_tokens: 82_080, + output_tokens: 23_760, + reasoning_tokens: None, + total_tokens: 105_840, + total_usd_micros: Some(1_710_000), }, - model: ModelReference { + model: ModelReference { id: "Codex 5.3".into(), }, - stages: 9, + stages: 9, }, ], } @@ -1449,26 +1440,25 @@ mod insights { pub(super) fn history() -> Vec { vec![ HistoryEntry { - id: "h1".into(), - sql: "SELECT workflow_name, COUNT(*) FROM runs GROUP BY 1".into(), + id: "h1".into(), + sql: "SELECT workflow_name, COUNT(*) FROM runs GROUP BY 1".into(), timestamp: ts("2025-09-15T13:58:00Z"), - elapsed: 0.342, + elapsed: 0.342, row_count: 6, }, HistoryEntry { - id: "h2".into(), - sql: "SELECT * FROM runs WHERE status = 'failed' LIMIT 100".into(), + id: "h2".into(), + sql: "SELECT * FROM runs WHERE status = 'failed' LIMIT 100".into(), timestamp: ts("2025-09-15T13:52:00Z"), - elapsed: 0.127, + elapsed: 0.127, row_count: 23, }, HistoryEntry { - id: "h3".into(), - sql: - "SELECT date_trunc('day', created_at) as d, COUNT(*) FROM runs GROUP BY 1" - .into(), + id: "h3".into(), + sql: "SELECT date_trunc('day', created_at) as d, COUNT(*) FROM runs GROUP BY 1" + .into(), timestamp: ts("2025-09-15T13:45:00Z"), - elapsed: 0.531, + elapsed: 0.531, row_count: 30, }, ] diff --git a/lib/crates/fabro-server/src/diagnostics.rs b/lib/crates/fabro-server/src/diagnostics.rs index 56e10cb5c..c9f81a4e3 100644 --- a/lib/crates/fabro-server/src/diagnostics.rs +++ b/lib/crates/fabro-server/src/diagnostics.rs @@ -20,7 +20,7 @@ use crate::server::AppState; #[derive(Debug, Serialize)] pub struct DiagnosticsReport { - pub version: String, + pub version: String, pub sections: Vec, } @@ -162,18 +162,18 @@ pub async fn run_all(state: &AppState) -> DiagnosticsReport { let crypto = check_crypto(state); DiagnosticsReport { - version: FABRO_VERSION.to_string(), + version: FABRO_VERSION.to_string(), sections: vec![ CheckSection { - title: "Credentials".to_string(), + title: "Credentials".to_string(), checks: vec![llm, github, sandbox, brave], }, CheckSection { - title: "System".to_string(), + title: "System".to_string(), checks: vec![check_dot()], }, CheckSection { - title: "Configuration".to_string(), + title: "Configuration".to_string(), checks: vec![crypto], }, ], @@ -194,10 +194,10 @@ async fn check_llm_providers(state: &AppState) -> CheckResult { if configured.is_empty() { return CheckResult { - name: "LLM Providers".to_string(), - status: CheckStatus::Error, - summary: "none configured".to_string(), - details: Vec::new(), + name: "LLM Providers".to_string(), + status: CheckStatus::Error, + summary: "none configured".to_string(), + details: Vec::new(), remediation: Some("Set at least one provider API key".to_string()), }; } @@ -206,10 +206,10 @@ async fn check_llm_providers(state: &AppState) -> CheckResult { Ok(client) => client, Err(err) => { return CheckResult { - name: "LLM Providers".to_string(), - status: CheckStatus::Error, - summary: "failed to initialize".to_string(), - details: vec![CheckDetail::new(err)], + name: "LLM Providers".to_string(), + status: CheckStatus::Error, + summary: "failed to initialize".to_string(), + details: vec![CheckDetail::new(err)], remediation: Some("Check configured provider credentials".to_string()), }; } @@ -266,19 +266,19 @@ fn probe_model(provider: Provider) -> String { async fn probe_llm_provider(client: &LlmClient, provider: Provider) -> Result<(), String> { let request = Request { - model: probe_model(provider), - messages: vec![Message::user("hi")], - provider: Some(provider.as_str().to_string()), - tools: None, - tool_choice: None, - response_format: None, - temperature: None, - top_p: None, - max_tokens: Some(16), - stop_sequences: None, + model: probe_model(provider), + messages: vec![Message::user("hi")], + provider: Some(provider.as_str().to_string()), + tools: None, + tool_choice: None, + response_format: None, + temperature: None, + top_p: None, + max_tokens: Some(16), + stop_sequences: None, reasoning_effort: None, - speed: None, - metadata: None, + speed: None, + metadata: None, provider_options: None, }; client @@ -314,29 +314,29 @@ async fn check_github_app(state: &AppState) -> CheckResult { && !webhook_secret { return CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Warning, - summary: "not configured".to_string(), - details: Vec::new(), + name: "GitHub App".to_string(), + status: CheckStatus::Warning, + summary: "not configured".to_string(), + details: Vec::new(), remediation: Some("Configure GitHub App settings and secrets".to_string()), }; } let Some(app_id) = app_id else { return CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "missing app_id".to_string(), - details: Vec::new(), + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "missing app_id".to_string(), + details: Vec::new(), remediation: Some("Set git.app_id in settings.toml".to_string()), }; }; let Some(private_key_raw) = private_key_raw else { return CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "missing private key".to_string(), - details: Vec::new(), + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "missing private key".to_string(), + details: Vec::new(), remediation: Some("Set GITHUB_APP_PRIVATE_KEY".to_string()), }; }; @@ -345,10 +345,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { Ok(value) => value, Err(err) => { return CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "private key invalid".to_string(), - details: vec![CheckDetail::new(err.clone())], + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "private key invalid".to_string(), + details: vec![CheckDetail::new(err.clone())], remediation: Some(err), }; } @@ -358,10 +358,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { Ok(jwt) => jwt, Err(err) => { return CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "JWT signing failed".to_string(), - details: vec![CheckDetail::new(err.clone())], + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "JWT signing failed".to_string(), + details: vec![CheckDetail::new(err.clone())], remediation: Some(err), }; } @@ -375,24 +375,24 @@ async fn check_github_app(state: &AppState) -> CheckResult { .await; match auth_result { Ok(Ok(_app)) => CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Pass, - summary: slug.unwrap_or_else(|| "configured".to_string()), - details: Vec::new(), + name: "GitHub App".to_string(), + status: CheckStatus::Pass, + summary: slug.unwrap_or_else(|| "configured".to_string()), + details: Vec::new(), remediation: None, }, Ok(Err(err)) => CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "connectivity error".to_string(), - details: vec![CheckDetail::new(err.clone())], + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "connectivity error".to_string(), + details: vec![CheckDetail::new(err.clone())], remediation: Some(err), }, Err(_) => CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "timeout".to_string(), - details: vec![CheckDetail::new("GitHub probe timed out".to_string())], + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "timeout".to_string(), + details: vec![CheckDetail::new("GitHub probe timed out".to_string())], remediation: Some("Check GitHub connectivity and credentials".to_string()), }, } @@ -401,18 +401,18 @@ async fn check_github_app(state: &AppState) -> CheckResult { fn check_sandbox(state: &AppState) -> CheckResult { if state.secret_or_env("DAYTONA_API_KEY").is_some() { CheckResult { - name: "Sandbox".to_string(), - status: CheckStatus::Pass, - summary: "Daytona configured".to_string(), - details: Vec::new(), + name: "Sandbox".to_string(), + status: CheckStatus::Pass, + summary: "Daytona configured".to_string(), + details: Vec::new(), remediation: None, } } else { CheckResult { - name: "Sandbox".to_string(), - status: CheckStatus::Warning, - summary: "not configured".to_string(), - details: Vec::new(), + name: "Sandbox".to_string(), + status: CheckStatus::Warning, + summary: "not configured".to_string(), + details: Vec::new(), remediation: Some("Set DAYTONA_API_KEY to enable cloud sandbox execution".to_string()), } } @@ -421,10 +421,10 @@ fn check_sandbox(state: &AppState) -> CheckResult { async fn check_brave_search(state: &AppState) -> CheckResult { let Some(api_key) = state.secret_or_env("BRAVE_SEARCH_API_KEY") else { return CheckResult { - name: "Brave Search".to_string(), - status: CheckStatus::Warning, - summary: "not configured".to_string(), - details: Vec::new(), + name: "Brave Search".to_string(), + status: CheckStatus::Warning, + summary: "not configured".to_string(), + details: Vec::new(), remediation: Some("Set BRAVE_SEARCH_API_KEY to enable web search".to_string()), }; }; @@ -441,31 +441,31 @@ async fn check_brave_search(state: &AppState) -> CheckResult { match probe { Ok(Ok(response)) if response.status().is_success() => CheckResult { - name: "Brave Search".to_string(), - status: CheckStatus::Pass, - summary: "configured and reachable".to_string(), - details: Vec::new(), + name: "Brave Search".to_string(), + status: CheckStatus::Pass, + summary: "configured and reachable".to_string(), + details: Vec::new(), remediation: None, }, Ok(Ok(response)) => CheckResult { - name: "Brave Search".to_string(), - status: CheckStatus::Warning, - summary: format!("HTTP {}", response.status()), - details: Vec::new(), + name: "Brave Search".to_string(), + status: CheckStatus::Warning, + summary: format!("HTTP {}", response.status()), + details: Vec::new(), remediation: Some("Check BRAVE_SEARCH_API_KEY and network connectivity".to_string()), }, Ok(Err(err)) => CheckResult { - name: "Brave Search".to_string(), - status: CheckStatus::Warning, - summary: "connectivity error".to_string(), - details: vec![CheckDetail::new(err.clone())], + name: "Brave Search".to_string(), + status: CheckStatus::Warning, + summary: "connectivity error".to_string(), + details: vec![CheckDetail::new(err.clone())], remediation: Some(err), }, Err(_) => CheckResult { - name: "Brave Search".to_string(), - status: CheckStatus::Warning, - summary: "timeout".to_string(), - details: vec![CheckDetail::new("Brave Search probe timed out".to_string())], + name: "Brave Search".to_string(), + status: CheckStatus::Warning, + summary: "timeout".to_string(), + details: vec![CheckDetail::new("Brave Search probe timed out".to_string())], remediation: Some("Check BRAVE_SEARCH_API_KEY and network connectivity".to_string()), }, } @@ -493,10 +493,10 @@ fn check_crypto(state: &AppState) -> CheckResult { if !has_jwt && !has_mtls { return CheckResult { - name: "Crypto".to_string(), - status: CheckStatus::Warning, - summary: "no authentication configured".to_string(), - details: Vec::new(), + name: "Crypto".to_string(), + status: CheckStatus::Warning, + summary: "no authentication configured".to_string(), + details: Vec::new(), remediation: Some( "Configure strategies under [server.auth.api.jwt] or [server.auth.api.mtls]" .to_string(), diff --git a/lib/crates/fabro-server/src/error.rs b/lib/crates/fabro-server/src/error.rs index dd855713b..022c4be5f 100644 --- a/lib/crates/fabro-server/src/error.rs +++ b/lib/crates/fabro-server/src/error.rs @@ -55,7 +55,7 @@ pub type Result = std::result::Result; #[derive(Serialize)] struct ErrorEntry { status: String, - title: String, + title: String, detail: String, } diff --git a/lib/crates/fabro-server/src/github_webhooks.rs b/lib/crates/fabro-server/src/github_webhooks.rs index 698abac4c..bb6148d92 100644 --- a/lib/crates/fabro-server/src/github_webhooks.rs +++ b/lib/crates/fabro-server/src/github_webhooks.rs @@ -103,7 +103,7 @@ fn parse_event_metadata(body: &[u8]) -> (String, String) { /// A running webhook listener that can be shut down. pub struct WebhookListener { - port: u16, + port: u16, shutdown_tx: oneshot::Sender<()>, } diff --git a/lib/crates/fabro-server/src/jwt_auth.rs b/lib/crates/fabro-server/src/jwt_auth.rs index 2d91a6358..45decf9c6 100644 --- a/lib/crates/fabro-server/src/jwt_auth.rs +++ b/lib/crates/fabro-server/src/jwt_auth.rs @@ -39,8 +39,8 @@ struct Claims { #[derive(Clone, Debug)] pub enum AuthStrategy { Jwt { - key: Arc, - validation: Arc, + key: Arc, + validation: Arc, allowed_usernames: Vec, }, Cookie, @@ -99,9 +99,9 @@ pub fn resolve_auth_mode(settings: &ResolvedServerSettings) -> Result /// Describes which API auth strategies are enabled in resolved server settings. struct ResolvedAuthStrategies { - jwt_enabled: bool, - mtls_enabled: bool, - tls_present: bool, + jwt_enabled: bool, + mtls_enabled: bool, + tls_present: bool, allowed_usernames: Vec, } @@ -177,8 +177,8 @@ where ) })?; strategies.push(AuthStrategy::Jwt { - key: Arc::new(key), - validation: Arc::new(jwt_validation()), + key: Arc::new(key), + validation: Arc::new(jwt_validation()), allowed_usernames: allowed_usernames.clone(), }); } @@ -372,7 +372,7 @@ impl FromRequestParts for AuthenticatedService { /// Axum extractor that authenticates and extracts the request subject. pub struct AuthenticatedSubject { - pub login: Option, + pub login: Option, pub auth_method: RunAuthMethod, } @@ -388,7 +388,7 @@ impl FromRequestParts for AuthenticatedSubject { let strategies = match auth_mode { AuthMode::Disabled => { return Ok(Self { - login: None, + login: None, auth_method: RunAuthMethod::Disabled, }); } @@ -406,7 +406,7 @@ impl FromRequestParts for AuthenticatedSubject { AuthStrategy::Cookie => { if let Some(session) = parts.extensions.get::() { return Ok(Self { - login: Some(session.login.clone()), + login: Some(session.login.clone()), auth_method: RunAuthMethod::Cookie, }); } @@ -420,7 +420,7 @@ impl FromRequestParts for AuthenticatedSubject { if try_jwt(parts, key, validation, allowed_usernames).is_ok() { if let Some(login) = extract_jwt_login(parts, key, validation) { return Ok(Self { - login: Some(login), + login: Some(login), auth_method: RunAuthMethod::Jwt, }); } @@ -431,7 +431,7 @@ impl FromRequestParts for AuthenticatedSubject { if try_mtls(parts).is_ok() { if let Some(login) = extract_mtls_cn(parts) { return Ok(Self { - login: Some(login), + login: Some(login), auth_method: RunAuthMethod::Mtls, }); } @@ -670,8 +670,8 @@ enabled = true fn jwt_mode(decoding: DecodingKey, allowed_usernames: Vec<&str>) -> AuthMode { AuthMode::Strategies(vec![AuthStrategy::Jwt { - key: Arc::new(decoding), - validation: Arc::new(jwt_validation()), + key: Arc::new(decoding), + validation: Arc::new(jwt_validation()), allowed_usernames: allowed_usernames.into_iter().map(String::from).collect(), }]) } @@ -1003,13 +1003,13 @@ enabled = true .body(Body::empty()) .unwrap(); req.extensions_mut().insert(SessionCookie { - login: "brynary".to_string(), - name: "Brynary".to_string(), - email: "b@example.com".to_string(), + login: "brynary".to_string(), + name: "Brynary".to_string(), + email: "b@example.com".to_string(), avatar_url: "https://example.com/avatar.png".to_string(), - user_url: "https://github.com/brynary".to_string(), - github_id: 1, - exp: 9_999_999_999, + user_url: "https://github.com/brynary".to_string(), + github_id: 1, + exp: 9_999_999_999, }); let response = app.oneshot(req).await.unwrap(); @@ -1094,8 +1094,8 @@ enabled = true let (_, decoding) = generate_test_keypair(); let mode = AuthMode::Strategies(vec![ AuthStrategy::Jwt { - key: Arc::new(decoding), - validation: Arc::new(jwt_validation()), + key: Arc::new(decoding), + validation: Arc::new(jwt_validation()), allowed_usernames: vec!["brynary".to_string()], }, AuthStrategy::Mtls, @@ -1112,11 +1112,14 @@ enabled = true #[tokio::test] async fn mtls_and_jwt_falls_back_to_jwt() { let (encoding, decoding) = generate_test_keypair(); - let mode = AuthMode::Strategies(vec![AuthStrategy::Mtls, AuthStrategy::Jwt { - key: Arc::new(decoding), - validation: Arc::new(jwt_validation()), - allowed_usernames: vec!["brynary".to_string()], - }]); + let mode = AuthMode::Strategies(vec![ + AuthStrategy::Mtls, + AuthStrategy::Jwt { + key: Arc::new(decoding), + validation: Arc::new(jwt_validation()), + allowed_usernames: vec!["brynary".to_string()], + }, + ]); let app = test_router(mode); let token = sign_token( diff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/crates/fabro-server/src/run_manifest.rs index f04c13e1b..47c97d976 100644 --- a/lib/crates/fabro-server/src/run_manifest.rs +++ b/lib/crates/fabro-server/src/run_manifest.rs @@ -29,7 +29,7 @@ use fabro_types::settings::run::{ use fabro_types::settings::{ServerSettings, SettingsLayer}; use fabro_util::check_report::{CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus}; use fabro_validate::Severity; -use fabro_workflow::error::FabroError; +use fabro_workflow::Error as WorkflowError; use fabro_workflow::operations::{CreateRunInput, ValidateInput, WorkflowInput, validate}; use fabro_workflow::pipeline::Validated; use fabro_workflow::run_materialization::materialize_run; @@ -39,14 +39,14 @@ use crate::server::AppState; #[derive(Clone)] pub(crate) struct PreparedManifest { - pub cwd: PathBuf, - pub git: Option, - pub root_source: String, - pub run_id: Option, - pub settings: SettingsLayer, - pub target_path: PathBuf, - pub workflow_bundle: WorkflowBundle, - pub workflow_input: BundledWorkflow, + pub cwd: PathBuf, + pub git: Option, + pub root_source: String, + pub run_id: Option, + pub settings: SettingsLayer, + pub target_path: PathBuf, + pub workflow_bundle: WorkflowBundle, + pub workflow_input: BundledWorkflow, pub working_directory: PathBuf, } @@ -118,11 +118,11 @@ pub(crate) fn prepare_manifest_with_mode( pub(crate) fn validate_prepared_manifest( prepared: &PreparedManifest, -) -> Result { +) -> Result { validate(ValidateInput { - workflow: WorkflowInput::Bundled(prepared.workflow_input.clone()), - settings: prepared.settings.clone(), - cwd: prepared.cwd.clone(), + workflow: WorkflowInput::Bundled(prepared.workflow_input.clone()), + settings: prepared.settings.clone(), + cwd: prepared.cwd.clone(), custom_transforms: Vec::new(), }) } @@ -178,11 +178,14 @@ fn workflow_bundle_from_manifest( .iter() .map(|(key, entry)| (PathBuf::from(key), entry.content.clone())) .collect::>(); - Ok::<_, anyhow::Error>((PathBuf::from(path), BundledWorkflow { - logical_path: PathBuf::from(path), - source: workflow.source.clone(), - files, - })) + Ok::<_, anyhow::Error>(( + PathBuf::from(path), + BundledWorkflow { + logical_path: PathBuf::from(path), + source: workflow.source.clone(), + files, + }, + )) }) .collect::>>()?; Ok(WorkflowBundle::new(workflows)) @@ -217,8 +220,8 @@ fn manifest_args_layer(args: Option<&types::ManifestArgs>) -> SettingsLayer { }; let model = (args.model.is_some() || args.provider.is_some()).then(|| RunModelLayer { - provider: args.provider.as_deref().map(InterpString::parse), - name: args.model.as_deref().map(InterpString::parse), + provider: args.provider.as_deref().map(InterpString::parse), + name: args.model.as_deref().map(InterpString::parse), fallbacks: Vec::new(), }); let sandbox = @@ -231,7 +234,7 @@ fn manifest_args_layer(args: Option<&types::ManifestArgs>) -> SettingsLayer { let execution_has_any = args.dry_run.is_some() || args.auto_approve.is_some() || args.no_retro.is_some(); let execution = execution_has_any.then(|| RunExecutionLayer { - mode: args + mode: args .dry_run .map(|d| if d { RunMode::DryRun } else { RunMode::Normal }), approval: args.auto_approve.map(|a| { @@ -241,7 +244,7 @@ fn manifest_args_layer(args: Option<&types::ManifestArgs>) -> SettingsLayer { ApprovalMode::Prompt } }), - retros: args.no_retro.map(|nr| !nr), + retros: args.no_retro.map(|nr| !nr), }); let run_has_any = @@ -369,10 +372,10 @@ async fn build_preflight_report( }, ); checks.push(CheckResult { - name: "Repository".into(), - status: CheckStatus::Pass, - summary: repo_summary, - details: vec![ + name: "Repository".into(), + status: CheckStatus::Pass, + summary: repo_summary, + details: vec![ CheckDetail::new(format!("Setup commands: {setup_command_count}")), CheckDetail { text: format!( @@ -389,10 +392,10 @@ async fn build_preflight_report( remediation: None, }); checks.push(CheckResult { - name: "Workflow".into(), - status: CheckStatus::Pass, - summary: graph.name.clone(), - details: vec![ + name: "Workflow".into(), + status: CheckStatus::Pass, + summary: graph.name.clone(), + details: vec![ CheckDetail::new(format!("Nodes: {}", graph.nodes.len())), CheckDetail::new(format!("Edges: {}", graph.edges.len())), CheckDetail::new(format!("Goal: {}", graph.goal())), @@ -415,7 +418,7 @@ async fn build_preflight_report( Ok(( CheckReport { - title: "Run Preflight".into(), + title: "Run Preflight".into(), sections: vec![CheckSection { title: String::new(), checks, @@ -482,10 +485,10 @@ async fn run_sandbox_check( Ok(()) => { let _ = sandbox.cleanup().await; checks.push(CheckResult { - name: "Sandbox".into(), - status: CheckStatus::Pass, - summary: sandbox_provider.to_string(), - details: vec![CheckDetail::new(format!("Provider: {sandbox_provider}"))], + name: "Sandbox".into(), + status: CheckStatus::Pass, + summary: sandbox_provider.to_string(), + details: vec![CheckDetail::new(format!("Provider: {sandbox_provider}"))], remediation: None, }); true @@ -493,10 +496,10 @@ async fn run_sandbox_check( Err(err) => { let _ = sandbox.cleanup().await; checks.push(CheckResult { - name: "Sandbox".into(), - status: CheckStatus::Error, - summary: "failed".into(), - details: vec![CheckDetail::new(format!("Provider: {sandbox_provider}"))], + name: "Sandbox".into(), + status: CheckStatus::Error, + summary: "failed".into(), + details: vec![CheckDetail::new(format!("Provider: {sandbox_provider}"))], remediation: Some(format!("Sandbox init failed: {err}")), }); false @@ -504,10 +507,10 @@ async fn run_sandbox_check( }, Err(err) => { checks.push(CheckResult { - name: "Sandbox".into(), - status: CheckStatus::Error, - summary: "failed".into(), - details: vec![CheckDetail::new(format!("Provider: {sandbox_provider}"))], + name: "Sandbox".into(), + status: CheckStatus::Error, + summary: "failed".into(), + details: vec![CheckDetail::new(format!("Provider: {sandbox_provider}"))], remediation: Some(err), }); false @@ -583,12 +586,10 @@ async fn run_llm_check( } Err(err) => { checks.push(CheckResult { - name: "LLM".into(), - status: CheckStatus::Error, - summary: model_id.clone(), - details: vec![CheckDetail::new(format!( - "Provider: {provider_name}" - ))], + name: "LLM".into(), + status: CheckStatus::Error, + summary: model_id.clone(), + details: vec![CheckDetail::new(format!("Provider: {provider_name}"))], remediation: Some(format!( "Invalid provider \"{provider_name}\": {err}" )), @@ -601,10 +602,10 @@ async fn run_llm_check( } Err(err) => { checks.push(CheckResult { - name: "LLM".into(), - status: CheckStatus::Error, - summary: "initialization failed".into(), - details: vec![], + name: "LLM".into(), + status: CheckStatus::Error, + summary: "initialization failed".into(), + details: vec![], remediation: Some(format!("LLM client init failed: {err}")), }); false @@ -643,15 +644,15 @@ fn render_resolve_errors(errors: &[fabro_config::ResolveError]) -> String { fn runtime_daytona_config(settings: &DaytonaSettings) -> DaytonaConfig { DaytonaConfig { auto_stop_interval: settings.auto_stop_interval, - labels: (!settings.labels.is_empty()).then_some(settings.labels.clone()), - snapshot: settings + labels: (!settings.labels.is_empty()).then_some(settings.labels.clone()), + snapshot: settings .snapshot .as_ref() .map(|snapshot| DaytonaSnapshotSettings { - name: snapshot.name.clone(), - cpu: snapshot.cpu, - memory: snapshot.memory_gb, - disk: snapshot.disk_gb, + name: snapshot.name.clone(), + cpu: snapshot.cpu, + memory: snapshot.memory_gb, + disk: snapshot.disk_gb, dockerfile: snapshot .dockerfile .as_ref() @@ -664,14 +665,14 @@ fn runtime_daytona_config(settings: &DaytonaSettings) -> DaytonaConfig { } }), }), - network: settings.network.as_ref().map(|network| match network { + network: settings.network.as_ref().map(|network| match network { DaytonaNetworkLayer::Block => DaytonaNetwork::Block, DaytonaNetworkLayer::AllowAll => DaytonaNetwork::AllowAll, DaytonaNetworkLayer::AllowList { allow_list } => { DaytonaNetwork::AllowList(allow_list.clone()) } }), - skip_clone: settings.skip_clone, + skip_clone: settings.skip_clone, } } @@ -703,26 +704,26 @@ async fn run_github_token_check( (Some(creds), Some(git)) => { match mint_github_token(creds, &git.origin_url, &github_permissions).await { Ok(_) => checks.push(CheckResult { - name: "GitHub Token".into(), - status: CheckStatus::Pass, - summary: "minted".into(), - details: perm_details, + name: "GitHub Token".into(), + status: CheckStatus::Pass, + summary: "minted".into(), + details: perm_details, remediation: None, }), Err(err) => checks.push(CheckResult { - name: "GitHub Token".into(), - status: CheckStatus::Error, - summary: "failed".into(), - details: perm_details, + name: "GitHub Token".into(), + status: CheckStatus::Error, + summary: "failed".into(), + details: perm_details, remediation: Some(format!("Failed to mint GitHub token: {err}")), }), } } _ => checks.push(CheckResult { - name: "GitHub Token".into(), - status: CheckStatus::Warning, - summary: "skipped".into(), - details: vec![], + name: "GitHub Token".into(), + status: CheckStatus::Warning, + summary: "skipped".into(), + details: vec![], remediation: Some("No GitHub App credentials or origin URL available".to_string()), }), } @@ -763,11 +764,11 @@ fn preflight_response( checks: report_to_api(report), workflow: types::PreflightWorkflowSummary { diagnostics: diagnostics_to_api(validated.diagnostics()), - edges: i64::try_from(validated.graph().edges.len()).unwrap(), - goal: validated.graph().goal().to_string(), - graph_path: Some(target_path.display().to_string()), - name: validated.graph().name.clone(), - nodes: i64::try_from(validated.graph().nodes.len()).unwrap(), + edges: i64::try_from(validated.graph().edges.len()).unwrap(), + goal: validated.graph().goal().to_string(), + graph_path: Some(target_path.display().to_string()), + name: validated.graph().name.clone(), + nodes: i64::try_from(validated.graph().nodes.len()).unwrap(), }, } } @@ -778,14 +779,14 @@ fn diagnostics_to_api( diagnostics .iter() .map(|diagnostic| types::WorkflowDiagnostic { - edge: diagnostic + edge: diagnostic .edge .as_ref() .map(|edge: &(String, String)| [edge.0.clone(), edge.1.clone()]), - fix: diagnostic.fix.clone(), - message: diagnostic.message.clone(), - node_id: diagnostic.node_id.clone(), - rule: diagnostic.rule.clone(), + fix: diagnostic.fix.clone(), + message: diagnostic.message.clone(), + node_id: diagnostic.node_id.clone(), + rule: diagnostic.rule.clone(), severity: match diagnostic.severity { Severity::Error => types::WorkflowDiagnosticSeverity::Error, Severity::Warning => types::WorkflowDiagnosticSeverity::Warning, @@ -805,7 +806,7 @@ fn report_to_api(report: &CheckReport) -> types::PreflightCheckReport { .checks .iter() .map(|check| types::PreflightCheckResult { - details: check + details: check .details .iter() .map(|detail| types::PreflightCheckDetail { @@ -813,20 +814,20 @@ fn report_to_api(report: &CheckReport) -> types::PreflightCheckReport { warn: detail.warn, }) .collect(), - name: check.name.clone(), + name: check.name.clone(), remediation: check.remediation.clone(), - status: match check.status { + status: match check.status { CheckStatus::Pass => types::PreflightCheckResultStatus::Pass, CheckStatus::Warning => types::PreflightCheckResultStatus::Warning, CheckStatus::Error => types::PreflightCheckResultStatus::Error, }, - summary: check.summary.clone(), + summary: check.summary.clone(), }) .collect(), - title: section.title.clone(), + title: section.title.clone(), }) .collect(), - title: report.title.clone(), + title: report.title.clone(), } } @@ -836,24 +837,27 @@ mod tests { fn minimal_manifest() -> types::RunManifest { types::RunManifest { - args: None, - configs: Vec::new(), - cwd: "/tmp/project".to_string(), - git: None, - goal: None, - run_id: None, - target: types::ManifestTarget { + args: None, + configs: Vec::new(), + cwd: "/tmp/project".to_string(), + git: None, + goal: None, + run_id: None, + target: types::ManifestTarget { identifier: "workflow.fabro".to_string(), - path: "workflow.fabro".to_string(), + path: "workflow.fabro".to_string(), }, - version: 1, - workflows: HashMap::from([("workflow.fabro".to_string(), types::ManifestWorkflow { - config: None, - files: HashMap::new(), - source: - "digraph Demo { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }" - .to_string(), - })]), + version: 1, + workflows: HashMap::from([( + "workflow.fabro".to_string(), + types::ManifestWorkflow { + config: None, + files: HashMap::new(), + source: + "digraph Demo { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }" + .to_string(), + }, + )]), } } @@ -899,15 +903,15 @@ root = "/srv/fabro" ); let mut manifest = minimal_manifest(); manifest.args = Some(types::ManifestArgs { - auto_approve: None, - dry_run: Some(true), - label: Vec::new(), - model: None, - no_retro: None, + auto_approve: None, + dry_run: Some(true), + label: Vec::new(), + model: None, + no_retro: None, preserve_sandbox: None, - provider: None, - sandbox: None, - verbose: None, + provider: None, + sandbox: None, + verbose: None, }); let prepared = prepare_manifest_with_mode(&server_settings, &manifest, false).unwrap(); @@ -941,7 +945,7 @@ app_id = "snapshotted-app-id" let mut manifest = minimal_manifest(); manifest.workflows.get_mut("workflow.fabro").unwrap().config = Some(types::ManifestWorkflowConfig { - path: "workflow.toml".to_string(), + path: "workflow.toml".to_string(), source: r#" _version = 1 @@ -951,7 +955,7 @@ script = "workflow-setup" .to_string(), }); manifest.configs.push(types::ManifestConfig { - path: Some("/tmp/home/.fabro/settings.toml".to_string()), + path: Some("/tmp/home/.fabro/settings.toml".to_string()), source: Some( r#" _version = 1 @@ -964,7 +968,7 @@ app_id = "snapshotted-app-id" "# .to_string(), ), - type_: types::ManifestConfigType::User, + type_: types::ManifestConfigType::User, }); let prepared = prepare_manifest_with_mode(&server_settings, &manifest, true).unwrap(); @@ -973,9 +977,10 @@ app_id = "snapshotted-app-id" // v2 merge matrix: run.prepare.steps replaces the whole list across // layers, so the higher-precedence workflow layer wins over cli. - assert_eq!(resolved_run.prepare.commands, vec![ - "workflow-setup".to_string() - ]); + assert_eq!( + resolved_run.prepare.commands, + vec!["workflow-setup".to_string()] + ); assert_eq!( resolved_server .integrations diff --git a/lib/crates/fabro-server/src/secret_store.rs b/lib/crates/fabro-server/src/secret_store.rs index d87c53014..d700a3826 100644 --- a/lib/crates/fabro-server/src/secret_store.rs +++ b/lib/crates/fabro-server/src/secret_store.rs @@ -4,14 +4,14 @@ use std::path::{Path, PathBuf}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct SecretEntry { - pub value: String, + pub value: String, pub created_at: String, pub updated_at: String, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct SecretMetadata { - pub name: String, + pub name: String, pub created_at: String, pub updated_at: String, } @@ -51,7 +51,7 @@ impl From for SecretStoreError { #[derive(Debug)] pub struct SecretStore { - path: PathBuf, + path: PathBuf, entries: HashMap, } @@ -75,7 +75,7 @@ impl SecretStore { .get(name) .map_or_else(|| now.clone(), |entry| entry.created_at.clone()); let entry = SecretEntry { - value: value.to_string(), + value: value.to_string(), created_at: created_at.clone(), updated_at: now.clone(), }; @@ -103,7 +103,7 @@ impl SecretStore { .entries .iter() .map(|(name, entry)| SecretMetadata { - name: name.clone(), + name: name.clone(), created_at: entry.created_at.clone(), updated_at: entry.updated_at.clone(), }) diff --git a/lib/crates/fabro-server/src/serve.rs b/lib/crates/fabro-server/src/serve.rs index 1244b759e..4db97e2ef 100644 --- a/lib/crates/fabro-server/src/serve.rs +++ b/lib/crates/fabro-server/src/serve.rs @@ -707,15 +707,15 @@ mod tests { fn apply_runtime_settings_preserves_storage_dir() { let base = SettingsLayer::default(); let args = ServeArgs { - bind: None, - model: None, - provider: None, - dry_run: false, - sandbox: None, - web: false, - no_web: false, + bind: None, + model: None, + provider: None, + dry_run: false, + sandbox: None, + web: false, + no_web: false, max_concurrent_runs: None, - config: None, + config: None, }; let resolved = @@ -741,15 +741,15 @@ enabled = false ", ); let args = ServeArgs { - bind: None, - model: None, - provider: None, - dry_run: false, - sandbox: None, - web: true, - no_web: false, + bind: None, + model: None, + provider: None, + dry_run: false, + sandbox: None, + web: true, + no_web: false, max_concurrent_runs: None, - config: None, + config: None, }; let resolved = apply_runtime_settings(&base, &args, false, &PathBuf::from("/srv/fabro")); @@ -768,15 +768,15 @@ enabled = false fn apply_runtime_settings_disables_web_from_cli_flag() { let base = SettingsLayer::default(); let args = ServeArgs { - bind: None, - model: None, - provider: None, - dry_run: false, - sandbox: None, - web: false, - no_web: true, + bind: None, + model: None, + provider: None, + dry_run: false, + sandbox: None, + web: false, + no_web: true, max_concurrent_runs: None, - config: None, + config: None, }; let resolved = apply_runtime_settings(&base, &args, false, &PathBuf::from("/srv/fabro")); diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs index 20339da37..a736dcafe 100644 --- a/lib/crates/fabro-server/src/server.rs +++ b/lib/crates/fabro-server/src/server.rs @@ -72,8 +72,8 @@ use fabro_types::{ }; use fabro_util::redact::redact_jsonl_line; use fabro_util::version::FABRO_VERSION; +use fabro_workflow::Error as WorkflowError; use fabro_workflow::artifact_upload::ArtifactSink; -use fabro_workflow::error::FabroError; use fabro_workflow::event::{self as workflow_event, Emitter}; use fabro_workflow::handler::HandlerRegistry; use fabro_workflow::operations::{self}; @@ -121,7 +121,7 @@ pub fn default_page_limit() -> u32 { #[derive(serde::Deserialize)] pub struct PaginationParams { #[serde(rename = "page[limit]", default = "default_page_limit")] - pub limit: u32, + pub limit: u32, #[serde(rename = "page[offset]", default)] pub offset: u32, } @@ -129,13 +129,13 @@ pub struct PaginationParams { #[derive(serde::Deserialize)] struct ModelListParams { #[serde(rename = "page[limit]", default = "default_page_limit")] - limit: u32, + limit: u32, #[serde(rename = "page[offset]", default)] - offset: u32, + offset: u32, #[serde(default)] provider: Option, #[serde(default)] - query: Option, + query: Option, } #[derive(serde::Deserialize)] @@ -149,7 +149,7 @@ struct EventListParams { #[serde(default)] since_seq: Option, #[serde(default)] - limit: Option, + limit: Option, } impl EventListParams { @@ -188,7 +188,7 @@ struct ArtifactFilenameParams { #[derive(serde::Deserialize)] struct SandboxFilesParams { - path: String, + path: String, #[serde(default)] depth: Option, } @@ -216,22 +216,22 @@ impl ListResponse { /// Snapshot of a managed run. struct ManagedRun { - dot_source: String, - status: RunStatus, - error: Option, - created_at: chrono::DateTime, - enqueued_at: Instant, + dot_source: String, + status: RunStatus, + error: Option, + created_at: chrono::DateTime, + enqueued_at: Instant, // Populated when running: - answer_transport: Option, + answer_transport: Option, accepted_questions: HashSet, - event_tx: Option>, - checkpoint: Option, - cancel_tx: Option>, - cancel_token: Option>, - worker_pid: Option, - worker_pgid: Option, - run_dir: Option, - execution_mode: RunExecutionMode, + event_tx: Option>, + checkpoint: Option, + cancel_tx: Option>, + cancel_token: Option>, + worker_pid: Option, + worker_pgid: Option, + run_dir: Option, + execution_mode: RunExecutionMode, } #[derive(Clone, Copy)] @@ -241,7 +241,7 @@ enum RunExecutionMode { } enum ExecutionResult { - Completed(Box>), + Completed(Box>), CancelledBySignal, } @@ -259,18 +259,18 @@ const MAX_MULTIPART_MANIFEST_BYTES: usize = 256 * 1024; #[derive(Clone)] struct ArtifactUploadTokenKeys { - encoding: Arc, - decoding: Arc, + encoding: Arc, + decoding: Arc, validation: Arc, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] struct ArtifactUploadClaims { - iss: String, - iat: u64, - exp: u64, + iss: String, + iat: u64, + exp: u64, run_id: String, - scope: String, + scope: String, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -280,29 +280,29 @@ struct ArtifactBatchUploadManifest { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] struct ArtifactBatchUploadEntry { - part: String, - path: String, + part: String, + path: String, #[serde(default, skip_serializing_if = "Option::is_none")] - sha256: Option, + sha256: Option, #[serde(default, skip_serializing_if = "Option::is_none")] expected_bytes: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - content_type: Option, + content_type: Option, } /// Per-model billing totals. #[derive(Default)] struct ModelBillingTotals { - stages: i64, + stages: i64, billing: BilledTokenCounts, } /// In-memory aggregate billing counters, reset on server restart. #[derive(Default)] struct BillingAccumulator { - total_runs: i64, + total_runs: i64, total_runtime_secs: f64, - by_model: HashMap, + by_model: HashMap, } type RegistryFactoryOverride = dyn Fn(Arc) -> HandlerRegistry + Send + Sync; @@ -359,15 +359,15 @@ impl RunAnswerTransport { #[derive(Debug, Clone)] struct LoadedPendingInterview { - run_id: RunId, - qid: String, + run_id: RunId, + qid: String, question: InterviewQuestionRecord, } #[derive(Clone)] struct SlackService { - client: SlackClient, - app_token: String, + client: SlackClient, + app_token: String, default_channel: String, posted_messages: Arc>>, thread_registry: Arc, @@ -401,12 +401,12 @@ impl SlackService { } let question = runtime_question_from_interview_record(&InterviewQuestionRecord { - id: props.question_id.clone(), - text: props.question.clone(), - stage: props.stage.clone(), - question_type: InterviewQuestionType::from_wire_name(&props.question_type), - options: props.options.clone(), - allow_freeform: props.allow_freeform, + id: props.question_id.clone(), + text: props.question.clone(), + stage: props.stage.clone(), + question_type: InterviewQuestionType::from_wire_name(&props.question_type), + options: props.options.clone(), + allow_freeform: props.allow_freeform, timeout_seconds: props.timeout_seconds, context_display: props.context_display.clone(), }); @@ -505,25 +505,25 @@ impl SlackService { /// Shared application state for the server. pub struct AppState { - runs: Mutex>, - aggregate_billing: Mutex, - store: Arc, - artifact_store: ArtifactStore, + runs: Mutex>, + aggregate_billing: Mutex, + store: Arc, + artifact_store: ArtifactStore, artifact_upload_tokens: ArtifactUploadTokenKeys, - started_at: Instant, - max_concurrent_runs: usize, - scheduler_notify: Notify, - global_event_tx: broadcast::Sender, + started_at: Instant, + max_concurrent_runs: usize, + scheduler_notify: Notify, + global_event_tx: broadcast::Sender, - pub(crate) secret_store: AsyncRwLock, - pub(crate) settings: Arc>, - pub(crate) server_settings: RwLock>, - pub(crate) config_path: PathBuf, + pub(crate) secret_store: AsyncRwLock, + pub(crate) settings: Arc>, + pub(crate) server_settings: RwLock>, + pub(crate) config_path: PathBuf, pub(crate) local_daemon_mode: bool, - shutting_down: AtomicBool, - registry_factory_override: Option>, - slack_service: Option>, - slack_started: AtomicBool, + shutting_down: AtomicBool, + registry_factory_override: Option>, + slack_service: Option>, + slack_started: AtomicBool, } fn nonzero_i64(value: i64) -> Option { @@ -532,26 +532,26 @@ fn nonzero_i64(value: i64) -> Option { fn api_billed_token_counts_from_domain(billing: &BilledTokenCounts) -> ApiBilledTokenCounts { ApiBilledTokenCounts { - cache_read_tokens: nonzero_i64(billing.cache_read_tokens), + cache_read_tokens: nonzero_i64(billing.cache_read_tokens), cache_write_tokens: nonzero_i64(billing.cache_write_tokens), - input_tokens: billing.input_tokens, - output_tokens: billing.output_tokens, - reasoning_tokens: nonzero_i64(billing.reasoning_tokens), - total_tokens: billing.total_tokens, - total_usd_micros: billing.total_usd_micros, + input_tokens: billing.input_tokens, + output_tokens: billing.output_tokens, + reasoning_tokens: nonzero_i64(billing.reasoning_tokens), + total_tokens: billing.total_tokens, + total_usd_micros: billing.total_usd_micros, } } fn api_billed_token_counts_from_usage(usage: &BilledModelUsage) -> ApiBilledTokenCounts { let tokens = usage.tokens(); ApiBilledTokenCounts { - cache_read_tokens: nonzero_i64(tokens.cache_read_tokens), + cache_read_tokens: nonzero_i64(tokens.cache_read_tokens), cache_write_tokens: nonzero_i64(tokens.cache_write_tokens), - input_tokens: tokens.input_tokens, - output_tokens: tokens.output_tokens, - reasoning_tokens: nonzero_i64(tokens.reasoning_tokens), - total_tokens: tokens.total_tokens(), - total_usd_micros: usage.total_usd_micros, + input_tokens: tokens.input_tokens, + output_tokens: tokens.output_tokens, + reasoning_tokens: nonzero_i64(tokens.reasoning_tokens), + total_tokens: tokens.total_tokens(), + total_usd_micros: usage.total_usd_micros, } } @@ -654,11 +654,11 @@ impl AppState { .map(|duration| duration.as_secs()) .unwrap_or(0); let claims = ArtifactUploadClaims { - iss: ARTIFACT_UPLOAD_TOKEN_ISSUER.to_string(), - iat: now, - exp: now + ARTIFACT_UPLOAD_TOKEN_TTL_SECS, + iss: ARTIFACT_UPLOAD_TOKEN_ISSUER.to_string(), + iat: now, + exp: now + ARTIFACT_UPLOAD_TOKEN_TTL_SECS, run_id: run_id.to_string(), - scope: ARTIFACT_UPLOAD_TOKEN_SCOPE.to_string(), + scope: ARTIFACT_UPLOAD_TOKEN_SCOPE.to_string(), }; jsonwebtoken::encode( &Header::new(Algorithm::HS256), @@ -717,8 +717,8 @@ fn artifact_upload_token_keys() -> ArtifactUploadTokenKeys { validation.set_issuer(&[ARTIFACT_UPLOAD_TOKEN_ISSUER]); ArtifactUploadTokenKeys { - encoding: Arc::new(EncodingKey::from_secret(&secret)), - decoding: Arc::new(DecodingKey::from_secret(&secret)), + encoding: Arc::new(EncodingKey::from_secret(&secret)), + decoding: Arc::new(DecodingKey::from_secret(&secret)), validation: Arc::new(validation), } } @@ -1163,16 +1163,16 @@ async fn get_system_info( }; let response = SystemInfoResponse { - version: Some(FABRO_VERSION.to_string()), - git_sha: option_env!("FABRO_GIT_SHA").map(str::to_string), - build_date: option_env!("FABRO_BUILD_DATE").map(str::to_string), - os: Some(std::env::consts::OS.to_string()), - arch: Some(std::env::consts::ARCH.to_string()), - storage_engine: Some("slatedb".to_string()), - storage_dir: Some(state.server_storage_dir().display().to_string()), - uptime_secs: Some(to_i64(state.started_at.elapsed().as_secs())), - runs: Some(SystemRunCounts { - total: Some(to_i64(total_runs)), + version: Some(FABRO_VERSION.to_string()), + git_sha: option_env!("FABRO_GIT_SHA").map(str::to_string), + build_date: option_env!("FABRO_BUILD_DATE").map(str::to_string), + os: Some(std::env::consts::OS.to_string()), + arch: Some(std::env::consts::ARCH.to_string()), + storage_engine: Some("slatedb".to_string()), + storage_dir: Some(state.server_storage_dir().display().to_string()), + uptime_secs: Some(to_i64(state.started_at.elapsed().as_secs())), + runs: Some(SystemRunCounts { + total: Some(to_i64(total_runs)), active: Some(to_i64(active_runs)), }), sandbox_provider: Some(system_sandbox_provider(&settings)), @@ -1255,12 +1255,12 @@ async fn prune_runs( return ( StatusCode::OK, Json(PruneRunsResponse { - dry_run: Some(true), - runs: Some(prune_plan.rows), - total_count: Some(to_i64(prune_plan.run_ids.len())), + dry_run: Some(true), + runs: Some(prune_plan.rows), + total_count: Some(to_i64(prune_plan.run_ids.len())), total_size_bytes: Some(to_i64(prune_plan.total_size_bytes)), - deleted_count: Some(0), - freed_bytes: Some(0), + deleted_count: Some(0), + freed_bytes: Some(0), }), ) .into_response(); @@ -1275,12 +1275,12 @@ async fn prune_runs( ( StatusCode::OK, Json(PruneRunsResponse { - dry_run: Some(false), - runs: None, - total_count: Some(to_i64(prune_plan.run_ids.len())), + dry_run: Some(false), + runs: None, + total_count: Some(to_i64(prune_plan.run_ids.len())), total_size_bytes: Some(to_i64(prune_plan.total_size_bytes)), - deleted_count: Some(to_i64(prune_plan.run_ids.len())), - freed_bytes: Some(to_i64(prune_plan.total_size_bytes)), + deleted_count: Some(to_i64(prune_plan.run_ids.len())), + freed_bytes: Some(to_i64(prune_plan.total_size_bytes)), }), ) .into_response() @@ -1315,8 +1315,8 @@ async fn attach_events( } struct PrunePlan { - run_ids: Vec, - rows: Vec, + run_ids: Vec, + rows: Vec, total_size_bytes: u64, } @@ -1344,12 +1344,12 @@ fn build_disk_usage_response( } if verbose { run_rows.push(DiskUsageRunRow { - run_id: Some(run.run_id().to_string()), + run_id: Some(run.run_id().to_string()), workflow_name: Some(run.workflow_name()), - status: Some(run.status().to_string()), - start_time: Some(run.start_time()), - size_bytes: Some(to_i64(size)), - reclaimable: Some(!run.status().is_active()), + status: Some(run.status().to_string()), + start_time: Some(run.start_time()), + size_bytes: Some(to_i64(size)), + reclaimable: Some(!run.status().is_active()), }); } } @@ -1370,25 +1370,25 @@ fn build_disk_usage_response( } Ok(DiskUsageResponse { - summary: vec![ + summary: vec![ DiskUsageSummaryRow { - type_: Some("runs".to_string()), - count: Some(to_i64(runs.len())), - active: Some(to_i64(active_count)), - size_bytes: Some(to_i64(total_run_size)), + type_: Some("runs".to_string()), + count: Some(to_i64(runs.len())), + active: Some(to_i64(active_count)), + size_bytes: Some(to_i64(total_run_size)), reclaimable_bytes: Some(to_i64(reclaimable_run_size)), }, DiskUsageSummaryRow { - type_: Some("logs".to_string()), - count: Some(to_i64(log_count)), - active: None, - size_bytes: Some(to_i64(total_log_size)), + type_: Some("logs".to_string()), + count: Some(to_i64(log_count)), + active: None, + size_bytes: Some(to_i64(total_log_size)), reclaimable_bytes: Some(to_i64(total_log_size)), }, ], - total_size_bytes: Some(to_i64(total_run_size + total_log_size)), + total_size_bytes: Some(to_i64(total_run_size + total_log_size)), total_reclaimable_bytes: Some(to_i64(reclaimable_run_size + total_log_size)), - runs: verbose.then_some(run_rows), + runs: verbose.then_some(run_rows), }) } @@ -1438,10 +1438,10 @@ fn build_prune_plan( let rows = filtered .iter() .map(|run| PruneRunEntry { - run_id: Some(run.run_id().to_string()), - dir_name: Some(run.dir_name.clone()), + run_id: Some(run.run_id().to_string()), + dir_name: Some(run.dir_name.clone()), workflow_name: Some(run.workflow_name()), - size_bytes: Some(to_i64(dir_size(&run.path))), + size_bytes: Some(to_i64(dir_size(&run.path))), }) .collect::>(); let total_size_bytes = rows @@ -1677,8 +1677,8 @@ async fn delete_secret( #[derive(serde::Deserialize)] struct GitHubRepoResponse { default_branch: String, - private: bool, - permissions: Option, + private: bool, + permissions: Option, } async fn get_github_repo( @@ -1870,8 +1870,8 @@ async fn get_aggregate_billing( .iter() .map(|(model, totals)| BillingByModel { billing: api_billed_token_counts_from_domain(&totals.billing), - model: ModelReference { id: model.clone() }, - stages: totals.stages, + model: ModelReference { id: model.clone() }, + stages: totals.stages, }) .collect(); let total_billing = by_model @@ -1890,15 +1890,15 @@ async fn get_aggregate_billing( }); let response = AggregateBilling { totals: AggregateBillingTotals { - cache_read_tokens: nonzero_i64(total_billing.cache_read_tokens), + cache_read_tokens: nonzero_i64(total_billing.cache_read_tokens), cache_write_tokens: nonzero_i64(total_billing.cache_write_tokens), - input_tokens: total_billing.input_tokens, - output_tokens: total_billing.output_tokens, - reasoning_tokens: nonzero_i64(total_billing.reasoning_tokens), - runs: agg.total_runs, - runtime_secs: agg.total_runtime_secs, - total_tokens: total_billing.total_tokens, - total_usd_micros: total_billing.total_usd_micros, + input_tokens: total_billing.input_tokens, + output_tokens: total_billing.output_tokens, + reasoning_tokens: nonzero_i64(total_billing.reasoning_tokens), + runs: agg.total_runs, + runtime_secs: agg.total_runtime_secs, + total_tokens: total_billing.total_tokens, + total_usd_micros: total_billing.total_usd_micros, }, by_model, }; @@ -1928,16 +1928,16 @@ async fn get_run_billing( let Some(checkpoint) = checkpoint else { let empty = RunBilling { by_model: Vec::new(), - stages: Vec::new(), - totals: RunBillingTotals { - cache_read_tokens: None, + stages: Vec::new(), + totals: RunBillingTotals { + cache_read_tokens: None, cache_write_tokens: None, - input_tokens: 0, - output_tokens: 0, - reasoning_tokens: None, - runtime_secs: 0.0, - total_tokens: 0, - total_usd_micros: None, + input_tokens: 0, + output_tokens: 0, + reasoning_tokens: None, + runtime_secs: 0.0, + total_tokens: 0, + total_usd_micros: None, }, }; return (StatusCode::OK, Json(empty)).into_response(); @@ -1977,7 +1977,7 @@ async fn get_run_billing( model: ModelReference { id: model_id }, runtime_secs: duration_ms as f64 / 1000.0, stage: BillingStageRef { - id: node_id.clone(), + id: node_id.clone(), name: node_id.clone(), }, }); @@ -1988,8 +1988,8 @@ async fn get_run_billing( .into_iter() .map(|(model, totals)| BillingByModel { billing: api_billed_token_counts_from_domain(&totals.billing), - model: ModelReference { id: model }, - stages: totals.stages, + model: ModelReference { id: model }, + stages: totals.stages, }) .collect::>(); @@ -2556,19 +2556,22 @@ fn release_run_answer_claim(state: &AppState, run_id: RunId, qid: &str) { #[derive(Clone, Copy)] struct LiveWorkerProcess { - run_id: RunId, + run_id: RunId, process_group_id: u32, } fn failure_for_incomplete_run( pending_control: Option, terminated_message: String, -) -> (FabroError, Option) { +) -> (WorkflowError, Option) { if pending_control == Some(RunControlAction::Cancel) { - (FabroError::Cancelled, Some(WorkflowStatusReason::Cancelled)) + ( + WorkflowError::Cancelled, + Some(WorkflowStatusReason::Cancelled), + ) } else { ( - FabroError::engine(terminated_message), + WorkflowError::engine(terminated_message), Some(WorkflowStatusReason::Terminated), ) } @@ -2739,9 +2742,9 @@ async fn persist_cancelled_run_status(state: &AppState, run_id: RunId) -> anyhow &run_store, &run_id, &workflow_event::Event::WorkflowRunFailed { - error: FabroError::Cancelled, - duration_ms: 0, - reason: Some(WorkflowStatusReason::Cancelled), + error: WorkflowError::Cancelled, + duration_ms: 0, + reason: Some(WorkflowStatusReason::Cancelled), git_commit_sha: None, }, ) @@ -3055,41 +3058,41 @@ fn runtime_question_type(question_type: InterviewQuestionType) -> QuestionType { fn runtime_question_from_interview_record(question: &InterviewQuestionRecord) -> Question { Question { - id: question.id.clone(), - text: question.text.clone(), - question_type: runtime_question_type(question.question_type), - options: question + id: question.id.clone(), + text: question.text.clone(), + question_type: runtime_question_type(question.question_type), + options: question .options .iter() .map(|option| fabro_interview::QuestionOption { - key: option.key.clone(), + key: option.key.clone(), label: option.label.clone(), }) .collect(), - allow_freeform: question.allow_freeform, - default: None, + allow_freeform: question.allow_freeform, + default: None, timeout_seconds: question.timeout_seconds, - stage: question.stage.clone(), - metadata: HashMap::new(), + stage: question.stage.clone(), + metadata: HashMap::new(), context_display: question.context_display.clone(), } } fn api_question_from_interview_record(question: &InterviewQuestionRecord) -> ApiQuestion { ApiQuestion { - id: question.id.clone(), - text: question.text.clone(), - stage: question.stage.clone(), - question_type: api_question_type(question.question_type), - options: question + id: question.id.clone(), + text: question.text.clone(), + stage: question.stage.clone(), + question_type: api_question_type(question.question_type), + options: question .options .iter() .map(|option| ApiQuestionOption { - key: option.key.clone(), + key: option.key.clone(), label: option.label.clone(), }) .collect(), - allow_freeform: question.allow_freeform, + allow_freeform: question.allow_freeform, timeout_seconds: question.timeout_seconds, context_display: question.context_display.clone(), } @@ -3107,7 +3110,7 @@ async fn load_pending_interview( ) -> Result { let run_store = match state.store.open_run_reader(&run_id).await { Ok(run_store) => run_store, - Err(fabro_store::StoreError::RunNotFound(_)) => { + Err(fabro_store::Error::RunNotFound(_)) => { return Err(ApiError::not_found("Run not found.").into_response()); } Err(err) => { @@ -3246,10 +3249,13 @@ fn answer_from_request( .find(|option| option.key == key) .cloned(); match option { - Some(option) => Ok(Answer::selected(key, fabro_interview::QuestionOption { - key: option.key, - label: option.label, - })), + Some(option) => Ok(Answer::selected( + key, + fabro_interview::QuestionOption { + key: option.key, + label: option.label, + }, + )), None => Err(ApiError::bad_request("Invalid option key.").into_response()), } } else if !req.selected_option_keys.is_empty() { @@ -3298,7 +3304,7 @@ async fn create_run( let created = match Box::pin(operations::create(state.store.as_ref(), create_input)).await { Ok(created) => created, - Err(FabroError::ValidationFailed { .. } | FabroError::Parse(_)) => { + Err(WorkflowError::ValidationFailed { .. } | WorkflowError::Parse(_)) => { return ApiError::bad_request("Validation failed").into_response(); } Err(err) => { @@ -3342,12 +3348,12 @@ async fn create_run( fn run_provenance(headers: &HeaderMap, subject: &AuthenticatedSubject) -> RunProvenance { RunProvenance { - server: Some(RunServerProvenance { + server: Some(RunServerProvenance { version: FABRO_VERSION.to_string(), }), - client: run_client_provenance(headers), + client: run_client_provenance(headers), subject: Some(RunSubjectProvenance { - login: subject.login.clone(), + login: subject.login.clone(), auth_method: subject.auth_method, }), } @@ -3396,7 +3402,7 @@ async fn run_preflight( }; let validated = match run_manifest::validate_prepared_manifest(&prepared) { Ok(validated) => validated, - Err(FabroError::Parse(_)) => { + Err(WorkflowError::Parse(_)) => { return ApiError::bad_request("Validation failed").into_response(); } Err(err) => return ApiError::bad_request(err.to_string()).into_response(), @@ -3555,13 +3561,13 @@ async fn start_run( ( StatusCode::OK, Json(RunStatusResponse { - id: id.to_string(), - status: RunStatus::Queued, - error: None, - queue_position: None, - status_reason: None, + id: id.to_string(), + status: RunStatus::Queued, + error: None, + queue_position: None, + status_reason: None, pending_control: None, - created_at: id.created_at(), + created_at: id.created_at(), }), ) .into_response() @@ -3799,7 +3805,7 @@ async fn execute_run_in_process(state: Arc, run_id: RunId) { info!(run_id = %run_id, "Run completed"); managed_run.status = RunStatus::Completed; } - Err(FabroError::Cancelled) => { + Err(WorkflowError::Cancelled) => { info!(run_id = %run_id, "Run cancelled"); managed_run.status = RunStatus::Cancelled; } @@ -3809,7 +3815,7 @@ async fn execute_run_in_process(state: Arc, run_id: RunId) { managed_run.error = Some(e.to_string()); } }, - Err(FabroError::Cancelled) => { + Err(WorkflowError::Cancelled) => { info!(run_id = %run_id, "Run cancelled"); managed_run.status = RunStatus::Cancelled; } @@ -3874,9 +3880,9 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { &run_store, &run_id, &workflow_event::Event::WorkflowRunFailed { - error: FabroError::engine(err.to_string()), - duration_ms: 0, - reason: Some(WorkflowStatusReason::LaunchFailed), + error: WorkflowError::engine(err.to_string()), + duration_ms: 0, + reason: Some(WorkflowStatusReason::LaunchFailed), git_commit_sha: None, }, ) @@ -3895,9 +3901,9 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { &run_store, &run_id, &workflow_event::Event::WorkflowRunFailed { - error: FabroError::engine(message.clone()), - duration_ms: 0, - reason: Some(WorkflowStatusReason::LaunchFailed), + error: WorkflowError::engine(message.clone()), + duration_ms: 0, + reason: Some(WorkflowStatusReason::LaunchFailed), git_commit_sha: None, }, ) @@ -3924,9 +3930,9 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { &run_store, &run_id, &workflow_event::Event::WorkflowRunFailed { - error: FabroError::engine(message.clone()), - duration_ms: 0, - reason: Some(WorkflowStatusReason::LaunchFailed), + error: WorkflowError::engine(message.clone()), + duration_ms: 0, + reason: Some(WorkflowStatusReason::LaunchFailed), git_commit_sha: None, }, ) @@ -3944,9 +3950,9 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { &run_store, &run_id, &workflow_event::Event::WorkflowRunFailed { - error: FabroError::engine(message.clone()), - duration_ms: 0, - reason: Some(WorkflowStatusReason::LaunchFailed), + error: WorkflowError::engine(message.clone()), + duration_ms: 0, + reason: Some(WorkflowStatusReason::LaunchFailed), git_commit_sha: None, }, ) @@ -3976,9 +3982,9 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { &run_store, &run_id, &workflow_event::Event::WorkflowRunFailed { - error: FabroError::engine(err.to_string()), - duration_ms: 0, - reason: Some(WorkflowStatusReason::Terminated), + error: WorkflowError::engine(err.to_string()), + duration_ms: 0, + reason: Some(WorkflowStatusReason::Terminated), git_commit_sha: None, }, ) @@ -4159,7 +4165,7 @@ async fn get_run_settings( }; let run_store = match state.store.open_run_reader(&id).await { Ok(store) => store, - Err(fabro_store::StoreError::RunNotFound(_)) => { + Err(fabro_store::Error::RunNotFound(_)) => { return ApiError::not_found("Run not found.").into_response(); } Err(err) => { @@ -4212,7 +4218,7 @@ async fn get_questions( ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response() } }, - Err(fabro_store::StoreError::RunNotFound(_)) => { + Err(fabro_store::Error::RunNotFound(_)) => { ApiError::not_found("Run not found.").into_response() } Err(err) => { @@ -4600,11 +4606,11 @@ async fn list_run_artifacts( data: entries .into_iter() .map(|entry| RunArtifactEntry { - stage_id: entry.node.to_string(), - node_slug: entry.node.node_id().to_string(), - retry: entry.node.visit().cast_signed(), + stage_id: entry.node.to_string(), + node_slug: entry.node.node_id().to_string(), + retry: entry.node.visit().cast_signed(), relative_path: entry.filename, - size: entry.size.cast_signed(), + size: entry.size.cast_signed(), }) .collect(), }) @@ -4652,8 +4658,8 @@ enum ArtifactUploadContentType { } struct ValidatedArtifactBatchEntry { - path: String, - sha256: Option, + path: String, + sha256: Option, expected_bytes: Option, } @@ -4785,11 +4791,14 @@ fn validate_artifact_batch_manifest( } } if entries - .insert(entry.part.clone(), ValidatedArtifactBatchEntry { - path, - sha256: entry.sha256.map(|value| value.to_ascii_lowercase()), - expected_bytes: entry.expected_bytes, - }) + .insert( + entry.part.clone(), + ValidatedArtifactBatchEntry { + path, + sha256: entry.sha256.map(|value| value.to_ascii_lowercase()), + expected_bytes: entry.expected_bytes, + }, + ) .is_some() { return Err(bad_request_response(format!( @@ -5109,7 +5118,7 @@ async fn generate_preview_url( { Ok(preview) => PreviewUrlResponse { token: None, - url: preview.url, + url: preview.url, }, Err(err) => { return ApiError::new(StatusCode::CONFLICT, err).into_response(); @@ -5119,7 +5128,7 @@ async fn generate_preview_url( match sandbox.get_preview_link(port).await { Ok(preview) => PreviewUrlResponse { token: Some(preview.token), - url: preview.url, + url: preview.url, }, Err(err) => { return ApiError::new(StatusCode::CONFLICT, err).into_response(); @@ -5170,8 +5179,8 @@ async fn list_sandbox_files( .into_iter() .map(|entry| SandboxFileEntry { is_dir: entry.is_dir, - name: entry.name, - size: entry.size.map(u64::cast_signed), + name: entry.name, + size: entry.size.map(u64::cast_signed), }) .collect(), }) @@ -5783,9 +5792,9 @@ async fn create_completion( req.tools .into_iter() .map(|t| ToolDefinition { - name: t.name, + name: t.name, description: t.description, - parameters: t.parameters, + parameters: t.parameters, }) .collect(), ) @@ -5828,18 +5837,21 @@ async fn create_completion( if state.dry_run() { let msg_id = Ulid::new().to_string(); if use_stream { - let finish_event = - StreamEvent::finish(FinishReason::Stop, TokenCounts::default(), LlmResponse { - id: msg_id.clone(), - model: model_id.clone(), - provider: String::new(), - message: LlmMessage::assistant(""), + let finish_event = StreamEvent::finish( + FinishReason::Stop, + TokenCounts::default(), + LlmResponse { + id: msg_id.clone(), + model: model_id.clone(), + provider: String::new(), + message: LlmMessage::assistant(""), finish_reason: FinishReason::Stop, - usage: TokenCounts::default(), - raw: None, - warnings: vec![], - rate_limit: None, - }); + usage: TokenCounts::default(), + raw: None, + warnings: vec![], + rate_limit: None, + }, + ); let json = serde_json::to_string(&finish_event).unwrap_or_default(); let sse_stream = stream::iter(vec![Ok::<_, std::convert::Infallible>( Event::default().event("stream_event").data(json), @@ -5847,21 +5859,21 @@ async fn create_completion( return Sse::new(sse_stream).into_response(); } let empty_msg = CompletionMessage { - role: CompletionMessageRole::Assistant, - content: vec![], - name: None, + role: CompletionMessageRole::Assistant, + content: vec![], + name: None, tool_call_id: None, }; return Json(CompletionResponse { - id: msg_id, - model: model_id, - message: empty_msg, + id: msg_id, + model: model_id, + message: empty_msg, stop_reason: "end_turn".to_string(), - usage: CompletionUsage { - input_tokens: 0, + usage: CompletionUsage { + input_tokens: 0, output_tokens: 0, }, - output: None, + output: None, }) .into_response(); } @@ -5944,15 +5956,15 @@ async fn create_completion( } match generate_object(params, schema).await { Ok(result) => Json(CompletionResponse { - id: msg_id, - model: model_id, - message: convert_llm_message(&result.response.message), + id: msg_id, + model: model_id, + message: convert_llm_message(&result.response.message), stop_reason: finish_reason_to_api_stop_reason(&result.finish_reason), - usage: CompletionUsage { - input_tokens: result.usage.input_tokens, + usage: CompletionUsage { + input_tokens: result.usage.input_tokens, output_tokens: result.usage.output_tokens, }, - output: result.output, + output: result.output, }) .into_response(), Err(e) => ApiError::new(StatusCode::BAD_GATEWAY, format!("LLM error: {e}")) @@ -5961,15 +5973,15 @@ async fn create_completion( } else { match client.complete(&request).await { Ok(response) => Json(CompletionResponse { - id: response.id, - model: response.model, - message: convert_llm_message(&response.message), + id: response.id, + model: response.model, + message: convert_llm_message(&response.message), stop_reason: finish_reason_to_api_stop_reason(&response.finish_reason), - usage: CompletionUsage { - input_tokens: response.usage.input_tokens, + usage: CompletionUsage { + input_tokens: response.usage.input_tokens, output_tokens: response.usage.output_tokens, }, - output: None, + output: None, }) .into_response(), Err(e) => ApiError::new(StatusCode::BAD_GATEWAY, format!("LLM error: {e}")) @@ -6314,11 +6326,14 @@ mod tests { .iter() .map(|model| model["id"].as_str().unwrap().to_string()) .collect::>(); - assert_eq!(model_ids, vec![ - "gpt-5.2-codex".to_string(), - "gpt-5.3-codex".to_string(), - "gpt-5.3-codex-spark".to_string() - ]); + assert_eq!( + model_ids, + vec![ + "gpt-5.2-codex".to_string(), + "gpt-5.3-codex".to_string(), + "gpt-5.3-codex-spark".to_string() + ] + ); } #[tokio::test] @@ -6589,18 +6604,18 @@ slug = "fabro" async fn submit_pending_interview_answer_rejects_invalid_answer_shape() { let state = create_app_state(); let pending = LoadedPendingInterview { - run_id: fixtures::RUN_1, - qid: "q-1".to_string(), + run_id: fixtures::RUN_1, + qid: "q-1".to_string(), question: InterviewQuestionRecord { - id: "q-1".to_string(), - text: "Approve deploy?".to_string(), - stage: "gate".to_string(), - question_type: InterviewQuestionType::MultipleChoice, - options: vec![fabro_types::run_event::InterviewOption { - key: "approve".to_string(), + id: "q-1".to_string(), + text: "Approve deploy?".to_string(), + stage: "gate".to_string(), + question_type: InterviewQuestionType::MultipleChoice, + options: vec![fabro_types::run_event::InterviewOption { + key: "approve".to_string(), label: "Approve".to_string(), }], - allow_freeform: false, + allow_freeform: false, timeout_seconds: None, context_display: None, }, @@ -7016,10 +7031,14 @@ slug = "fabro" "content-type", format!("multipart/form-data; boundary={boundary}"), ) - .body(multipart_body(boundary, &manifest, &[ - ("file1", "src/lib.rs", source_bytes), - ("file2", "logs/output.txt", log_bytes), - ])) + .body(multipart_body( + boundary, + &manifest, + &[ + ("file1", "src/lib.rs", source_bytes), + ("file2", "logs/output.txt", log_bytes), + ], + )) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); if response.status() != StatusCode::NO_CONTENT { @@ -7789,32 +7808,42 @@ level = "debug" async fn startup_reconciliation_marks_inflight_runs_terminal() { let state = create_app_state(); - create_durable_run_with_events(&state, fixtures::RUN_1, &[ - workflow_event::Event::RunSubmitted { - reason: None, + create_durable_run_with_events( + &state, + fixtures::RUN_1, + &[workflow_event::Event::RunSubmitted { + reason: None, definition_blob: None, - }, - ]) + }], + ) .await; - create_durable_run_with_events(&state, fixtures::RUN_2, &[ - workflow_event::Event::RunSubmitted { - reason: None, - definition_blob: None, - }, - workflow_event::Event::RunStarting { reason: None }, - workflow_event::Event::RunRunning { reason: None }, - ]) + create_durable_run_with_events( + &state, + fixtures::RUN_2, + &[ + workflow_event::Event::RunSubmitted { + reason: None, + definition_blob: None, + }, + workflow_event::Event::RunStarting { reason: None }, + workflow_event::Event::RunRunning { reason: None }, + ], + ) .await; - create_durable_run_with_events(&state, fixtures::RUN_3, &[ - workflow_event::Event::RunSubmitted { - reason: None, - definition_blob: None, - }, - workflow_event::Event::RunStarting { reason: None }, - workflow_event::Event::RunRunning { reason: None }, - workflow_event::Event::RunPaused, - workflow_event::Event::RunCancelRequested { actor: None }, - ]) + create_durable_run_with_events( + &state, + fixtures::RUN_3, + &[ + workflow_event::Event::RunSubmitted { + reason: None, + definition_blob: None, + }, + workflow_event::Event::RunStarting { reason: None }, + workflow_event::Event::RunRunning { reason: None }, + workflow_event::Event::RunPaused, + workflow_event::Event::RunCancelRequested { actor: None }, + ], + ) .await; let reconciled = reconcile_incomplete_runs_on_startup(&state).await.unwrap(); @@ -7862,14 +7891,18 @@ level = "debug" let state = create_app_state(); let run_id = fixtures::RUN_4; - create_durable_run_with_events(&state, run_id, &[ - workflow_event::Event::RunSubmitted { - reason: None, - definition_blob: None, - }, - workflow_event::Event::RunStarting { reason: None }, - workflow_event::Event::RunRunning { reason: None }, - ]) + create_durable_run_with_events( + &state, + run_id, + &[ + workflow_event::Event::RunSubmitted { + reason: None, + definition_blob: None, + }, + workflow_event::Event::RunStarting { reason: None }, + workflow_event::Event::RunRunning { reason: None }, + ], + ) .await; let temp_dir = tempfile::tempdir().unwrap(); diff --git a/lib/crates/fabro-server/src/web_auth.rs b/lib/crates/fabro-server/src/web_auth.rs index ae424c03d..4a103e91f 100644 --- a/lib/crates/fabro-server/src/web_auth.rs +++ b/lib/crates/fabro-server/src/web_auth.rs @@ -20,18 +20,18 @@ const OAUTH_STATE_COOKIE_NAME: &str = "fabro_oauth_state"; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SessionCookie { - pub login: String, - pub name: String, - pub email: String, + pub login: String, + pub name: String, + pub email: String, pub avatar_url: String, - pub user_url: String, - pub github_id: i64, - pub exp: i64, + pub user_url: String, + pub github_id: i64, + pub exp: i64, } #[derive(Deserialize)] struct OAuthCallbackParams { - code: String, + code: String, state: String, } @@ -52,22 +52,22 @@ struct SetupStatusResponse { #[derive(Serialize)] struct AuthMeResponse { - user: SessionUser, - provider: &'static str, + user: SessionUser, + provider: &'static str, #[serde(rename = "demoMode")] demo_mode: bool, - features: serde_json::Value, + features: serde_json::Value, } #[derive(Serialize)] struct SessionUser { - login: String, - name: String, - email: String, + login: String, + name: String, + email: String, #[serde(rename = "avatarUrl")] avatar_url: String, #[serde(rename = "userUrl")] - user_url: String, + user_url: String, } #[derive(Deserialize)] @@ -77,27 +77,27 @@ struct GitHubTokenResponse { #[derive(Deserialize)] struct GitHubUser { - id: i64, - login: String, - name: Option, + id: i64, + login: String, + name: Option, avatar_url: String, } #[derive(Deserialize)] struct GitHubEmail { - email: String, - primary: bool, + email: String, + primary: bool, verified: bool, } #[derive(Deserialize)] struct GitHubManifestConversion { - id: i64, - slug: String, - client_id: String, - client_secret: String, + id: i64, + slug: String, + client_id: String, + client_secret: String, webhook_secret: Option, - pem: String, + pem: String, } pub fn routes() -> Router> { @@ -206,14 +206,16 @@ async fn login_github(State(state): State>) -> Response { } let state_token = format!("fabro-{}", ulid::Ulid::new()); - let authorize_url = - reqwest::Url::parse_with_params("https://github.com/login/oauth/authorize", &[ + let authorize_url = reqwest::Url::parse_with_params( + "https://github.com/login/oauth/authorize", + &[ ("client_id", client_id.as_str()), ("redirect_uri", &format!("{web_url}/auth/callback/github")), ("scope", "read:user user:email"), ("state", state_token.as_str()), - ]) - .expect("GitHub authorize URL should be valid"); + ], + ) + .expect("GitHub authorize URL should be valid"); debug!(redirect_uri = %format!("{web_url}/auth/callback/github"), "OAuth login redirecting to GitHub"); @@ -396,13 +398,13 @@ async fn callback_github( .unwrap_or_default(); let now = chrono::Utc::now(); let session = SessionCookie { - login: profile.login.clone(), - name: profile.name.unwrap_or_else(|| profile.login.clone()), - email: primary_email, + login: profile.login.clone(), + name: profile.name.unwrap_or_else(|| profile.login.clone()), + email: primary_email, avatar_url: profile.avatar_url, - user_url: format!("https://github.com/{}", profile.login), - github_id: profile.id, - exp: (now + chrono::Duration::days(30)).timestamp(), + user_url: format!("https://github.com/{}", profile.login), + github_id: profile.id, + exp: (now + chrono::Duration::days(30)).timestamp(), }; info!(login = %session.login, "OAuth login succeeded"); @@ -476,11 +478,11 @@ async fn auth_me(State(state): State>, headers: HeaderMap) -> Resp .is_some_and(|cookie| cookie.value() == "1"); Json(AuthMeResponse { user: SessionUser { - login: session.login, - name: session.name, - email: session.email, + login: session.login, + name: session.name, + email: session.email, avatar_url: session.avatar_url, - user_url: session.user_url, + user_url: session.user_url, }, provider: "github", demo_mode, @@ -745,11 +747,11 @@ mod tests { fn sample_conversion() -> GitHubManifestConversion { GitHubManifestConversion { - id: 123, - slug: "fabro".to_string(), - client_id: "abc".to_string(), - client_secret: "shh".to_string(), - pem: String::new(), + id: 123, + slug: "fabro".to_string(), + client_id: "abc".to_string(), + client_secret: "shh".to_string(), + pem: String::new(), webhook_secret: None, } } diff --git a/lib/crates/fabro-server/tests/it/api/mtls.rs b/lib/crates/fabro-server/tests/it/api/mtls.rs index 34e353eee..4af5c7045 100644 --- a/lib/crates/fabro-server/tests/it/api/mtls.rs +++ b/lib/crates/fabro-server/tests/it/api/mtls.rs @@ -19,20 +19,20 @@ fn fixture_path(name: &str) -> PathBuf { fn fixture_pki() -> PkiPaths { PkiPaths { - ca_cert: fixture_path("ca.crt"), + ca_cert: fixture_path("ca.crt"), server_cert: fixture_path("server.crt"), - server_key: fixture_path("server.key"), + server_key: fixture_path("server.key"), client_cert: fixture_path("client.crt"), - client_key: fixture_path("client.key"), + client_key: fixture_path("client.key"), } } struct PkiPaths { - ca_cert: PathBuf, + ca_cert: PathBuf, server_cert: PathBuf, - server_key: PathBuf, + server_key: PathBuf, client_cert: PathBuf, - client_key: PathBuf, + client_key: PathBuf, } /// Start a TLS server on a random port, returning the bound address. @@ -94,8 +94,8 @@ async fn mtls_accepts_valid_client_cert() { let tls_settings = TlsSettings { cert: pki.server_cert.clone(), - key: pki.server_key.clone(), - ca: pki.ca_cert.clone(), + key: pki.server_key.clone(), + ca: pki.ca_cert.clone(), }; let auth_mode = AuthMode::Strategies(vec![AuthStrategy::Mtls]); @@ -119,8 +119,8 @@ async fn mtls_only_rejects_wrong_ca_client_cert() { let tls_settings = TlsSettings { cert: pki.server_cert.clone(), - key: pki.server_key.clone(), - ca: pki.ca_cert.clone(), + key: pki.server_key.clone(), + ca: pki.ca_cert.clone(), }; let auth_mode = AuthMode::Strategies(vec![AuthStrategy::Mtls]); @@ -157,8 +157,8 @@ async fn mtls_only_rejects_no_client_cert() { let tls_settings = TlsSettings { cert: pki.server_cert.clone(), - key: pki.server_key.clone(), - ca: pki.ca_cert.clone(), + key: pki.server_key.clone(), + ca: pki.ca_cert.clone(), }; // mTLS is the ONLY strategy -> client cert is required at TLS level @@ -213,18 +213,21 @@ async fn mtls_and_jwt_accepts_valid_jwt_without_client_cert() { let tls_settings = TlsSettings { cert: pki.server_cert.clone(), - key: pki.server_key.clone(), - ca: pki.ca_cert.clone(), + key: pki.server_key.clone(), + ca: pki.ca_cert.clone(), }; let (encoding_key, decoding_key) = fixture_jwt_keypair(); // Both mTLS and JWT strategies; mTLS is optional since JWT is also present - let auth_mode = AuthMode::Strategies(vec![AuthStrategy::Mtls, AuthStrategy::Jwt { - key: Arc::new(decoding_key), - validation: Arc::new(fabro_server::jwt_auth::jwt_validation()), - allowed_usernames: vec!["brynary".to_string()], - }]); + let auth_mode = AuthMode::Strategies(vec![ + AuthStrategy::Mtls, + AuthStrategy::Jwt { + key: Arc::new(decoding_key), + validation: Arc::new(fabro_server::jwt_auth::jwt_validation()), + allowed_usernames: vec!["brynary".to_string()], + }, + ]); let addr = start_tls_server(&tls_settings, ClientAuth::Optional, auth_mode).await; // Client trusts the server CA but presents NO client cert diff --git a/lib/crates/fabro-server/tests/it/scenario/lifecycle.rs b/lib/crates/fabro-server/tests/it/scenario/lifecycle.rs index 3bba3c4ec..5e5bc26ae 100644 --- a/lib/crates/fabro-server/tests/it/scenario/lifecycle.rs +++ b/lib/crates/fabro-server/tests/it/scenario/lifecycle.rs @@ -280,8 +280,11 @@ async fn cancel_at_human_gate_persists_cancelled_terminal_event() { }) .collect::>(); - assert_eq!(failed_reasons, vec![( - Some("cancelled".to_string()), - Some("Pipeline cancelled".to_string()) - )]); + assert_eq!( + failed_reasons, + vec![( + Some("cancelled".to_string()), + Some("Pipeline cancelled".to_string()) + )] + ); } diff --git a/lib/crates/fabro-slack/src/blocks.rs b/lib/crates/fabro-slack/src/blocks.rs index 7211b2322..784273654 100644 --- a/lib/crates/fabro-slack/src/blocks.rs +++ b/lib/crates/fabro-slack/src/blocks.rs @@ -65,8 +65,8 @@ pub fn question_to_blocks(run_id: &str, question_id: &str, question: &Question) &opt.label, &encode_action_value(&SlackActionPayload::Selected { run_id: run_id.to_string(), - qid: question_id.to_string(), - key: opt.key.clone(), + qid: question_id.to_string(), + key: opt.key.clone(), }), ANSWER_ACTION_ID, ) @@ -165,15 +165,15 @@ mod tests { let mut q = Question::new("Pick a language:", QuestionType::MultipleChoice); q.options = vec![ QuestionOption { - key: "rs".to_string(), + key: "rs".to_string(), label: "Rust".to_string(), }, QuestionOption { - key: "ts".to_string(), + key: "ts".to_string(), label: "TypeScript".to_string(), }, QuestionOption { - key: "py".to_string(), + key: "py".to_string(), label: "Python".to_string(), }, ]; @@ -251,11 +251,11 @@ mod tests { let mut q = Question::new("Select features:", QuestionType::MultiSelect); q.options = vec![ QuestionOption { - key: "a".to_string(), + key: "a".to_string(), label: "Auth".to_string(), }, QuestionOption { - key: "b".to_string(), + key: "b".to_string(), label: "Billing".to_string(), }, ]; diff --git a/lib/crates/fabro-slack/src/client.rs b/lib/crates/fabro-slack/src/client.rs index 1fcd0d374..9ab5ca506 100644 --- a/lib/crates/fabro-slack/src/client.rs +++ b/lib/crates/fabro-slack/src/client.rs @@ -7,14 +7,14 @@ const SLACK_API_BASE: &str = "https://slack.com/api"; #[derive(Debug, Clone)] pub struct PostedMessage { pub channel_id: String, - pub ts: String, + pub ts: String, } #[derive(Clone)] pub struct SlackClient { bot_token: String, - api_base: String, - http: Client, + api_base: String, + http: Client, } impl SlackClient { @@ -110,7 +110,7 @@ pub fn parse_post_message_response(response: &Value) -> Result Option { } pub struct SlackRuntimeOptions { - pub config: SlackOptions, + pub config: SlackOptions, pub credentials: SlackCredentials, } diff --git a/lib/crates/fabro-slack/src/dispatch.rs b/lib/crates/fabro-slack/src/dispatch.rs index b0c0f9f8b..37832ada9 100644 --- a/lib/crates/fabro-slack/src/dispatch.rs +++ b/lib/crates/fabro-slack/src/dispatch.rs @@ -35,7 +35,7 @@ pub fn dispatch(envelope: &SocketEnvelope, thread_registry: &ThreadRegistry) -> }; DispatchAction::SubmitAnswer(SlackAnswerSubmission { run_id: question_ref.run_id, - qid: question_ref.qid, + qid: question_ref.qid, answer: fabro_interview::Answer::text(text), }) } @@ -55,8 +55,8 @@ mod tests { let registry = ThreadRegistry::new(); let envelope = SocketEnvelope { envelope_type: "hello".to_string(), - envelope_id: None, - payload: None, + envelope_id: None, + payload: None, }; let action = dispatch(&envelope, ®istry); assert!(matches!(action, DispatchAction::Connected)); @@ -67,8 +67,8 @@ mod tests { let registry = ThreadRegistry::new(); let envelope = SocketEnvelope { envelope_type: "interactive".to_string(), - envelope_id: Some("env-1".to_string()), - payload: Some(serde_json::json!({ + envelope_id: Some("env-1".to_string()), + payload: Some(serde_json::json!({ "type": "block_actions", "actions": [{ "action_id": "interview.answer", @@ -93,8 +93,8 @@ mod tests { let registry = ThreadRegistry::new(); let envelope = SocketEnvelope { envelope_type: "interactive".to_string(), - envelope_id: Some("env-2".to_string()), - payload: Some(serde_json::json!({ + envelope_id: Some("env-2".to_string()), + payload: Some(serde_json::json!({ "type": "view_submission" })), }; @@ -107,8 +107,8 @@ mod tests { let registry = ThreadRegistry::new(); let envelope = SocketEnvelope { envelope_type: "interactive".to_string(), - envelope_id: Some("env-3".to_string()), - payload: None, + envelope_id: Some("env-3".to_string()), + payload: None, }; let action = dispatch(&envelope, ®istry); assert!(matches!(action, DispatchAction::Ignored)); @@ -119,8 +119,8 @@ mod tests { let registry = ThreadRegistry::new(); let envelope = SocketEnvelope { envelope_type: "disconnect".to_string(), - envelope_id: None, - payload: None, + envelope_id: None, + payload: None, }; let action = dispatch(&envelope, ®istry); assert!(matches!(action, DispatchAction::Reconnect)); @@ -131,8 +131,8 @@ mod tests { let registry = ThreadRegistry::new(); let envelope = SocketEnvelope { envelope_type: "events_api".to_string(), - envelope_id: Some("env-4".to_string()), - payload: Some(serde_json::json!({ + envelope_id: Some("env-4".to_string()), + payload: Some(serde_json::json!({ "event": { "type": "app_mention", "text": "hello" } })), }; @@ -146,8 +146,8 @@ mod tests { registry.register("1234.5678", "run-10", "q-10"); let envelope = SocketEnvelope { envelope_type: "events_api".to_string(), - envelope_id: Some("env-5".to_string()), - payload: Some(serde_json::json!({ + envelope_id: Some("env-5".to_string()), + payload: Some(serde_json::json!({ "event": { "type": "message", "text": "https://github.com/org/repo", @@ -175,8 +175,8 @@ mod tests { let registry = ThreadRegistry::new(); let envelope = SocketEnvelope { envelope_type: "events_api".to_string(), - envelope_id: Some("env-6".to_string()), - payload: Some(serde_json::json!({ + envelope_id: Some("env-6".to_string()), + payload: Some(serde_json::json!({ "event": { "type": "message", "text": "some reply", @@ -194,8 +194,8 @@ mod tests { let registry = ThreadRegistry::new(); let envelope = SocketEnvelope { envelope_type: "weird_type".to_string(), - envelope_id: None, - payload: None, + envelope_id: None, + payload: None, }; let action = dispatch(&envelope, ®istry); assert!(matches!(action, DispatchAction::Ignored)); diff --git a/lib/crates/fabro-slack/src/interaction.rs b/lib/crates/fabro-slack/src/interaction.rs index 6a75c3c01..023292c56 100644 --- a/lib/crates/fabro-slack/src/interaction.rs +++ b/lib/crates/fabro-slack/src/interaction.rs @@ -28,9 +28,9 @@ pub fn parse_interaction(payload: &Value) -> Option { SlackActionPayload::Yes { .. } => Answer::yes(), SlackActionPayload::No { .. } => Answer::no(), SlackActionPayload::Selected { key, .. } => Answer { - value: fabro_interview::AnswerValue::Selected(key), + value: fabro_interview::AnswerValue::Selected(key), selected_option: None, - text: None, + text: None, }, SlackActionPayload::SubmitMulti { .. } => return None, }, diff --git a/lib/crates/fabro-slack/src/payload.rs b/lib/crates/fabro-slack/src/payload.rs index 12846658e..c8cf2035b 100644 --- a/lib/crates/fabro-slack/src/payload.rs +++ b/lib/crates/fabro-slack/src/payload.rs @@ -4,13 +4,13 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SlackQuestionRef { pub run_id: String, - pub qid: String, + pub qid: String, } #[derive(Debug, Clone)] pub struct SlackAnswerSubmission { pub run_id: String, - pub qid: String, + pub qid: String, pub answer: Answer, } @@ -19,20 +19,20 @@ pub struct SlackAnswerSubmission { pub enum SlackActionPayload { Yes { run_id: String, - qid: String, + qid: String, }, No { run_id: String, - qid: String, + qid: String, }, Selected { run_id: String, - qid: String, - key: String, + qid: String, + key: String, }, SubmitMulti { run_id: String, - qid: String, + qid: String, }, } @@ -45,7 +45,7 @@ impl SlackActionPayload { | Self::Selected { run_id, qid, .. } | Self::SubmitMulti { run_id, qid } => SlackQuestionRef { run_id: run_id.clone(), - qid: qid.clone(), + qid: qid.clone(), }, } } @@ -64,8 +64,8 @@ mod tests { fn action_payload_serializes_run_id_and_qid() { let payload = SlackActionPayload::Selected { run_id: "run_123".to_string(), - qid: "q_123".to_string(), - key: "approve".to_string(), + qid: "q_123".to_string(), + key: "approve".to_string(), }; let json = encode_action_value(&payload); assert_eq!( diff --git a/lib/crates/fabro-slack/src/socket.rs b/lib/crates/fabro-slack/src/socket.rs index f99cc1c53..82253796e 100644 --- a/lib/crates/fabro-slack/src/socket.rs +++ b/lib/crates/fabro-slack/src/socket.rs @@ -5,8 +5,8 @@ use serde_json::Value; pub struct SocketEnvelope { #[serde(rename = "type")] pub envelope_type: String, - pub envelope_id: Option, - pub payload: Option, + pub envelope_id: Option, + pub payload: Option, } #[derive(Debug, Clone, Serialize)] @@ -104,8 +104,8 @@ mod tests { fn classify_hello() { let envelope = SocketEnvelope { envelope_type: "hello".to_string(), - envelope_id: None, - payload: None, + envelope_id: None, + payload: None, }; assert_eq!(classify_envelope(&envelope), SocketEventKind::Hello); } @@ -114,8 +114,8 @@ mod tests { fn classify_interactive() { let envelope = SocketEnvelope { envelope_type: "interactive".to_string(), - envelope_id: Some("e1".to_string()), - payload: Some(serde_json::json!({"type": "block_actions"})), + envelope_id: Some("e1".to_string()), + payload: Some(serde_json::json!({"type": "block_actions"})), }; assert_eq!(classify_envelope(&envelope), SocketEventKind::Interactive); } @@ -124,8 +124,8 @@ mod tests { fn classify_events_api() { let envelope = SocketEnvelope { envelope_type: "events_api".to_string(), - envelope_id: Some("e2".to_string()), - payload: Some(serde_json::json!({"event": {}})), + envelope_id: Some("e2".to_string()), + payload: Some(serde_json::json!({"event": {}})), }; assert_eq!(classify_envelope(&envelope), SocketEventKind::EventsApi); } @@ -134,8 +134,8 @@ mod tests { fn classify_disconnect() { let envelope = SocketEnvelope { envelope_type: "disconnect".to_string(), - envelope_id: None, - payload: None, + envelope_id: None, + payload: None, }; assert_eq!(classify_envelope(&envelope), SocketEventKind::Disconnect); } @@ -144,8 +144,8 @@ mod tests { fn classify_unknown() { let envelope = SocketEnvelope { envelope_type: "something_else".to_string(), - envelope_id: None, - payload: None, + envelope_id: None, + payload: None, }; assert_eq!(classify_envelope(&envelope), SocketEventKind::Unknown); } diff --git a/lib/crates/fabro-slack/src/threads.rs b/lib/crates/fabro-slack/src/threads.rs index de14dfde1..b83ea8a62 100644 --- a/lib/crates/fabro-slack/src/threads.rs +++ b/lib/crates/fabro-slack/src/threads.rs @@ -19,10 +19,13 @@ impl ThreadRegistry { self.ts_to_question .lock() .expect("thread registry lock poisoned") - .insert(message_ts.to_string(), SlackQuestionRef { - run_id: run_id.to_string(), - qid: question_id.to_string(), - }); + .insert( + message_ts.to_string(), + SlackQuestionRef { + run_id: run_id.to_string(), + qid: question_id.to_string(), + }, + ); } pub fn resolve(&self, thread_ts: &str) -> Option { @@ -84,7 +87,7 @@ mod tests { registry.resolve("1234.5678"), Some(SlackQuestionRef { run_id: "run-1".to_string(), - qid: "q-1".to_string(), + qid: "q-1".to_string(), }) ); } diff --git a/lib/crates/fabro-store/src/artifact_store.rs b/lib/crates/fabro-store/src/artifact_store.rs index 21ad7360d..9181b31e5 100644 --- a/lib/crates/fabro-store/src/artifact_store.rs +++ b/lib/crates/fabro-store/src/artifact_store.rs @@ -9,7 +9,7 @@ use object_store::path::Path as ObjectPath; use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, percent_decode_str, utf8_percent_encode}; use tokio::io::AsyncWriteExt; -use crate::{Result, StageId, StoreError}; +use crate::{Error, Result, StageId}; const ARTIFACT_SEGMENT_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.').remove(b'_').remove(b'-'); @@ -17,15 +17,15 @@ const STREAM_BUFFER_BYTES: usize = 1024 * 1024; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct NodeArtifact { - pub node: StageId, + pub node: StageId, pub filename: String, - pub size: u64, + pub size: u64, } #[derive(Clone)] pub struct ArtifactStore { object_store: Arc, - prefix: ObjectPath, + prefix: ObjectPath, } impl std::fmt::Debug for ArtifactStore { @@ -84,12 +84,12 @@ impl ArtifactStore { writer .write_all(&chunk) .await - .map_err(|err| StoreError::Other(format!("artifact write failed: {err}")))?; + .map_err(|err| Error::Other(format!("artifact write failed: {err}")))?; } writer .shutdown() .await - .map_err(|err| StoreError::Other(format!("artifact finalize failed: {err}")))?; + .map_err(|err| Error::Other(format!("artifact finalize failed: {err}")))?; Ok(()) } @@ -177,13 +177,13 @@ impl ArtifactStore { fn validate_filename_segments(filename: &str) -> Result> { if filename.contains('\\') { - return Err(StoreError::Other( + return Err(Error::Other( "artifact filename must not contain backslashes".to_string(), )); } let segments = filename.split('/').collect::>(); if segments.is_empty() || segments.iter().any(|segment| segment.is_empty()) { - return Err(StoreError::Other( + return Err(Error::Other( "artifact filename must be a non-empty relative path".to_string(), )); } @@ -191,7 +191,7 @@ fn validate_filename_segments(filename: &str) -> Result> { .iter() .any(|segment| matches!(*segment, "." | "..")) { - return Err(StoreError::Other( + return Err(Error::Other( "artifact filename must not contain '.' or '..' segments".to_string(), )); } @@ -206,7 +206,7 @@ fn decode_path_segment(kind: &str, value: &str) -> Result { percent_decode_str(value) .decode_utf8() .map(std::borrow::Cow::into_owned) - .map_err(|err| StoreError::Other(format!("invalid {kind}: {err}"))) + .map_err(|err| Error::Other(format!("invalid {kind}: {err}"))) } fn decode_artifact_location( @@ -215,23 +215,23 @@ fn decode_artifact_location( size: u64, ) -> Result { let mut parts = location.prefix_match(prefix).ok_or_else(|| { - StoreError::Other(format!( + Error::Other(format!( "artifact location {location} does not match expected prefix {prefix}" )) })?; let stage_part = parts.next().ok_or_else(|| { - StoreError::Other(format!( + Error::Other(format!( "artifact location {location} is missing a stage segment" )) })?; let (encoded_node_id, visit) = stage_part.as_ref().rsplit_once('@').ok_or_else(|| { - StoreError::Other(format!( + Error::Other(format!( "artifact location {location} has an invalid stage segment" )) })?; let node_id = decode_path_segment("artifact node id", encoded_node_id)?; let visit = visit.parse::().map_err(|err| { - StoreError::Other(format!( + Error::Other(format!( "artifact location {location} has an invalid visit number: {err}" )) })?; @@ -239,7 +239,7 @@ fn decode_artifact_location( .map(|part| decode_path_segment("artifact filename segment", part.as_ref())) .collect::>>()?; if filename_segments.is_empty() { - return Err(StoreError::Other(format!( + return Err(Error::Other(format!( "artifact location {location} is missing a filename" ))); } @@ -252,7 +252,7 @@ fn decode_artifact_location( fn decode_filename(prefix: &ObjectPath, location: &ObjectPath) -> Result { let mut parts = location.prefix_match(prefix).ok_or_else(|| { - StoreError::Other(format!( + Error::Other(format!( "artifact location {location} does not match expected prefix {prefix}" )) })?; @@ -261,7 +261,7 @@ fn decode_filename(prefix: &ObjectPath, location: &ObjectPath) -> Result .map(|part| decode_path_segment("artifact filename segment", part.as_ref())) .collect::>>()?; if filename_segments.is_empty() { - return Err(StoreError::Other(format!( + return Err(Error::Other(format!( "artifact location {location} is missing a filename" ))); } @@ -270,7 +270,7 @@ fn decode_filename(prefix: &ObjectPath, location: &ObjectPath) -> Result fn parse_object_path(raw: &str) -> Result { ObjectPath::parse(raw) - .map_err(|err| StoreError::Other(format!("invalid artifact object path {raw:?}: {err}"))) + .map_err(|err| Error::Other(format!("invalid artifact object path {raw:?}: {err}"))) } #[cfg(test)] @@ -299,16 +299,18 @@ mod tests { store.get(&run_id, &node, filename).await.unwrap(), Some(Bytes::from_static(b"hello")) ); - assert_eq!(store.list_for_node(&run_id, &node).await.unwrap(), vec![ - filename.to_string() - ]); - assert_eq!(store.list_for_run(&run_id).await.unwrap(), vec![ - NodeArtifact { + assert_eq!( + store.list_for_node(&run_id, &node).await.unwrap(), + vec![filename.to_string()] + ); + assert_eq!( + store.list_for_run(&run_id).await.unwrap(), + vec![NodeArtifact { node, filename: filename.to_string(), size: 5, - } - ]); + }] + ); } #[tokio::test] diff --git a/lib/crates/fabro-store/src/error.rs b/lib/crates/fabro-store/src/error.rs index fd5f4cc3b..f73aa472a 100644 --- a/lib/crates/fabro-store/src/error.rs +++ b/lib/crates/fabro-store/src/error.rs @@ -1,5 +1,4 @@ pub type Result = std::result::Result; -pub type StoreError = Error; #[derive(Debug, thiserror::Error)] pub enum Error { diff --git a/lib/crates/fabro-store/src/lib.rs b/lib/crates/fabro-store/src/lib.rs index 9df7b83e9..f3ec04e67 100644 --- a/lib/crates/fabro-store/src/lib.rs +++ b/lib/crates/fabro-store/src/lib.rs @@ -8,7 +8,7 @@ mod slate; mod types; pub use artifact_store::{ArtifactStore, NodeArtifact}; -pub use error::{Error, Result, StoreError}; +pub use error::{Error, Result}; pub use fabro_types::{RunBlobId, StageId}; pub use run_state::{NodeState, PendingInterviewRecord, RunProjection}; pub use slate::{Database, RunDatabase, Runs}; @@ -17,5 +17,5 @@ pub use types::{EventEnvelope, EventPayload, RunSummary}; #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct ListRunsQuery { pub start: Option>, - pub end: Option>, + pub end: Option>, } diff --git a/lib/crates/fabro-store/src/run_state.rs b/lib/crates/fabro-store/src/run_state.rs index 47a02aa01..4c73de2fc 100644 --- a/lib/crates/fabro-store/src/run_state.rs +++ b/lib/crates/fabro-store/src/run_state.rs @@ -15,53 +15,53 @@ use fabro_types::{ }; use serde_json::Value; -use crate::{EventEnvelope, Result, RunSummary, StageId, StoreError}; +use crate::{Error, EventEnvelope, Result, RunSummary, StageId}; #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[serde(default)] pub struct RunProjection { - pub run: Option, - pub graph_source: Option, - pub start: Option, - pub status: Option, - pub pending_control: Option, - pub checkpoint: Option, - pub checkpoints: Vec<(u32, Checkpoint)>, - pub conclusion: Option, - pub retro: Option, - pub retro_prompt: Option, - pub retro_response: Option, - pub sandbox: Option, - pub final_patch: Option, - pub pull_request: Option, + pub run: Option, + pub graph_source: Option, + pub start: Option, + pub status: Option, + pub pending_control: Option, + pub checkpoint: Option, + pub checkpoints: Vec<(u32, Checkpoint)>, + pub conclusion: Option, + pub retro: Option, + pub retro_prompt: Option, + pub retro_response: Option, + pub sandbox: Option, + pub final_patch: Option, + pub pull_request: Option, pub pending_interviews: BTreeMap, - nodes: HashMap, + nodes: HashMap, } #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] pub struct PendingInterviewRecord { - pub question: InterviewQuestionRecord, + pub question: InterviewQuestionRecord, pub started_at: Option>, } #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] pub struct NodeState { - pub prompt: Option, - pub response: Option, - pub status: Option, - pub provider_used: Option, - pub diff: Option, + pub prompt: Option, + pub response: Option, + pub status: Option, + pub provider_used: Option, + pub diff: Option, pub script_invocation: Option, - pub script_timing: Option, - pub parallel_results: Option, - pub stdout: Option, - pub stderr: Option, + pub script_timing: Option, + pub parallel_results: Option, + pub stdout: Option, + pub stderr: Option, } #[derive(Debug, Clone, Default)] pub(crate) struct EventProjectionCache { pub last_seq: u32, - pub state: RunProjection, + pub state: RunProjection, } impl RunProjection { @@ -75,7 +75,7 @@ impl RunProjection { pub fn apply_event(&mut self, event: &EventEnvelope) -> Result<()> { let stored = RunEvent::from_ref(event.payload.as_value()) - .map_err(|err| StoreError::InvalidEvent(format!("invalid stored event: {err}")))?; + .map_err(|err| Error::InvalidEvent(format!("invalid stored event: {err}")))?; let ts = stored.ts; let run_id = stored.run_id; @@ -172,11 +172,11 @@ impl RunProjection { } EventBody::SandboxInitialized(props) => { self.sandbox = Some(SandboxRecord { - provider: props.provider.clone(), - working_directory: props.working_directory.clone(), - identifier: props.identifier.clone(), + provider: props.provider.clone(), + working_directory: props.working_directory.clone(), + identifier: props.identifier.clone(), host_working_directory: props.host_working_directory.clone(), - container_mount_point: props.container_mount_point.clone(), + container_mount_point: props.container_mount_point.clone(), }); } EventBody::RetroStarted(props) => { @@ -189,41 +189,41 @@ impl RunProjection { .clone() .map(serde_json::from_value) .transpose() - .map_err(|err| { - StoreError::InvalidEvent(format!("invalid retro payload: {err}")) - })?; + .map_err(|err| Error::InvalidEvent(format!("invalid retro payload: {err}")))?; } EventBody::PullRequestCreated(props) => { self.pull_request = Some(PullRequestRecord { - html_url: props.pr_url.clone(), - number: props.pr_number, - owner: props.owner.clone(), - repo: props.repo.clone(), + html_url: props.pr_url.clone(), + number: props.pr_number, + owner: props.owner.clone(), + repo: props.repo.clone(), base_branch: props.base_branch.clone(), head_branch: props.head_branch.clone(), - title: props.title.clone(), + title: props.title.clone(), }); } EventBody::InterviewStarted(props) => { if props.question_id.is_empty() { return Ok(()); } - self.pending_interviews - .insert(props.question_id.clone(), PendingInterviewRecord { - question: InterviewQuestionRecord { - id: props.question_id.clone(), - text: props.question.clone(), - stage: props.stage.clone(), - question_type: InterviewQuestionType::from_wire_name( + self.pending_interviews.insert( + props.question_id.clone(), + PendingInterviewRecord { + question: InterviewQuestionRecord { + id: props.question_id.clone(), + text: props.question.clone(), + stage: props.stage.clone(), + question_type: InterviewQuestionType::from_wire_name( &props.question_type, ), - options: props.options.clone(), - allow_freeform: props.allow_freeform, + options: props.options.clone(), + allow_freeform: props.allow_freeform, timeout_seconds: props.timeout_seconds, context_display: props.context_display.clone(), }, started_at: Some(ts), - }); + }, + ); } EventBody::InterviewCompleted(props) => { if !props.question_id.is_empty() { @@ -302,7 +302,7 @@ impl RunProjection { let visit = self.current_visit_for(node_id).unwrap_or(1); self.node_mut(node_id, visit).script_invocation = Some(serde_json::to_value(props).map_err(|err| { - StoreError::InvalidEvent(format!("invalid command.started payload: {err}")) + Error::InvalidEvent(format!("invalid command.started payload: {err}")) })?); } EventBody::CommandCompleted(props) => { @@ -314,7 +314,7 @@ impl RunProjection { node.stdout = Some(props.stdout.clone()); node.stderr = Some(props.stderr.clone()); node.script_timing = Some(serde_json::to_value(props).map_err(|err| { - StoreError::InvalidEvent(format!("invalid command.completed payload: {err}")) + Error::InvalidEvent(format!("invalid command.completed payload: {err}")) })?); } EventBody::ParallelCompleted(props) => { @@ -324,9 +324,7 @@ impl RunProjection { let visit = self.current_visit_for(node_id).unwrap_or(1); self.node_mut(node_id, visit).parallel_results = Some(serde_json::to_value(&props.results).map_err(|err| { - StoreError::InvalidEvent(format!( - "invalid parallel.completed payload: {err}" - )) + Error::InvalidEvent(format!("invalid parallel.completed payload: {err}")) })?); } _ => {} @@ -478,9 +476,8 @@ fn conclusion_from_completed( ) -> Result { Ok(Conclusion { timestamp, - status: StageStatus::from_str(&props.status).map_err(|err| { - StoreError::InvalidEvent(format!("invalid completed stage status: {err}")) - })?, + status: StageStatus::from_str(&props.status) + .map_err(|err| Error::InvalidEvent(format!("invalid completed stage status: {err}")))?, duration_ms: props.duration_ms, failure_reason: None, final_git_commit_sha: props.final_git_commit_sha.clone(), @@ -516,21 +513,21 @@ fn stage_visit( fn stage_outcome_from_props(props: &StageCompletedProps) -> Outcome> { Outcome { - status: props.status.clone(), - preferred_label: props.preferred_label.clone(), + status: props.status.clone(), + preferred_label: props.preferred_label.clone(), suggested_next_ids: props.suggested_next_ids.clone(), - context_updates: props + context_updates: props .context_updates .clone() .unwrap_or_default() .into_iter() .collect(), - jump_to_node: props.jump_to_node.clone(), - notes: props.notes.clone(), - failure: props.failure.clone(), - usage: props.billing.clone(), - files_touched: props.files_touched.clone(), - duration_ms: Some(props.duration_ms), + jump_to_node: props.jump_to_node.clone(), + notes: props.notes.clone(), + failure: props.failure.clone(), + usage: props.billing.clone(), + files_touched: props.files_touched.clone(), + duration_ms: Some(props.duration_ms), } } @@ -687,25 +684,31 @@ mod tests { fn set_node_round_trips_through_json() { let mut state = RunProjection { pending_control: Some(RunControlAction::Unpause), - checkpoints: vec![(7, Checkpoint { - timestamp: "2026-04-07T12:00:00Z".parse().unwrap(), - current_node: "build".to_string(), - completed_nodes: vec!["build".to_string()], - node_retries: HashMap::new(), - context_values: HashMap::new(), - node_outcomes: HashMap::new(), - next_node_id: None, - git_commit_sha: None, - loop_failure_signatures: HashMap::new(), - restart_failure_signatures: HashMap::new(), - node_visits: HashMap::from([("build".to_string(), 2usize)]), - })], + checkpoints: vec![( + 7, + Checkpoint { + timestamp: "2026-04-07T12:00:00Z".parse().unwrap(), + current_node: "build".to_string(), + completed_nodes: vec!["build".to_string()], + node_retries: HashMap::new(), + context_values: HashMap::new(), + node_outcomes: HashMap::new(), + next_node_id: None, + git_commit_sha: None, + loop_failure_signatures: HashMap::new(), + restart_failure_signatures: HashMap::new(), + node_visits: HashMap::from([("build".to_string(), 2usize)]), + }, + )], ..RunProjection::default() }; - state.set_node(StageId::new("build", 2), NodeState { - stdout: Some("done".to_string()), - ..NodeState::default() - }); + state.set_node( + StageId::new("build", 2), + NodeState { + stdout: Some("done".to_string()), + ..NodeState::default() + }, + ); let round_tripped: RunProjection = serde_json::from_value(serde_json::to_value(&state).unwrap()).unwrap(); @@ -732,21 +735,21 @@ mod tests { .apply_event(&test_event( 1, EventBody::InterviewStarted(InterviewStartedProps { - question_id: "q-1".to_string(), - question: "Approve deploy?".to_string(), - stage: "gate".to_string(), - question_type: "multiple_choice".to_string(), - options: vec![ + question_id: "q-1".to_string(), + question: "Approve deploy?".to_string(), + stage: "gate".to_string(), + question_type: "multiple_choice".to_string(), + options: vec![ InterviewOption { - key: "approve".to_string(), + key: "approve".to_string(), label: "Approve".to_string(), }, InterviewOption { - key: "revise".to_string(), + key: "revise".to_string(), label: "Revise".to_string(), }, ], - allow_freeform: true, + allow_freeform: true, timeout_seconds: Some(30.0), context_display: Some("Latest draft".to_string()), }), @@ -777,8 +780,8 @@ mod tests { 2, EventBody::InterviewCompleted(InterviewCompletedProps { question_id: "q-1".to_string(), - question: "Approve deploy?".to_string(), - answer: "approve".to_string(), + question: "Approve deploy?".to_string(), + answer: "approve".to_string(), duration_ms: 42, }), Some("gate"), @@ -798,7 +801,7 @@ mod tests { RunBlobId::new(br#"{"version":1,"workflow_path":"workflow.fabro"}"#).to_string(); let events = vec![ EventEnvelope { - seq: 1, + seq: 1, payload: EventPayload::new( json!({ "id": "evt-run-created", @@ -824,7 +827,7 @@ mod tests { .unwrap(), }, EventEnvelope { - seq: 2, + seq: 2, payload: EventPayload::new( json!({ "id": "evt-run-submitted", diff --git a/lib/crates/fabro-store/src/slate/catalog.rs b/lib/crates/fabro-store/src/slate/catalog.rs index 1de9c6711..49181ac08 100644 --- a/lib/crates/fabro-store/src/slate/catalog.rs +++ b/lib/crates/fabro-store/src/slate/catalog.rs @@ -18,9 +18,8 @@ pub(crate) async fn list_run_ids(db: &Db, query: &ListRunsQuery) -> Result, - base_prefix: String, + object_store: Arc, + base_prefix: String, flush_interval: Duration, - db: Arc>, - active_runs: Arc>>>, + db: Arc>, + active_runs: Arc>>>, } impl std::fmt::Debug for Database { @@ -96,14 +96,14 @@ impl Database { if let Some(active) = self.get_active_run(run_id).await { if run_exists && !active.matches_run(run_id) { - return Err(StoreError::RunAlreadyExists(run_id.to_string())); + return Err(Error::RunAlreadyExists(run_id.to_string())); } catalog::write_index(&db, run_id).await?; return Ok(active); } if run_exists { - return Err(StoreError::RunAlreadyExists(run_id.to_string())); + return Err(Error::RunAlreadyExists(run_id.to_string())); } catalog::write_index(&db, run_id).await?; @@ -116,14 +116,14 @@ impl Database { let db = self.open_db().await?; if let Some(active) = self.get_active_run(run_id).await { if !active.matches_run(run_id) { - return Err(StoreError::Other(format!( + return Err(Error::Other(format!( "active run cache mismatch for run_id {run_id:?}" ))); } return Ok(active); } if !RunDatabase::has_any_events(&db, run_id).await? { - return Err(StoreError::RunNotFound(run_id.to_string())); + return Err(Error::RunNotFound(run_id.to_string())); } let run_store = RunDatabase::open_writer(*run_id, db).await?; self.cache_active_run(&run_store).await; @@ -134,14 +134,14 @@ impl Database { let db = self.open_db().await?; if let Some(active) = self.get_active_run(run_id).await { if !active.matches_run(run_id) { - return Err(StoreError::Other(format!( + return Err(Error::Other(format!( "active run cache mismatch for run_id {run_id:?}" ))); } return Ok(active.read_only_clone()); } if !RunDatabase::has_any_events(&db, run_id).await? { - return Err(StoreError::RunNotFound(run_id.to_string())); + return Err(Error::RunNotFound(run_id.to_string())); } RunDatabase::open_reader(*run_id, db).await } @@ -176,7 +176,7 @@ impl Database { let mut iter = db.scan_prefix(prefix.as_bytes()).await?; while let Some(entry) = iter.next().await? { keys_to_delete.push(String::from_utf8(entry.key.to_vec()).map_err(|err| { - StoreError::Other(format!("stored key is not valid UTF-8: {err}")) + Error::Other(format!("stored key is not valid UTF-8: {err}")) })?); } } @@ -206,7 +206,7 @@ impl Runs { pub async fn find(&self, run_id: &RunId) -> Result> { match self.db.open_run_reader(run_id).await { Ok(run_db) => Ok(Some(run_db.state().await?.build_summary(run_id))), - Err(StoreError::RunNotFound(_)) => Ok(None), + Err(Error::RunNotFound(_)) => Ok(None), Err(err) => Err(err), } } @@ -436,7 +436,7 @@ mod tests { )) .await .unwrap_err(); - assert!(matches!(err, StoreError::ReadOnly)); + assert!(matches!(err, Error::ReadOnly)); } #[tokio::test] diff --git a/lib/crates/fabro-store/src/slate/run_store.rs b/lib/crates/fabro-store/src/slate/run_store.rs index 12eb0e858..7d2134abb 100644 --- a/lib/crates/fabro-store/src/slate/run_store.rs +++ b/lib/crates/fabro-store/src/slate/run_store.rs @@ -11,12 +11,12 @@ use tokio::sync::{Mutex, broadcast, mpsc}; use tokio_stream::wrappers::UnboundedReceiverStream; use crate::run_state::EventProjectionCache; -use crate::{EventEnvelope, EventPayload, Result, RunProjection, RunSummary, StoreError, keys}; +use crate::{Error, EventEnvelope, EventPayload, Result, RunProjection, RunSummary, keys}; const DEFAULT_EVENT_TAIL_LIMIT: usize = 1024; #[derive(Clone)] pub struct RunDatabase { - inner: Arc, + inner: Arc, read_only: bool, } @@ -30,15 +30,15 @@ impl std::fmt::Debug for RunDatabase { } pub(crate) struct RunDatabaseInner { - run_id: RunId, - db: Db, - event_seq: AtomicU32, - close_lock: Mutex<()>, - state_lock: Mutex<()>, - projection_cache: Mutex, - recent_events: Mutex>, + run_id: RunId, + db: Db, + event_seq: AtomicU32, + close_lock: Mutex<()>, + state_lock: Mutex<()>, + projection_cache: Mutex, + recent_events: Mutex>, recent_event_limit: usize, - event_tx: broadcast::Sender, + event_tx: broadcast::Sender, } impl RunDatabase { @@ -51,7 +51,7 @@ impl RunDatabase { .await?; let (event_tx, _) = broadcast::channel(DEFAULT_EVENT_TAIL_LIMIT.max(16)); Ok(Self { - inner: Arc::new(RunDatabaseInner { + inner: Arc::new(RunDatabaseInner { run_id, db, event_seq: AtomicU32::new(event_seq), @@ -75,7 +75,7 @@ impl RunDatabase { .await?; let (event_tx, _) = broadcast::channel(DEFAULT_EVENT_TAIL_LIMIT.max(16)); Ok(Self { - inner: Arc::new(RunDatabaseInner { + inner: Arc::new(RunDatabaseInner { run_id, db, event_seq: AtomicU32::new(event_seq), @@ -99,7 +99,7 @@ impl RunDatabase { pub(crate) fn read_only_clone(&self) -> Self { Self { - inner: Arc::clone(&self.inner), + inner: Arc::clone(&self.inner), read_only: true, } } @@ -196,7 +196,7 @@ impl RunDatabase { impl RunDatabase { pub async fn append_event(&self, payload: &EventPayload) -> Result { if self.read_only { - return Err(StoreError::ReadOnly); + return Err(Error::ReadOnly); } payload.validate(&self.inner.run_id)?; let _state_guard = self.inner.state_lock.lock().await; @@ -292,7 +292,7 @@ impl RunDatabase { pub async fn write_blob(&self, data: &[u8]) -> Result { if self.read_only { - return Err(StoreError::ReadOnly); + return Err(Error::ReadOnly); } let id = RunBlobId::new(data); self.inner.db.put(keys::blob_key(&id), data).await?; @@ -389,7 +389,7 @@ where fn key_to_string(key: &Bytes) -> Result { String::from_utf8(key.to_vec()) - .map_err(|err| StoreError::Other(format!("stored key is not valid UTF-8: {err}"))) + .map_err(|err| Error::Other(format!("stored key is not valid UTF-8: {err}"))) } #[cfg(test)] diff --git a/lib/crates/fabro-store/src/types.rs b/lib/crates/fabro-store/src/types.rs index ca060837b..fe3fae6a8 100644 --- a/lib/crates/fabro-store/src/types.rs +++ b/lib/crates/fabro-store/src/types.rs @@ -4,21 +4,21 @@ use chrono::{DateTime, Utc}; use fabro_types::{RunControlAction, RunEvent, RunId, RunStatus, StatusReason}; use serde::{Deserialize, Serialize}; -use crate::{Result, StoreError}; +use crate::{Error, Result}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RunSummary { - pub run_id: RunId, - pub workflow_name: Option, - pub workflow_slug: Option, - pub goal: Option, - pub labels: HashMap, - pub host_repo_path: Option, - pub start_time: Option>, - pub status: Option, - pub status_reason: Option, - pub pending_control: Option, - pub duration_ms: Option, + pub run_id: RunId, + pub workflow_name: Option, + pub workflow_slug: Option, + pub goal: Option, + pub labels: HashMap, + pub host_repo_path: Option, + pub start_time: Option>, + pub status: Option, + pub status_reason: Option, + pub pending_control: Option, + pub duration_ms: Option, pub total_usd_micros: Option, } @@ -34,15 +34,16 @@ impl EventPayload { } pub(crate) fn validate(&self, expected_run_id: &RunId) -> Result<()> { - let obj = self.0.as_object().ok_or_else(|| { - StoreError::InvalidEvent("event payload must be a JSON object".into()) - })?; + let obj = self + .0 + .as_object() + .ok_or_else(|| Error::InvalidEvent("event payload must be a JSON object".into()))?; for field in ["id", "ts", "run_id", "event"] { match obj.get(field) { Some(serde_json::Value::String(_)) => {} _ => { - return Err(StoreError::InvalidEvent(format!( + return Err(Error::InvalidEvent(format!( "missing or non-string required field: {field}" ))); } @@ -53,10 +54,10 @@ impl EventPayload { Some(serde_json::Value::String(run_id)) if run_id == &expected_run_id.to_string() => { Ok(()) } - Some(serde_json::Value::String(run_id)) => Err(StoreError::InvalidEvent(format!( + Some(serde_json::Value::String(run_id)) => Err(Error::InvalidEvent(format!( "payload run_id {run_id:?} does not match store run_id {expected_run_id:?}" ))), - _ => Err(StoreError::InvalidEvent( + _ => Err(Error::InvalidEvent( "missing or non-string required field: run_id".into(), )), } @@ -72,17 +73,17 @@ impl EventPayload { } impl TryFrom<&EventPayload> for RunEvent { - type Error = StoreError; + type Error = Error; fn try_from(value: &EventPayload) -> Result { Self::from_ref(value.as_value()) - .map_err(|err| StoreError::InvalidEvent(format!("invalid stored event: {err}"))) + .map_err(|err| Error::InvalidEvent(format!("invalid stored event: {err}"))) } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct EventEnvelope { - pub seq: u32, + pub seq: u32, #[serde(flatten)] pub payload: EventPayload, } @@ -98,27 +99,27 @@ mod tests { #[test] fn wire_event_envelope_round_trips() { let event = RunEvent { - id: "evt_1".to_string(), - ts: Utc.with_ymd_and_hms(2026, 4, 9, 12, 0, 0).unwrap(), - run_id: fixtures::RUN_1, - node_id: Some("code".to_string()), - node_label: Some("Code".to_string()), - stage_id: Some(StageId::new("code", 1)), - parallel_group_id: None, + id: "evt_1".to_string(), + ts: Utc.with_ymd_and_hms(2026, 4, 9, 12, 0, 0).unwrap(), + run_id: fixtures::RUN_1, + node_id: Some("code".to_string()), + node_label: Some("Code".to_string()), + stage_id: Some(StageId::new("code", 1)), + parallel_group_id: None, parallel_branch_id: None, - session_id: None, - parent_session_id: None, - tool_call_id: None, - actor: None, - body: EventBody::RunCompleted(RunCompletedProps { - duration_ms: 42, - artifact_count: 0, - status: "success".to_string(), - reason: None, - total_usd_micros: None, + session_id: None, + parent_session_id: None, + tool_call_id: None, + actor: None, + body: EventBody::RunCompleted(RunCompletedProps { + duration_ms: 42, + artifact_count: 0, + status: "success".to_string(), + reason: None, + total_usd_micros: None, final_git_commit_sha: None, - final_patch: None, - billing: None, + final_patch: None, + billing: None, }), }; let payload = EventPayload::new(event.to_value().unwrap(), &fixtures::RUN_1).unwrap(); @@ -139,30 +140,30 @@ mod tests { let group = StageId::new("review", 2); let branch = ParallelBranchId::new(group.clone(), 3); let event = RunEvent { - id: "evt_2".to_string(), - ts: Utc.with_ymd_and_hms(2026, 4, 9, 13, 0, 0).unwrap(), - run_id: fixtures::RUN_1, - node_id: Some("review".to_string()), - node_label: Some("Review".to_string()), - stage_id: Some(StageId::new("review", 2)), - parallel_group_id: Some(group), + id: "evt_2".to_string(), + ts: Utc.with_ymd_and_hms(2026, 4, 9, 13, 0, 0).unwrap(), + run_id: fixtures::RUN_1, + node_id: Some("review".to_string()), + node_label: Some("Review".to_string()), + stage_id: Some(StageId::new("review", 2)), + parallel_group_id: Some(group), parallel_branch_id: Some(branch), - session_id: Some("ses_42".to_string()), - parent_session_id: Some("ses_root".to_string()), - tool_call_id: Some("tool_call_xyz".to_string()), - actor: Some(ActorRef::agent( + session_id: Some("ses_42".to_string()), + parent_session_id: Some("ses_root".to_string()), + tool_call_id: Some("tool_call_xyz".to_string()), + actor: Some(ActorRef::agent( Some("ses_42".to_string()), Some("claude-sonnet".to_string()), )), - body: EventBody::RunCompleted(RunCompletedProps { - duration_ms: 100, - artifact_count: 1, - status: "success".to_string(), - reason: None, - total_usd_micros: None, + body: EventBody::RunCompleted(RunCompletedProps { + duration_ms: 100, + artifact_count: 1, + status: "success".to_string(), + reason: None, + total_usd_micros: None, final_git_commit_sha: None, - final_patch: None, - billing: None, + final_patch: None, + billing: None, }), }; let payload = EventPayload::new(event.to_value().unwrap(), &fixtures::RUN_1).unwrap(); diff --git a/lib/crates/fabro-telemetry/src/buffer.rs b/lib/crates/fabro-telemetry/src/buffer.rs index e5682e0ac..04d125993 100644 --- a/lib/crates/fabro-telemetry/src/buffer.rs +++ b/lib/crates/fabro-telemetry/src/buffer.rs @@ -6,14 +6,14 @@ use crate::event::Track; #[derive(Clone, Copy)] pub(crate) struct BufferPolicy { pub count_threshold: usize, - pub time_threshold: Duration, + pub time_threshold: Duration, } impl Default for BufferPolicy { fn default() -> Self { Self { count_threshold: 20, - time_threshold: Duration::from_secs(60), + time_threshold: Duration::from_secs(60), } } } @@ -70,13 +70,13 @@ mod tests { fn make_track(event: &str) -> Track { Track { - user: User::AnonymousId { + user: User::AnonymousId { anonymous_id: "test".to_string(), }, - event: event.to_string(), + event: event.to_string(), properties: json!({}), - context: None, - timestamp: None, + context: None, + timestamp: None, message_id: format!("msg-{event}"), } } @@ -98,7 +98,7 @@ mod tests { &rx, BufferPolicy { count_threshold: 2, - time_threshold: Duration::from_secs(60), + time_threshold: Duration::from_secs(60), }, move |tracks| { let events: Vec = tracks.iter().map(|t| t.event.clone()).collect(); @@ -133,7 +133,7 @@ mod tests { &rx, BufferPolicy { count_threshold: 2, - time_threshold: Duration::from_secs(60), + time_threshold: Duration::from_secs(60), }, move |_| { *mid.lock().unwrap() = true; @@ -164,7 +164,7 @@ mod tests { &rx, BufferPolicy { count_threshold: 100, // won't trigger - time_threshold: Duration::from_millis(50), + time_threshold: Duration::from_millis(50), }, move |tracks| { let events: Vec = tracks.iter().map(|t| t.event.clone()).collect(); @@ -206,7 +206,7 @@ mod tests { &rx, BufferPolicy { count_threshold: 100, // won't trigger - time_threshold: Duration::from_secs(60), + time_threshold: Duration::from_secs(60), }, move |tracks| { let events: Vec = tracks.iter().map(|t| t.event.clone()).collect(); diff --git a/lib/crates/fabro-telemetry/src/event.rs b/lib/crates/fabro-telemetry/src/event.rs index 1e98cab9d..dcdd0df33 100644 --- a/lib/crates/fabro-telemetry/src/event.rs +++ b/lib/crates/fabro-telemetry/src/event.rs @@ -4,13 +4,13 @@ use serde_json::Value; #[derive(Debug, Serialize, Deserialize)] pub struct Track { #[serde(flatten)] - pub user: User, - pub event: String, + pub user: User, + pub event: String, pub properties: Value, #[serde(skip_serializing_if = "Option::is_none")] - pub context: Option, + pub context: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub timestamp: Option, + pub timestamp: Option, #[serde(rename = "messageId")] pub message_id: String, } @@ -20,7 +20,7 @@ pub struct Track { pub enum User { Both { #[serde(rename = "userId")] - user_id: String, + user_id: String, #[serde(rename = "anonymousId")] anonymous_id: String, }, @@ -43,13 +43,13 @@ mod tests { #[test] fn track_serialization_anonymous() { let track = Track { - user: User::AnonymousId { + user: User::AnonymousId { anonymous_id: "abc-123".to_string(), }, - event: "Command Invoked".to_string(), + event: "Command Invoked".to_string(), properties: json!({"command": "run"}), - context: Some(json!({"os": {"name": "macos"}})), - timestamp: Some("2025-01-01T00:00:00Z".to_string()), + context: Some(json!({"os": {"name": "macos"}})), + timestamp: Some("2025-01-01T00:00:00Z".to_string()), message_id: "msg-1".to_string(), }; @@ -74,13 +74,13 @@ mod tests { #[test] fn track_serialization_user_id() { let track = Track { - user: User::UserId { + user: User::UserId { user_id: "user-456".to_string(), }, - event: "test".to_string(), + event: "test".to_string(), properties: json!({}), - context: None, - timestamp: None, + context: None, + timestamp: None, message_id: "msg-2".to_string(), }; @@ -97,14 +97,14 @@ mod tests { #[test] fn track_serialization_both() { let track = Track { - user: User::Both { - user_id: "user-456".to_string(), + user: User::Both { + user_id: "user-456".to_string(), anonymous_id: "abc-123".to_string(), }, - event: "test".to_string(), + event: "test".to_string(), properties: json!({}), - context: None, - timestamp: None, + context: None, + timestamp: None, message_id: "msg-3".to_string(), }; @@ -122,13 +122,13 @@ mod tests { #[test] fn track_round_trip() { let track = Track { - user: User::AnonymousId { + user: User::AnonymousId { anonymous_id: "abc".to_string(), }, - event: "test".to_string(), + event: "test".to_string(), properties: json!({"key": "value"}), - context: Some(json!({"app": {"name": "fabro"}})), - timestamp: Some("2025-06-01T12:00:00Z".to_string()), + context: Some(json!({"app": {"name": "fabro"}})), + timestamp: Some("2025-06-01T12:00:00Z".to_string()), message_id: "msg-rt".to_string(), }; diff --git a/lib/crates/fabro-telemetry/src/lib.rs b/lib/crates/fabro-telemetry/src/lib.rs index 0018c8c05..e19bf7222 100644 --- a/lib/crates/fabro-telemetry/src/lib.rs +++ b/lib/crates/fabro-telemetry/src/lib.rs @@ -24,11 +24,11 @@ pub enum TelemetryLevel { } struct Global { - sender: Mutex>>, + sender: Mutex>>, anonymous_id: String, - context: Value, - level: TelemetryLevel, - thread: Mutex>>, + context: Value, + level: TelemetryLevel, + thread: Mutex>>, } static GLOBAL: OnceLock = OnceLock::new(); diff --git a/lib/crates/fabro-telemetry/src/sender.rs b/lib/crates/fabro-telemetry/src/sender.rs index f1181561b..1703649b9 100644 --- a/lib/crates/fabro-telemetry/src/sender.rs +++ b/lib/crates/fabro-telemetry/src/sender.rs @@ -229,13 +229,13 @@ mod tests { // SEGMENT_WRITE_KEY is not set at compile time in tests, // so emit() should return immediately without spawning. let track = Track { - user: User::AnonymousId { + user: User::AnonymousId { anonymous_id: "test".to_string(), }, - event: "test".to_string(), + event: "test".to_string(), properties: json!({}), - context: None, - timestamp: None, + context: None, + timestamp: None, message_id: "msg-test".to_string(), }; @@ -254,13 +254,13 @@ mod tests { #[test] fn upload_blocking_noops_without_write_key() { let track = Track { - user: User::AnonymousId { + user: User::AnonymousId { anonymous_id: "test".to_string(), }, - event: "test".to_string(), + event: "test".to_string(), properties: json!({}), - context: None, - timestamp: None, + context: None, + timestamp: None, message_id: "msg-test".to_string(), }; diff --git a/lib/crates/fabro-template/src/lib.rs b/lib/crates/fabro-template/src/lib.rs index 58e0d92b2..5302b4110 100644 --- a/lib/crates/fabro-template/src/lib.rs +++ b/lib/crates/fabro-template/src/lib.rs @@ -9,9 +9,9 @@ use thiserror::Error; #[derive(Debug, Default, Clone)] pub struct TemplateContext { - goal: Option, + goal: Option, inputs: HashMap, - env: Option, + env: Option, } impl TemplateContext { @@ -38,7 +38,7 @@ impl TemplateContext { E: Env + Clone + Send + Sync + fmt::Debug + 'static, { self.env = Some(Value::from_object(EnvLookup { - env: env.clone(), + env: env.clone(), allowlist: None, })); self @@ -50,7 +50,7 @@ impl TemplateContext { E: Env + Clone + Send + Sync + fmt::Debug + 'static, { self.env = Some(Value::from_object(EnvLookup { - env: env.clone(), + env: env.clone(), allowlist: Some(allowlist.to_vec()), })); self @@ -66,9 +66,9 @@ impl TemplateContext { #[derive(Debug, Clone)] struct RenderContext { - goal: Option, + goal: Option, inputs: Value, - env: Option, + env: Option, } impl Object for RenderContext { @@ -84,7 +84,7 @@ impl Object for RenderContext { #[derive(Debug, Clone)] pub struct EnvLookup { - env: E, + env: E, allowlist: Option>, } diff --git a/lib/crates/fabro-test/src/lib.rs b/lib/crates/fabro-test/src/lib.rs index 6d1ae6d70..290c6d795 100644 --- a/lib/crates/fabro-test/src/lib.rs +++ b/lib/crates/fabro-test/src/lib.rs @@ -104,23 +104,23 @@ pub fn require_env(name: &str) -> Option { /// shared per nextest run when `NEXTEST_RUN_ID` is present, otherwise shared /// per test process. pub struct TestContext { - pub temp_dir: PathBuf, - pub home_dir: PathBuf, - pub storage_dir: PathBuf, - test_case_id: String, - test_run_id: String, - session_root: PathBuf, - fabro_bin: PathBuf, - filters: Vec<(String, String)>, - active_socket_path: PathBuf, - isolated_server: Option, + pub temp_dir: PathBuf, + pub home_dir: PathBuf, + pub storage_dir: PathBuf, + test_case_id: String, + test_run_id: String, + session_root: PathBuf, + fabro_bin: PathBuf, + filters: Vec<(String, String)>, + active_socket_path: PathBuf, + isolated_server: Option, managed_storage_dirs: Vec, - _context_root: tempfile::TempDir, + _context_root: tempfile::TempDir, } #[derive(Debug, Clone)] struct ServerPaths { - root: PathBuf, + root: PathBuf, storage_dir: PathBuf, socket_path: PathBuf, config_path: PathBuf, @@ -128,7 +128,7 @@ struct ServerPaths { #[derive(Debug, Clone)] struct SessionPaths { - root: PathBuf, + root: PathBuf, server: ServerPaths, } @@ -140,7 +140,7 @@ enum SessionMode { #[derive(Debug, Serialize)] struct ClientMarker { - pid: u32, + pid: u32, touched_at_ms: u128, } @@ -186,29 +186,37 @@ fn session_paths() -> (SessionMode, String, SessionPaths) { if !run_id.trim().is_empty() { let short_id = shorten_session_id(&run_id); let root = base_dir.join(format!("n-{short_id}")); - return (SessionMode::Nextest, run_id, SessionPaths { - server: ServerPaths { - root: root.clone(), - storage_dir: root.join("storage"), - socket_path: root.join("fabro.sock"), - config_path: root.join("settings.toml"), + return ( + SessionMode::Nextest, + run_id, + SessionPaths { + server: ServerPaths { + root: root.clone(), + storage_dir: root.join("storage"), + socket_path: root.join("fabro.sock"), + config_path: root.join("settings.toml"), + }, + root, }, - root, - }); + ); } } let process_id = format!("process-{}", current_pid()); let root = base_dir.join(format!("p-{}", current_pid())); - (SessionMode::Process, process_id, SessionPaths { - server: ServerPaths { - root: root.clone(), - storage_dir: root.join("storage"), - socket_path: root.join("fabro.sock"), - config_path: root.join("settings.toml"), + ( + SessionMode::Process, + process_id, + SessionPaths { + server: ServerPaths { + root: root.clone(), + storage_dir: root.join("storage"), + socket_path: root.join("fabro.sock"), + config_path: root.join("settings.toml"), + }, + root, }, - root, - }) + ) } fn short_session_base_dir() -> PathBuf { @@ -316,7 +324,7 @@ fn live_marker_count(root: &Path) -> usize { fn write_marker(root: &Path) { let marker = ClientMarker { - pid: current_pid(), + pid: current_pid(), touched_at_ms: current_timestamp_ms(), }; let marker_path = session_marker_path(root, marker.pid); @@ -664,7 +672,7 @@ fn test_server_stop_timeout() -> std::time::Duration { fn shared_server_paths(root: &Path) -> ServerPaths { ServerPaths { - root: root.to_path_buf(), + root: root.to_path_buf(), storage_dir: root.join("storage"), socket_path: root.join("fabro.sock"), config_path: root.join("settings.toml"), @@ -678,7 +686,7 @@ fn isolated_server_paths( ) -> ServerPaths { let server_root = root.join("isolated").join(test_case_id); ServerPaths { - root: server_root.clone(), + root: server_root.clone(), storage_dir: storage_dir.unwrap_or_else(|| server_root.join("storage")), socket_path: server_root.join("fabro.sock"), config_path: server_root.join("settings.toml"), @@ -697,7 +705,7 @@ fn reap_isolated_servers(root: &Path) { continue; } stop_test_server(&ServerPaths { - root: server_root.clone(), + root: server_root.clone(), storage_dir: server_root.join("storage"), socket_path: server_root.join("fabro.sock"), config_path: server_root.join("settings.toml"), @@ -1236,7 +1244,7 @@ impl Drop for TestContext { fn drop(&mut self) { for storage_dir in &self.managed_storage_dirs { stop_test_server(&ServerPaths { - root: storage_dir.clone(), + root: storage_dir.clone(), storage_dir: storage_dir.clone(), socket_path: PathBuf::new(), config_path: PathBuf::new(), @@ -1382,7 +1390,7 @@ pub struct TwinOpenAi { pub struct TwinGitHub { pub base_url: String, - server: twin_github::TestServer, + server: twin_github::TestServer, } pub fn test_http_client() -> reqwest::Client { @@ -1471,7 +1479,7 @@ impl TwinScenarios { #[derive(Debug, Clone)] pub struct TwinScenario { matcher: Map, - script: Value, + script: Value, } impl TwinScenario { @@ -1485,7 +1493,7 @@ impl TwinScenario { ), ("model".to_string(), Value::String(model.into())), ]), - script: json!({ "kind": "success" }), + script: json!({ "kind": "success" }), } } @@ -1579,7 +1587,7 @@ impl TwinScenario { #[derive(Debug, Clone)] pub struct TwinToolCall { - name: String, + name: String, arguments: Value, } @@ -1661,7 +1669,7 @@ pub async fn twin_openai() -> &'static TwinOpenAi { let base_url = format!("http://127.0.0.1:{}/v1", addr.port()); let config = TwinConfig { - bind_addr: addr, + bind_addr: addr, require_auth: true, enable_admin: true, }; @@ -1810,18 +1818,18 @@ mod tests { fn run_and_create_commands_include_test_labels() { let context_root = tempfile::tempdir().expect("failed to create temp dir"); let context = TestContext { - temp_dir: context_root.path().join("temp"), - home_dir: context_root.path().join("home"), - storage_dir: context_root.path().join("storage"), - test_case_id: "case-123".to_string(), - test_run_id: "run-cmd-labels".to_string(), - session_root: context_root.path().join("session"), - fabro_bin: context_root.path().join("fabro"), - filters: Vec::new(), - active_socket_path: context_root.path().join("fabro.sock"), - isolated_server: None, + temp_dir: context_root.path().join("temp"), + home_dir: context_root.path().join("home"), + storage_dir: context_root.path().join("storage"), + test_case_id: "case-123".to_string(), + test_run_id: "run-cmd-labels".to_string(), + session_root: context_root.path().join("session"), + fabro_bin: context_root.path().join("fabro"), + filters: Vec::new(), + active_socket_path: context_root.path().join("fabro.sock"), + isolated_server: None, managed_storage_dirs: Vec::new(), - _context_root: context_root, + _context_root: context_root, }; let run_args = context @@ -1853,7 +1861,7 @@ mod tests { } struct EnvGuard { - key: &'static str, + key: &'static str, original: Option, } diff --git a/lib/crates/fabro-tracker/src/github.rs b/lib/crates/fabro-tracker/src/github.rs index 6ac1aa627..4a0cbbd7b 100644 --- a/lib/crates/fabro-tracker/src/github.rs +++ b/lib/crates/fabro-tracker/src/github.rs @@ -29,12 +29,12 @@ async fn execute_github_graphql( /// /// Scoped to a single project board identified by `project_number`. pub struct GitHubTracker { - creds: GitHubAppCredentials, - client: reqwest::Client, - owner: String, - repo: String, - project_number: u64, - base_url: String, + creds: GitHubAppCredentials, + client: reqwest::Client, + owner: String, + repo: String, + project_number: u64, + base_url: String, project_node_id: OnceCell, } @@ -627,7 +627,7 @@ mod tests { fn mock_github_tracker(server_url: &str, pem: String) -> GitHubTracker { GitHubTracker::new( GitHubAppCredentials { - app_id: "test-app".to_string(), + app_id: "test-app".to_string(), private_key_pem: pem, }, test_http_client(), @@ -640,20 +640,20 @@ mod tests { fn make_test_issue(state: &str) -> Issue { Issue { - id: "I_issue1".to_string(), + id: "I_issue1".to_string(), project_item_id: Some("PVTI_item1".to_string()), - identifier: "#42".to_string(), - title: "Fix bug".to_string(), - description: None, - priority: None, - state: state.to_string(), - branch_name: None, - url: "https://github.com/owner/repo/issues/42".to_string(), - assignee_id: None, - labels: vec![], - blocked_by: vec![], - created_at: None, - updated_at: None, + identifier: "#42".to_string(), + title: "Fix bug".to_string(), + description: None, + priority: None, + state: state.to_string(), + branch_name: None, + url: "https://github.com/owner/repo/issues/42".to_string(), + assignee_id: None, + labels: vec![], + blocked_by: vec![], + created_at: None, + updated_at: None, } } diff --git a/lib/crates/fabro-tracker/src/lib.rs b/lib/crates/fabro-tracker/src/lib.rs index f31846652..db38b0a90 100644 --- a/lib/crates/fabro-tracker/src/lib.rs +++ b/lib/crates/fabro-tracker/src/lib.rs @@ -67,15 +67,15 @@ pub(crate) async fn execute_graphql_request( #[derive(Debug, Clone)] pub struct BlockerRef { - pub id: String, + pub id: String, pub identifier: String, - pub state: String, + pub state: String, } #[derive(Debug, Clone)] pub struct Issue { /// Provider-native issue node ID. - pub id: String, + pub id: String, /// Provider-native project-item ID for status updates. /// None for providers where the issue ID is sufficient (e.g. Linear). /// For GitHub Projects, this is the ProjectV2Item node ID. @@ -83,18 +83,18 @@ pub struct Issue { /// even when an issue belongs to multiple project boards. pub project_item_id: Option, /// Human-readable identifier (e.g. "ABC-123" or "#42"). - pub identifier: String, - pub title: String, - pub description: Option, - pub priority: Option, - pub state: String, - pub branch_name: Option, - pub url: String, - pub assignee_id: Option, - pub labels: Vec, - pub blocked_by: Vec, - pub created_at: Option, - pub updated_at: Option, + pub identifier: String, + pub title: String, + pub description: Option, + pub priority: Option, + pub state: String, + pub branch_name: Option, + pub url: String, + pub assignee_id: Option, + pub labels: Vec, + pub blocked_by: Vec, + pub created_at: Option, + pub updated_at: Option, } /// Unified interface for project management / issue tracking systems. diff --git a/lib/crates/fabro-tracker/src/linear.rs b/lib/crates/fabro-tracker/src/linear.rs index 6e11d5c11..77115dcaa 100644 --- a/lib/crates/fabro-tracker/src/linear.rs +++ b/lib/crates/fabro-tracker/src/linear.rs @@ -11,7 +11,7 @@ const BLOCKS_RELATION_TYPE: &str = "blocks"; #[derive(Clone, Debug)] pub struct LinearOptions { - pub api_key: String, + pub api_key: String, pub endpoint: String, } @@ -93,9 +93,9 @@ fn normalize_issue(node: &Value) -> Result { .filter_map(|rel| { let issue = &rel["issue"]; Some(BlockerRef { - id: issue["id"].as_str()?.to_string(), + id: issue["id"].as_str()?.to_string(), identifier: issue["identifier"].as_str()?.to_string(), - state: issue["state"]["name"].as_str()?.to_string(), + state: issue["state"]["name"].as_str()?.to_string(), }) }) .collect() @@ -154,8 +154,8 @@ fn extract_issues(response: &Value) -> Result, String> { /// A `Tracker` implementation backed by Linear. pub struct LinearTracker { - config: LinearOptions, - client: reqwest::Client, + config: LinearOptions, + client: reqwest::Client, project_slug: String, } @@ -361,27 +361,27 @@ mod tests { fn mock_config(server_url: &str) -> LinearOptions { LinearOptions { - api_key: "lin_api_test123".to_string(), + api_key: "lin_api_test123".to_string(), endpoint: format!("{server_url}/graphql"), } } fn make_test_issue() -> Issue { Issue { - id: "issue-1".to_string(), + id: "issue-1".to_string(), project_item_id: None, - identifier: "T-1".to_string(), - title: "Test".to_string(), - description: None, - priority: None, - state: "Todo".to_string(), - branch_name: None, - url: "https://linear.app/t".to_string(), - assignee_id: None, - labels: vec![], - blocked_by: vec![], - created_at: None, - updated_at: None, + identifier: "T-1".to_string(), + title: "Test".to_string(), + description: None, + priority: None, + state: "Todo".to_string(), + branch_name: None, + url: "https://linear.app/t".to_string(), + assignee_id: None, + labels: vec![], + blocked_by: vec![], + created_at: None, + updated_at: None, } } diff --git a/lib/crates/fabro-types/src/checkpoint.rs b/lib/crates/fabro-types/src/checkpoint.rs index 6772034c1..58d5d8337 100644 --- a/lib/crates/fabro-types/src/checkpoint.rs +++ b/lib/crates/fabro-types/src/checkpoint.rs @@ -10,21 +10,21 @@ use crate::outcome::Outcome; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Checkpoint { - pub timestamp: DateTime, - pub current_node: String, - pub completed_nodes: Vec, - pub node_retries: HashMap, - pub context_values: HashMap, + pub timestamp: DateTime, + pub current_node: String, + pub completed_nodes: Vec, + pub node_retries: HashMap, + pub context_values: HashMap, #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub node_outcomes: HashMap>>, + pub node_outcomes: HashMap>>, #[serde(default, skip_serializing_if = "Option::is_none")] - pub next_node_id: Option, + pub next_node_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub git_commit_sha: Option, + pub git_commit_sha: Option, #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub loop_failure_signatures: HashMap, + pub loop_failure_signatures: HashMap, #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub restart_failure_signatures: HashMap, #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub node_visits: HashMap, + pub node_visits: HashMap, } diff --git a/lib/crates/fabro-types/src/conclusion.rs b/lib/crates/fabro-types/src/conclusion.rs index 15fec67c5..4b5e3e17e 100644 --- a/lib/crates/fabro-types/src/conclusion.rs +++ b/lib/crates/fabro-types/src/conclusion.rs @@ -6,27 +6,27 @@ use crate::outcome::StageStatus; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StageSummary { - pub stage_id: String, - pub stage_label: String, - pub duration_ms: u64, + pub stage_id: String, + pub stage_label: String, + pub duration_ms: u64, #[serde(default, skip_serializing_if = "Option::is_none")] pub billing_usd_micros: Option, - pub retries: u32, + pub retries: u32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Conclusion { - pub timestamp: DateTime, - pub status: StageStatus, - pub duration_ms: u64, + pub timestamp: DateTime, + pub status: StageStatus, + pub duration_ms: u64, #[serde(default, skip_serializing_if = "Option::is_none")] - pub failure_reason: Option, + pub failure_reason: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub final_git_commit_sha: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub stages: Vec, + pub stages: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] - pub billing: Option, + pub billing: Option, #[serde(default)] - pub total_retries: u32, + pub total_retries: u32, } diff --git a/lib/crates/fabro-types/src/graph.rs b/lib/crates/fabro-types/src/graph.rs index 9caa03376..6375d75e6 100644 --- a/lib/crates/fabro-types/src/graph.rs +++ b/lib/crates/fabro-types/src/graph.rs @@ -99,8 +99,8 @@ pub fn shape_to_handler_type(shape: &str) -> Option<&'static str> { /// A node in the workflow graph. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Node { - pub id: String, - pub attrs: HashMap, + pub id: String, + pub attrs: HashMap, /// CSS-like classes for model stylesheet targeting (from `class` attr and /// subgraph derivation). #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -110,8 +110,8 @@ pub struct Node { impl Node { pub fn new(id: impl Into) -> Self { Self { - id: id.into(), - attrs: HashMap::new(), + id: id.into(), + attrs: HashMap::new(), classes: Vec::new(), } } @@ -261,16 +261,16 @@ impl Node { /// An edge connecting two nodes in the workflow graph. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Edge { - pub from: String, - pub to: String, + pub from: String, + pub to: String, pub attrs: HashMap, } impl Edge { pub fn new(from: impl Into, to: impl Into) -> Self { Self { - from: from.into(), - to: to.into(), + from: from.into(), + to: to.into(), attrs: HashMap::new(), } } @@ -327,7 +327,7 @@ impl Edge { /// attributes. #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] pub struct Graph { - pub name: String, + pub name: String, pub nodes: HashMap, pub edges: Vec, pub attrs: HashMap, @@ -336,7 +336,7 @@ pub struct Graph { impl Graph { pub fn new(name: impl Into) -> Self { Self { - name: name.into(), + name: name.into(), nodes: HashMap::new(), edges: Vec::new(), attrs: HashMap::new(), diff --git a/lib/crates/fabro-types/src/interview.rs b/lib/crates/fabro-types/src/interview.rs index da7e3b4a3..d3d42ddfd 100644 --- a/lib/crates/fabro-types/src/interview.rs +++ b/lib/crates/fabro-types/src/interview.rs @@ -43,17 +43,17 @@ impl fmt::Display for InterviewQuestionType { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] pub struct InterviewQuestionRecord { #[serde(default)] - pub id: String, + pub id: String, #[serde(default)] - pub text: String, + pub text: String, #[serde(default)] - pub stage: String, + pub stage: String, #[serde(default)] - pub question_type: InterviewQuestionType, + pub question_type: InterviewQuestionType, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub options: Vec, + pub options: Vec, #[serde(default)] - pub allow_freeform: bool, + pub allow_freeform: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub timeout_seconds: Option, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/lib/crates/fabro-types/src/node_status.rs b/lib/crates/fabro-types/src/node_status.rs index 00cb112b6..89b315678 100644 --- a/lib/crates/fabro-types/src/node_status.rs +++ b/lib/crates/fabro-types/src/node_status.rs @@ -5,10 +5,10 @@ use crate::outcome::StageStatus; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NodeStatusRecord { - pub status: StageStatus, + pub status: StageStatus, #[serde(default)] - pub notes: Option, + pub notes: Option, #[serde(default)] pub failure_reason: Option, - pub timestamp: DateTime, + pub timestamp: DateTime, } diff --git a/lib/crates/fabro-types/src/outcome.rs b/lib/crates/fabro-types/src/outcome.rs index 176802209..5660f6aa2 100644 --- a/lib/crates/fabro-types/src/outcome.rs +++ b/lib/crates/fabro-types/src/outcome.rs @@ -119,9 +119,9 @@ impl FailureCategory { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct FailureDetail { - pub message: String, + pub message: String, #[serde(rename = "failure_class")] - pub category: FailureCategory, + pub category: FailureCategory, #[serde( rename = "failure_signature", default, @@ -143,40 +143,40 @@ impl FailureDetail { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(bound = "M: OutcomeMeta")] pub struct Outcome { - pub status: StageStatus, + pub status: StageStatus, #[serde(default, skip_serializing_if = "Option::is_none")] - pub preferred_label: Option, + pub preferred_label: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub suggested_next_ids: Vec, #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub context_updates: HashMap, + pub context_updates: HashMap, #[serde(default, skip_serializing_if = "Option::is_none")] - pub jump_to_node: Option, + pub jump_to_node: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub notes: Option, + pub notes: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub failure: Option, + pub failure: Option, #[serde(default)] - pub usage: M, + pub usage: M, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub files_touched: Vec, + pub files_touched: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] - pub duration_ms: Option, + pub duration_ms: Option, } impl Default for Outcome { fn default() -> Self { Self { - status: StageStatus::Success, - preferred_label: None, + status: StageStatus::Success, + preferred_label: None, suggested_next_ids: Vec::new(), - context_updates: HashMap::new(), - jump_to_node: None, - notes: None, - failure: None, - usage: M::default(), - files_touched: Vec::new(), - duration_ms: None, + context_updates: HashMap::new(), + jump_to_node: None, + notes: None, + failure: None, + usage: M::default(), + files_touched: Vec::new(), + duration_ms: None, } } } @@ -190,8 +190,8 @@ impl Outcome { Self { status: StageStatus::Fail, failure: Some(FailureDetail { - message: message.to_string(), - category: FailureCategory::Deterministic, + message: message.to_string(), + category: FailureCategory::Deterministic, signature: None, }), ..Self::default() @@ -209,9 +209,9 @@ impl Outcome { #[derive(Debug, Clone)] pub struct NodeResult { - pub outcome: Outcome, - pub duration: Duration, - pub attempts: u32, + pub outcome: Outcome, + pub duration: Duration, + pub attempts: u32, pub max_attempts: u32, } diff --git a/lib/crates/fabro-types/src/pull_request.rs b/lib/crates/fabro-types/src/pull_request.rs index 8eb66b86d..c1170abdc 100644 --- a/lib/crates/fabro-types/src/pull_request.rs +++ b/lib/crates/fabro-types/src/pull_request.rs @@ -3,11 +3,11 @@ use serde::{Deserialize, Serialize}; /// Record of a pull request created for a workflow run. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct PullRequestRecord { - pub html_url: String, - pub number: u64, - pub owner: String, - pub repo: String, + pub html_url: String, + pub number: u64, + pub owner: String, + pub repo: String, pub base_branch: String, pub head_branch: String, - pub title: String, + pub title: String, } diff --git a/lib/crates/fabro-types/src/retro.rs b/lib/crates/fabro-types/src/retro.rs index 8b1f02bff..a43f99cea 100644 --- a/lib/crates/fabro-types/src/retro.rs +++ b/lib/crates/fabro-types/src/retro.rs @@ -40,7 +40,7 @@ pub enum LearningCategory { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Learning { pub category: LearningCategory, - pub text: String, + pub text: String, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -55,10 +55,10 @@ pub enum FrictionKind { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FrictionPoint { - pub kind: FrictionKind, + pub kind: FrictionKind, pub description: String, #[serde(default, skip_serializing_if = "Option::is_none")] - pub stage_id: Option, + pub stage_id: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -72,72 +72,72 @@ pub enum OpenItemKind { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OpenItem { - pub kind: OpenItemKind, + pub kind: OpenItemKind, pub description: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StageRetro { - pub stage_id: String, - pub stage_label: String, - pub status: String, - pub duration_ms: u64, - pub retries: u32, + pub stage_id: String, + pub stage_label: String, + pub status: String, + pub duration_ms: u64, + pub retries: u32, #[serde(default, skip_serializing_if = "Option::is_none")] pub billing_usd_micros: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub notes: Option, + pub notes: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub failure_reason: Option, + pub failure_reason: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub files_touched: Vec, + pub files_touched: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AggregateStats { - pub total_duration_ms: u64, + pub total_duration_ms: u64, #[serde(default, skip_serializing_if = "Option::is_none")] pub total_billing_usd_micros: Option, - pub total_retries: u32, + pub total_retries: u32, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub files_touched: Vec, - pub stages_completed: usize, - pub stages_failed: usize, + pub files_touched: Vec, + pub stages_completed: usize, + pub stages_failed: usize, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RetroNarrative { - pub smoothness: SmoothnessRating, - pub intent: String, - pub outcome: String, + pub smoothness: SmoothnessRating, + pub intent: String, + pub outcome: String, #[serde(default)] - pub learnings: Vec, + pub learnings: Vec, #[serde(default)] pub friction_points: Vec, #[serde(default)] - pub open_items: Vec, + pub open_items: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Retro { - pub run_id: RunId, - pub workflow_name: String, - pub goal: String, - pub timestamp: DateTime, + pub run_id: RunId, + pub workflow_name: String, + pub goal: String, + pub timestamp: DateTime, #[serde(default, skip_serializing_if = "Option::is_none")] - pub smoothness: Option, - pub stages: Vec, - pub stats: AggregateStats, + pub smoothness: Option, + pub stages: Vec, + pub stats: AggregateStats, #[serde(default, skip_serializing_if = "Option::is_none")] - pub intent: Option, + pub intent: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub outcome: Option, + pub outcome: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub learnings: Option>, + pub learnings: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub friction_points: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] - pub open_items: Option>, + pub open_items: Option>, } impl Retro { diff --git a/lib/crates/fabro-types/src/run.rs b/lib/crates/fabro-types/src/run.rs index 30650f451..cfa79d7e7 100644 --- a/lib/crates/fabro-types/src/run.rs +++ b/lib/crates/fabro-types/src/run.rs @@ -27,48 +27,48 @@ pub struct RunClientProvenance { #[serde(default, skip_serializing_if = "Option::is_none")] pub user_agent: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub name: Option, + pub name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub version: Option, + pub version: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RunSubjectProvenance { #[serde(default, skip_serializing_if = "Option::is_none")] - pub login: Option, + pub login: Option, pub auth_method: RunAuthMethod, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct RunProvenance { #[serde(default, skip_serializing_if = "Option::is_none")] - pub server: Option, + pub server: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub client: Option, + pub client: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub subject: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RunRecord { - pub run_id: RunId, - pub settings: SettingsLayer, - pub graph: Graph, + pub run_id: RunId, + pub settings: SettingsLayer, + pub graph: Graph, #[serde(default, skip_serializing_if = "Option::is_none")] - pub workflow_slug: Option, + pub workflow_slug: Option, pub working_directory: PathBuf, #[serde(default, skip_serializing_if = "Option::is_none")] - pub host_repo_path: Option, + pub host_repo_path: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub repo_origin_url: Option, + pub repo_origin_url: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub base_branch: Option, + pub base_branch: Option, #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub labels: HashMap, + pub labels: HashMap, #[serde(default, skip_serializing_if = "Option::is_none")] - pub provenance: Option, + pub provenance: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub manifest_blob: Option, + pub manifest_blob: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub definition_blob: Option, + pub definition_blob: Option, } diff --git a/lib/crates/fabro-types/src/run_event/agent.rs b/lib/crates/fabro-types/src/run_event/agent.rs index 475dee841..2ea2c3ba2 100644 --- a/lib/crates/fabro-types/src/run_event/agent.rs +++ b/lib/crates/fabro-types/src/run_event/agent.rs @@ -8,8 +8,8 @@ pub struct AgentSessionStartedProps { #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub model: Option, - pub visit: u32, + pub model: Option, + pub visit: u32, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -24,34 +24,34 @@ pub struct AgentProcessingEndProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AgentInputProps { - pub text: String, + pub text: String, pub visit: u32, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AgentMessageProps { - pub text: String, - pub model: String, - pub billing: BilledTokenCounts, + pub text: String, + pub model: String, + pub billing: BilledTokenCounts, pub tool_call_count: usize, - pub visit: u32, + pub visit: u32, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AgentToolStartedProps { - pub tool_name: String, + pub tool_name: String, pub tool_call_id: String, - pub arguments: Value, - pub visit: u32, + pub arguments: Value, + pub visit: u32, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AgentToolCompletedProps { - pub tool_name: String, + pub tool_name: String, pub tool_call_id: String, - pub output: Value, - pub is_error: bool, - pub visit: u32, + pub output: Value, + pub is_error: bool, + pub visit: u32, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -62,10 +62,10 @@ pub struct AgentErrorProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AgentWarningProps { - pub kind: String, + pub kind: String, pub message: String, pub details: Value, - pub visit: u32, + pub visit: u32, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -76,83 +76,83 @@ pub struct AgentLoopDetectedProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AgentTurnLimitReachedProps { pub max_turns: usize, - pub visit: u32, + pub visit: u32, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AgentSteeringInjectedProps { - pub text: String, + pub text: String, pub visit: u32, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AgentCompactionStartedProps { - pub estimated_tokens: usize, + pub estimated_tokens: usize, pub context_window_size: usize, - pub visit: u32, + pub visit: u32, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AgentCompactionCompletedProps { - pub original_turn_count: usize, - pub preserved_turn_count: usize, + pub original_turn_count: usize, + pub preserved_turn_count: usize, pub summary_token_estimate: usize, - pub tracked_file_count: usize, - pub visit: u32, + pub tracked_file_count: usize, + pub visit: u32, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AgentLlmRetryProps { - pub provider: String, - pub model: String, - pub attempt: usize, + pub provider: String, + pub model: String, + pub attempt: usize, pub delay_secs: f64, - pub error: Value, - pub visit: u32, + pub error: Value, + pub visit: u32, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AgentSubSpawnedProps { pub agent_id: String, - pub depth: usize, - pub task: String, - pub visit: u32, + pub depth: usize, + pub task: String, + pub visit: u32, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AgentSubCompletedProps { - pub agent_id: String, - pub depth: usize, - pub success: bool, + pub agent_id: String, + pub depth: usize, + pub success: bool, pub turns_used: usize, - pub visit: u32, + pub visit: u32, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AgentSubFailedProps { pub agent_id: String, - pub depth: usize, - pub error: Value, - pub visit: u32, + pub depth: usize, + pub error: Value, + pub visit: u32, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AgentSubClosedProps { pub agent_id: String, - pub depth: usize, - pub visit: u32, + pub depth: usize, + pub visit: u32, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AgentMcpReadyProps { pub server_name: String, - pub tool_count: usize, - pub visit: u32, + pub tool_count: usize, + pub visit: u32, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AgentMcpFailedProps { pub server_name: String, - pub error: String, - pub visit: u32, + pub error: String, + pub visit: u32, } diff --git a/lib/crates/fabro-types/src/run_event/infra.rs b/lib/crates/fabro-types/src/run_event/infra.rs index 4460c0c2b..5d46e4035 100644 --- a/lib/crates/fabro-types/src/run_event/infra.rs +++ b/lib/crates/fabro-types/src/run_event/infra.rs @@ -7,22 +7,22 @@ pub struct SandboxInitializingProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SandboxReadyProps { - pub provider: String, + pub provider: String, pub duration_ms: u64, #[serde(default, skip_serializing_if = "Option::is_none")] - pub name: Option, + pub name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub cpu: Option, + pub cpu: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub memory: Option, + pub memory: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub url: Option, + pub url: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SandboxFailedProps { - pub provider: String, - pub error: String, + pub provider: String, + pub error: String, pub duration_ms: u64, } @@ -33,14 +33,14 @@ pub struct SandboxCleanupStartedProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SandboxCleanupCompletedProps { - pub provider: String, + pub provider: String, pub duration_ms: u64, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SandboxCleanupFailedProps { pub provider: String, - pub error: String, + pub error: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -50,45 +50,45 @@ pub struct SnapshotNameProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SnapshotCompletedProps { - pub name: String, + pub name: String, pub duration_ms: u64, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SnapshotFailedProps { - pub name: String, + pub name: String, pub error: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct GitCloneStartedProps { - pub url: String, + pub url: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub branch: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct GitCloneCompletedProps { - pub url: String, + pub url: String, pub duration_ms: u64, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct GitCloneFailedProps { - pub url: String, + pub url: String, pub error: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SandboxInitializedProps { - pub working_directory: String, - pub provider: String, + pub working_directory: String, + pub provider: String, #[serde(default, skip_serializing_if = "Option::is_none")] - pub identifier: Option, + pub identifier: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub host_working_directory: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub container_mount_point: Option, + pub container_mount_point: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -99,14 +99,14 @@ pub struct SetupStartedProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SetupCommandStartedProps { pub command: String, - pub index: usize, + pub index: usize, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SetupCommandCompletedProps { - pub command: String, - pub index: usize, - pub exit_code: i32, + pub command: String, + pub index: usize, + pub exit_code: i32, pub duration_ms: u64, } @@ -117,10 +117,10 @@ pub struct SetupCompletedProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SetupFailedProps { - pub command: String, - pub index: usize, + pub command: String, + pub index: usize, pub exit_code: i32, - pub stderr: String, + pub stderr: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -131,62 +131,62 @@ pub struct CliEnsureStartedProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct CliEnsureCompletedProps { - pub cli_name: String, - pub provider: String, + pub cli_name: String, + pub provider: String, pub already_installed: bool, - pub node_installed: bool, - pub duration_ms: u64, + pub node_installed: bool, + pub duration_ms: u64, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct CliEnsureFailedProps { - pub cli_name: String, - pub provider: String, - pub error: String, + pub cli_name: String, + pub provider: String, + pub error: String, pub duration_ms: u64, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct DevcontainerResolvedProps { - pub dockerfile_lines: usize, - pub environment_count: usize, + pub dockerfile_lines: usize, + pub environment_count: usize, pub lifecycle_command_count: usize, - pub workspace_folder: String, + pub workspace_folder: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct DevcontainerLifecycleStartedProps { - pub phase: String, + pub phase: String, pub command_count: usize, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct DevcontainerLifecycleCommandStartedProps { - pub phase: String, + pub phase: String, pub command: String, - pub index: usize, + pub index: usize, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct DevcontainerLifecycleCommandCompletedProps { - pub phase: String, - pub command: String, - pub index: usize, - pub exit_code: i32, + pub phase: String, + pub command: String, + pub index: usize, + pub exit_code: i32, pub duration_ms: u64, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct DevcontainerLifecycleCompletedProps { - pub phase: String, + pub phase: String, pub duration_ms: u64, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct DevcontainerLifecycleFailedProps { - pub phase: String, - pub command: String, - pub index: usize, + pub phase: String, + pub command: String, + pub index: usize, pub exit_code: i32, - pub stderr: String, + pub stderr: String, } diff --git a/lib/crates/fabro-types/src/run_event/misc.rs b/lib/crates/fabro-types/src/run_event/misc.rs index 7a3c4d302..b12aa597f 100644 --- a/lib/crates/fabro-types/src/run_event/misc.rs +++ b/lib/crates/fabro-types/src/run_event/misc.rs @@ -3,15 +3,15 @@ use serde_json::Value; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct InterviewOption { - pub key: String, + pub key: String, pub label: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ParallelStartedProps { - pub visit: u32, + pub visit: u32, pub branch_count: usize, - pub join_policy: String, + pub join_policy: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -21,35 +21,35 @@ pub struct ParallelBranchStartedProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ParallelBranchCompletedProps { - pub index: usize, + pub index: usize, pub duration_ms: u64, - pub status: String, + pub status: String, #[serde(default, skip_serializing_if = "Option::is_none")] - pub head_sha: Option, + pub head_sha: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ParallelCompletedProps { - pub visit: u32, - pub duration_ms: u64, + pub visit: u32, + pub duration_ms: u64, pub success_count: usize, pub failure_count: usize, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub results: Vec, + pub results: Vec, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct InterviewStartedProps { #[serde(default)] - pub question_id: String, - pub question: String, + pub question_id: String, + pub question: String, #[serde(default)] - pub stage: String, - pub question_type: String, + pub stage: String, + pub question_type: String, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub options: Vec, + pub options: Vec, #[serde(default)] - pub allow_freeform: bool, + pub allow_freeform: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub timeout_seconds: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -60,8 +60,8 @@ pub struct InterviewStartedProps { pub struct InterviewCompletedProps { #[serde(default)] pub question_id: String, - pub question: String, - pub answer: String, + pub question: String, + pub answer: String, pub duration_ms: u64, } @@ -69,9 +69,9 @@ pub struct InterviewCompletedProps { pub struct InterviewTimeoutProps { #[serde(default)] pub question_id: String, - pub question: String, + pub question: String, #[serde(default)] - pub stage: String, + pub stage: String, pub duration_ms: u64, } @@ -79,10 +79,10 @@ pub struct InterviewTimeoutProps { pub struct InterviewInterruptedProps { #[serde(default)] pub question_id: String, - pub question: String, + pub question: String, #[serde(default)] - pub stage: String, - pub reason: String, + pub stage: String, + pub reason: String, pub duration_ms: u64, } @@ -93,19 +93,19 @@ pub struct GitCommitProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct GitPushProps { - pub branch: String, + pub branch: String, pub success: bool, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct GitBranchProps { pub branch: String, - pub sha: String, + pub sha: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct GitWorktreeAddProps { - pub path: String, + pub path: String, pub branch: String, } @@ -116,7 +116,7 @@ pub struct GitWorktreeRemoveProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct GitFetchProps { - pub branch: String, + pub branch: String, pub success: bool, } @@ -127,25 +127,25 @@ pub struct GitResetProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct EdgeSelectedProps { - pub from_node: String, - pub to_node: String, + pub from_node: String, + pub to_node: String, #[serde(default, skip_serializing_if = "Option::is_none")] - pub label: Option, + pub label: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub condition: Option, - pub reason: String, + pub condition: Option, + pub reason: String, #[serde(default, skip_serializing_if = "Option::is_none")] - pub preferred_label: Option, + pub preferred_label: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub suggested_next_ids: Vec, - pub stage_status: String, - pub is_jump: bool, + pub stage_status: String, + pub is_jump: bool, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct LoopRestartProps { pub from_node: String, - pub to_node: String, + pub to_node: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -156,8 +156,8 @@ pub struct SubgraphStartedProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SubgraphCompletedProps { pub steps_executed: usize, - pub status: String, - pub duration_ms: u64, + pub status: String, + pub duration_ms: u64, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -167,13 +167,13 @@ pub struct StallWatchdogTimeoutProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ArtifactCapturedProps { - pub attempt: u32, - pub node_slug: String, - pub path: String, - pub mime: String, - pub content_md5: String, + pub attempt: u32, + pub node_slug: String, + pub path: String, + pub mime: String, + pub content_md5: String, pub content_sha256: String, - pub bytes: u64, + pub bytes: u64, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -184,58 +184,58 @@ pub struct SshAccessReadyProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct FailoverProps { pub from_provider: String, - pub from_model: String, - pub to_provider: String, - pub to_model: String, - pub error: String, + pub from_model: String, + pub to_provider: String, + pub to_model: String, + pub error: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct CommandStartedProps { - pub script: String, - pub command: String, - pub language: String, + pub script: String, + pub command: String, + pub language: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub timeout_ms: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct CommandCompletedProps { - pub stdout: String, - pub stderr: String, + pub stdout: String, + pub stderr: String, #[serde(default, skip_serializing_if = "Option::is_none")] - pub exit_code: Option, + pub exit_code: Option, pub duration_ms: u64, - pub timed_out: bool, + pub timed_out: bool, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AgentCliStartedProps { - pub visit: u32, - pub mode: String, + pub visit: u32, + pub mode: String, pub provider: String, - pub model: String, - pub command: String, + pub model: String, + pub command: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AgentCliCompletedProps { - pub stdout: String, - pub stderr: String, - pub exit_code: i32, + pub stdout: String, + pub stderr: String, + pub exit_code: i32, pub duration_ms: u64, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct PullRequestCreatedProps { - pub pr_url: String, - pub pr_number: u64, - pub owner: String, - pub repo: String, + pub pr_url: String, + pub pr_number: u64, + pub owner: String, + pub repo: String, pub base_branch: String, pub head_branch: String, - pub title: String, - pub draft: bool, + pub title: String, + pub draft: bool, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -246,24 +246,24 @@ pub struct PullRequestFailedProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RetroStartedProps { #[serde(default, skip_serializing_if = "Option::is_none")] - pub prompt: Option, + pub prompt: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub model: Option, + pub model: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RetroCompletedProps { pub duration_ms: u64, #[serde(default, skip_serializing_if = "Option::is_none")] - pub response: Option, + pub response: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub retro: Option, + pub retro: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RetroFailedProps { - pub error: String, + pub error: String, pub duration_ms: u64, } diff --git a/lib/crates/fabro-types/src/run_event/mod.rs b/lib/crates/fabro-types/src/run_event/mod.rs index 6a5706af7..1496563e4 100644 --- a/lib/crates/fabro-types/src/run_event/mod.rs +++ b/lib/crates/fabro-types/src/run_event/mod.rs @@ -36,9 +36,9 @@ pub enum ActorKind { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ActorRef { - pub kind: ActorKind, + pub kind: ActorKind, #[serde(default, skip_serializing_if = "Option::is_none")] - pub id: Option, + pub id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub display: Option, } @@ -47,8 +47,8 @@ impl ActorRef { #[must_use] pub fn user(login: String) -> Self { Self { - kind: ActorKind::User, - id: Some(login.clone()), + kind: ActorKind::User, + id: Some(login.clone()), display: Some(login), } } @@ -65,19 +65,19 @@ impl ActorRef { #[derive(Debug, Clone, PartialEq)] pub struct RunEvent { - pub id: String, - pub ts: DateTime, - pub run_id: RunId, - pub node_id: Option, - pub node_label: Option, - pub stage_id: Option, - pub parallel_group_id: Option, + pub id: String, + pub ts: DateTime, + pub run_id: RunId, + pub node_id: Option, + pub node_label: Option, + pub stage_id: Option, + pub parallel_group_id: Option, pub parallel_branch_id: Option, - pub session_id: Option, - pub parent_session_id: Option, - pub tool_call_id: Option, - pub actor: Option, - pub body: EventBody, + pub session_id: Option, + pub parent_session_id: Option, + pub tool_call_id: Option, + pub actor: Option, + pub body: EventBody, } #[allow(clippy::large_enum_variant)] @@ -297,37 +297,37 @@ pub enum EventBody { #[serde(rename = "retro.failed")] RetroFailed(RetroFailedProps), Unknown { - name: String, + name: String, properties: Value, }, } #[derive(Debug, Clone, Deserialize)] struct RunEventRaw { - id: String, - ts: DateTime, - run_id: RunId, + id: String, + ts: DateTime, + run_id: RunId, #[serde(default)] - node_id: Option, + node_id: Option, #[serde(default)] - node_label: Option, + node_label: Option, #[serde(default)] - stage_id: Option, + stage_id: Option, #[serde(default)] - parallel_group_id: Option, + parallel_group_id: Option, #[serde(default)] parallel_branch_id: Option, #[serde(default)] - session_id: Option, + session_id: Option, #[serde(default)] - parent_session_id: Option, + parent_session_id: Option, #[serde(default)] - tool_call_id: Option, + tool_call_id: Option, #[serde(default)] - actor: Option, - event: String, + actor: Option, + event: String, #[serde(default = "default_properties")] - properties: Value, + properties: Value, } fn default_properties() -> Value { @@ -335,20 +335,20 @@ fn default_properties() -> Value { } struct RunEventParts<'a> { - id: String, - ts: DateTime, - run_id: RunId, - node_id: Option, - node_label: Option, - stage_id: Option, - parallel_group_id: Option, + id: String, + ts: DateTime, + run_id: RunId, + node_id: Option, + node_label: Option, + stage_id: Option, + parallel_group_id: Option, parallel_branch_id: Option, - session_id: Option, - parent_session_id: Option, - tool_call_id: Option, - actor: Option, - event: &'a str, - properties: &'a Value, + session_id: Option, + parent_session_id: Option, + tool_call_id: Option, + actor: Option, + event: &'a str, + properties: &'a Value, } impl EventBody { @@ -593,20 +593,20 @@ impl RunEvent { pub fn from_value(value: Value) -> serde_json::Result { let raw: RunEventRaw = serde_json::from_value(value)?; Self::from_parts(RunEventParts { - id: raw.id, - ts: raw.ts, - run_id: raw.run_id, - node_id: raw.node_id, - node_label: raw.node_label, - stage_id: raw.stage_id, - parallel_group_id: raw.parallel_group_id, + id: raw.id, + ts: raw.ts, + run_id: raw.run_id, + node_id: raw.node_id, + node_label: raw.node_label, + stage_id: raw.stage_id, + parallel_group_id: raw.parallel_group_id, parallel_branch_id: raw.parallel_branch_id, - session_id: raw.session_id, - parent_session_id: raw.parent_session_id, - tool_call_id: raw.tool_call_id, - actor: raw.actor, - event: &raw.event, - properties: &raw.properties, + session_id: raw.session_id, + parent_session_id: raw.parent_session_id, + tool_call_id: raw.tool_call_id, + actor: raw.actor, + event: &raw.event, + properties: &raw.properties, }) } @@ -670,7 +670,7 @@ impl RunEvent { Ok(body) => body, Err(err) if is_known_event_name(parts.event) => return Err(err), Err(_) => EventBody::Unknown { - name: parts.event.to_string(), + name: parts.event.to_string(), properties: parts.properties.clone(), }, }; @@ -783,21 +783,21 @@ mod tests { #[test] fn run_event_round_trips_json() { let event = RunEvent { - id: "evt_1".to_string(), - ts: DateTime::parse_from_rfc3339("2026-04-04T12:00:00.000Z") + id: "evt_1".to_string(), + ts: DateTime::parse_from_rfc3339("2026-04-04T12:00:00.000Z") .unwrap() .with_timezone(&Utc), - run_id: fixtures::RUN_1, - node_id: Some("build".to_string()), - node_label: Some("Build".to_string()), - stage_id: None, - parallel_group_id: None, + run_id: fixtures::RUN_1, + node_id: Some("build".to_string()), + node_label: Some("Build".to_string()), + stage_id: None, + parallel_group_id: None, parallel_branch_id: None, - session_id: None, - parent_session_id: None, - tool_call_id: None, - actor: None, - body: EventBody::StageCompleted(StageCompletedProps { + session_id: None, + parent_session_id: None, + tool_call_id: None, + actor: None, + body: EventBody::StageCompleted(StageCompletedProps { index: 1, duration_ms: 1234, status: crate::StageStatus::Success, @@ -829,15 +829,18 @@ mod tests { fn run_event_deserializes_adjacent_layout() { let settings = SettingsLayer::default(); let graph = Graph { - name: "test".to_string(), - nodes: HashMap::from([("start".to_string(), Node { - id: "start".to_string(), - attrs: HashMap::new(), - classes: Vec::new(), - })]), + name: "test".to_string(), + nodes: HashMap::from([( + "start".to_string(), + Node { + id: "start".to_string(), + attrs: HashMap::new(), + classes: Vec::new(), + }, + )]), edges: vec![Edge { - from: "start".to_string(), - to: "done".to_string(), + from: "start".to_string(), + to: "done".to_string(), attrs: HashMap::new(), }], attrs: HashMap::new(), @@ -891,9 +894,9 @@ mod tests { fn interview_interrupted_kind_matches_event_name() { let body = EventBody::InterviewInterrupted(InterviewInterruptedProps { question_id: "q-1".to_string(), - question: "approve?".to_string(), - stage: "gate".to_string(), - reason: "interrupted".to_string(), + question: "approve?".to_string(), + stage: "gate".to_string(), + reason: "interrupted".to_string(), duration_ms: 12, }); @@ -1025,27 +1028,27 @@ mod tests { #[test] fn run_event_omits_absent_envelope_fields() { let event = RunEvent { - id: "evt_bare".to_string(), - ts: DateTime::parse_from_rfc3339("2026-04-04T12:00:00.000Z") + id: "evt_bare".to_string(), + ts: DateTime::parse_from_rfc3339("2026-04-04T12:00:00.000Z") .unwrap() .with_timezone(&Utc), - run_id: fixtures::RUN_1, - node_id: None, - node_label: None, - stage_id: None, - parallel_group_id: None, + run_id: fixtures::RUN_1, + node_id: None, + node_label: None, + stage_id: None, + parallel_group_id: None, parallel_branch_id: None, - session_id: None, - parent_session_id: None, - tool_call_id: None, - actor: None, - body: EventBody::RunStarted(RunStartedProps { - name: "demo".to_string(), - base_branch: None, - base_sha: None, - run_branch: None, + session_id: None, + parent_session_id: None, + tool_call_id: None, + actor: None, + body: EventBody::RunStarted(RunStartedProps { + name: "demo".to_string(), + base_branch: None, + base_sha: None, + run_branch: None, worktree_dir: None, - goal: None, + goal: None, }), }; diff --git a/lib/crates/fabro-types/src/run_event/run.rs b/lib/crates/fabro-types/src/run_event/run.rs index 2b5488b4d..90fdb2fda 100644 --- a/lib/crates/fabro-types/src/run_event/run.rs +++ b/lib/crates/fabro-types/src/run_event/run.rs @@ -8,45 +8,45 @@ use crate::{Graph, RunBlobId, RunControlAction, RunProvenance, StatusReason}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RunCreatedProps { - pub settings: SettingsLayer, - pub graph: Graph, + pub settings: SettingsLayer, + pub graph: Graph, #[serde(default, skip_serializing_if = "Option::is_none")] - pub workflow_source: Option, + pub workflow_source: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub workflow_config: Option, + pub workflow_config: Option, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub labels: BTreeMap, - pub run_dir: String, + pub labels: BTreeMap, + pub run_dir: String, pub working_directory: String, #[serde(default, skip_serializing_if = "Option::is_none")] - pub host_repo_path: Option, + pub host_repo_path: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub repo_origin_url: Option, + pub repo_origin_url: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub base_branch: Option, + pub base_branch: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub workflow_slug: Option, + pub workflow_slug: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub db_prefix: Option, + pub db_prefix: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub provenance: Option, + pub provenance: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub manifest_blob: Option, + pub manifest_blob: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RunStartedProps { - pub name: String, + pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] - pub base_branch: Option, + pub base_branch: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub base_sha: Option, + pub base_sha: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub run_branch: Option, + pub run_branch: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub worktree_dir: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub goal: Option, + pub goal: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -58,7 +58,7 @@ pub struct RunStatusTransitionProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RunSubmittedProps { #[serde(default, skip_serializing_if = "Option::is_none")] - pub reason: Option, + pub reason: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub definition_blob: Option, } @@ -75,44 +75,44 @@ pub struct RunControlEffectProps {} #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RunRewoundProps { pub target_checkpoint_ordinal: usize, - pub target_node_id: String, - pub target_visit: usize, + pub target_node_id: String, + pub target_visit: usize, #[serde(default, skip_serializing_if = "Option::is_none")] - pub previous_status: Option, + pub previous_status: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub run_commit_sha: Option, + pub run_commit_sha: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RunCompletedProps { - pub duration_ms: u64, - pub artifact_count: usize, - pub status: String, + pub duration_ms: u64, + pub artifact_count: usize, + pub status: String, #[serde(default, skip_serializing_if = "Option::is_none")] - pub reason: Option, + pub reason: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub total_usd_micros: Option, + pub total_usd_micros: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub final_git_commit_sha: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub final_patch: Option, + pub final_patch: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub billing: Option, + pub billing: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RunFailedProps { - pub error: String, - pub duration_ms: u64, + pub error: String, + pub duration_ms: u64, #[serde(default, skip_serializing_if = "Option::is_none")] - pub reason: Option, + pub reason: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub git_commit_sha: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RunNoticeProps { - pub level: RunNoticeLevel, - pub code: String, + pub level: RunNoticeLevel, + pub code: String, pub message: String, } diff --git a/lib/crates/fabro-types/src/run_event/stage.rs b/lib/crates/fabro-types/src/run_event/stage.rs index 311420e73..6b84868ef 100644 --- a/lib/crates/fabro-types/src/run_event/stage.rs +++ b/lib/crates/fabro-types/src/run_event/stage.rs @@ -7,9 +7,9 @@ use crate::{BilledModelUsage, FailureDetail, Outcome, StageStatus}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct StageStartedProps { - pub index: usize, + pub index: usize, pub handler_type: String, - pub attempt: usize, + pub attempt: usize, pub max_attempts: usize, } @@ -50,39 +50,39 @@ pub struct StageCompletedProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct StageFailedProps { - pub index: usize, + pub index: usize, #[serde(default, skip_serializing_if = "Option::is_none")] - pub failure: Option, + pub failure: Option, pub will_retry: bool, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct StageRetryingProps { - pub index: usize, - pub attempt: usize, + pub index: usize, + pub attempt: usize, pub max_attempts: usize, - pub delay_ms: u64, + pub delay_ms: u64, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct StagePromptProps { - pub visit: u32, - pub text: String, + pub visit: u32, + pub text: String, #[serde(default, skip_serializing_if = "Option::is_none")] - pub mode: Option, + pub mode: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub model: Option, + pub model: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct PromptCompletedProps { pub response: String, - pub model: String, + pub model: String, pub provider: String, #[serde(default, skip_serializing_if = "Option::is_none")] - pub billing: Option, + pub billing: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/lib/crates/fabro-types/src/sandbox_record.rs b/lib/crates/fabro-types/src/sandbox_record.rs index 787d612df..59741a709 100644 --- a/lib/crates/fabro-types/src/sandbox_record.rs +++ b/lib/crates/fabro-types/src/sandbox_record.rs @@ -2,12 +2,12 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SandboxRecord { - pub provider: String, - pub working_directory: String, + pub provider: String, + pub working_directory: String, #[serde(default, skip_serializing_if = "Option::is_none")] - pub identifier: Option, + pub identifier: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub host_working_directory: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub container_mount_point: Option, + pub container_mount_point: Option, } diff --git a/lib/crates/fabro-types/src/settings/cli.rs b/lib/crates/fabro-types/src/settings/cli.rs index 6031c8f93..0a63102d6 100644 --- a/lib/crates/fabro-types/src/settings/cli.rs +++ b/lib/crates/fabro-types/src/settings/cli.rs @@ -15,10 +15,10 @@ use super::run::{AgentPermissions, McpEntryLayer, McpServerSettings}; /// A structurally resolved `[cli]` view for consumers. #[derive(Debug, Clone, Default, PartialEq)] pub struct CliSettings { - pub target: Option, - pub auth: CliAuthSettings, - pub exec: CliExecSettings, - pub output: CliOutputSettings, + pub target: Option, + pub auth: CliAuthSettings, + pub exec: CliExecSettings, + pub output: CliOutputSettings, pub updates: CliUpdatesSettings, pub logging: CliLoggingSettings, } @@ -37,8 +37,8 @@ pub enum CliTargetSettings { #[derive(Debug, Clone, PartialEq)] pub struct CliTargetTlsSettings { pub cert: InterpString, - pub key: InterpString, - pub ca: InterpString, + pub key: InterpString, + pub ca: InterpString, } #[derive(Debug, Clone, Default, PartialEq)] @@ -49,25 +49,25 @@ pub struct CliAuthSettings { #[derive(Debug, Clone, Default, PartialEq)] pub struct CliExecSettings { pub prevent_idle_sleep: bool, - pub model: CliExecModelSettings, - pub agent: CliExecAgentSettings, + pub model: CliExecModelSettings, + pub agent: CliExecAgentSettings, } #[derive(Debug, Clone, Default, PartialEq)] pub struct CliExecModelSettings { pub provider: Option, - pub name: Option, + pub name: Option, } #[derive(Debug, Clone, Default, PartialEq)] pub struct CliExecAgentSettings { pub permissions: Option, - pub mcps: HashMap, + pub mcps: HashMap, } #[derive(Debug, Clone, Default, PartialEq)] pub struct CliOutputSettings { - pub format: OutputFormat, + pub format: OutputFormat, pub verbosity: OutputVerbosity, } @@ -86,13 +86,13 @@ pub struct CliLoggingSettings { #[serde(deny_unknown_fields)] pub struct CliLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub target: Option, + pub target: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub auth: Option, + pub auth: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub exec: Option, + pub exec: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub output: Option, + pub output: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub updates: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -121,9 +121,9 @@ pub struct CliTargetTlsLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub cert: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub key: Option, + pub key: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub ca: Option, + pub ca: Option, } /// `[cli.auth]` — explicit auth strategy selection. @@ -151,9 +151,9 @@ pub struct CliExecLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub prevent_idle_sleep: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub model: Option, + pub model: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub agent: Option, + pub agent: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] @@ -162,7 +162,7 @@ pub struct CliExecModelLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub name: Option, + pub name: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] @@ -172,7 +172,7 @@ pub struct CliExecAgentLayer { pub permissions: Option, /// Agent-scoped MCP entries for `fabro exec`. #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub mcps: HashMap, + pub mcps: HashMap, } /// `[cli.output]` — generic CLI output defaults. @@ -180,7 +180,7 @@ pub struct CliExecAgentLayer { #[serde(deny_unknown_fields)] pub struct CliOutputLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub format: Option, + pub format: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub verbosity: Option, } diff --git a/lib/crates/fabro-types/src/settings/duration.rs b/lib/crates/fabro-types/src/settings/duration.rs index 17d64c12b..7ea3a7690 100644 --- a/lib/crates/fabro-types/src/settings/duration.rs +++ b/lib/crates/fabro-types/src/settings/duration.rs @@ -141,7 +141,7 @@ impl FromStr for Duration { other => { return Err(ParseDurationError::InvalidUnit { input: input.to_owned(), - unit: other.to_owned(), + unit: other.to_owned(), }); } }; diff --git a/lib/crates/fabro-types/src/settings/interp.rs b/lib/crates/fabro-types/src/settings/interp.rs index fc5e2603c..f893628ea 100644 --- a/lib/crates/fabro-types/src/settings/interp.rs +++ b/lib/crates/fabro-types/src/settings/interp.rs @@ -181,7 +181,7 @@ impl From<&str> for InterpString { /// The outcome of a successful env interpolation resolution. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Resolved { - pub value: String, + pub value: String, pub provenance: Provenance, } @@ -300,9 +300,12 @@ mod tests { .resolve(lookup_from(&[("API_KEY", "secret-123")])) .unwrap(); assert_eq!(resolved.value, "secret-123"); - assert_eq!(resolved.provenance, Provenance::EnvSourced { - names: vec!["API_KEY".into()], - }); + assert_eq!( + resolved.provenance, + Provenance::EnvSourced { + names: vec!["API_KEY".into()], + } + ); } #[test] @@ -319,9 +322,12 @@ mod tests { .resolve(lookup_from(&[("USER", "root"), ("HOST", "example.com")])) .unwrap(); assert_eq!(resolved.value, "root@example.com"); - assert_eq!(resolved.provenance, Provenance::EnvSourced { - names: vec!["USER".into(), "HOST".into()], - }); + assert_eq!( + resolved.provenance, + Provenance::EnvSourced { + names: vec!["USER".into(), "HOST".into()], + } + ); } #[test] diff --git a/lib/crates/fabro-types/src/settings/layer.rs b/lib/crates/fabro-types/src/settings/layer.rs index 81cd94c5e..6a0a2186b 100644 --- a/lib/crates/fabro-types/src/settings/layer.rs +++ b/lib/crates/fabro-types/src/settings/layer.rs @@ -18,17 +18,17 @@ use super::workflow::WorkflowLayer; #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct SettingsLayer { #[serde(default, rename = "_version", skip_serializing_if = "Option::is_none")] - pub version: Option, + pub version: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub project: Option, + pub project: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub workflow: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub run: Option, + pub run: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub cli: Option, + pub cli: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub server: Option, + pub server: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub features: Option, } diff --git a/lib/crates/fabro-types/src/settings/model_ref.rs b/lib/crates/fabro-types/src/settings/model_ref.rs index ee912ff51..26545599f 100644 --- a/lib/crates/fabro-types/src/settings/model_ref.rs +++ b/lib/crates/fabro-types/src/settings/model_ref.rs @@ -81,7 +81,7 @@ impl FromStr for ModelRef { } else { Ok(Self::Qualified { provider: (*provider).to_owned(), - model: (*model).to_owned(), + model: (*model).to_owned(), }) } } @@ -104,9 +104,9 @@ impl fmt::Display for ModelRef { /// An error returned when resolving an ambiguous bare model reference. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AmbiguousModelRef { - pub input: String, + pub input: String, pub providers: Vec, - pub models: Vec, + pub models: Vec, } impl fmt::Display for AmbiguousModelRef { @@ -130,7 +130,7 @@ pub enum ResolvedModelRef { /// The reference named a model (qualified or unambiguously bare). Model { provider: Option, - model: String, + model: String, }, } @@ -162,7 +162,7 @@ impl ModelRef { match self { Self::Qualified { provider, model } => Ok(ResolvedModelRef::Model { provider: Some(provider.clone()), - model: model.clone(), + model: model.clone(), }), Self::Bare(token) => { let is_provider = registry.is_provider(token); @@ -171,17 +171,17 @@ impl ModelRef { (true, false) => Ok(ResolvedModelRef::Provider(token.clone())), (false, true) => Ok(ResolvedModelRef::Model { provider: registry.provider_of(token), - model: token.clone(), + model: token.clone(), }), (true, true) => Err(AmbiguousModelRef { - input: token.clone(), + input: token.clone(), providers: vec![token.clone()], - models: vec![token.clone()], + models: vec![token.clone()], }), // Unknown tokens flow through as bare models — provider TBD at runtime. (false, false) => Ok(ResolvedModelRef::Model { provider: None, - model: token.clone(), + model: token.clone(), }), } } @@ -227,7 +227,7 @@ mod tests { struct TestRegistry { providers: &'static [&'static str], - models: &'static [&'static str], + models: &'static [&'static str], } impl ModelRegistry for TestRegistry { @@ -260,7 +260,7 @@ mod tests { "gemini/gemini-flash".parse::().unwrap(), ModelRef::Qualified { provider: "gemini".into(), - model: "gemini-flash".into(), + model: "gemini-flash".into(), } ); } @@ -295,7 +295,7 @@ mod tests { fn resolves_unique_provider_token() { let reg = TestRegistry { providers: &["openai"], - models: &[], + models: &[], }; let resolved = ModelRef::Bare("openai".into()).resolve(®).unwrap(); assert_eq!(resolved, ResolvedModelRef::Provider("openai".into())); @@ -305,20 +305,23 @@ mod tests { fn resolves_unique_model_token() { let reg = TestRegistry { providers: &[], - models: &["gpt-5.4"], + models: &["gpt-5.4"], }; let resolved = ModelRef::Bare("gpt-5.4".into()).resolve(®).unwrap(); - assert_eq!(resolved, ResolvedModelRef::Model { - provider: Some("test".into()), - model: "gpt-5.4".into(), - }); + assert_eq!( + resolved, + ResolvedModelRef::Model { + provider: Some("test".into()), + model: "gpt-5.4".into(), + } + ); } #[test] fn ambiguous_bare_token_errors() { let reg = TestRegistry { providers: &["ambiguous"], - models: &["ambiguous"], + models: &["ambiguous"], }; let err = ModelRef::Bare("ambiguous".into()) .resolve(®) @@ -330,18 +333,21 @@ mod tests { fn qualified_never_ambiguous() { let reg = TestRegistry { providers: &["ambiguous"], - models: &["ambiguous"], + models: &["ambiguous"], }; let resolved = ModelRef::Qualified { provider: "a".into(), - model: "b".into(), + model: "b".into(), } .resolve(®) .unwrap(); - assert_eq!(resolved, ResolvedModelRef::Model { - provider: Some("a".into()), - model: "b".into(), - }); + assert_eq!( + resolved, + ResolvedModelRef::Model { + provider: Some("a".into()), + model: "b".into(), + } + ); } #[test] diff --git a/lib/crates/fabro-types/src/settings/project.rs b/lib/crates/fabro-types/src/settings/project.rs index f7164ae44..e0c97f03e 100644 --- a/lib/crates/fabro-types/src/settings/project.rs +++ b/lib/crates/fabro-types/src/settings/project.rs @@ -10,10 +10,10 @@ use serde::{Deserialize, Serialize}; /// A structurally resolved `[project]` view for consumers. #[derive(Debug, Clone, Default, PartialEq)] pub struct ProjectSettings { - pub name: Option, + pub name: Option, pub description: Option, - pub directory: String, - pub metadata: HashMap, + pub directory: String, + pub metadata: HashMap, } /// A sparse `[project]` layer as it appears in a single settings file. @@ -21,13 +21,13 @@ pub struct ProjectSettings { #[serde(deny_unknown_fields)] pub struct ProjectLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub name: Option, + pub name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, /// The Fabro-managed project directory inside the repo. Defaults to /// `.` after layering when unspecified. #[serde(default, skip_serializing_if = "Option::is_none")] - pub directory: Option, + pub directory: Option, #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub metadata: HashMap, + pub metadata: HashMap, } diff --git a/lib/crates/fabro-types/src/settings/resolved.rs b/lib/crates/fabro-types/src/settings/resolved.rs index b68ef51b9..3382ff034 100644 --- a/lib/crates/fabro-types/src/settings/resolved.rs +++ b/lib/crates/fabro-types/src/settings/resolved.rs @@ -5,10 +5,10 @@ use super::{ /// A fully resolved settings view across all namespaces. #[derive(Debug, Clone, Default, PartialEq)] pub struct Settings { - pub project: ProjectSettings, + pub project: ProjectSettings, pub workflow: WorkflowSettings, - pub run: RunSettings, - pub cli: CliSettings, - pub server: ServerSettings, + pub run: RunSettings, + pub cli: CliSettings, + pub server: ServerSettings, pub features: FeaturesSettings, } diff --git a/lib/crates/fabro-types/src/settings/run.rs b/lib/crates/fabro-types/src/settings/run.rs index fafe34958..403c19223 100644 --- a/lib/crates/fabro-types/src/settings/run.rs +++ b/lib/crates/fabro-types/src/settings/run.rs @@ -18,23 +18,23 @@ use super::model_ref::ModelRef; /// A structurally resolved `[run]` view for consumers. #[derive(Debug, Clone, Default, PartialEq)] pub struct RunSettings { - pub goal: Option, - pub working_dir: Option, - pub metadata: HashMap, - pub inputs: HashMap, - pub model: RunModelSettings, - pub git: RunGitSettings, - pub prepare: RunPrepareSettings, - pub execution: RunExecutionSettings, - pub checkpoint: RunCheckpointSettings, - pub sandbox: RunSandboxSettings, + pub goal: Option, + pub working_dir: Option, + pub metadata: HashMap, + pub inputs: HashMap, + pub model: RunModelSettings, + pub git: RunGitSettings, + pub prepare: RunPrepareSettings, + pub execution: RunExecutionSettings, + pub checkpoint: RunCheckpointSettings, + pub sandbox: RunSandboxSettings, pub notifications: HashMap, - pub interviews: RunInterviewsSettings, - pub agent: RunAgentSettings, - pub hooks: Vec, - pub scm: RunScmSettings, - pub pull_request: Option, - pub artifacts: ArtifactsSettings, + pub interviews: RunInterviewsSettings, + pub agent: RunAgentSettings, + pub hooks: Vec, + pub scm: RunScmSettings, + pub pull_request: Option, + pub artifacts: ArtifactsSettings, } /// The resolved source of a run goal. @@ -46,8 +46,8 @@ pub enum RunGoal { #[derive(Debug, Clone, Default, PartialEq)] pub struct RunModelSettings { - pub provider: Option, - pub name: Option, + pub provider: Option, + pub name: Option, pub fallbacks: Vec, } @@ -58,20 +58,20 @@ pub struct RunGitSettings { #[derive(Debug, Clone, Default, PartialEq)] pub struct GitAuthorSettings { - pub name: Option, + pub name: Option, pub email: Option, } #[derive(Debug, Clone, PartialEq)] pub struct RunPrepareSettings { - pub commands: Vec, + pub commands: Vec, pub timeout_ms: u64, } impl Default for RunPrepareSettings { fn default() -> Self { Self { - commands: Vec::new(), + commands: Vec::new(), timeout_ms: 300_000, } } @@ -79,17 +79,17 @@ impl Default for RunPrepareSettings { #[derive(Debug, Clone, PartialEq)] pub struct RunExecutionSettings { - pub mode: RunMode, + pub mode: RunMode, pub approval: ApprovalMode, - pub retros: bool, + pub retros: bool, } impl Default for RunExecutionSettings { fn default() -> Self { Self { - mode: RunMode::Normal, + mode: RunMode::Normal, approval: ApprovalMode::Prompt, - retros: true, + retros: true, } } } @@ -101,23 +101,23 @@ pub struct RunCheckpointSettings { #[derive(Debug, Clone, PartialEq)] pub struct RunSandboxSettings { - pub provider: String, - pub preserve: bool, + pub provider: String, + pub preserve: bool, pub devcontainer: bool, - pub env: HashMap, - pub local: LocalSandboxSettings, - pub daytona: Option, + pub env: HashMap, + pub local: LocalSandboxSettings, + pub daytona: Option, } impl Default for RunSandboxSettings { fn default() -> Self { Self { - provider: "local".to_string(), - preserve: false, + provider: "local".to_string(), + preserve: false, devcontainer: false, - env: HashMap::new(), - local: LocalSandboxSettings::default(), - daytona: None, + env: HashMap::new(), + local: LocalSandboxSettings::default(), + daytona: None, } } } @@ -130,10 +130,10 @@ pub struct LocalSandboxSettings { #[derive(Debug, Clone, Default, PartialEq)] pub struct DaytonaSettings { pub auto_stop_interval: Option, - pub labels: HashMap, - pub snapshot: Option, - pub network: Option, - pub skip_clone: bool, + pub labels: HashMap, + pub snapshot: Option, + pub network: Option, + pub skip_clone: bool, } #[derive(Debug, Clone, PartialEq)] @@ -144,21 +144,21 @@ pub enum DockerfileSource { #[derive(Debug, Clone, PartialEq)] pub struct DaytonaSnapshotSettings { - pub name: String, - pub cpu: Option, - pub memory_gb: Option, - pub disk_gb: Option, + pub name: String, + pub cpu: Option, + pub memory_gb: Option, + pub disk_gb: Option, pub dockerfile: Option, } #[derive(Debug, Clone, Default, PartialEq)] pub struct NotificationRouteSettings { - pub enabled: bool, + pub enabled: bool, pub provider: Option, - pub events: Vec, - pub slack: Option, - pub discord: Option, - pub teams: Option, + pub events: Vec, + pub slack: Option, + pub discord: Option, + pub teams: Option, } #[derive(Debug, Clone, Default, PartialEq)] @@ -169,9 +169,9 @@ pub struct NotificationProviderSettings { #[derive(Debug, Clone, Default, PartialEq)] pub struct RunInterviewsSettings { pub provider: Option, - pub slack: Option, - pub discord: Option, - pub teams: Option, + pub slack: Option, + pub discord: Option, + pub teams: Option, } #[derive(Debug, Clone, Default, PartialEq)] @@ -182,27 +182,27 @@ pub struct InterviewProviderSettings { #[derive(Debug, Clone, Default, PartialEq)] pub struct RunAgentSettings { pub permissions: Option, - pub mcps: HashMap, + pub mcps: HashMap, } #[derive(Debug, Clone, PartialEq)] pub struct McpServerSettings { - pub name: String, - pub transport: McpTransport, + pub name: String, + pub transport: McpTransport, pub startup_timeout_secs: u64, - pub tool_timeout_secs: u64, + pub tool_timeout_secs: u64, } impl Default for McpServerSettings { fn default() -> Self { Self { - name: String::new(), - transport: McpTransport::Stdio { + name: String::new(), + transport: McpTransport::Stdio { command: Vec::new(), - env: HashMap::new(), + env: HashMap::new(), }, startup_timeout_secs: 10, - tool_timeout_secs: 60, + tool_timeout_secs: 60, } } } @@ -223,16 +223,16 @@ impl McpServerSettings { pub enum McpTransport { Stdio { command: Vec, - env: HashMap, + env: HashMap, }, Http { - url: String, + url: String, headers: HashMap, }, Sandbox { command: Vec, - port: u16, - env: HashMap, + port: u16, + env: HashMap, }, } @@ -252,36 +252,36 @@ pub enum HookType { command: String, }, Http { - url: String, - headers: Option>, + url: String, + headers: Option>, #[serde(default)] allowed_env_vars: Vec, #[serde(default)] - tls: TlsMode, + tls: TlsMode, }, Prompt { prompt: String, - model: Option, + model: Option, }, Agent { - prompt: String, - model: Option, + prompt: String, + model: Option, max_tool_rounds: Option, }, } #[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] pub struct HookDefinition { - pub name: Option, - pub event: HookEvent, + pub name: Option, + pub event: HookEvent, #[serde(default)] - pub command: Option, + pub command: Option, #[serde(flatten)] - pub hook_type: Option, - pub matcher: Option, - pub blocking: Option, + pub hook_type: Option, + pub matcher: Option, + pub blocking: Option, pub timeout_ms: Option, - pub sandbox: Option, + pub sandbox: Option, } impl HookDefinition { @@ -350,10 +350,10 @@ impl HookDefinition { #[derive(Debug, Clone, Default, PartialEq)] pub struct RunScmSettings { - pub provider: Option, - pub owner: Option, + pub provider: Option, + pub owner: Option, pub repository: Option, - pub github: Option, + pub github: Option, } #[derive(Debug, Clone, Default, PartialEq)] @@ -361,18 +361,18 @@ pub struct ScmGitHubSettings; #[derive(Debug, Clone, PartialEq)] pub struct PullRequestSettings { - pub enabled: bool, - pub draft: bool, - pub auto_merge: bool, + pub enabled: bool, + pub draft: bool, + pub auto_merge: bool, pub merge_strategy: MergeStrategy, } impl Default for PullRequestSettings { fn default() -> Self { Self { - enabled: false, - draft: true, - auto_merge: false, + enabled: false, + draft: true, + auto_merge: false, merge_strategy: MergeStrategy::Squash, } } @@ -388,41 +388,41 @@ pub struct ArtifactsSettings { #[serde(deny_unknown_fields)] pub struct RunLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub goal: Option, + pub goal: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub working_dir: Option, + pub working_dir: Option, /// Flat string-to-string map. Replaces wholesale across layers. #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub metadata: HashMap, + pub metadata: HashMap, /// Run inputs: typed scalar values. Replaces wholesale across layers. #[serde(default, skip_serializing_if = "Option::is_none")] - pub inputs: Option>, + pub inputs: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] - pub model: Option, + pub model: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub git: Option, + pub git: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub prepare: Option, + pub prepare: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub execution: Option, + pub execution: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub checkpoint: Option, + pub checkpoint: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub sandbox: Option, + pub sandbox: Option, #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub notifications: HashMap, #[serde(default, skip_serializing_if = "Option::is_none")] - pub interviews: Option, + pub interviews: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub agent: Option, + pub agent: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub hooks: Vec, + pub hooks: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] - pub scm: Option, + pub scm: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub pull_request: Option, + pub pull_request: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub artifacts: Option, + pub artifacts: Option, } /// The source of a run's goal, either inline literal text or a reference to @@ -460,7 +460,7 @@ pub enum RunGoalLayer { /// goals without having to re-walk the layer. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResolvedRunGoal { - pub text: String, + pub text: String, pub source: ResolvedGoalSource, } @@ -479,9 +479,9 @@ pub enum ResolvedGoalSource { #[serde(deny_unknown_fields)] pub struct RunModelLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub provider: Option, + pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub name: Option, + pub name: Option, /// Ordered list of fallback model references. Supports `...` splice marker /// at layering time — see [`super::splice_array`]. #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -528,7 +528,7 @@ pub struct RunGitLayer { #[serde(deny_unknown_fields)] pub struct GitAuthorLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub name: Option, + pub name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub email: Option, } @@ -539,7 +539,7 @@ pub struct GitAuthorLayer { #[serde(deny_unknown_fields)] pub struct RunPrepareLayer { #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub steps: Vec, + pub steps: Vec, /// Optional timeout applied to each prepare step. #[serde(default, skip_serializing_if = "Option::is_none")] pub timeout: Option, @@ -550,11 +550,11 @@ pub struct RunPrepareLayer { #[serde(deny_unknown_fields)] pub struct PrepareStep { #[serde(default, skip_serializing_if = "Option::is_none")] - pub script: Option, + pub script: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub command: Option>, #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub env: HashMap, + pub env: HashMap, } /// `[run.execution]` — run posture knobs. @@ -562,12 +562,12 @@ pub struct PrepareStep { #[serde(deny_unknown_fields)] pub struct RunExecutionLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub mode: Option, + pub mode: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub approval: Option, /// Positive-form: `true` runs retros, `false` skips them. #[serde(default, skip_serializing_if = "Option::is_none")] - pub retros: Option, + pub retros: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -597,18 +597,18 @@ pub struct RunCheckpointLayer { #[serde(deny_unknown_fields)] pub struct RunSandboxLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub provider: Option, + pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub preserve: Option, + pub preserve: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub devcontainer: Option, /// Sticky merge-by-key across layers. #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub env: HashMap, + pub env: HashMap, #[serde(default, skip_serializing_if = "Option::is_none")] - pub local: Option, + pub local: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub daytona: Option, + pub daytona: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] @@ -635,26 +635,26 @@ pub struct DaytonaSandboxLayer { pub auto_stop_interval: Option, /// Sticky merge-by-key (provider-native labels). #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub labels: HashMap, + pub labels: HashMap, #[serde(default, skip_serializing_if = "Option::is_none")] - pub snapshot: Option, + pub snapshot: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub network: Option, + pub network: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub skip_clone: Option, + pub skip_clone: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct DaytonaSnapshotLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub name: Option, + pub name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub cpu: Option, + pub cpu: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub memory: Option, + pub memory: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub disk: Option, + pub disk: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub dockerfile: Option, } @@ -679,19 +679,19 @@ pub enum DaytonaNetworkLayer { #[serde(deny_unknown_fields)] pub struct NotificationRouteLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, + pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, /// Raw Fabro event names. Splice marker supported at layering time. #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub events: Vec, + pub events: Vec, /// Provider-specific destination subtables. First-pass chat providers. #[serde(default, skip_serializing_if = "Option::is_none")] - pub slack: Option, + pub slack: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub discord: Option, + pub discord: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub teams: Option, + pub teams: Option, } /// A single string array entry that may be the splice marker. @@ -736,11 +736,11 @@ pub struct InterviewsLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub slack: Option, + pub slack: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub discord: Option, + pub discord: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub teams: Option, + pub teams: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] @@ -758,7 +758,7 @@ pub struct RunAgentLayer { pub permissions: Option, /// Agent-scoped MCP server entries, keyed by name. #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub mcps: HashMap, + pub mcps: HashMap, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -777,43 +777,43 @@ pub enum AgentPermissions { pub enum McpEntryLayer { Http { #[serde(default)] - enabled: Option, - url: InterpString, + enabled: Option, + url: InterpString, #[serde(default)] - headers: HashMap, + headers: HashMap, #[serde(default)] startup_timeout: Option, #[serde(default)] - tool_timeout: Option, + tool_timeout: Option, }, Stdio { #[serde(default)] - enabled: Option, + enabled: Option, #[serde(default)] - script: Option, + script: Option, #[serde(default)] - command: Option>, + command: Option>, #[serde(default)] - env: HashMap, + env: HashMap, #[serde(default)] startup_timeout: Option, #[serde(default)] - tool_timeout: Option, + tool_timeout: Option, }, Sandbox { #[serde(default)] - enabled: Option, + enabled: Option, #[serde(default)] - script: Option, + script: Option, #[serde(default)] - command: Option>, - port: u16, + command: Option>, + port: u16, #[serde(default)] - env: HashMap, + env: HashMap, #[serde(default)] startup_timeout: Option, #[serde(default)] - tool_timeout: Option, + tool_timeout: Option, }, } @@ -825,40 +825,40 @@ pub enum McpEntryLayer { pub struct HookEntry { /// Optional merge identity. Hooks with the same `id` replace in place. #[serde(default, skip_serializing_if = "Option::is_none")] - pub id: Option, + pub id: Option, /// Display-only human name. #[serde(default, skip_serializing_if = "Option::is_none")] - pub name: Option, - pub event: HookEvent, + pub name: Option, + pub event: HookEvent, #[serde(default, skip_serializing_if = "Option::is_none")] - pub matcher: Option, + pub matcher: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub blocking: Option, + pub blocking: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub timeout: Option, + pub timeout: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub sandbox: Option, + pub sandbox: Option, // Exactly one of the following groups is expected: #[serde(default, skip_serializing_if = "Option::is_none")] - pub script: Option, + pub script: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub command: Option>, + pub command: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] - pub url: Option, + pub url: Option, #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub headers: HashMap, + pub headers: HashMap, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub allowed_env_vars: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] - pub tls: Option, + pub tls: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub prompt: Option, + pub prompt: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub model: Option, + pub model: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_tool_rounds: Option, + pub max_tool_rounds: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub agent: Option, + pub agent: Option, } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] @@ -906,14 +906,14 @@ pub enum HookEvent { #[serde(deny_unknown_fields)] pub struct RunScmLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub provider: Option, + pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub owner: Option, + pub owner: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub repository: Option, /// Provider-specific SCM leaves. First-pass providers. #[serde(default, skip_serializing_if = "Option::is_none")] - pub github: Option, + pub github: Option, } /// `[run.scm.github]` — GitHub-specific SCM leaf. Intentionally minimal in @@ -928,11 +928,11 @@ pub struct ScmGitHubLayer; #[serde(deny_unknown_fields)] pub struct RunPullRequestLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, + pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub draft: Option, + pub draft: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub auto_merge: Option, + pub auto_merge: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub merge_strategy: Option, } diff --git a/lib/crates/fabro-types/src/settings/server.rs b/lib/crates/fabro-types/src/settings/server.rs index 716e93403..b061e2330 100644 --- a/lib/crates/fabro-types/src/settings/server.rs +++ b/lib/crates/fabro-types/src/settings/server.rs @@ -17,15 +17,15 @@ use super::interp::InterpString; /// A structurally resolved `[server]` view for consumers. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ServerSettings { - pub listen: ServerListenSettings, - pub api: ServerApiSettings, - pub web: ServerWebSettings, - pub auth: ServerAuthSettings, - pub storage: ServerStorageSettings, - pub artifacts: ServerArtifactsSettings, - pub slatedb: ServerSlateDbSettings, - pub scheduler: ServerSchedulerSettings, - pub logging: ServerLoggingSettings, + pub listen: ServerListenSettings, + pub api: ServerApiSettings, + pub web: ServerWebSettings, + pub auth: ServerAuthSettings, + pub storage: ServerStorageSettings, + pub artifacts: ServerArtifactsSettings, + pub slatedb: ServerSlateDbSettings, + pub scheduler: ServerSchedulerSettings, + pub logging: ServerLoggingSettings, pub integrations: ServerIntegrationsSettings, } @@ -33,7 +33,7 @@ pub struct ServerSettings { pub enum ServerListenSettings { Tcp { address: SocketAddr, - tls: Option, + tls: Option, }, Unix { path: InterpString, @@ -51,16 +51,16 @@ impl Default for ServerListenSettings { #[derive(Debug, Clone, PartialEq, Eq)] pub struct TlsConfig { pub cert: InterpString, - pub key: InterpString, - pub ca: InterpString, + pub key: InterpString, + pub ca: InterpString, } impl Default for TlsConfig { fn default() -> Self { Self { cert: InterpString::parse(""), - key: InterpString::parse(""), - ca: InterpString::parse(""), + key: InterpString::parse(""), + ca: InterpString::parse(""), } } } @@ -73,14 +73,14 @@ pub struct ServerApiSettings { #[derive(Debug, Clone, PartialEq, Eq)] pub struct ServerWebSettings { pub enabled: bool, - pub url: InterpString, + pub url: InterpString, } impl Default for ServerWebSettings { fn default() -> Self { Self { enabled: false, - url: InterpString::parse(""), + url: InterpString::parse(""), } } } @@ -93,27 +93,27 @@ pub struct ServerAuthSettings { #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ServerAuthApiSettings { - pub jwt: Option, + pub jwt: Option, pub mtls: Option, } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ServerAuthApiJwtSettings { - pub enabled: bool, - pub issuer: Option, + pub enabled: bool, + pub issuer: Option, pub audience: Option, } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ServerAuthApiMtlsSettings { pub enabled: bool, - pub ca: Option, + pub ca: Option, } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ServerAuthWebSettings { pub allowed_usernames: Vec, - pub providers: ServerAuthWebProvidersSettings, + pub providers: ServerAuthWebProvidersSettings, } #[derive(Debug, Clone, Default, PartialEq, Eq)] @@ -123,8 +123,8 @@ pub struct ServerAuthWebProvidersSettings { #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct GithubOauthSettings { - pub enabled: bool, - pub client_id: Option, + pub enabled: bool, + pub client_id: Option, pub client_secret: Option, } @@ -144,30 +144,30 @@ impl Default for ServerStorageSettings { #[derive(Debug, Clone, PartialEq, Eq)] pub struct ServerArtifactsSettings { pub prefix: InterpString, - pub store: ObjectStoreSettings, + pub store: ObjectStoreSettings, } impl Default for ServerArtifactsSettings { fn default() -> Self { Self { prefix: InterpString::parse(""), - store: ObjectStoreSettings::default(), + store: ObjectStoreSettings::default(), } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ServerSlateDbSettings { - pub prefix: InterpString, - pub store: ObjectStoreSettings, + pub prefix: InterpString, + pub store: ObjectStoreSettings, pub flush_interval: StdDuration, } impl Default for ServerSlateDbSettings { fn default() -> Self { Self { - prefix: InterpString::parse(""), - store: ObjectStoreSettings::default(), + prefix: InterpString::parse(""), + store: ObjectStoreSettings::default(), flush_interval: StdDuration::ZERO, } } @@ -179,9 +179,9 @@ pub enum ObjectStoreSettings { root: InterpString, }, S3 { - bucket: InterpString, - region: InterpString, - endpoint: Option, + bucket: InterpString, + region: InterpString, + endpoint: Option, path_style: bool, }, } @@ -206,25 +206,25 @@ pub struct ServerLoggingSettings { #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ServerIntegrationsSettings { - pub github: GithubIntegrationSettings, - pub slack: SlackIntegrationSettings, + pub github: GithubIntegrationSettings, + pub slack: SlackIntegrationSettings, pub discord: DiscordIntegrationSettings, - pub teams: TeamsIntegrationSettings, + pub teams: TeamsIntegrationSettings, } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct GithubIntegrationSettings { - pub enabled: bool, - pub app_id: Option, - pub client_id: Option, - pub slug: Option, + pub enabled: bool, + pub app_id: Option, + pub client_id: Option, + pub slug: Option, pub permissions: HashMap, - pub webhooks: Option, + pub webhooks: Option, } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct SlackIntegrationSettings { - pub enabled: bool, + pub enabled: bool, pub default_channel: Option, } @@ -248,23 +248,23 @@ pub struct IntegrationWebhooksSettings { #[serde(deny_unknown_fields)] pub struct ServerLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub listen: Option, + pub listen: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub api: Option, + pub api: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub web: Option, + pub web: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub auth: Option, + pub auth: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub storage: Option, + pub storage: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub artifacts: Option, + pub artifacts: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub slatedb: Option, + pub slatedb: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub scheduler: Option, + pub scheduler: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub logging: Option, + pub logging: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub integrations: Option, } @@ -278,7 +278,7 @@ pub enum ServerListenLayer { #[serde(default)] address: Option, #[serde(default)] - tls: Option, + tls: Option, }, Unix { #[serde(default)] @@ -292,9 +292,9 @@ pub struct ServerListenTlsLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub cert: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub key: Option, + pub key: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub ca: Option, + pub ca: Option, } /// `[server.api]` — API surface settings. @@ -314,7 +314,7 @@ pub struct ServerWebLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub url: Option, + pub url: Option, } /// `[server.auth]` — cohesive server auth surface. @@ -338,7 +338,7 @@ pub struct ServerAuthLayer { #[serde(deny_unknown_fields)] pub struct ServerAuthApiLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub jwt: Option, + pub jwt: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub mtls: Option, } @@ -348,9 +348,9 @@ pub struct ServerAuthApiLayer { #[serde(deny_unknown_fields)] pub struct ServerAuthApiJwtLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, + pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub issuer: Option, + pub issuer: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub audience: Option, } @@ -362,7 +362,7 @@ pub struct ServerAuthApiMtlsLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub ca: Option, + pub ca: Option, } /// `[server.auth.web]` — provider-neutral access rules plus keyed providers. @@ -372,7 +372,7 @@ pub struct ServerAuthWebLayer { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub allowed_usernames: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] - pub providers: Option, + pub providers: Option, } /// `[server.auth.web.providers.]` — web auth providers keyed by @@ -389,9 +389,9 @@ pub struct ServerAuthWebProvidersLayer { #[serde(deny_unknown_fields)] pub struct ServerAuthWebGithubLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, + pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub client_id: Option, + pub client_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub client_secret: Option, } @@ -411,11 +411,11 @@ pub struct ServerArtifactsLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub prefix: Option, + pub prefix: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub local: Option, + pub local: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub s3: Option, + pub s3: Option, } /// `[server.slatedb]` — SlateDB bottomless storage plus tunables. @@ -423,15 +423,15 @@ pub struct ServerArtifactsLayer { #[serde(deny_unknown_fields)] pub struct ServerSlateDbLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub provider: Option, + pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub prefix: Option, + pub prefix: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub flush_interval: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub local: Option, + pub local: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub s3: Option, + pub s3: Option, } /// Closed enum of object-store providers. Unknown providers hard-fail @@ -456,11 +456,11 @@ pub struct ObjectStoreLocalLayer { #[serde(deny_unknown_fields)] pub struct ObjectStoreS3Layer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub bucket: Option, + pub bucket: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub region: Option, + pub region: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub endpoint: Option, + pub endpoint: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub path_style: Option, } @@ -489,13 +489,13 @@ pub struct ServerLoggingLayer { #[serde(deny_unknown_fields)] pub struct ServerIntegrationsLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub github: Option, + pub github: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub slack: Option, + pub slack: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub discord: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub teams: Option, + pub teams: Option, } /// `[server.integrations.github]` — GitHub App, credentials, and inbound @@ -504,17 +504,17 @@ pub struct ServerIntegrationsLayer { #[serde(deny_unknown_fields)] pub struct GithubIntegrationLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, + pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub app_id: Option, + pub app_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub client_id: Option, + pub client_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub slug: Option, + pub slug: Option, #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub permissions: HashMap, #[serde(default, skip_serializing_if = "Option::is_none")] - pub webhooks: Option, + pub webhooks: Option, } /// `[server.integrations.slack]` — Slack workspace credentials and defaults. @@ -522,7 +522,7 @@ pub struct GithubIntegrationLayer { #[serde(deny_unknown_fields)] pub struct SlackIntegrationLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, + pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub default_channel: Option, } diff --git a/lib/crates/fabro-types/src/settings/size.rs b/lib/crates/fabro-types/src/settings/size.rs index 26803ffe9..c4fffb839 100644 --- a/lib/crates/fabro-types/src/settings/size.rs +++ b/lib/crates/fabro-types/src/settings/size.rs @@ -125,7 +125,7 @@ impl FromStr for Size { other => { return Err(ParseSizeError::InvalidUnit { input: input.to_owned(), - unit: other.to_owned(), + unit: other.to_owned(), }); } }; diff --git a/lib/crates/fabro-types/src/settings/workflow.rs b/lib/crates/fabro-types/src/settings/workflow.rs index 7dc0d0b27..42f8d38c7 100644 --- a/lib/crates/fabro-types/src/settings/workflow.rs +++ b/lib/crates/fabro-types/src/settings/workflow.rs @@ -10,10 +10,10 @@ use serde::{Deserialize, Serialize}; /// A structurally resolved `[workflow]` view for consumers. #[derive(Debug, Clone, Default, PartialEq)] pub struct WorkflowSettings { - pub name: Option, + pub name: Option, pub description: Option, - pub graph: String, - pub metadata: HashMap, + pub graph: String, + pub metadata: HashMap, } /// A sparse `[workflow]` layer as it appears in a single settings file. @@ -21,12 +21,12 @@ pub struct WorkflowSettings { #[serde(deny_unknown_fields)] pub struct WorkflowLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub name: Option, + pub name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, /// Optional override for the default `workflow.fabro` graph path. #[serde(default, skip_serializing_if = "Option::is_none")] - pub graph: Option, + pub graph: Option, #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub metadata: HashMap, + pub metadata: HashMap, } diff --git a/lib/crates/fabro-types/src/stage_id.rs b/lib/crates/fabro-types/src/stage_id.rs index baae58864..eef135774 100644 --- a/lib/crates/fabro-types/src/stage_id.rs +++ b/lib/crates/fabro-types/src/stage_id.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct StageId { node_id: String, - visit: u32, + visit: u32, } impl StageId { @@ -190,11 +190,14 @@ mod tests { StageId::new("code", 1), ]; stages.sort(); - assert_eq!(stages, vec![ - StageId::new("build", 1), - StageId::new("code", 1), - StageId::new("code", 2), - ]); + assert_eq!( + stages, + vec![ + StageId::new("build", 1), + StageId::new("code", 1), + StageId::new("code", 2), + ] + ); } #[test] diff --git a/lib/crates/fabro-types/src/start.rs b/lib/crates/fabro-types/src/start.rs index 82c200575..6a78addd6 100644 --- a/lib/crates/fabro-types/src/start.rs +++ b/lib/crates/fabro-types/src/start.rs @@ -5,10 +5,10 @@ use crate::run_id::RunId; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StartRecord { - pub run_id: RunId, + pub run_id: RunId, pub start_time: DateTime, #[serde(default, skip_serializing_if = "Option::is_none")] pub run_branch: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub base_sha: Option, + pub base_sha: Option, } diff --git a/lib/crates/fabro-types/src/status.rs b/lib/crates/fabro-types/src/status.rs index 411496be9..7b6f9189e 100644 --- a/lib/crates/fabro-types/src/status.rs +++ b/lib/crates/fabro-types/src/status.rs @@ -109,7 +109,7 @@ impl std::error::Error for ParseRunStatusError {} #[derive(Debug, Clone, PartialEq)] pub struct InvalidTransition { pub from: RunStatus, - pub to: RunStatus, + pub to: RunStatus, } impl fmt::Display for InvalidTransition { @@ -146,9 +146,9 @@ pub enum RunControlAction { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RunStatusRecord { - pub status: RunStatus, + pub status: RunStatus, #[serde(default, skip_serializing_if = "Option::is_none")] - pub reason: Option, + pub reason: Option, pub updated_at: DateTime, } diff --git a/lib/crates/fabro-types/tests/run_event_serde.rs b/lib/crates/fabro-types/tests/run_event_serde.rs index 1ccb99cb1..8a2e71b64 100644 --- a/lib/crates/fabro-types/tests/run_event_serde.rs +++ b/lib/crates/fabro-types/tests/run_event_serde.rs @@ -37,20 +37,20 @@ fn templated_settings() -> SettingsLayer { #[test] fn run_created_props_round_trip_templated_settings() { let props = RunCreatedProps { - settings: templated_settings(), - graph: Graph::new("ship"), - workflow_source: Some("digraph Ship { start -> exit }".to_string()), - workflow_config: Some("[run]\ngoal = \"Ship {{ env.TASK }}\"".to_string()), - labels: BTreeMap::from([("team".to_string(), "platform".to_string())]), - run_dir: "/tmp/run".to_string(), + settings: templated_settings(), + graph: Graph::new("ship"), + workflow_source: Some("digraph Ship { start -> exit }".to_string()), + workflow_config: Some("[run]\ngoal = \"Ship {{ env.TASK }}\"".to_string()), + labels: BTreeMap::from([("team".to_string(), "platform".to_string())]), + run_dir: "/tmp/run".to_string(), working_directory: "/tmp/project".to_string(), - host_repo_path: Some("/tmp/project".to_string()), - repo_origin_url: Some("https://github.com/fabro-sh/fabro.git".to_string()), - base_branch: Some("main".to_string()), - workflow_slug: Some("demo".to_string()), - db_prefix: Some("run_".to_string()), - provenance: None, - manifest_blob: None, + host_repo_path: Some("/tmp/project".to_string()), + repo_origin_url: Some("https://github.com/fabro-sh/fabro.git".to_string()), + base_branch: Some("main".to_string()), + workflow_slug: Some("demo".to_string()), + db_prefix: Some("run_".to_string()), + provenance: None, + manifest_blob: None, }; let json = serde_json::to_value(&props).expect("props should serialize"); diff --git a/lib/crates/fabro-types/tests/run_record_serde.rs b/lib/crates/fabro-types/tests/run_record_serde.rs index 9212dc640..bf35b4c2a 100644 --- a/lib/crates/fabro-types/tests/run_record_serde.rs +++ b/lib/crates/fabro-types/tests/run_record_serde.rs @@ -39,18 +39,18 @@ fn templated_settings() -> SettingsLayer { #[test] fn run_record_round_trips_templated_settings() { let record = RunRecord { - run_id: fixtures::RUN_1, - settings: templated_settings(), - graph: Graph::new("ship"), - workflow_slug: Some("demo".to_string()), + run_id: fixtures::RUN_1, + settings: templated_settings(), + graph: Graph::new("ship"), + workflow_slug: Some("demo".to_string()), working_directory: PathBuf::from("/tmp/project"), - host_repo_path: Some("/tmp/project".to_string()), - repo_origin_url: Some("https://github.com/fabro-sh/fabro.git".to_string()), - base_branch: Some("main".to_string()), - labels: HashMap::from([("team".to_string(), "platform".to_string())]), - provenance: None, - manifest_blob: None, - definition_blob: None, + host_repo_path: Some("/tmp/project".to_string()), + repo_origin_url: Some("https://github.com/fabro-sh/fabro.git".to_string()), + base_branch: Some("main".to_string()), + labels: HashMap::from([("team".to_string(), "platform".to_string())]), + provenance: None, + manifest_blob: None, + definition_blob: None, }; let json = serde_json::to_value(&record).expect("record should serialize"); diff --git a/lib/crates/fabro-util/build.rs b/lib/crates/fabro-util/build.rs index 2c007b06a..8e7bb6470 100644 --- a/lib/crates/fabro-util/build.rs +++ b/lib/crates/fabro-util/build.rs @@ -7,25 +7,25 @@ use serde::Deserialize; #[derive(Deserialize)] struct Config { allowlist: Option, - rules: Vec, + rules: Vec, } #[derive(Deserialize)] struct GlobalAllowlist { - regexes: Option>, + regexes: Option>, stopwords: Option>, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct Rule { - id: String, - regex: String, + id: String, + regex: String, #[serde(default)] - keywords: Vec, - entropy: Option, + keywords: Vec, + entropy: Option, #[serde(default)] - allowlist: Option, + allowlist: Option, #[allow(dead_code)] description: Option, } @@ -33,8 +33,8 @@ struct Rule { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct RuleAllowlist { - regexes: Option>, - stopwords: Option>, + regexes: Option>, + stopwords: Option>, regex_target: Option, } diff --git a/lib/crates/fabro-util/src/backoff.rs b/lib/crates/fabro-util/src/backoff.rs index 35a47a2da..7784dc6ed 100644 --- a/lib/crates/fabro-util/src/backoff.rs +++ b/lib/crates/fabro-util/src/backoff.rs @@ -5,18 +5,18 @@ use rand::Rng; #[derive(Debug, Clone)] pub struct BackoffPolicy { pub initial_delay: Duration, - pub factor: f64, - pub max_delay: Duration, - pub jitter: bool, + pub factor: f64, + pub max_delay: Duration, + pub jitter: bool, } impl Default for BackoffPolicy { fn default() -> Self { Self { initial_delay: Duration::from_secs(1), - factor: 2.0, - max_delay: Duration::from_secs(60), - jitter: false, + factor: 2.0, + max_delay: Duration::from_secs(60), + jitter: false, } } } @@ -50,9 +50,9 @@ mod tests { fn delay_first_attempt() { let b = BackoffPolicy { initial_delay: Duration::from_millis(100), - factor: 2.0, - max_delay: Duration::from_secs(10), - jitter: false, + factor: 2.0, + max_delay: Duration::from_secs(10), + jitter: false, }; assert_eq!(b.delay_for_attempt(1), Duration::from_millis(100)); } @@ -61,9 +61,9 @@ mod tests { fn delay_exponential() { let b = BackoffPolicy { initial_delay: Duration::from_millis(100), - factor: 2.0, - max_delay: Duration::from_secs(10), - jitter: false, + factor: 2.0, + max_delay: Duration::from_secs(10), + jitter: false, }; assert_eq!(b.delay_for_attempt(2), Duration::from_millis(200)); assert_eq!(b.delay_for_attempt(3), Duration::from_millis(400)); @@ -74,9 +74,9 @@ mod tests { fn delay_capped_at_max() { let b = BackoffPolicy { initial_delay: Duration::from_millis(100), - factor: 2.0, - max_delay: Duration::from_millis(300), - jitter: false, + factor: 2.0, + max_delay: Duration::from_millis(300), + jitter: false, }; assert_eq!(b.delay_for_attempt(1), Duration::from_millis(100)); assert_eq!(b.delay_for_attempt(2), Duration::from_millis(200)); @@ -88,9 +88,9 @@ mod tests { fn delay_with_jitter_within_range() { let b = BackoffPolicy { initial_delay: Duration::from_millis(1000), - factor: 1.0, - max_delay: Duration::from_secs(10), - jitter: true, + factor: 1.0, + max_delay: Duration::from_secs(10), + jitter: true, }; let base = Duration::from_millis(1000); let min = base.mul_f64(0.5); @@ -109,9 +109,9 @@ mod tests { fn delay_linear_factor() { let b = BackoffPolicy { initial_delay: Duration::from_millis(500), - factor: 1.0, - max_delay: Duration::from_secs(60), - jitter: false, + factor: 1.0, + max_delay: Duration::from_secs(60), + jitter: false, }; assert_eq!(b.delay_for_attempt(1), Duration::from_millis(500)); assert_eq!(b.delay_for_attempt(2), Duration::from_millis(500)); diff --git a/lib/crates/fabro-util/src/check_report.rs b/lib/crates/fabro-util/src/check_report.rs index 4f07871b6..b68d9922f 100644 --- a/lib/crates/fabro-util/src/check_report.rs +++ b/lib/crates/fabro-util/src/check_report.rs @@ -30,22 +30,22 @@ impl CheckDetail { #[derive(Debug, Clone, Serialize)] pub struct CheckResult { - pub name: String, - pub status: CheckStatus, - pub summary: String, - pub details: Vec, + pub name: String, + pub status: CheckStatus, + pub summary: String, + pub details: Vec, pub remediation: Option, } #[derive(Debug, Clone, Serialize)] pub struct CheckSection { - pub title: String, + pub title: String, pub checks: Vec, } #[derive(Debug, Clone, Serialize)] pub struct CheckReport { - pub title: String, + pub title: String, pub sections: Vec, } @@ -195,37 +195,37 @@ mod tests { fn pass_check(name: &str) -> CheckResult { CheckResult { - name: name.to_string(), - status: CheckStatus::Pass, - summary: "all good".to_string(), - details: vec![CheckDetail::new("everything is fine".to_string())], + name: name.to_string(), + status: CheckStatus::Pass, + summary: "all good".to_string(), + details: vec![CheckDetail::new("everything is fine".to_string())], remediation: None, } } fn warning_check(name: &str) -> CheckResult { CheckResult { - name: name.to_string(), - status: CheckStatus::Warning, - summary: "not configured".to_string(), - details: vec![CheckDetail::new("missing something".to_string())], + name: name.to_string(), + status: CheckStatus::Warning, + summary: "not configured".to_string(), + details: vec![CheckDetail::new("missing something".to_string())], remediation: Some("fix it".to_string()), } } fn error_check(name: &str) -> CheckResult { CheckResult { - name: name.to_string(), - status: CheckStatus::Error, - summary: "broken".to_string(), - details: vec![CheckDetail::new("something is wrong".to_string())], + name: name.to_string(), + status: CheckStatus::Error, + summary: "broken".to_string(), + details: vec![CheckDetail::new("something is wrong".to_string())], remediation: Some("repair it".to_string()), } } fn report(checks: Vec) -> CheckReport { CheckReport { - title: "Test Report".into(), + title: "Test Report".into(), sections: vec![CheckSection { title: String::new(), checks, @@ -410,9 +410,9 @@ mod tests { #[test] fn render_uses_custom_title() { let r = CheckReport { - title: "My Custom Title".into(), + title: "My Custom Title".into(), sections: vec![CheckSection { - title: String::new(), + title: String::new(), checks: vec![pass_check("Test")], }], }; @@ -431,10 +431,10 @@ mod tests { #[test] fn render_truncates_long_detail_lines() { let r = report(vec![CheckResult { - name: "Test".into(), - status: CheckStatus::Pass, - summary: "ok".into(), - details: vec![CheckDetail::new( + name: "Test".into(), + status: CheckStatus::Pass, + summary: "ok".into(), + details: vec![CheckDetail::new( "This is a very long detail line for test".into(), )], remediation: None, @@ -454,10 +454,10 @@ mod tests { #[test] fn render_no_truncation_when_fits() { let r = report(vec![CheckResult { - name: "Test".into(), - status: CheckStatus::Pass, - summary: "ok".into(), - details: vec![CheckDetail::new("short".into())], + name: "Test".into(), + status: CheckStatus::Pass, + summary: "ok".into(), + details: vec![CheckDetail::new("short".into())], remediation: None, }]); let out = r.render(&Styles::new(false), true, None, Some(80)); @@ -470,10 +470,10 @@ mod tests { #[test] fn render_warn_detail_uses_red() { let r = report(vec![CheckResult { - name: "Repo".into(), - status: CheckStatus::Pass, - summary: "ok".into(), - details: vec![CheckDetail { + name: "Repo".into(), + status: CheckStatus::Pass, + summary: "ok".into(), + details: vec![CheckDetail { text: "Git clean: false".into(), warn: true, }], diff --git a/lib/crates/fabro-util/src/redact/gitleaks.rs b/lib/crates/fabro-util/src/redact/gitleaks.rs index 859750ead..e38b3666a 100644 --- a/lib/crates/fabro-util/src/redact/gitleaks.rs +++ b/lib/crates/fabro-util/src/redact/gitleaks.rs @@ -215,7 +215,7 @@ impl GitleaksEngine { regions.push(Region { start: secret_match.start(), - end: secret_match.end(), + end: secret_match.end(), }); } } diff --git a/lib/crates/fabro-util/src/redact/mod.rs b/lib/crates/fabro-util/src/redact/mod.rs index ba472c917..25127fbd0 100644 --- a/lib/crates/fabro-util/src/redact/mod.rs +++ b/lib/crates/fabro-util/src/redact/mod.rs @@ -8,7 +8,7 @@ pub use jsonl::{redact_json_value, redact_jsonl_line}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Region { pub start: usize, - pub end: usize, + pub end: usize, } /// Replace all detected secrets in `s` with "REDACTED". diff --git a/lib/crates/fabro-util/src/run_log.rs b/lib/crates/fabro-util/src/run_log.rs index 75ec04a73..05211a64f 100644 --- a/lib/crates/fabro-util/src/run_log.rs +++ b/lib/crates/fabro-util/src/run_log.rs @@ -15,14 +15,14 @@ static RUN_LOG: OnceLock = OnceLock::new(); #[derive(Clone, Debug)] pub struct RunLogWriter { active: Arc, - file: Arc>>>, + file: Arc>>>, } impl RunLogWriter { fn new() -> Self { Self { active: Arc::new(AtomicBool::new(false)), - file: Arc::new(Mutex::new(None)), + file: Arc::new(Mutex::new(None)), } } } @@ -33,7 +33,7 @@ impl<'a> MakeWriter<'a> for RunLogWriter { fn make_writer(&'a self) -> Self::Writer { if self.active.load(Ordering::Relaxed) { RunLogGuard::Active { - buf: Vec::new(), + buf: Vec::new(), file: self.file.clone(), } } else { @@ -46,7 +46,7 @@ impl<'a> MakeWriter<'a> for RunLogWriter { pub enum RunLogGuard { Inactive, Active { - buf: Vec, + buf: Vec, file: Arc>>>, }, } diff --git a/lib/crates/fabro-util/src/terminal.rs b/lib/crates/fabro-util/src/terminal.rs index b65c9bfd9..6d8a2d0b5 100644 --- a/lib/crates/fabro-util/src/terminal.rs +++ b/lib/crates/fabro-util/src/terminal.rs @@ -4,19 +4,19 @@ use console::Style; /// Each style is forced on/off based on the `use_color` flag passed to /// [`Styles::new`]. pub struct Styles { - pub use_color: bool, - pub bold: Style, - pub dim: Style, - pub cyan: Style, - pub green: Style, - pub yellow: Style, - pub red: Style, - pub magenta: Style, - pub underline: Style, - pub bold_dim: Style, - pub bold_cyan: Style, + pub use_color: bool, + pub bold: Style, + pub dim: Style, + pub cyan: Style, + pub green: Style, + pub yellow: Style, + pub red: Style, + pub magenta: Style, + pub underline: Style, + pub bold_dim: Style, + pub bold_cyan: Style, pub bold_green: Style, - pub bold_red: Style, + pub bold_red: Style, } impl Styles { diff --git a/lib/crates/fabro-validate/src/lib.rs b/lib/crates/fabro-validate/src/lib.rs index 733a3a605..3438d842d 100644 --- a/lib/crates/fabro-validate/src/lib.rs +++ b/lib/crates/fabro-validate/src/lib.rs @@ -14,12 +14,12 @@ pub enum Severity { /// A validation diagnostic produced by a lint rule. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Diagnostic { - pub rule: String, + pub rule: String, pub severity: Severity, - pub message: String, - pub node_id: Option, - pub edge: Option<(String, String)>, - pub fix: Option, + pub message: String, + pub node_id: Option, + pub edge: Option<(String, String)>, + pub fix: Option, } /// A lint rule that validates a graph. @@ -159,12 +159,12 @@ mod tests { } fn apply(&self, _graph: &Graph) -> Vec { vec![Diagnostic { - rule: "always_warn".to_string(), + rule: "always_warn".to_string(), severity: Severity::Warning, - message: "custom warning".to_string(), - node_id: None, - edge: None, - fix: None, + message: "custom warning".to_string(), + node_id: None, + edge: None, + fix: None, }] } } diff --git a/lib/crates/fabro-validate/src/rules.rs b/lib/crates/fabro-validate/src/rules.rs index 5be714324..20ecd7f8d 100644 --- a/lib/crates/fabro-validate/src/rules.rs +++ b/lib/crates/fabro-validate/src/rules.rs @@ -57,26 +57,26 @@ impl LintRule for StartNodeRule { .count(); if start_count == 0 { return vec![Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Error, message: "Pipeline must have exactly one start node (shape=Mdiamond or id start/Start)" .to_string(), - node_id: None, - edge: None, - fix: Some("Add a node with shape=Mdiamond or id 'start'".to_string()), + node_id: None, + edge: None, + fix: Some("Add a node with shape=Mdiamond or id 'start'".to_string()), }]; } if start_count > 1 { return vec![Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Error, - message: format!( + message: format!( "Pipeline has {start_count} start nodes but must have exactly one" ), - node_id: None, - edge: None, - fix: Some("Remove extra start nodes".to_string()), + node_id: None, + edge: None, + fix: Some("Remove extra start nodes".to_string()), }]; } Vec::new() @@ -106,26 +106,26 @@ impl LintRule for TerminalNodeRule { .count(); if terminal_count == 0 { return vec![Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Error, message: "Pipeline must have exactly one terminal node (shape=Msquare or id exit/end)" .to_string(), - node_id: None, - edge: None, - fix: Some("Add a node with shape=Msquare or id 'exit'/'end'".to_string()), + node_id: None, + edge: None, + fix: Some("Add a node with shape=Msquare or id 'exit'/'end'".to_string()), }]; } if terminal_count > 1 { return vec![Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Error, - message: format!( + message: format!( "Pipeline must have exactly one terminal node, found {terminal_count}" ), - node_id: None, - edge: None, - fix: Some("Remove extra terminal nodes so exactly one remains".to_string()), + node_id: None, + edge: None, + fix: Some("Remove extra terminal nodes so exactly one remains".to_string()), }]; } Vec::new() @@ -170,12 +170,12 @@ impl LintRule for ReachabilityRule { unreachable .into_iter() .map(|node_id| Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Warning, - message: format!("Node '{node_id}' is not reachable from the start node"), - node_id: Some(node_id.to_string()), - edge: None, - fix: Some(format!( + message: format!("Node '{node_id}' is not reachable from the start node"), + node_id: Some(node_id.to_string()), + edge: None, + fix: Some(format!( "Add an edge path from the start node to '{node_id}'" )), }) @@ -197,25 +197,25 @@ impl LintRule for EdgeTargetExistsRule { for edge in &graph.edges { if !graph.nodes.contains_key(&edge.to) { diagnostics.push(Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Error, - message: format!( + message: format!( "Edge from '{}' targets non-existent node '{}'", edge.from, edge.to ), - node_id: None, - edge: Some((edge.from.clone(), edge.to.clone())), - fix: Some(format!("Define node '{}' or fix the edge target", edge.to)), + node_id: None, + edge: Some((edge.from.clone(), edge.to.clone())), + fix: Some(format!("Define node '{}' or fix the edge target", edge.to)), }); } if !graph.nodes.contains_key(&edge.from) { diagnostics.push(Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Error, - message: format!("Edge source '{}' references non-existent node", edge.from), - node_id: None, - edge: Some((edge.from.clone(), edge.to.clone())), - fix: Some(format!( + message: format!("Edge source '{}' references non-existent node", edge.from), + node_id: None, + edge: Some((edge.from.clone(), edge.to.clone())), + fix: Some(format!( "Define node '{}' or fix the edge source", edge.from )), @@ -242,16 +242,16 @@ impl LintRule for StartNoIncomingRule { let incoming = graph.incoming_edges(&start.id); if !incoming.is_empty() { return vec![Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Error, - message: format!( + message: format!( "Start node '{}' has {} incoming edge(s) but must have none", start.id, incoming.len() ), - node_id: Some(start.id.clone()), - edge: None, - fix: Some("Remove incoming edges to the start node".to_string()), + node_id: Some(start.id.clone()), + edge: None, + fix: Some("Remove incoming edges to the start node".to_string()), }]; } Vec::new() @@ -279,16 +279,16 @@ impl LintRule for ExitNoOutgoingRule { let outgoing = graph.outgoing_edges(&node.id); if !outgoing.is_empty() { diagnostics.push(Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Error, - message: format!( + message: format!( "Exit node '{}' has {} outgoing edge(s) but must have none", node.id, outgoing.len() ), - node_id: Some(node.id.clone()), - edge: None, - fix: Some("Remove outgoing edges from the exit node".to_string()), + node_id: Some(node.id.clone()), + edge: None, + fix: Some("Remove outgoing edges from the exit node".to_string()), }); } } @@ -317,15 +317,15 @@ impl LintRule for ConditionSyntaxRule { } if let Err(e) = parse_condition(condition) { diagnostics.push(Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Error, - message: format!( + message: format!( "Condition '{condition}' on edge {} -> {} failed parse: {e}", edge.from, edge.to ), - node_id: None, - edge: Some((edge.from.clone(), edge.to.clone())), - fix: Some( + node_id: None, + edge: Some((edge.from.clone(), edge.to.clone())), + fix: Some( "Use key=value, key!=value, key>value, key contains value, \ key matches pattern, or bare key syntax" .to_string(), @@ -354,12 +354,12 @@ impl LintRule for StylesheetSyntaxRule { match parse_stylesheet(stylesheet) { Ok(_) => Vec::new(), Err(e) => vec![Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Error, - message: format!("Model stylesheet parse error: {e}"), - node_id: None, - edge: None, - fix: Some("Fix the model_stylesheet syntax".to_string()), + message: format!("Model stylesheet parse error: {e}"), + node_id: None, + edge: None, + fix: Some("Fix the model_stylesheet syntax".to_string()), }], } } @@ -397,12 +397,12 @@ impl LintRule for TypeKnownRule { if let Some(node_type) = node.node_type() { if !KNOWN_HANDLER_TYPES.contains(&node_type) { diagnostics.push(Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Warning, - message: format!("Node '{}' has unrecognized type '{node_type}'", node.id), - node_id: Some(node.id.clone()), - edge: None, - fix: Some(format!("Use one of: {}", KNOWN_HANDLER_TYPES.join(", "))), + message: format!("Node '{}' has unrecognized type '{node_type}'", node.id), + node_id: Some(node.id.clone()), + edge: None, + fix: Some(format!("Use one of: {}", KNOWN_HANDLER_TYPES.join(", "))), }); } } @@ -446,15 +446,15 @@ impl LintRule for FidelityValidRule { if let Some(fidelity) = node.fidelity() { if fidelity.parse::().is_err() { diagnostics.push(Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Warning, - message: format!( + message: format!( "Node '{}' has invalid fidelity mode '{fidelity}'", node.id ), - node_id: Some(node.id.clone()), - edge: None, - fix: Some(Self::fix_message()), + node_id: Some(node.id.clone()), + edge: None, + fix: Some(Self::fix_message()), }); } } @@ -463,15 +463,15 @@ impl LintRule for FidelityValidRule { if let Some(fidelity) = edge.fidelity() { if fidelity.parse::().is_err() { diagnostics.push(Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Warning, - message: format!( + message: format!( "Edge {} -> {} has invalid fidelity mode '{fidelity}'", edge.from, edge.to ), - node_id: None, - edge: Some((edge.from.clone(), edge.to.clone())), - fix: Some(Self::fix_message()), + node_id: None, + edge: Some((edge.from.clone(), edge.to.clone())), + fix: Some(Self::fix_message()), }); } } @@ -479,12 +479,12 @@ impl LintRule for FidelityValidRule { if let Some(fidelity) = graph.default_fidelity() { if fidelity.parse::().is_err() { diagnostics.push(Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Warning, - message: format!("Graph has invalid default_fidelity '{fidelity}'"), - node_id: None, - edge: None, - fix: Some(Self::fix_message()), + message: format!("Graph has invalid default_fidelity '{fidelity}'"), + node_id: None, + edge: None, + fix: Some(Self::fix_message()), }); } } @@ -507,30 +507,30 @@ impl LintRule for RetryTargetExistsRule { if let Some(target) = node.retry_target() { if !graph.nodes.contains_key(target) { diagnostics.push(Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Warning, - message: format!( + message: format!( "Node '{}' has retry_target '{}' that does not exist", node.id, target ), - node_id: Some(node.id.clone()), - edge: None, - fix: Some(format!("Define node '{target}' or fix retry_target")), + node_id: Some(node.id.clone()), + edge: None, + fix: Some(format!("Define node '{target}' or fix retry_target")), }); } } if let Some(target) = node.fallback_retry_target() { if !graph.nodes.contains_key(target) { diagnostics.push(Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Warning, - message: format!( + message: format!( "Node '{}' has fallback_retry_target '{}' that does not exist", node.id, target ), - node_id: Some(node.id.clone()), - edge: None, - fix: Some(format!( + node_id: Some(node.id.clone()), + edge: None, + fix: Some(format!( "Define node '{target}' or fix fallback_retry_target" )), }); @@ -540,26 +540,26 @@ impl LintRule for RetryTargetExistsRule { if let Some(target) = graph.retry_target() { if !graph.nodes.contains_key(target) { diagnostics.push(Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Warning, - message: format!("Graph has retry_target '{target}' that does not exist"), - node_id: None, - edge: None, - fix: Some(format!("Define node '{target}' or fix graph retry_target")), + message: format!("Graph has retry_target '{target}' that does not exist"), + node_id: None, + edge: None, + fix: Some(format!("Define node '{target}' or fix graph retry_target")), }); } } if let Some(target) = graph.fallback_retry_target() { if !graph.nodes.contains_key(target) { diagnostics.push(Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Warning, - message: format!( + message: format!( "Graph has fallback_retry_target '{target}' that does not exist" ), - node_id: None, - edge: None, - fix: Some(format!( + node_id: None, + edge: None, + fix: Some(format!( "Define node '{target}' or fix graph fallback_retry_target" )), }); @@ -628,15 +628,12 @@ impl LintRule for PromptOnLlmNodesRule { .is_some_and(|l| !l.is_empty()); if !has_prompt && !has_label { diagnostics.push(Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Warning, - message: format!( - "LLM node '{}' has no prompt or label attribute", - node.id - ), - node_id: Some(node.id.clone()), - edge: None, - fix: Some("Add a prompt or label attribute".to_string()), + message: format!("LLM node '{}' has no prompt or label attribute", node.id), + node_id: Some(node.id.clone()), + edge: None, + fix: Some("Add a prompt or label attribute".to_string()), }); } } @@ -703,12 +700,12 @@ impl LintRule for DirectionValidRule { return Vec::new(); } vec![Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Warning, - message: format!("Graph has invalid rankdir '{rankdir}'"), - node_id: None, - edge: None, - fix: Some(format!("Use one of: {}", VALID_DIRECTIONS.join(", "))), + message: format!("Graph has invalid rankdir '{rankdir}'"), + node_id: None, + edge: None, + fix: Some(format!("Use one of: {}", VALID_DIRECTIONS.join(", "))), }] } } @@ -732,15 +729,15 @@ impl LintRule for ReservedKeywordNodeIdRule { .values() .filter(|node| DOT_RESERVED_KEYWORDS.contains(&node.id.to_lowercase().as_str())) .map(|node| Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Warning, - message: format!( + message: format!( "Node ID '{}' is a DOT reserved keyword and may cause parsing failures", node.id ), - node_id: Some(node.id.clone()), - edge: None, - fix: Some(format!( + node_id: Some(node.id.clone()), + edge: None, + fix: Some(format!( "Rename '{}' to '{}_step' or another non-reserved ID", node.id, node.id.to_lowercase() @@ -1052,23 +1049,23 @@ impl LintRule for ImportErrorRule { for node in graph.nodes.values() { if let Some(AttrValue::String(message)) = node.attrs.get("import_error") { diagnostics.push(Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Error, - message: message.clone(), - node_id: Some(node.id.clone()), - edge: None, - fix: Some("Fix the imported workflow or import path".to_string()), + message: message.clone(), + node_id: Some(node.id.clone()), + edge: None, + fix: Some("Fix the imported workflow or import path".to_string()), }); } if node.attrs.contains_key("import") { diagnostics.push(Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Error, - message: "unresolved import (no base directory available)".to_string(), - node_id: Some(node.id.clone()), - edge: None, - fix: Some( + message: "unresolved import (no base directory available)".to_string(), + node_id: Some(node.id.clone()), + edge: None, + fix: Some( "Load the workflow from a file so imports can resolve relative to it" .to_string(), ), @@ -1148,15 +1145,15 @@ impl LintRule for ThreadIdRequiresFidelityFullRule { if node.thread_id().is_some() && node.fidelity() != Some("full") && !graph_default_full { diagnostics.push(Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Warning, - message: format!( + message: format!( "Node '{}' has thread_id but fidelity is not 'full'", node.id ), - node_id: Some(node.id.clone()), - edge: None, - fix: Some(Self::FIX.to_string()), + node_id: Some(node.id.clone()), + edge: None, + fix: Some(Self::FIX.to_string()), }); } } @@ -1168,15 +1165,15 @@ impl LintRule for ThreadIdRequiresFidelityFullRule { graph.nodes.get(&edge.to).and_then(|n| n.fidelity()) == Some("full"); if !edge_full && !target_full && !graph_default_full { diagnostics.push(Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Warning, - message: format!( + message: format!( "Edge {} -> {} has thread_id but fidelity is not 'full'", edge.from, edge.to ), - node_id: None, - edge: Some((edge.from.clone(), edge.to.clone())), - fix: Some(Self::FIX.to_string()), + node_id: None, + edge: Some((edge.from.clone(), edge.to.clone())), + fix: Some(Self::FIX.to_string()), }); } } @@ -1184,12 +1181,12 @@ impl LintRule for ThreadIdRequiresFidelityFullRule { if graph.default_thread().is_some() && !graph_default_full { diagnostics.push(Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Warning, - message: "Graph has default_thread but default_fidelity is not 'full'".to_string(), - node_id: None, - edge: None, - fix: Some(Self::FIX.to_string()), + message: "Graph has default_thread but default_fidelity is not 'full'".to_string(), + node_id: None, + edge: None, + fix: Some(Self::FIX.to_string()), }); } @@ -1214,12 +1211,12 @@ impl LintRule for SelectionValidRule { if let Some(sel) = node.attrs.get("selection").and_then(AttrValue::as_str) { if !VALID_SELECTIONS.contains(&sel) { diagnostics.push(Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Warning, - message: format!("Node '{}' has invalid selection mode '{sel}'", node.id), - node_id: Some(node.id.clone()), - edge: None, - fix: Some(format!("Use one of: {}", VALID_SELECTIONS.join(", "))), + message: format!("Node '{}' has invalid selection mode '{sel}'", node.id), + node_id: Some(node.id.clone()), + edge: None, + fix: Some(format!("Use one of: {}", VALID_SELECTIONS.join(", "))), }); } } diff --git a/lib/crates/fabro-workflow/src/artifact.rs b/lib/crates/fabro-workflow/src/artifact.rs index eb3b73f59..3827368e7 100644 --- a/lib/crates/fabro-workflow/src/artifact.rs +++ b/lib/crates/fabro-workflow/src/artifact.rs @@ -11,7 +11,7 @@ use futures::future::BoxFuture; use serde_json::Value; use crate::context::{self, Context}; -use crate::error::{FabroError, Result}; +use crate::error::{Error, Result}; use crate::outcome::Outcome; use crate::records::Checkpoint; use crate::runtime_store::RunStoreHandle; @@ -38,13 +38,13 @@ pub async fn offload_large_values( ) -> Result<()> { for value in updates.values_mut() { let bytes = serde_json::to_vec(&*value) - .map_err(|e| FabroError::engine(format!("artifact serialize failed: {e}")))?; + .map_err(|e| Error::engine(format!("artifact serialize failed: {e}")))?; if bytes.len() > BLOB_OFFLOAD_THRESHOLD { let blob_id = run_store .write_blob(&bytes) .await - .map_err(|e| FabroError::engine(format!("artifact blob write failed: {e}")))?; + .map_err(|e| Error::engine(format!("artifact blob write failed: {e}")))?; *value = Value::String(format_blob_ref(&blob_id)); } } @@ -168,14 +168,14 @@ pub async fn sync_artifacts_to_env( Ok(true) => continue, Ok(false) => {} Err(e) => { - return Err(FabroError::engine(format!( + return Err(Error::engine(format!( "failed to check artifact existence: {e}" ))); } } let content = std::fs::read_to_string(&local_path).map_err(|e| { - FabroError::engine(format!("failed to read local artifact {local_path}: {e}")) + Error::engine(format!("failed to read local artifact {local_path}: {e}")) })?; let filename = std::path::Path::new(&local_path) @@ -185,9 +185,9 @@ pub async fn sync_artifacts_to_env( let remote_path = format!("{}/.fabro/artifacts/{filename}", env.working_directory()); - env.write_file(&remote_path, &content).await.map_err(|e| { - FabroError::engine(format!("failed to write artifact to remote env: {e}")) - })?; + env.write_file(&remote_path, &content) + .await + .map_err(|e| Error::engine(format!("failed to write artifact to remote env: {e}")))?; *value = Value::String(format!("{ARTIFACT_POINTER_PREFIX}{remote_path}")); } @@ -275,8 +275,8 @@ async fn materialize_blob_ref( let bytes = run_store .read_blob(blob_id) .await - .map_err(|e| FabroError::engine(format!("artifact blob read failed: {e}")))? - .ok_or_else(|| FabroError::engine(format!("artifact blob missing: {blob_id}")))?; + .map_err(|e| Error::engine(format!("artifact blob read failed: {e}")))? + .ok_or_else(|| Error::engine(format!("artifact blob missing: {blob_id}")))?; if is_local_execution(env, run_dir).await? { let path = local_materialized_blob_path(run_dir, blob_id); @@ -293,14 +293,13 @@ async fn materialize_blob_ref( if !env .file_exists(&remote_path) .await - .map_err(|e| FabroError::engine(format!("failed to check blob existence: {e}")))? + .map_err(|e| Error::engine(format!("failed to check blob existence: {e}")))? { - let content = String::from_utf8(bytes.to_vec()).map_err(|e| { - FabroError::engine(format!("artifact blob was not valid UTF-8 JSON: {e}")) - })?; - env.write_file(&remote_path, &content).await.map_err(|e| { - FabroError::engine(format!("failed to write artifact blob to sandbox: {e}")) - })?; + let content = String::from_utf8(bytes.to_vec()) + .map_err(|e| Error::engine(format!("artifact blob was not valid UTF-8 JSON: {e}")))?; + env.write_file(&remote_path, &content) + .await + .map_err(|e| Error::engine(format!("failed to write artifact blob to sandbox: {e}")))?; } Ok(format!("{ARTIFACT_POINTER_PREFIX}{remote_path}")) @@ -309,19 +308,18 @@ async fn materialize_blob_ref( async fn resolve_explicit_file_ref(value: &str, env: &dyn Sandbox) -> Result { let local_path = value .strip_prefix(ARTIFACT_POINTER_PREFIX) - .ok_or_else(|| FabroError::engine(format!("invalid artifact pointer: {value}")))?; + .ok_or_else(|| Error::engine(format!("invalid artifact pointer: {value}")))?; if env .file_exists(local_path) .await - .map_err(|e| FabroError::engine(format!("failed to check artifact existence: {e}")))? + .map_err(|e| Error::engine(format!("failed to check artifact existence: {e}")))? { return Ok(value.to_string()); } - let content = std::fs::read_to_string(local_path).map_err(|e| { - FabroError::engine(format!("failed to read local artifact {local_path}: {e}")) - })?; + let content = std::fs::read_to_string(local_path) + .map_err(|e| Error::engine(format!("failed to read local artifact {local_path}: {e}")))?; let filename = Path::new(local_path) .file_name() .and_then(|file| file.to_str()) @@ -331,11 +329,11 @@ async fn resolve_explicit_file_ref(value: &str, env: &dyn Sandbox) -> Result Result Result { env.file_exists(&run_dir.to_string_lossy()) .await - .map_err(|e| FabroError::engine(format!("failed to inspect sandbox locality: {e}"))) + .map_err(|e| Error::engine(format!("failed to inspect sandbox locality: {e}"))) } fn local_materialized_blob_path(run_dir: &Path, blob_id: &RunBlobId) -> PathBuf { @@ -475,11 +473,11 @@ mod tests { fn normalize_checkpoint_for_resume_converts_legacy_blob_file_refs_and_drops_preamble() { let blob_id = fabro_types::RunBlobId::new(b"legacy"); let mut checkpoint = crate::records::Checkpoint { - timestamp: chrono::Utc::now(), - current_node: "work".to_string(), - completed_nodes: vec!["work".to_string()], - node_retries: HashMap::new(), - context_values: HashMap::from([ + timestamp: chrono::Utc::now(), + current_node: "work".to_string(), + completed_nodes: vec!["work".to_string()], + node_retries: HashMap::new(), + context_values: HashMap::from([ ( crate::context::keys::CURRENT_PREAMBLE.to_string(), serde_json::json!("runtime only"), @@ -489,7 +487,7 @@ mod tests { serde_json::json!(format!("file:///sandbox/.fabro/artifacts/{blob_id}.json")), ), ]), - node_outcomes: HashMap::from([( + node_outcomes: HashMap::from([( "work".to_string(), crate::outcome::Outcome { context_updates: HashMap::from([( @@ -501,11 +499,11 @@ mod tests { ..crate::outcome::Outcome::success() }, )]), - next_node_id: Some("exit".to_string()), - git_commit_sha: None, - loop_failure_signatures: HashMap::new(), + next_node_id: Some("exit".to_string()), + git_commit_sha: None, + loop_failure_signatures: HashMap::new(), restart_failure_signatures: HashMap::new(), - node_visits: HashMap::new(), + node_visits: HashMap::new(), }; normalize_checkpoint_for_resume(&mut checkpoint); @@ -533,8 +531,8 @@ mod tests { use std::sync::Mutex; struct TestSyncEnv { - accessible: bool, - written: Mutex>, + accessible: bool, + written: Mutex>, working_dir: String, } diff --git a/lib/crates/fabro-workflow/src/artifact_snapshot.rs b/lib/crates/fabro-workflow/src/artifact_snapshot.rs index 1a6e69bba..3bc4f6413 100644 --- a/lib/crates/fabro-workflow/src/artifact_snapshot.rs +++ b/lib/crates/fabro-workflow/src/artifact_snapshot.rs @@ -9,29 +9,29 @@ use tracing::{debug, warn}; /// A file discovered by the find command. #[derive(Debug, Clone)] pub struct DiscoveredFile { - pub relative_path: String, - pub size: u64, + pub relative_path: String, + pub size: u64, pub mtime_epoch_secs: f64, } /// Metadata for a single captured artifact file. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CapturedArtifactInfo { - pub path: String, - pub mime: String, - pub content_md5: String, + pub path: String, + pub mime: String, + pub content_md5: String, pub content_sha256: String, - pub bytes: u64, + pub bytes: u64, } /// Summary of an artifact collection run. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ArtifactCollectionSummary { - pub files_copied: usize, - pub total_bytes: u64, - pub files_skipped: usize, + pub files_copied: usize, + pub total_bytes: u64, + pub files_skipped: usize, pub download_errors: usize, - pub hash_errors: usize, + pub hash_errors: usize, pub captured_assets: Vec, } @@ -361,9 +361,9 @@ mod tests { /// Minimal mock sandbox for artifact_snapshot tests. struct AssetMockSandbox { - files: HashMap, - exec_result: ExecResult, - working_dir: &'static str, + files: HashMap, + exec_result: ExecResult, + working_dir: &'static str, platform_str: &'static str, } @@ -372,10 +372,10 @@ mod tests { Self { files, exec_result: ExecResult { - stdout: exec_stdout.to_string(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: exec_stdout.to_string(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 10, }, working_dir: "/home/test", @@ -507,8 +507,8 @@ mod tests { #[test] fn select_files_skips_old_mtime() { let discovered = vec![DiscoveredFile { - relative_path: "test-results/old.xml".to_string(), - size: 1024, + relative_path: "test-results/old.xml".to_string(), + size: 1024, mtime_epoch_secs: 500.0, }]; let selected = select_files_to_collect(&discovered, 1000.0); @@ -518,8 +518,8 @@ mod tests { #[test] fn select_files_skips_oversized() { let discovered = vec![DiscoveredFile { - relative_path: "test-results/huge.xml".to_string(), - size: MAX_FILE_SIZE + 1, + relative_path: "test-results/huge.xml".to_string(), + size: MAX_FILE_SIZE + 1, mtime_epoch_secs: 2000.0, }]; let selected = select_files_to_collect(&discovered, 1000.0); @@ -530,18 +530,18 @@ mod tests { fn select_files_sorts_smallest_first() { let discovered = vec![ DiscoveredFile { - relative_path: "a.xml".to_string(), - size: 3000, + relative_path: "a.xml".to_string(), + size: 3000, mtime_epoch_secs: 2000.0, }, DiscoveredFile { - relative_path: "b.xml".to_string(), - size: 1000, + relative_path: "b.xml".to_string(), + size: 1000, mtime_epoch_secs: 2000.0, }, DiscoveredFile { - relative_path: "c.xml".to_string(), - size: 2000, + relative_path: "c.xml".to_string(), + size: 2000, mtime_epoch_secs: 2000.0, }, ]; @@ -556,8 +556,8 @@ mod tests { fn select_files_enforces_total_budget() { let discovered: Vec = (0..6) .map(|i| DiscoveredFile { - relative_path: format!("file{i}.xml"), - size: 9 * 1024 * 1024, // 9 MB each + relative_path: format!("file{i}.xml"), + size: 9 * 1024 * 1024, // 9 MB each mtime_epoch_secs: 2000.0, }) .collect(); @@ -617,18 +617,18 @@ mod tests { fn normalize_paths_strips_root_prefix() { let files = vec![ DiscoveredFile { - relative_path: "/workspace/test-results/r.xml".to_string(), - size: 100, + relative_path: "/workspace/test-results/r.xml".to_string(), + size: 100, mtime_epoch_secs: 1000.0, }, DiscoveredFile { - relative_path: "./test-results/s.xml".to_string(), - size: 200, + relative_path: "./test-results/s.xml".to_string(), + size: 200, mtime_epoch_secs: 1000.0, }, DiscoveredFile { - relative_path: "test-results/t.xml".to_string(), - size: 300, + relative_path: "test-results/t.xml".to_string(), + size: 300, mtime_epoch_secs: 1000.0, }, ]; @@ -722,8 +722,8 @@ mod tests { // Create 150 small, recent files — should be capped at MAX_FILE_COUNT (100) let discovered: Vec = (0..150) .map(|i| DiscoveredFile { - relative_path: format!("file{i}.txt"), - size: 100, // tiny files, well within total budget + relative_path: format!("file{i}.txt"), + size: 100, // tiny files, well within total budget mtime_epoch_secs: 2000.0, }) .collect(); diff --git a/lib/crates/fabro-workflow/src/devcontainer_bridge.rs b/lib/crates/fabro-workflow/src/devcontainer_bridge.rs index d9f5d13a8..d99c09b29 100644 --- a/lib/crates/fabro-workflow/src/devcontainer_bridge.rs +++ b/lib/crates/fabro-workflow/src/devcontainer_bridge.rs @@ -8,7 +8,7 @@ use fabro_sandbox::daytona::{DaytonaSnapshotConfig, DockerfileSource}; use futures::future::try_join_all; use sha2::{Digest, Sha256}; -use crate::error::FabroError; +use crate::error::Error; use crate::event::{Emitter, Event}; use crate::handler::sandbox_cancel_token; @@ -22,11 +22,11 @@ pub fn snapshot_name_for_dockerfile(dockerfile: &str) -> String { /// Map a `DevcontainerSpec` to a `DaytonaSnapshotConfig`. pub fn devcontainer_to_snapshot_config(dc: &DevcontainerSpec) -> DaytonaSnapshotConfig { DaytonaSnapshotConfig { - name: snapshot_name_for_dockerfile(&dc.dockerfile), + name: snapshot_name_for_dockerfile(&dc.dockerfile), dockerfile: Some(DockerfileSource::Inline(dc.dockerfile.clone())), - cpu: None, - memory: None, - disk: None, + cpu: None, + memory: None, + disk: None, } } @@ -40,13 +40,13 @@ pub async fn run_devcontainer_lifecycle( commands: &[fabro_devcontainer::Command], timeout_ms: u64, cancel_requested: Option>, -) -> Result<(), FabroError> { +) -> Result<(), Error> { if commands.is_empty() { return Ok(()); } emitter.emit(&Event::DevcontainerLifecycleStarted { - phase: phase.to_string(), + phase: phase.to_string(), command_count: commands.len(), }); let phase_start = Instant::now(); @@ -111,13 +111,13 @@ pub async fn run_devcontainer_lifecycle( ) .await .map_err(|e| { - FabroError::engine(format!( + Error::engine(format!( "Devcontainer {phase} parallel command '{name}' failed: {e}" )) })?; if let Some(token) = &cancel_token { if token.is_cancelled() { - return Err(FabroError::Cancelled); + return Err(Error::Cancelled); } token.cancel(); } @@ -132,7 +132,7 @@ pub async fn run_devcontainer_lifecycle( stderr: result.stderr.clone(), }, ); - return Err(FabroError::engine(format!( + return Err(Error::engine(format!( "Devcontainer {phase} parallel command '{name}' failed (exit code {}): {}", result.exit_code, result.stderr, @@ -158,7 +158,7 @@ pub async fn run_devcontainer_lifecycle( let phase_duration = crate::millis_u64(phase_start.elapsed()); emitter.emit(&Event::DevcontainerLifecycleCompleted { - phase: phase.to_string(), + phase: phase.to_string(), duration_ms: phase_duration, }); Ok(()) @@ -172,7 +172,7 @@ async fn run_single_lifecycle_command( index: usize, timeout_ms: u64, cancel_requested: Option>, -) -> Result<(), FabroError> { +) -> Result<(), Error> { emitter.emit(&Event::DevcontainerLifecycleCommandStarted { phase: phase.to_string(), command: command.to_string(), @@ -183,10 +183,10 @@ async fn run_single_lifecycle_command( let result = sandbox .exec_command(command, timeout_ms, None, None, cancel_token.clone()) .await - .map_err(|e| FabroError::engine(format!("Devcontainer {phase} command failed: {e}")))?; + .map_err(|e| Error::engine(format!("Devcontainer {phase} command failed: {e}")))?; if let Some(token) = &cancel_token { if token.is_cancelled() { - return Err(FabroError::Cancelled); + return Err(Error::Cancelled); } token.cancel(); } @@ -199,7 +199,7 @@ async fn run_single_lifecycle_command( exit_code: result.exit_code, stderr: result.stderr.clone(), }); - return Err(FabroError::engine(format!( + return Err(Error::engine(format!( "Devcontainer {phase} command failed (exit code {}): {command}\n{}", result.exit_code, result.stderr, ))); @@ -228,18 +228,18 @@ mod tests { /// Simple test sandbox that records commands and returns a fixed exit code. struct TestSandbox { - commands: Mutex>, - cancel_tokens: Mutex>, - exit_code: i32, + commands: Mutex>, + cancel_tokens: Mutex>, + exit_code: i32, wait_for_cancel: bool, } impl TestSandbox { fn new() -> Self { Self { - commands: Mutex::new(Vec::new()), - cancel_tokens: Mutex::new(Vec::new()), - exit_code: 0, + commands: Mutex::new(Vec::new()), + cancel_tokens: Mutex::new(Vec::new()), + exit_code: 0, wait_for_cancel: false, } } @@ -255,9 +255,9 @@ mod tests { fn waiting_for_cancel() -> Self { Self { - commands: Mutex::new(Vec::new()), - cancel_tokens: Mutex::new(Vec::new()), - exit_code: 0, + commands: Mutex::new(Vec::new()), + cancel_tokens: Mutex::new(Vec::new()), + exit_code: 0, wait_for_cancel: true, } } @@ -314,22 +314,22 @@ mod tests { let token = cancel_token.ok_or_else(|| "missing cancel token".to_string())?; token.cancelled().await; return Ok(ExecResult { - stdout: String::new(), - stderr: "cancelled".to_string(), - exit_code: -1, - timed_out: true, + stdout: String::new(), + stderr: "cancelled".to_string(), + exit_code: -1, + timed_out: true, duration_ms: 10, }); } Ok(ExecResult { - stdout: String::new(), - stderr: if self.exit_code != 0 { + stdout: String::new(), + stderr: if self.exit_code != 0 { "command failed".to_string() } else { String::new() }, - exit_code: self.exit_code, - timed_out: false, + exit_code: self.exit_code, + timed_out: false, duration_ms: 10, }) } @@ -559,7 +559,7 @@ mod tests { ) .await; - assert!(matches!(result, Err(FabroError::Cancelled))); + assert!(matches!(result, Err(Error::Cancelled))); assert_eq!(sandbox.captured_cancel_tokens(), vec![true]); } @@ -583,7 +583,7 @@ mod tests { ) .await; - assert!(matches!(result, Err(FabroError::Cancelled))); + assert!(matches!(result, Err(Error::Cancelled))); let captured = sandbox.captured_cancel_tokens(); assert!(!captured.is_empty()); assert!(captured.iter().all(|saw_token| *saw_token)); @@ -591,21 +591,21 @@ mod tests { fn test_devcontainer_config(dockerfile: &str) -> DevcontainerSpec { DevcontainerSpec { - dockerfile: dockerfile.to_string(), - build_context: std::path::PathBuf::from("."), - build_args: HashMap::new(), - build_target: None, - initialize_commands: vec![], - on_create_commands: vec![], + dockerfile: dockerfile.to_string(), + build_context: std::path::PathBuf::from("."), + build_args: HashMap::new(), + build_target: None, + initialize_commands: vec![], + on_create_commands: vec![], post_create_commands: vec![], - post_start_commands: vec![], - environment: HashMap::new(), - container_env: HashMap::new(), - remote_user: None, - workspace_folder: "/workspaces/test".to_string(), - forwarded_ports: vec![], - compose_files: vec![], - compose_service: None, + post_start_commands: vec![], + environment: HashMap::new(), + container_env: HashMap::new(), + remote_user: None, + workspace_folder: "/workspaces/test".to_string(), + forwarded_ports: vec![], + compose_files: vec![], + compose_service: None, } } } diff --git a/lib/crates/fabro-workflow/src/error.rs b/lib/crates/fabro-workflow/src/error.rs index d85afbbc7..3c870c85f 100644 --- a/lib/crates/fabro-workflow/src/error.rs +++ b/lib/crates/fabro-workflow/src/error.rs @@ -202,13 +202,13 @@ pub enum Error { #[error("Engine error: {message}")] Engine { - message: String, + message: String, failure_class: FailureCategory, }, #[error("Handler error: {message}")] Handler { - message: String, + message: String, failure_class: FailureCategory, }, @@ -308,8 +308,8 @@ impl Error { /// Build a fail `Outcome` with structured `FailureDetail`. pub fn to_fail_outcome(&self) -> Outcome { let failure = FailureDetail { - message: self.to_string(), - category: self.failure_category(), + message: self.to_string(), + category: self.failure_category(), signature: self.failure_signature_hint(), }; Outcome { @@ -367,7 +367,6 @@ impl From for Error { } pub type Result = std::result::Result; -pub type FabroError = Error; #[cfg(test)] mod tests { @@ -379,26 +378,26 @@ mod tests { #[test] fn parse_error_display() { - let err = FabroError::Parse("unexpected token".to_string()); + let err = Error::Parse("unexpected token".to_string()); assert_eq!(err.to_string(), "Parse error: unexpected token"); } #[test] fn validation_error_display() { - let err = FabroError::Validation("missing start node".to_string()); + let err = Error::Validation("missing start node".to_string()); assert_eq!(err.to_string(), "Validation error: missing start node"); } #[test] fn validation_failed_display() { - let err = FabroError::ValidationFailed { + let err = Error::ValidationFailed { diagnostics: vec![Diagnostic { - rule: "test".to_string(), + rule: "test".to_string(), severity: fabro_validate::Severity::Error, - message: "missing start node".to_string(), - node_id: None, - edge: None, - fix: None, + message: "missing start node".to_string(), + node_id: None, + edge: None, + fix: None, }], }; assert_eq!(err.to_string(), "Validation failed"); @@ -406,33 +405,33 @@ mod tests { #[test] fn engine_error_display() { - let err = FabroError::engine("no outgoing edge"); + let err = Error::engine("no outgoing edge"); assert_eq!(err.to_string(), "Engine error: no outgoing edge"); } #[test] fn handler_error_display() { - let err = FabroError::handler("LLM call failed"); + let err = Error::handler("LLM call failed"); assert_eq!(err.to_string(), "Handler error: LLM call failed"); } #[test] fn checkpoint_error_display() { - let err = FabroError::Checkpoint("file not found".to_string()); + let err = Error::Checkpoint("file not found".to_string()); assert_eq!(err.to_string(), "Checkpoint error: file not found"); } #[test] fn io_error_display() { - let err = FabroError::Io("permission denied".to_string()); + let err = Error::Io("permission denied".to_string()); assert_eq!(err.to_string(), "I/O error: permission denied"); } #[test] fn io_error_from_std() { let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found"); - let err = FabroError::from(io_err); - assert!(matches!(err, FabroError::Io(_))); + let err = Error::from(io_err); + assert!(matches!(err, Error::Io(_))); assert!(err.to_string().contains("not found")); } @@ -441,7 +440,7 @@ mod tests { let ok: Result = Ok(42); assert!(ok.is_ok()); - let err: Result = Err(FabroError::Parse("bad".to_string())); + let err: Result = Err(Error::Parse("bad".to_string())); assert!(err.is_err()); } @@ -449,13 +448,13 @@ mod tests { fn metadata_checkpoint_deserialize_error_preserves_source_detail() { let source = serde_json::from_str::("not json").unwrap_err(); let source_message = source.to_string(); - let fabro_error = FabroError::from(MetadataError::Deserialize { + let fabro_error = Error::from(MetadataError::Deserialize { entity: "checkpoint", branch: "fabro/meta/run-1".to_string(), source, }); - assert!(matches!(fabro_error, FabroError::Checkpoint(_))); + assert!(matches!(fabro_error, Error::Checkpoint(_))); let message = fabro_error.to_string(); assert!(message.contains("deserialize checkpoint on branch fabro/meta/run-1")); assert!(message.contains(&source_message)); @@ -465,13 +464,13 @@ mod tests { fn metadata_non_checkpoint_deserialize_error_maps_to_engine_with_source_detail() { let source = serde_json::from_str::("not json").unwrap_err(); let source_message = source.to_string(); - let fabro_error = FabroError::from(MetadataError::Deserialize { + let fabro_error = Error::from(MetadataError::Deserialize { entity: "run record", branch: "fabro/meta/run-1".to_string(), source, }); - assert!(matches!(fabro_error, FabroError::Engine { .. })); + assert!(matches!(fabro_error, Error::Engine { .. })); let message = fabro_error.to_string(); assert!(message.contains("deserialize run record on branch fabro/meta/run-1")); assert!(message.contains(&source_message)); @@ -479,34 +478,34 @@ mod tests { #[test] fn cancelled_error_display() { - let err = FabroError::Cancelled; + let err = Error::Cancelled; assert_eq!(err.to_string(), "Pipeline cancelled"); } #[test] fn cancelled_is_not_retryable() { - assert!(!FabroError::Cancelled.is_retryable()); + assert!(!Error::Cancelled.is_retryable()); } #[test] fn is_retryable_terminal_errors() { - assert!(!FabroError::Parse("bad".to_string()).is_retryable()); - assert!(!FabroError::Validation("bad".to_string()).is_retryable()); + assert!(!Error::Parse("bad".to_string()).is_retryable()); + assert!(!Error::Validation("bad".to_string()).is_retryable()); assert!( - !FabroError::ValidationFailed { + !Error::ValidationFailed { diagnostics: vec![], } .is_retryable() ); - assert!(!FabroError::Stylesheet("bad".to_string()).is_retryable()); - assert!(!FabroError::Checkpoint("bad".to_string()).is_retryable()); + assert!(!Error::Stylesheet("bad".to_string()).is_retryable()); + assert!(!Error::Checkpoint("bad".to_string()).is_retryable()); } #[test] fn is_retryable_transient_errors() { - assert!(FabroError::handler("timeout").is_retryable()); - assert!(FabroError::engine("transient").is_retryable()); - assert!(FabroError::Io("connection reset".to_string()).is_retryable()); + assert!(Error::handler("timeout").is_retryable()); + assert!(Error::engine("transient").is_retryable()); + assert!(Error::Io("connection reset".to_string()).is_retryable()); } // --- FailureCategory Display/FromStr/serde tests --- @@ -677,9 +676,9 @@ mod tests { fn llm_error_display() { let sdk_err = SdkError::Network { message: "connection refused".into(), - source: None, + source: None, }; - let err = FabroError::Llm(sdk_err); + let err = Error::Llm(sdk_err); assert_eq!( err.to_string(), "LLM error: Network error: connection refused" @@ -688,15 +687,15 @@ mod tests { #[test] fn llm_error_retryable_delegates_to_sdk() { - let retryable = FabroError::Llm(SdkError::Network { + let retryable = Error::Llm(SdkError::Network { message: "timeout".into(), - source: None, + source: None, }); assert!(retryable.is_retryable()); - let non_retryable = FabroError::Llm(SdkError::Configuration { + let non_retryable = Error::Llm(SdkError::Configuration { message: "bad config".into(), - source: None, + source: None, }); assert!(!non_retryable.is_retryable()); } @@ -705,10 +704,10 @@ mod tests { fn llm_error_from_sdk_error() { let sdk_err = SdkError::Stream { message: "broken pipe".into(), - source: None, + source: None, }; - let err = FabroError::from(sdk_err); - assert!(matches!(err, FabroError::Llm(_))); + let err = Error::from(sdk_err); + assert!(matches!(err, Error::Llm(_))); } // --- failure_class() method tests --- @@ -716,7 +715,7 @@ mod tests { #[test] fn failure_class_cancelled() { assert_eq!( - FabroError::Cancelled.failure_category(), + Error::Cancelled.failure_category(), FailureCategory::Canceled ); } @@ -724,7 +723,7 @@ mod tests { #[test] fn failure_class_io() { assert_eq!( - FabroError::Io("disk full".into()).failure_category(), + Error::Io("disk full".into()).failure_category(), FailureCategory::TransientInfra ); } @@ -732,7 +731,7 @@ mod tests { #[test] fn failure_class_parse() { assert_eq!( - FabroError::Parse("bad syntax".into()).failure_category(), + Error::Parse("bad syntax".into()).failure_category(), FailureCategory::Deterministic ); } @@ -740,7 +739,7 @@ mod tests { #[test] fn failure_class_handler_with_timeout() { assert_eq!( - FabroError::handler("request timed out").failure_category(), + Error::handler("request timed out").failure_category(), FailureCategory::TransientInfra ); } @@ -748,15 +747,15 @@ mod tests { #[test] fn failure_class_handler_deterministic() { assert_eq!( - FabroError::handler("invalid configuration").failure_category(), + Error::handler("invalid configuration").failure_category(), FailureCategory::Deterministic ); } #[test] fn failure_class_llm_rate_limit() { - let err = FabroError::Llm(SdkError::Provider { - kind: ProviderErrorKind::RateLimit, + let err = Error::Llm(SdkError::Provider { + kind: ProviderErrorKind::RateLimit, detail: Box::new(ProviderErrorDetail::new("too fast", "openai")), }); assert_eq!(err.failure_category(), FailureCategory::TransientInfra); @@ -764,8 +763,8 @@ mod tests { #[test] fn failure_class_llm_context_length() { - let err = FabroError::Llm(SdkError::Provider { - kind: ProviderErrorKind::ContextLength, + let err = Error::Llm(SdkError::Provider { + kind: ProviderErrorKind::ContextLength, detail: Box::new(ProviderErrorDetail::new("too long", "openai")), }); assert_eq!(err.failure_category(), FailureCategory::BudgetExhausted); @@ -773,8 +772,8 @@ mod tests { #[test] fn failure_class_llm_auth() { - let err = FabroError::Llm(SdkError::Provider { - kind: ProviderErrorKind::Authentication, + let err = Error::Llm(SdkError::Provider { + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), }); assert_eq!(err.failure_category(), FailureCategory::Deterministic); @@ -782,7 +781,7 @@ mod tests { #[test] fn failure_class_llm_abort() { - let err = FabroError::Llm(SdkError::Interrupt { + let err = Error::Llm(SdkError::Interrupt { message: "user cancelled".into(), }); assert_eq!(err.failure_category(), FailureCategory::Canceled); @@ -790,9 +789,9 @@ mod tests { #[test] fn failure_class_llm_timeout() { - let err = FabroError::Llm(SdkError::RequestTimeout { + let err = Error::Llm(SdkError::RequestTimeout { message: "timed out".into(), - source: None, + source: None, }); assert_eq!(err.failure_category(), FailureCategory::TransientInfra); } @@ -802,7 +801,7 @@ mod tests { #[test] fn classify_sdk_rate_limit() { let err = SdkError::Provider { - kind: ProviderErrorKind::RateLimit, + kind: ProviderErrorKind::RateLimit, detail: Box::new(ProviderErrorDetail::new("too fast", "openai")), }; assert_eq!(classify_sdk_error(&err), FailureCategory::TransientInfra); @@ -811,7 +810,7 @@ mod tests { #[test] fn classify_sdk_server() { let err = SdkError::Provider { - kind: ProviderErrorKind::Server, + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail::new("500", "openai")), }; assert_eq!(classify_sdk_error(&err), FailureCategory::TransientInfra); @@ -820,7 +819,7 @@ mod tests { #[test] fn classify_sdk_context_length() { let err = SdkError::Provider { - kind: ProviderErrorKind::ContextLength, + kind: ProviderErrorKind::ContextLength, detail: Box::new(ProviderErrorDetail::new("too long", "openai")), }; assert_eq!(classify_sdk_error(&err), FailureCategory::BudgetExhausted); @@ -829,7 +828,7 @@ mod tests { #[test] fn classify_sdk_quota_exceeded() { let err = SdkError::Provider { - kind: ProviderErrorKind::QuotaExceeded, + kind: ProviderErrorKind::QuotaExceeded, detail: Box::new(ProviderErrorDetail::new("out of quota", "openai")), }; assert_eq!(classify_sdk_error(&err), FailureCategory::BudgetExhausted); @@ -838,7 +837,7 @@ mod tests { #[test] fn classify_sdk_auth() { let err = SdkError::Provider { - kind: ProviderErrorKind::Authentication, + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), }; assert_eq!(classify_sdk_error(&err), FailureCategory::Deterministic); @@ -848,7 +847,7 @@ mod tests { fn classify_sdk_request_timeout() { let err = SdkError::RequestTimeout { message: "timed out".into(), - source: None, + source: None, }; assert_eq!(classify_sdk_error(&err), FailureCategory::TransientInfra); } @@ -1492,8 +1491,8 @@ mod tests { #[test] fn failure_signature_hint_llm_returns_some() { - let err = FabroError::Llm(SdkError::Provider { - kind: ProviderErrorKind::Authentication, + let err = Error::Llm(SdkError::Provider { + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), }); assert_eq!( @@ -1504,13 +1503,13 @@ mod tests { #[test] fn failure_signature_hint_handler_returns_none() { - let err = FabroError::handler("something failed"); + let err = Error::handler("something failed"); assert_eq!(err.failure_signature_hint(), None); } #[test] fn failure_signature_hint_engine_returns_none() { - let err = FabroError::engine("engine error"); + let err = Error::engine("engine error"); assert_eq!(err.failure_signature_hint(), None); } @@ -1518,8 +1517,8 @@ mod tests { #[test] fn to_fail_outcome_llm_has_class_and_signature() { - let err = FabroError::Llm(SdkError::Provider { - kind: ProviderErrorKind::Authentication, + let err = Error::Llm(SdkError::Provider { + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), }); let outcome = err.to_fail_outcome(); @@ -1534,7 +1533,7 @@ mod tests { #[test] fn to_fail_outcome_handler_has_class_but_no_signature() { - let err = FabroError::handler("connection refused"); + let err = Error::handler("connection refused"); let outcome = err.to_fail_outcome(); assert_eq!(outcome.status, crate::outcome::StageStatus::Fail); let failure = outcome.failure.as_ref().unwrap(); @@ -1544,9 +1543,9 @@ mod tests { #[test] fn to_fail_outcome_includes_error_message_as_reason() { - let err = FabroError::Llm(SdkError::Network { + let err = Error::Llm(SdkError::Network { message: "connection refused".into(), - source: None, + source: None, }); let outcome = err.to_fail_outcome(); assert!( @@ -1559,9 +1558,9 @@ mod tests { #[test] fn to_fail_outcome_no_context_updates() { - let err = FabroError::Llm(SdkError::Network { + let err = Error::Llm(SdkError::Network { message: "refused".into(), - source: None, + source: None, }); let outcome = err.to_fail_outcome(); assert!(outcome.context_updates.is_empty()); @@ -1571,15 +1570,15 @@ mod tests { #[test] fn handler_eager_classification() { - let err = FabroError::handler("connection refused"); + let err = Error::handler("connection refused"); assert_eq!(err.failure_category(), FailureCategory::TransientInfra); } #[test] fn handler_eager_classification_roundtrip() { - let err = FabroError::handler("connection refused"); + let err = Error::handler("connection refused"); let json = serde_json::to_string(&err).unwrap(); - let deserialized: FabroError = serde_json::from_str(&json).unwrap(); + let deserialized: Error = serde_json::from_str(&json).unwrap(); assert_eq!( deserialized.failure_category(), FailureCategory::TransientInfra @@ -1588,45 +1587,45 @@ mod tests { #[test] fn handler_smart_constructor_preserves_message() { - let err = FabroError::handler("some error"); + let err = Error::handler("some error"); assert!(err.to_string().contains("some error")); } #[test] fn engine_eager_classification() { - let err = FabroError::engine("rate limit exceeded"); + let err = Error::engine("rate limit exceeded"); assert_eq!(err.failure_category(), FailureCategory::TransientInfra); } #[test] fn arc_error_serde_roundtrip_all_variants() { - let errors: Vec = vec![ - FabroError::Parse("bad".into()), - FabroError::Validation("bad".into()), - FabroError::ValidationFailed { + let errors: Vec = vec![ + Error::Parse("bad".into()), + Error::Validation("bad".into()), + Error::ValidationFailed { diagnostics: vec![Diagnostic { - rule: "test".into(), + rule: "test".into(), severity: fabro_validate::Severity::Error, - message: "bad".into(), - node_id: None, - edge: None, - fix: None, + message: "bad".into(), + node_id: None, + edge: None, + fix: None, }], }, - FabroError::engine("engine err"), - FabroError::handler("handler err"), - FabroError::Llm(SdkError::Network { + Error::engine("engine err"), + Error::handler("handler err"), + Error::Llm(SdkError::Network { message: "refused".into(), - source: None, + source: None, }), - FabroError::Checkpoint("cp err".into()), - FabroError::Stylesheet("style err".into()), - FabroError::Io("io err".into()), - FabroError::Cancelled, + Error::Checkpoint("cp err".into()), + Error::Stylesheet("style err".into()), + Error::Io("io err".into()), + Error::Cancelled, ]; for err in &errors { let json = serde_json::to_string(err).unwrap(); - let deserialized: FabroError = serde_json::from_str(&json).unwrap(); + let deserialized: Error = serde_json::from_str(&json).unwrap(); assert_eq!(err.to_string(), deserialized.to_string()); } } @@ -1634,7 +1633,7 @@ mod tests { #[test] fn handler_display_unchanged() { assert_eq!( - FabroError::handler("LLM call failed").to_string(), + Error::handler("LLM call failed").to_string(), "Handler error: LLM call failed" ); } @@ -1642,7 +1641,7 @@ mod tests { #[test] fn engine_display_unchanged() { assert_eq!( - FabroError::engine("no outgoing edge").to_string(), + Error::engine("no outgoing edge").to_string(), "Engine error: no outgoing edge" ); } @@ -1660,7 +1659,7 @@ mod tests { ]; for msg in messages { assert_eq!( - FabroError::handler(msg).failure_category(), + Error::handler(msg).failure_category(), classify_failure_reason(msg), "mismatch for message: {msg}" ); @@ -1669,7 +1668,7 @@ mod tests { #[test] fn to_fail_outcome_preserves_class() { - let err = FabroError::handler("timeout"); + let err = Error::handler("timeout"); let outcome = err.to_fail_outcome(); assert_eq!( outcome.failure_category(), @@ -1683,15 +1682,15 @@ mod tests { fn e2e_llm_error_to_outcome_to_event_preserves_classification() { use crate::event::Event; - // 1. Create SdkError → FabroError + // 1. Create SdkError → Error let sdk_err = SdkError::Provider { - kind: ProviderErrorKind::RateLimit, + kind: ProviderErrorKind::RateLimit, detail: Box::new(ProviderErrorDetail::new("too fast", "openai")), }; - let arc_err = FabroError::Llm(sdk_err); + let arc_err = Error::Llm(sdk_err); assert_eq!(arc_err.failure_category(), FailureCategory::TransientInfra); - // 2. FabroError → Outcome + // 2. Error → Outcome let outcome = arc_err.to_fail_outcome(); assert_eq!( outcome.failure_category(), @@ -1701,10 +1700,10 @@ mod tests { // 3. Outcome → StageFailed event let failure = outcome.failure.clone().unwrap(); let event = Event::StageFailed { - node_id: "code".into(), - name: "code".into(), - index: 0, - failure: failure.clone(), + node_id: "code".into(), + name: "code".into(), + index: 0, + failure: failure.clone(), will_retry: false, }; @@ -1720,7 +1719,7 @@ mod tests { #[test] fn e2e_handler_error_classified_at_edge() { // handler smart constructor classifies eagerly - let err = FabroError::handler("connection refused"); + let err = Error::handler("connection refused"); assert_eq!(err.failure_category(), FailureCategory::TransientInfra); // to_fail_outcome preserves @@ -1737,13 +1736,13 @@ mod tests { #[test] fn e2e_handler_retryable_checks() { - assert!(FabroError::handler("timeout").is_retryable()); - assert!(FabroError::handler("auth error").is_retryable()); + assert!(Error::handler("timeout").is_retryable()); + assert!(Error::handler("auth error").is_retryable()); } #[test] fn e2e_serde_stability_arc_error() { - let err = FabroError::handler("connection refused"); + let err = Error::handler("connection refused"); let json = serde_json::to_string(&err).unwrap(); let v: serde_json::Value = serde_json::from_str(&json).unwrap(); @@ -1758,7 +1757,7 @@ mod tests { assert_eq!(v["data"]["failure_class"], "transient_infra"); // Round-trip - let deserialized: FabroError = serde_json::from_str(&json).unwrap(); + let deserialized: Error = serde_json::from_str(&json).unwrap(); assert_eq!( deserialized.failure_category(), FailureCategory::TransientInfra @@ -1770,7 +1769,7 @@ mod tests { use fabro_agent::Error as AgentError; let err = AgentError::Llm(SdkError::Provider { - kind: ProviderErrorKind::RateLimit, + kind: ProviderErrorKind::RateLimit, detail: Box::new(ProviderErrorDetail::new("too fast", "openai")), }); let json = serde_json::to_string(&err).unwrap(); diff --git a/lib/crates/fabro-workflow/src/event.rs b/lib/crates/fabro-workflow/src/event.rs index b47bbffdf..eea7bc11c 100644 --- a/lib/crates/fabro-workflow/src/event.rs +++ b/lib/crates/fabro-workflow/src/event.rs @@ -23,7 +23,7 @@ use tokio::sync::{Mutex as AsyncMutex, mpsc, oneshot}; use uuid::Uuid; use crate::context::{Context as WfContext, WorkflowContext}; -use crate::error::FabroError; +use crate::error::Error; use crate::outcome::{BilledModelUsage, FailureDetail, Outcome}; use crate::run_dir::visit_from_context; use crate::runtime_store::RunStoreHandle; @@ -33,48 +33,48 @@ use crate::runtime_store::RunStoreHandle; #[allow(clippy::large_enum_variant)] pub enum Event { RunCreated { - run_id: RunId, - settings: serde_json::Value, - graph: serde_json::Value, + run_id: RunId, + settings: serde_json::Value, + graph: serde_json::Value, #[serde(default, skip_serializing_if = "Option::is_none")] - workflow_source: Option, + workflow_source: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - workflow_config: Option, - labels: BTreeMap, - run_dir: String, + workflow_config: Option, + labels: BTreeMap, + run_dir: String, working_directory: String, #[serde(default, skip_serializing_if = "Option::is_none")] - host_repo_path: Option, + host_repo_path: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - repo_origin_url: Option, + repo_origin_url: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - base_branch: Option, + base_branch: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - workflow_slug: Option, + workflow_slug: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - db_prefix: Option, + db_prefix: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - provenance: Option, + provenance: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - manifest_blob: Option, + manifest_blob: Option, }, WorkflowRunStarted { - name: String, - run_id: RunId, + name: String, + run_id: RunId, #[serde(default, skip_serializing_if = "Option::is_none")] - base_branch: Option, + base_branch: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - base_sha: Option, + base_sha: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - run_branch: Option, + run_branch: Option, #[serde(default, skip_serializing_if = "Option::is_none")] worktree_dir: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - goal: Option, + goal: Option, }, RunSubmitted { #[serde(default, skip_serializing_if = "Option::is_none")] - reason: Option, + reason: Option, #[serde(default, skip_serializing_if = "Option::is_none")] definition_blob: Option, }, @@ -106,48 +106,48 @@ pub enum Event { RunUnpaused, RunRewound { target_checkpoint_ordinal: usize, - target_node_id: String, - target_visit: usize, + target_node_id: String, + target_visit: usize, #[serde(default, skip_serializing_if = "Option::is_none")] - previous_status: Option, + previous_status: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - run_commit_sha: Option, + run_commit_sha: Option, }, WorkflowRunCompleted { - duration_ms: u64, - artifact_count: usize, + duration_ms: u64, + artifact_count: usize, #[serde(default)] - status: String, + status: String, #[serde(default, skip_serializing_if = "Option::is_none")] - reason: Option, + reason: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - total_usd_micros: Option, + total_usd_micros: Option, #[serde(default, skip_serializing_if = "Option::is_none")] final_git_commit_sha: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - final_patch: Option, + final_patch: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - billing: Option, + billing: Option, }, WorkflowRunFailed { - error: FabroError, - duration_ms: u64, + error: Error, + duration_ms: u64, #[serde(default, skip_serializing_if = "Option::is_none")] - reason: Option, + reason: Option, #[serde(default, skip_serializing_if = "Option::is_none")] git_commit_sha: Option, }, RunNotice { - level: RunNoticeLevel, - code: String, + level: RunNoticeLevel, + code: String, message: String, }, StageStarted { - node_id: String, - name: String, - index: usize, + node_id: String, + name: String, + index: usize, handler_type: String, - attempt: usize, + attempt: usize, max_attempts: usize, }, StageCompleted { @@ -181,60 +181,60 @@ pub enum Event { max_attempts: usize, }, StageFailed { - node_id: String, - name: String, - index: usize, - failure: FailureDetail, + node_id: String, + name: String, + index: usize, + failure: FailureDetail, will_retry: bool, }, StageRetrying { - node_id: String, - name: String, - index: usize, - attempt: usize, + node_id: String, + name: String, + index: usize, + attempt: usize, max_attempts: usize, - delay_ms: u64, + delay_ms: u64, }, ParallelStarted { - node_id: String, - visit: u32, + node_id: String, + visit: u32, branch_count: usize, - join_policy: String, + join_policy: String, }, ParallelBranchStarted { - parallel_group_id: StageId, + parallel_group_id: StageId, parallel_branch_id: ParallelBranchId, - branch: String, - index: usize, + branch: String, + index: usize, }, ParallelBranchCompleted { - parallel_group_id: StageId, + parallel_group_id: StageId, parallel_branch_id: ParallelBranchId, - branch: String, - index: usize, - duration_ms: u64, - status: String, + branch: String, + index: usize, + duration_ms: u64, + status: String, #[serde(default, skip_serializing_if = "Option::is_none")] - head_sha: Option, + head_sha: Option, }, ParallelCompleted { - node_id: String, - visit: u32, - duration_ms: u64, + node_id: String, + visit: u32, + duration_ms: u64, success_count: usize, failure_count: usize, #[serde(default, skip_serializing_if = "Vec::is_empty")] - results: Vec, + results: Vec, }, InterviewStarted { - question_id: String, - question: String, - stage: String, - question_type: String, + question_id: String, + question: String, + stage: String, + question_type: String, #[serde(default, skip_serializing_if = "Vec::is_empty")] - options: Vec, + options: Vec, #[serde(default)] - allow_freeform: bool, + allow_freeform: bool, #[serde(default, skip_serializing_if = "Option::is_none")] timeout_seconds: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -242,21 +242,21 @@ pub enum Event { }, InterviewCompleted { question_id: String, - question: String, - answer: String, + question: String, + answer: String, duration_ms: u64, }, InterviewTimeout { question_id: String, - question: String, - stage: String, + question: String, + stage: String, duration_ms: u64, }, InterviewInterrupted { question_id: String, - question: String, - stage: String, - reason: String, + question: String, + stage: String, + reason: String, duration_ms: u64, }, CheckpointCompleted { @@ -286,96 +286,96 @@ pub enum Event { }, CheckpointFailed { node_id: String, - error: String, + error: String, }, GitCommit { #[serde(default, skip_serializing_if = "Option::is_none")] node_id: Option, - sha: String, + sha: String, }, GitPush { - branch: String, + branch: String, success: bool, }, GitBranch { branch: String, - sha: String, + sha: String, }, GitWorktreeAdd { - path: String, + path: String, branch: String, }, GitWorktreeRemove { path: String, }, GitFetch { - branch: String, + branch: String, success: bool, }, GitReset { sha: String, }, EdgeSelected { - from_node: String, - to_node: String, - label: Option, - condition: Option, + from_node: String, + to_node: String, + label: Option, + condition: Option, /// Which selection step chose this edge (e.g. "condition", /// "preferred_label", "jump"). - reason: String, + reason: String, /// The stage's preferred label hint, if any. #[serde(default, skip_serializing_if = "Option::is_none")] - preferred_label: Option, + preferred_label: Option, /// The stage's suggested next node IDs, if any. #[serde(default, skip_serializing_if = "Vec::is_empty")] suggested_next_ids: Vec, /// The stage outcome status that influenced routing. - stage_status: String, + stage_status: String, /// Whether this was a direct jump (bypassing normal edge selection). - is_jump: bool, + is_jump: bool, }, LoopRestart { from_node: String, - to_node: String, + to_node: String, }, Prompt { - stage: String, - visit: u32, - text: String, + stage: String, + visit: u32, + text: String, #[serde(default, skip_serializing_if = "Option::is_none")] - mode: Option, + mode: Option, #[serde(default, skip_serializing_if = "Option::is_none")] provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - model: Option, + model: Option, }, PromptCompleted { - node_id: String, + node_id: String, response: String, - model: String, + model: String, provider: String, #[serde(default, skip_serializing_if = "Option::is_none")] - billing: Option, + billing: Option, }, /// Forwarded from an agent session, tagged with the workflow stage. Agent { - stage: String, - visit: u32, - event: AgentEvent, + stage: String, + visit: u32, + event: AgentEvent, #[serde(default, skip_serializing_if = "Option::is_none")] - session_id: Option, + session_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] parent_session_id: Option, }, SubgraphStarted { - node_id: String, + node_id: String, start_node: String, }, SubgraphCompleted { - node_id: String, + node_id: String, steps_executed: usize, - status: String, - duration_ms: u64, + status: String, + duration_ms: u64, }, /// Forwarded from a sandbox lifecycle operation. Sandbox { @@ -383,174 +383,174 @@ pub enum Event { }, /// Emitted after the sandbox has been initialized (by engine lifecycle). SandboxInitialized { - working_directory: String, - provider: String, + working_directory: String, + provider: String, #[serde(default, skip_serializing_if = "Option::is_none")] - identifier: Option, + identifier: Option, #[serde(default, skip_serializing_if = "Option::is_none")] host_working_directory: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - container_mount_point: Option, + container_mount_point: Option, }, SetupStarted { command_count: usize, }, SetupCommandStarted { command: String, - index: usize, + index: usize, }, SetupCommandCompleted { - command: String, - index: usize, - exit_code: i32, + command: String, + index: usize, + exit_code: i32, duration_ms: u64, }, SetupCompleted { duration_ms: u64, }, SetupFailed { - command: String, - index: usize, + command: String, + index: usize, exit_code: i32, - stderr: String, + stderr: String, }, StallWatchdogTimeout { - node: String, + node: String, idle_seconds: u64, }, ArtifactCaptured { - node_id: String, - attempt: u32, - node_slug: String, - path: String, - mime: String, - content_md5: String, + node_id: String, + attempt: u32, + node_slug: String, + path: String, + mime: String, + content_md5: String, content_sha256: String, - bytes: u64, + bytes: u64, }, SshAccessReady { ssh_command: String, }, Failover { - stage: String, + stage: String, from_provider: String, - from_model: String, - to_provider: String, - to_model: String, - error: String, + from_model: String, + to_provider: String, + to_model: String, + error: String, }, CliEnsureStarted { cli_name: String, provider: String, }, CliEnsureCompleted { - cli_name: String, - provider: String, + cli_name: String, + provider: String, already_installed: bool, - node_installed: bool, - duration_ms: u64, + node_installed: bool, + duration_ms: u64, }, CliEnsureFailed { - cli_name: String, - provider: String, - error: String, + cli_name: String, + provider: String, + error: String, duration_ms: u64, }, CommandStarted { - node_id: String, - script: String, - command: String, - language: String, + node_id: String, + script: String, + command: String, + language: String, #[serde(default, skip_serializing_if = "Option::is_none")] timeout_ms: Option, }, CommandCompleted { - node_id: String, - stdout: String, - stderr: String, + node_id: String, + stdout: String, + stderr: String, #[serde(default, skip_serializing_if = "Option::is_none")] - exit_code: Option, + exit_code: Option, duration_ms: u64, - timed_out: bool, + timed_out: bool, }, AgentCliStarted { - node_id: String, - visit: u32, - mode: String, + node_id: String, + visit: u32, + mode: String, provider: String, - model: String, - command: String, + model: String, + command: String, }, AgentCliCompleted { - node_id: String, - stdout: String, - stderr: String, - exit_code: i32, + node_id: String, + stdout: String, + stderr: String, + exit_code: i32, duration_ms: u64, }, PullRequestCreated { - pr_url: String, - pr_number: u64, - owner: String, - repo: String, + pr_url: String, + pr_number: u64, + owner: String, + repo: String, base_branch: String, head_branch: String, - title: String, - draft: bool, + title: String, + draft: bool, }, PullRequestFailed { error: String, }, DevcontainerResolved { - dockerfile_lines: usize, - environment_count: usize, + dockerfile_lines: usize, + environment_count: usize, lifecycle_command_count: usize, - workspace_folder: String, + workspace_folder: String, }, DevcontainerLifecycleStarted { - phase: String, + phase: String, command_count: usize, }, DevcontainerLifecycleCommandStarted { - phase: String, + phase: String, command: String, - index: usize, + index: usize, }, DevcontainerLifecycleCommandCompleted { - phase: String, - command: String, - index: usize, - exit_code: i32, + phase: String, + command: String, + index: usize, + exit_code: i32, duration_ms: u64, }, DevcontainerLifecycleCompleted { - phase: String, + phase: String, duration_ms: u64, }, DevcontainerLifecycleFailed { - phase: String, - command: String, - index: usize, + phase: String, + command: String, + index: usize, exit_code: i32, - stderr: String, + stderr: String, }, RetroStarted { #[serde(default, skip_serializing_if = "Option::is_none")] - prompt: Option, + prompt: Option, #[serde(default, skip_serializing_if = "Option::is_none")] provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - model: Option, + model: Option, }, RetroCompleted { duration_ms: u64, #[serde(default, skip_serializing_if = "Option::is_none")] - response: Option, + response: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - retro: Option, + retro: Option, }, RetroFailed { - error: String, + error: String, duration_ms: u64, }, } @@ -1267,15 +1267,15 @@ pub fn event_name(event: &Event) -> &'static str { #[derive(Debug, Default)] struct StoredEventFields { - session_id: Option, - parent_session_id: Option, - node_id: Option, - node_label: Option, - stage_id: Option, - parallel_group_id: Option, + session_id: Option, + parent_session_id: Option, + node_id: Option, + node_label: Option, + stage_id: Option, + parallel_group_id: Option, parallel_branch_id: Option, - tool_call_id: Option, - actor: Option, + tool_call_id: Option, + actor: Option, } fn default_node_label(node_id: Option<&String>, node_label: Option) -> Option { @@ -1293,13 +1293,13 @@ fn node_stored_fields(node_id: Option) -> StoredEventFields { fn billed_token_counts_from_llm(usage: &LlmTokenCounts) -> BilledTokenCounts { BilledTokenCounts { - input_tokens: usage.input_tokens, - output_tokens: usage.output_tokens, - total_tokens: usage.total_tokens(), - reasoning_tokens: usage.reasoning_tokens, - cache_read_tokens: usage.cache_read_tokens, + input_tokens: usage.input_tokens, + output_tokens: usage.output_tokens, + total_tokens: usage.total_tokens(), + reasoning_tokens: usage.reasoning_tokens, + cache_read_tokens: usage.cache_read_tokens, cache_write_tokens: usage.cache_write_tokens, - total_usd_micros: None, + total_usd_micros: None, } } @@ -1479,21 +1479,20 @@ fn event_body_from_event(event: &Event) -> EventBody { manifest_blob, .. } => EventBody::RunCreated(fabro_types::RunCreatedProps { - settings: serde_json::from_value(settings.clone()) - .expect("run.created settings"), - graph: serde_json::from_value(graph.clone()).expect("run.created graph"), - workflow_source: workflow_source.clone(), - workflow_config: workflow_config.clone(), - labels: labels.clone(), - run_dir: run_dir.clone(), + settings: serde_json::from_value(settings.clone()).expect("run.created settings"), + graph: serde_json::from_value(graph.clone()).expect("run.created graph"), + workflow_source: workflow_source.clone(), + workflow_config: workflow_config.clone(), + labels: labels.clone(), + run_dir: run_dir.clone(), working_directory: working_directory.clone(), - host_repo_path: host_repo_path.clone(), - repo_origin_url: repo_origin_url.clone(), - base_branch: base_branch.clone(), - workflow_slug: workflow_slug.clone(), - db_prefix: db_prefix.clone(), - provenance: provenance.clone(), - manifest_blob: *manifest_blob, + host_repo_path: host_repo_path.clone(), + repo_origin_url: repo_origin_url.clone(), + base_branch: base_branch.clone(), + workflow_slug: workflow_slug.clone(), + db_prefix: db_prefix.clone(), + provenance: provenance.clone(), + manifest_blob: *manifest_blob, }), Event::WorkflowRunStarted { name, @@ -1504,18 +1503,18 @@ fn event_body_from_event(event: &Event) -> EventBody { goal, .. } => EventBody::RunStarted(fabro_types::RunStartedProps { - name: name.clone(), - base_branch: base_branch.clone(), - base_sha: base_sha.clone(), - run_branch: run_branch.clone(), + name: name.clone(), + base_branch: base_branch.clone(), + base_sha: base_sha.clone(), + run_branch: run_branch.clone(), worktree_dir: worktree_dir.clone(), - goal: goal.clone(), + goal: goal.clone(), }), Event::RunSubmitted { reason, definition_blob, } => EventBody::RunSubmitted(fabro_types::RunSubmittedProps { - reason: *reason, + reason: *reason, definition_blob: *definition_blob, }), Event::RunStarting { reason } => { @@ -1552,10 +1551,10 @@ fn event_body_from_event(event: &Event) -> EventBody { run_commit_sha, } => EventBody::RunRewound(fabro_types::RunRewoundProps { target_checkpoint_ordinal: *target_checkpoint_ordinal, - target_node_id: target_node_id.clone(), - target_visit: *target_visit, - previous_status: previous_status.clone(), - run_commit_sha: run_commit_sha.clone(), + target_node_id: target_node_id.clone(), + target_visit: *target_visit, + previous_status: previous_status.clone(), + run_commit_sha: run_commit_sha.clone(), }), Event::WorkflowRunCompleted { duration_ms, @@ -1567,14 +1566,14 @@ fn event_body_from_event(event: &Event) -> EventBody { final_patch, billing, } => EventBody::RunCompleted(fabro_types::RunCompletedProps { - duration_ms: *duration_ms, - artifact_count: *artifact_count, - status: status.clone(), - reason: *reason, - total_usd_micros: *total_usd_micros, + duration_ms: *duration_ms, + artifact_count: *artifact_count, + status: status.clone(), + reason: *reason, + total_usd_micros: *total_usd_micros, final_git_commit_sha: final_git_commit_sha.clone(), - final_patch: final_patch.clone(), - billing: billing.clone(), + final_patch: final_patch.clone(), + billing: billing.clone(), }), Event::WorkflowRunFailed { error, @@ -1582,9 +1581,9 @@ fn event_body_from_event(event: &Event) -> EventBody { reason, git_commit_sha, } => EventBody::RunFailed(fabro_types::RunFailedProps { - error: error.to_string(), - duration_ms: *duration_ms, - reason: *reason, + error: error.to_string(), + duration_ms: *duration_ms, + reason: *reason, git_commit_sha: git_commit_sha.clone(), }), Event::RunNotice { @@ -1592,8 +1591,8 @@ fn event_body_from_event(event: &Event) -> EventBody { code, message, } => EventBody::RunNotice(fabro_types::RunNoticeProps { - level: *level, - code: code.clone(), + level: *level, + code: code.clone(), message: message.clone(), }), Event::StageStarted { @@ -1603,9 +1602,9 @@ fn event_body_from_event(event: &Event) -> EventBody { max_attempts, .. } => EventBody::StageStarted(fabro_types::StageStartedProps { - index: *index, + index: *index, handler_type: handler_type.clone(), - attempt: *attempt, + attempt: *attempt, max_attempts: *max_attempts, }), Event::StageCompleted { @@ -1654,8 +1653,8 @@ fn event_body_from_event(event: &Event) -> EventBody { will_retry, .. } => EventBody::StageFailed(fabro_types::StageFailedProps { - index: *index, - failure: Some(failure.clone()), + index: *index, + failure: Some(failure.clone()), will_retry: *will_retry, }), Event::StageRetrying { @@ -1665,10 +1664,10 @@ fn event_body_from_event(event: &Event) -> EventBody { delay_ms, .. } => EventBody::StageRetrying(fabro_types::StageRetryingProps { - index: *index, - attempt: *attempt, + index: *index, + attempt: *attempt, max_attempts: *max_attempts, - delay_ms: *delay_ms, + delay_ms: *delay_ms, }), Event::ParallelStarted { visit, @@ -1676,9 +1675,9 @@ fn event_body_from_event(event: &Event) -> EventBody { join_policy, .. } => EventBody::ParallelStarted(fabro_types::ParallelStartedProps { - visit: *visit, + visit: *visit, branch_count: *branch_count, - join_policy: join_policy.clone(), + join_policy: join_policy.clone(), }), Event::ParallelBranchStarted { index, .. } => { EventBody::ParallelBranchStarted(fabro_types::ParallelBranchStartedProps { @@ -1692,10 +1691,10 @@ fn event_body_from_event(event: &Event) -> EventBody { head_sha, .. } => EventBody::ParallelBranchCompleted(fabro_types::ParallelBranchCompletedProps { - index: *index, + index: *index, duration_ms: *duration_ms, - status: status.clone(), - head_sha: head_sha.clone(), + status: status.clone(), + head_sha: head_sha.clone(), }), Event::ParallelCompleted { visit, @@ -1705,11 +1704,11 @@ fn event_body_from_event(event: &Event) -> EventBody { results, .. } => EventBody::ParallelCompleted(fabro_types::ParallelCompletedProps { - visit: *visit, - duration_ms: *duration_ms, + visit: *visit, + duration_ms: *duration_ms, success_count: *success_count, failure_count: *failure_count, - results: results.clone(), + results: results.clone(), }), Event::InterviewStarted { question_id, @@ -1721,12 +1720,12 @@ fn event_body_from_event(event: &Event) -> EventBody { timeout_seconds, context_display, } => EventBody::InterviewStarted(fabro_types::InterviewStartedProps { - question_id: question_id.clone(), - question: question.clone(), - stage: stage.clone(), - question_type: question_type.clone(), - options: options.clone(), - allow_freeform: *allow_freeform, + question_id: question_id.clone(), + question: question.clone(), + stage: stage.clone(), + question_type: question_type.clone(), + options: options.clone(), + allow_freeform: *allow_freeform, timeout_seconds: *timeout_seconds, context_display: context_display.clone(), }), @@ -1737,8 +1736,8 @@ fn event_body_from_event(event: &Event) -> EventBody { duration_ms, } => EventBody::InterviewCompleted(fabro_types::InterviewCompletedProps { question_id: question_id.clone(), - question: question.clone(), - answer: answer.clone(), + question: question.clone(), + answer: answer.clone(), duration_ms: *duration_ms, }), Event::InterviewTimeout { @@ -1748,8 +1747,8 @@ fn event_body_from_event(event: &Event) -> EventBody { duration_ms, } => EventBody::InterviewTimeout(fabro_types::InterviewTimeoutProps { question_id: question_id.clone(), - question: question.clone(), - stage: stage.clone(), + question: question.clone(), + stage: stage.clone(), duration_ms: *duration_ms, }), Event::InterviewInterrupted { @@ -1760,9 +1759,9 @@ fn event_body_from_event(event: &Event) -> EventBody { duration_ms, } => EventBody::InterviewInterrupted(fabro_types::InterviewInterruptedProps { question_id: question_id.clone(), - question: question.clone(), - stage: stage.clone(), - reason: reason.clone(), + question: question.clone(), + stage: stage.clone(), + reason: reason.clone(), duration_ms: *duration_ms, }), Event::CheckpointCompleted { @@ -1802,16 +1801,16 @@ fn event_body_from_event(event: &Event) -> EventBody { EventBody::GitCommit(fabro_types::GitCommitProps { sha: sha.clone() }) } Event::GitPush { branch, success } => EventBody::GitPush(fabro_types::GitPushProps { - branch: branch.clone(), + branch: branch.clone(), success: *success, }), Event::GitBranch { branch, sha } => EventBody::GitBranch(fabro_types::GitBranchProps { branch: branch.clone(), - sha: sha.clone(), + sha: sha.clone(), }), Event::GitWorktreeAdd { path, branch } => { EventBody::GitWorktreeAdd(fabro_types::GitWorktreeAddProps { - path: path.clone(), + path: path.clone(), branch: branch.clone(), }) } @@ -1819,7 +1818,7 @@ fn event_body_from_event(event: &Event) -> EventBody { EventBody::GitWorktreeRemove(fabro_types::GitWorktreeRemoveProps { path: path.clone() }) } Event::GitFetch { branch, success } => EventBody::GitFetch(fabro_types::GitFetchProps { - branch: branch.clone(), + branch: branch.clone(), success: *success, }), Event::GitReset { sha } => { @@ -1836,20 +1835,20 @@ fn event_body_from_event(event: &Event) -> EventBody { stage_status, is_jump, } => EventBody::EdgeSelected(fabro_types::EdgeSelectedProps { - from_node: from_node.clone(), - to_node: to_node.clone(), - label: label.clone(), - condition: condition.clone(), - reason: reason.clone(), - preferred_label: preferred_label.clone(), + from_node: from_node.clone(), + to_node: to_node.clone(), + label: label.clone(), + condition: condition.clone(), + reason: reason.clone(), + preferred_label: preferred_label.clone(), suggested_next_ids: suggested_next_ids.clone(), - stage_status: stage_status.clone(), - is_jump: *is_jump, + stage_status: stage_status.clone(), + is_jump: *is_jump, }), Event::LoopRestart { from_node, to_node } => { EventBody::LoopRestart(fabro_types::LoopRestartProps { from_node: from_node.clone(), - to_node: to_node.clone(), + to_node: to_node.clone(), }) } Event::Prompt { @@ -1860,11 +1859,11 @@ fn event_body_from_event(event: &Event) -> EventBody { model, .. } => EventBody::StagePrompt(fabro_types::StagePromptProps { - visit: *visit, - text: text.clone(), - mode: mode.clone(), + visit: *visit, + text: text.clone(), + mode: mode.clone(), provider: provider.clone(), - model: model.clone(), + model: model.clone(), }), Event::PromptCompleted { response, @@ -1874,16 +1873,16 @@ fn event_body_from_event(event: &Event) -> EventBody { .. } => EventBody::PromptCompleted(fabro_types::PromptCompletedProps { response: response.clone(), - model: model.clone(), + model: model.clone(), provider: provider.clone(), - billing: billing.clone(), + billing: billing.clone(), }), Event::Agent { visit, event, .. } => match event { AgentEvent::SessionStarted { provider, model } => { EventBody::AgentSessionStarted(fabro_types::AgentSessionStartedProps { provider: provider.clone(), - model: model.clone(), - visit: *visit, + model: model.clone(), + visit: *visit, }) } AgentEvent::SessionEnded => { @@ -1895,7 +1894,7 @@ fn event_body_from_event(event: &Event) -> EventBody { }) } AgentEvent::UserInput { text } => EventBody::AgentInput(fabro_types::AgentInputProps { - text: text.clone(), + text: text.clone(), visit: *visit, }), AgentEvent::AssistantMessage { @@ -1904,21 +1903,21 @@ fn event_body_from_event(event: &Event) -> EventBody { usage, tool_call_count, } => EventBody::AgentMessage(fabro_types::AgentMessageProps { - text: text.clone(), - model: model.clone(), - billing: billed_token_counts_from_llm(usage), + text: text.clone(), + model: model.clone(), + billing: billed_token_counts_from_llm(usage), tool_call_count: *tool_call_count, - visit: *visit, + visit: *visit, }), AgentEvent::ToolCallStarted { tool_name, tool_call_id, arguments, } => EventBody::AgentToolStarted(fabro_types::AgentToolStartedProps { - tool_name: tool_name.clone(), + tool_name: tool_name.clone(), tool_call_id: tool_call_id.clone(), - arguments: arguments.clone(), - visit: *visit, + arguments: arguments.clone(), + visit: *visit, }), AgentEvent::ToolCallCompleted { tool_name, @@ -1926,11 +1925,11 @@ fn event_body_from_event(event: &Event) -> EventBody { output, is_error, } => EventBody::AgentToolCompleted(fabro_types::AgentToolCompletedProps { - tool_name: tool_name.clone(), + tool_name: tool_name.clone(), tool_call_id: tool_call_id.clone(), - output: output.clone(), - is_error: *is_error, - visit: *visit, + output: output.clone(), + is_error: *is_error, + visit: *visit, }), AgentEvent::Error { error } => EventBody::AgentError(fabro_types::AgentErrorProps { error: serde_json::to_value(error).expect("serializable agent error"), @@ -1941,10 +1940,10 @@ fn event_body_from_event(event: &Event) -> EventBody { message, details, } => EventBody::AgentWarning(fabro_types::AgentWarningProps { - kind: kind.clone(), + kind: kind.clone(), message: message.clone(), details: details.clone(), - visit: *visit, + visit: *visit, }), AgentEvent::LoopDetected => { EventBody::AgentLoopDetected(fabro_types::AgentLoopDetectedProps { visit: *visit }) @@ -1952,12 +1951,12 @@ fn event_body_from_event(event: &Event) -> EventBody { AgentEvent::TurnLimitReached { max_turns } => { EventBody::AgentTurnLimitReached(fabro_types::AgentTurnLimitReachedProps { max_turns: *max_turns, - visit: *visit, + visit: *visit, }) } AgentEvent::SteeringInjected { text } => { EventBody::AgentSteeringInjected(fabro_types::AgentSteeringInjectedProps { - text: text.clone(), + text: text.clone(), visit: *visit, }) } @@ -1965,9 +1964,9 @@ fn event_body_from_event(event: &Event) -> EventBody { estimated_tokens, context_window_size, } => EventBody::AgentCompactionStarted(fabro_types::AgentCompactionStartedProps { - estimated_tokens: *estimated_tokens, + estimated_tokens: *estimated_tokens, context_window_size: *context_window_size, - visit: *visit, + visit: *visit, }), AgentEvent::CompactionCompleted { original_turn_count, @@ -1975,11 +1974,11 @@ fn event_body_from_event(event: &Event) -> EventBody { summary_token_estimate, tracked_file_count, } => EventBody::AgentCompactionCompleted(fabro_types::AgentCompactionCompletedProps { - original_turn_count: *original_turn_count, - preserved_turn_count: *preserved_turn_count, + original_turn_count: *original_turn_count, + preserved_turn_count: *preserved_turn_count, summary_token_estimate: *summary_token_estimate, - tracked_file_count: *tracked_file_count, - visit: *visit, + tracked_file_count: *tracked_file_count, + visit: *visit, }), AgentEvent::LlmRetry { provider, @@ -1988,12 +1987,12 @@ fn event_body_from_event(event: &Event) -> EventBody { delay_secs, error, } => EventBody::AgentLlmRetry(fabro_types::AgentLlmRetryProps { - provider: provider.clone(), - model: model.clone(), - attempt: *attempt, + provider: provider.clone(), + model: model.clone(), + attempt: *attempt, delay_secs: *delay_secs, - error: serde_json::to_value(error).expect("serializable sdk error"), - visit: *visit, + error: serde_json::to_value(error).expect("serializable sdk error"), + visit: *visit, }), AgentEvent::SubAgentSpawned { agent_id, @@ -2001,9 +2000,9 @@ fn event_body_from_event(event: &Event) -> EventBody { task, } => EventBody::AgentSubSpawned(fabro_types::AgentSubSpawnedProps { agent_id: agent_id.clone(), - depth: *depth, - task: task.clone(), - visit: *visit, + depth: *depth, + task: task.clone(), + visit: *visit, }), AgentEvent::SubAgentCompleted { agent_id, @@ -2011,11 +2010,11 @@ fn event_body_from_event(event: &Event) -> EventBody { success, turns_used, } => EventBody::AgentSubCompleted(fabro_types::AgentSubCompletedProps { - agent_id: agent_id.clone(), - depth: *depth, - success: *success, + agent_id: agent_id.clone(), + depth: *depth, + success: *success, turns_used: *turns_used, - visit: *visit, + visit: *visit, }), AgentEvent::SubAgentFailed { agent_id, @@ -2023,15 +2022,15 @@ fn event_body_from_event(event: &Event) -> EventBody { error, } => EventBody::AgentSubFailed(fabro_types::AgentSubFailedProps { agent_id: agent_id.clone(), - depth: *depth, - error: serde_json::to_value(error).expect("serializable agent error"), - visit: *visit, + depth: *depth, + error: serde_json::to_value(error).expect("serializable agent error"), + visit: *visit, }), AgentEvent::SubAgentClosed { agent_id, depth } => { EventBody::AgentSubClosed(fabro_types::AgentSubClosedProps { agent_id: agent_id.clone(), - depth: *depth, - visit: *visit, + depth: *depth, + visit: *visit, }) } AgentEvent::McpServerReady { @@ -2039,14 +2038,14 @@ fn event_body_from_event(event: &Event) -> EventBody { tool_count, } => EventBody::AgentMcpReady(fabro_types::AgentMcpReadyProps { server_name: server_name.clone(), - tool_count: *tool_count, - visit: *visit, + tool_count: *tool_count, + visit: *visit, }), AgentEvent::McpServerFailed { server_name, error } => { EventBody::AgentMcpFailed(fabro_types::AgentMcpFailedProps { server_name: server_name.clone(), - error: error.clone(), - visit: *visit, + error: error.clone(), + visit: *visit, }) } AgentEvent::AssistantTextStart @@ -2070,8 +2069,8 @@ fn event_body_from_event(event: &Event) -> EventBody { .. } => EventBody::SubgraphCompleted(fabro_types::SubgraphCompletedProps { steps_executed: *steps_executed, - status: status.clone(), - duration_ms: *duration_ms, + status: status.clone(), + duration_ms: *duration_ms, }), Event::Sandbox { event } => match event { SandboxEvent::Initializing { provider } => { @@ -2087,20 +2086,20 @@ fn event_body_from_event(event: &Event) -> EventBody { memory, url, } => EventBody::SandboxReady(fabro_types::SandboxReadyProps { - provider: provider.clone(), + provider: provider.clone(), duration_ms: *duration_ms, - name: name.clone(), - cpu: *cpu, - memory: *memory, - url: url.clone(), + name: name.clone(), + cpu: *cpu, + memory: *memory, + url: url.clone(), }), SandboxEvent::InitializeFailed { provider, error, duration_ms, } => EventBody::SandboxFailed(fabro_types::SandboxFailedProps { - provider: provider.clone(), - error: error.clone(), + provider: provider.clone(), + error: error.clone(), duration_ms: *duration_ms, }), SandboxEvent::CleanupStarted { provider } => { @@ -2112,13 +2111,13 @@ fn event_body_from_event(event: &Event) -> EventBody { provider, duration_ms, } => EventBody::SandboxCleanupCompleted(fabro_types::SandboxCleanupCompletedProps { - provider: provider.clone(), + provider: provider.clone(), duration_ms: *duration_ms, }), SandboxEvent::CleanupFailed { provider, error } => { EventBody::SandboxCleanupFailed(fabro_types::SandboxCleanupFailedProps { provider: provider.clone(), - error: error.clone(), + error: error.clone(), }) } SandboxEvent::SnapshotPulling { name } => { @@ -2126,7 +2125,7 @@ fn event_body_from_event(event: &Event) -> EventBody { } SandboxEvent::SnapshotPulled { name, duration_ms } => { EventBody::SnapshotPulled(fabro_types::SnapshotCompletedProps { - name: name.clone(), + name: name.clone(), duration_ms: *duration_ms, }) } @@ -2138,31 +2137,31 @@ fn event_body_from_event(event: &Event) -> EventBody { } SandboxEvent::SnapshotReady { name, duration_ms } => { EventBody::SnapshotReady(fabro_types::SnapshotCompletedProps { - name: name.clone(), + name: name.clone(), duration_ms: *duration_ms, }) } SandboxEvent::SnapshotFailed { name, error } => { EventBody::SnapshotFailed(fabro_types::SnapshotFailedProps { - name: name.clone(), + name: name.clone(), error: error.clone(), }) } SandboxEvent::GitCloneStarted { url, branch } => { EventBody::GitCloneStarted(fabro_types::GitCloneStartedProps { - url: url.clone(), + url: url.clone(), branch: branch.clone(), }) } SandboxEvent::GitCloneCompleted { url, duration_ms } => { EventBody::GitCloneCompleted(fabro_types::GitCloneCompletedProps { - url: url.clone(), + url: url.clone(), duration_ms: *duration_ms, }) } SandboxEvent::GitCloneFailed { url, error } => { EventBody::GitCloneFailed(fabro_types::GitCloneFailedProps { - url: url.clone(), + url: url.clone(), error: error.clone(), }) } @@ -2174,11 +2173,11 @@ fn event_body_from_event(event: &Event) -> EventBody { host_working_directory, container_mount_point, } => EventBody::SandboxInitialized(fabro_types::SandboxInitializedProps { - working_directory: working_directory.clone(), - provider: provider.clone(), - identifier: identifier.clone(), + working_directory: working_directory.clone(), + provider: provider.clone(), + identifier: identifier.clone(), host_working_directory: host_working_directory.clone(), - container_mount_point: container_mount_point.clone(), + container_mount_point: container_mount_point.clone(), }), Event::SetupStarted { command_count } => { EventBody::SetupStarted(fabro_types::SetupStartedProps { @@ -2188,7 +2187,7 @@ fn event_body_from_event(event: &Event) -> EventBody { Event::SetupCommandStarted { command, index } => { EventBody::SetupCommandStarted(fabro_types::SetupCommandStartedProps { command: command.clone(), - index: *index, + index: *index, }) } Event::SetupCommandCompleted { @@ -2197,9 +2196,9 @@ fn event_body_from_event(event: &Event) -> EventBody { exit_code, duration_ms, } => EventBody::SetupCommandCompleted(fabro_types::SetupCommandCompletedProps { - command: command.clone(), - index: *index, - exit_code: *exit_code, + command: command.clone(), + index: *index, + exit_code: *exit_code, duration_ms: *duration_ms, }), Event::SetupCompleted { duration_ms } => { @@ -2213,10 +2212,10 @@ fn event_body_from_event(event: &Event) -> EventBody { exit_code, stderr, } => EventBody::SetupFailed(fabro_types::SetupFailedProps { - command: command.clone(), - index: *index, + command: command.clone(), + index: *index, exit_code: *exit_code, - stderr: stderr.clone(), + stderr: stderr.clone(), }), Event::StallWatchdogTimeout { idle_seconds, .. } => { EventBody::StallWatchdogTimeout(fabro_types::StallWatchdogTimeoutProps { @@ -2233,13 +2232,13 @@ fn event_body_from_event(event: &Event) -> EventBody { bytes, .. } => EventBody::ArtifactCaptured(fabro_types::ArtifactCapturedProps { - attempt: *attempt, - node_slug: node_slug.clone(), - path: path.clone(), - mime: mime.clone(), - content_md5: content_md5.clone(), + attempt: *attempt, + node_slug: node_slug.clone(), + path: path.clone(), + mime: mime.clone(), + content_md5: content_md5.clone(), content_sha256: content_sha256.clone(), - bytes: *bytes, + bytes: *bytes, }), Event::SshAccessReady { ssh_command } => { EventBody::SshAccessReady(fabro_types::SshAccessReadyProps { @@ -2255,10 +2254,10 @@ fn event_body_from_event(event: &Event) -> EventBody { .. } => EventBody::Failover(fabro_types::FailoverProps { from_provider: from_provider.clone(), - from_model: from_model.clone(), - to_provider: to_provider.clone(), - to_model: to_model.clone(), - error: error.clone(), + from_model: from_model.clone(), + to_provider: to_provider.clone(), + to_model: to_model.clone(), + error: error.clone(), }), Event::CliEnsureStarted { cli_name, provider } => { EventBody::CliEnsureStarted(fabro_types::CliEnsureStartedProps { @@ -2273,11 +2272,11 @@ fn event_body_from_event(event: &Event) -> EventBody { node_installed, duration_ms, } => EventBody::CliEnsureCompleted(fabro_types::CliEnsureCompletedProps { - cli_name: cli_name.clone(), - provider: provider.clone(), + cli_name: cli_name.clone(), + provider: provider.clone(), already_installed: *already_installed, - node_installed: *node_installed, - duration_ms: *duration_ms, + node_installed: *node_installed, + duration_ms: *duration_ms, }), Event::CliEnsureFailed { cli_name, @@ -2285,9 +2284,9 @@ fn event_body_from_event(event: &Event) -> EventBody { error, duration_ms, } => EventBody::CliEnsureFailed(fabro_types::CliEnsureFailedProps { - cli_name: cli_name.clone(), - provider: provider.clone(), - error: error.clone(), + cli_name: cli_name.clone(), + provider: provider.clone(), + error: error.clone(), duration_ms: *duration_ms, }), Event::CommandStarted { @@ -2297,9 +2296,9 @@ fn event_body_from_event(event: &Event) -> EventBody { timeout_ms, .. } => EventBody::CommandStarted(fabro_types::CommandStartedProps { - script: script.clone(), - command: command.clone(), - language: language.clone(), + script: script.clone(), + command: command.clone(), + language: language.clone(), timeout_ms: *timeout_ms, }), Event::CommandCompleted { @@ -2310,11 +2309,11 @@ fn event_body_from_event(event: &Event) -> EventBody { timed_out, .. } => EventBody::CommandCompleted(fabro_types::CommandCompletedProps { - stdout: stdout.clone(), - stderr: stderr.clone(), - exit_code: *exit_code, + stdout: stdout.clone(), + stderr: stderr.clone(), + exit_code: *exit_code, duration_ms: *duration_ms, - timed_out: *timed_out, + timed_out: *timed_out, }), Event::AgentCliStarted { visit, @@ -2324,11 +2323,11 @@ fn event_body_from_event(event: &Event) -> EventBody { command, .. } => EventBody::AgentCliStarted(fabro_types::AgentCliStartedProps { - visit: *visit, - mode: mode.clone(), + visit: *visit, + mode: mode.clone(), provider: provider.clone(), - model: model.clone(), - command: command.clone(), + model: model.clone(), + command: command.clone(), }), Event::AgentCliCompleted { stdout, @@ -2337,9 +2336,9 @@ fn event_body_from_event(event: &Event) -> EventBody { duration_ms, .. } => EventBody::AgentCliCompleted(fabro_types::AgentCliCompletedProps { - stdout: stdout.clone(), - stderr: stderr.clone(), - exit_code: *exit_code, + stdout: stdout.clone(), + stderr: stderr.clone(), + exit_code: *exit_code, duration_ms: *duration_ms, }), Event::PullRequestCreated { @@ -2352,14 +2351,14 @@ fn event_body_from_event(event: &Event) -> EventBody { title, draft, } => EventBody::PullRequestCreated(fabro_types::PullRequestCreatedProps { - pr_url: pr_url.clone(), - pr_number: *pr_number, - owner: owner.clone(), - repo: repo.clone(), + pr_url: pr_url.clone(), + pr_number: *pr_number, + owner: owner.clone(), + repo: repo.clone(), base_branch: base_branch.clone(), head_branch: head_branch.clone(), - title: title.clone(), - draft: *draft, + title: title.clone(), + draft: *draft, }), Event::PullRequestFailed { error } => { EventBody::PullRequestFailed(fabro_types::PullRequestFailedProps { @@ -2372,17 +2371,17 @@ fn event_body_from_event(event: &Event) -> EventBody { lifecycle_command_count, workspace_folder, } => EventBody::DevcontainerResolved(fabro_types::DevcontainerResolvedProps { - dockerfile_lines: *dockerfile_lines, - environment_count: *environment_count, + dockerfile_lines: *dockerfile_lines, + environment_count: *environment_count, lifecycle_command_count: *lifecycle_command_count, - workspace_folder: workspace_folder.clone(), + workspace_folder: workspace_folder.clone(), }), Event::DevcontainerLifecycleStarted { phase, command_count, } => EventBody::DevcontainerLifecycleStarted( fabro_types::DevcontainerLifecycleStartedProps { - phase: phase.clone(), + phase: phase.clone(), command_count: *command_count, }, ), @@ -2392,9 +2391,9 @@ fn event_body_from_event(event: &Event) -> EventBody { index, } => EventBody::DevcontainerLifecycleCommandStarted( fabro_types::DevcontainerLifecycleCommandStartedProps { - phase: phase.clone(), + phase: phase.clone(), command: command.clone(), - index: *index, + index: *index, }, ), Event::DevcontainerLifecycleCommandCompleted { @@ -2405,17 +2404,17 @@ fn event_body_from_event(event: &Event) -> EventBody { duration_ms, } => EventBody::DevcontainerLifecycleCommandCompleted( fabro_types::DevcontainerLifecycleCommandCompletedProps { - phase: phase.clone(), - command: command.clone(), - index: *index, - exit_code: *exit_code, + phase: phase.clone(), + command: command.clone(), + index: *index, + exit_code: *exit_code, duration_ms: *duration_ms, }, ), Event::DevcontainerLifecycleCompleted { phase, duration_ms } => { EventBody::DevcontainerLifecycleCompleted( fabro_types::DevcontainerLifecycleCompletedProps { - phase: phase.clone(), + phase: phase.clone(), duration_ms: *duration_ms, }, ) @@ -2428,11 +2427,11 @@ fn event_body_from_event(event: &Event) -> EventBody { stderr, } => { EventBody::DevcontainerLifecycleFailed(fabro_types::DevcontainerLifecycleFailedProps { - phase: phase.clone(), - command: command.clone(), - index: *index, + phase: phase.clone(), + command: command.clone(), + index: *index, exit_code: *exit_code, - stderr: stderr.clone(), + stderr: stderr.clone(), }) } Event::RetroStarted { @@ -2440,9 +2439,9 @@ fn event_body_from_event(event: &Event) -> EventBody { provider, model, } => EventBody::RetroStarted(fabro_types::RetroStartedProps { - prompt: prompt.clone(), + prompt: prompt.clone(), provider: provider.clone(), - model: model.clone(), + model: model.clone(), }), Event::RetroCompleted { duration_ms, @@ -2450,12 +2449,12 @@ fn event_body_from_event(event: &Event) -> EventBody { retro, } => EventBody::RetroCompleted(fabro_types::RetroCompletedProps { duration_ms: *duration_ms, - response: response.clone(), - retro: retro.clone(), + response: response.clone(), + retro: retro.clone(), }), Event::RetroFailed { error, duration_ms } => { EventBody::RetroFailed(fabro_types::RetroFailedProps { - error: error.clone(), + error: error.clone(), duration_ms: *duration_ms, }) } @@ -2467,9 +2466,9 @@ fn event_body_from_event(event: &Event) -> EventBody { /// that happen inside a concrete stage execution. #[derive(Clone, Debug)] pub struct StageScope { - pub node_id: String, - pub visit: u32, - pub parallel_group_id: Option, + pub node_id: String, + pub visit: u32, + pub parallel_group_id: Option, pub parallel_branch_id: Option, } @@ -2478,9 +2477,9 @@ impl StageScope { /// ids from the current context. pub fn from_context(context: &WfContext, node_id: impl Into) -> Self { Self { - node_id: node_id.into(), - visit: u32::try_from(visit_from_context(context)).unwrap_or(u32::MAX), - parallel_group_id: context.parallel_group_id(), + node_id: node_id.into(), + visit: u32::try_from(visit_from_context(context)).unwrap_or(u32::MAX), + parallel_group_id: context.parallel_group_id(), parallel_branch_id: context.parallel_branch_id(), } } @@ -2512,9 +2511,9 @@ impl StageScope { parallel_branch_id: ParallelBranchId, ) -> Self { Self { - node_id: target_node_id.into(), - visit: target_visit, - parallel_group_id: Some(parallel_group_id), + node_id: target_node_id.into(), + visit: target_visit, + parallel_group_id: Some(parallel_group_id), parallel_branch_id: Some(parallel_branch_id), } } @@ -2759,8 +2758,8 @@ type EventListener = Arc; /// Callback-based event emitter for workflow run events. pub struct Emitter { - run_id: RunId, - listeners: std::sync::Mutex>, + run_id: RunId, + listeners: std::sync::Mutex>, /// Epoch milliseconds of the last `emit()` or `touch()` call. 0 until first /// event. last_event_at: AtomicI64, @@ -2893,13 +2892,13 @@ mod tests { received_clone.lock().unwrap().push(event.clone()); }); emitter.emit(&Event::WorkflowRunStarted { - name: "test".to_string(), - run_id: fixtures::RUN_1, - base_branch: None, - base_sha: None, - run_branch: None, + name: "test".to_string(), + run_id: fixtures::RUN_1, + base_branch: None, + base_sha: None, + run_branch: None, worktree_dir: None, - goal: None, + goal: None, }); let events = received.lock().unwrap(); assert_eq!(events.len(), 1); @@ -2942,9 +2941,9 @@ mod tests { }, Utc::now(), Some(&StageScope { - node_id: "plan".to_string(), - visit: 1, - parallel_group_id: None, + node_id: "plan".to_string(), + visit: 1, + parallel_group_id: None, parallel_branch_id: None, }), ); @@ -2962,28 +2961,31 @@ mod tests { #[test] fn run_event_stage_completed_keeps_response_and_signature_snapshots() { - let stored = to_run_event(&fixtures::RUN_2, &Event::StageCompleted { - node_id: "plan".to_string(), - name: "Plan".to_string(), - index: 0, - duration_ms: 5000, - status: "success".to_string(), - preferred_label: None, - suggested_next_ids: Vec::new(), - billing: None, - failure: None, - notes: None, - files_touched: Vec::new(), - context_updates: None, - jump_to_node: None, - context_values: None, - node_visits: None, - loop_failure_signatures: Some(BTreeMap::from([("sig-a".to_string(), 2usize)])), - restart_failure_signatures: Some(BTreeMap::from([("sig-b".to_string(), 1usize)])), - response: Some("done".to_string()), - attempt: 1, - max_attempts: 1, - }); + let stored = to_run_event( + &fixtures::RUN_2, + &Event::StageCompleted { + node_id: "plan".to_string(), + name: "Plan".to_string(), + index: 0, + duration_ms: 5000, + status: "success".to_string(), + preferred_label: None, + suggested_next_ids: Vec::new(), + billing: None, + failure: None, + notes: None, + files_touched: Vec::new(), + context_updates: None, + jump_to_node: None, + context_values: None, + node_visits: None, + loop_failure_signatures: Some(BTreeMap::from([("sig-a".to_string(), 2usize)])), + restart_failure_signatures: Some(BTreeMap::from([("sig-b".to_string(), 1usize)])), + response: Some("done".to_string()), + attempt: 1, + max_attempts: 1, + }, + ); let properties = stored.properties().unwrap(); assert_eq!(properties["response"], "done"); @@ -2993,16 +2995,19 @@ mod tests { #[test] fn run_event_stage_failure_keeps_failure_detail() { - let stored = to_run_event(&fixtures::RUN_3, &Event::StageFailed { - node_id: "code".to_string(), - name: "Code".to_string(), - index: 1, - failure: FailureDetail::new( - "lint failed", - crate::outcome::FailureCategory::Deterministic, - ), - will_retry: true, - }); + let stored = to_run_event( + &fixtures::RUN_3, + &Event::StageFailed { + node_id: "code".to_string(), + name: "Code".to_string(), + index: 1, + failure: FailureDetail::new( + "lint failed", + crate::outcome::FailureCategory::Deterministic, + ), + will_retry: true, + }, + ); assert_eq!(stored.event_name(), "stage.failed"); let properties = stored.properties().unwrap(); @@ -3013,17 +3018,20 @@ mod tests { #[test] fn run_event_agent_tool_started_moves_session_metadata_to_header() { - let stored = to_run_event(&fixtures::RUN_4, &Event::Agent { - stage: "code".to_string(), - visit: 2, - event: AgentEvent::ToolCallStarted { - tool_name: "read_file".to_string(), - tool_call_id: "call_1".to_string(), - arguments: serde_json::json!({"path": "src/main.rs"}), + let stored = to_run_event( + &fixtures::RUN_4, + &Event::Agent { + stage: "code".to_string(), + visit: 2, + event: AgentEvent::ToolCallStarted { + tool_name: "read_file".to_string(), + tool_call_id: "call_1".to_string(), + arguments: serde_json::json!({"path": "src/main.rs"}), + }, + session_id: Some("ses_child".to_string()), + parent_session_id: Some("ses_parent".to_string()), }, - session_id: Some("ses_child".to_string()), - parent_session_id: Some("ses_parent".to_string()), - }); + ); assert_eq!(stored.event_name(), "agent.tool.started"); assert_eq!(stored.node_id.as_deref(), Some("code")); @@ -3038,16 +3046,19 @@ mod tests { #[test] fn run_event_sandbox_event_keeps_properties_nested() { - let stored = to_run_event(&fixtures::RUN_5, &Event::Sandbox { - event: SandboxEvent::Ready { - provider: "daytona".to_string(), - duration_ms: 2500, - name: Some("sandbox-1".to_string()), - cpu: Some(4.0), - memory: Some(8.0), - url: Some("https://example.test".to_string()), + let stored = to_run_event( + &fixtures::RUN_5, + &Event::Sandbox { + event: SandboxEvent::Ready { + provider: "daytona".to_string(), + duration_ms: 2500, + name: Some("sandbox-1".to_string()), + cpu: Some(4.0), + memory: Some(8.0), + url: Some("https://example.test".to_string()), + }, }, - }); + ); assert_eq!(stored.event_name(), "sandbox.ready"); assert!(stored.node_id.is_none()); @@ -3058,12 +3069,15 @@ mod tests { #[test] fn run_event_workflow_failure_uses_display_error() { - let stored = to_run_event(&fixtures::RUN_6, &Event::WorkflowRunFailed { - error: FabroError::handler("boom"), - duration_ms: 900, - reason: Some(StatusReason::WorkflowError), - git_commit_sha: Some("abc123".to_string()), - }); + let stored = to_run_event( + &fixtures::RUN_6, + &Event::WorkflowRunFailed { + error: Error::handler("boom"), + duration_ms: 900, + reason: Some(StatusReason::WorkflowError), + git_commit_sha: Some("abc123".to_string()), + }, + ); assert_eq!(stored.event_name(), "run.failed"); let properties = stored.properties().unwrap(); @@ -3079,11 +3093,14 @@ mod tests { std::time::Duration::from_millis(1), ); let run_store = store.create_run(&fixtures::RUN_7).await.unwrap(); - let stored = to_run_event(&fixtures::RUN_7, &Event::RunNotice { - level: RunNoticeLevel::Warn, - code: "example".to_string(), - message: "notice".to_string(), - }); + let stored = to_run_event( + &fixtures::RUN_7, + &Event::RunNotice { + level: RunNoticeLevel::Warn, + code: "example".to_string(), + message: "notice".to_string(), + }, + ); let payload = build_redacted_event_payload(&stored, &fixtures::RUN_7).unwrap(); run_store.append_event(&payload).await.unwrap(); @@ -3140,11 +3157,14 @@ mod tests { #[test] fn build_redacted_event_payload_requires_id() { - let stored = to_run_event(&fixtures::RUN_8, &Event::RetroStarted { - prompt: Some("Analyze the run".to_string()), - provider: None, - model: None, - }); + let stored = to_run_event( + &fixtures::RUN_8, + &Event::RetroStarted { + prompt: Some("Analyze the run".to_string()), + provider: None, + model: None, + }, + ); let payload = build_redacted_event_payload(&stored, &fixtures::RUN_8).unwrap(); assert_eq!(payload.as_value()["id"], stored.id); assert_eq!(payload.as_value()["event"], "retro.started"); @@ -3158,31 +3178,31 @@ mod tests { fn event_name_matches_new_dot_notation() { assert_eq!( event_name(&Event::RetroStarted { - prompt: None, + prompt: None, provider: None, - model: None, + model: None, }), "retro.started" ); assert_eq!( event_name(&Event::ParallelBranchStarted { - parallel_group_id: StageId::new("plan", 1), + parallel_group_id: StageId::new("plan", 1), parallel_branch_id: ParallelBranchId::new(StageId::new("plan", 1), 0), - branch: "fork".to_string(), - index: 0, + branch: "fork".to_string(), + index: 0, }), "parallel.branch.started" ); assert_eq!( event_name(&Event::Agent { - stage: "code".to_string(), - visit: 1, - event: AgentEvent::SubAgentSpawned { + stage: "code".to_string(), + visit: 1, + event: AgentEvent::SubAgentSpawned { agent_id: "a1".to_string(), - depth: 1, - task: "do it".to_string(), + depth: 1, + task: "do it".to_string(), }, - session_id: None, + session_id: None, parent_session_id: None, }), "agent.sub.spawned" @@ -3194,18 +3214,18 @@ mod tests { let stored = to_run_event_at( &fixtures::RUN_1, &Event::StageStarted { - node_id: "review".to_string(), - name: "review".to_string(), - index: 1, + node_id: "review".to_string(), + name: "review".to_string(), + index: 1, handler_type: "agent".to_string(), - attempt: 1, + attempt: 1, max_attempts: 1, }, Utc::now(), Some(&StageScope { - node_id: "review".to_string(), - visit: 1, - parallel_group_id: Some(StageId::new("fanout", 2)), + node_id: "review".to_string(), + visit: 1, + parallel_group_id: Some(StageId::new("fanout", 2)), parallel_branch_id: Some(ParallelBranchId::new(StageId::new("fanout", 2), 1)), }), ); @@ -3218,24 +3238,30 @@ mod tests { #[test] fn parallel_started_populates_parallel_group_id() { - let stored = to_run_event(&fixtures::RUN_1, &Event::ParallelStarted { - node_id: "fanout".to_string(), - visit: 2, - branch_count: 3, - join_policy: "wait_all".to_string(), - }); + let stored = to_run_event( + &fixtures::RUN_1, + &Event::ParallelStarted { + node_id: "fanout".to_string(), + visit: 2, + branch_count: 3, + join_policy: "wait_all".to_string(), + }, + ); assert_eq!(stored.parallel_group_id, Some(StageId::new("fanout", 2))); assert!(stored.parallel_branch_id.is_none()); } #[test] fn parallel_branch_started_populates_group_and_branch_ids() { - let stored = to_run_event(&fixtures::RUN_1, &Event::ParallelBranchStarted { - parallel_group_id: StageId::new("fanout", 2), - parallel_branch_id: ParallelBranchId::new(StageId::new("fanout", 2), 1), - branch: "review".to_string(), - index: 1, - }); + let stored = to_run_event( + &fixtures::RUN_1, + &Event::ParallelBranchStarted { + parallel_group_id: StageId::new("fanout", 2), + parallel_branch_id: ParallelBranchId::new(StageId::new("fanout", 2), 1), + branch: "review".to_string(), + index: 1, + }, + ); assert_eq!(stored.parallel_group_id, Some(StageId::new("fanout", 2))); assert_eq!( stored.parallel_branch_id, @@ -3248,21 +3274,21 @@ mod tests { let stored = to_run_event_at( &fixtures::RUN_1, &Event::Agent { - stage: "code".to_string(), - visit: 3, - event: AgentEvent::ToolCallStarted { - tool_name: "read_file".to_string(), + stage: "code".to_string(), + visit: 3, + event: AgentEvent::ToolCallStarted { + tool_name: "read_file".to_string(), tool_call_id: "call_abc".to_string(), - arguments: serde_json::json!({"path": "src/main.rs"}), + arguments: serde_json::json!({"path": "src/main.rs"}), }, - session_id: Some("ses_1".to_string()), + session_id: Some("ses_1".to_string()), parent_session_id: None, }, Utc::now(), Some(&StageScope { - node_id: "code".to_string(), - visit: 3, - parallel_group_id: Some(StageId::new("fanout", 2)), + node_id: "code".to_string(), + visit: 3, + parallel_group_id: Some(StageId::new("fanout", 2)), parallel_branch_id: Some(ParallelBranchId::new(StageId::new("fanout", 2), 0)), }), ); @@ -3282,19 +3308,19 @@ mod tests { // Prompt, InterviewStarted, Failover, GitCommit) should pick up stage_id // / parallel_group_id / parallel_branch_id from the scope argument. let scope = StageScope { - node_id: "build".to_string(), - visit: 2, - parallel_group_id: Some(StageId::new("fanout", 1)), + node_id: "build".to_string(), + visit: 2, + parallel_group_id: Some(StageId::new("fanout", 1)), parallel_branch_id: Some(ParallelBranchId::new(StageId::new("fanout", 1), 0)), }; let command_started = to_run_event_at( &fixtures::RUN_1, &Event::CommandStarted { - node_id: "build".to_string(), - script: "echo".to_string(), - command: "echo".to_string(), - language: "shell".to_string(), + node_id: "build".to_string(), + script: "echo".to_string(), + command: "echo".to_string(), + language: "shell".to_string(), timeout_ms: None, }, Utc::now(), @@ -3307,12 +3333,12 @@ mod tests { let prompt = to_run_event_at( &fixtures::RUN_1, &Event::Prompt { - stage: "build".to_string(), - visit: 2, - text: "do it".to_string(), - mode: None, + stage: "build".to_string(), + visit: 2, + text: "do it".to_string(), + mode: None, provider: None, - model: None, + model: None, }, Utc::now(), Some(&scope), @@ -3323,7 +3349,7 @@ mod tests { &fixtures::RUN_1, &Event::GitCommit { node_id: Some("build".to_string()), - sha: "deadbeef".to_string(), + sha: "deadbeef".to_string(), }, Utc::now(), Some(&scope), @@ -3342,42 +3368,52 @@ mod tests { #[test] fn control_action_events_carry_actor_in_envelope() { let actor = ActorRef { - kind: ActorKind::User, - id: Some("alice".to_string()), + kind: ActorKind::User, + id: Some("alice".to_string()), display: Some("alice".to_string()), }; - let cancel = to_run_event(&fixtures::RUN_1, &Event::RunCancelRequested { - actor: Some(actor.clone()), - }); + let cancel = to_run_event( + &fixtures::RUN_1, + &Event::RunCancelRequested { + actor: Some(actor.clone()), + }, + ); assert_eq!(cancel.event_name(), "run.cancel.requested"); assert_eq!(cancel.actor.as_ref().expect("actor set"), &actor); - let pause = to_run_event(&fixtures::RUN_1, &Event::RunPauseRequested { - actor: Some(actor.clone()), - }); + let pause = to_run_event( + &fixtures::RUN_1, + &Event::RunPauseRequested { + actor: Some(actor.clone()), + }, + ); assert_eq!(pause.actor.as_ref().expect("actor set"), &actor); - let unpause = to_run_event(&fixtures::RUN_1, &Event::RunUnpauseRequested { - actor: None, - }); + let unpause = to_run_event( + &fixtures::RUN_1, + &Event::RunUnpauseRequested { actor: None }, + ); assert!(unpause.actor.is_none()); } #[test] fn agent_assistant_message_populates_agent_actor() { - let stored = to_run_event(&fixtures::RUN_1, &Event::Agent { - stage: "code".to_string(), - visit: 1, - event: AgentEvent::AssistantMessage { - text: "ok".to_string(), - model: "claude-sonnet".to_string(), - usage: LlmTokenCounts::default(), - tool_call_count: 0, + let stored = to_run_event( + &fixtures::RUN_1, + &Event::Agent { + stage: "code".to_string(), + visit: 1, + event: AgentEvent::AssistantMessage { + text: "ok".to_string(), + model: "claude-sonnet".to_string(), + usage: LlmTokenCounts::default(), + tool_call_count: 0, + }, + session_id: Some("ses_agent".to_string()), + parent_session_id: None, }, - session_id: Some("ses_agent".to_string()), - parent_session_id: None, - }); + ); let actor = stored.actor.as_ref().expect("actor set"); assert_eq!(actor.kind, ActorKind::Agent); assert_eq!(actor.id.as_deref(), Some("ses_agent")); @@ -3390,31 +3426,34 @@ mod tests { use ::fabro_types::{Graph, RunAuthMethod, RunSubjectProvenance, fixtures}; let provenance = RunProvenance { - server: None, - client: None, + server: None, + client: None, subject: Some(RunSubjectProvenance { - login: Some("alice".to_string()), + login: Some("alice".to_string()), auth_method: RunAuthMethod::Cookie, }), }; - let stored = to_run_event(&fixtures::RUN_1, &Event::RunCreated { - run_id: fixtures::RUN_1, - settings: serde_json::to_value(SettingsLayer::default()).unwrap(), - graph: serde_json::to_value(Graph::new("test")).unwrap(), - workflow_source: None, - workflow_config: None, - labels: BTreeMap::default(), - run_dir: "/tmp/run".to_string(), - working_directory: "/tmp/run".to_string(), - host_repo_path: None, - repo_origin_url: None, - base_branch: None, - workflow_slug: None, - db_prefix: None, - provenance: Some(provenance), - manifest_blob: None, - }); + let stored = to_run_event( + &fixtures::RUN_1, + &Event::RunCreated { + run_id: fixtures::RUN_1, + settings: serde_json::to_value(SettingsLayer::default()).unwrap(), + graph: serde_json::to_value(Graph::new("test")).unwrap(), + workflow_source: None, + workflow_config: None, + labels: BTreeMap::default(), + run_dir: "/tmp/run".to_string(), + working_directory: "/tmp/run".to_string(), + host_repo_path: None, + repo_origin_url: None, + base_branch: None, + workflow_slug: None, + db_prefix: None, + provenance: Some(provenance), + manifest_blob: None, + }, + ); let actor = stored.actor.as_ref().expect("actor set"); assert_eq!(actor.kind, ActorKind::User); assert_eq!(actor.id.as_deref(), Some("alice")); diff --git a/lib/crates/fabro-workflow/src/file_resolver.rs b/lib/crates/fabro-workflow/src/file_resolver.rs index c53ec7c30..ec6afbe21 100644 --- a/lib/crates/fabro-workflow/src/file_resolver.rs +++ b/lib/crates/fabro-workflow/src/file_resolver.rs @@ -8,7 +8,7 @@ pub trait FileResolver: Send + Sync { #[derive(Clone, Debug, PartialEq, Eq)] pub struct ResolvedFile { pub logical_path: PathBuf, - pub content: String, + pub content: String, } #[derive(Clone, Debug, Default)] diff --git a/lib/crates/fabro-workflow/src/git.rs b/lib/crates/fabro-workflow/src/git.rs index 5480f1484..7f194f451 100644 --- a/lib/crates/fabro-workflow/src/git.rs +++ b/lib/crates/fabro-workflow/src/git.rs @@ -9,7 +9,7 @@ use fabro_types::settings::SettingsLayer; use tokio::task::{JoinError, spawn_blocking}; use tokio::time::timeout; -use crate::error::{FabroError, Result}; +use crate::error::{Error, Result}; /// Branch prefix for workflow run branches (e.g. `fabro/run/{run_id}`). pub const RUN_BRANCH_PREFIX: &str = "fabro/run/"; @@ -22,8 +22,8 @@ pub fn git_author_from_settings(settings: &SettingsLayer) -> GitAuthor { .unwrap_or_default() } -fn git_error(msg: impl Into) -> FabroError { - FabroError::engine(msg.into()) +fn git_error(msg: impl Into) -> Error { + Error::engine(msg.into()) } /// Return a pre-configured `git` command with auto-maintenance disabled. @@ -196,7 +196,7 @@ pub fn push_run_branches( /// Error from [`blocking_push_with_timeout`]. pub enum BlockingPushError { /// The git push itself failed. - Push(FabroError), + Push(Error), /// The spawned blocking task panicked. Panicked(JoinError), /// The push did not complete within the timeout. @@ -420,93 +420,121 @@ mod tests { let store = test_store(); let run = store.create_run(&fixtures::RUN_1).await.unwrap(); - append_event(&run, &fixtures::RUN_1, &Event::Prompt { - stage: "work".into(), - visit: 2, - text: "hello".into(), - mode: Some("prompt".into()), - provider: Some("openai".into()), - model: Some("gpt-5.4".into()), - }) + append_event( + &run, + &fixtures::RUN_1, + &Event::Prompt { + stage: "work".into(), + visit: 2, + text: "hello".into(), + mode: Some("prompt".into()), + provider: Some("openai".into()), + model: Some("gpt-5.4".into()), + }, + ) .await .unwrap(); - append_event(&run, &fixtures::RUN_1, &Event::PromptCompleted { - node_id: "work".into(), - response: "world".into(), - model: "gpt-5.4".into(), - provider: "openai".into(), - billing: None, - }) + append_event( + &run, + &fixtures::RUN_1, + &Event::PromptCompleted { + node_id: "work".into(), + response: "world".into(), + model: "gpt-5.4".into(), + provider: "openai".into(), + billing: None, + }, + ) .await .unwrap(); - append_event(&run, &fixtures::RUN_1, &Event::StageCompleted { - node_id: "work".into(), - name: "Work".into(), - index: 2, - duration_ms: 100, - status: "success".into(), - preferred_label: None, - suggested_next_ids: Vec::new(), - billing: None, - failure: None, - notes: None, - files_touched: Vec::new(), - context_updates: None, - jump_to_node: None, - context_values: None, - node_visits: Some(std::collections::BTreeMap::from([("work".into(), 2)])), - loop_failure_signatures: None, - restart_failure_signatures: None, - response: Some("world".into()), - attempt: 1, - max_attempts: 1, - }) + append_event( + &run, + &fixtures::RUN_1, + &Event::StageCompleted { + node_id: "work".into(), + name: "Work".into(), + index: 2, + duration_ms: 100, + status: "success".into(), + preferred_label: None, + suggested_next_ids: Vec::new(), + billing: None, + failure: None, + notes: None, + files_touched: Vec::new(), + context_updates: None, + jump_to_node: None, + context_values: None, + node_visits: Some(std::collections::BTreeMap::from([("work".into(), 2)])), + loop_failure_signatures: None, + restart_failure_signatures: None, + response: Some("world".into()), + attempt: 1, + max_attempts: 1, + }, + ) .await .unwrap(); - append_event(&run, &fixtures::RUN_1, &Event::CommandStarted { - node_id: "work".into(), - script: "echo hi".into(), - command: "echo hi".into(), - language: "shell".into(), - timeout_ms: None, - }) + append_event( + &run, + &fixtures::RUN_1, + &Event::CommandStarted { + node_id: "work".into(), + script: "echo hi".into(), + command: "echo hi".into(), + language: "shell".into(), + timeout_ms: None, + }, + ) .await .unwrap(); - append_event(&run, &fixtures::RUN_1, &Event::CommandCompleted { - node_id: "work".into(), - stdout: "hi\n".into(), - stderr: String::new(), - exit_code: Some(0), - duration_ms: 10, - timed_out: false, - }) + append_event( + &run, + &fixtures::RUN_1, + &Event::CommandCompleted { + node_id: "work".into(), + stdout: "hi\n".into(), + stderr: String::new(), + exit_code: Some(0), + duration_ms: 10, + timed_out: false, + }, + ) .await .unwrap(); - append_event(&run, &fixtures::RUN_1, &Event::ParallelCompleted { - node_id: "work".into(), - visit: 2, - duration_ms: 100, - success_count: 1, - failure_count: 0, - results: vec![serde_json::json!({"id": "a"})], - }) + append_event( + &run, + &fixtures::RUN_1, + &Event::ParallelCompleted { + node_id: "work".into(), + visit: 2, + duration_ms: 100, + success_count: 1, + failure_count: 0, + results: vec![serde_json::json!({"id": "a"})], + }, + ) .await .unwrap(); - append_event(&run, &fixtures::RUN_1, &Event::CheckpointCompleted { - node_id: "work".into(), - status: "success".into(), - current_node: "work".into(), - completed_nodes: Vec::new(), - node_retries: std::collections::BTreeMap::new(), - context_values: std::collections::BTreeMap::new(), - node_outcomes: std::collections::BTreeMap::new(), - next_node_id: None, - git_commit_sha: None, - loop_failure_signatures: std::collections::BTreeMap::new(), - restart_failure_signatures: std::collections::BTreeMap::new(), - node_visits: std::collections::BTreeMap::from([("work".into(), 2)]), - diff: Some("diff --git a/story.txt b/story.txt".into()), - }) + append_event( + &run, + &fixtures::RUN_1, + &Event::CheckpointCompleted { + node_id: "work".into(), + status: "success".into(), + current_node: "work".into(), + completed_nodes: Vec::new(), + node_retries: std::collections::BTreeMap::new(), + context_values: std::collections::BTreeMap::new(), + node_outcomes: std::collections::BTreeMap::new(), + next_node_id: None, + git_commit_sha: None, + loop_failure_signatures: std::collections::BTreeMap::new(), + restart_failure_signatures: std::collections::BTreeMap::new(), + node_visits: std::collections::BTreeMap::from([("work".into(), 2)]), + diff: Some("diff --git a/story.txt b/story.txt".into()), + }, + ) .await .unwrap(); diff --git a/lib/crates/fabro-workflow/src/graph.rs b/lib/crates/fabro-workflow/src/graph.rs index ce4aa4c54..5ccfed63d 100644 --- a/lib/crates/fabro-workflow/src/graph.rs +++ b/lib/crates/fabro-workflow/src/graph.rs @@ -3,7 +3,7 @@ mod routing; use std::collections::HashMap; use std::sync::Arc; -use fabro_core::error::{CoreError, Result as CoreResult}; +use fabro_core::error::{Error as CoreError, Result as CoreResult}; use fabro_core::graph::{EdgeSelection as CoreEdgeSelection, EdgeSpec, Graph, NodeSpec}; use fabro_graphviz::graph::types::{Edge as GvEdge, Graph as GvGraph, Node as GvNode}; @@ -114,7 +114,7 @@ impl Graph for WorkflowGraph { node.inner().selection(), ); selection.map(|sel| CoreEdgeSelection { - edge: WorkflowEdge(Arc::new(sel.edge.clone())), + edge: WorkflowEdge(Arc::new(sel.edge.clone())), reason: sel.reason, }) } diff --git a/lib/crates/fabro-workflow/src/graph/routing.rs b/lib/crates/fabro-workflow/src/graph/routing.rs index 58851bd8a..a96215569 100644 --- a/lib/crates/fabro-workflow/src/graph/routing.rs +++ b/lib/crates/fabro-workflow/src/graph/routing.rs @@ -9,7 +9,7 @@ use crate::outcome::{Outcome, StageStatus}; /// Result of edge selection: the chosen edge and the reason it was selected. pub(crate) struct SelectedGraphEdge<'a> { - pub(crate) edge: &'a GvEdge, + pub(crate) edge: &'a GvEdge, pub(crate) reason: &'static str, } diff --git a/lib/crates/fabro-workflow/src/handler/agent.rs b/lib/crates/fabro-workflow/src/handler/agent.rs index 86f3570d1..5f7359385 100644 --- a/lib/crates/fabro-workflow/src/handler/agent.rs +++ b/lib/crates/fabro-workflow/src/handler/agent.rs @@ -11,7 +11,7 @@ use fabro_types::RunId; use super::{EngineServices, Handler}; use crate::context::{Context, WorkflowContext, keys}; -use crate::error::FabroError; +use crate::error::Error; use crate::event::{Emitter, Event, StageScope}; use crate::outcome::{ BilledModelUsage, FailureCategory, FailureDetail, Outcome, OutcomeExt, StageStatus, @@ -20,9 +20,9 @@ use crate::outcome::{ /// Result from a `CodergenBackend` invocation. pub enum CodergenResult { Text { - text: String, - usage: Option, - files_touched: Vec, + text: String, + usage: Option, + files_touched: Vec, last_file_touched: Option, }, Full(Outcome), @@ -42,7 +42,7 @@ pub trait CodergenBackend: Send + Sync { emitter: &Arc, sandbox: &Arc, tool_hooks: Option>, - ) -> Result; + ) -> Result; /// Run a single LLM call with no tools (one_shot mode). async fn one_shot( @@ -50,8 +50,8 @@ pub trait CodergenBackend: Send + Sync { _node: &Node, _prompt: &str, _system_prompt: Option<&str>, - ) -> Result { - Err(FabroError::Validation( + ) -> Result { + Err(Error::Validation( "one_shot mode not supported by this backend".into(), )) } @@ -74,7 +74,7 @@ pub(crate) fn expand_variables( text: &str, graph: &Graph, inputs: &HashMap, -) -> Result { +) -> Result { let ctx = TemplateContext::new() .with_goal(graph.goal()) .with_inputs(inputs.clone()); @@ -224,7 +224,7 @@ impl Handler for AgentHandler { _graph: &Graph, _run_dir: &Path, _services: &EngineServices, - ) -> Result { + ) -> Result { Ok(simulate_llm_handler(node)) } @@ -235,7 +235,7 @@ impl Handler for AgentHandler { graph: &Graph, _run_dir: &Path, services: &EngineServices, - ) -> Result { + ) -> Result { // 1. Build prompt (prepend fidelity preamble if present) let raw_prompt = node .prompt() @@ -257,12 +257,12 @@ impl Handler for AgentHandler { let stage_scope = StageScope::for_handler(context, &node.id); services.emitter.emit_scoped( &Event::Prompt { - stage: node.id.clone(), - visit: stage_scope.visit, - text: prompt.clone(), - mode: Some("agent".to_string()), + stage: node.id.clone(), + visit: stage_scope.visit, + text: prompt.clone(), + mode: Some("agent".to_string()), provider: prompt_provider, - model: prompt_model, + model: prompt_model, }, &stage_scope, ); @@ -272,7 +272,7 @@ impl Handler for AgentHandler { let run_id = context .run_id() .parse::() - .map_err(|err| FabroError::handler(format!("invalid internal run_id: {err}")))?; + .map_err(|err| Error::handler(format!("invalid internal run_id: {err}")))?; let tool_hooks: Option> = services.hook_runner.as_ref().map(|hr| { Arc::new(fabro_hooks::WorkflowToolHookCallback { @@ -333,11 +333,11 @@ impl Handler for AgentHandler { .unwrap_or_default(); services.emitter.emit_scoped( &Event::PromptCompleted { - node_id: node.id.clone(), + node_id: node.id.clone(), response: response_text.clone(), - model: response_model, + model: response_model, provider: response_provider, - billing: stage_usage.clone(), + billing: stage_usage.clone(), }, &stage_scope, ); @@ -601,13 +601,12 @@ mod tests { _emitter: &Arc, _sandbox: &Arc, _tool_hooks: Option>, - ) -> Result { + ) -> Result { Ok(CodergenResult::Text { - text: - r#"Done. {"outcome": "success", "preferred_next_label": "approve"}"# - .to_string(), - usage: None, - files_touched: Vec::new(), + text: r#"Done. {"outcome": "success", "preferred_next_label": "approve"}"# + .to_string(), + usage: None, + files_touched: Vec::new(), last_file_touched: None, }) } @@ -658,11 +657,11 @@ mod tests { _emitter: &Arc, _sandbox: &Arc, _tool_hooks: Option>, - ) -> Result { + ) -> Result { Ok(CodergenResult::Text { - text: "Done writing results.".to_string(), - usage: None, - files_touched: vec!["results.md".to_string()], + text: "Done writing results.".to_string(), + usage: None, + files_touched: vec!["results.md".to_string()], last_file_touched: Some("results.md".to_string()), }) } @@ -716,25 +715,25 @@ mod tests { emitter: &Arc, _sandbox: &Arc, _tool_hooks: Option>, - ) -> Result { + ) -> Result { let scope = StageScope::for_handler(context, &node.id); emitter.emit_scoped( &crate::event::Event::Agent { - stage: node.id.clone(), - visit: scope.visit, - event: fabro_agent::AgentEvent::SessionStarted { + stage: node.id.clone(), + visit: scope.visit, + event: fabro_agent::AgentEvent::SessionStarted { provider: Some("openai".to_string()), - model: Some("gpt-5.4".to_string()), + model: Some("gpt-5.4".to_string()), }, - session_id: Some("session_123".to_string()), + session_id: Some("session_123".to_string()), parent_session_id: None, }, &scope, ); Ok(CodergenResult::Text { - text: "done".to_string(), - usage: None, - files_touched: Vec::new(), + text: "done".to_string(), + usage: None, + files_touched: Vec::new(), last_file_touched: None, }) } @@ -827,12 +826,12 @@ mod tests { _emitter: &Arc, _sandbox: &Arc, _tool_hooks: Option>, - ) -> Result { + ) -> Result { *self.captured_thread_id.lock().unwrap() = Some(thread_id.map(String::from)); Ok(CodergenResult::Text { - text: "ok".to_string(), - usage: None, - files_touched: Vec::new(), + text: "ok".to_string(), + usage: None, + files_touched: Vec::new(), last_file_touched: None, }) } @@ -879,12 +878,12 @@ mod tests { _emitter: &Arc, _sandbox: &Arc, _tool_hooks: Option>, - ) -> Result { + ) -> Result { *self.captured_thread_id.lock().unwrap() = Some(thread_id.map(String::from)); Ok(CodergenResult::Text { - text: "ok".to_string(), - usage: None, - files_touched: Vec::new(), + text: "ok".to_string(), + usage: None, + files_touched: Vec::new(), last_file_touched: None, }) } @@ -926,8 +925,8 @@ mod tests { _emitter: &Arc, _sandbox: &Arc, _tool_hooks: Option>, - ) -> Result { - Err(FabroError::handler("Request timed out".to_string())) + ) -> Result { + Err(Error::handler("Request timed out".to_string())) } } @@ -1069,8 +1068,8 @@ Some text in between. _emitter: &Arc, _sandbox: &Arc, _tool_hooks: Option>, - ) -> Result { - Err(FabroError::Validation("bad config".to_string())) + ) -> Result { + Err(Error::Validation("bad config".to_string())) } } @@ -1107,12 +1106,12 @@ Some text in between. _emitter: &Arc, _sandbox: &Arc, _tool_hooks: Option>, - ) -> Result { + ) -> Result { *self.captured_prompt.lock().unwrap() = Some(prompt.to_string()); Ok(CodergenResult::Text { - text: "ok".to_string(), - usage: None, - files_touched: Vec::new(), + text: "ok".to_string(), + usage: None, + files_touched: Vec::new(), last_file_touched: None, }) } @@ -1176,12 +1175,12 @@ Some text in between. _emitter: &Arc, _sandbox: &Arc, _tool_hooks: Option>, - ) -> Result { + ) -> Result { *self.captured_prompt.lock().unwrap() = Some(prompt.to_string()); Ok(CodergenResult::Text { - text: "ok".to_string(), - usage: None, - files_touched: Vec::new(), + text: "ok".to_string(), + usage: None, + files_touched: Vec::new(), last_file_touched: None, }) } diff --git a/lib/crates/fabro-workflow/src/handler/command.rs b/lib/crates/fabro-workflow/src/handler/command.rs index 39966568d..92cfe83f1 100644 --- a/lib/crates/fabro-workflow/src/handler/command.rs +++ b/lib/crates/fabro-workflow/src/handler/command.rs @@ -5,7 +5,7 @@ use fabro_graphviz::graph::{Graph, Node}; use super::{EngineServices, Handler}; use crate::context::{Context, keys}; -use crate::error::FabroError; +use crate::error::Error; use crate::event::{Event, StageScope}; use crate::outcome::{Outcome, OutcomeExt}; @@ -34,7 +34,7 @@ impl Handler for CommandHandler { _graph: &Graph, _run_dir: &Path, _services: &EngineServices, - ) -> Result { + ) -> Result { let script = node .attrs .get("script") @@ -60,7 +60,7 @@ impl Handler for CommandHandler { _graph: &Graph, _run_dir: &Path, services: &EngineServices, - ) -> Result { + ) -> Result { let script = node .attrs .get("script") @@ -92,10 +92,10 @@ impl Handler for CommandHandler { let stage_scope = StageScope::for_handler(context, &node.id); services.emitter.emit_scoped( &Event::CommandStarted { - node_id: node.id.clone(), - script: script.to_string(), - command: command.clone(), - language: language.to_string(), + node_id: node.id.clone(), + script: script.to_string(), + command: command.clone(), + language: language.to_string(), timeout_ms: timeout_ms(node), }, &stage_scope, @@ -118,23 +118,22 @@ impl Handler for CommandHandler { if let Some(token) = cancel_token { token.cancel(); } - let result = - result.map_err(|e| FabroError::handler(format!("Failed to spawn script: {e}")))?; + let result = result.map_err(|e| Error::handler(format!("Failed to spawn script: {e}")))?; services.emitter.emit_scoped( &Event::CommandCompleted { - node_id: node.id.clone(), - stdout: result.stdout.clone(), - stderr: result.stderr.clone(), - exit_code: (!result.timed_out).then_some(result.exit_code), + node_id: node.id.clone(), + stdout: result.stdout.clone(), + stderr: result.stderr.clone(), + exit_code: (!result.timed_out).then_some(result.exit_code), duration_ms: result.duration_ms, - timed_out: result.timed_out, + timed_out: result.timed_out, }, &stage_scope, ); if result.timed_out { - return Err(FabroError::handler(format!( + return Err(Error::handler(format!( "Script timed out after {timeout_ms}ms: {script}", ))); } @@ -713,9 +712,9 @@ mod tests { /// proving that `CommandHandler` delegates to the sandbox rather than /// spawning a host process. struct SpySandbox { - exec_result: fabro_agent::sandbox::ExecResult, - captured_command: std::sync::Mutex>, - captured_env_vars: std::sync::Mutex>>, + exec_result: fabro_agent::sandbox::ExecResult, + captured_command: std::sync::Mutex>, + captured_env_vars: std::sync::Mutex>>, captured_cancel_token: std::sync::Mutex>, } @@ -816,10 +815,10 @@ mod tests { #[tokio::test] async fn executes_script_via_sandbox() { let spy = std::sync::Arc::new(SpySandbox::new(fabro_agent::sandbox::ExecResult { - stdout: "SANDBOX_MARKER\n".into(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: "SANDBOX_MARKER\n".into(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 5, })); @@ -861,10 +860,10 @@ mod tests { #[tokio::test] async fn executes_python_script_via_sandbox() { let spy = std::sync::Arc::new(SpySandbox::new(fabro_agent::sandbox::ExecResult { - stdout: "PYTHON_SANDBOX\n".into(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: "PYTHON_SANDBOX\n".into(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 5, })); @@ -904,10 +903,10 @@ mod tests { #[tokio::test] async fn passes_env_vars_to_sandbox() { let spy = std::sync::Arc::new(SpySandbox::new(fabro_agent::sandbox::ExecResult { - stdout: String::new(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: String::new(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 5, })); @@ -939,10 +938,10 @@ mod tests { #[tokio::test] async fn passes_run_cancellation_to_sandbox() { let spy = std::sync::Arc::new(SpySandbox::new(fabro_agent::sandbox::ExecResult { - stdout: String::new(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: String::new(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 5, })); @@ -1032,7 +1031,7 @@ mod tests { // // Pragmatic approach: verify the error construction matches what the // handler produces. The timeout test covers the other Err path. - let err = FabroError::handler(format!("Failed to spawn script: {}", "No such file")); + let err = Error::handler(format!("Failed to spawn script: {}", "No such file")); assert!(err.to_string().contains("Failed to spawn script")); } diff --git a/lib/crates/fabro-workflow/src/handler/conditional.rs b/lib/crates/fabro-workflow/src/handler/conditional.rs index df6b03ea1..71e1960c2 100644 --- a/lib/crates/fabro-workflow/src/handler/conditional.rs +++ b/lib/crates/fabro-workflow/src/handler/conditional.rs @@ -5,7 +5,7 @@ use fabro_graphviz::graph::{Graph, Node}; use super::{EngineServices, Handler}; use crate::context::Context; -use crate::error::FabroError; +use crate::error::Error; use crate::outcome::Outcome; /// Conditional routing handler. Returns SUCCESS with a note; actual routing @@ -21,7 +21,7 @@ impl Handler for ConditionalHandler { _graph: &Graph, _run_dir: &Path, _services: &EngineServices, - ) -> Result { + ) -> Result { let mut outcome = Outcome::success(); outcome.notes = Some(format!("Conditional node evaluated: {}", node.id)); Ok(outcome) diff --git a/lib/crates/fabro-workflow/src/handler/exit.rs b/lib/crates/fabro-workflow/src/handler/exit.rs index 0b600913b..5d5bc59a3 100644 --- a/lib/crates/fabro-workflow/src/handler/exit.rs +++ b/lib/crates/fabro-workflow/src/handler/exit.rs @@ -5,7 +5,7 @@ use fabro_graphviz::graph::{Graph, Node}; use super::{EngineServices, Handler}; use crate::context::Context; -use crate::error::FabroError; +use crate::error::Error; use crate::outcome::Outcome; /// No-op handler for pipeline exit point. Returns SUCCESS immediately. @@ -20,7 +20,7 @@ impl Handler for ExitHandler { _graph: &Graph, _run_dir: &Path, _services: &EngineServices, - ) -> Result { + ) -> Result { Ok(Outcome::success()) } } diff --git a/lib/crates/fabro-workflow/src/handler/fan_in.rs b/lib/crates/fabro-workflow/src/handler/fan_in.rs index 32f7b088e..1689620c9 100644 --- a/lib/crates/fabro-workflow/src/handler/fan_in.rs +++ b/lib/crates/fabro-workflow/src/handler/fan_in.rs @@ -8,7 +8,7 @@ use fabro_graphviz::graph::{Graph, Node}; use super::agent::{CodergenBackend, CodergenResult}; use super::{EngineServices, Handler}; use crate::context::{Context, keys}; -use crate::error::FabroError; +use crate::error::Error; use crate::event::{Emitter, Event, StageScope}; use crate::outcome::{Outcome, OutcomeExt}; use crate::sandbox_git::git_merge_ff_only; @@ -35,7 +35,7 @@ impl Handler for FanInHandler { _graph: &Graph, _run_dir: &Path, _services: &EngineServices, - ) -> Result { + ) -> Result { let results = context.get(keys::PARALLEL_RESULTS); let Some(results) = results else { return Ok(Outcome::fail_deterministic( @@ -66,7 +66,7 @@ impl Handler for FanInHandler { _graph: &Graph, run_dir: &Path, services: &EngineServices, - ) -> Result { + ) -> Result { let results = context.get(keys::PARALLEL_RESULTS); let Some(results) = results else { return Ok(Outcome::fail_deterministic( @@ -141,9 +141,9 @@ impl Handler for FanInHandler { } struct Candidate { - id: String, + id: String, status: String, - score: f64, + score: f64, } fn status_rank(status: &str) -> u32 { @@ -161,16 +161,16 @@ fn heuristic_select(results: &serde_json::Value) -> Candidate { let arr = results.as_array().unwrap_or(&empty_vec); if arr.is_empty() { return Candidate { - id: "unknown".to_string(), + id: "unknown".to_string(), status: "fail".to_string(), - score: 0.0, + score: 0.0, }; } let mut candidates: Vec = arr .iter() .map(|v| Candidate { - id: v + id: v .get("id") .and_then(|v| v.as_str()) .unwrap_or("unknown") @@ -180,7 +180,7 @@ fn heuristic_select(results: &serde_json::Value) -> Candidate { .and_then(|v| v.as_str()) .unwrap_or("fail") .to_string(), - score: v + score: v .get("score") .and_then(serde_json::Value::as_f64) .unwrap_or(0.0), @@ -204,9 +204,9 @@ fn heuristic_select(results: &serde_json::Value) -> Candidate { }); candidates.into_iter().next().unwrap_or_else(|| Candidate { - id: "unknown".to_string(), + id: "unknown".to_string(), status: "fail".to_string(), - score: 0.0, + score: 0.0, }) } @@ -221,7 +221,7 @@ async fn llm_evaluate( node_id: &str, emitter: &Arc, sandbox: &Arc, -) -> Result { +) -> Result { let results_text = serde_json::to_string_pretty(results).unwrap_or_else(|_| results.to_string()); @@ -234,12 +234,12 @@ async fn llm_evaluate( emitter.emit_scoped( &Event::Prompt { - stage: node_id.to_string(), - visit: stage_scope.visit, - text: full_prompt.clone(), - mode: Some("fan_in".to_string()), + stage: node_id.to_string(), + visit: stage_scope.visit, + text: full_prompt.clone(), + mode: Some("fan_in".to_string()), provider: None, - model: None, + model: None, }, &stage_scope, ); @@ -273,28 +273,28 @@ async fn llm_evaluate( serde_json::to_string_pretty(&outcome).unwrap_or_else(|_| "{}".to_string()); emitter.emit_scoped( &Event::PromptCompleted { - node_id: node_id.to_string(), + node_id: node_id.to_string(), response: response_text.clone(), - model: String::new(), + model: String::new(), provider: String::new(), - billing: None, + billing: None, }, &stage_scope, ); Ok(Candidate { - id: best_id, + id: best_id, status: outcome.status.to_string(), - score: 0.0, + score: 0.0, }) } Ok(CodergenResult::Text { text, .. }) => { emitter.emit_scoped( &Event::PromptCompleted { - node_id: node_id.to_string(), + node_id: node_id.to_string(), response: text.clone(), - model: String::new(), + model: String::new(), provider: String::new(), - billing: None, + billing: None, }, &stage_scope, ); @@ -470,12 +470,12 @@ mod tests { _emitter: &Arc, _sandbox: &Arc, _tool_hooks: Option>, - ) -> Result { + ) -> Result { // Return text that contains the ID "branch_b" Ok(CodergenResult::Text { - text: "The best candidate is branch_b".to_string(), - usage: None, - files_touched: Vec::new(), + text: "The best candidate is branch_b".to_string(), + usage: None, + files_touched: Vec::new(), last_file_touched: None, }) } diff --git a/lib/crates/fabro-workflow/src/handler/human.rs b/lib/crates/fabro-workflow/src/handler/human.rs index d75275343..d0491a252 100644 --- a/lib/crates/fabro-workflow/src/handler/human.rs +++ b/lib/crates/fabro-workflow/src/handler/human.rs @@ -11,16 +11,16 @@ use ulid::Ulid; use super::{EngineServices, Handler}; use crate::context::{Context, keys}; -use crate::error::FabroError; +use crate::error::Error; use crate::event::{Emitter, Event, StageScope}; use crate::millis_u64; use crate::outcome::{Outcome, OutcomeExt}; /// A choice derived from an outgoing edge. struct Choice { - key: String, + key: String, label: String, - to: String, + to: String, } /// Parse an accelerator key from a label. @@ -69,7 +69,7 @@ fn parse_accelerator_key(label: &str) -> String { /// Blocks until a human selects an option derived from outgoing edges. pub struct HumanHandler { interviewer: Arc, - emitter: Option>, + emitter: Option>, } impl HumanHandler { @@ -103,7 +103,7 @@ impl Handler for HumanHandler { graph: &Graph, _run_dir: &Path, _services: &EngineServices, - ) -> Result { + ) -> Result { let edges = graph.outgoing_edges(&node.id); let first_choice = edges.iter().find(|e| !e.freeform()); @@ -146,7 +146,7 @@ impl Handler for HumanHandler { graph: &Graph, _run_dir: &Path, services: &EngineServices, - ) -> Result { + ) -> Result { // 1. Derive choices from outgoing edges let edges = graph.outgoing_edges(&node.id); let mut freeform_target: Option = None; @@ -176,7 +176,7 @@ impl Handler for HumanHandler { let options: Vec = choices .iter() .map(|c| QuestionOption { - key: c.key.clone(), + key: c.key.clone(), label: c.label.clone(), }) .collect(); @@ -211,19 +211,19 @@ impl Handler for HumanHandler { self.emit( &services.emitter, &Event::InterviewStarted { - question_id: question_id.clone(), - question: question_text.clone(), - stage: node.id.clone(), - question_type: question.question_type.to_string(), - options: question + question_id: question_id.clone(), + question: question_text.clone(), + stage: node.id.clone(), + question_type: question.question_type.to_string(), + options: question .options .iter() .map(|option| InterviewOption { - key: option.key.clone(), + key: option.key.clone(), label: option.label.clone(), }) .collect(), - allow_freeform: question.allow_freeform, + allow_freeform: question.allow_freeform, timeout_seconds: question.timeout_seconds, context_display: question.context_display.clone(), }, @@ -238,8 +238,8 @@ impl Handler for HumanHandler { &services.emitter, &Event::InterviewTimeout { question_id: question_id.clone(), - question: question_text, - stage: node.id.clone(), + question: question_text, + stage: node.id.clone(), duration_ms: millis_u64(interview_start.elapsed()), }, &stage_scope, @@ -259,7 +259,7 @@ impl Handler for HumanHandler { } if answer.value == AnswerValue::Cancelled { - return Err(FabroError::Cancelled); + return Err(Error::Cancelled); } // 5. Handle unanswered / interrupted interview sessions. @@ -269,15 +269,15 @@ impl Handler for HumanHandler { .as_ref() .is_some_and(|flag| flag.load(Ordering::SeqCst)) { - return Err(FabroError::Cancelled); + return Err(Error::Cancelled); } self.emit( &services.emitter, &Event::InterviewInterrupted { question_id: question_id.clone(), - question: question_text, - stage: node.id.clone(), - reason: "interrupted".to_string(), + question: question_text, + stage: node.id.clone(), + reason: "interrupted".to_string(), duration_ms: millis_u64(interview_start.elapsed()), }, &stage_scope, @@ -560,7 +560,7 @@ mod tests { .await .unwrap_err(); - assert!(matches!(error, FabroError::Cancelled)); + assert!(matches!(error, Error::Cancelled)); } #[tokio::test] diff --git a/lib/crates/fabro-workflow/src/handler/llm/api.rs b/lib/crates/fabro-workflow/src/handler/llm/api.rs index 68f66fd49..555a93043 100644 --- a/lib/crates/fabro-workflow/src/handler/llm/api.rs +++ b/lib/crates/fabro-workflow/src/handler/llm/api.rs @@ -17,7 +17,7 @@ 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::error::Error; use crate::event::{Emitter, Event, StageScope}; use crate::outcome::billed_model_usage_from_llm; @@ -41,7 +41,7 @@ struct FileTracking { /// Set of all file paths successfully written/edited. touched: HashSet, /// Most recently modified file path. - last: Option, + last: Option, } fn track_file_event(event: &AgentEvent, state: &mut FileTracking) { @@ -98,10 +98,10 @@ fn spawn_event_forwarder( { emitter.emit_scoped( &Event::Agent { - stage: node_id.clone(), - visit: scope.visit, - event: event.event.clone(), - session_id: Some(event.session_id.clone()), + stage: node_id.clone(), + visit: scope.visit, + event: event.event.clone(), + session_id: Some(event.session_id.clone()), parent_session_id: event.parent_session_id.clone(), }, &scope, @@ -116,12 +116,12 @@ fn spawn_event_forwarder( /// For `full` fidelity nodes sharing a thread key, sessions are cached /// and reused so the LLM sees the full conversation history. pub struct AgentApiBackend { - model: String, - provider: Provider, + model: String, + provider: Provider, fallback_chain: Vec, - sessions: Mutex>, - env: HashMap, - mcp_servers: Vec, + sessions: Mutex>, + env: HashMap, + mcp_servers: Vec, } impl AgentApiBackend { @@ -154,7 +154,7 @@ impl AgentApiBackend { node: &Node, sandbox: &Arc, tool_hooks: Option>, - ) -> Result { + ) -> Result { let model = node.model().unwrap_or(&self.model); let provider = node .provider() @@ -180,10 +180,10 @@ impl AgentApiBackend { env: &HashMap, tool_hooks: Option>, mcp_servers: Vec, - ) -> Result { + ) -> Result { let client = Client::from_env() .await - .map_err(|e| FabroError::handler(format!("Failed to create LLM client: {e}")))?; + .map_err(|e| Error::handler(format!("Failed to create LLM client: {e}")))?; let mut profile = build_profile(model, provider); @@ -263,10 +263,10 @@ impl CodergenBackend for AgentApiBackend { node: &Node, prompt: &str, system_prompt: Option<&str>, - ) -> Result { + ) -> Result { let client = Client::from_env() .await - .map_err(|e| FabroError::handler(format!("Failed to create LLM client: {e}")))?; + .map_err(|e| Error::handler(format!("Failed to create LLM client: {e}")))?; let model = node.model().unwrap_or(&self.model); let provider = node @@ -367,16 +367,16 @@ impl CodergenBackend for AgentApiBackend { Err(err) if err.failover_eligible() => { last_err = err; } - Err(err) => return Err(FabroError::Llm(err)), + Err(err) => return Err(Error::Llm(err)), } } match found { Some(triple) => triple, - None => return Err(FabroError::Llm(last_err)), + None => return Err(Error::Llm(last_err)), } } - Err(sdk_err) => return Err(FabroError::Llm(sdk_err)), + Err(sdk_err) => return Err(Error::Llm(sdk_err)), }; let actual_provider = actual_provider.parse::().unwrap_or(self.provider); @@ -388,9 +388,9 @@ impl CodergenBackend for AgentApiBackend { ); Ok(CodergenResult::Text { - text: response.text(), - usage: Some(stage_usage), - files_touched: Vec::new(), + text: response.text(), + usage: Some(stage_usage), + files_touched: Vec::new(), last_file_touched: None, }) } @@ -404,7 +404,7 @@ impl CodergenBackend for AgentApiBackend { emitter: &Arc, sandbox: &Arc, tool_hooks: Option>, - ) -> Result { + ) -> Result { let actual_model = node.model().unwrap_or(&self.model).to_string(); let _actual_provider = node .provider() @@ -449,7 +449,7 @@ impl CodergenBackend for AgentApiBackend { let file_tracking = Arc::new(Mutex::new(FileTracking { pending: HashMap::new(), touched: HashSet::new(), - last: None, + last: None, })); let stage_scope = StageScope::for_handler(context, &node.id); @@ -474,25 +474,25 @@ impl CodergenBackend for AgentApiBackend { // On failover-eligible error, try fallback providers. let result = match result { Ok(()) => Ok(()), - Err(fabro_agent::AgentError::Llm(ref sdk_err)) + Err(fabro_agent::Error::Llm(ref sdk_err)) if sdk_err.failover_eligible() && !self.fallback_chain.is_empty() => { let error_msg = sdk_err.to_string(); let from_provider = self.provider.as_str().to_string(); let from_model = self.model.clone(); - let mut last_err = FabroError::Llm(sdk_err.clone()); + let mut last_err = Error::Llm(sdk_err.clone()); let mut succeeded = false; for target in &self.fallback_chain { emitter.emit_scoped( &Event::Failover { - stage: node.id.clone(), + stage: node.id.clone(), from_provider: from_provider.clone(), - from_model: from_model.clone(), - to_provider: target.provider.clone(), - to_model: target.model.clone(), - error: error_msg.clone(), + from_model: from_model.clone(), + to_provider: target.provider.clone(), + to_model: target.model.clone(), + error: error_msg.clone(), }, &stage_scope, ); @@ -536,28 +536,24 @@ impl CodergenBackend for AgentApiBackend { succeeded = true; break; } - Err(fabro_agent::AgentError::Llm(err)) if err.failover_eligible() => { - last_err = FabroError::Llm(err); + Err(fabro_agent::Error::Llm(err)) if err.failover_eligible() => { + last_err = Error::Llm(err); } - Err(fabro_agent::AgentError::Llm(err)) => return Err(FabroError::Llm(err)), - Err(fabro_agent::AgentError::Interrupted(_)) => { - return Err(FabroError::Cancelled); + Err(fabro_agent::Error::Llm(err)) => return Err(Error::Llm(err)), + Err(fabro_agent::Error::Interrupted(_)) => { + return Err(Error::Cancelled); } Err(other) => { - return Err(FabroError::handler(format!( - "Agent session failed: {other}" - ))); + return Err(Error::handler(format!("Agent session failed: {other}"))); } } } if succeeded { Ok(()) } else { Err(last_err) } } - Err(fabro_agent::AgentError::Llm(sdk_err)) => Err(FabroError::Llm(sdk_err)), - Err(fabro_agent::AgentError::Interrupted(_)) => Err(FabroError::Cancelled), - Err(other) => Err(FabroError::handler(format!( - "Agent session failed: {other}" - ))), + Err(fabro_agent::Error::Llm(sdk_err)) => Err(Error::Llm(sdk_err)), + Err(fabro_agent::Error::Interrupted(_)) => Err(Error::Cancelled), + Err(other) => Err(Error::handler(format!("Agent session failed: {other}"))), }; // On error, drop the session (don't cache failed state). @@ -645,7 +641,7 @@ mod tests { FileTracking { pending: HashMap::new(), touched: HashSet::new(), - last: None, + last: None, } } @@ -661,9 +657,9 @@ mod tests { track_file_event( &AgentEvent::ToolCallStarted { - tool_name: "write_file".to_string(), + tool_name: "write_file".to_string(), tool_call_id: "tc1".to_string(), - arguments: serde_json::Value::Object(args), + arguments: serde_json::Value::Object(args), }, &mut state, ); @@ -672,9 +668,9 @@ mod tests { track_file_event( &AgentEvent::ToolCallCompleted { tool_call_id: "tc1".to_string(), - tool_name: "write_file".to_string(), - is_error: false, - output: serde_json::Value::String("ok".to_string()), + tool_name: "write_file".to_string(), + is_error: false, + output: serde_json::Value::String("ok".to_string()), }, &mut state, ); @@ -694,9 +690,9 @@ mod tests { track_file_event( &AgentEvent::ToolCallStarted { - tool_name: "edit_file".to_string(), + tool_name: "edit_file".to_string(), tool_call_id: "tc-sub".to_string(), - arguments: serde_json::Value::Object(args), + arguments: serde_json::Value::Object(args), }, &mut state, ); @@ -705,9 +701,9 @@ mod tests { track_file_event( &AgentEvent::ToolCallCompleted { tool_call_id: "tc-sub".to_string(), - tool_name: "edit_file".to_string(), - is_error: false, - output: serde_json::Value::String("ok".to_string()), + tool_name: "edit_file".to_string(), + is_error: false, + output: serde_json::Value::String("ok".to_string()), }, &mut state, ); @@ -727,9 +723,9 @@ mod tests { track_file_event( &AgentEvent::ToolCallStarted { - tool_name: "edit_file".to_string(), + tool_name: "edit_file".to_string(), tool_call_id: "tc-err".to_string(), - arguments: serde_json::Value::Object(args), + arguments: serde_json::Value::Object(args), }, &mut state, ); @@ -737,9 +733,9 @@ mod tests { track_file_event( &AgentEvent::ToolCallCompleted { tool_call_id: "tc-err".to_string(), - tool_name: "edit_file".to_string(), - is_error: true, - output: serde_json::Value::String("failed".to_string()), + tool_name: "edit_file".to_string(), + is_error: true, + output: serde_json::Value::String("failed".to_string()), }, &mut state, ); diff --git a/lib/crates/fabro-workflow/src/handler/llm/cli.rs b/lib/crates/fabro-workflow/src/handler/llm/cli.rs index a6dd9e394..3d277ce22 100644 --- a/lib/crates/fabro-workflow/src/handler/llm/cli.rs +++ b/lib/crates/fabro-workflow/src/handler/llm/cli.rs @@ -11,7 +11,7 @@ use tokio::time::sleep; use super::super::agent::{CodergenBackend, CodergenResult}; use crate::context::Context; -use crate::error::FabroError; +use crate::error::Error; use crate::event::{Emitter, Event, StageScope}; use crate::outcome::billed_model_usage_from_llm; @@ -63,7 +63,7 @@ async fn ensure_cli( provider: Provider, sandbox: &Arc, emitter: &Arc, -) -> Result<(), FabroError> { +) -> Result<(), Error> { let start = std::time::Instant::now(); let cli_name = cli.name(); let provider_str = provider.as_str(); @@ -84,7 +84,7 @@ async fn ensure_cli( None, ) .await - .map_err(|e| FabroError::handler(format!("Failed to check {cli_name} version: {e}")))?; + .map_err(|e| Error::handler(format!("Failed to check {cli_name} version: {e}")))?; if version_check.exit_code == 0 { let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX); @@ -109,7 +109,7 @@ async fn ensure_cli( let install_result = sandbox .exec_command(&install_cmd, 180_000, None, None, None) .await - .map_err(|e| FabroError::handler(format!("Failed to install {cli_name}: {e}")))?; + .map_err(|e| Error::handler(format!("Failed to install {cli_name}: {e}")))?; let node_installed = true; if install_result.exit_code != 0 { @@ -137,7 +137,7 @@ async fn ensure_cli( error: error_msg.clone(), duration_ms, }); - return Err(FabroError::handler(error_msg)); + return Err(Error::handler(error_msg)); } let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX); @@ -210,8 +210,8 @@ pub fn cli_command_for_provider(provider: Provider, model: &str, prompt_file: &s /// Parsed response from a CLI tool invocation. #[derive(Debug)] pub struct CliResponse { - pub text: String, - pub input_tokens: i64, + pub text: String, + pub input_tokens: i64, pub output_tokens: i64, } @@ -376,9 +376,9 @@ fn shell_escape(val: &str) -> String { /// CLI backend that invokes external CLI tools (claude, codex, gemini) via /// `exec_command()`. pub struct AgentCliBackend { - model: String, - provider: Provider, - env: HashMap, + model: String, + provider: Provider, + env: HashMap, poll_interval: std::time::Duration, } @@ -467,7 +467,7 @@ impl CodergenBackend for AgentCliBackend { emitter: &Arc, sandbox: &Arc, _tool_hooks: Option>, - ) -> Result { + ) -> Result { // 1. Snapshot git state before the CLI run let files_before = self.detect_changed_files(sandbox).await; @@ -483,7 +483,7 @@ impl CodergenBackend for AgentCliBackend { sandbox .write_file(&prompt_path, prompt) .await - .map_err(|e| FabroError::handler(format!("Failed to write prompt file: {e}")))?; + .map_err(|e| Error::handler(format!("Failed to write prompt file: {e}")))?; // 3. Build CLI command let model = node.model().unwrap_or(&self.model); @@ -500,12 +500,12 @@ impl CodergenBackend for AgentCliBackend { let stage_scope = StageScope::for_handler(context, &node.id); emitter.emit_scoped( &Event::AgentCliStarted { - node_id: node.id.clone(), - visit: stage_scope.visit, - mode: "cli".to_string(), + node_id: node.id.clone(), + visit: stage_scope.visit, + mode: "cli".to_string(), provider: provider.as_str().to_string(), - model: model.to_string(), - command: command.clone(), + model: model.to_string(), + command: command.clone(), }, &stage_scope, ); @@ -537,7 +537,7 @@ impl CodergenBackend for AgentCliBackend { let login_result = sandbox .exec_command(&login_cmd, 30_000, None, None, None) .await - .map_err(|e| FabroError::handler(format!("codex login failed: {e}")))?; + .map_err(|e| Error::handler(format!("codex login failed: {e}")))?; if login_result.exit_code != 0 { tracing::warn!( exit_code = login_result.exit_code, @@ -560,7 +560,7 @@ impl CodergenBackend for AgentCliBackend { sandbox .write_file(&env_path, &env_lines.join("\n")) .await - .map_err(|e| FabroError::handler(format!("Failed to write env file: {e}")))?; + .map_err(|e| Error::handler(format!("Failed to write env file: {e}")))?; } // 3a. Disable auto-stop so the sandbox stays alive during long CLI runs @@ -587,7 +587,7 @@ impl CodergenBackend for AgentCliBackend { let launch_result = sandbox .exec_command(&bg_command, 30_000, None, launch_env_ref, None) .await - .map_err(|e| FabroError::handler(format!("Failed to launch CLI command: {e}")))?; + .map_err(|e| Error::handler(format!("Failed to launch CLI command: {e}")))?; let pid = launch_result.stdout.trim(); tracing::info!(pid, "CLI process launched in background"); @@ -601,7 +601,7 @@ impl CodergenBackend for AgentCliBackend { let poll_result = sandbox .exec_command(&poll_command, 30_000, None, None, None) .await - .map_err(|e| FabroError::handler(format!("Failed to poll CLI command: {e}")))?; + .map_err(|e| Error::handler(format!("Failed to poll CLI command: {e}")))?; let status = poll_result.stdout.trim(); if status != "running" { @@ -614,11 +614,11 @@ impl CodergenBackend for AgentCliBackend { let stdout_result = sandbox .exec_command(&format!("cat {stdout_path}"), 60_000, None, None, None) .await - .map_err(|e| FabroError::handler(format!("Failed to read stdout: {e}")))?; + .map_err(|e| Error::handler(format!("Failed to read stdout: {e}")))?; let stderr_result = sandbox .exec_command(&format!("cat {stderr_path}"), 60_000, None, None, None) .await - .map_err(|e| FabroError::handler(format!("Failed to read stderr: {e}")))?; + .map_err(|e| Error::handler(format!("Failed to read stderr: {e}")))?; let result = ExecResult { stdout: stdout_result.stdout, @@ -629,10 +629,10 @@ impl CodergenBackend for AgentCliBackend { }; emitter.emit_scoped( &Event::AgentCliCompleted { - node_id: node.id.clone(), - stdout: result.stdout.clone(), - stderr: result.stderr.clone(), - exit_code: result.exit_code, + node_id: node.id.clone(), + stdout: result.stdout.clone(), + stderr: result.stderr.clone(), + exit_code: result.exit_code, duration_ms: result.duration_ms, }, &stage_scope, @@ -661,7 +661,7 @@ impl CodergenBackend for AgentCliBackend { (true, false) => format!("stdout: {stdout_tail}"), (true, true) => format!("command: {command}"), }; - return Err(FabroError::handler(format!( + return Err(Error::handler(format!( "CLI command exited with code {}: {detail}", result.exit_code, ))); @@ -669,7 +669,7 @@ impl CodergenBackend for AgentCliBackend { // 4. Parse the CLI output let parsed = parse_cli_response(provider, &result.stdout) - .ok_or_else(|| FabroError::handler("Failed to parse CLI output".to_string()))?; + .ok_or_else(|| Error::handler("Failed to parse CLI output".to_string()))?; // 5. Detect changed files let files_after = self.detect_changed_files(sandbox).await; @@ -699,12 +699,16 @@ impl CodergenBackend for AgentCliBackend { } }; - let stage_usage = - billed_model_usage_from_llm(model, provider, node.speed(), &TokenCounts { + let stage_usage = billed_model_usage_from_llm( + model, + provider, + node.speed(), + &TokenCounts { input_tokens: parsed.input_tokens, output_tokens: parsed.output_tokens, ..TokenCounts::default() - }); + }, + ); Ok(CodergenResult::Text { text: parsed.text, @@ -760,7 +764,7 @@ impl CodergenBackend for BackendRouter { emitter: &Arc, sandbox: &Arc, tool_hooks: Option>, - ) -> Result { + ) -> Result { if self.should_use_cli(node) { self.cli_backend .run( @@ -781,7 +785,7 @@ impl CodergenBackend for BackendRouter { node: &Node, prompt: &str, system_prompt: Option<&str>, - ) -> Result { + ) -> Result { // CLI backend doesn't support one_shot, always route to API self.api_backend.one_shot(node, prompt, system_prompt).await } @@ -834,7 +838,7 @@ mod tests { /// Mock sandbox that returns pre-configured ExecResults in FIFO order. struct CliMockSandbox { - results: Mutex>, + results: Mutex>, commands: Arc>>, } @@ -927,20 +931,20 @@ mod tests { fn ok_result() -> ExecResult { ExecResult { - exit_code: 0, - stdout: String::new(), - stderr: String::new(), - timed_out: false, + exit_code: 0, + stdout: String::new(), + stderr: String::new(), + timed_out: false, duration_ms: 10, } } fn fail_result(code: i32) -> ExecResult { ExecResult { - exit_code: code, - stdout: String::new(), - stderr: "error".to_string(), - timed_out: false, + exit_code: code, + stdout: String::new(), + stderr: "error".to_string(), + timed_out: false, duration_ms: 10, } } @@ -1202,11 +1206,11 @@ mod tests { _emitter: &Arc, _sandbox: &Arc, _tool_hooks: Option>, - ) -> Result { + ) -> Result { Ok(CodergenResult::Text { - text: "stub".to_string(), - usage: None, - files_touched: Vec::new(), + text: "stub".to_string(), + usage: None, + files_touched: Vec::new(), last_file_touched: None, }) } diff --git a/lib/crates/fabro-workflow/src/handler/llm/preamble.rs b/lib/crates/fabro-workflow/src/handler/llm/preamble.rs index b8d8ab7d6..e985cb805 100644 --- a/lib/crates/fabro-workflow/src/handler/llm/preamble.rs +++ b/lib/crates/fabro-workflow/src/handler/llm/preamble.rs @@ -624,11 +624,16 @@ mod tests { use crate::outcome::{BilledModelUsage, billed_model_usage_from_llm}; fn stage_usage(model: &str, input_tokens: i64, output_tokens: i64) -> BilledModelUsage { - billed_model_usage_from_llm(model, Provider::Anthropic, None, &TokenCounts { - input_tokens, - output_tokens, - ..TokenCounts::default() - }) + billed_model_usage_from_llm( + model, + Provider::Anthropic, + None, + &TokenCounts { + input_tokens, + output_tokens, + ..TokenCounts::default() + }, + ) } // --- truncate mode --- diff --git a/lib/crates/fabro-workflow/src/handler/manager_loop.rs b/lib/crates/fabro-workflow/src/handler/manager_loop.rs index db886e820..5aff1304d 100644 --- a/lib/crates/fabro-workflow/src/handler/manager_loop.rs +++ b/lib/crates/fabro-workflow/src/handler/manager_loop.rs @@ -15,7 +15,7 @@ use super::{EngineServices, Handler}; use crate::artifact_upload::ArtifactSink; use crate::condition::evaluate_condition; use crate::context::{Context, WorkflowContext, keys}; -use crate::error::FabroError; +use crate::error::Error; use crate::operations::{ValidateInput, WorkflowInput, validate}; use crate::outcome::{Outcome, OutcomeExt, StageStatus}; use crate::pipeline; @@ -28,7 +28,7 @@ use crate::run_options::RunOptions; pub struct SubWorkflowHandler; struct ParsedChildWorkflow { - graph: Graph, + graph: Graph, workflow_path: Option, } @@ -59,10 +59,7 @@ fn parse_duration_str(s: &str) -> Duration { /// `stack.child_workflow` / `stack.child_dotfile` (with file inlining). /// `stack.child_workflow` is preferred; `stack.child_dotfile` is kept for /// backward compatibility. -fn parse_child_graph( - node: &Node, - services: &EngineServices, -) -> Result { +fn parse_child_graph(node: &Node, services: &EngineServices) -> Result { let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); if let Some(dot) = node @@ -71,12 +68,12 @@ fn parse_child_graph( .and_then(|v| v.as_str()) { let validated = validate(ValidateInput { - workflow: WorkflowInput::DotSource { - source: dot.to_string(), + workflow: WorkflowInput::DotSource { + source: dot.to_string(), base_dir: None, }, - settings: SettingsLayer::default(), - cwd: cwd.clone(), + settings: SettingsLayer::default(), + cwd: cwd.clone(), custom_transforms: Vec::new(), })?; validated.raise_on_errors()?; @@ -98,13 +95,13 @@ fn parse_child_graph( .resolve_child(current_workflow_path, path) .cloned() .ok_or_else(|| { - FabroError::handler(format!( + Error::handler(format!( "child workflow is not present in the persisted bundle: {path}" )) })?, ), (Some(_), None) => { - return Err(FabroError::engine( + return Err(Error::engine( "workflow bundle is missing the current workflow path".to_string(), )); } @@ -128,7 +125,7 @@ fn parse_child_graph( workflow_path, }); } - Err(FabroError::handler("No child workflow source".to_string())) + Err(Error::handler("No child workflow source".to_string())) } /// Compute the context diff: keys that changed or were added relative to @@ -155,7 +152,7 @@ impl Handler for SubWorkflowHandler { _graph: &Graph, run_dir: &Path, services: &EngineServices, - ) -> Result { + ) -> Result { let poll_interval = node .attrs .get("manager.poll_interval") @@ -204,18 +201,18 @@ impl Handler for SubWorkflowHandler { let child_cancel = Arc::clone(&cancel_token); let child_run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: child_logs, - cancel_token: Some(cancel_token), + settings: SettingsLayer::default(), + run_dir: child_logs, + cancel_token: Some(cancel_token), // Child workflows are part of the parent run's event stream. - run_id: services.emitter.run_id(), - labels: HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + run_id: services.emitter.run_id(), + labels: HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; // Clone parent context for child; inject parent preamble @@ -246,7 +243,7 @@ impl Handler for SubWorkflowHandler { let run_store = store .create_run(&child_run_options.run_id) .await - .map_err(|err| FabroError::engine(err.to_string()))?; + .map_err(|err| Error::engine(err.to_string()))?; let artifact_store = ArtifactStore::new(object_store, "artifacts"); // Spawn child engine @@ -275,7 +272,7 @@ impl Handler for SubWorkflowHandler { provider: fabro_llm::Provider::Anthropic, }; let executed = pipeline::execute(initialized).await; - Ok::<_, FabroError>((executed.outcome?, executed.final_context)) + Ok::<_, Error>((executed.outcome?, executed.final_context)) }); // Poll loop @@ -486,7 +483,7 @@ mod tests { _graph: &Graph, _run_dir: &Path, _services: &EngineServices, - ) -> Result { + ) -> Result { let target = context.get_string("review.target", ""); let mut outcome = Outcome::success(); outcome @@ -594,8 +591,8 @@ mod tests { PathBuf::from("children/review.fabro"), BundledWorkflow { logical_path: PathBuf::from("children/review.fabro"), - source: child_dot_succeeds().to_string(), - files: HashMap::new(), + source: child_dot_succeeds().to_string(), + files: HashMap::new(), }, )])))); @@ -671,7 +668,7 @@ mod tests { _graph: &Graph, _run_dir: &Path, _services: &EngineServices, - ) -> Result { + ) -> Result { tokio::time::sleep(Duration::from_secs(10)).await; Ok(Outcome::success()) } @@ -724,7 +721,7 @@ mod tests { _graph: &Graph, _run_dir: &Path, _services: &EngineServices, - ) -> Result { + ) -> Result { tokio::time::sleep(Duration::from_secs(10)).await; Ok(Outcome::success()) } @@ -878,7 +875,7 @@ mod tests { _graph: &Graph, _run_dir: &Path, _services: &EngineServices, - ) -> Result { + ) -> Result { let target = context.get_string("review.target", ""); let mut outcome = Outcome::success(); outcome @@ -960,7 +957,7 @@ mod tests { _graph: &Graph, _run_dir: &Path, _services: &EngineServices, - ) -> Result { + ) -> Result { let parent_preamble = context.get_string(keys::INTERNAL_PARENT_PREAMBLE, ""); let mut outcome = Outcome::success(); outcome.context_updates.insert( diff --git a/lib/crates/fabro-workflow/src/handler/mod.rs b/lib/crates/fabro-workflow/src/handler/mod.rs index 99fec2d30..304ebaf8c 100644 --- a/lib/crates/fabro-workflow/src/handler/mod.rs +++ b/lib/crates/fabro-workflow/src/handler/mod.rs @@ -31,7 +31,7 @@ use tokio::time; use tokio_util::sync::CancellationToken; use crate::context::Context; -use crate::error::FabroError; +use crate::error::Error; use crate::event::Emitter; use crate::outcome::{Outcome, OutcomeExt}; use crate::runtime_store::RunStoreHandle; @@ -40,29 +40,29 @@ use crate::workflow_bundle::WorkflowBundle; /// Shared services available to all handlers during execution. pub struct EngineServices { - pub registry: Arc, - pub emitter: Arc, - pub sandbox: Arc, - pub run_store: RunStoreHandle, + pub registry: Arc, + pub emitter: Arc, + pub sandbox: Arc, + pub run_store: RunStoreHandle, /// Git state for the current run. Set via `set_git_state` at the start of /// `run_via_core` and read by parallel/fan-in handlers. pub(crate) git_state: std::sync::RwLock>>, /// Hook runner for user-defined lifecycle hooks. - pub hook_runner: Option>, + pub hook_runner: Option>, /// Environment variables from `[sandbox.env]` config, injected into command /// nodes. - pub env: HashMap, + pub env: HashMap, /// Typed values from `[run.inputs]`, available to prompt templates. - pub inputs: HashMap, + pub inputs: HashMap, /// When true, handlers should skip real execution and return simulated /// results. - pub dry_run: bool, + pub dry_run: bool, /// Optional run-scoped cancellation flag from the core executor. pub cancel_requested: Option>, /// Logical path of the current workflow when running from a bundle. - pub workflow_path: Option, + pub workflow_path: Option, /// Bundled workflows available for child-workflow resolution. - pub workflow_bundle: Option>, + pub workflow_bundle: Option>, } impl EngineServices { @@ -100,14 +100,14 @@ impl EngineServices { Duration::from_millis(1), )); Self { - registry: Arc::new(HandlerRegistry::new(Box::new(start::StartHandler))), - emitter: Arc::new(Emitter::default()), - sandbox: Arc::new(fabro_agent::LocalSandbox::new( + registry: Arc::new(HandlerRegistry::new(Box::new(start::StartHandler))), + emitter: Arc::new(Emitter::default()), + sandbox: Arc::new(fabro_agent::LocalSandbox::new( std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")), )), // Build the test run store on a dedicated runtime so this helper // remains safe to call from both sync tests and #[tokio::test]. - run_store: std::thread::spawn(move || { + run_store: std::thread::spawn(move || { tokio::runtime::Builder::new_current_thread() .enable_all() .build() @@ -122,14 +122,14 @@ impl EngineServices { .join() .expect("test run store thread should join") .into(), - git_state: std::sync::RwLock::new(None), - hook_runner: None, - env: HashMap::new(), - inputs: HashMap::new(), - dry_run: false, + git_state: std::sync::RwLock::new(None), + hook_runner: None, + env: HashMap::new(), + inputs: HashMap::new(), + dry_run: false, cancel_requested: None, - workflow_path: None, - workflow_bundle: None, + workflow_path: None, + workflow_bundle: None, } } } @@ -172,7 +172,7 @@ pub trait Handler: Send + Sync { graph: &Graph, run_dir: &Path, services: &EngineServices, - ) -> Result; + ) -> Result; /// Produce a simulated result for dry-run mode. /// Override for handlers that need custom context updates. @@ -183,13 +183,13 @@ pub trait Handler: Send + Sync { _graph: &Graph, _run_dir: &Path, _services: &EngineServices, - ) -> Result { + ) -> Result { Ok(Outcome::simulated(&node.id)) } /// Determines whether an error should be retried. /// Default implementation retries transient errors only. - fn should_retry(&self, err: &FabroError) -> bool { + fn should_retry(&self, err: &Error) -> bool { err.is_retryable() } } @@ -214,7 +214,7 @@ pub async fn dispatch_handler( graph: &Graph, run_dir: &Path, services: &EngineServices, -) -> Result { +) -> Result { if services.dry_run { handler .simulate(node, context, graph, run_dir, services) @@ -228,7 +228,7 @@ pub async fn dispatch_handler( /// Maps handler type strings to handler implementations. pub struct HandlerRegistry { - handlers: HashMap>, + handlers: HashMap>, default_handler: Box, } @@ -333,7 +333,7 @@ mod tests { _graph: &Graph, _run_dir: &Path, _services: &EngineServices, - ) -> Result { + ) -> Result { Ok(Outcome::success()) } } @@ -395,8 +395,8 @@ mod tests { let handler = TestHandler { _name: "test".to_string(), }; - assert!(handler.should_retry(&FabroError::handler("timeout".to_string()))); - assert!(!handler.should_retry(&FabroError::Parse("bad".to_string()))); + assert!(handler.should_retry(&Error::handler("timeout".to_string()))); + assert!(!handler.should_retry(&Error::Parse("bad".to_string()))); } struct NeverRetryHandler; @@ -410,11 +410,11 @@ mod tests { _graph: &Graph, _run_dir: &Path, _services: &EngineServices, - ) -> Result { + ) -> Result { Ok(Outcome::success()) } - fn should_retry(&self, _err: &FabroError) -> bool { + fn should_retry(&self, _err: &Error) -> bool { false } } @@ -422,8 +422,8 @@ mod tests { #[test] fn custom_should_retry_override() { let handler = NeverRetryHandler; - assert!(!handler.should_retry(&FabroError::handler("timeout".to_string()))); - assert!(!handler.should_retry(&FabroError::Io("connection reset".to_string()))); + assert!(!handler.should_retry(&Error::handler("timeout".to_string()))); + assert!(!handler.should_retry(&Error::Io("connection reset".to_string()))); } #[test] diff --git a/lib/crates/fabro-workflow/src/handler/parallel.rs b/lib/crates/fabro-workflow/src/handler/parallel.rs index 41bf13c5a..3cdafeb5c 100644 --- a/lib/crates/fabro-workflow/src/handler/parallel.rs +++ b/lib/crates/fabro-workflow/src/handler/parallel.rs @@ -11,7 +11,7 @@ use tokio::sync::Semaphore; use super::{EngineServices, Handler}; use crate::context::{Context, WorkflowContext, keys}; -use crate::error::FabroError; +use crate::error::Error; use crate::event::{Event, StageScope}; use crate::git::sanitize_ref_component; use crate::hook_context::set_hook_node; @@ -48,9 +48,9 @@ fn parse_join_policy(raw: &str) -> JoinPolicy { } struct BranchResult { - id: String, - outcome: Outcome, - head_sha: Option, + id: String, + outcome: Outcome, + head_sha: Option, worktree_path: Option, } @@ -63,7 +63,7 @@ impl Handler for ParallelHandler { graph: &Graph, run_dir: &Path, services: &EngineServices, - ) -> Result { + ) -> Result { let branches = graph.outgoing_edges(&node.id); if branches.is_empty() { return Ok(Outcome::fail_classify("No branches for parallel node")); @@ -125,15 +125,15 @@ impl Handler for ParallelHandler { graph: &Graph, run_dir: &Path, services: &EngineServices, - ) -> Result { + ) -> Result { // Build per-branch sandboxes (sequentially for git setup) struct BranchSetup { - target_id: String, - branch_index: usize, + target_id: String, + branch_index: usize, parallel_branch_id: ParallelBranchId, - branch_context: Context, - sandbox: Arc, - worktree_path: Option, + branch_context: Context, + sandbox: Arc, + worktree_path: Option, } let parallel_start = Instant::now(); @@ -154,10 +154,10 @@ impl Handler for ParallelHandler { services.emitter.emit_scoped( &Event::ParallelStarted { - node_id: node.id.clone(), - visit: parallel_stage_scope.visit, + node_id: node.id.clone(), + visit: parallel_stage_scope.visit, branch_count: branches.len(), - join_policy: join_policy.to_string(), + join_policy: join_policy.to_string(), }, ¶llel_stage_scope, ); @@ -165,7 +165,7 @@ impl Handler for ParallelHandler { let run_id = context .run_id() .parse::() - .map_err(|err| FabroError::handler(format!("invalid internal run_id: {err}")))?; + .map_err(|err| Error::handler(format!("invalid internal run_id: {err}")))?; let mut hook_ctx = HookContext::new(HookEvent::ParallelStart, run_id, graph.name.clone()); set_hook_node(&mut hook_ctx, node); @@ -249,9 +249,9 @@ impl Handler for ParallelHandler { // Set up worktree via WorktreeSandbox let wt_config = WorktreeOptions { - branch_name: branch_name.clone(), - base_sha: bsha.clone(), - worktree_path: wt_path_str.clone(), + branch_name: branch_name.clone(), + base_sha: bsha.clone(), + worktree_path: wt_path_str.clone(), skip_branch_creation: false, }; let mut wt_sandbox = WorktreeSandbox::new(Arc::clone(&services.sandbox), wt_config); @@ -259,7 +259,7 @@ impl Handler for ParallelHandler { wt_sandbox .initialize() .await - .map_err(|e| FabroError::handler(format!("worktree setup failed: {e}")))?; + .map_err(|e| Error::handler(format!("worktree setup failed: {e}")))?; branch_context.set(keys::INTERNAL_WORK_DIR, serde_json::json!(&wt_path_str)); @@ -314,14 +314,14 @@ impl Handler for ParallelHandler { let _permit = sem .acquire() .await - .map_err(|e| FabroError::handler(format!("semaphore error: {e}")))?; + .map_err(|e| Error::handler(format!("semaphore error: {e}")))?; emitter.emit_scoped( &Event::ParallelBranchStarted { - parallel_group_id: group_id.clone(), + parallel_group_id: group_id.clone(), parallel_branch_id: setup.parallel_branch_id.clone(), - branch: setup.target_id.clone(), - index: setup.branch_index, + branch: setup.target_id.clone(), + index: setup.branch_index, }, &branch_scope, ); @@ -334,13 +334,13 @@ impl Handler for ParallelHandler { )); emitter.emit_scoped( &Event::ParallelBranchCompleted { - parallel_group_id: group_id.clone(), + parallel_group_id: group_id.clone(), parallel_branch_id: setup.parallel_branch_id.clone(), - branch: setup.target_id.clone(), - index: setup.branch_index, - duration_ms: millis_u64(branch_start.elapsed()), - status: "fail".to_string(), - head_sha: None, + branch: setup.target_id.clone(), + index: setup.branch_index, + duration_ms: millis_u64(branch_start.elapsed()), + status: "fail".to_string(), + head_sha: None, }, &branch_scope, ); @@ -413,7 +413,7 @@ impl Handler for ParallelHandler { emitter.emit_scoped( &Event::GitCommit { node_id: Some(setup.target_id.clone()), - sha: sha.clone(), + sha: sha.clone(), }, &branch_scope, ); @@ -427,18 +427,18 @@ impl Handler for ParallelHandler { emitter.emit_scoped( &Event::ParallelBranchCompleted { - parallel_group_id: group_id.clone(), + parallel_group_id: group_id.clone(), parallel_branch_id: setup.parallel_branch_id.clone(), - branch: setup.target_id.clone(), - index: setup.branch_index, - duration_ms: millis_u64(branch_start.elapsed()), - status: outcome.status.to_string(), - head_sha: head_sha.clone(), + branch: setup.target_id.clone(), + index: setup.branch_index, + duration_ms: millis_u64(branch_start.elapsed()), + status: outcome.status.to_string(), + head_sha: head_sha.clone(), }, &branch_scope, ); - Ok::(BranchResult { + Ok::(BranchResult { id: setup.target_id, outcome, head_sha, @@ -457,19 +457,17 @@ impl Handler for ParallelHandler { } Ok(Err(e)) => { results.push(BranchResult { - id: String::new(), - outcome: e.to_fail_outcome(), - head_sha: None, + id: String::new(), + outcome: e.to_fail_outcome(), + head_sha: None, worktree_path: None, }); } Err(join_err) => { results.push(BranchResult { - id: String::new(), - outcome: Outcome::fail_classify(format!( - "task join error: {join_err}" - )), - head_sha: None, + id: String::new(), + outcome: Outcome::fail_classify(format!("task join error: {join_err}")), + head_sha: None, worktree_path: None, }); } @@ -547,7 +545,7 @@ impl Handler for ParallelHandler { let run_id = context .run_id() .parse::() - .map_err(|err| FabroError::handler(format!("invalid internal run_id: {err}")))?; + .map_err(|err| Error::handler(format!("invalid internal run_id: {err}")))?; let mut hook_ctx = HookContext::new(HookEvent::ParallelComplete, run_id, graph.name.clone()); set_hook_node(&mut hook_ctx, node); diff --git a/lib/crates/fabro-workflow/src/handler/prompt.rs b/lib/crates/fabro-workflow/src/handler/prompt.rs index f35438872..a107a2eae 100644 --- a/lib/crates/fabro-workflow/src/handler/prompt.rs +++ b/lib/crates/fabro-workflow/src/handler/prompt.rs @@ -9,7 +9,7 @@ use super::agent::{ }; use super::{EngineServices, Handler}; use crate::context::{Context, WorkflowContext, keys}; -use crate::error::FabroError; +use crate::error::Error; use crate::event::{Event, StageScope}; use crate::outcome::Outcome; @@ -34,7 +34,7 @@ impl Handler for PromptHandler { _graph: &Graph, _run_dir: &Path, _services: &EngineServices, - ) -> Result { + ) -> Result { Ok(super::agent::simulate_llm_handler(node)) } @@ -45,7 +45,7 @@ impl Handler for PromptHandler { graph: &Graph, _run_dir: &Path, services: &EngineServices, - ) -> Result { + ) -> Result { // 1. Build prompt (prepend fidelity preamble if present) let raw_prompt = node .prompt() @@ -91,12 +91,12 @@ impl Handler for PromptHandler { let stage_scope = StageScope::for_handler(context, &node.id); services.emitter.emit_scoped( &Event::Prompt { - stage: node.id.clone(), - visit: stage_scope.visit, - text: prompt.clone(), - mode: Some("prompt".to_string()), + stage: node.id.clone(), + visit: stage_scope.visit, + text: prompt.clone(), + mode: Some("prompt".to_string()), provider: prompt_provider.clone(), - model: prompt_model.clone(), + model: prompt_model.clone(), }, &stage_scope, ); @@ -143,11 +143,11 @@ impl Handler for PromptHandler { services.emitter.emit_scoped( &Event::PromptCompleted { - node_id: node.id.clone(), + node_id: node.id.clone(), response: response_text.clone(), - model: response_model, + model: response_model, provider: response_provider, - billing: stage_usage.clone(), + billing: stage_usage.clone(), }, &stage_scope, ); @@ -269,7 +269,7 @@ mod tests { _emitter: &Arc, _sandbox: &Arc, _tool_hooks: Option>, - ) -> Result { + ) -> Result { panic!("run() should not be called for prompt handler"); } @@ -278,11 +278,11 @@ mod tests { _node: &Node, _prompt: &str, _system_prompt: Option<&str>, - ) -> Result { + ) -> Result { Ok(CodergenResult::Text { - text: "one-shot response".to_string(), - usage: None, - files_touched: Vec::new(), + text: "one-shot response".to_string(), + usage: None, + files_touched: Vec::new(), last_file_touched: None, }) } @@ -329,7 +329,7 @@ mod tests { _emitter: &Arc, _sandbox: &Arc, _tool_hooks: Option>, - ) -> Result { + ) -> Result { panic!("run() should not be called for prompt handler"); } @@ -338,11 +338,11 @@ mod tests { _node: &Node, _prompt: &str, _system_prompt: Option<&str>, - ) -> Result { + ) -> Result { Ok(CodergenResult::Text { - text: "one-shot response".to_string(), - usage: None, - files_touched: Vec::new(), + text: "one-shot response".to_string(), + usage: None, + files_touched: Vec::new(), last_file_touched: None, }) } @@ -371,7 +371,7 @@ mod tests { } struct OneShotCapturingBackend { - captured_prompt: Arc>>, + captured_prompt: Arc>>, captured_system_prompt: Arc>>>, } @@ -386,7 +386,7 @@ mod tests { _emitter: &Arc, _sandbox: &Arc, _tool_hooks: Option>, - ) -> Result { + ) -> Result { panic!("run() should not be called for prompt handler"); } @@ -395,13 +395,13 @@ mod tests { _node: &Node, prompt: &str, system_prompt: Option<&str>, - ) -> Result { + ) -> Result { *self.captured_prompt.lock().unwrap() = Some(prompt.to_string()); *self.captured_system_prompt.lock().unwrap() = Some(system_prompt.map(String::from)); Ok(CodergenResult::Text { - text: "classified".to_string(), - usage: None, - files_touched: Vec::new(), + text: "classified".to_string(), + usage: None, + files_touched: Vec::new(), last_file_touched: None, }) } @@ -413,7 +413,7 @@ mod tests { let captured = Arc::new(Mutex::new(None)); let backend = OneShotCapturingBackend { - captured_prompt: captured.clone(), + captured_prompt: captured.clone(), captured_system_prompt: Arc::new(Mutex::new(None)), }; let handler = PromptHandler::new(Some(Box::new(backend))); @@ -450,7 +450,7 @@ mod tests { let captured_sys = Arc::new(Mutex::new(None)); let backend = OneShotCapturingBackend { - captured_prompt: Arc::new(Mutex::new(None)), + captured_prompt: Arc::new(Mutex::new(None)), captured_system_prompt: captured_sys.clone(), }; let handler = PromptHandler::new(Some(Box::new(backend))); @@ -483,7 +483,7 @@ mod tests { let captured_sys = Arc::new(Mutex::new(None)); let backend = OneShotCapturingBackend { - captured_prompt: Arc::new(Mutex::new(None)), + captured_prompt: Arc::new(Mutex::new(None)), captured_system_prompt: captured_sys.clone(), }; let handler = PromptHandler::new(Some(Box::new(backend))); diff --git a/lib/crates/fabro-workflow/src/handler/start.rs b/lib/crates/fabro-workflow/src/handler/start.rs index fab68b244..48c87e2e8 100644 --- a/lib/crates/fabro-workflow/src/handler/start.rs +++ b/lib/crates/fabro-workflow/src/handler/start.rs @@ -5,7 +5,7 @@ use fabro_graphviz::graph::{Graph, Node}; use super::{EngineServices, Handler}; use crate::context::Context; -use crate::error::FabroError; +use crate::error::Error; use crate::outcome::Outcome; /// No-op handler for pipeline entry point. Returns SUCCESS immediately. @@ -20,7 +20,7 @@ impl Handler for StartHandler { _graph: &Graph, _run_dir: &Path, _services: &EngineServices, - ) -> Result { + ) -> Result { Ok(Outcome::success()) } } diff --git a/lib/crates/fabro-workflow/src/handler/wait.rs b/lib/crates/fabro-workflow/src/handler/wait.rs index b23d7450d..9d47c5e47 100644 --- a/lib/crates/fabro-workflow/src/handler/wait.rs +++ b/lib/crates/fabro-workflow/src/handler/wait.rs @@ -6,7 +6,7 @@ use tokio::time::sleep; use super::{EngineServices, Handler}; use crate::context::Context; -use crate::error::FabroError; +use crate::error::Error; use crate::outcome::Outcome; /// Sleeps for a configured duration before proceeding. @@ -21,13 +21,13 @@ impl Handler for WaitHandler { _graph: &Graph, _run_dir: &Path, _services: &EngineServices, - ) -> Result { + ) -> Result { let duration = node .attrs .get("duration") .and_then(AttrValue::as_duration) .ok_or_else(|| { - FabroError::Validation(format!( + Error::Validation(format!( "wait node {:?} is missing a valid `duration` attribute", node.id )) diff --git a/lib/crates/fabro-workflow/src/lib.rs b/lib/crates/fabro-workflow/src/lib.rs index 9fd4d9928..b96b7ec5f 100644 --- a/lib/crates/fabro-workflow/src/lib.rs +++ b/lib/crates/fabro-workflow/src/lib.rs @@ -72,15 +72,15 @@ pub fn build_completed_stages(cp: &records::Checkpoint, run_failed: bool) -> Vec last.failed = true; } else { stages.push(CompletedStage { - node_id: "unknown".to_string(), - status: "fail".to_string(), - succeeded: false, - failed: true, - retries: 0, + node_id: "unknown".to_string(), + status: "fail".to_string(), + succeeded: false, + failed: true, + retries: 0, billing_usd_micros: None, - notes: None, - failure_reason: None, - files_touched: vec![], + notes: None, + failure_reason: None, + files_touched: vec![], }); } } @@ -139,9 +139,7 @@ pub(crate) mod run_dir; pub mod run_dump; pub mod run_lookup; -pub use error::{ - Error, FabroError, FailureCategory, FailureSignature, FailureSignatureExt, Result, -}; +pub use error::{Error, FailureCategory, FailureSignature, FailureSignatureExt, Result}; pub mod run_materialization; pub mod run_options; pub mod run_status; diff --git a/lib/crates/fabro-workflow/src/lifecycle/artifact.rs b/lib/crates/fabro-workflow/src/lifecycle/artifact.rs index 0b3daa8af..17a2e4603 100644 --- a/lib/crates/fabro-workflow/src/lifecycle/artifact.rs +++ b/lib/crates/fabro-workflow/src/lifecycle/artifact.rs @@ -3,7 +3,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Duration; use async_trait::async_trait; -use fabro_core::error::{CoreError, Result as CoreResult}; +use fabro_core::error::{Error as CoreError, Result as CoreResult}; use fabro_core::graph::NodeSpec; use fabro_core::lifecycle::{AttemptContext, AttemptResultContext, NodeDecision, RunLifecycle}; use fabro_core::outcome::NodeResult; @@ -34,15 +34,15 @@ const ARTIFACT_UPLOAD_RETRY_DELAYS: [Duration; 3] = [ /// Sub-lifecycle responsible for artifact collection, offloading, and syncing. pub(crate) struct ArtifactLifecycle { - pub sandbox: Arc, - pub run_store: RunStoreHandle, - pub emitter: Arc, - pub run_id: RunId, - pub artifact_globs: Vec, - pub artifact_sink: Option, + pub sandbox: Arc, + pub run_store: RunStoreHandle, + pub emitter: Arc, + pub run_id: RunId, + pub artifact_globs: Vec, + pub artifact_sink: Option, pub captured_artifact_count: Arc, /// Per-attempt state: epoch seconds when the attempt started. - attempt_start_epoch: std::sync::Mutex>, + attempt_start_epoch: std::sync::Mutex>, } impl ArtifactLifecycle { @@ -129,8 +129,8 @@ impl RunLifecycle for ArtifactLifecycle { .await { self.emitter.emit(&Event::RunNotice { - level: RunNoticeLevel::Warn, - code: "artifact_upload_failed".to_string(), + level: RunNoticeLevel::Warn, + code: "artifact_upload_failed".to_string(), message: format!("[node: {node_id}] artifact upload failed: {err}"), }); return Ok(()); @@ -140,14 +140,14 @@ impl RunLifecycle for ArtifactLifecycle { self.captured_artifact_count.fetch_add(1, Ordering::Relaxed); self.emitter.emit_scoped( &Event::ArtifactCaptured { - node_id: node_id.to_string(), - attempt: ctx.attempt, - node_slug: node_slug.clone(), - path: asset.path.clone(), - mime: asset.mime.clone(), - content_md5: asset.content_md5.clone(), + node_id: node_id.to_string(), + attempt: ctx.attempt, + node_slug: node_slug.clone(), + path: asset.path.clone(), + mime: asset.mime.clone(), + content_md5: asset.content_md5.clone(), content_sha256: asset.content_sha256.clone(), - bytes: asset.bytes, + bytes: asset.bytes, }, &scope, ); @@ -156,8 +156,8 @@ impl RunLifecycle for ArtifactLifecycle { Ok(_) => {} // no files collected Err(e) => { self.emitter.emit(&Event::RunNotice { - level: RunNoticeLevel::Warn, - code: "artifact_collection_failed".to_string(), + level: RunNoticeLevel::Warn, + code: "artifact_collection_failed".to_string(), message: format!("[node: {node_id}] artifact collection failed: {e}"), }); } @@ -179,8 +179,8 @@ impl RunLifecycle for ArtifactLifecycle { offload_large_values(&mut result.outcome.context_updates, &self.run_store).await { self.emitter.emit(&Event::RunNotice { - level: RunNoticeLevel::Warn, - code: "artifact_offload_failed".to_string(), + level: RunNoticeLevel::Warn, + code: "artifact_offload_failed".to_string(), message: format!("[node: {node_id}] artifact offload failed: {e}"), }); } @@ -192,8 +192,8 @@ impl RunLifecycle for ArtifactLifecycle { sync_artifacts_to_env(&mut result.outcome.context_updates, &*self.sandbox).await { self.emitter.emit(&Event::RunNotice { - level: RunNoticeLevel::Warn, - code: "artifact_sync_failed".to_string(), + level: RunNoticeLevel::Warn, + code: "artifact_sync_failed".to_string(), message: format!("[node: {node_id}] artifact sync failed: {e}"), }); } diff --git a/lib/crates/fabro-workflow/src/lifecycle/circuit_breaker.rs b/lib/crates/fabro-workflow/src/lifecycle/circuit_breaker.rs index 7e777f1b5..d2b6507ad 100644 --- a/lib/crates/fabro-workflow/src/lifecycle/circuit_breaker.rs +++ b/lib/crates/fabro-workflow/src/lifecycle/circuit_breaker.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::sync::Mutex; use async_trait::async_trait; -use fabro_core::error::{CoreError, Result as CoreResult}; +use fabro_core::error::{Error as CoreError, Result as CoreResult}; use fabro_core::lifecycle::{EdgeContext, EdgeDecision, RunLifecycle}; use fabro_core::outcome::NodeResult; use fabro_core::state::ExecutionState; @@ -17,8 +17,8 @@ type WfNodeResult = NodeResult>; /// Sub-lifecycle responsible for tracking failure signatures and tripping the /// circuit breaker when deterministic failure cycles are detected. pub(crate) struct CircuitBreakerLifecycle { - loop_failure_signatures: Mutex>, - restart_failure_signatures: Mutex>, + loop_failure_signatures: Mutex>, + restart_failure_signatures: Mutex>, loop_restart_signature_limit: usize, } diff --git a/lib/crates/fabro-workflow/src/lifecycle/event.rs b/lib/crates/fabro-workflow/src/lifecycle/event.rs index 3e598b4f0..83f0becc9 100644 --- a/lib/crates/fabro-workflow/src/lifecycle/event.rs +++ b/lib/crates/fabro-workflow/src/lifecycle/event.rs @@ -16,7 +16,7 @@ use fabro_types::{BilledTokenCounts, RunId, StatusReason}; use super::circuit_breaker::CircuitBreakerLifecycle; use super::git::GitCheckpointResult; use crate::context::WorkflowContext; -use crate::error::FabroError; +use crate::error::Error; use crate::event::{Emitter, Event, StageScope}; use crate::graph::{WorkflowGraph, WorkflowNode}; use crate::outcome::{BilledModelUsage, FailureCategory, FailureDetail, Outcome, StageStatus}; @@ -31,25 +31,25 @@ type FailureSignatureSnapshot = ( /// Sub-lifecycle responsible for emitting workflow run events. pub(crate) struct EventLifecycle { - pub emitter: Arc, - pub graph_name: String, - pub run_id: RunId, - pub run_start: Mutex, + pub emitter: Arc, + pub graph_name: String, + pub run_id: RunId, + pub run_start: Mutex, /// Set in on_edge_selected when loop_restart approved; emitted+cleared in /// on_run_start. - pub restarted_from: Arc>>, + pub restarted_from: Arc>>, // Config for WorkflowRunStarted payload - pub base_branch: Option, - pub base_sha: Option, - pub run_branch: Option, - pub worktree_dir: Option, - pub goal: Option, + pub base_branch: Option, + pub base_sha: Option, + pub run_branch: Option, + pub worktree_dir: Option, + pub goal: Option, pub captured_artifact_count: Arc, // Cross-lifecycle data - pub checkpoint_git_result: Arc>>, - pub last_git_sha: Arc>>, - pub final_patch: Arc>>, - pub circuit_breaker: Arc, + pub checkpoint_git_result: Arc>>, + pub last_git_sha: Arc>>, + pub final_patch: Arc>>, + pub circuit_breaker: Arc, } fn snapshot_failure_signatures( @@ -85,9 +85,9 @@ fn stage_visit(state: &WfRunState, node_id: &str) -> u32 { pub(crate) fn stage_scope_for(state: &WfRunState, node_id: &str) -> StageScope { StageScope { - node_id: node_id.to_string(), - visit: stage_visit(state, node_id), - parallel_group_id: state.context.parallel_group_id(), + node_id: node_id.to_string(), + visit: stage_visit(state, node_id), + parallel_group_id: state.context.parallel_group_id(), parallel_branch_id: state.context.parallel_branch_id(), } } @@ -109,13 +109,13 @@ impl RunLifecycle for EventLifecycle { // Emit RunStarted self.emitter.emit(&Event::WorkflowRunStarted { - name: self.graph_name.clone(), - run_id: self.run_id, - base_branch: self.base_branch.clone(), - base_sha: self.base_sha.clone(), - run_branch: self.run_branch.clone(), + name: self.graph_name.clone(), + run_id: self.run_id, + base_branch: self.base_branch.clone(), + base_sha: self.base_sha.clone(), + run_branch: self.run_branch.clone(), worktree_dir: self.worktree_dir.clone(), - goal: self.goal.clone(), + goal: self.goal.clone(), }); self.emitter.emit(&Event::RunRunning { reason: None }); @@ -138,11 +138,11 @@ impl RunLifecycle for EventLifecycle { snapshot_failure_signatures(&self.circuit_breaker); self.emitter.emit_scoped( &Event::StageStarted { - node_id: gv.id.clone(), - name: gv.label().to_string(), - index: stage_index, + node_id: gv.id.clone(), + name: gv.label().to_string(), + index: stage_index, handler_type: gv.handler_type().unwrap_or_default().to_string(), - attempt: 1, + attempt: 1, max_attempts: 1, }, &scope, @@ -186,11 +186,11 @@ impl RunLifecycle for EventLifecycle { let scope = stage_scope_for(state, &gv.id); self.emitter.emit_scoped( &Event::StageStarted { - node_id: gv.id.clone(), - name: gv.label().to_string(), - index: state.stage_index, + node_id: gv.id.clone(), + name: gv.label().to_string(), + index: state.stage_index, handler_type: gv.handler_type().unwrap_or_default().to_string(), - attempt: ctx.attempt as usize, + attempt: ctx.attempt as usize, max_attempts: ctx.max_attempts as usize, }, &scope, @@ -211,10 +211,10 @@ impl RunLifecycle for EventLifecycle { self.emitter.emit_scoped( &Event::StageFailed { - node_id: gv.id.clone(), - name: gv.label().to_string(), - index: stage_index, - failure: outcome.failure.clone().unwrap_or_else(|| { + node_id: gv.id.clone(), + name: gv.label().to_string(), + index: stage_index, + failure: outcome.failure.clone().unwrap_or_else(|| { FailureDetail::new("handler failed", FailureCategory::TransientInfra) }), will_retry: true, @@ -224,12 +224,12 @@ impl RunLifecycle for EventLifecycle { self.emitter.emit_scoped( &Event::StageRetrying { - node_id: gv.id.clone(), - name: gv.label().to_string(), - index: stage_index, - attempt: ctx.attempt as usize, + node_id: gv.id.clone(), + name: gv.label().to_string(), + index: stage_index, + attempt: ctx.attempt as usize, max_attempts: ctx.result.max_attempts as usize, - delay_ms: ctx + delay_ms: ctx .backoff_delay .map_or(0, |d| u64::try_from(d.as_millis()).unwrap()), }, @@ -260,10 +260,10 @@ impl RunLifecycle for EventLifecycle { if outcome.status == StageStatus::Fail { self.emitter.emit_scoped( &Event::StageFailed { - node_id: gv.id.clone(), - name: gv.label().to_string(), - index: stage_index, - failure: outcome.failure.clone().unwrap_or_else(|| { + node_id: gv.id.clone(), + name: gv.label().to_string(), + index: stage_index, + failure: outcome.failure.clone().unwrap_or_else(|| { FailureDetail::new("handler failed", FailureCategory::Deterministic) }), will_retry: false, @@ -399,14 +399,14 @@ impl RunLifecycle for EventLifecycle { self.emitter.emit_scoped( &Event::GitCommit { node_id: Some(node.id().to_string()), - sha: sha.clone(), + sha: sha.clone(), }, &scope, ); } for (branch, success) in &result.push_results { self.emitter.emit(&Event::GitPush { - branch: branch.clone(), + branch: branch.clone(), success: *success, }); } @@ -449,7 +449,7 @@ impl RunLifecycle for EventLifecycle { if state.cancelled { self.emitter.emit(&Event::WorkflowRunFailed { - error: FabroError::Cancelled, + error: Error::Cancelled, duration_ms, reason: Some(StatusReason::Cancelled), git_commit_sha: last_sha, @@ -477,7 +477,7 @@ impl RunLifecycle for EventLifecycle { .as_ref() .map_or_else(|| "run failed".to_string(), |f| f.message.clone()); self.emitter.emit(&Event::WorkflowRunFailed { - error: FabroError::engine(error_msg), + error: Error::engine(error_msg), duration_ms, reason: Some(StatusReason::WorkflowError), git_commit_sha: last_sha, diff --git a/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs b/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs index 4e92d39a2..019c8fde3 100644 --- a/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs +++ b/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs @@ -3,7 +3,7 @@ use std::sync::{Arc, Mutex}; use async_trait::async_trait; use fabro_agent::Sandbox; -use fabro_core::error::{CoreError, Result as CoreResult}; +use fabro_core::error::{Error as CoreError, Result as CoreResult}; use fabro_core::graph::NodeSpec; use fabro_core::lifecycle::{EdgeContext, EdgeDecision, NodeDecision, RunLifecycle}; use fabro_core::state::ExecutionState; @@ -29,11 +29,11 @@ struct IncomingEdgeData { /// Sub-lifecycle responsible for fidelity/thread resolution and context key /// setup. pub(crate) struct FidelityLifecycle { - pub graph: Arc, - pub sandbox: Arc, - pub run_store: RunStoreHandle, - pub run_dir: PathBuf, - incoming_edge_data: Mutex>, + pub graph: Arc, + pub sandbox: Arc, + pub run_store: RunStoreHandle, + pub run_dir: PathBuf, + incoming_edge_data: Mutex>, /// True on the first node after checkpoint resume when prior fidelity was /// Full. degrade_fidelity_on_resume: Mutex, diff --git a/lib/crates/fabro-workflow/src/lifecycle/git.rs b/lib/crates/fabro-workflow/src/lifecycle/git.rs index 086ebd973..ca12e5a55 100644 --- a/lib/crates/fabro-workflow/src/lifecycle/git.rs +++ b/lib/crates/fabro-workflow/src/lifecycle/git.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::sync::{Arc, Mutex}; use async_trait::async_trait; -use fabro_core::error::{CoreError, Result as CoreResult}; +use fabro_core::error::{Error as CoreError, Result as CoreResult}; use fabro_core::graph::NodeSpec; use fabro_core::lifecycle::RunLifecycle; use fabro_core::outcome::NodeResult; @@ -54,24 +54,24 @@ fn build_checkpoint( /// Result of a git checkpoint operation, shared with EventLifecycle. #[derive(Debug, Clone)] pub(crate) struct GitCheckpointResult { - pub commit_sha: Option, + pub commit_sha: Option, pub push_results: Vec<(String, bool)>, - pub diff: Option, + pub diff: Option, } /// Sub-lifecycle responsible for git operations (checkpoint commits, pushes, /// diffs). pub(crate) struct GitLifecycle { - pub sandbox: Arc, - pub emitter: Arc, - pub run_id: RunId, - pub run_store: RunStoreHandle, - pub run_options: Arc, - pub start_node_id: Option, + pub sandbox: Arc, + pub emitter: Arc, + pub run_id: RunId, + pub run_store: RunStoreHandle, + pub run_options: Arc, + pub start_node_id: Option, // Cross-lifecycle data (shared with EventLifecycle) pub checkpoint_git_result: Arc>>, - pub last_git_sha: Arc>>, - pub final_patch: Arc>>, + pub last_git_sha: Arc>>, + pub final_patch: Arc>>, } #[async_trait] @@ -165,8 +165,8 @@ impl RunLifecycle for GitLifecycle { Ok(sha) => Some(sha), Err(e) => { self.emitter.emit(&Event::RunNotice { - level: RunNoticeLevel::Warn, - code: "checkpoint_metadata_write_failed".to_string(), + level: RunNoticeLevel::Warn, + code: "checkpoint_metadata_write_failed".to_string(), message: format!( "[node: {node_id}] metadata checkpoint write failed: {e}" ), @@ -199,9 +199,9 @@ impl RunLifecycle for GitLifecycle { match commit_result { Ok(sha) => { let mut git_result = GitCheckpointResult { - commit_sha: Some(sha.clone()), + commit_sha: Some(sha.clone()), push_results: Vec::new(), - diff: None, + diff: None, }; // Push run branch (skip in dry-run mode) @@ -270,8 +270,8 @@ impl RunLifecycle for GitLifecycle { Ok(_) => {} Err(err) => { self.emitter.emit(&Event::RunNotice { - level: RunNoticeLevel::Warn, - code: "git_diff_failed".to_string(), + level: RunNoticeLevel::Warn, + code: "git_diff_failed".to_string(), message: format!("[node: {node_id}] git diff failed: {err}"), }); } @@ -287,7 +287,7 @@ impl RunLifecycle for GitLifecycle { self.emitter.emit_scoped( &Event::CheckpointFailed { node_id: node_id.to_string(), - error: e.clone(), + error: e.clone(), }, &scope, ); @@ -321,8 +321,8 @@ impl RunLifecycle for GitLifecycle { Err(err) => { *self.final_patch.lock().unwrap() = None; self.emitter.emit(&Event::RunNotice { - level: RunNoticeLevel::Warn, - code: "git_diff_failed".to_string(), + level: RunNoticeLevel::Warn, + code: "git_diff_failed".to_string(), message: format!("final diff failed: {err}"), }); } diff --git a/lib/crates/fabro-workflow/src/lifecycle/hook.rs b/lib/crates/fabro-workflow/src/lifecycle/hook.rs index 32e3cf553..90f35da4c 100644 --- a/lib/crates/fabro-workflow/src/lifecycle/hook.rs +++ b/lib/crates/fabro-workflow/src/lifecycle/hook.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use std::sync::Arc; use async_trait::async_trait; -use fabro_core::error::{CoreError, Result as CoreResult}; +use fabro_core::error::{Error as CoreError, Result as CoreResult}; use fabro_core::lifecycle::{ AttemptContext, EdgeContext, EdgeDecision, NodeDecision, RunLifecycle, }; @@ -22,11 +22,11 @@ type WfNodeDecision = NodeDecision>; /// Sub-lifecycle responsible for running workflow hooks. pub(crate) struct HookLifecycle { - pub hook_runner: Option>, - pub sandbox: Arc, + pub hook_runner: Option>, + pub sandbox: Arc, pub hook_work_dir: Option, - pub run_id: RunId, - pub graph_name: String, + pub run_id: RunId, + pub graph_name: String, } impl HookLifecycle { diff --git a/lib/crates/fabro-workflow/src/lifecycle/mod.rs b/lib/crates/fabro-workflow/src/lifecycle/mod.rs index a597391ff..8db964934 100644 --- a/lib/crates/fabro-workflow/src/lifecycle/mod.rs +++ b/lib/crates/fabro-workflow/src/lifecycle/mod.rs @@ -49,28 +49,28 @@ type WfNodeDecision = NodeDecision>; /// Orchestrates all sub-lifecycles with explicit per-callback ordering. /// Implements `RunLifecycle` by delegating to focused structs. pub(crate) struct WorkflowLifecycle { - event: EventLifecycle, - hook: HookLifecycle, - fidelity: FidelityLifecycle, - auto_status: AutoStatusLifecycle, - circuit_breaker: Arc, - git: GitLifecycle, - artifact: ArtifactLifecycle, - on_node: crate::OnNodeCallback, - emitter: Arc, - run_control: Option>, + event: EventLifecycle, + hook: HookLifecycle, + fidelity: FidelityLifecycle, + auto_status: AutoStatusLifecycle, + circuit_breaker: Arc, + git: GitLifecycle, + artifact: ArtifactLifecycle, + on_node: crate::OnNodeCallback, + emitter: Arc, + run_control: Option>, /// Set in on_edge_selected when loop_restart approved; read+cleared by /// EventLifecycle::on_run_start - restarted_from: Arc>>, + restarted_from: Arc>>, /// Shared git checkpoint result (written by git, read by event) checkpoint_git_result: Arc>>, /// True when constructed with a checkpoint; cleared after first /// on_run_start. Gates context seeding on initial resume. - is_initial_resume: AtomicBool, + is_initial_resume: AtomicBool, // Config needed for context seeding - graph: Arc, - run_id: RunId, - working_directory: Option, + graph: Arc, + run_id: RunId, + working_directory: Option, } impl WorkflowLifecycle { @@ -111,21 +111,21 @@ impl WorkflowLifecycle { }; let event = EventLifecycle { - emitter: Arc::clone(emitter), - graph_name: graph.name.clone(), - run_id: run_options.run_id, - run_start: Mutex::new(Instant::now()), - restarted_from: Arc::clone(&restarted_from), - base_branch: run_options.base_branch.clone(), - base_sha: run_options.git.as_ref().and_then(|g| g.base_sha.clone()), - run_branch: run_options.git.as_ref().and_then(|g| g.run_branch.clone()), - worktree_dir: working_directory.clone(), - goal: (!graph.goal().is_empty()).then(|| graph.goal().to_string()), + emitter: Arc::clone(emitter), + graph_name: graph.name.clone(), + run_id: run_options.run_id, + run_start: Mutex::new(Instant::now()), + restarted_from: Arc::clone(&restarted_from), + base_branch: run_options.base_branch.clone(), + base_sha: run_options.git.as_ref().and_then(|g| g.base_sha.clone()), + run_branch: run_options.git.as_ref().and_then(|g| g.run_branch.clone()), + worktree_dir: working_directory.clone(), + goal: (!graph.goal().is_empty()).then(|| graph.goal().to_string()), captured_artifact_count: Arc::clone(&captured_artifact_count), - last_git_sha: Arc::clone(&last_git_sha), - final_patch: Arc::clone(&final_patch), - checkpoint_git_result: Arc::clone(&checkpoint_git_result), - circuit_breaker: Arc::clone(&circuit_breaker), + last_git_sha: Arc::clone(&last_git_sha), + final_patch: Arc::clone(&final_patch), + checkpoint_git_result: Arc::clone(&checkpoint_git_result), + circuit_breaker: Arc::clone(&circuit_breaker), }; let hook = HookLifecycle { diff --git a/lib/crates/fabro-workflow/src/node_handler.rs b/lib/crates/fabro-workflow/src/node_handler.rs index d6583a4f1..80f125158 100644 --- a/lib/crates/fabro-workflow/src/node_handler.rs +++ b/lib/crates/fabro-workflow/src/node_handler.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use std::sync::Arc; use async_trait::async_trait; -use fabro_core::error::{CoreError, HandlerErrorDetail, Result as CoreResult}; +use fabro_core::error::{Error as CoreError, HandlerErrorDetail, Result as CoreResult}; use fabro_core::handler::NodeHandler; use fabro_core::outcome::FailureCategory; use fabro_core::retry::RetryPolicy as CoreRetryPolicy; @@ -13,7 +13,7 @@ use tokio::time::timeout; use crate::artifact; use crate::context::Context; -use crate::error::FabroError; +use crate::error::Error; use crate::graph::{WorkflowGraph, WorkflowNode}; use crate::handler::{EngineServices, dispatch_handler, format_panic_message}; use crate::outcome::{Outcome, StageStatus}; @@ -26,8 +26,8 @@ use crate::retry::build_retry_policy; /// then diffs and applies changes back. pub(crate) struct WorkflowNodeHandler { pub services: Arc, - pub run_dir: PathBuf, - pub graph: Arc, + pub run_dir: PathBuf, + pub graph: Arc, } #[async_trait] @@ -50,9 +50,9 @@ impl NodeHandler for WorkflowNodeHandler { .await .map_err(|err| { CoreError::handler(HandlerErrorDetail { - message: err.to_string(), + message: err.to_string(), retryable: true, - category: Some(FailureCategory::TransientInfra), + category: Some(FailureCategory::TransientInfra), signature: None, }) })?; @@ -78,9 +78,9 @@ impl NodeHandler for WorkflowNodeHandler { Ok(inner) => inner, Err(_elapsed) => { return Err(CoreError::handler(HandlerErrorDetail { - message: format!("handler timed out after {}ms", duration.as_millis()), + message: format!("handler timed out after {}ms", duration.as_millis()), retryable: true, - category: Some(FailureCategory::TransientInfra), + category: Some(FailureCategory::TransientInfra), signature: None, })); } @@ -101,7 +101,7 @@ impl NodeHandler for WorkflowNodeHandler { match timed_result { Ok(Ok(wf_outcome)) => Ok(wf_outcome), - Ok(Err(FabroError::Cancelled)) => Err(CoreError::Cancelled), + Ok(Err(Error::Cancelled)) => Err(CoreError::Cancelled), Ok(Err(fabro_err)) => { let retryable = handler.should_retry(&fabro_err); Err(CoreError::handler(HandlerErrorDetail { @@ -114,9 +114,9 @@ impl NodeHandler for WorkflowNodeHandler { Err(panic_payload) => { let msg = format_panic_message(&panic_payload); Err(CoreError::handler(HandlerErrorDetail { - message: msg, + message: msg, retryable: false, - category: Some(FailureCategory::Deterministic), + category: Some(FailureCategory::Deterministic), signature: None, })) } diff --git a/lib/crates/fabro-workflow/src/operations/create.rs b/lib/crates/fabro-workflow/src/operations/create.rs index 1fbd54ad4..105da69b5 100644 --- a/lib/crates/fabro-workflow/src/operations/create.rs +++ b/lib/crates/fabro-workflow/src/operations/create.rs @@ -15,7 +15,7 @@ use fabro_types::{RunId, RunProvenance}; use fabro_util::json::normalize_json_value; use super::source::{ResolveWorkflowInput, WorkflowInput, resolve_workflow}; -use crate::error::FabroError; +use crate::error::Error; use crate::event::{Event, append_event, to_run_event_at}; use crate::file_resolver::FileResolver; use crate::pipeline::types::PersistOptions; @@ -45,32 +45,32 @@ pub struct CreateRunInput { #[derive(Debug)] pub struct CreatedRun { pub persisted: Persisted, - pub run_id: RunId, - pub run_dir: PathBuf, - pub dot_path: Option, + pub run_id: RunId, + pub run_dir: PathBuf, + pub dot_path: Option, } struct PersistCreateOptions { - settings: SettingsLayer, - run_id: Option, - run_dir: Option, - workflow_slug: Option, - labels: HashMap, - base_branch: Option, + settings: SettingsLayer, + run_id: Option, + run_dir: Option, + workflow_slug: Option, + labels: HashMap, + base_branch: Option, working_directory: PathBuf, - host_repo_path: Option, - repo_origin_url: Option, - provenance: Option, + host_repo_path: Option, + repo_origin_url: Option, + provenance: Option, } /// Resolve workflow inputs, normalize settings, and persist a run directory. -pub async fn create(store: &Database, request: CreateRunInput) -> Result { +pub async fn create(store: &Database, request: CreateRunInput) -> Result { let resolved = resolve_workflow(ResolveWorkflowInput { workflow: request.workflow, settings: request.settings, - cwd: request.cwd, + cwd: request.cwd, }) - .map_err(|err| FabroError::Parse(err.to_string()))?; + .map_err(|err| Error::Parse(err.to_string()))?; if fabro_config::resolve_run_from_file(&resolved.settings) .map(|settings| settings.execution.mode != RunMode::DryRun) @@ -103,7 +103,7 @@ pub async fn create(store: &Database, request: CreateRunInput) -> Result, submitted_manifest_bytes: Option<&[u8]>, accepted_definition: Option<&RunDefinition>, -) -> Result<(), FabroError> { +) -> Result<(), Error> { let record = persisted.run_record(); let run_store = match store.create_run(&record.run_id).await { Ok(run_store) => run_store, Err(err) => store .open_run(&record.run_id) .await - .map_err(|open_err| FabroError::engine(open_err.to_string())) - .map_err(|_| FabroError::engine(err.to_string()))?, + .map_err(|open_err| Error::engine(open_err.to_string())) + .map_err(|_| Error::engine(err.to_string()))?, }; let manifest_blob = match submitted_manifest_bytes { Some(bytes) => Some(run_store.write_blob(bytes).await.map_err(store_error)?), @@ -200,8 +200,8 @@ async fn persist_created_run( }; let definition_blob = match accepted_definition { Some(definition) => { - let bytes = serde_json::to_vec(definition) - .map_err(|err| FabroError::engine(err.to_string()))?; + let bytes = + serde_json::to_vec(definition).map_err(|err| Error::engine(err.to_string()))?; Some(run_store.write_blob(&bytes).await.map_err(store_error)?) } None => None, @@ -213,11 +213,11 @@ async fn persist_created_run( run_id: record.run_id, settings: normalize_json_value( serde_json::to_value(&record.settings) - .map_err(|err| FabroError::engine(err.to_string()))?, + .map_err(|err| Error::engine(err.to_string()))?, ), graph: normalize_json_value( serde_json::to_value(&record.graph) - .map_err(|err| FabroError::engine(err.to_string()))?, + .map_err(|err| Error::engine(err.to_string()))?, ), workflow_source: (!workflow_source.is_empty()).then(|| workflow_source.to_string()), workflow_config, @@ -240,7 +240,7 @@ async fn persist_created_run( None, ); let payload = fabro_store::EventPayload::new( - serde_json::to_value(&stored).map_err(|err| FabroError::engine(err.to_string()))?, + serde_json::to_value(&stored).map_err(|err| Error::engine(err.to_string()))?, &record.run_id, ) .map_err(store_error)?; @@ -249,16 +249,20 @@ async fn persist_created_run( .await .map(|_| ()) .map_err(store_error)?; - append_event(&run_store, &record.run_id, &Event::RunSubmitted { - reason: None, - definition_blob, - }) + append_event( + &run_store, + &record.run_id, + &Event::RunSubmitted { + reason: None, + definition_blob, + }, + ) .await .map_err(store_error) } -fn store_error(err: impl std::fmt::Display) -> FabroError { - FabroError::engine(err.to_string()) +fn store_error(err: impl std::fmt::Display) -> Error { + Error::engine(err.to_string()) } fn render_resolve_errors(errors: &[fabro_config::ResolveError]) -> String { @@ -269,9 +273,9 @@ fn render_resolve_errors(errors: &[fabro_config::ResolveError]) -> String { .join("; ") } -fn resolve_settings_tree(settings: &SettingsLayer) -> Result { +fn resolve_settings_tree(settings: &SettingsLayer) -> Result { fabro_config::resolve(settings) - .map_err(|errors| FabroError::Precondition(render_resolve_errors(&errors))) + .map_err(|errors| Error::Precondition(render_resolve_errors(&errors))) } fn combined_labels(settings: &Settings) -> HashMap { @@ -281,14 +285,14 @@ fn combined_labels(settings: &Settings) -> HashMap { labels } -fn validate_sandbox_provider(settings: &SettingsLayer) -> Result<(), FabroError> { +fn validate_sandbox_provider(settings: &SettingsLayer) -> Result<(), Error> { let resolved = fabro_config::resolve_run_from_file(settings) - .map_err(|errors| FabroError::Precondition(render_resolve_errors(&errors)))?; + .map_err(|errors| Error::Precondition(render_resolve_errors(&errors)))?; resolved .sandbox .provider .parse::() - .map_err(|err| FabroError::Precondition(format!("Invalid sandbox provider: {err}")))?; + .map_err(|err| Error::Precondition(format!("Invalid sandbox provider: {err}")))?; Ok(()) } @@ -299,7 +303,7 @@ fn create_from_source( current_dir: Option, file_resolver: Option>, goal_override: Option<&str>, -) -> Result { +) -> Result { let validated = preprocess_and_validate( dot_source, current_dir, @@ -310,7 +314,7 @@ fn create_from_source( )?; if validated.has_errors() { - return Err(FabroError::ValidationFailed { + return Err(Error::ValidationFailed { diagnostics: validated.diagnostics().to_vec(), }); } @@ -325,7 +329,7 @@ pub(super) fn preprocess_and_validate( custom_transforms: Vec>, settings: Option<&SettingsLayer>, goal_override: Option<&str>, -) -> Result { +) -> Result { let inputs = run_inputs(settings); let source = render_template( dot_source, @@ -333,17 +337,20 @@ pub(super) fn preprocess_and_validate( .with_goal("{{ goal }}") .with_inputs(inputs.clone()), ) - .map_err(|error| FabroError::Parse(format!("template expansion failed: {error}")))?; + .map_err(|error| Error::Parse(format!("template expansion failed: {error}")))?; let mut parsed = pipeline::parse(&source)?; apply_goal_override(&mut parsed.graph, goal_override); - let transformed = pipeline::transform(parsed, &TransformOptions { - current_dir, - file_resolver, - inputs, - custom_transforms, - })?; + let transformed = pipeline::transform( + parsed, + &TransformOptions { + current_dir, + file_resolver, + inputs, + custom_transforms, + }, + )?; Ok(pipeline::validate(transformed, &[])) } @@ -367,7 +374,7 @@ fn apply_goal_override(graph: &mut Graph, goal_override: Option<&str>) { fn persist_validated( validated: Validated, options: PersistCreateOptions, -) -> Result { +) -> Result { let PersistCreateOptions { settings, run_id, @@ -401,10 +408,13 @@ fn persist_validated( definition_blob: None, }; - pipeline::persist(validated, PersistOptions { - run_dir, - run_record, - }) + pipeline::persist( + validated, + PersistOptions { + run_dir, + run_record, + }, + ) } pub(crate) fn default_run_dir(run_id: &RunId) -> PathBuf { @@ -444,7 +454,7 @@ mod tests { fn validate_dot(dot_source: &str, settings: SettingsLayer) -> Validated { validate(ValidateInput { workflow: WorkflowInput::DotSource { - source: dot_source.to_string(), + source: dot_source.to_string(), base_dir: None, }, settings, @@ -563,12 +573,12 @@ mod tests { #[test] fn validate_returns_error_on_invalid_dot() { let result = validate(ValidateInput { - workflow: WorkflowInput::DotSource { - source: "not a graph".to_string(), + workflow: WorkflowInput::DotSource { + source: "not a graph".to_string(), base_dir: None, }, - settings: SettingsLayer::default(), - cwd: PathBuf::from("."), + settings: SettingsLayer::default(), + cwd: PathBuf::from("."), custom_transforms: Vec::new(), }); assert!(result.is_err()); @@ -594,7 +604,7 @@ mod tests { fn apply( &self, graph: fabro_graphviz::graph::Graph, - ) -> Result { + ) -> Result { let mut graph = graph; for node in graph.nodes.values_mut() { node.attrs @@ -606,12 +616,12 @@ mod tests { } let validated = validate(ValidateInput { - workflow: WorkflowInput::DotSource { - source: MINIMAL_DOT.to_string(), + workflow: WorkflowInput::DotSource { + source: MINIMAL_DOT.to_string(), base_dir: None, }, - settings: SettingsLayer::default(), - cwd: PathBuf::from("."), + settings: SettingsLayer::default(), + cwd: PathBuf::from("."), custom_transforms: vec![Box::new(TagTransform)], }) .unwrap(); @@ -642,9 +652,9 @@ mod tests { .unwrap(); let validated = validate(ValidateInput { - workflow: WorkflowInput::Path(dot_path), - settings: SettingsLayer::default(), - cwd: dir.path().to_path_buf(), + workflow: WorkflowInput::Path(dot_path), + settings: SettingsLayer::default(), + cwd: dir.path().to_path_buf(), custom_transforms: Vec::new(), }) .unwrap(); @@ -655,9 +665,9 @@ mod tests { #[test] fn validate_from_bundle_resolves_nested_import_files_relative_to_imported_graph() { let validated = validate(ValidateInput { - workflow: WorkflowInput::Bundled(BundledWorkflow { + workflow: WorkflowInput::Bundled(BundledWorkflow { logical_path: PathBuf::from("workflow.fabro"), - source: r#"digraph Test { + source: r#"digraph Test { graph [goal="Ship"] start [shape=Mdiamond] validate [import="./child/validate.fabro"] @@ -665,7 +675,7 @@ mod tests { start -> validate -> exit }"# .to_string(), - files: HashMap::from([ + files: HashMap::from([ ( PathBuf::from("child/validate.fabro"), r#"digraph Validate { @@ -682,8 +692,8 @@ mod tests { ), ]), }), - settings: SettingsLayer::default(), - cwd: PathBuf::from("."), + settings: SettingsLayer::default(), + cwd: PathBuf::from("."), custom_transforms: Vec::new(), }) .unwrap(); @@ -706,28 +716,31 @@ mod tests { }"#; let dir = tempfile::tempdir().unwrap(); let store = memory_store(); - let err = create(&store, CreateRunInput { - workflow: WorkflowInput::DotSource { - source: dot.to_string(), - base_dir: None, + let err = create( + &store, + CreateRunInput { + workflow: WorkflowInput::DotSource { + source: dot.to_string(), + base_dir: None, + }, + settings: SettingsLayer::default(), + cwd: dir.path().to_path_buf(), + workflow_slug: None, + workflow_path: None, + workflow_bundle: None, + submitted_manifest_bytes: None, + run_id: None, + host_repo_path: None, + repo_origin_url: None, + base_branch: None, + provenance: None, }, - settings: SettingsLayer::default(), - cwd: dir.path().to_path_buf(), - workflow_slug: None, - workflow_path: None, - workflow_bundle: None, - submitted_manifest_bytes: None, - run_id: None, - host_repo_path: None, - repo_origin_url: None, - base_branch: None, - provenance: None, - }) + ) .await .unwrap_err(); match err { - FabroError::ValidationFailed { diagnostics } => { + Error::ValidationFailed { diagnostics } => { assert!(!diagnostics.is_empty()); } other => panic!("expected ValidationFailed, got {other:?}"), @@ -738,50 +751,53 @@ mod tests { async fn create_persists_normalized_config_and_initial_state() { let dir = tempfile::tempdir().unwrap(); let store = memory_store(); - let created = create(&store, CreateRunInput { - workflow: WorkflowInput::DotSource { - source: MINIMAL_DOT.to_string(), - base_dir: None, + let created = create( + &store, + CreateRunInput { + workflow: WorkflowInput::DotSource { + source: MINIMAL_DOT.to_string(), + base_dir: None, + }, + settings: { + use fabro_types::settings::run::{ + RunExecutionLayer, RunGoalLayer, RunLayer, RunMode, RunModelLayer, + RunPullRequestLayer, + }; + let mut metadata = HashMap::new(); + metadata.insert("env".to_string(), "test".to_string()); + SettingsLayer { + run: Some(RunLayer { + goal: Some(RunGoalLayer::Inline(InterpString::parse("override goal"))), + metadata, + model: Some(RunModelLayer { + name: Some(InterpString::parse("sonnet")), + ..RunModelLayer::default() + }), + pull_request: Some(RunPullRequestLayer { + enabled: Some(false), + ..RunPullRequestLayer::default() + }), + execution: Some(RunExecutionLayer { + mode: Some(RunMode::DryRun), + ..RunExecutionLayer::default() + }), + ..RunLayer::default() + }), + ..SettingsLayer::default() + } + }, + cwd: dir.path().to_path_buf(), + workflow_slug: Some("slug".to_string()), + workflow_path: None, + workflow_bundle: None, + submitted_manifest_bytes: None, + run_id: Some(fixtures::RUN_1), + host_repo_path: Some(dir.path().display().to_string()), + repo_origin_url: None, + base_branch: Some("main".to_string()), + provenance: None, }, - settings: { - use fabro_types::settings::run::{ - RunExecutionLayer, RunGoalLayer, RunLayer, RunMode, RunModelLayer, - RunPullRequestLayer, - }; - let mut metadata = HashMap::new(); - metadata.insert("env".to_string(), "test".to_string()); - SettingsLayer { - run: Some(RunLayer { - goal: Some(RunGoalLayer::Inline(InterpString::parse("override goal"))), - metadata, - model: Some(RunModelLayer { - name: Some(InterpString::parse("sonnet")), - ..RunModelLayer::default() - }), - pull_request: Some(RunPullRequestLayer { - enabled: Some(false), - ..RunPullRequestLayer::default() - }), - execution: Some(RunExecutionLayer { - mode: Some(RunMode::DryRun), - ..RunExecutionLayer::default() - }), - ..RunLayer::default() - }), - ..SettingsLayer::default() - } - }, - cwd: dir.path().to_path_buf(), - workflow_slug: Some("slug".to_string()), - workflow_path: None, - workflow_bundle: None, - submitted_manifest_bytes: None, - run_id: Some(fixtures::RUN_1), - host_repo_path: Some(dir.path().display().to_string()), - repo_origin_url: None, - base_branch: Some("main".to_string()), - provenance: None, - }) + ) .await .unwrap(); @@ -846,36 +862,39 @@ mod tests { std::fs::create_dir_all(&workspace).unwrap(); let store = memory_store(); - let created = create(&store, CreateRunInput { - workflow: WorkflowInput::DotSource { - source: MINIMAL_DOT.to_string(), - base_dir: None, - }, - settings: { - use fabro_types::settings::run::{RunExecutionLayer, RunLayer, RunMode}; - SettingsLayer { - run: Some(RunLayer { - working_dir: Some(InterpString::parse("workspace")), - execution: Some(RunExecutionLayer { - mode: Some(RunMode::DryRun), - ..RunExecutionLayer::default() + let created = create( + &store, + CreateRunInput { + workflow: WorkflowInput::DotSource { + source: MINIMAL_DOT.to_string(), + base_dir: None, + }, + settings: { + use fabro_types::settings::run::{RunExecutionLayer, RunLayer, RunMode}; + SettingsLayer { + run: Some(RunLayer { + working_dir: Some(InterpString::parse("workspace")), + execution: Some(RunExecutionLayer { + mode: Some(RunMode::DryRun), + ..RunExecutionLayer::default() + }), + ..RunLayer::default() }), - ..RunLayer::default() - }), - ..SettingsLayer::default() - } + ..SettingsLayer::default() + } + }, + cwd: dir.path().to_path_buf(), + workflow_slug: None, + workflow_path: None, + workflow_bundle: None, + submitted_manifest_bytes: None, + run_id: Some(fixtures::RUN_2), + host_repo_path: None, + repo_origin_url: None, + base_branch: None, + provenance: None, }, - cwd: dir.path().to_path_buf(), - workflow_slug: None, - workflow_path: None, - workflow_bundle: None, - submitted_manifest_bytes: None, - run_id: Some(fixtures::RUN_2), - host_repo_path: None, - repo_origin_url: None, - base_branch: None, - provenance: None, - }) + ) .await .unwrap(); @@ -897,23 +916,26 @@ mod tests { async fn create_persists_repo_origin_url_from_request() { let dir = tempfile::tempdir().unwrap(); let store = memory_store(); - let created = create(&store, CreateRunInput { - workflow: WorkflowInput::DotSource { - source: MINIMAL_DOT.to_string(), - base_dir: None, + let created = create( + &store, + CreateRunInput { + workflow: WorkflowInput::DotSource { + source: MINIMAL_DOT.to_string(), + base_dir: None, + }, + settings: dry_run_only_settings(), + cwd: dir.path().to_path_buf(), + workflow_slug: None, + workflow_path: None, + workflow_bundle: None, + submitted_manifest_bytes: None, + run_id: Some(fixtures::RUN_2), + host_repo_path: None, + repo_origin_url: Some("https://github.com/acme/widgets".to_string()), + base_branch: None, + provenance: None, }, - settings: dry_run_only_settings(), - cwd: dir.path().to_path_buf(), - workflow_slug: None, - workflow_path: None, - workflow_bundle: None, - submitted_manifest_bytes: None, - run_id: Some(fixtures::RUN_2), - host_repo_path: None, - repo_origin_url: Some("https://github.com/acme/widgets".to_string()), - base_branch: None, - provenance: None, - }) + ) .await .unwrap(); @@ -966,23 +988,26 @@ mod tests { let object_store = Arc::new(LocalFileSystem::new_with_prefix(storage_dir.join("store")).unwrap()); let store = Arc::new(Database::new(object_store, "", Duration::from_millis(1))); - let created = create(store.as_ref(), CreateRunInput { - workflow: WorkflowInput::DotSource { - source: MINIMAL_DOT.to_string(), - base_dir: None, + let created = create( + store.as_ref(), + CreateRunInput { + workflow: WorkflowInput::DotSource { + source: MINIMAL_DOT.to_string(), + base_dir: None, + }, + settings: dry_run_with_storage(&storage_dir), + cwd: dir.path().to_path_buf(), + workflow_slug: Some("slug".to_string()), + workflow_path: None, + workflow_bundle: None, + submitted_manifest_bytes: None, + run_id: Some(fixtures::RUN_3), + host_repo_path: None, + repo_origin_url: None, + base_branch: None, + provenance: None, }, - settings: dry_run_with_storage(&storage_dir), - cwd: dir.path().to_path_buf(), - workflow_slug: Some("slug".to_string()), - workflow_path: None, - workflow_bundle: None, - submitted_manifest_bytes: None, - run_id: Some(fixtures::RUN_3), - host_repo_path: None, - repo_origin_url: None, - base_branch: None, - provenance: None, - }) + ) .await .unwrap(); let run_store = store.open_run_reader(&created.run_id).await.unwrap(); @@ -1002,36 +1027,39 @@ mod tests { let object_store = Arc::new(LocalFileSystem::new_with_prefix(storage_dir.join("store")).unwrap()); let store = Arc::new(Database::new(object_store, "", Duration::from_millis(1))); - let created = create(store.as_ref(), CreateRunInput { - workflow: WorkflowInput::DotSource { - source: MINIMAL_DOT.to_string(), - base_dir: None, + let created = create( + store.as_ref(), + CreateRunInput { + workflow: WorkflowInput::DotSource { + source: MINIMAL_DOT.to_string(), + base_dir: None, + }, + settings: dry_run_with_storage(&storage_dir), + cwd: dir.path().to_path_buf(), + workflow_slug: Some("slug".to_string()), + workflow_path: None, + workflow_bundle: None, + submitted_manifest_bytes: None, + run_id: Some(fixtures::RUN_64), + host_repo_path: None, + repo_origin_url: None, + base_branch: None, + provenance: Some(fabro_types::RunProvenance { + server: Some(fabro_types::RunServerProvenance { + version: "0.9.0".to_string(), + }), + client: Some(fabro_types::RunClientProvenance { + user_agent: Some("fabro-cli/0.9.0".to_string()), + name: Some("fabro-cli".to_string()), + version: Some("0.9.0".to_string()), + }), + subject: Some(fabro_types::RunSubjectProvenance { + login: None, + auth_method: fabro_types::RunAuthMethod::Disabled, + }), + }), }, - settings: dry_run_with_storage(&storage_dir), - cwd: dir.path().to_path_buf(), - workflow_slug: Some("slug".to_string()), - workflow_path: None, - workflow_bundle: None, - submitted_manifest_bytes: None, - run_id: Some(fixtures::RUN_64), - host_repo_path: None, - repo_origin_url: None, - base_branch: None, - provenance: Some(fabro_types::RunProvenance { - server: Some(fabro_types::RunServerProvenance { - version: "0.9.0".to_string(), - }), - client: Some(fabro_types::RunClientProvenance { - user_agent: Some("fabro-cli/0.9.0".to_string()), - name: Some("fabro-cli".to_string()), - version: Some("0.9.0".to_string()), - }), - subject: Some(fabro_types::RunSubjectProvenance { - login: None, - auth_method: fabro_types::RunAuthMethod::Disabled, - }), - }), - }) + ) .await .unwrap(); diff --git a/lib/crates/fabro-workflow/src/operations/fork.rs b/lib/crates/fabro-workflow/src/operations/fork.rs index 8f039fa04..07c8f624d 100644 --- a/lib/crates/fabro-workflow/src/operations/fork.rs +++ b/lib/crates/fabro-workflow/src/operations/fork.rs @@ -11,8 +11,8 @@ use crate::records::{Checkpoint, RunRecord, StartRecord}; #[derive(Debug, Clone)] pub struct ForkRunInput { pub source_run_id: RunId, - pub target: Option, - pub push: bool, + pub target: Option, + pub push: bool, } /// Create a new run that branches from an existing run at a specific @@ -89,10 +89,10 @@ fn fork_from_entry( let now = new_run_id.created_at(); let start_record = StartRecord { - run_id: new_run_id, + run_id: new_run_id, start_time: now, run_branch: Some(new_run_branch.clone()), - base_sha: None, + base_sha: None, }; let new_start_record_bytes = serde_json::to_vec_pretty(&start_record).context("failed to serialize new start.json")?; @@ -243,11 +243,14 @@ mod tests { let source_run_id = parse_run_id("01ARZ3NDEKTSV4RRFFQ69G5FAV"); let run_oids = setup_source_run(&store, &source_run_id, &["start", "build", "test"]); - let new_run_id = fork(&store, &ForkRunInput { - source_run_id, - target: Some(RewindTarget::from_str("@2").unwrap()), - push: false, - }) + let new_run_id = fork( + &store, + &ForkRunInput { + source_run_id, + target: Some(RewindTarget::from_str("@2").unwrap()), + push: false, + }, + ) .unwrap(); let new_run_branch = format!("{RUN_BRANCH_PREFIX}{new_run_id}"); @@ -287,11 +290,11 @@ mod tests { .write_entry("checkpoint.json", &cp, "checkpoint") .unwrap(); let entry = TimelineEntry { - ordinal: 1, - node_name: "start".to_string(), - visit: 1, + ordinal: 1, + node_name: "start".to_string(), + visit: 1, metadata_commit_oid: oid, - run_commit_sha: None, + run_commit_sha: None, }; let err = fork_from_entry(&store, &run_id, &entry, false) diff --git a/lib/crates/fabro-workflow/src/operations/rebuild_meta.rs b/lib/crates/fabro-workflow/src/operations/rebuild_meta.rs index d6c6c9ba2..4a7513634 100644 --- a/lib/crates/fabro-workflow/src/operations/rebuild_meta.rs +++ b/lib/crates/fabro-workflow/src/operations/rebuild_meta.rs @@ -166,7 +166,7 @@ pub async fn build_timeline_or_rebuild( } Ok(RunTimeline { - entries: Vec::new(), + entries: Vec::new(), parallel_map: HashMap::new(), }) } @@ -397,11 +397,11 @@ mod tests { fn sample_sandbox_record() -> SandboxRecord { SandboxRecord { - provider: "local".to_string(), - working_directory: "/tmp/project".to_string(), - identifier: None, + provider: "local".to_string(), + working_directory: "/tmp/project".to_string(), + identifier: None, host_working_directory: None, - container_mount_point: None, + container_mount_point: None, } } @@ -412,20 +412,20 @@ mod tests { git_commit_sha: Option<&str>, ) -> Checkpoint { Checkpoint { - timestamp: created_at(), - current_node: current_node.to_string(), - completed_nodes: completed_nodes + timestamp: created_at(), + current_node: current_node.to_string(), + completed_nodes: completed_nodes .iter() .map(|node| (*node).to_string()) .collect(), - node_retries: HashMap::new(), - context_values: HashMap::new(), - node_outcomes: HashMap::new(), - next_node_id: None, - git_commit_sha: git_commit_sha.map(ToOwned::to_owned), - loop_failure_signatures: HashMap::new(), + node_retries: HashMap::new(), + context_values: HashMap::new(), + node_outcomes: HashMap::new(), + next_node_id: None, + git_commit_sha: git_commit_sha.map(ToOwned::to_owned), + loop_failure_signatures: HashMap::new(), restart_failure_signatures: HashMap::new(), - node_visits: node_visits + node_visits: node_visits .iter() .map(|(node, visit)| ((*node).to_string(), *visit)) .collect(), @@ -439,23 +439,27 @@ mod tests { ) -> DurableRunStore { let run_store = store.create_run(&run_id).await.unwrap(); let run_record = sample_run_record(run_id, host_repo_path); - append_event(&run_store, &run_id, &Event::RunCreated { - run_id, - settings: serde_json::to_value(&run_record.settings).unwrap(), - graph: serde_json::to_value(&run_record.graph).unwrap(), - workflow_source: None, - workflow_config: None, - labels: run_record.labels.clone().into_iter().collect(), - run_dir: String::new(), - working_directory: run_record.working_directory.display().to_string(), - host_repo_path: run_record.host_repo_path.clone(), - repo_origin_url: run_record.repo_origin_url.clone(), - base_branch: run_record.base_branch.clone(), - workflow_slug: run_record.workflow_slug.clone(), - db_prefix: None, - provenance: run_record.provenance.clone(), - manifest_blob: None, - }) + append_event( + &run_store, + &run_id, + &Event::RunCreated { + run_id, + settings: serde_json::to_value(&run_record.settings).unwrap(), + graph: serde_json::to_value(&run_record.graph).unwrap(), + workflow_source: None, + workflow_config: None, + labels: run_record.labels.clone().into_iter().collect(), + run_dir: String::new(), + working_directory: run_record.working_directory.display().to_string(), + host_repo_path: run_record.host_repo_path.clone(), + repo_origin_url: run_record.repo_origin_url.clone(), + base_branch: run_record.base_branch.clone(), + workflow_slug: run_record.workflow_slug.clone(), + db_prefix: None, + provenance: run_record.provenance.clone(), + manifest_blob: None, + }, + ) .await .unwrap(); run_store @@ -463,28 +467,36 @@ mod tests { async fn append_start_event(run_store: &DurableRunStore, run_id: RunId) { let start = sample_start_record(run_id); - append_event(run_store, &run_id, &Event::WorkflowRunStarted { - name: "test".to_string(), - run_id, - base_branch: None, - base_sha: start.base_sha, - run_branch: start.run_branch, - worktree_dir: None, - goal: None, - }) + append_event( + run_store, + &run_id, + &Event::WorkflowRunStarted { + name: "test".to_string(), + run_id, + base_branch: None, + base_sha: start.base_sha, + run_branch: start.run_branch, + worktree_dir: None, + goal: None, + }, + ) .await .unwrap(); } async fn append_sandbox_event(run_store: &DurableRunStore, run_id: RunId) { let sandbox = sample_sandbox_record(); - append_event(run_store, &run_id, &Event::SandboxInitialized { - provider: sandbox.provider, - working_directory: sandbox.working_directory, - identifier: sandbox.identifier, - host_working_directory: sandbox.host_working_directory, - container_mount_point: sandbox.container_mount_point, - }) + append_event( + run_store, + &run_id, + &Event::SandboxInitialized { + provider: sandbox.provider, + working_directory: sandbox.working_directory, + identifier: sandbox.identifier, + host_working_directory: sandbox.host_working_directory, + container_mount_point: sandbox.container_mount_point, + }, + ) .await .unwrap(); } @@ -494,31 +506,35 @@ mod tests { run_id: RunId, checkpoint: Checkpoint, ) { - append_event(run_store, &run_id, &Event::CheckpointCompleted { - node_id: checkpoint.current_node.clone(), - status: "success".to_string(), - current_node: checkpoint.current_node.clone(), - completed_nodes: checkpoint.completed_nodes.clone(), - node_retries: checkpoint.node_retries.clone().into_iter().collect(), - context_values: checkpoint.context_values.clone().into_iter().collect(), - node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(), - next_node_id: checkpoint.next_node_id.clone(), - git_commit_sha: checkpoint.git_commit_sha.clone(), - loop_failure_signatures: checkpoint - .loop_failure_signatures - .clone() - .into_iter() - .map(|(signature, count)| (signature.to_string(), count)) - .collect(), - restart_failure_signatures: checkpoint - .restart_failure_signatures - .clone() - .into_iter() - .map(|(signature, count)| (signature.to_string(), count)) - .collect(), - node_visits: checkpoint.node_visits.clone().into_iter().collect(), - diff: None, - }) + append_event( + run_store, + &run_id, + &Event::CheckpointCompleted { + node_id: checkpoint.current_node.clone(), + status: "success".to_string(), + current_node: checkpoint.current_node.clone(), + completed_nodes: checkpoint.completed_nodes.clone(), + node_retries: checkpoint.node_retries.clone().into_iter().collect(), + context_values: checkpoint.context_values.clone().into_iter().collect(), + node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(), + next_node_id: checkpoint.next_node_id.clone(), + git_commit_sha: checkpoint.git_commit_sha.clone(), + loop_failure_signatures: checkpoint + .loop_failure_signatures + .clone() + .into_iter() + .map(|(signature, count)| (signature.to_string(), count)) + .collect(), + restart_failure_signatures: checkpoint + .restart_failure_signatures + .clone() + .into_iter() + .map(|(signature, count)| (signature.to_string(), count)) + .collect(), + node_visits: checkpoint.node_visits.clone().into_iter().collect(), + diff: None, + }, + ) .await .unwrap(); } @@ -529,14 +545,18 @@ mod tests { node: &StageId, text: &str, ) { - append_event(run_store, &run_id, &Event::Prompt { - stage: node.node_id().to_string(), - visit: node.visit(), - text: text.to_string(), - mode: None, - provider: None, - model: None, - }) + append_event( + run_store, + &run_id, + &Event::Prompt { + stage: node.node_id().to_string(), + visit: node.visit(), + text: text.to_string(), + mode: None, + provider: None, + model: None, + }, + ) .await .unwrap(); } diff --git a/lib/crates/fabro-workflow/src/operations/resume.rs b/lib/crates/fabro-workflow/src/operations/resume.rs index f16bff91d..3afb187dc 100644 --- a/lib/crates/fabro-workflow/src/operations/resume.rs +++ b/lib/crates/fabro-workflow/src/operations/resume.rs @@ -1,22 +1,22 @@ use std::path::Path; use super::start::{StartServices, Started, execute_persisted_run}; -use crate::error::FabroError; +use crate::error::Error; use crate::event::{Event, append_event_to_sink}; use crate::outcome::StageStatus; use crate::run_status::RunStatus; /// Resume a workflow run from its checkpoint. Errors if no checkpoint is found. -pub async fn resume(run_dir: &Path, services: StartServices) -> Result { +pub async fn resume(run_dir: &Path, services: StartServices) -> Result { let state = services .run_store .state() .await - .map_err(|err| FabroError::engine(err.to_string()))?; + .map_err(|err| Error::engine(err.to_string()))?; if let Some(record) = state.status { if record.status == RunStatus::Succeeded { - return Err(FabroError::Precondition( + return Err(Error::Precondition( "run already finished successfully — nothing to resume".to_string(), )); } @@ -26,7 +26,7 @@ pub async fn resume(run_dir: &Path, services: StartServices) -> Result Result Result, + pub run_commit_sha: Option, } #[derive(Debug, Clone)] pub struct RunTimeline { - pub entries: Vec, + pub entries: Vec, pub parallel_map: HashMap, } @@ -116,7 +116,7 @@ impl RunTimeline { pub struct RewindInput { pub run_id: RunId, pub target: RewindTarget, - pub push: bool, + pub push: bool, } pub fn build_timeline(store: &Store, run_id: &str) -> Result { @@ -157,7 +157,7 @@ pub fn build_timeline(store: &Store, run_id: &str) -> Result { backfill_run_shas(store, run_id, &mut timeline); Ok(RunTimeline { - entries: timeline, + entries: timeline, parallel_map: load_parallel_map(store, run_id), }) } @@ -419,27 +419,27 @@ mod tests { #[test] fn resolve_latest_visit() { let timeline = RunTimeline { - entries: vec![ + entries: vec![ TimelineEntry { - ordinal: 1, - node_name: "start".to_string(), - visit: 1, + ordinal: 1, + node_name: "start".to_string(), + visit: 1, metadata_commit_oid: Oid::zero(), - run_commit_sha: Some("aaa".to_string()), + run_commit_sha: Some("aaa".to_string()), }, TimelineEntry { - ordinal: 2, - node_name: "build".to_string(), - visit: 1, + ordinal: 2, + node_name: "build".to_string(), + visit: 1, metadata_commit_oid: Oid::zero(), - run_commit_sha: Some("bbb".to_string()), + run_commit_sha: Some("bbb".to_string()), }, TimelineEntry { - ordinal: 3, - node_name: "build".to_string(), - visit: 2, + ordinal: 3, + node_name: "build".to_string(), + visit: 2, metadata_commit_oid: Oid::zero(), - run_commit_sha: Some("ccc".to_string()), + run_commit_sha: Some("ccc".to_string()), }, ], parallel_map: HashMap::new(), @@ -476,13 +476,13 @@ mod tests { graph.nodes.insert("a".to_string(), a); graph.edges.push(fabro_graphviz::graph::Edge { - from: "parallel1".to_string(), - to: "a".to_string(), + from: "parallel1".to_string(), + to: "a".to_string(), attrs: HashMap::new(), }); graph.edges.push(fabro_graphviz::graph::Edge { - from: "a".to_string(), - to: "fan_in1".to_string(), + from: "a".to_string(), + to: "fan_in1".to_string(), attrs: HashMap::new(), }); @@ -508,11 +508,14 @@ mod tests { bs.write_entry("checkpoint.json", &cp2, "checkpoint") .unwrap(); - rewind(&store, &RewindInput { - run_id: fixtures::RUN_1, - target: RewindTarget::Ordinal(1), - push: false, - }) + rewind( + &store, + &RewindInput { + run_id: fixtures::RUN_1, + target: RewindTarget::Ordinal(1), + push: false, + }, + ) .unwrap(); let resolved = store.resolve_ref(&branch).unwrap().unwrap(); diff --git a/lib/crates/fabro-workflow/src/operations/source.rs b/lib/crates/fabro-workflow/src/operations/source.rs index 40553e277..d6728e871 100644 --- a/lib/crates/fabro-workflow/src/operations/source.rs +++ b/lib/crates/fabro-workflow/src/operations/source.rs @@ -13,7 +13,7 @@ use crate::workflow_bundle::BundledWorkflow; pub enum WorkflowInput { Path(PathBuf), DotSource { - source: String, + source: String, base_dir: Option, }, Bundled(BundledWorkflow), @@ -23,20 +23,20 @@ pub enum WorkflowInput { pub(crate) struct ResolveWorkflowInput { pub workflow: WorkflowInput, pub settings: SettingsLayer, - pub cwd: PathBuf, + pub cwd: PathBuf, } #[derive(Clone)] pub(crate) struct ResolvedWorkflow { - pub raw_source: String, - pub settings: SettingsLayer, - pub workflow_slug: Option, + pub raw_source: String, + pub settings: SettingsLayer, + pub workflow_slug: Option, pub workflow_toml_path: Option, - pub dot_path: Option, - pub current_dir: Option, - pub file_resolver: Option>, - pub goal_override: Option, - pub working_directory: PathBuf, + pub dot_path: Option, + pub current_dir: Option, + pub file_resolver: Option>, + pub goal_override: Option, + pub working_directory: PathBuf, } fn workflow_slug_from_path(workflow_path: &Path) -> Option { @@ -155,7 +155,7 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let resolved = resolve_workflow(ResolveWorkflowInput { workflow: WorkflowInput::DotSource { - source: "digraph Test { start -> exit }".to_string(), + source: "digraph Test { start -> exit }".to_string(), base_dir: None, }, settings: SettingsLayer { @@ -165,7 +165,7 @@ mod tests { }), ..SettingsLayer::default() }, - cwd: dir.path().to_path_buf(), + cwd: dir.path().to_path_buf(), }) .unwrap(); diff --git a/lib/crates/fabro-workflow/src/operations/start.rs b/lib/crates/fabro-workflow/src/operations/start.rs index a88afaac4..03ef1d587 100644 --- a/lib/crates/fabro-workflow/src/operations/start.rs +++ b/lib/crates/fabro-workflow/src/operations/start.rs @@ -29,7 +29,7 @@ use tokio::runtime::Handle; use crate::artifact_upload::ArtifactSink; use crate::context::Context; -use crate::error::FabroError; +use crate::error::Error; use crate::event::{ Emitter, Event, EventBody, RunEventLogger, RunEventSink, RunNoticeLevel, append_event_to_sink, }; @@ -49,74 +49,74 @@ use crate::runtime_store::RunStoreHandle; use crate::workflow_bundle::{RunDefinition, WorkflowBundle}; struct RunSession { - cancel_token: Option>, - emitter: Arc, - sandbox: SandboxSpec, - llm: LlmSpec, - interviewer: Arc, - on_node: crate::OnNodeCallback, - lifecycle: LifecycleOptions, - hooks: fabro_hooks::HookSettings, - sandbox_env: SandboxEnvSpec, - devcontainer: Option, - seed_context: Option, - run_store: RunStoreHandle, - event_sink: RunEventSink, - artifact_sink: Option, - git: Option, - github_app: Option, - worktree_mode: Option, + cancel_token: Option>, + emitter: Arc, + sandbox: SandboxSpec, + llm: LlmSpec, + interviewer: Arc, + on_node: crate::OnNodeCallback, + lifecycle: LifecycleOptions, + hooks: fabro_hooks::HookSettings, + sandbox_env: SandboxEnvSpec, + devcontainer: Option, + seed_context: Option, + run_store: RunStoreHandle, + event_sink: RunEventSink, + artifact_sink: Option, + git: Option, + github_app: Option, + worktree_mode: Option, registry_override: Option>, - retro_enabled: bool, - preserve_sandbox: bool, - pr_config: Option, - pr_github_app: Option, - pr_origin_url: Option, - pr_model: String, - workflow_path: Option, - workflow_bundle: Option>, - run_control: Option>, + retro_enabled: bool, + preserve_sandbox: bool, + pr_config: Option, + pr_github_app: Option, + pr_origin_url: Option, + pr_model: String, + workflow_path: Option, + workflow_bundle: Option>, + run_control: Option>, } pub struct StartServices { - pub run_id: RunId, - pub cancel_token: Option>, - pub emitter: Arc, - pub interviewer: Arc, - pub run_store: RunStoreHandle, - pub event_sink: RunEventSink, - pub artifact_sink: Option, - pub run_control: Option>, - pub github_app: Option, - pub on_node: crate::OnNodeCallback, + pub run_id: RunId, + pub cancel_token: Option>, + pub emitter: Arc, + pub interviewer: Arc, + pub run_store: RunStoreHandle, + pub event_sink: RunEventSink, + pub artifact_sink: Option, + pub run_control: Option>, + pub github_app: Option, + pub on_node: crate::OnNodeCallback, pub registry_override: Option>, } pub struct Started { - pub finalized: Finalized, - pub final_context: Option, - pub retro: Option, + pub finalized: Finalized, + pub final_context: Option, + pub retro: Option, pub retro_duration: Duration, } /// Start a fresh workflow run. Errors if a checkpoint already exists (use /// `resume()` instead). -pub async fn start(run_dir: &Path, services: StartServices) -> Result { - std::fs::create_dir_all(run_dir).map_err(|err| FabroError::Io(err.to_string()))?; +pub async fn start(run_dir: &Path, services: StartServices) -> Result { + std::fs::create_dir_all(run_dir).map_err(|err| Error::Io(err.to_string()))?; let state = services .run_store .state() .await - .map_err(|err| FabroError::engine(err.to_string()))?; + .map_err(|err| Error::engine(err.to_string()))?; if state.checkpoint.is_some() { - return Err(FabroError::Precondition( + return Err(Error::Precondition( "checkpoint already exists in the run store — did you mean to resume?".to_string(), )); } if let Some(record) = state.status { if !matches!(record.status, RunStatus::Submitted | RunStatus::Starting) { - return Err(FabroError::Precondition(format!( + return Err(Error::Precondition(format!( "cannot start run: status is {:?}, expected submitted", record.status ))); @@ -130,13 +130,13 @@ pub(super) async fn execute_persisted_run( run_dir: &Path, checkpoint: Option, services: StartServices, -) -> Result { +) -> Result { let cancel_token = services.cancel_token.clone(); let run_id = services.run_id; let run_store = services.run_store.clone(); let event_sink = services.event_sink.clone(); if let Err(err) = run_store.state().await { - let error = FabroError::engine(err.to_string()); + let error = Error::engine(err.to_string()); let _ = persist_detached_failure( run_id, &event_sink, @@ -148,12 +148,16 @@ pub(super) async fn execute_persisted_run( .await; return Err(error); } - if let Err(err) = append_event_to_sink(&event_sink, &run_id, &Event::RunStarting { - reason: Some(StatusReason::SandboxInitializing), - }) + if let Err(err) = append_event_to_sink( + &event_sink, + &run_id, + &Event::RunStarting { + reason: Some(StatusReason::SandboxInitializing), + }, + ) .await { - let error = FabroError::engine(err.to_string()); + let error = Error::engine(err.to_string()); let _ = persist_detached_failure( run_id, &event_sink, @@ -235,10 +239,10 @@ async fn persist_terminal_engine_failure( run_store: &RunStoreHandle, event_sink: &RunEventSink, _run_dir: &Path, - error: &FabroError, + error: &Error, duration: Duration, ) { - let engine_result: Result = Err(error.clone()); + let engine_result: Result = Err(error.clone()); let (final_status, failure_reason, _run_status, status_reason) = classify_engine_result(&engine_result); let _conclusion = build_conclusion_from_store( @@ -249,12 +253,16 @@ async fn persist_terminal_engine_failure( None, ) .await; - if let Err(err) = append_event_to_sink(event_sink, &run_id, &Event::WorkflowRunFailed { - error: error.clone(), - duration_ms: u64::try_from(duration.as_millis()).unwrap(), - reason: status_reason, - git_commit_sha: None, - }) + if let Err(err) = append_event_to_sink( + event_sink, + &run_id, + &Event::WorkflowRunFailed { + error: error.clone(), + duration_ms: u64::try_from(duration.as_millis()).unwrap(), + reason: status_reason, + git_commit_sha: None, + }, + ) .await { tracing::warn!(error = %err, "Failed to append terminal engine failure event"); @@ -262,7 +270,7 @@ async fn persist_terminal_engine_failure( } impl RunSession { - async fn new(persisted: &Persisted, services: StartServices) -> Result { + async fn new(persisted: &Persisted, services: StartServices) -> Result { let record = persisted.run_record(); let settings = &record.settings; let working_directory = record.working_directory.clone(); @@ -270,11 +278,11 @@ impl RunSession { .run_store .state() .await - .map_err(|err| FabroError::engine(err.to_string()))?; + .map_err(|err| Error::engine(err.to_string()))?; let git = state.start.and_then(|start| { start.run_branch.as_ref().map(|_| GitCheckpointOptions { - base_sha: start.base_sha.clone(), - run_branch: start.run_branch.clone(), + base_sha: start.base_sha.clone(), + run_branch: start.run_branch.clone(), meta_branch: Some(MetadataStore::branch_name(&record.run_id.to_string())), }) }); @@ -296,7 +304,7 @@ impl RunSession { .unwrap_or((None, None)); let resolved = fabro_config::resolve_run_from_file(settings) - .map_err(|errors| FabroError::Precondition(render_resolve_errors(&errors)))?; + .map_err(|errors| Error::Precondition(render_resolve_errors(&errors)))?; let sandbox_provider = resolve_sandbox_provider(&resolved)?; let sandbox_provider = @@ -320,7 +328,7 @@ impl RunSession { .as_deref() .map(str::parse::) .transpose() - .map_err(|err| FabroError::Precondition(err.clone()))? + .map_err(|err| Error::Precondition(err.clone()))? .unwrap_or_else(Provider::default_from_env); let fallback_chain = resolve_fallback_chain(provider_enum, &model, &resolved.model); @@ -342,9 +350,9 @@ impl RunSession { }, }, SandboxProvider::Daytona => SandboxSpec::Daytona { - config: resolve_daytona_config(&resolved).unwrap_or_default(), - github_app: services.github_app.clone(), - run_id: Some(record.run_id), + config: resolve_daytona_config(&resolved).unwrap_or_default(), + github_app: services.github_app.clone(), + run_id: Some(record.run_id), clone_branch: detected_base_branch.or_else(|| record.base_branch.clone()), }, }; @@ -356,7 +364,7 @@ impl RunSession { .map(|(k, v)| (k.clone(), resolve_interp(v))) .collect(); let resolved_server = fabro_config::resolve_server_from_file(settings) - .map_err(|errors| FabroError::Precondition(render_resolve_errors(&errors)))?; + .map_err(|errors| Error::Precondition(render_resolve_errors(&errors)))?; let github_permissions: Option> = (!resolved_server.integrations.github.permissions.is_empty()).then(|| { resolved_server @@ -375,7 +383,7 @@ impl RunSession { }; let devcontainer = resolved.sandbox.devcontainer.then(|| DevcontainerSpec { - enabled: true, + enabled: true, resolve_dir: working_directory.clone(), }); @@ -404,9 +412,9 @@ impl RunSession { interviewer, on_node: services.on_node, lifecycle: LifecycleOptions { - setup_commands: resolved.prepare.commands.clone(), + setup_commands: resolved.prepare.commands.clone(), setup_command_timeout_ms: resolved.prepare.timeout_ms, - devcontainer_phases: Vec::new(), + devcontainer_phases: Vec::new(), }, hooks: fabro_hooks::HookSettings { hooks: resolved.hooks.iter().map(runtime_hook_definition).collect(), @@ -441,25 +449,25 @@ fn resolve_interp(value: &InterpString) -> String { async fn load_accepted_run_definition( run_store: &RunStoreHandle, blob_id: fabro_types::RunBlobId, -) -> Result { +) -> Result { let bytes = run_store .read_blob(&blob_id) .await - .map_err(|err| FabroError::engine(err.to_string()))? + .map_err(|err| Error::engine(err.to_string()))? .ok_or_else(|| { - FabroError::engine(format!( + Error::engine(format!( "run definition blob is missing from the run store: {blob_id}" )) })?; - serde_json::from_slice(&bytes).map_err(|err| FabroError::Parse(err.to_string())) + serde_json::from_slice(&bytes).map_err(|err| Error::Parse(err.to_string())) } -fn resolve_sandbox_provider(settings: &ResolvedRunSettings) -> Result { +fn resolve_sandbox_provider(settings: &ResolvedRunSettings) -> Result { Some(str::parse::( settings.sandbox.provider.as_str(), )) .transpose() - .map_err(|err| FabroError::Precondition(format!("Invalid sandbox provider: {err}")))? + .map_err(|err| Error::Precondition(format!("Invalid sandbox provider: {err}")))? .map_or_else(|| Ok(SandboxProvider::default()), Ok) } @@ -510,39 +518,39 @@ fn render_resolve_errors(errors: &[fabro_config::ResolveError]) -> String { fn runtime_mcp_server(settings: &ResolvedMcpServerSettings) -> McpServerSettings { McpServerSettings { - name: settings.name.clone(), - transport: match &settings.transport { + name: settings.name.clone(), + transport: match &settings.transport { ResolvedMcpTransport::Stdio { command, env } => McpTransport::Stdio { command: command.clone(), - env: env.clone(), + env: env.clone(), }, ResolvedMcpTransport::Http { url, headers } => McpTransport::Http { - url: url.clone(), + url: url.clone(), headers: headers.clone(), }, ResolvedMcpTransport::Sandbox { command, port, env } => McpTransport::Sandbox { command: command.clone(), - port: *port, - env: env.clone(), + port: *port, + env: env.clone(), }, }, startup_timeout_secs: settings.startup_timeout_secs, - tool_timeout_secs: settings.tool_timeout_secs, + tool_timeout_secs: settings.tool_timeout_secs, } } fn runtime_daytona_config(settings: &DaytonaSettings) -> DaytonaConfig { DaytonaConfig { auto_stop_interval: settings.auto_stop_interval, - labels: (!settings.labels.is_empty()).then_some(settings.labels.clone()), - snapshot: settings + labels: (!settings.labels.is_empty()).then_some(settings.labels.clone()), + snapshot: settings .snapshot .as_ref() .map(|snapshot| DaytonaSnapshotSettings { - name: snapshot.name.clone(), - cpu: snapshot.cpu, - memory: snapshot.memory_gb, - disk: snapshot.disk_gb, + name: snapshot.name.clone(), + cpu: snapshot.cpu, + memory: snapshot.memory_gb, + disk: snapshot.disk_gb, dockerfile: snapshot .dockerfile .as_ref() @@ -555,21 +563,21 @@ fn runtime_daytona_config(settings: &DaytonaSettings) -> DaytonaConfig { } }), }), - network: settings.network.as_ref().map(|network| match network { + network: settings.network.as_ref().map(|network| match network { DaytonaNetworkLayer::Block => DaytonaNetwork::Block, DaytonaNetworkLayer::AllowAll => DaytonaNetwork::AllowAll, DaytonaNetworkLayer::AllowList { allow_list } => { DaytonaNetwork::AllowList(allow_list.clone()) } }), - skip_clone: settings.skip_clone, + skip_clone: settings.skip_clone, } } fn runtime_hook_definition(definition: &ResolvedHookDefinition) -> fabro_hooks::HookDefinition { fabro_hooks::HookDefinition { - name: definition.name.clone(), - event: match definition.event { + name: definition.name.clone(), + event: match definition.event { ResolvedHookEvent::RunStart => fabro_hooks::HookEvent::RunStart, ResolvedHookEvent::RunComplete => fabro_hooks::HookEvent::RunComplete, ResolvedHookEvent::RunFailed => fabro_hooks::HookEvent::RunFailed, @@ -587,12 +595,12 @@ fn runtime_hook_definition(definition: &ResolvedHookDefinition) -> fabro_hooks:: ResolvedHookEvent::PostToolUse => fabro_hooks::HookEvent::PostToolUse, ResolvedHookEvent::PostToolUseFailure => fabro_hooks::HookEvent::PostToolUseFailure, }, - command: definition.command.clone(), - hook_type: definition.hook_type.as_ref().map(runtime_hook_type), - matcher: definition.matcher.clone(), - blocking: definition.blocking, + command: definition.command.clone(), + hook_type: definition.hook_type.as_ref().map(runtime_hook_type), + matcher: definition.matcher.clone(), + blocking: definition.blocking, timeout_ms: definition.timeout_ms, - sandbox: definition.sandbox, + sandbox: definition.sandbox, } } @@ -607,10 +615,10 @@ fn runtime_hook_type(hook_type: &ResolvedHookType) -> fabro_hooks::HookType { allowed_env_vars, tls, } => fabro_hooks::HookType::Http { - url: url.clone(), - headers: headers.clone(), + url: url.clone(), + headers: headers.clone(), allowed_env_vars: allowed_env_vars.clone(), - tls: match tls { + tls: match tls { ResolvedTlsMode::Verify => fabro_hooks::TlsMode::Verify, ResolvedTlsMode::NoVerify => fabro_hooks::TlsMode::NoVerify, ResolvedTlsMode::Off => fabro_hooks::TlsMode::Off, @@ -618,15 +626,15 @@ fn runtime_hook_type(hook_type: &ResolvedHookType) -> fabro_hooks::HookType { }, ResolvedHookType::Prompt { prompt, model } => fabro_hooks::HookType::Prompt { prompt: prompt.clone(), - model: model.clone(), + model: model.clone(), }, ResolvedHookType::Agent { prompt, model, max_tool_rounds, } => fabro_hooks::HookType::Agent { - prompt: prompt.clone(), - model: model.clone(), + prompt: prompt.clone(), + model: model.clone(), max_tool_rounds: *max_tool_rounds, }, } @@ -638,23 +646,23 @@ impl RunSession { self, persisted: Persisted, checkpoint: Option, - ) -> Result { + ) -> Result { let preserve_sandbox = self.preserve_sandbox; let on_node = self.on_node.clone(); let record = persisted.run_record(); let run_options = RunOptions { - settings: record.settings.clone(), - run_dir: persisted.run_dir().to_path_buf(), - cancel_token: self.cancel_token, - run_id: record.run_id, - labels: record.labels.clone(), - workflow_slug: record.workflow_slug.clone(), - github_app: self.github_app.clone(), - host_repo_path: record.host_repo_path.as_deref().map(PathBuf::from), - base_branch: record.base_branch.clone(), + settings: record.settings.clone(), + run_dir: persisted.run_dir().to_path_buf(), + cancel_token: self.cancel_token, + run_id: record.run_id, + labels: record.labels.clone(), + workflow_slug: record.workflow_slug.clone(), + github_app: self.github_app.clone(), + host_repo_path: record.host_repo_path.as_deref().map(PathBuf::from), + base_branch: record.base_branch.clone(), display_base_sha: None, - git: self.git.clone(), + git: self.git.clone(), }; let last_git_sha: Arc>> = Arc::new(Mutex::new(None)); @@ -754,21 +762,21 @@ impl RunSession { let retro_duration = retro_start.elapsed(); let finalize_opts = FinalizeOptions { - run_dir: retroed.run_options.run_dir.clone(), - run_id: retroed.run_options.run_id, - run_store: retroed.run_store.clone(), - workflow_name: retroed.graph.name.clone(), - hook_runner: retroed.hook_runner.clone(), + run_dir: retroed.run_options.run_dir.clone(), + run_id: retroed.run_options.run_id, + run_store: retroed.run_store.clone(), + workflow_name: retroed.graph.name.clone(), + hook_runner: retroed.hook_runner.clone(), preserve_sandbox: self.preserve_sandbox, - last_git_sha: last_git_sha.lock().unwrap().clone(), + last_git_sha: last_git_sha.lock().unwrap().clone(), }; let pr_opts = PullRequestOptions { - run_dir: retroed.run_options.run_dir.clone(), - run_store: retroed.run_store.clone(), - pr_config: self.pr_config, + run_dir: retroed.run_options.run_dir.clone(), + run_store: retroed.run_store.clone(), + pr_config: self.pr_config, github_app: self.pr_github_app, origin_url: self.pr_origin_url, - model: self.pr_model, + model: self.pr_model, }; let retro = retroed.retro.clone(); @@ -788,10 +796,10 @@ impl RunSession { } struct DetachedRunBootstrapGuard { - run_id: RunId, - event_sink: RunEventSink, + run_id: RunId, + event_sink: RunEventSink, cancel_token: Option>, - active: bool, + active: bool, } impl DetachedRunBootstrapGuard { @@ -830,12 +838,16 @@ impl Drop for DetachedRunBootstrapGuard { let event_sink = self.event_sink.clone(); if let Ok(handle) = Handle::try_current() { handle.spawn(async move { - let _ = append_event_to_sink(&event_sink, &run_id, &Event::WorkflowRunFailed { - error: FabroError::engine(format!("{reason:?}")), - duration_ms: 0, - reason: Some(reason), - git_commit_sha: None, - }) + let _ = append_event_to_sink( + &event_sink, + &run_id, + &Event::WorkflowRunFailed { + error: Error::engine(format!("{reason:?}")), + duration_ms: 0, + reason: Some(reason), + git_commit_sha: None, + }, + ) .await; }); } @@ -847,10 +859,10 @@ const POSTRUN_INTERRUPTED_MESSAGE: &str = "Run interrupted before post-run final const POSTRUN_CANCELLED_MESSAGE: &str = "Run cancelled before post-run finalization completed."; struct DetachedRunCompletionGuard { - event_sink: RunEventSink, - run_id: RunId, + event_sink: RunEventSink, + run_id: RunId, cancel_token: Option>, - active: bool, + active: bool, } impl DetachedRunCompletionGuard { @@ -897,18 +909,26 @@ impl Drop for DetachedRunCompletionGuard { let run_id = self.run_id; if let Ok(handle) = Handle::try_current() { handle.spawn(async move { - let _ = append_event_to_sink(&event_sink, &run_id, &Event::WorkflowRunFailed { - error: FabroError::engine(message.to_string()), - duration_ms: 0, - reason: Some(reason), - git_commit_sha: None, - }) + let _ = append_event_to_sink( + &event_sink, + &run_id, + &Event::WorkflowRunFailed { + error: Error::engine(message.to_string()), + duration_ms: 0, + reason: Some(reason), + git_commit_sha: None, + }, + ) .await; - let _ = append_event_to_sink(&event_sink, &run_id, &Event::RunNotice { - level: RunNoticeLevel::Error, - code: code.to_string(), - message: message.to_string(), - }) + let _ = append_event_to_sink( + &event_sink, + &run_id, + &Event::RunNotice { + level: RunNoticeLevel::Error, + code: code.to_string(), + message: message.to_string(), + }, + ) .await; }); } @@ -921,24 +941,28 @@ async fn persist_detached_failure( _run_dir: &Path, phase: &'static str, reason: StatusReason, - error: &FabroError, -) -> Result<(), FabroError> { + error: &Error, +) -> Result<(), Error> { let message = error.to_string(); - if let Err(err) = append_event_to_sink(event_sink, &run_id, &Event::WorkflowRunFailed { - error: error.clone(), - duration_ms: 0, - reason: Some(reason), - git_commit_sha: None, - }) + if let Err(err) = append_event_to_sink( + event_sink, + &run_id, + &Event::WorkflowRunFailed { + error: error.clone(), + duration_ms: 0, + reason: Some(reason), + git_commit_sha: None, + }, + ) .await { tracing::warn!(error = %err, "Failed to append detached failure event"); } let event = Event::RunNotice { - level: RunNoticeLevel::Error, - code: format!("{phase}_failed"), + level: RunNoticeLevel::Error, + code: format!("{phase}_failed"), message: message.clone(), }; if let Err(err) = append_event_to_sink(event_sink, &run_id, &event).await { @@ -990,35 +1014,38 @@ mod tests { async fn persisted_workflow(dot: &str, run_dir: &Path) -> (Persisted, Arc) { let store = memory_store(); - let created = crate::operations::create(&store, crate::operations::CreateRunInput { - workflow: crate::operations::WorkflowInput::DotSource { - source: dot.to_string(), - base_dir: None, - }, - settings: SettingsLayer { - run: Some(RunLayer { - execution: Some(RunExecutionLayer { - mode: Some(RunMode::DryRun), - ..RunExecutionLayer::default() + let created = crate::operations::create( + &store, + crate::operations::CreateRunInput { + workflow: crate::operations::WorkflowInput::DotSource { + source: dot.to_string(), + base_dir: None, + }, + settings: SettingsLayer { + run: Some(RunLayer { + execution: Some(RunExecutionLayer { + mode: Some(RunMode::DryRun), + ..RunExecutionLayer::default() + }), + ..RunLayer::default() }), - ..RunLayer::default() - }), - ..SettingsLayer::default() + ..SettingsLayer::default() + }, + cwd: run_dir + .parent() + .unwrap_or_else(|| Path::new(".")) + .to_path_buf(), + workflow_slug: Some("test".to_string()), + workflow_path: None, + workflow_bundle: None, + submitted_manifest_bytes: None, + run_id: Some(fixtures::RUN_1), + host_repo_path: None, + repo_origin_url: None, + base_branch: None, + provenance: None, }, - cwd: run_dir - .parent() - .unwrap_or_else(|| Path::new(".")) - .to_path_buf(), - workflow_slug: Some("test".to_string()), - workflow_path: None, - workflow_bundle: None, - submitted_manifest_bytes: None, - run_id: Some(fixtures::RUN_1), - host_repo_path: None, - repo_origin_url: None, - base_branch: None, - provenance: None, - }) + ) .await .unwrap(); (created.persisted, store) @@ -1136,9 +1163,11 @@ mod tests { let registry = Arc::new(test_registry()); let store = memory_store(); let workflow_bundle = WorkflowBundle::new(HashMap::from([ - (PathBuf::from("workflow.fabro"), BundledWorkflow { - logical_path: PathBuf::from("workflow.fabro"), - source: r#"digraph Root { + ( + PathBuf::from("workflow.fabro"), + BundledWorkflow { + logical_path: PathBuf::from("workflow.fabro"), + source: r#"digraph Root { graph [goal="Bundle child"] start [shape=Mdiamond] manager [ @@ -1150,49 +1179,56 @@ mod tests { exit [shape=Msquare] start -> manager -> exit }"# - .to_string(), - files: HashMap::new(), - }), - (PathBuf::from("children/review.fabro"), BundledWorkflow { - logical_path: PathBuf::from("children/review.fabro"), - source: r"digraph Review { + .to_string(), + files: HashMap::new(), + }, + ), + ( + PathBuf::from("children/review.fabro"), + BundledWorkflow { + logical_path: PathBuf::from("children/review.fabro"), + source: r"digraph Review { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }" - .to_string(), - files: HashMap::new(), - }), + .to_string(), + files: HashMap::new(), + }, + ), ])); - crate::operations::create(&store, crate::operations::CreateRunInput { - workflow: crate::operations::WorkflowInput::Bundled( - workflow_bundle - .workflow(Path::new("workflow.fabro")) - .unwrap() - .clone(), - ), - settings: SettingsLayer { - run: Some(RunLayer { - execution: Some(RunExecutionLayer { - mode: Some(RunMode::DryRun), - ..RunExecutionLayer::default() + crate::operations::create( + &store, + crate::operations::CreateRunInput { + workflow: crate::operations::WorkflowInput::Bundled( + workflow_bundle + .workflow(Path::new("workflow.fabro")) + .unwrap() + .clone(), + ), + settings: SettingsLayer { + run: Some(RunLayer { + execution: Some(RunExecutionLayer { + mode: Some(RunMode::DryRun), + ..RunExecutionLayer::default() + }), + ..RunLayer::default() }), - ..RunLayer::default() - }), - ..SettingsLayer::default() + ..SettingsLayer::default() + }, + cwd: temp.path().to_path_buf(), + workflow_slug: Some("bundle-child".to_string()), + workflow_path: Some(PathBuf::from("workflow.fabro")), + workflow_bundle: Some(workflow_bundle), + submitted_manifest_bytes: None, + run_id: Some(fixtures::RUN_1), + host_repo_path: None, + repo_origin_url: None, + base_branch: None, + provenance: None, }, - cwd: temp.path().to_path_buf(), - workflow_slug: Some("bundle-child".to_string()), - workflow_path: Some(PathBuf::from("workflow.fabro")), - workflow_bundle: Some(workflow_bundle), - submitted_manifest_bytes: None, - run_id: Some(fixtures::RUN_1), - host_repo_path: None, - repo_origin_url: None, - base_branch: None, - provenance: None, - }) + ) .await .unwrap(); @@ -1216,15 +1252,18 @@ mod tests { let (_persisted, store) = persisted_workflow(MINIMAL_DOT, &run_dir).await; - let started = start(&run_dir, StartServices { - on_node: Some(Arc::new({ - let visited = Arc::clone(&visited); - move |node_id: &str| { - visited.lock().unwrap().push(node_id.to_string()); - } - })), - ..test_start_services(&store, &run_dir, emitter, registry).await - }) + let started = start( + &run_dir, + StartServices { + on_node: Some(Arc::new({ + let visited = Arc::clone(&visited); + move |node_id: &str| { + visited.lock().unwrap().push(node_id.to_string()); + } + })), + ..test_start_services(&store, &run_dir, emitter, registry).await + }, + ) .await .unwrap(); @@ -1244,17 +1283,17 @@ mod tests { // Seed an authoritative checkpoint event so start() sees it let checkpoint = Checkpoint { - timestamp: chrono::Utc::now(), - current_node: "start".into(), - completed_nodes: vec!["start".to_string()], - node_retries: HashMap::new(), - context_values: Context::new().snapshot(), - node_outcomes: HashMap::new(), - next_node_id: Some("exit".to_string()), - git_commit_sha: None, - loop_failure_signatures: HashMap::new(), + timestamp: chrono::Utc::now(), + current_node: "start".into(), + completed_nodes: vec!["start".to_string()], + node_retries: HashMap::new(), + context_values: Context::new().snapshot(), + node_outcomes: HashMap::new(), + next_node_id: Some("exit".to_string()), + git_commit_sha: None, + loop_failure_signatures: HashMap::new(), restart_failure_signatures: HashMap::new(), - node_visits: HashMap::new(), + node_visits: HashMap::new(), }; crate::event::append_event( &store.open_run(&fixtures::RUN_1).await.unwrap(), @@ -1295,7 +1334,7 @@ mod tests { let result = start(&run_dir, services).await; assert!( - matches!(&result, Err(crate::error::FabroError::Precondition(_))), + matches!(&result, Err(crate::error::Error::Precondition(_))), "expected Precondition error, got: {result:?}", result = result.as_ref().map(|_| "Ok"), ); @@ -1317,7 +1356,7 @@ mod tests { .await; assert!( - matches!(&result, Err(crate::error::FabroError::Precondition(_))), + matches!(&result, Err(crate::error::Error::Precondition(_))), "expected Precondition error, got: {result:?}", result = result.as_ref().map(|_| "Ok"), ); @@ -1345,51 +1384,59 @@ mod tests { HashMap::new(), ); let conclusion = crate::records::Conclusion { - timestamp: Utc::now(), - status: StageStatus::Success, - duration_ms: 1, - failure_reason: None, + timestamp: Utc::now(), + status: StageStatus::Success, + duration_ms: 1, + failure_reason: None, final_git_commit_sha: None, - stages: vec![], - billing: None, - total_retries: 0, + stages: vec![], + billing: None, + total_retries: 0, }; let run_store = store.open_run(&fixtures::RUN_1).await.unwrap(); - crate::event::append_event(&run_store, &fixtures::RUN_1, &Event::CheckpointCompleted { - node_id: checkpoint.current_node.clone(), - status: "success".to_string(), - current_node: checkpoint.current_node.clone(), - completed_nodes: checkpoint.completed_nodes.clone(), - node_retries: checkpoint.node_retries.clone().into_iter().collect(), - context_values: checkpoint.context_values.clone().into_iter().collect(), - node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(), - next_node_id: checkpoint.next_node_id.clone(), - git_commit_sha: checkpoint.git_commit_sha.clone(), - loop_failure_signatures: checkpoint - .loop_failure_signatures - .iter() - .map(|(sig, count)| (sig.to_string(), *count)) - .collect(), - restart_failure_signatures: checkpoint - .restart_failure_signatures - .iter() - .map(|(sig, count)| (sig.to_string(), *count)) - .collect(), - node_visits: checkpoint.node_visits.clone().into_iter().collect(), - diff: None, - }) + crate::event::append_event( + &run_store, + &fixtures::RUN_1, + &Event::CheckpointCompleted { + node_id: checkpoint.current_node.clone(), + status: "success".to_string(), + current_node: checkpoint.current_node.clone(), + completed_nodes: checkpoint.completed_nodes.clone(), + node_retries: checkpoint.node_retries.clone().into_iter().collect(), + context_values: checkpoint.context_values.clone().into_iter().collect(), + node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(), + next_node_id: checkpoint.next_node_id.clone(), + git_commit_sha: checkpoint.git_commit_sha.clone(), + loop_failure_signatures: checkpoint + .loop_failure_signatures + .iter() + .map(|(sig, count)| (sig.to_string(), *count)) + .collect(), + restart_failure_signatures: checkpoint + .restart_failure_signatures + .iter() + .map(|(sig, count)| (sig.to_string(), *count)) + .collect(), + node_visits: checkpoint.node_visits.clone().into_iter().collect(), + diff: None, + }, + ) .await .unwrap(); - crate::event::append_event(&run_store, &fixtures::RUN_1, &Event::WorkflowRunCompleted { - duration_ms: conclusion.duration_ms, - artifact_count: 0, - status: "success".to_string(), - reason: None, - total_usd_micros: None, - final_git_commit_sha: None, - final_patch: None, - billing: None, - }) + crate::event::append_event( + &run_store, + &fixtures::RUN_1, + &Event::WorkflowRunCompleted { + duration_ms: conclusion.duration_ms, + artifact_count: 0, + status: "success".to_string(), + reason: None, + total_usd_micros: None, + final_git_commit_sha: None, + final_patch: None, + billing: None, + }, + ) .await .unwrap(); @@ -1400,7 +1447,7 @@ mod tests { .await; assert!( - matches!(&result, Err(crate::error::FabroError::Precondition(_))), + matches!(&result, Err(crate::error::Error::Precondition(_))), "expected Precondition error, got: {result:?}", result = result.as_ref().map(|_| "Ok"), ); diff --git a/lib/crates/fabro-workflow/src/operations/validate.rs b/lib/crates/fabro-workflow/src/operations/validate.rs index 720b6bce3..05597e567 100644 --- a/lib/crates/fabro-workflow/src/operations/validate.rs +++ b/lib/crates/fabro-workflow/src/operations/validate.rs @@ -4,14 +4,14 @@ use fabro_types::settings::SettingsLayer; use super::create::preprocess_and_validate; use super::source::{ResolveWorkflowInput, WorkflowInput, resolve_workflow}; -use crate::error::FabroError; +use crate::error::Error; use crate::pipeline::Validated; use crate::transforms::Transform; pub struct ValidateInput { - pub workflow: WorkflowInput, - pub settings: SettingsLayer, - pub cwd: PathBuf, + pub workflow: WorkflowInput, + pub settings: SettingsLayer, + pub cwd: PathBuf, pub custom_transforms: Vec>, } @@ -19,13 +19,13 @@ pub struct ValidateInput { /// /// Returns `Validated` even when validation produced errors. Call /// `validated.raise_on_errors()` if the caller wants to fail fast. -pub fn validate(input: ValidateInput) -> Result { +pub fn validate(input: ValidateInput) -> Result { let resolved = resolve_workflow(ResolveWorkflowInput { workflow: input.workflow, settings: input.settings, - cwd: input.cwd, + cwd: input.cwd, }) - .map_err(|err| FabroError::Parse(err.to_string()))?; + .map_err(|err| Error::Parse(err.to_string()))?; preprocess_and_validate( &resolved.raw_source, diff --git a/lib/crates/fabro-workflow/src/outcome.rs b/lib/crates/fabro-workflow/src/outcome.rs index ada74ab72..d5c1daf15 100644 --- a/lib/crates/fabro-workflow/src/outcome.rs +++ b/lib/crates/fabro-workflow/src/outcome.rs @@ -170,10 +170,10 @@ mod tests { #[test] fn billed_model_usage_from_llm_bills_anthropic_fast_mode_cache_write_pricing() { let usage = TokenCounts { - input_tokens: 100_000, - output_tokens: 10_000, - reasoning_tokens: 5_000, - cache_read_tokens: 20_000, + input_tokens: 100_000, + output_tokens: 10_000, + reasoning_tokens: 5_000, + cache_read_tokens: 20_000, cache_write_tokens: 30_000, }; let billed = billed_model_usage_from_llm( @@ -189,10 +189,10 @@ mod tests { #[test] fn billed_model_usage_round_trips_dense_token_counts() { let usage = TokenCounts { - input_tokens: 100, - output_tokens: 40, - reasoning_tokens: 5, - cache_read_tokens: 20, + input_tokens: 100, + output_tokens: 40, + reasoning_tokens: 5, + cache_read_tokens: 20, cache_write_tokens: 10, }; let billed = diff --git a/lib/crates/fabro-workflow/src/pipeline/execute.rs b/lib/crates/fabro-workflow/src/pipeline/execute.rs index 047627ec4..d9bcb1518 100644 --- a/lib/crates/fabro-workflow/src/pipeline/execute.rs +++ b/lib/crates/fabro-workflow/src/pipeline/execute.rs @@ -10,7 +10,7 @@ use tokio_util::sync::CancellationToken; use super::types::{Executed, Initialized}; use crate::artifact; use crate::context::{self, Context}; -use crate::error::FabroError; +use crate::error::Error; use crate::event::Event; use crate::graph::WorkflowGraph; use crate::handler::EngineServices; @@ -98,8 +98,8 @@ pub async fn execute(init: Initialized) -> Executed { let handler = Arc::new(WorkflowNodeHandler { services: shared_services, - run_dir: run_options.run_dir.clone(), - graph: Arc::clone(&graph_arc), + run_dir: run_options.run_dir.clone(), + graph: Arc::clone(&graph_arc), }); let settings_arc = Arc::new(run_options.clone()); @@ -132,7 +132,7 @@ pub async fn execute(init: Initialized) -> Executed { } let state = if let Some(ref cp) = checkpoint { - match ExecutionState::new(&wf_graph).map_err(|e| FabroError::engine(e.to_string())) { + match ExecutionState::new(&wf_graph).map_err(|e| Error::engine(e.to_string())) { Ok(mut s) => { for (k, v) in &cp.context_values { s.context.set(k.clone(), v.clone()); @@ -180,7 +180,7 @@ pub async fn execute(init: Initialized) -> Executed { } } } else if let Some(seed) = seed_context { - match ExecutionState::new(&wf_graph).map_err(|e| FabroError::engine(e.to_string())) { + match ExecutionState::new(&wf_graph).map_err(|e| Error::engine(e.to_string())) { Ok(s) => { for (k, v) in seed.snapshot() { s.context.set(k, v); @@ -205,7 +205,7 @@ pub async fn execute(init: Initialized) -> Executed { } } } else { - match ExecutionState::new(&wf_graph).map_err(|e| FabroError::engine(e.to_string())) { + match ExecutionState::new(&wf_graph).map_err(|e| Error::engine(e.to_string())) { Ok(s) => s, Err(err) => { return Executed { @@ -310,25 +310,25 @@ pub async fn execute(init: Initialized) -> Executed { }; (Ok(result), ctx) } - Err(fabro_core::CoreError::StallTimeout { node_id }) => { + Err(fabro_core::Error::StallTimeout { node_id }) => { let stall_timeout = graph.stall_timeout().unwrap_or_default(); let idle_secs = stall_timeout.as_secs(); emitter.emit(&Event::StallWatchdogTimeout { - node: node_id.clone(), + node: node_id.clone(), idle_seconds: idle_secs, }); ( - Err(FabroError::engine(format!( + Err(Error::engine(format!( "stall watchdog: node \"{node_id}\" had no activity for {idle_secs}s" ))), initial_context, ) } - Err(fabro_core::CoreError::Cancelled) => (Err(FabroError::Cancelled), initial_context), - Err(fabro_core::CoreError::Blocked { message }) => { - (Err(FabroError::engine(message)), initial_context) + Err(fabro_core::Error::Cancelled) => (Err(Error::Cancelled), initial_context), + Err(fabro_core::Error::Blocked { message }) => { + (Err(Error::engine(message)), initial_context) } - Err(e) => (Err(FabroError::engine(e.to_string())), initial_context), + Err(e) => (Err(Error::engine(e.to_string())), initial_context), }; let duration_ms = crate::millis_u64(start.elapsed()); diff --git a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs index 5fff59cbf..e2f326602 100644 --- a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs +++ b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs @@ -19,7 +19,7 @@ use object_store::memory::InMemory; use super::*; use crate::context::{self, Context}; -use crate::error::FabroError; +use crate::error::Error; use crate::event::{Emitter, StoreProgressLogger}; use crate::handler::start::StartHandler; use crate::handler::{Handler as HandlerTrait, HandlerRegistry}; @@ -88,17 +88,17 @@ fn test_emitter_arc(label: &str) -> Arc { fn test_run_options(run_dir: &Path, run_id: &str) -> RunOptions { RunOptions { - run_dir: run_dir.to_path_buf(), - cancel_token: None, - run_id: test_run_id(run_id), - settings: SettingsLayer::default(), - git: None, - host_repo_path: None, - labels: HashMap::new(), - github_app: None, - base_branch: None, + run_dir: run_dir.to_path_buf(), + cancel_token: None, + run_id: test_run_id(run_id), + settings: SettingsLayer::default(), + git: None, + host_repo_path: None, + labels: HashMap::new(), + github_app: None, + base_branch: None, display_base_sha: None, - workflow_slug: None, + workflow_slug: None, } } @@ -196,27 +196,27 @@ async fn execute_test_run_with_options( working_directory: std::env::current_dir().unwrap(), }, llm: LlmSpec { - model: "test-model".to_string(), - provider: fabro_llm::Provider::Anthropic, + model: "test-model".to_string(), + provider: fabro_llm::Provider::Anthropic, fallback_chain: Vec::new(), - mcp_servers: Vec::new(), - dry_run: true, + mcp_servers: Vec::new(), + dry_run: true, }, interviewer: Arc::new(AutoApproveInterviewer), lifecycle: LifecycleOptions { - setup_commands: vec![], + setup_commands: vec![], setup_command_timeout_ms: 1_000, - devcontainer_phases: vec![], + devcontainer_phases: vec![], }, run_options, workflow_path: None, workflow_bundle: None, hooks: HookSettings { hooks: vec![] }, sandbox_env: SandboxEnvSpec { - devcontainer_env: HashMap::new(), - toml_env: HashMap::new(), + devcontainer_env: HashMap::new(), + toml_env: HashMap::new(), github_permissions: None, - origin_url: None, + origin_url: None, }, devcontainer: None, git: git_options, @@ -245,44 +245,44 @@ async fn execute_runs_start_to_exit_and_returns_final_context() { let initialized = initialize( persisted_workflow(graph, source, &run_dir, test_run_id("run-test")), InitOptions { - run_id: test_run_id("run-test"), - run_store: test_run_store(&test_run_id("run-test")).await.into(), - dry_run: false, - emitter: test_emitter_arc("run-test"), - sandbox: SandboxSpec::Local { + run_id: test_run_id("run-test"), + run_store: test_run_store(&test_run_id("run-test")).await.into(), + dry_run: false, + emitter: test_emitter_arc("run-test"), + sandbox: SandboxSpec::Local { working_directory: std::env::current_dir().unwrap(), }, - llm: LlmSpec { - model: "test-model".to_string(), - provider: fabro_llm::Provider::Anthropic, + llm: LlmSpec { + model: "test-model".to_string(), + provider: fabro_llm::Provider::Anthropic, fallback_chain: Vec::new(), - mcp_servers: Vec::new(), - dry_run: true, + mcp_servers: Vec::new(), + dry_run: true, }, - interviewer: Arc::new(AutoApproveInterviewer), - lifecycle: LifecycleOptions { - setup_commands: vec![], + interviewer: Arc::new(AutoApproveInterviewer), + lifecycle: LifecycleOptions { + setup_commands: vec![], setup_command_timeout_ms: 1_000, - devcontainer_phases: vec![], + devcontainer_phases: vec![], }, - run_options: test_run_options(&run_dir, "run-test"), - workflow_path: None, - workflow_bundle: None, - hooks: HookSettings { hooks: vec![] }, - sandbox_env: SandboxEnvSpec { - devcontainer_env: HashMap::new(), - toml_env: HashMap::new(), + run_options: test_run_options(&run_dir, "run-test"), + workflow_path: None, + workflow_bundle: None, + hooks: HookSettings { hooks: vec![] }, + sandbox_env: SandboxEnvSpec { + devcontainer_env: HashMap::new(), + toml_env: HashMap::new(), github_permissions: None, - origin_url: None, + origin_url: None, }, - devcontainer: None, - git: None, - worktree_mode: None, - run_control: None, + devcontainer: None, + git: None, + worktree_mode: None, + run_control: None, registry_override: None, - artifact_sink: None, - checkpoint: None, - seed_context: None, + artifact_sink: None, + checkpoint: None, + seed_context: None, }, ) .await @@ -309,7 +309,7 @@ async fn run_with_lifecycle( graph: &Graph, run_options: RunOptions, lifecycle: LifecycleOptions, -) -> Result { +) -> Result { std::fs::create_dir_all(&run_options.run_dir).unwrap(); let run_dir = run_options.run_dir.clone(); let run_id = run_options.run_id; @@ -324,11 +324,11 @@ async fn run_with_lifecycle( working_directory: PathBuf::from(sandbox.working_directory()), }, llm: LlmSpec { - model: "test-model".to_string(), - provider: fabro_llm::Provider::Anthropic, + model: "test-model".to_string(), + provider: fabro_llm::Provider::Anthropic, fallback_chain: Vec::new(), - mcp_servers: Vec::new(), - dry_run: true, + mcp_servers: Vec::new(), + dry_run: true, }, interviewer: Arc::new(AutoApproveInterviewer), lifecycle, @@ -337,10 +337,10 @@ async fn run_with_lifecycle( workflow_bundle: None, hooks: HookSettings { hooks: vec![] }, sandbox_env: SandboxEnvSpec { - devcontainer_env: HashMap::new(), - toml_env: HashMap::new(), + devcontainer_env: HashMap::new(), + toml_env: HashMap::new(), github_permissions: None, - origin_url: None, + origin_url: None, }, devcontainer: None, git: None, @@ -367,7 +367,7 @@ impl HandlerTrait for AlwaysFailHandler { _graph: &Graph, _run_dir: &Path, _services: &crate::handler::EngineServices, - ) -> std::result::Result { + ) -> std::result::Result { Ok(Outcome::fail_classify("always fails")) } } @@ -385,7 +385,7 @@ impl HandlerTrait for SlowHandler { _graph: &Graph, _run_dir: &Path, _services: &crate::handler::EngineServices, - ) -> std::result::Result { + ) -> std::result::Result { tokio::time::sleep(Duration::from_millis(self.sleep_ms)).await; Ok(Outcome::success()) } @@ -402,7 +402,7 @@ impl HandlerTrait for PanickingHandler { _graph: &Graph, _run_dir: &Path, _services: &crate::handler::EngineServices, - ) -> std::result::Result { + ) -> std::result::Result { panic!("test panic message"); } } @@ -420,9 +420,9 @@ impl HandlerTrait for FailOnceThenSucceedHandler { _graph: &Graph, _run_dir: &Path, _services: &crate::handler::EngineServices, - ) -> std::result::Result { + ) -> std::result::Result { if self.call_count.fetch_add(1, Ordering::Relaxed) == 0 { - Err(FabroError::handler("transient failure")) + Err(Error::handler("transient failure")) } else { Ok(Outcome::success()) } @@ -641,8 +641,8 @@ async fn execute_writes_start_json_and_node_status() { let dir = tempfile::tempdir().unwrap(); let mut run_options = test_run_options(dir.path(), "test-run"); run_options.git = Some(GitCheckpointOptions { - base_sha: Some("abc123".into()), - run_branch: Some(format!("fabro/run/{}", test_run_id("test-run"))), + base_sha: Some("abc123".into()), + run_branch: Some(format!("fabro/run/{}", test_run_id("test-run"))), meta_branch: None, }); @@ -750,7 +750,7 @@ async fn execute_cancelled_mid_run() { &run_options, ) .await; - assert!(matches!(result, Err(FabroError::Cancelled))); + assert!(matches!(result, Err(Error::Cancelled))); } #[tokio::test] @@ -781,7 +781,7 @@ async fn execute_cancelled_mid_run_persists_cancelled_status() { let executed = execute_test_run_with_options(run_options, g, Some(Arc::new(registry))).await; - assert!(matches!(executed.outcome, Err(FabroError::Cancelled))); + assert!(matches!(executed.outcome, Err(Error::Cancelled))); let status = executed.run_store.state().await.unwrap().status.unwrap(); assert_eq!(status.status, RunStatus::Failed); assert_eq!(status.reason, Some(StatusReason::Cancelled)); diff --git a/lib/crates/fabro-workflow/src/pipeline/finalize.rs b/lib/crates/fabro-workflow/src/pipeline/finalize.rs index 3eac0df4b..b7705cb62 100644 --- a/lib/crates/fabro-workflow/src/pipeline/finalize.rs +++ b/lib/crates/fabro-workflow/src/pipeline/finalize.rs @@ -4,7 +4,7 @@ use fabro_hooks::{HookContext, HookEvent, HookRunner}; use fabro_types::BilledTokenCounts; use super::types::{Concluded, FinalizeOptions, Retroed}; -use crate::error::FabroError; +use crate::error::Error; use crate::event::{Emitter, Event, RunNoticeLevel}; use crate::git::MetadataStore; use crate::outcome::{Outcome, OutcomeExt, StageStatus}; @@ -29,7 +29,7 @@ fn emit_run_notice( } pub fn classify_engine_result( - engine_result: &Result, + engine_result: &Result, ) -> (StageStatus, Option, RunStatus, Option) { match engine_result { Ok(outcome) => { @@ -48,7 +48,7 @@ pub fn classify_engine_result( }; (status, failure_reason, run_status, status_reason) } - Err(FabroError::Cancelled) => ( + Err(Error::Cancelled) => ( StageStatus::Fail, Some("Cancelled".to_string()), RunStatus::Failed, @@ -222,11 +222,8 @@ async fn cleanup_sandbox( /// /// # Errors /// -/// Returns `FabroError` if persisting terminal state fails. -pub async fn finalize( - retroed: Retroed, - options: &FinalizeOptions, -) -> Result { +/// Returns `Error` if persisting terminal state fails. +pub async fn finalize(retroed: Retroed, options: &FinalizeOptions) -> Result { let Retroed { graph, outcome, @@ -322,17 +319,17 @@ mod tests { fn test_run_options(run_dir: &std::path::Path) -> RunOptions { RunOptions { - settings: SettingsLayer::default(), - run_dir: run_dir.to_path_buf(), - cancel_token: None, - run_id: test_run_id(), - labels: HashMap::new(), - workflow_slug: None, - github_app: None, - host_repo_path: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: run_dir.to_path_buf(), + cancel_token: None, + run_id: test_run_id(), + labels: HashMap::new(), + workflow_slug: None, + github_app: None, + host_repo_path: None, + base_branch: None, display_base_sha: None, - git: None, + git: None, } } @@ -368,15 +365,18 @@ mod tests { retro: None, }; - let concluded = finalize(retroed, &FinalizeOptions { - run_dir: run_dir.clone(), - run_id: test_run_id(), - run_store: run_store.clone().into(), - workflow_name: "test".to_string(), - hook_runner: None, - preserve_sandbox: true, - last_git_sha: None, - }) + let concluded = finalize( + retroed, + &FinalizeOptions { + run_dir: run_dir.clone(), + run_id: test_run_id(), + run_store: run_store.clone().into(), + workflow_name: "test".to_string(), + hook_runner: None, + preserve_sandbox: true, + last_git_sha: None, + }, + ) .await .unwrap(); store_logger.flush().await; diff --git a/lib/crates/fabro-workflow/src/pipeline/initialize.rs b/lib/crates/fabro-workflow/src/pipeline/initialize.rs index 7256fed7c..6592633da 100644 --- a/lib/crates/fabro-workflow/src/pipeline/initialize.rs +++ b/lib/crates/fabro-workflow/src/pipeline/initialize.rs @@ -20,7 +20,7 @@ use tokio::time::timeout as tokio_timeout; use super::types::{InitOptions, Initialized, LlmSpec, Persisted, SandboxEnvSpec}; use crate::devcontainer_bridge::{devcontainer_to_snapshot_config, run_devcontainer_lifecycle}; -use crate::error::FabroError; +use crate::error::Error; use crate::event::{Emitter, Event, RunNoticeLevel}; use crate::git::{self, GitSyncStatus, MetadataStore}; use crate::handler::llm::{AgentApiBackend, AgentCliBackend, BackendRouter}; @@ -28,9 +28,9 @@ use crate::handler::{HandlerRegistry, default_registry, sandbox_cancel_token}; use crate::run_options::GitCheckpointOptions; struct WorktreePlan { - branch_name: String, - base_sha: String, - worktree_path: PathBuf, + branch_name: String, + base_sha: String, + worktree_path: PathBuf, skip_branch_creation: bool, } @@ -59,9 +59,7 @@ fn emit_run_notice( }); } -async fn resolve_worktree_plan( - options: &mut InitOptions, -) -> Result, FabroError> { +async fn resolve_worktree_plan(options: &mut InitOptions) -> Result, Error> { let Some(worktree_mode) = options.worktree_mode else { options.run_options.display_base_sha = None; return Ok(None); @@ -72,10 +70,9 @@ async fn resolve_worktree_plan( if let (Some(run_branch), Some(base_sha)) = (&git.run_branch, &git.base_sha) { options.run_options.display_base_sha = Some(base_sha.clone()); return Ok(Some(WorktreePlan { - branch_name: run_branch.clone(), - base_sha: base_sha.clone(), - worktree_path: RunScratch::new(&options.run_options.run_dir) - .worktree_dir(), + branch_name: run_branch.clone(), + base_sha: base_sha.clone(), + worktree_path: RunScratch::new(&options.run_options.run_dir).worktree_dir(), skip_branch_creation: true, })); } @@ -207,15 +204,14 @@ async fn mint_github_token( creds: &fabro_github::GitHubAppCredentials, origin_url: &str, permissions: &HashMap, -) -> Result { +) -> Result { let https_url = fabro_github::ssh_url_to_https(origin_url); - let (owner, repo) = fabro_github::parse_github_owner_repo(&https_url) - .map_err(|e| FabroError::engine(e.clone()))?; + let (owner, repo) = + fabro_github::parse_github_owner_repo(&https_url).map_err(|e| Error::engine(e.clone()))?; let jwt = fabro_github::sign_app_jwt(&creds.app_id, &creds.private_key_pem) - .map_err(|e| FabroError::engine(e.clone()))?; + .map_err(|e| Error::engine(e.clone()))?; let client = reqwest::Client::new(); - let perms_json = - serde_json::to_value(permissions).map_err(|e| FabroError::engine(e.to_string()))?; + let perms_json = serde_json::to_value(permissions).map_err(|e| Error::engine(e.to_string()))?; fabro_github::create_installation_access_token_with_permissions( &client, &jwt, @@ -225,14 +221,14 @@ async fn mint_github_token( perms_json, ) .await - .map_err(|e| FabroError::engine(e.clone())) + .map_err(|e| Error::engine(e.clone())) } async fn build_sandbox_env( spec: &SandboxEnvSpec, github_app: Option<&fabro_github::GitHubAppCredentials>, emitter: &Emitter, -) -> Result, FabroError> { +) -> Result, Error> { let mut env = spec.devcontainer_env.clone(); env.extend(spec.toml_env.clone()); @@ -262,7 +258,7 @@ async fn build_registry( interviewer: Arc, sandbox_env: &HashMap, graph: &graph::Graph, -) -> Result<(Arc, Option, bool), FabroError> { +) -> Result<(Arc, Option, bool), Error> { let build_no_backend = || Arc::new(default_registry(Arc::clone(&interviewer), || None)); if spec.dry_run { @@ -277,7 +273,7 @@ async fn build_registry( match Client::from_env().await { Ok(client) if client.provider_names().is_empty() => { if graph_needs_llm { - return Err(FabroError::Precondition( + return Err(Error::Precondition( "No LLM providers configured. Set ANTHROPIC_API_KEY or OPENAI_API_KEY, or pass --dry-run to simulate.".to_string(), )); } @@ -300,7 +296,7 @@ async fn build_registry( } Err(e) => { if graph_needs_llm { - return Err(FabroError::Precondition(format!( + return Err(Error::Precondition(format!( "Failed to initialize LLM client: {e}. Set ANTHROPIC_API_KEY or OPENAI_API_KEY, or pass --dry-run to simulate.", ))); } @@ -309,7 +305,7 @@ async fn build_registry( } } -async fn resolve_devcontainer(options: &mut InitOptions) -> Result<(), FabroError> { +async fn resolve_devcontainer(options: &mut InitOptions) -> Result<(), Error> { let Some(devcontainer) = options.devcontainer.clone() else { return Ok(()); }; @@ -319,7 +315,7 @@ async fn resolve_devcontainer(options: &mut InitOptions) -> Result<(), FabroErro let config = fabro_devcontainer::DevcontainerResolver::resolve(&devcontainer.resolve_dir) .await - .map_err(|e| FabroError::engine(format!("Failed to resolve devcontainer: {e}")))?; + .map_err(|e| Error::engine(format!("Failed to resolve devcontainer: {e}")))?; let lifecycle_command_count = config.on_create_commands.len() + config.post_create_commands.len() @@ -361,12 +357,12 @@ async fn resolve_devcontainer(options: &mut InitOptions) -> Result<(), FabroErro ) .await .map_err(|_| { - FabroError::engine(format!( + Error::engine(format!( "Devcontainer initializeCommand timed out: {shell_command}" )) })? .map_err(|e| { - FabroError::engine(format!( + Error::engine(format!( "Failed to execute devcontainer initializeCommand: {shell_command}: {e}" )) })?; @@ -377,7 +373,7 @@ async fn resolve_devcontainer(options: &mut InitOptions) -> Result<(), FabroErro .code() .map_or_else(|| "unknown".to_string(), |code| code.to_string()); let stderr = String::from_utf8_lossy(&output.stderr); - return Err(FabroError::engine(format!( + return Err(Error::engine(format!( "Devcontainer initializeCommand failed (exit code {code}): {shell_command}\n{stderr}" ))); } @@ -403,7 +399,7 @@ async fn resolve_devcontainer(options: &mut InitOptions) -> Result<(), FabroErro pub async fn initialize( persisted: Persisted, mut options: InitOptions, -) -> Result { +) -> Result { let (graph, source, _diagnostics, run_dir, _run_record) = persisted.into_parts(); options.run_options.run_dir = run_dir.clone(); options.run_options.git = options.git.clone(); @@ -419,8 +415,8 @@ pub async fn initialize( let worktree_plan = resolve_worktree_plan(&mut options).await?; if let Some(plan) = worktree_plan.as_ref() { options.run_options.git = Some(GitCheckpointOptions { - base_sha: Some(plan.base_sha.clone()), - run_branch: Some(plan.branch_name.clone()), + base_sha: Some(plan.base_sha.clone()), + run_branch: Some(plan.branch_name.clone()), meta_branch: Some(MetadataStore::branch_name(&options.run_id.to_string())), }); } @@ -437,13 +433,16 @@ pub async fn initialize( .sandbox .build(Some(Arc::clone(&sandbox_event_callback))) .await - .map_err(|e| FabroError::engine(e.to_string()))?; - let mut worktree = WorktreeSandbox::new(inner, WorktreeOptions { - branch_name: plan.branch_name.clone(), - base_sha: plan.base_sha.clone(), - worktree_path: plan.worktree_path.to_string_lossy().into_owned(), - skip_branch_creation: plan.skip_branch_creation, - }); + .map_err(|e| Error::engine(e.to_string()))?; + let mut worktree = WorktreeSandbox::new( + inner, + WorktreeOptions { + branch_name: plan.branch_name.clone(), + base_sha: plan.base_sha.clone(), + worktree_path: plan.worktree_path.to_string_lossy().into_owned(), + skip_branch_creation: plan.skip_branch_creation, + }, + ); worktree.set_event_callback(Arc::clone(&options.emitter).worktree_callback()); match worktree.initialize().await { Ok(()) => { @@ -462,7 +461,7 @@ pub async fn initialize( .sandbox .build(Some(Arc::clone(&sandbox_event_callback))) .await - .map_err(|e| FabroError::engine(e.to_string()))?, + .map_err(|e| Error::engine(e.to_string()))?, )) } } @@ -472,7 +471,7 @@ pub async fn initialize( .sandbox .build(Some(Arc::clone(&sandbox_event_callback))) .await - .map_err(|e| FabroError::engine(e.to_string()))?, + .map_err(|e| Error::engine(e.to_string()))?, )) }; if worktree_plan.is_some() && !worktree_created { @@ -489,7 +488,7 @@ pub async fn initialize( sandbox .initialize() .await - .map_err(|e| FabroError::engine(format!("Failed to initialize sandbox: {e}")))?; + .map_err(|e| Error::engine(format!("Failed to initialize sandbox: {e}")))?; let hook_ctx = HookContext::new( HookEvent::SandboxReady, @@ -505,16 +504,16 @@ pub async fn initialize( .await; if let HookDecision::Block { reason } = decision { let msg = reason.unwrap_or_else(|| "blocked by SandboxReady hook".into()); - return Err(FabroError::engine(msg)); + return Err(Error::engine(msg)); } let sandbox_record = options.sandbox.to_sandbox_record(&*sandbox); options.emitter.emit(&Event::SandboxInitialized { - working_directory: sandbox_record.working_directory.clone(), - provider: sandbox_record.provider.clone(), - identifier: sandbox_record.identifier.clone(), + working_directory: sandbox_record.working_directory.clone(), + provider: sandbox_record.provider.clone(), + identifier: sandbox_record.identifier.clone(), host_working_directory: sandbox_record.host_working_directory.clone(), - container_mount_point: sandbox_record.container_mount_point.clone(), + container_mount_point: sandbox_record.container_mount_point.clone(), }); let env = build_sandbox_env( @@ -604,10 +603,10 @@ pub async fn initialize( cancel_token.clone(), ) .await - .map_err(|e| FabroError::engine(format!("Setup command failed: {e}")))?; + .map_err(|e| Error::engine(format!("Setup command failed: {e}")))?; if let Some(token) = &cancel_token { if token.is_cancelled() { - return Err(FabroError::Cancelled); + return Err(Error::Cancelled); } token.cancel(); } @@ -619,7 +618,7 @@ pub async fn initialize( exit_code: result.exit_code, stderr: result.stderr.clone(), }); - return Err(FabroError::engine(format!( + return Err(Error::engine(format!( "Setup command failed (exit code {}): {command}\n{}", result.exit_code, result.stderr, ))); @@ -740,17 +739,17 @@ mod tests { fn test_settings(run_dir: &std::path::Path) -> RunOptions { RunOptions { - settings: SettingsLayer::default(), - run_dir: run_dir.to_path_buf(), - cancel_token: None, - run_id: test_run_id(), - labels: HashMap::new(), - workflow_slug: None, - github_app: None, - host_repo_path: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: run_dir.to_path_buf(), + cancel_token: None, + run_id: test_run_id(), + labels: HashMap::new(), + workflow_slug: None, + github_app: None, + host_repo_path: None, + base_branch: None, display_base_sha: None, - git: None, + git: None, } } @@ -786,50 +785,53 @@ mod tests { let persisted = test_persisted(graph, source.clone(), &run_dir); let emitter = Arc::new(crate::event::Emitter::new(test_run_id())); - let initialized = initialize(persisted, InitOptions { - run_id: test_run_id(), - run_store: { - let store = memory_store(); - let inner = store.create_run(&test_run_id()).await.unwrap(); - inner.into() + let initialized = initialize( + persisted, + InitOptions { + run_id: test_run_id(), + run_store: { + let store = memory_store(); + let inner = store.create_run(&test_run_id()).await.unwrap(); + inner.into() + }, + dry_run: false, + emitter, + sandbox: SandboxSpec::Local { + working_directory: std::env::current_dir().unwrap(), + }, + llm: LlmSpec { + model: "test-model".to_string(), + provider: fabro_llm::Provider::Anthropic, + fallback_chain: Vec::new(), + mcp_servers: Vec::new(), + dry_run: true, + }, + interviewer: Arc::new(AutoApproveInterviewer), + lifecycle: crate::run_options::LifecycleOptions { + setup_commands: vec![], + setup_command_timeout_ms: 1_000, + devcontainer_phases: vec![], + }, + run_options: test_settings(&run_dir), + workflow_path: None, + workflow_bundle: None, + hooks: fabro_hooks::HookSettings { hooks: vec![] }, + sandbox_env: SandboxEnvSpec { + devcontainer_env: HashMap::new(), + toml_env: HashMap::from([("TEST_KEY".to_string(), "value".to_string())]), + github_permissions: None, + origin_url: None, + }, + devcontainer: None, + git: None, + worktree_mode: None, + run_control: None, + registry_override: None, + artifact_sink: None, + checkpoint: None, + seed_context: None, }, - dry_run: false, - emitter, - sandbox: SandboxSpec::Local { - working_directory: std::env::current_dir().unwrap(), - }, - llm: LlmSpec { - model: "test-model".to_string(), - provider: fabro_llm::Provider::Anthropic, - fallback_chain: Vec::new(), - mcp_servers: Vec::new(), - dry_run: true, - }, - interviewer: Arc::new(AutoApproveInterviewer), - lifecycle: crate::run_options::LifecycleOptions { - setup_commands: vec![], - setup_command_timeout_ms: 1_000, - devcontainer_phases: vec![], - }, - run_options: test_settings(&run_dir), - workflow_path: None, - workflow_bundle: None, - hooks: fabro_hooks::HookSettings { hooks: vec![] }, - sandbox_env: SandboxEnvSpec { - devcontainer_env: HashMap::new(), - toml_env: HashMap::from([("TEST_KEY".to_string(), "value".to_string())]), - github_permissions: None, - origin_url: None, - }, - devcontainer: None, - git: None, - worktree_mode: None, - run_control: None, - registry_override: None, - artifact_sink: None, - checkpoint: None, - seed_context: None, - }) + ) .await .unwrap(); @@ -864,46 +866,49 @@ mod tests { }); store_logger.register(&emitter); - let initialized = initialize(persisted, InitOptions { - run_id: test_run_id(), - run_store: run_store.into(), - dry_run: false, - emitter, - sandbox: SandboxSpec::Local { - working_directory: std::env::current_dir().unwrap(), + let initialized = initialize( + persisted, + InitOptions { + run_id: test_run_id(), + run_store: run_store.into(), + dry_run: false, + emitter, + sandbox: SandboxSpec::Local { + working_directory: std::env::current_dir().unwrap(), + }, + llm: LlmSpec { + model: "test-model".to_string(), + provider: fabro_llm::Provider::Anthropic, + fallback_chain: Vec::new(), + mcp_servers: Vec::new(), + dry_run: true, + }, + interviewer: Arc::new(AutoApproveInterviewer), + lifecycle: crate::run_options::LifecycleOptions { + setup_commands: vec!["true".to_string()], + setup_command_timeout_ms: 1_000, + devcontainer_phases: vec![], + }, + run_options: test_settings(&run_dir), + workflow_path: None, + workflow_bundle: None, + hooks: fabro_hooks::HookSettings { hooks: vec![] }, + sandbox_env: SandboxEnvSpec { + devcontainer_env: HashMap::new(), + toml_env: HashMap::new(), + github_permissions: None, + origin_url: None, + }, + devcontainer: None, + git: None, + worktree_mode: None, + run_control: None, + registry_override: None, + artifact_sink: None, + checkpoint: None, + seed_context: None, }, - llm: LlmSpec { - model: "test-model".to_string(), - provider: fabro_llm::Provider::Anthropic, - fallback_chain: Vec::new(), - mcp_servers: Vec::new(), - dry_run: true, - }, - interviewer: Arc::new(AutoApproveInterviewer), - lifecycle: crate::run_options::LifecycleOptions { - setup_commands: vec!["true".to_string()], - setup_command_timeout_ms: 1_000, - devcontainer_phases: vec![], - }, - run_options: test_settings(&run_dir), - workflow_path: None, - workflow_bundle: None, - hooks: fabro_hooks::HookSettings { hooks: vec![] }, - sandbox_env: SandboxEnvSpec { - devcontainer_env: HashMap::new(), - toml_env: HashMap::new(), - github_permissions: None, - origin_url: None, - }, - devcontainer: None, - git: None, - worktree_mode: None, - run_control: None, - registry_override: None, - artifact_sink: None, - checkpoint: None, - seed_context: None, - }) + ) .await .unwrap(); store_logger.flush().await; @@ -928,53 +933,56 @@ mod tests { let mut run_options = test_settings(&run_dir); run_options.cancel_token = Some(cancel_token); - let result = initialize(persisted, InitOptions { - run_id: test_run_id(), - run_store: { - let store = memory_store(); - let inner = store.create_run(&test_run_id()).await.unwrap(); - inner.into() + let result = initialize( + persisted, + InitOptions { + run_id: test_run_id(), + run_store: { + let store = memory_store(); + let inner = store.create_run(&test_run_id()).await.unwrap(); + inner.into() + }, + dry_run: false, + emitter: Arc::new(crate::event::Emitter::new(test_run_id())), + sandbox: SandboxSpec::Local { + working_directory: std::env::current_dir().unwrap(), + }, + llm: LlmSpec { + model: "test-model".to_string(), + provider: fabro_llm::Provider::Anthropic, + fallback_chain: Vec::new(), + mcp_servers: Vec::new(), + dry_run: true, + }, + interviewer: Arc::new(AutoApproveInterviewer), + lifecycle: crate::run_options::LifecycleOptions { + setup_commands: vec!["sleep 5".to_string()], + setup_command_timeout_ms: 5_000, + devcontainer_phases: vec![], + }, + run_options, + workflow_path: None, + workflow_bundle: None, + hooks: fabro_hooks::HookSettings { hooks: vec![] }, + sandbox_env: SandboxEnvSpec { + devcontainer_env: HashMap::new(), + toml_env: HashMap::new(), + github_permissions: None, + origin_url: None, + }, + devcontainer: None, + git: None, + worktree_mode: None, + run_control: None, + registry_override: None, + artifact_sink: None, + checkpoint: None, + seed_context: None, }, - dry_run: false, - emitter: Arc::new(crate::event::Emitter::new(test_run_id())), - sandbox: SandboxSpec::Local { - working_directory: std::env::current_dir().unwrap(), - }, - llm: LlmSpec { - model: "test-model".to_string(), - provider: fabro_llm::Provider::Anthropic, - fallback_chain: Vec::new(), - mcp_servers: Vec::new(), - dry_run: true, - }, - interviewer: Arc::new(AutoApproveInterviewer), - lifecycle: crate::run_options::LifecycleOptions { - setup_commands: vec!["sleep 5".to_string()], - setup_command_timeout_ms: 5_000, - devcontainer_phases: vec![], - }, - run_options, - workflow_path: None, - workflow_bundle: None, - hooks: fabro_hooks::HookSettings { hooks: vec![] }, - sandbox_env: SandboxEnvSpec { - devcontainer_env: HashMap::new(), - toml_env: HashMap::new(), - github_permissions: None, - origin_url: None, - }, - devcontainer: None, - git: None, - worktree_mode: None, - run_control: None, - registry_override: None, - artifact_sink: None, - checkpoint: None, - seed_context: None, - }) + ) .await; - assert!(matches!(result, Err(FabroError::Cancelled))); + assert!(matches!(result, Err(Error::Cancelled))); } #[tokio::test] @@ -988,54 +996,58 @@ mod tests { let mut run_options = test_settings(&run_dir); run_options.cancel_token = Some(cancel_token); - let result = initialize(persisted, InitOptions { - run_id: test_run_id(), - run_store: { - let store = memory_store(); - let inner = store.create_run(&test_run_id()).await.unwrap(); - inner.into() + let result = initialize( + persisted, + InitOptions { + run_id: test_run_id(), + run_store: { + let store = memory_store(); + let inner = store.create_run(&test_run_id()).await.unwrap(); + inner.into() + }, + dry_run: false, + emitter: Arc::new(crate::event::Emitter::new(test_run_id())), + sandbox: SandboxSpec::Local { + working_directory: std::env::current_dir().unwrap(), + }, + llm: LlmSpec { + model: "test-model".to_string(), + provider: fabro_llm::Provider::Anthropic, + fallback_chain: Vec::new(), + mcp_servers: Vec::new(), + dry_run: true, + }, + interviewer: Arc::new(AutoApproveInterviewer), + lifecycle: crate::run_options::LifecycleOptions { + setup_commands: vec![], + setup_command_timeout_ms: 5_000, + devcontainer_phases: vec![( + "on_create".to_string(), + vec![fabro_devcontainer::Command::Shell("sleep 5".to_string())], + )], + }, + run_options, + workflow_path: None, + workflow_bundle: None, + hooks: fabro_hooks::HookSettings { hooks: vec![] }, + sandbox_env: SandboxEnvSpec { + devcontainer_env: HashMap::new(), + toml_env: HashMap::new(), + github_permissions: None, + origin_url: None, + }, + devcontainer: None, + git: None, + worktree_mode: None, + run_control: None, + registry_override: None, + artifact_sink: None, + checkpoint: None, + seed_context: None, }, - dry_run: false, - emitter: Arc::new(crate::event::Emitter::new(test_run_id())), - sandbox: SandboxSpec::Local { - working_directory: std::env::current_dir().unwrap(), - }, - llm: LlmSpec { - model: "test-model".to_string(), - provider: fabro_llm::Provider::Anthropic, - fallback_chain: Vec::new(), - mcp_servers: Vec::new(), - dry_run: true, - }, - interviewer: Arc::new(AutoApproveInterviewer), - lifecycle: crate::run_options::LifecycleOptions { - setup_commands: vec![], - setup_command_timeout_ms: 5_000, - devcontainer_phases: vec![("on_create".to_string(), vec![ - fabro_devcontainer::Command::Shell("sleep 5".to_string()), - ])], - }, - run_options, - workflow_path: None, - workflow_bundle: None, - hooks: fabro_hooks::HookSettings { hooks: vec![] }, - sandbox_env: SandboxEnvSpec { - devcontainer_env: HashMap::new(), - toml_env: HashMap::new(), - github_permissions: None, - origin_url: None, - }, - devcontainer: None, - git: None, - worktree_mode: None, - run_control: None, - registry_override: None, - artifact_sink: None, - checkpoint: None, - seed_context: None, - }) + ) .await; - assert!(matches!(result, Err(FabroError::Cancelled))); + assert!(matches!(result, Err(Error::Cancelled))); } } diff --git a/lib/crates/fabro-workflow/src/pipeline/parse.rs b/lib/crates/fabro-workflow/src/pipeline/parse.rs index 08f18cc0c..95a8665b0 100644 --- a/lib/crates/fabro-workflow/src/pipeline/parse.rs +++ b/lib/crates/fabro-workflow/src/pipeline/parse.rs @@ -1,14 +1,14 @@ use fabro_graphviz::parser; use super::types::Parsed; -use crate::error::FabroError; +use crate::error::Error; /// PARSE phase: parse DOT source into a `Parsed` graph. /// /// # Errors /// -/// Returns `FabroError::Parse` if the DOT source is invalid. -pub fn parse(dot_source: &str) -> Result { +/// Returns `Error::Parse` if the DOT source is invalid. +pub fn parse(dot_source: &str) -> Result { let graph = parser::parse(dot_source)?; Ok(Parsed { graph, diff --git a/lib/crates/fabro-workflow/src/pipeline/persist.rs b/lib/crates/fabro-workflow/src/pipeline/persist.rs index 85885e6d7..aaa8d848c 100644 --- a/lib/crates/fabro-workflow/src/pipeline/persist.rs +++ b/lib/crates/fabro-workflow/src/pipeline/persist.rs @@ -1,7 +1,7 @@ use std::path::Path; use super::types::{PersistOptions, Persisted, Validated}; -use crate::error::FabroError; +use crate::error::Error; use crate::runtime_store::RunStoreHandle; /// PERSIST phase: create the run directory and return durable metadata for @@ -9,7 +9,7 @@ use crate::runtime_store::RunStoreHandle; pub(crate) fn persist( validated: Validated, mut options: PersistOptions, -) -> Result { +) -> Result { let (graph, source, diagnostics) = validated.into_parts(); options.run_record.graph = graph.clone(); @@ -27,14 +27,14 @@ pub(crate) fn persist( pub(crate) async fn load_from_store( run_store: &RunStoreHandle, run_dir: &Path, -) -> Result { +) -> Result { let state = run_store .state() .await - .map_err(|err| FabroError::engine(err.to_string()))?; + .map_err(|err| Error::engine(err.to_string()))?; let run_record = state .run - .ok_or_else(|| FabroError::Precondition("run record missing from store".to_string()))?; + .ok_or_else(|| Error::Precondition("run record missing from store".to_string()))?; let graph = run_record.graph.clone(); let source = state.graph_source.unwrap_or_default(); @@ -157,23 +157,27 @@ mod tests { async fn seeded_store(run_dir: &Path, record: &RunRecord, source: Option<&str>) -> RunDatabase { let store = memory_store(); let run_store = store.create_run(&record.run_id).await.unwrap(); - append_event(&run_store, &record.run_id, &Event::RunCreated { - run_id: record.run_id, - settings: serde_json::to_value(&record.settings).unwrap(), - graph: serde_json::to_value(&record.graph).unwrap(), - workflow_source: source.map(ToOwned::to_owned), - workflow_config: None, - labels: record.labels.clone().into_iter().collect(), - run_dir: run_dir.to_string_lossy().to_string(), - working_directory: record.working_directory.display().to_string(), - host_repo_path: record.host_repo_path.clone(), - repo_origin_url: record.repo_origin_url.clone(), - base_branch: record.base_branch.clone(), - workflow_slug: record.workflow_slug.clone(), - db_prefix: None, - provenance: record.provenance.clone(), - manifest_blob: None, - }) + append_event( + &run_store, + &record.run_id, + &Event::RunCreated { + run_id: record.run_id, + settings: serde_json::to_value(&record.settings).unwrap(), + graph: serde_json::to_value(&record.graph).unwrap(), + workflow_source: source.map(ToOwned::to_owned), + workflow_config: None, + labels: record.labels.clone().into_iter().collect(), + run_dir: run_dir.to_string_lossy().to_string(), + working_directory: record.working_directory.display().to_string(), + host_repo_path: record.host_repo_path.clone(), + repo_origin_url: record.repo_origin_url.clone(), + base_branch: record.base_branch.clone(), + workflow_slug: record.workflow_slug.clone(), + db_prefix: None, + provenance: record.provenance.clone(), + manifest_blob: None, + }, + ) .await .unwrap(); run_store @@ -187,7 +191,7 @@ mod tests { let persisted = persist( Validated::new(graph.clone(), source, vec![]), PersistOptions { - run_dir: run_dir.clone(), + run_dir: run_dir.clone(), run_record: sample_record(different_graph()), }, ) @@ -214,7 +218,7 @@ mod tests { let persisted = persist( Validated::new(graph.clone(), source, vec![]), PersistOptions { - run_dir: run_dir.clone(), + run_dir: run_dir.clone(), run_record: sample_record(different_graph()), }, ) @@ -239,7 +243,7 @@ mod tests { persist( Validated::new(graph, source.clone(), vec![]), PersistOptions { - run_dir: run_dir.clone(), + run_dir: run_dir.clone(), run_record: expected.clone(), }, ) @@ -279,13 +283,16 @@ mod tests { std::fs::write(&run_dir, "not a directory").unwrap(); let (graph, source) = graph_and_source(); - let err = persist(Validated::new(graph, source, vec![]), PersistOptions { - run_dir, - run_record: sample_record(different_graph()), - }) + let err = persist( + Validated::new(graph, source, vec![]), + PersistOptions { + run_dir, + run_record: sample_record(different_graph()), + }, + ) .unwrap_err(); - assert!(matches!(err, FabroError::Io(_))); + assert!(matches!(err, Error::Io(_))); } #[tokio::test] diff --git a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs index 583572a21..3010cac3f 100644 --- a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs +++ b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs @@ -540,14 +540,14 @@ pub async fn pull_request(concluded: Concluded, options: &PullRequestOptions) -> { Ok(Some(record)) => { emitter.emit(&Event::PullRequestCreated { - pr_url: record.html_url.clone(), - pr_number: record.number, - owner: record.owner.clone(), - repo: record.repo.clone(), + pr_url: record.html_url.clone(), + pr_number: record.number, + owner: record.owner.clone(), + repo: record.repo.clone(), base_branch: record.base_branch.clone(), head_branch: record.head_branch.clone(), - title: record.title.clone(), - draft: pr_cfg.draft, + title: record.title.clone(), + draft: pr_cfg.draft, }); pr_url = Some(record.html_url.clone()); } @@ -586,10 +586,9 @@ mod tests { use chrono::Utc; use fabro_graphviz::graph::Graph; use fabro_llm::client::Client; - use fabro_llm::error::SdkError; use fabro_llm::provider::{ProviderAdapter, StreamEventStream}; - use fabro_llm::set_default_client; use fabro_llm::types::{FinishReason, Message, Request, Response, StreamEvent, TokenCounts}; + use fabro_llm::{Error as LlmError, set_default_client}; use fabro_retro::retro::{ AggregateStats, FrictionKind, FrictionPoint, OpenItem, OpenItemKind, StageRetro, }; @@ -621,25 +620,25 @@ mod tests { "mock" } - async fn complete(&self, _request: &Request) -> Result { + async fn complete(&self, _request: &Request) -> Result { Ok(Response { - id: "resp_1".into(), - model: "mock-model".into(), - provider: "mock".into(), - message: Message::assistant(&self.response_text), + id: "resp_1".into(), + model: "mock-model".into(), + provider: "mock".into(), + message: Message::assistant(&self.response_text), finish_reason: FinishReason::Stop, - usage: TokenCounts { + usage: TokenCounts { input_tokens: 10, output_tokens: 20, ..Default::default() }, - raw: None, - warnings: vec![], - rate_limit: None, + raw: None, + warnings: vec![], + rate_limit: None, }) } - async fn stream(&self, _request: &Request) -> Result { + async fn stream(&self, _request: &Request) -> Result { let text = self.response_text.clone(); let events = vec![ Ok(StreamEvent::text_delta(&text, Some("t1".into()))), @@ -651,19 +650,19 @@ mod tests { ..Default::default() }, Response { - id: "resp_1".into(), - model: "mock-model".into(), - provider: "mock".into(), - message: Message::assistant(&text), + id: "resp_1".into(), + model: "mock-model".into(), + provider: "mock".into(), + message: Message::assistant(&text), finish_reason: FinishReason::Stop, - usage: TokenCounts { + usage: TokenCounts { input_tokens: 10, output_tokens: 20, ..Default::default() }, - raw: None, - warnings: vec![], - rate_limit: None, + raw: None, + warnings: vec![], + rate_limit: None, }, )), ]; @@ -694,109 +693,109 @@ mod tests { fn make_test_conclusion() -> Conclusion { Conclusion { - timestamp: Utc::now(), - status: crate::outcome::StageStatus::Success, - duration_ms: 150_000, - failure_reason: None, + timestamp: Utc::now(), + status: crate::outcome::StageStatus::Success, + duration_ms: 150_000, + failure_reason: None, final_git_commit_sha: None, - stages: vec![ + stages: vec![ StageSummary { - stage_id: "plan".to_string(), - stage_label: "plan".to_string(), - duration_ms: 45_000, + stage_id: "plan".to_string(), + stage_label: "plan".to_string(), + duration_ms: 45_000, billing_usd_micros: Some(120_000), - retries: 0, + retries: 0, }, StageSummary { - stage_id: "implement".to_string(), - stage_label: "implement".to_string(), - duration_ms: 90_000, + stage_id: "implement".to_string(), + stage_label: "implement".to_string(), + duration_ms: 90_000, billing_usd_micros: Some(250_000), - retries: 0, + retries: 0, }, StageSummary { - stage_id: "simplify".to_string(), - stage_label: "simplify".to_string(), - duration_ms: 15_000, + stage_id: "simplify".to_string(), + stage_label: "simplify".to_string(), + duration_ms: 15_000, billing_usd_micros: Some(50_000), - retries: 0, + retries: 0, }, ], - billing: Some(BilledTokenCounts { + billing: Some(BilledTokenCounts { total_usd_micros: Some(420_000), ..BilledTokenCounts::default() }), - total_retries: 0, + total_retries: 0, } } fn make_test_retro() -> Retro { Retro { - run_id: fixtures::RUN_1, - workflow_name: "implement".to_string(), - goal: "Fix the bug".to_string(), - timestamp: Utc::now(), - smoothness: None, - stages: vec![ + run_id: fixtures::RUN_1, + workflow_name: "implement".to_string(), + goal: "Fix the bug".to_string(), + timestamp: Utc::now(), + smoothness: None, + stages: vec![ StageRetro { - stage_id: "plan".to_string(), - stage_label: "plan".to_string(), - status: "success".to_string(), - duration_ms: 45_000, - retries: 0, + stage_id: "plan".to_string(), + stage_label: "plan".to_string(), + status: "success".to_string(), + duration_ms: 45_000, + retries: 0, billing_usd_micros: Some(120_000), - notes: None, - failure_reason: None, - files_touched: vec![], + notes: None, + failure_reason: None, + files_touched: vec![], }, StageRetro { - stage_id: "implement".to_string(), - stage_label: "implement".to_string(), - status: "success".to_string(), - duration_ms: 90_000, - retries: 0, + stage_id: "implement".to_string(), + stage_label: "implement".to_string(), + status: "success".to_string(), + duration_ms: 90_000, + retries: 0, billing_usd_micros: Some(250_000), - notes: None, - failure_reason: None, - files_touched: vec!["src/main.rs".to_string(), "src/lib.rs".to_string()], + notes: None, + failure_reason: None, + files_touched: vec!["src/main.rs".to_string(), "src/lib.rs".to_string()], }, StageRetro { - stage_id: "simplify".to_string(), - stage_label: "simplify".to_string(), - status: "success".to_string(), - duration_ms: 15_000, - retries: 0, + stage_id: "simplify".to_string(), + stage_label: "simplify".to_string(), + status: "success".to_string(), + duration_ms: 15_000, + retries: 0, billing_usd_micros: Some(50_000), - notes: None, - failure_reason: None, - files_touched: vec![], + notes: None, + failure_reason: None, + files_touched: vec![], }, ], - stats: AggregateStats { - total_duration_ms: 150_000, + stats: AggregateStats { + total_duration_ms: 150_000, total_billing_usd_micros: Some(420_000), - total_retries: 0, - files_touched: vec!["src/lib.rs".to_string(), "src/main.rs".to_string()], - stages_completed: 3, - stages_failed: 0, + total_retries: 0, + files_touched: vec!["src/lib.rs".to_string(), "src/main.rs".to_string()], + stages_completed: 3, + stages_failed: 0, }, - intent: None, - outcome: None, - learnings: None, + intent: None, + outcome: None, + learnings: None, friction_points: Some(vec![ FrictionPoint { - kind: FrictionKind::ToolFailure, + kind: FrictionKind::ToolFailure, description: "Daytona sandbox didn't have cargo on PATH".to_string(), - stage_id: None, + stage_id: None, }, FrictionPoint { - kind: FrictionKind::Timeout, + kind: FrictionKind::Timeout, description: "Proxy timeouts during cold compilations".to_string(), - stage_id: None, + stage_id: None, }, ]), - open_items: Some(vec![OpenItem { - kind: OpenItemKind::TechDebt, + open_items: Some(vec![OpenItem { + kind: OpenItemKind::TechDebt, description: "`ToolApprovalFn` type alias still exists".to_string(), }]), } @@ -835,25 +834,25 @@ mod tests { #[test] fn format_retro_section_empty_stats() { let retro = Retro { - run_id: fixtures::RUN_2, - workflow_name: "test".to_string(), - goal: "test".to_string(), - timestamp: Utc::now(), - smoothness: None, - stages: vec![], - stats: AggregateStats { - total_duration_ms: 0, + run_id: fixtures::RUN_2, + workflow_name: "test".to_string(), + goal: "test".to_string(), + timestamp: Utc::now(), + smoothness: None, + stages: vec![], + stats: AggregateStats { + total_duration_ms: 0, total_billing_usd_micros: None, - total_retries: 0, - files_touched: vec![], - stages_completed: 0, - stages_failed: 0, + total_retries: 0, + files_touched: vec![], + stages_completed: 0, + stages_failed: 0, }, - intent: None, - outcome: None, - learnings: None, + intent: None, + outcome: None, + learnings: None, friction_points: None, - open_items: None, + open_items: None, }; let section = format_retro_section(&retro); @@ -1084,43 +1083,51 @@ mod tests { let run_store = store.create_run(&fixtures::RUN_1).await.unwrap(); let run_record = RunRecord { - run_id: fixtures::RUN_1, - settings: SettingsLayer::default(), - graph: Graph::new("test"), - workflow_slug: Some("test".to_string()), + run_id: fixtures::RUN_1, + settings: SettingsLayer::default(), + graph: Graph::new("test"), + workflow_slug: Some("test".to_string()), working_directory: PathBuf::from("/tmp/project"), - host_repo_path: Some("/tmp/project".to_string()), - repo_origin_url: None, - base_branch: Some("main".to_string()), - labels: HashMap::new(), - provenance: None, - manifest_blob: None, - definition_blob: None, + host_repo_path: Some("/tmp/project".to_string()), + repo_origin_url: None, + base_branch: Some("main".to_string()), + labels: HashMap::new(), + provenance: None, + manifest_blob: None, + definition_blob: None, }; - append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated { - run_id: fixtures::RUN_1, - settings: serde_json::to_value(&run_record.settings).unwrap(), - graph: serde_json::to_value(&run_record.graph).unwrap(), - workflow_source: Some("digraph test { plan -> code }".to_string()), - workflow_config: None, - labels: run_record.labels.clone().into_iter().collect(), - run_dir: run_record.working_directory.display().to_string(), - working_directory: run_record.working_directory.display().to_string(), - host_repo_path: run_record.host_repo_path.clone(), - repo_origin_url: run_record.repo_origin_url.clone(), - base_branch: run_record.base_branch.clone(), - workflow_slug: run_record.workflow_slug.clone(), - db_prefix: None, - provenance: run_record.provenance.clone(), - manifest_blob: None, - }) + append_event( + &run_store, + &fixtures::RUN_1, + &Event::RunCreated { + run_id: fixtures::RUN_1, + settings: serde_json::to_value(&run_record.settings).unwrap(), + graph: serde_json::to_value(&run_record.graph).unwrap(), + workflow_source: Some("digraph test { plan -> code }".to_string()), + workflow_config: None, + labels: run_record.labels.clone().into_iter().collect(), + run_dir: run_record.working_directory.display().to_string(), + working_directory: run_record.working_directory.display().to_string(), + host_repo_path: run_record.host_repo_path.clone(), + repo_origin_url: run_record.repo_origin_url.clone(), + base_branch: run_record.base_branch.clone(), + workflow_slug: run_record.workflow_slug.clone(), + db_prefix: None, + provenance: run_record.provenance.clone(), + manifest_blob: None, + }, + ) .await .unwrap(); - append_event(&run_store, &fixtures::RUN_1, &Event::RetroCompleted { - duration_ms: 1, - response: Some(String::new()), - retro: Some(serde_json::to_value(make_test_retro()).unwrap()), - }) + append_event( + &run_store, + &fixtures::RUN_1, + &Event::RetroCompleted { + duration_ms: 1, + response: Some(String::new()), + retro: Some(serde_json::to_value(make_test_retro()).unwrap()), + }, + ) .await .unwrap(); @@ -1149,60 +1156,68 @@ mod tests { let run_store = store.create_run(&fixtures::RUN_1).await.unwrap(); let run_record = RunRecord { - run_id: fixtures::RUN_1, - settings: SettingsLayer::default(), - graph: Graph::new("test"), - workflow_slug: Some("test".to_string()), + run_id: fixtures::RUN_1, + settings: SettingsLayer::default(), + graph: Graph::new("test"), + workflow_slug: Some("test".to_string()), working_directory: PathBuf::from("/tmp/project"), - host_repo_path: Some("/tmp/project".to_string()), - repo_origin_url: None, - base_branch: Some("main".to_string()), - labels: HashMap::new(), - provenance: None, - manifest_blob: None, - definition_blob: None, + host_repo_path: Some("/tmp/project".to_string()), + repo_origin_url: None, + base_branch: Some("main".to_string()), + labels: HashMap::new(), + provenance: None, + manifest_blob: None, + definition_blob: None, }; - append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated { - run_id: fixtures::RUN_1, - settings: serde_json::to_value(&run_record.settings).unwrap(), - graph: serde_json::to_value(&run_record.graph).unwrap(), - workflow_source: Some("digraph test { plan -> code }".to_string()), - workflow_config: None, - labels: run_record.labels.clone().into_iter().collect(), - run_dir: run_record.working_directory.display().to_string(), - working_directory: run_record.working_directory.display().to_string(), - host_repo_path: run_record.host_repo_path.clone(), - repo_origin_url: run_record.repo_origin_url.clone(), - base_branch: run_record.base_branch.clone(), - workflow_slug: run_record.workflow_slug.clone(), - db_prefix: None, - provenance: run_record.provenance.clone(), - manifest_blob: None, - }) + append_event( + &run_store, + &fixtures::RUN_1, + &Event::RunCreated { + run_id: fixtures::RUN_1, + settings: serde_json::to_value(&run_record.settings).unwrap(), + graph: serde_json::to_value(&run_record.graph).unwrap(), + workflow_source: Some("digraph test { plan -> code }".to_string()), + workflow_config: None, + labels: run_record.labels.clone().into_iter().collect(), + run_dir: run_record.working_directory.display().to_string(), + working_directory: run_record.working_directory.display().to_string(), + host_repo_path: run_record.host_repo_path.clone(), + repo_origin_url: run_record.repo_origin_url.clone(), + base_branch: run_record.base_branch.clone(), + workflow_slug: run_record.workflow_slug.clone(), + db_prefix: None, + provenance: run_record.provenance.clone(), + manifest_blob: None, + }, + ) .await .unwrap(); - append_event(&run_store, &fixtures::RUN_1, &Event::StageCompleted { - node_id: "plan".to_string(), - name: "plan".to_string(), - index: 0, - duration_ms: 1, - status: "success".to_string(), - preferred_label: None, - suggested_next_ids: vec![], - billing: None, - failure: None, - notes: None, - files_touched: vec![], - context_updates: None, - jump_to_node: None, - context_values: None, - node_visits: None, - loop_failure_signatures: None, - restart_failure_signatures: None, - response: Some("Plan from store".to_string()), - attempt: 1, - max_attempts: 1, - }) + append_event( + &run_store, + &fixtures::RUN_1, + &Event::StageCompleted { + node_id: "plan".to_string(), + name: "plan".to_string(), + index: 0, + duration_ms: 1, + status: "success".to_string(), + preferred_label: None, + suggested_next_ids: vec![], + billing: None, + failure: None, + notes: None, + files_touched: vec![], + context_updates: None, + jump_to_node: None, + context_values: None, + node_visits: None, + loop_failure_signatures: None, + restart_failure_signatures: None, + response: Some("Plan from store".to_string()), + attempt: 1, + max_attempts: 1, + }, + ) .await .unwrap(); @@ -1340,7 +1355,7 @@ mod tests { let store = test_store(); let run_store = store.create_run(&fixtures::RUN_1).await.unwrap(); let creds = GitHubAppCredentials { - app_id: "123".to_string(), + app_id: "123".to_string(), private_key_pem: "unused".to_string(), }; let result = maybe_open_pull_request( @@ -1367,50 +1382,58 @@ mod tests { let store = test_store(); let run_store = store.create_run(&fixtures::RUN_1).await.unwrap(); let run_record = RunRecord { - run_id: fixtures::RUN_1, - settings: SettingsLayer::default(), - graph: Graph::new("test"), - workflow_slug: None, + run_id: fixtures::RUN_1, + settings: SettingsLayer::default(), + graph: Graph::new("test"), + workflow_slug: None, working_directory: tmp.path().to_path_buf(), - host_repo_path: None, - repo_origin_url: None, - base_branch: None, - labels: std::collections::HashMap::new(), - provenance: None, - manifest_blob: None, - definition_blob: None, + host_repo_path: None, + repo_origin_url: None, + base_branch: None, + labels: std::collections::HashMap::new(), + provenance: None, + manifest_blob: None, + definition_blob: None, }; - append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated { - run_id: fixtures::RUN_1, - settings: serde_json::to_value(&run_record.settings).unwrap(), - graph: serde_json::to_value(&run_record.graph).unwrap(), - workflow_source: None, - workflow_config: None, - labels: run_record.labels.clone().into_iter().collect(), - run_dir: run_record.working_directory.display().to_string(), - working_directory: tmp.path().display().to_string(), - host_repo_path: None, - repo_origin_url: run_record.repo_origin_url.clone(), - base_branch: None, - workflow_slug: None, - db_prefix: None, - provenance: run_record.provenance.clone(), - manifest_blob: None, - }) + append_event( + &run_store, + &fixtures::RUN_1, + &Event::RunCreated { + run_id: fixtures::RUN_1, + settings: serde_json::to_value(&run_record.settings).unwrap(), + graph: serde_json::to_value(&run_record.graph).unwrap(), + workflow_source: None, + workflow_config: None, + labels: run_record.labels.clone().into_iter().collect(), + run_dir: run_record.working_directory.display().to_string(), + working_directory: tmp.path().display().to_string(), + host_repo_path: None, + repo_origin_url: run_record.repo_origin_url.clone(), + base_branch: None, + workflow_slug: None, + db_prefix: None, + provenance: run_record.provenance.clone(), + manifest_blob: None, + }, + ) .await .unwrap(); - append_event(&run_store, &fixtures::RUN_1, &Event::WorkflowRunCompleted { - duration_ms: 1, - artifact_count: 0, - status: "success".to_string(), - reason: None, - total_usd_micros: None, - final_git_commit_sha: None, - final_patch: Some( - "diff --git a/src/lib.rs b/src/lib.rs\n+fn from_store() {}\n".to_string(), - ), - billing: None, - }) + append_event( + &run_store, + &fixtures::RUN_1, + &Event::WorkflowRunCompleted { + duration_ms: 1, + artifact_count: 0, + status: "success".to_string(), + reason: None, + total_usd_micros: None, + final_git_commit_sha: None, + final_patch: Some( + "diff --git a/src/lib.rs b/src/lib.rs\n+fn from_store() {}\n".to_string(), + ), + billing: None, + }, + ) .await .unwrap(); diff --git a/lib/crates/fabro-workflow/src/pipeline/retro.rs b/lib/crates/fabro-workflow/src/pipeline/retro.rs index 402c295c4..a67095c3d 100644 --- a/lib/crates/fabro-workflow/src/pipeline/retro.rs +++ b/lib/crates/fabro-workflow/src/pipeline/retro.rs @@ -16,7 +16,7 @@ pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option { tracing::warn!(error = %e, "Could not load run state, skipping retro"); if let Some(ref emitter) = options.emitter { emitter.emit(&Event::RetroFailed { - error: e.to_string(), + error: e.to_string(), duration_ms: 0, }); } @@ -27,7 +27,7 @@ pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option { tracing::warn!("Could not load checkpoint, skipping retro"); if let Some(ref emitter) = options.emitter { emitter.emit(&Event::RetroFailed { - error: "checkpoint not found".to_string(), + error: "checkpoint not found".to_string(), duration_ms: 0, }); } @@ -55,9 +55,9 @@ pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option { let retro_prompt = build_retro_prompt(RETRO_DATA_DIR); if let Some(ref emitter) = options.emitter { emitter.emit(&Event::RetroStarted { - prompt: Some(retro_prompt), + prompt: Some(retro_prompt), provider: Some(options.provider.as_str().to_string()), - model: Some(options.model.clone()), + model: Some(options.model.clone()), }); } @@ -71,10 +71,10 @@ pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option { emitter.touch(); if !event.event.is_streaming_noise() { emitter.emit(&Event::Agent { - stage: "retro".to_string(), - visit: 1, - event: event.event.clone(), - session_id: Some(event.session_id.clone()), + stage: "retro".to_string(), + visit: 1, + event: event.event.clone(), + session_id: Some(event.session_id.clone()), parent_session_id: event.parent_session_id.clone(), }); } @@ -86,7 +86,7 @@ pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option { tracing::warn!(error = %err, "Could not load events from store, skipping retro"); if let Some(ref emitter) = options.emitter { emitter.emit(&Event::RetroFailed { - error: err.to_string(), + error: err.to_string(), duration_ms: 0, }); } @@ -232,63 +232,71 @@ mod tests { let inner = test_store().create_run(&test_run_id()).await.unwrap(); let run_store = inner; let run_record = RunRecord { - run_id: test_run_id(), - settings: SettingsLayer::default(), - graph: Graph::new("test"), - workflow_slug: None, + run_id: test_run_id(), + settings: SettingsLayer::default(), + graph: Graph::new("test"), + workflow_slug: None, working_directory: run_dir.to_path_buf(), - host_repo_path: None, - repo_origin_url: None, - base_branch: None, - labels: std::collections::HashMap::new(), - provenance: None, - manifest_blob: None, - definition_blob: None, + host_repo_path: None, + repo_origin_url: None, + base_branch: None, + labels: std::collections::HashMap::new(), + provenance: None, + manifest_blob: None, + definition_blob: None, }; - append_event(&run_store, &test_run_id(), &Event::RunCreated { - run_id: test_run_id(), - settings: serde_json::to_value(&run_record.settings).unwrap(), - graph: serde_json::to_value(&run_record.graph).unwrap(), - workflow_source: None, - workflow_config: None, - labels: run_record.labels.clone().into_iter().collect(), - run_dir: run_dir.to_string_lossy().to_string(), - working_directory: run_dir.to_string_lossy().to_string(), - host_repo_path: None, - repo_origin_url: run_record.repo_origin_url.clone(), - base_branch: None, - workflow_slug: None, - db_prefix: None, - provenance: run_record.provenance.clone(), - manifest_blob: None, - }) + append_event( + &run_store, + &test_run_id(), + &Event::RunCreated { + run_id: test_run_id(), + settings: serde_json::to_value(&run_record.settings).unwrap(), + graph: serde_json::to_value(&run_record.graph).unwrap(), + workflow_source: None, + workflow_config: None, + labels: run_record.labels.clone().into_iter().collect(), + run_dir: run_dir.to_string_lossy().to_string(), + working_directory: run_dir.to_string_lossy().to_string(), + host_repo_path: None, + repo_origin_url: run_record.repo_origin_url.clone(), + base_branch: None, + workflow_slug: None, + db_prefix: None, + provenance: run_record.provenance.clone(), + manifest_blob: None, + }, + ) .await .unwrap(); - append_event(&run_store, &test_run_id(), &Event::CheckpointCompleted { - node_id: checkpoint.current_node.clone(), - status: "success".to_string(), - current_node: checkpoint.current_node.clone(), - completed_nodes: checkpoint.completed_nodes.clone(), - node_retries: checkpoint.node_retries.clone().into_iter().collect(), - context_values: checkpoint.context_values.clone().into_iter().collect(), - node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(), - next_node_id: checkpoint.next_node_id.clone(), - git_commit_sha: checkpoint.git_commit_sha.clone(), - loop_failure_signatures: checkpoint - .loop_failure_signatures - .clone() - .into_iter() - .map(|(signature, count)| (signature.to_string(), count)) - .collect(), - restart_failure_signatures: checkpoint - .restart_failure_signatures - .clone() - .into_iter() - .map(|(signature, count)| (signature.to_string(), count)) - .collect(), - node_visits: checkpoint.node_visits.clone().into_iter().collect(), - diff: None, - }) + append_event( + &run_store, + &test_run_id(), + &Event::CheckpointCompleted { + node_id: checkpoint.current_node.clone(), + status: "success".to_string(), + current_node: checkpoint.current_node.clone(), + completed_nodes: checkpoint.completed_nodes.clone(), + node_retries: checkpoint.node_retries.clone().into_iter().collect(), + context_values: checkpoint.context_values.clone().into_iter().collect(), + node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(), + next_node_id: checkpoint.next_node_id.clone(), + git_commit_sha: checkpoint.git_commit_sha.clone(), + loop_failure_signatures: checkpoint + .loop_failure_signatures + .clone() + .into_iter() + .map(|(signature, count)| (signature.to_string(), count)) + .collect(), + restart_failure_signatures: checkpoint + .restart_failure_signatures + .clone() + .into_iter() + .map(|(signature, count)| (signature.to_string(), count)) + .collect(), + node_visits: checkpoint.node_visits.clone().into_iter().collect(), + diff: None, + }, + ) .await .unwrap(); run_store @@ -296,17 +304,17 @@ mod tests { fn test_run_options(run_dir: &std::path::Path) -> RunOptions { RunOptions { - settings: SettingsLayer::default(), - run_dir: run_dir.to_path_buf(), - cancel_token: None, - run_id: test_run_id(), - labels: HashMap::new(), - workflow_slug: None, - github_app: None, - host_repo_path: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: run_dir.to_path_buf(), + cancel_token: None, + run_id: test_run_id(), + labels: HashMap::new(), + workflow_slug: None, + github_app: None, + host_repo_path: None, + base_branch: None, display_base_sha: None, - git: None, + git: None, } } @@ -325,35 +333,38 @@ mod tests { std::env::current_dir().unwrap(), )); let executed = Executed { - graph: Graph::new("test"), - outcome: Ok(crate::outcome::Outcome::success()), - run_options: test_run_options(&run_dir), - run_store: run_store.clone().into(), - hook_runner: None, - emitter: Arc::clone(&emitter), - sandbox: Arc::clone(&sandbox), - duration_ms: 1, + graph: Graph::new("test"), + outcome: Ok(crate::outcome::Outcome::success()), + run_options: test_run_options(&run_dir), + run_store: run_store.clone().into(), + hook_runner: None, + emitter: Arc::clone(&emitter), + sandbox: Arc::clone(&sandbox), + duration_ms: 1, final_context: Context::new(), - llm_client: None, - model: "test-model".to_string(), - provider: fabro_llm::Provider::Anthropic, + llm_client: None, + model: "test-model".to_string(), + provider: fabro_llm::Provider::Anthropic, }; - let retroed = retro(executed, &RetroOptions { - run_id: test_run_id(), - run_store: run_store.into(), - workflow_name: "test".to_string(), - goal: "Ship it".to_string(), - run_dir: run_dir.clone(), - sandbox, - emitter: Some(emitter), - failed: false, - run_duration_ms: 1, - enabled: true, - llm_client: None, - provider: fabro_llm::Provider::Anthropic, - model: "test-model".to_string(), - }) + let retroed = retro( + executed, + &RetroOptions { + run_id: test_run_id(), + run_store: run_store.into(), + workflow_name: "test".to_string(), + goal: "Ship it".to_string(), + run_dir: run_dir.clone(), + sandbox, + emitter: Some(emitter), + failed: false, + run_duration_ms: 1, + enabled: true, + llm_client: None, + provider: fabro_llm::Provider::Anthropic, + model: "test-model".to_string(), + }, + ) .await; store_logger.flush().await; @@ -376,21 +387,21 @@ mod tests { let retro = run_retro( &RetroOptions { - run_id: test_run_id(), - run_store: test_run_store(&run_dir, &checkpoint).await.into(), - workflow_name: "test".to_string(), - goal: "Ship it".to_string(), - run_dir: run_dir.clone(), - sandbox: Arc::new(fabro_agent::LocalSandbox::new( + run_id: test_run_id(), + run_store: test_run_store(&run_dir, &checkpoint).await.into(), + workflow_name: "test".to_string(), + goal: "Ship it".to_string(), + run_dir: run_dir.clone(), + sandbox: Arc::new(fabro_agent::LocalSandbox::new( std::env::current_dir().unwrap(), )), - emitter: Some(Arc::clone(&emitter)), - failed: false, + emitter: Some(Arc::clone(&emitter)), + failed: false, run_duration_ms: 1, - enabled: true, - llm_client: None, - provider: fabro_llm::Provider::Anthropic, - model: "test-model".to_string(), + enabled: true, + llm_client: None, + provider: fabro_llm::Provider::Anthropic, + model: "test-model".to_string(), }, true, ) diff --git a/lib/crates/fabro-workflow/src/pipeline/transform.rs b/lib/crates/fabro-workflow/src/pipeline/transform.rs index c119ee532..308f4fff8 100644 --- a/lib/crates/fabro-workflow/src/pipeline/transform.rs +++ b/lib/crates/fabro-workflow/src/pipeline/transform.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use super::types::{Parsed, TransformOptions, Transformed}; -use crate::error::FabroError; +use crate::error::Error; use crate::transforms::{ FileInliningTransform, ImportTransform, ModelResolutionTransform, StylesheetApplicationTransform, TemplateTransform, Transform, @@ -11,7 +11,7 @@ use crate::transforms::{ /// /// Returns `Transformed` with a graph for post-transform adjustments /// (e.g. goal override) before validation. -pub fn transform(parsed: Parsed, options: &TransformOptions) -> Result { +pub fn transform(parsed: Parsed, options: &TransformOptions) -> Result { let Parsed { graph, source } = parsed; // Built-in transforms (PreambleTransform moved to engine execution time) @@ -81,12 +81,15 @@ mod tests { start -> work -> exit }"#; let parsed = parse(dot).unwrap(); - let transformed = transform(parsed, &TransformOptions { - current_dir: None, - file_resolver: None, - inputs: HashMap::new(), - custom_transforms: vec![], - }) + let transformed = transform( + parsed, + &TransformOptions { + current_dir: None, + file_resolver: None, + inputs: HashMap::new(), + custom_transforms: vec![], + }, + ) .unwrap(); let prompt = transformed.graph.nodes["work"] .attrs @@ -106,12 +109,15 @@ mod tests { start -> work -> exit }"#; let parsed = parse(dot).unwrap(); - let transformed = transform(parsed, &TransformOptions { - current_dir: None, - file_resolver: None, - inputs: HashMap::new(), - custom_transforms: vec![], - }) + let transformed = transform( + parsed, + &TransformOptions { + current_dir: None, + file_resolver: None, + inputs: HashMap::new(), + custom_transforms: vec![], + }, + ) .unwrap(); assert_eq!( transformed.graph.nodes["work"].attrs.get("model"), @@ -134,12 +140,15 @@ mod tests { }"#, ) .unwrap(); - let transformed = transform(parsed, &TransformOptions { - current_dir: Some(dir.path().to_path_buf()), - file_resolver: Some(Arc::new(FilesystemFileResolver::new(None))), - inputs: HashMap::new(), - custom_transforms: vec![], - }) + let transformed = transform( + parsed, + &TransformOptions { + current_dir: Some(dir.path().to_path_buf()), + file_resolver: Some(Arc::new(FilesystemFileResolver::new(None))), + inputs: HashMap::new(), + custom_transforms: vec![], + }, + ) .unwrap(); assert_eq!( @@ -178,15 +187,18 @@ mod tests { }"#, ) .unwrap(); - let transformed = transform(parsed, &TransformOptions { - current_dir: Some(dir.path().to_path_buf()), - file_resolver: Some(Arc::new(FilesystemFileResolver::new(None))), - inputs: HashMap::from([( - "task".to_string(), - toml::Value::String("Launch".to_string()), - )]), - custom_transforms: vec![], - }) + let transformed = transform( + parsed, + &TransformOptions { + current_dir: Some(dir.path().to_path_buf()), + file_resolver: Some(Arc::new(FilesystemFileResolver::new(None))), + inputs: HashMap::from([( + "task".to_string(), + toml::Value::String("Launch".to_string()), + )]), + custom_transforms: vec![], + }, + ) .unwrap(); let lint = &transformed.graph.nodes["validate.lint"]; diff --git a/lib/crates/fabro-workflow/src/pipeline/types.rs b/lib/crates/fabro-workflow/src/pipeline/types.rs index 19d819329..0e85f6543 100644 --- a/lib/crates/fabro-workflow/src/pipeline/types.rs +++ b/lib/crates/fabro-workflow/src/pipeline/types.rs @@ -19,7 +19,7 @@ use fabro_validate::{Diagnostic, Severity}; use crate::artifact_upload::ArtifactSink; use crate::context::Context; -use crate::error::FabroError; +use crate::error::Error; use crate::event::Emitter; use crate::file_resolver::FileResolver; use crate::handler::HandlerRegistry; @@ -34,7 +34,7 @@ use crate::workflow_bundle::WorkflowBundle; /// Output of the PARSE phase. #[non_exhaustive] pub struct Parsed { - pub graph: Graph, + pub graph: Graph, pub source: String, } @@ -42,7 +42,7 @@ pub struct Parsed { /// post-transform adjustments (e.g. goal override) before validation. #[non_exhaustive] pub struct Transformed { - pub graph: Graph, + pub graph: Graph, pub source: String, } @@ -51,8 +51,8 @@ pub struct Transformed { /// Graph is read-only — use accessors, not direct field access. #[non_exhaustive] pub struct Validated { - graph: Graph, - source: String, + graph: Graph, + source: String, diagnostics: Vec, } @@ -86,10 +86,10 @@ impl Validated { .any(|d| d.severity == Severity::Error) } - /// Returns `Err(FabroError::Validation)` if any Error-severity diagnostics + /// Returns `Err(Error::Validation)` if any Error-severity diagnostics /// exist. Diagnostics remain accessible via `diagnostics()` for /// printing before this call. - pub fn raise_on_errors(&self) -> Result<(), FabroError> { + pub fn raise_on_errors(&self) -> Result<(), Error> { if self.has_errors() { let message = self .diagnostics @@ -98,7 +98,7 @@ impl Validated { .map(|d| d.message.as_str()) .collect::>() .join("; "); - return Err(FabroError::Validation(message)); + return Err(Error::Validation(message)); } Ok(()) } @@ -111,7 +111,7 @@ impl Validated { /// Options for the PERSIST phase. pub(crate) struct PersistOptions { - pub run_dir: PathBuf, + pub run_dir: PathBuf, pub run_record: RunRecord, } @@ -120,11 +120,11 @@ pub(crate) struct PersistOptions { #[derive(Debug)] #[non_exhaustive] pub struct Persisted { - graph: Graph, - source: String, + graph: Graph, + source: String, diagnostics: Vec, - run_dir: PathBuf, - run_record: RunRecord, + run_dir: PathBuf, + run_record: RunRecord, } impl Persisted { @@ -173,9 +173,9 @@ impl Persisted { .any(|d| d.severity == Severity::Error) } - /// Returns `Err(FabroError::Validation)` if any Error-severity diagnostics + /// Returns `Err(Error::Validation)` if any Error-severity diagnostics /// exist. - pub fn raise_on_errors(&self) -> Result<(), FabroError> { + pub fn raise_on_errors(&self) -> Result<(), Error> { if self.has_errors() { let message = self .diagnostics @@ -184,7 +184,7 @@ impl Persisted { .map(|d| d.message.as_str()) .collect::>() .join("; "); - return Err(FabroError::Validation(message)); + return Err(Error::Validation(message)); } Ok(()) } @@ -203,179 +203,179 @@ impl Persisted { pub async fn load_from_store( run_store: &RunStoreHandle, run_dir: &Path, - ) -> Result { + ) -> Result { super::persist::load_from_store(run_store, run_dir).await } } #[derive(Clone)] pub struct LlmSpec { - pub model: String, - pub provider: Provider, + pub model: String, + pub provider: Provider, pub fallback_chain: Vec, - pub mcp_servers: Vec, - pub dry_run: bool, + pub mcp_servers: Vec, + pub dry_run: bool, } #[derive(Clone)] pub struct SandboxEnvSpec { - pub devcontainer_env: HashMap, - pub toml_env: HashMap, + pub devcontainer_env: HashMap, + pub toml_env: HashMap, pub github_permissions: Option>, - pub origin_url: Option, + pub origin_url: Option, } #[derive(Clone)] pub struct DevcontainerSpec { - pub enabled: bool, + pub enabled: bool, pub resolve_dir: PathBuf, } pub struct InitOptions { - pub run_id: RunId, - pub run_store: RunStoreHandle, - pub dry_run: bool, - pub emitter: Arc, - pub sandbox: SandboxSpec, - pub llm: LlmSpec, - pub interviewer: Arc, - pub lifecycle: LifecycleOptions, - pub run_options: RunOptions, - pub workflow_path: Option, - pub workflow_bundle: Option>, - pub hooks: fabro_hooks::HookSettings, - pub sandbox_env: SandboxEnvSpec, - pub devcontainer: Option, - pub git: Option, - pub worktree_mode: Option, + pub run_id: RunId, + pub run_store: RunStoreHandle, + pub dry_run: bool, + pub emitter: Arc, + pub sandbox: SandboxSpec, + pub llm: LlmSpec, + pub interviewer: Arc, + pub lifecycle: LifecycleOptions, + pub run_options: RunOptions, + pub workflow_path: Option, + pub workflow_bundle: Option>, + pub hooks: fabro_hooks::HookSettings, + pub sandbox_env: SandboxEnvSpec, + pub devcontainer: Option, + pub git: Option, + pub worktree_mode: Option, pub registry_override: Option>, - pub artifact_sink: Option, - pub run_control: Option>, - pub checkpoint: Option, - pub seed_context: Option, + pub artifact_sink: Option, + pub run_control: Option>, + pub checkpoint: Option, + pub seed_context: Option, } /// Output of the INITIALIZE phase. #[non_exhaustive] pub struct Initialized { - pub graph: Graph, - pub source: String, - pub inputs: HashMap, - pub run_options: RunOptions, - pub workflow_path: Option, - pub workflow_bundle: Option>, - pub run_store: RunStoreHandle, - pub(crate) checkpoint: Option, + pub graph: Graph, + pub source: String, + pub inputs: HashMap, + pub run_options: RunOptions, + pub workflow_path: Option, + pub workflow_bundle: Option>, + pub run_store: RunStoreHandle, + pub(crate) checkpoint: Option, pub(crate) seed_context: Option, - pub emitter: Arc, - pub sandbox: Arc, - pub registry: Arc, - pub on_node: crate::OnNodeCallback, - pub artifact_sink: Option, - pub run_control: Option>, - pub hook_runner: Option>, - pub env: HashMap, - pub dry_run: bool, - pub llm_client: Option, - pub model: String, - pub provider: Provider, + pub emitter: Arc, + pub sandbox: Arc, + pub registry: Arc, + pub on_node: crate::OnNodeCallback, + pub artifact_sink: Option, + pub run_control: Option>, + pub hook_runner: Option>, + pub env: HashMap, + pub dry_run: bool, + pub llm_client: Option, + pub model: String, + pub provider: Provider, } /// Output of the EXECUTE phase. #[non_exhaustive] pub struct Executed { - pub graph: Graph, - pub outcome: Result, - pub run_options: RunOptions, - pub run_store: RunStoreHandle, - pub hook_runner: Option>, - pub emitter: Arc, - pub sandbox: Arc, - pub duration_ms: u64, + pub graph: Graph, + pub outcome: Result, + pub run_options: RunOptions, + pub run_store: RunStoreHandle, + pub hook_runner: Option>, + pub emitter: Arc, + pub sandbox: Arc, + pub duration_ms: u64, pub final_context: Context, - pub llm_client: Option, - pub model: String, - pub provider: Provider, + pub llm_client: Option, + pub model: String, + pub provider: Provider, } /// Output of the RETRO phase. #[non_exhaustive] pub struct Retroed { - pub graph: Graph, - pub outcome: Result, + pub graph: Graph, + pub outcome: Result, pub run_options: RunOptions, - pub run_store: RunStoreHandle, + pub run_store: RunStoreHandle, pub hook_runner: Option>, - pub emitter: Arc, - pub sandbox: Arc, + pub emitter: Arc, + pub sandbox: Arc, pub duration_ms: u64, - pub retro: Option, + pub retro: Option, } /// Output of the FINALIZE phase. #[non_exhaustive] pub struct Concluded { - pub run_id: RunId, - pub outcome: Result, - pub conclusion: Conclusion, + pub run_id: RunId, + pub outcome: Result, + pub conclusion: Conclusion, pub pushed_branch: Option, - pub graph: Graph, - pub run_options: RunOptions, - pub emitter: Arc, + pub graph: Graph, + pub run_options: RunOptions, + pub emitter: Arc, } /// Output of the PULL_REQUEST phase. #[non_exhaustive] pub struct Finalized { - pub run_id: RunId, - pub outcome: Result, - pub conclusion: Conclusion, + pub run_id: RunId, + pub outcome: Result, + pub conclusion: Conclusion, pub pushed_branch: Option, - pub pr_url: Option, + pub pr_url: Option, } /// Options for the TRANSFORM phase. pub struct TransformOptions { - pub current_dir: Option, - pub file_resolver: Option>, - pub inputs: HashMap, + pub current_dir: Option, + pub file_resolver: Option>, + pub inputs: HashMap, pub custom_transforms: Vec>, } /// Options for the RETRO phase. pub struct RetroOptions { - pub run_id: RunId, - pub run_store: RunStoreHandle, - pub workflow_name: String, - pub goal: String, - pub run_dir: PathBuf, - pub sandbox: Arc, - pub emitter: Option>, - pub failed: bool, + pub run_id: RunId, + pub run_store: RunStoreHandle, + pub workflow_name: String, + pub goal: String, + pub run_dir: PathBuf, + pub sandbox: Arc, + pub emitter: Option>, + pub failed: bool, pub run_duration_ms: u64, - pub enabled: bool, - pub llm_client: Option, - pub provider: Provider, - pub model: String, + pub enabled: bool, + pub llm_client: Option, + pub provider: Provider, + pub model: String, } /// Options for the FINALIZE phase. pub struct FinalizeOptions { - pub run_dir: PathBuf, - pub run_id: RunId, - pub run_store: RunStoreHandle, - pub workflow_name: String, - pub hook_runner: Option>, + pub run_dir: PathBuf, + pub run_id: RunId, + pub run_store: RunStoreHandle, + pub workflow_name: String, + pub hook_runner: Option>, pub preserve_sandbox: bool, - pub last_git_sha: Option, + pub last_git_sha: Option, } /// Options for the PULL_REQUEST phase. pub struct PullRequestOptions { - pub run_dir: PathBuf, - pub run_store: RunStoreHandle, - pub pr_config: Option, + pub run_dir: PathBuf, + pub run_store: RunStoreHandle, + pub pr_config: Option, pub github_app: Option, pub origin_url: Option, - pub model: String, + pub model: String, } diff --git a/lib/crates/fabro-workflow/src/pipeline/validate.rs b/lib/crates/fabro-workflow/src/pipeline/validate.rs index 83c140ac7..e07cf8747 100644 --- a/lib/crates/fabro-workflow/src/pipeline/validate.rs +++ b/lib/crates/fabro-workflow/src/pipeline/validate.rs @@ -21,12 +21,15 @@ mod tests { fn run_pipeline(dot: &str) -> Validated { let parsed = parse(dot).unwrap(); - let transformed = transform::transform(parsed, &TransformOptions { - current_dir: None, - file_resolver: None, - inputs: std::collections::HashMap::new(), - custom_transforms: vec![], - }) + let transformed = transform::transform( + parsed, + &TransformOptions { + current_dir: None, + file_resolver: None, + inputs: std::collections::HashMap::new(), + custom_transforms: vec![], + }, + ) .unwrap(); validate(transformed, &[]) } diff --git a/lib/crates/fabro-workflow/src/retry.rs b/lib/crates/fabro-workflow/src/retry.rs index 3a92925b6..72b122418 100644 --- a/lib/crates/fabro-workflow/src/retry.rs +++ b/lib/crates/fabro-workflow/src/retry.rs @@ -5,9 +5,9 @@ use fabro_graphviz::graph::types::{Graph as GvGraph, Node as GvNode}; const DEFAULT_BACKOFF: BackoffPolicy = BackoffPolicy { initial_delay: Duration::from_millis(5_000), - factor: 2.0, - max_delay: Duration::from_millis(60_000), - jitter: true, + factor: 2.0, + max_delay: Duration::from_millis(60_000), + jitter: true, }; /// Build a retry policy from node and graph attributes. @@ -35,22 +35,22 @@ fn preset_retry_policy(preset: &str) -> Option { match preset { "none" => Some(RetryPolicy { max_attempts: 1, - backoff: DEFAULT_BACKOFF, + backoff: DEFAULT_BACKOFF, }), "standard" => Some(RetryPolicy { max_attempts: 5, - backoff: DEFAULT_BACKOFF, + backoff: DEFAULT_BACKOFF, }), "aggressive" => Some(RetryPolicy { max_attempts: 5, - backoff: BackoffPolicy { + backoff: BackoffPolicy { initial_delay: Duration::from_millis(500), ..DEFAULT_BACKOFF }, }), "linear" => Some(RetryPolicy { max_attempts: 3, - backoff: BackoffPolicy { + backoff: BackoffPolicy { initial_delay: Duration::from_millis(500), factor: 1.0, ..DEFAULT_BACKOFF @@ -58,7 +58,7 @@ fn preset_retry_policy(preset: &str) -> Option { }), "patient" => Some(RetryPolicy { max_attempts: 3, - backoff: BackoffPolicy { + backoff: BackoffPolicy { initial_delay: Duration::from_millis(2_000), factor: 3.0, ..DEFAULT_BACKOFF diff --git a/lib/crates/fabro-workflow/src/run_control.rs b/lib/crates/fabro-workflow/src/run_control.rs index 6a61f1054..3bb4ca0a3 100644 --- a/lib/crates/fabro-workflow/src/run_control.rs +++ b/lib/crates/fabro-workflow/src/run_control.rs @@ -8,7 +8,7 @@ use crate::event::{Emitter, Event}; #[derive(Default)] pub struct RunControlState { pause_requested: AtomicBool, - notify: Notify, + notify: Notify, } impl RunControlState { diff --git a/lib/crates/fabro-workflow/src/run_dump.rs b/lib/crates/fabro-workflow/src/run_dump.rs index 509c3b09f..ec8f573c2 100644 --- a/lib/crates/fabro-workflow/src/run_dump.rs +++ b/lib/crates/fabro-workflow/src/run_dump.rs @@ -17,7 +17,7 @@ pub struct RunDump { #[derive(Debug, Clone)] pub struct RunDumpEntry { - path: String, + path: String, contents: RunDumpContents, } @@ -298,42 +298,42 @@ impl RunDump { impl RunDumpEntry { fn text(path: impl Into, contents: String) -> Self { Self { - path: path.into(), + path: path.into(), contents: RunDumpContents::Text(contents), } } fn text_path(path: &Path, contents: String) -> Self { Self { - path: path_to_string(path), + path: path_to_string(path), contents: RunDumpContents::Text(contents), } } fn json(path: impl Into, contents: serde_json::Value) -> Self { Self { - path: path.into(), + path: path.into(), contents: RunDumpContents::Json(contents), } } fn json_path(path: &Path, contents: serde_json::Value) -> Self { Self { - path: path_to_string(path), + path: path_to_string(path), contents: RunDumpContents::Json(contents), } } fn bytes(path: impl Into, contents: Vec) -> Self { Self { - path: path.into(), + path: path.into(), contents: RunDumpContents::Bytes(contents), } } fn bytes_path(path: &Path, contents: Vec) -> Self { Self { - path: path_to_string(path), + path: path_to_string(path), contents: RunDumpContents::Bytes(contents), } } diff --git a/lib/crates/fabro-workflow/src/run_lookup.rs b/lib/crates/fabro-workflow/src/run_lookup.rs index 6a6c0c060..6c1032cce 100644 --- a/lib/crates/fabro-workflow/src/run_lookup.rs +++ b/lib/crates/fabro-workflow/src/run_lookup.rs @@ -13,26 +13,26 @@ use crate::run_status::{RunStatus, StatusReason}; #[derive(Debug, Clone)] struct RunLocalState { - dir_name: String, + dir_name: String, start_time_dt: Option>, - end_time: Option>, - path: PathBuf, - is_orphan: bool, + end_time: Option>, + path: PathBuf, + is_orphan: bool, } #[derive(Debug, Clone, Serialize)] pub struct RunInfo { #[serde(skip)] - summary: Option, - pub dir_name: String, + summary: Option, + pub dir_name: String, #[serde(skip)] pub start_time_dt: Option>, #[serde(skip)] - pub end_time: Option>, + pub end_time: Option>, #[serde(skip)] - pub path: PathBuf, + pub path: PathBuf, #[serde(skip)] - pub is_orphan: bool, + pub is_orphan: bool, } impl RunInfo { @@ -170,13 +170,16 @@ fn scan_orphan_runs(base: &Path) -> Result> { .and_then(|m| m.modified().ok()) .map(|time| -> DateTime { time.into() }); - runs.push(RunInfo::new(None, RunLocalState { - dir_name, - start_time_dt: mtime_dt, - end_time: None, - path, - is_orphan: true, - })); + runs.push(RunInfo::new( + None, + RunLocalState { + dir_name, + start_time_dt: mtime_dt, + end_time: None, + path, + is_orphan: true, + }, + )); } runs.sort_by(|a, b| { @@ -240,13 +243,16 @@ fn run_info_from_summary(summary: &RunSummary, scratch_base: &Path) -> Option RunRecord { RunRecord { - run_id: fixtures::RUN_1, - settings: SettingsLayer::default(), - graph: Graph::new("test"), - workflow_slug: Some("test".to_string()), + run_id: fixtures::RUN_1, + settings: SettingsLayer::default(), + graph: Graph::new("test"), + workflow_slug: Some("test".to_string()), working_directory: PathBuf::from("/tmp/project"), - host_repo_path: Some("/tmp/project".to_string()), - repo_origin_url: None, - base_branch: Some("main".to_string()), - labels: HashMap::new(), - provenance: None, - manifest_blob: None, - definition_blob: None, + host_repo_path: Some("/tmp/project".to_string()), + repo_origin_url: None, + base_branch: Some("main".to_string()), + labels: HashMap::new(), + provenance: None, + manifest_blob: None, + definition_blob: None, } } @@ -434,29 +440,37 @@ mod tests { let store = memory_store(); let run_record = sample_run_record(); let run_store = store.create_run(&fixtures::RUN_1).await.unwrap(); - append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated { - run_id: fixtures::RUN_1, - settings: serde_json::to_value(&run_record.settings).unwrap(), - graph: serde_json::to_value(&run_record.graph).unwrap(), - workflow_source: None, - workflow_config: None, - labels: run_record.labels.clone().into_iter().collect(), - run_dir: run_dir.display().to_string(), - working_directory: run_record.working_directory.display().to_string(), - host_repo_path: run_record.host_repo_path.clone(), - repo_origin_url: run_record.repo_origin_url.clone(), - base_branch: run_record.base_branch.clone(), - workflow_slug: run_record.workflow_slug.clone(), - db_prefix: None, - provenance: run_record.provenance.clone(), - manifest_blob: None, - }) + append_event( + &run_store, + &fixtures::RUN_1, + &Event::RunCreated { + run_id: fixtures::RUN_1, + settings: serde_json::to_value(&run_record.settings).unwrap(), + graph: serde_json::to_value(&run_record.graph).unwrap(), + workflow_source: None, + workflow_config: None, + labels: run_record.labels.clone().into_iter().collect(), + run_dir: run_dir.display().to_string(), + working_directory: run_record.working_directory.display().to_string(), + host_repo_path: run_record.host_repo_path.clone(), + repo_origin_url: run_record.repo_origin_url.clone(), + base_branch: run_record.base_branch.clone(), + workflow_slug: run_record.workflow_slug.clone(), + db_prefix: None, + provenance: run_record.provenance.clone(), + manifest_blob: None, + }, + ) .await .unwrap(); - append_event(&run_store, &fixtures::RUN_1, &Event::RunSubmitted { - reason: None, - definition_blob: None, - }) + append_event( + &run_store, + &fixtures::RUN_1, + &Event::RunSubmitted { + reason: None, + definition_blob: None, + }, + ) .await .unwrap(); diff --git a/lib/crates/fabro-workflow/src/runtime_store.rs b/lib/crates/fabro-workflow/src/runtime_store.rs index df8ba9c05..a30b77d9c 100644 --- a/lib/crates/fabro-workflow/src/runtime_store.rs +++ b/lib/crates/fabro-workflow/src/runtime_store.rs @@ -131,18 +131,18 @@ mod tests { fn test_run_record() -> RunRecord { RunRecord { - run_id: fixtures::RUN_1, - settings: SettingsLayer::default(), - graph: Graph::new("test"), - workflow_slug: Some("test".to_string()), + run_id: fixtures::RUN_1, + settings: SettingsLayer::default(), + graph: Graph::new("test"), + workflow_slug: Some("test".to_string()), working_directory: PathBuf::from("/tmp/test"), - host_repo_path: None, - repo_origin_url: None, - base_branch: None, - labels: HashMap::new(), - provenance: None, - manifest_blob: None, - definition_blob: None, + host_repo_path: None, + repo_origin_url: None, + base_branch: None, + labels: HashMap::new(), + provenance: None, + manifest_blob: None, + definition_blob: None, } } @@ -150,23 +150,27 @@ mod tests { async fn local_handle_loads_state_and_events() { let run_store = test_run_store().await; let record = test_run_record(); - append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated { - run_id: fixtures::RUN_1, - settings: serde_json::to_value(&record.settings).unwrap(), - graph: serde_json::to_value(&record.graph).unwrap(), - workflow_source: Some("digraph test {}".to_string()), - workflow_config: None, - labels: std::collections::BTreeMap::new(), - run_dir: "/tmp/test".to_string(), - working_directory: "/tmp/test".to_string(), - host_repo_path: None, - repo_origin_url: None, - base_branch: None, - workflow_slug: Some("test".to_string()), - db_prefix: None, - provenance: None, - manifest_blob: None, - }) + append_event( + &run_store, + &fixtures::RUN_1, + &Event::RunCreated { + run_id: fixtures::RUN_1, + settings: serde_json::to_value(&record.settings).unwrap(), + graph: serde_json::to_value(&record.graph).unwrap(), + workflow_source: Some("digraph test {}".to_string()), + workflow_config: None, + labels: std::collections::BTreeMap::new(), + run_dir: "/tmp/test".to_string(), + working_directory: "/tmp/test".to_string(), + host_repo_path: None, + repo_origin_url: None, + base_branch: None, + workflow_slug: Some("test".to_string()), + db_prefix: None, + provenance: None, + manifest_blob: None, + }, + ) .await .unwrap(); @@ -184,20 +188,20 @@ mod tests { let handle = RunStoreHandle::local(run_store); let event = RunEvent { - id: "evt-run-submitted".to_string(), - ts: Utc::now(), - run_id: fixtures::RUN_1, - node_id: None, - node_label: None, - stage_id: None, - parallel_group_id: None, + id: "evt-run-submitted".to_string(), + ts: Utc::now(), + run_id: fixtures::RUN_1, + node_id: None, + node_label: None, + stage_id: None, + parallel_group_id: None, parallel_branch_id: None, - session_id: None, - parent_session_id: None, - tool_call_id: None, - actor: None, - body: EventBody::RunSubmitted(RunSubmittedProps { - reason: None, + session_id: None, + parent_session_id: None, + tool_call_id: None, + actor: None, + body: EventBody::RunSubmitted(RunSubmittedProps { + reason: None, definition_blob: None, }), }; diff --git a/lib/crates/fabro-workflow/src/sandbox_git.rs b/lib/crates/fabro-workflow/src/sandbox_git.rs index 468c839e0..11bb54495 100644 --- a/lib/crates/fabro-workflow/src/sandbox_git.rs +++ b/lib/crates/fabro-workflow/src/sandbox_git.rs @@ -12,12 +12,12 @@ use crate::git::{GitAuthor, blocking_push_with_timeout, push_ref}; /// Captured git state for a workflow run, shared with handlers. #[derive(Debug, Clone)] pub struct GitState { - pub run_id: RunId, - pub base_sha: String, - pub run_branch: Option, - pub meta_branch: Option, + pub run_id: RunId, + pub base_sha: String, + pub run_branch: Option, + pub meta_branch: Option, pub checkpoint_exclude_globs: Vec, - pub git_author: GitAuthor, + pub git_author: GitAuthor, } pub const GIT_REMOTE: &str = "git -c maintenance.auto=0 -c gc.auto=0"; @@ -71,18 +71,18 @@ pub async fn git_checkpoint( let completed_str = completed_count.to_string(); let mut trailers = vec![ Trailer { - key: "Fabro-Run", + key: "Fabro-Run", value: run_id, }, Trailer { - key: "Fabro-Completed", + key: "Fabro-Completed", value: &completed_str, }, ]; let shadow_sha_ref = shadow_sha.as_deref().unwrap_or(""); if shadow_sha.is_some() { trailers.push(Trailer { - key: "Fabro-Checkpoint", + key: "Fabro-Checkpoint", value: shadow_sha_ref, }); } diff --git a/lib/crates/fabro-workflow/src/test_support.rs b/lib/crates/fabro-workflow/src/test_support.rs index c701fecae..d9d480be5 100644 --- a/lib/crates/fabro-workflow/src/test_support.rs +++ b/lib/crates/fabro-workflow/src/test_support.rs @@ -10,7 +10,7 @@ use fabro_store::{ArtifactStore, Database, RunProjection}; use object_store::local::LocalFileSystem; use crate::artifact_upload::ArtifactSink; -use crate::error::{FabroError, Result}; +use crate::error::{Error, Result}; use crate::event::{Emitter, Event, StoreProgressLogger, append_event}; use crate::handler::HandlerRegistry; use crate::outcome::Outcome; @@ -30,12 +30,12 @@ pub fn test_store_dir(run_dir: &std::path::Path) -> PathBuf { struct InitializedOptions { hook_runner: Option>, - env: HashMap, - checkpoint: Option, + env: HashMap, + checkpoint: Option, } struct InitializedState { - initialized: Initialized, + initialized: Initialized, store_logger: StoreProgressLogger, } @@ -71,33 +71,37 @@ async fn initialized( .await .expect("failed to create slate-backed test run store"); let run_store = inner_store; - append_event(&run_store, &run_options.run_id, &Event::RunCreated { - run_id: run_options.run_id, - settings: serde_json::to_value(&run_options.settings) - .expect("failed to serialize settings"), - graph: serde_json::to_value(graph).expect("failed to serialize graph"), - workflow_source: None, - workflow_config: None, - labels: run_options - .labels - .clone() - .into_iter() - .collect::>(), - run_dir: run_options.run_dir.display().to_string(), - working_directory: PathBuf::from(sandbox.working_directory()) - .display() - .to_string(), - host_repo_path: run_options - .host_repo_path - .as_ref() - .map(|path| path.display().to_string()), - repo_origin_url: None, - base_branch: run_options.base_branch.clone(), - workflow_slug: run_options.workflow_slug.clone(), - db_prefix: None, - provenance: None, - manifest_blob: None, - }) + append_event( + &run_store, + &run_options.run_id, + &Event::RunCreated { + run_id: run_options.run_id, + settings: serde_json::to_value(&run_options.settings) + .expect("failed to serialize settings"), + graph: serde_json::to_value(graph).expect("failed to serialize graph"), + workflow_source: None, + workflow_config: None, + labels: run_options + .labels + .clone() + .into_iter() + .collect::>(), + run_dir: run_options.run_dir.display().to_string(), + working_directory: PathBuf::from(sandbox.working_directory()) + .display() + .to_string(), + host_repo_path: run_options + .host_repo_path + .as_ref() + .map(|path| path.display().to_string()), + repo_origin_url: None, + base_branch: run_options.base_branch.clone(), + workflow_slug: run_options.workflow_slug.clone(), + db_prefix: None, + provenance: None, + manifest_blob: None, + }, + ) .await .expect("failed to seed run.created event in run store"); let emitter = bound_emitter(run_options.run_id, &emitter); @@ -158,8 +162,8 @@ pub async fn run_graph( run_options, InitializedOptions { hook_runner: None, - env: HashMap::new(), - checkpoint: None, + env: HashMap::new(), + checkpoint: None, }, ) .await; @@ -182,8 +186,8 @@ pub async fn run_graph_with_state( run_options, InitializedOptions { hook_runner: None, - env: HashMap::new(), - checkpoint: None, + env: HashMap::new(), + checkpoint: None, }, ) .await; @@ -194,7 +198,7 @@ pub async fn run_graph_with_state( .run_store .state() .await - .map_err(|err| FabroError::engine(err.to_string()))?; + .map_err(|err| Error::engine(err.to_string()))?; Ok((outcome, state)) } @@ -215,8 +219,8 @@ pub async fn run_graph_with_hooks( run_options, InitializedOptions { hook_runner: Some(hook_runner), - env: env.unwrap_or_default(), - checkpoint: None, + env: env.unwrap_or_default(), + checkpoint: None, }, ) .await; @@ -241,8 +245,8 @@ pub async fn run_graph_with_hooks_and_state( run_options, InitializedOptions { hook_runner: Some(hook_runner), - env: env.unwrap_or_default(), - checkpoint: None, + env: env.unwrap_or_default(), + checkpoint: None, }, ) .await; @@ -253,7 +257,7 @@ pub async fn run_graph_with_hooks_and_state( .run_store .state() .await - .map_err(|err| FabroError::engine(err.to_string()))?; + .map_err(|err| Error::engine(err.to_string()))?; Ok((outcome, state)) } @@ -273,8 +277,8 @@ pub async fn run_graph_from_checkpoint( run_options, InitializedOptions { hook_runner: None, - env: HashMap::new(), - checkpoint: Some(checkpoint.clone()), + env: HashMap::new(), + checkpoint: Some(checkpoint.clone()), }, ) .await; @@ -298,8 +302,8 @@ pub async fn run_graph_from_checkpoint_with_state( run_options, InitializedOptions { hook_runner: None, - env: HashMap::new(), - checkpoint: Some(checkpoint.clone()), + env: HashMap::new(), + checkpoint: Some(checkpoint.clone()), }, ) .await; @@ -310,14 +314,14 @@ pub async fn run_graph_from_checkpoint_with_state( .run_store .state() .await - .map_err(|err| FabroError::engine(err.to_string()))?; + .map_err(|err| Error::engine(err.to_string()))?; Ok((outcome, state)) } pub struct WorkflowRunner { registry: std::sync::Mutex>, - emitter: Arc, - sandbox: Arc, + emitter: Arc, + sandbox: Arc, } impl WorkflowRunner { diff --git a/lib/crates/fabro-workflow/src/transforms/file_inlining.rs b/lib/crates/fabro-workflow/src/transforms/file_inlining.rs index 96188f079..50a19ee3a 100644 --- a/lib/crates/fabro-workflow/src/transforms/file_inlining.rs +++ b/lib/crates/fabro-workflow/src/transforms/file_inlining.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use fabro_graphviz::graph::{AttrValue, Graph}; use super::Transform; -use crate::error::FabroError; +use crate::error::Error; use crate::file_resolver::FileResolver; /// Resolve a potential `@path` file reference. @@ -24,7 +24,7 @@ pub fn resolve_file_ref(value: &str, current_dir: &Path, resolver: &dyn FileReso /// Inlines `@file` references in node prompts and the graph-level goal. pub struct FileInliningTransform { current_dir: PathBuf, - resolver: Arc, + resolver: Arc, } impl FileInliningTransform { @@ -38,7 +38,7 @@ impl FileInliningTransform { } impl Transform for FileInliningTransform { - fn apply(&self, graph: Graph) -> Result { + fn apply(&self, graph: Graph) -> Result { let mut graph = graph; // Inline @file refs in node prompts diff --git a/lib/crates/fabro-workflow/src/transforms/import.rs b/lib/crates/fabro-workflow/src/transforms/import.rs index db8496cbf..da2a85026 100644 --- a/lib/crates/fabro-workflow/src/transforms/import.rs +++ b/lib/crates/fabro-workflow/src/transforms/import.rs @@ -7,36 +7,36 @@ use fabro_graphviz::parser; use fabro_template::{TemplateContext, render as render_template}; use super::{FileInliningTransform, Transform}; -use crate::error::FabroError; +use crate::error::Error; use crate::file_resolver::{FileResolver, ResolvedFile}; pub struct ImportTransform { current_dir: PathBuf, - resolver: Arc, - inputs: HashMap, + resolver: Arc, + inputs: HashMap, } struct PlaceholderOptions { - default_attrs: HashMap, - class_names: Vec, + default_attrs: HashMap, + class_names: Vec, normalized_class: String, } struct PreparedImport { - graph: Graph, - start_id: String, - exit_id: String, - entry_id: String, + graph: Graph, + start_id: String, + exit_id: String, + entry_id: String, exit_predecessor_id: String, } enum ImportPrepareError { - Hard(FabroError), + Hard(Error), Soft(String), } -impl From for ImportPrepareError { - fn from(error: FabroError) -> Self { +impl From for ImportPrepareError { + fn from(error: Error) -> Self { Self::Hard(error) } } @@ -75,7 +75,7 @@ impl ImportTransform { import_path: &str, current_base_dir: &Path, import_stack: &mut Vec, - ) -> Result<(), FabroError> { + ) -> Result<(), Error> { if !graph.nodes.contains_key(placeholder_id) { return Ok(()); } @@ -162,9 +162,7 @@ impl ImportTransform { .with_goal("{{ goal }}") .with_inputs(self.inputs.clone()), ) - .map_err(|error| { - ImportPrepareError::Hard(FabroError::Validation(error.to_string())) - })?; + .map_err(|error| ImportPrepareError::Hard(Error::Validation(error.to_string())))?; let mut graph = parser::parse(&rendered_source).map_err(|error| { ImportPrepareError::Soft(format!( @@ -597,7 +595,7 @@ impl PreparedImport { } impl Transform for ImportTransform { - fn apply(&self, graph: Graph) -> Result { + fn apply(&self, graph: Graph) -> Result { let mut graph = graph; let imports = Self::collect_import_nodes(&graph); let mut import_stack = Vec::new(); diff --git a/lib/crates/fabro-workflow/src/transforms/mod.rs b/lib/crates/fabro-workflow/src/transforms/mod.rs index 14fbd873a..466d7b342 100644 --- a/lib/crates/fabro-workflow/src/transforms/mod.rs +++ b/lib/crates/fabro-workflow/src/transforms/mod.rs @@ -1,11 +1,11 @@ use fabro_graphviz::graph::Graph; -use crate::error::FabroError; +use crate::error::Error; /// A transform that modifies the pipeline graph after parsing and before /// validation. pub trait Transform { - fn apply(&self, graph: Graph) -> Result; + fn apply(&self, graph: Graph) -> Result; } mod file_inlining; diff --git a/lib/crates/fabro-workflow/src/transforms/model_resolution.rs b/lib/crates/fabro-workflow/src/transforms/model_resolution.rs index e9fb05ab9..54c09bc75 100644 --- a/lib/crates/fabro-workflow/src/transforms/model_resolution.rs +++ b/lib/crates/fabro-workflow/src/transforms/model_resolution.rs @@ -1,14 +1,14 @@ use fabro_graphviz::graph::{AttrValue, Graph}; use super::Transform; -use crate::error::FabroError; +use crate::error::Error; /// Resolves model aliases to canonical IDs and infers the provider from the /// model catalog. pub struct ModelResolutionTransform; impl Transform for ModelResolutionTransform { - fn apply(&self, graph: Graph) -> Result { + fn apply(&self, graph: Graph) -> Result { let mut graph = graph; for node in graph.nodes.values_mut() { let model = node diff --git a/lib/crates/fabro-workflow/src/transforms/preamble.rs b/lib/crates/fabro-workflow/src/transforms/preamble.rs index 4078dd760..8e4b0767f 100644 --- a/lib/crates/fabro-workflow/src/transforms/preamble.rs +++ b/lib/crates/fabro-workflow/src/transforms/preamble.rs @@ -1,14 +1,14 @@ use fabro_graphviz::graph::{AttrValue, Graph}; use super::Transform; -use crate::error::FabroError; +use crate::error::Error; /// For nodes whose fidelity is not `Full`, prepend a context mode preamble to /// the prompt. pub struct PreambleTransform; impl Transform for PreambleTransform { - fn apply(&self, graph: Graph) -> Result { + fn apply(&self, graph: Graph) -> Result { use crate::context::keys::Fidelity; let mut graph = graph; diff --git a/lib/crates/fabro-workflow/src/transforms/stylesheet_application.rs b/lib/crates/fabro-workflow/src/transforms/stylesheet_application.rs index ba49b5b1c..6e7ca2292 100644 --- a/lib/crates/fabro-workflow/src/transforms/stylesheet_application.rs +++ b/lib/crates/fabro-workflow/src/transforms/stylesheet_application.rs @@ -2,14 +2,14 @@ use fabro_graphviz::graph::Graph; use super::Transform; use super::stylesheet::{apply_stylesheet, parse_stylesheet}; -use crate::error::FabroError; +use crate::error::Error; /// Applies the `model_stylesheet` graph attribute to resolve LLM properties for /// each node. pub struct StylesheetApplicationTransform; impl Transform for StylesheetApplicationTransform { - fn apply(&self, graph: Graph) -> Result { + fn apply(&self, graph: Graph) -> Result { let mut graph = graph; let stylesheet_text = graph.model_stylesheet().to_string(); if stylesheet_text.is_empty() { diff --git a/lib/crates/fabro-workflow/src/transforms/variable_expansion.rs b/lib/crates/fabro-workflow/src/transforms/variable_expansion.rs index 30a5e2435..c65c3db7e 100644 --- a/lib/crates/fabro-workflow/src/transforms/variable_expansion.rs +++ b/lib/crates/fabro-workflow/src/transforms/variable_expansion.rs @@ -4,7 +4,7 @@ use fabro_graphviz::graph::{AttrValue, Graph}; use fabro_template::{TemplateContext, render as render_template}; use super::Transform; -use crate::error::FabroError; +use crate::error::Error; /// Expands `{{ goal }}` / `{{ inputs.* }}` across all string attributes. pub struct TemplateTransform { @@ -15,7 +15,7 @@ impl TemplateTransform { fn render_attrs( attrs: &mut HashMap, ctx: &TemplateContext, - ) -> Result<(), FabroError> { + ) -> Result<(), Error> { for value in attrs.values_mut() { if let AttrValue::String(text) = value { *text = render_template(text, ctx)?; @@ -24,7 +24,7 @@ impl TemplateTransform { Ok(()) } - fn resolved_goal(&self, graph: &Graph) -> Result { + fn resolved_goal(&self, graph: &Graph) -> Result { let ctx = TemplateContext::new() .with_goal("{{ goal }}") .with_inputs(self.inputs.clone()); @@ -33,7 +33,7 @@ impl TemplateTransform { } impl Transform for TemplateTransform { - fn apply(&self, graph: Graph) -> Result { + fn apply(&self, graph: Graph) -> Result { let mut graph = graph; let resolved_goal = self.resolved_goal(&graph)?; let ctx = TemplateContext::new() @@ -84,8 +84,8 @@ mod tests { graph.nodes.insert("plan".to_string(), node); graph.edges.push(Edge { - from: "start".to_string(), - to: "plan".to_string(), + from: "start".to_string(), + to: "plan".to_string(), attrs: HashMap::from([( "label".to_string(), AttrValue::String("{{ inputs.greeting }}".to_string()), diff --git a/lib/crates/fabro-workflow/src/workflow_bundle.rs b/lib/crates/fabro-workflow/src/workflow_bundle.rs index ba5cfb643..fd7afe078 100644 --- a/lib/crates/fabro-workflow/src/workflow_bundle.rs +++ b/lib/crates/fabro-workflow/src/workflow_bundle.rs @@ -9,8 +9,8 @@ use crate::file_resolver::{BundleFileResolver, FileResolver, normalize_logical_p #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct BundledWorkflow { pub logical_path: PathBuf, - pub source: String, - pub files: HashMap, + pub source: String, + pub files: HashMap, } impl BundledWorkflow { @@ -63,7 +63,7 @@ impl WorkflowBundle { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RunDefinition { pub workflow_path: PathBuf, - pub workflows: HashMap, + pub workflows: HashMap, } impl RunDefinition { diff --git a/lib/crates/fabro-workflow/tests/it/cp_integration.rs b/lib/crates/fabro-workflow/tests/it/cp_integration.rs index deb9fc2e1..d879466fb 100644 --- a/lib/crates/fabro-workflow/tests/it/cp_integration.rs +++ b/lib/crates/fabro-workflow/tests/it/cp_integration.rs @@ -16,11 +16,11 @@ use fabro_sandbox::reconnect::reconnect; fn local_record(working_directory: &std::path::Path) -> SandboxRecord { SandboxRecord { - provider: "local".to_string(), - working_directory: working_directory.to_string_lossy().to_string(), - identifier: None, + provider: "local".to_string(), + working_directory: working_directory.to_string_lossy().to_string(), + identifier: None, host_working_directory: None, - container_mount_point: None, + container_mount_point: None, } } @@ -118,11 +118,11 @@ async fn local_cp_creates_parent_dirs() { fn docker_record(host_dir: &std::path::Path, mount_point: &str) -> SandboxRecord { SandboxRecord { - provider: "docker".to_string(), - working_directory: mount_point.to_string(), - identifier: None, + provider: "docker".to_string(), + working_directory: mount_point.to_string(), + identifier: None, host_working_directory: Some(host_dir.to_string_lossy().to_string()), - container_mount_point: Some(mount_point.to_string()), + container_mount_point: Some(mount_point.to_string()), } } diff --git a/lib/crates/fabro-workflow/tests/it/daytona_integration.rs b/lib/crates/fabro-workflow/tests/it/daytona_integration.rs index fff786360..2caf8cccc 100644 --- a/lib/crates/fabro-workflow/tests/it/daytona_integration.rs +++ b/lib/crates/fabro-workflow/tests/it/daytona_integration.rs @@ -27,7 +27,7 @@ use fabro_types::settings::run::{RunArtifactsLayer, RunLayer}; use fabro_types::{RunId, StageId}; use fabro_workflow::artifact::sync_artifacts_to_env; use fabro_workflow::context::Context; -use fabro_workflow::error::FabroError; +use fabro_workflow::error::Error; use fabro_workflow::event::Emitter; use fabro_workflow::handler::exit::ExitHandler; use fabro_workflow::handler::start::StartHandler; @@ -92,7 +92,7 @@ fn load_run_checkpoint(run_dir: &Path) -> Result(state); + return Ok::<_, fabro_store::Error>(state); } tokio::time::sleep(std::time::Duration::from_millis(10)).await; } @@ -130,7 +130,7 @@ fn load_run_checkpoint(run_dir: &Path) -> Result(state); + return Ok::<_, fabro_store::Error>(state); } tokio::time::sleep(std::time::Duration::from_millis(10)).await; } @@ -351,10 +351,10 @@ async fn daytona_snapshot_sandbox() { let config = DaytonaConfig { auto_stop_interval: Some(60), snapshot: Some(DaytonaSnapshotConfig { - name: "fabro-test-snapshot".to_string(), - cpu: Some(2), - memory: Some(4), - disk: Some(10), + name: "fabro-test-snapshot".to_string(), + cpu: Some(2), + memory: Some(4), + disk: Some(10), dockerfile: Some(fabro_sandbox::daytona::DockerfileSource::Inline( "FROM ubuntu:22.04\nRUN apt-get update && apt-get install -y ripgrep".to_string(), )), @@ -446,7 +446,7 @@ impl Handler for LargeOutputHandler { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let mut outcome = Outcome::success(); let large_value = "x".repeat(150 * 1024); outcome.context_updates.insert( @@ -501,17 +501,17 @@ async fn daytona_pipeline_artifact_offload_and_sync() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), env.clone()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = engine .run(&graph, &run_options) @@ -559,7 +559,7 @@ impl Handler for FileWriterHandler { _graph: &Graph, _run_dir: &Path, services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let content = format!("output from {}", node.id); let cmd = format!("echo '{content}' > {}.txt", node.id); let _ = services @@ -679,19 +679,19 @@ async fn daytona_git_checkpoint_remote_emits_events() { let engine = WorkflowRunner::new(registry, Arc::new(emitter), env.clone()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("git-cp-test"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("git-cp-test"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: Some(dir.path().to_path_buf()), - git: Some(GitCheckpointOptions { - base_sha: Some(base_sha), - run_branch: Some(branch_name), + host_repo_path: Some(dir.path().to_path_buf()), + git: Some(GitCheckpointOptions { + base_sha: Some(base_sha), + run_branch: Some(branch_name), meta_branch: None, }), }; @@ -861,8 +861,8 @@ async fn daytona_parallel_git_branching_e2e() { display_base_sha: None, host_repo_path: Some(run_tmp.path().to_path_buf()), git: Some(GitCheckpointOptions { - base_sha: Some(base_sha), - run_branch: Some(branch_name), + base_sha: Some(base_sha), + run_branch: Some(branch_name), meta_branch: None, }), }; @@ -980,10 +980,10 @@ async fn run_daytona_cli_test(provider: Provider, model: &str, install_command: let creds = load_github_app_credentials(); let config = DaytonaConfig { snapshot: Some(DaytonaSnapshotConfig { - name: "daytona-medium".into(), - cpu: None, - memory: None, - disk: None, + name: "daytona-medium".into(), + cpu: None, + memory: None, + disk: None, dockerfile: None, }), ..DaytonaConfig::default() @@ -1210,8 +1210,8 @@ async fn daytona_git_checkpoint_with_shadow_branch() { display_base_sha: None, host_repo_path: Some(host_repo.path().to_path_buf()), git: Some(GitCheckpointOptions { - base_sha: Some(base_sha), - run_branch: Some(branch_name), + base_sha: Some(base_sha), + run_branch: Some(branch_name), meta_branch: Some(meta_branch), }), }; @@ -1269,7 +1269,7 @@ impl Handler for AssetCreatorHandler { _graph: &Graph, _run_dir: &Path, services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let script = concat!( "mkdir -p test-results && ", "echo '' > test-results/report.xml && ", @@ -1279,7 +1279,7 @@ impl Handler for AssetCreatorHandler { .sandbox .exec_command(script, 30_000, None, None, None) .await - .map_err(|e| FabroError::handler(format!("exec failed: {e}")))?; + .map_err(|e| Error::handler(format!("exec failed: {e}")))?; Ok(Outcome::success()) } } @@ -1333,7 +1333,7 @@ async fn daytona_asset_collection() { graph.edges.push(Edge::new("create_assets", "exit")); let run_options = RunOptions { - settings: SettingsLayer { + settings: SettingsLayer { run: Some(RunLayer { artifacts: Some(RunArtifactsLayer { include: vec!["test-results/**".to_string()], @@ -1342,16 +1342,16 @@ async fn daytona_asset_collection() { }), ..SettingsLayer::default() }, - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("artifact-test-daytona"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("artifact-test-daytona"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = engine .run(&graph, &run_options) @@ -1611,8 +1611,8 @@ async fn daytona_git_push_run_branch_to_origin() { display_base_sha: None, host_repo_path: Some(dir.path().to_path_buf()), git: Some(GitCheckpointOptions { - base_sha: Some(base_sha), - run_branch: Some(branch_name.clone()), + base_sha: Some(base_sha), + run_branch: Some(branch_name.clone()), meta_branch: None, }), }; @@ -1815,11 +1815,11 @@ async fn daytona_cp_upload_download_round_trip() { // 2. Build a SandboxRecord (same as `fabro run` would persist) let record = SandboxRecord { - provider: "daytona".to_string(), - working_directory: env.working_directory().to_string(), - identifier: Some(sandbox_name.clone()), + provider: "daytona".to_string(), + working_directory: env.working_directory().to_string(), + identifier: Some(sandbox_name.clone()), host_working_directory: None, - container_mount_point: None, + container_mount_point: None, }; // 3. Reconnect via the real cp::reconnect path @@ -1891,10 +1891,10 @@ async fn daytona_computer_use_browser_screenshot() { use base64::Engine; let config = DaytonaConfig { snapshot: Some(DaytonaSnapshotConfig { - name: "daytona-medium".into(), - cpu: None, - memory: None, - disk: None, + name: "daytona-medium".into(), + cpu: None, + memory: None, + disk: None, dockerfile: None, }), skip_clone: true, @@ -2045,10 +2045,10 @@ async fn daytona_playwright_mcp_sandbox_transport() { // Create sandbox from daytona-medium (has Node.js + Chromium) let config = DaytonaConfig { snapshot: Some(DaytonaSnapshotConfig { - name: "daytona-medium".into(), - cpu: None, - memory: None, - disk: None, + name: "daytona-medium".into(), + cpu: None, + memory: None, + disk: None, dockerfile: None, }), skip_clone: true, @@ -2090,8 +2090,8 @@ async fn daytona_playwright_mcp_sandbox_transport() { // 2. Start the Playwright MCP server via the sandbox transport resolution path let mcp_port = 3100u16; let mcp_config = fabro_mcp::config::McpServerSettings { - name: "playwright".into(), - transport: fabro_mcp::config::McpTransport::Sandbox { + name: "playwright".into(), + transport: fabro_mcp::config::McpTransport::Sandbox { command: vec![ "npx".into(), "@playwright/mcp@latest".into(), @@ -2101,11 +2101,11 @@ async fn daytona_playwright_mcp_sandbox_transport() { "--browser".into(), "chromium".into(), ], - port: mcp_port, - env: std::collections::HashMap::new(), + port: mcp_port, + env: std::collections::HashMap::new(), }, startup_timeout_secs: 30, - tool_timeout_secs: 120, + tool_timeout_secs: 120, }; // Resolve the sandbox transport: start the server, get preview URL, rewrite to @@ -2158,10 +2158,10 @@ async fn daytona_playwright_mcp_sandbox_transport() { eprintln!("Preview URL: {url}"); fabro_mcp::config::McpServerSettings { - name: mcp_config.name.clone(), - transport: fabro_mcp::config::McpTransport::Http { url, headers }, + name: mcp_config.name.clone(), + transport: fabro_mcp::config::McpTransport::Http { url, headers }, startup_timeout_secs: mcp_config.startup_timeout_secs, - tool_timeout_secs: mcp_config.tool_timeout_secs, + tool_timeout_secs: mcp_config.tool_timeout_secs, } } _ => unreachable!(), diff --git a/lib/crates/fabro-workflow/tests/it/git_integration.rs b/lib/crates/fabro-workflow/tests/it/git_integration.rs index 8d8f587e2..1a4241f33 100644 --- a/lib/crates/fabro-workflow/tests/it/git_integration.rs +++ b/lib/crates/fabro-workflow/tests/it/git_integration.rs @@ -157,17 +157,17 @@ fn make_registry() -> HandlerRegistry { fn test_run_options(run_dir: &Path) -> RunOptions { RunOptions { - run_dir: run_dir.to_path_buf(), - cancel_token: None, - run_id: fixtures::RUN_2, - settings: SettingsLayer::default(), - git: None, - host_repo_path: None, - labels: HashMap::new(), - github_app: None, - base_branch: None, + run_dir: run_dir.to_path_buf(), + cancel_token: None, + run_id: fixtures::RUN_2, + settings: SettingsLayer::default(), + git: None, + host_repo_path: None, + labels: HashMap::new(), + github_app: None, + base_branch: None, display_base_sha: None, - workflow_slug: None, + workflow_slug: None, } } @@ -287,8 +287,8 @@ async fn git_checkpoint_skips_start_node() { let mut run_options = test_run_options(run_tmp.path()); run_options.git = Some(GitCheckpointOptions { - base_sha: Some(base_sha), - run_branch: None, + base_sha: Some(base_sha), + run_branch: None, meta_branch: Some(MetadataStore::branch_name(&fixtures::RUN_2.to_string())), }); run_options.host_repo_path = Some(PathBuf::from(repo)); diff --git a/lib/crates/fabro-workflow/tests/it/integration.rs b/lib/crates/fabro-workflow/tests/it/integration.rs index 3daae26d4..2514138d1 100644 --- a/lib/crates/fabro-workflow/tests/it/integration.rs +++ b/lib/crates/fabro-workflow/tests/it/integration.rs @@ -32,7 +32,7 @@ use fabro_types::settings::run::{RunArtifactsLayer, RunLayer}; use fabro_types::{RunEvent, RunId, StageId}; use fabro_validate::{Severity, validate, validate_or_raise}; use fabro_workflow::context::Context; -use fabro_workflow::error::{FabroError, FailureSignatureExt}; +use fabro_workflow::error::{Error, FailureSignatureExt}; use fabro_workflow::event::{Emitter, Event}; use fabro_workflow::handler::agent::{AgentHandler, CodergenBackend, CodergenResult}; use fabro_workflow::handler::command::CommandHandler; @@ -123,7 +123,7 @@ fn load_run_checkpoint(run_dir: &Path) -> Result(state); + return Ok::<_, fabro_store::Error>(state); } tokio::time::sleep(Duration::from_millis(10)).await; } @@ -161,7 +161,7 @@ fn load_run_checkpoint(run_dir: &Path) -> Result(state); + return Ok::<_, fabro_store::Error>(state); } tokio::time::sleep(Duration::from_millis(10)).await; } @@ -337,17 +337,17 @@ async fn end_to_end_linear_pipeline() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -466,17 +466,17 @@ async fn end_to_end_branching_pipeline() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -571,9 +571,9 @@ async fn end_to_end_human_gate_pipeline() { // Pre-fill the queue with an answer selecting "R" let answers = VecDeque::from([Answer { - value: AnswerValue::Selected("R".to_string()), + value: AnswerValue::Selected("R".to_string()), selected_option: None, - text: None, + text: None, }]); let interviewer = Arc::new(QueueInterviewer::new(answers)); @@ -585,17 +585,17 @@ async fn end_to_end_human_gate_pipeline() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -680,17 +680,17 @@ async fn human_gate_interrupted_input_fails_closed_without_fail_route() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -790,17 +790,17 @@ async fn human_gate_interrupted_input_routes_via_outcome_fail_condition() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -837,7 +837,7 @@ impl Handler for AlwaysFailHandler { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { Ok(Outcome::fail_classify(format!( "forced failure for {}", node.id @@ -903,17 +903,17 @@ async fn goal_gate_routes_to_retry_target_on_failure() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let result = engine.run(&graph, &run_options).await; assert!( @@ -962,7 +962,7 @@ async fn goal_gate_routes_to_retry_target_when_present() { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let count = self .call_count .fetch_add(1, std::sync::atomic::Ordering::SeqCst); @@ -1023,17 +1023,17 @@ async fn goal_gate_routes_to_retry_target_when_present() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -1279,7 +1279,7 @@ async fn retry_on_failure_then_succeed() { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let count = self .call_count .fetch_add(1, std::sync::atomic::Ordering::SeqCst); @@ -1337,17 +1337,17 @@ async fn retry_on_failure_then_succeed() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = engine .run(&graph, &run_options) @@ -1411,17 +1411,17 @@ async fn pipeline_with_many_nodes() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -1500,15 +1500,15 @@ impl CodergenBackend for MockCodergenBackend { _emitter: &Arc, _sandbox: &Arc, _tool_hooks: Option>, - ) -> Result { + ) -> Result { Ok(CodergenResult::Text { - text: format!( + text: format!( "Response for {}: processed prompt '{}'", node.id, &prompt[..prompt.len().min(50)] ), - usage: None, - files_touched: Vec::new(), + usage: None, + files_touched: Vec::new(), last_file_touched: None, }) } @@ -1533,7 +1533,7 @@ impl Handler for CounterHandler { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let count = self .call_count .fetch_add(1, std::sync::atomic::Ordering::SeqCst); @@ -1559,7 +1559,7 @@ impl Handler for LargeOutputHandler { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let mut outcome = Outcome::success(); // 150KB string — well above the 100KB artifact threshold let large_value = "x".repeat(150 * 1024); @@ -1574,7 +1574,7 @@ impl Handler for LargeOutputHandler { #[derive(Clone)] struct ContextValueCaptureHandler { values: Arc>>, - key: String, + key: String, } #[async_trait::async_trait] @@ -1586,7 +1586,7 @@ impl Handler for ContextValueCaptureHandler { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let value = context .get(&self.key) .and_then(|value| value.as_str().map(ToOwned::to_owned)) @@ -1608,7 +1608,7 @@ impl Handler for ContextSetterHandler { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let mut outcome = Outcome::success(); outcome .context_updates @@ -1757,17 +1757,17 @@ async fn smoke_test_with_mock_codergen_backend() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -1858,17 +1858,17 @@ async fn end_to_end_parallel_fan_out_fan_in() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -1970,17 +1970,17 @@ async fn resume_from_checkpoint_completes_pipeline() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_from_checkpoint_with_state(&graph, &run_options, &checkpoint) @@ -2068,17 +2068,17 @@ async fn resume_from_checkpoint_preserves_goal_gate_outcomes() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; // This should succeed because goal gate for gated_work is satisfied // via restored outcomes @@ -2110,17 +2110,17 @@ async fn graph_goal_in_context() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (_outcome, state) = engine .run_with_state(&graph, &run_options) @@ -2148,17 +2148,17 @@ async fn event_streaming_lifecycle() { let events = collect_events(&emitter); let engine = WorkflowRunner::new(make_linear_registry(), Arc::new(emitter), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine.run(&graph, &run_options).await.expect("run"); @@ -2227,17 +2227,17 @@ async fn context_flow_between_stages() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (_outcome, state) = engine .run_with_state(&graph, &run_options) @@ -2282,17 +2282,17 @@ async fn tool_handler_e2e() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, _state) = engine .run_with_state(&graph, &run_options) @@ -2356,17 +2356,17 @@ async fn auto_approve_interviewer_e2e() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, _state) = engine .run_with_state(&graph, &run_options) @@ -2395,17 +2395,17 @@ async fn codergen_without_backend_simulated() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (_outcome, state) = engine .run_with_state(&graph, &run_options) @@ -2442,7 +2442,7 @@ async fn branching_loop_back_on_failure() { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let count = self .call_count .fetch_add(1, std::sync::atomic::Ordering::SeqCst); @@ -2499,17 +2499,17 @@ async fn branching_loop_back_on_failure() { ); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, _state) = engine .run_with_state(&graph, &run_options) @@ -2565,14 +2565,14 @@ async fn human_gate_loops_back() { let answers = VecDeque::from([ Answer { - value: AnswerValue::Selected("F".to_string()), + value: AnswerValue::Selected("F".to_string()), selected_option: None, - text: None, + text: None, }, Answer { - value: AnswerValue::Selected("A".to_string()), + value: AnswerValue::Selected("A".to_string()), selected_option: None, - text: None, + text: None, }, ]); let interviewer = Arc::new(QueueInterviewer::new(answers)); @@ -2584,17 +2584,17 @@ async fn human_gate_loops_back() { registry.register("human", Box::new(HumanHandler::new(interviewer))); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, _state) = engine .run_with_state(&graph, &run_options) @@ -2648,17 +2648,17 @@ async fn scenario_ship_a_feature() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, _state) = engine .run_with_state(&graph, &run_options) @@ -2732,17 +2732,17 @@ async fn scenario_parallel_expert_review() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -2777,7 +2777,7 @@ async fn scenario_node_retries_on_retry_status() { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let count = self .call_count .fetch_add(1, std::sync::atomic::Ordering::SeqCst); @@ -2818,17 +2818,17 @@ async fn scenario_node_retries_on_retry_status() { ); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -2882,17 +2882,17 @@ async fn scenario_loop_restart_resets_context() { ); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = engine.run(&graph, &run_options).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -2949,17 +2949,17 @@ async fn scenario_bug_triage_router() { registry.register("conditional", Box::new(ConditionalHandler)); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3010,17 +3010,17 @@ async fn scenario_crash_recovery() { registry.register("exit", Box::new(ExitHandler)); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_from_checkpoint_with_state(&graph, &run_options, &checkpoint) @@ -3049,7 +3049,7 @@ async fn manager_loop_stop_condition_satisfied_e2e() { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let mut outcome = Outcome::success(); outcome .context_updates @@ -3070,7 +3070,7 @@ async fn manager_loop_stop_condition_satisfied_e2e() { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { tokio::time::sleep(std::time::Duration::from_secs(10)).await; Ok(Outcome::success()) } @@ -3119,17 +3119,17 @@ async fn manager_loop_stop_condition_satisfied_e2e() { registry.register("stack.manager_loop", Box::new(SubWorkflowHandler)); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3163,7 +3163,7 @@ async fn manager_loop_max_cycles_exceeded_e2e() { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { tokio::time::sleep(std::time::Duration::from_secs(10)).await; Ok(Outcome::success()) } @@ -3200,17 +3200,17 @@ async fn manager_loop_max_cycles_exceeded_e2e() { registry.register("stack.manager_loop", Box::new(SubWorkflowHandler)); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3340,17 +3340,17 @@ async fn conditional_branching_success_fail_paths() { registry.register("always_fail", Box::new(AlwaysFailHandler)); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3395,17 +3395,17 @@ async fn edge_selection_condition_match_wins_over_weight() { registry.register("exit", Box::new(ExitHandler)); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (_outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3444,17 +3444,17 @@ async fn edge_selection_weight_breaks_ties() { registry.register("exit", Box::new(ExitHandler)); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (_outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3485,17 +3485,17 @@ async fn edge_selection_lexical_tiebreak() { registry.register("exit", Box::new(ExitHandler)); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (_outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3545,17 +3545,17 @@ async fn context_updates_visible_across_nodes() { registry.register("context_setter", Box::new(ContextSetterHandler)); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (_outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3591,17 +3591,17 @@ async fn stylesheet_applies_model_override() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = engine.run(&graph, &run_options).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -3620,7 +3620,7 @@ async fn custom_handler_registration_and_execution() { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let mut outcome = Outcome::success(); outcome .context_updates @@ -3646,17 +3646,17 @@ async fn custom_handler_registration_and_execution() { registry.register("my_custom", Box::new(CustomHandler)); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (_outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3723,17 +3723,17 @@ async fn integration_smoke_plan_implement_review_done() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3814,17 +3814,17 @@ async fn manager_loop_runs_child_engine_e2e() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3870,7 +3870,7 @@ async fn manager_loop_context_flows_e2e() { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let target = context.get_string("review.target", ""); let mut outcome = Outcome::success(); outcome @@ -3896,7 +3896,7 @@ async fn manager_loop_context_flows_e2e() { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let mut outcome = Outcome::success(); outcome.context_updates.insert( "review.target".to_string(), @@ -3947,17 +3947,17 @@ async fn manager_loop_context_flows_e2e() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -4022,17 +4022,17 @@ async fn manager_loop_child_dotfile_e2e() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = engine.run(&graph, &run_options).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -4081,14 +4081,17 @@ async fn import_e2e_through_engine() { }"#, ) .expect("parse should succeed"); - let transformed = transform(parsed, &TransformOptions { - current_dir: Some(dir.path().to_path_buf()), - file_resolver: Some(std::sync::Arc::new( - fabro_workflow::file_resolver::FilesystemFileResolver::new(None), - )), - inputs: std::collections::HashMap::new(), - custom_transforms: vec![], - }) + let transformed = transform( + parsed, + &TransformOptions { + current_dir: Some(dir.path().to_path_buf()), + file_resolver: Some(std::sync::Arc::new( + fabro_workflow::file_resolver::FilesystemFileResolver::new(None), + )), + inputs: std::collections::HashMap::new(), + custom_transforms: vec![], + }, + ) .unwrap(); let validated = validate(transformed, &[]); validated @@ -4125,17 +4128,17 @@ async fn import_e2e_through_engine() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -4197,7 +4200,7 @@ type SharedVec = Arc>>; struct FidelityCaptures { fidelities: SharedVec<(String, String)>, thread_ids: SharedVec<(String, Option)>, - preambles: SharedVec<(String, String)>, + preambles: SharedVec<(String, String)>, } impl FidelityCaptures { @@ -4205,7 +4208,7 @@ impl FidelityCaptures { Self { fidelities: Arc::new(std::sync::Mutex::new(Vec::new())), thread_ids: Arc::new(std::sync::Mutex::new(Vec::new())), - preambles: Arc::new(std::sync::Mutex::new(Vec::new())), + preambles: Arc::new(std::sync::Mutex::new(Vec::new())), } } } @@ -4225,7 +4228,7 @@ impl Handler for FidelityCapturingHandler { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let fidelity = context.get_string("internal.fidelity", "none"); self.captures .fidelities @@ -4279,17 +4282,17 @@ async fn fidelity_default_is_compact() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine.run(&graph, &run_options).await.expect("run"); @@ -4335,17 +4338,17 @@ async fn fidelity_graph_default_applied() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine.run(&graph, &run_options).await.expect("run"); @@ -4387,17 +4390,17 @@ async fn fidelity_node_overrides_graph_default() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine.run(&graph, &run_options).await.expect("run"); @@ -4445,17 +4448,17 @@ async fn fidelity_edge_overrides_node_and_graph() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine.run(&graph, &run_options).await.expect("run"); @@ -4493,17 +4496,17 @@ async fn fidelity_full_produces_empty_preamble() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine.run(&graph, &run_options).await.expect("run"); @@ -4551,17 +4554,17 @@ async fn fidelity_truncate_preamble_minimal() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine.run(&graph, &run_options).await.expect("run"); @@ -4622,17 +4625,17 @@ async fn fidelity_summary_low_mode() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine.run(&graph, &run_options).await.expect("run"); @@ -4688,17 +4691,17 @@ async fn fidelity_summary_medium_mode() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine.run(&graph, &run_options).await.expect("run"); @@ -4754,17 +4757,17 @@ async fn fidelity_summary_high_mode() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine.run(&graph, &run_options).await.expect("run"); @@ -4813,17 +4816,17 @@ async fn fidelity_full_sets_thread_id_in_context() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine.run(&graph, &run_options).await.expect("run"); @@ -4883,17 +4886,17 @@ async fn fidelity_full_nodes_share_thread_id() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine.run(&graph, &run_options).await.expect("run"); @@ -4963,17 +4966,17 @@ async fn fidelity_resume_degrades_full_to_summary_high() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine .run_from_checkpoint(&graph, &run_options, &checkpoint) @@ -5059,17 +5062,17 @@ async fn fidelity_resume_degrade_only_affects_first_hop() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine .run_from_checkpoint(&graph, &run_options, &checkpoint) @@ -5142,17 +5145,17 @@ async fn fidelity_resume_no_degrade_when_not_full() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine .run_from_checkpoint(&graph, &run_options, &checkpoint) @@ -5183,17 +5186,17 @@ async fn fidelity_stored_in_checkpoint_context() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (_outcome, state) = engine .run_with_state(&graph, &run_options) @@ -5275,17 +5278,17 @@ async fn fidelity_precedence_multi_node_pipeline() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine.run(&graph, &run_options).await.expect("run"); @@ -5342,17 +5345,17 @@ async fn fidelity_compact_preamble_includes_completed_stages_and_context() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine.run(&graph, &run_options).await.expect("run"); @@ -5416,17 +5419,17 @@ async fn fidelity_summary_low_excludes_context_values_in_pipeline() { ); let engine_low = WorkflowRunner::new(registry_low, Arc::new(Emitter::default()), local_env()); let run_options_low = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir_low.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir_low.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine_low .run(&graph_low, &run_options_low) @@ -5482,17 +5485,17 @@ async fn fidelity_summary_low_excludes_context_values_in_pipeline() { ); let engine_med = WorkflowRunner::new(registry_med, Arc::new(Emitter::default()), local_env()); let run_options_med = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir_med.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir_med.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine_med .run(&graph_med, &run_options_med) @@ -5553,17 +5556,17 @@ async fn fidelity_thread_id_fallback_to_previous_node_in_pipeline() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine.run(&graph, &run_options).await.expect("run"); @@ -5606,17 +5609,17 @@ async fn fidelity_thread_id_from_node_class_in_pipeline() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine.run(&graph, &run_options).await.expect("run"); @@ -5662,17 +5665,17 @@ async fn fidelity_edge_thread_id_override_in_pipeline() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine.run(&graph, &run_options).await.expect("run"); @@ -5719,17 +5722,17 @@ async fn fidelity_full_without_explicit_thread_id_uses_previous_node() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine.run(&graph, &run_options).await.expect("run"); @@ -5786,17 +5789,17 @@ async fn fidelity_from_parsed_dot_pipeline() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine.run(&graph, &run_options).await.expect("run"); @@ -5834,17 +5837,17 @@ async fn fidelity_checkpoint_roundtrip_preserves_fidelity() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (_outcome, state) = engine .run_with_state(&graph, &run_options) @@ -5906,17 +5909,17 @@ async fn fidelity_node_thread_id_overrides_edge_thread_id_in_pipeline() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine.run(&graph, &run_options).await.expect("run"); @@ -5992,17 +5995,17 @@ async fn fidelity_resume_preserves_context_values_across_checkpoint() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (_outcome, state) = engine .run_from_checkpoint_with_state(&graph, &run_options, &checkpoint) @@ -6039,12 +6042,12 @@ mod real_llm { use fabro_llm::types::{Message, Request}; use fabro_types::settings::SettingsLayer; use fabro_workflow::context::Context; - use fabro_workflow::error::FabroError; + use fabro_workflow::error::Error; use fabro_workflow::handler::agent::{AgentHandler, CodergenBackend, CodergenResult}; struct LlmCodergenBackend { - client: Arc, - model: String, + client: Arc, + model: String, provider: String, } @@ -6059,7 +6062,7 @@ mod real_llm { _emitter: &Arc, _sandbox: &Arc, _tool_hooks: Option>, - ) -> Result { + ) -> Result { self.complete(prompt).await } @@ -6068,38 +6071,38 @@ mod real_llm { _node: &Node, prompt: &str, _system_prompt: Option<&str>, - ) -> Result { + ) -> Result { self.complete(prompt).await } } impl LlmCodergenBackend { - async fn complete(&self, prompt: &str) -> Result { + async fn complete(&self, prompt: &str) -> Result { let request = Request { - model: self.model.clone(), - messages: vec![Message::user(prompt)], - provider: Some(self.provider.clone()), - tools: None, - tool_choice: None, - response_format: None, - temperature: Some(0.0), - top_p: None, - max_tokens: Some(200), - stop_sequences: None, + model: self.model.clone(), + messages: vec![Message::user(prompt)], + provider: Some(self.provider.clone()), + tools: None, + tool_choice: None, + response_format: None, + temperature: Some(0.0), + top_p: None, + max_tokens: Some(200), + stop_sequences: None, reasoning_effort: None, - speed: None, - metadata: None, + speed: None, + metadata: None, provider_options: None, }; let response = self .client .complete(&request) .await - .map_err(|e| FabroError::handler(e.to_string()))?; + .map_err(|e| Error::handler(e.to_string()))?; Ok(CodergenResult::Text { - text: response.text(), - usage: None, - files_touched: Vec::new(), + text: response.text(), + usage: None, + files_touched: Vec::new(), last_file_touched: None, }) } @@ -6228,17 +6231,17 @@ mod real_llm { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = tokio::time::timeout( std::time::Duration::from_secs(120), @@ -6336,17 +6339,17 @@ mod real_llm { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = tokio::time::timeout( std::time::Duration::from_secs(120), @@ -6468,17 +6471,17 @@ mod real_llm { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = tokio::time::timeout( std::time::Duration::from_secs(120), @@ -6568,17 +6571,17 @@ mod real_llm { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = tokio::time::timeout( std::time::Duration::from_secs(30), @@ -6661,17 +6664,17 @@ async fn human_gate_freeform_only_routes_text() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -6777,9 +6780,9 @@ async fn human_gate_freeform_with_fixed_choice_match() { // Answer selects "A" which matches the Approve choice let answers = VecDeque::from([Answer { - value: AnswerValue::Selected("A".to_string()), + value: AnswerValue::Selected("A".to_string()), selected_option: None, - text: None, + text: None, }]); let interviewer = Arc::new(QueueInterviewer::new(answers)); @@ -6791,17 +6794,17 @@ async fn human_gate_freeform_with_fixed_choice_match() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -6907,17 +6910,17 @@ async fn human_gate_freeform_fallback_on_unmatched_text() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -7018,9 +7021,9 @@ async fn human_gate_freeform_sets_allow_freeform_on_question() { graph.edges.push(Edge::new("freeform_target", "exit")); let answers = VecDeque::from([Answer { - value: AnswerValue::Selected("A".to_string()), + value: AnswerValue::Selected("A".to_string()), selected_option: None, - text: None, + text: None, }]); let inner = QueueInterviewer::new(answers); let recorder = Arc::new(RecordingInterviewer::new(Box::new(inner))); @@ -7034,17 +7037,17 @@ async fn human_gate_freeform_sets_allow_freeform_on_question() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = engine .run(&graph, &run_options) @@ -7126,9 +7129,9 @@ async fn human_gate_without_freeform_sets_allow_freeform_false() { graph.edges.push(Edge::new("reject", "exit")); let answers = VecDeque::from([Answer { - value: AnswerValue::Selected("A".to_string()), + value: AnswerValue::Selected("A".to_string()), selected_option: None, - text: None, + text: None, }]); let inner = QueueInterviewer::new(answers); let recorder = Arc::new(RecordingInterviewer::new(Box::new(inner))); @@ -7142,17 +7145,17 @@ async fn human_gate_without_freeform_sets_allow_freeform_false() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = engine .run(&graph, &run_options) @@ -7378,12 +7381,12 @@ fn hook_runner_from_defs(hooks: Vec) -> Arc, + emitter: Arc, hook_runner: Arc, } impl HookTestRunner { - async fn run(&self, graph: &Graph, run_options: &RunOptions) -> Result { + async fn run(&self, graph: &Graph, run_options: &RunOptions) -> Result { run_graph_with_hooks( make_linear_registry(), Arc::clone(&self.emitter), @@ -7400,7 +7403,7 @@ impl HookTestRunner { &self, graph: &Graph, run_options: &RunOptions, - ) -> Result<(Outcome, fabro_store::RunProjection), FabroError> { + ) -> Result<(Outcome, fabro_store::RunProjection), Error> { Box::pin( fabro_workflow::test_support::run_graph_with_hooks_and_state( make_linear_registry(), @@ -7424,7 +7427,7 @@ fn emitter_with_events() -> (Arc, Arc>>) fn engine_with_hooks(hooks: Vec) -> HookTestRunner { HookTestRunner { - emitter: Arc::new(Emitter::default()), + emitter: Arc::new(Emitter::default()), hook_runner: hook_runner_from_defs(hooks), } } @@ -7444,17 +7447,17 @@ fn engine_with_hooks_and_events( fn make_run_options(dir: &std::path::Path) -> RunOptions { RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.to_path_buf(), - cancel_token: None, - run_id: test_run_id("hook-test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.to_path_buf(), + cancel_token: None, + run_id: test_run_id("hook-test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, } } @@ -8039,26 +8042,26 @@ async fn hook_config_merge_concatenates() { let server_hooks = HookSettings { hooks: vec![HookDefinition { - name: Some("server-hook".into()), - event: HookEvent::RunStart, - command: Some("exit 0".into()), - hook_type: None, - matcher: None, - blocking: None, + name: Some("server-hook".into()), + event: HookEvent::RunStart, + command: Some("exit 0".into()), + hook_type: None, + matcher: None, + blocking: None, timeout_ms: None, - sandbox: Some(false), + sandbox: Some(false), }], }; let run_hooks = HookSettings { hooks: vec![HookDefinition { - name: Some("run-hook".into()), - event: HookEvent::StageComplete, - command: Some("exit 0".into()), - hook_type: None, - matcher: None, - blocking: None, + name: Some("run-hook".into()), + event: HookEvent::StageComplete, + command: Some("exit 0".into()), + hook_type: None, + matcher: None, + blocking: None, timeout_ms: None, - sandbox: Some(false), + sandbox: Some(false), }], }; @@ -8074,26 +8077,26 @@ async fn hook_config_merge_run_overrides_by_name() { let server_hooks = HookSettings { hooks: vec![HookDefinition { - name: Some("shared".into()), - event: HookEvent::RunStart, - command: Some("exit 1".into()), // would block - hook_type: None, - matcher: None, - blocking: None, + name: Some("shared".into()), + event: HookEvent::RunStart, + command: Some("exit 1".into()), // would block + hook_type: None, + matcher: None, + blocking: None, timeout_ms: None, - sandbox: Some(false), + sandbox: Some(false), }], }; let run_hooks = HookSettings { hooks: vec![HookDefinition { - name: Some("shared".into()), - event: HookEvent::RunStart, - command: Some("exit 0".into()), // allows - hook_type: None, - matcher: None, - blocking: None, + name: Some("shared".into()), + event: HookEvent::RunStart, + command: Some("exit 0".into()), // allows + hook_type: None, + matcher: None, + blocking: None, timeout_ms: None, - sandbox: Some(false), + sandbox: Some(false), }], }; @@ -8382,17 +8385,17 @@ async fn run_fidelity_prompt_pipeline(fidelity: &str) -> String { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (_outcome, state) = engine .run_with_state(&graph, &run_options) @@ -8583,17 +8586,17 @@ async fn large_context_values_are_offloaded_to_artifact_store() { let events = collect_events(&emitter); let engine = WorkflowRunner::new(registry, Arc::new(emitter), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, _state) = engine .run_with_state(&graph, &run_options) @@ -8642,16 +8645,16 @@ async fn large_context_values_are_offloaded_to_artifact_store() { /// A mock sandbox where `file_exists` always returns false, /// simulating a remote container that doesn't have local artifact files. struct RemoteMockEnv { - working_dir: String, - written: std::sync::Mutex>, + working_dir: String, + written: std::sync::Mutex>, existing_paths: std::sync::Mutex>, } impl RemoteMockEnv { fn new(working_dir: &str) -> Self { Self { - working_dir: working_dir.to_string(), - written: std::sync::Mutex::new(Vec::new()), + working_dir: working_dir.to_string(), + written: std::sync::Mutex::new(Vec::new()), existing_paths: std::sync::Mutex::new(std::collections::HashSet::new()), } } @@ -8787,17 +8790,17 @@ async fn artifact_pointers_rewritten_for_remote_sandbox() { let remote_env = Arc::new(RemoteMockEnv::new("/sandbox")); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), remote_env.clone()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, _state) = engine .run_with_state(&graph, &run_options) @@ -8868,23 +8871,23 @@ async fn downstream_local_execution_materializes_blob_refs_to_runtime_files() { "capture_context", Box::new(ContextValueCaptureHandler { values: Arc::clone(&captured), - key: "response.big_output".to_string(), + key: "response.big_output".to_string(), }), ); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, _state) = engine .run_with_state(&graph, &run_options) @@ -8954,24 +8957,24 @@ async fn downstream_remote_execution_materializes_blob_refs_to_sandbox_files() { "capture_context", Box::new(ContextValueCaptureHandler { values: Arc::clone(&captured), - key: "response.big_output".to_string(), + key: "response.big_output".to_string(), }), ); let remote_env = Arc::new(RemoteMockEnv::new("/sandbox")); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), remote_env.clone()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, _state) = engine .run_with_state(&graph, &run_options) @@ -9026,7 +9029,7 @@ async fn node_dir_uses_visit_count_on_revisit() { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let n = self .call_count .fetch_add(1, std::sync::atomic::Ordering::SeqCst); @@ -9091,17 +9094,17 @@ async fn node_dir_uses_visit_count_on_revisit() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -9136,25 +9139,25 @@ async fn node_dir_uses_visit_count_on_revisit() { /// responses based on command content. struct CliTestEnv { /// All commands passed to exec_command, in order. - commands: std::sync::Mutex>, + commands: std::sync::Mutex>, /// All (path, content) pairs from write_file. - written_files: std::sync::Mutex>, + written_files: std::sync::Mutex>, /// The stdout to return when the CLI command (not git) is executed. - cli_stdout: String, + cli_stdout: String, /// Files returned by "git diff --name-only" AFTER the CLI runs. /// First call returns empty (before), second returns these (after). git_diff_call_count: std::sync::atomic::AtomicU32, - git_diff_after: String, + git_diff_after: String, } impl CliTestEnv { fn new(cli_stdout: &str) -> Self { Self { - commands: std::sync::Mutex::new(Vec::new()), - written_files: std::sync::Mutex::new(Vec::new()), - cli_stdout: cli_stdout.to_string(), + commands: std::sync::Mutex::new(Vec::new()), + written_files: std::sync::Mutex::new(Vec::new()), + cli_stdout: cli_stdout.to_string(), git_diff_call_count: std::sync::atomic::AtomicU32::new(0), - git_diff_after: String::new(), + git_diff_after: String::new(), } } @@ -9241,10 +9244,10 @@ impl fabro_agent::Sandbox for CliTestEnv { // Background launch: return PID if command.contains("echo $!") { return Ok(fabro_agent::ExecResult { - stdout: "12345\n".into(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: "12345\n".into(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 1, }); } @@ -9252,10 +9255,10 @@ impl fabro_agent::Sandbox for CliTestEnv { // Poll for completion: return exit code 0 immediately if command.contains("exit_code") && command.contains("echo running") { return Ok(fabro_agent::ExecResult { - stdout: "0\n".into(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: "0\n".into(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 1, }); } @@ -9263,10 +9266,10 @@ impl fabro_agent::Sandbox for CliTestEnv { // Read stdout file if command.starts_with("cat") && command.contains("stdout.log") { return Ok(fabro_agent::ExecResult { - stdout: self.cli_stdout.clone(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: self.cli_stdout.clone(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 1, }); } @@ -9274,10 +9277,10 @@ impl fabro_agent::Sandbox for CliTestEnv { // Read stderr file if command.starts_with("cat") && command.contains("stderr.log") { return Ok(fabro_agent::ExecResult { - stdout: String::new(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: String::new(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 1, }); } @@ -9285,20 +9288,20 @@ impl fabro_agent::Sandbox for CliTestEnv { // Cleanup temp files if command.starts_with("rm -f") { return Ok(fabro_agent::ExecResult { - stdout: String::new(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: String::new(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 1, }); } // Fallback Ok(fabro_agent::ExecResult { - stdout: self.cli_stdout.clone(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: self.cli_stdout.clone(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 100, }) } @@ -9534,48 +9537,48 @@ async fn cli_backend_run_fails_on_nonzero_exit() { ) -> Result { if command.starts_with("git") { return Ok(fabro_agent::ExecResult { - stdout: String::new(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: String::new(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 0, }); } // Background launch: return PID if command.contains("echo $!") { return Ok(fabro_agent::ExecResult { - stdout: "12345\n".into(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: "12345\n".into(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 0, }); } // Poll: return non-zero exit code if command.contains("exit_code") && command.contains("echo running") { return Ok(fabro_agent::ExecResult { - stdout: "127\n".into(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: "127\n".into(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 0, }); } // Read stderr file if command.starts_with("cat") && command.contains("stderr.log") { return Ok(fabro_agent::ExecResult { - stdout: "command not found: claude".into(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: "command not found: claude".into(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 0, }); } Ok(fabro_agent::ExecResult { - stdout: String::new(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: String::new(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 0, }) } @@ -9961,17 +9964,17 @@ async fn full_pipeline_with_cli_backend_node() { let dir = tempfile::tempdir().unwrap(); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), env); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -10079,17 +10082,17 @@ async fn stylesheet_backend_property_routes_to_cli() { let dir = tempfile::tempdir().unwrap(); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), env); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -10170,14 +10173,14 @@ impl Handler for FileWriterHandler { _graph: &Graph, _run_dir: &Path, services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let work_dir = services.sandbox.working_directory().to_string(); let file_path = format!("{}/{}.txt", work_dir, node.id); services .sandbox .write_file(&file_path, &format!("written by {}", node.id)) .await - .map_err(|e| FabroError::handler(format!("write_file failed: {e}")))?; + .map_err(|e| Error::handler(format!("write_file failed: {e}")))?; Ok(Outcome::success()) } } @@ -10274,19 +10277,19 @@ async fn git_checkpoint_host_emits_events_and_diff_patch() { let engine = WorkflowRunner::new(registry, Arc::new(emitter), env); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: run_dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-docker"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: run_dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-docker"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: Some(worktree_path.clone()), - git: Some(GitCheckpointOptions { - base_sha: Some(base_sha.clone()), - run_branch: Some(run_branch), + host_repo_path: Some(worktree_path.clone()), + git: Some(GitCheckpointOptions { + base_sha: Some(base_sha.clone()), + run_branch: Some(run_branch), meta_branch: None, }), }; @@ -10452,8 +10455,8 @@ async fn git_checkpoint_host_writes_shadow_branch() { display_base_sha: None, host_repo_path: Some(worktree_path.clone()), git: Some(GitCheckpointOptions { - base_sha: Some(base_sha), - run_branch: Some(format!("fabro/run/{run_id}")), + base_sha: Some(base_sha), + run_branch: Some(format!("fabro/run/{run_id}")), meta_branch: Some(meta_branch), }), }; @@ -10651,8 +10654,8 @@ async fn parallel_git_branching_host_e2e() { display_base_sha: None, host_repo_path: Some(worktree_path.clone()), git: Some(GitCheckpointOptions { - base_sha: Some(base_sha.clone()), - run_branch: Some(run_branch.clone()), + base_sha: Some(base_sha.clone()), + run_branch: Some(run_branch.clone()), meta_branch: None, }), }; @@ -10889,19 +10892,19 @@ async fn git_checkpoint_host_skips_empty_diff_patch() { let engine = WorkflowRunner::new(registry, Arc::new(emitter), env); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: run_dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("empty-diff"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: run_dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("empty-diff"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: Some(worktree_path.clone()), - git: Some(GitCheckpointOptions { - base_sha: Some(base_sha.clone()), - run_branch: Some(run_branch), + host_repo_path: Some(worktree_path.clone()), + git: Some(GitCheckpointOptions { + base_sha: Some(base_sha.clone()), + run_branch: Some(run_branch), meta_branch: None, }), }; @@ -10945,7 +10948,7 @@ impl Handler for DeterministicFailHandler { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { Ok(Outcome::fail_classify(&self.reason)) } } @@ -10962,7 +10965,7 @@ impl Handler for TransientInfraFailHandler { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { Ok(Outcome::fail_classify("connection refused")) } } @@ -10980,7 +10983,7 @@ impl Handler for SignatureHintHandler { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { Ok( Outcome::fail_classify("error at line 42 in commit abc123def0") .with_signature(Some("custom-grouping-key")), @@ -11016,7 +11019,7 @@ impl Handler for VaryingReasonFailHandler { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let n = self .counter .fetch_add(1, std::sync::atomic::Ordering::SeqCst) as usize; @@ -11030,7 +11033,7 @@ impl Handler for VaryingReasonFailHandler { /// before that. struct SucceedOnNthHandler { succeed_on: u32, - counter: std::sync::atomic::AtomicU32, + counter: std::sync::atomic::AtomicU32, } #[async_trait::async_trait] @@ -11042,7 +11045,7 @@ impl Handler for SucceedOnNthHandler { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let n = self .counter .fetch_add(1, std::sync::atomic::Ordering::SeqCst); @@ -11259,17 +11262,17 @@ async fn e2e_circuit_breaker_deterministic_self_loop() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("e2e-circuit-breaker"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("e2e-circuit-breaker"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let result = engine.run(&graph, &run_options).await; assert!(result.is_err(), "pipeline should abort, not loop forever"); @@ -11305,17 +11308,17 @@ async fn e2e_circuit_breaker_custom_limit() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("e2e-custom-limit"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("e2e-custom-limit"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let result = engine.run(&graph, &run_options).await; assert!(result.is_err()); @@ -11344,17 +11347,17 @@ async fn e2e_circuit_breaker_ignores_transient_failures() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("e2e-transient-no-breaker"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("e2e-transient-no-breaker"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let result = engine.run(&graph, &run_options).await; assert!(result.is_err()); @@ -11390,17 +11393,17 @@ async fn e2e_circuit_breaker_different_reasons_separate_counters() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("e2e-varying-reasons"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("e2e-varying-reasons"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let result = engine.run(&graph, &run_options).await; assert!(result.is_err()); @@ -11429,17 +11432,17 @@ async fn e2e_circuit_breaker_loop_restart() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("e2e-restart-breaker"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("e2e-restart-breaker"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let result = engine.run(&graph, &run_options).await; assert!( @@ -11491,17 +11494,17 @@ async fn e2e_failure_signature_persisted_in_context() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("e2e-sig-context"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("e2e-sig-context"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine.run_with_state(&graph, &run_options).await.unwrap(); // Pipeline reaches exit (terminal) with goal gates satisfied. @@ -11554,17 +11557,17 @@ async fn e2e_failure_signature_hint_overrides_reason_in_context() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("e2e-sig-hint"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("e2e-sig-hint"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (_outcome, state) = engine.run_with_state(&graph, &run_options).await.unwrap(); @@ -11605,23 +11608,23 @@ async fn e2e_signature_maps_persist_in_checkpoint() { "test_handler", Box::new(SucceedOnNthHandler { succeed_on: 3, - counter: std::sync::atomic::AtomicU32::new(0), + counter: std::sync::atomic::AtomicU32::new(0), }), ); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("e2e-sig-persist"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("e2e-sig-persist"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine.run_with_state(&graph, &run_options).await.unwrap(); assert_eq!(outcome.status, StageStatus::Success); @@ -11738,17 +11741,17 @@ async fn e2e_circuit_breaker_emits_events_before_abort() { let engine = WorkflowRunner::new(registry, Arc::new(emitter), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("e2e-events"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("e2e-events"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let result = engine.run(&graph, &run_options).await; assert!(result.is_err()); @@ -11798,23 +11801,23 @@ async fn e2e_circuit_breaker_does_not_fire_below_limit() { "test_handler", Box::new(SucceedOnNthHandler { succeed_on: 4, - counter: std::sync::atomic::AtomicU32::new(0), + counter: std::sync::atomic::AtomicU32::new(0), }), ); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("e2e-below-limit"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("e2e-below-limit"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine.run_with_state(&graph, &run_options).await.unwrap(); assert_eq!( @@ -11899,17 +11902,17 @@ async fn e2e_circuit_breaker_multi_stage_impl_verify_cycle() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("e2e-impl-verify-cycle"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("e2e-impl-verify-cycle"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let result = engine.run(&graph, &run_options).await; assert!( @@ -11933,8 +11936,8 @@ async fn e2e_circuit_breaker_multi_stage_impl_verify_cycle() { /// Nth call. struct ClassifiedFailHandler { failure_class: &'static str, - succeed_on: u32, - counter: std::sync::atomic::AtomicU32, + succeed_on: u32, + counter: std::sync::atomic::AtomicU32, } impl ClassifiedFailHandler { @@ -11964,7 +11967,7 @@ impl Handler for ClassifiedFailHandler { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let n = self .counter .fetch_add(1, std::sync::atomic::Ordering::SeqCst); @@ -11996,17 +11999,17 @@ async fn e2e_loop_restart_blocked_for_deterministic_failure() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("e2e-restart-blocked-det"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("e2e-restart-blocked-det"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let result = engine.run(&graph, &run_options).await; assert!( @@ -12035,17 +12038,17 @@ async fn e2e_loop_restart_blocked_for_structural_failure() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("e2e-restart-blocked-struct"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("e2e-restart-blocked-struct"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let result = engine.run(&graph, &run_options).await; assert!( @@ -12074,17 +12077,17 @@ async fn e2e_loop_restart_blocked_for_budget_exhausted_failure() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("e2e-restart-blocked-budget"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("e2e-restart-blocked-budget"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let result = engine.run(&graph, &run_options).await; assert!( @@ -12113,17 +12116,17 @@ async fn e2e_loop_restart_blocked_for_canceled_failure() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("e2e-restart-blocked-canceled"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("e2e-restart-blocked-canceled"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let result = engine.run(&graph, &run_options).await; assert!(result.is_err(), "canceled failure should not loop_restart"); @@ -12149,17 +12152,17 @@ async fn e2e_loop_restart_blocked_for_compilation_loop_failure() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("e2e-restart-blocked-comploop"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("e2e-restart-blocked-comploop"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let result = engine.run(&graph, &run_options).await; assert!( @@ -12189,17 +12192,17 @@ async fn e2e_loop_restart_allowed_for_transient_infra() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("e2e-restart-allowed-transient"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("e2e-restart-allowed-transient"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let result = engine.run(&graph, &run_options).await; assert!( @@ -12225,7 +12228,7 @@ impl Handler for HangingHandler { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { tokio::time::sleep(std::time::Duration::from_secs(60)).await; Ok(Outcome::success()) } @@ -12234,7 +12237,7 @@ impl Handler for HangingHandler { /// Handler that emits keepalive events periodically, then succeeds. struct KeepaliveHandler { interval_ms: u64, - total_ms: u64, + total_ms: u64, } #[async_trait::async_trait] @@ -12246,17 +12249,17 @@ impl Handler for KeepaliveHandler { _graph: &Graph, _run_dir: &Path, services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { let start = std::time::Instant::now(); while start.elapsed() < std::time::Duration::from_millis(self.total_ms) { tokio::time::sleep(std::time::Duration::from_millis(self.interval_ms)).await; services.emitter.emit(&Event::Prompt { - stage: node.id.clone(), - visit: 1, - text: "keepalive".to_string(), - mode: None, + stage: node.id.clone(), + visit: 1, + text: "keepalive".to_string(), + mode: None, provider: None, - model: None, + model: None, }); } Ok(Outcome::success()) @@ -12296,17 +12299,17 @@ async fn e2e_stall_watchdog_triggers_from_dot_parsed_pipeline() { let engine = WorkflowRunner::new(registry, Arc::new(emitter), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("stall-e2e"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("stall-e2e"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let result = engine.run(&graph, &run_options).await; assert!(result.is_err(), "expected stall watchdog error"); @@ -12345,23 +12348,23 @@ async fn e2e_stall_watchdog_kept_alive_by_handler_events() { "keepalive", Box::new(KeepaliveHandler { interval_ms: 10, - total_ms: 50, + total_ms: 50, }), ); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("stall-alive-e2e"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("stall-alive-e2e"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = engine .run(&graph, &run_options) @@ -12396,17 +12399,17 @@ async fn e2e_stall_watchdog_disabled_with_zero_timeout() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("stall-disabled-e2e"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("stall-disabled-e2e"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = engine .run(&graph, &run_options) @@ -12430,7 +12433,7 @@ impl Handler for SlowTestHandler { _graph: &Graph, _run_dir: &Path, _services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { tokio::time::sleep(std::time::Duration::from_millis(self.sleep_ms)).await; Ok(Outcome::success()) } @@ -12461,17 +12464,17 @@ async fn e2e_stall_watchdog_with_explicit_timeout_override() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("stall-override-e2e"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("stall-override-e2e"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let start = std::time::Instant::now(); let result = engine.run(&graph, &run_options).await; @@ -12518,7 +12521,7 @@ impl Handler for AssetCreatorHandler { _graph: &Graph, _run_dir: &Path, services: &fabro_workflow::handler::EngineServices, - ) -> Result { + ) -> Result { // Create artifact files via the sandbox's exec_command let script = concat!( "mkdir -p test-results && ", @@ -12529,7 +12532,7 @@ impl Handler for AssetCreatorHandler { .sandbox .exec_command(script, 30_000, None, None, None) .await - .map_err(|e| FabroError::handler(format!("exec failed: {e}")))?; + .map_err(|e| Error::handler(format!("exec failed: {e}")))?; if self.should_fail { Ok(Outcome::fail_classify("intentional failure")) @@ -12593,7 +12596,7 @@ async fn asset_collection_local_sandbox_success() { graph.edges.push(Edge::new("create_assets", "exit")); let run_options = RunOptions { - settings: SettingsLayer { + settings: SettingsLayer { run: Some(RunLayer { artifacts: Some(RunArtifactsLayer { include: vec!["test-results/**".to_string()], @@ -12602,16 +12605,16 @@ async fn asset_collection_local_sandbox_success() { }), ..SettingsLayer::default() }, - run_dir: run_dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("artifact-test-local"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + run_dir: run_dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("artifact-test-local"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = engine .run(&graph, &run_options) @@ -12725,7 +12728,7 @@ async fn asset_collection_local_sandbox_on_failure() { graph.edges.push(Edge::new("create_assets", "exit")); let run_options = RunOptions { - settings: SettingsLayer { + settings: SettingsLayer { run: Some(RunLayer { artifacts: Some(RunArtifactsLayer { include: vec!["test-results/**".to_string()], @@ -12734,16 +12737,16 @@ async fn asset_collection_local_sandbox_on_failure() { }), ..SettingsLayer::default() }, - run_dir: run_dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("artifact-test-fail"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + run_dir: run_dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("artifact-test-fail"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = engine .run(&graph, &run_options) @@ -12830,7 +12833,7 @@ async fn asset_collection_docker_sandbox() { graph.edges.push(Edge::new("create_assets", "exit")); let run_options = RunOptions { - settings: SettingsLayer { + settings: SettingsLayer { run: Some(RunLayer { artifacts: Some(RunArtifactsLayer { include: vec!["test-results/**".to_string()], @@ -12839,16 +12842,16 @@ async fn asset_collection_docker_sandbox() { }), ..SettingsLayer::default() }, - run_dir: run_dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("artifact-test-docker"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + run_dir: run_dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("artifact-test-docker"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = engine .run(&graph, &run_options) @@ -12906,17 +12909,17 @@ async fn wait_timer_e2e() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = engine.run(&graph, &run_options).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); diff --git a/test/twin/github/src/fixtures.rs b/test/twin/github/src/fixtures.rs index bd341b559..ba67c9ab6 100644 --- a/test/twin/github/src/fixtures.rs +++ b/test/twin/github/src/fixtures.rs @@ -14,92 +14,92 @@ use crate::state::{ #[derive(Debug, Clone, Deserialize, Default)] pub struct FixtureState { #[serde(default)] - pub apps: Vec, + pub apps: Vec, #[serde(default)] - pub installations: Vec, + pub installations: Vec, #[serde(default)] - pub repositories: Vec, + pub repositories: Vec, #[serde(default)] - pub pull_requests: Vec, + pub pull_requests: Vec, #[serde(default)] - pub active_tokens: Vec, + pub active_tokens: Vec, #[serde(default)] - pub projects: Vec, + pub projects: Vec, #[serde(default)] - pub releases: Vec, + pub releases: Vec, #[serde(default)] pub manifest_conversions: Vec, #[serde(default)] - pub comments: Vec, + pub comments: Vec, #[serde(default)] - pub webhook_config: FixtureWebhookOptions, + pub webhook_config: FixtureWebhookOptions, pub next_installation_id: Option, - pub next_pr_number: Option, - pub viewer_id: Option, + pub next_pr_number: Option, + pub viewer_id: Option, } #[derive(Debug, Clone, Deserialize)] pub struct FixtureApp { - pub app_id: String, - pub slug: String, - pub owner_login: String, - pub public: bool, + pub app_id: String, + pub slug: String, + pub owner_login: String, + pub public: bool, pub private_key_pem: String, - pub webhook_secret: Option, + pub webhook_secret: Option, } #[derive(Debug, Clone, Deserialize)] pub struct FixtureInstallation { - pub id: u64, - pub app_id: String, - pub owner: String, + pub id: u64, + pub app_id: String, + pub owner: String, #[serde(default)] pub repositories: Vec, #[serde(default)] - pub suspended: bool, + pub suspended: bool, } #[derive(Debug, Clone, Deserialize)] pub struct FixtureRepository { - pub owner: String, - pub name: String, + pub owner: String, + pub name: String, #[serde(default)] - pub branches: Vec, + pub branches: Vec, pub default_branch: Option, #[serde(default)] - pub private: bool, + pub private: bool, } #[derive(Debug, Clone, Deserialize)] pub struct FixturePullRequest { - pub owner: String, - pub repo: String, + pub owner: String, + pub repo: String, #[serde(flatten)] pub pull_request: PullRequest, } #[derive(Debug, Clone, Deserialize)] pub struct FixtureActiveToken { - pub token: String, - pub app_id: String, + pub token: String, + pub app_id: String, pub installation_id: u64, #[serde(default)] - pub repositories: Vec, + pub repositories: Vec, #[serde(default = "empty_json_object")] - pub permissions: serde_json::Value, + pub permissions: serde_json::Value, } #[derive(Debug, Clone, Deserialize)] pub struct FixtureProject { - pub node_id: String, - pub number: u64, - pub owner: String, - pub owner_type: FixtureOwnerType, + pub node_id: String, + pub number: u64, + pub owner: String, + pub owner_type: FixtureOwnerType, pub status_field_id: String, #[serde(default)] - pub status_options: Vec, + pub status_options: Vec, #[serde(default)] - pub items: Vec, + pub items: Vec, } #[derive(Debug, Clone, Deserialize)] @@ -111,59 +111,59 @@ pub enum FixtureOwnerType { #[derive(Debug, Clone, Deserialize)] pub struct FixtureStatusOption { - pub id: String, + pub id: String, pub name: String, } #[derive(Debug, Clone, Deserialize)] pub struct FixtureProjectItem { - pub id: String, - pub status: String, + pub id: String, + pub status: String, pub content: FixtureIssueContent, } #[derive(Debug, Clone, Deserialize)] pub struct FixtureIssueContent { - pub id: String, - pub number: u64, - pub title: String, - pub body: String, - pub url: String, - pub created_at: String, - pub updated_at: String, + pub id: String, + pub number: u64, + pub title: String, + pub body: String, + pub url: String, + pub created_at: String, + pub updated_at: String, #[serde(default)] pub assignee_ids: Vec, #[serde(default)] - pub labels: Vec, + pub labels: Vec, } #[derive(Debug, Clone, Deserialize)] pub struct FixtureRelease { - pub owner: String, - pub repo: String, + pub owner: String, + pub repo: String, pub tag_name: String, } #[derive(Debug, Clone, Deserialize)] pub struct FixtureManifestConversion { - pub code: String, - pub app_id: i64, - pub slug: String, - pub client_id: String, - pub client_secret: String, + pub code: String, + pub app_id: i64, + pub slug: String, + pub client_id: String, + pub client_secret: String, pub webhook_secret: Option, - pub pem: String, + pub pem: String, } #[derive(Debug, Clone, Deserialize)] pub struct FixtureComment { pub issue_node_id: String, - pub body: String, + pub body: String, } #[derive(Debug, Clone, Deserialize, Default)] pub struct FixtureWebhookOptions { - pub url: Option, + pub url: Option, pub content_type: Option, } @@ -182,12 +182,12 @@ impl FixtureState { for app in self.apps { let app_id = app.app_id.clone(); let config = AppOptions { - app_id: app.app_id, - slug: app.slug, - owner_login: app.owner_login, - public: app.public, + app_id: app.app_id, + slug: app.slug, + owner_login: app.owner_login, + public: app.public, private_key_pem: app.private_key_pem, - webhook_secret: app.webhook_secret, + webhook_secret: app.webhook_secret, }; catch_unwind(AssertUnwindSafe(|| state.register_app(config))) @@ -198,11 +198,11 @@ impl FixtureState { .installations .into_iter() .map(|installation| Installation { - id: installation.id, - app_id: installation.app_id, - owner: installation.owner, + id: installation.id, + app_id: installation.app_id, + owner: installation.owner, repositories: installation.repositories, - suspended: installation.suspended, + suspended: installation.suspended, }) .collect(); @@ -210,14 +210,14 @@ impl FixtureState { .repositories .into_iter() .map(|repository| Repository { - owner: repository.owner, - name: repository.name, - branches: repository.branches, + owner: repository.owner, + name: repository.name, + branches: repository.branches, default_branch: repository .default_branch .unwrap_or_else(|| "main".to_string()), - private: repository.private, - git_dir: None, + private: repository.private, + git_dir: None, }) .collect(); @@ -233,12 +233,15 @@ impl FixtureState { .active_tokens .into_iter() .map(|token| { - (token.token, TokenInfo { - app_id: token.app_id, - installation_id: token.installation_id, - repositories: token.repositories, - permissions: token.permissions, - }) + ( + token.token, + TokenInfo { + app_id: token.app_id, + installation_id: token.installation_id, + repositories: token.repositories, + permissions: token.permissions, + }, + ) }) .collect(); @@ -246,38 +249,38 @@ impl FixtureState { .projects .into_iter() .map(|project| Project { - node_id: project.node_id, - number: project.number, - owner: project.owner, - owner_type: match project.owner_type { + node_id: project.node_id, + number: project.number, + owner: project.owner, + owner_type: match project.owner_type { FixtureOwnerType::Organization => OwnerType::Organization, FixtureOwnerType::User => OwnerType::User, }, status_field_id: project.status_field_id, - status_options: project + status_options: project .status_options .into_iter() .map(|option| StatusOption { - id: option.id, + id: option.id, name: option.name, }) .collect(), - items: project + items: project .items .into_iter() .map(|item| ProjectItem { - id: item.id, - status: item.status, + id: item.id, + status: item.status, content: IssueContent { - id: item.content.id, - number: item.content.number, - title: item.content.title, - body: item.content.body, - url: item.content.url, - created_at: item.content.created_at, - updated_at: item.content.updated_at, + id: item.content.id, + number: item.content.number, + title: item.content.title, + body: item.content.body, + url: item.content.url, + created_at: item.content.created_at, + updated_at: item.content.updated_at, assignee_ids: item.content.assignee_ids, - labels: item.content.labels, + labels: item.content.labels, }, }) .collect(), @@ -288,9 +291,12 @@ impl FixtureState { .releases .into_iter() .map(|release| { - ((release.owner, release.repo), Release { - tag_name: release.tag_name, - }) + ( + (release.owner, release.repo), + Release { + tag_name: release.tag_name, + }, + ) }) .collect::>(); @@ -299,15 +305,18 @@ impl FixtureState { .into_iter() .map(|conversion| { let code = conversion.code.clone(); - (code, ManifestConversion { - code: conversion.code, - app_id: conversion.app_id, - slug: conversion.slug, - client_id: conversion.client_id, - client_secret: conversion.client_secret, - webhook_secret: conversion.webhook_secret, - pem: conversion.pem, - }) + ( + code, + ManifestConversion { + code: conversion.code, + app_id: conversion.app_id, + slug: conversion.slug, + client_id: conversion.client_id, + client_secret: conversion.client_secret, + webhook_secret: conversion.webhook_secret, + pem: conversion.pem, + }, + ) }) .collect::>(); @@ -316,12 +325,12 @@ impl FixtureState { .into_iter() .map(|comment| Comment { issue_node_id: comment.issue_node_id, - body: comment.body, + body: comment.body, }) .collect(); state.webhook_config = WebhookOptions { - url: self.webhook_config.url, + url: self.webhook_config.url, content_type: self.webhook_config.content_type, }; @@ -367,12 +376,12 @@ impl FixtureState { fn single_app_fixture_for_test() -> Self { Self { apps: vec![FixtureApp { - app_id: "100".to_string(), - slug: "fixture-app".to_string(), - owner_login: "acme".to_string(), - public: true, + app_id: "100".to_string(), + slug: "fixture-app".to_string(), + owner_login: "acme".to_string(), + public: true, private_key_pem: test_rsa_key().to_string(), - webhook_secret: Some("whsec".to_string()), + webhook_secret: Some("whsec".to_string()), }], ..Self::default() } diff --git a/test/twin/github/src/handlers/app.rs b/test/twin/github/src/handlers/app.rs index ec67fe395..58212dcde 100644 --- a/test/twin/github/src/handlers/app.rs +++ b/test/twin/github/src/handlers/app.rs @@ -120,12 +120,12 @@ mod tests { let pem = test_rsa_private_key(); let mut state = AppState::new(); state.register_app(AppOptions { - app_id: "12345".to_string(), - slug: "my-app".to_string(), - owner_login: "my-org".to_string(), - public: true, + app_id: "12345".to_string(), + slug: "my-app".to_string(), + owner_login: "my-org".to_string(), + public: true, private_key_pem: pem.to_string(), - webhook_secret: None, + webhook_secret: None, }); let server = TestServer::start(state).await; @@ -152,12 +152,12 @@ mod tests { let pem = test_rsa_private_key(); let mut state = AppState::new(); state.register_app(AppOptions { - app_id: "12345".to_string(), - slug: "my-app".to_string(), - owner_login: "my-org".to_string(), - public: true, + app_id: "12345".to_string(), + slug: "my-app".to_string(), + owner_login: "my-org".to_string(), + public: true, private_key_pem: pem.to_string(), - webhook_secret: None, + webhook_secret: None, }); let server = TestServer::start(state).await; @@ -178,12 +178,12 @@ mod tests { let pem = test_rsa_private_key(); let mut state = AppState::new(); state.register_app(AppOptions { - app_id: "12345".to_string(), - slug: "my-app".to_string(), - owner_login: "my-org".to_string(), - public: true, + app_id: "12345".to_string(), + slug: "my-app".to_string(), + owner_login: "my-org".to_string(), + public: true, private_key_pem: pem.to_string(), - webhook_secret: None, + webhook_secret: None, }); let server = TestServer::start(state).await; @@ -209,12 +209,12 @@ mod tests { let pem = test_rsa_private_key(); let mut state = AppState::new(); state.register_app(AppOptions { - app_id: "12345".to_string(), - slug: "private-app".to_string(), - owner_login: "my-org".to_string(), - public: false, + app_id: "12345".to_string(), + slug: "private-app".to_string(), + owner_login: "my-org".to_string(), + public: false, private_key_pem: pem.to_string(), - webhook_secret: None, + webhook_secret: None, }); let server = TestServer::start(state).await; diff --git a/test/twin/github/src/handlers/branches.rs b/test/twin/github/src/handlers/branches.rs index 46b1aacc2..a4a21b420 100644 --- a/test/twin/github/src/handlers/branches.rs +++ b/test/twin/github/src/handlers/branches.rs @@ -134,12 +134,12 @@ mod tests { let pem = test_rsa_private_key(); let mut state = AppState::new(); state.register_app(AppOptions { - app_id: "100".to_string(), - slug: "test-app".to_string(), - owner_login: "owner".to_string(), - public: true, + app_id: "100".to_string(), + slug: "test-app".to_string(), + owner_login: "owner".to_string(), + public: true, private_key_pem: pem.to_string(), - webhook_secret: None, + webhook_secret: None, }); state.add_installation("100", "owner", vec!["repo".to_string()], false); state.add_repository( @@ -177,12 +177,12 @@ mod tests { let pem = test_rsa_private_key(); let mut state = AppState::new(); state.register_app(AppOptions { - app_id: "100".to_string(), - slug: "test-app".to_string(), - owner_login: "owner".to_string(), - public: true, + app_id: "100".to_string(), + slug: "test-app".to_string(), + owner_login: "owner".to_string(), + public: true, private_key_pem: pem.to_string(), - webhook_secret: None, + webhook_secret: None, }); state.add_installation("100", "owner", vec!["repo".to_string()], false); state.add_repository("owner", "repo", vec!["main".to_string()], false); diff --git a/test/twin/github/src/handlers/graphql.rs b/test/twin/github/src/handlers/graphql.rs index d4f4985d6..9b2d292d1 100644 --- a/test/twin/github/src/handlers/graphql.rs +++ b/test/twin/github/src/handlers/graphql.rs @@ -160,7 +160,7 @@ async fn handle_enable_auto_merge( } } pr.auto_merge = Some(AutoMerge { - enabled_at: now.clone(), + enabled_at: now.clone(), merge_method: method.clone(), }); return ( @@ -665,12 +665,12 @@ mod tests { pem: &str, ) -> (TestServer, reqwest::Client, String) { state.register_app(AppOptions { - app_id: "100".to_string(), - slug: "test-app".to_string(), - owner_login: "owner".to_string(), - public: true, + app_id: "100".to_string(), + slug: "test-app".to_string(), + owner_login: "owner".to_string(), + public: true, private_key_pem: pem.to_string(), - webhook_secret: None, + webhook_secret: None, }); state.add_installation("100", "owner", vec!["repo".to_string()], false); state.add_repository("owner", "repo", vec!["main".to_string()], false); @@ -717,23 +717,23 @@ mod tests { .entry(("owner".to_string(), "repo".to_string())) .or_default() .push(PullRequest { - number: 1, - node_id: "PR_test123".to_string(), - title: "Test".to_string(), - body: String::new(), - state: "open".to_string(), - draft: false, - mergeable: true, - additions: 10, - deletions: 5, + number: 1, + node_id: "PR_test123".to_string(), + title: "Test".to_string(), + body: String::new(), + state: "open".to_string(), + draft: false, + mergeable: true, + additions: 10, + deletions: 5, changed_files: 2, - html_url: "https://github.com/owner/repo/pull/1".to_string(), - user_login: "test-bot[bot]".to_string(), - head_ref: "feature".to_string(), - base_ref: "main".to_string(), - created_at: "2026-01-01T00:00:00Z".to_string(), - updated_at: "2026-01-01T00:00:00Z".to_string(), - auto_merge: None, + html_url: "https://github.com/owner/repo/pull/1".to_string(), + user_login: "test-bot[bot]".to_string(), + head_ref: "feature".to_string(), + base_ref: "main".to_string(), + created_at: "2026-01-01T00:00:00Z".to_string(), + updated_at: "2026-01-01T00:00:00Z".to_string(), + auto_merge: None, }); let (server, client, token) = setup_with_token(&mut state, pem).await; @@ -794,38 +794,38 @@ mod tests { let pem = test_rsa_private_key(); let mut state = AppState::new(); state.projects.push(crate::state::Project { - node_id: "PVT_org123".to_string(), - number: 1, - owner: "owner".to_string(), - owner_type: crate::state::OwnerType::Organization, + node_id: "PVT_org123".to_string(), + number: 1, + owner: "owner".to_string(), + owner_type: crate::state::OwnerType::Organization, status_field_id: "PVTSSF_status1".to_string(), - status_options: vec![ + status_options: vec![ crate::state::StatusOption { - id: "opt1".to_string(), + id: "opt1".to_string(), name: "Todo".to_string(), }, crate::state::StatusOption { - id: "opt2".to_string(), + id: "opt2".to_string(), name: "In Progress".to_string(), }, crate::state::StatusOption { - id: "opt3".to_string(), + id: "opt3".to_string(), name: "Done".to_string(), }, ], - items: vec![crate::state::ProjectItem { - id: "PVTI_item1".to_string(), - status: "Todo".to_string(), + items: vec![crate::state::ProjectItem { + id: "PVTI_item1".to_string(), + status: "Todo".to_string(), content: crate::state::IssueContent { - id: "I_issue1".to_string(), - number: 42, - title: "Fix bug".to_string(), - body: "Description".to_string(), - url: "https://github.com/owner/repo/issues/42".to_string(), - created_at: "2026-01-01T00:00:00Z".to_string(), - updated_at: "2026-01-02T00:00:00Z".to_string(), + id: "I_issue1".to_string(), + number: 42, + title: "Fix bug".to_string(), + body: "Description".to_string(), + url: "https://github.com/owner/repo/issues/42".to_string(), + created_at: "2026-01-01T00:00:00Z".to_string(), + updated_at: "2026-01-02T00:00:00Z".to_string(), assignee_ids: vec![], - labels: vec!["bug".to_string()], + labels: vec!["bug".to_string()], }, }], }); @@ -864,25 +864,25 @@ mod tests { let pem = test_rsa_private_key(); let mut state = AppState::new(); state.projects.push(crate::state::Project { - node_id: "PVT_test".to_string(), - number: 1, - owner: "owner".to_string(), - owner_type: crate::state::OwnerType::Organization, + node_id: "PVT_test".to_string(), + number: 1, + owner: "owner".to_string(), + owner_type: crate::state::OwnerType::Organization, status_field_id: "PVTSSF_s".to_string(), - status_options: vec![], - items: vec![crate::state::ProjectItem { - id: "PVTI_1".to_string(), - status: "Todo".to_string(), + status_options: vec![], + items: vec![crate::state::ProjectItem { + id: "PVTI_1".to_string(), + status: "Todo".to_string(), content: crate::state::IssueContent { - id: "I_1".to_string(), - number: 1, - title: "Issue 1".to_string(), - body: "Body".to_string(), - url: "https://github.com/owner/repo/issues/1".to_string(), - created_at: "2026-01-01T00:00:00Z".to_string(), - updated_at: "2026-01-01T00:00:00Z".to_string(), + id: "I_1".to_string(), + number: 1, + title: "Issue 1".to_string(), + body: "Body".to_string(), + url: "https://github.com/owner/repo/issues/1".to_string(), + created_at: "2026-01-01T00:00:00Z".to_string(), + updated_at: "2026-01-01T00:00:00Z".to_string(), assignee_ids: vec!["U_user1".to_string()], - labels: vec!["bug".to_string(), "urgent".to_string()], + labels: vec!["bug".to_string(), "urgent".to_string()], }, }], }); @@ -948,34 +948,34 @@ mod tests { let pem = test_rsa_private_key(); let mut state = AppState::new(); state.projects.push(crate::state::Project { - node_id: "PVT_test".to_string(), - number: 1, - owner: "owner".to_string(), - owner_type: crate::state::OwnerType::Organization, + node_id: "PVT_test".to_string(), + number: 1, + owner: "owner".to_string(), + owner_type: crate::state::OwnerType::Organization, status_field_id: "PVTSSF_s".to_string(), - status_options: vec![ + status_options: vec![ crate::state::StatusOption { - id: "opt1".to_string(), + id: "opt1".to_string(), name: "Todo".to_string(), }, crate::state::StatusOption { - id: "opt2".to_string(), + id: "opt2".to_string(), name: "Done".to_string(), }, ], - items: vec![crate::state::ProjectItem { - id: "PVTI_1".to_string(), - status: "Todo".to_string(), + items: vec![crate::state::ProjectItem { + id: "PVTI_1".to_string(), + status: "Todo".to_string(), content: crate::state::IssueContent { - id: "I_1".to_string(), - number: 1, - title: "Issue 1".to_string(), - body: String::new(), - url: "https://github.com/owner/repo/issues/1".to_string(), - created_at: "2026-01-01T00:00:00Z".to_string(), - updated_at: "2026-01-01T00:00:00Z".to_string(), + id: "I_1".to_string(), + number: 1, + title: "Issue 1".to_string(), + body: String::new(), + url: "https://github.com/owner/repo/issues/1".to_string(), + created_at: "2026-01-01T00:00:00Z".to_string(), + updated_at: "2026-01-01T00:00:00Z".to_string(), assignee_ids: vec![], - labels: vec![], + labels: vec![], }, }], }); diff --git a/test/twin/github/src/handlers/installations.rs b/test/twin/github/src/handlers/installations.rs index 9422e2a0c..91bd6ca2e 100644 --- a/test/twin/github/src/handlers/installations.rs +++ b/test/twin/github/src/handlers/installations.rs @@ -185,12 +185,12 @@ mod tests { let pem = test_rsa_private_key(); let mut state = AppState::new(); state.register_app(AppOptions { - app_id: "100".to_string(), - slug: "test-app".to_string(), - owner_login: "owner".to_string(), - public: true, + app_id: "100".to_string(), + slug: "test-app".to_string(), + owner_login: "owner".to_string(), + public: true, private_key_pem: pem.to_string(), - webhook_secret: None, + webhook_secret: None, }); state.add_installation("100", "owner", vec!["repo".to_string()], false); state.add_repository("owner", "repo", vec!["main".to_string()], false); @@ -217,12 +217,12 @@ mod tests { let pem = test_rsa_private_key(); let mut state = AppState::new(); state.register_app(AppOptions { - app_id: "100".to_string(), - slug: "test-app".to_string(), - owner_login: "owner".to_string(), - public: true, + app_id: "100".to_string(), + slug: "test-app".to_string(), + owner_login: "owner".to_string(), + public: true, private_key_pem: pem.to_string(), - webhook_secret: None, + webhook_secret: None, }); // No installation added let server = TestServer::start(state).await; @@ -245,12 +245,12 @@ mod tests { let pem = test_rsa_private_key(); let mut state = AppState::new(); state.register_app(AppOptions { - app_id: "100".to_string(), - slug: "test-app".to_string(), - owner_login: "owner".to_string(), - public: true, + app_id: "100".to_string(), + slug: "test-app".to_string(), + owner_login: "owner".to_string(), + public: true, private_key_pem: pem.to_string(), - webhook_secret: None, + webhook_secret: None, }); state.add_installation("100", "owner", vec!["repo".to_string()], true); // suspended let server = TestServer::start(state).await; @@ -273,12 +273,12 @@ mod tests { let pem = test_rsa_private_key(); let mut state = AppState::new(); state.register_app(AppOptions { - app_id: "100".to_string(), - slug: "test-app".to_string(), - owner_login: "owner".to_string(), - public: true, + app_id: "100".to_string(), + slug: "test-app".to_string(), + owner_login: "owner".to_string(), + public: true, private_key_pem: pem.to_string(), - webhook_secret: None, + webhook_secret: None, }); let install_id = state.add_installation("100", "owner", vec!["repo".to_string()], false); let server = TestServer::start(state).await; @@ -311,12 +311,12 @@ mod tests { let pem = test_rsa_private_key(); let mut state = AppState::new(); state.register_app(AppOptions { - app_id: "100".to_string(), - slug: "test-app".to_string(), - owner_login: "owner".to_string(), - public: true, + app_id: "100".to_string(), + slug: "test-app".to_string(), + owner_login: "owner".to_string(), + public: true, private_key_pem: pem.to_string(), - webhook_secret: None, + webhook_secret: None, }); let install_id = state.add_installation("100", "owner", vec!["repo".to_string()], false); let server = TestServer::start(state).await; diff --git a/test/twin/github/src/handlers/manifests.rs b/test/twin/github/src/handlers/manifests.rs index c7711078a..a9fca6a6d 100644 --- a/test/twin/github/src/handlers/manifests.rs +++ b/test/twin/github/src/handlers/manifests.rs @@ -41,19 +41,19 @@ mod tests { #[tokio::test] async fn manifest_conversion_returns_app_credentials() { let mut state = AppState::new(); - state - .manifest_conversions - .insert("test-code".to_string(), ManifestConversion { - code: "test-code".to_string(), - app_id: 99, - slug: "test-dev".to_string(), - client_id: "Iv1.abc123".to_string(), - client_secret: "secret123".to_string(), + state.manifest_conversions.insert( + "test-code".to_string(), + ManifestConversion { + code: "test-code".to_string(), + app_id: 99, + slug: "test-dev".to_string(), + client_id: "Iv1.abc123".to_string(), + client_secret: "secret123".to_string(), webhook_secret: Some("whsecret".to_string()), - pem: - "-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----" - .to_string(), - }); + pem: "-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----" + .to_string(), + }, + ); let server = TestServer::start(state).await; let client = crate::test_support::test_http_client(); diff --git a/test/twin/github/src/handlers/pulls.rs b/test/twin/github/src/handlers/pulls.rs index 79041a121..d1dbbc959 100644 --- a/test/twin/github/src/handlers/pulls.rs +++ b/test/twin/github/src/handlers/pulls.rs @@ -344,12 +344,12 @@ mod tests { pem: &str, ) -> (TestServer, reqwest::Client, String) { state.register_app(AppOptions { - app_id: "100".to_string(), - slug: "test-app".to_string(), - owner_login: "owner".to_string(), - public: true, + app_id: "100".to_string(), + slug: "test-app".to_string(), + owner_login: "owner".to_string(), + public: true, private_key_pem: pem.to_string(), - webhook_secret: None, + webhook_secret: None, }); state.add_installation("100", "owner", vec!["repo".to_string()], false); state.add_repository( diff --git a/test/twin/github/src/server.rs b/test/twin/github/src/server.rs index 89fc93416..d06d38cf7 100644 --- a/test/twin/github/src/server.rs +++ b/test/twin/github/src/server.rs @@ -12,10 +12,10 @@ pub type SharedState = Arc>; /// A running test server instance. pub struct TestServer { - url: String, + url: String, shutdown_tx: Option>, - handle: Option>, - _git_root: TempDir, // Kept alive for the server's lifetime; cleaned up on drop + handle: Option>, + _git_root: TempDir, // Kept alive for the server's lifetime; cleaned up on drop } impl TestServer { diff --git a/test/twin/github/src/state.rs b/test/twin/github/src/state.rs index 0104a8b3a..cd6b057b1 100644 --- a/test/twin/github/src/state.rs +++ b/test/twin/github/src/state.rs @@ -8,18 +8,18 @@ const TEST_RSA_PUBLIC_PEM: &str = include_str!("testdata/rsa_public.pem"); /// Configuration for a registered GitHub App (user-facing input). #[derive(Debug, Clone)] pub struct AppOptions { - pub app_id: String, - pub slug: String, - pub owner_login: String, - pub public: bool, + pub app_id: String, + pub slug: String, + pub owner_login: String, + pub public: bool, pub private_key_pem: String, - pub webhook_secret: Option, + pub webhook_secret: Option, } /// Internal enriched app config with derived public key. #[derive(Debug, Clone)] pub struct RegisteredApp { - pub config: AppOptions, + pub config: AppOptions, /// Derived from `private_key_pem` during `register_app`. Used for JWT /// verification. pub public_key_pem: String, @@ -28,62 +28,62 @@ pub struct RegisteredApp { /// An installation of a GitHub App on a specific owner. #[derive(Debug, Clone)] pub struct Installation { - pub id: u64, - pub app_id: String, - pub owner: String, + pub id: u64, + pub app_id: String, + pub owner: String, pub repositories: Vec, - pub suspended: bool, + pub suspended: bool, } /// A repository in the fake. #[derive(Debug, Clone)] pub struct Repository { - pub owner: String, - pub name: String, - pub branches: Vec, + pub owner: String, + pub name: String, + pub branches: Vec, pub default_branch: String, - pub private: bool, - pub git_dir: Option, + pub private: bool, + pub git_dir: Option, } /// A pull request. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PullRequest { - pub number: u64, - pub node_id: String, - pub title: String, - pub body: String, - pub state: String, - pub draft: bool, - pub mergeable: bool, - pub additions: u64, - pub deletions: u64, + pub number: u64, + pub node_id: String, + pub title: String, + pub body: String, + pub state: String, + pub draft: bool, + pub mergeable: bool, + pub additions: u64, + pub deletions: u64, pub changed_files: u64, - pub html_url: String, - pub user_login: String, - pub head_ref: String, - pub base_ref: String, - pub created_at: String, - pub updated_at: String, - pub auto_merge: Option, + pub html_url: String, + pub user_login: String, + pub head_ref: String, + pub base_ref: String, + pub created_at: String, + pub updated_at: String, + pub auto_merge: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AutoMerge { - pub enabled_at: String, + pub enabled_at: String, pub merge_method: String, } /// A GitHub Projects V2 project. #[derive(Debug, Clone)] pub struct Project { - pub node_id: String, - pub number: u64, - pub owner: String, - pub owner_type: OwnerType, + pub node_id: String, + pub number: u64, + pub owner: String, + pub owner_type: OwnerType, pub status_field_id: String, - pub status_options: Vec, - pub items: Vec, + pub status_options: Vec, + pub items: Vec, } #[derive(Debug, Clone, PartialEq)] @@ -94,28 +94,28 @@ pub enum OwnerType { #[derive(Debug, Clone)] pub struct StatusOption { - pub id: String, + pub id: String, pub name: String, } #[derive(Debug, Clone)] pub struct ProjectItem { - pub id: String, - pub status: String, + pub id: String, + pub status: String, pub content: IssueContent, } #[derive(Debug, Clone)] pub struct IssueContent { - pub id: String, - pub number: u64, - pub title: String, - pub body: String, - pub url: String, - pub created_at: String, - pub updated_at: String, + pub id: String, + pub number: u64, + pub title: String, + pub body: String, + pub url: String, + pub created_at: String, + pub updated_at: String, pub assignee_ids: Vec, - pub labels: Vec, + pub labels: Vec, } /// A release. @@ -127,26 +127,26 @@ pub struct Release { /// An app manifest conversion record. #[derive(Debug, Clone)] pub struct ManifestConversion { - pub code: String, - pub app_id: i64, - pub slug: String, - pub client_id: String, - pub client_secret: String, + pub code: String, + pub app_id: i64, + pub slug: String, + pub client_id: String, + pub client_secret: String, pub webhook_secret: Option, - pub pem: String, + pub pem: String, } /// A comment on an issue. #[derive(Debug, Clone)] pub struct Comment { pub issue_node_id: String, - pub body: String, + pub body: String, } /// Stores webhook configuration. #[derive(Debug, Clone, Default)] pub struct WebhookOptions { - pub url: Option, + pub url: Option, pub content_type: Option, } @@ -178,18 +178,18 @@ pub enum TokenPermission { #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct TokenPermissions { - pub contents: PermissionLevel, - pub pull_requests: PermissionLevel, - pub issues: PermissionLevel, + pub contents: PermissionLevel, + pub pull_requests: PermissionLevel, + pub issues: PermissionLevel, pub organization_projects: PermissionLevel, } impl TokenPermissions { pub fn from_json(value: &serde_json::Value) -> Self { Self { - contents: PermissionLevel::from_json(value.get("contents")), - pull_requests: PermissionLevel::from_json(value.get("pull_requests")), - issues: PermissionLevel::from_json(value.get("issues")), + contents: PermissionLevel::from_json(value.get("contents")), + pull_requests: PermissionLevel::from_json(value.get("pull_requests")), + issues: PermissionLevel::from_json(value.get("issues")), organization_projects: PermissionLevel::from_json(value.get("organization_projects")), } } @@ -207,10 +207,10 @@ impl TokenPermissions { /// Info about an active installation access token. #[derive(Debug, Clone)] pub struct TokenInfo { - pub app_id: String, + pub app_id: String, pub installation_id: u64, - pub repositories: Vec, - pub permissions: serde_json::Value, + pub repositories: Vec, + pub permissions: serde_json::Value, } impl TokenInfo { @@ -230,19 +230,19 @@ impl TokenInfo { /// Central in-memory state for the fake GitHub server. #[derive(Debug, Clone)] pub struct AppState { - pub apps: HashMap, - pub installations: Vec, - pub repositories: Vec, - pub pull_requests: HashMap<(String, String), Vec>, - pub active_tokens: HashMap, - pub projects: Vec, - pub releases: HashMap<(String, String), Release>, + pub apps: HashMap, + pub installations: Vec, + pub repositories: Vec, + pub pull_requests: HashMap<(String, String), Vec>, + pub active_tokens: HashMap, + pub projects: Vec, + pub releases: HashMap<(String, String), Release>, pub manifest_conversions: HashMap, - pub comments: Vec, - pub webhook_config: WebhookOptions, + pub comments: Vec, + pub webhook_config: WebhookOptions, pub next_installation_id: u64, - pub next_pr_number: u64, - pub viewer_id: String, + pub next_pr_number: u64, + pub viewer_id: String, } /// Derive the RSA public key PEM from a private key PEM using the openssl CLI. @@ -279,29 +279,32 @@ pub fn derive_public_key_pem(private_key_pem: &str) -> String { impl AppState { pub fn new() -> Self { Self { - apps: HashMap::new(), - installations: Vec::new(), - repositories: Vec::new(), - pull_requests: HashMap::new(), - active_tokens: HashMap::new(), - projects: Vec::new(), - releases: HashMap::new(), + apps: HashMap::new(), + installations: Vec::new(), + repositories: Vec::new(), + pull_requests: HashMap::new(), + active_tokens: HashMap::new(), + projects: Vec::new(), + releases: HashMap::new(), manifest_conversions: HashMap::new(), - comments: Vec::new(), - webhook_config: WebhookOptions::default(), + comments: Vec::new(), + webhook_config: WebhookOptions::default(), next_installation_id: 1, - next_pr_number: 1, - viewer_id: "U_fakeviewer".to_string(), + next_pr_number: 1, + viewer_id: "U_fakeviewer".to_string(), } } pub fn register_app(&mut self, config: AppOptions) { let public_key_pem = derive_public_key_pem(&config.private_key_pem); let app_id = config.app_id.clone(); - self.apps.insert(app_id, RegisteredApp { - config, - public_key_pem, - }); + self.apps.insert( + app_id, + RegisteredApp { + config, + public_key_pem, + }, + ); } pub fn add_installation( @@ -358,12 +361,15 @@ impl AppState { permissions: serde_json::Value, ) -> String { let token = format!("ghs_{}", uuid::Uuid::new_v4().to_string().replace('-', "")); - self.active_tokens.insert(token.clone(), TokenInfo { - app_id: app_id.to_string(), - installation_id, - repositories, - permissions, - }); + self.active_tokens.insert( + token.clone(), + TokenInfo { + app_id: app_id.to_string(), + installation_id, + repositories, + permissions, + }, + ); token } @@ -465,12 +471,12 @@ mod tests { fn can_register_app() { let mut state = AppState::new(); state.register_app(AppOptions { - app_id: "12345".to_string(), - slug: "test-app".to_string(), - owner_login: "test-owner".to_string(), - public: true, + app_id: "12345".to_string(), + slug: "test-app".to_string(), + owner_login: "test-owner".to_string(), + public: true, private_key_pem: test_rsa_private_key().to_string(), - webhook_secret: Some("secret".to_string()), + webhook_secret: Some("secret".to_string()), }); assert_eq!(state.apps.len(), 1); assert_eq!(state.apps["12345"].config.slug, "test-app"); diff --git a/test/twin/openai/src/config.rs b/test/twin/openai/src/config.rs index 1bb77929a..73598f117 100644 --- a/test/twin/openai/src/config.rs +++ b/test/twin/openai/src/config.rs @@ -4,7 +4,7 @@ use anyhow::{Context, Result}; #[derive(Clone, Debug)] pub struct Config { - pub bind_addr: SocketAddr, + pub bind_addr: SocketAddr, pub require_auth: bool, pub enable_admin: bool, } @@ -40,7 +40,7 @@ impl Config { impl Default for Config { fn default() -> Self { Self::from_env().unwrap_or(Self { - bind_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3000), + bind_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3000), require_auth: true, enable_admin: true, }) diff --git a/test/twin/openai/src/engine/failures.rs b/test/twin/openai/src/engine/failures.rs index 8ad78f60f..045597887 100644 --- a/test/twin/openai/src/engine/failures.rs +++ b/test/twin/openai/src/engine/failures.rs @@ -6,22 +6,22 @@ use crate::openai::models::{ErrorBody, ErrorEnvelope}; #[derive(Clone, Copy, Debug, Default)] pub struct TransportOptions { pub delay_before_headers_ms: u64, - pub inter_event_delay_ms: u64, - pub close_after_chunks: Option, - pub malformed_sse: bool, + pub inter_event_delay_ms: u64, + pub close_after_chunks: Option, + pub malformed_sse: bool, } #[derive(Clone, Debug)] pub struct SuccessOutcome { - pub plan: ResponsePlan, + pub plan: ResponsePlan, pub transport: TransportOptions, } #[derive(Clone, Debug)] pub struct ErrorOutcome { - pub status: StatusCode, - pub body: ErrorEnvelope, - pub retry_after: Option, + pub status: StatusCode, + pub body: ErrorEnvelope, + pub retry_after: Option, pub delay_before_headers_ms: u64, } diff --git a/test/twin/openai/src/engine/mod.rs b/test/twin/openai/src/engine/mod.rs index 56359ecd6..ad011a195 100644 --- a/test/twin/openai/src/engine/mod.rs +++ b/test/twin/openai/src/engine/mod.rs @@ -19,10 +19,10 @@ pub fn execute_responses_request( ) -> Result { request.validate()?; let context = RequestContext { - endpoint: "responses".to_owned(), - model: request.model.clone(), - stream: request.stream, - metadata: request.metadata.clone(), + endpoint: "responses".to_owned(), + model: request.model.clone(), + stream: request.stream, + metadata: request.metadata.clone(), input_text: request.extract_user_text(), }; state.log_request(namespace, context.clone()); @@ -39,7 +39,7 @@ pub fn execute_responses_request( Ok(ExecutionOutcome::Success(enforce_tool_choice( request.tool_choice_mode(), SuccessOutcome { - plan: build_default_response_plan(state.next_response_id(namespace), request), + plan: build_default_response_plan(state.next_response_id(namespace), request), transport: TransportOptions::default(), }, )?)) @@ -52,10 +52,10 @@ pub fn execute_chat_request( ) -> Result { request.validate()?; let context = RequestContext { - endpoint: "chat.completions".to_owned(), - model: request.model.clone(), - stream: request.stream, - metadata: serde_json::Map::new(), + endpoint: "chat.completions".to_owned(), + model: request.model.clone(), + stream: request.stream, + metadata: serde_json::Map::new(), input_text: request.extract_user_text(), }; state.log_request(namespace, context.clone()); @@ -72,7 +72,7 @@ pub fn execute_chat_request( Ok(ExecutionOutcome::Success(enforce_tool_choice( request.tool_choice_mode(), SuccessOutcome { - plan: build_default_chat_plan( + plan: build_default_chat_plan( state.next_response_id(namespace), request.model.clone(), &request.extract_user_text(), diff --git a/test/twin/openai/src/engine/plan.rs b/test/twin/openai/src/engine/plan.rs index 054078ebe..0d0455ce7 100644 --- a/test/twin/openai/src/engine/plan.rs +++ b/test/twin/openai/src/engine/plan.rs @@ -2,21 +2,21 @@ use serde_json::{Value, json}; #[derive(Clone, Debug)] pub struct ResponsePlan { - pub id: String, - pub created: u64, - pub model: String, - pub response_text: String, + pub id: String, + pub created: u64, + pub model: String, + pub response_text: String, pub structured_output: Option, - pub reasoning: Vec, - pub tool_calls: Vec, - pub input_tokens: u64, - pub output_tokens: u64, + pub reasoning: Vec, + pub tool_calls: Vec, + pub input_tokens: u64, + pub output_tokens: u64, } #[derive(Clone, Debug)] pub struct ToolCallPlan { - pub id: String, - pub name: String, + pub id: String, + pub name: String, pub arguments: Value, } diff --git a/test/twin/openai/src/engine/scenario.rs b/test/twin/openai/src/engine/scenario.rs index 59d8fd642..2c0b812d8 100644 --- a/test/twin/openai/src/engine/scenario.rs +++ b/test/twin/openai/src/engine/scenario.rs @@ -14,16 +14,16 @@ pub struct ScenarioEnvelope { #[derive(Clone, Debug, Deserialize)] pub struct Scenario { pub matcher: ScenarioMatcher, - pub script: ScenarioScript, + pub script: ScenarioScript, } #[derive(Clone, Debug, Deserialize)] pub struct ScenarioMatcher { - pub endpoint: String, - pub model: Option, - pub stream: Option, + pub endpoint: String, + pub model: Option, + pub stream: Option, #[serde(default)] - pub metadata: Map, + pub metadata: Map, pub input_contains: Option, } @@ -31,21 +31,21 @@ pub struct ScenarioMatcher { #[serde(tag = "kind", rename_all = "snake_case")] pub enum ScenarioScript { Success { - response_text: Option, - reasoning: Option>, - structured_output: Option, - tool_calls: Option>, + response_text: Option, + reasoning: Option>, + structured_output: Option, + tool_calls: Option>, delay_before_headers_ms: Option, - inter_event_delay_ms: Option, - close_after_chunks: Option, - malformed_sse: Option, + inter_event_delay_ms: Option, + close_after_chunks: Option, + malformed_sse: Option, }, Error { - status: u16, - message: String, - error_type: String, - code: String, - retry_after: Option, + status: u16, + message: String, + error_type: String, + code: String, + retry_after: Option, delay_before_headers_ms: Option, }, Hang { @@ -55,17 +55,17 @@ pub enum ScenarioScript { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ToolCallTemplate { - pub id: Option, - pub name: String, + pub id: Option, + pub name: String, pub arguments: Value, } #[derive(Clone, Debug)] pub struct RequestContext { - pub endpoint: String, - pub model: String, - pub stream: bool, - pub metadata: Map, + pub endpoint: String, + pub model: String, + pub stream: bool, + pub metadata: Map, pub input_text: String, } @@ -127,7 +127,7 @@ impl Scenario { close_after_chunks, malformed_sse, } => ExecutionOutcome::Success(SuccessOutcome { - plan: build_plan_from_script( + plan: build_plan_from_script( response_number, request.model.clone(), &request.extract_user_text(), @@ -138,9 +138,9 @@ impl Scenario { ), transport: TransportOptions { delay_before_headers_ms: delay_before_headers_ms.unwrap_or_default(), - inter_event_delay_ms: inter_event_delay_ms.unwrap_or_default(), - close_after_chunks: *close_after_chunks, - malformed_sse: malformed_sse.unwrap_or(false), + inter_event_delay_ms: inter_event_delay_ms.unwrap_or_default(), + close_after_chunks: *close_after_chunks, + malformed_sse: malformed_sse.unwrap_or(false), }, }), ScenarioScript::Error { @@ -182,7 +182,7 @@ impl Scenario { close_after_chunks, malformed_sse, } => ExecutionOutcome::Success(SuccessOutcome { - plan: build_plan_from_script( + plan: build_plan_from_script( response_number, request.model.clone(), &request.extract_user_text(), @@ -193,9 +193,9 @@ impl Scenario { ), transport: TransportOptions { delay_before_headers_ms: delay_before_headers_ms.unwrap_or_default(), - inter_event_delay_ms: inter_event_delay_ms.unwrap_or_default(), - close_after_chunks: *close_after_chunks, - malformed_sse: malformed_sse.unwrap_or(false), + inter_event_delay_ms: inter_event_delay_ms.unwrap_or_default(), + close_after_chunks: *close_after_chunks, + malformed_sse: malformed_sse.unwrap_or(false), }, }), ScenarioScript::Error { @@ -249,10 +249,10 @@ fn build_plan_from_script( .into_iter() .enumerate() .map(|(index, tool_call)| ToolCallPlan { - id: tool_call + id: tool_call .id .unwrap_or_else(|| format!("call_{response_number}_{index}")), - name: tool_call.name, + name: tool_call.name, arguments: tool_call.arguments, }) .collect(), diff --git a/test/twin/openai/src/logs.rs b/test/twin/openai/src/logs.rs index 1edc6cd8b..01528b0db 100644 --- a/test/twin/openai/src/logs.rs +++ b/test/twin/openai/src/logs.rs @@ -3,9 +3,9 @@ use serde_json::{Map, Value}; #[derive(Clone, Debug, Serialize)] pub struct RequestLog { - pub endpoint: String, - pub model: String, - pub stream: bool, + pub endpoint: String, + pub model: String, + pub stream: bool, pub input_text: String, - pub metadata: Map, + pub metadata: Map, } diff --git a/test/twin/openai/src/openai/models.rs b/test/twin/openai/src/openai/models.rs index 45246ddcc..92eedca74 100644 --- a/test/twin/openai/src/openai/models.rs +++ b/test/twin/openai/src/openai/models.rs @@ -8,23 +8,23 @@ use serde_json::{Map, Value}; /// via `#[serde(flatten)]` so the twin stays compatible as the API evolves. #[derive(Clone, Debug, Deserialize)] pub struct ResponsesRequest { - pub model: String, + pub model: String, #[serde(default)] - pub input: ResponseInput, + pub input: ResponseInput, #[serde(default)] - pub stream: bool, + pub stream: bool, #[serde(default)] - pub metadata: Map, - pub stop: Option, + pub metadata: Map, + pub stop: Option, pub previous_response_id: Option, - pub reasoning: Option, - pub text: Option, - pub tools: Option>, - pub tool_choice: Option, + pub reasoning: Option, + pub text: Option, + pub tools: Option>, + pub tool_choice: Option, /// Catch-all for fields the twin doesn't use (temperature, top_p, etc.) #[allow(dead_code)] #[serde(flatten)] - extra: Map, + extra: Map, } impl ResponsesRequest { @@ -135,16 +135,16 @@ impl ResponseInput { #[derive(Clone, Debug, Deserialize)] pub struct InputItem { #[serde(default)] - pub role: Option, + pub role: Option, #[serde(default)] - pub content: InputContent, + pub content: InputContent, #[serde(default)] #[serde(rename = "type")] pub item_type: Option, #[serde(default)] - pub output: Option, + pub output: Option, #[serde(default)] - pub call_id: Option, + pub call_id: Option, } impl InputItem { @@ -196,9 +196,9 @@ impl InputContent { #[serde(deny_unknown_fields)] pub struct ContentPart { #[serde(rename = "type")] - pub kind: String, + pub kind: String, #[serde(default)] - pub text: Option, + pub text: Option, #[serde(default)] pub image_url: Option, } @@ -329,17 +329,17 @@ pub struct TextOptions { #[serde(deny_unknown_fields)] pub struct TextFormat { #[serde(rename = "type")] - pub kind: String, + pub kind: String, #[serde(default)] pub json_schema: Option, #[serde(default)] - pub name: Option, + pub name: Option, #[serde(default)] - pub schema: Option, + pub schema: Option, #[serde(default)] pub description: Option, #[serde(default)] - pub strict: Option, + pub strict: Option, } impl TextFormat { @@ -362,29 +362,29 @@ pub struct ErrorEnvelope { #[derive(Clone, Debug, Serialize)] pub struct ErrorBody { - pub message: String, + pub message: String, #[serde(rename = "type")] pub error_type: String, - pub param: Value, - pub code: String, + pub param: Value, + pub code: String, } #[derive(Clone, Debug)] pub struct OpenAiError { pub status: StatusCode, - pub body: ErrorEnvelope, + pub body: ErrorEnvelope, } impl OpenAiError { pub fn invalid_request(param: &str, message: &str) -> Self { Self { status: StatusCode::BAD_REQUEST, - body: ErrorEnvelope { + body: ErrorEnvelope { error: ErrorBody { - message: message.to_owned(), + message: message.to_owned(), error_type: "invalid_request_error".to_owned(), - param: Value::String(param.to_owned()), - code: "invalid_request".to_owned(), + param: Value::String(param.to_owned()), + code: "invalid_request".to_owned(), }, }, } @@ -447,14 +447,14 @@ pub fn normalize_whitespace(input: &str) -> String { #[derive(Clone, Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct ChatCompletionsRequest { - pub model: String, - pub messages: Vec, + pub model: String, + pub messages: Vec, #[serde(default)] - pub stream: bool, - pub tools: Option>, - pub tool_choice: Option, + pub stream: bool, + pub tools: Option>, + pub tool_choice: Option, pub response_format: Option, - pub stop: Option, + pub stop: Option, } impl ChatCompletionsRequest { @@ -532,7 +532,7 @@ impl ChatCompletionsRequest { #[derive(Clone, Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct ChatMessage { - pub role: String, + pub role: String, pub content: Value, } @@ -665,9 +665,9 @@ fn validate_chat_message_part(part: &Value, role: &str) -> Result<(), OpenAiErro #[serde(deny_unknown_fields)] pub struct ChatResponseFormat { #[serde(rename = "type")] - pub kind: String, + pub kind: String, #[serde(default)] - pub schema: Option, + pub schema: Option, #[serde(default)] pub json_schema: Option, } diff --git a/test/twin/openai/src/state.rs b/test/twin/openai/src/state.rs index d456ab6c5..b8ca1fed2 100644 --- a/test/twin/openai/src/state.rs +++ b/test/twin/openai/src/state.rs @@ -31,25 +31,25 @@ pub struct DebugSnapshot { #[derive(Clone, Debug, Serialize)] pub struct NamespaceSnapshot { - pub key: String, - pub scenarios: Vec, + pub key: String, + pub scenarios: Vec, pub request_logs: Vec, } #[derive(Clone, Debug, Serialize)] pub struct ScenarioSnapshot { - pub endpoint: String, - pub model: Option, - pub stream: Option, + pub endpoint: String, + pub model: Option, + pub stream: Option, pub input_contains: Option, - pub metadata: serde_json::Map, - pub script_kind: String, + pub metadata: serde_json::Map, + pub script_kind: String, } #[derive(Clone, Debug)] pub struct AppState { pub config: Config, - inner: Arc, + inner: Arc, } #[derive(Debug)] @@ -60,16 +60,16 @@ struct AppStateInner { #[derive(Debug)] struct NamespaceState { next_response_number: u64, - scenarios: Vec, - request_logs: Vec, + scenarios: Vec, + request_logs: Vec, } impl Default for NamespaceState { fn default() -> Self { Self { next_response_number: 1, - scenarios: Vec::new(), - request_logs: Vec::new(), + scenarios: Vec::new(), + request_logs: Vec::new(), } } } @@ -125,11 +125,11 @@ impl AppState { .or_default() .request_logs .push(RequestLog { - endpoint: request.endpoint, - model: request.model, - stream: request.stream, + endpoint: request.endpoint, + model: request.model, + stream: request.stream, input_text: request.input_text, - metadata: request.metadata, + metadata: request.metadata, }); } @@ -156,17 +156,17 @@ impl AppState { let mut result = Vec::new(); for (key, ns) in namespaces.iter() { result.push(NamespaceSnapshot { - key: key.to_string(), - scenarios: ns + key: key.to_string(), + scenarios: ns .scenarios .iter() .map(|s| ScenarioSnapshot { - endpoint: s.matcher.endpoint.clone(), - model: s.matcher.model.clone(), - stream: s.matcher.stream, + endpoint: s.matcher.endpoint.clone(), + model: s.matcher.model.clone(), + stream: s.matcher.stream, input_contains: s.matcher.input_contains.clone(), - metadata: s.matcher.metadata.clone(), - script_kind: s.script.script_kind().to_owned(), + metadata: s.matcher.metadata.clone(), + script_kind: s.script.script_kind().to_owned(), }) .collect(), request_logs: ns.request_logs.clone(), diff --git a/test/twin/openai/tests/common/mod.rs b/test/twin/openai/tests/common/mod.rs index f9dff9743..ead0d99fe 100644 --- a/test/twin/openai/tests/common/mod.rs +++ b/test/twin/openai/tests/common/mod.rs @@ -15,50 +15,50 @@ use tokio::net::{TcpListener, TcpStream}; use twin_openai::config::Config; pub struct TestServer { - pub base_url: String, - pub client: Client, - pub auth_client: Client, + pub base_url: String, + pub client: Client, + pub auth_client: Client, pub bearer_token: String, } #[derive(Clone)] pub struct ApiClient { pub base_url: String, - client: Client, + client: Client, bearer_token: Option, organization: Option, - project: Option, + project: Option, } pub struct RecordedResponse { - pub status: reqwest::StatusCode, + pub status: reqwest::StatusCode, pub headers: HashMap, - pub body: Vec, + pub body: Vec, } pub struct RawStreamResponse { - pub status: u16, + pub status: u16, pub headers: HashMap, - pub body: Vec, + pub body: Vec, } pub struct TimedStreamResponse { - pub status: reqwest::StatusCode, + pub status: reqwest::StatusCode, pub first_event_elapsed: Duration, - pub chunks: Vec, + pub chunks: Vec, } #[derive(Debug)] pub struct ParsedSseTranscript { pub blocks: Vec, pub events: Vec, - pub done: bool, + pub done: bool, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct ParsedSseEvent { pub event: Option, - pub data: String, + pub data: String, } static NEXT_BEARER_TOKEN: AtomicU64 = AtomicU64::new(1); @@ -71,7 +71,7 @@ pub async fn spawn_server() -> Result { let listener = TcpListener::bind("127.0.0.1:0").await?; let addr: SocketAddr = listener.local_addr()?; let app = twin_openai::build_app_with_config(Config { - bind_addr: "127.0.0.1:0".parse().expect("valid addr"), + bind_addr: "127.0.0.1:0".parse().expect("valid addr"), require_auth: true, enable_admin: true, }); diff --git a/test/twin/openai/tests/debug_ui.rs b/test/twin/openai/tests/debug_ui.rs index d5f0741c2..05596c24a 100644 --- a/test/twin/openai/tests/debug_ui.rs +++ b/test/twin/openai/tests/debug_ui.rs @@ -216,7 +216,7 @@ async fn debug_routes_not_accessible_when_admin_disabled() { .expect("bind should succeed"); let addr = listener.local_addr().expect("should have addr"); let app = twin_openai::build_app_with_config(Config { - bind_addr: "127.0.0.1:0".parse().expect("valid addr"), + bind_addr: "127.0.0.1:0".parse().expect("valid addr"), require_auth: false, enable_admin: false, }); diff --git a/test/twin/openai/tests/live_openai_contract.rs b/test/twin/openai/tests/live_openai_contract.rs index 9de99d204..13f5471f3 100644 --- a/test/twin/openai/tests/live_openai_contract.rs +++ b/test/twin/openai/tests/live_openai_contract.rs @@ -24,7 +24,7 @@ const RESPONSES_STREAM_MILESTONES: &[&str] = &[ #[derive(Clone)] struct LiveOptions { - api: common::ApiClient, + api: common::ApiClient, model: String, } @@ -46,7 +46,7 @@ enum ChatStreamMilestone { #[derive(Clone, Debug, PartialEq, Eq)] struct ToolCallObservation { - name: String, + name: String, arguments: Value, } @@ -1526,7 +1526,7 @@ fn extract_response_tool_call(body: &Value) -> Option { .find(|item| item.get("type").and_then(Value::as_str) == Some("function_call")) .and_then(|tool_call| { Some(ToolCallObservation { - name: tool_call.get("name")?.as_str()?.to_owned(), + name: tool_call.get("name")?.as_str()?.to_owned(), arguments: parse_json_object_string(tool_call.get("arguments")?.as_str()?)?, }) }) @@ -1628,7 +1628,7 @@ fn extract_chat_tool_call(body: &Value) -> Option { .iter() .find_map(|tool_call| { Some(ToolCallObservation { - name: tool_call.get("function")?.get("name")?.as_str()?.to_owned(), + name: tool_call.get("function")?.get("name")?.as_str()?.to_owned(), arguments: parse_json_object_string( tool_call.get("function")?.get("arguments")?.as_str()?, )?, diff --git a/test/twin/openai/tests/responses_contract.rs b/test/twin/openai/tests/responses_contract.rs index 9c2546ebb..d399332b1 100644 --- a/test/twin/openai/tests/responses_contract.rs +++ b/test/twin/openai/tests/responses_contract.rs @@ -255,19 +255,22 @@ async fn responses_stream_emits_expected_sse_sequence() { .collect::>(); assert_eq!(status, 200); - assert_eq!(events, vec![ - "response.created", - "response.in_progress", - "response.output_item.added", - "response.output_item.done", - "response.output_item.added", - "response.content_part.added", - "response.output_text.delta", - "response.output_text.done", - "response.content_part.done", - "response.output_item.done", - "response.completed", - ]); + assert_eq!( + events, + vec![ + "response.created", + "response.in_progress", + "response.output_item.added", + "response.output_item.done", + "response.output_item.added", + "response.content_part.added", + "response.output_text.delta", + "response.output_text.done", + "response.content_part.done", + "response.output_item.done", + "response.completed", + ] + ); assert!(!transcript.done); assert!(joined.contains("deterministic: stream this request")); assert!( @@ -352,19 +355,22 @@ async fn responses_stream_emits_structured_output_events() { .and_then(|text| serde_json::from_str::(&text).ok()) .expect("structured stream output text"); assert_eq!(status, 200); - assert_eq!(events, vec![ - "response.created", - "response.in_progress", - "response.output_item.added", - "response.output_item.done", - "response.output_item.added", - "response.content_part.added", - "response.output_text.delta", - "response.output_text.done", - "response.content_part.done", - "response.output_item.done", - "response.completed", - ]); + assert_eq!( + events, + vec![ + "response.created", + "response.in_progress", + "response.output_item.added", + "response.output_item.done", + "response.output_item.added", + "response.content_part.added", + "response.output_text.delta", + "response.output_text.done", + "response.content_part.done", + "response.output_item.done", + "response.completed", + ] + ); assert!(!transcript.done); assert_eq!( streamed_json["message"], diff --git a/test/twin/openai/tests/tool_and_schema_contract.rs b/test/twin/openai/tests/tool_and_schema_contract.rs index 81d15ff90..6792572e0 100644 --- a/test/twin/openai/tests/tool_and_schema_contract.rs +++ b/test/twin/openai/tests/tool_and_schema_contract.rs @@ -378,17 +378,20 @@ async fn responses_stream_supports_tool_only_turn_without_fabricated_text() { .collect::>(); assert_eq!(status, 200); - assert_eq!(events, vec![ - "response.created", - "response.in_progress", - "response.output_item.added", - "response.output_item.done", - "response.output_item.added", - "response.function_call_arguments.delta", - "response.function_call_arguments.done", - "response.output_item.done", - "response.completed", - ]); + assert_eq!( + events, + vec![ + "response.created", + "response.in_progress", + "response.output_item.added", + "response.output_item.done", + "response.output_item.added", + "response.function_call_arguments.delta", + "response.function_call_arguments.done", + "response.output_item.done", + "response.completed", + ] + ); assert!(joined.contains("\"id\":\"fc_call_weather\"")); assert!(joined.contains("\"call_id\":\"call_weather\"")); assert!(joined.contains("\"arguments\":\"{\\\"city\\\":\\\"Boston\\\"}\""));