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