Files
zesdex/src/app/workflow/company.rs
T

389 lines
16 KiB
Rust
Raw Normal View History

//! Company-style workflow orchestrator: runs the complete division pipeline
//! (Strategy → Engineering → [Quality || Security || Documentation] in parallel)
//! with findings flowing between stages, then returns a consolidated executive
//! summary to the CEO (main agent).
//!
//! Flow:
//! ```
//! CEO Main Agent
//! │ delegates to run_company_pipeline(request)
//! ▼
//! ┌──────────────────────────────────────────────────┐
//! │ Strategy Division — plan + mermaid diagrams │ (runs sequentially first)
//! └─────────────────────────┬────────────────────────┘
//! ▼
//! ┌──────────────────────────────────────────────────┐
//! │ Engineering Division — implement per plan │ (runs sequentially second)
//! └─────────────────────────┬────────────────────────┘
//! ▼
//! ┌────────────┼────────────┐
//! ▼ ▼ ▼
//! ┌───────────┐┌───────────┐┌───────────┐
//! │ Quality ││ Security ││ Docs │ (run concurrently in parallel)
//! └───────────┘└───────────┘└───────────┘
//! │ │ │
//! └────────────┼────────────┘
//! ▼
//! CEO Main Agent delivers consolidated summary to user
//! ```
use std::collections::HashMap;
use std::fmt::Write;
use std::sync::{Arc, Mutex, atomic::AtomicBool};
use crate::app::workflow::script::{ScriptPrimitive, ScriptOptions, WorkflowScript};
use crate::app::workflow::engine::{execute_primitive, LiveStateFn, AgentStatus};
use crate::app::subagent::division;
/// Construct the 4 specialized agents for a division.
fn make_division_specialists(
div: &division::Division,
user_request: &str,
) -> 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()
.map(|(label, focus)| {
// Prepend [Division Name: Specialist Label] so the first 40 chars
// of the prompt become the agent_name in spawn_single_agent.
let prompt = format!(
"[{}: {}]\n\n{}\n\n{}\n\nUser request: {}\n\nFindings from previous divisions: {{findings}}",
div.name,
label,
focus,
div_prompt,
user_request,
);
ScriptPrimitive::Agent(prompt)
})
.collect()
}
/// Construct a named Phase wrapper containing a Parallel block of division specialists.
fn make_division_phase(
div: &division::Division,
user_request: &str,
) -> ScriptPrimitive {
let specialists = make_division_specialists(div, user_request);
ScriptPrimitive::Phase {
name: div.name.to_string(),
script: Box::new(ScriptPrimitive::Parallel(specialists)),
}
}
/// 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)
///
/// Each division receives findings from all previous divisions, enabling
/// context to flow through the pipeline.
///
/// Returns a consolidated executive summary string.
pub fn run_company_pipeline(
user_request: &str,
session_dir: &std::path::Path,
workspaces: &[std::path::PathBuf],
turn_events: Option<&Arc<Mutex<std::collections::VecDeque<crate::app::state::runtime::TurnEvent>>>>,
abort_flag: &Option<Arc<AtomicBool>>,
) -> 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 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 parallel_divisions = ScriptPrimitive::Parallel(vec![
quality_phase,
security_phase,
documentation_phase,
]);
let pipeline_primitive = ScriptPrimitive::Pipeline(vec![
strategy_phase,
engineering_phase,
parallel_divisions,
]);
let wf = WorkflowScript {
name: "company-pipeline".to_string(),
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
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| {
let display_name = agent_name.chars().take(30).collect::<String>();
if let Ok(mut q) = events.lock() {
q.push_back(crate::app::state::runtime::TurnEvent::WorkflowAgentUpdate {
agent_id: display_name.clone(),
agent_name: display_name,
status,
});
}
});
f
});
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(
&wf.script,
&args,
wf.options.max_concurrency,
true,
abort_flag,
live_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))
}
/// Run a quick company pipeline that skips non-essential divisions
/// for simple tasks. Flow: Strategy → Engineering → Quality.
///
/// This is for smaller tasks where security audit and full docs are overkill.
pub fn run_company_pipeline_quick(
user_request: &str,
session_dir: &std::path::Path,
workspaces: &[std::path::PathBuf],
turn_events: Option<&Arc<Mutex<std::collections::VecDeque<crate::app::state::runtime::TurnEvent>>>>,
abort_flag: &Option<Arc<AtomicBool>>,
) -> 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 pipeline_primitive = ScriptPrimitive::Pipeline(vec![
strategy_phase,
engineering_phase,
quality_phase,
]);
let wf = WorkflowScript {
name: "company-pipeline-quick".to_string(),
description: "Company Pipeline (quick): Strategy → Engineering → Quality".to_string(),
script: pipeline_primitive,
options: ScriptOptions {
max_concurrency: 10,
continue_on_error: true,
timeout_ms: None,
},
};
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| {
let display_name = agent_name.chars().take(30).collect::<String>();
if let Ok(mut q) = events.lock() {
q.push_back(crate::app::state::runtime::TurnEvent::WorkflowAgentUpdate {
agent_id: display_name.clone(),
agent_name: display_name,
status,
});
}
});
f
});
let args: HashMap<String, String> = HashMap::new();
let findings: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let results = execute_primitive(
&wf.script, &args, wf.options.max_concurrency, true,
abort_flag, live.as_ref(), session_dir, workspaces, &findings, None,
)?;
let all_findings = findings.lock()
.map(|f| f.clone())
.unwrap_or_default();
Ok(build_executive_summary(user_request, &results, &all_findings, quick_divisions))
}
/// 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.
fn build_executive_summary(
request: &str,
results: &[String],
findings: &[String],
divisions: &[division::Division],
) -> String {
let mut summary = String::new();
writeln!(summary, "Pipeline for: {request}").unwrap();
for (i, div) in divisions.iter().enumerate() {
let mut division_verdicts = Vec::new();
for offset in 0..4 {
if let Some(r) = results.get(4 * i + offset) {
let first_line = r.lines().next().unwrap_or(r);
let trimmed = first_line.chars().take(40).collect::<String>();
division_verdicts.push(trimmed);
}
}
let verdict = if division_verdicts.is_empty() {
"—".to_string()
} else {
division_verdicts.join(" | ")
};
writeln!(summary, " {}: {}", div.name, verdict).unwrap();
}
if !findings.is_empty() {
writeln!(summary, " Notes: {} cross-division finding(s)", findings.len()).unwrap();
}
summary
}
/// Determine whether a request is complex enough for the full pipeline
/// or can use the quick version.
///
/// Simple = single file, minor fix, quick lookup, config change.
/// Complex = new feature, multi-file refactor, architecture change.
///
/// Used by the auto-CEO pipeline trigger in `run_agent_turn` to decide
/// whether to delegate to the full company pipeline or handle directly.
///
/// Heuristics:
/// - Very short requests (< 10 chars) are never complex.
/// - 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.
pub fn is_complex_request(request: &str) -> bool {
let trimmed = request.trim();
// Very short requests are never complex
if trimmed.len() < 10 {
return false;
}
// Single-line simple update patterns
let lower = trimmed.to_lowercase();
let negative_keywords = [
"simple", "trivial", "typo", "just a", "only a", "minor",
"quick", "tiny", "small fix", "rename", "nitpick",
"cosmetic", "formatting", "spelling", "grammar",
"bump", "version bump", "update comment",
];
if negative_keywords.iter().any(|k| lower.contains(k)) {
return false;
}
// Multi-line/multi-sentence → likely complex
let sentences = trimmed.split(['.', '!', '?'])
.filter(|s| !s.trim().is_empty())
.count();
if sentences >= 3 {
return true;
}
// Positive complexity keywords
let complexity_keywords = [
"refactor", "redesign", "architecture", "feature", "implement",
"migrate", "restructure", "rewrite", "new module", "new component",
"scaffold", "multi", "multiple files", "api", "endpoint",
"integration", "system", "workflow", "pipeline", "database",
"authentication", "authorization", "full stack",
];
complexity_keywords.iter().any(|k| lower.contains(k))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_complex_request_too_short() {
assert!(!is_complex_request("abc"));
}
#[test]
fn test_is_complex_request_simple_keywords() {
assert!(!is_complex_request("just a simple update to the readme"));
assert!(!is_complex_request("minor typo fix in main.rs"));
}
#[test]
fn test_is_complex_request_multi_sentence() {
assert!(is_complex_request("This is sentence one. This is sentence two. This is sentence three."));
}
#[test]
fn test_is_complex_request_complex_keywords() {
assert!(is_complex_request("implement user authentication endpoint"));
assert!(is_complex_request("refactor the whole engine module"));
}
}