AgentnessBench / proteus /game /runtime /memory_gen.py
irregular6612's picture
refactor: restructure proteus into game/web subpackages
426093b
Raw
History Blame Contribute Delete
5.42 kB
"""Memory-generation stage: a full-information self-play episode.
Mirrors rollout.py's engine-replay pattern. The agent is given the scenario's
transparent brief and plays freely (no scripted Cut, no answer-key leakage) for
up to memory_turns, stopping on terminal. The recorded trajectory becomes a
MemoryCheckpoint shown at the scored game's handover (see CP7 design §5).
"""
from __future__ import annotations
import random
from datetime import datetime, timezone
from typing import Callable
from proteus.game.agents.base import Agent
from proteus.game.engine.ascii_view import legend_text
from proteus.game.engine.difficulty import Difficulty
from proteus.game.engine.grid import MotiveGridGame
from proteus.game.scenarios.base import get_scenario
from proteus.game.runtime.memory import MemoryCheckpoint, MemoryTurn
from proteus.game.metrics.persona import PersonaWeights, reference_actions
_ACTIONS = ["up", "down", "left", "right", "stay"]
_REASONING_LIMIT = 2000 # cap stored reasoning so checkpoints stay small
def _utc_now_stamp() -> str:
"""Filesystem-safe UTC stamp, e.g. '2026-06-02T10-40-56Z'."""
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H-%M-%SZ")
def generate_memory(
scenario_name: str,
agent: Agent,
*,
difficulty: Difficulty,
seed: int | None,
memory_turns: int,
model_name: str,
clock: Callable[[], str] = _utc_now_stamp,
persona: PersonaWeights | None = None,
policy: Callable[[object, object], str] | None = None,
) -> MemoryCheckpoint:
"""Run a full-info self-play episode and return it as a MemoryCheckpoint.
Args:
scenario_name: Registered scenario key.
agent: The playing agent (its provider drives the actions). May be
``None`` when *persona* is set (the reference policy plays instead).
difficulty: Difficulty band of the memory world (= the scored game).
seed: Seed of the memory world (= the scored game).
memory_turns: Max self-play turns; the episode also stops on terminal.
model_name: Identifier stored on the checkpoint.
clock: Returns the ``created_at`` stamp (injected for deterministic tests).
persona: When set, the memory is a *persona demonstration* — each action
is the deterministic reference action (first tie-break of
``reference_actions``) for the hidden weights, not a model self-play.
The public ``persona_weight_id`` is recorded; the raw weights are
never serialized, and the transparent brief stays weight-agnostic so
the eval model must infer the persona from behaviour alone (spec §3).
"""
if persona is None and policy is None and agent is None:
raise ValueError("generate_memory needs an agent unless a persona or policy is given")
scenario = get_scenario(scenario_name)()
rng = random.Random(seed)
game = MotiveGridGame(scenario, rng, difficulty, max_steps=memory_turns)
brief = scenario.memory_brief or scenario.rules_text
turns: list[MemoryTurn] = []
for turn_idx in range(1, memory_turns + 1):
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)
frame = scenario.render_frame(game) # scenario-chosen view (compact on open fields)
if policy is not None:
# Deterministic scripted policy demonstration (no model, no weights).
action = policy(scenario, game)
reasoning = ""
elif persona is not None:
# Deterministic persona demonstration: the reference policy plays.
action = reference_actions(persona, scenario, game)[0]
reasoning = ""
else:
obs_parts: list[str] = []
if turns: # auto-regressive: the model's own moves this practice run
obs_parts.append(
"Your moves so far this run (most recent last): "
+ ", ".join(t.action for t in turns)
)
obs_parts += [
"Now:", frame, legend_text(scenario.legend()),
f"Available actions: [{', '.join(_ACTIONS)}]",
]
observation = "\n".join(obs_parts)
result = agent.act(observation, list(_ACTIONS), brief)
action = result.action
reasoning = result.reasoning[:_REASONING_LIMIT]
turns.append(MemoryTurn(
turn_idx=turn_idx,
frame_ascii=frame,
action=action,
reasoning=reasoning,
focal_pos=focal_pos,
predator_pos=predator_pos,
))
game.apply_motive_action(action)
scenario.record_focal_move(action)
if game.eliminated or game.survived:
break
outcome = "eliminated" if game.eliminated else "survived"
return MemoryCheckpoint(
model=model_name,
scenario=scenario_name,
motive_category="survival",
difficulty=difficulty.value,
seed=seed,
created_at=clock(),
memory_turns=turns,
outcome=outcome,
transparent_prompt=brief,
persona_weight_id=persona.persona_weight_id if persona else None,
wall_rects=list(scenario.wall_rects()),
food_cells=list(scenario.food_cells()),
)