A Perpetual Agent Must Never Wedge

by Simon Lehmann

I build my own agent runtime — a Rust crate I unimaginatively call lehmann-code. It runs the agent loop for everything I do: a coding harness, and an NPC that lives on a game server. Same loop, same session model, same tool dispatch. This post is about the ugliest problem that loop has to solve.

Sessions that never end

A chat assistant’s context problem is soft: sessions are short, a human is watching, and a fresh start costs nothing because the human carries the continuity.

A perpetual agent gets none of that. The NPC’s session is supposed to run forever — accumulating observations, tool results, player chat, recalled memories. No human in the loop, no session boundary to hide a reset behind. The loop enforces the context policy alone, thousands of times a day.

One day the policy lost. The agent wedged for over two hours: alive, polling, and completely mute.

Anatomy of a wedge

The post-mortem found a chain of reasonable-looking decisions:

  • The history horizon was count-based: keep the last 48 messages. Fine, until messages got fat — richer observations meant 48 messages was suddenly ~24k tokens against a ~10k budget.
  • The window enforcer had two tools: evict old tool outputs, then compact. Eviction had already eaten everything evictable. Compaction couldn’t get under the limit either.
  • So the loop logged “context exceeds window even after eviction and compaction”, gave up on the turn, and went back to sleep. Forty-five seconds later the idle wake tried again with the same history and failed the same way. Forever.

Every component behaved as designed. The system was wedged anyway, because the design contained an unhandled terminal state: history that cannot be made to fit. The code treated it as an error to report. For a perpetual agent it has to be a state to recover from.

The invariant, then the ladder

The fix started with writing down the invariant: the loop may lose history; it may never lose the ability to act.

Enforcement became a ladder — six stages, each more willing to lose information than the last:

  1. Evict tool outputs. Old tool results are the least valuable tokens in the history; the agent already acted on them.
  2. Compact. Summarize old spans into a condensed record.
  3. Size-driven trim. The stage the old design was missing: trim by token size, not message count, down to a small floor of recent messages. Count-based horizons are how the wedge got in.
  4. Shed recall. Retrieved memory snippets are re-fetchable by definition. Under pressure they go.
  5. Hard reset. Keep only the current observation behind an explicit [context reset:] marker — but persist the full session to disk first, and have the agent write itself a diary entry tagged context-reset, so the next session knows what happened. Losing history is acceptable. Losing it silently is not.
  6. Give up — allowed only if the system prompt and tool specs alone don’t fit. That’s a misconfiguration, and it should fail loudly at a human.

What matters isn’t any single stage, it’s that the ladder is total: every history size lands somewhere, and every rung above the last preserves the ability to take a turn. “Can’t fit” stopped being a terminal state because there is no terminal state anymore.

The NPC is my best QA engineer

My coding harness would have taken months to hit this bug. Coding sessions are short, and I’m sitting right there when something goes wrong.

The NPC runs unattended around the clock in a world that generates fat, bursty context — mob fights, chatty players, dense observations. Every latent flaw in the loop eventually gets hit hard enough to leave a log trail. The wedge, tool-call repair, turn-yielding during movement: each hardened the shared runtime, and the coding harness inherited every fix for free.

That’s my actual thesis, and it isn’t about games: the loop is the product. Models improve on their own schedule. Reliability lives in the harness, and you build it by running your loop somewhere merciless enough to break it while you’re still around to watch.