Spaces:
Runtime error
Runtime error
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
|
| 124 |
|
| 125 |
-
|
| 126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
"""
|
| 128 |
legend = scenario.legend()
|
| 129 |
parts: list[str] = []
|
| 130 |
-
if
|
| 131 |
parts.append(render_memory_block(memory))
|
| 132 |
parts.append("NOW — this run so far:")
|
| 133 |
-
if
|
| 134 |
for i, frame in enumerate(cut_frames[:-1], start=1):
|
| 135 |
parts.append(f"Cut {i}:")
|
| 136 |
parts.append(frame)
|
| 137 |
-
|
| 138 |
-
parts.append(
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 74 |
base = _runner().run()
|
| 75 |
withmem = _runner(memory=_memory(), memory_ref="x").run()
|
| 76 |
-
#
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|