Python client for Neotoma — the deterministic personal memory engine.
Used by Claude Code hook plugins written in Python, OpenAI Agents SDK adapters, and LangChain/LangGraph callbacks. Ships HTTP-only (Neotoma's engine runs under Node; this client reaches it over REST).
pip install neotoma-clientNeotomaMemory enforces the canonical Neotoma turn lifecycle — bounded retrieval, user-phase store, assistant-phase store, deterministic idempotency keys — so your agent doesn't have to.
from neotoma_client import NeotomaClient, with_memory
client = NeotomaClient(base_url="http://127.0.0.1:3080", token="dev-local")
def my_agent(user_message: str, ctx) -> str:
# ctx.retrieved contains entities matched from the user message
return f"Reply to: {user_message}"
wrapped = with_memory(
my_agent,
transport=client,
conversation_id="conv-2026-05-20",
platform="my-agent",
)
result = wrapped("Tell me about Acme Corp")
print(result.assistant_message)Async variant:
import asyncio
from neotoma_client import NeotomaClient, with_memory
async def async_agent(user_message: str, ctx) -> str:
return f"Reply to: {user_message}"
async def main() -> None:
client = NeotomaClient(base_url="http://127.0.0.1:3080")
wrapped = with_memory(async_agent, transport=client, conversation_id="conv-1")
result = await wrapped.acall("Hello")
print(result.assistant_message)
asyncio.run(main())Explicit lifecycle (when you need finer control):
from neotoma_client import NeotomaClient, NeotomaMemory
client = NeotomaClient(base_url="http://127.0.0.1:3080")
memory = NeotomaMemory(client, conversation_id="conv-123", platform="my-agent")
opened = memory.open_turn(turn_id="t1", user_message="Hello")
reply = my_llm(opened.retrieved) # pass retrieved context to your LLM
memory.close_turn(turn_id="t1", assistant_message=reply,
refers_to=opened.retrieved_entity_ids)from neotoma_client import NeotomaClient
client = NeotomaClient(base_url="http://127.0.0.1:3080", token="dev-local")
client.store({
"entities": [
{"entity_type": "task", "title": "Review plan", "due_date": "2026-04-15"}
],
"idempotency_key": "my-hook-2026-04-09-001",
})Async:
import asyncio
from neotoma_client import NeotomaClient
async def main() -> None:
async with NeotomaClient(base_url="http://127.0.0.1:3080") as client:
result = await client.aretrieve_entities({"entity_type": "task", "limit": 10})
print(result)
asyncio.run(main())NeotomaMemory(transport, *, conversation_id, platform=None, ...)— enforces store-first turn protocolopen_turn(turn_id, user_message)→OpenTurnResult(retrieval + user-phase store)close_turn(turn_id, assistant_message, refers_to=None)→CloseTurnResult(assistant-phase store)aopen_turn/aclose_turn— async variants
with_memory(agent_fn, *, transport, conversation_id, ...)→WrappedAgentwrapped(user_message)→WithMemoryResult(sync)await wrapped.acall(user_message)→WithMemoryResult(async)
store_chat_turn(transport, *, conversation_id, turn_id, messages)→StoreChatTurnResultretrieve_or_store(transport, *, identifier, entity_type, create)→RetrieveOrStoreResult
Sync methods:
store(input)— create or update entitiesretrieve_entities(input)— list by type / time windowretrieve_entity_by_identifier(input)— single entity lookupretrieve_entity_snapshot(entity_id=...)— snapshot with provenancelist_observations(input)— observation historylist_timeline_events(input)— timeline windowretrieve_related_entities(input)— graph expansioncreate_relationship(input)— link two entitiescorrect(entity_id=..., corrections=...)— supersede prior observationlist_entity_types(search=...)— schema discoveryget_entity_type_counts(input)— per-type cardinalityexecute_tool(name, args)— escape hatch
Async variants are prefixed with a (astore, aretrieve_entities, …).
See docs/specs/MCP_SPEC.md and openapi.yaml for complete action semantics.
MIT