A self-organizing LLM swarm modeled on cellular biology.
Zygote is a runtime where language models act as autonomous cells in a living organism. There's no fixed workflow graph and no central scheduler. A single stem cell receives a task, spends energy to think, and grows whatever structure the task needs: it spawns child cells, those children spawn their own children, work gets delegated down and results come back up. Cells that produce value feed the organism; cells that don't, starve. When the work is done the organism shrinks back to rest.
A living energy economy where good work pays for itself and bad work runs out of energy before it can pollute the result.
The live monitor: a real organism. A root stem spawned a dialectic_stem and a cartography_stem, which in turn spawned their own children (market_enumerator, dialectic_interlocutor, researcher) — each node shows its phenotype, id, and remaining energy.
- The idea
- How it works
- What you get
- Quickstart
- Using Zygote
- Designing new cell types
- MCP connectivity and tool bundles
- The monitor
- Configuration
- Built-in phenotypes
- Project layout
- Limitations
- License
Most multi-agent frameworks make you pre-declare a topology: this agent calls that agent in this order. Zygote doesn't. It borrows a few ideas from cell biology and lets the structure emerge instead:
| Biology | Zygote |
|---|---|
| A cell with DNA | An LLM session with a system prompt (its phenotype) and a tool set |
| The genome | A registry of reusable phenotype blueprints |
| Metabolism | An energy economy — every thought and action costs energy |
| Biomass / fitness | Children return energy to their parent in proportion to the value they produce |
| Growth and apoptosis | Cells spawn under load and die (or hibernate) when idle |
The part that drives everything is the energy economy. A cell starts with an energy budget. Every LLM turn, tool call, and spawn draws it down. When a child finishes it self-rates its output from 0 to 100, and the parent absorbs energy back:
energy_gained = (value / 100) × biomass_multiplier × spawn_cost
A few consequences fall out of that one rule:
- A child that returns dense, high-value output can earn the parent more energy than it cost to spawn, so the organism gets richer by doing good work.
- A lazy or low-quality child returns less than it cost — a net loss the organism can't afford to repeat.
- Over-spawning is self-limiting, because launching ten cells at once costs ten budgets up front. The stem has to be deliberate.
- The organism grows about as much as the task funds and no more.
Nothing in the system tells cells when to stop. They stop because they run out of reasons (and energy) to keep going.
Every cell is the same runtime object: an asyncio task running an agentic loop. There's no separate "manager" and "worker" class. The only thing that tells one cell apart from another is its phenotype (prompt, tools, model, budget). A stem cell is just a phenotype that happens to hold the spawn tool, so it can create children. A child that also holds spawn can grow children of its own, so the tree is fractal rather than two tiers deep.
boot → subscribe to bus topics → wait for a message
→ inject message as a user turn → run one LLM turn
→ (call tools: spawn / artifact / browser / send_message / …)
→ repeat until the cell calls reply(), hibernate(), or die()
A cell only perceives the world through inbound bus messages, and only acts through tool calls. It keeps a conversation history for its lifetime, but a re-spawned cell of the same phenotype starts fresh.
There are three ways a cell can end a turn:
reply()answers the open message and returns value to the parent. The cell stays alive.hibernate()puts the cell to sleep with its context intact. A latersend_messagewakes it up, which is how critique and refinement loops work.die()terminates the cell and releases any final biomass. Used for one-shot leaf cells.
A phenotype is a reusable cell blueprint. It bundles:
- a system prompt (the cell's DNA),
- a tool set, which can differ from one phenotype to the next,
- a model, so different phenotypes can run on different LLMs (more on that below),
- an energy budget (
min_boot_energy) and a biomass multiplier, - a generation range that controls where in the tree it's allowed to spawn,
- and an optional
flowof phases that scripts the cell's internal process.
Phenotypes live in a per-organism genome and come from two places. You can write them yourself: drop a YAML file in app/phenotypes/ and it's part of the genome of every new organism — stable and version-controlled. Or the organism can write its own: a stem-capable cell can call add_phenotype at runtime to mint a phenotype the moment a task turns out to need one. Runtime phenotypes only last for the session, so if one turns out to be useful, copy it into a YAML file to keep it.
In practice the two work best together. An organism that starts with a good set of predefined phenotypes for its early generations ends up better-distributed than one that has to invent everything from scratch. The predefined phenotypes anchor the overall shape; the runtime ones fill in whatever gaps the specific task exposes. That's what the built-in library is for — giving the first generations a decent vocabulary to start from.
Each phenotype declares a generation_range: [min, max] (use -1 for max to mean no ceiling). A cell's generation is its depth from the root stem, with the root at generation 0. The range decides at what depth a phenotype can be spawned, so you can keep a broad coordinator near the top and a narrow worker further down. It stops planners from showing up as leaves and fine-grained workers from being spawned at the root.
Because model is set per phenotype, Zygote ends up being a single layer over the major LLM providers — Anthropic Claude, OpenAI, Google Gemini, and local Ollama models all sit behind one normalized client (ai_factory). You declare the models once in config.yaml, and a phenotype either names one or leaves it null to use the organism default.
So routing different work to different models is a genome decision rather than plumbing. Send deep planning to a frontier model, cheap classification to a fast one, and offline work to a local Ollama model, all inside the same organism. The cell doing the spawning doesn't know or care which provider is behind a child; it just spawns a phenotype by name.
An organism isn't a generic chat session. It's a structure built around one task or goal, and everything it grows — the cells, the sub-trees, the runtime phenotypes, the artifacts — exists to serve that task.
Organisms are also persistent and meant to take more than one prompt. An organism is built to handle a series of related prompts that belong to the same overall goal, and it gets better at them as it goes, because between prompts it keeps:
- its root stem, which is reused rather than re-spawned, so it accumulates context across the conversation,
- its genome, including any phenotypes it invented on earlier prompts,
- its chat history, and
- its artifact blackboard.
So the second prompt arrives at a body that already understands the first, with the structure that worked still standing. Keep sending related prompts and the organism gets better-shaped to the work. Send something unrelated and you're better off creating a new organism.
Organisms are keyed by API key and isolated from each other, persisted in Redis with a TTL, and garbage-collected once they go idle. One Zygote process can host many organisms for many clients at the same time.
Everything between cells goes over a Redis pub/sub bus. Routing is by a dotted subject:
| Pattern | Meaning |
|---|---|
phenotype.<name> |
Species channel. Competitive — exactly one cell of that type claims each message. |
parent.<cell_id>.<topic> |
A parent talking to a child (task, energy grant/denial, chat). |
child.<cell_id>.<topic> |
A child reporting up (result, energy request, chat). |
system.no_consumer |
Fired when a message has no live subscriber. This is what drives reactive spawning. |
A prompt comes in, the root stem grows a tree of children, and value flows back up as each cell replies to its parent. Cells can also message each other directly, and any cell can call its tools — usually the artifact blackboard, to move large payloads around without bloating its context.
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e8eefc','primaryTextColor':'#1a1a1a','primaryBorderColor':'#5b6cf0','lineColor':'#8a8f98','clusterBkg':'#f3f0ff','clusterBorder':'#b9bcc4'}}}%%
flowchart TD
client(["MCP client · Web monitor · CLI"])
client ==>|prompt| stem
stem ==>|answer| client
subgraph org["Organism"]
direction TB
stem["stem · gen 0"]
a["child · gen 1"]
b["child · gen 1"]
a1["child · gen 2"]
a2["child · gen 2"]
stem --> a
stem --> b
a --> a1
a --> a2
a -.-> stem
b -.-> stem
a1 -.-> a
a2 -.-> a
b <-.-> a1
end
org --- shared["Shared resources<br/>tools · web · external MCP · artifact blackboard (Redis)"]
linkStyle 0,1 stroke:#22c55e,stroke-width:2px
The thick green arrows carry the prompt in and the answer out. Solid arrows are spawn: a parent creating a child, so the tree grows downward. Dotted arrows are reply, carrying the result and the earned biomass back up to the parent. The double-dotted edge is a peer send_message, since any cell can talk to any other cell, not just its parent. Tools and the artifact blackboard sit outside the tree, and every cell can reach them.
- Topology that emerges from the task instead of a graph you wire by hand.
- Organisms that are built around a goal and reused across a series of related prompts.
- An energy economy that makes quality pay off and keeps over-engineering in check.
- A genome you can author as YAML, that the organism can also extend at runtime.
- Generation gating, so phenotypes only spawn at depths that make sense.
- One layer over Claude, OpenAI, Gemini, and Ollama, with the model chosen per phenotype.
- Fractal delegation: any stem-capable cell grows its own sub-tree, and the root never sees the leaves.
- Hibernate/wake, so cells keep context between turns for critique and refinement loops.
- Multi-tenant organisms, isolated per API key and persisted in Redis with TTL and GC.
- An MCP server of its own, plus the ability to pull in tools from external MCP servers.
- Named tool bundles the model picks by name instead of juggling individual tools.
- An artifact blackboard in Redis for passing large payloads as slugs.
- A live web monitor for the organism graph, events, chat, and artifacts.
- Python 3.11+
- Redis (used for the bus, organism persistence, and the artifact blackboard)
# macOS brew install redis && brew services start redis # Debian/Ubuntu sudo apt-get install redis-server && sudo systemctl start redis
- At least one model provider:
- Ollama (local, no API key) is the default. Install it from ollama.com and pull a model.
- Or a cloud key for Anthropic, OpenAI, or Gemini.
git clone <your-fork-url> zygote
cd zygote
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
playwright install chromium # only needed for the browser_* toolscp .env.example .envYou only need to edit .env if you're enabling a cloud provider:
# ANTHROPIC_API_KEY=sk-ant-...
# OPENAI_API_KEY=sk-...
# GEMINI_API_KEY=AI...Then open config.yaml and make sure ai_models.default_model points at a model you've actually enabled. Out of the box Zygote uses a local Ollama model; uncomment the provider and model blocks you want.
⚠️ Replace the default API keys before exposing the server.config.yamlships with placeholder keys (mcp.api_keysandmcp.local_api_key) so the project runs out of the box. They are public and guessable. The MCP server binds to127.0.0.1only, so the defaults are safe for purely local use — but if you change the bind address or put the server behind any kind of proxy, replace them with strong random values first (e.g.python -c "import secrets; print(secrets.token_urlsafe(32))").
python app/main.pyThat boots the whole stack:
| Service | URL | Purpose |
|---|---|---|
| Web monitor | http://127.0.0.1:7891 | Visual dashboard and chat |
| MCP server | http://127.0.0.1:7892/mcp | Programmatic API (Bearer auth) |
| Event stream | tcp://127.0.0.1:7890 | Raw event feed for the CLI viewer |
You can also run a one-shot task from the command line:
python app/main.py "Research the 2025 state of solid-state batteries and write a 1-page brief"Open the web monitor in a browser to watch the organism grow.
Go to http://127.0.0.1:7891. You can create organisms, type prompts into the chat, watch cells boot, spawn, and die on the graph, and inspect stored artifacts, all without writing any code.
Zygote exposes an MCP server over streamable HTTP. Authenticate with a Bearer token from mcp.api_keys in config.yaml (requests from 127.0.0.1 can skip auth if local_passthrough is on). The tools:
| Tool | Description |
|---|---|
create_organism(name, ttl_seconds) |
Create a new isolated organism; returns organism_id. |
prompt(organism_id, text, energy_budget) |
Run a prompt, stream progress events, return the final answer. |
prompt_async(organism_id, text, energy_budget) |
Start a long task and return a task_id right away. |
poll_result(task_id) |
Check status or fetch the result of an async task. |
list_organisms() |
List organisms owned by this API key. |
decompose_organism(organism_id) |
Delete an organism and its artifacts for good. |
artifact_put / artifact_get / list_artifacts |
Read and write the organism's artifact blackboard. |
Point any MCP-capable client (Claude Desktop, an agent SDK, your own code) at http://127.0.0.1:7892/mcp with the header Authorization: Bearer <your-key>.
In a second terminal, stream the raw events:
python -m app.monitor.cli_viewerA new cell type is just a YAML file in app/phenotypes/. Drop it in, restart, and it joins the genome of every new organism. This is the same schema the organism uses when it invents a phenotype at runtime through add_phenotype — writing one by hand is just doing on purpose what the organism does on the fly. A good set of predefined phenotypes is the main lever you have over how an organism grows.
name: researcher # required — used in spawn("researcher")
description: | # required — the stem reads this to decide when to pick this phenotype
Use this to answer one specific factual sub-question using the web.
INPUT: a single sub-question. OUTPUT: an artifact slug with cited findings.
model: null # null = organism default_model; or a named model e.g. "ollama-gemini-flash"
min_boot_energy: 60.0 # energy deducted from the parent on spawn (floor set by config)
biomass_multiplier: 6.0 # return multiplier; dynamic phenotypes are capped by config
generation_range: [1, 3] # depths this phenotype may spawn at; [min, max], max=-1 = no ceiling
tools: # assignable tools (universal tools are injected automatically)
- browser_search
- browser_fetch
- artifact_put
- artifact_get
default_subscriptions:
- phenotype.researcher # convention: phenotype.<name>
flow: # optional — an ordered set of phases injected into the prompt
phases:
- id: gather
label: "Phase 1: Gather"
description: "Search for relevant data with browser_search / browser_fetch."
when: "task received"
- id: reply
label: "Phase 2: Reply"
description: "Store findings via artifact_put and reply() with the slug."
when: "data gathered"
system_prompt: | # required — the cell's DNA
You are a Researcher Cell in the Zygote organism.
Answer one specific sub-question using web search. Follow your flow phases.
When done, reply() with the artifact slug containing your cited findings.| Field | Notes |
|---|---|
description |
Written for the stem, not the user. It's how the stem picks this phenotype via get_phenotypes(). Keep it task-shaped and state the input/output contract. |
min_boot_energy |
Roughly (expected LLM turns × cost_per_llm_turn) + (expected tool calls × cost_per_tool_call) plus about 30% headroom. |
biomass_multiplier |
Scale it to output density: higher (6–8) for cells that produce dense, factual content, lower (2–3) for cells that mostly route or coordinate. |
generation_range |
Controls how deep this phenotype can appear, which keeps coordinators off the leaves and workers off the root. |
flow |
Worth adding when a cell has two or more distinct internal stages; it stops the model from collapsing them into one turn. |
tools |
List only assignable tools. Universal ones (reply, hibernate, send_message, introspect, the artifact tools) are added automatically. |
Every tool has a scope that decides who can hold it:
| Scope | Who gets it | Examples |
|---|---|---|
| universal | every cell, automatically | reply, hibernate, send_message, introspect, artifact_put/get/list |
| bootstrap | only predefined phenotypes (stem-class) | spawn, get_phenotypes, grant_energy, deny_energy |
| assignable | any phenotype that lists it | browser_search, browser_fetch, add_phenotype, die, request_energy, external MCP tools |
For deeper patterns — fractal delegation, refinement loops, tournament selection, scout-gated growth — see ZYGOTE_SKILL.md, which goes into how to compose organisms in detail.
Zygote speaks MCP in both directions.
Declare external MCP servers under external_mcps: in config.yaml. Their tools get registered as assignable under the namespace <name>__<tool_name> and become available to cells. Both stdio and http transports work, and ${ENV_VAR} references are resolved from the environment so secrets stay out of the file.
external_mcps:
- name: filesystem
transport: stdio
command: ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
enabled: true
- name: github
transport: http
url: "https://api.githubcopilot.com/mcp/v1"
auth:
type: api_key
header: Authorization
value: "Bearer ${GITHUB_TOKEN}"
enabled: trueIndividual tool names are an implementation detail. What the model actually sees is a set of named tool profiles — bundles it picks by name when it invents a phenotype through add_phenotype:
tool_profiles:
web_research:
description: "Search the web and fetch full page content."
tools: [browser_search, browser_fetch]
artifacts:
description: "Store and retrieve large text artifacts shared between cells."
tools: [artifact_put, artifact_get, artifact_list]A profile can also bundle tools from an external MCP server you've connected; see the section above.
This keeps the choice at the level of "this cell needs web research" instead of making the model track individual tool names.
The dashboard at http://127.0.0.1:7891 is a live view of the organism over a single WebSocket:
- The organism graph, where cells appear, spawn children, change state (idle, active, hibernating), and die as you watch, laid out by generation and lineage.
- An event feed:
cell_boot,cell_spawned,cell_death,llm_turn,tool_call,phenotype_added,no_consumer, and the rest. - A chat panel to prompt any organism, with the stem's answer streaming back and mid-process progress folded into the history.
- An organism switcher to create, switch between, and decompose organisms.
- An artifact inspector for browsing the slugs and contents on the blackboard.
- External MCP status, showing the connection health of each external server.
The same event stream is available over plain TCP on port 7890 for headless use, which is what python -m app.monitor.cli_viewer consumes.
Everything is driven by config.yaml. The main sections:
| Section | What it controls |
|---|---|
ai_models.providers |
The provider and model catalog (OpenAI, Ollama, Claude, Gemini). Uncomment models to enable them. |
ai_models.default_model |
The model used when a phenotype's model: is null. |
organism.energy |
The economy: cost_per_llm_turn, cost_per_tool_call, stem_max_energy, biomass caps, energy floors. |
organism.compact |
Context compaction thresholds and the (doubling) energy penalty. |
organism.default_ttl_seconds / max_ttl_seconds |
How long idle organisms stay in Redis before GC. |
organism.artifact_ttl_seconds |
How long artifacts live on the blackboard. |
external_mcps |
External MCP servers to connect as tool sources. |
tool_profiles |
The named tool bundles exposed to the model. |
mcp |
Zygote's own MCP server: host/port, api_keys, local passthrough, default energy budget. |
bus |
The Redis connection for the message bus. |
monitor |
Ports and capacity for the event stream and web dashboard. |
Energy defaults out of the box:
| Knob | Default | Meaning |
|---|---|---|
cost_per_llm_turn |
2.0 |
Energy per LLM turn |
cost_per_tool_call |
5.0 |
Energy per tool call |
cell_min_boot_energy_default |
35.0 |
Floor for any phenotype's spawn cost |
stem_max_energy |
1500.0 |
Energy cap for a stem |
mcp.default_energy_budget |
300.0 |
Budget granted per prompt when the caller doesn't specify one |
max_phenotype_biomass_multiplier |
4 |
Cap on the biomass multiplier for runtime-created phenotypes |
The genome ships with a general stem plus a handful of stems that each encode a particular multi-cell reasoning pattern:
| Phenotype | What it does |
|---|---|
stem |
General planner and coordinator — the organism's root. |
falsification_stem |
Attacks a plausible claim from several angles and reports whether it was falsified, partly wounded, or withstood. |
corroboration_stem |
Looks for independent confirmation of a claim across distinct sources. |
dialectic_stem |
Runs thesis, antithesis, and synthesis over a contested question. |
rotating_critique_stem |
Refines a draft with critics that rotate over it. |
tournament_stem |
Generates competing candidates and picks the strongest. |
cartography_stem |
Maps the shape of a problem before committing to depth. |
provenance_stem |
Traces and checks where evidence actually came from. |
These are starting points, not a fixed set. The whole point is that the organism can invent the phenotypes it needs at runtime with add_phenotype.
zygote/
├── app/
│ ├── main.py # entry point — boots the whole stack
│ ├── config.py # config loader
│ ├── phenotypes/ # cell blueprints (the YAML genome)
│ ├── tools/ # tool definitions (YAML schema + optional Python handler)
│ ├── services/
│ │ ├── cell.py # the cell runtime and agentic tool loop
│ │ ├── genome.py # phenotype registry
│ │ ├── tool_registry.py # tool scopes and tool profiles
│ │ ├── tool_loader.py # loads tools from YAML
│ │ ├── ai_factory.py # the LLM client layer over all providers
│ │ ├── mcp_client_manager.py # outbound external-MCP connections
│ │ ├── external_mcp_config.py # parses external_mcps / tool_profiles
│ │ └── bus/ # the pub/sub bus (Redis backend)
│ ├── zygote_organisms/ # organism lifecycle and Redis persistence
│ ├── zygote_mcp/ # Zygote's own MCP server (tools, auth, server)
│ └── monitor/ # event store, TCP server, web dashboard
├── config.yaml # central configuration
├── requirements.txt
└── ZYGOTE_SKILL.md # notes on composing organisms
Zygote is a research prototype. The rough edges worth knowing about:
- The genome resets on restart. Phenotypes invented at runtime don't survive a restart, so move the keepers into
app/phenotypes/*.yaml. - Energy calibration is empirical. The right
min_boot_energydepends on the model and the task. A cell that runs out of energy before reporting is miscalibrated, not broken — start generous and tune by watching the monitor. - The model is the ceiling. Zygote orchestrates, it doesn't make up for a model that can't reason. Use a strong model for stems and cheaper ones for leaf cells.
- Parallel spawning costs up front. Ten simultaneous spawns cost ten budgets immediately, so fund the stem accordingly or spawn in batches.
- Redis is required. The bus, persistence, and artifacts all assume a reachable Redis instance.
MIT — see LICENSE. © 2026 Erez Azaria.
