1use std::sync::Arc;
2use std::sync::atomic::AtomicUsize;
3
4use async_trait::async_trait;
5use schemars::JsonSchema;
6use serde::Deserialize;
7use serde_json::{Value, json};
8
9use ai_agents_core::{Tool, ToolResult};
10use ai_agents_llm::LLMRegistry;
11use ai_agents_tools::generate_schema;
12
13use super::types::RoutingMethod;
14use crate::spawner::AgentRegistry;
15use crate::turn_context::{current_turn_actor_context, scope_actor_context};
16
17pub struct RouteToAgentTool {
22 registry: Arc<AgentRegistry>,
23 llm: Arc<LLMRegistry>,
24 counter: AtomicUsize,
25}
26
27#[derive(Debug, Deserialize, JsonSchema)]
28#[allow(dead_code)]
29struct RouteToAgentInput {
30 input: String,
32 candidates: Vec<String>,
34 method: Option<String>,
36}
37
38impl RouteToAgentTool {
39 pub fn new(registry: Arc<AgentRegistry>, llm: Arc<LLMRegistry>) -> Self {
40 Self {
41 registry,
42 llm,
43 counter: AtomicUsize::new(0),
44 }
45 }
46}
47
48#[async_trait]
49impl Tool for RouteToAgentTool {
50 fn id(&self) -> &str {
51 "route_to_agent"
52 }
53
54 fn name(&self) -> &str {
55 "Route to Agent"
56 }
57
58 fn description(&self) -> &str {
59 "Send input to the best-matched agent from a set of candidates."
60 }
61
62 fn input_schema(&self) -> Value {
63 generate_schema::<RouteToAgentInput>()
64 }
65
66 async fn execute(&self, args: Value, _ctx: ai_agents_core::ToolExecutionContext) -> ToolResult {
67 let input = match args.get("input").and_then(|v| v.as_str()) {
68 Some(s) => s,
69 None => return ToolResult::error("missing required field: input"),
70 };
71 let candidates: Vec<String> = match args.get("candidates").and_then(|v| v.as_array()) {
72 Some(arr) => arr
73 .iter()
74 .filter_map(|v| v.as_str().map(String::from))
75 .collect(),
76 None => return ToolResult::error("missing required field: candidates"),
77 };
78 let method = match args.get("method").and_then(|v| v.as_str()) {
79 Some("round_robin") => RoutingMethod::RoundRobin,
80 _ => RoutingMethod::Llm,
81 };
82
83 let llm = match self.llm.get("router") {
84 Ok(p) => p,
85 Err(_) => return ToolResult::error("no router LLM configured"),
86 };
87
88 let route_future = super::route(
89 &self.registry,
90 llm.as_ref(),
91 input,
92 &candidates,
93 method,
94 Some(&self.counter),
95 );
96 let route_result = if let Some(context) = current_turn_actor_context() {
97 scope_actor_context(context, route_future).await
98 } else {
99 route_future.await
100 };
101
102 match route_result {
103 Ok(result) => ToolResult::ok(
104 json!({
105 "selected_agent": result.selected_agent,
106 "response": result.response.content,
107 "reason": result.reason,
108 })
109 .to_string(),
110 ),
111 Err(e) => ToolResult::error(format!("routing failed: {}", e)),
112 }
113 }
114}
115
116pub struct PipelineProcessTool {
121 registry: Arc<AgentRegistry>,
122}
123
124#[derive(Debug, Deserialize, JsonSchema)]
125#[allow(dead_code)]
126struct PipelineProcessInput {
127 input: String,
129 stages: Vec<String>,
131 stage_inputs: Option<Vec<Option<String>>>,
133}
134
135impl PipelineProcessTool {
136 pub fn new(registry: Arc<AgentRegistry>) -> Self {
137 Self { registry }
138 }
139}
140
141#[async_trait]
142impl Tool for PipelineProcessTool {
143 fn id(&self) -> &str {
144 "pipeline_process"
145 }
146
147 fn name(&self) -> &str {
148 "Pipeline Process"
149 }
150
151 fn description(&self) -> &str {
152 "Chain agents sequentially. Each agent processes the previous output."
153 }
154
155 fn input_schema(&self) -> Value {
156 generate_schema::<PipelineProcessInput>()
157 }
158
159 async fn execute(&self, args: Value, _ctx: ai_agents_core::ToolExecutionContext) -> ToolResult {
160 let input = match args.get("input").and_then(|v| v.as_str()) {
161 Some(s) => s,
162 None => return ToolResult::error("missing required field: input"),
163 };
164 let stage_ids: Vec<String> = match args.get("stages").and_then(|v| v.as_array()) {
165 Some(arr) => arr
166 .iter()
167 .filter_map(|v| v.as_str().map(String::from))
168 .collect(),
169 None => return ToolResult::error("missing required field: stages"),
170 };
171
172 let stage_inputs: Vec<Option<String>> = args
173 .get("stage_inputs")
174 .and_then(|v| v.as_array())
175 .map(|arr| arr.iter().map(|v| v.as_str().map(String::from)).collect())
176 .unwrap_or_default();
177
178 let pipeline_stages: Vec<super::types::PipelineStage> = stage_ids
179 .into_iter()
180 .enumerate()
181 .map(|(i, id)| {
182 let mut stage = super::types::PipelineStage::id(id);
183 if let Some(tmpl) = stage_inputs.get(i).and_then(|o| o.clone()) {
184 stage = stage.with_input(tmpl);
185 }
186 stage
187 })
188 .collect();
189
190 let pipeline_future =
191 super::pipeline(&self.registry, input, &pipeline_stages, None, None, None);
192 let pipeline_result = if let Some(context) = current_turn_actor_context() {
193 scope_actor_context(context, pipeline_future).await
194 } else {
195 pipeline_future.await
196 };
197
198 match pipeline_result {
199 Ok(result) => ToolResult::ok(
200 json!({
201 "response": result.response.content,
202 "stages": result.stage_outputs.iter().map(|s| {
203 json!({
204 "agent_id": s.agent_id,
205 "output": s.output,
206 "duration_ms": s.duration_ms,
207 "skipped": s.skipped,
208 })
209 }).collect::<Vec<_>>(),
210 })
211 .to_string(),
212 ),
213 Err(e) => ToolResult::error(format!("pipeline failed: {}", e)),
214 }
215 }
216}
217
218pub struct ConcurrentAskTool {
223 registry: Arc<AgentRegistry>,
224 llm: Arc<LLMRegistry>,
225}
226
227#[derive(Debug, Deserialize, JsonSchema)]
228#[allow(dead_code)]
229struct ConcurrentAskInput {
230 question: String,
232 agents: Vec<String>,
234 aggregation: Option<String>,
236}
237
238impl ConcurrentAskTool {
239 pub fn new(registry: Arc<AgentRegistry>, llm: Arc<LLMRegistry>) -> Self {
240 Self { registry, llm }
241 }
242}
243
244#[async_trait]
245impl Tool for ConcurrentAskTool {
246 fn id(&self) -> &str {
247 "concurrent_ask"
248 }
249
250 fn name(&self) -> &str {
251 "Concurrent Ask"
252 }
253
254 fn description(&self) -> &str {
255 "Ask multiple agents the same question in parallel and aggregate results."
256 }
257
258 fn input_schema(&self) -> Value {
259 generate_schema::<ConcurrentAskInput>()
260 }
261
262 async fn execute(&self, args: Value, _ctx: ai_agents_core::ToolExecutionContext) -> ToolResult {
263 let question = match args.get("question").and_then(|v| v.as_str()) {
264 Some(s) => s,
265 None => return ToolResult::error("missing required field: question"),
266 };
267 let agent_ids: Vec<String> = match args.get("agents").and_then(|v| v.as_array()) {
268 Some(arr) => arr
269 .iter()
270 .filter_map(|v| v.as_str().map(String::from))
271 .collect(),
272 None => return ToolResult::error("missing required field: agents"),
273 };
274 let strategy_str = args
275 .get("aggregation")
276 .and_then(|v| v.as_str())
277 .unwrap_or("all");
278
279 let strategy = match strategy_str {
280 "llm_synthesis" => ai_agents_state::AggregationStrategy::LlmSynthesis,
281 "first_wins" => ai_agents_state::AggregationStrategy::FirstWins,
282 "voting" => ai_agents_state::AggregationStrategy::Voting,
283 _ => ai_agents_state::AggregationStrategy::All,
284 };
285
286 let agents: Vec<ai_agents_state::ConcurrentAgentRef> = agent_ids
287 .iter()
288 .map(|id| ai_agents_state::ConcurrentAgentRef::Id(id.clone()))
289 .collect();
290
291 let aggregation = ai_agents_state::AggregationConfig {
292 strategy,
293 synthesizer_llm: Some("router".to_string()),
294 synthesizer_prompt: None,
295 vote: None,
296 };
297
298 let llm = self.llm.get("router").ok();
299
300 let concurrent_future = super::concurrent(
301 &self.registry,
302 question,
303 &agents,
304 &aggregation,
305 llm.as_deref(),
306 None,
307 None,
308 ai_agents_state::PartialFailureAction::ProceedWithAvailable,
309 None,
310 );
311 let concurrent_result = if let Some(context) = current_turn_actor_context() {
312 scope_actor_context(context, concurrent_future).await
313 } else {
314 concurrent_future.await
315 };
316
317 match concurrent_result {
318 Ok(result) => ToolResult::ok(
319 json!({
320 "response": result.response.content,
321 "agent_results": result.agent_results.iter().map(|ar| {
322 json!({
323 "agent_id": ar.agent_id,
324 "response": ar.response.as_ref().map(|r| r.content.as_str()),
325 "success": ar.success,
326 })
327 }).collect::<Vec<_>>(),
328 })
329 .to_string(),
330 ),
331 Err(e) => ToolResult::error(format!("concurrent ask failed: {}", e)),
332 }
333 }
334}
335
336pub struct GroupDiscussionTool {
341 registry: Arc<AgentRegistry>,
342 llm: Arc<LLMRegistry>,
343}
344
345#[derive(Debug, Deserialize, JsonSchema)]
346#[allow(dead_code)]
347struct GroupDiscussionInput {
348 topic: String,
350 participants: Vec<String>,
352 style: Option<String>,
354 max_rounds: Option<u32>,
356}
357
358impl GroupDiscussionTool {
359 pub fn new(registry: Arc<AgentRegistry>, llm: Arc<LLMRegistry>) -> Self {
360 Self { registry, llm }
361 }
362}
363
364#[async_trait]
365impl Tool for GroupDiscussionTool {
366 fn id(&self) -> &str {
367 "group_discussion"
368 }
369
370 fn name(&self) -> &str {
371 "Group Discussion"
372 }
373
374 fn description(&self) -> &str {
375 "Run a multi-agent conversation on a topic with configurable style."
376 }
377
378 fn input_schema(&self) -> Value {
379 generate_schema::<GroupDiscussionInput>()
380 }
381
382 async fn execute(&self, args: Value, _ctx: ai_agents_core::ToolExecutionContext) -> ToolResult {
383 let topic = match args.get("topic").and_then(|v| v.as_str()) {
384 Some(s) => s,
385 None => return ToolResult::error("missing required field: topic"),
386 };
387 let participant_ids: Vec<String> = match args.get("participants").and_then(|v| v.as_array())
388 {
389 Some(arr) => arr
390 .iter()
391 .filter_map(|v| v.as_str().map(String::from))
392 .collect(),
393 None => return ToolResult::error("missing required field: participants"),
394 };
395 let style = match args.get("style").and_then(|v| v.as_str()) {
396 Some("debate") => ai_agents_state::ChatStyle::Debate,
397 Some("consensus") => ai_agents_state::ChatStyle::Consensus,
398 _ => ai_agents_state::ChatStyle::Brainstorm,
399 };
400 let max_rounds = args
401 .get("max_rounds")
402 .and_then(|v| v.as_u64())
403 .map(|v| v as u32)
404 .unwrap_or(3);
405
406 let participants: Vec<ai_agents_state::ChatParticipant> = participant_ids
407 .iter()
408 .map(|id| ai_agents_state::ChatParticipant {
409 id: id.clone(),
410 role: None,
411 })
412 .collect();
413
414 let config = ai_agents_state::GroupChatStateConfig {
415 participants,
416 style,
417 max_rounds,
418 manager: None,
419 termination: ai_agents_state::TerminationConfig {
420 method: ai_agents_state::TerminationMethod::MaxRounds,
421 max_stall_rounds: 2,
422 },
423 debate: None,
424 maker_checker: None,
425 timeout_ms: None,
426 input: None,
427 context_mode: None,
428 };
429
430 let llm = self.llm.get("router").ok();
431
432 let group_future = super::group_chat(&self.registry, topic, &config, llm.as_deref(), None);
433 let group_result = if let Some(context) = current_turn_actor_context() {
434 scope_actor_context(context, group_future).await
435 } else {
436 group_future.await
437 };
438
439 match group_result {
440 Ok(result) => ToolResult::ok(
441 json!({
442 "conclusion": result.response.content,
443 "rounds": result.rounds_completed,
444 "termination": result.termination_reason,
445 "transcript": result.transcript.iter().map(|t| {
446 json!({
447 "speaker": t.speaker,
448 "round": t.round,
449 "content": t.content,
450 })
451 }).collect::<Vec<_>>(),
452 })
453 .to_string(),
454 ),
455 Err(e) => ToolResult::error(format!("group discussion failed: {}", e)),
456 }
457 }
458}
459
460pub struct HandoffConversationTool {
465 registry: Arc<AgentRegistry>,
466 llm: Arc<LLMRegistry>,
467}
468
469#[derive(Debug, Deserialize, JsonSchema)]
470#[allow(dead_code)]
471struct HandoffConversationInput {
472 input: String,
474 initial_agent: String,
476 available_agents: Vec<String>,
478 max_handoffs: Option<u32>,
480}
481
482impl HandoffConversationTool {
483 pub fn new(registry: Arc<AgentRegistry>, llm: Arc<LLMRegistry>) -> Self {
484 Self { registry, llm }
485 }
486}
487
488#[async_trait]
489impl Tool for HandoffConversationTool {
490 fn id(&self) -> &str {
491 "handoff_conversation"
492 }
493
494 fn name(&self) -> &str {
495 "Handoff Conversation"
496 }
497
498 fn description(&self) -> &str {
499 "Start a conversation with one agent and allow dynamic handoffs to other agents."
500 }
501
502 fn input_schema(&self) -> Value {
503 generate_schema::<HandoffConversationInput>()
504 }
505
506 async fn execute(&self, args: Value, _ctx: ai_agents_core::ToolExecutionContext) -> ToolResult {
507 let input = match args.get("input").and_then(|v| v.as_str()) {
508 Some(s) => s,
509 None => return ToolResult::error("missing required field: input"),
510 };
511 let initial_agent = match args.get("initial_agent").and_then(|v| v.as_str()) {
512 Some(s) => s,
513 None => return ToolResult::error("missing required field: initial_agent"),
514 };
515 let available_agents: Vec<String> =
516 match args.get("available_agents").and_then(|v| v.as_array()) {
517 Some(arr) => arr
518 .iter()
519 .filter_map(|v| v.as_str().map(String::from))
520 .collect(),
521 None => return ToolResult::error("missing required field: available_agents"),
522 };
523 let max_handoffs = args
524 .get("max_handoffs")
525 .and_then(|v| v.as_u64())
526 .map(|v| v as u32)
527 .unwrap_or(5);
528
529 let llm = match self.llm.get("router") {
530 Ok(p) => p,
531 Err(_) => return ToolResult::error("no router LLM configured"),
532 };
533
534 let handoff_future = super::handoff(
535 &self.registry,
536 input,
537 initial_agent,
538 &available_agents,
539 max_handoffs,
540 llm.as_ref(),
541 None,
542 );
543 let handoff_result = if let Some(context) = current_turn_actor_context() {
544 scope_actor_context(context, handoff_future).await
545 } else {
546 handoff_future.await
547 };
548
549 match handoff_result {
550 Ok(result) => ToolResult::ok(
551 json!({
552 "response": result.response.content,
553 "final_agent": result.final_agent,
554 "handoffs": result.handoff_chain.iter().map(|h| {
555 json!({
556 "from": h.from_agent,
557 "to": h.to_agent,
558 "reason": h.reason,
559 })
560 }).collect::<Vec<_>>(),
561 })
562 .to_string(),
563 ),
564 Err(e) => ToolResult::error(format!("handoff failed: {}", e)),
565 }
566 }
567}
568
569pub fn configure_orchestration_tools(
571 config: &crate::spec::OrchestrationToolsConfig,
572 registry: Arc<AgentRegistry>,
573 llm: Arc<LLMRegistry>,
574) -> Vec<Arc<dyn Tool>> {
575 let mut tools: Vec<Arc<dyn Tool>> = Vec::new();
576
577 if config.includes("route_to_agent") {
578 tools.push(Arc::new(RouteToAgentTool::new(
579 Arc::clone(®istry),
580 Arc::clone(&llm),
581 )));
582 }
583 if config.includes("pipeline_process") {
584 tools.push(Arc::new(PipelineProcessTool::new(Arc::clone(®istry))));
585 }
586 if config.includes("concurrent_ask") {
587 tools.push(Arc::new(ConcurrentAskTool::new(
588 Arc::clone(®istry),
589 Arc::clone(&llm),
590 )));
591 }
592 if config.includes("group_discussion") {
593 tools.push(Arc::new(GroupDiscussionTool::new(
594 Arc::clone(®istry),
595 Arc::clone(&llm),
596 )));
597 }
598 if config.includes("handoff_conversation") {
599 tools.push(Arc::new(HandoffConversationTool::new(
600 Arc::clone(®istry),
601 Arc::clone(&llm),
602 )));
603 }
604
605 tools
606}