Skip to main content

rig_compose/
delegate.rs

1//! Agent delegation primitives — **the default path for agent-to-agent
2//! collaboration.**
3//!
4//! A delegate is another agent-shaped capability exposed as a normal
5//! [`Tool`]. The parent agent's model still decides when to call it;
6//! the runtime merely provides an implementation. This keeps delegation
7//! agentic while letting hosts run child agents in-process when that is
8//! cheaper than MCP or another remote transport.
9//!
10//! Because the surface is [`Tool`], delegation is symmetric across
11//! transports: a child agent registered in-process today can be moved
12//! behind an MCP server tomorrow with no change to the parent — the
13//! parent emits the same tool call.
14//!
15//! For deterministic, no-LLM routing by signal tag (e.g. an overseer
16//! fanning one specialist out per partition), see
17//! [`crate::coordinator::CoordinatorAgent`] instead.
18
19use std::sync::Arc;
20
21use async_trait::async_trait;
22use dashmap::DashMap;
23use serde::{Deserialize, Serialize};
24use serde_json::{Value, json};
25
26use crate::agent::Agent;
27use crate::context::{InvestigationContext, Signal};
28use crate::registry::KernelError;
29use crate::tool::{Tool, ToolSchema};
30
31/// Stable key for a delegate executor. Manifests usually reference this
32/// through `delegates[].agent`; when omitted, `delegates[].name` is used.
33pub type DelegateName = String;
34
35/// Snapshot record for a registered in-process delegate executor.
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub struct DelegateDescriptor {
38    /// Stable delegate name used by hosts and manifests.
39    pub name: DelegateName,
40}
41
42/// Async implementation behind a delegate tool.
43#[async_trait]
44pub trait DelegateExecutor: Send + Sync {
45    async fn invoke(&self, args: Value) -> Result<Value, KernelError>;
46}
47
48/// Registry of in-process delegate executors provided by the host.
49#[derive(Clone, Default)]
50pub struct DelegateRegistry {
51    inner: Arc<DashMap<DelegateName, Arc<dyn DelegateExecutor>>>,
52}
53
54impl DelegateRegistry {
55    pub fn new() -> Self {
56        Self::default()
57    }
58
59    pub fn register(&self, name: impl Into<String>, executor: Arc<dyn DelegateExecutor>) {
60        self.inner.insert(name.into(), executor);
61    }
62
63    pub fn get(&self, name: &str) -> Option<Arc<dyn DelegateExecutor>> {
64        self.inner.get(name).map(|v| v.clone())
65    }
66
67    pub fn len(&self) -> usize {
68        self.inner.len()
69    }
70
71    pub fn is_empty(&self) -> bool {
72        self.inner.is_empty()
73    }
74
75    /// Deterministic catalog snapshot of every registered delegate executor.
76    pub fn descriptors(&self) -> Vec<DelegateDescriptor> {
77        let mut descriptors: Vec<_> = self
78            .inner
79            .iter()
80            .map(|entry| DelegateDescriptor {
81                name: entry.key().clone(),
82            })
83            .collect();
84        descriptors.sort_by(|left, right| left.name.cmp(&right.name));
85        descriptors
86    }
87}
88
89/// Tool wrapper around a delegate executor.
90pub struct DelegateTool {
91    schema: ToolSchema,
92    executor: Arc<dyn DelegateExecutor>,
93}
94
95impl DelegateTool {
96    pub fn new(
97        name: impl Into<String>,
98        description: impl Into<String>,
99        executor: Arc<dyn DelegateExecutor>,
100    ) -> Self {
101        Self {
102            schema: ToolSchema {
103                name: name.into(),
104                description: description.into(),
105                args_schema: json!({"type": "object"}),
106                result_schema: json!({"type": "object"}),
107            },
108            executor,
109        }
110    }
111
112    pub fn with_schema(schema: ToolSchema, executor: Arc<dyn DelegateExecutor>) -> Self {
113        Self { schema, executor }
114    }
115}
116
117#[async_trait]
118impl Tool for DelegateTool {
119    fn schema(&self) -> ToolSchema {
120        self.schema.clone()
121    }
122
123    fn name(&self) -> crate::ToolName {
124        self.schema.name.clone()
125    }
126
127    async fn invoke(&self, args: Value) -> Result<Value, KernelError> {
128        self.executor.invoke(args).await
129    }
130}
131
132/// Delegate executor that drives another [`Agent`] in the same process.
133pub struct InProcessAgentDelegate {
134    agent: Arc<dyn Agent>,
135}
136
137impl InProcessAgentDelegate {
138    pub fn new(agent: Arc<dyn Agent>) -> Self {
139        Self { agent }
140    }
141
142    pub fn arc(agent: Arc<dyn Agent>) -> Arc<dyn DelegateExecutor> {
143        Arc::new(Self::new(agent))
144    }
145}
146
147#[async_trait]
148impl DelegateExecutor for InProcessAgentDelegate {
149    async fn invoke(&self, args: Value) -> Result<Value, KernelError> {
150        let mut ctx = context_from_args(args)?;
151        let result = self.agent.step(&mut ctx).await?;
152        Ok(json!({
153            "delegate_kind": "in_process",
154            "agent": self.agent.name(),
155            "result": {
156                "skills_run": result.skills_run,
157                "skills_skipped": result.skills_skipped,
158                "confidence": result.confidence,
159                "concluded": result.concluded,
160            },
161            "context": ctx,
162        }))
163    }
164}
165
166fn context_from_args(args: Value) -> Result<InvestigationContext, KernelError> {
167    if let Some(context) = args.get("context") {
168        return Ok(serde_json::from_value(context.clone())?);
169    }
170    if let Ok(ctx) = serde_json::from_value::<InvestigationContext>(args.clone()) {
171        return Ok(ctx);
172    }
173
174    let entity_id = args
175        .get("entity_id")
176        .and_then(Value::as_str)
177        .unwrap_or("delegate")
178        .to_string();
179    let partition = args
180        .get("partition")
181        .and_then(Value::as_str)
182        .unwrap_or("default")
183        .to_string();
184    let mut ctx = InvestigationContext::new(entity_id, partition);
185    if let Some(confidence) = args.get("confidence").and_then(Value::as_f64) {
186        ctx.confidence = (confidence as f32).clamp(0.0, 1.0);
187    }
188    if let Some(signals) = args.get("signals").and_then(Value::as_array) {
189        ctx.signals.extend(
190            signals
191                .iter()
192                .filter_map(Value::as_str)
193                .map(|s| Signal::new(s.to_string())),
194        );
195    }
196    Ok(ctx)
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::{GenericAgent, Skill, SkillOutcome, SkillRegistry, ToolRegistry};
203
204    struct ConfidenceSkill;
205
206    #[async_trait]
207    impl Skill for ConfidenceSkill {
208        fn id(&self) -> &str {
209            "test.confidence"
210        }
211
212        fn description(&self) -> &str {
213            "raises confidence"
214        }
215
216        fn applies(&self, ctx: &InvestigationContext) -> bool {
217            ctx.has_signal("raise")
218        }
219
220        async fn execute(
221            &self,
222            _ctx: &mut InvestigationContext,
223            _tools: &ToolRegistry,
224        ) -> Result<SkillOutcome, KernelError> {
225            Ok(SkillOutcome::default().with_delta(0.4))
226        }
227    }
228
229    #[tokio::test]
230    async fn in_process_delegate_drives_child_agent() {
231        let skills = SkillRegistry::new();
232        skills.register(Arc::new(ConfidenceSkill));
233        let tools = ToolRegistry::new();
234        let agent = GenericAgent::builder("child")
235            .with_skills(["test.confidence"])
236            .build(&skills, &tools)
237            .expect("agent");
238        let executor = InProcessAgentDelegate::arc(Arc::new(agent));
239        let tool = DelegateTool::new("child_agent", "child", executor);
240        let out = tool
241            .invoke(json!({
242                "entity_id": "host-a",
243                "partition": "lab",
244                "signals": ["raise"],
245            }))
246            .await
247            .expect("invoke");
248        assert_eq!(out["delegate_kind"], "in_process");
249        assert_eq!(out["agent"], "child");
250        assert_eq!(out["result"]["skills_run"][0], "test.confidence");
251        assert!((out["result"]["confidence"].as_f64().unwrap() - 0.4).abs() < 1e-6);
252    }
253
254    #[test]
255    fn delegate_registry_descriptors_are_sorted() {
256        let registry = DelegateRegistry::new();
257        let skills = SkillRegistry::new();
258        let tools = ToolRegistry::new();
259        let agent = GenericAgent::builder("child")
260            .build(&skills, &tools)
261            .expect("agent");
262        let executor = InProcessAgentDelegate::arc(Arc::new(agent));
263
264        registry.register("zeta.delegate", executor.clone());
265        registry.register("alpha.delegate", executor);
266
267        let names: Vec<_> = registry
268            .descriptors()
269            .into_iter()
270            .map(|descriptor| descriptor.name)
271            .collect();
272        assert_eq!(names, vec!["alpha.delegate", "zeta.delegate"]);
273    }
274}