Long-running research agent with durable replay
You’ll build a research agent that plans and runs a multi-step task and, if the process crashes or the host reboots partway through, picks up where it left off instead of starting over. Use this pattern for any run long or expensive enough that losing it to a deploy or an OOM kill would hurt.
import asyncio
from datetime import timedelta
from loomflow import Agent, Tuning, tool
from loomflow.architecture.tool_host_wrappers import ExtendedToolHost
from loomflow.governance.budget import BudgetConfig, StandardBudget
from loomflow.mcp import MCPRegistry, MCPServerSpec
from loomflow.runtime import SqliteRuntime
# A fixed id: every restart must reuse it to find the saved progress.
SESSION_ID = "research-agent-harnesses-2026"
TASK = "Research the state of agent harnesses in 2026."
@tool
async def web_search(query: str) -> str:
"""Search the web."""
...
@tool
async def fetch(url: str) -> str:
"""Fetch a URL."""
...
mcp_servers = MCPRegistry([
MCPServerSpec.stdio(
"fs", "npx", ["-y", "@modelcontextprotocol/server-filesystem", "./notes"]
),
])
async def main():
agent = Agent(
"You are a research assistant. Plan a multi-step research task, "
"execute each step with the available tools, then summarize.",
model="claude-opus-4-7",
runtime=SqliteRuntime("./research_journal.db"),
tuning=Tuning(checkpoint=True),
tools=ExtendedToolHost(mcp_servers, [web_search, fetch]),
budget=StandardBudget(BudgetConfig(
max_tokens=500_000,
max_cost_usd=20.0,
max_wall_clock=timedelta(hours=2),
)),
)
if await agent.list_checkpoints(SESSION_ID):
# A previous process got partway: continue from its last checkpoint.
result = await agent.resume(session_id=SESSION_ID)
else:
result = await agent.run(TASK, session_id=SESSION_ID)
print(result.output)
asyncio.run(main())Run the script; if it dies partway, run it again. Three pieces make the restart work:
- A fixed
session_id.agent.run()mints a new session id when you don’t pass one, and a new id has no saved progress. ReusingSESSION_IDis what lets the second process find the first one’s work. In a real app, derive it from something stable, such as a job or ticket id. If your runs passuser_id=, pass the same one tolist_checkpoints()andresume()too: journals and checkpoints are stored per session and user. Tuning(checkpoint=True)on a durable runtime. After every pass of the agent loop, the transcript, turn count and usage are saved toresearch_journal.db.agent.resume()restores the latest snapshot, so completed turns are never re-run or re-billed; with no prompt, it tells the model to continue the interrupted task. See Checkpoints.- The journal.
SqliteRuntimealso records each model call and tool dispatch, so a step that re-fires with identical inputs after the restore point returns its saved result instead of executing again. Only an unfinished run replays this way: once a run on the session finishes, a lateragent.run()on it executes its steps again instead of replaying them.
ExtendedToolHost combines the MCP servers with the local
@tool functions into one tool host. The budget caps each process at
500k tokens, $20 and two hours of running time; because restored
turns aren’t re-billed, a resumed process counts only the model calls
it makes itself.
Last updated on