Spaces:
Running
Running
| """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 | |