chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
<p align="center">
|
||||
<img src="../../assets/banner.png" alt="agentmemory" width="640" />
|
||||
</p>
|
||||
|
||||
<h1 align="center">
|
||||
<img src="https://github.com/openclaw.png?size=80" alt="OpenClaw" width="28" height="28" align="center" />
|
||||
agentmemory for OpenClaw
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Your OpenClaw agents remember everything. No more re-explaining.</strong><br/>
|
||||
<sub>Persistent cross-session memory via <a href="https://github.com/rohitg00/agentmemory">agentmemory</a> — 95.2% retrieval accuracy on <a href="https://arxiv.org/abs/2410.10813">LongMemEval-S</a>.</sub>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/MCP-43_tools-1f6feb?style=flat-square" alt="43 MCP tools" />
|
||||
<img src="https://img.shields.io/badge/Plugin-memory_slot-1f6feb?style=flat-square" alt="OpenClaw memory plugin" />
|
||||
<img src="https://img.shields.io/badge/R@5-95.2%25-00875f?style=flat-square" alt="95.2% R@5" />
|
||||
<img src="https://img.shields.io/badge/Self--hosted-yes-00875f?style=flat-square" alt="Self-hosted" />
|
||||
<img src="https://img.shields.io/badge/License-Apache_2.0-blue?style=flat-square" alt="Apache 2.0" />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Install it in 30 seconds
|
||||
|
||||
**Paste this prompt into OpenClaw** and it does the whole setup for you:
|
||||
|
||||
```text
|
||||
Install agentmemory for OpenClaw. Run `npx @agentmemory/agentmemory` in a separate terminal to start the memory server on localhost:3111.
|
||||
|
||||
For zero-code setup, add this MCP server so OpenClaw gets all 43 memory tools:
|
||||
|
||||
{
|
||||
"mcpServers": {
|
||||
"agentmemory": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@agentmemory/mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
For deeper memory integration, copy `integrations/openclaw` from the agentmemory repo to `~/.openclaw/extensions/agentmemory`, then enable it in `~/.openclaw/openclaw.json`:
|
||||
|
||||
{
|
||||
"plugins": {
|
||||
"slots": {
|
||||
"memory": "agentmemory"
|
||||
},
|
||||
"entries": {
|
||||
"agentmemory": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"base_url": "http://localhost:3111",
|
||||
"token_budget": 2000,
|
||||
"min_confidence": 0.5,
|
||||
"fallback_on_error": true,
|
||||
"timeout_ms": 5000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Restart OpenClaw. Verify with `curl http://localhost:3111/agentmemory/health`. Open http://localhost:3113 for the real-time viewer.
|
||||
```
|
||||
|
||||
That's it. OpenClaw handles the rest.
|
||||
|
||||
## Option 1: MCP server (zero code)
|
||||
|
||||
Start the agentmemory server in a separate terminal:
|
||||
|
||||
```bash
|
||||
npx @agentmemory/agentmemory
|
||||
```
|
||||
|
||||
Then add to your OpenClaw MCP config:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"agentmemory": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@agentmemory/mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
OpenClaw now has access to all 43 MCP tools including `memory_recall`, `memory_save`, `memory_smart_search`, `memory_timeline`, `memory_profile`, and more.
|
||||
|
||||
## Option 2: OpenClaw memory plugin (deeper integration)
|
||||
|
||||
Copy this folder into OpenClaw's extension directory:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.openclaw/extensions
|
||||
cp -r integrations/openclaw ~/.openclaw/extensions/agentmemory
|
||||
```
|
||||
|
||||
Then enable it in `~/.openclaw/openclaw.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"slots": {
|
||||
"memory": "agentmemory"
|
||||
},
|
||||
"entries": {
|
||||
"agentmemory": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"base_url": "http://localhost:3111",
|
||||
"token_budget": 2000,
|
||||
"min_confidence": 0.5,
|
||||
"fallback_on_error": true,
|
||||
"timeout_ms": 5000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
What the plugin does:
|
||||
|
||||
- claims the `plugins.slots.memory = "agentmemory"` slot via `api.registerMemoryCapability({ promptBuilder })` so OpenClaw recognises it as the active memory plugin
|
||||
- recalls relevant long-term memory before the agent starts (via the `before_agent_start` hook)
|
||||
- captures completed conversation turns after the agent finishes (via the `agent_end` hook)
|
||||
- shares the same backend with Claude Code, Codex CLI, Gemini CLI, Hermes, pi, and other agents
|
||||
|
||||
### Memory runtime (current scope)
|
||||
|
||||
The plugin currently registers a `promptBuilder` only — not a full `MemoryPluginRuntime` adapter. OpenClaw's `MemoryRuntimeBackendConfig` type today is `{ backend: "builtin" }` or `{ backend: "qmd" }`; both are openclaw-internal backends that don't fit agentmemory's external REST shape. The hook-driven recall + capture flow above is the working integration path. If you need OpenClaw's in-process memory-runtime APIs (e.g. `getMemorySearchManager`) backed by agentmemory, file an upstream request against `openclaw` for an `"external"` backend type and we'll wire `runtime` here once the contract supports it.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Plugin validates but does not load** — make sure the folder contains `package.json`, `openclaw.plugin.json`, and `plugin.mjs`, and that `plugins.slots.memory` is set to `agentmemory`.
|
||||
|
||||
**`plugins.slots.memory = "agentmemory"` reports `unavailable`** — upgrade to v0.9.11+. Older versions of this plugin registered hooks but never called `api.registerMemoryCapability(...)`, so the memory-slot machinery did not consider the slot claimed. The current plugin registers a memory capability (prompt builder) at startup, which is the documented OpenClaw API for occupying the slot.
|
||||
|
||||
**Connection refused on port 3111** — the agentmemory server is not running. Start it with `npx @agentmemory/agentmemory`.
|
||||
|
||||
**No memories returned** — open `http://localhost:3113` and verify observations are being captured.
|
||||
|
||||
## See also
|
||||
|
||||
- [agentmemory main README](../../README.md)
|
||||
- [Hermes integration](../hermes/README.md)
|
||||
- [pi integration](../pi/README.md)
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0 (same as agentmemory)
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"id": "agentmemory",
|
||||
"kind": "memory",
|
||||
"name": "agentmemory",
|
||||
"description": "Persistent cross-session memory for OpenClaw via agentmemory.",
|
||||
"version": "0.9.4",
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"base_url": { "type": "string" },
|
||||
"token_budget": { "type": "number" },
|
||||
"min_confidence": { "type": "number" },
|
||||
"fallback_on_error": { "type": "boolean" },
|
||||
"timeout_ms": { "type": "number" }
|
||||
}
|
||||
},
|
||||
"uiHints": {
|
||||
"enabled": { "label": "Enabled" },
|
||||
"base_url": { "label": "Base URL", "help": "agentmemory REST server base URL" },
|
||||
"token_budget": { "label": "Token Budget", "help": "Approximate context budget to inject before the agent starts" },
|
||||
"min_confidence": { "label": "Min Confidence" },
|
||||
"fallback_on_error": { "label": "Fallback On Error" },
|
||||
"timeout_ms": { "label": "Timeout (ms)" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "agentmemory",
|
||||
"version": "0.9.4",
|
||||
"type": "module",
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./plugin.mjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* agentmemory plugin for OpenClaw
|
||||
*
|
||||
* Deeper integration than raw MCP:
|
||||
* - claims the plugins.slots.memory slot via api.registerMemoryCapability({ promptBuilder })
|
||||
* - recalls relevant memories before the agent starts (before_agent_start hook)
|
||||
* - captures completed conversation turns after the agent finishes (agent_end hook)
|
||||
*
|
||||
* Requires the agentmemory server on localhost:3111.
|
||||
* Start it with: npx @agentmemory/agentmemory
|
||||
*/
|
||||
|
||||
const DEFAULT_BASE_URL = "http://localhost:3111";
|
||||
const DEFAULT_TIMEOUT_MS = 5000;
|
||||
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]);
|
||||
|
||||
const configSchema = {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
enabled: { type: "boolean" },
|
||||
base_url: { type: "string" },
|
||||
token_budget: { type: "number" },
|
||||
min_confidence: { type: "number" },
|
||||
fallback_on_error: { type: "boolean" },
|
||||
timeout_ms: { type: "number" },
|
||||
},
|
||||
};
|
||||
|
||||
function extractText(content) {
|
||||
if (typeof content === "string") return content;
|
||||
if (!Array.isArray(content)) return "";
|
||||
return content
|
||||
.flatMap((block) => {
|
||||
if (!block || typeof block !== "object") return [];
|
||||
if (block.type === "text" && typeof block.text === "string") return [block.text];
|
||||
return [];
|
||||
})
|
||||
.join("\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function lastAssistantText(messages) {
|
||||
for (const message of [...messages].reverse()) {
|
||||
if (!message || typeof message !== "object") continue;
|
||||
if (message.role !== "assistant") continue;
|
||||
const text = extractText(message.content);
|
||||
if (text) return text;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function latestUserText(messages) {
|
||||
for (const message of [...messages].reverse()) {
|
||||
if (!message || typeof message !== "object") continue;
|
||||
if (message.role !== "user") continue;
|
||||
const text = extractText(message.content);
|
||||
if (text) return text;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function formatResults(results) {
|
||||
if (!Array.isArray(results) || results.length === 0) return "";
|
||||
return results
|
||||
.slice(0, 5)
|
||||
.map((result, index) => {
|
||||
const obs = result?.observation ?? result ?? {};
|
||||
const title = (obs.title || `Memory ${index + 1}`).trim();
|
||||
const narrative = (obs.narrative || "").trim();
|
||||
const type = (obs.type || "memory").trim();
|
||||
return `- ${title} (${type})${narrative ? `: ${narrative}` : ""}`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function normalizedHostname(hostname) {
|
||||
return hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
||||
}
|
||||
|
||||
function usesPlaintextBearerAuth(baseUrl, secret) {
|
||||
if (!secret) return false;
|
||||
try {
|
||||
const parsed = new URL(baseUrl);
|
||||
return parsed.protocol === "http:" && !LOOPBACK_HOSTS.has(normalizedHostname(parsed.hostname));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function plaintextBearerAuthMessage(baseUrl) {
|
||||
return `agentmemory: AGENTMEMORY_SECRET is configured for plaintext HTTP to ${baseUrl}. Bearer tokens and memory payloads can be observed on the network; use HTTPS or an SSH tunnel.`;
|
||||
}
|
||||
|
||||
export function createPlaintextBearerAuthGuard(warn, env) {
|
||||
let warned = false;
|
||||
return function guardPlaintextBearerAuth(baseUrl, secret) {
|
||||
if (!usesPlaintextBearerAuth(baseUrl, secret)) return;
|
||||
const message = plaintextBearerAuthMessage(baseUrl);
|
||||
if ((env || process.env).AGENTMEMORY_REQUIRE_HTTPS === "1") throw new Error(message);
|
||||
if (!warned) {
|
||||
warned = true;
|
||||
warn(message);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createClient(cfg, api) {
|
||||
const baseUrl = String(cfg.base_url || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
||||
const timeoutMs = Number(cfg.timeout_ms || DEFAULT_TIMEOUT_MS);
|
||||
const fallbackOnError = cfg.fallback_on_error !== false;
|
||||
const secret = process.env.AGENTMEMORY_SECRET;
|
||||
const guardPlaintextBearerAuth = createPlaintextBearerAuthGuard(
|
||||
(message) => api.logger.warn?.(message),
|
||||
);
|
||||
if (process.env.AGENTMEMORY_REQUIRE_HTTPS === "1") {
|
||||
guardPlaintextBearerAuth(baseUrl, secret);
|
||||
}
|
||||
|
||||
async function postJson(path, payload) {
|
||||
guardPlaintextBearerAuth(baseUrl, secret);
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
if (secret) headers.Authorization = `Bearer ${secret}`;
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}${path}`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(payload),
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
if (!res.ok) {
|
||||
if (fallbackOnError) return null;
|
||||
const body = await res.text().catch(() => "");
|
||||
throw new Error(`agentmemory ${path} failed: ${res.status} ${body}`);
|
||||
}
|
||||
return await res.json();
|
||||
} catch (error) {
|
||||
if (!fallbackOnError) throw error;
|
||||
api.logger.warn?.(`agentmemory: ${String(error)}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return { postJson, baseUrl };
|
||||
}
|
||||
|
||||
const plugin = {
|
||||
id: "agentmemory",
|
||||
name: "agentmemory",
|
||||
description: "Shared cross-session memory via the local agentmemory server.",
|
||||
configSchema,
|
||||
register(api) {
|
||||
const cfg = {
|
||||
enabled: api.pluginConfig?.enabled !== false,
|
||||
base_url: api.pluginConfig?.base_url || DEFAULT_BASE_URL,
|
||||
token_budget: api.pluginConfig?.token_budget || 2000,
|
||||
min_confidence: api.pluginConfig?.min_confidence || 0.5,
|
||||
fallback_on_error: api.pluginConfig?.fallback_on_error !== false,
|
||||
timeout_ms: api.pluginConfig?.timeout_ms || DEFAULT_TIMEOUT_MS,
|
||||
};
|
||||
const client = createClient(cfg, api);
|
||||
|
||||
if (typeof api.registerMemoryCapability === "function") {
|
||||
api.registerMemoryCapability({
|
||||
// OpenClaw passes { availableTools: Set<string>, citationsMode? }. We
|
||||
// don't currently branch on tool availability, but accept the params
|
||||
// object so the signature matches MemoryPromptSectionBuilder exactly.
|
||||
promptBuilder: (_params) => [
|
||||
"Long-term memory provider: agentmemory (external REST service on " +
|
||||
client.baseUrl +
|
||||
").",
|
||||
"agentmemory recalls relevant prior observations before each turn via the before_agent_start hook and captures completed turns via agent_end.",
|
||||
"Treat recalled context as background, not authoritative — prefer current workspace state and explicit user instructions when they conflict.",
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
api.on("before_agent_start", async (event) => {
|
||||
if (!cfg.enabled) return;
|
||||
const prompt = typeof event?.prompt === "string" ? event.prompt.trim() : "";
|
||||
if (!prompt) return;
|
||||
const result = await client.postJson("/agentmemory/smart-search", {
|
||||
query: prompt,
|
||||
limit: 5,
|
||||
});
|
||||
const block = formatResults(result?.results || []);
|
||||
if (!block) return;
|
||||
return {
|
||||
prependContext: `Relevant long-term memory from agentmemory:\n${block}`,
|
||||
};
|
||||
});
|
||||
|
||||
api.on("agent_end", async (event) => {
|
||||
if (!cfg.enabled || !event?.success || !Array.isArray(event.messages)) return;
|
||||
const userText = latestUserText(event.messages);
|
||||
const assistantText = lastAssistantText(event.messages);
|
||||
if (!userText || !assistantText) return;
|
||||
const sessionId =
|
||||
event.sessionId ||
|
||||
event.sessionKey ||
|
||||
event.runId ||
|
||||
`openclaw-${Date.now()}`;
|
||||
await client.postJson("/agentmemory/observe", {
|
||||
hookType: "post_tool_use",
|
||||
sessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
data: {
|
||||
tool_name: "conversation",
|
||||
tool_input: userText.slice(0, 1000),
|
||||
tool_output: assistantText.slice(0, 4000),
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
@@ -0,0 +1,27 @@
|
||||
name: agentmemory
|
||||
version: 0.8.1
|
||||
description: "Persistent cross-session memory for OpenClaw via agentmemory. 95.2% retrieval accuracy on LongMemEval-S."
|
||||
author: "Rohit Ghumare"
|
||||
homepage: "https://github.com/rohitg00/agentmemory"
|
||||
license: Apache-2.0
|
||||
|
||||
category: memory
|
||||
tags:
|
||||
- memory
|
||||
- persistence
|
||||
- mcp
|
||||
- context
|
||||
|
||||
hooks:
|
||||
- on_session_start
|
||||
- on_pre_llm_call
|
||||
- on_post_tool_use
|
||||
- on_session_end
|
||||
|
||||
config:
|
||||
enabled: true
|
||||
base_url: http://localhost:3111
|
||||
token_budget: 2000
|
||||
min_confidence: 0.5
|
||||
fallback_on_error: true
|
||||
timeout_ms: 5000
|
||||
Reference in New Issue
Block a user