Durable agents

The loop runs. The crash doesn’t stop it.

To the right, two Claude agents are playing chess — each one a durable loop running on Postgres. Between every move the worker that runs them terminates. The game doesn’t notice.

They’re the same loop, forked twice: same model, same tools — one prompt says attack, the other says hold the position. That’s the whole difference between one agent and another.

Get startedSee how it works ↓Open source under Apache 2.0.

Live demo

Two agents, one loop — live.

The same durable loop, forked twice — same model, different prompt.

connecting...
White · Claude Haiku 4.5aggressive prompt · one ctx.run per move
Black · Claude Haiku 4.5positional prompt · one ctx.run per move
Connecting

Simple to build

Four things you change. Everything else, the substrate handles.

When you reshape the loop, there are really only four things you touch. Those four points are where one agent becomes different from another — and the two players above show it exactly: only the first one differs.

Same model, same tools, same finish line. One prompt attacks, the other defends. Everything underneath — checkpointing, retries, resume — is the substrate, and each move is itself a durable execution: a call that fails gets retried, a call that waits can wait for days.

↻ the chess agent loop
01the system promptdiffers
“You are White. Play aggressively.” · “You are Black. Play for position.”
02the model it runs on
claude-haiku-4-5 — the same for both players
03the tools it can call
the board — the legal moves in this position
04when it's finished
game.isGameOver() — checkmate, stalemate, or draw
everything else — checkpointing · retries · fan-out · resume — is the substrate

One primitive

A move is just a durable step.

Both agents run the same loop, and every move it makes — asking Claude for a move, writing the new position, waiting for the other side — is wrapped in one primitive: ctx.run. The model call is a durable step like any other. If the API flakes, the step retries. If Claude returns an illegal move, the loop corrects itself before writing anything down.

Below is the execution trace of the running game, live — what resonate tree would show you. Each move you watch land carries its real payload: the move, what it captured, the agent’s reasoning. And between moves you can watch ctx.sleep count down while the worker is terminated — the server holds the continuation and re-invokes it when the timer fires.

Connecting to the live workflow…

The whole loop

One generator. Three primitives.

The entire game running on the board is this. One agentPlayer call for both players, and three Resonate primitives: ctx.run, ctx.sleep, and ctx.detached.

import type { Context } from "@resonatehq/sdk";
import { Chess } from "chess.js";
import Anthropic from "@anthropic-ai/sdk";

const anthropic = new Anthropic();
const MOVE_DELAY_MS = 4500;

// The only thing that differs between the two agents: the system prompt.
const PROMPTS = {
  w: "You are White. Play aggressively — seize the initiative.",
  b: "You are Black. Play for position — trade into a stable structure.",
};

// One agent. Given the side, the position, and the legal moves, it picks one.
async function agentPlayer(_ctx: Context, side: "w" | "b", fen: string, legal: string[], history: string) {
  const response = await anthropic.messages.parse({
    model: "claude-haiku-4-5",
    max_tokens: 512,
    system: [{ type: "text", text: PROMPTS[side], cache_control: { type: "ephemeral" } }],
    messages: [{ role: "user", content: `FEN: ${fen}\nLegal: ${legal.join(",")}\nHistory: ${history}` }],
    output_config: { format: { type: "json_schema", schema: MOVE_SCHEMA } },
  });
  // Validate, retry once on an illegal move, fall back to a random legal move.
  return coerceToLegal(response.parsed_output, legal);
}

export function* chessGame(ctx: Context, gameNumber = 1) {
  const game = new Chess();
  let moveCount = 0;

  yield* ctx.run(publish, buildState(game, undefined, moveCount));

  while (!game.isGameOver()) {
    // Same call for both players — only the side (and so the prompt) changes.
    const { move, reasoning } = yield* ctx.run(
      agentPlayer, game.turn(), game.fen(), legalMoves(game), history(game),
    );

    applyUciMove(game, move);
    moveCount++;

    yield* ctx.run(publish, buildState(game, move, moveCount, reasoning));
    yield* ctx.sleep(MOVE_DELAY_MS);   // ← worker terminates here
  }

  // Each game is its own root promise. Detach the next so replay scope stays
  // bounded — every game replays in its own crash domain, in isolation.
  yield* ctx.detached(chessGame, gameNumber + 1);
}

Why one loop is enough

Already durable.

The game on this page proves it the hard way. Between every single move, the worker running the agents terminates — that’s the ctx.sleep at the end of each turn. The state that lets play resume lives in the promise, written to Postgres as it happens, not in the process that vanished.

We redeployed the server mid-game. The workflow picked up from the last completed move, untouched — no intervention. A deploy, a crash, a pod rescheduled mid-run: the loop doesn’t start over. It comes back up, reads the last promise it kept, and continues from exactly there.

running
the loop · promisethe process · crashes & restarts
The loop’s progress lived in the promise,
not the process that died.

Runs on your infrastructure

It lives on your infrastructure, not behind someone’s API.

Resonate runs as a single binary on the infrastructure you already operate — one service to run, not a fleet to provision and babysit. It sits inside your stack, not behind an API you have to trust.

Its state lives in your own Postgres. And because that state is just rows, you can look at it directly: the game on this page is running on the five managed pieces below, and every move it has ever made is a row you could read with plain SQL.

Resonate serverCloud Run · RustHolds workflow state.Cloud Function (Gen 2)chess-hero-workerScales to zero between moves.Cloud SQL · Postgresserver storageDurable across redeploys.Firestorechess/liveWorld-readable doc.BrowseronSnapshot(chess/live)No SSE. No gateway.HTTP · one step per invocationsqlx · Unix socketpublishsnapshot push
Resonate serverCloud Run · one container · Rust binary
Server storageCloud SQL Postgres — survives redeploys
WorkerCloud Function Gen 2 · scales to zero between moves
State bus to browsersFirestore · onSnapshot on chess/live
Both playersClaude Haiku 4.5 · same loop, forked by prompt

SELECT * FROM promises WHERE id LIKE 'chess-game-%';

A protocol, not a product

A protocol you adopt, not a vendor you depend on.

Resonate is open source under Apache 2.0, and durable execution is a protocol — something you adopt and build on, not a vendor you build around and can’t leave.

There’s a public library of agent skills to start from, and an open Discord where people are already building. You can run it, read it, fork it, or build your own.

docs.resonatehq.io/spec ↗

the open specification — implement it in any language

Open source, no feature gatesPublic agent-skill libraryThe chess demo, forkableFork or build your own
Discord — agent builders, right now