feat(launch): ADV_MODEL — per-role model override for the Adversary loop
This commit is contained in:
@ -12,6 +12,7 @@ Usage:
|
|||||||
Env (all optional — defaults shown):
|
Env (all optional — defaults shown):
|
||||||
LOOP_BACKEND claude (default) | opencode
|
LOOP_BACKEND claude (default) | opencode
|
||||||
LOOP_MODEL model flag, e.g. "sonnet" (claude) or "tinfoil/deepseek-v4-pro" (opencode)
|
LOOP_MODEL model flag, e.g. "sonnet" (claude) or "tinfoil/deepseek-v4-pro" (opencode)
|
||||||
|
ADV_MODEL optional model override for the Adversary only (falls back to LOOP_MODEL)
|
||||||
RESUME_PHASE 1 = keep current phase index on start (default resets to 0)
|
RESUME_PHASE 1 = keep current phase index on start (default resets to 0)
|
||||||
|
|
||||||
CLAUDE_BIN claude
|
CLAUDE_BIN claude
|
||||||
@ -46,9 +47,10 @@ LOG_DIR = os.environ.get("LOG_DIR", "/srv/cc-ci/.cc-ci-logs")
|
|||||||
# Backend is read from env, falling back to a persisted file written by `start`.
|
# Backend is read from env, falling back to a persisted file written by `start`.
|
||||||
# This ensures the watchdog (which runs in its own tmux session without the caller's env)
|
# This ensures the watchdog (which runs in its own tmux session without the caller's env)
|
||||||
# uses the same backend/model when it restarts a dead session.
|
# uses the same backend/model when it restarts a dead session.
|
||||||
_BACKEND_FILE = os.path.join(LOG_DIR, ".loop-backend")
|
_BACKEND_FILE = os.path.join(LOG_DIR, ".loop-backend")
|
||||||
_PHASES_FILE = os.path.join(LOG_DIR, ".phases-spec")
|
_PHASES_FILE = os.path.join(LOG_DIR, ".phases-spec")
|
||||||
_MODEL_FILE = os.path.join(LOG_DIR, ".loop-model")
|
_MODEL_FILE = os.path.join(LOG_DIR, ".loop-model")
|
||||||
|
_ADV_MODEL_FILE = os.path.join(LOG_DIR, ".loop-model-adv")
|
||||||
|
|
||||||
def _read_file_default(path, default):
|
def _read_file_default(path, default):
|
||||||
try:
|
try:
|
||||||
@ -59,6 +61,12 @@ def _read_file_default(path, default):
|
|||||||
|
|
||||||
BACKEND = os.environ.get("LOOP_BACKEND") or _read_file_default(_BACKEND_FILE, "claude")
|
BACKEND = os.environ.get("LOOP_BACKEND") or _read_file_default(_BACKEND_FILE, "claude")
|
||||||
LOOP_MODEL = os.environ.get("LOOP_MODEL") or _read_file_default(_MODEL_FILE, "")
|
LOOP_MODEL = os.environ.get("LOOP_MODEL") or _read_file_default(_MODEL_FILE, "")
|
||||||
|
# Per-role override: the Adversary may run a different model (e.g. opus for review while the
|
||||||
|
# Builder runs fable). Empty → falls back to LOOP_MODEL at the call site.
|
||||||
|
ADV_MODEL = os.environ.get("ADV_MODEL") or _read_file_default(_ADV_MODEL_FILE, "")
|
||||||
|
|
||||||
|
def role_model(role):
|
||||||
|
return ADV_MODEL if (role == "adversary" and ADV_MODEL) else LOOP_MODEL
|
||||||
REMOTE_CONTROL = os.environ.get("REMOTE_CONTROL", "1") == "1"
|
REMOTE_CONTROL = os.environ.get("REMOTE_CONTROL", "1") == "1"
|
||||||
|
|
||||||
CLAUDE_BIN = os.environ.get("CLAUDE_BIN", "claude")
|
CLAUDE_BIN = os.environ.get("CLAUDE_BIN", "claude")
|
||||||
@ -245,14 +253,15 @@ def start_agent(role, session, workdir):
|
|||||||
kf = Path(LOG_DIR) / f".kickoff-{session}.txt"
|
kf = Path(LOG_DIR) / f".kickoff-{session}.txt"
|
||||||
kf.write_text(build_kickoff(role, idx))
|
kf.write_text(build_kickoff(role, idx))
|
||||||
|
|
||||||
model_flag = f"--model '{LOOP_MODEL}'" if LOOP_MODEL else ""
|
model = role_model(role)
|
||||||
|
model_flag = f"--model '{model}'" if model else ""
|
||||||
|
|
||||||
session_cwd = workdir
|
session_cwd = workdir
|
||||||
|
|
||||||
if BACKEND == "claude":
|
if BACKEND == "claude":
|
||||||
rc = f"--remote-control '{session}'" if REMOTE_CONTROL else ""
|
rc = f"--remote-control '{session}'" if REMOTE_CONTROL else ""
|
||||||
cmd = f"{CLAUDE_BIN} {rc} {model_flag} {CLAUDE_FLAGS} \"$(cat '{kf}')\""
|
cmd = f"{CLAUDE_BIN} {rc} {model_flag} {CLAUDE_FLAGS} \"$(cat '{kf}')\""
|
||||||
log(f"starting {session} (backend=claude, phase={pid}, plan={plan}, model={LOOP_MODEL or 'default'})")
|
log(f"starting {session} (backend=claude, phase={pid}, plan={plan}, model={model or 'default'})")
|
||||||
elif BACKEND == "opencode":
|
elif BACKEND == "opencode":
|
||||||
# Attach each TUI to the shared opencode web server so sessions are recorded the same
|
# Attach each TUI to the shared opencode web server so sessions are recorded the same
|
||||||
# way as browser-created sessions, including a populated `path` in the DB.
|
# way as browser-created sessions, including a populated `path` in the DB.
|
||||||
@ -263,7 +272,7 @@ def start_agent(role, session, workdir):
|
|||||||
f"set -a; . /srv/cc-ci/.testenv; set +a; "
|
f"set -a; . /srv/cc-ci/.testenv; set +a; "
|
||||||
f"NO_COLOR=1 {OPENCODE_BIN} attach {OPENCODE_SERVER} --dir {session_cwd}"
|
f"NO_COLOR=1 {OPENCODE_BIN} attach {OPENCODE_SERVER} --dir {session_cwd}"
|
||||||
)
|
)
|
||||||
log(f"starting {session} (backend=opencode, phase={pid}, model={LOOP_MODEL or 'default'})")
|
log(f"starting {session} (backend=opencode, phase={pid}, model={model or 'default'})")
|
||||||
log(f" visible at http://oc.commoninternet.net (tailnet only)")
|
log(f" visible at http://oc.commoninternet.net (tailnet only)")
|
||||||
else:
|
else:
|
||||||
die(f"unknown BACKEND '{BACKEND}' — set LOOP_BACKEND=claude or LOOP_BACKEND=opencode")
|
die(f"unknown BACKEND '{BACKEND}' — set LOOP_BACKEND=claude or LOOP_BACKEND=opencode")
|
||||||
@ -619,7 +628,7 @@ def start_watchdog():
|
|||||||
# caller's env — it would fall back to the default spec and mis-detect phase completion.
|
# caller's env — it would fall back to the default spec and mis-detect phase completion.
|
||||||
env_prefix = (
|
env_prefix = (
|
||||||
f"PHASES_SPEC='{PHASES_SPEC}' PHASE_IDX_FILE='{PHASE_IDX_FILE}' "
|
f"PHASES_SPEC='{PHASES_SPEC}' PHASE_IDX_FILE='{PHASE_IDX_FILE}' "
|
||||||
f"LOOP_BACKEND='{BACKEND}' LOOP_MODEL='{LOOP_MODEL}' "
|
f"LOOP_BACKEND='{BACKEND}' LOOP_MODEL='{LOOP_MODEL}' ADV_MODEL='{ADV_MODEL}' "
|
||||||
)
|
)
|
||||||
subprocess.run([
|
subprocess.run([
|
||||||
"tmux", "new-session", "-d", "-s", WATCHDOG_SESSION, "-c", PLAN_DIR,
|
"tmux", "new-session", "-d", "-s", WATCHDOG_SESSION, "-c", PLAN_DIR,
|
||||||
@ -692,8 +701,10 @@ def main():
|
|||||||
# Persist backend/model so the watchdog uses them when restarting dead sessions.
|
# Persist backend/model so the watchdog uses them when restarting dead sessions.
|
||||||
Path(_BACKEND_FILE).write_text(BACKEND)
|
Path(_BACKEND_FILE).write_text(BACKEND)
|
||||||
Path(_MODEL_FILE).write_text(LOOP_MODEL)
|
Path(_MODEL_FILE).write_text(LOOP_MODEL)
|
||||||
|
Path(_ADV_MODEL_FILE).write_text(ADV_MODEL)
|
||||||
Path(_PHASES_FILE).write_text(PHASES_SPEC)
|
Path(_PHASES_FILE).write_text(PHASES_SPEC)
|
||||||
log(f"backend={BACKEND} model={LOOP_MODEL or '<default>'} (persisted to {_BACKEND_FILE})")
|
log(f"backend={BACKEND} model={LOOP_MODEL or '<default>'} "
|
||||||
|
f"adv-model={ADV_MODEL or '<same>'} (persisted to {_BACKEND_FILE})")
|
||||||
log(f"phases='{all_ids()}' (persisted to {_PHASES_FILE})")
|
log(f"phases='{all_ids()}' (persisted to {_PHASES_FILE})")
|
||||||
start_loops()
|
start_loops()
|
||||||
start_watchdog()
|
start_watchdog()
|
||||||
@ -734,7 +745,7 @@ def main():
|
|||||||
launch.py logs builder|adversary|watchdog tail a log
|
launch.py logs builder|adversary|watchdog tail a log
|
||||||
launch.py watchdog run watchdog in foreground
|
launch.py watchdog run watchdog in foreground
|
||||||
|
|
||||||
Backend: {BACKEND} Model: {LOOP_MODEL or '<default>'}
|
Backend: {BACKEND} Model: {LOOP_MODEL or '<default>'} Adversary model: {ADV_MODEL or '<same>'}
|
||||||
Phase sequence ({len(PHASES)} phases, auto-advance on ## DONE, stop after last):
|
Phase sequence ({len(PHASES)} phases, auto-advance on ## DONE, stop after last):
|
||||||
{all_ids()}
|
{all_ids()}
|
||||||
""")
|
""")
|
||||||
|
|||||||
Reference in New Issue
Block a user