Skip to main content
Give a deepagents agent persistent, searchable memory through Synap. deepagents reaches storage through BackendProtocol, a filesystem interface, and reaches memory through MemoryMiddleware, which pastes whole AGENTS.md files into the system prompt. This package plugs Synap into both, and adds a retrieval path the stock middleware cannot express.
Requires Python 3.11+ and deepagents>=0.7.4.This covers the deepagents library: agents you build with create_deep_agent(...). The separate deepagents-code terminal agent constructs its memory backend internally and does not accept a custom one, so it cannot use this package.
Mount SynapBackend on a route, never as backend= on its own. As the default backend it would route the agent’s source-code reads and writes through a memory API, and the agent would lose its working tree.

Overview

Surfaces mapped onto deepagents’ own extension points. Adopt only the ones you need. All of them take an already-constructed MaximemSynapSDK: your app owns the SDK and its credentials.

How the integration connects

Give SynapMemoryMiddleware both a conversation_id and a user_id and you do not open a connection yourself. The first event it records opens Synap’s gRPC stream, and from then on the user’s question, the model’s reasoning, each tool call, each tool result and the answer go over it. Opening gets up to 10 seconds; if it fails, the middleware tries once more a few seconds later, then no more than once a minute, and writes each turn over REST in the meantime, never both. It never raises inside your agent.
  • A stream your own code opened with listen() is used as is.
  • To keep the integration on REST, set SYNAP_AUTO_LISTEN=false in the environment, or call set_auto_listen(sdk, False) from synap_integrations_common.
  • Tool calls, tool results and reasoning exist only on the stream. Without one they are not reported, and the turns still are.
  • Initialize the SDK first. Until it is initialized the middleware cannot open the stream.
Without a conversation_id, SynapMemoryMiddleware only reads, and nothing records the conversation. See gRPC vs REST for what the stream carries.

When this is worth it

The payoff scales with how much the agent remembers, and it is worth being concrete about that. In our own testing, an agent answering a question against a 200-memory scope needed 96 characters of retrieved context to get it right. The same task with a plain-file memory backend pasted 24,853 characters (the entire corpus) into the prompt on every turn to reach the same answer. That gap is the reason this integration exists, and it widens as memory grows. The trade is latency and small-corpus behaviour: a retrieval call is a network round trip, so expect a couple of extra seconds per turn versus reading a local file, and on a handful of memories a file backend is both faster and more reliable. Use Synap where memory outgrows a file (long-lived agents, many sessions, memory shared across projects) and leave a small static AGENTS.md on FilesystemBackend. CompositeBackend lets you do both at once.

Setup

Basic integration

Mount Synap at /memories/ and leave the repository on a normal filesystem backend:
In an async application, call await sdk.initialize() instead and run the agent with await agent.ainvoke(...). That is the better fit for agents that write memory, because the writes are awaited on your event loop and never block a worker thread. Synchronous invoke works too. CompositeBackend routes by longest path prefix. Anything under /memories/ reaches Synap; everything else goes to the repository, untouched.

Core concepts

This is the part worth knowing. On a Synap route, the agent’s grep tool is not a regex match over file bytes. The pattern is passed to Synap as a natural-language query:
Stock deepagents grep can only find what is literally written in a file. This searches by meaning instead, so a memory can be phrased differently from the query and still match. Matches are attributed to /memories/AGENTS.md, so the agent can read that file for more context. If your agent tends to write regexes, say so in its system prompt. A model that assumes literal matching will write ^prefer.* and misread the misses.
Retrieval is a relevance search, not a guarantee. Some phrasings return nothing even when a matching memory is in scope, so treat an empty grep as “no strong match for this wording” rather than “not in memory”. Where it matters, have the agent try a second phrasing, or read /memories/AGENTS.md directly for the unfiltered recall block.

A new scope needs a corpus before recall is useful

Recall is only as good as what is in the scope, and a nearly-empty scope returns nothing at all rather than a little. In testing, a scope holding three memories returned an empty context block on every setting, while the same scope at a dozen memories returned content consistently. That is worth knowing because of how it looks from the outside: the agent reads /memories/AGENTS.md, gets file_not_found, and behaves exactly as if the integration were broken. It isn’t: there is genuinely nothing to return yet. Seed a real corpus, or let the agent accumulate one over several sessions, before judging recall quality.

The recall file is synthesized, not stored

/memories/AGENTS.md does not exist anywhere. Reading it calls sdk.fetch() with a question (recall_query, by default “What do you know about this user?”) and returns the formatted context as the file body. That is what makes create_deep_agent(memory=["/memories/AGENTS.md"]) work with no other changes. Synap returns nothing for a fetch with no question, and retrieval is relevance-ranked, so the recall file is a best-effort sample of what Synap knows, not a full dump. Point it at what the agent should start with:
For anything specific, have the agent use grep (or the middleware below), which ask a real question each time.

Queued writes and read-after-write

Synap ingestion is asynchronous: create returns {ingestion_id, document_id, status: QUEUED}. A memory written a moment ago is not yet retrievable, so an agent that writes a file and reads it back in the same turn would otherwise get nothing, which reads as data loss. SynapBackend keeps a short-TTL, per-process write-through cache so your own writes are always readable back. It is a read-after-write guarantee, not semantic dedup, and it does not survive a restart. The package never polls wait_for_completion on the agent’s path. Waiting would cost turn latency without improving the answer: Synap may split one submitted document into several memories, or merge it with existing ones, so there is no count to wait for.

There is no delete

sdk.memories.create() returns an ingestion_id; sdk.memories.delete() needs a memory_id. They are different identifiers, and the memory does not exist yet at write time. So a path written through this backend cannot be resolved back to a durable memory. delete is optional in BackendProtocol, so SynapBackend inherits the default that raises NotImplementedError, and CompositeBackend reports it cleanly. Remove memories through the Synap API or dashboard with a memory id.

Complete example: query-conditioned recall

create_deep_agent(memory=[...]) installs deepagents’ own MemoryMiddleware, which calls backend.download_files(paths): paths, and nothing else. Even with SynapBackend underneath, that is one unqueried digest per run. SynapMemoryMiddleware reads the pending user message first and passes it as the search query, so what lands in the prompt is scoped to what was actually asked:
Use memory=[...] or SynapMemoryMiddleware, not both: they write to the same part of the system prompt, and you would pay for two retrievals to say the same thing twice. With a conversation_id and a user_id, this one middleware does three jobs:
  • Recall: the question becomes the search query, and the result goes into the system prompt.
  • Recording: the question, reasoning, tool calls, tool results and answer go to Synap, once per run.
  • History: once Synap has compacted the conversation, the model is sent Synap’s compaction in place of the turns before the current one, then the turns after the compaction, then the current turn with its tool calls and results as they are. Until then, and whenever Synap cannot answer, the agent’s messages go through untouched.
The compaction goes into the system message, because some providers reject a system message that is not first. deepagents’ own summarizer stays installed as the fallback. It runs before any middleware you add, so on a long history it can still fire once before the splice makes it unnecessary. conversation_id is required for the last two jobs, and the agent’s own thread id is not used for it. Use one middleware per conversation, and do not add a SynapStreamMiddleware for the same conversation: this one already runs it, and every event would go out twice.

Advanced patterns

Short-term context

Long-term memory and short-term context are different things. Short-term is the compacted history of the current conversation. If you already use SynapMemoryMiddleware(conversation_id=...), you have it. The pieces below are for the other cases. SynapStreamMiddleware is the conversation half of that middleware on its own: it records the run and splices in the compaction, with no recall. SynapShortTermMiddleware is the history half alone: it reads and splices, and records nothing, so pair it with a writer. synap_st_instructions adds the compaction to a static system prompt:
That is a snapshot taken once, at construction, and it sits beside the agent’s full history. For a long-running agent, use SynapShortTermMiddleware instead. It reads the compaction on every model call, because a long tool loop can cross the compaction partway, and replaces the turns it covers:

Tuning retrieval

Reads sit on the agent’s startup path; grep is a deliberate question. They get different defaults, and both are configurable:

Error policy

Read failures are always reported as file_not_found, never any other code. That is deliberate: deepagents’ MemoryMiddleware raises ValueError on any download error code except file_not_found, so returning anything else during a Synap outage would end the agent run instead of degrading to an empty memory block.

Going further

  • Mount the backend and the tools together. The backend covers automatic recall through AGENTS.md; the tools cover deliberate lookups with names the model already understands.
  • Writes use the SDK default document_type="ai-chat-conversation", which Synap extracts into retrievable memories within about half a minute. Change document_type only when the agent is storing something else, for example document_type="meeting-transcript". Do not use document_type="document" for short notes: in testing, text saved that way did not become retrievable.
  • Scope with customer_id alone for shared team memory, or user_id + customer_id for per-person memory inside an organisation.

Next steps

LangGraph

deepagents runs on LangGraph, so the cross-thread store and the run reporting there compose with this one.

LangChain

Retrievers, tools, and short-term context for LangChain itself.