2026-07-20 09:04:57 +07:00
|
|
|
//! Workflow tools — orchestrate multi-step agent workflows and hive-mind convergence.
|
2026-07-20 15:53:20 +07:00
|
|
|
//!
|
|
|
|
|
//! `WorkflowRun` executes a YAML-defined multi-step workflow. `NoteFinding` and
|
|
|
|
|
//! `ReadFindings` record and retrieve findings during execution. `HiveMind`
|
|
|
|
|
//! orchestrates a convergence — multiple parallel agent cycles followed by
|
|
|
|
|
//! consensus synthesis.
|
2026-07-20 09:04:57 +07:00
|
|
|
|
|
|
|
|
use anyhow::Result;
|
|
|
|
|
use serde_json::{json, Value};
|
2026-07-20 15:53:20 +07:00
|
|
|
use tracing::{debug, info, instrument, warn};
|
2026-07-20 09:04:57 +07:00
|
|
|
|
|
|
|
|
use crate::llm::provider::LlmClient;
|
|
|
|
|
use crate::tools::{arg_str, Tool, ToolCtx};
|
|
|
|
|
use crate::workflow::engine::execution::execute_workflow;
|
|
|
|
|
use crate::workflow::hive_mind::cycle::execute_cycle;
|
|
|
|
|
use crate::workflow::hive_mind::synthesis::synthesize_consensus;
|
2026-07-21 06:24:20 +07:00
|
|
|
use zesdex_domain::workflow::{CognitiveCycle, NodeDirective, NodeOutput, WorkflowScript};
|
2026-07-20 09:04:57 +07:00
|
|
|
|
|
|
|
|
/// Execute a multi-step workflow defined in YAML.
|
|
|
|
|
///
|
|
|
|
|
/// Flow: parse YAML → build plan from script phases → execute via workflow engine.
|
|
|
|
|
pub struct WorkflowRun;
|
|
|
|
|
|
|
|
|
|
impl Tool for WorkflowRun {
|
|
|
|
|
fn name(&self) -> &'static str {
|
|
|
|
|
"workflow_run"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn description(&self) -> &'static str {
|
|
|
|
|
"Execute a multi-step workflow defined in YAML"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parameters(&self) -> Value {
|
|
|
|
|
json!({
|
|
|
|
|
"type": "object",
|
|
|
|
|
"properties": {
|
|
|
|
|
"workflow_yaml": {
|
|
|
|
|
"type": "string",
|
|
|
|
|
"description": "YAML workflow definition"
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
"required": ["workflow_yaml"]
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 15:53:20 +07:00
|
|
|
#[instrument(skip(self, ctx, args))]
|
2026-07-20 09:04:57 +07:00
|
|
|
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
|
|
|
|
let yaml = arg_str(args, "workflow_yaml")?;
|
2026-07-21 06:24:20 +07:00
|
|
|
let script = crate::workflow::script::parse_workflow_script(&yaml)?;
|
2026-07-20 09:04:57 +07:00
|
|
|
info!(
|
|
|
|
|
"Workflow started: {} ({} phases)",
|
|
|
|
|
script.name,
|
|
|
|
|
script.phases.len()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let phase_names: Vec<&str> = script.phases.iter().map(|p| p.name.as_str()).collect();
|
|
|
|
|
info!(
|
|
|
|
|
"Workflow '{}' phases: {}",
|
|
|
|
|
script.name,
|
|
|
|
|
phase_names.join(", ")
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let llm_client = LlmClient::new(
|
|
|
|
|
crate::llm::provider::DEFAULT_API_KEY.to_string(),
|
2026-07-21 07:00:15 +07:00
|
|
|
zesdex_domain::agent::defaults::DEFAULT_MODEL.to_string(),
|
2026-07-20 09:04:57 +07:00
|
|
|
None,
|
|
|
|
|
);
|
|
|
|
|
let rt = tokio::runtime::Runtime::new()?;
|
|
|
|
|
let result: Vec<String> =
|
|
|
|
|
rt.block_on(async { execute_workflow(&script, ctx, &llm_client).await })?;
|
|
|
|
|
|
2026-07-20 15:53:20 +07:00
|
|
|
info!(phase_count = result.len(), "Workflow completed");
|
2026-07-20 09:04:57 +07:00
|
|
|
Ok(format!(
|
|
|
|
|
"Workflow '{}' completed.\n\n{}",
|
|
|
|
|
script.name,
|
|
|
|
|
result.join("\n---\n")
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 15:53:20 +07:00
|
|
|
/// Record a finding during workflow or hive-mind execution.
|
|
|
|
|
///
|
|
|
|
|
/// Flow: extract finding text and optional category → prepend `[category]` tag
|
|
|
|
|
/// → push onto `ctx.workflow_findings` shared list.
|
2026-07-20 09:04:57 +07:00
|
|
|
pub struct NoteFinding;
|
|
|
|
|
|
|
|
|
|
impl Tool for NoteFinding {
|
|
|
|
|
fn name(&self) -> &'static str {
|
|
|
|
|
"note_finding"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn description(&self) -> &'static str {
|
|
|
|
|
"Record a finding during workflow or hive-mind execution"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parameters(&self) -> Value {
|
|
|
|
|
json!({
|
|
|
|
|
"type": "object",
|
|
|
|
|
"properties": {
|
|
|
|
|
"finding": {
|
|
|
|
|
"type": "string",
|
|
|
|
|
"description": "The finding text"
|
|
|
|
|
},
|
|
|
|
|
"category": {
|
|
|
|
|
"type": "string",
|
|
|
|
|
"description": "Category for the finding"
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
"required": ["finding"]
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 15:53:20 +07:00
|
|
|
#[instrument(skip(self, ctx, args))]
|
2026-07-20 09:04:57 +07:00
|
|
|
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
|
|
|
|
let finding = crate::tools::arg_str(args, "finding")?;
|
2026-07-20 12:42:53 +07:00
|
|
|
let category = args
|
|
|
|
|
.get("category")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.unwrap_or("general");
|
|
|
|
|
|
|
|
|
|
let tagged = format!("[{category}] {finding}");
|
2026-07-20 09:04:57 +07:00
|
|
|
|
|
|
|
|
if let Some(ref findings) = ctx.workflow_findings {
|
|
|
|
|
if let Ok(mut guard) = findings.lock() {
|
2026-07-20 12:42:53 +07:00
|
|
|
guard.push(tagged);
|
2026-07-20 15:53:20 +07:00
|
|
|
info!(finding_count = guard.len(), category = %category, "finding recorded");
|
2026-07-20 09:04:57 +07:00
|
|
|
}
|
2026-07-20 15:53:20 +07:00
|
|
|
} else {
|
|
|
|
|
debug!("no workflow_findings channel available — finding not persisted");
|
2026-07-20 09:04:57 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(format!("Finding recorded: {finding}"))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 15:53:20 +07:00
|
|
|
/// Read all findings recorded so far in the current workflow.
|
|
|
|
|
///
|
|
|
|
|
/// Flow: lock `ctx.workflow_findings` → clone the list → format as numbered output.
|
2026-07-20 09:04:57 +07:00
|
|
|
pub struct ReadFindings;
|
|
|
|
|
|
|
|
|
|
impl Tool for ReadFindings {
|
|
|
|
|
fn name(&self) -> &'static str {
|
|
|
|
|
"read_findings"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn description(&self) -> &'static str {
|
|
|
|
|
"Read all findings recorded so far in the current workflow"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parameters(&self) -> Value {
|
|
|
|
|
json!({
|
|
|
|
|
"type": "object",
|
|
|
|
|
"properties": {}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 15:53:20 +07:00
|
|
|
#[instrument(skip(self, ctx, _args))]
|
2026-07-20 09:04:57 +07:00
|
|
|
fn run(&self, ctx: &ToolCtx, _args: &Value) -> Result<String> {
|
|
|
|
|
let findings = ctx
|
|
|
|
|
.workflow_findings
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|f| f.lock().ok())
|
2026-07-20 15:53:20 +07:00
|
|
|
.map(|guard| {
|
|
|
|
|
debug!(finding_count = guard.len(), "reading findings");
|
|
|
|
|
guard.clone()
|
|
|
|
|
})
|
2026-07-20 09:04:57 +07:00
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
if findings.is_empty() {
|
|
|
|
|
return Ok("No findings recorded yet.".to_string());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(format!(
|
|
|
|
|
"Findings ({}):\n{}",
|
|
|
|
|
findings.len(),
|
|
|
|
|
findings.join("\n")
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Orchestrate a hive-mind convergence — multiple agents across parallel cycles.
|
|
|
|
|
///
|
|
|
|
|
/// Flow: parse cycles from args → execute each cycle via `execute_cycle` →
|
|
|
|
|
/// collect all node outputs → synthesize consensus → return report.
|
|
|
|
|
pub struct HiveMind;
|
|
|
|
|
|
|
|
|
|
impl Tool for HiveMind {
|
|
|
|
|
fn name(&self) -> &'static str {
|
|
|
|
|
"hive_mind"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn description(&self) -> &'static str {
|
|
|
|
|
"Run a hive-mind convergence with multiple nodes across sequential cycles"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parameters(&self) -> Value {
|
|
|
|
|
json!({
|
|
|
|
|
"type": "object",
|
|
|
|
|
"properties": {
|
|
|
|
|
"cycles": {
|
|
|
|
|
"type": "array",
|
|
|
|
|
"items": {
|
|
|
|
|
"type": "object",
|
|
|
|
|
"properties": {
|
|
|
|
|
"directives": {
|
|
|
|
|
"type": "array",
|
|
|
|
|
"items": {
|
|
|
|
|
"type": "object",
|
|
|
|
|
"properties": {
|
|
|
|
|
"directive": {"type": "string"},
|
|
|
|
|
"access": {"type": "string", "enum": ["read", "write", "full"]}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
"description": "Array of cycles, each with an array of node directives"
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
"required": ["cycles"]
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 15:53:20 +07:00
|
|
|
#[instrument(skip(self, ctx, args))]
|
2026-07-20 09:04:57 +07:00
|
|
|
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
|
|
|
|
let cycles_val = args
|
|
|
|
|
.get("cycles")
|
|
|
|
|
.and_then(|v| v.as_array())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("missing 'cycles' array"))?;
|
|
|
|
|
|
|
|
|
|
info!("Hive mind starting with {} cycles", cycles_val.len());
|
|
|
|
|
let rt = tokio::runtime::Runtime::new()?;
|
|
|
|
|
let mut all_node_outputs = Vec::new();
|
|
|
|
|
|
|
|
|
|
for (cycle_idx, cycle_val) in cycles_val.iter().enumerate() {
|
2026-07-20 12:42:53 +07:00
|
|
|
let directives: Vec<NodeDirective> = cycle_val
|
2026-07-20 09:04:57 +07:00
|
|
|
.get("directives")
|
|
|
|
|
.and_then(|v| v.as_array())
|
|
|
|
|
.map(|arr| {
|
|
|
|
|
arr.iter()
|
2026-07-20 12:42:53 +07:00
|
|
|
.filter_map(|d| {
|
|
|
|
|
let directive = d
|
|
|
|
|
.get("directive")
|
|
|
|
|
.and_then(|v| v.as_str())?;
|
|
|
|
|
let access = d
|
|
|
|
|
.get("access")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.unwrap_or("read");
|
|
|
|
|
Some(NodeDirective {
|
|
|
|
|
directive: directive.to_string(),
|
|
|
|
|
access_tier: access.to_string(),
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-07-20 09:04:57 +07:00
|
|
|
.collect()
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
2026-07-20 15:53:20 +07:00
|
|
|
info!(cycle_index = cycle_idx, node_count = directives.len(), "executing hive-mind cycle");
|
2026-07-20 09:04:57 +07:00
|
|
|
let cycle = CognitiveCycle {
|
|
|
|
|
index: cycle_idx as u32,
|
|
|
|
|
directives,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let nodes: Vec<NodeOutput> =
|
|
|
|
|
rt.block_on(async { execute_cycle(&cycle, ctx).await })?;
|
|
|
|
|
all_node_outputs.extend(nodes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let node_count = all_node_outputs.len();
|
2026-07-20 15:53:20 +07:00
|
|
|
info!(node_count, "all cycles completed, synthesizing consensus");
|
2026-07-20 09:04:57 +07:00
|
|
|
let consensus =
|
|
|
|
|
rt.block_on(async { synthesize_consensus(&all_node_outputs, ctx).await })?;
|
|
|
|
|
|
|
|
|
|
let report = format!(
|
|
|
|
|
"Hive mind convergence completed.\nNodes executed: {}\n\nConsensus:\n{}",
|
|
|
|
|
node_count, consensus
|
|
|
|
|
);
|
|
|
|
|
Ok(report)
|
|
|
|
|
}
|
|
|
|
|
}
|