The previous Mem0 chapter focused on an external memory layer: extract facts, deduplicate or preserve them, retrieve a small relevant set, and inject those memories into the model view. Letta is easier to read if we shift the mental model. The durable object is the running agent, with preferences, persona, tools, active messages, and file context restored together. If those pieces are only searched at answer time, the agent keeps feeling newly assembled on every turn.

Reading contract. After this chapter, you should be able to explain how Letta memory_blocks differ from Mem0 memory records, why a Block update can force a system prompt rebuild, and why Letta Code's MemFS is more like versioned agent context than file syncing.

To keep Block from sounding like a retrieved snippet, this chapter follows one representative item: a human block that says the user prefers shorter answers. It enters state when the agent is created, gets compiled into the system prompt at answer time, may trigger a prompt rebuild when updated, and comes back with the message window, tools, and identity when the agent is restored. The key idea is state projection: Letta maintains what the agent owns, then projects that state into the model view when building a request.

Stage Trigger Durable Owner Model View Change
Create agent memory_blocks in the create request AgentState.blocks The preference initializes with the agent and can be projected later.
Persist snapshot The runtime persists agent state AgentState.blocks Memory restores beside model, tools, and message ids.
Enter the model The runtime builds an LLM request Memory.compile() reads blocks The block becomes part of this turn's system prompt.
Update later A user, tool, or API updates the block AgentState.blocks; Letta Code can also map context into MemFS files The next compile may change the first system message.

Evidence boundary. This chapter uses public README and source material from letta-ai/letta and letta-ai/letta-code. Source links point at the current public main paths. Hosted-service internals, model policies, and private background jobs are not described as source facts.

1. Letta Starts Memory At Agent Creation

1.1 Decide What The Agent Owns Before Deciding What To Retrieve

The Letta README creates a stateful agent with memory_blocks right in the quickstart. The example labels two blocks as human and persona, in the same create call that chooses the model and tools. That is the first signal that this memory is not a retrieval result found at answer time. It is part of the initialized agent state. You can see the same contract in the README example and in CreateAgent.memory_blocks.

class CreateAgent(BaseModel, validate_assignment=True):
    name: str = Field(default_factory=lambda: create_random_username())

    # memory creation
    memory_blocks: Optional[List[CreateBlock]] = Field(
        None,
        description="The blocks to create in the agent's in-context memory.",
    )
    tools: Optional[List[str]] = Field(None, description="The tools used by the agent.")

The creation contract already places memory and tools on the same surface. Letta first decides which durable context the agent is born with; later, the runtime projects that context into the current step.

That entry point changes the question. An external memory layer usually asks which historical snippets should be retrieved for this query. Letta asks an earlier question: which long-lived identity, user profile, and work constraints should this agent own when it is created and later restored? In Mem0, “the user likes concise answers” is closer to a searchable memory record. In Letta, it is closer to a human block or persona constraint that must re-enter the model view whenever the agent is reconstructed.

At creation:
  1. caller passes memory_blocks, model, tools
  2. server builds AgentState
  3. memory blocks become durable state

At answer time:
  1. Memory.compile() renders blocks
  2. system prompt / model view receives current memory
  3. successful step checkpoints the message window

1.2 AgentState Keeps Memory, Tools, And The Message Window In One Snapshot

AgentState makes that boundary explicit. The source describes it as the state of an agent at a given time, persisted in the database backend, with the information needed to recreate a persisted agent. The fields include message_ids, the system prompt, agent type, model settings, compaction settings, blocks, tools, sources, tags, secrets, identities, and more. Letta memory therefore lives next to the runtime objects that make the agent work, not in a separate facts table.

That is why Letta does not read like a memory plugin. If a long-running agent has to keep working for weeks, the system must restore who the agent was, which tools it had, where the active message window stopped, which context blocks compile into the prompt, and which sources or identities still apply. AgentState puts those restoration surfaces together. Memory is not only content to be found; it is material required to rebuild the agent.

class AgentState(OrmMetadataBase, validate_assignment=True):
    """Representation of an agent's state... persisted in the DB backend."""

    message_ids: Optional[List[str]] = Field(...)
    system: str = Field(..., description="The system prompt used by the agent.")
    model: Optional[str] = Field(None, description="The model handle used by the agent.")
    blocks: List[Block] = Field(..., description="The memory blocks used by the agent.")
    tools: List[Tool] = Field(..., description="The tools used by the agent.")
    secrets: List[AgentEnvironmentVariable] = Field(default_factory=list)
    identities: List[Identity] = Field([])

That snippet is the core ownership boundary. Blocks follow the lifecycle of agent state, not the lifecycle of a single retrieval request.

Question Letta boundary Consequence
Which facts become durable? memory_blocks / blocks Stable information becomes part of agent state.
What does the model see? Memory.compile() and system prompt Blocks are rendered into context instead of waiting behind a search API.
How does the active thread continue? message_ids / in-context messages The active message window is checkpointed after a safe step.
How is memory maintained? Block update, block history, MemFS Memory edits can change prompts and can be carried by history or git-backed files.

2. Blocks Are Reserved Context, Not Search Hits

2.1 A Block Has Budget, Label, And Permissions

Letta's Block is documented as a reserved section of the LLM context window. It has a value, limit, label, read_only, description, hidden state, and tags. The same file defines default Human and Persona block types. That is why Letta memory reads like a set of state slots: each block has a name, budget, description, and permission surface.

class BaseBlock(LettaBase, validate_assignment=True):
    """Base block of the LLM context"""

    value: str = Field(..., description="Value of the block.")
    limit: int = Field(CORE_MEMORY_BLOCK_CHAR_LIMIT)
    label: Optional[str] = Field(None, description="Label of the block...")
    read_only: bool = Field(False, description="Whether the agent has read-only access.")
    description: Optional[str] = Field(None, description="Description of the block.")
    hidden: Optional[bool] = Field(None, description="If set to True, the block will be hidden.")

Those fields protect a context boundary. limit prevents an always-visible block from expanding without restraint. read_only makes some blocks behave more like constraints. description tells the model or tools what belongs in the slot. Hidden state leaves room for runtime-owned material that is not necessarily shown in the same way. If blocks are reduced to “retrieval hits,” the main design is missed: a block is a governed slot in the context, and the slot itself has rules.

2.2 compile() Projects State Into The Current Model View

The blocks become model-visible through Memory. The source describes Memory as in-context memory that contains labelled Block objects and tools to edit them. The standard renderer emits <memory_blocks> and writes each block's label, description, metadata, and value. Then compile() chooses the rendering mode based on agent type, model provider, and whether git-backed memory is enabled.

def compile(self, tool_usage_rules=None, sources=None, max_files_open=None, llm_config=None, client_skills=None) -> str:
    """Efficiently render memory, tool rules, and sources into a prompt string."""

    if not is_react:
        if self.git_enabled:
            self._render_memory_blocks_git(s)
        elif is_line_numbered:
            self._render_memory_blocks_line_numbered(s)
        else:
            self._render_memory_blocks_standard(s)

This is Letta's model-view projection. Durable state may contain many objects, but the model only sees the compiled view for this step. The hard problem is therefore not just “is there long-term storage,” but how blocks, sources, tool rules, and the message window become a stable prompt before a step runs. Mem0 commonly injects a retrieved set into the current answer. Letta first compiles agent-owned state, then runs the current step with that base view.

Letta memory mechanism diagram showing memory_blocks entering AgentState, Memory.compile producing a system prompt, and successful steps updating message_ids and DB state
Letta's main line is state ownership: blocks belong to agent state, Memory.compile() renders them into the system prompt, and successful steps checkpoint the active message window.

3. Block Updates Can Rebuild The System Prompt

3.1 First Decide Whether The Change Affects The Prompt

If a block is reserved context, changing it can change the system prompt. Letta's BlockManager lists prompt-affecting block fields: description, label, limit, read_only, and value. Its update_block_async path checks whether anything actually changed and rebuilds system prompts for connected agents after prompt-affecting updates.

PROMPT_AFFECTING_BLOCK_FIELDS = {"description", "label", "limit", "read_only", "value"}

has_prompt_changes = any(
    key in PROMPT_AFFECTING_BLOCK_FIELDS and getattr(block, key) != value
    for key, value in update_data.items()
)

if has_prompt_changes:
    await self._rebuild_system_prompts_for_connected_agents(block_id, actor)
Shape-level example: how a block update changes the model view

before:
  block(label="human", value="User prefers shorter answers")
  compiled system prompt contains:
    <memory_blocks>
      <human>User prefers shorter answers</human>
    </memory_blocks>

update:
  value = "User prefers conclusion first, then details"

after rebuild:
  AgentState.blocks stores the new value
  the first system message receives the updated memory projection

On the next model request, the runtime recompiles the memory projection from AgentState.blocks, so the first system message changes. That is Letta's injection point: the runtime projects agent state into the prompt as the path for this kind of context.

That check means more than writing a database row. Changing block metadata may be bookkeeping. Changing value, label, or limit can change what the model sees next turn. Letta lists those fields explicitly because a memory update can be upstream of a prompt update. Without the check, durable state and model view can diverge, or every harmless field edit can trigger unnecessary prompt rebuilds.

3.2 Rebuilding Memory And Checkpointing Messages Are Separate Responsibilities

That is the sharpest difference from an external retrieval layer. Mem0 retrieves memories before an answer. Letta block edits can change the agent's base view. The _rebuild_memory path refreshes memory, files, sources, and tool rules, recompiles memory, and updates the first system message if the compiled prompt differs.

Letta still does not keep every conversation token in the prompt forever. In v3, _checkpoint_messages persists new messages only when a step has completed safely, then updates message_ids or the conversation's in-context message set. The code also treats system-prompt overflow as its own stop reason, which is a reminder that blocks, tools, and system instructions have a separate capacity boundary.

Action Object Changed What Breaks If It Is Blurred
Update block Long-term state and later system-prompt projection. Treating it as an ordinary message loses the state-slot semantics.
Rebuild memory / system prompt The base context visible to the next model call. If skipped, durable state and model view can disagree.
Checkpoint messages The post-step message window and recovery position. If all history becomes blocks, the prompt grows without a useful boundary.

4. Letta Code Turns State Into Long-Running Work

4.1 MemFS Turns Context Assets Into Versioned Files

Letta Code describes itself as a stateful agent harness: agents have memory, identity, and experience over time. Its feature table is the practical surface of the same model. Self-improvement says agents can rewrite their own context, including memory blocks, skills, and prompts. MemFS says all context, including memory blocks, is tracked through git.

That pushes Letta's ownership model one level further. Ordinary memory blocks answer how long-lived information enters agent state. MemFS asks how those context assets accumulate, can be audited, can be rolled back, and can be read by other runtime mechanisms. A coding agent that learns over time is not only carrying a user profile; it is carrying prompts, skills, notes, file-backed context, and version history.

4.2 Git-Backed Rendering Keeps Files And Prompt Projection Separate

The Letta source shows how that becomes prompt-visible. In git-backed memory, _render_memory_blocks_git renders system/persona into a dedicated <self> section, renders other system/* blocks under nested <memory> tags, and projects external blocks as a file tree. compile_available_skills also renders agent-scoped skills from skills/ blocks. MemFS is therefore not just file syncing. It is versioned agent context that can be projected into the model view.

The boundary still matters. Git-backed memory does not mean the model can freely rewrite every context object, and it does not mean every file belongs in the prompt. The projection rule remains the owner: which blocks become <self>, which enter <memory>, and which skills are exposed as usable capabilities. Letta Code's design turns long-lived context from one prompt blob into manageable assets; the current model view is still produced by the compile path.

5. Put It Next To Mem0

5.1 Start With Ownership: Memory Service Or Agent-Owned State

Mem0 and Letta are both memory systems, but they answer different questions. Mem0 asks how an application can delegate cross-session facts to a memory service. Letta asks how a long-running agent owns its own state, tools, active message window, and editable memory. The former centers retrieval-time context assembly; the latter centers stateful agent reconstruction.

Question More like Mem0 More like Letta
Who owns memory? External memory layer Agent state
When does memory enter the model? Retrieved before the answer Compiled into system prompt or current context
What does an update affect? Memory records and later retrieval ranking Blocks, model-visible context, and connected system prompts
Best fit Shared user facts and preferences across apps Long-running agents with tools, identity, and self-maintained context

The next chapter moves to Graphiti / Zep. The question changes again: if memory is a temporal context graph rather than a state slot, how does the system answer what was true before, what is true now, and where a fact came from?

Sources