1use std::sync::Arc;
20
21use async_trait::async_trait;
22use dashmap::DashMap;
23use serde_json::{Value, json};
24
25use crate::agent::Agent;
26use crate::context::{InvestigationContext, Signal};
27use crate::registry::KernelError;
28use crate::tool::{Tool, ToolSchema};
29
30pub type DelegateName = String;
33
34#[async_trait]
36pub trait DelegateExecutor: Send + Sync {
37 async fn invoke(&self, args: Value) -> Result<Value, KernelError>;
38}
39
40#[derive(Clone, Default)]
42pub struct DelegateRegistry {
43 inner: Arc<DashMap<DelegateName, Arc<dyn DelegateExecutor>>>,
44}
45
46impl DelegateRegistry {
47 pub fn new() -> Self {
48 Self::default()
49 }
50
51 pub fn register(&self, name: impl Into<String>, executor: Arc<dyn DelegateExecutor>) {
52 self.inner.insert(name.into(), executor);
53 }
54
55 pub fn get(&self, name: &str) -> Option<Arc<dyn DelegateExecutor>> {
56 self.inner.get(name).map(|v| v.clone())
57 }
58
59 pub fn len(&self) -> usize {
60 self.inner.len()
61 }
62
63 pub fn is_empty(&self) -> bool {
64 self.inner.is_empty()
65 }
66}
67
68pub struct DelegateTool {
70 schema: ToolSchema,
71 executor: Arc<dyn DelegateExecutor>,
72}
73
74impl DelegateTool {
75 pub fn new(
76 name: impl Into<String>,
77 description: impl Into<String>,
78 executor: Arc<dyn DelegateExecutor>,
79 ) -> Self {
80 Self {
81 schema: ToolSchema {
82 name: name.into(),
83 description: description.into(),
84 args_schema: json!({"type": "object"}),
85 result_schema: json!({"type": "object"}),
86 },
87 executor,
88 }
89 }
90
91 pub fn with_schema(schema: ToolSchema, executor: Arc<dyn DelegateExecutor>) -> Self {
92 Self { schema, executor }
93 }
94}
95
96#[async_trait]
97impl Tool for DelegateTool {
98 fn schema(&self) -> ToolSchema {
99 self.schema.clone()
100 }
101
102 fn name(&self) -> crate::ToolName {
103 self.schema.name.clone()
104 }
105
106 async fn invoke(&self, args: Value) -> Result<Value, KernelError> {
107 self.executor.invoke(args).await
108 }
109}
110
111pub struct InProcessAgentDelegate {
113 agent: Arc<dyn Agent>,
114}
115
116impl InProcessAgentDelegate {
117 pub fn new(agent: Arc<dyn Agent>) -> Self {
118 Self { agent }
119 }
120
121 pub fn arc(agent: Arc<dyn Agent>) -> Arc<dyn DelegateExecutor> {
122 Arc::new(Self::new(agent))
123 }
124}
125
126#[async_trait]
127impl DelegateExecutor for InProcessAgentDelegate {
128 async fn invoke(&self, args: Value) -> Result<Value, KernelError> {
129 let mut ctx = context_from_args(args)?;
130 let result = self.agent.step(&mut ctx).await?;
131 Ok(json!({
132 "delegate_kind": "in_process",
133 "agent": self.agent.name(),
134 "result": {
135 "skills_run": result.skills_run,
136 "skills_skipped": result.skills_skipped,
137 "confidence": result.confidence,
138 "concluded": result.concluded,
139 },
140 "context": ctx,
141 }))
142 }
143}
144
145fn context_from_args(args: Value) -> Result<InvestigationContext, KernelError> {
146 if let Some(context) = args.get("context") {
147 return Ok(serde_json::from_value(context.clone())?);
148 }
149 if let Ok(ctx) = serde_json::from_value::<InvestigationContext>(args.clone()) {
150 return Ok(ctx);
151 }
152
153 let entity_id = args
154 .get("entity_id")
155 .and_then(Value::as_str)
156 .unwrap_or("delegate")
157 .to_string();
158 let partition = args
159 .get("partition")
160 .and_then(Value::as_str)
161 .unwrap_or("default")
162 .to_string();
163 let mut ctx = InvestigationContext::new(entity_id, partition);
164 if let Some(confidence) = args.get("confidence").and_then(Value::as_f64) {
165 ctx.confidence = (confidence as f32).clamp(0.0, 1.0);
166 }
167 if let Some(signals) = args.get("signals").and_then(Value::as_array) {
168 ctx.signals.extend(
169 signals
170 .iter()
171 .filter_map(Value::as_str)
172 .map(|s| Signal::new(s.to_string())),
173 );
174 }
175 Ok(ctx)
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use crate::{GenericAgent, Skill, SkillOutcome, SkillRegistry, ToolRegistry};
182
183 struct ConfidenceSkill;
184
185 #[async_trait]
186 impl Skill for ConfidenceSkill {
187 fn id(&self) -> &str {
188 "test.confidence"
189 }
190
191 fn description(&self) -> &str {
192 "raises confidence"
193 }
194
195 fn applies(&self, ctx: &InvestigationContext) -> bool {
196 ctx.has_signal("raise")
197 }
198
199 async fn execute(
200 &self,
201 _ctx: &mut InvestigationContext,
202 _tools: &ToolRegistry,
203 ) -> Result<SkillOutcome, KernelError> {
204 Ok(SkillOutcome::default().with_delta(0.4))
205 }
206 }
207
208 #[tokio::test]
209 async fn in_process_delegate_drives_child_agent() {
210 let skills = SkillRegistry::new();
211 skills.register(Arc::new(ConfidenceSkill));
212 let tools = ToolRegistry::new();
213 let agent = GenericAgent::builder("child")
214 .with_skills(["test.confidence"])
215 .build(&skills, &tools)
216 .expect("agent");
217 let executor = InProcessAgentDelegate::arc(Arc::new(agent));
218 let tool = DelegateTool::new("child_agent", "child", executor);
219 let out = tool
220 .invoke(json!({
221 "entity_id": "host-a",
222 "partition": "lab",
223 "signals": ["raise"],
224 }))
225 .await
226 .expect("invoke");
227 assert_eq!(out["delegate_kind"], "in_process");
228 assert_eq!(out["agent"], "child");
229 assert_eq!(out["result"]["skills_run"][0], "test.confidence");
230 assert!((out["result"]["confidence"].as_f64().unwrap() - 0.4).abs() < 1e-6);
231 }
232}