supermemory-agent-framework 1.0.3__tar.gz → 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: supermemory-agent-framework
3
- Version: 1.0.3
3
+ Version: 2.0.0
4
4
  Summary: Memory tools and middleware for Microsoft Agent Framework with supermemory
5
5
  Project-URL: Homepage, https://supermemory.ai
6
6
  Project-URL: Repository, https://github.com/supermemoryai/supermemory
@@ -20,7 +20,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
21
  Requires-Python: >=3.10
22
22
  Requires-Dist: agent-framework-core>=1.0.0rc3
23
- Requires-Dist: supermemory<5,>=3.16.0
23
+ Requires-Dist: supermemory<6,>=5.0.0
24
24
  Requires-Dist: typing-extensions>=4.0.0
25
25
  Description-Content-Type: text/markdown
26
26
 
@@ -32,16 +32,20 @@ This package provides both **automatic memory injection middleware** and **manua
32
32
 
33
33
  ## Installation
34
34
 
35
+ Adapter version `>=2.0.0,<3` supports the Supermemory Python SDK `>=5.0.0,<6`.
36
+
37
+ 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.
38
+
35
39
  Install using uv (recommended):
36
40
 
37
41
  ```bash
38
- uv add supermemory-agent-framework
42
+ uv add "supermemory-agent-framework>=2.0.0,<3" agent-framework-openai
39
43
  ```
40
44
 
41
45
  Or with pip:
42
46
 
43
47
  ```bash
44
- pip install supermemory-agent-framework
48
+ pip install "supermemory-agent-framework>=2.0.0,<3" agent-framework-openai
45
49
  ```
46
50
 
47
51
  ## Quick Start
@@ -52,7 +56,7 @@ The easiest way to add memory capabilities is using the `SupermemoryChatMiddlewa
52
56
 
53
57
  ```python
54
58
  import asyncio
55
- from agent_framework.openai import OpenAIResponsesClient
59
+ from agent_framework.openai import OpenAIChatClient
56
60
  from supermemory_agent_framework import (
57
61
  AgentSupermemory,
58
62
  SupermemoryChatMiddleware,
@@ -75,7 +79,7 @@ async def main():
75
79
  )
76
80
 
77
81
  # Create agent with middleware
78
- agent = OpenAIResponsesClient().as_agent(
82
+ agent = OpenAIChatClient(model="gpt-5").as_agent(
79
83
  name="MemoryAgent",
80
84
  instructions="You are a helpful assistant with memory.",
81
85
  middleware=[middleware],
@@ -85,6 +89,7 @@ async def main():
85
89
  response = await agent.run(
86
90
  "What's my favorite programming language?"
87
91
  )
92
+ await middleware.wait_for_background_tasks()
88
93
  print(response.text)
89
94
 
90
95
  asyncio.run(main())
@@ -97,7 +102,7 @@ The most idiomatic way to add memory in Agent Framework, using the same pattern
97
102
  ```python
98
103
  import asyncio
99
104
  from agent_framework import AgentSession
100
- from agent_framework.openai import OpenAIResponsesClient
105
+ from agent_framework.openai import OpenAIChatClient
101
106
  from supermemory_agent_framework import AgentSupermemory, SupermemoryContextProvider
102
107
 
103
108
  async def main():
@@ -113,7 +118,7 @@ async def main():
113
118
  )
114
119
 
115
120
  # Create agent with context provider
116
- agent = OpenAIResponsesClient().as_agent(
121
+ agent = OpenAIChatClient(model="gpt-5").as_agent(
117
122
  name="MemoryAgent",
118
123
  instructions="You are a helpful assistant with memory.",
119
124
  context_providers=[provider],
@@ -136,7 +141,7 @@ For explicit tool-based memory access:
136
141
 
137
142
  ```python
138
143
  import asyncio
139
- from agent_framework.openai import OpenAIResponsesClient
144
+ from agent_framework.openai import OpenAIChatClient
140
145
  from supermemory_agent_framework import AgentSupermemory, SupermemoryTools
141
146
 
142
147
  async def main():
@@ -147,7 +152,7 @@ async def main():
147
152
  tools = SupermemoryTools(connection)
148
153
 
149
154
  # Create agent
150
- agent = OpenAIResponsesClient().as_agent(
155
+ agent = OpenAIChatClient(model="gpt-5").as_agent(
151
156
  name="MemoryAgent",
152
157
  instructions="You are a helpful assistant with access to user memories.",
153
158
  )
@@ -168,7 +173,7 @@ For maximum flexibility, use both middleware (automatic context injection) and t
168
173
 
169
174
  ```python
170
175
  import asyncio
171
- from agent_framework.openai import OpenAIResponsesClient
176
+ from agent_framework.openai import OpenAIChatClient
172
177
  from supermemory_agent_framework import (
173
178
  AgentSupermemory,
174
179
  SupermemoryChatMiddleware,
@@ -190,7 +195,7 @@ async def main():
190
195
 
191
196
  tools = SupermemoryTools(connection)
192
197
 
193
- agent = OpenAIResponsesClient().as_agent(
198
+ agent = OpenAIChatClient(model="gpt-5").as_agent(
194
199
  name="MemoryAgent",
195
200
  instructions="You are a helpful assistant with memory.",
196
201
  middleware=[middleware],
@@ -284,10 +289,23 @@ result = await tools.add_memory("User prefers dark mode")
284
289
  result = await tools.get_profile()
285
290
  ```
286
291
 
287
- `search_memories` uses v4 hybrid search, so results can contain either a
292
+ `search_memories` uses v5 hybrid search, so results can contain either a
288
293
  structured memory or a source chunk. The old Python-only `include_full_docs`
289
- argument is deprecated and ignored because v4 search does not return full
290
- source documents; it is not exposed to the model as a tool parameter.
294
+ argument remains deprecated and ignored; this tool does not request full
295
+ source documents, and the argument is not exposed to the model.
296
+
297
+ ### V5 compatibility
298
+
299
+ - Keep passing `container_tag` and `conversation_id`. The adapter passes the container tag as the v5 namespace and uses the unchanged `conversation_<conversation_id>` value as the document `id`. Choose a separate container tag for each tenant; the default `msft_agent_chat` is shared, not tenant-specific.
300
+ - 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.
301
+ - `entity_context` remains display context prepended to retrieved memories; this migration does not start sending it as ingestion `supporting_context`.
302
+ - 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.
303
+ - 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.
304
+ - 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.
305
+
306
+ 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.
307
+
308
+ 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.
291
309
 
292
310
  ### SupermemoryChatMiddleware
293
311
 
@@ -333,6 +351,8 @@ except SupermemoryConfigurationError as e:
333
351
 
334
352
  ### Exception Types
335
353
 
354
+ 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.
355
+
336
356
  - **`SupermemoryError`** - Base class for all Supermemory exceptions
337
357
  - **`SupermemoryConfigurationError`** - Missing API keys, invalid configuration
338
358
  - **`SupermemoryAPIError`** - API request failures (includes status codes)
@@ -349,7 +369,7 @@ except SupermemoryConfigurationError as e:
349
369
 
350
370
  ### Required
351
371
  - `agent-framework-core>=1.0.0rc3` - Microsoft Agent Framework
352
- - `supermemory>=3.16.0` - Supermemory client with v4 hybrid search support
372
+ - `supermemory>=5.0.0,<6` - Namespace-first Supermemory v5 client
353
373
  - `typing-extensions>=4.0.0` - Typing compatibility helpers
354
374
 
355
375
  ## Development
@@ -368,8 +388,11 @@ uv run mypy src/supermemory_agent_framework
368
388
  # Formatting
369
389
  uv run black src/ tests/
370
390
  uv run isort src/ tests/
391
+ uv run flake8 src/ tests/ --ignore=E501,W503,E704
371
392
  ```
372
393
 
394
+ 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`.
395
+
373
396
  ## License
374
397
 
375
398
  MIT License - see LICENSE file for details.
@@ -6,16 +6,20 @@ This package provides both **automatic memory injection middleware** and **manua
6
6
 
7
7
  ## Installation
8
8
 
9
+ Adapter version `>=2.0.0,<3` supports the Supermemory Python SDK `>=5.0.0,<6`.
10
+
11
+ 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.
12
+
9
13
  Install using uv (recommended):
10
14
 
11
15
  ```bash
12
- uv add supermemory-agent-framework
16
+ uv add "supermemory-agent-framework>=2.0.0,<3" agent-framework-openai
13
17
  ```
14
18
 
15
19
  Or with pip:
16
20
 
17
21
  ```bash
18
- pip install supermemory-agent-framework
22
+ pip install "supermemory-agent-framework>=2.0.0,<3" agent-framework-openai
19
23
  ```
20
24
 
21
25
  ## Quick Start
@@ -26,7 +30,7 @@ The easiest way to add memory capabilities is using the `SupermemoryChatMiddlewa
26
30
 
27
31
  ```python
28
32
  import asyncio
29
- from agent_framework.openai import OpenAIResponsesClient
33
+ from agent_framework.openai import OpenAIChatClient
30
34
  from supermemory_agent_framework import (
31
35
  AgentSupermemory,
32
36
  SupermemoryChatMiddleware,
@@ -49,7 +53,7 @@ async def main():
49
53
  )
50
54
 
51
55
  # Create agent with middleware
52
- agent = OpenAIResponsesClient().as_agent(
56
+ agent = OpenAIChatClient(model="gpt-5").as_agent(
53
57
  name="MemoryAgent",
54
58
  instructions="You are a helpful assistant with memory.",
55
59
  middleware=[middleware],
@@ -59,6 +63,7 @@ async def main():
59
63
  response = await agent.run(
60
64
  "What's my favorite programming language?"
61
65
  )
66
+ await middleware.wait_for_background_tasks()
62
67
  print(response.text)
63
68
 
64
69
  asyncio.run(main())
@@ -71,7 +76,7 @@ The most idiomatic way to add memory in Agent Framework, using the same pattern
71
76
  ```python
72
77
  import asyncio
73
78
  from agent_framework import AgentSession
74
- from agent_framework.openai import OpenAIResponsesClient
79
+ from agent_framework.openai import OpenAIChatClient
75
80
  from supermemory_agent_framework import AgentSupermemory, SupermemoryContextProvider
76
81
 
77
82
  async def main():
@@ -87,7 +92,7 @@ async def main():
87
92
  )
88
93
 
89
94
  # Create agent with context provider
90
- agent = OpenAIResponsesClient().as_agent(
95
+ agent = OpenAIChatClient(model="gpt-5").as_agent(
91
96
  name="MemoryAgent",
92
97
  instructions="You are a helpful assistant with memory.",
93
98
  context_providers=[provider],
@@ -110,7 +115,7 @@ For explicit tool-based memory access:
110
115
 
111
116
  ```python
112
117
  import asyncio
113
- from agent_framework.openai import OpenAIResponsesClient
118
+ from agent_framework.openai import OpenAIChatClient
114
119
  from supermemory_agent_framework import AgentSupermemory, SupermemoryTools
115
120
 
116
121
  async def main():
@@ -121,7 +126,7 @@ async def main():
121
126
  tools = SupermemoryTools(connection)
122
127
 
123
128
  # Create agent
124
- agent = OpenAIResponsesClient().as_agent(
129
+ agent = OpenAIChatClient(model="gpt-5").as_agent(
125
130
  name="MemoryAgent",
126
131
  instructions="You are a helpful assistant with access to user memories.",
127
132
  )
@@ -142,7 +147,7 @@ For maximum flexibility, use both middleware (automatic context injection) and t
142
147
 
143
148
  ```python
144
149
  import asyncio
145
- from agent_framework.openai import OpenAIResponsesClient
150
+ from agent_framework.openai import OpenAIChatClient
146
151
  from supermemory_agent_framework import (
147
152
  AgentSupermemory,
148
153
  SupermemoryChatMiddleware,
@@ -164,7 +169,7 @@ async def main():
164
169
 
165
170
  tools = SupermemoryTools(connection)
166
171
 
167
- agent = OpenAIResponsesClient().as_agent(
172
+ agent = OpenAIChatClient(model="gpt-5").as_agent(
168
173
  name="MemoryAgent",
169
174
  instructions="You are a helpful assistant with memory.",
170
175
  middleware=[middleware],
@@ -258,10 +263,23 @@ result = await tools.add_memory("User prefers dark mode")
258
263
  result = await tools.get_profile()
259
264
  ```
260
265
 
261
- `search_memories` uses v4 hybrid search, so results can contain either a
266
+ `search_memories` uses v5 hybrid search, so results can contain either a
262
267
  structured memory or a source chunk. The old Python-only `include_full_docs`
263
- argument is deprecated and ignored because v4 search does not return full
264
- source documents; it is not exposed to the model as a tool parameter.
268
+ argument remains deprecated and ignored; this tool does not request full
269
+ source documents, and the argument is not exposed to the model.
270
+
271
+ ### V5 compatibility
272
+
273
+ - Keep passing `container_tag` and `conversation_id`. The adapter passes the container tag as the v5 namespace and uses the unchanged `conversation_<conversation_id>` value as the document `id`. Choose a separate container tag for each tenant; the default `msft_agent_chat` is shared, not tenant-specific.
274
+ - 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.
275
+ - `entity_context` remains display context prepended to retrieved memories; this migration does not start sending it as ingestion `supporting_context`.
276
+ - 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.
277
+ - 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.
278
+ - 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.
279
+
280
+ 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.
281
+
282
+ 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.
265
283
 
266
284
  ### SupermemoryChatMiddleware
267
285
 
@@ -307,6 +325,8 @@ except SupermemoryConfigurationError as e:
307
325
 
308
326
  ### Exception Types
309
327
 
328
+ 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.
329
+
310
330
  - **`SupermemoryError`** - Base class for all Supermemory exceptions
311
331
  - **`SupermemoryConfigurationError`** - Missing API keys, invalid configuration
312
332
  - **`SupermemoryAPIError`** - API request failures (includes status codes)
@@ -323,7 +343,7 @@ except SupermemoryConfigurationError as e:
323
343
 
324
344
  ### Required
325
345
  - `agent-framework-core>=1.0.0rc3` - Microsoft Agent Framework
326
- - `supermemory>=3.16.0` - Supermemory client with v4 hybrid search support
346
+ - `supermemory>=5.0.0,<6` - Namespace-first Supermemory v5 client
327
347
  - `typing-extensions>=4.0.0` - Typing compatibility helpers
328
348
 
329
349
  ## Development
@@ -342,8 +362,11 @@ uv run mypy src/supermemory_agent_framework
342
362
  # Formatting
343
363
  uv run black src/ tests/
344
364
  uv run isort src/ tests/
365
+ uv run flake8 src/ tests/ --ignore=E501,W503,E704
345
366
  ```
346
367
 
368
+ 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`.
369
+
347
370
  ## License
348
371
 
349
372
  MIT License - see LICENSE file for details.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "supermemory-agent-framework"
7
- version = "1.0.3"
7
+ version = "2.0.0"
8
8
  description = "Memory tools and middleware for Microsoft Agent Framework with supermemory"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -25,7 +25,7 @@ classifiers = [
25
25
  requires-python = ">=3.10"
26
26
  dependencies = [
27
27
  "agent-framework-core>=1.0.0rc3",
28
- "supermemory>=3.16.0,<5",
28
+ "supermemory>=5.0.0,<6",
29
29
  "typing-extensions>=4.0.0",
30
30
  ]
31
31
 
@@ -3,38 +3,33 @@
3
3
  from .connection import (
4
4
  AgentSupermemory,
5
5
  )
6
-
7
- from .tools import (
8
- SupermemoryTools,
9
- MemorySearchResult,
10
- MemoryAddResult,
11
- ProfileResult,
6
+ from .context_provider import (
7
+ SupermemoryContextProvider,
8
+ )
9
+ from .exceptions import (
10
+ SupermemoryAPIError,
11
+ SupermemoryConfigurationError,
12
+ SupermemoryError,
13
+ SupermemoryMemoryOperationError,
14
+ SupermemoryNetworkError,
15
+ SupermemoryTimeoutError,
12
16
  )
13
-
14
17
  from .middleware import (
15
18
  SupermemoryChatMiddleware,
16
19
  SupermemoryMiddlewareOptions,
17
20
  )
18
-
19
- from .context_provider import (
20
- SupermemoryContextProvider,
21
+ from .tools import (
22
+ MemoryAddResult,
23
+ MemorySearchResult,
24
+ ProfileResult,
25
+ SupermemoryTools,
21
26
  )
22
-
23
27
  from .utils import (
28
+ DeduplicatedMemories,
24
29
  Logger,
30
+ convert_profile_to_markdown,
25
31
  create_logger,
26
32
  deduplicate_memories,
27
- DeduplicatedMemories,
28
- convert_profile_to_markdown,
29
- )
30
-
31
- from .exceptions import (
32
- SupermemoryError,
33
- SupermemoryConfigurationError,
34
- SupermemoryAPIError,
35
- SupermemoryMemoryOperationError,
36
- SupermemoryTimeoutError,
37
- SupermemoryNetworkError,
38
33
  )
39
34
 
40
35
  __all__ = [
@@ -57,4 +52,4 @@ __all__ = [
57
52
  "SupermemoryMemoryOperationError",
58
53
  "SupermemoryTimeoutError",
59
54
  "SupermemoryNetworkError",
60
- ]
55
+ ]
@@ -14,13 +14,11 @@ from agent_framework import Message
14
14
  try:
15
15
  from agent_framework import BaseContextProvider # type: ignore[attr-defined]
16
16
  except ImportError:
17
- # Renamed in agent-framework-core 1.0.0 stable; the interface is
18
- # unchanged (source_id __init__, before_run/after_run hooks with
19
- # identical keyword-only signatures).
20
17
  from agent_framework import ContextProvider as BaseContextProvider
21
18
 
22
19
  from .connection import AgentSupermemory
23
20
  from .utils import (
21
+ _fetch_profile_and_search,
24
22
  convert_profile_to_markdown,
25
23
  create_logger,
26
24
  deduplicate_memories,
@@ -40,7 +38,7 @@ class SupermemoryContextProvider(BaseContextProvider):
40
38
  Example:
41
39
  ```python
42
40
  from agent_framework import Agent, AgentSession
43
- from agent_framework.openai import OpenAIResponsesClient
41
+ from agent_framework.openai import OpenAIChatClient
44
42
  from supermemory_agent_framework import (
45
43
  AgentSupermemory,
46
44
  SupermemoryContextProvider,
@@ -54,7 +52,7 @@ class SupermemoryContextProvider(BaseContextProvider):
54
52
  store_conversations=True,
55
53
  )
56
54
 
57
- agent = OpenAIResponsesClient().as_agent(
55
+ agent = OpenAIChatClient(model="gpt-5").as_agent(
58
56
  name="MemoryAgent",
59
57
  instructions="You are a helpful assistant with memory.",
60
58
  context_providers=[provider],
@@ -107,7 +105,6 @@ class SupermemoryContextProvider(BaseContextProvider):
107
105
  state: dict[str, Any],
108
106
  ) -> None:
109
107
  """Search Supermemory for relevant memories and inject into context."""
110
- # Extract query text from input messages
111
108
  query_text = ""
112
109
  if self._mode != "profile":
113
110
  query_text = self._extract_query_from_context(context)
@@ -137,11 +134,9 @@ class SupermemoryContextProvider(BaseContextProvider):
137
134
  self._logger.debug("No memories found")
138
135
  return
139
136
 
140
- # Prepend entity context if available
141
137
  if self._connection.entity_context:
142
138
  memories_text = f"{self._connection.entity_context}\n\n{memories_text}"
143
139
 
144
- # Inject memories into the session context
145
140
  full_text = wrap_memory_injection(memories_text, self._context_prompt)
146
141
 
147
142
  self._logger.debug(
@@ -149,11 +144,9 @@ class SupermemoryContextProvider(BaseContextProvider):
149
144
  {"length": len(memories_text)},
150
145
  )
151
146
 
152
- # Use extend_instructions to add memory context
153
147
  if hasattr(context, "extend_instructions"):
154
148
  context.extend_instructions(self.source_id, full_text)
155
149
  elif hasattr(context, "extend_messages"):
156
- # Fallback: add as a system message
157
150
  context.extend_messages(
158
151
  self.source_id,
159
152
  [Message("system", [full_text])],
@@ -185,13 +178,11 @@ class SupermemoryContextProvider(BaseContextProvider):
185
178
  },
186
179
  )
187
180
 
188
- add_params: dict[str, Any] = {
189
- "content": conversation_text,
190
- "container_tag": self._container_tag,
191
- "custom_id": self._connection.custom_id,
192
- }
193
-
194
- await self._client.add(**add_params)
181
+ await self._client.add(
182
+ self._container_tag,
183
+ content=conversation_text,
184
+ id=self._connection.custom_id,
185
+ )
195
186
 
196
187
  self._logger.info("Conversation stored successfully")
197
188
 
@@ -203,20 +194,16 @@ class SupermemoryContextProvider(BaseContextProvider):
203
194
 
204
195
  async def _fetch_memories(self, query_text: str = "") -> str:
205
196
  """Fetch and format memories from Supermemory."""
206
- kwargs: dict[str, Any] = {"container_tag": self._container_tag}
207
- if query_text:
208
- kwargs["q"] = query_text
209
-
210
- response = await self._client.profile(**kwargs)
211
-
212
- profile = response.profile if response.profile else None
197
+ response, search = await _fetch_profile_and_search(
198
+ self._client,
199
+ self._container_tag,
200
+ include_profile=self._mode != "query",
201
+ query=query_text if self._mode != "profile" else "",
202
+ )
203
+ profile = response.profile if response else None
213
204
  static = list(profile.static) if profile and profile.static else []
214
205
  dynamic = list(profile.dynamic) if profile and profile.dynamic else []
215
- search_results_raw = (
216
- list(response.search_results.results)
217
- if response.search_results and response.search_results.results
218
- else []
219
- )
206
+ search_results_raw = list(search.results) if search else []
220
207
 
221
208
  deduplicated = deduplicate_memories(
222
209
  static=static if self._mode != "query" else [],
@@ -224,7 +211,6 @@ class SupermemoryContextProvider(BaseContextProvider):
224
211
  search_results=search_results_raw,
225
212
  )
226
213
 
227
- # Build formatted text based on mode
228
214
  profile_text = ""
229
215
  if self._mode != "query":
230
216
  profile_text = convert_profile_to_markdown(
@@ -290,13 +276,11 @@ class SupermemoryContextProvider(BaseContextProvider):
290
276
  """Extract conversation text from context for storage."""
291
277
  messages: list[Any] = []
292
278
 
293
- # Gather input messages
294
279
  if hasattr(context, "input_messages"):
295
280
  messages.extend(context.input_messages or [])
296
281
  elif hasattr(context, "messages"):
297
282
  messages.extend(context.messages or [])
298
283
 
299
- # Gather response messages
300
284
  if hasattr(context, "response") and context.response:
301
285
  resp = context.response
302
286
  if hasattr(resp, "text") and resp.text:
@@ -15,9 +15,11 @@ from .connection import AgentSupermemory
15
15
  from .exceptions import (
16
16
  SupermemoryMemoryOperationError,
17
17
  SupermemoryNetworkError,
18
+ SupermemoryTimeoutError,
18
19
  )
19
20
  from .utils import (
20
21
  Logger,
22
+ _fetch_profile_and_search,
21
23
  convert_profile_to_markdown,
22
24
  create_logger,
23
25
  deduplicate_memories,
@@ -125,20 +127,16 @@ async def _build_memories_text(
125
127
  query_text: str = "",
126
128
  ) -> str:
127
129
  """Build formatted memories text from Supermemory API."""
128
- kwargs: dict[str, Any] = {"container_tag": container_tag}
129
- if query_text:
130
- kwargs["q"] = query_text
131
-
132
- memories_response = await client.profile(**kwargs)
133
-
134
- profile = memories_response.profile if memories_response.profile else None
130
+ memories_response, search = await _fetch_profile_and_search(
131
+ client,
132
+ container_tag,
133
+ include_profile=mode != "query",
134
+ query=query_text if mode != "profile" else "",
135
+ )
136
+ profile = memories_response.profile if memories_response else None
135
137
  static = list(profile.static) if profile and profile.static else []
136
138
  dynamic = list(profile.dynamic) if profile and profile.dynamic else []
137
- search_results_raw = (
138
- list(memories_response.search_results.results)
139
- if memories_response.search_results and memories_response.search_results.results
140
- else []
141
- )
139
+ search_results_raw = list(search.results) if search else []
142
140
 
143
141
  logger.info(
144
142
  "Memory search completed",
@@ -146,9 +144,7 @@ async def _build_memories_text(
146
144
  "container_tag": container_tag,
147
145
  "memory_count_static": len(static),
148
146
  "memory_count_dynamic": len(dynamic),
149
- "query_text": (
150
- query_text[:100] + ("..." if len(query_text) > 100 else "")
151
- ),
147
+ "query_text": (query_text[:100] + ("..." if len(query_text) > 100 else "")),
152
148
  "mode": mode,
153
149
  },
154
150
  )
@@ -190,13 +186,7 @@ async def _save_memory(
190
186
  ) -> None:
191
187
  """Save a memory to Supermemory."""
192
188
  try:
193
- add_params: dict[str, Any] = {
194
- "content": content,
195
- "container_tag": container_tag,
196
- "custom_id": custom_id,
197
- }
198
-
199
- response = await client.add(**add_params)
189
+ response = await client.add(container_tag, content=content, id=custom_id)
200
190
 
201
191
  logger.info(
202
192
  "Memory saved successfully",
@@ -207,10 +197,11 @@ async def _save_memory(
207
197
  "memory_id": getattr(response, "id", None),
208
198
  },
209
199
  )
210
- except (OSError, ConnectionError) as network_error:
211
- logger.error(
212
- "Network error while saving memory", {"error": str(network_error)}
213
- )
200
+ except supermemory.APITimeoutError as timeout_error:
201
+ logger.error("Timeout while saving memory", {"error": str(timeout_error)})
202
+ raise SupermemoryTimeoutError("Timed out saving memory", timeout_error)
203
+ except (supermemory.APIConnectionError, OSError, ConnectionError) as network_error:
204
+ logger.error("Network error while saving memory", {"error": str(network_error)})
214
205
  raise SupermemoryNetworkError(
215
206
  "Failed to save memory due to network error", network_error
216
207
  )
@@ -228,7 +219,7 @@ class SupermemoryChatMiddleware(ChatMiddleware):
228
219
 
229
220
  Example:
230
221
  ```python
231
- from agent_framework.openai import OpenAIResponsesClient
222
+ from agent_framework.openai import OpenAIChatClient
232
223
  from supermemory_agent_framework import (
233
224
  AgentSupermemory,
234
225
  SupermemoryChatMiddleware,
@@ -246,7 +237,7 @@ class SupermemoryChatMiddleware(ChatMiddleware):
246
237
  ),
247
238
  )
248
239
 
249
- agent = OpenAIResponsesClient().as_agent(
240
+ agent = OpenAIChatClient(model="gpt-5").as_agent(
250
241
  name="MemoryAgent",
251
242
  instructions="You are a helpful assistant with memory.",
252
243
  middleware=[middleware],
@@ -274,12 +265,9 @@ class SupermemoryChatMiddleware(ChatMiddleware):
274
265
  call_next: Callable[[], Awaitable[None]],
275
266
  ) -> None:
276
267
  """Process the chat request by injecting memories and optionally saving conversations."""
277
- # Remove stale SDK-owned context before every lifecycle path. A failed,
278
- # empty, or skipped lookup must never leak memories from a prior run.
279
268
  _inject_memories(context, "")
280
269
  messages = context.messages
281
270
 
282
- # Save conversation memory in background if configured
283
271
  if self._options.add_memory == "always":
284
272
  user_message = _get_last_user_message(messages)
285
273
  if user_message and user_message.strip():
@@ -310,7 +298,6 @@ class SupermemoryChatMiddleware(ChatMiddleware):
310
298
 
311
299
  task.add_done_callback(_handle_task_exception)
312
300
 
313
- # Determine query text based on mode
314
301
  query_text = ""
315
302
  if self._options.mode != "profile":
316
303
  user_message = _get_last_user_message(messages)
@@ -329,7 +316,6 @@ class SupermemoryChatMiddleware(ChatMiddleware):
329
316
  },
330
317
  )
331
318
 
332
- # Fetch and build memories text
333
319
  try:
334
320
  memories = await _build_memories_text(
335
321
  self._container_tag,
@@ -347,7 +333,6 @@ class SupermemoryChatMiddleware(ChatMiddleware):
347
333
  return
348
334
 
349
335
  if memories:
350
- # Prepend entity context if available
351
336
  if self._connection.entity_context:
352
337
  memories = f"{self._connection.entity_context}\n\n{memories}"
353
338
 
@@ -356,14 +341,11 @@ class SupermemoryChatMiddleware(ChatMiddleware):
356
341
  {"content": memories[:200], "full_length": len(memories)},
357
342
  )
358
343
 
359
- # Inject memories into messages
360
344
  _inject_memories(context, memories)
361
345
 
362
346
  await call_next()
363
347
 
364
- async def wait_for_background_tasks(
365
- self, timeout: Optional[float] = 10.0
366
- ) -> None:
348
+ async def wait_for_background_tasks(self, timeout: Optional[float] = 10.0) -> None:
367
349
  """Wait for all background memory storage tasks to complete."""
368
350
  if not self._background_tasks:
369
351
  return
@@ -8,8 +8,10 @@ import warnings
8
8
  from typing import Annotated, Any, Optional, TypedDict
9
9
 
10
10
  from agent_framework import FunctionTool, tool
11
+ from supermemory.types import SearchResponse
11
12
 
12
13
  from .connection import AgentSupermemory
14
+ from .utils import _fetch_profile_and_search
13
15
 
14
16
 
15
17
  class MemorySearchResult(TypedDict, total=False):
@@ -50,13 +52,30 @@ def _to_jsonable(value: Any) -> Any:
50
52
  try:
51
53
  return _to_jsonable(model_dump(mode="json"))
52
54
  except TypeError:
53
- # Compatibility with pydantic-like models whose model_dump does not
54
- # accept Pydantic v2's ``mode`` argument.
55
55
  return _to_jsonable(model_dump())
56
56
 
57
57
  return value
58
58
 
59
59
 
60
+ def _serialize_search_results(response: SearchResponse) -> dict[str, Any]:
61
+ """Keep the tool's legacy result fields while retaining v5 metadata."""
62
+ results = [
63
+ {
64
+ "chunks": None,
65
+ "context": None,
66
+ "documents": None,
67
+ "filepath": None,
68
+ "is_aggregated": None,
69
+ "root_memory_id": None,
70
+ "version": None,
71
+ **_to_jsonable(item),
72
+ "updated_at": item.system.updated_at,
73
+ }
74
+ for item in response.results
75
+ ]
76
+ return {"results": results, "timing": response.search_time, "total": len(results)}
77
+
78
+
60
79
  class SupermemoryTools:
61
80
  """Memory tools for Microsoft Agent Framework.
62
81
 
@@ -92,28 +111,28 @@ class SupermemoryTools:
92
111
  """Search stored memories and source chunks.
93
112
 
94
113
  ``include_full_docs`` remains a deprecated Python-only argument for
95
- source compatibility. V4 search cannot return full source documents.
114
+ source compatibility. This tool does not return full source documents.
96
115
  """
97
116
  if include_full_docs is not None:
98
117
  warnings.warn(
99
- "include_full_docs is deprecated and ignored because v4 search "
100
- "does not return full source documents",
118
+ "include_full_docs is deprecated and ignored; search_memories "
119
+ "does not request full source documents",
101
120
  DeprecationWarning,
102
121
  stacklevel=2,
103
122
  )
104
123
 
105
124
  try:
106
- response = await self._client.search.memories(
107
- q=information_to_get,
108
- container_tag=self._connection.container_tag,
125
+ response = await self._client.search(
126
+ self._connection.container_tag,
127
+ query=information_to_get,
109
128
  limit=limit,
110
129
  threshold=0.6,
111
130
  search_mode="hybrid",
112
131
  )
113
- results = response.results or []
132
+ results = _serialize_search_results(response)["results"]
114
133
  result: MemorySearchResult = {
115
134
  "success": True,
116
- "results": [_to_jsonable(item) for item in results],
135
+ "results": results,
117
136
  "count": len(results),
118
137
  }
119
138
  return json.dumps(result, default=str)
@@ -131,9 +150,9 @@ class SupermemoryTools:
131
150
  """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."""
132
151
  try:
133
152
  response = await self._client.add(
153
+ self._connection.container_tag,
134
154
  content=memory,
135
- container_tag=self._connection.container_tag,
136
- custom_id=self._connection.custom_id,
155
+ id=self._connection.custom_id,
137
156
  )
138
157
  result: MemoryAddResult = {
139
158
  "success": True,
@@ -153,23 +172,24 @@ class SupermemoryTools:
153
172
  ) -> str:
154
173
  """Get user profile containing static memories (permanent facts) and dynamic memories (recent context). Optionally include search results by providing a query."""
155
174
  try:
156
- kwargs: dict[str, Any] = {"container_tag": self._connection.container_tag}
157
- if query:
158
- kwargs["q"] = query
159
-
160
- response = await self._client.profile(**kwargs)
175
+ response, search = await _fetch_profile_and_search(
176
+ self._client, self._connection.container_tag, query=query
177
+ )
161
178
  result: dict[str, Any] = {
162
179
  "success": True,
163
180
  "profile": (
164
- _to_jsonable(response.profile)
165
- if hasattr(response, "profile")
166
- else None
167
- ),
168
- "search_results": (
169
- _to_jsonable(response.search_results)
170
- if hasattr(response, "search_results")
181
+ {
182
+ "static": [fact.memory for fact in response.profile.static],
183
+ "dynamic": [fact.memory for fact in response.profile.dynamic],
184
+ "buckets": {
185
+ name: [fact.memory for fact in facts]
186
+ for name, facts in response.profile.buckets.items()
187
+ },
188
+ }
189
+ if response
171
190
  else None
172
191
  ),
192
+ "search_results": _serialize_search_results(search) if search else None,
173
193
  }
174
194
  return json.dumps(result, default=str)
175
195
  except Exception as error:
@@ -1,9 +1,13 @@
1
1
  """Utility functions for Supermemory Agent Framework integration."""
2
2
 
3
+ import asyncio
3
4
  import json
4
5
  import re
5
6
  from typing import Any, Optional, Protocol
6
7
 
8
+ import supermemory
9
+ from supermemory.types import ProfileResponse, SearchResponse
10
+
7
11
  DEFAULT_CONTEXT_PROMPT = "The following are retrieved memories about the user."
8
12
  MEMORY_CONTEXT_PATTERN = re.compile(
9
13
  r'(?:\r?\n)?<supermemory context="user-memories" readonly>.*?</supermemory>',
@@ -15,6 +19,31 @@ SUPERMEMORY_TAG_PATTERN = re.compile(
15
19
  )
16
20
 
17
21
 
22
+ async def _fetch_profile_and_search(
23
+ client: supermemory.AsyncSupermemory,
24
+ container_tag: str,
25
+ *,
26
+ include_profile: bool = True,
27
+ query: str = "",
28
+ ) -> tuple[Optional[ProfileResponse], Optional[SearchResponse]]:
29
+ """Fetch the independent v5 profile and query resources in one namespace."""
30
+ if include_profile and query:
31
+ profile, search = await asyncio.gather(
32
+ client.profile(container_tag),
33
+ client.search(
34
+ container_tag, query=query, threshold=0.6, search_mode="memories"
35
+ ),
36
+ )
37
+ return profile, search
38
+ if include_profile:
39
+ return await client.profile(container_tag), None
40
+ if query:
41
+ return None, await client.search(
42
+ container_tag, query=query, threshold=0.6, search_mode="memories"
43
+ )
44
+ return None, None
45
+
46
+
18
47
  def _escape_supermemory_tags(content: str) -> str:
19
48
  """Escape nested Supermemory tags supplied as untrusted memory data."""
20
49
 
@@ -134,7 +163,6 @@ def deduplicate_memories(
134
163
  if isinstance(value, str) and value.strip():
135
164
  return value.strip()
136
165
  return None
137
- # Stainless SDK returns pydantic models (attribute access, snake_case).
138
166
  for field in ("memory", "chunk", "content"):
139
167
  value = getattr(item, field, None)
140
168
  if isinstance(value, str) and value.strip():