State primitives¶
Memory is the per-agent conversation history layer. Store is the
shared, optionally-persistent key-value blackboard.
For narrative usage see Guides → Mid → Memory and Guides → Mid → Store. For wiring data between Plan steps see Sentinels & predicates.
lazybridge.Memory ¶
Memory(*, strategy: Literal['auto', 'sliding', 'summary', 'none'] = 'auto', max_tokens: int | None = 4000, max_turns: int | None = _DEFAULT_MAX_TURNS, store: Any | None = None, summarizer: Any | None = None, summarizer_timeout: float | None = _DEFAULT_SUMMARIZER_TIMEOUT)
Conversation memory with configurable compression strategy.
Per-agent use: memory=Memory() on the Agent — tracks its message history. Shared use: sources=[memory] on multiple Agents — live view of shared text.
The text() method returns the current memory as a context string,
re-read on every invocation (live view — never a stale snapshot).
LLM summarization¶
Pass any callable (typically an Agent) as summarizer= to enable
LLM-based compression instead of the keyword-extraction fallback::
summarizer = Agent("claude-haiku-4-5-20251001", system="Summarize conversations concisely.")
memory = Memory(strategy="summary", summarizer=summarizer)
agent = Agent("claude-opus-4-7", memory=memory)
When compression triggers the summarizer is called with a formatted transcript of the turns being dropped. Three callable shapes are handled transparently:
- Sync callable returning a string / Envelope / anything with
.text()— called directly. Agent— its__call__already bridges async internally.- Plain
async def summarize(prompt): ...— the returned coroutine is driven to completion (in a worker thread when called from inside an event loop, to avoid nested-loop errors).
If the summarizer raises, compression falls back to keyword extraction — never silent garbage.
Create a Memory instance.
Parameters¶
strategy:
"auto" — compress when token budget is exceeded
(requires max_tokens to be set).
"sliding" — compress by turn count regardless of
max_tokens; does NOT require a token budget.
"summary" — same trigger as "sliding" but uses the
LLM summarizer instead of keyword extraction.
"none" — no compression; only the hard max_turns
cap applies.
max_tokens:
Token budget for strategy="auto". Ignored by
"sliding" and "summary" (they compress by turn
count). None with strategy="auto" disables
compression entirely.
max_turns:
Hard cap on retained turns after compression runs. None
disables the cap (unbounded history — only safe for short
sessions). Default: :attr:_DEFAULT_MAX_TURNS (1000).
store:
Optional :class:~lazybridge.store.Store for persistent
memory across sessions.
summarizer:
Callable used by strategy="summary" — typically an
:class:~lazybridge.agent.Agent. See the class docstring
for accepted shapes.
summarizer_timeout:
Deadline (seconds) applied to async summariser calls. On
timeout the keyword-extraction fallback runs and a one-shot
warning is emitted via :attr:_summarizer_warned. None
disables the deadline. Default: 30 s.
Source code in lazybridge/memory.py
messages ¶
Return full message list including summary prefix if compressed.
Source code in lazybridge/memory.py
text ¶
Return current memory as a plain-text string (live view).
Source code in lazybridge/memory.py
lazybridge.Store ¶
Key-value store for PlanState and shared data.
db=None → in-memory (lost on exit). db="file" → SQLite with WAL mode (persistent).
Source code in lazybridge/store/__init__.py
close ¶
Close every thread-local SQLite connection opened by this Store.
Idempotent. After close() the Store raises RuntimeError
on further reads / writes so callers fail fast instead of
silently re-opening connections.
Source code in lazybridge/store/__init__.py
write ¶
Store value under key.
Copy semantics (in-memory path): the value is deep-copied
before storage via :func:_deep_copy_safe so that callers
mutating the original object after write() cannot silently
alter the stored value and defeat :meth:compare_and_swap.
The SQLite path gets equivalent isolation for free via the
JSON round-trip.
Pydantic instances are serialised via model_dump(mode="json")
before JSON encoding — otherwise SQLite storage would fall
through default=str and persist the model's __repr__
(e.g. "x=42 name='hello'"), which is NOT round-trippable.
In-memory storage keeps the instance as-is since Python dicts
don't need JSON round-tripping.
Source code in lazybridge/store/__init__.py
read ¶
Return the value stored under key, or default.
Copy semantics (in-memory path): returns a deep copy of the
stored value via :func:_deep_copy_safe so that callers mutating
the returned object cannot silently alter the stored value and
defeat :meth:compare_and_swap. The SQLite path returns a fresh
object on every call because json.loads always allocates new
containers.
Source code in lazybridge/store/__init__.py
items ¶
Return (key, value) pairs, optionally restricted to keys starting
with prefix.
Uses an indexed B-tree range scan on the SQLite path (a single
WHERE key >= ? AND key < ? query) — sub-linear in the total
keyspace. The in-memory path filters under the store lock.
Parameters¶
prefix:
Optional key prefix to filter by. None (default) or ""
returns every pair in the store. Any other string restricts the
results to keys starting with that prefix.
Source code in lazybridge/store/__init__.py
compare_and_swap ¶
Atomically set key to new only if its current value
deep-equals expected.
expected=None means "the key must not currently exist" (a
missing key compares equal to None).
Returns True on success, False when another writer has
moved the value since the caller's last read — the caller is
expected to treat this as a lost race (raise, back off, re-read,
etc.). Comparison uses the JSON-normalised shape (via
:func:_to_jsonable), so Pydantic models / nested dicts compare
the same as they do on disk and survive SQLite round-trips.
SQLite path: wraps the read-check-write in BEGIN IMMEDIATE
so concurrent writers serialise on the SQLite reserved lock
instead of interleaving. Exceptions are caught as
(sqlite3.Error, ValueError) — the ValueError arm covers
json.JSONDecodeError (a subclass) raised by json.loads
on corrupt rows; without it, a corrupt row would leave the
BEGIN IMMEDIATE transaction open on the thread-local
connection and poison every subsequent call on that thread.
Source code in lazybridge/store/__init__.py
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 | |
At-rest encryption¶
EncryptedStoreAdapter wraps any Store and encrypts values before
they hit the inner store; reads decrypt transparently. Keys are
not encrypted — Store treats keys as opaque routing strings,
and encrypting them would break iteration / __contains__.
Install the optional extra:
The extra pulls cryptography; the
default install stays pure-Python.
from cryptography.fernet import Fernet
from lazybridge.store import Store
from lazybridge.store.encryption import EncryptedStoreAdapter
key = Fernet.generate_key() # persist somewhere safe (KMS, env var, etc.)
store = EncryptedStoreAdapter(Store(db="state.sqlite"), key=key)
store.write("agent.notes", {"draft": "secret thoughts"})
# SQLite row holds an `lb-enc-v1::` Fernet token, not the JSON.
store.read("agent.notes")
# {"draft": "secret thoughts"}
For key rotation use MultiFernet semantics by passing a list — the
first key is used for new writes, every key is tried on read:
lazybridge.store.encryption.EncryptedStoreAdapter ¶
Encrypt values written to an inner :class:Store.
Parameters¶
inner:
The Store to wrap. Any Store flavour works (in-memory or
SQLite-backed); the adapter doesn't care about persistence.
key:
A Fernet key (bytes) or list of keys. A list activates
:class:cryptography.fernet.MultiFernet for rotation —
encryption uses the first key, decryption tries each in
order until one succeeds.
Raises¶
ImportError:
If cryptography isn't installed. Install
lazybridge[encryption] to add it.
ValueError:
If key is empty or not bytes / list of bytes.
Source code in lazybridge/store/encryption.py
items ¶
Return (key, decrypted-value) pairs, optionally restricted to
keys starting with prefix.
Source code in lazybridge/store/encryption.py
compare_and_swap ¶
CAS over the decrypted value.
Fernet ciphertexts embed a nonce, so two encryptions of the
same plaintext are never equal. Delegating to the inner
Store's CAS would therefore always fail. We decrypt the
current ciphertext, compare against expected in plaintext
space, then write the new encrypted value — as long as the
ciphertext hasn't changed since the read.
This double-check is what makes the adapter race-safe: another
writer may have updated the value between our decrypt and the
inner CAS attempt, in which case the inner CAS returns
False and we propagate that.