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 });
107 }
108
109 Ok(steps)
110 }
111}
112
113#[derive(Debug, Clone, Default)]
115pub struct WorkflowRegistry {
116 workflows: HashMap<String, Workflow>,
117}
118
119impl WorkflowRegistry {
120 pub fn new() -> Self {
122 Self::default()
123 }
124
125 pub fn with_builtins() -> Self {
127 let mut registry = Self::new();
128 for workflow in builtin_workflows() {
129 registry.register(workflow);
130 }
131 registry
132 }
133
134 pub fn register(&mut self, workflow: Workflow) {
136 self.workflows.insert(workflow.name.clone(), workflow);
137 }
138
139 pub fn get(&self, name: &str) -> Option<&Workflow> {
141 self.workflows.get(name)
142 }
143
144 pub fn list(&self) -> Vec<&Workflow> {
146 self.workflows.values().collect()
147 }
148
149 pub fn remove(&mut self, name: &str) -> Option<Workflow> {
151 self.workflows.remove(name)
152 }
153}
154
155pub fn builtin_workflows() -> Vec<Workflow> {
157 vec![
158 Workflow {
159 name: "issue_to_pr".into(),
160 description: "Take an issue description and implement a solution, creating a PR-ready commit."
161 .into(),
162 steps: vec![
163 WorkflowStep {
164 name: "analyze_issue".into(),
165 action: StepAction::Skill {
166 skill: "summarize".into(),
167 arguments: {
168 let mut m = HashMap::new();
169 m.insert("target".into(), "{issue_url}".into());
170 m
171 },
172 },
173 config: None,
174 failure_policy: StepFailurePolicy::default(),
175 },
176 WorkflowStep {
177 name: "implement_solution".into(),
178 action: StepAction::Skill {
179 skill: "implement".into(),
180 arguments: {
181 let mut m = HashMap::new();
182 m.insert("description".into(), "{issue_url}".into());
183 m
184 },
185 },
186 config: None,
187 failure_policy: StepFailurePolicy {
188 retries: 1,
189 recovery_prompt: Some(
190 "Previous implementation failed. Try a different approach.".into(),
191 ),
192 },
193 },
194 WorkflowStep {
195 name: "write_tests".into(),
196 action: StepAction::Skill {
197 skill: "write_tests".into(),
198 arguments: {
199 let mut m = HashMap::new();
200 m.insert("target".into(), ".".into());
201 m
202 },
203 },
204 config: None,
205 failure_policy: StepFailurePolicy::default(),
206 },
207 WorkflowStep {
208 name: "run_checks".into(),
209 action: StepAction::Skill {
210 skill: "pre_push".into(),
211 arguments: HashMap::new(),
212 },
213 config: None,
214 failure_policy: StepFailurePolicy {
215 retries: 2,
216 recovery_prompt: Some("Fix failures and rerun checks.".into()),
217 },
218 },
219 ],
220 arguments: vec![WorkflowArgument {
221 name: "issue_url".into(),
222 description: "GitHub issue URL or issue description".into(),
223 required: true,
224 }],
225 },
226 Workflow {
227 name: "refactor_and_test".into(),
228 description:
229 "Refactor code toward a goal, write/update tests, and verify success.".into(),
230 steps: vec![
231 WorkflowStep {
232 name: "refactor".into(),
233 action: StepAction::Skill {
234 skill: "refactor".into(),
235 arguments: {
236 let mut m = HashMap::new();
237 m.insert("target".into(), "{target_file}".into());
238 m.insert("goal".into(), "{refactor_goal}".into());
239 m
240 },
241 },
242 config: None,
243 failure_policy: StepFailurePolicy {
244 retries: 1,
245 recovery_prompt: None,
246 },
247 },
248 WorkflowStep {
249 name: "update_tests".into(),
250 action: StepAction::Skill {
251 skill: "write_tests".into(),
252 arguments: {
253 let mut m = HashMap::new();
254 m.insert("target".into(), "{target_file}".into());
255 m
256 },
257 },
258 config: None,
259 failure_policy: StepFailurePolicy::default(),
260 },
261 WorkflowStep {
262 name: "verify_quality".into(),
263 action: StepAction::Prompt {
264 prompt: "Run tests on {target_file} and verify all tests pass.".into(),
265 },
266 config: None,
267 failure_policy: StepFailurePolicy::default(),
268 },
269 ],
270 arguments: vec![
271 WorkflowArgument {
272 name: "target_file".into(),
273 description: "File or module to refactor".into(),
274 required: true,
275 },
276 WorkflowArgument {
277 name: "refactor_goal".into(),
278 description: "What the refactoring should achieve".into(),
279 required: true,
280 },
281 ],
282 },
283 Workflow {
284 name: "review_and_fix".into(),
285 description: "Review code or PR, identify issues, and apply fixes.".into(),
286 steps: vec![
287 WorkflowStep {
288 name: "review".into(),
289 action: StepAction::Skill {
290 skill: "code_review".into(),
291 arguments: {
292 let mut m = HashMap::new();
293 m.insert("target".into(), "{review_target}".into());
294 m
295 },
296 },
297 config: None,
298 failure_policy: StepFailurePolicy::default(),
299 },
300 WorkflowStep {
301 name: "apply_fixes".into(),
302 action: StepAction::Prompt {
303 prompt:
304 "Based on the review feedback for {review_target}, apply all suggested fixes."
305 .into(),
306 },
307 config: None,
308 failure_policy: StepFailurePolicy {
309 retries: 1,
310 recovery_prompt: Some(
311 "Review failed. Try a different approach to fixing the issues.".into(),
312 ),
313 },
314 },
315 WorkflowStep {
316 name: "verify_fixes".into(),
317 action: StepAction::Skill {
318 skill: "code_review".into(),
319 arguments: {
320 let mut m = HashMap::new();
321 m.insert("target".into(), "{review_target}".into());
322 m
323 },
324 },
325 config: None,
326 failure_policy: StepFailurePolicy::default(),
327 },
328 ],
329 arguments: vec![WorkflowArgument {
330 name: "review_target".into(),
331 description: "Code, PR URL, or file path to review".into(),
332 required: true,
333 }],
334 },
335 ]
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn workflow_instantiation() {
344 let mut args = HashMap::new();
345 args.insert(
346 "issue_url".into(),
347 "https://github.com/owner/repo/issues/42".into(),
348 );
349
350 let registry = WorkflowRegistry::with_builtins();
351 let workflow = registry.get("issue_to_pr").expect("workflow not found");
352 let steps = workflow.instantiate(&args).expect("instantiation failed");
353
354 assert!(!steps.is_empty());
355 if let StepAction::Skill { arguments, .. } = &steps[0].action {
357 let target = arguments.get("target").expect("target argument missing");
358 assert_eq!(target, "https://github.com/owner/repo/issues/42");
359 } else {
360 panic!("expected skill action");
361 }
362 }
363
364 #[test]
365 fn missing_required_argument() {
366 let registry = WorkflowRegistry::with_builtins();
367 let workflow = registry.get("issue_to_pr").expect("workflow not found");
368 let result = workflow.instantiate(&HashMap::new());
369
370 assert!(result.is_err());
371 assert!(
372 result
373 .unwrap_err()
374 .to_string()
375 .contains("missing required argument")
376 );
377 }
378
379 #[test]
380 fn multiple_placeholders() {
381 let mut args = HashMap::new();
382 args.insert("target_file".into(), "src/lib.rs".into());
383 args.insert("refactor_goal".into(), "improve readability".into());
384
385 let registry = WorkflowRegistry::with_builtins();
386 let workflow = registry
387 .get("refactor_and_test")
388 .expect("workflow not found");
389 let steps = workflow.instantiate(&args).expect("instantiation failed");
390
391 assert!(!steps.is_empty());
392 if let StepAction::Skill { arguments, .. } = &steps[0].action {
393 assert_eq!(arguments.get("target").unwrap(), "src/lib.rs");
394 assert_eq!(arguments.get("goal").unwrap(), "improve readability");
395 } else {
396 panic!("expected skill action");
397 }
398 }
399
400 #[test]
401 fn builtin_workflows_registered() {
402 let registry = WorkflowRegistry::with_builtins();
403 assert!(registry.get("issue_to_pr").is_some());
404 assert!(registry.get("refactor_and_test").is_some());
405 assert!(registry.get("review_and_fix").is_some());
406 assert_eq!(registry.list().len(), 3);
407 }
408
409 #[test]
410 fn workflow_registration() {
411 let mut registry = WorkflowRegistry::new();
412 assert!(registry.get("test_workflow").is_none());
413
414 let workflow = Workflow {
415 name: "test_workflow".into(),
416 description: "Test workflow".into(),
417 steps: vec![],
418 arguments: vec![],
419 };
420 registry.register(workflow);
421
422 assert!(registry.get("test_workflow").is_some());
423 assert_eq!(registry.list().len(), 1);
424
425 let removed = registry.remove("test_workflow");
426 assert!(removed.is_some());
427 assert!(registry.get("test_workflow").is_none());
428 }
429}