A minimal, MCP-native, cloud-native AI agent runtime. One small static Rust
binary runs one agent: hand it an instruction and one LLM endpoint, and it
runs the agentic loop — think, call a tool, observe, repeat — until the task
reaches a terminal status or a new event wakes it. Every tool comes from a
remote MCP server over HTTPS (agentd ships none of its own and never runs
local code), it reacts to the world through MCP resource subscriptions,
speaks A2A to other agents, and can drive agent-authored workflows. It
is built to be a cloud-native unit of work — drop it into a Job, a CronJob,
or a long-lived reactive Deployment.
binary 3.0 MiB static (musl, FROM scratch) · image ~1.2 MiB pull · cold start <1 ms
idle daemon ~2 MiB RSS · 3 direct external deps · HTTPS everywhere · Apache-2.0
- Why agentd
- How it works
- Install
- Quickstart
- Five modes
- Workflows
- Embedding — the engine in your app
- Composition: serving, subagents, A2A
- Security model
- Operating it
- Scaling out
- Build features
- Footprint (measured)
- Documentation map
- Minimalism as the moat. Three direct external dependencies (
serde,serde_json,libc) — no async runtime, no framework, no C toolchain. The HTTP client/server, cron parser, Prometheus text, OTLP export, and inotify watch are all hand-rolled onstd+libc. The result is a 3 MiB static binary that starts in under a millisecond, idles at ~2 MiB, and ships as a single-layerFROM scratchimage with nothing to CVE-scan but agentd itself. - MCP as the universal interface. agentd has no built-in
fs/http/shelltool library and executes nothing locally. Every capability is a remote MCP server you declare with--mcp name=https://…. One protocol in, one protocol out — tools and resources are all MCP, and agentd itself is addressable as an MCP server. - Reactivity via resource subscriptions. Instead of polling, a reactive agentd idles at near-zero CPU and wakes when an MCP resource it subscribed to changes (notify-then-read). An upstream change is the trigger; an agent can even subscribe itself mid-reasoning to schedule its own future wake.
- Two loops, strictly separated. A tiny supervisor owns lifecycle,
triggers, limits, and the kill ladder — and never talks to the LLM. The
reasoning lives in subagent child processes (the same binary, re-exec'd)
the supervisor can always
SIGKILL. A runaway or crashing model is contained by construction; limits are enforced by a process that cannot be prompted. - Composability, three ways. An agentd serves its own MCP
(
--serve-mcp), so one agent is just another tool/resource surface a second agent connects to. It delegates over A2A (--a2a-peer) to remote agents as spec-conformant Tasks. And it nests subagents as an OS process tree with narrowed, per-child context and trust. Agents compose like Unix processes.
triggers: interval · cron · MCP resource change · A2A request
│
┌────────────────────────────────▼─────────────────────────────────┐
│ supervisor (never talks to the LLM) │
│ config → validate → trifecta gate → mode driver → kill ladder │
│ limits: steps · tokens · deadline · depth · cgroup mem/pids │
└────────────┬─────────────────────────────────────┬───────────────┘
│ spawn (re-exec, narrowed payload) │ serve (optional)
┌────────────▼────────────────┐ ┌────────────▼───────────────┐
│ subagent (agentic loop) │ │ self-MCP over HTTP(S) │
│ think → tool → observe … │ │ tools · agent:// resources│
│ or: workflow driver │ │ A2A Tasks · operator ctl │
└──────┬───────────┬──────────┘ └────────────────────────────┘
│ HTTPS │ HTTPS
┌──────▼─────┐ ┌───▼──────────────┐
│ intelligence│ │ MCP servers │
│ (one LLM │ │ --mcp a=https://…│
│ endpoint, │ │ --mcp b=https://…│
│ failover) │ │ tools+resources │
└────────────┘ └──────────────────┘
Every network edge is HTTP(S) — the LLM, the MCP servers, the served self-MCP,
A2A peers, and operator control — with mTLS and/or bearer auth (plaintext
http:// is loopback-only, for dev). agentd links no unix/vsock/stdio
transport and spawns no tool processes.
Release binaries (static musl, amd64 + arm64, with SHA256SUMS):
$ curl -LO https://github.com/agentd-dev/source-code/releases/download/v1.2.0/agentd-v1.2.0-x86_64-unknown-linux-musl.tar.gz
$ tar xzf agentd-v1.2.0-x86_64-unknown-linux-musl.tar.gz && ./agentd --version
agentd 1.2.0Container image (multi-arch, cosign-signed, single layer, ~1.2 MiB pull):
$ docker run --rm ghcr.io/agentd-dev/agentd:1.2.0 --capabilitiesFrom source (Rust stable; no C toolchain needed):
$ cargo build -p agentd-cli --release
$ cargo build -p agentd-cli --release \
--features "serve-https,a2a,events,metrics,cron,otel,cluster,hot-reload,config-watch,workflow" # the shipped set# one-shot: instruction + one LLM endpoint + one MCP server, then exit
$ agentd \
--instruction "Read /data/report.md and write a 3-bullet summary to /data/summary.md" \
--intelligence https://gw.example/v1 \
--mcp fs=https://mcp-fs.internal/mcpstdout carries the result; stderr carries JSON-lines telemetry (one structured
event per line, trace-correlated); the exit code maps the terminal status. Bad
config exits 2 in milliseconds, before any LLM round-trip — and
--validate-config checks a config without running anything. The intelligence
endpoint speaks the OpenAI-compatible wire with native tool-calling; a
comma-list of endpoints is a failover order. See
docs/getting-started.md.
# once (default): run to a terminal status, then exit — Job / CLI shape
$ agentd --instruction "…" --intelligence https://gw.example/v1 --mode once
# loop: re-enter on a cadence until a bound or a drain signal
$ agentd --instruction "…" --intelligence https://gw.example/v1 \
--mode loop --interval 5m --deadline 24h
# reactive: idle, wake on MCP resource changes (requires ≥1 --subscribe)
$ agentd --instruction "…" --intelligence https://gw.example/v1 \
--mcp queue=https://mcp-q.internal/mcp \
--mode reactive --subscribe "queue://inbox"
# schedule: built-in interval/cron timer (--features cron for --cron)
$ agentd --instruction "…" --intelligence https://gw.example/v1 \
--mode schedule --cron "0 8 * * *"
# workflow: drive a pinned workflow graph, supervised like any run
$ agentd --mode workflow --workflow ./pipeline.json \
--intelligence https://gw.example/v1 --mcp fs=https://mcp-fs.internal/mcpReactive extras: --continue <uri> routes a subscription into one warm
session (context persists across wakes, tool lists refresh live);
--traceparent continues an upstream W3C trace. See
docs/modes-and-triggers.md.
With --features workflow, agentd runs agent-authored cyclic workflows: a
serde-only graph the model authors for itself at runtime (workflow.define /
workflow.run self-tools) or an operator pins from a file (--mode workflow).
Deterministic steps cost zero model tokens — the graph walker measured at
~146k steps/sec:
{
"start": "fetch",
"nodes": {
"fetch": { "kind": "agent", "instruction": "fetch the next item", "writes": "item",
"edges": { "ok": "route", "error": "done" } },
"route": { "kind": "branch",
"cases": [ { "when": {"op":"eq","key":"item","pointer":"/status","value":"pending"},
"goto": "work" } ],
"default": "done" },
"work": { "kind": "tool", "server": "fs", "tool": "process",
"args": { "id": { "$from": "item", "pointer": "/id" } },
"writes": "item", "edges": { "ok": "fetch", "error": "done" } },
"done": { "kind": "halt", "status": "completed", "result_from": "item" }
}
}- Twelve node kinds:
agent(a full agentic sub-run),tool(direct MCP call),assign(pure data shaping),infer(schema-checked structured intelligence),branch(JSON-pointer predicates + one semantic tier),foreach(deterministic fan-out, ≤1024 items, ≤8 parallel lanes),parallel(named heterogeneous branches, one result object),subgraph(sync orasync: true→ spawned child),join(fan-in of async handles),wait,human(a human gate: the served A2A task projectsinput-requiredand resumes on a spec-nativeSendMessagereply),halt. - A blackboard threads data between nodes (
writes/reads/{"$from": …}JSON-pointer references; 1 MiB per-value clamp), with reducers (writes_mode: append|merge|union) to accumulate instead of overwrite. - Durable state — the MCP checkpointer: per-superstep envelopes to any MCP
server implementing a 3-tool profile;
--workflow-resumegives crash-resume with budgets carried over, and@sequnder a new run-id is a fork (time-travel). No database is linked — the store is behind MCP. - Cycles are legal, runaways are not: layered termination — step budget,
shared token pool, wall deadline, per-node visit caps, and a progress guard —
each with a distinct
reasonin the result. - Optional CEL (
--features cel— the one gated dependency exception):{"op":"cel"}predicates, computedassign.expr,infer.checkconstraints, and reactive wake conditions. Hermetic, terminating, compile-checked at define time; a non-CEL build rejects CEL graphs fail-closed. - Reactive workflows:
--mode reactive --workflowsuspends on awaitnode and resumes on the trigger — a durable, event-driven pipeline.
See docs/workflows.md.
agentd is also a library: the binary is a thin shell (agentd-cli) over
the published engine crate (agentd-core, lib name agentd). Any Rust app can
run agentic logic as a function call, with native Rust tools the model
calls alongside MCP tools:
// 1. Register your code as a tool (it joins the model's catalogue — and wins
// name collisions with remote servers; first-party is unstealable).
agentd::tools::register(agentd::tools::CodeTool::new(
"word_count", "Count the words in a text.",
json!({"type":"object","properties":{"text":{"type":"string"}},"required":["text"]}),
|args| Ok(json!({ "words": args["text"].as_str().unwrap_or("").split_whitespace().count() })),
))?;
// 2. One agentic run, as a call — Outcome + token Usage back as plain values.
let intel = IntelClient::from_parts("https://gw.example/v1", token)?;
let (outcome, usage) = run_loop(&intel, &mcp_servers, &LoopInput {
instruction: "Count the words in this review, then summarize it.".into(),
output_contract: Some("JSON: {words, summary}".into()),
model: "my-model".into(), max_steps: 10, max_tokens: 20_000,
deadline: Instant::now() + Duration::from_secs(120),
seed: vec![], cancel: None,
}, &mut NoSelfTools, &log)?;Workflows embed the same way — author a dialect-2 graph as data, drive() it
with your own executor, and code tools are addressable from tool nodes as the
reserved server code. Two compile-guaranteed examples ship in-tree:
embedded-agent.rs (the loop in a
host app) and custom-cli.rs (a custom
CLI driving a code-tool workflow offline). The stock CLI registers nothing
— its no-local-code posture holds by construction. Reusable on their own:
agentd-mcp (MCP client/server + wire) and agentd-net (transports). Recipes,
the embedder obligations (the re-exec dispatch!), and the API-stability tiers:
docs/embedding.md + RFC 0022.
Serve your agent as MCP — tools status / subagent.spawn / subagent.send
/ subagent.status / subagent.cancel, plus live agent:// resources
(status, capabilities, inventory, intelligence, events, workflow,
run/<id>, config/effective):
$ agentd --mode reactive --subscribe "queue://inbox" --instruction "…" \
--intelligence https://gw.example/v1 \
--serve-mcp https://0.0.0.0:8443 --serve-cert tls.crt --serve-key tls.key \
--serve-client-ca clients.crt # and/or --serve-bearer <token>Delegate over A2A (--features a2a) — a served run is a spec-conformant
A2A Task; peers call SendMessage / GetTask / CancelTask /
SendStreamingMessage (SSE) on the same listener, and an agent delegates
outward with --a2a-peer name=https://… (bearer and/or mTLS client identity):
$ agentd --instruction "delegate the analysis to the research agent" \
--intelligence https://gw.example/v1 \
--a2a-peer research=https://research-agent.internal:8443Nest subagents — a parent spawns a child by re-exec'ing the same binary
with a narrowed spawn payload (subset of servers, tighter limits, its own
cgroup). The tree is bounded by --max-depth and a spawn-rate token bucket;
every child is one SIGKILL from gone.
- No local execution. There is no
exec, no shell, no local tool — the attack surface of a tool call is the remote MCP server's, not the host's. - Rule-of-Two trifecta gate. Tag servers with
--mcp-tags name=untrusted_input,sensitive,egress; a config that wires all three legs into one agent is refused at startup unless you explicitly--allow-trifecta. - Authenticated everything. Outbound: bearer/OAuth 2.1 client-credentials +
bundled webpki roots (+
--tls-cafor private PKI). Inbound: mTLS client CA and/or constant-time bearer; operator verbs require the Management identity — unauthenticated peers can't even see them. - Hardened served surface. Cross-origin requests are rejected (403);
sessions get unique
Mcp-Session-Ids; plaintext serving is loopback-only. - Secrets discipline. Tokens come from env or mounted files
(
--intelligence-token-filerotates live) and are never logged; telemetry logs lengths, not contents, unless you opt in with--log-content. - Contained blast radius. Reasoning runs in killable child processes under
optional per-run cgroups (
--cgroup,--cgroup-memory-max,--cgroup-pids-max) with atomiccgroup.killteardown.
See docs/security.md and rfcs/0012.
Calling an MCP server protected by AAuth? Build with --features aauth and
agentd gets an Ed25519 identity, an agent token from an Agent Provider,
and signs every MCP request (RFC 9421) — no shared API key, and the server
knows exactly which agent is calling:
$ agentd --instruction "…" --intelligence https://gw.example/v1 \
--mcp secure=https://mcp.secure.example/mcp \
--aauth-provider https://apd.example --aauth-enroll-token '{{secret:ENROLL}}'The token is fetched, cached, and refreshed automatically; the whole subagent tree signs under one identity. Draft support (Case A end-to-end); ships build-from-source, like CEL. See docs/aauth.md + RFC 0023.
Exit codes are the contract (RFC 0011): 0 completed · 1 crash · 2
config/usage (fails in ms, pre-LLM) · 3 stalled/partial · 4 intelligence
unavailable · 5 refused · 6 required MCP server down · 7 budget/deadline
exhausted · 124 supervisor hard-kill backstop · 137/143 external kills. A
clean drain is always 0, never 143. Policy codes (3/7) can be remapped with --budget-exit-code for
schedulers that treat nonzero as retry-forever.
Telemetry: JSON-lines on stderr (trace-correlated, --log-level),
optional --report-file run-outcome report (atomic write), Prometheus
/metrics + /healthz + /readyz via --metrics-addr (--features metrics), OTLP spans with GenAI semconv via --features otel, a liveness
heartbeat file via --health-file, and the agent://events live ring when
serving (--features events).
Discovery: agentd --capabilities prints a machine-readable manifest
(contract_version: "1.0" + a surfaces{} block of exactly what's compiled
and configured in) and exits — feature-detect from this, not the version
string.
Control plane: a Management-authenticated peer drives the served endpoint
with the a2a.Drain / a2a.LameDuck / a2a.Pause / a2a.Resume /
a2a.Cancel admin methods. SIGTERM starts a graceful drain
(--drain-timeout < pod grace).
Hot reload (--features hot-reload): SIGHUP — or a ConfigMap volume
swap with --watch-config (--features config-watch) — revalidates and
reapplies the reloadable subset (model, limits, log level, subscriptions,
live MCP server set) at a quiesce boundary, restart-free.
See docs/operations.md and docs/observability.md.
With --features cluster (RFC 0019):
--shard K/N— deterministic hash-partitioning of the URI/key space across a fleet of identical replicas (works for reactive and timer modes).--claim <uri>=<server>+--claim-ttl— claim/lease an item before processing it, so at-least-once event delivery becomes exactly-one-owner processing.--standby --assign-from <server>:<uri>— a warm worker pool that claim-pulls assignments.agent://capacity+ Prometheus metrics feed autoscaling.
See docs/scaling.md.
The default build is intentionally small; everything else is opt-in at compile
time. A flag whose feature is absent exits 2 loudly — never a silent no-op.
| Feature | What it adds | Extra deps |
|---|---|---|
tls (default) |
rustls + ring + bundled roots — direct https:// everywhere |
rustls stack |
serve-https |
serve agentd's own MCP over HTTP(S) with mTLS/bearer | — |
a2a |
A2A Task surface + outbound delegation peers | — |
workflow |
agent-authored cyclic workflows (all ten node kinds) | — |
cel |
CEL predicates/expressions in workflows + reactive conditions | cel-interpreter (the one exception) |
events |
the agent://events live ring resource |
— |
metrics |
hand-written Prometheus text + health endpoints | — |
otel |
hand-rolled OTLP/HTTP span export, GenAI semconv | — |
cron |
5-field UTC cron scheduling (hand-rolled parser) | — |
cluster |
sharding, claim/lease, standby pools, capacity signal | — |
oauth |
OAuth 2.1 client-credentials for remote endpoints | — |
hot-reload / config-watch |
SIGHUP / inotify restart-free reconfig | — |
Shipped release feature set:
serve-https,a2a,events,metrics,cron,otel,cluster,hot-reload,config-watch,workflow.
Measured on the v1.0.0 release build (x86_64, musl, stripped):
| Metric | Value |
|---|---|
Binary (static-PIE, runs on scratch) |
3.0 MiB (1.5 MiB gzipped) |
| Container image pull | ~1.2 MiB, single layer |
Cold start (--version / --capabilities) |
< 1 ms |
| Idle serving daemon RSS | ~2 MiB, flat under load |
Served request overhead (tools/call, loopback, fresh conn) |
p50 0.26 ms |
| Deterministic workflow steps | ~146k steps/sec (single lane, 0 model tokens) |
| Direct external dependencies | 3 (serde, serde_json, libc) |
- docs/README.md — the task-oriented guide index: getting started · configuration · architecture · mcp · modes & triggers · workflows · subagents · intelligence · security · observability · operations · deployment · scaling · use cases
- rfcs/README.md — the normative specifications (RFC 0001–0022; RFC 0001 is the narrative front door).
- examples/SAMPLES.md — runnable samples: shell
one-liners, Docker Compose, Kubernetes
Job/CronJob/Deploymentmanifests, a systemd unit. - CHANGELOG.md — release history (v1.2.0 current).
- Website: agentd.dev — rendered docs + RFCs.
Apache-2.0 — see LICENSE.