Skip to main content

faultline_agents/
lib.rs

1pub mod error;
2pub mod model;
3
4use std::collections::BTreeMap;
5
6use crate::error::FaultlineAgentsError;
7use crate::model::{
8    ArtifactRecord, ToolCall, ToolDefinition, WorkflowEdge, WorkflowGraph, WorkflowNode,
9    WorkflowStep, WorkflowStepKind,
10};
11
12pub trait Planner {
13    fn plan(&self, objective: &str) -> Result<WorkflowGraph, FaultlineAgentsError>;
14}
15
16pub trait Executor {
17    fn execute(
18        &self,
19        workflow: &WorkflowGraph,
20    ) -> Result<Vec<ArtifactRecord>, FaultlineAgentsError>;
21}
22
23pub trait Reviewer {
24    fn review(&self, artifacts: &[ArtifactRecord]) -> Result<String, FaultlineAgentsError>;
25}
26
27pub trait ArtifactStore {
28    fn persist(&mut self, artifact: ArtifactRecord);
29    fn get(&self, artifact_id: uuid::Uuid) -> Result<ArtifactRecord, FaultlineAgentsError>;
30}
31
32#[derive(Debug, Default)]
33pub struct InMemoryToolRegistry {
34    tools: BTreeMap<String, ToolDefinition>,
35}
36
37impl InMemoryToolRegistry {
38    pub fn new() -> Self {
39        Self {
40            tools: BTreeMap::new(),
41        }
42    }
43
44    pub fn register(&mut self, tool: ToolDefinition) {
45        self.tools.insert(tool.name.clone(), tool);
46    }
47
48    pub fn resolve(&self, name: &str) -> Result<&ToolDefinition, FaultlineAgentsError> {
49        self.tools
50            .get(name)
51            .ok_or_else(|| FaultlineAgentsError::ToolNotFound(name.to_string()))
52    }
53}
54
55#[derive(Debug, Default)]
56pub struct InMemoryArtifactStore {
57    records: BTreeMap<uuid::Uuid, ArtifactRecord>,
58}
59
60impl InMemoryArtifactStore {
61    pub fn new() -> Self {
62        Self {
63            records: BTreeMap::new(),
64        }
65    }
66}
67
68impl ArtifactStore for InMemoryArtifactStore {
69    fn persist(&mut self, artifact: ArtifactRecord) {
70        self.records.insert(artifact.artifact_id, artifact);
71    }
72
73    fn get(&self, artifact_id: uuid::Uuid) -> Result<ArtifactRecord, FaultlineAgentsError> {
74        self.records
75            .get(&artifact_id)
76            .cloned()
77            .ok_or(FaultlineAgentsError::ArtifactNotFound(artifact_id))
78    }
79}
80
81#[derive(Debug, Default)]
82pub struct DeterministicPlanner;
83
84impl Planner for DeterministicPlanner {
85    fn plan(&self, objective: &str) -> Result<WorkflowGraph, FaultlineAgentsError> {
86        if objective.trim().is_empty() {
87            return Err(FaultlineAgentsError::InvalidWorkflow(
88                "objective must not be empty",
89            ));
90        }
91
92        let plan_step = WorkflowStep::new(WorkflowStepKind::Plan, format!("plan: {objective}"));
93        let execute_step =
94            WorkflowStep::new(WorkflowStepKind::Execute, format!("execute: {objective}"));
95        let review_step =
96            WorkflowStep::new(WorkflowStepKind::Review, format!("review: {objective}"));
97
98        let plan_node = WorkflowNode {
99            node_id: uuid::Uuid::new_v4(),
100            step: plan_step,
101            tool_calls: vec![ToolCall {
102                tool_name: "plan_builder".to_string(),
103                arguments: serde_json::json!({"objective": objective}),
104            }],
105        };
106        let execute_node = WorkflowNode {
107            node_id: uuid::Uuid::new_v4(),
108            step: execute_step,
109            tool_calls: vec![ToolCall {
110                tool_name: "executor".to_string(),
111                arguments: serde_json::json!({"objective": objective}),
112            }],
113        };
114        let review_node = WorkflowNode {
115            node_id: uuid::Uuid::new_v4(),
116            step: review_step,
117            tool_calls: vec![ToolCall {
118                tool_name: "reviewer".to_string(),
119                arguments: serde_json::json!({"objective": objective}),
120            }],
121        };
122
123        let mut graph = WorkflowGraph::new();
124        graph.edges.push(WorkflowEdge {
125            from: plan_node.node_id,
126            to: execute_node.node_id,
127        });
128        graph.edges.push(WorkflowEdge {
129            from: execute_node.node_id,
130            to: review_node.node_id,
131        });
132        graph.nodes.push(plan_node);
133        graph.nodes.push(execute_node);
134        graph.nodes.push(review_node);
135
136        Ok(graph)
137    }
138}
139
140#[derive(Debug, Default)]
141pub struct DeterministicExecutor;
142
143impl Executor for DeterministicExecutor {
144    fn execute(
145        &self,
146        workflow: &WorkflowGraph,
147    ) -> Result<Vec<ArtifactRecord>, FaultlineAgentsError> {
148        if workflow.nodes.is_empty() {
149            return Err(FaultlineAgentsError::InvalidWorkflow(
150                "workflow requires at least one node",
151            ));
152        }
153
154        let artifacts = workflow
155            .nodes
156            .iter()
157            .map(|node| {
158                ArtifactRecord::new(serde_json::json!({
159                    "node_id": node.node_id,
160                    "step_kind": format!("{:?}", node.step.kind),
161                    "tool_call_count": node.tool_calls.len(),
162                }))
163            })
164            .collect();
165        Ok(artifacts)
166    }
167}
168
169#[derive(Debug, Default)]
170pub struct DeterministicReviewer;
171
172impl Reviewer for DeterministicReviewer {
173    fn review(&self, artifacts: &[ArtifactRecord]) -> Result<String, FaultlineAgentsError> {
174        if artifacts.is_empty() {
175            return Err(FaultlineAgentsError::InvalidWorkflow(
176                "cannot review empty artifact list",
177            ));
178        }
179
180        Ok(format!("review complete: {} artifacts", artifacts.len()))
181    }
182}
183
184#[allow(unused_imports)]
185pub use error::*;
186#[allow(unused_imports)]
187pub use model::*;
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn deterministic_plan_execute_review_flow() {
195        let planner = DeterministicPlanner;
196        let executor = DeterministicExecutor;
197        let reviewer = DeterministicReviewer;
198
199        let graph = planner
200            .plan("diff and sync parcels")
201            .expect("plan workflow");
202        assert_eq!(graph.nodes.len(), 3);
203
204        let artifacts = executor.execute(&graph).expect("execute workflow");
205        assert_eq!(artifacts.len(), 3);
206
207        let review = reviewer.review(&artifacts).expect("review workflow");
208        assert!(review.contains("3 artifacts"));
209    }
210
211    #[test]
212    fn artifact_store_round_trip() {
213        let mut store = InMemoryArtifactStore::new();
214        let artifact = ArtifactRecord::new(serde_json::json!({"result": "ok"}));
215        let artifact_id = artifact.artifact_id;
216
217        store.persist(artifact);
218        let restored = store.get(artifact_id).expect("restore artifact");
219        assert_eq!(restored.payload["result"], "ok");
220    }
221}