use crate::agentic;
use crate::agentic::artifact_read::ArtifactReadContext;
use crate::agentic::content_spill::SpillStore;
use crate::agentic::crew_tool::CrewRunner;
use crate::agentic::display::ToolDisplay;
use crate::agentic::git_tool::GitTool;
use crate::agentic::mcp::McpTools;
use crate::agentic::memory_fetch::MemorySource;
use crate::agentic::note_sink::NoteSink;
use crate::agentic::permissions::PermissionGate;
use crate::agentic::prompt_intake::PromptDisposition;
use crate::agentic::prompt_read::PromptReadContext;
use crate::agentic::recall::RecallSource;
use crate::agentic::tools::live_output::ToolSpinner;
use crate::agentic::tools::{execute_tool_inner, shell, tool_presentation};
#[allow(clippy::too_many_arguments)]
#[derive(Default)]
pub(crate) struct ToolCollaborators<'a, 'gate> {
pub(crate) command_budget: crate::RunCommandBudget,
pub(crate) shell_dialect: &'static str,
pub(crate) read_history: Option<&'a mut super::ReadHistory>,
pub(crate) default_command_cwd: Option<&'a std::path::Path>,
pub(crate) worktree_session: Option<&'a crate::worktree_adoption::WorktreeSession>,
pub(crate) invocation: Option<&'a agentic::smart_harness::ToolInvocation<'a>>,
pub(crate) build_check_cmd: Option<&'a str>,
pub(crate) tool_evidence: Option<&'a agentic::capability_check::Evidence>,
pub(crate) note_sink: Option<&'a mut dyn NoteSink>,
pub(crate) recall_source: Option<&'a dyn RecallSource>,
pub(crate) memory_source: Option<&'a dyn MemorySource>,
pub(crate) prompt_context: Option<PromptReadContext<'a>>,
pub(crate) artifact_context: Option<ArtifactReadContext<'a>>,
pub(crate) artifact_sink: Option<&'a dyn agentic::artifact_read::PromptArtifactSink>,
pub(crate) permission_gate: Option<&'gate mut dyn PermissionGate>,
pub(crate) exec_floor: Option<&'a crate::caveats::Scope<String>>,
pub(crate) git_tool: Option<&'a dyn GitTool>,
pub(crate) crew_runner: Option<&'a dyn CrewRunner>,
pub(crate) scratchpad_store: Option<&'a dyn agentic::scratchpad::ScratchpadStore>,
pub(crate) code_search: Option<agentic::semantic::CodeSearch<'a>>,
pub(crate) where_is: Option<&'a crate::where_is::WhereIsIndex>,
pub(crate) nav: Option<crate::navigator::NavToolCtx<'a>>,
pub(crate) experience_store: Option<&'a dyn agentic::experiential::ExperienceStore>,
pub(crate) step_ledger: Option<&'a dyn agentic::scheduled::StepLedger>,
pub(crate) operating_mode_control: Option<&'a dyn agentic::OperatingModeControl>,
pub(crate) plan_mode_control: Option<&'a dyn agentic::PlanModeControl>,
pub(crate) plan_draft_sink: Option<&'a dyn agentic::PlanDraftSink>,
pub(crate) spill_store: Option<&'a dyn SpillStore>,
pub(crate) persona_tools: Option<&'a [String]>,
pub(crate) hidden_tools: Option<&'a super::HiddenTools>,
pub(crate) live_tool_output: Option<std::sync::Arc<dyn crate::agentic::LiveToolOutput>>,
pub(crate) completed_spill_renderer:
Option<std::sync::Arc<dyn crate::agentic::CompletedSpillRenderer>>,
pub(crate) report_capture: Option<&'a std::sync::Mutex<super::super::observed_report::Capture>>,
pub(crate) execution: Option<&'a std::sync::OnceLock<crate::ExecOutcome>>,
pub(crate) retry_authorized: Option<&'a std::sync::OnceLock<()>>,
pub(crate) governed_pr: Option<&'a std::sync::OnceLock<crate::git_staging::Outcome>>,
pub(crate) command_directory: Option<&'a std::sync::OnceLock<std::path::PathBuf>>,
pub(crate) routed_to: Option<&'a std::sync::OnceLock<(&'static str, serde_json::Value)>>,
pub(crate) pending_rerun: Option<&'gate mut Option<super::PendingRerun>>,
}
#[allow(clippy::too_many_arguments)]
pub async fn execute_tool(
name: &str,
args: &serde_json::Value,
workspace: &str,
color: bool,
tool_output_lines: usize,
caveats: &crate::caveats::Caveats,
mcp: &mut dyn McpTools,
build_check_cmd: Option<&str>,
note_sink: Option<&mut dyn NoteSink>,
recall_source: Option<&dyn RecallSource>,
memory_source: Option<&dyn MemorySource>,
permission_gate: Option<&mut dyn PermissionGate>,
exec_floor: Option<&crate::caveats::Scope<String>>,
git_tool: Option<&dyn GitTool>,
crew_runner: Option<&dyn CrewRunner>,
scratchpad_store: Option<&dyn agentic::scratchpad::ScratchpadStore>,
code_search: Option<agentic::semantic::CodeSearch<'_>>,
where_is: Option<&crate::where_is::WhereIsIndex>,
experience_store: Option<&dyn agentic::experiential::ExperienceStore>,
step_ledger: Option<&dyn agentic::scheduled::StepLedger>,
) -> String {
let collab = ToolCollaborators {
build_check_cmd,
note_sink: note_sink.map(|s| &mut *s as &mut dyn NoteSink),
recall_source,
memory_source,
permission_gate: permission_gate.map(|g| &mut *g as &mut dyn PermissionGate),
exec_floor,
git_tool,
crew_runner,
scratchpad_store,
code_search,
where_is,
nav: None,
experience_store,
step_ledger,
..Default::default()
};
execute_tool_with_collaborators(
name,
args,
workspace,
color,
tool_output_lines,
caveats,
mcp,
collab,
false,
PromptDisposition::Act,
None,
)
.await
.expect("legacy dispatch has no durable completion writer")
.expect("tool execution without a cancellation flag cannot be interrupted")
}
#[allow(clippy::too_many_arguments)]
pub async fn execute_tool_with_offload(
name: &str,
args: &serde_json::Value,
workspace: &str,
color: bool,
tool_output_lines: usize,
caveats: &crate::caveats::Caveats,
mcp: &mut dyn McpTools,
build_check_cmd: Option<&str>,
note_sink: Option<&mut dyn NoteSink>,
recall_source: Option<&dyn RecallSource>,
memory_source: Option<&dyn MemorySource>,
permission_gate: Option<&mut dyn PermissionGate>,
exec_floor: Option<&crate::caveats::Scope<String>>,
git_tool: Option<&dyn GitTool>,
crew_runner: Option<&dyn CrewRunner>,
scratchpad_store: Option<&dyn agentic::scratchpad::ScratchpadStore>,
code_search: Option<agentic::semantic::CodeSearch<'_>>,
where_is: Option<&crate::where_is::WhereIsIndex>,
experience_store: Option<&dyn agentic::experiential::ExperienceStore>,
step_ledger: Option<&dyn agentic::scheduled::StepLedger>,
tool_offload: bool,
spill_store: Option<&dyn SpillStore>,
persona_tools: Option<&[String]>,
) -> String {
let collab = ToolCollaborators {
build_check_cmd,
note_sink: note_sink.map(|s| &mut *s as &mut dyn NoteSink),
recall_source,
memory_source,
permission_gate: permission_gate.map(|g| &mut *g as &mut dyn PermissionGate),
exec_floor,
git_tool,
crew_runner,
scratchpad_store,
code_search,
where_is,
nav: None,
experience_store,
step_ledger,
spill_store,
persona_tools,
..Default::default()
};
execute_tool_with_collaborators(
name,
args,
workspace,
color,
tool_output_lines,
caveats,
mcp,
collab,
tool_offload,
PromptDisposition::Act,
None,
)
.await
.expect("legacy dispatch has no durable completion writer")
.expect("tool execution without a cancellation flag cannot be interrupted")
}
#[allow(clippy::too_many_arguments)]
pub async fn execute_tool_with_offload_and_prompt_and_artifacts(
name: &str,
args: &serde_json::Value,
workspace: &str,
color: bool,
tool_output_lines: usize,
caveats: &crate::caveats::Caveats,
mcp: &mut dyn McpTools,
build_check_cmd: Option<&str>,
note_sink: Option<&mut dyn NoteSink>,
recall_source: Option<&dyn RecallSource>,
memory_source: Option<&dyn MemorySource>,
prompt_context: Option<PromptReadContext<'_>>,
artifact_context: Option<ArtifactReadContext<'_>>,
artifact_sink: Option<&dyn agentic::artifact_read::PromptArtifactSink>,
permission_gate: Option<&mut dyn PermissionGate>,
exec_floor: Option<&crate::caveats::Scope<String>>,
git_tool: Option<&dyn GitTool>,
crew_runner: Option<&dyn CrewRunner>,
scratchpad_store: Option<&dyn agentic::scratchpad::ScratchpadStore>,
code_search: Option<agentic::semantic::CodeSearch<'_>>,
where_is: Option<&crate::where_is::WhereIsIndex>,
experience_store: Option<&dyn agentic::experiential::ExperienceStore>,
step_ledger: Option<&dyn agentic::scheduled::StepLedger>,
tool_offload: bool,
spill_store: Option<&dyn SpillStore>,
persona_tools: Option<&[String]>,
disposition: PromptDisposition,
) -> String {
let collab = ToolCollaborators {
build_check_cmd,
note_sink: note_sink.map(|s| &mut *s as &mut dyn NoteSink),
recall_source,
memory_source,
prompt_context,
artifact_context,
artifact_sink,
permission_gate: permission_gate.map(|g| &mut *g as &mut dyn PermissionGate),
exec_floor,
git_tool,
crew_runner,
scratchpad_store,
code_search,
where_is,
nav: None,
experience_store,
step_ledger,
spill_store,
persona_tools,
..Default::default()
};
execute_tool_with_collaborators(
name,
args,
workspace,
color,
tool_output_lines,
caveats,
mcp,
collab,
tool_offload,
disposition,
None,
)
.await
.expect("legacy dispatch has no durable completion writer")
.expect("tool execution without a cancellation flag cannot be interrupted")
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn execute_tool_with_collaborators(
name: &str,
args: &serde_json::Value,
workspace: &str,
color: bool,
tool_output_lines: usize,
caveats: &crate::caveats::Caveats,
mcp: &mut dyn McpTools,
collab: ToolCollaborators<'_, '_>,
tool_offload: bool,
disposition: PromptDisposition,
cancel: Option<&std::sync::atomic::AtomicBool>,
) -> anyhow::Result<Option<String>> {
let mut display = ToolDisplay::new(
std::io::stdout(),
color,
agentic::display::term_cols(),
agentic::display::spill_lines(),
agentic::display::spill_summary(),
);
if let Some(ref renderer) = collab.completed_spill_renderer {
display.set_completed_spill_renderer(renderer.clone());
}
execute_tool_with_display_cancellable(
&mut display,
name,
args,
workspace,
color,
tool_output_lines,
caveats,
mcp,
collab,
tool_offload,
disposition,
cancel,
)
.await
}
async fn wait_for_tool_cancellation(cancel: Option<&std::sync::atomic::AtomicBool>) {
match cancel {
None => std::future::pending::<()>().await,
Some(flag) => {
while !flag.load(std::sync::atomic::Ordering::Relaxed) {
tokio::time::sleep(std::time::Duration::from_millis(15)).await;
}
}
}
}
#[allow(clippy::too_many_arguments)]
pub(super) async fn execute_tool_with_display_cancellable<W: std::io::Write + Send>(
display: &mut ToolDisplay<W>,
name: &str,
args: &serde_json::Value,
workspace: &str,
color: bool,
tool_output_lines: usize,
caveats: &crate::caveats::Caveats,
mcp: &mut dyn McpTools,
collab: ToolCollaborators<'_, '_>,
tool_offload: bool,
disposition: PromptDisposition,
cancel: Option<&std::sync::atomic::AtomicBool>,
) -> anyhow::Result<Option<String>> {
let presentation_args = shell::command_args_with_default_cwd(
name,
args,
workspace,
collab.default_command_cwd,
collab.worktree_session,
)
.unwrap_or(std::borrow::Cow::Borrowed(args));
let (presentation_name, presentation_detail) = tool_presentation(
name,
presentation_args.as_ref(),
std::path::Path::new(workspace),
&caveats.fs_read,
);
display.call(&presentation_name, &presentation_detail);
let invocation = collab.invocation;
let execution = collab
.execution
.or_else(|| invocation.map(|call| call.execution_slot()));
let collab = ToolCollaborators {
execution,
..collab
};
let spill = collab.spill_store;
let result = {
let spinner = ToolSpinner::start(&presentation_name, color);
let collab = ToolCollaborators {
live_tool_output: spinner.wrap(collab.live_tool_output),
..collab
};
let execution = execute_tool_inner(
display,
name,
args,
workspace,
color,
tool_output_lines,
caveats,
mcp,
collab,
tool_offload,
disposition,
);
tokio::pin!(execution);
tokio::select! {
biased;
_ = wait_for_tool_cancellation(cancel) => None,
result = &mut execution => Some(result),
}
};
match result {
Some(result) => {
if let Some(invocation) = invocation {
if let Err(error) = invocation.observe(
&result,
execution.and_then(|slot| slot.get().copied()),
spill,
) {
display.result(&result);
display.result(&format!("error: tool completion failed: {error:#}"));
return Err(error);
}
}
display.result(&result);
Ok(Some(result))
}
None => {
display.drop_completed_spill_renderer();
let result = format!("error: {name} interrupted — tool cancelled before completion");
display.result(&result);
Ok(None)
}
}
}
#[cfg(test)]
mod stack_tests {
use super::*;
#[test]
fn publish_early_dispatch_future_has_bounded_inline_state() {
let args = serde_json::json!({});
let caveats = crate::Caveats::top();
let mut mcp = crate::agentic::mcp::NoMcp;
let future = execute_tool_with_collaborators(
"read_file",
&args,
".",
false,
20,
&caveats,
&mut mcp,
ToolCollaborators::default(),
false,
PromptDisposition::Act,
None,
);
let size = std::mem::size_of_val(&future);
assert!(size < 16 * 1024, "dispatch future occupies {size} bytes");
}
}