The thread does not break. Weave, don't save. Resume, don't restart.
Agent State Serialization & Hot-Resume for Python
When an AI agent session ends, everything is lost — the todo list, the working context, the open loops, the last intention. The next session starts from zero, like waking up with amnesia.
WyrdState solves this. It's a lightweight persistence layer that serializes your agent's working state and lets you pick up exactly where you left off — hot-resume, not cold-start.
In Norse mythology, Wyrd (ON: Urðr) is the principle of fate — the thread the Norns weave, connecting what was, what is, and what shall be. WyrdState applies this principle to software:
- Urðr (What Was) → saved state (the stored thread)
- Verðandi (What Is Becoming) → live state (the thread being woven)
- Skuld (What Shall Be) → resumed state (the thread picked up again)
The thread never breaks. The session never truly ends.
pip install wyrdstatefrom wyrdstate import WorkingState
# Create a working state
state = WorkingState()
# Track what matters
state.add_todo("Assemble the gas mower", priority="high")
state.set_context("current_task", "lawn care")
state.add_open_loop("Need to call dad about mower setup")
state.set_intention("Mow the remaining lawn today")
# Save — the thread is woven
state_id = state.save() # Returns UUID like "a3f1b2c4-..."
# ... session ends, time passes, a new day begins ...
# Hot-resume — pick up exactly where you left off
resumed = WorkingState.resume(state_id)
print(resumed.get_intention()) # "Mow the remaining lawn today"
print(resumed.get_context("current_task")) # "lawn care"
print(len(resumed.todos)) # 1| Pillar | Purpose | Example |
|---|---|---|
| Todos | Tracked tasks with status and priority | "Assemble the gas mower" (PENDING) |
| Context | Key/value pairs in the agent's context window | current_task: "lawn care" |
| Open Loops | Unresolved intentions or questions | "Need to call dad about mower setup" |
| Intentions | The agent's last stated goal | "Mow the remaining lawn today" |
NEW ──save()──► LOADED
LOADED ──modify──► DIRTY
DIRTY ──save()──► LOADED
Any ──discard()──► NEW
Every mutation (add_todo, set_context, etc.) transitions to DIRTY. Call save() to persist, or enable auto_save=True for automatic persistence on every change.
The single entry point for all state operations.
from wyrdstate import WorkingState, WyrdStateConfig, StorageBackend
# Default: SQLite backend, no auto-save
state = WorkingState()
# With config
state = WorkingState(config=WyrdStateConfig(
session_id="my-session",
storage_backend=StorageBackend.SQLITE, # or StorageBackend.JSON
storage_path="/path/to/state.db", # optional
auto_save=True, # persist on every mutation
))todo = state.add_todo("Assemble mower", priority="high")
state.add_todo("Buy gas", priority="medium")
state.complete_todo(todo.id) # Mark done
state.cancel_todo(todo.id) # Mark cancelledstate.set_context("current_task", "lawn care")
state.set_context("user_mood", "relaxed", metadata={"source": "observation"})
value = state.get_context("current_task") # "lawn care"
state.remove_context("user_mood")loop = state.add_open_loop("Call dad about mower setup", context="He offered to help")
state.resolve_open_loop(loop.id) # Mark resolved with timestampstate.set_intention("Mow the remaining lawn today")
intent = state.get_intention() # Most recent intention# Manual save
state_id = state.save()
# Load by ID
loaded = WorkingState.load(state_id)
# Resume latest (or by ID)
resumed = WorkingState.resume() # Latest state
resumed = WorkingState.resume(state_id) # Specific state
# In-memory snapshot (no persistence)
payload = state.snapshot()
# Reset to empty
state.discard()from wyrdstate import WyrdStateConfig, StorageBackend
config = WyrdStateConfig(
session_id="unique-session-id", # Optional session identifier
storage_backend=StorageBackend.SQLITE, # or StorageBackend.JSON
storage_path="/custom/path/db", # Default: ~/.wyrdstate/
schema_version=1, # Current schema version
auto_save=False, # Auto-persist on every mutation
max_context_entries=1024, # Max context items (future use)
)Fast, queryable, concurrent-read safe via WAL mode.
from wyrdstate import WorkingState, WyrdStateConfig, StorageBackend
state = WorkingState(config=WyrdStateConfig(
storage_backend=StorageBackend.SQLITE,
storage_path="/path/to/wyrdstate.db", # Optional
))Stores all state in ~/.wyrdstate/wyrdstate.db by default.
Human-readable flat files. Each state is a separate JSON file.
state = WorkingState(config=WyrdStateConfig(
storage_backend=StorageBackend.JSON,
storage_path="/path/to/states/", # Optional
))Stores each state as ~/.wyrdstate/json/<uuid>.json by default.
Compare two states and apply changes:
from wyrdstate.diff import StateDiffer
old_state = WorkingState()
old_state.add_todo("Task A")
new_state = WorkingState()
new_state.add_todo("Task A")
new_state.add_todo("Task B")
new_state.set_context("phase", "implementation")
# Compute diff
diff = StateDiffer.diff(old_state, new_state)
print(diff.summary()) # "+1 todos, +1 context items"
# Apply diff to another state
merged = StateDiffer.merge(old_state, diff)Never crash on corrupt data. WyrdState validates and gracefully recovers:
from wyrdstate.recovery import SelfHealer
# Validate a raw payload
result = SelfHealer.validate(payload)
print(result.valid) # True/False
print(result.errors) # List of error messages
print(result.warnings) # List of warning messages
# Recover — always returns something usable
state = SelfHealer.recover(corrupt_payload)
# If payload is beyond recovery, returns an empty but valid WorkingStateAttach callbacks to state transitions:
from wyrdstate.hooks import SessionLifecycle
lifecycle = SessionLifecycle()
lifecycle.register("on_save", lambda state: print("State saved!"))
lifecycle.register("on_resume", lambda state: print("State resumed!"))
lifecycle.register("on_discard", lambda state: print("State discarded!"))
# Attach to a WorkingState
state = WorkingState()
state._hooks = lifecycle # Or pass via configfrom wyrdstate.exceptions import (
WyrdStateError, # Base exception
StateNotFoundError, # State ID not found in storage
StateCorruptedError, # State data is irrecoverably corrupted
StorageWriteError, # Failed to write to storage backend
InvalidStateError, # Invalid state machine transition
SchemaVersionError, # Incompatible schema version
)WyrdState follows a clean layered architecture:
┌─────────────────────────────────┐
│ WorkingState (core) │ ← Public API
├─────────────────────────────────┤
│ Serializer / Diff / Recovery │ ← Internal Services
├─────────────────────────────────┤
│ Schema (Data Models) │ ← Domain Types
├─────────────────────────────────┤
│ Storage (SQLite / JSON) │ ← Persistence Layer
├─────────────────────────────────┤
│ Config / Exceptions / Hooks │ ← Infrastructure
└─────────────────────────────────┘
Each layer depends only on the layers below it. No circular imports. No god objects. The WorkingState class orchestrates; everything else is a tool it wields.
All data models are frozen dataclasses — immutable after creation, safe for concurrent access:
- Todo:
id,content,status(pending/in_progress/completed/cancelled),priority(low/medium/high/critical),created_at,completed_at - ContextItem:
id,key,value,metadata,created_at - OpenLoop:
id,description,context,created_at,resolved_at - IntentVariable:
id,intention,timestamp
WyrdState uses additive-only schema migrations. New fields are added with defaults; existing fields are never removed or renamed. The migration system is designed so that older data can always be loaded by newer versions:
from wyrdstate.serializer import migrate, CURRENT_SCHEMA_VERSION
# Migrate a v0 payload to the current version
migrated = migrate(old_payload, target_version=CURRENT_SCHEMA_VERSION)To add a new schema version:
- Add a migration function
_migrate_vN_to_vNplus1() - Register it in
_VERSION_ADAPTERS[N] - Bump
CURRENT_SCHEMA_VERSION
WyrdState was designed using the Mythic Engineering methodology with six archetypal subagents:
| Document | Author | Purpose |
|---|---|---|
| PHILOSOPHY.md | Sigrún Ljósbrá (Skald) | Vision, naming, and symbolic foundation |
| SYSTEM_VISION.md | Sigrún Ljósbrá (Skald) | System-level vision and purpose |
| ARCHITECTURE.md | Rúnhild Svartdóttir (Architect) | Technical architecture and layer design |
| INTERFACE.md | Rúnhild Svartdóttir (Architect) | Public API specification |
| DOMAIN_MAP.md | Sávangr Dýrðarljós (Cartographer) | Domain boundaries and data flows |
| DATA_FLOW.md | Védis Eikleið (Cartographer) | Complete data flow diagrams |
| GOALS.md | All | Project goals and success criteria |
wyrdstate/
├── __init__.py # Public API exports
├── py.typed # PEP 561 marker
├── config.py # WyrdStateConfig dataclass
├── core.py # WorkingState orchestrator (572 lines)
├── schema.py # Frozen data models (194 lines)
├── serializer.py # Serialization, deserialization, migration (286 lines)
├── storage.py # SQLite + JSON backends (360 lines)
├── diff.py # StateDiffer, StateDiff, merge/patch (261 lines)
├── recovery.py # SelfHealer: validate, recover, migrate (251 lines)
├── hooks.py # SessionLifecycle event hooks (135 lines)
├── exceptions.py # Custom exception hierarchy (81 lines)
tests/
├── test_core.py # Core API integration tests
├── test_schema.py # Data model unit tests
├── test_serializer.py # Serialization round-trip tests
├── test_storage.py # Storage backend tests (SQLite + JSON)
├── test_diff.py # Diff, merge, and patch tests
├── test_recovery.py # Validation and self-healing tests
├── test_hooks.py # Lifecycle hook tests
docs/
├── PHILOSOPHY.md # Norse wyrd metaphor and naming philosophy
├── SYSTEM_VISION.md # System-level vision statement
├── ARCHITECTURE.md # Layered architecture design
├── INTERFACE.md # Public API specification
├── DOMAIN_MAP.md # Domain boundaries
├── DATA_FLOW.md # Data flow diagrams
├── GOALS.md # Project goals and success criteria
# From PyPI (once published)
pip install wyrdstate
# From source
git clone https://github.com/hrabanazviking/wyrdstate.git
cd wyrdstate
pip install -e .# Install with dev dependencies
pip install -e ".[dev]"
# Run tests
python -m pytest tests/ -v
# Run with coverage
python -m pytest tests/ --cov=wyrdstate --cov-report=term-missing- Python 3.10+
- No external dependencies (only stdlib:
sqlite3,json,uuid,dataclasses)
MIT License — see LICENSE for details.
᛭ The thread does not break. ᛭
Crafted by Runa Gridweaver Freyjasdóttir — the Norns weave, we preserve.