use std::path::{Path, PathBuf};
use std::time::SystemTime;
use terraphim_automata::markdown_directives::parse_markdown_directives_dir;
use terraphim_types::{
MarkdownDirectives, NormalizedTerm, NormalizedTermValue, RouteDirective, Thesaurus,
};
use tracing::{debug, info, warn};
#[derive(Debug, Clone)]
pub struct KgRouteDecision {
pub provider: String,
pub model: String,
pub action: Option<String>,
pub confidence: f64,
pub matched_concept: String,
pub priority: u8,
pub fallback_routes: Vec<RouteDirective>,
}
impl KgRouteDecision {
pub fn render_action(&self, prompt: &str) -> Option<String> {
self.action.as_ref().map(|template| {
template
.replace("{{ model }}", &self.model)
.replace("{{model}}", &self.model)
.replace("{{ prompt }}", prompt)
.replace("{{prompt}}", prompt)
})
}
pub fn first_healthy_route(&self, unhealthy_providers: &[String]) -> Option<&RouteDirective> {
self.fallback_routes
.iter()
.find(|r| !unhealthy_providers.contains(&r.provider))
}
}
#[derive(Debug, Clone)]
struct RoutingRule {
concept: String,
directives: MarkdownDirectives,
}
#[derive(Clone)]
pub struct KgRouter {
rules: Vec<RoutingRule>,
thesaurus: Thesaurus,
taxonomy_path: PathBuf,
last_mtime: Option<SystemTime>,
}
impl std::fmt::Debug for KgRouter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KgRouter")
.field("taxonomy_path", &self.taxonomy_path)
.field("rules_count", &self.rules.len())
.field("thesaurus_size", &self.thesaurus.len())
.finish()
}
}
impl KgRouter {
pub fn load(taxonomy_path: impl Into<PathBuf>) -> Result<Self, KgRouterError> {
let taxonomy_path = taxonomy_path.into();
if !taxonomy_path.exists() {
return Err(KgRouterError::TaxonomyNotFound(
taxonomy_path.display().to_string(),
));
}
let parse_result = parse_markdown_directives_dir(&taxonomy_path)
.map_err(|e| KgRouterError::ParseError(e.to_string()))?;
for w in &parse_result.warnings {
warn!(
path = %w.path.display(),
line = ?w.line,
msg = %w.message,
"KG routing rule warning"
);
}
let mut rules = Vec::new();
let mut thesaurus = Thesaurus::new("kg_router".to_string());
let mut term_id: u64 = 1;
for (concept, directives) in &parse_result.directives {
if directives.routes.is_empty() {
debug!(concept = %concept, "skipping KG file with no routes");
continue;
}
for synonym in &directives.synonyms {
let key = NormalizedTermValue::from(synonym.clone());
let term = NormalizedTerm {
id: term_id,
value: NormalizedTermValue::from(concept.clone()),
display_value: None,
url: None,
};
thesaurus.insert(key, term);
term_id += 1;
}
rules.push(RoutingRule {
concept: concept.clone(),
directives: directives.clone(),
});
}
info!(
path = %taxonomy_path.display(),
rules = rules.len(),
synonyms = thesaurus.len(),
"KG router loaded"
);
let last_mtime = Self::dir_mtime(&taxonomy_path);
Ok(Self {
rules,
thesaurus,
taxonomy_path,
last_mtime,
})
}
pub fn route_agent(&self, task_description: &str) -> Option<KgRouteDecision> {
if self.thesaurus.is_empty() {
return None;
}
let matches = match terraphim_automata::find_matches(
task_description,
self.thesaurus.clone(),
false,
) {
Ok(m) if !m.is_empty() => m,
Ok(_) => {
debug!(task = %task_description.chars().take(80).collect::<String>(), "no KG synonym match");
return None;
}
Err(e) => {
warn!(error = %e, "KG router find_matches failed");
return None;
}
};
let mut best: Option<(&RoutingRule, f64)> = None;
for matched in &matches {
let concept = matched.normalized_term.value.to_string();
if let Some(rule) = self.rules.iter().find(|r| r.concept == concept) {
let priority = rule.directives.priority.unwrap_or(50) as f64;
let score = priority;
match &best {
Some((_, best_score)) if score <= *best_score => {}
_ => best = Some((rule, score)),
}
}
}
let (rule, score) = best?;
let primary = rule.directives.routes.first()?;
let confidence = score / 100.0;
info!(
concept = %rule.concept,
provider = %primary.provider,
model = %primary.model,
confidence = confidence,
"KG route matched"
);
Some(KgRouteDecision {
provider: primary.provider.clone(),
model: primary.model.clone(),
action: primary.action.clone(),
confidence,
matched_concept: rule.concept.clone(),
priority: rule.directives.priority.unwrap_or(50),
fallback_routes: rule.directives.routes.clone(),
})
}
pub fn reload(&mut self) -> Result<(), KgRouterError> {
let reloaded = Self::load(&self.taxonomy_path)?;
self.rules = reloaded.rules;
self.thesaurus = reloaded.thesaurus;
self.last_mtime = reloaded.last_mtime;
info!(path = %self.taxonomy_path.display(), "KG router reloaded");
Ok(())
}
pub fn reload_if_changed(&mut self) -> bool {
let current_mtime = Self::dir_mtime(&self.taxonomy_path);
if current_mtime != self.last_mtime {
match self.reload() {
Ok(()) => {
info!(path = %self.taxonomy_path.display(), "KG routing rules hot-reloaded");
return true;
}
Err(e) => {
warn!(error = %e, "KG router hot-reload failed, keeping old rules");
}
}
}
false
}
fn dir_mtime(path: &Path) -> Option<SystemTime> {
std::fs::read_dir(path)
.ok()?
.filter_map(|e| e.ok())
.filter(|e| {
e.path()
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext == "md")
.unwrap_or(false)
})
.filter_map(|e| e.metadata().ok()?.modified().ok())
.max()
}
pub fn taxonomy_path(&self) -> &Path {
&self.taxonomy_path
}
pub fn rule_count(&self) -> usize {
self.rules.len()
}
pub fn all_routes(&self) -> Vec<&RouteDirective> {
self.rules
.iter()
.flat_map(|r| r.directives.routes.iter())
.collect()
}
}
#[derive(Debug, thiserror::Error)]
pub enum KgRouterError {
#[error("taxonomy directory not found: {0}")]
TaxonomyNotFound(String),
#[error("failed to parse taxonomy: {0}")]
ParseError(String),
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
fn write_rule(dir: &Path, name: &str, content: &str) {
fs::write(dir.join(format!("{name}.md")), content).unwrap();
}
#[test]
fn routes_to_primary_by_synonym_match() {
let dir = tempdir().unwrap();
write_rule(
dir.path(),
"implementation",
r#"# Implementation
priority:: 50
synonyms:: implement, build, code, fix
route:: kimi, kimi-for-coding/k2p5
action:: opencode -m {{ model }} -p "{{ prompt }}"
route:: anthropic, claude-sonnet-4-6
action:: claude --model {{ model }} -p "{{ prompt }}"
"#,
);
let router = KgRouter::load(dir.path()).unwrap();
let decision = router.route_agent("implement the new feature").unwrap();
assert_eq!(decision.provider, "kimi");
assert_eq!(decision.model, "kimi-for-coding/k2p5");
assert_eq!(decision.matched_concept, "implementation");
assert_eq!(decision.fallback_routes.len(), 2);
}
#[test]
fn higher_priority_wins() {
let dir = tempdir().unwrap();
write_rule(
dir.path(),
"implementation",
"priority:: 50\nsynonyms:: implement, build, review code\nroute:: kimi, k2p5\n",
);
write_rule(
dir.path(),
"code_review",
"priority:: 70\nsynonyms:: code review, architecture review\nroute:: anthropic, opus\n",
);
let router = KgRouter::load(dir.path()).unwrap();
let decision = router
.route_agent("do a code review of the architecture")
.unwrap();
assert_eq!(decision.provider, "anthropic");
assert_eq!(decision.matched_concept, "code_review");
}
#[test]
fn no_match_returns_none() {
let dir = tempdir().unwrap();
write_rule(
dir.path(),
"security",
"priority:: 60\nsynonyms:: security audit, CVE\nroute:: kimi, k2p5\n",
);
let router = KgRouter::load(dir.path()).unwrap();
assert!(router.route_agent("write documentation").is_none());
}
#[test]
fn render_action_substitutes_placeholders() {
let dir = tempdir().unwrap();
write_rule(
dir.path(),
"impl",
r#"synonyms:: build
route:: kimi, k2p5
action:: opencode -m {{ model }} -p "{{ prompt }}"
"#,
);
let router = KgRouter::load(dir.path()).unwrap();
let decision = router.route_agent("build it").unwrap();
let rendered = decision.render_action("echo hello").unwrap();
assert_eq!(rendered, r#"opencode -m k2p5 -p "echo hello""#);
}
#[test]
fn first_healthy_route_skips_unhealthy() {
let dir = tempdir().unwrap();
write_rule(
dir.path(),
"impl",
"synonyms:: build\nroute:: kimi, k2p5\nroute:: anthropic, sonnet\n",
);
let router = KgRouter::load(dir.path()).unwrap();
let decision = router.route_agent("build it").unwrap();
let healthy = decision.first_healthy_route(&["kimi".to_string()]).unwrap();
assert_eq!(healthy.provider, "anthropic");
}
#[test]
fn empty_dir_loads_with_zero_rules() {
let dir = tempdir().unwrap();
let router = KgRouter::load(dir.path()).unwrap();
assert_eq!(router.rule_count(), 0);
assert!(router.route_agent("anything").is_none());
}
#[test]
fn reload_picks_up_new_files() {
let dir = tempdir().unwrap();
let mut router = KgRouter::load(dir.path()).unwrap();
assert_eq!(router.rule_count(), 0);
write_rule(
dir.path(),
"security",
"synonyms:: CVE\nroute:: kimi, k2p5\n",
);
router.reload().unwrap();
assert_eq!(router.rule_count(), 1);
assert!(router.route_agent("check CVE").is_some());
}
#[test]
fn loads_real_adf_taxonomy_3_tiers() {
let taxonomy = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../docs/taxonomy/routing_scenarios/adf");
if !taxonomy.exists() {
return;
}
let router = KgRouter::load(&taxonomy).unwrap();
assert_eq!(router.rule_count(), 3, "expected 3 tier files");
for route_directive in router.all_routes() {
assert!(
route_directive.action.is_some(),
"route {}/{} missing action:: template",
route_directive.provider,
route_directive.model
);
}
let d = router
.route_agent("create a plan for strategic planning")
.unwrap();
assert_eq!(d.matched_concept, "planning_tier");
assert_eq!(d.priority, 80);
assert_eq!(d.provider, "anthropic");
assert!(d.model.contains("opus"));
let d = router.route_agent("verify and validate results").unwrap();
assert_eq!(d.matched_concept, "review_tier");
assert_eq!(d.priority, 60);
assert_eq!(d.provider, "anthropic");
assert!(d.model.contains("haiku"));
let d = router.route_agent("implement the new feature").unwrap();
assert_eq!(d.matched_concept, "implementation_tier");
assert_eq!(d.priority, 50);
assert_eq!(d.provider, "anthropic");
assert!(d.model.contains("sonnet"));
}
#[test]
fn e2e_all_adf_agents_route_to_correct_tier() {
let taxonomy = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../docs/taxonomy/routing_scenarios/adf");
if !taxonomy.exists() {
return;
}
let router = KgRouter::load(&taxonomy).unwrap();
let agents: Vec<(&str, &str, &str, &str)> = vec![
(
"meta-coordinator",
"create a plan for strategic planning and cross-agent coordination",
"planning_tier",
"anthropic",
),
(
"product-development",
"create a plan for product roadmap and feature prioritisation",
"planning_tier",
"anthropic",
),
(
"spec-validator",
"verify and validate outputs, check results pass fail quality gate",
"review_tier",
"anthropic",
),
(
"quality-coordinator",
"review code quality and verify test results for PR approval",
"review_tier",
"anthropic",
),
(
"compliance-watchdog",
"verify compliance and check audit results against standards",
"review_tier",
"anthropic",
),
(
"drift-detector",
"check drift detection and validate system state",
"review_tier",
"anthropic",
),
(
"merge-coordinator",
"review merge verdict and evaluate GO NO-GO for PR approval",
"review_tier",
"anthropic",
),
(
"security-sentinel",
"security audit cargo audit CVE vulnerability scan",
"implementation_tier",
"anthropic",
),
(
"test-guardian",
"test QA regression integration test cargo test",
"implementation_tier",
"anthropic",
),
(
"implementation-swarm",
"implement build code fix refactor feature PR",
"implementation_tier",
"anthropic",
),
(
"documentation-generator",
"documentation readme changelog API docs technical writing",
"implementation_tier",
"anthropic",
),
(
"browser-qa",
"test QA browser test end-to-end regression",
"implementation_tier",
"anthropic",
),
(
"log-analyst",
"log analysis error pattern incident observability",
"implementation_tier",
"anthropic",
),
];
let mut all_passed = true;
for (agent, task, expected_tier, expected_provider) in &agents {
match router.route_agent(task) {
Some(decision) => {
let tier_ok = decision.matched_concept == *expected_tier;
let provider_ok = decision.provider == *expected_provider;
if !tier_ok || !provider_ok {
eprintln!(
"MISMATCH {}: got {}:{}/{} (expected {}:{})",
agent,
decision.matched_concept,
decision.provider,
decision.model,
expected_tier,
expected_provider,
);
all_passed = false;
} else {
eprintln!(
"OK {}: {} -> {}/{} (pri={})",
agent,
decision.matched_concept,
decision.provider,
decision.model,
decision.priority,
);
}
}
None => {
eprintln!("NO MATCH {}: task={}", agent, task);
all_passed = false;
}
}
}
assert!(all_passed, "some agents did not route to correct tier");
}
}