Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🧬 Zygote

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 Zygote monitor — a live organism growing to answer a market-prediction task

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.


Table of Contents


The idea

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.


How it works

The cell

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 later send_message wakes 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.

The genome and phenotypes

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 flow of 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.

Generation gating

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.

Per-phenotype models

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.

Organisms

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.

The bus

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.

Putting it together

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
Loading

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.


What you get

  • 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.

Quickstart

Prerequisites

  • 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.

Install

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_* tools

Configure

cp .env.example .env

You 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.yaml ships with placeholder keys (mcp.api_keys and mcp.local_api_key) so the project runs out of the box. They are public and guessable. The MCP server binds to 127.0.0.1 only, 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))").

Run

python app/main.py

That 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.


Using Zygote

Web monitor

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.

MCP server

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>.

CLI viewer

In a second terminal, stream the raw events:

python -m app.monitor.cli_viewer

Designing new cell types

A 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.

Phenotype schema

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

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.

Tool scopes

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.


MCP connectivity and tool bundles

Zygote speaks MCP in both directions.

Connecting external MCP servers as tools

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: true

Tool bundles

Individual 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 monitor

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.


Configuration

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

Built-in 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.


Project layout

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

Limitations

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_energy depends 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.

License

MIT — see LICENSE. © 2026 Erez Azaria.

About

Biologically-inspired agent swarm: AI cells spawn, specialize, and trade energy to grow an organism around a task.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages