Build plan execution tool for reactive/interactive mode. In plan mode (--plan_mode), execution is driven programmatically by _run_plan_mode() and does NOT use this tool. This tool exists for cases where the LLM generates a plan in reactive mode and wants to execute it. The executio
(
task_board: TaskBoard,
plan_executor: Any, # PlanExecutor (avoid circular import)
summarize_fn: Any, # Callable[[str, str], str] — summarizes raw result
)
| 298 | |
| 299 | |
| 300 | def build_execution_tools( |
| 301 | task_board: TaskBoard, |
| 302 | plan_executor: Any, # PlanExecutor (avoid circular import) |
| 303 | summarize_fn: Any, # Callable[[str, str], str] — summarizes raw result |
| 304 | ) -> list[OrchestratorTool]: |
| 305 | """Build plan execution tool for reactive/interactive mode. |
| 306 | |
| 307 | In plan mode (--plan_mode), execution is driven programmatically by |
| 308 | _run_plan_mode() and does NOT use this tool. This tool exists for cases |
| 309 | where the LLM generates a plan in reactive mode and wants to execute it. |
| 310 | |
| 311 | The execution result is summarized via summarize_fn before being returned, |
| 312 | keeping the orchestrator's context compact. The full raw output stays |
| 313 | inside the PlanExecutor's isolated scope. |
| 314 | |
| 315 | Args: |
| 316 | task_board: TaskBoard for looking up plan paths and updating results. |
| 317 | plan_executor: PlanExecutor instance for running YAML plans through |
| 318 | the existing AgentSPEX Interpreter. |
| 319 | summarize_fn: Callable(raw_result, task_description) -> compact summary. |
| 320 | Typically AgenticLoop._summarize_execution_result, which uses a |
| 321 | cheap LLM call to produce a structured summary. |
| 322 | """ |
| 323 | |
| 324 | def handle_execute_plan(args: dict) -> str: |
| 325 | task_id = args["task_id"] |
| 326 | task = task_board.get_task(task_id) |
| 327 | if not task: |
| 328 | return f"Error: Task {task_id} not found on the task board." |
| 329 | |
| 330 | # Guard: must have a generated and approved plan |
| 331 | if not task.plan_file: |
| 332 | return ( |
| 333 | f"Error: Task {task_id} has no plan file. " |
| 334 | "Generate a plan with generate_plan first." |
| 335 | ) |
| 336 | if not task.plan_verified: |
| 337 | return ( |
| 338 | f"Error: Task {task_id} plan is not verified/approved. " |
| 339 | "The plan must be generated and approved before execution." |
| 340 | ) |
| 341 | |
| 342 | plan_path = Path(task.plan_file) |
| 343 | if not plan_path.exists(): |
| 344 | return f"Error: Plan file {task.plan_file} not found on disk." |
| 345 | |
| 346 | # Mark task as in_progress before execution |
| 347 | task_board.update_task(task_id, status="in_progress") |
| 348 | |
| 349 | try: |
| 350 | # Execute the plan in isolation (fresh Interpreter, no orchestrator context) |
| 351 | execution_result = plan_executor.execute( |
| 352 | plan_path=plan_path, |
| 353 | model=args.get("model"), |
| 354 | ) |
| 355 | |
| 356 | # Summarize the raw result using a cheap LLM call — |
| 357 | # only the compact summary enters the orchestrator's context, |