2026-07-12 11:28:39 +07:00
//! 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.
2026-07-11 13:16:10 +07:00
use serde_json ::{ json , Value };
use anyhow ::{ Result , anyhow };
use super ::Tool ;
use super ::ToolCtx ;
2026-07-12 11:28:39 +07:00
/// Tool that parses and executes a JSON-encoded workflow script (Agent/Parallel/Pipeline/Phase).
2026-07-11 13:16:10 +07:00
pub struct WorkflowRun ;
impl Tool for WorkflowRun {
fn name ( & self ) -> & 'static str {
"workflow_run"
}
fn description ( & self ) -> & 'static str {
2026-07-11 23:45:13 +07:00
"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."
2026-07-11 13:16:10 +07:00
}
fn parameters ( & self ) -> Value {
json! ({
"type" : "object" ,
"properties" : {
"script" : {
"type" : "string" ,
2026-07-11 23:45:13 +07:00
"description" : "JSON-encoded workflow script with name, description, script (Agent/Parallel/Pipeline/Phase primitives), and options (max_concurrency, continue_on_error)"
2026-07-11 13:16:10 +07:00
},
"args" : {
"type" : "object" ,
2026-07-11 23:45:13 +07:00
"description" : "Optional string key-value arguments passed to the workflow script for template substitution ({{key}} placeholders)"
2026-07-11 13:16:10 +07:00
}
},
"required" : [ "script" ]
})
}
2026-07-12 11:28:39 +07:00
/// Parse the `script`/`args` tool arguments and execute the workflow.
///
/// Flow: extract `script` string → deserialize into `WorkflowScript` →
/// collect optional `args` object into a `HashMap<String, String>` 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.
2026-07-13 08:12:02 +07:00
fn run ( & self , ctx : & ToolCtx , args : & Value ) -> Result < String > {
2026-07-11 20:21:59 +07:00
let script_str = args . get ( "script" )
2026-07-11 13:16:10 +07:00
. and_then ( | v | v . as_str ())
. ok_or_else ( || anyhow! ( "missing required argument: script" )) ? ;
2026-07-11 20:21:59 +07:00
let workflow_script : crate ::app ::workflow ::script ::WorkflowScript =
serde_json ::from_str ( script_str )
2026-07-13 08:12:02 +07:00
. map_err ( | e | anyhow! ( "failed to parse workflow script: {e}" )) ? ;
2026-07-11 20:21:59 +07:00
let workflow_args : std ::collections ::HashMap < String , String > = 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 ();
2026-07-12 18:09:03 +07:00
crate ::app ::workflow ::engine ::run_workflow (
2026-07-13 08:12:02 +07:00
& workflow_script , & workflow_args , & ctx . session_dir , & ctx . workspaces ,
2026-07-12 18:09:03 +07:00
)
2026-07-11 13:16:10 +07:00
}
}
2026-07-11 23:45:13 +07:00
2026-07-12 11:28:39 +07:00
/// Tool that shares a text finding with sibling agents in the current workflow run.
2026-07-11 23:45:13 +07:00
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" ]
})
}
2026-07-12 11:28:39 +07:00
/// Record `text` as a finding visible to sibling agents in the run.
///
2026-07-13 03:12:37 +07:00
/// Flow: extract `text` argument → push into
/// `ctx.workflow_findings` (the per-invocation Arc threaded through
/// `execute_primitive`) → return a truncated confirmation echo.
2026-07-12 11:28:39 +07:00
///
2026-07-13 03:12:37 +07:00
/// 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.
2026-07-12 11:28:39 +07:00
///
/// Return: confirmation string containing up to the first 80 chars
/// of the recorded text.
2026-07-13 03:12:37 +07:00
fn run ( & self , ctx : & ToolCtx , args : & Value ) -> Result < String > {
2026-07-11 23:45:13 +07:00
let text = args . get ( "text" )
. and_then ( | v | v . as_str ())
. ok_or_else ( || anyhow! ( "missing required argument: text" )) ? ;
2026-07-13 03:12:37 +07:00
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 ::< String > (),
);
}
2026-07-11 23:45:13 +07:00
Ok ( format! ( "finding recorded: {} " , text . chars (). take ( 80 ). collect ::< String > ()))
}
}
2026-07-13 05:23:38 +07:00
/// 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"
}
},
"required" : [ "request" ]
})
}
2026-07-13 08:12:02 +07:00
fn run ( & self , ctx : & ToolCtx , args : & Value ) -> Result < String > {
2026-07-13 05:23:38 +07:00
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" );
match mode {
"quick" => {
crate ::app ::workflow ::company ::run_company_pipeline_quick (
request ,
2026-07-13 08:12:02 +07:00
& ctx . session_dir ,
& ctx . workspaces ,
ctx . turn_events . as_ref (),
2026-07-13 05:23:38 +07:00
)
}
_ => {
crate ::app ::workflow ::company ::run_company_pipeline (
request ,
2026-07-13 08:12:02 +07:00
& ctx . session_dir ,
& ctx . workspaces ,
ctx . turn_events . as_ref (),
2026-07-13 05:23:38 +07:00
)
}
}
}
}