README
¶
durable_agent / local
Zero-infrastructure durability lab on the local runtime. Local is durable by
default via durable-go — no external
server, no client/worker split, nothing to install. Every LLM call and tool execution
is journaled to disk under ./agent_data/local-durable-agent. Kill the agent process
mid-stream, restart it from the same working directory, and reconnect with
GetAgentStream — durable-go replays each already-completed step from the journal as
one coalesced message, then continues live once reconnect reaches the in-flight step.
Unlike Temporal, local's
Eventsdoes not supportWithOffset(n)forn > 0— there is no per-token durable log to seek into, only step-level replay. This example never callsWithOffset; the saved state file only needs arun_id.
State file: /tmp/durable_agent_local_runstate.json.
For the Temporal split worker/agent lab, see ../temporal/. For the
Restate single-process lab, see ../restate/.
Prerequisites
- Configuration —
examples/README.mdConfiguration (an LLM provider API key). - Working directory — run commands from
examples/, and always restart from the same directory so the agent finds its journal at./agent_data/local-durable-agent.
That's it — no server to start, no task infra:* step.
Quick start
# From examples/
go run ./durable_agent/local "Hello from the local durable agent!"
Interactive REPL (no args):
go run ./durable_agent/local
Type prompts at you>. Type exit / quit / bye to stop.
Scenarios to try (durability)
Single process, no separate server. Use two terminals when you kill/restart the
agent (terminal 1 = agent REPL, terminal 2 = kill / cat / copy commands).
Run all commands from the
examples/directory — the journal directory (./agent_data/local-durable-agent) is relative to the working directory the process starts in.Reconnect requires a live run. Once the run finishes (or is cancelled), streaming events are no longer available for replay. If you restart after the run is already terminal,
GetAgentStreamreturnsErrRunAlreadyCompletedand the agent asks you to start a new turn. Timing matters for scenario 2: reconnect while the run is still executing (a hard kill leaves it live). Scenarios 3 and 4 deliberately exercise the terminal-run path instead — for different reasons (scenario 3: gracefulClosecancels it; scenario 4: it's allowed to actually finish).Clean up between scenarios (optional — clears the journal, a leftover state file, and any stray agent process):
pkill -SIGKILL -f 'go run ./durable_agent/local|go-build/.*/local' 2>/dev/null rm -f /tmp/durable_agent_local_runstate.json rm -rf ./agent_data/local-durable-agent trueLLM reply text varies by model and run. Labels below: Expected startup output (fixed banners), Sample response shape (structure only), Expected behavior.
1 — Happy path
Terminal 1 — start the agent:
go run ./durable_agent/local
Expected startup output:
=== durable_agent/local interactive stream ===
Durability: ON (default) — steps are journaled under ./agent_data/local-durable-agent.
No external server required.
Kill -9 this process mid-stream, then restart (same working directory) to reconnect mid-run.
(Ctrl+C is a graceful Close — it cancels the run instead of leaving it resumable; see README scenario 3.)
Type 'exit' or 'quit' or 'bye' to stop.
you>
Terminal 1 — type a short prompt:
Hello from the local durable agent!
Sample response shape (LLM text varies):
[run_id] <uuid>
--- stream start ---
<assistant reply>
--- stream end ---
you>
Optional: ls ./agent_data/local-durable-agent — durable-go's journal files are there.
Type bye when finished, or leave the REPL open for the next scenario.
2 — Kill agent mid-stream, reconnect (crash)
This scenario shows GetAgentStream: the agent process is killed while streaming,
the journal on disk survives, and a restart resumes the same run.
Timing is critical. Use a long prompt so the LLM call takes several seconds — enough time to kill, check the state file, and restart before the run finishes. If you wait too long, you get the "already completed" path (that is scenario 4).
Terminal 1 — start the agent and send a long prompt:
go run ./durable_agent/local
you> Write a detailed day-by-day travel plan for a 7-day trip to Japan.
Watch the [run_id] line — the state file is written before tokens arrive:
[run_id] <uuid>
--- stream start ---
Day 1: Arrival in Tokyo...
While tokens are streaming (within the first few seconds), kill the agent from terminal 2:
pkill -SIGKILL -f 'go run ./durable_agent/local|go-build/.*/local'
Terminal 1 exits immediately (no cleanup) — simulating a crash. Nothing else needs to keep running — the journal is just a directory on disk.
Terminal 2 — confirm the state file was saved:
cat /tmp/durable_agent_local_runstate.json
{"run_id":"<uuid>","prompt":"Write a detailed day-by-day travel plan for a 7-day trip to Japan."}
Terminal 1 — restart quickly, from the same directory (while the run is still executing on the journal's timeline):
go run ./durable_agent/local
On startup the saved state is detected:
[reconnect] found saved run state:
run_id : <uuid>
prompt : "Write a detailed day-by-day travel plan for a 7-day trip to Japan."
Reconnect? [y/n]>
Type y. The agent reconnects and replays the run's step history from the
journal — already-completed steps print as one coalesced message each
([step_replayed] — step granularity, not the original token-by-token stream), then
live tokens resume once reconnect catches up to the in-flight step:
[reconnect] reconnecting run_id=<uuid>
[reconnect] original prompt: "Write a detailed day-by-day travel plan for a 7-day trip to Japan."
--- stream resumed ---
[step_replayed] step=... status=completed — a previously completed step replayed as one coalesced message, not the original tokens
...Day 7: Farewell day in Kyoto...
--- stream end ---
The state file is cleared on RUN_FINISHED. The REPL then continues normally.
3 — Graceful Ctrl+C mid-stream — this cancels the run (not the same as a crash!)
This scenario is the opposite lesson from scenario 2. It's tempting to assume Ctrl+C
and pkill -SIGKILL behave the same because both "kill the process" — they do
not, on local. a.Close() (which this example's signal handler calls on Ctrl+C, for
graceful cleanup — flushing OTLP exporters, etc.) closes the durable-go engine it
owns, which cancels every in-flight run on that engine and waits for the
cancellation to be journaled before Close() returns. That run becomes terminal — not
resumable — exactly as if it had finished. A hard kill (kill -9 / pkill -SIGKILL)
skips this shutdown path entirely, which is why scenario 2's resume works and this
one doesn't.
This is specific to local. On Temporal/Restate, closing the client process never touches the server-side run — see Durable Execution.
Terminal 1 — start the agent and send a long prompt:
go run ./durable_agent/local
you> Write a detailed essay about durable execution and why crash recovery matters.
Wait until you see streaming tokens:
[run_id] <uuid>
--- stream start ---
Durable execution means...
Terminal 1 — press Ctrl+C once while tokens are still streaming.
Expected shutdown output:
Shutdown signal received; closing agent...
durable_agent/local stopped.
(If shutdown hangs, press Ctrl+C a second time to force exit — but it shouldn't:
Close waits for the cancellation to finish journaling, which is fast.)
Terminal 1 — restart from the same directory:
go run ./durable_agent/local
[reconnect] found saved run state:
run_id : <uuid>
prompt : "Write a detailed essay about durable execution and why crash recovery matters."
Reconnect? [y/n]>
Type y.
Expected behavior — the run is already terminal (cancelled by Close), not live:
[reconnect] reconnecting run_id=<uuid>
[reconnect] original prompt: "Write a detailed essay about durable execution and why crash recovery matters."
[reconnect] the run completed successfully while you were disconnected.
[reconnect] the response was generated, but streaming events are no longer available.
[reconnect] if conversation history is configured, the response is already saved —
[reconnect] start a new turn to continue. otherwise, start a new run.
[reconnect] original prompt: "Write a detailed essay about durable execution and why crash recovery matters."
(The message text says "completed successfully" because the SDK's ErrRunAlreadyCompleted
covers every terminal outcome — success, failure, and cancellation alike; it does not
distinguish which one happened. The run was actually cancelled, not completed.)
Learn: graceful shutdown (Close) and a crash are not equivalent for local
durability. Durability protects against unclean process death — it is not a mechanism
for "pause and resume later" across your own planned shutdowns.
4 — Restart after the run already finished (normally, not cancelled)
Scenario 3 already showed one way to hit ErrRunAlreadyCompleted (cancellation via
Close). This scenario hits the same error via the more obvious path — letting a run
actually finish — while also demonstrating a local-specific quirk: since there is
no server, nothing finishes an abandoned run for you. You have to reconnect once to
drive it to completion yourself.
Local has no separate server, unlike Temporal (a worker keeps polling the task queue) or Restate (the durable invocation keeps going in Restate's server). Nothing drives an abandoned local run forward on its own — a run you kill mid-stream stays parked at
Runningin the journal until some process reconnects to it and finishes driving it. So to observe the completed-run path here, you first have to let the run finish via one normal reconnect, then replay an older, already-superseded state file against that now-terminal run.
Step A — create a mid-run state file (same start as scenario 2)
Terminal 1 — start and send a long prompt:
go run ./durable_agent/local
you> Write a detailed day-by-day travel plan for a 7-day trip to Japan.
As soon as you see [run_id] / streaming tokens, kill from terminal 2:
pkill -SIGKILL -f 'go run ./durable_agent/local|go-build/.*/local'
Terminal 2 — copy the state file somewhere safe (do not skip this — this stale copy is what step C replays):
cat /tmp/durable_agent_local_runstate.json
cp /tmp/durable_agent_local_runstate.json /tmp/stale_local_runstate.json
Step B — reconnect once and let it actually finish this time
go run ./durable_agent/local
[reconnect] found saved run state: ...
Reconnect? [y/n]> y
This time do not kill it again — let --- stream end --- print normally. This is
the step that drives the run to a terminal state; the real state file is cleared
automatically on RUN_FINISHED.
Step C — restore the stale (now-superseded) state file and restart
cp /tmp/stale_local_runstate.json /tmp/durable_agent_local_runstate.json
go run ./durable_agent/local
On startup:
[reconnect] found saved run state:
run_id : <uuid>
prompt : "Write a detailed day-by-day travel plan for a 7-day trip to Japan."
Reconnect? [y/n]>
Type y.
Expected behavior — run already completed:
[reconnect] reconnecting run_id=<uuid>
[reconnect] original prompt: "Write a detailed day-by-day travel plan for a 7-day trip to Japan."
[reconnect] the run completed successfully while you were disconnected.
[reconnect] the response was generated, but streaming events are no longer available.
[reconnect] if conversation history is configured, the response is already saved —
[reconnect] start a new turn to continue. otherwise, start a new run.
[reconnect] original prompt: "Write a detailed day-by-day travel plan for a 7-day trip to Japan."
The state file is cleared. You then get a normal you> prompt — type a new
message to start a fresh run (this example does not wire multi-turn conversation
history, so the completed reply is not shown again).
Alternate (simpler) check — no stale file:
- Run a short prompt to completion (
--- stream end ---). - Confirm state is gone:
ls /tmp/durable_agent_local_runstate.json→ No such file. - Restart
go run ./durable_agent/local— no reconnect prompt; justyou>.
Learn: Durability means the journal captured the work even though the client that started it is gone. What you lose after completion is only the streaming replay for that run ID — not the ability to start a new turn.
5 — DURABILITY=off — the opt-out contrast
This scenario shows what you're actually opting out of by disabling durability. With
local.DurabilityOff(), execution is pure in-memory — no journal, no reconnect, ever.
Terminal 1 — start with durability off and send a long prompt:
DURABILITY=off go run ./durable_agent/local
Expected startup output:
=== durable_agent/local interactive stream ===
Durability: OFF (DURABILITY=off) — pure in-memory, no journal, no reconnect after a crash.
Kill -9 this process mid-stream, then restart (same working directory) to reconnect mid-run.
(Ctrl+C is a graceful Close — it cancels the run instead of leaving it resumable; see README scenario 3.)
Type 'exit' or 'quit' or 'bye' to stop.
you> Write a detailed day-by-day travel plan for a 7-day trip to Japan.
While tokens are streaming, kill the agent from terminal 2 (same command as scenario 2).
Terminal 1 — restart (durability still off or on — doesn't matter, there is no journal for this run either way):
go run ./durable_agent/local
[reconnect] found saved run state: ...
Reconnect? [y/n]> y
[reconnect] reconnecting run_id=<uuid>
[reconnect] ErrStreamNotFound — no journal for this run.
[reconnect] this is expected if DURABILITY=off was set for the crashed process,
[reconnect] or if ./agent_data/local-durable-agent was deleted/moved before restart.
Learn: this is the entire value proposition of durable-by-default in one contrast — with durability on (scenario 2), the same kill+restart sequence resumes transparently; with it off, the work started by the killed process is simply gone.
Notes
- Topology is a single process, no server, no worker —
./agent_data/local-durable-agenton disk is the durability mechanism. - No
WithOffset: local'sEventsonly supportsfromOffset0. Reconnect always replays full step history (as coalescedstep_replayedmessages), then goes live — it is not seekable to an arbitrary token position the way Temporal's stream is. - Approvals (if triggered) use stream
Approvethe same as on Temporal/Restate. - This lab does not wire multi-turn conversation history.
./agent_data/is git-ignored; delete it any time to reset — see "Clean up between scenarios" above.- Focused reconnect demo (Temporal or Restate, token-level
WithOffset):../../agent_with_reconnect/.
Documentation
¶
Overview ¶
Interactive streaming REPL for the durable_agent Local lab.
Usage (from examples/):
go run ./durable_agent/local [initial prompt]
Zero infrastructure: the local runtime is durable **by default** via durable-go — no external server, no client/worker split, nothing to install. Every LLM call and tool execution is journaled to disk (DataDir "./agent_data/local-durable-agent" by default, relative to the working directory this binary runs from). Kill this process mid-run, then restart it (same working directory) to reconnect with GetAgentStream — durable-go replays each already-completed step as one coalesced step_replayed event, then live events resume once reconnect reaches the in-flight step.
Unlike Temporal, local's Events does not support WithOffset(n) for n > 0 (there is no per-token durable log to seek into — only step-level replay); every reconnect call omits WithOffset entirely.
Set DURABILITY=off to opt out via local.DurabilityOff() and see the contrast: without a journal, a killed process cannot reconnect at all (ErrStreamNotFound).
On startup the agent checks /tmp/durable_agent_local_runstate.json for a saved runID and offers reconnect.
At the "you>" prompt type any message. Type "exit" or "quit" to stop.