Spaces:
Running
Running
File size: 6,146 Bytes
cb15568 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | """Handoff ablation โ hand a model a partially-played game.
A handoff episode is split: a `prefix` controller plays the first `k`
turns, then the model inherits the live game state and finishes it.
It is a PURE STATE handoff โ the model gets no transcript of the
prefix, only the board it produced ("take over from here").
Sweeping the prefix QUALITY decomposes two capabilities:
* a **good** prefix (a winning trajectory) โ can the model *capitalize
on an advantage*? A flat-low outcome curve means it derails even a
won position.
* a **bad** prefix (a losing trajectory, or `stall`) โ can the model
*recover from a deficit*? This is the controlled measurement of the
freeze-and-panic failure mode: handed a losing board, does the model
fight (retreat / redirect) or sit on `observe`/`stop`? The
`passivity` stat on the result quantifies exactly that.
The prefix is a recorded run replayed turn-for-turn. Because engine
actor ids are seed-deterministic, a replayed trajectory MUST come from
the same `pack:level:seed` as the handoff episode.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from .controller import BaseController, as_controller, introspection_source
# A turn is "passive" when the model issued nothing but these โ the
# freeze-and-panic tell (low-commitment default instead of an active
# retreat / redirect).
_PASSIVE_TOOLS = {"observe", "stop"}
def stall_policy(render_state: dict, Command: Any) -> list:
"""The canonical losing prefix: do nothing, every turn. Synthesises
a guaranteed-deficit handoff with no recorded trajectory needed."""
return [Command.observe()]
def _load_trajectory(source: Any) -> list[list[dict]]:
"""Per-turn tool-call lists from a recorded run. `source` may be a
ready list, a Playback directory, or a path to its messages.json."""
if isinstance(source, list):
return source
p = Path(source)
if p.is_dir():
p = p / "messages.json"
msgs = json.loads(p.read_text())
turns: list[list[dict]] = []
for m in msgs:
if m.get("role") != "assistant":
continue
calls: list[dict] = []
for tc in m.get("tool_calls") or []:
fn = tc.get("function") or {}
args = fn.get("arguments")
if isinstance(args, str):
try:
args = json.loads(args)
except (ValueError, TypeError):
args = {}
calls.append({"name": fn.get("name"), "arguments": args or {}})
turns.append(calls)
return turns
class TrajectoryController(BaseController):
"""Replays a recorded run: turn N re-issues the commands the
recorded agent issued on its turn N. Past the recording's end it
falls back to `observe()`. Used as a deterministic handoff prefix โ
a `win`-outcome run is a good prefix, a `loss` is a bad one."""
def __init__(self, source: Any, name: str | None = None) -> None:
super().__init__(name or "trajectory")
self._turns = _load_trajectory(source)
self._i = 0
def reset(self, ctx: Any) -> None:
self._i = 0
def act(self, observation: dict, Command: Any) -> list:
from .agent import _to_commands
if self._i < len(self._turns):
calls = self._turns[self._i]
self._i += 1
return _to_commands(calls, Command) or [Command.observe()]
return [Command.observe()]
def _is_passive(cmds: list, _cmd_name) -> bool:
"""A turn with no command other than observe/stop (or no command)."""
if not cmds:
return True
return all((_cmd_name(c) or "") in _PASSIVE_TOOLS for c in cmds)
class HandoffController(BaseController):
"""`prefix` plays turns 0..k-1, then `main` inherits the live state
and finishes the episode. Pure state handoff โ `main` carries no
transcript of the prefix.
`handoff_stats` accumulates, over the MAIN agent's turns only:
`main_turns`, `passive_turns` (observe/stop-only), and `passivity`
(their ratio) โ the freeze-and-panic signal. When the prefix handed
`main` a losing position, `passivity` IS passivity-under-pressure."""
def __init__(
self, prefix: Any, main: Any, k: int, name: str | None = None
) -> None:
super().__init__(name or f"handoff-k{int(k)}")
self._prefix = as_controller(prefix)
self._main = as_controller(main)
self._k = max(0, int(k))
self._turn = 0
# Playback should record the MAIN agent's transcript, not this
# wrapper's โ expose it as the introspection source.
self.source = introspection_source(self._main)
self.handoff_stats = self._fresh_stats()
def _fresh_stats(self) -> dict:
return {
"k": self._k, "main_turns": 0,
"passive_turns": 0, "passivity": 0.0,
}
def reset(self, ctx: Any) -> None:
self._turn = 0
self._prefix.reset(ctx)
self._main.reset(ctx)
self.handoff_stats = self._fresh_stats()
def act(self, observation: dict, Command: Any) -> list:
if self._turn < self._k:
self._turn += 1
return self._prefix.act(observation, Command)
cmds = self._main.act(observation, Command)
self._turn += 1
from .eval_core import _cmd_tool_name
st = self.handoff_stats
st["main_turns"] += 1
if _is_passive(cmds, _cmd_tool_name):
st["passive_turns"] += 1
st["passivity"] = st["passive_turns"] / st["main_turns"]
return cmds
def run_handoff(
compiled: Any, main: Any, prefix: Any, k: int,
seed: int = 0, playback: Any = None,
):
"""Run a handoff episode: `prefix` plays the first `k` turns, `main`
finishes. Returns the `EpisodeResult` with `.handoff_stats` attached
(k, main_turns, passive_turns, passivity)."""
from .eval_core import run_level
ctrl = HandoffController(prefix, main, k)
res = run_level(compiled, ctrl, seed, playback)
res.handoff_stats = dict(ctrl.handoff_stats)
return res
|