> Full SurrealDB documentation index: https://surrealdb.com/docs/llms.txt

# Adding memory to an existing app

Introduce turns without replacing your LLM client.

Most teams do not rebuild their application to add memory. This guide covers the minimal integration path: intercepting existing LLM calls to extract memory, injecting context before those calls, and gradually expanding the integration without disrupting what already works.

## The minimal integration

SurrealDB Agent Memory's minimum viable integration is two operations around your existing LLM call:

1. **Before the call** - retrieve relevant context and prepend it to the system prompt.
2. **After the call** - record the user message and assistant response as turns.

No sessions are required for the first pass. You keep your existing data model and LLM client; SurrealDB Agent Memory sits beside them as the memory layer.

```python
from surrealdb.memory import AsyncMemory

memory = AsyncMemory(context="my-app", api_key="sp-...")

CONTEXT_ID = "my-app"
DEFAULT_SCOPE = ["org/my-org"]  # Start with a single scope

async def call_llm_with_memory(user_message: str,
    session_id: str | None = None) -> str:
    # 1. Create a session on first call; afterwards carry the id forward.
    #    There is no "open" call - a session is addressed by its id.
    if session_id is None:
        session = await memory.sessions.create(scopes=DEFAULT_SCOPE)
        session_id = session.id

    # Session context is one string, ready for a prompt
    ctx = await memory.sessions.context(session_id, user_message)

    # 2. Inject into your existing system prompt
    system = your_existing_system_prompt()
    if ctx.context:
        system = f"{system}\n\n## Relevant context\n{ctx.context}"

    # 3. Your existing LLM call - unchanged
    response = your_existing_llm_call(system=system, user=user_message)

    # 4. Record the exchange
    await memory.remember(user_message, session_id=session_id, role="user")
    await memory.remember(response, session_id=session_id, role="assistant")

    return response
```

```typescript
import { AgentMemory } from "@surrealdb/memory";

const memory = new AgentMemory({ context: "my-app", apiKey: "sp-..." });

const DEFAULT_SCOPE = ["org/my-org"];

async function callLlmWithMemory(
    userMessage: string,
    sessionId?: string,
): Promise<{ response: string; sessionId: string }> {
    const id = sessionId
        ?? (await memory.sessions.create({ scopes: DEFAULT_SCOPE })).id;

    // The JavaScript SDK reads session context through a Session object.
    // With only the id to hand, read the Context's memory through the same scope.
    const ctx = await memory.context(userMessage, { lens: DEFAULT_SCOPE });

    let system = yourExistingSystemPrompt();
    if (ctx.context) {
        system = `${system}\n\n## Relevant context\n${ctx.context}`;
    }

    const response = await yourExistingLlmCall({ system, user: userMessage });

    await memory.remember(userMessage, { sessionId: id, role: "user" });
    await memory.remember(response, { sessionId: id, role: "assistant" });

    return { response, sessionId: id };
}
```

The key principle: **do not change what your LLM receives if there is no relevant memory**. The `if ctx.context` guard ensures that when SurrealDB Agent Memory has nothing useful to add, the call is identical to the original.

## Intercepting existing LLM calls

If your application already has a wrapper around LLM calls, add memory extraction at that layer. This avoids scattering SurrealDB Agent Memory calls throughout your codebase.

```python
# Before: a simple LLM wrapper
async def llm(system: str, user: str) -> str:
    return await openai_client.chat(system=system, user=user)

# After: the same wrapper with memory
async def llm(system: str, user: str,
    session_id: str | None = None) -> str:
    if session_id:
        ctx = await memory.sessions.context(session_id, user)
        if ctx.context:
            system = f"{system}\n\n{ctx.context}"

    response = await openai_client.chat(system=system, user=user)

    if session_id:
        await memory.remember(user, session_id=session_id, role="user")
        await memory.remember(response, session_id=session_id, role="assistant")

    return response
```

```typescript
// Before
async function llm(system: string, user: string): Promise<string> {
    return openaiClient.chat({ system, user });
}

// After
async function llm(system: string, user: string,
    sessionId?: string): Promise<string> {
    let enrichedSystem = system;

    if (sessionId) {
        const ctx = await memory.context(user, { lens: DEFAULT_SCOPE });
        if (ctx.context) enrichedSystem = `${system}\n\n${ctx.context}`;
    }

    const response = await openaiClient.chat({ system: enrichedSystem, user });

    if (sessionId) {
        await memory.remember(user, { sessionId, role: "user" });
        await memory.remember(response, { sessionId, role: "assistant" });
    }

    return response;
}
```

Making `session_id` optional means the change is backwards-compatible - all existing call sites continue to work without passing a session.

## Starting with a single scope

Do not attempt multi-user scoping on day one. Start with a single organisational scope and confirm the extraction pipeline is working correctly before splitting by user.

```python
# Phase 1: single scope, all conversations share it
DEFAULT_SCOPE = ["org/my-app"]

# Phase 2 (later): add user dimension
def scope_for_user(user_id: str) -> list[str]:
    return [f"org/my-app/user/{user_id}"]
```

The profile and context endpoints are scope-matched, and a read never reaches an ancestor scope. A reader limited to `["org/my-app/user/alice"]` sees memory stored at that path and below it, but not memory stored at the broader `["org/my-app"]` path. A reader whose grants cover `org/my-app/*` sees both.

## Expanding to multi-user

Once single-scope extraction is verified, add the user dimension. The only change is in how you create sessions:

```python
# Before
session = await memory.sessions.create(scopes=["org/my-app"])

# After
session = await memory.sessions.create(scopes=[f"org/my-app/user/{user_id}"])
```

New memory is stored under the user scope. Existing memory under the org-only scope stays where it is, and is readable only by keys whose grants cover `org/my-app` itself, such as a key granted `org/my-app/*`. A key limited to one user's path does not see it, because the org path is an ancestor of the user path. If per-user keys need that earlier memory, replay it under each user's scope.

## Profile injection at session start

Once you have per-user memory accumulating, add profile injection at the start of each new conversation:

```python
async def start_session(user_id: str) -> tuple[str, str]:
    session = await memory.sessions.create(
        scopes=[f"org/my-app/user/{user_id}"],
    )

    profile = await memory.profile()
    system = your_existing_system_prompt()
    profile_block = format_profile(profile)   # see Profiles: injecting into prompts
    if profile_block:
        system = f"{system}\n\n## About this user\n{profile_block}"

    return session.id, system
```

```typescript
async function startSession(userId: string): Promise<{ sessionId: string; system: string }> {
    const [session, profile] = await Promise.all([
        memory.sessions.create({ scopes: [`org/my-app/user/${userId}`] }),
        memory.profile(),
    ]);

    let system = yourExistingSystemPrompt();
    const profileBlock = formatProfile(profile);   // see Profiles: injecting into prompts
    if (profileBlock) {
        system = `${system}\n\n## About this user\n${profileBlock}`;
    }

    return { sessionId: session.id, system };
}
```

## Migration path summary

| Phase | What you add | What changes |
|---|---|---|
| 1 - Extraction only | Record turns after each LLM call | Memory accumulates but is not used |
| 2 - Context injection | Retrieve context and inject before each call | Responses become memory-aware |
| 3 - Profile injection | Inject profile at session start | New sessions start with full user context |
| 4 - Per-user scoping | Add `user` scope dimension | Memory is isolated per user |
| 5 - Documents | Upload reference documents | Agents answer from your documents |

Each phase is independently deployable and backwards-compatible with the previous one.
