Durable Agent Tasks: Making Hermes More Powerful with Restate
A request-scoped agent dies when its process does. Wrap Hermes-driven workflows in durable execution and they survive crashes, waits, approvals, and schedules.
Tested against Hermes Agent v0.20.0 · Restate 1.x server & TypeScript SDK 1.16.9 · August 2026
Hermes today: great at requests, silent between them
Hermes is superb at the request/response shape of agentic work: you type a message, it executes tools across multiple reasoning turns and replies.[4] When the process stays alive, this works beautifully.
But some of the most valuable work you'd want from an agent doesn't fit that shape:
- research tasks that take longer than your SSH session,
- agents watching a repo and acting hours later,
- pipelines where each stage needs sign-off before continuing.
| Execution model | Mechanics | What happens when the process dies |
|---|---|---|
| Simple request | normal agent loop | Process restarts mid-task lose all progress |
| Durable task | Hermes runs the loop inside Restate | Every step is journaled and resumed after a crash |
We have already solved part of this puzzle in-house: when a provider dies mid-turn, Hermes switches models on a properly separated fallback chain (see Hermes Provider Fallbacks). That fixes the model hop. It does nothing for every other way a long task can fall over.
What Restate adds around Hermes
Durable execution. Your Hermes loop becomes a Restate handler. As it runs, Restate journals each step; on crash or restart, a fresh process replays the journal — completed steps are skipped, in-flight ones resume. No special infrastructure, no workflow DSL: plain TypeScript functions with a ctx.[1]
$ npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
$ restate-server & # ingress :8080 · UI :9070
$ npm install @restatedev/restate-sdk
$ restate deployments register http://localhost:9080After registration, every invocation is durable and observable at localhost:9070 — no extra telemetry wiring needed.[3]
Simple request Durable task
│ │
▼ ▼
normal agent loop Restate runtime
│
┌──────────────────────┼──────────────────────┐
▼ ▼ ▼
LLM MCP Agents
│ │ │
└─────────── durable execution ──────────────┘
│
wait / schedule
│
human approval
│
resumeThe five pillars, concretely
| Pillar | What you get | How it's expressed |
|---|---|---|
| LLM calls | Responses persisted in the journal, replayed on recovery[1] | ctx.run("LLM call", …) or durableCalls(ctx) middleware |
| MCP / tools | Side effects run exactly once even across retries[2] | Wrap each tool execution in ctx.run() |
| Time | Timers survive restarts — no cron sprawl, no lost sleeps[3] | await ctx.sleep(ms) between durable steps |
| Human in the loop | Suspend for hours or days; resume on approval signal[3] | Signals, awakeables, workflow promises |
| Observability | Step-by-step trace of every LLM call, tool run, and wait[1] | Restate UI at localhost:9070 |
Getting started with Hermes + Restate
The details below describe our target stack. If you reproduce this before we publish part two, expect small API drift.
Before adding durability, prove that Hermes works plainly:[4]
hermes --version
hermes model # confirm primary provider
hermes chat -q "Reply with PRIMARY_OK"Two concrete examples
Example 1 — a durable repo watcher
The pattern: register one durable workflow per watched repository. Its run handler fires up a long-lived loop, and each wake-up gets its own journaled start-to-finish. In the sketch below, hermesExecute stands in for Hermes doing real work; swap in the calls your Hermes deployment uses there.
import * as restate from "@restatedev/restate-sdk";
import { hermesExecute } from "./hermes-adapter"; // thin wrapper around your Hermes invocation
// One durable workflow run (one key) per watched repository.
export const repoWatcher = restate.workflow({
name: "RepoWatcher",
handlers: {
// POST /RepoWatcher/{repoUrl}/run — starts watching one repo
run: async (ctx: restate.WorkflowContext, repoUrl: string) => {
const label = `watcher:${repoUrl}`;
while (true) {
// Durable wait — suspends the task completely, survives restarts
await ctx.sleep(6 * 60 * 60 * 1000); // 6 hours
// Durable step — journaled, retried until success, never duplicated
const commits = await ctx.run(`${label}: fetch new commits`, () =>
fetchNewCommits(repoUrl)
);
if (commits.length === 0) continue;
const review = await ctx.run(`${label}: Hermes review`, () =>
hermesExecute({ prompt: buildReviewPrompt(repoUrl, commits) })
);
await ctx.run(`${label}: publish notes`, () =>
postToTeamChannel(review, repoUrl)
);
}
},
},
});
restate.serve({ services: [repoWatcher], port: 9080 });Each branch — LLM, MCP, Agents in the diagram above — can fail independently, yet the watcher keeps its place. A deploy in the middle changes nothing: on restart, Restate replays completed steps like fetchNewCommits and continues from wherever the journal says it stopped.
Example 2 — an agent pipeline with human approval
Multi-stage agents often need human sign-off between stages. With Restate, waiting for approval is cheap and suspends rather than consumes a machine: the task parks, survives restarts, and resumes once you approve.
// Inside the same durable pipeline: draft -> review -> publish
export const contentPipeline = restate.service({
name: "ContentPipeline",
handlers: {
produceDraft: async (ctx: restate.Context, topic: string) => {
const draft = await ctx.run("hermes draft", () =>
hermesExecute({ prompt: `Write a technical deep-dive on ${topic}.` })
);
// Build an external callback token, then hand it to a reviewer
const { id, promise } = ctx.awakeable<string>();
await ctx.run("notify reviewer", () => sendSlackReviewRequest(topic, draft, id));
// Task suspends here — minutes, hours, or days cost nothing
// Approver resolves over HTTP:
// curl $RESTATE/awakeables/<id>/resolve --json '"Approved"'
const decision = await promise;
if (decision !== "Approved") throw new restate.TerminalError("Rejected");
return ctx.run("publish", () => publishPost(draft));
},
},
});Connecting MCP servers through Restate
Hermes can connect to external tool servers so the agent can use tools living outside Hermes itself — GitHub, databases, file systems, browsers.[2] Wrap each such side effect in ctx.run(), and the result is journaled. On recovery, the result is replayed rather than the call repeated, which keeps flaky third-party tools from duplicating writes:
// Any SDK's MCP client works — the durability comes from the Restate context
async function mcpToolCall(
ctx: restate.Context,
server: string,
toolName: string,
args: Record<string, unknown>,
) {
return ctx.run(`MCP ${server}/${toolName}`, () =>
mcpClient.callTool(server, toolName, args) // your Hermes-configured MCP client
);
}Plain Hermes vs. Hermes on Restate
| Situation | Plain Hermes | Hermes + Restate |
|---|---|---|
| 1 · Fan-out | Three MCP tools fire — web_search, browser tool, git clone. Process is killed by an OOM killer. | Each fan-out is a ctx.run step; a new worker replays the journal and only re-runs unfinished steps. |
| 2 · Provider blip | Rate limit kills the synthesis call; whole research task restarts from zero. | ctx.run retries with backoff; on success the response is journaled and never refetched. |
| 3 · Scheduled check | "Check back tomorrow" is impossible without external crons and glue code. | await ctx.sleep(duration) — timer survives restarts; Restate wakes the task. |
| 4 · Human approval | Agent blocks or polls; state lives in chat history that may be gone next week. | Task suspends on a signal/awakeable; approvals arrive over HTTP to resume it. |
Choosing a coordination primitive
Restate offers three primitives for “wait for something outside the loop”:[3]
| Primitive | Addressed by | Best for |
|---|---|---|
| Signal | invocation ID + signal name | One-off approvals, agent steering ("stop searching, write up") |
| Awakeable | generated one-shot ID | Callback tokens handed to an external system |
| Workflow promise | workflow key + promise name | Shared results any workflow handler can read during retention |
Where we're taking it
This is no longer hypothetical: our Polaris assistant (a Hermes agent driven over Telegram) now runs a durable lead-discovery pipeline built exactly this way — a scheduled ProspectLoop workflow that scores candidates against our ICP with Hermes, dedupes them through a LeadRegistry virtual object, and drafts outreach behind an approval promise you resolve from chat.
- Simple request — one prompt, one reply — keep Hermes exactly as it is
- Long research briefing — durable context, from-there recovery instead of a do-over
- Watchtower / monitor — service per watched project; delayed wake-up for re-checks
- Human-approval gate — agent suspends days if needed; resumes when you resolve its awakeable
- Self-healing pipeline — object registered inside the loop; every cycle for free forever
Sources
Frequently Asked Questions
What does Restate actually add to a Hermes agent?
Durability around the agent loop. Every LLM call and tool execution becomes a journaled step, so a crashed task resumes exactly where it stopped instead of restarting from zero — plus durable timers, suspending waits, and a step-by-step execution trace in the Restate UI.
Do I have to rewrite my agent logic or change model providers?
No. The handler still contains an ordinary agent loop, and Hermes keeps its configured primary and fallback providers. You wrap side-effecting calls in ctx.run() and expose one Restate service handler; the rest of the logic is unchanged.
How does waiting for days for a human approval cost nothing?
When the only pending thing is an external event, Restate suspends the invocation instead of holding it in memory. On resume, the journaled steps replay instantly. It works like await, not like polling.
Is durable execution only about crash recovery?
No — recovery is just the headline. Scheduled check-ins (ctx.sleep), human-in-the-loop gates (signals and awakeables that you resolve over HTTP when ready), exactly-once tool effects, and per-invocation observability all come from the same journal.