use std::{borrow::Cow, collections::HashSet, sync::Arc};
use crate::{
ModelInfo, Role,
error::RuntimeError,
provider::Provider,
runtime::{RuntimeIntrinsicTool, handle::RuntimeHandle},
};
use super::{
Agent, AgentConfig, AgentSpawnOptions, SpawnedAgentStatus, SpawnedAgentSummary,
TeammateIdentity, ToolProfile,
};
const SUBAGENT_MAX_ROUNDS: usize = 30;
const SUBAGENT_SYSTEM_PROMPT: &str = "You are a subagent working for another agent. Solve the delegated task, use tools when helpful, and finish with a concise final answer for the parent agent.";
#[derive(Clone)]
#[must_use = "a template does nothing on its own -- spawn it with spawn_subagent_from, \
or the override methods called on it are silently discarded"]
pub struct DisposableSubagentTemplate {
runtime: RuntimeHandle,
source_agent_id: String,
model: String,
context_window: Option<usize>,
parent_name: String,
config: AgentConfig,
provider: Arc<dyn Provider>,
hidden_tools: HashSet<String>,
teammate_identity: Option<TeammateIdentity>,
model_override: Option<ModelInfo>,
}
impl DisposableSubagentTemplate {
pub(crate) fn from_agent(agent: &Agent) -> Self {
Self {
runtime: agent.runtime.clone(),
source_agent_id: agent.id.clone(),
model: agent.model.clone(),
context_window: agent.context_window,
parent_name: agent.name.clone(),
config: agent.config.clone(),
provider: Arc::clone(&agent.provider),
hidden_tools: agent.hidden_tools.clone(),
teammate_identity: agent.teammate_identity.clone(),
model_override: None,
}
}
pub(crate) fn verify_source(
&self,
receiver_agent_id: &str,
receiver_runtime: &RuntimeHandle,
) -> Result<(), RuntimeError> {
if self.source_agent_id == receiver_agent_id
&& self.runtime.same_runtime_as(receiver_runtime)
{
return Ok(());
}
Err(RuntimeError::SubagentTemplateMismatch {
template_source: self.source_agent_id.clone(),
receiver: receiver_agent_id.to_string(),
})
}
#[must_use = "with_tool_profile returns a new template rather than mutating in place; \
a discarded return value leaves the override applied to nothing"]
pub fn with_tool_profile(mut self, tool_profile: ToolProfile) -> Self {
self.config.tool_profile = tool_profile;
self
}
#[must_use = "with_model returns a new template rather than mutating in place; \
a discarded return value leaves the override applied to nothing"]
pub fn with_model(mut self, model: ModelInfo) -> Self {
self.model_override = Some(model);
self
}
#[must_use = "with_system returns a new template rather than mutating in place; \
a discarded return value leaves the override applied to nothing"]
pub fn with_system(mut self, system: impl Into<String>) -> Self {
self.config.system = Some(system.into());
self
}
pub(crate) fn spawn(&self) -> Result<Agent, RuntimeError> {
self.build_agent(self.model_override.clone())
}
pub(crate) async fn spawn_from(&self) -> Result<Agent, RuntimeError> {
let model_override = match self.model_override.clone() {
Some(model) if model.context_window.is_none() => {
Some(self.listed_context_window(model).await)
}
other => other,
};
self.build_agent(model_override)
}
async fn listed_context_window(&self, mut model: ModelInfo) -> ModelInfo {
if let Some(provider) = self.runtime.get_provider(Some(&model.provider))
&& let Ok(listed_models) = provider.list_models().await
&& let Some(listed) = listed_models
.into_iter()
.find(|listed| listed.id == model.id)
{
model.context_window = listed.context_window;
}
model
}
fn build_agent(&self, model_override: Option<ModelInfo>) -> Result<Agent, RuntimeError> {
let mut hidden_tools = self.hidden_tools.clone();
hidden_tools.insert(RuntimeIntrinsicTool::Task.to_string());
let mut config = self.config.clone();
config.system = Some(build_subagent_system_prompt(
self.config.system.as_deref().map(Cow::Borrowed),
));
let (model, context_window, provider) = match model_override {
Some(model) => {
let provider = self
.runtime
.get_provider(Some(&model.provider))
.ok_or_else(|| RuntimeError::ProviderNotFound(Some(model.provider.clone())))?;
(model.id, model.context_window, provider)
}
None => (
self.model.clone(),
self.context_window,
Arc::clone(&self.provider),
),
};
Agent::new(
self.runtime.clone(),
model,
context_window,
format!("{}::task", self.parent_name),
config,
provider,
AgentSpawnOptions {
hidden_tools,
max_rounds: Some(SUBAGENT_MAX_ROUNDS),
teammate_identity: self.teammate_identity.clone(),
},
)
}
}
impl Agent {
pub(crate) fn spawn_subagent(&self) -> Result<Self, RuntimeError> {
self.disposable_subagent_template().spawn()
}
pub(crate) async fn spawn_subagent_from(
&self,
template: DisposableSubagentTemplate,
) -> Result<Self, RuntimeError> {
template.verify_source(&self.id, &self.runtime)?;
template.spawn_from().await
}
pub(crate) fn disposable_subagent_template(&self) -> DisposableSubagentTemplate {
DisposableSubagentTemplate::from_agent(self)
}
pub(crate) fn register_subagent(&mut self, agent: &Agent) -> SpawnedAgentSummary {
let summary = SpawnedAgentSummary {
id: agent.id.clone(),
name: agent.name.clone(),
model: agent.model.clone(),
status: SpawnedAgentStatus::Running,
};
let summary_for_snapshot = summary.clone();
self.mutate_snapshot(|snapshot| {
snapshot.subagents.push(summary_for_snapshot);
});
summary
}
pub(crate) fn finish_subagent(
&mut self,
id: &str,
status: SpawnedAgentStatus,
) -> Option<SpawnedAgentSummary> {
let mut finished = None;
self.mutate_snapshot(|snapshot| {
if let Some(summary) = snapshot.subagents.iter_mut().find(|agent| agent.id == id) {
summary.status = status;
finished = Some(summary.clone());
}
});
finished
}
pub(crate) fn final_text_summary(&self) -> String {
let Some(message) = self.last_message() else {
return "(no summary)".to_string();
};
if message.role != Role::Assistant {
return "(no summary)".to_string();
}
let text = message.text();
if text.is_empty() {
"(no summary)".to_string()
} else {
text
}
}
}
pub(super) fn build_subagent_system_prompt(base: Option<Cow<'_, str>>) -> String {
match base {
Some(system) => format!("{system}\n\n{SUBAGENT_SYSTEM_PROMPT}"),
None => SUBAGENT_SYSTEM_PROMPT.to_string(),
}
}