osyra_ome.adapters.langchain.chat_history
Private pre-release / network boundary: The currently usable customer surface is offline verification of an existing signed artifact against an out-of-band trust anchor. Memory/network RPC APIs and host snippets on this page describe protocol and interface shape; the public Edge bridge, package publication, and release acceptance remain pending. Do not treat
api.osyra.ai:443examples as a released customer route.
LangChain :class:`BaseChatMessageHistory` backed by the OME memory namespace.
Each call to :meth:`OmeChatMessageHistory.add_messages` persists every message as an OME :class:`~osyra_ome.Claim` written via ``client.memory.write_claim``. The session id is encoded as the ``subject_id`` (``did:web:session.osyra.ai/<session_id>``) so all messages in a conversation thread are queryable by subject.
:meth:`messages` is intentionally NOT a thin GET — the v1.0.0 SDK exposes ``client.memory.get_claim`` (single-row read) but no list-by- subject batch endpoint. To keep adapter behaviour predictable in the v0.1 window, ``OmeChatMessageHistory`` maintains an in-process buffer of message ids it has written and re-fetches each one on ``messages``. Workloads that need server-side replay should use the v0.2 ``list_claims`` surface when it lands (deferred-ENG-FU per design doc §3.3 row "memory.list_claims by subject").
This adapter ships sync (:class:`OmeChatMessageHistory`) and async (:class:`OmeAsyncChatMessageHistory`) variants. The sync class wraps :class:`osyra_ome.OmeClient`; the async class wraps :class:`osyra_ome.OmeAsyncClient`.
Example::
from osyra_ome import OmeClient
from osyra_ome.adapters.langchain import OmeChatMessageHistory
client = OmeClient(target="api.osyra.ai:443", token="ey...",
workspace_id="ws-prod")
history = OmeChatMessageHistory(
client=client,
session_id="user-42-session-abc",
)
history.add_user_message("Hello")
history.add_ai_message("Hi there")
print(history.messages) # [HumanMessage("Hello"), AIMessage("Hi there")]Classes
class OmeAsyncChatMessageHistory
Bases: BaseChatMessageHistory
Async LangChain chat-message-history backed by OME.
Args:
client: a :class:osyra_ome.OmeAsyncClient instance.
session_id: identifier for the conversation thread; encoded
into the subject_id of every emitted Claim.
workspace_id: OME workspace UUID-like id; if omitted, defers
to the workspace_id bound on the wrapped client.
verify_on_read: bind each replayed body to the claim SIGNATURE
(DEFAULT True). See the sync :class:OmeChatMessageHistory.
trusted_signer_key: caller-pinned 32-byte Ed25519 trust anchor;
REQUIRED when verify_on_read is True (H7).
Methods
__init__
def __init__(self, *, client: OmeAsyncClient, session_id: str, workspace_id: str | None = None, verify_on_read: bool = True, trusted_signer_key: bytes | None = None) -> Noneaadd_messages (async)
async def aadd_messages(self, messages: Sequence[BaseMessage]) -> NoneAsync persist each message as an OME Claim.
aclear (async)
async def aclear(self) -> Noneadd_messages
def add_messages(self, messages: Sequence[BaseMessage]) -> NoneSync convenience that errors out for the async client.
The base abstract requires a ``add_messages`` even on async consumers, so we satisfy the contract with a guard that surfaces a clear message rather than silently calling the async path on the wrong event loop.
aget_messages (async)
async def aget_messages(self) -> list[BaseMessage]Async replay of all messages written via this adapter.
Same replay semantics as the sync sibling's :attr:`messages`:
- ORDER (arch Low): WRITE order (iterating the in-process written-claim cache), NOT timestamp-sorted like the TS sibling — to be aligned when the v0.2 list-by-subject surface lands.
- SOURCE (arch Low): on the real v0.1 wire, ``ClaimProjection`` strips the body (``get_claim(claim_id).body == b""``), so production replay binds an empty body and FAILS CLOSED (returns no data, never forged data); the in-process write-cache is the only v0.1 preimage+body source. Full cross-instance replay needs the v0.2 preimage-bearing ``GetClaimBody`` (ENG-2225).
- Per-claim isolation (arch Nit): a signed-but-foreign claim with no ``"message"`` envelope key is SKIPPED, not a KeyError.
clear
def clear(self) -> Nonemessages @property
def messages(self) -> list[BaseMessage]Synchronous read of the cached message buffer.
Async LangChain consumers typically prefer :meth:`aget_messages`; this sync property is provided as a fallback for the parts of the LangChain runnables surface that touch ``.messages`` synchronously (a known wart in the framework). When the sync path is hit on an async adapter, we surface the locally cached buffer and DO NOT issue an RPC — that would deadlock on the running event loop.
session_id @property
def session_id(self) -> strworkspace_id @property
def workspace_id(self) -> strwritten_claim_ids @property
def written_claim_ids(self) -> tuple[str, ...]class OmeChatMessageHistory
Bases: BaseChatMessageHistory
LangChain chat-message-history backed by the OME memory namespace.
Args:
client: a :class:osyra_ome.OmeClient instance (sync).
session_id: identifier for the conversation thread; encoded
into the subject_id of every emitted Claim.
workspace_id: OME workspace UUID-like id; if omitted, defers
to the workspace_id bound on the wrapped client.
verify_on_read: when True (the DEFAULT), every replayed message
body is bound to the SIGNATURE-attested content hash of the
claim it was written under (ENG-2413). The adapter caches the
:class:~osyra_ome.types.SignedClaim it wrote (the v0.1-wire
source of the signing preimage) and re-binds the read-back body
to it; a tampered body fails CLOSED (raises). Set False ONLY to
opt out (you then trust the transport / verify out-of-band).
trusted_signer_key: caller-pinned 32-byte Ed25519 public key used as
the trust anchor for verify_on_read. REQUIRED when
verify_on_read is True — the in-band signer_pub is NOT a
trust anchor, so the constructor fails CLOSED (raises) without it
(H7 parity with the TS adapter).
Methods
__init__
def __init__(self, *, client: OmeClient, session_id: str, workspace_id: str | None = None, verify_on_read: bool = True, trusted_signer_key: bytes | None = None) -> Noneadd_messages
def add_messages(self, messages: Sequence[BaseMessage]) -> NonePersist each message as an OME Claim and audit the write.
clear
def clear(self) -> NoneForget the in-memory buffer of written claim ids.
v0.1 NOTE: this DOES NOT tombstone the underlying OME claims — tombstone is a v0.2 surface (``client.memory.tombstone``). To cryptographically erase the conversation thread, upgrade to v0.2 and pair :meth:`clear` with a follow-up loop over :attr:`written_claim_ids` calling tombstone() with reason "langchain.adapter.clear".
Bookkeeping: clears the cached read-back and the per-instance buffer so subsequent reads on this adapter start from empty.
messages @property
def messages(self) -> list[BaseMessage]Replay all messages written via this adapter instance.
v0.1 surface: re-fetches each written claim from the OME memory namespace and reconstitutes the LangChain message via :func:`messages_from_dict`. Cached after the first call; invalidated on :meth:`add_messages` and :meth:`clear`.
Replay ORDER (arch Low): messages come back in WRITE order — this iterates the in-process written-claim cache (:attr:`written_claim_ids`), it does NOT timestamp-sort like the TS sibling. To be aligned when the v0.2 list-by-subject surface lands (ENG-2225 ``GetClaimBody``); see the wire-source note below.
Replay SOURCE (arch Low): on the real v0.1 wire, ``ClaimProjection`` STRIPS the body — ``get_claim(claim_id).body`` is ``b""`` — so a production replay binds an EMPTY body and FAILS CLOSED (returns NO data, never forged data), because the signature-attested ch of a real message never equals ``SHA-256(b"")``. The in-process write-cache populated by :meth:`add_messages` is therefore the ONLY v0.1 preimage+body source. Full cross-instance replay needs the v0.2 preimage-bearing ``GetClaimBody`` (ENG-2225).
Per-claim isolation (arch Nit): a genuinely-signed-but-foreign claim whose decoded envelope lacks the adapter ``"message"`` key is SKIPPED (post-binding, NOT a forge vector), not a KeyError that aborts the whole replay — see :func:`_extract_serialized_message`.
session_id @property
def session_id(self) -> strThe user-supplied LangChain session identifier.
workspace_id @property
def workspace_id(self) -> strThe OME workspace id the chat history binds to.
written_claim_ids @property
def written_claim_ids(self) -> tuple[str, ...]The list of claim ids this adapter instance has written so far.