diff --git a/apps/fabro-web/app/data/runs.test.ts b/apps/fabro-web/app/data/runs.test.ts index e614114d3..3fe9a67db 100644 --- a/apps/fabro-web/app/data/runs.test.ts +++ b/apps/fabro-web/app/data/runs.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { mapRunSummaryToRunItem } from "./runs"; +import { mapRunSummaryToRunItem, isRunStatus } from "./runs"; describe("mapRunSummaryToRunItem", () => { test("maps store run summary to RunItem", () => { @@ -15,6 +15,7 @@ describe("mapRunSummaryToRunItem", () => { labels: {}, start_time: "2026-04-08T12:00:00Z", status_reason: null, + blocked_reason: null, pending_control: null, }; const item = mapRunSummaryToRunItem(summary); @@ -38,6 +39,7 @@ describe("mapRunSummaryToRunItem", () => { labels: {}, start_time: null, status_reason: null, + blocked_reason: null, pending_control: null, }; const item = mapRunSummaryToRunItem(summary); @@ -46,4 +48,18 @@ describe("mapRunSummaryToRunItem", () => { expect(item.workflow).toBe("unknown"); expect(item.repo).toBe("unknown"); }); -}); + + test("summary mapping accepts blocked, paused, completed, and cancelled statuses", () => { + for (const status of ["blocked", "paused", "completed", "cancelled"]) { + expect(isRunStatus(status)).toBe(true); + } + }); + + test("no UI code depends on waiting status", () => { + expect(isRunStatus("waiting")).toBe(false); + }); + + test("no UI code depends on dead status", () => { + expect(isRunStatus("dead")).toBe(false); + }); +}); \ No newline at end of file diff --git a/apps/fabro-web/app/data/runs.ts b/apps/fabro-web/app/data/runs.ts index eb9c30745..d1c4b5184 100644 --- a/apps/fabro-web/app/data/runs.ts +++ b/apps/fabro-web/app/data/runs.ts @@ -29,17 +29,13 @@ export interface RunItem { sandboxId?: string; } -export type ColumnStatus = "working" | "initializing" | "review" | "merge" | "running" | "waiting" | "succeeded" | "failed"; +export type ColumnStatus = "working" | "blocked" | "review" | "merge"; export const columnNames: Record = { working: "Working", - initializing: "Initializing", + blocked: "Blocked", review: "Verify", merge: "Merge", - running: "Running", - waiting: "Waiting", - succeeded: "Succeeded", - failed: "Failed", }; export interface RunWithStatus extends RunItem { @@ -83,6 +79,7 @@ export interface RunSummaryResponse { host_repo_path: string | null; status: string | null; status_reason: string | null; + blocked_reason: string | null; pending_control: string | null; duration_ms: number | null; total_usd_micros: number | null; @@ -113,34 +110,34 @@ export function deriveCiStatus(checks: CheckRun[]): CiStatus { export const statusColors: Record = { working: { dot: "bg-teal-500", text: "text-teal-500" }, - initializing: { dot: "bg-amber", text: "text-amber" }, + blocked: { dot: "bg-amber", text: "text-amber" }, review: { dot: "bg-mint", text: "text-mint" }, merge: { dot: "bg-teal-300", text: "text-teal-300" }, - running: { dot: "bg-teal-500", text: "text-teal-500" }, - waiting: { dot: "bg-amber", text: "text-amber" }, - succeeded: { dot: "bg-teal-300", text: "text-teal-300" }, - failed: { dot: "bg-coral", text: "text-coral" }, }; export type RunStatus = | "submitted" + | "queued" | "starting" | "running" + | "blocked" | "paused" | "removing" - | "succeeded" + | "completed" | "failed" - | "dead"; + | "cancelled"; export const runStatusDisplay: Record = { submitted: { label: "Submitted", dot: "bg-fg-muted", text: "text-fg-muted" }, + queued: { label: "Queued", dot: "bg-fg-muted", text: "text-fg-muted" }, starting: { label: "Starting", dot: "bg-amber", text: "text-amber" }, running: { label: "Running", dot: "bg-teal-500", text: "text-teal-500" }, + blocked: { label: "Blocked", dot: "bg-amber", text: "text-amber" }, paused: { label: "Paused", dot: "bg-amber", text: "text-amber" }, removing: { label: "Removing", dot: "bg-fg-muted", text: "text-fg-muted" }, - succeeded: { label: "Succeeded", dot: "bg-mint", text: "text-mint" }, + completed: { label: "Completed", dot: "bg-mint", text: "text-mint" }, failed: { label: "Failed", dot: "bg-coral", text: "text-coral" }, - dead: { label: "Dead", dot: "bg-coral", text: "text-coral" }, + cancelled: { label: "Cancelled", dot: "bg-coral", text: "text-coral" }, }; const knownRunStatuses = new Set(Object.keys(runStatusDisplay)); @@ -160,4 +157,4 @@ export const ciConfig: Record = { working: { accent: "bg-teal-500", iconColor: "text-teal-500", iconType: "branch", actions: ["Watch", "Steer"] }, - initializing: { accent: "bg-amber", iconColor: "text-amber", iconType: "branch", actions: [] }, + blocked: { accent: "bg-amber", iconColor: "text-amber", iconType: "branch", actions: ["Answer Question"] }, review: { accent: "bg-mint", iconColor: "text-mint", iconType: "pr", actions: [] }, merge: { accent: "bg-teal-300", iconColor: "text-teal-300", iconType: "pr", actions: ["Merge"] }, - running: { accent: "bg-teal-500", iconColor: "text-teal-500", iconType: "branch", actions: ["Watch", "Steer"] }, - waiting: { accent: "bg-amber", iconColor: "text-amber", iconType: "branch", actions: ["Answer Question"] }, - succeeded: { accent: "bg-teal-300", iconColor: "text-teal-300", iconType: "pr", actions: [] }, - failed: { accent: "bg-coral", iconColor: "text-coral", iconType: "branch", actions: [] }, }; const defaultColumnStyle: ColumnStyle = { accent: "bg-fg-muted", iconColor: "text-fg-muted", iconType: "branch", actions: [] }; @@ -528,6 +524,8 @@ export default function Runs({ loaderData }: any) { const STATUS_EVENTS = new Set([ "run.submitted", "run.starting", "run.running", "run.paused", "run.completed", "run.failed", + "interview.started", "interview.completed", + "interview.timeout", "interview.interrupted", ]); useEffect(() => { @@ -690,4 +688,4 @@ export default function Runs({ loaderData }: any) { ); -} +} \ No newline at end of file diff --git a/docs/api-reference/fabro-api.yaml b/docs/api-reference/fabro-api.yaml index 3228cbefd..6f9ea824a 100644 --- a/docs/api-reference/fabro-api.yaml +++ b/docs/api-reference/fabro-api.yaml @@ -2089,10 +2089,11 @@ components: - queued - starting - running + - blocked + - paused - completed - failed - cancelled - - paused RunManifest: description: Self-contained workflow run manifest. @@ -2481,6 +2482,10 @@ components: allOf: - $ref: "#/components/schemas/StatusReason" nullable: true + blocked_reason: + allOf: + - $ref: "#/components/schemas/BlockedReason" + nullable: true pending_control: allOf: - $ref: "#/components/schemas/RunControlAction" @@ -2876,13 +2881,21 @@ components: type: string enum: - submitted + - queued - starting - running + - blocked - paused - removing - - succeeded + - completed - failed - - dead + - cancelled + + BlockedReason: + description: Reason why a run is blocked. + type: string + enum: + - human_input_required StatusReason: description: Optional reason attached to a run status transition. @@ -2921,6 +2934,10 @@ components: oneOf: - $ref: "#/components/schemas/StatusReason" - type: "null" + blocked_reason: + oneOf: + - $ref: "#/components/schemas/BlockedReason" + - type: "null" updated_at: type: string format: date-time @@ -3088,6 +3105,9 @@ components: status_reason: type: string nullable: true + blocked_reason: + type: string + nullable: true pending_control: allOf: - $ref: "#/components/schemas/RunControlAction" @@ -3109,7 +3129,7 @@ components: type: string enum: - working - - initializing + - blocked - review - merge @@ -4525,4 +4545,4 @@ components: login: type: string description: User's login identifier (e.g. GitHub username). - example: octocat + example: octocat \ No newline at end of file diff --git a/lib/crates/fabro-agent/src/compaction.rs b/lib/crates/fabro-agent/src/compaction.rs index 30f1a383a..3f245266a 100644 --- a/lib/crates/fabro-agent/src/compaction.rs +++ b/lib/crates/fabro-agent/src/compaction.rs @@ -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 @@ -63,10 +66,13 @@ pub async fn compact_context( 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,24 +108,24 @@ 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, }; @@ -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 be5bbf05e..13ce6aec9 100644 --- a/lib/crates/fabro-agent/src/error.rs +++ b/lib/crates/fabro-agent/src/error.rs @@ -48,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(_))); @@ -91,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(); @@ -101,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(); @@ -155,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()), @@ -173,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 c2495f25a..a7a0f14c6 100644 --- a/lib/crates/fabro-agent/src/event.rs +++ b/lib/crates/fabro-agent/src/event.rs @@ -54,16 +54,22 @@ mod tests { 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: Error::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: Error::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/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 f50eec2ba..4995ebe04 100644 --- a/lib/crates/fabro-agent/src/session.rs +++ b/lib/crates/fabro-agent/src/session.rs @@ -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, + }, + ); } } } @@ -415,9 +423,12 @@ impl Session { } fn emit_llm_error(&mut self, err: LlmError) -> Error { - self.event_emitter.emit(self.id.clone(), AgentEvent::Error { - error: Error::Llm(err.clone()), - }); + self.event_emitter.emit( + self.id.clone(), + AgentEvent::Error { + error: Error::Llm(err.clone()), + }, + ); if is_auth_error(&err) { self.transition(SessionState::Closed); } @@ -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(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, }, ); @@ -796,7 +818,7 @@ impl Session { let Some(response) = response else { 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: Error::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 @@ -1013,7 +1040,7 @@ mod tests { } struct ScriptedStreamProvider { - calls: Vec, + calls: Vec, call_index: AtomicUsize, } @@ -1062,7 +1089,7 @@ mod tests { async fn complete(&self, _request: &Request) -> Result { Err(LlmError::Configuration { message: "ScriptedStreamProvider does not implement complete()".into(), - source: None, + source: None, }) } @@ -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(); @@ -1497,7 +1524,7 @@ mod tests { async fn auth_error_closes_session() { let error_provider = Arc::new(MockErrorProvider { error: LlmError::Provider { - kind: ProviderErrorKind::Authentication, + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail::new("invalid api key", "mock")), }, }); @@ -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: LlmError::Stream { + error: LlmError::Stream { message: "connection reset".into(), - source: None, + source: None, }, }); let client = make_client(provider as Arc).await; @@ -2146,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 = LlmError::Provider { - kind: ProviderErrorKind::Authentication, + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail { status_code: Some(401), ..ProviderErrorDetail::new("bad key", "mock") @@ -2207,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"); } @@ -2305,7 +2338,7 @@ mod tests { // provider that errors on complete() but succeeds on stream(). struct StreamOnlyProvider { - responses: Vec, + responses: Vec, call_index: AtomicUsize, } @@ -2318,7 +2351,7 @@ mod tests { async fn complete(&self, _request: &Request) -> Result { Err(LlmError::Stream { message: "summarization failed".into(), - source: None, + source: None, }) } @@ -2399,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>, } @@ -2429,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()) }) }), }; @@ -2542,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() @@ -2654,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()) 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 a27cde76b..0031422cf 100644 --- a/lib/crates/fabro-agent/src/subagent.rs +++ b/lib/crates/fabro-agent/src/subagent.rs @@ -24,8 +24,8 @@ pub type SubAgentEventCallback = Arc>>, + 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, } @@ -119,24 +119,27 @@ 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) @@ -303,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": { @@ -328,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 { @@ -356,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": { @@ -373,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")?; @@ -391,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": { @@ -404,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")?; @@ -423,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": { @@ -436,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")?; @@ -715,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()), })); @@ -747,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 d7284bc11..13f663f10 100644 --- a/lib/crates/fabro-agent/src/test_support.rs +++ b/lib/crates/fabro-agent/src/test_support.rs @@ -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, } @@ -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()) }) }), } @@ -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: LlmError, + pub error: LlmError, } #[async_trait] @@ -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 c3b1a8bc5..e6fa82bed 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(|| { @@ -642,11 +642,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"); } @@ -684,8 +687,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, }, ) @@ -714,8 +717,8 @@ mod tests { "new_string": "goodbye" }), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), + env: env_clone, + cancel: CancellationToken::new(), tool_env: None, }, ) @@ -796,8 +799,8 @@ mod tests { "replace_all": true }), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), + env: env_clone, + cancel: CancellationToken::new(), tool_env: None, }, ) @@ -813,19 +816,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")); @@ -840,8 +846,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, }, ) @@ -854,19 +860,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")); @@ -878,19 +887,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")); @@ -906,8 +918,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()), }, ) @@ -921,11 +933,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); @@ -936,10 +951,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() @@ -950,8 +965,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()), }, ) @@ -970,11 +985,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()")); @@ -988,11 +1006,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")); @@ -1003,11 +1024,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!( @@ -1020,11 +1044,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!( @@ -1061,10 +1088,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() @@ -1073,8 +1100,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, }, ) @@ -1131,8 +1158,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, }, ) @@ -1153,8 +1180,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, }, ) @@ -1173,10 +1200,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() @@ -1200,10 +1227,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() @@ -1240,18 +1267,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() @@ -1277,12 +1303,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() @@ -1317,7 +1342,7 @@ mod tests { // "other_provider" is the default — it rejects all requests. let default_provider: Arc = Arc::new(MockErrorProvider { error: LlmError::Provider { - kind: ProviderErrorKind::NotFound, + kind: ProviderErrorKind::NotFound, detail: Box::new(ProviderErrorDetail::new( "model not found", "other_provider", @@ -1341,17 +1366,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() @@ -1434,11 +1459,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(); @@ -1463,11 +1491,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 965c98ee6..6d4532db1 100644 --- a/lib/crates/fabro-agent/src/types.rs +++ b/lib/crates/fabro-agent/src/types.rs @@ -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: 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: LlmError, + 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: Error, + 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: Error::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: Error::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(); @@ -704,7 +719,7 @@ mod tests { let event = AgentEvent::Error { 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: LlmError::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: Error::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(); @@ -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-auth/src/context.rs b/lib/crates/fabro-auth/src/context.rs index 93bf14c40..2bc56b46e 100644 --- a/lib/crates/fabro-auth/src/context.rs +++ b/lib/crates/fabro-auth/src/context.rs @@ -3,13 +3,13 @@ use fabro_model::Provider; #[derive(Debug, Clone, PartialEq, Eq)] pub enum AuthContextRequest { ApiKey { - provider: Provider, + provider: Provider, env_var_names: Vec, }, DeviceCode { - user_code: String, + user_code: String, verification_uri: String, - expires_in: u64, + expires_in: u64, }, } diff --git a/lib/crates/fabro-auth/src/credential.rs b/lib/crates/fabro-auth/src/credential.rs index ac5479a77..2b244a667 100644 --- a/lib/crates/fabro-auth/src/credential.rs +++ b/lib/crates/fabro-auth/src/credential.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; pub struct AuthCredential { pub provider: Provider, #[serde(flatten)] - pub details: AuthDetails, + pub details: AuthDetails, } impl AuthCredential { @@ -28,8 +28,8 @@ pub enum AuthDetails { key: String, }, CodexOAuth { - tokens: OAuthTokens, - config: OAuthConfig, + tokens: OAuthTokens, + config: OAuthConfig, #[serde(default, skip_serializing_if = "Option::is_none")] account_id: Option, }, @@ -37,9 +37,9 @@ pub enum AuthDetails { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct OAuthTokens { - pub access_token: String, + pub access_token: String, pub refresh_token: Option, - pub expires_at: DateTime, + pub expires_at: DateTime, } pub(crate) fn expires_at_from_now(expires_in: Option) -> DateTime { @@ -49,12 +49,12 @@ pub(crate) fn expires_at_from_now(expires_in: Option) -> DateTime { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct OAuthConfig { - pub auth_url: String, - pub token_url: String, - pub client_id: String, - pub scopes: Vec, + pub auth_url: String, + pub token_url: String, + pub client_id: String, + pub scopes: Vec, pub redirect_uri: Option, - pub use_pkce: bool, + pub use_pkce: bool, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -94,19 +94,19 @@ mod tests { fn oauth_credential(expires_at: DateTime) -> AuthCredential { AuthCredential { provider: Provider::OpenAi, - details: AuthDetails::CodexOAuth { - tokens: OAuthTokens { + details: AuthDetails::CodexOAuth { + tokens: OAuthTokens { access_token: "access".to_string(), refresh_token: Some("refresh".to_string()), expires_at, }, - config: OAuthConfig { - auth_url: "https://auth.openai.com".to_string(), - token_url: "https://auth.openai.com/oauth/token".to_string(), - client_id: "client".to_string(), - scopes: vec!["openid".to_string()], + config: OAuthConfig { + auth_url: "https://auth.openai.com".to_string(), + token_url: "https://auth.openai.com/oauth/token".to_string(), + client_id: "client".to_string(), + scopes: vec!["openid".to_string()], redirect_uri: Some("https://auth.openai.com/deviceauth/callback".to_string()), - use_pkce: true, + use_pkce: true, }, account_id: Some("acct_123".to_string()), }, @@ -137,7 +137,7 @@ mod tests { fn credential_id_for_openai_api_key() { let credential = AuthCredential { provider: Provider::OpenAi, - details: AuthDetails::ApiKey { + details: AuthDetails::ApiKey { key: "sk-test".to_string(), }, }; diff --git a/lib/crates/fabro-auth/src/refresh.rs b/lib/crates/fabro-auth/src/refresh.rs index c1eb04be4..5ef7f4e1f 100644 --- a/lib/crates/fabro-auth/src/refresh.rs +++ b/lib/crates/fabro-auth/src/refresh.rs @@ -25,15 +25,15 @@ pub async fn refresh_oauth_credential( .map_err(anyhow::Error::msg)?; Ok(AuthCredential { provider: credential.provider, - details: AuthDetails::CodexOAuth { - tokens: OAuthTokens { - access_token: response.access_token, + details: AuthDetails::CodexOAuth { + tokens: OAuthTokens { + access_token: response.access_token, refresh_token: response .refresh_token .or_else(|| tokens.refresh_token.clone()), - expires_at: expires_at_from_now(response.expires_in), + expires_at: expires_at_from_now(response.expires_in), }, - config: config.clone(), + config: config.clone(), account_id: account_id.clone(), }, }) diff --git a/lib/crates/fabro-auth/src/resolve.rs b/lib/crates/fabro-auth/src/resolve.rs index b4136ba3b..4ca2e447d 100644 --- a/lib/crates/fabro-auth/src/resolve.rs +++ b/lib/crates/fabro-auth/src/resolve.rs @@ -28,18 +28,18 @@ pub enum CredentialUsage { #[derive(Debug, Clone, PartialEq, Eq)] pub struct ApiCredential { - pub provider: Provider, - pub auth_header: ApiKeyHeader, + pub provider: Provider, + pub auth_header: ApiKeyHeader, pub extra_headers: HashMap, - pub base_url: Option, - pub codex_mode: bool, - pub org_id: Option, - pub project_id: Option, + pub base_url: Option, + pub codex_mode: bool, + pub org_id: Option, + pub project_id: Option, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CliCredential { - pub env_vars: HashMap, + pub env_vars: HashMap, pub login_command: Option, } @@ -57,7 +57,7 @@ pub enum ResolveError { RefreshFailed { provider: Provider, #[source] - source: anyhow::Error, + source: anyhow::Error, }, #[error("{0} requires re-authentication: missing refresh token")] RefreshTokenMissing(Provider), @@ -65,7 +65,7 @@ pub enum ResolveError { #[derive(Clone)] pub struct CredentialResolver { - vault: Arc>, + vault: Arc>, env_lookup: EnvLookup, } @@ -200,7 +200,7 @@ impl CredentialResolver { provider: credential.provider, auth_header: match credential.provider { Provider::Anthropic => ApiKeyHeader::Custom { - name: "x-api-key".to_string(), + name: "x-api-key".to_string(), value: key.clone(), }, _ => ApiKeyHeader::Bearer(key.clone()), @@ -349,13 +349,13 @@ mod tests { fn oauth_credential(token_url: String, expires_at: chrono::DateTime) -> AuthCredential { AuthCredential { provider: Provider::OpenAi, - details: AuthDetails::CodexOAuth { - tokens: OAuthTokens { + details: AuthDetails::CodexOAuth { + tokens: OAuthTokens { access_token: "expired-access".to_string(), refresh_token: Some("refresh-token".to_string()), expires_at, }, - config: OAuthConfig { + config: OAuthConfig { auth_url: "https://auth.openai.com".to_string(), token_url, client_id: "test-client".to_string(), @@ -469,10 +469,13 @@ mod tests { panic!("expected api credential"); }; - assert_eq!(api.auth_header, ApiKeyHeader::Custom { - name: "x-api-key".to_string(), - value: "anthropic-key".to_string(), - }); + assert_eq!( + api.auth_header, + ApiKeyHeader::Custom { + name: "x-api-key".to_string(), + value: "anthropic-key".to_string(), + } + ); } #[tokio::test] @@ -653,9 +656,10 @@ mod tests { let resolver = test_resolver(vault, Arc::new(|_| None)); let vault = resolver.vault.read().await; - assert_eq!(resolver.configured_providers(&vault), vec![ - Provider::OpenAi - ]); + assert_eq!( + resolver.configured_providers(&vault), + vec![Provider::OpenAi] + ); } #[tokio::test] @@ -668,9 +672,10 @@ mod tests { ); let vault = resolver.vault.read().await; - assert_eq!(resolver.configured_providers(&vault), vec![ - Provider::OpenAi - ]); + assert_eq!( + resolver.configured_providers(&vault), + vec![Provider::OpenAi] + ); } #[tokio::test] diff --git a/lib/crates/fabro-auth/src/strategies/api_key.rs b/lib/crates/fabro-auth/src/strategies/api_key.rs index 5de78ea6b..c5f062907 100644 --- a/lib/crates/fabro-auth/src/strategies/api_key.rs +++ b/lib/crates/fabro-auth/src/strategies/api_key.rs @@ -20,7 +20,7 @@ impl ApiKeyStrategy { impl AuthStrategy for ApiKeyStrategy { async fn init(&mut self) -> anyhow::Result { Ok(AuthContextRequest::ApiKey { - provider: self.provider, + provider: self.provider, env_var_names: self .provider .api_key_env_vars() @@ -34,7 +34,7 @@ impl AuthStrategy for ApiKeyStrategy { match response { AuthContextResponse::ApiKey { key } => Ok(AuthCredential { provider: self.provider, - details: AuthDetails::ApiKey { key }, + details: AuthDetails::ApiKey { key }, }), AuthContextResponse::DeviceCodeConfirmed => { Err(anyhow::anyhow!("expected API key response")) diff --git a/lib/crates/fabro-auth/src/strategies/codex_device.rs b/lib/crates/fabro-auth/src/strategies/codex_device.rs index 3a7197a90..f6d7315fa 100644 --- a/lib/crates/fabro-auth/src/strategies/codex_device.rs +++ b/lib/crates/fabro-auth/src/strategies/codex_device.rs @@ -38,9 +38,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(Debug, Deserialize)] @@ -100,48 +100,48 @@ impl U64OrString { #[derive(Debug, Deserialize)] struct DeviceCodeInitResponse { device_auth_id: String, - user_code: String, + user_code: String, #[serde(default)] - interval: Option, + interval: Option, #[serde(default)] - expires_in: Option, + expires_in: Option, #[serde(default)] - expires_at: Option>, + expires_at: Option>, } #[derive(Debug, Deserialize)] struct DeviceCodePollResponse { #[serde(default)] - status: Option, + status: Option, #[serde(default)] authorization_code: Option, #[serde(default)] - code_verifier: Option, + code_verifier: Option, } #[derive(Debug, Serialize)] struct DeviceCodeInitRequest<'a> { client_id: &'a str, #[serde(skip_serializing_if = "Option::is_none")] - scope: Option, + scope: Option, } #[derive(Debug)] struct PendingDeviceAuth { device_auth_id: String, - user_code: String, - poll_interval: Duration, - deadline: Instant, + user_code: String, + poll_interval: Duration, + deadline: Instant, } #[derive(Debug)] struct DeviceAuthorization { authorization_code: String, - code_verifier: String, + code_verifier: String, } pub struct CodexDeviceStrategy { - config: OAuthConfig, + config: OAuthConfig, pending: Option, } @@ -247,7 +247,7 @@ impl AuthStrategy for CodexDeviceStrategy { .header("originator", "fabro") .json(&DeviceCodeInitRequest { client_id: &self.config.client_id, - scope: (!self.config.scopes.is_empty()).then(|| self.config.scopes.join(" ")), + scope: (!self.config.scopes.is_empty()).then(|| self.config.scopes.join(" ")), }) .send() .await?; @@ -301,13 +301,13 @@ impl AuthStrategy for CodexDeviceStrategy { Ok(AuthCredential { provider: fabro_model::Provider::OpenAi, - details: AuthDetails::CodexOAuth { - tokens: OAuthTokens { - access_token: token_response.access_token, + details: AuthDetails::CodexOAuth { + tokens: OAuthTokens { + access_token: token_response.access_token, refresh_token: token_response.refresh_token, - expires_at: expires_at_from_now(token_response.expires_in), + expires_at: expires_at_from_now(token_response.expires_in), }, - config: self.config.clone(), + config: self.config.clone(), account_id: token_response .id_token .as_deref() @@ -329,17 +329,17 @@ mod tests { fn test_config(server: &MockServer) -> OAuthConfig { OAuthConfig { - auth_url: server.url(""), - token_url: server.url("/oauth/token"), - client_id: "test-client".to_string(), - scopes: vec![ + auth_url: server.url(""), + token_url: server.url("/oauth/token"), + client_id: "test-client".to_string(), + scopes: vec![ "openid".to_string(), "profile".to_string(), "email".to_string(), "offline_access".to_string(), ], redirect_uri: Some("https://auth.openai.com/deviceauth/callback".to_string()), - use_pkce: false, + use_pkce: false, } } @@ -415,11 +415,14 @@ mod tests { let request = strategy.init().await.unwrap(); - assert_eq!(request, AuthContextRequest::DeviceCode { - user_code: "ABCD-EFGH".to_string(), - verification_uri: "https://auth.openai.com/codex/device".to_string(), - expires_in: 300, - }); + assert_eq!( + request, + AuthContextRequest::DeviceCode { + user_code: "ABCD-EFGH".to_string(), + verification_uri: "https://auth.openai.com/codex/device".to_string(), + expires_in: 300, + } + ); init_mock.assert_async().await; } diff --git a/lib/crates/fabro-auth/src/strategy.rs b/lib/crates/fabro-auth/src/strategy.rs index 1d28afccc..d0ca9f3cb 100644 --- a/lib/crates/fabro-auth/src/strategy.rs +++ b/lib/crates/fabro-auth/src/strategy.rs @@ -25,17 +25,17 @@ pub enum AuthMethod { #[must_use] pub fn codex_oauth_config() -> OAuthConfig { OAuthConfig { - auth_url: CODEX_AUTH_URL.to_string(), - token_url: CODEX_TOKEN_URL.to_string(), - client_id: CODEX_CLIENT_ID.to_string(), - scopes: vec![ + auth_url: CODEX_AUTH_URL.to_string(), + token_url: CODEX_TOKEN_URL.to_string(), + client_id: CODEX_CLIENT_ID.to_string(), + scopes: vec![ "openid".to_string(), "profile".to_string(), "email".to_string(), "offline_access".to_string(), ], redirect_uri: Some(format!("{CODEX_AUTH_URL}/deviceauth/callback")), - use_pkce: false, + use_pkce: false, } } @@ -73,9 +73,12 @@ mod tests { async fn api_key_strategy_uses_provider_env_names() { let mut strategy = ApiKeyStrategy::new(Provider::Anthropic); let request = strategy.init().await.unwrap(); - assert_eq!(request, AuthContextRequest::ApiKey { - provider: Provider::Anthropic, - env_var_names: vec!["ANTHROPIC_API_KEY".to_string()], - }); + assert_eq!( + request, + AuthContextRequest::ApiKey { + provider: Provider::Anthropic, + env_var_names: vec!["ANTHROPIC_API_KEY".to_string()], + } + ); } } diff --git a/lib/crates/fabro-auth/src/vault_ext.rs b/lib/crates/fabro-auth/src/vault_ext.rs index 99d1c5306..fc185cce5 100644 --- a/lib/crates/fabro-auth/src/vault_ext.rs +++ b/lib/crates/fabro-auth/src/vault_ext.rs @@ -48,19 +48,19 @@ mod tests { fn oauth_credential() -> AuthCredential { AuthCredential { provider: Provider::OpenAi, - details: AuthDetails::CodexOAuth { - tokens: OAuthTokens { - access_token: "access".to_string(), + details: AuthDetails::CodexOAuth { + tokens: OAuthTokens { + access_token: "access".to_string(), refresh_token: Some("refresh".to_string()), - expires_at: Utc::now() + Duration::hours(1), + expires_at: Utc::now() + Duration::hours(1), }, - config: OAuthConfig { - auth_url: "https://auth.openai.com".to_string(), - token_url: "https://auth.openai.com/oauth/token".to_string(), - client_id: "client".to_string(), - scopes: vec!["openid".to_string()], + config: OAuthConfig { + auth_url: "https://auth.openai.com".to_string(), + token_url: "https://auth.openai.com/oauth/token".to_string(), + client_id: "client".to_string(), + scopes: vec!["openid".to_string()], redirect_uri: Some("https://auth.openai.com/deviceauth/callback".to_string()), - use_pkce: true, + use_pkce: true, }, account_id: Some("acct_123".to_string()), }, @@ -86,12 +86,16 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let mut vault = Vault::load(dir.path().join("secrets.json")).unwrap(); vault_set_credential(&mut vault, "openai_codex", &oauth_credential()).unwrap(); - vault_set_credential(&mut vault, "anthropic", &AuthCredential { - provider: Provider::Anthropic, - details: AuthDetails::ApiKey { - key: "anthropic-key".to_string(), + vault_set_credential( + &mut vault, + "anthropic", + &AuthCredential { + provider: Provider::Anthropic, + details: AuthDetails::ApiKey { + key: "anthropic-key".to_string(), + }, }, - }) + ) .unwrap(); let credentials = vault_credentials_for_provider(&vault, Provider::OpenAi); 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 a25d6f41f..28c3264f1 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(), } } @@ -369,10 +369,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") @@ -433,10 +434,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 4ff7e509b..39739afef 100644 --- a/lib/crates/fabro-cli/src/args.rs +++ b/lib/crates/fabro-cli/src/args.rs @@ -297,17 +297,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, @@ -428,9 +428,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, @@ -442,18 +442,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)] @@ -462,10 +462,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, @@ -477,7 +477,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, @@ -523,14 +523,14 @@ pub(crate) enum SecretTypeArg { #[derive(Args)] pub(crate) struct SecretSetArgs { /// Name of the secret - pub(crate) key: String, + pub(crate) key: String, /// Value to store (omit to enter interactively) - pub(crate) value: Option, + pub(crate) value: Option, /// Read the secret value from stdin #[arg(long, conflicts_with = "value")] pub(crate) value_stdin: bool, #[arg(long, value_enum, default_value = "environment")] - pub(crate) r#type: SecretTypeArg, + pub(crate) r#type: SecretTypeArg, #[arg(long)] pub(crate) description: Option, } @@ -709,10 +709,10 @@ 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, /// Create PR even if the run status is not success/partial_success #[arg(short, long)] - pub(crate) force: bool, + pub(crate) force: bool, } #[derive(Args)] @@ -994,7 +994,7 @@ pub(crate) enum Commands { /// Set up the Fabro environment (LLMs, certs, GitHub) Install { #[command(flatten)] - args: InstallArgs, + args: InstallArgs, #[command(subcommand)] command: Option, }, diff --git a/lib/crates/fabro-cli/src/command_context.rs b/lib/crates/fabro-cli/src/command_context.rs index 2361e8f36..151bebe86 100644 --- a/lib/crates/fabro-cli/src/command_context.rs +++ b/lib/crates/fabro-cli/src/command_context.rs @@ -19,20 +19,20 @@ pub(crate) enum ServerMode { target_override: Option, }, ByStorageDir { - target_override: Option, + target_override: Option, storage_dir_override: Option, }, } pub(crate) struct CommandContext { #[allow(dead_code)] - printer: Printer, - cwd: PathBuf, + printer: Printer, + 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 { @@ -69,7 +69,7 @@ impl CommandContext { Self::new( printer, ServerMode::ByStorageDir { - target_override: args.target.server.clone(), + target_override: args.target.server.clone(), storage_dir_override: args.storage_dir.clone_path(), }, cli_settings, @@ -92,10 +92,13 @@ impl CommandContext { .. } => user_config::load_settings_with_storage_dir(storage_dir_override.as_deref())?, }; - let machine_settings = combine_files(disk_settings, SettingsLayer { - cli: Some(cli_layer.clone()), - ..SettingsLayer::default() - }); + let machine_settings = combine_files( + disk_settings, + SettingsLayer { + cli: Some(cli_layer.clone()), + ..SettingsLayer::default() + }, + ); Ok(Self { printer, diff --git a/lib/crates/fabro-cli/src/commands/artifact/cp.rs b/lib/crates/fabro-cli/src/commands/artifact/cp.rs index dba10bd53..9d2f5be21 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/cp.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/cp.rs @@ -197,11 +197,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 930dc422f..cb2893c6a 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/mod.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/mod.rs @@ -15,11 +15,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 894dc4da3..ae7f18367 100644 --- a/lib/crates/fabro-cli/src/commands/doctor.rs +++ b/lib/crates/fabro-cli/src/commands/doctor.rs @@ -30,10 +30,10 @@ pub(crate) fn check_config( (Some(path), true) => { let display = contract_tilde(&path); CheckResult { - name: "Configuration".to_string(), - status: CheckStatus::Pass, - summary: display.display().to_string(), - details: vec![CheckDetail::new(format!( + name: "Configuration".to_string(), + status: CheckStatus::Pass, + summary: display.display().to_string(), + details: vec![CheckDetail::new(format!( "Loaded from {}", display.display() ))], @@ -43,10 +43,10 @@ pub(crate) fn check_config( (Some(path), false) => { let display = contract_tilde(&path); CheckResult { - name: "Configuration".to_string(), - status: CheckStatus::Warning, - summary: display.display().to_string(), - details: std::iter::once(CheckDetail::new(format!( + name: "Configuration".to_string(), + status: CheckStatus::Warning, + summary: display.display().to_string(), + details: std::iter::once(CheckDetail::new(format!( "Loaded from {}", display.display() ))) @@ -62,10 +62,10 @@ pub(crate) fn check_config( } } (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())) @@ -78,10 +78,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()), @@ -93,10 +93,10 @@ fn check_legacy_env(path: Option) -> Option { path.map(|path| { let display = contract_tilde(&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", display.display() ))], @@ -107,8 +107,8 @@ fn check_legacy_env(path: Option) -> Option { #[derive(Debug, Clone, PartialEq, Eq)] struct StorageDirStatus { - path: PathBuf, - exists: bool, + path: PathBuf, + exists: bool, readable: bool, writable: bool, } @@ -168,20 +168,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( @@ -194,10 +194,10 @@ fn check_version_parity(server_version: &str) -> CheckResult { fn skipped_version_parity(reason: &str) -> CheckResult { CheckResult { - name: "Version parity".to_string(), - status: CheckStatus::Warning, - summary: "skipped".to_string(), - details: vec![CheckDetail::new(format!( + name: "Version parity".to_string(), + status: CheckStatus::Warning, + summary: "skipped".to_string(), + details: vec![CheckDetail::new(format!( "Could not retrieve server version: {reason}" ))], remediation: None, @@ -216,15 +216,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 { @@ -317,9 +317,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: local_checks, }], }; @@ -328,12 +328,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(), @@ -358,12 +358,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(), @@ -386,12 +386,12 @@ pub(crate) async fn run_doctor( if let Err(err) = server.api().get_health().send().await { 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(), ), @@ -411,12 +411,12 @@ pub(crate) async fn run_doctor( } report.sections.push(CheckSection { - title: "Server".to_string(), + title: "Server".to_string(), checks: vec![CheckResult { - name: "Location".to_string(), - status: CheckStatus::Pass, - summary: server.base_url().to_string(), - details: vec![], + name: "Location".to_string(), + status: CheckStatus::Pass, + summary: server.base_url().to_string(), + details: vec![], remediation: None, }], }); @@ -436,12 +436,12 @@ pub(crate) async fn run_doctor( .checks .push(skipped_version_parity(&err.to_string())); 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(), @@ -516,12 +516,15 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let status = probe_storage_dir(dir.path()); - assert_eq!(status, StorageDirStatus { - path: dir.path().to_path_buf(), - exists: true, - readable: true, - writable: true, - }); + assert_eq!( + status, + StorageDirStatus { + path: dir.path().to_path_buf(), + exists: true, + readable: true, + writable: true, + } + ); } #[test] @@ -530,19 +533,22 @@ mod tests { let path = dir.path().join("missing"); let status = probe_storage_dir(&path); - assert_eq!(status, StorageDirStatus { - path, - exists: false, - readable: false, - writable: false, - }); + assert_eq!( + status, + StorageDirStatus { + path, + exists: false, + readable: false, + writable: false, + } + ); } #[test] fn check_storage_dir_pass() { let result = check_storage_dir(&StorageDirStatus { - path: PathBuf::from("/home/user/.fabro"), - exists: true, + path: PathBuf::from("/home/user/.fabro"), + exists: true, readable: true, writable: true, }); @@ -556,8 +562,8 @@ mod tests { #[test] fn check_storage_dir_not_exists() { let result = check_storage_dir(&StorageDirStatus { - path: PathBuf::from("/tmp/nonexistent-fabro-doctor-test-xyz"), - exists: false, + path: PathBuf::from("/tmp/nonexistent-fabro-doctor-test-xyz"), + exists: false, readable: false, writable: false, }); @@ -570,8 +576,8 @@ mod tests { #[test] fn check_storage_dir_not_writable() { let result = check_storage_dir(&StorageDirStatus { - path: PathBuf::from("/home/user/.fabro"), - exists: true, + path: PathBuf::from("/home/user/.fabro"), + exists: true, readable: true, writable: false, }); @@ -605,14 +611,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 28fe4b69f..cc6c25194 100644 --- a/lib/crates/fabro-cli/src/commands/exec.rs +++ b/lib/crates/fabro-cli/src/commands/exec.rs @@ -39,7 +39,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())) @@ -141,10 +141,10 @@ pub(crate) async fn execute( .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) = raw_settings @@ -166,10 +166,10 @@ pub(crate) async fn execute( .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 12a873dc8..2b47c04b6 100644 --- a/lib/crates/fabro-cli/src/commands/graph.rs +++ b/lib/crates/fabro-cli/src/commands/graph.rs @@ -30,12 +30,12 @@ pub(crate) async fn run( let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?; 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?; @@ -52,8 +52,8 @@ pub(crate) async fn run( let rendered = client .render_workflow_graph(types::RenderWorkflowGraphRequest { - manifest: built.manifest, - format: Some(types::RenderWorkflowGraphFormat::Svg), + manifest: built.manifest, + format: Some(types::RenderWorkflowGraphFormat::Svg), direction: args.direction.map(|direction| match direction { GraphDirection::Lr => types::RenderWorkflowGraphDirection::Lr, GraphDirection::Tb => types::RenderWorkflowGraphDirection::Tb, diff --git a/lib/crates/fabro-cli/src/commands/install.rs b/lib/crates/fabro-cli/src/commands/install.rs index 76088205b..1217efe26 100644 --- a/lib/crates/fabro-cli/src/commands/install.rs +++ b/lib/crates/fabro-cli/src/commands/install.rs @@ -430,7 +430,7 @@ enum GitHubInstallSelection { token: String, }, App { - owner: GitHubAppOwner, + owner: GitHubAppOwner, username: Option, }, } @@ -782,11 +782,9 @@ impl InstallInputSource for NonInteractiveInstallInputSource { Ok(GitHubInstallSelection::Token { token }) } Some(InstallGitHubStrategyArg::App) => Ok(GitHubInstallSelection::App { - owner: GitHubAppOwner::parse_scripted( - self.args.github_owner.as_deref().context( - "non-interactive install requires --github-owner for --github-strategy app", - )?, - )?, + owner: GitHubAppOwner::parse_scripted(self.args.github_owner.as_deref().context( + "non-interactive install requires --github-owner for --github-strategy app", + )?)?, username: best_effort_github_username().await, }), None => bail!("non-interactive install requires --github-strategy"), @@ -867,7 +865,7 @@ async fn choose_install_github_selection( Ok(GitHubInstallSelection::Token { token }) } Some(InstallGitHubStrategyArg::App) => Ok(GitHubInstallSelection::App { - owner: GitHubAppOwner::parse_scripted(github_args.owner.as_deref().context( + owner: GitHubAppOwner::parse_scripted(github_args.owner.as_deref().context( "install github --non-interactive requires --owner for --strategy app", )?)?, username: best_effort_github_username().await, @@ -1030,8 +1028,8 @@ fn build_github_app_manifest(app_name: &str, port: u16, web_url: &str) -> serde_ /// Run the GitHub App manifest registration flow via a temporary local server. /// Returns the app metadata and secret pairs to persist for the local server. struct GitHubAppRegistration { - app_id: String, - slug: String, + app_id: String, + slug: String, client_id: String, env_pairs: Vec<(String, String)>, } @@ -1039,17 +1037,17 @@ struct GitHubAppRegistration { enum PendingGitHubSettings { Token, App { - app_id: String, - slug: String, - client_id: String, + app_id: String, + slug: String, + client_id: String, allowed_usernames: Vec, }, } #[derive(Clone, Copy)] struct PendingSettingsWrite<'a> { - path: &'a Path, - contents: &'a str, + path: &'a Path, + contents: &'a str, previous_contents: Option<&'a str>, } @@ -1265,9 +1263,9 @@ async fn persist_vault_secrets_via_server( client .create_secret() .body(CreateSecretRequest { - name: secret.name.clone(), - value: secret.value.clone(), - type_: secret.type_, + name: secret.name.clone(), + value: secret.value.clone(), + type_: secret.type_, description: secret.description.clone(), }) .send() @@ -1306,9 +1304,9 @@ async fn persist_vault_secrets_with( fn credential_secret_request(credential: &AuthCredential) -> Result { Ok(CreateSecretRequest { - name: credential_id_for(credential).map_err(anyhow::Error::msg)?, - value: serde_json::to_string(credential)?, - type_: ApiSecretType::Credential, + name: credential_id_for(credential).map_err(anyhow::Error::msg)?, + value: serde_json::to_string(credential)?, + type_: ApiSecretType::Credential, description: None, }) } @@ -1345,11 +1343,11 @@ async fn persist_install_outputs( } struct PendingGitHubInstallWrite<'a> { - settings_write: PendingSettingsWrite<'a>, - server_env_set: Vec<(String, String)>, + settings_write: PendingSettingsWrite<'a>, + server_env_set: Vec<(String, String)>, server_env_remove: Vec<&'static str>, - vault_set: Vec<(String, String)>, - vault_remove: Vec<&'static str>, + vault_set: Vec<(String, String)>, + vault_remove: Vec<&'static str>, } fn restore_optional_file(path: &Path, previous_contents: Option<&str>) -> Result<()> { @@ -1693,17 +1691,20 @@ async fn run_install_github_inner( } let settings_toml = toml::to_string_pretty(&doc)?; - persist_github_install_changes(&storage_dir, &PendingGitHubInstallWrite { - settings_write: PendingSettingsWrite { - path: &config_path, - contents: settings_toml.as_str(), - previous_contents: Some(existing_config_contents.as_str()), + persist_github_install_changes( + &storage_dir, + &PendingGitHubInstallWrite { + settings_write: PendingSettingsWrite { + path: &config_path, + contents: settings_toml.as_str(), + previous_contents: Some(existing_config_contents.as_str()), + }, + server_env_set, + server_env_remove, + vault_set, + vault_remove, }, - server_env_set, - server_env_remove, - vault_set, - vault_remove, - })?; + )?; if let Some(restart_outcome) = maybe_restart_server_after_github_install(&storage_dir, &config_path, server_was_running) @@ -1856,9 +1857,9 @@ async fn run_install_inner( s.green.apply_to("✔") ); vault_secrets.push(CreateSecretRequest { - name: "GITHUB_TOKEN".to_string(), - value: token, - type_: ApiSecretType::Environment, + name: "GITHUB_TOKEN".to_string(), + value: token, + type_: ApiSecretType::Environment, description: None, }); Some(PendingGitHubSettings::Token) @@ -1889,9 +1890,9 @@ async fn run_install_inner( ); server_env_pairs.extend(registration.env_pairs.iter().cloned()); Some(PendingGitHubSettings::App { - app_id: registration.app_id, - slug: registration.slug, - client_id: registration.client_id, + app_id: registration.app_id, + slug: registration.slug, + client_id: registration.client_id, allowed_usernames: vec![allowed_username], }) } @@ -1996,8 +1997,8 @@ async fn run_install_inner( &server_env_pairs, &vault_secrets, Some(PendingSettingsWrite { - path: &config_path, - contents: settings_toml.as_str(), + path: &config_path, + contents: settings_toml.as_str(), previous_contents: existing_config_contents.as_deref(), }), server_was_running, @@ -2079,7 +2080,7 @@ async fn run_install_inner( || async { fabro_util::printerr!(printer, ""); let doctor_args = DoctorArgs { - target: ServerTargetArgs::default(), + target: ServerTargetArgs::default(), verbose: false, }; doctor::run_doctor(&doctor_args, false, cli, cli_layer, printer).await @@ -2211,9 +2212,10 @@ mod tests { .and_then(|s| s.auth.as_ref()) .and_then(|a| a.methods.clone()) .expect("server.auth.methods should be set"); - assert_eq!(methods, vec![ - fabro_types::settings::ServerAuthMethod::DevToken - ]); + assert_eq!( + methods, + vec![fabro_types::settings::ServerAuthMethod::DevToken] + ); } #[test] @@ -2412,9 +2414,13 @@ client_id = "client-id" let mut doc = toml::Value::Table(toml::Table::default()); merge_server_settings(&mut doc).unwrap(); - write_github_app_settings(&mut doc, "123", "fabro-app", "client-id", &[ - "brynary".to_string() - ]) + write_github_app_settings( + &mut doc, + "123", + "fabro-app", + "client-id", + &["brynary".to_string()], + ) .unwrap(); let github = doc @@ -2602,14 +2608,14 @@ client_id = "client-id" ]; let vault_secrets = vec![ CreateSecretRequest { - name: "GITHUB_TOKEN".to_string(), - value: "gh-token".to_string(), - type_: ApiSecretType::Environment, + name: "GITHUB_TOKEN".to_string(), + value: "gh-token".to_string(), + type_: ApiSecretType::Environment, description: None, }, credential_secret_request(&AuthCredential { provider: Provider::Anthropic, - details: fabro_auth::AuthDetails::ApiKey { + details: fabro_auth::AuthDetails::ApiKey { key: "anthropic-key".to_string(), }, }) @@ -2673,9 +2679,9 @@ client_id = "client-id" async fn persist_vault_secrets_with_leaves_running_server_up() { let dir = tempfile::tempdir().unwrap(); let vault_secrets = vec![CreateSecretRequest { - name: "GITHUB_TOKEN".to_string(), - value: "gh-token".to_string(), - type_: ApiSecretType::Environment, + name: "GITHUB_TOKEN".to_string(), + value: "gh-token".to_string(), + type_: ApiSecretType::Environment, description: None, }]; let server = MockServer::start_async().await; @@ -2887,9 +2893,9 @@ client_id = "client-id" let dir = tempfile::tempdir().unwrap(); let server_env_pairs = vec![("SESSION_SECRET".to_string(), "session".to_string())]; let vault_secrets = vec![CreateSecretRequest { - name: "GITHUB_CLI_TOKEN".to_string(), - value: "gh-token".to_string(), - type_: ApiSecretType::Environment, + name: "GITHUB_CLI_TOKEN".to_string(), + value: "gh-token".to_string(), + type_: ApiSecretType::Environment, description: None, }]; let settings_path = dir.path().join(SETTINGS_CONFIG_FILENAME); @@ -2900,8 +2906,8 @@ client_id = "client-id" &server_env_pairs, &vault_secrets, Some(PendingSettingsWrite { - path: &settings_path, - contents: "_version = 1\n", + path: &settings_path, + contents: "_version = 1\n", previous_contents: None, }), false, @@ -2930,9 +2936,9 @@ client_id = "client-id" let dir = tempfile::tempdir().unwrap(); let server_env_pairs = vec![("SESSION_SECRET".to_string(), "session".to_string())]; let vault_secrets = vec![CreateSecretRequest { - name: "GITHUB_CLI_TOKEN".to_string(), - value: "gh-token".to_string(), - type_: ApiSecretType::Environment, + name: "GITHUB_CLI_TOKEN".to_string(), + value: "gh-token".to_string(), + type_: ApiSecretType::Environment, description: None, }]; let settings_path = dir.path().join(SETTINGS_CONFIG_FILENAME); @@ -2943,8 +2949,8 @@ client_id = "client-id" &server_env_pairs, &vault_secrets, Some(PendingSettingsWrite { - path: &settings_path, - contents: "_version = 1\n[server]\nfoo = \"bar\"\n", + path: &settings_path, + contents: "_version = 1\n[server]\nfoo = \"bar\"\n", previous_contents: Some("_version = 1\n[server]\n"), }), false, @@ -2988,21 +2994,24 @@ client_id = "client-id" let settings_path = dir.path().join(SETTINGS_CONFIG_FILENAME); std::fs::write(&settings_path, "before").unwrap(); - persist_github_install_changes(dir.path(), &PendingGitHubInstallWrite { - settings_write: PendingSettingsWrite { - path: &settings_path, - contents: "after", - previous_contents: Some("before"), + persist_github_install_changes( + dir.path(), + &PendingGitHubInstallWrite { + settings_write: PendingSettingsWrite { + path: &settings_path, + contents: "after", + previous_contents: Some("before"), + }, + server_env_set: Vec::new(), + server_env_remove: vec![ + GITHUB_APP_PRIVATE_KEY_KEY, + GITHUB_APP_CLIENT_SECRET_KEY, + GITHUB_APP_WEBHOOK_SECRET_KEY, + ], + vault_set: vec![(GITHUB_TOKEN_SECRET_KEY.to_string(), "token".to_string())], + vault_remove: Vec::new(), }, - server_env_set: Vec::new(), - server_env_remove: vec![ - GITHUB_APP_PRIVATE_KEY_KEY, - GITHUB_APP_CLIENT_SECRET_KEY, - GITHUB_APP_WEBHOOK_SECRET_KEY, - ], - vault_set: vec![(GITHUB_TOKEN_SECRET_KEY.to_string(), "token".to_string())], - vault_remove: Vec::new(), - }) + ) .unwrap(); let server_env = envfile::read_env_file(&server_env_path).unwrap(); @@ -3046,30 +3055,33 @@ client_id = "client-id" let settings_path = dir.path().join(SETTINGS_CONFIG_FILENAME); std::fs::write(&settings_path, "before").unwrap(); - persist_github_install_changes(dir.path(), &PendingGitHubInstallWrite { - settings_write: PendingSettingsWrite { - path: &settings_path, - contents: "after", - previous_contents: Some("before"), + persist_github_install_changes( + dir.path(), + &PendingGitHubInstallWrite { + settings_write: PendingSettingsWrite { + path: &settings_path, + contents: "after", + previous_contents: Some("before"), + }, + server_env_set: vec![ + ( + GITHUB_APP_PRIVATE_KEY_KEY.to_string(), + "private".to_string(), + ), + ( + GITHUB_APP_CLIENT_SECRET_KEY.to_string(), + "client".to_string(), + ), + ], + server_env_remove: vec![ + GITHUB_APP_PRIVATE_KEY_KEY, + GITHUB_APP_CLIENT_SECRET_KEY, + GITHUB_APP_WEBHOOK_SECRET_KEY, + ], + vault_set: Vec::new(), + vault_remove: vec![GITHUB_TOKEN_SECRET_KEY], }, - server_env_set: vec![ - ( - GITHUB_APP_PRIVATE_KEY_KEY.to_string(), - "private".to_string(), - ), - ( - GITHUB_APP_CLIENT_SECRET_KEY.to_string(), - "client".to_string(), - ), - ], - server_env_remove: vec![ - GITHUB_APP_PRIVATE_KEY_KEY, - GITHUB_APP_CLIENT_SECRET_KEY, - GITHUB_APP_WEBHOOK_SECRET_KEY, - ], - vault_set: Vec::new(), - vault_remove: vec![GITHUB_TOKEN_SECRET_KEY], - }) + ) .unwrap(); let server_env = envfile::read_env_file(&server_env_path).unwrap(); @@ -3137,10 +3149,13 @@ root = "{}" #[test] fn non_interactive_source_rejects_hidden_args_without_switch() { - let args = install_args(false, InstallNonInteractiveArgs { - llm_provider: Some(Provider::Anthropic), - ..InstallNonInteractiveArgs::default() - }); + let args = install_args( + false, + InstallNonInteractiveArgs { + llm_provider: Some(Provider::Anthropic), + ..InstallNonInteractiveArgs::default() + }, + ); let err = NonInteractiveInstallInputSource::new(&args).unwrap_err(); assert!( err.to_string() @@ -3150,14 +3165,17 @@ root = "{}" #[test] fn non_interactive_source_rejects_conflicting_api_key_inputs() { - let args = install_args(true, InstallNonInteractiveArgs { - llm_provider: Some(Provider::Anthropic), - llm_api_key_stdin: true, - llm_api_key_env: Some("ANTHROPIC_API_KEY".to_string()), - github_strategy: Some(InstallGitHubStrategyArg::Token), - github_username: Some("brynary".to_string()), - ..InstallNonInteractiveArgs::default() - }); + let args = install_args( + true, + InstallNonInteractiveArgs { + llm_provider: Some(Provider::Anthropic), + llm_api_key_stdin: true, + llm_api_key_env: Some("ANTHROPIC_API_KEY".to_string()), + github_strategy: Some(InstallGitHubStrategyArg::Token), + github_username: Some("brynary".to_string()), + ..InstallNonInteractiveArgs::default() + }, + ); let err = NonInteractiveInstallInputSource::new(&args).unwrap_err(); assert!( err.to_string() @@ -3330,7 +3348,7 @@ root = "{}" let err = validate_install_github_non_interactive( &InstallGithubArgs { strategy: Some(InstallGitHubStrategyArg::Token), - owner: Some("personal".to_string()), + owner: Some("personal".to_string()), }, true, ) @@ -3347,7 +3365,7 @@ root = "{}" let err = validate_install_github_non_interactive( &InstallGithubArgs { strategy: Some(InstallGitHubStrategyArg::App), - owner: None, + owner: None, }, true, ) diff --git a/lib/crates/fabro-cli/src/commands/model.rs b/lib/crates/fabro-cli/src/commands/model.rs index 68c23407b..c48dc3555 100644 --- a/lib/crates/fabro-cli/src/commands/model.rs +++ b/lib/crates/fabro-cli/src/commands/model.rs @@ -24,21 +24,21 @@ 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, - skipped: u32, + skipped: u32, } pub(crate) async fn execute( @@ -159,25 +159,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) @@ -477,19 +477,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 bcd497a12..6c3762ca3 100644 --- a/lib/crates/fabro-cli/src/commands/pr/list.rs +++ b/lib/crates/fabro-cli/src/commands/pr/list.rs @@ -18,9 +18,9 @@ use crate::shared::{color_if, 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 6ebba6168..7caf44827 100644 --- a/lib/crates/fabro-cli/src/commands/preflight.rs +++ b/lib/crates/fabro-cli/src/commands/preflight.rs @@ -26,12 +26,12 @@ pub(crate) async fn execute( args.verbose = args.verbose || cli.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 49ab984cf..4eb61d1f9 100644 --- a/lib/crates/fabro-cli/src/commands/run/attach.rs +++ b/lib/crates/fabro-cli/src/commands/run/attach.rs @@ -119,10 +119,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( @@ -295,7 +295,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(); @@ -448,7 +448,7 @@ fn state_exit_code(state: &server_client::RunProjection) -> Option { } match state.status.as_ref() { - Some(record) if record.status == RunStatus::Succeeded => Some(ExitCode::from(0)), + Some(record) if record.status == RunStatus::Completed => Some(ExitCode::from(0)), Some(record) if record.status.is_terminal() => Some(ExitCode::from(1)), Some(_) | None => None, } @@ -571,14 +571,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 76a324b32..bc18ac010 100644 --- a/lib/crates/fabro-cli/src/commands/run/cp.rs +++ b/lib/crates/fabro-cli/src/commands/run/cp.rs @@ -16,13 +16,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, }, } @@ -110,13 +110,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 8019aa27c..8bd0a7509 100644 --- a/lib/crates/fabro-cli/src/commands/run/create.rs +++ b/lib/crates/fabro-cli/src/commands/run/create.rs @@ -16,7 +16,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 facd3ff3a..8bf6ec2c5 100644 --- a/lib/crates/fabro-cli/src/commands/run/fork.rs +++ b/lib/crates/fabro-cli/src/commands/run/fork.rs @@ -49,11 +49,14 @@ pub(crate) async fn run( .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 8531b0e4b..a17a2505b 100644 --- a/lib/crates/fabro-cli/src/commands/run/output.rs +++ b/lib/crates/fabro-cli/src/commands/run/output.rs @@ -73,19 +73,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(), } } @@ -97,24 +97,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, } @@ -63,11 +63,14 @@ pub(crate) async fn run( 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?; @@ -94,9 +97,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 4640061f5..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::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::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 88a7a1f9d..a3b0370b9 100644 --- a/lib/crates/fabro-cli/src/commands/run/runner.rs +++ b/lib/crates/fabro-cli/src/commands/run/runner.rs @@ -247,8 +247,8 @@ fn build_artifact_uploader( } struct HttpArtifactUploader { - run_id: RunId, - client: server_client::ServerStoreClient, + run_id: RunId, + client: server_client::ServerStoreClient, bearer_token: String, } @@ -313,7 +313,7 @@ impl StageArtifactUploader for MissingArtifactUploadTokenUploader { struct HttpRunStore { run_id: RunId, client: server_client::ServerStoreClient, - state: Arc>, + state: Arc>, events: Arc>>>, } @@ -464,7 +464,7 @@ fn worker_title(run_id: &RunId, phase: WorkerTitlePhase) -> String { WorkerTitlePhase::Running => "running", WorkerTitlePhase::Waiting => "waiting", WorkerTitlePhase::Paused => "paused", - WorkerTitlePhase::Succeeded => "succeeded", + WorkerTitlePhase::Succeeded => "completed", WorkerTitlePhase::Failed => "failed", WorkerTitlePhase::Cancelled => "cancelled", }; @@ -607,7 +607,7 @@ mod tests { ); assert_eq!( worker_title(&fixtures::RUN_1, WorkerTitlePhase::Succeeded), - format!("fabro {short_id} succeeded") + format!("fabro {short_id} completed") ); } @@ -637,12 +637,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, })), @@ -651,39 +651,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) @@ -815,7 +815,7 @@ mod tests { "anthropic", &serde_json::to_string(&AuthCredential { provider: Provider::Anthropic, - details: AuthDetails::ApiKey { + details: AuthDetails::ApiKey { key: "vault-key".to_string(), }, }) diff --git a/lib/crates/fabro-cli/src/commands/run/wait.rs b/lib/crates/fabro-cli/src/commands/run/wait.rs index 33bfaf18a..483e73f97 100644 --- a/lib/crates/fabro-cli/src/commands/run/wait.rs +++ b/lib/crates/fabro-cli/src/commands/run/wait.rs @@ -52,7 +52,7 @@ pub(crate) async fn run( if started_waiting_at.elapsed() < WAIT_STARTUP_GRACE { RunStatus::Submitted } else { - RunStatus::Dead + RunStatus::Failed } }); @@ -86,7 +86,7 @@ pub(crate) async fn run( print_human_output(final_status, &run_id, conclusion.as_ref(), styles, printer); } - if final_status == RunStatus::Succeeded { + if final_status == RunStatus::Completed { Ok(()) } else { std::process::exit(1); @@ -123,10 +123,10 @@ fn print_human_output( printer: Printer, ) { let (style, label) = match status { - RunStatus::Succeeded => (&styles.bold_green, "Succeeded"), + RunStatus::Completed => (&styles.bold_green, "Completed"), RunStatus::Failed => (&styles.bold_red, "Failed"), - RunStatus::Dead => (&styles.bold_red, "Dead"), - // Poll loop only breaks on is_terminal() which is Succeeded | Failed | Dead + RunStatus::Cancelled => (&styles.bold_red, "Cancelled"), + // Poll loop only breaks on is_terminal() which is Completed | Failed | Cancelled _ => unreachable!(), }; let status_display = style.apply_to(label); @@ -167,29 +167,29 @@ mod tests { } #[test] - fn json_output_succeeded_with_conclusion() { + fn json_output_completed_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)); + let json = build_json_output(RunStatus::Completed, &run_id, Some(&conclusion)); assert_eq!(json["run_id"], run_id.to_string()); - assert_eq!(json["status"], "succeeded"); + assert_eq!(json["status"], "completed"); assert_eq!(json["duration_ms"], 12345); assert_eq!(json["total_usd_micros"], 420_000); } @@ -205,23 +205,23 @@ mod tests { } #[test] - fn json_output_dead_status() { - let json = build_json_output(RunStatus::Dead, &fixtures::RUN_3, None); - assert_eq!(json["status"], "dead"); + fn json_output_cancelled_status() { + let json = build_json_output(RunStatus::Cancelled, &fixtures::RUN_3, None); + assert_eq!(json["status"], "cancelled"); } #[test] 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()); @@ -229,30 +229,30 @@ mod tests { } #[test] - fn human_output_succeeded() { + fn human_output_completed() { 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, + RunStatus::Completed, &run_id, Some(&conclusion), &styles, @@ -276,7 +276,7 @@ mod tests { fn poll_terminal_immediately() { let dir = tempfile::tempdir().unwrap(); let status_path = dir.path().join("status.json"); - let record = RunStatusRecord::new(RunStatus::Succeeded, None); + let record = RunStatusRecord::new(RunStatus::Completed, None); std::fs::write(&status_path, serde_json::to_string_pretty(&record).unwrap()).unwrap(); // Simulate what the poll loop does @@ -286,18 +286,18 @@ mod tests { .unwrap() .status; assert!(status.is_terminal()); - assert_eq!(status, RunStatus::Succeeded); + assert_eq!(status, RunStatus::Completed); } #[test] - fn missing_status_treated_as_dead() { + fn missing_status_treated_as_failed() { let status = match std::fs::read_to_string(std::path::Path::new("/nonexistent/status.json")) { Ok(data) => serde_json::from_str::(&data) .map(|record| record.status) - .unwrap_or(RunStatus::Dead), - Err(_) => RunStatus::Dead, + .unwrap_or(RunStatus::Failed), + Err(_) => RunStatus::Failed, }; - assert_eq!(status, RunStatus::Dead); + assert_eq!(status, RunStatus::Failed); } } diff --git a/lib/crates/fabro-cli/src/commands/runs/inspect.rs b/lib/crates/fabro-cli/src/commands/runs/inspect.rs index a014e9d57..4aeb0f24e 100644 --- a/lib/crates/fabro-cli/src/commands/runs/inspect.rs +++ b/lib/crates/fabro-cli/src/commands/runs/inspect.rs @@ -12,13 +12,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( @@ -40,24 +40,24 @@ pub(crate) async fn run( 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/runs/list.rs b/lib/crates/fabro-cli/src/commands/runs/list.rs index dafe0d418..3a7d91f1e 100644 --- a/lib/crates/fabro-cli/src/commands/runs/list.rs +++ b/lib/crates/fabro-cli/src/commands/runs/list.rs @@ -146,15 +146,16 @@ pub(crate) async fn list_command( fn status_cell(status: RunStatus, use_color: bool) -> CellStruct { let text = status.to_string(); let color = match status { - RunStatus::Succeeded => Some(Color::Green), - RunStatus::Failed => Some(Color::Red), - RunStatus::Running | RunStatus::Starting | RunStatus::Submitted => Some(Color::Cyan), - RunStatus::Removing => Some(Color::Yellow), + RunStatus::Completed => Some(Color::Green), + RunStatus::Failed | RunStatus::Cancelled => Some(Color::Red), + RunStatus::Running | RunStatus::Starting | RunStatus::Submitted | RunStatus::Queued => { + Some(Color::Cyan) + } + RunStatus::Blocked | RunStatus::Removing => Some(Color::Yellow), RunStatus::Paused => Some(Color::Magenta), - RunStatus::Dead => Some(Color::Ansi256(8)), }; text.cell() - .bold(use_color && color != Some(Color::Ansi256(8))) + .bold(use_color) .foreground_color(color_if(use_color, color.unwrap_or(Color::Ansi256(8)))) } diff --git a/lib/crates/fabro-cli/src/commands/server/foreground.rs b/lib/crates/fabro-cli/src/commands/server/foreground.rs index 390dc4a38..e3d8a3d5a 100644 --- a/lib/crates/fabro-cli/src/commands/server/foreground.rs +++ b/lib/crates/fabro-cli/src/commands/server/foreground.rs @@ -52,13 +52,16 @@ 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(), - dev_token_path: dev_token_path.clone(), - started_at: Utc::now(), - }) + record::write_server_record( + &record_path, + &record::ServerRecord { + pid, + bind: resolved_bind.clone(), + log_path: log_path.clone(), + dev_token_path: dev_token_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 c98b886f9..3de73718f 100644 --- a/lib/crates/fabro-cli/src/commands/server/record.rs +++ b/lib/crates/fabro-cli/src/commands/server/record.rs @@ -9,17 +9,17 @@ 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, #[serde(skip_serializing_if = "Option::is_none")] pub dev_token_path: Option, - pub started_at: DateTime, + 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 b3c51e199..0e4926262 100644 --- a/lib/crates/fabro-cli/src/commands/server/start.rs +++ b/lib/crates/fabro-cli/src/commands/server/start.rs @@ -81,14 +81,14 @@ async fn ensure_server_running_with_bind( } let serve_args = ServeArgs { - bind: bind_request.as_ref().map(ToString::to_string), - web: false, - no_web: false, - model: None, - provider: None, - sandbox: None, + bind: bind_request.as_ref().map(ToString::to_string), + web: false, + no_web: false, + model: None, + provider: None, + 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 = if let Some(bind_request) = bind_request { @@ -266,13 +266,16 @@ async fn execute_foreground( Some(storage_dir), move |resolved_bind| { print_dev_token(printer, &home, &token); - record::write_server_record(&record_path, &record::ServerRecord { - pid, - bind: resolved_bind.clone(), - log_path: log_path.clone(), - dev_token_path: Some(home.dev_token_path()), - started_at: Utc::now(), - }) + record::write_server_record( + &record_path, + &record::ServerRecord { + pid, + bind: resolved_bind.clone(), + log_path: log_path.clone(), + dev_token_path: Some(home.dev_token_path()), + 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 67764a7d3..83360cea8 100644 --- a/lib/crates/fabro-cli/src/commands/store/dump.rs +++ b/lib/crates/fabro-cli/src/commands/store/dump.rs @@ -90,9 +90,9 @@ fn finalize_export( } struct DumpArtifact { - stage_id: StageId, + stage_id: StageId, relative_path: String, - data: Vec, + data: Vec, } trait DumpDataSource { @@ -105,9 +105,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)] @@ -148,9 +148,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) @@ -371,52 +371,50 @@ mod tests { fn sample_status() -> RunStatusRecord { RunStatusRecord { - status: RunStatus::Running, - reason: Some(StatusReason::SandboxInitializing), + status: RunStatus::Running, + reason: Some(StatusReason::SandboxInitializing), + blocked_reason: None, 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, } } @@ -429,12 +427,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()), @@ -446,11 +444,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, } } @@ -487,171 +485,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( @@ -705,7 +755,7 @@ mod tests { assert_eq!(exported_start.run_id, run_id); let exported_status: RunStatusRecord = read_json(&output.path().join("status.json")); - assert_eq!(exported_status.status, RunStatus::Succeeded); + assert_eq!(exported_status.status, RunStatus::Completed); let exported_checkpoint: Checkpoint = read_json(&output.path().join("checkpoint.json")); assert_eq!(exported_checkpoint.current_node, "code"); diff --git a/lib/crates/fabro-cli/src/commands/system/prune.rs b/lib/crates/fabro-cli/src/commands/system/prune.rs index cb85235b6..3ea340180 100644 --- a/lib/crates/fabro-cli/src/commands/system/prune.rs +++ b/lib/crates/fabro-cli/src/commands/system/prune.rs @@ -24,12 +24,12 @@ pub(super) async fn prune_command( .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 c1d56b49f..cbeec8335 100644 --- a/lib/crates/fabro-cli/src/commands/uninstall.rs +++ b/lib/crates/fabro-cli/src/commands/uninstall.rs @@ -18,13 +18,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, } @@ -221,12 +221,12 @@ fn print_preview(inventory: &Inventory, printer: Printer) { #[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, } async fn execute_uninstall(inventory: &Inventory, json: bool, printer: Printer) -> Result<()> { @@ -235,12 +235,12 @@ async fn execute_uninstall(inventory: &Inventory, json: bool, printer: Printer) 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 158b85907..97833d5d4 100644 --- a/lib/crates/fabro-cli/src/commands/upgrade.rs +++ b/lib/crates/fabro-cli/src/commands/upgrade.rs @@ -192,7 +192,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, } @@ -411,7 +411,7 @@ async fn check_and_print_notice(printer: Printer) -> 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); @@ -507,7 +507,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(); @@ -519,7 +519,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()); @@ -532,7 +532,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()); @@ -543,7 +543,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 a530e5470..c263f1215 100644 --- a/lib/crates/fabro-cli/src/commands/validate.rs +++ b/lib/crates/fabro-cli/src/commands/validate.rs @@ -21,12 +21,12 @@ pub(crate) async fn run( ) -> anyhow::Result<()> { let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?; 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/commands/version.rs b/lib/crates/fabro-cli/src/commands/version.rs index 6b90f976d..e37cb6ecd 100644 --- a/lib/crates/fabro-cli/src/commands/version.rs +++ b/lib/crates/fabro-cli/src/commands/version.rs @@ -31,23 +31,23 @@ pub(crate) async fn version_command( Ok(response) => { let response = response.into_inner(); ServerVersionInfo::Success { - address: server_address, - version: response.version, - git_sha: response.git_sha, - build_date: response.build_date, - os: response.os, - arch: response.arch, + address: server_address, + version: response.version, + git_sha: response.git_sha, + build_date: response.build_date, + os: response.os, + arch: response.arch, uptime_secs: response.uptime_secs, } } Err(err) => ServerVersionInfo::Error { address: server_address, - error: err.to_string(), + error: err.to_string(), }, }, Err(err) => ServerVersionInfo::Error { address: server_address, - error: err.to_string(), + error: err.to_string(), }, }; @@ -61,36 +61,36 @@ pub(crate) async fn version_command( } struct ClientVersionInfo { - version: &'static str, - git_sha: &'static str, + version: &'static str, + git_sha: &'static str, build_date: &'static str, - os: &'static str, - arch: &'static str, + os: &'static str, + arch: &'static str, } enum ServerVersionInfo { Success { - address: String, - version: Option, - git_sha: Option, - build_date: Option, - os: Option, - arch: Option, + address: String, + version: Option, + git_sha: Option, + build_date: Option, + os: Option, + arch: Option, uptime_secs: Option, }, Error { address: String, - error: String, + error: String, }, } fn client_info() -> ClientVersionInfo { ClientVersionInfo { - version: env!("CARGO_PKG_VERSION"), - git_sha: env!("FABRO_GIT_SHA"), + version: env!("CARGO_PKG_VERSION"), + git_sha: env!("FABRO_GIT_SHA"), build_date: env!("FABRO_BUILD_DATE"), - os: std::env::consts::OS, - arch: std::env::consts::ARCH, + os: std::env::consts::OS, + arch: std::env::consts::ARCH, } } diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs index 1ec8d43bb..45df1e33f 100644 --- a/lib/crates/fabro-cli/src/main.rs +++ b/lib/crates/fabro-cli/src/main.rs @@ -140,10 +140,13 @@ async fn main_inner() -> (String, Result<()>) { Ok(settings) => settings, Err(err) => return (command_name, Err(err)), }; - let combined_settings = combine_files(user_settings, SettingsLayer { - cli: Some(cli_layer.clone()), - ..SettingsLayer::default() - }); + let combined_settings = combine_files( + user_settings, + SettingsLayer { + cli: Some(cli_layer.clone()), + ..SettingsLayer::default() + }, + ); let cli_settings = match user_config::resolve_cli_settings(&combined_settings) { Ok(cli_settings) => cli_settings, Err(err) => return (command_name, Err(err)), diff --git a/lib/crates/fabro-cli/src/manifest_builder.rs b/lib/crates/fabro-cli/src/manifest_builder.rs index 4aefd3007..c5d184eeb 100644 --- a/lib/crates/fabro-cli/src/manifest_builder.rs +++ b/lib/crates/fabro-cli/src/manifest_builder.rs @@ -19,14 +19,14 @@ use crate::args::{PreflightArgs, RunArgs}; #[derive(Debug)] pub(crate) struct ManifestBuildInput { - pub workflow: PathBuf, - pub cwd: PathBuf, - pub args_layer: SettingsLayer, - pub args: Option, - pub run_id: Option, + pub workflow: PathBuf, + pub cwd: PathBuf, + pub args_layer: SettingsLayer, + pub args: Option, + pub run_id: Option, /// User-level settings layer. Production callers load via /// `load_settings_user()`; tests pass `SettingsLayer::default()`. - pub user_layer: SettingsLayer, + pub user_layer: SettingsLayer, /// Path to the user settings file (for inclusion in /// `RunManifest.configs`). `None` skips the user config entry. pub user_settings_path: Option, @@ -34,21 +34,21 @@ pub(crate) struct ManifestBuildInput { #[derive(Debug)] pub(crate) struct BuiltManifest { - pub manifest: types::RunManifest, + pub manifest: types::RunManifest, pub target_path: PathBuf, } struct CollectContext<'a> { - cwd: &'a Path, - workflows: HashMap, + cwd: &'a Path, + workflows: HashMap, visited_workflows: HashSet, } #[derive(Clone)] struct WorkflowScanInput { absolute_dot_path: PathBuf, - logical_dot_path: PathBuf, - source: String, + logical_dot_path: PathBuf, + source: String, } pub(crate) fn build_run_manifest(input: ManifestBuildInput) -> Result { @@ -64,8 +64,8 @@ pub(crate) fn build_run_manifest(input: ManifestBuildInput) -> Result Result Result Result Option { let payload = types::ManifestArgs { - auto_approve: args.auto_approve.then_some(true), - dry_run: args.dry_run.then_some(true), - label: args.label.clone(), - model: args.model.clone(), - no_retro: args.no_retro.then_some(true), + auto_approve: args.auto_approve.then_some(true), + dry_run: args.dry_run.then_some(true), + label: args.label.clone(), + model: args.model.clone(), + no_retro: args.no_retro.then_some(true), preserve_sandbox: args.preserve_sandbox.then_some(true), - provider: args.provider.clone(), - sandbox: args + provider: args.provider.clone(), + sandbox: args .sandbox .map(|provider| fabro_sandbox::SandboxProvider::from(provider).to_string()), - verbose: args.verbose.then_some(true), + verbose: args.verbose.then_some(true), }; (!manifest_args_is_empty(&payload)).then_some(payload) } pub(crate) fn preflight_manifest_args(args: &PreflightArgs) -> Option { let payload = types::ManifestArgs { - auto_approve: None, - dry_run: None, - label: Vec::new(), - model: args.model.clone(), - no_retro: None, + auto_approve: None, + dry_run: None, + label: Vec::new(), + model: args.model.clone(), + no_retro: None, preserve_sandbox: None, - provider: args.provider.clone(), - sandbox: args + provider: args.provider.clone(), + sandbox: args .sandbox .map(|provider| fabro_sandbox::SandboxProvider::from(provider).to_string()), - verbose: args.verbose.then_some(true), + verbose: args.verbose.then_some(true), }; (!manifest_args_is_empty(&payload)).then_some(payload) } @@ -191,7 +191,7 @@ fn collect_workflow_entry( .with_context(|| format!("Failed to read {}", resolution.dot_path.display()))?; let config = if let Some(workflow_toml_path) = resolution.workflow_toml_path.as_ref() { Some(types::ManifestWorkflowConfig { - path: logical_path_string(&to_logical_path(workflow_toml_path, context.cwd)?), + path: logical_path_string(&to_logical_path(workflow_toml_path, context.cwd)?), source: std::fs::read_to_string(workflow_toml_path) .with_context(|| format!("Failed to read {}", workflow_toml_path.display()))?, }) @@ -211,13 +211,14 @@ fn collect_workflow_entry( } collect_workflow_files(context, &scan, &mut files, &mut visited_imports)?; - context - .workflows - .insert(logical_dot_key, types::ManifestWorkflow { + context.workflows.insert( + logical_dot_key, + types::ManifestWorkflow { config, files, source, - }); + }, + ); Ok(()) } @@ -288,8 +289,8 @@ fn collect_workflow_files( })?; let imported_scan = WorkflowScanInput { absolute_dot_path: imported.absolute_path, - logical_dot_path: imported.logical_path, - source: imported_source, + logical_dot_path: imported.logical_path, + source: imported_source, }; collect_workflow_files(context, &imported_scan, files, visited_imports)?; } @@ -347,7 +348,7 @@ fn collect_workflow_config_files( struct BundledFile { absolute_path: PathBuf, - logical_path: PathBuf, + logical_path: PathBuf, } fn collect_bundled_file( @@ -365,14 +366,17 @@ fn collect_bundled_file( if !files.contains_key(&key) { let content = std::fs::read_to_string(&absolute_path) .with_context(|| format!("Failed to read {}", absolute_path.display()))?; - files.insert(key.clone(), types::ManifestFileEntry { - content, - ref_: types::ManifestFileRef { - from: from.map(|value| logical_path_string(&value)), - original: reference.to_string(), - type_: ref_type, + files.insert( + key.clone(), + types::ManifestFileEntry { + content, + ref_: types::ManifestFileRef { + from: from.map(|value| logical_path_string(&value)), + original: reference.to_string(), + type_: ref_type, + }, }, - }); + ); } Ok(BundledFile { @@ -421,16 +425,16 @@ fn resolve_manifest_goal( ) .ok_or_else(|| anyhow!("unsupported manifest goal reference: {reference}"))?; return Ok(Some(types::ManifestGoal { - path: Some(reference.to_string()), - text: std::fs::read_to_string(&goal_path) + path: Some(reference.to_string()), + text: std::fs::read_to_string(&goal_path) .with_context(|| format!("Failed to read {}", goal_path.display()))?, type_: types::ManifestGoalType::Graph, })); } Ok(Some(types::ManifestGoal { - path: None, - text: goal.to_string(), + path: None, + text: goal.to_string(), type_: types::ManifestGoalType::Graph, })) } @@ -441,13 +445,13 @@ fn resolve_manifest_goal( fn resolved_goal_to_manifest(resolved: ResolvedRunGoal) -> types::ManifestGoal { match resolved.source { ResolvedGoalSource::Inline => types::ManifestGoal { - path: None, - text: resolved.text, + path: None, + text: resolved.text, type_: types::ManifestGoalType::Value, }, ResolvedGoalSource::File { path } => types::ManifestGoal { - path: Some(path.to_string_lossy().into_owned()), - text: resolved.text, + path: Some(path.to_string_lossy().into_owned()), + text: resolved.text, type_: types::ManifestGoalType::File, }, } @@ -600,12 +604,12 @@ mod tests { .unwrap(); let built = build_run_manifest(ManifestBuildInput { - workflow: PathBuf::from(".fabro/workflows/demo/workflow.toml"), - cwd: project.to_path_buf(), - args_layer: SettingsLayer::default(), - args: None, - run_id: None, - user_layer: SettingsLayer::default(), + workflow: PathBuf::from(".fabro/workflows/demo/workflow.toml"), + cwd: project.to_path_buf(), + args_layer: SettingsLayer::default(), + args: None, + run_id: None, + user_layer: SettingsLayer::default(), user_settings_path: None, }) .unwrap(); @@ -680,12 +684,12 @@ file = "prompts/goal.md" .unwrap(); let built = build_run_manifest(ManifestBuildInput { - workflow: PathBuf::from(".fabro/workflows/demo/workflow.toml"), - cwd: project.to_path_buf(), - args_layer: SettingsLayer::default(), - args: None, - run_id: None, - user_layer: SettingsLayer::default(), + workflow: PathBuf::from(".fabro/workflows/demo/workflow.toml"), + cwd: project.to_path_buf(), + args_layer: SettingsLayer::default(), + args: None, + run_id: None, + user_layer: SettingsLayer::default(), user_settings_path: None, }) .unwrap(); @@ -733,12 +737,12 @@ file = "prompts/goal.md" .unwrap(); let built = build_run_manifest(ManifestBuildInput { - workflow: PathBuf::from(".fabro/workflows/demo/workflow.toml"), - cwd: project.to_path_buf(), - args_layer: SettingsLayer::default(), - args: None, - run_id: None, - user_layer: SettingsLayer::default(), + workflow: PathBuf::from(".fabro/workflows/demo/workflow.toml"), + cwd: project.to_path_buf(), + args_layer: SettingsLayer::default(), + args: None, + run_id: None, + user_layer: SettingsLayer::default(), user_settings_path: None, }) .unwrap(); diff --git a/lib/crates/fabro-cli/src/server_client.rs b/lib/crates/fabro-cli/src/server_client.rs index eae036e4b..bed9835a8 100644 --- a/lib/crates/fabro-cli/src/server_client.rs +++ b/lib/crates/fabro-cli/src/server_client.rs @@ -31,20 +31,20 @@ use crate::{sse, user_config}; #[derive(Clone)] pub(crate) struct ServerStoreClient { - client: fabro_api::Client, + client: fabro_api::Client, http_client: fabro_http::HttpClient, - 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, } @@ -109,7 +109,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 } @@ -436,14 +436,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 { @@ -887,11 +887,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(( @@ -1168,13 +1168,16 @@ mod tests { let record_path = fabro_config::Storage::new(storage.path()) .server_state() .record_path(); - record::write_server_record(&record_path, &record::ServerRecord { - pid: std::process::id(), - bind: Bind::Unix(temp_home.path().join("fabro.sock")), - log_path: storage.path().join("server.log"), - dev_token_path: Some(token_path), - started_at: chrono::Utc::now(), - }) + record::write_server_record( + &record_path, + &record::ServerRecord { + pid: std::process::id(), + bind: Bind::Unix(temp_home.path().join("fabro.sock")), + log_path: storage.path().join("server.log"), + dev_token_path: Some(token_path), + started_at: chrono::Utc::now(), + }, + ) .unwrap(); std::env::remove_var("FABRO_DEV_TOKEN"); diff --git a/lib/crates/fabro-cli/src/server_runs.rs b/lib/crates/fabro-cli/src/server_runs.rs index 8020f7882..2eee54fe8 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 { @@ -63,7 +63,7 @@ impl ServerRunSummaryInfo { } pub(crate) fn status(&self) -> RunStatus { - self.summary.status.unwrap_or(RunStatus::Dead) + self.summary.status.unwrap_or(RunStatus::Failed) } pub(crate) fn status_reason(&self) -> Option { @@ -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/provider_auth.rs b/lib/crates/fabro-cli/src/shared/provider_auth.rs index 0f5843cf6..d1ecfe2f5 100644 --- a/lib/crates/fabro-cli/src/shared/provider_auth.rs +++ b/lib/crates/fabro-cli/src/shared/provider_auth.rs @@ -71,7 +71,7 @@ pub(crate) enum ApiKeySource { pub(crate) async fn validate_api_key(provider: Provider, api_key: &str) -> Result<(), String> { let auth_header = if provider == Provider::Anthropic { ApiKeyHeader::Custom { - name: "x-api-key".to_string(), + name: "x-api-key".to_string(), value: api_key.to_string(), } } else { diff --git a/lib/crates/fabro-cli/src/user_config.rs b/lib/crates/fabro-cli/src/user_config.rs index 4074284d9..45869a197 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)) } @@ -268,7 +268,7 @@ mod tests { .unwrap(), Some(ServerTarget::HttpUrl { api_url: "https://cli.example.com".to_string(), - tls: None, + tls: None, }) ); } @@ -314,7 +314,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, } ); } @@ -338,7 +338,7 @@ url = "https://config.example.com" .unwrap(), ServerTarget::HttpUrl { api_url: "https://cli.example.com".to_string(), - tls: None, + tls: None, } ); } @@ -371,7 +371,7 @@ url = "https://config.example.com" .unwrap(), ServerTarget::HttpUrl { api_url: "https://cli.example.com".to_string(), - tls: None, + tls: None, } ); } @@ -380,8 +380,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#" @@ -405,7 +405,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/attach.rs b/lib/crates/fabro-cli/tests/it/cmd/attach.rs index bfd19be7b..012d8a529 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/attach.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/attach.rs @@ -257,7 +257,7 @@ fn attach_before_completion_streams_to_finished_state() { stderr: stderr_reader.join().expect("stderr reader should join"), }; let snapshot = format_output_snapshot(&output, &filters); - wait_for_status(&run.run_dir, &["succeeded"]); + wait_for_status(&run.run_dir, &["completed"]); insta::assert_snapshot!(snapshot, @" success: true @@ -848,5 +848,5 @@ fn attach_json_errors_without_prompting_for_human_input() { .expect("answer submission should succeed"); assert_eq!(response.status(), fabro_http::StatusCode::NO_CONTENT); }); - wait_for_status(&run.run_dir, &["succeeded"]); + wait_for_status(&run.run_dir, &["completed"]); } diff --git a/lib/crates/fabro-cli/tests/it/cmd/config.rs b/lib/crates/fabro-cli/tests/it/cmd/config.rs index 50dcba537..33b2440cd 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/config.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/config.rs @@ -516,9 +516,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"].as_bool(), 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/inspect.rs b/lib/crates/fabro-cli/tests/it/cmd/inspect.rs index d35ec3082..6a4adc508 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/inspect.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/inspect.rs @@ -77,7 +77,7 @@ fn inspect_completed_run_shows_run_start_conclusion_checkpoint() { [ { "run_id": "[ULID]", - "status": "succeeded", + "status": "completed", "run_record": { "goal": "Run tests and report results", "workflow_name": "Simple", @@ -143,7 +143,7 @@ fn inspect_completed_run_reads_store_without_disk_metadata_files() { [ { "run_id": "[ULID]", - "status": "succeeded", + "status": "completed", "run_record": { "goal": "Run tests and report results", "workflow_name": "Simple", @@ -194,7 +194,7 @@ fn inspect_git_backed_run_exposes_checkpoint_and_sandbox_state() { [ { "run_id": "[ULID]", - "status": "succeeded", + "status": "completed", "run_record": { "goal": "Edit a tracked file", "workflow_name": "Flow", 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 1f1338816..ea80a7e8c 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/pr_view.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/pr_view.rs @@ -74,14 +74,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/ps.rs b/lib/crates/fabro-cli/tests/it/cmd/ps.rs index c10bb077d..d3e010456 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/ps.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/ps.rs @@ -136,7 +136,7 @@ fn ps_all_json_lists_created_and_completed_runs() { "ps should include the created run: {runs:#?}" ); assert!( - runs.iter().any(|run| run["status"] == "succeeded"), + runs.iter().any(|run| run["status"] == "completed"), "ps should include the completed run: {runs:#?}" ); } @@ -247,7 +247,7 @@ fn ps_filters_by_workflow_and_label() { ); let run = &runs[0]; assert_eq!(run["workflow_name"], "Simple"); - assert_eq!(run["status"], "succeeded"); + assert_eq!(run["status"], "completed"); assert_eq!(run["labels"]["suite"], "alpha"); assert_eq!(run["labels"]["fabro_test_case"], context.test_case_id()); assert_eq!(run["labels"]["fabro_test_run"], context.test_run_id()); @@ -274,7 +274,7 @@ fn ps_uses_configured_server_target_without_server_flag() { }, "host_repo_path": "/srv/repo", "start_time": "2026-04-05T12:00:00Z", - "status": "succeeded", + "status": "completed", "status_reason": null, "duration_ms": 123, "total_usd_micros": null diff --git a/lib/crates/fabro-cli/tests/it/cmd/resume.rs b/lib/crates/fabro-cli/tests/it/cmd/resume.rs index 5a1a940dc..ee4a7d239 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); @@ -98,7 +98,7 @@ fn resume_rewound_run_succeeds() { exit_code: 0 ----- stdout ----- ----- stderr ----- - Succeeded [ULID] [DURATION] + Completed [ULID] [DURATION] "); assert_eq!( @@ -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/rm.rs b/lib/crates/fabro-cli/tests/it/cmd/rm.rs index 3b9a663d5..1d336c56b 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/rm.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/rm.rs @@ -279,7 +279,7 @@ fn rm_uses_configured_server_target_without_local_run_dir() { "labels": {}, "host_repo_path": null, "start_time": "2026-04-05T12:00:00Z", - "status": "succeeded", + "status": "completed", "status_reason": null, "duration_ms": 123, "total_usd_micros": null diff --git a/lib/crates/fabro-cli/tests/it/cmd/run.rs b/lib/crates/fabro-cli/tests/it/cmd/run.rs index 01b2220bf..f75601667 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/run.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/run.rs @@ -97,7 +97,7 @@ fn seed_anthropic_vault(storage_dir: &std::path::Path, base_url: &str) { "anthropic", &serde_json::to_string(&AuthCredential { provider: Provider::Anthropic, - details: AuthDetails::ApiKey { + details: AuthDetails::ApiKey { key: "vault-anthropic-key".to_string(), }, }) diff --git a/lib/crates/fabro-cli/tests/it/cmd/start.rs b/lib/crates/fabro-cli/tests/it/cmd/start.rs index 800989341..a4d044676 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/start.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/start.rs @@ -78,7 +78,7 @@ fn start_by_run_id_starts_created_run() { }), @r#" { - "status": "succeeded" + "status": "completed" } "# ); @@ -123,7 +123,7 @@ fn start_by_run_id_starts_created_run_without_run_json_or_status_json() { }), @r#" { - "status": "succeeded" + "status": "completed" } "# ); @@ -173,7 +173,7 @@ fn start_rejects_already_active_or_completed_run() { "); gate.release(); - wait_for_status(&run.run_dir, &["succeeded"]); + wait_for_status(&run.run_dir, &["completed"]); let mut completed_cmd = context.command(); completed_cmd.args(["start", &run_id]); @@ -182,7 +182,7 @@ fn start_rejects_already_active_or_completed_run() { exit_code: 1 ----- stdout ----- ----- stderr ----- - error: cannot start run: status is Succeeded, expected submitted + error: cannot start run: status is Completed, expected submitted "); } @@ -237,5 +237,5 @@ fn start_runs_under_server_ownership_without_launcher_record() { ); gate.release(); - wait_for_status(&run.run_dir, &["succeeded"]); + wait_for_status(&run.run_dir, &["completed"]); } diff --git a/lib/crates/fabro-cli/tests/it/cmd/support.rs b/lib/crates/fabro-cli/tests/it/cmd/support.rs index b8b814fbe..cf980be8a 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/support.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs @@ -35,23 +35,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, } @@ -157,10 +157,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 } @@ -659,7 +659,7 @@ fn block_on(future: impl std::future::Future) -> T { #[derive(Debug, serde::Deserialize)] struct TestServerRecord { - bind: Bind, + bind: Bind, #[serde(default)] dev_token_path: Option, } @@ -829,11 +829,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()) @@ -846,11 +845,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()) @@ -1038,9 +1040,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]; @@ -1049,9 +1053,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]; @@ -1059,8 +1063,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-cli/tests/it/cmd/system_df.rs b/lib/crates/fabro-cli/tests/it/cmd/system_df.rs index da931e5a0..aab3ef0fc 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/system_df.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/system_df.rs @@ -84,7 +84,7 @@ fn system_df_verbose_lists_runs_with_reclaimable_marker() { "verbose system df should include the workflow name: {stdout}" ); assert!( - stdout.contains("succeeded"), + stdout.contains("completed"), "verbose system df should include the run status: {stdout}" ); assert!( diff --git a/lib/crates/fabro-cli/tests/it/cmd/wait.rs b/lib/crates/fabro-cli/tests/it/cmd/wait.rs index 548879852..486cfe161 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/wait.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/wait.rs @@ -49,7 +49,7 @@ fn wait_completed_run_prints_success_summary() { exit_code: 0 ----- stdout ----- ----- stderr ----- - Succeeded [ULID] [DURATION] + Completed [ULID] [DURATION] "); } @@ -70,7 +70,7 @@ fn wait_completed_run_reads_store_without_status_or_conclusion_files() { exit_code: 0 ----- stdout ----- ----- stderr ----- - Succeeded [ULID] [DURATION] + Completed [ULID] [DURATION] "); } @@ -92,7 +92,7 @@ fn wait_completed_run_json_outputs_status_and_duration() { ----- stdout ----- { "run_id": "[ULID]", - "status": "succeeded", + "status": "completed", "duration_ms": [DURATION_MS] } ----- stderr ----- diff --git a/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs b/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs index 895e23f92..cedf40bd9 100644 --- a/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs +++ b/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs @@ -144,7 +144,7 @@ fn dry_run_create_start_attach_works_with_default_run_lookup() { }), @r#" { - "status": "succeeded", + "status": "completed", "has_conclusion": true } "# diff --git a/lib/crates/fabro-config/src/effective_settings.rs b/lib/crates/fabro-config/src/effective_settings.rs index b16f2065b..425e83cf9 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/envfile.rs b/lib/crates/fabro-config/src/envfile.rs index 67a02bdf7..49a1b0f82 100644 --- a/lib/crates/fabro-config/src/envfile.rs +++ b/lib/crates/fabro-config/src/envfile.rs @@ -137,10 +137,13 @@ mod tests { let path = dir.path().join("server.env"); std::fs::write(&path, "EXISTING=value\n").unwrap(); - let entries = merge_env_file(&path, [ - ("SESSION_SECRET", "secret"), - ("FABRO_JWT_PUBLIC_KEY", "jwt"), - ]) + let entries = merge_env_file( + &path, + [ + ("SESSION_SECRET", "secret"), + ("FABRO_JWT_PUBLIC_KEY", "jwt"), + ], + ) .unwrap(); assert_eq!(entries.get("EXISTING").map(String::as_str), Some("value")); 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 a72aa2f60..a988de740 100644 --- a/lib/crates/fabro-config/src/merge.rs +++ b/lib/crates/fabro-config/src/merge.rs @@ -32,12 +32,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), } } @@ -76,10 +76,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), } } @@ -87,10 +87,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), } } @@ -98,30 +98,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), } } @@ -163,7 +163,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), } } @@ -175,9 +175,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), } } @@ -195,13 +195,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), } } @@ -209,10 +209,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), } } @@ -238,12 +238,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), } } @@ -276,9 +276,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), } } @@ -286,7 +286,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), } } @@ -321,18 +321,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), } } @@ -341,10 +341,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: merge_option(lower.output, higher.output, combine_cli_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: merge_option(lower.output, higher.output, combine_cli_output), updates: merge_option(lower.updates, higher.updates, combine_cli_updates), logging: higher.logging.or(lower.logging), } @@ -358,8 +358,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), } } @@ -369,7 +369,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), } } @@ -379,13 +379,13 @@ 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), } } fn combine_cli_output(lower: CliOutputLayer, higher: CliOutputLayer) -> CliOutputLayer { CliOutputLayer { - format: higher.format.or(lower.format), + format: higher.format.or(lower.format), verbosity: higher.verbosity.or(lower.verbosity), } } @@ -400,15 +400,15 @@ fn combine_cli_updates(lower: CliUpdatesLayer, higher: CliUpdatesLayer) -> CliUp 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, @@ -425,14 +425,14 @@ 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), } } fn combine_server_auth(lower: ServerAuthLayer, higher: ServerAuthLayer) -> ServerAuthLayer { ServerAuthLayer { methods: higher.methods.or(lower.methods), - github: higher.github.or(lower.github), + github: higher.github.or(lower.github), } } @@ -451,9 +451,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), } } @@ -462,12 +462,12 @@ 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), - disk_cache: higher.disk_cache.or(lower.disk_cache), + local: higher.local.or(lower.local), + s3: higher.s3.or(lower.s3), + disk_cache: higher.disk_cache.or(lower.disk_cache), } } @@ -485,10 +485,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 de7c8303a..fc8386204 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 { prevent_idle_sleep: exec .prevent_idle_sleep .expect("defaults.toml should provide cli.exec.prevent_idle_sleep"), - 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 b0ac5c82b..0a8a908b3 100644 --- a/lib/crates/fabro-config/src/resolve/mod.rs +++ b/lib/crates/fabro-config/src/resolve/mod.rs @@ -31,11 +31,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), }; @@ -99,7 +99,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)) @@ -166,7 +166,7 @@ Authorization = "Bearer {{ env.HOOK_TOKEN }}" mcps.get("stdio").map(|mcp| &mcp.transport), Some(&McpTransport::Stdio { command: vec!["fabro-mcp".to_string(), "--stdio".to_string()], - env: HashMap::from([( + env: HashMap::from([( "TOKEN".to_string(), "Bearer {{ env.MCP_STDIO_TOKEN }}".to_string(), )]), @@ -175,7 +175,7 @@ Authorization = "Bearer {{ env.HOOK_TOKEN }}" assert_eq!( mcps.get("http").map(|mcp| &mcp.transport), Some(&McpTransport::Http { - url: "https://mcp.example.com".to_string(), + url: "https://mcp.example.com".to_string(), headers: HashMap::from([( "Authorization".to_string(), "Bearer {{ env.MCP_HTTP_TOKEN }}".to_string(), @@ -186,8 +186,8 @@ Authorization = "Bearer {{ env.HOOK_TOKEN }}" mcps.get("sandbox").map(|mcp| &mcp.transport), Some(&McpTransport::Sandbox { command: vec!["fabro-mcp".to_string(), "--sandbox".to_string()], - port: 3333, - env: HashMap::from([( + port: 3333, + env: HashMap::from([( "TOKEN".to_string(), "{{ env.MCP_SANDBOX_TOKEN }}".to_string(), )]), @@ -203,13 +203,13 @@ Authorization = "Bearer {{ env.HOOK_TOKEN }}" assert_eq!( hook.resolved_hook_type().as_deref(), Some(&HookType::Http { - url: "https://hooks.example.com".to_string(), - headers: Some(HashMap::from([( + url: "https://hooks.example.com".to_string(), + headers: Some(HashMap::from([( "Authorization".to_string(), "Bearer {{ env.HOOK_TOKEN }}".to_string(), )])), allowed_env_vars: Vec::new(), - tls: TlsMode::Verify, + tls: TlsMode::Verify, }) ); } diff --git a/lib/crates/fabro-config/src/resolve/project.rs b/lib/crates/fabro-config/src/resolve/project.rs index dbed59297..65b15a09d 100644 --- a/lib/crates/fabro-config/src/resolve/project.rs +++ b/lib/crates/fabro-config/src/resolve/project.rs @@ -4,12 +4,12 @@ use super::ResolveError; 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() .expect("defaults.toml should provide project.directory"), - 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 353715645..a09e0a161 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(), }) }), @@ -100,7 +100,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(), }), } @@ -118,13 +118,13 @@ fn resolve_execution(execution: Option<&RunExecutionLayer>) -> RunExecutionSetti let execution = execution.expect("defaults.toml should provide run.execution defaults"); RunExecutionSettings { - mode: execution + mode: execution .mode .expect("defaults.toml should provide run.execution.mode"), approval: execution .approval .expect("defaults.toml should provide run.execution.approval"), - retros: execution + retros: execution .retros .expect("defaults.toml should provide run.execution.retros"), } @@ -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}"), }), } @@ -186,13 +186,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() @@ -206,16 +206,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 { @@ -223,9 +223,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), } } @@ -244,9 +244,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), } } @@ -263,7 +263,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))) @@ -280,13 +280,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())) @@ -300,8 +300,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(), @@ -362,7 +362,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(), }); } @@ -426,19 +426,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), }) } @@ -448,10 +448,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), } } @@ -462,9 +462,9 @@ fn resolve_pull_request(pull_request: Option<&RunPullRequestLayer>) -> Option, layer: Option<&ServerWebLayer>) -> enabled: layer .enabled .expect("defaults.toml should provide server.web.enabled"), - url: layer + url: layer .url .clone() .expect("defaults.toml should provide server.web.url"), @@ -124,7 +124,7 @@ fn resolve_auth( .unwrap_or_else(|| vec![ServerAuthMethod::DevToken]); if methods.is_empty() { errors.push(ResolveError::Invalid { - path: "server.auth.methods".to_string(), + path: "server.auth.methods".to_string(), reason: "must not be empty".to_string(), }); } @@ -136,7 +136,7 @@ fn resolve_auth( .unwrap_or_default(); if methods.contains(&ServerAuthMethod::Github) && github.allowed_usernames.is_empty() { errors.push(ResolveError::Invalid { - path: "server.auth.github.allowed_usernames".to_string(), + path: "server.auth.github.allowed_usernames".to_string(), reason: "must not be empty when github auth is enabled".to_string(), }); } @@ -162,7 +162,7 @@ fn resolve_artifacts( prefix: layer .and_then(|artifacts| artifacts.prefix.clone()) .expect("defaults.toml should provide server.artifacts.prefix"), - 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()), @@ -257,16 +257,16 @@ fn object_store_default_root(storage_root: &InterpString, domain: &str) -> Inter 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), - strategy: github.strategy.unwrap_or_default(), - app_id: github.app_id.clone(), - client_id: github.client_id.clone(), - slug: github.slug.clone(), + enabled: github.enabled.unwrap_or(true), + strategy: github.strategy.unwrap_or_default(), + 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 { @@ -274,10 +274,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(), @@ -287,7 +287,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 dee80d1ba..cbd9bff1d 100644 --- a/lib/crates/fabro-config/src/resolve/workflow.rs +++ b/lib/crates/fabro-config/src/resolve/workflow.rs @@ -7,12 +7,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() .expect("defaults.toml should provide workflow.graph"), - 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 1ba3a71f9..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() @@ -118,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(), @@ -142,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()); @@ -162,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 60053930b..13148ce21 100644 --- a/lib/crates/fabro-core/src/executor.rs +++ b/lib/crates/fabro-core/src/executor.rs @@ -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, } } } @@ -970,26 +970,26 @@ mod tests { let handler = Arc::new( CountingHandler::new(vec![ Err(Error::handler(HandlerErrorDetail { - message: "fail1".into(), + message: "fail1".into(), retryable: true, - category: None, + category: None, signature: None, })), Err(Error::handler(HandlerErrorDetail { - message: "fail2".into(), + 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, }, }), ); @@ -1041,9 +1041,9 @@ mod tests { async fn executor_retry_non_retryable_error_no_retry() { let handler = Arc::new( CountingHandler::new(vec![Err(Error::handler(HandlerErrorDetail { - message: "fatal".into(), + message: "fatal".into(), retryable: false, - category: None, + category: None, signature: None, }))]) .with_retry_policy(RetryPolicy::with_max_attempts(3)), @@ -1064,9 +1064,9 @@ mod tests { // Default policy is RetryPolicy::none() (max_attempts=1) 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, }, } } @@ -1143,20 +1143,20 @@ mod tests { let handler = Arc::new( CountingHandler::new(vec![ Err(Error::handler(HandlerErrorDetail { - message: "r".into(), + 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, }, }), ); @@ -1187,20 +1187,20 @@ mod tests { let handler = Arc::new( CountingHandler::new(vec![ Err(Error::handler(HandlerErrorDetail { - message: "r".into(), + 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, }, }), ); @@ -1237,20 +1237,20 @@ mod tests { let handler = Arc::new( CountingHandler::new(vec![ Err(Error::handler(HandlerErrorDetail { - message: "r".into(), + 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, }, }), ); @@ -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] @@ -2036,9 +2042,9 @@ mod tests { // First call: fail with retryable, then cancel stall during backoff self.stall.cancel(); Err(Error::handler(HandlerErrorDetail { - message: "transient".into(), + 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, }, } } 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/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/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 dcb544b77..c653af921 100644 --- a/lib/crates/fabro-core/src/test_fixtures.rs +++ b/lib/crates/fabro-core/src/test_fixtures.rs @@ -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, } @@ -180,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", }); } @@ -193,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", }); } @@ -202,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", }); } @@ -211,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", }); } @@ -286,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 { 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(), } } @@ -336,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 { @@ -380,17 +380,17 @@ impl NodeHandler for DispatchHandler { /// 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, @@ -399,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(), 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 bd88e1ed7..e7e33b520 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. @@ -631,16 +631,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) @@ -757,18 +757,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(); @@ -781,30 +784,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"]); @@ -822,54 +831,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 @@ -887,22 +908,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( @@ -931,22 +955,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( @@ -975,16 +1002,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( @@ -1015,30 +1042,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"]); @@ -1051,30 +1084,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"]); @@ -1121,37 +1160,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 @@ -1215,22 +1254,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( @@ -1248,16 +1290,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 e0f7035c0..f074dbf0e 100644 --- a/lib/crates/fabro-github/src/lib.rs +++ b/lib/crates/fabro-github/src/lib.rs @@ -18,21 +18,21 @@ fn http_client() -> Result { /// 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)] @@ -55,14 +55,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, } @@ -173,7 +173,7 @@ pub enum HttpMethod { /// A minimal HTTP response for testability. pub struct HttpResponse { pub status: u16, - body: String, + body: String, } impl HttpResponse { @@ -453,8 +453,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. @@ -476,8 +476,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 client = http_client()?; @@ -542,8 +542,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, }) } @@ -1303,11 +1303,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, } @@ -1554,7 +1554,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubCredentials::App(GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }); let result = @@ -1586,7 +1586,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubCredentials::App(GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }); let result = @@ -1618,7 +1618,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubCredentials::App(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; @@ -1804,7 +1804,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubCredentials::App(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, "") @@ -1841,7 +1841,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubCredentials::App(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, "") @@ -1915,7 +1915,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubCredentials::App(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", "") @@ -1942,7 +1942,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubCredentials::App(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", "") @@ -1970,7 +1970,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubCredentials::App(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", "") @@ -2007,7 +2007,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubCredentials::App(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, "") @@ -2034,7 +2034,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubCredentials::App(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 1943e4ce1..e32be0e75 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() -> GitHubCredentials { GitHubCredentials::App(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() -> GitHubCredentials { 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 09e82eb9d..b610dfd6e 100644 --- a/lib/crates/fabro-graphviz/src/condition.rs +++ b/lib/crates/fabro-graphviz/src/condition.rs @@ -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, } @@ -207,7 +207,7 @@ fn is_word_operator_context(tokens: &[Token]) -> bool { struct Parser { tokens: Vec, - pos: usize, + pos: usize, } impl Parser { @@ -403,8 +403,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(), }) ); @@ -417,13 +417,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(), }), ]) @@ -436,8 +436,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(), }) ); @@ -449,8 +449,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(), }) ); @@ -529,8 +529,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(), }) ); @@ -549,8 +549,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(), }) ); @@ -562,8 +562,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/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/semantic.rs b/lib/crates/fabro-graphviz/src/parser/semantic.rs index 64d7b6954..eb82fa16b 100644 --- a/lib/crates/fabro-graphviz/src/parser/semantic.rs +++ b/lib/crates/fabro-graphviz/src/parser/semantic.rs @@ -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(), } @@ -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 5037cefd7..4cbd95347 100644 --- a/lib/crates/fabro-graphviz/src/stylesheet.rs +++ b/lib/crates/fabro-graphviz/src/stylesheet.rs @@ -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, } 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 b7cba714f..1e5fc8a9f 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: fabro_http::HttpClient, + verify: fabro_http::HttpClient, no_verify: fabro_http::HttpClient, - off: fabro_http::HttpClient, + off: fabro_http::HttpClient, } 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-http/src/lib.rs b/lib/crates/fabro-http/src/lib.rs index f439b3814..597d46c53 100644 --- a/lib/crates/fabro-http/src/lib.rs +++ b/lib/crates/fabro-http/src/lib.rs @@ -65,7 +65,7 @@ macro_rules! define_builder { ($builder_name:ident, $inner_builder:ty, $inner_new:expr, $client_type:ty) => { #[derive(Default)] pub struct $builder_name { - inner: $inner_builder, + inner: $inner_builder, proxy_policy: Option, } @@ -73,7 +73,7 @@ macro_rules! define_builder { #[must_use] pub fn new() -> Self { Self { - inner: $inner_new, + inner: $inner_new, proxy_policy: None, } } @@ -214,7 +214,7 @@ mod tests { use super::*; struct EnvGuard { - key: &'static str, + key: &'static str, original: Option, } 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 f589c47be..0321948cf 100644 --- a/lib/crates/fabro-llm/src/client.rs +++ b/lib/crates/fabro-llm/src/client.rs @@ -18,9 +18,9 @@ const INCEPTION_BASE_URL: &str = "https://api.inceptionlabs.ai/v1"; /// 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 { @@ -49,16 +49,16 @@ impl Client { let mut credentials = Vec::new(); if let Ok(key) = std::env::var("ANTHROPIC_API_KEY") { credentials.push(ApiCredential { - provider: fabro_model::Provider::Anthropic, - auth_header: ApiKeyHeader::Custom { - name: "x-api-key".to_string(), + provider: fabro_model::Provider::Anthropic, + auth_header: ApiKeyHeader::Custom { + name: "x-api-key".to_string(), value: key, }, extra_headers: HashMap::new(), - base_url: std::env::var("ANTHROPIC_BASE_URL").ok(), - codex_mode: false, - org_id: None, - project_id: None, + base_url: std::env::var("ANTHROPIC_BASE_URL").ok(), + codex_mode: false, + org_id: None, + project_id: None, }); } if let Ok(key) = std::env::var("OPENAI_API_KEY") { @@ -85,57 +85,57 @@ impl Client { std::env::var("GEMINI_API_KEY").or_else(|_| std::env::var("GOOGLE_API_KEY")) { credentials.push(ApiCredential { - provider: fabro_model::Provider::Gemini, - auth_header: ApiKeyHeader::Bearer(key), + provider: fabro_model::Provider::Gemini, + auth_header: ApiKeyHeader::Bearer(key), extra_headers: HashMap::new(), - base_url: std::env::var("GEMINI_BASE_URL").ok(), - codex_mode: false, - org_id: None, - project_id: None, + base_url: std::env::var("GEMINI_BASE_URL").ok(), + codex_mode: false, + org_id: None, + project_id: None, }); } if let Ok(key) = std::env::var("KIMI_API_KEY") { credentials.push(ApiCredential { - provider: fabro_model::Provider::Kimi, - auth_header: ApiKeyHeader::Bearer(key), + provider: fabro_model::Provider::Kimi, + auth_header: ApiKeyHeader::Bearer(key), extra_headers: HashMap::new(), - base_url: None, - codex_mode: false, - org_id: None, - project_id: None, + base_url: None, + codex_mode: false, + org_id: None, + project_id: None, }); } if let Ok(key) = std::env::var("ZAI_API_KEY") { credentials.push(ApiCredential { - provider: fabro_model::Provider::Zai, - auth_header: ApiKeyHeader::Bearer(key), + provider: fabro_model::Provider::Zai, + auth_header: ApiKeyHeader::Bearer(key), extra_headers: HashMap::new(), - base_url: None, - codex_mode: false, - org_id: None, - project_id: None, + base_url: None, + codex_mode: false, + org_id: None, + project_id: None, }); } if let Ok(key) = std::env::var("MINIMAX_API_KEY") { credentials.push(ApiCredential { - provider: fabro_model::Provider::Minimax, - auth_header: ApiKeyHeader::Bearer(key), + provider: fabro_model::Provider::Minimax, + auth_header: ApiKeyHeader::Bearer(key), extra_headers: HashMap::new(), - base_url: None, - codex_mode: false, - org_id: None, - project_id: None, + base_url: None, + codex_mode: false, + org_id: None, + project_id: None, }); } if let Ok(key) = std::env::var("INCEPTION_API_KEY") { credentials.push(ApiCredential { - provider: fabro_model::Provider::Inception, - auth_header: ApiKeyHeader::Bearer(key), + provider: fabro_model::Provider::Inception, + auth_header: ApiKeyHeader::Bearer(key), extra_headers: HashMap::new(), - base_url: None, - codex_mode: false, - org_id: None, - project_id: None, + base_url: None, + codex_mode: false, + org_id: None, + project_id: None, }); } Self::from_credentials(credentials).await @@ -148,9 +148,9 @@ impl Client { /// Returns `Error` if any provider adapter fails to initialize. pub async fn from_credentials(credentials: Vec) -> Result { let mut client = Self { - providers: HashMap::new(), + providers: HashMap::new(), default_provider: None, - middleware: Vec::new(), + middleware: Vec::new(), }; for credential in credentials { @@ -251,7 +251,7 @@ impl Client { return Err(Error::Configuration { message: "Provider::OpenAiCompatible is not supported by from_credentials" .to_string(), - source: None, + source: None, }); } } @@ -304,7 +304,7 @@ impl Client { .or(self.default_provider.as_deref()) .ok_or_else(|| Error::Configuration { message: "No provider specified and no default provider set".into(), - source: None, + source: None, })?; self.providers @@ -312,7 +312,7 @@ impl Client { .cloned() .ok_or_else(|| Error::Configuration { message: format!("Provider '{provider_name}' not registered"), - source: None, + source: None, }) } @@ -446,19 +446,19 @@ mod tests { 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, }) } @@ -489,19 +489,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, } } @@ -564,25 +564,25 @@ mod tests { async fn from_credentials_registers_multiple_providers() { let client = Client::from_credentials(vec![ ApiCredential { - provider: fabro_model::Provider::Anthropic, - auth_header: ApiKeyHeader::Custom { - name: "x-api-key".to_string(), + provider: fabro_model::Provider::Anthropic, + auth_header: ApiKeyHeader::Custom { + name: "x-api-key".to_string(), value: "anthropic-key".to_string(), }, extra_headers: HashMap::new(), - base_url: None, - codex_mode: false, - org_id: None, - project_id: None, + base_url: None, + codex_mode: false, + org_id: None, + project_id: None, }, ApiCredential { - provider: fabro_model::Provider::OpenAi, - auth_header: ApiKeyHeader::Bearer("openai-key".to_string()), + provider: fabro_model::Provider::OpenAi, + auth_header: ApiKeyHeader::Bearer("openai-key".to_string()), extra_headers: HashMap::new(), - base_url: None, - codex_mode: false, - org_id: None, - project_id: None, + base_url: None, + codex_mode: false, + org_id: None, + project_id: None, }, ]) .await @@ -597,13 +597,13 @@ mod tests { #[tokio::test] async fn from_credentials_supports_openai_compatible_provider_constants() { let client = Client::from_credentials(vec![ApiCredential { - provider: fabro_model::Provider::Kimi, - auth_header: ApiKeyHeader::Bearer("kimi-key".to_string()), + provider: fabro_model::Provider::Kimi, + auth_header: ApiKeyHeader::Bearer("kimi-key".to_string()), extra_headers: HashMap::new(), - base_url: None, - codex_mode: false, - org_id: None, - project_id: None, + base_url: None, + codex_mode: false, + org_id: None, + project_id: None, }]) .await .unwrap(); diff --git a/lib/crates/fabro-llm/src/error.rs b/lib/crates/fabro-llm/src/error.rs index fd21dd49d..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)), } } @@ -291,7 +291,7 @@ pub fn error_from_status_code( 408 => { return Error::RequestTimeout { message: detail.message, - source: None, + source: None, }; } 413 => ProviderErrorKind::ContextLength, @@ -349,7 +349,7 @@ pub fn error_from_grpc_status( "DEADLINE_EXCEEDED" => { return Error::RequestTimeout { message: detail.message, - source: None, + source: None, }; } _ => ProviderErrorKind::Server, @@ -372,7 +372,7 @@ mod tests { #[test] fn retryable_classification() { let auth_err = Error::Provider { - kind: ProviderErrorKind::Authentication, + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail { status_code: Some(401), ..ProviderErrorDetail::new("bad key", "openai") @@ -381,7 +381,7 @@ mod tests { assert!(!auth_err.retryable()); let rate_err = Error::Provider { - kind: ProviderErrorKind::RateLimit, + kind: ProviderErrorKind::RateLimit, detail: Box::new(ProviderErrorDetail { status_code: Some(429), retry_after: Some(2.0), @@ -392,7 +392,7 @@ mod tests { assert_eq!(rate_err.retry_after(), Some(2.0)); let server_err = Error::Provider { - kind: ProviderErrorKind::Server, + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail { status_code: Some(500), ..ProviderErrorDetail::new("internal error", "anthropic") @@ -402,19 +402,19 @@ mod tests { let timeout = Error::RequestTimeout { message: "timed out".into(), - source: None, + source: None, }; assert!(!timeout.retryable()); let network = Error::Network { message: "connection refused".into(), - source: None, + source: None, }; assert!(network.retryable()); let config = Error::Configuration { message: "missing provider".into(), - source: None, + source: None, }; assert!(!config.retryable()); } @@ -424,37 +424,37 @@ mod tests { let detail = || Box::new(ProviderErrorDetail::new("error", "openai")); let access_denied = Error::Provider { - kind: ProviderErrorKind::AccessDenied, + kind: ProviderErrorKind::AccessDenied, detail: detail(), }; assert!(!access_denied.retryable()); let not_found = Error::Provider { - kind: ProviderErrorKind::NotFound, + kind: ProviderErrorKind::NotFound, detail: detail(), }; assert!(!not_found.retryable()); let invalid_req = Error::Provider { - kind: ProviderErrorKind::InvalidRequest, + kind: ProviderErrorKind::InvalidRequest, detail: detail(), }; assert!(!invalid_req.retryable()); let ctx_length = Error::Provider { - kind: ProviderErrorKind::ContextLength, + kind: ProviderErrorKind::ContextLength, detail: detail(), }; assert!(!ctx_length.retryable()); let quota = Error::Provider { - kind: ProviderErrorKind::QuotaExceeded, + kind: ProviderErrorKind::QuotaExceeded, detail: detail(), }; assert!(!quota.retryable()); let content_filter = Error::Provider { - kind: ProviderErrorKind::ContentFilter, + kind: ProviderErrorKind::ContentFilter, detail: detail(), }; assert!(!content_filter.retryable()); @@ -488,32 +488,44 @@ mod tests { None, None, ); - assert!(matches!(err, Error::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, Error::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, Error::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, Error::Provider { - kind: ProviderErrorKind::InvalidRequest, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::InvalidRequest, + .. + } + )); let err = error_from_status_code( 422, @@ -523,20 +535,26 @@ mod tests { None, None, ); - assert!(matches!(err, Error::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, Error::RequestTimeout { .. })); let err = error_from_status_code(413, "too large".into(), "openai".into(), None, None, None); - assert!(matches!(err, Error::Provider { - kind: ProviderErrorKind::ContextLength, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::ContextLength, + .. + } + )); let err = error_from_status_code( 429, @@ -546,26 +564,35 @@ mod tests { None, Some(5.0), ); - assert!(matches!(err, Error::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, Error::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, Error::Provider { - kind: ProviderErrorKind::Server, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::Server, + .. + } + )); let err = error_from_status_code( 529, @@ -575,10 +602,13 @@ mod tests { None, None, ); - assert!(matches!(err, Error::Provider { - kind: ProviderErrorKind::Server, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::Server, + .. + } + )); assert!(err.retryable()); } @@ -592,10 +622,13 @@ mod tests { None, None, ); - assert!(matches!(err, Error::Provider { - kind: ProviderErrorKind::ContextLength, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::ContextLength, + .. + } + )); } #[test] @@ -608,10 +641,13 @@ mod tests { None, None, ); - assert!(matches!(err, Error::Provider { - kind: ProviderErrorKind::ContextLength, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::ContextLength, + .. + } + )); } #[test] @@ -624,10 +660,13 @@ mod tests { None, None, ); - assert!(matches!(err, Error::Provider { - kind: ProviderErrorKind::ContentFilter, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::ContentFilter, + .. + } + )); } #[test] @@ -640,10 +679,13 @@ mod tests { None, None, ); - assert!(matches!(err, Error::Provider { - kind: ProviderErrorKind::ContentFilter, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::ContentFilter, + .. + } + )); } #[test] @@ -656,10 +698,13 @@ mod tests { None, None, ); - assert!(matches!(err, Error::Provider { - kind: ProviderErrorKind::NotFound, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::NotFound, + .. + } + )); } #[test] @@ -672,10 +717,13 @@ mod tests { None, None, ); - assert!(matches!(err, Error::Provider { - kind: ProviderErrorKind::NotFound, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::NotFound, + .. + } + )); } #[test] @@ -688,10 +736,13 @@ mod tests { None, None, ); - assert!(matches!(err, Error::Provider { - kind: ProviderErrorKind::Authentication, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::Authentication, + .. + } + )); } #[test] @@ -704,10 +755,13 @@ mod tests { None, None, ); - assert!(matches!(err, Error::Provider { - kind: ProviderErrorKind::Authentication, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::Authentication, + .. + } + )); } #[test] @@ -720,10 +774,13 @@ mod tests { None, None, ); - assert!(matches!(err, Error::Provider { - kind: ProviderErrorKind::NotFound, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::NotFound, + .. + } + )); let err = error_from_grpc_status( "RESOURCE_EXHAUSTED", @@ -733,10 +790,13 @@ mod tests { None, None, ); - assert!(matches!(err, Error::Provider { - kind: ProviderErrorKind::RateLimit, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::RateLimit, + .. + } + )); assert!(err.retryable()); let err = error_from_grpc_status( @@ -747,10 +807,13 @@ mod tests { None, None, ); - assert!(matches!(err, Error::Provider { - kind: ProviderErrorKind::Authentication, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::Authentication, + .. + } + )); let err = error_from_grpc_status( "DEADLINE_EXCEEDED", @@ -770,16 +833,19 @@ mod tests { None, None, ); - assert!(matches!(err, Error::Provider { - kind: ProviderErrorKind::Server, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::Server, + .. + } + )); } #[test] fn error_display_messages() { let err = Error::Provider { - kind: ProviderErrorKind::Authentication, + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail { status_code: Some(401), ..ProviderErrorDetail::new("invalid api key", "openai") @@ -792,7 +858,7 @@ mod tests { let err = Error::Configuration { message: "no provider".into(), - source: None, + source: None, }; assert_eq!(err.to_string(), "Configuration error: no provider"); } @@ -800,7 +866,7 @@ mod tests { #[test] fn status_code_accessor() { let err = Error::Provider { - kind: ProviderErrorKind::Server, + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail { status_code: Some(503), ..ProviderErrorDetail::new("error", "openai") @@ -810,7 +876,7 @@ mod tests { let err = Error::Network { message: "refused".into(), - source: None, + source: None, }; assert_eq!(err.status_code(), None); } @@ -818,7 +884,7 @@ mod tests { #[test] fn provider_name_from_provider_variant() { let err = Error::Provider { - kind: ProviderErrorKind::Authentication, + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), }; assert_eq!(err.provider_name(), "openai"); @@ -828,7 +894,7 @@ mod tests { fn provider_name_defaults_to_unknown() { let err = Error::Network { message: "refused".into(), - source: None, + source: None, }; assert_eq!(err.provider_name(), "unknown"); } @@ -839,7 +905,7 @@ mod tests { assert!( Error::Provider { - kind: ProviderErrorKind::RateLimit, + kind: ProviderErrorKind::RateLimit, detail: detail(), } .failover_eligible() @@ -847,7 +913,7 @@ mod tests { assert!( Error::Provider { - kind: ProviderErrorKind::Server, + kind: ProviderErrorKind::Server, detail: detail(), } .failover_eligible() @@ -855,7 +921,7 @@ mod tests { assert!( Error::Provider { - kind: ProviderErrorKind::QuotaExceeded, + kind: ProviderErrorKind::QuotaExceeded, detail: detail(), } .failover_eligible() @@ -867,7 +933,7 @@ mod tests { assert!( Error::RequestTimeout { message: "timed out".into(), - source: None, + source: None, } .failover_eligible() ); @@ -875,7 +941,7 @@ mod tests { assert!( Error::Network { message: "refused".into(), - source: None, + source: None, } .failover_eligible() ); @@ -883,7 +949,7 @@ mod tests { assert!( Error::Stream { message: "broken".into(), - source: None, + source: None, } .failover_eligible() ); @@ -895,7 +961,7 @@ mod tests { assert!( !Error::Provider { - kind: ProviderErrorKind::Authentication, + kind: ProviderErrorKind::Authentication, detail: detail(), } .failover_eligible() @@ -903,7 +969,7 @@ mod tests { assert!( !Error::Provider { - kind: ProviderErrorKind::InvalidRequest, + kind: ProviderErrorKind::InvalidRequest, detail: detail(), } .failover_eligible() @@ -911,7 +977,7 @@ mod tests { assert!( !Error::Provider { - kind: ProviderErrorKind::ContextLength, + kind: ProviderErrorKind::ContextLength, detail: detail(), } .failover_eligible() @@ -919,7 +985,7 @@ mod tests { assert!( !Error::Provider { - kind: ProviderErrorKind::ContentFilter, + kind: ProviderErrorKind::ContentFilter, detail: detail(), } .failover_eligible() @@ -931,7 +997,7 @@ mod tests { assert!( !Error::Configuration { message: "bad".into(), - source: None, + source: None, } .failover_eligible() ); @@ -968,7 +1034,7 @@ mod tests { #[test] fn failure_signature_hint_provider_transient() { let err = Error::Provider { - kind: ProviderErrorKind::RateLimit, + kind: ProviderErrorKind::RateLimit, detail: Box::new(ProviderErrorDetail::new("too fast", "openai")), }; assert_eq!( @@ -977,7 +1043,7 @@ mod tests { ); let err = Error::Provider { - kind: ProviderErrorKind::Server, + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail::new("500", "anthropic")), }; assert_eq!( @@ -989,7 +1055,7 @@ mod tests { #[test] fn failure_signature_hint_provider_deterministic() { let err = Error::Provider { - kind: ProviderErrorKind::Authentication, + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), }; assert_eq!( @@ -998,7 +1064,7 @@ mod tests { ); let err = Error::Provider { - kind: ProviderErrorKind::AccessDenied, + kind: ProviderErrorKind::AccessDenied, detail: Box::new(ProviderErrorDetail::new("denied", "anthropic")), }; assert_eq!( @@ -1007,7 +1073,7 @@ mod tests { ); let err = Error::Provider { - kind: ProviderErrorKind::NotFound, + kind: ProviderErrorKind::NotFound, detail: Box::new(ProviderErrorDetail::new("missing", "openai")), }; assert_eq!( @@ -1016,7 +1082,7 @@ mod tests { ); let err = Error::Provider { - kind: ProviderErrorKind::InvalidRequest, + kind: ProviderErrorKind::InvalidRequest, detail: Box::new(ProviderErrorDetail::new("bad", "openai")), }; assert_eq!( @@ -1025,7 +1091,7 @@ mod tests { ); let err = Error::Provider { - kind: ProviderErrorKind::ContentFilter, + kind: ProviderErrorKind::ContentFilter, detail: Box::new(ProviderErrorDetail::new("blocked", "openai")), }; assert_eq!( @@ -1034,7 +1100,7 @@ mod tests { ); let err = Error::Provider { - kind: ProviderErrorKind::ContextLength, + kind: ProviderErrorKind::ContextLength, detail: Box::new(ProviderErrorDetail::new("too long", "openai")), }; assert_eq!( @@ -1043,7 +1109,7 @@ mod tests { ); let err = Error::Provider { - kind: ProviderErrorKind::QuotaExceeded, + kind: ProviderErrorKind::QuotaExceeded, detail: Box::new(ProviderErrorDetail::new("out of quota", "openai")), }; assert_eq!( @@ -1057,7 +1123,7 @@ mod tests { assert_eq!( Error::RequestTimeout { message: "timed out".into(), - source: None, + source: None, } .failure_signature_hint(), "api_transient|unknown|timeout" @@ -1065,7 +1131,7 @@ mod tests { assert_eq!( Error::Network { message: "refused".into(), - source: None, + source: None, } .failure_signature_hint(), "api_transient|unknown|network" @@ -1073,7 +1139,7 @@ mod tests { assert_eq!( Error::Stream { message: "broken".into(), - source: None, + source: None, } .failure_signature_hint(), "api_transient|unknown|stream" @@ -1088,7 +1154,7 @@ mod tests { assert_eq!( Error::Configuration { message: "bad".into(), - source: None, + source: None, } .failure_signature_hint(), "api_deterministic|unknown|configuration" diff --git a/lib/crates/fabro-llm/src/generate.rs b/lib/crates/fabro-llm/src/generate.rs index f73c42b18..16f6ce2d3 100644 --- a/lib/crates/fabro-llm/src/generate.rs +++ b/lib/crates/fabro-llm/src/generate.rs @@ -48,7 +48,7 @@ fn build_initial_messages(params: &GenerateParams) -> Result, Error if params.messages.is_some() { 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(), } } @@ -175,7 +175,7 @@ pub async fn generate(params: GenerateParams) -> Result { warn!(timeout_secs = per_step, "Per-step timeout exceeded"); Error::RequestTimeout { message: format!("Per-step timeout of {per_step}s exceeded"), - source: None, + source: None, } })? } else { @@ -274,7 +274,7 @@ pub async fn generate(params: GenerateParams) -> Result { warn!(timeout_secs = total, "Total generation timeout exceeded"); Error::RequestTimeout { message: format!("Total timeout of {total}s exceeded"), - source: None, + source: None, } })? } else { @@ -288,30 +288,30 @@ pub type StopCondition = Arc 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, } } @@ -563,7 +563,7 @@ impl Default for StreamAccumulator { /// 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, } @@ -716,7 +716,7 @@ async fn stream_with_tool_loop(params: GenerateParams) -> Result Result Result Some(( Err(Error::RequestTimeout { message: format!("Total timeout of {total_copy}s exceeded"), - source: None, + source: None, }), (stream, true), )), @@ -956,9 +956,9 @@ pub async fn generate_object( ) -> Result { let params = GenerateParams { response_format: Some(ResponseFormat { - kind: ResponseFormatType::JsonSchema, + kind: ResponseFormatType::JsonSchema, json_schema: Some(schema), - strict: true, + strict: true, }), ..params }; @@ -988,7 +988,7 @@ pub type ObjectStream = /// 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, } @@ -1045,9 +1045,9 @@ pub async fn stream_object( ) -> Result { let params = GenerateParams { response_format: Some(ResponseFormat { - kind: ResponseFormatType::JsonSchema, + kind: ResponseFormatType::JsonSchema, json_schema: Some(schema), - strict: true, + strict: true, }), ..params }; @@ -1081,7 +1081,7 @@ 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(), })); } @@ -1101,7 +1101,7 @@ pub async fn stream_object( Err(e) => { events.push(Err(Error::Stream { message: format!("{e}"), - source: None, + source: None, })); } } @@ -1148,19 +1148,19 @@ mod tests { 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, }) } @@ -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, }, )), ]; @@ -1281,45 +1281,45 @@ mod tests { 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, }) } } @@ -1377,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( @@ -1534,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); @@ -1559,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()); @@ -1570,7 +1570,7 @@ mod tests { /// Mock provider that streams JSON tokens incrementally. struct StreamingJsonMockProvider { - deltas: Vec, + deltas: Vec, full_text: String, } @@ -1592,15 +1592,15 @@ mod tests { 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, }) } @@ -1619,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, }, ))); @@ -1783,7 +1783,7 @@ mod tests { 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, } @@ -1800,24 +1800,24 @@ mod tests { 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, }) } @@ -1831,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(); @@ -1986,15 +1986,15 @@ mod tests { 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, }) } @@ -2006,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 }), @@ -2038,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()))), @@ -2156,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( @@ -2314,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] @@ -2325,15 +2325,15 @@ mod tests { 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, }) } @@ -2342,7 +2342,7 @@ mod tests { if count < self.failures { return Err(Error::Provider { - kind: ProviderErrorKind::Server, + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail { status_code: Some(500), ..ProviderErrorDetail::new("server error", "mock") @@ -2352,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()))), @@ -2383,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(); @@ -2439,15 +2439,15 @@ mod tests { 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, }) } @@ -2455,15 +2455,15 @@ mod tests { 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()))), @@ -2538,15 +2538,15 @@ mod tests { 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, }) } @@ -2558,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 }), @@ -2587,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()))), 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/providers/anthropic.rs b/lib/crates/fabro-llm/src/providers/anthropic.rs index aa96e021a..3c18de450 100644 --- a/lib/crates/fabro-llm/src/providers/anthropic.rs +++ b/lib/crates/fabro-llm/src/providers/anthropic.rs @@ -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(), } } @@ -78,7 +78,7 @@ impl Adapter { 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(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, @@ -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, @@ -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 2a227476e..d606519e7 100644 --- a/lib/crates/fabro-llm/src/providers/common.rs +++ b/lib/crates/fabro-llm/src/providers/common.rs @@ -213,8 +213,8 @@ pub async fn send_and_read_response( /// configurable delimiter (e.g. `"\n"` for Gemini/OpenAI-compatible, `"\n\n"` /// for Anthropic/OpenAI SSE event blocks). pub struct LineReader { - response: fabro_http::Response, - buffer: String, + response: fabro_http::Response, + buffer: String, stream_read_timeout: Option, } @@ -267,7 +267,7 @@ impl LineReader { warn!("Stream read timed out waiting for next event"); 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 da9c643e8..3a6862345 100644 --- a/lib/crates/fabro-llm/src/providers/fabro_server.rs +++ b/lib/crates/fabro-llm/src/providers/fabro_server.rs @@ -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: fabro_http::HttpClient, - base_url: String, + client: fabro_http::HttpClient, + 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, } @@ -230,19 +230,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, } } diff --git a/lib/crates/fabro-llm/src/providers/gemini.rs b/lib/crates/fabro-llm/src/providers/gemini.rs index e66d81f2c..6273a1ffc 100644 --- a/lib/crates/fabro-llm/src/providers/gemini.rs +++ b/lib/crates/fabro-llm/src/providers/gemini.rs @@ -58,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, } @@ -84,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. @@ -106,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 --- @@ -116,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, } @@ -136,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, } @@ -163,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)); @@ -359,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(), }] @@ -682,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 { @@ -852,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() { @@ -865,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) @@ -920,7 +920,7 @@ impl ProviderAdapter for Adapter { .as_ref() .and_then(|c| c.first()) .ok_or_else(|| Error::Provider { - kind: ProviderErrorKind::Server, + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail::new( "no candidates in Gemini response", "gemini", @@ -944,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, @@ -991,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, } } @@ -1136,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]); @@ -1155,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]); @@ -1173,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]); @@ -1192,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]); @@ -1219,10 +1219,13 @@ mod tests { None, None, ); - assert!(matches!(err, Error::Provider { - kind: ProviderErrorKind::NotFound, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::NotFound, + .. + } + )); let err = gemini_error( 400, @@ -1231,10 +1234,13 @@ mod tests { None, None, ); - assert!(matches!(err, Error::Provider { - kind: ProviderErrorKind::InvalidRequest, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::InvalidRequest, + .. + } + )); let err = gemini_error( 429, @@ -1243,10 +1249,13 @@ mod tests { None, None, ); - assert!(matches!(err, Error::Provider { - kind: ProviderErrorKind::RateLimit, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::RateLimit, + .. + } + )); let err = gemini_error( 401, @@ -1255,10 +1264,13 @@ mod tests { None, None, ); - assert!(matches!(err, Error::Provider { - kind: ProviderErrorKind::Authentication, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::Authentication, + .. + } + )); let err = gemini_error( 403, @@ -1267,10 +1279,13 @@ mod tests { None, None, ); - assert!(matches!(err, Error::Provider { - kind: ProviderErrorKind::AccessDenied, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::AccessDenied, + .. + } + )); let err = gemini_error( 504, @@ -1287,16 +1302,22 @@ mod tests { use crate::error::ProviderErrorKind; let err = gemini_error(429, "rate limited".into(), None, None, None); - assert!(matches!(err, Error::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, Error::Provider { - kind: ProviderErrorKind::Server, - .. - })); + assert!(matches!( + err, + Error::Provider { + kind: ProviderErrorKind::Server, + .. + } + )); } #[test] @@ -1374,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]); @@ -1396,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 09e29e44e..31622509d 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: fabro_http::HttpClient, - pub(crate) request_timeout: Option, + pub(crate) api_key: String, + pub(crate) base_url: String, + pub(crate) default_headers: HashMap, + pub(crate) client: fabro_http::HttpClient, + 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 23d5d020d..a0bb977c5 100644 --- a/lib/crates/fabro-llm/src/providers/openai.rs +++ b/lib/crates/fabro-llm/src/providers/openai.rs @@ -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, } @@ -112,7 +112,7 @@ impl Adapter { } 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. @@ -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(), @@ -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, @@ -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 = fabro_http::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 354b83b38..15c7d0967 100644 --- a/lib/crates/fabro-llm/src/providers/openai_compatible.rs +++ b/lib/crates/fabro-llm/src/providers/openai_compatible.rs @@ -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, @@ -470,7 +470,7 @@ impl ProviderAdapter for Adapter { .map_err(|e| Error::network(format!("failed to parse response: {e}"), e))?; let choice = api_resp.choices.first().ok_or_else(|| Error::Provider { - kind: ProviderErrorKind::Server, + 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, @@ -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,28 +662,28 @@ impl ProviderAdapter for Adapter { /// State for flattening batched events into individual stream events. struct FlattenState { - inner: std::pin::Pin, Error>> + 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 { @@ -787,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, }); } @@ -848,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, })); } @@ -882,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( @@ -1085,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(); @@ -1141,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()); @@ -1175,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]); @@ -1199,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", @@ -1208,7 +1208,7 @@ mod tests { serde_json::json!({"city": "NYC"}), )), ], - name: None, + name: None, tool_call_id: None, }; let translated = translate_messages(&[msg]); @@ -1226,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]); @@ -1262,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]); @@ -1284,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, } } @@ -1393,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]); @@ -1412,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]); @@ -1432,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]); @@ -1452,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 0feddc07a..09ea45c85 100644 --- a/lib/crates/fabro-llm/src/retry.rs +++ b/lib/crates/fabro-llm/src/retry.rs @@ -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, } } @@ -129,7 +129,7 @@ mod tests { let count = cc.fetch_add(1, Ordering::SeqCst); if count < 2 { Err(Error::Provider { - kind: ProviderErrorKind::Server, + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail { status_code: Some(500), ..ProviderErrorDetail::new("error", "test") @@ -162,7 +162,7 @@ mod tests { async move { cc.fetch_add(1, Ordering::SeqCst); Err::(Error::Provider { - kind: ProviderErrorKind::Server, + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail { status_code: Some(500), ..ProviderErrorDetail::new("error", "test") @@ -192,7 +192,7 @@ mod tests { async move { cc.fetch_add(1, Ordering::SeqCst); Err::(Error::Provider { - kind: ProviderErrorKind::Authentication, + 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() }; @@ -227,7 +227,7 @@ mod tests { async move { cc.fetch_add(1, Ordering::SeqCst); Err::(Error::Provider { - kind: ProviderErrorKind::RateLimit, + 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() }; @@ -265,7 +265,7 @@ mod tests { let count = cc.fetch_add(1, Ordering::SeqCst); if count < 1 { Err(Error::Provider { - kind: ProviderErrorKind::RateLimit, + 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); })), }; @@ -308,7 +308,7 @@ mod tests { let count = cc.fetch_add(1, Ordering::SeqCst); if count < 2 { Err(Error::Provider { - kind: ProviderErrorKind::Server, + 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 8a4bb1c79..f6abe5f76 100644 --- a/lib/crates/fabro-llm/src/types.rs +++ b/lib/crates/fabro-llm/src/types.rs @@ -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: Error, - raw: Option, + raw: Option, }, } @@ -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), } } @@ -696,9 +696,9 @@ pub type OnRetryCallback = Arc, + 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); } @@ -1088,7 +1091,7 @@ mod tests { fn stream_event_error() { 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 1c2cfde0f..7d0da5630 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 38f7b39a0..cd106a3c4 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`. @@ -162,7 +162,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() @@ -283,10 +283,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", @@ -320,10 +320,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", @@ -350,30 +350,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 3af18547b..2186ca195 100644 --- a/lib/crates/fabro-model/src/provider.rs +++ b/lib/crates/fabro-model/src/provider.rs @@ -246,9 +246,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] @@ -280,9 +281,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 08dbd5741..da08b5451 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, } @@ -101,10 +101,10 @@ pub struct OAuthEndpoint<'a> { #[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, } // --------------------------------------------------------------------------- @@ -207,9 +207,9 @@ pub async fn refresh_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 6f0f8f004..78cfa7eef 100644 --- a/lib/crates/fabro-sandbox/src/daytona/mod.rs +++ b/lib/crates/fabro-sandbox/src/daytona/mod.rs @@ -41,20 +41,20 @@ async fn build_daytona_client( /// 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 { @@ -248,11 +248,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, @@ -319,7 +319,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, } @@ -443,7 +443,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 = @@ -457,17 +457,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(), }) }; @@ -509,7 +509,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(); @@ -521,7 +521,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 @@ -537,7 +537,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()) @@ -561,7 +561,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 = @@ -577,12 +577,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 { @@ -590,7 +594,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, }); @@ -686,12 +690,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(()) @@ -708,7 +712,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); } @@ -946,9 +950,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 @@ -984,8 +988,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 892e258eb..77bc15353 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 { @@ -830,10 +830,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(); @@ -848,10 +852,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 e3633bda1..19c5384a3 100644 --- a/lib/crates/fabro-sandbox/src/sandbox_spec.rs +++ b/lib/crates/fabro-sandbox/src/sandbox_spec.rs @@ -27,11 +27,11 @@ 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, - api_key: Option, + api_key: Option, }, } @@ -157,15 +157,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 b7e18879f..74e9dd552 100644 --- a/lib/crates/fabro-server/src/demo/mod.rs +++ b/lib/crates/fabro-server/src/demo/mod.rs @@ -58,7 +58,7 @@ pub(crate) async fn list_board_runs( data.truncate(limit); let columns = json!([ {"id": "working", "name": "Working"}, - {"id": "pending", "name": "Pending"}, + {"id": "blocked", "name": "Blocked"}, {"id": "review", "name": "Review"}, {"id": "merge", "name": "Merge"}, ]); @@ -591,7 +591,7 @@ pub(crate) async fn get_system_disk_usage( { "run_id": "01JQ0000000000000000000001", "workflow_name": "Demo Workflow", - "status": "succeeded", + "status": "completed", "start_time": "2026-04-06T15:00:00Z", "size_bytes": 1024, "reclaimable": true @@ -675,454 +675,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::Initializing, + status: BoardColumn::Working, 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::Initializing, + status: BoardColumn::Working, 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"), }, ] } @@ -1130,32 +1121,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()), }, ] } @@ -1177,139 +1168,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, }, ], } @@ -1318,40 +1309,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, }, @@ -1400,62 +1391,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, }, ], } @@ -1478,26 +1469,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 4682119ec..77a081a7d 100644 --- a/lib/crates/fabro-server/src/diagnostics.rs +++ b/lib/crates/fabro-server/src/diagnostics.rs @@ -33,7 +33,7 @@ fn http_client_or_check( #[derive(Debug, Serialize)] pub struct DiagnosticsReport { - pub version: String, + pub version: String, pub sections: Vec, } @@ -92,14 +92,14 @@ 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: "Configuration".to_string(), + title: "Configuration".to_string(), checks: vec![crypto], }, ], @@ -111,20 +111,20 @@ async fn check_llm_providers(state: &AppState) -> CheckResult { Ok(result) => result, 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()), }; } }; if result.client.provider_names().is_empty() && result.auth_issues.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()), }; } @@ -185,19 +185,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 @@ -215,19 +215,19 @@ async fn check_github_app(state: &AppState) -> CheckResult { Ok(Some(_)) => unreachable!("token strategy should not return app credentials"), Ok(None) => { return CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Warning, - summary: "not configured".to_string(), - details: Vec::new(), + name: "GitHub Token".to_string(), + status: CheckStatus::Warning, + summary: "not configured".to_string(), + details: Vec::new(), remediation: Some("Run fabro install or set GITHUB_TOKEN".to_string()), }; } Err(err) => { return CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Error, - summary: "missing token".to_string(), - details: vec![CheckDetail::new(err.clone())], + name: "GitHub Token".to_string(), + status: CheckStatus::Error, + summary: "missing token".to_string(), + details: vec![CheckDetail::new(err.clone())], remediation: Some(err), }; } @@ -249,18 +249,18 @@ async fn check_github_app(state: &AppState) -> CheckResult { return match probe { Ok(Ok(response)) if response.status().is_success() => CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Pass, - summary: "configured".to_string(), - details: Vec::new(), + name: "GitHub Token".to_string(), + status: CheckStatus::Pass, + summary: "configured".to_string(), + details: Vec::new(), remediation: None, }, Ok(Ok(response)) if response.status() == fabro_http::StatusCode::UNAUTHORIZED => { CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Error, - summary: "token invalid".to_string(), - details: vec![CheckDetail::new(format!( + name: "GitHub Token".to_string(), + status: CheckStatus::Error, + summary: "token invalid".to_string(), + details: vec![CheckDetail::new(format!( "GitHub returned {}", response.status() ))], @@ -268,27 +268,27 @@ async fn check_github_app(state: &AppState) -> CheckResult { } } Ok(Ok(response)) => CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Error, - summary: "connectivity error".to_string(), - details: vec![CheckDetail::new(format!( + name: "GitHub Token".to_string(), + status: CheckStatus::Error, + summary: "connectivity error".to_string(), + details: vec![CheckDetail::new(format!( "GitHub returned {}", response.status() ))], remediation: Some("Check GitHub connectivity and GITHUB_TOKEN".to_string()), }, Ok(Err(err)) => CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Error, - summary: "connectivity error".to_string(), - details: vec![CheckDetail::new(err.to_string())], + name: "GitHub Token".to_string(), + status: CheckStatus::Error, + summary: "connectivity error".to_string(), + details: vec![CheckDetail::new(err.to_string())], remediation: Some("Check GitHub connectivity and GITHUB_TOKEN".to_string()), }, Err(_) => CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Error, - summary: "timeout".to_string(), - details: vec![CheckDetail::new("GitHub probe timed out".to_string())], + name: "GitHub Token".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 GITHUB_TOKEN".to_string()), }, }; @@ -318,20 +318,20 @@ 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 [server.integrations.github].app_id in settings.toml".to_string(), ), @@ -339,10 +339,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { }; 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()), }; }; @@ -351,10 +351,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), }; } @@ -364,10 +364,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), }; } @@ -384,24 +384,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()), }, } @@ -410,18 +410,18 @@ async fn check_github_app(state: &AppState) -> CheckResult { fn check_sandbox(state: &AppState) -> CheckResult { if state.vault_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: "recommended, not configured".to_string(), - details: Vec::new(), + name: "Sandbox".to_string(), + status: CheckStatus::Warning, + summary: "recommended, not configured".to_string(), + details: Vec::new(), remediation: Some( "Run `fabro secret set DAYTONA_API_KEY` to enable cloud sandbox execution" .to_string(), @@ -433,10 +433,10 @@ fn check_sandbox(state: &AppState) -> CheckResult { async fn check_brave_search(state: &AppState) -> CheckResult { let Some(api_key) = state.vault_or_env("BRAVE_SEARCH_API_KEY") else { return CheckResult { - name: "Web Search (Brave)".to_string(), - status: CheckStatus::Warning, - summary: "optional, not configured".to_string(), - details: Vec::new(), + name: "Web Search (Brave)".to_string(), + status: CheckStatus::Warning, + summary: "optional, not configured".to_string(), + details: Vec::new(), remediation: Some( "Run `fabro secret set BRAVE_SEARCH_API_KEY` to enable web search".to_string(), ), @@ -459,31 +459,31 @@ async fn check_brave_search(state: &AppState) -> CheckResult { match probe { Ok(Ok(response)) if response.status().is_success() => CheckResult { - name: "Web Search (Brave)".to_string(), - status: CheckStatus::Pass, - summary: "configured and reachable".to_string(), - details: Vec::new(), + name: "Web Search (Brave)".to_string(), + status: CheckStatus::Pass, + summary: "configured and reachable".to_string(), + details: Vec::new(), remediation: None, }, Ok(Ok(response)) => CheckResult { - name: "Web Search (Brave)".to_string(), - status: CheckStatus::Warning, - summary: format!("HTTP {}", response.status()), - details: Vec::new(), + name: "Web Search (Brave)".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: "Web Search (Brave)".to_string(), - status: CheckStatus::Warning, - summary: "connectivity error".to_string(), - details: vec![CheckDetail::new(err.clone())], + name: "Web Search (Brave)".to_string(), + status: CheckStatus::Warning, + summary: "connectivity error".to_string(), + details: vec![CheckDetail::new(err.clone())], remediation: Some(err), }, Err(_) => CheckResult { - name: "Web Search (Brave)".to_string(), - status: CheckStatus::Warning, - summary: "timeout".to_string(), - details: vec![CheckDetail::new( + name: "Web Search (Brave)".to_string(), + status: CheckStatus::Warning, + summary: "timeout".to_string(), + details: vec![CheckDetail::new( "Web Search (Brave) probe timed out".to_string(), )], remediation: Some("Check BRAVE_SEARCH_API_KEY and network connectivity".to_string()), diff --git a/lib/crates/fabro-server/src/error.rs b/lib/crates/fabro-server/src/error.rs index d2f849c07..cd28b4d01 100644 --- a/lib/crates/fabro-server/src/error.rs +++ b/lib/crates/fabro-server/src/error.rs @@ -54,7 +54,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 ca556dc99..5e0c159ec 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 3319fe45a..f88845439 100644 --- a/lib/crates/fabro-server/src/jwt_auth.rs +++ b/lib/crates/fabro-server/src/jwt_auth.rs @@ -21,15 +21,15 @@ pub enum CredentialSource { #[derive(Clone, Debug, PartialEq, Eq)] pub struct VerifiedAuth { - pub login: String, - pub auth_method: RunAuthMethod, + pub login: String, + pub auth_method: RunAuthMethod, pub credential_source: CredentialSource, - pub provider_id: Option, + pub provider_id: Option, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct ConfiguredAuth { - pub methods: Vec, + pub methods: Vec, pub dev_token: Option, } @@ -138,10 +138,10 @@ fn authenticate_bearer(token: &str, config: &ConfiguredAuth) -> Result Result 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, } @@ -210,7 +210,7 @@ impl FromRequestParts for AuthenticatedSubject { match auth_mode { AuthMode::Disabled => Ok(Self { - login: None, + login: None, auth_method: RunAuthMethod::Disabled, }), AuthMode::Enabled(config) => { @@ -220,7 +220,7 @@ impl FromRequestParts for AuthenticatedSubject { authenticate_session(parts, config)? }; Ok(Self { - login: Some(auth.login), + login: Some(auth.login), auth_method: auth.auth_method, }) } @@ -304,7 +304,7 @@ mod tests { fn dev_token_mode() -> AuthMode { AuthMode::Enabled(ConfiguredAuth { - methods: vec![ServerAuthMethod::DevToken], + methods: vec![ServerAuthMethod::DevToken], dev_token: Some( "fabro_dev_abababababababababababababababababababababababababababababababab" .to_string(), @@ -463,7 +463,7 @@ client_id = "Iv1.test" #[tokio::test] async fn cookie_session_reports_github_provenance() { let app = subject_router(AuthMode::Enabled(ConfiguredAuth { - methods: vec![ServerAuthMethod::Github], + methods: vec![ServerAuthMethod::Github], dev_token: None, })); let response = app diff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/crates/fabro-server/src/run_manifest.rs index fe5a1a119..addbe12b6 100644 --- a/lib/crates/fabro-server/src/run_manifest.rs +++ b/lib/crates/fabro-server/src/run_manifest.rs @@ -40,14 +40,14 @@ use crate::server_secrets::auth_issue_message; #[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, } @@ -121,9 +121,9 @@ pub(crate) fn validate_prepared_manifest( prepared: &PreparedManifest, ) -> 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(), }) } @@ -183,11 +183,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)) @@ -222,8 +225,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 = @@ -236,7 +239,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| { @@ -246,7 +249,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 = @@ -345,7 +348,7 @@ async fn build_preflight_report( if validated.has_errors() { return Ok(( CheckReport { - title: "Run Preflight".into(), + title: "Run Preflight".into(), sections: vec![CheckSection { title: String::new(), checks, @@ -408,7 +411,7 @@ async fn build_preflight_report( Ok(( CheckReport { - title: "Run Preflight".into(), + title: "Run Preflight".into(), sections: vec![CheckSection { title: String::new(), checks, @@ -435,10 +438,10 @@ fn base_preflight_checks(prepared: &PreparedManifest, graph: &Graph) -> Vec Vec { 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 @@ -538,10 +541,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 @@ -549,10 +552,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 @@ -646,12 +649,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}" )), @@ -664,10 +665,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 @@ -715,15 +716,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() @@ -736,14 +737,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, } } @@ -775,26 +776,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 credentials or origin URL available".to_string()), }), } @@ -842,11 +843,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(), }, } } @@ -857,14 +858,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, @@ -884,7 +885,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 { @@ -892,20 +893,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(), } } @@ -915,35 +916,41 @@ 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(), + }, + )]), } } fn invalid_manifest() -> types::RunManifest { types::RunManifest { - workflows: HashMap::from([("workflow.fabro".to_string(), types::ManifestWorkflow { - config: None, - files: HashMap::new(), - source: "digraph Invalid { exit [shape=Msquare] orphan exit -> orphan }" - .to_string(), - })]), + workflows: HashMap::from([( + "workflow.fabro".to_string(), + types::ManifestWorkflow { + config: None, + files: HashMap::new(), + source: "digraph Invalid { exit [shape=Msquare] orphan exit -> orphan }" + .to_string(), + }, + )]), ..minimal_manifest() } } @@ -967,15 +974,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(); @@ -1009,7 +1016,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 @@ -1019,7 +1026,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 @@ -1032,7 +1039,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(); @@ -1041,9 +1048,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 @@ -1084,7 +1092,7 @@ app_id = "snapshotted-app-id" let state = crate::server::create_app_state(); let mut manifest = minimal_manifest(); manifest.configs.push(types::ManifestConfig { - path: Some("/tmp/project/.fabro/project.toml".to_string()), + path: Some("/tmp/project/.fabro/project.toml".to_string()), source: Some( r" _version = 1 @@ -1094,7 +1102,7 @@ enabled = true " .to_string(), ), - type_: types::ManifestConfigType::Project, + type_: types::ManifestConfigType::Project, }); let prepared = @@ -1122,7 +1130,7 @@ enabled = true let state = crate::server::create_app_state(); let mut manifest = minimal_manifest(); manifest.configs.push(types::ManifestConfig { - path: Some("/tmp/project/.fabro/project.toml".to_string()), + path: Some("/tmp/project/.fabro/project.toml".to_string()), source: Some( r#" _version = 1 @@ -1132,7 +1140,7 @@ provider = "daytona" "# .to_string(), ), - type_: types::ManifestConfigType::Project, + type_: types::ManifestConfigType::Project, }); let prepared = diff --git a/lib/crates/fabro-server/src/serve.rs b/lib/crates/fabro-server/src/serve.rs index 45c3a1ec4..f8f10559f 100644 --- a/lib/crates/fabro-server/src/serve.rs +++ b/lib/crates/fabro-server/src/serve.rs @@ -217,7 +217,7 @@ fn bind_override_layer(bind: BindRequest) -> SettingsLayer { }, BindRequest::Tcp(address) => ServerListenLayer::Tcp { address: Some(InterpString::parse(&address.to_string())), - tls: None, + tls: None, }, BindRequest::TcpHost(_) => { unreachable!("host-only bind requests are handled before building a settings override") @@ -692,14 +692,14 @@ mod tests { fn apply_runtime_settings_preserves_storage_dir() { let base = SettingsLayer::default(); let args = ServeArgs { - bind: None, - model: None, - provider: None, - sandbox: None, - web: false, - no_web: false, + bind: None, + model: None, + provider: None, + sandbox: None, + web: false, + no_web: false, max_concurrent_runs: None, - config: None, + config: None, }; let resolved = apply_runtime_settings(&base, &args, &PathBuf::from("/srv/fabro-storage")); @@ -724,14 +724,14 @@ enabled = false ", ); let args = ServeArgs { - bind: None, - model: None, - provider: None, - sandbox: None, - web: true, - no_web: false, + bind: None, + model: None, + provider: None, + sandbox: None, + web: true, + no_web: false, max_concurrent_runs: None, - config: None, + config: None, }; let resolved = apply_runtime_settings(&base, &args, &PathBuf::from("/srv/fabro")); @@ -750,14 +750,14 @@ enabled = false fn apply_runtime_settings_disables_web_from_cli_flag() { let base = SettingsLayer::default(); let args = ServeArgs { - bind: None, - model: None, - provider: None, - sandbox: None, - web: false, - no_web: true, + bind: None, + model: None, + provider: None, + sandbox: None, + web: false, + no_web: true, max_concurrent_runs: None, - config: None, + config: None, }; let resolved = apply_runtime_settings(&base, &args, &PathBuf::from("/srv/fabro")); diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs index 86f0cc6ab..438dee441 100644 --- a/lib/crates/fabro-server/src/server.rs +++ b/lib/crates/fabro-server/src/server.rs @@ -126,7 +126,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, } @@ -134,13 +134,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)] @@ -154,7 +154,7 @@ struct EventListParams { #[serde(default)] since_seq: Option, #[serde(default)] - limit: Option, + limit: Option, } impl EventListParams { @@ -193,7 +193,7 @@ struct ArtifactFilenameParams { #[derive(serde::Deserialize)] struct SandboxFilesParams { - path: String, + path: String, #[serde(default)] depth: Option, } @@ -221,22 +221,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)] @@ -281,18 +281,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)] @@ -302,29 +302,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, } pub(crate) type RegistryFactoryOverride = @@ -382,15 +382,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, @@ -424,12 +424,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(), }); @@ -528,38 +528,38 @@ 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) vault: Arc>, - pub(crate) server_secrets: ServerSecrets, + pub(crate) vault: Arc>, + pub(crate) server_secrets: ServerSecrets, pub(crate) provider_credentials: ProviderCredentials, - pub(crate) settings: Arc>, - pub(crate) server_settings: RwLock>, - pub(crate) local_daemon_mode: bool, - shutting_down: AtomicBool, - registry_factory_override: Option>, - slack_service: Option>, - slack_started: AtomicBool, + pub(crate) settings: Arc>, + pub(crate) server_settings: RwLock>, + pub(crate) local_daemon_mode: bool, + shutting_down: AtomicBool, + registry_factory_override: Option>, + slack_service: Option>, + slack_started: AtomicBool, } pub(crate) struct AppStateConfig { - pub(crate) settings: Arc>, + pub(crate) settings: Arc>, pub(crate) registry_factory_override: Option>, - pub(crate) max_concurrent_runs: usize, - pub(crate) store: Arc, - pub(crate) artifact_store: ArtifactStore, - pub(crate) vault_path: PathBuf, - pub(crate) server_env_path: PathBuf, - pub(crate) local_daemon_mode: bool, - pub(crate) env_lookup: EnvLookup, + pub(crate) max_concurrent_runs: usize, + pub(crate) store: Arc, + pub(crate) artifact_store: ArtifactStore, + pub(crate) vault_path: PathBuf, + pub(crate) server_env_path: PathBuf, + pub(crate) local_daemon_mode: bool, + pub(crate) env_lookup: EnvLookup, } fn nonzero_i64(value: i64) -> Option { @@ -568,26 +568,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, } } @@ -690,11 +690,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), @@ -748,8 +748,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), } } @@ -1223,20 +1223,20 @@ 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)), - features: Some(system_features(&settings)), + features: Some(system_features(&settings)), }; (StatusCode::OK, Json(response)).into_response() } @@ -1250,7 +1250,7 @@ fn system_features(settings: &SettingsLayer) -> SystemFeatures { .unwrap_or(false); SystemFeatures { session_sandboxes: Some(session_sandboxes), - retros: Some(retros), + retros: Some(retros), } } @@ -1329,12 +1329,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(); @@ -1349,12 +1349,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() @@ -1389,8 +1389,8 @@ async fn attach_events( } struct PrunePlan { - run_ids: Vec, - rows: Vec, + run_ids: Vec, + rows: Vec, total_size_bytes: u64, } @@ -1418,12 +1418,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()), }); } } @@ -1444,25 +1444,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), }) } @@ -1512,10 +1512,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 @@ -1763,8 +1763,8 @@ async fn delete_secret_by_name( #[derive(serde::Deserialize)] struct GitHubRepoResponse { default_branch: String, - private: bool, - permissions: Option, + private: bool, + permissions: Option, } async fn get_github_repo( @@ -2025,8 +2025,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 @@ -2045,15 +2045,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, }; @@ -2152,11 +2152,11 @@ async fn list_run_stages( if let Some(next_id) = &checkpoint.next_node_id { if run_is_active && next_id != "exit" && !checkpoint.completed_nodes.contains(next_id) { stages.push(RunStage { - id: next_id.clone(), - name: next_id.clone(), - status: ApiStageStatus::Running, + id: next_id.clone(), + name: next_id.clone(), + status: ApiStageStatus::Running, duration_secs: None, - dot_id: Some(next_id.clone()), + dot_id: Some(next_id.clone()), }); } } @@ -2187,16 +2187,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(); @@ -2236,7 +2236,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(), }, }); @@ -2247,8 +2247,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::>(); @@ -2533,22 +2533,24 @@ fn test_secret_store_path() -> PathBuf { fn board_column(status: WorkflowRunStatus) -> Option<&'static str> { match status { - WorkflowRunStatus::Submitted | WorkflowRunStatus::Starting => Some("initializing"), - WorkflowRunStatus::Running => Some("running"), - WorkflowRunStatus::Paused => Some("waiting"), - WorkflowRunStatus::Succeeded => Some("succeeded"), - WorkflowRunStatus::Failed | WorkflowRunStatus::Dead => Some("failed"), - WorkflowRunStatus::Removing => None, + WorkflowRunStatus::Running | WorkflowRunStatus::Paused => Some("working"), + WorkflowRunStatus::Blocked => Some("blocked"), + WorkflowRunStatus::Completed => Some("merge"), + WorkflowRunStatus::Submitted + | WorkflowRunStatus::Queued + | WorkflowRunStatus::Starting + | WorkflowRunStatus::Failed + | WorkflowRunStatus::Cancelled + | WorkflowRunStatus::Removing => None, } } fn board_columns() -> serde_json::Value { serde_json::json!([ - {"id": "initializing", "name": "Initializing"}, - {"id": "running", "name": "Running"}, - {"id": "waiting", "name": "Waiting"}, - {"id": "succeeded", "name": "Succeeded"}, - {"id": "failed", "name": "Failed"}, + {"id": "working", "name": "Working"}, + {"id": "blocked", "name": "Blocked"}, + {"id": "review", "name": "Review"}, + {"id": "merge", "name": "Merge"}, ]) } @@ -2918,7 +2920,7 @@ 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, } @@ -2944,6 +2946,7 @@ fn should_reconcile_run_on_startup(status: WorkflowRunStatus) -> bool { status, WorkflowRunStatus::Starting | WorkflowRunStatus::Running + | WorkflowRunStatus::Blocked | WorkflowRunStatus::Paused | WorkflowRunStatus::Removing ) @@ -3104,9 +3107,9 @@ async fn persist_cancelled_run_status(state: &AppState, run_id: RunId) -> anyhow &run_store, &run_id, &workflow_event::Event::WorkflowRunFailed { - error: WorkflowError::Cancelled, - duration_ms: 0, - reason: Some(WorkflowStatusReason::Cancelled), + error: WorkflowError::Cancelled, + duration_ms: 0, + reason: Some(WorkflowStatusReason::Cancelled), git_commit_sha: None, }, ) @@ -3163,18 +3166,18 @@ fn managed_run( fn api_status_from_workflow( status: WorkflowRunStatus, - reason: Option, + _reason: Option, ) -> RunStatus { match status { WorkflowRunStatus::Submitted => RunStatus::Submitted, + WorkflowRunStatus::Queued => RunStatus::Queued, WorkflowRunStatus::Starting => RunStatus::Starting, WorkflowRunStatus::Running | WorkflowRunStatus::Removing => RunStatus::Running, + WorkflowRunStatus::Blocked => RunStatus::Blocked, WorkflowRunStatus::Paused => RunStatus::Paused, - WorkflowRunStatus::Succeeded => RunStatus::Completed, - WorkflowRunStatus::Failed if reason == Some(WorkflowStatusReason::Cancelled) => { - RunStatus::Cancelled - } - WorkflowRunStatus::Failed | WorkflowRunStatus::Dead => RunStatus::Failed, + WorkflowRunStatus::Completed => RunStatus::Completed, + WorkflowRunStatus::Failed => RunStatus::Failed, + WorkflowRunStatus::Cancelled => RunStatus::Cancelled, } } @@ -3269,6 +3272,19 @@ fn update_live_run_from_event(state: &Arc, run_id: RunId, event: &RunE }; managed_run.error = Some(props.error.clone()); } + EventBody::InterviewStarted(props) if !props.question_id.is_empty() => { + managed_run.status = RunStatus::Blocked; + } + EventBody::InterviewCompleted(_) + | EventBody::InterviewTimeout(_) + | EventBody::InterviewInterrupted(_) => { + // Return to Running if no pending interviews remain. + // We check the durable projection lazily; for the live model, + // assume the run returns to Running on any interview resolution. + if managed_run.status == RunStatus::Blocked { + managed_run.status = RunStatus::Running; + } + } _ => {} } } @@ -3425,41 +3441,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(), } @@ -3616,10 +3632,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() { @@ -3704,6 +3723,7 @@ async fn create_run( error: None, queue_position: None, status_reason: None, + blocked_reason: None, pending_control: None, created_at, }), @@ -3713,12 +3733,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, }), } @@ -3922,13 +3942,14 @@ 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, + blocked_reason: None, pending_control: None, - created_at: id.created_at(), + created_at: id.created_at(), }), ) .into_response() @@ -4270,9 +4291,9 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { &run_store, &run_id, &workflow_event::Event::WorkflowRunFailed { - error: WorkflowError::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, }, ) @@ -4291,9 +4312,9 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { &run_store, &run_id, &workflow_event::Event::WorkflowRunFailed { - error: WorkflowError::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, }, ) @@ -4320,9 +4341,9 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { &run_store, &run_id, &workflow_event::Event::WorkflowRunFailed { - error: WorkflowError::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, }, ) @@ -4340,9 +4361,9 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { &run_store, &run_id, &workflow_event::Event::WorkflowRunFailed { - error: WorkflowError::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, }, ) @@ -4372,9 +4393,9 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { &run_store, &run_id, &workflow_event::Event::WorkflowRunFailed { - error: WorkflowError::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, }, ) @@ -4996,11 +5017,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(), }) @@ -5048,8 +5069,8 @@ enum ArtifactUploadContentType { } struct ValidatedArtifactBatchEntry { - path: String, - sha256: Option, + path: String, + sha256: Option, expected_bytes: Option, } @@ -5181,11 +5202,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!( @@ -5505,7 +5529,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(); @@ -5515,7 +5539,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(); @@ -5566,8 +5590,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(), }) @@ -5763,6 +5787,7 @@ async fn cancel_run( | RunStatus::Queued | RunStatus::Starting | RunStatus::Running + | RunStatus::Blocked | RunStatus::Paused => { let use_cancel_signal = !matches!( managed_run.answer_transport, @@ -5848,6 +5873,7 @@ async fn cancel_run( error: None, queue_position: None, status_reason, + blocked_reason: None, pending_control, created_at, }), @@ -5916,6 +5942,7 @@ async fn pause_run( error: None, queue_position: None, status_reason, + blocked_reason: None, pending_control, created_at, }), @@ -5984,6 +6011,7 @@ async fn unpause_run( error: None, queue_position: None, status_reason, + blocked_reason: None, pending_control, created_at, }), @@ -6192,9 +6220,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(), ) @@ -6315,15 +6343,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}")) @@ -6332,15 +6360,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}")) @@ -6662,10 +6690,10 @@ type = "http" let vault = state.vault.read().await; assert!(!vault.snapshot().contains_key("/tmp/test.pem")); - assert_eq!(vault.file_secrets(), vec![( - "/tmp/test.pem".to_string(), - "pem-data".to_string() - )]); + assert_eq!( + vault.file_secrets(), + vec![("/tmp/test.pem".to_string(), "pem-data".to_string())] + ); } #[tokio::test] @@ -6674,21 +6702,21 @@ type = "http" let app = build_router(Arc::clone(&state), AuthMode::Disabled); let credential = fabro_auth::AuthCredential { provider: Provider::OpenAi, - details: fabro_auth::AuthDetails::CodexOAuth { - tokens: fabro_auth::OAuthTokens { - access_token: "access".to_string(), + details: fabro_auth::AuthDetails::CodexOAuth { + tokens: fabro_auth::OAuthTokens { + access_token: "access".to_string(), refresh_token: Some("refresh".to_string()), - expires_at: chrono::DateTime::parse_from_rfc3339("2030-01-01T00:00:00Z") + expires_at: chrono::DateTime::parse_from_rfc3339("2030-01-01T00:00:00Z") .unwrap() .with_timezone(&chrono::Utc), }, - config: fabro_auth::OAuthConfig { - auth_url: "https://auth.openai.com".to_string(), - token_url: "https://auth.openai.com/oauth/token".to_string(), - client_id: "client".to_string(), - scopes: vec!["openid".to_string()], + config: fabro_auth::OAuthConfig { + auth_url: "https://auth.openai.com".to_string(), + token_url: "https://auth.openai.com/oauth/token".to_string(), + client_id: "client".to_string(), + scopes: vec!["openid".to_string()], redirect_uri: Some("https://auth.openai.com/deviceauth/callback".to_string()), - use_pkce: true, + use_pkce: true, }, account_id: Some("acct_123".to_string()), }, @@ -7117,11 +7145,14 @@ type = "http" .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] @@ -7162,7 +7193,7 @@ slug = "fabro" false, ), AuthMode::Enabled(ConfiguredAuth { - methods: vec![ServerAuthMethod::Github], + methods: vec![ServerAuthMethod::Github], dev_token: None, }), ); @@ -7363,18 +7394,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, }, @@ -7490,7 +7521,7 @@ slug = "fabro" let app = build_router( Arc::clone(&state), AuthMode::Enabled(ConfiguredAuth { - methods: vec![ServerAuthMethod::DevToken], + methods: vec![ServerAuthMethod::DevToken], dev_token: Some(DEV_TOKEN.to_string()), }), ); @@ -7866,10 +7897,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 { @@ -8525,10 +8560,10 @@ level = "debug" let response = app.clone().oneshot(req).await.unwrap(); let body = body_json(response.into_body()).await; - assert_eq!(body["status"].as_str().unwrap(), "failed"); + assert_eq!(body["status"].as_str().unwrap(), "cancelled"); assert_eq!(body["status_reason"].as_str().unwrap(), "cancelled"); - // Cancelled runs appear on the board in the "failed" column + // Cancelled runs are off-board (not mapped to any column) let req = Request::builder() .method("GET") .uri(api("/boards/runs")) @@ -8543,18 +8578,13 @@ level = "debug" .iter() .find(|item| item["id"].as_str() == Some(run_id_str.as_str())); assert!( - board_item.is_some(), - "cancelled run should appear on the board" - ); - assert_eq!( - board_item.unwrap()["status"].as_str(), - Some("failed"), - "cancelled run should be in the failed column" + board_item.is_none(), + "cancelled run should not appear on the board" ); let run_store = state.store.open_run_reader(&run_id).await.unwrap(); let status = run_store.state().await.unwrap().status.unwrap(); - assert_eq!(status.status, WorkflowRunStatus::Failed); + assert_eq!(status.status, WorkflowRunStatus::Cancelled); assert_eq!(status.reason, Some(WorkflowStatusReason::Cancelled)); } @@ -8653,8 +8683,7 @@ level = "debug" let body = body_json(response.into_body()).await; assert_eq!(body["pending_control"].as_str(), Some("pause")); - // Verify the run appears on the board (store has Submitted status → - // "initializing" column) + // Submitted status is off-board (no board column for Submitted/Queued/Starting) let req = Request::builder() .method("GET") .uri(api("/boards/runs")) @@ -8666,9 +8695,8 @@ level = "debug" .as_array() .unwrap() .iter() - .find(|item| item["id"].as_str() == Some(run_id_str.as_str())) - .expect("board item should exist"); - assert_eq!(item["status"].as_str(), Some("initializing")); + .find(|item| item["id"].as_str() == Some(run_id_str.as_str())); + assert!(item.is_none(), "submitted run should not be on the board"); } #[tokio::test] @@ -8704,32 +8732,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(); @@ -8766,7 +8804,7 @@ level = "debug" .await .unwrap(); let run_3_status = run_3.status.unwrap(); - assert_eq!(run_3_status.status, WorkflowRunStatus::Failed); + assert_eq!(run_3_status.status, WorkflowRunStatus::Cancelled); assert_eq!(run_3_status.reason, Some(WorkflowStatusReason::Cancelled)); assert_eq!(run_3.pending_control, None); } @@ -8777,14 +8815,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(); @@ -8887,7 +8929,7 @@ timeout = "30s" let mut status_record = None; for _ in 0..50 { if let Some(record) = run_store.state().await.unwrap().status { - if record.status == WorkflowRunStatus::Failed + if record.status == WorkflowRunStatus::Cancelled && record.reason == Some(WorkflowStatusReason::Cancelled) { status_record = Some(record); @@ -8898,7 +8940,7 @@ timeout = "30s" } let status_record = status_record.expect("status record should be persisted"); - assert_eq!(status_record.status, WorkflowRunStatus::Failed); + assert_eq!(status_record.status, WorkflowRunStatus::Cancelled); assert_eq!(status_record.reason, Some(WorkflowStatusReason::Cancelled)); } @@ -9108,15 +9150,22 @@ timeout = "30s" async fn boards_runs_returns_run_list_items_with_board_columns() { let state = create_app_state(); let app = build_router(Arc::clone(&state), AuthMode::Disabled); - let run_id = create_and_start_run(&app, MINIMAL_DOT).await; + let run_id = fixtures::RUN_1; - // Set run to running so it appears on the board - { - let id = run_id.parse::().unwrap(); - let mut runs = state.runs.lock().expect("runs lock poisoned"); - let managed_run = runs.get_mut(&id).expect("run should exist"); - managed_run.status = RunStatus::Running; - } + // Create a durable run in Running status so it appears on the board + 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 req = Request::builder() .method("GET") @@ -9129,7 +9178,7 @@ timeout = "30s" let data = body["data"].as_array().expect("data should be array"); let item = data .iter() - .find(|i| i["id"].as_str() == Some(&run_id)) + .find(|i| i["id"].as_str() == Some(&run_id.to_string())) .expect("run should be in board"); // Should have RunListItem fields assert!(item["title"].is_string()); @@ -9138,7 +9187,7 @@ timeout = "30s" // Status should be a board column, not a lifecycle status let status = item["status"].as_str().unwrap(); assert!( - ["working", "initializing", "review", "merge"].contains(&status), + ["working", "blocked", "review", "merge"].contains(&status), "status should be a board column, got: {status}" ); assert!(item["created_at"].is_string()); @@ -9151,15 +9200,19 @@ timeout = "30s" let run_id = fixtures::RUN_1; // A run in Removing status should not appear on the board - 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 }, - workflow_event::Event::RunRemoving { 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 }, + workflow_event::Event::RunRemoving { reason: None }, + ], + ) .await; let req = Request::builder() @@ -9185,34 +9238,42 @@ timeout = "30s" let paused_id = fixtures::RUN_1; let succeeded_id = fixtures::RUN_2; - create_durable_run_with_events(&state, paused_id, &[ - 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, - ]) + create_durable_run_with_events( + &state, + paused_id, + &[ + 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, + ], + ) .await; - create_durable_run_with_events(&state, succeeded_id, &[ - workflow_event::Event::RunSubmitted { - reason: None, - definition_blob: None, - }, - workflow_event::Event::RunStarting { reason: None }, - workflow_event::Event::RunRunning { reason: None }, - workflow_event::Event::WorkflowRunCompleted { - duration_ms: 1000, - artifact_count: 0, - status: "success".to_string(), - reason: None, - total_usd_micros: None, - final_git_commit_sha: None, - final_patch: None, - billing: None, - }, - ]) + create_durable_run_with_events( + &state, + succeeded_id, + &[ + workflow_event::Event::RunSubmitted { + reason: None, + definition_blob: None, + }, + workflow_event::Event::RunStarting { reason: None }, + workflow_event::Event::RunRunning { reason: None }, + workflow_event::Event::WorkflowRunCompleted { + duration_ms: 1000, + artifact_count: 0, + status: "success".to_string(), + reason: None, + total_usd_micros: None, + final_git_commit_sha: None, + final_patch: None, + billing: None, + }, + ], + ) .await; let req = Request::builder() @@ -9228,22 +9289,18 @@ timeout = "30s" .iter() .find(|i| i["id"].as_str() == Some(&paused_id.to_string())) .expect("paused run should be on board"); - assert_eq!(paused_item["status"].as_str().unwrap(), "waiting"); + assert_eq!(paused_item["status"].as_str().unwrap(), "working"); - let succeeded_item = data + let completed_item = data .iter() .find(|i| i["id"].as_str() == Some(&succeeded_id.to_string())) - .expect("succeeded run should be on board"); - assert_eq!(succeeded_item["status"].as_str().unwrap(), "succeeded"); + .expect("completed run should be on board"); + assert_eq!(completed_item["status"].as_str().unwrap(), "merge"); // Verify columns are included in the response let columns = body["columns"].as_array().expect("columns should be array"); assert!(columns.len() > 0); - assert!(columns.iter().any(|c| c["id"].as_str() == Some("waiting"))); - assert!( - columns - .iter() - .any(|c| c["id"].as_str() == Some("succeeded")) - ); + assert!(columns.iter().any(|c| c["id"].as_str() == Some("working"))); + assert!(columns.iter().any(|c| c["id"].as_str() == Some("merge"))); } } diff --git a/lib/crates/fabro-server/src/server_secrets.rs b/lib/crates/fabro-server/src/server_secrets.rs index d296f0f0b..79c05bb5e 100644 --- a/lib/crates/fabro-server/src/server_secrets.rs +++ b/lib/crates/fabro-server/src/server_secrets.rs @@ -18,9 +18,9 @@ pub(crate) enum Error { } pub(crate) struct ServerSecrets { - path: PathBuf, + path: PathBuf, file_entries: HashMap, - env_lookup: EnvLookup, + env_lookup: EnvLookup, } impl ServerSecrets { @@ -58,7 +58,7 @@ impl std::fmt::Debug for ServerSecrets { #[derive(Clone)] pub(crate) struct ProviderCredentials { - vault: Arc>, + vault: Arc>, env_lookup: EnvLookup, } @@ -119,7 +119,7 @@ impl ProviderCredentials { } pub(crate) struct LlmClientResult { - pub client: LlmClient, + pub client: LlmClient, pub auth_issues: Vec<(Provider, ResolveError)>, } @@ -168,9 +168,10 @@ mod tests { (name == "OPENAI_API_KEY").then(|| "openai-key".to_string()) }); - assert_eq!(credentials.configured_providers().await, vec![ - Provider::OpenAi - ]); + assert_eq!( + credentials.configured_providers().await, + vec![Provider::OpenAi] + ); } #[tokio::test] @@ -182,7 +183,7 @@ mod tests { "anthropic", &serde_json::to_string(&AuthCredential { provider: Provider::Anthropic, - details: AuthDetails::ApiKey { + details: AuthDetails::ApiKey { key: "anthropic-key".to_string(), }, }) @@ -194,8 +195,9 @@ mod tests { let credentials = ProviderCredentials::with_env_lookup(Arc::new(AsyncRwLock::new(vault)), |_| None); - assert_eq!(credentials.configured_providers().await, vec![ - Provider::Anthropic - ]); + assert_eq!( + credentials.configured_providers().await, + vec![Provider::Anthropic] + ); } } diff --git a/lib/crates/fabro-server/src/web_auth.rs b/lib/crates/fabro-server/src/web_auth.rs index ed999c482..8601a11c4 100644 --- a/lib/crates/fabro-server/src/web_auth.rs +++ b/lib/crates/fabro-server/src/web_auth.rs @@ -22,21 +22,21 @@ const OAUTH_STATE_COOKIE_NAME: &str = "fabro_oauth_state"; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SessionCookie { - pub v: u8, - pub login: String, + pub v: u8, + pub login: String, pub auth_method: RunAuthMethod, pub provider_id: Option, - pub name: String, - pub email: String, - pub avatar_url: String, - pub user_url: String, - pub iat: i64, - pub exp: i64, + pub name: String, + pub email: String, + pub avatar_url: String, + pub user_url: String, + pub iat: i64, + pub exp: i64, } #[derive(Deserialize)] struct OAuthCallbackParams { - code: String, + code: String, state: String, } @@ -57,21 +57,21 @@ struct AuthConfigResponse { #[derive(Serialize)] struct AuthMeResponse { - user: SessionUser, - provider: String, + user: SessionUser, + provider: String, #[serde(rename = "demoMode")] demo_mode: bool, } #[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)] @@ -81,16 +81,16 @@ 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, } @@ -245,16 +245,16 @@ async fn login_dev_token( let now = chrono::Utc::now(); let session = SessionCookie { - v: 1, - login: "dev".to_string(), + v: 1, + login: "dev".to_string(), auth_method: RunAuthMethod::DevToken, provider_id: None, - name: "Development User".to_string(), - email: "dev@localhost".to_string(), - avatar_url: "/logo.svg".to_string(), - user_url: String::new(), - iat: now.timestamp(), - exp: (now + chrono::Duration::days(30)).timestamp(), + name: "Development User".to_string(), + email: "dev@localhost".to_string(), + avatar_url: "/logo.svg".to_string(), + user_url: String::new(), + iat: now.timestamp(), + exp: (now + chrono::Duration::days(30)).timestamp(), }; let mut jar = CookieJar::new(); @@ -334,14 +334,16 @@ async fn login_github( } let state_token = format!("fabro-{}", ulid::Ulid::new()); - let authorize_url = - fabro_http::Url::parse_with_params("https://github.com/login/oauth/authorize", &[ + let authorize_url = fabro_http::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"); @@ -533,16 +535,16 @@ async fn callback_github( .unwrap_or_default(); let now = chrono::Utc::now(); let session = SessionCookie { - v: 1, - login: profile.login.clone(), + v: 1, + login: profile.login.clone(), auth_method: RunAuthMethod::Github, provider_id: Some(profile.id), - 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), - iat: now.timestamp(), - exp: (now + chrono::Duration::days(30)).timestamp(), + 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), + iat: now.timestamp(), + exp: (now + chrono::Duration::days(30)).timestamp(), }; info!(login = %session.login, "OAuth login succeeded"); @@ -609,11 +611,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: session_provider(session.auth_method).to_string(), demo_mode, @@ -662,14 +664,14 @@ mod tests { fn dev_token_auth_mode() -> AuthMode { AuthMode::Enabled(ConfiguredAuth { - methods: vec![ServerAuthMethod::DevToken], + methods: vec![ServerAuthMethod::DevToken], dev_token: Some(DEV_TOKEN.to_string()), }) } fn github_auth_mode() -> AuthMode { AuthMode::Enabled(ConfiguredAuth { - methods: vec![ServerAuthMethod::Github], + methods: vec![ServerAuthMethod::Github], dev_token: None, }) } @@ -679,11 +681,11 @@ mod tests { server: Some(ServerLayer { web: Some(ServerWebLayer { enabled: Some(true), - url: Some(web_url.into()), + url: Some(web_url.into()), }), auth: Some(ServerAuthLayer { methods: Some(vec![ServerAuthMethod::Github]), - github: Some(ServerAuthGithubLayer { + github: Some(ServerAuthGithubLayer { allowed_usernames: vec!["octocat".to_string()], }), }), diff --git a/lib/crates/fabro-server/tests/it/api/system.rs b/lib/crates/fabro-server/tests/it/api/system.rs index 5fc8008e2..83896ec7a 100644 --- a/lib/crates/fabro-server/tests/it/api/system.rs +++ b/lib/crates/fabro-server/tests/it/api/system.rs @@ -157,8 +157,8 @@ async fn get_system_disk_usage_returns_summary_and_verbose_rows() { let run_id = create_run(&app, minimal_manifest_json_with_dry_run(MINIMAL_DOT)).await; start_run(&app, &run_id).await; - let status = wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await; - assert_eq!(status, "succeeded"); + let status = wait_for_run_status(&app, &run_id, &["completed", "failed"]).await; + assert_eq!(status, "completed"); let logs_dir = storage_dir.join("logs"); std::fs::create_dir_all(&logs_dir).unwrap(); @@ -189,8 +189,8 @@ async fn prune_runs_supports_dry_run_and_deletion() { let run_id = create_run(&app, minimal_manifest_json_with_dry_run(MINIMAL_DOT)).await; start_run(&app, &run_id).await; - let status = wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await; - assert_eq!(status, "succeeded"); + let status = wait_for_run_status(&app, &run_id, &["completed", "failed"]).await; + assert_eq!(status, "completed"); let run_id_parsed: RunId = run_id.parse().unwrap(); let run_dir = Storage::new(&storage_dir) diff --git a/lib/crates/fabro-server/tests/it/api/tls.rs b/lib/crates/fabro-server/tests/it/api/tls.rs index a666b548d..d473ed146 100644 --- a/lib/crates/fabro-server/tests/it/api/tls.rs +++ b/lib/crates/fabro-server/tests/it/api/tls.rs @@ -15,16 +15,16 @@ fn fixture_path(name: &str) -> PathBuf { } struct PkiPaths { - ca_cert: PathBuf, + ca_cert: PathBuf, server_cert: PathBuf, - server_key: PathBuf, + server_key: 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"), } } @@ -64,7 +64,7 @@ fn install_crypto_provider() { fn tls_settings(pki: &PkiPaths) -> TlsConfig { TlsConfig { cert: InterpString::parse(&pki.server_cert.to_string_lossy()), - key: InterpString::parse(&pki.server_key.to_string_lossy()), + key: InterpString::parse(&pki.server_key.to_string_lossy()), } } @@ -90,7 +90,7 @@ async fn tls_dev_token_auth_does_not_require_client_cert() { let pki = fixture_pki(); let dev_token = "fabro_dev_abababababababababababababababababababababababababababababababab"; let auth_mode = AuthMode::Enabled(ConfiguredAuth { - methods: vec![ServerAuthMethod::DevToken], + methods: vec![ServerAuthMethod::DevToken], dev_token: Some(dev_token.to_string()), }); let addr = start_tls_server(&tls_settings(&pki), auth_mode).await; diff --git a/lib/crates/fabro-server/tests/it/scenario/dry_run.rs b/lib/crates/fabro-server/tests/it/scenario/dry_run.rs index b08ced4a6..7b40007bd 100644 --- a/lib/crates/fabro-server/tests/it/scenario/dry_run.rs +++ b/lib/crates/fabro-server/tests/it/scenario/dry_run.rs @@ -15,7 +15,7 @@ use crate::helpers::{ static ENV_LOCK: LazyLock> = LazyLock::new(|| Mutex::new(())); struct ProxyPolicyGuard { - _lock: MutexGuard<'static, ()>, + _lock: MutexGuard<'static, ()>, previous: Option, } @@ -80,8 +80,8 @@ async fn dry_run_serve_starts_and_runs_workflow() { create_and_start_run_from_manifest(&app, minimal_manifest_json_with_dry_run(MINIMAL_DOT)) .await; - let status = wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await; - assert_eq!(status, "succeeded"); + let status = wait_for_run_status(&app, &run_id, &["completed", "failed"]).await; + assert_eq!(status, "completed"); } #[tokio::test] diff --git a/lib/crates/fabro-server/tests/it/scenario/lifecycle.rs b/lib/crates/fabro-server/tests/it/scenario/lifecycle.rs index 3bba3c4ec..440971673 100644 --- a/lib/crates/fabro-server/tests/it/scenario/lifecycle.rs +++ b/lib/crates/fabro-server/tests/it/scenario/lifecycle.rs @@ -155,8 +155,8 @@ async fn full_http_lifecycle_approve_and_complete() { assert_eq!(response.status(), StatusCode::NO_CONTENT); // 4. Poll until the run reaches a terminal success or failure state. - let final_status = wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await; - assert_eq!(final_status, "succeeded"); + let final_status = wait_for_run_status(&app, &run_id, &["completed", "failed"]).await; + assert_eq!(final_status, "completed"); // 5. Verify no pending questions let req = Request::builder() @@ -214,8 +214,8 @@ async fn full_http_lifecycle_cancel() { assert_eq!(body["status"], "running"); assert_eq!(body["pending_control"], "cancel"); - // Verify the durable store view converges to cancelled failure. - let body = wait_for_run_state(&app, &run_id, "failed", "cancelled").await; + // Verify the durable store view converges to cancelled status. + let body = wait_for_run_state(&app, &run_id, "cancelled", "cancelled").await; assert_eq!(body["status_reason"], "cancelled"); } @@ -254,8 +254,8 @@ async fn cancel_at_human_gate_persists_cancelled_terminal_event() { let response = app.clone().oneshot(req).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); - let status = wait_for_run_status(&app, &run_id, &["failed"]).await; - assert_eq!(status, "failed"); + let status = wait_for_run_status(&app, &run_id, &["cancelled", "failed"]).await; + assert_eq!(status, "cancelled"); let req = Request::builder() .method("GET") @@ -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-server/tests/it/scenario/run_completion.rs b/lib/crates/fabro-server/tests/it/scenario/run_completion.rs index 36aa38b98..c535502ff 100644 --- a/lib/crates/fabro-server/tests/it/scenario/run_completion.rs +++ b/lib/crates/fabro-server/tests/it/scenario/run_completion.rs @@ -17,8 +17,8 @@ async fn run_completes_and_status_is_completed() { create_and_start_run_from_manifest(&app, minimal_manifest_json_with_dry_run(MINIMAL_DOT)) .await; - let status = wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await; - assert_eq!(status, "succeeded"); + let status = wait_for_run_status(&app, &run_id, &["completed", "failed"]).await; + assert_eq!(status, "completed"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -61,8 +61,8 @@ async fn attach_run_events_replays_terminal_event_after_completion() { let run_id = create_and_start_run_from_manifest(&app, minimal_manifest_json_with_dry_run(MINIMAL_DOT)) .await; - let status = wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await; - assert_eq!(status, "succeeded"); + let status = wait_for_run_status(&app, &run_id, &["completed", "failed"]).await; + assert_eq!(status, "completed"); let req = Request::builder() .method("GET") diff --git a/lib/crates/fabro-server/tests/it/scenario/usage.rs b/lib/crates/fabro-server/tests/it/scenario/usage.rs index 93a26beaa..0303276f6 100644 --- a/lib/crates/fabro-server/tests/it/scenario/usage.rs +++ b/lib/crates/fabro-server/tests/it/scenario/usage.rs @@ -19,8 +19,8 @@ async fn aggregate_billing_increments_after_run_completes() { .await; // Poll until run completes - let status = wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await; - assert_eq!(status, "succeeded"); + let status = wait_for_run_status(&app, &run_id, &["completed", "failed"]).await; + assert_eq!(status, "completed"); let mut total_runs = 0; for _ in 0..POLL_ATTEMPTS { 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 b3b38b493..5bb2d700a 100644 --- a/lib/crates/fabro-slack/src/client.rs +++ b/lib/crates/fabro-slack/src/client.rs @@ -6,14 +6,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: fabro_http::HttpClient, + api_base: String, + http: fabro_http::HttpClient, } impl SlackClient { @@ -109,7 +109,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 3f3e72afb..4cb4647ee 100644 --- a/lib/crates/fabro-store/src/artifact_store.rs +++ b/lib/crates/fabro-store/src/artifact_store.rs @@ -18,15 +18,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 { @@ -332,16 +332,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/keys.rs b/lib/crates/fabro-store/src/keys.rs index 9a7981c0f..98c709606 100644 --- a/lib/crates/fabro-store/src/keys.rs +++ b/lib/crates/fabro-store/src/keys.rs @@ -141,12 +141,10 @@ mod tests { let run_id: RunId = "01JT56VE4Z5NZ814GZN2JZD65A".parse().unwrap(); let key = run_event_key(&run_id, 7, 123); let segments: Vec<&str> = SlateKey::segments(key.as_str()).collect(); - assert_eq!(segments, [ - "runs", - "01JT56VE4Z5NZ814GZN2JZD65A", - "events", - "000007-123" - ]); + assert_eq!( + segments, + ["runs", "01JT56VE4Z5NZ814GZN2JZD65A", "events", "000007-123"] + ); } #[test] @@ -154,13 +152,16 @@ mod tests { let run_id: RunId = "01JT56VE4Z5NZ814GZN2JZD65A".parse().unwrap(); let key = runs_index_by_start_key(&run_id); let segments: Vec<&str> = SlateKey::segments(key.as_str()).collect(); - assert_eq!(segments, [ - "runs", - "_index", - "by-start", - &run_id.created_at().format("%Y-%m-%d").to_string(), - &run_id.to_string(), - ]); + assert_eq!( + segments, + [ + "runs", + "_index", + "by-start", + &run_id.created_at().format("%Y-%m-%d").to_string(), + &run_id.to_string(), + ] + ); } #[test] diff --git a/lib/crates/fabro-store/src/lib.rs b/lib/crates/fabro-store/src/lib.rs index 0cff9c5db..f3ec04e67 100644 --- a/lib/crates/fabro-store/src/lib.rs +++ b/lib/crates/fabro-store/src/lib.rs @@ -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 cb0be518d..5199972fd 100644 --- a/lib/crates/fabro-store/src/run_state.rs +++ b/lib/crates/fabro-store/src/run_state.rs @@ -8,10 +8,10 @@ use fabro_types::run_event::{ RunFailedProps, StageCompletedProps, StagePromptProps, }; use fabro_types::{ - BilledModelUsage, Checkpoint, Conclusion, EventBody, FailureSignature, InterviewQuestionRecord, - InterviewQuestionType, NodeStatusRecord, Outcome, PullRequestRecord, Retro, RunControlAction, - RunEvent, RunId, RunRecord, RunStatus, RunStatusRecord, SandboxRecord, StageStatus, - StartRecord, StatusReason, + BilledModelUsage, BlockedReason, Checkpoint, Conclusion, EventBody, FailureSignature, + InterviewQuestionRecord, InterviewQuestionType, NodeStatusRecord, Outcome, PullRequestRecord, + Retro, RunControlAction, RunEvent, RunId, RunRecord, RunStatus, RunStatusRecord, SandboxRecord, + StageStatus, StartRecord, StatusReason, }; use serde_json::Value; @@ -20,48 +20,48 @@ 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 { @@ -140,14 +140,19 @@ impl RunProjection { self.pending_control = None; } EventBody::RunCompleted(props) => { - self.status = Some(run_status_record(RunStatus::Succeeded, props.reason, ts)); + self.status = Some(run_status_record(RunStatus::Completed, props.reason, ts)); self.pending_control = None; self.conclusion = Some(conclusion_from_completed(props, ts)?); self.final_patch.clone_from(&props.final_patch); self.pending_interviews.clear(); } EventBody::RunFailed(props) => { - self.status = Some(run_status_record(RunStatus::Failed, props.reason, ts)); + let projected_status = if props.reason == Some(StatusReason::Cancelled) { + RunStatus::Cancelled + } else { + RunStatus::Failed + }; + self.status = Some(run_status_record(projected_status, props.reason, ts)); self.pending_control = None; self.conclusion = Some(conclusion_from_failed(props, ts)); self.pending_interviews.clear(); @@ -172,11 +177,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) => { @@ -193,50 +198,64 @@ impl RunProjection { } 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), - }); + }, + ); + let mut record = run_status_record(RunStatus::Blocked, None, ts); + record.blocked_reason = Some(BlockedReason::HumanInputRequired); + self.status = Some(record); } EventBody::InterviewCompleted(props) => { if !props.question_id.is_empty() { self.pending_interviews.remove(&props.question_id); } + if self.pending_interviews.is_empty() { + self.status = Some(run_status_record(RunStatus::Running, None, ts)); + } } EventBody::InterviewTimeout(props) => { if !props.question_id.is_empty() { self.pending_interviews.remove(&props.question_id); } + if self.pending_interviews.is_empty() { + self.status = Some(run_status_record(RunStatus::Running, None, ts)); + } } EventBody::InterviewInterrupted(props) => { if !props.question_id.is_empty() { self.pending_interviews.remove(&props.question_id); } + if self.pending_interviews.is_empty() { + self.status = Some(run_status_record(RunStatus::Running, None, ts)); + } } EventBody::StagePrompt(props) => { let Some(node_id) = stored.node_id.as_deref() else { @@ -385,6 +404,10 @@ impl RunProjection { start_time: self.start.as_ref().map(|start| start.start_time), status: self.status.as_ref().map(|status| status.status), status_reason: self.status.as_ref().and_then(|status| status.reason), + blocked_reason: self + .status + .as_ref() + .and_then(|status| status.blocked_reason), pending_control: self.pending_control, duration_ms: self .conclusion @@ -435,6 +458,7 @@ fn run_status_record( RunStatusRecord { status, reason, + blocked_reason: None, updated_at, } } @@ -511,21 +535,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), } } @@ -682,25 +706,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(); @@ -727,21 +757,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()), }), @@ -772,8 +802,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"), @@ -793,7 +823,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", @@ -819,7 +849,7 @@ mod tests { .unwrap(), }, EventEnvelope { - seq: 2, + seq: 2, payload: EventPayload::new( json!({ "id": "evt-run-submitted", @@ -848,4 +878,205 @@ mod tests { events[1].payload.as_value()["properties"]["definition_blob"] ); } + + #[test] + fn interview_started_sets_blocked_with_human_input_reason() { + use fabro_types::run_event::{RunStatusTransitionProps, RunSubmittedProps}; + use fabro_types::{BlockedReason, RunStatus}; + + let mut state = RunProjection::default(); + state + .apply_event(&test_event( + 1, + EventBody::RunSubmitted(RunSubmittedProps { + reason: None, + definition_blob: None, + }), + None, + )) + .unwrap(); + state + .apply_event(&test_event( + 2, + EventBody::RunRunning(RunStatusTransitionProps { reason: None }), + None, + )) + .unwrap(); + state + .apply_event(&test_event( + 3, + 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![], + allow_freeform: false, + timeout_seconds: None, + context_display: None, + }), + Some("gate"), + )) + .unwrap(); + + let status = state.status.as_ref().expect("status should be set"); + assert_eq!(status.status, RunStatus::Blocked); + assert_eq!( + status.blocked_reason, + Some(BlockedReason::HumanInputRequired) + ); + } + + #[test] + fn interview_completion_returns_to_running_when_no_pending() { + use fabro_types::RunStatus; + use fabro_types::run_event::{RunStatusTransitionProps, RunSubmittedProps}; + + let mut state = RunProjection::default(); + state + .apply_event(&test_event( + 1, + EventBody::RunSubmitted(RunSubmittedProps { + reason: None, + definition_blob: None, + }), + None, + )) + .unwrap(); + state + .apply_event(&test_event( + 2, + EventBody::RunRunning(RunStatusTransitionProps { reason: None }), + None, + )) + .unwrap(); + state + .apply_event(&test_event( + 3, + EventBody::InterviewStarted(InterviewStartedProps { + question_id: "q-1".to_string(), + question: "Approve?".to_string(), + stage: "gate".to_string(), + question_type: "freeform".to_string(), + options: vec![], + allow_freeform: true, + timeout_seconds: None, + context_display: None, + }), + Some("gate"), + )) + .unwrap(); + assert_eq!(state.status.as_ref().unwrap().status, RunStatus::Blocked); + + state + .apply_event(&test_event( + 4, + EventBody::InterviewCompleted(InterviewCompletedProps { + question_id: "q-1".to_string(), + question: "Approve?".to_string(), + answer: "yes".to_string(), + duration_ms: 100, + }), + Some("gate"), + )) + .unwrap(); + + assert!(state.pending_interviews.is_empty()); + assert_eq!(state.status.as_ref().unwrap().status, RunStatus::Running); + assert_eq!(state.status.as_ref().unwrap().blocked_reason, None); + } + + #[test] + fn pause_unpause_never_routes_through_blocked() { + use fabro_types::RunStatus; + use fabro_types::run_event::{RunStatusTransitionProps, RunSubmittedProps}; + + let mut state = RunProjection::default(); + state + .apply_event(&test_event( + 1, + EventBody::RunSubmitted(RunSubmittedProps { + reason: None, + definition_blob: None, + }), + None, + )) + .unwrap(); + state + .apply_event(&test_event( + 2, + EventBody::RunRunning(RunStatusTransitionProps { reason: None }), + None, + )) + .unwrap(); + state + .apply_event(&test_event( + 3, + EventBody::RunPaused(Default::default()), + None, + )) + .unwrap(); + assert_eq!(state.status.as_ref().unwrap().status, RunStatus::Paused); + + state + .apply_event(&test_event( + 4, + EventBody::RunUnpaused(Default::default()), + None, + )) + .unwrap(); + assert_eq!(state.status.as_ref().unwrap().status, RunStatus::Running); + } + + #[test] + fn cancelled_failure_projects_to_cancelled_status() { + use fabro_types::run_event::{RunFailedProps, RunStatusTransitionProps, RunSubmittedProps}; + use fabro_types::{RunStatus, StatusReason}; + + let mut state = RunProjection::default(); + state + .apply_event(&test_event( + 1, + EventBody::RunSubmitted(RunSubmittedProps { + reason: None, + definition_blob: None, + }), + None, + )) + .unwrap(); + state + .apply_event(&test_event( + 2, + EventBody::RunRunning(RunStatusTransitionProps { reason: None }), + None, + )) + .unwrap(); + state + .apply_event(&test_event( + 3, + EventBody::RunFailed(RunFailedProps { + error: "cancelled by user".to_string(), + duration_ms: 100, + reason: Some(StatusReason::Cancelled), + git_commit_sha: None, + }), + None, + )) + .unwrap(); + + let status = state.status.as_ref().expect("status should be set"); + assert_eq!(status.status, RunStatus::Cancelled); + assert_eq!(status.reason, Some(StatusReason::Cancelled)); + } + + #[test] + fn queued_status_round_trips_through_serialization() { + use fabro_types::RunStatus; + + let record = fabro_types::RunStatusRecord::new(RunStatus::Queued, None); + let json = serde_json::to_string(&record).unwrap(); + assert!(json.contains("\"queued\"")); + let round_tripped: fabro_types::RunStatusRecord = serde_json::from_str(&json).unwrap(); + assert_eq!(round_tripped.status, RunStatus::Queued); + } } diff --git a/lib/crates/fabro-store/src/slate/mod.rs b/lib/crates/fabro-store/src/slate/mod.rs index d6801631e..5ee4a7fb4 100644 --- a/lib/crates/fabro-store/src/slate/mod.rs +++ b/lib/crates/fabro-store/src/slate/mod.rs @@ -17,12 +17,12 @@ use crate::{Error, ListRunsQuery, Result, RunSummary, keys}; #[derive(Clone)] pub struct Database { - object_store: Arc, - base_prefix: String, + object_store: Arc, + base_prefix: String, flush_interval: Duration, - cache_path: Option, - db: Arc>, - active_runs: Arc>>>, + cache_path: Option, + db: Arc>, + active_runs: Arc>>>, } impl std::fmt::Debug for Database { @@ -402,7 +402,7 @@ mod tests { assert_eq!(summary[1].run_id, test_run_id("run-1")); assert_eq!(summary[1].workflow_name, Some("night-sky".to_string())); assert_eq!(summary[1].goal, Some("map the constellations".to_string())); - assert_eq!(summary[1].status, Some(RunStatus::Succeeded)); + assert_eq!(summary[1].status, Some(RunStatus::Completed)); assert_eq!(summary[1].status_reason, Some(StatusReason::Completed)); let reopened = store.open_run(&test_run_id("run-1")).await.unwrap(); @@ -536,7 +536,7 @@ mod tests { let summary = store.list_runs(&ListRunsQuery::default()).await.unwrap(); assert_eq!(summary.len(), 1); - assert_eq!(summary[0].status, Some(RunStatus::Failed)); + assert_eq!(summary[0].status, Some(RunStatus::Cancelled)); assert_eq!(summary[0].status_reason, Some(StatusReason::Cancelled)); assert_eq!(summary[0].pending_control, None); } @@ -581,6 +581,6 @@ mod tests { let summary = reopened.list_runs(&ListRunsQuery::default()).await.unwrap(); assert_eq!(summary.len(), 1); assert_eq!(summary[0].run_id, test_run_id("run-1")); - assert_eq!(summary[0].status, Some(RunStatus::Succeeded)); + assert_eq!(summary[0].status, Some(RunStatus::Completed)); } } diff --git a/lib/crates/fabro-store/src/slate/run_store.rs b/lib/crates/fabro-store/src/slate/run_store.rs index 388e5a188..e76b2dd73 100644 --- a/lib/crates/fabro-store/src/slate/run_store.rs +++ b/lib/crates/fabro-store/src/slate/run_store.rs @@ -16,7 +16,7 @@ use crate::{Error, EventEnvelope, EventPayload, Result, RunProjection, RunSummar 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 { @@ -47,7 +47,7 @@ impl RunDatabase { recover_next_seq(&db, keys::run_events_prefix(&run_id), keys::parse_event_seq).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), @@ -67,7 +67,7 @@ impl RunDatabase { recover_next_seq(&db, keys::run_events_prefix(&run_id), keys::parse_event_seq).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), @@ -91,7 +91,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, } } diff --git a/lib/crates/fabro-store/src/types.rs b/lib/crates/fabro-store/src/types.rs index 0450b5c4c..704cff4dd 100644 --- a/lib/crates/fabro-store/src/types.rs +++ b/lib/crates/fabro-store/src/types.rs @@ -1,24 +1,25 @@ use std::collections::HashMap; use chrono::{DateTime, Utc}; -use fabro_types::{RunControlAction, RunEvent, RunId, RunStatus, StatusReason}; +use fabro_types::{BlockedReason, RunControlAction, RunEvent, RunId, RunStatus, StatusReason}; use serde::{Deserialize, Serialize}; 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 blocked_reason: Option, + pub pending_control: Option, + pub duration_ms: Option, pub total_usd_micros: Option, } @@ -83,7 +84,7 @@ impl TryFrom<&EventPayload> for RunEvent { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct EventEnvelope { - pub seq: u32, + pub seq: u32, #[serde(flatten)] pub payload: EventPayload, } @@ -99,27 +100,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(); @@ -140,30 +141,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 5997159e9..00b7c0aeb 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; @@ -168,7 +168,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(); @@ -210,7 +210,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 0f2d8b829..d6500829a 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 8f97db039..a1b403d89 100644 --- a/lib/crates/fabro-telemetry/src/sender.rs +++ b/lib/crates/fabro-telemetry/src/sender.rs @@ -228,13 +228,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(), }; @@ -253,13 +253,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 99b917430..b7590bebd 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 3046c1c7b..3e958e8c3 100644 --- a/lib/crates/fabro-test/src/lib.rs +++ b/lib/crates/fabro-test/src/lib.rs @@ -106,23 +106,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, @@ -130,7 +130,7 @@ struct ServerPaths { #[derive(Debug, Clone)] struct SessionPaths { - root: PathBuf, + root: PathBuf, server: ServerPaths, } @@ -142,7 +142,7 @@ enum SessionMode { #[derive(Debug, Serialize)] struct ClientMarker { - pid: u32, + pid: u32, touched_at_ms: u128, } @@ -192,29 +192,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 { @@ -326,7 +334,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); @@ -688,7 +696,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"), @@ -702,7 +710,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"), @@ -721,7 +729,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"), @@ -1284,7 +1292,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(), @@ -1430,7 +1438,7 @@ pub struct TwinOpenAi { pub struct TwinGitHub { pub base_url: String, - server: twin_github::TestServer, + server: twin_github::TestServer, } pub fn test_http_client() -> fabro_http::HttpClient { @@ -1519,7 +1527,7 @@ impl TwinScenarios { #[derive(Debug, Clone)] pub struct TwinScenario { matcher: Map, - script: Value, + script: Value, } impl TwinScenario { @@ -1533,7 +1541,7 @@ impl TwinScenario { ), ("model".to_string(), Value::String(model.into())), ]), - script: json!({ "kind": "success" }), + script: json!({ "kind": "success" }), } } @@ -1627,7 +1635,7 @@ impl TwinScenario { #[derive(Debug, Clone)] pub struct TwinToolCall { - name: String, + name: String, arguments: Value, } @@ -1709,7 +1717,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, }; @@ -1858,18 +1866,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 @@ -1901,7 +1909,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 10aa4de9e..acab00aa0 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: GitHubCredentials, - client: fabro_http::HttpClient, - owner: String, - repo: String, - project_number: u64, - base_url: String, + creds: GitHubCredentials, + client: fabro_http::HttpClient, + owner: String, + repo: String, + project_number: u64, + base_url: String, project_node_id: OnceCell, } @@ -632,7 +632,7 @@ mod tests { fn mock_github_tracker(server_url: &str, pem: String) -> GitHubTracker { GitHubTracker::new( GitHubCredentials::App(GitHubAppCredentials { - app_id: "test-app".to_string(), + app_id: "test-app".to_string(), private_key_pem: pem, }), test_http_client(), @@ -645,20 +645,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 ca202caf2..677e2bf66 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 cbe6fa0dc..d24009d0a 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: fabro_http::HttpClient, + config: LinearOptions, + client: fabro_http::HttpClient, project_slug: String, } @@ -365,27 +365,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/lib.rs b/lib/crates/fabro-types/src/lib.rs index 1b994278c..599df8d45 100644 --- a/lib/crates/fabro-types/src/lib.rs +++ b/lib/crates/fabro-types/src/lib.rs @@ -53,6 +53,6 @@ pub use sandbox_record::SandboxRecord; pub use stage_id::{ParallelBranchId, StageId}; pub use start::StartRecord; pub use status::{ - InvalidTransition, ParseRunStatusError, RunControlAction, RunStatus, RunStatusRecord, - StatusReason, + BlockedReason, InvalidTransition, ParseRunStatusError, RunControlAction, RunStatus, + RunStatusRecord, StatusReason, }; 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 ec9fcd6b7..ec0143897 100644 --- a/lib/crates/fabro-types/src/run.rs +++ b/lib/crates/fabro-types/src/run.rs @@ -26,48 +26,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 e115da768..062a93fb7 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,41 +50,41 @@ 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 will_retry: bool, + pub failure: Option, + pub will_retry: bool, #[serde(default)] pub duration_ms: u64, } #[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 177484feb..105cb7236 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, Serialize)] 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, } @@ -38,8 +38,8 @@ pub enum CliTargetSettings { #[derive(Debug, Clone, PartialEq, Serialize)] pub struct CliTargetTlsSettings { pub cert: InterpString, - pub key: InterpString, - pub ca: InterpString, + pub key: InterpString, + pub ca: InterpString, } #[derive(Debug, Clone, Default, PartialEq, Serialize)] @@ -50,25 +50,25 @@ pub struct CliAuthSettings { #[derive(Debug, Clone, Default, PartialEq, Serialize)] 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, Serialize)] pub struct CliExecModelSettings { pub provider: Option, - pub name: Option, + pub name: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize)] pub struct CliExecAgentSettings { pub permissions: Option, - pub mcps: HashMap, + pub mcps: HashMap, } #[derive(Debug, Clone, Default, PartialEq, Serialize)] pub struct CliOutputSettings { - pub format: OutputFormat, + pub format: OutputFormat, pub verbosity: OutputVerbosity, } @@ -87,13 +87,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")] @@ -122,9 +122,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. @@ -152,9 +152,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)] @@ -163,7 +163,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)] @@ -173,7 +173,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. @@ -181,7 +181,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 dcdd529bb..bab3575c3 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, Serialize)] 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 79a4825ff..8fb582c9a 100644 --- a/lib/crates/fabro-types/src/settings/resolved.rs +++ b/lib/crates/fabro-types/src/settings/resolved.rs @@ -7,11 +7,11 @@ use super::{ /// A fully resolved settings view across all namespaces. #[derive(Debug, Clone, Default, PartialEq, Serialize)] 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, } @@ -44,8 +44,8 @@ mod tests { url: InterpString::parse("https://api.example.com"), tls: Some(CliTargetTlsSettings { cert: InterpString::parse("/tmp/client.crt"), - key: InterpString::parse("/tmp/client.key"), - ca: InterpString::parse("/tmp/ca.pem"), + key: InterpString::parse("/tmp/client.key"), + ca: InterpString::parse("/tmp/ca.pem"), }), }) .unwrap(), @@ -71,8 +71,8 @@ mod tests { assert_eq!( serde_json::to_value(McpTransport::Sandbox { command: vec!["fabro-mcp".to_string(), "--serve".to_string()], - port: 3333, - env: HashMap::from([("TOKEN".to_string(), "{{ env.MCP_TOKEN }}".to_string())]), + port: 3333, + env: HashMap::from([("TOKEN".to_string(), "{{ env.MCP_TOKEN }}".to_string())]), }) .unwrap(), json!({ @@ -98,9 +98,9 @@ mod tests { assert_eq!( serde_json::to_value(ObjectStoreSettings::S3 { - bucket: InterpString::parse("fabro-artifacts"), - region: InterpString::parse("us-east-1"), - endpoint: Some(InterpString::parse("https://s3.example.com")), + bucket: InterpString::parse("fabro-artifacts"), + region: InterpString::parse("us-east-1"), + endpoint: Some(InterpString::parse("https://s3.example.com")), path_style: true, }) .unwrap(), @@ -119,9 +119,9 @@ mod tests { assert_eq!( serde_json::to_value(ServerListenSettings::Tcp { address: "127.0.0.1:8080".parse().unwrap(), - tls: Some(TlsConfig { + tls: Some(TlsConfig { cert: InterpString::parse("/tmp/server.crt"), - key: InterpString::parse("/tmp/server.key"), + key: InterpString::parse("/tmp/server.key"), }), }) .unwrap(), @@ -138,29 +138,32 @@ mod tests { let settings = Settings { server: ServerSettings { slatedb: ServerSlateDbSettings { - prefix: InterpString::parse("slatedb/"), - store: ObjectStoreSettings::Local { + prefix: InterpString::parse("slatedb/"), + store: ObjectStoreSettings::Local { root: InterpString::parse("/srv/slatedb"), }, flush_interval: StdDuration::from_secs(30), - disk_cache: false, + disk_cache: false, }, ..ServerSettings::default() }, run: RunSettings { agent: RunAgentSettings { - mcps: HashMap::from([("sandboxed".to_string(), McpServerSettings { - name: "sandboxed".to_string(), - transport: McpTransport::Http { - url: "https://mcp.example.com".to_string(), - headers: HashMap::from([( - "Authorization".to_string(), - "Bearer {{ env.MCP_TOKEN }}".to_string(), - )]), + mcps: HashMap::from([( + "sandboxed".to_string(), + McpServerSettings { + name: "sandboxed".to_string(), + transport: McpTransport::Http { + url: "https://mcp.example.com".to_string(), + headers: HashMap::from([( + "Authorization".to_string(), + "Bearer {{ env.MCP_TOKEN }}".to_string(), + )]), + }, + startup_timeout_secs: 15, + tool_timeout_secs: 90, }, - startup_timeout_secs: 15, - tool_timeout_secs: 90, - })]), + )]), ..RunAgentSettings::default() }, ..RunSettings::default() diff --git a/lib/crates/fabro-types/src/settings/run.rs b/lib/crates/fabro-types/src/settings/run.rs index c5c45c13c..a5eb43ce4 100644 --- a/lib/crates/fabro-types/src/settings/run.rs +++ b/lib/crates/fabro-types/src/settings/run.rs @@ -19,23 +19,23 @@ use super::model_ref::ModelRef; /// A structurally resolved `[run]` view for consumers. #[derive(Debug, Clone, Default, PartialEq, Serialize)] 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. @@ -48,8 +48,8 @@ pub enum RunGoal { #[derive(Debug, Clone, Default, PartialEq, Serialize)] pub struct RunModelSettings { - pub provider: Option, - pub name: Option, + pub provider: Option, + pub name: Option, pub fallbacks: Vec, } @@ -60,20 +60,20 @@ pub struct RunGitSettings { #[derive(Debug, Clone, Default, PartialEq, Serialize)] pub struct GitAuthorSettings { - pub name: Option, + pub name: Option, pub email: Option, } #[derive(Debug, Clone, PartialEq, Serialize)] 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, } } @@ -81,17 +81,17 @@ impl Default for RunPrepareSettings { #[derive(Debug, Clone, PartialEq, Serialize)] 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, } } } @@ -103,23 +103,23 @@ pub struct RunCheckpointSettings { #[derive(Debug, Clone, PartialEq, Serialize)] 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, } } } @@ -132,10 +132,10 @@ pub struct LocalSandboxSettings { #[derive(Debug, Clone, Default, PartialEq, Serialize)] 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)] @@ -166,21 +166,21 @@ impl Serialize for DockerfileSource { #[derive(Debug, Clone, PartialEq, Serialize)] 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, Serialize)] 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, Serialize)] @@ -191,9 +191,9 @@ pub struct NotificationProviderSettings { #[derive(Debug, Clone, Default, PartialEq, Serialize)] 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, Serialize)] @@ -204,27 +204,27 @@ pub struct InterviewProviderSettings { #[derive(Debug, Clone, Default, PartialEq, Serialize)] pub struct RunAgentSettings { pub permissions: Option, - pub mcps: HashMap, + pub mcps: HashMap, } #[derive(Debug, Clone, PartialEq, Serialize)] 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, } } } @@ -246,16 +246,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, }, } @@ -275,36 +275,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 { @@ -373,10 +373,10 @@ impl HookDefinition { #[derive(Debug, Clone, Default, PartialEq, Serialize)] 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)] @@ -394,18 +394,18 @@ impl Serialize for ScmGitHubSettings { #[derive(Debug, Clone, PartialEq, Serialize)] 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, } } @@ -421,41 +421,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 @@ -493,7 +493,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, } @@ -512,9 +512,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")] @@ -561,7 +561,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, } @@ -572,7 +572,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, @@ -583,11 +583,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. @@ -595,12 +595,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)] @@ -630,18 +630,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)] @@ -668,26 +668,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, } @@ -712,19 +712,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. @@ -769,11 +769,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)] @@ -791,7 +791,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)] @@ -810,43 +810,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, }, } @@ -858,40 +858,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)] @@ -939,14 +939,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 @@ -961,11 +961,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 42c80ae6e..825f0905a 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, Serialize)] 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, } @@ -35,7 +35,7 @@ pub enum ServerListenSettings { Tcp { #[serde(serialize_with = "serialize_socket_addr")] address: SocketAddr, - tls: Option, + tls: Option, }, Unix { path: InterpString, @@ -53,14 +53,14 @@ impl Default for ServerListenSettings { #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct TlsConfig { pub cert: InterpString, - pub key: InterpString, + pub key: InterpString, } impl Default for TlsConfig { fn default() -> Self { Self { cert: InterpString::parse(""), - key: InterpString::parse(""), + key: InterpString::parse(""), } } } @@ -73,14 +73,14 @@ pub struct ServerApiSettings { #[derive(Debug, Clone, PartialEq, Eq, Serialize)] 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(""), } } } @@ -88,14 +88,14 @@ impl Default for ServerWebSettings { #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct ServerAuthSettings { pub methods: Vec, - pub github: ServerAuthGithubSettings, + pub github: ServerAuthGithubSettings, } impl Default for ServerAuthSettings { fn default() -> Self { Self { methods: vec![ServerAuthMethod::DevToken], - github: ServerAuthGithubSettings::default(), + github: ServerAuthGithubSettings::default(), } } } @@ -128,34 +128,34 @@ impl Default for ServerStorageSettings { #[derive(Debug, Clone, PartialEq, Eq, Serialize)] 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, Serialize)] pub struct ServerSlateDbSettings { - pub prefix: InterpString, - pub store: ObjectStoreSettings, + pub prefix: InterpString, + pub store: ObjectStoreSettings, #[serde(serialize_with = "serialize_std_duration")] pub flush_interval: StdDuration, - pub disk_cache: bool, + pub disk_cache: bool, } impl Default for ServerSlateDbSettings { fn default() -> Self { Self { - prefix: InterpString::parse(""), - store: ObjectStoreSettings::default(), + prefix: InterpString::parse(""), + store: ObjectStoreSettings::default(), flush_interval: StdDuration::ZERO, - disk_cache: false, + disk_cache: false, } } } @@ -167,9 +167,9 @@ pub enum ObjectStoreSettings { root: InterpString, }, S3 { - bucket: InterpString, - region: InterpString, - endpoint: Option, + bucket: InterpString, + region: InterpString, + endpoint: Option, path_style: bool, }, } @@ -194,26 +194,26 @@ pub struct ServerLoggingSettings { #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] 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, Serialize)] pub struct GithubIntegrationSettings { - pub enabled: bool, - pub strategy: GithubIntegrationStrategy, - pub app_id: Option, - pub client_id: Option, - pub slug: Option, + pub enabled: bool, + pub strategy: GithubIntegrationStrategy, + 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, Serialize)] pub struct SlackIntegrationSettings { - pub enabled: bool, + pub enabled: bool, pub default_channel: Option, } @@ -251,23 +251,23 @@ where #[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, } @@ -281,7 +281,7 @@ pub enum ServerListenLayer { #[serde(default)] address: Option, #[serde(default)] - tls: Option, + tls: Option, }, Unix { #[serde(default)] @@ -295,7 +295,7 @@ 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, } /// `[server.api]` — API surface settings. @@ -315,7 +315,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. @@ -329,7 +329,7 @@ pub struct ServerAuthLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub methods: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] - pub github: Option, + pub github: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] @@ -354,11 +354,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. @@ -366,17 +366,17 @@ 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, #[serde(default, skip_serializing_if = "Option::is_none")] - pub disk_cache: Option, + pub disk_cache: Option, } /// Closed enum of object-store providers. Unknown providers hard-fail @@ -401,11 +401,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, } @@ -434,13 +434,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 @@ -449,19 +449,19 @@ 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 strategy: Option, + pub strategy: 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. @@ -469,7 +469,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 d97c7e74e..a98be82dd 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, Serialize)] 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..73727d288 100644 --- a/lib/crates/fabro-types/src/status.rs +++ b/lib/crates/fabro-types/src/status.rs @@ -8,45 +8,58 @@ use serde::{Deserialize, Serialize}; #[serde(rename_all = "snake_case")] pub enum RunStatus { Submitted, + Queued, Starting, Running, + Blocked, Paused, Removing, - Succeeded, + Completed, Failed, - Dead, + Cancelled, } impl RunStatus { pub fn is_terminal(self) -> bool { - matches!(self, Self::Succeeded | Self::Failed | Self::Dead) + matches!(self, Self::Completed | Self::Failed | Self::Cancelled) } pub fn is_active(self) -> bool { matches!( self, - Self::Submitted | Self::Starting | Self::Running | Self::Paused | Self::Removing + Self::Submitted + | Self::Queued + | Self::Starting + | Self::Running + | Self::Blocked + | Self::Paused + | Self::Removing ) } pub fn can_transition_to(self, to: Self) -> bool { - if to == Self::Dead { - return true; - } if self.is_terminal() { return false; } matches!( (self, to), - (Self::Submitted, Self::Starting) - | (Self::Starting | Self::Paused, Self::Running) + (Self::Submitted, Self::Queued | Self::Starting) | ( - Self::Starting | Self::Running | Self::Paused | Self::Removing, - Self::Failed + Self::Queued, + Self::Starting | Self::Failed | Self::Cancelled + ) + | (Self::Starting | Self::Paused | Self::Blocked, Self::Running) + | ( + Self::Starting | Self::Running | Self::Blocked | Self::Paused | Self::Removing, + Self::Failed | Self::Cancelled ) | ( Self::Running, - Self::Succeeded | Self::Paused | Self::Removing + Self::Completed | Self::Blocked | Self::Paused | Self::Removing + ) + | ( + Self::Blocked, + Self::Completed | Self::Paused | Self::Removing ) | (Self::Paused, Self::Removing) ) @@ -65,13 +78,15 @@ impl fmt::Display for RunStatus { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { Self::Submitted => "submitted", + Self::Queued => "queued", Self::Starting => "starting", Self::Running => "running", + Self::Blocked => "blocked", Self::Paused => "paused", Self::Removing => "removing", - Self::Succeeded => "succeeded", + Self::Completed => "completed", Self::Failed => "failed", - Self::Dead => "dead", + Self::Cancelled => "cancelled", }; f.write_str(s) } @@ -83,13 +98,15 @@ impl FromStr for RunStatus { fn from_str(s: &str) -> Result { match s { "submitted" => Ok(Self::Submitted), + "queued" => Ok(Self::Queued), "starting" => Ok(Self::Starting), "running" => Ok(Self::Running), + "blocked" => Ok(Self::Blocked), "paused" => Ok(Self::Paused), "removing" => Ok(Self::Removing), - "succeeded" => Ok(Self::Succeeded), + "completed" => Ok(Self::Completed), "failed" => Ok(Self::Failed), - "dead" => Ok(Self::Dead), + "cancelled" => Ok(Self::Cancelled), _ => Err(ParseRunStatusError(s.to_string())), } } @@ -109,7 +126,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 { @@ -136,6 +153,12 @@ pub enum StatusReason { SandboxInitializing, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum BlockedReason { + HumanInputRequired, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RunControlAction { @@ -146,9 +169,11 @@ 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, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub blocked_reason: Option, pub updated_at: DateTime, } @@ -157,6 +182,7 @@ impl RunStatusRecord { Self { status, reason, + blocked_reason: None, updated_at: Utc::now(), } } 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 c20d307a3..bc75905dd 100644 --- a/lib/crates/fabro-util/src/check_report.rs +++ b/lib/crates/fabro-util/src/check_report.rs @@ -46,22 +46,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, } @@ -213,37 +213,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, @@ -428,9 +428,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")], }], }; @@ -449,10 +449,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, @@ -472,10 +472,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)); @@ -488,10 +488,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, }], @@ -507,10 +507,10 @@ mod tests { #[test] fn render_remediation_backticks_no_color() { let r = report(vec![CheckResult { - name: "Sandbox".into(), - status: CheckStatus::Warning, - summary: "not configured".into(), - details: Vec::new(), + name: "Sandbox".into(), + status: CheckStatus::Warning, + summary: "not configured".into(), + details: Vec::new(), remediation: Some("Run `fabro secret set KEY` to fix".into()), }]); let out = r.render(&Styles::new(false), false, None, None); @@ -529,10 +529,10 @@ mod tests { #[test] fn render_remediation_backticks_with_color() { let r = report(vec![CheckResult { - name: "Sandbox".into(), - status: CheckStatus::Warning, - summary: "not configured".into(), - details: Vec::new(), + name: "Sandbox".into(), + status: CheckStatus::Warning, + summary: "not configured".into(), + details: Vec::new(), remediation: Some("Run `fabro secret set KEY` to fix".into()), }]); let out = r.render(&Styles::new(true), false, None, None); 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-vault/src/lib.rs b/lib/crates/fabro-vault/src/lib.rs index 676249333..d3822d00d 100644 --- a/lib/crates/fabro-vault/src/lib.rs +++ b/lib/crates/fabro-vault/src/lib.rs @@ -13,24 +13,24 @@ pub enum SecretType { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct SecretEntry { - pub value: String, + pub value: String, #[serde(rename = "type", default)] pub secret_type: SecretType, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, - pub created_at: String, - pub updated_at: 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, #[serde(rename = "type")] pub secret_type: SecretType, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, - pub created_at: String, - pub updated_at: String, + pub created_at: String, + pub updated_at: String, } #[derive(Debug)] @@ -68,7 +68,7 @@ impl From for Error { #[derive(Debug)] pub struct Vault { - path: PathBuf, + path: PathBuf, entries: HashMap, } @@ -136,11 +136,11 @@ impl Vault { .entries .iter() .map(|(name, entry)| SecretMetadata { - name: name.clone(), + name: name.clone(), secret_type: entry.secret_type, description: entry.description.clone(), - created_at: entry.created_at.clone(), - updated_at: entry.updated_at.clone(), + created_at: entry.created_at.clone(), + updated_at: entry.updated_at.clone(), }) .collect::>(); data.sort_by(|a, b| a.name.cmp(&b.name)); @@ -343,10 +343,10 @@ mod tests { .unwrap(); let reloaded = Vault::load(path).unwrap(); - assert_eq!(reloaded.file_secrets(), vec![( - "/tmp/key.pem".to_string(), - "pem".to_string() - )]); + assert_eq!( + reloaded.file_secrets(), + vec![("/tmp/key.pem".to_string(), "pem".to_string())] + ); } #[test] diff --git a/lib/crates/fabro-workflow/src/artifact.rs b/lib/crates/fabro-workflow/src/artifact.rs index 3347c717e..b6d95323b 100644 --- a/lib/crates/fabro-workflow/src/artifact.rs +++ b/lib/crates/fabro-workflow/src/artifact.rs @@ -473,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"), @@ -487,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([( @@ -499,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); @@ -531,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 b206e06de..d99c09b29 100644 --- a/lib/crates/fabro-workflow/src/devcontainer_bridge.rs +++ b/lib/crates/fabro-workflow/src/devcontainer_bridge.rs @@ -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, } } @@ -46,7 +46,7 @@ pub async fn run_devcontainer_lifecycle( } emitter.emit(&Event::DevcontainerLifecycleStarted { - phase: phase.to_string(), + phase: phase.to_string(), command_count: commands.len(), }); let phase_start = Instant::now(); @@ -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(()) @@ -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, }) } @@ -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 c4736eb13..a14a63fed 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 { @@ -392,12 +392,12 @@ mod tests { fn validation_failed_display() { 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"); @@ -676,7 +676,7 @@ mod tests { fn llm_error_display() { let sdk_err = SdkError::Network { message: "connection refused".into(), - source: None, + source: None, }; let err = Error::Llm(sdk_err); assert_eq!( @@ -689,13 +689,13 @@ mod tests { fn llm_error_retryable_delegates_to_sdk() { let retryable = Error::Llm(SdkError::Network { message: "timeout".into(), - source: None, + source: None, }); assert!(retryable.is_retryable()); let non_retryable = Error::Llm(SdkError::Configuration { message: "bad config".into(), - source: None, + source: None, }); assert!(!non_retryable.is_retryable()); } @@ -704,7 +704,7 @@ mod tests { fn llm_error_from_sdk_error() { let sdk_err = SdkError::Stream { message: "broken pipe".into(), - source: None, + source: None, }; let err = Error::from(sdk_err); assert!(matches!(err, Error::Llm(_))); @@ -755,7 +755,7 @@ mod tests { #[test] fn failure_class_llm_rate_limit() { let err = Error::Llm(SdkError::Provider { - kind: ProviderErrorKind::RateLimit, + kind: ProviderErrorKind::RateLimit, detail: Box::new(ProviderErrorDetail::new("too fast", "openai")), }); assert_eq!(err.failure_category(), FailureCategory::TransientInfra); @@ -764,7 +764,7 @@ mod tests { #[test] fn failure_class_llm_context_length() { let err = Error::Llm(SdkError::Provider { - kind: ProviderErrorKind::ContextLength, + kind: ProviderErrorKind::ContextLength, detail: Box::new(ProviderErrorDetail::new("too long", "openai")), }); assert_eq!(err.failure_category(), FailureCategory::BudgetExhausted); @@ -773,7 +773,7 @@ mod tests { #[test] fn failure_class_llm_auth() { let err = Error::Llm(SdkError::Provider { - kind: ProviderErrorKind::Authentication, + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), }); assert_eq!(err.failure_category(), FailureCategory::Deterministic); @@ -791,7 +791,7 @@ mod tests { fn failure_class_llm_timeout() { let err = Error::Llm(SdkError::RequestTimeout { message: "timed out".into(), - source: None, + source: None, }); assert_eq!(err.failure_category(), FailureCategory::TransientInfra); } @@ -801,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); @@ -810,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); @@ -819,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); @@ -828,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); @@ -837,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); @@ -847,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,7 +1492,7 @@ mod tests { #[test] fn failure_signature_hint_llm_returns_some() { let err = Error::Llm(SdkError::Provider { - kind: ProviderErrorKind::Authentication, + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), }); assert_eq!( @@ -1518,7 +1518,7 @@ mod tests { #[test] fn to_fail_outcome_llm_has_class_and_signature() { let err = Error::Llm(SdkError::Provider { - kind: ProviderErrorKind::Authentication, + kind: ProviderErrorKind::Authentication, detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), }); let outcome = err.to_fail_outcome(); @@ -1545,7 +1545,7 @@ mod tests { fn to_fail_outcome_includes_error_message_as_reason() { let err = Error::Llm(SdkError::Network { message: "connection refused".into(), - source: None, + source: None, }); let outcome = err.to_fail_outcome(); assert!( @@ -1560,7 +1560,7 @@ mod tests { fn to_fail_outcome_no_context_updates() { 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()); @@ -1604,19 +1604,19 @@ mod tests { 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, }], }, Error::engine("engine err"), Error::handler("handler err"), Error::Llm(SdkError::Network { message: "refused".into(), - source: None, + source: None, }), Error::Checkpoint("cp err".into()), Error::Stylesheet("style err".into()), @@ -1684,7 +1684,7 @@ mod tests { // 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 = Error::Llm(sdk_err); @@ -1700,11 +1700,11 @@ 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(), - will_retry: false, + node_id: "code".into(), + name: "code".into(), + index: 0, + failure: failure.clone(), + will_retry: false, duration_ms: 0, }; @@ -1770,7 +1770,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 a11bff16c..f339098b9 100644 --- a/lib/crates/fabro-workflow/src/event.rs +++ b/lib/crates/fabro-workflow/src/event.rs @@ -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: Error, - 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,61 +181,61 @@ pub enum Event { max_attempts: usize, }, StageFailed { - node_id: String, - name: String, - index: usize, - failure: FailureDetail, - will_retry: bool, + node_id: String, + name: String, + index: usize, + failure: FailureDetail, + will_retry: bool, duration_ms: u64, }, 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")] @@ -243,21 +243,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 { @@ -287,96 +287,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 { @@ -384,174 +384,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, }, } @@ -1268,15 +1268,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 { @@ -1294,13 +1294,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, } } @@ -1480,21 +1480,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, @@ -1505,18 +1504,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 } => { @@ -1553,10 +1552,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, @@ -1568,14 +1567,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, @@ -1583,9 +1582,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 { @@ -1593,8 +1592,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 { @@ -1604,9 +1603,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 { @@ -1656,9 +1655,9 @@ fn event_body_from_event(event: &Event) -> EventBody { duration_ms, .. } => EventBody::StageFailed(fabro_types::StageFailedProps { - index: *index, - failure: Some(failure.clone()), - will_retry: *will_retry, + index: *index, + failure: Some(failure.clone()), + will_retry: *will_retry, duration_ms: *duration_ms, }), Event::StageRetrying { @@ -1668,10 +1667,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, @@ -1679,9 +1678,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 { @@ -1695,10 +1694,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, @@ -1708,11 +1707,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, @@ -1724,12 +1723,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(), }), @@ -1740,8 +1739,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 { @@ -1751,8 +1750,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 { @@ -1763,9 +1762,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 { @@ -1805,16 +1804,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(), }) } @@ -1822,7 +1821,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 } => { @@ -1839,20 +1838,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 { @@ -1863,11 +1862,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, @@ -1877,16 +1876,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 => { @@ -1898,7 +1897,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 { @@ -1907,21 +1906,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, @@ -1929,11 +1928,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"), @@ -1944,10 +1943,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 }) @@ -1955,12 +1954,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, }) } @@ -1968,9 +1967,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, @@ -1978,11 +1977,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, @@ -1991,12 +1990,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, @@ -2004,9 +2003,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, @@ -2014,11 +2013,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, @@ -2026,15 +2025,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 { @@ -2042,14 +2041,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 @@ -2073,8 +2072,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 } => { @@ -2090,20 +2089,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 } => { @@ -2115,13 +2114,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 } => { @@ -2129,7 +2128,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, }) } @@ -2141,31 +2140,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(), }) } @@ -2177,11 +2176,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 { @@ -2191,7 +2190,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 { @@ -2200,9 +2199,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 } => { @@ -2216,10 +2215,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 { @@ -2236,13 +2235,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 { @@ -2258,10 +2257,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 { @@ -2276,11 +2275,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, @@ -2288,9 +2287,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 { @@ -2300,9 +2299,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 { @@ -2313,11 +2312,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, @@ -2327,11 +2326,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, @@ -2340,9 +2339,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 { @@ -2355,14 +2354,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 { @@ -2375,17 +2374,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, }, ), @@ -2395,9 +2394,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 { @@ -2408,17 +2407,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, }, ) @@ -2431,11 +2430,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 { @@ -2443,9 +2442,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, @@ -2453,12 +2452,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, }) } @@ -2470,9 +2469,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, } @@ -2481,9 +2480,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(), } } @@ -2515,9 +2514,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), } } @@ -2762,8 +2761,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, @@ -2896,13 +2895,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); @@ -2945,9 +2944,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, }), ); @@ -2965,28 +2964,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"); @@ -2996,17 +2998,20 @@ 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, - duration_ms: 5000, - }); + 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, + duration_ms: 5000, + }, + ); assert_eq!(stored.event_name(), "stage.failed"); let properties = stored.properties().unwrap(); @@ -3017,17 +3022,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")); @@ -3042,16 +3050,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()); @@ -3062,12 +3073,15 @@ mod tests { #[test] fn run_event_workflow_failure_uses_display_error() { - 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()), - }); + 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(); @@ -3084,11 +3098,14 @@ mod tests { None, ); 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(); @@ -3145,11 +3162,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"); @@ -3163,31 +3183,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" @@ -3199,18 +3219,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)), }), ); @@ -3223,24 +3243,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, @@ -3253,21 +3279,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)), }), ); @@ -3287,19 +3313,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(), @@ -3312,12 +3338,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), @@ -3328,7 +3354,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), @@ -3347,42 +3373,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")); @@ -3395,31 +3431,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::Github, }), }; - 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 60d4dc710..3b2b458e1 100644 --- a/lib/crates/fabro-workflow/src/git.rs +++ b/lib/crates/fabro-workflow/src/git.rs @@ -433,93 +433,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 b4eb1390f..5ccfed63d 100644 --- a/lib/crates/fabro-workflow/src/graph.rs +++ b/lib/crates/fabro-workflow/src/graph.rs @@ -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 0c9008a7e..a3e832cb3 100644 --- a/lib/crates/fabro-workflow/src/handler/agent.rs +++ b/lib/crates/fabro-workflow/src/handler/agent.rs @@ -19,9 +19,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), @@ -256,12 +256,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, ); @@ -332,11 +332,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, ); @@ -603,11 +603,10 @@ mod tests { _tool_hooks: Option>, ) -> 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, }) } @@ -660,9 +659,9 @@ mod tests { _tool_hooks: Option>, ) -> 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()), }) } @@ -720,21 +719,21 @@ mod tests { 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, }) } @@ -830,9 +829,9 @@ mod tests { ) -> 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, }) } @@ -882,9 +881,9 @@ mod tests { ) -> 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, }) } @@ -1110,9 +1109,9 @@ Some text in between. ) -> 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, }) } @@ -1179,9 +1178,9 @@ Some text in between. ) -> 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 c88e43b68..766a9e3ea 100644 --- a/lib/crates/fabro-workflow/src/handler/command.rs +++ b/lib/crates/fabro-workflow/src/handler/command.rs @@ -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, @@ -122,12 +122,12 @@ impl Handler for CommandHandler { 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, ); @@ -713,9 +713,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 +816,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 +861,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 +904,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 +939,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, })); diff --git a/lib/crates/fabro-workflow/src/handler/fan_in.rs b/lib/crates/fabro-workflow/src/handler/fan_in.rs index 3c411140d..1689620c9 100644 --- a/lib/crates/fabro-workflow/src/handler/fan_in.rs +++ b/lib/crates/fabro-workflow/src/handler/fan_in.rs @@ -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, }) } @@ -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, ); @@ -473,9 +473,9 @@ mod tests { ) -> 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 9dbeeaf87..d0491a252 100644 --- a/lib/crates/fabro-workflow/src/handler/human.rs +++ b/lib/crates/fabro-workflow/src/handler/human.rs @@ -18,9 +18,9 @@ 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 { @@ -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, @@ -275,9 +275,9 @@ impl Handler for HumanHandler { &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, diff --git a/lib/crates/fabro-workflow/src/handler/llm/api.rs b/lib/crates/fabro-workflow/src/handler/llm/api.rs index 27ad86ae8..bd9049a8d 100644 --- a/lib/crates/fabro-workflow/src/handler/llm/api.rs +++ b/lib/crates/fabro-workflow/src/handler/llm/api.rs @@ -36,7 +36,7 @@ fn build_profile(model: &str, provider: Provider) -> Box { } pub(crate) struct LlmClientBuildResult { - pub(crate) client: Client, + pub(crate) client: Client, pub(crate) auth_issues: Vec<(Provider, ResolveError)>, } @@ -101,7 +101,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) { @@ -158,10 +158,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, @@ -176,13 +176,13 @@ 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, - resolver: Option, + sessions: Mutex>, + env: HashMap, + mcp_servers: Vec, + resolver: Option, } impl AgentApiBackend { @@ -470,9 +470,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, }) } @@ -531,7 +531,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); @@ -569,12 +569,12 @@ impl CodergenBackend for AgentApiBackend { 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, ); @@ -730,7 +730,7 @@ mod tests { FileTracking { pending: HashMap::new(), touched: HashSet::new(), - last: None, + last: None, } } @@ -746,9 +746,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, ); @@ -757,9 +757,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, ); @@ -779,9 +779,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, ); @@ -790,9 +790,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, ); @@ -812,9 +812,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, ); @@ -822,9 +822,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, ); @@ -857,7 +857,7 @@ mod tests { "anthropic", &serde_json::to_string(&AuthCredential { provider: Provider::Anthropic, - details: AuthDetails::ApiKey { + details: AuthDetails::ApiKey { key: "anthropic-key".to_string(), }, }) diff --git a/lib/crates/fabro-workflow/src/handler/llm/cli.rs b/lib/crates/fabro-workflow/src/handler/llm/cli.rs index a34c15d28..dbb4cd35b 100644 --- a/lib/crates/fabro-workflow/src/handler/llm/cli.rs +++ b/lib/crates/fabro-workflow/src/handler/llm/cli.rs @@ -211,8 +211,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, } @@ -380,11 +380,11 @@ fn shell_quote(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, - resolver: Option, + resolver: Option, } impl AgentCliBackend { @@ -517,12 +517,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, ); @@ -654,10 +654,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, @@ -724,12 +724,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, @@ -859,7 +863,7 @@ mod tests { /// Mock sandbox that returns pre-configured ExecResults in FIFO order. struct CliMockSandbox { - results: Mutex>, + results: Mutex>, commands: Arc>>, } @@ -952,20 +956,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, } } @@ -1229,9 +1233,9 @@ mod tests { _tool_hooks: Option>, ) -> 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 87150a7f6..66968e89d 100644 --- a/lib/crates/fabro-workflow/src/handler/manager_loop.rs +++ b/lib/crates/fabro-workflow/src/handler/manager_loop.rs @@ -28,7 +28,7 @@ use crate::run_options::RunOptions; pub struct SubWorkflowHandler; struct ParsedChildWorkflow { - graph: Graph, + graph: Graph, workflow_path: Option, } @@ -68,12 +68,12 @@ fn parse_child_graph(node: &Node, services: &EngineServices) -> Result, - 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>, /// Resolved default provider for the current run. - pub provider: Provider, + pub provider: Provider, /// 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 { @@ -108,14 +108,14 @@ impl EngineServices { None, )); 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() @@ -130,15 +130,15 @@ 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, - provider: Provider::Anthropic, - workflow_path: None, - workflow_bundle: None, + provider: Provider::Anthropic, + workflow_path: None, + workflow_bundle: None, } } } @@ -237,7 +237,7 @@ pub async fn dispatch_handler( /// Maps handler type strings to handler implementations. pub struct HandlerRegistry { - handlers: HashMap>, + handlers: HashMap>, default_handler: Box, } diff --git a/lib/crates/fabro-workflow/src/handler/parallel.rs b/lib/crates/fabro-workflow/src/handler/parallel.rs index 8b1fec8c0..87afb627c 100644 --- a/lib/crates/fabro-workflow/src/handler/parallel.rs +++ b/lib/crates/fabro-workflow/src/handler/parallel.rs @@ -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, } @@ -128,12 +128,12 @@ impl Handler for ParallelHandler { ) -> 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, ); @@ -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); @@ -319,10 +319,10 @@ impl Handler for ParallelHandler { 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, ); @@ -335,13 +335,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, ); @@ -415,7 +415,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, ); @@ -429,13 +429,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: 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, ); @@ -459,19 +459,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, }); } diff --git a/lib/crates/fabro-workflow/src/handler/prompt.rs b/lib/crates/fabro-workflow/src/handler/prompt.rs index df5e1f1ca..b62c50225 100644 --- a/lib/crates/fabro-workflow/src/handler/prompt.rs +++ b/lib/crates/fabro-workflow/src/handler/prompt.rs @@ -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, ); @@ -281,9 +281,9 @@ mod tests { _system_prompt: Option<&str>, ) -> 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, }) } @@ -341,9 +341,9 @@ mod tests { _system_prompt: Option<&str>, ) -> 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, }) } @@ -372,7 +372,7 @@ mod tests { } struct OneShotCapturingBackend { - captured_prompt: Arc>>, + captured_prompt: Arc>>, captured_system_prompt: Arc>>>, } @@ -400,9 +400,9 @@ mod tests { *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, }) } @@ -414,7 +414,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))); @@ -451,7 +451,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))); @@ -484,7 +484,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/lib.rs b/lib/crates/fabro-workflow/src/lib.rs index d709327da..9eda2b32c 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![], }); } } diff --git a/lib/crates/fabro-workflow/src/lifecycle/artifact.rs b/lib/crates/fabro-workflow/src/lifecycle/artifact.rs index 5cab268e7..17a2e4603 100644 --- a/lib/crates/fabro-workflow/src/lifecycle/artifact.rs +++ b/lib/crates/fabro-workflow/src/lifecycle/artifact.rs @@ -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 7b4bdf135..d2b6507ad 100644 --- a/lib/crates/fabro-workflow/src/lifecycle/circuit_breaker.rs +++ b/lib/crates/fabro-workflow/src/lifecycle/circuit_breaker.rs @@ -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 273e6d733..df5fafbc4 100644 --- a/lib/crates/fabro-workflow/src/lifecycle/event.rs +++ b/lib/crates/fabro-workflow/src/lifecycle/event.rs @@ -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, @@ -226,12 +226,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()), }, @@ -402,14 +402,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, }); } diff --git a/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs b/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs index c39eded75..019c8fde3 100644 --- a/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs +++ b/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs @@ -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 faa6f10ba..ca12e5a55 100644 --- a/lib/crates/fabro-workflow/src/lifecycle/git.rs +++ b/lib/crates/fabro-workflow/src/lifecycle/git.rs @@ -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 f3d66a674..90f35da4c 100644 --- a/lib/crates/fabro-workflow/src/lifecycle/hook.rs +++ b/lib/crates/fabro-workflow/src/lifecycle/hook.rs @@ -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 98018ffda..80f125158 100644 --- a/lib/crates/fabro-workflow/src/node_handler.rs +++ b/lib/crates/fabro-workflow/src/node_handler.rs @@ -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, })); } @@ -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 6fbda6340..501c6ea6b 100644 --- a/lib/crates/fabro-workflow/src/operations/create.rs +++ b/lib/crates/fabro-workflow/src/operations/create.rs @@ -46,22 +46,22 @@ 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, - working_directory: PathBuf, - host_repo_path: Option, - repo_origin_url: Option, - provenance: 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, configured_providers: Vec, } @@ -70,7 +70,7 @@ pub async fn create(store: &Database, request: CreateRunInput) -> Result PathBuf { @@ -455,7 +465,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, @@ -574,12 +584,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()); @@ -617,12 +627,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(); @@ -653,9 +663,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(); @@ -666,9 +676,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"] @@ -676,7 +686,7 @@ mod tests { start -> validate -> exit }"# .to_string(), - files: HashMap::from([ + files: HashMap::from([ ( PathBuf::from("child/validate.fabro"), r#"digraph Validate { @@ -693,8 +703,8 @@ mod tests { ), ]), }), - settings: SettingsLayer::default(), - cwd: PathBuf::from("."), + settings: SettingsLayer::default(), + cwd: PathBuf::from("."), custom_transforms: Vec::new(), }) .unwrap(); @@ -717,24 +727,27 @@ 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, + configured_providers: Vec::new(), }, - 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, - configured_providers: Vec::new(), - }) + ) .await .unwrap_err(); @@ -750,51 +763,54 @@ 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, + configured_providers: Vec::new(), }, - 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, - configured_providers: Vec::new(), - }) + ) .await .unwrap(); @@ -859,37 +875,40 @@ 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, + configured_providers: Vec::new(), }, - 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, - configured_providers: Vec::new(), - }) + ) .await .unwrap(); @@ -911,24 +930,27 @@ 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, + configured_providers: Vec::new(), }, - 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, - configured_providers: Vec::new(), - }) + ) .await .unwrap(); @@ -986,24 +1008,27 @@ mod tests { Duration::from_millis(1), None, )); - 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, + configured_providers: Vec::new(), }, - 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, - configured_providers: Vec::new(), - }) + ) .await .unwrap(); let run_store = store.open_run_reader(&created.run_id).await.unwrap(); @@ -1028,37 +1053,40 @@ mod tests { Duration::from_millis(1), None, )); - 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, + }), + }), + configured_providers: Vec::new(), }, - 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, - }), - }), - configured_providers: Vec::new(), - }) + ) .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 923d5cd60..ab24a1ef8 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(), }) } @@ -398,11 +398,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, } } @@ -413,20 +413,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(), @@ -440,23 +440,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 @@ -464,28 +468,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(); } @@ -495,31 +507,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(); } @@ -530,14 +546,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 3afb187dc..f06056597 100644 --- a/lib/crates/fabro-workflow/src/operations/resume.rs +++ b/lib/crates/fabro-workflow/src/operations/resume.rs @@ -15,7 +15,7 @@ pub async fn resume(run_dir: &Path, services: StartServices) -> 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 1387852fd..cfd8aaaf1 100644 --- a/lib/crates/fabro-workflow/src/operations/start.rs +++ b/lib/crates/fabro-workflow/src/operations/start.rs @@ -52,55 +52,55 @@ 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>, - vault: 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>, + vault: 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 vault: 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 vault: 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, } @@ -153,9 +153,13 @@ 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 = Error::engine(err.to_string()); @@ -254,12 +258,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"); @@ -278,8 +286,8 @@ impl RunSession { .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())), }) }); @@ -393,7 +401,7 @@ impl RunSession { }; let devcontainer = resolved.sandbox.devcontainer.then(|| DevcontainerSpec { - enabled: true, + enabled: true, resolve_dir: working_directory.clone(), }); @@ -422,9 +430,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(), @@ -529,39 +537,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() @@ -574,21 +582,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, @@ -606,12 +614,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, } } @@ -626,10 +634,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, @@ -637,15 +645,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, }, } @@ -663,17 +671,17 @@ impl RunSession { 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)); @@ -774,21 +782,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(); @@ -808,10 +816,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 { @@ -850,12 +858,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: Error::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; }); } @@ -867,10 +879,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 { @@ -917,18 +929,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: Error::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; }); } @@ -945,20 +965,24 @@ async fn persist_detached_failure( ) -> 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 { @@ -1011,36 +1035,39 @@ 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, + configured_providers: Vec::new(), }, - 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, - configured_providers: Vec::new(), - }) + ) .await .unwrap(); (created.persisted, store) @@ -1159,9 +1186,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 [ @@ -1173,50 +1202,57 @@ 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, + configured_providers: Vec::new(), }, - 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, - configured_providers: Vec::new(), - }) + ) .await .unwrap(); @@ -1240,15 +1276,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(); @@ -1268,17 +1307,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(), @@ -1369,51 +1408,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(); diff --git a/lib/crates/fabro-workflow/src/operations/validate.rs b/lib/crates/fabro-workflow/src/operations/validate.rs index 830076609..05597e567 100644 --- a/lib/crates/fabro-workflow/src/operations/validate.rs +++ b/lib/crates/fabro-workflow/src/operations/validate.rs @@ -9,9 +9,9 @@ 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>, } @@ -23,7 +23,7 @@ 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| Error::Parse(err.to_string()))?; 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 79f74a3c3..1d305030d 100644 --- a/lib/crates/fabro-workflow/src/pipeline/execute.rs +++ b/lib/crates/fabro-workflow/src/pipeline/execute.rs @@ -99,8 +99,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()); @@ -315,7 +315,7 @@ pub async fn execute(init: Initialized) -> Executed { 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, }); ( diff --git a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs index 3320d8bc2..666c48ba9 100644 --- a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs +++ b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs @@ -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, } } @@ -197,27 +197,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, }, vault: None, devcontainer: None, @@ -247,45 +247,45 @@ 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, }, - vault: None, - devcontainer: None, - git: None, - worktree_mode: None, - run_control: None, + vault: 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 @@ -327,11 +327,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, @@ -340,10 +340,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, }, vault: None, devcontainer: None, @@ -645,8 +645,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, }); @@ -787,7 +787,7 @@ async fn execute_cancelled_mid_run_persists_cancelled_status() { 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.status, RunStatus::Cancelled); 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 bd9ed3d34..7000b5b63 100644 --- a/lib/crates/fabro-workflow/src/pipeline/finalize.rs +++ b/lib/crates/fabro-workflow/src/pipeline/finalize.rs @@ -37,10 +37,10 @@ pub fn classify_engine_result( let failure_reason = outcome.failure_reason().map(String::from); let (run_status, status_reason) = match status { StageStatus::Success | StageStatus::Skipped => { - (RunStatus::Succeeded, Some(StatusReason::Completed)) + (RunStatus::Completed, Some(StatusReason::Completed)) } StageStatus::PartialSuccess => { - (RunStatus::Succeeded, Some(StatusReason::PartialSuccess)) + (RunStatus::Completed, Some(StatusReason::PartialSuccess)) } StageStatus::Fail | StageStatus::Retry => { (RunStatus::Failed, Some(StatusReason::WorkflowError)) @@ -319,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, } } @@ -366,15 +366,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 dd4c13cca..f41ce10f8 100644 --- a/lib/crates/fabro-workflow/src/pipeline/initialize.rs +++ b/lib/crates/fabro-workflow/src/pipeline/initialize.rs @@ -32,9 +32,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, } @@ -74,10 +74,9 @@ async fn resolve_worktree_plan(options: &mut InitOptions) -> Result { @@ -571,11 +573,11 @@ pub async fn initialize( 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( @@ -844,17 +846,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, } } @@ -890,51 +892,54 @@ 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, + }, + vault: 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, - }, - vault: None, - devcontainer: None, - git: None, - worktree_mode: None, - run_control: None, - registry_override: None, - artifact_sink: None, - checkpoint: None, - seed_context: None, - }) + ) .await .unwrap(); @@ -960,7 +965,7 @@ mod tests { "anthropic", &serde_json::to_string(&AuthCredential { provider: fabro_llm::Provider::Anthropic, - details: AuthDetails::ApiKey { + details: AuthDetails::ApiKey { key: "anthropic-key".to_string(), }, }) @@ -973,11 +978,11 @@ mod tests { let (_, llm_client, effective_dry_run) = build_registry( &LlmSpec { - model: "claude-opus-4-6".to_string(), - provider: fabro_llm::Provider::Anthropic, + model: "claude-opus-4-6".to_string(), + provider: fabro_llm::Provider::Anthropic, fallback_chain: Vec::new(), - mcp_servers: Vec::new(), - dry_run: false, + mcp_servers: Vec::new(), + dry_run: false, }, Arc::new(AutoApproveInterviewer), &HashMap::new(), @@ -1009,47 +1014,50 @@ 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, + }, + vault: 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, - }, - vault: 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; @@ -1074,51 +1082,54 @@ 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, + }, + vault: 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, - }, - vault: 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(Error::Cancelled))); @@ -1135,53 +1146,57 @@ 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, + }, + vault: 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, - }, - vault: 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(Error::Cancelled))); diff --git a/lib/crates/fabro-workflow/src/pipeline/persist.rs b/lib/crates/fabro-workflow/src/pipeline/persist.rs index 523abf0b0..a947b05e4 100644 --- a/lib/crates/fabro-workflow/src/pipeline/persist.rs +++ b/lib/crates/fabro-workflow/src/pipeline/persist.rs @@ -158,23 +158,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 @@ -188,7 +192,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()), }, ) @@ -215,7 +219,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()), }, ) @@ -240,7 +244,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(), }, ) @@ -280,10 +284,13 @@ 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, Error::Io(_))); diff --git a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs index a44cf7b15..edbf3f952 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()); } @@ -622,19 +622,19 @@ mod tests { 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, }) } @@ -650,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 +694,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 +835,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 +1084,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 +1157,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 +1356,7 @@ mod tests { let store = test_store(); let run_store = store.create_run(&fixtures::RUN_1).await.unwrap(); let creds = GitHubCredentials::App(fabro_github::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 +1383,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 c61afe749..af93cc2fd 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, }); } @@ -233,63 +233,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 @@ -297,17 +305,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, } } @@ -326,35 +334,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; @@ -377,21 +388,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 ced3b2571..308f4fff8 100644 --- a/lib/crates/fabro-workflow/src/pipeline/transform.rs +++ b/lib/crates/fabro-workflow/src/pipeline/transform.rs @@ -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 1c69b3fe3..2e3be9711 100644 --- a/lib/crates/fabro-workflow/src/pipeline/types.rs +++ b/lib/crates/fabro-workflow/src/pipeline/types.rs @@ -36,7 +36,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, } @@ -44,7 +44,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, } @@ -53,8 +53,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, } @@ -113,7 +113,7 @@ impl Validated { /// Options for the PERSIST phase. pub(crate) struct PersistOptions { - pub run_dir: PathBuf, + pub run_dir: PathBuf, pub run_record: RunRecord, } @@ -122,11 +122,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 { @@ -212,173 +212,173 @@ impl Persisted { #[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 vault: Option>>, - 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 vault: Option>>, + 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 24036067a..8e784770f 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 { @@ -72,7 +72,7 @@ impl RunInfo { self.summary .as_ref() .and_then(|summary| summary.status) - .unwrap_or(RunStatus::Dead) + .unwrap_or(RunStatus::Failed) } pub fn status_reason(&self) -> Option { @@ -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, } } @@ -435,29 +441,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/run_options.rs b/lib/crates/fabro-workflow/src/run_options.rs index 66beb3518..77e944ce8 100644 --- a/lib/crates/fabro-workflow/src/run_options.rs +++ b/lib/crates/fabro-workflow/src/run_options.rs @@ -12,34 +12,34 @@ use crate::git::{GitAuthor, git_author_from_settings}; /// Git checkpoint options for a workflow run. #[derive(Clone)] pub struct GitCheckpointOptions { - pub base_sha: Option, - pub run_branch: Option, + pub base_sha: Option, + pub run_branch: Option, pub meta_branch: Option, } /// Options for a workflow run. #[derive(Clone)] pub struct RunOptions { - pub settings: SettingsLayer, - pub run_dir: PathBuf, - pub cancel_token: Option>, + pub settings: SettingsLayer, + pub run_dir: PathBuf, + pub cancel_token: Option>, /// Unique identifier for this workflow run. - pub run_id: RunId, + pub run_id: RunId, /// User-defined key-value labels for this run. - pub labels: HashMap, + pub labels: HashMap, /// Workflow directory slug (e.g. "smoke" from `.fabro/workflows/smoke/`). - pub workflow_slug: Option, + pub workflow_slug: Option, /// GitHub credentials for pushing metadata branches to origin. - pub github_app: Option, + pub github_app: Option, /// Host repo path for MetadataStore (shadow commits) and host-side pushes. - pub host_repo_path: Option, + pub host_repo_path: Option, /// Name of the branch the run was started from (for PR base). - pub base_branch: Option, + pub base_branch: Option, /// Base commit SHA to display in lifecycle events/UI even when /// checkpointing is disabled. pub display_base_sha: Option, /// Git checkpoint options; `None` means checkpointing disabled. - pub git: Option, + pub git: Option, } impl RunOptions { @@ -69,9 +69,9 @@ impl RunOptions { /// Options for sandbox lifecycle management within the engine. pub struct LifecycleOptions { /// Setup commands to run inside the sandbox after initialization. - pub setup_commands: Vec, + pub setup_commands: Vec, /// Timeout in milliseconds for each setup command. pub setup_command_timeout_ms: u64, /// Devcontainer lifecycle phases and their commands. - pub devcontainer_phases: Vec<(String, Vec)>, + pub devcontainer_phases: Vec<(String, Vec)>, } diff --git a/lib/crates/fabro-workflow/src/runtime_store.rs b/lib/crates/fabro-workflow/src/runtime_store.rs index d2523be14..90e5b2315 100644 --- a/lib/crates/fabro-workflow/src/runtime_store.rs +++ b/lib/crates/fabro-workflow/src/runtime_store.rs @@ -132,18 +132,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, } } @@ -151,23 +151,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(); @@ -185,20 +189,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 75fff9d4e..1b67f2b0f 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 d2d51b0a0..cd6e684c6 100644 --- a/lib/crates/fabro-workflow/src/test_support.rs +++ b/lib/crates/fabro-workflow/src/test_support.rs @@ -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, } @@ -72,33 +72,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); @@ -159,8 +163,8 @@ pub async fn run_graph( run_options, InitializedOptions { hook_runner: None, - env: HashMap::new(), - checkpoint: None, + env: HashMap::new(), + checkpoint: None, }, ) .await; @@ -183,8 +187,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; @@ -216,8 +220,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; @@ -242,8 +246,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; @@ -274,8 +278,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; @@ -299,8 +303,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; @@ -317,8 +321,8 @@ pub async fn run_graph_from_checkpoint_with_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 fe1df53b2..2941f8f9e 100644 --- a/lib/crates/fabro-workflow/src/transforms/file_inlining.rs +++ b/lib/crates/fabro-workflow/src/transforms/file_inlining.rs @@ -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 { diff --git a/lib/crates/fabro-workflow/src/transforms/import.rs b/lib/crates/fabro-workflow/src/transforms/import.rs index 81b6db2bc..da2a85026 100644 --- a/lib/crates/fabro-workflow/src/transforms/import.rs +++ b/lib/crates/fabro-workflow/src/transforms/import.rs @@ -12,21 +12,21 @@ 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, } diff --git a/lib/crates/fabro-workflow/src/transforms/variable_expansion.rs b/lib/crates/fabro-workflow/src/transforms/variable_expansion.rs index 3549177d3..c65c3db7e 100644 --- a/lib/crates/fabro-workflow/src/transforms/variable_expansion.rs +++ b/lib/crates/fabro-workflow/src/transforms/variable_expansion.rs @@ -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 63ff50152..421680bbf 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 dc41f9e49..e22bcb74c 100644 --- a/lib/crates/fabro-workflow/tests/it/daytona_integration.rs +++ b/lib/crates/fabro-workflow/tests/it/daytona_integration.rs @@ -360,10 +360,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(), )), @@ -510,17 +510,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) @@ -688,19 +688,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, }), }; @@ -870,8 +870,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, }), }; @@ -989,10 +989,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() @@ -1219,8 +1219,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), }), }; @@ -1342,7 +1342,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()], @@ -1351,16 +1351,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) @@ -1620,8 +1620,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, }), }; @@ -1824,11 +1824,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 @@ -1902,10 +1902,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, @@ -2056,10 +2056,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, @@ -2101,8 +2101,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(), @@ -2112,11 +2112,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 @@ -2169,10 +2169,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 72b508206..2c76be5ac 100644 --- a/lib/crates/fabro-workflow/tests/it/git_integration.rs +++ b/lib/crates/fabro-workflow/tests/it/git_integration.rs @@ -150,17 +150,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, } } @@ -280,8 +280,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 5a30a0a4c..6d2ab2a16 100644 --- a/lib/crates/fabro-workflow/tests/it/integration.rs +++ b/lib/crates/fabro-workflow/tests/it/integration.rs @@ -350,17 +350,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) @@ -479,17 +479,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) @@ -584,9 +584,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)); @@ -598,17 +598,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) @@ -693,17 +693,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) @@ -803,17 +803,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) @@ -916,17 +916,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!( @@ -1036,17 +1036,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) @@ -1350,17 +1350,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) @@ -1424,17 +1424,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) @@ -1515,13 +1515,13 @@ impl CodergenBackend for MockCodergenBackend { _tool_hooks: Option>, ) -> 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, }) } @@ -1587,7 +1587,7 @@ impl Handler for LargeOutputHandler { #[derive(Clone)] struct ContextValueCaptureHandler { values: Arc>>, - key: String, + key: String, } #[async_trait::async_trait] @@ -1770,17 +1770,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) @@ -1871,17 +1871,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) @@ -1983,17 +1983,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) @@ -2081,17 +2081,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 @@ -2123,17 +2123,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) @@ -2161,17 +2161,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"); @@ -2240,17 +2240,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) @@ -2295,17 +2295,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) @@ -2369,17 +2369,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) @@ -2408,17 +2408,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) @@ -2512,17 +2512,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) @@ -2578,14 +2578,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)); @@ -2597,17 +2597,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) @@ -2661,17 +2661,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) @@ -2745,17 +2745,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) @@ -2831,17 +2831,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) @@ -2895,17 +2895,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); @@ -2962,17 +2962,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) @@ -3023,17 +3023,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) @@ -3132,17 +3132,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) @@ -3213,17 +3213,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) @@ -3353,17 +3353,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) @@ -3408,17 +3408,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) @@ -3457,17 +3457,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) @@ -3498,17 +3498,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) @@ -3558,17 +3558,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) @@ -3604,17 +3604,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); @@ -3659,17 +3659,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) @@ -3736,17 +3736,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) @@ -3827,17 +3827,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) @@ -3960,17 +3960,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) @@ -4035,17 +4035,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); @@ -4094,14 +4094,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 @@ -4138,17 +4141,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) @@ -4210,7 +4213,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 { @@ -4218,7 +4221,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())), } } } @@ -4292,17 +4295,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"); @@ -4348,17 +4351,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"); @@ -4400,17 +4403,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"); @@ -4458,17 +4461,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"); @@ -4506,17 +4509,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"); @@ -4564,17 +4567,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"); @@ -4635,17 +4638,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"); @@ -4701,17 +4704,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"); @@ -4767,17 +4770,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"); @@ -4826,17 +4829,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"); @@ -4896,17 +4899,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"); @@ -4976,17 +4979,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) @@ -5072,17 +5075,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) @@ -5155,17 +5158,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) @@ -5196,17 +5199,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) @@ -5288,17 +5291,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"); @@ -5355,17 +5358,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"); @@ -5429,17 +5432,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) @@ -5495,17 +5498,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) @@ -5566,17 +5569,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"); @@ -5619,17 +5622,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"); @@ -5675,17 +5678,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"); @@ -5732,17 +5735,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"); @@ -5799,17 +5802,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"); @@ -5847,17 +5850,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) @@ -5919,17 +5922,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"); @@ -6005,17 +6008,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) @@ -6056,8 +6059,8 @@ mod real_llm { use fabro_workflow::handler::agent::{AgentHandler, CodergenBackend, CodergenResult}; struct LlmCodergenBackend { - client: Arc, - model: String, + client: Arc, + model: String, provider: String, } @@ -6089,19 +6092,19 @@ mod real_llm { impl LlmCodergenBackend { 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 @@ -6110,9 +6113,9 @@ mod real_llm { .await .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, }) } @@ -6241,17 +6244,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), @@ -6349,17 +6352,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), @@ -6481,17 +6484,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), @@ -6581,17 +6584,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), @@ -6674,17 +6677,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) @@ -6790,9 +6793,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)); @@ -6804,17 +6807,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) @@ -6920,17 +6923,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) @@ -7031,9 +7034,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))); @@ -7047,17 +7050,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) @@ -7139,9 +7142,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))); @@ -7155,17 +7158,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) @@ -7391,7 +7394,7 @@ fn hook_runner_from_defs(hooks: Vec) -> Arc, + emitter: Arc, hook_runner: Arc, } @@ -7437,7 +7440,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), } } @@ -7457,17 +7460,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, } } @@ -8052,26 +8055,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), }], }; @@ -8087,26 +8090,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), }], }; @@ -8395,17 +8398,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) @@ -8596,17 +8599,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) @@ -8655,16 +8658,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()), } } @@ -8800,17 +8803,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) @@ -8881,23 +8884,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) @@ -8967,24 +8970,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) @@ -9104,17 +9107,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) @@ -9149,25 +9152,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(), } } @@ -9254,10 +9257,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, }); } @@ -9265,10 +9268,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, }); } @@ -9276,10 +9279,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, }); } @@ -9287,10 +9290,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, }); } @@ -9298,20 +9301,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, }) } @@ -9547,48 +9550,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, }) } @@ -9974,17 +9977,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) @@ -10092,17 +10095,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) @@ -10287,19 +10290,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, }), }; @@ -10465,8 +10468,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), }), }; @@ -10664,8 +10667,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, }), }; @@ -10902,19 +10905,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, }), }; @@ -11043,7 +11046,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] @@ -11272,17 +11275,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"); @@ -11318,17 +11321,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()); @@ -11357,17 +11360,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()); @@ -11403,17 +11406,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()); @@ -11442,17 +11445,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!( @@ -11504,17 +11507,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. @@ -11567,17 +11570,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(); @@ -11618,23 +11621,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); @@ -11751,17 +11754,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()); @@ -11811,23 +11814,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!( @@ -11912,17 +11915,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!( @@ -11946,8 +11949,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 { @@ -12009,17 +12012,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!( @@ -12048,17 +12051,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!( @@ -12087,17 +12090,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!( @@ -12126,17 +12129,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"); @@ -12162,17 +12165,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!( @@ -12202,17 +12205,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!( @@ -12247,7 +12250,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] @@ -12264,12 +12267,12 @@ impl Handler for KeepaliveHandler { 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()) @@ -12309,17 +12312,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"); @@ -12358,23 +12361,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) @@ -12409,17 +12412,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) @@ -12474,17 +12477,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; @@ -12606,7 +12609,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()], @@ -12615,16 +12618,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) @@ -12738,7 +12741,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()], @@ -12747,16 +12750,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) @@ -12843,7 +12846,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()], @@ -12852,16 +12855,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) @@ -12919,17 +12922,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/lib/packages/fabro-api-client/src/models/blocked-reason.ts b/lib/packages/fabro-api-client/src/models/blocked-reason.ts new file mode 100644 index 000000000..91e6af9f9 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/blocked-reason.ts @@ -0,0 +1,27 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * Reason why a run is blocked. + */ + +export const BlockedReason = { + HUMAN_INPUT_REQUIRED: 'human_input_required' +} as const; + +export type BlockedReason = typeof BlockedReason[keyof typeof BlockedReason]; + + diff --git a/lib/packages/fabro-api-client/src/models/board-column.ts b/lib/packages/fabro-api-client/src/models/board-column.ts index 24385ce4b..350f695c4 100644 --- a/lib/packages/fabro-api-client/src/models/board-column.ts +++ b/lib/packages/fabro-api-client/src/models/board-column.ts @@ -20,7 +20,7 @@ export const BoardColumn = { WORKING: 'working', - INITIALIZING: 'initializing', + BLOCKED: 'blocked', REVIEW: 'review', MERGE: 'merge' } as const; @@ -28,4 +28,3 @@ export const BoardColumn = { export type BoardColumn = typeof BoardColumn[keyof typeof BoardColumn]; - diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index ef42d8cb7..c79550ea3 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -13,6 +13,7 @@ export * from './assistant-stage-turn'; export * from './billed-token-counts'; export * from './billing-by-model'; export * from './billing-stage-ref'; +export * from './blocked-reason'; export * from './board-column'; export * from './check-run'; export * from './check-run-status'; @@ -143,4 +144,4 @@ export * from './tool-use'; export * from './user-response'; export * from './workflow-diagnostic'; export * from './workflow-reference'; -export * from './write-blob-response'; +export * from './write-blob-response'; \ No newline at end of file diff --git a/lib/packages/fabro-api-client/src/models/internal-run-status.ts b/lib/packages/fabro-api-client/src/models/internal-run-status.ts index e54bd35da..c55b1755d 100644 --- a/lib/packages/fabro-api-client/src/models/internal-run-status.ts +++ b/lib/packages/fabro-api-client/src/models/internal-run-status.ts @@ -20,16 +20,17 @@ export const InternalRunStatus = { SUBMITTED: 'submitted', + QUEUED: 'queued', STARTING: 'starting', RUNNING: 'running', + BLOCKED: 'blocked', PAUSED: 'paused', REMOVING: 'removing', - SUCCEEDED: 'succeeded', + COMPLETED: 'completed', FAILED: 'failed', - DEAD: 'dead' + CANCELLED: 'cancelled' } as const; export type InternalRunStatus = typeof InternalRunStatus[keyof typeof InternalRunStatus]; - diff --git a/lib/packages/fabro-api-client/src/models/run-status-record.ts b/lib/packages/fabro-api-client/src/models/run-status-record.ts index 8a74e7576..b2258b5e9 100644 --- a/lib/packages/fabro-api-client/src/models/run-status-record.ts +++ b/lib/packages/fabro-api-client/src/models/run-status-record.ts @@ -13,6 +13,9 @@ */ +// May contain unused imports in some cases +// @ts-ignore +import type { BlockedReason } from './blocked-reason'; // May contain unused imports in some cases // @ts-ignore import type { InternalRunStatus } from './internal-run-status'; @@ -26,8 +29,8 @@ import type { StatusReason } from './status-reason'; export interface RunStatusRecord { 'status': InternalRunStatus; 'reason'?: StatusReason | null; + 'blocked_reason'?: BlockedReason | null; 'updated_at': string; } - diff --git a/lib/packages/fabro-api-client/src/models/run-status-response.ts b/lib/packages/fabro-api-client/src/models/run-status-response.ts index 2c7c0a17c..e5f6ee53b 100644 --- a/lib/packages/fabro-api-client/src/models/run-status-response.ts +++ b/lib/packages/fabro-api-client/src/models/run-status-response.ts @@ -13,6 +13,9 @@ */ +// May contain unused imports in some cases +// @ts-ignore +import type { BlockedReason } from './blocked-reason'; // May contain unused imports in some cases // @ts-ignore import type { RunControlAction } from './run-control-action'; @@ -41,6 +44,7 @@ export interface RunStatusResponse { */ 'queue_position'?: number; 'status_reason'?: StatusReason; + 'blocked_reason'?: BlockedReason; 'pending_control'?: RunControlAction; /** * Timestamp when the run was created. @@ -49,4 +53,3 @@ export interface RunStatusResponse { } - diff --git a/lib/packages/fabro-api-client/src/models/run-status.ts b/lib/packages/fabro-api-client/src/models/run-status.ts index 61b235092..22899df56 100644 --- a/lib/packages/fabro-api-client/src/models/run-status.ts +++ b/lib/packages/fabro-api-client/src/models/run-status.ts @@ -23,13 +23,13 @@ export const RunStatus = { QUEUED: 'queued', STARTING: 'starting', RUNNING: 'running', + BLOCKED: 'blocked', + PAUSED: 'paused', COMPLETED: 'completed', FAILED: 'failed', - CANCELLED: 'cancelled', - PAUSED: 'paused' + CANCELLED: 'cancelled' } as const; export type RunStatus = typeof RunStatus[keyof typeof RunStatus]; - 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 5eca78d4f..a58a1f2d6 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 1794c4065..e9756da4f 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, fabro_http::HttpClient, 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 5dcbcbb76..6e93ddf17 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, fabro_http::HttpClient, 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 21764dd50..d7494e53c 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. @@ -283,29 +283,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( @@ -362,12 +365,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 } @@ -477,12 +483,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 af1f7c93e..1437ff407 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: fabro_http::StatusCode, + pub status: fabro_http::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: fabro_http::StatusCode, + pub status: fabro_http::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 a748ecda1..ec0a035b8 100644 --- a/test/twin/openai/tests/debug_ui.rs +++ b/test/twin/openai/tests/debug_ui.rs @@ -225,7 +225,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 ac971d9ab..54ec90519 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 4839db86b..1ea00b2cd 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\\\"}\""));