Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@ keyword argument is a MINOR, even pre-1.0.

_Nothing yet._

## [0.4.0] — 2026-10-02

Three keyword arguments the API has accepted since the revision chain
shipped and the SDK had never learned, plus the two response fields that
make the chain visible. New public names, so a MINOR under the rule above;
nothing existing changes shape or wire behaviour.

### Added

- **`read(..., include_history=True)` and `recent(..., include_history=True)`**
ask for superseded revisions alongside live ones (`POST /memory/read` and
`/memory/recent`, `include_history`). Sent only when `True`, so a request
against an older core is byte-identical to before.
- **`read(..., fingerprint=...)` and `write(..., fingerprint=...)`**: an exact
episodic match on a fingerprint given at write time (at most 512
characters); `ReadResponse.episodic_hit` says whether it hit.
- **`MemoryItem.supersedes` / `.superseded_by` and `RecentItem.supersedes` /
`.superseded_by`**: the revision chain as the server reports it. Both are
omitted on the wire when they do not apply, so they default to `[]` and
`None`; a present `superseded_by` means the item is history.

## [0.3.2] — 2026-10-02

Package metadata and README text only. No public name, signature or request
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,10 @@ async with AsyncMnemoClient(api_key="mk_live_YOUR_KEY") as client:

| Method | Description |
|--------|-------------|
| `write(content, concepts, domain, metadata, external_ref, supersedes)` | Store a memory |
| `write(content, concepts, domain, metadata, external_ref, supersedes, fingerprint)` | Store a memory |
| `write_batch(items)` | Store up to 500 memories |
| `read(query, top_k, domain, since, until, order_by, exclude_author)` | Natural-language search — "what do I know about X" |
| `recent(domain, since, until, exclude_author, limit, cursor)` | Newest-first feed — "what happened lately" |
| `read(query, top_k, domain, since, until, order_by, exclude_author, include_history, fingerprint)` | Natural-language search — "what do I know about X" |
| `recent(domain, since, until, exclude_author, limit, cursor, include_history)` | Newest-first feed — "what happened lately" |
| `graph(seeds, depth, domain, min_weight, limit)` | Bounded read of the concept-association graph around `seeds` |
| `feedback(atom_ids, outcome)` | Report success/failure |
| `stats()` | Memory statistics |
Expand Down
2 changes: 1 addition & 1 deletion mnemoverse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
HealthResponse,
)

__version__ = "0.3.2"
__version__ = "0.4.0"

__all__ = [
"MnemoClient",
Expand Down
26 changes: 26 additions & 0 deletions mnemoverse/_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ async def write(
metadata: dict[str, Any] | None = None,
external_ref: str | None = None,
supersedes: list[UUID | str] | None = None,
fingerprint: str | None = None,
) -> WriteResponse:
"""Store a single memory atom.

Expand All @@ -197,6 +198,9 @@ async def write(
stored and every listed one is marked superseded by it, in a single
transaction — all of it or none. Rejected by the server (422) on
:meth:`write_batch`, and alongside an ``xroom:`` domain.
``fingerprint`` (at most 512 characters, a task hash for example) lets a
later :meth:`read` with the same fingerprint find this atom by exact
match instead of by similarity.
"""
body: dict[str, Any] = {"content": content, "domain": domain}
if concepts:
Expand All @@ -207,6 +211,8 @@ async def write(
body["external_ref"] = external_ref
if supersedes:
body["supersedes"] = [str(sid) for sid in supersedes]
if fingerprint is not None:
body["fingerprint"] = fingerprint
data = await self._request("POST", "/api/v1/memory/write", json=body)
return WriteResponse.model_validate(data)

Expand All @@ -231,9 +237,19 @@ async def read(
until: datetime | str | None = None,
order_by: str | None = None,
exclude_author: str | None = None,
include_history: bool = False,
fingerprint: str | None = None,
) -> ReadResponse:
"""Query memory with semantic search + Hebbian expansion.

``include_history=True`` asks for superseded revisions alongside live
ones: each carries ``superseded_by``, and any item that replaced
something carries ``supersedes``. History competes under the same
ranking and ``top_k`` as everything else, so a read may return part of
a chain; for the whole chain, follow ``superseded_by`` by id.
``fingerprint`` asks for an exact episodic match on a fingerprint given
at write time (``episodic_hit`` on the response says whether it hit).

``since`` / ``until`` bound the result by creation time, inclusive at
both ends; naive datetimes are read as UTC. ``order_by="recency"``
re-sorts the matched set newest-first without changing which entries
Expand Down Expand Up @@ -262,6 +278,10 @@ async def read(
body["order_by"] = order_by
if exclude_author is not None:
body["exclude_author"] = exclude_author
if include_history:
body["include_history"] = True
if fingerprint is not None:
body["fingerprint"] = fingerprint
data = await self._request("POST", "/api/v1/memory/read", json=body)
return ReadResponse.model_validate(data)

Expand All @@ -274,9 +294,13 @@ async def recent(
exclude_author: str | None = None,
limit: int = 20,
cursor: str | None = None,
include_history: bool = False,
) -> RecentResponse:
"""List the newest entries first — no query, complete by construction.

``include_history=True`` includes superseded revisions in the feed,
each carrying ``superseded_by``; by default the feed lists live tips.

The temporal complement of :meth:`read`. Nothing is ranked away, so this
is what to use when you need to be sure you are seeing everything:
resuming after a break, catching up on a shared room, or auditing.
Expand All @@ -297,6 +321,8 @@ async def recent(
body["exclude_author"] = exclude_author
if cursor:
body["cursor"] = cursor
if include_history:
body["include_history"] = True
data = await self._request("POST", "/api/v1/memory/recent", json=body)
return RecentResponse.model_validate(data)

Expand Down
17 changes: 15 additions & 2 deletions mnemoverse/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ def write(
metadata: dict[str, Any] | None = None,
external_ref: str | None = None,
supersedes: list[UUID | str] | None = None,
fingerprint: str | None = None,
) -> WriteResponse:
"""Store a single memory atom.

Expand All @@ -356,12 +357,14 @@ def write(
stored and every listed one is marked superseded by it, in a single
transaction — all of it or none. Rejected by the server (422) on
:meth:`write_batch`, and alongside an ``xroom:`` domain.
``fingerprint`` (at most 512 characters) lets a later :meth:`read` with
the same fingerprint find this atom by exact match.
"""
return self._run(
self._async_client.write(
content, concepts=concepts, domain=domain,
metadata=metadata, external_ref=external_ref,
supersedes=supersedes,
supersedes=supersedes, fingerprint=fingerprint,
)
)

Expand All @@ -382,9 +385,15 @@ def read(
until: datetime | str | None = None,
order_by: str | None = None,
exclude_author: str | None = None,
include_history: bool = False,
fingerprint: str | None = None,
) -> ReadResponse:
"""Query memory with semantic search + Hebbian expansion.

``include_history=True`` returns superseded revisions alongside live
ones (each carries ``superseded_by``); ``fingerprint`` asks for an exact
episodic match on a fingerprint given at write time.

``since`` / ``until`` bound the result by creation time, inclusive at
both ends. ``order_by="recency"`` re-sorts the matched set newest-first
without changing which entries matched. ``exclude_author`` drops one
Expand All @@ -402,6 +411,7 @@ def read(
concepts=concepts,
since=since, until=until,
order_by=order_by, exclude_author=exclude_author,
include_history=include_history, fingerprint=fingerprint,
)
)

Expand All @@ -414,18 +424,21 @@ def recent(
exclude_author: str | None = None,
limit: int = 20,
cursor: str | None = None,
include_history: bool = False,
) -> RecentResponse:
"""List the newest entries first — no query, complete by construction.

The temporal complement of :meth:`read`: nothing is ranked away, so use
this when you need to be sure you are seeing everything. Paged by
``next_cursor``, which continues the listing with no skips and no
duplicates even while writes are landing.
duplicates even while writes are landing. ``include_history=True``
includes superseded revisions, each carrying ``superseded_by``.
"""
return self._run(
self._async_client.recent(
domain=domain, since=since, until=until,
exclude_author=exclude_author, limit=limit, cursor=cursor,
include_history=include_history,
)
)

Expand Down
8 changes: 8 additions & 0 deletions mnemoverse/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ class MemoryItem(BaseModel):
# timestamp and no author even when the API sent both.
created_at: datetime | None = None
provenance: Provenance | None = None
# Revision chain, reported as a fact of the data regardless of server
# settings. Both are OMITTED on the wire when they do not apply: an item
# that replaced nothing has no ``supersedes``; the live tip has no
# ``superseded_by``. Present ``superseded_by`` means the item is history.
supersedes: list[UUID] = []
superseded_by: UUID | None = None


class RecentItem(BaseModel):
Expand All @@ -88,6 +94,8 @@ class RecentItem(BaseModel):
created_at: datetime
concepts: list[str] = []
provenance: Provenance | None = None
supersedes: list[UUID] = []
superseded_by: UUID | None = None


class RecentResponse(BaseModel):
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "mnemoverse"
version = "0.3.2"
version = "0.4.0"
description = "Official Python SDK for Mnemoverse — hosted AI agent memory that learns from outcomes. Sync and async REST client."
readme = "README.md"
license = "MIT"
Expand Down
Loading
Loading