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
+428
View File
@@ -0,0 +1,428 @@
//! Auto-subagent orchestration: the main agent automatically delegates
//! review, test-generation, architecture-review, and security-review tasks
//! to subagents without requiring explicit tool calls from the LLM.
//!
//! Two modes:
//! - **Inline** (`spawn_quick_review`): runs synchronously within the turn
//! after each write/edit tool call. Results are fed back into the LLM
//! conversation so the agent can act on feedback immediately.
//! - **Background** (`spawn_background_*`): runs asynchronously on a
//! dedicated OS thread at the end of a turn. Reports results via
//! `TurnEvent::SystemNote`, consumed by the TUI on the next Tick.
//!
//! Why inline vs background:
//! - Inline reviews give the agent an immediate feedback loop ("I just
//! wrote this file, let me check if it's correct before continuing").
//! - Background reviews catch broader concerns (missing tests, architectural
//! drift, security issues) without blocking the main agent's flow.
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::collections::VecDeque;
use crate::app::state::runtime::TurnEvent;
use crate::app::subagent::context::build_subagent_context;
use crate::app::subagent::engine::run_subagent;
use crate::app::subagent::spawn::AgentDefinition;
use crate::app::subagent::event::SubagentEvent;
/// File extensions that should not trigger auto-review (config, lock, data).
const SKIP_REVIEW_EXTENSIONS: &[&str] = &[
".lock", ".md", ".txt", ".json", ".toml", ".yaml", ".yml",
".svg", ".png", ".jpg", ".ico", ".woff", ".woff2",
];
/// File names that should not trigger auto-review.
const SKIP_REVIEW_FILES: &[&str] = &[
"Cargo.lock", "yarn.lock", "package-lock.json",
".gitignore", ".env", ".env.example",
];
/// Maximum LLM steps for a quick-review subagent. Keeps reviews fast.
const QUICK_REVIEW_MAX_STEPS: usize = 2;
/// Maximum LLM steps for background subagents (test gen, arch, security).
const BG_SUBAGENT_MAX_STEPS: usize = 8;
/// ─── Helpers ───
/// Check whether a file path is worth auto-reviewing (not config/lock/data).
pub fn is_reviewable_path(path: &str) -> bool {
let lower = path.to_lowercase();
if SKIP_REVIEW_FILES.iter().any(|f| lower.ends_with(f)) {
return false;
}
if SKIP_REVIEW_EXTENSIONS.iter().any(|e| lower.ends_with(e)) {
return false;
}
// Skip paths that are clearly generated or vendored
if lower.contains("/target/") || lower.contains("/node_modules/")
|| lower.contains("/.git/") || lower.contains("/vendor/")
{
return false;
}
true
}
/// Determine whether a file change looks like it modifies production logic
/// (vs. tests, config, or documentation) — used to decide if a test-gen
/// or security-review background subagent should fire.
fn is_production_code(path: &str) -> bool {
let lower = path.to_lowercase();
// Skip test files — they don't need test-gen from another agent
if lower.contains("test") || lower.contains("spec") || lower.contains("_test.") {
return false;
}
// Only source files
lower.ends_with(".rs") || lower.ends_with(".ts") || lower.ends_with(".tsx")
|| lower.ends_with(".js") || lower.ends_with(".jsx") || lower.ends_with(".go")
|| lower.ends_with(".py") || lower.ends_with(".java") || lower.ends_with(".kt")
|| lower.ends_with(".swift") || lower.ends_with(".c") || lower.ends_with(".cpp")
|| lower.ends_with(".h") || lower.ends_with(".hpp")
}
/// ─── Inline Quick Review (synchronous, feeds back to LLM) ───
/// Spawn a lightweight inline code review subagent for the given file.
///
/// The subagent reads the file (read-only), checks for common issues,
/// and returns a concise text verdict. This runs synchronously so the
/// main agent's `run_agent_turn` can inject the result back into the
/// LLM conversation for immediate action.
///
/// Returns `Ok(verdict)` if the review completed, or an error if the
/// subagent could not be spawned or failed internally. Callers should
/// log and swallow errors gracefully — a failed inline review should
/// never interrupt the main agent's flow.
pub fn spawn_quick_review(
file_path: &str,
session_dir: &Path,
workspaces: &[std::path::PathBuf],
) -> anyhow::Result<String> {
let prompt = format!(
"{}\n\nFile to review: {}",
crate::resources::AUTO_REVIEWER_PROMPT,
file_path,
);
let def = AgentDefinition::new(
"quick-reviewer".to_string(),
"reviewer".to_string(),
)
.with_system_prompt(prompt)
.with_max_steps(QUICK_REVIEW_MAX_STEPS);
let mut ctx = build_subagent_context(def);
ctx.session_dir = session_dir.to_path_buf();
ctx.workspaces = workspaces.to_vec();
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
let _drain = std::thread::spawn(move || {
while let Some(event) = rx.blocking_recv() {
match &event {
SubagentEvent::ToolCall { _tool, .. } => {
tracing::debug!("[auto-review] tool call: {}", _tool);
}
SubagentEvent::ToolResult { _tool, .. } => {
tracing::debug!("[auto-review] tool result: {}", _tool);
}
SubagentEvent::Completed { .. } => {
tracing::debug!("[auto-review] completed");
}
_ => {}
}
}
});
let verdict = run_subagent(ctx, tx)?;
tracing::info!(
"[auto-review] quick review for '{}': {}",
file_path,
verdict.lines().next().unwrap_or(&verdict),
);
Ok(verdict)
}
/// ─── Background Subagent Spawners (async, report via SystemNote) ───
/// Spawn a background subagent that generates tests for modified files.
///
/// Uses the test-generator prompt and has read-write access so it can
/// create test files. Runs in a separate OS thread and reports completion
/// via `TurnEvent::SystemNote { kind: "bg-test-gen" }`.
pub fn spawn_background_test_gen(
file_paths: &[String],
session_dir: &Path,
workspaces: &[std::path::PathBuf],
turn_events: &Arc<Mutex<VecDeque<TurnEvent>>>,
) {
if file_paths.is_empty() {
return;
}
let paths = file_paths.to_vec();
let sd = session_dir.to_path_buf();
let ws = workspaces.to_vec();
let events = turn_events.clone();
std::thread::spawn(move || {
tracing::info!(
"[bg-test-gen] spawning for {} file(s): {:?}",
paths.len(),
paths,
);
let file_list = paths.join("\n");
let prompt = format!(
"{}\n\nModified files that need tests:\n{}",
crate::resources::TEST_GENERATOR_PROMPT,
file_list,
);
let def = AgentDefinition::new(
"test-generator".to_string(),
"coder".to_string(), // needs write access
)
.with_system_prompt(prompt)
.with_max_steps(BG_SUBAGENT_MAX_STEPS);
let mut ctx = build_subagent_context(def);
ctx.session_dir = sd;
ctx.workspaces = ws;
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
let _drain = std::thread::spawn(move || {
while let Some(event) = rx.blocking_recv() {
match &event {
SubagentEvent::ToolCall { _tool, .. } => {
tracing::debug!("[bg-test-gen] tool: {}", _tool);
}
SubagentEvent::ToolResult { _tool, .. } => {
tracing::debug!("[bg-test-gen] result: {}", _tool);
}
SubagentEvent::StepCompleted { _step, .. } => {
tracing::trace!("[bg-test-gen] step {} done", _step);
}
SubagentEvent::StepFailed { _step, _error } => {
tracing::warn!("[bg-test-gen] step {} failed: {}", _step, _error);
}
SubagentEvent::Completed { .. } => {
tracing::debug!("[bg-test-gen] completed");
}
}
}
});
let result = run_subagent(ctx, tx);
let message = match &result {
Ok(output) => {
let first = output.lines().next().unwrap_or(output);
format!("Auto test-gen: {}", first)
}
Err(e) => format!("Auto test-gen failed: {}", e),
};
if let Ok(mut q) = events.lock() {
q.push_back(TurnEvent::SystemNote {
kind: "bg-test-gen".to_string(),
message,
});
}
});
}
/// Spawn a background architecture-review subagent.
///
/// Inspects the modified files for architectural consistency (layering,
/// coupling, module boundaries). Reports via
/// `TurnEvent::SystemNote { kind: "bg-arch-review" }`.
pub fn spawn_background_arch_review(
file_paths: &[String],
session_dir: &Path,
workspaces: &[std::path::PathBuf],
turn_events: &Arc<Mutex<VecDeque<TurnEvent>>>,
) {
if file_paths.is_empty() {
return;
}
let paths = file_paths.to_vec();
let sd = session_dir.to_path_buf();
let ws = workspaces.to_vec();
let events = turn_events.clone();
std::thread::spawn(move || {
let file_list = paths.join("\n");
let prompt = format!(
"{}\n\nModified files for architecture review:\n{}",
crate::resources::ARCH_REVIEWER_PROMPT,
file_list,
);
let def = AgentDefinition::new(
"arch-reviewer".to_string(),
"reviewer".to_string(),
)
.with_system_prompt(prompt)
.with_max_steps(BG_SUBAGENT_MAX_STEPS);
let mut ctx = build_subagent_context(def);
ctx.session_dir = sd;
ctx.workspaces = ws;
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
let _drain = std::thread::spawn(move || {
while let Some(event) = rx.blocking_recv() {
match &event {
SubagentEvent::ToolCall { _tool, .. } => {
tracing::debug!("[bg-arch] tool: {}", _tool);
}
SubagentEvent::ToolResult { _tool, .. } => {
tracing::debug!("[bg-arch] result: {}", _tool);
}
SubagentEvent::Completed { .. } => {
tracing::debug!("[bg-arch] completed");
}
_ => {}
}
}
});
let result = run_subagent(ctx, tx);
let message = match &result {
Ok(output) => {
let first = output.lines().next().unwrap_or(output);
format!("Architecture review: {}", first)
}
Err(e) => format!("Architecture review failed: {}", e),
};
if let Ok(mut q) = events.lock() {
q.push_back(TurnEvent::SystemNote {
kind: "bg-arch-review".to_string(),
message,
});
}
});
}
/// Spawn a background security-review subagent.
///
/// Checks modified files for security vulnerabilities. Reports via
/// `TurnEvent::SystemNote { kind: "bg-security-review" }`.
pub fn spawn_background_security_review(
file_paths: &[String],
session_dir: &Path,
workspaces: &[std::path::PathBuf],
turn_events: &Arc<Mutex<VecDeque<TurnEvent>>>,
) {
if file_paths.is_empty() {
return;
}
// Only review production code files for security — test files and
// config files are out of scope for security review.
let prod_paths: Vec<String> = file_paths
.iter()
.filter(|p| is_production_code(p))
.cloned()
.collect();
if prod_paths.is_empty() {
return;
}
let paths = prod_paths;
let sd = session_dir.to_path_buf();
let ws = workspaces.to_vec();
let events = turn_events.clone();
std::thread::spawn(move || {
let file_list = paths.join("\n");
let prompt = format!(
"{}\n\nModified files for security review:\n{}",
crate::resources::SECURITY_REVIEWER_PROMPT,
file_list,
);
let def = AgentDefinition::new(
"security-reviewer".to_string(),
"reviewer".to_string(),
)
.with_system_prompt(prompt)
.with_max_steps(BG_SUBAGENT_MAX_STEPS);
let mut ctx = build_subagent_context(def);
ctx.session_dir = sd;
ctx.workspaces = ws;
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
let _drain = std::thread::spawn(move || {
while let Some(event) = rx.blocking_recv() {
match &event {
SubagentEvent::ToolCall { _tool, .. } => {
tracing::debug!("[bg-security] tool: {}", _tool);
}
SubagentEvent::ToolResult { _tool, .. } => {
tracing::debug!("[bg-security] result: {}", _tool);
}
SubagentEvent::Completed { .. } => {
tracing::debug!("[bg-security] completed");
}
_ => {}
}
}
});
let result = run_subagent(ctx, tx);
let message = match &result {
Ok(output) => {
let first = output.lines().next().unwrap_or(output);
format!("Security review: {}", first)
}
Err(e) => format!("Security review failed: {}", e),
};
if let Ok(mut q) = events.lock() {
q.push_back(TurnEvent::SystemNote {
kind: "bg-security-review".to_string(),
message,
});
}
});
}
/// Convenience: spawn all applicable background subagents for a set of edited
/// file paths. Called once at the end of a main agent turn.
///
/// Flow: always spawns arch-review and security-review if there are
/// reviewable production files → spawns test-gen only if there are source
/// files that aren't already tests.
pub fn spawn_all_background(
file_paths: &[String],
session_dir: &Path,
workspaces: &[std::path::PathBuf],
turn_events: &Arc<Mutex<VecDeque<TurnEvent>>>,
) {
if file_paths.is_empty() {
return;
}
// Background test-gen: only for non-test source files
let source_paths: Vec<String> = file_paths
.iter()
.filter(|p| is_production_code(p))
.cloned()
.collect();
spawn_background_test_gen(&source_paths, session_dir, workspaces, turn_events);
// Background arch review: for all files that are reviewable
let reviewable: Vec<String> = file_paths
.iter()
.filter(|p| is_reviewable_path(p))
.cloned()
.collect();
spawn_background_arch_review(&reviewable, session_dir, workspaces, turn_events);
// Background security review: only production source files
spawn_background_security_review(&source_paths, session_dir, workspaces, turn_events);
}
+236
View File
@@ -0,0 +1,236 @@
//! Company-style agent divisions: specialized subagent roles that form an
//! organizational hierarchy like a company.
//!
//! ```text
//! CEO (Main Agent)
//! ├── Strategy Division (planner) — architecture, diagrams, plan
//! ├── Engineering Division (coder) — implementation
//! ├── Quality Division (tester) — review, test
//! ├── Security Division (auditor) — security audit
//! └── Documentation Division (doc) — documentation
//! ```
//!
//! Each division has a specific role, tools, and system prompt tailored to
//! its function. The main agent (CEO) delegates work to divisions via
//! the company pipeline workflow.
use crate::app::subagent::spawn::AgentDefinition;
/// Division roles — used as both the `role` field in AgentDefinition
/// and as the key for pipeline routing.
pub mod roles {
/// Strategy Division: plans architecture, creates diagrams, breaks down work.
pub const STRATEGY: &str = "planner";
/// Engineering Division: implements code per the plan.
pub const ENGINEERING: &str = "coder";
/// Quality Division: reviews implementation, writes tests.
pub const QUALITY: &str = "tester";
/// Security Division: audits for vulnerabilities.
pub const SECURITY: &str = "auditor";
/// Documentation Division: updates docs, README, inline documentation.
pub const DOCUMENTATION: &str = "documenter";
}
/// ─── Division Agent Definitions ───
/// Build the Strategy Division agent — chief architect and planner.
///
/// Tools: read-only (read, grep, glob, search, lsp, plan, seqthink, recall)
/// Role: never writes code; produces detailed plans with mermaid diagrams.
pub fn strategy_division() -> AgentDefinition {
AgentDefinition::new(
"strategy-division".to_string(),
roles::STRATEGY.to_string(),
)
.with_system_prompt(crate::resources::DIVISION_PLANNER_PROMPT.to_string())
.with_max_steps(15)
.with_allowed_tools(vec![
"read".to_string(),
"grep".to_string(),
"glob".to_string(),
"search".to_string(),
"seqthink".to_string(),
"plan".to_string(),
"recall".to_string(),
"lsp_connect".to_string(),
"lsp_diagnostics".to_string(),
"lsp_hover".to_string(),
"lsp_definition".to_string(),
"lsp_references".to_string(),
])
}
/// Build the Engineering Division agent — implements code per the plan.
///
/// Tools: full access (all write/edit/bash/git/LSP tools)
/// Role: executes the strategy plan, one file at a time.
pub fn engineering_division() -> AgentDefinition {
AgentDefinition::new(
"engineering-division".to_string(),
roles::ENGINEERING.to_string(),
)
.with_system_prompt(crate::resources::DIVISION_IMPLEMENTER_PROMPT.to_string())
.with_max_steps(50)
.with_allowed_tools(vec![
"read".to_string(),
"write".to_string(),
"edit".to_string(),
"delete".to_string(),
"bash".to_string(),
"grep".to_string(),
"glob".to_string(),
"git_operator".to_string(),
"seqthink".to_string(),
"lsp_connect".to_string(),
"lsp_diagnostics".to_string(),
"lsp_hover".to_string(),
"lsp_definition".to_string(),
"lsp_references".to_string(),
"lsp_completion".to_string(),
"lsp_disconnect".to_string(),
"todowrite".to_string(),
"todofinish".to_string(),
])
}
/// Build the Quality Division agent — reviews code and writes tests.
///
/// Tools: read, write, grep, glob, bash (for running tests), LSP, memory
/// Role: verifies correctness and creates/runs tests.
pub fn quality_division() -> AgentDefinition {
AgentDefinition::new(
"quality-division".to_string(),
roles::QUALITY.to_string(),
)
.with_system_prompt(crate::resources::DIVISION_TESTER_PROMPT.to_string())
.with_max_steps(30)
.with_allowed_tools(vec![
"read".to_string(),
"write".to_string(),
"edit".to_string(),
"grep".to_string(),
"glob".to_string(),
"bash".to_string(),
"seqthink".to_string(),
"recall".to_string(),
"remember".to_string(),
"lsp_connect".to_string(),
"lsp_diagnostics".to_string(),
"lsp_hover".to_string(),
"lsp_definition".to_string(),
"lsp_references".to_string(),
])
}
/// Build the Security Division agent — security auditor.
///
/// Tools: read-only + search + memory
/// Role: audits implementation for vulnerabilities.
pub fn security_division() -> AgentDefinition {
AgentDefinition::new(
"security-division".to_string(),
roles::SECURITY.to_string(),
)
.with_system_prompt(crate::resources::SECURITY_REVIEWER_PROMPT.to_string())
.with_max_steps(15)
.with_allowed_tools(vec![
"read".to_string(),
"grep".to_string(),
"glob".to_string(),
"search".to_string(),
"seqthink".to_string(),
"recall".to_string(),
"remember".to_string(),
"lsp_connect".to_string(),
"lsp_diagnostics".to_string(),
"lsp_hover".to_string(),
"lsp_definition".to_string(),
"lsp_references".to_string(),
])
}
/// Build the Documentation Division agent — documentation maintainer.
///
/// Tools: read, grep, glob, write, edit, memory
/// Role: updates README, inline docs, architecture docs.
pub fn documentation_division() -> AgentDefinition {
AgentDefinition::new(
"documentation-division".to_string(),
roles::DOCUMENTATION.to_string(),
)
.with_system_prompt(crate::resources::DIVISION_DOCUMENTER_PROMPT.to_string())
.with_max_steps(15)
.with_allowed_tools(vec![
"read".to_string(),
"write".to_string(),
"edit".to_string(),
"grep".to_string(),
"glob".to_string(),
"recall".to_string(),
"remember".to_string(),
])
}
/// ─── Division Registry ───
/// A named division with its agent definition and display metadata.
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct Division {
/// Display name for the division (e.g. "Strategy", "Engineering").
pub name: &'static str,
/// Role tag used for pipeline routing (matches `roles::*` constants).
pub role: &'static str,
/// One-line description of what this division does.
pub description: &'static str,
/// Agent definition with tools, prompt, and step budget.
pub agent_def: AgentDefinition,
}
impl Division {
pub fn new(
name: &'static str,
role: &'static str,
description: &'static str,
agent_def: AgentDefinition,
) -> Self {
Division { name, role, description, agent_def }
}
}
/// Return all company divisions as an ordered list matching the pipeline flow:
/// Strategy → Engineering → Quality → Security → Documentation.
pub fn all_divisions() -> Vec<Division> {
vec![
Division::new(
"Strategy",
roles::STRATEGY,
"Architecture planning with diagrams and step-by-step breakdown",
strategy_division(),
),
Division::new(
"Engineering",
roles::ENGINEERING,
"Code implementation following the plan",
engineering_division(),
),
Division::new(
"Quality",
roles::QUALITY,
"Code review and comprehensive testing",
quality_division(),
),
Division::new(
"Security",
roles::SECURITY,
"Security vulnerability audit",
security_division(),
),
Division::new(
"Documentation",
roles::DOCUMENTATION,
"Documentation updates and maintenance",
documentation_division(),
),
]
}
+2
View File
@@ -1,7 +1,9 @@
//! Subagent management: spawning, context building, engine loop, and
//! progress events.
pub mod auto;
pub mod context;
pub mod division;
pub mod engine;
pub mod event;
pub mod spawn;