We're open source. If actrone-memory has been useful to you, a star on GitHub means a lot to us.

Personal assistants that remember the person, not just the chat

Preferences, plans and personal details survive the end of a conversation. Every fact comes back tagged none, low, pii or sensitive, so you choose what reaches a hosted model.

npm install actrone-memory  ·  pip install actrone-memory

The problem

Every conversation starts cold

Chat history ends with the session, and replaying all of it to find one preference is slow and costly.
  • Users repeat themselves

    Their language, their diet and how brief they like answers have to be restated in every new conversation.

  • Replaying history is expensive

    Sending every past conversation to find one preference costs tokens on each request and still misses things.

  • Personal details need handling

    An assistant hears about health, money and family. Treating that like any other text is how it ends up somewhere it should not.

How it works

Four steps, one scope per user

Each user gets their own scope, such as user:42. Conversations are sessions inside it: they come and go, and what you remembered about the user stays.
  1. Remember what the user tells you

    injectMemory, in Python inject_memory

    Keep a fact under the user’s scope with the sensitivity you judge it to have: a language preference is low, an allergy is sensitive.

  2. Keep each conversation

    storeTurn, in Python store_turn

    Records each exchange in the conversation’s session, so the next reply in the same conversation sees it.

  3. Close the conversation, keep the person

    clearSession, in Python clear_session

    Drops the conversation’s turns. Long-term facts stay for the next conversation.

  4. Recall with tags attached

    retrieveContext, in Python retrieve_context

    Returns the relevant facts, each with its sensitivity, so you decide what a hosted model sees and what stays out.

The code

The whole integration, in both languages

The test suites of both packages run this code and check that a preference outlives the conversation that taught it, that the allergy comes back tagged sensitive and that one user never sees another’s facts.
assistant.ts
import { MemoryManager, type Sensitivity } from "actrone-memory";

// One scope per user: everything below is recalled for this user only.
const scopeFor = (userId: string) => `user:${userId}`;

// Keep something the user told you, with the sensitivity you judge it to have.
export async function rememberAboutUser(
  memory: MemoryManager,
  userId: string,
  fact: string,
  sensitivity: Sensitivity = "low",
) {
  return memory.injectMemory(scopeFor(userId), fact, 0.9, "profile", [], "user", sensitivity);
}

// Each conversation is a session: its turns feed the next reply in the same conversation.
export async function recordTurn(
  memory: MemoryManager,
  userId: string,
  conversationId: string,
  userMessage: string,
  reply: string,
) {
  await memory.storeTurn(scopeFor(userId), conversationId, userMessage, reply);
}

// Recall for a new message; each fact keeps its tag, so you decide what the model sees.
export async function recallFor(
  memory: MemoryManager,
  userId: string,
  conversationId: string,
  message: string,
) {
  const context = await memory.retrieveContext(scopeFor(userId), conversationId, message, 1500);
  return {
    facts: context.episodicMemories.map((m) => ({ content: m.content, sensitivity: m.sensitivity })),
    recentTurns: context.recentTurns.length,
  };
}

// Closing a conversation drops its turns. What you remembered about the user stays.
export async function endConversation(memory: MemoryManager, userId: string, conversationId: string) {
  await memory.clearSession(scopeFor(userId), conversationId);
}
assistant.py
from dataclasses import dataclass

from actrone_memory import MemoryManager, Sensitivity


def scope_for(user_id: str) -> str:
    """One scope per user: everything below is recalled for this user only."""
    return f"user:{user_id}"


async def remember_about_user(
    memory: MemoryManager, user_id: str, fact: str, sensitivity: Sensitivity = "low"
) -> str:
    """Keep something the user told you, with the sensitivity you judge it to have."""
    return await memory.inject_memory(
        agent_id=scope_for(user_id),
        content=fact,
        importance=0.9,
        session_id="profile",
        source="user",
        sensitivity=sensitivity,
    )


async def record_turn(
    memory: MemoryManager, user_id: str, conversation_id: str, user_message: str, reply: str
) -> None:
    """Each conversation is a session: its turns feed the next reply in the same conversation."""
    await memory.store_turn(
        agent_id=scope_for(user_id),
        session_id=conversation_id,
        user_message=user_message,
        assistant_message=reply,
    )


@dataclass
class Recall:
    facts: list[tuple[str, Sensitivity]]
    recent_turns: int


async def recall_for(
    memory: MemoryManager, user_id: str, conversation_id: str, message: str
) -> Recall:
    """Recall for a new message; each fact keeps its tag, so you decide what the model sees."""
    context = await memory.retrieve_context(
        agent_id=scope_for(user_id), session_id=conversation_id, query=message, token_budget=1500
    )
    return Recall(
        facts=[(m.content, m.sensitivity) for m in context.episodic_memories],
        recent_turns=len(context.recent_turns),
    )


async def end_conversation(memory: MemoryManager, user_id: str, conversation_id: str) -> None:
    """Closing a conversation drops its turns. What you remembered about the user stays."""
    await memory.clear_session(scope_for(user_id), conversation_id)

Create the manager once and pass it in: await MemoryManager.create() in TypeScript, or async with create_memory_manager() as memory: in Python.

Limits

What this does not do for a personal assistant

It remembers and recalls. The judgement stays with you.
  • It does not decide what is sensitive

    Tags come from you, or from fact extraction with a model you configure. A hosted extraction model sees the raw text.

  • It does not resolve contradictions

    If a user moves city, both facts stay until you delete the old one with deleteMemory. It stores and recalls; it does not reason about which fact is current.

  • Recall matches shared words until you add a model

    With no extra packages the embedder compares words, so “Write a short note in British English” finds the British English preference because the words overlap. Install fastembed in TypeScript or the onnx extra in Python for recall by meaning, still on your machine.

  • In-process until you add a store

    The default store lives in your process, so a restart forgets everything. Pass the Redis, Postgres or Qdrant adapters when memory has to outlive a deploy. The calls in the code do not change.

  • It runs where your code runs

    The libraries need Node.js 22 or newer, or Python 3.11 or newer. A mobile or browser assistant needs a backend to hold its memory.