diff --git a/.github/workflows/ci-python.yml b/.github/workflows/ci-python.yml
index e2942b80..2dda9c58 100644
--- a/.github/workflows/ci-python.yml
+++ b/.github/workflows/ci-python.yml
@@ -31,10 +31,10 @@ jobs:
include:
- python-version: "3.10"
dependency-lane: minimum-supermemory
- supermemory-version: "3.16.0"
+ supermemory-version: "5.0.0"
- python-version: "3.13"
dependency-lane: current-supermemory
- supermemory-version: "3.59.0"
+ supermemory-version: "5.0.0"
defaults:
run:
working-directory: packages/agent-framework-python
diff --git a/apps/docs/integrations/agent-framework.mdx b/apps/docs/integrations/agent-framework.mdx
index f8b872bf..a716408b 100644
--- a/apps/docs/integrations/agent-framework.mdx
+++ b/apps/docs/integrations/agent-framework.mdx
@@ -16,19 +16,19 @@ Microsoft's [Agent Framework](https://github.com/microsoft/agent-framework) is a
## Setup
-Install the package:
+Install adapter version `>=2.0.0,<3` with the OpenAI client package:
```bash
-pip install --pre supermemory-agent-framework
+pip install "supermemory-agent-framework>=2.0.0,<3" agent-framework-openai
```
Or with uv:
```bash
-uv add --prerelease=allow supermemory-agent-framework
+uv add "supermemory-agent-framework>=2.0.0,<3" agent-framework-openai
```
-The `--pre` / `--prerelease=allow` flag is required because `agent-framework-core` depends on pre-release versions of Azure packages.
+The adapter requires `supermemory>=5.0.0,<6`. The OpenAI examples also require the separate `agent-framework-openai` package and use its current `OpenAIChatClient` Responses API client. Older supported framework cores can have different OpenAI client names and model arguments.
Set up your environment:
@@ -74,6 +74,12 @@ tools = SupermemoryTools(conn)
provider = SupermemoryContextProvider(conn, mode="full")
```
+### Existing callers and data
+
+Keep the existing `container_tag` and `conversation_id` arguments. The adapter sends the container tag as the v5 namespace and the unchanged `conversation_` as the document `id`. Choose a distinct container tag per user or tenant: the default `msft_agent_chat` scope is shared. Repeated writes append/diff into the same conversation document; they do not replace previous turns. `entity_context` remains context prepended to retrieved memories, not an ingestion setting.
+
+Existing Agent Framework session exports remain loadable because the provider does not change its scoped state. Reconstruct the connection with the same identifiers after loading a session; connections and API credentials are not part of provider state. This adapter migration does not move existing server-side data. Confirm old data is available in the corresponding v5 namespace using the [v5 migration guide](/migration/api-v5); the adapter does not delete or rewrite legacy data.
+
---
## Context provider (recommended)
@@ -83,7 +89,7 @@ The most idiomatic integration. Follows the same pattern as Agent Framework's bu
```python
import asyncio
from agent_framework import AgentSession
-from agent_framework.openai import OpenAIResponsesClient
+from agent_framework.openai import OpenAIChatClient
from supermemory_agent_framework import AgentSupermemory, SupermemoryContextProvider
async def main():
@@ -91,7 +97,7 @@ async def main():
provider = SupermemoryContextProvider(conn, mode="full")
- agent = OpenAIResponsesClient().as_agent(
+ agent = OpenAIChatClient(model="gpt-5").as_agent(
name="MemoryAgent",
instructions="You are a helpful assistant with memory.",
context_providers=[provider],
@@ -130,14 +136,14 @@ Give agents explicit control over memory operations. The agent decides when to s
```python
import asyncio
-from agent_framework.openai import OpenAIResponsesClient
+from agent_framework.openai import OpenAIChatClient
from supermemory_agent_framework import AgentSupermemory, SupermemoryTools
async def main():
conn = AgentSupermemory(container_tag="user-123")
tools = SupermemoryTools(conn)
- agent = OpenAIResponsesClient().as_agent(
+ agent = OpenAIChatClient(model="gpt-5").as_agent(
name="MemoryAgent",
instructions="""You are a helpful assistant with memory.
When users share preferences, save them. When they ask questions, search memories first.""",
@@ -160,6 +166,8 @@ The agent gets three tools:
- **`add_memory`** — Store new information for later recall
- **`get_profile`** — Fetch the user's full profile (static + dynamic facts)
+Tool JSON envelopes are preserved. Profiles still expose string arrays for static, dynamic, and bucket facts. A query passed to `get_profile` triggers a separate v5 search, returned under `search_results` with `results`, `timing`, and `total` (the number returned). Search results keep their top-level `updated_at` timestamp and include v5 metadata; legacy optional fields not returned by v5 remain present as `null` because their values cannot be reconstructed. The deprecated Python-only `include_full_docs` argument remains ignored and is not exposed to the model.
+
---
## Chat middleware
@@ -168,7 +176,7 @@ Intercept chat requests to automatically inject memory context. Useful when you
```python
import asyncio
-from agent_framework.openai import OpenAIResponsesClient
+from agent_framework.openai import OpenAIChatClient
from supermemory_agent_framework import (
AgentSupermemory,
SupermemoryChatMiddleware,
@@ -186,13 +194,14 @@ async def main():
),
)
- agent = OpenAIResponsesClient().as_agent(
+ agent = OpenAIChatClient(model="gpt-5").as_agent(
name="MemoryAgent",
instructions="You are a helpful assistant.",
middleware=[middleware],
)
response = await agent.run("What's my favorite programming language?")
+ await middleware.wait_for_background_tasks()
print(response.text)
asyncio.run(main())
@@ -212,6 +221,8 @@ SupermemoryContextProvider(conn, mode="full") # or "profile" / "query"
| `"query"` | Memories relevant to the current message only | Targeted recall, no profile data |
| `"full"` (default) | Profile + query search combined | Maximum context |
+V5 profile retrieval does not accept a search query. Full mode makes separate profile and search requests and deduplicates overlapping facts before injection. Profile-associated searches keep the legacy memory-only mode and `0.6` threshold; the explicit search tool keeps hybrid mode and `0.6`. Writes are asynchronous; an accepted document may not be immediately searchable, and this migration does not change the SDK's default processing mode.
+
---
## Example: support agent with memory
@@ -221,7 +232,7 @@ A support agent that remembers customers across sessions:
```python
import asyncio
from agent_framework import AgentSession
-from agent_framework.openai import OpenAIResponsesClient
+from agent_framework.openai import OpenAIChatClient
from supermemory_agent_framework import (
AgentSupermemory,
SupermemoryChatMiddleware,
@@ -247,13 +258,13 @@ async def main():
conn,
options=SupermemoryMiddlewareOptions(
mode="full",
- add_memory="always",
+ add_memory="never",
),
)
tools = SupermemoryTools(conn)
- agent = OpenAIResponsesClient().as_agent(
+ agent = OpenAIChatClient(model="gpt-5").as_agent(
name="SupportAgent",
instructions="""You are a customer support agent.
@@ -285,12 +296,16 @@ Save important new information about the customer.""",
asyncio.run(main())
```
+This example stores conversations through the provider only. Enabling middleware storage as well can submit overlapping content; use both only when intentional.
+
---
## Error handling
The package provides specific exception types:
+Tools return operation failures as JSON (`success: false`, `error`). Provider retrieval/storage and middleware retrieval failures are logged and let the agent continue without memories. Middleware background write failures are logged rather than raised by `wait_for_background_tasks()`; a timeout on that wait still raises `asyncio.TimeoutError`.
+
```python
from supermemory_agent_framework import (
AgentSupermemory,
diff --git a/packages/agent-framework-python/README.md b/packages/agent-framework-python/README.md
index 3908692d..9d2afe47 100644
--- a/packages/agent-framework-python/README.md
+++ b/packages/agent-framework-python/README.md
@@ -6,16 +6,20 @@ This package provides both **automatic memory injection middleware** and **manua
## Installation
+Adapter version `>=2.0.0,<3` supports the Supermemory Python SDK `>=5.0.0,<6`.
+
+The OpenAI client is a separate Agent Framework package. Include `agent-framework-openai` when using the OpenAI examples below, which target its current `OpenAIChatClient` Responses API client. The adapter also supports older framework cores, but their OpenAI client names and model arguments can differ.
+
Install using uv (recommended):
```bash
-uv add supermemory-agent-framework
+uv add "supermemory-agent-framework>=2.0.0,<3" agent-framework-openai
```
Or with pip:
```bash
-pip install supermemory-agent-framework
+pip install "supermemory-agent-framework>=2.0.0,<3" agent-framework-openai
```
## Quick Start
@@ -26,7 +30,7 @@ The easiest way to add memory capabilities is using the `SupermemoryChatMiddlewa
```python
import asyncio
-from agent_framework.openai import OpenAIResponsesClient
+from agent_framework.openai import OpenAIChatClient
from supermemory_agent_framework import (
AgentSupermemory,
SupermemoryChatMiddleware,
@@ -49,7 +53,7 @@ async def main():
)
# Create agent with middleware
- agent = OpenAIResponsesClient().as_agent(
+ agent = OpenAIChatClient(model="gpt-5").as_agent(
name="MemoryAgent",
instructions="You are a helpful assistant with memory.",
middleware=[middleware],
@@ -59,6 +63,7 @@ async def main():
response = await agent.run(
"What's my favorite programming language?"
)
+ await middleware.wait_for_background_tasks()
print(response.text)
asyncio.run(main())
@@ -71,7 +76,7 @@ The most idiomatic way to add memory in Agent Framework, using the same pattern
```python
import asyncio
from agent_framework import AgentSession
-from agent_framework.openai import OpenAIResponsesClient
+from agent_framework.openai import OpenAIChatClient
from supermemory_agent_framework import AgentSupermemory, SupermemoryContextProvider
async def main():
@@ -87,7 +92,7 @@ async def main():
)
# Create agent with context provider
- agent = OpenAIResponsesClient().as_agent(
+ agent = OpenAIChatClient(model="gpt-5").as_agent(
name="MemoryAgent",
instructions="You are a helpful assistant with memory.",
context_providers=[provider],
@@ -110,7 +115,7 @@ For explicit tool-based memory access:
```python
import asyncio
-from agent_framework.openai import OpenAIResponsesClient
+from agent_framework.openai import OpenAIChatClient
from supermemory_agent_framework import AgentSupermemory, SupermemoryTools
async def main():
@@ -121,7 +126,7 @@ async def main():
tools = SupermemoryTools(connection)
# Create agent
- agent = OpenAIResponsesClient().as_agent(
+ agent = OpenAIChatClient(model="gpt-5").as_agent(
name="MemoryAgent",
instructions="You are a helpful assistant with access to user memories.",
)
@@ -142,7 +147,7 @@ For maximum flexibility, use both middleware (automatic context injection) and t
```python
import asyncio
-from agent_framework.openai import OpenAIResponsesClient
+from agent_framework.openai import OpenAIChatClient
from supermemory_agent_framework import (
AgentSupermemory,
SupermemoryChatMiddleware,
@@ -164,7 +169,7 @@ async def main():
tools = SupermemoryTools(connection)
- agent = OpenAIResponsesClient().as_agent(
+ agent = OpenAIChatClient(model="gpt-5").as_agent(
name="MemoryAgent",
instructions="You are a helpful assistant with memory.",
middleware=[middleware],
@@ -258,10 +263,23 @@ result = await tools.add_memory("User prefers dark mode")
result = await tools.get_profile()
```
-`search_memories` uses v4 hybrid search, so results can contain either a
+`search_memories` uses v5 hybrid search, so results can contain either a
structured memory or a source chunk. The old Python-only `include_full_docs`
-argument is deprecated and ignored because v4 search does not return full
-source documents; it is not exposed to the model as a tool parameter.
+argument remains deprecated and ignored; this tool does not request full
+source documents, and the argument is not exposed to the model.
+
+### V5 compatibility
+
+- Keep passing `container_tag` and `conversation_id`. The adapter passes the container tag as the v5 namespace and uses the unchanged `conversation_` value as the document `id`. Choose a separate container tag for each tenant; the default `msft_agent_chat` is shared, not tenant-specific.
+- All writes still use add/append, including tool writes and automatic conversation storage. Reusing a conversation ID adds or diffs new content into its document; it does not replace earlier turns. No document update or replacement operation is used.
+- `entity_context` remains display context prepended to retrieved memories; this migration does not start sending it as ingestion `supporting_context`.
+- Profile mode makes one profile request, query mode makes one search request, and full mode makes both when there is a user query. V5 profiles no longer accept a query. Profile-associated search keeps the legacy memory-only mode and `0.6` threshold rather than adopting v5's broader defaults; the explicit search tool keeps its hybrid mode and `0.6` threshold. Provider and middleware context still contains fact text rather than `{id, memory}` objects and deduplicates facts across profile/search results.
+- Tool JSON envelopes remain unchanged: search returns `success`, `results`, and `count`; add returns `success` and `memory`; profile returns `success`, `profile`, and `search_results`. Profile static/dynamic/bucket values remain strings. Profile search results retain `results`, `timing`, and `total` (the number returned). Search results retain a top-level `updated_at` mapped from v5 `system.updated_at`, along with v5 fields. Legacy optional fields that v5 does not return, such as version numbers and file paths, remain present as `null`; their values cannot be reconstructed.
+- The provider has no adapter-owned persisted state schema and leaves its scoped session state unchanged. Existing framework session exports remain loadable; keep using the same container tag and conversation ID when reconstructing the connection. The API client's credentials are not serialized into session state.
+
+This maps requests but does not move server-side data. If existing v3/v4 data has not been migrated into the corresponding v5 namespace, follow the [v5 migration guide](https://supermemory.ai/docs/migration/api-v5) before relying on historical recall. The adapter does not delete or rewrite the old data.
+
+Writes are accepted asynchronously; `queued` is not a guarantee that a later search already contains the new memory. The SDK's default processing mode is unchanged. Enabling both provider storage and middleware storage can submit overlapping conversation content, so use one automatic storage path unless that is intentional.
### SupermemoryChatMiddleware
@@ -307,6 +325,8 @@ except SupermemoryConfigurationError as e:
### Exception Types
+Tools return failures as JSON with `success: false` and `error`. Provider retrieval/storage and middleware retrieval failures are logged and do not abort the agent run. Middleware background write failures are logged; `wait_for_background_tasks()` waits for those tasks but does not re-raise their operation errors (its own wait timeout still raises `asyncio.TimeoutError`). SDK connection and request timeout failures are classified separately for background writes.
+
- **`SupermemoryError`** - Base class for all Supermemory exceptions
- **`SupermemoryConfigurationError`** - Missing API keys, invalid configuration
- **`SupermemoryAPIError`** - API request failures (includes status codes)
@@ -323,7 +343,7 @@ except SupermemoryConfigurationError as e:
### Required
- `agent-framework-core>=1.0.0rc3` - Microsoft Agent Framework
-- `supermemory>=3.16.0` - Supermemory client with v4 hybrid search support
+- `supermemory>=5.0.0,<6` - Namespace-first Supermemory v5 client
- `typing-extensions>=4.0.0` - Typing compatibility helpers
## Development
@@ -342,8 +362,11 @@ uv run mypy src/supermemory_agent_framework
# Formatting
uv run black src/ tests/
uv run isort src/ tests/
+uv run flake8 src/ tests/ --ignore=E501,W503,E704
```
+The HTTP-transport regression suite uses the actual Supermemory SDK and runs a real Agent Framework agent/tool loop without API credentials or a live model. It is verified against both `agent-framework-core==1.0.0rc3` and `1.21.0`.
+
## License
MIT License - see LICENSE file for details.
diff --git a/packages/agent-framework-python/pyproject.toml b/packages/agent-framework-python/pyproject.toml
index 8a7c749e..6f4f2197 100644
--- a/packages/agent-framework-python/pyproject.toml
+++ b/packages/agent-framework-python/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "supermemory-agent-framework"
-version = "1.0.3"
+version = "2.0.0"
description = "Memory tools and middleware for Microsoft Agent Framework with supermemory"
readme = "README.md"
license = "MIT"
@@ -25,7 +25,7 @@ classifiers = [
requires-python = ">=3.10"
dependencies = [
"agent-framework-core>=1.0.0rc3",
- "supermemory>=3.16.0,<5",
+ "supermemory>=5.0.0,<6",
"typing-extensions>=4.0.0",
]
diff --git a/packages/agent-framework-python/src/supermemory_agent_framework/__init__.py b/packages/agent-framework-python/src/supermemory_agent_framework/__init__.py
index 10bab443..7aa50dd2 100644
--- a/packages/agent-framework-python/src/supermemory_agent_framework/__init__.py
+++ b/packages/agent-framework-python/src/supermemory_agent_framework/__init__.py
@@ -3,38 +3,33 @@
from .connection import (
AgentSupermemory,
)
-
-from .tools import (
- SupermemoryTools,
- MemorySearchResult,
- MemoryAddResult,
- ProfileResult,
+from .context_provider import (
+ SupermemoryContextProvider,
+)
+from .exceptions import (
+ SupermemoryAPIError,
+ SupermemoryConfigurationError,
+ SupermemoryError,
+ SupermemoryMemoryOperationError,
+ SupermemoryNetworkError,
+ SupermemoryTimeoutError,
)
-
from .middleware import (
SupermemoryChatMiddleware,
SupermemoryMiddlewareOptions,
)
-
-from .context_provider import (
- SupermemoryContextProvider,
+from .tools import (
+ MemoryAddResult,
+ MemorySearchResult,
+ ProfileResult,
+ SupermemoryTools,
)
-
from .utils import (
+ DeduplicatedMemories,
Logger,
+ convert_profile_to_markdown,
create_logger,
deduplicate_memories,
- DeduplicatedMemories,
- convert_profile_to_markdown,
-)
-
-from .exceptions import (
- SupermemoryError,
- SupermemoryConfigurationError,
- SupermemoryAPIError,
- SupermemoryMemoryOperationError,
- SupermemoryTimeoutError,
- SupermemoryNetworkError,
)
__all__ = [
@@ -57,4 +52,4 @@ __all__ = [
"SupermemoryMemoryOperationError",
"SupermemoryTimeoutError",
"SupermemoryNetworkError",
-]
\ No newline at end of file
+]
diff --git a/packages/agent-framework-python/src/supermemory_agent_framework/context_provider.py b/packages/agent-framework-python/src/supermemory_agent_framework/context_provider.py
index 11cd3637..14150657 100644
--- a/packages/agent-framework-python/src/supermemory_agent_framework/context_provider.py
+++ b/packages/agent-framework-python/src/supermemory_agent_framework/context_provider.py
@@ -14,13 +14,11 @@ from agent_framework import Message
try:
from agent_framework import BaseContextProvider # type: ignore[attr-defined]
except ImportError:
- # Renamed in agent-framework-core 1.0.0 stable; the interface is
- # unchanged (source_id __init__, before_run/after_run hooks with
- # identical keyword-only signatures).
from agent_framework import ContextProvider as BaseContextProvider
from .connection import AgentSupermemory
from .utils import (
+ _fetch_profile_and_search,
convert_profile_to_markdown,
create_logger,
deduplicate_memories,
@@ -40,7 +38,7 @@ class SupermemoryContextProvider(BaseContextProvider):
Example:
```python
from agent_framework import Agent, AgentSession
- from agent_framework.openai import OpenAIResponsesClient
+ from agent_framework.openai import OpenAIChatClient
from supermemory_agent_framework import (
AgentSupermemory,
SupermemoryContextProvider,
@@ -54,7 +52,7 @@ class SupermemoryContextProvider(BaseContextProvider):
store_conversations=True,
)
- agent = OpenAIResponsesClient().as_agent(
+ agent = OpenAIChatClient(model="gpt-5").as_agent(
name="MemoryAgent",
instructions="You are a helpful assistant with memory.",
context_providers=[provider],
@@ -107,7 +105,6 @@ class SupermemoryContextProvider(BaseContextProvider):
state: dict[str, Any],
) -> None:
"""Search Supermemory for relevant memories and inject into context."""
- # Extract query text from input messages
query_text = ""
if self._mode != "profile":
query_text = self._extract_query_from_context(context)
@@ -137,11 +134,9 @@ class SupermemoryContextProvider(BaseContextProvider):
self._logger.debug("No memories found")
return
- # Prepend entity context if available
if self._connection.entity_context:
memories_text = f"{self._connection.entity_context}\n\n{memories_text}"
- # Inject memories into the session context
full_text = wrap_memory_injection(memories_text, self._context_prompt)
self._logger.debug(
@@ -149,11 +144,9 @@ class SupermemoryContextProvider(BaseContextProvider):
{"length": len(memories_text)},
)
- # Use extend_instructions to add memory context
if hasattr(context, "extend_instructions"):
context.extend_instructions(self.source_id, full_text)
elif hasattr(context, "extend_messages"):
- # Fallback: add as a system message
context.extend_messages(
self.source_id,
[Message("system", [full_text])],
@@ -185,13 +178,11 @@ class SupermemoryContextProvider(BaseContextProvider):
},
)
- add_params: dict[str, Any] = {
- "content": conversation_text,
- "container_tag": self._container_tag,
- "custom_id": self._connection.custom_id,
- }
-
- await self._client.add(**add_params)
+ await self._client.add(
+ self._container_tag,
+ content=conversation_text,
+ id=self._connection.custom_id,
+ )
self._logger.info("Conversation stored successfully")
@@ -203,20 +194,16 @@ class SupermemoryContextProvider(BaseContextProvider):
async def _fetch_memories(self, query_text: str = "") -> str:
"""Fetch and format memories from Supermemory."""
- kwargs: dict[str, Any] = {"container_tag": self._container_tag}
- if query_text:
- kwargs["q"] = query_text
-
- response = await self._client.profile(**kwargs)
-
- profile = response.profile if response.profile else None
+ response, search = await _fetch_profile_and_search(
+ self._client,
+ self._container_tag,
+ include_profile=self._mode != "query",
+ query=query_text if self._mode != "profile" else "",
+ )
+ profile = response.profile if response else None
static = list(profile.static) if profile and profile.static else []
dynamic = list(profile.dynamic) if profile and profile.dynamic else []
- search_results_raw = (
- list(response.search_results.results)
- if response.search_results and response.search_results.results
- else []
- )
+ search_results_raw = list(search.results) if search else []
deduplicated = deduplicate_memories(
static=static if self._mode != "query" else [],
@@ -224,7 +211,6 @@ class SupermemoryContextProvider(BaseContextProvider):
search_results=search_results_raw,
)
- # Build formatted text based on mode
profile_text = ""
if self._mode != "query":
profile_text = convert_profile_to_markdown(
@@ -290,13 +276,11 @@ class SupermemoryContextProvider(BaseContextProvider):
"""Extract conversation text from context for storage."""
messages: list[Any] = []
- # Gather input messages
if hasattr(context, "input_messages"):
messages.extend(context.input_messages or [])
elif hasattr(context, "messages"):
messages.extend(context.messages or [])
- # Gather response messages
if hasattr(context, "response") and context.response:
resp = context.response
if hasattr(resp, "text") and resp.text:
diff --git a/packages/agent-framework-python/src/supermemory_agent_framework/middleware.py b/packages/agent-framework-python/src/supermemory_agent_framework/middleware.py
index 7052ced9..2fed27d5 100644
--- a/packages/agent-framework-python/src/supermemory_agent_framework/middleware.py
+++ b/packages/agent-framework-python/src/supermemory_agent_framework/middleware.py
@@ -15,9 +15,11 @@ from .connection import AgentSupermemory
from .exceptions import (
SupermemoryMemoryOperationError,
SupermemoryNetworkError,
+ SupermemoryTimeoutError,
)
from .utils import (
Logger,
+ _fetch_profile_and_search,
convert_profile_to_markdown,
create_logger,
deduplicate_memories,
@@ -125,20 +127,16 @@ async def _build_memories_text(
query_text: str = "",
) -> str:
"""Build formatted memories text from Supermemory API."""
- kwargs: dict[str, Any] = {"container_tag": container_tag}
- if query_text:
- kwargs["q"] = query_text
-
- memories_response = await client.profile(**kwargs)
-
- profile = memories_response.profile if memories_response.profile else None
+ memories_response, search = await _fetch_profile_and_search(
+ client,
+ container_tag,
+ include_profile=mode != "query",
+ query=query_text if mode != "profile" else "",
+ )
+ profile = memories_response.profile if memories_response else None
static = list(profile.static) if profile and profile.static else []
dynamic = list(profile.dynamic) if profile and profile.dynamic else []
- search_results_raw = (
- list(memories_response.search_results.results)
- if memories_response.search_results and memories_response.search_results.results
- else []
- )
+ search_results_raw = list(search.results) if search else []
logger.info(
"Memory search completed",
@@ -146,9 +144,7 @@ async def _build_memories_text(
"container_tag": container_tag,
"memory_count_static": len(static),
"memory_count_dynamic": len(dynamic),
- "query_text": (
- query_text[:100] + ("..." if len(query_text) > 100 else "")
- ),
+ "query_text": (query_text[:100] + ("..." if len(query_text) > 100 else "")),
"mode": mode,
},
)
@@ -190,13 +186,7 @@ async def _save_memory(
) -> None:
"""Save a memory to Supermemory."""
try:
- add_params: dict[str, Any] = {
- "content": content,
- "container_tag": container_tag,
- "custom_id": custom_id,
- }
-
- response = await client.add(**add_params)
+ response = await client.add(container_tag, content=content, id=custom_id)
logger.info(
"Memory saved successfully",
@@ -207,10 +197,11 @@ async def _save_memory(
"memory_id": getattr(response, "id", None),
},
)
- except (OSError, ConnectionError) as network_error:
- logger.error(
- "Network error while saving memory", {"error": str(network_error)}
- )
+ except supermemory.APITimeoutError as timeout_error:
+ logger.error("Timeout while saving memory", {"error": str(timeout_error)})
+ raise SupermemoryTimeoutError("Timed out saving memory", timeout_error)
+ except (supermemory.APIConnectionError, OSError, ConnectionError) as network_error:
+ logger.error("Network error while saving memory", {"error": str(network_error)})
raise SupermemoryNetworkError(
"Failed to save memory due to network error", network_error
)
@@ -228,7 +219,7 @@ class SupermemoryChatMiddleware(ChatMiddleware):
Example:
```python
- from agent_framework.openai import OpenAIResponsesClient
+ from agent_framework.openai import OpenAIChatClient
from supermemory_agent_framework import (
AgentSupermemory,
SupermemoryChatMiddleware,
@@ -246,7 +237,7 @@ class SupermemoryChatMiddleware(ChatMiddleware):
),
)
- agent = OpenAIResponsesClient().as_agent(
+ agent = OpenAIChatClient(model="gpt-5").as_agent(
name="MemoryAgent",
instructions="You are a helpful assistant with memory.",
middleware=[middleware],
@@ -274,12 +265,9 @@ class SupermemoryChatMiddleware(ChatMiddleware):
call_next: Callable[[], Awaitable[None]],
) -> None:
"""Process the chat request by injecting memories and optionally saving conversations."""
- # Remove stale SDK-owned context before every lifecycle path. A failed,
- # empty, or skipped lookup must never leak memories from a prior run.
_inject_memories(context, "")
messages = context.messages
- # Save conversation memory in background if configured
if self._options.add_memory == "always":
user_message = _get_last_user_message(messages)
if user_message and user_message.strip():
@@ -310,7 +298,6 @@ class SupermemoryChatMiddleware(ChatMiddleware):
task.add_done_callback(_handle_task_exception)
- # Determine query text based on mode
query_text = ""
if self._options.mode != "profile":
user_message = _get_last_user_message(messages)
@@ -329,7 +316,6 @@ class SupermemoryChatMiddleware(ChatMiddleware):
},
)
- # Fetch and build memories text
try:
memories = await _build_memories_text(
self._container_tag,
@@ -347,7 +333,6 @@ class SupermemoryChatMiddleware(ChatMiddleware):
return
if memories:
- # Prepend entity context if available
if self._connection.entity_context:
memories = f"{self._connection.entity_context}\n\n{memories}"
@@ -356,14 +341,11 @@ class SupermemoryChatMiddleware(ChatMiddleware):
{"content": memories[:200], "full_length": len(memories)},
)
- # Inject memories into messages
_inject_memories(context, memories)
await call_next()
- async def wait_for_background_tasks(
- self, timeout: Optional[float] = 10.0
- ) -> None:
+ async def wait_for_background_tasks(self, timeout: Optional[float] = 10.0) -> None:
"""Wait for all background memory storage tasks to complete."""
if not self._background_tasks:
return
diff --git a/packages/agent-framework-python/src/supermemory_agent_framework/tools.py b/packages/agent-framework-python/src/supermemory_agent_framework/tools.py
index 2b70bdd7..e3dd13e8 100644
--- a/packages/agent-framework-python/src/supermemory_agent_framework/tools.py
+++ b/packages/agent-framework-python/src/supermemory_agent_framework/tools.py
@@ -8,8 +8,10 @@ import warnings
from typing import Annotated, Any, Optional, TypedDict
from agent_framework import FunctionTool, tool
+from supermemory.types import SearchResponse
from .connection import AgentSupermemory
+from .utils import _fetch_profile_and_search
class MemorySearchResult(TypedDict, total=False):
@@ -50,13 +52,30 @@ def _to_jsonable(value: Any) -> Any:
try:
return _to_jsonable(model_dump(mode="json"))
except TypeError:
- # Compatibility with pydantic-like models whose model_dump does not
- # accept Pydantic v2's ``mode`` argument.
return _to_jsonable(model_dump())
return value
+def _serialize_search_results(response: SearchResponse) -> dict[str, Any]:
+ """Keep the tool's legacy result fields while retaining v5 metadata."""
+ results = [
+ {
+ "chunks": None,
+ "context": None,
+ "documents": None,
+ "filepath": None,
+ "is_aggregated": None,
+ "root_memory_id": None,
+ "version": None,
+ **_to_jsonable(item),
+ "updated_at": item.system.updated_at,
+ }
+ for item in response.results
+ ]
+ return {"results": results, "timing": response.search_time, "total": len(results)}
+
+
class SupermemoryTools:
"""Memory tools for Microsoft Agent Framework.
@@ -92,28 +111,28 @@ class SupermemoryTools:
"""Search stored memories and source chunks.
``include_full_docs`` remains a deprecated Python-only argument for
- source compatibility. V4 search cannot return full source documents.
+ source compatibility. This tool does not return full source documents.
"""
if include_full_docs is not None:
warnings.warn(
- "include_full_docs is deprecated and ignored because v4 search "
- "does not return full source documents",
+ "include_full_docs is deprecated and ignored; search_memories "
+ "does not request full source documents",
DeprecationWarning,
stacklevel=2,
)
try:
- response = await self._client.search.memories(
- q=information_to_get,
- container_tag=self._connection.container_tag,
+ response = await self._client.search(
+ self._connection.container_tag,
+ query=information_to_get,
limit=limit,
threshold=0.6,
search_mode="hybrid",
)
- results = response.results or []
+ results = _serialize_search_results(response)["results"]
result: MemorySearchResult = {
"success": True,
- "results": [_to_jsonable(item) for item in results],
+ "results": results,
"count": len(results),
}
return json.dumps(result, default=str)
@@ -131,9 +150,9 @@ class SupermemoryTools:
"""Add (remember) memories/details/information about the user or other facts or entities. Run when explicitly asked or when the user mentions any information generalizable beyond the context of the current conversation."""
try:
response = await self._client.add(
+ self._connection.container_tag,
content=memory,
- container_tag=self._connection.container_tag,
- custom_id=self._connection.custom_id,
+ id=self._connection.custom_id,
)
result: MemoryAddResult = {
"success": True,
@@ -153,23 +172,24 @@ class SupermemoryTools:
) -> str:
"""Get user profile containing static memories (permanent facts) and dynamic memories (recent context). Optionally include search results by providing a query."""
try:
- kwargs: dict[str, Any] = {"container_tag": self._connection.container_tag}
- if query:
- kwargs["q"] = query
-
- response = await self._client.profile(**kwargs)
+ response, search = await _fetch_profile_and_search(
+ self._client, self._connection.container_tag, query=query
+ )
result: dict[str, Any] = {
"success": True,
"profile": (
- _to_jsonable(response.profile)
- if hasattr(response, "profile")
- else None
- ),
- "search_results": (
- _to_jsonable(response.search_results)
- if hasattr(response, "search_results")
+ {
+ "static": [fact.memory for fact in response.profile.static],
+ "dynamic": [fact.memory for fact in response.profile.dynamic],
+ "buckets": {
+ name: [fact.memory for fact in facts]
+ for name, facts in response.profile.buckets.items()
+ },
+ }
+ if response
else None
),
+ "search_results": _serialize_search_results(search) if search else None,
}
return json.dumps(result, default=str)
except Exception as error:
diff --git a/packages/agent-framework-python/src/supermemory_agent_framework/utils.py b/packages/agent-framework-python/src/supermemory_agent_framework/utils.py
index f194ce79..07022ab6 100644
--- a/packages/agent-framework-python/src/supermemory_agent_framework/utils.py
+++ b/packages/agent-framework-python/src/supermemory_agent_framework/utils.py
@@ -1,9 +1,13 @@
"""Utility functions for Supermemory Agent Framework integration."""
+import asyncio
import json
import re
from typing import Any, Optional, Protocol
+import supermemory
+from supermemory.types import ProfileResponse, SearchResponse
+
DEFAULT_CONTEXT_PROMPT = "The following are retrieved memories about the user."
MEMORY_CONTEXT_PATTERN = re.compile(
r'(?:\r?\n)?.*?',
@@ -15,6 +19,31 @@ SUPERMEMORY_TAG_PATTERN = re.compile(
)
+async def _fetch_profile_and_search(
+ client: supermemory.AsyncSupermemory,
+ container_tag: str,
+ *,
+ include_profile: bool = True,
+ query: str = "",
+) -> tuple[Optional[ProfileResponse], Optional[SearchResponse]]:
+ """Fetch the independent v5 profile and query resources in one namespace."""
+ if include_profile and query:
+ profile, search = await asyncio.gather(
+ client.profile(container_tag),
+ client.search(
+ container_tag, query=query, threshold=0.6, search_mode="memories"
+ ),
+ )
+ return profile, search
+ if include_profile:
+ return await client.profile(container_tag), None
+ if query:
+ return None, await client.search(
+ container_tag, query=query, threshold=0.6, search_mode="memories"
+ )
+ return None, None
+
+
def _escape_supermemory_tags(content: str) -> str:
"""Escape nested Supermemory tags supplied as untrusted memory data."""
@@ -134,7 +163,6 @@ def deduplicate_memories(
if isinstance(value, str) and value.strip():
return value.strip()
return None
- # Stainless SDK returns pydantic models (attribute access, snake_case).
for field in ("memory", "chunk", "content"):
value = getattr(item, field, None)
if isinstance(value, str) and value.strip():
diff --git a/packages/agent-framework-python/test_real.py b/packages/agent-framework-python/test_real.py
index fc733eb1..f39581c0 100644
--- a/packages/agent-framework-python/test_real.py
+++ b/packages/agent-framework-python/test_real.py
@@ -1,6 +1,8 @@
import asyncio
import os
-from agent_framework.openai import OpenAIResponsesClient
+
+from agent_framework.openai import OpenAIChatClient
+
from supermemory_agent_framework import (
AgentSupermemory,
SupermemoryChatMiddleware,
@@ -26,7 +28,9 @@ async def main():
tools = SupermemoryTools(conn)
- agent = OpenAIResponsesClient(api_key=os.environ["OPENAI_API_KEY"], model_id="gpt-4o-mini").as_agent(
+ agent = OpenAIChatClient(
+ api_key=os.environ["OPENAI_API_KEY"], model="gpt-5"
+ ).as_agent(
name="MemoryAgent",
instructions="You are a helpful assistant with memory.",
middleware=[middleware],
@@ -48,6 +52,7 @@ async def main():
break
response = await agent.run(user_input)
+ await middleware.wait_for_background_tasks()
print(f"\nAgent: {response.text}")
diff --git a/packages/agent-framework-python/tests/test_context_provider.py b/packages/agent-framework-python/tests/test_context_provider.py
index 35e9639f..49d59ed7 100644
--- a/packages/agent-framework-python/tests/test_context_provider.py
+++ b/packages/agent-framework-python/tests/test_context_provider.py
@@ -28,9 +28,7 @@ class TestContextProviderConfiguration:
def test_custom_source_id(self) -> None:
conn = _make_conn()
- provider = SupermemoryContextProvider(
- conn, source_id="custom-source"
- )
+ provider = SupermemoryContextProvider(conn, source_id="custom-source")
assert provider.source_id == "custom-source"
def test_default_mode(self) -> None:
@@ -136,13 +134,18 @@ class TestMemoryRetrieval:
conn.client.profile = AsyncMock(
return_value=SimpleNamespace(
profile=SimpleNamespace(static=[fact], dynamic=[]),
- search_results=SimpleNamespace(
- results=[SimpleNamespace(memory=fact)]
- ),
+ search_results=SimpleNamespace(results=[SimpleNamespace(memory=fact)]),
)
)
+ conn.client.search = AsyncMock(
+ return_value=SimpleNamespace(results=[SimpleNamespace(memory=fact)])
+ )
provider = SupermemoryContextProvider(conn, mode="query")
memories = await provider._fetch_memories("machine learning")
assert fact in memories
+ conn.client.profile.assert_not_awaited()
+ conn.client.search.assert_awaited_once_with(
+ "user-123", query="machine learning", threshold=0.6, search_mode="memories"
+ )
diff --git a/packages/agent-framework-python/tests/test_middleware.py b/packages/agent-framework-python/tests/test_middleware.py
index c5c867fd..7132ccfd 100644
--- a/packages/agent-framework-python/tests/test_middleware.py
+++ b/packages/agent-framework-python/tests/test_middleware.py
@@ -11,9 +11,9 @@ from supermemory_agent_framework import (
SupermemoryMiddlewareOptions,
)
from supermemory_agent_framework.middleware import (
- _get_last_user_message,
- _get_conversation_content,
_build_memories_text,
+ _get_conversation_content,
+ _get_last_user_message,
_inject_memories,
)
@@ -141,14 +141,15 @@ class TestMemoryInjection:
assert "Be helpful." in content
assert "Fresh profile fact" in content
assert "Stale profile fact" not in content
- assert content.count(
- ''
- ) == 1
+ assert content.count('') == 1
@pytest.mark.asyncio
async def test_query_mode_keeps_search_fact_also_present_in_profile(self) -> None:
fact = "User likes machine learning projects"
client = SimpleNamespace(
+ search=AsyncMock(
+ return_value=SimpleNamespace(results=[SimpleNamespace(memory=fact)])
+ ),
profile=AsyncMock(
return_value=SimpleNamespace(
profile=SimpleNamespace(static=[fact], dynamic=[]),
@@ -156,7 +157,7 @@ class TestMemoryInjection:
results=[SimpleNamespace(memory=fact)]
),
)
- )
+ ),
)
logger = Mock()
@@ -165,3 +166,7 @@ class TestMemoryInjection:
)
assert fact in memories
+ client.profile.assert_not_awaited()
+ client.search.assert_awaited_once_with(
+ "user-123", query="machine learning", threshold=0.6, search_mode="memories"
+ )
diff --git a/packages/agent-framework-python/tests/test_tools.py b/packages/agent-framework-python/tests/test_tools.py
index 00da0f7d..da1ee1d0 100644
--- a/packages/agent-framework-python/tests/test_tools.py
+++ b/packages/agent-framework-python/tests/test_tools.py
@@ -1,7 +1,5 @@
"""Tests for Supermemory tools."""
-import pytest
-
from supermemory_agent_framework import AgentSupermemory, SupermemoryTools
diff --git a/packages/agent-framework-python/tests/test_utils.py b/packages/agent-framework-python/tests/test_utils.py
index 6cc3de31..15517be4 100644
--- a/packages/agent-framework-python/tests/test_utils.py
+++ b/packages/agent-framework-python/tests/test_utils.py
@@ -3,7 +3,6 @@
import pytest
from supermemory_agent_framework.utils import (
- DeduplicatedMemories,
SimpleLogger,
convert_profile_to_markdown,
create_logger,
@@ -29,8 +28,14 @@ class TestDeduplicateMemories:
def test_deduplication_priority(self) -> None:
result = deduplicate_memories(
static=[{"memory": "User likes Python"}],
- dynamic=[{"memory": "User likes Python"}, {"memory": "User works remotely"}],
- search_results=[{"memory": "User likes Python"}, {"memory": "User prefers async"}],
+ dynamic=[
+ {"memory": "User likes Python"},
+ {"memory": "User works remotely"},
+ ],
+ search_results=[
+ {"memory": "User likes Python"},
+ {"memory": "User prefers async"},
+ ],
)
assert result.static == ["User likes Python"]
assert result.dynamic == ["User works remotely"]
diff --git a/packages/agent-framework-python/tests/test_v5_transport.py b/packages/agent-framework-python/tests/test_v5_transport.py
new file mode 100644
index 00000000..1e0ef05f
--- /dev/null
+++ b/packages/agent-framework-python/tests/test_v5_transport.py
@@ -0,0 +1,479 @@
+"""Exercise the published v5 SDK through HTTP and the real framework pipeline."""
+
+import inspect
+import json
+from copy import deepcopy
+from typing import Any
+from unittest.mock import AsyncMock, Mock
+
+import httpx
+import pytest
+import pytest_asyncio
+import supermemory
+from agent_framework import (
+ AgentSession,
+ BaseChatClient,
+ ChatContext,
+ ChatMiddlewareLayer,
+ ChatResponse,
+ Content,
+ FunctionInvocationLayer,
+ Message,
+ SessionContext,
+)
+
+from supermemory_agent_framework import (
+ AgentSupermemory,
+ SupermemoryChatMiddleware,
+ SupermemoryContextProvider,
+ SupermemoryMemoryOperationError,
+ SupermemoryMiddlewareOptions,
+ SupermemoryNetworkError,
+ SupermemoryTimeoutError,
+ SupermemoryTools,
+)
+from supermemory_agent_framework.middleware import _save_memory
+from supermemory_agent_framework.utils import wrap_memory_injection
+
+
+class MemoryAPI:
+ def __init__(self) -> None:
+ self.requests: list[tuple[str, str, dict[str, Any]]] = []
+ self.failure: int | str | None = None
+ self.empty = False
+
+ def handle(self, request: httpx.Request) -> httpx.Response:
+ body = json.loads(request.content)
+ self.requests.append((request.method, request.url.path, body))
+ if self.failure == "network":
+ raise httpx.ConnectError("Offline", request=request)
+ if self.failure == "timeout":
+ raise httpx.ReadTimeout("Timed out", request=request)
+ if isinstance(self.failure, int):
+ return httpx.Response(self.failure, json={"error": "Unavailable"})
+
+ namespace = request.url.path.split("/")[2]
+ fact = f"{namespace} prefers Python"
+ if request.url.path.endswith("/profile"):
+ return httpx.Response(
+ 200,
+ json={
+ "profile": {
+ "static": (
+ [] if self.empty else [{"id": "fact", "memory": fact}]
+ ),
+ "dynamic": (
+ []
+ if self.empty
+ else [
+ {
+ "id": "recent",
+ "memory": f"{namespace} is building an agent",
+ }
+ ]
+ ),
+ "buckets": {"work": [{"id": "work", "memory": "Uses async"}]},
+ }
+ },
+ )
+ if request.url.path.endswith("/search"):
+ response = httpx.Response(
+ 200,
+ json={
+ "results": (
+ []
+ if self.empty
+ else [
+ {
+ "id": "fact",
+ "memory": fact,
+ "metadata": {"tenant": namespace},
+ "similarity": 0.95,
+ "isLatest": True,
+ "isInference": False,
+ "system": {"updatedAt": "2026-10-08T00:00:00Z"},
+ },
+ {
+ "id": "chunk",
+ "chunk": f"{namespace} source passage",
+ "metadata": {},
+ "similarity": 0.9,
+ "isLatest": True,
+ "isInference": False,
+ "system": {"updatedAt": "2026-10-08T00:00:00Z"},
+ },
+ ]
+ ),
+ "searchTime": 2.5,
+ },
+ )
+ if body.get("searchMode") == "memories":
+ data = response.json()
+ data["results"] = [
+ result for result in data["results"] if "memory" in result
+ ]
+ return httpx.Response(200, json=data)
+ return response
+ assert request.url.path.endswith("/document")
+ assert request.method == "POST"
+ return httpx.Response(200, json={"id": body["id"], "status": "queued"})
+
+
+@pytest_asyncio.fixture
+async def memory_api(monkeypatch: pytest.MonkeyPatch):
+ api = MemoryAPI()
+ client_class = supermemory.AsyncSupermemory
+ async with httpx.AsyncClient(transport=httpx.MockTransport(api.handle)) as http:
+ monkeypatch.setattr(
+ supermemory,
+ "AsyncSupermemory",
+ lambda **kwargs: client_class(**kwargs, http_client=http, max_retries=0),
+ )
+ yield api
+
+
+@pytest.mark.parametrize("mode", ["profile", "query", "full"])
+async def test_provider_and_middleware_retrieval_modes(memory_api, mode):
+ connection = AgentSupermemory(
+ api_key="test", container_tag="tenant-a", entity_context="Custom entity context"
+ )
+ provider = SupermemoryContextProvider(
+ connection, mode=mode, context_prompt="Known facts"
+ )
+ context = SessionContext(input_messages=[Message("user", ["preferences"])])
+ state = {"legacy_user_value": {"keep": True}}
+ await provider.before_run(agent=None, session=None, context=context, state=state)
+ injected = "\n".join(context.instructions)
+ assert "tenant-a prefers Python" in injected
+ assert injected.count("tenant-a prefers Python") == 1
+ assert "Custom entity context" in injected
+ assert "Known facts" in injected
+ assert ("tenant-a is building an agent" in injected) == (mode != "query")
+ assert "tenant-a source passage" not in injected
+ assert state == {"legacy_user_value": {"keep": True}}
+ await provider.after_run(agent=None, session=None, context=context, state=state)
+ expected = {"profile"} if mode == "profile" else {"search"}
+ if mode == "full":
+ expected.add("profile")
+ assert {path.rsplit("/", 1)[1] for _, path, _ in memory_api.requests} == expected
+ for _, path, body in memory_api.requests:
+ assert path.startswith("/ns/tenant-a/")
+ assert body == (
+ {"query": "preferences", "threshold": 0.6, "searchMode": "memories"}
+ if path.endswith("search")
+ else {}
+ )
+
+ memory_api.requests.clear()
+ middleware = SupermemoryChatMiddleware(
+ connection, SupermemoryMiddlewareOptions(mode=mode)
+ )
+ chat = ChatContext(
+ client=BaseChatClient,
+ messages=[Message("system", ["Be helpful"]), Message("user", ["preferences"])],
+ options={},
+ )
+ next_call = AsyncMock()
+ await middleware.process(chat, next_call)
+ next_call.assert_awaited_once()
+ text = chat.messages[0].text
+ assert "Be helpful" in text
+ assert "tenant-a prefers Python" in text
+ assert text.count("tenant-a prefers Python") == 1
+ assert "Custom entity context" in text
+ assert {path.rsplit("/", 1)[1] for _, path, _ in memory_api.requests} == expected
+
+
+async def test_tool_formats_and_append_mapping(memory_api):
+ connection = AgentSupermemory(
+ api_key="test", container_tag="tenant-a", conversation_id="existing"
+ )
+ tools = SupermemoryTools(connection)
+ with pytest.warns(DeprecationWarning):
+ result = json.loads(await tools.search_memories("Python", True, limit=3))
+ assert result["success"] is True
+ assert result["count"] == 2
+ assert result["results"][0]["memory"] == "tenant-a prefers Python"
+ assert result["results"][1]["chunk"] == "tenant-a source passage"
+ assert result["results"][0]["updated_at"] == "2026-10-08T00:00:00Z"
+ for field in (
+ "chunks",
+ "context",
+ "documents",
+ "filepath",
+ "is_aggregated",
+ "root_memory_id",
+ "version",
+ ):
+ assert result["results"][0][field] is None
+ assert memory_api.requests[-1] == (
+ "POST",
+ "/ns/tenant-a/search",
+ {"query": "Python", "limit": 3, "threshold": 0.6, "searchMode": "hybrid"},
+ )
+
+ profile = json.loads(await tools.get_profile("preferences"))
+ assert profile["profile"] == {
+ "static": ["tenant-a prefers Python"],
+ "dynamic": ["tenant-a is building an agent"],
+ "buckets": {"work": ["Uses async"]},
+ }
+ assert profile["search_results"]["total"] == 1
+ assert profile["search_results"]["timing"] == 2.5
+ assert json.loads(await tools.get_profile())["search_results"] is None
+
+ for memory in ["First fact", "Second fact"]:
+ added = json.loads(await tools.add_memory(memory))
+ assert added == {
+ "success": True,
+ "memory": {"id": "conversation_existing", "status": "queued"},
+ }
+ assert memory_api.requests[-1] == (
+ "POST",
+ "/ns/tenant-a/document",
+ {"content": memory, "id": "conversation_existing"},
+ )
+ search_tool = tools.get_tools()[0]
+ assert "include_full_docs" not in search_tool.parameters()["properties"]
+
+
+@pytest.mark.parametrize("failure", [401, 429, 503, "network", "timeout"])
+async def test_failure_policy_with_sdk_errors(memory_api, failure):
+ memory_api.failure = failure
+ connection = AgentSupermemory(api_key="test", container_tag="tenant-a")
+ tools = SupermemoryTools(connection)
+ for operation in [
+ tools.search_memories("preferences"),
+ tools.add_memory("A fact"),
+ tools.get_profile("preferences"),
+ ]:
+ result = json.loads(await operation)
+ assert result["success"] is False
+ assert result["error"]
+
+ provider = SupermemoryContextProvider(connection, store_conversations=True)
+ context = SessionContext(input_messages=[Message("user", ["preferences"])])
+ await provider.before_run(agent=None, session=None, context=context, state={})
+ assert not context.instructions
+ await provider.after_run(agent=None, session=None, context=context, state={})
+
+ middleware = SupermemoryChatMiddleware(
+ connection, SupermemoryMiddlewareOptions(mode="full", add_memory="always")
+ )
+ chat = ChatContext(
+ client=BaseChatClient,
+ messages=[
+ Message("system", ["Be helpful\n" + wrap_memory_injection("Stale memory")]),
+ Message("user", ["preferences"]),
+ ],
+ options={},
+ )
+ next_call = AsyncMock()
+ await middleware.process(chat, next_call)
+ await middleware.wait_for_background_tasks()
+ next_call.assert_awaited_once()
+ assert chat.messages[0].text == "Be helpful"
+ error_type = (
+ SupermemoryNetworkError
+ if failure == "network"
+ else (
+ SupermemoryTimeoutError
+ if failure == "timeout"
+ else SupermemoryMemoryOperationError
+ )
+ )
+ with pytest.raises(error_type) as caught:
+ await _save_memory(connection.client, "tenant-a", "fact", "existing", Mock())
+ assert isinstance(caught.value.original_error, supermemory.APIError)
+
+
+async def test_empty_and_missing_query_do_not_inject_stale_memories(memory_api):
+ memory_api.empty = True
+ connection = AgentSupermemory(api_key="test")
+ provider = SupermemoryContextProvider(connection)
+ context = SessionContext(input_messages=[Message("user", ["hello"])])
+ await provider.before_run(agent=None, session=None, context=context, state={})
+ assert not context.instructions
+ assert json.loads(await SupermemoryTools(connection).search_memories("hello")) == {
+ "success": True,
+ "results": [],
+ "count": 0,
+ }
+ memory_api.requests.clear()
+ query_provider = SupermemoryContextProvider(connection, mode="query")
+ await query_provider.before_run(
+ agent=None,
+ session=None,
+ context=SessionContext(input_messages=[]),
+ state={},
+ )
+ assert not memory_api.requests
+ middleware = SupermemoryChatMiddleware(
+ connection, SupermemoryMiddlewareOptions(mode="query")
+ )
+ chat = ChatContext(
+ client=BaseChatClient,
+ messages=[Message("system", [wrap_memory_injection("Stale memory")])],
+ options={},
+ )
+ next_call = AsyncMock()
+ await middleware.process(chat, next_call)
+ next_call.assert_awaited_once()
+ assert not chat.messages
+ assert not memory_api.requests
+
+
+if "function_middleware" in inspect.signature(FunctionInvocationLayer).parameters:
+
+ class SmokeChatClientBase(
+ ChatMiddlewareLayer, FunctionInvocationLayer, BaseChatClient
+ ):
+ pass
+
+else:
+
+ class SmokeChatClientBase(
+ FunctionInvocationLayer, ChatMiddlewareLayer, BaseChatClient
+ ):
+ pass
+
+
+class SmokeChatClient(SmokeChatClientBase):
+ def __init__(self) -> None:
+ super().__init__()
+ self.calls: list[tuple[list[Message], dict[str, Any]]] = []
+
+ async def _inner_get_response(self, *, messages, stream, options, **kwargs):
+ assert not stream
+ self.calls.append((deepcopy(list(messages)), dict(options)))
+ if not any(
+ content.type == "function_result"
+ for message in messages
+ for content in message.contents
+ ):
+ return ChatResponse(
+ messages=[
+ Message(
+ "assistant",
+ [
+ Content.from_function_call(
+ "search",
+ "search_memories",
+ arguments={"information_to_get": "Python"},
+ ),
+ Content.from_function_call(
+ "add",
+ "add_memory",
+ arguments={"memory": "An explicit new fact"},
+ ),
+ Content.from_function_call(
+ "profile",
+ "get_profile",
+ arguments={"query": "preferences"},
+ ),
+ ],
+ )
+ ]
+ )
+ return ChatResponse(messages=[Message("assistant", ["Remembered"])])
+
+
+async def test_real_framework_pipeline_and_tenant_isolation(memory_api):
+ for tenant in ["tenant-a", "tenant-b"]:
+ connection = AgentSupermemory(
+ api_key="test", container_tag=tenant, conversation_id="existing"
+ )
+ provider = SupermemoryContextProvider(connection, store_conversations=True)
+ middleware = SupermemoryChatMiddleware(
+ connection, SupermemoryMiddlewareOptions(mode="full", add_memory="always")
+ )
+ model = SmokeChatClient()
+ agent = model.as_agent(
+ name="MemoryAgent",
+ instructions="Be helpful",
+ context_providers=[provider],
+ middleware=[middleware],
+ tools=SupermemoryTools(connection).get_tools(),
+ )
+ old_state = {
+ "type": "session",
+ "session_id": "persisted-session",
+ "service_session_id": None,
+ "state": {
+ "supermemory": {
+ "legacy_caller_data": {
+ "container_tag": tenant,
+ "conversation_id": "existing",
+ }
+ }
+ },
+ }
+ session = AgentSession.from_dict(deepcopy(old_state))
+ assert session.to_dict() == old_state
+ start = len(memory_api.requests)
+ response = await agent.run("My preferences?", session=session)
+ await middleware.wait_for_background_tasks()
+ assert response.text == "Remembered"
+ assert len(model.calls) == 2
+ assert session.state["supermemory"] == old_state["state"]["supermemory"]
+ assert (
+ AgentSession.from_dict(session.to_dict()).state["supermemory"]
+ == old_state["state"]["supermemory"]
+ )
+ for messages, options in model.calls:
+ text = "\n".join(message.text for message in messages)
+ assert f"{tenant} prefers Python" in text
+ assert f"{tenant} prefers Python" in options["instructions"]
+ other_tenant = "tenant-b" if tenant == "tenant-a" else "tenant-a"
+ assert other_tenant not in text
+ assert other_tenant not in options["instructions"]
+ tool_results = [
+ json.loads(content.result)
+ for message in model.calls[-1][0]
+ for content in message.contents
+ if content.type == "function_result"
+ ]
+ assert len(tool_results) == 3
+ assert all(result["success"] for result in tool_results)
+ requests = memory_api.requests[start:]
+ assert all(path.startswith(f"/ns/{tenant}/") for _, path, _ in requests)
+ writes = [body for _, path, body in requests if path.endswith("/document")]
+ assert all(body["id"] == "conversation_existing" for body in writes)
+ assert any(body["content"] == "An explicit new fact" for body in writes)
+ assert any("Assistant: Remembered" in body["content"] for body in writes)
+ assert any(body["content"] == "User: My preferences?" for body in writes)
+
+
+@pytest.mark.parametrize("failure", [503, "network", "timeout"])
+async def test_real_framework_pipeline_continues_on_memory_failure(memory_api, failure):
+ memory_api.failure = failure
+ connection = AgentSupermemory(api_key="test", container_tag="tenant-a")
+ provider = SupermemoryContextProvider(connection, store_conversations=True)
+ middleware = SupermemoryChatMiddleware(
+ connection, SupermemoryMiddlewareOptions(mode="full", add_memory="always")
+ )
+ model = SmokeChatClient()
+ agent = model.as_agent(
+ name="MemoryAgent",
+ instructions="Be helpful",
+ context_providers=[provider],
+ middleware=[middleware],
+ tools=SupermemoryTools(connection).get_tools(),
+ )
+ response = await agent.run("My preferences?", session=AgentSession())
+ await middleware.wait_for_background_tasks()
+ assert response.text == "Remembered"
+ assert len(model.calls) == 2
+ for messages, options in model.calls:
+ assert "supermemory context=" not in "\n".join(
+ message.text for message in messages
+ )
+ assert options["instructions"] == "Be helpful"
+ results = [
+ json.loads(content.result)
+ for message in model.calls[-1][0]
+ for content in message.contents
+ if content.type == "function_result"
+ ]
+ assert len(results) == 3
+ assert all(result["success"] is False and result["error"] for result in results)