diff --git a/agentmain.py b/agentmain.py index 2f20f6de..be751462 100644 --- a/agentmain.py +++ b/agentmain.py @@ -14,10 +14,12 @@ except Exception: pass from ga import GenericAgentHandler, smart_format, get_global_memory, format_error, consume_file script_dir = os.path.dirname(os.path.abspath(__file__)) +BANNED_TOOLS = (['ask_user', 'start_long_term_update'] if '--no-user-tools' in sys.argv else []) def load_tool_schema(suffix=''): global TOOLS_SCHEMA TS = open(os.path.join(script_dir, f'assets/tools_schema{suffix}.json'), 'r', encoding='utf-8').read() TOOLS_SCHEMA = json.loads(TS if os.name == 'nt' else TS.replace('powershell', 'bash')) + TOOLS_SCHEMA = [t for t in TOOLS_SCHEMA if t.get('function', {}).get('name') not in BANNED_TOOLS] load_tool_schema() lang_suffix = '_en' if os.environ.get('GA_LANG', '') == 'en' else '' @@ -205,6 +207,7 @@ if __name__ == '__main__': parser.add_argument('--verbose', action='store_true') parser.add_argument('--nobg', action='store_true') parser.add_argument('--nolog', action='store_true') + parser.add_argument('--no-user-tools', action='store_true') args, _unknown = parser.parse_known_args() _extra_args = dict(zip([k.lstrip('-') for k in _unknown[::2]], _unknown[1::2])) if _unknown else {} diff --git a/assets/ga_ultraplan.py b/assets/ga_ultraplan.py index 4690a530..653cfbc7 100644 --- a/assets/ga_ultraplan.py +++ b/assets/ga_ultraplan.py @@ -1,15 +1,43 @@ -from contextlib import contextmanager +from contextlib import contextmanager, redirect_stdout, redirect_stderr from concurrent.futures import ThreadPoolExecutor from time import time, sleep -import html, os, subprocess, sys, tempfile, threading, webbrowser +import html, io, json, os, re, subprocess, sys, tempfile, threading, traceback, urllib.request, webbrowser from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -__all__ = ["phase", "parallel", "mapchain"] +__all__ = ["plan", "phase", "parallel", "mapchain"] -_T0 = time(); _phases = []; _phase_stack = []; _tasks = []; _current = "idle"; _events = []; _srv = None; _last = time(); _lock = threading.Lock() -_RUN_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "temp", f"ultraplan_{int(_T0)}_{os.getpid()}") +_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +_PORT = int(os.environ.get("GA_ULTRAPLAN_PORT", "47831")) +_T0 = time(); _phases = []; _phase_stack = []; _tasks = []; _current = "idle"; _events = []; _srv = None; _last = time(); _lock = threading.Lock(); _exec_lock = threading.Lock() +_TASK_SLUG = "task"; _FUNC_SEQ = 0; _PLANNED = False; _SESSION = None; _sessions = {} +_RUN_DIR = os.path.abspath(os.environ.get("GA_ULTRAPLAN_RUNDIR", os.path.join(_ROOT, "temp", "ultraplan_default"))) os.makedirs(_RUN_DIR, exist_ok=True) +def _bind(rundir): + global _SESSION, _RUN_DIR, _phases, _phase_stack, _tasks, _current, _events, _FUNC_SEQ, _TASK_SLUG + key = os.path.abspath(rundir); os.makedirs(key, exist_ok=True) + s = _sessions.setdefault(key, {"rundir": key, "phases": [], "phase_stack": [], "tasks": [], "current": "idle", "events": [], "func_seq": 0, "task_slug": "task"}) + _SESSION = key; _RUN_DIR = key; _phases = s["phases"]; _phase_stack = s["phase_stack"]; _tasks = s["tasks"]; _current = s["current"]; _events = s["events"]; _FUNC_SEQ = s["func_seq"]; _TASK_SLUG = s["task_slug"] + return s + +def _save_session(): + if _SESSION in _sessions: + _sessions[_SESSION].update(current=_current, func_seq=_FUNC_SEQ, task_slug=_TASK_SLUG) + +def _need_plan(): + if not _PLANNED: raise RuntimeError("call plan(rundir) as the first UltraPlan statement") + +def _slug(s): + s = re.sub(r"[^a-zA-Z0-9]+", "_", str(s)).strip("_").lower() + return s[:80] or "task" + +def _task_slug(path): + stem = os.path.splitext(os.path.basename(path or "task"))[0] + parts = [_slug(x) for x in re.split(r"[_\-]+", stem)] + stop = {"ultra", "ultraplan", "script", "boot", "build", "test", "debug", "verify", "explore", "reduce", "phase"} + parts = [p for p in parts if p and not p.isdigit() and p not in stop] + return "_".join(parts) or _slug(stem) + def _note(s): global _last with _lock: @@ -27,39 +55,78 @@ def _phase_lines(nodes, depth=0): def _page(): with _lock: - lines = ["GA UltraPlan", "", f"current: {_current}", "", "phases:"] - lines += _phase_lines(_phases) or ["(none)"] - lines += ["", "recent tasks:"] - lines += [f"{t['status']:<7} {t['desc']}" for t in _tasks[-12:]] or ["(none)"] - lines += ["", "events:", *_events[-30:]] + lines = ["GA UltraPlan"] + for key, s in _sessions.items(): + lines += ["", f"== {os.path.basename(key) or key} ==", f"rundir: {key}", f"current: {s['current']}", "", "phases:"] + lines += _phase_lines(s["phases"]) or ["(none)"] + lines += ["", "recent tasks:"] + lines += [f"{t['status']:<7} {t['desc']}" for t in s["tasks"][-12:]] or ["(none)"] + lines += ["", "events:", *s["events"][-30:]] + if not _sessions: lines += ["", "(no sessions)"] return "
" + html.escape("\n".join(lines)) + "
" class _H(BaseHTTPRequestHandler): def do_GET(self): b = _page().encode("utf-8"); self.send_response(200); self.send_header("Content-Type", "text/html; charset=utf-8"); self.end_headers(); self.wfile.write(b) + def do_POST(self): + global _TASK_SLUG, _PLANNED + if self.path != "/exec": self.send_response(404); self.end_headers(); return + n = int(self.headers.get("Content-Length", "0")); req = json.loads(self.rfile.read(n).decode("utf-8")) + out = io.StringIO(); err = io.StringIO(); rc = 0 + with _exec_lock, redirect_stdout(out), redirect_stderr(err): + _bind(req["rundir"]); _note("exec: " + req.get("path", "