1use std::collections::HashMap;
8use std::sync::Arc;
9
10use serde_json::Value;
11
12use crate::config::AgentConfig;
13use crate::execution::Execute;
14use crate::registry::AgentRegistry;
15use crate::{ContextProviderHandle, ToonAgents};
16#[cfg(feature = "pipeline")]
17use cordis::Plugin;
18
19fn block_on_plugin<S: cordis::Service + 'static>(
20 ctx: &std::sync::Arc<cordis::Context>,
21 svc: S,
22) -> Result<cordis::FiberId, cordis::CordisError> {
23 tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(ctx.plugin(svc)))
24}
25
26pub struct RunTrackerHandle(pub Arc<dyn crate::RunTracker>);
31
32impl cordis::Service for RunTrackerHandle {
33 fn name(&self) -> &'static str {
34 "run_tracker"
35 }
36}
37
38impl RunTrackerHandle {
39 pub fn new(inner: Arc<dyn crate::RunTracker>) -> Self {
41 Self(inner)
42 }
43}
44
45pub struct ToonAgentsHandle(pub Arc<dyn ToonAgents>);
47
48impl cordis::Service for ToonAgentsHandle {
49 fn name(&self) -> &'static str {
50 "toon_agents"
51 }
52}
53
54impl ToonAgentsHandle {
55 pub fn new(inner: Arc<dyn ToonAgents>) -> Self {
57 Self(inner)
58 }
59}
60
61pub struct OverlayAgentConfigs(pub HashMap<String, AgentConfig>);
63
64impl cordis::Service for OverlayAgentConfigs {
65 fn name(&self) -> &'static str {
66 "overlay_agent_configs"
67 }
68}
69
70fn parse_agents(config: &Value) -> Result<HashMap<String, AgentConfig>, cordis::CordisError> {
75 if config.is_null() {
76 return Ok(HashMap::new());
77 }
78 let Some(obj) = config.as_object() else {
79 return Err(cordis::CordisError::Configuration(
80 "Execute config must be a JSON object or null".into(),
81 ));
82 };
83 if obj.is_empty() {
84 return Ok(HashMap::new());
85 }
86 let source = match obj.get("agents") {
87 Some(Value::Null) => return Ok(HashMap::new()),
88 Some(agents) => agents,
89 None => config,
90 };
91 if source.as_object().is_some_and(serde_json::Map::is_empty) {
92 return Ok(HashMap::new());
93 }
94 serde_json::from_value(source.clone()).map_err(|e| {
95 cordis::CordisError::Configuration(format!("invalid Execute agents config: {e}"))
96 })
97}
98
99fn factory_execute(
100 ctx: &Arc<cordis::Context>,
101 config: &Value,
102) -> Result<cordis::FiberId, cordis::CordisError> {
103 let tools = ctx
104 .get::<ares_tools::Tools>()
105 .ok_or_else(|| cordis::CordisError::Configuration("Tools is not on context".into()))?;
106 #[cfg(feature = "postgres")]
107 if ctx.get::<ares_store::TenantRealms>().is_none() {
108 ctx.provide(ares_store::TenantRealms::new(
109 std::any::TypeId::of::<ares_tools::Tools>(),
110 std::any::TypeId::of::<Execute>(),
111 ));
112 }
113
114 let registry = if let Some(existing) = ctx.get::<AgentRegistry>() {
115 existing
116 } else {
117 let mut agents = parse_agents(config)?;
118 if agents.is_empty() {
119 if let Some(overlay_agents) = ctx.get::<OverlayAgentConfigs>() {
120 agents = overlay_agents.0.clone();
121 }
122 }
123 let providers = match ctx.get::<ares_llm::Llm>() {
124 Some(llm) => llm.registry(),
125 None => Arc::new(ares_llm::ProviderRegistry::new()),
126 };
127 let registry = if let Some(toon) = ctx.get::<ToonAgentsHandle>() {
128 Arc::new(AgentRegistry::with_dynamic_config(
129 agents,
130 providers,
131 Arc::clone(&tools),
132 Arc::clone(&toon.0),
133 ))
134 } else {
135 Arc::new(AgentRegistry::from_config(
136 agents,
137 providers,
138 Arc::clone(&tools),
139 ))
140 };
141 ctx.provide_arc(Arc::clone(®istry));
142 registry
143 };
144
145 if ctx.get::<crate::EmergencyStop>().is_none() {
146 ctx.provide(crate::EmergencyStop::new(false));
147 }
148
149 #[cfg(feature = "postgres")]
150 if ctx.get::<crate::skills::SkillEngine>().is_none() {
151 if let (Some(pg), Some(llm)) = (
152 ctx.get::<ares_store::PostgresClient>(),
153 ctx.get::<ares_llm::Llm>(),
154 ) {
155 ctx.provide_arc(Arc::new(crate::skills::SkillEngine::new(
156 pg.pool.clone(),
157 Arc::clone(&tools),
158 llm,
159 )));
160 }
161 }
162 let _ = tools;
163
164 let mut execute = Execute::new().with_agent_registry(registry);
165 if let Some(handle) = ctx.get::<RunTrackerHandle>() {
166 execute = execute.with_run_tracker(Arc::clone(&handle.0));
167 }
168 if let Some(handle) = ctx.get::<ContextProviderHandle>() {
169 execute = execute.with_context_provider(Arc::clone(handle.inner()));
170 }
171 block_on_plugin(ctx, execute)
172}
173
174pub fn register_plugins(reg: &cordis::PluginRegistry) {
177 reg.register("Execute", Arc::new(factory_execute));
178 #[cfg(feature = "scheduler")]
179 reg.register("SchedulerService", Arc::new(factory_scheduler));
180 #[cfg(feature = "pipeline")]
181 reg.register("PipelineService", Arc::new(factory_pipeline));
182 #[cfg(feature = "trigger")]
183 reg.register("TriggerService", Arc::new(factory_trigger));
184}
185
186#[cfg(feature = "inventory")]
187inventory::submit! {
188 cordis::CordisPluginFactory { name: "Execute", make: factory_execute }
189}
190#[cfg(all(feature = "inventory", feature = "scheduler"))]
191inventory::submit! {
192 cordis::CordisPluginFactory { name: "SchedulerService", make: factory_scheduler }
193}
194#[cfg(all(feature = "inventory", feature = "pipeline"))]
195inventory::submit! {
196 cordis::CordisPluginFactory { name: "PipelineService", make: factory_pipeline }
197}
198#[cfg(all(feature = "inventory", feature = "trigger"))]
199inventory::submit! {
200 cordis::CordisPluginFactory { name: "TriggerService", make: factory_trigger }
201}
202
203fn inject_sync<T: cordis::Service + 'static>(ctx: &Arc<cordis::Context>) -> Arc<T> {
204 tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(ctx.inject::<T>()))
205}
206
207#[cfg(feature = "scheduler")]
208fn factory_scheduler(
209 ctx: &Arc<cordis::Context>,
210 config: &Value,
211) -> Result<cordis::FiberId, cordis::CordisError> {
212 let cfg: crate::scheduler::SchedulerConfig =
213 if config.is_null() || config.as_object().is_some_and(|o| o.is_empty()) {
214 crate::scheduler::SchedulerConfig::default()
215 } else {
216 serde_json::from_value(config.clone()).unwrap_or_default()
217 };
218 let db = inject_sync::<ares_store::PostgresClient>(ctx);
219 let execution = inject_sync::<Execute>(ctx);
220 block_on_plugin(
221 ctx,
222 crate::scheduler::SchedulerService::new(db, execution, cfg.tick_ms),
223 )
224}
225
226#[cfg(feature = "pipeline")]
227fn factory_pipeline(
228 ctx: &Arc<cordis::Context>,
229 config: &Value,
230) -> Result<cordis::FiberId, cordis::CordisError> {
231 let cfg: crate::pipeline::PipelineConfig =
232 if config.is_null() || config.as_object().is_some_and(|o| o.is_empty()) {
233 crate::pipeline::PipelineConfig::default()
234 } else {
235 serde_json::from_value(config.clone()).unwrap_or_default()
236 };
237 let _ = crate::pipeline::PipelinePlugin.apply(ctx, cfg)?;
238 let db = inject_sync::<ares_store::PostgresClient>(ctx);
239 let execution = inject_sync::<Execute>(ctx);
240 block_on_plugin(ctx, crate::pipeline::PipelineService::new(db, execution))
241}
242
243#[cfg(feature = "trigger")]
244fn factory_trigger(
245 ctx: &Arc<cordis::Context>,
246 _config: &Value,
247) -> Result<cordis::FiberId, cordis::CordisError> {
248 let db = inject_sync::<ares_store::PostgresClient>(ctx);
249 let execution = inject_sync::<Execute>(ctx);
250 block_on_plugin(ctx, crate::trigger::TriggerService::new(db, execution))
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256 use cordis::PluginRegistry;
257 use serde_json::json;
258
259 #[test]
260 fn register_plugins_registers_execute() {
261 let reg = PluginRegistry::new();
262 register_plugins(®);
263 assert!(reg.get("Execute").is_some());
264 assert!(reg.get("AgentRegistry").is_none());
265 assert!(reg.get("ExecutionStack").is_none());
266 assert!(reg.get("ServerRuntime").is_none());
267 #[cfg(feature = "scheduler")]
268 assert!(reg.get("SchedulerService").is_some());
269 #[cfg(not(feature = "scheduler"))]
270 assert!(reg.get("SchedulerService").is_none());
271 #[cfg(feature = "pipeline")]
272 assert!(reg.get("PipelineService").is_some());
273 #[cfg(feature = "trigger")]
274 assert!(reg.get("TriggerService").is_some());
275 }
276
277 #[test]
278 fn parse_agents_null_and_empty() {
279 assert!(parse_agents(&Value::Null).unwrap().is_empty());
280 assert!(parse_agents(&json!({})).unwrap().is_empty());
281 assert!(parse_agents(&json!({"agents": null})).unwrap().is_empty());
282 assert!(parse_agents(&json!({"agents": {}})).unwrap().is_empty());
283 }
284
285 #[test]
286 fn parse_agents_raw_map_and_wrapped() {
287 let raw = json!({
288 "research": { "model": "gpt-4", "system_prompt": "be helpful" }
289 });
290 let wrapped = json!({
291 "agents": {
292 "research": { "model": "gpt-4", "system_prompt": "be helpful" }
293 }
294 });
295 let from_raw = parse_agents(&raw).unwrap();
296 let from_wrapped = parse_agents(&wrapped).unwrap();
297 assert_eq!(from_raw["research"].model, "gpt-4");
298 assert_eq!(from_wrapped["research"].model, "gpt-4");
299 assert_eq!(
300 from_raw["research"].system_prompt.as_deref(),
301 Some("be helpful")
302 );
303 }
304}