use super::super::{Resolved, refusal, seam, stop_signal};
use super::{DECLINED, Entry, FAILED, Invocation, NAME, OK};
use crate::prompt::Error;
use crate::prompt::tool::{ExecError, ToolCall, ToolExecutor, ToolOutcome};
use std::path::Path;
use std::sync::atomic::AtomicBool;
#[derive(Clone, Copy)]
pub(super) struct Ctx<'a> {
pub(super) executor: &'a dyn ToolExecutor,
pub(super) stop: &'a AtomicBool,
}
pub(super) struct Inner<'a> {
pub(super) outer_id: &'a str,
pub(super) k: usize,
pub(super) inv: &'a Invocation,
pub(super) step_dir_abs: &'a Path,
pub(super) conv_repo: &'a Path,
pub(super) conv_id: &'a str,
}
pub(super) enum Gated {
Declined(Entry),
Ready(String),
}
pub(super) fn gate(
inner: &Inner<'_>,
resolved: &Resolved<'_>,
stop: &AtomicBool,
) -> Result<Option<Gated>, Error> {
let inv = inner.inv;
if inv.name == NAME {
return Ok(Some(Gated::Declined(Entry {
name: inv.name.clone(),
status: DECLINED,
text: format!(
"{NAME:?} may not contain itself (depth 1): \
list the nested invocations in this envelope directly."
),
})));
}
if let Some(decline) = refusal(resolved.grant.role, resolved.grant.tools, &inv.name) {
return Ok(Some(Gated::Declined(Entry {
name: inv.name.clone(),
status: DECLINED,
text: decline,
})));
}
let inner_id = format!("{}-{}", inner.outer_id, inner.k);
match seam::adjudicate(
resolved.workflow.tool_control.as_ref(),
resolved.grant.role,
&inner_id,
&inv.name,
&inv.input,
inner.conv_repo,
inner.conv_id,
stop,
)? {
seam::Gate::Stopped => return Ok(None),
seam::Gate::Refuse(reason) => {
return Ok(Some(Gated::Declined(Entry {
name: inv.name.clone(),
status: DECLINED,
text: seam::refusal_text(&inv.name, &reason),
})));
}
seam::Gate::Hold(reason) => {
return Ok(Some(Gated::Declined(Entry {
name: inv.name.clone(),
status: DECLINED,
text: format!(
"the workflow's tool control held {:?} ({reason}), and a hold cannot \
park mid-envelope: re-issue this invocation as a top-level tool_use \
to have it reviewed (ARCH §3.3 Tool control).",
inv.name
),
})));
}
seam::Gate::Proceed => {}
}
Ok(Some(Gated::Ready(inner_id)))
}
pub(super) fn finish(
name: &str,
result: Result<ToolOutcome, ExecError>,
stop: &AtomicBool,
) -> Result<Option<Entry>, Error> {
match result {
Ok(outcome) => Ok(Some(Entry {
name: name.to_string(),
status: if outcome.is_error { FAILED } else { OK },
text: String::from_utf8_lossy(&outcome.content).into_owned(),
})),
Err(ExecError::KilledBySignal { .. }) if stop_signal::stopped(stop) => Ok(None),
Err(source) => Err(Error::ToolExec {
tool: name.to_string(),
source,
}),
}
}
pub(super) fn run_inner(
inner: &Inner<'_>,
resolved: &Resolved<'_>,
ctx: Ctx<'_>,
) -> Result<Option<Entry>, Error> {
let inner_id = match gate(inner, resolved, ctx.stop)? {
None => return Ok(None),
Some(Gated::Declined(entry)) => return Ok(Some(entry)),
Some(Gated::Ready(id)) => id,
};
let result = ctx.executor.execute(
ToolCall {
id: &inner_id,
name: &inner.inv.name,
input: &inner.inv.input,
},
inner.step_dir_abs,
ctx.stop,
resolved.workflow.tool_output,
);
finish(&inner.inv.name, result, ctx.stop)
}