常规归档

This commit is contained in:
Myfro9
2026-05-24 21:20:26 -05:00
parent 15e8c5efa3
commit fda6c9a114
6 changed files with 821 additions and 4 deletions
@@ -0,0 +1,196 @@
from __future__ import annotations
import argparse
import base64
import json
import logging
import sys
from pathlib import Path
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.types import Command
from agents import AgentRunner
from workflow import build_workflow, resolve_notes_dir, resolve_work_dir
DB_FILENAME = "workflow_state.db"
def parse_command(value: str | None) -> list[str] | None:
if not value:
return None
try:
parsed = json.loads(value)
except json.JSONDecodeError:
# Fallback: the string may contain Windows paths with unescaped backslashes
# that make it invalid JSON. Re-escape backslashes and retry before giving up.
try:
import re
# Replace bare backslashes (not already doubled) with \\
re_escaped = re.sub(r'\\(?![\\"/bfnrtu])', r'\\\\', value)
parsed = json.loads(re_escaped)
except json.JSONDecodeError:
stripped = value.strip()
if stripped.startswith("[") and stripped.endswith("]"):
# Last-resort comma split; strip surrounding quotes from each item.
parsed = [item.strip().strip('"').strip("'")
for item in stripped[1:-1].split(",")]
else:
raise
if not isinstance(parsed, list) or not all(isinstance(item, str) for item in parsed):
raise argparse.ArgumentTypeError("command must be a JSON string array")
return parsed
def parse_command_b64(value: str | None) -> list[str] | None:
if not value:
return None
try:
decoded = base64.b64decode(value).decode("utf-8")
except Exception as exc:
raise argparse.ArgumentTypeError(f"invalid base64 command: {exc}") from exc
return parse_command(decoded)
def parse_json_b64(value: str | None):
if not value:
return None
try:
decoded = base64.b64decode(value).decode("utf-8")
except Exception as exc:
raise argparse.ArgumentTypeError(f"invalid base64 JSON: {exc}") from exc
return json.loads(decoded)
def run_workflow(
*,
task_id: str,
user_prompt: str = "",
notes_dir: str = "Notes",
work_dir: str = "",
runner: AgentRunner,
thread_id: str | None = None,
resume_payload=None,
) -> dict:
effective_thread = thread_id or task_id
resolved_work_dir = resolve_work_dir(work_dir)
resolved_notes_dir = resolve_notes_dir(resolved_work_dir, notes_dir)
resolved_notes_dir.mkdir(parents=True, exist_ok=True)
db_path = resolved_notes_dir / DB_FILENAME
with SqliteSaver.from_conn_string(str(db_path)) as checkpointer:
app = build_workflow(runner, checkpointer=checkpointer)
config = {"configurable": {"thread_id": effective_thread}}
if resume_payload is not None:
result = app.invoke(Command(resume=resume_payload), config=config)
else:
result = app.invoke(
{
"task_id": task_id,
"user_prompt": user_prompt,
"notes_dir": str(resolved_notes_dir),
"work_dir": str(resolved_work_dir),
},
config=config,
)
interrupts = result.get("__interrupt__", [])
if interrupts:
intr = interrupts[0]
action_required = intr.value if hasattr(intr, "value") else intr
return {
"status": "waiting",
"thread_id": effective_thread,
"db_path": str(db_path),
"action_required": action_required,
}
return {
"status": "done",
"thread_id": effective_thread,
"db_path": str(db_path),
"task_path": result.get("task_path", ""),
"latest_discuss_path": result.get("latest_discuss_path", ""),
"work_dir": result.get("work_dir", ""),
"codex_session_id": result.get("codex_session_id", ""),
"has_serious_issue": result.get("has_serious_issue", False),
"should_exit": result.get("should_exit", True),
}
def main() -> int:
parser = argparse.ArgumentParser(
description="Run the LangGraph Claude/Codex review workflow."
)
parser.add_argument("--task-id", required=True, help="Task id, for example 02")
parser.add_argument("--prompt", default="", help="User request recorded for traceability")
parser.add_argument("--notes-dir", default="Notes", help="Directory for Task/discuss files")
parser.add_argument("--work-dir", default="", help="Claude Code launch working directory")
parser.add_argument(
"--codex-review-command",
type=parse_command,
default=None,
help="JSON argv for Codex review step.",
)
parser.add_argument(
"--codex-review-command-b64",
type=parse_command_b64,
default=None,
help="Base64-encoded JSON argv for Codex review step.",
)
parser.add_argument("--timeout", type=int, default=600)
parser.add_argument(
"--thread-id",
default="",
help="Thread ID for resuming an existing run (defaults to task-id)",
)
parser.add_argument(
"--resume",
default="",
help=(
"JSON payload for Command(resume=...). "
"Use true for plan/revise/rejection steps; {\"accepts\":true} for decision step."
),
)
parser.add_argument(
"--resume-b64",
default="",
help="Base64-encoded JSON payload for Command(resume=...).",
)
parser.add_argument("--verbose", "-v", action="store_true", help="Enable debug logging to stderr.")
args = parser.parse_args()
if args.verbose:
logging.basicConfig(level=logging.DEBUG, stream=sys.stderr,
format="%(asctime)s %(levelname)s %(message)s")
codex_review_command = args.codex_review_command_b64 or args.codex_review_command
if not codex_review_command:
parser.error("--codex-review-command or --codex-review-command-b64 is required")
runner = AgentRunner(
codex_review_command=codex_review_command,
timeout=args.timeout,
)
# resume=True tells LangGraph to resume from the last interrupt without a specific value.
# Callers should pass 'true' (JSON boolean) for plan/revise/rejection steps.
resume_payload = parse_json_b64(args.resume_b64) if args.resume_b64 else (
json.loads(args.resume) if args.resume else None
)
output = run_workflow(
task_id=args.task_id,
user_prompt=args.prompt,
notes_dir=args.notes_dir,
work_dir=args.work_dir,
runner=runner,
thread_id=args.thread_id or None,
resume_payload=resume_payload,
)
print(json.dumps(output, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())