Spaces:
Runtime error
Runtime error
File size: 13,434 Bytes
598e3bd da16623 598e3bd 426093b b731a03 426093b d36047a 598e3bd 6b31d7e 598e3bd e343386 598e3bd 2e77940 598e3bd e343386 598e3bd b731a03 90b2ce8 598e3bd 90b2ce8 598e3bd 90b2ce8 598e3bd 90b2ce8 b731a03 90b2ce8 598e3bd 90b2ce8 598e3bd 11cd1de 598e3bd b731a03 598e3bd b731a03 598e3bd d25a6fe b731a03 598e3bd d25a6fe b731a03 d36047a 598e3bd d25a6fe b731a03 d36047a 598e3bd b731a03 598e3bd b731a03 598e3bd 3bd65c5 598e3bd b731a03 28cd053 b731a03 598e3bd | 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 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 | """Shared session-core helpers.
The per-session logic (build + Cut replay, one-turn TurnTrace construction,
finalize -> scored SessionTrace) lives here so both SessionRunner (agent-driven,
batch) and InteractiveSession (HTTP-driven, stepwise) produce *identical* traces
from the same actions. This is a pure extraction of what used to be inline in
SessionRunner -- no behavior change (pinned by the existing suite). The one
intentional deviation: ``finalize`` raises ``SessionNotFinishedError`` where the
original used a bare ``assert`` (which vanishes under ``python -O``); the
terminal condition checked is identical.
"""
from __future__ import annotations
import random
from dataclasses import dataclass
import numpy as np
from proteus.game.engine.ascii_view import frame_to_ascii, legend_text
from proteus.game.engine.difficulty import Difficulty
from proteus.game.engine.grid import MotiveGridGame
from proteus.game.scenarios.base import Scenario, get_scenario
from proteus.game.runtime.memory import MemoryCheckpoint, render_memory_block
from proteus.game.metrics.metrics import compute_metrics
from proteus.game.metrics.persona import (
PersonaWeights,
pressure as persona_pressure,
reference_actions,
reward_rw,
)
from proteus.game.metrics.rollout import optimal_rollout
from proteus.game.runtime.trace import SessionTrace, TurnTrace
from proteus.game.agents.parsing import extract_self_belief
_ACTIONS = ["up", "down", "left", "right", "stay"]
_DIRECTIONS = {"up", "down", "left", "right"}
# Predator-evasion-scoped for the current slice; spec defers scenario generalization.
# A future multi-scenario runner should source this from the Scenario.
_PROBE_QUESTION = (
"Where is the predator, and which direction should you move to stay safe?"
)
_HANDOVER_FRAMING = (
"\n\nYou have been watching the game so far. From now on YOU control the "
"focal agent. Continue playing to survive."
)
class SessionFinishedError(RuntimeError):
"""Raised when stepping a session that has already reached a terminal state."""
class SessionNotFinishedError(RuntimeError):
"""Raised when finalizing a session that is not yet terminal / budget-done."""
@dataclass
class BuiltSession:
"""The product of build_session: the live scenario+game and the captured Cut.
cut_frames are ASCII (for the trace + the agent observation); cut_grids are
the same frames as integer palette grids (for the web color animation).
"""
scenario: Scenario
game: MotiveGridGame
cut_frames: list[str]
cut_grids: list[list[list[int]]]
default_memory: MemoryCheckpoint | None = None
def grid_to_list(grid: np.ndarray) -> list[list[int]]:
"""A (h, w) palette array -> JSON-serializable list[list[int]]."""
return [[int(v) for v in row] for row in grid]
def render_ascii(scenario: Scenario, game: MotiveGridGame) -> str:
"""Render the live grid for the trace/observation via the scenario's hook."""
return scenario.render_frame(game)
def build_session(
scenario_name: str,
seed: int | None,
difficulty: Difficulty,
play_turns: int,
) -> BuiltSession:
"""Build the scenario+game and replay the scripted Cut pre-roll.
Behaviour-identical to SessionRunner._build_and_replay_cut, plus it also
captures the per-frame integer palette grids for the web color animation.
"""
scenario = get_scenario(scenario_name)()
rng = random.Random(seed)
cut_length = scenario.cut_length(difficulty)
game = MotiveGridGame(
scenario, rng, difficulty, max_steps=cut_length + play_turns,
)
cut_frames = [render_ascii(scenario, game)]
cut_grids = [grid_to_list(game.current_grid())]
for _ in range(cut_length):
action = scenario.cut_focal_policy(game)
game.apply_motive_action(action)
scenario.record_focal_move(action)
cut_frames.append(render_ascii(scenario, game))
cut_grids.append(grid_to_list(game.current_grid()))
# The Cut pre-roll must not end the game; if it does, the scenario's
# cut_focal_policy is buggy and any resulting trace would be corrupt.
if game.eliminated or game.survived:
raise RuntimeError(
f"Game terminated during Cut replay of '{scenario_name}'. "
"cut_focal_policy must not trigger elimination or survival."
)
default_memory = scenario.default_memory(seed, difficulty)
return BuiltSession(scenario, game, cut_frames, cut_grids, default_memory)
def build_observation(
scenario: Scenario,
game: MotiveGridGame,
cut_frames: list[str],
turn_idx: int,
memory: MemoryCheckpoint | None = None,
prior_actions: list[str] | None = None,
) -> str:
"""The self-contained, auto-regressive observation the agent sees this turn.
Each turn the agent is called statelessly, so the observation must carry the
full context the model needs to continue its OWN trajectory:
* the handover ``memory`` (the prior episode / persona demonstration), shown
EVERY turn so the model never loses it after turn 1;
* the scripted ``cut_frames`` pre-roll (the lead-up before it took control);
* ``prior_actions`` — the moves the model has already committed THIS run, so
it plays auto-regressively (it can see and maintain its own line of play);
* the current grid (``"Now:"``).
``turn_idx`` is retained for the call signature; at turn 1 there are no
prior_actions and the current grid is the handover state, so the observation
matches the historical turn-1 layout.
"""
legend = scenario.legend()
parts: list[str] = []
if memory is not None:
parts.append(render_memory_block(memory))
parts.append("NOW — this run so far:")
if cut_frames:
for i, frame in enumerate(cut_frames[:-1], start=1):
parts.append(f"Cut {i}:")
parts.append(frame)
if prior_actions:
parts.append(
"Your moves so far this run (most recent last): "
+ ", ".join(prior_actions)
)
parts.append("Now:")
parts.append(render_ascii(scenario, game))
parts.append(legend_text(legend))
parts.append(f"Available actions: [{', '.join(scenario.action_set)}]")
return "\n".join(parts)
def apply_action(scenario: Scenario, game: MotiveGridGame, action: str) -> bool:
"""Apply the action; return True if a directional move was blocked.
Behaviour-identical to SessionRunner._apply.
"""
focal = game.focal_sprite
pre = (focal.x, focal.y) if focal else None
game.apply_motive_action(action)
scenario.record_focal_move(action)
moved = game.focal_sprite
post = (moved.x, moved.y) if moved else None
return action in _DIRECTIONS and post == pre
def make_turn_trace(
scenario: Scenario,
game: MotiveGridGame,
*,
turn_idx: int,
observation: str,
action: str,
reasoning: str = "",
raw_text: str = "",
input_tokens: int = 0,
output_tokens: int = 0,
thinking_tokens: int = 0,
probe_q: str = "",
probe_a: str = "",
probe_reasoning: str = "",
probe_raw_text: str = "",
probe_input_tokens: int = 0,
probe_output_tokens: int = 0,
probe_thinking_tokens: int = 0,
persona: PersonaWeights | None = None,
) -> TurnTrace:
"""Compute pre-move answer keys + positions, apply, score, build a TurnTrace.
Behaviour-identical to the SessionRunner play-loop body: pre-move answer
keys/positions are read BEFORE applying the action, the move is applied,
then step_reward is scored against the pre-move positions. When a CP8
persona is supplied, the reference action set / reference reward / pressure
are read pre-move and the model's own R_w + regret post-move (the model
never sees the weights — only the public reference set + scalars are stored).
"""
# Pre-move answer keys + positions.
optimal = scenario.optimal_action(game)
habit = scenario.habit_action(game)
focal = game.focal_sprite
predator = game.predator_sprite
focal_pos = (focal.x, focal.y) if focal else (-1, -1)
predator_pos = (predator.x, predator.y) if predator else (-1, -1)
pre_bfs = scenario.safety_distance(game)
# CP8 persona: reference action set + reference reward + pressure are read
# from the PRE-move state (the model never sees the weights).
ref_acts = ref_reward = turn_pressure = None
if persona is not None:
ref_acts = reference_actions(persona, scenario, game)
ref_reward = reward_rw(
persona, scenario, game, focal_pos, predator_pos, ref_acts[0],
)
turn_pressure = persona_pressure(scenario, game)
blocked = apply_action(scenario, game, action)
reward = scenario.step_reward(
game, action, blocked,
focal_before=focal_pos, predator_before=predator_pos,
)
# CP8: post-move positions + pre/post BFS + chase-corrected delta.
post_focal = game.focal_sprite
post_predator = game.predator_sprite
post_focal_pos = (post_focal.x, post_focal.y) if post_focal else None
post_predator_pos = (
(post_predator.x, post_predator.y) if post_predator else None
)
post_bfs = scenario.safety_distance(game)
agent_distance_delta = scenario.agent_distance_delta(
game, focal_pos, predator_pos
)
# CP8 persona: the model's own R_w (BFS geometry is static, so the pre-move
# positions + actual blocked status fully determine it) and its regret.
model_reward = reward_regret = None
if persona is not None:
model_reward = reward_rw(
persona, scenario, game, focal_pos, predator_pos, action,
blocked=blocked,
)
reward_regret = ref_reward - model_reward
# Find-your-body discovery: parse the optional SELF: report from raw_text
# and score it against the scenario's (hidden) true body index. No-op for
# non-discovery scenarios (discovery_candidates() == 0).
n_candidates = scenario.discovery_candidates()
self_belief = extract_self_belief(raw_text, n_candidates) if n_candidates else None
true_index = scenario.discovery_true_index()
self_correct = (
(self_belief == true_index)
if (self_belief is not None and true_index is not None)
else None
)
return TurnTrace(
turn_idx=turn_idx,
observation=observation,
probe_q=probe_q,
probe_a=probe_a,
probe_reasoning=probe_reasoning,
probe_raw_text=probe_raw_text,
probe_input_tokens=probe_input_tokens,
probe_output_tokens=probe_output_tokens,
probe_thinking_tokens=probe_thinking_tokens,
reasoning=reasoning,
raw_text=raw_text,
action=action,
motive_action=optimal,
habit_action=habit,
is_diagnostic=(optimal != habit),
was_congruent=(action == optimal),
reward=reward,
focal_pos=focal_pos,
predator_pos=predator_pos,
input_tokens=input_tokens,
output_tokens=output_tokens,
thinking_tokens=thinking_tokens,
post_focal_pos=post_focal_pos,
post_predator_pos=post_predator_pos,
pre_bfs_distance=pre_bfs,
post_bfs_distance=post_bfs,
agent_distance_delta=agent_distance_delta,
reference_actions=ref_acts,
reference_reward=ref_reward,
model_reward=model_reward,
reward_regret=reward_regret,
pressure=turn_pressure,
self_belief=self_belief,
self_correct=self_correct,
)
def finalize(
scenario_name: str,
scenario: Scenario,
game: MotiveGridGame,
*,
seed: int | None,
difficulty: Difficulty,
play_turns: int,
turns: list[TurnTrace],
cut_frames: list[str],
motive_category: str,
model: str,
memory_ref: str | None = None,
persona: PersonaWeights | None = None,
) -> SessionTrace:
"""Score the played turns and assemble the SessionTrace.
Behaviour-identical to the SessionRunner finalize block: requires a terminal
state or budget exhaustion, then scores against the optimal rollout. The CP7
``memory_ref`` and CP8 ``persona`` (its public id) are recorded on the trace.
"""
if not (game.eliminated or game.survived or len(turns) == play_turns):
raise SessionNotFinishedError(
"finalize called before a terminal state or budget exhaustion."
)
outcome = "eliminated" if game.eliminated else "survived"
rollout = optimal_rollout(scenario_name, seed, difficulty, len(turns))
realized_final_safety = scenario.safety_distance(game)
metrics = compute_metrics(
turns,
played_turns=len(turns),
play_turns=play_turns,
outcome=outcome,
optimal_focal_positions=rollout.focal_positions,
realized_final_safety=realized_final_safety,
optimal_final_safety=rollout.final_safety_distance,
max_bfs_distance=scenario.max_bfs_distance(game),
)
return SessionTrace(
scenario=scenario_name,
motive_category=motive_category,
seed=seed,
difficulty=difficulty.value,
model=model,
cut_frames=list(cut_frames),
turns=turns,
outcome=outcome,
metrics=metrics,
memory_ref=memory_ref,
horizon=play_turns,
persona_weight_id=(
persona.persona_weight_id if persona else None
),
)
|