A run can now carry an optional spend budget. New [run.limits] settings table with max_cost (USD) and max_tokens; when accumulated LLM spend exceeds either limit, the run halts immediately and fails with the existing BudgetExhausted failure reason. - RunBudget (fabro-model): thread-safe accumulator shared across the engine, agent sessions, hooks, and the prompt path. Charges per LLM response; queues one-shot 80% warnings drained into run notices. - Enforcement is inline at every usage-recording site: the agent turn loop (interrupting like the wall-clock timeout via a new InterruptReason::BudgetExhausted), subagent sessions, the one-shot prompt path with schema-repair retries, context compaction, web_fetch summarization, and prompt/agent hooks (which skip once the budget is spent, matching their fail-open posture). - Budget exhaustion terminates the run as an engine-level error (fabro_core::Error::BudgetExhausted, following the StallTimeout pattern) so fail edges are not followed, plus a between-stage gate for non-LLM stages and a run-start gate that makes a resumed budget-failed run fail fast until the limit is raised (spend is seeded from the billing projection). - Exposed on the RunNamespace OpenAPI schema (RunLimitsSettings); Rust and TypeScript clients regenerated; docs updated. Known gaps, by design for now: ACP external agents report no usable per-response usage (cumulative UsageUpdate capture is a follow-up), and post-run PR content generation is outside the engine. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| src | ||
| tests | ||
| 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
StageOutcome(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=succeeded"]
gate -> implement [label="No", condition="outcome!=succeeded"]
}
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 -> conclude -> publish -> 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=succeeded
outcome!=failed
outcome=succeeded && 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. Branches receive isolated context forks, share the same sandbox checkout, and always finish before the workflow continues. Use max_parallel to limit concurrency; concurrent workspace writes are user-managed.
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)