Skip to content

Stores

A ChunkStore is the ordered log behind a node and an extension point for custom storage. Readers and writers are its cursors.

A11 supplies node semantics over established storage infrastructure instead of requiring a separate durable-stream database. Choose the backend by deployment:

  • LocalChunkStore keeps short-lived data inside one process;
  • SQLiteChunkStore uses an embedded SQLite database for one-machine persistence;
  • RedisChunkStore lets independent programs share named streams through an existing Redis deployment.

Persistence and transactions retain the behavior and operational constraints of SQLite or Redis. Redis deployments also retain their existing replication, monitoring, and scaling facilities. A11 adds chunk ordering, reader and writer cursors, node finality, and structured status.

ChunkStore

The main store operations are intentionally low-level: put appends a fragment, get waits at a sequence position, and close_writes_with_status publishes the terminal state to every reader.

a11.stores.chunk_store.ChunkStore

ChunkStore()

Construct the abstract base. Subclass this in Python to back an agent with a custom asynchronous chunk store; every data method returns an awaitable so callers never block the event loop.

clear_data

clear_data(seq: SupportsInt) -> Future[NodeFragment]

Erase the payload of the fragment at a sequence number while keeping its slot, and await the resulting fragment.

close_writes_with_status

close_writes_with_status(status: Status, return_status_if_already_closed: bool = False) -> Future[Status]

Seal the store against further writes with a terminal status and await completion. Waiting readers are released. With return_status_if_already_closed, a repeated close returns the status recorded by the first.

Examples:

Publish clean producer completion to all readers:

await store.close_writes_with_status(Status.ok())

get

get(seq: SupportsInt, deadline: Time | None = None) -> Future[NodeFragment]

Await the fragment stored at a sequence number. The future resolves when the fragment is available or the optional deadline elapses.

Examples:

Read back a fragment after retaining its assigned position:

fragment = await store.get(seq)

get_by_arrival_order

get_by_arrival_order(arrival_order: SupportsInt, deadline: Time | None = None) -> Future[NodeFragment]

Await the fragment identified by the order in which it arrived rather than its sequence number. The future resolves when the fragment is present or the optional deadline passes.

get_final_seq

get_final_seq() -> Future[int | None]

Await the explicitly marked final sequence, or None if no fragment has declared finality. Finality is independent of write closure: closing the store does not create a final sequence.

get_id

get_id() -> str

Return the store's node identifier. Raises if a Python subclass does not override get_id.

get_seq_for_arrival_order

get_seq_for_arrival_order(arrival_order: SupportsInt) -> Future[int]

Await the sequence number that corresponds to a given arrival order.

next

next(deadline: Time | None = None, limit: SupportsInt = 1) -> Future[list[NodeFragment | None]]

Await up to limit of the next available fragments as a stream. This is the primary way an agent reads chunks as they are produced: the future resolves with whatever is ready before the optional deadline, and slots may be None when a fragment is missing. Loop over successive calls to follow a growing store.

put

put(fragment: NodeFragment) -> Future[int]

Append a single fragment and await its assigned sequence number. The future resolves once the backing store accepts the write.

Examples:

Store a fragment and retain its assigned position:

seq = await store.put(fragment)

put_many

put_many(fragments: Sequence[NodeFragment]) -> Future[list[int]]

Append several fragments in one batch and await their assigned sequence numbers. Prefer this over repeated put calls when an agent emits many chunks at once, to reduce round-trips.

size

size() -> Future[int]

Await the number of fragments currently in the store.

a11.stores.chunk_store.ChunkStoreFactory module-attribute

ChunkStoreFactory = Callable[[NameString], ChunkStore]

a11.stores.chunk_store.native_chunk_store

native_chunk_store(store: ChunkStore) -> ChunkStore

The native store behind store, or store itself.

A facade that only forwards -- an unsubclassed LocalChunkStore -- is indistinguishable from the store it wraps except in cost, and the cost is large: a native reader or writer calling such a store re-enters Python for every fragment, so each read and each write becomes an event-loop turn and none of the inline fast paths in ChunkStoreReader or ChunkStoreWriter can fire. Handing native code the store underneath removes the round trip.

A facade only advertises itself when nothing is overriding it, so a subclass with Python behaviour still goes through the trampoline and stays virtual.

LocalChunkStore

a11.stores.local_chunk_store.LocalChunkStore

LocalChunkStore(node_id: NameString)

Bases: ChunkStore

An in-memory store whose state and synchronization live in C++.

The small forwarding layer keeps Python overrides virtual when a subclass is passed back into native readers, writers, nodes, or sessions.

An instance of this class exposes its underlying native store directly to native readers and writers. Subclasses retain the forwarding layer so their Python overrides remain active.

get async

get(seq: int, deadline: Time | None = infinite_future()) -> NodeFragment

Wait for and return the fragment at sequence number seq.

Use a ChunkStoreReader for ordinary sequential reads; direct lookup is useful for replay, inspection, and custom retention logic.

get_by_arrival_order async

get_by_arrival_order(arrival_order: int, deadline: Time | None = infinite_future()) -> NodeFragment

Wait for a fragment by its zero-based ingestion order.

Arrival order can differ from sequence order when fragments reach a store out of order.

next async

next(deadline: Time | None = infinite_future(), limit: int = 1) -> list[NodeFragment | None]

Read from the store's shared logical-sequence cursor.

The cursor advances through sequence numbers 0, 1, 2, and so on, waiting at gaps. None is the clean end sentinel, not a placeholder for a missing fragment. Use get_by_arrival_order for ingestion order; most agent code should let ChunkStoreReader manage this cursor, buffering, ordering, and end-of-stream handling.

put async

put(fragment: NodeFragment) -> int

Append one fragment and return its accepted sequence number.

put_many async

put_many(fragments: Sequence[NodeFragment]) -> list[int]

Append a batch and return sequence numbers in matching order.

clear_data async

clear_data(seq: int) -> NodeFragment

Discard one stored payload while retaining its sequence slot.

This supports pop_chunks readers and retention policies without changing the ordering metadata seen by other readers.

get_seq_for_arrival_order async

get_seq_for_arrival_order(arrival_order: int) -> int

Translate a zero-based ingestion position to its sequence number.

get_final_seq async

get_final_seq() -> int | None

Return the logical final sequence, if a final fragment was written.

Finality and closure are independent: closing writes does not create a final sequence, and writing a final fragment does not close the store.

close_writes_with_status async

close_writes_with_status(status: Status, return_status_if_already_closed: bool = False) -> Status

Seal writes with status and wake blocked readers.

This records whether production completed or failed, but it does not mark any fragment as the final data value. Producers that need semantic finality should write a fragment with continued=False first (most commonly through AsyncNode.finalize, which does both).

size async

size() -> int

Return the number of fragment slots currently held in memory.

get_id

get_id() -> NameString

Return the node id whose fragment log this store backs.

get_impl

get_impl() -> LocalChunkStore

Return the owned native implementation.

RedisChunkStore

a11.stores.redis_chunk_store.RedisChunkStore

RedisChunkStore(id: str, client: RedisClient | None = None, options: RedisChunkStoreOptions | dict[str, Any] | None = None)

Bases: ChunkStore

A persistent, multi-process ChunkStore backed by Redis Streams.

Open one node's persistent stream with an injected Redis client.

Stores for the same id address the same Redis state. Pass a shared RedisClient in production so many nodes reuse one connection pool; call initialize when metadata must exist before the first write.

client property

client: RedisClient

The explicitly composed RedisClient.

keys property

A copy of the sharding-safe Redis key layout.

options property

A copy of this store's key and payload policy.

create staticmethod

create(id: str, client: RedisClient | None = None, options: RedisChunkStoreOptions | dict[str, Any] | None = None) -> RedisChunkStore

Create a Redis-backed fragment log for one node id.

clear_data async

clear_data(seq: int) -> NodeFragment

Tombstone one payload while retaining ordering metadata.

close_writes_with_status async

close_writes_with_status(status: Status, return_status_if_already_closed: bool = False) -> Status

Atomically seal writes with a terminal status and wake readers.

This closes the producer side but does not mark data final. Write a final fragment first when consumers use whole-value semantics such as AsyncNode.consume.

get async

get(seq: int, deadline: Time | None = None) -> NodeFragment

Wait for and return a fragment by sequence number.

The deadline bounds both Redis work and the wait for a future fragment.

get_by_arrival_order async

get_by_arrival_order(arrival_order: int, deadline: Time | None = None) -> NodeFragment

Wait for a fragment by its zero-based Redis ingestion order.

get_final_seq async

get_final_seq() -> int | None

Return the logical final sequence, if one has been written.

The final marker is independent of Redis write closure. Closing a store does not synthesize it, and a final fragment does not close the store.

get_metadata async

get_metadata() -> RedisChunkStoreMetadata

Read size, finality, and closure state without scanning fragments.

get_seq_for_arrival_order async

get_seq_for_arrival_order(arrival_order: int) -> int

Translate a zero-based ingestion position to its sequence number.

initialize async

initialize() -> None

Ensure node metadata exists without writing a fragment.

This is useful during provisioning or health checks; ordinary writes initialize the store lazily.

next async

next(deadline: Time | None = None, limit: int = 1) -> list[NodeFragment | None]

Read from the persistent shared logical-sequence cursor.

The cursor advances through sequence numbers and waits at gaps; None marks clean end-of-stream. Use get_by_arrival_order for ingestion order. Prefer ChunkStoreReader for normal node reads; it adds buffering, offsets, and final-sequence handling above this primitive.

put async

put(fragment: NodeFragment) -> int

Atomically append one fragment and return its sequence number.

put_many async

put_many(fragments: Sequence[NodeFragment]) -> list[int]

Atomically append a batch and return its assigned sequences.

size async

size() -> int

Return the number of fragment entries recorded for this node.

a11.stores.redis_chunk_store.RedisChunkStoreOptions

RedisChunkStoreOptions(key_prefix: str = 'a11:', inline_data_threshold: SupportsInt | None = 262144)

Key layout and inline-payload policy for RedisChunkStore.

Construct validated Redis chunk-store options.

inline_data_threshold property writable

inline_data_threshold: int

Chunk data larger than this many bytes uses the blob hash.

key_prefix property writable

key_prefix: str

Prefix before the per-node Redis Cluster hash tag.

from_environment staticmethod

from_environment() -> RedisChunkStoreOptions

Read the A11_REDIS_CHUNK_STORE_* environment variables.

validate

validate() -> None

Raise if the key layout policy is invalid.

a11.stores.redis_chunk_store.RedisChunkStoreMetadata

Node-level Redis state read without iterating over chunk entries.

closed property

closed: bool

Whether the store rejects new writes.

final_seq property

final_seq: int | None

The declared final sequence, if one has arrived.

id property

id: str

The owning AsyncNode identifier.

max_seq property

max_seq: int | None

Largest sequence currently present.

next_cursor property

next_cursor: int

Global SPMC cursor used by next().

revision property

revision: int

Monotonic mutation generation published to waiters.

size property

size: int

Number of chunk slots in the store.

status property

status: Status | None

Return the terminal status when closed, otherwise None.

total_chunks_put property

total_chunks_put: int

Number of chunks appended over the store lifetime.

a11.stores.redis_chunk_store.RedisChunkStoreKeys

The sharding-safe Redis keys owned by one node stream.

arrival_index property

arrival_index: str

Arrival-order-to-sequence hash.

blobs property

blobs: str

Hash containing large encoded chunk payloads.

events property

events: str

Pub/Sub channel used for invalidation notifications.

metadata property

metadata: str

Hash containing node-level metadata.

sequence_index property

sequence_index: str

Sequence-to-stream-entry hash.

stream property

stream: str

Redis Stream containing chunk and control entries.

script_keys

script_keys() -> list[str]

Return keys in the stable order used by the Lua state machine.

SQLiteChunkStore

Durable storage without a server: fragments live in one SQLite database per storage root, with payloads above 128 KiB moved into adjacent blob files. It is also the only backend that accepts NodeRef payloads, storing the reference target as indexed columns so traversal between nodes is a query.

The SQLite page covers the on-disk layout, durability, and configuration, and is where SQLiteChunkStore, SQLiteChunkStoreFactory, SQLiteChunkStoreOptions, and SQLiteChunkStoreMetadata are documented.

ChunkStoreReader

Create a reader at an offset and use next for one record or async iteration to drain its configured range.

a11.stores.chunk_store_reader.ChunkStoreReader

ChunkStoreReader(store: ChunkStore, options: ChunkStoreReaderOptions | dict[str, Any] | None = None)

Open a reader over store.

options (a ChunkStoreReaderOptions or plain dict) tunes ordering, buffering, starting offset, sticky mimetypes, and whether chunks are popped as they are read.

buffer_size property

buffer_size: int

Number of prefetched fragments currently held in the reader's buffer.

options property

The ChunkStoreReaderOptions this reader was created with.

store property

store: ChunkStore

The ChunkStore this reader draws fragments from.

cancel

cancel() -> None

Stop the background read pump. Pending next awaitables are resolved and no further chunks are fetched.

ensure_started

ensure_started() -> None

Start the background read pump if it is not already running. Reading normally starts it lazily; call this to begin buffering before the first next.

get_status

get_status() -> Status

Return the reader's current status. An agent can inspect this to distinguish a healthy stream from one that has failed or ended.

next

next(timeout: Duration = ...)

Return an awaitable for the next fragment in this reader's view.

It resolves to None after the configured range or final sequence is exhausted. timeout bounds this wait only; a timed-out read does not close the store or prevent a later call from continuing the stream.

Raises:

Type Description
StatusException

If the timeout expires, the store closes with an error, or the reader encounters invalid stream state.

Examples:

Resume a replay after an application checkpoint:

reader = ChunkStoreReader(store, {"offset": checkpoint + 1})
while fragment := await reader.next():
    await replay(fragment)

wait

wait() -> Future[None]

Await completion of the background read pump. The returned future resolves once the reader has drained the store or been cancelled.

a11.stores.chunk_store_reader.ChunkStoreReaderOptions

ChunkStoreReaderOptions(ordered: bool = True, pop_chunks: bool = False, num_chunks_to_buffer: SupportsInt | None = 32, offset: SupportsInt | None = 0, max_chunks_to_read: SupportsInt | None = None, sticky_mimetype: bool = False)

Construct validated options for a ChunkStoreReader.

max_chunks_to_read property writable

max_chunks_to_read: int | None

Optional cap on the total number of chunks to read.

num_chunks_to_buffer property writable

num_chunks_to_buffer: int

Maximum number of chunks to prefetch into the buffer.

offset property writable

offset: int

Sequence number at which reading begins.

ordered property writable

ordered: bool

Whether chunks are delivered strictly in sequence order.

pop_chunks property writable

pop_chunks: bool

Whether chunks are removed from the store as they are read.

sticky_mimetype property writable

sticky_mimetype: bool

Whether ordered chunks inherit the last explicitly set mimetype.

validate

validate() -> None

Raise if the options are not internally consistent.

ChunkStoreWriter

put_chunk returns a confirmation future after bounded admission. Await that future when the application must checkpoint only after storage accepts the chunk, then call drain_and_close at producer shutdown. This is the storage-level cursor; application code reaches it through a node, where finalize marks the logical end of the data and closes in one call. Closing also tees a closure marker to every attached stream, so a peer mirroring this node closes its own write half — see the node lifecycle.

a11.stores.chunk_store_writer.ChunkStoreWriter

ChunkStoreWriter(chunk_store: ChunkStore, options: ChunkStoreWriterOptions | dict[str, Any] | None = None)

Open a writer over chunk_store.

options (a ChunkStoreWriterOptions or plain dict) tunes the starting offset, sticky mimetypes, and how much is buffered/flushed at once.

options property

The ChunkStoreWriterOptions this writer was created with.

queue_size property

queue_size: int

Number of chunks currently waiting in the writer's flush queue.

store property

store: ChunkStore

The ChunkStore this writer persists chunks to.

abort_with_status

abort_with_status(status: Status) -> Future[None]

Abort the writer with an error status and await teardown. Readers then observe the error rather than a clean end-of-stream.

attach_stream

attach_stream(stream: WireStream) -> None

Tee stored fragments to an additional wire stream. After the store accepts a batch, the writer calls send on attached streams; a successful send confirms local transport admission, not peer delivery. A transport failure stops later writes but cannot revoke the current batch's store confirmations. The writer keeps the stream alive while attached.

cancel

cancel() -> Future[None]

Stop the writer immediately and await teardown, discarding any chunks still queued.

detach_stream

detach_stream(stream: WireStream) -> None

Stop mirroring fragments to a previously attached wire stream. Raises if the stream was not attached.

drain_and_close

drain_and_close() -> Future[None]

Flush every queued chunk, close the writer, and await completion. This does not append a final fragment: mark the last chunk final before draining when readers need a final sequence number.

enqueue_chunk

enqueue_chunk(chunk: Chunk, seq: SupportsInt | None = None, final: bool = False) -> tuple

Enqueue a chunk and get back a (confirmation, admission) pair of awaitables. Unlike put_chunk, this exposes backpressure explicitly: admission resolves when the chunk is accepted into the bounded queue (None if it fit immediately) and confirmation resolves with the sequence assigned by the backing store. An agent awaits admission to pace production and confirmation to know the store accepted the write.

ensure_started

ensure_started() -> None

Start the background flush loop if it is not already running. Writing normally starts it lazily; call this to begin flushing before the first chunk is enqueued.

flush

flush() -> None

Run the flush loop now, on this thread, if it is idle. A store that accepts the batch without waiting -- an in-memory one does -- has confirmed those writes by the time this returns, so their confirmations resolve without an event-loop turn. A store that cannot leaves the batch in flight and this does nothing, so chunks enqueued behind it still go out together. Awaiting a confirmation calls this for you; call it directly only to push a queue out without awaiting anything.

get_abort_status

get_abort_status() -> Status | None

Return the status the writer was aborted with, or None if it was not aborted. Use this to distinguish a clean close from an error-driven abort.

get_status

get_status() -> Status | None

Return the writer's terminal status, or None while it is still open. An agent can poll this to detect that the stream has closed or failed.

is_writable

is_writable() -> bool

Return whether the writer still accepts chunks. False once the stream has been drained, closed, or aborted.

put async

put(obj: Any, seq: int | None = None, final: bool = False) -> Future[int]

Write a chunk and return its store-confirmation future.

The writer operates at the chunk level; pass an already-serialized Chunk (use AsyncNode to write arbitrary Python objects). Returns a asyncio.Future resolving to the stored sequence number.

put_chunk async

put_chunk(chunk: Chunk, seq: int | None = None, final: bool = False) -> Future[int]

Enqueue a native chunk and return its store-confirmation future.

Set final=True on the last chunk when readers must know the logical end of the sequence. Calling drain_and_close later only flushes and closes the writer; it does not add that final marker for you. A node does both at once -- see AsyncNode.finalize.

Examples:

Checkpoint only after the store accepts the final event:

confirmation = await writer.put_chunk(
    a11.to_chunk(event), final=True
)
stored_seq = await confirmation
await checkpoints.save(stored_seq)

Awaiting the confirmation flushes the writer on this thread, so a store that accepts the write without waiting resolves it without an event-loop turn. Awaiting only this coroutine does not flush: the write goes out on the writer's own pump, which is what lets a producer run ahead of a slow store.

wait_for_buffer_to_drain

wait_for_buffer_to_drain() -> Future[None]

Await until the in-flight write buffer empties. An agent can use this as a backpressure checkpoint before enqueuing more chunks.

a11.stores.chunk_store_writer.ChunkStoreWriterOptions

ChunkStoreWriterOptions(offset: SupportsInt | None = 0, max_chunks_to_write_at_once: SupportsInt | None = 8, num_chunks_to_buffer: SupportsInt | None = None, sticky_mimetype: bool = False)

Construct validated options for a ChunkStoreWriter.

max_chunks_to_write_at_once property writable

max_chunks_to_write_at_once: int

Maximum number of chunks flushed to the store per batch.

num_chunks_to_buffer property writable

num_chunks_to_buffer: int | None

Optional bound on the in-flight write buffer size.

offset property writable

offset: int

Sequence number at which writing begins.

sticky_mimetype property writable

sticky_mimetype: bool

Whether repeated contiguous chunk mimetypes are omitted.

validate

validate() -> None

Raise if the options are not internally consistent.