Always-on personal agents keep dying the same three deaths: leaked keys, runaway spend, and silence. The first two get the headlines. The third one is the one that actually made me want to throw my phone: you ask your agent to do something, it starts, and you never hear from it again. Revenant is my answer to all three. It is a lean agent runtime in Rust with chat channels, skills, cron loops, subagents, and graph memory, built on one architectural rule and one behavioral rule. The architectural rule: the harness never talks to a model provider directly. The behavioral rule: a turn always ends in a visible outcome, no exceptions.

This post covers the concept, the model-tier design that makes a free local model the default with cloud failover behind it, and the night I found out the behavioral rule takes real engineering to keep. The code is public at themsquared/revenant.

Why the harness never holds a provider key

Every LLM, MCP, and A2A call Revenant makes flows through a bundled, supervised agentgateway. The gateway owns the provider keys, the model aliasing, cross-provider failover, token and dollar budgets, and the GenAI telemetry. The harness renders the gateway’s config; the gateway enforces it.

you ──▶ telegram / web / tui / cli ──▶ revenant ──▶ agentgateway ──▶ any model, any MCP server
                                        (harness)    (data plane: aliases, failover,
                                                      budgets, guardrails, metrics)

This split is the whole security story. Prompt injection cannot exfiltrate keys from a process that has never seen them. A runaway loop cannot blow past a spend cap that is enforced below the agent, in a data plane the agent has no ability to reconfigure. The agent gets a loopback URL and the names of model tiers. That is all it gets.

It also means every hard reliability problem lives in exactly one place. Failover, health eviction, request logging, rate limits: these are gateway policies rendered from the harness config, not logic scattered through the agent. When I write about governing agent traffic through a gateway in other contexts, like an LLM-driven robot fleet, it is the same argument. Revenant just commits to it completely: there is no code path around the gateway to audit, because there is no code path around the gateway.

How a free local model became the default

Model access in Revenant is organized as tiers: fast, balanced, deep, and local. Each tier is a list of targets in priority order, and the gateway renders the list as one virtual model with failover routing. The agent asks for “balanced” and never learns which provider answered.

When NVIDIA shipped Nemotron 3.5 Lightning on Ollama, the economics of the tier list flipped. It is a 30B mixture-of-experts model with 3B active parameters, built explicitly for the execution layer of always-on agents, and it runs comfortably on an M-series Mac. So my default tier now looks like this:

[tiers.balanced]
strategy = "failover"

[[tiers.balanced.targets]]
provider = "ollama"
model = "nemotron-3.5-lightning:latest"
reasoning_effort = "none"

[[tiers.balanced.targets]]
provider = "openai"
model = "kimi-k3"
api_key_env = "MOONSHOT_API_KEY"
base_url = "https://api.moonshot.ai/v1"

[[tiers.balanced.targets]]
provider = "anthropic"
model = "claude-sonnet-5"
api_key_env = "ANTHROPIC_API_KEY"

Priority 0 is a local model that costs nothing per token. The cloud models are failover, not the default. The gateway gives each failover member an outlier-detection policy, so a target that answers with “I am broken” status codes gets evicted for 60 seconds and the virtual model routes to the next priority:

unhealthyExpression: response.code >= 500 || response.code == 529 || response.code == 429 || response.code == 402 || response.code == 401 || response.code == 403 || response.code == 404
eviction:
  duration: 60s

I tested the direction everyone worries about: what happens when the local model is down. The first request fails with a 503 and evicts the Ollama target; every request in the eviction window cleanly serves from the cloud. The harness absorbs even that first failure, because the turn loop retries once when nothing has streamed yet. Ollama dies, the user notices nothing, and the bill goes up by one cloud call. The reverse direction matters too: when a cloud account runs out of credit, the same eviction machinery routes back down. Deep reasoning stays on a cloud tier by choice, not by dependency.

Local reasoning models break agents in two specific ways

Nemotron is a reasoning model, and putting a reasoning model at priority 0 of an agent runtime surfaced two failure modes worth knowing before you try this.

First, Ollama rejects forced tool selection while thinking is enabled. Any harness feature that pins tool_choice to a specific tool for structured output gets this back:

{"type":"error","error":{"type":"invalid_request_error","message":"tool_choice 'specified' is incompatible with thinking enabled"}}

In Revenant that broke every structured-output path at once, including the agent’s own self-review loop, which retried on a five-minute cadence and failed identically every time.

Second, reasoning is silent. The thinking tokens do not translate into streamable output through the gateway’s Anthropic-shape endpoint, so the user watches nothing happen for minutes. And when the reasoning burn exceeds the request’s max_tokens, the final message arrives empty. From the outside, both look like a dead agent.

The fix for both was one knob. Ollama’s OpenAI-compatible endpoint accepts reasoning_effort: "none", and agentgateway can force request-body values per model with a static override. Revenant now renders that from the reasoning_effort line in the tier config above:

- name: balanced/0
  params:
    model: nemotron-3.5-lightning:latest
  overrides:
    reasoning_effort: none
  provider: ollama

With thinking off, forced tool calls return clean tool_use blocks and answers stream immediately. The local tier’s job is fast execution. Deliberate thinking belongs to the deep tier, where a frontier model does it better anyway.

A turn can never go silent

Here is the behavioral rule, stated the way I gave it to the project: a conversation never goes without a response, and a job never fails to report. An agent that errors is annoying. An agent that goes quiet is useless, because you cannot tell it apart from an agent that is working.

I thought the rule held until I went looking. One debugging night turned up three independent holes, and each one alone produces a silent turn.

Unbounded waits. The LLM client had a connect timeout and nothing else, on the theory that streaming responses can be long-lived. True, and also fatal: a stalled stream (half-open TCP, wedged upstream) hung the turn forever. Worse, the session model treats messages that arrive during a running turn as mid-turn interjections, so every follow-up you send while a turn is hung gets silently queued behind a turn that will never finish. The fix is layered deadlines on every stream: time to first byte, maximum idle gap between events, and a total per-attempt ceiling, each tunable by environment variable. A stall becomes an error, and an error becomes a reply.

No backstop. Per-step deadlines cannot cover what they cannot see, so the session actor now races every turn against a wall-clock watchdog. On expiry it cancels the turn, grants a grace period to unwind, then drops the future outright and reports the overrun. The drop is safe because the turn’s session state lives in an RAII guard that unwinds on any exit path.

Restart amnesia. This was the one actually firing, and it had nothing to do with models. A turn in flight when the daemon stops just dies with the process, and nothing ever told the owner. My canary box made it comic: the auto-rollout script compared the full git SHA against a marker file it had written in short form, the comparison could never match, and so the deploy timer concluded a new deploy was needed every five minutes. Every turn longer than the window died mid-flight, all day, every day. That single character class, short SHA versus full SHA, was the original “my agent never finishes anything” bug.

The rollout comparison is a one-line fix. The structural fix is that the daemon now sweeps on boot for sessions whose most recent message is the user’s, which is precisely a turn that was accepted and never answered, and reports each one: “the daemon restarted while this turn was running, the task did not complete.” The notice is persisted into the session transcript, so the next boot does not re-report it and the conversation keeps an honest record of where the turn was cut.

Validation is a soak harness that speaks to the live control API: submit a turn, then require a terminal event within a deadline, with verdicts of OK, SLOW, or SILENT. SILENT is an invariant violation, full stop. The final run on a stable daemon:

[1/6] OK  turn_completed   33s  'What is 41*17? Reply with just the number.'
[2/6] OK  turn_completed   43s  'Say hello in exactly three words.'
[3/6] OK  assistant_reply  121s 'What time is it right now? Use a tool if you have one.'
[4/6] OK  turn_completed  108s  'List two of your available skills, one line each.'
[5/6] OK  turn_completed   67s  'What is 12! (twelve factorial)? Just the number.'
[6/6] OK  turn_completed   94s  'Name your current model tier in one sentence.'

PASS: invariant held on all 6 turns (0 slow, 6 within SLA)

The run before that one is the more interesting result: a mid-soak deploy killed a turn on purpose, and the sweep on the next boot converted it into a reported failure instead of silence. The invariant held through the exact event that used to break it.

The agent cut its own release

One more thing happened that night that I want on the record because it is the point of the project. After the liveness fixes merged, I went to tag the release and found the tag already existed. Revenant’s self-improvement loop had watched its changes land on main, tagged v2026.8.0 itself, and handed the tag to CI, which built and published the release. The tagger on the annotated tag is the agent’s own identity.

That loop, called ascension, runs the same way everything else does: through the gateway, under budgets, behind an approval broker with default-deny, gated by evals, and with a PR-based path so a human review sits between the agent and its own code. Self-releasing sounds alarming until you notice the agent has strictly fewer privileges than the CI system most teams already trust. A runtime where the agent maintains itself is exactly where always-on agents are going, and I would rather learn the failure modes on my own fleet of two machines than read about them later. If the scale end of this interests you, I wrote about running thousands of agents on tens of pods with kagent’s substrate.

Try it

The installer fetches pinned, checksum-verified binaries for the harness and gateway, then walks through provider setup:

curl -fsSL https://raw.githubusercontent.com/themsquared/revenant/main/installer/install.sh | sh
revenant chat              # supervised gateway + streaming REPL
revenant service install   # run it always-on (launchd / systemd)
revenant open              # web UI: chat, approvals, spend, loops

No cloud key is required. With Ollama installed, a local tier serves everything, and the tier config above is the template for adding cloud failover later.

Where this goes

Revenant is my working answer to what an always-on personal agent should be: a harness that cannot leak what it does not hold, a gateway that enforces what the agent cannot override, a free local model doing the everyday work with the cloud as backup, and a liveness rule with teeth. The next steps are making local-first the setup wizard’s default when Ollama is present, and letting the two-machine fleet keep upgrading itself. The code, the lore, and the tier configs are all in themsquared/revenant.