irregular6612 commited on
Commit
efc481d
·
1 Parent(s): da16623

refactor(web): SessionRunner delegates to _session_core (no behavior change)

Browse files
Files changed (1) hide show
  1. 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
- 1. Build the scenario + game from (scenario, seed, difficulty).
5
- 2. Replay the scripted Cut pre-roll, capturing ASCII frames (the history
6
- shown to the agent at handover).
7
- 3. Hand control to the agent and play up to ``play_turns`` turns:
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.grid.game import MotiveGridGame
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
- self._build_and_replay_cut()
83
- assert self._scenario is not None and self._game is not None
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 = self._observation(turn_idx)
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
- turns.append(
120
- TurnTrace(
121
- turn_idx=turn_idx,
122
- observation=observation,
123
- probe_q=probe_q,
124
- probe_a=probe_a,
125
- probe_reasoning=probe_reasoning,
126
- probe_raw_text=probe_raw_text,
127
- probe_input_tokens=probe_input_tokens,
128
- probe_output_tokens=probe_output_tokens,
129
- probe_thinking_tokens=probe_thinking_tokens,
130
- reasoning=result.reasoning,
131
- raw_text=result.raw_text,
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
- if self._game.eliminated or self._game.survived:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  break
148
 
149
- assert (
150
- self._game.eliminated
151
- or self._game.survived
152
- or len(turns) == self._play_turns
153
- ), "play loop exited without a terminal state or exhausting the budget"
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.