use std::collections::HashSet;
use crate::error::{Result, TinyAgentsError};
use crate::language::types::Blueprint;
use crate::registry::CapabilityRegistry;
pub const DEFAULT_NODE_KINDS: &[&str] = &[
"agent",
"model",
"tool_executor",
"subgraph",
"graph",
"subagent",
"repl_agent",
"router",
"interrupt",
"join",
"human",
];
#[derive(Clone, Debug, Default)]
pub struct CapabilityResolver {
models: HashSet<String>,
tools: HashSet<String>,
subgraphs: HashSet<String>,
routers: HashSet<String>,
reducers: HashSet<String>,
agents: HashSet<String>,
scripts: HashSet<String>,
node_kinds: HashSet<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ReferenceClass {
Model,
Subgraph,
Router,
Agent,
Script,
}
impl ReferenceClass {
pub fn word(self) -> &'static str {
match self {
ReferenceClass::Model => "model",
ReferenceClass::Subgraph => "subgraph",
ReferenceClass::Router => "router",
ReferenceClass::Agent => "agent",
ReferenceClass::Script => "script",
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct PrimaryReference<'a> {
pub class: ReferenceClass,
pub target: &'a str,
}
impl CapabilityResolver {
pub fn new() -> Self {
Self::default()
}
pub fn from_lists<M, T>(models: M, tools: T) -> Self
where
M: IntoIterator<Item = String>,
T: IntoIterator<Item = String>,
{
Self {
models: models.into_iter().collect(),
tools: tools.into_iter().collect(),
..Self::default()
}
}
pub fn from_registry<State: Send + Sync>(registry: &CapabilityRegistry<State>) -> Self {
use crate::registry::ComponentKind;
let collect = |kind| registry.names_including_aliases(kind).into_iter().collect();
Self {
models: collect(ComponentKind::Model),
tools: collect(ComponentKind::Tool),
subgraphs: collect(ComponentKind::Graph),
routers: collect(ComponentKind::Router),
reducers: collect(ComponentKind::Reducer),
agents: collect(ComponentKind::Agent),
scripts: collect(ComponentKind::Script),
node_kinds: DEFAULT_NODE_KINDS.iter().map(|k| (*k).to_owned()).collect(),
}
}
pub fn allow_model(mut self, name: impl Into<String>) -> Self {
self.models.insert(name.into());
self
}
pub fn allow_tool(mut self, name: impl Into<String>) -> Self {
self.tools.insert(name.into());
self
}
pub fn allow_subgraph(mut self, name: impl Into<String>) -> Self {
self.subgraphs.insert(name.into());
self
}
pub fn allow_router(mut self, name: impl Into<String>) -> Self {
self.routers.insert(name.into());
self
}
pub fn allow_reducer(mut self, name: impl Into<String>) -> Self {
self.reducers.insert(name.into());
self
}
pub fn allow_agent(mut self, name: impl Into<String>) -> Self {
self.agents.insert(name.into());
self
}
pub fn allow_script(mut self, name: impl Into<String>) -> Self {
self.scripts.insert(name.into());
self
}
pub fn with_node_kinds<I, S>(mut self, kinds: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.node_kinds = kinds.into_iter().map(Into::into).collect();
self
}
pub fn model_allowed(&self, name: &str) -> bool {
self.models.contains(name)
}
pub fn tool_allowed(&self, name: &str) -> bool {
self.tools.contains(name)
}
pub fn subgraph_allowed(&self, name: &str) -> bool {
self.subgraphs.contains(name)
}
pub fn router_allowed(&self, name: &str) -> bool {
self.routers.contains(name)
}
pub fn reducer_allowed(&self, name: &str) -> bool {
self.reducers.contains(name)
}
pub fn agent_allowed(&self, name: &str) -> bool {
self.agents.contains(name)
}
pub fn script_allowed(&self, name: &str) -> bool {
self.scripts.contains(name)
}
pub fn classify_reference<'a>(
kind: &str,
model: Option<&'a str>,
subgraph: Option<&'a str>,
agent: Option<&'a str>,
script: Option<&'a str>,
) -> Option<PrimaryReference<'a>> {
let (class, target) = match kind {
"subgraph" | "graph" => (ReferenceClass::Subgraph, subgraph?),
"router" => (ReferenceClass::Router, model?),
"subagent" => (ReferenceClass::Agent, agent?),
"repl_agent" => (ReferenceClass::Script, script?),
_ => (ReferenceClass::Model, model?),
};
Some(PrimaryReference { class, target })
}
pub fn reference_allowed(&self, class: ReferenceClass, target: &str) -> bool {
match class {
ReferenceClass::Model => self.model_allowed(target),
ReferenceClass::Subgraph => self.subgraph_allowed(target),
ReferenceClass::Router => self.router_allowed(target),
ReferenceClass::Agent => self.agent_allowed(target),
ReferenceClass::Script => self.script_allowed(target),
}
}
pub fn node_kind_allowed(&self, kind: &str) -> bool {
self.node_kinds.is_empty() || self.node_kinds.contains(kind)
}
pub fn bind_blueprint(&self, blueprint: &Blueprint) -> Result<()> {
for node in &blueprint.nodes {
if !self.node_kind_allowed(&node.kind) {
return Err(TinyAgentsError::Compile(format!(
"node `{}` has unknown kind `{}`",
node.name, node.kind
)));
}
let subgraph_target = node.subgraph.as_deref().or(node.model.as_deref());
if let Some(reference) = Self::classify_reference(
&node.kind,
node.model.as_deref(),
subgraph_target,
node.agent.as_deref(),
node.script.as_deref(),
) && !self.reference_allowed(reference.class, reference.target)
{
return Err(TinyAgentsError::Capability(format!(
"node `{}` references unknown {} `{}`",
node.name,
reference.class.word(),
reference.target
)));
}
for tool in &node.tools {
if !self.tool_allowed(tool) {
return Err(TinyAgentsError::Capability(format!(
"node `{}` references unknown tool `{tool}`",
node.name
)));
}
}
}
for channel in &blueprint.channels {
if !self.reducer_allowed(&channel.reducer) {
return Err(TinyAgentsError::Capability(format!(
"channel `{}` references unknown reducer `{}`",
channel.name, channel.reducer
)));
}
}
Ok(())
}
}
pub fn bind_capabilities(blueprint: &Blueprint, allow: &CapabilityResolver) -> Result<()> {
for node in &blueprint.nodes {
if let Some(model) = &node.model
&& !allow.model_allowed(model)
{
return Err(TinyAgentsError::Capability(format!(
"node `{}` references unknown model `{model}`",
node.name
)));
}
for tool in &node.tools {
if !allow.tool_allowed(tool) {
return Err(TinyAgentsError::Capability(format!(
"node `{}` references unknown tool `{tool}`",
node.name
)));
}
}
}
Ok(())
}
pub fn bind_capabilities_with_registry<State: Send + Sync>(
blueprint: &Blueprint,
registry: &CapabilityRegistry<State>,
) -> Result<()> {
CapabilityResolver::from_registry(registry).bind_blueprint(blueprint)
}