feat: remove pipeline command and refactor workflow execution to use custom specialists

This commit is contained in:
asepharyana
2026-07-13 14:39:39 +07:00
parent 3b660e09a8
commit 00e29139c5
8 changed files with 277 additions and 178 deletions
+91 -74
View File
@@ -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");
}
}
}
+44 -2
View File
@@ -134,7 +134,35 @@ fn spawn_single_agent(
);
}
let def = AgentDefinition::new(agent_name.to_string(), "coder".to_string());
let mut role = "coder".to_string();
let mut allowed_tools = None;
if agent_name.contains("Strategy") {
let div_def = crate::app::subagent::division::strategy_division();
role = div_def.role;
allowed_tools = div_def.allowed_tools;
} else if agent_name.contains("Engineering") {
let div_def = crate::app::subagent::division::engineering_division();
role = div_def.role;
allowed_tools = div_def.allowed_tools;
} else if agent_name.contains("Quality") {
let div_def = crate::app::subagent::division::quality_division();
role = div_def.role;
allowed_tools = div_def.allowed_tools;
} else if agent_name.contains("Security") {
let div_def = crate::app::subagent::division::security_division();
role = div_def.role;
allowed_tools = div_def.allowed_tools;
} else if agent_name.contains("Documentation") {
let div_def = crate::app::subagent::division::documentation_division();
role = div_def.role;
allowed_tools = div_def.allowed_tools;
}
let mut def = AgentDefinition::new(agent_name.to_string(), role);
if let Some(tools) = allowed_tools {
def = def.with_allowed_tools(tools);
}
let mut ctx = build_subagent_context(&def);
ctx.session_dir = session_dir.to_path_buf();
ctx.workspaces = workspaces.to_vec();
@@ -344,8 +372,22 @@ pub fn execute_primitive(
) -> anyhow::Result<Vec<String>> {
match primitive {
ScriptPrimitive::Agent(prompt) => {
let resolved = resolve_template(prompt, args);
let mut resolved_args = args.clone();
let findings_snapshot = findings.lock().map(|f| f.clone()).unwrap_or_default();
if !resolved_args.contains_key("findings") {
let formatted_findings = if findings_snapshot.is_empty() {
"None".to_string()
} else {
findings_snapshot
.iter()
.enumerate()
.map(|(i, f)| format!("{}. {}", i + 1, f))
.collect::<Vec<_>>()
.join("\n")
};
resolved_args.insert("findings".to_string(), formatted_findings);
}
let resolved = resolve_template(prompt, &resolved_args);
let agent_id = uuid::Uuid::new_v4().to_string();
let agent_name = resolved.chars().take(40).collect::<String>();
match spawn_single_agent(&agent_id, &agent_name, &resolved, &findings_snapshot, findings, abort_flag, live, session_dir, workspaces, timeout_ms) {