diff --git a/tasks.py b/tasks.py index 839bb8a3..4f69e6dd 100644 --- a/tasks.py +++ b/tasks.py @@ -15,6 +15,7 @@ from gitea_client import GiteaClient HERMES_HOME = Path.home() / ".hermes" TIMMY_HOME = Path.home() / ".timmy" HERMES_AGENT_DIR = HERMES_HOME / "hermes-agent" +HERMES_PYTHON = HERMES_AGENT_DIR / "venv" / "bin" / "python3" METRICS_DIR = TIMMY_HOME / "metrics" REPOS = [ "Timmy_Foundation/the-nexus", @@ -35,50 +36,133 @@ def newest_file(directory, pattern): files = sorted(directory.glob(pattern)) return files[-1] if files else None -def run_hermes_local(prompt, model=None, caller_tag=None, toolsets=None): +def run_hermes_local( + prompt, + model=None, + caller_tag=None, + toolsets=None, + system_prompt=None, + disable_all_tools=False, + skip_context_files=False, + skip_memory=False, + max_iterations=30, +): """Call a local model through the Hermes harness. - Uses provider="local-llama.cpp" which routes through the custom_providers - entry in config.yaml → llama-server at localhost:8081. + Runs Hermes inside its own venv so task execution matches the same + environment and provider routing as normal Hermes usage. Returns response text plus session metadata or None on failure. Every call creates a Hermes session with telemetry. """ _model = model or HEARTBEAT_MODEL tagged = f"[{caller_tag}] {prompt}" if caller_tag else prompt - # Import hermes cli.main directly — no subprocess, no env vars - _agent_dir = str(HERMES_AGENT_DIR) - if _agent_dir not in sys.path: - sys.path.insert(0, _agent_dir) - old_cwd = os.getcwd() - os.chdir(_agent_dir) - try: - from cli import main as hermes_main - import io - from contextlib import redirect_stdout, redirect_stderr + runner = """ +import io +import json +import sys +from contextlib import redirect_stderr, redirect_stdout +from pathlib import Path - buf = io.StringIO() - err = io.StringIO() - kwargs = dict( - query=tagged, - model=_model, - provider="local-llama.cpp", - quiet=True, +agent_dir = Path(sys.argv[1]) +query = sys.argv[2] +model = sys.argv[3] +system_prompt = sys.argv[4] or None +disable_all_tools = sys.argv[5] == "1" +skip_context_files = sys.argv[6] == "1" +skip_memory = sys.argv[7] == "1" +max_iterations = int(sys.argv[8]) +if str(agent_dir) not in sys.path: + sys.path.insert(0, str(agent_dir)) +from hermes_cli.runtime_provider import resolve_runtime_provider +from run_agent import AIAgent +from toolsets import get_all_toolsets + +buf = io.StringIO() +err = io.StringIO() +payload = {} +exit_code = 0 + +try: + runtime = resolve_runtime_provider() + kwargs = { + "model": model, + "api_key": runtime.get("api_key"), + "base_url": runtime.get("base_url"), + "provider": runtime.get("provider"), + "api_mode": runtime.get("api_mode"), + "acp_command": runtime.get("command"), + "acp_args": list(runtime.get("args") or []), + "max_iterations": max_iterations, + "quiet_mode": True, + "ephemeral_system_prompt": system_prompt, + "skip_context_files": skip_context_files, + "skip_memory": skip_memory, + } + if disable_all_tools: + kwargs["disabled_toolsets"] = sorted(get_all_toolsets().keys()) + agent = AIAgent(**kwargs) + with redirect_stdout(buf), redirect_stderr(err): + result = agent.run_conversation(query, sync_honcho=False) + payload = { + "response": result.get("final_response", ""), + "session_id": getattr(agent, "session_id", None), + "provider": runtime.get("provider"), + "base_url": runtime.get("base_url"), + "stdout": buf.getvalue(), + "stderr": err.getvalue(), + } +except Exception as exc: + exit_code = 1 + payload = { + "error": str(exc), + "stdout": buf.getvalue(), + "stderr": err.getvalue(), + } + +print(json.dumps(payload)) +sys.exit(exit_code) +""" + command = [ + str(HERMES_PYTHON) if HERMES_PYTHON.exists() else sys.executable, + "-c", + runner, + str(HERMES_AGENT_DIR), + tagged, + _model, + system_prompt or "", + "1" if disable_all_tools else "0", + "1" if skip_context_files else "0", + "1" if skip_memory else "0", + str(max_iterations), + ] + + result = subprocess.run( + command, + cwd=str(HERMES_AGENT_DIR), + capture_output=True, + text=True, + timeout=900, + ) + payload = json.loads((result.stdout or "").strip() or "{}") + output = str(payload.get("response", "")).strip() + stderr_output = str(payload.get("stderr", "")).strip() + stdout_output = str(payload.get("stdout", "")).strip() + if result.returncode != 0: + raise RuntimeError( + ( + result.stderr + or str(payload.get("error", "")).strip() + or stderr_output + or stdout_output + or output + or "hermes run failed" + ).strip() ) - if toolsets: - kwargs["toolsets"] = toolsets - with redirect_stdout(buf), redirect_stderr(err): - hermes_main(**kwargs) - output = buf.getvalue().strip() - session_id = None - lines = [] - for line in output.split("\n"): - if line.startswith("session_id:"): - session_id = line.split(":", 1)[1].strip() or None - continue - lines.append(line) - response = "\n".join(lines).strip() + + session_id = payload.get("session_id") + response = output # Log to metrics jsonl METRICS_DIR.mkdir(parents=True, exist_ok=True) @@ -100,7 +184,7 @@ def run_hermes_local(prompt, model=None, caller_tag=None, toolsets=None): return { "response": response, "session_id": session_id, - "raw_output": output, + "raw_output": json.dumps(payload, sort_keys=True), } except Exception as e: # Log failure @@ -116,8 +200,6 @@ def run_hermes_local(prompt, model=None, caller_tag=None, toolsets=None): with open(metrics_file, "a") as f: f.write(json.dumps(record) + "\n") return None - finally: - os.chdir(old_cwd) def hermes_local(prompt, model=None, caller_tag=None, toolsets=None): @@ -132,6 +214,28 @@ def hermes_local(prompt, model=None, caller_tag=None, toolsets=None): return result.get("response") +ARCHIVE_EPHEMERAL_SYSTEM_PROMPT = ( + "You are running a private archive-processing microtask for Timmy.\n" + "Use only the supplied user message.\n" + "Do not use tools, memory, Honcho, SOUL.md, AGENTS.md, or outside knowledge.\n" + "Do not invent facts.\n" + "If the prompt requests JSON, return only valid JSON." +) + + +def run_archive_hermes(prompt, caller_tag, model=None): + return run_hermes_local( + prompt=prompt, + model=model, + caller_tag=caller_tag, + system_prompt=ARCHIVE_EPHEMERAL_SYSTEM_PROMPT, + disable_all_tools=True, + skip_context_files=True, + skip_memory=True, + max_iterations=3, + ) + + # ── Know Thy Father: Twitter Archive Ingestion ─────────────────────── ARCHIVE_DIR = TIMMY_HOME / "twitter-archive" @@ -693,7 +797,7 @@ def _know_thy_father_impl(): prior_note=previous_note, batch_rows=batch_rows, ) - draft_run = run_hermes_local( + draft_run = run_archive_hermes( prompt=draft_prompt, caller_tag=f"know-thy-father-draft:{batch_id}", ) @@ -707,7 +811,7 @@ def _know_thy_father_impl(): return {"status": "error", "reason": "draft pass did not return JSON", "batch_id": batch_id} critique_prompt = build_archive_critique_prompt(batch_id=batch_id, draft_payload=draft_payload, batch_rows=batch_rows) - critique_run = run_hermes_local( + critique_run = run_archive_hermes( prompt=critique_prompt, caller_tag=f"know-thy-father-critique:{batch_id}", ) @@ -825,7 +929,7 @@ def _archive_weekly_insights_impl(): ) prompt = build_weekly_insight_prompt(profile=profile, recent_batches=recent_batches) - insight_run = run_hermes_local(prompt=prompt, caller_tag="archive-weekly-insights") + insight_run = run_archive_hermes(prompt=prompt, caller_tag="archive-weekly-insights") if not insight_run: return {"status": "error", "reason": "insight pass failed"}