import json import time from collections.abc import Iterator from _core.llm import LLMClient from _core.models import estimate_cost from _core.tracer import Step ORCH_PROMPT = ( "You are an orchestrator. Decompose the task into 2-4 independent subtasks, " "each assigned to a named specialist. Output ONLY a JSON array of objects " '{"role": str, "subtask": str}.' ) SYNTH_PROMPT = ( "You are an orchestrator. Combine the workers' outputs into one coherent answer " "to the original task." ) class OrchestratorAgent: def __init__(self, llm: LLMClient, max_workers: int = 4): self.llm = llm self.max_workers = max_workers def _ask(self, system: str, user: str): start = time.monotonic() resp = self.llm.chat( [{"role": "system", "content": system}, {"role": "user", "content": user}] ) latency = int((time.monotonic() - start) * 1000) cost = estimate_cost(self.llm.model, resp.prompt_tokens, resp.completion_tokens) return resp, latency, cost def run(self, task: str) -> Iterator[Step]: resp, lat, cost = self._ask(ORCH_PROMPT, task) try: parsed = json.loads(resp.content or "[]") except json.JSONDecodeError: parsed = [] # Coerce whatever the model returned into a list of {role, subtask} dicts — # small models often return a bare string array or a single object. if not isinstance(parsed, list): parsed = [parsed] subtasks = [] for item in parsed[: self.max_workers]: if isinstance(item, dict): subtasks.append( {"role": item.get("role", "specialist"), "subtask": item.get("subtask", task)} ) else: subtasks.append({"role": "generalist", "subtask": str(item)}) if not subtasks: subtasks = [{"role": "generalist", "subtask": task}] yield Step( kind="thought", content="Decomposition:\n" + "\n".join(f"- [{s['role']}] {s['subtask']}" for s in subtasks), tokens=resp.prompt_tokens + resp.completion_tokens, cost_usd=cost, latency_ms=lat, ) outputs: list[str] = [] for s in subtasks: yield Step(kind="action", content=f"Dispatch to {s['role']}: {s['subtask']}") wresp, wlat, wcost = self._ask( f"You are a {s['role']}. Answer concisely.", s["subtask"] ) yield Step( kind="observation", content=wresp.content or "", tokens=wresp.prompt_tokens + wresp.completion_tokens, cost_usd=wcost, latency_ms=wlat, ) outputs.append(f"[{s['role']}] {wresp.content}") fresp, flat, fcost = self._ask( SYNTH_PROMPT, f"Task: {task}\nWorker outputs:\n" + "\n".join(outputs) ) yield Step( kind="final", content=fresp.content or "", tokens=fresp.prompt_tokens + fresp.completion_tokens, cost_usd=fcost, latency_ms=flat, )