multi-agent-lab / docs /architecture /next-steps /phase-2-reflection-structured.md
agharsallah
Refactor code structure for improved readability and maintainability
5424fe6
|
Raw
History Blame Contribute Delete
5.79 kB

A newer version of the Gradio SDK is available: 6.19.0

Upgrade

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:

# 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

Estimated effort: 1–2 days