Files
2026-07-20 18:14:49 +08:00

121 lines
4.2 KiB
Python

#!/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 <b64 json>] [--codex-review-command-b64 <b64 json>] [--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())