feat: remove pipeline command and refactor workflow execution to use custom specialists
This commit is contained in:
+101
-70
@@ -84,10 +84,6 @@ pub enum Action {
|
||||
RunWorkflow {
|
||||
script: String,
|
||||
},
|
||||
/// User-initiated pipeline via `/pipeline full|quick|skip`.
|
||||
RunPipeline {
|
||||
mode: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Apply an `Action` to the application state.
|
||||
@@ -529,9 +525,6 @@ 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 {
|
||||
@@ -593,30 +586,7 @@ 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: {current} (use /pipeline full|quick|skip to change)")));
|
||||
}
|
||||
_ => {
|
||||
state.push_toast(Toast::new(ToastKind::Error, format!("Unknown pipeline mode: {mode} (use: full, quick, skip)")));
|
||||
}
|
||||
}
|
||||
state.dirty = true;
|
||||
}
|
||||
|
||||
Action::RunWorkflow { script } => {
|
||||
// Open the Workflow overlay so the user can see progress.
|
||||
state.misc.overlay = Overlay::Workflow;
|
||||
@@ -775,7 +745,6 @@ 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)
|
||||
@@ -795,7 +764,6 @@ 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 {
|
||||
@@ -824,9 +792,6 @@ 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
|
||||
@@ -1017,11 +982,6 @@ fn run_agent_turn(
|
||||
|
||||
// ── AUTO CEO PIPELINE ──
|
||||
// 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 to avoid re-planning.
|
||||
let user_msg_count = msgs.iter()
|
||||
.filter(|m| matches!(m.role, crate::dto::chat::message::Role::User))
|
||||
.count();
|
||||
@@ -1034,14 +994,7 @@ fn run_agent_turn(
|
||||
if user_request.is_empty() {
|
||||
false
|
||||
} else {
|
||||
match tc.pipeline_mode.as_deref() {
|
||||
Some("skip") => {
|
||||
tracing::debug!("[ceo] pipeline skipped via /pipeline skip");
|
||||
false
|
||||
}
|
||||
Some("full" | "quick") => true,
|
||||
_ => crate::app::workflow::company::is_complex_request(user_request),
|
||||
}
|
||||
crate::app::workflow::company::is_complex_request(user_request)
|
||||
}
|
||||
} else {
|
||||
false
|
||||
@@ -1053,10 +1006,10 @@ fn run_agent_turn(
|
||||
.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" };
|
||||
let use_full = true;
|
||||
let mode_label = "full";
|
||||
tracing::info!(
|
||||
"[ceo] pipeline triggered (mode={}) — delegating to company pipeline",
|
||||
"[ceo] pipeline triggered (mode={}) — dynamically generating planning workflow via LLM",
|
||||
mode_label
|
||||
);
|
||||
|
||||
@@ -1064,31 +1017,109 @@ fn run_agent_turn(
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
kind: "pipeline".to_string(),
|
||||
message: format!(
|
||||
"Company pipeline started ({}): {} → Engineering → Quality{}",
|
||||
"CEO is planning workflow (mode={})...",
|
||||
mode_label,
|
||||
"Strategy",
|
||||
if use_full { " → Security → Documentation" } else { "" },
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let pipeline_abort = Some(tc.abort_flag.clone());
|
||||
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),
|
||||
&pipeline_abort,
|
||||
)
|
||||
|
||||
// Ask LLM to dynamically generate the workflow specialists plan
|
||||
let required_divisions = if use_full {
|
||||
"all 5 divisions (Strategy, Engineering, Quality, Security, Documentation)"
|
||||
} else {
|
||||
crate::app::workflow::company::run_company_pipeline_quick(
|
||||
user_request,
|
||||
&tc.edit_log_session_dir,
|
||||
&tc.workspace_roots,
|
||||
Some(events_q),
|
||||
&pipeline_abort,
|
||||
)
|
||||
"the 3 quick divisions (Strategy, Engineering, Quality)"
|
||||
};
|
||||
let example_json = if use_full {
|
||||
r#"{
|
||||
"Strategy": [ ["Architect", "Analyze component tree..."] ],
|
||||
"Engineering": [ ["Developer", "Implement core algorithms..."] ],
|
||||
"Quality": [ ["Tester", "Write unit tests..."] ],
|
||||
"Security": [ ["Auditor", "Review dependencies..."] ],
|
||||
"Documentation": [ ["Writer", "Document API endpoints..."] ]
|
||||
}"#
|
||||
} else {
|
||||
r#"{
|
||||
"Strategy": [ ["Architect", "Analyze component tree..."] ],
|
||||
"Engineering": [ ["Developer", "Implement core algorithms..."] ],
|
||||
"Quality": [ ["Tester", "Write unit tests..."] ]
|
||||
}"#
|
||||
};
|
||||
|
||||
let system_msg = ChatMessage::system(
|
||||
"You are a professional software architect and workflow planner. \
|
||||
Generate a tailored, structured multi-agent workflow specialists plan for the requested task. \
|
||||
Do not explain. Return ONLY raw JSON matching the requested structure."
|
||||
);
|
||||
let user_msg = ChatMessage::user(format!(
|
||||
"Design a structured multi-agent workflow plan for the following task:\n\n\
|
||||
\"{}\"\n\n\
|
||||
You must output a JSON object representing the 'specialists' configuration for {}.\n\
|
||||
Each division must have a list of custom specialists defined by a pair of [label, focus_description].\n\n\
|
||||
Return ONLY a JSON object with this exact structure, with no markdown codeblocks and no explanation:\n\
|
||||
{}",
|
||||
user_request, required_divisions, example_json
|
||||
));
|
||||
|
||||
let planner_result = tc.client.chat_with_tools_non_streaming(&[system_msg, user_msg], None);
|
||||
let pipeline_result = match planner_result {
|
||||
Ok((reply, _)) => {
|
||||
let reply_text = reply.content.as_deref().unwrap_or("").trim();
|
||||
let clean_json = if reply_text.starts_with("```") {
|
||||
let mut lines = reply_text.lines();
|
||||
lines.next();
|
||||
let mut content = lines.collect::<Vec<&str>>();
|
||||
if content.last().map(|s| s.trim() == "```").unwrap_or(false) {
|
||||
content.pop();
|
||||
}
|
||||
content.join("\n")
|
||||
} else {
|
||||
reply_text.to_string()
|
||||
};
|
||||
|
||||
match serde_json::from_str::<std::collections::HashMap<String, Vec<(String, String)>>>(&clean_json) {
|
||||
Ok(custom_specialists) => {
|
||||
let spec_desc = custom_specialists.iter()
|
||||
.map(|(k, v)| format!("{}: {} agents", k, v.len()))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
kind: "pipeline".to_string(),
|
||||
message: format!(
|
||||
"CEO planned: Strategy → Engineering → Quality{}. (Config: {}) Running specialists...",
|
||||
if use_full { " → Security → Documentation" } else { "" },
|
||||
spec_desc
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if use_full {
|
||||
crate::app::workflow::company::run_company_pipeline(
|
||||
user_request,
|
||||
&tc.edit_log_session_dir,
|
||||
&tc.workspace_roots,
|
||||
Some(events_q),
|
||||
&pipeline_abort,
|
||||
custom_specialists,
|
||||
)
|
||||
} else {
|
||||
crate::app::workflow::company::run_company_pipeline_quick(
|
||||
user_request,
|
||||
&tc.edit_log_session_dir,
|
||||
&tc.workspace_roots,
|
||||
Some(events_q),
|
||||
&pipeline_abort,
|
||||
custom_specialists,
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(e) => Err(anyhow::anyhow!("Failed to parse LLM planning JSON: {}. Cleaned JSON was: {}", e, clean_json)),
|
||||
}
|
||||
}
|
||||
Err(e) => Err(anyhow::anyhow!("Failed to query LLM for planning workflow: {}", e)),
|
||||
};
|
||||
|
||||
match pipeline_result {
|
||||
|
||||
@@ -69,9 +69,6 @@ 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(),
|
||||
|
||||
Reference in New Issue
Block a user