Signatures quoted from source. Usage: Host a Statewire, Consume a Statewire. Wire: protocol spec. Related: Statewire reference.
Entry points
| Import from | Exports |
|---|---|
statewire | StateProxy, append, plain, StatewireReject, StatewireObserver, StatewireCommandContext, StatewireClientHandle, command, Statewire, HeldFrame |
statewire.client | StatewireClient, Timing, StatewireSendError, StatewireHttp, StatewireWebsocket, apply_ops, StatewireProtocolError, StatewireVersionError, TransportError |
statewire.state | collect_meta |
statewire.testing | command_context |
statewire.langgraph | replace_messages, append_langgraph_event |
statepatch | observable, Observable, Proxy, append, plain, Op, Path, Segment, Listener, WireEncodingError (ValueError subclass for encoding diagnostics) |
statewire
StateProxy, append, plain
Re-exports of statepatch's Proxy, append, and plain. Statewire.state is a StateProxy.
StatewireReject, StatewireObserver
class StatewireReject(Exception):
def __init__(self, message: str, *, payload: Any = None) -> None
class StatewireObserver:
def on_admit(self, method: str | None, outcome: str) -> None
def on_snapshot(self, client_id: str, size_bytes: int, elapsed: float) -> None
def on_duplicate_statement(self, client_id: str, seq: int, method: str, verdict: str | None, admitted_through: int) -> None
def on_start(self, method: str) -> None
def on_applied(self, method: str, elapsed: float) -> None
def on_settle(self, method: str, verdict: str, reason: str | None, elapsed: float) -> NoneStatewireReject is the definitive rejection: a rejected verdict, the seq consumed, ops recorded before the raise standing. StatewireObserver is the telemetry seam set as observer. on_applied and on_settle receive seconds since command receipt. on_snapshot receives the encoded hello size and seconds spent constructing and encoding it, before attach hooks.
| Argument | Values |
|---|---|
outcome | executed, absorbed, refused, or the rejection code |
on_settle.verdict | accepted, rejected, failed, or crashed |
on_duplicate_statement.verdict | result, rejected, failed, crashed, applied, expired, or None while pending |
StatewireCommandContext, StatewireClientHandle
class StatewireCommandContext:
caller: StatewireClientHandle
attach: StatewireAttach
context: dict[str, Any] # snapshot captured at command admission
version: str # the command protocol's version the submitting attach negotiated
def applied(self) -> None
async def dispatch(self, target: Callable[..., Any], *args: Any) -> str
class StatewireClientHandle:
client_id: str
auth_result: Any
is_connected: boolapplied() releases the command's receipt while the handler keeps running (idempotent, no-op after settle). dispatch is exactly-once admission into a tray @inbox keyed by (client_id, seq); on a host with no durability it raises RuntimeError: ctx.dispatch requires a durable host.
command
def command(target: Handler | str) -> Handler | Callable[[Handler], Handler]@command registers an async def instance method under its own name. @command("name") renames. Keyword-only ctx and auth_result parameters are injected. TypeError on a non-async def handler, a non-instance method, a positional ctx/auth_result, or a caller parameter.
Statewire
class Statewire(PinnedAPI):
@classmethod
def command_names(cls) -> tuple[str, ...]
state: StateProxy # settable with a dict or Observable; set in lifespan before yield
async def lifespan(self) -> AsyncIterator[None]
async def prepare(self, auth_result: Any, conn: HTTPConnection) -> None
def attaching(self, attach: StatewireAttach) -> None
def validate_context(self, protocol: str, value: dict[str, Any], attach: StatewireAttach) -> None
def context_changed(self, protocol: str, value: dict[str, Any], attach: StatewireAttach) -> None
def attached(self, client_id: str, auth_result: Any, snapshot: bytes) -> None
def detached(self, attach: StatewireAttach) -> None
def filter(self, protocol: str, key: tuple[str, ...] | None, attach: StatewireAttach) -> Filter | UNFILTERED
def pushback_ms(self) -> int | None
def client_departed(self, client_id: str) -> None; def has_subscriber(self, client_id: str) -> bool
def flush(self) -> None; def schedule(self, fn: Callable[[], None]) -> None; def drain(self) -> None
def create_task(self, coro: Coroutine[Any, Any, Any]) -> asyncio.Task[Any]
async def idle(self) -> None
def error(self, message: str | None = None, *, payload: Any = None, retry: int | None = None) -> None
def gone(self, message: str | None = None, *, payload: Any = None) -> None
def reconnect(self, message: str | None = None, *, code: str | None = None, retry: int | None = None, payload: Any = None) -> None # ends the host
def reattach(self, match: Callable[[StatewireAttach], bool] | None = None, code: str = "reauthorize", *, retry: int | None = None) -> None
def stats(self) -> dict[str, Any] # {"commands": settlements that were accepted or applied}
async def start_session(self, client_id: str, auth_result: Any, *, ctx: Sequence[dict[str, Any]] = (), cmd: Sequence[dict[str, Any]] = ()) -> AsyncIterator[dict[str, Any]]
async def reauthorize_admission(self, identity_doc: Any) -> Any
async def held_frames(self, client_id: str | None = None) -> list[HeldFrame]
async def discard_held_frames(self, client_id: str, frames: Sequence[HeldFrame] | None = None) -> list[HeldFrame]Host base class mounting GET|POST /stream, POST /frames, and /ws. __init_subclass__ raises TypeError on a non-positive max_subscribers/max_inflight_commands, an invalid max_lease_lifetime, a @route on a reserved path, a handler shadowing a runtime member, or a statewire/ method name. pushback_ms runs only for a frame carrying new statements and must return a non-negative int or None (TypeError otherwise); a delay refuses the statements with a {syn} carrying retry. Every host also serves the built-in statewire/commands command, answering the sorted command names. start_session is the in-process attach: decoded packets, snapshot first, until the fin; ValueError on an invalid client id or lane. Reading state before it is set raises RuntimeError, as does a lifespan that yields without setting it or yields twice; setting it to a non-dict raises TypeError, to an already-mounted root ValueError. attached raising fails that attach. drain() raises RuntimeError once one callable has run 50 times within a single drain.
Lease context
StatewireClient(context={"acme-chat": {"locale": "en"}}) retains an initial object per protocol. client.set_context("acme-chat", {"locale": "de"}) replaces that object. The client restores its latest values whenever an attachment is rebuilt, including a fresh hello on a preserved socket. Updates use native ctx entries, without consuming command sequence numbers or producing command verdicts.
validate_context(protocol, value, attach) validates a candidate before any entry in the frame is installed. context_changed(protocol, value, attach) runs synchronously after installation and before command admission. The attach exposes caller (including its authenticated auth_result) and context(protocol), a detached copy of its current object. Command handlers read ctx.context, the snapshot captured when that command was admitted; later replacements cannot change a queued command's context. Durable recovery preserves this original snapshot.
Context uses the document encoding, including datetime metadata, and a 64 KiB limit per entry. Full replacements are ordered within a lease; there are no revisions or incremental patches. Context is connection state rather than a durable command log.
Read filters
ANY: Any # path segment: any one object key or array index; repr "statewire.ANY"
UNFILTERED: Any # Statewire.filter's "send as is"; repr "statewire.UNFILTERED"
@dataclass(frozen=True, eq=False)
class Filter:
entries: tuple[tuple[tuple[str | ANY, ...], Callable[[Any], Any]], ...]
class StatewireAttach:
def reattach(self, code: str = "reauthorize", *, retry: int | None = None) -> NoneFilter(entries)validates at construction: a path segment is a nonempty string orANY, a rule is callable, and no two entries can match the same node.TypeErrorotherwise. Aliased asStatewire.Filter,Statewire.ANY,Statewire.UNFILTERED.- A rule receives the matched node's plain value (datetimes intact) and returns
"keep",None(null the slot),{"pick": [...]}or{"omit": [...]}. Nested entries apply inside the kept part.pick/omiton a non-dict, or any other return, fails the filter. filter(protocol, key, attach)runs synchronously for each document entering an attach's view: in registration afterattaching, and at the flush that adds a document. The decision holds for the mount's lifetime. ReturningNoneraisesTypeError.- A raising decider, or a rule failure, ends the attaches it serves with
finerror/filter-failed; other attaches continue. Over HTTP the attach answers500 {"fin": …};start_sessionyields thatfinand ends. - Attaches sharing one document and one
Filterinstance compute the filtered value and ops once per flush. - Rules re-run on the final state for every node an op touched at or under, once per flush. A changed instruction sends one
replaceof the node. - Adapters (
state_down,documents_down) receive the filtered view. reattachends the attach withfinreconnect/code; the host keeps serving.Statewire.reattach(match)does it for every live attachmatchaccepts. The client reattaches:authorize,prepareandfilterrun again. Durable-lane commands stay pending and resend; ephemeral-lane commands are lost.- Read
filter's roles from host state.preparecan load missing entries before the hello. command_names()returns the sorted registered command names as a tuple.
Durable admission records
@dataclass(frozen=True)
class HeldFrame:
client_id: str
location: str # "pool" | "quarantine"
statements: list[dict[str, Any]] # the stored record
def export(self) -> dict[str, Any] # {"client_id", "location", "statements"}, JSON-safeDurable hosts keep one client_commands tray row per client id. Pool entries:
| Stored entry | Runs |
|---|---|
{"statewire_admission": 1, "auth", "statements"} | Under auth |
Bare statement list, with identity set and every seq at or below identity_through[protocol] | Under identity |
| Bare statement list, otherwise | Never: held in place |
admission_record = "list"writes bare lists and advancesidentity_through;"envelope"writes envelopes. Both modes read every shape.- A held frame stays in
poolunchanged, row mark included. Frames behind it wait. - Seqs of held frames answer
rejectedwithcode: "identity-unavailable", on re-sends and on gap-fill attaches. - While a client id holds frames, new durable statements settle
rejectedidentity-unavailableand their seqs are consumed; nothing runs. quarantineentries written by statewire 0.20.0–0.21.0 count as held frames.- A record with sequence history, no identity, and no held frames binds
identityto the next attach'sauth_result. A differentauth_resultlater is refused with 403. reauthorize_admission(identity_doc)runs before a pooled frame without a live lease: a cold replay after a restart, or an attach that ended before its frame ran. Its return value is the frame'sauth_result. Default:identity_doc.StatewireRejectsettles every statementrejectedwith itsmessageandcode; any other exception is logged and settlesrejectedwithcode: "unauthorized".held_framesreads everyclient_commandsrow of the instance.discard_held_framesremoves the given frames (default: all of the client id's) after the durable receipt, keeps their seqs consumed, and resumes the frames behind them.ValueErrorfor a frame that is not held;RuntimeErroron a host without durability.
Upgrade steps: Upgrading from 0.19 with existing data.
Statewire class attributes
Set on the subclass; the SocketHostOptions knobs.
| Name | Type | Default | Meaning |
|---|---|---|---|
wire_version | VersionRange | VersionRange("2026-11-02", "2026-09-30") | Served Statewire versions; 2026-11-01 attaches gap-fill the hello from res. VersionRange(version, min_version=None) takes its newest version first: a host lowering the floor keeps the current wire with VersionRange(WIRE_VERSION, "2026-09-13") (statewire.protocol.WIRE_VERSION). A version newer than WIRE_VERSION raises at class creation |
stream_queue_size | int | 256 | Queued SSE packets before a lagging stream closes |
max_lease_lifetime | float | None | 86400.0 | Maximum authorization lease age in seconds; finite positive, None disables. Expiry ends with reconnect / lease-expired |
heartbeat_interval | float | 15.0 | Heartbeat interval, seconds |
ws_init_timeout | float | 5.0 | WebSocket init and authorization window, seconds |
client_record_limit | int | 1024 | LRU cap on stored client records |
max_subscribers | int | 1024 | Concurrently attached stream subscribers; an attach past it is refused (503) |
max_inflight_commands | int | 1024 | Commands admitted but not fully released across every client (statewire.limits.MAX_INFLIGHT_COMMANDS_PER_HOST) |
fin_on_idle | bool | False | Fin every attach at the idle flag's rising edge |
observer | StatewireObserver | None | None | Telemetry hooks |
admission_record | str | "envelope" | Durable pool entry shape: "envelope" or "list" (records); anything else raises ValueError |
statewire.state
def collect_meta(value: Any, root: list[str | int] | None = None) -> list[dict[str, Any]] | None: ...- Returns datetime metadata paths in traversal order, relative to
root. - Use with
ops.add_opandops.replace_opwhen encoding projected values containing dates.
statewire.client
Outside statewire.__all__.
StatewireClient
class StatewireClient:
def __init__(self, transport: Any, *, context: dict[str, dict[str, Any]] | None = None, timing: Timing | None = None, on_connection_change: Callable[[dict[str, Any]], None] | None = None, on_command_session_lost: Callable[[dict[str, Any]], str | None] | None = None, on_done: Callable[[], None] | None = None)
@classmethod
def sse(cls, url: str, **kwargs: Any) -> "StatewireClient" # transport kwargs: headers: dict[str, str] | None, http: httpx.AsyncClient | None
@classmethod
def ws(cls, url: str, **kwargs: Any) -> "StatewireClient" # transport kwargs: connect: Callable[[str, list[str]], Awaitable[Any]] | None, query: dict[str, str] | None, headers
async def __aenter__(self) -> "StatewireClient"; async def __aexit__(self, *exc: Any) -> None; async def aclose(self) -> None
commands: _CommandsProxy # client.commands.<method>(*params) -> asyncio.Future
def send(self, method: str, *params: Any) -> asyncio.Future[Any]
def set_context(self, protocol: str, value: dict[str, Any]) -> None
def subscribe(self, listener: Callable[[Any], None]) -> Callable[[], None]
async def wait_state(self, predicate: Callable[[Any], bool] | None = None, *, timeout: float = 5.0) -> Any
state: Any; connection: dict[str, Any]; client_id: str; durable_seq: int; idle: bool # properties
class StatewireSendError(Exception):
def __init__(self, failure: dict[str, Any]) # .failure: the JS SendFailure shape as a dictsse/wspop the transport kwargs, passtimingto both the transport and the client, and forward the rest (on_connection_change,on_command_session_lost,on_done) to the constructor.on_command_session_lostis called per held command after a record gap with{"method", "params", "seq"}. Returning"replay"resends it under a fresh seq.on_donefires on the broadcast idle flag's rising edge.wait_statedefaults tostate is not Noneand raisesTimeoutError(statewire: wait_state timed out).sendoff a closed client rejects{"fate": "aborted", "cause": "unmount"}. After a terminal fin (gone, or a second consecutiveunauthorized) it rejects{"fate": "aborted", "cause": "stream-gone"}; any other fin reattaches andsendkeeps working.set_contextraisesTypeErrorfor a non-dict.- An auth denial (
fin: unauthorized) gets one redial withcause: "unauthorized"; a consecutive repeat parks the client asgone. connectionvalues and the reconnect backoff: Python client.
Timing
@dataclass
class Timing:
backoff_base: float = 0.25
backoff_max: float = 30.0
backoff_reset: float = 30.0
heartbeat_timeout: float = 30.0
rng: random.Random = field(default_factory=random.Random)| Name | Type | Default | Meaning |
|---|---|---|---|
backoff_base | float | 0.25 | First reconnect wait, seconds |
backoff_max | float | 30.0 | Reconnect wait ceiling, seconds; also caps host retry hints |
backoff_reset | float | 30.0 | Liveness after which the attempt count restarts, seconds |
heartbeat_timeout | float | 30.0 | Bound on each read and POST /frames, seconds |
rng | random.Random | random.Random() | Jitter source |
statewire.testing
async def command_context(sw: Statewire, *, client_id: str = "test", auth_result: Any = None, context: dict[str, Any] | None = None, seq: int = 1) -> tuple[StatewireCommandContext, Callable[[], list[dict[str, Any]]]]- Returns a real context for calling a
@commandhandler directly, pluseffects(): each call flushes the host and returns every envelope delivered to the client so far, decoded (attach snapshot excluded). - Raises
TypeErroroff a non-Statewire,RuntimeErrorwhilestateis unset,TypeErroron a non-dictcontext,ValueErroron an invalidclient_idorseq < 1.
statewire.langgraph
Extra statewire[langgraph] (langchain-core>=0.3).
def replace_messages(state: Any, messages: list[Any]) -> None
def append_langgraph_event(state: Any, _namespace: str, type: str, payload: Any) -> Nonereplace_messagesrebaselinesstate["messages"]tomessageswith granular ops.append_langgraph_eventfolds oneastreamevent:"messages"merges the chunk into the message with the sameidortool_call_id(an openAIMessageChunktail otherwise) or appends it;"updates"writes each non-messageschannel tostate[channel]. Host-stamped keys on an existing message survive the merge.
statepatch
observable, Observable, Proxy
def observable(initial: Any = None) -> Observable
class Observable:
value: Any # plain snapshot; assignment emits a root replace
def subscribe(self, listener: Listener) -> Callable[[], None]
# forwards to Proxy: __getitem__, __setitem__, __delitem__, __contains__, __iter__, __len__,
# append, insert, pop, remove, clear, extend, update, get, setdefault, keys, values, items
class Proxy:
def __init__(self, root: Observable, path: Path) -> None
# __getitem__, __setitem__, __delitem__, __contains__, __iter__, __len__, __eq__, __bool__,
# append, insert, pop, remove, clear, extend, update, get, setdefault, keys, values, items, indexObservable is one observed JSON tree. Every mutation emits an Op. An observable stored inside another mounts its op stream at that path. Wrapping an observable directly raises TypeError. Proxy is the mutation cursor: reads resolve live, writes record ops on the deepest mounted observable. Writes deep-copy and validate: string keys, finite floats, ints within ±(2**53 - 1), timezone-aware datetimes. Anything else raises TypeError/ValueError.
append, plain, snapshot
def append(parent: Any, key: Segment, suffix: str) -> None
def plain(value: Any) -> Any
def snapshot(value: Any) -> Anyappend emits one end-offset text-insert add op (TypeError on a parent that is neither proxy nor observable, a non-string suffix, or a non-string target; empty suffix is a no-op). plain resolves a proxy or observable to its stored node, the live node or a deep snapshot when a mounted observable sits under it (mutations there emit no ops), and passes anything else through.
snapshot copies the selected value into an independent plain tree, resolving proxies and mounted observables. Pass a field or subtree to copy only that scope.
Op, Path, Segment, Listener
Segment = str | int
Path = tuple[Segment, ...]
class Op(NamedTuple):
kind: str # "replace" | "add" | "remove"
path: Path
value: Any = None
Listener = Callable[[Op], None]