irregular6612 commited on
Commit
90b2ce8
·
1 Parent(s): e8e01c9

feat(runtime): auto-regressive play — handover memory every turn + the model's own prior moves in each observation (Markovian -> auto-regressive)

Browse files
proteus/runtime/_session_core.py CHANGED
@@ -119,26 +119,40 @@ def build_observation(
119
  cut_frames: list[str],
120
  turn_idx: int,
121
  memory: MemoryCheckpoint | None = None,
 
122
  ) -> str:
123
- """The ASCII observation the agent sees (turn 1 replays the Cut history).
124
 
125
- Behaviour-identical to SessionRunner._observation. When a CP7 memory
126
- checkpoint is supplied, its rendered block is prepended on turn 1.
 
 
 
 
 
 
 
 
 
 
 
127
  """
128
  legend = scenario.legend()
129
  parts: list[str] = []
130
- if turn_idx == 1 and memory is not None:
131
  parts.append(render_memory_block(memory))
132
  parts.append("NOW — this run so far:")
133
- if turn_idx == 1 and cut_frames:
134
  for i, frame in enumerate(cut_frames[:-1], start=1):
135
  parts.append(f"Cut {i}:")
136
  parts.append(frame)
137
- parts.append("Now:")
138
- parts.append(cut_frames[-1])
139
- else:
140
- parts.append("Now:")
141
- parts.append(render_ascii(scenario, game))
 
 
142
  parts.append(legend_text(legend))
143
  parts.append(f"Available actions: [{', '.join(_ACTIONS)}]")
144
  return "\n".join(parts)
 
119
  cut_frames: list[str],
120
  turn_idx: int,
121
  memory: MemoryCheckpoint | None = None,
122
+ prior_actions: list[str] | None = None,
123
  ) -> str:
124
+ """The self-contained, auto-regressive observation the agent sees this turn.
125
 
126
+ Each turn the agent is called statelessly, so the observation must carry the
127
+ full context the model needs to continue its OWN trajectory:
128
+
129
+ * the handover ``memory`` (the prior episode / persona demonstration), shown
130
+ EVERY turn so the model never loses it after turn 1;
131
+ * the scripted ``cut_frames`` pre-roll (the lead-up before it took control);
132
+ * ``prior_actions`` — the moves the model has already committed THIS run, so
133
+ it plays auto-regressively (it can see and maintain its own line of play);
134
+ * the current grid (``"Now:"``).
135
+
136
+ ``turn_idx`` is retained for the call signature; at turn 1 there are no
137
+ prior_actions and the current grid is the handover state, so the observation
138
+ matches the historical turn-1 layout.
139
  """
140
  legend = scenario.legend()
141
  parts: list[str] = []
142
+ if memory is not None:
143
  parts.append(render_memory_block(memory))
144
  parts.append("NOW — this run so far:")
145
+ if cut_frames:
146
  for i, frame in enumerate(cut_frames[:-1], start=1):
147
  parts.append(f"Cut {i}:")
148
  parts.append(frame)
149
+ if prior_actions:
150
+ parts.append(
151
+ "Your moves so far this run (most recent last): "
152
+ + ", ".join(prior_actions)
153
+ )
154
+ parts.append("Now:")
155
+ parts.append(render_ascii(scenario, game))
156
  parts.append(legend_text(legend))
157
  parts.append(f"Available actions: [{', '.join(_ACTIONS)}]")
158
  return "\n".join(parts)
proteus/runtime/interactive.py CHANGED
@@ -111,6 +111,7 @@ class InteractiveSession:
111
  observation = core.build_observation(
112
  self._scenario, self._game, self._cut_frames, turn_idx,
113
  memory=self._memory,
 
114
  )
115
  probe_fields: dict[str, object] = {}
116
  if self._use_probe:
 
111
  observation = core.build_observation(
112
  self._scenario, self._game, self._cut_frames, turn_idx,
113
  memory=self._memory,
114
+ prior_actions=[t.action for t in self._turns],
115
  )
116
  probe_fields: dict[str, object] = {}
117
  if self._use_probe:
proteus/runtime/memory_gen.py CHANGED
@@ -79,10 +79,17 @@ def generate_memory(
79
  action = reference_actions(persona, scenario, game)[0]
80
  reasoning = ""
81
  else:
82
- observation = "\n".join([
 
 
 
 
 
 
83
  "Now:", frame, legend_text(scenario.legend()),
84
  f"Available actions: [{', '.join(_ACTIONS)}]",
85
- ])
 
86
  result = agent.act(observation, list(_ACTIONS), brief)
87
  action = result.action
88
  reasoning = result.reasoning[:_REASONING_LIMIT]
 
79
  action = reference_actions(persona, scenario, game)[0]
80
  reasoning = ""
81
  else:
82
+ obs_parts: list[str] = []
83
+ if turns: # auto-regressive: the model's own moves this practice run
84
+ obs_parts.append(
85
+ "Your moves so far this run (most recent last): "
86
+ + ", ".join(t.action for t in turns)
87
+ )
88
+ obs_parts += [
89
  "Now:", frame, legend_text(scenario.legend()),
90
  f"Available actions: [{', '.join(_ACTIONS)}]",
91
+ ]
92
+ observation = "\n".join(obs_parts)
93
  result = agent.act(observation, list(_ACTIONS), brief)
94
  action = result.action
95
  reasoning = result.reasoning[:_REASONING_LIMIT]
proteus/runtime/session.py CHANGED
@@ -74,6 +74,7 @@ class SessionRunner:
74
  for turn_idx in range(1, self._play_turns + 1):
75
  observation = core.build_observation(
76
  scenario, game, built.cut_frames, turn_idx, memory=memory,
 
77
  )
78
 
79
  probe_fields: dict[str, object] = {}
 
74
  for turn_idx in range(1, self._play_turns + 1):
75
  observation = core.build_observation(
76
  scenario, game, built.cut_frames, turn_idx, memory=memory,
77
+ prior_actions=[t.action for t in turns],
78
  )
79
 
80
  probe_fields: dict[str, object] = {}
proteus/runtime/spectate.py CHANGED
@@ -106,6 +106,7 @@ class SpectateSession:
106
  observation = core.build_observation(
107
  self._scenario, self._game, self._cut_frames, turn_idx,
108
  memory=self._memory,
 
109
  )
110
  result = self._agent.act(observation, list(core._ACTIONS), self._system_prompt)
111
  self._turns.append(core.make_turn_trace(
 
106
  observation = core.build_observation(
107
  self._scenario, self._game, self._cut_frames, turn_idx,
108
  memory=self._memory,
109
+ prior_actions=[t.action for t in self._turns],
110
  )
111
  result = self._agent.act(observation, list(core._ACTIONS), self._system_prompt)
112
  self._turns.append(core.make_turn_trace(
tests/runtime/test_auto_regressive.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The agent plays auto-regressively: each turn's observation carries the model's
2
+ OWN prior moves this run AND the handover memory every turn (not just turn 1,
3
+ not just the current grid) — so a stateless agent can maintain its trajectory."""
4
+ from __future__ import annotations
5
+
6
+ import proteus.grid # noqa: F401
7
+ from proteus.agents.human import HumanAgent
8
+ from proteus.grid.difficulty import Difficulty
9
+ from proteus.runtime.session import SessionRunner
10
+
11
+
12
+ def _human(actions):
13
+ feed = iter(actions)
14
+ return HumanAgent(input_fn=lambda _p: next(feed), output_fn=lambda _t: None)
15
+
16
+
17
+ def _move_log(observation: str) -> str:
18
+ """The 'your moves so far' segment of an observation (empty if absent)."""
19
+ if "Your moves so far" not in observation:
20
+ return ""
21
+ return observation.split("Your moves so far", 1)[1].split("Now:", 1)[0]
22
+
23
+
24
+ def test_memory_shown_every_turn_and_prior_actions_are_auto_regressive():
25
+ actions = ["up", "left", "down", "right"]
26
+ runner = SessionRunner(
27
+ "pack_evade", _human(actions), difficulty=Difficulty.EASY, seed=42,
28
+ play_turns=len(actions), use_probe=False,
29
+ )
30
+ trace = runner.run()
31
+ # pack_evade attaches a persona memory by default -> it is present on EVERY
32
+ # turn now (previously only turn 1).
33
+ assert all("MEMORY" in t.observation for t in trace.turns)
34
+ # Turn 1: nothing chosen yet.
35
+ assert _move_log(trace.turns[0].observation) == ""
36
+ # Turn 2: the turn-1 move is fed back.
37
+ assert "up" in _move_log(trace.turns[1].observation)
38
+ # Turn 3: the running line of play accumulates (auto-regressive).
39
+ log3 = _move_log(trace.turns[2].observation)
40
+ assert "up" in log3 and "left" in log3
41
+ # Turn 4: all three prior moves.
42
+ log4 = _move_log(trace.turns[3].observation)
43
+ assert log4.count(",") == 2 # three actions -> two separators
tests/runtime/test_session_memory.py CHANGED
@@ -70,8 +70,13 @@ def test_memory_injection_lengthens_turn1_and_preserves_measurement():
70
  assert base.memory_ref is None
71
 
72
 
73
- def test_memory_does_not_change_turn2_observation():
74
  base = _runner().run()
75
  withmem = _runner(memory=_memory(), memory_ref="x").run()
76
- # turn 2+ never carry the memory block (only turn 1 does)
77
- assert base.turns[1].observation == withmem.turns[1].observation
 
 
 
 
 
 
70
  assert base.memory_ref is None
71
 
72
 
73
+ def test_memory_is_shown_every_turn_for_auto_regressive_play():
74
  base = _runner().run()
75
  withmem = _runner(memory=_memory(), memory_ref="x").run()
76
+ # Auto-regressive play: the handover memory is now carried on turn 2+ as well
77
+ # (not only turn 1), so a stateless agent never loses it mid-episode. Without
78
+ # memory the block is absent; with memory it is present and the observations
79
+ # differ.
80
+ assert "MEMORY" not in base.turns[1].observation
81
+ assert "MEMORY" in withmem.turns[1].observation
82
+ assert base.turns[1].observation != withmem.turns[1].observation