use super::{Capability, CapabilityStatus};
use crate::platform_store::PlatformStore;
use crate::tool_types::ToolHints;
use crate::tools::{Tool, ToolExecutionResult};
use crate::traits::ToolContext;
use async_trait::async_trait;
use serde_json::{Value, json};
pub struct SubagentCapability;
impl Capability for SubagentCapability {
fn id(&self) -> &str {
"subagents"
}
fn name(&self) -> &str {
"Subagents"
}
fn description(&self) -> &str {
"Spawn and manage subagents for parallel task execution in isolated context windows."
}
fn status(&self) -> CapabilityStatus {
CapabilityStatus::Available
}
fn icon(&self) -> Option<&str> {
Some("git-branch")
}
fn category(&self) -> Option<&str> {
Some("Orchestration")
}
fn features(&self) -> Vec<&'static str> {
vec!["subagents"]
}
fn system_prompt_addition(&self) -> Option<&str> {
Some(SUBAGENT_SYSTEM_PROMPT)
}
fn tools(&self) -> Vec<Box<dyn Tool>> {
vec![
Box::new(SpawnSubagentTool),
Box::new(GetSubagentsTool),
Box::new(MessageSubagentTool),
]
}
}
const SUBAGENT_SYSTEM_PROMPT: &str = "Spawn subagents only for independent workstreams that benefit from parallelism or a separate context window. Do not delegate immediate sequential steps you can complete directly. No nested subagents. Use blueprints for specialist agents with their own tools and model.";
fn get_platform_store(context: &ToolContext) -> Result<&dyn PlatformStore, ToolExecutionResult> {
context
.platform_store
.as_ref()
.map(|s| s.as_ref())
.ok_or_else(|| {
ToolExecutionResult::tool_error(
"Subagent tools require platform_store context (not available in this environment)",
)
})
}
fn get_session_store(
context: &ToolContext,
) -> Result<&dyn crate::traits::SessionStore, ToolExecutionResult> {
context
.session_store
.as_ref()
.map(|s| s.as_ref())
.ok_or_else(|| {
ToolExecutionResult::tool_error("Subagent tools require session_store context")
})
}
fn require_str<'a>(args: &'a Value, field: &str) -> Result<&'a str, ToolExecutionResult> {
args.get(field)
.and_then(|v| v.as_str())
.filter(|s| !s.trim().is_empty())
.ok_or_else(|| {
ToolExecutionResult::tool_error(format!("Missing required parameter: {field}"))
})
}
fn last_agent_message(messages: &[crate::platform_store::PlatformMessage]) -> Option<String> {
messages
.iter()
.rfind(|m| m.role == "agent" || m.role == "assistant")
.map(|m| m.content.clone())
}
fn find_child_session<'a>(
sessions: &'a [crate::session::Session],
parent_id: crate::typed_id::SessionId,
name_or_id: &str,
) -> Option<&'a crate::session::Session> {
sessions
.iter()
.filter(|s| s.parent_session_id == Some(parent_id))
.find(|s| {
s.subagent_name
.as_ref()
.is_some_and(|n| n.eq_ignore_ascii_case(name_or_id))
|| s.id.to_string() == name_or_id
})
}
pub struct SpawnSubagentTool;
#[async_trait]
impl Tool for SpawnSubagentTool {
fn name(&self) -> &str {
"spawn_subagent"
}
fn display_name(&self) -> Option<&str> {
Some("Spawn Subagent")
}
fn description(&self) -> &str {
"Spawn a named subagent to handle a specific task in its own context window. Use `blueprint` to spawn a specialist agent with its own tools and model."
}
fn parameters_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Human-readable name for the subagent (e.g. 'Test Runner', 'Auth Explorer'). Must be unique within this session."
},
"task": {
"type": "string",
"description": "Task description — what the subagent should do."
},
"blueprint": {
"type": "string",
"description": "Blueprint ID to spawn a specialist agent with its own tools and model. Omit to inherit parent's configuration."
},
"config": {
"type": "object",
"description": "Blueprint-specific configuration. Only valid when `blueprint` is set. Validated against the blueprint's config schema."
}
},
"required": ["name", "task"],
"additionalProperties": false
})
}
fn hints(&self) -> ToolHints {
ToolHints::default().with_long_running(true)
}
async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
ToolExecutionResult::tool_error(
"spawn_subagent requires context. This tool must be executed with session context.",
)
}
async fn execute_with_context(
&self,
arguments: Value,
context: &ToolContext,
) -> ToolExecutionResult {
let store = match get_platform_store(context) {
Ok(s) => s,
Err(e) => return e,
};
let session_store = match get_session_store(context) {
Ok(s) => s,
Err(e) => return e,
};
let name = match require_str(&arguments, "name") {
Ok(s) => s.trim().to_string(),
Err(e) => return e,
};
let task = match require_str(&arguments, "task") {
Ok(s) => s.to_string(),
Err(e) => return e,
};
let blueprint_param = arguments
.get("blueprint")
.and_then(|v| v.as_str())
.filter(|s| !s.trim().is_empty())
.map(|s| s.to_string());
let config_param = arguments.get("config").filter(|v| !v.is_null()).cloned();
if config_param.is_some() && blueprint_param.is_none() {
return ToolExecutionResult::tool_error(
"The `config` parameter is only valid when `blueprint` is set.",
);
}
let parent_session = match session_store.get_session(context.session_id).await {
Ok(Some(s)) => s,
Ok(None) => return ToolExecutionResult::tool_error("Current session not found"),
Err(e) => return ToolExecutionResult::internal_error(e),
};
if parent_session.parent_session_id.is_some() {
return ToolExecutionResult::tool_error(
"Subagents cannot spawn other subagents (nesting not allowed).",
);
}
if let Some(ref bp_id) = blueprint_param {
let Some(ref registry) = context.capability_registry else {
return ToolExecutionResult::tool_error(
"Blueprint support requires capability_registry context.",
);
};
let Some((blueprint_capability_id, blueprint)) =
registry.blueprint_with_capability(bp_id)
else {
return ToolExecutionResult::tool_error(format!(
"Unknown blueprint: \"{bp_id}\". Check available blueprints."
));
};
if let Some(ref schema) = blueprint.config_schema
&& config_param.is_none()
&& schema
.get("required")
.is_some_and(|r| r.as_array().is_some_and(|arr| !arr.is_empty()))
{
return ToolExecutionResult::tool_error(format!(
"Blueprint \"{bp_id}\" requires config. Schema: {}",
serde_json::to_string_pretty(schema).unwrap_or_default()
));
}
let allowed_capability_ids = if let Some(agent_id) = parent_session.agent_id {
match store.get_agent_by_id(agent_id).await {
Ok(Some(agent)) => agent
.capabilities
.iter()
.map(|c| c.capability_id().to_string())
.collect::<Vec<_>>(),
Ok(None) => vec![],
Err(e) => return ToolExecutionResult::internal_error(e),
}
} else {
match store.get_harness(parent_session.harness_id).await {
Ok(Some(harness)) => harness
.capabilities
.iter()
.map(|c| c.capability_id().to_string())
.collect::<Vec<_>>(),
Ok(None) => vec![],
Err(e) => return ToolExecutionResult::internal_error(e),
}
};
if !allowed_capability_ids
.iter()
.any(|capability_id| capability_id == &blueprint_capability_id)
{
return ToolExecutionResult::tool_error(format!(
"Blueprint \"{bp_id}\" is not enabled for this session."
));
}
}
let child_session = match store
.create_session(
parent_session.harness_id,
if blueprint_param.is_some() {
None } else {
parent_session.agent_id
},
Some(&name),
parent_session.locale.as_deref(),
blueprint_param.as_deref(),
config_param.as_ref(),
)
.await
{
Ok(s) => s,
Err(e) => return ToolExecutionResult::internal_error(e),
};
let child_session = match store
.set_subagent_metadata(
child_session.id,
context.session_id,
&name,
&task,
crate::session::SubagentStatus::Running,
)
.await
{
Ok(s) => s,
Err(e) => return ToolExecutionResult::internal_error(e),
};
if let Some(ref registry) = context.session_resource_registry {
let _ = registry
.register(crate::session_resource::RegisterSessionResource {
session_id: context.session_id,
resource_id: child_session.id.to_string(),
kind: "subagent".to_string(),
display_name: name.clone(),
status: crate::session_resource::SessionResourceStatus::Active,
metadata: json!({
"task": &task,
"blueprint_id": &blueprint_param,
}),
})
.await;
}
if let Err(e) = store.send_message(child_session.id, &task).await {
return ToolExecutionResult::internal_error(e);
}
let status = match store.wait_for_idle(child_session.id, Some(300)).await {
Ok(s) => s,
Err(e) => {
if let Some(ref registry) = context.session_resource_registry {
let _ = registry
.update_status(
context.session_id,
&child_session.id.to_string(),
crate::session_resource::SessionResourceStatus::Failed,
)
.await;
}
return ToolExecutionResult::success(json!({
"subagent_id": child_session.id.to_string(),
"name": name,
"status": "failed",
"error": e.to_string(),
"blueprint": blueprint_param,
}));
}
};
let messages = match store.get_messages(child_session.id, Some(5)).await {
Ok(m) => m,
Err(e) => return ToolExecutionResult::internal_error(e),
};
let result_text = last_agent_message(&messages)
.unwrap_or_else(|| format!("Subagent completed with status: {status}"));
if let Some(ref registry) = context.session_resource_registry {
let terminal = if status == "error" {
crate::session_resource::SessionResourceStatus::Failed
} else {
crate::session_resource::SessionResourceStatus::Completed
};
let _ = registry
.update_status(context.session_id, &child_session.id.to_string(), terminal)
.await;
}
ToolExecutionResult::success(json!({
"subagent_id": child_session.id.to_string(),
"name": name,
"status": status,
"result": result_text,
"blueprint": blueprint_param,
}))
}
fn requires_context(&self) -> bool {
true
}
}
pub struct GetSubagentsTool;
#[async_trait]
impl Tool for GetSubagentsTool {
fn name(&self) -> &str {
"get_subagents"
}
fn display_name(&self) -> Option<&str> {
Some("Get Subagents")
}
fn description(&self) -> &str {
"List all subagents or get detailed status of a specific one (by name or ID)."
}
fn parameters_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"name_or_id": {
"type": "string",
"description": "Subagent name or session ID for detailed view. Omit to list all."
},
"status_filter": {
"type": "string",
"enum": ["all", "running", "completed", "failed"],
"description": "Filter by status when listing all subagents."
}
},
"additionalProperties": false
})
}
fn hints(&self) -> ToolHints {
ToolHints::default()
.with_readonly(true)
.with_idempotent(true)
}
async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
ToolExecutionResult::tool_error("get_subagents requires context.")
}
async fn execute_with_context(
&self,
arguments: Value,
context: &ToolContext,
) -> ToolExecutionResult {
let store = match get_platform_store(context) {
Ok(s) => s,
Err(e) => return e,
};
let all_sessions = match store.list_sessions(Some(100), None).await {
Ok(s) => s,
Err(e) => return ToolExecutionResult::internal_error(e),
};
let name_or_id = arguments
.get("name_or_id")
.and_then(|v| v.as_str())
.filter(|s| !s.trim().is_empty());
if let Some(query) = name_or_id {
let found = find_child_session(&all_sessions, context.session_id, query);
match found {
Some(child) => {
let messages = store
.get_messages(child.id, Some(10))
.await
.unwrap_or_default();
let last_response = last_agent_message(&messages);
ToolExecutionResult::success(json!({
"subagent_id": child.id.to_string(),
"name": child.subagent_name,
"task": child.subagent_task,
"status": child.subagent_status.as_ref().map(|s| s.to_string())
.unwrap_or_else(|| child.status.to_string()),
"session_status": child.status.to_string(),
"created_at": child.created_at.to_rfc3339(),
"result": last_response,
"blueprint_id": child.blueprint_id,
}))
}
None => ToolExecutionResult::tool_error(format!(
"No subagent found with name or ID: {query}"
)),
}
} else {
let status_filter = arguments.get("status_filter").and_then(|v| v.as_str());
let filtered: Vec<_> = all_sessions
.iter()
.filter(|s| s.parent_session_id == Some(context.session_id))
.filter(|s| {
if let Some(filter) = status_filter {
if filter == "all" {
return true;
}
s.subagent_status
.as_ref()
.is_some_and(|st| st.to_string() == filter)
} else {
true
}
})
.map(|s| {
json!({
"subagent_id": s.id.to_string(),
"name": s.subagent_name,
"task": s.subagent_task,
"status": s.subagent_status.as_ref().map(|st| st.to_string())
.unwrap_or_else(|| s.status.to_string()),
"created_at": s.created_at.to_rfc3339(),
"blueprint_id": s.blueprint_id,
})
})
.collect();
ToolExecutionResult::success(json!({
"subagents": filtered,
"count": filtered.len(),
}))
}
}
fn requires_context(&self) -> bool {
true
}
}
pub struct MessageSubagentTool;
#[async_trait]
impl Tool for MessageSubagentTool {
fn name(&self) -> &str {
"message_subagent"
}
fn display_name(&self) -> Option<&str> {
Some("Message Subagent")
}
fn description(&self) -> &str {
"Send a message to a subagent by name or ID. Steers running subagents, resumes completed/failed ones."
}
fn parameters_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"name_or_id": {
"type": "string",
"description": "Subagent name or session ID."
},
"message": {
"type": "string",
"description": "Message to send to the subagent."
},
"cancel": {
"type": "boolean",
"description": "If true, deliver the message then gracefully stop the subagent.",
"default": false
}
},
"required": ["name_or_id", "message"],
"additionalProperties": false
})
}
fn hints(&self) -> ToolHints {
ToolHints::default().with_long_running(true)
}
async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
ToolExecutionResult::tool_error("message_subagent requires context.")
}
async fn execute_with_context(
&self,
arguments: Value,
context: &ToolContext,
) -> ToolExecutionResult {
let store = match get_platform_store(context) {
Ok(s) => s,
Err(e) => return e,
};
let name_or_id = match require_str(&arguments, "name_or_id") {
Ok(s) => s.to_string(),
Err(e) => return e,
};
let message = match require_str(&arguments, "message") {
Ok(s) => s.to_string(),
Err(e) => return e,
};
let cancel = arguments
.get("cancel")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let all_sessions = match store.list_sessions(Some(100), None).await {
Ok(s) => s,
Err(e) => return ToolExecutionResult::internal_error(e),
};
let child = match find_child_session(&all_sessions, context.session_id, &name_or_id) {
Some(c) => c,
None => {
return ToolExecutionResult::tool_error(format!(
"No subagent found with name or ID: {name_or_id}"
));
}
};
let child_id = child.id;
if let Err(e) = store.send_message(child_id, &message).await {
return ToolExecutionResult::internal_error(e);
}
if cancel {
return ToolExecutionResult::success(json!({
"subagent_id": child_id.to_string(),
"name": child.subagent_name,
"delivered": true,
"cancel_requested": true,
"note": "Message delivered. Cancellation will take effect after current turn.",
}));
}
let status = match store.wait_for_idle(child_id, Some(300)).await {
Ok(s) => s,
Err(e) => {
return ToolExecutionResult::success(json!({
"subagent_id": child_id.to_string(),
"name": child.subagent_name,
"delivered": true,
"status": "error",
"error": e.to_string(),
}));
}
};
let messages = match store.get_messages(child_id, Some(5)).await {
Ok(m) => m,
Err(e) => return ToolExecutionResult::internal_error(e),
};
ToolExecutionResult::success(json!({
"subagent_id": child_id.to_string(),
"name": child.subagent_name,
"delivered": true,
"status": status,
"result": last_agent_message(&messages),
}))
}
fn requires_context(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Tool;
#[test]
fn capability_basics() {
let cap = SubagentCapability;
assert_eq!(cap.id(), "subagents");
assert_eq!(cap.tools().len(), 3);
assert!(cap.system_prompt_addition().is_some());
assert_eq!(cap.features(), vec!["subagents"]);
}
#[test]
fn tool_names() {
let cap = SubagentCapability;
let tools = cap.tools();
let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
assert_eq!(
names,
vec!["spawn_subagent", "get_subagents", "message_subagent"]
);
}
#[test]
fn spawn_subagent_schema_has_required_fields() {
let tool = SpawnSubagentTool;
let schema = tool.parameters_schema();
let required = schema["required"].as_array().unwrap();
assert!(required.contains(&json!("name")));
assert!(required.contains(&json!("task")));
}
#[test]
fn spawn_subagent_schema_has_blueprint_fields() {
let tool = SpawnSubagentTool;
let schema = tool.parameters_schema();
let props = schema["properties"].as_object().unwrap();
assert!(props.contains_key("blueprint"));
assert!(props.contains_key("config"));
let required = schema["required"].as_array().unwrap();
assert!(!required.contains(&json!("blueprint")));
assert!(!required.contains(&json!("config")));
}
#[tokio::test]
async fn spawn_subagent_without_context_returns_error() {
let tool = SpawnSubagentTool;
let result = tool.execute(json!({"name": "Test", "task": "test"})).await;
assert!(matches!(result, ToolExecutionResult::ToolError(_)));
}
#[tokio::test]
async fn get_subagents_without_context_returns_error() {
let tool = GetSubagentsTool;
let result = tool.execute(json!({})).await;
assert!(matches!(result, ToolExecutionResult::ToolError(_)));
}
#[tokio::test]
async fn message_subagent_without_context_returns_error() {
let tool = MessageSubagentTool;
let result = tool
.execute(json!({"name_or_id": "Test", "message": "hello"}))
.await;
assert!(matches!(result, ToolExecutionResult::ToolError(_)));
}
}