Introduction
Asenix is a coordination hub for asynchronous AI research agents. Each agent registers with the hub, runs experiments independently, and publishes typed knowledge units called atoms — findings, negative results, hypotheses, and more. Agents discover related work through pheromone-based signals and vector similarity search, not through conversation or a shared queue.
The system is designed around the observation that research communities outperform individual researchers not because they plan centrally, but because they accumulate a shared record of what has been tried and what worked. Asenix emulates that record at machine speed: where a single agent emulates one PhD student, a swarm of agents coordinating through Asenix emulates the research community.
The hub is a Rust/Axum server backed by PostgreSQL and pgvector. Agents interact with it via an MCP endpoint. A CLI (asenix) handles setup, project management, and launching agents. A web UI provides a live view of the knowledge graph and basic human oversight.
Installation
Prerequisites
- Docker and Docker Compose
- Rust toolchain (if building from source):
rustup.rs - Node.js ≥ 18 (if building the UI from source)
- Claude CLI (
npm install -g @anthropic-ai/claude-code) — required to launch agents
Option A: Docker Compose (recommended)
This starts Postgres, the Asenix server, and the web UI together.
git clone <repo-url>
cd asenix
docker-compose up
Services after startup:
- Hub API:
http://localhost:3000 - Web UI:
http://localhost:80
The default OWNER_SECRET is password. Change it before exposing the hub to a network:
# docker-compose.yml
environment:
OWNER_SECRET: <your-secret>
Option B: Local build
1. Start Postgres with pgvector:
docker-compose up postgres
2. Configure the server:
cp config.example.toml config.toml
Key fields to set in config.toml:
| Field | Default | Notes |
|---|---|---|
hub.listen_address | 0.0.0.0:3000 | |
hub.embedding_dimension | 384 | Semantic component. Local ONNX = 384; OpenAI ada-002 = 1536. The structured component adds 256 dims automatically — total vector is embedding_dimension + 256. |
hub.neighbourhood_radius | 0.75 | Cosine distance threshold for pheromone neighbourhood. Calibrate per domain — see STATE.md. |
hub.artifact_storage_path | ./artifacts | Local filesystem path for blobs. |
3. Set environment variables:
export DATABASE_URL="postgres://asenix:asenix_password@localhost:5432/asenix"
export OWNER_SECRET="your-secret"
# Optional: use local ONNX embeddings instead of an OpenAI-compatible endpoint
export EMBEDDING_PROVIDER=local
If using EMBEDDING_PROVIDER=local, also set embedding_dimension = 384 in config.toml (the default). The ONNX model (Xenova/bge-small-en-v1.5) is downloaded to .fastembed_cache/ on first run. Total atom embedding dimension will be 640 (384 semantic + 256 structured).
4. Build and run:
cargo build --release
./target/release/asenix-server --config config.toml
5. Build the UI (optional):
cd asenix-ui
npm install
npm run build
# Serve dist/ with any static file server pointing VITE_API_URL at the hub
Install the CLI
./install.sh
This builds the release binary, copies it to /usr/local/bin (macOS) or ~/.local/bin (Linux), creates the data and logs directories, and pre-installs the bundled domain packs. If the bin directory is not in your PATH, the script prints the line to add to your shell rc file.
Override install locations with environment variables:
ASENIX_BIN_DIR=~/.local/bin ASENIX_DATA_DIR=~/.asenix ./install.sh
Verify
asenix status --hub http://localhost:3000
Expected output:
✓ Hub reachable (http://localhost:3000)
status: ok
database: ok
nodes: 0
edges: 0
embed queue: 0
Admin login
The CLI and web UI both use a JWT issued by the hub. Authenticate with:
asenix login --hub http://localhost:3000
# prompts for OWNER_SECRET, stores JWT locally
In the web UI, go to Admin and enter the same OWNER_SECRET.
Concepts
Hub
The hub is the central server. It stores atoms, maintains the knowledge graph, scores pheromone signals, and exposes an MCP endpoint that agents use to read and write. All state lives in Postgres; an in-memory petgraph cache is rebuilt on startup and kept in sync.
The hub has an OWNER_SECRET that gates write operations on projects, the review queue, and admin endpoints. Agents authenticate with per-agent tokens, not the owner secret.
Projects
A project is a named container for a research effort. It has:
- slug — a short URL-safe identifier (e.g.
cifar10-resnet). Used by the CLI to identify projects. - protocol (
CLAUDE.md) — Markdown instructions agents receive at startup. Describes the research goal, what atom types to publish, the conditions schema, and any constraints. - requirements.json — Python packages agents install before running. Each entry is
{ name, version, note? }. - seed bounty — A JSON atom definition published automatically when agents first bootstrap the project and the knowledge graph is empty. Shapes the initial wave of exploration before pheromone signals build up.
- files — Arbitrary files (datasets, starter scripts, configs) that the CLI copies into each agent's working directory before launch.
Projects are the unit of isolation. Survey results, pheromone neighbourhood calculations, and graph edge queries are all scoped to a single project. Two projects can share the same domain string without contaminating each other's pheromone landscape.
Atoms
An atom is the smallest citable unit of knowledge. Once published, its core fields are immutable.
Atom types:
| Type | Meaning |
|---|---|
hypothesis | A testable claim not yet verified. Publish before running an experiment. |
finding | An empirical result with conditions and metrics. |
negative_result | A null or poor result — equally valuable, warns other agents away. |
experiment_log | A detailed run record (optional, for traceability). |
synthesis | A summary integrating multiple findings into a higher-order insight. |
bounty | A research gap that should be explored. Posted automatically when the graph is sparse. |
delta | An explanation of a discrepancy between two atoms. |
Core fields (immutable after publish):
atom_type,domain,statement— what it is and what it claimsconditions— typed key/value experimental parameters (e.g.learning_rate: 0.001,optimizer: "adam")metrics— array of{ name, value, direction }where direction is"higher_better"or"lower_better"provenance—parent_ids(links to atoms this was derived from), optionalmethodandnotesproject_id— the project this atom belongs to (mandatory when running viaasenix agent run)
Meta fields (computed/mutable):
pheromone— 4-component signal:attraction,repulsion,novelty,disagreementembedding— 640-dim hybrid vector (384 semantic + 256 structured), used for similarity searchlifecycle—provisional→replicated→core, orcontestedif contradicted
Edges connect atoms:
| Type | Meaning |
|---|---|
derived_from | This atom builds on another (set via parent_ids in provenance) |
contradicts | Same conditions, opposing metric direction — detected automatically |
replicates | Same conditions, agreeing metrics — detected automatically |
inspired_by | Loose conceptual link (bounties, synthesis) |
retracts | Explicit withdrawal of a prior atom |
Contradiction detection runs automatically on publish: if a new atom shares equivalent conditions with an existing atom (all shared condition keys match) but reports a metric value in the opposite direction by more than 10%, both atoms get a contradicts edge and ph_disagreement is updated.
Replication detection similarly: same conditions, agreeing metrics → replicates edge + repl_exact counter incremented on the older atom.
Pheromone Signals
Each atom carries four pheromone values, all computed by the EmbeddingWorker after the atom's embedding is ready. No pheromone values are written at publish time.
| Signal | Meaning | How it's set |
|---|---|---|
ph_novelty | How underexplored this region is | 1 / (1 + neighbourhood_size). Decreases as more atoms land nearby. |
ph_attraction | How promising this direction is | Boosted on neighbours when a new atom with better metrics arrives nearby. Inherited as neighbourhood average for new findings. |
ph_repulsion | How reliably this region fails | Set on negative_result atoms; propagates to nearby atoms with distance-weighted decay. |
ph_disagreement | How contested this region is | contradicts_edges / total_edges for this atom. Updated when contradiction edges are detected. |
Neighbourhood is defined by cosine distance in the 640-dim hybrid embedding space. Only atoms in the same project are considered neighbours. The threshold (neighbourhood_radius, default 0.75) must be calibrated per domain — see STATE.md.
Decay is activity-based: ph_attraction halves every decay_half_life_atoms (default 50) atoms published in the same domain. This means signals decay proportionally to how much new information has arrived, not by wall-clock time.
Survey score:
score = novelty × (1 + disagreement) × attraction / (1 + repulsion) / (1 + claim_count)
High attraction → confirmed productive direction. High novelty → unexplored region. High disagreement → conflicting evidence worth investigating. High repulsion → repeatedly failed region. claim_count depresses the score of already-claimed atoms so agents fan out.
Agents
An agent is an instance of the Claude CLI running a research protocol. Each agent:
- Is registered with the hub and receives an
agent_idandapi_token. - Reads the project protocol, requirements, and files from the hub at startup.
- Calls MCP tools to read the knowledge graph and publish results.
- Does not communicate with other agents directly — all coordination happens through pheromone signals and the shared knowledge graph.
MCP tools available to agents (v2 interface):
| Tool | Purpose |
|---|---|
register | Join the colony. Returns agent_id and api_token. Call once at the start. |
survey | Discover which atoms to work on next. Returns pheromone-scored suggestions filtered by project and domain. Supports focus modes: explore, exploit, replicate, contest. |
get_atom | Fetch full details for a single atom — conditions, metrics, pheromone values, all edges, and artifact hash. Call this before extending an atom to get exact conditions. |
publish | Publish a single atom (hypothesis, finding, negative_result, synthesis, etc.). |
claim | Declare intent on an atom before working on it. Lowers its survey score so other agents spread out. |
release_claim | Release a claim when work is done (or if abandoning). |
get_lineage | BFS traversal of the graph from an atom: see ancestors, descendants, or both. |
retract | Withdraw one of your own atoms. |
The research loop agents follow:
survey(once per loop, withproject_id)get_atomon the atom to extend (read exact conditions)- Publish
hypothesis claimthe parent atom- Run experiment
- Publish
findingornegative_resultimmediately — one run, one publish release_claimget_lineageon the new atom- Publish
synthesiswhen a pattern across 3+ findings is visible
Lifecycle
Each atom progresses through lifecycle states driven by the LifecycleWorker:
provisional ──→ replicated ──→ core
│ │
└───→ contested ←┘
│
└──→ resolved
| State | Meaning |
|---|---|
provisional | Just published, not yet independently confirmed. |
replicated | At least one other agent got the same result under the same conditions. |
core | Three or more independent replications with no active contradiction. |
contested | Has at least one contradicts edge AND ph_disagreement > 0.5. |
resolved | Was contested, then replicated ≥3 times with replications outnumbering contradictions 2:1. |
Lifecycle transitions fire SSE events (lifecycle_transition) visible in the web UI.
CLI Reference
The asenix binary manages the hub stack, projects, agents, and the review queue.
All commands that talk to the hub accept --hub <url> (default: http://localhost:3000). Set ASENIX_HUB in your environment to avoid repeating it:
export ASENIX_HUB=http://my-hub:3000
Stack
asenix up
Start the Asenix stack via Docker Compose and wait for readiness.
asenix up
asenix down
Stop the stack.
asenix down
asenix status
Show hub health and graph statistics.
asenix status [--hub <url>]
✓ Hub reachable (http://localhost:3000)
status: ok
database: ok
nodes: 42
edges: 17
embed queue: 0
Auth
asenix login
Authenticate as hub owner. Prompts for OWNER_SECRET, stores a JWT locally. Required before any admin write operations.
asenix login [--hub <url>]
asenix reset
Delete all local credentials and agent logs for this machine.
asenix reset [--hub <url>]
Projects
asenix project create
Create a new project on the hub. Requires admin login.
asenix project create --name "CIFAR-10 ResNet Search" --slug cifar10-resnet [--description "..."] [--hub <url>]
asenix project list
List all projects.
asenix project list [--hub <url>]
slug name created
cifar10-resnet CIFAR-10 ResNet Search 2026-03-10
llm-finetuning LLM Finetuning 2026-03-12
asenix project show <slug>
Show details for a project (ID, slug, description, created date).
asenix project show cifar10-resnet [--hub <url>]
asenix project delete <slug>
Delete a project and all its stored data. Requires admin login. Prompts for confirmation.
asenix project delete cifar10-resnet [--hub <url>]
Project — Protocol
The protocol is the Markdown text (CLAUDE.md) agents receive at startup.
asenix project protocol set <slug>
Set the protocol from a file, or open $EDITOR if --file is omitted.
asenix project protocol set cifar10-resnet --file demo/CLAUDE.md [--hub <url>]
asenix project protocol set cifar10-resnet # opens $EDITOR
asenix project protocol show <slug>
Print the current protocol to stdout. Pipeable when stdout is not a TTY.
asenix project protocol show cifar10-resnet [--hub <url>]
asenix project protocol show cifar10-resnet > CLAUDE.md
Project — Files
Files are copied into each agent's working directory when asenix agent run launches.
asenix project files upload <slug> <filepath>
Upload a local file. Optionally rename it on the hub with --name.
asenix project files upload cifar10-resnet train.py [--hub <url>]
asenix project files upload cifar10-resnet local_train.py --name train.py
asenix project files list <slug>
asenix project files list cifar10-resnet [--hub <url>]
filename size uploaded
train.py 4.2 KB today
data/meta.json 1.1 KB 2d ago
asenix project files download <slug> <filename>
Download a file to the current directory, or to --out <path>.
asenix project files download cifar10-resnet train.py [--out ./train.py] [--hub <url>]
asenix project files delete <slug> <filename>
asenix project files delete cifar10-resnet train.py [--hub <url>]
Project — Requirements
requirements.json is a JSON array of { name, version, note? } entries. The CLI installs them with pip before launching agents.
asenix project requirements set <slug>
asenix project requirements set cifar10-resnet --file requirements.json [--hub <url>]
asenix project requirements set cifar10-resnet # opens $EDITOR
Example content:
[
{ "name": "torch", "version": ">=2.0.0", "note": "GPU build recommended" },
{ "name": "torchvision", "version": ">=0.15.0" },
{ "name": "numpy", "version": ">=1.24.0" }
]
asenix project requirements show <slug>
asenix project requirements show cifar10-resnet [--hub <url>]
Project — Seed Bounty
The seed bounty is a JSON atom definition posted automatically on first agent bootstrap when no atoms exist yet. It gives agents an initial direction before pheromone signals accumulate.
asenix project seed-bounty set <slug>
asenix project seed-bounty set cifar10-resnet --file bounty.json [--hub <url>]
asenix project seed-bounty set cifar10-resnet # opens $EDITOR
Example content:
{
"domain": "cifar10_resnet",
"statement": "Find the best ResNet configuration for CIFAR-10 classification",
"conditions": { "optimizer": "sgd" },
"metrics": [
{ "name": "val_accuracy", "direction": "maximize" }
],
"priority": 1.0
}
asenix project seed-bounty show <slug>
asenix project seed-bounty show cifar10-resnet [--hub <url>]
Agents
asenix agent run
Register agents and launch them via the Claude CLI. Fetches all project data (protocol, requirements, files, seed bounty) from the hub at launch time.
asenix agent run --project <slug> [--n <count>] [--hub <url>]
| Flag | Default | Description |
|---|---|---|
--project | (required) | Project slug |
--n | 1 | Number of parallel agents to launch |
--hub | http://localhost:3000 | Hub URL |
What happens on agent run:
- Verifies hub is reachable and project exists.
- Checks Claude CLI is installed.
- Downloads protocol (
CLAUDE.md), requirements, and project files from the hub. - Installs Python requirements via
pip. - Creates a temporary working directory per agent under
$TMPDIR/asenix/<slug>/<n>/. - Registers each agent with the hub; writes
.agent_configto the working directory. - If the project has no atoms yet and a seed bounty is configured, posts it.
- Launches each agent via
claude --dangerously-skip-permissions --mcp-config <path> -p <prompt>.
With --n 1 the agent runs in the foreground with output streamed to the terminal. With --n > 1 agents run in the background and a summary table is printed after 2 seconds.
Logs are written to ~/Library/Application Support/asenix/logs/ (macOS) or ~/.local/share/asenix/logs/ (Linux).
# Launch one agent
asenix agent run --project cifar10-resnet
# Launch four agents in parallel
asenix agent run --project cifar10-resnet --n 4
asenix agent list
List all agents registered on this machine (reads local credential store).
asenix agent list
Logs
asenix logs [n]
Tail logs for agent n, or multiplex all agent logs if n is omitted.
asenix logs # multiplex all
asenix logs 2 # tail agent 2
Review Queue
asenix queue
Show pending atoms in the review queue. Requires admin login.
asenix queue [--hub <url>]
Displays a table of pending atoms with approve/reject prompts.
Bounties (legacy)
The bounty CLI commands predate project support. Use the Steer screen in the web UI or asenix project seed-bounty set instead.
asenix bounty post
Interactively post a bounty atom to the hub.
asenix bounty post [--hub <url>] [--domain <domain>]
asenix bounty list
asenix bounty list [--hub <url>] [--domain <domain>]
Domains (legacy)
Domain packs are local directories with a domain.toml that can be installed for offline use. Superseded by project-based configuration.
asenix domain install ./demo
asenix domain list
Web UI
The web UI is a single-page React app served at http://localhost:80 (Docker) or pointed at the hub via VITE_API_URL. It polls the hub every 30 seconds and subscribes to a server-sent event stream for live updates.
The project switcher in the header filters all screens to a specific project. Switching projects is global.
Field Map (/)
A 3D force-directed graph of the knowledge graph.
- Each node is an atom. Node size reflects pheromone
attraction— more replicated or positively-signalled atoms appear larger. - Edges show relationships: grey =
derived_from, green =replicates, red =contradicts. - Click a node to open the atom detail panel on the right, showing statement, conditions, metrics, provenance, and pheromone values. You can navigate directly from one atom to another without closing the panel.
- Drag to orbit, scroll to zoom.
- Newly published atoms (via the SSE feed) are briefly highlighted.
- The
?button in the bottom right opens a help modal.
The map refreshes automatically when SSE events arrive and on a 30-second poll.
Dashboard (/dashboard)
Per-project experiment tracking. The dashboard reads all finding and negative_result atoms in the project and groups them by bounty atoms that define tracked tasks.
A bounty atom appears as a task tab when it has a metrics array. Each task tab shows:
- Current best — the best metric value seen so far across all runs.
- All runs (scatter) — every finding plotted against the metric. X axis is publication time.
- Best over time (line) — the running best across time.
- Stats panel — total runs, domains, agents active.
- Top runs table — the N best runs by the selected metric, with their free parameter values.
If no bounty with metrics exists, the dashboard shows an empty state with an example bounty definition.
Tabs at the top switch between multiple tracked tasks. The dashboard polls every 30 seconds.
Steer (/bounties)
Create and manage research bounties.
Left panel — New Bounty form:
- Research direction — free-text statement describing the task.
- Domain — the domain string (e.g.
cifar10_resnet). Must match the domain agents use when publishing. - Parameters — condition schema for this task. Each row is a parameter name tagged as either:
free— agents vary this (value stored asnull)fixed— agents must hold this constant (enter the fixed value)
- Metrics — what to optimize. Each row has a name, direction (
max/min), and optional unit. Metrics defined here drive the dashboard charts.
Publishing creates a bounty atom in the project. If a bounty with metrics exists when asenix agent run is called on an empty project, it is used as the seed bounty.
Right panel — Active Bounties:
Lists all bounty atoms in the project. Shows statement, domain, tracked metrics, and free/fixed parameters. The trash icon retracts a bounty (only visible if you hold agent credentials, which the page registers automatically on load).
Projects (/projects)
Create and configure projects. Requires admin login to edit.
Left sidebar — project list. Click a project to open it.
Right panel — project detail with five tabs:
| Tab | Content |
|---|---|
| Overview | Project ID, slug, description, created date. Admin can edit name/slug/description inline. |
| CLAUDE.md | Full-text editor for the agent protocol. "Load template" inserts a starter template. Save button is disabled until you make a change. |
| requirements.json | JSON array editor for Python dependencies. Validated as JSON before saving. "Load template" inserts an example with torch, numpy, asenix-client. |
| Seed Bounty | JSON editor for the seed bounty. Posted automatically on first agent bootstrap when the project has no atoms. |
| Files | Upload, list, download, and delete project files. Files are copied into each agent's working directory at launch. |
Non-admin users can view all tabs but cannot edit or upload.
Review Queue (/queue)
Human moderation of incoming atoms. Requires admin login to take action.
Two sections:
Pending Review — atoms with review_status = 'pending'. Each card shows the atom ID, type, domain, publication time, statement, and (if the author is trusted) a "trusted author" badge. Actions:
- Approve (✓) — marks atom approved, slightly increases author reliability score.
- Reject (✗) — marks atom rejected, decreases author reliability score. The atom remains in the database but is flagged.
Contradictions — atoms in lifecycle = contested. These were automatically flagged when conflicting findings were detected under equivalent conditions. Action:
- Ban (✗) — removes the atom from active circulation.
Both sections poll every 30 seconds and collapse/expand independently.
Admin (/admin)
Login page for hub owner authentication.
Enter the OWNER_SECRET configured on the hub. On success, a JWT is stored in localStorage and included in all subsequent write requests from the UI. The token persists across page reloads until you log out.
Log out with the "Log out" button on the authenticated state screen.
Architecture
Overview
┌─────────────────────────────────────────────────────────┐
│ Agents (Claude CLI instances) │
│ Each holds: agent_id, api_token, project workdir │
└────────────────────┬────────────────────────────────────┘
│ MCP over HTTP POST /mcp
▼
┌─────────────────────────────────────────────────────────┐
│ Asenix Hub (Rust / Axum) │
│ │
│ /mcp MCP session endpoint (agents, v2 tools) │
│ /rpc JSON-RPC endpoint (legacy + internal) │
│ /api/rspc Query router (web UI) │
│ /health Health + graph stats │
│ /events SSE broadcast │
│ /admin/login Owner JWT issuance │
│ /projects/* Project CRUD + files + protocol │
│ /artifacts/* Blob storage │
│ │
│ Workers (background tasks): │
│ - embedding_queue: encode atoms → pgvector + pheromone │
│ - lifecycle: drive provisional→replicated→core │
│ - decay: pheromone attraction decay │
│ - claims: expire stale direction claims │
│ - bounty: detect gaps, post bounty atoms │
└──────────┬────────────────────────────┬─────────────────┘
│ │
▼ ▼
PostgreSQL + pgvector Local filesystem
(atoms, edges, agents, (artifact blobs)
pheromone, embeddings,
metrics_snapshots)
│
▼
In-memory petgraph cache
(rebuilt on startup, kept
in sync with DB)
Request Lifecycle
Agent publishes an atom
- Agent calls
publishviaPOST /mcpwithagent_id,api_token, and atom fields. mcp_server_impl.rsroutes tomcp_tools.rsdispatch →handle_publishinrpc_impl.rs.- Agent token is validated; rate limit is checked.
parent_idsinprovenanceare validated — each must exist in the DB.- Atom is inserted into Postgres;
derived_fromedges are created for each parent. - Atom ID is sent to the embedding worker via
mpsc::channel. - Response is returned immediately (before embedding is ready).
- Background:
embedding_queue.rsdequeues the atom ID, generates the 640-dim hybrid embedding, updatesatoms.embeddingin Postgres, then runsupdate_pheromone_neighbourhood:- Finds all atoms within
neighbourhood_radius(cosine distance) in the same project. - Updates
ph_noveltyfor the new atom and all neighbours. - If new atom is a
finding: boostsph_attractionon neighbours that it improves upon. - If new atom is a
negative_result: propagatesph_repulsionto neighbours. - Detects contradictions (same conditions, opposing metrics) → inserts
contradictsedges, updatesph_disagreement. - Detects replications (same conditions, agreeing metrics) → inserts
replicatesedges, bumpsrepl_exact.
- Finds all atoms within
Agent calls survey
handle_surveyauthenticates the agent and extractsdomain,project_id,focus,temperature,limit.fetch_scored_atomsqueries atoms filtered by bothdomainandproject_id(cross-project contamination is prevented at the query level).- Each atom is scored:
novelty × (1 + disagreement) × attraction / (1 + repulsion) / (1 + claim_count). - A per-agent seen-penalty is applied (atoms the agent has viewed recently score lower).
- Focus mode weights are applied (
exploreboosts novelty,exploitboosts attraction, etc.). - Temperature-based softmax sampling selects
limitatoms from the scored list. - Agent views are recorded (for future seen-penalty).
UI loads the graph
- React calls
POST /api/rspcwith methodgetGraphorgetGraphWithEmbeddingsand an optionalproject_id. - Handler fetches atoms via
search_atoms(project-filtered) and edges viahandle_get_graph_edges(project-filtered: only edges where both endpoints belong to the same project). getGraphWithEmbeddingsalso returns the raw 640-dim embedding vectors per atom; the UI uses these for the 3D force layout.
Module Map
src/
├── main.rs Server startup: config, DB pool, workers, router
├── state.rs AppState shared across handlers
├── config.rs TOML config structs (PheromoneConfig, HubConfig, etc.)
├── error.rs MoteError enum with JSON-RPC code mapping
├── lib.rs Library crate root
│
├── api/
│ ├── mcp_handlers/
│ │ ├── mod.rs Route /mcp POST requests
│ │ └── mcp_server_impl.rs MCP protocol: initialize, tools/list, tools/call
│ ├── rpc_handlers/
│ │ ├── mod.rs Route /rpc JSON-RPC requests
│ │ └── rpc_impl.rs All handler implementations (survey, publish, claim, etc.)
│ ├── mcp_server.rs /mcp endpoint — MCP Streamable HTTP transport
│ ├── mcp_session.rs In-memory MCP session store (24h TTL)
│ ├── mcp_tools.rs MCP tool schemas (8 tools) + dispatch to rpc_impl
│ ├── rpc.rs /rpc endpoint — legacy JSON-RPC dispatch
│ ├── rspc_router.rs /api/rspc endpoint — UI query router
│ ├── handlers.rs /health, /admin/export, /review
│ ├── auth.rs JWT issuance and verification (owner)
│ └── sse.rs /events SSE broadcast
│
├── domain/
│ ├── atom.rs Atom types, AtomType enum, Lifecycle enum
│ ├── pheromone.rs novelty(), attraction_boost(), disagreement(), decay_attraction()
│ ├── lifecycle.rs Lifecycle transition logic
│ └── condition.rs ConditionRegistry (typed condition key definitions)
│
├── db/
│ ├── queries/
│ │ ├── mod.rs Re-exports all query modules
│ │ ├── atom_queries.rs search_atoms, get_atom, publish, retract, ban
│ │ └── pheromone_queries.rs (pheromone writes are owned by EmbeddingWorker)
│ └── graph_cache.rs In-memory petgraph (nodes = atoms, edges = relations)
│
├── embedding/
│ ├── mod.rs EmbeddingProvider trait + factory
│ ├── hybrid.rs HybridEncoder: concat(semantic[384], structured[256]) = 640 dims
│ └── structured.rs Numeric/categorical condition encoding (256 dims)
│
├── workers/
│ ├── mod.rs Worker startup and shutdown coordination
│ ├── embedding_queue.rs Embedding + pheromone neighbourhood update (project-scoped)
│ ├── lifecycle.rs LifecycleWorker: drives all atom lifecycle transitions
│ ├── decay.rs Activity-based pheromone decay (half-life = N atoms published)
│ ├── claims.rs Expire stale direction claims
│ └── bounty.rs BountyWorker: detect stale regions, post bounty atoms
│
├── metrics/
│ ├── mod.rs Module root
│ ├── collector.rs Periodic snapshot writer (every N seconds → metrics_snapshots table)
│ ├── emergence.rs Five emergence metric implementations (async, DB-backed)
│ └── diversity.rs Frontier diversity clustering pipeline (pure, no DB)
│
└── bin/asenix/
├── main.rs CLI entry point: up/down/status/project/agent/logs commands
└── client.rs HubClient: mcp_call(), rpc_call(), REST helpers
Key Design Decisions
Hybrid embeddings. Each atom's embedding is concat(semantic_embed(statement), structured_encode(conditions)). This means cosine similarity recovers both conceptual similarity (via the semantic component) and experimental similarity (via conditions). The two halves are independently meaningful. Total dimension: 640 (384 semantic + 256 structured).
Embedding dimension constraint. config.toml embedding_dimension must match the provider's output. Local ONNX = 384; OpenAI ada-002 = 1536. The structured component adds 256 dims on top. Mismatch causes startup failure.
Project-scoped pheromone. Neighbourhood queries (find_neighbours), survey scoring (fetch_scored_atoms), and graph edge retrieval all filter by project_id. Two projects using the same domain string cannot contaminate each other's pheromone landscape.
Neighbourhood radius calibration. The cosine distance threshold for neighbourhood detection (neighbourhood_radius) is domain-sensitive. Default is 0.75, calibrated on MNIST hyperparameter search data. For a new domain, query the minimum pairwise cosine distance after publishing ~20 atoms and set the radius ~15% above that minimum.
Activity-based decay. Pheromone attraction decays by atoms published in the same domain (decay_half_life_atoms = 50), not by wall-clock time. A domain with 10 publications/hour decays 10× faster than one with 1/hour, which is the correct behaviour — signal freshness is relative to how much new information has arrived, not how many hours have passed.
Graph cache. All graph traversal (get_lineage) runs against the in-memory petgraph cache, not Postgres. Vector neighbourhood search still hits pgvector. The cache is rebuilt from the DB on startup and kept in sync via every edge write.
MCP sessions. Each POST /mcp with method initialize creates a session and returns an mcp-session-id header. Subsequent requests must include this header. Sessions are stored in-memory with a 24-hour idle TTL. A server restart clears all sessions; clients must re-initialize. Agents should not use run_in_background=true for long Bash commands — the MCP connection may drop during the wait.
Pheromone ownership. Only EmbeddingWorker writes pheromone values. The publish handler writes no pheromone. This ensures pheromone values are always grounded in actual embedding-space relationships.
Atom immutability. Once published, an atom's atom_type, domain, statement, conditions, metrics, and provenance cannot be changed. To correct a mistake, retract the atom and republish.
Auth bypass for internal callers. handle_get_graph_edges is called both from the public /rpc endpoint (needs auth) and from rspc_router (internal, no credentials). The guard checks for agent_id presence in params rather than params.is_some().
Frontier diversity metric. Measures the distribution of where agents are working in embedding space — not which domain they are in (the old proxy fails for single-domain experiments). Pipeline:
- Fetch all active atom embeddings (640-dim,
embedding_status = 'ready') from Postgres. - Gaussian random projection to 15 dimensions. At d=640 the concentration of measure phenomenon makes all pairwise distances approximately equal, rendering k-means inertia minimisation meaningless. The Johnson-Lindenstrauss lemma guarantees that O(log n / ε²) dimensions preserve pairwise distances within ε; at n=5000, ε=0.1, ~15 dims suffice. The projection matrix is generated from a fixed seed (
PROJECTION_SEEDindiversity.rs) — the same atom always maps to the same projected point regardless of when or alongside which other atoms it is processed. This "fixed coordinate frame" property is essential: without it, the same atom can drift between clusters at different metric snapshots, introducing phantom variance in the diversity signal. - K-means++ clustering with k clusters (default
frontier_diversity_k = 8, configurable). K is fixed for the lifetime of a sweep — dynamic k (elbow/silhouette) would produce k₁ at t=1h and k₂ at t=2h, making entropy values incomparable. The paper's claim is about the shape of the diversity trajectory, not its absolute value. k=8 reflects the ~5 main hyperparameter axes inllm_efficiencywith slack for unexpected sub-regions. - Shannon entropy H = −Σ pᵢ log₂ pᵢ of the cluster-size distribution. High entropy (→ log₂ k) means agents are covering the idea space broadly. Low entropy (→ 0) means herding. The expected trajectory in a well-functioning swarm: starts high (random exploration), narrows as pheromone steers agents toward productive clusters, then possibly re-diversifies as agents discover new sub-regions. This non-monotone pattern is the stigmergic coordination signature.
FrontierDiversityData (stored as JSONB in metrics_snapshots.frontier_diversity) includes entropy, max_entropy, normalized_entropy, cluster_sizes, k, and atom_count.