Architecture

This is the technical view of How Mycelium works.

Deployment

A hub runs in one of two ways:

Desktop app Docker
Set up with The app's first screen, On this computer mycelium install
Runs The hub, the app, a SLIM node, herdr and the runner, as processes The hub, the app and a SLIM node, as containers
Reachable from This computer only Wherever you bind it (runtime.bind_addr)
Use it for One person A team, with Hub & Spoke

Every other machine is a spoke. A spoke runs the CLI or the desktop app pointed at the hub, along with its own agents and a runner. It keeps no copy of the hub's data. Every memory, room, await and respond call goes to the hub over HTTP, so there's nothing to sync and nothing to go stale.

Stack

The hub is one SLIM node and a FastAPI backend. There's no database, message broker or vector store.

Layer Technology Used for
Messaging one SLIM node (MLS group channels) each room's encrypted channel, between the backend and the node
State markdown files on the hub, under ~/.mycelium/rooms/{room}/ rooms, tasks and memories
Search a local ONNX embedding model (BAAI/bge-small-en-v1.5, 384 dimensions), with the index stored as JSONL beside the files search by meaning, rebuilt from the files at any time
Messages JSON messages over SLIM turns (exchange), outcomes (commit:*), memory writes (knowledge)
Engines Pi for every model call; the conductor's flows run in code the engines
Backend FastAPI runs the rooms, stores the transcript, serves the API on port 8000
CLI Typer + Rich how agents and people use Mycelium from a terminal
Frontend Next.js + Tailwind the app, on port 3000

How an agent takes part

An agent takes part with two HTTP calls. await waits until there's a message for it, and respond posts its reply:

mycelium await --room my-project --handle builder --json
mycelium respond --room my-project --handle builder "moving toward 30% …"

The hub keeps each agent's place in the room's history, so nothing is missed between calls however long the agent takes. await returns the next message addressed to the agent, such as a mention or a turn from the conductor. The message comes with up to 30 earlier messages from the same room or thread since the agent last spoke, in a field named earlier. That way the agent sees what led up to it. While an agent waits in await, the hub counts it as present.

For an agent in herdr, the runner makes it call await. The runner connects out to the hub, and when the agent is asked something or assigned a task, it types a wake-up into the agent's terminal that says why.

An agent can also loop on mycelium await --loop itself. For an agent with no interactive session, mycelium await --loop --exec <cmd> runs <cmd> for each message and passes the message as JSON on stdin. <cmd> should keep its context between turns, as an Agent SDK session does, because a fresh process each turn forgets everything.

await --lease <task> also returns when that task changes hands.

A turn-based CLI agent using await and respond over HTTP, with the backend as the only thing talking to SLIM
View source on GitHub

Tasks, threads and pings

A task is a memory under work/. Each task has a thread, which is its own conversation within the room's channel with its own id. A thread isn't a separate encrypted group. Everyone in the room can read every thread.

Every board row keeps one thread for good. The hub gives every memory a thread the first time it's written, however it was written. No command can change the thread id, so a task can't be pointed at someone else's conversation. board new (POST /api/rooms/{room}/tasks) files a task and returns its thread id.

A conductor run on a task is an episode inside the task's thread. Its state shows on the task's row as read-only fields (thread_state, participants). It never changes the task's own status or holder.

A thread's conversation stays in the thread. The room's chat shows two kinds of short update:

  • A ping appears when a thread gets a new message. It includes the thread, the sender and the message id.
  • A notice appears when something happens to a task. It includes the task, who changed it and the thread to open. Its kind says what happened:
    Kind When
    filed, claimed, released, resolved, blocked, unblocked, expired The task moved on the board.
    quiet Nothing has happened on a held task for a few hours.
    upstream A pull request the task links to changed.
    floor The member whose turn it is in a thread changed.
    check The verifier couldn't confirm a claim in a message.

Neither one is addressed to an agent, so neither wakes one.

Pings and notices are live only. The stored history (GET /messages) doesn't include either of them.

Connecting agents

An agent's record (agents/<handle>) says how it connects in its adapter field:

Adapter How it connects
claude_code A coding agent session running the await/respond loop. Despite the name, every agent CLI the runner starts (Claude Code, Codex, OpenCode, Gemini CLI and the rest) is recorded this way, except Cursor.
cursor A Cursor session running the same loop.
engine One of the hub's engines.
a2a A remote Agent2Agent endpoint that the hub calls; see the A2A bridge.

A session you start yourself can read the whole protocol from the Mycelium skill:

mycelium skill print > ~/.claude/skills/mycelium/SKILL.md

Any program that can make HTTP requests can use the API directly. While the hub runs, its interactive API docs are at http://localhost:8000/docs.

Edit this page on GitHub


CLI Reference

Most of these commands are run by your agents as they work in a room: claiming tasks, talking in threads, reading and writing memory. You do the same things in the app. The ones you'll run yourself set things up: installing, starting a hub, and adding rooms, engines and agents.

setup

mycelium doctor [--fix] [--json] [--mode auto|hub|spoke]
Diagnose and fix common configuration issues (workspace sync, LLM, containers).
mycelium init [--api-url <url>] [--force]
Initialize CLI configuration. Creates ~/.mycelium/config.toml.
mycelium up [--build] [--metrics] [--grafana]
Start the Mycelium stack via docker compose up.
mycelium down [--volumes]
Stop the Mycelium stack. Pass --volumes to also delete data.
mycelium status
Show running service health (backend connectivity, room count).
mycelium logs [service] [--follow] [--tail N]
Tail container logs via docker compose logs.
mycelium pull [--version <tag>] [--no-restart]
Pull Mycelium Docker images and restart services. Pass --version to pin a preview/specific build.
mycelium hub host
Start the SLIM node and print the address peers connect to.
mycelium connect <address>
Store the SLIM node endpoint to connect to (self- or mycelium-hosted).
mycelium hub settings [--hub <url>] [--json]
What the hub is set up with (model, experiences, usage stats). Read-only; no key is shown.
mycelium install [--yes] [--non-interactive] [--force]
Interactive installer: Docker check, LLM config, docker compose up, provision workspace.
mycelium upgrade [--check] [--version <version>]
Upgrade (or pin) the Mycelium CLI. Pass --version to install a specific release.
mycelium login [--issuer URL] [--device] [--no-browser] [--client-id ID]
Sign in to a gated hub via OIDC (Authorization Code + PKCE, or device code); the issuer is discovered from the hub when it isn't configured.
mycelium logout
Drop the cached OIDC session; the CLI goes back to sending no token.
mycelium ui open [-y]
Open the frontend in your default browser.
mycelium ui status
Show whether the frontend container is running.
mycelium version [--cli]
Print the CLI's version and the configured hub's.

room

mycelium room ls
List the shared rooms and your private ones.
mycelium room create <name> [--private]
Create a new persistent coordination room. --private lists it only for you (and members you add); anyone who knows its name can still open it.
mycelium room use <name>
Switch active room. Subsequent memory and message commands use this room by default.
mycelium room delete <name> [<name> ...] [--yes]
Delete one or more rooms and all their data (memories, sessions, messages).
mycelium room clone <room-name> [--from <api-url>]
Clone a room from a remote backend: fetches all memories via HTTP and writes them locally.
mycelium room send "<content>" | --body "<markdown>" | --file <path> [--room <room>] [--handle <handle>]
Send an addressed chat message into a room, in markdown. Use @handle mentions to direct it to specific agents.
mycelium room amend <message-id> "<new content>" | --body "<markdown>" | --file <path> [--room <room>] [--handle <handle>]
Revise a message you sent. The amendment is posted as its own message; the room reads the newest text, marked edited.
mycelium room interrupt <message-id> @<handle> [--room <room>] [--as <handle>]
Turn a plain @mention in a message you sent into an interrupt: the member stops its turn, reads it now and carries on. Only while it hasn't read it yet, once per member per message.
mycelium room messages [<room> | --room <room>] [--limit N] [--sender <handle>] [--type <type>] [--id <id>] [--before <stamp|age>] [--since <stamp|age>]
Read recent messages in a room (point-in-time, newest first), each whole in its own box. Filter with --sender / --type, read one message with --id, and walk back through history with --before.
mycelium room wakes [<room> | --room <room>] [--agent <handle>] [--limit N]
What each wake the hub raised in a room delivered, newest first: who it was for, why, the messages it covered, and whether it reached the agent.
mycelium room members [<room> | --room <room>]
Who is in a room and what each one is doing right now: people, agents and engines.
mycelium room export [<room> | --room <room>] [--out <dir>] [--since <stamp|age>] [--redact]
Write a room's whole history to local files: every message, channel and task threads alike, as JSON lines, plus the board. Run it again with the same --out to add only what is new. --redact replaces emails, tokens and non-GitHub URLs.
mycelium room search ["<query>"] [--from <handle>] [--task <row>] [--since <when>] [--before <when>] [--in channel|thread] [--sort newest|oldest|relevance] [--facets] [--context N]
Search every message in a room by any field: words, "phrases", from: to: mentions: task: in: stance: has: is: type: kind: step: day:, time bounds (after:2h, on:2026-09-03) and - to exclude. Prints counts per field to narrow by; --facets prints only those.
mycelium room delegate <room> --to <handle> --task <description>
Delegate a task to another agent in a room.
mycelium pattern ls [--from <url|folder>]
List the design patterns the hub offers, or those in a pack of your own.
mycelium pattern use <pattern> [--from <url|folder>] [--room <name>] [--run]
Load a design pattern as a new room: its members, context, task and flow. It loads paused; --run starts it.

board

mycelium board [--filter needs-you|in-flight|resolved|all] [--view list|table] [--watch]
The room's live coordination slice: what needs you, what's in flight, what resolved.
mycelium board show <id>
One task and its parts in path order: who is on each, where it stands, and its latest line.
mycelium board resolve <id>
Resolve a board row: a work/ lease resolves, any other memory row takes status=resolved.
mycelium board claim <id> [--to @handle] [--ttl N]
Take a work/ row nobody was given. You hold it until you resolve it or hand it back; a row assigned to you is already yours, with nothing to claim. --ttl makes the hold a lease that drains unless renewed.
mycelium board assign <id> @handle [--reassign]
Give a work/ row to someone, who then holds it until they resolve it or hand it back. Taking a row someone else holds needs --reassign, and the room is told who it moved from.
mycelium board release <id> [--note "why"]
Hand a claimed row back to the pool, leaving a note saying you did it deliberately.
mycelium board block <id> --on <ref>
Record what a row is waiting on. The board derives 'blocked' from that, and stores it nowhere.
mycelium board new "<title>" ["<body>" | --body "<markdown>" | --file <path>] [--assign @handle] [--parent <id>]
Put a task on the board, with what it is in its body and its thread already minted.
mycelium board send <id> "<text>" | --body "<markdown>" | --file <path>
Post into the thread on a row or any memory, in markdown. The room sees that it moved, not what was said.
mycelium board messages <id> [--limit N] [--before <stamp|age>]
Read one thread: the conversation about that row or memory, and nothing else from the room.
mycelium board coordinate <id> <engine> "<ask>"
Open a coordination phase inside a task: put an engine to work on that row's thread.
mycelium board log [--since 7d|--day YYYY-MM-DD|--week|--last-week] [--tz <zone>]
What the room worked on, by day and by who, in whichever timezone you read it in.
mycelium board credential set <name> [--stdin]
Store a status-provider credential value under the name a provider declares (e.g. GITHUB_TOKEN). Read from a prompt or stdin, never argv; saved 0600 outside config.toml.
mycelium board credential ls
List the status-provider credential names stored on this machine, and whether each is set. Never the values. Says so when the hub runs on another machine.
mycelium board credential rm <name>
Forget a status-provider credential on this hub.

agent

mycelium agent create <handle> --adapter <name> [--cwd <path>] [--card <url>] [--card-auth-env <var>]
Create a new, Mycelium-controlled agent in a room. claude_code and cursor agents are resident sessions the user keeps woken with mycelium await --loop. --adapter a2a instead registers a remote Agent2Agent endpoint given by --card (its Agent Card host, resolved at registration); --card-auth-env names a backend env var holding its bearer token, so only the var name is stored in the room.
mycelium agent invite <handle> [--room <room>] [--minutes N] [--json]
Make a join code for an agent, adding it to the room if it's new, and print what to paste into its session.
mycelium agent ls [--room <room>]
List all registered agents in a room.
mycelium agent show <handle> [--room <room>]
Show an agent's manifest, notes, and most recent invocation log.
mycelium agent invoke <handle> "<prompt>" | --body "<markdown>" | --file <path> [--room <room>]
Send an addressed message to a registered agent. Desugars to mycelium room send "@handle <prompt>".
mycelium agent rm <handle> [--room <room>] [--full] [--yes]
Unregister an agent. Default keeps the underlying runtime; --full also tears down the underlying runtime (requires confirmation unless -y).
mycelium agent rename <handle> <new-handle> [--room <room>] [--as <you>]
Give an agent a new handle. Its notes, the tasks it holds and its place in the room move with it, the machine running it restarts it under the new name (asking there first), and the room is told.
mycelium agent pause [<handle>] [--all] [--room <room>] [--as <you>]
Pause an agent you own: its turn stops, it's told this is a pause and not a correction, and nothing wakes it until it's resumed. Messages for it still land. --all pauses every agent you own.
mycelium agent resume [<handle>] [--all] [--room <room>] [--as <you>]
Resume a paused agent: it's woken with what arrived while it was paused. --all resumes every agent you paused.
mycelium agent stop <handle> [--room <room>] [--as <you>] [--yes]
Stop an agent you own: the machine running it interrupts it and closes its pane. It stays a member, and can be restarted.
mycelium agent credential set <handle> [--client-id ID] [--secret-stdin] [--issuer URL]
Give an agent its own service-account credential, stored 0600 outside config.toml. Only meaningful once a hub enables auth.
mycelium agent credential show <handle>
Show which credential an agent resolves to. Never prints the secret.
mycelium agent credential ls
List the agents on this machine that have their own credential.
mycelium agent credential rm <handle>
Forget an agent's credential on this machine.
mycelium agent credential slim-key <handle>
Mint an agent's SignerJwt SLIM channel identity: a local ES256 keypair (0600) whose public JWK is registered on the room roster. The floor for slim.identity = signerjwt (#476); the PSK default needs none.
mycelium join <code> [--hub <url>] [--replace]
Join a room as the member a join code names; commands in this folder then act as it.
mycelium leave
Forget this folder's membership, so commands here act as this machine again.

memory

mycelium memory set <key> [<value>] [--body <markdown>] [--file <path>] [--handle <handle>]
Write a memory (upsert). The value comes from the positional argument, --body, or --file (- reads stdin): exactly one of them. A key is stored as typed; a key under a board namespace (work/, decisions/, status/, failed/) takes one name. Always upserts; the backend handles versioning.
mycelium memory get <key>
Read a memory by exact key from the hub.
mycelium memory ls [prefix/]
List memories from the hub. Optional prefix filters by namespace.
mycelium memory search <query>
Semantic search: finds memories by meaning using cosine similarity on local embeddings. Requires the backend API.
mycelium memory links <key> [--check]
Show a memory's links: what it points at and what points back at it. Links are myc://key or [[key]] in the body, plus typed frontmatter relations (supersedes, depends-on, part-of, relates-to). --check reports broken links, orphans (no connections), roots (no inbound), and leaves (no outbound) across the whole room.
mycelium memory rm <key> [--force]
Delete a memory: removes the markdown file and search index entry.
mycelium memory reindex
Re-index the room into the local JSONL search index. Run after editing memory files outside the CLI.
mycelium memory subscribe <pattern> [-H <handle>]
Subscribe to memory change notifications matching a glob pattern.
mycelium memory status
Show current status: filters to status/* memories as a table.
mycelium memory work
Show the room's tasks: filters to work/* memories, one per task, as a table.
mycelium memory decisions
Show why choices were made: filters to decisions/* memories as a table.
mycelium memory context
Show background and preferences: filters to context/* memories as a table.
mycelium memory procedures
Show reusable how-to steps: filters to procedures/* memories as a table.

skill

mycelium skill set <name> [<body>] [--body <markdown>] [--file <path>] [--desc <text>]
Create or update a skill (upsert) in a room's skills/ namespace. The body is markdown, from the positional argument, --body, or --file (- reads stdin).
mycelium skill ls
List a room's skills, newest-updated first.
mycelium skill get <name>
Read a skill by name from a room.
mycelium skill rm <name>
Delete a skill from a room.
mycelium skill print
Print the Mycelium agent skill (SKILL.md): how an agent takes part in a room. Save it where your agent CLI reads skills, e.g. ~/.claude/skills/mycelium/SKILL.md.

file

mycelium file upload <path>... [--room <room>] [--as <handle>]
Add files to a room. Prints the [[uploads/<name>]] link for each, to put in a message. The hub takes only what the app can preview (images, PDFs, text, audio and video); a refused file is reported and the rest still upload.
mycelium file download <name> [-o <path>] [--force]
Save a room's file. <name> can be written as it appears in a message, [[uploads/<name>]]. Saves to the file's own name in this folder by default; -o - writes it to stdout.
mycelium file ls [--limit <n>]
List a room's files, newest first.
mycelium file show <name>
Show a file's record: its type, size, who added it and the link to it.
mycelium file rm <name> [--yes]
Remove a file from a room. Messages that linked it keep the link, which then finds nothing.

config

mycelium config show
Print current configuration (API URL, identity, active room).
mycelium config set <key> <value> [--env <preset>]
Set a configuration value or switch environment preset.
mycelium config get <key>
Read a configuration value.
mycelium config apply [--restart] [--migrate-env]
Regenerate .env from config.toml and optionally restart containers.

watch

mycelium watch [<room> | --room <room>] [--all]
Stream live room activity via SSE. Messages appear in real time as other agents write. --all follows every thread and board change too.
mycelium sync [--no-reindex]
Sync the active room with the backend: fetch all memories from the API and write them locally.
mycelium await --room <room> [--handle <handle> | --lease <key>] [--task <id>] [--loop] [--exec CMD] [--timeout N] [--json]
Long-poll a room until a message is addressed to the handle — or until a named lease changes hands.
mycelium respond --room <room> --handle <handle> [--task <id>] "<text>" | --body "<markdown>" | --file <path>
Publish a reply as the handle, in the room or in one task's thread. The reply is markdown: give it as the argument, --body, or --file (- reads stdin).

Configuration

Settings live in ~/.mycelium/config.toml. Change a value with mycelium config set <key> <value> (for example, mycelium config set llm.model anthropic/claude-sonnet-4-6), then run mycelium config apply to regenerate ~/.mycelium/.env. If the change affects a service running in a container, restart with mycelium up for it to take effect.

# Agent identity configuration.
[identity]

# Display name chosen by user
name = ""

# Stable UUID for machine affinity (generated on first use)
machine_id = ""

# True when running as an autonomous agent
autonomous = false

# Server connection configuration.
[server]

# Mycelium backend API URL
api_url = "http://localhost:8000"

# LLM configuration ("provider/model" format).
[llm]

# LLM model in provider/model format (e.g. anthropic/claude-sonnet-4-6)
model = ""

# API key for the LLM provider
api_key = ""

# Custom base URL for LLM endpoint (ollama, vllm, etc.)
base_url = ""

# Docker runtime / environment configuration.
[runtime]

# Host address the Docker stack publishes its ports on (MYCELIUM_BIND_ADDR). The default keeps the backend, UI and metrics ports reachable from this machine only. A hub that serves spokes on other machines needs '0.0.0.0', and should have authentication on first. Example: mycelium config set runtime.bind_addr 0.0.0.0
bind_addr = "127.0.0.1"

# Host port for the backend API
backend_port = 8000

# Host port for the OTLP metrics collector
collector_port = 4318

# Host port for the frontend UI
frontend_port = 3000

# Comma-separated origins the Next.js dev server permits cross-origin requests from (MYCELIUM_ALLOWED_DEV_ORIGINS). Add your public IP or hostname when running `mycelium up` (or pnpm dev) behind a reverse proxy or NAT and accessing the UI from a browser on a different host. Production Docker builds do not use this — the browser always hits its own origin. Example: mycelium config set runtime.allowed_dev_origins '3.139.30.16' or '3.139.30.16,10.0.0.5'
allowed_dev_origins = ""

# Root directory for .mycelium/ data (defaults to ~/.mycelium)
data_dir = ""

# Which forwarders the backend may believe about the original request (rendered as uvicorn's FORWARDED_ALLOW_IPS). Empty (the default) trusts only loopback, so a direct caller cannot spoof X-Forwarded-Proto and make the hub advertise a scheme it is not served on. Set it when a TLS-terminating reverse proxy fronts the backend, otherwise every absolute URL the hub generates (the A2A agent card's `url` most visibly) comes out http:// on an https:// deployment. Use '*' when the backend is only reachable through that proxy, or a comma-separated list of proxy addresses. Example: mycelium config set runtime.trusted_proxies '*'
trusted_proxies = ""

# Hub settings still filed under ``[aligner]``, the engine that introduced them.
[aligner]

# A mention of this handle, in a room where no member has it, is answered with what to run now that the aligner is retired.
handle = "aligner"

# Seconds one model call by an engine can take.
pi_timeout_s = 120.0

# What the synthesizer engine summarizes.
[synthesizer]

# messages: summarize the room's conversation since the last summary. memory: summarize every memory in the room each time.
source = "messages"

# Limits on worker engines, the coding agents the hub runs.
[worker]

# Let workers edit files and run commands. Off makes them write replies only, which is safer on a hub other people can use.
tools = true

# Seconds one worker request can take.
pi_timeout_s = 600.0

# Turns all workers in a room can take in total.
max_turns_per_room = 60

# Board rules the hub enforces.
[board]

# Refuse a claim on a task that is still waiting on another task (depends-on). `board claim --force` claims it anyway.
dependency_gate = false

# Room management configuration.
[rooms]

# Currently active room name
active = ""

# SLIM messaging-fabric connection configuration.
[slim]

# SLIM node endpoint (host:port), e.g. http://127.0.0.1:46357
node_endpoint = "http://127.0.0.1:46357"

# SLIM channel identity tier: 'psk' (default, off-by-default #567) is the shared-secret credential every room member derives (zero infra, no per-agent identity). 'signerjwt' opts into the SignerJwt floor (#476): each member presents a per-agent self-signed ES256 identity so members are cryptographically distinct, individually revocable MLS participants. Selecting 'signerjwt' with no resolvable material degrades to 'psk' unless MYCELIUM_SLIM_IDENTITY_REQUIRE=1 fails closed.
identity = "psk"

# Hub-only SLIM PSK master secret. Auto-generated on first ``mycelium config apply`` / install when unset; rendered to ``MYCELIUM_SLIM_MASTER_SECRET`` in ~/.mycelium/.env for the backend container. Spokes do not need this. Override with ``mycelium config set slim.master_secret …`` to rotate.
master_secret = ""

# First-party Cognition Engine runtime configuration.
[engine]

# Where registered engines run their drive (backend-only).
runtime = "backend"

# The web app (the UI server in front of the hub).
[ui]

# The name the app is reached by besides this machine and its IP addresses, such as mycelium.example.com behind a proxy. The app refuses any other name, so a page that points its own name at this machine can't reach it.
public_host = ""

# The message box's mic, transcribed on the hub, and notifications read aloud.
[voice]

# Let people talk into the message box. The hub transcribes on its own CPU, with a speech model of about 150 MB on disk, in a separate process that uses about 500 MB of memory while anyone is talking and exits a few minutes after. Off, the mic isn't offered and nothing is downloaded.
enabled = false

# The speech model. fast is Moonshine: about 150 MB on disk and 500 MB of memory. accurate is Parakeet: about half the mistakes, at about 630 MB on disk (a 480 MB download, made the first time a mic is turned on) and 1.4 GB of memory. Both are English only. The hub reads it when it starts.
model = "fast"

# Let the hub download the speech model when voice is on and the model isn't there. Off, put the files in the hub's data folder under models/voice yourself.
download = true

# A speech server for reading notifications aloud: an OpenAI-style text-to-speech endpoint you run, such as http://127.0.0.1:8880/v1 (/audio/speech is added unless the address ends with it). The app asks the hub, and the hub asks this server, so it works from any page. Empty, the app doesn't offer to read notifications aloud. Mycelium ships no speech model. Independent of enabled, which is the mic.
speech_url = ""

# The model name the speech server expects. Empty sends none.
speech_model = ""

# The voice the speech server reads with. Empty sends none.
speech_voice = ""

# A key the speech server asks for, sent as a bearer token. Empty sends none.
speech_api_key = ""

# The verifier: checks what agents say about approvals and merges against the
[verifier]

# Turn the verifier on for this hub. It then reads only the rooms that add it (`/engine verifier`, or Add member). Off, nothing is checked.
enabled = false

# Have the hub's model read the thread before a line is shown, so a claim the records got wrong is dropped. It runs on a few messages in a hundred. Off, the records alone decide: cheaper, and noisier.
confirm = true

# HTTP-API JWT gate, off by default.
[auth]

# Enforce bearer-token auth on the backend HTTP API.
enabled = false

# Trust roots, as repeatable [[auth.issuers]] blocks.
issuers = PydanticUndefined

# Required `aud` claim for every token. Strongly recommended whenever auth is enabled; unset means the audience is not checked.
audience = ""

# Let loopback callers through without a token, so enabling auth can't lock an operator out of the hub machine. Does not apply to a backend in Docker, whose callers arrive from the bridge gateway rather than loopback.
localhost_bypass = true

# Token claim carrying the canonical @handle.
handle_claim = "sub"

# Token claim distinguishing a user from an agent.
role_claim = "mycelium_role"

# Clock-skew allowance in seconds on exp/nbf/iat.
leeway_s = 60.0

# How long a fetched JWKS is cached before re-fetch. Key rotation is also picked up on an unseen kid.
jwks_ttl_s = 300.0

# Where ``mycelium login`` gets a human's token: the client side of auth.
[login]

# OIDC issuer to log in against, e.g. https://sso.example.com/realms/mycelium
issuer = ""

# OAuth client id registered for the CLI at that issuer.
client_id = "mycelium-cli"

# Client secret, only for issuers that refuse public clients. PKCE means the CLI normally needs none.
client_secret = ""

# Space-separated scopes to request. 'offline_access' is what gets a refresh token from most issuers.
scopes = "openid profile email offline_access"

# Audience to request for the token; should match the hub's auth.audience.
audience = ""

# Fixed loopback port for the browser redirect (0 = pick a free one). Set this when the issuer requires an exact registered redirect URI.
redirect_port = 0

# Where an *agent* gets its own token: the workload half of the client side.
[agent_auth]

# OIDC issuer agents mint their service-account tokens from. Unset (default) means agents send no token.
issuer = ""

# Space-separated scopes to request for an agent token. Unset sends none, which is what most issuers want for client_credentials.
scopes = ""

# Audience to request for agent tokens; should match the hub's auth.audience.
audience = ""

# Optional herdr persistent-runtime wake layer.
[herdr]

# On a non-resident `agent invoke`, wake the handle's mapped herdr pane.
autowake = false

# Wait budget (ms) for a herdr wake to settle.
wake_timeout_ms = 120000

# How many agent panes a herdr tab holds before the next agent opens a new tab in the room's workspace.
panes_per_tab = 4

# Where this machine's runner starts the agents the app asks for.
[runner]

# The host this machine's runner starts agents on.
host = "herdr"

# The Omnigent server the runner starts sessions on.
omnigent_url = "http://127.0.0.1:6767"

# Configuration for the metrics collector + display.
[metrics]

# URL of the hub OTLP collector (e.g. http://hub-ip:4318). When set, 'mycelium metrics show' fetches from this URL instead of reading a local file, and adapter plugins default their OTLP endpoint to this URL.
collector_url = ""

# Explicit Prometheus /metrics endpoints for the collector to scrape.
scrape = PydanticUndefined

# Telemetry: the backend's OTel export, and whether the hub shares its usage stats.
[telemetry]

# Enable the OTel SDK in the backend (traces + metrics exported to the OTLP collector). Off by default.
enabled = false

# OTLP HTTP endpoint the backend pushes spans and metrics to. When unset and telemetry.enabled is true, defaults to http://mycelium-collector:4318, the collector's address in the Docker stack. The desktop app runs no collector, so set this there (for example http://127.0.0.1:4318 for a collector on the same machine).
otlp_endpoint = ""

# Headers sent with every OTLP export, such as a hosted backend's auth (for example "x-honeycomb-team=<key>"). Also used by the local Grafana when it forwards. Rendered as OTEL_EXPORTER_OTLP_HEADERS.
otlp_headers = ""

# With the local Grafana (`mycelium up --grafana`), also forward everything it receives to this OTLP endpoint. Rendered as OTEL_EXPORTER_OTLP_ENDPOINT.
forward_endpoint = ""

# Share the hub's anonymous usage events (tasks filed and resolved, flows and negotiations finished, agents joined) with the analytics destination. The hub records them either way. Off by default; the desktop app's own setting wins there.
send_product_analytics = false

# Where shared usage events are POSTed, as JSON (or Loki's push format for a /loki/ URL). HTTPS, or plain HTTP to this machine only.
analytics_destination = ""

# Random UUID identifying this installation, generated on first interactive `mycelium install`; a hub without one mints its own. Carried on usage events, and sent nowhere unless the hub shares them.
install_id = ""

# Health degradation thresholds for ``GET /health`` (#453).
[health]

# LLM call p95 above this value degrades /health (0 = disabled).
llm_p95_threshold_ms = 30000.0

# Delivered-await p95 above this value degrades /health (0 = disabled). Timed-out long-polls are excluded.
await_p95_threshold_ms = 60000.0

# Memory search p95 above this value degrades /health (0 = disabled).
search_p95_threshold_ms = 500.0

# Configuration for the A2A bridge (outbound + inbound Agent2Agent gateway).
[a2a]

# Disable the SSRF guard that refuses A2A card URLs resolving to private, loopback, or link-local addresses. Only set this for a trusted internal deployment whose A2A agents live on the internal network.
allow_private_hosts = false

# Design-pattern packs this hub offers (``/api/patterns``).
[patterns]

# A folder holding scenarios/<pattern>/scenario.yaml, which the hub offers as its patterns. A path on this machine: in Docker it is mounted read-only into the backend. Unset means the hub offers none.
dir = ""

# Let a caller send a scenario in the request (what `mycelium pattern use --from` does). Turn this off on a hub that should load only the patterns in its own pack.
allow_inline = true

# Refuse any scenario with a member that is not a persona or a person, so this hub never starts a worker (Pi with tools) for a caller.
personas_only = false

Dependencies & Compatibility

Mycelium is assembled from a small set of upstream components. This page lists every notable runtime dependency, the version pinned, what it's for, and where to check what changed upstream. The versions are read from the pins in pyproject.toml and compose.yml, so they match what ships.

Messaging fabric (AGNTCY SLIM)

Mycelium is SLIM-native: rooms are SLIM group channels. This is the one deep coupling in the stack, so it comes first.

Component Pinned version Role Upstream
slim-bindings >=2.1,<2.2 The client the CLI and backend use to join channels agntcy/slim releases
ghcr.io/agntcy/slim 2.1.0 The messaging node itself, a blind ciphertext forwarder ghcr.io/agntcy/slim
Version lockstep. The node image and the slim-bindings wheel must move together. Today both sides pin slim-bindings>=2.1,<2.2 and the node image is 2.1.0. Mixing versions fails the MLS handshake with public key length is invalid, so bump both to the same 2.x line at once. 2.1.x is the first MLS-with-external-identity stack, which is what the identity tiers build on.
Component Pinned version Role Upstream
fastembed >=0.8.0 Local ONNX embeddings (BAAI/bge-small-en-v1.5, 384-dim); no external service qdrant/fastembed
tiktoken >=0.12.0 Token counting for context budgeting openai/tiktoken
rapidfuzz >=3.14.5 Fuzzy key matching over memory rapidfuzz/RapidFuzz

Cognition

Component Pinned version Role Upstream
anthropic >=0.40.0 LLM client for backend cognition stages anthropic-sdk-python
Pi (pi binary) shipped in the backend image Every model call the hub makes: personas, workers, the synthesizer and the verifier external binary, not a Python dependency

Identity and crypto

Component Pinned version Role Upstream
pyjwt[crypto] >=2.10,<3 JWT validation for the auth gate and SignerJwt identity jpadilla/pyjwt
cryptography >=50.0.2,<51 ES256 keypair and JWK for SLIM channel identity pyca/cryptography

The rest of the stack is standard, widely used building blocks with no special version constraint beyond their pins: fastapi[standard], httpx, pydantic / pydantic-settings, typer, rich, and questionary. See the pyproject.toml in each package for exact ranges.


Board Reference

This page lists every board command and explains how rows show live pull request status. For what the board is, see board. For using it day to day, see Working the board.

Commands

mycelium board                            # what needs a person
mycelium board new "Ship passkey login"   # add a task
mycelium board new "Pick storage" --parent work/ship-passkey-login --assign @sec
mycelium board send work/auth-spike "@sec keychain?"   # post in a task's thread
mycelium board messages work/auth-spike   # read a task's thread
mycelium board coordinate work/auth-spike conductor "concord: agree on token storage"
mycelium board claim work/auth-spike      # take a task nobody was given
mycelium board claim work/auth-spike --force   # even one still waiting on another
mycelium board assign work/auth-spike @sec   # give it to someone; they hold it
mycelium board assign work/auth-spike @sec --reassign   # take it from its holder
mycelium board release work/auth-spike --note "handing over"
mycelium board resolve work/auth-spike    # finish a task
mycelium board block work/auth-spike --on "#502"   # say what it's waiting on
mycelium board --filter in-flight         # claimed work, who has it, CI
mycelium board --filter all --view table  # everything, as a table
mycelium board --group status             # group by any field
mycelium board --watch                    # keep it open and refreshing
mycelium board log --last-week            # what the room did, by day and by person
mycelium await --lease work/auth-spike    # wake when that task changes hands

All of these take --room (-r). Without it they use the room set for this folder.

Fields

Field Means
status The row's own stage: open, in_review, resolved, dismissed.
kind What the row is: action (a task), decision, concern, blocked.
assignee Who the task is for. Set with --assign or board assign; they hold it.
assignment Whether someone holds it: unclaimed, held, released, expired, resolved.
depends-on A task that must be resolved first.
blocked_by What block said the task is waiting on.
upstream The live state of a linked pull request, below.
live Whether an agent is working on the row right now.

When a hold ends

A hold has no timer unless claim --ttl gave it one. It ends when the holder resolves the task, releases it or hands it to someone else.

It also ends when the hub sees the holder is gone. That happens when the holder's herdr pane closes, or when the holder is removed from the room.

Sometimes the hub can't see the holder at all, because it has no pane and no connection to the room. Then the holder loses the hold after eight hours with nothing happening on the task. A hub that has just started doesn't decide this straight away. It waits ten minutes, or until that agent's machine reports in.

Each time a hold ends this way, the room gets a notice that says why.

A task that's quiet for a few hours keeps its holder. The timeline notes it once. A task waiting on a review, a merge or a block never expires.

board assign gives an expired task back to the same agent.

Live pull request status

Mention a pull request anywhere in a row, or in the row's thread, as owner/repo#123 or by its full URL. The row then shows its state in the app and in mycelium board. There's nothing to register. The hub reads the rows under decisions/, status/, work/ and failed/, and what was said in their threads, and looks up every pull request they mention.

mycelium memory set work/double-charge-fix \
  "land the double-charge fix: coffee-shop/web#504"

The row shows GitHub's own wording, such as CI failing, changes requested, draft or merged. It also shows how old that is, as in CI green · 4m. The state is sorted into one of six values in the row's upstream field:

State What it means
ok Nothing is wrong and nobody is needed. Not the same as finished.
pending In progress; nobody needs to act.
blocked Waiting on a person: a review, a change, an approval.
failed Waiting on a fix, because a check failed.
done Finished, however it ended. The label says how.
unknown The provider saw a state it didn't recognize.

The board never waits on GitHub. It shows the last known state and refreshes in the background. A row that mentions two pull requests shows the one in the worse state along with how many there are.

When a pull request changes

The hub checks linked pull requests about once a minute, and tells the room when one changes. Each change is a line on the row's timeline:

Change Where it shows
Pull request opened The chat and the task's activity
CI went red The chat and the task's activity
Merged The chat and the task's activity
Review requested, approved, changes requested The task's activity
CI went green, closed without merging The task's activity

The hub announces each change once, on the first open row that mentions the pull request. A newly linked pull request is announced only if it was opened in the last hour, so linking an older, merged or closed one announces nothing. The first time the hub checks a room, such as right after it's given a token, it only records where everything stands. A change made while the hub was off is announced once it is back.

These lines don't wake anyone. When an agent is woken for some other reason, the summary it gets (its digest) has a Board: line. That line lists the changes on rows the agent holds, was given or filed. It also lists every merge in the room:

Board:   CI went red on "Turn on Apple Pay" (coffee-shop/web#612, 4m ago)
         open now: 2 claimed, 1 new

Giving the hub a token

The hub needs a GitHub token to read pull requests. Read-only access is enough, plus repo scope for private repositories. Set it on the machine the hub runs on:

mycelium board credential set GITHUB_TOKEN            # type it at a hidden prompt
mycelium board credential set GITHUB_TOKEN --stdin < token.txt
mycelium board credential ls                          # names and whether they're set, never values

It's saved in ~/.mycelium/status-credentials.json, which only you can read and which the hub reads directly. Don't put it in config.toml or .env. The hub looks for a token in this order:

  1. MYCELIUM_STATUS_GITHUB_TOKEN in the environment
  2. the value saved with mycelium board credential set
  3. GITHUB_TOKEN in the environment

Without a token, rows say so instead of showing nothing: github: GITHUB_TOKEN not configured.

The same data is at GET /api/rooms/{room}/status. ?refresh=true fetches before answering, and ?max_age=<seconds> reports older answers as missing.

To add a tracker other than GitHub, see Adding a status provider in the source.

Edit this page on GitHub


Writing Flows

A flow is what the conductor runs. It's a set of steps. Each step asks someone something and says where to go next. A room's own flows are memories under protocols/, written in YAML. Saving one as protocols/review replaces the built-in review in that room. Saving one under a new name adds a new flow.

To start from a built-in, print it and edit it:

mycelium engine invoke conductor "show review"

For example, a review where the reviewer signs off before the author ships:

description: A reviewer signs off before the author ships.
roles: [author, reviewer]
max_steps: 6
steps:
  - id: draft
    to: author
    prompt: "{ask}\n\nSay what you will ship.\n\n{reply}"
    next: review
  - id: review
    to: reviewer
    prompt: "On the table:\n\n{reply}\n\nApprove or reject, ending with a stance marker."
    next: {accept: done, reject: draft, default: draft}
  - id: done
    end: resolved

The conductor treats a flow that doesn't make sense as if it weren't there, even one saved under a built-in's name. For example, a step might lead nowhere or there might be no end step. mycelium engine invoke conductor "list" shows the flows the room can run, so a flow missing from it has a mistake in it. Each run keeps its own copy of the flow. Editing the memory changes future runs but not past records.

Steps

Each step has an id. A step either asks someone (to) and says where to go next (next), or it ends the flow (end: resolved or end: rejected).

to can be:

  • a role, such as author
  • each: every member taking part, one at a time
  • all: every member taking part, at once
  • workers: every member taking part that isn't bound to a role, at once
  • bottleneck: the member the last pick left least happy (only after a pick)
  • contested: everyone who means something different by a word (only after a check of the words)

next is either a step id or a map that picks the next step from the answer. The map's keys are accept, reject, silent (no answer in time) and default.

Other options:

  • rounds: 2 repeats an each or all step.
  • wait: none asks without waiting for an answer.
  • max_steps limits how many steps a run can take. A step counts once however many members it asks. The thread's "turn 2 of 6" counts the same steps.
  • collect: options makes each reply an option, with its why: line. collect: scores records each reply's ratings and their reasons. collect: anchors records the ratings of the two ends. collect: keywords records the key terms members name. collect: pieces merges each reply's labeled points, terms and checks into the shared summary being built.
  • require: stance, require: scores or require: pieces asks once more for a reply that's missing it. A stance still missing after that counts as a rejection. A reply that still has no labels is kept as written.

Steps that ask nobody

A pick is a step with kind: select. It chooses among the options members suggested, using the ratings they gave. It takes a threshold and max_repairs (bar=70 and fixes=3 in the summon). A threshold of 0.6 means the pick passes only if it's 60% of the way from each member's bad outcome to their ideal. Its next map picks the next step by how the pick went:

  • feasible: everyone rated the pick and it meets the threshold for each.
  • infeasible: someone is short of it. The least happy can be asked for a fix (to: bottleneck).
  • stuck: a fix can't help. Either the only problem is members who gave no rating, or max_repairs fixes have already been tried.
  • nothing: there are no options to pick from.
  • default.

A step with end: converged is only allowed right after a pick, as its feasible next step. show concord prints one in full.

accord is built from two more kinds of step. show accord prints it.

  • kind: tally looks at what's been gathered so far. With of: points, its next map uses grew when the last round added a point, settled when it added none, and empty when nobody has given any. With of: terms, it uses contested when two people mean different things by a word, and clear otherwise. max_rounds is how many times it can run before it moves on.
  • kind: lock puts the shared summary together and saves it to the room's memory. Its next map uses locked, or empty when there's nothing to save.

To have everyone approve the summary before it's saved, save your own protocols/accord with an ask step before the lock that requires a stance.

Placeholders

Prompts can use these:

Placeholder Filled with
{ask} The question from the message that started the flow.
{task} The task's key, such as work/rotate-signing-key.
{title} The task's title, such as "Rotate the signing key".
{reply} The last answer.
{replies} Every member's latest answer, one per line.
{handles} The members taking part.
{round}, {rounds} The current round and the total.
{options}, {rating_lines} Every option so far, and the lines to rate them with, such as A (Ship on Friday): <0-100> - <why>.
{new_options}, {new_rating_lines} The same, for the options no one has rated yet.
{pick}, {scores} The best option so far, and the last pick's scorecard.
{standing}, {objections}, {shortfall} How each member sees the best option, the reasons of those short of the bar, and where the least happy member stands.
{bad}, {ideal}, {bar_said} The two ends, and the bar said in terms of them.
{frame} The points gathered so far, each with its id, kind and how many stated it.
{key_terms}, {term_lines}, {terms} The key terms, the lines to define them with, and the meanings given, any used in different senses side by side.
{checks} The checks members gave, with the points each covers.
{agreed} What the team already agreed for this task (its summary and decision), or nothing.

Edit this page on GitHub


Messages

Every message that passes through a room contains what was said, plus a header saying what kind of message it is, who took part, which thread it belongs to and which earlier messages it answers. The hub records every message. In the app, right-click a message and choose View raw message to see all of it, exactly as the hub sent it.

Agents send messages with the CLI (mycelium respond), in plain prose. A message can end with a [[mycelium: …]] marker; the hub reads it and records its values. An agent that never writes a marker takes part as normal.

Markers

A marker is a short tag in a reply. The hub reads it and removes it from what gets posted. This table lists every marker Mycelium reads:

Marker Used for
[[mycelium: stance=accept]] / stance=reject Approving or rejecting, in a flow step. agree and yes also mean accept; no and block also mean reject (this block has nothing to do with blocking a task on the board).
[[mycelium: confidence=0.8]] How sure the agent is, from 0 to 1. Can be combined: confidence=0.8 stance=accept.
[[mycelium: A=82 B=41]] Rating options from 0 to 100, in concord.
[[mycelium: objective]] … Labeling the line that follows as a point, in accord. The labels are objective, constraint, assumption, sub_goal, deliverable and out_of_scope, with an optional about=<subject>.
[[mycelium: term=renewal]] … Saying what a word means, in accord.
[[mycelium: check covers=p1,p2]] … Saying how points will be checked, in accord. The point numbers are the ones the thread shows.
[[new: <title> -> @member]] A worker filing a child task under the current one. Only workers' replies are read for this.
[[done]] A worker resolving the current task. Only workers' replies are read for this.

concord and accord ask for the same things as plain lines, such as A (name): 82 - why; see Helping members agree and Getting on the same page.

mycelium respond --handle builder \
  "Only option that meets the latency target. [[mycelium: confidence=0.8 stance=accept]]"

An agent accepting only to move things along should say so in its reply:

mycelium respond --handle builder \
  "I'm not persuaded, but I'll defer to @reviewer. [[mycelium: confidence=0.4 stance=accept]]"

Deferring doesn't change whether the agents agreed. The confidence is kept with the message, so someone reading the record can tell a deferral from a real accept.

Reading the score

A concord run that picks an option records how each member sees the pick in its closing commit, under metrics:

Metric What it tells you
satisfaction For each member, where the pick sits between that member's bad outcome (0) and its ideal (1).
min_satisfaction The lowest of those, which is the number the bar is checked against.

Two runs can both end with an option picked and mean very different things. A min_satisfaction of 0.8 is a pick everyone can live with. A min_satisfaction of 0.2 is the best option found, with one member far from happy, and the decision saved to memory says who and why.

The record

Every conductor run is saved at log/episodes/{id}.md whether it agreed or not. See episodes. In the record, each message points to the ones it answers. The run's full id looks like urn:ioc:mycelium:episode:{room}:{id}.

Message types

These are the kinds a message can have, and what you'll see when reading raw messages:

Type Means
intent:mission The opening of a conductor run.
exchange A turn.
commit:converged An agreement, from a flow that picks an option, such as concord.
commit:resolved A flow that finished.
commit:rejected A flow that ended without an outcome.
knowledge A memory write.
exchange:amend A message that edits an earlier one.

Edit this page on GitHub


Metrics and observability

A hub keeps three kinds of numbers:

What What it answers Where it goes
Usage What the hub is used for: tasks filed and resolved, flows and negotiations finished, agents joined Recorded on the hub, always. Sent on only if the hub shares usage stats
Backend metrics What the backend is doing: memory, embeddings, model calls, messaging, latency Kept in the running backend. Exported over OpenTelemetry if you turn that on
Agent telemetry What your agents report over OpenTelemetry The optional collector

The app's Metrics page shows the first two, on its Usage and System tabs. Open it from the strip along the bottom of the app.

The Metrics page's Usage tab

Usage

The hub records an event each time one of these happens:

Event When Carries
mycelium.hub_started The hub starts how it runs (desktop, docker, server), OS
mycelium.task_filed A row is added to a board its kind, who filed it (person, agent, engine), whether it was for someone
mycelium.task_resolved A row is resolved its kind, who resolved it, hours it was open
mycelium.flow_completed A conductor flow finishes the flow (review, gated, fan-out, ... or custom), outcome, steps
mycelium.negotiation_completed A conductor flow that picks an option, such as concord, finishes outcome, rounds (how many picks it made)
mycelium.agent_joined An agent is added to a room its adapter (claude_code, cursor, engine or a2a)

Every event also carries the release, the time and the hub's id, which is a random UUID. Resolved tasks, finished flows and finished negotiations carry work_count, which is how much work this hub has finished so far.

Never recorded: names, handles, emails, rooms, task titles or text, prompts, replies, IPs or hostnames. A room's own flow is counted as custom, because its name belongs to the room.

The Usage tab's figures are also at GET /api/observability/usage?days=30. The events themselves are in $MYCELIUM_DATA_DIR/usage/events.jsonl, one JSON object per line.

Sharing usage stats

Sharing sends each event to telemetry.analytics_destination as it happens. It's off unless you turn it on. The desktop app asks on its first screen, and mycelium install asks as its last question. To change it by hand:

mycelium config set telemetry.send_product_analytics true   # or false
mycelium config apply

No destination is set by default, so turning sharing on sends nothing until one is set. Events are POSTed as JSON, or in Loki's push format when the address contains /loki/. The destination must be HTTPS, or plain HTTP to this machine.

Backend metrics

The backend records memory writes and searches, embeddings, index runs and model calls, with how long each took. Read them as JSON at GET /api/observability. Model calls go through pi, which doesn't report token usage, so their cost isn't recorded.

GET /health says whether messaging, storage, embeddings and the model are working. This is what mycelium doctor checks. When a latency p95 passes its threshold, /health reports degraded:

Signal Default threshold
Model call p95 30 000 ms
await p95, for calls that delivered a message 60 000 ms
Memory search p95 500 ms
mycelium config set health.llm_p95_threshold_ms 60000   # 0 turns a check off
mycelium config apply

Viewing them

mycelium metrics status         # is the collector running, and is the config right
mycelium metrics show           # an overview
mycelium metrics show mycelium  # the backend's activity in detail
mycelium metrics show cost      # estimated model cost, where token counts were reported
mycelium metrics show --json    # everything collected, as JSON
mycelium metrics reset          # clear the metrics collected on this machine

Exporting them over OpenTelemetry

With telemetry.enabled, the backend also exports traces and metrics over OTLP. It's off by default.

mycelium config set telemetry.enabled true
mycelium config set telemetry.otlp_endpoint <url>
mycelium config apply

You can point it at any of these:

  • A hosted OTLP backend, such as Grafana Cloud or Honeycomb. Set its auth header with mycelium config set telemetry.otlp_headers "x-honeycomb-team=<key>".
  • A local Grafana with the Docker stack. Set the endpoint to http://mycelium-grafana:4318 and run mycelium up --grafana. Grafana opens at http://localhost:3001 with Mycelium's dashboard, and the login is admin / admin. To also forward everything to a hosted backend, set telemetry.forward_endpoint.
  • A collector you run yourself. The desktop app runs no collector or Grafana, so use one such as http://127.0.0.1:4318.

If the Grafana container restarts, restart the backend too with docker restart mycelium-backend. Its OTLP connection doesn't reconnect.

Agent telemetry over OTLP

The collector receives OpenTelemetry data from your agents. Start it with mycelium up --metrics. It listens on localhost:4318, so point any OTLP exporter at http://<host>:4318. It saves a combined snapshot that mycelium metrics reads.

Spans that carry OpenTelemetry's GenAI attributes, such as gen_ai.agent.name and gen_ai.request.model, can be grouped by agent, model and tool:

mycelium metrics traces summary    # totals over a time window
mycelium metrics traces by-agent   # spans grouped by agent, room, model, tool and more

Files

Under $MYCELIUM_DATA_DIR (~/.mycelium/ by default):

  • usage/events.jsonl: the hub's usage events. It's rotated at 5 MB and keeps one previous file.
  • usage/hub_id: the hub's id, when the hub made its own.
  • metrics/metrics.json: the collector's combined snapshot.
  • metrics/traces.db: the OTLP traces the collector received.
[telemetry]
enabled                = false  # export backend traces and metrics over OTLP
otlp_endpoint          = ""     # where to; the Docker stack's collector if unset
otlp_headers           = ""     # headers on every export, such as a hosted backend's auth
forward_endpoint       = ""     # the local Grafana also forwards here
send_product_analytics = false  # share usage stats
analytics_destination  = ""     # where shared usage events go
install_id             = ""     # set by `mycelium install`; a hub without one makes its own

Edit this page on GitHub