File size: 5,777 Bytes
ba6dd5f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0b0d9be
 
ba6dd5f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f6566bb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""ContextBuilder — the shared blackboard reaches the prompt (ADR-0023).

Before this, an agent saw only the world text and its own past lines, so small
models looped on one clue and never reacted to anyone. The builder now surfaces
``projection.agent_notes`` (peers' public lines) so the table is actually shared.
"""

from __future__ import annotations

from src.core.context import ContextBuilder
from src.core.projections import StageProjection


def test_blackboard_surfaces_peer_lines():
    proj = StageProjection(goal="g", current_scene="scene")
    proj.agent_notes = ["spy-cara: a morning fuel", "spy-nil: warm and comforting"]
    prompt = ContextBuilder().build(agent_name="spy-bex", persona="(bex)", projection=proj, all_events=())
    assert "WHAT'S BEEN SAID" in prompt
    assert "a morning fuel" in prompt
    assert "warm and comforting" in prompt
    # nudges toward a fresh contribution, not an echo
    assert "new angle" in prompt.lower() and "echo" in prompt.lower()


def test_blackboard_prompts_the_first_speaker_to_open():
    proj = StageProjection(goal="g", current_scene="scene")  # no notes yet
    prompt = ContextBuilder().build(agent_name="spy-cara", persona="(cara)", projection=proj, all_events=())
    assert "WHAT'S BEEN SAID" in prompt
    assert "you go first" in prompt.lower()


def test_persona_and_goal_still_lead():
    proj = StageProjection(goal="catch the spy", current_scene="scene")
    prompt = ContextBuilder().build(agent_name="a", persona="I am A.", projection=proj, all_events=())
    # IDENTITY and SHARED GOAL must still come before the blackboard.
    assert prompt.index("I am A.") < prompt.index("catch the spy") < prompt.index("WHAT'S BEEN SAID")


def _spoke(actor, text, turn=1, kind="agent.spoke"):
    from src.core.events import Event

    return Event(run_id="r", turn=turn, kind=kind, actor=actor, payload={"text": text})


def test_judge_gets_the_full_ordered_transcript_not_just_the_tail():
    # A judge rules on the WHOLE discussion — every spoken line, in order — not the
    # recency-biased blackboard tail a worker reacts to (ADR-0023 follow-up).
    events = tuple(_spoke("debater-a" if i % 2 == 0 else "debater-b", f"point number {i}", turn=i) for i in range(12))
    proj = StageProjection(goal="g", current_scene="scene")
    prompt = ContextBuilder().build(
        agent_name="debate-judge", persona="(judge)", projection=proj, all_events=events, role="judge"
    )
    assert "THE EXCHANGE TO JUDGE" in prompt and "WHAT'S BEEN SAID" not in prompt
    # every line is present, oldest first
    assert "point number 0" in prompt and "point number 11" in prompt
    assert prompt.index("point number 0") < prompt.index("point number 11")


def test_judge_transcript_excludes_private_thoughts():
    # Thoughts are the mind-reader's alone; a judge rules on what was SAID.
    events = (_spoke("a", "said aloud"), _spoke("b", "secret scheming", kind="agent.thought"))
    prompt = ContextBuilder().build(
        agent_name="j",
        persona="(judge)",
        projection=StageProjection(current_scene="s"),
        all_events=events,
        role="judge",
    )
    assert "said aloud" in prompt
    assert "secret scheming" not in prompt


def test_worker_gets_blackboard_not_transcript():
    proj = StageProjection(current_scene="scene")
    proj.agent_notes = ["a: hello there"]
    prompt = ContextBuilder().build(
        agent_name="w", persona="(w)", projection=proj, all_events=(_spoke("a", "hello there"),), role="worker"
    )
    assert "WHAT'S BEEN SAID" in prompt and "THE EXCHANGE TO JUDGE" not in prompt


def test_memory_does_not_repeat_a_line_already_in_the_discussion():
    # A spoken line shown in the blackboard must not be printed again in YOUR MEMORY:
    # the union is unchanged, we just don't duplicate (blackboard=recent, memory=earlier).
    proj = StageProjection(current_scene="scene")
    proj.agent_notes = ["a: the bench needs shade"]
    memory_text = "[turn 003][agent.spoke] the bench needs shade\n[turn 001][world.observed] an older beat"
    prompt = ContextBuilder().build(
        agent_name="w", persona="(w)", projection=proj, all_events=(), memory_text=memory_text, role="worker"
    )
    # the duplicated line appears once (in the blackboard), the unique earlier beat survives in memory
    assert prompt.count("the bench needs shade") == 1
    assert "an older beat" in prompt


def test_memory_shows_a_pointer_when_fully_covered_by_the_discussion():
    proj = StageProjection(current_scene="scene")
    proj.agent_notes = ["a: only line"]
    prompt = ContextBuilder().build(
        agent_name="w",
        persona="(w)",
        projection=proj,
        all_events=(),
        memory_text="[turn 003][agent.spoke] only line",
        role="worker",
    )
    assert "nothing beyond the exchange above" in prompt


def test_a_peer_thought_never_reaches_another_agent():
    # A spoken event carries a private `thought` (the mind-reader content). It must
    # ride only on its own payload — peers see the public `text`, never the thought.
    from src.core.events import Event

    proj = StageProjection(goal="g", current_scene="scene")
    proj.apply(
        Event(
            run_id="r",
            turn=1,
            kind="agent.spoke",
            actor="spy-nil",
            payload={"text": "warm and comforting", "thought": "I'm the spy — keep it vague!"},
        )
    )
    assert any("warm and comforting" in n for n in proj.agent_notes)
    assert not any("spy" in n.lower() and "vague" in n.lower() for n in proj.agent_notes)
    prompt = ContextBuilder().build(agent_name="spy-bex", persona="(bex)", projection=proj, all_events=())
    assert "warm and comforting" in prompt
    assert "keep it vague" not in prompt