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 +8 -6
- docs/adr/0015-litellm-gateway-modal-models.md +4 -3
- docs/architecture/config-system.md +10 -0
- docs/architecture/long-running.md +12 -0
- docs/architecture/memory-stack.md +14 -0
- docs/architecture/model-routing.md +1 -1
- docs/architecture/next-steps/architecture-review-and-next-steps.md +12 -0
- docs/architecture/next-steps/phase-6-illustrated-serial.md +12 -16
- docs/architecture/observer-pattern.md +10 -8
- docs/architecture/overview.md +13 -11
- docs/architecture/scenario-authoring.md +10 -0
- docs/architecture/structured-output.md +18 -0
- docs/architecture/subscription-routing.md +10 -0
- docs/architecture/tool-contract.md +13 -3
- docs/architecture/turn-lifecycle.md +25 -0
- docs/blog/engine-architecture.md +9 -0
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 |
-
#
|
|
|
|
|
|
|
| 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`
|
| 55 |
-
|
| 56 |
-
|
|
|
|
| 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 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 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 |
-
|
| 9 |
-
|
| 10 |
-
|
| 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 |
-
```
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 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 |
-
|
| 53 |
-
|
| 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
|