use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use adk_core::Agent;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{LoopAgent, ParallelAgent, SequentialAgent};
use super::{RelationshipKind, TeamError, TeamMemberSpec, TeamPolicy, TeamRelationship, TeamSpec};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", tag = "architecture")]
pub enum TeamArchitectureTemplate {
Supervisor {
name: String,
coordinator: String,
specialists: Vec<String>,
relationship: RelationshipKind,
#[serde(default)]
policy: TeamPolicy,
},
Router {
name: String,
router: String,
routes: Vec<String>,
relationship: RelationshipKind,
#[serde(default)]
policy: TeamPolicy,
},
Hierarchical {
name: String,
root: String,
branches: Vec<TeamManagerBranch>,
#[serde(default)]
policy: TeamPolicy,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct TeamManagerBranch {
pub manager: String,
pub workers: Vec<String>,
}
impl TeamArchitectureTemplate {
pub fn lower(&self) -> std::result::Result<TeamSpec, TeamError> {
let spec = match self {
Self::Supervisor { name, coordinator, specialists, relationship, policy } => TeamSpec {
name: name.clone(),
description: "Supervisor team compiled from a portable architecture template"
.to_string(),
coordinator: coordinator.clone(),
members: std::iter::once(coordinator)
.chain(specialists)
.map(|member| TeamMemberSpec::new(member.clone()))
.collect(),
relationships: specialists
.iter()
.map(|specialist| TeamRelationship::new(coordinator, specialist, *relationship))
.collect(),
policy: policy.clone(),
},
Self::Router { name, router, routes, relationship, policy } => TeamSpec {
name: name.clone(),
description: "Exact router team compiled from a portable architecture template"
.to_string(),
coordinator: router.clone(),
members: std::iter::once(router)
.chain(routes)
.map(|member| TeamMemberSpec::new(member.clone()))
.collect(),
relationships: routes
.iter()
.map(|route| TeamRelationship::new(router, route, *relationship))
.collect(),
policy: policy.clone(),
},
Self::Hierarchical { name, root, branches, policy } => {
let mut members = vec![TeamMemberSpec::new(root.clone())];
let mut relationships = Vec::new();
for branch in branches {
members.push(TeamMemberSpec::new(branch.manager.clone()));
relationships.push(TeamRelationship::new(
root,
&branch.manager,
RelationshipKind::Delegate,
));
for worker in &branch.workers {
members.push(TeamMemberSpec::new(worker.clone()));
relationships.push(TeamRelationship::new(
&branch.manager,
worker,
RelationshipKind::Delegate,
));
}
}
TeamSpec {
name: name.clone(),
description: "Hierarchical team compiled from a portable architecture template"
.to_string(),
coordinator: root.clone(),
members,
relationships,
policy: policy.clone(),
}
}
};
spec.validate()?;
Ok(spec)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", tag = "architecture")]
pub enum WorkflowArchitectureTemplate {
Sequential {
name: String,
steps: Vec<String>,
},
Parallel {
name: String,
members: Vec<String>,
#[serde(default)]
shared_state: bool,
},
FanOutFanIn {
name: String,
workers: Vec<String>,
aggregator: String,
#[serde(default)]
shared_state: bool,
},
ReviewLoop {
name: String,
producer: String,
reviewer: String,
max_iterations: u32,
},
}
impl WorkflowArchitectureTemplate {
pub fn compile(
&self,
agents: impl IntoIterator<Item = Arc<dyn Agent>>,
) -> std::result::Result<Arc<dyn Agent>, TeamError> {
let registry: HashMap<String, Arc<dyn Agent>> =
agents.into_iter().map(|agent| (agent.name().to_string(), agent)).collect();
let resolve = |names: &[String]| -> std::result::Result<Vec<Arc<dyn Agent>>, TeamError> {
names
.iter()
.map(|name| {
registry.get(name).cloned().ok_or_else(|| TeamError::MissingAgent(name.clone()))
})
.collect()
};
let ensure_unique = |names: &[String]| -> std::result::Result<(), TeamError> {
let mut seen = HashSet::new();
for name in names {
if !seen.insert(name) {
return Err(TeamError::DuplicateMember(name.clone()));
}
}
Ok(())
};
match self {
Self::Sequential { name, steps } => {
ensure_unique(steps)?;
Ok(Arc::new(SequentialAgent::new(name, resolve(steps)?)))
}
Self::Parallel { name, members, shared_state } => {
ensure_unique(members)?;
let parallel = ParallelAgent::new(name, resolve(members)?);
Ok(Arc::new(if *shared_state { parallel.with_shared_state() } else { parallel }))
}
Self::FanOutFanIn { name, workers, aggregator, shared_state } => {
ensure_unique(workers)?;
if workers.iter().any(|worker| worker == aggregator) {
return Err(TeamError::DuplicateMember(aggregator.clone()));
}
let parallel = ParallelAgent::new(format!("{name}_fan_out"), resolve(workers)?);
let parallel = if *shared_state { parallel.with_shared_state() } else { parallel };
let aggregator = registry
.get(aggregator)
.cloned()
.ok_or_else(|| TeamError::MissingAgent(aggregator.clone()))?;
Ok(Arc::new(SequentialAgent::new(name, vec![Arc::new(parallel), aggregator])))
}
Self::ReviewLoop { name, producer, reviewer, max_iterations } => {
if *max_iterations == 0 {
return Err(TeamError::InvalidPolicy("reviewLoop.maxIterations"));
}
if producer == reviewer {
return Err(TeamError::DuplicateMember(producer.clone()));
}
let members = resolve(&[producer.clone(), reviewer.clone()])?;
Ok(Arc::new(LoopAgent::new(name, members).with_max_iterations(*max_iterations)))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct NoopAgent(String);
#[async_trait::async_trait]
impl Agent for NoopAgent {
fn name(&self) -> &str {
&self.0
}
fn description(&self) -> &str {
"workflow template test agent"
}
fn sub_agents(&self) -> &[Arc<dyn Agent>] {
&[]
}
async fn run(
&self,
_ctx: Arc<dyn adk_core::InvocationContext>,
) -> adk_core::Result<adk_core::EventStream> {
Ok(Box::pin(futures::stream::empty()))
}
}
#[test]
fn supervisor_template_lowers_to_exact_edges() {
let spec = TeamArchitectureTemplate::Supervisor {
name: "support".to_string(),
coordinator: "supervisor".to_string(),
specialists: vec!["billing".to_string(), "technical".to_string()],
relationship: RelationshipKind::Handoff,
policy: TeamPolicy::default(),
}
.lower()
.unwrap();
assert_eq!(spec.members.len(), 3);
assert_eq!(spec.relationships.len(), 2);
assert!(spec.relationships.iter().all(|edge| edge.from == "supervisor"));
}
#[test]
fn hierarchical_template_rejects_duplicate_workers() {
let error = TeamArchitectureTemplate::Hierarchical {
name: "org".to_string(),
root: "director".to_string(),
branches: vec![
TeamManagerBranch {
manager: "research".to_string(),
workers: vec!["analyst".to_string()],
},
TeamManagerBranch {
manager: "review".to_string(),
workers: vec!["analyst".to_string()],
},
],
policy: TeamPolicy::default(),
}
.lower()
.unwrap_err();
assert_eq!(error, TeamError::DuplicateMember("analyst".to_string()));
}
#[test]
fn workflow_template_compiles_to_existing_primitives() {
let root = WorkflowArchitectureTemplate::FanOutFanIn {
name: "research".to_string(),
workers: vec!["facts".to_string(), "risks".to_string()],
aggregator: "reviewer".to_string(),
shared_state: true,
}
.compile([
Arc::new(NoopAgent("facts".to_string())) as Arc<dyn Agent>,
Arc::new(NoopAgent("risks".to_string())),
Arc::new(NoopAgent("reviewer".to_string())),
])
.unwrap();
assert_eq!(root.name(), "research");
assert_eq!(root.sub_agents().len(), 2);
assert_eq!(root.sub_agents()[0].name(), "research_fan_out");
}
}