Quick Start
How It Works
The loop is re-driven from the top on resume; recorded steps return cached results while real work restarts at the first un-journalled step. The journal, run ledger, checkpoints, and inbound journal are four adjacent persistence primitives — pick correctly with this map.Configuration Options
The journal is a zero-dependency SQLite primitive with a single constructor argument.RunJournal(db_path=None) constructor
| Argument | Type | Default | Description |
|---|---|---|---|
db_path | Optional[str] | None → ~/.praisonai/runs/journal.db | Path to the SQLite journal DB. Use ":memory:" for tests. |
| Kind constant | String | Payload shape | Purpose |
|---|---|---|---|
KIND_MODEL_DECISION | "model_decision" | {"text": ...} | Memoised on replay so the LLM is not re-called (and not re-billed). |
KIND_TOOL_CALL | "tool_call" | {"name", "args", "idempotency_key"} | Records that a tool was issued. |
KIND_TOOL_RESULT | "tool_result" | {"result": ...} | Memoised on replay so side-effecting tools are not re-run. |
KIND_APPROVAL | "approval" | {"decision": ...} | Human/policy approval decision — enables durable pause-for-approval HITL. |
KIND_ITERATION | "iteration" | {"index": int} | Loop cursor — restored on resume via last_iteration(). |
JournalEvent dataclass
| Field | Type | Default | Description |
|---|---|---|---|
run_id | str | — | Owning run. |
seq | int | — | Step number. With kind forms the natural key (run_id, seq, kind). |
kind | str | — | One of the 5 event kinds above. Anything else raises ValueError. |
payload | Dict[str, Any] | {} | Any JSON-serialisable dict; non-JSON values fall back to str(...). |
created_at | float | time.time() | Wall-clock, set at construction. |
RunMeta dataclass
| Field | Type | Default | Description |
|---|---|---|---|
run_id | str | — | Owning run. |
agent | str | "" | Agent identifier at open time. |
task | str | "" | Task label at open time. |
status | str | "running" | One of running, done, succeeded, failed, cancelled. |
outcome | Optional[str] | None | Terminal outcome string (as passed to close_run). |
checkpoint_id | Optional[str] | None | Binds the files store — a resume restores this checkpoint before replaying. |
created_at | float | time.time() | Wall-clock at first open. Preserved on reopen. |
updated_at | float | time.time() | Wall-clock at latest write. |
metadata | Dict[str, Any] | {} | Free-form JSON dict. |
RunJournal public methods
| Method | Signature | What it does |
|---|---|---|
open_run | open_run(run_id, *, agent="", task="", checkpoint_id=None, metadata=None) | Register run_id as running (idempotent). On reopen: preserves the journal, keeps created_at, COALESCEs checkpoint_id (a None reopen preserves the existing binding; a supplied one binds it). |
set_checkpoint | set_checkpoint(run_id, checkpoint_id) | Bind the latest files checkpoint to run_id. |
close_run | close_run(run_id, outcome="done") | Mark terminal so it is no longer resumed. Outcome coerced to {done, succeeded, failed, cancelled} (anything else → "done"). |
run_meta | run_meta(run_id) -> RunMeta | None | Read back the metadata row (or None). |
interrupted_runs | interrupted_runs() -> List[str] | Ids of runs still running — resume candidates on restart, ordered oldest-first. |
append | append(JournalEvent) | Append an event. Idempotent on (run_id, seq, kind): re-appending updates the payload rather than raising. Unknown kinds raise ValueError. |
events | events(run_id) -> List[JournalEvent] | All events ordered by (seq, rowid) — tool_call always precedes its tool_result within a step. |
replay_index | replay_index(run_id) -> Dict[(seq, kind), payload] | Memoised lookup used during resume: a hit returns the recorded result instantly; a miss = real work restarts there. |
last_iteration | last_iteration(run_id) -> Optional[int] | Highest journalled iteration index — restores the loop cursor exactly on resume. |
assert_replay_order | assert_replay_order(run_id, expected) | Determinism guardrail. expected accepts either (seq, kind) tuples (preferred, position-aware) or bare kind strings (kind-only compare). Raises RuntimeError on mismatch. |
close | close() | Close the underlying SQLite connection. |
Common Patterns
Three patterns cover the full open → journal → resume lifecycle. Journal every boundary as it happens. Record amodel_decision, tool_call, tool_result, and iteration at each step.
replay_index(); a hit skips re-execution.
running after a crash is a resume candidate.
Best Practices
Journal at every meaningful boundary
Journal at every meaningful boundary
The five event kinds cover every point where re-execution would be either wasteful (
model_decision) or unsafe (tool_call/tool_result, approval). Omitting a boundary means that boundary re-runs on resume.Always append the iteration event alongside model_decision/tool_result
Always append the iteration event alongside model_decision/tool_result
last_iteration() reads this to restore the loop cursor exactly on resume; without it the resumed loop must scan the journal to find its position.Bind a checkpoint_id before appending tool events
Bind a checkpoint_id before appending tool events
open_run(..., checkpoint_id=...) (or a later set_checkpoint) is what lets a resume restore the workspace to a consistent state before replaying — otherwise replay may run against a workspace that has drifted.Use assert_replay_order() in tests, not in production hot loops
Use assert_replay_order() in tests, not in production hot loops
The full
(seq, kind) position-aware compare is the determinism guardrail — perfect for test coverage of your loop wiring, unnecessary on every replay step in production.Related
Run Ledger
Tracks run status (queued/running/done) — the journal tracks the per-event cursor.
Checkpoints
Workspace file snapshots bound via
checkpoint_id for restore-before-replay.Durable Approvals
Approval decisions journalled as
KIND_APPROVAL events for pause-for-approval HITL.Inbound Journal
Webhook dedup and in-flight recovery — a different scope from run-state replay.

