Spaces:
Running on Zero
Running on Zero
| # Phase 2: Reflection Events + Structured Output | |
| > **Status: ✅ Realized.** Reflection, structured output, and manifest-driven | |
| > agents shipped. This page is kept as the original plan of record; see ADR-0009 | |
| > and the architecture docs for the as-built design. | |
| ## Goal | |
| Make agents coherent over long runs. After 30+ turns without reflection, small | |
| models forget their character and repeat themselves. This phase wires two | |
| mechanisms that solve it: **reflection** (memory compaction into beliefs) and | |
| **structured JSON output** (reliable, parseable, character-consistent responses). | |
| **Acceptance criteria**: | |
| - Agents emit `agent.reflected` events when their `reflection_threshold` is hit. | |
| - Every agent prompt includes a JSON constraint block and the parser handles all | |
| three compliance cases (clean JSON / embedded JSON / prose fallback). | |
| - The `_raw_fallback` rate tracked in run stats drops below 10% with a real model. | |
| - An agent running for 50 turns stays visibly in character (eval: human review of ledger). | |
| --- | |
| ## Implementation plan | |
| ### 2.1 Wire ReflectionTracker into ManifestAgent | |
| `ReflectionTracker` is already implemented in `src/core/memory.py`. | |
| `ManifestAgent` in `src/agents/base.py` needs to check it each turn: | |
| ```python | |
| # In ManifestAgent.act(): | |
| threshold = self.manifest.memory.reflection_threshold | |
| if threshold is not None: | |
| if not hasattr(self, "_reflection_tracker"): | |
| from src.core.memory import ReflectionTracker | |
| self._reflection_tracker = ReflectionTracker(self.manifest.name, threshold) | |
| if self._reflection_tracker.observe(recent_events): | |
| return self._emit_reflection(run_id, turn, projection, recent_events) | |
| ``` | |
| The reflection path calls the model with a special prompt asking for a one-sentence | |
| belief synthesis, emits `agent.reflected` with payload `{"belief": "...", "based_on": [...]}`. | |
| ### 2.2 Reflection prompt | |
| ```python | |
| def _emit_reflection(self, run_id, turn, projection, recent_events) -> Event: | |
| memory = EpisodicMemory(self.manifest.name, max_recent=20).format_for_prompt(recent_events) | |
| prompt = ( | |
| f"IDENTITY\n{self.manifest.persona}\n\n" | |
| f"RECENT MEMORY (last 20 events you witnessed)\n{memory}\n\n" | |
| "TASK\n" | |
| "Synthesise the above into ONE high-level belief about yourself or the world. " | |
| "This belief will replace the raw memories in your future context.\n" | |
| 'OUTPUT FORMAT\n{"kind": "agent.reflected", "text": "<one sentence belief>"}' | |
| ) | |
| raw = self.model.complete(self.manifest.name + "-reflect", prompt) | |
| parsed = parse_agent_output(raw, ["agent.reflected"], "agent.reflected") | |
| return Event(run_id=run_id, turn=turn, kind="agent.reflected", | |
| actor=self.manifest.name, payload=parsed) | |
| ``` | |
| ### 2.3 Wire JSON instruction into ManifestAgent | |
| `ManifestAgent.act()` already calls `json_instruction()` (Phase 2 infrastructure | |
| is in `src/core/structured.py`). The missing piece is passing `extra_fields` | |
| from the manifest to support per-scenario payload shape. | |
| Add to `AgentManifest`: | |
| ```python | |
| output_extra_fields: list[str] = [] | |
| # e.g. ["emotion"] → agents emit {"kind": "...", "text": "...", "emotion": "..."} | |
| ``` | |
| ### 2.4 Track _raw_fallback rate in run stats | |
| Update `render_stats()` to count `_raw_fallback=True` events: | |
| ```python | |
| fallback_count = sum(1 for e in events if e.payload.get("_raw_fallback")) | |
| lines.append(f" raw fallback rate: {fallback_count}/{len(events)}") | |
| ``` | |
| ### 2.5 Update Thousand Token Wood agents to use ManifestAgent | |
| Convert `SceneWhisperer`, `MischiefCritic`, `PocketActor`, `EchoAgent` from | |
| extending `Agent` (Phase 1) to extending `ManifestAgent` (Phase 2): | |
| ```python | |
| class SceneWhisperer(ManifestAgent): | |
| manifest = AgentManifest( | |
| name="scene-whisperer", | |
| role="worker", | |
| persona="...", | |
| subscribes_to=["run.started", "user.injected"], | |
| may_emit=["world.observed"], | |
| schedule=ScheduleConfig(tick_every=3), | |
| model_profile="fast", | |
| memory=MemoryConfig(window=6, reflection_threshold=20), | |
| ) | |
| ``` | |
| This migration also moves the scenario from legacy scheduling to manifest-based routing. | |
| --- | |
| ## New event kind: `agent.reflected` | |
| Add to `EventKind` in `src/core/events.py`: | |
| ```python | |
| EventKind = Literal[ | |
| "run.started", | |
| "world.observed", | |
| "agent.thought", | |
| "agent.spoke", | |
| "agent.reflected", # ← new | |
| "judge.verdict", | |
| "user.injected", | |
| ] | |
| ``` | |
| Update `StageProjection.apply()` to render reflections: | |
| ```python | |
| elif event.kind == "agent.reflected": | |
| self.agent_notes.append(f"💭 {event.actor} believes: {event.payload.get('text', '')}") | |
| ``` | |
| --- | |
| ## Testing plan | |
| | Test | File | What it verifies | | |
| |---|---|---| | |
| | `test_reflection_tracker_triggers` | `test_salience_memory.py` | Already passing | | |
| | `test_manifest_agent_emits_reflection` | `test_manifest.py` | ManifestAgent emits reflected event at threshold | | |
| | `test_reflected_event_globally_visible` | `test_memory.py` | EpisodicMemory includes agent.reflected for all agents | | |
| | `test_fallback_rate_tracked` | `test_conductor.py` | run stats include fallback count | | |
| | `test_structured_output_end_to_end` | new `test_integration.py` | Full step with real-model stub returns parseable JSON | | |
| --- | |
| ## Files to change | |
| | File | Change | | |
| |---|---| | |
| | `src/core/events.py` | Add `agent.reflected` to EventKind | | |
| | `src/core/projections.py` | Render `agent.reflected` events | | |
| | `src/agents/base.py` | Wire ReflectionTracker into ManifestAgent.act() | | |
| | `src/core/manifest.py` | Add `output_extra_fields` field | | |
| | `src/agents/tiny_wood.py` | Convert agents to ManifestAgent | | |
| | `src/ui/render.py` | Track and display _raw_fallback rate | | |
| | `tests/test_manifest.py` | Tests for ManifestAgent reflection | | |
| --- | |
| ## Estimated effort: 1–2 days | |