mod types;
pub use types::*;
use std::sync::Arc;
use async_trait::async_trait;
use serde_json::{Value, json};
use crate::error::{Result, TinyAgentsError};
use crate::harness::context::{RunConfig, RunContext};
use crate::harness::events::{AgentEvent, EventSink};
use crate::harness::message::Message;
use crate::harness::middleware::AgentRun;
use crate::harness::runtime::AgentHarness;
use crate::harness::tool::{Tool, ToolCall, ToolResult, ToolSchema};
impl<State: Send + Sync, Ctx: Send + Sync> SubAgent<State, Ctx> {
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
harness: Arc<AgentHarness<State, Ctx>>,
) -> Self {
Self {
harness,
name: name.into(),
description: description.into(),
system_prompt: None,
}
}
pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.system_prompt = Some(prompt.into());
self
}
pub fn name(&self) -> &str {
&self.name
}
pub fn description(&self) -> &str {
&self.description
}
pub fn harness(&self) -> &Arc<AgentHarness<State, Ctx>> {
&self.harness
}
fn seed_messages(&self, input: String) -> Vec<Message> {
let mut messages = Vec::with_capacity(2);
if let Some(prompt) = &self.system_prompt {
messages.push(Message::system(prompt.clone()));
}
messages.push(Message::user(input));
messages
}
fn child_config(&self, parent_depth: usize) -> Result<RunConfig> {
let max_depth = self.harness.policy().limits.max_depth;
let child_depth = parent_depth + 1;
if child_depth > max_depth {
return Err(TinyAgentsError::SubAgentDepth(max_depth));
}
Ok(RunConfig::new(format!("{}-d{child_depth}", self.name))
.with_depth(child_depth)
.with_max_depth(max_depth))
}
pub async fn invoke(
&self,
state: &State,
ctx_data: Ctx,
parent_depth: usize,
input: impl Into<String>,
) -> Result<AgentRun> {
let config = self.child_config(parent_depth)?;
let ctx = RunContext::new(config, ctx_data);
self.run_child(state, ctx, input.into()).await
}
pub async fn invoke_with_events(
&self,
state: &State,
ctx_data: Ctx,
parent_depth: usize,
input: impl Into<String>,
events: &EventSink,
) -> Result<AgentRun> {
let config = self.child_config(parent_depth)?;
let ctx = RunContext::new(config, ctx_data).with_events(events.clone());
self.run_child(state, ctx, input.into()).await
}
pub async fn invoke_in_parent(
&self,
state: &State,
ctx_data: Ctx,
parent: &RunContext<Ctx>,
input: impl Into<String>,
) -> Result<AgentRun> {
self.invoke_with_events(state, ctx_data, parent.depth(), input, &parent.events)
.await
}
async fn run_child(
&self,
state: &State,
ctx: RunContext<Ctx>,
input: String,
) -> Result<AgentRun> {
let depth = ctx.depth();
let messages = self.seed_messages(input);
let events = ctx.events.clone();
events.emit(AgentEvent::SubAgentStarted {
name: self.name.clone(),
depth,
});
let run = self.harness.invoke_in_context(state, ctx, messages).await?;
events.emit(AgentEvent::SubAgentCompleted {
name: self.name.clone(),
depth,
});
Ok(run)
}
}
impl<State: Send + Sync, Ctx: Send + Sync> SubAgentSession<State, Ctx> {
pub fn new(subagent: Arc<SubAgent<State, Ctx>>) -> Self {
Self {
subagent,
transcript: Vec::new(),
turn: 0,
parent_depth: 0,
events: EventSink::new(),
seeded: false,
}
}
pub fn from_subagent(subagent: SubAgent<State, Ctx>) -> Self {
Self::new(Arc::new(subagent))
}
pub fn with_events(mut self, events: EventSink) -> Self {
self.events = events;
self
}
pub fn with_parent_depth(mut self, parent_depth: usize) -> Self {
self.parent_depth = parent_depth;
self
}
pub fn subagent(&self) -> &Arc<SubAgent<State, Ctx>> {
&self.subagent
}
pub fn transcript(&self) -> &[Message] {
&self.transcript
}
pub fn turns(&self) -> usize {
self.turn
}
pub fn reset(&mut self) {
self.transcript.clear();
self.turn = 0;
self.seeded = false;
}
fn child_config(&self) -> Result<RunConfig> {
let max_depth = self.subagent.harness.policy().limits.max_depth;
let child_depth = self.parent_depth + 1;
if child_depth > max_depth {
return Err(TinyAgentsError::SubAgentDepth(max_depth));
}
Ok(RunConfig::new(format!(
"{}-t{}-d{child_depth}",
self.subagent.name, self.turn
))
.with_depth(child_depth)
.with_max_depth(max_depth))
}
pub async fn send(
&mut self,
state: &State,
ctx_data: Ctx,
input: Vec<Message>,
) -> Result<AgentRun> {
if !self.seeded {
if let Some(prompt) = &self.subagent.system_prompt {
self.transcript.push(Message::system(prompt.clone()));
}
self.seeded = true;
}
self.transcript.extend(input);
let config = self.child_config()?;
let depth = config.depth;
let ctx = RunContext::new(config, ctx_data).with_events(self.events.clone());
let events = self.events.clone();
events.emit(AgentEvent::SubAgentStarted {
name: self.subagent.name.clone(),
depth,
});
if self.turn > 0 {
events.emit(AgentEvent::SubAgentReused {
name: self.subagent.name.clone(),
turn: self.turn,
});
}
let run = self
.subagent
.harness
.invoke_in_context(state, ctx, self.transcript.clone())
.await?;
events.emit(AgentEvent::SubAgentCompleted {
name: self.subagent.name.clone(),
depth,
});
self.transcript = run.messages.clone();
self.turn += 1;
Ok(run)
}
}
impl<State: Send + Sync, Ctx: Send + Sync> SubAgentTool<State, Ctx> {
fn default_parameters() -> Value {
json!({
"type": "object",
"properties": {
SUBAGENT_INPUT_FIELD: {
"type": "string",
"description": "The task or question to delegate to the sub-agent."
}
},
"required": [SUBAGENT_INPUT_FIELD]
})
}
pub fn new(subagent: Arc<SubAgent<State, Ctx>>) -> Self {
let tool_name = subagent.name().to_owned();
Self {
subagent,
tool_name,
parent_depth: 0,
parameters: Self::default_parameters(),
}
}
pub fn with_tool_name(mut self, name: impl Into<String>) -> Self {
self.tool_name = name.into();
self
}
pub fn with_parent_depth(mut self, parent_depth: usize) -> Self {
self.parent_depth = parent_depth;
self
}
pub fn with_parameters(mut self, parameters: Value) -> Self {
self.parameters = parameters;
self
}
fn extract_input(arguments: &Value) -> String {
match arguments {
Value::String(s) => s.clone(),
Value::Object(map) => map
.get(SUBAGENT_INPUT_FIELD)
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned(),
_ => String::new(),
}
}
}
#[async_trait]
impl<State, Ctx> Tool<State> for SubAgentTool<State, Ctx>
where
State: Send + Sync,
Ctx: Send + Sync + Default,
{
fn name(&self) -> &str {
&self.tool_name
}
fn description(&self) -> &str {
self.subagent.description()
}
fn schema(&self) -> ToolSchema {
ToolSchema::new(
self.tool_name.clone(),
self.subagent.description().to_owned(),
self.parameters.clone(),
)
}
async fn call(&self, state: &State, call: ToolCall) -> Result<ToolResult> {
let input = Self::extract_input(&call.arguments);
let run = self
.subagent
.invoke(state, Ctx::default(), self.parent_depth, input)
.await?;
let text = run.text().unwrap_or_default();
Ok(ToolResult::text(call.id, &self.tool_name, text))
}
}
#[cfg(test)]
mod test;