> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Run-State Journal

> Resume an agent run where it crashed — no re-executed tools, no re-billed LLM calls

The run-state journal records the execution cursor of an agent run so a crash mid tool-loop can resume without re-executing side-effecting tools or re-billing LLM calls.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.runtime import RunJournal, JournalEvent

journal = RunJournal()  # ~/.praisonai/runs/journal.db
journal.open_run("r1", agent="coder", task="migrate module")

# Record each boundary as it happens
journal.append(JournalEvent("r1", 0, "model_decision", {"text": "call tool X"}))
journal.append(JournalEvent("r1", 0, "tool_result", {"result": "done"}))

# On resume, journalled steps return recorded results — no re-billing, no re-running
replay = journal.replay_index("r1")
cached = replay.get((0, "tool_result"))  # instant recorded result

journal.close_run("r1", "succeeded")
```

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Run-State Journal"
        Boundary[📝 Model/Tool boundary] --> Append[📒 append event]
        Append --> Store[(🗄️ journal.db)]
        Crash[💥 Crash] -.-> Resume[▶️ resume run]
        Resume --> Replay[⚡ memoised replay]
        Store --> Replay
        Replay --> Live[🔁 restart live at first un-journalled step]
    end

    classDef input fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef success fill:#10B981,stroke:#7C90A0,color:#fff
    classDef warn fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef store fill:#8B0000,stroke:#7C90A0,color:#fff

    class Boundary input
    class Append,Resume process
    class Live,Replay success
    class Crash warn
    class Store store
```

## Quick Start

<Steps>
  <Step title="Simple Usage">
    Open a run, append a couple of events, then close it.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents.runtime import RunJournal, JournalEvent

    journal = RunJournal(":memory:")
    journal.open_run("r1", agent="coder", task="migrate module")

    seq = 0
    for i in range(3):
        journal.append(JournalEvent("r1", seq, "model_decision", {"text": f"call {i}"}))
        journal.append(JournalEvent("r1", seq, "tool_call", {"name": f"tool{i}", "args": {}, "idempotency_key": f"k{i}"}))
        journal.append(JournalEvent("r1", seq, "tool_result", {"result": i}))
        journal.append(JournalEvent("r1", seq, "iteration", {"index": i}))
        seq += 1

    journal.close_run("r1", "succeeded")
    ```
  </Step>

  <Step title="Resume after a crash">
    Compute `replay_index()` and drive the loop — journalled model calls and tools return their recorded results instantly, so the LLM is not re-billed and side-effecting tools are not re-run.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    replay = journal.replay_index("r1")

    def call_model(seq):
        rec = replay.get((seq, "model_decision"))
        if rec is not None:
            return rec["text"]        # cached — no LLM call, no re-bill
        # …live LLM call here, then journal.append(...) the new decision

    def run_tool(seq):
        rec = replay.get((seq, "tool_result"))
        if rec is not None:
            return rec["result"]      # cached — no re-execution of side effects
        # …live tool call here, then journal.append(...) the new result
    ```
  </Step>
</Steps>

***

## 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.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Agent
    participant Journal as RunJournal
    participant Tool
    participant LLM

    User->>Agent: agent.start("...")
    Agent->>Journal: open_run(run_id, agent, task)
    Agent->>LLM: model decision (0)
    Agent->>Journal: append(model_decision, seq=0)
    Agent->>Tool: run tool (0)
    Agent->>Journal: append(tool_call, seq=0)
    Agent->>Journal: append(tool_result, seq=0)
    Agent->>Journal: append(iteration, index=0)
    Note over Agent: 💥 CRASH before step 1

    User->>Agent: resume(run_id)
    Agent->>Journal: replay_index(run_id)
    Journal-->>Agent: {(0, "model_decision"): ..., (0, "tool_result"): ...}
    Note over Agent: seq 0 recorded — return cached, do NOT re-call LLM/tool
    Agent->>LLM: model decision (1)  [live — first un-journalled step]
    Agent->>Tool: run tool (1)
    Agent->>Journal: append(...)
    Agent->>Journal: close_run(run_id, "succeeded")
    Agent-->>User: final result
```

The journal, run ledger, checkpoints, and inbound journal are four adjacent persistence primitives — pick correctly with this map.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Start{What do you need to persist?}
    Start -->|Per-event execution cursor of ONE run| Journal[Run-State Journal<br/>this page]
    Start -->|Run status<br/>queued/running/done| Ledger[Run Ledger]
    Start -->|Workspace file snapshots| Checkpoints[Checkpoints]
    Start -->|Webhook/inbound message dedup| Inbound[Inbound Journal]
    Start -->|Finished conversation messages| Session[Session Store]

    classDef choice fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef feature fill:#189AB4,stroke:#7C90A0,color:#fff

    class Start choice
    class Journal,Ledger,Checkpoints,Inbound,Session feature
```

***

## 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. |

**Event kinds** — the five boundaries the journal records:

| 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. |

Import the primitive and its event kind constants directly:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.runtime import RunJournal, JournalEvent, RunMeta
from praisonaiagents.runtime.journal import (
    KIND_MODEL_DECISION,
    KIND_TOOL_CALL,
    KIND_TOOL_RESULT,
    KIND_APPROVAL,
    KIND_ITERATION,
)
```

***

## Common Patterns

Three patterns cover the full open → journal → resume lifecycle.

**Journal every boundary as it happens.** Record a `model_decision`, `tool_call`, `tool_result`, and `iteration` at each step.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.runtime import RunJournal, JournalEvent

journal = RunJournal(":memory:")
journal.open_run("r1", agent="coder", task="migrate module")

seq = 0
for i in range(3):
    journal.append(JournalEvent("r1", seq, "model_decision", {"text": f"call {i}"}))
    journal.append(JournalEvent("r1", seq, "tool_call", {"name": f"tool{i}", "args": {}, "idempotency_key": f"k{i}"}))
    journal.append(JournalEvent("r1", seq, "tool_result", {"result": i}))
    journal.append(JournalEvent("r1", seq, "iteration", {"index": i}))
    seq += 1

journal.close_run("r1", "succeeded")
```

**Resume with memoised replay.** Look up each step in `replay_index()`; a hit skips re-execution.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
replay = journal.replay_index("r1")

def run_tool(seq):
    rec = replay.get((seq, "tool_result"))
    if rec is not None:
        return rec["result"]      # cached — no re-execution of side effects
    # …live tool call here, then journal.append(...) the new result
```

**Discover interrupted runs on gateway startup.** Any run still `running` after a crash is a resume candidate.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
journal = RunJournal()
for run_id in journal.interrupted_runs():
    # gateway resumes each run left `running` after the last crash
    resume_run(run_id)
```

***

## Best Practices

<AccordionGroup>
  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="Run Ledger" icon="database" href="/docs/features/run-ledger">
    Tracks run status (queued/running/done) — the journal tracks the per-event cursor.
  </Card>

  <Card title="Checkpoints" icon="clock-rotate-left" href="/docs/features/checkpoints">
    Workspace file snapshots bound via `checkpoint_id` for restore-before-replay.
  </Card>

  <Card title="Durable Approvals" icon="database" href="/docs/features/durable-approvals">
    Approval decisions journalled as `KIND_APPROVAL` events for pause-for-approval HITL.
  </Card>

  <Card title="Inbound Journal" icon="book" href="/docs/features/inbound-journal">
    Webhook dedup and in-flight recovery — a different scope from run-state replay.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.