Skip to main content

rig_compose/
delegate.rs

1//! Agent delegation primitives.
2//!
3//! A delegate is another agent-shaped capability exposed as a normal
4//! [`Tool`]. The model still decides when to call it; the runtime merely
5//! provides an implementation. This keeps delegation agentic while allowing
6//! hosts to run child agents in the same process when that is cheaper than
7//! MCP or another remote transport.
8
9use std::sync::Arc;
10
11use async_trait::async_trait;
12use dashmap::DashMap;
13use serde_json::{Value, json};
14
15use crate::agent::Agent;
16use crate::context::{InvestigationContext, Signal};
17use crate::registry::KernelError;
18use crate::tool::{Tool, ToolSchema};
19
20/// Stable key for a delegate executor. Manifests usually reference this
21/// through `delegates[].agent`; when omitted, `delegates[].name` is used.
22pub type DelegateName = String;
23
24/// Async implementation behind a delegate tool.
25#[async_trait]
26pub trait DelegateExecutor: Send + Sync {
27    async fn invoke(&self, args: Value) -> Result<Value, KernelError>;
28}
29
30/// Registry of in-process delegate executors provided by the host.
31#[derive(Clone, Default)]
32pub struct DelegateRegistry {
33    inner: Arc<DashMap<DelegateName, Arc<dyn DelegateExecutor>>>,
34}
35
36impl DelegateRegistry {
37    pub fn new() -> Self {
38        Self::default()
39    }
40
41    pub fn register(&self, name: impl Into<String>, executor: Arc<dyn DelegateExecutor>) {
42        self.inner.insert(name.into(), executor);
43    }
44
45    pub fn get(&self, name: &str) -> Option<Arc<dyn DelegateExecutor>> {
46        self.inner.get(name).map(|v| v.clone())
47    }
48
49    pub fn len(&self) -> usize {
50        self.inner.len()
51    }
52
53    pub fn is_empty(&self) -> bool {
54        self.inner.is_empty()
55    }
56}
57
58/// Tool wrapper around a delegate executor.
59pub struct DelegateTool {
60    schema: ToolSchema,
61    executor: Arc<dyn DelegateExecutor>,
62}
63
64impl DelegateTool {
65    pub fn new(
66        name: impl Into<String>,
67        description: impl Into<String>,
68        executor: Arc<dyn DelegateExecutor>,
69    ) -> Self {
70        Self {
71            schema: ToolSchema {
72                name: name.into(),
73                description: description.into(),
74                args_schema: json!({"type": "object"}),
75                result_schema: json!({"type": "object"}),
76            },
77            executor,
78        }
79    }
80
81    pub fn with_schema(schema: ToolSchema, executor: Arc<dyn DelegateExecutor>) -> Self {
82        Self { schema, executor }
83    }
84}
85
86#[async_trait]
87impl Tool for DelegateTool {
88    fn schema(&self) -> ToolSchema {
89        self.schema.clone()
90    }
91
92    fn name(&self) -> crate::ToolName {
93        self.schema.name.clone()
94    }
95
96    async fn invoke(&self, args: Value) -> Result<Value, KernelError> {
97        self.executor.invoke(args).await
98    }
99}
100
101/// Delegate executor that drives another [`Agent`] in the same process.
102pub struct InProcessAgentDelegate {
103    agent: Arc<dyn Agent>,
104}
105
106impl InProcessAgentDelegate {
107    pub fn new(agent: Arc<dyn Agent>) -> Self {
108        Self { agent }
109    }
110
111    pub fn arc(agent: Arc<dyn Agent>) -> Arc<dyn DelegateExecutor> {
112        Arc::new(Self::new(agent))
113    }
114}
115
116#[async_trait]
117impl DelegateExecutor for InProcessAgentDelegate {
118    async fn invoke(&self, args: Value) -> Result<Value, KernelError> {
119        let mut ctx = context_from_args(args)?;
120        let result = self.agent.step(&mut ctx).await?;
121        Ok(json!({
122            "delegate_kind": "in_process",
123            "agent": self.agent.name(),
124            "result": {
125                "skills_run": result.skills_run,
126                "skills_skipped": result.skills_skipped,
127                "confidence": result.confidence,
128                "concluded": result.concluded,
129            },
130            "context": ctx,
131        }))
132    }
133}
134
135fn context_from_args(args: Value) -> Result<InvestigationContext, KernelError> {
136    if let Some(context) = args.get("context") {
137        return Ok(serde_json::from_value(context.clone())?);
138    }
139    if let Ok(ctx) = serde_json::from_value::<InvestigationContext>(args.clone()) {
140        return Ok(ctx);
141    }
142
143    let entity_id = args
144        .get("entity_id")
145        .and_then(Value::as_str)
146        .unwrap_or("delegate")
147        .to_string();
148    let partition = args
149        .get("partition")
150        .and_then(Value::as_str)
151        .unwrap_or("default")
152        .to_string();
153    let mut ctx = InvestigationContext::new(entity_id, partition);
154    if let Some(confidence) = args.get("confidence").and_then(Value::as_f64) {
155        ctx.confidence = (confidence as f32).clamp(0.0, 1.0);
156    }
157    if let Some(signals) = args.get("signals").and_then(Value::as_array) {
158        ctx.signals.extend(
159            signals
160                .iter()
161                .filter_map(Value::as_str)
162                .map(|s| Signal::new(s.to_string())),
163        );
164    }
165    Ok(ctx)
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use crate::{GenericAgent, Skill, SkillOutcome, SkillRegistry, ToolRegistry};
172
173    struct ConfidenceSkill;
174
175    #[async_trait]
176    impl Skill for ConfidenceSkill {
177        fn id(&self) -> &str {
178            "test.confidence"
179        }
180
181        fn description(&self) -> &str {
182            "raises confidence"
183        }
184
185        fn applies(&self, ctx: &InvestigationContext) -> bool {
186            ctx.has_signal("raise")
187        }
188
189        async fn execute(
190            &self,
191            _ctx: &mut InvestigationContext,
192            _tools: &ToolRegistry,
193        ) -> Result<SkillOutcome, KernelError> {
194            Ok(SkillOutcome::default().with_delta(0.4))
195        }
196    }
197
198    #[tokio::test]
199    async fn in_process_delegate_drives_child_agent() {
200        let skills = SkillRegistry::new();
201        skills.register(Arc::new(ConfidenceSkill));
202        let tools = ToolRegistry::new();
203        let agent = GenericAgent::builder("child")
204            .with_skills(["test.confidence"])
205            .build(&skills, &tools)
206            .expect("agent");
207        let executor = InProcessAgentDelegate::arc(Arc::new(agent));
208        let tool = DelegateTool::new("child_agent", "child", executor);
209        let out = tool
210            .invoke(json!({
211                "entity_id": "host-a",
212                "partition": "lab",
213                "signals": ["raise"],
214            }))
215            .await
216            .expect("invoke");
217        assert_eq!(out["delegate_kind"], "in_process");
218        assert_eq!(out["agent"], "child");
219        assert_eq!(out["result"]["skills_run"][0], "test.confidence");
220        assert!((out["result"]["confidence"].as_f64().unwrap() - 0.4).abs() < 1e-6);
221    }
222}