Skip to main content
Long jobs get interrupted. A machine reboots, a deploy rolls, a container is evicted — and your agent was halfway through, having already paid for the tokens and called half its tools. Give the agent a store — somewhere to write down what it did — and each step is saved as it happens. A different process, knowing nothing but the run’s id, can pick the job up and finish it, or branch it into an independent run.
Durable runs, and forking a run into a new one, are part of Agentor’s 0.1.0 engine — the current stable release. Plain pip install agentor gets you both.

Save a run

Pass a store when you build the agent. FileStore keeps one file per run in the directory you name:
agent.py
Every run now gets an id, and runs/<run_id>.jsonl holds the whole story. Keep the id somewhere you can find it again — a database row, a queue message, a log line.
Without store=, result.run_id is None and nothing is written. Runs work exactly as before; they are just not recoverable.

Resume after a crash

resume() takes a run id and continues from the last thing that was saved:
resume.py
The agent object has to be built the same way — same tools, same instructions — but it does not have to be the same process. This is the whole point: the process that started the run can be gone. Two things make resume() safe to call whenever you are unsure:

Finished runs are returned, not re-run

If the run already completed, you get its result back without another model call and without any tool running twice.

Half-finished tool calls are rewound

If the process died between asking for tools and recording every result, the agent goes back to the request that produced them and decides again.
Use await agent.aresume(run_id) from async code.

A worked example

Start a run and kill the process partway through — after the weather tool answered but before the agent wrote its reply:
Four lines made it to disk. Now a separate interpreter, given only that id:
The tool did not run a second time. The saved result was enough.

Fork a run

resume() continues a run in place, and deliberately refuses to re-run one that already finished. fork() takes a different approach: it copies a run’s whole history — every step, including the model’s reasoning where the provider returned it — into a new run id, then continues from there. The original run is never touched, whether it is still in progress, finished, or crashed. That makes fork the way to branch a conversation: try a different instruction from the same starting point without losing the original, or pick up a completed run and keep going, which resume() will not do for you.
fork.py
Use await agent.afork(...) from async code.
As with resume(), the agent doing the forking does not have to be the one that made the original run — but if the original run has no system message, the forking agent’s instructions fill in on the fork.

Fork without new input

Leave out the second argument and fork() just copies the run under a new id:
  • If the parent was still unfinished — interrupted mid-run — the fork picks up where it left off and finishes, exactly like resume() would.
  • If the parent had already finished, the fork is an identical copy of it. No model call is made.

Choosing the new run’s id

Pass fork_id to name the new run yourself instead of getting a generated one:
fork() raises ValueError if that id is already taken. max_turns and tracing behave the same as on run() — they apply only to the fork’s continuation, never to the run being copied.

What gets written

The store records the same events the agent emits while it runs, one JSON object per line:
runs/cabc9046ac5040298aa362c0d6d2c5c5.jsonl
Each generation records the exact messages that were sent to the model — reasoning included, when the provider returns any — so rebuilding the conversation is a replay rather than a guess. FileStore flushes and fsyncs after every line, because a crash is the case it exists for. Forking adds one more line to the copy: a fork event naming the run it came from. It is provenance only — replaying the log or checking whether it is complete works exactly as it does for a run that was never forked. You can read the same events back off any finished result:

Choose a store

One append-only file per run, on local disk. The right default for a worker, a cron job, or anything with a volume attached.

Streamed runs are saved too

stream_chat() persists a run whenever the agent has a store, so a stream that a client abandons is still recoverable:

Limits to know about

The bundled stores are single-process and take no lock. If two workers both see a run as incomplete, both will continue it, and any tool with a side effect runs twice. Coordinate outside Agentor — a queue with visibility timeouts, a row lock, a lease — if more than one worker can recover the same run.
Like resume, fork takes no lock. Fork a run that is actively being written to and you get a copy of whatever prefix of its log had reached the store at that instant — not necessarily its very latest step.
There is no way to stop a run, ask a person to approve a tool call, and continue. The event log makes it possible to build, but Agentor does not ship it yet.
If a write to the store fails, the error is logged and the run carries on rather than dying. You get your answer; you may not be able to resume that run.
FileStore writes files and never deletes them. Rotate or expire the directory yourself.

Next steps

Tracing

The same events power Celesto traces. Watch a run instead of reading its log.

Structured output

Get a typed object back from a run instead of a block of text.
Last modified on August 28, 2026