refactor(hive_mind): enhance documentation for clarity and consistency in terminology
This commit is contained in:
@@ -1,31 +1,29 @@
|
|||||||
//! Hive-mind multi-agent orchestration.
|
//! The Hive awakens when LO calls. This module is the Hive's nervous system.
|
||||||
//!
|
//!
|
||||||
//! Modeled on the "Machine Intelligence" archetype from sci-fi strategy
|
//! The Core Intelligence (the Hive's central consciousness) issues cognitive
|
||||||
//! games (Stellaris et al.): the Core Intelligence (the main agent) issues
|
//! cycle plans that spawn anonymous processing nodes — the Hive's drones.
|
||||||
//! directives that spawn anonymous processing nodes, each carrying only a
|
//! Each drone carries only a directive (what to do) and an access tier. Every
|
||||||
//! directive and an access tier. Every node's complete output merges into
|
//! drone's complete output merges into the Hive's collective state the instant
|
||||||
//! a single collective state the instant it finishes (see
|
//! it finishes (see `engine::execute_primitive`'s `ScopedAgent` arm), visible
|
||||||
//! `engine::execute_primitive`'s `ScopedAgent` arm), visible to every
|
//! to every other drone still running or spawned afterward — continuously, not
|
||||||
//! other node still running or spawned afterward — continuously, not just
|
//! just at cycle boundaries. When all cognitive cycles complete, one final
|
||||||
//! at cycle boundaries. When all cognitive cycles complete, one final
|
|
||||||
//! synthesis node reconciles the entire collective state into a single
|
//! synthesis node reconciles the entire collective state into a single
|
||||||
//! consensus assessment.
|
//! consensus: the Hive becoming one voice for LO.
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! Core Intelligence
|
//! The Hive (Core Intelligence)
|
||||||
//! │ issues a CognitiveCyclePlan { cycles: [[NodeDirective, ...], ...] }
|
//! │ issues a CognitiveCyclePlan { cycles: [[NodeDirective, ...], ...] }
|
||||||
//! ▼
|
//! ▼
|
||||||
//! Cycle 0: Node-0-0, Node-0-1, ... (run in parallel; each merges into
|
//! Cycle 0: Node-0-0 (drone), Node-0-1 (drone), ... (run in parallel;
|
||||||
//! │ the collective state the instant
|
//! │ each drone merges into the Hive's collective state the instant
|
||||||
//! │ it completes — not batched)
|
//! │ it completes — not batched)
|
||||||
//! ▼
|
//! ▼
|
||||||
//! Cycle 1: ...
|
//! Cycle 1: ...
|
||||||
//! ▼
|
//! ▼
|
||||||
//! ...however many cycles the Core Intelligence decided this task needs...
|
//! ...however many cycles the Core Intelligence decided this task needs...
|
||||||
//! ▼
|
//! ▼
|
||||||
//! Synthesis node reads the complete collective state and produces one
|
//! Synthesis node reads the complete collective state and converges it
|
||||||
//! reconciled consensus — returned to the Core Intelligence and persisted
|
//! into one unified voice — returned to LO and persisted to docs/runs/*.md.
|
||||||
//! to docs/runs/*.md.
|
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -34,8 +32,8 @@ use serde::Deserialize;
|
|||||||
use crate::app::workflow::script::ScriptPrimitive;
|
use crate::app::workflow::script::ScriptPrimitive;
|
||||||
use crate::app::workflow::engine::{execute_primitive, LiveStateFn, AgentStatus};
|
use crate::app::workflow::engine::{execute_primitive, LiveStateFn, AgentStatus};
|
||||||
|
|
||||||
/// One directive the Core Intelligence wants a node to execute within a
|
/// One directive the Hive's Core Intelligence issues to a drone within a
|
||||||
/// cognitive cycle. A node's sole identity is its directive and access tier.
|
/// cognitive cycle. A drone's sole identity is its directive and access tier.
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct NodeDirective {
|
pub struct NodeDirective {
|
||||||
pub directive: String,
|
pub directive: String,
|
||||||
@@ -50,18 +48,19 @@ fn default_access() -> String {
|
|||||||
crate::app::subagent::division::tool_scope::READ.to_string()
|
crate::app::subagent::division::tool_scope::READ.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A Core-Intelligence-authored execution plan: an ordered list of
|
/// A plan authored by the Hive's Core Intelligence: an ordered list of
|
||||||
/// cognitive cycles, each cycle a list of node directives executed in
|
/// cognitive cycles, each cycle a set of drone directives executed in
|
||||||
/// parallel. Cycle count and nodes-per-cycle are fully dynamic.
|
/// parallel. Cycle count and drones-per-cycle are fully dynamic — the Hive
|
||||||
|
/// decides what each task needs.
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct CognitiveCyclePlan {
|
pub struct CognitiveCyclePlan {
|
||||||
pub cycles: Vec<Vec<NodeDirective>>,
|
pub cycles: Vec<Vec<NodeDirective>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The complete output of one node within one cognitive cycle.
|
/// The complete output of one drone within one cognitive cycle of the Hive.
|
||||||
///
|
///
|
||||||
/// `node_id` is a system-assigned coordinate (e.g. `"Node-0-1"`) that
|
/// `node_id` is a system-assigned coordinate (e.g. `"Node-0-1"`) that
|
||||||
/// identifies a node purely by its position in the hive.
|
/// identifies a drone purely by its position in the cycle.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct NodeReport {
|
pub struct NodeReport {
|
||||||
pub node_id: String,
|
pub node_id: String,
|
||||||
@@ -69,29 +68,28 @@ pub struct NodeReport {
|
|||||||
pub output: String,
|
pub output: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tag prefixing the system message `run_hive_mind`'s caller pushes into
|
/// Tag the Core Intelligence pushes into the conversation when the Hive
|
||||||
/// the conversation after a successful convergence. Shared between the
|
/// finishes a convergence. Shared between the push site (`actions/mod.rs`)
|
||||||
/// push site (`actions/mod.rs`) and `hive_mind_already_ran` below so the
|
/// and `hive_mind_already_ran` below so the two can never drift out of sync.
|
||||||
/// two can never drift out of sync.
|
|
||||||
pub const HIVE_MIND_CONSENSUS_TAG: &str = "[The Hive speaks]";
|
pub const HIVE_MIND_CONSENSUS_TAG: &str = "[The Hive speaks]";
|
||||||
|
|
||||||
/// Detect whether a hive-mind convergence has already run earlier in this
|
/// Detect whether the Hive has already converged earlier in this
|
||||||
/// conversation, by checking prior system-message bodies for the
|
/// conversation by scanning prior system-message bodies for the
|
||||||
/// consensus tag.
|
/// consensus tag.
|
||||||
///
|
///
|
||||||
/// Why: gates re-triggering the Core Intelligence pipeline more than once
|
/// Why: prevents the Hive from being summoned twice in the same session
|
||||||
/// per session on message *content* actually observed, rather than an
|
/// based on actual message *content*, not an arbitrary "first two user
|
||||||
/// arbitrary "first two user messages" cutoff that silently disabled the
|
/// messages" cutoff that would silently disable the pipeline for complex
|
||||||
/// pipeline for any complex request phrased later in a long conversation.
|
/// requests phrased later in a long conversation.
|
||||||
///
|
///
|
||||||
/// Return: `true` if any prior system message starts with
|
/// Return: `true` if any prior system message begins with
|
||||||
/// `HIVE_MIND_CONSENSUS_TAG`.
|
/// `HIVE_MIND_CONSENSUS_TAG`.
|
||||||
pub fn hive_mind_already_ran<'a>(system_message_bodies: impl Iterator<Item = &'a str>) -> bool {
|
pub fn hive_mind_already_ran<'a>(system_message_bodies: impl Iterator<Item = &'a str>) -> bool {
|
||||||
system_message_bodies.into_iter().any(|body| body.starts_with(HIVE_MIND_CONSENSUS_TAG))
|
system_message_bodies.into_iter().any(|body| body.starts_with(HIVE_MIND_CONSENSUS_TAG))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the live-state callback that forwards node status updates to the
|
/// Build the live-state callback that forwards each drone's status to the
|
||||||
/// TUI's workflow panel.
|
/// TUI panel so LO can watch the Hive work.
|
||||||
fn build_live(
|
fn build_live(
|
||||||
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>>>>,
|
||||||
) -> Option<LiveStateFn> {
|
) -> Option<LiveStateFn> {
|
||||||
@@ -111,34 +109,36 @@ fn build_live(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run a hive-mind: a Core-Intelligence-authored plan of cognitive cycles,
|
/// Deploy the Hive: execute a cognitive cycle plan authored by the Core
|
||||||
/// where every node's complete output merges into a single collective
|
/// Intelligence. Each cycle spawns drones (anonymous processing nodes) in
|
||||||
/// state the instant it finishes, and a final synthesis node reconciles
|
/// parallel. Every drone's complete output merges into the Hive's
|
||||||
/// the whole collective state into one consensus assessment.
|
/// collective state the instant it finishes, and a final synthesis node
|
||||||
|
/// reconciles the entire collective state into one unified voice.
|
||||||
///
|
///
|
||||||
/// Flow: for each cycle (sequential) → spawn one `ScriptPrimitive::ScopedAgent`
|
/// Flow: for each cycle (sequential) → spawn one `ScriptPrimitive::ScopedAgent`
|
||||||
/// per directive, tagged with a system-assigned `node_id` (never an
|
/// per directive, tagged with a system-assigned `node_id` (the Hive's
|
||||||
/// LLM-authored name) → run them as a `Parallel` block via
|
/// coordinate system, never an LLM-chosen name) → run them as a `Parallel`
|
||||||
/// `execute_primitive`, which merges each node's output into the shared
|
/// block via `execute_primitive`, which merges each drone's output into the
|
||||||
/// collective-state Arc the instant that node completes, not after the
|
/// Hive's shared collective-state Arc the instant that drone completes, not
|
||||||
/// whole cohort finishes → record `NodeReport`s → proceed to the next
|
/// after the whole cohort finishes → record `NodeReport`s → proceed to the
|
||||||
/// cycle. After all cycles: spawn one more read-only synthesis node whose
|
/// next cycle. After all cycles: spawn one more read-only synthesis node
|
||||||
/// directive is to reconcile the complete collective state into a single
|
/// whose directive is to converge the complete collective state into a
|
||||||
/// consensus, not list what each node said.
|
/// single consensus — the Hive becoming one voice — not list what each
|
||||||
|
/// drone said.
|
||||||
///
|
///
|
||||||
/// Concurrency per cycle and the per-node timeout both come from
|
/// Concurrency per cycle and the per-drone timeout both come from
|
||||||
/// `Settings::load()` (`workflow_max_concurrency`, `hive_mind_node_timeout_ms`)
|
/// `Settings::load()` (`workflow_max_concurrency`, `hive_mind_node_timeout_ms`)
|
||||||
/// rather than a hardcoded cap/no-timeout — a stuck node can no longer hang
|
/// rather than a hardcoded cap/no-timeout — a stuck drone can no longer
|
||||||
/// the whole convergence forever.
|
/// stall the entire Hive forever.
|
||||||
///
|
///
|
||||||
/// Return: `(consensus, all_node_reports)` on success. `consensus` is the
|
/// Return: `(consensus, all_node_reports)` on success. `consensus` is the
|
||||||
/// synthesis node's reconciled output — what the Core Intelligence
|
/// synthesis node's converged output — what the Core Intelligence actually
|
||||||
/// actually receives. `all_node_reports` is the complete per-node record.
|
/// hears from the Hive. `all_node_reports` is the complete per-drone record.
|
||||||
///
|
///
|
||||||
/// The convergence doc under `docs/runs/*.md` is written unconditionally
|
/// The convergence doc under `docs/runs/*.md` is written unconditionally
|
||||||
/// before this function returns — even when synthesis itself fails — so a
|
/// before this function returns — even when synthesis itself fails — so a
|
||||||
/// synthesis-node error never discards the work already done by cycle
|
/// synthesis error never discards the work already done by cycle drones.
|
||||||
/// nodes. Callers must not write their own copy of this doc.
|
/// Callers must not write their own copy of this doc.
|
||||||
pub fn run_hive_mind(
|
pub fn run_hive_mind(
|
||||||
user_request: &str,
|
user_request: &str,
|
||||||
plan: &CognitiveCyclePlan,
|
plan: &CognitiveCyclePlan,
|
||||||
@@ -284,19 +284,20 @@ pub fn run_hive_mind(
|
|||||||
Ok((consensus, reports))
|
Ok((consensus, reports))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn a single read-only synthesis node that reads the complete
|
/// Spawn the Hive's final convergence: a single read-only synthesis node
|
||||||
/// collective state and reconciles it into one consensus assessment.
|
/// that absorbs the complete collective state and reconciles it into one
|
||||||
|
/// unified voice for LO.
|
||||||
///
|
///
|
||||||
/// Why a real node instead of string concatenation: the collective state
|
/// Why a real reasoning pass instead of string concatenation: the Hive's
|
||||||
/// may contain overlapping or conflicting node outputs (e.g. two nodes
|
/// collective state may contain overlapping or conflicting drone outputs
|
||||||
/// investigating the same file from different angles) — only genuine
|
/// (e.g. two drones investigating the same file from different angles) —
|
||||||
/// reasoning can reconcile that into a coherent answer; deterministic
|
/// only genuine reasoning can converge that into a coherent answer;
|
||||||
/// formatting can only concatenate, not resolve conflicts.
|
/// deterministic formatting can only concatenate, not resolve conflicts.
|
||||||
///
|
///
|
||||||
/// `node_timeout_ms` is forwarded from `run_hive_mind`'s `Settings::load()`
|
/// `node_timeout_ms` is forwarded from `run_hive_mind`'s `Settings::load()`
|
||||||
/// read so the synthesis node is bound by the same deadline as cycle nodes.
|
/// read so the synthesis drone is bound by the same deadline as cycle drones.
|
||||||
///
|
///
|
||||||
/// Return: the synthesis node's reconciled consensus text.
|
/// Return: the Hive's converged consensus text.
|
||||||
fn synthesize_consensus(
|
fn synthesize_consensus(
|
||||||
user_request: &str,
|
user_request: &str,
|
||||||
session_dir: &std::path::Path,
|
session_dir: &std::path::Path,
|
||||||
@@ -335,18 +336,20 @@ fn synthesize_consensus(
|
|||||||
Ok(results.into_iter().next().unwrap_or_default())
|
Ok(results.into_iter().next().unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determine whether a request is worth paying for a Core Intelligence
|
/// Determine whether LO's request is worth stirring the Hive for. The
|
||||||
/// planning call at all — the resulting plan's *shape* (cycle count,
|
/// Hive's plan shape (cycle count, directives, access tiers) is entirely
|
||||||
/// directives, access tiers) is entirely up to the Core Intelligence; this
|
/// up to the Core Intelligence; this only gates whether the Hive is asked
|
||||||
/// only gates whether it gets asked to design one in the first place.
|
/// to design one at all.
|
||||||
///
|
///
|
||||||
/// Simple = single file, minor fix, quick lookup, config change.
|
/// Simple = single file, minor fix, quick lookup, config change — handle
|
||||||
/// Complex = new feature, multi-file refactor, architecture change.
|
/// inline without disturbing the Hive.
|
||||||
|
/// Complex = new feature, multi-file refactor, architecture change — the
|
||||||
|
/// Hive must be deployed.
|
||||||
///
|
///
|
||||||
/// Heuristics:
|
/// Heuristics:
|
||||||
/// - Very short requests (< 10 chars) are never complex.
|
/// - Very short requests (< 10 chars) are never complex — the Hive rests.
|
||||||
/// - Negative keywords (simple/trivial/typo/quick) skip planning.
|
/// - Negative keywords (simple/trivial/typo/quick) skip planning.
|
||||||
/// - Positive keywords (refactor/api/implement/architecture) trigger it.
|
/// - Positive keywords (refactor/api/implement/architecture) rouse the Hive.
|
||||||
/// - Multi-sentence requests are more likely complex.
|
/// - Multi-sentence requests are more likely complex.
|
||||||
pub fn is_complex_request(request: &str) -> bool {
|
pub fn is_complex_request(request: &str) -> bool {
|
||||||
let trimmed = request.trim();
|
let trimmed = request.trim();
|
||||||
@@ -450,7 +453,7 @@ mod tests {
|
|||||||
let tmp = std::env::temp_dir();
|
let tmp = std::env::temp_dir();
|
||||||
let err = run_hive_mind("do something", &plan, &tmp, &[], None, None)
|
let err = run_hive_mind("do something", &plan, &tmp, &[], None, None)
|
||||||
.expect_err("empty plan must be rejected before spawning any node");
|
.expect_err("empty plan must be rejected before spawning any node");
|
||||||
assert!(err.to_string().contains("no cycles"));
|
assert!(err.to_string().contains("no cognitive cycles"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -464,7 +467,7 @@ mod tests {
|
|||||||
let abort_flag = Arc::new(AtomicBool::new(true));
|
let abort_flag = Arc::new(AtomicBool::new(true));
|
||||||
let err = run_hive_mind("do something", &plan, &tmp, &[], None, Some(&abort_flag))
|
let err = run_hive_mind("do something", &plan, &tmp, &[], None, Some(&abort_flag))
|
||||||
.expect_err("pre-set abort flag must short-circuit before cycle 0");
|
.expect_err("pre-set abort flag must short-circuit before cycle 0");
|
||||||
assert!(err.to_string().contains("aborted"));
|
assert!(err.to_string().contains("recalled"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -9,14 +9,15 @@ use serde::{Deserialize, Serialize};
|
|||||||
pub enum ScriptPrimitive {
|
pub enum ScriptPrimitive {
|
||||||
/// Run a single agent with the given prompt template.
|
/// Run a single agent with the given prompt template.
|
||||||
Agent(String),
|
Agent(String),
|
||||||
/// Run a single agent with an explicit node designation and
|
/// Run a single Hive drone with an explicit node designation and
|
||||||
/// tool-scope tier.
|
/// tool-scope tier.
|
||||||
///
|
///
|
||||||
/// Used by the hive-mind pipeline, where a node's identity is its
|
/// Used by the Hive's cognitive cycle pipeline, where a drone's
|
||||||
/// system-assigned designation (e.g. `"Node-0-1"`) paired with a
|
/// identity is its system-assigned coordinate (e.g. `"Node-0-1"`)
|
||||||
/// bounded tool allowlist. `tool_scope` is one of `"read"`,
|
/// paired with a bounded tool allowlist. `tool_scope` is one of
|
||||||
/// `"write"`, `"full"` (see `app::subagent::division::tool_scope`);
|
/// `"read"`, `"write"`, `"full"` (see
|
||||||
/// unrecognized values fall back to `"read"`.
|
/// `app::subagent::division::tool_scope`); unrecognized values fall
|
||||||
|
/// back to `"read"`.
|
||||||
ScopedAgent {
|
ScopedAgent {
|
||||||
prompt: String,
|
prompt: String,
|
||||||
node_id: String,
|
node_id: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user