Run the full plan-execute loop for a question. Steps: 1. Discover available servers from registered MCP servers. 2. Use the LLM to decompose the question into an execution plan. 3. Execute each plan step by routing tool calls to MCP servers. 4. Summar
(self, question: str)
| 108 | self._executor = Executor(self._meter, server_paths) |
| 109 | |
| 110 | async def run(self, question: str) -> OrchestratorResult: |
| 111 | """Run the full plan-execute loop for a question. |
| 112 | |
| 113 | Steps: |
| 114 | 1. Discover available servers from registered MCP servers. |
| 115 | 2. Use the LLM to decompose the question into an execution plan. |
| 116 | 3. Execute each plan step by routing tool calls to MCP servers. |
| 117 | 4. Summarise the step results into a final answer. |
| 118 | |
| 119 | Args: |
| 120 | question: The user question to answer. |
| 121 | |
| 122 | Returns: |
| 123 | OrchestratorResult with the final answer, the generated plan, and |
| 124 | the per-step execution trajectory. |
| 125 | """ |
| 126 | with agent_run_span( |
| 127 | "plan-execute", model=self._llm.model_id, question=question |
| 128 | ) as span: |
| 129 | run_started = time.perf_counter() |
| 130 | self._meter.reset() |
| 131 | |
| 132 | # 1. Discover |
| 133 | _log.info("Discovering server capabilities...") |
| 134 | server_descriptions = await self._executor.get_server_descriptions() |
| 135 | |
| 136 | # 2. Plan |
| 137 | _log.info("Planning...") |
| 138 | planning_started = time.perf_counter() |
| 139 | plan = self._planner.generate_plan(question, server_descriptions) |
| 140 | planning_ms = (time.perf_counter() - planning_started) * 1000 |
| 141 | _log.info("Plan has %d step(s).", len(plan.steps)) |
| 142 | |
| 143 | # 3. Execute |
| 144 | trajectory = await self._executor.execute_plan(plan, question) |
| 145 | |
| 146 | # 4. Summarise |
| 147 | _log.info("Summarising...") |
| 148 | results_text = "\n\n".join( |
| 149 | f"Step {r.step_number} — {r.task} (server: {r.server}):\n" |
| 150 | + (r.response if r.success else f"ERROR: {r.error}") |
| 151 | for r in trajectory |
| 152 | ) |
| 153 | summarization_started = time.perf_counter() |
| 154 | answer = self._meter.generate( |
| 155 | _SUMMARIZE_PROMPT.format(question=question, results=results_text) |
| 156 | ) |
| 157 | summarization_ms = (time.perf_counter() - summarization_started) * 1000 |
| 158 | duration_ms = (time.perf_counter() - run_started) * 1000 |
| 159 | |
| 160 | result = OrchestratorResult( |
| 161 | question=question, |
| 162 | answer=answer, |
| 163 | plan=plan, |
| 164 | trajectory=trajectory, |
| 165 | ) |
| 166 | span.set_attribute("agent.plan.steps", len(plan.steps)) |
| 167 | span.set_attribute("agent.answer.length", len(answer or "")) |