From 91b96ae6b31466716b03962dceca724ac59f48d0 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sat, 11 Apr 2026 13:43:30 -0400 Subject: [PATCH] fmt: apply nightly rustfmt after merge Restore a clean nightly rustfmt baseline on the merged main branch so cargo +nightly fmt --check --all passes again after bringing in origin/main. --- lib/crates/fabro-agent/src/compaction.rs | 113 +- lib/crates/fabro-agent/src/config.rs | 9 +- lib/crates/fabro-agent/src/error.rs | 18 +- lib/crates/fabro-agent/src/event.rs | 59 +- lib/crates/fabro-agent/src/file_tracker.rs | 4 +- lib/crates/fabro-agent/src/history.rs | 194 +- lib/crates/fabro-agent/src/loop_detection.rs | 16 +- lib/crates/fabro-agent/src/mcp_integration.rs | 14 +- .../fabro-agent/src/profiles/anthropic.rs | 12 +- lib/crates/fabro-agent/src/profiles/mod.rs | 26 +- lib/crates/fabro-agent/src/session.rs | 259 +- lib/crates/fabro-agent/src/skills.rs | 43 +- lib/crates/fabro-agent/src/subagent.rs | 83 +- lib/crates/fabro-agent/src/test_support.rs | 74 +- lib/crates/fabro-agent/src/tool_execution.rs | 99 +- lib/crates/fabro-agent/src/tool_registry.rs | 24 +- lib/crates/fabro-agent/src/tools.rs | 325 +-- lib/crates/fabro-agent/src/types.rs | 277 +- lib/crates/fabro-agent/src/v4a_patch.rs | 114 +- .../fabro-agent/tests/it/parity_matrix.rs | 10 +- lib/crates/fabro-checkpoint/src/author.rs | 6 +- lib/crates/fabro-checkpoint/src/branch.rs | 12 +- lib/crates/fabro-checkpoint/src/error.rs | 2 +- lib/crates/fabro-checkpoint/src/git.rs | 8 +- lib/crates/fabro-checkpoint/src/metadata.rs | 17 +- lib/crates/fabro-checkpoint/src/trailer.rs | 71 +- lib/crates/fabro-cli/src/args.rs | 28 +- lib/crates/fabro-cli/src/command_context.rs | 12 +- .../fabro-cli/src/commands/artifact/cp.rs | 8 +- .../fabro-cli/src/commands/artifact/mod.rs | 8 +- lib/crates/fabro-cli/src/commands/doctor.rs | 165 +- lib/crates/fabro-cli/src/commands/exec.rs | 14 +- lib/crates/fabro-cli/src/commands/graph.rs | 16 +- lib/crates/fabro-cli/src/commands/install.rs | 2 +- lib/crates/fabro-cli/src/commands/model.rs | 48 +- lib/crates/fabro-cli/src/commands/pr/list.rs | 6 +- .../fabro-cli/src/commands/preflight.rs | 12 +- .../fabro-cli/src/commands/run/attach.rs | 16 +- lib/crates/fabro-cli/src/commands/run/cp.rs | 16 +- .../fabro-cli/src/commands/run/create.rs | 2 +- lib/crates/fabro-cli/src/commands/run/fork.rs | 13 +- .../fabro-cli/src/commands/run/output.rs | 22 +- .../fabro-cli/src/commands/run/overrides.rs | 8 +- .../fabro-cli/src/commands/run/rewind.rs | 25 +- .../src/commands/run/run_progress/event.rs | 287 +- .../src/commands/run/run_progress/mod.rs | 716 ++--- .../src/commands/run/run_progress/renderer.rs | 6 +- .../run/run_progress/stage_display.rs | 43 +- .../fabro-cli/src/commands/run/runner.rs | 48 +- lib/crates/fabro-cli/src/commands/run/wait.rs | 66 +- .../fabro-cli/src/commands/runs/inspect.rs | 24 +- .../src/commands/server/foreground.rs | 15 +- .../fabro-cli/src/commands/server/record.rs | 8 +- .../fabro-cli/src/commands/server/start.rs | 31 +- .../fabro-cli/src/commands/store/dump.rs | 413 ++- .../fabro-cli/src/commands/system/prune.rs | 10 +- .../fabro-cli/src/commands/uninstall.rs | 34 +- lib/crates/fabro-cli/src/commands/upgrade.rs | 12 +- lib/crates/fabro-cli/src/commands/validate.rs | 12 +- lib/crates/fabro-cli/src/server_client.rs | 28 +- lib/crates/fabro-cli/src/server_runs.rs | 6 +- lib/crates/fabro-cli/src/shared/openai_jwt.rs | 4 +- lib/crates/fabro-cli/src/user_config.rs | 24 +- lib/crates/fabro-cli/tests/it/cmd/config.rs | 15 +- lib/crates/fabro-cli/tests/it/cmd/fork.rs | 16 +- lib/crates/fabro-cli/tests/it/cmd/logs.rs | 38 +- lib/crates/fabro-cli/tests/it/cmd/pr_view.rs | 12 +- lib/crates/fabro-cli/tests/it/cmd/resume.rs | 16 +- lib/crates/fabro-cli/tests/it/cmd/rewind.rs | 8 +- lib/crates/fabro-cli/tests/it/cmd/support.rs | 57 +- .../fabro-config/src/effective_settings.rs | 6 +- lib/crates/fabro-config/src/error.rs | 14 +- lib/crates/fabro-config/src/merge.rs | 168 +- lib/crates/fabro-config/src/parse.rs | 2 +- lib/crates/fabro-config/src/project.rs | 12 +- lib/crates/fabro-config/src/resolve/cli.rs | 22 +- lib/crates/fabro-config/src/resolve/mod.rs | 10 +- .../fabro-config/src/resolve/project.rs | 6 +- lib/crates/fabro-config/src/resolve/run.rs | 108 +- lib/crates/fabro-config/src/resolve/server.rs | 40 +- .../fabro-config/src/resolve/workflow.rs | 6 +- lib/crates/fabro-config/src/run.rs | 4 +- lib/crates/fabro-core/src/error.rs | 32 +- lib/crates/fabro-core/src/executor.rs | 142 +- lib/crates/fabro-core/src/graph.rs | 2 +- lib/crates/fabro-core/src/lifecycle.rs | 84 +- lib/crates/fabro-core/src/retry.rs | 4 +- lib/crates/fabro-core/src/stall.rs | 12 +- lib/crates/fabro-core/src/state.rs | 32 +- lib/crates/fabro-core/src/test_fixtures.rs | 66 +- lib/crates/fabro-devcontainer/src/compose.rs | 12 +- .../fabro-devcontainer/src/dockerfile.rs | 4 +- lib/crates/fabro-devcontainer/src/features.rs | 492 ++-- lib/crates/fabro-devcontainer/src/lib.rs | 30 +- lib/crates/fabro-devcontainer/src/types.rs | 25 +- lib/crates/fabro-github/src/lib.rs | 76 +- lib/crates/fabro-github/tests/integration.rs | 12 +- lib/crates/fabro-graphviz/src/condition.rs | 47 +- lib/crates/fabro-graphviz/src/parser/ast.rs | 12 +- .../fabro-graphviz/src/parser/grammar.rs | 13 +- .../fabro-graphviz/src/parser/semantic.rs | 50 +- lib/crates/fabro-graphviz/src/stylesheet.rs | 12 +- lib/crates/fabro-hooks/src/bridge.rs | 35 +- lib/crates/fabro-hooks/src/config.rs | 26 +- lib/crates/fabro-hooks/src/executor.rs | 102 +- lib/crates/fabro-hooks/src/runner.rs | 8 +- lib/crates/fabro-hooks/src/types.rs | 78 +- .../fabro-interview/src/auto_approve.rs | 10 +- lib/crates/fabro-interview/src/console.rs | 28 +- lib/crates/fabro-interview/src/control.rs | 4 +- .../fabro-interview/src/control_protocol.rs | 14 +- lib/crates/fabro-interview/src/lib.rs | 64 +- lib/crates/fabro-interview/src/recording.rs | 2 +- lib/crates/fabro-llm/src/client.rs | 52 +- lib/crates/fabro-llm/src/error.rs | 364 +-- lib/crates/fabro-llm/src/generate.rs | 502 ++-- lib/crates/fabro-llm/src/model_test.rs | 38 +- .../fabro-llm/src/providers/anthropic.rs | 377 +-- lib/crates/fabro-llm/src/providers/common.rs | 18 +- .../fabro-llm/src/providers/fabro_server.rs | 38 +- lib/crates/fabro-llm/src/providers/gemini.rs | 259 +- .../fabro-llm/src/providers/http_api.rs | 10 +- lib/crates/fabro-llm/src/providers/openai.rs | 214 +- .../src/providers/openai_compatible.rs | 298 +- lib/crates/fabro-llm/src/retry.rs | 34 +- lib/crates/fabro-llm/src/tools.rs | 8 +- lib/crates/fabro-llm/src/types.rs | 363 ++- lib/crates/fabro-llm/tests/integration.rs | 48 +- lib/crates/fabro-mcp/src/client.rs | 4 +- .../fabro-mcp/src/connection_manager.rs | 25 +- .../fabro-mcp/tests/stdio_integration.rs | 8 +- lib/crates/fabro-model/src/billing.rs | 126 +- lib/crates/fabro-model/src/catalog.rs | 54 +- lib/crates/fabro-model/src/model_ref.rs | 11 +- lib/crates/fabro-model/src/provider.rs | 14 +- lib/crates/fabro-model/src/types.rs | 34 +- lib/crates/fabro-oauth/src/lib.rs | 14 +- lib/crates/fabro-proc/src/title.rs | 2 +- lib/crates/fabro-retro/src/retro.rs | 32 +- lib/crates/fabro-retro/src/retro_agent.rs | 12 +- lib/crates/fabro-sandbox/src/config.rs | 16 +- lib/crates/fabro-sandbox/src/daytona/mod.rs | 78 +- lib/crates/fabro-sandbox/src/docker.rs | 56 +- lib/crates/fabro-sandbox/src/local.rs | 28 +- lib/crates/fabro-sandbox/src/read_guard.rs | 2 +- lib/crates/fabro-sandbox/src/sandbox.rs | 92 +- lib/crates/fabro-sandbox/src/sandbox_spec.rs | 22 +- lib/crates/fabro-sandbox/src/test_support.rs | 84 +- lib/crates/fabro-sandbox/src/worktree.rs | 66 +- lib/crates/fabro-server/src/demo/mod.rs | 710 ++--- lib/crates/fabro-server/src/diagnostics.rs | 178 +- lib/crates/fabro-server/src/error.rs | 2 +- .../fabro-server/src/github_webhooks.rs | 2 +- lib/crates/fabro-server/src/jwt_auth.rs | 57 +- lib/crates/fabro-server/src/run_manifest.rs | 239 +- lib/crates/fabro-server/src/secret_store.rs | 10 +- lib/crates/fabro-server/src/serve.rs | 48 +- lib/crates/fabro-server/src/server.rs | 606 ++-- lib/crates/fabro-server/src/web_auth.rs | 88 +- lib/crates/fabro-server/tests/it/api/mtls.rs | 41 +- .../tests/it/scenario/lifecycle.rs | 11 +- lib/crates/fabro-slack/src/blocks.rs | 14 +- lib/crates/fabro-slack/src/client.rs | 16 +- lib/crates/fabro-slack/src/config.rs | 2 +- lib/crates/fabro-slack/src/dispatch.rs | 38 +- lib/crates/fabro-slack/src/interaction.rs | 4 +- lib/crates/fabro-slack/src/payload.rs | 20 +- lib/crates/fabro-slack/src/socket.rs | 24 +- lib/crates/fabro-slack/src/threads.rs | 13 +- lib/crates/fabro-store/src/artifact_store.rs | 22 +- lib/crates/fabro-store/src/lib.rs | 2 +- lib/crates/fabro-store/src/run_state.rs | 174 +- lib/crates/fabro-store/src/slate/mod.rs | 8 +- lib/crates/fabro-store/src/slate/run_store.rs | 24 +- lib/crates/fabro-store/src/types.rs | 100 +- lib/crates/fabro-telemetry/src/buffer.rs | 20 +- lib/crates/fabro-telemetry/src/event.rs | 44 +- lib/crates/fabro-telemetry/src/lib.rs | 8 +- lib/crates/fabro-telemetry/src/sender.rs | 16 +- lib/crates/fabro-template/src/lib.rs | 14 +- lib/crates/fabro-test/src/lib.rs | 112 +- lib/crates/fabro-tracker/src/github.rs | 40 +- lib/crates/fabro-tracker/src/lib.rs | 30 +- lib/crates/fabro-tracker/src/linear.rs | 38 +- lib/crates/fabro-types/src/checkpoint.rs | 20 +- lib/crates/fabro-types/src/conclusion.rs | 22 +- lib/crates/fabro-types/src/graph.rs | 20 +- lib/crates/fabro-types/src/interview.rs | 12 +- lib/crates/fabro-types/src/node_status.rs | 6 +- lib/crates/fabro-types/src/outcome.rs | 50 +- lib/crates/fabro-types/src/pull_request.rs | 10 +- lib/crates/fabro-types/src/retro.rs | 66 +- lib/crates/fabro-types/src/run.rs | 32 +- lib/crates/fabro-types/src/run_event/agent.rs | 90 +- lib/crates/fabro-types/src/run_event/infra.rs | 94 +- lib/crates/fabro-types/src/run_event/misc.rs | 144 +- lib/crates/fabro-types/src/run_event/mod.rs | 197 +- lib/crates/fabro-types/src/run_event/run.rs | 70 +- lib/crates/fabro-types/src/run_event/stage.rs | 26 +- lib/crates/fabro-types/src/sandbox_record.rs | 8 +- lib/crates/fabro-types/src/settings/cli.rs | 44 +- .../fabro-types/src/settings/duration.rs | 2 +- lib/crates/fabro-types/src/settings/interp.rs | 20 +- lib/crates/fabro-types/src/settings/layer.rs | 10 +- .../fabro-types/src/settings/model_ref.rs | 54 +- .../fabro-types/src/settings/project.rs | 12 +- .../fabro-types/src/settings/resolved.rs | 8 +- lib/crates/fabro-types/src/settings/run.rs | 338 +-- lib/crates/fabro-types/src/settings/server.rs | 160 +- lib/crates/fabro-types/src/settings/size.rs | 2 +- .../fabro-types/src/settings/workflow.rs | 12 +- lib/crates/fabro-types/src/stage_id.rs | 15 +- lib/crates/fabro-types/src/start.rs | 4 +- lib/crates/fabro-types/src/status.rs | 6 +- .../fabro-types/tests/run_event_serde.rs | 26 +- .../fabro-types/tests/run_record_serde.rs | 22 +- lib/crates/fabro-util/build.rs | 18 +- lib/crates/fabro-util/src/backoff.rs | 42 +- lib/crates/fabro-util/src/check_report.rs | 66 +- lib/crates/fabro-util/src/redact/gitleaks.rs | 2 +- lib/crates/fabro-util/src/redact/mod.rs | 2 +- lib/crates/fabro-util/src/run_log.rs | 8 +- lib/crates/fabro-util/src/terminal.rs | 24 +- lib/crates/fabro-validate/src/lib.rs | 20 +- lib/crates/fabro-validate/src/rules.rs | 279 +- lib/crates/fabro-workflow/src/artifact.rs | 24 +- .../fabro-workflow/src/artifact_snapshot.rs | 74 +- .../fabro-workflow/src/devcontainer_bridge.rs | 74 +- lib/crates/fabro-workflow/src/error.rs | 78 +- lib/crates/fabro-workflow/src/event.rs | 1225 ++++---- .../fabro-workflow/src/file_resolver.rs | 2 +- lib/crates/fabro-workflow/src/git.rs | 178 +- lib/crates/fabro-workflow/src/graph.rs | 2 +- .../fabro-workflow/src/graph/routing.rs | 2 +- .../fabro-workflow/src/handler/agent.rs | 77 +- .../fabro-workflow/src/handler/command.rs | 56 +- .../fabro-workflow/src/handler/fan_in.rs | 48 +- .../fabro-workflow/src/handler/human.rs | 32 +- .../fabro-workflow/src/handler/llm/api.rs | 70 +- .../fabro-workflow/src/handler/llm/cli.rs | 62 +- .../src/handler/llm/preamble.rs | 15 +- .../src/handler/manager_loop.rs | 34 +- lib/crates/fabro-workflow/src/handler/mod.rs | 44 +- .../fabro-workflow/src/handler/parallel.rs | 74 +- .../fabro-workflow/src/handler/prompt.rs | 42 +- lib/crates/fabro-workflow/src/lib.rs | 16 +- .../fabro-workflow/src/lifecycle/artifact.rs | 44 +- .../src/lifecycle/circuit_breaker.rs | 4 +- .../fabro-workflow/src/lifecycle/event.rs | 92 +- .../fabro-workflow/src/lifecycle/fidelity.rs | 10 +- .../fabro-workflow/src/lifecycle/git.rs | 38 +- .../fabro-workflow/src/lifecycle/hook.rs | 8 +- .../fabro-workflow/src/lifecycle/mod.rs | 58 +- lib/crates/fabro-workflow/src/node_handler.rs | 16 +- .../fabro-workflow/src/operations/create.rs | 414 ++- .../fabro-workflow/src/operations/fork.rs | 29 +- .../src/operations/rebuild_meta.rs | 182 +- .../fabro-workflow/src/operations/rewind.rs | 61 +- .../fabro-workflow/src/operations/source.rs | 24 +- .../fabro-workflow/src/operations/start.rs | 567 ++-- .../fabro-workflow/src/operations/validate.rs | 8 +- lib/crates/fabro-workflow/src/outcome.rs | 16 +- .../fabro-workflow/src/pipeline/execute.rs | 6 +- .../src/pipeline/execute/tests.rs | 114 +- .../fabro-workflow/src/pipeline/finalize.rs | 41 +- .../fabro-workflow/src/pipeline/initialize.rs | 413 ++- .../fabro-workflow/src/pipeline/persist.rs | 55 +- .../src/pipeline/pull_request.rs | 476 ++- .../fabro-workflow/src/pipeline/retro.rs | 231 +- .../fabro-workflow/src/pipeline/transform.rs | 66 +- .../fabro-workflow/src/pipeline/types.rs | 218 +- .../fabro-workflow/src/pipeline/validate.rs | 15 +- lib/crates/fabro-workflow/src/retry.rs | 16 +- lib/crates/fabro-workflow/src/run_control.rs | 2 +- lib/crates/fabro-workflow/src/run_dump.rs | 14 +- lib/crates/fabro-workflow/src/run_lookup.rs | 124 +- .../fabro-workflow/src/runtime_store.rs | 86 +- lib/crates/fabro-workflow/src/sandbox_git.rs | 16 +- lib/crates/fabro-workflow/src/test_support.rs | 92 +- .../src/transforms/file_inlining.rs | 2 +- .../fabro-workflow/src/transforms/import.rs | 16 +- .../src/transforms/variable_expansion.rs | 4 +- .../fabro-workflow/src/workflow_bundle.rs | 6 +- .../fabro-workflow/tests/it/cp_integration.rs | 16 +- .../tests/it/daytona_integration.rs | 132 +- .../tests/it/git_integration.rs | 24 +- .../fabro-workflow/tests/it/integration.rs | 2565 ++++++++--------- test/twin/github/src/fixtures.rs | 235 +- test/twin/github/src/handlers/app.rs | 40 +- test/twin/github/src/handlers/branches.rs | 20 +- test/twin/github/src/handlers/graphql.rs | 150 +- .../twin/github/src/handlers/installations.rs | 50 +- test/twin/github/src/handlers/manifests.rs | 24 +- test/twin/github/src/handlers/pulls.rs | 10 +- test/twin/github/src/server.rs | 6 +- test/twin/github/src/state.rs | 212 +- test/twin/openai/src/config.rs | 4 +- test/twin/openai/src/engine/failures.rs | 14 +- test/twin/openai/src/engine/mod.rs | 20 +- test/twin/openai/src/engine/plan.rs | 20 +- test/twin/openai/src/engine/scenario.rs | 66 +- test/twin/openai/src/logs.rs | 8 +- test/twin/openai/src/openai/models.rs | 74 +- test/twin/openai/src/state.rs | 46 +- test/twin/openai/tests/common/mod.rs | 28 +- test/twin/openai/tests/debug_ui.rs | 2 +- .../twin/openai/tests/live_openai_contract.rs | 8 +- test/twin/openai/tests/responses_contract.rs | 58 +- .../openai/tests/tool_and_schema_contract.rs | 25 +- 309 files changed, 11602 insertions(+), 12496 deletions(-) diff --git a/lib/crates/fabro-agent/src/compaction.rs b/lib/crates/fabro-agent/src/compaction.rs index 3f245266a..30f1a383a 100644 --- a/lib/crates/fabro-agent/src/compaction.rs +++ b/lib/crates/fabro-agent/src/compaction.rs @@ -28,21 +28,18 @@ 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 @@ -66,13 +63,10 @@ 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 { @@ -108,24 +102,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, }; @@ -147,15 +141,12 @@ 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(()) } @@ -268,27 +259,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(), @@ -307,11 +298,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(), @@ -326,7 +317,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 @@ -348,7 +339,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 344694361..6e2f626b6 100644 --- a/lib/crates/fabro-agent/src/config.rs +++ b/lib/crates/fabro-agent/src/config.rs @@ -216,12 +216,9 @@ 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 13ce6aec9..be5bbf05e 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 a7a0f14c6..c2495f25a 100644 --- a/lib/crates/fabro-agent/src/event.rs +++ b/lib/crates/fabro-agent/src/event.rs @@ -54,22 +54,16 @@ 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); } @@ -79,12 +73,9 @@ 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!( @@ -114,12 +105,9 @@ 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] @@ -134,24 +122,21 @@ 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 ed8cc04fd..10d6e274b 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 cf439e20c..74593dbc2 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 2bb0d6554..926bd2a3c 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 10daffc8a..65d8a3fef 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 4995ebe04..f50eec2ba 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,13 +103,11 @@ 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 @@ -157,22 +155,18 @@ 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(), + }); } } } @@ -222,10 +216,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) => { @@ -234,13 +228,11 @@ 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, + }); } } } @@ -423,12 +415,9 @@ 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); } @@ -633,33 +622,29 @@ 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(); @@ -671,23 +656,19 @@ 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; } @@ -715,16 +696,13 @@ 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() }; @@ -804,7 +782,7 @@ impl Session { self.event_emitter.emit( self.id.clone(), AgentEvent::AssistantOutputReplace { - text: String::new(), + text: String::new(), reasoning: None, }, ); @@ -818,7 +796,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, })); }; @@ -844,15 +822,13 @@ 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; @@ -942,12 +918,9 @@ 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}")), + }); } } } @@ -962,7 +935,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 @@ -1040,7 +1013,7 @@ mod tests { } struct ScriptedStreamProvider { - calls: Vec, + calls: Vec, call_index: AtomicUsize, } @@ -1089,7 +1062,7 @@ mod tests { async fn complete(&self, _request: &Request) -> Result { Err(LlmError::Configuration { message: "ScriptedStreamProvider does not implement complete()".into(), - source: None, + source: None, }) } @@ -1471,11 +1444,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(); @@ -1524,7 +1497,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")), }, }); @@ -1725,9 +1698,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"} @@ -1735,7 +1708,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()) }) }), }); @@ -1766,9 +1739,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"} @@ -1776,7 +1749,7 @@ mod tests { "required": ["text"] }), }, - executor: Arc::new(|_args, _ctx| { + executor: Arc::new(|_args, _ctx| { Box::pin(async move { Ok("tool executed".to_string()) }) }), }); @@ -2088,9 +2061,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; @@ -2173,22 +2146,19 @@ 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") @@ -2237,15 +2207,12 @@ 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"); } @@ -2338,7 +2305,7 @@ mod tests { // provider that errors on complete() but succeeds on stream(). struct StreamOnlyProvider { - responses: Vec, + responses: Vec, call_index: AtomicUsize, } @@ -2351,7 +2318,7 @@ mod tests { async fn complete(&self, _request: &Request) -> Result { Err(LlmError::Stream { message: "summarization failed".into(), - source: None, + source: None, }) } @@ -2432,8 +2399,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>, } @@ -2462,11 +2429,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()) }) }), }; @@ -2575,13 +2542,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() @@ -2687,11 +2654,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 6c48f38d9..a4d00226e 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,14 +524,11 @@ 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 0031422cf..a27cde76b 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,27 +119,24 @@ 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) @@ -306,9 +303,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": { @@ -331,7 +328,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 { @@ -359,9 +356,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": { @@ -376,7 +373,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")?; @@ -394,9 +391,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": { @@ -407,7 +404,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")?; @@ -426,9 +423,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": { @@ -439,7 +436,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")?; @@ -718,21 +715,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()), })); @@ -750,7 +747,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 13f663f10..d7284bc11 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 11ce189fa..31bcfbf1a 100644 --- a/lib/crates/fabro-agent/src/tool_execution.rs +++ b/lib/crates/fabro-agent/src/tool_execution.rs @@ -180,14 +180,11 @@ 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 { @@ -200,21 +197,15 @@ 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); } @@ -222,22 +213,16 @@ 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 { @@ -308,10 +293,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(), } } @@ -365,9 +350,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"} @@ -375,7 +360,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}")) @@ -387,11 +372,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()) }) }), } @@ -399,26 +384,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 997808943..cc0984249 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 d67b55ea6..a620f8214 100644 --- a/lib/crates/fabro-agent/src/tools.rs +++ b/lib/crates/fabro-agent/src/tools.rs @@ -15,7 +15,7 @@ const MAX_WEB_FETCH_BYTES: usize = 100 * 1024; /// Configuration for the optional LLM-based summarizer used by `web_fetch`. #[derive(Clone)] pub struct WebFetchSummarizer { - pub client: Client, + pub client: Client, pub model_id: ModelHandle, } @@ -72,9 +72,9 @@ pub(crate) fn required_str<'a>(args: &'a serde_json::Value, key: &str) -> Result pub fn make_read_file_tool() -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "read_file".into(), + name: "read_file".into(), description: "Read the contents of a file".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "file_path": {"type": "string", "description": "Absolute path to the file"}, @@ -84,7 +84,7 @@ pub fn make_read_file_tool() -> RegisteredTool { "required": ["file_path"] }), }, - executor: Arc::new(|args, ctx| { + executor: Arc::new(|args, ctx| { Box::pin(async move { let file_path = required_str(&args, "file_path")?; let offset = args.get("offset").and_then(serde_json::Value::as_u64); @@ -108,9 +108,9 @@ pub fn make_read_file_tool() -> RegisteredTool { pub fn make_write_file_tool() -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "write_file".into(), + name: "write_file".into(), description: "Write content to a file".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "file_path": {"type": "string", "description": "Absolute path to the file"}, @@ -119,7 +119,7 @@ pub fn make_write_file_tool() -> RegisteredTool { "required": ["file_path", "content"] }), }, - executor: Arc::new(|args, ctx| { + executor: Arc::new(|args, ctx| { Box::pin(async move { let file_path = required_str(&args, "file_path")?; let content = required_str(&args, "content")?; @@ -135,9 +135,9 @@ pub fn make_write_file_tool() -> RegisteredTool { pub fn make_edit_file_tool() -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "edit_file".into(), + name: "edit_file".into(), description: "Edit a file by replacing a string".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "file_path": {"type": "string", "description": "Absolute path to the file"}, @@ -148,7 +148,7 @@ pub fn make_edit_file_tool() -> RegisteredTool { "required": ["file_path", "old_string", "new_string"] }), }, - executor: Arc::new(|args, ctx| { + executor: Arc::new(|args, ctx| { Box::pin(async move { let file_path = required_str(&args, "file_path")?; let old_string = required_str(&args, "old_string")?; @@ -201,9 +201,9 @@ pub fn make_shell_tool_with_config(config: &SessionOptions) -> RegisteredTool { let max_timeout = config.max_command_timeout_ms; RegisteredTool { definition: ToolDefinition { - name: "shell".into(), + name: "shell".into(), description: "Execute a shell command".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "command": {"type": "string", "description": "The shell command to execute"}, @@ -213,7 +213,7 @@ pub fn make_shell_tool_with_config(config: &SessionOptions) -> RegisteredTool { "required": ["command"] }), }, - executor: Arc::new(move |args, ctx| { + executor: Arc::new(move |args, ctx| { Box::pin(async move { let command = required_str(&args, "command")?; let timeout_ms = args @@ -259,9 +259,9 @@ pub fn make_shell_tool_with_config(config: &SessionOptions) -> RegisteredTool { pub fn make_grep_tool() -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "grep".into(), + name: "grep".into(), description: "Search file contents with a regex pattern".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "pattern": {"type": "string", "description": "Regex pattern to search for"}, @@ -273,7 +273,7 @@ pub fn make_grep_tool() -> RegisteredTool { "required": ["pattern"] }), }, - executor: Arc::new(|args, ctx| { + executor: Arc::new(|args, ctx| { Box::pin(async move { let pattern = required_str(&args, "pattern")?; let path = args @@ -316,9 +316,9 @@ pub fn make_grep_tool() -> RegisteredTool { pub fn make_glob_tool() -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "glob".into(), + name: "glob".into(), description: "Find files matching a glob pattern".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "pattern": {"type": "string", "description": "Glob pattern to match files"}, @@ -327,7 +327,7 @@ pub fn make_glob_tool() -> RegisteredTool { "required": ["pattern"] }), }, - executor: Arc::new(|args, ctx| { + executor: Arc::new(|args, ctx| { Box::pin(async move { let pattern = required_str(&args, "pattern")?; let path = args.get("path").and_then(serde_json::Value::as_str); @@ -343,9 +343,9 @@ pub fn make_glob_tool() -> RegisteredTool { pub(crate) fn make_read_many_files_tool() -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "read_many_files".into(), + name: "read_many_files".into(), description: "Read multiple files at once".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "paths": { @@ -357,7 +357,7 @@ pub(crate) fn make_read_many_files_tool() -> RegisteredTool { "required": ["paths"] }), }, - executor: Arc::new(|args, ctx| { + executor: Arc::new(|args, ctx| { Box::pin(async move { let paths = args["paths"] .as_array() @@ -388,9 +388,9 @@ pub(crate) fn make_read_many_files_tool() -> RegisteredTool { pub(crate) fn make_list_dir_tool() -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "list_dir".into(), + name: "list_dir".into(), description: "List directory contents with depth control".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "path": {"type": "string", "description": "Directory path to list"}, @@ -399,7 +399,7 @@ pub(crate) fn make_list_dir_tool() -> RegisteredTool { "required": ["path"] }), }, - executor: Arc::new(|args, ctx| { + executor: Arc::new(|args, ctx| { Box::pin(async move { let path = required_str(&args, "path")?; let depth = args @@ -471,9 +471,9 @@ fn make_web_search_tool_with_api_key(api_key: Option) -> RegisteredTool RegisteredTool { definition: ToolDefinition { - name: "web_search".into(), + name: "web_search".into(), description: "Search the web using Brave Search".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "query": {"type": "string", "description": "Search query"}, @@ -482,7 +482,7 @@ fn make_web_search_tool_with_api_key(api_key: Option) -> RegisteredTool "required": ["query"] }), }, - executor: Arc::new(move |args, _ctx| { + executor: Arc::new(move |args, _ctx| { let api_key = api_key.clone(); Box::pin(async move { let api_key = api_key.ok_or_else(|| { @@ -638,14 +638,11 @@ 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"); } @@ -683,8 +680,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, }, ) @@ -713,8 +710,8 @@ mod tests { "new_string": "goodbye" }), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), + env: env_clone, + cancel: CancellationToken::new(), tool_env: None, }, ) @@ -795,8 +792,8 @@ mod tests { "replace_all": true }), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), + env: env_clone, + cancel: CancellationToken::new(), tool_env: None, }, ) @@ -812,22 +809,19 @@ 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")); @@ -842,8 +836,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, }, ) @@ -856,22 +850,19 @@ 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")); @@ -883,22 +874,19 @@ 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")); @@ -914,8 +902,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()), }, ) @@ -929,14 +917,11 @@ 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); @@ -947,10 +932,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() @@ -961,8 +946,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()), }, ) @@ -981,14 +966,11 @@ 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()")); @@ -1002,14 +984,11 @@ 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")); @@ -1020,14 +999,11 @@ 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!( @@ -1040,14 +1016,11 @@ 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!( @@ -1084,10 +1057,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() @@ -1096,8 +1069,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, }, ) @@ -1154,8 +1127,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, }, ) @@ -1176,8 +1149,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, }, ) @@ -1196,10 +1169,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() @@ -1223,10 +1196,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() @@ -1263,17 +1236,18 @@ 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() @@ -1299,11 +1273,12 @@ 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() @@ -1338,7 +1313,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", @@ -1362,17 +1337,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() @@ -1455,14 +1430,11 @@ 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(); @@ -1487,14 +1459,11 @@ 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 6d4532db1..965c98ee6 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,21 +427,18 @@ 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); } @@ -449,30 +446,24 @@ 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, + original_turn_count: 20, + preserved_turn_count: 6, summary_token_estimate: 500, - tracked_file_count: 3, + tracked_file_count: 3, }; - assert!(matches!( - completed, - AgentEvent::CompactionCompleted { - original_turn_count: 20, - .. - } - )); + assert!(matches!(completed, AgentEvent::CompactionCompleted { + original_turn_count: 20, + .. + })); } #[test] @@ -489,39 +480,36 @@ mod tests { fn subagent_spawned_constructible() { let event = AgentEvent::SubAgentSpawned { agent_id: "sa-1".into(), - depth: 1, - task: "list files".into(), + depth: 1, + task: "list files".into(), }; - assert!(matches!( - event, - AgentEvent::SubAgentSpawned { depth: 1, .. } - )); + 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, + agent_id: "sa-1".into(), + depth: 1, + success: true, turns_used: 5, }; - assert!(matches!( - event, - AgentEvent::SubAgentCompleted { - 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, .. })); } @@ -530,7 +518,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, .. })); } @@ -540,23 +528,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(); @@ -567,12 +555,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(); @@ -585,24 +573,21 @@ 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(); @@ -621,19 +606,19 @@ mod tests { fn mcp_server_ready_constructible() { let event = AgentEvent::McpServerReady { server_name: "filesystem".into(), - tool_count: 3, + 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") @@ -645,20 +630,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 { .. } @@ -668,16 +653,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 { @@ -698,7 +683,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(); @@ -719,7 +704,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(); @@ -736,19 +721,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, }), }, }; @@ -767,8 +752,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(); @@ -795,7 +780,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 eb63aa9db..eb45971b1 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,13 +448,10 @@ 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] @@ -466,12 +463,9 @@ 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] @@ -588,21 +582,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()), @@ -721,12 +715,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()), @@ -752,21 +746,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()), ], @@ -788,7 +782,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(), }]; @@ -809,12 +803,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()), ], @@ -862,12 +856,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()), ], @@ -888,16 +882,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()), ], @@ -982,8 +976,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()), ], @@ -1031,12 +1025,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()), ], @@ -1063,8 +1057,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) @@ -1076,8 +1070,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 69668efec..51d3b7311 100644 --- a/lib/crates/fabro-agent/tests/it/parity_matrix.rs +++ b/lib/crates/fabro-agent/tests/it/parity_matrix.rs @@ -18,7 +18,7 @@ use tokio::sync::Mutex as AsyncMutex; #[derive(Clone)] struct OpenAiTwinOptions { base_url: String, - api_key: String, + api_key: String, } fn summarizer_model_id(provider: Provider) -> ModelHandle { @@ -30,22 +30,22 @@ fn summarizer_model_id(provider: Provider) -> ModelHandle { | Provider::Inception | Provider::OpenAiCompatible => ModelHandle::ByName { provider: Provider::OpenAi, - model: "gpt-5.4-mini".to_string(), + model: "gpt-5.4-mini".to_string(), }, Provider::Gemini => ModelHandle::ByName { provider: Provider::Gemini, - model: "gemini-3-flash-preview".to_string(), + model: "gemini-3-flash-preview".to_string(), }, Provider::Anthropic => ModelHandle::ByName { provider: Provider::Anthropic, - model: "claude-haiku-4-5".to_string(), + model: "claude-haiku-4-5".to_string(), }, } } fn build_summarizer(provider: Provider, client: &Client) -> WebFetchSummarizer { WebFetchSummarizer { - client: client.clone(), + client: client.clone(), model_id: summarizer_model_id(provider), } } diff --git a/lib/crates/fabro-checkpoint/src/author.rs b/lib/crates/fabro-checkpoint/src/author.rs index 9cd815406..d8e364c69 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 f275222f0..52958ee87 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 fa5df4467..6d59481c7 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 a60d9aed2..30f4eca69 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 28e864772..d0afc7d15 100644 --- a/lib/crates/fabro-checkpoint/src/metadata.rs +++ b/lib/crates/fabro-checkpoint/src/metadata.rs @@ -15,14 +15,14 @@ use crate::git::Store; /// (`fabro/meta/{run_id}`) so that runs can be resumed from git alone. pub struct MetadataStore { repo_path: PathBuf, - author: GitAuthor, + author: GitAuthor, } impl MetadataStore { pub fn new(repo_path: impl Into, author: &GitAuthor) -> Self { Self { repo_path: repo_path.into(), - author: author.clone(), + author: author.clone(), } } @@ -364,11 +364,10 @@ 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") @@ -429,10 +428,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 a94dff43c..cd97085af 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,26 +92,20 @@ 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" @@ -121,13 +115,10 @@ 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" @@ -176,20 +167,16 @@ 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" @@ -198,14 +185,10 @@ 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 be975cf26..216d0e532 100644 --- a/lib/crates/fabro-cli/src/args.rs +++ b/lib/crates/fabro-cli/src/args.rs @@ -261,17 +261,17 @@ pub(crate) struct LogsArgs { pub(crate) server: ServerTargetArgs, /// Run ID prefix or workflow name (most recent run) - pub(crate) run: String, + pub(crate) run: String, /// Follow log output #[arg(short, long)] pub(crate) follow: bool, /// Logs since timestamp or relative (e.g. "42m", "2h", /// "2026-01-02T13:00:00Z") #[arg(long)] - pub(crate) since: Option, + pub(crate) since: Option, /// Lines from end (default: all) #[arg(short = 'n', long)] - pub(crate) tail: Option, + pub(crate) tail: Option, /// Formatted colored output with rendered assistant text #[arg(short = 'p', long)] pub(crate) pretty: bool, @@ -403,9 +403,9 @@ pub(crate) struct CpArgs { pub(crate) server: ServerTargetArgs, /// Source: : or local path - pub(crate) src: String, + pub(crate) src: String, /// Destination: : or local path - pub(crate) dst: String, + pub(crate) dst: String, /// Recurse into directories #[arg(short, long)] pub(crate) recursive: bool, @@ -417,18 +417,18 @@ pub(crate) struct PreviewArgs { pub(crate) server: ServerTargetArgs, /// Run ID or prefix - pub(crate) run: String, + pub(crate) run: String, /// Port number - pub(crate) port: u16, + pub(crate) port: u16, /// Generate a signed URL (embeds auth token, no headers needed) #[arg(long)] pub(crate) signed: bool, /// Signed URL expiry in seconds (default 3600, requires --signed) #[arg(long, default_value = "3600", requires = "signed")] - pub(crate) ttl: i32, + pub(crate) ttl: i32, /// Open URL in browser (implies --signed) #[arg(long)] - pub(crate) open: bool, + pub(crate) open: bool, } #[derive(Args)] @@ -437,10 +437,10 @@ pub(crate) struct SshArgs { pub(crate) server: ServerTargetArgs, /// Run ID or prefix - pub(crate) run: String, + pub(crate) run: String, /// SSH access expiry in minutes (default 60) #[arg(long, default_value = "60")] - pub(crate) ttl: f64, + pub(crate) ttl: f64, /// Print the SSH command instead of connecting #[arg(long)] pub(crate) print: bool, @@ -452,7 +452,7 @@ pub(crate) struct DiffArgs { pub(crate) server: ServerTargetArgs, /// Run ID or prefix - pub(crate) run: String, + pub(crate) run: String, /// Show diff for a specific node #[arg(long)] pub(crate) node: Option, @@ -492,7 +492,7 @@ pub(crate) struct SecretRmArgs { #[derive(Args)] pub(crate) struct SecretSetArgs { /// Name of the secret - pub(crate) key: String, + pub(crate) key: String, /// Value to store pub(crate) value: String, } @@ -661,7 +661,7 @@ pub(crate) struct PrCreateArgs { pub(crate) run_id: String, /// LLM model for generating PR description #[arg(long)] - pub(crate) model: Option, + pub(crate) model: Option, } #[derive(Args)] diff --git a/lib/crates/fabro-cli/src/command_context.rs b/lib/crates/fabro-cli/src/command_context.rs index f93bb9d8c..465984c95 100644 --- a/lib/crates/fabro-cli/src/command_context.rs +++ b/lib/crates/fabro-cli/src/command_context.rs @@ -16,18 +16,18 @@ pub(crate) enum ServerMode { target_override: Option, }, ByStorageDir { - target_override: Option, + target_override: Option, storage_dir_override: Option, }, } pub(crate) struct CommandContext { - cwd: PathBuf, + cwd: PathBuf, base_config_path: PathBuf, machine_settings: SettingsLayer, - cli_settings: CliSettings, - server_mode: ServerMode, - server: OnceCell>, + cli_settings: CliSettings, + server_mode: ServerMode, + server: OnceCell>, } impl CommandContext { @@ -43,7 +43,7 @@ impl CommandContext { pub(crate) fn for_connection(args: &ServerConnectionArgs) -> Result { Self::new(ServerMode::ByStorageDir { - target_override: args.target.server.clone(), + target_override: args.target.server.clone(), storage_dir_override: args.storage_dir.clone_path(), }) } diff --git a/lib/crates/fabro-cli/src/commands/artifact/cp.rs b/lib/crates/fabro-cli/src/commands/artifact/cp.rs index 4cb33537e..c120f5a26 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/cp.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/cp.rs @@ -180,11 +180,11 @@ mod tests { #[test] fn format_candidate_includes_retry() { let entry = super::super::ArtifactEntry { - node_slug: "retry_assets".to_string(), - retry: 2, - stage_id: fabro_types::StageId::new("retry_assets", 2), + node_slug: "retry_assets".to_string(), + retry: 2, + stage_id: fabro_types::StageId::new("retry_assets", 2), relative_path: "assets/retry/report.txt".to_string(), - size: 6, + size: 6, }; assert_eq!(format_candidate(&entry), "retry_assets:retry_2"); diff --git a/lib/crates/fabro-cli/src/commands/artifact/mod.rs b/lib/crates/fabro-cli/src/commands/artifact/mod.rs index 0395903f8..f01c8aadf 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/mod.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/mod.rs @@ -12,11 +12,11 @@ use crate::server_runs::ServerSummaryLookup; #[derive(Clone, Debug, serde::Serialize)] pub(super) struct ArtifactEntry { #[serde(skip_serializing)] - pub(super) stage_id: StageId, - pub(super) node_slug: String, - pub(super) retry: u32, + pub(super) stage_id: StageId, + pub(super) node_slug: String, + pub(super) retry: u32, pub(super) relative_path: String, - pub(super) size: u64, + pub(super) size: u64, } pub(super) async fn resolve_artifacts( diff --git a/lib/crates/fabro-cli/src/commands/doctor.rs b/lib/crates/fabro-cli/src/commands/doctor.rs index ac74284ba..989a94f94 100644 --- a/lib/crates/fabro-cli/src/commands/doctor.rs +++ b/lib/crates/fabro-cli/src/commands/doctor.rs @@ -22,11 +22,11 @@ use crate::command_context::CommandContext; use crate::shared::print_json_pretty; pub(crate) struct DepSpec { - pub name: &'static str, - command: &'static [&'static str], - pub required: bool, + pub name: &'static str, + command: &'static [&'static str], + pub required: bool, pub min_version: Version, - pattern: &'static LazyLock, + pattern: &'static LazyLock, } #[derive(Debug, Clone, PartialEq)] @@ -43,18 +43,18 @@ static DOT_RE: LazyLock = pub(crate) const DEP_SPECS: &[DepSpec] = &[ DepSpec { - name: "openssl", - command: &["openssl", "version"], - required: true, + name: "openssl", + command: &["openssl", "version"], + required: true, min_version: Version::new(3, 0, 0), - pattern: &OPENSSL_RE, + pattern: &OPENSSL_RE, }, DepSpec { - name: "dot", - command: &["dot", "-V"], - required: false, + name: "dot", + command: &["dot", "-V"], + required: false, min_version: Version::new(2, 0, 0), - pattern: &DOT_RE, + pattern: &DOT_RE, }, ]; @@ -156,28 +156,31 @@ pub(crate) fn check_config( ) -> CheckResult { match (settings_path, legacy_paths.is_empty()) { (Some(path), true) => CheckResult { - name: "Configuration".to_string(), - status: CheckStatus::Pass, - summary: path.display().to_string(), - details: vec![CheckDetail::new(format!("Loaded from {}", path.display()))], + name: "Configuration".to_string(), + status: CheckStatus::Pass, + summary: path.display().to_string(), + details: vec![CheckDetail::new(format!("Loaded from {}", path.display()))], remediation: None, }, (Some(path), false) => CheckResult { - name: "Configuration".to_string(), - status: CheckStatus::Warning, - summary: path.display().to_string(), - details: std::iter::once(CheckDetail::new(format!("Loaded from {}", path.display()))) - .chain(legacy_paths.iter().map(|legacy| { - CheckDetail::new(format!("Ignoring legacy config file {}", legacy.display())) - })) - .collect(), + name: "Configuration".to_string(), + status: CheckStatus::Warning, + summary: path.display().to_string(), + details: std::iter::once(CheckDetail::new(format!( + "Loaded from {}", + path.display() + ))) + .chain(legacy_paths.iter().map(|legacy| { + CheckDetail::new(format!("Ignoring legacy config file {}", legacy.display())) + })) + .collect(), remediation: Some("Delete or rename legacy config files".to_string()), }, (None, false) => CheckResult { - name: "Configuration".to_string(), - status: CheckStatus::Warning, - summary: "legacy config files ignored".to_string(), - details: legacy_paths + name: "Configuration".to_string(), + status: CheckStatus::Warning, + summary: "legacy config files ignored".to_string(), + details: legacy_paths .iter() .map(|legacy| { CheckDetail::new(format!("Found legacy config file {}", legacy.display())) @@ -190,10 +193,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()), @@ -204,10 +207,10 @@ pub(crate) fn check_config( fn check_legacy_env(path: Option) -> CheckResult { match path { Some(path) => CheckResult { - name: "Legacy .env".to_string(), - status: CheckStatus::Warning, - summary: "legacy secrets file detected".to_string(), - details: vec![CheckDetail::new(format!( + name: "Legacy .env".to_string(), + status: CheckStatus::Warning, + summary: "legacy secrets file detected".to_string(), + details: vec![CheckDetail::new(format!( "{} is no longer read by fabro", path.display() ))], @@ -217,10 +220,10 @@ fn check_legacy_env(path: Option) -> CheckResult { ), }, None => CheckResult { - name: "Legacy .env".to_string(), - status: CheckStatus::Pass, - summary: "not present".to_string(), - details: Vec::new(), + name: "Legacy .env".to_string(), + status: CheckStatus::Pass, + summary: "not present".to_string(), + details: Vec::new(), remediation: None, }, } @@ -230,20 +233,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( @@ -266,15 +269,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 { @@ -342,9 +345,9 @@ pub(crate) async fn run_doctor( }; let mut report = CheckReport { - title: "Fabro Doctor".to_string(), + title: "Fabro Doctor".to_string(), sections: vec![CheckSection { - title: "Local".to_string(), + title: "Local".to_string(), checks: vec![ check_config( settings_config_path @@ -361,12 +364,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(), @@ -391,12 +394,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(), @@ -421,12 +424,12 @@ pub(crate) async fn run_doctor( Ok(response) => response.into_inner(), Err(err) => { report.sections.push(CheckSection { - title: "Server".to_string(), + title: "Server".to_string(), checks: vec![CheckResult { - name: "Fabro server".to_string(), - status: CheckStatus::Error, - summary: "health check failed".to_string(), - details: vec![CheckDetail::new(err.to_string())], + name: "Fabro server".to_string(), + status: CheckStatus::Error, + summary: "health check failed".to_string(), + details: vec![CheckDetail::new(err.to_string())], remediation: Some( "Check that the server is reachable and responding to /health.".to_string(), ), @@ -459,12 +462,12 @@ pub(crate) async fn run_doctor( } Err(err) => { report.sections.push(CheckSection { - title: "Server".to_string(), + title: "Server".to_string(), checks: vec![CheckResult { - name: "Diagnostics".to_string(), - status: CheckStatus::Error, - summary: "probe failed".to_string(), - details: vec![CheckDetail::new(err.to_string())], + name: "Diagnostics".to_string(), + status: CheckStatus::Error, + summary: "probe failed".to_string(), + details: vec![CheckDetail::new(err.to_string())], remediation: Some( "Fix the server diagnostics failure and run `fabro doctor` again." .to_string(), @@ -550,14 +553,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 7772ef774..0e73b6a62 100644 --- a/lib/crates/fabro-cli/src/commands/exec.rs +++ b/lib/crates/fabro-cli/src/commands/exec.rs @@ -38,7 +38,7 @@ fn runtime_mcp_server(name: &str, entry: &McpEntryLayer) -> McpServerSettings { } } McpEntryLayer::Http { url, headers, .. } => McpTransport::Http { - url: url.as_source(), + url: url.as_source(), headers: headers .iter() .map(|(key, value)| (key.clone(), value.as_source())) @@ -146,10 +146,10 @@ pub(crate) async fn execute(mut args: ExecArgs, globals: &GlobalArgs) -> Result< .mcps .values() .map(|server| McpServerSettings { - name: server.name.clone(), - transport: server.transport.clone(), + name: server.name.clone(), + transport: server.transport.clone(), startup_timeout_secs: server.startup_timeout_secs, - tool_timeout_secs: server.tool_timeout_secs, + tool_timeout_secs: server.tool_timeout_secs, }) .collect() } else if let Some(mcps) = cli_settings @@ -171,10 +171,10 @@ pub(crate) async fn execute(mut args: ExecArgs, globals: &GlobalArgs) -> Result< .mcps .values() .map(|server| McpServerSettings { - name: server.name.clone(), - transport: server.transport.clone(), + name: server.name.clone(), + transport: server.transport.clone(), startup_timeout_secs: server.startup_timeout_secs, - tool_timeout_secs: server.tool_timeout_secs, + tool_timeout_secs: server.tool_timeout_secs, }) .collect() }) diff --git a/lib/crates/fabro-cli/src/commands/graph.rs b/lib/crates/fabro-cli/src/commands/graph.rs index eeef38768..d7bc13a03 100644 --- a/lib/crates/fabro-cli/src/commands/graph.rs +++ b/lib/crates/fabro-cli/src/commands/graph.rs @@ -25,12 +25,12 @@ pub(crate) async fn run( let ctx = CommandContext::for_target(&args.target)?; let built = build_run_manifest(ManifestBuildInput { - workflow: args.workflow.clone(), - cwd: ctx.cwd().to_path_buf(), - args_layer: SettingsLayer::default(), - args: None, - run_id: None, - user_layer: load_settings_user()?, + workflow: args.workflow.clone(), + cwd: ctx.cwd().to_path_buf(), + args_layer: SettingsLayer::default(), + args: None, + run_id: None, + user_layer: load_settings_user()?, user_settings_path: Some(active_settings_path(None)), })?; let client = ctx.server().await?; @@ -47,8 +47,8 @@ pub(crate) async fn run( let rendered = client .render_workflow_graph(types::RenderWorkflowGraphRequest { - manifest: built.manifest, - format: Some(match args.format { + manifest: built.manifest, + format: Some(match args.format { GraphOutputFormat::Svg => types::RenderWorkflowGraphFormat::Svg, GraphOutputFormat::Png => types::RenderWorkflowGraphFormat::Png, }), diff --git a/lib/crates/fabro-cli/src/commands/install.rs b/lib/crates/fabro-cli/src/commands/install.rs index 997b480f4..b415601dd 100644 --- a/lib/crates/fabro-cli/src/commands/install.rs +++ b/lib/crates/fabro-cli/src/commands/install.rs @@ -952,7 +952,7 @@ pub(crate) async fn run_install(args: &InstallArgs, globals: &GlobalArgs) -> Res if run_doctor { eprintln!(); let doctor_args = DoctorArgs { - target: ServerTargetArgs::default(), + target: ServerTargetArgs::default(), verbose: true, }; let _ = doctor::run_doctor(&doctor_args, true, globals).await?; diff --git a/lib/crates/fabro-cli/src/commands/model.rs b/lib/crates/fabro-cli/src/commands/model.rs index 3a9c2de64..17816c297 100644 --- a/lib/crates/fabro-cli/src/commands/model.rs +++ b/lib/crates/fabro-cli/src/commands/model.rs @@ -21,19 +21,19 @@ enum ModelTestResultKind { #[derive(Serialize)] struct ModelTestRow { - model: String, + model: String, provider: Provider, - result: ModelTestResultKind, + result: ModelTestResultKind, #[serde(skip_serializing_if = "Option::is_none")] - detail: Option, + detail: Option, #[serde(skip_serializing_if = "Option::is_none")] - error: Option, + error: Option, } #[derive(Serialize)] struct ModelTestOutput { - results: Vec, - total: usize, + results: Vec, + total: usize, failures: u32, } @@ -145,25 +145,25 @@ fn model_test_row_from_status(model: &Model, status: &str, result_color: Color) let trimmed = status.trim(); match result_color { Color::Green => ModelTestRow { - model: model.id.clone(), + model: model.id.clone(), provider: model.provider, - result: ModelTestResultKind::Pass, - detail: None, - error: None, + result: ModelTestResultKind::Pass, + detail: None, + error: None, }, Color::Yellow => ModelTestRow { - model: model.id.clone(), + model: model.id.clone(), provider: model.provider, - result: ModelTestResultKind::Skip, - detail: Some(trimmed.to_string()), - error: None, + result: ModelTestResultKind::Skip, + detail: Some(trimmed.to_string()), + error: None, }, _ => ModelTestRow { - model: model.id.clone(), + model: model.id.clone(), provider: model.provider, - result: ModelTestResultKind::Fail, - detail: None, - error: Some( + result: ModelTestResultKind::Fail, + detail: None, + error: Some( trimmed .strip_prefix("error: ") .unwrap_or(trimmed) @@ -435,19 +435,19 @@ mod tests { display_name: format!("{id} display"), limits: ModelLimits { context_window: 128_000, - max_output: Some(4096), + max_output: Some(4096), }, training: None, knowledge_cutoff: None, features: ModelFeatures { - tools: true, - vision: false, + tools: true, + vision: false, reasoning: false, - effort: false, + effort: false, }, costs: ModelCosts { - input_cost_per_mtok: Some(1.0), - output_cost_per_mtok: Some(2.0), + input_cost_per_mtok: Some(1.0), + output_cost_per_mtok: Some(2.0), cache_input_cost_per_mtok: None, }, estimated_output_tps: Some(100.0), diff --git a/lib/crates/fabro-cli/src/commands/pr/list.rs b/lib/crates/fabro-cli/src/commands/pr/list.rs index 2f7d15d0f..3d885807a 100644 --- a/lib/crates/fabro-cli/src/commands/pr/list.rs +++ b/lib/crates/fabro-cli/src/commands/pr/list.rs @@ -12,9 +12,9 @@ use crate::shared::print_json_pretty; struct PrRow { run_id: String, number: u64, - state: String, - title: String, - url: String, + state: String, + title: String, + url: String, } pub(super) async fn list_command( diff --git a/lib/crates/fabro-cli/src/commands/preflight.rs b/lib/crates/fabro-cli/src/commands/preflight.rs index 045c08cbb..82e80c82a 100644 --- a/lib/crates/fabro-cli/src/commands/preflight.rs +++ b/lib/crates/fabro-cli/src/commands/preflight.rs @@ -19,12 +19,12 @@ pub(crate) async fn execute(mut args: PreflightArgs, globals: &GlobalArgs) -> an args.verbose = args.verbose || ctx.cli_settings().output.verbosity == OutputVerbosity::Verbose; let manifest = build_run_manifest(ManifestBuildInput { - workflow: args.workflow.clone(), - cwd: ctx.cwd().to_path_buf(), - args_layer: preflight_args_layer(&args)?, - args: preflight_manifest_args(&args), - run_id: None, - user_layer: load_settings_user()?, + workflow: args.workflow.clone(), + cwd: ctx.cwd().to_path_buf(), + args_layer: preflight_args_layer(&args)?, + args: preflight_manifest_args(&args), + run_id: None, + user_layer: load_settings_user()?, user_settings_path: Some(active_settings_path(None)), })?; let client = ctx.server().await?; diff --git a/lib/crates/fabro-cli/src/commands/run/attach.rs b/lib/crates/fabro-cli/src/commands/run/attach.rs index 98e11849f..372b80600 100644 --- a/lib/crates/fabro-cli/src/commands/run/attach.rs +++ b/lib/crates/fabro-cli/src/commands/run/attach.rs @@ -108,10 +108,10 @@ pub(crate) async fn attach_run_with_client( } struct AttachOptions { - auto_approve: bool, - verbose: bool, + auto_approve: bool, + verbose: bool, kill_on_detach: bool, - json_output: bool, + json_output: bool, } fn replay_run_with_client( @@ -276,7 +276,7 @@ fn api_question_to_question(question: &types::ApiQuestion) -> Question { .options .iter() .map(|option| QuestionOption { - key: option.key.clone(), + key: option.key.clone(), label: option.label.clone(), }) .collect(); @@ -552,14 +552,14 @@ mod tests { #[test] fn answer_requires_reattach_for_interrupted_and_skipped_answers() { let interrupted = Answer { - value: AnswerValue::Interrupted, + value: AnswerValue::Interrupted, selected_option: None, - text: None, + text: None, }; let skipped = Answer { - value: AnswerValue::Skipped, + value: AnswerValue::Skipped, selected_option: None, - text: None, + text: None, }; let answered = Answer::yes(); diff --git a/lib/crates/fabro-cli/src/commands/run/cp.rs b/lib/crates/fabro-cli/src/commands/run/cp.rs index 99a64f311..52245830d 100644 --- a/lib/crates/fabro-cli/src/commands/run/cp.rs +++ b/lib/crates/fabro-cli/src/commands/run/cp.rs @@ -13,13 +13,13 @@ use crate::shared::{print_json_pretty, split_run_path}; #[derive(Debug)] enum CopyDirection { Download { - run_prefix: String, + run_prefix: String, remote_path: String, - local_path: PathBuf, + local_path: PathBuf, }, Upload { - local_path: PathBuf, - run_prefix: String, + local_path: PathBuf, + run_prefix: String, remote_path: String, }, } @@ -98,13 +98,13 @@ fn parse_direction(src: &str, dst: &str) -> Result { match (src_parts, dst_parts) { (Some((run_prefix, remote_path)), None) => Ok(CopyDirection::Download { - run_prefix: run_prefix.to_string(), + run_prefix: run_prefix.to_string(), remote_path: remote_path.to_string(), - local_path: PathBuf::from(dst), + local_path: PathBuf::from(dst), }), (None, Some((run_prefix, remote_path))) => Ok(CopyDirection::Upload { - local_path: PathBuf::from(src), - run_prefix: run_prefix.to_string(), + local_path: PathBuf::from(src), + run_prefix: run_prefix.to_string(), remote_path: remote_path.to_string(), }), (Some(_), Some(_)) => { diff --git a/lib/crates/fabro-cli/src/commands/run/create.rs b/lib/crates/fabro-cli/src/commands/run/create.rs index 4398dc290..603b4a647 100644 --- a/lib/crates/fabro-cli/src/commands/run/create.rs +++ b/lib/crates/fabro-cli/src/commands/run/create.rs @@ -15,7 +15,7 @@ use crate::manifest_builder::{ManifestBuildInput, build_run_manifest, run_manife use crate::user_config::{self, ServerTarget}; pub(crate) struct CreatedRun { - pub(crate) run_id: RunId, + pub(crate) run_id: RunId, pub(crate) local_run_dir: Option, } diff --git a/lib/crates/fabro-cli/src/commands/run/fork.rs b/lib/crates/fabro-cli/src/commands/run/fork.rs index ca8f6c72a..155ae55f7 100644 --- a/lib/crates/fabro-cli/src/commands/run/fork.rs +++ b/lib/crates/fabro-cli/src/commands/run/fork.rs @@ -40,14 +40,11 @@ pub(crate) async fn run(args: &ForkArgs, styles: &Styles, globals: &GlobalArgs) .as_deref() .map(str::parse::) .transpose()?; - let new_run_id = fork( - &store, - &ForkRunInput { - source_run_id: run_id, - target, - push: !args.no_push, - }, - )?; + let new_run_id = fork(&store, &ForkRunInput { + source_run_id: run_id, + target, + push: !args.no_push, + })?; let run_id_string = run_id.to_string(); let new_run_id_string = new_run_id.to_string(); diff --git a/lib/crates/fabro-cli/src/commands/run/output.rs b/lib/crates/fabro-cli/src/commands/run/output.rs index 43ac31d10..8eb06a944 100644 --- a/lib/crates/fabro-cli/src/commands/run/output.rs +++ b/lib/crates/fabro-cli/src/commands/run/output.rs @@ -67,19 +67,19 @@ pub(crate) fn print_preflight_workflow_summary( fn api_diagnostic_to_local(diagnostic: &types::WorkflowDiagnostic) -> fabro_validate::Diagnostic { fabro_validate::Diagnostic { - rule: diagnostic.rule.clone(), + rule: diagnostic.rule.clone(), severity: match diagnostic.severity { types::WorkflowDiagnosticSeverity::Error => fabro_validate::Severity::Error, types::WorkflowDiagnosticSeverity::Warning => fabro_validate::Severity::Warning, types::WorkflowDiagnosticSeverity::Info => fabro_validate::Severity::Info, }, - message: diagnostic.message.clone(), - node_id: diagnostic.node_id.clone(), - edge: diagnostic + message: diagnostic.message.clone(), + node_id: diagnostic.node_id.clone(), + edge: diagnostic .edge .as_ref() .map(|edge| (edge[0].clone(), edge[1].clone())), - fix: diagnostic.fix.clone(), + fix: diagnostic.fix.clone(), } } @@ -91,24 +91,24 @@ pub(crate) fn api_diagnostics_to_local( pub(crate) fn api_check_report_to_local(report: &types::PreflightCheckReport) -> CheckReport { CheckReport { - title: report.title.clone(), + title: report.title.clone(), sections: report .sections .iter() .map(|section| CheckSection { - title: section.title.clone(), + title: section.title.clone(), checks: section .checks .iter() .map(|check| CheckResult { - name: check.name.clone(), - status: match check.status { + name: check.name.clone(), + status: match check.status { types::PreflightCheckResultStatus::Pass => CheckStatus::Pass, types::PreflightCheckResultStatus::Warning => CheckStatus::Warning, types::PreflightCheckResultStatus::Error => CheckStatus::Error, }, - summary: check.summary.clone(), - details: check + summary: check.summary.clone(), + details: check .details .iter() .map(|detail| CheckDetail { diff --git a/lib/crates/fabro-cli/src/commands/run/overrides.rs b/lib/crates/fabro-cli/src/commands/run/overrides.rs index 36daa69b2..9a8a813b4 100644 --- a/lib/crates/fabro-cli/src/commands/run/overrides.rs +++ b/lib/crates/fabro-cli/src/commands/run/overrides.rs @@ -30,8 +30,8 @@ fn model_from_args(model: Option<&str>, provider: Option<&str>) -> Option, } @@ -54,14 +54,11 @@ pub(crate) async fn run(args: &RewindArgs, styles: &Styles, globals: &GlobalArgs let target = args.target.as_deref().unwrap().parse::()?; - rewind( - &store, - &RewindInput { - run_id, - target: target.clone(), - push: !args.no_push, - }, - )?; + rewind(&store, &RewindInput { + run_id, + target: target.clone(), + push: !args.no_push, + })?; let entry = timeline.resolve(&target)?; reset_rewound_run_state(lookup.client(), &store, &run_id, entry).await?; @@ -87,9 +84,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,20 +477,17 @@ 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!( @@ -545,14 +542,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, }; @@ -634,12 +631,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()), }, }; @@ -659,8 +656,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 6670eed8e..4640061f5 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,25 +484,22 @@ 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 { @@ -547,26 +544,20 @@ 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"); @@ -575,18 +566,15 @@ 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, @@ -599,24 +587,18 @@ 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(); @@ -635,27 +617,21 @@ 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()); } @@ -674,101 +650,86 @@ 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::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::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, - }, - ), + }), + 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, }, ]; @@ -795,26 +756,20 @@ 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")); @@ -825,68 +780,47 @@ 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) @@ -906,140 +840,104 @@ 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, - 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" - }), - }, - ), + 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, - 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, - }, + agent_event("code", AgentEvent::Warning { + kind: "context_window".into(), + message: "high usage".into(), + details: serde_json::json!({"usage_percent": 92}), + }), ); 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}), + 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::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(), + }), ); emit( &mut ui, - 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, - }, - ), + 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#" @@ -1062,33 +960,24 @@ 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] @@ -1102,36 +991,27 @@ 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"); @@ -1151,11 +1031,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, @@ -1164,29 +1044,23 @@ 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 0f25fbf09..c522efac2 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 0fbb7eb56..e02124787 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,16 +117,13 @@ 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 bcbb28644..735c28737 100644 --- a/lib/crates/fabro-cli/src/commands/run/runner.rs +++ b/lib/crates/fabro-cli/src/commands/run/runner.rs @@ -216,8 +216,8 @@ fn build_artifact_uploader( } struct HttpArtifactUploader { - run_id: RunId, - client: server_client::ServerStoreClient, + run_id: RunId, + client: server_client::ServerStoreClient, bearer_token: String, } @@ -282,7 +282,7 @@ impl StageArtifactUploader for MissingArtifactUploadTokenUploader { struct HttpRunStore { run_id: RunId, client: server_client::ServerStoreClient, - state: Arc>, + state: Arc>, events: Arc>>>, } @@ -593,12 +593,12 @@ mod tests { ); assert_eq!( worker_title_phase_for_event(&EventBody::InterviewStarted(InterviewStartedProps { - question_id: "q-1".to_string(), - question: "Approve?".to_string(), - stage: "gate".to_string(), - question_type: "yes_no".to_string(), - options: Vec::new(), - allow_freeform: false, + question_id: "q-1".to_string(), + question: "Approve?".to_string(), + stage: "gate".to_string(), + question_type: "yes_no".to_string(), + options: Vec::new(), + allow_freeform: false, timeout_seconds: None, context_display: None, })), @@ -607,39 +607,39 @@ mod tests { assert_eq!( worker_title_phase_for_event(&EventBody::InterviewCompleted(InterviewCompletedProps { question_id: "q-1".to_string(), - question: "Approve?".to_string(), - answer: "yes".to_string(), + question: "Approve?".to_string(), + answer: "yes".to_string(), duration_ms: 10, })), Some(WorkerTitlePhase::Running) ); assert_eq!( worker_title_phase_for_event(&EventBody::RunCompleted(RunCompletedProps { - duration_ms: 10, - artifact_count: 0, - status: "success".to_string(), - reason: None, - total_usd_micros: None, + duration_ms: 10, + artifact_count: 0, + status: "success".to_string(), + reason: None, + total_usd_micros: None, final_git_commit_sha: None, - final_patch: None, - billing: None, + final_patch: None, + billing: None, })), Some(WorkerTitlePhase::Succeeded) ); assert_eq!( worker_title_phase_for_event(&EventBody::RunFailed(RunFailedProps { - error: "cancelled".to_string(), - duration_ms: 10, - reason: Some(StatusReason::Cancelled), + error: "cancelled".to_string(), + duration_ms: 10, + reason: Some(StatusReason::Cancelled), git_commit_sha: None, })), Some(WorkerTitlePhase::Cancelled) ); assert_eq!( worker_title_phase_for_event(&EventBody::RunFailed(RunFailedProps { - error: "boom".to_string(), - duration_ms: 10, - reason: Some(StatusReason::Terminated), + error: "boom".to_string(), + duration_ms: 10, + reason: Some(StatusReason::Terminated), git_commit_sha: None, })), Some(WorkerTitlePhase::Failed) diff --git a/lib/crates/fabro-cli/src/commands/run/wait.rs b/lib/crates/fabro-cli/src/commands/run/wait.rs index 60ba6fe2a..f655645f9 100644 --- a/lib/crates/fabro-cli/src/commands/run/wait.rs +++ b/lib/crates/fabro-cli/src/commands/run/wait.rs @@ -158,22 +158,22 @@ mod tests { fn json_output_succeeded_with_conclusion() { let run_id = fixtures::RUN_1; let conclusion = Conclusion { - timestamp: chrono::Utc::now(), - status: StageStatus::Success, - duration_ms: 12345, - failure_reason: None, + timestamp: chrono::Utc::now(), + status: StageStatus::Success, + duration_ms: 12345, + failure_reason: None, final_git_commit_sha: None, - stages: vec![], - billing: Some(BilledTokenCounts { - input_tokens: 0, - output_tokens: 0, - total_tokens: 0, - reasoning_tokens: 0, - cache_read_tokens: 0, + stages: vec![], + billing: Some(BilledTokenCounts { + input_tokens: 0, + output_tokens: 0, + total_tokens: 0, + reasoning_tokens: 0, + cache_read_tokens: 0, cache_write_tokens: 0, - total_usd_micros: Some(420_000), + total_usd_micros: Some(420_000), }), - total_retries: 0, + total_retries: 0, }; let json = build_json_output(RunStatus::Succeeded, &run_id, Some(&conclusion)); assert_eq!(json["run_id"], run_id.to_string()); @@ -202,14 +202,14 @@ mod tests { fn json_output_no_cost_when_none() { let run_id = fixtures::RUN_4; let conclusion = Conclusion { - timestamp: chrono::Utc::now(), - status: StageStatus::Fail, - duration_ms: 500, - failure_reason: Some("error".into()), + timestamp: chrono::Utc::now(), + status: StageStatus::Fail, + duration_ms: 500, + failure_reason: Some("error".into()), final_git_commit_sha: None, - stages: vec![], - billing: None, - total_retries: 0, + stages: vec![], + billing: None, + total_retries: 0, }; let json = build_json_output(RunStatus::Failed, &run_id, Some(&conclusion)); assert!(json.get("total_usd_micros").is_none()); @@ -221,22 +221,22 @@ mod tests { let styles = no_color_styles(); let run_id = fixtures::RUN_5; let conclusion = Conclusion { - timestamp: chrono::Utc::now(), - status: StageStatus::Success, - duration_ms: 8000, - failure_reason: None, + timestamp: chrono::Utc::now(), + status: StageStatus::Success, + duration_ms: 8000, + failure_reason: None, final_git_commit_sha: None, - stages: vec![], - billing: Some(BilledTokenCounts { - input_tokens: 0, - output_tokens: 0, - total_tokens: 0, - reasoning_tokens: 0, - cache_read_tokens: 0, + stages: vec![], + billing: Some(BilledTokenCounts { + input_tokens: 0, + output_tokens: 0, + total_tokens: 0, + reasoning_tokens: 0, + cache_read_tokens: 0, cache_write_tokens: 0, - total_usd_micros: Some(150_000), + total_usd_micros: Some(150_000), }), - total_retries: 0, + total_retries: 0, }; // Just verify no panic; actual stderr output is hard to capture print_human_output(RunStatus::Succeeded, &run_id, Some(&conclusion), &styles); diff --git a/lib/crates/fabro-cli/src/commands/runs/inspect.rs b/lib/crates/fabro-cli/src/commands/runs/inspect.rs index 1f22e693b..3933c3fed 100644 --- a/lib/crates/fabro-cli/src/commands/runs/inspect.rs +++ b/lib/crates/fabro-cli/src/commands/runs/inspect.rs @@ -9,13 +9,13 @@ use crate::server_runs::{ServerRunSummaryInfo, ServerSummaryLookup}; #[derive(Debug, Serialize)] pub(crate) struct InspectOutput { - pub run_id: String, - pub status: RunStatus, - pub run_record: Option, + pub run_id: String, + pub status: RunStatus, + pub run_record: Option, pub start_record: Option, - pub conclusion: Option, - pub checkpoint: Option, - pub sandbox: Option, + pub conclusion: Option, + pub checkpoint: Option, + pub sandbox: Option, } pub(crate) async fn run(args: &InspectArgs, _globals: &GlobalArgs) -> Result<()> { @@ -32,24 +32,24 @@ pub(crate) async fn run(args: &InspectArgs, _globals: &GlobalArgs) -> Result<()> fn inspect_run_state(run: &ServerRunSummaryInfo, state: RunProjection) -> InspectOutput { InspectOutput { - run_id: run.run_id().to_string(), - status: state + run_id: run.run_id().to_string(), + status: state .status .as_ref() .map_or(run.status(), |record| record.status), - run_record: state + run_record: state .run .and_then(|record| serde_json::to_value(record).ok()), start_record: state .start .and_then(|record| serde_json::to_value(record).ok()), - conclusion: state + conclusion: state .conclusion .and_then(|record| serde_json::to_value(record).ok()), - checkpoint: state + checkpoint: state .checkpoint .and_then(|record| serde_json::to_value(record).ok()), - sandbox: state + sandbox: state .sandbox .and_then(|record| serde_json::to_value(record).ok()), } diff --git a/lib/crates/fabro-cli/src/commands/server/foreground.rs b/lib/crates/fabro-cli/src/commands/server/foreground.rs index a429a084c..ba469d061 100644 --- a/lib/crates/fabro-cli/src/commands/server/foreground.rs +++ b/lib/crates/fabro-cli/src/commands/server/foreground.rs @@ -48,15 +48,12 @@ pub(crate) async fn execute( styles, storage_dir, move |resolved_bind| { - record::write_server_record( - &record_path, - &record::ServerRecord { - pid, - bind: resolved_bind.clone(), - log_path: log_path.clone(), - started_at: Utc::now(), - }, - ) + record::write_server_record(&record_path, &record::ServerRecord { + pid, + bind: resolved_bind.clone(), + log_path: log_path.clone(), + started_at: Utc::now(), + }) }, )) .await diff --git a/lib/crates/fabro-cli/src/commands/server/record.rs b/lib/crates/fabro-cli/src/commands/server/record.rs index 629b7083a..27129208a 100644 --- a/lib/crates/fabro-cli/src/commands/server/record.rs +++ b/lib/crates/fabro-cli/src/commands/server/record.rs @@ -9,15 +9,15 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct ServerRecord { - pub pid: u32, - pub bind: Bind, - pub log_path: PathBuf, + pub pid: u32, + pub bind: Bind, + pub log_path: PathBuf, pub started_at: DateTime, } #[derive(Debug, Clone)] pub(crate) struct ActiveServerRecord { - pub record: ServerRecord, + pub record: ServerRecord, pub record_path: PathBuf, } diff --git a/lib/crates/fabro-cli/src/commands/server/start.rs b/lib/crates/fabro-cli/src/commands/server/start.rs index 00b40ac8d..c15680923 100644 --- a/lib/crates/fabro-cli/src/commands/server/start.rs +++ b/lib/crates/fabro-cli/src/commands/server/start.rs @@ -69,15 +69,15 @@ fn ensure_server_running_with_bind( } let serve_args = ServeArgs { - bind: None, - web: false, - no_web: false, - model: None, - provider: None, - dry_run: false, - sandbox: None, + bind: None, + web: false, + no_web: false, + model: None, + provider: None, + dry_run: false, + sandbox: None, max_concurrent_runs: server_max_concurrent_runs_override(), - config: Some(config_path.to_path_buf()), + config: Some(config_path.to_path_buf()), }; let bind_request = match &bind { @@ -148,15 +148,12 @@ async fn execute_foreground( styles, Some(storage_dir), move |resolved_bind| { - record::write_server_record( - &record_path, - &record::ServerRecord { - pid, - bind: resolved_bind.clone(), - log_path: log_path.clone(), - started_at: Utc::now(), - }, - ) + record::write_server_record(&record_path, &record::ServerRecord { + pid, + bind: resolved_bind.clone(), + log_path: log_path.clone(), + started_at: Utc::now(), + }) }, )) .await diff --git a/lib/crates/fabro-cli/src/commands/store/dump.rs b/lib/crates/fabro-cli/src/commands/store/dump.rs index ae12fafa4..16b4020c9 100644 --- a/lib/crates/fabro-cli/src/commands/store/dump.rs +++ b/lib/crates/fabro-cli/src/commands/store/dump.rs @@ -82,9 +82,9 @@ fn finalize_export( } struct DumpArtifact { - stage_id: StageId, + stage_id: StageId, relative_path: String, - data: Vec, + data: Vec, } trait DumpDataSource { @@ -97,9 +97,9 @@ trait DumpDataSource { #[cfg(test)] struct LocalDumpSource<'a> { - run_store: &'a RunDatabase, + run_store: &'a RunDatabase, artifact_store: &'a ArtifactStore, - run_id: RunId, + run_id: RunId, } #[cfg(test)] @@ -140,9 +140,9 @@ impl DumpDataSource for LocalDumpSource<'_> { ) })?; artifacts.push(DumpArtifact { - stage_id: asset.node, + stage_id: asset.node, relative_path: asset.filename, - data: data.to_vec(), + data: data.to_vec(), }); } Ok(artifacts) @@ -362,49 +362,52 @@ mod tests { fn sample_status() -> RunStatusRecord { RunStatusRecord { - status: RunStatus::Running, - reason: Some(StatusReason::SandboxInitializing), + status: RunStatus::Running, + reason: Some(StatusReason::SandboxInitializing), updated_at: dt("2026-03-27T12:05:00Z"), } } fn sample_checkpoint(current_node: &str, visit: u32) -> Checkpoint { Checkpoint { - timestamp: dt("2026-03-27T12:10:00Z"), - current_node: current_node.to_string(), - completed_nodes: vec!["plan".to_string()], - node_retries: HashMap::from([(current_node.to_string(), visit.saturating_sub(1))]), - context_values: HashMap::from([( + timestamp: dt("2026-03-27T12:10:00Z"), + current_node: current_node.to_string(), + completed_nodes: vec!["plan".to_string()], + node_retries: HashMap::from([( + current_node.to_string(), + visit.saturating_sub(1), + )]), + context_values: HashMap::from([( "artifact".to_string(), serde_json::json!({"kind": "summary"}), )]), - node_outcomes: HashMap::new(), - next_node_id: Some("review".to_string()), - git_commit_sha: Some("def456".to_string()), - loop_failure_signatures: HashMap::new(), + node_outcomes: HashMap::new(), + next_node_id: Some("review".to_string()), + git_commit_sha: Some("def456".to_string()), + loop_failure_signatures: HashMap::new(), restart_failure_signatures: HashMap::new(), - node_visits: HashMap::from([(current_node.to_string(), visit as usize)]), + node_visits: HashMap::from([(current_node.to_string(), visit as usize)]), } } fn sample_conclusion() -> Conclusion { Conclusion { - timestamp: dt("2026-03-27T12:15:00Z"), - status: StageStatus::Success, - duration_ms: 3210, - failure_reason: None, + timestamp: dt("2026-03-27T12:15:00Z"), + status: StageStatus::Success, + duration_ms: 3210, + failure_reason: None, final_git_commit_sha: Some("feedbeef".to_string()), - stages: Vec::new(), - billing: Some(BilledTokenCounts { - input_tokens: 10, - output_tokens: 20, - total_tokens: 150, - reasoning_tokens: 50, - cache_read_tokens: 30, + stages: Vec::new(), + billing: Some(BilledTokenCounts { + input_tokens: 10, + output_tokens: 20, + total_tokens: 150, + reasoning_tokens: 50, + cache_read_tokens: 30, cache_write_tokens: 40, - total_usd_micros: Some(1_250_000), + total_usd_micros: Some(1_250_000), }), - total_retries: 2, + total_retries: 2, } } @@ -417,12 +420,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()), @@ -434,11 +437,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, } } @@ -475,223 +478,171 @@ mod tests { ); let node = StageId::new("code", 2); - append_event( - &run, - &run_id, - &Event::RunCreated { - run_id, - settings: serde_json::to_value(&run_record.settings).unwrap(), - graph: serde_json::to_value(&run_record.graph).unwrap(), - workflow_source: Some("digraph night_sky {}".to_string()), - workflow_config: None, - labels: run_record.labels.clone().into_iter().collect(), - run_dir: "/tmp/night-sky-run".to_string(), - working_directory: run_record.working_directory.display().to_string(), - host_repo_path: run_record.host_repo_path.clone(), - repo_origin_url: run_record.repo_origin_url.clone(), - base_branch: run_record.base_branch.clone(), - workflow_slug: run_record.workflow_slug.clone(), - db_prefix: None, - provenance: run_record.provenance.clone(), - manifest_blob: None, - }, - ) + append_event(&run, &run_id, &Event::RunCreated { + run_id, + settings: serde_json::to_value(&run_record.settings).unwrap(), + graph: serde_json::to_value(&run_record.graph).unwrap(), + workflow_source: Some("digraph night_sky {}".to_string()), + workflow_config: None, + labels: run_record.labels.clone().into_iter().collect(), + run_dir: "/tmp/night-sky-run".to_string(), + working_directory: run_record.working_directory.display().to_string(), + host_repo_path: run_record.host_repo_path.clone(), + repo_origin_url: run_record.repo_origin_url.clone(), + base_branch: run_record.base_branch.clone(), + workflow_slug: run_record.workflow_slug.clone(), + db_prefix: None, + provenance: run_record.provenance.clone(), + manifest_blob: None, + }) .await .unwrap(); - append_event( - &run, - &run_id, - &Event::WorkflowRunStarted { - name: "night-sky".to_string(), - run_id, - base_branch: run_record.base_branch.clone(), - base_sha: start_record.base_sha.clone(), - run_branch: start_record.run_branch.clone(), - worktree_dir: None, - goal: Some("map the constellations".to_string()), - }, - ) + append_event(&run, &run_id, &Event::WorkflowRunStarted { + name: "night-sky".to_string(), + run_id, + base_branch: run_record.base_branch.clone(), + base_sha: start_record.base_sha.clone(), + run_branch: start_record.run_branch.clone(), + worktree_dir: None, + goal: Some("map the constellations".to_string()), + }) .await .unwrap(); - append_event( - &run, - &run_id, - &Event::RunRunning { - reason: status_record.reason, - }, - ) + append_event(&run, &run_id, &Event::RunRunning { + reason: status_record.reason, + }) .await .unwrap(); for checkpoint in [&first_checkpoint, &second_checkpoint] { - append_event( - &run, - &run_id, - &Event::CheckpointCompleted { - node_id: checkpoint.current_node.clone(), - status: "success".to_string(), - current_node: checkpoint.current_node.clone(), - completed_nodes: checkpoint.completed_nodes.clone(), - node_retries: checkpoint.node_retries.clone().into_iter().collect(), - context_values: checkpoint.context_values.clone().into_iter().collect(), - node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(), - next_node_id: checkpoint.next_node_id.clone(), - git_commit_sha: checkpoint.git_commit_sha.clone(), - loop_failure_signatures: checkpoint - .loop_failure_signatures - .clone() - .into_iter() - .map(|(signature, count)| (signature.to_string(), count)) - .collect(), - restart_failure_signatures: checkpoint - .restart_failure_signatures - .clone() - .into_iter() - .map(|(signature, count)| (signature.to_string(), count)) - .collect(), - node_visits: checkpoint.node_visits.clone().into_iter().collect(), - diff: None, - }, - ) + append_event(&run, &run_id, &Event::CheckpointCompleted { + node_id: checkpoint.current_node.clone(), + status: "success".to_string(), + current_node: checkpoint.current_node.clone(), + completed_nodes: checkpoint.completed_nodes.clone(), + node_retries: checkpoint.node_retries.clone().into_iter().collect(), + context_values: checkpoint.context_values.clone().into_iter().collect(), + node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(), + next_node_id: checkpoint.next_node_id.clone(), + git_commit_sha: checkpoint.git_commit_sha.clone(), + loop_failure_signatures: checkpoint + .loop_failure_signatures + .clone() + .into_iter() + .map(|(signature, count)| (signature.to_string(), count)) + .collect(), + restart_failure_signatures: checkpoint + .restart_failure_signatures + .clone() + .into_iter() + .map(|(signature, count)| (signature.to_string(), count)) + .collect(), + node_visits: checkpoint.node_visits.clone().into_iter().collect(), + diff: None, + }) .await .unwrap(); } - append_event( - &run, - &run_id, - &Event::SandboxInitialized { - working_directory: sandbox.working_directory.clone(), - provider: sandbox.provider.clone(), - identifier: sandbox.identifier.clone(), - host_working_directory: sandbox.host_working_directory.clone(), - container_mount_point: sandbox.container_mount_point.clone(), - }, - ) + append_event(&run, &run_id, &Event::SandboxInitialized { + working_directory: sandbox.working_directory.clone(), + provider: sandbox.provider.clone(), + identifier: sandbox.identifier.clone(), + host_working_directory: sandbox.host_working_directory.clone(), + container_mount_point: sandbox.container_mount_point.clone(), + }) .await .unwrap(); - append_event( - &run, - &run_id, - &Event::Prompt { - stage: "code".to_string(), - visit: 2, - text: "Plan the fix".to_string(), - mode: None, - provider: None, - model: None, - }, - ) + append_event(&run, &run_id, &Event::Prompt { + stage: "code".to_string(), + visit: 2, + text: "Plan the fix".to_string(), + mode: None, + provider: None, + model: None, + }) .await .unwrap(); - append_event( - &run, - &run_id, - &Event::PromptCompleted { - node_id: "code".to_string(), - response: "Implemented".to_string(), - model: "gpt-5".to_string(), - provider: "openai".to_string(), - billing: None, - }, - ) + append_event(&run, &run_id, &Event::PromptCompleted { + node_id: "code".to_string(), + response: "Implemented".to_string(), + model: "gpt-5".to_string(), + provider: "openai".to_string(), + billing: None, + }) .await .unwrap(); - append_event( - &run, - &run_id, - &Event::StageCompleted { - node_id: "code".to_string(), - name: "Code".to_string(), - index: 1, - duration_ms: 250, - status: "partial_success".to_string(), - preferred_label: None, - suggested_next_ids: Vec::new(), - billing: None, - failure: None, - notes: Some("captured output".to_string()), - files_touched: Vec::new(), - context_updates: None, - jump_to_node: None, - context_values: None, - node_visits: Some(std::collections::BTreeMap::from([( - "code".to_string(), - 2usize, - )])), - loop_failure_signatures: None, - restart_failure_signatures: None, - response: Some("Implemented".to_string()), - attempt: 1, - max_attempts: 1, - }, - ) + append_event(&run, &run_id, &Event::StageCompleted { + node_id: "code".to_string(), + name: "Code".to_string(), + index: 1, + duration_ms: 250, + status: "partial_success".to_string(), + preferred_label: None, + suggested_next_ids: Vec::new(), + billing: None, + failure: None, + notes: Some("captured output".to_string()), + files_touched: Vec::new(), + context_updates: None, + jump_to_node: None, + context_values: None, + node_visits: Some(std::collections::BTreeMap::from([( + "code".to_string(), + 2usize, + )])), + loop_failure_signatures: None, + restart_failure_signatures: None, + response: Some("Implemented".to_string()), + attempt: 1, + max_attempts: 1, + }) .await .unwrap(); - append_event( - &run, - &run_id, - &Event::CommandStarted { - node_id: "code".to_string(), - script: "echo hi".to_string(), - command: "echo hi".to_string(), - language: "sh".to_string(), - timeout_ms: None, - }, - ) + append_event(&run, &run_id, &Event::CommandStarted { + node_id: "code".to_string(), + script: "echo hi".to_string(), + command: "echo hi".to_string(), + language: "sh".to_string(), + timeout_ms: None, + }) .await .unwrap(); - append_event( - &run, - &run_id, - &Event::CommandCompleted { - node_id: "code".to_string(), - stdout: "stdout line".to_string(), - stderr: String::new(), - exit_code: Some(0), - duration_ms: 100, - timed_out: false, - }, - ) + append_event(&run, &run_id, &Event::CommandCompleted { + node_id: "code".to_string(), + stdout: "stdout line".to_string(), + stderr: String::new(), + exit_code: Some(0), + duration_ms: 100, + timed_out: false, + }) .await .unwrap(); - append_event( - &run, - &run_id, - &Event::RetroStarted { - prompt: Some("How did it go?".to_string()), - provider: None, - model: None, - }, - ) + append_event(&run, &run_id, &Event::RetroStarted { + prompt: Some("How did it go?".to_string()), + provider: None, + model: None, + }) .await .unwrap(); - append_event( - &run, - &run_id, - &Event::RetroCompleted { - duration_ms: 50, - response: Some("Smooth enough".to_string()), - retro: Some(serde_json::to_value(&retro).unwrap()), - }, - ) + append_event(&run, &run_id, &Event::RetroCompleted { + duration_ms: 50, + response: Some("Smooth enough".to_string()), + retro: Some(serde_json::to_value(&retro).unwrap()), + }) .await .unwrap(); - append_event( - &run, - &run_id, - &Event::WorkflowRunCompleted { - duration_ms: conclusion.duration_ms, - artifact_count: 0, - status: "success".to_string(), - reason: None, - total_usd_micros: conclusion - .billing - .as_ref() - .and_then(|billing| billing.total_usd_micros), - final_git_commit_sha: conclusion.final_git_commit_sha.clone(), - final_patch: None, - billing: conclusion.billing.clone(), - }, - ) + append_event(&run, &run_id, &Event::WorkflowRunCompleted { + duration_ms: conclusion.duration_ms, + artifact_count: 0, + status: "success".to_string(), + reason: None, + total_usd_micros: conclusion + .billing + .as_ref() + .and_then(|billing| billing.total_usd_micros), + final_git_commit_sha: conclusion.final_git_commit_sha.clone(), + final_patch: None, + billing: conclusion.billing.clone(), + }) .await .unwrap(); run.append_event( diff --git a/lib/crates/fabro-cli/src/commands/system/prune.rs b/lib/crates/fabro-cli/src/commands/system/prune.rs index 375962c18..df5c7e1c7 100644 --- a/lib/crates/fabro-cli/src/commands/system/prune.rs +++ b/lib/crates/fabro-cli/src/commands/system/prune.rs @@ -16,12 +16,12 @@ pub(super) async fn prune_command(args: &RunsPruneArgs, globals: &GlobalArgs) -> .api() .prune_runs() .body(types::PruneRunsRequest { - before: args.filter.before.clone(), - dry_run: !args.yes, - labels: parse_label_filters(&args.filter.label), + before: args.filter.before.clone(), + dry_run: !args.yes, + labels: parse_label_filters(&args.filter.label), older_than: args.older_than.map(format_duration), - orphans: args.filter.orphans, - workflow: args.filter.workflow.clone(), + orphans: args.filter.orphans, + workflow: args.filter.workflow.clone(), }) .send() .await diff --git a/lib/crates/fabro-cli/src/commands/uninstall.rs b/lib/crates/fabro-cli/src/commands/uninstall.rs index 6cceb5bb1..17cfd0b13 100644 --- a/lib/crates/fabro-cli/src/commands/uninstall.rs +++ b/lib/crates/fabro-cli/src/commands/uninstall.rs @@ -15,13 +15,13 @@ use crate::user_config; #[derive(Debug, Serialize)] struct Inventory { - home_root: PathBuf, - storage_dir: PathBuf, - home_exists: bool, - home_size: u64, - server_running: bool, - shell_configs: Vec, - binary_path: Option, + home_root: PathBuf, + storage_dir: PathBuf, + home_exists: bool, + home_size: u64, + server_running: bool, + shell_configs: Vec, + binary_path: Option, binary_is_managed: bool, } @@ -203,12 +203,12 @@ fn print_preview(inventory: &Inventory) { #[derive(Debug, Serialize)] struct UninstallResult { - status: &'static str, - home_removed: bool, - server_stopped: bool, + status: &'static str, + home_removed: bool, + server_stopped: bool, shell_configs_cleaned: Vec, - binary_removed: bool, - binary_hint: Option, + binary_removed: bool, + binary_hint: Option, } fn execute_uninstall(inventory: &Inventory, json: bool) -> Result<()> { @@ -217,12 +217,12 @@ fn execute_uninstall(inventory: &Inventory, json: bool) -> Result<()> { let bold = console::Style::new().bold(); let mut critical_failure = false; let mut result = UninstallResult { - status: "completed", - home_removed: false, - server_stopped: false, + status: "completed", + home_removed: false, + server_stopped: false, shell_configs_cleaned: Vec::new(), - binary_removed: false, - binary_hint: None, + binary_removed: false, + binary_hint: None, }; // Unit 3a: Server stop diff --git a/lib/crates/fabro-cli/src/commands/upgrade.rs b/lib/crates/fabro-cli/src/commands/upgrade.rs index 43755de70..ee88accd8 100644 --- a/lib/crates/fabro-cli/src/commands/upgrade.rs +++ b/lib/crates/fabro-cli/src/commands/upgrade.rs @@ -193,7 +193,7 @@ const LAST_CHECK_FILE: &str = "last_upgrade_check.json"; #[derive(serde::Serialize, serde::Deserialize)] struct UpgradeCheckState { - checked_at: u64, + checked_at: u64, latest_version: String, } @@ -410,7 +410,7 @@ async fn check_and_print_notice() -> Result<()> { .unwrap_or_default() .as_secs(); let state = UpgradeCheckState { - checked_at: now, + checked_at: now, latest_version: latest.to_string(), }; let _ = state.save(&state_path); @@ -503,7 +503,7 @@ mod tests { #[test] fn upgrade_check_state_roundtrip() { let state = UpgradeCheckState { - checked_at: 1_710_000_000, + checked_at: 1_710_000_000, latest_version: "0.5.0".to_string(), }; let json = serde_json::to_string(&state).unwrap(); @@ -515,7 +515,7 @@ mod tests { #[test] fn upgrade_check_state_stale() { let old = UpgradeCheckState { - checked_at: 0, // epoch — definitely stale + checked_at: 0, // epoch — definitely stale latest_version: "0.1.0".to_string(), }; assert!(old.is_stale()); @@ -528,7 +528,7 @@ mod tests { .unwrap() .as_secs(); let fresh = UpgradeCheckState { - checked_at: now, + checked_at: now, latest_version: "0.5.0".to_string(), }; assert!(!fresh.is_stale()); @@ -539,7 +539,7 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("state.json"); let state = UpgradeCheckState { - checked_at: 1_710_000_000, + checked_at: 1_710_000_000, latest_version: "0.5.0".to_string(), }; state.save(&path).unwrap(); diff --git a/lib/crates/fabro-cli/src/commands/validate.rs b/lib/crates/fabro-cli/src/commands/validate.rs index f35666cfb..f8a86fbaa 100644 --- a/lib/crates/fabro-cli/src/commands/validate.rs +++ b/lib/crates/fabro-cli/src/commands/validate.rs @@ -17,12 +17,12 @@ pub(crate) async fn run( ) -> anyhow::Result<()> { let ctx = CommandContext::for_target(&args.target)?; let built = build_run_manifest(ManifestBuildInput { - workflow: args.workflow.clone(), - cwd: ctx.cwd().to_path_buf(), - args_layer: SettingsLayer::default(), - args: None, - run_id: None, - user_layer: load_settings_user()?, + workflow: args.workflow.clone(), + cwd: ctx.cwd().to_path_buf(), + args_layer: SettingsLayer::default(), + args: None, + run_id: None, + user_layer: load_settings_user()?, user_settings_path: Some(active_settings_path(None)), })?; let client = ctx.server().await?; diff --git a/lib/crates/fabro-cli/src/server_client.rs b/lib/crates/fabro-cli/src/server_client.rs index 132a30a9c..9cc115c46 100644 --- a/lib/crates/fabro-cli/src/server_client.rs +++ b/lib/crates/fabro-cli/src/server_client.rs @@ -27,20 +27,20 @@ use crate::{sse, user_config}; #[derive(Clone)] pub(crate) struct ServerStoreClient { - client: fabro_api::Client, + client: fabro_api::Client, http_client: reqwest::Client, - base_url: String, + base_url: String, } #[derive(Debug, Clone)] struct LocalServerRuntime { active_config_path: PathBuf, - storage_dir: PathBuf, + storage_dir: PathBuf, } pub(crate) struct RunAttachEventStream { - stream: progenitor_client::ByteStream, - pending_bytes: Vec, + stream: progenitor_client::ByteStream, + pending_bytes: Vec, buffered_events: VecDeque, } @@ -105,7 +105,7 @@ pub(crate) async fn connect_server_with_settings( let target = user_config::resolve_server_target(args, settings)?; let runtime = LocalServerRuntime { active_config_path: base_config_path.to_path_buf(), - storage_dir: user_config::storage_dir(settings)?, + storage_dir: user_config::storage_dir(settings)?, }; connect_target_api_client_bundle(&target, &runtime).await } @@ -236,14 +236,14 @@ struct ArtifactBatchUploadManifest { #[derive(Debug, Serialize)] struct ArtifactBatchUploadEntry { - part: String, - path: String, + part: String, + path: String, #[serde(skip_serializing_if = "Option::is_none")] - sha256: Option, + sha256: Option, #[serde(skip_serializing_if = "Option::is_none")] expected_bytes: Option, #[serde(skip_serializing_if = "Option::is_none")] - content_type: Option, + content_type: Option, } impl ServerStoreClient { @@ -660,11 +660,11 @@ impl ServerStoreClient { .len(); manifest_entries.push(ArtifactBatchUploadEntry { - part: part_name.clone(), - path: artifact.path.clone(), - sha256: Some(artifact.content_sha256.clone()), + part: part_name.clone(), + path: artifact.path.clone(), + sha256: Some(artifact.content_sha256.clone()), expected_bytes: Some(artifact.bytes), - content_type: Some(artifact.mime.clone()), + content_type: Some(artifact.mime.clone()), }); file_parts.push(( diff --git a/lib/crates/fabro-cli/src/server_runs.rs b/lib/crates/fabro-cli/src/server_runs.rs index f3ef05b79..8020f7882 100644 --- a/lib/crates/fabro-cli/src/server_runs.rs +++ b/lib/crates/fabro-cli/src/server_runs.rs @@ -11,9 +11,9 @@ use fabro_workflow::run_lookup::{RunInfo, resolve_run_from_summaries, scratch_ba use crate::server_client::{self, ServerStoreClient}; pub(crate) struct ServerRunLookup { - client: ServerStoreClient, + client: ServerStoreClient, scratch_base: PathBuf, - summaries: Vec, + summaries: Vec, } impl ServerRunLookup { @@ -105,7 +105,7 @@ impl ServerRunSummaryInfo { pub(crate) struct ServerSummaryLookup { client: Arc, - runs: Vec, + runs: Vec, } impl ServerSummaryLookup { diff --git a/lib/crates/fabro-cli/src/shared/openai_jwt.rs b/lib/crates/fabro-cli/src/shared/openai_jwt.rs index 712fbf7fd..e15665550 100644 --- a/lib/crates/fabro-cli/src/shared/openai_jwt.rs +++ b/lib/crates/fabro-cli/src/shared/openai_jwt.rs @@ -11,9 +11,9 @@ struct JwtPayload { #[serde(default)] chatgpt_account_id: Option, #[serde(default, rename = "https://api.openai.com/auth")] - auth_claim: Option, + auth_claim: Option, #[serde(default)] - organizations: Option>, + organizations: Option>, } #[derive(Deserialize)] diff --git a/lib/crates/fabro-cli/src/user_config.rs b/lib/crates/fabro-cli/src/user_config.rs index 533ad95fa..317a9b332 100644 --- a/lib/crates/fabro-cli/src/user_config.rs +++ b/lib/crates/fabro-cli/src/user_config.rs @@ -12,8 +12,8 @@ use tracing::debug; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub(crate) struct ClientTlsSettings { pub cert: PathBuf, - pub key: PathBuf, - pub ca: PathBuf, + pub key: PathBuf, + pub ca: PathBuf, } use crate::args::ServerTargetArgs; @@ -85,7 +85,7 @@ pub(crate) fn apply_storage_dir_override( pub(crate) enum ServerTarget { HttpUrl { api_url: String, - tls: Option, + tls: Option, }, UnixSocket(PathBuf), } @@ -99,8 +99,8 @@ fn cli_target_from_settings(settings: &CliSettings) -> Option<(String, Option { let tls_settings = tls.as_ref().map(|tls| ClientTlsSettings { cert: PathBuf::from(tls.cert.as_source()), - key: PathBuf::from(tls.key.as_source()), - ca: PathBuf::from(tls.ca.as_source()), + key: PathBuf::from(tls.key.as_source()), + ca: PathBuf::from(tls.ca.as_source()), }); Some((url.as_source(), tls_settings)) } @@ -265,7 +265,7 @@ mod tests { .unwrap(), Some(ServerTarget::HttpUrl { api_url: "https://cli.example.com".to_string(), - tls: None, + tls: None, }) ); } @@ -311,7 +311,7 @@ url = "https://config.example.com" resolve_server_target(&server_target_args(None), &settings).unwrap(), ServerTarget::HttpUrl { api_url: "https://config.example.com".to_string(), - tls: None, + tls: None, } ); } @@ -335,7 +335,7 @@ url = "https://config.example.com" .unwrap(), ServerTarget::HttpUrl { api_url: "https://cli.example.com".to_string(), - tls: None, + tls: None, } ); } @@ -368,7 +368,7 @@ url = "https://config.example.com" .unwrap(), ServerTarget::HttpUrl { api_url: "https://cli.example.com".to_string(), - tls: None, + tls: None, } ); } @@ -377,8 +377,8 @@ url = "https://config.example.com" fn remote_target_uses_tls_from_config() { let expected_tls = ClientTlsSettings { cert: PathBuf::from("cert.pem"), - key: PathBuf::from("key.pem"), - ca: PathBuf::from("ca.pem"), + key: PathBuf::from("key.pem"), + ca: PathBuf::from("ca.pem"), }; let settings = parse_v2( r#" @@ -402,7 +402,7 @@ ca = "ca.pem" .unwrap(), Some(ServerTarget::HttpUrl { api_url: "https://cli.example.com".to_string(), - tls: Some(expected_tls), + tls: Some(expected_tls), }) ); } diff --git a/lib/crates/fabro-cli/tests/it/cmd/config.rs b/lib/crates/fabro-cli/tests/it/cmd/config.rs index ea6c7fe8e..22e89b974 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/config.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/config.rs @@ -435,10 +435,10 @@ fn settings_local_workflow_name_applies_run_overlay_and_deep_merges() { // checkpoint.exclude_globs is a security/policy list: replace by default. let checkpoint = run_checkpoint(&cfg); - assert_eq!( - checkpoint.exclude_globs, - vec!["run-only".to_string(), "shared".to_string()] - ); + assert_eq!(checkpoint.exclude_globs, vec![ + "run-only".to_string(), + "shared".to_string() + ]); // Hooks: id-based replacement. The "shared" hook appears in both cli and // workflow layers and resolves to the workflow entry; project and run-only @@ -528,10 +528,9 @@ fn settings_local_explicit_workflow_path_uses_workflow_project_layers() { assert!(auto_approve_enabled(&cfg)); // v2 R30: run.prepare.steps replaces the whole ordered list across layers. // The highest-precedence layer (workflow) wins. - assert_eq!( - run_prepare_commands(&cfg), - vec!["workflow-setup".to_string()] - ); + assert_eq!(run_prepare_commands(&cfg), vec![ + "workflow-setup".to_string() + ]); assert_eq!(run_sandbox(&cfg).preserve, Some(true)); } diff --git a/lib/crates/fabro-cli/tests/it/cmd/fork.rs b/lib/crates/fabro-cli/tests/it/cmd/fork.rs index cfea00b85..162bdf6a5 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/fork.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/fork.rs @@ -84,10 +84,10 @@ fn fork_latest_prints_new_run_and_resume_hint() { ); let new_run_id = &new_run_ids[0]; - let new_head = git_stdout( - &setup.repo_dir, - &["rev-parse", &format!("fabro/run/{new_run_id}")], - ); + let new_head = git_stdout(&setup.repo_dir, &[ + "rev-parse", + &format!("fabro/run/{new_run_id}"), + ]); let expected_head = run_branch_commits(&setup.repo_dir, &setup.run.run_id) .into_iter() .last() @@ -128,10 +128,10 @@ fn fork_from_earlier_checkpoint_uses_expected_sha() { ); let new_run_id = &new_run_ids[0]; - let new_head = git_stdout( - &setup.repo_dir, - &["rev-parse", &format!("fabro/run/{new_run_id}")], - ); + let new_head = git_stdout(&setup.repo_dir, &[ + "rev-parse", + &format!("fabro/run/{new_run_id}"), + ]); assert_eq!(new_head.trim(), expected_head); let checkpoint = git_show_json( diff --git a/lib/crates/fabro-cli/tests/it/cmd/logs.rs b/lib/crates/fabro-cli/tests/it/cmd/logs.rs index 08749487e..0ec7bfe45 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/logs.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/logs.rs @@ -91,17 +91,14 @@ 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] @@ -227,15 +224,12 @@ 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 49fe73cf4..6a6e56753 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/pr_view.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/pr_view.rs @@ -98,14 +98,14 @@ fn pr_view_reads_pull_request_from_store_without_pull_request_json() { tool_call_id: None, actor: None, body: EventBody::PullRequestCreated(PullRequestCreatedProps { - pr_url: "https://github.com/fabro-sh/fabro/pull/123".to_string(), - pr_number: 123, - owner: "fabro-sh".to_string(), - repo: "fabro".to_string(), + pr_url: "https://github.com/fabro-sh/fabro/pull/123".to_string(), + pr_number: 123, + owner: "fabro-sh".to_string(), + repo: "fabro".to_string(), base_branch: "main".to_string(), head_branch: "fabro/run/demo".to_string(), - title: "Map the constellations".to_string(), - draft: false, + title: "Map the constellations".to_string(), + draft: false, }), }; client diff --git a/lib/crates/fabro-cli/tests/it/cmd/resume.rs b/lib/crates/fabro-cli/tests/it/cmd/resume.rs index aad477a14..5a1a940dc 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/resume.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/resume.rs @@ -69,10 +69,10 @@ fn resume_rewound_run_succeeds() { String::from_utf8_lossy(&rewind.stdout), output_stderr(&rewind) ); - let rewound_head = git_stdout( - &setup.repo_dir, - &["rev-parse", &format!("fabro/run/{}", setup.run.run_id)], - ); + let rewound_head = git_stdout(&setup.repo_dir, &[ + "rev-parse", + &format!("fabro/run/{}", setup.run.run_id), + ]); let mut resume_cmd = context.command(); resume_cmd.current_dir(&setup.repo_dir); @@ -109,10 +109,10 @@ fn resume_rewound_run_succeeds() { std::fs::read_to_string(setup.repo_dir.join("story.txt")).unwrap(), "line 1\n" ); - let resumed_head = git_stdout( - &setup.repo_dir, - &["rev-parse", &format!("fabro/run/{}", setup.run.run_id)], - ); + let resumed_head = git_stdout(&setup.repo_dir, &[ + "rev-parse", + &format!("fabro/run/{}", setup.run.run_id), + ]); assert_ne!(resumed_head.trim(), rewound_head.trim()); } diff --git a/lib/crates/fabro-cli/tests/it/cmd/rewind.rs b/lib/crates/fabro-cli/tests/it/cmd/rewind.rs index b5052f165..3122c81e7 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/rewind.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/rewind.rs @@ -99,10 +99,10 @@ fn rewind_target_updates_metadata_and_resume_hint() { "); assert!(output.status.success(), "rewind should succeed"); - let run_head = git_stdout( - &setup.repo_dir, - &["rev-parse", &format!("fabro/run/{}", setup.run.run_id)], - ); + let run_head = git_stdout(&setup.repo_dir, &[ + "rev-parse", + &format!("fabro/run/{}", setup.run.run_id), + ]); assert_eq!(run_head.trim(), expected_run_head); let mut list_cmd = context.command(); diff --git a/lib/crates/fabro-cli/tests/it/cmd/support.rs b/lib/crates/fabro-cli/tests/it/cmd/support.rs index 71f702430..5c78df473 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/support.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs @@ -31,23 +31,23 @@ struct RunSummaryRecord { } pub(crate) struct RunSetup { - pub(crate) run_id: String, + pub(crate) run_id: String, pub(crate) run_dir: PathBuf, } pub(crate) struct GitRunSetup { - pub(crate) run: RunSetup, + pub(crate) run: RunSetup, pub(crate) repo_dir: PathBuf, pub(crate) base_sha: String, } pub(crate) struct ProjectFixture { pub(crate) project_dir: PathBuf, - pub(crate) fabro_root: PathBuf, + pub(crate) fabro_root: PathBuf, } pub(crate) struct WorkspaceRunSetup { - pub(crate) run: RunSetup, + pub(crate) run: RunSetup, pub(crate) workspace_dir: PathBuf, } @@ -148,10 +148,10 @@ fn run_completed_dry_run(context: &TestContext, workflow: &Path) -> RunSetup { run_dir: context.find_run_dir(&run_id), run_id, }; - wait_for_event_names( - &run_setup.run_dir, - &["run.completed", "sandbox.cleanup.completed"], - ); + wait_for_event_names(&run_setup.run_dir, &[ + "run.completed", + "sandbox.cleanup.completed", + ]); run_setup } @@ -719,10 +719,11 @@ 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()) @@ -735,14 +736,11 @@ 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()) @@ -930,11 +928,9 @@ 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]; @@ -943,9 +939,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]; @@ -953,9 +949,8 @@ fn setup_git_backed_run(context: &TestContext, workflow: GitWorkflowKind) -> Git start -> check -> exit; } "# - } - }, - ); + } + }); git_success(&repo_dir, &["add", "story.txt", "flow.fabro"]); git_success(&repo_dir, &["commit", "-qm", "init"]); diff --git a/lib/crates/fabro-config/src/effective_settings.rs b/lib/crates/fabro-config/src/effective_settings.rs index aed8cefb4..46fd13ee9 100644 --- a/lib/crates/fabro-config/src/effective_settings.rs +++ b/lib/crates/fabro-config/src/effective_settings.rs @@ -25,10 +25,10 @@ pub enum EffectiveSettingsMode { #[derive(Clone, Debug, Default)] pub struct EffectiveSettingsLayers { - pub args: SettingsLayer, + pub args: SettingsLayer, pub workflow: SettingsLayer, - pub project: SettingsLayer, - pub user: SettingsLayer, + pub project: SettingsLayer, + pub user: SettingsLayer, } impl EffectiveSettingsLayers { diff --git a/lib/crates/fabro-config/src/error.rs b/lib/crates/fabro-config/src/error.rs index 6061c8d62..ead3042ad 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 cc8a4e11f..9ff89feef 100644 --- a/lib/crates/fabro-config/src/merge.rs +++ b/lib/crates/fabro-config/src/merge.rs @@ -31,12 +31,12 @@ use fabro_types::settings::workflow::WorkflowLayer; #[must_use] pub fn combine_files(lower: SettingsLayer, higher: SettingsLayer) -> SettingsLayer { SettingsLayer { - version: higher.version.or(lower.version), - project: merge_option(lower.project, higher.project, combine_project), + version: higher.version.or(lower.version), + project: merge_option(lower.project, higher.project, combine_project), workflow: merge_option(lower.workflow, higher.workflow, combine_workflow), - run: merge_option(lower.run, higher.run, combine_run), - cli: merge_option(lower.cli, higher.cli, combine_cli), - server: merge_option(lower.server, higher.server, combine_server), + run: merge_option(lower.run, higher.run, combine_run), + cli: merge_option(lower.cli, higher.cli, combine_cli), + server: merge_option(lower.server, higher.server, combine_server), features: replace_if_some(lower.features, higher.features), } } @@ -75,10 +75,10 @@ fn merge_string_map_sticky( fn combine_project(lower: ProjectLayer, higher: ProjectLayer) -> ProjectLayer { ProjectLayer { - name: higher.name.or(lower.name), + name: higher.name.or(lower.name), description: higher.description.or(lower.description), - directory: higher.directory.or(lower.directory), - metadata: merge_string_map_replace(lower.metadata, higher.metadata), + directory: higher.directory.or(lower.directory), + metadata: merge_string_map_replace(lower.metadata, higher.metadata), } } @@ -86,10 +86,10 @@ fn combine_project(lower: ProjectLayer, higher: ProjectLayer) -> ProjectLayer { fn combine_workflow(lower: WorkflowLayer, higher: WorkflowLayer) -> WorkflowLayer { WorkflowLayer { - name: higher.name.or(lower.name), + name: higher.name.or(lower.name), description: higher.description.or(lower.description), - graph: higher.graph.or(lower.graph), - metadata: merge_string_map_replace(lower.metadata, higher.metadata), + graph: higher.graph.or(lower.graph), + metadata: merge_string_map_replace(lower.metadata, higher.metadata), } } @@ -97,30 +97,30 @@ fn combine_workflow(lower: WorkflowLayer, higher: WorkflowLayer) -> WorkflowLaye fn combine_run(lower: RunLayer, higher: RunLayer) -> RunLayer { RunLayer { - goal: higher.goal.or(lower.goal), - working_dir: higher.working_dir.or(lower.working_dir), - metadata: merge_string_map_replace(lower.metadata, higher.metadata), - inputs: higher.inputs.or(lower.inputs), - model: merge_option(lower.model, higher.model, combine_run_model), - git: merge_option(lower.git, higher.git, combine_run_git), - prepare: merge_option(lower.prepare, higher.prepare, combine_run_prepare), - execution: merge_option(lower.execution, higher.execution, combine_run_execution), - checkpoint: merge_option(lower.checkpoint, higher.checkpoint, combine_run_checkpoint), - sandbox: merge_option(lower.sandbox, higher.sandbox, combine_run_sandbox), + goal: higher.goal.or(lower.goal), + working_dir: higher.working_dir.or(lower.working_dir), + metadata: merge_string_map_replace(lower.metadata, higher.metadata), + inputs: higher.inputs.or(lower.inputs), + model: merge_option(lower.model, higher.model, combine_run_model), + git: merge_option(lower.git, higher.git, combine_run_git), + prepare: merge_option(lower.prepare, higher.prepare, combine_run_prepare), + execution: merge_option(lower.execution, higher.execution, combine_run_execution), + checkpoint: merge_option(lower.checkpoint, higher.checkpoint, combine_run_checkpoint), + sandbox: merge_option(lower.sandbox, higher.sandbox, combine_run_sandbox), notifications: combine_notifications(lower.notifications, higher.notifications), - interviews: merge_option(lower.interviews, higher.interviews, combine_interviews), - agent: merge_option(lower.agent, higher.agent, combine_run_agent), - hooks: combine_hooks(lower.hooks, higher.hooks), - scm: merge_option(lower.scm, higher.scm, combine_run_scm), - pull_request: merge_option(lower.pull_request, higher.pull_request, combine_run_pr), - artifacts: replace_if_some(lower.artifacts, higher.artifacts), + interviews: merge_option(lower.interviews, higher.interviews, combine_interviews), + agent: merge_option(lower.agent, higher.agent, combine_run_agent), + hooks: combine_hooks(lower.hooks, higher.hooks), + scm: merge_option(lower.scm, higher.scm, combine_run_scm), + pull_request: merge_option(lower.pull_request, higher.pull_request, combine_run_pr), + artifacts: replace_if_some(lower.artifacts, higher.artifacts), } } fn combine_run_model(lower: RunModelLayer, higher: RunModelLayer) -> RunModelLayer { RunModelLayer { - provider: higher.provider.or(lower.provider), - name: higher.name.or(lower.name), + provider: higher.provider.or(lower.provider), + name: higher.name.or(lower.name), fallbacks: splice_model_fallbacks(lower.fallbacks, higher.fallbacks), } } @@ -162,7 +162,7 @@ fn combine_run_git(lower: RunGitLayer, higher: RunGitLayer) -> RunGitLayer { fn combine_git_author(lower: GitAuthorLayer, higher: GitAuthorLayer) -> GitAuthorLayer { GitAuthorLayer { - name: higher.name.or(lower.name), + name: higher.name.or(lower.name), email: higher.email.or(lower.email), } } @@ -174,9 +174,9 @@ fn combine_run_prepare(_lower: RunPrepareLayer, higher: RunPrepareLayer) -> RunP fn combine_run_execution(lower: RunExecutionLayer, higher: RunExecutionLayer) -> RunExecutionLayer { RunExecutionLayer { - mode: higher.mode.or(lower.mode), + mode: higher.mode.or(lower.mode), approval: higher.approval.or(lower.approval), - retros: higher.retros.or(lower.retros), + retros: higher.retros.or(lower.retros), } } @@ -194,13 +194,13 @@ fn combine_run_checkpoint( fn combine_run_sandbox(lower: RunSandboxLayer, higher: RunSandboxLayer) -> RunSandboxLayer { RunSandboxLayer { - provider: higher.provider.or(lower.provider), - preserve: higher.preserve.or(lower.preserve), + provider: higher.provider.or(lower.provider), + preserve: higher.preserve.or(lower.preserve), devcontainer: higher.devcontainer.or(lower.devcontainer), // Sticky merge-by-key for run.sandbox.env per R71. - env: merge_string_map_sticky(lower.env, higher.env), - local: higher.local.or(lower.local), - daytona: merge_option(lower.daytona, higher.daytona, combine_daytona), + env: merge_string_map_sticky(lower.env, higher.env), + local: higher.local.or(lower.local), + daytona: merge_option(lower.daytona, higher.daytona, combine_daytona), } } @@ -208,10 +208,10 @@ fn combine_daytona(lower: DaytonaSandboxLayer, higher: DaytonaSandboxLayer) -> D DaytonaSandboxLayer { auto_stop_interval: higher.auto_stop_interval.or(lower.auto_stop_interval), // Sticky merge-by-key for provider-native labels per R71. - labels: merge_string_map_sticky(lower.labels, higher.labels), - snapshot: higher.snapshot.or(lower.snapshot), - network: higher.network.or(lower.network), - skip_clone: higher.skip_clone.or(lower.skip_clone), + labels: merge_string_map_sticky(lower.labels, higher.labels), + snapshot: higher.snapshot.or(lower.snapshot), + network: higher.network.or(lower.network), + skip_clone: higher.skip_clone.or(lower.skip_clone), } } @@ -237,12 +237,12 @@ fn combine_notification_route( higher: NotificationRouteLayer, ) -> NotificationRouteLayer { NotificationRouteLayer { - enabled: higher.enabled.or(lower.enabled), + enabled: higher.enabled.or(lower.enabled), provider: higher.provider.or(lower.provider), - events: splice_events(lower.events, higher.events), - slack: higher.slack.or(lower.slack), - discord: higher.discord.or(lower.discord), - teams: higher.teams.or(lower.teams), + events: splice_events(lower.events, higher.events), + slack: higher.slack.or(lower.slack), + discord: higher.discord.or(lower.discord), + teams: higher.teams.or(lower.teams), } } @@ -275,9 +275,9 @@ fn splice_events(lower: Vec, higher: Vec) -> Vec fn combine_interviews(lower: InterviewsLayer, higher: InterviewsLayer) -> InterviewsLayer { InterviewsLayer { provider: higher.provider.or(lower.provider), - slack: higher.slack.or(lower.slack), - discord: higher.discord.or(lower.discord), - teams: higher.teams.or(lower.teams), + slack: higher.slack.or(lower.slack), + discord: higher.discord.or(lower.discord), + teams: higher.teams.or(lower.teams), } } @@ -285,7 +285,7 @@ fn combine_run_agent(lower: RunAgentLayer, higher: RunAgentLayer) -> RunAgentLay RunAgentLayer { permissions: higher.permissions.or(lower.permissions), // MCP entries: field-merge per key. Higher replaces lower for same keys. - mcps: merge_string_map_sticky(lower.mcps, higher.mcps), + mcps: merge_string_map_sticky(lower.mcps, higher.mcps), } } @@ -320,18 +320,18 @@ fn combine_hooks(lower: Vec, higher: Vec) -> Vec RunScmLayer { RunScmLayer { - provider: higher.provider.or(lower.provider), - owner: higher.owner.or(lower.owner), + provider: higher.provider.or(lower.provider), + owner: higher.owner.or(lower.owner), repository: higher.repository.or(lower.repository), - github: higher.github.or(lower.github), + github: higher.github.or(lower.github), } } fn combine_run_pr(lower: RunPullRequestLayer, higher: RunPullRequestLayer) -> RunPullRequestLayer { RunPullRequestLayer { - enabled: higher.enabled.or(lower.enabled), - draft: higher.draft.or(lower.draft), - auto_merge: higher.auto_merge.or(lower.auto_merge), + enabled: higher.enabled.or(lower.enabled), + draft: higher.draft.or(lower.draft), + auto_merge: higher.auto_merge.or(lower.auto_merge), merge_strategy: higher.merge_strategy.or(lower.merge_strategy), } } @@ -340,10 +340,10 @@ fn combine_run_pr(lower: RunPullRequestLayer, higher: RunPullRequestLayer) -> Ru fn combine_cli(lower: CliLayer, higher: CliLayer) -> CliLayer { CliLayer { - target: merge_option(lower.target, higher.target, combine_cli_target), - auth: higher.auth.or(lower.auth), - exec: merge_option(lower.exec, higher.exec, combine_cli_exec), - output: higher.output.or(lower.output), + target: merge_option(lower.target, higher.target, combine_cli_target), + auth: higher.auth.or(lower.auth), + exec: merge_option(lower.exec, higher.exec, combine_cli_exec), + output: higher.output.or(lower.output), updates: higher.updates.or(lower.updates), logging: higher.logging.or(lower.logging), } @@ -357,8 +357,8 @@ fn combine_cli_target(_lower: CliTargetLayer, higher: CliTargetLayer) -> CliTarg fn combine_cli_exec(lower: CliExecLayer, higher: CliExecLayer) -> CliExecLayer { CliExecLayer { prevent_idle_sleep: higher.prevent_idle_sleep.or(lower.prevent_idle_sleep), - model: merge_option(lower.model, higher.model, combine_cli_exec_model), - agent: merge_option(lower.agent, higher.agent, combine_cli_exec_agent), + model: merge_option(lower.model, higher.model, combine_cli_exec_model), + agent: merge_option(lower.agent, higher.agent, combine_cli_exec_agent), } } @@ -368,7 +368,7 @@ fn combine_cli_exec_model( ) -> CliExecModelLayer { CliExecModelLayer { provider: higher.provider.or(lower.provider), - name: higher.name.or(lower.name), + name: higher.name.or(lower.name), } } @@ -378,7 +378,7 @@ fn combine_cli_exec_agent( ) -> CliExecAgentLayer { CliExecAgentLayer { permissions: higher.permissions.or(lower.permissions), - mcps: merge_string_map_sticky(lower.mcps, higher.mcps), + mcps: merge_string_map_sticky(lower.mcps, higher.mcps), } } @@ -386,15 +386,15 @@ fn combine_cli_exec_agent( fn combine_server(lower: ServerLayer, higher: ServerLayer) -> ServerLayer { ServerLayer { - listen: merge_option(lower.listen, higher.listen, combine_listen), - api: higher.api.or(lower.api), - web: merge_option(lower.web, higher.web, combine_server_web), - auth: merge_option(lower.auth, higher.auth, combine_server_auth), - storage: merge_option(lower.storage, higher.storage, combine_server_storage), - artifacts: merge_option(lower.artifacts, higher.artifacts, combine_server_artifacts), - slatedb: merge_option(lower.slatedb, higher.slatedb, combine_server_slatedb), - scheduler: merge_option(lower.scheduler, higher.scheduler, combine_server_scheduler), - logging: higher.logging.or(lower.logging), + listen: merge_option(lower.listen, higher.listen, combine_listen), + api: higher.api.or(lower.api), + web: merge_option(lower.web, higher.web, combine_server_web), + auth: merge_option(lower.auth, higher.auth, combine_server_auth), + storage: merge_option(lower.storage, higher.storage, combine_server_storage), + artifacts: merge_option(lower.artifacts, higher.artifacts, combine_server_artifacts), + slatedb: merge_option(lower.slatedb, higher.slatedb, combine_server_slatedb), + scheduler: merge_option(lower.scheduler, higher.scheduler, combine_server_scheduler), + logging: higher.logging.or(lower.logging), integrations: merge_option( lower.integrations, higher.integrations, @@ -411,7 +411,7 @@ fn combine_listen(_lower: ServerListenLayer, higher: ServerListenLayer) -> Serve fn combine_server_web(lower: ServerWebLayer, higher: ServerWebLayer) -> ServerWebLayer { ServerWebLayer { enabled: higher.enabled.or(lower.enabled), - url: higher.url.or(lower.url), + url: higher.url.or(lower.url), } } @@ -437,9 +437,9 @@ fn combine_server_artifacts( ) -> ServerArtifactsLayer { ServerArtifactsLayer { provider: higher.provider.or(lower.provider), - prefix: higher.prefix.or(lower.prefix), - local: higher.local.or(lower.local), - s3: higher.s3.or(lower.s3), + prefix: higher.prefix.or(lower.prefix), + local: higher.local.or(lower.local), + s3: higher.s3.or(lower.s3), } } @@ -448,11 +448,11 @@ fn combine_server_slatedb( higher: ServerSlateDbLayer, ) -> ServerSlateDbLayer { ServerSlateDbLayer { - provider: higher.provider.or(lower.provider), - prefix: higher.prefix.or(lower.prefix), + provider: higher.provider.or(lower.provider), + prefix: higher.prefix.or(lower.prefix), flush_interval: higher.flush_interval.or(lower.flush_interval), - local: higher.local.or(lower.local), - s3: higher.s3.or(lower.s3), + local: higher.local.or(lower.local), + s3: higher.s3.or(lower.s3), } } @@ -470,10 +470,10 @@ fn combine_server_integrations( higher: ServerIntegrationsLayer, ) -> ServerIntegrationsLayer { ServerIntegrationsLayer { - github: higher.github.or(lower.github), - slack: higher.slack.or(lower.slack), + github: higher.github.or(lower.github), + slack: higher.slack.or(lower.slack), discord: higher.discord.or(lower.discord), - teams: higher.teams.or(lower.teams), + teams: higher.teams.or(lower.teams), } } diff --git a/lib/crates/fabro-config/src/parse.rs b/lib/crates/fabro-config/src/parse.rs index ea975708b..219c04939 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 862d8ad3c..087fd619e 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 b2ba0decf..f588c4fb3 100644 --- a/lib/crates/fabro-config/src/resolve/cli.rs +++ b/lib/crates/fabro-config/src/resolve/cli.rs @@ -8,13 +8,13 @@ use super::{ResolveError, require_interp}; pub fn resolve_cli(layer: &CliLayer, errors: &mut Vec) -> CliSettings { CliSettings { - target: resolve_target(layer.target.as_ref(), errors), - auth: CliAuthSettings { + target: resolve_target(layer.target.as_ref(), errors), + auth: CliAuthSettings { strategy: layer.auth.as_ref().and_then(|auth| auth.strategy), }, - exec: resolve_exec(layer.exec.as_ref()), - output: CliOutputSettings { - format: layer + exec: resolve_exec(layer.exec.as_ref()), + output: CliOutputSettings { + format: layer .output .as_ref() .and_then(|output| output.format) @@ -50,8 +50,8 @@ fn resolve_target( url: require_interp(url.as_ref(), "cli.target.url", errors), tls: tls.as_ref().map(|tls| CliTargetTlsSettings { cert: require_interp(tls.cert.as_ref(), "cli.target.tls.cert", errors), - key: require_interp(tls.key.as_ref(), "cli.target.tls.key", errors), - ca: require_interp(tls.ca.as_ref(), "cli.target.tls.ca", errors), + key: require_interp(tls.key.as_ref(), "cli.target.tls.key", errors), + ca: require_interp(tls.ca.as_ref(), "cli.target.tls.ca", errors), }), }), Some(CliTargetLayer::Unix { path }) => Some(CliTargetSettings::Unix { @@ -68,13 +68,13 @@ fn resolve_exec(exec: Option<&CliExecLayer>) -> CliExecSettings { CliExecSettings { prevent_idle_sleep: exec.prevent_idle_sleep.unwrap_or(false), - model: CliExecModelSettings { + model: CliExecModelSettings { provider: exec.model.as_ref().and_then(|model| model.provider.clone()), - name: exec.model.as_ref().and_then(|model| model.name.clone()), + name: exec.model.as_ref().and_then(|model| model.name.clone()), }, - agent: CliExecAgentSettings { + agent: CliExecAgentSettings { permissions: exec.agent.as_ref().and_then(|agent| agent.permissions), - mcps: exec + mcps: exec .agent .as_ref() .map(|agent| { diff --git a/lib/crates/fabro-config/src/resolve/mod.rs b/lib/crates/fabro-config/src/resolve/mod.rs index 6f27cbe23..2698134b5 100644 --- a/lib/crates/fabro-config/src/resolve/mod.rs +++ b/lib/crates/fabro-config/src/resolve/mod.rs @@ -28,11 +28,11 @@ pub fn resolve(file: &SettingsLayer) -> Result> { let features_layer = file.features.clone().unwrap_or_default(); let settings = Settings { - project: resolve_project(&project_layer, &mut errors), + project: resolve_project(&project_layer, &mut errors), workflow: resolve_workflow(&workflow_layer, &mut errors), - run: resolve_run(&run_layer, &mut errors), - cli: resolve_cli(&cli_layer, &mut errors), - server: resolve_server(&server_layer, &mut errors), + run: resolve_run(&run_layer, &mut errors), + cli: resolve_cli(&cli_layer, &mut errors), + server: resolve_server(&server_layer, &mut errors), features: resolve_features(&features_layer, &mut errors), }; @@ -96,7 +96,7 @@ pub(crate) fn parse_socket_addr( Ok(address) => address, Err(err) => { errors.push(ResolveError::ParseFailure { - path: path.to_string(), + path: path.to_string(), reason: err.to_string(), }); std::net::SocketAddr::from(([127, 0, 0, 1], 0)) diff --git a/lib/crates/fabro-config/src/resolve/project.rs b/lib/crates/fabro-config/src/resolve/project.rs index 7c4f563bd..16d5db288 100644 --- a/lib/crates/fabro-config/src/resolve/project.rs +++ b/lib/crates/fabro-config/src/resolve/project.rs @@ -6,12 +6,12 @@ const DEFAULT_PROJECT_DIRECTORY: &str = "."; pub fn resolve_project(layer: &ProjectLayer, _errors: &mut Vec) -> ProjectSettings { ProjectSettings { - name: layer.name.clone(), + name: layer.name.clone(), description: layer.description.clone(), - directory: layer + directory: layer .directory .clone() .unwrap_or_else(|| DEFAULT_PROJECT_DIRECTORY.to_string()), - metadata: layer.metadata.clone(), + metadata: layer.metadata.clone(), } } diff --git a/lib/crates/fabro-config/src/resolve/run.rs b/lib/crates/fabro-config/src/resolve/run.rs index d228b69fe..ed1c06265 100644 --- a/lib/crates/fabro-config/src/resolve/run.rs +++ b/lib/crates/fabro-config/src/resolve/run.rs @@ -17,32 +17,32 @@ use super::ResolveError; pub fn resolve_run(layer: &RunLayer, errors: &mut Vec) -> RunSettings { RunSettings { - goal: resolve_goal(layer.goal.as_ref()), - working_dir: layer.working_dir.clone(), - metadata: layer.metadata.clone(), - inputs: layer.inputs.clone().unwrap_or_default(), - model: resolve_model(layer.model.as_ref()), - git: resolve_git(layer.git.as_ref()), - prepare: resolve_prepare(layer.prepare.as_ref(), errors), - execution: resolve_execution(layer.execution.as_ref()), - checkpoint: resolve_checkpoint(layer.checkpoint.as_ref()), - sandbox: resolve_sandbox(layer.sandbox.as_ref(), errors), + goal: resolve_goal(layer.goal.as_ref()), + working_dir: layer.working_dir.clone(), + metadata: layer.metadata.clone(), + inputs: layer.inputs.clone().unwrap_or_default(), + model: resolve_model(layer.model.as_ref()), + git: resolve_git(layer.git.as_ref()), + prepare: resolve_prepare(layer.prepare.as_ref(), errors), + execution: resolve_execution(layer.execution.as_ref()), + checkpoint: resolve_checkpoint(layer.checkpoint.as_ref()), + sandbox: resolve_sandbox(layer.sandbox.as_ref(), errors), notifications: layer .notifications .iter() .map(|(name, route)| (name.clone(), resolve_notification_route(route))) .collect(), - interviews: resolve_interviews(layer.interviews.as_ref()), - agent: resolve_agent(layer.agent.as_ref()), - hooks: layer + interviews: resolve_interviews(layer.interviews.as_ref()), + agent: resolve_agent(layer.agent.as_ref()), + hooks: layer .hooks .iter() .enumerate() .map(|(index, hook)| resolve_hook(hook, index, errors)) .collect(), - scm: resolve_scm(layer.scm.as_ref()), - pull_request: resolve_pull_request(layer.pull_request.as_ref()), - artifacts: resolve_artifacts(layer.artifacts.as_ref()), + scm: resolve_scm(layer.scm.as_ref()), + pull_request: resolve_pull_request(layer.pull_request.as_ref()), + artifacts: resolve_artifacts(layer.artifacts.as_ref()), } } @@ -59,8 +59,8 @@ fn resolve_model(model: Option<&RunModelLayer>) -> RunModelSettings { }; RunModelSettings { - provider: model.provider.clone(), - name: model.name.clone(), + provider: model.provider.clone(), + name: model.name.clone(), fallbacks: model .fallbacks .iter() @@ -76,7 +76,7 @@ fn resolve_git(git: Option<&RunGitLayer>) -> RunGitSettings { RunGitSettings { author: git.and_then(|git| { git.author.as_ref().map(|author| GitAuthorSettings { - name: author.name.clone(), + name: author.name.clone(), email: author.email.clone(), }) }), @@ -102,7 +102,7 @@ fn resolve_prepare( .join(" "), ), (Some(_), Some(_)) | (None, None) => errors.push(ResolveError::Invalid { - path: format!("run.prepare.steps[{index}]"), + path: format!("run.prepare.steps[{index}]"), reason: "exactly one of script or command must be set".to_string(), }), } @@ -122,9 +122,9 @@ fn resolve_execution(execution: Option<&RunExecutionLayer>) -> RunExecutionSetti }; RunExecutionSettings { - mode: execution.mode.unwrap_or(RunMode::Normal), + mode: execution.mode.unwrap_or(RunMode::Normal), approval: execution.approval.unwrap_or(ApprovalMode::Prompt), - retros: execution.retros.unwrap_or(true), + retros: execution.retros.unwrap_or(true), } } @@ -151,7 +151,7 @@ fn resolve_sandbox( match provider.as_str() { "local" | "docker" | "daytona" => {} other => errors.push(ResolveError::Invalid { - path: "run.sandbox.provider".to_string(), + path: "run.sandbox.provider".to_string(), reason: format!("unknown sandbox provider: {other}"), }), } @@ -179,13 +179,13 @@ fn resolve_local_sandbox(sandbox: &RunSandboxLayer) -> LocalSandboxSettings { fn resolve_daytona(daytona: &DaytonaSandboxLayer) -> DaytonaSettings { DaytonaSettings { auto_stop_interval: daytona.auto_stop_interval, - labels: daytona.labels.clone(), - snapshot: daytona.snapshot.as_ref().and_then(|snapshot| { + labels: daytona.labels.clone(), + snapshot: daytona.snapshot.as_ref().and_then(|snapshot| { snapshot.name.as_ref().map(|name| DaytonaSnapshotSettings { - name: name.clone(), - cpu: snapshot.cpu, - memory_gb: snapshot.memory.map(|size| size_to_gb_i32(size.as_bytes())), - disk_gb: snapshot.disk.map(|size| size_to_gb_i32(size.as_bytes())), + name: name.clone(), + cpu: snapshot.cpu, + memory_gb: snapshot.memory.map(|size| size_to_gb_i32(size.as_bytes())), + disk_gb: snapshot.disk.map(|size| size_to_gb_i32(size.as_bytes())), dockerfile: snapshot .dockerfile .as_ref() @@ -199,16 +199,16 @@ fn resolve_daytona(daytona: &DaytonaSandboxLayer) -> DaytonaSettings { }), }) }), - network: daytona.network.clone(), - skip_clone: daytona.skip_clone.unwrap_or(false), + network: daytona.network.clone(), + skip_clone: daytona.skip_clone.unwrap_or(false), } } fn resolve_notification_route(route: &NotificationRouteLayer) -> NotificationRouteSettings { NotificationRouteSettings { - enabled: route.enabled.unwrap_or(false), + enabled: route.enabled.unwrap_or(false), provider: route.provider.clone(), - events: route + events: route .events .iter() .filter_map(|event| match event { @@ -216,9 +216,9 @@ fn resolve_notification_route(route: &NotificationRouteLayer) -> NotificationRou StringOrSplice::Splice => None, }) .collect(), - slack: route.slack.as_ref().map(resolve_notification_provider), - discord: route.discord.as_ref().map(resolve_notification_provider), - teams: route.teams.as_ref().map(resolve_notification_provider), + slack: route.slack.as_ref().map(resolve_notification_provider), + discord: route.discord.as_ref().map(resolve_notification_provider), + teams: route.teams.as_ref().map(resolve_notification_provider), } } @@ -237,9 +237,9 @@ fn resolve_interviews(interviews: Option<&InterviewsLayer>) -> RunInterviewsSett RunInterviewsSettings { provider: interviews.provider.clone(), - slack: interviews.slack.as_ref().map(resolve_interview_provider), - discord: interviews.discord.as_ref().map(resolve_interview_provider), - teams: interviews.teams.as_ref().map(resolve_interview_provider), + slack: interviews.slack.as_ref().map(resolve_interview_provider), + discord: interviews.discord.as_ref().map(resolve_interview_provider), + teams: interviews.teams.as_ref().map(resolve_interview_provider), } } @@ -256,7 +256,7 @@ fn resolve_agent(agent: Option<&RunAgentLayer>) -> RunAgentSettings { RunAgentSettings { permissions: agent.permissions, - mcps: agent + mcps: agent .mcps .iter() .map(|(name, entry)| (name.clone(), resolve_mcp_entry(name, entry))) @@ -273,13 +273,13 @@ pub(crate) fn resolve_mcp_entry(name: &str, entry: &McpEntryLayer) -> McpServerS .. } => McpTransport::Stdio { command: resolve_mcp_command(script.as_ref(), command.as_ref()), - env: env + env: env .iter() .map(|(key, value)| (key.clone(), value.as_source())) .collect(), }, McpEntryLayer::Http { url, headers, .. } => McpTransport::Http { - url: url.as_source(), + url: url.as_source(), headers: headers .iter() .map(|(key, value)| (key.clone(), value.as_source())) @@ -293,8 +293,8 @@ pub(crate) fn resolve_mcp_entry(name: &str, entry: &McpEntryLayer) -> McpServerS .. } => McpTransport::Sandbox { command: resolve_mcp_command(script.as_ref(), command.as_ref()), - port: *port, - env: env + port: *port, + env: env .iter() .map(|(key, value)| (key.clone(), value.as_source())) .collect(), @@ -355,7 +355,7 @@ fn resolve_hook(hook: &HookEntry, index: usize, errors: &mut Vec) if variants != 1 { errors.push(ResolveError::Invalid { - path: format!("run.hooks[{index}]"), + path: format!("run.hooks[{index}]"), reason: "exactly one hook transport must be configured".to_string(), }); } @@ -419,19 +419,19 @@ fn resolve_hook_type(hook: &HookEntry) -> Option { if hook.agent == Some(HookAgentMarker::Enabled) { return Some(HookType::Agent { - prompt: hook + prompt: hook .prompt .as_ref() .map(InterpString::as_source) .unwrap_or_default(), - model: hook.model.as_ref().map(InterpString::as_source), + model: hook.model.as_ref().map(InterpString::as_source), max_tool_rounds: hook.max_tool_rounds, }); } hook.prompt.as_ref().map(|prompt| HookType::Prompt { prompt: prompt.as_source(), - model: hook.model.as_ref().map(InterpString::as_source), + model: hook.model.as_ref().map(InterpString::as_source), }) } @@ -441,10 +441,10 @@ fn resolve_scm(scm: Option<&RunScmLayer>) -> RunScmSettings { }; RunScmSettings { - provider: scm.provider.clone(), - owner: scm.owner.clone(), + provider: scm.provider.clone(), + owner: scm.owner.clone(), repository: scm.repository.clone(), - github: scm.github.as_ref().map(|_| ScmGitHubSettings), + github: scm.github.as_ref().map(|_| ScmGitHubSettings), } } @@ -455,9 +455,9 @@ fn resolve_pull_request(pull_request: Option<&RunPullRequestLayer>) -> Option, layer: Option<&ServerWebLayer>) -> ServerWebSettings { ServerWebSettings { enabled: layer.and_then(|web| web.enabled).unwrap_or(true), - url: layer + url: layer .and_then(|web| web.url.clone()) .unwrap_or_else(|| InterpString::parse("http://localhost:3000")), } @@ -125,20 +125,20 @@ fn resolve_auth( let jwt = api.and_then(|api| { api.jwt.as_ref().map(|jwt| ServerAuthApiJwtSettings { - enabled: jwt.enabled.unwrap_or(true), - issuer: jwt.issuer.clone(), + enabled: jwt.enabled.unwrap_or(true), + issuer: jwt.issuer.clone(), audience: jwt.audience.clone(), }) }); let mtls = api.and_then(|api| { api.mtls.as_ref().map(|mtls| ServerAuthApiMtlsSettings { enabled: mtls.enabled.unwrap_or(true), - ca: mtls.ca.clone(), + ca: mtls.ca.clone(), }) }); if mtls.as_ref().is_some_and(|mtls| mtls.enabled) && !valid_tls { errors.push(ResolveError::Invalid { - path: "server.auth.api.mtls".to_string(), + path: "server.auth.api.mtls".to_string(), reason: "requires tcp listen with tls cert, key, and ca configured".to_string(), }); } @@ -149,7 +149,7 @@ fn resolve_auth( allowed_usernames: web .map(|web| web.allowed_usernames.clone()) .unwrap_or_default(), - providers: ServerAuthWebProvidersSettings { + providers: ServerAuthWebProvidersSettings { github: web .and_then(|web| web.providers.as_ref()) .and_then(|providers| providers.github.as_ref()) @@ -161,8 +161,8 @@ fn resolve_auth( fn resolve_web_github(layer: &ServerAuthWebGithubLayer) -> GithubOauthSettings { GithubOauthSettings { - enabled: layer.enabled.unwrap_or(true), - client_id: layer.client_id.clone(), + enabled: layer.enabled.unwrap_or(true), + client_id: layer.client_id.clone(), client_secret: layer.client_secret.clone(), } } @@ -180,7 +180,7 @@ fn resolve_artifacts( prefix: layer .and_then(|artifacts| artifacts.prefix.clone()) .unwrap_or_else(|| InterpString::parse("artifacts")), - store: resolve_object_store( + store: resolve_object_store( provider, layer.and_then(|artifacts| artifacts.local.as_ref()), layer.and_then(|artifacts| artifacts.s3.as_ref()), @@ -201,10 +201,10 @@ fn resolve_slatedb( .unwrap_or(ObjectStoreProvider::Local); ServerSlateDbSettings { - prefix: layer + prefix: layer .and_then(|slatedb| slatedb.prefix.clone()) .unwrap_or_else(|| InterpString::parse("")), - store: resolve_object_store( + store: resolve_object_store( provider, layer.and_then(|slatedb| slatedb.local.as_ref()), layer.and_then(|slatedb| slatedb.s3.as_ref()), @@ -255,15 +255,15 @@ fn resolve_object_store( fn resolve_integrations(layer: Option<&ServerIntegrationsLayer>) -> ServerIntegrationsSettings { ServerIntegrationsSettings { - github: layer + github: layer .and_then(|integrations| integrations.github.as_ref()) .map(|github| GithubIntegrationSettings { - enabled: github.enabled.unwrap_or(true), - app_id: github.app_id.clone(), - client_id: github.client_id.clone(), - slug: github.slug.clone(), + enabled: github.enabled.unwrap_or(true), + app_id: github.app_id.clone(), + client_id: github.client_id.clone(), + slug: github.slug.clone(), permissions: github.permissions.clone(), - webhooks: github + webhooks: github .webhooks .as_ref() .map(|webhooks| IntegrationWebhooksSettings { @@ -271,10 +271,10 @@ fn resolve_integrations(layer: Option<&ServerIntegrationsLayer>) -> ServerIntegr }), }) .unwrap_or_default(), - slack: layer + slack: layer .and_then(|integrations| integrations.slack.as_ref()) .map(|slack| SlackIntegrationSettings { - enabled: slack.enabled.unwrap_or(true), + enabled: slack.enabled.unwrap_or(true), default_channel: slack.default_channel.clone(), }) .unwrap_or_default(), @@ -284,7 +284,7 @@ fn resolve_integrations(layer: Option<&ServerIntegrationsLayer>) -> ServerIntegr enabled: discord.enabled.unwrap_or(true), }) .unwrap_or_default(), - teams: layer + teams: layer .and_then(|integrations| integrations.teams.as_ref()) .map(|teams| TeamsIntegrationSettings { enabled: teams.enabled.unwrap_or(true), diff --git a/lib/crates/fabro-config/src/resolve/workflow.rs b/lib/crates/fabro-config/src/resolve/workflow.rs index 18e0603b7..770c9a385 100644 --- a/lib/crates/fabro-config/src/resolve/workflow.rs +++ b/lib/crates/fabro-config/src/resolve/workflow.rs @@ -9,12 +9,12 @@ pub fn resolve_workflow( _errors: &mut Vec, ) -> WorkflowSettings { WorkflowSettings { - name: layer.name.clone(), + name: layer.name.clone(), description: layer.description.clone(), - graph: layer + graph: layer .graph .clone() .unwrap_or_else(|| DEFAULT_WORKFLOW_GRAPH.to_string()), - metadata: layer.metadata.clone(), + metadata: layer.metadata.clone(), } } diff --git a/lib/crates/fabro-config/src/run.rs b/lib/crates/fabro-config/src/run.rs index d6a6d93af..e4e536913 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 f1877a10e..1ba3a71f9 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 13148ce21..60053930b 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,13 +1619,10 @@ 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] @@ -1695,13 +1692,10 @@ 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] @@ -1750,10 +1744,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] @@ -2042,9 +2036,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 { @@ -2054,11 +2048,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 0cb1744be..304e266a8 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 836925e37..16d7ecf4f 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 a13cf0082..e16b413cf 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 10c7e8fe9..8068900bc 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 10617ceb5..24dd1a0dd 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 c653af921..dcb544b77 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 065604233..6a29213a4 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 5f7a5725b..24e11268b 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 e6a3e1421..6d56ce254 100644 --- a/lib/crates/fabro-devcontainer/src/features.rs +++ b/lib/crates/fabro-devcontainer/src/features.rs @@ -13,9 +13,9 @@ use crate::types::{FeatureMetadata, LifecycleCommand}; #[derive(Debug, Clone)] pub(crate) struct FeatureLayer { /// Feature identifier (e.g. "ghcr.io/devcontainers/features/node:1") - pub id: String, + pub id: String, /// Directory name for COPY - pub dir_name: String, + pub dir_name: String, /// Dockerfile snippet for this feature pub dockerfile_snippet: String, } @@ -23,11 +23,11 @@ pub(crate) struct FeatureLayer { /// All resolved feature data: layers, environment, and lifecycle hooks. #[derive(Debug, Clone, Default)] pub(crate) struct ResolvedFeatures { - pub layers: Vec, - pub container_env: HashMap, - pub on_create_commands: Vec, + pub layers: Vec, + pub container_env: HashMap, + pub on_create_commands: Vec, pub post_create_commands: Vec, - pub post_start_commands: Vec, + pub post_start_commands: Vec, } /// Extract the directory name from a feature ID. @@ -628,16 +628,16 @@ pub(crate) async fn resolve_features( .get(id) .cloned() .unwrap_or_else(|| FeatureMetadata { - id: None, - name: None, - version: None, - options: HashMap::new(), - installs_after: Vec::new(), - depends_on: HashMap::new(), - container_env: HashMap::new(), - on_create_command: None, + id: None, + name: None, + version: None, + options: HashMap::new(), + installs_after: Vec::new(), + depends_on: HashMap::new(), + container_env: HashMap::new(), + on_create_command: None, post_create_command: None, - post_start_command: None, + post_start_command: None, }); // Collect feature containerEnv (later features override earlier) @@ -754,21 +754,18 @@ 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,36 +778,30 @@ 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"]); @@ -828,66 +819,54 @@ 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 @@ -905,25 +884,22 @@ 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( @@ -952,25 +928,22 @@ 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( @@ -999,16 +972,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( @@ -1039,36 +1012,30 @@ 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"]); @@ -1081,36 +1048,30 @@ 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"]); @@ -1157,37 +1118,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 @@ -1251,25 +1212,22 @@ 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( @@ -1287,16 +1245,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 832e197c6..036eeea06 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 a881bdad2..2e97d4e43 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,10 +228,9 @@ 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")); } @@ -243,10 +242,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 abb0dbd68..9b47fbfe6 100644 --- a/lib/crates/fabro-github/src/lib.rs +++ b/lib/crates/fabro-github/src/lib.rs @@ -13,21 +13,21 @@ pub fn github_api_base_url() -> String { /// Detailed information about a pull request from the GitHub API. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct PullRequestDetail { - pub number: u64, - pub title: String, - pub body: Option, - pub state: String, - pub draft: bool, - pub mergeable: Option, - pub additions: u64, - pub deletions: u64, + pub number: u64, + pub title: String, + pub body: Option, + pub state: String, + pub draft: bool, + pub mergeable: Option, + pub additions: u64, + pub deletions: u64, pub changed_files: u64, - pub html_url: String, - pub user: PullRequestUser, - pub head: PullRequestRef, - pub base: PullRequestRef, - pub created_at: String, - pub updated_at: String, + pub html_url: String, + pub user: PullRequestUser, + pub head: PullRequestRef, + pub base: PullRequestRef, + pub created_at: String, + pub updated_at: String, } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -50,14 +50,14 @@ pub struct AppOwner { /// Information about a GitHub App from the authenticated `/app` endpoint. #[derive(Debug, Clone, Deserialize)] pub struct AppInfo { - pub slug: String, + pub slug: String, pub owner: AppOwner, } /// Credentials for authenticating as a GitHub App. #[derive(Clone, Debug)] pub struct GitHubAppCredentials { - pub app_id: String, + pub app_id: String, pub private_key_pem: String, } @@ -106,7 +106,7 @@ pub enum HttpMethod { /// A minimal HTTP response for testability. pub struct HttpResponse { pub status: u16, - body: String, + body: String, } impl HttpResponse { @@ -386,8 +386,8 @@ pub async fn create_installation_access_token_for_pr( /// Result of a successful pull request creation. pub struct CreatedPullRequest { pub html_url: String, - pub number: u64, - pub node_id: String, + pub number: u64, + pub node_id: String, } /// Create a pull request on GitHub. @@ -409,8 +409,8 @@ pub async fn create_pull_request( #[derive(Deserialize)] struct PullRequestResponse { html_url: String, - number: u64, - node_id: String, + number: u64, + node_id: String, } let jwt = sign_app_jwt(&creds.app_id, &creds.private_key_pem)?; @@ -470,8 +470,8 @@ pub async fn create_pull_request( Ok(CreatedPullRequest { html_url: pr.html_url, - number: pr.number, - node_id: pr.node_id, + number: pr.number, + node_id: pr.node_id, }) } @@ -1219,11 +1219,11 @@ mod tests { // ----------------------------------------------------------------------- struct MockRoute { - method: HttpMethod, - path: String, - status: u16, - response_body: String, - assert_header: Option<(String, MockHeaderCheck)>, + method: HttpMethod, + path: String, + status: u16, + response_body: String, + assert_header: Option<(String, MockHeaderCheck)>, assert_body_json: Option, } @@ -1470,7 +1470,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }; let result = @@ -1502,7 +1502,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }; let result = @@ -1534,7 +1534,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }; let result = branch_exists_with_client(&mock, &creds, "owner", "repo", "broken", "").await; @@ -1702,7 +1702,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }; let detail = get_pull_request_with_client(&mock, &creds, "owner", "repo", 42, "") @@ -1739,7 +1739,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }; let err = get_pull_request_with_client(&mock, &creds, "owner", "repo", 999, "") @@ -1777,7 +1777,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }; merge_pull_request_with_client(&mock, &creds, "owner", "repo", 42, "squash", "") @@ -1804,7 +1804,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }; let err = merge_pull_request_with_client(&mock, &creds, "owner", "repo", 42, "squash", "") @@ -1832,7 +1832,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }; let err = merge_pull_request_with_client(&mock, &creds, "owner", "repo", 42, "squash", "") @@ -1869,7 +1869,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }; close_pull_request_with_client(&mock, &creds, "owner", "repo", 42, "") @@ -1896,7 +1896,7 @@ mod tests { let pem = test_rsa_key(); let creds = GitHubAppCredentials { - app_id: "test".to_string(), + app_id: "test".to_string(), private_key_pem: pem.to_string(), }; let err = close_pull_request_with_client(&mock, &creds, "owner", "repo", 999, "") diff --git a/lib/crates/fabro-github/tests/integration.rs b/lib/crates/fabro-github/tests/integration.rs index 8410cde49..47fa65e54 100644 --- a/lib/crates/fabro-github/tests/integration.rs +++ b/lib/crates/fabro-github/tests/integration.rs @@ -9,7 +9,7 @@ const TEST_RSA_KEY: &str = include_str!("../src/testdata/rsa_private.pem"); fn github_credentials() -> GitHubAppCredentials { GitHubAppCredentials { - app_id: "42".to_string(), + app_id: "42".to_string(), private_key_pem: TEST_RSA_KEY.to_string(), } } @@ -17,12 +17,12 @@ fn github_credentials() -> GitHubAppCredentials { fn standard_app_state() -> GitHubAppState { let mut state = GitHubAppState::new(); state.register_app(GitHubAppOptions { - app_id: "42".into(), - slug: "test-app".into(), - owner_login: "acme".into(), - public: true, + app_id: "42".into(), + slug: "test-app".into(), + owner_login: "acme".into(), + public: true, private_key_pem: TEST_RSA_KEY.into(), - webhook_secret: None, + webhook_secret: None, }); state.add_installation("42", "acme", vec!["widgets".into()], false); state.add_repository( diff --git a/lib/crates/fabro-graphviz/src/condition.rs b/lib/crates/fabro-graphviz/src/condition.rs index ca3ab63f0..09e82eb9d 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 { @@ -318,18 +318,15 @@ impl Parser { if op == Op::Eq || op == Op::NotEq { String::new() } else { - return Err(Error::Parse( - "expected value after operator".to_string(), - )); + return Err(Error::Parse("expected value after operator".to_string())); } } }; // Validate regex at parse time if op == Op::Matches { - regex::Regex::new(&value).map_err(|e| { - Error::Parse(format!("invalid regex pattern '{value}': {e}")) - })?; + regex::Regex::new(&value) + .map_err(|e| Error::Parse(format!("invalid regex pattern '{value}': {e}")))?; } Ok(ConditionExpr::Clause(Clause { key, op, value })) @@ -406,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(), }) ); @@ -420,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(), }), ]) @@ -439,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(), }) ); @@ -452,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(), }) ); @@ -532,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(), }) ); @@ -552,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(), }) ); @@ -565,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 8ba32cda7..8aa50e449 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 cd37b1825..8cd4d1b87 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,13 +141,10 @@ 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 eb82fa16b..64d7b6954 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 2b9a444e9..5037cefd7 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, } @@ -93,9 +93,7 @@ fn parse_selector(remaining: &mut &str) -> Result { .find(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != '-') .unwrap_or(remaining.len()); if end == 0 { - return Err(Error::Stylesheet( - "expected identifier after '#'".into(), - )); + return Err(Error::Stylesheet("expected identifier after '#'".into())); } let id = remaining[..end].to_string(); *remaining = remaining[end..].trim(); @@ -106,9 +104,7 @@ fn parse_selector(remaining: &mut &str) -> Result { .find(|c: char| !c.is_ascii_lowercase() && !c.is_ascii_digit() && c != '-') .unwrap_or(remaining.len()); if end == 0 { - return Err(Error::Stylesheet( - "expected class name after '.'".into(), - )); + return Err(Error::Stylesheet("expected class name after '.'".into())); } let class = remaining[..end].to_string(); *remaining = remaining[end..].trim(); diff --git a/lib/crates/fabro-hooks/src/bridge.rs b/lib/crates/fabro-hooks/src/bridge.rs index 08765a764..0c51172a5 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,19 +184,16 @@ 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)], @@ -214,7 +211,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)], @@ -243,7 +240,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 d20ef3bd8..a6205c967 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 5b84a7d5e..807d69ef7 100644 --- a/lib/crates/fabro-hooks/src/executor.rs +++ b/lib/crates/fabro-hooks/src/executor.rs @@ -374,19 +374,19 @@ impl HookExecutorImpl { for _ in 0..rounds { let request = Request { - model: resolved_model.clone(), - messages: messages.clone(), - provider: None, - tools: Some(tool_defs.clone()), - tool_choice: None, - response_format: None, - temperature: None, - top_p: None, - max_tokens: None, - stop_sequences: None, + model: resolved_model.clone(), + messages: messages.clone(), + provider: None, + tools: Some(tool_defs.clone()), + tool_choice: None, + response_format: None, + temperature: None, + top_p: None, + max_tokens: None, + stop_sequences: None, reasoning_effort: None, - speed: None, - metadata: None, + speed: None, + metadata: None, provider_options: None, }; @@ -408,8 +408,8 @@ impl HookExecutorImpl { for tc in &tool_calls { let tool = registry.get(&tc.name).cloned(); let ctx = ToolContext { - env: sandbox.clone(), - cancel: cancel.child_token(), + env: sandbox.clone(), + cancel: cancel.child_token(), tool_env: None, }; let result = match tool { @@ -560,17 +560,17 @@ impl HookExecutorImpl { /// Cached HTTP clients keyed by TLS mode. struct HttpClientCache { - verify: reqwest::Client, + verify: reqwest::Client, no_verify: reqwest::Client, - off: reqwest::Client, + off: reqwest::Client, } impl HttpClientCache { fn new() -> Self { Self { - verify: HookExecutorImpl::build_http_client(TlsMode::Verify), + verify: HookExecutorImpl::build_http_client(TlsMode::Verify), no_verify: HookExecutorImpl::build_http_client(TlsMode::NoVerify), - off: HookExecutorImpl::build_http_client(TlsMode::Off), + off: HookExecutorImpl::build_http_client(TlsMode::Off), } } @@ -710,14 +710,14 @@ mod tests { fn make_definition(command: &str) -> HookDefinition { HookDefinition { - name: Some("test-hook".into()), - event: HookEvent::StageStart, - command: Some(command.into()), - hook_type: None, - matcher: None, - blocking: None, + name: Some("test-hook".into()), + event: HookEvent::StageStart, + command: Some(command.into()), + hook_type: None, + matcher: None, + blocking: None, timeout_ms: Some(5000), - sandbox: Some(false), // host execution for tests + sandbox: Some(false), // host execution for tests } } @@ -816,12 +816,9 @@ 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] @@ -840,14 +837,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(); @@ -1009,12 +1006,9 @@ 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] @@ -1230,19 +1224,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 3059c3f77..5c617dcab 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 5c7c8cfbb..28def4e70 100644 --- a/lib/crates/fabro-hooks/src/types.rs +++ b/lib/crates/fabro-hooks/src/types.rs @@ -6,41 +6,41 @@ pub use crate::config::HookEvent; /// Rich JSON payload sent to hooks. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HookContext { - pub event: HookEvent, - pub run_id: RunId, - pub workflow_name: String, + pub event: HookEvent, + pub run_id: RunId, + pub workflow_name: String, #[serde(default, skip_serializing_if = "Option::is_none")] - pub cwd: Option, + pub cwd: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub node_id: Option, + pub node_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub node_label: Option, + pub node_label: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub handler_type: Option, + pub handler_type: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub status: Option, + pub status: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub edge_from: Option, + pub edge_from: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub edge_to: Option, + pub edge_to: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub edge_label: Option, + pub edge_label: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub failure_reason: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub attempt: Option, + pub attempt: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_attempts: Option, + pub max_attempts: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_name: Option, + pub tool_name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_input: Option, + pub tool_input: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_call_id: Option, + pub tool_call_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_output: Option, + pub tool_output: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub error_message: Option, + pub error_message: Option, } impl HookContext { @@ -73,7 +73,7 @@ impl HookContext { /// Response returned by prompt/agent hooks from the LLM. #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct PromptHookResponse { - pub ok: bool, + pub ok: bool, #[serde(default)] pub reason: Option, } @@ -119,8 +119,8 @@ impl HookDecision { /// Result from executing a single hook. #[derive(Debug, Clone)] pub struct HookResult { - pub hook_name: Option, - pub decision: HookDecision, + pub hook_name: Option, + pub decision: HookDecision, pub duration_ms: u64, } @@ -133,25 +133,25 @@ mod tests { #[test] fn hook_context_serde_round_trip() { let ctx = HookContext { - event: HookEvent::StageStart, - run_id: fixtures::RUN_1, - workflow_name: "test-wf".into(), - cwd: Some("/tmp".into()), - node_id: Some("plan".into()), - node_label: Some("Plan".into()), - handler_type: Some("agent".into()), - status: None, - edge_from: None, - edge_to: None, - edge_label: None, + event: HookEvent::StageStart, + run_id: fixtures::RUN_1, + workflow_name: "test-wf".into(), + cwd: Some("/tmp".into()), + node_id: Some("plan".into()), + node_label: Some("Plan".into()), + handler_type: Some("agent".into()), + status: None, + edge_from: None, + edge_to: None, + edge_label: None, failure_reason: None, - attempt: Some(1), - max_attempts: Some(3), - tool_name: None, - tool_input: None, - tool_call_id: None, - tool_output: None, - error_message: None, + attempt: Some(1), + max_attempts: Some(3), + tool_name: None, + tool_input: None, + tool_call_id: None, + tool_output: None, + error_message: None, }; let json = serde_json::to_string(&ctx).unwrap(); let back: HookContext = serde_json::from_str(&json).unwrap(); diff --git a/lib/crates/fabro-interview/src/auto_approve.rs b/lib/crates/fabro-interview/src/auto_approve.rs index 947e77ab5..8efb620ff 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 dddb40492..e3217c3b5 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 418e1dfeb..602c17d2b 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 f62783c33..56284cb70 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 92965ce8d..c057b2fc4 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 18f1e366d..8c7ef8f2b 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 a0d4a6e2d..7dea66ab5 100644 --- a/lib/crates/fabro-llm/src/client.rs +++ b/lib/crates/fabro-llm/src/client.rs @@ -12,9 +12,9 @@ use crate::types::{Request, Response}; /// The core client that routes requests to provider adapters (Section 2.2, 3). #[derive(Clone)] pub struct Client { - providers: HashMap>, + providers: HashMap>, default_provider: Option, - middleware: Vec>, + middleware: Vec>, } impl Client { @@ -53,9 +53,9 @@ impl Client { F: Fn(&str) -> Option, { let mut client = Self { - providers: HashMap::new(), + providers: HashMap::new(), default_provider: None, - middleware: Vec::new(), + middleware: Vec::new(), }; // Register providers whose API keys are present in the environment. @@ -167,7 +167,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 @@ -175,7 +175,7 @@ impl Client { .cloned() .ok_or_else(|| Error::Configuration { message: format!("Provider '{provider_name}' not registered"), - source: None, + source: None, }) } @@ -303,19 +303,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, }) } @@ -346,19 +346,19 @@ mod tests { fn test_request() -> Request { Request { - model: "mock-model".into(), - messages: vec![Message::user("Hello")], - provider: None, - tools: None, - tool_choice: None, - response_format: None, - temperature: None, - top_p: None, - max_tokens: None, - stop_sequences: None, + model: "mock-model".into(), + messages: vec![Message::user("Hello")], + provider: None, + tools: None, + tool_choice: None, + response_format: None, + temperature: None, + top_p: None, + max_tokens: None, + stop_sequences: None, reasoning_effort: None, - speed: None, - metadata: None, + speed: None, + metadata: None, provider_options: None, } } diff --git a/lib/crates/fabro-llm/src/error.rs b/lib/crates/fabro-llm/src/error.rs index 876fca13c..fd21dd49d 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,44 +488,32 @@ 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, @@ -535,26 +523,20 @@ 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, @@ -564,35 +546,26 @@ 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, @@ -602,13 +575,10 @@ mod tests { None, None, ); - assert!(matches!( - err, - Error::Provider { - kind: ProviderErrorKind::Server, - .. - } - )); + assert!(matches!(err, Error::Provider { + kind: ProviderErrorKind::Server, + .. + })); assert!(err.retryable()); } @@ -622,13 +592,10 @@ mod tests { None, None, ); - assert!(matches!( - err, - Error::Provider { - kind: ProviderErrorKind::ContextLength, - .. - } - )); + assert!(matches!(err, Error::Provider { + kind: ProviderErrorKind::ContextLength, + .. + })); } #[test] @@ -641,13 +608,10 @@ mod tests { None, None, ); - assert!(matches!( - err, - Error::Provider { - kind: ProviderErrorKind::ContextLength, - .. - } - )); + assert!(matches!(err, Error::Provider { + kind: ProviderErrorKind::ContextLength, + .. + })); } #[test] @@ -660,13 +624,10 @@ mod tests { None, None, ); - assert!(matches!( - err, - Error::Provider { - kind: ProviderErrorKind::ContentFilter, - .. - } - )); + assert!(matches!(err, Error::Provider { + kind: ProviderErrorKind::ContentFilter, + .. + })); } #[test] @@ -679,13 +640,10 @@ mod tests { None, None, ); - assert!(matches!( - err, - Error::Provider { - kind: ProviderErrorKind::ContentFilter, - .. - } - )); + assert!(matches!(err, Error::Provider { + kind: ProviderErrorKind::ContentFilter, + .. + })); } #[test] @@ -698,13 +656,10 @@ mod tests { None, None, ); - assert!(matches!( - err, - Error::Provider { - kind: ProviderErrorKind::NotFound, - .. - } - )); + assert!(matches!(err, Error::Provider { + kind: ProviderErrorKind::NotFound, + .. + })); } #[test] @@ -717,13 +672,10 @@ mod tests { None, None, ); - assert!(matches!( - err, - Error::Provider { - kind: ProviderErrorKind::NotFound, - .. - } - )); + assert!(matches!(err, Error::Provider { + kind: ProviderErrorKind::NotFound, + .. + })); } #[test] @@ -736,13 +688,10 @@ mod tests { None, None, ); - assert!(matches!( - err, - Error::Provider { - kind: ProviderErrorKind::Authentication, - .. - } - )); + assert!(matches!(err, Error::Provider { + kind: ProviderErrorKind::Authentication, + .. + })); } #[test] @@ -755,13 +704,10 @@ mod tests { None, None, ); - assert!(matches!( - err, - Error::Provider { - kind: ProviderErrorKind::Authentication, - .. - } - )); + assert!(matches!(err, Error::Provider { + kind: ProviderErrorKind::Authentication, + .. + })); } #[test] @@ -774,13 +720,10 @@ 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", @@ -790,13 +733,10 @@ 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( @@ -807,13 +747,10 @@ 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", @@ -833,19 +770,16 @@ 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") @@ -858,7 +792,7 @@ mod tests { let err = Error::Configuration { message: "no provider".into(), - source: None, + source: None, }; assert_eq!(err.to_string(), "Configuration error: no provider"); } @@ -866,7 +800,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") @@ -876,7 +810,7 @@ mod tests { let err = Error::Network { message: "refused".into(), - source: None, + source: None, }; assert_eq!(err.status_code(), None); } @@ -884,7 +818,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"); @@ -894,7 +828,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"); } @@ -905,7 +839,7 @@ mod tests { assert!( Error::Provider { - kind: ProviderErrorKind::RateLimit, + kind: ProviderErrorKind::RateLimit, detail: detail(), } .failover_eligible() @@ -913,7 +847,7 @@ mod tests { assert!( Error::Provider { - kind: ProviderErrorKind::Server, + kind: ProviderErrorKind::Server, detail: detail(), } .failover_eligible() @@ -921,7 +855,7 @@ mod tests { assert!( Error::Provider { - kind: ProviderErrorKind::QuotaExceeded, + kind: ProviderErrorKind::QuotaExceeded, detail: detail(), } .failover_eligible() @@ -933,7 +867,7 @@ mod tests { assert!( Error::RequestTimeout { message: "timed out".into(), - source: None, + source: None, } .failover_eligible() ); @@ -941,7 +875,7 @@ mod tests { assert!( Error::Network { message: "refused".into(), - source: None, + source: None, } .failover_eligible() ); @@ -949,7 +883,7 @@ mod tests { assert!( Error::Stream { message: "broken".into(), - source: None, + source: None, } .failover_eligible() ); @@ -961,7 +895,7 @@ mod tests { assert!( !Error::Provider { - kind: ProviderErrorKind::Authentication, + kind: ProviderErrorKind::Authentication, detail: detail(), } .failover_eligible() @@ -969,7 +903,7 @@ mod tests { assert!( !Error::Provider { - kind: ProviderErrorKind::InvalidRequest, + kind: ProviderErrorKind::InvalidRequest, detail: detail(), } .failover_eligible() @@ -977,7 +911,7 @@ mod tests { assert!( !Error::Provider { - kind: ProviderErrorKind::ContextLength, + kind: ProviderErrorKind::ContextLength, detail: detail(), } .failover_eligible() @@ -985,7 +919,7 @@ mod tests { assert!( !Error::Provider { - kind: ProviderErrorKind::ContentFilter, + kind: ProviderErrorKind::ContentFilter, detail: detail(), } .failover_eligible() @@ -997,7 +931,7 @@ mod tests { assert!( !Error::Configuration { message: "bad".into(), - source: None, + source: None, } .failover_eligible() ); @@ -1034,7 +968,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!( @@ -1043,7 +977,7 @@ mod tests { ); let err = Error::Provider { - kind: ProviderErrorKind::Server, + kind: ProviderErrorKind::Server, detail: Box::new(ProviderErrorDetail::new("500", "anthropic")), }; assert_eq!( @@ -1055,7 +989,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!( @@ -1064,7 +998,7 @@ mod tests { ); let err = Error::Provider { - kind: ProviderErrorKind::AccessDenied, + kind: ProviderErrorKind::AccessDenied, detail: Box::new(ProviderErrorDetail::new("denied", "anthropic")), }; assert_eq!( @@ -1073,7 +1007,7 @@ mod tests { ); let err = Error::Provider { - kind: ProviderErrorKind::NotFound, + kind: ProviderErrorKind::NotFound, detail: Box::new(ProviderErrorDetail::new("missing", "openai")), }; assert_eq!( @@ -1082,7 +1016,7 @@ mod tests { ); let err = Error::Provider { - kind: ProviderErrorKind::InvalidRequest, + kind: ProviderErrorKind::InvalidRequest, detail: Box::new(ProviderErrorDetail::new("bad", "openai")), }; assert_eq!( @@ -1091,7 +1025,7 @@ mod tests { ); let err = Error::Provider { - kind: ProviderErrorKind::ContentFilter, + kind: ProviderErrorKind::ContentFilter, detail: Box::new(ProviderErrorDetail::new("blocked", "openai")), }; assert_eq!( @@ -1100,7 +1034,7 @@ mod tests { ); let err = Error::Provider { - kind: ProviderErrorKind::ContextLength, + kind: ProviderErrorKind::ContextLength, detail: Box::new(ProviderErrorDetail::new("too long", "openai")), }; assert_eq!( @@ -1109,7 +1043,7 @@ mod tests { ); let err = Error::Provider { - kind: ProviderErrorKind::QuotaExceeded, + kind: ProviderErrorKind::QuotaExceeded, detail: Box::new(ProviderErrorDetail::new("out of quota", "openai")), }; assert_eq!( @@ -1123,7 +1057,7 @@ mod tests { assert_eq!( Error::RequestTimeout { message: "timed out".into(), - source: None, + source: None, } .failure_signature_hint(), "api_transient|unknown|timeout" @@ -1131,7 +1065,7 @@ mod tests { assert_eq!( Error::Network { message: "refused".into(), - source: None, + source: None, } .failure_signature_hint(), "api_transient|unknown|network" @@ -1139,7 +1073,7 @@ mod tests { assert_eq!( Error::Stream { message: "broken".into(), - source: None, + source: None, } .failure_signature_hint(), "api_transient|unknown|stream" @@ -1154,7 +1088,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 16f6ce2d3..f73c42b18 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 4fe1efced..edf6e8155 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 4ce868cf9..d0ab4a060 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,15 +968,18 @@ 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, @@ -1280,9 +1283,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, @@ -1444,15 +1447,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, }, ]; @@ -1473,9 +1476,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); @@ -1486,15 +1489,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?"})], }, ]; @@ -1513,18 +1516,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"})], }, ]; @@ -1540,7 +1543,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"})], }]; @@ -1561,23 +1564,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"})], }, ]; @@ -1644,9 +1647,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"); @@ -1656,9 +1659,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"); @@ -1675,23 +1678,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"); @@ -1718,19 +1721,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, } } @@ -1752,9 +1755,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; @@ -1780,14 +1783,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> = @@ -1806,9 +1809,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; @@ -1830,9 +1833,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; @@ -1849,9 +1852,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; @@ -1870,9 +1873,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; @@ -1955,24 +1958,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, @@ -1998,10 +2001,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"); @@ -2012,10 +2015,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"); @@ -2027,10 +2030,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"); @@ -2072,23 +2075,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!({ @@ -2105,23 +2108,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!({ @@ -2146,8 +2149,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 57007c48e..72e1e7cb3 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( /// delimiter (e.g. `"\n"` for Gemini/OpenAI-compatible, `"\n\n"` for /// Anthropic/OpenAI SSE event blocks). pub struct LineReader { - response: reqwest::Response, - buffer: String, + response: reqwest::Response, + buffer: String, stream_read_timeout: Option, } @@ -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 35180a801..513a2512a 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: reqwest::Client, - base_url: String, + client: reqwest::Client, + base_url: String, provider_name: String, } @@ -35,16 +35,16 @@ impl Adapter { #[derive(serde::Deserialize)] struct ServerCompletionResponse { - id: String, - model: String, - message: Message, + id: String, + model: String, + message: Message, stop_reason: String, - usage: ServerUsage, + usage: ServerUsage, } #[derive(serde::Deserialize)] struct ServerUsage { - input_tokens: i64, + input_tokens: i64, output_tokens: i64, } @@ -234,19 +234,19 @@ mod tests { fn make_request() -> Request { Request { - model: "test-model".to_string(), - messages: vec![Message::user("Hello")], - provider: None, - tools: None, - tool_choice: None, - response_format: None, - temperature: None, - top_p: None, - max_tokens: None, - stop_sequences: None, + model: "test-model".to_string(), + messages: vec![Message::user("Hello")], + provider: None, + tools: None, + tool_choice: None, + response_format: None, + temperature: None, + top_p: None, + max_tokens: None, + stop_sequences: None, reasoning_effort: None, - speed: None, - metadata: None, + speed: None, + metadata: None, provider_options: None, } } diff --git a/lib/crates/fabro-llm/src/providers/gemini.rs b/lib/crates/fabro-llm/src/providers/gemini.rs index 931481644..f77e43970 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,13 +1219,10 @@ 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, @@ -1234,13 +1231,10 @@ 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, @@ -1249,13 +1243,10 @@ 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, @@ -1264,13 +1255,10 @@ 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, @@ -1279,13 +1267,10 @@ 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, @@ -1302,22 +1287,16 @@ 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] @@ -1395,9 +1374,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]); @@ -1417,9 +1396,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 30cfe0d81..539c554e8 100644 --- a/lib/crates/fabro-llm/src/providers/http_api.rs +++ b/lib/crates/fabro-llm/src/providers/http_api.rs @@ -9,11 +9,11 @@ use crate::types::AdapterTimeout; /// configuration that every provider needs. Provider-specific fields live on /// the adapter struct itself. pub struct HttpApi { - pub(crate) api_key: String, - pub(crate) base_url: String, - pub(crate) default_headers: HashMap, - pub(crate) client: reqwest::Client, - pub(crate) request_timeout: Option, + pub(crate) api_key: String, + pub(crate) base_url: String, + pub(crate) default_headers: HashMap, + pub(crate) client: reqwest::Client, + pub(crate) request_timeout: Option, pub(crate) stream_read_timeout: Option, } diff --git a/lib/crates/fabro-llm/src/providers/openai.rs b/lib/crates/fabro-llm/src/providers/openai.rs index 49a34788f..8476676d0 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 = reqwest::Response::from(http_resp); SseStreamState { - line_reader: LineReader::new(response, None), - model: String::new(), - response_id: String::new(), - response_model: String::new(), - accumulated_text: String::new(), - tool_calls: Vec::new(), - reasoning_items: Vec::new(), - message_items: Vec::new(), - usage: TokenCounts::default(), - finish_reason: FinishReason::Stop, - emitted_start: true, - emitted_text_start: false, + line_reader: LineReader::new(response, None), + model: String::new(), + response_id: String::new(), + response_model: String::new(), + accumulated_text: String::new(), + tool_calls: Vec::new(), + reasoning_items: Vec::new(), + message_items: Vec::new(), + usage: TokenCounts::default(), + finish_reason: FinishReason::Stop, + emitted_start: true, + emitted_text_start: false, emitted_reasoning_start: false, - raw_response: None, - rate_limit: None, + raw_response: None, + rate_limit: None, } } diff --git a/lib/crates/fabro-llm/src/providers/openai_compatible.rs b/lib/crates/fabro-llm/src/providers/openai_compatible.rs index 392b18dbe..10d39632c 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 09ea45c85..0feddc07a 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 e15267afb..fc7b04c5e 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 f6abe5f76..8a4bb1c79 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,26 +975,23 @@ 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"); } @@ -1002,12 +999,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", @@ -1015,14 +1012,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); @@ -1033,27 +1030,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."); @@ -1062,15 +1059,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); } @@ -1091,7 +1088,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, .. } => { @@ -1108,9 +1105,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() }; @@ -1128,9 +1125,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() }; @@ -1144,9 +1141,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() }; @@ -1173,10 +1170,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(_))); } @@ -1192,10 +1189,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()); @@ -1228,19 +1225,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 36324633e..28de85a83 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 8fdfba7b2..81bbeb2b9 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 b436e6ce1..59e12f6cc 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,15 +132,12 @@ 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 b07d0f1a5..8da5c24d6 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 b3bbb4f31..2e16353d9 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 fdf0ecea4..09b544555 100644 --- a/lib/crates/fabro-model/src/catalog.rs +++ b/lib/crates/fabro-model/src/catalog.rs @@ -15,7 +15,7 @@ static GLOBAL_CATALOG: LazyLock = LazyLock::new(|| { #[derive(Debug, Clone, PartialEq, Eq)] pub struct FallbackTarget { pub provider: String, - pub model: String, + pub model: String, } /// Typed model catalog backed by a `Vec`. @@ -153,7 +153,7 @@ impl Catalog { let provider = provider_str.parse::().ok()?; self.closest(provider, reference).map(|m| FallbackTarget { provider: provider_str.clone(), - model: m.id.clone(), + model: m.id.clone(), }) }) .collect() @@ -274,10 +274,10 @@ mod tests { #[test] fn builtin_build_fallback_chain() { - let fallbacks = HashMap::from([( - "anthropic".to_string(), - vec!["gemini".to_string(), "openai".to_string()], - )]); + let fallbacks = HashMap::from([("anthropic".to_string(), vec![ + "gemini".to_string(), + "openai".to_string(), + ])]); let chain = Catalog::builtin().build_fallback_chain( Provider::Anthropic, "claude-opus-4-6", @@ -311,10 +311,10 @@ mod tests { #[test] fn builtin_build_fallback_chain_skips_no_capability_match() { - let fallbacks = HashMap::from([( - "anthropic".to_string(), - vec!["openai".to_string(), "kimi".to_string()], - )]); + let fallbacks = HashMap::from([("anthropic".to_string(), vec![ + "openai".to_string(), + "kimi".to_string(), + ])]); let chain = Catalog::builtin().build_fallback_chain( Provider::Anthropic, "claude-haiku-4-5", @@ -341,30 +341,30 @@ mod tests { use crate::types::{Model, ModelCosts, ModelFeatures, ModelLimits}; let models = vec![Model { - id: "test-model".to_string(), - provider: Provider::Anthropic, - family: "test".to_string(), - display_name: "Test Model".to_string(), - limits: ModelLimits { + id: "test-model".to_string(), + provider: Provider::Anthropic, + family: "test".to_string(), + display_name: "Test Model".to_string(), + limits: ModelLimits { context_window: 100_000, - max_output: Some(4096), + max_output: Some(4096), }, - training: None, - knowledge_cutoff: None, - features: ModelFeatures { - tools: true, - vision: false, + training: None, + knowledge_cutoff: None, + features: ModelFeatures { + tools: true, + vision: false, reasoning: false, - effort: false, + effort: false, }, - costs: ModelCosts { - input_cost_per_mtok: Some(1.0), - output_cost_per_mtok: Some(5.0), + costs: ModelCosts { + input_cost_per_mtok: Some(1.0), + output_cost_per_mtok: Some(5.0), cache_input_cost_per_mtok: None, }, estimated_output_tps: None, - aliases: vec!["test".to_string()], - default: true, + aliases: vec!["test".to_string()], + default: true, }]; let catalog = Catalog::from_models(models); diff --git a/lib/crates/fabro-model/src/model_ref.rs b/lib/crates/fabro-model/src/model_ref.rs index 38ad13d6b..96cbced9b 100644 --- a/lib/crates/fabro-model/src/model_ref.rs +++ b/lib/crates/fabro-model/src/model_ref.rs @@ -11,7 +11,10 @@ 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 { @@ -62,7 +65,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"); } @@ -71,7 +74,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); @@ -96,7 +99,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 1f33fb5ec..e5666850a 100644 --- a/lib/crates/fabro-model/src/provider.rs +++ b/lib/crates/fabro-model/src/provider.rs @@ -217,10 +217,9 @@ 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] @@ -252,10 +251,9 @@ 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 bff08dfa1..57d89b545 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 255858ee8..e91ee0d10 100644 --- a/lib/crates/fabro-oauth/src/lib.rs +++ b/lib/crates/fabro-oauth/src/lib.rs @@ -16,7 +16,7 @@ use tokio::sync::oneshot; // --------------------------------------------------------------------------- pub struct PkceCodes { - pub verifier: String, + pub verifier: String, pub challenge: String, } @@ -96,10 +96,10 @@ pub fn build_authorize_url( #[derive(Debug, Deserialize)] pub struct TokenResponse { - pub id_token: Option, - pub access_token: String, + pub id_token: Option, + pub access_token: String, pub refresh_token: Option, - pub expires_in: Option, + pub expires_in: Option, } // --------------------------------------------------------------------------- @@ -198,9 +198,9 @@ pub async fn refresh_access_token( #[derive(Deserialize)] struct CallbackParams { - code: Option, - state: String, - error: Option, + code: Option, + state: String, + error: Option, error_description: Option, } diff --git a/lib/crates/fabro-proc/src/title.rs b/lib/crates/fabro-proc/src/title.rs index e9dafa81f..f1f087636 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 3e1e112de..13011a934 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 b5c0643e8..e0a08e27b 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 f57074887..4f5ed268c 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 671ee24d9..557cf8dc0 100644 --- a/lib/crates/fabro-sandbox/src/daytona/mod.rs +++ b/lib/crates/fabro-sandbox/src/daytona/mod.rs @@ -28,20 +28,20 @@ pub use crate::config::{ /// Sandbox that runs all operations inside a Daytona cloud sandbox. pub struct DaytonaSandbox { - config: DaytonaConfig, - client: daytona_sdk::Client, - github_app: Option, - sandbox: OnceCell, - rg_available: OnceCell, + config: DaytonaConfig, + client: daytona_sdk::Client, + github_app: Option, + sandbox: OnceCell, + rg_available: OnceCell, event_callback: Option, /// HTTPS origin URL stored after clone so we can refresh push credentials /// later. - origin_url: OnceCell, - run_id: Option, + origin_url: OnceCell, + run_id: Option, /// Explicit branch to clone. When set, overrides the branch detected by /// `detect_repo_info` — avoids cloning a local-only worktree branch /// (e.g. `fabro/run/...`) that was never pushed to origin. - clone_branch: Option, + clone_branch: Option, } impl DaytonaSandbox { @@ -231,11 +231,11 @@ impl DaytonaSandbox { }; let params = daytona_sdk::CreateSnapshotParams { - name: snap_cfg.name.clone(), - image: daytona_sdk::ImageSource::Custom( + name: snap_cfg.name.clone(), + image: daytona_sdk::ImageSource::Custom( daytona_sdk::DockerImage::from_dockerfile(dockerfile), ), - resources: Some(daytona_sdk::Resources { + resources: Some(daytona_sdk::Resources { cpu: snap_cfg.cpu, memory: snap_cfg.memory, disk: snap_cfg.disk, @@ -302,7 +302,7 @@ use fabro_github::ssh_url_to_https; #[derive(Clone, Debug)] pub struct GitCloneParams { /// Clean HTTPS URL (no embedded credentials). - pub url: String, + pub url: String, /// Branch to clone. If None, uses the remote's default. pub branch: Option, } @@ -426,7 +426,7 @@ impl Sandbox for DaytonaSandbox { let snap_start = Instant::now(); if let Err(e) = self.ensure_snapshot(snap_cfg).await { self.emit(SandboxEvent::SnapshotFailed { - name: snap_cfg.name.clone(), + name: snap_cfg.name.clone(), error: e.clone(), }); let duration_ms = @@ -440,17 +440,17 @@ impl Sandbox for DaytonaSandbox { } let snap_duration = u64::try_from(snap_start.elapsed().as_millis()).unwrap_or(u64::MAX); self.emit(SandboxEvent::SnapshotReady { - name: snap_cfg.name.clone(), + name: snap_cfg.name.clone(), duration_ms: snap_duration, }); daytona_sdk::CreateParams::Snapshot(daytona_sdk::SnapshotParams { - base: self.base_params(), + base: self.base_params(), snapshot: snap_cfg.name.clone(), }) } else { daytona_sdk::CreateParams::Snapshot(daytona_sdk::SnapshotParams { - base: self.base_params(), + base: self.base_params(), snapshot: DEFAULT_SNAPSHOT.to_string(), }) }; @@ -492,7 +492,7 @@ impl Sandbox for DaytonaSandbox { // Daytona clones over HTTPS with token auth, so rewrite SSH URLs. let url = ssh_url_to_https(&detected_url); self.emit(SandboxEvent::GitCloneStarted { - url: url.clone(), + url: url.clone(), branch: branch.clone(), }); let clone_start = Instant::now(); @@ -504,7 +504,7 @@ impl Sandbox for DaytonaSandbox { .map_err(|e| { let err = format!("Failed to parse GitHub URL for clone: {e}"); self.emit(SandboxEvent::GitCloneFailed { - url: url.clone(), + url: url.clone(), error: err.clone(), }); err @@ -520,7 +520,7 @@ impl Sandbox for DaytonaSandbox { let err = format!("Failed to get GitHub App credentials for clone: {e}"); self.emit(SandboxEvent::GitCloneFailed { - url: url.clone(), + url: url.clone(), error: err.clone(), }); let duration_ms = u64::try_from(init_start.elapsed().as_millis()) @@ -544,7 +544,7 @@ impl Sandbox for DaytonaSandbox { Ok(g) => g, Err(e) => { self.emit(SandboxEvent::GitCloneFailed { - url: url.clone(), + url: url.clone(), error: e.clone(), }); let duration_ms = @@ -560,16 +560,12 @@ 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 { @@ -577,7 +573,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, }); @@ -673,12 +669,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(()) @@ -695,7 +691,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); } @@ -933,9 +929,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 @@ -971,8 +967,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 ccea98232..c61730ec8 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 6ded36382..209e9647b 100644 --- a/lib/crates/fabro-sandbox/src/local.rs +++ b/lib/crates/fabro-sandbox/src/local.rs @@ -14,8 +14,8 @@ use crate::{ pub struct LocalSandbox { working_directory: PathBuf, - event_callback: Option, - rg_available: std::sync::OnceLock, + event_callback: Option, + rg_available: std::sync::OnceLock, } impl LocalSandbox { @@ -795,14 +795,10 @@ 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(); @@ -817,14 +813,10 @@ 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 910eec845..19b65404a 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 50bce5456..8b8284fc5 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 6023c2c5d..a2031dd81 100644 --- a/lib/crates/fabro-sandbox/src/sandbox_spec.rs +++ b/lib/crates/fabro-sandbox/src/sandbox_spec.rs @@ -27,9 +27,9 @@ pub enum SandboxSpec { }, #[cfg(feature = "daytona")] Daytona { - config: DaytonaConfig, - github_app: Option, - run_id: Option, + config: DaytonaConfig, + github_app: Option, + run_id: Option, clone_branch: Option, }, } @@ -156,15 +156,15 @@ impl SandboxSpec { #[cfg(feature = "docker")] Self::Docker { config } => { let mut sandbox = DockerSandbox::new(DockerSandboxOptions { - image: config.image.clone(), + image: config.image.clone(), host_working_directory: config.host_working_directory.clone(), - container_mount_point: config.container_mount_point.clone(), - network_mode: config.network_mode.clone(), - extra_mounts: config.extra_mounts.clone(), - memory_limit: config.memory_limit, - cpu_quota: config.cpu_quota, - auto_pull: config.auto_pull, - env_vars: config.env_vars.clone(), + container_mount_point: config.container_mount_point.clone(), + network_mode: config.network_mode.clone(), + extra_mounts: config.extra_mounts.clone(), + memory_limit: config.memory_limit, + cpu_quota: config.cpu_quota, + auto_pull: config.auto_pull, + env_vars: config.env_vars.clone(), }) .map_err(|e| anyhow!("Failed to create Docker sandbox: {e}"))?; if let Some(callback) = event_callback { diff --git a/lib/crates/fabro-sandbox/src/test_support.rs b/lib/crates/fabro-sandbox/src/test_support.rs index a166daaf6..d9fe1423c 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 75e09b86a..da4d3c3e3 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 7226cfbee..92e5d7ebe 100644 --- a/lib/crates/fabro-server/src/demo/mod.rs +++ b/lib/crates/fabro-server/src/demo/mod.rs @@ -646,445 +646,454 @@ mod runs { pub(super) fn list_items() -> Vec { vec![ RunListItem { - id: "run-1".into(), - repository: RepositoryReference { + id: "run-1".into(), + repository: RepositoryReference { name: "api-server".into(), }, - title: "Add rate limiting to auth endpoints".into(), - workflow: WorkflowReference { + title: "Add rate limiting to auth endpoints".into(), + workflow: WorkflowReference { slug: "implement".into(), }, - status: BoardColumn::Working, + status: BoardColumn::Working, pull_request: None, - timings: Some(RunTimings { - elapsed_secs: 420.0, + timings: Some(RunTimings { + elapsed_secs: 420.0, elapsed_warning: Some(false), }), - sandbox: Some(RunSandbox { - id: "sb-a1b2c3d4".into(), - resources: Some(SandboxResources { cpu: 4, memory: 8 }), + sandbox: Some(RunSandbox { + id: "sb-a1b2c3d4".into(), + resources: Some(SandboxResources { + cpu: 4, + memory: 8, + }), }), - question: None, - created_at: ts("2026-03-06T14:30:00Z"), + question: None, + created_at: ts("2026-03-06T14:30:00Z"), }, RunListItem { - id: "run-2".into(), - repository: RepositoryReference { + id: "run-2".into(), + repository: RepositoryReference { name: "web-dashboard".into(), }, - title: "Migrate to React Router v7".into(), - workflow: WorkflowReference { + title: "Migrate to React Router v7".into(), + workflow: WorkflowReference { slug: "implement".into(), }, - status: BoardColumn::Working, + status: BoardColumn::Working, pull_request: None, - timings: Some(RunTimings { - elapsed_secs: 8100.0, + timings: Some(RunTimings { + elapsed_secs: 8100.0, elapsed_warning: Some(false), }), - sandbox: Some(RunSandbox { - id: "sb-e5f6g7h8".into(), - resources: Some(SandboxResources { cpu: 8, memory: 16 }), + sandbox: Some(RunSandbox { + id: "sb-e5f6g7h8".into(), + resources: Some(SandboxResources { + cpu: 8, + memory: 16, + }), }), - question: None, - created_at: ts("2026-03-06T12:00:00Z"), + question: None, + created_at: ts("2026-03-06T12:00:00Z"), }, RunListItem { - id: "run-3".into(), - repository: RepositoryReference { + id: "run-3".into(), + repository: RepositoryReference { name: "cli-tools".into(), }, - title: "Fix config parsing for nested values".into(), - workflow: WorkflowReference { + title: "Fix config parsing for nested values".into(), + workflow: WorkflowReference { slug: "fix_build".into(), }, - status: BoardColumn::Working, + status: BoardColumn::Working, pull_request: None, - timings: Some(RunTimings { - elapsed_secs: 2700.0, + timings: Some(RunTimings { + elapsed_secs: 2700.0, elapsed_warning: Some(false), }), - sandbox: Some(RunSandbox { - id: "sb-i9j0k1l2".into(), - resources: Some(SandboxResources { cpu: 2, memory: 4 }), + sandbox: Some(RunSandbox { + id: "sb-i9j0k1l2".into(), + resources: Some(SandboxResources { + cpu: 2, + memory: 4, + }), }), - question: None, - created_at: ts("2026-03-05T09:20:00Z"), + question: None, + created_at: ts("2026-03-05T09:20:00Z"), }, RunListItem { - id: "run-4".into(), - repository: RepositoryReference { + id: "run-4".into(), + repository: RepositoryReference { name: "api-server".into(), }, - title: "Update OpenAPI spec for v3".into(), - workflow: WorkflowReference { + title: "Update OpenAPI spec for v3".into(), + workflow: WorkflowReference { slug: "expand".into(), }, - status: BoardColumn::Pending, + status: BoardColumn::Pending, pull_request: Some(RunPullRequest { - number: 0, + number: 0, additions: Some(567), deletions: Some(234), - comments: Some(0), - checks: vec![], + comments: Some(0), + checks: vec![], }), - timings: Some(RunTimings { - elapsed_secs: 4320.0, + timings: Some(RunTimings { + elapsed_secs: 4320.0, elapsed_warning: Some(false), }), - sandbox: Some(RunSandbox { - id: "sb-q7r8s9t0".into(), + sandbox: Some(RunSandbox { + id: "sb-q7r8s9t0".into(), resources: None, }), - question: Some(RunQuestion { + question: Some(RunQuestion { text: "Accept or push for another round?".into(), }), - created_at: ts("2026-03-04T15:00:00Z"), + created_at: ts("2026-03-04T15:00:00Z"), }, RunListItem { - id: "run-5".into(), - repository: RepositoryReference { + id: "run-5".into(), + repository: RepositoryReference { name: "shared-types".into(), }, - title: "Add pipeline event types".into(), - workflow: WorkflowReference { + title: "Add pipeline event types".into(), + workflow: WorkflowReference { slug: "implement".into(), }, - status: BoardColumn::Pending, + status: BoardColumn::Pending, pull_request: Some(RunPullRequest { - number: 0, + number: 0, additions: Some(145), deletions: Some(23), - comments: Some(0), - checks: vec![], + comments: Some(0), + checks: vec![], }), - timings: Some(RunTimings { - elapsed_secs: 1680.0, + timings: Some(RunTimings { + elapsed_secs: 1680.0, elapsed_warning: Some(false), }), - sandbox: Some(RunSandbox { - id: "sb-u1v2w3x4".into(), + sandbox: Some(RunSandbox { + id: "sb-u1v2w3x4".into(), resources: None, }), - question: Some(RunQuestion { + question: Some(RunQuestion { text: "Proceed from investigation to fix?".into(), }), - created_at: ts("2026-03-04T10:00:00Z"), + created_at: ts("2026-03-04T10:00:00Z"), }, RunListItem { - id: "run-6".into(), - repository: RepositoryReference { + id: "run-6".into(), + repository: RepositoryReference { name: "web-dashboard".into(), }, - title: "Add dark mode toggle".into(), - workflow: WorkflowReference { + title: "Add dark mode toggle".into(), + workflow: WorkflowReference { slug: "implement".into(), }, - status: BoardColumn::Review, + status: BoardColumn::Review, pull_request: Some(RunPullRequest { - number: 889, + number: 889, additions: Some(234), deletions: Some(67), - comments: Some(4), - checks: vec![ + comments: Some(4), + checks: vec![ CheckRun { - name: "lint".into(), - status: CheckRunStatus::Success, + name: "lint".into(), + status: CheckRunStatus::Success, duration_secs: Some(23.0), }, CheckRun { - name: "typecheck".into(), - status: CheckRunStatus::Success, + name: "typecheck".into(), + status: CheckRunStatus::Success, duration_secs: Some(72.0), }, CheckRun { - name: "unit-tests".into(), - status: CheckRunStatus::Success, + name: "unit-tests".into(), + status: CheckRunStatus::Success, duration_secs: Some(154.0), }, CheckRun { - name: "integration-tests".into(), - status: CheckRunStatus::Failure, + name: "integration-tests".into(), + status: CheckRunStatus::Failure, duration_secs: Some(296.0), }, CheckRun { - name: "e2e / chrome".into(), - status: CheckRunStatus::Failure, + name: "e2e / chrome".into(), + status: CheckRunStatus::Failure, duration_secs: Some(182.0), }, CheckRun { - name: "build".into(), - status: CheckRunStatus::Success, + name: "build".into(), + status: CheckRunStatus::Success, duration_secs: Some(105.0), }, CheckRun { - name: "coverage".into(), - status: CheckRunStatus::Skipped, + name: "coverage".into(), + status: CheckRunStatus::Skipped, duration_secs: None, }, ], }), - timings: Some(RunTimings { - elapsed_secs: 2100.0, + timings: Some(RunTimings { + elapsed_secs: 2100.0, elapsed_warning: Some(false), }), - sandbox: Some(RunSandbox { - id: "sb-m3n4o5p6".into(), + sandbox: Some(RunSandbox { + id: "sb-m3n4o5p6".into(), resources: None, }), - question: None, - created_at: ts("2026-03-03T16:45:00Z"), + question: None, + created_at: ts("2026-03-03T16:45:00Z"), }, RunListItem { - id: "run-7".into(), - repository: RepositoryReference { + id: "run-7".into(), + repository: RepositoryReference { name: "infrastructure".into(), }, - title: "Terraform module for Redis cluster".into(), - workflow: WorkflowReference { + title: "Terraform module for Redis cluster".into(), + workflow: WorkflowReference { slug: "implement".into(), }, - status: BoardColumn::Review, + status: BoardColumn::Review, pull_request: Some(RunPullRequest { - number: 156, + number: 156, additions: Some(412), deletions: Some(0), - comments: Some(1), - checks: vec![ + comments: Some(1), + checks: vec![ CheckRun { - name: "lint".into(), - status: CheckRunStatus::Success, + name: "lint".into(), + status: CheckRunStatus::Success, duration_secs: Some(18.0), }, CheckRun { - name: "typecheck".into(), - status: CheckRunStatus::Success, + name: "typecheck".into(), + status: CheckRunStatus::Success, duration_secs: Some(56.0), }, CheckRun { - name: "unit-tests".into(), - status: CheckRunStatus::Pending, + name: "unit-tests".into(), + status: CheckRunStatus::Pending, duration_secs: None, }, CheckRun { - name: "integration-tests".into(), - status: CheckRunStatus::Queued, + name: "integration-tests".into(), + status: CheckRunStatus::Queued, duration_secs: None, }, CheckRun { - name: "build".into(), - status: CheckRunStatus::Pending, + name: "build".into(), + status: CheckRunStatus::Pending, duration_secs: None, }, ], }), - timings: Some(RunTimings { - elapsed_secs: 720.0, + timings: Some(RunTimings { + elapsed_secs: 720.0, elapsed_warning: Some(false), }), - sandbox: Some(RunSandbox { - id: "sb-y5z6a7b8".into(), + sandbox: Some(RunSandbox { + id: "sb-y5z6a7b8".into(), resources: None, }), - question: None, - created_at: ts("2026-03-03T11:00:00Z"), + question: None, + created_at: ts("2026-03-03T11:00:00Z"), }, RunListItem { - id: "run-8".into(), - repository: RepositoryReference { + id: "run-8".into(), + repository: RepositoryReference { name: "api-server".into(), }, - title: "Implement webhook retry logic".into(), - workflow: WorkflowReference { + title: "Implement webhook retry logic".into(), + workflow: WorkflowReference { slug: "implement".into(), }, - status: BoardColumn::Merge, + status: BoardColumn::Merge, pull_request: Some(RunPullRequest { - number: 1249, + number: 1249, additions: Some(189), deletions: Some(45), - comments: Some(7), - checks: vec![ + comments: Some(7), + checks: vec![ CheckRun { - name: "lint".into(), - status: CheckRunStatus::Success, + name: "lint".into(), + status: CheckRunStatus::Success, duration_secs: Some(21.0), }, CheckRun { - name: "typecheck".into(), - status: CheckRunStatus::Success, + name: "typecheck".into(), + status: CheckRunStatus::Success, duration_secs: Some(68.0), }, CheckRun { - name: "unit-tests".into(), - status: CheckRunStatus::Success, + name: "unit-tests".into(), + status: CheckRunStatus::Success, duration_secs: Some(192.0), }, CheckRun { - name: "integration-tests".into(), - status: CheckRunStatus::Success, + name: "integration-tests".into(), + status: CheckRunStatus::Success, duration_secs: Some(334.0), }, CheckRun { - name: "e2e / chrome".into(), - status: CheckRunStatus::Success, + name: "e2e / chrome".into(), + status: CheckRunStatus::Success, duration_secs: Some(262.0), }, CheckRun { - name: "e2e / firefox".into(), - status: CheckRunStatus::Success, + name: "e2e / firefox".into(), + status: CheckRunStatus::Success, duration_secs: Some(285.0), }, CheckRun { - name: "build".into(), - status: CheckRunStatus::Success, + name: "build".into(), + status: CheckRunStatus::Success, duration_secs: Some(121.0), }, CheckRun { - name: "deploy-preview".into(), - status: CheckRunStatus::Success, + name: "deploy-preview".into(), + status: CheckRunStatus::Success, duration_secs: Some(93.0), }, CheckRun { - name: "security-scan".into(), - status: CheckRunStatus::Skipped, + name: "security-scan".into(), + status: CheckRunStatus::Skipped, duration_secs: None, }, CheckRun { - name: "performance".into(), - status: CheckRunStatus::Success, + name: "performance".into(), + status: CheckRunStatus::Success, duration_secs: Some(138.0), }, CheckRun { - name: "bundle-size".into(), - status: CheckRunStatus::Success, + name: "bundle-size".into(), + status: CheckRunStatus::Success, duration_secs: Some(34.0), }, CheckRun { - name: "accessibility".into(), - status: CheckRunStatus::Success, + name: "accessibility".into(), + status: CheckRunStatus::Success, duration_secs: Some(72.0), }, ], }), - timings: Some(RunTimings { - elapsed_secs: 259200.0, + timings: Some(RunTimings { + elapsed_secs: 259200.0, elapsed_warning: Some(true), }), - sandbox: Some(RunSandbox { - id: "sb-c9d0e1f2".into(), + sandbox: Some(RunSandbox { + id: "sb-c9d0e1f2".into(), resources: None, }), - question: None, - created_at: ts("2026-02-28T14:00:00Z"), + question: None, + created_at: ts("2026-02-28T14:00:00Z"), }, RunListItem { - id: "run-9".into(), - repository: RepositoryReference { + id: "run-9".into(), + repository: RepositoryReference { name: "cli-tools".into(), }, - title: "Add --verbose flag to run command".into(), - workflow: WorkflowReference { + title: "Add --verbose flag to run command".into(), + workflow: WorkflowReference { slug: "expand".into(), }, - status: BoardColumn::Merge, + status: BoardColumn::Merge, pull_request: Some(RunPullRequest { - number: 430, + number: 430, additions: Some(56), deletions: Some(12), - comments: Some(2), - checks: vec![ + comments: Some(2), + checks: vec![ CheckRun { - name: "lint".into(), - status: CheckRunStatus::Success, + name: "lint".into(), + status: CheckRunStatus::Success, duration_secs: Some(15.0), }, CheckRun { - name: "typecheck".into(), - status: CheckRunStatus::Success, + name: "typecheck".into(), + status: CheckRunStatus::Success, duration_secs: Some(48.0), }, CheckRun { - name: "unit-tests".into(), - status: CheckRunStatus::Success, + name: "unit-tests".into(), + status: CheckRunStatus::Success, duration_secs: Some(116.0), }, CheckRun { - name: "build".into(), - status: CheckRunStatus::Success, + name: "build".into(), + status: CheckRunStatus::Success, duration_secs: Some(82.0), }, CheckRun { - name: "coverage".into(), - status: CheckRunStatus::Success, + name: "coverage".into(), + status: CheckRunStatus::Success, duration_secs: Some(124.0), }, CheckRun { - name: "bundle-size".into(), - status: CheckRunStatus::Skipped, + name: "bundle-size".into(), + status: CheckRunStatus::Skipped, duration_secs: None, }, ], }), - timings: Some(RunTimings { - elapsed_secs: 3900.0, + timings: Some(RunTimings { + elapsed_secs: 3900.0, elapsed_warning: Some(false), }), - sandbox: Some(RunSandbox { - id: "sb-g3h4i5j6".into(), + sandbox: Some(RunSandbox { + id: "sb-g3h4i5j6".into(), resources: None, }), - question: None, - created_at: ts("2026-02-27T09:00:00Z"), + question: None, + created_at: ts("2026-02-27T09:00:00Z"), }, RunListItem { - id: "run-10".into(), - repository: RepositoryReference { + id: "run-10".into(), + repository: RepositoryReference { name: "shared-types".into(), }, - title: "Export utility type helpers".into(), - workflow: WorkflowReference { + title: "Export utility type helpers".into(), + workflow: WorkflowReference { slug: "sync_drift".into(), }, - status: BoardColumn::Merge, + status: BoardColumn::Merge, pull_request: Some(RunPullRequest { - number: 76, + number: 76, additions: Some(34), deletions: Some(8), - comments: Some(0), - checks: vec![ + comments: Some(0), + checks: vec![ CheckRun { - name: "lint".into(), - status: CheckRunStatus::Success, + name: "lint".into(), + status: CheckRunStatus::Success, duration_secs: Some(12.0), }, CheckRun { - name: "typecheck".into(), - status: CheckRunStatus::Success, + name: "typecheck".into(), + status: CheckRunStatus::Success, duration_secs: Some(34.0), }, CheckRun { - name: "unit-tests".into(), - status: CheckRunStatus::Success, + name: "unit-tests".into(), + status: CheckRunStatus::Success, duration_secs: Some(75.0), }, CheckRun { - name: "build".into(), - status: CheckRunStatus::Success, + name: "build".into(), + status: CheckRunStatus::Success, duration_secs: Some(58.0), }, ], }), - timings: Some(RunTimings { - elapsed_secs: 2880.0, + timings: Some(RunTimings { + elapsed_secs: 2880.0, elapsed_warning: Some(false), }), - sandbox: Some(RunSandbox { - id: "sb-k7l8m9n0".into(), + sandbox: Some(RunSandbox { + id: "sb-k7l8m9n0".into(), resources: None, }), - question: None, - created_at: ts("2026-02-26T08:00:00Z"), + question: None, + created_at: ts("2026-02-26T08:00:00Z"), }, ] } @@ -1092,32 +1101,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()), }, ] } @@ -1139,139 +1148,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, }, ], } @@ -1280,40 +1289,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, }, @@ -1362,62 +1371,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, }, ], } @@ -1440,25 +1449,26 @@ 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 c9f81a4e3..56e10cb5c 100644 --- a/lib/crates/fabro-server/src/diagnostics.rs +++ b/lib/crates/fabro-server/src/diagnostics.rs @@ -20,7 +20,7 @@ use crate::server::AppState; #[derive(Debug, Serialize)] pub struct DiagnosticsReport { - pub version: String, + pub version: String, pub sections: Vec, } @@ -162,18 +162,18 @@ pub async fn run_all(state: &AppState) -> DiagnosticsReport { let crypto = check_crypto(state); DiagnosticsReport { - version: FABRO_VERSION.to_string(), + version: FABRO_VERSION.to_string(), sections: vec![ CheckSection { - title: "Credentials".to_string(), + title: "Credentials".to_string(), checks: vec![llm, github, sandbox, brave], }, CheckSection { - title: "System".to_string(), + title: "System".to_string(), checks: vec![check_dot()], }, CheckSection { - title: "Configuration".to_string(), + title: "Configuration".to_string(), checks: vec![crypto], }, ], @@ -194,10 +194,10 @@ async fn check_llm_providers(state: &AppState) -> CheckResult { if configured.is_empty() { return CheckResult { - name: "LLM Providers".to_string(), - status: CheckStatus::Error, - summary: "none configured".to_string(), - details: Vec::new(), + name: "LLM Providers".to_string(), + status: CheckStatus::Error, + summary: "none configured".to_string(), + details: Vec::new(), remediation: Some("Set at least one provider API key".to_string()), }; } @@ -206,10 +206,10 @@ async fn check_llm_providers(state: &AppState) -> CheckResult { Ok(client) => client, Err(err) => { return CheckResult { - name: "LLM Providers".to_string(), - status: CheckStatus::Error, - summary: "failed to initialize".to_string(), - details: vec![CheckDetail::new(err)], + name: "LLM Providers".to_string(), + status: CheckStatus::Error, + summary: "failed to initialize".to_string(), + details: vec![CheckDetail::new(err)], remediation: Some("Check configured provider credentials".to_string()), }; } @@ -266,19 +266,19 @@ fn probe_model(provider: Provider) -> String { async fn probe_llm_provider(client: &LlmClient, provider: Provider) -> Result<(), String> { let request = Request { - model: probe_model(provider), - messages: vec![Message::user("hi")], - provider: Some(provider.as_str().to_string()), - tools: None, - tool_choice: None, - response_format: None, - temperature: None, - top_p: None, - max_tokens: Some(16), - stop_sequences: None, + model: probe_model(provider), + messages: vec![Message::user("hi")], + provider: Some(provider.as_str().to_string()), + tools: None, + tool_choice: None, + response_format: None, + temperature: None, + top_p: None, + max_tokens: Some(16), + stop_sequences: None, reasoning_effort: None, - speed: None, - metadata: None, + speed: None, + metadata: None, provider_options: None, }; client @@ -314,29 +314,29 @@ async fn check_github_app(state: &AppState) -> CheckResult { && !webhook_secret { return CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Warning, - summary: "not configured".to_string(), - details: Vec::new(), + name: "GitHub App".to_string(), + status: CheckStatus::Warning, + summary: "not configured".to_string(), + details: Vec::new(), remediation: Some("Configure GitHub App settings and secrets".to_string()), }; } let Some(app_id) = app_id else { return CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "missing app_id".to_string(), - details: Vec::new(), + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "missing app_id".to_string(), + details: Vec::new(), remediation: Some("Set git.app_id in settings.toml".to_string()), }; }; let Some(private_key_raw) = private_key_raw else { return CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "missing private key".to_string(), - details: Vec::new(), + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "missing private key".to_string(), + details: Vec::new(), remediation: Some("Set GITHUB_APP_PRIVATE_KEY".to_string()), }; }; @@ -345,10 +345,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { Ok(value) => value, Err(err) => { return CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "private key invalid".to_string(), - details: vec![CheckDetail::new(err.clone())], + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "private key invalid".to_string(), + details: vec![CheckDetail::new(err.clone())], remediation: Some(err), }; } @@ -358,10 +358,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { Ok(jwt) => jwt, Err(err) => { return CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "JWT signing failed".to_string(), - details: vec![CheckDetail::new(err.clone())], + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "JWT signing failed".to_string(), + details: vec![CheckDetail::new(err.clone())], remediation: Some(err), }; } @@ -375,24 +375,24 @@ async fn check_github_app(state: &AppState) -> CheckResult { .await; match auth_result { Ok(Ok(_app)) => CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Pass, - summary: slug.unwrap_or_else(|| "configured".to_string()), - details: Vec::new(), + name: "GitHub App".to_string(), + status: CheckStatus::Pass, + summary: slug.unwrap_or_else(|| "configured".to_string()), + details: Vec::new(), remediation: None, }, Ok(Err(err)) => CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "connectivity error".to_string(), - details: vec![CheckDetail::new(err.clone())], + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "connectivity error".to_string(), + details: vec![CheckDetail::new(err.clone())], remediation: Some(err), }, Err(_) => CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "timeout".to_string(), - details: vec![CheckDetail::new("GitHub probe timed out".to_string())], + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "timeout".to_string(), + details: vec![CheckDetail::new("GitHub probe timed out".to_string())], remediation: Some("Check GitHub connectivity and credentials".to_string()), }, } @@ -401,18 +401,18 @@ async fn check_github_app(state: &AppState) -> CheckResult { fn check_sandbox(state: &AppState) -> CheckResult { if state.secret_or_env("DAYTONA_API_KEY").is_some() { CheckResult { - name: "Sandbox".to_string(), - status: CheckStatus::Pass, - summary: "Daytona configured".to_string(), - details: Vec::new(), + name: "Sandbox".to_string(), + status: CheckStatus::Pass, + summary: "Daytona configured".to_string(), + details: Vec::new(), remediation: None, } } else { CheckResult { - name: "Sandbox".to_string(), - status: CheckStatus::Warning, - summary: "not configured".to_string(), - details: Vec::new(), + name: "Sandbox".to_string(), + status: CheckStatus::Warning, + summary: "not configured".to_string(), + details: Vec::new(), remediation: Some("Set DAYTONA_API_KEY to enable cloud sandbox execution".to_string()), } } @@ -421,10 +421,10 @@ fn check_sandbox(state: &AppState) -> CheckResult { async fn check_brave_search(state: &AppState) -> CheckResult { let Some(api_key) = state.secret_or_env("BRAVE_SEARCH_API_KEY") else { return CheckResult { - name: "Brave Search".to_string(), - status: CheckStatus::Warning, - summary: "not configured".to_string(), - details: Vec::new(), + name: "Brave Search".to_string(), + status: CheckStatus::Warning, + summary: "not configured".to_string(), + details: Vec::new(), remediation: Some("Set BRAVE_SEARCH_API_KEY to enable web search".to_string()), }; }; @@ -441,31 +441,31 @@ async fn check_brave_search(state: &AppState) -> CheckResult { match probe { Ok(Ok(response)) if response.status().is_success() => CheckResult { - name: "Brave Search".to_string(), - status: CheckStatus::Pass, - summary: "configured and reachable".to_string(), - details: Vec::new(), + name: "Brave Search".to_string(), + status: CheckStatus::Pass, + summary: "configured and reachable".to_string(), + details: Vec::new(), remediation: None, }, Ok(Ok(response)) => CheckResult { - name: "Brave Search".to_string(), - status: CheckStatus::Warning, - summary: format!("HTTP {}", response.status()), - details: Vec::new(), + name: "Brave Search".to_string(), + status: CheckStatus::Warning, + summary: format!("HTTP {}", response.status()), + details: Vec::new(), remediation: Some("Check BRAVE_SEARCH_API_KEY and network connectivity".to_string()), }, Ok(Err(err)) => CheckResult { - name: "Brave Search".to_string(), - status: CheckStatus::Warning, - summary: "connectivity error".to_string(), - details: vec![CheckDetail::new(err.clone())], + name: "Brave Search".to_string(), + status: CheckStatus::Warning, + summary: "connectivity error".to_string(), + details: vec![CheckDetail::new(err.clone())], remediation: Some(err), }, Err(_) => CheckResult { - name: "Brave Search".to_string(), - status: CheckStatus::Warning, - summary: "timeout".to_string(), - details: vec![CheckDetail::new("Brave Search probe timed out".to_string())], + name: "Brave Search".to_string(), + status: CheckStatus::Warning, + summary: "timeout".to_string(), + details: vec![CheckDetail::new("Brave Search probe timed out".to_string())], remediation: Some("Check BRAVE_SEARCH_API_KEY and network connectivity".to_string()), }, } @@ -493,10 +493,10 @@ fn check_crypto(state: &AppState) -> CheckResult { if !has_jwt && !has_mtls { return CheckResult { - name: "Crypto".to_string(), - status: CheckStatus::Warning, - summary: "no authentication configured".to_string(), - details: Vec::new(), + name: "Crypto".to_string(), + status: CheckStatus::Warning, + summary: "no authentication configured".to_string(), + details: Vec::new(), remediation: Some( "Configure strategies under [server.auth.api.jwt] or [server.auth.api.mtls]" .to_string(), diff --git a/lib/crates/fabro-server/src/error.rs b/lib/crates/fabro-server/src/error.rs index 022c4be5f..dd855713b 100644 --- a/lib/crates/fabro-server/src/error.rs +++ b/lib/crates/fabro-server/src/error.rs @@ -55,7 +55,7 @@ pub type Result = std::result::Result; #[derive(Serialize)] struct ErrorEntry { status: String, - title: String, + title: String, detail: String, } diff --git a/lib/crates/fabro-server/src/github_webhooks.rs b/lib/crates/fabro-server/src/github_webhooks.rs index bb6148d92..698abac4c 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 45decf9c6..2d91a6358 100644 --- a/lib/crates/fabro-server/src/jwt_auth.rs +++ b/lib/crates/fabro-server/src/jwt_auth.rs @@ -39,8 +39,8 @@ struct Claims { #[derive(Clone, Debug)] pub enum AuthStrategy { Jwt { - key: Arc, - validation: Arc, + key: Arc, + validation: Arc, allowed_usernames: Vec, }, Cookie, @@ -99,9 +99,9 @@ pub fn resolve_auth_mode(settings: &ResolvedServerSettings) -> Result /// Describes which API auth strategies are enabled in resolved server settings. struct ResolvedAuthStrategies { - jwt_enabled: bool, - mtls_enabled: bool, - tls_present: bool, + jwt_enabled: bool, + mtls_enabled: bool, + tls_present: bool, allowed_usernames: Vec, } @@ -177,8 +177,8 @@ where ) })?; strategies.push(AuthStrategy::Jwt { - key: Arc::new(key), - validation: Arc::new(jwt_validation()), + key: Arc::new(key), + validation: Arc::new(jwt_validation()), allowed_usernames: allowed_usernames.clone(), }); } @@ -372,7 +372,7 @@ impl FromRequestParts for AuthenticatedService { /// Axum extractor that authenticates and extracts the request subject. pub struct AuthenticatedSubject { - pub login: Option, + pub login: Option, pub auth_method: RunAuthMethod, } @@ -388,7 +388,7 @@ impl FromRequestParts for AuthenticatedSubject { let strategies = match auth_mode { AuthMode::Disabled => { return Ok(Self { - login: None, + login: None, auth_method: RunAuthMethod::Disabled, }); } @@ -406,7 +406,7 @@ impl FromRequestParts for AuthenticatedSubject { AuthStrategy::Cookie => { if let Some(session) = parts.extensions.get::() { return Ok(Self { - login: Some(session.login.clone()), + login: Some(session.login.clone()), auth_method: RunAuthMethod::Cookie, }); } @@ -420,7 +420,7 @@ impl FromRequestParts for AuthenticatedSubject { if try_jwt(parts, key, validation, allowed_usernames).is_ok() { if let Some(login) = extract_jwt_login(parts, key, validation) { return Ok(Self { - login: Some(login), + login: Some(login), auth_method: RunAuthMethod::Jwt, }); } @@ -431,7 +431,7 @@ impl FromRequestParts for AuthenticatedSubject { if try_mtls(parts).is_ok() { if let Some(login) = extract_mtls_cn(parts) { return Ok(Self { - login: Some(login), + login: Some(login), auth_method: RunAuthMethod::Mtls, }); } @@ -670,8 +670,8 @@ enabled = true fn jwt_mode(decoding: DecodingKey, allowed_usernames: Vec<&str>) -> AuthMode { AuthMode::Strategies(vec![AuthStrategy::Jwt { - key: Arc::new(decoding), - validation: Arc::new(jwt_validation()), + key: Arc::new(decoding), + validation: Arc::new(jwt_validation()), allowed_usernames: allowed_usernames.into_iter().map(String::from).collect(), }]) } @@ -1003,13 +1003,13 @@ enabled = true .body(Body::empty()) .unwrap(); req.extensions_mut().insert(SessionCookie { - login: "brynary".to_string(), - name: "Brynary".to_string(), - email: "b@example.com".to_string(), + login: "brynary".to_string(), + name: "Brynary".to_string(), + email: "b@example.com".to_string(), avatar_url: "https://example.com/avatar.png".to_string(), - user_url: "https://github.com/brynary".to_string(), - github_id: 1, - exp: 9_999_999_999, + user_url: "https://github.com/brynary".to_string(), + github_id: 1, + exp: 9_999_999_999, }); let response = app.oneshot(req).await.unwrap(); @@ -1094,8 +1094,8 @@ enabled = true let (_, decoding) = generate_test_keypair(); let mode = AuthMode::Strategies(vec![ AuthStrategy::Jwt { - key: Arc::new(decoding), - validation: Arc::new(jwt_validation()), + key: Arc::new(decoding), + validation: Arc::new(jwt_validation()), allowed_usernames: vec!["brynary".to_string()], }, AuthStrategy::Mtls, @@ -1112,14 +1112,11 @@ enabled = true #[tokio::test] async fn mtls_and_jwt_falls_back_to_jwt() { let (encoding, decoding) = generate_test_keypair(); - let mode = AuthMode::Strategies(vec![ - AuthStrategy::Mtls, - AuthStrategy::Jwt { - key: Arc::new(decoding), - validation: Arc::new(jwt_validation()), - allowed_usernames: vec!["brynary".to_string()], - }, - ]); + let mode = AuthMode::Strategies(vec![AuthStrategy::Mtls, AuthStrategy::Jwt { + key: Arc::new(decoding), + validation: Arc::new(jwt_validation()), + allowed_usernames: vec!["brynary".to_string()], + }]); let app = test_router(mode); let token = sign_token( diff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/crates/fabro-server/src/run_manifest.rs index 47c97d976..b5e7ab274 100644 --- a/lib/crates/fabro-server/src/run_manifest.rs +++ b/lib/crates/fabro-server/src/run_manifest.rs @@ -39,14 +39,14 @@ use crate::server::AppState; #[derive(Clone)] pub(crate) struct PreparedManifest { - pub cwd: PathBuf, - pub git: Option, - pub root_source: String, - pub run_id: Option, - pub settings: SettingsLayer, - pub target_path: PathBuf, - pub workflow_bundle: WorkflowBundle, - pub workflow_input: BundledWorkflow, + pub cwd: PathBuf, + pub git: Option, + pub root_source: String, + pub run_id: Option, + pub settings: SettingsLayer, + pub target_path: PathBuf, + pub workflow_bundle: WorkflowBundle, + pub workflow_input: BundledWorkflow, pub working_directory: PathBuf, } @@ -120,9 +120,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(), }) } @@ -178,14 +178,11 @@ 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)) @@ -220,8 +217,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 = @@ -234,7 +231,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| { @@ -244,7 +241,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 = @@ -372,10 +369,10 @@ async fn build_preflight_report( }, ); checks.push(CheckResult { - name: "Repository".into(), - status: CheckStatus::Pass, - summary: repo_summary, - details: vec![ + name: "Repository".into(), + status: CheckStatus::Pass, + summary: repo_summary, + details: vec![ CheckDetail::new(format!("Setup commands: {setup_command_count}")), CheckDetail { text: format!( @@ -392,10 +389,10 @@ async fn build_preflight_report( remediation: None, }); checks.push(CheckResult { - name: "Workflow".into(), - status: CheckStatus::Pass, - summary: graph.name.clone(), - details: vec![ + name: "Workflow".into(), + status: CheckStatus::Pass, + summary: graph.name.clone(), + details: vec![ CheckDetail::new(format!("Nodes: {}", graph.nodes.len())), CheckDetail::new(format!("Edges: {}", graph.edges.len())), CheckDetail::new(format!("Goal: {}", graph.goal())), @@ -418,7 +415,7 @@ async fn build_preflight_report( Ok(( CheckReport { - title: "Run Preflight".into(), + title: "Run Preflight".into(), sections: vec![CheckSection { title: String::new(), checks, @@ -485,10 +482,10 @@ async fn run_sandbox_check( Ok(()) => { let _ = sandbox.cleanup().await; checks.push(CheckResult { - name: "Sandbox".into(), - status: CheckStatus::Pass, - summary: sandbox_provider.to_string(), - details: vec![CheckDetail::new(format!("Provider: {sandbox_provider}"))], + name: "Sandbox".into(), + status: CheckStatus::Pass, + summary: sandbox_provider.to_string(), + details: vec![CheckDetail::new(format!("Provider: {sandbox_provider}"))], remediation: None, }); true @@ -496,10 +493,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 @@ -507,10 +504,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 @@ -586,10 +583,12 @@ 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}" )), @@ -602,10 +601,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 @@ -644,15 +643,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() @@ -665,14 +664,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, } } @@ -704,26 +703,26 @@ async fn run_github_token_check( (Some(creds), Some(git)) => { match mint_github_token(creds, &git.origin_url, &github_permissions).await { Ok(_) => checks.push(CheckResult { - name: "GitHub Token".into(), - status: CheckStatus::Pass, - summary: "minted".into(), - details: perm_details, + name: "GitHub Token".into(), + status: CheckStatus::Pass, + summary: "minted".into(), + details: perm_details, remediation: None, }), Err(err) => checks.push(CheckResult { - name: "GitHub Token".into(), - status: CheckStatus::Error, - summary: "failed".into(), - details: perm_details, + name: "GitHub Token".into(), + status: CheckStatus::Error, + summary: "failed".into(), + details: perm_details, remediation: Some(format!("Failed to mint GitHub token: {err}")), }), } } _ => checks.push(CheckResult { - name: "GitHub Token".into(), - status: CheckStatus::Warning, - summary: "skipped".into(), - details: vec![], + name: "GitHub Token".into(), + status: CheckStatus::Warning, + summary: "skipped".into(), + details: vec![], remediation: Some("No GitHub App credentials or origin URL available".to_string()), }), } @@ -764,11 +763,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(), }, } } @@ -779,14 +778,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, @@ -806,7 +805,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 { @@ -814,20 +813,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(), } } @@ -837,27 +836,24 @@ 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(), + })]), } } @@ -903,15 +899,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(); @@ -945,7 +941,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 @@ -955,7 +951,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 @@ -968,7 +964,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(); @@ -977,10 +973,9 @@ app_id = "snapshotted-app-id" // v2 merge matrix: run.prepare.steps replaces the whole list across // layers, so the higher-precedence workflow layer wins over cli. - assert_eq!( - resolved_run.prepare.commands, - vec!["workflow-setup".to_string()] - ); + assert_eq!(resolved_run.prepare.commands, vec![ + "workflow-setup".to_string() + ]); assert_eq!( resolved_server .integrations diff --git a/lib/crates/fabro-server/src/secret_store.rs b/lib/crates/fabro-server/src/secret_store.rs index d700a3826..d87c53014 100644 --- a/lib/crates/fabro-server/src/secret_store.rs +++ b/lib/crates/fabro-server/src/secret_store.rs @@ -4,14 +4,14 @@ use std::path::{Path, PathBuf}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct SecretEntry { - pub value: String, + pub value: String, pub created_at: String, pub updated_at: String, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct SecretMetadata { - pub name: String, + pub name: String, pub created_at: String, pub updated_at: String, } @@ -51,7 +51,7 @@ impl From for SecretStoreError { #[derive(Debug)] pub struct SecretStore { - path: PathBuf, + path: PathBuf, entries: HashMap, } @@ -75,7 +75,7 @@ impl SecretStore { .get(name) .map_or_else(|| now.clone(), |entry| entry.created_at.clone()); let entry = SecretEntry { - value: value.to_string(), + value: value.to_string(), created_at: created_at.clone(), updated_at: now.clone(), }; @@ -103,7 +103,7 @@ impl SecretStore { .entries .iter() .map(|(name, entry)| SecretMetadata { - name: name.clone(), + name: name.clone(), created_at: entry.created_at.clone(), updated_at: entry.updated_at.clone(), }) diff --git a/lib/crates/fabro-server/src/serve.rs b/lib/crates/fabro-server/src/serve.rs index 4db97e2ef..1244b759e 100644 --- a/lib/crates/fabro-server/src/serve.rs +++ b/lib/crates/fabro-server/src/serve.rs @@ -707,15 +707,15 @@ mod tests { fn apply_runtime_settings_preserves_storage_dir() { let base = SettingsLayer::default(); let args = ServeArgs { - bind: None, - model: None, - provider: None, - dry_run: false, - sandbox: None, - web: false, - no_web: false, + bind: None, + model: None, + provider: None, + dry_run: false, + sandbox: None, + web: false, + no_web: false, max_concurrent_runs: None, - config: None, + config: None, }; let resolved = @@ -741,15 +741,15 @@ enabled = false ", ); let args = ServeArgs { - bind: None, - model: None, - provider: None, - dry_run: false, - sandbox: None, - web: true, - no_web: false, + bind: None, + model: None, + provider: None, + dry_run: false, + sandbox: None, + web: true, + no_web: false, max_concurrent_runs: None, - config: None, + config: None, }; let resolved = apply_runtime_settings(&base, &args, false, &PathBuf::from("/srv/fabro")); @@ -768,15 +768,15 @@ enabled = false fn apply_runtime_settings_disables_web_from_cli_flag() { let base = SettingsLayer::default(); let args = ServeArgs { - bind: None, - model: None, - provider: None, - dry_run: false, - sandbox: None, - web: false, - no_web: true, + bind: None, + model: None, + provider: None, + dry_run: false, + sandbox: None, + web: false, + no_web: true, max_concurrent_runs: None, - config: None, + config: None, }; let resolved = apply_runtime_settings(&base, &args, false, &PathBuf::from("/srv/fabro")); diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs index a736dcafe..5e24cc52b 100644 --- a/lib/crates/fabro-server/src/server.rs +++ b/lib/crates/fabro-server/src/server.rs @@ -121,7 +121,7 @@ pub fn default_page_limit() -> u32 { #[derive(serde::Deserialize)] pub struct PaginationParams { #[serde(rename = "page[limit]", default = "default_page_limit")] - pub limit: u32, + pub limit: u32, #[serde(rename = "page[offset]", default)] pub offset: u32, } @@ -129,13 +129,13 @@ pub struct PaginationParams { #[derive(serde::Deserialize)] struct ModelListParams { #[serde(rename = "page[limit]", default = "default_page_limit")] - limit: u32, + limit: u32, #[serde(rename = "page[offset]", default)] - offset: u32, + offset: u32, #[serde(default)] provider: Option, #[serde(default)] - query: Option, + query: Option, } #[derive(serde::Deserialize)] @@ -149,7 +149,7 @@ struct EventListParams { #[serde(default)] since_seq: Option, #[serde(default)] - limit: Option, + limit: Option, } impl EventListParams { @@ -188,7 +188,7 @@ struct ArtifactFilenameParams { #[derive(serde::Deserialize)] struct SandboxFilesParams { - path: String, + path: String, #[serde(default)] depth: Option, } @@ -216,22 +216,22 @@ impl ListResponse { /// Snapshot of a managed run. struct ManagedRun { - dot_source: String, - status: RunStatus, - error: Option, - created_at: chrono::DateTime, - enqueued_at: Instant, + dot_source: String, + status: RunStatus, + error: Option, + created_at: chrono::DateTime, + enqueued_at: Instant, // Populated when running: - answer_transport: Option, + answer_transport: Option, accepted_questions: HashSet, - event_tx: Option>, - checkpoint: Option, - cancel_tx: Option>, - cancel_token: Option>, - worker_pid: Option, - worker_pgid: Option, - run_dir: Option, - execution_mode: RunExecutionMode, + event_tx: Option>, + checkpoint: Option, + cancel_tx: Option>, + cancel_token: Option>, + worker_pid: Option, + worker_pgid: Option, + run_dir: Option, + execution_mode: RunExecutionMode, } #[derive(Clone, Copy)] @@ -259,18 +259,18 @@ const MAX_MULTIPART_MANIFEST_BYTES: usize = 256 * 1024; #[derive(Clone)] struct ArtifactUploadTokenKeys { - encoding: Arc, - decoding: Arc, + encoding: Arc, + decoding: Arc, validation: Arc, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] struct ArtifactUploadClaims { - iss: String, - iat: u64, - exp: u64, + iss: String, + iat: u64, + exp: u64, run_id: String, - scope: String, + scope: String, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -280,29 +280,29 @@ struct ArtifactBatchUploadManifest { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] struct ArtifactBatchUploadEntry { - part: String, - path: String, + part: String, + path: String, #[serde(default, skip_serializing_if = "Option::is_none")] - sha256: Option, + sha256: Option, #[serde(default, skip_serializing_if = "Option::is_none")] expected_bytes: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - content_type: Option, + content_type: Option, } /// Per-model billing totals. #[derive(Default)] struct ModelBillingTotals { - stages: i64, + stages: i64, billing: BilledTokenCounts, } /// In-memory aggregate billing counters, reset on server restart. #[derive(Default)] struct BillingAccumulator { - total_runs: i64, + total_runs: i64, total_runtime_secs: f64, - by_model: HashMap, + by_model: HashMap, } type RegistryFactoryOverride = dyn Fn(Arc) -> HandlerRegistry + Send + Sync; @@ -359,15 +359,15 @@ impl RunAnswerTransport { #[derive(Debug, Clone)] struct LoadedPendingInterview { - run_id: RunId, - qid: String, + run_id: RunId, + qid: String, question: InterviewQuestionRecord, } #[derive(Clone)] struct SlackService { - client: SlackClient, - app_token: String, + client: SlackClient, + app_token: String, default_channel: String, posted_messages: Arc>>, thread_registry: Arc, @@ -401,12 +401,12 @@ impl SlackService { } let question = runtime_question_from_interview_record(&InterviewQuestionRecord { - id: props.question_id.clone(), - text: props.question.clone(), - stage: props.stage.clone(), - question_type: InterviewQuestionType::from_wire_name(&props.question_type), - options: props.options.clone(), - allow_freeform: props.allow_freeform, + id: props.question_id.clone(), + text: props.question.clone(), + stage: props.stage.clone(), + question_type: InterviewQuestionType::from_wire_name(&props.question_type), + options: props.options.clone(), + allow_freeform: props.allow_freeform, timeout_seconds: props.timeout_seconds, context_display: props.context_display.clone(), }); @@ -505,25 +505,25 @@ impl SlackService { /// Shared application state for the server. pub struct AppState { - runs: Mutex>, - aggregate_billing: Mutex, - store: Arc, - artifact_store: ArtifactStore, + runs: Mutex>, + aggregate_billing: Mutex, + store: Arc, + artifact_store: ArtifactStore, artifact_upload_tokens: ArtifactUploadTokenKeys, - started_at: Instant, - max_concurrent_runs: usize, - scheduler_notify: Notify, - global_event_tx: broadcast::Sender, + started_at: Instant, + max_concurrent_runs: usize, + scheduler_notify: Notify, + global_event_tx: broadcast::Sender, - pub(crate) secret_store: AsyncRwLock, - pub(crate) settings: Arc>, - pub(crate) server_settings: RwLock>, - pub(crate) config_path: PathBuf, + pub(crate) secret_store: AsyncRwLock, + pub(crate) settings: Arc>, + pub(crate) server_settings: RwLock>, + pub(crate) config_path: PathBuf, pub(crate) local_daemon_mode: bool, - shutting_down: AtomicBool, - registry_factory_override: Option>, - slack_service: Option>, - slack_started: AtomicBool, + shutting_down: AtomicBool, + registry_factory_override: Option>, + slack_service: Option>, + slack_started: AtomicBool, } fn nonzero_i64(value: i64) -> Option { @@ -532,26 +532,26 @@ fn nonzero_i64(value: i64) -> Option { fn api_billed_token_counts_from_domain(billing: &BilledTokenCounts) -> ApiBilledTokenCounts { ApiBilledTokenCounts { - cache_read_tokens: nonzero_i64(billing.cache_read_tokens), + cache_read_tokens: nonzero_i64(billing.cache_read_tokens), cache_write_tokens: nonzero_i64(billing.cache_write_tokens), - input_tokens: billing.input_tokens, - output_tokens: billing.output_tokens, - reasoning_tokens: nonzero_i64(billing.reasoning_tokens), - total_tokens: billing.total_tokens, - total_usd_micros: billing.total_usd_micros, + input_tokens: billing.input_tokens, + output_tokens: billing.output_tokens, + reasoning_tokens: nonzero_i64(billing.reasoning_tokens), + total_tokens: billing.total_tokens, + total_usd_micros: billing.total_usd_micros, } } fn api_billed_token_counts_from_usage(usage: &BilledModelUsage) -> ApiBilledTokenCounts { let tokens = usage.tokens(); ApiBilledTokenCounts { - cache_read_tokens: nonzero_i64(tokens.cache_read_tokens), + cache_read_tokens: nonzero_i64(tokens.cache_read_tokens), cache_write_tokens: nonzero_i64(tokens.cache_write_tokens), - input_tokens: tokens.input_tokens, - output_tokens: tokens.output_tokens, - reasoning_tokens: nonzero_i64(tokens.reasoning_tokens), - total_tokens: tokens.total_tokens(), - total_usd_micros: usage.total_usd_micros, + input_tokens: tokens.input_tokens, + output_tokens: tokens.output_tokens, + reasoning_tokens: nonzero_i64(tokens.reasoning_tokens), + total_tokens: tokens.total_tokens(), + total_usd_micros: usage.total_usd_micros, } } @@ -654,11 +654,11 @@ impl AppState { .map(|duration| duration.as_secs()) .unwrap_or(0); let claims = ArtifactUploadClaims { - iss: ARTIFACT_UPLOAD_TOKEN_ISSUER.to_string(), - iat: now, - exp: now + ARTIFACT_UPLOAD_TOKEN_TTL_SECS, + iss: ARTIFACT_UPLOAD_TOKEN_ISSUER.to_string(), + iat: now, + exp: now + ARTIFACT_UPLOAD_TOKEN_TTL_SECS, run_id: run_id.to_string(), - scope: ARTIFACT_UPLOAD_TOKEN_SCOPE.to_string(), + scope: ARTIFACT_UPLOAD_TOKEN_SCOPE.to_string(), }; jsonwebtoken::encode( &Header::new(Algorithm::HS256), @@ -717,8 +717,8 @@ fn artifact_upload_token_keys() -> ArtifactUploadTokenKeys { validation.set_issuer(&[ARTIFACT_UPLOAD_TOKEN_ISSUER]); ArtifactUploadTokenKeys { - encoding: Arc::new(EncodingKey::from_secret(&secret)), - decoding: Arc::new(DecodingKey::from_secret(&secret)), + encoding: Arc::new(EncodingKey::from_secret(&secret)), + decoding: Arc::new(DecodingKey::from_secret(&secret)), validation: Arc::new(validation), } } @@ -1163,16 +1163,16 @@ async fn get_system_info( }; let response = SystemInfoResponse { - version: Some(FABRO_VERSION.to_string()), - git_sha: option_env!("FABRO_GIT_SHA").map(str::to_string), - build_date: option_env!("FABRO_BUILD_DATE").map(str::to_string), - os: Some(std::env::consts::OS.to_string()), - arch: Some(std::env::consts::ARCH.to_string()), - storage_engine: Some("slatedb".to_string()), - storage_dir: Some(state.server_storage_dir().display().to_string()), - uptime_secs: Some(to_i64(state.started_at.elapsed().as_secs())), - runs: Some(SystemRunCounts { - total: Some(to_i64(total_runs)), + version: Some(FABRO_VERSION.to_string()), + git_sha: option_env!("FABRO_GIT_SHA").map(str::to_string), + build_date: option_env!("FABRO_BUILD_DATE").map(str::to_string), + os: Some(std::env::consts::OS.to_string()), + arch: Some(std::env::consts::ARCH.to_string()), + storage_engine: Some("slatedb".to_string()), + storage_dir: Some(state.server_storage_dir().display().to_string()), + uptime_secs: Some(to_i64(state.started_at.elapsed().as_secs())), + runs: Some(SystemRunCounts { + total: Some(to_i64(total_runs)), active: Some(to_i64(active_runs)), }), sandbox_provider: Some(system_sandbox_provider(&settings)), @@ -1255,12 +1255,12 @@ async fn prune_runs( return ( StatusCode::OK, Json(PruneRunsResponse { - dry_run: Some(true), - runs: Some(prune_plan.rows), - total_count: Some(to_i64(prune_plan.run_ids.len())), + dry_run: Some(true), + runs: Some(prune_plan.rows), + total_count: Some(to_i64(prune_plan.run_ids.len())), total_size_bytes: Some(to_i64(prune_plan.total_size_bytes)), - deleted_count: Some(0), - freed_bytes: Some(0), + deleted_count: Some(0), + freed_bytes: Some(0), }), ) .into_response(); @@ -1275,12 +1275,12 @@ async fn prune_runs( ( StatusCode::OK, Json(PruneRunsResponse { - dry_run: Some(false), - runs: None, - total_count: Some(to_i64(prune_plan.run_ids.len())), + dry_run: Some(false), + runs: None, + total_count: Some(to_i64(prune_plan.run_ids.len())), total_size_bytes: Some(to_i64(prune_plan.total_size_bytes)), - deleted_count: Some(to_i64(prune_plan.run_ids.len())), - freed_bytes: Some(to_i64(prune_plan.total_size_bytes)), + deleted_count: Some(to_i64(prune_plan.run_ids.len())), + freed_bytes: Some(to_i64(prune_plan.total_size_bytes)), }), ) .into_response() @@ -1315,8 +1315,8 @@ async fn attach_events( } struct PrunePlan { - run_ids: Vec, - rows: Vec, + run_ids: Vec, + rows: Vec, total_size_bytes: u64, } @@ -1344,12 +1344,12 @@ fn build_disk_usage_response( } if verbose { run_rows.push(DiskUsageRunRow { - run_id: Some(run.run_id().to_string()), + run_id: Some(run.run_id().to_string()), workflow_name: Some(run.workflow_name()), - status: Some(run.status().to_string()), - start_time: Some(run.start_time()), - size_bytes: Some(to_i64(size)), - reclaimable: Some(!run.status().is_active()), + status: Some(run.status().to_string()), + start_time: Some(run.start_time()), + size_bytes: Some(to_i64(size)), + reclaimable: Some(!run.status().is_active()), }); } } @@ -1370,25 +1370,25 @@ fn build_disk_usage_response( } Ok(DiskUsageResponse { - summary: vec![ + summary: vec![ DiskUsageSummaryRow { - type_: Some("runs".to_string()), - count: Some(to_i64(runs.len())), - active: Some(to_i64(active_count)), - size_bytes: Some(to_i64(total_run_size)), + type_: Some("runs".to_string()), + count: Some(to_i64(runs.len())), + active: Some(to_i64(active_count)), + size_bytes: Some(to_i64(total_run_size)), reclaimable_bytes: Some(to_i64(reclaimable_run_size)), }, DiskUsageSummaryRow { - type_: Some("logs".to_string()), - count: Some(to_i64(log_count)), - active: None, - size_bytes: Some(to_i64(total_log_size)), + type_: Some("logs".to_string()), + count: Some(to_i64(log_count)), + active: None, + size_bytes: Some(to_i64(total_log_size)), reclaimable_bytes: Some(to_i64(total_log_size)), }, ], - total_size_bytes: Some(to_i64(total_run_size + total_log_size)), + total_size_bytes: Some(to_i64(total_run_size + total_log_size)), total_reclaimable_bytes: Some(to_i64(reclaimable_run_size + total_log_size)), - runs: verbose.then_some(run_rows), + runs: verbose.then_some(run_rows), }) } @@ -1438,10 +1438,10 @@ fn build_prune_plan( let rows = filtered .iter() .map(|run| PruneRunEntry { - run_id: Some(run.run_id().to_string()), - dir_name: Some(run.dir_name.clone()), + run_id: Some(run.run_id().to_string()), + dir_name: Some(run.dir_name.clone()), workflow_name: Some(run.workflow_name()), - size_bytes: Some(to_i64(dir_size(&run.path))), + size_bytes: Some(to_i64(dir_size(&run.path))), }) .collect::>(); let total_size_bytes = rows @@ -1677,8 +1677,8 @@ async fn delete_secret( #[derive(serde::Deserialize)] struct GitHubRepoResponse { default_branch: String, - private: bool, - permissions: Option, + private: bool, + permissions: Option, } async fn get_github_repo( @@ -1870,8 +1870,8 @@ async fn get_aggregate_billing( .iter() .map(|(model, totals)| BillingByModel { billing: api_billed_token_counts_from_domain(&totals.billing), - model: ModelReference { id: model.clone() }, - stages: totals.stages, + model: ModelReference { id: model.clone() }, + stages: totals.stages, }) .collect(); let total_billing = by_model @@ -1890,15 +1890,15 @@ async fn get_aggregate_billing( }); let response = AggregateBilling { totals: AggregateBillingTotals { - cache_read_tokens: nonzero_i64(total_billing.cache_read_tokens), + cache_read_tokens: nonzero_i64(total_billing.cache_read_tokens), cache_write_tokens: nonzero_i64(total_billing.cache_write_tokens), - input_tokens: total_billing.input_tokens, - output_tokens: total_billing.output_tokens, - reasoning_tokens: nonzero_i64(total_billing.reasoning_tokens), - runs: agg.total_runs, - runtime_secs: agg.total_runtime_secs, - total_tokens: total_billing.total_tokens, - total_usd_micros: total_billing.total_usd_micros, + input_tokens: total_billing.input_tokens, + output_tokens: total_billing.output_tokens, + reasoning_tokens: nonzero_i64(total_billing.reasoning_tokens), + runs: agg.total_runs, + runtime_secs: agg.total_runtime_secs, + total_tokens: total_billing.total_tokens, + total_usd_micros: total_billing.total_usd_micros, }, by_model, }; @@ -1928,16 +1928,16 @@ async fn get_run_billing( let Some(checkpoint) = checkpoint else { let empty = RunBilling { by_model: Vec::new(), - stages: Vec::new(), - totals: RunBillingTotals { - cache_read_tokens: None, + stages: Vec::new(), + totals: RunBillingTotals { + cache_read_tokens: None, cache_write_tokens: None, - input_tokens: 0, - output_tokens: 0, - reasoning_tokens: None, - runtime_secs: 0.0, - total_tokens: 0, - total_usd_micros: None, + input_tokens: 0, + output_tokens: 0, + reasoning_tokens: None, + runtime_secs: 0.0, + total_tokens: 0, + total_usd_micros: None, }, }; return (StatusCode::OK, Json(empty)).into_response(); @@ -1977,7 +1977,7 @@ async fn get_run_billing( model: ModelReference { id: model_id }, runtime_secs: duration_ms as f64 / 1000.0, stage: BillingStageRef { - id: node_id.clone(), + id: node_id.clone(), name: node_id.clone(), }, }); @@ -1988,8 +1988,8 @@ async fn get_run_billing( .into_iter() .map(|(model, totals)| BillingByModel { billing: api_billed_token_counts_from_domain(&totals.billing), - model: ModelReference { id: model }, - stages: totals.stages, + model: ModelReference { id: model }, + stages: totals.stages, }) .collect::>(); @@ -2556,7 +2556,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, } @@ -2742,9 +2742,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, }, ) @@ -3058,41 +3058,41 @@ fn runtime_question_type(question_type: InterviewQuestionType) -> QuestionType { fn runtime_question_from_interview_record(question: &InterviewQuestionRecord) -> Question { Question { - id: question.id.clone(), - text: question.text.clone(), - question_type: runtime_question_type(question.question_type), - options: question + id: question.id.clone(), + text: question.text.clone(), + question_type: runtime_question_type(question.question_type), + options: question .options .iter() .map(|option| fabro_interview::QuestionOption { - key: option.key.clone(), + key: option.key.clone(), label: option.label.clone(), }) .collect(), - allow_freeform: question.allow_freeform, - default: None, + allow_freeform: question.allow_freeform, + default: None, timeout_seconds: question.timeout_seconds, - stage: question.stage.clone(), - metadata: HashMap::new(), + stage: question.stage.clone(), + metadata: HashMap::new(), context_display: question.context_display.clone(), } } fn api_question_from_interview_record(question: &InterviewQuestionRecord) -> ApiQuestion { ApiQuestion { - id: question.id.clone(), - text: question.text.clone(), - stage: question.stage.clone(), - question_type: api_question_type(question.question_type), - options: question + id: question.id.clone(), + text: question.text.clone(), + stage: question.stage.clone(), + question_type: api_question_type(question.question_type), + options: question .options .iter() .map(|option| ApiQuestionOption { - key: option.key.clone(), + key: option.key.clone(), label: option.label.clone(), }) .collect(), - allow_freeform: question.allow_freeform, + allow_freeform: question.allow_freeform, timeout_seconds: question.timeout_seconds, context_display: question.context_display.clone(), } @@ -3249,13 +3249,10 @@ 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() { @@ -3348,12 +3345,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, }), } @@ -3561,13 +3558,13 @@ async fn start_run( ( StatusCode::OK, Json(RunStatusResponse { - id: id.to_string(), - status: RunStatus::Queued, - error: None, - queue_position: None, - status_reason: None, + id: id.to_string(), + status: RunStatus::Queued, + error: None, + queue_position: None, + status_reason: None, pending_control: None, - created_at: id.created_at(), + created_at: id.created_at(), }), ) .into_response() @@ -3880,9 +3877,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, }, ) @@ -3901,9 +3898,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, }, ) @@ -3930,9 +3927,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, }, ) @@ -3950,9 +3947,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, }, ) @@ -3982,9 +3979,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, }, ) @@ -4606,11 +4603,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(), }) @@ -4658,8 +4655,8 @@ enum ArtifactUploadContentType { } struct ValidatedArtifactBatchEntry { - path: String, - sha256: Option, + path: String, + sha256: Option, expected_bytes: Option, } @@ -4791,14 +4788,11 @@ 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!( @@ -5118,7 +5112,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(); @@ -5128,7 +5122,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(); @@ -5179,8 +5173,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(), }) @@ -5792,9 +5786,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(), ) @@ -5837,21 +5831,18 @@ async fn create_completion( if state.dry_run() { let msg_id = Ulid::new().to_string(); if use_stream { - let finish_event = StreamEvent::finish( - FinishReason::Stop, - TokenCounts::default(), - LlmResponse { - id: msg_id.clone(), - model: model_id.clone(), - provider: String::new(), - message: LlmMessage::assistant(""), + let finish_event = + StreamEvent::finish(FinishReason::Stop, TokenCounts::default(), LlmResponse { + id: msg_id.clone(), + model: model_id.clone(), + provider: String::new(), + message: LlmMessage::assistant(""), finish_reason: FinishReason::Stop, - usage: TokenCounts::default(), - raw: None, - warnings: vec![], - rate_limit: None, - }, - ); + usage: TokenCounts::default(), + raw: None, + warnings: vec![], + rate_limit: None, + }); let json = serde_json::to_string(&finish_event).unwrap_or_default(); let sse_stream = stream::iter(vec![Ok::<_, std::convert::Infallible>( Event::default().event("stream_event").data(json), @@ -5859,21 +5850,21 @@ async fn create_completion( return Sse::new(sse_stream).into_response(); } let empty_msg = CompletionMessage { - role: CompletionMessageRole::Assistant, - content: vec![], - name: None, + role: CompletionMessageRole::Assistant, + content: vec![], + name: None, tool_call_id: None, }; return Json(CompletionResponse { - id: msg_id, - model: model_id, - message: empty_msg, + id: msg_id, + model: model_id, + message: empty_msg, stop_reason: "end_turn".to_string(), - usage: CompletionUsage { - input_tokens: 0, + usage: CompletionUsage { + input_tokens: 0, output_tokens: 0, }, - output: None, + output: None, }) .into_response(); } @@ -5956,15 +5947,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}")) @@ -5973,15 +5964,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}")) @@ -6326,14 +6317,11 @@ mod tests { .iter() .map(|model| model["id"].as_str().unwrap().to_string()) .collect::>(); - assert_eq!( - model_ids, - vec![ - "gpt-5.2-codex".to_string(), - "gpt-5.3-codex".to_string(), - "gpt-5.3-codex-spark".to_string() - ] - ); + assert_eq!(model_ids, vec![ + "gpt-5.2-codex".to_string(), + "gpt-5.3-codex".to_string(), + "gpt-5.3-codex-spark".to_string() + ]); } #[tokio::test] @@ -6604,18 +6592,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, }, @@ -7031,14 +7019,10 @@ 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 { @@ -7808,42 +7792,32 @@ 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(); @@ -7891,18 +7865,14 @@ level = "debug" let state = create_app_state(); let run_id = fixtures::RUN_4; - create_durable_run_with_events( - &state, - run_id, - &[ - workflow_event::Event::RunSubmitted { - reason: None, - definition_blob: None, - }, - workflow_event::Event::RunStarting { reason: None }, - workflow_event::Event::RunRunning { reason: None }, - ], - ) + create_durable_run_with_events(&state, run_id, &[ + workflow_event::Event::RunSubmitted { + reason: None, + definition_blob: None, + }, + workflow_event::Event::RunStarting { reason: None }, + workflow_event::Event::RunRunning { reason: None }, + ]) .await; let temp_dir = tempfile::tempdir().unwrap(); diff --git a/lib/crates/fabro-server/src/web_auth.rs b/lib/crates/fabro-server/src/web_auth.rs index 4a103e91f..ae424c03d 100644 --- a/lib/crates/fabro-server/src/web_auth.rs +++ b/lib/crates/fabro-server/src/web_auth.rs @@ -20,18 +20,18 @@ const OAUTH_STATE_COOKIE_NAME: &str = "fabro_oauth_state"; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SessionCookie { - pub login: String, - pub name: String, - pub email: String, + pub login: String, + pub name: String, + pub email: String, pub avatar_url: String, - pub user_url: String, - pub github_id: i64, - pub exp: i64, + pub user_url: String, + pub github_id: i64, + pub exp: i64, } #[derive(Deserialize)] struct OAuthCallbackParams { - code: String, + code: String, state: String, } @@ -52,22 +52,22 @@ struct SetupStatusResponse { #[derive(Serialize)] struct AuthMeResponse { - user: SessionUser, - provider: &'static str, + user: SessionUser, + provider: &'static str, #[serde(rename = "demoMode")] demo_mode: bool, - features: serde_json::Value, + features: serde_json::Value, } #[derive(Serialize)] struct SessionUser { - login: String, - name: String, - email: String, + login: String, + name: String, + email: String, #[serde(rename = "avatarUrl")] avatar_url: String, #[serde(rename = "userUrl")] - user_url: String, + user_url: String, } #[derive(Deserialize)] @@ -77,27 +77,27 @@ struct GitHubTokenResponse { #[derive(Deserialize)] struct GitHubUser { - id: i64, - login: String, - name: Option, + id: i64, + login: String, + name: Option, avatar_url: String, } #[derive(Deserialize)] struct GitHubEmail { - email: String, - primary: bool, + email: String, + primary: bool, verified: bool, } #[derive(Deserialize)] struct GitHubManifestConversion { - id: i64, - slug: String, - client_id: String, - client_secret: String, + id: i64, + slug: String, + client_id: String, + client_secret: String, webhook_secret: Option, - pem: String, + pem: String, } pub fn routes() -> Router> { @@ -206,16 +206,14 @@ async fn login_github(State(state): State>) -> Response { } let state_token = format!("fabro-{}", ulid::Ulid::new()); - let authorize_url = reqwest::Url::parse_with_params( - "https://github.com/login/oauth/authorize", - &[ + let authorize_url = + reqwest::Url::parse_with_params("https://github.com/login/oauth/authorize", &[ ("client_id", client_id.as_str()), ("redirect_uri", &format!("{web_url}/auth/callback/github")), ("scope", "read:user user:email"), ("state", state_token.as_str()), - ], - ) - .expect("GitHub authorize URL should be valid"); + ]) + .expect("GitHub authorize URL should be valid"); debug!(redirect_uri = %format!("{web_url}/auth/callback/github"), "OAuth login redirecting to GitHub"); @@ -398,13 +396,13 @@ async fn callback_github( .unwrap_or_default(); let now = chrono::Utc::now(); let session = SessionCookie { - login: profile.login.clone(), - name: profile.name.unwrap_or_else(|| profile.login.clone()), - email: primary_email, + login: profile.login.clone(), + name: profile.name.unwrap_or_else(|| profile.login.clone()), + email: primary_email, avatar_url: profile.avatar_url, - user_url: format!("https://github.com/{}", profile.login), - github_id: profile.id, - exp: (now + chrono::Duration::days(30)).timestamp(), + user_url: format!("https://github.com/{}", profile.login), + github_id: profile.id, + exp: (now + chrono::Duration::days(30)).timestamp(), }; info!(login = %session.login, "OAuth login succeeded"); @@ -478,11 +476,11 @@ async fn auth_me(State(state): State>, headers: HeaderMap) -> Resp .is_some_and(|cookie| cookie.value() == "1"); Json(AuthMeResponse { user: SessionUser { - login: session.login, - name: session.name, - email: session.email, + login: session.login, + name: session.name, + email: session.email, avatar_url: session.avatar_url, - user_url: session.user_url, + user_url: session.user_url, }, provider: "github", demo_mode, @@ -747,11 +745,11 @@ mod tests { fn sample_conversion() -> GitHubManifestConversion { GitHubManifestConversion { - id: 123, - slug: "fabro".to_string(), - client_id: "abc".to_string(), - client_secret: "shh".to_string(), - pem: String::new(), + id: 123, + slug: "fabro".to_string(), + client_id: "abc".to_string(), + client_secret: "shh".to_string(), + pem: String::new(), webhook_secret: None, } } diff --git a/lib/crates/fabro-server/tests/it/api/mtls.rs b/lib/crates/fabro-server/tests/it/api/mtls.rs index 4af5c7045..34e353eee 100644 --- a/lib/crates/fabro-server/tests/it/api/mtls.rs +++ b/lib/crates/fabro-server/tests/it/api/mtls.rs @@ -19,20 +19,20 @@ fn fixture_path(name: &str) -> PathBuf { fn fixture_pki() -> PkiPaths { PkiPaths { - ca_cert: fixture_path("ca.crt"), + ca_cert: fixture_path("ca.crt"), server_cert: fixture_path("server.crt"), - server_key: fixture_path("server.key"), + server_key: fixture_path("server.key"), client_cert: fixture_path("client.crt"), - client_key: fixture_path("client.key"), + client_key: fixture_path("client.key"), } } struct PkiPaths { - ca_cert: PathBuf, + ca_cert: PathBuf, server_cert: PathBuf, - server_key: PathBuf, + server_key: PathBuf, client_cert: PathBuf, - client_key: PathBuf, + client_key: PathBuf, } /// Start a TLS server on a random port, returning the bound address. @@ -94,8 +94,8 @@ async fn mtls_accepts_valid_client_cert() { let tls_settings = TlsSettings { cert: pki.server_cert.clone(), - key: pki.server_key.clone(), - ca: pki.ca_cert.clone(), + key: pki.server_key.clone(), + ca: pki.ca_cert.clone(), }; let auth_mode = AuthMode::Strategies(vec![AuthStrategy::Mtls]); @@ -119,8 +119,8 @@ async fn mtls_only_rejects_wrong_ca_client_cert() { let tls_settings = TlsSettings { cert: pki.server_cert.clone(), - key: pki.server_key.clone(), - ca: pki.ca_cert.clone(), + key: pki.server_key.clone(), + ca: pki.ca_cert.clone(), }; let auth_mode = AuthMode::Strategies(vec![AuthStrategy::Mtls]); @@ -157,8 +157,8 @@ async fn mtls_only_rejects_no_client_cert() { let tls_settings = TlsSettings { cert: pki.server_cert.clone(), - key: pki.server_key.clone(), - ca: pki.ca_cert.clone(), + key: pki.server_key.clone(), + ca: pki.ca_cert.clone(), }; // mTLS is the ONLY strategy -> client cert is required at TLS level @@ -213,21 +213,18 @@ async fn mtls_and_jwt_accepts_valid_jwt_without_client_cert() { let tls_settings = TlsSettings { cert: pki.server_cert.clone(), - key: pki.server_key.clone(), - ca: pki.ca_cert.clone(), + key: pki.server_key.clone(), + ca: pki.ca_cert.clone(), }; let (encoding_key, decoding_key) = fixture_jwt_keypair(); // Both mTLS and JWT strategies; mTLS is optional since JWT is also present - let auth_mode = AuthMode::Strategies(vec![ - AuthStrategy::Mtls, - AuthStrategy::Jwt { - key: Arc::new(decoding_key), - validation: Arc::new(fabro_server::jwt_auth::jwt_validation()), - allowed_usernames: vec!["brynary".to_string()], - }, - ]); + let auth_mode = AuthMode::Strategies(vec![AuthStrategy::Mtls, AuthStrategy::Jwt { + key: Arc::new(decoding_key), + validation: Arc::new(fabro_server::jwt_auth::jwt_validation()), + allowed_usernames: vec!["brynary".to_string()], + }]); let addr = start_tls_server(&tls_settings, ClientAuth::Optional, auth_mode).await; // Client trusts the server CA but presents NO client cert diff --git a/lib/crates/fabro-server/tests/it/scenario/lifecycle.rs b/lib/crates/fabro-server/tests/it/scenario/lifecycle.rs index 5e5bc26ae..3bba3c4ec 100644 --- a/lib/crates/fabro-server/tests/it/scenario/lifecycle.rs +++ b/lib/crates/fabro-server/tests/it/scenario/lifecycle.rs @@ -280,11 +280,8 @@ async fn cancel_at_human_gate_persists_cancelled_terminal_event() { }) .collect::>(); - assert_eq!( - failed_reasons, - vec![( - Some("cancelled".to_string()), - Some("Pipeline cancelled".to_string()) - )] - ); + assert_eq!(failed_reasons, vec![( + Some("cancelled".to_string()), + Some("Pipeline cancelled".to_string()) + )]); } diff --git a/lib/crates/fabro-slack/src/blocks.rs b/lib/crates/fabro-slack/src/blocks.rs index 784273654..7211b2322 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 9ab5ca506..1fcd0d374 100644 --- a/lib/crates/fabro-slack/src/client.rs +++ b/lib/crates/fabro-slack/src/client.rs @@ -7,14 +7,14 @@ const SLACK_API_BASE: &str = "https://slack.com/api"; #[derive(Debug, Clone)] pub struct PostedMessage { pub channel_id: String, - pub ts: String, + pub ts: String, } #[derive(Clone)] pub struct SlackClient { bot_token: String, - api_base: String, - http: Client, + api_base: String, + http: Client, } impl SlackClient { @@ -110,7 +110,7 @@ pub fn parse_post_message_response(response: &Value) -> Result Option { } pub struct SlackRuntimeOptions { - pub config: SlackOptions, + pub config: SlackOptions, pub credentials: SlackCredentials, } diff --git a/lib/crates/fabro-slack/src/dispatch.rs b/lib/crates/fabro-slack/src/dispatch.rs index 37832ada9..b0c0f9f8b 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 023292c56..6a75c3c01 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 c8cf2035b..12846658e 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 82253796e..f99cc1c53 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 b83ea8a62..de14dfde1 100644 --- a/lib/crates/fabro-slack/src/threads.rs +++ b/lib/crates/fabro-slack/src/threads.rs @@ -19,13 +19,10 @@ 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 { @@ -87,7 +84,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 9181b31e5..eb5df75fb 100644 --- a/lib/crates/fabro-store/src/artifact_store.rs +++ b/lib/crates/fabro-store/src/artifact_store.rs @@ -17,15 +17,15 @@ const STREAM_BUFFER_BYTES: usize = 1024 * 1024; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct NodeArtifact { - pub node: StageId, + pub node: StageId, pub filename: String, - pub size: u64, + pub size: u64, } #[derive(Clone)] pub struct ArtifactStore { object_store: Arc, - prefix: ObjectPath, + prefix: ObjectPath, } impl std::fmt::Debug for ArtifactStore { @@ -299,18 +299,16 @@ 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/lib.rs b/lib/crates/fabro-store/src/lib.rs index f3ec04e67..0cff9c5db 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 4c73de2fc..cb0be518d 100644 --- a/lib/crates/fabro-store/src/run_state.rs +++ b/lib/crates/fabro-store/src/run_state.rs @@ -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 { @@ -172,11 +172,11 @@ impl RunProjection { } EventBody::SandboxInitialized(props) => { self.sandbox = Some(SandboxRecord { - provider: props.provider.clone(), - working_directory: props.working_directory.clone(), - identifier: props.identifier.clone(), + provider: props.provider.clone(), + working_directory: props.working_directory.clone(), + identifier: props.identifier.clone(), host_working_directory: props.host_working_directory.clone(), - container_mount_point: props.container_mount_point.clone(), + container_mount_point: props.container_mount_point.clone(), }); } EventBody::RetroStarted(props) => { @@ -193,37 +193,35 @@ 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), - }, - ); + }); } EventBody::InterviewCompleted(props) => { if !props.question_id.is_empty() { @@ -513,21 +511,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), } } @@ -684,31 +682,25 @@ 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(); @@ -735,21 +727,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()), }), @@ -780,8 +772,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"), @@ -801,7 +793,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", @@ -827,7 +819,7 @@ mod tests { .unwrap(), }, EventEnvelope { - seq: 2, + seq: 2, payload: EventPayload::new( json!({ "id": "evt-run-submitted", diff --git a/lib/crates/fabro-store/src/slate/mod.rs b/lib/crates/fabro-store/src/slate/mod.rs index ae10b58c4..601d35ffd 100644 --- a/lib/crates/fabro-store/src/slate/mod.rs +++ b/lib/crates/fabro-store/src/slate/mod.rs @@ -16,11 +16,11 @@ 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, - db: Arc>, - active_runs: Arc>>>, + db: Arc>, + active_runs: Arc>>>, } impl std::fmt::Debug for Database { diff --git a/lib/crates/fabro-store/src/slate/run_store.rs b/lib/crates/fabro-store/src/slate/run_store.rs index 7d2134abb..d2779565a 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 { @@ -51,7 +51,7 @@ impl RunDatabase { .await?; let (event_tx, _) = broadcast::channel(DEFAULT_EVENT_TAIL_LIMIT.max(16)); Ok(Self { - inner: Arc::new(RunDatabaseInner { + inner: Arc::new(RunDatabaseInner { run_id, db, event_seq: AtomicU32::new(event_seq), @@ -75,7 +75,7 @@ impl RunDatabase { .await?; let (event_tx, _) = broadcast::channel(DEFAULT_EVENT_TAIL_LIMIT.max(16)); Ok(Self { - inner: Arc::new(RunDatabaseInner { + inner: Arc::new(RunDatabaseInner { run_id, db, event_seq: AtomicU32::new(event_seq), @@ -99,7 +99,7 @@ impl RunDatabase { pub(crate) fn read_only_clone(&self) -> Self { Self { - inner: Arc::clone(&self.inner), + inner: Arc::clone(&self.inner), read_only: true, } } diff --git a/lib/crates/fabro-store/src/types.rs b/lib/crates/fabro-store/src/types.rs index fe3fae6a8..0450b5c4c 100644 --- a/lib/crates/fabro-store/src/types.rs +++ b/lib/crates/fabro-store/src/types.rs @@ -8,17 +8,17 @@ use crate::{Error, Result}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RunSummary { - pub run_id: RunId, - pub workflow_name: Option, - pub workflow_slug: Option, - pub goal: Option, - pub labels: HashMap, - pub host_repo_path: Option, - pub start_time: Option>, - pub status: Option, - pub status_reason: Option, - pub pending_control: Option, - pub duration_ms: Option, + pub run_id: RunId, + pub workflow_name: Option, + pub workflow_slug: Option, + pub goal: Option, + pub labels: HashMap, + pub host_repo_path: Option, + pub start_time: Option>, + pub status: Option, + pub status_reason: Option, + pub pending_control: Option, + pub duration_ms: Option, pub total_usd_micros: Option, } @@ -83,7 +83,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 +99,27 @@ mod tests { #[test] fn wire_event_envelope_round_trips() { let event = RunEvent { - id: "evt_1".to_string(), - ts: Utc.with_ymd_and_hms(2026, 4, 9, 12, 0, 0).unwrap(), - run_id: fixtures::RUN_1, - node_id: Some("code".to_string()), - node_label: Some("Code".to_string()), - stage_id: Some(StageId::new("code", 1)), - parallel_group_id: None, + id: "evt_1".to_string(), + ts: Utc.with_ymd_and_hms(2026, 4, 9, 12, 0, 0).unwrap(), + run_id: fixtures::RUN_1, + node_id: Some("code".to_string()), + node_label: Some("Code".to_string()), + stage_id: Some(StageId::new("code", 1)), + parallel_group_id: None, parallel_branch_id: None, - session_id: None, - parent_session_id: None, - tool_call_id: None, - actor: None, - body: EventBody::RunCompleted(RunCompletedProps { - duration_ms: 42, - artifact_count: 0, - status: "success".to_string(), - reason: None, - total_usd_micros: None, + session_id: None, + parent_session_id: None, + tool_call_id: None, + actor: None, + body: EventBody::RunCompleted(RunCompletedProps { + duration_ms: 42, + artifact_count: 0, + status: "success".to_string(), + reason: None, + total_usd_micros: None, final_git_commit_sha: None, - final_patch: None, - billing: None, + final_patch: None, + billing: None, }), }; let payload = EventPayload::new(event.to_value().unwrap(), &fixtures::RUN_1).unwrap(); @@ -140,30 +140,30 @@ mod tests { let group = StageId::new("review", 2); let branch = ParallelBranchId::new(group.clone(), 3); let event = RunEvent { - id: "evt_2".to_string(), - ts: Utc.with_ymd_and_hms(2026, 4, 9, 13, 0, 0).unwrap(), - run_id: fixtures::RUN_1, - node_id: Some("review".to_string()), - node_label: Some("Review".to_string()), - stage_id: Some(StageId::new("review", 2)), - parallel_group_id: Some(group), + id: "evt_2".to_string(), + ts: Utc.with_ymd_and_hms(2026, 4, 9, 13, 0, 0).unwrap(), + run_id: fixtures::RUN_1, + node_id: Some("review".to_string()), + node_label: Some("Review".to_string()), + stage_id: Some(StageId::new("review", 2)), + parallel_group_id: Some(group), parallel_branch_id: Some(branch), - session_id: Some("ses_42".to_string()), - parent_session_id: Some("ses_root".to_string()), - tool_call_id: Some("tool_call_xyz".to_string()), - actor: Some(ActorRef::agent( + session_id: Some("ses_42".to_string()), + parent_session_id: Some("ses_root".to_string()), + tool_call_id: Some("tool_call_xyz".to_string()), + actor: Some(ActorRef::agent( Some("ses_42".to_string()), Some("claude-sonnet".to_string()), )), - body: EventBody::RunCompleted(RunCompletedProps { - duration_ms: 100, - artifact_count: 1, - status: "success".to_string(), - reason: None, - total_usd_micros: None, + body: EventBody::RunCompleted(RunCompletedProps { + duration_ms: 100, + artifact_count: 1, + status: "success".to_string(), + reason: None, + total_usd_micros: None, final_git_commit_sha: None, - final_patch: None, - billing: None, + final_patch: None, + billing: None, }), }; let payload = EventPayload::new(event.to_value().unwrap(), &fixtures::RUN_1).unwrap(); diff --git a/lib/crates/fabro-telemetry/src/buffer.rs b/lib/crates/fabro-telemetry/src/buffer.rs index 04d125993..e5682e0ac 100644 --- a/lib/crates/fabro-telemetry/src/buffer.rs +++ b/lib/crates/fabro-telemetry/src/buffer.rs @@ -6,14 +6,14 @@ use crate::event::Track; #[derive(Clone, Copy)] pub(crate) struct BufferPolicy { pub count_threshold: usize, - pub time_threshold: Duration, + pub time_threshold: Duration, } impl Default for BufferPolicy { fn default() -> Self { Self { count_threshold: 20, - time_threshold: Duration::from_secs(60), + time_threshold: Duration::from_secs(60), } } } @@ -70,13 +70,13 @@ mod tests { fn make_track(event: &str) -> Track { Track { - user: User::AnonymousId { + user: User::AnonymousId { anonymous_id: "test".to_string(), }, - event: event.to_string(), + event: event.to_string(), properties: json!({}), - context: None, - timestamp: None, + context: None, + timestamp: None, message_id: format!("msg-{event}"), } } @@ -98,7 +98,7 @@ mod tests { &rx, BufferPolicy { count_threshold: 2, - time_threshold: Duration::from_secs(60), + time_threshold: Duration::from_secs(60), }, move |tracks| { let events: Vec = tracks.iter().map(|t| t.event.clone()).collect(); @@ -133,7 +133,7 @@ mod tests { &rx, BufferPolicy { count_threshold: 2, - time_threshold: Duration::from_secs(60), + time_threshold: Duration::from_secs(60), }, move |_| { *mid.lock().unwrap() = true; @@ -164,7 +164,7 @@ mod tests { &rx, BufferPolicy { count_threshold: 100, // won't trigger - time_threshold: Duration::from_millis(50), + time_threshold: Duration::from_millis(50), }, move |tracks| { let events: Vec = tracks.iter().map(|t| t.event.clone()).collect(); @@ -206,7 +206,7 @@ mod tests { &rx, BufferPolicy { count_threshold: 100, // won't trigger - time_threshold: Duration::from_secs(60), + time_threshold: Duration::from_secs(60), }, move |tracks| { let events: Vec = tracks.iter().map(|t| t.event.clone()).collect(); diff --git a/lib/crates/fabro-telemetry/src/event.rs b/lib/crates/fabro-telemetry/src/event.rs index dcdd0df33..1e98cab9d 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 e19bf7222..0018c8c05 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 1703649b9..f1181561b 100644 --- a/lib/crates/fabro-telemetry/src/sender.rs +++ b/lib/crates/fabro-telemetry/src/sender.rs @@ -229,13 +229,13 @@ mod tests { // SEGMENT_WRITE_KEY is not set at compile time in tests, // so emit() should return immediately without spawning. let track = Track { - user: User::AnonymousId { + user: User::AnonymousId { anonymous_id: "test".to_string(), }, - event: "test".to_string(), + event: "test".to_string(), properties: json!({}), - context: None, - timestamp: None, + context: None, + timestamp: None, message_id: "msg-test".to_string(), }; @@ -254,13 +254,13 @@ mod tests { #[test] fn upload_blocking_noops_without_write_key() { let track = Track { - user: User::AnonymousId { + user: User::AnonymousId { anonymous_id: "test".to_string(), }, - event: "test".to_string(), + event: "test".to_string(), properties: json!({}), - context: None, - timestamp: None, + context: None, + timestamp: None, message_id: "msg-test".to_string(), }; diff --git a/lib/crates/fabro-template/src/lib.rs b/lib/crates/fabro-template/src/lib.rs index 5302b4110..58e0d92b2 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 290c6d795..6d1ae6d70 100644 --- a/lib/crates/fabro-test/src/lib.rs +++ b/lib/crates/fabro-test/src/lib.rs @@ -104,23 +104,23 @@ pub fn require_env(name: &str) -> Option { /// shared per nextest run when `NEXTEST_RUN_ID` is present, otherwise shared /// per test process. pub struct TestContext { - pub temp_dir: PathBuf, - pub home_dir: PathBuf, - pub storage_dir: PathBuf, - test_case_id: String, - test_run_id: String, - session_root: PathBuf, - fabro_bin: PathBuf, - filters: Vec<(String, String)>, - active_socket_path: PathBuf, - isolated_server: Option, + pub temp_dir: PathBuf, + pub home_dir: PathBuf, + pub storage_dir: PathBuf, + test_case_id: String, + test_run_id: String, + session_root: PathBuf, + fabro_bin: PathBuf, + filters: Vec<(String, String)>, + active_socket_path: PathBuf, + isolated_server: Option, managed_storage_dirs: Vec, - _context_root: tempfile::TempDir, + _context_root: tempfile::TempDir, } #[derive(Debug, Clone)] struct ServerPaths { - root: PathBuf, + root: PathBuf, storage_dir: PathBuf, socket_path: PathBuf, config_path: PathBuf, @@ -128,7 +128,7 @@ struct ServerPaths { #[derive(Debug, Clone)] struct SessionPaths { - root: PathBuf, + root: PathBuf, server: ServerPaths, } @@ -140,7 +140,7 @@ enum SessionMode { #[derive(Debug, Serialize)] struct ClientMarker { - pid: u32, + pid: u32, touched_at_ms: u128, } @@ -186,37 +186,29 @@ 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"), - }, - root, + 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, + }); } } 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"), - }, - root, + (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, + }) } fn short_session_base_dir() -> PathBuf { @@ -324,7 +316,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); @@ -672,7 +664,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"), @@ -686,7 +678,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"), @@ -705,7 +697,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"), @@ -1244,7 +1236,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(), @@ -1390,7 +1382,7 @@ pub struct TwinOpenAi { pub struct TwinGitHub { pub base_url: String, - server: twin_github::TestServer, + server: twin_github::TestServer, } pub fn test_http_client() -> reqwest::Client { @@ -1479,7 +1471,7 @@ impl TwinScenarios { #[derive(Debug, Clone)] pub struct TwinScenario { matcher: Map, - script: Value, + script: Value, } impl TwinScenario { @@ -1493,7 +1485,7 @@ impl TwinScenario { ), ("model".to_string(), Value::String(model.into())), ]), - script: json!({ "kind": "success" }), + script: json!({ "kind": "success" }), } } @@ -1587,7 +1579,7 @@ impl TwinScenario { #[derive(Debug, Clone)] pub struct TwinToolCall { - name: String, + name: String, arguments: Value, } @@ -1669,7 +1661,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, }; @@ -1818,18 +1810,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 @@ -1861,7 +1853,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 4a0cbbd7b..6ac1aa627 100644 --- a/lib/crates/fabro-tracker/src/github.rs +++ b/lib/crates/fabro-tracker/src/github.rs @@ -29,12 +29,12 @@ async fn execute_github_graphql( /// /// Scoped to a single project board identified by `project_number`. pub struct GitHubTracker { - creds: GitHubAppCredentials, - client: reqwest::Client, - owner: String, - repo: String, - project_number: u64, - base_url: String, + creds: GitHubAppCredentials, + client: reqwest::Client, + owner: String, + repo: String, + project_number: u64, + base_url: String, project_node_id: OnceCell, } @@ -627,7 +627,7 @@ mod tests { fn mock_github_tracker(server_url: &str, pem: String) -> GitHubTracker { GitHubTracker::new( GitHubAppCredentials { - app_id: "test-app".to_string(), + app_id: "test-app".to_string(), private_key_pem: pem, }, test_http_client(), @@ -640,20 +640,20 @@ mod tests { fn make_test_issue(state: &str) -> Issue { Issue { - id: "I_issue1".to_string(), + id: "I_issue1".to_string(), project_item_id: Some("PVTI_item1".to_string()), - identifier: "#42".to_string(), - title: "Fix bug".to_string(), - description: None, - priority: None, - state: state.to_string(), - branch_name: None, - url: "https://github.com/owner/repo/issues/42".to_string(), - assignee_id: None, - labels: vec![], - blocked_by: vec![], - created_at: None, - updated_at: None, + identifier: "#42".to_string(), + title: "Fix bug".to_string(), + description: None, + priority: None, + state: state.to_string(), + branch_name: None, + url: "https://github.com/owner/repo/issues/42".to_string(), + assignee_id: None, + labels: vec![], + blocked_by: vec![], + created_at: None, + updated_at: None, } } diff --git a/lib/crates/fabro-tracker/src/lib.rs b/lib/crates/fabro-tracker/src/lib.rs index db38b0a90..f31846652 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 77115dcaa..6e11d5c11 100644 --- a/lib/crates/fabro-tracker/src/linear.rs +++ b/lib/crates/fabro-tracker/src/linear.rs @@ -11,7 +11,7 @@ const BLOCKS_RELATION_TYPE: &str = "blocks"; #[derive(Clone, Debug)] pub struct LinearOptions { - pub api_key: String, + pub api_key: String, pub endpoint: String, } @@ -93,9 +93,9 @@ fn normalize_issue(node: &Value) -> Result { .filter_map(|rel| { let issue = &rel["issue"]; Some(BlockerRef { - id: issue["id"].as_str()?.to_string(), + id: issue["id"].as_str()?.to_string(), identifier: issue["identifier"].as_str()?.to_string(), - state: issue["state"]["name"].as_str()?.to_string(), + state: issue["state"]["name"].as_str()?.to_string(), }) }) .collect() @@ -154,8 +154,8 @@ fn extract_issues(response: &Value) -> Result, String> { /// A `Tracker` implementation backed by Linear. pub struct LinearTracker { - config: LinearOptions, - client: reqwest::Client, + config: LinearOptions, + client: reqwest::Client, project_slug: String, } @@ -361,27 +361,27 @@ mod tests { fn mock_config(server_url: &str) -> LinearOptions { LinearOptions { - api_key: "lin_api_test123".to_string(), + api_key: "lin_api_test123".to_string(), endpoint: format!("{server_url}/graphql"), } } fn make_test_issue() -> Issue { Issue { - id: "issue-1".to_string(), + id: "issue-1".to_string(), project_item_id: None, - identifier: "T-1".to_string(), - title: "Test".to_string(), - description: None, - priority: None, - state: "Todo".to_string(), - branch_name: None, - url: "https://linear.app/t".to_string(), - assignee_id: None, - labels: vec![], - blocked_by: vec![], - created_at: None, - updated_at: None, + identifier: "T-1".to_string(), + title: "Test".to_string(), + description: None, + priority: None, + state: "Todo".to_string(), + branch_name: None, + url: "https://linear.app/t".to_string(), + assignee_id: None, + labels: vec![], + blocked_by: vec![], + created_at: None, + updated_at: None, } } diff --git a/lib/crates/fabro-types/src/checkpoint.rs b/lib/crates/fabro-types/src/checkpoint.rs index 58d5d8337..6772034c1 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 4b5e3e17e..15fec67c5 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 6375d75e6..9caa03376 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 d3d42ddfd..da7e3b4a3 100644 --- a/lib/crates/fabro-types/src/interview.rs +++ b/lib/crates/fabro-types/src/interview.rs @@ -43,17 +43,17 @@ impl fmt::Display for InterviewQuestionType { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] pub struct InterviewQuestionRecord { #[serde(default)] - pub id: String, + pub id: String, #[serde(default)] - pub text: String, + pub text: String, #[serde(default)] - pub stage: String, + pub stage: String, #[serde(default)] - pub question_type: InterviewQuestionType, + pub question_type: InterviewQuestionType, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub options: Vec, + pub options: Vec, #[serde(default)] - pub allow_freeform: bool, + pub allow_freeform: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub timeout_seconds: Option, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/lib/crates/fabro-types/src/node_status.rs b/lib/crates/fabro-types/src/node_status.rs index 89b315678..00cb112b6 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 5660f6aa2..176802209 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 c1170abdc..8eb66b86d 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 a43f99cea..8b1f02bff 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 cfa79d7e7..30650f451 100644 --- a/lib/crates/fabro-types/src/run.rs +++ b/lib/crates/fabro-types/src/run.rs @@ -27,48 +27,48 @@ pub struct RunClientProvenance { #[serde(default, skip_serializing_if = "Option::is_none")] pub user_agent: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub name: Option, + pub name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub version: Option, + pub version: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RunSubjectProvenance { #[serde(default, skip_serializing_if = "Option::is_none")] - pub login: Option, + pub login: Option, pub auth_method: RunAuthMethod, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct RunProvenance { #[serde(default, skip_serializing_if = "Option::is_none")] - pub server: Option, + pub server: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub client: Option, + pub client: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub subject: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RunRecord { - pub run_id: RunId, - pub settings: SettingsLayer, - pub graph: Graph, + pub run_id: RunId, + pub settings: SettingsLayer, + pub graph: Graph, #[serde(default, skip_serializing_if = "Option::is_none")] - pub workflow_slug: Option, + pub workflow_slug: Option, pub working_directory: PathBuf, #[serde(default, skip_serializing_if = "Option::is_none")] - pub host_repo_path: Option, + pub host_repo_path: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub repo_origin_url: Option, + pub repo_origin_url: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub base_branch: Option, + pub base_branch: Option, #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub labels: HashMap, + pub labels: HashMap, #[serde(default, skip_serializing_if = "Option::is_none")] - pub provenance: Option, + pub provenance: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub manifest_blob: Option, + pub manifest_blob: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub definition_blob: Option, + pub definition_blob: Option, } diff --git a/lib/crates/fabro-types/src/run_event/agent.rs b/lib/crates/fabro-types/src/run_event/agent.rs index 2ea2c3ba2..475dee841 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 5d46e4035..4460c0c2b 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 b12aa597f..7a3c4d302 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 1496563e4..6a5706af7 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,18 +829,15 @@ 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(), @@ -894,9 +891,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, }); @@ -1028,27 +1025,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 90fdb2fda..2b5488b4d 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 6b84868ef..311420e73 100644 --- a/lib/crates/fabro-types/src/run_event/stage.rs +++ b/lib/crates/fabro-types/src/run_event/stage.rs @@ -7,9 +7,9 @@ use crate::{BilledModelUsage, FailureDetail, Outcome, StageStatus}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct StageStartedProps { - pub index: usize, + pub index: usize, pub handler_type: String, - pub attempt: usize, + pub attempt: usize, pub max_attempts: usize, } @@ -50,39 +50,39 @@ pub struct StageCompletedProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct StageFailedProps { - pub index: usize, + pub index: usize, #[serde(default, skip_serializing_if = "Option::is_none")] - pub failure: Option, + pub failure: Option, pub will_retry: bool, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct StageRetryingProps { - pub index: usize, - pub attempt: usize, + pub index: usize, + pub attempt: usize, pub max_attempts: usize, - pub delay_ms: u64, + pub delay_ms: u64, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct StagePromptProps { - pub visit: u32, - pub text: String, + pub visit: u32, + pub text: String, #[serde(default, skip_serializing_if = "Option::is_none")] - pub mode: Option, + pub mode: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub model: Option, + pub model: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct PromptCompletedProps { pub response: String, - pub model: String, + pub model: String, pub provider: String, #[serde(default, skip_serializing_if = "Option::is_none")] - pub billing: Option, + pub billing: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/lib/crates/fabro-types/src/sandbox_record.rs b/lib/crates/fabro-types/src/sandbox_record.rs index 59741a709..787d612df 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 0a63102d6..6031c8f93 100644 --- a/lib/crates/fabro-types/src/settings/cli.rs +++ b/lib/crates/fabro-types/src/settings/cli.rs @@ -15,10 +15,10 @@ use super::run::{AgentPermissions, McpEntryLayer, McpServerSettings}; /// A structurally resolved `[cli]` view for consumers. #[derive(Debug, Clone, Default, PartialEq)] pub struct CliSettings { - pub target: Option, - pub auth: CliAuthSettings, - pub exec: CliExecSettings, - pub output: CliOutputSettings, + pub target: Option, + pub auth: CliAuthSettings, + pub exec: CliExecSettings, + pub output: CliOutputSettings, pub updates: CliUpdatesSettings, pub logging: CliLoggingSettings, } @@ -37,8 +37,8 @@ pub enum CliTargetSettings { #[derive(Debug, Clone, PartialEq)] pub struct CliTargetTlsSettings { pub cert: InterpString, - pub key: InterpString, - pub ca: InterpString, + pub key: InterpString, + pub ca: InterpString, } #[derive(Debug, Clone, Default, PartialEq)] @@ -49,25 +49,25 @@ pub struct CliAuthSettings { #[derive(Debug, Clone, Default, PartialEq)] pub struct CliExecSettings { pub prevent_idle_sleep: bool, - pub model: CliExecModelSettings, - pub agent: CliExecAgentSettings, + pub model: CliExecModelSettings, + pub agent: CliExecAgentSettings, } #[derive(Debug, Clone, Default, PartialEq)] pub struct CliExecModelSettings { pub provider: Option, - pub name: Option, + pub name: Option, } #[derive(Debug, Clone, Default, PartialEq)] pub struct CliExecAgentSettings { pub permissions: Option, - pub mcps: HashMap, + pub mcps: HashMap, } #[derive(Debug, Clone, Default, PartialEq)] pub struct CliOutputSettings { - pub format: OutputFormat, + pub format: OutputFormat, pub verbosity: OutputVerbosity, } @@ -86,13 +86,13 @@ pub struct CliLoggingSettings { #[serde(deny_unknown_fields)] pub struct CliLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub target: Option, + pub target: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub auth: Option, + pub auth: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub exec: Option, + pub exec: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub output: Option, + pub output: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub updates: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -121,9 +121,9 @@ pub struct CliTargetTlsLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub cert: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub key: Option, + pub key: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub ca: Option, + pub ca: Option, } /// `[cli.auth]` — explicit auth strategy selection. @@ -151,9 +151,9 @@ pub struct CliExecLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub prevent_idle_sleep: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub model: Option, + pub model: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub agent: Option, + pub agent: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] @@ -162,7 +162,7 @@ pub struct CliExecModelLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub name: Option, + pub name: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] @@ -172,7 +172,7 @@ pub struct CliExecAgentLayer { pub permissions: Option, /// Agent-scoped MCP entries for `fabro exec`. #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub mcps: HashMap, + pub mcps: HashMap, } /// `[cli.output]` — generic CLI output defaults. @@ -180,7 +180,7 @@ pub struct CliExecAgentLayer { #[serde(deny_unknown_fields)] pub struct CliOutputLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub format: Option, + pub format: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub verbosity: Option, } diff --git a/lib/crates/fabro-types/src/settings/duration.rs b/lib/crates/fabro-types/src/settings/duration.rs index 7ea3a7690..17d64c12b 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 f893628ea..fc5e2603c 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,12 +300,9 @@ 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] @@ -322,12 +319,9 @@ 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 6a0a2186b..81cd94c5e 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 26545599f..ee912ff51 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,23 +305,20 @@ 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(®) @@ -333,21 +330,18 @@ 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 e0c97f03e..f7164ae44 100644 --- a/lib/crates/fabro-types/src/settings/project.rs +++ b/lib/crates/fabro-types/src/settings/project.rs @@ -10,10 +10,10 @@ use serde::{Deserialize, Serialize}; /// A structurally resolved `[project]` view for consumers. #[derive(Debug, Clone, Default, PartialEq)] pub struct ProjectSettings { - pub name: Option, + pub name: Option, pub description: Option, - pub directory: String, - pub metadata: HashMap, + pub directory: String, + pub metadata: HashMap, } /// A sparse `[project]` layer as it appears in a single settings file. @@ -21,13 +21,13 @@ pub struct ProjectSettings { #[serde(deny_unknown_fields)] pub struct ProjectLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub name: Option, + pub name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, /// The Fabro-managed project directory inside the repo. Defaults to /// `.` after layering when unspecified. #[serde(default, skip_serializing_if = "Option::is_none")] - pub directory: Option, + pub directory: Option, #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub metadata: HashMap, + pub metadata: HashMap, } diff --git a/lib/crates/fabro-types/src/settings/resolved.rs b/lib/crates/fabro-types/src/settings/resolved.rs index 3382ff034..b68ef51b9 100644 --- a/lib/crates/fabro-types/src/settings/resolved.rs +++ b/lib/crates/fabro-types/src/settings/resolved.rs @@ -5,10 +5,10 @@ use super::{ /// A fully resolved settings view across all namespaces. #[derive(Debug, Clone, Default, PartialEq)] pub struct Settings { - pub project: ProjectSettings, + pub project: ProjectSettings, pub workflow: WorkflowSettings, - pub run: RunSettings, - pub cli: CliSettings, - pub server: ServerSettings, + pub run: RunSettings, + pub cli: CliSettings, + pub server: ServerSettings, pub features: FeaturesSettings, } diff --git a/lib/crates/fabro-types/src/settings/run.rs b/lib/crates/fabro-types/src/settings/run.rs index 403c19223..fafe34958 100644 --- a/lib/crates/fabro-types/src/settings/run.rs +++ b/lib/crates/fabro-types/src/settings/run.rs @@ -18,23 +18,23 @@ use super::model_ref::ModelRef; /// A structurally resolved `[run]` view for consumers. #[derive(Debug, Clone, Default, PartialEq)] pub struct RunSettings { - pub goal: Option, - pub working_dir: Option, - pub metadata: HashMap, - pub inputs: HashMap, - pub model: RunModelSettings, - pub git: RunGitSettings, - pub prepare: RunPrepareSettings, - pub execution: RunExecutionSettings, - pub checkpoint: RunCheckpointSettings, - pub sandbox: RunSandboxSettings, + pub goal: Option, + pub working_dir: Option, + pub metadata: HashMap, + pub inputs: HashMap, + pub model: RunModelSettings, + pub git: RunGitSettings, + pub prepare: RunPrepareSettings, + pub execution: RunExecutionSettings, + pub checkpoint: RunCheckpointSettings, + pub sandbox: RunSandboxSettings, pub notifications: HashMap, - pub interviews: RunInterviewsSettings, - pub agent: RunAgentSettings, - pub hooks: Vec, - pub scm: RunScmSettings, - pub pull_request: Option, - pub artifacts: ArtifactsSettings, + pub interviews: RunInterviewsSettings, + pub agent: RunAgentSettings, + pub hooks: Vec, + pub scm: RunScmSettings, + pub pull_request: Option, + pub artifacts: ArtifactsSettings, } /// The resolved source of a run goal. @@ -46,8 +46,8 @@ pub enum RunGoal { #[derive(Debug, Clone, Default, PartialEq)] pub struct RunModelSettings { - pub provider: Option, - pub name: Option, + pub provider: Option, + pub name: Option, pub fallbacks: Vec, } @@ -58,20 +58,20 @@ pub struct RunGitSettings { #[derive(Debug, Clone, Default, PartialEq)] pub struct GitAuthorSettings { - pub name: Option, + pub name: Option, pub email: Option, } #[derive(Debug, Clone, PartialEq)] pub struct RunPrepareSettings { - pub commands: Vec, + pub commands: Vec, pub timeout_ms: u64, } impl Default for RunPrepareSettings { fn default() -> Self { Self { - commands: Vec::new(), + commands: Vec::new(), timeout_ms: 300_000, } } @@ -79,17 +79,17 @@ impl Default for RunPrepareSettings { #[derive(Debug, Clone, PartialEq)] pub struct RunExecutionSettings { - pub mode: RunMode, + pub mode: RunMode, pub approval: ApprovalMode, - pub retros: bool, + pub retros: bool, } impl Default for RunExecutionSettings { fn default() -> Self { Self { - mode: RunMode::Normal, + mode: RunMode::Normal, approval: ApprovalMode::Prompt, - retros: true, + retros: true, } } } @@ -101,23 +101,23 @@ pub struct RunCheckpointSettings { #[derive(Debug, Clone, PartialEq)] pub struct RunSandboxSettings { - pub provider: String, - pub preserve: bool, + pub provider: String, + pub preserve: bool, pub devcontainer: bool, - pub env: HashMap, - pub local: LocalSandboxSettings, - pub daytona: Option, + pub env: HashMap, + pub local: LocalSandboxSettings, + pub daytona: Option, } impl Default for RunSandboxSettings { fn default() -> Self { Self { - provider: "local".to_string(), - preserve: false, + provider: "local".to_string(), + preserve: false, devcontainer: false, - env: HashMap::new(), - local: LocalSandboxSettings::default(), - daytona: None, + env: HashMap::new(), + local: LocalSandboxSettings::default(), + daytona: None, } } } @@ -130,10 +130,10 @@ pub struct LocalSandboxSettings { #[derive(Debug, Clone, Default, PartialEq)] pub struct DaytonaSettings { pub auto_stop_interval: Option, - pub labels: HashMap, - pub snapshot: Option, - pub network: Option, - pub skip_clone: bool, + pub labels: HashMap, + pub snapshot: Option, + pub network: Option, + pub skip_clone: bool, } #[derive(Debug, Clone, PartialEq)] @@ -144,21 +144,21 @@ pub enum DockerfileSource { #[derive(Debug, Clone, PartialEq)] pub struct DaytonaSnapshotSettings { - pub name: String, - pub cpu: Option, - pub memory_gb: Option, - pub disk_gb: Option, + pub name: String, + pub cpu: Option, + pub memory_gb: Option, + pub disk_gb: Option, pub dockerfile: Option, } #[derive(Debug, Clone, Default, PartialEq)] pub struct NotificationRouteSettings { - pub enabled: bool, + pub enabled: bool, pub provider: Option, - pub events: Vec, - pub slack: Option, - pub discord: Option, - pub teams: Option, + pub events: Vec, + pub slack: Option, + pub discord: Option, + pub teams: Option, } #[derive(Debug, Clone, Default, PartialEq)] @@ -169,9 +169,9 @@ pub struct NotificationProviderSettings { #[derive(Debug, Clone, Default, PartialEq)] pub struct RunInterviewsSettings { pub provider: Option, - pub slack: Option, - pub discord: Option, - pub teams: Option, + pub slack: Option, + pub discord: Option, + pub teams: Option, } #[derive(Debug, Clone, Default, PartialEq)] @@ -182,27 +182,27 @@ pub struct InterviewProviderSettings { #[derive(Debug, Clone, Default, PartialEq)] pub struct RunAgentSettings { pub permissions: Option, - pub mcps: HashMap, + pub mcps: HashMap, } #[derive(Debug, Clone, PartialEq)] pub struct McpServerSettings { - pub name: String, - pub transport: McpTransport, + pub name: String, + pub transport: McpTransport, pub startup_timeout_secs: u64, - pub tool_timeout_secs: u64, + pub tool_timeout_secs: u64, } impl Default for McpServerSettings { fn default() -> Self { Self { - name: String::new(), - transport: McpTransport::Stdio { + name: String::new(), + transport: McpTransport::Stdio { command: Vec::new(), - env: HashMap::new(), + env: HashMap::new(), }, startup_timeout_secs: 10, - tool_timeout_secs: 60, + tool_timeout_secs: 60, } } } @@ -223,16 +223,16 @@ impl McpServerSettings { pub enum McpTransport { Stdio { command: Vec, - env: HashMap, + env: HashMap, }, Http { - url: String, + url: String, headers: HashMap, }, Sandbox { command: Vec, - port: u16, - env: HashMap, + port: u16, + env: HashMap, }, } @@ -252,36 +252,36 @@ pub enum HookType { command: String, }, Http { - url: String, - headers: Option>, + url: String, + headers: Option>, #[serde(default)] allowed_env_vars: Vec, #[serde(default)] - tls: TlsMode, + tls: TlsMode, }, Prompt { prompt: String, - model: Option, + model: Option, }, Agent { - prompt: String, - model: Option, + prompt: String, + model: Option, max_tool_rounds: Option, }, } #[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] pub struct HookDefinition { - pub name: Option, - pub event: HookEvent, + pub name: Option, + pub event: HookEvent, #[serde(default)] - pub command: Option, + pub command: Option, #[serde(flatten)] - pub hook_type: Option, - pub matcher: Option, - pub blocking: Option, + pub hook_type: Option, + pub matcher: Option, + pub blocking: Option, pub timeout_ms: Option, - pub sandbox: Option, + pub sandbox: Option, } impl HookDefinition { @@ -350,10 +350,10 @@ impl HookDefinition { #[derive(Debug, Clone, Default, PartialEq)] pub struct RunScmSettings { - pub provider: Option, - pub owner: Option, + pub provider: Option, + pub owner: Option, pub repository: Option, - pub github: Option, + pub github: Option, } #[derive(Debug, Clone, Default, PartialEq)] @@ -361,18 +361,18 @@ pub struct ScmGitHubSettings; #[derive(Debug, Clone, PartialEq)] pub struct PullRequestSettings { - pub enabled: bool, - pub draft: bool, - pub auto_merge: bool, + pub enabled: bool, + pub draft: bool, + pub auto_merge: bool, pub merge_strategy: MergeStrategy, } impl Default for PullRequestSettings { fn default() -> Self { Self { - enabled: false, - draft: true, - auto_merge: false, + enabled: false, + draft: true, + auto_merge: false, merge_strategy: MergeStrategy::Squash, } } @@ -388,41 +388,41 @@ pub struct ArtifactsSettings { #[serde(deny_unknown_fields)] pub struct RunLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub goal: Option, + pub goal: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub working_dir: Option, + pub working_dir: Option, /// Flat string-to-string map. Replaces wholesale across layers. #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub metadata: HashMap, + pub metadata: HashMap, /// Run inputs: typed scalar values. Replaces wholesale across layers. #[serde(default, skip_serializing_if = "Option::is_none")] - pub inputs: Option>, + pub inputs: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] - pub model: Option, + pub model: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub git: Option, + pub git: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub prepare: Option, + pub prepare: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub execution: Option, + pub execution: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub checkpoint: Option, + pub checkpoint: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub sandbox: Option, + pub sandbox: Option, #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub notifications: HashMap, #[serde(default, skip_serializing_if = "Option::is_none")] - pub interviews: Option, + pub interviews: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub agent: Option, + pub agent: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub hooks: Vec, + pub hooks: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] - pub scm: Option, + pub scm: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub pull_request: Option, + pub pull_request: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub artifacts: Option, + pub artifacts: Option, } /// The source of a run's goal, either inline literal text or a reference to @@ -460,7 +460,7 @@ pub enum RunGoalLayer { /// goals without having to re-walk the layer. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResolvedRunGoal { - pub text: String, + pub text: String, pub source: ResolvedGoalSource, } @@ -479,9 +479,9 @@ pub enum ResolvedGoalSource { #[serde(deny_unknown_fields)] pub struct RunModelLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub provider: Option, + pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub name: Option, + pub name: Option, /// Ordered list of fallback model references. Supports `...` splice marker /// at layering time — see [`super::splice_array`]. #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -528,7 +528,7 @@ pub struct RunGitLayer { #[serde(deny_unknown_fields)] pub struct GitAuthorLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub name: Option, + pub name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub email: Option, } @@ -539,7 +539,7 @@ pub struct GitAuthorLayer { #[serde(deny_unknown_fields)] pub struct RunPrepareLayer { #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub steps: Vec, + pub steps: Vec, /// Optional timeout applied to each prepare step. #[serde(default, skip_serializing_if = "Option::is_none")] pub timeout: Option, @@ -550,11 +550,11 @@ pub struct RunPrepareLayer { #[serde(deny_unknown_fields)] pub struct PrepareStep { #[serde(default, skip_serializing_if = "Option::is_none")] - pub script: Option, + pub script: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub command: Option>, #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub env: HashMap, + pub env: HashMap, } /// `[run.execution]` — run posture knobs. @@ -562,12 +562,12 @@ pub struct PrepareStep { #[serde(deny_unknown_fields)] pub struct RunExecutionLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub mode: Option, + pub mode: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub approval: Option, /// Positive-form: `true` runs retros, `false` skips them. #[serde(default, skip_serializing_if = "Option::is_none")] - pub retros: Option, + pub retros: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -597,18 +597,18 @@ pub struct RunCheckpointLayer { #[serde(deny_unknown_fields)] pub struct RunSandboxLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub provider: Option, + pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub preserve: Option, + pub preserve: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub devcontainer: Option, /// Sticky merge-by-key across layers. #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub env: HashMap, + pub env: HashMap, #[serde(default, skip_serializing_if = "Option::is_none")] - pub local: Option, + pub local: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub daytona: Option, + pub daytona: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] @@ -635,26 +635,26 @@ pub struct DaytonaSandboxLayer { pub auto_stop_interval: Option, /// Sticky merge-by-key (provider-native labels). #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub labels: HashMap, + pub labels: HashMap, #[serde(default, skip_serializing_if = "Option::is_none")] - pub snapshot: Option, + pub snapshot: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub network: Option, + pub network: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub skip_clone: Option, + pub skip_clone: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct DaytonaSnapshotLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub name: Option, + pub name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub cpu: Option, + pub cpu: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub memory: Option, + pub memory: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub disk: Option, + pub disk: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub dockerfile: Option, } @@ -679,19 +679,19 @@ pub enum DaytonaNetworkLayer { #[serde(deny_unknown_fields)] pub struct NotificationRouteLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, + pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, /// Raw Fabro event names. Splice marker supported at layering time. #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub events: Vec, + pub events: Vec, /// Provider-specific destination subtables. First-pass chat providers. #[serde(default, skip_serializing_if = "Option::is_none")] - pub slack: Option, + pub slack: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub discord: Option, + pub discord: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub teams: Option, + pub teams: Option, } /// A single string array entry that may be the splice marker. @@ -736,11 +736,11 @@ pub struct InterviewsLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub slack: Option, + pub slack: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub discord: Option, + pub discord: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub teams: Option, + pub teams: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] @@ -758,7 +758,7 @@ pub struct RunAgentLayer { pub permissions: Option, /// Agent-scoped MCP server entries, keyed by name. #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub mcps: HashMap, + pub mcps: HashMap, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -777,43 +777,43 @@ pub enum AgentPermissions { pub enum McpEntryLayer { Http { #[serde(default)] - enabled: Option, - url: InterpString, + enabled: Option, + url: InterpString, #[serde(default)] - headers: HashMap, + headers: HashMap, #[serde(default)] startup_timeout: Option, #[serde(default)] - tool_timeout: Option, + tool_timeout: Option, }, Stdio { #[serde(default)] - enabled: Option, + enabled: Option, #[serde(default)] - script: Option, + script: Option, #[serde(default)] - command: Option>, + command: Option>, #[serde(default)] - env: HashMap, + env: HashMap, #[serde(default)] startup_timeout: Option, #[serde(default)] - tool_timeout: Option, + tool_timeout: Option, }, Sandbox { #[serde(default)] - enabled: Option, + enabled: Option, #[serde(default)] - script: Option, + script: Option, #[serde(default)] - command: Option>, - port: u16, + command: Option>, + port: u16, #[serde(default)] - env: HashMap, + env: HashMap, #[serde(default)] startup_timeout: Option, #[serde(default)] - tool_timeout: Option, + tool_timeout: Option, }, } @@ -825,40 +825,40 @@ pub enum McpEntryLayer { pub struct HookEntry { /// Optional merge identity. Hooks with the same `id` replace in place. #[serde(default, skip_serializing_if = "Option::is_none")] - pub id: Option, + pub id: Option, /// Display-only human name. #[serde(default, skip_serializing_if = "Option::is_none")] - pub name: Option, - pub event: HookEvent, + pub name: Option, + pub event: HookEvent, #[serde(default, skip_serializing_if = "Option::is_none")] - pub matcher: Option, + pub matcher: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub blocking: Option, + pub blocking: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub timeout: Option, + pub timeout: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub sandbox: Option, + pub sandbox: Option, // Exactly one of the following groups is expected: #[serde(default, skip_serializing_if = "Option::is_none")] - pub script: Option, + pub script: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub command: Option>, + pub command: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] - pub url: Option, + pub url: Option, #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub headers: HashMap, + pub headers: HashMap, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub allowed_env_vars: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] - pub tls: Option, + pub tls: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub prompt: Option, + pub prompt: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub model: Option, + pub model: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_tool_rounds: Option, + pub max_tool_rounds: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub agent: Option, + pub agent: Option, } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] @@ -906,14 +906,14 @@ pub enum HookEvent { #[serde(deny_unknown_fields)] pub struct RunScmLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub provider: Option, + pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub owner: Option, + pub owner: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub repository: Option, /// Provider-specific SCM leaves. First-pass providers. #[serde(default, skip_serializing_if = "Option::is_none")] - pub github: Option, + pub github: Option, } /// `[run.scm.github]` — GitHub-specific SCM leaf. Intentionally minimal in @@ -928,11 +928,11 @@ pub struct ScmGitHubLayer; #[serde(deny_unknown_fields)] pub struct RunPullRequestLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, + pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub draft: Option, + pub draft: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub auto_merge: Option, + pub auto_merge: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub merge_strategy: Option, } diff --git a/lib/crates/fabro-types/src/settings/server.rs b/lib/crates/fabro-types/src/settings/server.rs index b061e2330..716e93403 100644 --- a/lib/crates/fabro-types/src/settings/server.rs +++ b/lib/crates/fabro-types/src/settings/server.rs @@ -17,15 +17,15 @@ use super::interp::InterpString; /// A structurally resolved `[server]` view for consumers. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ServerSettings { - pub listen: ServerListenSettings, - pub api: ServerApiSettings, - pub web: ServerWebSettings, - pub auth: ServerAuthSettings, - pub storage: ServerStorageSettings, - pub artifacts: ServerArtifactsSettings, - pub slatedb: ServerSlateDbSettings, - pub scheduler: ServerSchedulerSettings, - pub logging: ServerLoggingSettings, + pub listen: ServerListenSettings, + pub api: ServerApiSettings, + pub web: ServerWebSettings, + pub auth: ServerAuthSettings, + pub storage: ServerStorageSettings, + pub artifacts: ServerArtifactsSettings, + pub slatedb: ServerSlateDbSettings, + pub scheduler: ServerSchedulerSettings, + pub logging: ServerLoggingSettings, pub integrations: ServerIntegrationsSettings, } @@ -33,7 +33,7 @@ pub struct ServerSettings { pub enum ServerListenSettings { Tcp { address: SocketAddr, - tls: Option, + tls: Option, }, Unix { path: InterpString, @@ -51,16 +51,16 @@ impl Default for ServerListenSettings { #[derive(Debug, Clone, PartialEq, Eq)] pub struct TlsConfig { pub cert: InterpString, - pub key: InterpString, - pub ca: InterpString, + pub key: InterpString, + pub ca: InterpString, } impl Default for TlsConfig { fn default() -> Self { Self { cert: InterpString::parse(""), - key: InterpString::parse(""), - ca: InterpString::parse(""), + key: InterpString::parse(""), + ca: InterpString::parse(""), } } } @@ -73,14 +73,14 @@ pub struct ServerApiSettings { #[derive(Debug, Clone, PartialEq, Eq)] pub struct ServerWebSettings { pub enabled: bool, - pub url: InterpString, + pub url: InterpString, } impl Default for ServerWebSettings { fn default() -> Self { Self { enabled: false, - url: InterpString::parse(""), + url: InterpString::parse(""), } } } @@ -93,27 +93,27 @@ pub struct ServerAuthSettings { #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ServerAuthApiSettings { - pub jwt: Option, + pub jwt: Option, pub mtls: Option, } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ServerAuthApiJwtSettings { - pub enabled: bool, - pub issuer: Option, + pub enabled: bool, + pub issuer: Option, pub audience: Option, } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ServerAuthApiMtlsSettings { pub enabled: bool, - pub ca: Option, + pub ca: Option, } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ServerAuthWebSettings { pub allowed_usernames: Vec, - pub providers: ServerAuthWebProvidersSettings, + pub providers: ServerAuthWebProvidersSettings, } #[derive(Debug, Clone, Default, PartialEq, Eq)] @@ -123,8 +123,8 @@ pub struct ServerAuthWebProvidersSettings { #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct GithubOauthSettings { - pub enabled: bool, - pub client_id: Option, + pub enabled: bool, + pub client_id: Option, pub client_secret: Option, } @@ -144,30 +144,30 @@ impl Default for ServerStorageSettings { #[derive(Debug, Clone, PartialEq, Eq)] pub struct ServerArtifactsSettings { pub prefix: InterpString, - pub store: ObjectStoreSettings, + pub store: ObjectStoreSettings, } impl Default for ServerArtifactsSettings { fn default() -> Self { Self { prefix: InterpString::parse(""), - store: ObjectStoreSettings::default(), + store: ObjectStoreSettings::default(), } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ServerSlateDbSettings { - pub prefix: InterpString, - pub store: ObjectStoreSettings, + pub prefix: InterpString, + pub store: ObjectStoreSettings, pub flush_interval: StdDuration, } impl Default for ServerSlateDbSettings { fn default() -> Self { Self { - prefix: InterpString::parse(""), - store: ObjectStoreSettings::default(), + prefix: InterpString::parse(""), + store: ObjectStoreSettings::default(), flush_interval: StdDuration::ZERO, } } @@ -179,9 +179,9 @@ pub enum ObjectStoreSettings { root: InterpString, }, S3 { - bucket: InterpString, - region: InterpString, - endpoint: Option, + bucket: InterpString, + region: InterpString, + endpoint: Option, path_style: bool, }, } @@ -206,25 +206,25 @@ pub struct ServerLoggingSettings { #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ServerIntegrationsSettings { - pub github: GithubIntegrationSettings, - pub slack: SlackIntegrationSettings, + pub github: GithubIntegrationSettings, + pub slack: SlackIntegrationSettings, pub discord: DiscordIntegrationSettings, - pub teams: TeamsIntegrationSettings, + pub teams: TeamsIntegrationSettings, } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct GithubIntegrationSettings { - pub enabled: bool, - pub app_id: Option, - pub client_id: Option, - pub slug: Option, + pub enabled: bool, + pub app_id: Option, + pub client_id: Option, + pub slug: Option, pub permissions: HashMap, - pub webhooks: Option, + pub webhooks: Option, } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct SlackIntegrationSettings { - pub enabled: bool, + pub enabled: bool, pub default_channel: Option, } @@ -248,23 +248,23 @@ pub struct IntegrationWebhooksSettings { #[serde(deny_unknown_fields)] pub struct ServerLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub listen: Option, + pub listen: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub api: Option, + pub api: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub web: Option, + pub web: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub auth: Option, + pub auth: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub storage: Option, + pub storage: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub artifacts: Option, + pub artifacts: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub slatedb: Option, + pub slatedb: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub scheduler: Option, + pub scheduler: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub logging: Option, + pub logging: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub integrations: Option, } @@ -278,7 +278,7 @@ pub enum ServerListenLayer { #[serde(default)] address: Option, #[serde(default)] - tls: Option, + tls: Option, }, Unix { #[serde(default)] @@ -292,9 +292,9 @@ pub struct ServerListenTlsLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub cert: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub key: Option, + pub key: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub ca: Option, + pub ca: Option, } /// `[server.api]` — API surface settings. @@ -314,7 +314,7 @@ pub struct ServerWebLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub url: Option, + pub url: Option, } /// `[server.auth]` — cohesive server auth surface. @@ -338,7 +338,7 @@ pub struct ServerAuthLayer { #[serde(deny_unknown_fields)] pub struct ServerAuthApiLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub jwt: Option, + pub jwt: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub mtls: Option, } @@ -348,9 +348,9 @@ pub struct ServerAuthApiLayer { #[serde(deny_unknown_fields)] pub struct ServerAuthApiJwtLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, + pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub issuer: Option, + pub issuer: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub audience: Option, } @@ -362,7 +362,7 @@ pub struct ServerAuthApiMtlsLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub ca: Option, + pub ca: Option, } /// `[server.auth.web]` — provider-neutral access rules plus keyed providers. @@ -372,7 +372,7 @@ pub struct ServerAuthWebLayer { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub allowed_usernames: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] - pub providers: Option, + pub providers: Option, } /// `[server.auth.web.providers.]` — web auth providers keyed by @@ -389,9 +389,9 @@ pub struct ServerAuthWebProvidersLayer { #[serde(deny_unknown_fields)] pub struct ServerAuthWebGithubLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, + pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub client_id: Option, + pub client_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub client_secret: Option, } @@ -411,11 +411,11 @@ pub struct ServerArtifactsLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub prefix: Option, + pub prefix: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub local: Option, + pub local: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub s3: Option, + pub s3: Option, } /// `[server.slatedb]` — SlateDB bottomless storage plus tunables. @@ -423,15 +423,15 @@ pub struct ServerArtifactsLayer { #[serde(deny_unknown_fields)] pub struct ServerSlateDbLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub provider: Option, + pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub prefix: Option, + pub prefix: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub flush_interval: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub local: Option, + pub local: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub s3: Option, + pub s3: Option, } /// Closed enum of object-store providers. Unknown providers hard-fail @@ -456,11 +456,11 @@ pub struct ObjectStoreLocalLayer { #[serde(deny_unknown_fields)] pub struct ObjectStoreS3Layer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub bucket: Option, + pub bucket: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub region: Option, + pub region: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub endpoint: Option, + pub endpoint: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub path_style: Option, } @@ -489,13 +489,13 @@ pub struct ServerLoggingLayer { #[serde(deny_unknown_fields)] pub struct ServerIntegrationsLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub github: Option, + pub github: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub slack: Option, + pub slack: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub discord: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub teams: Option, + pub teams: Option, } /// `[server.integrations.github]` — GitHub App, credentials, and inbound @@ -504,17 +504,17 @@ pub struct ServerIntegrationsLayer { #[serde(deny_unknown_fields)] pub struct GithubIntegrationLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, + pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub app_id: Option, + pub app_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub client_id: Option, + pub client_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub slug: Option, + pub slug: Option, #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub permissions: HashMap, #[serde(default, skip_serializing_if = "Option::is_none")] - pub webhooks: Option, + pub webhooks: Option, } /// `[server.integrations.slack]` — Slack workspace credentials and defaults. @@ -522,7 +522,7 @@ pub struct GithubIntegrationLayer { #[serde(deny_unknown_fields)] pub struct SlackIntegrationLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, + pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub default_channel: Option, } diff --git a/lib/crates/fabro-types/src/settings/size.rs b/lib/crates/fabro-types/src/settings/size.rs index c4fffb839..26803ffe9 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 42f8d38c7..7dc0d0b27 100644 --- a/lib/crates/fabro-types/src/settings/workflow.rs +++ b/lib/crates/fabro-types/src/settings/workflow.rs @@ -10,10 +10,10 @@ use serde::{Deserialize, Serialize}; /// A structurally resolved `[workflow]` view for consumers. #[derive(Debug, Clone, Default, PartialEq)] pub struct WorkflowSettings { - pub name: Option, + pub name: Option, pub description: Option, - pub graph: String, - pub metadata: HashMap, + pub graph: String, + pub metadata: HashMap, } /// A sparse `[workflow]` layer as it appears in a single settings file. @@ -21,12 +21,12 @@ pub struct WorkflowSettings { #[serde(deny_unknown_fields)] pub struct WorkflowLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub name: Option, + pub name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, /// Optional override for the default `workflow.fabro` graph path. #[serde(default, skip_serializing_if = "Option::is_none")] - pub graph: Option, + pub graph: Option, #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub metadata: HashMap, + pub metadata: HashMap, } diff --git a/lib/crates/fabro-types/src/stage_id.rs b/lib/crates/fabro-types/src/stage_id.rs index eef135774..baae58864 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,14 +190,11 @@ 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 6a78addd6..82c200575 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 7b6f9189e..411496be9 100644 --- a/lib/crates/fabro-types/src/status.rs +++ b/lib/crates/fabro-types/src/status.rs @@ -109,7 +109,7 @@ impl std::error::Error for ParseRunStatusError {} #[derive(Debug, Clone, PartialEq)] pub struct InvalidTransition { pub from: RunStatus, - pub to: RunStatus, + pub to: RunStatus, } impl fmt::Display for InvalidTransition { @@ -146,9 +146,9 @@ pub enum RunControlAction { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RunStatusRecord { - pub status: RunStatus, + pub status: RunStatus, #[serde(default, skip_serializing_if = "Option::is_none")] - pub reason: Option, + pub reason: Option, pub updated_at: DateTime, } diff --git a/lib/crates/fabro-types/tests/run_event_serde.rs b/lib/crates/fabro-types/tests/run_event_serde.rs index 8a2e71b64..1ccb99cb1 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 bf35b4c2a..9212dc640 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 8e7bb6470..2c007b06a 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 7784dc6ed..35a47a2da 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 b68d9922f..4f07871b6 100644 --- a/lib/crates/fabro-util/src/check_report.rs +++ b/lib/crates/fabro-util/src/check_report.rs @@ -30,22 +30,22 @@ impl CheckDetail { #[derive(Debug, Clone, Serialize)] pub struct CheckResult { - pub name: String, - pub status: CheckStatus, - pub summary: String, - pub details: Vec, + pub name: String, + pub status: CheckStatus, + pub summary: String, + pub details: Vec, pub remediation: Option, } #[derive(Debug, Clone, Serialize)] pub struct CheckSection { - pub title: String, + pub title: String, pub checks: Vec, } #[derive(Debug, Clone, Serialize)] pub struct CheckReport { - pub title: String, + pub title: String, pub sections: Vec, } @@ -195,37 +195,37 @@ mod tests { fn pass_check(name: &str) -> CheckResult { CheckResult { - name: name.to_string(), - status: CheckStatus::Pass, - summary: "all good".to_string(), - details: vec![CheckDetail::new("everything is fine".to_string())], + name: name.to_string(), + status: CheckStatus::Pass, + summary: "all good".to_string(), + details: vec![CheckDetail::new("everything is fine".to_string())], remediation: None, } } fn warning_check(name: &str) -> CheckResult { CheckResult { - name: name.to_string(), - status: CheckStatus::Warning, - summary: "not configured".to_string(), - details: vec![CheckDetail::new("missing something".to_string())], + name: name.to_string(), + status: CheckStatus::Warning, + summary: "not configured".to_string(), + details: vec![CheckDetail::new("missing something".to_string())], remediation: Some("fix it".to_string()), } } fn error_check(name: &str) -> CheckResult { CheckResult { - name: name.to_string(), - status: CheckStatus::Error, - summary: "broken".to_string(), - details: vec![CheckDetail::new("something is wrong".to_string())], + name: name.to_string(), + status: CheckStatus::Error, + summary: "broken".to_string(), + details: vec![CheckDetail::new("something is wrong".to_string())], remediation: Some("repair it".to_string()), } } fn report(checks: Vec) -> CheckReport { CheckReport { - title: "Test Report".into(), + title: "Test Report".into(), sections: vec![CheckSection { title: String::new(), checks, @@ -410,9 +410,9 @@ mod tests { #[test] fn render_uses_custom_title() { let r = CheckReport { - title: "My Custom Title".into(), + title: "My Custom Title".into(), sections: vec![CheckSection { - title: String::new(), + title: String::new(), checks: vec![pass_check("Test")], }], }; @@ -431,10 +431,10 @@ mod tests { #[test] fn render_truncates_long_detail_lines() { let r = report(vec![CheckResult { - name: "Test".into(), - status: CheckStatus::Pass, - summary: "ok".into(), - details: vec![CheckDetail::new( + name: "Test".into(), + status: CheckStatus::Pass, + summary: "ok".into(), + details: vec![CheckDetail::new( "This is a very long detail line for test".into(), )], remediation: None, @@ -454,10 +454,10 @@ mod tests { #[test] fn render_no_truncation_when_fits() { let r = report(vec![CheckResult { - name: "Test".into(), - status: CheckStatus::Pass, - summary: "ok".into(), - details: vec![CheckDetail::new("short".into())], + name: "Test".into(), + status: CheckStatus::Pass, + summary: "ok".into(), + details: vec![CheckDetail::new("short".into())], remediation: None, }]); let out = r.render(&Styles::new(false), true, None, Some(80)); @@ -470,10 +470,10 @@ mod tests { #[test] fn render_warn_detail_uses_red() { let r = report(vec![CheckResult { - name: "Repo".into(), - status: CheckStatus::Pass, - summary: "ok".into(), - details: vec![CheckDetail { + name: "Repo".into(), + status: CheckStatus::Pass, + summary: "ok".into(), + details: vec![CheckDetail { text: "Git clean: false".into(), warn: true, }], diff --git a/lib/crates/fabro-util/src/redact/gitleaks.rs b/lib/crates/fabro-util/src/redact/gitleaks.rs index e38b3666a..859750ead 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 25127fbd0..ba472c917 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 05211a64f..75ec04a73 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 6d8a2d0b5..b65c9bfd9 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 3438d842d..733a3a605 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 20ecd7f8d..5be714324 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,12 +628,15 @@ 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()), }); } } @@ -700,12 +703,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(", "))), }] } } @@ -729,15 +732,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() @@ -1049,23 +1052,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(), ), @@ -1145,15 +1148,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()), }); } } @@ -1165,15 +1168,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()), }); } } @@ -1181,12 +1184,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()), }); } @@ -1211,12 +1214,12 @@ impl LintRule for SelectionValidRule { if let Some(sel) = node.attrs.get("selection").and_then(AttrValue::as_str) { if !VALID_SELECTIONS.contains(&sel) { diagnostics.push(Diagnostic { - rule: self.name().to_string(), + rule: self.name().to_string(), severity: Severity::Warning, - message: format!("Node '{}' has invalid selection mode '{sel}'", node.id), - node_id: Some(node.id.clone()), - edge: None, - fix: Some(format!("Use one of: {}", VALID_SELECTIONS.join(", "))), + message: format!("Node '{}' has invalid selection mode '{sel}'", node.id), + node_id: Some(node.id.clone()), + edge: None, + fix: Some(format!("Use one of: {}", VALID_SELECTIONS.join(", "))), }); } } diff --git a/lib/crates/fabro-workflow/src/artifact.rs b/lib/crates/fabro-workflow/src/artifact.rs index 3827368e7..6e7f851f1 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 3bc4f6413..1a6e69bba 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 d99c09b29..b206e06de 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 3c870c85f..3adbf8da1 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,10 +1700,10 @@ mod tests { // 3. Outcome → StageFailed event let failure = outcome.failure.clone().unwrap(); let event = Event::StageFailed { - node_id: "code".into(), - name: "code".into(), - index: 0, - failure: failure.clone(), + node_id: "code".into(), + name: "code".into(), + index: 0, + failure: failure.clone(), will_retry: false, }; @@ -1769,7 +1769,7 @@ mod tests { use fabro_agent::Error as AgentError; let err = AgentError::Llm(SdkError::Provider { - kind: ProviderErrorKind::RateLimit, + kind: ProviderErrorKind::RateLimit, detail: Box::new(ProviderErrorDetail::new("too fast", "openai")), }); let json = serde_json::to_string(&err).unwrap(); diff --git a/lib/crates/fabro-workflow/src/event.rs b/lib/crates/fabro-workflow/src/event.rs index eea7bc11c..fd67d0018 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,60 +181,60 @@ pub enum Event { max_attempts: usize, }, StageFailed { - node_id: String, - name: String, - index: usize, - failure: FailureDetail, + node_id: String, + name: String, + index: usize, + failure: FailureDetail, will_retry: bool, }, StageRetrying { - node_id: String, - name: String, - index: usize, - attempt: usize, + node_id: String, + name: String, + index: usize, + attempt: usize, max_attempts: usize, - delay_ms: u64, + delay_ms: u64, }, ParallelStarted { - node_id: String, - visit: u32, + node_id: String, + visit: u32, branch_count: usize, - join_policy: String, + join_policy: String, }, ParallelBranchStarted { - parallel_group_id: StageId, + parallel_group_id: StageId, parallel_branch_id: ParallelBranchId, - branch: String, - index: usize, + branch: String, + index: usize, }, ParallelBranchCompleted { - parallel_group_id: StageId, + parallel_group_id: StageId, parallel_branch_id: ParallelBranchId, - branch: String, - index: usize, - duration_ms: u64, - status: String, + branch: String, + index: usize, + duration_ms: u64, + status: String, #[serde(default, skip_serializing_if = "Option::is_none")] - head_sha: Option, + head_sha: Option, }, ParallelCompleted { - node_id: String, - visit: u32, - duration_ms: u64, + node_id: String, + visit: u32, + duration_ms: u64, success_count: usize, failure_count: usize, #[serde(default, skip_serializing_if = "Vec::is_empty")] - results: Vec, + results: Vec, }, InterviewStarted { - question_id: String, - question: String, - stage: String, - question_type: String, + question_id: String, + question: String, + stage: String, + question_type: String, #[serde(default, skip_serializing_if = "Vec::is_empty")] - options: Vec, + options: Vec, #[serde(default)] - allow_freeform: bool, + allow_freeform: bool, #[serde(default, skip_serializing_if = "Option::is_none")] timeout_seconds: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -242,21 +242,21 @@ pub enum Event { }, InterviewCompleted { question_id: String, - question: String, - answer: String, + question: String, + answer: String, duration_ms: u64, }, InterviewTimeout { question_id: String, - question: String, - stage: String, + question: String, + stage: String, duration_ms: u64, }, InterviewInterrupted { question_id: String, - question: String, - stage: String, - reason: String, + question: String, + stage: String, + reason: String, duration_ms: u64, }, CheckpointCompleted { @@ -286,96 +286,96 @@ pub enum Event { }, CheckpointFailed { node_id: String, - error: String, + error: String, }, GitCommit { #[serde(default, skip_serializing_if = "Option::is_none")] node_id: Option, - sha: String, + sha: String, }, GitPush { - branch: String, + branch: String, success: bool, }, GitBranch { branch: String, - sha: String, + sha: String, }, GitWorktreeAdd { - path: String, + path: String, branch: String, }, GitWorktreeRemove { path: String, }, GitFetch { - branch: String, + branch: String, success: bool, }, GitReset { sha: String, }, EdgeSelected { - from_node: String, - to_node: String, - label: Option, - condition: Option, + from_node: String, + to_node: String, + label: Option, + condition: Option, /// Which selection step chose this edge (e.g. "condition", /// "preferred_label", "jump"). - reason: String, + reason: String, /// The stage's preferred label hint, if any. #[serde(default, skip_serializing_if = "Option::is_none")] - preferred_label: Option, + preferred_label: Option, /// The stage's suggested next node IDs, if any. #[serde(default, skip_serializing_if = "Vec::is_empty")] suggested_next_ids: Vec, /// The stage outcome status that influenced routing. - stage_status: String, + stage_status: String, /// Whether this was a direct jump (bypassing normal edge selection). - is_jump: bool, + is_jump: bool, }, LoopRestart { from_node: String, - to_node: String, + to_node: String, }, Prompt { - stage: String, - visit: u32, - text: String, + stage: String, + visit: u32, + text: String, #[serde(default, skip_serializing_if = "Option::is_none")] - mode: Option, + mode: Option, #[serde(default, skip_serializing_if = "Option::is_none")] provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - model: Option, + model: Option, }, PromptCompleted { - node_id: String, + node_id: String, response: String, - model: String, + model: String, provider: String, #[serde(default, skip_serializing_if = "Option::is_none")] - billing: Option, + billing: Option, }, /// Forwarded from an agent session, tagged with the workflow stage. Agent { - stage: String, - visit: u32, - event: AgentEvent, + stage: String, + visit: u32, + event: AgentEvent, #[serde(default, skip_serializing_if = "Option::is_none")] - session_id: Option, + session_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] parent_session_id: Option, }, SubgraphStarted { - node_id: String, + node_id: String, start_node: String, }, SubgraphCompleted { - node_id: String, + node_id: String, steps_executed: usize, - status: String, - duration_ms: u64, + status: String, + duration_ms: u64, }, /// Forwarded from a sandbox lifecycle operation. Sandbox { @@ -383,174 +383,174 @@ pub enum Event { }, /// Emitted after the sandbox has been initialized (by engine lifecycle). SandboxInitialized { - working_directory: String, - provider: String, + working_directory: String, + provider: String, #[serde(default, skip_serializing_if = "Option::is_none")] - identifier: Option, + identifier: Option, #[serde(default, skip_serializing_if = "Option::is_none")] host_working_directory: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - container_mount_point: Option, + container_mount_point: Option, }, SetupStarted { command_count: usize, }, SetupCommandStarted { command: String, - index: usize, + index: usize, }, SetupCommandCompleted { - command: String, - index: usize, - exit_code: i32, + command: String, + index: usize, + exit_code: i32, duration_ms: u64, }, SetupCompleted { duration_ms: u64, }, SetupFailed { - command: String, - index: usize, + command: String, + index: usize, exit_code: i32, - stderr: String, + stderr: String, }, StallWatchdogTimeout { - node: String, + node: String, idle_seconds: u64, }, ArtifactCaptured { - node_id: String, - attempt: u32, - node_slug: String, - path: String, - mime: String, - content_md5: String, + node_id: String, + attempt: u32, + node_slug: String, + path: String, + mime: String, + content_md5: String, content_sha256: String, - bytes: u64, + bytes: u64, }, SshAccessReady { ssh_command: String, }, Failover { - stage: String, + stage: String, from_provider: String, - from_model: String, - to_provider: String, - to_model: String, - error: String, + from_model: String, + to_provider: String, + to_model: String, + error: String, }, CliEnsureStarted { cli_name: String, provider: String, }, CliEnsureCompleted { - cli_name: String, - provider: String, + cli_name: String, + provider: String, already_installed: bool, - node_installed: bool, - duration_ms: u64, + node_installed: bool, + duration_ms: u64, }, CliEnsureFailed { - cli_name: String, - provider: String, - error: String, + cli_name: String, + provider: String, + error: String, duration_ms: u64, }, CommandStarted { - node_id: String, - script: String, - command: String, - language: String, + node_id: String, + script: String, + command: String, + language: String, #[serde(default, skip_serializing_if = "Option::is_none")] timeout_ms: Option, }, CommandCompleted { - node_id: String, - stdout: String, - stderr: String, + node_id: String, + stdout: String, + stderr: String, #[serde(default, skip_serializing_if = "Option::is_none")] - exit_code: Option, + exit_code: Option, duration_ms: u64, - timed_out: bool, + timed_out: bool, }, AgentCliStarted { - node_id: String, - visit: u32, - mode: String, + node_id: String, + visit: u32, + mode: String, provider: String, - model: String, - command: String, + model: String, + command: String, }, AgentCliCompleted { - node_id: String, - stdout: String, - stderr: String, - exit_code: i32, + node_id: String, + stdout: String, + stderr: String, + exit_code: i32, duration_ms: u64, }, PullRequestCreated { - pr_url: String, - pr_number: u64, - owner: String, - repo: String, + pr_url: String, + pr_number: u64, + owner: String, + repo: String, base_branch: String, head_branch: String, - title: String, - draft: bool, + title: String, + draft: bool, }, PullRequestFailed { error: String, }, DevcontainerResolved { - dockerfile_lines: usize, - environment_count: usize, + dockerfile_lines: usize, + environment_count: usize, lifecycle_command_count: usize, - workspace_folder: String, + workspace_folder: String, }, DevcontainerLifecycleStarted { - phase: String, + phase: String, command_count: usize, }, DevcontainerLifecycleCommandStarted { - phase: String, + phase: String, command: String, - index: usize, + index: usize, }, DevcontainerLifecycleCommandCompleted { - phase: String, - command: String, - index: usize, - exit_code: i32, + phase: String, + command: String, + index: usize, + exit_code: i32, duration_ms: u64, }, DevcontainerLifecycleCompleted { - phase: String, + phase: String, duration_ms: u64, }, DevcontainerLifecycleFailed { - phase: String, - command: String, - index: usize, + phase: String, + command: String, + index: usize, exit_code: i32, - stderr: String, + stderr: String, }, RetroStarted { #[serde(default, skip_serializing_if = "Option::is_none")] - prompt: Option, + prompt: Option, #[serde(default, skip_serializing_if = "Option::is_none")] provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - model: Option, + model: Option, }, RetroCompleted { duration_ms: u64, #[serde(default, skip_serializing_if = "Option::is_none")] - response: Option, + response: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - retro: Option, + retro: Option, }, RetroFailed { - error: String, + error: String, duration_ms: u64, }, } @@ -1267,15 +1267,15 @@ pub fn event_name(event: &Event) -> &'static str { #[derive(Debug, Default)] struct StoredEventFields { - session_id: Option, - parent_session_id: Option, - node_id: Option, - node_label: Option, - stage_id: Option, - parallel_group_id: Option, + session_id: Option, + parent_session_id: Option, + node_id: Option, + node_label: Option, + stage_id: Option, + parallel_group_id: Option, parallel_branch_id: Option, - tool_call_id: Option, - actor: Option, + tool_call_id: Option, + actor: Option, } fn default_node_label(node_id: Option<&String>, node_label: Option) -> Option { @@ -1293,13 +1293,13 @@ fn node_stored_fields(node_id: Option) -> StoredEventFields { fn billed_token_counts_from_llm(usage: &LlmTokenCounts) -> BilledTokenCounts { BilledTokenCounts { - input_tokens: usage.input_tokens, - output_tokens: usage.output_tokens, - total_tokens: usage.total_tokens(), - reasoning_tokens: usage.reasoning_tokens, - cache_read_tokens: usage.cache_read_tokens, + input_tokens: usage.input_tokens, + output_tokens: usage.output_tokens, + total_tokens: usage.total_tokens(), + reasoning_tokens: usage.reasoning_tokens, + cache_read_tokens: usage.cache_read_tokens, cache_write_tokens: usage.cache_write_tokens, - total_usd_micros: None, + total_usd_micros: None, } } @@ -1479,20 +1479,21 @@ 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, @@ -1503,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 } => { @@ -1551,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, @@ -1566,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, @@ -1581,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 { @@ -1591,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 { @@ -1602,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 { @@ -1653,8 +1654,8 @@ fn event_body_from_event(event: &Event) -> EventBody { will_retry, .. } => EventBody::StageFailed(fabro_types::StageFailedProps { - index: *index, - failure: Some(failure.clone()), + index: *index, + failure: Some(failure.clone()), will_retry: *will_retry, }), Event::StageRetrying { @@ -1664,10 +1665,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, @@ -1675,9 +1676,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 { @@ -1691,10 +1692,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, @@ -1704,11 +1705,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, @@ -1720,12 +1721,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(), }), @@ -1736,8 +1737,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 { @@ -1747,8 +1748,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 { @@ -1759,9 +1760,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 { @@ -1801,16 +1802,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(), }) } @@ -1818,7 +1819,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 } => { @@ -1835,20 +1836,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 { @@ -1859,11 +1860,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, @@ -1873,16 +1874,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 => { @@ -1894,7 +1895,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 { @@ -1903,21 +1904,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, @@ -1925,11 +1926,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"), @@ -1940,10 +1941,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 }) @@ -1951,12 +1952,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, }) } @@ -1964,9 +1965,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, @@ -1974,11 +1975,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, @@ -1987,12 +1988,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, @@ -2000,9 +2001,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, @@ -2010,11 +2011,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, @@ -2022,15 +2023,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 { @@ -2038,14 +2039,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 @@ -2069,8 +2070,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 } => { @@ -2086,20 +2087,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 } => { @@ -2111,13 +2112,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 } => { @@ -2125,7 +2126,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, }) } @@ -2137,31 +2138,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(), }) } @@ -2173,11 +2174,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 { @@ -2187,7 +2188,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 { @@ -2196,9 +2197,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 } => { @@ -2212,10 +2213,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 { @@ -2232,13 +2233,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 { @@ -2254,10 +2255,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 { @@ -2272,11 +2273,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, @@ -2284,9 +2285,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 { @@ -2296,9 +2297,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 { @@ -2309,11 +2310,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, @@ -2323,11 +2324,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, @@ -2336,9 +2337,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 { @@ -2351,14 +2352,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 { @@ -2371,17 +2372,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, }, ), @@ -2391,9 +2392,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 { @@ -2404,17 +2405,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, }, ) @@ -2427,11 +2428,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 { @@ -2439,9 +2440,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, @@ -2449,12 +2450,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, }) } @@ -2466,9 +2467,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, } @@ -2477,9 +2478,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(), } } @@ -2511,9 +2512,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), } } @@ -2758,8 +2759,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, @@ -2892,13 +2893,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); @@ -2941,9 +2942,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, }), ); @@ -2961,31 +2962,28 @@ 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"); @@ -2995,19 +2993,16 @@ mod tests { #[test] fn run_event_stage_failure_keeps_failure_detail() { - let stored = to_run_event( - &fixtures::RUN_3, - &Event::StageFailed { - node_id: "code".to_string(), - name: "Code".to_string(), - index: 1, - failure: FailureDetail::new( - "lint failed", - crate::outcome::FailureCategory::Deterministic, - ), - will_retry: true, - }, - ); + let stored = to_run_event(&fixtures::RUN_3, &Event::StageFailed { + node_id: "code".to_string(), + name: "Code".to_string(), + index: 1, + failure: FailureDetail::new( + "lint failed", + crate::outcome::FailureCategory::Deterministic, + ), + will_retry: true, + }); assert_eq!(stored.event_name(), "stage.failed"); let properties = stored.properties().unwrap(); @@ -3018,20 +3013,17 @@ 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"}), - }, - session_id: Some("ses_child".to_string()), - parent_session_id: Some("ses_parent".to_string()), + 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()), + }); assert_eq!(stored.event_name(), "agent.tool.started"); assert_eq!(stored.node_id.as_deref(), Some("code")); @@ -3046,19 +3038,16 @@ 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()); @@ -3069,15 +3058,12 @@ 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(); @@ -3093,14 +3079,11 @@ mod tests { std::time::Duration::from_millis(1), ); let run_store = store.create_run(&fixtures::RUN_7).await.unwrap(); - let stored = to_run_event( - &fixtures::RUN_7, - &Event::RunNotice { - level: RunNoticeLevel::Warn, - code: "example".to_string(), - message: "notice".to_string(), - }, - ); + let stored = to_run_event(&fixtures::RUN_7, &Event::RunNotice { + level: RunNoticeLevel::Warn, + code: "example".to_string(), + message: "notice".to_string(), + }); let payload = build_redacted_event_payload(&stored, &fixtures::RUN_7).unwrap(); run_store.append_event(&payload).await.unwrap(); @@ -3157,14 +3140,11 @@ 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"); @@ -3178,31 +3158,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" @@ -3214,18 +3194,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)), }), ); @@ -3238,30 +3218,24 @@ 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, @@ -3274,21 +3248,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)), }), ); @@ -3308,19 +3282,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(), @@ -3333,12 +3307,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), @@ -3349,7 +3323,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), @@ -3368,52 +3342,42 @@ 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, - }, - session_id: Some("ses_agent".to_string()), - parent_session_id: None, + 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, + }); let actor = stored.actor.as_ref().expect("actor set"); assert_eq!(actor.kind, ActorKind::Agent); assert_eq!(actor.id.as_deref(), Some("ses_agent")); @@ -3426,34 +3390,31 @@ mod tests { use ::fabro_types::{Graph, RunAuthMethod, RunSubjectProvenance, fixtures}; let provenance = RunProvenance { - server: None, - client: None, + server: None, + client: None, subject: Some(RunSubjectProvenance { - login: Some("alice".to_string()), + login: Some("alice".to_string()), auth_method: RunAuthMethod::Cookie, }), }; - let stored = to_run_event( - &fixtures::RUN_1, - &Event::RunCreated { - run_id: fixtures::RUN_1, - settings: serde_json::to_value(SettingsLayer::default()).unwrap(), - graph: serde_json::to_value(Graph::new("test")).unwrap(), - workflow_source: None, - workflow_config: None, - labels: BTreeMap::default(), - run_dir: "/tmp/run".to_string(), - working_directory: "/tmp/run".to_string(), - host_repo_path: None, - repo_origin_url: None, - base_branch: None, - workflow_slug: None, - db_prefix: None, - provenance: Some(provenance), - manifest_blob: None, - }, - ); + let stored = to_run_event(&fixtures::RUN_1, &Event::RunCreated { + run_id: fixtures::RUN_1, + settings: serde_json::to_value(SettingsLayer::default()).unwrap(), + graph: serde_json::to_value(Graph::new("test")).unwrap(), + workflow_source: None, + workflow_config: None, + labels: BTreeMap::default(), + run_dir: "/tmp/run".to_string(), + working_directory: "/tmp/run".to_string(), + host_repo_path: None, + repo_origin_url: None, + base_branch: None, + workflow_slug: None, + db_prefix: None, + provenance: Some(provenance), + manifest_blob: None, + }); let actor = stored.actor.as_ref().expect("actor set"); assert_eq!(actor.kind, ActorKind::User); assert_eq!(actor.id.as_deref(), Some("alice")); diff --git a/lib/crates/fabro-workflow/src/file_resolver.rs b/lib/crates/fabro-workflow/src/file_resolver.rs index ec6afbe21..c53ec7c30 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 7f194f451..e3e78f049 100644 --- a/lib/crates/fabro-workflow/src/git.rs +++ b/lib/crates/fabro-workflow/src/git.rs @@ -420,121 +420,93 @@ 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 5ccfed63d..b4eb1390f 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 a96215569..58851bd8a 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 5f7359385..33fda1aef 100644 --- a/lib/crates/fabro-workflow/src/handler/agent.rs +++ b/lib/crates/fabro-workflow/src/handler/agent.rs @@ -20,9 +20,9 @@ use crate::outcome::{ /// Result from a `CodergenBackend` invocation. pub enum CodergenResult { Text { - text: String, - usage: Option, - files_touched: Vec, + text: String, + usage: Option, + files_touched: Vec, last_file_touched: Option, }, Full(Outcome), @@ -257,12 +257,12 @@ impl Handler for AgentHandler { let stage_scope = StageScope::for_handler(context, &node.id); services.emitter.emit_scoped( &Event::Prompt { - stage: node.id.clone(), - visit: stage_scope.visit, - text: prompt.clone(), - mode: Some("agent".to_string()), + stage: node.id.clone(), + visit: stage_scope.visit, + text: prompt.clone(), + mode: Some("agent".to_string()), provider: prompt_provider, - model: prompt_model, + model: prompt_model, }, &stage_scope, ); @@ -333,11 +333,11 @@ impl Handler for AgentHandler { .unwrap_or_default(); services.emitter.emit_scoped( &Event::PromptCompleted { - node_id: node.id.clone(), + node_id: node.id.clone(), response: response_text.clone(), - model: response_model, + model: response_model, provider: response_provider, - billing: stage_usage.clone(), + billing: stage_usage.clone(), }, &stage_scope, ); @@ -603,10 +603,11 @@ 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, }) } @@ -659,9 +660,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()), }) } @@ -719,21 +720,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, }) } @@ -829,9 +830,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, }) } @@ -881,9 +882,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, }) } @@ -1109,9 +1110,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, }) } @@ -1178,9 +1179,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 92cfe83f1..1b2175c34 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, ); @@ -712,9 +712,9 @@ mod tests { /// proving that `CommandHandler` delegates to the sandbox rather than /// spawning a host process. struct SpySandbox { - exec_result: fabro_agent::sandbox::ExecResult, - captured_command: std::sync::Mutex>, - captured_env_vars: std::sync::Mutex>>, + exec_result: fabro_agent::sandbox::ExecResult, + captured_command: std::sync::Mutex>, + captured_env_vars: std::sync::Mutex>>, captured_cancel_token: std::sync::Mutex>, } @@ -815,10 +815,10 @@ mod tests { #[tokio::test] async fn executes_script_via_sandbox() { let spy = std::sync::Arc::new(SpySandbox::new(fabro_agent::sandbox::ExecResult { - stdout: "SANDBOX_MARKER\n".into(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: "SANDBOX_MARKER\n".into(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 5, })); @@ -860,10 +860,10 @@ mod tests { #[tokio::test] async fn executes_python_script_via_sandbox() { let spy = std::sync::Arc::new(SpySandbox::new(fabro_agent::sandbox::ExecResult { - stdout: "PYTHON_SANDBOX\n".into(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: "PYTHON_SANDBOX\n".into(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 5, })); @@ -903,10 +903,10 @@ mod tests { #[tokio::test] async fn passes_env_vars_to_sandbox() { let spy = std::sync::Arc::new(SpySandbox::new(fabro_agent::sandbox::ExecResult { - stdout: String::new(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: String::new(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 5, })); @@ -938,10 +938,10 @@ mod tests { #[tokio::test] async fn passes_run_cancellation_to_sandbox() { let spy = std::sync::Arc::new(SpySandbox::new(fabro_agent::sandbox::ExecResult { - stdout: String::new(), - stderr: String::new(), - exit_code: 0, - timed_out: false, + stdout: String::new(), + stderr: String::new(), + exit_code: 0, + timed_out: false, duration_ms: 5, })); diff --git a/lib/crates/fabro-workflow/src/handler/fan_in.rs b/lib/crates/fabro-workflow/src/handler/fan_in.rs index 1689620c9..3c411140d 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 d0491a252..9dbeeaf87 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 555a93043..bb934a5bc 100644 --- a/lib/crates/fabro-workflow/src/handler/llm/api.rs +++ b/lib/crates/fabro-workflow/src/handler/llm/api.rs @@ -41,7 +41,7 @@ struct FileTracking { /// Set of all file paths successfully written/edited. touched: HashSet, /// Most recently modified file path. - last: Option, + last: Option, } fn track_file_event(event: &AgentEvent, state: &mut FileTracking) { @@ -98,10 +98,10 @@ fn spawn_event_forwarder( { emitter.emit_scoped( &Event::Agent { - stage: node_id.clone(), - visit: scope.visit, - event: event.event.clone(), - session_id: Some(event.session_id.clone()), + stage: node_id.clone(), + visit: scope.visit, + event: event.event.clone(), + session_id: Some(event.session_id.clone()), parent_session_id: event.parent_session_id.clone(), }, &scope, @@ -116,12 +116,12 @@ fn spawn_event_forwarder( /// For `full` fidelity nodes sharing a thread key, sessions are cached /// and reused so the LLM sees the full conversation history. pub struct AgentApiBackend { - model: String, - provider: Provider, + model: String, + provider: Provider, fallback_chain: Vec, - sessions: Mutex>, - env: HashMap, - mcp_servers: Vec, + sessions: Mutex>, + env: HashMap, + mcp_servers: Vec, } impl AgentApiBackend { @@ -388,9 +388,9 @@ impl CodergenBackend for AgentApiBackend { ); Ok(CodergenResult::Text { - text: response.text(), - usage: Some(stage_usage), - files_touched: Vec::new(), + text: response.text(), + usage: Some(stage_usage), + files_touched: Vec::new(), last_file_touched: None, }) } @@ -449,7 +449,7 @@ impl CodergenBackend for AgentApiBackend { let file_tracking = Arc::new(Mutex::new(FileTracking { pending: HashMap::new(), touched: HashSet::new(), - last: None, + last: None, })); let stage_scope = StageScope::for_handler(context, &node.id); @@ -487,12 +487,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, ); @@ -641,7 +641,7 @@ mod tests { FileTracking { pending: HashMap::new(), touched: HashSet::new(), - last: None, + last: None, } } @@ -657,9 +657,9 @@ mod tests { track_file_event( &AgentEvent::ToolCallStarted { - tool_name: "write_file".to_string(), + tool_name: "write_file".to_string(), tool_call_id: "tc1".to_string(), - arguments: serde_json::Value::Object(args), + arguments: serde_json::Value::Object(args), }, &mut state, ); @@ -668,9 +668,9 @@ mod tests { track_file_event( &AgentEvent::ToolCallCompleted { tool_call_id: "tc1".to_string(), - tool_name: "write_file".to_string(), - is_error: false, - output: serde_json::Value::String("ok".to_string()), + tool_name: "write_file".to_string(), + is_error: false, + output: serde_json::Value::String("ok".to_string()), }, &mut state, ); @@ -690,9 +690,9 @@ mod tests { track_file_event( &AgentEvent::ToolCallStarted { - tool_name: "edit_file".to_string(), + tool_name: "edit_file".to_string(), tool_call_id: "tc-sub".to_string(), - arguments: serde_json::Value::Object(args), + arguments: serde_json::Value::Object(args), }, &mut state, ); @@ -701,9 +701,9 @@ mod tests { track_file_event( &AgentEvent::ToolCallCompleted { tool_call_id: "tc-sub".to_string(), - tool_name: "edit_file".to_string(), - is_error: false, - output: serde_json::Value::String("ok".to_string()), + tool_name: "edit_file".to_string(), + is_error: false, + output: serde_json::Value::String("ok".to_string()), }, &mut state, ); @@ -723,9 +723,9 @@ mod tests { track_file_event( &AgentEvent::ToolCallStarted { - tool_name: "edit_file".to_string(), + tool_name: "edit_file".to_string(), tool_call_id: "tc-err".to_string(), - arguments: serde_json::Value::Object(args), + arguments: serde_json::Value::Object(args), }, &mut state, ); @@ -733,9 +733,9 @@ mod tests { track_file_event( &AgentEvent::ToolCallCompleted { tool_call_id: "tc-err".to_string(), - tool_name: "edit_file".to_string(), - is_error: true, - output: serde_json::Value::String("failed".to_string()), + tool_name: "edit_file".to_string(), + is_error: true, + output: serde_json::Value::String("failed".to_string()), }, &mut state, ); diff --git a/lib/crates/fabro-workflow/src/handler/llm/cli.rs b/lib/crates/fabro-workflow/src/handler/llm/cli.rs index 3d277ce22..42b2915eb 100644 --- a/lib/crates/fabro-workflow/src/handler/llm/cli.rs +++ b/lib/crates/fabro-workflow/src/handler/llm/cli.rs @@ -210,8 +210,8 @@ pub fn cli_command_for_provider(provider: Provider, model: &str, prompt_file: &s /// Parsed response from a CLI tool invocation. #[derive(Debug)] pub struct CliResponse { - pub text: String, - pub input_tokens: i64, + pub text: String, + pub input_tokens: i64, pub output_tokens: i64, } @@ -376,9 +376,9 @@ fn shell_escape(val: &str) -> String { /// CLI backend that invokes external CLI tools (claude, codex, gemini) via /// `exec_command()`. pub struct AgentCliBackend { - model: String, - provider: Provider, - env: HashMap, + model: String, + provider: Provider, + env: HashMap, poll_interval: std::time::Duration, } @@ -500,12 +500,12 @@ impl CodergenBackend for AgentCliBackend { let stage_scope = StageScope::for_handler(context, &node.id); emitter.emit_scoped( &Event::AgentCliStarted { - node_id: node.id.clone(), - visit: stage_scope.visit, - mode: "cli".to_string(), + node_id: node.id.clone(), + visit: stage_scope.visit, + mode: "cli".to_string(), provider: provider.as_str().to_string(), - model: model.to_string(), - command: command.clone(), + model: model.to_string(), + command: command.clone(), }, &stage_scope, ); @@ -629,10 +629,10 @@ impl CodergenBackend for AgentCliBackend { }; emitter.emit_scoped( &Event::AgentCliCompleted { - node_id: node.id.clone(), - stdout: result.stdout.clone(), - stderr: result.stderr.clone(), - exit_code: result.exit_code, + node_id: node.id.clone(), + stdout: result.stdout.clone(), + stderr: result.stderr.clone(), + exit_code: result.exit_code, duration_ms: result.duration_ms, }, &stage_scope, @@ -699,16 +699,12 @@ 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, @@ -838,7 +834,7 @@ mod tests { /// Mock sandbox that returns pre-configured ExecResults in FIFO order. struct CliMockSandbox { - results: Mutex>, + results: Mutex>, commands: Arc>>, } @@ -931,20 +927,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, } } @@ -1208,9 +1204,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 e985cb805..b8d8ab7d6 100644 --- a/lib/crates/fabro-workflow/src/handler/llm/preamble.rs +++ b/lib/crates/fabro-workflow/src/handler/llm/preamble.rs @@ -624,16 +624,11 @@ 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 5aff1304d..adabfa9ae 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>, /// Logical path of the current workflow when running from a bundle. - pub workflow_path: Option, + pub workflow_path: Option, /// Bundled workflows available for child-workflow resolution. - pub workflow_bundle: Option>, + pub workflow_bundle: Option>, } impl EngineServices { @@ -100,14 +100,14 @@ impl EngineServices { Duration::from_millis(1), )); Self { - registry: Arc::new(HandlerRegistry::new(Box::new(start::StartHandler))), - emitter: Arc::new(Emitter::default()), - sandbox: Arc::new(fabro_agent::LocalSandbox::new( + registry: Arc::new(HandlerRegistry::new(Box::new(start::StartHandler))), + emitter: Arc::new(Emitter::default()), + sandbox: Arc::new(fabro_agent::LocalSandbox::new( std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")), )), // Build the test run store on a dedicated runtime so this helper // remains safe to call from both sync tests and #[tokio::test]. - run_store: std::thread::spawn(move || { + run_store: std::thread::spawn(move || { tokio::runtime::Builder::new_current_thread() .enable_all() .build() @@ -122,14 +122,14 @@ impl EngineServices { .join() .expect("test run store thread should join") .into(), - git_state: std::sync::RwLock::new(None), - hook_runner: None, - env: HashMap::new(), - inputs: HashMap::new(), - dry_run: false, + git_state: std::sync::RwLock::new(None), + hook_runner: None, + env: HashMap::new(), + inputs: HashMap::new(), + dry_run: false, cancel_requested: None, - workflow_path: None, - workflow_bundle: None, + workflow_path: None, + workflow_bundle: None, } } } @@ -228,7 +228,7 @@ pub async fn dispatch_handler( /// Maps handler type strings to handler implementations. pub struct HandlerRegistry { - handlers: HashMap>, + handlers: HashMap>, default_handler: Box, } diff --git a/lib/crates/fabro-workflow/src/handler/parallel.rs b/lib/crates/fabro-workflow/src/handler/parallel.rs index 3cdafeb5c..ea29b250e 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); @@ -318,10 +318,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, ); @@ -334,13 +334,13 @@ impl Handler for ParallelHandler { )); emitter.emit_scoped( &Event::ParallelBranchCompleted { - parallel_group_id: group_id.clone(), + parallel_group_id: group_id.clone(), parallel_branch_id: setup.parallel_branch_id.clone(), - branch: setup.target_id.clone(), - index: setup.branch_index, - duration_ms: millis_u64(branch_start.elapsed()), - status: "fail".to_string(), - head_sha: None, + branch: setup.target_id.clone(), + index: setup.branch_index, + duration_ms: millis_u64(branch_start.elapsed()), + status: "fail".to_string(), + head_sha: None, }, &branch_scope, ); @@ -413,7 +413,7 @@ impl Handler for ParallelHandler { emitter.emit_scoped( &Event::GitCommit { node_id: Some(setup.target_id.clone()), - sha: sha.clone(), + sha: sha.clone(), }, &branch_scope, ); @@ -427,13 +427,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, ); @@ -457,17 +457,19 @@ 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 a107a2eae..9e165ef6b 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, ); @@ -280,9 +280,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, }) } @@ -340,9 +340,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, }) } @@ -371,7 +371,7 @@ mod tests { } struct OneShotCapturingBackend { - captured_prompt: Arc>>, + captured_prompt: Arc>>, captured_system_prompt: Arc>>>, } @@ -399,9 +399,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, }) } @@ -413,7 +413,7 @@ mod tests { let captured = Arc::new(Mutex::new(None)); let backend = OneShotCapturingBackend { - captured_prompt: captured.clone(), + captured_prompt: captured.clone(), captured_system_prompt: Arc::new(Mutex::new(None)), }; let handler = PromptHandler::new(Some(Box::new(backend))); @@ -450,7 +450,7 @@ mod tests { let captured_sys = Arc::new(Mutex::new(None)); let backend = OneShotCapturingBackend { - captured_prompt: Arc::new(Mutex::new(None)), + captured_prompt: Arc::new(Mutex::new(None)), captured_system_prompt: captured_sys.clone(), }; let handler = PromptHandler::new(Some(Box::new(backend))); @@ -483,7 +483,7 @@ mod tests { let captured_sys = Arc::new(Mutex::new(None)); let backend = OneShotCapturingBackend { - captured_prompt: Arc::new(Mutex::new(None)), + captured_prompt: Arc::new(Mutex::new(None)), captured_system_prompt: captured_sys.clone(), }; let handler = PromptHandler::new(Some(Box::new(backend))); diff --git a/lib/crates/fabro-workflow/src/lib.rs b/lib/crates/fabro-workflow/src/lib.rs index b96b7ec5f..ad55b8fdf 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 17a2e4603..5cab268e7 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 d2b6507ad..7b4bdf135 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 83f0becc9..4b5348b22 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, @@ -211,10 +211,10 @@ impl RunLifecycle for EventLifecycle { self.emitter.emit_scoped( &Event::StageFailed { - node_id: gv.id.clone(), - name: gv.label().to_string(), - index: stage_index, - failure: outcome.failure.clone().unwrap_or_else(|| { + node_id: gv.id.clone(), + name: gv.label().to_string(), + index: stage_index, + failure: outcome.failure.clone().unwrap_or_else(|| { FailureDetail::new("handler failed", FailureCategory::TransientInfra) }), will_retry: true, @@ -224,12 +224,12 @@ impl RunLifecycle for EventLifecycle { self.emitter.emit_scoped( &Event::StageRetrying { - node_id: gv.id.clone(), - name: gv.label().to_string(), - index: stage_index, - attempt: ctx.attempt as usize, + node_id: gv.id.clone(), + name: gv.label().to_string(), + index: stage_index, + attempt: ctx.attempt as usize, max_attempts: ctx.result.max_attempts as usize, - delay_ms: ctx + delay_ms: ctx .backoff_delay .map_or(0, |d| u64::try_from(d.as_millis()).unwrap()), }, @@ -260,10 +260,10 @@ impl RunLifecycle for EventLifecycle { if outcome.status == StageStatus::Fail { self.emitter.emit_scoped( &Event::StageFailed { - node_id: gv.id.clone(), - name: gv.label().to_string(), - index: stage_index, - failure: outcome.failure.clone().unwrap_or_else(|| { + node_id: gv.id.clone(), + name: gv.label().to_string(), + index: stage_index, + failure: outcome.failure.clone().unwrap_or_else(|| { FailureDetail::new("handler failed", FailureCategory::Deterministic) }), will_retry: false, @@ -399,14 +399,14 @@ impl RunLifecycle for EventLifecycle { self.emitter.emit_scoped( &Event::GitCommit { node_id: Some(node.id().to_string()), - sha: sha.clone(), + sha: sha.clone(), }, &scope, ); } for (branch, success) in &result.push_results { self.emitter.emit(&Event::GitPush { - branch: branch.clone(), + branch: branch.clone(), success: *success, }); } diff --git a/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs b/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs index 019c8fde3..c39eded75 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 ca12e5a55..faa6f10ba 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 90f35da4c..f3d66a674 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 8db964934..a597391ff 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 80f125158..98018ffda 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 105da69b5..94870fe84 100644 --- a/lib/crates/fabro-workflow/src/operations/create.rs +++ b/lib/crates/fabro-workflow/src/operations/create.rs @@ -45,22 +45,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, + settings: SettingsLayer, + run_id: Option, + run_dir: Option, + workflow_slug: Option, + labels: HashMap, + base_branch: Option, working_directory: PathBuf, - host_repo_path: Option, - repo_origin_url: Option, - provenance: Option, + host_repo_path: Option, + repo_origin_url: Option, + provenance: Option, } /// Resolve workflow inputs, normalize settings, and persist a run directory. @@ -68,7 +68,7 @@ pub async fn create(store: &Database, request: CreateRunInput) -> Result PathBuf { @@ -454,7 +444,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, @@ -573,12 +563,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()); @@ -616,12 +606,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(); @@ -652,9 +642,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(); @@ -665,9 +655,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"] @@ -675,7 +665,7 @@ mod tests { start -> validate -> exit }"# .to_string(), - files: HashMap::from([ + files: HashMap::from([ ( PathBuf::from("child/validate.fabro"), r#"digraph Validate { @@ -692,8 +682,8 @@ mod tests { ), ]), }), - settings: SettingsLayer::default(), - cwd: PathBuf::from("."), + settings: SettingsLayer::default(), + cwd: PathBuf::from("."), custom_transforms: Vec::new(), }) .unwrap(); @@ -716,26 +706,23 @@ 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, - }, - 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, + 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, + }) .await .unwrap_err(); @@ -751,53 +738,50 @@ 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, - }, - 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, + 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, + }) .await .unwrap(); @@ -862,39 +846,36 @@ 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() - }), - ..RunLayer::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, + 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() + }), + ..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, + }) .await .unwrap(); @@ -916,26 +897,23 @@ 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, - }, - 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, + 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, + }) .await .unwrap(); @@ -988,26 +966,23 @@ mod tests { let object_store = Arc::new(LocalFileSystem::new_with_prefix(storage_dir.join("store")).unwrap()); let store = Arc::new(Database::new(object_store, "", Duration::from_millis(1))); - let created = create( - store.as_ref(), - CreateRunInput { - workflow: WorkflowInput::DotSource { - source: MINIMAL_DOT.to_string(), - base_dir: None, - }, - 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, + 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, + }) .await .unwrap(); let run_store = store.open_run_reader(&created.run_id).await.unwrap(); @@ -1027,39 +1002,36 @@ mod tests { let object_store = Arc::new(LocalFileSystem::new_with_prefix(storage_dir.join("store")).unwrap()); let store = Arc::new(Database::new(object_store, "", Duration::from_millis(1))); - let created = create( - store.as_ref(), - CreateRunInput { - workflow: WorkflowInput::DotSource { - source: MINIMAL_DOT.to_string(), - base_dir: None, - }, - 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, - }), - }), + 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, + }), + }), + }) .await .unwrap(); diff --git a/lib/crates/fabro-workflow/src/operations/fork.rs b/lib/crates/fabro-workflow/src/operations/fork.rs index 07c8f624d..8f039fa04 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,14 +243,11 @@ 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}"); @@ -290,11 +287,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 4a7513634..d6c6c9ba2 100644 --- a/lib/crates/fabro-workflow/src/operations/rebuild_meta.rs +++ b/lib/crates/fabro-workflow/src/operations/rebuild_meta.rs @@ -166,7 +166,7 @@ pub async fn build_timeline_or_rebuild( } Ok(RunTimeline { - entries: Vec::new(), + entries: Vec::new(), parallel_map: HashMap::new(), }) } @@ -397,11 +397,11 @@ mod tests { fn sample_sandbox_record() -> SandboxRecord { SandboxRecord { - provider: "local".to_string(), - working_directory: "/tmp/project".to_string(), - identifier: None, + provider: "local".to_string(), + working_directory: "/tmp/project".to_string(), + identifier: None, host_working_directory: None, - container_mount_point: None, + container_mount_point: None, } } @@ -412,20 +412,20 @@ mod tests { git_commit_sha: Option<&str>, ) -> Checkpoint { Checkpoint { - timestamp: created_at(), - current_node: current_node.to_string(), - completed_nodes: completed_nodes + timestamp: created_at(), + current_node: current_node.to_string(), + completed_nodes: completed_nodes .iter() .map(|node| (*node).to_string()) .collect(), - node_retries: HashMap::new(), - context_values: HashMap::new(), - node_outcomes: HashMap::new(), - next_node_id: None, - git_commit_sha: git_commit_sha.map(ToOwned::to_owned), - loop_failure_signatures: HashMap::new(), + node_retries: HashMap::new(), + context_values: HashMap::new(), + node_outcomes: HashMap::new(), + next_node_id: None, + git_commit_sha: git_commit_sha.map(ToOwned::to_owned), + loop_failure_signatures: HashMap::new(), restart_failure_signatures: HashMap::new(), - node_visits: node_visits + node_visits: node_visits .iter() .map(|(node, visit)| ((*node).to_string(), *visit)) .collect(), @@ -439,27 +439,23 @@ 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 @@ -467,36 +463,28 @@ 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(); } @@ -506,35 +494,31 @@ 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(); } @@ -545,18 +529,14 @@ 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/rewind.rs b/lib/crates/fabro-workflow/src/operations/rewind.rs index 8ec98435d..fef41e0fa 100644 --- a/lib/crates/fabro-workflow/src/operations/rewind.rs +++ b/lib/crates/fabro-workflow/src/operations/rewind.rs @@ -51,16 +51,16 @@ impl FromStr for RewindTarget { #[derive(Debug, Clone)] pub struct TimelineEntry { - pub ordinal: usize, - pub node_name: String, - pub visit: usize, + pub ordinal: usize, + pub node_name: String, + pub visit: usize, pub metadata_commit_oid: Oid, - pub run_commit_sha: Option, + 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,14 +508,11 @@ 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 d6728e871..40553e277 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 03ef1d587..f251458e8 100644 --- a/lib/crates/fabro-workflow/src/operations/start.rs +++ b/lib/crates/fabro-workflow/src/operations/start.rs @@ -49,53 +49,53 @@ use crate::runtime_store::RunStoreHandle; use crate::workflow_bundle::{RunDefinition, WorkflowBundle}; struct RunSession { - cancel_token: Option>, - emitter: Arc, - sandbox: SandboxSpec, - llm: LlmSpec, - interviewer: Arc, - on_node: crate::OnNodeCallback, - lifecycle: LifecycleOptions, - hooks: fabro_hooks::HookSettings, - sandbox_env: SandboxEnvSpec, - devcontainer: Option, - seed_context: Option, - run_store: RunStoreHandle, - event_sink: RunEventSink, - artifact_sink: Option, - git: Option, - github_app: Option, - worktree_mode: Option, + cancel_token: Option>, + emitter: Arc, + sandbox: SandboxSpec, + llm: LlmSpec, + interviewer: Arc, + on_node: crate::OnNodeCallback, + lifecycle: LifecycleOptions, + hooks: fabro_hooks::HookSettings, + sandbox_env: SandboxEnvSpec, + devcontainer: Option, + seed_context: Option, + run_store: RunStoreHandle, + event_sink: RunEventSink, + artifact_sink: Option, + git: Option, + github_app: Option, + worktree_mode: Option, registry_override: Option>, - retro_enabled: bool, - preserve_sandbox: bool, - pr_config: Option, - pr_github_app: Option, - pr_origin_url: Option, - pr_model: String, - workflow_path: Option, - workflow_bundle: Option>, - run_control: Option>, + retro_enabled: bool, + preserve_sandbox: bool, + pr_config: Option, + pr_github_app: Option, + pr_origin_url: Option, + pr_model: String, + workflow_path: Option, + workflow_bundle: Option>, + run_control: Option>, } pub struct StartServices { - pub run_id: RunId, - pub cancel_token: Option>, - pub emitter: Arc, - pub interviewer: Arc, - pub run_store: RunStoreHandle, - pub event_sink: RunEventSink, - pub artifact_sink: Option, - pub run_control: Option>, - pub github_app: Option, - pub on_node: crate::OnNodeCallback, + pub run_id: RunId, + pub cancel_token: Option>, + pub emitter: Arc, + pub interviewer: Arc, + pub run_store: RunStoreHandle, + pub event_sink: RunEventSink, + pub artifact_sink: Option, + pub run_control: Option>, + pub github_app: Option, + pub on_node: crate::OnNodeCallback, pub registry_override: Option>, } pub struct Started { - pub finalized: Finalized, - pub final_context: Option, - pub retro: Option, + pub finalized: Finalized, + pub final_context: Option, + pub retro: Option, pub retro_duration: Duration, } @@ -148,13 +148,9 @@ 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()); @@ -253,16 +249,12 @@ 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"); @@ -281,8 +273,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())), }) }); @@ -350,9 +342,9 @@ impl RunSession { }, }, SandboxProvider::Daytona => SandboxSpec::Daytona { - config: resolve_daytona_config(&resolved).unwrap_or_default(), - github_app: services.github_app.clone(), - run_id: Some(record.run_id), + config: resolve_daytona_config(&resolved).unwrap_or_default(), + github_app: services.github_app.clone(), + run_id: Some(record.run_id), clone_branch: detected_base_branch.or_else(|| record.base_branch.clone()), }, }; @@ -383,7 +375,7 @@ impl RunSession { }; let devcontainer = resolved.sandbox.devcontainer.then(|| DevcontainerSpec { - enabled: true, + enabled: true, resolve_dir: working_directory.clone(), }); @@ -412,9 +404,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(), @@ -518,39 +510,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() @@ -563,21 +555,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, @@ -595,12 +587,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, } } @@ -615,10 +607,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, @@ -626,15 +618,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, }, } @@ -652,17 +644,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)); @@ -762,21 +754,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(); @@ -796,10 +788,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 { @@ -838,16 +830,12 @@ 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; }); } @@ -859,10 +847,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 { @@ -909,26 +897,18 @@ 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,24 +925,20 @@ 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 { @@ -1014,38 +990,35 @@ 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() - }), - ..RunLayer::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, + 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() + }), + ..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, + }) .await .unwrap(); (created.persisted, store) @@ -1163,11 +1136,9 @@ 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 [ @@ -1179,56 +1150,49 @@ 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() - }), - ..RunLayer::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() }), - ..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, + ..RunLayer::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, + }) .await .unwrap(); @@ -1252,18 +1216,15 @@ 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(); @@ -1283,17 +1244,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(), @@ -1384,59 +1345,51 @@ 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 05597e567..830076609 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 d5c1daf15..ada74ab72 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 d9bcb1518..f6b555af9 100644 --- a/lib/crates/fabro-workflow/src/pipeline/execute.rs +++ b/lib/crates/fabro-workflow/src/pipeline/execute.rs @@ -98,8 +98,8 @@ pub async fn execute(init: Initialized) -> Executed { let handler = Arc::new(WorkflowNodeHandler { services: shared_services, - run_dir: run_options.run_dir.clone(), - graph: Arc::clone(&graph_arc), + run_dir: run_options.run_dir.clone(), + graph: Arc::clone(&graph_arc), }); let settings_arc = Arc::new(run_options.clone()); @@ -314,7 +314,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 e2f326602..2d995fe04 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, } } @@ -196,27 +196,27 @@ async fn execute_test_run_with_options( working_directory: std::env::current_dir().unwrap(), }, llm: LlmSpec { - model: "test-model".to_string(), - provider: fabro_llm::Provider::Anthropic, + model: "test-model".to_string(), + provider: fabro_llm::Provider::Anthropic, fallback_chain: Vec::new(), - mcp_servers: Vec::new(), - dry_run: true, + mcp_servers: Vec::new(), + dry_run: true, }, interviewer: Arc::new(AutoApproveInterviewer), lifecycle: LifecycleOptions { - setup_commands: vec![], + setup_commands: vec![], setup_command_timeout_ms: 1_000, - devcontainer_phases: vec![], + devcontainer_phases: vec![], }, run_options, workflow_path: None, workflow_bundle: None, hooks: HookSettings { hooks: vec![] }, sandbox_env: SandboxEnvSpec { - devcontainer_env: HashMap::new(), - toml_env: HashMap::new(), + devcontainer_env: HashMap::new(), + toml_env: HashMap::new(), github_permissions: None, - origin_url: None, + origin_url: None, }, devcontainer: None, git: git_options, @@ -245,44 +245,44 @@ async fn execute_runs_start_to_exit_and_returns_final_context() { let initialized = initialize( persisted_workflow(graph, source, &run_dir, test_run_id("run-test")), InitOptions { - run_id: test_run_id("run-test"), - run_store: test_run_store(&test_run_id("run-test")).await.into(), - dry_run: false, - emitter: test_emitter_arc("run-test"), - sandbox: SandboxSpec::Local { + run_id: test_run_id("run-test"), + run_store: test_run_store(&test_run_id("run-test")).await.into(), + dry_run: false, + emitter: test_emitter_arc("run-test"), + sandbox: SandboxSpec::Local { working_directory: std::env::current_dir().unwrap(), }, - llm: LlmSpec { - model: "test-model".to_string(), - provider: fabro_llm::Provider::Anthropic, + llm: LlmSpec { + model: "test-model".to_string(), + provider: fabro_llm::Provider::Anthropic, fallback_chain: Vec::new(), - mcp_servers: Vec::new(), - dry_run: true, + mcp_servers: Vec::new(), + dry_run: true, }, - interviewer: Arc::new(AutoApproveInterviewer), - lifecycle: LifecycleOptions { - setup_commands: vec![], + interviewer: Arc::new(AutoApproveInterviewer), + lifecycle: LifecycleOptions { + setup_commands: vec![], setup_command_timeout_ms: 1_000, - devcontainer_phases: vec![], + devcontainer_phases: vec![], }, - run_options: test_run_options(&run_dir, "run-test"), - workflow_path: None, - workflow_bundle: None, - hooks: HookSettings { hooks: vec![] }, - sandbox_env: SandboxEnvSpec { - devcontainer_env: HashMap::new(), - toml_env: HashMap::new(), + run_options: test_run_options(&run_dir, "run-test"), + workflow_path: None, + workflow_bundle: None, + hooks: HookSettings { hooks: vec![] }, + sandbox_env: SandboxEnvSpec { + devcontainer_env: HashMap::new(), + toml_env: HashMap::new(), github_permissions: None, - origin_url: None, + origin_url: None, }, - devcontainer: None, - git: None, - worktree_mode: None, - run_control: None, + devcontainer: None, + git: None, + worktree_mode: None, + run_control: None, registry_override: None, - artifact_sink: None, - checkpoint: None, - seed_context: None, + artifact_sink: None, + checkpoint: None, + seed_context: None, }, ) .await @@ -324,11 +324,11 @@ async fn run_with_lifecycle( working_directory: PathBuf::from(sandbox.working_directory()), }, llm: LlmSpec { - model: "test-model".to_string(), - provider: fabro_llm::Provider::Anthropic, + model: "test-model".to_string(), + provider: fabro_llm::Provider::Anthropic, fallback_chain: Vec::new(), - mcp_servers: Vec::new(), - dry_run: true, + mcp_servers: Vec::new(), + dry_run: true, }, interviewer: Arc::new(AutoApproveInterviewer), lifecycle, @@ -337,10 +337,10 @@ async fn run_with_lifecycle( workflow_bundle: None, hooks: HookSettings { hooks: vec![] }, sandbox_env: SandboxEnvSpec { - devcontainer_env: HashMap::new(), - toml_env: HashMap::new(), + devcontainer_env: HashMap::new(), + toml_env: HashMap::new(), github_permissions: None, - origin_url: None, + origin_url: None, }, devcontainer: None, git: None, @@ -641,8 +641,8 @@ async fn execute_writes_start_json_and_node_status() { let dir = tempfile::tempdir().unwrap(); let mut run_options = test_run_options(dir.path(), "test-run"); run_options.git = Some(GitCheckpointOptions { - base_sha: Some("abc123".into()), - run_branch: Some(format!("fabro/run/{}", test_run_id("test-run"))), + base_sha: Some("abc123".into()), + run_branch: Some(format!("fabro/run/{}", test_run_id("test-run"))), meta_branch: None, }); diff --git a/lib/crates/fabro-workflow/src/pipeline/finalize.rs b/lib/crates/fabro-workflow/src/pipeline/finalize.rs index b7705cb62..0c1a7dde9 100644 --- a/lib/crates/fabro-workflow/src/pipeline/finalize.rs +++ b/lib/crates/fabro-workflow/src/pipeline/finalize.rs @@ -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, } } @@ -365,18 +365,15 @@ 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 6592633da..5c6844ddd 100644 --- a/lib/crates/fabro-workflow/src/pipeline/initialize.rs +++ b/lib/crates/fabro-workflow/src/pipeline/initialize.rs @@ -28,9 +28,9 @@ use crate::handler::{HandlerRegistry, default_registry, sandbox_cancel_token}; use crate::run_options::GitCheckpointOptions; struct WorktreePlan { - branch_name: String, - base_sha: String, - worktree_path: PathBuf, + branch_name: String, + base_sha: String, + worktree_path: PathBuf, skip_branch_creation: bool, } @@ -70,9 +70,10 @@ async fn resolve_worktree_plan(options: &mut InitOptions) -> Result { @@ -509,11 +507,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( @@ -739,17 +737,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, } } @@ -785,53 +783,50 @@ 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() - }, - dry_run: false, - emitter, - sandbox: SandboxSpec::Local { - working_directory: std::env::current_dir().unwrap(), - }, - llm: LlmSpec { - model: "test-model".to_string(), - provider: fabro_llm::Provider::Anthropic, - fallback_chain: Vec::new(), - mcp_servers: Vec::new(), - dry_run: true, - }, - interviewer: Arc::new(AutoApproveInterviewer), - lifecycle: crate::run_options::LifecycleOptions { - setup_commands: vec![], - setup_command_timeout_ms: 1_000, - devcontainer_phases: vec![], - }, - run_options: test_settings(&run_dir), - workflow_path: None, - workflow_bundle: None, - hooks: fabro_hooks::HookSettings { hooks: vec![] }, - sandbox_env: SandboxEnvSpec { - devcontainer_env: HashMap::new(), - toml_env: HashMap::from([("TEST_KEY".to_string(), "value".to_string())]), - github_permissions: None, - origin_url: None, - }, - devcontainer: None, - git: None, - worktree_mode: None, - run_control: None, - registry_override: None, - artifact_sink: None, - checkpoint: None, - seed_context: None, + let initialized = initialize(persisted, InitOptions { + run_id: test_run_id(), + run_store: { + let store = memory_store(); + let inner = store.create_run(&test_run_id()).await.unwrap(); + inner.into() }, - ) + dry_run: false, + emitter, + sandbox: SandboxSpec::Local { + working_directory: std::env::current_dir().unwrap(), + }, + llm: LlmSpec { + model: "test-model".to_string(), + provider: fabro_llm::Provider::Anthropic, + fallback_chain: Vec::new(), + mcp_servers: Vec::new(), + dry_run: true, + }, + interviewer: Arc::new(AutoApproveInterviewer), + lifecycle: crate::run_options::LifecycleOptions { + setup_commands: vec![], + setup_command_timeout_ms: 1_000, + devcontainer_phases: vec![], + }, + run_options: test_settings(&run_dir), + workflow_path: None, + workflow_bundle: None, + hooks: fabro_hooks::HookSettings { hooks: vec![] }, + sandbox_env: SandboxEnvSpec { + devcontainer_env: HashMap::new(), + toml_env: HashMap::from([("TEST_KEY".to_string(), "value".to_string())]), + github_permissions: None, + origin_url: None, + }, + devcontainer: None, + git: None, + worktree_mode: None, + run_control: None, + registry_override: None, + artifact_sink: None, + checkpoint: None, + seed_context: None, + }) .await .unwrap(); @@ -866,49 +861,46 @@ 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(), - }, - llm: LlmSpec { - model: "test-model".to_string(), - provider: fabro_llm::Provider::Anthropic, - fallback_chain: Vec::new(), - mcp_servers: Vec::new(), - dry_run: true, - }, - interviewer: Arc::new(AutoApproveInterviewer), - lifecycle: crate::run_options::LifecycleOptions { - setup_commands: vec!["true".to_string()], - setup_command_timeout_ms: 1_000, - devcontainer_phases: vec![], - }, - run_options: test_settings(&run_dir), - workflow_path: None, - workflow_bundle: None, - hooks: fabro_hooks::HookSettings { hooks: vec![] }, - sandbox_env: SandboxEnvSpec { - devcontainer_env: HashMap::new(), - toml_env: HashMap::new(), - github_permissions: None, - origin_url: None, - }, - devcontainer: None, - git: None, - worktree_mode: None, - run_control: None, - registry_override: None, - artifact_sink: None, - checkpoint: None, - seed_context: None, + let initialized = initialize(persisted, InitOptions { + run_id: test_run_id(), + run_store: run_store.into(), + dry_run: false, + emitter, + sandbox: SandboxSpec::Local { + working_directory: std::env::current_dir().unwrap(), }, - ) + llm: LlmSpec { + model: "test-model".to_string(), + provider: fabro_llm::Provider::Anthropic, + fallback_chain: Vec::new(), + mcp_servers: Vec::new(), + dry_run: true, + }, + interviewer: Arc::new(AutoApproveInterviewer), + lifecycle: crate::run_options::LifecycleOptions { + setup_commands: vec!["true".to_string()], + setup_command_timeout_ms: 1_000, + devcontainer_phases: vec![], + }, + run_options: test_settings(&run_dir), + workflow_path: None, + workflow_bundle: None, + hooks: fabro_hooks::HookSettings { hooks: vec![] }, + sandbox_env: SandboxEnvSpec { + devcontainer_env: HashMap::new(), + toml_env: HashMap::new(), + github_permissions: None, + origin_url: None, + }, + devcontainer: None, + git: None, + worktree_mode: None, + run_control: None, + registry_override: None, + artifact_sink: None, + checkpoint: None, + seed_context: None, + }) .await .unwrap(); store_logger.flush().await; @@ -933,53 +925,50 @@ 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() - }, - dry_run: false, - emitter: Arc::new(crate::event::Emitter::new(test_run_id())), - sandbox: SandboxSpec::Local { - working_directory: std::env::current_dir().unwrap(), - }, - llm: LlmSpec { - model: "test-model".to_string(), - provider: fabro_llm::Provider::Anthropic, - fallback_chain: Vec::new(), - mcp_servers: Vec::new(), - dry_run: true, - }, - interviewer: Arc::new(AutoApproveInterviewer), - lifecycle: crate::run_options::LifecycleOptions { - setup_commands: vec!["sleep 5".to_string()], - setup_command_timeout_ms: 5_000, - devcontainer_phases: vec![], - }, - run_options, - workflow_path: None, - workflow_bundle: None, - hooks: fabro_hooks::HookSettings { hooks: vec![] }, - sandbox_env: SandboxEnvSpec { - devcontainer_env: HashMap::new(), - toml_env: HashMap::new(), - github_permissions: None, - origin_url: None, - }, - devcontainer: None, - git: None, - worktree_mode: None, - run_control: None, - registry_override: None, - artifact_sink: None, - checkpoint: None, - seed_context: None, + let result = initialize(persisted, InitOptions { + run_id: test_run_id(), + run_store: { + let store = memory_store(); + let inner = store.create_run(&test_run_id()).await.unwrap(); + inner.into() }, - ) + dry_run: false, + emitter: Arc::new(crate::event::Emitter::new(test_run_id())), + sandbox: SandboxSpec::Local { + working_directory: std::env::current_dir().unwrap(), + }, + llm: LlmSpec { + model: "test-model".to_string(), + provider: fabro_llm::Provider::Anthropic, + fallback_chain: Vec::new(), + mcp_servers: Vec::new(), + dry_run: true, + }, + interviewer: Arc::new(AutoApproveInterviewer), + lifecycle: crate::run_options::LifecycleOptions { + setup_commands: vec!["sleep 5".to_string()], + setup_command_timeout_ms: 5_000, + devcontainer_phases: vec![], + }, + run_options, + workflow_path: None, + workflow_bundle: None, + hooks: fabro_hooks::HookSettings { hooks: vec![] }, + sandbox_env: SandboxEnvSpec { + devcontainer_env: HashMap::new(), + toml_env: HashMap::new(), + github_permissions: None, + origin_url: None, + }, + devcontainer: None, + git: None, + worktree_mode: None, + run_control: None, + registry_override: None, + artifact_sink: None, + checkpoint: None, + seed_context: None, + }) .await; assert!(matches!(result, Err(Error::Cancelled))); @@ -996,56 +985,52 @@ 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() - }, - dry_run: false, - emitter: Arc::new(crate::event::Emitter::new(test_run_id())), - sandbox: SandboxSpec::Local { - working_directory: std::env::current_dir().unwrap(), - }, - llm: LlmSpec { - model: "test-model".to_string(), - provider: fabro_llm::Provider::Anthropic, - fallback_chain: Vec::new(), - mcp_servers: Vec::new(), - dry_run: true, - }, - interviewer: Arc::new(AutoApproveInterviewer), - lifecycle: crate::run_options::LifecycleOptions { - setup_commands: vec![], - setup_command_timeout_ms: 5_000, - devcontainer_phases: vec![( - "on_create".to_string(), - vec![fabro_devcontainer::Command::Shell("sleep 5".to_string())], - )], - }, - run_options, - workflow_path: None, - workflow_bundle: None, - hooks: fabro_hooks::HookSettings { hooks: vec![] }, - sandbox_env: SandboxEnvSpec { - devcontainer_env: HashMap::new(), - toml_env: HashMap::new(), - github_permissions: None, - origin_url: None, - }, - devcontainer: None, - git: None, - worktree_mode: None, - run_control: None, - registry_override: None, - artifact_sink: None, - checkpoint: None, - seed_context: None, + let result = initialize(persisted, InitOptions { + run_id: test_run_id(), + run_store: { + let store = memory_store(); + let inner = store.create_run(&test_run_id()).await.unwrap(); + inner.into() }, - ) + dry_run: false, + emitter: Arc::new(crate::event::Emitter::new(test_run_id())), + sandbox: SandboxSpec::Local { + working_directory: std::env::current_dir().unwrap(), + }, + llm: LlmSpec { + model: "test-model".to_string(), + provider: fabro_llm::Provider::Anthropic, + fallback_chain: Vec::new(), + mcp_servers: Vec::new(), + dry_run: true, + }, + interviewer: Arc::new(AutoApproveInterviewer), + lifecycle: crate::run_options::LifecycleOptions { + setup_commands: vec![], + setup_command_timeout_ms: 5_000, + devcontainer_phases: vec![("on_create".to_string(), vec![ + fabro_devcontainer::Command::Shell("sleep 5".to_string()), + ])], + }, + run_options, + workflow_path: None, + workflow_bundle: None, + hooks: fabro_hooks::HookSettings { hooks: vec![] }, + sandbox_env: SandboxEnvSpec { + devcontainer_env: HashMap::new(), + toml_env: HashMap::new(), + github_permissions: None, + origin_url: None, + }, + devcontainer: None, + git: None, + worktree_mode: None, + run_control: None, + registry_override: None, + artifact_sink: None, + checkpoint: None, + seed_context: None, + }) .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 aaa8d848c..4a0cc1b27 100644 --- a/lib/crates/fabro-workflow/src/pipeline/persist.rs +++ b/lib/crates/fabro-workflow/src/pipeline/persist.rs @@ -157,27 +157,23 @@ 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 @@ -191,7 +187,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()), }, ) @@ -218,7 +214,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()), }, ) @@ -243,7 +239,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(), }, ) @@ -283,13 +279,10 @@ 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 3010cac3f..b7521540a 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, }, )), ]; @@ -693,109 +693,109 @@ mod tests { fn make_test_conclusion() -> Conclusion { Conclusion { - timestamp: Utc::now(), - status: crate::outcome::StageStatus::Success, - duration_ms: 150_000, - failure_reason: None, + timestamp: Utc::now(), + status: crate::outcome::StageStatus::Success, + duration_ms: 150_000, + failure_reason: None, final_git_commit_sha: None, - stages: vec![ + stages: vec![ StageSummary { - stage_id: "plan".to_string(), - stage_label: "plan".to_string(), - duration_ms: 45_000, + stage_id: "plan".to_string(), + stage_label: "plan".to_string(), + duration_ms: 45_000, billing_usd_micros: Some(120_000), - retries: 0, + retries: 0, }, StageSummary { - stage_id: "implement".to_string(), - stage_label: "implement".to_string(), - duration_ms: 90_000, + stage_id: "implement".to_string(), + stage_label: "implement".to_string(), + duration_ms: 90_000, billing_usd_micros: Some(250_000), - retries: 0, + retries: 0, }, StageSummary { - stage_id: "simplify".to_string(), - stage_label: "simplify".to_string(), - duration_ms: 15_000, + stage_id: "simplify".to_string(), + stage_label: "simplify".to_string(), + duration_ms: 15_000, billing_usd_micros: Some(50_000), - retries: 0, + retries: 0, }, ], - billing: Some(BilledTokenCounts { + billing: Some(BilledTokenCounts { total_usd_micros: Some(420_000), ..BilledTokenCounts::default() }), - total_retries: 0, + total_retries: 0, } } fn make_test_retro() -> Retro { Retro { - run_id: fixtures::RUN_1, - workflow_name: "implement".to_string(), - goal: "Fix the bug".to_string(), - timestamp: Utc::now(), - smoothness: None, - stages: vec![ + run_id: fixtures::RUN_1, + workflow_name: "implement".to_string(), + goal: "Fix the bug".to_string(), + timestamp: Utc::now(), + smoothness: None, + stages: vec![ StageRetro { - stage_id: "plan".to_string(), - stage_label: "plan".to_string(), - status: "success".to_string(), - duration_ms: 45_000, - retries: 0, + stage_id: "plan".to_string(), + stage_label: "plan".to_string(), + status: "success".to_string(), + duration_ms: 45_000, + retries: 0, billing_usd_micros: Some(120_000), - notes: None, - failure_reason: None, - files_touched: vec![], + notes: None, + failure_reason: None, + files_touched: vec![], }, StageRetro { - stage_id: "implement".to_string(), - stage_label: "implement".to_string(), - status: "success".to_string(), - duration_ms: 90_000, - retries: 0, + stage_id: "implement".to_string(), + stage_label: "implement".to_string(), + status: "success".to_string(), + duration_ms: 90_000, + retries: 0, billing_usd_micros: Some(250_000), - notes: None, - failure_reason: None, - files_touched: vec!["src/main.rs".to_string(), "src/lib.rs".to_string()], + notes: None, + failure_reason: None, + files_touched: vec!["src/main.rs".to_string(), "src/lib.rs".to_string()], }, StageRetro { - stage_id: "simplify".to_string(), - stage_label: "simplify".to_string(), - status: "success".to_string(), - duration_ms: 15_000, - retries: 0, + stage_id: "simplify".to_string(), + stage_label: "simplify".to_string(), + status: "success".to_string(), + duration_ms: 15_000, + retries: 0, billing_usd_micros: Some(50_000), - notes: None, - failure_reason: None, - files_touched: vec![], + notes: None, + failure_reason: None, + files_touched: vec![], }, ], - stats: AggregateStats { - total_duration_ms: 150_000, + stats: AggregateStats { + total_duration_ms: 150_000, total_billing_usd_micros: Some(420_000), - total_retries: 0, - files_touched: vec!["src/lib.rs".to_string(), "src/main.rs".to_string()], - stages_completed: 3, - stages_failed: 0, + total_retries: 0, + files_touched: vec!["src/lib.rs".to_string(), "src/main.rs".to_string()], + stages_completed: 3, + stages_failed: 0, }, - intent: None, - outcome: None, - learnings: None, + intent: None, + outcome: None, + learnings: None, friction_points: Some(vec![ FrictionPoint { - kind: FrictionKind::ToolFailure, + kind: FrictionKind::ToolFailure, description: "Daytona sandbox didn't have cargo on PATH".to_string(), - stage_id: None, + stage_id: None, }, FrictionPoint { - kind: FrictionKind::Timeout, + kind: FrictionKind::Timeout, description: "Proxy timeouts during cold compilations".to_string(), - stage_id: None, + stage_id: None, }, ]), - open_items: Some(vec![OpenItem { - kind: OpenItemKind::TechDebt, + open_items: Some(vec![OpenItem { + kind: OpenItemKind::TechDebt, description: "`ToolApprovalFn` type alias still exists".to_string(), }]), } @@ -834,25 +834,25 @@ mod tests { #[test] fn format_retro_section_empty_stats() { let retro = Retro { - run_id: fixtures::RUN_2, - workflow_name: "test".to_string(), - goal: "test".to_string(), - timestamp: Utc::now(), - smoothness: None, - stages: vec![], - stats: AggregateStats { - total_duration_ms: 0, + run_id: fixtures::RUN_2, + workflow_name: "test".to_string(), + goal: "test".to_string(), + timestamp: Utc::now(), + smoothness: None, + stages: vec![], + stats: AggregateStats { + total_duration_ms: 0, total_billing_usd_micros: None, - total_retries: 0, - files_touched: vec![], - stages_completed: 0, - stages_failed: 0, + total_retries: 0, + files_touched: vec![], + stages_completed: 0, + stages_failed: 0, }, - intent: None, - outcome: None, - learnings: None, + intent: None, + outcome: None, + learnings: None, friction_points: None, - open_items: None, + open_items: None, }; let section = format_retro_section(&retro); @@ -1083,51 +1083,43 @@ 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(); @@ -1156,68 +1148,60 @@ 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(); @@ -1355,7 +1339,7 @@ mod tests { let store = test_store(); let run_store = store.create_run(&fixtures::RUN_1).await.unwrap(); let creds = GitHubAppCredentials { - app_id: "123".to_string(), + app_id: "123".to_string(), private_key_pem: "unused".to_string(), }; let result = maybe_open_pull_request( @@ -1382,58 +1366,50 @@ 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 a67095c3d..402c295c4 100644 --- a/lib/crates/fabro-workflow/src/pipeline/retro.rs +++ b/lib/crates/fabro-workflow/src/pipeline/retro.rs @@ -16,7 +16,7 @@ pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option { tracing::warn!(error = %e, "Could not load run state, skipping retro"); if let Some(ref emitter) = options.emitter { emitter.emit(&Event::RetroFailed { - error: e.to_string(), + error: e.to_string(), duration_ms: 0, }); } @@ -27,7 +27,7 @@ pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option { tracing::warn!("Could not load checkpoint, skipping retro"); if let Some(ref emitter) = options.emitter { emitter.emit(&Event::RetroFailed { - error: "checkpoint not found".to_string(), + error: "checkpoint not found".to_string(), duration_ms: 0, }); } @@ -55,9 +55,9 @@ pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option { let retro_prompt = build_retro_prompt(RETRO_DATA_DIR); if let Some(ref emitter) = options.emitter { emitter.emit(&Event::RetroStarted { - prompt: Some(retro_prompt), + prompt: Some(retro_prompt), provider: Some(options.provider.as_str().to_string()), - model: Some(options.model.clone()), + model: Some(options.model.clone()), }); } @@ -71,10 +71,10 @@ pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option { emitter.touch(); if !event.event.is_streaming_noise() { emitter.emit(&Event::Agent { - stage: "retro".to_string(), - visit: 1, - event: event.event.clone(), - session_id: Some(event.session_id.clone()), + stage: "retro".to_string(), + visit: 1, + event: event.event.clone(), + session_id: Some(event.session_id.clone()), parent_session_id: event.parent_session_id.clone(), }); } @@ -86,7 +86,7 @@ pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option { tracing::warn!(error = %err, "Could not load events from store, skipping retro"); if let Some(ref emitter) = options.emitter { emitter.emit(&Event::RetroFailed { - error: err.to_string(), + error: err.to_string(), duration_ms: 0, }); } @@ -232,71 +232,63 @@ 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 @@ -304,17 +296,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, } } @@ -333,38 +325,35 @@ 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; @@ -387,21 +376,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 308f4fff8..ced3b2571 100644 --- a/lib/crates/fabro-workflow/src/pipeline/transform.rs +++ b/lib/crates/fabro-workflow/src/pipeline/transform.rs @@ -81,15 +81,12 @@ 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 @@ -109,15 +106,12 @@ 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"), @@ -140,15 +134,12 @@ 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!( @@ -187,18 +178,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::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 0e85f6543..e75acd69d 100644 --- a/lib/crates/fabro-workflow/src/pipeline/types.rs +++ b/lib/crates/fabro-workflow/src/pipeline/types.rs @@ -34,7 +34,7 @@ use crate::workflow_bundle::WorkflowBundle; /// Output of the PARSE phase. #[non_exhaustive] pub struct Parsed { - pub graph: Graph, + pub graph: Graph, pub source: String, } @@ -42,7 +42,7 @@ pub struct Parsed { /// post-transform adjustments (e.g. goal override) before validation. #[non_exhaustive] pub struct Transformed { - pub graph: Graph, + pub graph: Graph, pub source: String, } @@ -51,8 +51,8 @@ pub struct Transformed { /// Graph is read-only — use accessors, not direct field access. #[non_exhaustive] pub struct Validated { - graph: Graph, - source: String, + graph: Graph, + source: String, diagnostics: Vec, } @@ -111,7 +111,7 @@ impl Validated { /// Options for the PERSIST phase. pub(crate) struct PersistOptions { - pub run_dir: PathBuf, + pub run_dir: PathBuf, pub run_record: RunRecord, } @@ -120,11 +120,11 @@ pub(crate) struct PersistOptions { #[derive(Debug)] #[non_exhaustive] pub struct Persisted { - graph: Graph, - source: String, + graph: Graph, + source: String, diagnostics: Vec, - run_dir: PathBuf, - run_record: RunRecord, + run_dir: PathBuf, + run_record: RunRecord, } impl Persisted { @@ -210,172 +210,172 @@ 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 devcontainer: Option, - pub git: Option, - pub worktree_mode: Option, + pub run_id: RunId, + pub run_store: RunStoreHandle, + pub dry_run: bool, + pub emitter: Arc, + pub sandbox: SandboxSpec, + pub llm: LlmSpec, + pub interviewer: Arc, + pub lifecycle: LifecycleOptions, + pub run_options: RunOptions, + pub workflow_path: Option, + pub workflow_bundle: Option>, + pub hooks: fabro_hooks::HookSettings, + pub sandbox_env: SandboxEnvSpec, + pub devcontainer: Option, + pub git: Option, + pub worktree_mode: Option, pub registry_override: Option>, - pub artifact_sink: Option, - pub run_control: Option>, - pub checkpoint: Option, - pub seed_context: Option, + pub artifact_sink: Option, + pub run_control: Option>, + pub checkpoint: Option, + pub seed_context: Option, } /// Output of the INITIALIZE phase. #[non_exhaustive] pub struct Initialized { - pub graph: Graph, - pub source: String, - pub inputs: HashMap, - pub run_options: RunOptions, - pub workflow_path: Option, - pub workflow_bundle: Option>, - pub run_store: RunStoreHandle, - pub(crate) checkpoint: Option, + pub graph: Graph, + pub source: String, + pub inputs: HashMap, + pub run_options: RunOptions, + pub workflow_path: Option, + pub workflow_bundle: Option>, + pub run_store: RunStoreHandle, + pub(crate) checkpoint: Option, pub(crate) seed_context: Option, - pub emitter: Arc, - pub sandbox: Arc, - pub registry: Arc, - pub on_node: crate::OnNodeCallback, - pub artifact_sink: Option, - pub run_control: Option>, - pub hook_runner: Option>, - pub env: HashMap, - pub dry_run: bool, - pub llm_client: Option, - pub model: String, - pub provider: Provider, + pub emitter: Arc, + pub sandbox: Arc, + pub registry: Arc, + pub on_node: crate::OnNodeCallback, + pub artifact_sink: Option, + pub run_control: Option>, + pub hook_runner: Option>, + pub env: HashMap, + pub dry_run: bool, + pub llm_client: Option, + pub model: String, + pub provider: Provider, } /// Output of the EXECUTE phase. #[non_exhaustive] pub struct Executed { - pub graph: Graph, - pub outcome: Result, - pub run_options: RunOptions, - pub run_store: RunStoreHandle, - pub hook_runner: Option>, - pub emitter: Arc, - pub sandbox: Arc, - pub duration_ms: u64, + pub graph: Graph, + pub outcome: Result, + pub run_options: RunOptions, + pub run_store: RunStoreHandle, + pub hook_runner: Option>, + pub emitter: Arc, + pub sandbox: Arc, + pub duration_ms: u64, pub final_context: Context, - pub llm_client: Option, - pub model: String, - pub provider: Provider, + pub llm_client: Option, + pub model: String, + pub provider: Provider, } /// Output of the RETRO phase. #[non_exhaustive] pub struct Retroed { - pub graph: Graph, - pub outcome: Result, + pub graph: Graph, + pub outcome: Result, pub run_options: RunOptions, - pub run_store: RunStoreHandle, + pub run_store: RunStoreHandle, pub hook_runner: Option>, - pub emitter: Arc, - pub sandbox: Arc, + pub emitter: Arc, + pub sandbox: Arc, pub duration_ms: u64, - pub retro: Option, + pub retro: Option, } /// Output of the FINALIZE phase. #[non_exhaustive] pub struct Concluded { - pub run_id: RunId, - pub outcome: Result, - pub conclusion: Conclusion, + pub run_id: RunId, + pub outcome: Result, + pub conclusion: Conclusion, pub pushed_branch: Option, - pub graph: Graph, - pub run_options: RunOptions, - pub emitter: Arc, + pub graph: Graph, + pub run_options: RunOptions, + pub emitter: Arc, } /// Output of the PULL_REQUEST phase. #[non_exhaustive] pub struct Finalized { - pub run_id: RunId, - pub outcome: Result, - pub conclusion: Conclusion, + pub run_id: RunId, + pub outcome: Result, + pub conclusion: Conclusion, pub pushed_branch: Option, - pub pr_url: Option, + pub pr_url: Option, } /// Options for the TRANSFORM phase. pub struct TransformOptions { - pub current_dir: Option, - pub file_resolver: Option>, - pub inputs: HashMap, + pub current_dir: Option, + pub file_resolver: Option>, + pub inputs: HashMap, pub custom_transforms: Vec>, } /// Options for the RETRO phase. pub struct RetroOptions { - pub run_id: RunId, - pub run_store: RunStoreHandle, - pub workflow_name: String, - pub goal: String, - pub run_dir: PathBuf, - pub sandbox: Arc, - pub emitter: Option>, - pub failed: bool, + pub run_id: RunId, + pub run_store: RunStoreHandle, + pub workflow_name: String, + pub goal: String, + pub run_dir: PathBuf, + pub sandbox: Arc, + pub emitter: Option>, + pub failed: bool, pub run_duration_ms: u64, - pub enabled: bool, - pub llm_client: Option, - pub provider: Provider, - pub model: String, + pub enabled: bool, + pub llm_client: Option, + pub provider: Provider, + pub model: String, } /// Options for the FINALIZE phase. pub struct FinalizeOptions { - pub run_dir: PathBuf, - pub run_id: RunId, - pub run_store: RunStoreHandle, - pub workflow_name: String, - pub hook_runner: Option>, + pub run_dir: PathBuf, + pub run_id: RunId, + pub run_store: RunStoreHandle, + pub workflow_name: String, + pub hook_runner: Option>, pub preserve_sandbox: bool, - pub last_git_sha: Option, + pub last_git_sha: Option, } /// Options for the PULL_REQUEST phase. pub struct PullRequestOptions { - pub run_dir: PathBuf, - pub run_store: RunStoreHandle, - pub pr_config: Option, + pub run_dir: PathBuf, + pub run_store: RunStoreHandle, + pub pr_config: Option, pub github_app: Option, pub origin_url: Option, - pub model: String, + pub model: String, } diff --git a/lib/crates/fabro-workflow/src/pipeline/validate.rs b/lib/crates/fabro-workflow/src/pipeline/validate.rs index e07cf8747..83c140ac7 100644 --- a/lib/crates/fabro-workflow/src/pipeline/validate.rs +++ b/lib/crates/fabro-workflow/src/pipeline/validate.rs @@ -21,15 +21,12 @@ 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 72b122418..3a92925b6 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 3bb4ca0a3..6a61f1054 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 ec8f573c2..509c3b09f 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 6c1032cce..6a6c0c060 100644 --- a/lib/crates/fabro-workflow/src/run_lookup.rs +++ b/lib/crates/fabro-workflow/src/run_lookup.rs @@ -13,26 +13,26 @@ use crate::run_status::{RunStatus, StatusReason}; #[derive(Debug, Clone)] struct RunLocalState { - dir_name: String, + dir_name: String, start_time_dt: Option>, - end_time: Option>, - path: PathBuf, - is_orphan: bool, + end_time: Option>, + path: PathBuf, + is_orphan: bool, } #[derive(Debug, Clone, Serialize)] pub struct RunInfo { #[serde(skip)] - summary: Option, - pub dir_name: String, + summary: Option, + pub dir_name: String, #[serde(skip)] pub start_time_dt: Option>, #[serde(skip)] - pub end_time: Option>, + pub end_time: Option>, #[serde(skip)] - pub path: PathBuf, + pub path: PathBuf, #[serde(skip)] - pub is_orphan: bool, + pub is_orphan: bool, } impl RunInfo { @@ -170,16 +170,13 @@ 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| { @@ -243,16 +240,13 @@ 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, } } @@ -440,37 +434,29 @@ mod tests { let store = memory_store(); let run_record = sample_run_record(); let run_store = store.create_run(&fixtures::RUN_1).await.unwrap(); - append_event( - &run_store, - &fixtures::RUN_1, - &Event::RunCreated { - run_id: fixtures::RUN_1, - settings: serde_json::to_value(&run_record.settings).unwrap(), - graph: serde_json::to_value(&run_record.graph).unwrap(), - workflow_source: None, - workflow_config: None, - labels: run_record.labels.clone().into_iter().collect(), - run_dir: run_dir.display().to_string(), - working_directory: run_record.working_directory.display().to_string(), - host_repo_path: run_record.host_repo_path.clone(), - repo_origin_url: run_record.repo_origin_url.clone(), - base_branch: run_record.base_branch.clone(), - workflow_slug: run_record.workflow_slug.clone(), - db_prefix: None, - provenance: run_record.provenance.clone(), - manifest_blob: None, - }, - ) + append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated { + run_id: fixtures::RUN_1, + settings: serde_json::to_value(&run_record.settings).unwrap(), + graph: serde_json::to_value(&run_record.graph).unwrap(), + workflow_source: None, + workflow_config: None, + labels: run_record.labels.clone().into_iter().collect(), + run_dir: run_dir.display().to_string(), + working_directory: run_record.working_directory.display().to_string(), + host_repo_path: run_record.host_repo_path.clone(), + repo_origin_url: run_record.repo_origin_url.clone(), + base_branch: run_record.base_branch.clone(), + workflow_slug: run_record.workflow_slug.clone(), + db_prefix: None, + provenance: run_record.provenance.clone(), + manifest_blob: None, + }) .await .unwrap(); - append_event( - &run_store, - &fixtures::RUN_1, - &Event::RunSubmitted { - reason: None, - definition_blob: None, - }, - ) + append_event(&run_store, &fixtures::RUN_1, &Event::RunSubmitted { + reason: None, + definition_blob: None, + }) .await .unwrap(); diff --git a/lib/crates/fabro-workflow/src/runtime_store.rs b/lib/crates/fabro-workflow/src/runtime_store.rs index a30b77d9c..df8ba9c05 100644 --- a/lib/crates/fabro-workflow/src/runtime_store.rs +++ b/lib/crates/fabro-workflow/src/runtime_store.rs @@ -131,18 +131,18 @@ mod tests { fn test_run_record() -> RunRecord { RunRecord { - run_id: fixtures::RUN_1, - settings: SettingsLayer::default(), - graph: Graph::new("test"), - workflow_slug: Some("test".to_string()), + run_id: fixtures::RUN_1, + settings: SettingsLayer::default(), + graph: Graph::new("test"), + workflow_slug: Some("test".to_string()), working_directory: PathBuf::from("/tmp/test"), - host_repo_path: None, - repo_origin_url: None, - base_branch: None, - labels: HashMap::new(), - provenance: None, - manifest_blob: None, - definition_blob: None, + host_repo_path: None, + repo_origin_url: None, + base_branch: None, + labels: HashMap::new(), + provenance: None, + manifest_blob: None, + definition_blob: None, } } @@ -150,27 +150,23 @@ 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(); @@ -188,20 +184,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 11bb54495..468c839e0 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 d9d480be5..c65e42082 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, } @@ -71,37 +71,33 @@ 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); @@ -162,8 +158,8 @@ pub async fn run_graph( run_options, InitializedOptions { hook_runner: None, - env: HashMap::new(), - checkpoint: None, + env: HashMap::new(), + checkpoint: None, }, ) .await; @@ -186,8 +182,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; @@ -219,8 +215,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; @@ -245,8 +241,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; @@ -277,8 +273,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; @@ -302,8 +298,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; @@ -320,8 +316,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 50a19ee3a..c63509b7a 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 da2a85026..81b6db2bc 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 c65c3db7e..3549177d3 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 fd7afe078..ba5cfb643 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 d879466fb..deb9fc2e1 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 2caf8cccc..07dadfa77 100644 --- a/lib/crates/fabro-workflow/tests/it/daytona_integration.rs +++ b/lib/crates/fabro-workflow/tests/it/daytona_integration.rs @@ -351,10 +351,10 @@ async fn daytona_snapshot_sandbox() { let config = DaytonaConfig { auto_stop_interval: Some(60), snapshot: Some(DaytonaSnapshotConfig { - name: "fabro-test-snapshot".to_string(), - cpu: Some(2), - memory: Some(4), - disk: Some(10), + name: "fabro-test-snapshot".to_string(), + cpu: Some(2), + memory: Some(4), + disk: Some(10), dockerfile: Some(fabro_sandbox::daytona::DockerfileSource::Inline( "FROM ubuntu:22.04\nRUN apt-get update && apt-get install -y ripgrep".to_string(), )), @@ -501,17 +501,17 @@ async fn daytona_pipeline_artifact_offload_and_sync() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), env.clone()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = engine .run(&graph, &run_options) @@ -679,19 +679,19 @@ async fn daytona_git_checkpoint_remote_emits_events() { let engine = WorkflowRunner::new(registry, Arc::new(emitter), env.clone()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("git-cp-test"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("git-cp-test"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: Some(dir.path().to_path_buf()), - git: Some(GitCheckpointOptions { - base_sha: Some(base_sha), - run_branch: Some(branch_name), + host_repo_path: Some(dir.path().to_path_buf()), + git: Some(GitCheckpointOptions { + base_sha: Some(base_sha), + run_branch: Some(branch_name), meta_branch: None, }), }; @@ -861,8 +861,8 @@ async fn daytona_parallel_git_branching_e2e() { display_base_sha: None, host_repo_path: Some(run_tmp.path().to_path_buf()), git: Some(GitCheckpointOptions { - base_sha: Some(base_sha), - run_branch: Some(branch_name), + base_sha: Some(base_sha), + run_branch: Some(branch_name), meta_branch: None, }), }; @@ -980,10 +980,10 @@ async fn run_daytona_cli_test(provider: Provider, model: &str, install_command: let creds = load_github_app_credentials(); let config = DaytonaConfig { snapshot: Some(DaytonaSnapshotConfig { - name: "daytona-medium".into(), - cpu: None, - memory: None, - disk: None, + name: "daytona-medium".into(), + cpu: None, + memory: None, + disk: None, dockerfile: None, }), ..DaytonaConfig::default() @@ -1210,8 +1210,8 @@ async fn daytona_git_checkpoint_with_shadow_branch() { display_base_sha: None, host_repo_path: Some(host_repo.path().to_path_buf()), git: Some(GitCheckpointOptions { - base_sha: Some(base_sha), - run_branch: Some(branch_name), + base_sha: Some(base_sha), + run_branch: Some(branch_name), meta_branch: Some(meta_branch), }), }; @@ -1333,7 +1333,7 @@ async fn daytona_asset_collection() { graph.edges.push(Edge::new("create_assets", "exit")); let run_options = RunOptions { - settings: SettingsLayer { + settings: SettingsLayer { run: Some(RunLayer { artifacts: Some(RunArtifactsLayer { include: vec!["test-results/**".to_string()], @@ -1342,16 +1342,16 @@ async fn daytona_asset_collection() { }), ..SettingsLayer::default() }, - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("artifact-test-daytona"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("artifact-test-daytona"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = engine .run(&graph, &run_options) @@ -1611,8 +1611,8 @@ async fn daytona_git_push_run_branch_to_origin() { display_base_sha: None, host_repo_path: Some(dir.path().to_path_buf()), git: Some(GitCheckpointOptions { - base_sha: Some(base_sha), - run_branch: Some(branch_name.clone()), + base_sha: Some(base_sha), + run_branch: Some(branch_name.clone()), meta_branch: None, }), }; @@ -1815,11 +1815,11 @@ async fn daytona_cp_upload_download_round_trip() { // 2. Build a SandboxRecord (same as `fabro run` would persist) let record = SandboxRecord { - provider: "daytona".to_string(), - working_directory: env.working_directory().to_string(), - identifier: Some(sandbox_name.clone()), + provider: "daytona".to_string(), + working_directory: env.working_directory().to_string(), + identifier: Some(sandbox_name.clone()), host_working_directory: None, - container_mount_point: None, + container_mount_point: None, }; // 3. Reconnect via the real cp::reconnect path @@ -1891,10 +1891,10 @@ async fn daytona_computer_use_browser_screenshot() { use base64::Engine; let config = DaytonaConfig { snapshot: Some(DaytonaSnapshotConfig { - name: "daytona-medium".into(), - cpu: None, - memory: None, - disk: None, + name: "daytona-medium".into(), + cpu: None, + memory: None, + disk: None, dockerfile: None, }), skip_clone: true, @@ -2045,10 +2045,10 @@ async fn daytona_playwright_mcp_sandbox_transport() { // Create sandbox from daytona-medium (has Node.js + Chromium) let config = DaytonaConfig { snapshot: Some(DaytonaSnapshotConfig { - name: "daytona-medium".into(), - cpu: None, - memory: None, - disk: None, + name: "daytona-medium".into(), + cpu: None, + memory: None, + disk: None, dockerfile: None, }), skip_clone: true, @@ -2090,8 +2090,8 @@ async fn daytona_playwright_mcp_sandbox_transport() { // 2. Start the Playwright MCP server via the sandbox transport resolution path let mcp_port = 3100u16; let mcp_config = fabro_mcp::config::McpServerSettings { - name: "playwright".into(), - transport: fabro_mcp::config::McpTransport::Sandbox { + name: "playwright".into(), + transport: fabro_mcp::config::McpTransport::Sandbox { command: vec![ "npx".into(), "@playwright/mcp@latest".into(), @@ -2101,11 +2101,11 @@ async fn daytona_playwright_mcp_sandbox_transport() { "--browser".into(), "chromium".into(), ], - port: mcp_port, - env: std::collections::HashMap::new(), + port: mcp_port, + env: std::collections::HashMap::new(), }, startup_timeout_secs: 30, - tool_timeout_secs: 120, + tool_timeout_secs: 120, }; // Resolve the sandbox transport: start the server, get preview URL, rewrite to @@ -2158,10 +2158,10 @@ async fn daytona_playwright_mcp_sandbox_transport() { eprintln!("Preview URL: {url}"); fabro_mcp::config::McpServerSettings { - name: mcp_config.name.clone(), - transport: fabro_mcp::config::McpTransport::Http { url, headers }, + name: mcp_config.name.clone(), + transport: fabro_mcp::config::McpTransport::Http { url, headers }, startup_timeout_secs: mcp_config.startup_timeout_secs, - tool_timeout_secs: mcp_config.tool_timeout_secs, + tool_timeout_secs: mcp_config.tool_timeout_secs, } } _ => unreachable!(), diff --git a/lib/crates/fabro-workflow/tests/it/git_integration.rs b/lib/crates/fabro-workflow/tests/it/git_integration.rs index 1a4241f33..8d8f587e2 100644 --- a/lib/crates/fabro-workflow/tests/it/git_integration.rs +++ b/lib/crates/fabro-workflow/tests/it/git_integration.rs @@ -157,17 +157,17 @@ fn make_registry() -> HandlerRegistry { fn test_run_options(run_dir: &Path) -> RunOptions { RunOptions { - run_dir: run_dir.to_path_buf(), - cancel_token: None, - run_id: fixtures::RUN_2, - settings: SettingsLayer::default(), - git: None, - host_repo_path: None, - labels: HashMap::new(), - github_app: None, - base_branch: None, + run_dir: run_dir.to_path_buf(), + cancel_token: None, + run_id: fixtures::RUN_2, + settings: SettingsLayer::default(), + git: None, + host_repo_path: None, + labels: HashMap::new(), + github_app: None, + base_branch: None, display_base_sha: None, - workflow_slug: None, + workflow_slug: None, } } @@ -287,8 +287,8 @@ async fn git_checkpoint_skips_start_node() { let mut run_options = test_run_options(run_tmp.path()); run_options.git = Some(GitCheckpointOptions { - base_sha: Some(base_sha), - run_branch: None, + base_sha: Some(base_sha), + run_branch: None, meta_branch: Some(MetadataStore::branch_name(&fixtures::RUN_2.to_string())), }); run_options.host_repo_path = Some(PathBuf::from(repo)); diff --git a/lib/crates/fabro-workflow/tests/it/integration.rs b/lib/crates/fabro-workflow/tests/it/integration.rs index 2514138d1..efa258767 100644 --- a/lib/crates/fabro-workflow/tests/it/integration.rs +++ b/lib/crates/fabro-workflow/tests/it/integration.rs @@ -337,17 +337,17 @@ async fn end_to_end_linear_pipeline() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -466,17 +466,17 @@ async fn end_to_end_branching_pipeline() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -571,9 +571,9 @@ async fn end_to_end_human_gate_pipeline() { // Pre-fill the queue with an answer selecting "R" let answers = VecDeque::from([Answer { - value: AnswerValue::Selected("R".to_string()), + value: AnswerValue::Selected("R".to_string()), selected_option: None, - text: None, + text: None, }]); let interviewer = Arc::new(QueueInterviewer::new(answers)); @@ -585,17 +585,17 @@ async fn end_to_end_human_gate_pipeline() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -680,17 +680,17 @@ async fn human_gate_interrupted_input_fails_closed_without_fail_route() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -790,17 +790,17 @@ async fn human_gate_interrupted_input_routes_via_outcome_fail_condition() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -903,17 +903,17 @@ async fn goal_gate_routes_to_retry_target_on_failure() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let result = engine.run(&graph, &run_options).await; assert!( @@ -1023,17 +1023,17 @@ async fn goal_gate_routes_to_retry_target_when_present() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -1337,17 +1337,17 @@ async fn retry_on_failure_then_succeed() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = engine .run(&graph, &run_options) @@ -1411,17 +1411,17 @@ async fn pipeline_with_many_nodes() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -1502,13 +1502,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, }) } @@ -1574,7 +1574,7 @@ impl Handler for LargeOutputHandler { #[derive(Clone)] struct ContextValueCaptureHandler { values: Arc>>, - key: String, + key: String, } #[async_trait::async_trait] @@ -1757,17 +1757,17 @@ async fn smoke_test_with_mock_codergen_backend() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -1858,17 +1858,17 @@ async fn end_to_end_parallel_fan_out_fan_in() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -1970,17 +1970,17 @@ async fn resume_from_checkpoint_completes_pipeline() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_from_checkpoint_with_state(&graph, &run_options, &checkpoint) @@ -2068,17 +2068,17 @@ async fn resume_from_checkpoint_preserves_goal_gate_outcomes() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; // This should succeed because goal gate for gated_work is satisfied // via restored outcomes @@ -2110,17 +2110,17 @@ async fn graph_goal_in_context() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (_outcome, state) = engine .run_with_state(&graph, &run_options) @@ -2148,17 +2148,17 @@ async fn event_streaming_lifecycle() { let events = collect_events(&emitter); let engine = WorkflowRunner::new(make_linear_registry(), Arc::new(emitter), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; engine.run(&graph, &run_options).await.expect("run"); @@ -2227,17 +2227,17 @@ async fn context_flow_between_stages() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (_outcome, state) = engine .run_with_state(&graph, &run_options) @@ -2282,17 +2282,17 @@ async fn tool_handler_e2e() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, _state) = engine .run_with_state(&graph, &run_options) @@ -2356,17 +2356,17 @@ async fn auto_approve_interviewer_e2e() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, _state) = engine .run_with_state(&graph, &run_options) @@ -2395,17 +2395,17 @@ async fn codergen_without_backend_simulated() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (_outcome, state) = engine .run_with_state(&graph, &run_options) @@ -2499,17 +2499,17 @@ async fn branching_loop_back_on_failure() { ); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, _state) = engine .run_with_state(&graph, &run_options) @@ -2565,14 +2565,14 @@ async fn human_gate_loops_back() { let answers = VecDeque::from([ Answer { - value: AnswerValue::Selected("F".to_string()), + value: AnswerValue::Selected("F".to_string()), selected_option: None, - text: None, + text: None, }, Answer { - value: AnswerValue::Selected("A".to_string()), + value: AnswerValue::Selected("A".to_string()), selected_option: None, - text: None, + text: None, }, ]); let interviewer = Arc::new(QueueInterviewer::new(answers)); @@ -2584,17 +2584,17 @@ async fn human_gate_loops_back() { registry.register("human", Box::new(HumanHandler::new(interviewer))); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, _state) = engine .run_with_state(&graph, &run_options) @@ -2648,17 +2648,17 @@ async fn scenario_ship_a_feature() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, _state) = engine .run_with_state(&graph, &run_options) @@ -2732,17 +2732,17 @@ async fn scenario_parallel_expert_review() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -2818,17 +2818,17 @@ async fn scenario_node_retries_on_retry_status() { ); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -2882,17 +2882,17 @@ async fn scenario_loop_restart_resets_context() { ); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = engine.run(&graph, &run_options).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -2949,17 +2949,17 @@ async fn scenario_bug_triage_router() { registry.register("conditional", Box::new(ConditionalHandler)); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3010,17 +3010,17 @@ async fn scenario_crash_recovery() { registry.register("exit", Box::new(ExitHandler)); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_from_checkpoint_with_state(&graph, &run_options, &checkpoint) @@ -3119,17 +3119,17 @@ async fn manager_loop_stop_condition_satisfied_e2e() { registry.register("stack.manager_loop", Box::new(SubWorkflowHandler)); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3200,17 +3200,17 @@ async fn manager_loop_max_cycles_exceeded_e2e() { registry.register("stack.manager_loop", Box::new(SubWorkflowHandler)); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3340,17 +3340,17 @@ async fn conditional_branching_success_fail_paths() { registry.register("always_fail", Box::new(AlwaysFailHandler)); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3395,17 +3395,17 @@ async fn edge_selection_condition_match_wins_over_weight() { registry.register("exit", Box::new(ExitHandler)); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (_outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3444,17 +3444,17 @@ async fn edge_selection_weight_breaks_ties() { registry.register("exit", Box::new(ExitHandler)); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (_outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3485,17 +3485,17 @@ async fn edge_selection_lexical_tiebreak() { registry.register("exit", Box::new(ExitHandler)); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (_outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3545,17 +3545,17 @@ async fn context_updates_visible_across_nodes() { registry.register("context_setter", Box::new(ContextSetterHandler)); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (_outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3591,17 +3591,17 @@ async fn stylesheet_applies_model_override() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = engine.run(&graph, &run_options).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -3646,17 +3646,17 @@ async fn custom_handler_registration_and_execution() { registry.register("my_custom", Box::new(CustomHandler)); let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (_outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3723,17 +3723,17 @@ async fn integration_smoke_plan_implement_review_done() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3814,17 +3814,17 @@ async fn manager_loop_runs_child_engine_e2e() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -3947,17 +3947,17 @@ async fn manager_loop_context_flows_e2e() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let (outcome, state) = engine .run_with_state(&graph, &run_options) @@ -4022,17 +4022,17 @@ async fn manager_loop_child_dotfile_e2e() { let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = engine.run(&graph, &run_options).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -4081,17 +4081,14 @@ 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 @@ -4128,17 +4125,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) @@ -4200,7 +4197,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 { @@ -4208,7 +4205,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())), } } } @@ -4282,17 +4279,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"); @@ -4338,17 +4335,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"); @@ -4390,17 +4387,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"); @@ -4448,17 +4445,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"); @@ -4496,17 +4493,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"); @@ -4554,17 +4551,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"); @@ -4625,17 +4622,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"); @@ -4691,17 +4688,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"); @@ -4757,17 +4754,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"); @@ -4816,17 +4813,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"); @@ -4886,17 +4883,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"); @@ -4966,17 +4963,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) @@ -5062,17 +5059,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) @@ -5145,17 +5142,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) @@ -5186,17 +5183,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) @@ -5278,17 +5275,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"); @@ -5345,17 +5342,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"); @@ -5419,17 +5416,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) @@ -5485,17 +5482,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) @@ -5556,17 +5553,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"); @@ -5609,17 +5606,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"); @@ -5665,17 +5662,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"); @@ -5722,17 +5719,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"); @@ -5789,17 +5786,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"); @@ -5837,17 +5834,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) @@ -5909,17 +5906,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"); @@ -5995,17 +5992,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) @@ -6046,8 +6043,8 @@ mod real_llm { use fabro_workflow::handler::agent::{AgentHandler, CodergenBackend, CodergenResult}; struct LlmCodergenBackend { - client: Arc, - model: String, + client: Arc, + model: String, provider: String, } @@ -6079,19 +6076,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 @@ -6100,9 +6097,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, }) } @@ -6231,17 +6228,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), @@ -6339,17 +6336,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), @@ -6471,17 +6468,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), @@ -6571,17 +6568,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), @@ -6664,17 +6661,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) @@ -6780,9 +6777,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)); @@ -6794,17 +6791,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) @@ -6910,17 +6907,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) @@ -7021,9 +7018,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))); @@ -7037,17 +7034,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) @@ -7129,9 +7126,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))); @@ -7145,17 +7142,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) @@ -7381,7 +7378,7 @@ fn hook_runner_from_defs(hooks: Vec) -> Arc, + emitter: Arc, hook_runner: Arc, } @@ -7427,7 +7424,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), } } @@ -7447,17 +7444,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, } } @@ -8042,26 +8039,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), }], }; @@ -8077,26 +8074,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), }], }; @@ -8385,17 +8382,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) @@ -8586,17 +8583,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) @@ -8645,16 +8642,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()), } } @@ -8790,17 +8787,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) @@ -8871,23 +8868,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) @@ -8957,24 +8954,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) @@ -9094,17 +9091,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) @@ -9139,25 +9136,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(), } } @@ -9244,10 +9241,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, }); } @@ -9255,10 +9252,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, }); } @@ -9266,10 +9263,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, }); } @@ -9277,10 +9274,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, }); } @@ -9288,20 +9285,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, }) } @@ -9537,48 +9534,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, }) } @@ -9964,17 +9961,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) @@ -10082,17 +10079,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) @@ -10277,19 +10274,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, }), }; @@ -10455,8 +10452,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), }), }; @@ -10654,8 +10651,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, }), }; @@ -10892,19 +10889,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, }), }; @@ -11033,7 +11030,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] @@ -11262,17 +11259,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"); @@ -11308,17 +11305,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()); @@ -11347,17 +11344,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()); @@ -11393,17 +11390,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()); @@ -11432,17 +11429,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!( @@ -11494,17 +11491,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. @@ -11557,17 +11554,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(); @@ -11608,23 +11605,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); @@ -11741,17 +11738,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()); @@ -11801,23 +11798,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!( @@ -11902,17 +11899,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!( @@ -11936,8 +11933,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 { @@ -11999,17 +11996,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!( @@ -12038,17 +12035,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!( @@ -12077,17 +12074,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!( @@ -12116,17 +12113,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"); @@ -12152,17 +12149,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!( @@ -12192,17 +12189,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!( @@ -12237,7 +12234,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] @@ -12254,12 +12251,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()) @@ -12299,17 +12296,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"); @@ -12348,23 +12345,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) @@ -12399,17 +12396,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) @@ -12464,17 +12461,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; @@ -12596,7 +12593,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()], @@ -12605,16 +12602,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) @@ -12728,7 +12725,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()], @@ -12737,16 +12734,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) @@ -12833,7 +12830,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()], @@ -12842,16 +12839,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) @@ -12909,17 +12906,17 @@ async fn wait_timer_e2e() { local_env(), ); let run_options = RunOptions { - settings: SettingsLayer::default(), - run_dir: dir.path().to_path_buf(), - cancel_token: None, - run_id: test_run_id("test-run"), - labels: std::collections::HashMap::new(), - workflow_slug: None, - github_app: None, - base_branch: None, + settings: SettingsLayer::default(), + run_dir: dir.path().to_path_buf(), + cancel_token: None, + run_id: test_run_id("test-run"), + labels: std::collections::HashMap::new(), + workflow_slug: None, + github_app: None, + base_branch: None, display_base_sha: None, - host_repo_path: None, - git: None, + host_repo_path: None, + git: None, }; let outcome = engine.run(&graph, &run_options).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); diff --git a/test/twin/github/src/fixtures.rs b/test/twin/github/src/fixtures.rs index ba67c9ab6..bd341b559 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,15 +233,12 @@ 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(); @@ -249,38 +246,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(), @@ -291,12 +288,9 @@ 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::>(); @@ -305,18 +299,15 @@ 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::>(); @@ -325,12 +316,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, }; @@ -376,12 +367,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 58212dcde..ec67fe395 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 a4a21b420..46b1aacc2 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 9b2d292d1..d4f4985d6 100644 --- a/test/twin/github/src/handlers/graphql.rs +++ b/test/twin/github/src/handlers/graphql.rs @@ -160,7 +160,7 @@ async fn handle_enable_auto_merge( } } pr.auto_merge = Some(AutoMerge { - enabled_at: now.clone(), + enabled_at: now.clone(), merge_method: method.clone(), }); return ( @@ -665,12 +665,12 @@ mod tests { pem: &str, ) -> (TestServer, reqwest::Client, String) { state.register_app(AppOptions { - app_id: "100".to_string(), - slug: "test-app".to_string(), - owner_login: "owner".to_string(), - public: true, + app_id: "100".to_string(), + slug: "test-app".to_string(), + owner_login: "owner".to_string(), + public: true, private_key_pem: pem.to_string(), - webhook_secret: None, + webhook_secret: None, }); state.add_installation("100", "owner", vec!["repo".to_string()], false); state.add_repository("owner", "repo", vec!["main".to_string()], false); @@ -717,23 +717,23 @@ mod tests { .entry(("owner".to_string(), "repo".to_string())) .or_default() .push(PullRequest { - number: 1, - node_id: "PR_test123".to_string(), - title: "Test".to_string(), - body: String::new(), - state: "open".to_string(), - draft: false, - mergeable: true, - additions: 10, - deletions: 5, + number: 1, + node_id: "PR_test123".to_string(), + title: "Test".to_string(), + body: String::new(), + state: "open".to_string(), + draft: false, + mergeable: true, + additions: 10, + deletions: 5, changed_files: 2, - html_url: "https://github.com/owner/repo/pull/1".to_string(), - user_login: "test-bot[bot]".to_string(), - head_ref: "feature".to_string(), - base_ref: "main".to_string(), - created_at: "2026-01-01T00:00:00Z".to_string(), - updated_at: "2026-01-01T00:00:00Z".to_string(), - auto_merge: None, + html_url: "https://github.com/owner/repo/pull/1".to_string(), + user_login: "test-bot[bot]".to_string(), + head_ref: "feature".to_string(), + base_ref: "main".to_string(), + created_at: "2026-01-01T00:00:00Z".to_string(), + updated_at: "2026-01-01T00:00:00Z".to_string(), + auto_merge: None, }); let (server, client, token) = setup_with_token(&mut state, pem).await; @@ -794,38 +794,38 @@ mod tests { let pem = test_rsa_private_key(); let mut state = AppState::new(); state.projects.push(crate::state::Project { - node_id: "PVT_org123".to_string(), - number: 1, - owner: "owner".to_string(), - owner_type: crate::state::OwnerType::Organization, + node_id: "PVT_org123".to_string(), + number: 1, + owner: "owner".to_string(), + owner_type: crate::state::OwnerType::Organization, status_field_id: "PVTSSF_status1".to_string(), - status_options: vec![ + status_options: vec![ crate::state::StatusOption { - id: "opt1".to_string(), + id: "opt1".to_string(), name: "Todo".to_string(), }, crate::state::StatusOption { - id: "opt2".to_string(), + id: "opt2".to_string(), name: "In Progress".to_string(), }, crate::state::StatusOption { - id: "opt3".to_string(), + id: "opt3".to_string(), name: "Done".to_string(), }, ], - items: vec![crate::state::ProjectItem { - id: "PVTI_item1".to_string(), - status: "Todo".to_string(), + items: vec![crate::state::ProjectItem { + id: "PVTI_item1".to_string(), + status: "Todo".to_string(), content: crate::state::IssueContent { - id: "I_issue1".to_string(), - number: 42, - title: "Fix bug".to_string(), - body: "Description".to_string(), - url: "https://github.com/owner/repo/issues/42".to_string(), - created_at: "2026-01-01T00:00:00Z".to_string(), - updated_at: "2026-01-02T00:00:00Z".to_string(), + id: "I_issue1".to_string(), + number: 42, + title: "Fix bug".to_string(), + body: "Description".to_string(), + url: "https://github.com/owner/repo/issues/42".to_string(), + created_at: "2026-01-01T00:00:00Z".to_string(), + updated_at: "2026-01-02T00:00:00Z".to_string(), assignee_ids: vec![], - labels: vec!["bug".to_string()], + labels: vec!["bug".to_string()], }, }], }); @@ -864,25 +864,25 @@ mod tests { let pem = test_rsa_private_key(); let mut state = AppState::new(); state.projects.push(crate::state::Project { - node_id: "PVT_test".to_string(), - number: 1, - owner: "owner".to_string(), - owner_type: crate::state::OwnerType::Organization, + node_id: "PVT_test".to_string(), + number: 1, + owner: "owner".to_string(), + owner_type: crate::state::OwnerType::Organization, status_field_id: "PVTSSF_s".to_string(), - status_options: vec![], - items: vec![crate::state::ProjectItem { - id: "PVTI_1".to_string(), - status: "Todo".to_string(), + status_options: vec![], + items: vec![crate::state::ProjectItem { + id: "PVTI_1".to_string(), + status: "Todo".to_string(), content: crate::state::IssueContent { - id: "I_1".to_string(), - number: 1, - title: "Issue 1".to_string(), - body: "Body".to_string(), - url: "https://github.com/owner/repo/issues/1".to_string(), - created_at: "2026-01-01T00:00:00Z".to_string(), - updated_at: "2026-01-01T00:00:00Z".to_string(), + id: "I_1".to_string(), + number: 1, + title: "Issue 1".to_string(), + body: "Body".to_string(), + url: "https://github.com/owner/repo/issues/1".to_string(), + created_at: "2026-01-01T00:00:00Z".to_string(), + updated_at: "2026-01-01T00:00:00Z".to_string(), assignee_ids: vec!["U_user1".to_string()], - labels: vec!["bug".to_string(), "urgent".to_string()], + labels: vec!["bug".to_string(), "urgent".to_string()], }, }], }); @@ -948,34 +948,34 @@ mod tests { let pem = test_rsa_private_key(); let mut state = AppState::new(); state.projects.push(crate::state::Project { - node_id: "PVT_test".to_string(), - number: 1, - owner: "owner".to_string(), - owner_type: crate::state::OwnerType::Organization, + node_id: "PVT_test".to_string(), + number: 1, + owner: "owner".to_string(), + owner_type: crate::state::OwnerType::Organization, status_field_id: "PVTSSF_s".to_string(), - status_options: vec![ + status_options: vec![ crate::state::StatusOption { - id: "opt1".to_string(), + id: "opt1".to_string(), name: "Todo".to_string(), }, crate::state::StatusOption { - id: "opt2".to_string(), + id: "opt2".to_string(), name: "Done".to_string(), }, ], - items: vec![crate::state::ProjectItem { - id: "PVTI_1".to_string(), - status: "Todo".to_string(), + items: vec![crate::state::ProjectItem { + id: "PVTI_1".to_string(), + status: "Todo".to_string(), content: crate::state::IssueContent { - id: "I_1".to_string(), - number: 1, - title: "Issue 1".to_string(), - body: String::new(), - url: "https://github.com/owner/repo/issues/1".to_string(), - created_at: "2026-01-01T00:00:00Z".to_string(), - updated_at: "2026-01-01T00:00:00Z".to_string(), + id: "I_1".to_string(), + number: 1, + title: "Issue 1".to_string(), + body: String::new(), + url: "https://github.com/owner/repo/issues/1".to_string(), + created_at: "2026-01-01T00:00:00Z".to_string(), + updated_at: "2026-01-01T00:00:00Z".to_string(), assignee_ids: vec![], - labels: vec![], + labels: vec![], }, }], }); diff --git a/test/twin/github/src/handlers/installations.rs b/test/twin/github/src/handlers/installations.rs index 91bd6ca2e..9422e2a0c 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 a9fca6a6d..c7711078a 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 d1dbbc959..79041a121 100644 --- a/test/twin/github/src/handlers/pulls.rs +++ b/test/twin/github/src/handlers/pulls.rs @@ -344,12 +344,12 @@ mod tests { pem: &str, ) -> (TestServer, reqwest::Client, String) { state.register_app(AppOptions { - app_id: "100".to_string(), - slug: "test-app".to_string(), - owner_login: "owner".to_string(), - public: true, + app_id: "100".to_string(), + slug: "test-app".to_string(), + owner_login: "owner".to_string(), + public: true, private_key_pem: pem.to_string(), - webhook_secret: None, + webhook_secret: None, }); state.add_installation("100", "owner", vec!["repo".to_string()], false); state.add_repository( diff --git a/test/twin/github/src/server.rs b/test/twin/github/src/server.rs index d06d38cf7..89fc93416 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 cd6b057b1..0104a8b3a 100644 --- a/test/twin/github/src/state.rs +++ b/test/twin/github/src/state.rs @@ -8,18 +8,18 @@ const TEST_RSA_PUBLIC_PEM: &str = include_str!("testdata/rsa_public.pem"); /// Configuration for a registered GitHub App (user-facing input). #[derive(Debug, Clone)] pub struct AppOptions { - pub app_id: String, - pub slug: String, - pub owner_login: String, - pub public: bool, + pub app_id: String, + pub slug: String, + pub owner_login: String, + pub public: bool, pub private_key_pem: String, - pub webhook_secret: Option, + pub webhook_secret: Option, } /// Internal enriched app config with derived public key. #[derive(Debug, Clone)] pub struct RegisteredApp { - pub config: AppOptions, + pub config: AppOptions, /// Derived from `private_key_pem` during `register_app`. Used for JWT /// verification. pub public_key_pem: String, @@ -28,62 +28,62 @@ pub struct RegisteredApp { /// An installation of a GitHub App on a specific owner. #[derive(Debug, Clone)] pub struct Installation { - pub id: u64, - pub app_id: String, - pub owner: String, + pub id: u64, + pub app_id: String, + pub owner: String, pub repositories: Vec, - pub suspended: bool, + pub suspended: bool, } /// A repository in the fake. #[derive(Debug, Clone)] pub struct Repository { - pub owner: String, - pub name: String, - pub branches: Vec, + pub owner: String, + pub name: String, + pub branches: Vec, pub default_branch: String, - pub private: bool, - pub git_dir: Option, + pub private: bool, + pub git_dir: Option, } /// A pull request. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PullRequest { - pub number: u64, - pub node_id: String, - pub title: String, - pub body: String, - pub state: String, - pub draft: bool, - pub mergeable: bool, - pub additions: u64, - pub deletions: u64, + pub number: u64, + pub node_id: String, + pub title: String, + pub body: String, + pub state: String, + pub draft: bool, + pub mergeable: bool, + pub additions: u64, + pub deletions: u64, pub changed_files: u64, - pub html_url: String, - pub user_login: String, - pub head_ref: String, - pub base_ref: String, - pub created_at: String, - pub updated_at: String, - pub auto_merge: Option, + pub html_url: String, + pub user_login: String, + pub head_ref: String, + pub base_ref: String, + pub created_at: String, + pub updated_at: String, + pub auto_merge: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AutoMerge { - pub enabled_at: String, + pub enabled_at: String, pub merge_method: String, } /// A GitHub Projects V2 project. #[derive(Debug, Clone)] pub struct Project { - pub node_id: String, - pub number: u64, - pub owner: String, - pub owner_type: OwnerType, + pub node_id: String, + pub number: u64, + pub owner: String, + pub owner_type: OwnerType, pub status_field_id: String, - pub status_options: Vec, - pub items: Vec, + pub status_options: Vec, + pub items: Vec, } #[derive(Debug, Clone, PartialEq)] @@ -94,28 +94,28 @@ pub enum OwnerType { #[derive(Debug, Clone)] pub struct StatusOption { - pub id: String, + pub id: String, pub name: String, } #[derive(Debug, Clone)] pub struct ProjectItem { - pub id: String, - pub status: String, + pub id: String, + pub status: String, pub content: IssueContent, } #[derive(Debug, Clone)] pub struct IssueContent { - pub id: String, - pub number: u64, - pub title: String, - pub body: String, - pub url: String, - pub created_at: String, - pub updated_at: String, + pub id: String, + pub number: u64, + pub title: String, + pub body: String, + pub url: String, + pub created_at: String, + pub updated_at: String, pub assignee_ids: Vec, - pub labels: Vec, + pub labels: Vec, } /// A release. @@ -127,26 +127,26 @@ pub struct Release { /// An app manifest conversion record. #[derive(Debug, Clone)] pub struct ManifestConversion { - pub code: String, - pub app_id: i64, - pub slug: String, - pub client_id: String, - pub client_secret: String, + pub code: String, + pub app_id: i64, + pub slug: String, + pub client_id: String, + pub client_secret: String, pub webhook_secret: Option, - pub pem: String, + pub pem: String, } /// A comment on an issue. #[derive(Debug, Clone)] pub struct Comment { pub issue_node_id: String, - pub body: String, + pub body: String, } /// Stores webhook configuration. #[derive(Debug, Clone, Default)] pub struct WebhookOptions { - pub url: Option, + pub url: Option, pub content_type: Option, } @@ -178,18 +178,18 @@ pub enum TokenPermission { #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct TokenPermissions { - pub contents: PermissionLevel, - pub pull_requests: PermissionLevel, - pub issues: PermissionLevel, + pub contents: PermissionLevel, + pub pull_requests: PermissionLevel, + pub issues: PermissionLevel, pub organization_projects: PermissionLevel, } impl TokenPermissions { pub fn from_json(value: &serde_json::Value) -> Self { Self { - contents: PermissionLevel::from_json(value.get("contents")), - pull_requests: PermissionLevel::from_json(value.get("pull_requests")), - issues: PermissionLevel::from_json(value.get("issues")), + contents: PermissionLevel::from_json(value.get("contents")), + pull_requests: PermissionLevel::from_json(value.get("pull_requests")), + issues: PermissionLevel::from_json(value.get("issues")), organization_projects: PermissionLevel::from_json(value.get("organization_projects")), } } @@ -207,10 +207,10 @@ impl TokenPermissions { /// Info about an active installation access token. #[derive(Debug, Clone)] pub struct TokenInfo { - pub app_id: String, + pub app_id: String, pub installation_id: u64, - pub repositories: Vec, - pub permissions: serde_json::Value, + pub repositories: Vec, + pub permissions: serde_json::Value, } impl TokenInfo { @@ -230,19 +230,19 @@ impl TokenInfo { /// Central in-memory state for the fake GitHub server. #[derive(Debug, Clone)] pub struct AppState { - pub apps: HashMap, - pub installations: Vec, - pub repositories: Vec, - pub pull_requests: HashMap<(String, String), Vec>, - pub active_tokens: HashMap, - pub projects: Vec, - pub releases: HashMap<(String, String), Release>, + pub apps: HashMap, + pub installations: Vec, + pub repositories: Vec, + pub pull_requests: HashMap<(String, String), Vec>, + pub active_tokens: HashMap, + pub projects: Vec, + pub releases: HashMap<(String, String), Release>, pub manifest_conversions: HashMap, - pub comments: Vec, - pub webhook_config: WebhookOptions, + pub comments: Vec, + pub webhook_config: WebhookOptions, pub next_installation_id: u64, - pub next_pr_number: u64, - pub viewer_id: String, + pub next_pr_number: u64, + pub viewer_id: String, } /// Derive the RSA public key PEM from a private key PEM using the openssl CLI. @@ -279,32 +279,29 @@ 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( @@ -361,15 +358,12 @@ 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 } @@ -471,12 +465,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 73598f117..1bb77929a 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 045597887..8ad78f60f 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 ad011a195..56359ecd6 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 0d0455ce7..054078ebe 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 2c0b812d8..59d8fd642 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 01528b0db..1edc6cd8b 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 92eedca74..45246ddcc 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 b8ca1fed2..d456ab6c5 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 ead0d99fe..f9dff9743 100644 --- a/test/twin/openai/tests/common/mod.rs +++ b/test/twin/openai/tests/common/mod.rs @@ -15,50 +15,50 @@ use tokio::net::{TcpListener, TcpStream}; use twin_openai::config::Config; pub struct TestServer { - pub base_url: String, - pub client: Client, - pub auth_client: Client, + pub base_url: String, + pub client: Client, + pub auth_client: Client, pub bearer_token: String, } #[derive(Clone)] pub struct ApiClient { pub base_url: String, - client: Client, + client: Client, bearer_token: Option, organization: Option, - project: Option, + project: Option, } pub struct RecordedResponse { - pub status: reqwest::StatusCode, + pub status: reqwest::StatusCode, pub headers: HashMap, - pub body: Vec, + pub body: Vec, } pub struct RawStreamResponse { - pub status: u16, + pub status: u16, pub headers: HashMap, - pub body: Vec, + pub body: Vec, } pub struct TimedStreamResponse { - pub status: reqwest::StatusCode, + pub status: reqwest::StatusCode, pub first_event_elapsed: Duration, - pub chunks: Vec, + pub chunks: Vec, } #[derive(Debug)] pub struct ParsedSseTranscript { pub blocks: Vec, pub events: Vec, - pub done: bool, + pub done: bool, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct ParsedSseEvent { pub event: Option, - pub data: String, + pub data: String, } static NEXT_BEARER_TOKEN: AtomicU64 = AtomicU64::new(1); @@ -71,7 +71,7 @@ pub async fn spawn_server() -> Result { let listener = TcpListener::bind("127.0.0.1:0").await?; let addr: SocketAddr = listener.local_addr()?; let app = twin_openai::build_app_with_config(Config { - bind_addr: "127.0.0.1:0".parse().expect("valid addr"), + bind_addr: "127.0.0.1:0".parse().expect("valid addr"), require_auth: true, enable_admin: true, }); diff --git a/test/twin/openai/tests/debug_ui.rs b/test/twin/openai/tests/debug_ui.rs index 05596c24a..d5f0741c2 100644 --- a/test/twin/openai/tests/debug_ui.rs +++ b/test/twin/openai/tests/debug_ui.rs @@ -216,7 +216,7 @@ async fn debug_routes_not_accessible_when_admin_disabled() { .expect("bind should succeed"); let addr = listener.local_addr().expect("should have addr"); let app = twin_openai::build_app_with_config(Config { - bind_addr: "127.0.0.1:0".parse().expect("valid addr"), + bind_addr: "127.0.0.1:0".parse().expect("valid addr"), require_auth: false, enable_admin: false, }); diff --git a/test/twin/openai/tests/live_openai_contract.rs b/test/twin/openai/tests/live_openai_contract.rs index 13f5471f3..9de99d204 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 d399332b1..9c2546ebb 100644 --- a/test/twin/openai/tests/responses_contract.rs +++ b/test/twin/openai/tests/responses_contract.rs @@ -255,22 +255,19 @@ 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!( @@ -355,22 +352,19 @@ 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 6792572e0..81d15ff90 100644 --- a/test/twin/openai/tests/tool_and_schema_contract.rs +++ b/test/twin/openai/tests/tool_and_schema_contract.rs @@ -378,20 +378,17 @@ 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\\\"}\""));