from __future__ import annotations from typing import Any from core.tools.registry import registry from core.events import bus class AgentLoop: """ Minimal deterministic agent loop. Orchestrates tools like: - search - intelligent_search - crawler - research Can later be upgraded with LLM-based planning. """ def __init__(self, ctx): self.ctx = ctx self.max_steps = 5 # ========================= # ENTRY POINT # ========================= def run(self, goal: str) -> dict[str, Any]: state = { "goal": goal, "steps": [], "memory": [], "final": None } bus.log( "AGENT", "agent_loop_start", "INFO", {"goal": goal} ) for step in range(self.max_steps): action = self._decide(state) if action["type"] == "stop": state["final"] = action.get("result") break result = self._execute(action, state) state["steps"].append(action) state["memory"].append(result) # simple convergence heuristic if self._is_satisfied(state): state["final"] = result break if not state["final"]: state["final"] = state["memory"][-1] if state["memory"] else {} return state # ========================= # DECISION LOGIC (RULE-BASED FOR NOW) # ========================= def _decide(self, state: dict[str, Any]) -> dict[str, Any]: """ Very simple heuristic planner. Later upgrade point: replace with LLM planner. """ goal = state["goal"] memory = state["memory"] if not memory: return { "type": "tool", "tool": "research", "input": { "query": goal, "depth": 1 } } last = memory[-1] # if research returned sources, refine or stop if isinstance(last, dict) and "sources" in last: if len(last["sources"]) >= 2: return { "type": "stop", "result": last } # if weak results, deepen search return { "type": "tool", "tool": "research", "input": { "query": goal, "depth": 2, "max_sources": 5 } } return { "type": "stop", "result": last } # ========================= # TOOL EXECUTION # ========================= def _execute(self, action: dict[str, Any], state: dict[str, Any]) -> Any: tool_name = action.get("tool") tool_input = action.get("input", {}) bus.log( "AGENT", "agent_tool_call", "INFO", { "tool": tool_name, "input": tool_input } ) if not tool_name: return {"error": "No tool specified"} try: result = registry.run( tool_name, tool_input, self.ctx ) return result except Exception as e: return { "error": str(e), "tool": tool_name } # ========================= # STOPPING CONDITION # ========================= def _is_satisfied(self, state: dict[str, Any]) -> bool: """ Simple satisfaction heuristic. Later upgrade: LLM-based evaluation. """ memory = state["memory"] if not memory: return False last = memory[-1] if isinstance(last, dict): # if research returned structured sources, assume OK if "sources" in last and len(last["sources"]) > 0: return True if "error" in last: return True return False