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
+199 -4
View File
@@ -380,6 +380,43 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
}
} else if kind == "connectivity" {
state.misc.api_connected = message == "connected";
} else if kind == "pipeline" {
// Clear old workflow agents when a new pipeline starts.
if message.contains("started") {
state.workflow_engine.agents.clear();
state.workflow_engine.findings.clear();
}
state.push_toast(Toast {
kind: ToastKind::Info,
message: message.clone(),
created_at: chrono::Utc::now().timestamp_millis(),
lifetime_ms: 12000,
});
state.dirty = true;
} else if kind == "bg-test-gen" {
state.push_toast(Toast {
kind: ToastKind::Info,
message: message.clone(),
created_at: chrono::Utc::now().timestamp_millis(),
lifetime_ms: 8000,
});
state.dirty = true;
} else if kind == "bg-arch-review" {
state.push_toast(Toast {
kind: ToastKind::Info,
message: message.clone(),
created_at: chrono::Utc::now().timestamp_millis(),
lifetime_ms: 10000,
});
state.dirty = true;
} else if kind == "bg-security-review" {
state.push_toast(Toast {
kind: ToastKind::Info,
message: message.clone(),
created_at: chrono::Utc::now().timestamp_millis(),
lifetime_ms: 10000,
});
state.dirty = true;
} else if kind == "workflow_done" {
state.push_toast(Toast {
kind: ToastKind::Success,
@@ -608,12 +645,11 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
// Build a live-state callback that pushes WorkflowAgentUpdate events
// into the turn_events queue so the TUI panel updates in real time.
let live: LiveStateFn = Arc::new(move |agent_id: String, status: AgentStatus| {
let name = agent_id.chars().take(30).collect::<String>();
let live: LiveStateFn = Arc::new(move |agent_id: String, agent_name: String, status: AgentStatus| {
if let Ok(mut q) = turn_events_live.lock() {
q.push_back(crate::app::state::runtime::TurnEvent::WorkflowAgentUpdate {
agent_id: agent_id.clone(),
agent_name: name,
agent_name,
status,
});
}
@@ -899,6 +935,11 @@ const MAX_TURN_STEPS: usize = 10000;
/// exhausted (e.g. slow LLM responses, stuck tool calls).
const MAX_TURN_TIMEOUT_MS: u64 = 300_000;
/// Maximum number of auto inline reviews spawned per single agent turn.
/// After N edits, the inline review is skipped to keep the turn fast;
/// background subagents still fire at the end of the turn.
const MAX_AUTO_REVIEWS_PER_TURN: usize = 2;
/// Execute one full agent turn: stream the conversation to the LLM,
/// handle tool calls, and loop until the LLM produces a non-tool response
/// or runs out of unfinished todo items.
@@ -924,10 +965,12 @@ const MAX_TURN_TIMEOUT_MS: u64 = 300_000;
fn run_agent_turn(
tc: TurnCtx,
messages: &[ChatMessage],
events_q: &std::sync::Mutex<VecDeque<TurnEvent>>,
events_q: &std::sync::Arc<std::sync::Mutex<VecDeque<TurnEvent>>>,
) -> anyhow::Result<()> {
let mut msgs = messages.to_vec();
let mut edits_this_turn = 0u32;
let mut edited_paths: Vec<String> = Vec::new();
let mut inline_reviews_count: usize = 0;
let mut prev_shaped = false;
let turn_start_ms = std::time::Instant::now();
@@ -949,6 +992,82 @@ fn run_agent_turn(
msgs.insert(0, sys);
}
// ── AUTO CEO PIPELINE ──
// Before the main agent starts working, check if the request is complex
// enough to warrant the full company pipeline. If so, delegate to the
// divisions (Strategy → Engineering → Quality → Security → Documentation)
// and inject the results before the main agent even starts.
//
// This only triggers on the first turn of a session (few user messages)
// to avoid re-planning mid-conversation.
let user_msg_count = msgs.iter()
.filter(|m| matches!(m.role, crate::dto::chat::message::Role::User))
.count();
if user_msg_count <= 2 {
let user_request = msgs.iter()
.rev()
.filter(|m| matches!(m.role, crate::dto::chat::message::Role::User))
.next()
.and_then(|m| m.content.as_deref())
.unwrap_or("");
if !user_request.is_empty()
&& crate::app::workflow::company::is_complex_request(user_request)
{
tracing::info!(
"[ceo] complex request detected — delegating to company pipeline"
);
// Notify TUI that pipeline is starting
if let Ok(mut q) = events_q.lock() {
q.push_back(TurnEvent::SystemNote {
kind: "pipeline".to_string(),
message: "Company pipeline started: Strategy → Engineering → Quality → Security → Documentation".to_string(),
});
}
// Run the full company pipeline (blocks this thread — OK since
// run_agent_turn already runs on a dedicated thread).
match crate::app::workflow::company::run_company_pipeline(
user_request,
&tc.edit_log_session_dir,
&tc.workspace_roots,
Some(events_q),
) {
Ok(summary) => {
tracing::info!("[ceo] company pipeline completed successfully");
let pipeline_msg = ChatMessage::system(format!(
"=== Company Pipeline — Executive Summary ===\n\
The divisions have completed their work.\n\
Review the results below as CEO, then deliver to the user.\n\n\
{}",
summary,
));
archive_message(&tc.db, &tc.session_id, &pipeline_msg);
msgs.push(pipeline_msg);
if let Ok(mut q) = events_q.lock() {
q.push_back(TurnEvent::SystemNote {
kind: "pipeline".to_string(),
message: "Company pipeline complete. CEO reviewing results...".to_string(),
});
}
}
Err(e) => {
tracing::warn!("[ceo] company pipeline failed: {}", e);
let fail_msg = ChatMessage::system(format!(
"[Pipeline Note] The company pipeline encountered issues: {}.\n\
Proceeding with direct execution as fallback.",
e,
));
msgs.push(fail_msg);
}
}
} else {
tracing::debug!("[ceo] request not complex — handling directly");
}
}
let mut turn_step = 0usize;
let mut todo_retry_count = 0usize;
const MAX_TODO_RETRIES: usize = 5;
@@ -1145,8 +1264,61 @@ fn run_agent_turn(
if is_edit {
edits_this_turn += 1;
// ── Auto-subagent orchestration ──
// Extract path from tool args for auto-review and
// background subagent tracking.
let edit_path = args.get("path")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if let Some(ref p) = edit_path {
edited_paths.push(p.clone());
// Inline quick-review: spawn a lightweight read-only
// subagent that reviews the written file and feeds
// its verdict back into the LLM conversation so the
// agent can fix issues immediately in the same turn.
if inline_reviews_count < MAX_AUTO_REVIEWS_PER_TURN
&& crate::app::subagent::auto::is_reviewable_path(p)
{
inline_reviews_count += 1;
let review_start = std::time::Instant::now();
match crate::app::subagent::auto::spawn_quick_review(
p,
&tc.edit_log_session_dir,
&tc.workspace_roots,
) {
Ok(verdict) => {
let elapsed = review_start.elapsed().as_millis();
let review_msg = ChatMessage::tool_result(
format!("auto-review-{}", inline_reviews_count),
format!(
"[Auto inline review: {} ({}ms)]\n{}",
p,
elapsed,
verdict.trim(),
),
);
archive_message(&tc.db, &tc.session_id, &review_msg);
msgs.push(review_msg);
tracing::info!(
"[auto-review] inline review for '{}' completed in {}ms: {}",
p, elapsed,
verdict.lines().next().unwrap_or(&verdict).trim(),
);
}
Err(e) => {
tracing::warn!(
"[auto-review] inline review failed for '{}': {}",
p, e,
);
}
}
}
}
}
let tool_path = args.get("path").and_then(|v| v.as_str()).map(|s| s.to_string());
{
@@ -1221,6 +1393,29 @@ fn run_agent_turn(
message: edits_this_turn.to_string(),
});
}
// ── Background auto-subagents ──
// After a turn with edits, spawn deeper-analysis subagents in the
// background (test generation, architecture review, security review).
// These run asynchronously on OS threads and report via SystemNote
// events, so they do not block the main agent or TUI.
//
// Only spawn background agents if we actually accumulated paths
// (safety check — should always be true when edits_this_turn > 0).
if !edited_paths.is_empty() {
let bg_paths = edited_paths.clone();
let bg_session_dir = tc.edit_log_session_dir.clone();
let bg_workspaces = tc.workspace_roots.clone();
let bg_events = events_q.clone();
std::thread::spawn(move || {
crate::app::subagent::auto::spawn_all_background(
&bg_paths,
&bg_session_dir,
&bg_workspaces,
&bg_events,
);
});
}
}
if let Ok(mut q) = events_q.lock() {