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 {
|
RunWorkflow {
|
||||||
script: String,
|
script: String,
|
||||||
},
|
},
|
||||||
/// User-initiated pipeline via `/pipeline full|quick|skip`.
|
|
||||||
RunPipeline {
|
|
||||||
mode: String,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply an `Action` to the application state.
|
/// Apply an `Action` to the application state.
|
||||||
@@ -529,9 +525,6 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if turn_finished {
|
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);
|
maybe_trigger_review(state);
|
||||||
}
|
}
|
||||||
if turn_finished || state.dirty {
|
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.push_toast(Toast::new(ToastKind::Info, format!("deleted lesson: {name}")));
|
||||||
state.dirty = true;
|
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 } => {
|
Action::RunWorkflow { script } => {
|
||||||
// Open the Workflow overlay so the user can see progress.
|
// Open the Workflow overlay so the user can see progress.
|
||||||
state.misc.overlay = Overlay::Workflow;
|
state.misc.overlay = Overlay::Workflow;
|
||||||
@@ -775,7 +745,6 @@ fn spawn_turn(state: &AppStateRest) {
|
|||||||
}) = true;
|
}) = true;
|
||||||
|
|
||||||
let events_q = turn_events.clone();
|
let events_q = turn_events.clone();
|
||||||
let pipeline_mode = state.misc.pipeline_override.clone();
|
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let db = crate::model::msglog::open_or_create(&edit_session_dir)
|
let db = crate::model::msglog::open_or_create(&edit_session_dir)
|
||||||
@@ -795,7 +764,6 @@ fn spawn_turn(state: &AppStateRest) {
|
|||||||
temperature,
|
temperature,
|
||||||
max_tokens,
|
max_tokens,
|
||||||
abort_flag,
|
abort_flag,
|
||||||
pipeline_mode,
|
|
||||||
};
|
};
|
||||||
let result = run_agent_turn(&tc, &messages, &events_q);
|
let result = run_agent_turn(&tc, &messages, &events_q);
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
@@ -824,9 +792,6 @@ struct TurnCtx {
|
|||||||
temperature: f32,
|
temperature: f32,
|
||||||
max_tokens: Option<u32>,
|
max_tokens: Option<u32>,
|
||||||
abort_flag: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
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
|
/// Build an ASCII tree of the workspace directory structure for the
|
||||||
@@ -1017,11 +982,6 @@ fn run_agent_turn(
|
|||||||
|
|
||||||
// ── AUTO CEO PIPELINE ──
|
// ── AUTO CEO PIPELINE ──
|
||||||
// Before the main agent starts working, check if the pipeline should run.
|
// 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()
|
let user_msg_count = msgs.iter()
|
||||||
.filter(|m| matches!(m.role, crate::dto::chat::message::Role::User))
|
.filter(|m| matches!(m.role, crate::dto::chat::message::Role::User))
|
||||||
.count();
|
.count();
|
||||||
@@ -1034,14 +994,7 @@ fn run_agent_turn(
|
|||||||
if user_request.is_empty() {
|
if user_request.is_empty() {
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
match tc.pipeline_mode.as_deref() {
|
crate::app::workflow::company::is_complex_request(user_request)
|
||||||
Some("skip") => {
|
|
||||||
tracing::debug!("[ceo] pipeline skipped via /pipeline skip");
|
|
||||||
false
|
|
||||||
}
|
|
||||||
Some("full" | "quick") => true,
|
|
||||||
_ => crate::app::workflow::company::is_complex_request(user_request),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
@@ -1053,10 +1006,10 @@ fn run_agent_turn(
|
|||||||
.and_then(|m| m.content.as_deref())
|
.and_then(|m| m.content.as_deref())
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
|
|
||||||
let use_full = tc.pipeline_mode.as_deref() != Some("quick");
|
let use_full = true;
|
||||||
let mode_label = if use_full { "full" } else { "quick" };
|
let mode_label = "full";
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"[ceo] pipeline triggered (mode={}) — delegating to company pipeline",
|
"[ceo] pipeline triggered (mode={}) — dynamically generating planning workflow via LLM",
|
||||||
mode_label
|
mode_label
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1064,31 +1017,109 @@ fn run_agent_turn(
|
|||||||
q.push_back(TurnEvent::SystemNote {
|
q.push_back(TurnEvent::SystemNote {
|
||||||
kind: "pipeline".to_string(),
|
kind: "pipeline".to_string(),
|
||||||
message: format!(
|
message: format!(
|
||||||
"Company pipeline started ({}): {} → Engineering → Quality{}",
|
"CEO is planning workflow (mode={})...",
|
||||||
mode_label,
|
mode_label,
|
||||||
"Strategy",
|
|
||||||
if use_full { " → Security → Documentation" } else { "" },
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let pipeline_abort = Some(tc.abort_flag.clone());
|
let pipeline_abort = Some(tc.abort_flag.clone());
|
||||||
let pipeline_result = if use_full {
|
|
||||||
crate::app::workflow::company::run_company_pipeline(
|
// Ask LLM to dynamically generate the workflow specialists plan
|
||||||
user_request,
|
let required_divisions = if use_full {
|
||||||
&tc.edit_log_session_dir,
|
"all 5 divisions (Strategy, Engineering, Quality, Security, Documentation)"
|
||||||
&tc.workspace_roots,
|
|
||||||
Some(events_q),
|
|
||||||
&pipeline_abort,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
crate::app::workflow::company::run_company_pipeline_quick(
|
"the 3 quick divisions (Strategy, Engineering, Quality)"
|
||||||
user_request,
|
};
|
||||||
&tc.edit_log_session_dir,
|
let example_json = if use_full {
|
||||||
&tc.workspace_roots,
|
r#"{
|
||||||
Some(events_q),
|
"Strategy": [ ["Architect", "Analyze component tree..."] ],
|
||||||
&pipeline_abort,
|
"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 {
|
match pipeline_result {
|
||||||
|
|||||||
@@ -69,9 +69,6 @@ pub fn apply_command(command: Command) -> Vec<Action> {
|
|||||||
Command::WorkflowRun { script } => {
|
Command::WorkflowRun { script } => {
|
||||||
vec![Action::RunWorkflow { script }]
|
vec![Action::RunWorkflow { script }]
|
||||||
}
|
}
|
||||||
Command::Pipeline { mode } => {
|
|
||||||
vec![Action::RunPipeline { mode }]
|
|
||||||
}
|
|
||||||
Command::Unknown(cmd) => {
|
Command::Unknown(cmd) => {
|
||||||
vec![Action::SystemNote {
|
vec![Action::SystemNote {
|
||||||
kind: "error".to_string(),
|
kind: "error".to_string(),
|
||||||
|
|||||||
@@ -90,10 +90,6 @@ const COMMANDS: &[&str] = &[
|
|||||||
"/model add",
|
"/model add",
|
||||||
"/workflow",
|
"/workflow",
|
||||||
"/workflow run",
|
"/workflow run",
|
||||||
"/pipeline",
|
|
||||||
"/pipeline full",
|
|
||||||
"/pipeline quick",
|
|
||||||
"/pipeline skip",
|
|
||||||
"/compact",
|
"/compact",
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -293,14 +289,6 @@ pub struct MiscState {
|
|||||||
pub api_context_length: Option<u32>,
|
pub api_context_length: Option<u32>,
|
||||||
pub tick_count: u64,
|
pub tick_count: u64,
|
||||||
pub todo_content: String,
|
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 {
|
impl MiscState {
|
||||||
@@ -319,7 +307,6 @@ impl MiscState {
|
|||||||
api_context_length: None,
|
api_context_length: None,
|
||||||
tick_count: 0,
|
tick_count: 0,
|
||||||
todo_content: String::new(),
|
todo_content: String::new(),
|
||||||
pipeline_override: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,16 @@ use super::event::SubagentEvent;
|
|||||||
fn build_subagent_tools(allowed_tools: &[String]) -> (Vec<Box<dyn crate::tool::Tool>>, Vec<ToolDef>) {
|
fn build_subagent_tools(allowed_tools: &[String]) -> (Vec<Box<dyn crate::tool::Tool>>, Vec<ToolDef>) {
|
||||||
let all = all_tools();
|
let all = all_tools();
|
||||||
let filtered: Vec<Box<dyn crate::tool::Tool>> = if allowed_tools.is_empty() {
|
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 {
|
} else {
|
||||||
all.into_iter()
|
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()
|
.collect()
|
||||||
};
|
};
|
||||||
let defs = tool_defs(&filtered);
|
let defs = tool_defs(&filtered);
|
||||||
|
|||||||
+91
-74
@@ -34,59 +34,28 @@ use crate::app::workflow::script::{ScriptPrimitive, ScriptOptions, WorkflowScrip
|
|||||||
use crate::app::workflow::engine::{execute_primitive, LiveStateFn, AgentStatus};
|
use crate::app::workflow::engine::{execute_primitive, LiveStateFn, AgentStatus};
|
||||||
use crate::app::subagent::division;
|
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(
|
fn make_division_specialists(
|
||||||
div: &division::Division,
|
div: &division::Division,
|
||||||
user_request: &str,
|
user_request: &str,
|
||||||
|
specs: &[(String, String)],
|
||||||
) -> Vec<ScriptPrimitive> {
|
) -> Vec<ScriptPrimitive> {
|
||||||
let div_prompt = div.agent_def.system_prompt.as_deref().unwrap_or("");
|
let div_prompt = div.agent_def.system_prompt.as_deref().unwrap_or("");
|
||||||
|
|
||||||
let specializations = match div.name {
|
specs
|
||||||
"Strategy" => vec![
|
.iter()
|
||||||
("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()
|
|
||||||
.map(|(label, focus)| {
|
.map(|(label, focus)| {
|
||||||
// Prepend [Division Name: Specialist Label] so the first 40 chars
|
// Prepend [Division Name: Specialist Label] so the first 40 chars
|
||||||
// of the prompt become the agent_name in spawn_single_agent.
|
// 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!(
|
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,
|
div.name,
|
||||||
label,
|
label,
|
||||||
focus,
|
focus,
|
||||||
@@ -99,11 +68,16 @@ fn make_division_specialists(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Construct a named Phase wrapper containing a Parallel block of 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(
|
fn make_division_phase(
|
||||||
div: &division::Division,
|
div: &division::Division,
|
||||||
user_request: &str,
|
user_request: &str,
|
||||||
|
specs: &[(String, String)],
|
||||||
) -> ScriptPrimitive {
|
) -> ScriptPrimitive {
|
||||||
let specialists = make_division_specialists(div, user_request);
|
let specialists = make_division_specialists(div, user_request, specs);
|
||||||
ScriptPrimitive::Phase {
|
ScriptPrimitive::Phase {
|
||||||
name: div.name.to_string(),
|
name: div.name.to_string(),
|
||||||
script: Box::new(ScriptPrimitive::Parallel(specialists)),
|
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.
|
/// Run the full company-style pipeline for a given user request.
|
||||||
///
|
///
|
||||||
/// This orchestrates all five divisions, running Strategy and Engineering
|
/// This orchestrates all five divisions, running Strategy and Engineering
|
||||||
/// sequentially, followed by Quality, Security, and Documentation in parallel:
|
/// 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)
|
|
||||||
///
|
///
|
||||||
/// Each division receives findings from all previous divisions, enabling
|
/// Each division receives findings from all previous divisions, enabling
|
||||||
/// context to flow through the pipeline.
|
/// context to flow through the pipeline.
|
||||||
@@ -128,15 +99,27 @@ pub fn run_company_pipeline(
|
|||||||
workspaces: &[std::path::PathBuf],
|
workspaces: &[std::path::PathBuf],
|
||||||
turn_events: Option<&Arc<Mutex<std::collections::VecDeque<crate::app::state::runtime::TurnEvent>>>>,
|
turn_events: Option<&Arc<Mutex<std::collections::VecDeque<crate::app::state::runtime::TurnEvent>>>>,
|
||||||
abort_flag: &Option<Arc<AtomicBool>>,
|
abort_flag: &Option<Arc<AtomicBool>>,
|
||||||
|
custom_specialists: HashMap<String, Vec<(String, String)>>,
|
||||||
) -> anyhow::Result<String> {
|
) -> anyhow::Result<String> {
|
||||||
let divisions = division::all_divisions();
|
let divisions = division::all_divisions();
|
||||||
|
|
||||||
let strategy_phase = make_division_phase(&divisions[0], user_request);
|
let strategy_specs = custom_specialists.get("Strategy")
|
||||||
let engineering_phase = make_division_phase(&divisions[1], user_request);
|
.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 strategy_phase = make_division_phase(&divisions[0], user_request, strategy_specs);
|
||||||
let security_phase = make_division_phase(&divisions[3], user_request);
|
let engineering_phase = make_division_phase(&divisions[1], user_request, engineering_specs);
|
||||||
let documentation_phase = make_division_phase(&divisions[4], user_request);
|
|
||||||
|
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![
|
let parallel_divisions = ScriptPrimitive::Parallel(vec![
|
||||||
quality_phase,
|
quality_phase,
|
||||||
@@ -155,14 +138,12 @@ pub fn run_company_pipeline(
|
|||||||
description: "Company Pipeline: Strategy → Engineering → (Quality || Security || Documentation)".to_string(),
|
description: "Company Pipeline: Strategy → Engineering → (Quality || Security || Documentation)".to_string(),
|
||||||
script: pipeline_primitive,
|
script: pipeline_primitive,
|
||||||
options: ScriptOptions {
|
options: ScriptOptions {
|
||||||
max_concurrency: 10, // Max concurrent agents in execution
|
max_concurrency: 10,
|
||||||
continue_on_error: true, // one division failing shouldn't block the rest
|
continue_on_error: true,
|
||||||
timeout_ms: None,
|
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 live: Option<LiveStateFn> = turn_events.map(|events| {
|
||||||
let events = events.clone();
|
let events = events.clone();
|
||||||
let f: LiveStateFn = Arc::new(move |_agent_id: String, agent_name: String, status: AgentStatus| {
|
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 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 findings: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
|
||||||
let results = execute_primitive(
|
let results = execute_primitive(
|
||||||
@@ -190,19 +168,18 @@ pub fn run_company_pipeline(
|
|||||||
wf.options.max_concurrency,
|
wf.options.max_concurrency,
|
||||||
true,
|
true,
|
||||||
abort_flag,
|
abort_flag,
|
||||||
live_ref,
|
live.as_ref(),
|
||||||
session_dir,
|
session_dir,
|
||||||
workspaces,
|
workspaces,
|
||||||
&findings,
|
&findings,
|
||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Collect all findings for the executive summary
|
|
||||||
let all_findings = findings.lock()
|
let all_findings = findings.lock()
|
||||||
.map(|f| f.clone())
|
.map(|f| f.clone())
|
||||||
.unwrap_or_default();
|
.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
|
/// Run a quick company pipeline that skips non-essential divisions
|
||||||
@@ -215,14 +192,21 @@ pub fn run_company_pipeline_quick(
|
|||||||
workspaces: &[std::path::PathBuf],
|
workspaces: &[std::path::PathBuf],
|
||||||
turn_events: Option<&Arc<Mutex<std::collections::VecDeque<crate::app::state::runtime::TurnEvent>>>>,
|
turn_events: Option<&Arc<Mutex<std::collections::VecDeque<crate::app::state::runtime::TurnEvent>>>>,
|
||||||
abort_flag: &Option<Arc<AtomicBool>>,
|
abort_flag: &Option<Arc<AtomicBool>>,
|
||||||
|
custom_specialists: HashMap<String, Vec<(String, String)>>,
|
||||||
) -> anyhow::Result<String> {
|
) -> anyhow::Result<String> {
|
||||||
let divisions = division::all_divisions();
|
let divisions = division::all_divisions();
|
||||||
// Only use first 3 divisions for quick pipeline: Strategy, Engineering, Quality
|
|
||||||
let quick_divisions = &divisions[..3];
|
let quick_divisions = &divisions[..3];
|
||||||
|
|
||||||
let strategy_phase = make_division_phase(&quick_divisions[0], user_request);
|
let strategy_specs = custom_specialists.get("Strategy")
|
||||||
let engineering_phase = make_division_phase(&quick_divisions[1], user_request);
|
.ok_or_else(|| anyhow::anyhow!("missing required division configuration: Strategy"))?;
|
||||||
let quality_phase = make_division_phase(&quick_divisions[2], user_request);
|
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![
|
let pipeline_primitive = ScriptPrimitive::Pipeline(vec![
|
||||||
strategy_phase,
|
strategy_phase,
|
||||||
@@ -268,27 +252,37 @@ pub fn run_company_pipeline_quick(
|
|||||||
.map(|f| f.clone())
|
.map(|f| f.clone())
|
||||||
.unwrap_or_default();
|
.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.
|
/// Build a compressed executive summary from pipeline results.
|
||||||
///
|
///
|
||||||
/// Keeps output brief to save context window space — just division verdicts
|
/// Flow: print user request header → for each division, fetch its specialist verdicts
|
||||||
/// and key findings, not full outputs. Full results are accessible to the
|
/// → join with pipes → append findings count.
|
||||||
/// CEO via the notes/findings that were archived during execution.
|
///
|
||||||
|
/// 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(
|
fn build_executive_summary(
|
||||||
request: &str,
|
request: &str,
|
||||||
results: &[String],
|
results: &[String],
|
||||||
findings: &[String],
|
findings: &[String],
|
||||||
divisions: &[division::Division],
|
divisions: &[division::Division],
|
||||||
|
custom_specialists: &HashMap<String, Vec<(String, String)>>,
|
||||||
) -> String {
|
) -> String {
|
||||||
let mut summary = String::new();
|
let mut summary = String::new();
|
||||||
writeln!(summary, "Pipeline for: {request}").unwrap();
|
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();
|
let mut division_verdicts = Vec::new();
|
||||||
for offset in 0..4 {
|
for offset in 0..count {
|
||||||
if let Some(r) = results.get(4 * i + offset) {
|
if let Some(r) = results.get(start_index + offset) {
|
||||||
let first_line = r.lines().next().unwrap_or(r);
|
let first_line = r.lines().next().unwrap_or(r);
|
||||||
let trimmed = first_line.chars().take(40).collect::<String>();
|
let trimmed = first_line.chars().take(40).collect::<String>();
|
||||||
division_verdicts.push(trimmed);
|
division_verdicts.push(trimmed);
|
||||||
@@ -302,6 +296,7 @@ fn build_executive_summary(
|
|||||||
};
|
};
|
||||||
|
|
||||||
writeln!(summary, " {}: {}", div.name, verdict).unwrap();
|
writeln!(summary, " {}: {}", div.name, verdict).unwrap();
|
||||||
|
start_index += count;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !findings.is_empty() {
|
if !findings.is_empty() {
|
||||||
@@ -325,6 +320,7 @@ fn build_executive_summary(
|
|||||||
/// - Negative keywords (simple/trivial/typo/quick) skip the pipeline.
|
/// - Negative keywords (simple/trivial/typo/quick) skip the pipeline.
|
||||||
/// - Positive keywords (refactor/api/implement/architecture) trigger it.
|
/// - Positive keywords (refactor/api/implement/architecture) trigger it.
|
||||||
/// - Multi-line or multi-sentence requests are more likely complex.
|
/// - Multi-line or multi-sentence requests are more likely complex.
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn is_complex_request(request: &str) -> bool {
|
pub fn is_complex_request(request: &str) -> bool {
|
||||||
let trimmed = request.trim();
|
let trimmed = request.trim();
|
||||||
// Very short requests are never complex
|
// Very short requests are never complex
|
||||||
@@ -385,4 +381,25 @@ mod tests {
|
|||||||
assert!(is_complex_request("implement user authentication endpoint"));
|
assert!(is_complex_request("implement user authentication endpoint"));
|
||||||
assert!(is_complex_request("refactor the whole engine module"));
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
let mut ctx = build_subagent_context(&def);
|
||||||
ctx.session_dir = session_dir.to_path_buf();
|
ctx.session_dir = session_dir.to_path_buf();
|
||||||
ctx.workspaces = workspaces.to_vec();
|
ctx.workspaces = workspaces.to_vec();
|
||||||
@@ -344,8 +372,22 @@ pub fn execute_primitive(
|
|||||||
) -> anyhow::Result<Vec<String>> {
|
) -> anyhow::Result<Vec<String>> {
|
||||||
match primitive {
|
match primitive {
|
||||||
ScriptPrimitive::Agent(prompt) => {
|
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();
|
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_id = uuid::Uuid::new_v4().to_string();
|
||||||
let agent_name = resolved.chars().take(40).collect::<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) {
|
match spawn_single_agent(&agent_id, &agent_name, &resolved, &findings_snapshot, findings, abort_flag, live, session_dir, workspaces, timeout_ms) {
|
||||||
|
|||||||
@@ -22,10 +22,6 @@ pub enum Command {
|
|||||||
WorkflowRun {
|
WorkflowRun {
|
||||||
script: String,
|
script: String,
|
||||||
},
|
},
|
||||||
/// /pipeline full|quick|skip
|
|
||||||
Pipeline {
|
|
||||||
mode: String,
|
|
||||||
},
|
|
||||||
Unknown(String),
|
Unknown(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,15 +75,6 @@ pub fn parse_command(text: &str) -> Command {
|
|||||||
"/workflow" => Command::WorkflowRun {
|
"/workflow" => Command::WorkflowRun {
|
||||||
script: arg1.to_string(),
|
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()),
|
_ => Command::Unknown(cmd.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-1
@@ -171,9 +171,22 @@ impl Tool for CompanyPipeline {
|
|||||||
"enum": ["full", "quick"],
|
"enum": ["full", "quick"],
|
||||||
"description": "Pipeline mode: 'full' (5 divisions) for complex tasks, 'quick' (3 divisions: Strategy→Engineering→Quality) for simpler tasks",
|
"description": "Pipeline mode: 'full' (5 divisions) for complex tasks, 'quick' (3 divisions: Strategy→Engineering→Quality) for simpler tasks",
|
||||||
"default": "full"
|
"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())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("full");
|
.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;
|
let no_abort: Option<std::sync::Arc<std::sync::atomic::AtomicBool>> = None;
|
||||||
match mode {
|
match mode {
|
||||||
"quick" => {
|
"quick" => {
|
||||||
@@ -195,6 +225,7 @@ impl Tool for CompanyPipeline {
|
|||||||
&ctx.workspaces,
|
&ctx.workspaces,
|
||||||
ctx.turn_events.as_ref(),
|
ctx.turn_events.as_ref(),
|
||||||
&no_abort,
|
&no_abort,
|
||||||
|
custom_specialists,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@@ -204,6 +235,7 @@ impl Tool for CompanyPipeline {
|
|||||||
&ctx.workspaces,
|
&ctx.workspaces,
|
||||||
ctx.turn_events.as_ref(),
|
ctx.turn_events.as_ref(),
|
||||||
&no_abort,
|
&no_abort,
|
||||||
|
custom_specialists,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user