Coverage for agentos/swarm/coordinator.py: 25%

654 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-08 20:40 +0800

1""" # noqa: E501 

2v1.9.8: Smart Swarm Coordinator with tool registry + intelligent routing. 

3 

4Full intelligence stack: 

5- TaskDecomposer: decompose complex tasks into sub-task DAGs 

6- ResultFusion: LLM-as-Judge aggregation with confidence scoring 

7- EvalFeedbackLoop: execute → evaluate → retry → converge 

8- CodeSandbox: safe code generation & execution with test cases 

9- HumanLoop: human-in-the-loop breakpoints for approval/intervention 

10- AgentMonitor: quality gates + self-monitoring pipeline 

11- ExecutionTrace: full span-tree observability + bottleneck detection 

12- AgentMemory: three-tier memory (working/short-term/long-term) + context window 

13- ToolRegistry: schema-based tool catalog with versioning, capabilities, search 

14- ToolRouter: intelligent tool selection with LLM + semantic matching 

15- ToolExecutor: safe tool execution with validation, rate limiting, destructive confirmation 

16""" 

17 

18from __future__ import annotations 

19 

20import asyncio 

21import json 

22import time 

23import uuid 

24from collections import defaultdict 

25from collections.abc import Callable 

26from dataclasses import dataclass, field 

27from enum import StrEnum 

28from typing import Any 

29 

30from agentos.core.di import Agent 

31from agentos.security.guard import ( 

32 GuardPipeline, 

33 create_strict_guard, 

34) 

35from agentos.swarm.agent_memory import ( 

36 AgentMemory, 

37) 

38from agentos.swarm.agent_monitor import ( 

39 AgentMonitor, 

40 MonitorReport, 

41 QualityGate, 

42 no_error_output, 

43 output_not_empty, 

44) 

45from agentos.swarm.code_sandbox import CodeFeedbackExtractor, CodeSandbox, SandboxResult, TestCase 

46from agentos.swarm.eval_feedback_loop import EvalFeedbackLoop, LoopResult, RetryConfig 

47from agentos.swarm.execution_trace import ( 

48 ExecutionTrace, 

49 TraceCollector, 

50 TraceEvent, 

51) 

52from agentos.swarm.human_loop import ( 

53 BreakpointType, 

54 HITLManager, 

55 HumanDecision, 

56) 

57from agentos.swarm.result_fusion import FusedResult, ResultFusion 

58from agentos.swarm.task_decomposer import Decomposition, TaskDecomposer 

59from agentos.swarm.tool_registry import ( 

60 RoutingContext, 

61 RoutingDecision, 

62 ToolCategory, 

63 ToolExecutor, 

64 ToolParam, 

65 ToolRegistry, 

66 ToolRouter, 

67 ToolSchema, 

68 create_tool, 

69) 

70 

71 

72class SwarmTopology(StrEnum): 

73 """Swarm topology types.""" 

74 

75 STAR = "star" # Central coordinator 

76 RING = "ring" # Circular message passing 

77 MESH = "mesh" # All-to-all communication 

78 TREE = "tree" # Hierarchical structure 

79 DAG = "dag" # Workflow-based dependencies 

80 HYBRID = "hybrid" # Dynamic topology switching 

81 

82 

83class ExecutionMode(StrEnum): 

84 """Execution strategy for the coordinator.""" 

85 

86 RAW = "raw" # Original topology-only execution 

87 SMART = "smart" # Decompose → Execute DAG → Fuse 

88 FEEDBACK = "feedback" # Smart + eval feedback loop 

89 

90 

91@dataclass 

92class AgentRole: 

93 """Agent 角色定义。""" 

94 

95 name: str 

96 goal: str 

97 backstory: str = "" 

98 tools: list[str] = field(default_factory=list) 

99 model: str = "auto" 

100 temperature: float = 0.7 

101 allow_delegation: bool = True 

102 verbose: bool = False 

103 

104 

105class MessageBus: 

106 """Agent 间消息总线 — 黑板模式。""" 

107 

108 def __init__(self): 

109 self._messages: list[dict] = [] 

110 self._subscribers: dict[str, list[Callable]] = {} 

111 self._shared_memory: dict[str, Any] = {} 

112 

113 def publish(self, sender: str, topic: str, data: dict): 

114 msg = {"sender": sender, "topic": topic, "data": data} 

115 self._messages.append(msg) 

116 if topic in self._subscribers: 

117 for cb in self._subscribers[topic]: 

118 cb(msg) 

119 

120 def subscribe(self, topic: str, callback: Callable): 

121 self._subscribers.setdefault(topic, []).append(callback) 

122 

123 @property 

124 def messages(self) -> list[dict]: 

125 return self._messages 

126 

127 @property 

128 def shared_memory(self) -> dict[str, Any]: 

129 return self._shared_memory 

130 

131 

132@dataclass 

133class SwarmMessage: 

134 """ 

135 Message in swarm communication. 

136 

137 Attributes: 

138 id: Unique identifier 

139 sender: Sender agent name 

140 receiver: Receiver agent name (None = broadcast) 

141 content: Message content 

142 metadata: Additional metadata 

143 timestamp: Message timestamp 

144 """ 

145 

146 id: str = field(default_factory=lambda: uuid.uuid4().hex[:12]) 

147 sender: str = "" 

148 receiver: str | None = None 

149 content: Any = None 

150 metadata: dict[str, Any] = field(default_factory=dict) 

151 timestamp: float = field(default_factory=time.time) 

152 

153 def to_dict(self) -> dict[str, Any]: 

154 """Convert to dict.""" 

155 return { 

156 "id": self.id, 

157 "sender": self.sender, 

158 "receiver": self.receiver, 

159 "content": self.content, 

160 "metadata": self.metadata, 

161 "timestamp": self.timestamp, 

162 } 

163 

164 

165@dataclass 

166class SwarmResult: 

167 """ 

168 Result of swarm execution. 

169 

170 Attributes: 

171 id: Unique identifier 

172 topology: Swarm topology 

173 mode: Execution mode used 

174 outputs: Agent outputs 

175 messages: Communication messages 

176 duration: Execution duration 

177 success: Whether execution succeeded 

178 fused: ResultFusion output (smart mode only) 

179 decomposition: Task decomposition used (smart mode only) 

180 feedback_loop: Feedback loop result (feedback mode only) 

181 """ 

182 

183 id: str = field(default_factory=lambda: uuid.uuid4().hex[:12]) 

184 topology: SwarmTopology = SwarmTopology.STAR 

185 mode: ExecutionMode = ExecutionMode.RAW 

186 outputs: dict[str, Any] = field(default_factory=dict) 

187 messages: list[SwarmMessage] = field(default_factory=list) 

188 duration: float = 0.0 

189 success: bool = True 

190 fused: FusedResult | None = None 

191 decomposition: Decomposition | None = None 

192 feedback_loop: LoopResult | None = None 

193 

194 def to_dict(self) -> dict[str, Any]: 

195 """Convert to dict.""" 

196 d: dict[str, Any] = { 

197 "id": self.id, 

198 "topology": self.topology.value, 

199 "mode": self.mode.value, 

200 "outputs": self.outputs, 

201 "messages": [m.to_dict() for m in self.messages], 

202 "duration": f"{self.duration:.2f}s", 

203 "success": self.success, 

204 } 

205 if self.fused: 

206 d["fused"] = { 

207 "action": self.fused.action, 

208 "confidence": self.fused.confidence, 

209 "reason": self.fused.reason, 

210 } 

211 if self.decomposition: 

212 d["decomposition"] = { 

213 "sub_tasks": [st.to_dict() for st in self.decomposition.sub_tasks], 

214 "total_steps": self.decomposition.total_steps, 

215 } 

216 if self.feedback_loop: 

217 d["feedback_loop"] = { 

218 "attempts": self.feedback_loop.attempts, 

219 "best_score": self.feedback_loop.best_score, 

220 "converged": self.feedback_loop.converged, 

221 "duration": f"{self.feedback_loop.duration:.2f}s", 

222 } 

223 return d 

224 

225 

226# ── Swarm Agent Role Enum (v1.16.2, migrated from orchestration/swarm_coordinator.py) ─ 

227 

228 

229class SwarmAgentRole(StrEnum): 

230 """Role of an agent within a swarm (enum-based, distinct from AgentRole dataclass).""" 

231 

232 COORDINATOR = "coordinator" 

233 WORKER = "worker" 

234 REVIEWER = "reviewer" 

235 OBSERVER = "observer" 

236 SPECIALIST = "specialist" 

237 

238 

239class TaskPriority(StrEnum): 

240 """Priority level for swarm tasks.""" 

241 

242 CRITICAL = "critical" 

243 HIGH = "high" 

244 MEDIUM = "medium" 

245 LOW = "low" 

246 

247 

248class TaskStatus(StrEnum): 

249 """Execution status of a swarm task.""" 

250 

251 PENDING = "pending" 

252 ASSIGNED = "assigned" 

253 RUNNING = "running" 

254 COMPLETED = "completed" 

255 FAILED = "failed" 

256 CANCELED = "canceled" 

257 

258 

259@dataclass 

260class SwarmAgentInfo: 

261 """Metadata about a swarm agent (v1.16.2, migrated from orchestration).""" 

262 

263 agent_id: str 

264 role: SwarmAgentRole 

265 capabilities: list[str] = field(default_factory=list) 

266 model: str = "" 

267 max_concurrency: int = 3 

268 current_load: int = 0 

269 is_alive: bool = True 

270 last_heartbeat: float = 0.0 

271 metadata: dict[str, Any] = field(default_factory=dict) 

272 

273 @property 

274 def is_available(self) -> bool: 

275 return self.is_alive and self.current_load < self.max_concurrency 

276 

277 

278@dataclass 

279class SwarmTask: 

280 """A task to be executed by the swarm (v1.16.2, migrated from orchestration).""" 

281 

282 task_id: str 

283 description: str 

284 priority: TaskPriority = TaskPriority.MEDIUM 

285 status: TaskStatus = TaskStatus.PENDING 

286 assigned_to: str = "" 

287 required_capabilities: list[str] = field(default_factory=list) 

288 parent_task_id: str = "" 

289 dependencies: list[str] = field(default_factory=list) 

290 result: Any = None 

291 error: str = "" 

292 started_at: float = 0.0 

293 completed_at: float = 0.0 

294 retry_count: int = 0 

295 max_retries: int = 3 

296 metadata: dict[str, Any] = field(default_factory=dict) 

297 

298 @property 

299 def is_ready(self) -> bool: 

300 return self.status == TaskStatus.PENDING and not self.dependencies 

301 

302 @property 

303 def duration_ms(self) -> float: 

304 if self.completed_at and self.started_at: 

305 return (self.completed_at - self.started_at) * 1000 

306 return 0.0 

307 

308 

309# ── Dynamic Task Allocator ────────────────────────────────────── 

310 

311 

312class TaskAllocator: 

313 """Workload-aware dynamic task allocation (v1.16.2, migrated from orchestration). 

314 

315 Considers: capabilities, load, priority, affinity. 

316 """ 

317 

318 def __init__(self): 

319 self._assignments: dict[str, str] = {} 

320 

321 def allocate( 

322 self, 

323 task: SwarmTask, 

324 agents: list[SwarmAgentInfo], 

325 ) -> str | None: 

326 available = [a for a in agents if a.is_available] 

327 if not available: 

328 return None 

329 

330 scored: list[tuple[SwarmAgentInfo, float]] = [] 

331 for agent in available: 

332 score = 0.0 

333 

334 if task.required_capabilities: 

335 match = len(set(task.required_capabilities) & set(agent.capabilities)) 

336 total = len(task.required_capabilities) 

337 score += (match / total) * 50 if total > 0 else 25 

338 

339 score -= agent.current_load * 10 

340 

341 if agent.role == SwarmAgentRole.SPECIALIST: 

342 if any(cap in agent.capabilities for cap in task.required_capabilities): 

343 score += 20 

344 

345 if task.parent_task_id and self._assignments.get(task.parent_task_id) == agent.agent_id: 

346 score += 15 

347 

348 scored.append((agent, score)) 

349 

350 scored.sort(key=lambda x: x[1], reverse=True) 

351 

352 if scored and scored[0][1] > 0: 

353 best = scored[0][0] 

354 self._assignments[task.task_id] = best.agent_id 

355 return best.agent_id 

356 

357 return available[0].agent_id if available else None 

358 

359 

360# ── Conflict Resolver ─────────────────────────────────────────── 

361 

362 

363class ConflictType(StrEnum): 

364 """Type of conflict between agent outputs.""" 

365 

366 FACTUAL = "factual" 

367 METHODOLOGICAL = "methodological" 

368 OUTPUT = "output" 

369 RESOURCE = "resource" 

370 

371 

372class ConflictResolver: 

373 """Detect and resolve conflicts between agents (v1.16.2, migrated from orchestration). 

374 

375 Strategies: majority vote, weighted vote, ranked choice, escalation. 

376 """ 

377 

378 def __init__(self): 

379 self._conflict_log: list[dict] = [] 

380 

381 def detect_conflict( 

382 self, 

383 agent_outputs: dict[str, Any], 

384 expected_type: str = "text", 

385 ) -> list[dict]: 

386 conflicts = [] 

387 agents = list(agent_outputs.keys()) 

388 if len(agents) < 2: 

389 return conflicts 

390 

391 outputs = list(agent_outputs.values()) 

392 

393 if all(isinstance(o, str) for o in outputs): 

394 for i in range(len(outputs)): 

395 for j in range(i + 1, len(outputs)): 

396 similarity = self._text_similarity(outputs[i], outputs[j]) 

397 if similarity < 0.3: 

398 conflicts.append( 

399 { 

400 "type": ConflictType.OUTPUT.value, 

401 "agents": [agents[i], agents[j]], 

402 "similarity": similarity, 

403 "outputs": { 

404 agents[i]: outputs[i][:200], 

405 agents[j]: outputs[j][:200], 

406 }, 

407 } 

408 ) 

409 

410 elif all(isinstance(o, (int, float)) for o in outputs): 

411 values = outputs 

412 mean_val = sum(values) / len(values) 

413 for i, val in enumerate(values): 

414 if abs(val - mean_val) / max(abs(mean_val), 1) > 0.5: 

415 conflicts.append( 

416 { 

417 "type": ConflictType.FACTUAL.value, 

418 "agents": [agents[i]], 

419 "value": val, 

420 "mean": mean_val, 

421 "deviation": abs(val - mean_val) / max(abs(mean_val), 1), 

422 } 

423 ) 

424 

425 return conflicts 

426 

427 def resolve( 

428 self, 

429 agent_outputs: dict[str, Any], 

430 weights: dict[str, float] | None = None, 

431 strategy: str = "majority", 

432 expected_type: str = "text", 

433 ) -> dict[str, Any]: 

434 if len(agent_outputs) == 1: 

435 agent_id = list(agent_outputs.keys())[0] 

436 return {"output": agent_outputs[agent_id], "method": "single_agent", "conflict": False} 

437 

438 outputs = list(agent_outputs.values()) 

439 

440 if all(isinstance(o, str) for o in outputs): 

441 return self._resolve_text(agent_outputs, weights, strategy) 

442 elif all(isinstance(o, (int, float)) for o in outputs): 

443 return self._resolve_numeric(agent_outputs, weights, strategy) 

444 else: 

445 return {"output": outputs[0], "method": "first", "conflict": True} 

446 

447 def _resolve_text( 

448 self, outputs: dict[str, str], weights: dict[str, float] | None, strategy: str 

449 ) -> dict: 

450 if strategy == "majority": 

451 votes: dict[str, list[str]] = defaultdict(list) 

452 agent_ids = list(outputs.keys()) 

453 for i, a1 in enumerate(agent_ids): 

454 best_match = a1 

455 best_sim = 0 

456 for j, a2 in enumerate(agent_ids): 

457 if i == j: 

458 continue 

459 sim = self._text_similarity(outputs[a1], outputs[a2]) 

460 if sim > best_sim: 

461 best_sim = sim 

462 best_match = a2 

463 key = outputs[best_match][:50] 

464 votes[key].append(a1) 

465 

466 winning_key = max(votes, key=lambda k: len(votes[k])) 

467 winning_agent = votes[winning_key][0] 

468 return { 

469 "output": outputs[winning_agent], 

470 "method": "majority", 

471 "votes": {k: len(v) for k, v in votes.items()}, 

472 "conflict": len(votes) > 1, 

473 } 

474 

475 elif strategy == "weighted": 

476 if not weights: 

477 return self._resolve_text(outputs, weights, "majority") 

478 best_agent = max(weights, key=weights.get) 

479 return { 

480 "output": outputs.get(best_agent, list(outputs.values())[0]), 

481 "method": "weighted", 

482 "conflict": False, 

483 } 

484 

485 else: 

486 return {"output": list(outputs.values())[0], "method": "first", "conflict": False} 

487 

488 def _resolve_numeric( 

489 self, outputs: dict[str, float], weights: dict[str, float] | None, strategy: str 

490 ) -> dict: 

491 values = list(outputs.values()) 

492 agents = list(outputs.keys()) 

493 

494 if strategy == "weighted" and weights: 

495 total_weight = sum(weights.get(a, 1.0) for a in agents) 

496 weighted = sum(weights.get(a, 1.0) * outputs[a] for a in agents) / total_weight 

497 return {"output": weighted, "method": "weighted_average", "conflict": False} 

498 else: 

499 avg = sum(values) / len(values) 

500 return {"output": avg, "method": "average", "conflict": False} 

501 

502 def _text_similarity(self, a: str, b: str) -> float: 

503 if a == b: 

504 return 1.0 

505 tokens_a = set(a.lower().split()) 

506 tokens_b = set(b.lower().split()) 

507 if not tokens_a or not tokens_b: 

508 return 0.0 

509 intersection = tokens_a & tokens_b 

510 union = tokens_a | tokens_b 

511 return len(intersection) / len(union) if union else 0.0 

512 

513 

514class SmartSwarmCoordinator: 

515 """ 

516 v1.9.4: Multi-agent coordination with intelligent orchestration. 

517 

518 Upgrades the coordinator with: 

519 - TaskDecomposer: LLM-driven sub-task DAG decomposition 

520 - ResultFusion: LLM-as-Judge aggregation with confidence scoring 

521 - EvalFeedbackLoop: execute → evaluate → retry → converge 

522 

523 Usage: 

524 coordinator = SmartSwarmCoordinator(topology=SwarmTopology.MESH) 

525 coordinator.register(agent1) 

526 coordinator.register(agent2) 

527 

528 # Smart mode with decomposition + fusion 

529 result = await coordinator.smart_execute("complex research task") 

530 

531 # Feedback mode with evaluation retry loop 

532 result = await coordinator.execute_with_feedback( 

533 task, expected_output, scorer 

534 ) 

535 """ 

536 

537 def __init__( 

538 self, 

539 topology: SwarmTopology = SwarmTopology.STAR, 

540 max_rounds: int = 10, 

541 execution_mode: ExecutionMode = ExecutionMode.SMART, 

542 decomposer: TaskDecomposer | None = None, 

543 fusion: ResultFusion | None = None, 

544 feedback_loop: EvalFeedbackLoop | None = None, 

545 sandbox: CodeSandbox | None = None, 

546 hitl_manager: HITLManager | None = None, 

547 monitor: AgentMonitor | None = None, 

548 trace_collector: TraceCollector | None = None, 

549 memory: AgentMemory | None = None, 

550 tool_registry: ToolRegistry | None = None, 

551 tool_router: ToolRouter | None = None, 

552 tool_executor: ToolExecutor | None = None, 

553 guard: GuardPipeline | None = None, 

554 ): 

555 """ 

556 Initialize smart swarm coordinator. 

557 

558 Args: 

559 topology: Swarm topology 

560 max_rounds: Maximum communication rounds 

561 execution_mode: Default execution mode 

562 decomposer: TaskDecomposer instance (created if None) 

563 fusion: ResultFusion instance (created if None) 

564 feedback_loop: EvalFeedbackLoop instance (created if None) 

565 sandbox: CodeSandbox instance for code execution (created if None) 

566 hitl_manager: HITLManager for human-in-the-loop (created if None) 

567 monitor: AgentMonitor for quality gating (created if None) 

568 trace_collector: TraceCollector for execution traces (created if None) 

569 memory: AgentMemory for layered memory (created if None) 

570 tool_registry: ToolRegistry for tool catalog (created if None) 

571 tool_router: ToolRouter for intelligent tool selection (created if None) 

572 tool_executor: ToolExecutor for safe tool execution (created if None) 

573 guard: GuardPipeline for input/output safety filtering (created if None) 

574 """ 

575 self.topology = topology 

576 self.max_rounds = max_rounds 

577 self.execution_mode = execution_mode 

578 self._agents: dict[str, Agent[Any, Any]] = {} 

579 self._message_queue: list[SwarmMessage] = [] 

580 

581 self.decomposer = decomposer or TaskDecomposer() 

582 self.fusion = fusion or ResultFusion() 

583 self.feedback = feedback_loop or EvalFeedbackLoop() 

584 self.sandbox = sandbox or CodeSandbox() 

585 self.hitl = hitl_manager or HITLManager() 

586 self.monitor = monitor or AgentMonitor() 

587 self.tracer = trace_collector or TraceCollector() 

588 self.memory = memory or AgentMemory() 

589 self.tool_registry = tool_registry or ToolRegistry() 

590 self.tool_router = tool_router or ToolRouter(self.tool_registry) 

591 self.tool_executor = tool_executor or ToolExecutor(self.tool_registry) 

592 self.guard = guard or create_strict_guard() 

593 

594 # Original topology methods bound for backward compatibility 

595 self._topo_handlers = { 

596 SwarmTopology.STAR: self._execute_star, 

597 SwarmTopology.RING: self._execute_ring, 

598 SwarmTopology.MESH: self._execute_mesh, 

599 SwarmTopology.TREE: self._execute_tree, 

600 } 

601 

602 # ── Agent management ────────────────────────────────────────── 

603 

604 def register(self, agent: Agent[Any, Any]) -> None: 

605 self._agents[agent.name] = agent 

606 

607 def unregister(self, agent_name: str) -> bool: 

608 if agent_name in self._agents: 

609 del self._agents[agent_name] 

610 return True 

611 return False 

612 

613 def get_agent(self, agent_name: str) -> Agent[Any, Any] | None: 

614 return self._agents.get(agent_name) 

615 

616 def list_agents(self) -> list[str]: 

617 return list(self._agents.keys()) 

618 

619 # ── Execution API ───────────────────────────────────────────── 

620 

621 async def execute( 

622 self, 

623 task: Any, 

624 mode: ExecutionMode | None = None, 

625 **metadata, 

626 ) -> SwarmResult: 

627 """Execute a task. Delegates to smart_execute or raw topology.""" 

628 mode = mode or self.execution_mode 

629 if mode == ExecutionMode.SMART: 

630 return await self.smart_execute(task, **metadata) 

631 return await self._execute_raw(task, **metadata) 

632 

633 async def smart_execute( 

634 self, 

635 task: Any, 

636 _trace: ExecutionTrace | None = None, 

637 **metadata, 

638 ) -> SwarmResult: 

639 """Smart execution: decompose → execute DAG → fuse. 

640 

641 Uses ExecutionTrace for observability when tracer is available. 

642 

643 Args: 

644 task: Task description (string or structured) 

645 _trace: Optional trace to attach (auto-created if self.tracer exists) 

646 **metadata: Additional metadata 

647 

648 Returns: 

649 SwarmResult with fused output and decomposition trace 

650 """ 

651 start_time = time.time() 

652 task_str = str(task) 

653 

654 # Step 0: Security guard — input filtering 

655 guard_result = self.guard.process_input(task_str) 

656 if guard_result.blocked: 

657 result = SwarmResult( 

658 topology=self.topology, 

659 mode=ExecutionMode.SMART, 

660 output=f"[BLOCKED] Input rejected by guard: {guard_result.blocked_by}. Reason: {', '.join(guard_result.warnings)}", # noqa: E501 

661 completed=False, 

662 ) 

663 return result 

664 if guard_result.final_content != task_str: 

665 task_str = guard_result.final_content # PII-redacted version 

666 

667 # Trace setup 

668 trace = _trace 

669 if trace is None and self.tracer is not None: 

670 trace = ExecutionTrace(task_name=task_str[:80]) 

671 self.tracer.add(trace) 

672 

673 if trace: 

674 root = trace.start_span( 

675 TraceEvent.TASK_START, name="smart_execute", data={"task": task_str} 

676 ) 

677 

678 result = SwarmResult( 

679 topology=self.topology, 

680 mode=ExecutionMode.SMART, 

681 ) 

682 

683 agent_names = self.list_agents() 

684 

685 # Step 0: Load memory context 

686 self.memory.set_task(task_str) 

687 memory_context = self.memory.get_context(query=task_str) if self.memory else "" 

688 

689 # Step 1: Decompose 

690 if trace: 

691 dspan = trace.start_span(TraceEvent.DECOMPOSE, name="decompose") 

692 decomp = self.decomposer.decompose(task_str, agents=agent_names) 

693 result.decomposition = decomp 

694 if trace and dspan: 

695 trace.end_span(dspan.id, status="done", data={"sub_tasks": len(decomp.sub_tasks)}) 

696 

697 # Step 2: Execute sub-tasks in dependency order 

698 sub_outputs: dict[str, dict[str, Any]] = {} 

699 completed: set[str] = set() 

700 

701 for _round in range(self.max_rounds): 

702 ready = [ 

703 st 

704 for st in decomp.sub_tasks 

705 if st.status == "pending" and all(dep in completed for dep in st.depends_on) 

706 ] 

707 if not ready: 

708 break 

709 

710 for st in ready: 

711 st.status = "running" 

712 

713 if trace: 

714 stspan = trace.start_span( 

715 TraceEvent.SUBTASK_START, name=st.description[:60], data={"id": st.id} 

716 ) 

717 

718 # Build context from dependencies 

719 context = task_str 

720 if memory_context: 

721 context = f"{memory_context}\n\n[Current Task]\n{task_str}" 

722 if st.depends_on: 

723 dep_contexts = [] 

724 for dep_id in st.depends_on: 

725 dep_outputs = sub_outputs.get(dep_id, {}) 

726 for name, out in dep_outputs.items(): 

727 dep_contexts.append(f"[{name}]: {str(out)[:300]}") 

728 if dep_contexts: 

729 context = f"{context}\n\nPrevious results:\n" + "\n".join(dep_contexts) 

730 

731 # Execute with all agents on this sub-task 

732 topo_result = await self._execute_raw(context, **metadata) 

733 sub_outputs[st.id] = topo_result.outputs 

734 st.output = topo_result.outputs 

735 st.status = "done" if topo_result.success else "failed" 

736 completed.add(st.id) 

737 

738 # Store sub-task result in memory 

739 self.memory.remember( 

740 content=f"SubTask [{st.description}]: {json.dumps(topo_result.outputs, default=str)[:500]}", 

741 role="assistant", 

742 importance=0.6, 

743 metadata={"subtask_id": st.id, "status": st.status}, 

744 ) 

745 

746 if trace and stspan: 

747 st_status = "done" if st.status == "done" else "failed" 

748 trace.end_span( 

749 stspan.id, 

750 status=st_status, 

751 data={"output_keys": list(topo_result.outputs.keys())}, 

752 ) 

753 

754 # Step 3: Fuse results from final sub-tasks 

755 if trace: 

756 fspan = trace.start_span(TraceEvent.FUSE, name="fuse_results") 

757 

758 final_subtasks = [ 

759 st 

760 for st in decomp.sub_tasks 

761 if st.status == "done" 

762 and st.id 

763 not in {s.id for s in decomp.sub_tasks if any(d == st.id for d in s.depends_on)} 

764 ] 

765 if final_subtasks: 

766 all_final: dict[str, Any] = {} 

767 for st in final_subtasks: 

768 if st.output: 

769 all_final.update(st.output) 

770 if all_final: 

771 fused = self.fusion.fuse(task_str, all_final) 

772 result.fused = fused 

773 result.outputs = all_final 

774 result.success = fused.confidence >= 0.3 

775 

776 if not result.outputs and sub_outputs: 

777 all_outputs: dict[str, Any] = {} 

778 for st_outputs in sub_outputs.values(): 

779 all_outputs.update(st_outputs) 

780 if all_outputs: 

781 fused = self.fusion.fuse(task_str, all_outputs) 

782 result.fused = fused 

783 result.outputs = all_outputs 

784 result.success = fused.confidence >= 0.3 

785 

786 if trace and fspan: 

787 trace.end_span( 

788 fspan.id, 

789 status="done", 

790 data={"confidence": result.fused.confidence if result.fused else 0}, 

791 ) 

792 

793 result.duration = time.time() - start_time 

794 

795 if trace and root: 

796 trace.end_span(root.id, status="done" if result.success else "failed") 

797 

798 # Output guard — filter agent output before returning to user 

799 if result.outputs: 

800 guarded_outputs: dict[str, Any] = {} 

801 for key, value in result.outputs.items(): 

802 output_str = str(value) 

803 output_guard = self.guard.process_output(output_str) 

804 if output_guard.blocked: 

805 guarded_outputs[key] = f"[BLOCKED by guard: {output_guard.blocked_by}]" 

806 elif output_guard.final_content != output_str: 

807 guarded_outputs[key] = output_guard.final_content 

808 else: 

809 guarded_outputs[key] = value 

810 result.outputs = guarded_outputs 

811 

812 return result 

813 

814 async def execute_with_feedback( 

815 self, 

816 task: Any, 

817 expected_output: str = "", 

818 scoring_strategy: str = "general", 

819 retry_config: RetryConfig | None = None, 

820 **metadata, 

821 ) -> SwarmResult: 

822 """Execution with eval-driven feedback loop. 

823 

824 Args: 

825 task: Task description 

826 expected_output: Reference for scoring 

827 scoring_strategy: Scoring strategy (qa/code/summary/translation) 

828 retry_config: Retry configuration 

829 **metadata: Additional metadata 

830 

831 Returns: 

832 SwarmResult with feedback_loop trace 

833 """ 

834 start_time = time.time() 

835 result = SwarmResult( 

836 topology=self.topology, 

837 mode=ExecutionMode.FEEDBACK, 

838 ) 

839 

840 task_str = str(task) 

841 

842 # Build executor that uses smart_execute 

843 async def executor(t: str) -> Any: 

844 r = await self.smart_execute(t, **metadata) 

845 fused = r.fused 

846 if fused and fused.merged: 

847 content = fused.merged 

848 # If it's a dict with agent outputs, stringify 

849 if isinstance(content, dict): 

850 parts = [] 

851 for k, v in content.items(): 

852 if v and not isinstance(v, dict): 

853 parts.append(str(v)) 

854 return "\n".join(parts) if parts else str(content) 

855 return str(content) 

856 return str(r.outputs) 

857 

858 # Wire scorer if available 

859 scorer = None 

860 try: 

861 from agentos.evaluation.scorers import CompositeScorerV2 

862 

863 scorer = CompositeScorerV2() 

864 except Exception: 

865 pass 

866 

867 feedback = EvalFeedbackLoop( 

868 scorer=scorer, 

869 config=retry_config or RetryConfig(max_retries=3), 

870 ) 

871 

872 loop_result = await feedback.run( 

873 task=task_str, 

874 executor=executor, 

875 expected=expected_output, 

876 strategy=scoring_strategy, 

877 ) 

878 

879 result.feedback_loop = loop_result 

880 result.outputs = { 

881 "final": str(loop_result.final_output) if loop_result.final_output else "" 

882 } 

883 result.success = loop_result.converged 

884 result.duration = time.time() - start_time 

885 return result 

886 

887 # ── Raw topology execution (backward compatible) ────────────── 

888 

889 async def _execute_raw( 

890 self, 

891 task: Any, 

892 **metadata, 

893 ) -> SwarmResult: 

894 """Original topology-only execution.""" 

895 handler = self._topo_handlers.get(self.topology) 

896 if handler is None: 

897 raise ValueError(f"Unknown topology: {self.topology}") 

898 return await handler(task, metadata) 

899 

900 # ── Star Topology ───────────────────────────────────────────── 

901 

902 async def _execute_star( 

903 self, 

904 task: Any, 

905 metadata: dict[str, Any], 

906 ) -> SwarmResult: 

907 result = SwarmResult(topology=SwarmTopology.STAR, mode=ExecutionMode.RAW) 

908 for agent_name, agent in self._agents.items(): 

909 try: 

910 message = SwarmMessage( 

911 sender="coordinator", 

912 receiver=agent_name, 

913 content=task, 

914 metadata=metadata, 

915 ) 

916 result.messages.append(message) 

917 output = await agent.invoke(task, **metadata) 

918 result.outputs[agent_name] = output 

919 response = SwarmMessage( 

920 sender=agent_name, 

921 receiver="coordinator", 

922 content=output, 

923 ) 

924 result.messages.append(response) 

925 except Exception as e: 

926 result.outputs[agent_name] = {"error": str(e)} 

927 result.success = False 

928 return result 

929 

930 # ── Ring Topology ───────────────────────────────────────────── 

931 

932 async def _execute_ring( 

933 self, 

934 task: Any, 

935 metadata: dict[str, Any], 

936 ) -> SwarmResult: 

937 result = SwarmResult(topology=SwarmTopology.RING, mode=ExecutionMode.RAW) 

938 agent_names = list(self._agents.keys()) 

939 if not agent_names: 

940 return result 

941 

942 current_input = task 

943 for i, agent_name in enumerate(agent_names): 

944 agent = self._agents[agent_name] 

945 next_agent = agent_names[(i + 1) % len(agent_names)] 

946 try: 

947 output = await agent.invoke(current_input, **metadata) 

948 result.outputs[agent_name] = output 

949 message = SwarmMessage( 

950 sender=agent_name, 

951 receiver=next_agent, 

952 content=output, 

953 ) 

954 result.messages.append(message) 

955 current_input = output 

956 except Exception as e: 

957 result.outputs[agent_name] = {"error": str(e)} 

958 result.success = False 

959 return result 

960 

961 # ── Mesh Topology ───────────────────────────────────────────── 

962 

963 async def _execute_mesh( 

964 self, 

965 task: Any, 

966 metadata: dict[str, Any], 

967 ) -> SwarmResult: 

968 result = SwarmResult(topology=SwarmTopology.MESH, mode=ExecutionMode.RAW) 

969 tasks_ = [] 

970 for agent_name, agent in self._agents.items(): 

971 tasks_.append(self._execute_agent_mesh(agent, task, metadata, result)) 

972 await asyncio.gather(*tasks_, return_exceptions=True) 

973 for sender_name, output in result.outputs.items(): 

974 for receiver_name in self._agents.keys(): 

975 if sender_name != receiver_name: 

976 message = SwarmMessage( 

977 sender=sender_name, 

978 receiver=receiver_name, 

979 content=output, 

980 ) 

981 result.messages.append(message) 

982 return result 

983 

984 async def _execute_agent_mesh( 

985 self, 

986 agent: Agent[Any, Any], 

987 task: Any, 

988 metadata: dict[str, Any], 

989 result: SwarmResult, 

990 ) -> None: 

991 try: 

992 output = await agent.invoke(task, **metadata) 

993 result.outputs[agent.name] = output 

994 except Exception as e: 

995 result.outputs[agent.name] = {"error": str(e)} 

996 result.success = False 

997 

998 # ── Tree Topology ───────────────────────────────────────────── 

999 

1000 async def _execute_tree( 

1001 self, 

1002 task: Any, 

1003 metadata: dict[str, Any], 

1004 ) -> SwarmResult: 

1005 result = SwarmResult(topology=SwarmTopology.TREE, mode=ExecutionMode.RAW) 

1006 agent_names = list(self._agents.keys()) 

1007 if not agent_names: 

1008 return result 

1009 

1010 root_name = agent_names[0] 

1011 root_agent = self._agents[root_name] 

1012 try: 

1013 root_output = await root_agent.invoke(task, **metadata) 

1014 result.outputs[root_name] = root_output 

1015 except Exception as e: 

1016 result.outputs[root_name] = {"error": str(e)} 

1017 result.success = False 

1018 return result 

1019 

1020 children = agent_names[1:] 

1021 for child_name in children: 

1022 child_agent = self._agents[child_name] 

1023 message = SwarmMessage( 

1024 sender=root_name, 

1025 receiver=child_name, 

1026 content=root_output, 

1027 ) 

1028 result.messages.append(message) 

1029 try: 

1030 child_output = await child_agent.invoke(root_output, **metadata) 

1031 result.outputs[child_name] = child_output 

1032 response = SwarmMessage( 

1033 sender=child_name, 

1034 receiver=root_name, 

1035 content=child_output, 

1036 ) 

1037 result.messages.append(response) 

1038 except Exception as e: 

1039 result.outputs[child_name] = {"error": str(e)} 

1040 result.success = False 

1041 return result 

1042 

1043 # ── Messaging ───────────────────────────────────────────────── 

1044 

1045 def send_message( 

1046 self, 

1047 sender: str, 

1048 receiver: str | None, 

1049 content: Any, 

1050 **metadata, 

1051 ) -> SwarmMessage: 

1052 message = SwarmMessage( 

1053 sender=sender, 

1054 receiver=receiver, 

1055 content=content, 

1056 metadata=metadata, 

1057 ) 

1058 self._message_queue.append(message) 

1059 return message 

1060 

1061 def get_messages( 

1062 self, 

1063 receiver: str | None = None, 

1064 ) -> list[SwarmMessage]: 

1065 if receiver: 

1066 return [m for m in self._message_queue if m.receiver == receiver or m.receiver is None] 

1067 return self._message_queue.copy() 

1068 

1069 def clear_messages(self) -> None: 

1070 self._message_queue.clear() 

1071 

1072 # ── Code Sandbox Execution (v1.9.5) ─────────────────────────── 

1073 

1074 async def execute_code( 

1075 self, 

1076 code: str, 

1077 func_name: str = "", 

1078 test_cases: list[TestCase] | None = None, 

1079 setup_code: str = "", 

1080 sandbox: CodeSandbox | None = None, 

1081 max_retries: int = 3, 

1082 code_generator: Callable[[str, list[str]], str] | None = None, 

1083 ) -> SandboxResult: 

1084 """Execute code in sandbox with test cases and feedback-driven retry. 

1085 

1086 Supports code generation: if code_generator is provided and initial run 

1087 fails, it will use the feedback extractor to guide re-generation. 

1088 

1089 Args: 

1090 code: Code to execute (or initial code if using generator) 

1091 func_name: Function name to test 

1092 test_cases: Test cases for validation 

1093 setup_code: Setup code (imports, fixtures) 

1094 sandbox: Custom sandbox instance 

1095 max_retries: Max retry attempts with code generation 

1096 code_generator: Callable(spec, feedback_suggestions) → new_code 

1097 

1098 Returns: 

1099 SandboxResult with execution details and test outcomes 

1100 """ 

1101 sb = sandbox or self.sandbox 

1102 

1103 result = sb.run(code, func_name, test_cases, setup_code) 

1104 

1105 # If initial run succeeded, we're done 

1106 if result.all_passed: 

1107 return result 

1108 

1109 # Feedback-driven retry loop 

1110 for attempt in range(1, max_retries + 1): 

1111 if not code_generator: 

1112 break 

1113 

1114 suggestions = CodeFeedbackExtractor.extract(result) 

1115 if not suggestions: 

1116 break 

1117 

1118 # Generate improved code 

1119 spec = f"Function: {func_name}, Test cases: {len(test_cases or [])}" 

1120 try: 

1121 new_code = code_generator(spec, suggestions) 

1122 except Exception: 

1123 break 

1124 

1125 if not new_code or new_code == code: 

1126 break 

1127 

1128 code = new_code 

1129 result = sb.run(code, func_name, test_cases, setup_code) 

1130 

1131 if result.all_passed: 

1132 break 

1133 

1134 if attempt == max_retries: 

1135 break # Don't overwrite last result 

1136 

1137 return result 

1138 

1139 # ── HITL-Enhanced Execution (v1.9.5) ────────────────────────── 

1140 

1141 async def smart_execute_with_hitl( 

1142 self, 

1143 task: Any, 

1144 hitl: HITLManager | None = None, 

1145 **metadata, 

1146 ) -> SwarmResult: 

1147 """Smart execution with human-in-the-loop breakpoints. 

1148 

1149 Same as smart_execute but pauses at configurable checkpoints: 

1150 - Before each sub-task (if hitl.break_on_every_task) 

1151 - On sub-task failure (if hitl.break_on_failure) 

1152 - On low-confidence fusion (if config threshold met) 

1153 

1154 Args: 

1155 task: Task description 

1156 hitl: HITLManager instance (uses self.hitl if None) 

1157 **metadata: Additional metadata 

1158 

1159 Returns: 

1160 SwarmResult with fused output 

1161 """ 

1162 hitl_mgr = hitl or self.hitl 

1163 start_time = time.time() 

1164 result = SwarmResult( 

1165 topology=self.topology, 

1166 mode=ExecutionMode.SMART, 

1167 ) 

1168 

1169 task_str = str(task) 

1170 agent_names = self.list_agents() 

1171 

1172 # Step 1: Decompose 

1173 decomp = self.decomposer.decompose(task_str, agents=agent_names) 

1174 result.decomposition = decomp 

1175 

1176 # Step 2: Execute sub-tasks with HITL gates 

1177 sub_outputs: dict[str, dict[str, Any]] = {} 

1178 completed: set[str] = set() 

1179 aborted = False 

1180 

1181 for _round in range(self.max_rounds): 

1182 if aborted: 

1183 break 

1184 

1185 ready = [ 

1186 st 

1187 for st in decomp.sub_tasks 

1188 if st.status == "pending" and all(dep in completed for dep in st.depends_on) 

1189 ] 

1190 if not ready: 

1191 break 

1192 

1193 for st in ready: 

1194 # HITL: check before executing sub-task 

1195 if hitl_mgr.config.break_on_every_task: 

1196 decision, feedback = await hitl_mgr.request_decision( 

1197 bp_type=BreakpointType.BEFORE_TASK, 

1198 task_id=st.id, 

1199 message=f"Execute sub-task: {st.description}?", 

1200 context={"task": task_str, "sub_task": st.description}, 

1201 options=["approve", "abort", "modify"], 

1202 ) 

1203 if decision == HumanDecision.ABORT: 

1204 aborted = True 

1205 break 

1206 if decision == HumanDecision.MODIFY and feedback: 

1207 st.description = f"{st.description} [modified: {feedback}]" 

1208 

1209 st.status = "running" 

1210 

1211 # Build context from dependencies 

1212 context = task_str 

1213 if st.depends_on: 

1214 dep_contexts = [] 

1215 for dep_id in st.depends_on: 

1216 dep_outputs = sub_outputs.get(dep_id, {}) 

1217 for name, out in dep_outputs.items(): 

1218 dep_contexts.append(f"[{name}]: {str(out)[:300]}") 

1219 if dep_contexts: 

1220 context = f"{task_str}\n\nPrevious results:\n" + "\n".join(dep_contexts) 

1221 

1222 # Execute 

1223 topo_result = await self._execute_raw(context, **metadata) 

1224 sub_outputs[st.id] = topo_result.outputs 

1225 st.output = topo_result.outputs 

1226 st.status = "done" if topo_result.success else "failed" 

1227 completed.add(st.id) 

1228 

1229 # HITL: check on failure 

1230 if not topo_result.success: 

1231 decision, feedback = await hitl_mgr.should_break_on_failure( 

1232 task_id=st.id, 

1233 error=topo_result.error or "Unknown error", 

1234 attempt=1, 

1235 ) 

1236 if decision == HumanDecision.ABORT: 

1237 aborted = True 

1238 break 

1239 if decision == HumanDecision.MODIFY and feedback: 

1240 st.description = f"{st.description} [retry with: {feedback}]" 

1241 st.status = "pending" # Re-queue for retry 

1242 completed.discard(st.id) 

1243 del sub_outputs[st.id] 

1244 

1245 if aborted: 

1246 result.success = False 

1247 result.error = "Aborted by human" 

1248 return result 

1249 

1250 # Step 3: Fuse results 

1251 final_subtasks = [ 

1252 st 

1253 for st in decomp.sub_tasks 

1254 if st.status == "done" 

1255 and st.id 

1256 not in {s.id for s in decomp.sub_tasks if any(d == st.id for d in s.depends_on)} 

1257 ] 

1258 if final_subtasks: 

1259 all_final: dict[str, Any] = {} 

1260 for st in final_subtasks: 

1261 if st.output: 

1262 all_final.update(st.output) 

1263 if all_final: 

1264 fused = self.fusion.fuse(task_str, all_final) 

1265 result.fused = fused 

1266 result.outputs = all_final 

1267 

1268 # HITL: check low confidence 

1269 if fused.confidence < hitl_mgr.config.break_on_low_confidence: 

1270 decision, feedback = await hitl_mgr.should_break_on_result( 

1271 task_id="final", 

1272 output=all_final, 

1273 confidence=fused.confidence, 

1274 ) 

1275 if decision == HumanDecision.ABORT: 

1276 result.success = False 

1277 result.error = "Aborted by human at final result" 

1278 return result 

1279 if decision == HumanDecision.REJECT: 

1280 result.success = False 

1281 result.error = f"Rejected: {feedback}" 

1282 return result 

1283 

1284 result.success = fused.confidence >= 0.3 

1285 

1286 if not result.outputs and sub_outputs: 

1287 all_outputs: dict[str, Any] = {} 

1288 for st_outputs in sub_outputs.values(): 

1289 all_outputs.update(st_outputs) 

1290 if all_outputs: 

1291 fused = self.fusion.fuse(task_str, all_outputs) 

1292 result.fused = fused 

1293 result.outputs = all_outputs 

1294 result.success = fused.confidence >= 0.3 

1295 

1296 result.duration = time.time() - start_time 

1297 return result 

1298 

1299 # ── Monitored Execution (v1.9.6) ───────────────────────────── 

1300 

1301 async def monitor_execute( 

1302 self, 

1303 task: Any, 

1304 quality_gates: list[QualityGate] | None = None, 

1305 fallback_fn: Callable[[], Any] | None = None, 

1306 **metadata, 

1307 ) -> tuple[Any, MonitorReport]: 

1308 """Execute with automatic quality gating. 

1309 

1310 Runs smart_execute through the AgentMonitor pipeline. If gates fail, 

1311 automatically retries or falls back based on gate configuration. 

1312 

1313 Args: 

1314 task: Task description 

1315 quality_gates: Custom quality gates (uses monitor defaults if None) 

1316 fallback_fn: Fallback function if all gates fail 

1317 **metadata: Additional metadata 

1318 

1319 Returns: 

1320 Tuple of (final_output, MonitorReport) 

1321 """ 

1322 # Configure monitor with custom gates if provided 

1323 monitor = self.monitor 

1324 if quality_gates: 

1325 monitor = AgentMonitor( 

1326 max_retries=self.monitor.max_retries, 

1327 default_fallback=self.monitor.default_fallback, 

1328 ) 

1329 monitor.add_gates(quality_gates) 

1330 elif not self.monitor._gates: 

1331 # Default gates if none configured 

1332 monitor = AgentMonitor() 

1333 monitor.add_gates( 

1334 [ 

1335 output_not_empty(), 

1336 no_error_output(), 

1337 ] 

1338 ) 

1339 

1340 # Track latency for latency gates 

1341 start = time.time() 

1342 

1343 async def execute_fn() -> Any: 

1344 result = await self.smart_execute(task, **metadata) 

1345 fused = result.fused 

1346 if fused and fused.merged: 

1347 return fused.merged 

1348 return result.outputs 

1349 

1350 output, report = await monitor.monitor_execution( 

1351 task_fn=execute_fn, 

1352 task_name=str(task)[:80], 

1353 context={"_latency_ms": 0}, 

1354 fallback_fn=fallback_fn, 

1355 ) 

1356 

1357 # Inject actual latency 

1358 elapsed = (time.time() - start) * 1000 

1359 for gate in report.gates: 

1360 gate.data["_latency_ms"] = elapsed 

1361 

1362 return output, report 

1363 

1364 # ── Tool Registry Convenience Methods ───────────────────────── 

1365 

1366 def register_tool( 

1367 self, 

1368 name: str, 

1369 description: str, 

1370 handler: Callable, 

1371 category: ToolCategory = ToolCategory.CUSTOM, 

1372 params: list[ToolParam] | None = None, 

1373 capabilities: list[str] | None = None, 

1374 tags: list[str] | None = None, 

1375 is_destructive: bool = False, 

1376 rate_limit: int = 0, 

1377 **kwargs, 

1378 ) -> ToolSchema: 

1379 """Register a tool in the coordinator's tool registry.""" 

1380 tool = create_tool( 

1381 name=name, 

1382 description=description, 

1383 handler=handler, 

1384 category=category, 

1385 params=params or [], 

1386 capabilities=capabilities or [], 

1387 tags=tags or [], 

1388 is_destructive=is_destructive, 

1389 rate_limit=rate_limit, 

1390 **kwargs, 

1391 ) 

1392 return self.tool_registry.register(tool) 

1393 

1394 def find_tool(self, query: str, top_k: int = 5) -> list[tuple[ToolSchema, float]]: 

1395 """Search for tools matching a natural language query.""" 

1396 return self.tool_registry.search(query, top_k=top_k) 

1397 

1398 def route_tool(self, task: str, **ctx_kwargs) -> RoutingDecision: 

1399 """Route a task to the best matching tool.""" 

1400 context = RoutingContext(task=task, **ctx_kwargs) 

1401 return self.tool_router.route(context) 

1402 

1403 def execute_tool( 

1404 self, tool_name: str, params: dict[str, Any] | None = None, force: bool = False 

1405 ) -> Any: 

1406 """Execute a registered tool safely.""" 

1407 return self.tool_executor.execute(tool_name, params, force=force) 

1408 

1409 

1410# ── Backward-compatible alias ───────────────────────────────────── 

1411SwarmCoordinator = SmartSwarmCoordinator