Spaces:
Running on Zero
A newer version of the Gradio SDK is available: 6.19.0
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.reflectedevents when theirreflection_thresholdis 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_fallbackrate 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:
# 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
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:
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:
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):
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:
EventKind = Literal[
"run.started",
"world.observed",
"agent.thought",
"agent.spoke",
"agent.reflected", # ← new
"judge.verdict",
"user.injected",
]
Update StageProjection.apply() to render reflections:
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 |