Files
zesdex/src/app/workflow/company.rs
T
asepharyana 1d50b94eec feat: update README and documentation for new tools and features
- Updated README.md to reflect the addition of 3 new built-in tools, bringing the total to 37.
- Revised architecture documentation to indicate the increase in tool count.
- Enhanced backend documentation with updated line counts for various modules.
- Modified data documentation to change edit log format from JSON to JSONL.
- Updated dependencies documentation to reflect version upgrades for several crates.
- Improved prompts for auto-reviewer, division implementer, planner, tester, and quality reviewer to enforce stricter coding standards regarding linter bypasses.
- Refactored code in various modules to improve clarity and performance, including updates to error handling and tool execution logic.
- Added comprehensive tests for IPC frame serialization and deserialization.
2026-07-13 14:39:39 +07:00

407 lines
16 KiB
Rust

//! 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 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("");
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:\n{{{{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.
///
/// 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, specs);
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.
///
/// Each division receives findings from all previous divisions, enabling
/// context to flow through the pipeline.
///
/// Returns a consolidated executive summary string.
#[allow(clippy::ref_option)]
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>>,
custom_specialists: &HashMap<String, Vec<(String, String)>>,
) -> anyhow::Result<String> {
let divisions = division::all_divisions();
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 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,
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,
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, &divisions, custom_specialists))
}
/// 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.
#[allow(clippy::ref_option)]
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>>,
custom_specialists: &HashMap<String, Vec<(String, String)>>,
) -> anyhow::Result<String> {
let divisions = division::all_divisions();
let quick_divisions = &divisions[..3];
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,
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, custom_specialists))
}
/// Build a compressed executive summary from pipeline results.
///
/// 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();
let mut start_index = 0;
for div in divisions {
let count = custom_specialists.get(div.name)
.map_or(0, Vec::len);
let mut division_verdicts = Vec::new();
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);
}
}
let verdict = if division_verdicts.is_empty() {
"—".to_string()
} else {
division_verdicts.join(" | ")
};
writeln!(summary, " {}: {}", div.name, verdict).unwrap();
start_index += count;
}
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.
#[allow(dead_code)]
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"));
}
#[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");
}
}
}