1use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11use crate::chain::{ChainStep, StepAction, StepFailurePolicy};
12use crate::types::SlotConfig;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Workflow {
17 pub name: String,
19
20 pub description: String,
22
23 pub steps: Vec<WorkflowStep>,
25
26 pub arguments: Vec<WorkflowArgument>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct WorkflowStep {
33 pub name: String,
35
36 pub action: StepAction,
38
39 pub config: Option<SlotConfig>,
41
42 #[serde(default)]
44 pub failure_policy: StepFailurePolicy,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct WorkflowArgument {
50 pub name: String,
52
53 pub description: String,
55
56 pub required: bool,
58}
59
60impl Workflow {
61 pub fn instantiate(&self, args: &HashMap<String, String>) -> crate::Result<Vec<ChainStep>> {
66 for arg in &self.arguments {
68 if arg.required && !args.contains_key(&arg.name) {
69 return Err(crate::Error::Store(format!(
70 "missing required argument '{}' for workflow '{}'",
71 arg.name, self.name
72 )));
73 }
74 }
75
76 let mut steps = Vec::new();
78 for ws in &self.steps {
79 let action = match &ws.action {
80 StepAction::Prompt { prompt } => {
81 let mut p = prompt.clone();
82 for (key, value) in args {
83 p = p.replace(&format!("{{{key}}}"), value);
84 }
85 StepAction::Prompt { prompt: p }
86 }
87 StepAction::Skill { skill, arguments } => {
88 let mut args_substituted = arguments.clone();
89 for value in args_substituted.values_mut() {
90 for (arg_key, arg_value) in args {
91 *value = value.replace(&format!("{{{arg_key}}}"), arg_value);
92 }
93 }
94 StepAction::Skill {
95 skill: skill.clone(),
96 arguments: args_substituted,
97 }
98 }
99 };
100
101 steps.push(ChainStep {
102 name: ws.name.clone(),
103 action,
104 config: ws.config.clone(),
105 failure_policy: ws.failure_policy.clone(),
106 output_vars: Default::default(),
107 });
108 }
109
110 Ok(steps)
111 }
112}
113
114#[derive(Debug, Clone, Default)]
116pub struct WorkflowRegistry {
117 workflows: HashMap<String, Workflow>,
118}
119
120impl WorkflowRegistry {
121 pub fn new() -> Self {
123 Self::default()
124 }
125
126 pub fn with_builtins() -> Self {
128 let mut registry = Self::new();
129 for workflow in builtin_workflows() {
130 registry.register(workflow);
131 }
132 registry
133 }
134
135 pub fn register(&mut self, workflow: Workflow) {
137 self.workflows.insert(workflow.name.clone(), workflow);
138 }
139
140 pub fn get(&self, name: &str) -> Option<&Workflow> {
142 self.workflows.get(name)
143 }
144
145 pub fn list(&self) -> Vec<&Workflow> {
147 self.workflows.values().collect()
148 }
149
150 pub fn remove(&mut self, name: &str) -> Option<Workflow> {
152 self.workflows.remove(name)
153 }
154}
155
156pub fn builtin_workflows() -> Vec<Workflow> {
158 vec![
159 Workflow {
160 name: "issue_to_pr".into(),
161 description: "Take an issue description and implement a solution, creating a PR-ready commit."
162 .into(),
163 steps: vec![
164 WorkflowStep {
165 name: "analyze_issue".into(),
166 action: StepAction::Skill {
167 skill: "summarize".into(),
168 arguments: {
169 let mut m = HashMap::new();
170 m.insert("target".into(), "{issue_url}".into());
171 m
172 },
173 },
174 config: None,
175 failure_policy: StepFailurePolicy::default(),
176 },
177 WorkflowStep {
178 name: "implement_solution".into(),
179 action: StepAction::Skill {
180 skill: "implement".into(),
181 arguments: {
182 let mut m = HashMap::new();
183 m.insert("description".into(), "{issue_url}".into());
184 m
185 },
186 },
187 config: None,
188 failure_policy: StepFailurePolicy {
189 retries: 1,
190 recovery_prompt: Some(
191 "Previous implementation failed. Try a different approach.".into(),
192 ),
193 },
194 },
195 WorkflowStep {
196 name: "write_tests".into(),
197 action: StepAction::Skill {
198 skill: "write_tests".into(),
199 arguments: {
200 let mut m = HashMap::new();
201 m.insert("target".into(), ".".into());
202 m
203 },
204 },
205 config: None,
206 failure_policy: StepFailurePolicy::default(),
207 },
208 WorkflowStep {
209 name: "run_checks".into(),
210 action: StepAction::Skill {
211 skill: "pre_push".into(),
212 arguments: HashMap::new(),
213 },
214 config: None,
215 failure_policy: StepFailurePolicy {
216 retries: 2,
217 recovery_prompt: Some("Fix failures and rerun checks.".into()),
218 },
219 },
220 ],
221 arguments: vec![WorkflowArgument {
222 name: "issue_url".into(),
223 description: "GitHub issue URL or issue description".into(),
224 required: true,
225 }],
226 },
227 Workflow {
228 name: "refactor_and_test".into(),
229 description:
230 "Refactor code toward a goal, write/update tests, and verify success.".into(),
231 steps: vec![
232 WorkflowStep {
233 name: "refactor".into(),
234 action: StepAction::Skill {
235 skill: "refactor".into(),
236 arguments: {
237 let mut m = HashMap::new();
238 m.insert("target".into(), "{target_file}".into());
239 m.insert("goal".into(), "{refactor_goal}".into());
240 m
241 },
242 },
243 config: None,
244 failure_policy: StepFailurePolicy {
245 retries: 1,
246 recovery_prompt: None,
247 },
248 },
249 WorkflowStep {
250 name: "update_tests".into(),
251 action: StepAction::Skill {
252 skill: "write_tests".into(),
253 arguments: {
254 let mut m = HashMap::new();
255 m.insert("target".into(), "{target_file}".into());
256 m
257 },
258 },
259 config: None,
260 failure_policy: StepFailurePolicy::default(),
261 },
262 WorkflowStep {
263 name: "verify_quality".into(),
264 action: StepAction::Prompt {
265 prompt: "Run tests on {target_file} and verify all tests pass.".into(),
266 },
267 config: None,
268 failure_policy: StepFailurePolicy::default(),
269 },
270 ],
271 arguments: vec![
272 WorkflowArgument {
273 name: "target_file".into(),
274 description: "File or module to refactor".into(),
275 required: true,
276 },
277 WorkflowArgument {
278 name: "refactor_goal".into(),
279 description: "What the refactoring should achieve".into(),
280 required: true,
281 },
282 ],
283 },
284 Workflow {
285 name: "review_and_fix".into(),
286 description: "Review code or PR, identify issues, and apply fixes.".into(),
287 steps: vec![
288 WorkflowStep {
289 name: "review".into(),
290 action: StepAction::Skill {
291 skill: "code_review".into(),
292 arguments: {
293 let mut m = HashMap::new();
294 m.insert("target".into(), "{review_target}".into());
295 m
296 },
297 },
298 config: None,
299 failure_policy: StepFailurePolicy::default(),
300 },
301 WorkflowStep {
302 name: "apply_fixes".into(),
303 action: StepAction::Prompt {
304 prompt:
305 "Based on the review feedback for {review_target}, apply all suggested fixes."
306 .into(),
307 },
308 config: None,
309 failure_policy: StepFailurePolicy {
310 retries: 1,
311 recovery_prompt: Some(
312 "Review failed. Try a different approach to fixing the issues.".into(),
313 ),
314 },
315 },
316 WorkflowStep {
317 name: "verify_fixes".into(),
318 action: StepAction::Skill {
319 skill: "code_review".into(),
320 arguments: {
321 let mut m = HashMap::new();
322 m.insert("target".into(), "{review_target}".into());
323 m
324 },
325 },
326 config: None,
327 failure_policy: StepFailurePolicy::default(),
328 },
329 ],
330 arguments: vec![WorkflowArgument {
331 name: "review_target".into(),
332 description: "Code, PR URL, or file path to review".into(),
333 required: true,
334 }],
335 },
336 ]
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
344 fn workflow_instantiation() {
345 let mut args = HashMap::new();
346 args.insert(
347 "issue_url".into(),
348 "https://github.com/owner/repo/issues/42".into(),
349 );
350
351 let registry = WorkflowRegistry::with_builtins();
352 let workflow = registry.get("issue_to_pr").expect("workflow not found");
353 let steps = workflow.instantiate(&args).expect("instantiation failed");
354
355 assert!(!steps.is_empty());
356 if let StepAction::Skill { arguments, .. } = &steps[0].action {
358 let target = arguments.get("target").expect("target argument missing");
359 assert_eq!(target, "https://github.com/owner/repo/issues/42");
360 } else {
361 panic!("expected skill action");
362 }
363 }
364
365 #[test]
366 fn missing_required_argument() {
367 let registry = WorkflowRegistry::with_builtins();
368 let workflow = registry.get("issue_to_pr").expect("workflow not found");
369 let result = workflow.instantiate(&HashMap::new());
370
371 assert!(result.is_err());
372 assert!(
373 result
374 .unwrap_err()
375 .to_string()
376 .contains("missing required argument")
377 );
378 }
379
380 #[test]
381 fn multiple_placeholders() {
382 let mut args = HashMap::new();
383 args.insert("target_file".into(), "src/lib.rs".into());
384 args.insert("refactor_goal".into(), "improve readability".into());
385
386 let registry = WorkflowRegistry::with_builtins();
387 let workflow = registry
388 .get("refactor_and_test")
389 .expect("workflow not found");
390 let steps = workflow.instantiate(&args).expect("instantiation failed");
391
392 assert!(!steps.is_empty());
393 if let StepAction::Skill { arguments, .. } = &steps[0].action {
394 assert_eq!(arguments.get("target").unwrap(), "src/lib.rs");
395 assert_eq!(arguments.get("goal").unwrap(), "improve readability");
396 } else {
397 panic!("expected skill action");
398 }
399 }
400
401 #[test]
402 fn builtin_workflows_registered() {
403 let registry = WorkflowRegistry::with_builtins();
404 assert!(registry.get("issue_to_pr").is_some());
405 assert!(registry.get("refactor_and_test").is_some());
406 assert!(registry.get("review_and_fix").is_some());
407 assert_eq!(registry.list().len(), 3);
408 }
409
410 #[test]
411 fn workflow_registration() {
412 let mut registry = WorkflowRegistry::new();
413 assert!(registry.get("test_workflow").is_none());
414
415 let workflow = Workflow {
416 name: "test_workflow".into(),
417 description: "Test workflow".into(),
418 steps: vec![],
419 arguments: vec![],
420 };
421 registry.register(workflow);
422
423 assert!(registry.get("test_workflow").is_some());
424 assert_eq!(registry.list().len(), 1);
425
426 let removed = registry.remove("test_workflow");
427 assert!(removed.is_some());
428 assert!(registry.get("test_workflow").is_none());
429 }
430}