Skip to Content
RecipesLong-running research agent

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. Reusing SESSION_ID is 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 pass user_id=, pass the same one to list_checkpoints() and resume() 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 to research_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. SqliteRuntime also 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 later agent.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