fabro-config no longer carries the legacy pass-through shims that
forwarded type re-exports from `fabro_types::settings::{hook,mcp,sandbox,
server,user,run}`. Consumers now import the runtime types directly
from `fabro_types::settings::*`, which is the only definitional
location.
Deleted files:
- `fabro-config/src/hook.rs` (1 LOC glob re-export)
- `fabro-config/src/mcp.rs` (1 LOC glob re-export)
- `fabro-config/src/sandbox.rs` (~8 LOC re-export list)
- `fabro-config/src/server.rs` (re-exports + `resolve_storage_dir`;
the `resolve_storage_dir` helper moved to `fabro_config`'s crate root
and takes `&SettingsFile` directly)
Shrunk files:
- `fabro-config/src/run.rs` lost the `ArtifactsSettings` /
`CheckpointSettings` / `GitHubSettings` / `LlmSettings` /
`MergeStrategy` / `PullRequestSettings` / `SetupSettings` re-export
block and the unused `resolve_env_refs` helper. What remains is just
the workflow TOML loader helpers (`parse_run_config`, `load_run_config`,
`resolve_graph_path`).
- `fabro-config/src/user.rs` lost the `ClientTlsSettings` /
`ExecSettings` / `OutputFormat` / `PermissionLevel` /
`ServerSettings` re-export block. The settings-path helpers and
legacy-config warning logic stay. `fabro-cli/src/user_config.rs`
now imports `ClientTlsSettings` directly from fabro_types.
Callers updated to use the canonical paths:
- `fabro-agent/src/cli.rs` imports `{OutputFormat, PermissionLevel}`
from `fabro_types::settings::user`; added `fabro-types` dep.
- `fabro-hooks/src/{config,types}.rs` re-export from
`fabro_types::settings::hook`.
- `fabro-mcp/src/config.rs` re-exports from `fabro_types::settings::mcp`.
- `fabro-sandbox/src/daytona/mod.rs` re-exports from
`fabro_types::settings::sandbox`.
- `fabro-server/src/{lib,jwt_auth,tls,serve,demo}.rs` +
`tests/it/openapi_conformance.rs` import server types from
`fabro_types::settings::server` and call `fabro_config::resolve_storage_dir`
from the crate root.
- `fabro-workflow/src/{operations/start,pipeline/types,pipeline/pull_request}.rs`
import sandbox / pull_request types from `fabro_types::settings::*`.
Build, clippy, fmt, and 3756 / 3756 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| .. | ||
| src | ||
| tests/it | ||
| Cargo.toml | ||
| README.md | ||
fabro-workflow
A DOT-based pipeline runner for multi-stage AI workflows. Define workflows as Graphviz digraph files and execute them with pluggable handlers, conditional routing, human-in-the-loop gates, parallel branching, retry policies, and checkpoint-based recovery.
Key Concepts
- Graph -- A directed graph parsed from DOT syntax containing nodes, edges, and attributes. The graph carries a
goaldescribing the pipeline's purpose. - Node -- A workflow step. Graphviz shapes map to handler types (e.g.,
Mdiamond= start,Msquare= exit,box= agent,tab= prompt,diamond= conditional,hexagon= human gate,component= parallel). - Edge -- A connection between nodes with optional
condition,label,weight, andfidelityattributes that control routing. - Handler -- An async trait implementation that executes a node and returns an
Outcome. Built-in handlers includeStartHandler,ExitHandler,AgentHandler,PromptHandler,ConditionalHandler,HumanHandler,ParallelHandler,FanInHandler,CommandHandler, andSubWorkflowHandler. - Outcome -- The result of executing a handler, carrying a
StageStatus(Success, Fail, PartialSuccess, Retry, Skipped), optional routing hints (preferred_label,suggested_next_ids), and context updates. - Context -- A thread-safe key-value store shared across pipeline stages, supporting snapshots and isolated cloning for parallel branches.
- Interviewer -- A trait for human-in-the-loop interactions. Implementations include
AutoApproveInterviewer,QueueInterviewer,CallbackInterviewer,ConsoleInterviewer, andRecordingInterviewer. - Checkpoint -- A serializable snapshot of execution state (completed nodes, context values) for crash recovery and resume.
Pipeline Definition
Pipelines are defined using Graphviz DOT syntax:
digraph MyPipeline {
graph [goal="Implement and validate a feature"]
rankdir=LR
node [shape=box, timeout="900s"]
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
plan [label="Plan", prompt="Plan the implementation"]
implement [label="Implement", prompt="Implement the plan"]
validate [label="Validate", prompt="Run tests"]
gate [shape=diamond, label="Tests passing?"]
start -> plan -> implement -> validate -> gate
gate -> exit [label="Yes", condition="outcome=success"]
gate -> implement [label="No", condition="outcome!=success"]
}
Usage
Parsing and Validating a Pipeline
use fabro_workflow::operations::{create, CreateOptions};
let dot_source = r#"digraph Simple {
graph [goal="Run tests"]
start [shape=Mdiamond]
exit [shape=Msquare]
work [shape=box, prompt="Run the test suite"]
start -> work -> exit
}"#;
let validated = create(dot_source, CreateOptions::default())
.expect("pipeline should parse");
validated.raise_on_errors().expect("pipeline should validate");
let (graph, _, _) = validated.into_parts();
assert_eq!(graph.name, "Simple");
assert_eq!(graph.goal(), "Run tests");
operations::create parses the DOT source, applies built-in transforms (variable expansion, stylesheet application, preamble injection), and returns diagnostics through Validated.
Running a Pipeline
use fabro_workflow::operations::start;
use fabro_workflow::pipeline;
// Use `operations::start(...)` for the full initialize -> execute -> retro -> finalize flow.
// Use `pipeline::initialize(...)` + `pipeline::execute(...)` when you need partial lifecycle control.
Custom Handlers
Implement the Handler trait to add custom node behavior:
use arc_workflows::handler::Handler;
use arc_workflows::context::Context;
use arc_workflows::graph::{Graph, Node};
use arc_workflows::outcome::Outcome;
use arc_workflows::error::ArcError;
use async_trait::async_trait;
use std::path::Path;
struct MyHandler;
#[async_trait]
impl Handler for MyHandler {
async fn execute(
&self,
node: &Node,
context: &Context,
graph: &Graph,
run_dir: &Path,
) -> Result<Outcome, ArcError> {
// Custom logic here
Ok(Outcome::success())
}
}
Model Stylesheets
CSS-like stylesheets control LLM model assignment with specificity-based cascading:
digraph Styled {
graph [
goal="Build feature",
model_stylesheet="
* { model: claude-sonnet-4-5;}
.code { model: claude-opus-4-6; }
#critical_review { model: gpt-5.2;}
"
]
// ...
}
Selectors by specificity: * (universal, 0) < shape (1) < .class (2) < #id (3). Explicit node attributes are never overridden.
Condition Expressions
Edge conditions use a simple expression syntax for routing:
outcome=success
outcome!=fail
outcome=success && context.tests_passed=true
my_flag
Clauses support =, !=, and bare key truthiness checks, joined with &&.
Human-in-the-Loop Gates
Nodes with shape=hexagon or type="human" pause execution for human input. Outgoing edge labels become selectable options, with accelerator key parsing for patterns like [A] Approve and F) Fix.
Parallel Execution
Nodes with shape=component fan out to branches concurrently. Configurable join policies: wait_all (default), first_success.
Checkpoints and Resume
The engine saves a checkpoint after each node. Resume from a checkpoint with engine.run_from_checkpoint(&graph, &config, &checkpoint).
Architecture
parser (DOT -> AST -> Graph)
-> transform (variable expansion, stylesheet, preamble)
-> validation (14 lint rules)
-> engine (execution loop with retry, edge selection, goal gates)
-> handler (pluggable node executors)
-> interviewer (human-in-the-loop I/O)