pub mod runtime;
pub mod sink;
use crate::durable::SessionId;
use crate::profile::{AgentProfileId, SkillFilter, ToolFilter};
use crate::tool::ToolDefinition;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
pub const DELEGATION_SCHEMA_VERSION: i64 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum ChildApprovalMode {
#[default]
PropagateToParent,
AutoApprove,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum SubAgentToolBase {
#[default]
ParentEffective,
ChildDefault,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct SubAgentToolPolicy {
#[serde(default)]
pub base: SubAgentToolBase,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub deny: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub additions: Vec<String>,
}
impl SubAgentToolPolicy {
pub fn inherit_parent() -> Self {
Self {
base: SubAgentToolBase::ParentEffective,
deny: Vec::new(),
additions: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DelegationRequest {
pub goal: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context: Option<String>,
#[serde(
rename = "profile",
alias = "profile_id",
default,
skip_serializing_if = "Option::is_none"
)]
pub profile_id: Option<AgentProfileId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requested_skills: Vec<String>,
#[serde(default = "default_max_iterations")]
pub max_iterations: u32,
#[serde(default)]
pub child_approval_mode: ChildApprovalMode,
#[serde(default)]
pub tool_policy: SubAgentToolPolicy,
}
fn default_max_iterations() -> u32 {
10
}
impl DelegationRequest {
pub fn validate(&self) -> Result<(), String> {
let goal = self.goal.trim();
if goal.is_empty() {
return Err("delegation goal is required".to_string());
}
if self.max_iterations == 0 {
return Err("max_iterations must be greater than zero".to_string());
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DelegationOutcome {
EndTurn,
Cancelled,
MaxTurnRequests,
Unknown,
}
impl From<agent_client_protocol::schema::v1::StopReason> for DelegationOutcome {
fn from(reason: agent_client_protocol::schema::v1::StopReason) -> Self {
match reason {
agent_client_protocol::schema::v1::StopReason::EndTurn => DelegationOutcome::EndTurn,
agent_client_protocol::schema::v1::StopReason::Cancelled => {
DelegationOutcome::Cancelled
}
agent_client_protocol::schema::v1::StopReason::MaxTurnRequests => {
DelegationOutcome::MaxTurnRequests
}
agent_client_protocol::schema::v1::StopReason::MaxTokens
| agent_client_protocol::schema::v1::StopReason::Refusal
| _ => DelegationOutcome::Unknown,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DelegationResult {
pub delegation_id: String,
pub child_session_id: SessionId,
pub outcome: DelegationOutcome,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub final_text: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DelegationMetadata {
pub delegation_id: String,
pub parent_session_id: SessionId,
pub parent_tool_call_id: Option<String>,
pub child_session_id: SessionId,
pub profile_id: Option<AgentProfileId>,
pub child_approval_mode: ChildApprovalMode,
pub max_iterations: u32,
pub outcome: Option<DelegationOutcome>,
pub tool_catalog_digest: String,
pub inherited_tools: Vec<String>,
pub removed_tools: Vec<String>,
pub added_tools: Vec<String>,
pub unavailable_requested_additions: Vec<String>,
pub excluded_by_profile: Vec<String>,
pub tool_policy_diagnostics: Vec<ToolPolicyDiagnostic>,
pub requested_skills: Vec<String>,
pub activated_skills: Vec<String>,
pub excluded_skills_by_profile: Vec<String>,
pub unavailable_requested_skills: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "reason")]
pub enum ToolPolicyDiagnosticReason {
Missing,
ExcludedByProfile,
McpServerNotEnabled {
#[serde(rename = "server_id")]
server_id: String,
},
McpServerNotHealthy {
#[serde(rename = "server_id")]
server_id: String,
#[serde(rename = "health")]
health: String,
},
PluginNotEnabled {
#[serde(rename = "plugin_id")]
plugin_id: String,
},
PluginNotInstalled {
#[serde(rename = "plugin_id")]
plugin_id: String,
},
PluginManifestMissing {
#[serde(rename = "plugin_id")]
plugin_id: String,
},
PluginNotHealthy {
#[serde(rename = "plugin_id")]
plugin_id: String,
#[serde(rename = "health")]
health: String,
},
PluginAuthRequired {
#[serde(rename = "plugin_id")]
plugin_id: String,
},
PluginScopeMissing {
#[serde(rename = "plugin_id")]
plugin_id: String,
#[serde(rename = "required")]
required: Vec<String>,
#[serde(rename = "missing")]
missing: Vec<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolPolicyDiagnostic {
pub tool_name: String,
pub reason: ToolPolicyDiagnosticReason,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DerivedToolCatalog {
pub definitions: Vec<ToolDefinition>,
pub inherited_tools: Vec<String>,
pub removed_tools: Vec<String>,
pub added_tools: Vec<String>,
pub unavailable_requested_additions: Vec<String>,
pub excluded_by_profile: Vec<String>,
pub diagnostics: Vec<ToolPolicyDiagnostic>,
pub digest: String,
}
pub fn compute_tool_catalog_digest(definitions: &[ToolDefinition]) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
let mut defs: Vec<&ToolDefinition> = definitions.iter().collect();
defs.sort_by(|a, b| a.name.cmp(&b.name));
for def in defs {
def.name.hash(&mut hasher);
def.description.hash(&mut hasher);
def.input_schema.to_string().hash(&mut hasher);
def.requires_approval.hash(&mut hasher);
}
format!("{:016x}", hasher.finish())
}
pub fn apply_tool_filter(
definitions: &[ToolDefinition],
filter: &ToolFilter,
) -> (Vec<ToolDefinition>, Vec<String>) {
let mut allowed = Vec::new();
let mut excluded = Vec::new();
match filter {
ToolFilter::Inherit => {
allowed = definitions.to_vec();
}
ToolFilter::Allow(names) => {
let set: HashSet<_> = names.iter().cloned().collect();
for def in definitions {
if set.contains(&def.name) {
allowed.push(def.clone());
} else {
excluded.push(def.name.clone());
}
}
}
ToolFilter::Deny(names) => {
let set: HashSet<_> = names.iter().cloned().collect();
for def in definitions {
if set.contains(&def.name) {
excluded.push(def.name.clone());
} else {
allowed.push(def.clone());
}
}
}
}
(allowed, excluded)
}
pub fn apply_skill_filter(
requested: &[String],
filter: &SkillFilter,
) -> (Vec<String>, Vec<String>) {
let mut allowed = Vec::new();
let mut excluded = Vec::new();
match filter {
SkillFilter::None => {
excluded.extend(requested.iter().cloned());
}
SkillFilter::Inherit => {
allowed.extend(requested.iter().cloned());
}
SkillFilter::Allow(names) => {
let set: HashSet<_> = names.iter().cloned().collect();
for skill in requested {
if set.contains(skill) {
allowed.push(skill.clone());
} else {
excluded.push(skill.clone());
}
}
}
}
(allowed, excluded)
}
pub fn validate_delegation_arguments(
value: &serde_json::Value,
) -> Result<DelegationRequest, String> {
let request: DelegationRequest = serde_json::from_value(value.clone())
.map_err(|e| format!("invalid delegate_task args: {}", e))?;
request.validate()?;
Ok(request)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tool::ToolDefinition;
#[test]
fn delegation_request_rejects_empty_goal() {
let req = DelegationRequest {
goal: " ".to_string(),
context: None,
profile_id: None,
requested_skills: Vec::new(),
max_iterations: 5,
child_approval_mode: ChildApprovalMode::AutoApprove,
tool_policy: SubAgentToolPolicy::default(),
};
assert!(req.validate().is_err());
}
#[test]
fn delegation_request_rejects_zero_iterations() {
let req = DelegationRequest {
goal: "do work".to_string(),
context: None,
profile_id: None,
requested_skills: Vec::new(),
max_iterations: 0,
child_approval_mode: ChildApprovalMode::AutoApprove,
tool_policy: SubAgentToolPolicy::default(),
};
assert!(req.validate().is_err());
}
#[test]
fn apply_tool_filter_inherit_passes_through() {
let defs = vec![ToolDefinition::new("read", "read", serde_json::json!({}))];
let (allowed, excluded) = apply_tool_filter(&defs, &ToolFilter::Inherit);
assert_eq!(allowed.len(), 1);
assert!(excluded.is_empty());
}
#[test]
fn apply_tool_filter_allow_excludes_others() {
let defs = vec![
ToolDefinition::new("read", "read", serde_json::json!({})),
ToolDefinition::new("write", "write", serde_json::json!({})),
];
let (allowed, excluded) =
apply_tool_filter(&defs, &ToolFilter::Allow(vec!["read".to_string()]));
assert_eq!(allowed.len(), 1);
assert_eq!(allowed[0].name, "read");
assert_eq!(excluded, vec!["write".to_string()]);
}
#[test]
fn apply_skill_filter_respects_allow() {
let (allowed, excluded) = apply_skill_filter(
&["rust".to_string(), "python".to_string()],
&SkillFilter::Allow(vec!["rust".to_string()]),
);
assert_eq!(allowed, vec!["rust".to_string()]);
assert_eq!(excluded, vec!["python".to_string()]);
}
}