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
+122 -52
View File
@@ -81,6 +81,10 @@ pub enum Action {
RunWorkflow {
script: String,
},
/// User-initiated pipeline via `/pipeline full|quick|skip`.
RunPipeline {
mode: String,
},
}
/// Apply an `Action` to the application state.
@@ -530,6 +534,9 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
}
}
if turn_finished {
// Consume pipeline override after each turn so it doesn't
// persist across multiple submissions.
state.misc.pipeline_override = None;
maybe_trigger_review(state);
}
if turn_finished || state.dirty {
@@ -591,6 +598,30 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
state.push_toast(Toast::new(ToastKind::Info, format!("deleted lesson: {}", name)));
state.dirty = true;
}
Action::RunPipeline { mode } => {
match mode.as_str() {
"full" => {
state.misc.pipeline_override = Some("full".to_string());
state.push_toast(Toast::new(ToastKind::Info, "Pipeline mode: full (5 divisions) — next request will run Strategy→Engineering→Quality→Security→Documentation".to_string()));
}
"quick" => {
state.misc.pipeline_override = Some("quick".to_string());
state.push_toast(Toast::new(ToastKind::Info, "Pipeline mode: quick (3 divisions) — next request will run Strategy→Engineering→Quality".to_string()));
}
"skip" => {
state.misc.pipeline_override = Some("skip".to_string());
state.push_toast(Toast::new(ToastKind::Info, "Pipeline mode: skip — next request will NOT run the company pipeline".to_string()));
}
"status" => {
let current = state.misc.pipeline_override.as_deref().unwrap_or("auto");
state.push_toast(Toast::new(ToastKind::Info, format!("Pipeline mode: {} (use /pipeline full|quick|skip to change)", current)));
}
_ => {
state.push_toast(Toast::new(ToastKind::Error, format!("Unknown pipeline mode: {} (use: full, quick, skip)", mode)));
}
}
state.dirty = true;
}
Action::RunWorkflow { script } => {
// Open the Workflow overlay so the user can see progress.
state.misc.overlay = Overlay::Workflow;
@@ -748,6 +779,7 @@ fn spawn_turn(state: &AppStateRest) {
}) = true;
let events_q = turn_events.clone();
let pipeline_mode = state.misc.pipeline_override.clone();
std::thread::spawn(move || {
let db = crate::model::msglog::open_or_create(&edit_session_dir)
@@ -767,6 +799,7 @@ fn spawn_turn(state: &AppStateRest) {
temperature,
max_tokens,
abort_flag,
pipeline_mode,
};
let result = run_agent_turn(tc, &messages, &events_q);
if let Err(e) = result {
@@ -795,6 +828,9 @@ struct TurnCtx {
temperature: f32,
max_tokens: Option<u32>,
abort_flag: std::sync::Arc<std::sync::atomic::AtomicBool>,
/// Pipeline override: None=auto, Some("full"), Some("quick"), Some("skip").
/// Set by the `/pipeline` slash command. Consumed once per turn.
pipeline_mode: Option<String>,
}
/// Build an ASCII tree of the workspace directory structure for the
@@ -993,17 +1029,16 @@ fn run_agent_turn(
}
// ── 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.
// Before the main agent starts working, check if the pipeline should run.
// The pipeline mode is determined by:
// 1. User override: `/pipeline full|quick|skip` (consumed once)
// 2. Auto-detect: `is_complex_request()` heuristics
//
// This only triggers on the first turn of a session (few user messages)
// to avoid re-planning mid-conversation.
// This only triggers on the first turn of a session to avoid re-planning.
let user_msg_count = msgs.iter()
.filter(|m| matches!(m.role, crate::dto::chat::message::Role::User))
.count();
if user_msg_count <= 2 {
let should_pipeline = if user_msg_count <= 2 {
let user_request = msgs.iter()
.rev()
.filter(|m| matches!(m.role, crate::dto::chat::message::Role::User))
@@ -1011,61 +1046,96 @@ fn run_agent_turn(
.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(),
});
if !user_request.is_empty() {
match tc.pipeline_mode.as_deref() {
Some("skip") => {
tracing::debug!("[ceo] pipeline skipped via /pipeline skip");
false
}
Some("full") => true,
Some("quick") => true,
_ => crate::app::workflow::company::is_complex_request(user_request),
}
} else {
false
}
} else {
false
};
// 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(
if should_pipeline {
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("");
let use_full = tc.pipeline_mode.as_deref() != Some("quick");
let mode_label = if use_full { "full" } else { "quick" };
tracing::info!(
"[ceo] pipeline triggered (mode={}) — delegating to company pipeline",
mode_label
);
if let Ok(mut q) = events_q.lock() {
q.push_back(TurnEvent::SystemNote {
kind: "pipeline".to_string(),
message: format!(
"Company pipeline started ({}): {} → Engineering → Quality{}",
mode_label,
"Strategy",
if use_full { " → Security → Documentation" } else { "" },
),
});
}
let pipeline_result = if use_full {
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);
)
} else {
crate::app::workflow::company::run_company_pipeline_quick(
user_request,
&tc.edit_log_session_dir,
&tc.workspace_roots,
Some(events_q),
)
};
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);
match pipeline_result {
Ok(summary) => {
tracing::info!("[ceo] company pipeline completed successfully");
let pipeline_msg = ChatMessage::system(format!(
"[Company Pipeline: {}]\n{}",
mode_label,
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: format!("Company pipeline ({}) complete. CEO reviewing results...", mode_label),
});
}
}
} else {
tracing::debug!("[ceo] request not complex — handling directly");
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] pipeline not triggered — handling directly");
}
let mut turn_step = 0usize;
+3
View File
@@ -69,6 +69,9 @@ pub fn apply_command(command: Command) -> Vec<Action> {
Command::WorkflowRun { script } => {
vec![Action::RunWorkflow { script }]
}
Command::Pipeline { mode } => {
vec![Action::RunPipeline { mode }]
}
Command::Unknown(cmd) => {
vec![Action::SystemNote {
kind: "error".to_string(),