AI Engineer Code 2025

From Stateless Nightmares to Durable Agents

Read the talk

From Stateless Nightmares to Durable Agents

A game of twenty questions exposes the restart problem in long-running agents. Temporal preserves completed work, while Python keeps control of the research workflow.

From a talk by Samuel Colvin

Before you start: Familiarity with Python async functions, LLM tool calls, and basic process failures will help; prior Temporal experience is not required.

When restarting becomes the problem

What happens when an agent dies after doing useful work? For a single question and response, starting again may be acceptable. For a longer workflow, restarting means paying for completed computation again—and making the user wait through it again. Samuel Colvin explores that boundary with Pydantic AI, Temporal, Pydantic Logfire, and Pydantic Evals. At the time of the recording, Pydantic AI supports Temporal and DBOS, with roughly five pull requests proposing other execution or orchestration backends.

Deep research is the motivating workload: enough time and computation accumulate that losing progress becomes expensive. Colvin tentatively identifies Temporal as infrastructure behind OpenAI’s deep research; that attribution is not established here. His own demonstration starts smaller, with a game whose intermediate work is easy to follow, before building a research pipeline.

0:000:16
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

0:00 · section reference included

Two agents try to identify a potato

The game has two agents. An answerer knows the secret object; a questioner must discover it. Rather than restricting replies to yes or no, the answerer can give qualified responses such as kind of or completely wrong. The extra information helps shorten otherwise slow games. The answerer uses Claude Haiku 3.5, and the secret—potato—is added to its context.

Python editor showing the Answer enumeration, answerer agent configuration, and a function adding the secret object to its instructions.Open full source frame
The twenty-questions answerer defines its response choices and receives the secret object through context.

The questioner receives instructions for playing and asks questions through ask_question. That tool runs the answerer agent and returns its response, so one agent’s tool call becomes another agent’s inference request. Colvin points to the public companion examples, then still in a pull request he intends to merge after the demonstration.

One run narrows the object through fruit versus vegetable, rules out fruit, asks whether it is orange, and eventually reaches potato. Other runs wander. Colvin reports that the game sometimes takes around fifty steps. If an endpoint fails or Kubernetes terminates the process during scaling, rerunning the ordinary script starts the game from scratch. Every completed exchange disappears from the new run’s working history.

The toy has the same dependency structure as deep research. Each answer informs the next question; a search or retrieval step plays the role of the answerer. Colvin likens it to asking the troll at the bottom of the garden for the next riddle on a quest. Replace that intermediary with web search or RAG, and preserving the chain of discoveries becomes a practical infrastructure requirement.

1:161:28
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

1:16 · section reference included

Record external work, replay the control flow

The durable version wraps both agents in TemporalAgent, which behaves like a Pydantic AI agent and subclasses AbstractAgent. For simplicity, this version puts potato directly into the answerer’s system prompt rather than passing it through context. The agent logic otherwise stays much the same. This is the recording’s API: the current Temporal integration guide recommends TemporalDurability and deprecates the wrapper approach.

Temporal separates workflows from activities. Workflow code must be deterministic so it can reproduce the same control flow during replay. External I/O and nondeterministic operations belong outside that replayable logic, in activities. Temporal records activity inputs and results; when it reconstructs a workflow, it supplies the recorded answers instead of repeating completed external operations.

Python code showing the questioner wrapped in TemporalAgent, the TwentyQuestionsWorkflow class, and client and worker setup below.Open full source frame
A TemporalAgent wrapper and the twenty-questions workflow.
ComponentResponsibilityDuring replay
WorkflowBranches, sequencing, orchestrationRuns again deterministically
Completed activityExternal operation with a recorded resultReturns the recorded result
TemporalAgentTurns model and tool I/O into activitiesPreserves the agent-facing interface

The wrapper makes the activity boundaries largely implicit, including the tool call that invokes the second agent. Colvin criticizes OpenAI’s then-current integration for lacking tool activities. The narrower distinction matters: a dated Temporal support example documents explicit activity_as_tool wrapping, so the blanket claim should not be read as an absence of any tool-activity support. Automatic wrapping and explicit adapters are different integration choices.

The runtime has three roles: a server stores execution history, a worker runs workflow code and activities, and a client starts or waits for workflows. Colvin connects to a separate local open-source Temporal server through localhost; he can reset this demo server to clear its state. He presents Temporal Cloud as a production deployment option. The launch call is execute_workflow, supplied with the workflow and its inputs. This version of twenty questions has no launch inputs because the answer is already in the prompt.

4:064:22
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

4:06 · section reference included

Retry a failure, then kill the process

The first durable run immediately raises an exception. Colvin has deliberately made the tool fail with a 20% probability. Temporal retries, but the demonstration then appears to stop progressing; the cause is not established. He resets the local server and reruns with a 10% injected failure probability. This time, questions continue despite occasional errors. These are configured fault-injection probabilities, not observed production failure rates.

Retries alone could be implemented without Temporal. The stronger test is to terminate the agent process while it is working and preserve the existing workflow. Colvin kills the process, then opens Pydantic Logfire. Its trace shows the Claude requests and the nested activity running the answerer. Recovery proceeds through the workflow’s identity:

  1. Open the top-level workflow-start trace and copy its workflow ID.
  2. Pass that ID to the demo script’s resume option.
  3. Restart the worker and wait for that existing workflow to finish.

The explicit ID is a convenience for this launch script: running it normally would start another workflow alongside the unfinished one. It is not resume logic added throughout the agents.

The restarted run quickly reaches question six and continues. Colvin reports roughly five-millisecond responses for the replayed LLM calls. Those responses come from recorded activity results, not fresh inference. The workflow executes its ordinary procedural logic, consumes the saved answers, and moves forward until it reaches work that still needs external I/O. Recovery replays the program while reusing completed work. It does not ask the model to reconstruct its previous reasoning.

7:067:22
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

7:06 · section reference included

A durable agent can still be wrong

The recovered questioner keeps running, but its reasoning is not improving: it starts exploring whether the object might be a salad bowl. That leads into a separate question—how should the models be compared? Colvin uses Pydantic Evals for twenty questions, showing GPT-4.1, Gemini, and Claude Sonnet 4.5. The displayed results include assertion outcomes, average cost, speed, and question count.

Gemini initially appears cheaper and faster, reaching an answer in fewer questions. Colvin subsequently inspects its answers and discovers why: it invents an incorrect answer, and his evaluation was not checking correctness. The apparent advantage therefore does not establish better task performance. The current companion evaluation includes normalized answer-equality checking, but that does not repair the comparison displayed in the recording.

Meanwhile, Colvin leaves the durable game after forty-six steps without a correct identification of the potato. Persistence has protected the sequence of operations; it has not made those operations useful. For this task, correctness must constrain the interpretation of question count, latency, and cost.

10:4410:55
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

10:44 · section reference included

Build research from smaller agents

The next example replaces the game with a deep-research pipeline, built as a quick prototype the previous night. Colvin considers its output competitive with other research systems, though he does not present a quality evaluation. It begins with a structured plan: an executive summary describing the intended work, a bounded list of web-search steps, and analysis instructions. The five-step limit keeps the research from running indefinitely. A compact Python representation is:

python

from pydantic import BaseModel, Field


class DeepResearchPlan(BaseModel):
    executive_summary: str
    web_search_steps: list[str] = Field(max_length=5)
    analysis_instructions: str

The schema gives the next stages concrete fields to consume rather than requiring them to parse a free-form planning essay.

Python editor showing the DeepResearchPlan fields and a planning agent configured to output that model.Open full source frame
The deep-research plan defines an executive summary, web-search steps, and analysis instructions.

This decomposition depends on what agent means. Colvin distinguishes three common uses:

  • AI: An LLM calling tools in a loop.
  • Engineering: A microservice.
  • Business: Something that can replace a human.

He describes a shift away from assuming one tool-calling loop per microservice. Smaller agents become units of development that combine into a larger autonomous task. Here, deep research is the application assembled from those units.

AgentJobOutput or capability
PlannerTurn the request into a planA Pydantic model instance
SearcherInvestigate a planned stepSearch tools and result text
AnalystSynthesize the findingsFinal research report

The searcher uses Gemini Flash for speed; the analyst uses Claude Sonnet 4.5. Colvin keeps the orchestration in ordinary Python because this control flow does not require a graph. For persistence, he prefers the finer activity boundaries of durable execution over graph-level snapshotting. The analyst also has a tool for further web search, although he does not think it uses it in this example.

12:1912:34
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

12:19 · section reference included

Plan, search in parallel, then synthesize

The orchestration has three phases. First, await the planner. Next, launch the planned searches concurrently using Python’s TaskGroup. After the group finishes, collect the search outputs. The concurrency portion can stay this small:

python

import asyncio
from collections.abc import Awaitable, Callable, Sequence


async def search_all(
    steps: Sequence[str],
    search: Callable[[str], Awaitable[str]],
) -> list[str]:
    async with asyncio.TaskGroup() as group:
        tasks = [group.create_task(search(step)) for step in steps]

    return [task.result() for task in tasks]

The task group waits for the searches; the result list preserves their planned order. Colvin then uses format_as_xml to package the collected text into readable context for the analysis agent, which produces the final synthesis.

The request is a sales-research question: find hedge funds that write Python in London. Colvin launches the script with uv run and inspects its progress in Logfire. In the ordinary research run, Colvin observes a nine-second planning step. The trace then shows searches running concurrently, followed by the analysis agent once their results are available. Individual search traces expose the questions, generated queries, retrieved material from sites including Medium, and additional context supplied to the agent.

During that inspection, Colvin refers to ten parallel searches, although the plan shown earlier is capped at five search steps. The distinction is not explained, so the demonstrated bound should be understood as a limit on planned steps, without inferring an exact count of underlying search operations. While analysis is still running, Colvin observes eight cents of cumulative spend. That is an interim cost, not the price of a completed report. Killing this ordinary process would discard its progress and require another run from the beginning.

14:5715:10
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

14:57 · section reference included

Keep the Python, add durable boundaries

The Temporal research version follows the same pipeline, with one recording-specific substitution. Colvin reports a Vertex SDK incompatibility with Temporal and switches to OpenAI Responses plus Tavily instead of built-in search. He duplicates the example for readability, although the shared code could have been imported.

Each agent receives a TemporalAgent wrapper. The analysis activity also receives a one-hour timeout because synthesis had exceeded a shorter limit. Colvin is unsure of that earlier default, so the useful operational point is to size an activity timeout for its actual workload, not to infer a universal default from the demonstration.

Inside the workflow, the orchestration remains imperative Python. The same TaskGroup runs concurrent searches; Colvin also names asyncio.gather as an alternative. A hypothetical seven-day sleep illustrates another benefit: Temporal can manage a durable suspension for periodic work instead of requiring a process to remain alive throughout the wait. The final stage still receives the combined context and runs the analyst.

After registering the agents and observability integrations through plugins, the client again launches with execute_workflow. This time, the question is which Python agent framework is best for durable execution and type safety. The changed research prompt does not change the execution structure: plan, parallel searches, then analysis.

16:5017:06
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

16:50 · section reference included

Recovery stops at the unfinished activity

Logfire shows the workflow starting and searches beginning. Colvin then terminates the process around the transition into the final stage. Instead of submitting the research request again, he restarts the worker and waits on the same workflow. In a deployed system, another available worker can continue the stored workflow after the original process disappears.

In the recovered research run, Colvin observes a twenty-four-millisecond replayed planning step. Completed searches also return their stored results almost immediately. The analysis stage still has to execute: Temporal has no completed result to substitute for that activity.

Work at interruptionRecovery behavior
Completed planReuse its recorded result
Completed searchesReuse their recorded results
Analysis without a completed resultExecute the activity

Durability preserves completed activities, not arbitrary progress inside an activity. If an activity is interrupted before completion, it must run again from its beginning. Choosing the activity boundary therefore determines how much work a failure can force the application to repeat.

The report eventually completes and recommends Pydantic AI with Temporal. It includes an executive summary, links, and discussion of Pydantic AI, LangGraph, and Temporal alone. Colvin’s favorable assessment of the report and jokes about competing frameworks are promotional commentary, rather than comparative findings established by this run. What the demonstration does establish is the completion of research after recovery; a polished interface for presenting that research remains to be built.

18:4818:56
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

18:48 · section reference included

The examples and the forthcoming gateway

Colvin closes by returning to the public pydantic-stack-demo repository and reiterating his plan to merge the durable-execution examples. The repository and on-screen QR code give viewers a route to the code behind the demonstrations.

GitHub repository page for pydantic/pydantic-stack-demo showing example folders, with a QR code beneath the speaker inset.Open full source frame
The public pydantic-stack-demo repository, with an on-screen QR code.

He also previews Pydantic AI Gateway, still forthcoming in the recording, and invites requests for early access. His proposed capabilities include buying inference from major providers and most open-source models, enterprise self-hosting, and observability. This extends the stack beyond orchestrating and inspecting agent work to providing access to the models that perform it.

21:1521:29
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

21:15 · section reference included

Resources

From the talk

Updates since the talk

Transcript download
Read the transcript and explore slides

Loading transcript and slides…