All posts
Reliability Engineering

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 modelMechanicsWhat happens when the process dies
Simple requestnormal agent loopProcess restarts mid-task lose all progress
Durable taskHermes runs the loop inside RestateEvery 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:9080

After 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
                                 │
                              resume

The five pillars, concretely

PillarWhat you getHow it's expressed
LLM callsResponses persisted in the journal, replayed on recovery[1]ctx.run("LLM call", …) or durableCalls(ctx) middleware
MCP / toolsSide effects run exactly once even across retries[2]Wrap each tool execution in ctx.run()
TimeTimers survive restarts — no cron sprawl, no lost sleeps[3]await ctx.sleep(ms) between durable steps
Human in the loopSuspend for hours or days; resume on approval signal[3]Signals, awakeables, workflow promises
ObservabilityStep-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

SituationPlain HermesHermes + Restate
1 · Fan-outThree 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 blipRate 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 approvalAgent 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]

PrimitiveAddressed byBest for
Signalinvocation ID + signal nameOne-off approvals, agent steering ("stop searching, write up")
Awakeablegenerated one-shot IDCallback tokens handed to an external system
Workflow promiseworkflow key + promise nameShared 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 requestone prompt, one reply — keep Hermes exactly as it is
  • Long research briefingdurable context, from-there recovery instead of a do-over
  • Watchtower / monitorservice per watched project; delayed wake-up for re-checks
  • Human-approval gateagent suspends days if needed; resumes when you resolve its awakeable
  • Self-healing pipelineobject registered inside the loop; every cycle for free forever

Sources

  1. Durable Agents — Restate documentation
  2. MCP (Model Context Protocol) — Hermes Agent documentation
  3. Signals and external events — Restate documentation
  4. Quickstart — Hermes Agent documentation

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.