feat: implement company pipeline orchestration with user commands for full, quick, and skip modes

This commit is contained in:
asepharyana
2026-07-13 05:33:04 +07:00
parent a6eed9e574
commit 2310c2df7f
9 changed files with 269 additions and 94 deletions
+43 -28
View File
@@ -185,7 +185,11 @@ pub fn run_company_pipeline_quick(
Ok(build_executive_summary(user_request, &results, &all_findings, quick_divisions))
}
/// Build a consolidated executive summary from pipeline results.
/// Build a compressed executive summary from pipeline results.
///
/// Keeps output brief to save context window space — just division verdicts
/// and key findings, not full outputs. Full results are accessible to the
/// CEO via the notes/findings that were archived during execution.
fn build_executive_summary(
request: &str,
results: &[String],
@@ -193,41 +197,23 @@ fn build_executive_summary(
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");
summary.push_str(&format!("Pipeline for: {}\n", request));
for (i, div) in divisions.iter().enumerate() {
let result_summary = results.get(i)
let verdict = 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()
}
r.lines().next().unwrap_or(r)
.chars().take(100).collect::<String>()
})
.unwrap_or_else(|| "No output".to_string());
.unwrap_or_else(|| "".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));
summary.push_str(&format!(" {}: {}\n", div.name, verdict));
}
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(&format!(" Notes: {} cross-division finding(s)\n", findings.len()));
}
summary.push_str("---\n");
summary.push_str(&format!(
"Pipeline completed: {} division(s) executed.\n",
divisions.len(),
));
summary
}
@@ -239,14 +225,43 @@ fn build_executive_summary(
///
/// Used by the auto-CEO pipeline trigger in run_agent_turn to decide
/// whether to delegate to the full company pipeline or handle directly.
///
/// Heuristics:
/// - Very short requests (< 10 chars) are never complex.
/// - Negative keywords (simple/trivial/typo/quick) skip the pipeline.
/// - Positive keywords (refactor/api/implement/architecture) trigger it.
/// - Multi-line or multi-sentence requests are more likely complex.
pub fn is_complex_request(request: &str) -> bool {
let trimmed = request.trim();
// Very short requests are never complex
if trimmed.len() < 10 {
return false;
}
// Single-line simple update patterns
let lower = trimmed.to_lowercase();
let negative_keywords = [
"simple", "trivial", "typo", "just a", "only a", "minor",
"quick", "tiny", "small fix", "rename", "nitpick",
"cosmetic", "formatting", "spelling", "grammar",
"bump", "version bump", "update comment",
];
if negative_keywords.iter().any(|k| lower.contains(k)) {
return false;
}
// Multi-line/multi-sentence → likely complex
let sentences = trimmed.split(|c| c == '.' || c == '!' || c == '?')
.filter(|s| !s.trim().is_empty())
.count();
if sentences >= 3 {
return true;
}
// Positive complexity keywords
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",
"authentication", "authorization", "full stack",
];
let lower = request.to_lowercase();
complexity_keywords.iter().any(|k| lower.contains(k))
}
+41 -9
View File
@@ -36,6 +36,9 @@ pub struct AgentStatus {
pub started_at: Option<i64>,
pub completed_at: Option<i64>,
pub error: Option<String>,
/// Human-readable progress message (e.g. "editing src/main.rs",
/// "running cargo test"). Shown in the TUI panel alongside the state.
pub progress: Option<String>,
}
/// A single agent tracked within a workflow run.
@@ -124,6 +127,7 @@ fn spawn_single_agent(
started_at: Some(started_at),
completed_at: None,
error: None,
progress: None,
},
);
}
@@ -156,26 +160,52 @@ fn spawn_single_agent(
// long-running agents. No abort mechanism is wired yet at this level;
// future work can expose a kill-switch per agent via the live callback.
// Create an mpsc channel and drain events in a background thread so
// run_subagent's blocking_send never blocks (previously the _rx was
// dropped immediately, which would cause blocking_send to panic/fail
// on a closed channel).
// Create an mpsc channel and drain events in a background thread.
// The drain thread also pushes intra-division progress updates to the
// live callback (current tool being executed), so the TUI panel shows
// real-time "editing X" or "running build" instead of just "Running…".
let (tx, rx) = tokio::sync::mpsc::channel(64);
let drain_agent_id = agent_id.to_string();
let drain_agent_name = agent_name.to_string();
let drain_live = live.cloned();
let drain_started_at = started_at;
let _drain_thread = std::thread::spawn(move || {
// Drain all events so run_subagent's blocking_send never blocks.
// Individual SubagentEvent items are not surfaced to the TUI —
// the live state callbacks above handle coarse-grained Running /
// Completed / Failed status. ToolCall / ToolResult / StepCompleted
// events are traced at debug level for observability.
use crate::app::subagent::event::SubagentEvent;
let mut rx = rx;
while let Some(event) = rx.blocking_recv() {
match &event {
SubagentEvent::ToolCall { _tool, _args } => {
tracing::debug!("[subagent] tool call: {}", _tool);
// Push intra-division progress: which tool is running
if let Some(ref f) = drain_live {
f(
drain_agent_id.clone(),
drain_agent_name.clone(),
AgentStatus {
state: AgentState::Running,
started_at: Some(drain_started_at),
completed_at: None,
error: None,
progress: Some(format!("tool: {}", _tool)),
},
);
}
}
SubagentEvent::ToolResult { _tool, .. } => {
tracing::debug!("[subagent] tool result: {}", _tool);
if let Some(ref f) = drain_live {
f(
drain_agent_id.clone(),
drain_agent_name.clone(),
AgentStatus {
state: AgentState::Running,
started_at: Some(drain_started_at),
completed_at: None,
error: None,
progress: Some(format!("done: {}", _tool)),
},
);
}
}
SubagentEvent::StepCompleted { _step, .. } => {
tracing::trace!("[subagent] step {} completed", _step);
@@ -226,6 +256,7 @@ fn spawn_single_agent(
started_at: Some(started_at),
completed_at: Some(completed_at),
error: None,
progress: None,
},
),
Err(e) => f(
@@ -236,6 +267,7 @@ fn spawn_single_agent(
started_at: Some(started_at),
completed_at: Some(completed_at),
error: Some(e.to_string()),
progress: None,
},
),
}