feat: remove pipeline command and refactor workflow execution to use custom specialists

This commit is contained in:
asepharyana
2026-07-13 14:39:39 +07:00
parent 3b660e09a8
commit 00e29139c5
8 changed files with 277 additions and 178 deletions
+101 -70
View File
@@ -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 {
-3
View File
@@ -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(),
-13
View File
@@ -90,10 +90,6 @@ const COMMANDS: &[&str] = &[
"/model add",
"/workflow",
"/workflow run",
"/pipeline",
"/pipeline full",
"/pipeline quick",
"/pipeline skip",
"/compact",
];
@@ -293,14 +289,6 @@ pub struct MiscState {
pub api_context_length: Option<u32>,
pub tick_count: u64,
pub todo_content: String,
/// Pipeline mode override set by `/pipeline` command.
/// - `None`: auto-detect (default)
/// - `Some("full")`: force full pipeline
/// - `Some("quick")`: force quick pipeline
/// - `Some("skip")`: skip pipeline, handle directly
///
/// Consumed on the next agent turn.
pub pipeline_override: Option<String>,
}
impl MiscState {
@@ -319,7 +307,6 @@ impl MiscState {
api_context_length: None,
tick_count: 0,
todo_content: String::new(),
pipeline_override: None,
}
}
+8 -2
View File
@@ -28,10 +28,16 @@ use super::event::SubagentEvent;
fn build_subagent_tools(allowed_tools: &[String]) -> (Vec<Box<dyn crate::tool::Tool>>, Vec<ToolDef>) {
let all = all_tools();
let filtered: Vec<Box<dyn crate::tool::Tool>> = if allowed_tools.is_empty() {
all
all.into_iter()
.filter(|t| t.name() != "company_pipeline" && t.name() != "workflow_run")
.collect()
} else {
all.into_iter()
.filter(|t| allowed_tools.contains(&t.name().to_string()))
.filter(|t| {
allowed_tools.contains(&t.name().to_string())
&& t.name() != "company_pipeline"
&& t.name() != "workflow_run"
})
.collect()
};
let defs = tool_defs(&filtered);
+91 -74
View File
@@ -34,59 +34,28 @@ use crate::app::workflow::script::{ScriptPrimitive, ScriptOptions, WorkflowScrip
use crate::app::workflow::engine::{execute_primitive, LiveStateFn, AgentStatus};
use crate::app::subagent::division;
/// Construct the 4 specialized agents for a division.
/// Construct the specialized agents for a division.
///
/// Flow: map division name to its specialization pool.
///
/// Return: a `Vec<ScriptPrimitive>` containing the specialist agents.
fn make_division_specialists(
div: &division::Division,
user_request: &str,
specs: &[(String, String)],
) -> Vec<ScriptPrimitive> {
let div_prompt = div.agent_def.system_prompt.as_deref().unwrap_or("");
let specializations = match div.name {
"Strategy" => vec![
("Architectural Analysis", "Focus on component tree, file layout, and module structure."),
("Data Flow Planning", "Focus on sequence of calls, interface definitions, and APIs."),
("Task Breakdown", "Focus on step-by-step TODO lists and implementation order."),
("Risk Evaluation", "Focus on edge cases, compatibility, and system constraints."),
],
"Engineering" => vec![
("Core Logic", "Focus on core algorithms, mathematical processing, and backend logic."),
("Interface & Endpoints", "Focus on implementing routes, IPC handlers, and struct mappings."),
("Error Handling & Logs", "Focus on implementing robust error handling, try/catch, tracing, and Result wrappers."),
("Utility & Helpers", "Focus on filesystem helpers, input sanitization, and parsing utilities."),
],
"Quality" => vec![
("Code Reviewer", "Focus on checking coding style, naming standards, and coding conventions."),
("Unit Testing", "Focus on writing and running unit tests for individual functions and modules."),
("Integration Testing", "Focus on writing and running integration tests for system interactions and state flows."),
("Performance Analyst", "Focus on efficiency check, bottleneck analysis, and time complexity."),
],
"Security" => vec![
("Dependency Auditor", "Focus on auditing cargo lock and checking dependencies for vulnerabilities."),
("Input Sanitizer", "Focus on auditing input validation, injection prevention, path traversal, and shell safety."),
("Access Control", "Focus on auditing authorization, filesystem access permissions, and API scopes."),
("Secrets Auditor", "Focus on auditing secrets leakage, credentials safety, and log auditing."),
],
"Documentation" => vec![
("README & Setup", "Focus on updating README, installation guides, usage examples, and high-level setup."),
("API Reference", "Focus on updating API reference, parameter details, and traits/functions documentation."),
("Changelog & Architecture", "Focus on updating CHANGELOG and describing system architecture/diagrams."),
("Inline Comments", "Focus on adding explanatory inline comments and documentation comments inside source files."),
],
_ => vec![
("Specialist 1", "Focus on general tasks and responsibilities of this division."),
("Specialist 2", "Focus on code review and validation."),
("Specialist 3", "Focus on error handling and reporting."),
("Specialist 4", "Focus on documentation and testing."),
],
};
specializations
.into_iter()
specs
.iter()
.map(|(label, focus)| {
// Prepend [Division Name: Specialist Label] so the first 40 chars
// of the prompt become the agent_name in spawn_single_agent.
// We use quadruple curly braces `{{{{findings}}}}` so that Rust's `format!` formats it
// into `{{findings}}` in the output string, which `resolve_template` then recognizes
// and replaces.
let prompt = format!(
"[{}: {}]\n\n{}\n\n{}\n\nUser request: {}\n\nFindings from previous divisions: {{findings}}",
"[{}: {}]\n\n{}\n\n{}\n\nUser request: {}\n\nFindings from previous divisions:\n{{{{findings}}}}",
div.name,
label,
focus,
@@ -99,11 +68,16 @@ fn make_division_specialists(
}
/// Construct a named Phase wrapper containing a Parallel block of division specialists.
///
/// Flow: construct division specialists → wrap in a `Parallel` primitive wrapper.
///
/// Return: a `ScriptPrimitive::Phase` wrapper.
fn make_division_phase(
div: &division::Division,
user_request: &str,
specs: &[(String, String)],
) -> ScriptPrimitive {
let specialists = make_division_specialists(div, user_request);
let specialists = make_division_specialists(div, user_request, specs);
ScriptPrimitive::Phase {
name: div.name.to_string(),
script: Box::new(ScriptPrimitive::Parallel(specialists)),
@@ -113,10 +87,7 @@ fn make_division_phase(
/// Run the full company-style pipeline for a given user request.
///
/// This orchestrates all five divisions, running Strategy and Engineering
/// sequentially, followed by Quality, Security, and Documentation in parallel:
/// 1. **Strategy** — create plan with diagrams (4 parallel subagents)
/// 2. **Engineering** — implement code per the plan (4 parallel subagents)
/// 3. **Quality** || **Security** || **Documentation** (in parallel, up to 10 concurrent subagents total)
/// sequentially, followed by Quality, Security, and Documentation in parallel.
///
/// Each division receives findings from all previous divisions, enabling
/// context to flow through the pipeline.
@@ -128,15 +99,27 @@ pub fn run_company_pipeline(
workspaces: &[std::path::PathBuf],
turn_events: Option<&Arc<Mutex<std::collections::VecDeque<crate::app::state::runtime::TurnEvent>>>>,
abort_flag: &Option<Arc<AtomicBool>>,
custom_specialists: HashMap<String, Vec<(String, String)>>,
) -> anyhow::Result<String> {
let divisions = division::all_divisions();
let strategy_phase = make_division_phase(&divisions[0], user_request);
let engineering_phase = make_division_phase(&divisions[1], user_request);
let strategy_specs = custom_specialists.get("Strategy")
.ok_or_else(|| anyhow::anyhow!("missing required division configuration: Strategy"))?;
let engineering_specs = custom_specialists.get("Engineering")
.ok_or_else(|| anyhow::anyhow!("missing required division configuration: Engineering"))?;
let quality_specs = custom_specialists.get("Quality")
.ok_or_else(|| anyhow::anyhow!("missing required division configuration: Quality"))?;
let security_specs = custom_specialists.get("Security")
.ok_or_else(|| anyhow::anyhow!("missing required division configuration: Security"))?;
let documentation_specs = custom_specialists.get("Documentation")
.ok_or_else(|| anyhow::anyhow!("missing required division configuration: Documentation"))?;
let quality_phase = make_division_phase(&divisions[2], user_request);
let security_phase = make_division_phase(&divisions[3], user_request);
let documentation_phase = make_division_phase(&divisions[4], user_request);
let strategy_phase = make_division_phase(&divisions[0], user_request, strategy_specs);
let engineering_phase = make_division_phase(&divisions[1], user_request, engineering_specs);
let quality_phase = make_division_phase(&divisions[2], user_request, quality_specs);
let security_phase = make_division_phase(&divisions[3], user_request, security_specs);
let documentation_phase = make_division_phase(&divisions[4], user_request, documentation_specs);
let parallel_divisions = ScriptPrimitive::Parallel(vec![
quality_phase,
@@ -155,14 +138,12 @@ pub fn run_company_pipeline(
description: "Company Pipeline: Strategy → Engineering → (Quality || Security || Documentation)".to_string(),
script: pipeline_primitive,
options: ScriptOptions {
max_concurrency: 10, // Max concurrent agents in execution
continue_on_error: true, // one division failing shouldn't block the rest
max_concurrency: 10,
continue_on_error: true,
timeout_ms: None,
},
};
// Build a live callback for TUI updates if turn_events is available.
// Uses agent_name (division + specialist name) for the display label in the panel.
let live: Option<LiveStateFn> = turn_events.map(|events| {
let events = events.clone();
let f: LiveStateFn = Arc::new(move |_agent_id: String, agent_name: String, status: AgentStatus| {
@@ -179,9 +160,6 @@ pub fn run_company_pipeline(
});
let args: HashMap<String, String> = HashMap::new();
let live_ref = live.as_ref();
// Create a per-pipeline findings scope so divisions can pass data
let findings: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let results = execute_primitive(
@@ -190,19 +168,18 @@ pub fn run_company_pipeline(
wf.options.max_concurrency,
true,
abort_flag,
live_ref,
live.as_ref(),
session_dir,
workspaces,
&findings,
None,
)?;
// Collect all findings for the executive summary
let all_findings = findings.lock()
.map(|f| f.clone())
.unwrap_or_default();
Ok(build_executive_summary(user_request, &results, &all_findings, &divisions))
Ok(build_executive_summary(user_request, &results, &all_findings, &divisions, &custom_specialists))
}
/// Run a quick company pipeline that skips non-essential divisions
@@ -215,14 +192,21 @@ pub fn run_company_pipeline_quick(
workspaces: &[std::path::PathBuf],
turn_events: Option<&Arc<Mutex<std::collections::VecDeque<crate::app::state::runtime::TurnEvent>>>>,
abort_flag: &Option<Arc<AtomicBool>>,
custom_specialists: HashMap<String, Vec<(String, String)>>,
) -> anyhow::Result<String> {
let divisions = division::all_divisions();
// Only use first 3 divisions for quick pipeline: Strategy, Engineering, Quality
let quick_divisions = &divisions[..3];
let strategy_phase = make_division_phase(&quick_divisions[0], user_request);
let engineering_phase = make_division_phase(&quick_divisions[1], user_request);
let quality_phase = make_division_phase(&quick_divisions[2], user_request);
let strategy_specs = custom_specialists.get("Strategy")
.ok_or_else(|| anyhow::anyhow!("missing required division configuration: Strategy"))?;
let engineering_specs = custom_specialists.get("Engineering")
.ok_or_else(|| anyhow::anyhow!("missing required division configuration: Engineering"))?;
let quality_specs = custom_specialists.get("Quality")
.ok_or_else(|| anyhow::anyhow!("missing required division configuration: Quality"))?;
let strategy_phase = make_division_phase(&quick_divisions[0], user_request, strategy_specs);
let engineering_phase = make_division_phase(&quick_divisions[1], user_request, engineering_specs);
let quality_phase = make_division_phase(&quick_divisions[2], user_request, quality_specs);
let pipeline_primitive = ScriptPrimitive::Pipeline(vec![
strategy_phase,
@@ -268,27 +252,37 @@ pub fn run_company_pipeline_quick(
.map(|f| f.clone())
.unwrap_or_default();
Ok(build_executive_summary(user_request, &results, &all_findings, quick_divisions))
Ok(build_executive_summary(user_request, &results, &all_findings, quick_divisions, &custom_specialists))
}
/// 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.
/// Flow: print user request header → for each division, fetch its specialist verdicts
/// → join with pipes → append findings count.
///
/// Why: keeps output brief to save context window space. Full results are accessible
/// to the CEO via findings.
///
/// Return: a formatted executive summary string.
fn build_executive_summary(
request: &str,
results: &[String],
findings: &[String],
divisions: &[division::Division],
custom_specialists: &HashMap<String, Vec<(String, String)>>,
) -> String {
let mut summary = String::new();
writeln!(summary, "Pipeline for: {request}").unwrap();
for (i, div) in divisions.iter().enumerate() {
let mut start_index = 0;
for div in divisions {
let count = custom_specialists.get(div.name)
.map(Vec::len)
.unwrap_or(0);
let mut division_verdicts = Vec::new();
for offset in 0..4 {
if let Some(r) = results.get(4 * i + offset) {
for offset in 0..count {
if let Some(r) = results.get(start_index + offset) {
let first_line = r.lines().next().unwrap_or(r);
let trimmed = first_line.chars().take(40).collect::<String>();
division_verdicts.push(trimmed);
@@ -302,6 +296,7 @@ fn build_executive_summary(
};
writeln!(summary, " {}: {}", div.name, verdict).unwrap();
start_index += count;
}
if !findings.is_empty() {
@@ -325,6 +320,7 @@ fn build_executive_summary(
/// - 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.
#[allow(dead_code)]
pub fn is_complex_request(request: &str) -> bool {
let trimmed = request.trim();
// Very short requests are never complex
@@ -385,4 +381,25 @@ mod tests {
assert!(is_complex_request("implement user authentication endpoint"));
assert!(is_complex_request("refactor the whole engine module"));
}
#[test]
fn test_make_division_specialists_custom() {
let divisions = division::all_divisions();
let div = &divisions[0];
let mut custom = HashMap::new();
custom.insert(
"Strategy".to_string(),
vec![
("Custom Label".to_string(), "Custom Focus Description".to_string())
]
);
let specs = make_division_specialists(div, "Test Request", &custom.get("Strategy").unwrap());
assert_eq!(specs.len(), 1);
if let ScriptPrimitive::Agent(prompt) = &specs[0] {
assert!(prompt.contains("Custom Label"));
assert!(prompt.contains("Custom Focus Description"));
} else {
panic!("Expected ScriptPrimitive::Agent");
}
}
}
+44 -2
View File
@@ -134,7 +134,35 @@ fn spawn_single_agent(
);
}
let def = AgentDefinition::new(agent_name.to_string(), "coder".to_string());
let mut role = "coder".to_string();
let mut allowed_tools = None;
if agent_name.contains("Strategy") {
let div_def = crate::app::subagent::division::strategy_division();
role = div_def.role;
allowed_tools = div_def.allowed_tools;
} else if agent_name.contains("Engineering") {
let div_def = crate::app::subagent::division::engineering_division();
role = div_def.role;
allowed_tools = div_def.allowed_tools;
} else if agent_name.contains("Quality") {
let div_def = crate::app::subagent::division::quality_division();
role = div_def.role;
allowed_tools = div_def.allowed_tools;
} else if agent_name.contains("Security") {
let div_def = crate::app::subagent::division::security_division();
role = div_def.role;
allowed_tools = div_def.allowed_tools;
} else if agent_name.contains("Documentation") {
let div_def = crate::app::subagent::division::documentation_division();
role = div_def.role;
allowed_tools = div_def.allowed_tools;
}
let mut def = AgentDefinition::new(agent_name.to_string(), role);
if let Some(tools) = allowed_tools {
def = def.with_allowed_tools(tools);
}
let mut ctx = build_subagent_context(&def);
ctx.session_dir = session_dir.to_path_buf();
ctx.workspaces = workspaces.to_vec();
@@ -344,8 +372,22 @@ pub fn execute_primitive(
) -> anyhow::Result<Vec<String>> {
match primitive {
ScriptPrimitive::Agent(prompt) => {
let resolved = resolve_template(prompt, args);
let mut resolved_args = args.clone();
let findings_snapshot = findings.lock().map(|f| f.clone()).unwrap_or_default();
if !resolved_args.contains_key("findings") {
let formatted_findings = if findings_snapshot.is_empty() {
"None".to_string()
} else {
findings_snapshot
.iter()
.enumerate()
.map(|(i, f)| format!("{}. {}", i + 1, f))
.collect::<Vec<_>>()
.join("\n")
};
resolved_args.insert("findings".to_string(), formatted_findings);
}
let resolved = resolve_template(prompt, &resolved_args);
let agent_id = uuid::Uuid::new_v4().to_string();
let agent_name = resolved.chars().take(40).collect::<String>();
match spawn_single_agent(&agent_id, &agent_name, &resolved, &findings_snapshot, findings, abort_flag, live, session_dir, workspaces, timeout_ms) {
-13
View File
@@ -22,10 +22,6 @@ pub enum Command {
WorkflowRun {
script: String,
},
/// /pipeline full|quick|skip
Pipeline {
mode: String,
},
Unknown(String),
}
@@ -79,15 +75,6 @@ pub fn parse_command(text: &str) -> Command {
"/workflow" => Command::WorkflowRun {
script: arg1.to_string(),
},
"/pipeline" if arg1.is_empty() => Command::Pipeline {
mode: "status".to_string(),
},
"/pipeline" if arg1 == "full" || arg1 == "quick" || arg1 == "skip" => {
Command::Pipeline {
mode: arg1.to_string(),
}
}
"/pipeline" => Command::Unknown(format!("/pipeline {arg1} (use: full|quick|skip)")),
_ => Command::Unknown(cmd.to_string()),
}
}
+33 -1
View File
@@ -171,9 +171,22 @@ impl Tool for CompanyPipeline {
"enum": ["full", "quick"],
"description": "Pipeline mode: 'full' (5 divisions) for complex tasks, 'quick' (3 divisions: Strategy→Engineering→Quality) for simpler tasks",
"default": "full"
},
"specialists": {
"type": "object",
"description": "Mapping from division name (Strategy, Engineering, Quality, Security, Documentation) to list of custom specialists. Each specialist is defined by a pair of [label, focus_description]. This parameter is mandatory. The CEO/main agent must fully define the specialized roles and focuses for every division in the pipeline to run.",
"additionalProperties": {
"type": "array",
"items": {
"type": "array",
"items": { "type": "string" },
"minItems": 2,
"maxItems": 2
}
}
}
},
"required": ["request"]
"required": ["request", "specialists"]
})
}
@@ -186,6 +199,23 @@ impl Tool for CompanyPipeline {
.and_then(|v| v.as_str())
.unwrap_or("full");
let custom_specialists: std::collections::HashMap<String, Vec<(String, String)>> = args.get("specialists")
.and_then(|v| v.as_object())
.map(|obj| {
obj.iter().map(|(k, v)| {
let specs = v.as_array().map(|arr| {
arr.iter().filter_map(|item| {
let pair = item.as_array()?;
let label = pair.get(0)?.as_str()?.to_string();
let focus = pair.get(1)?.as_str()?.to_string();
Some((label, focus))
}).collect()
}).unwrap_or_default();
(k.clone(), specs)
}).collect()
})
.ok_or_else(|| anyhow!("missing required argument: specialists"))?;
let no_abort: Option<std::sync::Arc<std::sync::atomic::AtomicBool>> = None;
match mode {
"quick" => {
@@ -195,6 +225,7 @@ impl Tool for CompanyPipeline {
&ctx.workspaces,
ctx.turn_events.as_ref(),
&no_abort,
custom_specialists,
)
}
_ => {
@@ -204,6 +235,7 @@ impl Tool for CompanyPipeline {
&ctx.workspaces,
ctx.turn_events.as_ref(),
&no_abort,
custom_specialists,
)
}
}