feat: refactor agent step limits and enhance workflow orchestration with new findings tool

This commit is contained in:
asepharyana
2026-07-13 14:39:39 +07:00
parent 0d6f558b2b
commit 3b660e09a8
9 changed files with 229 additions and 100 deletions
+176 -55
View File
@@ -1,6 +1,6 @@
//! Company-style workflow orchestrator: runs the complete division pipeline
//! (Strategy → Engineering → Quality Security Documentation) with
//! findings flowing between stages, then returns a consolidated executive
//! (Strategy → Engineering → [Quality || Security || Documentation] in parallel)
//! with findings flowing between stages, then returns a consolidated executive
//! summary to the CEO (main agent).
//!
//! Flow:
@@ -9,32 +9,114 @@
//! │ delegates to run_company_pipeline(request)
//! ▼
//! ┌──────────────────────────────────────────────────┐
//! │ Strategy Division — plan + mermaid diagrams │
//! │ Engineering Division — implement per plan │
//! Quality Division — review + write tests
//! │ Security Division — vulnerability audit │
//! │ Documentation Div — update docs │
//! └─────────────────────────────────────────────────┘
//! returns consolidated summary
//!
//! CEO Main Agent delivers to user
//! │ Strategy Division — plan + mermaid diagrams │ (runs sequentially first)
//! └─────────────────────────┬────────────────────────┘
//!
//! ┌──────────────────────────────────────────────────┐
//! │ Engineering Division — implement per plan │ (runs sequentially second)
//! └─────────────────────────────────────────────────┘
//!
//! ┌────────────┼────────────┐
//! ▼ ▼ ▼
//! ┌───────────┐┌───────────┐┌───────────┐
//! │ Quality ││ Security ││ Docs │ (run concurrently in parallel)
//! └───────────┘└───────────┘└───────────┘
//! │ │ │
//! └────────────┼────────────┘
//! ▼
//! CEO Main Agent delivers consolidated summary to user
//! ```
use std::collections::HashMap;
use std::fmt::Write;
use std::sync::{Arc, Mutex, atomic::AtomicBool};
use crate::app::workflow::engine::{execute_primitive, LiveStateFn, AgentStatus};
use crate::app::workflow::script::{ScriptPrimitive, ScriptOptions, WorkflowScript};
use crate::app::workflow::engine::{execute_primitive, LiveStateFn, AgentStatus};
use crate::app::subagent::division;
/// Construct the 4 specialized agents for a division.
fn make_division_specialists(
div: &division::Division,
user_request: &str,
) -> 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()
.map(|(label, focus)| {
// Prepend [Division Name: Specialist Label] so the first 40 chars
// of the prompt become the agent_name in spawn_single_agent.
let prompt = format!(
"[{}: {}]\n\n{}\n\n{}\n\nUser request: {}\n\nFindings from previous divisions: {{findings}}",
div.name,
label,
focus,
div_prompt,
user_request,
);
ScriptPrimitive::Agent(prompt)
})
.collect()
}
/// Construct a named Phase wrapper containing a Parallel block of division specialists.
fn make_division_phase(
div: &division::Division,
user_request: &str,
) -> ScriptPrimitive {
let specialists = make_division_specialists(div, user_request);
ScriptPrimitive::Phase {
name: div.name.to_string(),
script: Box::new(ScriptPrimitive::Parallel(specialists)),
}
}
/// Run the full company-style pipeline for a given user request.
///
/// This orchestrates all five divisions in sequence:
/// 1. **Strategy** — create plan with diagrams
/// 2. **Engineering** — implement code
/// 3. **Quality** — review + write tests
/// 4. **Security** — audit
/// 5. **Documentation** — update docs
/// 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)
///
/// Each division receives findings from all previous divisions, enabling
/// context to flow through the pipeline.
@@ -48,35 +130,39 @@ pub fn run_company_pipeline(
abort_flag: &Option<Arc<AtomicBool>>,
) -> anyhow::Result<String> {
let divisions = division::all_divisions();
let mut pipeline_scripts: Vec<ScriptPrimitive> = Vec::with_capacity(divisions.len());
for div in &divisions {
let div_prompt = div.agent_def.system_prompt.as_deref().unwrap_or("");
// Prepend [Division Name] so the first 40 chars of the prompt
// become the agent_name in spawn_single_agent, making the TUI
// panel show division names instead of UUID fragments.
let prompt = format!(
"[{}]\n\n{}\n\nUser request: {}\n\nFindings from previous divisions: {{findings}}",
div.name,
div_prompt,
user_request,
);
pipeline_scripts.push(ScriptPrimitive::Agent(prompt));
}
let strategy_phase = make_division_phase(&divisions[0], user_request);
let engineering_phase = make_division_phase(&divisions[1], user_request);
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 parallel_divisions = ScriptPrimitive::Parallel(vec![
quality_phase,
security_phase,
documentation_phase,
]);
let pipeline_primitive = ScriptPrimitive::Pipeline(vec![
strategy_phase,
engineering_phase,
parallel_divisions,
]);
let wf = WorkflowScript {
name: "company-pipeline".to_string(),
description: "Company Pipeline (full): Strategy → Engineering → Quality Security Documentation".to_string(),
script: ScriptPrimitive::Pipeline(pipeline_scripts),
description: "Company Pipeline: Strategy → Engineering → (Quality || Security || Documentation)".to_string(),
script: pipeline_primitive,
options: ScriptOptions {
max_concurrency: 1, // sequential by design
max_concurrency: 10, // Max concurrent agents in execution
continue_on_error: true, // one division failing shouldn't block the rest
timeout_ms: None,
},
};
// Build a live callback for TUI updates if turn_events is available.
// Uses agent_name (division name) for the display label in the panel.
// 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| {
@@ -101,7 +187,7 @@ pub fn run_company_pipeline(
let results = execute_primitive(
&wf.script,
&args,
1,
wf.options.max_concurrency,
true,
abort_flag,
live_ref,
@@ -134,24 +220,22 @@ pub fn run_company_pipeline_quick(
// Only use first 3 divisions for quick pipeline: Strategy, Engineering, Quality
let quick_divisions = &divisions[..3];
let mut pipeline_scripts: Vec<ScriptPrimitive> = Vec::with_capacity(quick_divisions.len());
for div in quick_divisions {
let div_prompt = div.agent_def.system_prompt.as_deref().unwrap_or("");
let prompt = format!(
"[{}]\n\n{}\n\nUser request: {}\n\nFindings from previous divisions: {{findings}}",
div.name,
div_prompt,
user_request,
);
pipeline_scripts.push(ScriptPrimitive::Agent(prompt));
}
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 pipeline_primitive = ScriptPrimitive::Pipeline(vec![
strategy_phase,
engineering_phase,
quality_phase,
]);
let wf = WorkflowScript {
name: "company-pipeline-quick".to_string(),
description: "Company Pipeline (quick): Strategy → Engineering → Quality".to_string(),
script: ScriptPrimitive::Pipeline(pipeline_scripts),
script: pipeline_primitive,
options: ScriptOptions {
max_concurrency: 1,
max_concurrency: 10,
continue_on_error: true,
timeout_ms: None,
},
@@ -176,7 +260,7 @@ pub fn run_company_pipeline_quick(
let findings: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let results = execute_primitive(
&wf.script, &args, 1, true,
&wf.script, &args, wf.options.max_concurrency, true,
abort_flag, live.as_ref(), session_dir, workspaces, &findings, None,
)?;
@@ -202,10 +286,20 @@ fn build_executive_summary(
writeln!(summary, "Pipeline for: {request}").unwrap();
for (i, div) in divisions.iter().enumerate() {
let verdict = results.get(i).map_or_else(|| "".to_string(), |r| {
r.lines().next().unwrap_or(r)
.chars().take(100).collect::<String>()
});
let mut division_verdicts = Vec::new();
for offset in 0..4 {
if let Some(r) = results.get(4 * i + offset) {
let first_line = r.lines().next().unwrap_or(r);
let trimmed = first_line.chars().take(40).collect::<String>();
division_verdicts.push(trimmed);
}
}
let verdict = if division_verdicts.is_empty() {
"".to_string()
} else {
division_verdicts.join(" | ")
};
writeln!(summary, " {}: {}", div.name, verdict).unwrap();
}
@@ -265,3 +359,30 @@ pub fn is_complex_request(request: &str) -> bool {
];
complexity_keywords.iter().any(|k| lower.contains(k))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_complex_request_too_short() {
assert!(!is_complex_request("abc"));
}
#[test]
fn test_is_complex_request_simple_keywords() {
assert!(!is_complex_request("just a simple update to the readme"));
assert!(!is_complex_request("minor typo fix in main.rs"));
}
#[test]
fn test_is_complex_request_multi_sentence() {
assert!(is_complex_request("This is sentence one. This is sentence two. This is sentence three."));
}
#[test]
fn test_is_complex_request_complex_keywords() {
assert!(is_complex_request("implement user authentication endpoint"));
assert!(is_complex_request("refactor the whole engine module"));
}
}
+1 -2
View File
@@ -134,8 +134,7 @@ fn spawn_single_agent(
);
}
let def = AgentDefinition::new(agent_name.to_string(), "coder".to_string())
.with_max_steps(50);
let def = AgentDefinition::new(agent_name.to_string(), "coder".to_string());
let mut ctx = build_subagent_context(&def);
ctx.session_dir = session_dir.to_path_buf();
ctx.workspaces = workspaces.to_vec();