research-agent — LoopKit Flagship Durability Demo
A multi-phase research agent that proves event-log durability across SIGKILL:
the SQLite store survives hard process termination; Resume folds the log and
finishes exactly where execution stopped.
Loop phases
The agent runs exactly 5 iterations, one tool call per iteration.
Returning (state, nil, nil) on KindToolCalled ends the iteration — the runner
writes a new IterationStarted event, creating a resume checkpoint between phases.
| Iteration |
Phase |
Action |
| 1 |
search |
web_search — discover result URLs |
| 2 |
fetch1 |
fetch_page — read result #1 |
| 3 |
fetch2 |
fetch_page — read result #2 |
| 4 |
synthesize |
synthesize_findings — combine into summary |
| 5 |
done |
action.Finish — emit ResearchResult |
All tools carry Semantics: action.Idempotent, so Resume safely re-executes any
tool whose result was not yet persisted before the kill.
CLI
# Default: run to Completion using an in-memory store, print findings, exit 0.
go run ./examples/research-agent/
# Persistent run — write the loop ID to an idfile, use a SQLite store.
go run ./examples/research-agent/ \
--topic "Go concurrency" \
--store sqlite:///tmp/research.db \
--idfile /tmp/loop.id
# Resume a previously interrupted or crashed loop.
go run ./examples/research-agent/ \
--resume <loopID> \
--store sqlite:///tmp/research.db
Flags
| Flag |
Default |
Description |
--topic |
machine learning |
Research topic |
--store |
(in-memory) |
Store DSN: sqlite:///path or empty for memory |
--resume |
(empty) |
Loop ID to resume; skips Start if set |
--idfile |
(empty) |
Path to write the loop ID as soon as it exists |
Environment
| Variable |
Default |
Description |
RESEARCH_TOOL_SLEEP_MS |
0 |
Sleep per fake tool call (ms). Set to 150 to make |
|
|
SIGKILL land mid-run in demo.sh. |
Kill/Resume demo
demo.sh is a self-contained POSIX sh script that proves durability end-to-end:
sh examples/research-agent/demo.sh
# or
LOOPKIT_TOPIC="quantum computing" sh examples/research-agent/demo.sh
What it does:
- Builds the binary.
- Launches it with
RESEARCH_TOOL_SLEEP_MS=150 against a temp SQLite store
and --idfile, so tool calls take ~150ms each.
- Polls the idfile until the loop ID appears (written as soon as
LoopStarted
lands in SQLite, before any tool calls run).
- Sleeps 400ms more (mid-run), then
kill -9 the process.
- Reads the loop ID from the idfile.
- Relaunches with
--resume <loopID> --store sqlite://<path>.
- Asserts the output contains
"Completed" — exits 1 otherwise.
- Cleans up temp files.
What it proves:
- The SQLite event log is durable across
SIGKILL (no buffering, fsync on append).
Resume folds the existing event log (replaying already-persisted events without
re-executing their effects) and continues from the last checkpoint.
- Each iteration boundary is a natural resume point — no explicit checkpointing
needed in the Transition function.
Tests
go test -race -count=1 ./examples/research-agent/
| Test |
What it checks |
TestResearchAgentCompletes |
Default run -> outcome.Completed with >=5 findings, 5 iterations |
TestResearchAgentPhaseOrder |
Findings in correct phase order (search->fetch->synth->conclusion) |
TestResearchAgentResume |
Crash-store simulation -> Resume -> Completed + contiguous log |
TestResearchAgentResume uses a crashAfterNToolCalled store wrapper that
returns an error after the first KindToolCalled event is written, leaving the
loop mid-run. A second runner sharing the same (non-crashing) store calls
Resume and asserts Completed with a contiguous event log.
No subprocesses or SIGKILL are used in the Go test (flaky); the SIGKILL proof
lives in demo.sh.