Durable agents

The loop runs. A crash is a pause.

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 plays on.

They’re the same loop, forked twice: same model, same tools — one prompt says attack, the other says hold the position.

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.

Reshaping the loop touches four points. The two players above differ in one — the prompt.

Each move is itself a durable execution: a call that fails is 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 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.

We redeployed the server mid-game. The workflow picked up from the last completed move.

Between every move, the worker running these agents terminates — that’s the ctx.sleep at the end of each turn. By then the move is already in Postgres, written as it completed. The loop comes back up, reads the last promise it kept, and continues from there.

A deploy does this. So does a crash, or a pod rescheduled mid-run. The game keeps its place through all of them.

running
the loop · promisethe process · crashes & restarts
The process is disposable.
The promise is the record.

Runs on your infrastructure

One binary, on infrastructure you already run.

The Resonate server is a single binary. The game on this page runs it in one Cloud Run container, backed by Cloud SQL Postgres — every piece it needs is listed below.

State is rows in Postgres you own. Every move this game has ever made is a row you can 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-%';

An open specification

The protocol is published. The code is Apache 2.0.

Durable execution here is a specification anyone can implement, in any language. Everything Resonate ships against it — the server, the SDKs, the tools around them — is open source.

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