#!/usr/bin/env python3 """In-process driver for the plan-review graph — NO langgraph dev server. Builds the compiled graph in-process (workflow.build_workflow) with a SqliteSaver so state survives between invocations, and drives interrupts directly. Sets HTTP(S)_PROXY from the Windows system proxy (or PLAN_REVIEW_PROXY) so the codex subprocess spawned by the review node can reach its backend through the proxy. CLI (mirrors the old run_workflow.py): --task-id 10 [--work-dir DIR] [--notes-dir Notes] [--prompt "..."] [--resume-b64 ] [--codex-review-command-b64 ] [--timeout 600] Prints one JSON line: {"status":"waiting","action_required":{...}} or {"status":"done",...}. """ from __future__ import annotations import os, sys, json, base64, argparse, sqlite3 from pathlib import Path SCRIPTS = Path(__file__).resolve().parent sys.path.insert(0, str(SCRIPTS)) def resolve_proxy() -> str: px = os.environ.get("PLAN_REVIEW_PROXY", "").strip() if not px: try: import winreg k = winreg.OpenKey( winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Internet Settings", ) enable, _ = winreg.QueryValueEx(k, "ProxyEnable") server, _ = winreg.QueryValueEx(k, "ProxyServer") if enable and server and "=" not in str(server): # simple host:port, not per-scheme px = "http://" + str(server) except Exception: px = "" if px: for v in ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"): os.environ[v] = px return px def open_saver(db_path: str): from langgraph.checkpoint.sqlite import SqliteSaver conn = sqlite3.connect(db_path, check_same_thread=False) saver = SqliteSaver(conn) try: saver.setup() except Exception: pass return saver def extract_interrupt(result): if not isinstance(result, dict): return None ints = result.get("__interrupt__") if not ints: return None first = ints[0] if isinstance(ints, (list, tuple)) else ints return getattr(first, "value", first) def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--task-id", required=True) ap.add_argument("--work-dir", default=".") ap.add_argument("--notes-dir", default="Notes") ap.add_argument("--prompt", default="") ap.add_argument("--resume-b64", default="") ap.add_argument("--codex-review-command-b64", default="") ap.add_argument("--timeout", type=int, default=600) ap.add_argument("--db", default=str(SCRIPTS / ".plan_review_state.sqlite")) args = ap.parse_args() proxy = resolve_proxy() from workflow import build_workflow from langgraph.types import Command codex_cmd = None if args.codex_review_command_b64: codex_cmd = json.loads(base64.b64decode(args.codex_review_command_b64).decode("utf-8")) saver = open_saver(args.db) graph = build_workflow(checkpointer=saver) config = {"configurable": {"thread_id": str(args.task_id), "codex_timeout": args.timeout}} if codex_cmd: config["configurable"]["codex_review_command"] = codex_cmd if args.resume_b64: resume_val = json.loads(base64.b64decode(args.resume_b64).decode("utf-8")) result = graph.invoke(Command(resume=resume_val), config) else: init = { "task_id": str(args.task_id), "user_prompt": args.prompt, "work_dir": args.work_dir, "notes_dir": args.notes_dir, "codex_session_id": "", } result = graph.invoke(init, config) payload = extract_interrupt(result) if payload is not None: out = {"status": "waiting", "thread_id": str(args.task_id), "action_required": payload, "proxy": proxy} else: out = {"status": "done", "thread_id": str(args.task_id), "has_serious_issue": bool(result.get("has_serious_issue")), "review_comment": result.get("review_comment", ""), "proxy": proxy} print(json.dumps(out, ensure_ascii=False)) return 0 if __name__ == "__main__": raise SystemExit(main())