Architecture and Core Concepts
Pragmatic.Agent is the local coordination substrate for Pragmatic.Design hosts. It’s a small sidecar process that provides a shared KV store, cluster membership, and a stable IPC surface — so individual app processes don’t need to talk to a centralised service for routine operational concerns.
Status: implemented preview subsystem. Already used inside the repo; packaging and operational surface are still evolving.
The Problem
Section titled “The Problem”A multi-host Pragmatic application needs a place to keep things like:
- Configuration snapshots — what’s the current
booking-servicefeature flag state? - Tenant → backend map — which hosts serve tenant
acme-eu? - Cluster membership — which hosts are alive right now?
- Control-plane commands — “please drain traffic from host-3”
You can centralise this on Redis / etcd / Consul / a custom SignalR hub, and Pragmatic has adapters for all of those. But you pay:
- a network hop for every read
- single-point-of-failure risk if the central store is down
- latency variance for operational hot paths (feature-flag evaluation)
- secrets + TLS management for every outbound call
The Agent solves the local half of this problem: every host has a local daemon that apps talk to over a Unix socket / Windows named pipe. The daemon gossips with peer Agents, persists state locally, and serves reads in microseconds.
Three packages
Section titled “Three packages”| Package | Role |
|---|---|
Pragmatic.Agent | The daemon executable + CLI (pragmatic-agent start, pragmatic-agent status, …) |
Pragmatic.Agent.Client | Runtime integration consumed by Pragmatic hosts (builder.UseAgent()) |
Pragmatic.Agent.Protocol | Wire protocol — frames, payloads, JSON/MessagePack serialisation, transport security |
Hosts reference Pragmatic.Agent.Client. Operators run Pragmatic.Agent as a daemon. Pragmatic.Agent.Protocol is an implementation detail pulled in transitively.
What UseAgent() replaces
Section titled “What UseAgent() replaces”The default implementations of these stores are in-memory / file-based; UseAgent() swaps them to Agent-backed implementations:
| Interface | Default | Agent-backed |
|---|---|---|
IControlPlane | No-op | AgentControlPlane (heartbeat, commands) |
IConfigurationStore | appsettings.json | Agent KV (config/{key}) |
IFeatureFlagStore | In-memory defaults | Agent KV (flags/{flag}) |
ITenantStore | Static | Agent KV (tenants/{tenant-id}) |
The Agent-backed versions fall back to graceful degradation: if the Agent is unavailable, reads return defaults (or the last known value from a local cache), writes are queued and retried. Your app keeps serving rather than crashing on a socket error.
Storage model
Section titled “Storage model”The daemon persists state in a file-backed KV store under a data directory:
~/.pragmatic/agent/on desktop Linux / macOS%LOCALAPPDATA%\Pragmatic\Agent\on Windows/var/lib/pragmatic/agent/or$DATA_DIRin containers and services- Multiple instances live in sibling directories (
--instance=staging→agent-staging/)
The KV layout is hierarchical:
config/ booking-service/ retry-max-attempts → "3" timeout-ms → "5000"flags/ new-checkout-flow → "enabled:true,rollout:50"tenants/ acme-eu → "{ 'connectionString': '...', 'region': 'eu-west-1' }"gateway/ routes/ booking → "{ 'backends': ['host-1', 'host-2'] }"Apps read with AgentClient.GetAsync("config/booking-service/retry-max-attempts"); the daemon serves from RAM (state is fully loaded at startup) and persists writes to disk.
Cluster model — SWIM gossip
Section titled “Cluster model — SWIM gossip”Multiple Agents form a cluster through SWIM-based gossip:
- membership — each agent maintains a view of live/suspect/dead peers
- failure detection — periodic indirect pings; an unreachable peer is marked suspect then dead after a grace period
- dissemination — KV changes piggyback on gossip messages, propagating through the cluster in O(log N) rounds
- piggybacking — the same gossip packets carry state updates plus health info, minimising traffic
This gives you cluster-wide propagation of config / flag / tenant changes without a central store. Each write lands in one Agent; within a few gossip rounds (~seconds on LAN), every Agent has the new value.
SWIM was chosen because it scales, tolerates partitions, and needs no leader. The tradeoff: state convergence is eventually consistent — a reader in a different host might see stale values for a few seconds.
For strong consistency (leader election, linearisable writes), use Pragmatic.ControlPlane on top of the Agent.
IPC transport
Section titled “IPC transport”Apps talk to the Agent over a local-only transport:
- Linux / macOS: Unix domain socket at
$XDG_RUNTIME_DIR/pragmatic-agent.sock(or configurable) - Windows: named pipe at
\\.\pipe\pragmatic-agent
Both transports are machine-local — no network exposure, no TLS needed, filesystem permissions gate who can talk to the daemon.
The wire protocol is framed binary with optional MessagePack bodies (default is JSON for debuggability). Clients reconnect automatically on daemon restart.
Lifecycle
Section titled “Lifecycle”- Daemon starts (manually, systemd, launchd, Docker
CMD, or auto-start fromUseAgent()in dev) - Daemon binds its local socket, loads persisted KV state, joins the gossip cluster
- Apps open connections on startup via
UseAgent(); each app sends a heartbeat every few seconds - Apps publish initial state (tenant mapping, flag defaults) if they own it
- Apps read runtime state on demand; reads are sub-millisecond via in-memory cache on the daemon
- Daemon persists KV mutations to disk as they happen
- On shutdown, daemon unregisters from gossip (if graceful), flushes state, closes sockets
The same socket is used by the CLI as by app clients:
pragmatic-agent start --data-dir=/var/lib/pragmatic/agentpragmatic-agent statuspragmatic-agent config get booking-service.retry-max-attemptspragmatic-agent config set booking-service.retry-max-attempts 5pragmatic-agent flag set new-checkout-flow enabled=true rollout=100pragmatic-agent cluster membersOperators don’t need to install a separate tool — the daemon ships the CLI in the same binary.
What the Agent does not do
Section titled “What the Agent does not do”- Application delivery — the Agent doesn’t deploy your app, it just coordinates it
- Strong consistency — for leader election / distributed locks, use
Pragmatic.ControlPlane - External gateway — routing decisions happen in
Pragmatic.Gateway, which consumes from the Agent - Service discovery across WAN — the gossip cluster is LAN-oriented; for WAN-scale discovery use an external tool (Consul, etcd)
- Secrets storage — values are file-backed but not encrypted at rest. Use a separate secrets store and put references in Agent KV if needed
Integration points
Section titled “Integration points”- Pragmatic.Gateway — reads routes and clusters from Agent KV
- Pragmatic.Composition.Host — hosts the app that calls
UseAgent() - Pragmatic.ControlPlane — layered on top for strong-consistency primitives
Related
Section titled “Related”- getting-started.md — starting the daemon, wiring a host
- common-mistakes.md
- troubleshooting.md — socket / connection issues