File size: 3,338 Bytes
5424fe6
 
 
 
 
 
 
 
f6566bb
5424fe6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""The modularity invariant — the load-bearing proof of the whole architecture.

A brand-new agent and a brand-new scenario are introduced as YAML files ONLY,
loaded by the registry, and run through the conductor.  No engine file is touched.
The new agent even mints its own namespaced event kind, and it still renders on
stage via the generic projection fallback.  If this passes, "config over code" is
real, not aspirational.
"""

from __future__ import annotations

from collections import Counter

import yaml

from src.core.conductor import Conductor
from src.core.registry import Registry


def _write_world(root):
    (root / "agents").mkdir()
    (root / "scenarios").mkdir()
    (root / "models.yaml").write_text(yaml.safe_dump({"offline": True}))

    # A wholly new agent that emits a wholly new, namespaced kind.
    (root / "agents" / "town-crier.yaml").write_text(
        yaml.safe_dump(
            {
                "name": "town-crier",
                "role": "worker",
                "persona": "You are the Town Crier. Announce one bit of news in a sentence.",
                "subscribes_to": [],
                "may_emit": ["crier.announced"],
                "schedule": {"tick_every": 1},
                "model_profile": "tiny",
                "memory": {"window": 5},
                "tools": [],
            }
        )
    )

    (root / "scenarios" / "town-square.yaml").write_text(
        yaml.safe_dump(
            {
                "name": "town-square",
                "title": "Town Square",
                "goal": "Keep the square informed.",
                "default_seed": "Market day in a town that forgets its own name nightly.",
                "cast": ["town-crier"],
                "genesis_text": "The square fills around '{seed}'.",
            }
        )
    )


class TestModularityInvariant:
    def test_new_agent_and_scenario_run_with_zero_engine_edits(self, tmp_path):
        _write_world(tmp_path)

        registry = Registry.from_dir(tmp_path)
        assert "town-crier" in registry.agents
        assert "town-square" in registry.scenarios

        scenario = registry.build_scenario("town-square")
        conductor = Conductor(scenario, governor=registry.governor_for("town-square"))
        conductor.reset(scenario.default_seed)
        for _ in range(3):
            conductor.step()

        kinds = Counter(e.kind for e in conductor.ledger.events)
        # The brand-new namespaced kind was emitted by the brand-new agent.
        assert kinds["crier.announced"] >= 1

    def test_custom_kind_renders_on_stage(self, tmp_path):
        _write_world(tmp_path)
        registry = Registry.from_dir(tmp_path)
        conductor = Conductor(registry.build_scenario("town-square"))
        conductor.reset("Market day.")
        conductor.step()
        # Generic projection fallback surfaces any text-bearing custom kind.
        notes = " ".join(conductor.projection.agent_notes)
        assert "crier.announced" in notes or "town-crier" in notes

    def test_goal_is_threaded_from_config(self, tmp_path):
        _write_world(tmp_path)
        registry = Registry.from_dir(tmp_path)
        conductor = Conductor(registry.build_scenario("town-square"))
        conductor.reset("Market day.")
        assert conductor.projection.goal == "Keep the square informed."