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:
@@ -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
@@ -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,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;
|
||||
|
||||
Reference in New Issue
Block a user