Skip to main content

runifold_agent/
descriptor.rs

1use std::collections::BTreeMap;
2
3use runifold_core::{CapabilityDescriptor, CapabilityId, CapabilityKind, EffectClass, RiskLevel};
4use runifold_model::ToolSpec;
5use serde::{Deserialize, Serialize};
6use serde_json::{Value, json};
7
8/// Versioned contract for invoking an agent through a gateway.
9#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
10pub struct AgentDescriptor {
11    /// Stable capability identity.
12    pub id: CapabilityId,
13    /// Model-facing delegation name.
14    pub name: String,
15    /// Semantic contract version.
16    pub version: String,
17    /// Model-facing description of when this agent should be used.
18    pub description: String,
19    /// Coarse risk classification for policy engines.
20    pub risk: RiskLevel,
21    /// Host-only namespaced metadata.
22    pub metadata: BTreeMap<String, Value>,
23}
24
25impl AgentDescriptor {
26    /// Creates an agent contract with a fresh ephemeral capability identity.
27    ///
28    /// Rebuilding this descriptor produces a different identity. Applications
29    /// that persist grants, policies, or audit records must restore the same
30    /// [`CapabilityId`] and call [`Self::with_id`].
31    pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
32        Self {
33            id: CapabilityId::new(),
34            name: name.into(),
35            version: "1".into(),
36            description: description.into(),
37            risk: RiskLevel::Medium,
38            metadata: BTreeMap::new(),
39        }
40    }
41
42    /// Replaces the capability identity with an application-owned stable ID.
43    ///
44    /// The ID should be loaded from durable configuration or storage and reused
45    /// across process restarts whenever grants or audit records outlive one
46    /// process.
47    #[must_use]
48    pub const fn with_id(mut self, id: CapabilityId) -> Self {
49        self.id = id;
50        self
51    }
52
53    /// Converts this descriptor into a grantable agent capability.
54    pub fn capability(&self) -> CapabilityDescriptor {
55        CapabilityDescriptor {
56            id: self.id,
57            name: self.name.clone(),
58            version: self.version.clone(),
59            kind: CapabilityKind::Agent,
60            input_schema: input_schema(),
61            output_schema: output_schema(),
62            effect: EffectClass::Unknown,
63            risk: self.risk,
64            metadata: self.metadata.clone(),
65        }
66    }
67
68    /// Converts this descriptor into the callable shape exposed to a model.
69    pub fn model_spec(&self) -> ToolSpec {
70        ToolSpec {
71            name: self.name.clone(),
72            description: self.description.clone(),
73            input_schema: input_schema(),
74            output_schema: Some(output_schema()),
75            metadata: self.metadata.clone(),
76        }
77    }
78}
79
80fn input_schema() -> Value {
81    json!({
82        "type": "object",
83        "properties": {
84            "input": {
85                "type": "string",
86                "description": "Task or question delegated to the agent"
87            }
88        },
89        "required": ["input"],
90        "additionalProperties": false
91    })
92}
93
94fn output_schema() -> Value {
95    json!({
96        "type": "object",
97        "properties": {
98            "agent": {"type": "string"},
99            "content": {"type": "array"},
100            "turns": {"type": "integer", "minimum": 0},
101            "tool_calls": {"type": "integer", "minimum": 0},
102            "delegations": {"type": "integer", "minimum": 0}
103        },
104        "required": ["agent", "content", "turns", "tool_calls", "delegations"],
105        "additionalProperties": false
106    })
107}
108
109#[cfg(test)]
110mod tests {
111    use runifold_core::CapabilityId;
112
113    use super::AgentDescriptor;
114
115    #[test]
116    fn configured_identity_survives_descriptor_reconstruction() {
117        let id: CapabilityId = "018f6f7e-6f1d-7f2a-9c40-7f4f8f0a3d21"
118            .parse()
119            .expect("configured UUID is valid");
120
121        let first = AgentDescriptor::new("researcher", "delegate research").with_id(id);
122        let second = AgentDescriptor::new("researcher", "delegate research").with_id(id);
123
124        assert_eq!(first.id, second.id);
125        assert_eq!(first.capability().id, second.capability().id);
126    }
127
128    #[test]
129    fn default_construction_remains_ephemeral() {
130        let first = AgentDescriptor::new("researcher", "delegate research");
131        let second = AgentDescriptor::new("researcher", "delegate research");
132
133        assert_ne!(first.id, second.id);
134    }
135}