agharsallah commited on
Commit
2a8eab2
Β·
1 Parent(s): 7cedfb2

feat: update model endpoint URLs and enhance documentation with mermaid diagrams

Browse files
config/models.yaml CHANGED
@@ -16,8 +16,10 @@
16
  # The LiteLLM model string for a custom OpenAI-compatible endpoint is
17
  # `openai/<served_model_id>` (the HF repo id) with `base_url` pointing at the
18
  # endpoint's /v1 URL. Modal serves each endpoint at
19
- # https://<workspace>--<endpoint_name>.modal.run/v1
20
- # so only the workspace is deploy-specific: it is templated from $MODAL_WORKSPACE
 
 
21
  # and never hard-coded. $MODAL_LLM_KEY is the endpoint key (vLLM accepts any
22
  # token, default "EMPTY"). Unset templates expand to "" β†’ the offline stub is
23
  # used and these live bindings are ignored.
@@ -29,25 +31,25 @@ offline: null
29
  profiles:
30
  tiny:
31
  model: openai/nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16
32
- base_url: https://${MODAL_WORKSPACE}--nemotron-3-nano-4b.modal.run/v1
33
  api_key: ${MODAL_LLM_KEY}
34
  temperature: 0.7
35
  max_tokens: 160
36
  fast:
37
  model: openai/openbmb/MiniCPM4.1-8B
38
- base_url: https://${MODAL_WORKSPACE}--minicpm-4-1-8b.modal.run/v1
39
  api_key: ${MODAL_LLM_KEY}
40
  temperature: 0.9
41
  max_tokens: 220
42
  balanced:
43
  model: openai/google/gemma-4-12B
44
- base_url: https://${MODAL_WORKSPACE}--gemma-4-12b.modal.run/v1
45
  api_key: ${MODAL_LLM_KEY}
46
  temperature: 0.8
47
  max_tokens: 320
48
  strong:
49
  model: openai/google/gemma-4-26B-A4B-it
50
- base_url: https://${MODAL_WORKSPACE}--gemma-4-26b.modal.run/v1
51
  api_key: ${MODAL_LLM_KEY}
52
  temperature: 0.6
53
  max_tokens: 480
 
16
  # The LiteLLM model string for a custom OpenAI-compatible endpoint is
17
  # `openai/<served_model_id>` (the HF repo id) with `base_url` pointing at the
18
  # endpoint's /v1 URL. Modal serves each endpoint at
19
+ # https://<workspace>--<app_name>-<endpoint_name>.modal.run/v1
20
+ # where <app_name> is the modal.App name (nvidia-llms / openbmb-llms / google-llms)
21
+ # and <endpoint_name> is the per-model function slug. So only the workspace is
22
+ # deploy-specific: it is templated from $MODAL_WORKSPACE
23
  # and never hard-coded. $MODAL_LLM_KEY is the endpoint key (vLLM accepts any
24
  # token, default "EMPTY"). Unset templates expand to "" β†’ the offline stub is
25
  # used and these live bindings are ignored.
 
31
  profiles:
32
  tiny:
33
  model: openai/nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16
34
+ base_url: https://${MODAL_WORKSPACE}--nvidia-llms-nemotron-3-nano-4b.modal.run/v1
35
  api_key: ${MODAL_LLM_KEY}
36
  temperature: 0.7
37
  max_tokens: 160
38
  fast:
39
  model: openai/openbmb/MiniCPM4.1-8B
40
+ base_url: https://${MODAL_WORKSPACE}--openbmb-llms-minicpm-4-1-8b.modal.run/v1
41
  api_key: ${MODAL_LLM_KEY}
42
  temperature: 0.9
43
  max_tokens: 220
44
  balanced:
45
  model: openai/google/gemma-4-12B
46
+ base_url: https://${MODAL_WORKSPACE}--google-llms-gemma-4-12b.modal.run/v1
47
  api_key: ${MODAL_LLM_KEY}
48
  temperature: 0.8
49
  max_tokens: 320
50
  strong:
51
  model: openai/google/gemma-4-26B-A4B-it
52
+ base_url: https://${MODAL_WORKSPACE}--google-llms-gemma-4-26b.modal.run/v1
53
  api_key: ${MODAL_LLM_KEY}
54
  temperature: 0.6
55
  max_tokens: 480
docs/adr/0015-litellm-gateway-modal-models.md CHANGED
@@ -51,9 +51,10 @@ self-served vLLM endpoint accepts any token, so the key defaults to the
51
  conventional `"EMPTY"` when unset.
52
 
53
  **Workspace is not hard-coded.** Modal serves each endpoint at a distinct
54
- subdomain `https://<workspace>--<endpoint>.modal.run/v1`, so a single base URL
55
- cannot address all four. `config/models.yaml` templates only the deploy-specific
56
- workspace: `base_url: https://${MODAL_WORKSPACE}--<endpoint>.modal.run/v1` and
 
57
  `api_key: ${MODAL_LLM_KEY}`. `Registry.from_dir()` expands these on load
58
  (`_expand_env`); if any referenced var is unset the whole string collapses to `""`
59
  (an incomplete binding is *not configured* rather than a broken half-URL) and a
 
51
  conventional `"EMPTY"` when unset.
52
 
53
  **Workspace is not hard-coded.** Modal serves each endpoint at a distinct
54
+ subdomain `https://<workspace>--<app>-<endpoint>.modal.run/v1` (app = nvidia-llms /
55
+ openbmb-llms / google-llms), so a single base URL cannot address all four.
56
+ `config/models.yaml` templates only the deploy-specific workspace:
57
+ `base_url: https://${MODAL_WORKSPACE}--<app>-<endpoint>.modal.run/v1` and
58
  `api_key: ${MODAL_LLM_KEY}`. `Registry.from_dir()` expands these on load
59
  (`_expand_env`); if any referenced var is unset the whole string collapses to `""`
60
  (an incomplete binding is *not configured* rather than a broken half-URL) and a
docs/architecture/config-system.md CHANGED
@@ -31,6 +31,16 @@ Drop a file in, and it exists. No engine edit, no import to add.
31
 
32
  `src/core/registry.py` loads the directory and assembles live objects:
33
 
 
 
 
 
 
 
 
 
 
 
34
  ```python
35
  reg = default_registry() # loads config/
36
  scenario = reg.build_scenario("mystery-roots") # cast names -> live agents
 
31
 
32
  `src/core/registry.py` loads the directory and assembles live objects:
33
 
34
+ ```mermaid
35
+ flowchart LR
36
+ Files["config/<br/>agents/*.yaml Β· scenarios/*.yaml Β· models.yaml"] --> Load["Registry.from_dir()"]
37
+ Load --> Val["validate_agent / validate_scenario<br/>Pydantic Β· extra=forbid"]
38
+ Val --> Reg["Registry<br/>agents Β· scenarios Β· models"]
39
+ Reg --> BS["build_scenario(name)"]
40
+ BS --> Cast["resolve cast β†’ live ManifestAgents<br/>+ ModelRouter + ToolRegistry"]
41
+ Cast --> Scn["Scenario (runnable)"]
42
+ ```
43
+
44
  ```python
45
  reg = default_registry() # loads config/
46
  scenario = reg.build_scenario("mystery-roots") # cast names -> live agents
docs/architecture/long-running.md CHANGED
@@ -19,6 +19,18 @@ time. This is what lets the same scenario "run in realtime" for a demo and
19
 
20
  ## The ledger is the checkpoint
21
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  Because all state derives from the append-only ledger, crash recovery is nearly
23
  free:
24
 
 
19
 
20
  ## The ledger is the checkpoint
21
 
22
+ ```mermaid
23
+ flowchart LR
24
+ Run["step(n_ticks=N)"] -->|append| L[("Ledger")]
25
+ L --> Snap{"turn % snapshot_every == 0?"}
26
+ Snap -->|yes| Backup["snapshot_to(path)"] --> Run
27
+ Snap -->|no| Run
28
+ Run -.->|crash| Crash([process dies])
29
+ Crash --> From["SQLiteLedger.from_file(db)"]
30
+ From --> Restore["conductor.restore()<br/>adopt run_id + last turn"]
31
+ Restore --> Run
32
+ ```
33
+
34
  Because all state derives from the append-only ledger, crash recovery is nearly
35
  free:
36
 
docs/architecture/memory-stack.md CHANGED
@@ -15,6 +15,20 @@ This solves four problems at once:
15
 
16
  ## Three Layers
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  ### Layer 1: EpisodicMemory (always on)
19
 
20
  The simplest layer. An agent sees:
 
15
 
16
  ## Three Layers
17
 
18
+ ```mermaid
19
+ flowchart TD
20
+ L[("Append-only Ledger")] --> V["Visibility filter<br/>own events βˆͺ globally-visible kinds"]
21
+ V --> E["Layer 1 Β· EpisodicMemory<br/>recent window (always on)"]
22
+ V --> S["Layer 2 Β· SalienceMemory<br/>top-k: relevance Γ— recency Γ— importance"]
23
+ Idx["MemoryIndex Β· optional Β· ADR-0018<br/>semantic relevance"] -.->|upgrades relevance term| S
24
+ V --> Rf["Layer 3 Β· ReflectionMemory<br/>emits agent.reflected every N events"]
25
+ Rf -->|"agent.reflected (globally visible)"| L
26
+ E --> CB["ContextBuilder β†’ prompt"]
27
+ S --> CB
28
+ ```
29
+
30
+ All three layers are *views over the one ledger* β€” none holds separate state.
31
+
32
  ### Layer 1: EpisodicMemory (always on)
33
 
34
  The simplest layer. An agent sees:
docs/architecture/model-routing.md CHANGED
@@ -72,7 +72,7 @@ offline: null # null=auto, true=stub everywhere, false=always live
72
  profiles:
73
  tiny:
74
  model: openai/nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16
75
- base_url: https://${MODAL_WORKSPACE}--nemotron-3-nano-4b.modal.run/v1
76
  api_key: ${MODAL_LLM_KEY}
77
  temperature: 0.7
78
  max_tokens: 160
 
72
  profiles:
73
  tiny:
74
  model: openai/nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16
75
+ base_url: https://${MODAL_WORKSPACE}--nvidia-llms-nemotron-3-nano-4b.modal.run/v1
76
  api_key: ${MODAL_LLM_KEY}
77
  temperature: 0.7
78
  max_tokens: 160
docs/architecture/next-steps/architecture-review-and-next-steps.md CHANGED
@@ -71,6 +71,18 @@ Trace **Mystery Roots** (the "convergent blackboard swarm"):
71
  | `devils-advocate` | `agent.thought` (subscribes to `agent.spoke`) | ❌ β€” and it can't even see the hypothesis it's meant to rebut |
72
  | `mystery-judge` | `judge.verdict` | β€” but it sees **none** of the clues or hypotheses |
73
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  The judge reaches a verdict having seen only the genesis `world.observed` (the seed),
75
  its own prior verdicts, and visitor injections. The clue-gathering and
76
  hypothesis-forming machinery produces events **no one downstream reads.** The
 
71
  | `devils-advocate` | `agent.thought` (subscribes to `agent.spoke`) | ❌ β€” and it can't even see the hypothesis it's meant to rebut |
72
  | `mystery-judge` | `judge.verdict` | β€” but it sees **none** of the clues or hypotheses |
73
 
74
+ ```mermaid
75
+ flowchart LR
76
+ CG["clue-gatherer<br/>emits agent.thought"]
77
+ HF["hypothesis-former<br/>emits agent.spoke"]
78
+ DA["devils-advocate"]
79
+ J["mystery-judge<br/>emits judge.verdict"]
80
+ HF -->|"triggers (subscribes_to agent.spoke)"| DA
81
+ HF -.->|"βœ— but DA cannot read it"| DA
82
+ CG -.->|"βœ— invisible"| J
83
+ HF -.->|"βœ— invisible"| J
84
+ ```
85
+
86
  The judge reaches a verdict having seen only the genesis `world.observed` (the seed),
87
  its own prior verdicts, and visitor injections. The clue-gathering and
88
  hypothesis-forming machinery produces events **no one downstream reads.** The
docs/architecture/next-steps/phase-6-illustrated-serial.md CHANGED
@@ -52,22 +52,18 @@ EventKind = Literal[
52
 
53
  ## The episode loop
54
 
55
- ```
56
- episode.started
57
- β”‚
58
- β”œβ”€β”€β†’ Beat Proposer β†’ beat.proposed
59
- β”‚ β”‚
60
- β”‚ β”œβ”€β”€β†’ Dialogue Writer β†’ agent.spoke
61
- β”‚ β”œβ”€β”€β†’ Scene Describer β†’ world.observed
62
- β”‚ β”‚ β”‚
63
- β”‚ β”‚ └──→ Artist β†’ image.generated (MCP call)
64
- β”‚ β”‚
65
- β”‚ └──→ Continuity Keeper β†’ agent.thought (checks for errors)
66
- β”‚
67
- └──→ Serial Judge reads all of the above
68
- β”‚
69
- β”œβ”€β”€ judge.approved β†’ Episode Publisher writes artifact
70
- └── judge.rejected β†’ Beat Proposer tries again (max 3 attempts)
71
  ```
72
 
73
  The judge holds the **publish gate**: no episode is published unless the judge approves.
 
52
 
53
  ## The episode loop
54
 
55
+ ```mermaid
56
+ flowchart TD
57
+ Start(["episode.started"]) --> Beat["Beat Proposer<br/>beat.proposed"]
58
+ Beat --> Dia["Dialogue Writer<br/>agent.spoke"]
59
+ Beat --> Scene["Scene Describer<br/>world.observed"]
60
+ Scene --> Art["Artist<br/>image.generated Β· MCP call"]
61
+ Beat --> Cont["Continuity Keeper<br/>agent.thought Β· checks errors"]
62
+ Dia --> Judge{"Serial Judge<br/>reads all of the above"}
63
+ Art --> Judge
64
+ Cont --> Judge
65
+ Judge -->|approved| Pub["Episode Publisher<br/>writes artifact"]
66
+ Judge -->|"rejected Β· retry ≀ 3"| Beat
 
 
 
 
67
  ```
68
 
69
  The judge holds the **publish gate**: no episode is published unless the judge approves.
docs/architecture/observer-pattern.md CHANGED
@@ -4,15 +4,17 @@
4
 
5
  The observer is the camera crew. It has exactly one rule: **never append events**.
6
 
 
 
 
 
 
 
 
7
  ```
8
- COGNITIVE LOOP (conductor + agents) RENDERING (observer)
9
- ↓ appends ↓ reads
10
- [ Ledger ] ────────────────────────→ Observer.consume()
11
- ↓
12
- ViewDiff
13
- ↓
14
- UI callback / SSE / WebSocket
15
- ```
16
 
17
  The cognitive loop runs identically whether or not any observer is attached.
18
  The world is not "observed into existence" β€” it runs, and the observer watches.
 
4
 
5
  The observer is the camera crew. It has exactly one rule: **never append events**.
6
 
7
+ ```mermaid
8
+ flowchart LR
9
+ Conductor --> Agents
10
+ Agents -->|append| Ledger[("Ledger")]
11
+ Ledger -->|read only| Obs["Observer.consume()"]
12
+ Obs --> Diff["ViewDiff β€” the delta"]
13
+ Diff --> Out["UI callback Β· SSE Β· WebSocket"]
14
  ```
15
+
16
+ The cognitive loop (left) writes; the observer (right) only reads. The world runs
17
+ identically whether or not any observer is attached.
 
 
 
 
 
18
 
19
  The cognitive loop runs identically whether or not any observer is attached.
20
  The world is not "observed into existence" β€” it runs, and the observer watches.
docs/architecture/overview.md CHANGED
@@ -6,19 +6,21 @@ One event-sourced engine; every world is a *configuration* of it. Agents on the
6
  left produce into a shared ledger; oversight and rendering on the right consume
7
  from it. The two sides never call each other.
8
 
9
- ```text
10
- config/ (YAML) ─────────────────────────────┐
11
- agents Β· scenarios Β· models β”‚ Registry
12
- β–Ό
13
- Visitor ─► Gradio UI ─► Conductor ─► ManifestAgent ─► ModelRouter ─► small model
14
- β”‚ β–² β”‚ └─► ToolRegistry (capability-checked)
15
- Governorβ”‚ β”‚ β–Ό
16
- (calls/tokens) β”‚ Event Ledger (append-only, idempotent)
17
- β”‚ β”‚
18
- β”‚ Projections ──► Observer ──► Gradio UI
19
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
20
  ```
21
 
 
 
 
22
  The ledger is the only source of truth. Agent memory, world state, UI state, and
23
  bloggable traces are all projections derived from events.
24
 
 
6
  left produce into a shared ledger; oversight and rendering on the right consume
7
  from it. The two sides never call each other.
8
 
9
+ ```mermaid
10
+ flowchart LR
11
+ Config["config/ YAML<br/>agents Β· scenarios Β· models"] --> Registry --> Conductor
12
+ Visitor --> UI["Gradio UI"] --> Conductor
13
+ Governor["Governor<br/>calls Β· tokens Β· spend"] -.->|checks| Conductor
14
+ Conductor --> Agent["ManifestAgent"]
15
+ Agent --> Router["ModelRouter β†’ small model"]
16
+ Agent --> Tools["ToolRegistry<br/>capability-checked"]
17
+ Agent -->|append| Ledger[("Event Ledger<br/>append-only Β· idempotent")]
18
+ Ledger -->|read| Projections --> Observer --> UI
 
19
  ```
20
 
21
+ *Producers (left of the ledger) only ever **append**; consumers (right) only ever
22
+ **read**. The two sides communicate through the ledger and never call each other.*
23
+
24
  The ledger is the only source of truth. Agent memory, world state, UI state, and
25
  bloggable traces are all projections derived from events.
26
 
docs/architecture/scenario-authoring.md CHANGED
@@ -73,6 +73,16 @@ a tool grant + handler, salience memory, and the visibility gotcha.
73
  Three custom kinds (`rumor.whispered`, `fact.checked`, `news.published`) β€” none of
74
  which the engine has ever heard of.
75
 
 
 
 
 
 
 
 
 
 
 
76
  ### Step 2 β€” Write the agent manifests
77
 
78
  `config/agents/rumor-starter.yaml`:
 
73
  Three custom kinds (`rumor.whispered`, `fact.checked`, `news.published`) β€” none of
74
  which the engine has ever heard of.
75
 
76
+ ```mermaid
77
+ flowchart TD
78
+ Seed(["genesis: world.observed (the seed)"]) --> RS["rumor-starter Β· tick 1<br/>rumor.whispered"]
79
+ RS --> G["gossip<br/>subscribes rumor.whispered<br/>emits rumor.whispered"]
80
+ RS --> FC["fact-checker<br/>subscribes rumor.whispered<br/>oracle tool β†’ fact.checked"]
81
+ G -.->|begets more gossip| G
82
+ G --> GZ["gazette Β· tick 5<br/>news.published"]
83
+ FC --> GZ
84
+ ```
85
+
86
  ### Step 2 β€” Write the agent manifests
87
 
88
  `config/agents/rumor-starter.yaml`:
docs/architecture/structured-output.md CHANGED
@@ -25,6 +25,24 @@ exposes `complete_structured`, it uses the validated path; otherwise it falls
25
  back to the prompt-and-parse path. Both feed the same `Event` construction, so
26
  the conductor, ledger, and projections are identical either way.
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  ---
29
 
30
  ## The constraint block
 
25
  back to the prompt-and-parse path. Both feed the same `Event` construction, so
26
  the conductor, ledger, and projections are identical either way.
27
 
28
+ ```mermaid
29
+ flowchart TD
30
+ A["act() β†’ _resolve_payload()"] --> Q{"provider has<br/>complete_structured?"}
31
+ Q -->|"yes Β· live"| Live["Instructor + LiteLLM<br/>kind-constrained model"]
32
+ Live --> LV{"validates?"}
33
+ LV -->|yes| Out["typed Event payload"]
34
+ LV -->|"no Β· retry ≀ max_retries"| Live
35
+ LV -->|"error / gives up"| Parse
36
+ Q -->|"no Β· offline stub"| Inst["append OUTPUT FORMAT β†’ complete()"]
37
+ Inst --> Parse["parse_agent_output()"]
38
+ Parse --> T1{"Tier 1: strict JSON?"}
39
+ T1 -->|yes| Out
40
+ T1 -->|no| T2{"Tier 2: extract JSON block?"}
41
+ T2 -->|yes| Out
42
+ T2 -->|no| T3["Tier 3: wrap raw text<br/>_raw_fallback = true"]
43
+ T3 --> Out
44
+ ```
45
+
46
  ---
47
 
48
  ## The constraint block
docs/architecture/subscription-routing.md CHANGED
@@ -17,6 +17,16 @@ conductor routes accordingly. Coupling is to the **event schema**, not to each
17
 
18
  The conductor runs **two tracks per step** in order:
19
 
 
 
 
 
 
 
 
 
 
 
20
  ### Track 1: Event-triggered (subscriptions)
21
 
22
  When any event is appended to the ledger, the conductor checks which agents
 
17
 
18
  The conductor runs **two tracks per step** in order:
19
 
20
+ ```mermaid
21
+ flowchart TD
22
+ S(["step() β†’ _tick(): turn += 1"]) --> Gov["governor.check()"]
23
+ Gov --> P1["Track 1 β€” drain trigger queue FIFO<br/>(agents whose subscribes_to matched)"]
24
+ P1 --> P2["Track 2 β€” run tick agents<br/>(schedule.tick_every fires)"]
25
+ P2 --> N["appended events β†’ notify_subscribers<br/>queue matching agents"]
26
+ N -.->|feeds next step| P1
27
+ P2 --> Snap["maybe snapshot"]
28
+ ```
29
+
30
  ### Track 1: Event-triggered (subscriptions)
31
 
32
  When any event is appended to the ledger, the conductor checks which agents
docs/architecture/tool-contract.md CHANGED
@@ -48,10 +48,20 @@ tool running out-of-process over the Model Context Protocol (MCP) β€” same contr
48
  swappable transport, invisible to agents. The capability check is the security
49
  boundary; MCP is only transport.
50
 
 
 
 
 
 
 
 
 
 
 
51
  ```
52
- Agent ──call_tool──► ToolRegistry ──(grant check FIRST)──► in-process fn (default)
53
- └──► MCP client ─► stdio server
54
- ```
55
 
56
  `ToolRegistry.call(...)` enforces `tool in manifest.tools` and raises
57
  `CapabilityViolation` on a denied call **before any transport is touched** β€” then
 
48
  swappable transport, invisible to agents. The capability check is the security
49
  boundary; MCP is only transport.
50
 
51
+ ```mermaid
52
+ flowchart TD
53
+ A["agent.call_tool(name, params)"] --> B["ToolRegistry.call()"]
54
+ B --> C{"name in manifest.tools?"}
55
+ C -->|no| X["raise CapabilityViolation"]
56
+ C -->|yes| D{"registered in-process?"}
57
+ D -->|yes| E["run() β†’ dict (default)"]
58
+ D -->|no| F{"resolver has it?"}
59
+ F -->|yes| G["MCP client β†’ stdio server β†’ dict"]
60
+ F -->|no| Y["raise KeyError"]
61
  ```
62
+
63
+ The capability check (node C) runs **first** β€” before any transport is touched β€”
64
+ so swapping in-process for MCP never weakens the security boundary.
65
 
66
  `ToolRegistry.call(...)` enforces `tool in manifest.tools` and raises
67
  `CapabilityViolation` on a denied call **before any transport is touched** β€” then
docs/architecture/turn-lifecycle.md CHANGED
@@ -3,6 +3,31 @@
3
  One sim-tick, end to end. `Conductor.step(n_ticks=1)` runs this loop; the model
4
  is stateless and all state is passed in per turn.
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  ```text
7
  0. step(n_ticks=N): repeat the tick body N times (two-clock: wall-clock maps to N).
8
  1. _tick(): turn += 1; governor.begin_turn(); governor.check() (turn/call/token caps).
 
3
  One sim-tick, end to end. `Conductor.step(n_ticks=1)` runs this loop; the model
4
  is stateless and all state is passed in per turn.
5
 
6
+ ```mermaid
7
+ sequenceDiagram
8
+ participant C as Conductor
9
+ participant G as Governor
10
+ participant A as Agent
11
+ participant R as ModelRouter
12
+ participant L as Ledger
13
+ participant O as Observer
14
+ C->>G: begin_turn / check caps
15
+ Note over C,A: Phase 1 subscription agents, then Phase 2 tick agents
16
+ loop each acting agent
17
+ C->>G: check()
18
+ C->>A: act(projection, recent_events)
19
+ A->>A: ContextBuilder assembles prompt
20
+ A->>R: for_profile(model_profile)
21
+ R-->>A: small model -> typed event
22
+ A-->>C: Event (kind in may_emit)
23
+ C->>G: record_call(tokens, cost)
24
+ C->>L: append(event)
25
+ C->>O: consume(event) -> ViewDiff
26
+ Note over C: notify_subscribers queues agents for NEXT tick
27
+ end
28
+ C->>L: maybe snapshot every N
29
+ ```
30
+
31
  ```text
32
  0. step(n_ticks=N): repeat the tick body N times (two-clock: wall-clock maps to N).
33
  1. _tick(): turn += 1; governor.begin_turn(); governor.check() (turn/call/token caps).
docs/blog/engine-architecture.md CHANGED
@@ -68,6 +68,15 @@ The window is capped at 8 events to stay within small-model context budgets.
68
  This is event sourcing plus CQRS in its simplest form: one write side (the ledger),
69
  many read sides (each agent's memory projection, the UI's stage view, the stats panel).
70
 
 
 
 
 
 
 
 
 
 
71
  ---
72
 
73
  ## The Context Builder: Prompt Assembly as a Separate Concern
 
68
  This is event sourcing plus CQRS in its simplest form: one write side (the ledger),
69
  many read sides (each agent's memory projection, the UI's stage view, the stats panel).
70
 
71
+ ```mermaid
72
+ flowchart LR
73
+ Agents --> L[("Append-only Ledger<br/>one write side")]
74
+ L --> M1["each agent's memory"]
75
+ L --> M2["stage view Β· UI"]
76
+ L --> M3["stats panel"]
77
+ L --> M4["judge analysis"]
78
+ ```
79
+
80
  ---
81
 
82
  ## The Context Builder: Prompt Assembly as a Separate Concern