$ ./redisfred
_ _ __ _
_ _ ___ __| (_)___/ _|_ _ ___ __| |
| '_/ -_) _` | (_-< _| '_/ -_) _` |
|_| \___\__,_|_/__/_| |_| \___\__,_|
agent orchestration runtime
Redis Fred. Rediscovered.
redisfred is the boring machinery between the agents: who holds what, who
runs what, what is already known, what is allowed, who is still alive, what
was decided. Six primitives, one shared Valkey, no surprises.
## philosophy — three-tier compression
Agents are good at deciding. What they are bad at is deciding the same
thing twice, deciding it at the same time as another agent, and remembering
what they decided last week. redisfred exists so they do not have to.
The design is three tiers of compression. Each tier holds less judgment
than the one above it, and is faster because of it. The human tier decides
what is worth wanting — taste, values, the direction of the whole thing.
The agent tier asks is this the right thing? and what it means
tactically, right now. The runtime tier decides nothing. It executes one
fixed pipeline, exactly the same way, every time.
[human] taste · values · what's worth wanting
│
▼ compress
[agent] is this the right thing? · tactical meaning
│
▼ compress
[runtime] lock → cache? → fanout → join → validate → release
The pipeline is deliberately boring. Every stage is a question with a
yes-or-no answer, and every answer is written to the ledger, so the runtime
gets smarter without the agents having to be.
- lock
- exclusive access, TTL-backed. a dead agent loses its grip.
- cache?
- was this already answered? if yes, do not pay for it again.
- fanout
- split the work. atomic claims, no double-grabs.
- join
- collect the pieces.
- validate
- check the work before anything trusts it.
- release
- free the lock. record the decision. done.
The expensive thing in orchestration is judgment. This stack spends it
only where it exists.
## primitives
Six modules, one shared Valkey connection, zero inter-module
dependencies. Each one is small enough to read in an afternoon and
independent enough to delete without breaking the others. The docstrings
below are the real ones — the code means what it says.
lock/ — Distributed locks with TTL auto-expiry. Dead agents can't hold locks forever.
Acquire is SET NX EX inside a Lua script — one round trip
to Valkey, no check-then-set race. Every acquisition carries a UUID
ownership token, and release is compare-and-delete: you can only release a
lock you still hold. If the process dies mid-task, the TTL reclaims the
lock. The default lease is 30 seconds. A lock does not care whether its
holder is alive, which is exactly why dead agents cannot hold one forever.
# set nx ex · uuid token · compare-and-delete · default ttl 30s
$ cat runtime/lock.py
-- acquire: SET NX EX — one round trip, no race
if redis.call('SET', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2]) then
return 1
end
return 0
-- release: compare-and-delete — only the owner can let go
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
end
return 0
queue/ — Dual sorted-set job queue. Scheduled (epoch-ms) + Ready (priority).
Work lives in two sorted sets: scheduled, keyed by epoch
milliseconds, for tasks that wait; ready, keyed by priority, for
tasks that run now. Claiming is one Lua script: pop the highest-priority
ready task, set its lock, hand it to an agent. Two agents can never grab
the same task, because the pop and the lock are the same atomic operation.
Tasks that fail past recovery go to the dead-letter list with a reason and
a timestamp — a pile for humans to inspect, not for the runtime to guess
about.
# zset scheduled (epoch ms) + zset ready (priority) · atomic claim via lua · dead-letter list
$ cat runtime/queue.py
-- claim: pop the highest-priority ready task, set its lock
local ready_key = KEYS[1]
local lock_prefix = ARGV[1]
local agent_id = ARGV[2]
local ttl = tonumber(ARGV[3])
local tasks = redis.call('ZRANGE', ready_key, 0, 0)
if #tasks == 0 then
return nil
end
local task_id = tasks[1]
redis.call('ZREM', ready_key, task_id)
redis.call('SETEX', lock_prefix .. task_id, ttl, agent_id)
return task_id
cache/ — Multi-tier TTL cache. Miss falls through to original path — no crashes.
Every miss returns None, and the caller runs the original
path. Every read that happens while Valkey is unreachable also returns
None, and the caller runs the original path. That is the entire
contract, and it is why the cache is allowed to exist. Keys are
sha256-hashed so namespaces never collide; default TTL is 300 seconds. A
cache is a performance layer. The day it can crash your runtime, it stops
being one.
# sha256(16) keys · ttl 300s · miss ⇒ original path · down ⇒ original path
ratelimit/ — Token bucket rate limiter via Lua EVAL. Per-provider, per-key, per-model.
Buckets are per provider, per key hash, per model — one slow
API cannot starve the others. When a bucket runs dry, the failure counter
climbs; at 20 failures the breaker opens, and the runtime stops calling
that endpoint entirely instead of hammering it while it is down. The
breaker resets on recovery, not on a timer: recovery is what re-opens the
door. Self-awareness at the infrastructure level.
# token bucket · lua eval · per provider/key/model · breaker opens @ 20 failures
('ok', 42) · ('rate_limited', 0) · ('blocked', 23)
registry/ — Agent registry — heartbeat, discovery, status tracking.
Agents heartbeat into a shared hash, and a per-agent key with
a 30-second TTL is the ground truth of liveness. Stop heartbeating and you
stop existing — no manual cleanup, no ghost agents lingering in a config
file. list_alive() answers who can I talk to right now?;
count() answers how many of us are there?
# heartbeat ttl 30s · per-agent liveness keys · auto-expiry on dead agents
ledger/ — Decision ledger — records every orchestration decision for pattern mining.
Every decision — what was chosen, by whom, with what context,
and how it turned out — is appended to a Valkey stream, capped at the last
100,000 entries. That stream is the raw material for pattern mining:
feed, mine, compile. When a pattern wins often enough, it stops being a
guess and becomes a rule. Probabilistic patterns graduate to deterministic
ones.
# streams · maxlen 100k · feed → mine → compile
[ design principle ]
Valkey is a performance accelerant, never a hard dependency.
Every layer degrades gracefully.
A missing cache costs a round trip. A missing queue costs a
re-run. A missing registry costs a restart. None of them cost an outage.
If Valkey disappears, the runtime should notice the way you notice a
shortcut is closed — you take the long way, and you arrive.