Restate is a serious durable runtime. Polyglot SDKs across TypeScript, Java, Kotlin, Go, Rust, and Python. Three handler shapes (services, Virtual Objects with keyed per-entity state and strongly-consistent access, and workflows), journaled execution, awakeables for human-in-the-loop, OpenTelemetry tracing, self-hostable on every major cloud. If your durability problem is polyglot and backend-services-shaped, Restate is a strong, honest answer.
Kitaru isn’t a durable runtime at all. It answers a different question: once an agent run happened — durable or not — can you re-run it, and know whether a change made it better or worse? Kitaru records runs as sessions, through a framework adapter or by importing the traces you already collect, and replays them against your real code.
The two layers don’t compete. A Restate-run agent can still be wrapped by a Kitaru adapter, or have its traces imported — Restate keeps it alive in production, Kitaru tells you whether the next change to it is safe to ship.
Use Kitaru if you are
- Testing a model, prompt, or code change to a Restate-run agent against real production sessions instead of a vibe check
- Recording agent runs via a framework adapter (PydanticAI, LangGraph, OpenAI Agents SDK, Mastra, Vercel AI SDK), or importing traces from Langfuse, LangSmith, Braintrust, or OTel
- Freezing the sessions that caught a regression into a cohort that gates every future change
- Self-hosting the eval infrastructure so traces and credentials stay in your own systems
Use Restate if you are
- Running a polyglot services stack (TypeScript, Java, Kotlin, Go, Rust, Python) that needs one durability primitive across all of it
- Doing durable RPC, sagas, or long-running backend workflows — not testing changes to an agent
- Modeling keyed per-entity state with strongly-consistent access (chat sessions, orders, devices, users) as a first-class abstraction
- Fine running `restate-server` as infra and using awakeables / durable promises as the core HITL primitive
Restate keeps your services — agents included — durable in production. Kitaru replays what they did in production to prove whether your next change helps.
Different layers
Defaults tell you what a tool is actually for. Restate’s defaults are services, Virtual Objects, and workflows — real durability primitives, shaped for backend app workloads. Kitaru doesn’t have durability primitives at all. What it has is a way to turn a run that already happened into something you can re-run and score.
- What Kitaru ships: an adapter that wraps your agent with no rewrite, a session recorded on every run, and replay with cohorts and experiments on top.
- Framework adapters: PydanticAI, LangGraph, the OpenAI Agents SDK, Mastra, and the Vercel AI SDK ship today; anything else comes in through a trace import.
- Both are real tools, aimed at different problems. What ships in the box tells you what the team was watching when they set the defaults.
From one session to a regression suite
Both tools persist state, for different reasons. Worth saying clearly.
- Restate: handler progress is journaled for deterministic replay during recovery. Virtual Objects expose keyed per-entity state with strongly-consistent access and single-writer semantics per object — the right shape for “the chat session,” “the order,” “the device.”
- Kitaru: a session is a recording of a finished run. Freeze the sessions that matter into a cohort, replay that cohort with one thing changed, and compare the two runs side by side — the regression suite that catches the next bad deploy.
- Honest read: VO-keyed per-entity state and replayable sessions aren’t two answers to the same question. One is state for a run in progress; the other is a recording of a run that’s done.
One adapter, many frameworks
Restate’s polyglot reach is real — six languages, one durability contract. Kitaru is narrower by design: it meets agents where most teams are actually building them today.
- Wrap it once:
KitaruAgent(agent, agent_id=...)wraps a PydanticAI, LangGraph, or OpenAI Agents SDK agent with no rewrite. Mastra and the Vercel AI SDK get native-signature TypeScript adapters. - No adapter for your framework? Import the traces you already send to Langfuse, LangSmith, or Braintrust, or emit OTLP and import that instead. Either path lands as a replayable session.
- The trade: polyglot backend teams often want exactly Restate’s model. Agent teams usually want their existing framework wrapped once, not a new one to adopt.
Embedded recording vs server-registered handlers
Both tools have a server. The wedge is whether your agent’s request path has to cross it.
- Kitaru:
uv add kitaru, wrap the agent with an adapter, and it runs in your process exactly as before. The Kitaru server stores session metadata and powers replay, the UI, the CLI, and auth — it isn’t a proxy every invocation crosses. - Restate: handlers register with
restate-server, and invocations flow through it. The server journals progress, routes requests, handles retries, and drives replay. That’s not incidental; it’s the durability model. - Different jobs, different shapes. Restate’s server is in the request path because durability requires it. Kitaru’s server holds metadata because recording doesn’t.
What makes Kitaru unique
| Feature | Kitaru | Restate |
|---|---|---|
| Durable execution and replay for production services | Not supported | Yes |
| Virtual Objects: keyed per-entity state with strongly-consistent access | Not supported | Yes |
| Awakeables and durable promises for human-in-the-loop | Not supported | Yes |
| Polyglot handler SDKs (TypeScript, Java, Kotlin, Go, Rust, Python) | Not supported | Yes |
| OpenTelemetry tracing (native instrumentation) | Not supported | Yes |
| Records agent runs as replayable sessions (adapter or trace import) | Yes | Not supported |
| Replays a session against real code, tool calls answered from the recording | Yes | Not supported |
| Cohorts: frozen session sets as regression suites | Yes | Not supported |
| Experiments: same cohort, one variable moved, compared | Yes | Not supported |
| Deterministic + model-graded evaluators | Yes | Not supported |
| Imports Langfuse / LangSmith / Braintrust / OTel traces | Yes | Not supported |
| Self-hostable (Kitaru: Apache 2.0; Restate: BSL 1.1, Apache 2.0 as the change license) | Yes | Yes |
How the two surfaces map
| Concept | Restate | Kitaru |
|---|---|---|
| Layer | Durable execution for services (keeps handlers alive in prod) | Replay-based evals (tests a change to the agent) |
| Durable unit | Handler (Service / Virtual Object / Workflow) | Session — recorded run, re-executable |
| Per-entity state | Virtual Object (keyed, strongly-consistent) | Not applicable — Kitaru doesn’t hold production state |
| Human-in-the-loop | Awakeable / durable promise | Not applicable to replay; recorded if the original run had one |
| Recording | N/A — Restate orchestrates, doesn’t record for eval | A framework adapter wraps the agent, or traces are imported |
| “Did my change help?” | Not what Restate answers | Two runs over the same cohort, diffed |
| Deployment | Self-hosted restate-server + registered handlers | Adapter embedded in your process; server + workers self-hosted |
Code comparison
from pydantic_ai import Agent
from kitaru_pydantic_ai import KitaruAgent
reviewer = KitaruAgent(
Agent("openai:gpt-5.4", system_prompt="You're a compliance reviewer."),
agent_id=AGENT_ID,
)
# Restate keeps this durable in production.
# Every run also lands as a Kitaru session.
result = reviewer.run_sync(case)
# Later: freeze the sessions that matter, test a change.
import kitaru
client = kitaru.KitaruClient()
cohort = client.cohorts.create("hard-cases", sessions=session_ids)
experiment = client.experiments.create(
"cheaper-model",
model="gpt-5-mini",
tool_policy=History(scope="cohort", on_miss="fail"),
)
before = experiment.run(cohort=cohort, version="v1")
after = experiment.run(cohort=cohort, version="pr-311")
client.compare(before, after)import restate
review = restate.Workflow("ReviewWorkflow")
@review.main()
async def run(ctx: restate.WorkflowContext, topic: str) -> str:
# Journaled step. Non-deterministic work lives inside ctx.run().
brief = await ctx.run(
"research",
lambda: call_llm(f"Research: {topic}"),
)
text = await ctx.run(
"draft",
lambda: call_llm(f"Draft: {brief}"),
)
# Awakeable: durable promise resolved by an external caller.
approval_id, approval = ctx.awakeable()
send_approval_link(approval_id, text)
approved = await approval
return text if approved else "Rejected"
app = restate.app([review])
# Register the workflow with restate-server, deploy the handler,
# and resolve the awakeable from wherever approvals come in.Put replay-based evals under your Restate-run agents
If your durability problem is shaped like a polyglot backend with keyed per-entity state, Restate is the right tool for that job. If the question is whether the change you’re about to ship to a Restate-run agent actually helps, Kitaru replays the sessions that already happened — record them once, via an adapter or an import, and every future change gets tested against real production behavior instead of a guess.
We’ve spent five years building the MLOps-ready version of this problem at ZenML. Kitaru is that same team, applied to the agent-eval layer.
uv add "kitaru[cli,worker]" kitaru-pydantic-ai