redisfred — agent orchestration runtime

fred@redisfred 2026-08-02 14:41:07 utc

$ ./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.

## status

lock queue cache ratelimit registry ledger status: nominal

static placeholder — the live line renders against a running valkey, one dot per module.