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"
2026-07-13 13:31:20 +07:00
},
"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
}
}
2026-07-13 05:23:38 +07:00
}
},
2026-07-13 13:31:20 +07:00
"required" : [ "request" , "specialists" ]
2026-07-13 05:23:38 +07:00
})
}
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" );
2026-07-13 13:31:20 +07:00
let custom_specialists : std ::collections ::HashMap < String , Vec < ( String , String ) >> = 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 . get ( 0 ) ? . 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" )) ? ;
2026-07-13 09:09:23 +07:00
let no_abort : Option < std ::sync ::Arc < std ::sync ::atomic ::AtomicBool >> = None ;
2026-07-13 05:23:38 +07:00
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 09:09:23 +07:00
& no_abort ,
2026-07-13 13:31:20 +07:00
custom_specialists ,
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 09:09:23 +07:00
& no_abort ,
2026-07-13 13:31:20 +07:00
custom_specialists ,
2026-07-13 05:23:38 +07:00
)
}
}
}
}
2026-07-13 11:12:31 +07:00
/// 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 < String > {
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 ::< Vec < _ >> ()
. join ( " \n " );
Ok ( format! ( "Findings in this workflow run: \n {} " , formatted ))
}
} else {
Ok ( "No findings database available (called outside a workflow run)." . to_string ())
}
}
}