cas-studio
by radsilent
README.md
# CAS Studio
A studio for designing and simulating **complex adaptive systems** (CAS) as
agent-based models. You define agent types (state + local rules), wire
agents into a directed, signed interaction graph, give the system an open
environment with sources and sinks, and run a deterministic, seeded,
synchronous simulation engine against it. The seven canonical CAS
properties are first-class, instrumented features — each has explicit
model constructs **and** analysis endpoints, not just documentation.
Pure Python + numpy core. FastAPI + SQLAlchemy + Alembic for the REST API
and persistence. Vanilla-JS canvas UI. No heavy deps, no network calls, no
external data.
## Quickstart
```bash
./run.sh # venv + deps (via uv), alembic upgrade, uvicorn
# HOST=127.0.0.1 PORT=8002 ./run.sh to override
```
Then open http://localhost:8000/ — on first start against an empty
database the **seed demo** is loaded (and audit-logged): *innovation
diffusion in a market* — 40 agents on a small-world ring lattice with
innovators, early-majority and laggard adopters, two skeptics (balancing
loops), two adaptive-price vendors, and an `information` source in the
environment. Two adjacent seeded adopters trigger an S-curve adoption
cascade (mean adoption 0.12 → 0.95 over ~11 steps) with flagged emergence
events at the takeoff; dropping to a single seed makes it fizzle.
Run the tests:
```bash
.venv/bin/python -m pytest tests/ -q
```
## The seven CAS properties
### 1. Emergence
Macro patterns arise from micro interactions. Every step the engine
records macro metrics: the **mean field** of every state var, population
variance, **active-cluster count** (connected components of "active"
agents — primary var ≥ 0.5 — over the undirected interaction graph), an
**order parameter** `|2·active_fraction − 1|`, and a Moran's-I-like
neighbor correlation. `GET /api/runs/{id}/emergence` returns the series
plus flagged **emergence events**: steps where the z-score of a macro
indicator's step-to-step delta exceeds 3 while exogenous inputs were
constant (no injections). *Honest caveat:* a z-score over a short series
is a blunt detector — treat events as flags for inspection, not proof.
### 2. Nonlinearity
`POST /api/systems/{id}/sensitivity` with
`{"param", "deltas": [...], "steps", "seed"}` reruns the simulation with
`param` perturbed by each delta and reports response ratios
`|Δoutcome/Δparam|` (outcome = final mean field of the primary var). It
flags `nonlinear` (ratios vary by >10× across magnitudes — superlinear
regime), `threshold` (some perturbations produce a response, others none),
and `sign_flip`. Parameter addressing:
| param form | meaning |
|---|---|
| `env:<var>` | environment initial value |
| `flow:<var>:inflow` / `flow:<var>:outflow_rate` | source/sink field |
| `type:<type>:<var>` | initial state var of every agent of a type |
| `seed_count:<type>:<var>` | how many agents of a type start with `<var>=1` (a contiguous block anchored at the current first active agent) |
The seed demo has a genuine threshold at
`seed_count:innovator:adopted`: one seed fizzles (adoption 0.075), two
adjacent seeds cascade (0.95).
### 3. Decentralization
Agents act **only** on local information: their own state, weighted means
of their direct in-neighbors' state, and environment variables via flux.
The rule DSL's condition vocabulary is closed — global state is
*unrepresentable by construction* (there is no "all agents" aggregate, no
global lookup). Self-organization is tracked by the Moran's-I-like
neighbor-state correlation in every run's series.
### 4. Feedback loops
`GET /api/systems/{id}/loops` enumerates elementary cycles of the directed
interaction graph (Johnson-style DFS, each cycle reported once from its
smallest node; `max_len` and a 1000-cycle cap bound the enumeration and
report `truncated`). Each loop is classified **reinforcing** (even count
of negative couplings) or **balancing** (odd count) with loop gain =
product of edge weights. Run summaries include per-loop **activity**: the
edge flux that flowed through each loop's edges during the run, where edge
flux per step = `|Δ primary var of the source| × |weight|` (a heuristic
for "how much change propagated along this edge", not a physical
quantity).
### 5. Adaptation
Agent types may declare
`"adaptation": {"target_var", "target_value", "rate"}`. Each step the
agent adds a bounded **bias** to `target_var`; after the step the bias is
updated by gradient-free reinforcement: if the step moved the var closer
to `target_value`, keep and amplify the bias (×1.25, capped at |1.0|),
else reverse and damp it (×−0.5). Deterministic given the seed. The seed
demo's vendors hill-climb `price` toward 0.7 this way.
### 6. Open boundaries
Every system has an **environment**: named float variables plus
sources/sinks — `{"var": "information", "inflow": 1.0, "outflow_rate":
0.05}` applies `v += inflow − outflow_rate·v` per step. Agents exchange
with the environment through conserving flux effects:
`{"flux": "information", "by": 0.02}` moves 0.02 units from the
environment into the agent (a state var of the same name); the environment
loses exactly what agents gain (verified by test). `POST
/api/runs/{id}/inject` with `{"step", "var", "amount"}` schedules an
exogenous pulse: it re-runs the base run's system/seed/steps with the
pulse added to the environment at that step, and returns the new run plus
the outcome delta.
### 7. Nested hierarchy
A `System` may have a `parent_id`; subsystems are full systems with their
own agents, rules and runs. `GET /api/systems/{id}/rollup` aggregates each
child system's latest-run macro metrics into the parent's report
(agent-count-weighted mean of the children's primary mean fields).
## Rule DSL
An agent type has `state` (dict of float vars) and `rules` — a list of
`{"if": <condition>, "then": [<effect>, ...]}`. The **first** matching
rule fires; later rules are ignored that step (a rule with `"then": []`
is an absorbing-state guard).
Conditions:
```jsonc
"always"
{"var": "adopted", "op": ">=", "value": 0.5} // own state
{"neighbor_mean": {"var": "adopted"}, "op": ">=", "value": 0.35} // in-neighbors
```
`op` ∈ `> < >= <= ==`. `neighbor_mean` is the weighted mean
`Σ wᵢxᵢ / Σ|wᵢ|` over in-neighbors that carry the var — so negative-weight
(skeptic) neighbors dilute the mean.
Effects:
```jsonc
{"set": "adopted", "value": 1.0} // set own var
{"adjust": "energy", "by": -0.1} // add to own var
{"flux": "information", "by": 0.02} // conserve quantity with the environment
```
Individual agents may carry a `state_override`: floats, or
`{"uniform": [lo, hi]}` which is sampled once per run from the run's
seeded RNG (`np.random.default_rng(seed)`) — this is what makes the run
seed meaningful. Same model + same seed → identical metric series
(tested).
The engine steps **synchronously**: every agent computes its next state
from the same snapshot — no update-order artifacts (tested with an
oscillating two-agent model).
## REST API
All mutations are audit-logged (`GET /api/audit`).
| Endpoint | Purpose |
|---|---|
| `GET/POST /api/systems`, `GET/DELETE /api/systems/{id}` | system CRUD (`parent_id` for hierarchy) |
| `POST /api/systems/{id}/agent-types`, `DELETE /api/agent-types/{id}` | agent types (DSL validated on write) |
| `POST /api/systems/{id}/agents`, `DELETE /api/agents/{id}` | agents |
| `POST /api/systems/{id}/interactions`, `DELETE /api/interactions/{id}` | directed signed edges |
| `GET/PUT /api/systems/{id}/environment` | environment variables + flows |
| `PATCH /api/agents/{id}/position` `{pos_x, pos_y}` | persist an agent's canvas position (saved by the UI on drag-end) |
| `POST /api/systems/{id}/reset_positions` | clear all saved canvas positions in a system (UI "Reset layout") |
| `PUT /api/agents/{id}` `{name?, state_override?}` | edit agent (validated, audit-logged) |
| `PUT /api/interactions/{id}` `{weight}` | edit edge weight (audit-logged) |
| `PUT /api/agent-types/{id}` | edit type state/rules/adaptation (DSL-validated) |
| `GET /api/examples`, `POST /api/examples/{name}` | curated example systems, one-click copies |
| `POST /api/agent/chat`, `POST /api/agent/generate` → `GET /api/agent/jobs/{id}` | LLM jobs: submit (202) then poll for progress/result |
| `POST /api/systems/{id}/runs` `{steps, seed}` | run the sim → `{run_id}` |
| `GET /api/runs/{id}` | summary (final mean field, loop activity, final states) |
| `GET /api/runs/{id}/series` | per-step macro metrics |
| `GET /api/runs/{id}/emergence` | emergence events |
| `POST /api/runs/{id}/inject` `{step, var, amount}` | exogenous pulse → new run |
| `GET /api/systems/{id}/loops` | feedback loops, classified |
| `POST /api/systems/{id}/sensitivity` | perturbation response analysis |
| `GET /api/systems/{id}/rollup` | aggregate children's latest runs |
## MCP integration
CAS Studio ships an **MCP (Model Context Protocol) stdio server** so AI
hosts (Cursor, Claude Desktop, …) can design and simulate systems directly.
It fronts the REST API over HTTP — point it at any running instance with
`CAS_API_URL` (default `http://127.0.0.1:8000`).
Register it in your host's MCP config (see `mcp-config.example.json`):
```json
{
"mcpServers": {
"cas-studio": {
"command": "/path/to/cas-studio/.venv/bin/python",
"args": ["-m", "app.mcp_server"],
"cwd": "/path/to/cas-studio",
"env": {"CAS_API_URL": "http://127.0.0.1:8000"}
}
}
}
```
Tools exposed (each proxies the matching REST endpoint above):
`list_systems`, `get_system`, `list_agent_types`, `create_agent_type`,
`create_agents` (batch), `add_interaction`, `set_environment_flow`,
`run_simulation`, `get_run_series`, `get_emergence_events`,
`list_feedback_loops`, `run_sensitivity`, `inject_pulse`, `get_rollup`,
and `describe_cas_properties` (the seven-property guide + rule DSL).
## AI agent
The UI's **AI Agent** tab is a built-in chat agent that drives the studio
through a local [Ollama](https://ollama.com) LLM — no API keys, nothing
leaves the machine.
Requirements: Ollama running (`ollama serve`) and at least one model,
e.g.:
```bash
ollama pull qwen3:8b # the default; supports tool calling and thinking
```
The model dropdown lists whatever Ollama reports installed and defaults to
`qwen3:8b`, then any `gemma3*`, then `llama3.1:8b`, then the first model
(`GET /api/agent/models`). Point the agent at another Ollama host with
`OLLAMA_URL` (default `http://127.0.0.1:11434`).
`POST /api/agent/chat` and `POST /api/agent/generate` run as **background
jobs** (HTTP 202 `{job_id}`) so slow local LLMs never hang a request:
poll `GET /api/agent/jobs/{id}` for
`{status: pending|running|done|error, progress: [...], result|error}` —
progress entries appear as the loop runs ("round 2: calling
run_simulation…", "attempt 2: validation failed: …") and every failure
ends in a specific human-readable error. LLM calls use Ollama's native
`/api/chat` with `think: false` (qwen3's thinking mode is ~10× slower on
CPU-bound Ollama and adds nothing to tool calls) and the default round
cap is 5.
`POST /api/agent/chat` `{messages, model?, system_id?, temperature?,
max_rounds?}` runs a tool-calling loop against Ollama's
`/api/chat`: the model picks a tool, the server executes it
(through the **same tool registry the MCP server uses**, `app/tools.py`),
appends the result, and repeats — up to `max_rounds` — until the model
writes a final answer. The job result is `{reply, model, tool_trace,
rounds}`; `tool_trace` lists every call with its arguments and a trimmed
result, and the UI renders it as a collapsible block under each reply.
Chain-of-thought (`<think>` blocks from qwen3) is stripped from replies.
If the selected model doesn't support tool calling, the agent falls back
to a single-shot no-tools answer with a note; if Ollama is down, the
models endpoint degrades gracefully and jobs end with a clear error.
`POST /api/agent/generate` `{description, name?, model?, system_id?,
temperature?, max_rounds?}` turns plain English into a **running CAS
model**. The LLM (Ollama JSON mode) drafts a system spec — agent types
with rule-DSL behavior, agent counts, an interaction topology
(`ring_lattice` / `random` / `small_world`), signed edge weights by source
type, environment variables with sources/sinks, and optional nested child
systems — guided by a compact DSL reference, a few-shot example, and
systems-engineering design rules (typology decomposition, at least one
reinforcing and one balancing loop, open-boundary flows for conserved
quantities, nesting for systems-of-systems). The server validates the spec
hard (schema, the full rule-DSL validator, topology materialization); on
failure the validation errors are fed back to the model, up to
`max_rounds` attempts (default 3), after which the job ends in error with
the collected messages. On success everything is created through the
repository (audit-logged as `generate_system` by `ai-agent`) and the
result carries the new system id, a summary, and the raw spec. The same
capability is exposed to MCP hosts as the `generate_system_from_description`
tool (it submits the job and waits), and in the UI as the **Generate
system** mode of the AI Agent tab (with live progress).
### LLM settings
The gear button in the AI Agent tab opens settings: **temperature**
(default 0.2) and **max tool rounds** (default 8) — sent per request as
`temperature` / `max_rounds` to both `/api/agent/chat` and
`/api/agent/generate` — plus the active Ollama URL and `ollama pull`
hints. Model, temperature and round choices persist in localStorage.
### Editing, examples, and guided UX
The canvas has an **edit toolbar** (Select / Add agent / Connect / Delete)
for manual model building — clicking a node or edge opens an editor in the
detail panel (name, state override, edge weight). The **Structure** tab
edits agent types (state schema + rule DSL with inline validation errors)
and the environment (variables + source/sink flows). New endpoints:
`PUT /api/agents/{id}`, `PUT /api/interactions/{id}`,
`PUT /api/agent-types/{id}` (all validated and audit-logged).
The **Examples** tab loads curated systems in one click
(`GET /api/examples`, `POST /api/examples/{name}`): the innovation-market
cascade, a predator–prey meadow, and a two-tier supply network (nested
hierarchy) — each with a what-to-watch blurb. A first-run help overlay,
per-tab explainer bars, and empty-state hints guide new users; the
emergence panel shows the strongest sub-threshold shifts
(`near_misses` on `GET /api/runs/{id}/emergence`) when no events fire.
### Graph canvas
The system graph is fully interactive: drag empty space to **pan**,
mouse-wheel to **zoom** (anchored at the cursor, with a zoom indicator and
a Fit button), drag nodes to rearrange them — positions **persist
server-side** (`pos_x`/`pos_y`, saved on drag-end) and auto-layout only
fills nodes without a saved position. Edges show direction arrows, green
solid / red dashed for positive / negative coupling, and weight labels
where they carry information beyond the sign. Nodes are labeled with name
+ primary state value after a run; hovering shows a tooltip with the full
agent state; a type legend sits in the corner. The pure geometry/layout
helpers live in `app/static/graph.js` and are unit-tested with
`node --test` (see `tests/js/`).
## Layout
```
app/
main.py FastAPI app, REST API, static UI
tools.py shared tool registry (names/schemas/execution) for MCP + agent
mcp_server.py MCP stdio server fronting the REST API (CAS_API_URL)
agent.py in-app AI agent: Ollama tool-calling loop (OLLAMA_URL)
jobs.py in-process background jobs for slow LLM work (submit/poll)
generate.py natural-language -> validated CAS system spec -> repository
examples.py curated one-click example systems
db.py engine + session factory (DATABASE_URL, default sqlite:///./cas_studio.db)
repository.py SQLAlchemy models + Repository (single DB access point)
rules.py rule DSL validation + evaluation (pure functions)
engine.py deterministic seeded synchronous simulation engine
analysis.py emergence events, loops, sensitivity, Moran's I
seed.py the innovation-diffusion demo model
static/index.html canvas UI (graph, run controls, chart, loops/sensitivity/inject panels, AI agent)
static/graph.js pure canvas helpers (view transform, force layout, edge geometry)
alembic/ schema migrations
tests/ pytest, one file per concern
```
## License
MIT — © 2026 Vector Stream Systems LLC.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues