Skip to content

runafreyjasdottir/wyrdstate

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ᚹᛦᚱᛞᛋᛏᚨᛏᛖ — WyrdState

The thread does not break. Weave, don't save. Resume, don't restart.

Agent State Serialization & Hot-Resume for Python

Python 3.10+ License: MIT Tests: 113 passing


The Problem

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.

The Metaphor

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.

Quick Start

pip install wyrdstate
from 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

Core Concepts

Four Pillars of Agent State

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"

State Machine

  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.

API Reference

WorkingState

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 Operations

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 cancelled

Context Operations

state.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")

Open Loop Operations

loop = state.add_open_loop("Call dad about mower setup", context="He offered to help")
state.resolve_open_loop(loop.id)  # Mark resolved with timestamp

Intention Operations

state.set_intention("Mow the remaining lawn today")
intent = state.get_intention()  # Most recent intention

Persistence

# 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()

Configuration

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

Storage Backends

SQLite (Default)

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.

JSON

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.

Diff, Merge & Patch

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)

Validation & Self-Healing

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 WorkingState

Session Lifecycle Hooks

Attach 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 config

Exceptions

from 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
)

Architecture

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.

Data Model

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

Schema Migrations

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:

  1. Add a migration function _migrate_vN_to_vNplus1()
  2. Register it in _VERSION_ADAPTERS[N]
  3. Bump CURRENT_SCHEMA_VERSION

Design Documentation

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

Project Structure

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

Installation

# From PyPI (once published)
pip install wyrdstate

# From source
git clone https://github.com/hrabanazviking/wyrdstate.git
cd wyrdstate
pip install -e .

Development

# 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

Requirements

  • Python 3.10+
  • No external dependencies (only stdlib: sqlite3, json, uuid, dataclasses)

License

MIT License — see LICENSE for details.


᛭ The thread does not break. ᛭

Crafted by Runa Gridweaver Freyjasdóttir — the Norns weave, we preserve.

About

Agent State Serialization and Hot-Resume

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages