Runtime guards and zero-touch YAML auto-instrumentation for AI agents.
Stops duplicate side effects on retry/redispatch, blocks bad tool args and out-of-scope calls, and keeps tool data fresh. Not recovery after. Not tracing or dashboards.
Early but API-stable (v1.18.0): breaking changes only at major versions. More guards planned.
Developers running agents with side-effect tools in production (payments, emails, API writes, long subagent calls) on LangGraph, CrewAI, or a plain Python loop.
Python 3.10+. Framework-agnostic.
These aren't reasoning failures. They're runtime failures. Mycelium sits between your agent loop and your tools (after the LLM returns tool_calls):
Core (mycelium init / mycelium run):
- Duplicate side effects on retry: classify tools (
readvskeyed_mutatevsnon_idempotent_mutate, etc.), hash a durable transition key, resolve duplicates by terminal state — not blind re-execute. Do not redispatch unless the previous transition is proven terminal or safely recoverable. This is a transition envelope (class + lease + terminal + hard-block / reconcile), not only an idempotency key plus a cached result.- Read tools: poll in-flight, reclaim expired leases, soft-block ambiguous
UNKNOWN(safe retry by default) - Mutating tools: hard-block ambiguity; reconcile via
external_operation_refwhen a provider lookup can prove run-or-not (COMPLETED/NOT_EXECUTED/ still blocked) - Operator release (v1.15.0): when a hard-block needs a human, an operator verifies with the provider and records it (
release(verified=...)/mycelium transitions release) —completedreturns the recorded result,not_executedgrants exactly one re-execution. One-shot, fail-closed, audit-stamped; triage viamycelium transitions list --stuck - Worker-death signal (v1.16.0, opt-in): when
reclaim_requires_death_signal: true, EXPIRED entries cannot be reclaimed or released without affirmative death evidence (mark_worker_dead()/mycelium transitions mark-dead, or heartbeat older than the grace window). Prevents reclaiming from a worker that is merely paused. - Provider idempotency-key validity (v1.17.0): when
provider_idempotency_key_ttlis set, a same-key retry that exceeds the window hard-blocks instead of retrying — the provider may have purged its deduplication state. - Atomicity contract (v1.18.0): every terminal-outcome write uses CAS (
try_transition) — already-resolved transitions refuse overwrites. Owner fencing in@ledger/@ledger_syncprevents stale workers from overwriting another worker's outcome. - Unclassified tools: tools without a
transition_bindinghave unknown side-effect semantics.unclassified_policy: strictroutes retries through a conservative binding so failed retries hard-block instead of re-executing (defaultwarnemits a one-timeUserWarning). Side-effecting tools using memory storage get a one-time warning — the duplicate-side-effect guard only holds within the process. - Stale lease (
EXPIRED): strict classes reclaim only when reconcile provesNOT_EXECUTED(fail-closed without a ref) - Lease auto-renew (v1.14.0): while a
@ledger/@ledger_synctool runs, Mycelium extendslease_untilautomatically (default everylease_ttl / 3) so long work does not look dead to a redispatched peer. Setlease_renew_interval: 0to disable; callrenew_lease()for a manual bump or when claiming outside the decorator. - LangGraph Cloud: long tools may be redispatched around ~180s (
BG_JOB_HEARTBEATsweep); Mycelium’s lease/poll/hard-block (with auto-renew) guards that window (langgraph#7417)
- Read tools: poll in-flight, reclaim expired leases, soft-block ambiguous
- Transition envelope fields (priority order):
side_effect_class→spendability→side_effect_boundary→terminal_outcome→external_operation_ref→retry_permission— payment/write needs the heavier set; without it, redispatch is an unsupported second transition, not a retry
Opt-in (configure or call explicitly):
- Stale or broken context: TTL-fresh tool data (
@protect); optional message/history validation before the next LLM turn - Bad tool calls: block invalid inputs and out-of-scope tools before they run (
@bounded/ registry)
Not Langfuse. Use both if you want traces and guards. Full resolution rules: sdk/README.md. Envelope field stack: sdk/README.md.
pip install mycelium-runtime
pip install 'mycelium-runtime[langgraph]' # automatic LangGraph runtime IDs
pip install 'mycelium-runtime[redis]' # multi-worker / cloud ledger
pip install 'mycelium-runtime[postgres]' # Postgres ledger backend
mycelium demo --slow # feature tour, paced for screen recording
mycelium demo # same tour, fast
mycelium demo --redis # optional Cloud-style two-worker Redis proof (#7417)
mycelium init # on-ramp: transition + one ledgered tool → mycelium.yaml
mycelium init --full # reference: all guards (fill TODOs; not the default)
mycelium init --minimal # smaller multi-guard scaffoldmycelium demo --redis runs two OS processes against a real Redis ledger — Worker B redispatches while A is in-flight; B polls and returns A's result. Needs Redis (MYCELIUM_TEST_REDIS_URL or redis://127.0.0.1:6379/15) and pip install 'mycelium-runtime[redis]'.
mycelium init is the real start path (duplicate-tool fix). Use --full when you want every section documented in one file.
# after: mycelium init
integrations:
langgraph:
enabled: true
transition:
agent_id: my-agent
policy_version: "2026.07.1"
# lease_ttl: 3600
# lease_renew_interval: 1200 # default = lease_ttl/3; 0 disables auto-renew
action_ledger:
storage: file
path: ./mycelium-ledger.json
unclassified_policy: strict # warn (default) or strict
tools: [my_side_effect_tool]
tools:
my_side_effect_tool:
callable: my_app.tools:my_side_effect_tool
side_effect_class: non_idempotent_mutateLaunch your existing Python application without adding decorators:
mycelium run --config mycelium.yaml -- python -m my_appmycelium run validates and wraps every configured callable before the
application starts. It preserves the child process's arguments, working
directory, signals, and exit code. The command accepts the current Python
interpreter only.
Explicit instrumentation remains supported when you prefer code-level control:
from mycelium import load_config
config = load_config("mycelium.yaml")
@config.apply
def my_side_effect_tool(...) -> dict:
...Without YAML, use the ledger decorators directly (@ledger / @ledger_sync for tools;
@task_ledger / @task_ledger_sync for coarser task-level idempotency). Same transition
envelope and gates — see sdk/README.md
and task-level idempotency.
Do not combine standalone guard decorators with command mode on the same
function. Fully configured @config.apply wrappers are detected and skipped.
Keep callable modules import-safe: registrations performed inside a target
module while that module is still importing cannot be retroactively replaced.
With the optional LangGraph integration, ToolNode / create_agent injects
ToolRuntime; Mycelium automatically maps its tool_call_id, thread, run, and
node into transition identity. Explicit IDs still override captured values.
Custom tool executors can continue passing tool_call_id manually. Redispatch
resolves the existing transition: read tools poll/soft-block; mutating tools
hard-block or reconcile against the provider when you record
external_operation_ref.
Multi-worker / cloud ledgers: pip install 'mycelium-runtime[redis]' or
'mycelium-runtime[postgres]'. See the handbook.
- Handbook: https://mycelium-labs.github.io/mycelium/
- Full API reference: sdk/README.md
- PyPI: https://pypi.org/project/mycelium-runtime/
Create a GitHub Personal Access Token with contents: write scope on this repo and add it as a repository secret named RELEASE_PAT at Settings → Secrets and variables → Actions. This is required because the tag push uses the PAT (instead of GITHUB_TOKEN) so that publish.yml's on: push: tags: v* trigger fires — GITHUB_TOKEN-pushed tags cannot trigger other workflows.
- Create a feature branch, make changes, open a PR to
main. - CI (pytest + ruff on Python 3.10–3.13) must pass.
- To release, bump the version in
sdk/pyproject.tomland add a## X.Y.Z (date)section toCHANGELOG.mdin the same PR. - Merge the PR. On push to
main, automation:- Reads the version from
sdk/pyproject.toml. - Checks whether tag
v{version}already exists — if it does, exits quietly (doc-only or non-version merges release nothing). - Runs the SDK tests and ruff (Python 3.12) as a safety gate before tagging.
- Creates an annotated tag
v{version}and pushes it (via PAT so the tag-push triggers the publish workflow). - Creates a GitHub Release with notes extracted from the matching
CHANGELOG.mdsection (falls back to auto-generated notes if extraction finds nothing). - The tag push triggers publish.yml (
on: push: tags: v*) which builds and uploads to PyPI via trusted publishing.
- Reads the version from
Manual escape hatch: pushing a v* tag or triggering workflow_dispatch on the publish workflow still works — the existing manual path is unchanged. If the automation fails, publish manually by running git push origin v{version} locally after merging.
MIT. See LICENSE.