Skip to content
Open
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
11 changes: 6 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- All extend pydantic-ai's `AbstractCapability`

**Persistent Memory (`pydantic_deep/features/memory/`)**
- `MemoryFile`: Loaded memory (agent_name, path, content)
- `AgentMemoryToolset`: FunctionToolset with read_memory, write_memory, update_memory
- `get_instructions()`: Injects memory into system prompt (first N lines)
- `load_memory()`, `format_memory_prompt()`, `get_memory_path()`
- Default path: `{memory_dir}/{agent_name}/MEMORY.md`
- Backed by the `Memory` capability from [pydantic-ai-harness](https://github.com/vstorm-co/pydantic-ai-harness); pydantic-deep re-exports it.
- `Memory`: Capability that injects `MEMORY.md` as **user-role** context and provides `read_memory` / `write_memory` (append or unique-replace via `old_text`) / `delete_memory` / `search_memory` tools. Supports multiple memory files, per-tenant `namespace`, CAS + idempotent writes.
- `MemoryStore`: Pluggable storage — `InMemoryStore` (default, ephemeral), `FileStore` (on-disk), `SqliteMemoryStore`, `PostgresMemoryStore`. **Independent of `deps.backend`.**
- `build_memory_store(memory_dir)`: Maps `create_deep_agent(memory_dir=...)` onto a `FileStore` (or `InMemoryStore` when `None`). Pass `memory_store=` to use an explicit store shared by the main agent and every subagent (scoped by `agent_name`).
- Memory files land at `{memory_dir}/{agent_name}/MEMORY.md` under a `FileStore`.
- **Deprecated:** the old backend-backed `MemoryCapability` / `AgentMemoryToolset` (+ `MemoryFile`, `load_memory`, `get_memory_path`, `format_memory_prompt`, `update_memory` tool) remain importable as shims and emit `DeprecationWarning`.

**Context Files (`pydantic_deep/features/context/`)**
- `ContextFile`: Loaded context file (name, path, content)
Expand Down
6 changes: 5 additions & 1 deletion apps/cli/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,9 +473,13 @@ def _on_mcp_degraded(name: str, _reason: str) -> None:
include_subagents=effective_subagents,
include_builtin_subagents=effective_subagents,
include_skills=effective_skills,
# Memory (store in .pydantic-deep/main/MEMORY.md)
# Memory (store in {working_dir}/.pydantic-deep/main/MEMORY.md).
# memory_dir is a host path now, and a relative one would resolve against
# the process CWD — anchor it to the working dir so memory follows
# --working-dir like backend files do, and stays where /remember writes.
include_memory=effective_memory,
memory_dir=".pydantic-deep",
memory_base_dir=str(root),
# Context files (auto-discover AGENTS.md, SOUL.md)
context_discovery=_context_disc if not lean else False,
include_teams=(include_teams if include_teams is not None else config.include_teams),
Expand Down
7 changes: 6 additions & 1 deletion docs/advanced/agent-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,14 @@ Overrides take precedence over the file, and they're the *only* way to supply no
| `model`, `instructions`, `retries` | `backend` |
| `include_*` feature flags | `tools`, `toolsets` |
| `model_settings`, `thinking` | `hooks`, `middleware`, `history_processors` |
| `subagents`, `skill_directories` | `output_type`, `checkpoint_store` |
| `subagents`, `skill_directories` | `output_type`, `checkpoint_store`, `memory_store` |
| `memory_dir`, `context_files` | `on_cost_update`, `on_context_update`, `on_eviction`, `on_before_compress`, `on_after_compress` |

!!! note "`memory_dir` vs `memory_store`"
`memory_dir` is serializable (a path string) and builds a `FileStore`. A
`memory_store` object (`InMemoryStore`, `FileStore`, `SqliteMemoryStore`,
`PostgresMemoryStore`) is a live object — pass it only as a runtime override.

!!! info "Backend defaults to in-memory"
If you don't pass `backend=`, the loaded `deps` uses a `StateBackend` so the
agent runs out of the box. Pass a `LocalBackend` or `DockerSandbox` override
Expand Down
2 changes: 1 addition & 1 deletion docs/advanced/forking.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ And four more for finer control: `terminate_branch(id)` cancels one branch, `dif

### Branches are isolated by default

Each branch reads through to the parent's files but writes into its own overlay — a copy-on-write wrapper, so branches never step on each other. Todos start empty per branch, and each branch gets its own message queue (so you can steer one without touching the others). History is always copied. These defaults are deliberately conservative; [`BranchIsolation`][pydantic_deep.features.forking.types.BranchIsolation] lets you loosen them.
Each branch reads through to the parent's files but writes into its own overlay — a copy-on-write wrapper, so branches never step on each other. Memory works the same way: a branch's `write_memory` is staged in a branch-local store and only replayed onto the parent when that branch wins the merge, so a note written by a discarded branch never reaches the parent. Todos start empty per branch, and each branch gets its own message queue (so you can steer one without touching the others). History is always copied. These defaults are deliberately conservative; [`BranchIsolation`][pydantic_deep.features.forking.types.BranchIsolation] lets you loosen them.

### Budgets

Expand Down
23 changes: 17 additions & 6 deletions docs/advanced/multi-user.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,19 @@ The trick is already built in: **dependencies are per-run, not per-agent.** You

## The one rule

Every stateful featurememory, checkpoints, plans, evicted files — reads and writes through `ctx.deps.backend`. So the question "do two users share state?" has exactly one answer: *do they share a backend?*
Most stateful features — plans and evicted files — read and write through `ctx.deps.backend`. So the backend question "do two users share files?" has exactly one answer: *do they share a backend?*

Give each user their own, and they're isolated. That's the whole idea.

!!! note "Memory has its own store"
Persistent memory no longer lives in the backend — it uses a pluggable
`MemoryStore`. It still follows `deps`, though: pass
`DeepAgentDeps(memory_store=...)` per request and that store wins over the one
the agent was built with, so a single shared agent stays isolated. Alternatively
give the agent a `memory_dir=`/`memory_store=` per user, or pass a per-tenant
`memory_namespace=` to partition one shared store. Checkpoints likewise use a
separate `checkpoint_store`.
Comment on lines +13 to +20

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This note is correct, and the table you added at line 131 is correct too ("Memory | DeepAgentDeps(memory_store=…) per run | If you skip it: Users see each other's memory"). But neither runnable example on the page does it, so the code a reader copies demonstrates exactly the leak the table warns about.

I checked, on a page titled "One agent, many users — without their state ever touching":

agent = create_deep_agent(model=..., memory_dir=d)     # built once, as the page says
async def run_as(user):
    deps = DeepAgentDeps()                              # exactly lines 30-32
    await agent.run("hi", deps=deps)
    return deps.memory_store
# alice store is bob store: True

That's deps_store_resolver doing its job — deps carry nothing, so it seeds them with the agent's shared store, and alice and bob end up in the same main/MEMORY.md.

Both examples need the extra line. Lines 30-32:

deps = DeepAgentDeps(
    backend=LocalBackend(root_dir=f"/workspaces/{user_id}"),  # (2)!
    memory_store=FileStore(f"/memory/{user_id}"),             # (3)!
)

And the FastAPI one at lines 155-158, which matters more because it's the production-shaped one and it already scopes checkpoint_store per user — memory is the one thing it misses:

return DeepAgentDeps(
    backend=LocalBackend(root_dir=f"/workspaces/{user_id}"),
    memory_store=FileStore(f"/memory/{user_id}"),
    checkpoint_store=FileCheckpointStore(f"/checkpoints/{user_id}"),
)

Given restoring per-tenant scoping is half the point of this PR, I'd rather the page's headline example show it than describe it.


```python
from pydantic_deep import create_deep_agent, DeepAgentDeps
from pydantic_ai_backends import LocalBackend
Expand Down Expand Up @@ -85,8 +94,9 @@ The backend is the dial you turn for the isolation-vs-persistence trade-off. Sam
)
```

Isolation **and** persistence — a user's memory and files are still there
next session. No process-level sandbox, so don't run untrusted code here.
Isolation **and** persistence — a user's files are still there next session
(and their memory too, if you point `memory_dir`/`memory_store` at a per-user
location). No process-level sandbox, so don't run untrusted code here.

=== "Sandboxed (Docker)"

Expand Down Expand Up @@ -117,11 +127,12 @@ The backend covers most state, but two things live outside it. Scope them per us

| State | Per-user via | If you skip it |
|-------|--------------|----------------|
| Files, memory, plans, evicted output | `backend=` | Users see each other's files |
| Files, plans, evicted output | `backend=` | Users see each other's files |
| Memory | `DeepAgentDeps(memory_store=…)` per run (or `memory_dir=` / `memory_namespace=` on the agent) | Users see each other's memory |
| Checkpoints | `checkpoint_store=` | Users see each other's checkpoints |
| Message history | your own store, keyed by user | Conversations bleed together |

Memory, plans, and evicted files all route through the backend, so a per-user backend handles them in one move. Checkpoints use a separate store. Message history is yours to keep — `agent.run()` doesn't remember anything between calls.
Plans and evicted files route through the backend, so a per-user backend handles them in one move. Memory lives in its own `MemoryStore`, but a `memory_store` on `DeepAgentDeps` still scopes it per run (or namespace a shared one). Checkpoints use a separate store. Message history is yours to keep — `agent.run()` doesn't remember anything between calls.

## Putting it together (FastAPI)

Expand Down Expand Up @@ -173,7 +184,7 @@ Multi-tenancy falls out of one design decision: deps are per-run.
- Build the **agent once**; it's stateless and shared across every request.
- Build **`DeepAgentDeps` per user** — that's where isolation lives.
- Pick the **backend** for your trade-off: `StateBackend` (ephemeral), `LocalBackend(root_dir=...)` (persistent), or a `SessionManager` sandbox (isolated execution).
- Scope the **checkpoint store** and **message history** per user too — they live outside the backend.
- Scope **memory** (`DeepAgentDeps(memory_store=…)`, or `memory_dir`/`memory_namespace` on the agent), the **checkpoint store**, and **message history** per user too — they live outside the backend.

Where to go next:

Expand Down
3 changes: 2 additions & 1 deletion docs/api/agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `memory_dir` | `str \| None` | `"/.deep/memory"` | Base directory for memory files |
| `memory_dir` | `str \| None` | `None` | If set, builds a `FileStore` rooted here (`{memory_dir}/{agent_name}/MEMORY.md`). If `None` (and no `memory_store`), memory uses an ephemeral in-memory store |
| `memory_store` | `MemoryStore \| None` | `None` | A pluggable memory store (`InMemoryStore`, `FileStore`, `SqliteMemoryStore`, `PostgresMemoryStore`). Independent of `deps.backend`, shared across the main agent and subagents (scoped by `agent_name`). Takes precedence over `memory_dir`, and is itself overridden per run by `DeepAgentDeps(memory_store=…)` |

#### Cost Tracking

Expand Down
14 changes: 10 additions & 4 deletions docs/api/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,17 @@ conceptual overview.
options:
show_source: false

## MemoryCapability
## Memory

::: pydantic_deep.capabilities.MemoryCapability
options:
show_source: false
Persistent memory is provided by the `Memory` capability from the external
**pydantic-ai-harness** package, re-exported as `pydantic_deep.Memory`. It stores
memory in a pluggable `MemoryStore` (independent of `deps.backend`). See the
[Memory API](memory.md) reference for full details.

!!! warning "`MemoryCapability` is deprecated"
The old `pydantic_deep.capabilities.MemoryCapability` class still imports (as a
shim that emits `DeprecationWarning`) but is deprecated in favor of the `Memory`
capability above.

## BrowserCapability

Expand Down
45 changes: 31 additions & 14 deletions docs/api/memory.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,53 @@
# Memory API

Persistent agent memory gives an agent a long-lived `MEMORY.md` file it can read
and update across runs. Enable it via `include_memory=True` (default) on
[`create_deep_agent`][pydantic_deep.agent.create_deep_agent]. See
Persistent agent memory gives an agent long-lived notes it can read and update
across runs. Enable it via `include_memory=True` (default) on
[`create_deep_agent`][pydantic_deep.agent.create_deep_agent]. Memory is backed by
the `Memory` capability from
[pydantic-ai-harness](https://github.com/vstorm-co/pydantic-ai-harness), which
stores memory in a pluggable `MemoryStore` (independent of the agent's backend),
injects `MEMORY.md` as user-role context, and provides `read_memory` /
`write_memory` / `delete_memory` / `search_memory` tools. See
[Memory](../learn/memory.md) for the conceptual overview.

## MemoryFile
## Memory

::: pydantic_deep.features.memory.MemoryFile
The persistent-memory capability. Re-exported as `pydantic_deep.Memory`.

::: pydantic_deep.features.memory.Memory
options:
show_source: false

## AgentMemoryToolset
## build_memory_store

::: pydantic_deep.features.memory.AgentMemoryToolset
::: pydantic_deep.features.memory.build_memory_store
options:
show_source: false

## load_memory
## Stores

`InMemoryStore` (ephemeral default), `FileStore` (on-disk), and
`SqliteMemoryStore` are re-exported from `pydantic_deep`;
`PostgresMemoryStore` is available from `pydantic_ai_harness.memory`.

::: pydantic_deep.features.memory.load_memory
::: pydantic_deep.features.memory.FileStore
options:
show_source: false

## format_memory_prompt

::: pydantic_deep.features.memory.format_memory_prompt
::: pydantic_deep.features.memory.SqliteMemoryStore
options:
show_source: false

## get_memory_path
## Deprecated (backend-backed) API

The original backend-backed memory implementation remains importable for
backward compatibility and emits a `DeprecationWarning`. Prefer the `Memory`
capability above.

::: pydantic_deep.features.memory.get_memory_path
::: pydantic_deep.features.memory.MemoryCapability
options:
show_source: false

::: pydantic_deep.features.memory.AgentMemoryToolset
options:
show_source: false
50 changes: 21 additions & 29 deletions docs/api/toolsets.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,41 +448,33 @@ Or via `create_deep_agent(include_teams=True)`.

## MemoryToolset

Persistent agent memory. See [Memory](../learn/memory.md).
Persistent agent memory. See [Memory](../learn/memory.md) and
[Memory API](memory.md).

Memory is provided by the `Memory` capability from the external
**pydantic-ai-harness** package (re-exported as `pydantic_deep.Memory`). Storage
uses a pluggable `MemoryStore` (`InMemoryStore` default, `FileStore`,
`SqliteMemoryStore`, `PostgresMemoryStore`), independent of `deps.backend`.

### Tools

| Tool | Description |
|------|-------------|
| `read_memory` | Read full memory content |
| `write_memory` | Append new content to memory |
| `update_memory` | Find and replace text in memory |

### Constructor

```python
from pydantic_deep.features.memory import AgentMemoryToolset

toolset = AgentMemoryToolset(
agent_name="main",
memory_dir="/.deep/memory",
max_lines=200,
descriptions={
"write_memory": "Save important findings to persistent memory",
},
)
```

**Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `agent_name` | `str` | `"main"` | Agent name (used for path and prompt label) |
| `memory_dir` | `str` | `"/.deep/memory"` | Base directory for memory files |
| `max_lines` | `int` | `200` | Max lines to inject into system prompt |
| `descriptions` | `dict[str, str] \| None` | `None` | Custom tool descriptions (keys: `read_memory`, `write_memory`, `update_memory`) |

Or via `create_deep_agent(include_memory=True)`.
| `write_memory` | Write memory (optional `old_text` for a unique find-and-replace, `file` to target a specific memory file) |
| `delete_memory` | Delete memory content |
| `search_memory` | Search across memory |

Enable via `create_deep_agent(include_memory=True, memory_dir=..., memory_store=...)`.
See [`create_deep_agent`](agent.md) for the `memory_dir` / `memory_store` parameters.

!!! warning "Deprecated API"
The homegrown `AgentMemoryToolset(agent_name=..., memory_dir=..., max_lines=...)`
constructor, the `update_memory` tool, and the `MemoryCapability` class are
**deprecated** (they still import but emit `DeprecationWarning`). Use the `Memory`
capability and the `MemoryStore` classes instead — see [Memory API](memory.md).
The `update_memory` tool has been removed; its replacement is
`write_memory(old_text=...)`.

---

Expand Down
6 changes: 6 additions & 0 deletions docs/api/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,12 @@ class OutputStyle:

### MemoryFile

!!! warning "Deprecated"
`MemoryFile` belongs to the old backend-backed memory implementation and is
**deprecated** (still importable, emits `DeprecationWarning`). Persistent memory
is now provided by the `Memory` capability backed by a `MemoryStore`. See the
[Memory API](memory.md).

Loaded agent memory file.

```python
Expand Down
Loading
Loading