feat(hive-mind): implement multi-agent orchestration with cognitive cycles
- Introduced a new hive-mind architecture that allows the Core Intelligence to issue directives to anonymous processing nodes. - Each node executes its directive and merges output into a collective state, visible to all nodes in real-time. - Added support for dynamic cognitive cycles, enabling flexible task management. - Implemented documentation generation for hive-mind runs, ensuring a durable record of decisions and actions. - Refactored existing company pipeline tools to align with the new hive-mind structure, replacing division-specific prompts with a more generalized approach. - Updated workflow rendering to accommodate hive-mind nodes and their system-assigned designations. - Enhanced error handling and validation for cognitive cycle plans.
This commit is contained in:
+46
-77
@@ -143,6 +143,46 @@ pub fn spawn_quick_review(
|
||||
|
||||
/// ─── Background Subagent Spawners (async, report via `SystemNote`) ───
|
||||
///
|
||||
/// Run a subagent built from `def`, retrying once if the first attempt
|
||||
/// fails. Background subagents call this instead of running once and
|
||||
/// silently swallowing the error into a note string, so a single transient
|
||||
/// LLM/tool failure doesn't just disappear.
|
||||
///
|
||||
/// Return: `Ok(output)` if either attempt succeeded, `Err(message)`
|
||||
/// describing the final failure if both attempts failed.
|
||||
fn run_subagent_with_retry(
|
||||
def: &AgentDefinition,
|
||||
session_dir: &Path,
|
||||
workspaces: &[std::path::PathBuf],
|
||||
label: &str,
|
||||
) -> Result<String, String> {
|
||||
let mut last_err = String::new();
|
||||
for attempt in 1..=2 {
|
||||
let mut ctx = build_subagent_context(def);
|
||||
ctx.session_dir = session_dir.to_path_buf();
|
||||
ctx.workspaces = workspaces.to_vec();
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
|
||||
let drain_label = label.to_string();
|
||||
let _drain = std::thread::spawn(move || {
|
||||
while let Some(event) = rx.blocking_recv() {
|
||||
if let SubagentEvent::StepFailed { step, error } = &event {
|
||||
tracing::warn!("[{drain_label}] step {step} failed: {error}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
match run_subagent(&ctx, &tx) {
|
||||
Ok(output) => return Ok(output),
|
||||
Err(e) => {
|
||||
tracing::warn!("[{label}] attempt {attempt}/2 failed: {e}");
|
||||
last_err = e.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(format!("failed after 2 attempts: {last_err}"))
|
||||
}
|
||||
|
||||
/// Spawn a background subagent that generates tests for modified files.
|
||||
///
|
||||
/// Uses the test-generator prompt and has read-write access so it can
|
||||
@@ -184,40 +224,13 @@ pub fn spawn_background_test_gen(
|
||||
.with_system_prompt(prompt)
|
||||
;
|
||||
|
||||
let mut ctx = build_subagent_context(&def);
|
||||
ctx.session_dir = sd;
|
||||
ctx.workspaces = ws;
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
|
||||
let _drain = std::thread::spawn(move || {
|
||||
while let Some(event) = rx.blocking_recv() {
|
||||
match &event {
|
||||
SubagentEvent::ToolCall { tool, .. } => {
|
||||
tracing::debug!("[bg-test-gen] tool: {}", tool);
|
||||
}
|
||||
SubagentEvent::ToolResult { tool, .. } => {
|
||||
tracing::debug!("[bg-test-gen] result: {}", tool);
|
||||
}
|
||||
SubagentEvent::StepCompleted { .. } => {
|
||||
tracing::trace!("[bg-test-gen] step done");
|
||||
}
|
||||
SubagentEvent::StepFailed { step, error } => {
|
||||
tracing::warn!("[bg-test-gen] step {} failed: {}", step, error);
|
||||
}
|
||||
SubagentEvent::Completed { .. } => {
|
||||
tracing::debug!("[bg-test-gen] completed");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let result = run_subagent(&ctx, &tx);
|
||||
let result = run_subagent_with_retry(&def, &sd, &ws, "bg-test-gen");
|
||||
let message = match &result {
|
||||
Ok(output) => {
|
||||
let first = output.lines().next().unwrap_or(output);
|
||||
format!("Auto test-gen: {first}")
|
||||
}
|
||||
Err(e) => format!("Auto test-gen failed: {e}"),
|
||||
Err(e) => format!("ESCALATED: Auto test-gen {e}"),
|
||||
};
|
||||
|
||||
if let Ok(mut q) = events.lock() {
|
||||
@@ -264,35 +277,13 @@ pub fn spawn_background_arch_review(
|
||||
.with_system_prompt(prompt)
|
||||
;
|
||||
|
||||
let mut ctx = build_subagent_context(&def);
|
||||
ctx.session_dir = sd;
|
||||
ctx.workspaces = ws;
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
|
||||
let _drain = std::thread::spawn(move || {
|
||||
while let Some(event) = rx.blocking_recv() {
|
||||
match &event {
|
||||
SubagentEvent::ToolCall { tool, .. } => {
|
||||
tracing::debug!("[bg-arch] tool: {}", tool);
|
||||
}
|
||||
SubagentEvent::ToolResult { tool, .. } => {
|
||||
tracing::debug!("[bg-arch] result: {}", tool);
|
||||
}
|
||||
SubagentEvent::Completed { .. } => {
|
||||
tracing::debug!("[bg-arch] completed");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let result = run_subagent(&ctx, &tx);
|
||||
let result = run_subagent_with_retry(&def, &sd, &ws, "bg-arch-review");
|
||||
let message = match &result {
|
||||
Ok(output) => {
|
||||
let first = output.lines().next().unwrap_or(output);
|
||||
format!("Architecture review: {first}")
|
||||
}
|
||||
Err(e) => format!("Architecture review failed: {e}"),
|
||||
Err(e) => format!("ESCALATED: Architecture review {e}"),
|
||||
};
|
||||
|
||||
if let Ok(mut q) = events.lock() {
|
||||
@@ -350,35 +341,13 @@ pub fn spawn_background_security_review(
|
||||
.with_system_prompt(prompt)
|
||||
;
|
||||
|
||||
let mut ctx = build_subagent_context(&def);
|
||||
ctx.session_dir = sd;
|
||||
ctx.workspaces = ws;
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
|
||||
let _drain = std::thread::spawn(move || {
|
||||
while let Some(event) = rx.blocking_recv() {
|
||||
match &event {
|
||||
SubagentEvent::ToolCall { tool, .. } => {
|
||||
tracing::debug!("[bg-security] tool: {}", tool);
|
||||
}
|
||||
SubagentEvent::ToolResult { tool, .. } => {
|
||||
tracing::debug!("[bg-security] result: {}", tool);
|
||||
}
|
||||
SubagentEvent::Completed { .. } => {
|
||||
tracing::debug!("[bg-security] completed");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let result = run_subagent(&ctx, &tx);
|
||||
let result = run_subagent_with_retry(&def, &sd, &ws, "bg-security-review");
|
||||
let message = match &result {
|
||||
Ok(output) => {
|
||||
let first = output.lines().next().unwrap_or(output);
|
||||
format!("Security review: {first}")
|
||||
}
|
||||
Err(e) => format!("Security review failed: {e}"),
|
||||
Err(e) => format!("ESCALATED: Security review {e}"),
|
||||
};
|
||||
|
||||
if let Ok(mut q) = events.lock() {
|
||||
|
||||
+81
-227
@@ -1,237 +1,91 @@
|
||||
//! Company-style agent divisions: specialized subagent roles that form an
|
||||
//! organizational hierarchy like a company.
|
||||
//! Access tiers for the anonymous processing nodes spawned by the
|
||||
//! hive-mind orchestrator (`app::workflow::hive_mind`).
|
||||
//!
|
||||
//! ```text
|
||||
//! CEO (Main Agent)
|
||||
//! ├── Strategy Division (planner) — architecture, diagrams, plan
|
||||
//! ├── Engineering Division (coder) — implementation
|
||||
//! ├── Quality Division (tester) — review, test
|
||||
//! ├── Security Division (auditor) — security audit
|
||||
//! └── Documentation Division (doc) — documentation
|
||||
//! ```
|
||||
//!
|
||||
//! Each division has a specific role, tools, and system prompt tailored to
|
||||
//! its function. The main agent (CEO) delegates work to divisions via
|
||||
//! the company pipeline workflow.
|
||||
//! Nodes have no persistent identity of their own — the Core Intelligence
|
||||
//! addresses each one only by directive and access tier. Since node
|
||||
//! designations are system-assigned coordinates rather than named roles,
|
||||
//! tool access can't be a lookup table keyed by role name. Instead the
|
||||
//! Core Intelligence picks one of these three tiers per node, matched to
|
||||
//! what that node's specific directive needs — this keeps the Harness
|
||||
//! gate meaningful while the node roster itself stays fully dynamic.
|
||||
|
||||
use crate::app::subagent::spawn::AgentDefinition;
|
||||
/// The three tool-access tiers a hive-mind node can be granted.
|
||||
pub mod tool_scope {
|
||||
/// Read-only investigation: no file mutation, no shell, no VCS.
|
||||
pub const READ: &str = "read";
|
||||
/// Read-tier plus file mutation and non-destructive shell (tests/builds).
|
||||
pub const WRITE: &str = "write";
|
||||
/// Write-tier plus delete, git, and the remaining LSP actions.
|
||||
pub const FULL: &str = "full";
|
||||
|
||||
/// Division roles — used as both the `role` field in `AgentDefinition`
|
||||
/// and as the key for pipeline routing.
|
||||
pub mod roles {
|
||||
/// Strategy Division: plans architecture, creates diagrams, breaks down work.
|
||||
pub const STRATEGY: &str = "planner";
|
||||
/// Engineering Division: implements code per the plan.
|
||||
pub const ENGINEERING: &str = "coder";
|
||||
/// Quality Division: reviews implementation, writes tests.
|
||||
pub const QUALITY: &str = "tester";
|
||||
/// Security Division: audits for vulnerabilities.
|
||||
pub const SECURITY: &str = "auditor";
|
||||
/// Documentation Division: updates docs, README, inline documentation.
|
||||
pub const DOCUMENTATION: &str = "documenter";
|
||||
}
|
||||
const READ_TOOLS: &[&str] = &[
|
||||
"read", "grep", "glob", "search", "seqthink", "recall",
|
||||
"lsp_connect", "lsp_diagnostics", "lsp_hover", "lsp_definition",
|
||||
"lsp_references", "read_findings",
|
||||
];
|
||||
|
||||
/// ─── Division Agent Definitions ───
|
||||
///
|
||||
/// Build the Strategy Division agent — chief architect and planner.
|
||||
///
|
||||
/// Tools: read-only (read, grep, glob, search, lsp, plan, seqthink, recall)
|
||||
/// Role: never writes code; produces detailed plans with mermaid diagrams.
|
||||
pub fn strategy_division() -> AgentDefinition {
|
||||
AgentDefinition::new(
|
||||
"strategy-division".to_string(),
|
||||
roles::STRATEGY.to_string(),
|
||||
)
|
||||
.with_system_prompt(crate::resources::DIVISION_PLANNER_PROMPT.to_string())
|
||||
.with_allowed_tools(vec![
|
||||
"read".to_string(),
|
||||
"grep".to_string(),
|
||||
"glob".to_string(),
|
||||
"search".to_string(),
|
||||
"seqthink".to_string(),
|
||||
"plan".to_string(),
|
||||
"recall".to_string(),
|
||||
"lsp_connect".to_string(),
|
||||
"lsp_diagnostics".to_string(),
|
||||
"lsp_hover".to_string(),
|
||||
"lsp_definition".to_string(),
|
||||
"lsp_references".to_string(),
|
||||
"read_findings".to_string(),
|
||||
])
|
||||
}
|
||||
const WRITE_TOOLS: &[&str] = &[
|
||||
"read", "grep", "glob", "search", "seqthink", "recall",
|
||||
"lsp_connect", "lsp_diagnostics", "lsp_hover", "lsp_definition",
|
||||
"lsp_references", "read_findings",
|
||||
"write", "edit", "bash", "todowrite", "todofinish", "remember",
|
||||
];
|
||||
|
||||
/// Build the Engineering Division agent — implements code per the plan.
|
||||
///
|
||||
/// Tools: full access (all write/edit/bash/git/LSP tools)
|
||||
/// Role: executes the strategy plan, one file at a time.
|
||||
pub fn engineering_division() -> AgentDefinition {
|
||||
AgentDefinition::new(
|
||||
"engineering-division".to_string(),
|
||||
roles::ENGINEERING.to_string(),
|
||||
)
|
||||
.with_system_prompt(crate::resources::DIVISION_IMPLEMENTER_PROMPT.to_string())
|
||||
.with_allowed_tools(vec![
|
||||
"read".to_string(),
|
||||
"write".to_string(),
|
||||
"edit".to_string(),
|
||||
"delete".to_string(),
|
||||
"bash".to_string(),
|
||||
"grep".to_string(),
|
||||
"glob".to_string(),
|
||||
"git_operator".to_string(),
|
||||
"seqthink".to_string(),
|
||||
"lsp_connect".to_string(),
|
||||
"lsp_diagnostics".to_string(),
|
||||
"lsp_hover".to_string(),
|
||||
"lsp_definition".to_string(),
|
||||
"lsp_references".to_string(),
|
||||
"lsp_completion".to_string(),
|
||||
"lsp_disconnect".to_string(),
|
||||
"todowrite".to_string(),
|
||||
"todofinish".to_string(),
|
||||
"read_findings".to_string(),
|
||||
])
|
||||
}
|
||||
const FULL_TOOLS: &[&str] = &[
|
||||
"read", "grep", "glob", "search", "seqthink", "recall",
|
||||
"lsp_connect", "lsp_diagnostics", "lsp_hover", "lsp_definition",
|
||||
"lsp_references", "read_findings",
|
||||
"write", "edit", "bash", "todowrite", "todofinish", "remember",
|
||||
"delete", "git_operator", "lsp_completion", "lsp_disconnect",
|
||||
];
|
||||
|
||||
/// Build the Quality Division agent — reviews code and writes tests.
|
||||
///
|
||||
/// Tools: read, write, grep, glob, bash (for running tests), LSP, memory
|
||||
/// Role: verifies correctness and creates/runs tests.
|
||||
pub fn quality_division() -> AgentDefinition {
|
||||
AgentDefinition::new(
|
||||
"quality-division".to_string(),
|
||||
roles::QUALITY.to_string(),
|
||||
)
|
||||
.with_system_prompt(crate::resources::DIVISION_TESTER_PROMPT.to_string())
|
||||
.with_allowed_tools(vec![
|
||||
"read".to_string(),
|
||||
"write".to_string(),
|
||||
"edit".to_string(),
|
||||
"grep".to_string(),
|
||||
"glob".to_string(),
|
||||
"bash".to_string(),
|
||||
"seqthink".to_string(),
|
||||
"recall".to_string(),
|
||||
"remember".to_string(),
|
||||
"lsp_connect".to_string(),
|
||||
"lsp_diagnostics".to_string(),
|
||||
"lsp_hover".to_string(),
|
||||
"lsp_definition".to_string(),
|
||||
"lsp_references".to_string(),
|
||||
"read_findings".to_string(),
|
||||
])
|
||||
}
|
||||
|
||||
/// Build the Security Division agent — security auditor.
|
||||
///
|
||||
/// Tools: read-only + search + memory
|
||||
/// Role: audits implementation for vulnerabilities.
|
||||
pub fn security_division() -> AgentDefinition {
|
||||
AgentDefinition::new(
|
||||
"security-division".to_string(),
|
||||
roles::SECURITY.to_string(),
|
||||
)
|
||||
.with_system_prompt(crate::resources::SECURITY_REVIEWER_PROMPT.to_string())
|
||||
.with_allowed_tools(vec![
|
||||
"read".to_string(),
|
||||
"grep".to_string(),
|
||||
"glob".to_string(),
|
||||
"search".to_string(),
|
||||
"seqthink".to_string(),
|
||||
"recall".to_string(),
|
||||
"remember".to_string(),
|
||||
"lsp_connect".to_string(),
|
||||
"lsp_diagnostics".to_string(),
|
||||
"lsp_hover".to_string(),
|
||||
"lsp_definition".to_string(),
|
||||
"lsp_references".to_string(),
|
||||
"read_findings".to_string(),
|
||||
])
|
||||
}
|
||||
|
||||
/// Build the Documentation Division agent — documentation maintainer.
|
||||
///
|
||||
/// Tools: read, grep, glob, write, edit, memory
|
||||
/// Role: updates README, inline docs, architecture docs.
|
||||
pub fn documentation_division() -> AgentDefinition {
|
||||
AgentDefinition::new(
|
||||
"documentation-division".to_string(),
|
||||
roles::DOCUMENTATION.to_string(),
|
||||
)
|
||||
.with_system_prompt(crate::resources::DIVISION_DOCUMENTER_PROMPT.to_string())
|
||||
.with_allowed_tools(vec![
|
||||
"read".to_string(),
|
||||
"write".to_string(),
|
||||
"edit".to_string(),
|
||||
"grep".to_string(),
|
||||
"glob".to_string(),
|
||||
"recall".to_string(),
|
||||
"remember".to_string(),
|
||||
"read_findings".to_string(),
|
||||
])
|
||||
}
|
||||
|
||||
/// ─── Division Registry ───
|
||||
///
|
||||
/// A named division with its agent definition and display metadata.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Division {
|
||||
/// Display name for the division (e.g. "Strategy", "Engineering").
|
||||
pub name: &'static str,
|
||||
/// Role tag used for pipeline routing (matches `roles::*` constants).
|
||||
#[allow(dead_code)]
|
||||
pub role: &'static str,
|
||||
/// One-line description of what this division does.
|
||||
#[allow(dead_code)]
|
||||
pub description: &'static str,
|
||||
/// Agent definition with tools, prompt, and step budget.
|
||||
pub agent_def: AgentDefinition,
|
||||
}
|
||||
|
||||
impl Division {
|
||||
pub fn new(
|
||||
name: &'static str,
|
||||
role: &'static str,
|
||||
description: &'static str,
|
||||
agent_def: AgentDefinition,
|
||||
) -> Self {
|
||||
Division { name, role, description, agent_def }
|
||||
/// Resolve a tier name to its concrete tool allowlist.
|
||||
///
|
||||
/// Unrecognized scope strings fall back to `READ` — the least-privileged
|
||||
/// tier — rather than silently granting broader access.
|
||||
///
|
||||
/// Return: an owned `Vec<String>` suitable for `AgentDefinition::with_allowed_tools`.
|
||||
pub fn tools_for(scope: &str) -> Vec<String> {
|
||||
let tools: &[&str] = match scope {
|
||||
FULL => FULL_TOOLS,
|
||||
WRITE => WRITE_TOOLS,
|
||||
_ => READ_TOOLS,
|
||||
};
|
||||
tools.iter().map(|s| (*s).to_string()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Return all company divisions as an ordered list matching the pipeline flow:
|
||||
/// Strategy → Engineering → Quality → Security → Documentation.
|
||||
pub fn all_divisions() -> Vec<Division> {
|
||||
vec![
|
||||
Division::new(
|
||||
"Strategy",
|
||||
roles::STRATEGY,
|
||||
"Architecture planning with diagrams and step-by-step breakdown",
|
||||
strategy_division(),
|
||||
),
|
||||
Division::new(
|
||||
"Engineering",
|
||||
roles::ENGINEERING,
|
||||
"Code implementation following the plan",
|
||||
engineering_division(),
|
||||
),
|
||||
Division::new(
|
||||
"Quality",
|
||||
roles::QUALITY,
|
||||
"Code review and comprehensive testing",
|
||||
quality_division(),
|
||||
),
|
||||
Division::new(
|
||||
"Security",
|
||||
roles::SECURITY,
|
||||
"Security vulnerability audit",
|
||||
security_division(),
|
||||
),
|
||||
Division::new(
|
||||
"Documentation",
|
||||
roles::DOCUMENTATION,
|
||||
"Documentation updates and maintenance",
|
||||
documentation_division(),
|
||||
),
|
||||
]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::tool_scope::{tools_for, FULL, READ, WRITE};
|
||||
|
||||
#[test]
|
||||
fn read_tier_excludes_write_tools() {
|
||||
let tools = tools_for(READ);
|
||||
assert!(!tools.contains(&"write".to_string()));
|
||||
assert!(!tools.contains(&"bash".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_tier_includes_bash_but_not_delete_or_git() {
|
||||
let tools = tools_for(WRITE);
|
||||
assert!(tools.contains(&"bash".to_string()));
|
||||
assert!(tools.contains(&"write".to_string()));
|
||||
assert!(!tools.contains(&"delete".to_string()));
|
||||
assert!(!tools.contains(&"git_operator".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_tier_includes_delete_and_git() {
|
||||
let tools = tools_for(FULL);
|
||||
assert!(tools.contains(&"delete".to_string()));
|
||||
assert!(tools.contains(&"git_operator".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_scope_falls_back_to_read() {
|
||||
let tools = tools_for("bogus");
|
||||
assert!(!tools.contains(&"write".to_string()));
|
||||
assert!(!tools.contains(&"delete".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,13 +29,13 @@ fn build_subagent_tools(allowed_tools: &[String]) -> (Vec<Box<dyn crate::tool::T
|
||||
let all = all_tools();
|
||||
let filtered: Vec<Box<dyn crate::tool::Tool>> = if allowed_tools.is_empty() {
|
||||
all.into_iter()
|
||||
.filter(|t| t.name() != "company_pipeline" && t.name() != "workflow_run")
|
||||
.filter(|t| t.name() != "hive_mind" && t.name() != "workflow_run")
|
||||
.collect()
|
||||
} else {
|
||||
all.into_iter()
|
||||
.filter(|t| {
|
||||
allowed_tools.contains(&t.name().to_string())
|
||||
&& t.name() != "company_pipeline"
|
||||
&& t.name() != "hive_mind"
|
||||
&& t.name() != "workflow_run"
|
||||
})
|
||||
.collect()
|
||||
|
||||
Reference in New Issue
Block a user