use std::{future::Future, sync::Arc};
use mentra::{
ContentBlock, DelegationArtifact, DelegationEdge, DelegationKind, DelegationStatus,
SpawnedAgentStatus, SpawnedAgentSummary,
agent::{AgentEventTapGuard, DeferredItem},
runtime::{CommandOutput, RunOptions},
tool::{ToolContext, ToolResult},
};
use crate::runtime::agents::{AgentRow, AgentTools};
use super::{
SpawnTool,
background::{Children, JoinAtEnd, Kind, Settled},
child::ChildSpec,
depth::Entered,
teammate::Teammate,
};
pub(super) async fn command(
ctx: &ToolContext<'_>,
command: &str,
target: Option<&str>,
) -> ToolResult {
let cwd = ctx.resolve_working_directory(None)?;
let output = ctx
.execute_shell_command_on(
target.map(str::to_string),
command.to_string(),
None,
None,
cwd,
)
.await?;
narrate(ctx, &output);
read_back(output)
}
fn narrate(ctx: &ToolContext<'_>, output: &CommandOutput) {
for line in output.stdout.lines() {
ctx.emit_progress(format!("stdout: {line}"));
}
for line in output.stderr.lines() {
ctx.emit_progress(format!("stderr: {line}"));
}
}
fn read_back(output: CommandOutput) -> ToolResult {
if output.success() {
return Ok(if output.stdout.is_empty() {
output.stderr
} else {
output.stdout
});
}
Err(if !output.stderr.trim().is_empty() {
output.stderr
} else if !output.stdout.trim().is_empty() {
output.stdout
} else if output.timed_out {
"Command timed out after the configured limit".to_string()
} else {
format!(
"Command exited with status {}",
output
.status_code
.map_or_else(|| "unknown".to_string(), |code| code.to_string())
)
})
}
pub(super) struct Delegation {
child_id: String,
started: SpawnedAgentSummary,
prompt: String,
teammate: Option<String>,
edge: DelegationEdge,
background: bool,
pub(super) options: RunOptions,
_adopted: Option<AgentRow>,
_entered: Entered,
_usage_relay: AgentEventTapGuard,
}
impl Delegation {
fn open(
tool: &SpawnTool,
ctx: &mut ToolContext<'_>,
child: &mentra::agent::Agent,
prompt: &str,
depth: usize,
teammate: Option<&str>,
background: bool,
) -> Self {
let child_id = child.id().to_string();
let agents = tool.agents.as_ref();
let adopted = match teammate {
Some(_) => None,
None => agents.and_then(|agents| agents.adopt(&ctx.agent_id, &child_id)),
};
let entered = tool.depth.entered(&child_id, depth + 1);
let started = ctx.register_subagent(child);
let usage_relay = ctx.relay_subagent_usage(child);
let edge = DelegationEdge {
kind: DelegationKind::Subagent,
local_agent_id: ctx.agent_id.clone(),
remote_agent_id: child_id.clone(),
};
let frame = match teammate {
Some(name) => format!(
"<delegation-request agent=\"{}\" teammate=\"{name}\" model=\"{}\">\n{prompt}\n\
</delegation-request>",
started.name, started.model
),
None => format!(
"<delegation-request agent=\"{}\" model=\"{}\">\n{prompt}\n</delegation-request>",
started.name, started.model
),
};
let requested = ctx.record_delegation_request(
frame,
artifact_for(
&started,
teammate,
prompt,
DelegationStatus::Requested,
None,
),
Some(edge.clone()),
);
note(ctx, requested);
let mut options = ctx.child_run_options();
if let Some(agents) = agents {
options.round_strategy = Some(Arc::new(JoinAtEnd::new(
Arc::clone(agents),
child_id.clone(),
None,
)));
}
Self {
child_id,
started,
prompt: prompt.to_string(),
teammate: teammate.map(str::to_string),
edge,
background,
options,
_adopted: adopted,
_entered: entered,
_usage_relay: usage_relay,
}
}
fn settle(self, answer: Result<mentra::Message, mentra::error::RuntimeError>) -> Settled {
let outcome = answer
.as_ref()
.map(|message| said(message.text()))
.map_err(ToString::to_string);
let (status, told, summary) = match &outcome {
Ok(text) => (DelegationStatus::Finished, "finished", text),
Err(error) => (DelegationStatus::Failed, "failed", error),
};
let handle = if self.background {
format!(" id=\"{}\"", self.child_id)
} else {
String::new()
};
let frame = match &self.teammate {
Some(name) => format!(
"<delegation-result agent=\"{}\" teammate=\"{name}\"{handle} status=\"{told}\">\n\
{summary}\n</delegation-result>",
self.started.name
),
None => format!(
"<delegation-result agent=\"{}\"{handle} status=\"{told}\">\n{summary}\n\
</delegation-result>",
self.started.name
),
};
let items = vec![
DeferredItem::SubagentFinished {
id: self.child_id.clone(),
status: match &outcome {
Ok(_) => SpawnedAgentStatus::Finished,
Err(error) => SpawnedAgentStatus::Failed(error.clone()),
},
},
DeferredItem::DelegationResult {
content: frame,
delegation: artifact_for(
&self.started,
self.teammate.as_deref(),
&self.prompt,
status,
Some(summary.clone()),
),
edge: Some(self.edge.clone()),
},
];
Settled { outcome, items }
}
}
pub(super) fn apply(ctx: &mut ToolContext<'_>, settled: &Settled) {
for item in &settled.items {
match item {
DeferredItem::SubagentFinished { id, status } => {
ctx.finish_subagent(id, status.clone());
}
DeferredItem::DelegationResult {
content,
delegation,
edge,
} => {
let written =
ctx.record_delegation_result(content.clone(), delegation.clone(), edge.clone());
note(ctx, written);
}
_ => {}
}
}
}
pub(super) async fn delegate(
tool: &SpawnTool,
ctx: &mut ToolContext<'_>,
prompt: &str,
depth: usize,
spec: ChildSpec,
) -> ToolResult {
let mut child = child_for(tool, ctx, spec).await?;
let delegation = Delegation::open(tool, ctx, &child, prompt, depth, None, false);
let options = delegation.options.clone();
let answer = Box::pin(child.run(vec![ContentBlock::text(prompt)], options)).await;
let settled = delegation.settle(answer);
apply(ctx, &settled);
refresh_tasks(ctx)?;
settled
.outcome
.map_err(|error| format!("the delegated run failed: {error}"))
}
pub(super) async fn delegate_in_background(
tool: &SpawnTool,
ctx: &mut ToolContext<'_>,
prompt: &str,
depth: usize,
spec: ChildSpec,
) -> ToolResult {
let children = children_of(tool, &ctx.agent_id)?;
let mut child = child_for(tool, ctx, spec).await?;
let delegation = Delegation::open(tool, ctx, &child, prompt, depth, None, true);
let id = delegation.child_id.clone();
let options = delegation.options.clone();
let prompt = prompt.to_string();
children.start(
tool.task_scope.as_ref(),
id.clone(),
None,
Kind::Subagent,
ctx.deferred(),
async move {
let answer = Box::pin(child.run(vec![ContentBlock::text(prompt)], options)).await;
delegation.settle(answer)
},
);
Ok(started(Kind::Subagent, &id, None, false))
}
async fn child_for(
tool: &SpawnTool,
ctx: &ToolContext<'_>,
spec: ChildSpec,
) -> Result<mentra::agent::Agent, String> {
let inherited = tool
.agents
.as_ref()
.and_then(|agents| agents.of(&ctx.agent_id));
let mut child = spawn_child(ctx, spec, inherited.as_deref())
.await
.map_err(|error| format!("spawn could not start a subagent: {error}"))?;
teach(tool, &mut child, inherited.as_deref());
Ok(child)
}
pub(super) fn children_of(tool: &SpawnTool, agent_id: &str) -> Result<Children, String> {
tool.agents
.as_ref()
.and_then(|agents| agents.of(agent_id))
.map(|tools| tools.background.clone())
.ok_or_else(|| {
"spawn keeps no background work on this runtime; leave `background` out and wait \
for the answer"
.to_string()
})
}
async fn spawn_child(
ctx: &ToolContext<'_>,
spec: ChildSpec,
inherited: Option<&AgentTools>,
) -> Result<mentra::agent::Agent, mentra::error::RuntimeError> {
if spec.is_inherit() {
return ctx.spawn_subagent();
}
let ChildSpec {
roster,
model,
system,
} = spec;
let mut template = ctx.disposable_subagent_template();
if let Some(roster) = roster {
let mut profile = roster.into_profile();
if let Some(inherited) = inherited {
profile
.hidden_tools
.extend(inherited.hidden.iter().cloned());
}
template = template.with_tool_profile(profile);
}
if let Some(model) = model {
template = template.with_model(model);
}
if let Some(system) = system {
template = template.with_system(system);
}
ctx.spawn_subagent_from(template).await
}
fn teach(tool: &SpawnTool, child: &mut mentra::agent::Agent, inherited: Option<&AgentTools>) {
let Some(refresh) = tool.refresh.clone() else {
return;
};
let copy = tool.for_agent(child.id(), refresh);
let copy = match inherited.and_then(|row| row.remote.as_ref()) {
Some(remote) => copy.for_remote_agents(remote),
None => copy,
};
child.register_tool(copy);
}
pub(super) struct TeammateCall {
pub(super) name: String,
pub(super) depth: usize,
pub(super) existing: Option<Arc<Teammate>>,
pub(super) spec: ChildSpec,
pub(super) background: bool,
}
pub(super) async fn teammate(
tool: &SpawnTool,
ctx: &mut ToolContext<'_>,
prompt: &str,
call: TeammateCall,
) -> ToolResult {
let TeammateCall {
name,
depth,
existing,
spec,
background,
} = call;
let name = name.as_str();
let children = if background {
Some(children_of(tool, &ctx.agent_id)?)
} else {
None
};
let agents = tool.agents.as_ref();
let inherited = agents.and_then(|agents| agents.of(&ctx.agent_id));
let (teammate, made) = match existing {
Some(teammate) => (teammate, false),
None => {
let mut child = spawn_child(ctx, spec, inherited.as_deref())
.await
.map_err(|error| format!("spawn could not start a teammate: {error}"))?;
teach(tool, &mut child, inherited.as_deref());
let row = agents.and_then(|agents| agents.adopt(&ctx.agent_id, child.id()));
let teammate = Arc::new(Teammate::new(name, child, row));
if let Some(row) = &inherited {
row.teammates.keep(Arc::clone(&teammate));
}
if let Some(refresh) = &tool.refresh {
refresh();
}
(teammate, true)
}
};
let name = teammate.name().to_string();
let Some(mut child) = teammate.take_owned() else {
return Err(format!(
"`{name}` is still working on the last task you handed it, and takes one task at a \
time; wait for that answer before handing it another"
));
};
let child_id = teammate.id().to_string();
let delegation = Delegation::open(tool, ctx, &child, prompt, depth, Some(&name), background);
let options = delegation.options.clone();
if let Some(children) = children {
let prompt = prompt.to_string();
children.start(
tool.task_scope.as_ref(),
child_id.clone(),
Some(name.clone()),
Kind::Teammate,
ctx.deferred(),
async move {
let answer = Box::pin(child.run(vec![ContentBlock::text(prompt)], options)).await;
drop(child);
delegation.settle(answer)
},
);
return Ok(started(Kind::Teammate, &child_id, Some(&name), made));
}
let answer = Box::pin(child.run(vec![ContentBlock::text(prompt)], options)).await;
drop(child);
let settled = delegation.settle(answer);
apply(ctx, &settled);
refresh_tasks(ctx)?;
let headline = if made {
format!("Made teammate `{name}` (id `{child_id}`). It replied:")
} else {
format!("Teammate `{name}` (id `{child_id}`) replied:")
};
match settled.outcome {
Ok(text) => Ok(format!("{headline}\n\n{text}")),
Err(error) => Err(format!("the teammate `{name}`'s turn failed: {error}")),
}
}
pub(super) async fn wait(
tool: &SpawnTool,
ctx: &mut ToolContext<'_>,
ids: &[String],
any: bool,
) -> ToolResult {
let children = children_of(tool, &ctx.agent_id)?;
let mut claimed = Vec::with_capacity(ids.len());
for id in ids {
match children.claim(id) {
Ok(handle) => claimed.push(handle),
Err(why) => {
let mut collected = Vec::new();
for handle in claimed {
if let Some(done) = children.release(handle) {
collected.push(done.settled().await);
}
}
let report = record(ctx, &children, collected)?;
return Err(if report.is_empty() {
why
} else {
format!("{why}\n\nMeanwhile:\n\n{report}")
});
}
}
}
let mut collected = Vec::new();
if any && claimed.len() > 1 {
let (index, first) = first_finished(&mut claimed).await;
let winner = claimed.swap_remove(index);
collected.push(winner.finished(first));
for loser in claimed {
if let Some(done) = children.release(loser) {
collected.push(done.settled().await);
}
}
} else {
for handle in claimed {
collected.push(handle.settled().await);
}
}
let single = collected.len() == 1 && !any;
let failed = collected
.iter()
.filter(|(_, _, _, settled)| settled.outcome.is_err())
.count();
let mut report = record(ctx, &children, collected)?;
let running = children.listed();
if !running.is_empty() {
report.push_str("\n\nStill running: ");
report.push_str(&super::describe::listed(
&running
.into_iter()
.map(|(id, name, _)| name.unwrap_or(id))
.collect::<Vec<_>>(),
));
}
if single && failed == 1 {
return Err(report);
}
Ok(report)
}
fn record(
ctx: &mut ToolContext<'_>,
children: &Children,
collected: Vec<(String, Option<String>, Kind, Settled)>,
) -> Result<String, String> {
let mut lines = Vec::with_capacity(collected.len());
for (id, name, kind, settled) in &collected {
apply(ctx, settled);
children.finish(id);
let who = match name {
Some(name) => format!("{} `{name}` (id `{id}`)", kind.noun()),
None => format!("{} `{id}`", kind.noun()),
};
lines.push(match &settled.outcome {
Ok(text) => format!("{who} finished:\n\n{text}"),
Err(error) => format!("{who} failed: {error}"),
});
}
if !collected.is_empty() {
refresh_tasks(ctx)?;
}
Ok(lines.join("\n\n"))
}
impl super::background::Claimed {
fn finished(
self,
result: Result<Settled, tokio::task::JoinError>,
) -> (String, Option<String>, Kind, Settled) {
let settled = result.unwrap_or_else(|error| Settled {
outcome: Err(format!("the delegated run stopped unexpectedly: {error}")),
items: Vec::new(),
});
(self.id, self.name, self.kind, settled)
}
}
async fn first_finished(
claimed: &mut [super::background::Claimed],
) -> (usize, Result<Settled, tokio::task::JoinError>) {
std::future::poll_fn(|cx| {
for (index, handle) in claimed.iter_mut().enumerate() {
if let std::task::Poll::Ready(result) = std::pin::Pin::new(&mut handle.task).poll(cx) {
return std::task::Poll::Ready((index, result));
}
}
std::task::Poll::Pending
})
.await
}
pub(super) fn started(kind: Kind, id: &str, name: Option<&str>, made: bool) -> String {
let (who, handle) = match (kind, name) {
(Kind::Teammate, Some(name)) if made => (
format!("Made teammate `{name}` (id `{id}`); it"),
format!("`@{name}` alone, or `wait: [\"{name}\"]`"),
),
(Kind::Teammate, Some(name)) => (
format!("Teammate `{name}` (id `{id}`)"),
format!("`@{name}` alone, or `wait: [\"{name}\"]`"),
),
(Kind::Remote, Some(name)) => (
format!("Remote agent `{name}`"),
format!("`@{name}` alone, or `wait: [\"{name}\"]`"),
),
_ => (
format!("Subagent `{id}`"),
format!("`wait: [\"{id}\"]`, or `@{id}` alone"),
),
};
format!(
"{who} is working on it in the background. Its answer arrives on its own when it is \
done, as a delegation result; {handle} collects it sooner. Anything still running when \
you finish is waited for before your answer is final."
)
}
fn refresh_tasks(ctx: &mut ToolContext<'_>) -> Result<(), String> {
ctx.refresh_tasks()
.map_err(|error| format!("the delegated run finished but its tasks did not load: {error}"))
}
fn said(text: String) -> String {
if text.trim().is_empty() {
return "The subagent finished without saying anything.".to_string();
}
text
}
fn artifact_for(
child: &SpawnedAgentSummary,
teammate: Option<&str>,
prompt: &str,
status: DelegationStatus,
result_summary: Option<String>,
) -> DelegationArtifact {
DelegationArtifact {
kind: DelegationKind::Subagent,
agent_id: child.id.clone(),
agent_name: child.name.clone(),
role: Some(match teammate {
Some(name) => format!("teammate:{name}"),
None => "subagent".to_string(),
}),
status,
task_summary: prompt.to_string(),
result_summary,
artifacts: Vec::new(),
}
}
fn note(ctx: &ToolContext<'_>, written: Result<(), mentra::error::RuntimeError>) {
if let Err(error) = written {
ctx.emit_progress(format!("the delegation was not recorded: {error}"));
}
}