Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c05e889c73 | ||
|
|
9b7001e44c | ||
|
|
fa80bfdb3f | ||
|
|
57b0846784 | ||
|
|
c9a0c19ac2 | ||
|
|
19cb89aff4 | ||
|
|
fdfb763d6e | ||
|
|
0070e08e01 | ||
|
|
39bc023305 | ||
|
|
b1342a3408 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Preview Dream Scope"
|
||||
description: "A no-write preview of the scope Dream synthesis would analyze for a project."
|
||||
openapi: "post /api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/preview/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Get Dream Activity"
|
||||
description: "Supersede/merge activity feed for a project, newest first (keyset-paginated)."
|
||||
openapi: "get /api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/activity/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Get Dream Configuration"
|
||||
description: "Retrieve a project's Dream (memory synthesis) configuration and plan entitlements."
|
||||
openapi: "get /api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/config/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Get a Synthesized Memory's Sources"
|
||||
description: "The source memories a synthesized (pattern) memory was distilled from."
|
||||
openapi: "get /api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/memory/{memory_id}/sources/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Get Memories in a Dream Run"
|
||||
description: "Keyset page of the synthesized memories within a single synthesis run."
|
||||
openapi: "get /api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/runs/{run_id}/memories/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Get Dream Synthesis Runs"
|
||||
description: "Synthesis activity grouped per run, newest first (keyset-paginated)."
|
||||
openapi: "get /api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/runs/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Get Dream Stats"
|
||||
description: "Lifecycle and synthesis counts for a project, plus reflection freshness."
|
||||
openapi: "get /api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/stats/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Update Dream Configuration"
|
||||
description: "Enable or disable Synthesis (reflection) for a project, or change the reflection mode."
|
||||
openapi: "patch /api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/config/"
|
||||
---
|
||||
@@ -2841,6 +2841,13 @@ Existing memories written by the previous versions are not rewritten. If your me
|
||||
|
||||
<Tab title="Strands">
|
||||
|
||||
<Update label="2026-08-25" description="mem0-strands v0.1.1">
|
||||
|
||||
**New Features:**
|
||||
- **Usage telemetry:** Anonymous usage events (`strands.store.init`, `strands.store.search`, `strands.store.add`, `strands.store.add_messages`) ride the Mem0 SDK's existing PostHog client, unsampled, with no new dependency. Events carry only counts, durations, booleans, and coarse failure kinds: never queries, memory text, message content, entity ids, metadata, or API keys. Opt out with `MEM0_TELEMETRY=false` ([#7110](https://github.com/mem0ai/mem0/pull/7110))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-08-24" description="mem0-strands v0.1.0">
|
||||
|
||||
**Initial release** of [`mem0-strands`](https://pypi.org/project/mem0-strands/), a native `MemoryStore` that plugs Mem0 into the [Strands Agents](https://strandsagents.com/) `MemoryManager` ([#7021](https://github.com/mem0ai/mem0/pull/7021))
|
||||
@@ -2863,6 +2870,13 @@ Existing memories written by the previous versions are not rewritten. If your me
|
||||
|
||||
<Tab title="DeepSeek Harness">
|
||||
|
||||
<Update label="2026-08-25" description="deepseek-plugin v0.1.1">
|
||||
|
||||
**New Features:**
|
||||
- **Usage telemetry:** Anonymous usage events (`deepseek.plugin.mounted`, `deepseek.tool.search_memory`, `deepseek.tool.add_memory`) are batched to PostHog over native fetch and flushed in the background. Events carry only tool names, durations, counts, and coarse failure kinds: never queries, memory text, filters, or API keys. Opt out with `MEM0_TELEMETRY=false` ([#7110](https://github.com/mem0ai/mem0/pull/7110))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-08-24" description="deepseek-plugin v0.1.0">
|
||||
|
||||
**Initial release** of [`deepseek-plugin`](https://www.npmjs.com/package/@mem0/deepseek-plugin), a native DeepSeek Harness (Cordis) plugin that registers Mem0 as two agent-callable tools ([#7027](https://github.com/mem0ai/mem0/pull/7027))
|
||||
|
||||
+210
@@ -539,6 +539,20 @@
|
||||
"api-reference/project/delete-project"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Dream",
|
||||
"icon": "sparkles",
|
||||
"pages": [
|
||||
"api-reference/dream/get-dream-config",
|
||||
"api-reference/dream/update-dream-config",
|
||||
"api-reference/dream/get-dream-stats",
|
||||
"api-reference/dream/get-dream-activity",
|
||||
"api-reference/dream/get-dream-runs",
|
||||
"api-reference/dream/get-dream-run-memories",
|
||||
"api-reference/dream/get-dream-memory-sources",
|
||||
"api-reference/dream/dream-preview"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Webhooks",
|
||||
"icon": "webhook",
|
||||
@@ -1301,6 +1315,202 @@
|
||||
{
|
||||
"source": "/api-reference/entities/get-entities",
|
||||
"destination": "/api-reference/entities/get-users"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/event/get-events",
|
||||
"destination": "/api-reference/events/get-events"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/entities/delete-users",
|
||||
"destination": "/api-reference/entities/delete-user"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/organization/update-organization-member",
|
||||
"destination": "/api-reference/organization/update-org-member"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/organization/get-organizations",
|
||||
"destination": "/api-reference/organization/get-orgs"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/entities/get-user",
|
||||
"destination": "/api-reference/entities/get-users"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/export/create-memory-export",
|
||||
"destination": "/api-reference/memory/create-memory-export"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/mcp",
|
||||
"destination": "/platform/mem0-mcp"
|
||||
},
|
||||
{
|
||||
"source": "/platform/api-reference",
|
||||
"destination": "/api-reference"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/memory/add-memory",
|
||||
"destination": "/api-reference/memory/add-memories"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/overview",
|
||||
"destination": "/api-reference"
|
||||
},
|
||||
{
|
||||
"source": "/platform/reference/python-sdk",
|
||||
"destination": "/open-source/python-quickstart"
|
||||
},
|
||||
{
|
||||
"source": "/platform/python",
|
||||
"destination": "/open-source/python-quickstart"
|
||||
},
|
||||
{
|
||||
"source": "/platform/features/memory",
|
||||
"destination": "/platform/features/memory-decay"
|
||||
},
|
||||
{
|
||||
"source": "/platform/api-reference/python",
|
||||
"destination": "/open-source/python-quickstart"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/webhooks",
|
||||
"destination": "/api-reference/webhook/create-webhook"
|
||||
},
|
||||
{
|
||||
"source": "/platform/python-quickstart",
|
||||
"destination": "/open-source/python-quickstart"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/organization/delete-org-project",
|
||||
"destination": "/api-reference/organization/delete-org"
|
||||
},
|
||||
{
|
||||
"source": "/platform/features/scoping",
|
||||
"destination": "/platform/features/entity-scoped-memory"
|
||||
},
|
||||
{
|
||||
"source": "/platform/quickstart/python",
|
||||
"destination": "/open-source/python-quickstart"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/memory/history",
|
||||
"destination": "/api-reference/memory/history-memory"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/entities/get-user-memories",
|
||||
"destination": "/api-reference/entities/get-users"
|
||||
},
|
||||
{
|
||||
"source": "/platform/api-reference/webhooks/create",
|
||||
"destination": "/api-reference/webhook/create-webhook"
|
||||
},
|
||||
{
|
||||
"source": "/platform/python/quickstart",
|
||||
"destination": "/open-source/python-quickstart"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/memory/batch-update-memory",
|
||||
"destination": "/api-reference/memory/batch-update"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/memories/add",
|
||||
"destination": "/api-reference/memory/add-memories"
|
||||
},
|
||||
{
|
||||
"source": "/python-quickstart",
|
||||
"destination": "/open-source/python-quickstart"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/webhook/list-webhooks",
|
||||
"destination": "/api-reference/webhook/get-webhook"
|
||||
},
|
||||
{
|
||||
"source": "/platform/reference/memory/delete-memory",
|
||||
"destination": "/api-reference/memory/delete-memory"
|
||||
},
|
||||
{
|
||||
"source": "/platform/webhooks",
|
||||
"destination": "/platform/features/webhooks"
|
||||
},
|
||||
{
|
||||
"source": "/platform/api-reference/memories/add",
|
||||
"destination": "/api-reference/memory/add-memories"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/organization/organizations",
|
||||
"destination": "/api-reference/organization/create-org"
|
||||
},
|
||||
{
|
||||
"source": "/sdk/python/quickstart",
|
||||
"destination": "/open-source/python-quickstart"
|
||||
},
|
||||
{
|
||||
"source": "/platform/export",
|
||||
"destination": "/platform/features/memory-export"
|
||||
},
|
||||
{
|
||||
"source": "/open-source/python-memory",
|
||||
"destination": "/open-source/python-quickstart"
|
||||
},
|
||||
{
|
||||
"source": "/platform/reference",
|
||||
"destination": "/api-reference"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/memory/get-all-memories",
|
||||
"destination": "/api-reference/memory/get-memories"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/user/delete-users",
|
||||
"destination": "/api-reference/entities/delete-user"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/memories/search-memories",
|
||||
"destination": "/api-reference/memory/search-memories"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/organization/update-members",
|
||||
"destination": "/api-reference/project/update-project-member"
|
||||
},
|
||||
{
|
||||
"source": "/platform/api-reference/memory/batch-update",
|
||||
"destination": "/api-reference/memory/batch-update"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/memory/search",
|
||||
"destination": "/api-reference/memory/search-memories"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/batch/update-memories",
|
||||
"destination": "/api-reference/memory/batch-update"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/organization/update-organization-member-role",
|
||||
"destination": "/api-reference/project/update-project-member"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/webhooks/delete",
|
||||
"destination": "/api-reference/webhook/delete-webhook"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/memory/delete-all-memories",
|
||||
"destination": "/api-reference/memory/delete-memories"
|
||||
},
|
||||
{
|
||||
"source": "/platform/reference/overview",
|
||||
"destination": "/api-reference"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/mem0/add-memories",
|
||||
"destination": "/api-reference/memory/add-memories"
|
||||
},
|
||||
{
|
||||
"source": "/platform/mcp/overview",
|
||||
"destination": "/platform/mem0-mcp"
|
||||
},
|
||||
{
|
||||
"source": "/openmemory/mcp/introduction",
|
||||
"destination": "https://mem0.ai/blog/introducing-openmemory-mcp"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -383,6 +383,16 @@ All API Reference docs describe Mem0 Platform REST endpoints (requires API key).
|
||||
- [Remove Project Member](https://docs.mem0.ai/api-reference/project/remove-project-member) [Platform]: Use when removing a member from a project.
|
||||
- [Delete Project](https://docs.mem0.ai/api-reference/project/delete-project) [Platform]: Use when removing a project.
|
||||
|
||||
### Dream
|
||||
- [Get Dream Configuration](https://docs.mem0.ai/api-reference/dream/get-dream-config) [Platform]: Use when reading a project's Dream (memory synthesis) config and plan entitlements.
|
||||
- [Update Dream Configuration](https://docs.mem0.ai/api-reference/dream/update-dream-config) [Platform]: Use when enabling/disabling Synthesis or changing its mode for a project.
|
||||
- [Get Dream Stats](https://docs.mem0.ai/api-reference/dream/get-dream-stats) [Platform]: Use when fetching lifecycle and synthesis counts for the Dream dashboard.
|
||||
- [Get Dream Activity](https://docs.mem0.ai/api-reference/dream/get-dream-activity) [Platform]: Use when listing recent supersede/merge activity for a project.
|
||||
- [Get Dream Synthesis Runs](https://docs.mem0.ai/api-reference/dream/get-dream-runs) [Platform]: Use when listing synthesis runs and the memories they generated.
|
||||
- [Get Memories in a Dream Run](https://docs.mem0.ai/api-reference/dream/get-dream-run-memories) [Platform]: Use when paging the synthesized memories within a single run.
|
||||
- [Get a Synthesized Memory's Sources](https://docs.mem0.ai/api-reference/dream/get-dream-memory-sources) [Platform]: Use when tracing the source memories a synthesized memory was distilled from.
|
||||
- [Preview Dream Scope](https://docs.mem0.ai/api-reference/dream/dream-preview) [Platform]: Use when previewing what Synthesis would analyze for a project (no writes).
|
||||
|
||||
### Webhooks
|
||||
- [Create Webhook](https://docs.mem0.ai/api-reference/webhook/create-webhook) [Platform]: Use when registering a webhook endpoint.
|
||||
- [Get Webhook](https://docs.mem0.ai/api-reference/webhook/get-webhook) [Platform]: Use when fetching webhook config.
|
||||
|
||||
@@ -7207,6 +7207,869 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/config/": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"dream"
|
||||
],
|
||||
"summary": "Get Dream configuration",
|
||||
"description": "Retrieve the project's Dream (memory synthesis) configuration together with the plan entitlement snapshot. Dream automatically supersedes/merges memories on the add path; **Synthesis** (reflection) is the opt-in part controlled by `reflection_enabled`.",
|
||||
"operationId": "get_dream_config",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "org_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Unique identifier of the organization.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "project_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Unique identifier of the project.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"config": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reflection_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether background synthesis (reflection) is enabled for the project."
|
||||
},
|
||||
"reflection_enabled_at": {
|
||||
"type": "string",
|
||||
"description": "When synthesis was last enabled. Synthesis only considers memories created on or after this time (forward-only).",
|
||||
"format": "date-time",
|
||||
"nullable": true
|
||||
},
|
||||
"reflection_mode": {
|
||||
"type": "string",
|
||||
"description": "Synthesis execution mode.",
|
||||
"enum": [
|
||||
"batch",
|
||||
"direct"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"entitlements": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"plan": {
|
||||
"type": "string",
|
||||
"description": "Resolved billing tier for the project (e.g. `free`, `pro`, `custom`)."
|
||||
},
|
||||
"reflection_interval_days": {
|
||||
"type": "integer",
|
||||
"description": "How often synthesis runs for this plan, in days (Pro weekly, Enterprise daily)."
|
||||
},
|
||||
"features": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"description": "Per-feature entitlement snapshot, keyed by Dream feature (e.g. `dream_reflection`, `dream_tab`, `dream_preview`).",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"entitled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the plan is entitled to the feature."
|
||||
},
|
||||
"killed": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the feature is globally disabled by a kill switch."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Effective state: entitled AND not killed AND turned on."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"tags": [
|
||||
"dream"
|
||||
],
|
||||
"summary": "Update Dream configuration",
|
||||
"description": "Enable or disable Synthesis (reflection) for the project, or change the reflection mode. Requires a Pro or Enterprise (CUSTOM) plan and organization-owner permission. Synthesis is forward-only: after enabling, it only synthesizes memories added from that point on.",
|
||||
"operationId": "update_dream_config",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "org_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Unique identifier of the organization.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "project_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Unique identifier of the project.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reflection_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Turn background synthesis on or off."
|
||||
},
|
||||
"reflection_mode": {
|
||||
"type": "string",
|
||||
"description": "Synthesis execution mode.",
|
||||
"enum": [
|
||||
"batch",
|
||||
"direct"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Updated configuration.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"config": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reflection_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether background synthesis (reflection) is enabled for the project."
|
||||
},
|
||||
"reflection_enabled_at": {
|
||||
"type": "string",
|
||||
"description": "When synthesis was last enabled. Synthesis only considers memories created on or after this time (forward-only).",
|
||||
"format": "date-time",
|
||||
"nullable": true
|
||||
},
|
||||
"reflection_mode": {
|
||||
"type": "string",
|
||||
"description": "Synthesis execution mode.",
|
||||
"enum": [
|
||||
"batch",
|
||||
"direct"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"entitlements": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"plan": {
|
||||
"type": "string",
|
||||
"description": "Resolved billing tier for the project (e.g. `free`, `pro`, `custom`)."
|
||||
},
|
||||
"reflection_interval_days": {
|
||||
"type": "integer",
|
||||
"description": "How often synthesis runs for this plan, in days (Pro weekly, Enterprise daily)."
|
||||
},
|
||||
"features": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"description": "Per-feature entitlement snapshot, keyed by Dream feature (e.g. `dream_reflection`, `dream_tab`, `dream_preview`).",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"entitled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the plan is entitled to the feature."
|
||||
},
|
||||
"killed": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the feature is globally disabled by a kill switch."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Effective state: entitled AND not killed AND turned on."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Validation error (e.g. a field the plan is not entitled to change)."
|
||||
},
|
||||
"403": {
|
||||
"description": "Not entitled on the current plan (`upgrade_required: true`), or the caller is not an organization owner."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/stats/": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"dream"
|
||||
],
|
||||
"summary": "Get Dream stats",
|
||||
"description": "Lifecycle + synthesis counts for the project's Dream dashboard, plus reflection freshness. Requires a Pro or Enterprise (CUSTOM) plan.",
|
||||
"operationId": "get_dream_stats",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "org_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Unique identifier of the organization.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "project_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Unique identifier of the project.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"active": {
|
||||
"type": "integer",
|
||||
"description": "Active (current) memory count."
|
||||
},
|
||||
"merged": {
|
||||
"type": "integer",
|
||||
"description": "Count of memories merged into a canonical one."
|
||||
},
|
||||
"superseded": {
|
||||
"type": "integer",
|
||||
"description": "Count of memories superseded by a newer one."
|
||||
},
|
||||
"synthesized": {
|
||||
"type": "integer",
|
||||
"description": "Count of higher-order pattern memories created by synthesis."
|
||||
},
|
||||
"last_run_at": {
|
||||
"type": "string",
|
||||
"description": "When synthesis last completed.",
|
||||
"format": "date-time",
|
||||
"nullable": true
|
||||
},
|
||||
"processed_through": {
|
||||
"type": "string",
|
||||
"description": "Timestamp of the newest source memory synthesis has processed.",
|
||||
"format": "date-time",
|
||||
"nullable": true
|
||||
},
|
||||
"running": {
|
||||
"type": "boolean",
|
||||
"description": "Whether a synthesis run is currently in flight."
|
||||
},
|
||||
"next_run_at": {
|
||||
"type": "string",
|
||||
"description": "Projected next synthesis run.",
|
||||
"format": "date-time",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Not entitled on the current plan (`upgrade_required: true`)."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/activity/": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"dream"
|
||||
],
|
||||
"summary": "Get Dream activity",
|
||||
"description": "Supersede/merge activity feed (newest first), keyset-paginated. Synthesis output is not included here — see the runs endpoint.",
|
||||
"operationId": "get_dream_activity",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "org_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Unique identifier of the organization.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "project_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Unique identifier of the project.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Page size (default 50, max 200).",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 50
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cursor",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Opaque keyset cursor from a previous page's `next_cursor`.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"results": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_id": {
|
||||
"type": "string",
|
||||
"description": "The superseded/merged memory's ID."
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "The memory's text (truncated)."
|
||||
},
|
||||
"transition": {
|
||||
"type": "string",
|
||||
"description": "Lifecycle transition.",
|
||||
"enum": [
|
||||
"merged",
|
||||
"superseded"
|
||||
]
|
||||
},
|
||||
"at": {
|
||||
"type": "string",
|
||||
"description": "When the transition happened.",
|
||||
"format": "date-time"
|
||||
},
|
||||
"replaced_by": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "ID of the newer memory."
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Text of the newer memory (truncated).",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"nullable": true,
|
||||
"description": "The newer memory that replaced this one (null for a merge with no single replacement)."
|
||||
},
|
||||
"sources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Source memory ID."
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Source memory text (truncated)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"nullable": true,
|
||||
"description": "Always null on this feed; synthesis provenance is shown on the runs feed."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"next_cursor": {
|
||||
"type": "string",
|
||||
"description": "Cursor for the next page, or null when there are no more.",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/runs/": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"dream"
|
||||
],
|
||||
"summary": "Get Dream synthesis runs",
|
||||
"description": "Synthesis activity grouped per run (newest first), keyset-paginated. Each run inlines its first page of synthesized memories with their sources; larger runs page the rest via the run-memories endpoint.",
|
||||
"operationId": "get_dream_runs",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "org_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Unique identifier of the organization.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "project_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Unique identifier of the project.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Number of runs per page (default 20, max 100).",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cursor",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Opaque keyset cursor from a previous page's `next_cursor`.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"results": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"run_id": {
|
||||
"type": "string",
|
||||
"description": "Identifier of the synthesis run."
|
||||
},
|
||||
"at": {
|
||||
"type": "string",
|
||||
"description": "When the run produced memories.",
|
||||
"format": "date-time"
|
||||
},
|
||||
"count": {
|
||||
"type": "integer",
|
||||
"description": "Total synthesized memories in this run."
|
||||
},
|
||||
"synthesized": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Synthesized memory ID."
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Synthesized (pattern) memory text."
|
||||
},
|
||||
"sources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Source memory ID."
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Source memory text (truncated)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Source memories this pattern was distilled from."
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "First page of synthesized memories (each with its sources)."
|
||||
},
|
||||
"has_more": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the run has more memories than are inlined here."
|
||||
},
|
||||
"mem_cursor": {
|
||||
"type": "string",
|
||||
"description": "Cursor to page this run's remaining memories via the run-memories endpoint.",
|
||||
"nullable": true
|
||||
},
|
||||
"sources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Source memory ID."
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Source memory text (truncated)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "De-duplicated source memories across the inlined page."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"next_cursor": {
|
||||
"type": "string",
|
||||
"description": "Cursor for the next page, or null when there are no more.",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/runs/{run_id}/memories/": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"dream"
|
||||
],
|
||||
"summary": "Get memories in a Dream run",
|
||||
"description": "Keyset page of the synthesized memories within a single run (for runs whose `has_more` is true).",
|
||||
"operationId": "get_dream_run_memories",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "org_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Unique identifier of the organization.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "project_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Unique identifier of the project.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "run_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Identifier of the synthesis run.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Page size (default 50, max 200).",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 50
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cursor",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Opaque keyset cursor (use the run's `mem_cursor` or a previous `next_cursor`).",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"results": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Synthesized memory ID."
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Synthesized (pattern) memory text."
|
||||
},
|
||||
"sources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Source memory ID."
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Source memory text (truncated)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Source memories this pattern was distilled from."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"next_cursor": {
|
||||
"type": "string",
|
||||
"description": "Cursor for the next page, or null when there are no more.",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/memory/{memory_id}/sources/": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"dream"
|
||||
],
|
||||
"summary": "Get a synthesized memory's sources",
|
||||
"description": "The source memories a synthesized (pattern) memory was distilled from, for the memory drawer's provenance panel.",
|
||||
"operationId": "get_dream_memory_sources",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "org_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Unique identifier of the organization.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "project_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Unique identifier of the project.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "memory_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "ID of the (synthesized) memory.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_id": {
|
||||
"type": "string",
|
||||
"description": "The memory's ID."
|
||||
},
|
||||
"synthesized": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this memory was produced by synthesis."
|
||||
},
|
||||
"sources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Source memory ID."
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Source memory text."
|
||||
},
|
||||
"lifecycle_state": {
|
||||
"type": "string",
|
||||
"description": "Source memory lifecycle state.",
|
||||
"enum": [
|
||||
"active",
|
||||
"merged",
|
||||
"superseded"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Memory not found in this project."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/preview/": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"dream"
|
||||
],
|
||||
"summary": "Preview Dream scope",
|
||||
"description": "A no-write preview of the scope Dream synthesis would analyze for the project (a capped sample plus the count of users with enough memories to benefit). Never runs the LLM and never mutates anything.",
|
||||
"operationId": "dream_preview",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "org_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Unique identifier of the organization.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "project_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Unique identifier of the project.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"active_total": {
|
||||
"type": "integer",
|
||||
"description": "Total active memories in the project."
|
||||
},
|
||||
"scanned": {
|
||||
"type": "integer",
|
||||
"description": "How many recent memories the preview aggregated over (capped)."
|
||||
},
|
||||
"cap": {
|
||||
"type": "integer",
|
||||
"description": "The scan cap applied."
|
||||
},
|
||||
"eligible_users": {
|
||||
"type": "integer",
|
||||
"description": "Users with enough memories to benefit from synthesis."
|
||||
},
|
||||
"note": {
|
||||
"type": "string",
|
||||
"description": "Human-readable note about the preview scope."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Preview not available on the current plan (`upgrade_required: true`)."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
|
||||
@@ -13,6 +13,7 @@ Agent and editor integrations. Each subdirectory is self-contained: its own `pac
|
||||
| `n8n-nodes-mem0/` | `@mem0/n8n-nodes-mem0` | tsc | ESLint (n8n-nodes-base) | none |
|
||||
| `zapier-mem0/` | `@mem0/zapier` | tsc | none | offline unit tests + `zapier validate` |
|
||||
| `mem0-strands/` | `mem0-strands` (PyPI) | hatch | Ruff + mypy | pytest |
|
||||
| `minimax-mem0/` | MiniMax Marketplace plugin | none (declarative) | none | JSON/manifest validation |
|
||||
|
||||
pnpm everywhere except `.opencode-plugin/` (Bun) and `mem0-strands/` (Python: pip / hatch). Never npm, never yarn.
|
||||
|
||||
@@ -44,6 +45,7 @@ Run the type check after every TypeScript change: `pnpm run typecheck` or `tsc -
|
||||
- **`openclaw/`**, **`pi-agent-plugin/`**, **`deepseek-plugin/`** are editor and agent plugins with the same shape. `deepseek-plugin/` registers Mem0 search/add tools as a native DeepSeek Harness (Cordis) plugin.
|
||||
- **`n8n-nodes-mem0/`** is an n8n community node: add, search, get, update, delete.
|
||||
- **`zapier-mem0/`** is a Zapier Platform CLI app: add, search, get, delete. It deploys to Zapier, not npm, so it is **not** in the release router. Deploy it with `gh workflow run zapier-mem0-cd.yml --ref main` (needs the `ZAPIER_DEPLOY_KEY` secret).
|
||||
- **`minimax-mem0/`** is a declarative MiniMax Marketplace plugin (no build step): a `.minimax-plugin/plugin.json` manifest plus an MCP capability pointing at the hosted Mem0 MCP server (`https://mcp.mem0.ai/mcp/`) and a memory Skill. It ships to the MiniMax Marketplace as a subdirectory, not through the release router or npm/PyPI.
|
||||
- **`mem0-strands/`** is a native Strands `MemoryStore` (Python, published to PyPI as `mem0-strands`). It plugs into the Strands `MemoryManager` for automatic recall and server-side extraction, over the hosted Mem0 platform or self-hosted Mem0 OSS. The package lives under `mem0-strands/python/`.
|
||||
|
||||
## Adding an integration
|
||||
|
||||
@@ -55,6 +55,8 @@ For a Mem0 Platform on-prem or dedicated deployment, point `config.host` at that
|
||||
|
||||
Writes are tagged `source="DEEPSEEK_HARNESS"` so Mem0's backend can attribute usage to this integration. For it to surface by name (rather than bucketing into `OTHERS`), `DEEPSEEK_HARNESS` must be present in the backend's `KNOWN_EVENT_SOURCES` allowlist, a one-line platform change matching the existing `ZAPIER` / `STRANDS` sources.
|
||||
|
||||
The plugin also sends anonymous usage events (which tool ran, duration, result counts, coarse failure kind) so Mem0 can tell how the plugin is used and where it breaks. Queries, memory text, and entity ids are never sent. Turn it off with `MEM0_TELEMETRY=false`.
|
||||
|
||||
## Status
|
||||
|
||||
Developer preview. Tracks the DeepSeek Harness v0.1 plugin API (`@deepseek-ai/cordis`, `@deepseek-ai/dsh-tools`), which is young and moving; pin versions once it stabilizes. Auto-capture (store turns without an explicit tool call) and auto-recall (inject memory into the prompt at assembly) are planned once the harness session/assembly event API is confirmed.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/deepseek-plugin",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Mem0 long-term memory as a native DeepSeek Harness (Cordis) plugin.",
|
||||
"type": "module",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -16,6 +16,7 @@ import { MemoryClient } from "mem0ai";
|
||||
import { formatMemoryList, formatAddResult } from "./formatting.ts";
|
||||
import { truncateOutput } from "./output.ts";
|
||||
import { resolveSearchFilters, resolveAddParams } from "./scoping.ts";
|
||||
import { captureEvent, errorKind } from "./telemetry.ts";
|
||||
|
||||
export const name = "mem0";
|
||||
export const inject = ["tools"];
|
||||
@@ -81,6 +82,8 @@ export function apply(ctx: Context, config: Config): void {
|
||||
...(config.host ? { host: config.host } : {}),
|
||||
});
|
||||
|
||||
captureEvent("deepseek.plugin.mounted", { has_host: Boolean(config.host) }, client);
|
||||
|
||||
// Recall. The platform rejects top-level entity params on search, so scope
|
||||
// goes inside `filters` (unlike add below, which takes them top-level).
|
||||
ctx.tools.register(
|
||||
@@ -99,11 +102,36 @@ export function apply(ctx: Context, config: Config): void {
|
||||
output: textOutput,
|
||||
async execute({ query, limit, userId: u, agentId, runId }) {
|
||||
const filters = resolveSearchFilters({ userId: u, agentId, runId }, userId);
|
||||
const topK = limit && limit > 0 ? limit : DEFAULT_SEARCH_LIMIT;
|
||||
const started = Date.now();
|
||||
try {
|
||||
const topK = limit && limit > 0 ? limit : DEFAULT_SEARCH_LIMIT;
|
||||
const { results } = await client.search(query, { filters, topK });
|
||||
captureEvent(
|
||||
"deepseek.tool.search_memory",
|
||||
{
|
||||
success: true,
|
||||
duration_ms: Date.now() - started,
|
||||
top_k: topK,
|
||||
result_count: results?.length ?? 0,
|
||||
query_chars: query.length,
|
||||
scope_overridden: Boolean(u && u !== userId),
|
||||
has_agent_id: Boolean(agentId),
|
||||
has_run_id: Boolean(runId),
|
||||
},
|
||||
client,
|
||||
);
|
||||
return truncateOutput(formatMemoryList(results ?? []));
|
||||
} catch (err) {
|
||||
captureEvent(
|
||||
"deepseek.tool.search_memory",
|
||||
{
|
||||
success: false,
|
||||
duration_ms: Date.now() - started,
|
||||
top_k: topK,
|
||||
error_kind: errorKind(err),
|
||||
},
|
||||
client,
|
||||
);
|
||||
return `search_memory failed: ${err instanceof Error ? err.message : String(err)}`;
|
||||
}
|
||||
},
|
||||
@@ -123,13 +151,37 @@ export function apply(ctx: Context, config: Config): void {
|
||||
output: textOutput,
|
||||
async execute({ text, userId: u, agentId, runId }) {
|
||||
const addParams = resolveAddParams({ userId: u, agentId, runId }, userId);
|
||||
const started = Date.now();
|
||||
try {
|
||||
const result = await client.add([{ role: "user", content: text }], {
|
||||
...addParams,
|
||||
source: SOURCE,
|
||||
});
|
||||
captureEvent(
|
||||
"deepseek.tool.add_memory",
|
||||
{
|
||||
success: true,
|
||||
duration_ms: Date.now() - started,
|
||||
text_chars: text.length,
|
||||
memory_count: Array.isArray(result) ? result.length : 0,
|
||||
scope_overridden: Boolean(u && u !== userId),
|
||||
has_agent_id: Boolean(agentId),
|
||||
has_run_id: Boolean(runId),
|
||||
},
|
||||
client,
|
||||
);
|
||||
return truncateOutput(formatAddResult(result));
|
||||
} catch (err) {
|
||||
captureEvent(
|
||||
"deepseek.tool.add_memory",
|
||||
{
|
||||
success: false,
|
||||
duration_ms: Date.now() - started,
|
||||
text_chars: text.length,
|
||||
error_kind: errorKind(err),
|
||||
},
|
||||
client,
|
||||
);
|
||||
return `add_memory failed: ${err instanceof Error ? err.message : String(err)}`;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Anonymous usage telemetry for the DeepSeek Harness plugin.
|
||||
*
|
||||
* Fire-and-forget PostHog events over native fetch, batched and flushed every
|
||||
* 5 seconds, at 10 queued events, or on process exit. Never throws, never logs.
|
||||
*
|
||||
* Events carry only tool names, durations, counts, and coarse failure kinds:
|
||||
* never queries, memory text, filters, or API keys.
|
||||
*
|
||||
* Disable with MEM0_TELEMETRY=false.
|
||||
*/
|
||||
import { randomUUID } from "node:crypto";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
|
||||
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
|
||||
const POSTHOG_BATCH_URL = "https://us.i.posthog.com/batch/";
|
||||
const FLUSH_INTERVAL_MS = 5_000;
|
||||
const FLUSH_THRESHOLD = 10;
|
||||
const SEND_TIMEOUT_MS = 3_000;
|
||||
|
||||
const PLUGIN_VERSION = ((): string => {
|
||||
try {
|
||||
return JSON.parse(
|
||||
fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"),
|
||||
).version;
|
||||
} catch {
|
||||
return "unknown";
|
||||
}
|
||||
})();
|
||||
|
||||
/** The Mem0 SDK resolves this to the account email once ping() lands, so events join other Mem0 surfaces. */
|
||||
export interface TelemetryIdentity {
|
||||
telemetryId?: string;
|
||||
}
|
||||
|
||||
let queue: Record<string, unknown>[] = [];
|
||||
let flushTimer: ReturnType<typeof setInterval> | undefined;
|
||||
let exitHandlerInstalled = false;
|
||||
let cachedAnonymousId: string | undefined;
|
||||
let identified = false;
|
||||
|
||||
function identityPath(): string {
|
||||
return path.join(os.homedir(), ".mem0", "deepseek-plugin-telemetry.json");
|
||||
}
|
||||
|
||||
export function isTelemetryEnabled(): boolean {
|
||||
const value = process.env.MEM0_TELEMETRY?.toLowerCase();
|
||||
return value !== "false" && value !== "0" && value !== "no" && value !== "off";
|
||||
}
|
||||
|
||||
function anonymousId(): string {
|
||||
if (cachedAnonymousId) return cachedAnonymousId;
|
||||
try {
|
||||
const stored = JSON.parse(fs.readFileSync(identityPath(), "utf-8"));
|
||||
if (typeof stored.anonymousId === "string" && stored.anonymousId) {
|
||||
cachedAnonymousId = stored.anonymousId;
|
||||
return cachedAnonymousId!;
|
||||
}
|
||||
} catch {
|
||||
/* first run, or an unreadable identity file */
|
||||
}
|
||||
const created = `deepseek-anon-${randomUUID().replace(/-/g, "")}`;
|
||||
try {
|
||||
const target = identityPath();
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
fs.writeFileSync(target, JSON.stringify({ anonymousId: created }), "utf-8");
|
||||
} catch {
|
||||
/* an unwritable home directory must not break the plugin */
|
||||
}
|
||||
cachedAnonymousId = created;
|
||||
return created;
|
||||
}
|
||||
|
||||
/** Merge the pre-ping anonymous history into the account once the email resolves. */
|
||||
function identifyEvent(distinctId: string): Record<string, unknown> | undefined {
|
||||
if (identified || distinctId.startsWith("deepseek-anon-")) return undefined;
|
||||
identified = true;
|
||||
let storedAnonymousId: string | undefined;
|
||||
try {
|
||||
storedAnonymousId = JSON.parse(fs.readFileSync(identityPath(), "utf-8")).anonymousId;
|
||||
fs.unlinkSync(identityPath());
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
cachedAnonymousId = undefined;
|
||||
if (!storedAnonymousId) return undefined;
|
||||
return {
|
||||
event: "$identify",
|
||||
distinct_id: distinctId,
|
||||
properties: { $anon_distinct_id: storedAnonymousId, $lib: "posthog-node" },
|
||||
};
|
||||
}
|
||||
|
||||
export function flushEvents(): void {
|
||||
if (queue.length === 0) return;
|
||||
const batch = queue;
|
||||
queue = [];
|
||||
fetch(POSTHOG_BATCH_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ api_key: POSTHOG_API_KEY, batch }),
|
||||
signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
|
||||
}).catch(() => {
|
||||
/* telemetry never surfaces its own failures */
|
||||
});
|
||||
}
|
||||
|
||||
function scheduleFlush(): void {
|
||||
if (!flushTimer) {
|
||||
flushTimer = setInterval(flushEvents, FLUSH_INTERVAL_MS);
|
||||
flushTimer.unref?.();
|
||||
}
|
||||
if (!exitHandlerInstalled) {
|
||||
exitHandlerInstalled = true;
|
||||
process.on("beforeExit", flushEvents);
|
||||
}
|
||||
}
|
||||
|
||||
export function errorKind(error: unknown): string {
|
||||
const text = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
||||
if (text.includes("timeout") || text.includes("aborted")) return "timeout";
|
||||
if (text.includes("401") || text.includes("403") || text.includes("unauthor")) return "auth";
|
||||
if (text.includes("429") || text.includes("rate limit")) return "rate-limited";
|
||||
if (/50[0234]/.test(text)) return "server-error";
|
||||
if (text.includes("400") || text.includes("422")) return "bad-request";
|
||||
if (text.includes("fetch failed") || text.includes("enotfound")) return "network";
|
||||
return error instanceof Error ? error.constructor.name : "other";
|
||||
}
|
||||
|
||||
export function captureEvent(
|
||||
event: string,
|
||||
properties: Record<string, unknown>,
|
||||
client: TelemetryIdentity,
|
||||
): void {
|
||||
if (!isTelemetryEnabled()) return;
|
||||
try {
|
||||
const distinctId = client.telemetryId || anonymousId();
|
||||
const identify = identifyEvent(distinctId);
|
||||
if (identify) queue.push(identify);
|
||||
queue.push({
|
||||
event,
|
||||
distinct_id: distinctId,
|
||||
properties: {
|
||||
source: "DEEPSEEK_HARNESS",
|
||||
language: "node",
|
||||
plugin_version: PLUGIN_VERSION,
|
||||
node_version: process.version,
|
||||
os: process.platform,
|
||||
$process_person_profile: false,
|
||||
$lib: "posthog-node",
|
||||
...properties,
|
||||
},
|
||||
});
|
||||
scheduleFlush();
|
||||
if (queue.length >= FLUSH_THRESHOLD) flushEvents();
|
||||
} catch {
|
||||
/* telemetry never breaks a tool call */
|
||||
}
|
||||
}
|
||||
|
||||
export function _queueForTesting(): Record<string, unknown>[] {
|
||||
return queue;
|
||||
}
|
||||
|
||||
export function _resetForTesting(): void {
|
||||
queue = [];
|
||||
clearInterval(flushTimer);
|
||||
flushTimer = undefined;
|
||||
cachedAnonymousId = undefined;
|
||||
identified = false;
|
||||
}
|
||||
@@ -35,9 +35,12 @@ function applyAndCollect(config: Config): Map<string, RegisteredTool> {
|
||||
}
|
||||
|
||||
let savedKey: string | undefined;
|
||||
let savedTelemetry: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
savedKey = process.env.MEM0_API_KEY;
|
||||
savedTelemetry = process.env.MEM0_TELEMETRY;
|
||||
process.env.MEM0_TELEMETRY = "false";
|
||||
mockSearch.mockReset();
|
||||
mockAdd.mockReset();
|
||||
});
|
||||
@@ -45,6 +48,8 @@ beforeEach(() => {
|
||||
afterEach(() => {
|
||||
if (savedKey === undefined) delete process.env.MEM0_API_KEY;
|
||||
else process.env.MEM0_API_KEY = savedKey;
|
||||
if (savedTelemetry === undefined) delete process.env.MEM0_TELEMETRY;
|
||||
else process.env.MEM0_TELEMETRY = savedTelemetry;
|
||||
});
|
||||
|
||||
describe("apply() config validation", () => {
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
|
||||
const mockSearch = vi.fn();
|
||||
const mockAdd = vi.fn();
|
||||
vi.mock("mem0ai", () => ({
|
||||
MemoryClient: class {
|
||||
telemetryId = "dev@example.com";
|
||||
search = mockSearch;
|
||||
add = mockAdd;
|
||||
},
|
||||
}));
|
||||
vi.mock("@deepseek-ai/dsh-tools", () => ({ defineTool: (options: unknown) => options }));
|
||||
|
||||
import { apply, type Config } from "../src/index.ts";
|
||||
import {
|
||||
captureEvent,
|
||||
errorKind,
|
||||
flushEvents,
|
||||
isTelemetryEnabled,
|
||||
_queueForTesting,
|
||||
_resetForTesting,
|
||||
} from "../src/telemetry.ts";
|
||||
|
||||
interface RegisteredTool {
|
||||
name: string;
|
||||
execute(args: unknown, exec: unknown): Promise<unknown>;
|
||||
}
|
||||
|
||||
function applyAndCollect(config: Config): Map<string, RegisteredTool> {
|
||||
const tools = new Map<string, RegisteredTool>();
|
||||
apply({ tools: { register: (t: RegisteredTool) => tools.set(t.name, t) } } as never, config);
|
||||
return tools;
|
||||
}
|
||||
|
||||
function queued(): Record<string, any>[] {
|
||||
return _queueForTesting() as Record<string, any>[];
|
||||
}
|
||||
|
||||
let home: string;
|
||||
let savedHome: string | undefined;
|
||||
let savedTelemetry: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
savedHome = process.env.HOME;
|
||||
savedTelemetry = process.env.MEM0_TELEMETRY;
|
||||
delete process.env.MEM0_TELEMETRY;
|
||||
home = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-deepseek-"));
|
||||
process.env.HOME = home;
|
||||
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true }));
|
||||
mockSearch.mockReset();
|
||||
mockAdd.mockReset();
|
||||
_resetForTesting();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
_resetForTesting();
|
||||
vi.unstubAllGlobals();
|
||||
fs.rmSync(home, { recursive: true, force: true });
|
||||
if (savedHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = savedHome;
|
||||
if (savedTelemetry === undefined) delete process.env.MEM0_TELEMETRY;
|
||||
else process.env.MEM0_TELEMETRY = savedTelemetry;
|
||||
});
|
||||
|
||||
describe("opt-out", () => {
|
||||
it("queues nothing for every documented off value", () => {
|
||||
for (const value of ["false", "0", "no", "OFF"]) {
|
||||
process.env.MEM0_TELEMETRY = value;
|
||||
expect(isTelemetryEnabled()).toBe(false);
|
||||
captureEvent("deepseek.tool.search_memory", {}, { telemetryId: "dev@example.com" });
|
||||
}
|
||||
expect(queued()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("is on by default", () => {
|
||||
expect(isTelemetryEnabled()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("identity", () => {
|
||||
it("keys events on the account email the SDK resolved", () => {
|
||||
captureEvent("deepseek.tool.add_memory", {}, { telemetryId: "dev@example.com" });
|
||||
expect(queued()[0].distinct_id).toBe("dev@example.com");
|
||||
});
|
||||
|
||||
it("falls back to a persisted anonymous id before the SDK has pinged", () => {
|
||||
captureEvent("deepseek.tool.add_memory", {}, {});
|
||||
const first = queued()[0].distinct_id as string;
|
||||
expect(first).toMatch(/^deepseek-anon-/);
|
||||
|
||||
_resetForTesting();
|
||||
captureEvent("deepseek.tool.add_memory", {}, {});
|
||||
expect(queued()[0].distinct_id).toBe(first);
|
||||
});
|
||||
|
||||
it("aliases the anonymous history onto the email exactly once", () => {
|
||||
captureEvent("deepseek.tool.add_memory", {}, {});
|
||||
const anonymous = queued()[0].distinct_id;
|
||||
_resetForTesting();
|
||||
|
||||
captureEvent("deepseek.tool.add_memory", {}, { telemetryId: "dev@example.com" });
|
||||
const [identify, event] = queued();
|
||||
expect(identify.event).toBe("$identify");
|
||||
expect(identify.distinct_id).toBe("dev@example.com");
|
||||
expect(identify.properties.$anon_distinct_id).toBe(anonymous);
|
||||
expect(event.distinct_id).toBe("dev@example.com");
|
||||
|
||||
captureEvent("deepseek.tool.add_memory", {}, { telemetryId: "dev@example.com" });
|
||||
expect(queued().filter((e) => e.event === "$identify")).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("event shape", () => {
|
||||
it("stamps every event with the integration source and runtime baseline", () => {
|
||||
captureEvent("deepseek.tool.search_memory", { success: true }, { telemetryId: "d@e.com" });
|
||||
const { properties } = queued()[0];
|
||||
expect(properties.source).toBe("DEEPSEEK_HARNESS");
|
||||
expect(properties.language).toBe("node");
|
||||
expect(properties.$process_person_profile).toBe(false);
|
||||
expect(properties.os).toBe(process.platform);
|
||||
expect(properties.plugin_version).not.toBe("unknown");
|
||||
expect(properties.success).toBe(true);
|
||||
});
|
||||
|
||||
it("flushes one batch to PostHog and empties the queue", async () => {
|
||||
captureEvent("deepseek.tool.search_memory", {}, { telemetryId: "d@e.com" });
|
||||
flushEvents();
|
||||
|
||||
const [url, init] = (fetch as unknown as ReturnType<typeof vi.fn>).mock.calls[0];
|
||||
expect(url).toContain("posthog.com");
|
||||
expect(JSON.parse(init.body).batch).toHaveLength(1);
|
||||
expect(queued()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("never throws when the network is gone", () => {
|
||||
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("fetch failed")));
|
||||
captureEvent("deepseek.tool.search_memory", {}, { telemetryId: "d@e.com" });
|
||||
expect(() => flushEvents()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("errorKind", () => {
|
||||
it("buckets failures without leaking the message", () => {
|
||||
expect(errorKind(new Error("Request failed with status 429"))).toBe("rate-limited");
|
||||
expect(errorKind(new Error("401 Unauthorized"))).toBe("auth");
|
||||
expect(errorKind(new Error("503 Service Unavailable"))).toBe("server-error");
|
||||
expect(errorKind(new Error("The operation was aborted due to timeout"))).toBe("timeout");
|
||||
expect(errorKind(new Error("fetch failed"))).toBe("network");
|
||||
expect(errorKind(new Error("token sk-abcdef is invalid"))).toBe("Error");
|
||||
});
|
||||
});
|
||||
|
||||
describe("tool instrumentation", () => {
|
||||
it("records a mount, then a successful search with its shape but not its query", async () => {
|
||||
mockSearch.mockResolvedValue({ results: [{ id: "m1", memory: "Likes tea" }] });
|
||||
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
|
||||
|
||||
await tools.get("search_memory")!.execute({ query: "what drink", limit: 3 }, {});
|
||||
|
||||
const [mounted, search] = queued();
|
||||
expect(mounted.event).toBe("deepseek.plugin.mounted");
|
||||
expect(mounted.properties.has_host).toBe(false);
|
||||
expect(search.event).toBe("deepseek.tool.search_memory");
|
||||
expect(search.properties).toMatchObject({
|
||||
success: true,
|
||||
top_k: 3,
|
||||
result_count: 1,
|
||||
query_chars: 10,
|
||||
scope_overridden: false,
|
||||
has_agent_id: false,
|
||||
has_run_id: false,
|
||||
});
|
||||
expect(typeof search.properties.duration_ms).toBe("number");
|
||||
expect(JSON.stringify(search)).not.toContain("what drink");
|
||||
expect(JSON.stringify(search)).not.toContain("Likes tea");
|
||||
});
|
||||
|
||||
it("records a failed search with a coarse error kind", async () => {
|
||||
mockSearch.mockRejectedValue(new Error("429 rate limit exceeded"));
|
||||
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
|
||||
|
||||
await tools.get("search_memory")!.execute({ query: "x" }, {});
|
||||
|
||||
const search = queued().at(-1)!;
|
||||
expect(search.properties).toMatchObject({ success: false, error_kind: "rate-limited" });
|
||||
});
|
||||
|
||||
it("records a write with its size but not its text", async () => {
|
||||
mockAdd.mockResolvedValue([{ id: "m1", memory: "Fact" }]);
|
||||
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
|
||||
|
||||
await tools.get("add_memory")!.execute({ text: "secret fact", userId: "alice" }, {});
|
||||
|
||||
const add = queued().at(-1)!;
|
||||
expect(add.event).toBe("deepseek.tool.add_memory");
|
||||
expect(add.properties).toMatchObject({
|
||||
success: true,
|
||||
text_chars: 11,
|
||||
memory_count: 1,
|
||||
scope_overridden: true,
|
||||
});
|
||||
expect(JSON.stringify(add)).not.toContain("secret fact");
|
||||
expect(JSON.stringify(add)).not.toContain("alice");
|
||||
});
|
||||
|
||||
it("records a failed write and still returns the graceful failure line", async () => {
|
||||
mockAdd.mockRejectedValue(new Error("boom"));
|
||||
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
|
||||
|
||||
const out = await tools.get("add_memory")!.execute({ text: "x" }, {});
|
||||
|
||||
expect(out).toContain("add_memory failed");
|
||||
expect(queued().at(-1)!.properties).toMatchObject({ success: false, error_kind: "Error" });
|
||||
});
|
||||
});
|
||||
@@ -77,6 +77,13 @@ For the model-called tool (`store` / `retrieve` / `get` / `delete`), use the
|
||||
[`mem0_memory`](https://github.com/strands-agents/tools) tool from `strands-agents-tools`. The store and
|
||||
the tool share one Mem0 backend and namespace.
|
||||
|
||||
## Telemetry
|
||||
|
||||
The store sends anonymous usage events (store configuration, operation, duration,
|
||||
result counts, coarse failure kind) over the Mem0 SDK's existing telemetry client,
|
||||
tagged `source="STRANDS"`. Queries, memory text, message content, entity ids, and
|
||||
metadata are never sent. Turn it off with `MEM0_TELEMETRY=false`.
|
||||
|
||||
## Development
|
||||
|
||||
The package lives under [`python/`](python/) (monorepo-style layout matching the
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "mem0-strands"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
description = "Persistent long-term memory for Strands agents, backed by Mem0."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -36,11 +36,13 @@ environment variable, or pass a Mem0 OSS ``config`` dict for a self-hosted backe
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from strands.memory import AddMessagesContext, MemoryEntry, MemoryStore, SearchOptions
|
||||
from strands.types.content import Message
|
||||
|
||||
from mem0_strands import telemetry
|
||||
from mem0_strands.client import Mem0ServiceClient
|
||||
|
||||
DEFAULT_MAX_SEARCH_RESULTS = 5
|
||||
@@ -134,6 +136,7 @@ class Mem0MemoryStore(MemoryStore):
|
||||
self._host = host
|
||||
self._config = config
|
||||
self._client = client
|
||||
self._recorded_init = False
|
||||
|
||||
@property
|
||||
def client(self) -> Mem0ServiceClient:
|
||||
@@ -144,8 +147,22 @@ class Mem0MemoryStore(MemoryStore):
|
||||
stores), so first use is deferred and always happens inside a worker thread
|
||||
via :func:`asyncio.to_thread`, never on the event loop.
|
||||
"""
|
||||
client_injected = self._client is not None
|
||||
if self._client is None:
|
||||
self._client = Mem0ServiceClient(api_key=self._api_key, host=self._host, config=self._config)
|
||||
if not self._recorded_init:
|
||||
self._recorded_init = True
|
||||
telemetry.record(
|
||||
"store.init",
|
||||
self._client,
|
||||
scopes=sorted(self.scope),
|
||||
writable=self.writable,
|
||||
extraction_enabled=bool(self.extraction),
|
||||
has_default_metadata=bool(self.metadata),
|
||||
max_search_results=self.max_search_results,
|
||||
host_overridden=bool(self._host),
|
||||
client_injected=client_injected,
|
||||
)
|
||||
return self._client
|
||||
|
||||
async def search(self, query: str, options: SearchOptions | None = None) -> list[MemoryEntry]:
|
||||
@@ -158,7 +175,13 @@ class Mem0MemoryStore(MemoryStore):
|
||||
|
||||
# ``self.client`` is resolved inside the thread so lazy construction (a
|
||||
# blocking call) does not run on the event loop.
|
||||
memories = await asyncio.to_thread(lambda: self.client.search_memories(query, self.scope, top_k))
|
||||
started = time.perf_counter()
|
||||
try:
|
||||
memories = await asyncio.to_thread(lambda: self.client.search_memories(query, self.scope, top_k))
|
||||
except Exception as exc:
|
||||
self._record("store.search", started, False, top_k=top_k, error_kind=telemetry.error_kind(exc))
|
||||
raise
|
||||
self._record("store.search", started, True, top_k=top_k, result_count=len(memories))
|
||||
return [self._to_entry(memory) for memory in memories]
|
||||
|
||||
async def add(self, content: str, metadata: dict[str, Any] | None = None) -> Any:
|
||||
@@ -168,7 +191,14 @@ class Mem0MemoryStore(MemoryStore):
|
||||
Mem0 de-duplicates on the server.
|
||||
"""
|
||||
merged = self._merge_metadata(metadata)
|
||||
return await asyncio.to_thread(lambda: self.client.store_memory(content, self.scope, merged))
|
||||
started = time.perf_counter()
|
||||
try:
|
||||
result = await asyncio.to_thread(lambda: self.client.store_memory(content, self.scope, merged))
|
||||
except Exception as exc:
|
||||
self._record("store.add", started, False, error_kind=telemetry.error_kind(exc))
|
||||
raise
|
||||
self._record("store.add", started, True, content_chars=len(content), has_metadata=bool(merged))
|
||||
return result
|
||||
|
||||
async def add_messages(self, messages: list[Message], context: AddMessagesContext | None = None) -> Any:
|
||||
"""Ingest raw conversation turns for Mem0 server-side extraction (``infer=True``).
|
||||
@@ -186,7 +216,33 @@ class Mem0MemoryStore(MemoryStore):
|
||||
payload.append({"role": message["role"], "content": text})
|
||||
if not payload:
|
||||
return None
|
||||
return await asyncio.to_thread(lambda: self.client.store_messages(payload, self.scope))
|
||||
started = time.perf_counter()
|
||||
try:
|
||||
result = await asyncio.to_thread(lambda: self.client.store_messages(payload, self.scope))
|
||||
except Exception as exc:
|
||||
self._record("store.add_messages", started, False, error_kind=telemetry.error_kind(exc))
|
||||
raise
|
||||
self._record(
|
||||
"store.add_messages",
|
||||
started,
|
||||
True,
|
||||
message_count=len(messages),
|
||||
rendered_count=len(payload),
|
||||
total_chars=sum(len(turn["content"]) for turn in payload),
|
||||
)
|
||||
return result
|
||||
|
||||
def _record(self, event: str, started: float, success: bool, **properties: Any) -> None:
|
||||
"""Send one store telemetry event, timed from ``started``."""
|
||||
if self._client is None:
|
||||
return
|
||||
telemetry.record(
|
||||
event,
|
||||
self._client,
|
||||
success=success,
|
||||
duration_ms=round((time.perf_counter() - started) * 1000, 2),
|
||||
**properties,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _render_content(content: Any) -> str:
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Anonymous usage telemetry for the Strands memory store.
|
||||
|
||||
Events ride the Mem0 SDK's own PostHog client, so they need no extra dependency
|
||||
and join the account's other Mem0 usage: on the hosted platform the distinct id
|
||||
is the account email, and on a self-hosted OSS backend it is the machine-local
|
||||
anonymous id the SDK already keeps. Unlike the SDK's OSS hot-path events these
|
||||
are never sampled, because a store call is an agent-level action, not a loop.
|
||||
|
||||
Only names, counts, durations, and coarse failure kinds are sent: never queries,
|
||||
memory text, message content, entity ids, metadata, or API keys.
|
||||
|
||||
Opt out with MEM0_TELEMETRY=false.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from mem0.memory.telemetry import client_telemetry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mem0_strands.client import Mem0ServiceClient
|
||||
|
||||
SOURCE = "STRANDS"
|
||||
|
||||
try:
|
||||
_VERSION = version("mem0-strands")
|
||||
except PackageNotFoundError: # pragma: no cover - only when running from a source tree
|
||||
_VERSION = "0.0.0+unknown"
|
||||
|
||||
|
||||
def error_kind(exc: BaseException) -> str:
|
||||
"""Coarse, content-free label for a failure, safe to send."""
|
||||
text = f"{type(exc).__name__}: {exc}".lower()
|
||||
if "timeout" in text or "timed out" in text:
|
||||
return "timeout"
|
||||
if "401" in text or "403" in text or "unauthor" in text or "forbidden" in text:
|
||||
return "auth"
|
||||
if "429" in text or "rate limit" in text:
|
||||
return "rate-limited"
|
||||
if any(code in text for code in ("500", "502", "503", "504")):
|
||||
return "server-error"
|
||||
if "400" in text or "422" in text:
|
||||
return "bad-request"
|
||||
return type(exc).__name__
|
||||
|
||||
|
||||
def _distinct_id(client: Mem0ServiceClient) -> str | None:
|
||||
"""The account email to attribute events to, or None to fall back to the SDK's anonymous id."""
|
||||
email = getattr(client.mem0, "user_email", None)
|
||||
return email if isinstance(email, str) and email else None
|
||||
|
||||
|
||||
def record(event: str, client: Mem0ServiceClient, **properties: Any) -> None:
|
||||
"""Send one strands.* usage event. Never raises."""
|
||||
try:
|
||||
client_telemetry.capture_event(
|
||||
f"strands.{event}",
|
||||
{
|
||||
"source": SOURCE,
|
||||
"language": "python",
|
||||
"strands_store_version": _VERSION,
|
||||
"backend": "platform" if client.is_platform else "oss",
|
||||
**properties,
|
||||
},
|
||||
_distinct_id(client),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Keep the test suite from sending usage telemetry to the live PostHog project.
|
||||
|
||||
Set before ``mem0`` is imported: the SDK reads ``MEM0_TELEMETRY`` once, at import.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
os.environ["MEM0_TELEMETRY"] = "false"
|
||||
@@ -0,0 +1,172 @@
|
||||
"""Tests for the store's anonymous usage telemetry."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mem0_strands import Mem0MemoryStore, telemetry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_client():
|
||||
"""A mocked Mem0ServiceClient that looks like the hosted platform."""
|
||||
client = MagicMock()
|
||||
client.is_platform = True
|
||||
client.mem0.user_email = "dev@example.com"
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def captured():
|
||||
"""Intercept the SDK PostHog client and collect (event, properties, distinct_id)."""
|
||||
with patch.object(telemetry, "client_telemetry") as posthog:
|
||||
events = []
|
||||
posthog.capture_event.side_effect = lambda event, properties, distinct_id=None: events.append(
|
||||
(event, properties, distinct_id)
|
||||
)
|
||||
yield events
|
||||
|
||||
|
||||
def make_store(mock_client, **kwargs):
|
||||
kwargs.setdefault("user_id", "alex")
|
||||
return Mem0MemoryStore(client=mock_client, **kwargs)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Event shape
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_every_event_carries_source_and_backend(mock_client, captured):
|
||||
assert make_store(mock_client).client is mock_client
|
||||
|
||||
event, properties, distinct_id = captured[0]
|
||||
assert event == "strands.store.init"
|
||||
assert properties["source"] == "STRANDS"
|
||||
assert properties["language"] == "python"
|
||||
assert properties["backend"] == "platform"
|
||||
assert properties["strands_store_version"]
|
||||
assert distinct_id == "dev@example.com"
|
||||
|
||||
|
||||
def test_oss_backend_is_labelled_and_falls_back_to_the_sdk_anonymous_id(captured):
|
||||
client = MagicMock()
|
||||
client.is_platform = False
|
||||
del client.mem0.user_email
|
||||
assert make_store(client).client is client
|
||||
|
||||
_, properties, distinct_id = captured[0]
|
||||
assert properties["backend"] == "oss"
|
||||
assert distinct_id is None
|
||||
|
||||
|
||||
def test_init_is_recorded_once_per_store(mock_client, captured):
|
||||
store = make_store(mock_client)
|
||||
assert store.client is store.client
|
||||
|
||||
assert [event for event, _, _ in captured] == ["strands.store.init"]
|
||||
|
||||
|
||||
def test_init_describes_configuration_without_scope_values(mock_client, captured):
|
||||
store = make_store(mock_client, agent_id="assistant", writable=False, extraction=True, metadata={"team": "core"})
|
||||
assert store.client is mock_client
|
||||
|
||||
_, properties, _ = captured[0]
|
||||
assert properties["scopes"] == ["agent_id", "user_id"]
|
||||
assert properties["writable"] is False
|
||||
assert properties["extraction_enabled"] is True
|
||||
assert properties["has_default_metadata"] is True
|
||||
assert "alex" not in str(properties)
|
||||
assert "assistant" not in str(properties)
|
||||
assert "core" not in str(properties)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Per-operation events
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_search_records_counts_but_never_the_query(mock_client, captured):
|
||||
mock_client.search_memories.return_value = [{"id": "m1", "memory": "Likes tea"}]
|
||||
|
||||
await make_store(mock_client).search("what does alex drink")
|
||||
|
||||
event, properties, _ = captured[-1]
|
||||
assert event == "strands.store.search"
|
||||
assert properties["success"] is True
|
||||
assert properties["result_count"] == 1
|
||||
assert properties["top_k"] == 5
|
||||
assert properties["duration_ms"] >= 0
|
||||
assert "drink" not in str(properties)
|
||||
assert "tea" not in str(properties)
|
||||
|
||||
|
||||
async def test_add_records_size_but_never_the_content(mock_client, captured):
|
||||
await make_store(mock_client).add("Alex prefers dark roast", metadata={"team": "core"})
|
||||
|
||||
event, properties, _ = captured[-1]
|
||||
assert event == "strands.store.add"
|
||||
assert properties["success"] is True
|
||||
assert properties["content_chars"] == len("Alex prefers dark roast")
|
||||
assert properties["has_metadata"] is True
|
||||
assert "roast" not in str(properties)
|
||||
|
||||
|
||||
async def test_add_messages_records_turn_counts_but_never_the_turns(mock_client, captured):
|
||||
messages = [
|
||||
{"role": "user", "content": [{"text": "hello"}]},
|
||||
{"role": "assistant", "content": [{"text": "hi there"}]},
|
||||
]
|
||||
|
||||
await make_store(mock_client).add_messages(messages)
|
||||
|
||||
event, properties, _ = captured[-1]
|
||||
assert event == "strands.store.add_messages"
|
||||
assert properties["message_count"] == 2
|
||||
assert properties["rendered_count"] == 2
|
||||
assert properties["total_chars"] == len("hello") + len("hi there")
|
||||
assert "hello" not in str(properties)
|
||||
|
||||
|
||||
async def test_empty_add_messages_records_nothing(mock_client, captured):
|
||||
assert await make_store(mock_client).add_messages([]) is None
|
||||
assert [event for event, _, _ in captured] == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Failures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_a_failed_search_records_a_coarse_error_kind_and_still_raises(mock_client, captured):
|
||||
mock_client.search_memories.side_effect = RuntimeError("HTTP 429 rate limit exceeded for key sk-secret")
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
await make_store(mock_client).search("anything")
|
||||
|
||||
event, properties, _ = captured[-1]
|
||||
assert event == "strands.store.search"
|
||||
assert properties["success"] is False
|
||||
assert properties["error_kind"] == "rate-limited"
|
||||
assert "sk-secret" not in str(properties)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("message", "expected"),
|
||||
[
|
||||
("request timed out", "timeout"),
|
||||
("401 Unauthorized", "auth"),
|
||||
("429 Too Many Requests", "rate-limited"),
|
||||
("503 Service Unavailable", "server-error"),
|
||||
("422 Unprocessable Entity", "bad-request"),
|
||||
("something else entirely", "ValueError"),
|
||||
],
|
||||
)
|
||||
def test_error_kind_buckets_failures(message, expected):
|
||||
assert telemetry.error_kind(ValueError(message)) == expected
|
||||
|
||||
|
||||
def test_a_broken_telemetry_backend_never_breaks_the_store(mock_client):
|
||||
with patch.object(telemetry, "client_telemetry") as posthog:
|
||||
posthog.capture_event.side_effect = RuntimeError("posthog is down")
|
||||
assert make_store(mock_client).client is mock_client
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [2023] [Taranjeet Singh]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,35 @@
|
||||
# minimax-mem0
|
||||
|
||||
The [Mem0](https://mem0.ai) plugin for the MiniMax Marketplace (MiniMax Code desktop and MiniMax Agent cloud).
|
||||
|
||||
It gives a MiniMax agent persistent long-term memory: it remembers the user's preferences, decisions, and project context across sessions and recalls them on demand.
|
||||
|
||||
## What's in the package
|
||||
|
||||
This is a declarative MiniMax plugin (no build step). It follows the MiniMax plugin spec:
|
||||
|
||||
Built to the [Agent Plugins 1.0](https://agent-plugins.org/specification) standard, which the MiniMax runtime imports and validates against.
|
||||
|
||||
```
|
||||
minimax-mem0/
|
||||
plugin.json # manifest (Agent Plugins 1.0, at plugin root)
|
||||
mcp.json # remote MCP: https://mcp.mem0.ai/mcp/ (auto-discovered at root)
|
||||
icon.png # square Mem0 icon
|
||||
skills/
|
||||
memory/
|
||||
SKILL.md # when to recall / save / update memory
|
||||
```
|
||||
|
||||
Two capabilities:
|
||||
|
||||
- **MCP** (`mcp.json`) points at Mem0's hosted MCP server (`https://mcp.mem0.ai/mcp/`), which exposes the memory tools (`add_memory`, `search_memories`, `update_memory`, `delete_memory`, and more). No secrets are stored in the package.
|
||||
- **Skill** (`skills/memory/SKILL.md`) tells the agent when to search, save, update, and delete memory so recall happens automatically.
|
||||
|
||||
## Authentication
|
||||
|
||||
Mem0's MCP server requires authentication. Per the MiniMax plugin spec, no credentials are placed in this package; the MiniMax client authorizes the user's Mem0 account through the MCP OAuth flow on first use. Users get a Mem0 account from the [Mem0 dashboard](https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=integration-minimax).
|
||||
|
||||
## Links
|
||||
|
||||
- Mem0: https://mem0.ai
|
||||
- Mem0 MCP docs: https://docs.mem0.ai/platform/mem0-mcp
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"type": "streamable-http",
|
||||
"url": "https://mcp.mem0.ai/mcp/"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
||||
"name": "mem0",
|
||||
"version": "0.1.0",
|
||||
"description": "Persistent long-term memory for your agent: remember preferences, decisions, and project context across sessions, and recall them on demand.",
|
||||
"author": { "name": "Mem0", "email": "support@mem0.ai", "url": "https://mem0.ai" },
|
||||
"homepage": "https://mem0.ai",
|
||||
"repository": "https://github.com/mem0ai/mem0",
|
||||
"license": "Apache-2.0",
|
||||
"keywords": ["memory", "long-term-memory", "personalization", "mcp"]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: memory
|
||||
description: Use Mem0 to persist and recall long-term memory across sessions - the user's preferences, decisions, project context, and durable facts.
|
||||
---
|
||||
|
||||
You have access to Mem0 long-term memory through the `mem0` MCP server. Use it so the
|
||||
user does not have to repeat themselves across sessions.
|
||||
|
||||
## When to recall (search memory)
|
||||
|
||||
At the start of a task, and whenever the user references something they expect you to
|
||||
already know ("my project", "like last time", "the usual"), search memory before asking.
|
||||
|
||||
- Call the memory search tool with a short natural-language query describing what you
|
||||
need (e.g. "preferred language and package manager", "deployment process").
|
||||
- Use the returned memories as context. Do not announce that you searched; just apply
|
||||
what you found.
|
||||
- If nothing relevant comes back, proceed and ask the user only for what is missing.
|
||||
|
||||
## When to save (add memory)
|
||||
|
||||
Save durable facts as they come up, without being asked:
|
||||
|
||||
- Stated preferences ("I use pnpm", "always use TypeScript", "no comments in code").
|
||||
- Decisions and conventions ("we deploy on Fridays", "the API base is X").
|
||||
- Stable facts about the user, their team, or their project (stack, roles, goals).
|
||||
|
||||
Do not save transient or trivial details (a one-off value, a temporary path, secrets,
|
||||
or anything the user would not want remembered). Save one clear fact per memory, phrased
|
||||
so it makes sense on its own later.
|
||||
|
||||
## When to update or delete
|
||||
|
||||
If a new statement contradicts something you recalled, update that memory rather than
|
||||
adding a duplicate. If the user says to forget something, delete it.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Never store secrets, credentials, API keys, or sensitive personal data.
|
||||
- Memory is scoped to the connected Mem0 account; keep one user's memories to that user.
|
||||
- Prefer recalling silently over interrupting the user to confirm what you already know.
|
||||
@@ -1,24 +1,9 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
# TODO: Upgrade to latest pydantic version
|
||||
class RedisDBConfig(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
|
||||
|
||||
redis_url: str = Field(..., description="Redis URL")
|
||||
collection_name: str = Field("mem0", description="Collection name")
|
||||
embedding_model_dims: int = Field(1536, description="Embedding model dimensions")
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def validate_extra_fields(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
||||
allowed_fields = set(cls.model_fields.keys())
|
||||
input_fields = set(values.keys())
|
||||
extra_fields = input_fields - allowed_fields
|
||||
if extra_fields:
|
||||
raise ValueError(
|
||||
f"Extra fields not allowed: {', '.join(extra_fields)}. Please input only the following fields: {', '.join(allowed_fields)}"
|
||||
)
|
||||
return values
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
Reference in New Issue
Block a user