Durable agents
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.
Live demo
The same durable loop, forked twice — same model, different prompt.
Simple to build
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.
One primitive
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.
The whole loop
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
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.
The process is disposable.
The promise is the record.
Runs on your infrastructure
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.
SELECT * FROM promises WHERE id LIKE 'chess-game-%';
An open specification
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.
the open specification — implement it in any language
We value your privacy
We use strictly necessary cookies to run this site. With your consent we also use Google Analytics to understand traffic. We do not load analytics until you choose to allow it. Privacy Policy.