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:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user