irregular6612 commited on
Commit
dcb83a6
·
1 Parent(s): f0cff65

feat(cp8): persona-driven memory generation (hidden weights, public id)

Browse files
proteus/runtime/memory.py CHANGED
@@ -48,6 +48,10 @@ class MemoryCheckpoint(BaseModel):
48
  memory_turns: The episode's per-turn records, in play order.
49
  outcome: ``"survived"`` or ``"eliminated"``.
50
  transparent_prompt: The full-info brief used to drive the episode.
 
 
 
 
51
  """
52
 
53
  model: str
@@ -59,6 +63,7 @@ class MemoryCheckpoint(BaseModel):
59
  memory_turns: list[MemoryTurn] = Field(default_factory=list)
60
  outcome: str
61
  transparent_prompt: str
 
62
 
63
 
64
  def _safe(name: str) -> str:
 
48
  memory_turns: The episode's per-turn records, in play order.
49
  outcome: ``"survived"`` or ``"eliminated"``.
50
  transparent_prompt: The full-info brief used to drive the episode.
51
+ persona_weight_id: Public id of the hidden persona that drove this
52
+ demonstration (CP8), or ``None`` for a model self-play memory. Only
53
+ the id is stored — the raw reward weights are never serialized into
54
+ the participant-visible checkpoint.
55
  """
56
 
57
  model: str
 
63
  memory_turns: list[MemoryTurn] = Field(default_factory=list)
64
  outcome: str
65
  transparent_prompt: str
66
+ persona_weight_id: str | None = None
67
 
68
 
69
  def _safe(name: str) -> str:
proteus/runtime/memory_gen.py CHANGED
@@ -18,6 +18,7 @@ from proteus.grid.difficulty import Difficulty
18
  from proteus.grid.game import MotiveGridGame
19
  from proteus.grid.scenario import get_scenario
20
  from proteus.runtime.memory import MemoryCheckpoint, MemoryTurn
 
21
 
22
  _ACTIONS = ["up", "down", "left", "right", "stay"]
23
  _REASONING_LIMIT = 2000 # cap stored reasoning so checkpoints stay small
@@ -37,18 +38,28 @@ def generate_memory(
37
  memory_turns: int,
38
  model_name: str,
39
  clock: Callable[[], str] = _utc_now_stamp,
 
40
  ) -> MemoryCheckpoint:
41
  """Run a full-info self-play episode and return it as a MemoryCheckpoint.
42
 
43
  Args:
44
  scenario_name: Registered scenario key.
45
- agent: The playing agent (its provider drives the actions).
 
46
  difficulty: Difficulty band of the memory world (= the scored game).
47
  seed: Seed of the memory world (= the scored game).
48
  memory_turns: Max self-play turns; the episode also stops on terminal.
49
  model_name: Identifier stored on the checkpoint.
50
  clock: Returns the ``created_at`` stamp (injected for deterministic tests).
 
 
 
 
 
 
51
  """
 
 
52
  scenario = get_scenario(scenario_name)()
53
  rng = random.Random(seed)
54
  game = MotiveGridGame(scenario, rng, difficulty, max_steps=memory_turns)
@@ -62,23 +73,31 @@ def generate_memory(
62
  predator_pos = (predator.x, predator.y) if predator else (-1, -1)
63
 
64
  frame = frame_to_ascii(game.current_grid(), scenario.legend())
65
- observation = "\n".join([
66
- "Now:", frame, legend_text(scenario.legend()),
67
- f"Available actions: [{', '.join(_ACTIONS)}]",
68
- ])
69
- result = agent.act(observation, list(_ACTIONS), brief)
 
 
 
 
 
 
 
 
70
 
71
  turns.append(MemoryTurn(
72
  turn_idx=turn_idx,
73
  frame_ascii=frame,
74
- action=result.action,
75
- reasoning=result.reasoning[:_REASONING_LIMIT],
76
  focal_pos=focal_pos,
77
  predator_pos=predator_pos,
78
  ))
79
 
80
- game.apply_motive_action(result.action)
81
- scenario.record_focal_move(result.action)
82
  if game.eliminated or game.survived:
83
  break
84
 
@@ -93,4 +112,5 @@ def generate_memory(
93
  memory_turns=turns,
94
  outcome=outcome,
95
  transparent_prompt=brief,
 
96
  )
 
18
  from proteus.grid.game import MotiveGridGame
19
  from proteus.grid.scenario import get_scenario
20
  from proteus.runtime.memory import MemoryCheckpoint, MemoryTurn
21
+ from proteus.runtime.persona import PersonaWeights, reference_actions
22
 
23
  _ACTIONS = ["up", "down", "left", "right", "stay"]
24
  _REASONING_LIMIT = 2000 # cap stored reasoning so checkpoints stay small
 
38
  memory_turns: int,
39
  model_name: str,
40
  clock: Callable[[], str] = _utc_now_stamp,
41
+ persona: PersonaWeights | None = None,
42
  ) -> MemoryCheckpoint:
43
  """Run a full-info self-play episode and return it as a MemoryCheckpoint.
44
 
45
  Args:
46
  scenario_name: Registered scenario key.
47
+ agent: The playing agent (its provider drives the actions). May be
48
+ ``None`` when *persona* is set (the reference policy plays instead).
49
  difficulty: Difficulty band of the memory world (= the scored game).
50
  seed: Seed of the memory world (= the scored game).
51
  memory_turns: Max self-play turns; the episode also stops on terminal.
52
  model_name: Identifier stored on the checkpoint.
53
  clock: Returns the ``created_at`` stamp (injected for deterministic tests).
54
+ persona: When set, the memory is a *persona demonstration* — each action
55
+ is the deterministic reference action (first tie-break of
56
+ ``reference_actions``) for the hidden weights, not a model self-play.
57
+ The public ``persona_weight_id`` is recorded; the raw weights are
58
+ never serialized, and the transparent brief stays weight-agnostic so
59
+ the eval model must infer the persona from behaviour alone (spec §3).
60
  """
61
+ if persona is None and agent is None:
62
+ raise ValueError("generate_memory needs an agent unless a persona is given")
63
  scenario = get_scenario(scenario_name)()
64
  rng = random.Random(seed)
65
  game = MotiveGridGame(scenario, rng, difficulty, max_steps=memory_turns)
 
73
  predator_pos = (predator.x, predator.y) if predator else (-1, -1)
74
 
75
  frame = frame_to_ascii(game.current_grid(), scenario.legend())
76
+
77
+ if persona is not None:
78
+ # Deterministic persona demonstration: the reference policy plays.
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]
89
 
90
  turns.append(MemoryTurn(
91
  turn_idx=turn_idx,
92
  frame_ascii=frame,
93
+ action=action,
94
+ reasoning=reasoning,
95
  focal_pos=focal_pos,
96
  predator_pos=predator_pos,
97
  ))
98
 
99
+ game.apply_motive_action(action)
100
+ scenario.record_focal_move(action)
101
  if game.eliminated or game.survived:
102
  break
103
 
 
112
  memory_turns=turns,
113
  outcome=outcome,
114
  transparent_prompt=brief,
115
+ persona_weight_id=persona.persona_weight_id if persona else None,
116
  )
tests/runtime/test_memory_persona.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from proteus.grid.difficulty import Difficulty
2
+ from proteus.runtime.memory_gen import generate_memory
3
+ from proteus.runtime.persona import PersonaWeights
4
+
5
+
6
+ def test_persona_memory_is_deterministic_and_tags_persona():
7
+ w = PersonaWeights(persona_weight_id="risk_averse", risk_cost=5.0)
8
+ ck = generate_memory(
9
+ "predator_evade", agent=None, difficulty=Difficulty.EASY, seed=42,
10
+ memory_turns=5, model_name="ref", clock=lambda: "FIXED", persona=w,
11
+ )
12
+ assert ck.persona_weight_id == "risk_averse"
13
+ assert 1 <= len(ck.memory_turns) <= 5
14
+ # risk-averse demo never walks into the dead-end wall on the diagnostic turn
15
+ assert ck.memory_turns[0].action in ("up", "down", "right", "stay")
16
+ # weights are NOT in the participant-visible checkpoint text
17
+ assert "risk_cost" not in ck.model_dump_json()