Microsoft Agent Framework integration
Add long-term agent memory to Microsoft Agent Framework agents
Microsoft Agent Framework agents using Zep gain long-term memory backed by a temporal knowledge graph. The zep-ms-agent-framework package attaches a context provider that persists each conversation turn, injects relevant context into the model on every run, and can register a model-callable graph-search tool.
Core benefits
- Native context-provider hook: Uses the framework’s own
before_run/after_runpipeline — the same surface as its built-in memory providers - Single round-trip: Persists the user turn and retrieves the context block in one call (or concurrently, with a custom context builder)
- Whole-user-graph recall: Context is fused across all of a user’s threads, so a new conversation still recalls earlier facts
- Pin-or-expose graph search:
expose_search_tool/create_zep_search_tooladd an on-demand tool overgraph.search, with every search parameter model-exposed by default or pinned/hidden per deployment - Per-user setup hook:
on_user_createdruns once per new user — for configuring ontology, extraction instructions, or user summary instructions - Out-of-band provisioning:
ensure_user/ensure_threadcreate resources up front and raise loudly on genuine failures; the run path falls back to lazy creation - Graceful degradation: A Zep failure on the run path is logged but never crashes the host agent — the turn proceeds without memory
How it works
The integration ships one main class, ZepContextProvider, which subclasses the framework’s ContextProvider and overrides the two lifecycle hooks called around every agent.run(...):
before_run — runs before the model is invoked. On each turn it:
- Registers the graph-search tool via
context.extend_tools(...), whenexpose_search_tool=True - Extracts the latest user message from
context.input_messages - Lazily creates the Zep user and thread on first use (cached thereafter), using the same logic as
ensure_user/ensure_thread - Persists the message — via
thread.add_messages(return_context=True)by default (a single round-trip), or concurrently with a customcontext_builderwhen one is set - Injects the resulting context block, wrapped in
context_template, into the model’s instructions viacontext.extend_instructions(...)
after_run — runs after the model responds. It reads the assistant’s reply from context.response.messages and persists it to the same thread, so both sides of the conversation are captured.
Because context is assembled from the entire user graph, the thread only scopes relevance — an agent on a new thread still recalls facts the same user shared earlier.
Installation
The package depends only on agent-framework-core. The example below also uses a model provider:
Requires Python 3.11+, agent-framework-core>=1.8.1, and a Zep Cloud API key. Get your API key from app.getzep.com.
Set up your environment variables:
Upgrading from zep-ms-agent-framework 0.1.x
Two changes can require attention: the default injected context wording follows the canonical DEFAULT_CONTEXT_TEMPLATE — pass context_template=... to keep custom wording — and on_user_created runs through ensure_user, so on the lazy run path a hook failure is logged, swallowed, and skips that turn’s Zep persistence, while a hook failure during an out-of-band ensure_user call propagates. See the package changelog for the full list of changes.
Usage
Attach a ZepContextProvider to an agent through the context_providers keyword argument:
Memory is scoped per ZepContextProvider instance to one user_id and thread_id. For a multi-user application, construct one provider per user or conversation, passing real names so Zep can resolve the user’s identity node in the graph.
On-demand graph search
Beyond the automatic context injection, create_zep_search_tool returns a model-callable agent_framework.FunctionTool over graph.search. The model decides when to look up specific facts, entities, or prior episodes. By default it searches the given user’s graph; pass graph_id=... to target a shared standalone graph instead.
The easiest way to use it is expose_search_tool=True on ZepContextProvider, which builds the tool once at construction and registers it on every run via context.extend_tools(...):
With this configuration, the model sees the un-pinned parameters (reranker, mmr_lambda, center_node_uuid). scope and limit are hidden from the schema and sent with the pinned values.
Every search parameter (scope, reranker, limit, mmr_lambda, center_node_uuid) is exposed to the model in the tool’s JSON schema by default, with documented defaults. Two options override this per deployment: search_pinned_params fixes a parameter to a constant value and hides it from the schema, and search_hidden_params hides a parameter without pinning it, so Zep’s server-side default applies. search_filters and bfs_origin_node_uuids are constructor-only — their complex shapes are not exposed to the model.
The standalone factory takes the same pin-or-expose options:
Model-exposed search parameters (when not pinned or hidden), with their defaults:
Provisioning
ensure_user and ensure_thread provision the Zep user and thread out-of-band, before the first run — useful for onboarding flows that want genuine failures (auth, network, 5xx) to raise loudly rather than degrade silently:
Both helpers are create-then-catch-conflict: they treat an “already exists” conflict as success (returning False), return True on genuine creation, and propagate genuine failures. Use the on_created hook (a UserSetupHook) — or the equivalent on_user_created option on ZepContextProvider — to configure per-user resources such as a custom ontology, custom extraction instructions, or user summary instructions exactly once; see customizing graph structure for the available options. If on_created raises, that exception propagates even though the user was created, so make the hook idempotent.
Calling these helpers is optional: before_run runs the same logic lazily on the run path, wrapped so that a genuine failure there — including an on_user_created hook failure — is logged, swallowed, and skips that turn’s Zep persistence rather than breaking the run. Called out-of-band, the same failures propagate to the caller.
Custom context building
Set context_builder on ZepContextProvider to replace the default context retrieval with custom logic — for example, searching a different graph, applying filters, or combining multiple sources:
ContextInput bundles zep (the AsyncZep client), user_id, thread_id, user_message, and session_context (the Agent Framework SessionContext for the turn). Returning None skips injection for that turn.
When context_builder is set, message persistence (add_messages without return_context) and the builder run concurrently, with per-side failure isolation:
- If the builder raises, a warning is logged and context injection is skipped for that turn — persistence still completes and the turn is marked as persisted.
- If persistence raises, a warning is logged and the turn is not marked as persisted (so
after_runskips writing the assistant reply, and the turn can be retried on the next invocation) — a successful builder result is still injected.
Context template
context_template controls how retrieved context is wrapped before injection. It must contain a literal {context} placeholder, rendered via plain string replacement (template.replace("{context}", context), never str.format), so context text containing {, }, or % is always safe to inject:
The default is DEFAULT_CONTEXT_TEMPLATE, an explicit <ZEP_CONTEXT>...</ZEP_CONTEXT> block with canonical wording shared across Zep’s framework integrations.
Configuration options
ZepContextProvider accepts:
Best practices
- Pass real names so Zep can anchor and resolve the user’s identity node in the graph
- One provider per user/conversation — memory is scoped to a single
user_idandthread_id - Reuse a single
AsyncZepclient across requests; the caller owns its lifecycle - Provision up front in onboarding flows with
ensure_user/ensure_threadso misconfiguration raises before the agent ever runs - Allow time for indexing — Zep extracts knowledge asynchronously, so facts from a turn are not instantly retrievable
Next steps
- Explore customizing graph structure for advanced knowledge organization
- Learn about searching the graph and how to tune search
- See code examples for additional patterns