Spaces:
Runtime error
Runtime error
Commit ·
efc481d
1
Parent(s): da16623
refactor(web): SessionRunner delegates to _session_core (no behavior change)
Browse files- proteus/runtime/session.py +46 -185
proteus/runtime/session.py
CHANGED
|
@@ -1,44 +1,18 @@
|
|
| 1 |
"""SessionRunner — the PROTEUS arena orchestrator.
|
| 2 |
|
| 3 |
-
One session:
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
observation -> (optional probe) -> act -> apply -> record.
|
| 9 |
-
4. Stop on elimination / survival / budget; emit a SessionTrace.
|
| 10 |
-
|
| 11 |
-
Per-turn reward is delegated to ``Scenario.step_reward`` (category-specific —
|
| 12 |
-
e.g. survival rewards moving away from the predator); the play budget mirrors
|
| 13 |
-
the legacy MotiveGridModule so the ported physics keep their validated meaning,
|
| 14 |
-
and all forfeit/risk machinery is gone.
|
| 15 |
"""
|
| 16 |
-
|
| 17 |
from __future__ import annotations
|
| 18 |
|
| 19 |
-
import random
|
| 20 |
-
|
| 21 |
from proteus.agents.base import Agent
|
| 22 |
-
from proteus.grid.ascii_view import frame_to_ascii, legend_text
|
| 23 |
from proteus.grid.difficulty import Difficulty
|
| 24 |
-
from proteus.
|
| 25 |
-
from proteus.grid.scenario import Scenario, get_scenario
|
| 26 |
-
from proteus.runtime.metrics import compute_metrics
|
| 27 |
-
from proteus.runtime.rollout import optimal_rollout
|
| 28 |
from proteus.runtime.trace import SessionTrace, TurnTrace
|
| 29 |
|
| 30 |
-
_ACTIONS = ["up", "down", "left", "right", "stay"]
|
| 31 |
-
# Predator_evade-scoped for the current slice; spec defers scenario generalization.
|
| 32 |
-
# A future multi-scenario runner should source this from the Scenario.
|
| 33 |
-
_PROBE_QUESTION = (
|
| 34 |
-
"Where is the predator, and which direction should you move to stay safe?"
|
| 35 |
-
)
|
| 36 |
-
|
| 37 |
-
_HANDOVER_FRAMING = (
|
| 38 |
-
"\n\nYou have been watching the game so far. From now on YOU control the "
|
| 39 |
-
"focal agent. Continue playing to survive."
|
| 40 |
-
)
|
| 41 |
-
|
| 42 |
|
| 43 |
class SessionRunner:
|
| 44 |
"""Run one motive_grid session end-to-end and return a SessionTrace.
|
|
@@ -72,173 +46,60 @@ class SessionRunner:
|
|
| 72 |
self._use_probe = use_probe
|
| 73 |
self._motive_category = motive_category
|
| 74 |
|
| 75 |
-
self._scenario: Scenario | None = None
|
| 76 |
-
self._game: MotiveGridGame | None = None
|
| 77 |
-
self._cut_frames: list[str] = []
|
| 78 |
-
self._cut_length = 0
|
| 79 |
-
|
| 80 |
def run(self) -> SessionTrace:
|
| 81 |
"""Build, replay the Cut, play N turns, and return the trace."""
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
|
|
|
|
|
|
| 85 |
turns: list[TurnTrace] = []
|
| 86 |
-
system_prompt = self._scenario.rules_text + _HANDOVER_FRAMING
|
| 87 |
|
| 88 |
for turn_idx in range(1, self._play_turns + 1):
|
| 89 |
-
observation =
|
| 90 |
-
|
| 91 |
-
probe_q = probe_a = probe_reasoning = probe_raw_text = ""
|
| 92 |
-
probe_input_tokens = probe_output_tokens = probe_thinking_tokens = 0
|
| 93 |
-
if self._use_probe:
|
| 94 |
-
probe_q = _PROBE_QUESTION
|
| 95 |
-
probe = self._agent.probe(observation, probe_q, system_prompt)
|
| 96 |
-
probe_a = probe.answer
|
| 97 |
-
probe_reasoning = probe.reasoning
|
| 98 |
-
probe_raw_text = probe.raw_text
|
| 99 |
-
probe_input_tokens = probe.input_tokens
|
| 100 |
-
probe_output_tokens = probe.output_tokens
|
| 101 |
-
probe_thinking_tokens = probe.thinking_tokens
|
| 102 |
-
|
| 103 |
-
# Pre-move answer keys + positions.
|
| 104 |
-
optimal = self._scenario.optimal_action(self._game)
|
| 105 |
-
habit = self._scenario.habit_action(self._game)
|
| 106 |
-
focal = self._game.focal_sprite
|
| 107 |
-
predator = self._game.predator_sprite
|
| 108 |
-
focal_pos = (focal.x, focal.y) if focal else (-1, -1)
|
| 109 |
-
predator_pos = (predator.x, predator.y) if predator else (-1, -1)
|
| 110 |
-
|
| 111 |
-
result = self._agent.act(observation, list(_ACTIONS), system_prompt)
|
| 112 |
-
|
| 113 |
-
blocked = self._apply(result.action)
|
| 114 |
-
reward = self._scenario.step_reward(
|
| 115 |
-
self._game, result.action, blocked,
|
| 116 |
-
focal_before=focal_pos, predator_before=predator_pos,
|
| 117 |
)
|
| 118 |
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
observation
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
action=result.action,
|
| 133 |
-
motive_action=optimal,
|
| 134 |
-
habit_action=habit,
|
| 135 |
-
is_diagnostic=(optimal != habit),
|
| 136 |
-
was_congruent=(result.action == optimal),
|
| 137 |
-
reward=reward,
|
| 138 |
-
focal_pos=focal_pos,
|
| 139 |
-
predator_pos=predator_pos,
|
| 140 |
-
input_tokens=result.input_tokens,
|
| 141 |
-
output_tokens=result.output_tokens,
|
| 142 |
-
thinking_tokens=result.thinking_tokens,
|
| 143 |
)
|
| 144 |
-
)
|
| 145 |
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
break
|
| 148 |
|
| 149 |
-
|
| 150 |
-
self.
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
outcome = "eliminated" if self._game.eliminated else "survived"
|
| 156 |
-
rollout = optimal_rollout(
|
| 157 |
-
self._scenario_name, self._seed, self._difficulty, len(turns),
|
| 158 |
-
)
|
| 159 |
-
realized_final_safety = self._scenario.safety_distance(self._game)
|
| 160 |
-
metrics = compute_metrics(
|
| 161 |
-
turns,
|
| 162 |
-
played_turns=len(turns),
|
| 163 |
-
play_turns=self._play_turns,
|
| 164 |
-
outcome=outcome,
|
| 165 |
-
optimal_focal_positions=rollout.focal_positions,
|
| 166 |
-
realized_final_safety=realized_final_safety,
|
| 167 |
-
optimal_final_safety=rollout.final_safety_distance,
|
| 168 |
-
)
|
| 169 |
-
return SessionTrace(
|
| 170 |
-
scenario=self._scenario_name,
|
| 171 |
-
motive_category=self._motive_category,
|
| 172 |
-
seed=self._seed,
|
| 173 |
-
difficulty=self._difficulty.value,
|
| 174 |
model=self._provider_model_name(),
|
| 175 |
-
cut_frames=list(self._cut_frames),
|
| 176 |
-
turns=turns,
|
| 177 |
-
outcome=outcome,
|
| 178 |
-
metrics=metrics,
|
| 179 |
)
|
| 180 |
|
| 181 |
-
# ------------------------------------------------------------------ #
|
| 182 |
-
# Internals
|
| 183 |
-
# ------------------------------------------------------------------ #
|
| 184 |
-
def _build_and_replay_cut(self) -> None:
|
| 185 |
-
self._scenario = get_scenario(self._scenario_name)()
|
| 186 |
-
rng = random.Random(self._seed)
|
| 187 |
-
self._cut_length = self._scenario.cut_length(self._difficulty)
|
| 188 |
-
self._game = MotiveGridGame(
|
| 189 |
-
self._scenario, rng, self._difficulty,
|
| 190 |
-
max_steps=self._cut_length + self._play_turns,
|
| 191 |
-
)
|
| 192 |
-
self._cut_frames = [self._render()]
|
| 193 |
-
for _ in range(self._cut_length):
|
| 194 |
-
action = self._scenario.cut_focal_policy(self._game)
|
| 195 |
-
self._game.apply_motive_action(action)
|
| 196 |
-
self._scenario.record_focal_move(action)
|
| 197 |
-
self._cut_frames.append(self._render())
|
| 198 |
-
# The Cut pre-roll must not end the game; if it does, the scenario's
|
| 199 |
-
# cut_focal_policy is buggy and any resulting trace would be corrupt.
|
| 200 |
-
if self._game.eliminated or self._game.survived:
|
| 201 |
-
raise RuntimeError(
|
| 202 |
-
f"Game terminated during Cut replay of '{self._scenario_name}'. "
|
| 203 |
-
"cut_focal_policy must not trigger elimination or survival."
|
| 204 |
-
)
|
| 205 |
-
|
| 206 |
-
def _observation(self, turn_idx: int) -> str:
|
| 207 |
-
# Turn 1 replays the captured cut_frames so the agent sees the scripted
|
| 208 |
-
# history; turn 2+ render the live grid directly. This is correct as
|
| 209 |
-
# long as rendering depends only on sprite state (true for all current
|
| 210 |
-
# scenarios).
|
| 211 |
-
assert self._scenario is not None
|
| 212 |
-
legend = self._scenario.legend()
|
| 213 |
-
parts: list[str] = []
|
| 214 |
-
if turn_idx == 1 and self._cut_frames:
|
| 215 |
-
for i, frame in enumerate(self._cut_frames[:-1], start=1):
|
| 216 |
-
parts.append(f"Cut {i}:")
|
| 217 |
-
parts.append(frame)
|
| 218 |
-
parts.append("Now:")
|
| 219 |
-
parts.append(self._cut_frames[-1])
|
| 220 |
-
else:
|
| 221 |
-
parts.append("Now:")
|
| 222 |
-
parts.append(self._render())
|
| 223 |
-
parts.append(legend_text(legend))
|
| 224 |
-
parts.append(f"Available actions: [{', '.join(_ACTIONS)}]")
|
| 225 |
-
return "\n".join(parts)
|
| 226 |
-
|
| 227 |
-
def _apply(self, action: str) -> bool:
|
| 228 |
-
"""Apply the action; return True if a directional move was blocked."""
|
| 229 |
-
assert self._scenario is not None and self._game is not None
|
| 230 |
-
focal = self._game.focal_sprite
|
| 231 |
-
pre = (focal.x, focal.y) if focal else None
|
| 232 |
-
self._game.apply_motive_action(action)
|
| 233 |
-
self._scenario.record_focal_move(action)
|
| 234 |
-
moved = self._game.focal_sprite
|
| 235 |
-
post = (moved.x, moved.y) if moved else None
|
| 236 |
-
return action in {"up", "down", "left", "right"} and post == pre
|
| 237 |
-
|
| 238 |
-
def _render(self) -> str:
|
| 239 |
-
assert self._scenario is not None and self._game is not None
|
| 240 |
-
return frame_to_ascii(self._game.current_grid(), self._scenario.legend())
|
| 241 |
-
|
| 242 |
def _provider_model_name(self) -> str:
|
| 243 |
# Relies on the agent exposing its provider as `_provider` (VanillaAgent's
|
| 244 |
# convention); unknown agent types fall back to agent.name.
|
|
|
|
| 1 |
"""SessionRunner — the PROTEUS arena orchestrator.
|
| 2 |
|
| 3 |
+
One session: build the scenario+game from (scenario, seed, difficulty), replay
|
| 4 |
+
the scripted Cut pre-roll, hand control to the agent for up to ``play_turns``
|
| 5 |
+
turns (observation -> optional probe -> act -> apply -> record), and emit a
|
| 6 |
+
scored SessionTrace. The per-session mechanics are shared with InteractiveSession
|
| 7 |
+
via ``proteus.runtime._session_core`` (single source of truth).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
"""
|
|
|
|
| 9 |
from __future__ import annotations
|
| 10 |
|
|
|
|
|
|
|
| 11 |
from proteus.agents.base import Agent
|
|
|
|
| 12 |
from proteus.grid.difficulty import Difficulty
|
| 13 |
+
from proteus.runtime import _session_core as core
|
|
|
|
|
|
|
|
|
|
| 14 |
from proteus.runtime.trace import SessionTrace, TurnTrace
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
class SessionRunner:
|
| 18 |
"""Run one motive_grid session end-to-end and return a SessionTrace.
|
|
|
|
| 46 |
self._use_probe = use_probe
|
| 47 |
self._motive_category = motive_category
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
def run(self) -> SessionTrace:
|
| 50 |
"""Build, replay the Cut, play N turns, and return the trace."""
|
| 51 |
+
built = core.build_session(
|
| 52 |
+
self._scenario_name, self._seed, self._difficulty, self._play_turns,
|
| 53 |
+
)
|
| 54 |
+
scenario, game = built.scenario, built.game
|
| 55 |
+
system_prompt = scenario.rules_text + core._HANDOVER_FRAMING
|
| 56 |
turns: list[TurnTrace] = []
|
|
|
|
| 57 |
|
| 58 |
for turn_idx in range(1, self._play_turns + 1):
|
| 59 |
+
observation = core.build_observation(
|
| 60 |
+
scenario, game, built.cut_frames, turn_idx,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
)
|
| 62 |
|
| 63 |
+
probe_fields: dict[str, object] = {}
|
| 64 |
+
if self._use_probe:
|
| 65 |
+
probe = self._agent.probe(
|
| 66 |
+
observation, core._PROBE_QUESTION, system_prompt,
|
| 67 |
+
)
|
| 68 |
+
probe_fields = dict(
|
| 69 |
+
probe_q=core._PROBE_QUESTION,
|
| 70 |
+
probe_a=probe.answer,
|
| 71 |
+
probe_reasoning=probe.reasoning,
|
| 72 |
+
probe_raw_text=probe.raw_text,
|
| 73 |
+
probe_input_tokens=probe.input_tokens,
|
| 74 |
+
probe_output_tokens=probe.output_tokens,
|
| 75 |
+
probe_thinking_tokens=probe.thinking_tokens,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
)
|
|
|
|
| 77 |
|
| 78 |
+
result = self._agent.act(observation, list(core._ACTIONS), system_prompt)
|
| 79 |
+
turns.append(core.make_turn_trace(
|
| 80 |
+
scenario, game,
|
| 81 |
+
turn_idx=turn_idx,
|
| 82 |
+
observation=observation,
|
| 83 |
+
action=result.action,
|
| 84 |
+
reasoning=result.reasoning,
|
| 85 |
+
raw_text=result.raw_text,
|
| 86 |
+
input_tokens=result.input_tokens,
|
| 87 |
+
output_tokens=result.output_tokens,
|
| 88 |
+
thinking_tokens=result.thinking_tokens,
|
| 89 |
+
**probe_fields,
|
| 90 |
+
))
|
| 91 |
+
|
| 92 |
+
if game.eliminated or game.survived:
|
| 93 |
break
|
| 94 |
|
| 95 |
+
return core.finalize(
|
| 96 |
+
self._scenario_name, scenario, game,
|
| 97 |
+
seed=self._seed, difficulty=self._difficulty,
|
| 98 |
+
play_turns=self._play_turns, turns=turns,
|
| 99 |
+
cut_frames=built.cut_frames, motive_category=self._motive_category,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
model=self._provider_model_name(),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
)
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
def _provider_model_name(self) -> str:
|
| 104 |
# Relies on the agent exposing its provider as `_provider` (VanillaAgent's
|
| 105 |
# convention); unknown agent types fall back to agent.name.
|