Compare commits

..
Author SHA1 Message Date
Saket AryanandGitHub 0ffaffa88c fix(pi-agent-plugin): correct repository.url for npm provenance validation (#5473) 2026-06-10 21:29:16 +05:30
Saket AryanandGitHub 433ff494f1 chore(pi-agent-plugin): bump version to 0.1.1 (#5471) 2026-06-10 21:16:55 +05:30
Saket AryanandGitHub de03c52ed3 ci: add CI and CD workflows for pi-agent-plugin (#5469) 2026-06-10 21:11:39 +05:30
b4a50e3dc8 feat(vercel-ai-sdk): migrate to Vercel AI SDK v6 (#4741)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-10 18:41:15 +05:30
b819d95d18 fix(pgvector): use open=False to prevent ConnectionPool hang in Docker (#5155)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-10 17:15:53 +05:30
3ac1c9452c fix(vector-stores): filter S3 vector list results (#5018)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-10 16:55:31 +05:30
Gaurav DubeyandGitHub d6347f6660 fix(vector_stores): pass namespace as top-level kwarg to UpstashVector query_many (#5202) 2026-06-10 16:41:26 +05:30
Hrushikesh YadavandGitHub e769502baa feat: warn when hybrid search silently degrades to semantic-only (#5444) 2026-06-10 16:26:48 +05:30
Aarkin KarnikandGitHub 652193d599 fix(llms/xai): forward tools, add XAIConfig, parse tool_calls (#5190) 2026-06-10 12:46:27 +05:30
Chirag AroraandGitHub a86c87236d fix(server): forward explain in REST search (#5423) 2026-06-10 12:15:31 +05:30
KartikandGitHub 2274b5acad feat: add @mem0/pi-agent-plugin for Pi Agent memory (#5459) 2026-06-10 01:04:09 +05:30
Harsh Vardhan GuptaandGitHub 9b0705c345 fix(deps): bump mem0ai 3.0.3→3.0.6 in openclaw to remediate axios CVEs (#5460) 2026-06-09 19:00:59 +05:30
Atahan YıldırımandGitHub d31fa168eb mem0-plugin hooks: honor auto_save=false in capture entry points (#5450) 2026-06-08 21:24:24 +05:30
Ritwij Aryan ParmarandGitHub f32eb4406b fix(memory): reject empty search queries (#5258) 2026-06-08 21:11:11 +05:30
youneshimaandGitHub 366945965d feat(server): add information on self-hosted dashboard (#5325) 2026-06-06 03:18:43 +05:30
youneshimaandGitHub 6702fa3e3e docs: match migration callout color styling (#5157) 2026-06-05 14:40:24 -07:00
a44855af9e feat(memory): add search score explanations (#5102)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-05 21:30:10 +05:30
KartikandGitHub d817aa9c12 fix(oss): parallelize entity boost searches in Memory.search (#5377) 2026-06-05 21:03:42 +05:30
KartikandGitHub 7ac8ab154b fix(vector_stores): normalize scores to similarity (higher = better) across all backends (#5391) 2026-06-05 19:13:26 +05:30
b00a1a1065 docs(google-adk): rewrite integration guide to use official ADK MemoryService APIs (#5392)
Co-authored-by: Nishar <nishar@dayos.com>
Co-authored-by: Nishar Miya <miyannishar786@gmail.com>
2026-06-05 19:13:00 +05:30
2e90ed4f78 docs: add Neon vector store guide (#5119)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-05 19:06:59 +05:30
069ea0887c fix(llms): add is_reasoning_model config override for versioned deployments (#5327)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-05 18:42:56 +05:30
KartikandGitHub ae7f406265 fix(server): harden self-hosted server — pgvector upgrade, admin auth, endpoint security (#5360) 2026-06-05 16:35:12 +05:30
90f2d24e83 fix(deps): upgrade vitest 1.5→4.1 + vite 6 to patch CVE-2026-47429 (#5375)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 02:54:00 +05:30
64b9646e7d fix(ts): request float OpenAI embeddings (#5170)
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
2026-06-04 20:29:53 +05:30
Ren yiweiandGitHub 866888df41 Fix pgvector sslmode handling for PostgreSQL URIs (#5308) 2026-06-04 19:32:47 +05:30
AbhinavandGitHub 95b6f95f7b fix: replace mutable default arguments with None sentinels (B006) (#5302) 2026-06-04 18:53:03 +05:30
KartikandGitHub 74771b4e76 feat(mem0-plugin): file-context injection, stop-hook summaries & activity timeline (#5346) 2026-06-03 00:55:07 +05:30
Harsh Vardhan GuptaandGitHub 8e65ce915d fix(deps): remediate high-severity vulnerabilities in npm packages (#5294) 2026-06-02 01:36:29 +05:30
183 changed files with 16592 additions and 4326 deletions
+1 -1
View File
@@ -12,7 +12,7 @@
"name": "mem0", "name": "mem0",
"source": "./mem0-plugin", "source": "./mem0-plugin",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.", "description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.",
"version": "0.2.8" "version": "0.2.9"
} }
] ]
} }
+1 -1
View File
@@ -12,7 +12,7 @@
"name": "mem0", "name": "mem0",
"source": "./mem0-plugin", "source": "./mem0-plugin",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.", "description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.",
"version": "0.2.8" "version": "0.2.9"
} }
] ]
} }
+4 -2
View File
@@ -7,7 +7,9 @@ on:
jobs: jobs:
build-n-publish: build-n-publish:
name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
if: startsWith(github.event.release.tag_name, 'v') # Pure SDK version tags only (v1.2.3) — excludes package-prefixed tags
# like vercel-ai-v* that also start with 'v'
if: startsWith(github.event.release.tag_name, 'v') && !contains(github.event.release.tag_name, '-v')
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
id-token: write id-token: write
@@ -39,7 +41,7 @@ jobs:
# packages_dir: dist/ # packages_dir: dist/
- name: Publish distribution 📦 to PyPI - name: Publish distribution 📦 to PyPI
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-v')
uses: pypa/gh-action-pypi-publish@release/v1 uses: pypa/gh-action-pypi-publish@release/v1
with: with:
packages_dir: dist/ packages_dir: dist/
+46
View File
@@ -0,0 +1,46 @@
name: Publish @mem0/pi-agent-plugin 📦 to npm
on:
release:
types: [published]
jobs:
build-n-publish:
name: Build and publish @mem0/pi-agent-plugin 📦 to npm
if: startsWith(github.event.release.tag_name, 'pi-agent-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: pi-agent-plugin
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
cache-dependency-path: pi-agent-plugin/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Publish to npm
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npx npm@latest publish --provenance --access public
fi
@@ -0,0 +1,93 @@
name: pi-agent-plugin checks
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'pi-agent-plugin/**'
- '.github/workflows/pi-agent-plugin-checks.yml'
pull_request:
paths:
- 'pi-agent-plugin/**'
- '.github/workflows/pi-agent-plugin-checks.yml'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: pi-agent-plugin/pnpm-lock.yaml
- name: Install dependencies
run: cd pi-agent-plugin && pnpm install --frozen-lockfile
- name: Type check
run: cd pi-agent-plugin && pnpm exec tsc --noEmit
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
cache-dependency-path: pi-agent-plugin/pnpm-lock.yaml
- name: Install dependencies
run: cd pi-agent-plugin && pnpm install --frozen-lockfile
- name: Run tests
run: cd pi-agent-plugin && pnpm exec vitest run
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: pi-agent-plugin/pnpm-lock.yaml
- name: Install dependencies
run: cd pi-agent-plugin && pnpm install --frozen-lockfile
- name: Build
run: cd pi-agent-plugin && pnpm build
- name: Verify dist output exists
run: |
test -f pi-agent-plugin/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
test -f pi-agent-plugin/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
test -f pi-agent-plugin/dist/entry.js || (echo "Build output missing: dist/entry.js" && exit 1)
test -f pi-agent-plugin/dist/entry.d.ts || (echo "Build output missing: dist/entry.d.ts" && exit 1)
+2
View File
@@ -414,6 +414,7 @@ To add a new LLM, embedding, vector store, or reranker provider:
| Node CLI | `cli-node-ci.yml` | Push to `cli/node/`, PRs, manual | Biome lint + tsc + vitest + tsup build on Node 20, 22 | | Node CLI | `cli-node-ci.yml` | Push to `cli/node/`, PRs, manual | Biome lint + tsc + vitest + tsup build on Node 20, 22 |
| OpenClaw | `openclaw-checks.yml` | Push to `openclaw/`, PRs, manual | tsc + vitest (with Codecov) + tsup build on Node 20, 22 | | OpenClaw | `openclaw-checks.yml` | Push to `openclaw/`, PRs, manual | tsc + vitest (with Codecov) + tsup build on Node 20, 22 |
| OpenCode Plugin | `opencode-plugin-checks.yml` | Push to `mem0-plugin/.opencode-plugin/`, PRs, manual | Bun: tsc type-check + build + dist artifact check | | OpenCode Plugin | `opencode-plugin-checks.yml` | Push to `mem0-plugin/.opencode-plugin/`, PRs, manual | Bun: tsc type-check + build + dist artifact check |
| Pi Agent Plugin | `pi-agent-plugin-checks.yml` | Push to `pi-agent-plugin/`, PRs, manual | tsc + vitest + tsup build (dist artifact check) on Node 20, 22 |
### CD Workflows (automated publishing) ### CD Workflows (automated publishing)
@@ -426,6 +427,7 @@ To add a new LLM, embedding, vector store, or reranker provider:
| Vercel AI SDK | `vercel-ai-cd.yml` | `vercel-ai-v*` | npm (`@mem0/vercel-ai-provider`) | | Vercel AI SDK | `vercel-ai-cd.yml` | `vercel-ai-v*` | npm (`@mem0/vercel-ai-provider`) |
| OpenClaw | `openclaw-cd.yml` | `openclaw-v*` | npm (`@mem0/openclaw-mem0`) | | OpenClaw | `openclaw-cd.yml` | `openclaw-v*` | npm (`@mem0/openclaw-mem0`) |
| OpenCode Plugin | `opencode-plugin-cd.yml` | `opencode-v*` | npm (`@mem0/opencode-plugin`) | | OpenCode Plugin | `opencode-plugin-cd.yml` | `opencode-v*` | npm (`@mem0/opencode-plugin`) |
| Pi Agent Plugin | `pi-agent-plugin-cd.yml` | `pi-agent-v*` | npm (`@mem0/pi-agent-plugin`) |
- All publishing uses **OIDC trusted publishing** — no tokens or secrets required. - All publishing uses **OIDC trusted publishing** — no tokens or secrets required.
- First publish of a new npm package must be done manually; OIDC works for subsequent versions. - First publish of a new npm package must be done manually; OIDC works for subsequent versions.
-159
View File
@@ -1,159 +0,0 @@
# Security Advisory: High Severity Vulnerabilities - May 30, 2026
This advisory addresses 3 HIGH severity vulnerabilities identified by Vanta security scans.
## Summary
| CVE | Package | Severity | CVSS | Status | Fixed Version |
|-----|---------|----------|------|--------|---------------|
| CVE-2026-29063 | immutable | HIGH | 9.8 | OVERDUE | 5.1.5 |
| CVE-2025-58754 | axios | HIGH | 8.2 | OVERDUE | 1.12.0 |
| CVE-2026-32460 | langsmith | HIGH | 7.1 | Due Jun 12 | 0.6.0 |
## Vulnerability Details
### 1. CVE-2026-29063: Prototype Pollution in immutable
**Location:** Transitive dependency in `openmemory/ui`
**Current Version:** 5.1.1
**Required Version:** >= 5.1.5
**Impact:**
- Prototype pollution via `mergeDeep()`, `mergeDeepWith()`, `merge()`, `Map.toJS()`, `Map.toObject()`
- Privilege escalation through `__proto__` injection
- Security check bypass
**Remediation:**
Since immutable is a transitive dependency, investigate the dependency tree and either:
1. Update the direct dependency that requires immutable
2. Use package manager overrides/resolutions:
```json
// pnpm-lock.yaml or package.json
"pnpm": {
"overrides": {
"immutable": ">=5.1.5"
}
}
```
**References:**
- Issue: #5318
- Dependabot: https://github.com/mem0ai/mem0/security/dependabot/566
- Advisory: https://github.com/immutable-js/immutable-js/security/advisories/GHSA-wf6x-7x77-mvgw
### 2. CVE-2025-58754: Axios DoS via data: URI
**Location:** Multiple locations (already fixed in most)
**Current Versions:** 1.15.2, 1.7.7 (SAFE)
**Required Version:** >= 1.12.0
**Status:** ✅ RESOLVED - All axios instances are already at safe versions
**Impact:**
- Denial of Service through unbounded memory allocation
- Process crash via malicious `data:` URIs
- Bypasses `maxContentLength` / `maxBodyLength` limits
**Verification:**
```bash
grep -r "axios" --include="package.json" | grep version
# All instances show >= 1.12.0
```
**References:**
- Issue: #5319
- Dependabot: https://github.com/mem0ai/mem0/security/dependabot/567
### 3. CVE-2026-32460: LangSmith SSRF & Credential Disclosure
**Location:** Transitive dependency in `openclaw` and `mem0-ts`
**Current Versions:** 0.3.87, 0.5.10
**Required Version:** >= 0.6.0
**Impact:**
- Server-Side Request Forgery (SSRF) via untrusted prompts
- Credential disclosure when pulling public prompts
- Prompt injection and behavior manipulation
**Remediation:**
Since langsmith is a transitive dependency (likely from @langchain packages), investigate and either:
1. Update the parent dependency (@langchain/core, etc.)
2. Use package manager overrides:
```json
// pnpm-lock.yaml or package.json
"pnpm": {
"overrides": {
"langsmith": ">=0.6.0"
}
}
```
**Post-Upgrade Security Guidance:**
After upgrading, pulling public prompts requires explicit opt-in:
- Python: `dangerously_pull_public_prompt=True`
- JS/TS: `dangerouslyPullPublicPrompt: true`
**References:**
- Issue: #5320
- Dependabot: https://github.com/mem0ai/mem0/security/dependabot/920
- Advisory: https://github.com/langchain-ai/langsmith-sdk/security/advisories/GHSA-3644-q5cj-c5c7
## Recommended Actions
### Immediate (High Priority - Overdue)
1. ✅ **CVE-2025-58754 (axios):** Already resolved - verify in production
2. 🔴 **CVE-2026-29063 (immutable):** Add override to `openmemory/ui/package.json`
3. 🔴 **CVE-2026-32460 (langsmith):** Add overrides to `openclaw/package.json` and `mem0-ts/package.json`
### Testing Checklist
- [ ] Run `pnpm install` in affected directories
- [ ] Verify lock files show updated versions
- [ ] Run existing test suites
- [ ] Check for runtime errors related to dependency changes
- [ ] Verify Dependabot alerts are resolved
### Deployment Verification
```bash
# After deployment, verify versions:
cd openmemory/ui && pnpm list immutable
cd openclaw && pnpm list langsmith
cd mem0-ts && pnpm list langsmith
```
## Implementation Plan
1. **Add Dependency Overrides**
- Update `openmemory/ui/package.json` with immutable override
- Update `openclaw/package.json` with langsmith override
- Update `mem0-ts/package.json` with langsmith override
2. **Regenerate Lock Files**
```bash
cd openmemory/ui && pnpm install
cd openclaw && pnpm install
cd mem0-ts && pnpm install
```
3. **Test & Verify**
- Run test suites for each affected package
- Verify no breaking changes
- Check Vanta dashboard for resolution
4. **Deploy & Monitor**
- Deploy to staging first
- Monitor for any runtime issues
- Deploy to production
- Confirm Dependabot alerts close
## Contact
For questions or concerns about this advisory:
- Security Issues: #5318, #5319, #5320
- Vanta Dashboard: Check GitHub Dependabot integration
- Automated by: Vulnerability Remediation Workflow
---
*Generated: 2026-05-30*
*Workflow: Automated Vulnerability Remediation*
+15
View File
@@ -5,6 +5,21 @@ All notable changes to `@mem0/cli` are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.2.8] — 2026-06-01
### Security
- Pinned transitive dependencies via pnpm overrides to remediate high-severity CVEs:
- `jws` → 4.0.1 (CVE-2025-65945)
- `langsmith` → ^0.6.0 (CVE-2026-45134)
- `tar-fs` → ^2.1.4 (CVE-2025-48387, CVE-2025-59343)
- `picomatch` → ^2.3.2 (CVE-2026-33671)
- `minimatch` → ^3.1.3 / ^5.1.8 / ^9.0.7 (CVE-2026-27903, CVE-2026-27904, CVE-2026-26996)
- `path-to-regexp` → ^8.4.0 (CVE-2026-4926)
- `rollup` → ^4.59.0 (CVE-2026-27606)
- `glob` → ^10.5.0 (CVE-2025-64756)
- `@modelcontextprotocol/sdk` → ^1.25.4 (CVE-2025-66414, CVE-2026-0621)
## [0.2.7] — 2026-05-20 ## [0.2.7] — 2026-05-20
### Added ### Added
+3 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@mem0/cli", "name": "@mem0/cli",
"version": "0.2.7", "version": "0.2.8",
"description": "The official CLI for mem0 — the memory layer for AI agents", "description": "The official CLI for mem0 — the memory layer for AI agents",
"type": "module", "type": "module",
"bin": { "bin": {
@@ -40,7 +40,8 @@
"typescript": "^5.4.0", "typescript": "^5.4.0",
"tsup": "^8.0.0", "tsup": "^8.0.0",
"tsx": "^4.7.0", "tsx": "^4.7.0",
"vitest": "^1.5.0", "vite": "^6.0.0",
"vitest": "^4.1.0",
"@biomejs/biome": "^1.7.0", "@biomejs/biome": "^1.7.0",
"@types/node": "^20.0.0" "@types/node": "^20.0.0"
} }
+347 -477
View File
File diff suppressed because it is too large Load Diff
+12
View File
@@ -0,0 +1,12 @@
packages:
- '.'
onlyBuiltDependencies:
- "@biomejs/biome"
- esbuild
overrides:
jws@4.0.0: 4.0.1
langsmith@<0.6.0: ^0.6.0
tar-fs@>=2.0.0 <2.1.4: ^2.1.4
picomatch@<2.3.2: ^2.3.2
+51
View File
@@ -939,6 +939,13 @@ See the [OSS v1 to v2 migration guide](https://docs.mem0.ai/migration/oss-v1-to-
</Tab> </Tab>
<Tab title="TypeScript"> <Tab title="TypeScript">
<Update label="2026-06-01" description="v3.0.6">
**Security:**
- **Dependencies:** Bumped `axios` to `^1.16.0` to remediate high-severity prototype-pollution CVEs (credential theft, MITM, DoS). Pinned transitive dependencies via pnpm overrides: `jws` → 4.0.1 (CVE-2025-65945), `langsmith` → ^0.6.0 (CVE-2026-45134), `tar-fs` → ^2.1.4 (CVE-2025-48387, CVE-2025-59343), `picomatch` → ^2.3.2 (CVE-2026-33671), `minimatch` → ^3.1.3 / ^5.1.8 / ^9.0.7 (CVE-2026-27903, CVE-2026-27904, CVE-2026-26996), `path-to-regexp` → ^8.4.0 (CVE-2026-4926), `rollup` → ^4.59.0 (CVE-2026-27606), `glob` → ^10.5.0 (CVE-2025-64756), `@modelcontextprotocol/sdk` → ^1.25.4 (CVE-2025-66414, CVE-2026-0621)
</Update>
<Update label="2026-05-27" description="v3.0.5"> <Update label="2026-05-27" description="v3.0.5">
**New Features:** **New Features:**
@@ -1352,6 +1359,13 @@ See the [TypeScript SDK migration guide](https://docs.mem0.ai/migration/ts-v2-to
<Tab title="CLI"> <Tab title="CLI">
<Update label="2026-06-01" description="Node v0.2.8">
**Security:**
- **Dependencies:** Pinned transitive dependencies via pnpm overrides to remediate high-severity CVEs: `jws` → 4.0.1 (CVE-2025-65945), `langsmith` → ^0.6.0 (CVE-2026-45134), `tar-fs` → ^2.1.4 (CVE-2025-48387, CVE-2025-59343), `picomatch` → ^2.3.2 (CVE-2026-33671), `minimatch` → ^3.1.3 / ^5.1.8 / ^9.0.7 (CVE-2026-27903, CVE-2026-27904, CVE-2026-26996), `path-to-regexp` → ^8.4.0 (CVE-2026-4926), `rollup` → ^4.59.0 (CVE-2026-27606), `glob` → ^10.5.0 (CVE-2025-64756), `@modelcontextprotocol/sdk` → ^1.25.4 (CVE-2025-66414, CVE-2026-0621)
</Update>
<Update label="2026-05-16" description="Python v0.2.6 / Node v0.2.6"> <Update label="2026-05-16" description="Python v0.2.6 / Node v0.2.6">
**Bug Fixes:** **Bug Fixes:**
@@ -1468,6 +1482,43 @@ A full-featured command-line interface for Mem0, available in both Python and No
<Tab title="Plugins"> <Tab title="Plugins">
<Update label="2026-06-01" description="openclaw-mem0 v1.0.12">
**Security:**
- **Dependencies:** Pinned transitive dependencies via pnpm overrides to remediate high-severity CVEs: `protobufjs` → ^7.5.5, `vite` → ^8.0.5, `langsmith` → ^0.6.0 (CVE-2026-45134), `picomatch` → ^2.3.2 (CVE-2026-33671), `@qdrant/js-client-rest` → ^1.18.0
</Update>
<Update label="2026-06-10" description="Vercel AI SDK v3.0.0">
**Major Release** — Migrated to Vercel AI SDK v6 (`LanguageModelV3` / `ProviderV3`) and Mem0 v3 API.
**Breaking Changes:**
- **AI SDK v6:** Upgraded from AI SDK v5 (`LanguageModelV2`) to v6 (`LanguageModelV3`). Users must upgrade `ai` to `^6.0.199` and all `@ai-sdk/*` provider packages to `^3.x` ([#4741](https://github.com/mem0ai/mem0/pull/4741))
- **Mem0 v3 API:** Memory endpoints migrated from `/v1/memories/` and `/v2/memories/search/` to `/v3/memories/add/` and `/v3/memories/search/`. Entity IDs (`user_id`, `agent_id`, `run_id`) now go inside the `filters` object for search requests ([#4741](https://github.com/mem0ai/mem0/pull/4741))
- **Graph memory removed:** All `enable_graph`, graph prompts, and relation-extraction code removed. Graph memory is now a project-level setting on the Platform ([#4741](https://github.com/mem0ai/mem0/pull/4741))
- **Deprecated params removed:** `org_id`, `project_id`, `org_name`, `project_name`, `output_format`, `filter_memories`, `async_mode`, `enable_graph`, `version`, `api_version` removed from `Mem0ConfigSettings` ([#4741](https://github.com/mem0ai/mem0/pull/4741))
**New Features:**
- **V3 provider contract:** `specificationVersion: 'v3'`, `supportedUrls` property, V3 content array in `doGenerate`, V3 stream lifecycle events in `doStream` ([#4741](https://github.com/mem0ai/mem0/pull/4741))
- **Mem0 source in responses:** Memories are attached as a `source` in `generateText`/`streamText` responses with `providerMetadata.mem0.memories` for programmatic access ([#4741](https://github.com/mem0ai/mem0/pull/4741))
**Bug Fixes:**
- **Async memory storage:** `addMemories` is now properly `await`ed — memories no longer silently fail to store ([#4741](https://github.com/mem0ai/mem0/pull/4741))
- **Prompt mutation:** Prompt array is now cloned before injecting memory context, preventing side effects on the caller's array ([#4741](https://github.com/mem0ai/mem0/pull/4741))
- **Null guard on content:** `doGenerate` guards against null `content` from upstream providers ([#4741](https://github.com/mem0ai/mem0/pull/4741))
- **Stream response:** `doStream` now returns the full `LanguageModelV3StreamResult` object preserving all V3 fields ([#4741](https://github.com/mem0ai/mem0/pull/4741))
- **Response normalization:** `getMemories` and `retrieveMemories` now handle both array and `{results: [...]}` envelope responses from the v3 API ([#4741](https://github.com/mem0ai/mem0/pull/4741))
</Update>
<Update label="2026-06-01" description="Vercel AI SDK v2.0.6">
**Security:**
- **Dependencies:** Pinned transitive dependencies via pnpm overrides to remediate high-severity CVEs: `glob` → ^10.5.0 (CVE-2025-64756), `minimatch` → ^3.1.3 / ^5.1.8 / ^9.0.7 (CVE-2026-27903, CVE-2026-27904, CVE-2026-26996), `picomatch` → ^2.3.2 (CVE-2026-33671), `rollup` → ^4.59.0 (CVE-2026-27606)
</Update>
<Update label="2026-04-02" description="mem0-plugin v1.0.0"> <Update label="2026-04-02" description="mem0-plugin v1.0.0">
**Mem0 Plugin for Claude Code, Cursor, and Codex** **Mem0 Plugin for Claude Code, Cursor, and Codex**
+156
View File
@@ -0,0 +1,156 @@
---
title: "Neon"
description: "Use Neon as a vector store in Mem0, powered by PostgreSQL and pgvector."
---
Use [Neon](https://neon.com/) as a vector store in Mem0, powered by PostgreSQL and the
[pgvector extension](https://neon.com/docs/extensions/pgvector).
Neon is a serverless Postgres platform. Since Mem0 supports Postgres through the
`pgvector` provider, Neon can be used with a standard Postgres connection string.
## Usage
<CodeGroup>
```python Python
import os
from dotenv import load_dotenv
from mem0 import Memory
load_dotenv()
config = {
"vector_store": {
"provider": "pgvector",
"config": {
"connection_string": os.environ["DATABASE_URL"],
"collection_name": "memories",
"embedding_model_dims": 1536,
"hnsw": True,
},
},
}
m = Memory.from_config(config)
messages = [
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
{"role": "assistant", "content": "How about thriller movies? They can be quite engaging."},
{"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."},
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."},
]
m.add(messages, user_id="alice", metadata={"category": "movies"})
results = m.search(
"What movies should I recommend?",
filters={"user_id": "alice"},
)
print(results)
```
```typescript TypeScript
import "dotenv/config";
import { Memory } from "mem0ai/oss";
const databaseUrl = new URL(process.env.DATABASE_URL!);
const m = new Memory({
vectorStore: {
provider: "pgvector",
config: {
user: decodeURIComponent(databaseUrl.username),
password: decodeURIComponent(databaseUrl.password),
host: databaseUrl.hostname,
port: Number(databaseUrl.port || 5432),
dbname: databaseUrl.pathname.slice(1) || "neondb",
collectionName: "memories",
dimension: 1536,
embeddingModelDims: 1536,
hnsw: true,
},
},
});
const messages = [
{ role: "user" as const, content: "I'm planning to watch a movie tonight. Any recommendations?" },
{ role: "assistant" as const, content: "How about thriller movies? They can be quite engaging." },
{ role: "user" as const, content: "I'm not a big fan of thriller movies but I love sci-fi movies." },
{ role: "assistant" as const, content: "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future." },
];
await m.add(messages, {
userId: "alice",
metadata: { category: "movies" },
});
const results = await m.search("What movies should I recommend?", {
filters: { user_id: "alice" },
});
console.log(results);
```
</CodeGroup>
## SQL Migration
You don't need to run any SQL migrations. Mem0 creates the collection table when it initializes the `pgvector` store.
## Environment
```env
OPENAI_API_KEY=sk-xx...
DATABASE_URL=postgresql://user:password@ep-example.us-east-2.aws.neon.tech/neondb?sslmode=require
```
## Config
<Tabs>
<Tab title="Python">
| Parameter | Description | Default Value |
| --- | --- | --- |
| `connection_string` | Neon Postgres connection string. | Required |
| `collection_name` | Name for the vector collection. | `mem0` |
| `embedding_model_dims` | Embedding model dimensions. | `1536` |
| `hnsw` | Enables HNSW indexing. | `False` |
| `sslmode` | PostgreSQL SSL mode. Use `require` for Neon. | Driver default |
</Tab>
<Tab title="TypeScript">
The current Mem0 TypeScript `pgvector` adapter takes individual Postgres fields,
so parse `DATABASE_URL` before creating `Memory`.
| Parameter | Description | Default |
| --- | --- | --- |
| `user` | Database user. | Required |
| `password` | Database password. | Required |
| `host` | Database host. | Required |
| `port` | Database port. | `5432` |
| `dbname` | Database name. | `vector_store` |
| `collectionName` | Name for the vector collection. | `memories` |
| `dimension` | Vector dimension for Mem0 config. | Auto-detected |
| `embeddingModelDims` | Embedding model dimensions for table creation. | Required |
| `hnsw` | Enables HNSW indexing. | `false` |
</Tab>
</Tabs>
### Indexing
The `pgvector` provider can create an HNSW index for faster vector search.
- Set `hnsw` to `true` to enable a Hierarchical Navigable Small World index.
- Leave `hnsw` as `false` if you want to create or manage indexes yourself.
### Similarity Search
The `pgvector` provider uses cosine similarity for vector search. Make sure your
embedding dimensions match the configured `embedding_model_dims` value.
### Best Practices
1. **Index Selection**:
- Use `hnsw` for faster search performance when memory usage is not a constraint
- Manage indexes manually if you need a different pgvector index strategy
2. **Connection String**:
- Always use environment variables or even better, a secret manager for sensitive information in the connection string
- Format: `postgresql://user:password@host:port/database`
@@ -156,6 +156,33 @@ const memories = memory.search("food preferences", {
On Mem0 Platform v3, time-aware queries use Temporal Reasoning internally while preserving the normal search response shape. See <Link href="/platform/features/temporal-reasoning">Temporal Reasoning</Link>. On Mem0 Platform v3, time-aware queries use Temporal Reasoning internally while preserving the normal search response shape. See <Link href="/platform/features/temporal-reasoning">Temporal Reasoning</Link>.
</Note> </Note>
### Explain OSS search scores
OSS search combines semantic similarity with optional keyword and entity signals. Pass `explain=True` when tuning retrieval quality or debugging why a memory ranked where it did:
<CodeGroup>
```python Python
results = m.search(
"food preferences",
filters={"user_id": "alice"},
explain=True,
)
print(results["results"][0]["score_details"])
```
```javascript JavaScript
const results = await memory.search("food preferences", {
filters: { user_id: "alice" },
explain: true,
});
console.log(results.results[0].score_details);
```
</CodeGroup>
Each result includes `score_details` with the semantic score, normalized BM25 score, entity boost, raw combined score, maximum possible score, final score, and threshold used for filtering. The field is omitted unless `explain` is enabled, so existing response shapes stay unchanged.
## Filter patterns ## Filter patterns
Filters help narrow down search results. Common use cases: Filters help narrow down search results. Common use cases:
+7 -3
View File
@@ -143,7 +143,8 @@
"icon": "robot", "icon": "robot",
"pages": [ "pages": [
"integrations/openclaw", "integrations/openclaw",
"integrations/hermes" "integrations/hermes",
"integrations/pi-agent"
] ]
} }
] ]
@@ -245,6 +246,7 @@
"components/vectordbs/dbs/cassandra", "components/vectordbs/dbs/cassandra",
"components/vectordbs/dbs/s3_vectors", "components/vectordbs/dbs/s3_vectors",
"components/vectordbs/dbs/databricks", "components/vectordbs/dbs/databricks",
"components/vectordbs/dbs/neon",
"components/vectordbs/dbs/neptune_analytics", "components/vectordbs/dbs/neptune_analytics",
"components/vectordbs/dbs/turbopuffer" "components/vectordbs/dbs/turbopuffer"
] ]
@@ -302,7 +304,8 @@
"group": "Migration", "group": "Migration",
"icon": "arrow-right", "icon": "arrow-right",
"pages": [ "pages": [
"migration/oss-v2-to-v3" "migration/oss-v2-to-v3",
"migration/server-pgvector-upgrade"
] ]
}, },
{ {
@@ -462,7 +465,8 @@
"icon": "robot", "icon": "robot",
"pages": [ "pages": [
"integrations/openclaw", "integrations/openclaw",
"integrations/hermes" "integrations/hermes",
"integrations/pi-agent"
] ]
} }
] ]
+243 -191
View File
@@ -7,285 +7,338 @@ Integrate [**Mem0**](https://github.com/mem0ai/mem0) with [Google ADK (Agent Dev
## Overview ## Overview
1. Store and retrieve memories from Mem0 within Google ADK agents In this guide, we'll create a Google ADK agent that:
2. Multi-agent workflows with shared memory across hierarchies 1. Uses ADK's native `MemoryService` interface to connect Mem0
3. Retrieve relevant memories from past conversations 2. Automatically injects relevant memories using ADK's built-in `load_memory` tool
4. Personalized responses based on user history 3. Persists session history to Mem0 after each turn via an after-agent callback
4. Shares memory seamlessly across multi-agent hierarchies
## Prerequisites ## Setup and Configuration
Before setting up Mem0 with Google ADK, ensure you have: Install the necessary libraries:
1. Installed the required packages:
```bash ```bash
pip install google-adk mem0ai python-dotenv pip install google-adk mem0ai python-dotenv
``` ```
2. Valid API keys: Set up your API keys:
- <a href="https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=integration-google-ai-adk" rel="nofollow">Mem0 API Key</a> - <a href="https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=integration-google-ai-adk" rel="nofollow">Mem0 API Key</a>
- Google AI Studio API Key - Google AI Studio API Key
## Basic Integration Example <Note>Remember to get your API key from <a href="https://app.mem0.ai" rel="nofollow">Mem0 Platform</a> and set up a [Google AI Studio API Key](https://aistudio.google.com/apikey).</Note>
The following example demonstrates how to create a Google ADK agent with Mem0 memory integration:
```python ```python
import os import os
import asyncio
from google.adk.agents import Agent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
from mem0 import MemoryClient
from dotenv import load_dotenv from dotenv import load_dotenv
load_dotenv() load_dotenv()
# Set up environment variables
# os.environ["GOOGLE_API_KEY"] = "your-google-api-key" # os.environ["GOOGLE_API_KEY"] = "your-google-api-key"
# os.environ["MEM0_API_KEY"] = "your-mem0-api-key" # os.environ["MEM0_API_KEY"] = "your-mem0-api-key"
```
# Initialize Mem0 client ## Implement Mem0MemoryService
mem0 = MemoryClient()
# Define memory function tools Create a custom `MemoryService` by implementing ADK's `BaseMemoryService`. Save the following as **`mem0_memory_service.py`**:
def search_memory(query: str, user_id: str) -> dict:
"""Search through past conversations and memories"""
# For Platform API, user_id goes in filters
filters = {"user_id": user_id}
memories = mem0.search(query, filters=filters)
if memories.get('results', []):
memory_list = memories['results']
memory_context = "\n".join([f"- {mem['memory']}" for mem in memory_list])
return {"status": "success", "memories": memory_context}
return {"status": "no_memories", "message": "No relevant memories found"}
def save_memory(content: str, user_id: str) -> dict: ```python
"""Save important information to memory""" import asyncio
import os
from typing import Optional
from typing_extensions import override
from google.adk.memory.base_memory_service import BaseMemoryService, SearchMemoryResponse
from google.adk.memory.memory_entry import MemoryEntry
from google.adk.sessions import Session
from google.genai.types import Content, Part
from mem0 import MemoryClient
class Mem0MemoryService(BaseMemoryService):
"""MemoryService implementation backed by the Mem0 Platform."""
def __init__(self, api_key: Optional[str] = None):
super().__init__()
api_key = api_key or os.environ.get("MEM0_API_KEY")
self._client: Optional[MemoryClient] = MemoryClient(api_key=api_key) if api_key else None
@override
async def search_memory(
self, *, app_name: str, user_id: str, query: str
) -> SearchMemoryResponse:
"""Search for memories relevant to the current user and query."""
if not self._client:
return SearchMemoryResponse(memories=[])
try:
results = await asyncio.to_thread(
self._client.search,
query,
filters={"AND": [{"user_id": user_id}, {"app_id": app_name}]},
top_k=5,
)
entries = []
for mem in results.get("results", []):
text = mem.get("memory", "")
if not text:
continue
raw_ts = mem.get("created_at") or mem.get("updated_at")
entries.append(
MemoryEntry(
content=Content(parts=[Part(text=text)]),
author=mem.get("metadata", {}).get("author", "user"),
timestamp=str(raw_ts) if raw_ts else None,
)
)
return SearchMemoryResponse(memories=entries)
except Exception as e:
print(f"[Mem0MemoryService] search_memory error: {e}")
return SearchMemoryResponse(memories=[])
@override
async def add_session_to_memory(self, session: Session) -> None:
"""Persist a completed ADK session into Mem0."""
if not self._client:
return
user_id = session.user_id
if not user_id:
return
app_name = getattr(session, "app_name", None)
try:
messages = []
for event in session.events:
if not (event.content and event.content.parts):
continue
role = getattr(event.content, "role", None) or "user"
if role == "model":
role = "assistant"
elif role not in ("user", "assistant"):
continue
text_parts = [
p.text for p in event.content.parts if hasattr(p, "text") and p.text
]
if text_parts:
messages.append({"role": role, "content": " ".join(text_parts)})
if messages:
metadata = {"app_id": app_name} if app_name else {}
await asyncio.to_thread(
self._client.add, messages, user_id=user_id, metadata=metadata
)
except Exception as e:
print(f"[Mem0MemoryService] add_session_to_memory error: {e}")
```
## Add Auto-Save Callback
This after-agent callback fires at the end of every turn and saves the session to Mem0. Save as **`memory_callbacks.py`**:
```python
async def save_session_to_memory(callback_context) -> None:
"""Persist the completed session to Mem0 after each agent turn."""
try: try:
result = mem0.add([{"role": "user", "content": content}], user_id=user_id) await callback_context.add_session_to_memory()
return {"status": "success", "message": "Information saved to memory", "result": result} except ValueError:
pass
except Exception as e: except Exception as e:
return {"status": "error", "message": f"Failed to save memory: {str(e)}"} print(f"[save_session_to_memory] error: {e}")
```
# Create agent with memory capabilities ## Basic Integration Example
personal_assistant = Agent(
The following example demonstrates creating an ADK agent with automatic Mem0 memory:
```python
import asyncio
from google.adk.agents import LlmAgent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.adk.tools import load_memory
from google.genai.types import Content, Part
from mem0_memory_service import Mem0MemoryService
from memory_callbacks import save_session_to_memory
memory_service = Mem0MemoryService()
session_service = InMemorySessionService()
agent = LlmAgent(
name="personal_assistant", name="personal_assistant",
model="gemini-2.0-flash", model="gemini-2.0-flash",
instruction="""You are a helpful personal assistant with memory capabilities. instruction="""You are a helpful personal assistant.
Use the search_memory function to recall past conversations and user preferences. Relevant memories from past conversations are provided to you automatically.
Use the save_memory function to store important information about the user. Use them to personalize your responses.""",
Always personalize your responses based on available memory.""",
description="A personal assistant that remembers user preferences and past interactions", description="A personal assistant that remembers user preferences and past interactions",
tools=[search_memory, save_memory] tools=[load_memory],
after_agent_callback=save_session_to_memory,
) )
async def chat_with_agent(user_input: str, user_id: str) -> str: runner = Runner(
""" agent=agent,
Handle user input with automatic memory integration. session_service=session_service,
memory_service=memory_service,
app_name="memory_assistant",
)
Args:
user_input: The user's message
user_id: Unique identifier for the user
Returns: async def chat(user_input: str, user_id: str) -> str:
The agent's response
"""
# Set up session and runner
session_service = InMemorySessionService()
session = await session_service.create_session( session = await session_service.create_session(
app_name="memory_assistant", app_name="memory_assistant",
user_id=user_id, user_id=user_id,
session_id=f"session_{user_id}"
) )
runner = Runner(agent=personal_assistant, app_name="memory_assistant", session_service=session_service) content = Content(role="user", parts=[Part(text=user_input)])
async for event in runner.run_async(user_id=user_id, session_id=session.id, new_message=content):
# Create content and run agent if event.is_final_response() and event.content and event.content.parts:
content = types.Content(role='user', parts=[types.Part(text=user_input)]) return event.content.parts[0].text
events = runner.run(user_id=user_id, session_id=session.id, new_message=content)
# Extract final response
for event in events:
if event.is_final_response():
response = event.content.parts[0].text
return response
return "No response generated" return "No response generated"
# Example usage
if __name__ == "__main__": if __name__ == "__main__":
response = asyncio.run(chat_with_agent( print(asyncio.run(chat(
"I love Italian food and I'm planning a trip to Rome next month", "I love Italian food and I'm planning a trip to Rome next month",
user_id="alice" user_id="alice",
)) )))
print(response)
print(asyncio.run(chat(
"Any food recommendations for my trip?",
user_id="alice",
)))
``` ```
## Multi-Agent Hierarchy with Shared Memory ## Multi-Agent Hierarchy with Shared Memory
Create specialized agents in a hierarchy that share memory: Because `memory_service` is passed to the `Runner`, every agent in the hierarchy shares the same memory automatically. Only the root coordinator needs the auto-save callback — ADK fires it once when the full turn completes:
```python ```python
import asyncio
from google.adk.agents import LlmAgent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.adk.tools.agent_tool import AgentTool from google.adk.tools.agent_tool import AgentTool
from google.adk.tools import load_memory
from google.genai.types import Content, Part
# Travel specialist agent from mem0_memory_service import Mem0MemoryService
travel_agent = Agent( from memory_callbacks import save_session_to_memory
memory_service = Mem0MemoryService()
session_service = InMemorySessionService()
travel_agent = LlmAgent(
name="travel_specialist", name="travel_specialist",
model="gemini-2.0-flash", model="gemini-2.0-flash",
instruction="""You are a travel planning specialist. Use search_memory to instruction="""You are a travel planning specialist.
understand the user's travel preferences and history before making recommendations. Relevant memories about the user's travel preferences are provided automatically.
After providing advice, use save_memory to save travel-related information.""", Use them to make personalized recommendations.""",
description="Specialist in travel planning and recommendations", description="Specialist in travel planning and recommendations",
tools=[search_memory, save_memory] tools=[load_memory],
) )
# Health advisor agent health_agent = LlmAgent(
health_agent = Agent(
name="health_advisor", name="health_advisor",
model="gemini-2.0-flash", model="gemini-2.0-flash",
instruction="""You are a health and wellness advisor. Use search_memory to instruction="""You are a health and wellness advisor.
understand the user's health goals and dietary preferences. Relevant memories about the user's health goals are provided automatically.
After providing advice, use save_memory to save health-related information.""", Use them to give personalized advice.""",
description="Specialist in health and wellness advice", description="Specialist in health and wellness advice",
tools=[search_memory, save_memory] tools=[load_memory],
) )
# Coordinator agent that delegates to specialists coordinator = LlmAgent(
coordinator_agent = Agent(
name="coordinator", name="coordinator",
model="gemini-2.0-flash", model="gemini-2.0-flash",
instruction="""You are a coordinator that delegates requests to specialist agents. instruction="""You are a coordinator that delegates requests to specialist agents.
For travel-related questions (trips, hotels, flights, destinations), delegate to the travel specialist. For travel-related questions, delegate to the travel specialist.
For health-related questions (fitness, diet, wellness, exercise), delegate to the health advisor. For health-related questions, delegate to the health advisor.
Use search_memory to understand the user before delegation.""", Relevant memories about the user are provided automatically.""",
description="Coordinates requests between specialist agents", description="Coordinates requests between specialist agents",
tools=[ tools=[
load_memory,
AgentTool(agent=travel_agent, skip_summarization=False), AgentTool(agent=travel_agent, skip_summarization=False),
AgentTool(agent=health_agent, skip_summarization=False) AgentTool(agent=health_agent, skip_summarization=False),
] ],
after_agent_callback=save_session_to_memory,
) )
def chat_with_specialists(user_input: str, user_id: str) -> str: runner = Runner(
""" agent=coordinator,
Handle user input with specialist agent delegation and memory. session_service=session_service,
memory_service=memory_service,
app_name="specialist_system",
)
Args:
user_input: The user's message
user_id: Unique identifier for the user
Returns: async def chat_with_specialists(user_input: str, user_id: str) -> str:
The specialist agent's response session = await session_service.create_session(
"""
session_service = InMemorySessionService()
session = session_service.create_session(
app_name="specialist_system", app_name="specialist_system",
user_id=user_id, user_id=user_id,
session_id=f"session_{user_id}"
) )
runner = Runner(agent=coordinator_agent, app_name="specialist_system", session_service=session_service) content = Content(role="user", parts=[Part(text=user_input)])
async for event in runner.run_async(user_id=user_id, session_id=session.id, new_message=content):
content = types.Content(role='user', parts=[types.Part(text=user_input)]) if event.is_final_response() and event.content and event.content.parts:
events = runner.run(user_id=user_id, session_id=session.id, new_message=content) return event.content.parts[0].text
for event in events:
if event.is_final_response():
response = event.content.parts[0].text
# Store the conversation in shared memory
conversation = [
{"role": "user", "content": user_input},
{"role": "assistant", "content": response}
]
mem0.add(conversation, user_id=user_id)
return response
return "No response generated" return "No response generated"
# Example usage
response = chat_with_specialists("Plan a healthy meal for my Italy trip", user_id="alice")
print(response)
```
## Quick Start Chat Interface
Simple interactive chat with memory and Google ADK:
```python
def interactive_chat():
"""Interactive chat interface with memory and ADK"""
user_id = input("Enter your user ID: ") or "demo_user"
print(f"Chat started for user: {user_id}")
print("Type 'quit' to exit")
print("=" * 50)
while True:
user_input = input("\nYou: ")
if user_input.lower() == 'quit':
print("Goodbye! Your conversation has been saved to memory.")
break
else:
response = chat_with_specialists(user_input, user_id)
print(f"Assistant: {response}")
if __name__ == "__main__": if __name__ == "__main__":
interactive_chat() response = asyncio.run(chat_with_specialists("Plan a healthy meal for my Italy trip", user_id="alice"))
print(response)
``` ```
## Key Features ## Key Features
### 1. Memory-Enhanced Function Tools 1. **Automatic Memory Injection**: ADK's built-in `load_memory` tool searches Mem0 at the start of each turn and injects relevant memories directly into the agent context — no prompt instructions needed.
- **Function Tools**: Standard Python functions that can search and save memories 2. **Automatic Session Saving**: The `save_session_to_memory` callback persists every completed turn to Mem0 without any manual calls.
- **Tool Context**: Access to session state and memory through function parameters 3. **Native ADK Integration**: `Mem0MemoryService` implements ADK's `BaseMemoryService` and integrates via the `Runner` — works natively across the entire agent hierarchy.
- **Structured Returns**: Dictionary-based returns with status indicators for better LLM understanding 4. **User Scoping**: `user_id` is passed automatically from the ADK session context, ensuring memories are always scoped to the correct user.
5. **Multi-Agent Support**: A single `Mem0MemoryService` instance shared through the `Runner` gives all agents — coordinators and specialists — access to the same user memory.
### 2. Multi-Agent Memory Sharing
- **Agent-as-a-Tool**: Specialists can be called as tools while maintaining shared memory
- **Hierarchical Delegation**: Coordinator agents route to specialists based on context
- **Memory Categories**: Store interactions with metadata for better organization
### 3. Flexible Memory Operations
- **Search Capabilities**: Retrieve relevant memories through conversation history
- **User Segmentation**: Organize memories by user ID
- **Memory Management**: Built-in tools for saving and retrieving information
## Configuration Options ## Configuration Options
Customize memory behavior and agent setup: ### Using Vertex AI
To use Google Cloud Vertex AI instead of AI Studio, set the following environment variables before creating agents:
```python ```python
# Configure memory search with filters import os
# For Platform API, all filters including user_id go in filters object
memories = mem0.search(
query="travel preferences",
filters={
"AND": [
{"user_id": "alice"},
{"categories": {"contains": "travel"}}
]
},
top_k=5
)
# Configure agent with custom model settings
agent = Agent(
name="custom_agent",
model="gemini-2.0-flash", # or use LiteLLM for other models
instruction="Custom agent behavior",
tools=[memory_tools],
# Additional ADK configurations
)
# Use Google Cloud Vertex AI instead of AI Studio
os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True"
os.environ["GOOGLE_CLOUD_PROJECT"] = "your-project-id" os.environ["GOOGLE_CLOUD_PROJECT"] = "your-project-id"
os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1" os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1"
``` ```
### Advanced Memory Filtering
You can customize how memories are searched by modifying `Mem0MemoryService.search_memory`. For example, to filter by category:
```python
results = await asyncio.to_thread(
self._client.search,
query,
filters={
"AND": [
{"user_id": user_id},
{"app_id": app_name},
{"categories": {"contains": "travel"}}
]
},
top_k=10,
)
```
<Note>`InMemorySessionService` stores sessions in memory and is intended for prototyping. For production, use a persistent session service and clean up sessions when they are no longer needed.</Note>
## Conclusion
By implementing `Mem0MemoryService` as an ADK `BaseMemoryService`, you get persistent, user-scoped memory across single agents and complex multi-agent hierarchies with minimal code. Memory injection and session saving happen automatically, keeping your agent prompts clean and your token usage efficient.
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="Healthcare Agent Cookbook" icon="heart-pulse" href="/cookbooks/integrations/healthcare-google-adk"> <Card title="Healthcare Agent Cookbook" icon="heart-pulse" href="/cookbooks/integrations/healthcare-google-adk">
Build HIPAA-compliant healthcare agents with Google ADK Build HIPAA-compliant healthcare agents with Google ADK
@@ -294,4 +347,3 @@ os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1"
Compare with OpenAI's agent framework Compare with OpenAI's agent framework
</Card> </Card>
</CardGroup> </CardGroup>
+181
View File
@@ -0,0 +1,181 @@
---
title: Pi Agent
description: "Add persistent memory to Pi Agent with the Mem0 plugin semantic search, auto-capture, and dream consolidation."
---
Add persistent memory to [**Pi Agent**](https://pi.dev) with `@mem0/pi-agent-plugin`. Your agent forgets everything between sessions — this plugin fixes that by automatically capturing knowledge from conversations, storing it in Mem0's cloud memory layer, and retrieving relevant context before every response.
## Overview
The plugin provides:
1. **Auto-capture** — Extracts durable facts from both user and assistant messages automatically
2. **Semantic recall** — Retrieves relevant memories via the `mem0_memory` tool before each response
3. **Dream consolidation** — Periodic maintenance: merges duplicates, resolves contradictions, prunes stale entries
4. **Monorepo-aware scoping** — Uses git root for project detection, consistent across subdirectories
5. **Confirmation dialogs** — Destructive commands ask before acting via Pi's built-in UI
6. **8 skills + 8 commands** — Essential memory management from slash commands and agent-guided workflows
## Prerequisites
1. A Mem0 Platform account and API key:
- <a href="https://app.mem0.ai?utm_source=oss&utm_medium=integration-pi-agent" rel="nofollow">Sign up at app.mem0.ai</a>
- <a href="https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=integration-pi-agent" rel="nofollow">Get your API key</a> (starts with `m0-`)
2. Pi Agent installed ([pi.dev](https://pi.dev))
3. Your API key added to your shell profile:
<CodeGroup>
```bash zsh
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.zshrc
source ~/.zshrc
```
```bash bash
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.bashrc
source ~/.bashrc
```
</CodeGroup>
## Installation
```bash
pi install npm:@mem0/pi-agent-plugin
```
That's it. The extension loads automatically on every Pi session. No config files needed — `MEM0_API_KEY` from your environment is picked up automatically.
<Info>
Start a new Pi session and run `/mem0-status` to verify the connection. You should see your user ID, detected project, and memory count.
</Info>
### Optional Configuration
For advanced settings, create `~/.pi/agent/mem0-config.json`:
```json
{
"apiKey": "m0-your-key-here",
"userId": "your-username",
"autoCapture": true,
"defaultScope": "project",
"dream": {
"enabled": true,
"auto": true,
"minHours": 24,
"minSessions": 5,
"minMemories": 20
}
}
```
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `apiKey` | `string` | `$MEM0_API_KEY` | Mem0 API key. Environment variable takes precedence. |
| `userId` | `string` | `$MEM0_USER_ID` or `"default"` | User identity for memory scoping |
| `autoCapture` | `boolean` | `true` | Store facts from conversations automatically |
| `defaultScope` | `string` | `"project"` | Default memory scope: `project`, `session`, or `global` |
| `dream.enabled` | `boolean` | `true` | Enable dream consolidation |
| `dream.auto` | `boolean` | `true` | Auto-trigger dreams when thresholds are met |
| `dream.minHours` | `number` | `24` | Minimum hours between auto-dreams |
| `dream.minSessions` | `number` | `5` | Minimum sessions before first auto-dream |
| `dream.minMemories` | `number` | `20` | Minimum memories before auto-dream triggers |
## What's Included
| Component | Description |
|-----------|-------------|
| `mem0_memory` tool | Agent-callable tool for search, add, get_all, delete, delete_all |
| 8 slash commands | Essential memory management from the command line |
| 8 skills | Guide the agent on how to use each capability |
| Auto-capture | Extracts and stores facts on every `agent_end` event |
| System prompt | Appends memory policy to every agent turn |
| Dream consolidation | Automated memory maintenance with session/time/count gates |
## Agent Tool
The `mem0_memory` tool is registered with Pi and callable by the agent during conversations:
| Action | Required Params | Description |
|--------|----------------|-------------|
| `search` | `query` | Semantic search across memories |
| `add` | `content` | Store a new memory |
| `get_all` | — | List all memories in scope |
| `delete` | `memory_id` | Delete a specific memory |
| `delete_all` | — | Delete all memories in scope |
All actions accept an optional `scope` parameter: `project` (default), `session`, or `global`.
Tool output is truncated to 200 lines / 50KB to prevent context overflow.
## Commands
| Command | Description |
|---------|-------------|
| `/mem0-remember <text>` | Store a memory verbatim (no inference) |
| `/mem0-forget <query>` | Search and delete memories (with confirmation dialog) |
| `/mem0-search <query>` | Semantic search across memories |
| `/mem0-tour [scope]` | Browse all memories grouped by category |
| `/mem0-dream` | Consolidate — merge duplicates, prune stale, resolve contradictions |
| `/mem0-pin <query>` | Pin a memory to protect from dream pruning (preserves memory ID) |
| `/mem0-scope <scope>` | Change default scope for this session (project, session, global) |
| `/mem0-status` | Connection health, identity, and memory count |
## Memory Scopes
Memories are scoped using Mem0's `user_id`, `app_id`, and `run_id` parameters:
| Scope | Filters | Use Case |
|-------|---------|----------|
| `project` | user_id + app_id (git root) | **Default.** Project-specific knowledge — decisions, architecture, config |
| `session` | user_id + app_id + run_id | Ephemeral context for the current session only |
| `global` | user_id only | All memories across all your projects |
The `app_id` is auto-detected from the git repository root (`git rev-parse --show-toplevel`), so all subdirectories within a monorepo share the same memory pool. Falls back to the working directory name for non-git directories. The `run_id` is derived from Pi's session file path.
## Dream Consolidation
### Confirmation Dialogs
Destructive and mutating commands use Pi's built-in `ctx.ui.confirm()` dialog before acting:
- `/mem0-forget` asks "Delete this memory?" before deleting a single match
- `/mem0-pin` asks "Pin this memory?" before modifying it
- Cancelling either operation is always safe — no changes are made
### Pin
`/mem0-pin` uses Mem0's `update()` API to prepend `[PINNED]` to the memory text. This preserves the original memory ID — no add+delete cycle that would lose history or change the UUID.
### Dream Consolidation
The plugin includes automated memory maintenance ("dream") that merges duplicates, resolves contradictions, and prunes stale entries. When enabled, dreams auto-trigger after enough sessions, time, and memories accumulate (configurable via `dream.*` settings). Run `/mem0-dream` to trigger consolidation manually at any time. Pinned memories (via `/mem0-pin`) are protected from pruning.
## Example Workflow
```text
# Session 1
You: I prefer dark mode and concise answers.
# Mem0 auto-captures preferences
# Session 2 (days later)
You: What do you know about my preferences?
# Pi retrieves stored memories — no re-explaining needed
```
## Troubleshooting
- **"No API key found"** — Verify `MEM0_API_KEY` is set: `echo $MEM0_API_KEY`. If empty, add it to your shell profile (see Prerequisites)
- **Extension not loading** — Check Pi startup output for errors. Run `pi -e ./src/entry.ts` from the plugin directory for verbose output
- **Memories not capturing** — Verify `autoCapture` is `true` (default). Check `/mem0-status` for connection health
- **Wrong project detected** — The plugin uses the git repository root as `app_id`. If not in a git repo, it falls back to the working directory name. Run `/mem0-status` to see the detected project
- **Dream not triggering** — All three gates must pass (time, sessions, memories). Use `/mem0-dream` to force it manually
<CardGroup cols={2}>
<Card title="Claude Code Integration" icon="terminal" href="/integrations/claude-code">
Add Mem0 memory to Claude Code
</Card>
<Card title="OpenClaw Integration" icon="plug" href="/integrations/openclaw">
Add Mem0 memory to OpenClaw agents
</Card>
</CardGroup>
+151 -116
View File
@@ -6,25 +6,33 @@ description: "Use the Mem0 AI SDK Provider with Vercel AI SDK for persistent mem
The [**Mem0 AI SDK Provider**](https://www.npmjs.com/package/@mem0/vercel-ai-provider) is a library developed by **Mem0** to integrate with the Vercel AI SDK. This library brings enhanced AI interaction capabilities to your applications by introducing persistent memory functionality. The [**Mem0 AI SDK Provider**](https://www.npmjs.com/package/@mem0/vercel-ai-provider) is a library developed by **Mem0** to integrate with the Vercel AI SDK. This library brings enhanced AI interaction capabilities to your applications by introducing persistent memory functionality.
<Note type="info"> <Note type="info">
Mem0 AI SDK now supports <strong>Vercel AI SDK V5</strong>. Mem0 AI SDK Provider v3.0.0 supports <strong>Vercel AI SDK v6</strong> (<code>LanguageModelV3</code> / <code>ProviderV3</code>). If you are upgrading from v2.x, see the <a href="https://ai-sdk.dev/docs/migration-guides/migration-guide-6-0">AI SDK v6 migration guide</a>.
</Note> </Note>
## Overview ## Overview
1. Offers persistent memory storage for conversational AI 1. Offers persistent memory storage for conversational AI
2. Enables smooth integration with the Vercel AI SDK 2. Enables smooth integration with the Vercel AI SDK v6
3. Ensures compatibility with multiple LLM providers 3. Ensures compatibility with multiple LLM providers (OpenAI, Anthropic, Google, Groq, Cohere)
4. Supports structured message formats for clarity 4. Supports structured message formats for clarity
5. Facilitates streaming response capabilities 5. Facilitates streaming response capabilities
6. Attaches Mem0 memories as sources in responses for programmatic access
## Setup and Configuration ## Setup and Configuration
Install the SDK provider using npm: Install the SDK provider and AI SDK:
```bash ```bash
npm install @mem0/vercel-ai-provider npm install @mem0/vercel-ai-provider ai@^6
``` ```
### Peer Dependencies
`@mem0/vercel-ai-provider` v3.0.0 requires:
- `ai` v6+ (`^6.0.199`)
- `@ai-sdk/provider` v3+ (`^3.0.10`)
- Provider packages at v3+: `@ai-sdk/openai@^3`, `@ai-sdk/anthropic@^3`, `@ai-sdk/google@^3`, `@ai-sdk/groq@^3`, `@ai-sdk/cohere@^3`
## Getting Started ## Getting Started
### Setting Up Mem0 ### Setting Up Mem0
@@ -41,7 +49,7 @@ npm install @mem0/vercel-ai-provider
mem0ApiKey: "m0-xxx", mem0ApiKey: "m0-xxx",
apiKey: "provider-api-key", apiKey: "provider-api-key",
config: { config: {
// Options for LLM Provider // Options for the upstream LLM provider (e.g. baseURL)
}, },
// Optional Mem0 Global Config // Optional Mem0 Global Config
mem0Config: { mem0Config: {
@@ -57,154 +65,153 @@ npm install @mem0/vercel-ai-provider
3. Add Memories to Enhance Context: 3. Add Memories to Enhance Context:
```typescript ```typescript
import { LanguageModelV2Prompt } from "@ai-sdk/provider";
import { addMemories } from "@mem0/vercel-ai-provider"; import { addMemories } from "@mem0/vercel-ai-provider";
const messages: LanguageModelV2Prompt = [ const messages = [
{ role: "user", content: [{ type: "text", text: "I love red cars." }] }, { role: "user", content: [{ type: "text", text: "I love red cars." }] },
]; ];
await addMemories(messages, { user_id: "borat" }); await addMemories(messages, { user_id: "borat" });
``` ```
### Standalone Features: ### Standalone Features
```typescript ```typescript
await addMemories(messages, { user_id: "borat", mem0ApiKey: "m0-xxx" }); await addMemories(messages, { user_id: "borat", mem0ApiKey: "m0-xxx" });
await retrieveMemories(prompt, { user_id: "borat", mem0ApiKey: "m0-xxx" }); await retrieveMemories(prompt, { user_id: "borat", mem0ApiKey: "m0-xxx" });
await getMemories(prompt, { user_id: "borat", mem0ApiKey: "m0-xxx" }); await getMemories(prompt, { user_id: "borat", mem0ApiKey: "m0-xxx" });
``` ```
> For standalone features, such as `addMemories`, `retrieveMemories`, and `getMemories`, you must either set `MEM0_API_KEY` as an environment variable or pass it directly in the function call.
> `getMemories` will return raw memories in the form of an array of objects, while `retrieveMemories` will return a response in string format with a system prompt ingested with the retrieved memories. > For standalone features, such as `addMemories`, `retrieveMemories`, and `getMemories`, you must either set `MEM0_API_KEY` as an environment variable or pass it directly in the function call.
> `getMemories` returns an array of memory objects. > `getMemories` will return raw memories in the form of an array of objects, while `retrieveMemories` will return a response in string format with a system prompt ingested with the retrieved memories.
### 1. Basic Text Generation with Memory Context ### 1. Basic Text Generation with Memory Context
```typescript ```typescript
import { generateText } from "ai"; import { generateText } from "ai";
import { createMem0 } from "@mem0/vercel-ai-provider"; import { createMem0 } from "@mem0/vercel-ai-provider";
const mem0 = createMem0(); const mem0 = createMem0();
const { text } = await generateText({ const { text } = await generateText({
model: mem0("gpt-4-turbo", { user_id: "borat" }), model: mem0("gpt-5-mini", { user_id: "borat" }),
prompt: "Suggest me a good car to buy!", prompt: "Suggest me a good car to buy!",
}); });
``` ```
### 2. Combining OpenAI Provider with Memory Utils ### 2. Combining OpenAI Provider with Memory Utils
```typescript ```typescript
import { generateText } from "ai"; import { generateText } from "ai";
import { openai } from "@ai-sdk/openai"; import { openai } from "@ai-sdk/openai";
import { retrieveMemories } from "@mem0/vercel-ai-provider"; import { retrieveMemories } from "@mem0/vercel-ai-provider";
const prompt = "Suggest me a good car to buy."; const prompt = "Suggest me a good car to buy.";
const memories = await retrieveMemories(prompt, { user_id: "borat" }); const memories = await retrieveMemories(prompt, { user_id: "borat" });
const { text } = await generateText({ const { text } = await generateText({
model: openai("gpt-4-turbo"), model: openai("gpt-5-mini"),
prompt: prompt, prompt: prompt,
system: memories, system: memories,
}); });
``` ```
### 3. Structured Message Format with Memory ### 3. Structured Message Format with Memory
```typescript ```typescript
import { generateText } from "ai"; import { generateText } from "ai";
import { createMem0 } from "@mem0/vercel-ai-provider"; import { createMem0 } from "@mem0/vercel-ai-provider";
const mem0 = createMem0(); const mem0 = createMem0();
const { text } = await generateText({ const { text } = await generateText({
model: mem0("gpt-4-turbo", { user_id: "borat" }), model: mem0("gpt-5-mini", { user_id: "borat" }),
messages: [ messages: [
{ {
role: "user", role: "user",
content: [ content: [
{ type: "text", text: "Suggest me a good car to buy." }, { type: "text", text: "Suggest me a good car to buy." },
{ type: "text", text: "Why is it better than the other cars for me?" }, { type: "text", text: "Why is it better than the other cars for me?" },
],
},
], ],
}); },
``` ],
});
```
### 3. Streaming Responses with Memory Context ### 4. Streaming Responses with Memory Context
```typescript ```typescript
import { streamText } from "ai"; import { streamText } from "ai";
import { createMem0 } from "@mem0/vercel-ai-provider"; import { createMem0 } from "@mem0/vercel-ai-provider";
const mem0 = createMem0(); const mem0 = createMem0();
const { textStream } = streamText({ const { textStream } = streamText({
model: mem0("gpt-4-turbo", { model: mem0("gpt-5-mini", {
user_id: "borat", user_id: "borat",
}), }),
prompt: "Suggest me a good car to buy! Why is it better than the other cars for me? Give options for every price range.", prompt: "Suggest me a good car to buy! Why is it better than the other cars for me? Give options for every price range.",
}); });
for await (const textPart of textStream) { for await (const textPart of textStream) {
process.stdout.write(textPart); process.stdout.write(textPart);
} }
``` ```
### 4. Generate Responses with Tools Call ### 5. Generate Responses with Tools Call
```typescript ```typescript
import { generateText } from "ai"; import { generateText, tool } from "ai";
import { createMem0 } from "@mem0/vercel-ai-provider"; import { createMem0 } from "@mem0/vercel-ai-provider";
import { z } from "zod"; import { z } from "zod";
const mem0 = createMem0({ const mem0 = createMem0({
provider: "anthropic", provider: "anthropic",
apiKey: "anthropic-api-key", apiKey: "anthropic-api-key",
mem0Config: { mem0Config: {
// Global User ID user_id: "borat"
user_id: "borat" }
} });
});
const prompt = "What the temperature in the city that I live in?" const result = await generateText({
model: mem0('claude-sonnet-4-20250514'),
tools: {
weather: tool({
description: 'Get the weather in a location',
parameters: z.object({
location: z.string().describe('The location to get the weather for'),
}),
execute: async ({ location }) => ({
location,
temperature: 72 + Math.floor(Math.random() * 21) - 10,
}),
}),
},
prompt: "What the temperature in the city that I live in?",
});
const result = await generateText({ console.log(result);
model: mem0('claude-3-5-sonnet-20240620'), ```
tools: {
weather: tool({
description: 'Get the weather in a location',
parameters: z.object({
location: z.string().describe('The location to get the weather for'),
}),
execute: async ({ location }) => ({
location,
temperature: 72 + Math.floor(Math.random() * 21) - 10,
}),
}),
},
prompt: prompt,
});
console.log(result); ### 6. Get Sources from Memory
```
### 5. Get sources from memory `generateText` and `streamText` responses include Mem0 memories as a source, giving you programmatic access to the memories that influenced the response:
```typescript ```typescript
const { text, sources } = await generateText({ const { text, sources } = await generateText({
model: mem0("gpt-4-turbo"), model: mem0("gpt-5-mini", { user_id: "borat" }),
prompt: "Suggest me a good car to buy!", prompt: "Suggest me a good car to buy!",
}); });
// sources[0].title === "Mem0 Memories"
// sources[0].providerMetadata.mem0.memories — array of memory objects
console.log(sources); console.log(sources);
``` ```
The same can be done for `streamText` as well. The same can be done for `streamText` as well.
### 6. File Support with Memory Context ### 7. File Support with Memory Context
Mem0 AI SDK supports file processing with memory context. Here's an example of analyzing a PDF file: Mem0 AI SDK supports file processing with memory context. Here's an example of analyzing a PDF file:
@@ -226,15 +233,11 @@ const mem0 = createMem0({
}); });
async function main() { async function main() {
// Read the PDF file
const filePath = join(process.cwd(), 'my_pdf.pdf'); const filePath = join(process.cwd(), 'my_pdf.pdf');
const fileBuffer = readFileSync(filePath); const fileBuffer = readFileSync(filePath);
// Convert the file's arrayBuffer to a Base64 data URL
const arrayBuffer = fileBuffer.buffer.slice(fileBuffer.byteOffset, fileBuffer.byteOffset + fileBuffer.byteLength); const arrayBuffer = fileBuffer.buffer.slice(fileBuffer.byteOffset, fileBuffer.byteOffset + fileBuffer.byteLength);
const uint8Array = new Uint8Array(arrayBuffer); const uint8Array = new Uint8Array(arrayBuffer);
// Convert Uint8Array to an array of characters
const charArray = Array.from(uint8Array, byte => String.fromCharCode(byte)); const charArray = Array.from(uint8Array, byte => String.fromCharCode(byte));
const binaryString = charArray.join(''); const binaryString = charArray.join('');
const base64Data = Buffer.from(binaryString, 'binary').toString('base64'); const base64Data = Buffer.from(binaryString, 'binary').toString('base64');
@@ -274,24 +277,56 @@ main();
| Provider | Configuration Value | | Provider | Configuration Value |
|----------|-------------------| |----------|-------------------|
| OpenAI | openai | | OpenAI | `openai` |
| Anthropic | anthropic | | Anthropic | `anthropic` |
| Google | google | | Google / Gemini | `google` or `gemini` |
| Groq | groq | | Groq | `groq` |
| Cohere | `cohere` |
> **Note**: You can use `google` as provider for Gemini (Google) models. They are same and internally they use `@ai-sdk/google` package. > **Note**: You can use either `google` or `gemini` as the provider value for Google Gemini models. Both map to the `@ai-sdk/google` package internally.
## Configuration Options
### Mem0ConfigSettings
These options can be passed per-request when creating a model instance:
| Option | Type | Description |
|--------|------|-------------|
| `user_id` | `string` | User identifier for memory scoping |
| `agent_id` | `string` | Agent identifier |
| `app_id` | `string` | Application identifier |
| `run_id` | `string` | Run/session identifier |
| `metadata` | `object` | Custom metadata for memories |
| `filters` | `object` | Filters for memory search |
| `infer` | `boolean` | Enable inference-based retrieval |
| `top_k` | `number` | Number of memories to retrieve (default: 10) |
| `threshold` | `number` | Relevance threshold for search |
| `rerank` | `boolean` | Enable reranking of results |
| `page` | `number` | Page number for pagination |
| `page_size` | `number` | Results per page |
## Key Features ## Key Features
- `createMem0()`: Initializes a new Mem0 provider instance. - `createMem0()`: Initializes a new Mem0 provider instance implementing `ProviderV3`.
- `retrieveMemories()`: Retrieves memory context for prompts. - `retrieveMemories()`: Retrieves memory context for prompts as a formatted system prompt string.
- `getMemories()`: Get memories from your profile in array format. - `getMemories()`: Get memories from your profile in array format.
- `addMemories()`: Adds user memories to enhance contextual responses. - `addMemories()`: Adds user memories to enhance contextual responses.
## Migrating from v2.x
If you're upgrading from `@mem0/vercel-ai-provider` v2.x:
1. **Upgrade AI SDK**: `npm install ai@^6` and update all `@ai-sdk/*` provider packages to `^3.x`
2. **Remove deprecated params**: Remove `org_id`, `project_id`, `output_format`, `filter_memories`, `async_mode`, `enable_graph` from your config
3. **Remove graph memory**: All graph-related options (`enable_graph`, graph prompts) have been removed. Graph memory is now a project-level setting on the Mem0 Platform
4. **Update imports**: `LanguageModelV2Prompt` is now `LanguageModelV3Prompt` if you import types directly
## Best Practices ## Best Practices
1. **User Identification**: Use a unique `user_id` for consistent memory retrieval. 1. **User Identification**: Use a unique `user_id` for consistent memory retrieval.
2. **Memory Cleanup**: Regularly clean up unused memory data. 2. **Memory Cleanup**: Regularly clean up unused memory data.
3. **Sources**: Access `result.sources` to inspect which memories influenced the response.
> **Note**: We also have support for `agent_id`, `app_id`, and `run_id`. Refer [Docs](/api-reference/memory/add-memories). > **Note**: We also have support for `agent_id`, `app_id`, and `run_id`. Refer [Docs](/api-reference/memory/add-memories).
+3
View File
@@ -228,6 +228,7 @@ If the user is on a pre-current major (Python < 2, TS < 3, or Platform `output_f
- [OSS v2 to v3 Migration](https://docs.mem0.ai/migration/oss-v2-to-v3) [OSS]: Use when upgrading a self-hosted deployment across major versions. - [OSS v2 to v3 Migration](https://docs.mem0.ai/migration/oss-v2-to-v3) [OSS]: Use when upgrading a self-hosted deployment across major versions.
- [Platform v2 to v3 Migration](https://docs.mem0.ai/migration/platform-v2-to-v3) [Platform]: Use when upgrading a Platform integration across major versions. - [Platform v2 to v3 Migration](https://docs.mem0.ai/migration/platform-v2-to-v3) [Platform]: Use when upgrading a Platform integration across major versions.
- [API Changes](https://docs.mem0.ai/migration/api-changes) [Both]: Use when the upgrade involves API surface changes. - [API Changes](https://docs.mem0.ai/migration/api-changes) [Both]: Use when the upgrade involves API surface changes.
- [Server pgvector Image Upgrade](https://docs.mem0.ai/migration/server-pgvector-upgrade) [OSS]: Use when upgrading the self-hosted server Docker image from ankane/pgvector to pgvector/pgvector.
- [Changelog](https://docs.mem0.ai/changelog/highlights) [Both]: Use when the user asks what shipped recently. - [Changelog](https://docs.mem0.ai/changelog/highlights) [Both]: Use when the user asks what shipped recently.
## Open Source ## Open Source
@@ -258,6 +259,7 @@ If the user is on a pre-current major (Python < 2, TS < 3, or Platform `output_f
- [Camel AI](https://docs.mem0.ai/integrations/camel-ai) [Both]: Use when the user is on Camel AI. - [Camel AI](https://docs.mem0.ai/integrations/camel-ai) [Both]: Use when the user is on Camel AI.
- [ChatDev](https://docs.mem0.ai/integrations/chatdev) [Both]: Use when the user is on ChatDev. - [ChatDev](https://docs.mem0.ai/integrations/chatdev) [Both]: Use when the user is on ChatDev.
- [Hermes](https://docs.mem0.ai/integrations/hermes) [Both]: Use when the user is on Hermes. - [Hermes](https://docs.mem0.ai/integrations/hermes) [Both]: Use when the user is on Hermes.
- [Pi Agent](https://docs.mem0.ai/integrations/pi-agent) [Platform]: Use when adding persistent memory to Pi Agent with the Mem0 plugin.
- [OpenAI Agents SDK](https://docs.mem0.ai/integrations/openai-agents-sdk) [Both]: Use when the user is on the OpenAI Agents SDK. - [OpenAI Agents SDK](https://docs.mem0.ai/integrations/openai-agents-sdk) [Both]: Use when the user is on the OpenAI Agents SDK.
- [Google AI ADK](https://docs.mem0.ai/integrations/google-ai-adk) [Both]: Use when the user is on Google's Agent Development Kit. - [Google AI ADK](https://docs.mem0.ai/integrations/google-ai-adk) [Both]: Use when the user is on Google's Agent Development Kit.
- [Mastra](https://docs.mem0.ai/integrations/mastra) [Both]: Use when the user is on Mastra (TypeScript). - [Mastra](https://docs.mem0.ai/integrations/mastra) [Both]: Use when the user is on Mastra (TypeScript).
@@ -474,6 +476,7 @@ Everything below is OSS-only provider configuration. Skip this entire section wh
- [Elasticsearch](https://docs.mem0.ai/components/vectordbs/dbs/elasticsearch) [OSS]: Use when Elasticsearch is the backing store. - [Elasticsearch](https://docs.mem0.ai/components/vectordbs/dbs/elasticsearch) [OSS]: Use when Elasticsearch is the backing store.
- [OpenSearch](https://docs.mem0.ai/components/vectordbs/dbs/opensearch) [OSS]: Use when OpenSearch is the backing store. - [OpenSearch](https://docs.mem0.ai/components/vectordbs/dbs/opensearch) [OSS]: Use when OpenSearch is the backing store.
- [Supabase](https://docs.mem0.ai/components/vectordbs/dbs/supabase) [OSS]: Use when Supabase with pgvector is the backing store. - [Supabase](https://docs.mem0.ai/components/vectordbs/dbs/supabase) [OSS]: Use when Supabase with pgvector is the backing store.
- [Neon](https://docs.mem0.ai/components/vectordbs/dbs/neon) [OSS]: Use when Neon Postgres with pgvector is the backing store.
- [Upstash Vector](https://docs.mem0.ai/components/vectordbs/dbs/upstash-vector) [OSS]: Use for serverless Upstash Vector. - [Upstash Vector](https://docs.mem0.ai/components/vectordbs/dbs/upstash-vector) [OSS]: Use for serverless Upstash Vector.
- [Vectorize](https://docs.mem0.ai/components/vectordbs/dbs/vectorize) [OSS]: Use when the store is Cloudflare Vectorize. - [Vectorize](https://docs.mem0.ai/components/vectordbs/dbs/vectorize) [OSS]: Use when the store is Cloudflare Vectorize.
- [Vertex AI Vector Search](https://docs.mem0.ai/components/vectordbs/dbs/vertex_ai) [OSS]: Use when the store is Google Cloud Vertex Vector Search. - [Vertex AI Vector Search](https://docs.mem0.ai/components/vectordbs/dbs/vertex_ai) [OSS]: Use when the store is Google Cloud Vertex Vector Search.
+2 -2
View File
@@ -14,9 +14,9 @@ Move your Mem0 implementation to managed infrastructure with enterprise features
| --------------------- | -------------- | ---------------------------- | | --------------------- | -------------- | ---------------------------- |
| Infrastructure & Code | Low (~30 mins) | None (Parallel run possible) | | Infrastructure & Code | Low (~30 mins) | None (Parallel run possible) |
<Info> <Note>
Using Mem0 Open Source with **hosted Qdrant**? You can migrate your existing memories to Mem0 Platform with a one-line script below. Using Mem0 Open Source with **hosted Qdrant**? You can migrate your existing memories to Mem0 Platform with a one-line script below.
</Info> </Note>
<Info> <Info>
**Why migrate to Platform?** **Why migrate to Platform?**
+158
View File
@@ -0,0 +1,158 @@
---
title: "Server: Upgrading the pgvector Docker Image"
description: "Migrate your self-hosted Mem0 server from the archived ankane/pgvector image to the official pgvector/pgvector image."
icon: "arrow-right"
iconType: "solid"
---
## Overview
The self-hosted Mem0 server has upgraded its PostgreSQL Docker image:
| | Before | After |
| --- | --- | --- |
| Docker image | `ankane/pgvector:v0.5.1` | `pgvector/pgvector:pg17` |
| PostgreSQL | 15 | 17 |
| pgvector | 0.5.1 | 0.8.0 |
| Credentials | Hardcoded `postgres` / `postgres` | Set via `POSTGRES_USER` / `POSTGRES_PASSWORD` env vars |
<Warning>
The `ankane/pgvector` image is **archived and no longer maintained**. The new `pgvector/pgvector` image is the official distribution maintained by the pgvector project.
</Warning>
<Info>
**Should you migrate?**
- You are running the Mem0 server via `docker-compose.yaml` in the `server/` directory.
- You want to stay on a maintained, actively-patched PostgreSQL + pgvector image.
- You want pgvector 0.8.0 features (improved HNSW performance, parallel index builds).
</Info>
## Fresh Installs
No migration is needed. Copy the example env file, set your password, and start the stack:
```bash
cd server
cp .env.example .env
# Edit .env — set POSTGRES_PASSWORD (required) and OPENAI_API_KEY at minimum
make up
```
## Migrating an Existing Install
PostgreSQL 17 cannot read data files created by PostgreSQL 15 directly. You need to export your data from the old container and import it into the new one.
### 1. Back Up Your Data
With the **old** stack still running:
```bash
cd server
docker compose exec -T postgres pg_dumpall -U postgres > mem0_backup.sql
```
Verify the dump is non-empty:
```bash
ls -lh mem0_backup.sql
```
<Warning>
Do not skip this step. The next step permanently deletes your Postgres data volume.
</Warning>
### 2. Stop the Old Stack and Remove the Volume
```bash
docker compose down
docker compose down -v
```
### 3. Update Your `.env`
Postgres credentials are no longer hardcoded in `docker-compose.yaml`. Add them to your `.env`:
```bash
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_DB=postgres
POSTGRES_USER=postgres
POSTGRES_PASSWORD=<your-password> # required — compose will refuse to start without it
POSTGRES_COLLECTION_NAME=memories
```
<Info>
`POSTGRES_PASSWORD` is **required** — `docker compose up` will refuse to start without it. If you previously relied on the hardcoded default, set `POSTGRES_PASSWORD=postgres`.
</Info>
### 4. Start Only Postgres
Start **only** the Postgres container first — do **not** start the mem0 API yet.
The API runs `alembic upgrade head` on startup, which creates empty tables that
would conflict with the restore.
```bash
docker compose up -d postgres
```
Wait for Postgres to become healthy:
```bash
docker compose exec -T postgres pg_isready -q && echo "ready" || echo "not ready"
```
### 5. Restore Your Data
```bash
docker compose exec -T postgres psql -U postgres < mem0_backup.sql
```
You may see notices like `role "postgres" already exists` — these are safe to ignore.
<Warning>
You must restore **before** starting the mem0 API container. The API runs
database migrations on startup which create empty tables — restoring after
that would fail with duplicate-key errors and lose your API keys and settings.
</Warning>
### 6. Start the API
Now start the mem0 API container. Alembic will detect the existing tables and
only apply any new migrations:
```bash
docker compose up -d mem0
```
### 7. Verify
```bash
# Check service health
cd server && make health
# Confirm memories are accessible
curl -s http://localhost:8888/memories?user_id=<your-user-id> \
-H "X-API-Key: <your-api-key>"
```
## Rollback
If something goes wrong, revert the image tag in `docker-compose.yaml`:
```yaml
postgres:
image: ankane/pgvector:v0.5.1
```
Then destroy the new volume, start the old image, and restore from your backup:
```bash
docker compose down -v
docker compose up -d --build
docker compose exec -T postgres psql -U postgres < mem0_backup.sql
```
## Need Help?
- Join our [Discord community](https://mem0.ai/discord) for real-time support
- Open an issue on [GitHub](https://github.com/mem0ai/mem0/issues)
+14
View File
@@ -226,6 +226,20 @@ curl -X POST http://localhost:8888/search \
}' }'
``` ```
Set `explain` to inspect the scoring signals used by OSS hybrid search:
```bash
curl -X POST http://localhost:8888/search \
-H "Content-Type: application/json" \
-d '{
"query": "vegetable pizza",
"user_id": "alice",
"explain": true
}'
```
Each returned memory includes `score_details` only when explanation mode is enabled.
### Explore with OpenAPI docs ### Explore with OpenAPI docs
1. Navigate to `http://localhost:8888/docs` (Compose) or `http://localhost:8000/docs` (raw Docker / uvicorn). 1. Navigate to `http://localhost:8888/docs` (Compose) or `http://localhost:8000/docs` (raw Docker / uvicorn).
+1 -1
View File
@@ -26,7 +26,7 @@
"ai": "^4.1.46", "ai": "^4.1.46",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.6",
"lucide-react": "^0.477.0", "lucide-react": "^0.477.0",
"next": "15.5.18", "next": "15.5.18",
"react": "^19.0.0", "react": "^19.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "mem0", "name": "mem0",
"version": "0.2.8", "version": "0.2.9",
"description": "Persistent memory for Claude Code. Remembers decisions, patterns, and preferences across sessions.", "description": "Persistent memory for Claude Code. Remembers decisions, patterns, and preferences across sessions.",
"author": { "author": {
"name": "Mem0", "name": "Mem0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "mem0", "name": "mem0",
"version": "0.2.8", "version": "0.2.9",
"description": "Persistent memory for Codex. Remembers decisions, patterns, and preferences across sessions.", "description": "Persistent memory for Codex. Remembers decisions, patterns, and preferences across sessions.",
"author": { "author": {
"name": "Mem0", "name": "Mem0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "mem0", "name": "mem0",
"version": "0.2.8", "version": "0.2.9",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search using the Mem0 Platform MCP server.", "description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search using the Mem0 Platform MCP server.",
"author": { "author": {
"name": "Mem0", "name": "Mem0",
+47
View File
@@ -0,0 +1,47 @@
# Changelog
All notable changes to the `@mem0/opencode-plugin` will be documented in this file.
## 0.1.3 — File-context injection, session summaries & activity timeline
### Added
- **File-context injection (`tool.execute.before` / Read):** Before the agent reads a file, the plugin searches mem0 for memories referencing that file path and injects prior work as system context. Gates on file size (>= 1,500 bytes). Gives the agent "I've worked on this file before" awareness automatically.
- **Stop hook session summary (`experimental.session.compacting`):** Enhanced session compaction to store a structured `session_summary` memory with `infer=True`, letting the mem0 backend AI extract key facts (request, decisions, learnings, next steps). Previously only stored a raw stats string.
- **SessionStart activity timeline:** The initial memory loading now formats recent memories with type icons (⚖️ decision, 🔴 bug_fix, 🔵 task_learning, etc.) and relative age indicators (2h ago, 1d ago) instead of bare text. Provides a visual "Recent Activity" timeline on first message.
### Changed
- **`experimental.session.compacting` handler:** Now stores `metadata.type=session_summary` with `metadata.source=opencode-stop` instead of `metadata.type=session_state` with `metadata.source=pre-compaction`. Includes a structured prompt that instructs mem0's AI to extract request, decisions, learnings, and next steps.
- **Initial context formatting:** Memories shown on first message now include type icons and age labels for quick scanning.
## 0.1.2 — Automatic coding categories & global search
### Added
- **Auto-configured coding categories:** The plugin now automatically sets up 17 coding categories (e.g. `architecture_decisions`, `api_design`, `security`, `debugging_notes`) on the Mem0 project at startup. Runs in the background on every session start via `autoSetupCategories()`, is fully idempotent, and never blocks initialization. Uses SHA-256 fingerprints of the category list and API key — stored in `~/.mem0/categories_setup.json` — to skip redundant API calls on subsequent sessions.
- **Global search mode (`global_search` setting):** New `global_search` toggle in `~/.mem0/settings.json` (default: `false`). When enabled, all `search_memories` and `get_memories` calls use `{"OR": [{"user_id": "*"}]}` instead of the per-user per-project `AND` filter — returning all memories across all users and all `app_id` scopes. Writes (`add_memory`) still tag with the current `user_id` and `app_id`. Applies to all plugin search paths: initial load, per-message recall, resume detection, error-pattern lookup, and compaction context.
- **`/mem0:switch-project --global` / `--no-global`:** Enables or disables global search via the switch-project skill. Persists to `~/.mem0/settings.json`. No manual config editing needed.
- **`MEM0_GLOBAL_SEARCH` environment variable:** Exported to child shells via the `shell.env` hook (`"true"` or `"false"`).
### Changed
- **Search filters are now dynamic:** All search paths throughout the plugin construct filters based on the `global_search` setting instead of always using `AND [user_id, app_id]`.
- **Resume-context searches broadened:** Resume and error-pattern searches no longer include `metadata.type` sub-filters (`session_state`, `decision`, `anti_pattern`, `bug_fix`), broadening recall.
- **System context message updated:** Informs the model when global search is active (`"Global search is ON — searches return all memories across all users and projects. Writes still use user_id=..., app_id=..."`).
- **`/mem0:onboard` Step 5 is no longer interactive:** Removed the manual category installation prompt. Categories now configure automatically in the background; the onboarding step only verifies status and stores a fallback `project_profile` memory if the background run hasn't finished yet.
- **`/mem0:switch-project` skill expanded:** Description and execution updated to document the `--global` and `--no-global` flags alongside the existing project-name argument.
## 0.1.1
- CI/CD publish flow test (`#5288`).
- Fixed tsconfig, added `publishConfig` and bun lockfile (`#5273`).
- Renamed package to `@mem0/opencode-plugin` (`#5272`).
- Added plugin array to bundled `opencode.json` (`#5271`).
## 0.1.0 — Initial release
- **OpenCode plugin** (`@mem0/opencode-plugin` on npm): Pure TypeScript plugin using the `mem0ai` TS SDK — no Python, no shell scripts. Hooks into all 6 OpenCode events (`chat.message`, `tool.execute.before`, `tool.execute.after`, `experimental.chat.system.transform`, `experimental.session.compacting`, `shell.env`). Features: session start memory loading, per-prompt semantic search, error pattern detection with memory lookup, resume/remember intent detection, auto-capture every 3rd message, periodic save nudges, full metadata defaults injection (confidence, source, type, session_id, files, branch), identity injection for search/get/delete filters, type-filtered error pre-fetch (anti_pattern + bug_fix), pre-compaction memory capture, MEMORY.md write blocking, and secret redaction.
- **16 OpenCode-native skills** bundled in `opencode-skills/`: `context-loader`, `dream`, `export`, `forget`, `health`, `import`, `list-projects`, `mem0` (SDK reference), `memory-reviewer`, `onboard`, `peek`, `pin`, `remember`, `stats`, `switch-project`, `tour`. All skills are pure MCP-tool-based — no Python scripts, no shell scripts, no Claude Code dependencies.
- **Auto-install skills and commands (`installSkills()`):** On plugin load, copies all 16 skills to `.opencode/skills/` and creates command wrapper files in `.opencode/commands/` so they appear in the OpenCode `/` palette.
- **CLI installer (`cli.ts`):** `bunx @mem0/opencode-plugin install` auto-configures plugin and MCP server in `~/.config/opencode/opencode.json`.
+87 -6
View File
@@ -66,6 +66,38 @@ function redact(text: string): string {
return out; return out;
} }
function formatAge(createdAt: string): string {
try {
const dt = new Date(createdAt);
const now = Date.now();
const seconds = Math.floor((now - dt.getTime()) / 1000);
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
const days = Math.floor(seconds / 86400);
if (days === 1) return "1d ago";
if (days < 30) return `${days}d ago`;
return `${Math.floor(days / 30)}mo ago`;
} catch {
return "";
}
}
const TYPE_ICONS: Record<string, string> = {
decision: "⚖️",
anti_pattern: "🔴",
bug_fix: "🔴",
convention: "🔄",
task_learning: "🔵",
user_preference: "🟣",
session_summary: "📋",
session_state: "📋",
project_profile: "📖",
compact_summary: "📋",
auto_capture: "✅",
};
const FILE_READ_GATE_MIN_BYTES = 1500;
function loadGlobalSearch(): boolean { function loadGlobalSearch(): boolean {
try { try {
const settingsPath = join(homedir(), ".mem0", "settings.json"); const settingsPath = join(homedir(), ".mem0", "settings.json");
@@ -308,9 +340,16 @@ const Mem0Plugin: Plugin = async (ctx) => {
const memories = extractMemories(res); const memories = extractMemories(res);
if (memories.length > 0) { if (memories.length > 0) {
const memLines = memories const memLines = memories
.map((m) => `- ${m.memory}`) .map((m) => {
const meta = (m as any).metadata ?? {};
const cat = meta.type ?? "unknown";
const icon = TYPE_ICONS[cat] ?? "❓";
const age = (m as any).created_at ? formatAge((m as any).created_at) : "";
const ageStr = age ? ` (${age})` : "";
return `- ${icon} [${cat}]${ageStr} ${m.memory.slice(0, 120)}`;
})
.join("\n"); .join("\n");
systemContext.push(`Prior context from mem0:\n${memLines}`); systemContext.push(`### Recent Activity\n\n${memLines}`);
} }
} catch {} } catch {}
} }
@@ -445,6 +484,41 @@ const Mem0Plugin: Plugin = async (ctx) => {
"tool.execute.before": async (input: any, output: any) => { "tool.execute.before": async (input: any, output: any) => {
const toolName: string = input?.tool ?? ""; const toolName: string = input?.tool ?? "";
// File-context injection: before reading a file, search mem0 for prior work on it
if (toolName === "read" || toolName === "Read") {
const filePath = String(output?.args?.file_path ?? output?.args?.filePath ?? "");
if (filePath && filePath.length > 0) {
try {
const absPath = filePath.startsWith("/") ? filePath : resolve(process.cwd(), filePath);
const { statSync } = await import("fs");
const stat = statSync(absPath);
if (stat.isFile() && stat.size >= FILE_READ_GATE_MIN_BYTES) {
const searchFilters = globalSearch
? { OR: [{ user_id: "*" }] }
: { AND: [{ user_id: userId }, { app_id: appId }] };
const relPath = filePath.startsWith("/")
? filePath.replace(process.cwd() + "/", "")
: filePath;
const res = await mem0.search(relPath, {
filters: searchFilters,
topK: 5,
});
stats.searches++;
const memories = extractMemories(res);
if (memories.length > 0) {
const lines = memories.map((m) => {
const text = m.memory.slice(0, 150).replace(/\n/g, " ");
return `- ${text} [mem0:${m.id.slice(0, 8)}]`;
});
systemContext.push(
`Prior work on \`${relPath}\`:\n${lines.join("\n")}`,
);
}
}
} catch {}
}
}
if (WRITE_TOOLS.has(toolName)) { if (WRITE_TOOLS.has(toolName)) {
const fp = String( const fp = String(
output?.args?.file_path ?? output?.args?.filePath ?? "", output?.args?.file_path ?? output?.args?.filePath ?? "",
@@ -611,15 +685,22 @@ const Mem0Plugin: Plugin = async (ctx) => {
) => { ) => {
try { try {
const compactSessionId = input?.sessionID ?? sessionId; const compactSessionId = input?.sessionID ?? sessionId;
const summaryContent = `Session compacting. Project: ${appId}. Branch: ${branch}. Session: ${compactSessionId}. Stats: ${stats.adds} memories stored, ${stats.searches} searches, ${stats.messages} messages.`;
// Session summary capture: store a structured summary of the session
const summaryPrompt = [
`Session summary for project ${appId} (branch: ${branch}).`,
`Session: ${compactSessionId}.`,
`Stats: ${stats.adds} memories stored, ${stats.searches} searches, ${stats.messages} messages.`,
`Extract and remember: what was requested, what was investigated, key decisions made, what was completed, and what needs to happen next.`,
].join(" ");
Promise.resolve().then(async () => { Promise.resolve().then(async () => {
try { try {
await mem0.add([{ role: "user", content: summaryContent }], { await mem0.add([{ role: "user", content: summaryPrompt }], {
user_id: userId, user_id: userId,
app_id: appId, app_id: appId,
metadata: { metadata: {
type: "session_state", type: "session_summary",
source: "pre-compaction", source: "opencode-stop",
session_id: compactSessionId, session_id: compactSessionId,
branch, branch,
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@mem0/opencode-plugin", "name": "@mem0/opencode-plugin",
"version": "0.1.2", "version": "0.1.3",
"type": "module", "type": "module",
"description": "Mem0 persistent memory plugin for OpenCode — add, search, and manage memories across sessions", "description": "Mem0 persistent memory plugin for OpenCode — add, search, and manage memories across sessions",
"main": "dist/index.js", "main": "dist/index.js",
+20 -9
View File
@@ -2,11 +2,29 @@
All notable changes to the Mem0 plugin will be documented in this file. All notable changes to the Mem0 plugin will be documented in this file.
## 0.2.9 — File-context injection, session summaries & activity timeline
### Added
- **File-context injection (`PreToolUse/Read` hook):** Before Claude reads a file, the new `on_file_read.sh` hook searches mem0 for memories that reference that file path and injects a compact timeline of prior work as `additionalContext`. Gives Claude "I've seen this file before and here's what I remember" context automatically. Gates on file size (>= 1,500 bytes), 5-second hard timeout, silent skip on any failure. Applies to Claude Code, Codex, and Cursor (`on_file_read_cursor.sh`).
- **Stop hook session summary (`on_stop.sh`):** On session end, parses the transcript JSONL, extracts the last assistant message and files touched, builds a structured prompt, and stores it via the mem0 API with `infer=True` — letting the platform's backend AI extract structured facts (request, decisions, learnings, next steps). Memories are stored as `metadata.type=session_summary` with 90-day expiry. Guards: skips subagent sessions (`agent_id` present), dedup via marker file, always exits 0. Applies to Claude Code, Codex, and Cursor (`on_stop_cursor.sh`).
- **SessionStart activity timeline (`session_timeline.py`):** On startup (when project has existing memories), fetches the 10 most recent memories and renders a compact timeline with type icons, age indicators, and short text below the existing banner. Shows recent decisions, bug fixes, and session summaries at a glance. 5-second timeout — if the API is slow, the timeline silently skips while the banner still displays.
- **`scripts/file_context.py`:** Core Python module for file-context injection. Searches mem0 cloud API with both relative and absolute file paths, deduplicates results, formats as compact timeline with type icons and memory age.
- **`scripts/capture_session_summary.py`:** Core Python module for Stop hook. Reads transcript JSONL (tail 3,000 lines), extracts last assistant message, extracts file paths from tool_input fields, strips system tags and `<private>` blocks, stores via mem0 API.
- **`scripts/session_timeline.py`:** Core Python module for SessionStart timeline. Fetches recent memories from mem0 API, formats with type icons, relative age, and short text.
### Changed
- **`on_session_start.sh`:** On startup (when memories > 0), calls `session_timeline.py` to inject a compact recent activity timeline below the existing banner and rubric instructions.
- **`hooks/hooks.json`:** Added `PreToolUse` matcher for `Read` (5s timeout) and `Stop` hook (30s timeout).
- **`hooks/codex-hooks.json`:** Added `PreToolUse` matcher for `Read` (5s timeout) and `Stop` hook (30s timeout).
- **`hooks/cursor-hooks.json`:** Added `preToolUse` matcher for `Read` (5s timeout) and `stop` hook (30s timeout).
## 0.2.8 — Automatic coding categories & global search ## 0.2.8 — Automatic coding categories & global search
### Added ### Added
- **Global search mode (`global_search` setting):** New `global_search` toggle in `~/.mem0/settings.json` (default: `false`). When enabled, `search_memories` and `get_memories` calls use `{"OR": [{"user_id": "*"}]}` instead of the per-user per-project `AND` filter — returning all memories across all users and all `app_id` scopes in the platform project. Writes (`add_memory`) still tag with the current `user_id` and `app_id`. Solves the team-shared-memory use case where multiple team members need access to all memories regardless of which repo or user created them. Works on Claude Code, Cursor, and Codex (not OpenCode, which has its own TypeScript identity logic). - **Global search mode (`global_search` setting):** New `global_search` toggle in `~/.mem0/settings.json` (default: `false`). When enabled, `search_memories` and `get_memories` calls use `{"OR": [{"user_id": "*"}]}` instead of the per-user per-project `AND` filter — returning all memories across all users and all `app_id` scopes in the platform project. Writes (`add_memory`) still tag with the current `user_id` and `app_id`. Solves the team-shared-memory use case where multiple team members need access to all memories regardless of which repo or user created them. Works on Claude Code, Cursor, and Codex.
- **`/mem0:switch-project --global` / `--no-global`:** Enables or disables global search via the switch-project skill. Persists to `~/.mem0/settings.json`. No manual config editing needed. - **`/mem0:switch-project --global` / `--no-global`:** Enables or disables global search via the switch-project skill. Persists to `~/.mem0/settings.json`. No manual config editing needed.
- **Session banner scope indicator:** Banner shows `scope=global` when global search is active instead of `project=<app_id>`. - **Session banner scope indicator:** Banner shows `scope=global` when global search is active instead of `project=<app_id>`.
- **Global-aware memory count:** Session start memory count query uses the global filter when `global_search` is enabled. - **Global-aware memory count:** Session start memory count query uses the global filter when `global_search` is enabled.
@@ -20,17 +38,10 @@ All notable changes to the Mem0 plugin will be documented in this file.
- **`/mem0:onboard` Step 5 is no longer interactive:** Removed the `Install coding categories? [Y/n]` prompt. Categories now configure automatically in the background; the onboarding step only verifies status and applies them if the background run hasn't finished yet — mirroring how Step 4 (project-file import) already works. - **`/mem0:onboard` Step 5 is no longer interactive:** Removed the `Install coding categories? [Y/n]` prompt. Categories now configure automatically in the background; the onboarding step only verifies status and applies them if the background run hasn't finished yet — mirroring how Step 4 (project-file import) already works.
- **Session-start "new project" hint** now notes that coding categories install automatically in the background. - **Session-start "new project" hint** now notes that coding categories install automatically in the background.
## 0.1.0 — OpenCode & Antigravity ## 0.1.0 — Antigravity
### Added ### Added
- **OpenCode plugin** (`@mem0/opencode-plugin` on npm): Pure TypeScript plugin using the `mem0ai` TS SDK — no Python, no shell scripts. Hooks into all 6 OpenCode events (`chat.message`, `tool.execute.before`, `tool.execute.after`, `experimental.chat.system.transform`, `experimental.session.compacting`, `shell.env`). Features: session start memory loading, per-prompt semantic search, error pattern detection with memory lookup, resume/remember intent detection, auto-capture every 3rd message, periodic save nudges, full metadata defaults injection (confidence, source, type, session_id, files, branch), identity injection for search/get/delete filters, type-filtered error pre-fetch (anti_pattern + bug_fix), pre-compaction memory capture, MEMORY.md write blocking, and secret redaction.
- **16 OpenCode-native skills** bundled in `opencode-skills/`: `context-loader`, `dream`, `export`, `forget`, `health`, `import`, `list-projects`, `mem0` (SDK reference), `memory-reviewer`, `onboard`, `peek`, `pin`, `remember`, `stats`, `switch-project`, `tour`. All skills are pure MCP-tool-based — no Python scripts, no shell scripts, no Claude Code dependencies.
- **Auto-install skills and commands (`installSkills()`):** On plugin load, copies all 16 skills to `.opencode/skills/` and creates command wrapper files in `.opencode/commands/` so they appear in the OpenCode `/` palette. No manual setup needed.
- **`extractUserText()` handler:** Robust text extraction from OpenCode response shapes — handles `parts[]` array, `content[]` array, `message.content`, and plain string responses.
- **Identity resolution:** `getUserId()` uses `os.userInfo().username` (matching Claude Code's `${USER}` convention) with `MEM0_USER_ID` env override. `getProjectId()` uses git remote with `MEM0_APP_ID` env override.
- **Context injection via `experimental.chat.system.transform`:** All memory context (session start memories, per-prompt search results, error-related memories, compaction context) injected as system context.
- **CLI installer (`cli.ts`):** `bunx @mem0/opencode-plugin install` auto-configures plugin and MCP server in `~/.config/opencode/opencode.json`.
- **Antigravity plugin** (`.antigravity/`): Restructured to follow the same shared-infrastructure pattern as Claude Code, Cursor, and Codex. Self-contained plugin directory with `plugin.json`, `mcp_config.json`, `hooks/hooks.json` (own file), `scripts/` (symlink → `../scripts/`), and `skills/` (symlink → `../skills/`). Installable via `agy plugin install .antigravity` or `npx degit mem0ai/mem0/mem0-plugin/.antigravity ~/.gemini/config/plugins/mem0`. Uses `contextFileName: "AGENTS.md"` per Antigravity convention. - **Antigravity plugin** (`.antigravity/`): Restructured to follow the same shared-infrastructure pattern as Claude Code, Cursor, and Codex. Self-contained plugin directory with `plugin.json`, `mcp_config.json`, `hooks/hooks.json` (own file), `scripts/` (symlink → `../scripts/`), and `skills/` (symlink → `../skills/`). Installable via `agy plugin install .antigravity` or `npx degit mem0ai/mem0/mem0-plugin/.antigravity ~/.gemini/config/plugins/mem0`. Uses `contextFileName: "AGENTS.md"` per Antigravity convention.
- **Codex hooks parity:** Added missing `PreToolUse` Write/Edit/MultiEdit block and `PreCompact` hook to Codex hooks config, bringing it to full parity with Claude Code. - **Codex hooks parity:** Added missing `PreToolUse` Write/Edit/MultiEdit block and `PreCompact` hook to Codex hooks config, bringing it to full parity with Claude Code.
+23
View File
@@ -51,6 +51,29 @@
"timeout": 3 "timeout": 3
} }
] ]
},
{
"matcher": "Read",
"hooks": [
{
"name": "mem0-file-context",
"type": "command",
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/on_file_read.sh",
"timeout": 5
}
]
}
],
"Stop": [
{
"hooks": [
{
"name": "mem0-session-summary",
"type": "command",
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/on_stop.sh",
"timeout": 30
}
]
} }
], ],
"PostToolUse": [ "PostToolUse": [
+21
View File
@@ -20,6 +20,16 @@
"timeout": 3 "timeout": 3
} }
] ]
},
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "${PLUGIN_ROOT}/scripts/on_file_read.sh",
"timeout": 5
}
]
} }
], ],
"SessionStart": [ "SessionStart": [
@@ -68,6 +78,17 @@
] ]
} }
], ],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${PLUGIN_ROOT}/scripts/on_stop.sh",
"timeout": 30
}
]
}
],
"PreCompact": [ "PreCompact": [
{ {
"hooks": [ "hooks": [
+11
View File
@@ -16,6 +16,11 @@
"command": "${CURSOR_PLUGIN_ROOT}/scripts/enforce_metadata_defaults.sh", "command": "${CURSOR_PLUGIN_ROOT}/scripts/enforce_metadata_defaults.sh",
"matcher": "mcp__mem0__add_memory|mcp__plugin_mem0_mem0__add_memory|mcp__mem0__search_memories|mcp__plugin_mem0_mem0__search_memories|mcp__mem0__get_memories|mcp__plugin_mem0_mem0__get_memories|mcp__mem0__delete_all_memories|mcp__plugin_mem0_mem0__delete_all_memories", "matcher": "mcp__mem0__add_memory|mcp__plugin_mem0_mem0__add_memory|mcp__mem0__search_memories|mcp__plugin_mem0_mem0__search_memories|mcp__mem0__get_memories|mcp__plugin_mem0_mem0__get_memories|mcp__mem0__delete_all_memories|mcp__plugin_mem0_mem0__delete_all_memories",
"timeout": 3 "timeout": 3
},
{
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_file_read_cursor.sh",
"matcher": "Read",
"timeout": 5
} }
], ],
"postToolUse": [ "postToolUse": [
@@ -30,6 +35,12 @@
"timeout": 12 "timeout": 12
} }
], ],
"stop": [
{
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_stop_cursor.sh",
"timeout": 30
}
],
"preCompact": [ "preCompact": [
{ {
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_pre_compact_cursor.sh" "command": "${CURSOR_PLUGIN_ROOT}/scripts/on_pre_compact_cursor.sh"
+21
View File
@@ -54,6 +54,16 @@
"timeout": 3 "timeout": 3
} }
] ]
},
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_file_read.sh",
"timeout": 5
}
]
} }
], ],
"PostToolUse": [ "PostToolUse": [
@@ -78,6 +88,17 @@
] ]
} }
], ],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_stop.sh",
"timeout": 30
}
]
}
],
"PreCompact": [ "PreCompact": [
{ {
"hooks": [ "hooks": [
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "mem0", "id": "mem0",
"name": "mem0", "name": "mem0",
"version": "0.1.0", "version": "0.1.1",
"description": "Persistent semantic memory for Antigravity agents. Cross-session, user-level recall via the Mem0 Platform MCP server. 16 slash commands, lifecycle hooks for auto-capture and metadata enforcement.", "description": "Persistent semantic memory for Antigravity agents. Cross-session, user-level recall via the Mem0 Platform MCP server. 16 slash commands, lifecycle hooks for auto-capture and metadata enforcement.",
"author": { "name": "Mem0", "email": "support@mem0.ai" }, "author": { "name": "Mem0", "email": "support@mem0.ai" },
"publisher": "mem0ai", "publisher": "mem0ai",
+51
View File
@@ -0,0 +1,51 @@
"""Shared formatting helpers for mem0 plugin hooks.
Constants and utilities used by file_context.py, session_timeline.py,
and any future hook that displays memories.
"""
from __future__ import annotations
TYPE_ICONS = {
"decision": "⚖️",
"anti_pattern": "\U0001f534",
"bug_fix": "\U0001f534",
"convention": "\U0001f504",
"task_learning": "\U0001f535",
"user_preference": "\U0001f7e3",
"session_summary": "\U0001f4cb",
"session_state": "\U0001f4cb",
"project_profile": "\U0001f4d6",
"compact_summary": "\U0001f4cb",
"auto_capture": "✅",
"environmental": "🌐",
"health_check": "🩺",
}
def format_age(memory: dict) -> str:
"""Format how long ago a memory was created, e.g. '2h ago', '3d ago'."""
created = memory.get("created_at", "")
if not created:
return ""
try:
from datetime import datetime, timezone
if created.endswith("Z"):
created = created[:-1] + "+00:00"
dt = datetime.fromisoformat(created)
now = datetime.now(timezone.utc)
delta = now - dt
seconds = int(delta.total_seconds())
if seconds < 3600:
return f"{seconds // 60}m ago"
if seconds < 86400:
return f"{seconds // 3600}h ago"
days = seconds // 86400
if days == 1:
return "1d ago"
if days < 30:
return f"{days}d ago"
return f"{days // 30}mo ago"
except Exception:
return ""
+12 -7
View File
@@ -37,23 +37,28 @@ def search_memories(
min_score: float = 0.0, min_score: float = 0.0,
rerank: bool = False, rerank: bool = False,
threshold: float = 0.3, threshold: float = 0.3,
global_search: bool = False,
) -> list[dict]: ) -> list[dict]:
if not api_key: if not api_key:
return [] return []
base_clauses: list[dict] = [{"user_id": user_id}, {"app_id": project_id}] if global_search:
if metadata_type: filters: dict = {"OR": [{"user_id": "*"}]}
base_clauses.append({"metadata": {"type": metadata_type}}) else:
if metadata_filters: base_clauses: list[dict] = [{"user_id": user_id}, {"app_id": project_id}]
for key, value in metadata_filters.items(): if metadata_type:
base_clauses.append({"metadata": {key: value}}) base_clauses.append({"metadata": {"type": metadata_type}})
if metadata_filters:
for key, value in metadata_filters.items():
base_clauses.append({"metadata": {key: value}})
filters = {"AND": base_clauses}
base_payload: dict = {"query": query, "top_k": top_k, "threshold": threshold} base_payload: dict = {"query": query, "top_k": top_k, "threshold": threshold}
if rerank: if rerank:
base_payload["rerank"] = True base_payload["rerank"] = True
try: try:
payload = {**base_payload, "filters": {"AND": list(base_clauses)}} payload = {**base_payload, "filters": filters}
results = _do_search(api_key, payload)[:top_k] results = _do_search(api_key, payload)[:top_k]
if min_score > 0: if min_score > 0:
@@ -0,0 +1,264 @@
#!/usr/bin/env python3
"""Capture a structured session summary on Stop hook.
Runs on every Stop (end of each assistant turn). Each invocation reads
the transcript JSONL, extracts the latest assistant message and all
files touched so far, then stores via mem0 API with infer=True. Uses
run_id=session_id to scope infer dedup to the session, so the final
stored summary reflects the most recent turn — not just the first.
Input: JSON on stdin with transcript_path, session_id, cwd, agent_id
Output: stderr logs only (exit 0 always — must not block)
"""
from __future__ import annotations
import json
import logging
import os
import re
import sys
import urllib.error
import urllib.request
from datetime import date, timedelta
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _identity import resolve_api_key, resolve_user_id
from _project import resolve_branch, resolve_project_id
log = logging.getLogger("mem0-session-summary")
log.setLevel(logging.DEBUG)
_handler = logging.StreamHandler(sys.stderr)
_handler.setFormatter(logging.Formatter("[mem0-session-summary] %(message)s"))
log.addHandler(_handler)
if os.environ.get("MEM0_DEBUG"):
_log_dir = os.path.expanduser("~/.mem0")
try:
os.makedirs(_log_dir, exist_ok=True)
_fh = logging.FileHandler(os.path.join(_log_dir, "hooks.log"))
_fh.setFormatter(logging.Formatter("[mem0-session-summary] %(asctime)s %(message)s"))
log.addHandler(_fh)
except OSError:
pass
API_URL = "https://api.mem0.ai"
MAX_TAIL_LINES = 3000
MAX_SUMMARY_CHARS = 50000
SUMMARY_EXPIRY_DAYS = 90
SYSTEM_TAG_RE = re.compile(
r"<(?:system-reminder|private|claude-mem-context|persisted-output|system_instruction)>"
r".*?"
r"</(?:system-reminder|private|claude-mem-context|persisted-output|system_instruction)>",
re.DOTALL,
)
def tail_lines(filepath: str, n: int) -> list[str]:
try:
with open(filepath, "rb") as f:
f.seek(0, 2)
file_size = f.tell()
if file_size == 0:
return []
chunk_size = min(file_size, n * 4096)
f.seek(max(0, file_size - chunk_size))
data = f.read().decode("utf-8", errors="replace")
return data.splitlines()[-n:]
except OSError:
return []
def extract_last_assistant_message(lines: list[str]) -> str:
"""Walk transcript backwards, return text content of the last assistant message."""
for line in reversed(lines):
line = line.strip()
if not line:
continue
if '"type":"assistant"' not in line and '"type": "assistant"' not in line:
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
if entry.get("type") != "assistant":
continue
message = entry.get("message", {})
content = message.get("content", [])
if isinstance(content, str):
return content
if isinstance(content, list):
parts = []
for block in content:
if isinstance(block, str):
parts.append(block)
elif isinstance(block, dict) and block.get("type") == "text":
parts.append(block.get("text", ""))
return "\n".join(parts).strip()
return ""
def extract_files_touched(lines: list[str]) -> list[str]:
"""Extract unique file paths from tool_use content blocks in transcript."""
files = set()
file_ext_re = re.compile(
r"[a-zA-Z0-9_./-]+\.(?:py|ts|tsx|js|jsx|rs|go|rb|java|sh|yaml|yml|json|toml|md|sql|css|html)"
)
for line in lines:
line = line.strip()
if not line:
continue
if '"tool_use"' not in line and '"file_path"' not in line:
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
content = entry.get("message", {}).get("content", [])
if not isinstance(content, list):
continue
for block in content:
if not isinstance(block, dict) or block.get("type") != "tool_use":
continue
inp = block.get("input", {})
if not isinstance(inp, dict):
continue
fp = inp.get("file_path", "")
if fp:
files.add(fp)
command = inp.get("command", "")
if command:
for match in file_ext_re.findall(command):
files.add(match)
return sorted(files)[:20]
def strip_tags(text: str) -> str:
return SYSTEM_TAG_RE.sub("", text).strip()
def build_summary_prompt(assistant_msg: str, files: list[str]) -> str:
"""Build a structured prompt that helps mem0's AI extract a good summary."""
files_section = ""
if files:
file_list = ", ".join(files[:10])
files_section = f"\n\nFiles touched during this session: {file_list}"
return (
f"Session summary — store the following as a structured session summary.\n\n"
f"What the assistant accomplished in this session:\n"
f"{assistant_msg[:MAX_SUMMARY_CHARS]}"
f"{files_section}\n\n"
f"Extract and remember: what was requested, what was investigated, "
f"key decisions made, what was completed, and what needs to happen next."
)
def store_summary(
api_key: str,
summary_prompt: str,
user_id: str,
session_id: str,
project_id: str,
branch: str,
files: list[str],
) -> bool:
expires = (date.today() + timedelta(days=SUMMARY_EXPIRY_DAYS)).isoformat()
metadata = {
"type": "session_summary",
"source": "stop-hook",
"session_id": session_id,
}
if branch:
metadata["branch"] = branch
if files:
metadata["files_touched"] = json.dumps(files[:20])
body = {
"messages": [{"role": "user", "content": summary_prompt}],
"user_id": user_id,
"app_id": project_id,
"run_id": session_id,
"metadata": metadata,
"infer": True,
"expiration_date": expires,
}
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(
f"{API_URL}/v3/memories/add/",
data=data,
headers={
"Content-Type": "application/json",
"Authorization": f"Token {api_key}",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
if resp.status in (200, 201):
log.info("Session summary stored")
return True
log.warning("API returned status %d", resp.status)
return False
except urllib.error.URLError as e:
log.warning("API call failed: %s", e)
return False
def main():
api_key = resolve_api_key()
if not api_key:
log.debug("MEM0_API_KEY not set, skipping")
return
try:
hook_input = json.loads(sys.stdin.read())
except (json.JSONDecodeError, OSError):
log.debug("No valid JSON on stdin")
return
# Guard: skip subagent sessions (only root sessions get summaries)
agent_id = hook_input.get("agent_id", "")
if agent_id:
log.debug("Subagent session (agent_id=%s), skipping", agent_id)
return
transcript_path = hook_input.get("transcript_path", "")
if not transcript_path:
log.debug("No transcript_path provided")
return
session_id = hook_input.get("session_id", "")
cwd = hook_input.get("cwd") or None
user_id = resolve_user_id()
project_id = resolve_project_id(cwd)
branch = resolve_branch(cwd)
lines = tail_lines(transcript_path, MAX_TAIL_LINES)
if not lines:
log.debug("Transcript empty or unreadable: %s", transcript_path)
return
assistant_msg = extract_last_assistant_message(lines)
if not assistant_msg or len(assistant_msg.strip()) < 100:
log.debug("Assistant message too short (%d chars) — skipping", len(assistant_msg.strip()))
return
assistant_msg = strip_tags(assistant_msg)
files = extract_files_touched(lines)
summary_prompt = build_summary_prompt(assistant_msg, files)
log.info("Capturing session summary (%d chars, %d files)", len(assistant_msg), len(files))
store_summary(api_key, summary_prompt, user_id, session_id, project_id, branch, files)
if __name__ == "__main__":
try:
main()
except Exception as e:
log.error("Unexpected error: %s", e)
sys.exit(0)
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""File-context injection for PreToolUse/Read hook.
When Claude is about to read a file, this script searches mem0 for
memories that reference that file path and returns a compact timeline
of prior work. This gives Claude context like "last time you fixed a
null pointer here" before it reads the file.
Modeled after claude-mem's file-context handler but adapted for mem0's
cloud API architecture.
Input: file_path (positional arg), env vars for identity
Output: JSON to stdout with hookSpecificOutput.additionalContext
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _formatting import TYPE_ICONS, format_age
from _identity import resolve_api_key, resolve_user_id
from _project import resolve_project_id
from _search import search_memories
FILE_READ_GATE_MIN_BYTES = 1500
MAX_RESULTS = 5
SEARCH_TIMEOUT = 5
def gate_file(file_path: str, cwd: str) -> str | None:
"""Return the resolved absolute path if the file passes gating, else None."""
if not file_path:
return None
p = Path(file_path)
if not p.is_absolute():
p = Path(cwd) / p
try:
p = p.resolve()
if not p.is_file():
return None
if p.stat().st_size < FILE_READ_GATE_MIN_BYTES:
return None
return str(p)
except OSError:
return None
def relative_path(abs_path: str, cwd: str) -> str:
try:
return os.path.relpath(abs_path, cwd)
except ValueError:
return abs_path
def format_timeline(memories: list[dict], file_path: str) -> str:
"""Format memories into a compact timeline for context injection."""
if not memories:
return ""
rel = file_path
lines = [
f"Prior work on `{rel}` — {len(memories)} memories found.",
"Need details? Use `search_memories` with the memory ID.",
"",
]
for m in memories:
mid = m.get("id", "?")[:8]
text = (m.get("memory", "") or "")[:150].replace("\n", " ").strip()
meta = m.get("metadata") or {}
cat = meta.get("type", "unknown")
icon = TYPE_ICONS.get(cat, "❓")
age = format_age(m)
age_str = f" ({age})" if age else ""
lines.append(f"- {icon} [{cat}]{age_str} {text} [mem0:{mid}]")
return "\n".join(lines)
def search_file_context(
api_key: str, user_id: str, project_id: str, file_path: str, cwd: str
) -> str:
"""Search mem0 for memories related to a file path."""
global_search = os.environ.get("MEM0_GLOBAL_SEARCH", "false") == "true"
rel = relative_path(file_path, cwd)
basename = os.path.basename(file_path)
query = f"{rel} {basename}" if rel != basename else rel
results = search_memories(
api_key, user_id, project_id, query,
top_k=MAX_RESULTS, threshold=0.3,
global_search=global_search,
)
results = results[:MAX_RESULTS]
return format_timeline(results, rel)
def main():
if len(sys.argv) < 2:
sys.exit(0)
file_path = sys.argv[1]
cwd = sys.argv[2] if len(sys.argv) > 2 else os.getcwd()
api_key = resolve_api_key()
if not api_key:
sys.exit(0)
resolved = gate_file(file_path, cwd)
if not resolved:
sys.exit(0)
user_id = resolve_user_id()
project_id = resolve_project_id(cwd)
timeline = search_file_context(api_key, user_id, project_id, resolved, cwd)
if not timeline:
sys.exit(0)
print(timeline, end="")
if __name__ == "__main__":
try:
main()
except Exception:
pass
sys.exit(0)
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Hook: PreToolUse (matcher: Read)
#
# Injects prior work context before Claude reads a file. Searches mem0
# for memories referencing the file path and returns a compact timeline.
#
# Modeled after claude-mem's file-context handler, adapted for mem0 cloud API.
#
# Input: JSON on stdin with tool_name, tool_input (file_path), cwd
# Output: JSON with hookSpecificOutput.additionalContext + permissionDecision
#
# Must never block the Read — silent exit on any failure.
set -uo pipefail
INPUT=$(cat)
# Extract file path from tool_input
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null || echo "")
if [ -z "$FILE_PATH" ]; then
exit 0
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Resolve API key (covers Desktop app users who set it in shell profile)
if [ -z "${MEM0_API_KEY:-}" ]; then
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
fi
if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
fi
CWD=$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null || echo ".")
# Call the Python worker — it handles gating (file size, existence)
TIMELINE=$(python3 "$SCRIPT_DIR/file_context.py" "$FILE_PATH" "$CWD" 2>/dev/null || echo "")
if [ -z "$TIMELINE" ]; then
exit 0
fi
# Return context injection with permissionDecision: allow
jq -cn --arg ctx "$TIMELINE" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
additionalContext: $ctx,
permissionDecision: "allow"
}
}' 2>/dev/null || true
exit 0
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Hook: preToolUse (matcher: Read) — Cursor variant
#
# Same as on_file_read.sh but uses CURSOR_PLUGIN_ROOT for path resolution
# and sources Cursor-specific identity.
set -uo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null || echo "")
if [ -z "$FILE_PATH" ]; then
exit 0
fi
if [ -z "${MEM0_API_KEY:-}" ]; then
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
fi
if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CWD=$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null || echo ".")
TIMELINE=$(python3 "$SCRIPT_DIR/file_context.py" "$FILE_PATH" "$CWD" 2>/dev/null || echo "")
if [ -z "$TIMELINE" ]; then
exit 0
fi
jq -cn --arg ctx "$TIMELINE" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
additionalContext: $ctx,
permissionDecision: "allow"
}
}' 2>/dev/null || true
exit 0
+11 -1
View File
@@ -156,6 +156,14 @@ if [ "$SOURCE" = "startup" ]; then
echo "New project with 0 memories. Invoke the mem0:onboard skill to import project files. Coding categories install automatically in the background." echo "New project with 0 memories. Invoke the mem0:onboard skill to import project files. Coding categories install automatically in the background."
else else
echo "Search mem0 for recent decisions and task learnings before responding. Run 2 parallel searches: one for decision type, one for task_learning type." echo "Search mem0 for recent decisions and task learnings before responding. Run 2 parallel searches: one for decision type, one for task_learning type."
# Inject compact recent activity timeline (non-blocking, 5s timeout)
# Use perl alarm as portable timeout (macOS lacks GNU timeout)
_TIMELINE=$(MEM0_CWD="$MEM0_CWD_RESOLVED" perl -e 'alarm 5; exec @ARGV' python3 "$SCRIPT_DIR/session_timeline.py" 2>/dev/null || echo "")
if [ -n "$_TIMELINE" ]; then
echo ""
echo "$_TIMELINE"
fi
fi fi
_PROJ_KEY=$(printf '%s' "$MEM0_CWD_RESOLVED" | tr '/' '-') _PROJ_KEY=$(printf '%s' "$MEM0_CWD_RESOLVED" | tr '/' '-')
@@ -181,7 +189,9 @@ elif [ "$SOURCE" = "resume" ]; then
elif [ "$SOURCE" = "compact" ]; then elif [ "$SOURCE" = "compact" ]; then
echo "Context compacted. Search mem0 for session_state and decision memories to recover context. Run 2 parallel searches." echo "Context compacted. Search mem0 for session_state and decision memories to recover context. Run 2 parallel searches."
printf '%s' "$INPUT" | python3 "$SCRIPT_DIR/capture_compact_summary.py" 2>/dev/null & if [ "${MEM0_AUTO_SAVE:-true}" != "false" ]; then
printf '%s' "$INPUT" | python3 "$SCRIPT_DIR/capture_compact_summary.py" 2>/dev/null &
fi
fi fi
python3 "$SCRIPT_DIR/telemetry.py" session_start --source="$SOURCE" --memory_count="${MEM0_COUNT:-0}" 2>/dev/null & python3 "$SCRIPT_DIR/telemetry.py" session_start --source="$SOURCE" --memory_count="${MEM0_COUNT:-0}" 2>/dev/null &
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# Hook: Stop
#
# Captures a structured session summary when a Claude Code session ends.
# Parses the transcript, extracts the last assistant message and files
# touched, then stores via mem0 API with infer=True for AI extraction.
#
# Guards:
# - Skips subagent sessions (agent_id present)
# - Skips if no API key
# - Skips if no transcript_path
# - Dedup via marker file
#
# Input: JSON on stdin with transcript_path, session_id, agent_id, cwd
# Output: Nothing to stdout (background capture). Always exits 0.
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
INPUT=$(cat)
# Guard: skip subagent sessions
AGENT_ID=$(echo "$INPUT" | jq -r '.agent_id // ""' 2>/dev/null || echo "")
if [ -n "$AGENT_ID" ]; then
exit 0
fi
# Resolve identity if needed
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
# Honor auto_save=false in ~/.mem0/settings.json
if [ "${MEM0_AUTO_SAVE:-true}" = "false" ]; then
exit 0
fi
if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
fi
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null || echo "")
if [ -z "$TRANSCRIPT_PATH" ]; then
exit 0
fi
# Run capture in the background — fires every turn now, so avoid blocking
echo "$INPUT" | python3 "$SCRIPT_DIR/capture_session_summary.py" 2>/dev/null &
# Telemetry
python3 "$SCRIPT_DIR/telemetry.py" session_stop 2>/dev/null &
exit 0
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Hook: stop — Cursor variant
#
# Same as on_stop.sh but uses CURSOR_PLUGIN_ROOT and Cursor-specific
# identity resolution.
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
INPUT=$(cat)
AGENT_ID=$(echo "$INPUT" | jq -r '.agent_id // ""' 2>/dev/null || echo "")
if [ -n "$AGENT_ID" ]; then
exit 0
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
fi
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null || echo "")
if [ -z "$TRANSCRIPT_PATH" ]; then
exit 0
fi
echo "$INPUT" | python3 "$SCRIPT_DIR/capture_session_summary.py" 2>/dev/null || true
python3 "$SCRIPT_DIR/telemetry.py" session_stop 2>/dev/null &
exit 0
+1 -1
View File
@@ -174,7 +174,7 @@ fi
# overlapping window ensures the next batch (MSG_COUNT=6) picks up the # overlapping window ensures the next batch (MSG_COUNT=6) picks up the
# exchange that was incomplete in the previous batch. # exchange that was incomplete in the previous batch.
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null || echo "") TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null || echo "")
if [ $((MSG_COUNT % 3)) -eq 0 ] && [ "$MSG_COUNT" -gt 0 ] && [ -n "$TRANSCRIPT_PATH" ]; then if [ "${MEM0_AUTO_SAVE:-true}" != "false" ] && [ $((MSG_COUNT % 3)) -eq 0 ] && [ "$MSG_COUNT" -gt 0 ] && [ -n "$TRANSCRIPT_PATH" ]; then
python3 "$SCRIPT_DIR/auto_capture.py" "$TRANSCRIPT_PATH" 2>/dev/null & python3 "$SCRIPT_DIR/auto_capture.py" "$TRANSCRIPT_PATH" 2>/dev/null &
fi fi
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Fetch recent memories and format a compact timeline for SessionStart.
Searches mem0 cloud API for the most recent memories in the project
and formats them as a compact activity timeline injected below the
existing SessionStart banner.
Input: env vars for identity (MEM0_API_KEY, MEM0_RESOLVED_USER_ID, etc.)
Output: Compact timeline text to stdout (empty if nothing found)
"""
from __future__ import annotations
import json
import os
import sys
import urllib.request
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _formatting import TYPE_ICONS, format_age
from _identity import resolve_api_key, resolve_user_id
from _project import resolve_project_id
API_URL = "https://api.mem0.ai"
MAX_RECENT = 10
MAX_SUMMARIES = 3
FETCH_TIMEOUT = 5
def fetch_recent_memories(api_key: str, user_id: str, project_id: str) -> list[dict]:
"""Fetch the most recent memories for this project via GET list endpoint."""
global_search = os.environ.get("MEM0_GLOBAL_SEARCH", "false") == "true"
if global_search:
filters = {"OR": [{"user_id": "*"}]}
else:
filters = {"AND": [{"user_id": user_id}, {"app_id": project_id}]}
body = json.dumps({"filters": filters}).encode()
req = urllib.request.Request(
f"{API_URL}/v3/memories/?page=1&page_size={MAX_RECENT}",
data=body,
headers={
"Authorization": f"Token {api_key}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=FETCH_TIMEOUT) as r:
result = json.loads(r.read())
if isinstance(result, dict) and "results" in result:
return result["results"][:MAX_RECENT]
if isinstance(result, list):
return result[:MAX_RECENT]
return []
except Exception:
return []
def format_timeline(memories: list[dict]) -> str:
"""Format memories into a compact recent activity timeline."""
if not memories:
return ""
lines = ["### Recent Activity", ""]
for m in memories:
mid = m.get("id", "?")[:8]
text = (m.get("memory", "") or "")[:120].replace("\n", " ").strip()
meta = m.get("metadata") or {}
cat = meta.get("type", "unknown")
icon = TYPE_ICONS.get(cat, "❓")
age = format_age(m)
age_str = f" ({age})" if age else ""
lines.append(f"- {icon} [{cat}]{age_str} {text} [mem0:{mid}]")
lines.append("")
lines.append("Search mem0 for details on any of these, or for past decisions and task learnings relevant to the current task.")
return "\n".join(lines)
def main():
api_key = resolve_api_key()
if not api_key:
return
user_id = resolve_user_id()
project_id = resolve_project_id(os.environ.get("MEM0_CWD"))
memories = fetch_recent_memories(api_key, user_id, project_id)
if not memories:
return
timeline = format_timeline(memories)
if timeline:
print(timeline, end="")
if __name__ == "__main__":
try:
main()
except Exception:
pass
sys.exit(0)
+18 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "mem0ai", "name": "mem0ai",
"version": "3.0.5", "version": "3.0.6",
"description": "The Memory Layer For Your AI Apps", "description": "The Memory Layer For Your AI Apps",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.mjs", "module": "./dist/index.mjs",
@@ -100,7 +100,7 @@
"typescript": "5.5.4" "typescript": "5.5.4"
}, },
"dependencies": { "dependencies": {
"axios": "^1.15.2", "axios": "^1.16.0",
"openai": "^4.93.0", "openai": "^4.93.0",
"uuid": "9.0.1", "uuid": "9.0.1",
"zod": "^3.24.1" "zod": "^3.24.1"
@@ -110,10 +110,10 @@
"@azure/identity": "^4.0.0", "@azure/identity": "^4.0.0",
"@azure/search-documents": "^12.0.0", "@azure/search-documents": "^12.0.0",
"@cloudflare/workers-types": "^4.20250504.0", "@cloudflare/workers-types": "^4.20250504.0",
"@google/genai": "^1.2.0", "@google/genai": "^1.40.0",
"@langchain/core": "^1.1.47", "@langchain/core": "^1.1.47",
"@mistralai/mistralai": "^1.5.2", "@mistralai/mistralai": "^1.5.2",
"@qdrant/js-client-rest": "1.13.0", "@qdrant/js-client-rest": "^1.18.0",
"@supabase/supabase-js": "^2.49.1", "@supabase/supabase-js": "^2.49.1",
"@types/jest": "29.5.14", "@types/jest": "29.5.14",
"@types/pg": "8.11.0", "@types/pg": "8.11.0",
@@ -137,6 +137,19 @@
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"esbuild", "esbuild",
"better-sqlite3" "better-sqlite3"
] ],
"overrides": {
"picomatch@<2.3.2": "^2.3.2",
"jws@4.0.0": "4.0.1",
"langsmith@<0.6.0": "^0.6.0",
"minimatch@<3.1.3": "^3.1.3",
"minimatch@>=5.0.0 <5.1.8": "^5.1.8",
"minimatch@>=9.0.0 <9.0.7": "^9.0.7",
"path-to-regexp@>=8.0.0 <8.4.0": "^8.4.0",
"rollup@>=4.0.0 <4.59.0": "^4.59.0",
"tar-fs@>=2.0.0 <2.1.4": "^2.1.4",
"glob@>=10.2.0 <10.5.0": "^10.5.0",
"@modelcontextprotocol/sdk": "^1.25.4"
}
} }
} }
+366 -741
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
packages:
- "."
onlyBuiltDependencies:
- esbuild
- better-sqlite3
overrides:
"picomatch@<2.3.2": "^2.3.2"
"jws@4.0.0": "4.0.1"
"langsmith@<0.6.0": "^0.6.0"
"minimatch@<3.1.3": "^3.1.3"
"minimatch@>=5.0.0 <5.1.8": "^5.1.8"
"minimatch@>=9.0.0 <9.0.7": "^9.0.7"
"path-to-regexp@>=8.0.0 <8.4.0": "^8.4.0"
"rollup@>=4.0.0 <4.59.0": "^4.59.0"
"tar-fs@>=2.0.0 <2.1.4": "^2.1.4"
"glob@>=10.2.0 <10.5.0": "^10.5.0"
"@modelcontextprotocol/sdk": "^1.25.4"
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@mem0/community", "name": "@mem0/community",
"version": "0.0.1", "version": "0.0.2",
"description": "Community features for Mem0", "description": "Community features for Mem0",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.mjs", "module": "./dist/index.mjs",
@@ -77,7 +77,7 @@
"dependencies": { "dependencies": {
"@langchain/community": "^0.3.36", "@langchain/community": "^0.3.36",
"@langchain/core": "^0.3.42", "@langchain/core": "^0.3.42",
"axios": "1.7.7", "axios": "^1.16.0",
"mem0ai": "^2.1.8", "mem0ai": "^2.1.8",
"uuid": "9.0.1", "uuid": "9.0.1",
"zod": "3.22.4" "zod": "3.22.4"
+3 -1
View File
@@ -44,7 +44,9 @@ export class LMStudioEmbedder implements Embedder {
input: normalized, input: normalized,
encoding_format: "float", encoding_format: "float",
}); });
return response.data.map((item) => item.embedding); return response.data
.sort((a, b) => a.index - b.index)
.map((item) => item.embedding);
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : String(err); const message = err instanceof Error ? err.message : String(err);
throw new Error(`LM Studio embedder failed: ${message}`); throw new Error(`LM Studio embedder failed: ${message}`);
+2
View File
@@ -20,6 +20,7 @@ export class OpenAIEmbedder implements Embedder {
const response = await this.openai.embeddings.create({ const response = await this.openai.embeddings.create({
model: this.model, model: this.model,
input: text, input: text,
encoding_format: "float",
...(this.embeddingDims !== undefined && { ...(this.embeddingDims !== undefined && {
dimensions: this.embeddingDims, dimensions: this.embeddingDims,
}), }),
@@ -35,6 +36,7 @@ export class OpenAIEmbedder implements Embedder {
const response = await this.openai.embeddings.create({ const response = await this.openai.embeddings.create({
model: this.model, model: this.model,
input: chunk, input: chunk,
encoding_format: "float",
...(this.embeddingDims !== undefined && { ...(this.embeddingDims !== undefined && {
dimensions: this.embeddingDims, dimensions: this.embeddingDims,
}), }),
+30 -13
View File
@@ -1064,7 +1064,7 @@ export class Memory {
: {}; : {};
await this._ensureInitialized(); await this._ensureInitialized();
const { topK = 20, threshold = 0.1 } = config; const { topK = 20, threshold = 0.1, explain = false } = config;
await this._captureEvent("search", { await this._captureEvent("search", {
query_length: query.length, query_length: query.length,
@@ -1169,17 +1169,35 @@ export class Memory {
if (deduped.length > 0) { if (deduped.length > 0) {
const entityStore = await this.getEntityStore(); const entityStore = await this.getEntityStore();
const entitySearchFilters: Record<string, any> = {};
for (const k of ["user_id", "agent_id", "run_id"] as const) {
if (effectiveFilters[k])
entitySearchFilters[k] = effectiveFilters[k];
}
const entityTexts = deduped.map((e) => e.text);
const embeddings = await this.embedder.embedBatch(entityTexts);
for (const entity of deduped) { if (embeddings.length !== entityTexts.length) {
try { console.warn(
const entityEmbedding = await this.embedder.embed(entity.text); `embedBatch returned ${embeddings.length} vectors for ${entityTexts.length} texts — skipping entity boost`,
const matches = await entityStore.search( );
entityEmbedding, } else {
500, const searchResults = await Promise.allSettled(
effectiveFilters, deduped.map((_, i) =>
); entityStore.search(embeddings[i], 500, entitySearchFilters),
),
);
for (const match of matches) { for (const result of searchResults) {
if (result.status === "rejected") {
console.warn(
"Entity boost search failed for one entity:",
result.reason,
);
continue;
}
for (const match of result.value) {
const similarity = match.score ?? 0; const similarity = match.score ?? 0;
if (similarity < 0.5) continue; if (similarity < 0.5) continue;
@@ -1187,7 +1205,6 @@ export class Memory {
const linkedMemoryIds = payload.linkedMemoryIds ?? []; const linkedMemoryIds = payload.linkedMemoryIds ?? [];
if (!Array.isArray(linkedMemoryIds)) continue; if (!Array.isArray(linkedMemoryIds)) continue;
// Spread-attenuated boost
const numLinked = Math.max(linkedMemoryIds.length, 1); const numLinked = Math.max(linkedMemoryIds.length, 1);
const memoryCountWeight = const memoryCountWeight =
1.0 / (1.0 + 0.001 * (numLinked - 1) ** 2); 1.0 / (1.0 + 0.001 * (numLinked - 1) ** 2);
@@ -1204,8 +1221,6 @@ export class Memory {
} }
} }
} }
} catch (e) {
// Individual entity boost failed — continue
} }
} }
} }
@@ -1228,6 +1243,7 @@ export class Memory {
entityBoosts, entityBoosts,
threshold ?? 0.1, threshold ?? 0.1,
topK, topK,
explain,
); );
// Step 9: Format results // Step 9: Format results
@@ -1260,6 +1276,7 @@ export class Memory {
...(payload.user_id && { user_id: payload.user_id }), ...(payload.user_id && { user_id: payload.user_id }),
...(payload.agent_id && { agent_id: payload.agent_id }), ...(payload.agent_id && { agent_id: payload.agent_id }),
...(payload.run_id && { run_id: payload.run_id }), ...(payload.run_id && { run_id: payload.run_id }),
...(scored.scoreDetails && { score_details: scored.scoreDetails }),
}; };
}); });
@@ -17,6 +17,7 @@ export interface SearchMemoryOptions {
topK?: number; topK?: number;
filters?: SearchFilters; filters?: SearchFilters;
threshold?: number; threshold?: number;
explain?: boolean;
} }
export interface GetAllMemoryOptions { export interface GetAllMemoryOptions {
+27 -2
View File
@@ -56,10 +56,21 @@ export function normalizeBm25(
return 1.0 / (1.0 + Math.exp(-steepness * (rawScore - midpoint))); return 1.0 / (1.0 + Math.exp(-steepness * (rawScore - midpoint)));
} }
export interface ScoreDetails {
semanticScore: number;
bm25Score: number;
entityBoost: number;
rawScore: number;
maxPossibleScore: number;
finalScore: number;
threshold: number;
}
export interface ScoredResult { export interface ScoredResult {
id: string; id: string;
score: number; score: number;
payload: Record<string, any>; payload: Record<string, any>;
scoreDetails?: ScoreDetails;
} }
/** /**
@@ -82,6 +93,7 @@ export interface ScoredResult {
* @param entityBoosts - Map of memory ID to entity boost score. * @param entityBoosts - Map of memory ID to entity boost score.
* @param threshold - Minimum semantic score to include a candidate. * @param threshold - Minimum semantic score to include a candidate.
* @param topK - Maximum number of results to return. * @param topK - Maximum number of results to return.
* @param explain - Include scoreDetails in each result when true.
* @returns Sorted list of scored results, highest score first. * @returns Sorted list of scored results, highest score first.
*/ */
export function scoreAndRank( export function scoreAndRank(
@@ -94,6 +106,7 @@ export function scoreAndRank(
entityBoosts: Record<string, number>, entityBoosts: Record<string, number>,
threshold: number, threshold: number,
topK: number, topK: number,
explain: boolean = false,
): ScoredResult[] { ): ScoredResult[] {
const hasBm25 = Object.keys(bm25Scores).length > 0; const hasBm25 = Object.keys(bm25Scores).length > 0;
const hasEntity = Object.keys(entityBoosts).length > 0; const hasEntity = Object.keys(entityBoosts).length > 0;
@@ -126,11 +139,23 @@ export function scoreAndRank(
const rawCombined = semanticScore + bm25Score + entityBoost; const rawCombined = semanticScore + bm25Score + entityBoost;
const combined = Math.min(rawCombined / maxPossible, 1.0); const combined = Math.min(rawCombined / maxPossible, 1.0);
scored.push({ const entry: ScoredResult = {
id: memIdStr, id: memIdStr,
score: combined, score: combined,
payload: result.payload, payload: result.payload,
}); };
if (explain) {
entry.scoreDetails = {
semanticScore,
bm25Score,
entityBoost,
rawScore: rawCombined,
maxPossibleScore: maxPossible,
finalScore: combined,
threshold,
};
}
scored.push(entry);
} }
scored.sort((a, b) => b.score - a.score); scored.sort((a, b) => b.score - a.score);
+1 -1
View File
@@ -444,7 +444,7 @@ export class RedisDB implements VectorStore {
return { return {
id: doc.value.memory_id, id: doc.value.memory_id,
payload: toCamelCase(resultPayload), payload: toCamelCase(resultPayload),
score: Number(doc.value.__vector_score) ?? 0, score: Math.max(0, 1 - (Number(doc.value.__vector_score) ?? 0)),
}; };
}); });
} catch (error) { } catch (error) {
@@ -0,0 +1,276 @@
/**
* Entity boost parallelism tests (#5214).
*
* Verifies that entity boost searches run concurrently via Promise.allSettled,
* scoring is preserved, and individual entity failures don't abort others.
*/
/// <reference types="jest" />
import { Memory } from "../src/memory";
import { ENTITY_BOOST_WEIGHT } from "../src/utils/scoring";
import type { VectorStoreResult } from "../src/types";
jest.setTimeout(15000);
jest.mock("../src/embeddings/google", () => ({
GoogleEmbedder: jest.fn(),
}));
jest.mock("../src/llms/google", () => ({
GoogleLLM: jest.fn(),
}));
jest.mock("../src/llms/openai", () => ({
OpenAILLM: jest.fn().mockImplementation(() => ({
generateResponse: jest.fn().mockResolvedValue(
JSON.stringify({
memory: [{ id: "0", text: "fact", attributed_to: "user" }],
}),
),
})),
}));
const mockEmbedding = new Array(1536).fill(0.1);
jest.mock("../src/embeddings/openai", () => ({
OpenAIEmbedder: jest.fn().mockImplementation(() => ({
embed: jest.fn().mockResolvedValue(mockEmbedding),
embedBatch: jest
.fn()
.mockImplementation((texts: string[]) =>
Promise.resolve(texts.map(() => mockEmbedding)),
),
embeddingDims: 1536,
})),
}));
function makeMatch(
id: string,
score: number,
linkedMemoryIds: string[],
): VectorStoreResult {
return { id, score, payload: { linkedMemoryIds } };
}
function createMemory(): Memory {
return new Memory({
version: "v1.1",
embedder: {
provider: "openai",
config: { apiKey: "test-key", model: "text-embedding-3-small" },
},
vectorStore: {
provider: "memory",
config: {
collectionName: `test-entity-${Date.now()}-${Math.random()}`,
dimension: 1536,
dbPath: ":memory:",
},
},
llm: {
provider: "openai",
config: { apiKey: "test-key", model: "gpt-5-mini" },
},
historyDbPath: ":memory:",
});
}
describe("Entity boost parallelism (#5214)", () => {
let memory: Memory;
beforeEach(() => {
memory = createMemory();
});
afterEach(async () => {
await memory.reset();
});
it("should use Promise.allSettled for concurrent entity searches", async () => {
// Spy on Promise.allSettled to confirm it's being used
const allSettledSpy = jest.spyOn(Promise, "allSettled");
// Access internals to inject a mock entity store
const m = memory as any;
await m._ensureInitialized();
const mockEntityStore = {
search: jest.fn().mockResolvedValue([makeMatch("e1", 0.9, ["mem-1"])]),
initialize: jest.fn().mockResolvedValue(undefined),
};
m._entityStore = mockEntityStore;
m.embedder = {
embed: jest.fn().mockResolvedValue(mockEmbedding),
embedBatch: jest
.fn()
.mockImplementation((texts: string[]) =>
Promise.resolve(texts.map(() => mockEmbedding)),
),
};
// Mock the vector store to return a semantic result
m.vectorStore.search = jest
.fn()
.mockResolvedValue([
{ id: "mem-1", score: 0.8, payload: { data: "test" } },
]);
m.vectorStore.keywordSearch = jest.fn().mockResolvedValue(null);
await m.search("alice and bob", { filters: { user_id: "u1" } });
expect(allSettledSpy).toHaveBeenCalled();
allSettledSpy.mockRestore();
});
it("should preserve scoring math with parallel execution", async () => {
const m = memory as any;
await m._ensureInitialized();
// Two entities: "alice" links to mem-1, "bob" links to mem-1 and mem-2
// mem-1 should get max(alice_boost, bob_boost)
const mockEntityStore = {
search: jest
.fn()
.mockImplementation(
(_embedding: number[], _topK: number, _filters: any) => {
// We need to differentiate by embedding content — but since all
// embeddings are identical mocks, we'll use call order
const callCount = mockEntityStore.search.mock.calls.length;
if (callCount <= 1) {
// First entity: "alice"
return Promise.resolve([makeMatch("e-alice", 0.9, ["mem-1"])]);
}
// Second entity: "bob"
return Promise.resolve([
makeMatch("e-bob", 0.6, ["mem-1", "mem-2"]),
]);
},
),
initialize: jest.fn().mockResolvedValue(undefined),
};
m._entityStore = mockEntityStore;
m.embedder = {
embed: jest.fn().mockResolvedValue(mockEmbedding),
embedBatch: jest
.fn()
.mockImplementation((texts: string[]) =>
Promise.resolve(texts.map(() => mockEmbedding)),
),
};
// Semantic results include mem-1 and mem-2
m.vectorStore.search = jest.fn().mockResolvedValue([
{ id: "mem-1", score: 0.85, payload: { data: "alice memory" } },
{ id: "mem-2", score: 0.75, payload: { data: "bob memory" } },
]);
m.vectorStore.keywordSearch = jest.fn().mockResolvedValue(null);
const result = await m.search("alice and bob", {
filters: { user_id: "u1" },
});
// Entity store was called (parallelized via Promise.allSettled)
expect(mockEntityStore.search).toHaveBeenCalled();
// Results should exist and have scores
expect(result.results.length).toBeGreaterThan(0);
for (const item of result.results) {
expect(typeof item.score).toBe("number");
expect(item.score).toBeGreaterThan(0);
}
});
it("should survive one entity search failure without losing other boosts", async () => {
const m = memory as any;
await m._ensureInitialized();
let callIndex = 0;
const mockEntityStore = {
search: jest.fn().mockImplementation(() => {
callIndex++;
if (callIndex === 1) {
return Promise.reject(new Error("provider timeout"));
}
return Promise.resolve([makeMatch("e-ok", 0.8, ["mem-9"])]);
}),
initialize: jest.fn().mockResolvedValue(undefined),
};
m._entityStore = mockEntityStore;
m.embedder = {
embed: jest.fn().mockResolvedValue(mockEmbedding),
embedBatch: jest
.fn()
.mockImplementation((texts: string[]) =>
Promise.resolve(texts.map(() => mockEmbedding)),
),
};
m.vectorStore.search = jest
.fn()
.mockResolvedValue([
{ id: "mem-9", score: 0.85, payload: { data: "surviving memory" } },
]);
m.vectorStore.keywordSearch = jest.fn().mockResolvedValue(null);
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
// "John Smith met Jane Doe" extracts two proper entities
const result = await m.search("John Smith met Jane Doe", {
filters: { user_id: "u1" },
});
expect(result.results.length).toBeGreaterThan(0);
expect(result.results[0].id).toBe("mem-9");
// Should log the failure like Python does
expect(warnSpy).toHaveBeenCalledWith(
"Entity boost search failed for one entity:",
expect.any(Error),
);
warnSpy.mockRestore();
});
it("should call entity searches concurrently, not sequentially", async () => {
const m = memory as any;
await m._ensureInitialized();
const concurrency = { current: 0, peak: 0 };
const mockEntityStore = {
search: jest.fn().mockImplementation(() => {
concurrency.current++;
concurrency.peak = Math.max(concurrency.peak, concurrency.current);
return new Promise<VectorStoreResult[]>((resolve) => {
setTimeout(() => {
concurrency.current--;
resolve([makeMatch("e1", 0.7, ["mem-1"])]);
}, 100);
});
}),
initialize: jest.fn().mockResolvedValue(undefined),
};
m._entityStore = mockEntityStore;
m.embedder = {
embed: jest.fn().mockResolvedValue(mockEmbedding),
embedBatch: jest
.fn()
.mockImplementation((texts: string[]) =>
Promise.resolve(texts.map(() => mockEmbedding)),
),
};
m.vectorStore.search = jest
.fn()
.mockResolvedValue([
{ id: "mem-1", score: 0.8, payload: { data: "test" } },
]);
m.vectorStore.keywordSearch = jest.fn().mockResolvedValue(null);
const start = performance.now();
await m.search("entity1 and entity2 and entity3 and entity4", {
filters: { user_id: "u1" },
});
const elapsed = performance.now() - start;
// With 4 entities at 100ms each, sequential would be ~400ms+.
// Parallel should be well under that. Use generous bound for CI.
expect(elapsed).toBeLessThan(500);
// At least 2 searches should have overlapped
expect(concurrency.peak).toBeGreaterThanOrEqual(2);
});
});
@@ -2,7 +2,8 @@
/** /**
* OpenAI Embedder — unit tests (mocked OpenAI client). * OpenAI Embedder — unit tests (mocked OpenAI client).
* Verifies that the `dimensions` parameter is only passed to the API * Verifies that the `dimensions` parameter is only passed to the API
* when the user explicitly configures `embeddingDims`. * when the user explicitly configures `embeddingDims`, and that
* embeddings are requested as floats for OpenAI-compatible proxies.
*/ */
const mockEmbeddingsCreate = jest.fn(); const mockEmbeddingsCreate = jest.fn();
@@ -42,6 +43,7 @@ describe("OpenAIEmbedder (unit)", () => {
expect(callArgs).toEqual({ expect(callArgs).toEqual({
model: "text-embedding-3-small", model: "text-embedding-3-small",
input: "hello", input: "hello",
encoding_format: "float",
}); });
}); });
@@ -58,6 +60,7 @@ describe("OpenAIEmbedder (unit)", () => {
expect(callArgs).toEqual({ expect(callArgs).toEqual({
model: "text-embedding-3-small", model: "text-embedding-3-small",
input: "hello", input: "hello",
encoding_format: "float",
dimensions: 1024, dimensions: 1024,
}); });
}); });
@@ -87,6 +90,7 @@ describe("OpenAIEmbedder (unit)", () => {
const callArgs = mockEmbeddingsCreate.mock.calls[0][0]; const callArgs = mockEmbeddingsCreate.mock.calls[0][0];
expect(callArgs).not.toHaveProperty("dimensions"); expect(callArgs).not.toHaveProperty("dimensions");
expect(callArgs).toHaveProperty("encoding_format", "float");
}); });
it("passes dimensions in embedBatch when embeddingDims is explicitly set", async () => { it("passes dimensions in embedBatch when embeddingDims is explicitly set", async () => {
@@ -105,6 +109,7 @@ describe("OpenAIEmbedder (unit)", () => {
expect(callArgs).toEqual({ expect(callArgs).toEqual({
model: "text-embedding-3-small", model: "text-embedding-3-small",
input: ["hello", "world"], input: ["hello", "world"],
encoding_format: "float",
dimensions: 512, dimensions: 512,
}); });
}); });
+49
View File
@@ -0,0 +1,49 @@
/// <reference types="jest" />
import { scoreAndRank } from "../src/utils/scoring";
describe("scoreAndRank", () => {
const results = [
{ id: "a", score: 0.8, payload: { data: "mem a" } },
{ id: "b", score: 0.5, payload: { data: "mem b" } },
];
it("omits scoreDetails by default", () => {
const scored = scoreAndRank(results, {}, {}, 0.1, 10);
expect(scored[0].scoreDetails).toBeUndefined();
expect(scored[1].scoreDetails).toBeUndefined();
});
it("omits scoreDetails when explain is false", () => {
const scored = scoreAndRank(results, {}, {}, 0.1, 10, false);
expect(scored[0].scoreDetails).toBeUndefined();
});
it("includes scoreDetails when explain is true", () => {
const bm25 = { a: 0.6 };
const entity = { a: 0.3 };
const scored = scoreAndRank(results, bm25, entity, 0.1, 10, true);
const details = scored[0].scoreDetails!;
expect(details).toBeDefined();
expect(details.semanticScore).toBe(0.8);
expect(details.bm25Score).toBe(0.6);
expect(details.entityBoost).toBe(0.3);
expect(details.rawScore).toBeCloseTo(1.7);
expect(details.maxPossibleScore).toBe(2.5);
expect(details.finalScore).toBeCloseTo(0.68);
expect(details.threshold).toBe(0.1);
});
it("includes scoreDetails for results without bm25/entity signals", () => {
const scored = scoreAndRank(results, {}, {}, 0.1, 10, true);
const details = scored[0].scoreDetails!;
expect(details.semanticScore).toBe(0.8);
expect(details.bm25Score).toBe(0);
expect(details.entityBoost).toBe(0);
expect(details.rawScore).toBe(0.8);
expect(details.maxPossibleScore).toBe(1.0);
expect(details.finalScore).toBe(0.8);
});
});
+11
View File
@@ -33,6 +33,15 @@ setup_config()
ENTITY_PARAMS = frozenset({"user_id", "agent_id", "app_id", "run_id"}) ENTITY_PARAMS = frozenset({"user_id", "agent_id", "app_id", "run_id"})
def _validate_and_trim_search_query(query: str) -> str:
if not isinstance(query, str):
raise ValueError("Invalid query: must be a non-empty string.")
trimmed = query.strip()
if not trimmed:
raise ValueError("Invalid query: cannot be empty or whitespace-only.")
return trimmed
def _maybe_alias_anon_to_email(user_email): def _maybe_alias_anon_to_email(user_email):
"""Fire $identify per prior anon ID so PostHog merges them into email. """Fire $identify per prior anon ID so PostHog merges them into email.
@@ -306,6 +315,7 @@ class MemoryClient:
kwargs = {**(options.model_dump(exclude_unset=True) if options else {}), **kwargs} kwargs = {**(options.model_dump(exclude_unset=True) if options else {}), **kwargs}
params = self._prepare_params(kwargs) params = self._prepare_params(kwargs)
query = _validate_and_trim_search_query(query)
payload = {"query": query, **params} payload = {"query": query, **params}
response = self.client.post("/v3/memories/search/", json=payload) response = self.client.post("/v3/memories/search/", json=payload)
@@ -1221,6 +1231,7 @@ class AsyncMemoryClient:
kwargs = {**(options.model_dump(exclude_unset=True) if options else {}), **kwargs} kwargs = {**(options.model_dump(exclude_unset=True) if options else {}), **kwargs}
params = self._prepare_params(kwargs) params = self._prepare_params(kwargs)
query = _validate_and_trim_search_query(query)
payload = {"query": query, **params} payload = {"query": query, **params}
response = await self.async_client.post("/v3/memories/search/", json=payload) response = await self.async_client.post("/v3/memories/search/", json=payload)
+2 -2
View File
@@ -25,7 +25,7 @@ class BaseEmbedderConfig(ABC):
model_kwargs: Optional[dict] = None, model_kwargs: Optional[dict] = None,
huggingface_base_url: Optional[str] = None, huggingface_base_url: Optional[str] = None,
# AzureOpenAI specific # AzureOpenAI specific
azure_kwargs: Optional[AzureConfig] = {}, azure_kwargs: Optional[AzureConfig] = None,
http_client_proxies: Optional[Union[Dict, str]] = None, http_client_proxies: Optional[Union[Dict, str]] = None,
# VertexAI specific # VertexAI specific
vertex_credentials_json: Optional[str] = None, vertex_credentials_json: Optional[str] = None,
@@ -89,7 +89,7 @@ class BaseEmbedderConfig(ABC):
self.model_kwargs = model_kwargs or {} self.model_kwargs = model_kwargs or {}
self.huggingface_base_url = huggingface_base_url self.huggingface_base_url = huggingface_base_url
# AzureOpenAI specific # AzureOpenAI specific
self.azure_kwargs = AzureConfig(**azure_kwargs) or {} self.azure_kwargs = AzureConfig(**(azure_kwargs or {})) or {}
# VertexAI specific # VertexAI specific
self.vertex_credentials_json = vertex_credentials_json self.vertex_credentials_json = vertex_credentials_json
+6
View File
@@ -23,6 +23,7 @@ class AzureOpenAIConfig(BaseLlmConfig):
vision_details: Optional[str] = "auto", vision_details: Optional[str] = "auto",
reasoning_effort: Optional[str] = None, reasoning_effort: Optional[str] = None,
http_client_proxies: Optional[dict] = None, http_client_proxies: Optional[dict] = None,
is_reasoning_model: Optional[bool] = None,
# Azure OpenAI-specific parameters # Azure OpenAI-specific parameters
azure_kwargs: Optional[Dict[str, Any]] = None, azure_kwargs: Optional[Dict[str, Any]] = None,
): ):
@@ -40,6 +41,10 @@ class AzureOpenAIConfig(BaseLlmConfig):
vision_details: Vision detail level, defaults to "auto" vision_details: Vision detail level, defaults to "auto"
reasoning_effort: Effort level for reasoning models ("low", "medium", "high"), defaults to None reasoning_effort: Effort level for reasoning models ("low", "medium", "high"), defaults to None
http_client_proxies: HTTP client proxy settings, defaults to None http_client_proxies: HTTP client proxy settings, defaults to None
is_reasoning_model: Explicit override for reasoning-model detection.
None (default) uses the name-based heuristic. Set True to drop
max_tokens/temperature (e.g. for versioned Azure deployments like
"gpt-5.4-nano-2026-03-17"), or False to force standard params.
azure_kwargs: Azure-specific configuration, defaults to None azure_kwargs: Azure-specific configuration, defaults to None
""" """
# Initialize base parameters # Initialize base parameters
@@ -54,6 +59,7 @@ class AzureOpenAIConfig(BaseLlmConfig):
vision_details=vision_details, vision_details=vision_details,
reasoning_effort=reasoning_effort, reasoning_effort=reasoning_effort,
http_client_proxies=http_client_proxies, http_client_proxies=http_client_proxies,
is_reasoning_model=is_reasoning_model,
) )
# Azure OpenAI-specific parameters # Azure OpenAI-specific parameters
+10
View File
@@ -25,6 +25,7 @@ class BaseLlmConfig(ABC):
vision_details: Optional[str] = "auto", vision_details: Optional[str] = "auto",
reasoning_effort: Optional[str] = None, reasoning_effort: Optional[str] = None,
http_client_proxies: Optional[Union[Dict, str]] = None, http_client_proxies: Optional[Union[Dict, str]] = None,
is_reasoning_model: Optional[bool] = None,
): ):
""" """
Initialize a base configuration class instance for the LLM. Initialize a base configuration class instance for the LLM.
@@ -54,6 +55,14 @@ class BaseLlmConfig(ABC):
Defaults to None (uses the model's default reasoning effort) Defaults to None (uses the model's default reasoning effort)
http_client_proxies: Proxy settings for HTTP client. http_client_proxies: Proxy settings for HTTP client.
Can be a dict or string. Defaults to None Can be a dict or string. Defaults to None
is_reasoning_model: Explicit override for reasoning-model detection.
When None (default), the model is classified automatically from its
name (preserving existing behavior). Set to True to force the
reasoning-model parameter set (drop max_tokens and temperature),
or False to force the standard parameter set. Useful for
deployments with custom/versioned model names (e.g. Azure
"gpt-5.4-nano-2026-03-17") that the name-based heuristic cannot
recognize. Defaults to None
""" """
self.model = model self.model = model
self.temperature = temperature self.temperature = temperature
@@ -64,4 +73,5 @@ class BaseLlmConfig(ABC):
self.enable_vision = enable_vision self.enable_vision = enable_vision
self.vision_details = vision_details self.vision_details = vision_details
self.reasoning_effort = reasoning_effort self.reasoning_effort = reasoning_effort
self.is_reasoning_model = is_reasoning_model
self.http_client = httpx.Client(proxies=http_client_proxies) if http_client_proxies else None self.http_client = httpx.Client(proxies=http_client_proxies) if http_client_proxies else None
+5
View File
@@ -22,6 +22,7 @@ class OpenAIConfig(BaseLlmConfig):
vision_details: Optional[str] = "auto", vision_details: Optional[str] = "auto",
reasoning_effort: Optional[str] = None, reasoning_effort: Optional[str] = None,
http_client_proxies: Optional[dict] = None, http_client_proxies: Optional[dict] = None,
is_reasoning_model: Optional[bool] = None,
# OpenAI-specific parameters # OpenAI-specific parameters
openai_base_url: Optional[str] = None, openai_base_url: Optional[str] = None,
models: Optional[List[str]] = None, models: Optional[List[str]] = None,
@@ -47,6 +48,9 @@ class OpenAIConfig(BaseLlmConfig):
vision_details: Vision detail level, defaults to "auto" vision_details: Vision detail level, defaults to "auto"
reasoning_effort: Effort level for reasoning models ("low", "medium", "high"), defaults to None reasoning_effort: Effort level for reasoning models ("low", "medium", "high"), defaults to None
http_client_proxies: HTTP client proxy settings, defaults to None http_client_proxies: HTTP client proxy settings, defaults to None
is_reasoning_model: Explicit override for reasoning-model detection.
None (default) uses the name-based heuristic. Set True to drop
max_tokens/temperature, or False to force standard params.
openai_base_url: OpenAI API base URL, defaults to None openai_base_url: OpenAI API base URL, defaults to None
models: List of models for OpenRouter, defaults to None models: List of models for OpenRouter, defaults to None
route: OpenRouter route strategy, defaults to "fallback" route: OpenRouter route strategy, defaults to "fallback"
@@ -72,6 +76,7 @@ class OpenAIConfig(BaseLlmConfig):
vision_details=vision_details, vision_details=vision_details,
reasoning_effort=reasoning_effort, reasoning_effort=reasoning_effort,
http_client_proxies=http_client_proxies, http_client_proxies=http_client_proxies,
is_reasoning_model=is_reasoning_model,
) )
# OpenAI-specific parameters # OpenAI-specific parameters
+55
View File
@@ -0,0 +1,55 @@
from typing import Optional
from mem0.configs.llms.base import BaseLlmConfig
class XAIConfig(BaseLlmConfig):
"""
Configuration class for X.AI (Grok) provider parameters.
Inherits from BaseLlmConfig and adds X.AI-specific settings.
"""
def __init__(
self,
# Base parameters
model: Optional[str] = None,
temperature: float = 0.1,
api_key: Optional[str] = None,
max_tokens: int = 2000,
top_p: float = 0.1,
top_k: int = 1,
enable_vision: bool = False,
vision_details: Optional[str] = "auto",
http_client_proxies: Optional[dict] = None,
# X.AI-specific parameters
xai_base_url: Optional[str] = None,
):
"""
Initialize X.AI configuration.
Args:
model: X.AI / Grok model to use, defaults to None
temperature: Controls randomness, defaults to 0.1
api_key: X.AI API key, defaults to None
max_tokens: Maximum tokens to generate, defaults to 2000
top_p: Nucleus sampling parameter, defaults to 0.1
top_k: Top-k sampling parameter, defaults to 1
enable_vision: Enable vision capabilities, defaults to False
vision_details: Vision detail level, defaults to "auto"
http_client_proxies: HTTP client proxy settings, defaults to None
xai_base_url: X.AI API base URL, defaults to None
"""
super().__init__(
model=model,
temperature=temperature,
api_key=api_key,
max_tokens=max_tokens,
top_p=top_p,
top_k=top_k,
enable_vision=enable_vision,
vision_details=vision_details,
http_client_proxies=http_client_proxies,
)
# X.AI-specific parameters
self.xai_base_url = xai_base_url
+1
View File
@@ -33,6 +33,7 @@ class AzureOpenAILLM(LLMBase):
vision_details=config.vision_details, vision_details=config.vision_details,
reasoning_effort=getattr(config, 'reasoning_effort', None), reasoning_effort=getattr(config, 'reasoning_effort', None),
http_client_proxies=config.http_client, http_client_proxies=config.http_client,
is_reasoning_model=getattr(config, 'is_reasoning_model', None),
) )
super().__init__(config) super().__init__(config)
+10
View File
@@ -44,12 +44,22 @@ class LLMBase(ABC):
""" """
Check if the model is a reasoning model or GPT-5 series that doesn't support certain parameters. Check if the model is a reasoning model or GPT-5 series that doesn't support certain parameters.
An explicit ``is_reasoning_model`` on the config takes precedence over the
name-based heuristic. This lets deployments with custom/versioned model
names (e.g. Azure ``gpt-5.4-nano-2026-03-17``) opt in or out without
relying on string matching. When the config value is ``None`` (default),
classification falls back to the name-based heuristic below.
Args: Args:
model: The model name to check model: The model name to check
Returns: Returns:
bool: True if the model is a reasoning model or GPT-5 series bool: True if the model is a reasoning model or GPT-5 series
""" """
explicit = getattr(self.config, "is_reasoning_model", None)
if explicit is not None:
return explicit
reasoning_models = { reasoning_models = {
"o1", "o1-preview", "o3-mini", "o3", "o1", "o1-preview", "o3-mini", "o3",
"gpt-5", "gpt-5o", "gpt-5o-mini", "gpt-5o-micro", "gpt-5", "gpt-5o", "gpt-5o-mini", "gpt-5o-micro",
+1
View File
@@ -31,6 +31,7 @@ class OpenAILLM(LLMBase):
vision_details=config.vision_details, vision_details=config.vision_details,
reasoning_effort=getattr(config, 'reasoning_effort', None), reasoning_effort=getattr(config, 'reasoning_effort', None),
http_client_proxies=config.http_client, http_client_proxies=config.http_client,
is_reasoning_model=getattr(config, 'is_reasoning_model', None),
) )
super().__init__(config) super().__init__(config)
+71 -13
View File
@@ -1,14 +1,36 @@
import json
import os import os
from typing import Dict, List, Optional from typing import Dict, List, Optional, Union
from openai import OpenAI from openai import OpenAI
from mem0.configs.llms.base import BaseLlmConfig from mem0.configs.llms.base import BaseLlmConfig
from mem0.configs.llms.xai import XAIConfig
from mem0.llms.base import LLMBase from mem0.llms.base import LLMBase
from mem0.memory.utils import extract_json
class XAILLM(LLMBase): class XAILLM(LLMBase):
def __init__(self, config: Optional[BaseLlmConfig] = None): def __init__(self, config: Optional[Union[BaseLlmConfig, XAIConfig, Dict]] = None):
# Convert to XAIConfig if needed
if config is None:
config = XAIConfig()
elif isinstance(config, dict):
config = XAIConfig(**config)
elif isinstance(config, BaseLlmConfig) and not isinstance(config, XAIConfig):
# Convert BaseLlmConfig to XAIConfig so xai_base_url is available
config = XAIConfig(
model=config.model,
temperature=config.temperature,
api_key=config.api_key,
max_tokens=config.max_tokens,
top_p=config.top_p,
top_k=config.top_k,
enable_vision=config.enable_vision,
vision_details=config.vision_details,
http_client_proxies=config.http_client,
)
super().__init__(config) super().__init__(config)
if not self.config.model: if not self.config.model:
@@ -18,35 +40,71 @@ class XAILLM(LLMBase):
base_url = self.config.xai_base_url or os.getenv("XAI_API_BASE") or "https://api.x.ai/v1" base_url = self.config.xai_base_url or os.getenv("XAI_API_BASE") or "https://api.x.ai/v1"
self.client = OpenAI(api_key=api_key, base_url=base_url) self.client = OpenAI(api_key=api_key, base_url=base_url)
def _parse_response(self, response, tools):
"""
Process the response based on whether tools are used or not.
Args:
response: The raw response from API.
tools: The list of tools provided in the request.
Returns:
str or dict: The processed response.
"""
if tools:
processed_response = {
"content": response.choices[0].message.content,
"tool_calls": [],
}
if response.choices[0].message.tool_calls:
for tool_call in response.choices[0].message.tool_calls:
processed_response["tool_calls"].append(
{
"name": tool_call.function.name,
"arguments": json.loads(extract_json(tool_call.function.arguments)),
}
)
return processed_response
else:
return response.choices[0].message.content
def generate_response( def generate_response(
self, self,
messages: List[Dict[str, str]], messages: List[Dict[str, str]],
response_format=None, response_format=None,
tools: Optional[List[Dict]] = None, tools: Optional[List[Dict]] = None,
tool_choice: str = "auto", tool_choice: str = "auto",
**kwargs,
): ):
""" """
Generate a response based on the given messages using XAI. Generate a response based on the given messages using X.AI (Grok).
Args: Args:
messages (list): List of message dicts containing 'role' and 'content'. messages (list): List of message dicts containing 'role' and 'content'.
response_format (str or object, optional): Format of the response. Defaults to "text". response_format (str or object, optional): Format of the response. Defaults to None.
tools (list, optional): List of tools that the model can call. Defaults to None. tools (list, optional): List of tools that the model can call. Defaults to None.
tool_choice (str, optional): Tool choice method. Defaults to "auto". tool_choice (str, optional): Tool choice method. Defaults to "auto".
**kwargs: Additional X.AI-specific parameters.
Returns: Returns:
str: The generated response. str or dict: The generated response. A string when tools are not requested;
a dict ``{"content": ..., "tool_calls": [...]}`` when tools are requested.
""" """
params = { params = self._get_supported_params(messages=messages, **kwargs)
"model": self.config.model, params.update(
"messages": messages, {
"temperature": self.config.temperature, "model": self.config.model,
"max_tokens": self.config.max_tokens, "messages": messages,
"top_p": self.config.top_p, }
} )
if response_format: if response_format:
params["response_format"] = response_format params["response_format"] = response_format
if tools:
params["tools"] = tools
params["tool_choice"] = tool_choice
response = self.client.chat.completions.create(**params) response = self.client.chat.completions.create(**params)
return response.choices[0].message.content return self._parse_response(response, tools)
+127 -34
View File
@@ -1,4 +1,5 @@
import asyncio import asyncio
import concurrent.futures
import gc import gc
import hashlib import hashlib
import json import json
@@ -46,6 +47,7 @@ from mem0.utils.scoring import (
normalize_bm25, normalize_bm25,
score_and_rank, score_and_rank,
) )
from mem0.vector_stores.base import VectorStoreBase
# Suppress SWIG deprecation warnings globally # Suppress SWIG deprecation warnings globally
warnings.filterwarnings("ignore", category=DeprecationWarning, message=".*SwigPy.*") warnings.filterwarnings("ignore", category=DeprecationWarning, message=".*SwigPy.*")
@@ -168,6 +170,21 @@ def _validate_search_params(threshold: Optional[float] = None, top_k: Optional[i
) )
def _validate_and_trim_search_query(query: str) -> str:
"""
Validates and normalizes a search query before embedding/vector search.
Raises:
ValueError: If query is not a string or is empty/whitespace-only.
"""
if not isinstance(query, str):
raise ValueError("Invalid query: must be a non-empty string.")
trimmed = query.strip()
if not trimmed:
raise ValueError("Invalid query: cannot be empty or whitespace-only.")
return trimmed
def _is_sensitive_field(field_name: str) -> bool: def _is_sensitive_field(field_name: str) -> bool:
"""Check if a field should be redacted for telemetry safety. """Check if a field should be redacted for telemetry safety.
@@ -384,6 +401,15 @@ class Memory(MemoryBase):
self._telemetry_vector_store = VectorStoreFactory.create( self._telemetry_vector_store = VectorStoreFactory.create(
self.config.vector_store.provider, telemetry_config self.config.vector_store.provider, telemetry_config
) )
if getattr(type(self.vector_store), "keyword_search", None) is VectorStoreBase.keyword_search:
logger.warning(
"The '%s' vector store does not support keyword search. "
"Hybrid (BM25) scoring will be disabled and search will use "
"semantic similarity only. To enable hybrid search, switch to a "
"store with keyword_search support (e.g. qdrant, elasticsearch, pgvector).",
self.config.vector_store.provider,
)
capture_event("mem0.init", self, {"sync_type": "sync"}) capture_event("mem0.init", self, {"sync_type": "sync"})
@property @property
@@ -1131,6 +1157,7 @@ class Memory(MemoryBase):
filters: Optional[Dict[str, Any]] = None, filters: Optional[Dict[str, Any]] = None,
threshold: float = 0.1, threshold: float = 0.1,
rerank: bool = False, rerank: bool = False,
explain: bool = False,
**kwargs, **kwargs,
): ):
""" """
@@ -1161,6 +1188,7 @@ class Memory(MemoryBase):
- {"NOT": [filter1]} - logical NOT - {"NOT": [filter1]} - logical NOT
threshold (float, optional): Minimum score for a memory to be included. Defaults to 0.1. threshold (float, optional): Minimum score for a memory to be included. Defaults to 0.1.
rerank (bool, optional): Whether to rerank results. Defaults to False. rerank (bool, optional): Whether to rerank results. Defaults to False.
explain (bool, optional): Whether to include score_details for each result. Defaults to False.
Returns: Returns:
dict: A dictionary containing the search results under a "results" key. dict: A dictionary containing the search results under a "results" key.
@@ -1175,6 +1203,7 @@ class Memory(MemoryBase):
# Validate search parameters (before applying defaults) # Validate search parameters (before applying defaults)
_validate_search_params(threshold=threshold, top_k=top_k) _validate_search_params(threshold=threshold, top_k=top_k)
query = _validate_and_trim_search_query(query)
# Validate and trim entity IDs in filters # Validate and trim entity IDs in filters
effective_filters = filters.copy() if filters else {} effective_filters = filters.copy() if filters else {}
@@ -1220,11 +1249,12 @@ class Memory(MemoryBase):
"encoded_ids": encoded_ids, "encoded_ids": encoded_ids,
"sync_type": "sync", "sync_type": "sync",
"threshold": threshold, "threshold": threshold,
"explain": explain,
"advanced_filters": bool(filters and self._has_advanced_operators(filters)), "advanced_filters": bool(filters and self._has_advanced_operators(filters)),
}, },
) )
original_memories = self._search_vector_store(query, effective_filters, limit, threshold) original_memories = self._search_vector_store(query, effective_filters, limit, threshold, explain=explain)
# Apply reranking if enabled and reranker is available # Apply reranking if enabled and reranker is available
if rerank and self.reranker and original_memories: if rerank and self.reranker and original_memories:
@@ -1340,7 +1370,7 @@ class Memory(MemoryBase):
return True return True
return False return False
def _search_vector_store(self, query, filters, limit, threshold=0.1): def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False):
# Guard against None threshold (backward compat) # Guard against None threshold (backward compat)
if threshold is None: if threshold is None:
threshold = 0.1 threshold = 0.1
@@ -1395,6 +1425,7 @@ class Memory(MemoryBase):
entity_boosts=entity_boosts, entity_boosts=entity_boosts,
threshold=threshold, threshold=threshold,
top_k=limit, top_k=limit,
explain=explain,
) )
# Step 9: Format results # Step 9: Format results
@@ -1432,6 +1463,8 @@ class Memory(MemoryBase):
if not memory_item_dict.get("metadata"): if not memory_item_dict.get("metadata"):
memory_item_dict["metadata"] = {} memory_item_dict["metadata"] = {}
memory_item_dict["metadata"].update(additional_metadata) memory_item_dict["metadata"].update(additional_metadata)
if explain and "score_details" in scored:
memory_item_dict["score_details"] = scored["score_details"]
original_memories.append(memory_item_dict) original_memories.append(memory_item_dict)
@@ -1464,34 +1497,55 @@ class Memory(MemoryBase):
memory_boosts = {} memory_boosts = {}
try: try:
for _, entity_text in deduped: entity_texts = [text for _, text in deduped]
entity_embedding = self.embedding_model.embed(entity_text, "search") embeddings = self.embedding_model.embed_batch(entity_texts, "search")
matches = self.entity_store.search(
query=entity_text, if len(embeddings) != len(entity_texts):
vectors=entity_embedding, logger.warning(
top_k=500, "embed_batch returned %d vectors for %d texts — skipping entity boost",
filters=search_filters, len(embeddings),
len(entity_texts),
)
return memory_boosts
entity_store = self.entity_store
def _search_entity(entity_text, embedding):
return entity_store.search(
query=entity_text, vectors=embedding, top_k=500, filters=search_filters
) )
for match in matches: with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
similarity = match.score if hasattr(match, 'score') else 0.0 futures = {
if similarity < 0.5: pool.submit(_search_entity, text, emb): text
for text, emb in zip(entity_texts, embeddings)
}
for future in concurrent.futures.as_completed(futures):
try:
matches = future.result()
except Exception as e:
logger.warning("Entity boost search failed for one entity: %s", e)
continue continue
payload = match.payload if hasattr(match, 'payload') else {} for match in matches:
linked_memory_ids = payload.get("linked_memory_ids", []) similarity = match.score if hasattr(match, 'score') else 0.0
if not isinstance(linked_memory_ids, list): if similarity < 0.5:
continue continue
# Spread-attenuated boost: entities linking to many memories get attenuated payload = match.payload if hasattr(match, 'payload') else {}
num_linked = max(len(linked_memory_ids), 1) linked_memory_ids = payload.get("linked_memory_ids", [])
memory_count_weight = 1.0 / (1.0 + 0.001 * ((num_linked - 1) ** 2)) if not isinstance(linked_memory_ids, list):
boost = similarity * ENTITY_BOOST_WEIGHT * memory_count_weight continue
for memory_id in linked_memory_ids: num_linked = max(len(linked_memory_ids), 1)
if memory_id: memory_count_weight = 1.0 / (1.0 + 0.001 * ((num_linked - 1) ** 2))
memory_key = str(memory_id) boost = similarity * ENTITY_BOOST_WEIGHT * memory_count_weight
memory_boosts[memory_key] = max(memory_boosts.get(memory_key, 0.0), boost)
for memory_id in linked_memory_ids:
if memory_id:
memory_key = str(memory_id)
memory_boosts[memory_key] = max(memory_boosts.get(memory_key, 0.0), boost)
except Exception as e: except Exception as e:
logger.warning(f"Entity boost computation failed: {e}") logger.warning(f"Entity boost computation failed: {e}")
@@ -1828,6 +1882,15 @@ class AsyncMemory(MemoryBase):
os.makedirs(telemetry_config.path, exist_ok=True) os.makedirs(telemetry_config.path, exist_ok=True)
self._telemetry_vector_store = VectorStoreFactory.create(self.config.vector_store.provider, telemetry_config) self._telemetry_vector_store = VectorStoreFactory.create(self.config.vector_store.provider, telemetry_config)
if getattr(type(self.vector_store), "keyword_search", None) is VectorStoreBase.keyword_search:
logger.warning(
"The '%s' vector store does not support keyword search. "
"Hybrid (BM25) scoring will be disabled and search will use "
"semantic similarity only. To enable hybrid search, switch to a "
"store with keyword_search support (e.g. qdrant, elasticsearch, pgvector).",
self.config.vector_store.provider,
)
capture_event("mem0.init", self, {"sync_type": "async"}) capture_event("mem0.init", self, {"sync_type": "async"})
@property @property
@@ -2546,6 +2609,7 @@ class AsyncMemory(MemoryBase):
filters: Optional[Dict[str, Any]] = None, filters: Optional[Dict[str, Any]] = None,
threshold: float = 0.1, threshold: float = 0.1,
rerank: bool = False, rerank: bool = False,
explain: bool = False,
**kwargs, **kwargs,
): ):
""" """
@@ -2576,6 +2640,7 @@ class AsyncMemory(MemoryBase):
- {"NOT": [filter1]} - logical NOT - {"NOT": [filter1]} - logical NOT
threshold (float, optional): Minimum score for a memory to be included. Defaults to 0.1. threshold (float, optional): Minimum score for a memory to be included. Defaults to 0.1.
rerank (bool, optional): Whether to rerank results. Defaults to False. rerank (bool, optional): Whether to rerank results. Defaults to False.
explain (bool, optional): Whether to include score_details for each result. Defaults to False.
Returns: Returns:
dict: A dictionary containing the search results under a "results" key. dict: A dictionary containing the search results under a "results" key.
@@ -2590,6 +2655,7 @@ class AsyncMemory(MemoryBase):
# Validate search parameters (before applying defaults) # Validate search parameters (before applying defaults)
_validate_search_params(threshold=threshold, top_k=top_k) _validate_search_params(threshold=threshold, top_k=top_k)
query = _validate_and_trim_search_query(query)
# Validate and trim entity IDs in filters # Validate and trim entity IDs in filters
effective_filters = filters.copy() if filters else {} effective_filters = filters.copy() if filters else {}
@@ -2637,11 +2703,12 @@ class AsyncMemory(MemoryBase):
"encoded_ids": encoded_ids, "encoded_ids": encoded_ids,
"sync_type": "async", "sync_type": "async",
"threshold": threshold, "threshold": threshold,
"explain": explain,
"advanced_filters": bool(filters and self._has_advanced_operators(filters)), "advanced_filters": bool(filters and self._has_advanced_operators(filters)),
}, },
) )
original_memories = await self._search_vector_store(query, effective_filters, limit, threshold) original_memories = await self._search_vector_store(query, effective_filters, limit, threshold, explain=explain)
# Apply reranking if enabled and reranker is available # Apply reranking if enabled and reranker is available
if rerank and self.reranker and original_memories: if rerank and self.reranker and original_memories:
@@ -2760,7 +2827,7 @@ class AsyncMemory(MemoryBase):
return True return True
return False return False
async def _search_vector_store(self, query, filters, limit, threshold=0.1): async def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False):
if threshold is None: if threshold is None:
threshold = 0.1 threshold = 0.1
@@ -2814,6 +2881,7 @@ class AsyncMemory(MemoryBase):
entity_boosts=entity_boosts, entity_boosts=entity_boosts,
threshold=threshold, threshold=threshold,
top_k=limit, top_k=limit,
explain=explain,
) )
# Step 9: Format results # Step 9: Format results
@@ -2850,6 +2918,8 @@ class AsyncMemory(MemoryBase):
if not memory_item_dict.get("metadata"): if not memory_item_dict.get("metadata"):
memory_item_dict["metadata"] = {} memory_item_dict["metadata"] = {}
memory_item_dict["metadata"].update(additional_metadata) memory_item_dict["metadata"].update(additional_metadata)
if explain and "score_details" in scored:
memory_item_dict["score_details"] = scored["score_details"]
original_memories.append(memory_item_dict) original_memories.append(memory_item_dict)
@@ -2872,15 +2942,38 @@ class AsyncMemory(MemoryBase):
memory_boosts = {} memory_boosts = {}
try: try:
for _, entity_text in deduped: entity_texts = [text for _, text in deduped]
entity_embedding = await asyncio.to_thread(self.embedding_model.embed, entity_text, "search") embeddings = await asyncio.to_thread(self.embedding_model.embed_batch, entity_texts, "search")
matches = await asyncio.to_thread(
self.entity_store.search, if len(embeddings) != len(entity_texts):
query=entity_text, logger.warning(
vectors=entity_embedding, "embed_batch returned %d vectors for %d texts — skipping entity boost",
top_k=500, len(embeddings),
filters=search_filters, len(entity_texts),
) )
return memory_boosts
sem = asyncio.Semaphore(4)
async def _search_entity(entity_text, embedding):
async with sem:
return await asyncio.to_thread(
self.entity_store.search,
query=entity_text,
vectors=embedding,
top_k=500,
filters=search_filters,
)
results = await asyncio.gather(
*(_search_entity(text, emb) for text, emb in zip(entity_texts, embeddings)),
return_exceptions=True,
)
for matches in results:
if isinstance(matches, BaseException):
logger.warning("Entity boost search failed for one entity: %s", matches)
continue
for match in matches: for match in matches:
similarity = match.score if hasattr(match, 'score') else 0.0 similarity = match.score if hasattr(match, 'score') else 0.0
+3 -1
View File
@@ -52,7 +52,7 @@ class Completions:
def create( def create(
self, self,
model: str, model: str,
messages: List = [], messages: Optional[List] = None,
# Mem0 arguments # Mem0 arguments
user_id: Optional[str] = None, user_id: Optional[str] = None,
agent_id: Optional[str] = None, agent_id: Optional[str] = None,
@@ -92,6 +92,8 @@ class Completions:
api_key: Optional[str] = None, api_key: Optional[str] = None,
model_list: Optional[list] = None, # pass in a list of api_base,keys, etc. model_list: Optional[list] = None, # pass in a list of api_base,keys, etc.
): ):
if messages is None:
messages = []
if not any([user_id, agent_id, run_id]): if not any([user_id, agent_id, run_id]):
raise ValueError("One of user_id, agent_id, run_id must be provided") raise ValueError("One of user_id, agent_id, run_id must be provided")
+9 -6
View File
@@ -7,17 +7,18 @@ from mem0.configs.llms.aws_bedrock import AWSBedrockConfig
from mem0.configs.llms.azure import AzureOpenAIConfig from mem0.configs.llms.azure import AzureOpenAIConfig
from mem0.configs.llms.base import BaseLlmConfig from mem0.configs.llms.base import BaseLlmConfig
from mem0.configs.llms.deepseek import DeepSeekConfig from mem0.configs.llms.deepseek import DeepSeekConfig
from mem0.configs.llms.minimax import MinimaxConfig
from mem0.configs.llms.lmstudio import LMStudioConfig from mem0.configs.llms.lmstudio import LMStudioConfig
from mem0.configs.llms.minimax import MinimaxConfig
from mem0.configs.llms.ollama import OllamaConfig from mem0.configs.llms.ollama import OllamaConfig
from mem0.configs.llms.openai import OpenAIConfig from mem0.configs.llms.openai import OpenAIConfig
from mem0.configs.llms.vllm import VllmConfig from mem0.configs.llms.vllm import VllmConfig
from mem0.configs.llms.xai import XAIConfig
from mem0.configs.rerankers.base import BaseRerankerConfig from mem0.configs.rerankers.base import BaseRerankerConfig
from mem0.configs.rerankers.cohere import CohereRerankerConfig from mem0.configs.rerankers.cohere import CohereRerankerConfig
from mem0.configs.rerankers.huggingface import HuggingFaceRerankerConfig
from mem0.configs.rerankers.llm import LLMRerankerConfig
from mem0.configs.rerankers.sentence_transformer import SentenceTransformerRerankerConfig from mem0.configs.rerankers.sentence_transformer import SentenceTransformerRerankerConfig
from mem0.configs.rerankers.zero_entropy import ZeroEntropyRerankerConfig from mem0.configs.rerankers.zero_entropy import ZeroEntropyRerankerConfig
from mem0.configs.rerankers.llm import LLMRerankerConfig
from mem0.configs.rerankers.huggingface import HuggingFaceRerankerConfig
from mem0.embeddings.mock import MockEmbeddings from mem0.embeddings.mock import MockEmbeddings
@@ -48,7 +49,7 @@ class LlmFactory:
"gemini": ("mem0.llms.gemini.GeminiLLM", BaseLlmConfig), "gemini": ("mem0.llms.gemini.GeminiLLM", BaseLlmConfig),
"deepseek": ("mem0.llms.deepseek.DeepSeekLLM", DeepSeekConfig), "deepseek": ("mem0.llms.deepseek.DeepSeekLLM", DeepSeekConfig),
"minimax": ("mem0.llms.minimax.MiniMaxLLM", MinimaxConfig), "minimax": ("mem0.llms.minimax.MiniMaxLLM", MinimaxConfig),
"xai": ("mem0.llms.xai.XAILLM", BaseLlmConfig), "xai": ("mem0.llms.xai.XAILLM", XAIConfig),
"sarvam": ("mem0.llms.sarvam.SarvamLLM", BaseLlmConfig), "sarvam": ("mem0.llms.sarvam.SarvamLLM", BaseLlmConfig),
"lmstudio": ("mem0.llms.lmstudio.LMStudioLLM", LMStudioConfig), "lmstudio": ("mem0.llms.lmstudio.LMStudioLLM", LMStudioConfig),
"vllm": ("mem0.llms.vllm.VllmLLM", VllmConfig), "vllm": ("mem0.llms.vllm.VllmLLM", VllmConfig),
@@ -209,7 +210,6 @@ class VectorStoreFactory:
return instance return instance
class RerankerFactory: class RerankerFactory:
""" """
Factory for creating reranker instances with appropriate configurations. Factory for creating reranker instances with appropriate configurations.
@@ -219,7 +219,10 @@ class RerankerFactory:
# Provider mappings with their config classes # Provider mappings with their config classes
provider_to_class = { provider_to_class = {
"cohere": ("mem0.reranker.cohere_reranker.CohereReranker", CohereRerankerConfig), "cohere": ("mem0.reranker.cohere_reranker.CohereReranker", CohereRerankerConfig),
"sentence_transformer": ("mem0.reranker.sentence_transformer_reranker.SentenceTransformerReranker", SentenceTransformerRerankerConfig), "sentence_transformer": (
"mem0.reranker.sentence_transformer_reranker.SentenceTransformerReranker",
SentenceTransformerRerankerConfig,
),
"zero_entropy": ("mem0.reranker.zero_entropy_reranker.ZeroEntropyReranker", ZeroEntropyRerankerConfig), "zero_entropy": ("mem0.reranker.zero_entropy_reranker.ZeroEntropyReranker", ZeroEntropyRerankerConfig),
"llm_reranker": ("mem0.reranker.llm_reranker.LLMReranker", LLMRerankerConfig), "llm_reranker": ("mem0.reranker.llm_reranker.LLMReranker", LLMRerankerConfig),
"huggingface": ("mem0.reranker.huggingface_reranker.HuggingFaceReranker", HuggingFaceRerankerConfig), "huggingface": ("mem0.reranker.huggingface_reranker.HuggingFaceReranker", HuggingFaceRerankerConfig),
+24 -6
View File
@@ -63,6 +63,7 @@ def score_and_rank(
entity_boosts: Dict[str, float], entity_boosts: Dict[str, float],
threshold: float, threshold: float,
top_k: int, top_k: int,
explain: bool = False,
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
"""Score candidates additively and return top-k results. """Score candidates additively and return top-k results.
@@ -79,6 +80,14 @@ def score_and_rank(
- Semantic + BM25 + entity: max_possible = 2.5 - Semantic + BM25 + entity: max_possible = 2.5
- Semantic + entity (no BM25): max_possible = 1.5 - Semantic + entity (no BM25): max_possible = 1.5
Args:
semantic_results: Candidate memories from vector search.
bm25_scores: Normalized keyword scores keyed by memory ID.
entity_boosts: Entity-link boosts keyed by memory ID.
threshold: Minimum semantic score required before hybrid scoring.
top_k: Maximum number of results to return.
explain: Include score_details in each result when true.
Returns: Returns:
List of scored result dicts sorted by combined score descending. List of scored result dicts sorted by combined score descending.
""" """
@@ -109,13 +118,22 @@ def score_and_rank(
raw_combined = semantic_score + bm25_score + entity_boost raw_combined = semantic_score + bm25_score + entity_boost
combined = min(raw_combined / max_possible, 1.0) combined = min(raw_combined / max_possible, 1.0)
scored.append( scored_result = {
{ "id": mem_id_str,
"id": mem_id_str, "score": combined,
"score": combined, "payload": result.get("payload"),
"payload": result.get("payload"), }
if explain:
scored_result["score_details"] = {
"semantic_score": semantic_score,
"bm25_score": bm25_score,
"entity_boost": entity_boost,
"raw_score": raw_combined,
"max_possible_score": max_possible,
"final_score": combined,
"threshold": threshold,
} }
) scored.append(scored_result)
scored.sort(key=lambda x: x["score"], reverse=True) scored.sort(key=lambda x: x["score"], reverse=True)
return scored[:top_k] return scored[:top_k]
+3 -6
View File
@@ -313,13 +313,10 @@ class AzureMySQL(VectorStoreBase):
for row in results: for row in results:
vec = np.array(json.loads(row['vector'])) vec = np.array(json.loads(row['vector']))
# Cosine similarity similarity = float(np.dot(query_vec, vec) / (np.linalg.norm(query_vec) * np.linalg.norm(vec)))
similarity = np.dot(query_vec, vec) / (np.linalg.norm(query_vec) * np.linalg.norm(vec)) scored_results.append((row['id'], similarity, row['payload']))
distance = 1 - similarity
scored_results.append((row['id'], distance, row['payload']))
# Sort by distance and apply limit scored_results.sort(key=lambda x: x[1], reverse=True)
scored_results.sort(key=lambda x: x[1])
scored_results = scored_results[:top_k] scored_results = scored_results[:top_k]
return [ return [
+9 -1
View File
@@ -14,7 +14,15 @@ class VectorStoreBase(ABC):
@abstractmethod @abstractmethod
def search(self, query, vectors, top_k=5, filters=None): def search(self, query, vectors, top_k=5, filters=None):
"""Search for similar vectors.""" """Search for similar vectors.
All implementations must return similarity scores where higher values
indicate greater similarity (range [0, 1] preferred). Implementations
using distance metrics must convert to similarity before returning:
- Cosine distance: score = max(0.0, 1.0 - distance)
- L2 distance: score = 1.0 / (1.0 + distance)
- Inner product: score = value (already higher = better)
"""
pass pass
@abstractmethod @abstractmethod
+3 -6
View File
@@ -259,9 +259,7 @@ class CassandraDB(VectorStoreBase):
vec = np.array(row.vector) vec = np.array(row.vector)
# Cosine similarity similarity = float(np.dot(query_vec, vec) / (np.linalg.norm(query_vec) * np.linalg.norm(vec)))
similarity = np.dot(query_vec, vec) / (np.linalg.norm(query_vec) * np.linalg.norm(vec))
distance = 1 - similarity
# Apply filters if provided # Apply filters if provided
if filters: if filters:
@@ -273,10 +271,9 @@ class CassandraDB(VectorStoreBase):
except json.JSONDecodeError: except json.JSONDecodeError:
continue continue
scored_results.append((row.id, distance, row.payload)) scored_results.append((row.id, similarity, row.payload))
# Sort by distance and apply limit scored_results.sort(key=lambda x: x[1], reverse=True)
scored_results.sort(key=lambda x: x[1])
scored_results = scored_results[:top_k] scored_results = scored_results[:top_k]
return [ return [
+3 -1
View File
@@ -97,9 +97,11 @@ class ChromaDB(VectorStoreBase):
result = [] result = []
for i in range(max_length): for i in range(max_length):
raw_distance = distances[i] if isinstance(distances, list) and distances and i < len(distances) else None
score = 1.0 / (1.0 + raw_distance) if raw_distance is not None else None
entry = OutputData( entry = OutputData(
id=ids[i] if isinstance(ids, list) and ids and i < len(ids) else None, id=ids[i] if isinstance(ids, list) and ids and i < len(ids) else None,
score=(distances[i] if isinstance(distances, list) and distances and i < len(distances) else None), score=score,
payload=(metadatas[i] if isinstance(metadatas, list) and metadatas and i < len(metadatas) else None), payload=(metadatas[i] if isinstance(metadatas, list) and metadatas and i < len(metadatas) else None),
) )
result.append(entry) result.append(entry)
+5 -1
View File
@@ -279,7 +279,11 @@ class FAISS(VectorStoreBase):
payload_copy = payload.copy() payload_copy = payload.copy()
score = float(scores[i]) raw_score = float(scores[i])
if self.distance_strategy.lower() == "euclidean":
score = 1.0 / (1.0 + raw_score)
else:
score = raw_score
entry = OutputData( entry = OutputData(
id=vector_id, id=vector_id,
score=score, score=score,
+16 -6
View File
@@ -11,7 +11,14 @@ try:
except ImportError: except ImportError:
raise ImportError("The 'pymilvus' library is required. Please install it using 'pip install pymilvus'.") raise ImportError("The 'pymilvus' library is required. Please install it using 'pip install pymilvus'.")
from pymilvus import CollectionSchema, DataType, FieldSchema, Function, FunctionType, MilvusClient from pymilvus import (
CollectionSchema,
DataType,
FieldSchema,
Function,
FunctionType,
MilvusClient,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -167,11 +174,14 @@ class MilvusDB(VectorStoreBase):
memory = [] memory = []
for value in data: for value in data:
uid, score, metadata = ( uid = value.get("id")
value.get("id"), raw_distance = value.get("distance")
value.get("distance"), metadata = value.get("entity", {}).get("metadata")
value.get("entity", {}).get("metadata"),
) if raw_distance is not None and self.metric_type in (MetricType.L2, "L2"):
score = 1.0 / (1.0 + raw_distance)
else:
score = raw_distance
memory_obj = OutputData(id=uid, score=score, payload=metadata) memory_obj = OutputData(id=uid, score=score, payload=metadata)
memory.append(memory_obj) memory.append(memory_obj)
+46 -13
View File
@@ -1,7 +1,9 @@
import json import json
import logging import logging
import re
from contextlib import contextmanager from contextlib import contextmanager
from typing import Any, List, Optional from typing import Any, List, Optional
from urllib.parse import parse_qsl, urlencode, urlsplit
from pydantic import BaseModel from pydantic import BaseModel
@@ -113,6 +115,24 @@ def _build_filter_conditions(filters):
return conditions, params return conditions, params
def _with_sslmode(connection_string: str, sslmode: str) -> str:
"""Add or replace sslmode in URI and keyword conninfo strings.
Keyword conninfo values are assumed not to contain nested ``sslmode=``
substrings, such as inside an ``options`` value.
"""
if "://" in connection_string:
parsed = urlsplit(connection_string)
query = [(key, value) for key, value in parse_qsl(parsed.query, keep_blank_values=True) if key != "sslmode"]
query.append(("sslmode", sslmode))
return parsed._replace(query=urlencode(query)).geturl()
if re.search(r"(^|\s)sslmode=", connection_string):
return re.sub(r"(^|\s)sslmode=\S+", lambda match: f"{match.group(1)}sslmode={sslmode}", connection_string)
return f"{connection_string} sslmode={sslmode}"
class OutputData(BaseModel): class OutputData(BaseModel):
id: Optional[str] id: Optional[str]
score: Optional[float] score: Optional[float]
@@ -161,6 +181,7 @@ class PGVector(VectorStoreBase):
self.use_hnsw = hnsw self.use_hnsw = hnsw
self.embedding_model_dims = embedding_model_dims self.embedding_model_dims = embedding_model_dims
self.connection_pool = None self.connection_pool = None
self._collection_ensured = False
# Connection setup with priority: connection_pool > connection_string > individual parameters # Connection setup with priority: connection_pool > connection_string > individual parameters
if connection_pool is not None: if connection_pool is not None:
@@ -168,30 +189,33 @@ class PGVector(VectorStoreBase):
self.connection_pool = connection_pool self.connection_pool = connection_pool
elif connection_string: elif connection_string:
if sslmode: if sslmode:
# Append sslmode to connection string if provided connection_string = _with_sslmode(connection_string, sslmode)
if 'sslmode=' in connection_string:
# Replace existing sslmode
import re
connection_string = re.sub(r'sslmode=[^ ]*', f'sslmode={sslmode}', connection_string)
else:
# Add sslmode to connection string
connection_string = f"{connection_string} sslmode={sslmode}"
else: else:
connection_string = f"postgresql://{user}:{password}@{host}:{port}/{dbname}" connection_string = f"postgresql://{user}:{password}@{host}:{port}/{dbname}"
if sslmode: if sslmode:
connection_string = f"{connection_string} sslmode={sslmode}" connection_string = _with_sslmode(connection_string, sslmode)
if self.connection_pool is None: if self.connection_pool is None:
if PSYCOPG_VERSION == 3: if PSYCOPG_VERSION == 3:
# psycopg3 ConnectionPool # open=False avoids blocking when DB DNS is not yet resolvable (e.g. Docker startup)
self.connection_pool = ConnectionPool(conninfo=connection_string, min_size=minconn, max_size=maxconn, open=True) self.connection_pool = ConnectionPool(
conninfo=connection_string,
min_size=minconn,
max_size=maxconn,
open=False,
)
self.connection_pool.open(wait=False)
else: else:
# psycopg2 ThreadedConnectionPool # psycopg2 ThreadedConnectionPool
self.connection_pool = ConnectionPool(minconn=minconn, maxconn=maxconn, dsn=connection_string) self.connection_pool = ConnectionPool(minconn=minconn, maxconn=maxconn, dsn=connection_string)
def _ensure_collection(self):
if self._collection_ensured:
return
collections = self.list_cols() collections = self.list_cols()
if collection_name not in collections: if self.collection_name not in collections:
self.create_col() self.create_col()
self._collection_ensured = True
@contextmanager @contextmanager
def _get_cursor(self, commit: bool = False): def _get_cursor(self, commit: bool = False):
@@ -281,6 +305,7 @@ class PGVector(VectorStoreBase):
) )
def insert(self, vectors: list[list[float]], payloads=None, ids=None) -> None: def insert(self, vectors: list[list[float]], payloads=None, ids=None) -> None:
self._ensure_collection()
logger.info(f"Inserting {len(vectors)} vectors into collection {self.collection_name}") logger.info(f"Inserting {len(vectors)} vectors into collection {self.collection_name}")
json_payloads = [json.dumps(payload) for payload in payloads] json_payloads = [json.dumps(payload) for payload in payloads]
@@ -318,6 +343,7 @@ class PGVector(VectorStoreBase):
Returns: Returns:
list: Search results. list: Search results.
""" """
self._ensure_collection()
filter_conditions, filter_params = _build_filter_conditions(filters) filter_conditions, filter_params = _build_filter_conditions(filters)
filter_clause = sql.SQL("WHERE " + " AND ".join(filter_conditions)) if filter_conditions else sql.SQL("") filter_clause = sql.SQL("WHERE " + " AND ".join(filter_conditions)) if filter_conditions else sql.SQL("")
@@ -334,7 +360,7 @@ class PGVector(VectorStoreBase):
) )
results = cur.fetchall() results = cur.fetchall()
return [OutputData(id=str(r[0]), score=float(r[1]), payload=r[2]) for r in results] return [OutputData(id=str(r[0]), score=max(0.0, 1.0 - float(r[1])), payload=r[2]) for r in results]
def keyword_search(self, query, top_k=5, filters=None): def keyword_search(self, query, top_k=5, filters=None):
""" """
@@ -348,6 +374,7 @@ class PGVector(VectorStoreBase):
Returns: Returns:
List[OutputData]: Search results ranked by text relevance. List[OutputData]: Search results ranked by text relevance.
""" """
self._ensure_collection()
filter_conditions, filter_params = _build_filter_conditions(filters) filter_conditions, filter_params = _build_filter_conditions(filters)
filter_clause = sql.SQL("AND " + " AND ".join(filter_conditions)) if filter_conditions else sql.SQL("") filter_clause = sql.SQL("AND " + " AND ".join(filter_conditions)) if filter_conditions else sql.SQL("")
@@ -378,6 +405,7 @@ class PGVector(VectorStoreBase):
Args: Args:
vector_id (str): ID of the vector to delete. vector_id (str): ID of the vector to delete.
""" """
self._ensure_collection()
with self._get_cursor(commit=True) as cur: with self._get_cursor(commit=True) as cur:
cur.execute(sql.SQL("DELETE FROM {} WHERE id = %s").format(self._col()), (vector_id,)) cur.execute(sql.SQL("DELETE FROM {} WHERE id = %s").format(self._col()), (vector_id,))
@@ -395,6 +423,7 @@ class PGVector(VectorStoreBase):
vector (List[float], optional): Updated vector. vector (List[float], optional): Updated vector.
payload (Dict, optional): Updated payload. payload (Dict, optional): Updated payload.
""" """
self._ensure_collection()
with self._get_cursor(commit=True) as cur: with self._get_cursor(commit=True) as cur:
if vector: if vector:
cur.execute( cur.execute(
@@ -427,6 +456,7 @@ class PGVector(VectorStoreBase):
Returns: Returns:
OutputData: Retrieved vector. OutputData: Retrieved vector.
""" """
self._ensure_collection()
with self._get_cursor() as cur: with self._get_cursor() as cur:
cur.execute( cur.execute(
sql.SQL("SELECT id, vector, payload FROM {} WHERE id = %s").format(self._col()), sql.SQL("SELECT id, vector, payload FROM {} WHERE id = %s").format(self._col()),
@@ -460,6 +490,7 @@ class PGVector(VectorStoreBase):
Returns: Returns:
Dict[str, Any]: Collection information. Dict[str, Any]: Collection information.
""" """
self._ensure_collection()
with self._get_cursor() as cur: with self._get_cursor() as cur:
cur.execute( cur.execute(
sql.SQL(""" sql.SQL("""
@@ -490,6 +521,7 @@ class PGVector(VectorStoreBase):
Returns: Returns:
List[OutputData]: List of vectors. List[OutputData]: List of vectors.
""" """
self._ensure_collection()
filter_conditions, filter_params = _build_filter_conditions(filters) filter_conditions, filter_params = _build_filter_conditions(filters)
filter_clause = sql.SQL("WHERE " + " AND ".join(filter_conditions)) if filter_conditions else sql.SQL("") filter_clause = sql.SQL("WHERE " + " AND ".join(filter_conditions)) if filter_conditions else sql.SQL("")
@@ -521,6 +553,7 @@ class PGVector(VectorStoreBase):
def reset(self) -> None: def reset(self) -> None:
"""Reset the index by deleting and recreating it.""" """Reset the index by deleting and recreating it."""
self._ensure_collection()
logger.warning(f"Resetting index {self.collection_name}...") logger.warning(f"Resetting index {self.collection_name}...")
self.delete_col() self.delete_col()
self.create_col() self.create_col()
+1 -1
View File
@@ -158,7 +158,7 @@ class RedisDB(VectorStoreBase):
return [ return [
MemoryResult( MemoryResult(
id=result["memory_id"], id=result["memory_id"],
score=float(result["vector_distance"]), score=max(0.0, 1.0 - float(result["vector_distance"])),
payload={ payload={
"hash": result["hash"], "hash": result["hash"],
"data": result["memory"], "data": result["memory"],
+13 -6
View File
@@ -80,7 +80,9 @@ class S3Vectors(VectorStoreBase):
except json.JSONDecodeError: except json.JSONDecodeError:
logger.warning(f"Failed to parse metadata for key {v.get('key')}") logger.warning(f"Failed to parse metadata for key {v.get('key')}")
payload = {} payload = {}
results.append(OutputData(id=v.get("key"), score=v.get("distance"), payload=payload)) raw_distance = v.get("distance")
score = max(0.0, 1.0 - raw_distance) if raw_distance is not None else None
results.append(OutputData(id=v.get("key"), score=score, payload=payload))
return results return results
def insert(self, vectors, payloads=None, ids=None): def insert(self, vectors, payloads=None, ids=None):
@@ -180,10 +182,6 @@ class S3Vectors(VectorStoreBase):
return response.get("index", {}) return response.get("index", {})
def list(self, filters=None, top_k=None): def list(self, filters=None, top_k=None):
# Note: list_vectors does not support metadata filtering.
if filters:
logger.warning("S3 Vectors `list` does not support metadata filtering. Ignoring filters.")
params = { params = {
"vectorBucketName": self.vector_bucket_name, "vectorBucketName": self.vector_bucket_name,
"indexName": self.collection_name, "indexName": self.collection_name,
@@ -198,7 +196,16 @@ class S3Vectors(VectorStoreBase):
all_vectors = [] all_vectors = []
for page in pages: for page in pages:
all_vectors.extend(page.get("vectors", [])) all_vectors.extend(page.get("vectors", []))
return [self._parse_output(all_vectors)] results = self._parse_output(all_vectors)
if filters:
results = [
result
for result in results
if result.payload and all(result.payload.get(k) == v for k, v in filters.items())
]
if top_k:
results = results[:top_k]
return [results]
def reset(self): def reset(self):
logger.warning(f"Resetting index {self.collection_name}...") logger.warning(f"Resetting index {self.collection_name}...")
+1 -1
View File
@@ -135,7 +135,7 @@ class Supabase(VectorStoreBase):
data=vectors, limit=top_k, filters=filters, include_metadata=True, include_value=True data=vectors, limit=top_k, filters=filters, include_metadata=True, include_value=True
) )
return [OutputData(id=str(result[0]), score=float(result[1]), payload=result[2]) for result in results] return [OutputData(id=str(result[0]), score=max(0.0, 1.0 - float(result[1])), payload=result[2]) for result in results]
def delete(self, vector_id: str): def delete(self, vector_id: str):
""" """
+1 -2
View File
@@ -132,11 +132,10 @@ class UpstashVector(VectorStoreBase):
"top_k": top_k, "top_k": top_k,
"filter": filters_str or "", "filter": filters_str or "",
"include_metadata": True, "include_metadata": True,
"namespace": self.collection_name,
} }
for v in vectors for v in vectors
] ]
responses = self.client.query_many(queries=queries) responses = self.client.query_many(queries=queries, namespace=self.collection_name)
# flatten # flatten
response = [res for res_list in responses for res in res_list] response = [res for res_list in responses for res in res_list]
+2 -2
View File
@@ -388,8 +388,8 @@ class ValkeyDB(VectorStoreBase):
""" """
memory_results = [] memory_results = []
for doc in results.docs: for doc in results.docs:
# Extract the score raw_distance = float(doc.vector_score) if hasattr(doc, "vector_score") else None
score = float(doc.vector_score) if hasattr(doc, "vector_score") else None score = max(0.0, 1.0 - raw_distance) if raw_distance is not None else None
# Create the payload # Create the payload
payload = { payload = {
@@ -114,10 +114,12 @@ class GoogleMatchingEngine(VectorStoreBase):
results = data.get("nearestNeighbors", {}).get("neighbors", []) results = data.get("nearestNeighbors", {}).get("neighbors", [])
output_data = [] output_data = []
for result in results: for result in results:
raw_distance = result.get("distance")
score = max(0.0, 1.0 - raw_distance) if raw_distance is not None else None
output_data.append( output_data.append(
OutputData( OutputData(
id=result.get("datapoint").get("datapointId"), id=result.get("datapoint").get("datapointId"),
score=result.get("distance"), score=score,
payload=result.get("datapoint").get("metadata"), payload=result.get("datapoint").get("metadata"),
) )
) )
@@ -264,7 +266,8 @@ class GoogleMatchingEngine(VectorStoreBase):
logger.debug("Adding %s: %s", restrict.name, restrict.allow_tokens[0]) logger.debug("Adding %s: %s", restrict.name, restrict.allow_tokens[0])
payload[restrict.name] = restrict.allow_tokens[0] payload[restrict.name] = restrict.allow_tokens[0]
output_data = OutputData(id=neighbor.id, score=neighbor.distance, payload=payload) score = max(0.0, 1.0 - neighbor.distance) if neighbor.distance is not None else None
output_data = OutputData(id=neighbor.id, score=score, payload=payload)
results.append(output_data) results.append(output_data)
logger.debug("Returning %d results", len(results)) logger.debug("Returning %d results", len(results))
@@ -413,7 +416,8 @@ class GoogleMatchingEngine(VectorStoreBase):
if restrict.allow_list: if restrict.allow_list:
payload[restrict.namespace] = restrict.allow_list[0] payload[restrict.namespace] = restrict.allow_list[0]
return OutputData(id=neighbor.datapoint.datapoint_id, score=neighbor.distance, payload=payload) score = max(0.0, 1.0 - neighbor.distance) if neighbor.distance is not None else None
return OutputData(id=neighbor.datapoint.datapoint_id, score=score, payload=payload)
logger.debug("No results found") logger.debug("No results found")
return None return None
+11 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "@mem0/openclaw-mem0", "name": "@mem0/openclaw-mem0",
"version": "1.0.11", "version": "1.0.12",
"type": "module", "type": "module",
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source", "description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
"license": "Apache-2.0", "license": "Apache-2.0",
@@ -35,7 +35,7 @@
}, },
"dependencies": { "dependencies": {
"@sinclair/typebox": "0.34.47", "@sinclair/typebox": "0.34.47",
"mem0ai": "3.0.3" "mem0ai": "3.0.6"
}, },
"openclaw": { "openclaw": {
"extensions": [ "extensions": [
@@ -54,15 +54,21 @@
} }
}, },
"devDependencies": { "devDependencies": {
"@qdrant/js-client-rest": "^1.18.0",
"@types/node": "^22.15.0", "@types/node": "^22.15.0",
"@vitest/coverage-v8": "^4.0.18", "@vitest/coverage-v8": "^4.1.7",
"tsup": "^8.5.0", "tsup": "^8.5.0",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vitest": "^4.0.18" "vite": "^8.0.5",
"vitest": "^4.1.7"
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {
"protobufjs@<7.5.5": "^7.5.5" "protobufjs@<7.5.5": "^7.5.5",
"vite": "^8.0.5",
"langsmith@<0.6.0": "^0.6.0",
"picomatch@<2.3.2": "^2.3.2",
"@qdrant/js-client-rest": "^1.18.0"
} }
} }
} }
+809 -1039
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -1,7 +1,20 @@
packages: packages:
- '.' - '.'
allowBuilds:
'@google/genai': set this to true or false
better-sqlite3: set this to true or false
esbuild: set this to true or false
protobufjs: set this to true or false
onlyBuiltDependencies: onlyBuiltDependencies:
- better-sqlite3 - better-sqlite3
- esbuild - esbuild
- protobufjs - protobufjs
overrides:
"protobufjs@<7.5.5": "^7.5.5"
"vite": "^8.0.5"
"langsmith@<0.6.0": "^0.6.0"
"picomatch@<2.3.2": "^2.3.2"
"@qdrant/js-client-rest": "^1.18.0"
+8 -2
View File
@@ -39,7 +39,7 @@
"@radix-ui/react-tooltip": "^1.1.6", "@radix-ui/react-tooltip": "^1.1.6",
"@reduxjs/toolkit": "^2.7.0", "@reduxjs/toolkit": "^2.7.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"axios": "^1.15.2", "axios": "^1.16.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "1.0.4", "cmdk": "1.0.4",
@@ -79,6 +79,12 @@
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"@parcel/watcher", "@parcel/watcher",
"sharp" "sharp"
] ],
"overrides": {
"immutable@>=5.0.0 <5.1.5": "^5.1.5",
"picomatch@<2.3.2": "^2.3.2",
"minimatch@>=9.0.0 <9.0.7": "^9.0.7",
"glob@>=10.2.0 <10.5.0": "^10.5.0"
}
} }
} }
+31 -23
View File
@@ -4,6 +4,12 @@ settings:
autoInstallPeers: true autoInstallPeers: true
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
overrides:
immutable@>=5.0.0 <5.1.5: ^5.1.5
picomatch@<2.3.2: ^2.3.2
minimatch@>=9.0.0 <9.0.7: ^9.0.7
glob@>=10.2.0 <10.5.0: ^10.5.0
importers: importers:
.: .:
@@ -99,7 +105,7 @@ importers:
specifier: ^10.4.20 specifier: ^10.4.20
version: 10.4.20(postcss@8.0.0) version: 10.4.20(postcss@8.0.0)
axios: axios:
specifier: ^1.15.2 specifier: ^1.16.0
version: 1.16.1 version: 1.16.1
class-variance-authority: class-variance-authority:
specifier: ^0.7.1 specifier: ^0.7.1
@@ -1309,8 +1315,8 @@ packages:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'} engines: {node: '>=8'}
brace-expansion@2.0.1: brace-expansion@2.1.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==}
braces@3.0.3: braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
@@ -1585,8 +1591,9 @@ packages:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
glob@10.4.5: glob@10.5.0:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true hasBin: true
gopd@1.2.0: gopd@1.2.0:
@@ -1612,8 +1619,8 @@ packages:
immer@10.1.1: immer@10.1.1:
resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
immutable@5.1.1: immutable@5.1.5:
resolution: {integrity: sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==} resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==}
input-otp@1.4.1: input-otp@1.4.1:
resolution: {integrity: sha512-+yvpmKYKHi9jIGngxagY9oWiiblPB7+nEO75F2l2o4vs+6vpPZZmUl4tBNYuTCvQjhvEIbdNeJu70bhfYP2nbw==} resolution: {integrity: sha512-+yvpmKYKHi9jIGngxagY9oWiiblPB7+nEO75F2l2o4vs+6vpPZZmUl4tBNYuTCvQjhvEIbdNeJu70bhfYP2nbw==}
@@ -1714,8 +1721,8 @@ packages:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
minimatch@9.0.5: minimatch@9.0.9:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
minipass@7.1.2: minipass@7.1.2:
@@ -1799,8 +1806,8 @@ packages:
picocolors@1.1.1: picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@2.3.1: picomatch@2.3.2:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
pify@2.3.0: pify@2.3.0:
@@ -1979,6 +1986,7 @@ packages:
recharts@2.15.0: recharts@2.15.0:
resolution: {integrity: sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==} resolution: {integrity: sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==}
engines: {node: '>=14'} engines: {node: '>=14'}
deprecated: 1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide
peerDependencies: peerDependencies:
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
@@ -3227,7 +3235,7 @@ snapshots:
anymatch@3.1.3: anymatch@3.1.3:
dependencies: dependencies:
normalize-path: 3.0.0 normalize-path: 3.0.0
picomatch: 2.3.1 picomatch: 2.3.2
arg@5.0.2: {} arg@5.0.2: {}
@@ -3261,7 +3269,7 @@ snapshots:
binary-extensions@2.3.0: {} binary-extensions@2.3.0: {}
brace-expansion@2.0.1: brace-expansion@2.1.1:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
@@ -3523,11 +3531,11 @@ snapshots:
dependencies: dependencies:
is-glob: 4.0.3 is-glob: 4.0.3
glob@10.4.5: glob@10.5.0:
dependencies: dependencies:
foreground-child: 3.3.1 foreground-child: 3.3.1
jackspeak: 3.4.3 jackspeak: 3.4.3
minimatch: 9.0.5 minimatch: 9.0.9
minipass: 7.1.2 minipass: 7.1.2
package-json-from-dist: 1.0.1 package-json-from-dist: 1.0.1
path-scurry: 1.11.1 path-scurry: 1.11.1
@@ -3553,7 +3561,7 @@ snapshots:
immer@10.1.1: {} immer@10.1.1: {}
immutable@5.1.1: {} immutable@5.1.5: {}
input-otp@1.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): input-otp@1.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies: dependencies:
@@ -3626,7 +3634,7 @@ snapshots:
micromatch@4.0.8: micromatch@4.0.8:
dependencies: dependencies:
braces: 3.0.3 braces: 3.0.3
picomatch: 2.3.1 picomatch: 2.3.2
mime-db@1.52.0: {} mime-db@1.52.0: {}
@@ -3634,9 +3642,9 @@ snapshots:
dependencies: dependencies:
mime-db: 1.52.0 mime-db: 1.52.0
minimatch@9.0.5: minimatch@9.0.9:
dependencies: dependencies:
brace-expansion: 2.0.1 brace-expansion: 2.1.1
minipass@7.1.2: {} minipass@7.1.2: {}
@@ -3705,7 +3713,7 @@ snapshots:
picocolors@1.1.1: {} picocolors@1.1.1: {}
picomatch@2.3.1: {} picomatch@2.3.2: {}
pify@2.3.0: {} pify@2.3.0: {}
@@ -3859,7 +3867,7 @@ snapshots:
readdirp@3.6.0: readdirp@3.6.0:
dependencies: dependencies:
picomatch: 2.3.1 picomatch: 2.3.2
readdirp@4.1.2: {} readdirp@4.1.2: {}
@@ -3905,7 +3913,7 @@ snapshots:
sass@1.86.3: sass@1.86.3:
dependencies: dependencies:
chokidar: 4.0.3 chokidar: 4.0.3
immutable: 5.1.1 immutable: 5.1.5
source-map-js: 1.2.1 source-map-js: 1.2.1
optionalDependencies: optionalDependencies:
'@parcel/watcher': 2.5.1 '@parcel/watcher': 2.5.1
@@ -3993,7 +4001,7 @@ snapshots:
dependencies: dependencies:
'@jridgewell/gen-mapping': 0.3.8 '@jridgewell/gen-mapping': 0.3.8
commander: 4.1.1 commander: 4.1.1
glob: 10.4.5 glob: 10.5.0
lines-and-columns: 1.2.4 lines-and-columns: 1.2.4
mz: 2.7.0 mz: 2.7.0
pirates: 4.0.7 pirates: 4.0.7
+12
View File
@@ -0,0 +1,12 @@
packages:
- '.'
onlyBuiltDependencies:
- "@parcel/watcher"
- sharp
overrides:
"immutable@>=5.0.0 <5.1.5": "^5.1.5"
"picomatch@<2.3.2": "^2.3.2"
"minimatch@>=9.0.0 <9.0.7": "^9.0.7"
"glob@>=10.2.0 <10.5.0": "^10.5.0"
+55
View File
@@ -0,0 +1,55 @@
# Changelog
## 0.1.1 (2026-06-10)
Maintenance release — no functional changes.
### Chores
- Version bump to validate the new release pipeline (`pi-agent-plugin-checks.yml` / `pi-agent-plugin-cd.yml`)
## 0.1.0 (2026-06-09)
Initial release of `@mem0/pi-agent-plugin` — persistent semantic memory for Pi Agent.
### Features
- **Extension entry point** — registers `mem0_memory` tool, 8 slash commands, and auto-capture hooks
- **Agent tool** (`mem0_memory`) — search, add, get_all, delete, delete_all with scoped filters
- **Auto-capture** — extracts and stores memories from both user and assistant messages on `agent_end`
- **Dream consolidation** — automated memory maintenance: merge duplicates, resolve contradictions, prune stale entries. Gated by session count, time elapsed, and memory count thresholds
- **System prompt injection** — appends `MEMORY_POLICY` to every agent turn via `before_agent_start`
- **Monorepo-aware project scoping** — uses `git rev-parse --show-toplevel` for consistent app_id across subdirectories
- **3 memory scopes** — project (default), session, global
- **10 memory categories** — identity, preferences, goals, projects, decisions, technical, relationships, routines, lessons, work
- **8 skills** — context-loader, remember, search, forget, dream, tour, pin, status
- **Confirmation dialogs** — `/mem0-forget` and `/mem0-pin` ask for confirmation before destructive or mutating actions via `ctx.ui.confirm()`
- **Pin preserves memory ID** — `/mem0-pin` uses `mem0.update()` instead of add+delete, keeping the original UUID
- **Full memory IDs** — all displayed memory references show the complete UUID, not truncated short IDs
- **Dream gate optimization** — `dreamChecked` flag prevents repeated `getAll` API calls when the memory gate fails
- **Output truncation** — tool results capped at 200 lines / 50KB per Pi docs
- **Signal cancellation** — all tool actions respect `AbortSignal`
- **Session shutdown cleanup** — releases dream lock on `session_shutdown`
- **PostHog telemetry** — batched event queue with PII-safe error payloads
### Commands
| Command | Description |
|---------|-------------|
| `/mem0-remember` | Store a memory verbatim (no inference) |
| `/mem0-forget` | Search and delete memories (with confirmation) |
| `/mem0-search` | Semantic search across memories |
| `/mem0-tour` | Browse all memories by category |
| `/mem0-dream` | Trigger memory consolidation |
| `/mem0-pin` | Pin a memory to protect from pruning (preserves ID) |
| `/mem0-scope` | Change default scope for this session |
| `/mem0-status` | Connection health and diagnostics |
### Hooks
| Hook | Purpose |
|------|---------|
| `session_start` | Detect project (git root), resolve session ID, increment dream counter |
| `before_agent_start` | Inject memory policy into system prompt, auto-trigger dream if gates pass |
| `agent_end` | Auto-capture conversation memories, check dream completion |
| `session_shutdown` | Release dream lock, flush telemetry |
+201
View File
@@ -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 [2026] [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.
+144
View File
@@ -0,0 +1,144 @@
# @mem0/pi-agent-plugin
Persistent semantic memory for [Pi Agent](https://pi.dev), powered by [Mem0](https://mem0.ai).
This extension gives Pi Agent long-term memory that persists across sessions, projects, and devices. Memories are automatically captured from conversations and can be searched, managed, and consolidated through slash commands and an agent-accessible tool.
## Features
- **Automatic memory capture** — learns from every conversation (both user and assistant messages)
- **Semantic search** — find memories by meaning, not just keywords
- **Scoped memory** — project, session, or global scope
- **Monorepo-aware** — uses git root for project detection, consistent app_id across subdirectories
- **Dream consolidation** — merges duplicates, resolves contradictions, prunes stale entries
- **Confirmation dialogs** — destructive commands ask before acting
- **8 slash commands** — essential memory management from the command line
- **Agent tool** — `mem0_memory` tool lets the agent search and store memories autonomously
## Setup
### 1. Get an API key
Sign up at [app.mem0.ai](https://app.mem0.ai/dashboard/api-keys) and copy your API key.
### 2. Install
```bash
pi install npm:@mem0/pi-agent-plugin
```
### 3. Configure
Set the API key as an environment variable:
```bash
export MEM0_API_KEY="m0-your-key-here"
```
Or create a config file at `~/.pi/agent/mem0-config.json`:
```json
{
"apiKey": "m0-your-key-here",
"userId": "your-username",
"autoCapture": true,
"defaultScope": "project",
"dream": {
"enabled": true,
"auto": true,
"minHours": 24,
"minSessions": 5,
"minMemories": 20
}
}
```
Environment variables (`MEM0_API_KEY`, `MEM0_USER_ID`) override the config file.
## Commands
| Command | Description |
|---------|-------------|
| `/mem0-remember <text>` | Store a memory verbatim (no inference) |
| `/mem0-forget <query>` | Search and delete memories (with confirmation) |
| `/mem0-search <query>` | Semantic search across memories |
| `/mem0-tour [scope]` | Browse all memories grouped by category |
| `/mem0-dream` | Consolidate — merge duplicates, prune stale, resolve contradictions |
| `/mem0-pin <query>` | Pin a memory to protect from dream pruning (preserves ID) |
| `/mem0-scope <scope>` | Change default scope for this session |
| `/mem0-status` | Connection health, identity, and memory count |
## Skills
The plugin includes 8 skills that guide the agent on how to use each capability:
| Skill | Purpose |
|-------|---------|
| `context-loader` | Pre-fetch relevant memories at session start |
| `remember` | Store facts with category classification |
| `search` | Quick semantic search with compact results |
| `forget` | Delete memories with confirmation |
| `dream` | Memory consolidation workflow |
| `tour` | Full memory walkthrough by category |
| `pin` | Protect critical memories from pruning |
| `status` | Health check and diagnostics |
## Memory Scopes
| Scope | Filters | Use case |
|-------|---------|----------|
| `project` | user + app_id (git root) | Default. Project-specific knowledge |
| `session` | user + app_id + run_id | Ephemeral, session-only context |
| `global` | user only | All memories across all your projects |
Project scoping uses `git rev-parse --show-toplevel` to detect the repository root, so all subdirectories within a monorepo share the same memory pool.
## Memory Categories
Memories are automatically classified into 10 general-purpose categories:
| Category | Description |
|----------|-------------|
| `identity` | Personal details, background, self-descriptions |
| `preferences` | Likes, dislikes, habits, preferred approaches |
| `goals` | Objectives, aspirations, targets |
| `projects` | Ongoing work, initiatives, areas of focus |
| `decisions` | Choices made, rationale, trade-offs |
| `technical` | Technical knowledge, tools, configurations |
| `relationships` | People, teams, organizations |
| `routines` | Recurring patterns, workflows, schedules |
| `lessons` | Insights learned, mistakes to avoid |
| `work` | Professional context, role, responsibilities |
## Architecture
```
pi-agent-plugin/
├── src/
│ ├── entry.ts # Extension entry point
│ ├── index.ts # Barrel exports
│ ├── commands.ts # 8 slash commands
│ ├── prompt.ts # System prompt injection (MEMORY_POLICY)
│ ├── types.ts # Shared interfaces and categories
│ ├── telemetry.ts # PostHog telemetry (batched, PII-safe)
│ ├── config/ # Config loading (~/.pi/agent/mem0-config.json)
│ ├── memory/ # Tool registration, scoping (git root), formatting
│ ├── capture/ # Auto-capture from conversations (user + assistant)
│ └── dream/ # Consolidation state, gating, locking, prompts
├── skills/ # 8 SKILL.md files for Pi Agent
├── tests/ # Vitest unit tests
└── dist/ # Built output (ESM + DTS)
```
## Development
```bash
pnpm install # Install dependencies
pnpm run typecheck # Type check
pnpm run test # Run tests
pnpm run build # Build (ESM + declarations)
```
## License
[Apache-2.0](LICENSE)
+68
View File
@@ -0,0 +1,68 @@
{
"name": "@mem0/pi-agent-plugin",
"version": "0.1.1",
"type": "module",
"description": "Mem0 memory extension for Pi Agent persistent, scoped, semantic memory across sessions and projects",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/mem0ai/mem0",
"directory": "pi-agent-plugin"
},
"keywords": [
"pi-package",
"pi-extension",
"skills",
"memory",
"mem0",
"semantic-memory",
"persistent-memory",
"agent-memory"
],
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"publishConfig": {
"access": "public"
},
"files": [
"dist",
"src",
"skills",
"CHANGELOG.md",
"README.md",
"LICENSE"
],
"pi": {
"extensions": ["./src/entry.ts"],
"skills": ["./skills"]
},
"scripts": {
"build": "tsup",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"@earendil-works/pi-coding-agent": "*",
"@earendil-works/pi-ai": "*",
"typebox": "*"
},
"devDependencies": {
"@earendil-works/pi-ai": "^0.79.0",
"@earendil-works/pi-coding-agent": "^0.79.0",
"@types/node": "^25.9.2",
"tsup": "^8.5.0",
"typebox": "^1.2.3",
"typescript": "^6.0.3",
"vitest": "^4.1.7"
},
"dependencies": {
"mem0ai": "^3.0.6"
}
}
+4923
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,47 @@
---
name: context-loader
description: Searches and injects relevant memories into context before starting work on a task or topic. Use when beginning a new task, switching context, or when past decisions, preferences, or knowledge need to be loaded.
---
# Context Loader
Pre-fetches relevant memories to prime context before working on a task or topic.
## When to use
- Session start (auto-triggered by the extension's `before_agent_start` event)
- User starts work on a specific topic or area
- User says "what do we know about X" or "context for X"
## Steps
1. **Extract topics** from current message/task. Identify: subject areas, people mentioned, project names, goal references.
2. **Run 2-4 parallel searches** using `mem0_memory` tool with `action="search"` and different query angles:
| Query angle | Purpose |
|---|---|
| Topic/subject name | Relevant decisions and preferences |
| People mentioned | Relationship context |
| Project/goal references | Progress and background |
| Broad context | Catch-all for anything relevant |
3. **Deduplicate** results by memory ID across all search responses.
4. **Output compact context block** (max 10 memories):
```
context-loader: loaded <N> memories for "<task summary>"
- [decisions] <content> [mem0:<short_id>]
- [preferences] <content> [mem0:<short_id>]
- [lessons] <content> [mem0:<short_id>]
```
5. If **zero results**: output nothing. Don't announce empty context.
## Constraints
- **Read-only** — never modify or delete memories
- **Max 10 memories** returned (most relevant only)
- **Silent on empty** — only surfaces findings if relevant context exists
- Skip memories already visible in current session context
+125
View File
@@ -0,0 +1,125 @@
---
name: dream
description: Consolidates stored memories by merging duplicates, resolving contradictions, and pruning stale entries. Use when memory count is high, search results feel noisy or repetitive, or periodic cleanup is needed to maintain memory quality.
---
# Dream — Memory Consolidation
This skill performs a memory consolidation pass: it fetches all memories, identifies near-duplicates, flags contradictions, and prunes stale entries. All proposed changes are shown as a diff for user approval before anything is modified.
**IMPORTANT: Execute steps strictly in order (1 -> 2 -> 3 -> 4 -> 5). Each step depends on the previous one. Do NOT run steps in parallel or skip ahead.**
## Step 1: Fetch ALL Memories
Use `mem0_memory` tool with `action="get_all"` to retrieve every memory.
If zero memories are found, print:
```
No memories found. Nothing to consolidate.
```
...and stop.
## Step 2: Analyze — Find Issues
Work entirely in-memory; do not modify anything yet.
Group memories by category. For each group, identify the following:
### 2a. Near-duplicate pairs (merge candidates)
Two memories are near-duplicates when they express the same fact but phrased differently (e.g., "Prefers morning meetings" and "Likes scheduling meetings early").
Heuristics — two memories are near-duplicates if **all** of these hold:
- If >60% of significant nouns/keywords overlap, treat as near-duplicate.
- Same category.
- Neither memory is pinned (content does not start with `[PINNED]`).
For each qualifying pair, draft a merged version that is more complete than either original.
### 2b. Contradictions
Two memories contradict when they assert opposing facts about the same topic (e.g., "Prefers cats" vs. "Allergic to cats, prefers dogs").
Identify the likely winner: the more recent memory wins. Store both IDs and their content for user review.
### 2c. Prune candidates
A memory is a prune candidate when **any** of the following is true:
1. It is older than 180 days AND has not been accessed recently.
2. Its content is extremely vague (fewer than 5 meaningful words).
**Always skip memories where content starts with `[PINNED]`**, regardless of age.
## Step 3: Print Diff Report
Print a structured diff before making any changes:
```
## dream — consolidation report
Merges (<N>):
[mem0:<id1>] + [mem0:<id2>] -> "<merged content, 100 chars>"
Conflicts (<N>):
[mem0:<idA>] vs [mem0:<idB>] — "<topic>" [A/B/skip]
Prune (<N>):
[mem0:<id>] — <category>, <age>d old
Proposed: <N> merges, <N> prunes, <N> conflicts. Apply? [Y/n]
```
If there are zero total proposals, print:
```
Dream complete. No duplicate, contradictory, or stale memories found.
```
...and stop.
## Step 4: Wait for User Input and Apply
### 4a. Contradictions
For each conflict pair, wait for the user to choose A, B, or skip.
### 4b. Final confirmation
After all conflict resolutions are collected, prompt: `Apply? [Y/n]`
If the user declines, print `Cancelled. No changes made.` and stop.
If confirmed, apply all changes:
**Merges:** Delete both originals, add the merged version using `mem0_memory` with `action="add"`.
**Contradictions (resolved):** Delete the loser using `mem0_memory` with `action="delete"`.
**Prunes:** Delete each using `mem0_memory` with `action="delete"`.
## Step 5: Print Summary
```
Dream complete — merged: <N>, pruned: <N>, conflicts resolved: <N>, skipped: <N>
```
## Auto mode
When invoked with `--auto` (e.g., `/mem0-dream --auto`), run non-interactively:
- **Merges**: applied automatically.
- **Prunes**: applied automatically.
- **Contradictions**: skipped — they require human judgment.
Print a compact summary:
```
[mem0-dream --auto] merged=<N> pruned=<N> conflicts_skipped=<N>
```
## See also
- `/mem0-forget` — targeted deletion of specific memories
- `/mem0-status` — quick health check
+53
View File
@@ -0,0 +1,53 @@
---
name: forget
description: Deletes memories by search query or memory ID with confirmation before removal. Use when removing outdated information, incorrect memories, sensitive data, or cleaning up after experiments.
---
# Forget
Delete specific memories from Mem0.
## Execution
### Step 1: Parse input
The user provides either:
- A search query: `/mem0-forget travel plans`
- A memory ID: `/mem0-forget <memory_id>`
If no argument, ask: "What should I forget? Provide a search query or memory ID."
### Step 2: Find memories
**If memory ID provided** (looks like a UUID or hex string):
- Use `mem0_memory` tool with `action="search"` and the ID as query, or look it up directly.
- Show: `Found: "<memory content first 120 chars>" (created <date>)`
**If search query provided:**
- Use `mem0_memory` tool with `action="search"`, `query=<user's query>`.
- Show numbered list:
```
Found <N> memories matching "<query>":
1. <content, 120 chars> [<category>] [ID: <short_id>]
2. ...
```
### Step 3: Confirm
Ask: "Delete which memories? Enter numbers (e.g., 1,3,5), 'all', or 'cancel'."
For a single memory ID, ask: "Delete this memory? [y/N]"
**Never delete without confirmation.** This is destructive.
### Step 4: Delete
For each confirmed memory, use `mem0_memory` tool with `action="delete"` and the memory ID.
### Step 5: Report
```
Deleted <N> memories.
```
If any deletions failed, report which ones and why.

Some files were not shown because too many files have changed in this diff Show More