Serverless Agents with PydanticAI and ResonateHQ
Deploying agents that can sleep, pause waiting for human input or distribute computation to expensive, dedicated machines, for only the tasks that matter
Note: You can find a ready to use example here
PydanticAI is one of the most popular AI agent frameworks in the ecosystem. Meanwhile, Resonate is a durable execution engine that brings distributed async/await semantics to your code, offering a remarkably simple developer and hosting experience.
One of the biggest advantages of Resonate is deployment flexibility: you can run the exact same code in a long-running server or a serverless environment like AWS Lambda. Because the framework handles retries, recovery, and execution state natively, a serverless runtime effectively behaves like a resilient, long-running process.
That got me wondering: what if PydanticAI integrated with Resonate? Could AI agents gain the same deployment flexibility?
Since PydanticAI tools are essentially just function calls, and Resonate’s core abstraction is durable function execution, the integration felt like a natural fit. Here is how it works.
Starting Simple: A PydanticAI Agent
PydanticAI agents are intentionally straightforward to build:
Instantiate an
Agent.Provide a model and a set of instructions.
Run the agent with a prompt.
At this stage, your agent is only as capable as the model you’ve selected and only as knowledgeable as the context provided to it. Everything else—tools, memory, retrieval, workflows—is layered on top.
When you extend the agent with tools, PydanticAI keeps things clean. Tools are just standard Python functions decorated with @agent.tool (or @agent.tool_plain when they don’t need execution context). Dependencies are injected per run through a strongly typed deps object. This means a tool like lookup_order never has to rely on global state; it simply reads from ctx.deps.
Outputs don’t have to be unstructured text, either. By passing output_type=Triage, PydanticAI validates the model’s response against a Pydantic schema, handing you a strongly typed object instead of a raw string.
At this point, you have a real AI agent: it calls tools, reasons over their results, and returns structured data. It’s a great developer experience and makes for a compelling demo.
Production, however, is a different story.
Agents need to scale, tolerate failures, and recover from crashes without losing progress. Durability solves half of that equation, and scalability solves the rest. Platforms like AWS Lambda give you cheap horizontal scale and burst capacity, while Resonate adds durable execution on top. With both, you don’t have to choose.
The Integration, in Two Lines of Code
The starting point is almost anticlimactic. Because a Resonate workflow and a PydanticAI tool are both just standard Python functions, any function you’d register with resonate.register(...) can be exposed directly to an agent as a tool. No adapters or wrappers are necessary.
Adopting Resonate only requires changing two lines in your calling code:
Your tools, your agent, and your output types remain untouched. The id you pass to run(...) becomes the workflow’s identity. If you invoke run(...) again with the same id, Resonate returns the existing result instead of executing the workflow a second time.
That small addition has massive implications.
What “Durable” Actually Means: Checkpoints and Replay
When PydanticAI invokes an ordinary tool, the runtime treats it as an opaque function call. It executes, returns a result, and finishes. If the process crashes mid-run, the entire turn is replayed, including every tool call.
A Resonate workflow behaves differently. It’s worth being precise about how, because this mechanism underpins everything else:
A durable run is driven by replay.
When a suspended run resumes, Resonate re-executes the workflow body from the top. However, every step that has already completed returns its recorded result instead of running again.
The recorded steps include model requests and
ctx.*calls (likectx.run,ctx.rpc,ctx.sleep, andctx.promise). Everything between those steps—including plain@tool_plainlogic likelookup_order—simply re-runs. That’s perfectly fine when the logic is cheap and pure, but it becomes a problem if it relies on side effects.
If a crash occurs after an issue_refund tool returns, but before the model consumes its output, recovery won’t reissue the refund. Instead, it replays the recorded result from the journal and continues seamlessly. From the agent’s perspective, the control loop remains unchanged; the underlying tool simply stops being ephemeral.
The Determinism Contract
The Rule: The workflow body must be deterministic across replays.
Anything non-deterministic or side-effecting must go through a
ctx.*step so its result is checkpointed. Wrap an HTTP call inctx.run(...); get the current time from a durable step; and always usectx.sleepinstead oftime.sleep.
This rule separates a fragile demo from a production-ready application. If you write a tool that calls random, uses datetime.now(), or fires an HTTP POST inline, you’ll be surprised on replay: the random number will change, the timestamp will drift, and the HTTP call will fire twice.
The AI model itself is inherently non-deterministic, but because its response is checkpointed, the replay remains perfectly stable. The LLM only sees the world through recorded, replayable steps.
Exactly-Once Side Effects: The Real Payoff
The greatest benefit of durable execution isn’t just avoiding recomputed work; it’s guaranteeing that side effects are recorded exactly once. Think charging a credit card, sending an email, or issuing a refund.
If a refund is dispatched using ctx.rpc(process_refund, ...), and the process dies right after the charge settles but before the model reads the result, recovery simply replays the recorded rpc result. The card is never charged twice. It’s the same tool, the same agent, and the same output type, but wrapping it in ctx.rpc is the difference between a reliable idempotent agent and an angry support ticket.
ResonateAgent: Tying the Two Worlds Together
ResonateAgent wraps a standard PydanticAI Agent and executes each run(...) as a durable Resonate workflow. Model requests are checkpointed, and tool invocations execute safely inside the workflow.
This introduces the most important property of the integration: execution is no longer bound to a specific process.
If a worker crashes—or is simply torn down because it’s running on an ephemeral AWS Lambda instance—another worker picks up the pending workflow and continues from the exact point the previous one stopped. The workflow’s identity lives in its durable execution state, not the machine running it.
Workers become completely disposable, while agent executions remain completely durable.
Durable Primitives Inside a Tool
Because a tool executes inside the workflow itself, it has full access to Resonate’s durable execution context. Tools aren’t limited to standard API calls and database queries; they can use Resonate primitives to coordinate long-running workflows directly.
In the example below, a refund tool retrieves the ambient workflow context via workflow_context() and interacts with the durable runtime:
Here is how those primitives work:
ctx.sleep(...): Creates a durable timer. The workflow is suspended without consuming compute, freeing the worker to terminate. Once the timer expires, execution resumes exactly where it left off.ctx.promise(...): Creates a durable suspension point. The tool generates a promise, hands the ID to an external actor (a human reviewer, a webhook, a Slack button), and pauses. No process needs to stay alive. Whether fulfilled in seconds or days, the workflow resumes on the next available worker.ctx.rpc(...): Invokes another durable workflow. The actual refund can execute as a separate, checkpointed workflow—potentially on a different worker pool or microservice—while remaining part of the original durable execution.
None of these primitives are specific to AI. They are the exact same tools Resonate provides for standard distributed workflows. Under the hood, this “simple” tool might wait a day for human approval, survive multiple deployments, and delegate work across services, all without the agent knowing anything about the underlying durable execution.
How This Actually Runs on Lambda
This architecture relies on two key serverless concepts:
Push, not poll: The Resonate server pushes one task at a time to the function’s URL. There is no worker sitting in an active loop. When a run is suspended (e.g., parked on
ctx.sleeporctx.promise), it returns the Lambda entirely. Nothing is left running.You don’t pay to wait: A human-approval refund can be suspended for a day on a durable promise and cost zero compute. No Lambda is active, no server is blocked, and no connection is held open. When the reviewer clicks approve, the next invocation resumes from that exact point.
A day-long human approval that costs nothing while it waits is arguably the single most compelling advantage of combining serverless computing with durable execution.
One Agent Run, Several Invocations
The concept of “one agent run = several Lambda invocations” is easiest to understand visually. Here is a full run crossing the serverless boundary:
Every arrow pointing into
λis a completely fresh Lambda invocation.Every arrow pointing out where
λshuts down represents compute you are no longer paying for.Every time the function resumes, it replays from the journal, ensuring previously checkpointed model requests are not re-executed.
Limitations and Things to Know
There are a few edge cases to keep in mind:
Segment Latency: Everything between two suspension points must finish inside a single invocation. Be sure to size your Lambda
Timeout(and the Resonate leasettlabove it) to accommodate your slowest model call plus any inline@tool_plainwork.Model Non-Determinism: While the model itself is non-deterministic, its response is checkpointed to keep replays stable. The determinism contract only strictly applies to the workflow body executing around the model.
Async Only:
run_syncis not supported. Resonate runs on asyncio, so you must useawait agent.run(...).
Wrapping Up
On the surface, this integration is incredibly lightweight: wrap the agent and give the run an ID. Underneath, it’s the piece that makes the rest of the system work.
Your tools remain ordinary Python functions. Your agent remains an ordinary PydanticAI Agent. What changes is that a crash no longer replays an expensive API call, a credit card never gets charged twice, and a human-approval step doesn’t require an active process sitting idle for a day.
It’s the exact same code on a server as it is on Lambda. Disposable workers, durable agents.








