use std::collections::{BTreeMap, HashMap};
use std::sync::{Arc, RwLock};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::Uuid;
pub const SCHEMA: &str = "intent-token/1";
#[derive(Debug, Error)]
pub enum IntentError {
#[error("unknown verb: {0}.{1}")]
UnknownVerb(String, String),
#[error("missing capability: {0}")]
MissingCapability(String),
#[error("constraint failed: {0}")]
ConstraintFailed(String),
#[error("schema mismatch: {0}")]
Schema(String),
#[error("unknown tool: {0}")]
UnknownTool(String),
#[error(transparent)]
Serde(#[from] serde_json::Error),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct IntentToken {
pub schema: String,
pub id: Uuid,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_id: Option<Uuid>,
pub verb: Verb,
pub object: Object,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub modifiers: BTreeMap<String, serde_json::Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub required_capabilities: Vec<String>,
pub provenance: Provenance,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub confidence: Option<f32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub constraints: Vec<Constraint>,
pub namespace: String,
}
impl IntentToken {
pub fn new(verb: Verb, object: Object, provenance: Provenance, namespace: String) -> Self {
Self {
schema: SCHEMA.to_string(),
id: Uuid::new_v4(),
parent_id: None,
verb,
object,
modifiers: BTreeMap::new(),
required_capabilities: Vec::new(),
provenance,
confidence: None,
constraints: Vec::new(),
namespace,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Verb {
pub namespace: String,
pub action: String,
}
impl Verb {
pub fn new(namespace: impl Into<String>, action: impl Into<String>) -> Self {
Self {
namespace: namespace.into(),
action: action.into(),
}
}
pub fn dotted(&self) -> String {
format!("{}.{}", self.namespace, self.action)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Object {
pub kind: String,
pub value: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "source", rename_all = "snake_case")]
pub enum Provenance {
User {
raw_input: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
ui_origin: Option<String>,
ts: DateTime<Utc>,
},
Llm {
model: String,
call_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
raw_input: Option<String>,
ts: DateTime<Utc>,
},
Reflex {
trigger: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
raw_input: Option<String>,
ts: DateTime<Utc>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum Constraint {
PathExists {
path: String,
},
NetReachable {
host: String,
port: u16,
},
EnvSet {
name: String,
},
UserPresent,
Custom {
name: String,
args: serde_json::Value,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BackendId(pub String);
impl BackendId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ToolSource {
McpServer { server: String },
NativeBackend { backend: BackendId },
Terminal,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ToolAnnotations {
#[serde(default)]
pub read_only_hint: bool,
#[serde(default)]
pub destructive_hint: bool,
#[serde(default)]
pub idempotent_hint: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ToolUsage {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub when_to_use: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub when_not_to: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub preconditions: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cost: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub example: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tier: Option<String>,
}
impl ToolUsage {
pub fn is_empty(&self) -> bool {
self.when_to_use.is_none()
&& self.when_not_to.is_none()
&& self.preconditions.is_empty()
&& self.cost.is_none()
&& self.example.is_none()
&& self.tier.is_none()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDescriptor {
pub tool_id: String,
pub source: ToolSource,
pub verb: Verb,
pub description: String,
pub input_schema: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_schema: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capabilities: Vec<String>,
#[serde(default)]
pub annotations: ToolAnnotations,
#[serde(default, skip_serializing_if = "ToolUsage::is_empty")]
pub usage: ToolUsage,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub embedding: Option<Vec<f32>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ToolRoute {
Mcp {
server: String,
tool: String,
},
NativeBackend {
backend: BackendId,
},
Terminal {
#[serde(default, skip_serializing_if = "Option::is_none")]
session_hint: Option<String>,
},
HumanConfirm {
ask: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScoredTool {
pub tool: ToolDescriptor,
pub score: f32,
}
#[async_trait]
pub trait IntentRouter: Send + Sync {
async fn resolve(&self, tok: &IntentToken) -> Result<ToolRoute, IntentError>;
}
#[async_trait]
pub trait ToolRegistry: Send + Sync {
async fn register(&self, descriptor: ToolDescriptor) -> Result<(), IntentError>;
async fn deregister(&self, tool_id: &str) -> Result<(), IntentError>;
async fn list(&self) -> Vec<ToolDescriptor>;
async fn get(&self, tool_id: &str) -> Option<ToolDescriptor>;
}
#[async_trait]
pub trait CapabilityIndex: Send + Sync {
async fn search(&self, q: &str, caps: &[String], k: usize) -> Vec<ScoredTool>;
async fn upsert(&self, t: &ToolDescriptor) -> Result<(), IntentError>;
}
#[async_trait]
pub trait BreakerCheck: Send + Sync {
async fn is_open(&self, tool_id: &str) -> bool;
}
pub struct DefaultIntentRouter {
registry: Arc<dyn ToolRegistry>,
breakers: Option<Arc<dyn BreakerCheck>>,
}
impl DefaultIntentRouter {
pub fn new(registry: Arc<dyn ToolRegistry>) -> Self {
Self {
registry,
breakers: None,
}
}
pub fn with_breakers(mut self, breakers: Arc<dyn BreakerCheck>) -> Self {
self.breakers = Some(breakers);
self
}
pub fn score(tok: &IntentToken, tool: &ToolDescriptor) -> f32 {
let mut score = 0.0_f32;
if tok.verb == tool.verb {
score += 2.0;
} else if tok.verb.namespace == tool.verb.namespace {
score += 1.0;
}
if tool.verb.namespace == "mcp" && tool.verb.action == tok.verb.action {
score += 0.5;
}
score += jaccard(&tok.required_capabilities, &tool.capabilities) * 1.5;
score
}
}
#[async_trait]
impl IntentRouter for DefaultIntentRouter {
async fn resolve(&self, tok: &IntentToken) -> Result<ToolRoute, IntentError> {
let tools = self.registry.list().await;
let mut best: Option<(ToolDescriptor, f32)> = None;
for t in tools {
if let Some(breakers) = &self.breakers {
if breakers.is_open(&t.tool_id).await {
continue;
}
}
let s = Self::score(tok, &t);
match best {
None if s > 0.0 => best = Some((t, s)),
Some((_, b)) if s > b => best = Some((t, s)),
_ => {}
}
}
Ok(match best {
Some((tool, _)) => route_for(&tool),
None => ToolRoute::HumanConfirm {
ask: format!(
"No tool registered for verb '{}.{}' — review the capability registry or add a matching backend.",
tok.verb.namespace, tok.verb.action,
),
},
})
}
}
fn route_for(tool: &ToolDescriptor) -> ToolRoute {
match &tool.source {
ToolSource::McpServer { server } => ToolRoute::Mcp {
server: server.clone(),
tool: tool.verb.action.clone(),
},
ToolSource::NativeBackend { backend } => ToolRoute::NativeBackend {
backend: backend.clone(),
},
ToolSource::Terminal => ToolRoute::Terminal { session_hint: None },
}
}
fn jaccard(a: &[String], b: &[String]) -> f32 {
if a.is_empty() && b.is_empty() {
return 0.0;
}
let set_a: std::collections::HashSet<&str> = a.iter().map(String::as_str).collect();
let set_b: std::collections::HashSet<&str> = b.iter().map(String::as_str).collect();
let intersection = set_a.intersection(&set_b).count() as f32;
let union = set_a.union(&set_b).count() as f32;
if union == 0.0 {
0.0
} else {
intersection / union
}
}
#[derive(Default)]
pub struct InMemoryToolRegistry {
tools: RwLock<HashMap<String, ToolDescriptor>>,
}
impl InMemoryToolRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn len(&self) -> usize {
self.tools.read().expect("registry lock poisoned").len()
}
pub fn is_empty(&self) -> bool {
self.tools
.read()
.expect("registry lock poisoned")
.is_empty()
}
}
#[async_trait]
impl ToolRegistry for InMemoryToolRegistry {
async fn register(&self, descriptor: ToolDescriptor) -> Result<(), IntentError> {
let mut tools = self.tools.write().expect("registry lock poisoned");
tools.insert(descriptor.tool_id.clone(), descriptor);
Ok(())
}
async fn deregister(&self, tool_id: &str) -> Result<(), IntentError> {
let mut tools = self.tools.write().expect("registry lock poisoned");
if tools.remove(tool_id).is_none() {
return Err(IntentError::UnknownTool(tool_id.to_string()));
}
Ok(())
}
async fn list(&self) -> Vec<ToolDescriptor> {
self.tools
.read()
.expect("registry lock poisoned")
.values()
.cloned()
.collect()
}
async fn get(&self, tool_id: &str) -> Option<ToolDescriptor> {
self.tools
.read()
.expect("registry lock poisoned")
.get(tool_id)
.cloned()
}
}
pub mod sanitization;
pub mod verbs;
#[cfg(test)]
mod tests;