runifold_agent/
descriptor.rs1use 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#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
10pub struct AgentDescriptor {
11 pub id: CapabilityId,
13 pub name: String,
15 pub version: String,
17 pub description: String,
19 pub risk: RiskLevel,
21 pub metadata: BTreeMap<String, Value>,
23}
24
25impl AgentDescriptor {
26 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 #[must_use]
48 pub const fn with_id(mut self, id: CapabilityId) -> Self {
49 self.id = id;
50 self
51 }
52
53 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 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}