mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 11:56:23 +00:00
a3a5c272a2
Model backend rows gain a harness column (claude | pi). A pi-harness row runs the agent through the pi coding agent instead of the claude binary — pi speaks the OpenAI Completions API natively, so a bare vLLM/llama.cpp/Ollama endpoint needs no LiteLLM/claude-code-router translation proxy, and the loop is far lighter for slow local token throughput. The Claude subscription and existing claude-harness backends are untouched. Parity comes from generated per-agent artifacts under ~/.handler-pi (outside the repo tree, so the clean-tree gate never trips): models.json + settings.json render the row as a pi provider pinned as the default model; a bundled bridge extension (pi_bridge.ts) adapts pi's events to the exact stdin/stdout contract of `python -m handler.hooks` — the Stop/completion gate re-prompts pi with blockers via a follow-up message, git push runs the test/build/approval gates and denies on failure, questions defer through an ask_operator tool into the normal answer/resume flow, and memory recall is injected at session start. The memory tools are registered natively (pi has no MCP), shelling to a new `python -m handler.mcpserver --call <tool>` seam that reuses the MCP server's implementations. Skills reuse the same ~/.claude/skills sync (pi implements the same SKILL.md standard) plus the repo's committed .claude/skills. Sessions are single JSONL files pre-assigned via --session, so cross-worker resume archives/materializes exactly like claude's; the prompt travels on stdin (pi has no -- separator). The supervisor normalizes pi's event stream on the fly: assistant message_end feeds last_output, the final agent_end becomes the run result. The whole chain was validated live against pi 0.84.1 with a stub OpenAI endpoint: memory injection, push-gate denial (including the protected- branch approval gate), stop-gate block loop, and ask_operator pause all ran end to end through the real hooks and DB. Also: harness selector in the dashboard Models form, pi baked into the control image (NodeSource 22 for pi's node >= 22.19 floor), PI_BIN override, docs in docs/local-models.md, fake_pi fixture + 12 tests (361 total green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KdGv3u3DfTsP1S188KDhVH
127 lines
4.2 KiB
Python
Executable File
127 lines
4.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""A stand-in ``pi`` binary for pi-harness headless-runner tests.
|
|
|
|
Wired in via the ``pi_bin`` setting (the same seam ``fake_claude.py`` uses for
|
|
``claude_bin``). Parses the real pi-harness argv (``-p --mode json --no-extensions
|
|
-e <bridge> --session <path>``), reads the prompt from **stdin** (pi has no ``--``
|
|
separator, so that is how the supervisor delivers it), emits a scripted ``--mode json``
|
|
event stream on stdout, and appends to the genuine ``--session`` transcript file so
|
|
archive/materialize/resume paths exercise the real single-file layout. Behavior is
|
|
selected with ``FAKE_PI_MODE``:
|
|
|
|
- ``success`` (default): session header + user/assistant message_end + agent_end +
|
|
agent_settled, exit 0.
|
|
- ``error``: header + one garbage line + agent_end whose assistant stopReason is
|
|
``error``, then exit 1 (pi's exit code for an errored final message).
|
|
- ``hang``: header, then sleep forever (kill/cancel tests SIGTERM it).
|
|
|
|
``FAKE_PI_EXPECT_HISTORY=1`` makes a run fail loudly (exit 3) when the ``--session``
|
|
file does not already exist — the cross-worker resume tests use it to prove the
|
|
archive really was materialized where pi would look.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import signal
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
|
|
def _parse_argv(argv: list[str]) -> dict:
|
|
opts = {
|
|
"print": False,
|
|
"mode": None,
|
|
"no_extensions": False,
|
|
"extension": None,
|
|
"session": None,
|
|
}
|
|
i = 0
|
|
while i < len(argv):
|
|
arg = argv[i]
|
|
if arg == "-p":
|
|
opts["print"] = True
|
|
elif arg == "--mode":
|
|
i += 1
|
|
opts["mode"] = argv[i]
|
|
elif arg == "--no-extensions":
|
|
opts["no_extensions"] = True
|
|
elif arg == "-e":
|
|
i += 1
|
|
opts["extension"] = argv[i]
|
|
elif arg == "--session":
|
|
i += 1
|
|
opts["session"] = argv[i]
|
|
i += 1
|
|
return opts
|
|
|
|
|
|
def _emit(event: dict) -> None:
|
|
sys.stdout.write(json.dumps(event) + "\n")
|
|
sys.stdout.flush()
|
|
|
|
|
|
def _assistant(text: str) -> dict:
|
|
return {
|
|
"role": "assistant",
|
|
"content": [{"type": "text", "text": text}],
|
|
"stopReason": "stop",
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
|
mode = os.environ.get("FAKE_PI_MODE", "success")
|
|
opts = _parse_argv(sys.argv[1:])
|
|
if not opts["print"] or opts["mode"] != "json" or not opts["session"]:
|
|
sys.stderr.write("fake_pi: expected -p --mode json --session <path>\n")
|
|
return 64
|
|
if not opts["no_extensions"] or not opts["extension"]:
|
|
sys.stderr.write("fake_pi: expected --no-extensions with an explicit -e bridge\n")
|
|
return 64
|
|
|
|
prompt = sys.stdin.read().strip()
|
|
session_path = Path(opts["session"])
|
|
is_resume = session_path.exists()
|
|
if os.environ.get("FAKE_PI_EXPECT_HISTORY") and not is_resume:
|
|
sys.stderr.write(f"fake_pi: session file missing at {session_path}\n")
|
|
return 3
|
|
|
|
_emit({"type": "session", "version": 3, "id": "internal-uuid", "cwd": os.getcwd()})
|
|
if mode == "hang":
|
|
time.sleep(3600)
|
|
return 0
|
|
|
|
user_msg = {"role": "user", "content": [{"type": "text", "text": prompt}]}
|
|
_emit({"type": "agent_start"})
|
|
_emit({"type": "message_end", "message": user_msg})
|
|
if mode == "error":
|
|
sys.stdout.write("this is not json\n")
|
|
sys.stdout.flush()
|
|
errored = {
|
|
"role": "assistant",
|
|
"content": [],
|
|
"stopReason": "error",
|
|
"errorMessage": "Connection error.",
|
|
}
|
|
_emit({"type": "agent_end", "messages": [user_msg, errored], "willRetry": False})
|
|
return 1
|
|
|
|
assistant = _assistant(f"working on: {prompt}")
|
|
_emit({"type": "message_end", "message": assistant})
|
|
|
|
session_path.parent.mkdir(parents=True, exist_ok=True)
|
|
with session_path.open("a") as fh:
|
|
fh.write(json.dumps({"type": "message", "message": user_msg}) + "\n")
|
|
fh.write(json.dumps({"type": "message", "message": assistant}) + "\n")
|
|
|
|
_emit({"type": "agent_end", "messages": [user_msg, assistant], "willRetry": False})
|
|
_emit({"type": "agent_settled"})
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|