1use 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
20pub type DelegateName = String;
23
24#[async_trait]
26pub trait DelegateExecutor: Send + Sync {
27 async fn invoke(&self, args: Value) -> Result<Value, KernelError>;
28}
29
30#[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
58pub 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
101pub 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}