Spaces:
Running on Zero
Running on Zero
File size: 10,761 Bytes
0d7db8e 5424fe6 f6566bb ba6dd5f 0d7db8e ba6dd5f 32a601f a196e34 32a601f f6566bb 32a601f 0d7db8e 32a601f f6566bb 32a601f 0d7db8e f6566bb 32a601f 0d7db8e f6566bb 0d7db8e f6566bb 0d7db8e ba6dd5f 32a601f f6566bb 27b304e ba6dd5f 32a601f 5424fe6 f6566bb a196e34 32a601f 5424fe6 32a601f f6566bb 32a601f a196e34 f6566bb a196e34 f6566bb a196e34 ba6dd5f f6566bb 27b304e f6566bb 27b304e f6566bb 27b304e ba6dd5f f6566bb ba6dd5f f6566bb ba6dd5f f6566bb ba6dd5f 0b0d9be ba6dd5f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | """Context builder β assembles a compact, role-scoped prompt per agent turn.
Layering order (innermost β outermost, smallest β largest prompt budget):
1. IDENTITY β pinned persona (permanent cost, never drops)
2. SHARED GOAL β the scenario objective (only when set; from the projection)
3. CURRENT SCENE β world state from the stage projection
4. THE DISCUSSION β role-aware (ADR-0023 + follow-up):
Β· workers see WHAT'S BEEN SAID β the recent table to react to;
Β· judges see THE EXCHANGE TO JUDGE β the *complete* ordered
transcript, so a ruling weighs the whole discussion, not its tail.
5. YOUR MEMORY β episodic/salience recall (the earlier arc + world beats/verdicts;
deduped against block 4 so no line is printed twice)
6. VISITOR β recent user injections (always salient)
[7. EXTRA] β injected by ManifestAgent subclass for scenario-specific context
[8. OUTPUT FORMAT] β JSON constraint block (appended by structured.py)
The builder owns the structure. Agents own only the persona and the action.
Changing the prompt strategy for all agents is a one-file edit here.
"""
from __future__ import annotations
from src import observability as obs
from src.core.events import Event
from src.core.memory import EpisodicMemory
from src.core.projections import StageProjection
# Public speech β the lines that constitute "the discussion" everyone can hear. A private
# ``agent.thought`` is deliberately excluded: it rides only its own event (the mind-reader),
# so a judge rules on what was *said*, and peers never read another mind.
_PUBLIC_SPEECH_KINDS = frozenset({"agent.spoke", "oracle.spoke"})
class ContextBuilder:
"""Assembles a compact, role-scoped prompt for a single agent turn."""
# How many recent peer lines a *worker's* "react now" blackboard shows. The longer arc
# of the discussion is carried by YOUR MEMORY (recall now includes peers' spoken lines β
# ADR-0023 follow-up), so this stays small: it's the immediate table to react to.
_BLACKBOARD_WINDOW = 8
# How many lines a *judge's* full transcript shows (most recent, if it overflows). Judges
# fire infrequently and must weigh the whole exchange, so this is generous β it comfortably
# covers every shipped scenario's discussion length.
_TRANSCRIPT_LIMIT = 80
def build(
self,
*,
agent_name: str,
persona: str,
projection: StageProjection,
all_events: tuple[Event, ...],
memory_window: int = 8,
memory_text: str | None = None,
role: str = "worker",
) -> str:
"""Build a prompt string from layered context.
Args:
agent_name: Used to filter visible events for memory.
persona: Fixed identity text (IDENTITY block).
projection: Current world-state view.
all_events: Full ledger tail (this run) β drives memory recall AND a judge's
full transcript.
memory_window: How many events to include (for EpisodicMemory).
memory_text: Pre-computed memory string (pass to override the default
EpisodicMemory computation, e.g. when using SalienceMemory).
role: The agent's role. ``"judge"`` gets the complete exchange to rule
on; everyone else gets the recent blackboard to react to.
"""
if memory_text is None:
memory_text = EpisodicMemory(agent_name, max_recent=memory_window).format_for_prompt(all_events)
# Block 4 is role-aware: a judge needs the WHOLE exchange to rule fairly; a worker
# needs the recent table to react to. Both return the set of discussion texts they
# already show, so YOUR MEMORY can drop those exact lines β the union an agent sees
# is unchanged, we just never print a line twice (blackboard/transcript hold the
# discussion, memory holds the earlier arc + world beats/verdicts).
discussion_block, shown_texts = self._discussion(role, projection, all_events)
shown_texts = set(shown_texts) | {(projection.current_scene or "").strip()}
memory_text = self._dedup_memory(memory_text, shown_texts)
visitor_lines = "\n".join(f"- {a}" for a in projection.user_artifacts[-3:]) or "(quiet)"
goal_block = f"SHARED GOAL\n{projection.goal}\n\n" if projection.goal else ""
# When dedup leaves nothing (common for a judge whose recall is fully covered by the
# transcript above), show a short pointer instead of a blank or a duplicate block.
memory_block = (
f"YOUR MEMORY (recent events you witnessed)\n{memory_text}"
if memory_text.strip()
else "YOUR MEMORY\n(nothing beyond the exchange above)"
)
prompt = (
f"IDENTITY\n{persona}\n\n"
f"{goal_block}"
f"CURRENT SCENE\n{projection.current_scene}\n\n"
f"{discussion_block}"
f"{memory_block}\n\n"
f"VISITOR DISTURBANCES\n{visitor_lines}"
)
# Structure + size of the assembled context (the full prompt is logged by the
# agent layer as ``agent.prompt``; here we record which sections were present).
discussion_section = "TRANSCRIPT" if role == "judge" else "BLACKBOARD"
sections = ["IDENTITY", "CURRENT SCENE", discussion_section, "MEMORY", "VISITOR"]
if goal_block:
sections.insert(1, "SHARED GOAL")
obs.log(
"context.build",
level="debug",
agent=agent_name,
role=role,
sections=sections,
prompt_chars=len(prompt),
memory_chars=len(memory_text),
)
return prompt
# ββ the discussion block (role-aware) βββββββββββββββββββββββββββββββββββββββββ
def _discussion(
self, role: str, projection: StageProjection, all_events: tuple[Event, ...]
) -> tuple[str, set[str]]:
"""The discussion block + the set of texts it shows (for memory dedup).
Judges get the full ordered transcript (rule on everything); everyone else gets the
recent blackboard (react to the table)."""
if role == "judge":
lines = self._public_lines(all_events)
return self._transcript_block(lines), {self._note_text(line) for line in lines}
shown_notes = [n for n in projection.agent_notes if n][-self._BLACKBOARD_WINDOW :]
return self._blackboard_block(shown_notes), {self._note_text(n) for n in shown_notes}
@staticmethod
def _public_lines(all_events: tuple[Event, ...]) -> list[str]:
"""Every public spoken line in the run, oldest β newest, as ``"actor: text"``.
This is the discussion a judge rules on. Private thoughts are excluded (see
``_PUBLIC_SPEECH_KINDS``); a line with no text is skipped."""
out: list[str] = []
for e in all_events:
if e.kind in _PUBLIC_SPEECH_KINDS:
text = str(e.payload.get("text", "")).strip()
if text:
out.append(f"{e.actor}: {text}")
return out
@classmethod
def _transcript_block(cls, lines: list[str]) -> str:
"""A judge's view: the complete exchange, in order, with a 'weigh all of it' nudge."""
if not lines:
return "THE EXCHANGE TO JUDGE\n(no one has spoken yet)\n\n"
shown = lines[-cls._TRANSCRIPT_LIMIT :]
head = (
f"(showing the last {cls._TRANSCRIPT_LIMIT} of {len(lines)} lines)\n"
if len(lines) > cls._TRANSCRIPT_LIMIT
else ""
)
body = "\n".join(f"- {line}" for line in shown)
return (
"THE EXCHANGE TO JUDGE (every spoken line, in order β weigh ALL of it, not just the last few):\n"
f"{head}{body}\n\n"
)
@staticmethod
def _note_text(note: str) -> str:
"""The spoken text of a discussion line, stripped of its ``actor:`` prefix.
Lines read ``"actor: text"``, ``"actor [kind]: text"``, or ``"π actor believes:
text"`` β all carry the content after the first ``": "``. Used to match the same
line where it appears in a memory entry (``[turn NNN][kind] text``)."""
_, _, text = note.partition(": ")
return (text or note).strip()
@staticmethod
def _dedup_memory(memory_text: str, shown_texts: set[str]) -> str:
"""Drop memory lines already shown in the discussion block or CURRENT SCENE.
Memory lines are ``[turn NNN][kind] <text>``. The most-recent ``world.observed`` is
also the CURRENT SCENE, and (now that peers' spoken lines are recallable) the
discussion lines also appear in block 4. We drop any memory line ending with one of
those already-shown texts β so the agent reads each line once: the blackboard/
transcript holds the discussion, memory holds the earlier arc + world/verdict beats.
Returns the empty string if every line was a duplicate β the caller then shows a
short pointer rather than a blank or a re-print of the discussion."""
shown = {s for s in shown_texts if s}
if not shown:
return memory_text
kept = [line for line in memory_text.splitlines() if not any(line.rstrip().endswith(s) for s in shown)]
return "\n".join(kept)
@staticmethod
def _blackboard_block(shown_notes: list[str]) -> str:
"""The shared blackboard: what the rest of the cast just said or did.
Without this an agent sees only the world text and its own past lines, so a
small model loops on the same clue and never reacts to anyone (the "shared
blackboard isn't shared" gap β ADR-0023). ``shown_notes`` is the already-sliced
recent tail and carries only the public ``text`` of each peer event (never their
private thought), so surfacing it shares the conversation without leaking minds.
"""
notes = [n for n in shown_notes if n]
if not notes:
return "WHAT'S BEEN SAID\n(you go first β set the tone)\n\n"
lines = "\n".join(f"- {n}" for n in notes)
return (
"WHAT'S BEEN SAID (the table so far β react to it)\n"
f"{lines}\n"
"Do NOT echo or rephrase any line above. Add a GENUINELY NEW angle β a different "
"sense, detail, or suspicion β that moves the conversation forward.\n\n"
)
|