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}