Add subagent prompts and implement company workflow orchestration

- Introduced prompts for various subagent roles: architecture reviewer, code quality reviewer, documentation maintainer, implementation team, testing team, and security reviewer.
- Implemented the auto-subagent orchestration in `auto.rs` to manage inline and background reviews.
- Created a division structure in `division.rs` to define roles and responsibilities for each subagent.
- Developed a company workflow orchestrator in `company.rs` to run the complete division pipeline, consolidating findings and generating executive summaries.
- Added logic to determine whether to run a full or quick pipeline based on request complexity.
This commit is contained in:
asepharyana
2026-07-13 05:23:38 +07:00
parent 2856dd78b8
commit a6eed9e574
21 changed files with 1577 additions and 85 deletions
+252
View File
@@ -0,0 +1,252 @@
//! Company-style workflow orchestrator: runs the complete division pipeline
//! (Strategy → Engineering → Quality → Security → Documentation) 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 │
//! │ Engineering Division — implement per plan │
//! │ Quality Division — review + write tests │
//! │ Security Division — vulnerability audit │
//! │ Documentation Div — update docs │
//! └──────────────────────────────────────────────────┘
//! │ returns consolidated summary
//! ▼
//! CEO Main Agent delivers to user
//! ```
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use crate::app::workflow::engine::{execute_primitive, LiveStateFn, AgentStatus};
use crate::app::workflow::script::{ScriptPrimitive, ScriptOptions, WorkflowScript};
use crate::app::subagent::division;
/// Run the full company-style pipeline for a given user request.
///
/// This orchestrates all five divisions in sequence:
/// 1. **Strategy** — create plan with diagrams
/// 2. **Engineering** — implement code
/// 3. **Quality** — review + write tests
/// 4. **Security** — audit
/// 5. **Documentation** — update docs
///
/// 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>>>>,
) -> anyhow::Result<String> {
let divisions = division::all_divisions();
let mut pipeline_scripts: Vec<ScriptPrimitive> = Vec::with_capacity(divisions.len());
for div in &divisions {
let div_prompt = div.agent_def.system_prompt.as_deref().unwrap_or("");
// Prepend [Division Name] so the first 40 chars of the prompt
// become the agent_name in spawn_single_agent, making the TUI
// panel show division names instead of UUID fragments.
let prompt = format!(
"[{}]\n\n{}\n\nUser request: {}\n\nFindings from previous divisions: {{findings}}",
div.name,
div_prompt,
user_request,
);
pipeline_scripts.push(ScriptPrimitive::Agent(prompt));
}
let wf = WorkflowScript {
name: "company-pipeline".to_string(),
description: format!(
"Company Pipeline (full): Strategy → Engineering → Quality → Security → Documentation",
),
script: ScriptPrimitive::Pipeline(pipeline_scripts),
options: ScriptOptions {
max_concurrency: 1, // sequential by design
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 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,
1,
true,
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>>>>,
) -> 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 mut pipeline_scripts: Vec<ScriptPrimitive> = Vec::with_capacity(quick_divisions.len());
for div in quick_divisions {
let div_prompt = div.agent_def.system_prompt.as_deref().unwrap_or("");
let prompt = format!(
"[{}]\n\n{}\n\nUser request: {}\n\nFindings from previous divisions: {{findings}}",
div.name,
div_prompt,
user_request,
);
pipeline_scripts.push(ScriptPrimitive::Agent(prompt));
}
let wf = WorkflowScript {
name: "company-pipeline-quick".to_string(),
description: "Company Pipeline (quick): Strategy → Engineering → Quality".to_string(),
script: ScriptPrimitive::Pipeline(pipeline_scripts),
options: ScriptOptions {
max_concurrency: 1,
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, 1, true,
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 consolidated executive summary from pipeline results.
fn build_executive_summary(
request: &str,
results: &[String],
findings: &[String],
divisions: &[division::Division],
) -> String {
let mut summary = String::new();
summary.push_str(&format!("# Company Pipeline — Executive Summary\n\n"));
summary.push_str(&format!("**Request**: {}\n\n", request));
summary.push_str("## Division Results\n\n");
for (i, div) in divisions.iter().enumerate() {
let result_summary = results.get(i)
.map(|r| {
let first_line = r.lines().next().unwrap_or(r);
if first_line.len() > 120 {
format!("{}...", &first_line[..117])
} else {
first_line.to_string()
}
})
.unwrap_or_else(|| "No output".to_string());
summary.push_str(&format!("### {} Division\n", div.name));
summary.push_str(&format!("- Role: {}\n", div.description));
summary.push_str(&format!("- Result: {}\n\n", result_summary));
}
if !findings.is_empty() {
summary.push_str("## Cross-Division Findings\n\n");
for (i, f) in findings.iter().enumerate() {
summary.push_str(&format!("{}. {}\n", i + 1, f));
}
summary.push_str("\n");
}
summary.push_str("---\n");
summary.push_str(&format!(
"Pipeline completed: {} division(s) executed.\n",
divisions.len(),
));
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.
pub fn is_complex_request(request: &str) -> bool {
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",
];
let lower = request.to_lowercase();
complexity_keywords.iter().any(|k| lower.contains(k))
}
+41 -22
View File
@@ -67,9 +67,14 @@ impl WorkflowEngine {
/// Shared live state used by `run_workflow_tracked` to push real-time
/// agent status updates into the TUI's `WorkflowEngine`.
///
/// The closure receives `(agent_id, new_status)` and should update the
/// corresponding agent in `AppStateRest::workflow_engine`.
pub type LiveStateFn = Arc<dyn Fn(String, AgentStatus) + Send + Sync>;
/// The closure receives `(agent_id, agent_name, new_status)`:
/// - `agent_id`: unique identifier (UUID) for upserting the agent.
/// - `agent_name`: human-readable display name for the TUI panel.
/// - `status`: the agent's lifecycle state and timing.
///
/// Callers should use `agent_id` as the stable key and `agent_name` for
/// display purposes (e.g. the division name in the company pipeline).
pub type LiveStateFn = Arc<dyn Fn(String, String, AgentStatus) + Send + Sync>;
/// Spawn a single synchronous subagent with the given prompt, passing it
/// any findings from earlier sibling agents. Updates live state before and
@@ -107,14 +112,20 @@ fn spawn_single_agent(
let started_at = chrono::Utc::now().timestamp_millis();
// Notify UI: this agent is now running
// Notify UI: this agent is now running.
// Pass both the unique agent_id (UUID for stable key) and agent_name
// (human-readable display name, e.g. division name).
if let Some(f) = live {
f(agent_id.to_string(), AgentStatus {
state: AgentState::Running,
started_at: Some(started_at),
completed_at: None,
error: None,
});
f(
agent_id.to_string(),
agent_name.to_string(),
AgentStatus {
state: AgentState::Running,
started_at: Some(started_at),
completed_at: None,
error: None,
},
);
}
let def = AgentDefinition::new(agent_name.to_string(), "coder".to_string())
@@ -207,18 +218,26 @@ fn spawn_single_agent(
// Notify UI: agent completed or failed
if let Some(f) = live {
match &result {
Ok(_) => f(agent_id.to_string(), AgentStatus {
state: AgentState::Completed,
started_at: Some(started_at),
completed_at: Some(completed_at),
error: None,
}),
Err(e) => f(agent_id.to_string(), AgentStatus {
state: AgentState::Failed,
started_at: Some(started_at),
completed_at: Some(completed_at),
error: Some(e.to_string()),
}),
Ok(_) => f(
agent_id.to_string(),
agent_name.to_string(),
AgentStatus {
state: AgentState::Completed,
started_at: Some(started_at),
completed_at: Some(completed_at),
error: None,
},
),
Err(e) => f(
agent_id.to_string(),
agent_name.to_string(),
AgentStatus {
state: AgentState::Failed,
started_at: Some(started_at),
completed_at: Some(completed_at),
error: Some(e.to_string()),
},
),
}
}
+1
View File
@@ -1,5 +1,6 @@
//! Workflow orchestration: a script interpreter that runs pipeline/parallel
//! primitives across multiple subagent instances.
pub mod company;
pub mod engine;
pub mod script;