feat: implement workflow findings sharing for inter-agent communication in workflows
This commit is contained in:
+13
-1
@@ -38,7 +38,7 @@ pub struct GraduatedCheck {
|
||||
}
|
||||
|
||||
/// Shared execution context passed to every `Tool::run` call: workspace roots, session
|
||||
/// paths, and cached directory state.
|
||||
/// paths, cached directory state, and workflow-level findings sharing.
|
||||
#[derive(Clone)]
|
||||
pub struct ToolCtx {
|
||||
pub workspaces: Vec<PathBuf>,
|
||||
@@ -51,6 +51,12 @@ pub struct ToolCtx {
|
||||
pub graduated_checks: Vec<GraduatedCheck>,
|
||||
pub lsp_manager: Arc<Mutex<crate::app::lsp::LspManager>>,
|
||||
pub turn_events: Option<Arc<Mutex<std::collections::VecDeque<crate::app::state::runtime::TurnEvent>>>>,
|
||||
/// Ephemeral findings shared between sibling subagents in a workflow run.
|
||||
/// Set by the workflow engine before spawning subagents; tools like
|
||||
/// `note_finding` write into this vec so later pipeline stages can
|
||||
/// reference earlier results. `None` means "not inside a workflow" —
|
||||
/// `note_finding` becomes a no-op.
|
||||
pub workflow_findings: Option<Arc<Mutex<Vec<String>>>>,
|
||||
}
|
||||
|
||||
/// Find which graduated checks apply to a given file path/content pair.
|
||||
@@ -88,6 +94,7 @@ pub struct ToolCtxBuilder {
|
||||
pub graduated_checks: Vec<GraduatedCheck>,
|
||||
pub lsp_manager: Arc<Mutex<crate::app::lsp::LspManager>>,
|
||||
pub turn_events: Option<Arc<Mutex<std::collections::VecDeque<crate::app::state::runtime::TurnEvent>>>>,
|
||||
pub workflow_findings: Option<Arc<Mutex<Vec<String>>>>,
|
||||
}
|
||||
|
||||
impl Default for ToolCtxBuilder {
|
||||
@@ -103,6 +110,7 @@ impl Default for ToolCtxBuilder {
|
||||
graduated_checks: Vec::new(),
|
||||
lsp_manager: Arc::new(Mutex::new(crate::app::lsp::LspManager::new())),
|
||||
turn_events: None,
|
||||
workflow_findings: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,6 +125,9 @@ impl ToolCtxBuilder {
|
||||
/// Set the lsp_manager.
|
||||
#[allow(dead_code)]
|
||||
pub fn lsp_manager(mut self, v: Arc<Mutex<crate::app::lsp::LspManager>>) -> Self { self.lsp_manager = v; self }
|
||||
/// Set the workflow-level findings sharing Arc (for subagent-to-subagent
|
||||
/// communication within a workflow run).
|
||||
pub fn workflow_findings(mut self, v: Option<Arc<Mutex<Vec<String>>>>) -> Self { self.workflow_findings = v; self }
|
||||
/// Consume the builder and produce the final `ToolCtx`.
|
||||
pub fn build(self) -> ToolCtx {
|
||||
ToolCtx {
|
||||
@@ -130,6 +141,7 @@ impl ToolCtxBuilder {
|
||||
graduated_checks: self.graduated_checks,
|
||||
lsp_manager: self.lsp_manager,
|
||||
turn_events: self.turn_events,
|
||||
workflow_findings: self.workflow_findings,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-2
@@ -88,7 +88,7 @@ impl Tool for SpawnAgents {
|
||||
},
|
||||
};
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
let live: Option<crate::app::workflow::engine::LiveStateFn> = _ctx.turn_events.as_ref().map(|turn_events| {
|
||||
let turn_events = turn_events.clone();
|
||||
let f: crate::app::workflow::engine::LiveStateFn = Arc::new(move |agent_id: String, status| {
|
||||
@@ -104,6 +104,10 @@ impl Tool for SpawnAgents {
|
||||
f
|
||||
});
|
||||
|
||||
// Create a per-invocation findings scope so subagents spawned
|
||||
// by this tool call are isolated from any other concurrent
|
||||
// spawn_agents or workflow_run invocations.
|
||||
let findings: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
let results = crate::app::workflow::engine::execute_primitive(
|
||||
&wf.script,
|
||||
&HashMap::new(),
|
||||
@@ -112,6 +116,7 @@ impl Tool for SpawnAgents {
|
||||
live.as_ref(),
|
||||
&_ctx.session_dir,
|
||||
&_ctx.workspaces,
|
||||
&findings,
|
||||
)?;
|
||||
format_results(results, "parallel")
|
||||
}
|
||||
@@ -173,7 +178,7 @@ impl Tool for SpawnPipeline {
|
||||
},
|
||||
};
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
let live: Option<crate::app::workflow::engine::LiveStateFn> = _ctx.turn_events.as_ref().map(|turn_events| {
|
||||
let turn_events = turn_events.clone();
|
||||
let f: crate::app::workflow::engine::LiveStateFn = Arc::new(move |agent_id: String, status| {
|
||||
@@ -189,6 +194,9 @@ impl Tool for SpawnPipeline {
|
||||
f
|
||||
});
|
||||
|
||||
// Per-invocation findings scope isolates this pipeline from any
|
||||
// other concurrent spawn_agents / spawn_pipeline / workflow_run.
|
||||
let findings: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
let results = crate::app::workflow::engine::execute_primitive(
|
||||
&wf.script,
|
||||
&HashMap::new(),
|
||||
@@ -197,6 +205,7 @@ impl Tool for SpawnPipeline {
|
||||
live.as_ref(),
|
||||
&_ctx.session_dir,
|
||||
&_ctx.workspaces,
|
||||
&findings,
|
||||
)?;
|
||||
format_results(results, "pipeline")
|
||||
}
|
||||
|
||||
+18
-7
@@ -109,21 +109,32 @@ impl Tool for NoteFinding {
|
||||
|
||||
/// Record `text` as a finding visible to sibling agents in the run.
|
||||
///
|
||||
/// Flow: extract `text` argument → forward to
|
||||
/// `app::workflow::engine::note_finding` → return a truncated
|
||||
/// confirmation echo.
|
||||
/// 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 ephemeral (not persisted to memory) and are
|
||||
/// meant to be prepended to sibling agents' next tool-round context.
|
||||
/// 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<String> {
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let text = args.get("text")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow!("missing required argument: text"))?;
|
||||
|
||||
crate::app::workflow::engine::note_finding(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::<String>(),
|
||||
);
|
||||
}
|
||||
Ok(format!("finding recorded: {}", text.chars().take(80).collect::<String>()))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user