use mentra::{
ContentBlock, DelegationArtifact, DelegationEdge, DelegationKind, DelegationStatus,
SpawnedAgentStatus, SpawnedAgentSummary,
runtime::CommandOutput,
tool::{ToolContext, ToolResult},
};
use std::sync::Arc;
use crate::runtime::agents::AgentTools;
use super::{SpawnTool, child::ChildSpec, 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) async fn delegate(
tool: &SpawnTool,
ctx: &mut ToolContext<'_>,
prompt: &str,
depth: usize,
spec: ChildSpec,
) -> ToolResult {
let ledger = &tool.depth;
let agents = tool.agents.as_ref();
let inherited = agents.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}"))?;
let child_id = child.id().to_string();
teach(tool, &mut child, inherited.as_deref());
let _adopted = agents.and_then(|agents| agents.adopt(&ctx.agent_id, &child_id));
let _entered = ledger.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 requested = ctx.record_delegation_request(
format!(
"<delegation-request agent=\"{}\" model=\"{}\">\n{prompt}\n</delegation-request>",
started.name, started.model
),
artifact(&started, prompt, DelegationStatus::Requested, None),
Some(edge.clone()),
);
note(ctx, requested);
let options = ctx.child_run_options();
let answer = Box::pin(child.run(vec![ContentBlock::text(prompt)], options)).await;
let outcome = answer
.as_ref()
.map(|message| said(message.text()))
.map_err(ToString::to_string);
ctx.finish_subagent(
&child_id,
match &outcome {
Ok(_) => SpawnedAgentStatus::Finished,
Err(error) => SpawnedAgentStatus::Failed(error.clone()),
},
);
let (status, told, summary) = match &outcome {
Ok(text) => (DelegationStatus::Finished, "finished", text),
Err(error) => (DelegationStatus::Failed, "failed", error),
};
let returned = ctx.record_delegation_result(
format!(
"<delegation-result agent=\"{}\" status=\"{told}\">\n{summary}\n</delegation-result>",
started.name
),
artifact(&started, prompt, status, Some(summary.clone())),
Some(edge),
);
note(ctx, returned);
ctx.refresh_tasks().map_err(|error| {
format!("the delegated run finished but its tasks did not load: {error}")
})?;
outcome.map_err(|error| format!("the delegated run failed: {error}"))
}
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) async fn teammate(
tool: &SpawnTool,
ctx: &mut ToolContext<'_>,
name: &str,
prompt: &str,
depth: usize,
existing: Option<Arc<Teammate>>,
spec: ChildSpec,
) -> ToolResult {
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();
let Some(mut child) = teammate.take() 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 _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 requested = ctx.record_delegation_request(
format!(
"<delegation-request agent=\"{}\" teammate=\"{name}\" model=\"{}\">\n{prompt}\n\
</delegation-request>",
started.name, started.model
),
teammate_artifact(&started, name, prompt, DelegationStatus::Requested, None),
Some(edge.clone()),
);
note(ctx, requested);
let options = ctx.child_run_options();
let answer = Box::pin(child.run(vec![ContentBlock::text(prompt)], options)).await;
drop(child);
let outcome = answer
.as_ref()
.map(|message| said(message.text()))
.map_err(ToString::to_string);
ctx.finish_subagent(
&child_id,
match &outcome {
Ok(_) => SpawnedAgentStatus::Finished,
Err(error) => SpawnedAgentStatus::Failed(error.clone()),
},
);
let (status, told, summary) = match &outcome {
Ok(text) => (DelegationStatus::Finished, "finished", text),
Err(error) => (DelegationStatus::Failed, "failed", error),
};
let returned = ctx.record_delegation_result(
format!(
"<delegation-result agent=\"{}\" teammate=\"{name}\" status=\"{told}\">\n{summary}\n\
</delegation-result>",
started.name
),
teammate_artifact(&started, name, prompt, status, Some(summary.clone())),
Some(edge),
);
note(ctx, returned);
ctx.refresh_tasks().map_err(|error| {
format!("the delegated run finished but its tasks did not load: {error}")
})?;
let headline = if made {
format!("Made teammate `{name}` (id `{child_id}`). It replied:")
} else {
format!("Teammate `{name}` (id `{child_id}`) replied:")
};
match outcome {
Ok(text) => Ok(format!("{headline}\n\n{text}")),
Err(error) => Err(format!("the teammate `{name}`'s turn failed: {error}")),
}
}
fn teammate_artifact(
child: &SpawnedAgentSummary,
name: &str,
prompt: &str,
status: DelegationStatus,
result_summary: Option<String>,
) -> DelegationArtifact {
DelegationArtifact {
role: Some(format!("teammate:{name}")),
..artifact(child, prompt, status, result_summary)
}
}
fn said(text: String) -> String {
if text.trim().is_empty() {
return "The subagent finished without saying anything.".to_string();
}
text
}
fn artifact(
child: &SpawnedAgentSummary,
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("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}"));
}
}