"""OpenAI Agents SDK loop demo: ReAct vs Plan-and-Execute. Companion to the All Things Agentic post 'Why Your Agent Goes In Circles'. Same two tools and the same seeded sample_repo/ as raw_react.py; what changes is the SDK does the loop for you. Run: uv sync export OPENAI_API_KEY=sk-... uv run python triage.py --mode react # default uv run python triage.py --mode plan """ from __future__ import annotations import argparse import asyncio import os import subprocess import sys from pathlib import Path from agents import Agent, Runner, function_tool from dotenv import load_dotenv load_dotenv() # searches upward for a .env file; picks up the repo root's MODEL = "gpt-5" MAX_TURNS = 10 SAMPLE_REPO = (Path(__file__).parent / "sample_repo").resolve() # --- tools ------------------------------------------------------------------ @function_tool def run_pytest() -> str: """Run pytest against sample_repo/ and return stdout.""" result = subprocess.run( [sys.executable, "-m", "pytest", str(SAMPLE_REPO), "--tb=short", "-q"], capture_output=True, text=True, timeout=60, ) return (result.stdout + result.stderr).strip() @function_tool def read_file(path: str) -> str: """Read a file inside sample_repo/. Refuses paths outside it.""" target = (SAMPLE_REPO / path).resolve() if SAMPLE_REPO not in target.parents and target != SAMPLE_REPO: return f"refused: path '{path}' is outside sample_repo/" if not target.exists(): return f"not found: {path}" return target.read_text() # --- ReAct mode ------------------------------------------------------------- REACT_PROMPT = """You investigate failing pytests in sample_repo/. Use run_pytest to see the failures, read_file to inspect each failing test and the implementation it imports, then cluster the failures by root cause. When you have clustered all failing tests and produced a summary, stop. """ async def run_react(user_input: str) -> str: agent = Agent( name="triage", instructions=REACT_PROMPT, tools=[run_pytest, read_file], model=MODEL, ) result = await Runner.run(agent, user_input, max_turns=MAX_TURNS) return result.final_output # --- Plan-and-Execute mode -------------------------------------------------- PLANNER_PROMPT = """You are a planner. Given the user's task, output a short numbered list of 3-5 steps that an executor agent will walk in order. Each step is one sentence and names which tool to use (run_pytest or read_file). Output only the list.""" EXECUTOR_PROMPT = """You are the executor. Walk one step of a plan. Use run_pytest or read_file as the step requires. Reply in one short paragraph summarising what the step found.""" async def run_plan(user_input: str) -> str: planner = Agent(name="planner", instructions=PLANNER_PROMPT, model=MODEL) executor = Agent( name="executor", instructions=EXECUTOR_PROMPT, tools=[run_pytest, read_file], model=MODEL, ) plan_result = await Runner.run(planner, user_input, max_turns=2) steps = [ line.strip().lstrip("0123456789.- )") for line in plan_result.final_output.splitlines() if line.strip() and any(c.isalpha() for c in line) ] print(f"plan ({len(steps)} steps):") for i, s in enumerate(steps, 1): print(f" {i}. {s}") if not steps: return "(planner returned no parseable steps)" notes: list[str] = [] for i, step in enumerate(steps, 1): print(f"\nexecuting step {i}: {step}") prompt = f"Original task: {user_input}\nThis step: {step}\nDo it." step_result = await Runner.run(executor, prompt, max_turns=8) notes.append(f"step {i}: {step_result.final_output}") return "\n\n".join(notes) # --- entry ------------------------------------------------------------------ def main(argv: list[str]) -> int: parser = argparse.ArgumentParser() parser.add_argument("--mode", choices=["react", "plan"], default="react") args = parser.parse_args(argv[1:]) if not os.environ.get("OPENAI_API_KEY"): print("OPENAI_API_KEY is not set", file=sys.stderr) return 1 user_input = "Triage the failing tests in sample_repo/." runner = run_react if args.mode == "react" else run_plan answer = asyncio.run(runner(user_input)) print(f"\ntriage> {answer}") return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv))