feat: remove pipeline command and refactor workflow execution to use custom specialists
This commit is contained in:
+91
-74
@@ -34,59 +34,28 @@ use crate::app::workflow::script::{ScriptPrimitive, ScriptOptions, WorkflowScrip
|
||||
use crate::app::workflow::engine::{execute_primitive, LiveStateFn, AgentStatus};
|
||||
use crate::app::subagent::division;
|
||||
|
||||
/// Construct the 4 specialized agents for a division.
|
||||
/// Construct the specialized agents for a division.
|
||||
///
|
||||
/// Flow: map division name to its specialization pool.
|
||||
///
|
||||
/// Return: a `Vec<ScriptPrimitive>` containing the specialist agents.
|
||||
fn make_division_specialists(
|
||||
div: &division::Division,
|
||||
user_request: &str,
|
||||
specs: &[(String, String)],
|
||||
) -> Vec<ScriptPrimitive> {
|
||||
let div_prompt = div.agent_def.system_prompt.as_deref().unwrap_or("");
|
||||
|
||||
let specializations = match div.name {
|
||||
"Strategy" => vec![
|
||||
("Architectural Analysis", "Focus on component tree, file layout, and module structure."),
|
||||
("Data Flow Planning", "Focus on sequence of calls, interface definitions, and APIs."),
|
||||
("Task Breakdown", "Focus on step-by-step TODO lists and implementation order."),
|
||||
("Risk Evaluation", "Focus on edge cases, compatibility, and system constraints."),
|
||||
],
|
||||
"Engineering" => vec![
|
||||
("Core Logic", "Focus on core algorithms, mathematical processing, and backend logic."),
|
||||
("Interface & Endpoints", "Focus on implementing routes, IPC handlers, and struct mappings."),
|
||||
("Error Handling & Logs", "Focus on implementing robust error handling, try/catch, tracing, and Result wrappers."),
|
||||
("Utility & Helpers", "Focus on filesystem helpers, input sanitization, and parsing utilities."),
|
||||
],
|
||||
"Quality" => vec![
|
||||
("Code Reviewer", "Focus on checking coding style, naming standards, and coding conventions."),
|
||||
("Unit Testing", "Focus on writing and running unit tests for individual functions and modules."),
|
||||
("Integration Testing", "Focus on writing and running integration tests for system interactions and state flows."),
|
||||
("Performance Analyst", "Focus on efficiency check, bottleneck analysis, and time complexity."),
|
||||
],
|
||||
"Security" => vec![
|
||||
("Dependency Auditor", "Focus on auditing cargo lock and checking dependencies for vulnerabilities."),
|
||||
("Input Sanitizer", "Focus on auditing input validation, injection prevention, path traversal, and shell safety."),
|
||||
("Access Control", "Focus on auditing authorization, filesystem access permissions, and API scopes."),
|
||||
("Secrets Auditor", "Focus on auditing secrets leakage, credentials safety, and log auditing."),
|
||||
],
|
||||
"Documentation" => vec![
|
||||
("README & Setup", "Focus on updating README, installation guides, usage examples, and high-level setup."),
|
||||
("API Reference", "Focus on updating API reference, parameter details, and traits/functions documentation."),
|
||||
("Changelog & Architecture", "Focus on updating CHANGELOG and describing system architecture/diagrams."),
|
||||
("Inline Comments", "Focus on adding explanatory inline comments and documentation comments inside source files."),
|
||||
],
|
||||
_ => vec![
|
||||
("Specialist 1", "Focus on general tasks and responsibilities of this division."),
|
||||
("Specialist 2", "Focus on code review and validation."),
|
||||
("Specialist 3", "Focus on error handling and reporting."),
|
||||
("Specialist 4", "Focus on documentation and testing."),
|
||||
],
|
||||
};
|
||||
|
||||
specializations
|
||||
.into_iter()
|
||||
specs
|
||||
.iter()
|
||||
.map(|(label, focus)| {
|
||||
// Prepend [Division Name: Specialist Label] so the first 40 chars
|
||||
// of the prompt become the agent_name in spawn_single_agent.
|
||||
// We use quadruple curly braces `{{{{findings}}}}` so that Rust's `format!` formats it
|
||||
// into `{{findings}}` in the output string, which `resolve_template` then recognizes
|
||||
// and replaces.
|
||||
let prompt = format!(
|
||||
"[{}: {}]\n\n{}\n\n{}\n\nUser request: {}\n\nFindings from previous divisions: {{findings}}",
|
||||
"[{}: {}]\n\n{}\n\n{}\n\nUser request: {}\n\nFindings from previous divisions:\n{{{{findings}}}}",
|
||||
div.name,
|
||||
label,
|
||||
focus,
|
||||
@@ -99,11 +68,16 @@ fn make_division_specialists(
|
||||
}
|
||||
|
||||
/// Construct a named Phase wrapper containing a Parallel block of division specialists.
|
||||
///
|
||||
/// Flow: construct division specialists → wrap in a `Parallel` primitive wrapper.
|
||||
///
|
||||
/// Return: a `ScriptPrimitive::Phase` wrapper.
|
||||
fn make_division_phase(
|
||||
div: &division::Division,
|
||||
user_request: &str,
|
||||
specs: &[(String, String)],
|
||||
) -> ScriptPrimitive {
|
||||
let specialists = make_division_specialists(div, user_request);
|
||||
let specialists = make_division_specialists(div, user_request, specs);
|
||||
ScriptPrimitive::Phase {
|
||||
name: div.name.to_string(),
|
||||
script: Box::new(ScriptPrimitive::Parallel(specialists)),
|
||||
@@ -113,10 +87,7 @@ fn make_division_phase(
|
||||
/// Run the full company-style pipeline for a given user request.
|
||||
///
|
||||
/// This orchestrates all five divisions, running Strategy and Engineering
|
||||
/// sequentially, followed by Quality, Security, and Documentation in parallel:
|
||||
/// 1. **Strategy** — create plan with diagrams (4 parallel subagents)
|
||||
/// 2. **Engineering** — implement code per the plan (4 parallel subagents)
|
||||
/// 3. **Quality** || **Security** || **Documentation** (in parallel, up to 10 concurrent subagents total)
|
||||
/// sequentially, followed by Quality, Security, and Documentation in parallel.
|
||||
///
|
||||
/// Each division receives findings from all previous divisions, enabling
|
||||
/// context to flow through the pipeline.
|
||||
@@ -128,15 +99,27 @@ pub fn run_company_pipeline(
|
||||
workspaces: &[std::path::PathBuf],
|
||||
turn_events: Option<&Arc<Mutex<std::collections::VecDeque<crate::app::state::runtime::TurnEvent>>>>,
|
||||
abort_flag: &Option<Arc<AtomicBool>>,
|
||||
custom_specialists: HashMap<String, Vec<(String, String)>>,
|
||||
) -> anyhow::Result<String> {
|
||||
let divisions = division::all_divisions();
|
||||
|
||||
let strategy_phase = make_division_phase(&divisions[0], user_request);
|
||||
let engineering_phase = make_division_phase(&divisions[1], user_request);
|
||||
let strategy_specs = custom_specialists.get("Strategy")
|
||||
.ok_or_else(|| anyhow::anyhow!("missing required division configuration: Strategy"))?;
|
||||
let engineering_specs = custom_specialists.get("Engineering")
|
||||
.ok_or_else(|| anyhow::anyhow!("missing required division configuration: Engineering"))?;
|
||||
let quality_specs = custom_specialists.get("Quality")
|
||||
.ok_or_else(|| anyhow::anyhow!("missing required division configuration: Quality"))?;
|
||||
let security_specs = custom_specialists.get("Security")
|
||||
.ok_or_else(|| anyhow::anyhow!("missing required division configuration: Security"))?;
|
||||
let documentation_specs = custom_specialists.get("Documentation")
|
||||
.ok_or_else(|| anyhow::anyhow!("missing required division configuration: Documentation"))?;
|
||||
|
||||
let quality_phase = make_division_phase(&divisions[2], user_request);
|
||||
let security_phase = make_division_phase(&divisions[3], user_request);
|
||||
let documentation_phase = make_division_phase(&divisions[4], user_request);
|
||||
let strategy_phase = make_division_phase(&divisions[0], user_request, strategy_specs);
|
||||
let engineering_phase = make_division_phase(&divisions[1], user_request, engineering_specs);
|
||||
|
||||
let quality_phase = make_division_phase(&divisions[2], user_request, quality_specs);
|
||||
let security_phase = make_division_phase(&divisions[3], user_request, security_specs);
|
||||
let documentation_phase = make_division_phase(&divisions[4], user_request, documentation_specs);
|
||||
|
||||
let parallel_divisions = ScriptPrimitive::Parallel(vec![
|
||||
quality_phase,
|
||||
@@ -155,14 +138,12 @@ pub fn run_company_pipeline(
|
||||
description: "Company Pipeline: Strategy → Engineering → (Quality || Security || Documentation)".to_string(),
|
||||
script: pipeline_primitive,
|
||||
options: ScriptOptions {
|
||||
max_concurrency: 10, // Max concurrent agents in execution
|
||||
continue_on_error: true, // one division failing shouldn't block the rest
|
||||
max_concurrency: 10,
|
||||
continue_on_error: true,
|
||||
timeout_ms: None,
|
||||
},
|
||||
};
|
||||
|
||||
// Build a live callback for TUI updates if turn_events is available.
|
||||
// Uses agent_name (division + specialist name) for the display label in the panel.
|
||||
let live: Option<LiveStateFn> = turn_events.map(|events| {
|
||||
let events = events.clone();
|
||||
let f: LiveStateFn = Arc::new(move |_agent_id: String, agent_name: String, status: AgentStatus| {
|
||||
@@ -179,9 +160,6 @@ pub fn run_company_pipeline(
|
||||
});
|
||||
|
||||
let args: HashMap<String, String> = HashMap::new();
|
||||
let live_ref = live.as_ref();
|
||||
|
||||
// Create a per-pipeline findings scope so divisions can pass data
|
||||
let findings: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
let results = execute_primitive(
|
||||
@@ -190,19 +168,18 @@ pub fn run_company_pipeline(
|
||||
wf.options.max_concurrency,
|
||||
true,
|
||||
abort_flag,
|
||||
live_ref,
|
||||
live.as_ref(),
|
||||
session_dir,
|
||||
workspaces,
|
||||
&findings,
|
||||
None,
|
||||
)?;
|
||||
|
||||
// Collect all findings for the executive summary
|
||||
let all_findings = findings.lock()
|
||||
.map(|f| f.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(build_executive_summary(user_request, &results, &all_findings, &divisions))
|
||||
Ok(build_executive_summary(user_request, &results, &all_findings, &divisions, &custom_specialists))
|
||||
}
|
||||
|
||||
/// Run a quick company pipeline that skips non-essential divisions
|
||||
@@ -215,14 +192,21 @@ pub fn run_company_pipeline_quick(
|
||||
workspaces: &[std::path::PathBuf],
|
||||
turn_events: Option<&Arc<Mutex<std::collections::VecDeque<crate::app::state::runtime::TurnEvent>>>>,
|
||||
abort_flag: &Option<Arc<AtomicBool>>,
|
||||
custom_specialists: HashMap<String, Vec<(String, String)>>,
|
||||
) -> anyhow::Result<String> {
|
||||
let divisions = division::all_divisions();
|
||||
// Only use first 3 divisions for quick pipeline: Strategy, Engineering, Quality
|
||||
let quick_divisions = &divisions[..3];
|
||||
|
||||
let strategy_phase = make_division_phase(&quick_divisions[0], user_request);
|
||||
let engineering_phase = make_division_phase(&quick_divisions[1], user_request);
|
||||
let quality_phase = make_division_phase(&quick_divisions[2], user_request);
|
||||
let strategy_specs = custom_specialists.get("Strategy")
|
||||
.ok_or_else(|| anyhow::anyhow!("missing required division configuration: Strategy"))?;
|
||||
let engineering_specs = custom_specialists.get("Engineering")
|
||||
.ok_or_else(|| anyhow::anyhow!("missing required division configuration: Engineering"))?;
|
||||
let quality_specs = custom_specialists.get("Quality")
|
||||
.ok_or_else(|| anyhow::anyhow!("missing required division configuration: Quality"))?;
|
||||
|
||||
let strategy_phase = make_division_phase(&quick_divisions[0], user_request, strategy_specs);
|
||||
let engineering_phase = make_division_phase(&quick_divisions[1], user_request, engineering_specs);
|
||||
let quality_phase = make_division_phase(&quick_divisions[2], user_request, quality_specs);
|
||||
|
||||
let pipeline_primitive = ScriptPrimitive::Pipeline(vec![
|
||||
strategy_phase,
|
||||
@@ -268,27 +252,37 @@ pub fn run_company_pipeline_quick(
|
||||
.map(|f| f.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(build_executive_summary(user_request, &results, &all_findings, quick_divisions))
|
||||
Ok(build_executive_summary(user_request, &results, &all_findings, quick_divisions, &custom_specialists))
|
||||
}
|
||||
|
||||
/// Build a compressed executive summary from pipeline results.
|
||||
///
|
||||
/// Keeps output brief to save context window space — just division verdicts
|
||||
/// and key findings, not full outputs. Full results are accessible to the
|
||||
/// CEO via the notes/findings that were archived during execution.
|
||||
/// Flow: print user request header → for each division, fetch its specialist verdicts
|
||||
/// → join with pipes → append findings count.
|
||||
///
|
||||
/// Why: keeps output brief to save context window space. Full results are accessible
|
||||
/// to the CEO via findings.
|
||||
///
|
||||
/// Return: a formatted executive summary string.
|
||||
fn build_executive_summary(
|
||||
request: &str,
|
||||
results: &[String],
|
||||
findings: &[String],
|
||||
divisions: &[division::Division],
|
||||
custom_specialists: &HashMap<String, Vec<(String, String)>>,
|
||||
) -> String {
|
||||
let mut summary = String::new();
|
||||
writeln!(summary, "Pipeline for: {request}").unwrap();
|
||||
|
||||
for (i, div) in divisions.iter().enumerate() {
|
||||
let mut start_index = 0;
|
||||
for div in divisions {
|
||||
let count = custom_specialists.get(div.name)
|
||||
.map(Vec::len)
|
||||
.unwrap_or(0);
|
||||
|
||||
let mut division_verdicts = Vec::new();
|
||||
for offset in 0..4 {
|
||||
if let Some(r) = results.get(4 * i + offset) {
|
||||
for offset in 0..count {
|
||||
if let Some(r) = results.get(start_index + offset) {
|
||||
let first_line = r.lines().next().unwrap_or(r);
|
||||
let trimmed = first_line.chars().take(40).collect::<String>();
|
||||
division_verdicts.push(trimmed);
|
||||
@@ -302,6 +296,7 @@ fn build_executive_summary(
|
||||
};
|
||||
|
||||
writeln!(summary, " {}: {}", div.name, verdict).unwrap();
|
||||
start_index += count;
|
||||
}
|
||||
|
||||
if !findings.is_empty() {
|
||||
@@ -325,6 +320,7 @@ fn build_executive_summary(
|
||||
/// - Negative keywords (simple/trivial/typo/quick) skip the pipeline.
|
||||
/// - Positive keywords (refactor/api/implement/architecture) trigger it.
|
||||
/// - Multi-line or multi-sentence requests are more likely complex.
|
||||
#[allow(dead_code)]
|
||||
pub fn is_complex_request(request: &str) -> bool {
|
||||
let trimmed = request.trim();
|
||||
// Very short requests are never complex
|
||||
@@ -385,4 +381,25 @@ mod tests {
|
||||
assert!(is_complex_request("implement user authentication endpoint"));
|
||||
assert!(is_complex_request("refactor the whole engine module"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_make_division_specialists_custom() {
|
||||
let divisions = division::all_divisions();
|
||||
let div = &divisions[0];
|
||||
let mut custom = HashMap::new();
|
||||
custom.insert(
|
||||
"Strategy".to_string(),
|
||||
vec![
|
||||
("Custom Label".to_string(), "Custom Focus Description".to_string())
|
||||
]
|
||||
);
|
||||
let specs = make_division_specialists(div, "Test Request", &custom.get("Strategy").unwrap());
|
||||
assert_eq!(specs.len(), 1);
|
||||
if let ScriptPrimitive::Agent(prompt) = &specs[0] {
|
||||
assert!(prompt.contains("Custom Label"));
|
||||
assert!(prompt.contains("Custom Focus Description"));
|
||||
} else {
|
||||
panic!("Expected ScriptPrimitive::Agent");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user