//! Tools for orchestrating multi-agent workflow runs. //! //! Flow: the LLM emits a `workflow_run` tool call with a JSON-encoded //! `WorkflowScript` (Agent/Parallel/Pipeline/Phase primitives) which is //! deserialized and handed to `app::workflow::engine::run_workflow` for //! execution. Sibling agents spawned within the same run can share //! ephemeral text via the `note_finding` tool, which forwards to //! `app::workflow::engine::note_finding`. //! //! Why: decomposing a task into a workflow script lets the harness fan //! out independent subtasks (parallel/pipeline/phased) instead of the //! agent handling everything inline; simple tasks should skip this tool //! entirely per its own description string. use serde_json::{json, Value}; use anyhow::{Result, anyhow}; use super::Tool; use super::ToolCtx; /// Tool that parses and executes a JSON-encoded workflow script (Agent/Parallel/Pipeline/Phase). pub struct WorkflowRun; impl Tool for WorkflowRun { fn name(&self) -> &'static str { "workflow_run" } fn description(&self) -> &'static str { "Execute a workflow script that can spawn multiple subagents in parallel, pipeline, or phased stages. Use when a task benefits from decomposition into independent subtasks. Simple tasks should be handled inline without this tool." } fn parameters(&self) -> Value { json!({ "type": "object", "properties": { "script": { "type": "string", "description": "JSON-encoded workflow script with name, description, script (Agent/Parallel/Pipeline/Phase primitives), and options (max_concurrency, continue_on_error)" }, "args": { "type": "object", "description": "Optional string key-value arguments passed to the workflow script for template substitution ({{key}} placeholders)" } }, "required": ["script"] }) } /// Parse the `script`/`args` tool arguments and execute the workflow. /// /// Flow: extract `script` string → deserialize into `WorkflowScript` → /// collect optional `args` object into a `HashMap` for /// `{{key}}` template substitution → delegate to /// `app::workflow::engine::run_workflow`. /// /// Why: template args are silently filtered to string values only /// (non-string values are dropped rather than erroring). /// /// Return: the workflow engine's output string, or an error if the /// script argument is missing or fails to parse as JSON. fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let script_str = args.get("script") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow!("missing required argument: script"))?; let workflow_script: crate::app::workflow::script::WorkflowScript = serde_json::from_str(script_str) .map_err(|e| anyhow!("failed to parse workflow script: {e}"))?; let workflow_args: std::collections::HashMap = args.get("args") .and_then(|v| v.as_object()) .map(|obj| { obj.iter().filter_map(|(k, v)| { v.as_str().map(|s| (k.clone(), s.to_string())) }).collect() }) .unwrap_or_default(); crate::app::workflow::engine::run_workflow( &workflow_script, &workflow_args, &ctx.session_dir, &ctx.workspaces, ) } } /// Tool that shares a text finding with sibling agents in the current workflow run. pub struct NoteFinding; impl Tool for NoteFinding { fn name(&self) -> &'static str { "note_finding" } fn description(&self) -> &'static str { "Share a finding with sibling agents in the same workflow_run. Findings are ephemeral to the current run and will be prepended to other agents' next tool-round context. Does not persist to memory." } fn parameters(&self) -> Value { json!({ "type": "object", "properties": { "text": { "type": "string", "description": "The finding to share with sibling agents" } }, "required": ["text"] }) } /// Record `text` as a finding visible to sibling agents in the run. /// /// Flow: extract `text` argument → push into /// `ctx.workflow_findings` (the per-invocation Arc threaded through /// `execute_primitive`) → return a truncated confirmation echo. /// /// Why: findings are scoped per workflow invocation, not global, /// so concurrent workflow runs are isolated from each other. /// If no workflow findings Arc is set (called outside a workflow), /// the call is silently ignored. /// /// Return: confirmation string containing up to the first 80 chars /// of the recorded text. fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let text = args.get("text") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow!("missing required argument: text"))?; if let Some(ref findings) = ctx.workflow_findings { if let Ok(mut f) = findings.lock() { f.push(text.to_string()); } } else { tracing::debug!( "[note_finding] called outside a workflow run — discarding: {}", text.chars().take(80).collect::(), ); } Ok(format!("finding recorded: {}", text.chars().take(80).collect::())) } } /// Tool that delegates work to the company-style division pipeline. /// /// The main agent (CEO) calls this tool to pass a user request through the /// full company organization: Strategy → Engineering → Quality → Security /// → Documentation. Returns an executive summary. /// /// Use this for any complex or multi-step task. For simple tasks, handle /// inline or use the quick variant. pub struct CompanyPipeline; impl Tool for CompanyPipeline { fn name(&self) -> &'static str { "company_pipeline" } fn description(&self) -> &'static str { "Delegate a task to the full company division pipeline: Strategy (plan+diagrams) → Engineering (implement) → Quality (review+test) → Security (audit) → Documentation (docs). Use this for ALL non-trivial tasks instead of doing them yourself. The pipeline returns an executive summary." } fn parameters(&self) -> Value { json!({ "type": "object", "properties": { "request": { "type": "string", "description": "The task description to delegate to the company pipeline" }, "mode": { "type": "string", "enum": ["full", "quick"], "description": "Pipeline mode: 'full' (5 divisions) for complex tasks, 'quick' (3 divisions: Strategy→Engineering→Quality) for simpler tasks", "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", "specialists"] }) } fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let request = args.get("request") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow!("missing required argument: request"))?; let mode = args.get("mode") .and_then(|v| v.as_str()) .unwrap_or("full"); let custom_specialists: std::collections::HashMap> = 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.first()?.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> = None; match mode { "quick" => { crate::app::workflow::company::run_company_pipeline_quick( request, &ctx.session_dir, &ctx.workspaces, ctx.turn_events.as_ref(), &no_abort, &custom_specialists, ) } _ => { crate::app::workflow::company::run_company_pipeline( request, &ctx.session_dir, &ctx.workspaces, ctx.turn_events.as_ref(), &no_abort, &custom_specialists, ) } } } } /// Tool that retrieves all findings shared by sibling agents in the current workflow run. pub struct ReadFindings; impl Tool for ReadFindings { fn name(&self) -> &'static str { "read_findings" } fn description(&self) -> &'static str { "Retrieve all findings shared by sibling agents in the current workflow run. Use this to get real-time context updates from other divisions/subagents working in parallel." } fn parameters(&self) -> Value { json!({ "type": "object", "properties": {} }) } fn run(&self, ctx: &ToolCtx, _args: &Value) -> Result { if let Some(ref findings) = ctx.workflow_findings { let f = findings.lock().map_err(|e| anyhow!("poisoned lock: {e}"))?; if f.is_empty() { Ok("No findings recorded yet in this workflow run.".to_string()) } else { let formatted = f .iter() .enumerate() .map(|(i, f)| format!("{}. {}", i + 1, f)) .collect::>() .join("\n"); Ok(format!("Findings in this workflow run:\n{formatted}")) } } else { Ok("No findings database available (called outside a workflow run).".to_string()) } } }