mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-10-28 06:24:43 +00:00
Merge 93a82deb3e into 550317c3bf
This commit is contained in:
commit
14b30087c7
6 changed files with 396 additions and 0 deletions
13
plugins/openclaw/.openclaw-plugin/plugin.json
Normal file
13
plugins/openclaw/.openclaw-plugin/plugin.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "reme",
|
||||
"version": "0.1.0",
|
||||
"description": "File-native long-term memory for OpenClaw, backed by a running ReMe MCP server. A memory skill recalls long-term knowledge on demand; recording is automatic via a Stop hook that records each session in the background.",
|
||||
"author": {
|
||||
"name": "EconML team of Alibaba Tongyi Lab",
|
||||
"email": "jinli.yl@alibaba-inc.com"
|
||||
},
|
||||
"homepage": "https://reme.agentscope.io/",
|
||||
"repository": "https://github.com/agentscope-ai/ReMe",
|
||||
"license": "Apache-2.0",
|
||||
"keywords": ["memory", "reme", "mcp", "long-term-memory", "agent"]
|
||||
}
|
||||
124
plugins/openclaw/README.md
Normal file
124
plugins/openclaw/README.md
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
# ReMe Plugin for OpenClaw
|
||||
|
||||
Connect OpenClaw to [ReMe](https://github.com/agentscope-ai/ReMe) — file-native long-term memory
|
||||
for AI agents. The plugin gives OpenClaw **recall** (read long-term memory) and **records every
|
||||
session automatically** via a Stop hook. Consolidation of daily notes into long-term `digest/`
|
||||
knowledge runs server-side in ReMe.
|
||||
|
||||
## What you get
|
||||
|
||||
- **MCP tools** from the `reme` server: `search`, `traverse`, `daily_list`, `frontmatter_read`,
|
||||
`read`, `auto_memory_cc`, and more.
|
||||
- **Stop hook** (`hooks/auto_memory.py`) — when a session ends it calls ReMe's server-side
|
||||
`auto_memory_cc` tool in a detached background process, passing **only the session id**. The
|
||||
server resolves that session's transcript on disk and records the durable facts into today's
|
||||
daily note. Recording is fully automatic and asynchronous — the agent never records by hand, and
|
||||
stopping is never delayed. Best-effort: if the server is down it logs and gives up silently.
|
||||
- **Skill** `reme-memory` — recall long-term memory before answering (semantic `search`,
|
||||
topological `traverse`, state `daily_list`/`frontmatter_read`, then `read` with citations),
|
||||
plus a server status check. Recording is handled silently by the Stop hook.
|
||||
|
||||
## Deployment model
|
||||
|
||||
The plugin **connects to a shared HTTP MCP server you start once** — it does not spawn ReMe. One
|
||||
server means one set of background watchers / dream cron across all your OpenClaw windows.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Install ReMe (Python 3.11+):
|
||||
|
||||
```bash
|
||||
pip install "reme-ai[core]"
|
||||
```
|
||||
|
||||
2. Configure model credentials in a `.env` (see `example.env`):
|
||||
|
||||
```bash
|
||||
EMBEDDING_API_KEY=sk-xxx
|
||||
EMBEDDING_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
LLM_API_KEY=sk-xxx
|
||||
LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
```
|
||||
|
||||
3. Start the ReMe MCP server (one time, leave it running):
|
||||
|
||||
```bash
|
||||
reme start service.backend=mcp service.transport=streamable-http
|
||||
```
|
||||
|
||||
It serves `http://127.0.0.1:2333/mcp`. To use a different port, start with
|
||||
`service.port=<port>` and update the `url` in `.mcp.json` to match.
|
||||
|
||||
## Install the plugin
|
||||
|
||||
OpenClaw manages MCP servers natively via its `openclaw mcp` command. The plugin ships a bundled
|
||||
`.mcp.json` and a Stop-hook manifest for auto-recording.
|
||||
|
||||
1. Copy or symlink the plugin directory to your OpenClaw plugins location:
|
||||
|
||||
```bash
|
||||
cp -r plugins/openclaw/ ~/.openclaw/plugins/reme
|
||||
```
|
||||
|
||||
2. Configure the MCP connection (OpenClaw natively handles MCP registration):
|
||||
|
||||
```bash
|
||||
# Option A — point OpenClaw at the bundled MCP config:
|
||||
openclaw mcp set reme file://~/.openclaw/plugins/reme/reme/.mcp.json
|
||||
|
||||
# Option B — register the URL directly:
|
||||
openclaw mcp add reme http --url http://127.0.0.1:2333/mcp
|
||||
```
|
||||
|
||||
3. Restart OpenClaw. Verify the `reme` server and its tools are connected, then the
|
||||
`reme-memory` skill can recall memory and report server health.
|
||||
|
||||
## Plugin structure
|
||||
|
||||
```
|
||||
plugins/openclaw/
|
||||
├── .openclaw-plugin/
|
||||
│ └── plugin.json # OpenClaw plugin manifest
|
||||
└── reme/
|
||||
├── hooks/
|
||||
│ ├── hooks.json # Stop hook configuration
|
||||
│ └── auto_memory.py # Fire-and-forget session recording hook
|
||||
├── skills/
|
||||
│ └── reme-memory/
|
||||
│ └── SKILL.md # Memory recall skill
|
||||
└── .mcp.json # MCP server connection config
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The plugin's MCP server URL lives in `plugins/openclaw/reme/.mcp.json`. Keep it in sync with how
|
||||
you start ReMe (host/port). The Stop hook reads this same file to find the server (override with
|
||||
`REME_HOST` / `REME_PORT` env vars).
|
||||
- The Stop hook needs `python3` on `PATH`. It logs to
|
||||
`plugins/openclaw/reme/logs/auto_memory_hook.log`.
|
||||
- Recording is best-effort: if the server is down, the hook logs and gives up silently without
|
||||
blocking the session.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Tools not showing up
|
||||
|
||||
1. Verify ReMe is running: `curl -s http://127.0.0.1:2333/health_check`
|
||||
2. Check the MCP URL matches your ReMe port: `openclaw mcp list`
|
||||
3. Re-register the MCP server and restart OpenClaw
|
||||
|
||||
### Stop hook not recording
|
||||
|
||||
1. Check the log: `cat ~/.openclaw/plugins/reme/reme/logs/auto_memory_hook.log`
|
||||
2. Verify `python3` is on `PATH`
|
||||
3. Ensure the ReMe server is running when you end a session
|
||||
|
||||
### Connection refused
|
||||
|
||||
The MCP server may not be running. Start it with:
|
||||
|
||||
```bash
|
||||
reme start service.backend=mcp service.transport=streamable-http
|
||||
```
|
||||
|
||||
Then retry.
|
||||
8
plugins/openclaw/reme/.mcp.json
Normal file
8
plugins/openclaw/reme/.mcp.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"reme": {
|
||||
"type": "http",
|
||||
"url": "http://127.0.0.1:2333/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
166
plugins/openclaw/reme/hooks/auto_memory.py
Normal file
166
plugins/openclaw/reme/hooks/auto_memory.py
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
#!/usr/bin/env python3
|
||||
"""ReMe Stop hook: fire-and-forget auto-memory for the current OpenClaw session.
|
||||
|
||||
OpenClaw runs this on the ``Stop`` event and feeds the hook payload as JSON
|
||||
on stdin. We read only ``session_id`` from it and hand that to ReMe's server-side
|
||||
``auto_memory_cc`` tool over the (already-running) MCP server — the server
|
||||
resolves *this* session's transcript on disk and records the durable facts. No
|
||||
messages are sent from here; the agent never has to record by hand.
|
||||
|
||||
The actual run spins up an inner agent and can take a while, so we detach
|
||||
(double-fork) and return immediately: stopping is never blocked. Any failure is
|
||||
logged, never surfaced — recording is best-effort.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
|
||||
_CALL_TIMEOUT = 600
|
||||
|
||||
|
||||
def _plugin_root() -> str:
|
||||
return os.environ.get("OPENCLAW_PLUGIN_ROOT") or os.path.dirname(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
)
|
||||
|
||||
|
||||
def _server_url() -> str:
|
||||
"""ReMe MCP endpoint. Prefer the bundled .mcp.json so it stays in sync."""
|
||||
mcp_json = os.path.join(_plugin_root(), ".mcp.json")
|
||||
try:
|
||||
with open(mcp_json, encoding="utf-8") as f:
|
||||
url = json.load(f)["mcpServers"]["reme"]["url"]
|
||||
if url:
|
||||
return url
|
||||
except Exception:
|
||||
pass
|
||||
host = os.environ.get("REME_HOST", "127.0.0.1")
|
||||
port = os.environ.get("REME_PORT", "2333")
|
||||
return f"http://{host}:{port}/mcp"
|
||||
|
||||
|
||||
def _log(session_id: str, status: str, detail: str = "") -> None:
|
||||
try:
|
||||
log_dir = os.path.join(_plugin_root(), "logs")
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
line = f"{stamp} session={session_id} {status}"
|
||||
if detail:
|
||||
line += f" {detail}"
|
||||
with open(os.path.join(log_dir, "auto_memory_hook.log"), "a", encoding="utf-8") as f:
|
||||
f.write(line + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _post(url: str, body: dict, headers: dict) -> "urllib.request.addinfourl":
|
||||
data = json.dumps(body).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
|
||||
return urllib.request.urlopen(req, timeout=_CALL_TIMEOUT)
|
||||
|
||||
|
||||
def _read_jsonrpc(resp) -> dict | None:
|
||||
"""Return the JSON-RPC envelope from a JSON or text/event-stream response."""
|
||||
ctype = resp.headers.get("content-type", "")
|
||||
body = resp.read().decode("utf-8", "replace")
|
||||
if "text/event-stream" in ctype:
|
||||
result = None
|
||||
for line in body.splitlines():
|
||||
line = line.strip()
|
||||
if not line.startswith("data:"):
|
||||
continue
|
||||
try:
|
||||
obj = json.loads(line[len("data:") :].strip())
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(obj, dict) and ("result" in obj or "error" in obj):
|
||||
result = obj
|
||||
return result
|
||||
try:
|
||||
return json.loads(body)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
def _mcp_call(url: str, tool: str, arguments: dict) -> dict | None:
|
||||
"""Minimal MCP streamable-http client: initialize -> initialized -> tools/call."""
|
||||
base = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"}
|
||||
|
||||
init = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"protocolVersion": "2025-06-18",
|
||||
"capabilities": {},
|
||||
"clientInfo": {"name": "reme-stop-hook", "version": "1.0"},
|
||||
},
|
||||
}
|
||||
with _post(url, init, base) as resp:
|
||||
mcp_session = resp.headers.get("mcp-session-id")
|
||||
_read_jsonrpc(resp)
|
||||
|
||||
headers = dict(base)
|
||||
if mcp_session:
|
||||
headers["mcp-session-id"] = mcp_session
|
||||
|
||||
try:
|
||||
with _post(url, {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}, headers) as resp:
|
||||
resp.read()
|
||||
except urllib.error.HTTPError:
|
||||
pass
|
||||
|
||||
call = {"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": tool, "arguments": arguments}}
|
||||
with _post(url, call, headers) as resp:
|
||||
return _read_jsonrpc(resp)
|
||||
|
||||
|
||||
def _daemonize() -> None:
|
||||
"""Double-fork + setsid so the (slow) call outlives the hook and is reaped by init."""
|
||||
if os.fork() > 0:
|
||||
os._exit(0)
|
||||
os.setsid()
|
||||
if os.fork() > 0:
|
||||
os._exit(0)
|
||||
devnull = os.open(os.devnull, os.O_RDWR)
|
||||
for fd in (0, 1, 2):
|
||||
os.dup2(devnull, fd)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Entry point: read the hook payload from stdin and record the session."""
|
||||
try:
|
||||
payload = json.loads(sys.stdin.read() or "{}")
|
||||
except Exception:
|
||||
payload = {}
|
||||
|
||||
session_id = payload.get("session_id") or ""
|
||||
if not session_id:
|
||||
return
|
||||
|
||||
if hasattr(os, "fork"):
|
||||
_daemonize()
|
||||
|
||||
url = _server_url()
|
||||
try:
|
||||
result = _mcp_call(url, "auto_memory_cc", {"session_id": session_id})
|
||||
if result is None:
|
||||
_log(session_id, "no-response")
|
||||
elif "error" in result:
|
||||
_log(session_id, "error", json.dumps(result["error"], ensure_ascii=False)[:500])
|
||||
else:
|
||||
_log(session_id, "ok")
|
||||
except urllib.error.URLError as exc:
|
||||
_log(session_id, "unreachable", str(exc.reason))
|
||||
except Exception as exc: # noqa: BLE001 - best-effort, never surface
|
||||
_log(session_id, "exception", repr(exc)[:500])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
16
plugins/openclaw/reme/hooks/hooks.json
Normal file
16
plugins/openclaw/reme/hooks/hooks.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"description": "On stop, record this OpenClaw session into ReMe long-term memory (background, async).",
|
||||
"hooks": {
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"${OPENCLAW_PLUGIN_ROOT}/hooks/auto_memory.py\"",
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
69
plugins/openclaw/reme/skills/reme-memory/SKILL.md
Normal file
69
plugins/openclaw/reme/skills/reme-memory/SKILL.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
name: reme-memory
|
||||
description: Use ReMe as file-native long-term memory in OpenClaw. RECALL — search ReMe before answering questions about past conversations, preferences, project history, or decisions.
|
||||
---
|
||||
|
||||
# ReMe Memory
|
||||
|
||||
ReMe is the persistent, file-native memory layer for this agent. It stores conversations and
|
||||
resources as Markdown files with frontmatter and `[[wikilinks]]`, and consolidates them into
|
||||
long-term **digest** knowledge. Your job with this plugin is **recall**.
|
||||
|
||||
The recall tools come from the `reme` MCP server. They are only available when the user has the
|
||||
server running:
|
||||
|
||||
```
|
||||
reme start service.backend=mcp service.transport=streamable-http
|
||||
```
|
||||
|
||||
If the tools are missing, that server is not running — tell the user the command above instead of
|
||||
guessing answers.
|
||||
|
||||
## MCP setup
|
||||
|
||||
OpenClaw manages MCP servers natively. Point the reme server at the bundled config:
|
||||
|
||||
```bash
|
||||
openclaw mcp set reme file://${OPENCLAW_PLUGIN_ROOT}/.mcp.json
|
||||
```
|
||||
|
||||
Or configure it directly via `openclaw mcp add` with the same HTTP URL
|
||||
(`http://127.0.0.1:2333/mcp`).
|
||||
|
||||
## Recall (read long-term memory)
|
||||
|
||||
Before answering questions about previous conversations, user preferences, project history,
|
||||
decisions, or long-term context, recall from ReMe first. ReMe answers **three independent kinds of
|
||||
question** — pick the mode the request needs; don't merge them into one call. Durable knowledge
|
||||
lives under `digest/`, daily notes under `daily/`, external materials under `resource/`.
|
||||
|
||||
1. **Semantic** (default — "what do we know about X?"): `search` with `query="<question/keywords>"`,
|
||||
`limit=5` (optional `min_score`). Hybrid vector + BM25 with one-hop wikilink expansion.
|
||||
2. **Topological** ("what links to this node?"): `traverse` with `path="<node>"`, `depth=1`
|
||||
(raise to 2 only when needed), `direction=both` to walk the `[[wikilink]]` graph.
|
||||
3. **State** ("what exists / what was recorded on <date>?"): `daily_list` with `date="YYYY-MM-DD"`
|
||||
(empty = today) to list a day's notes, or `frontmatter_read` with a `path` to inspect one file's
|
||||
frontmatter — structural lookup, no semantic matching.
|
||||
|
||||
Then `read` the relevant hits by `path` (optionally `start_line`/`end_line`; prefer `digest/` paths
|
||||
for durable knowledge) to pull the content behind a hit. Cite the workspace-relative paths you used.
|
||||
If nothing useful comes back, say so plainly rather than guessing.
|
||||
|
||||
## Server status
|
||||
|
||||
To check ReMe is up: call `version` and `health_check`, then summarize the version and the health
|
||||
snapshot (components, workspace). If the reme tools are not available at all, the server
|
||||
is not running — tell the user to start it with the command above. The plugin connects at
|
||||
`http://127.0.0.1:2333/mcp`; a different host/port must match the `url` in
|
||||
`plugins/openclaw/reme/.mcp.json`.
|
||||
|
||||
## Workspace model
|
||||
|
||||
```
|
||||
daily/ lightly-processed memory: daily facts, conversation summaries
|
||||
digest/ long-term consolidated knowledge (what recall mainly surfaces)
|
||||
resource/ external raw materials
|
||||
```
|
||||
|
||||
Consolidation of `daily/` into `digest/` and proactive interest extraction run **server-side** in
|
||||
the ReMe process (background watchers + dream cron). The plugin does not drive them.
|
||||
Loading…
Add table
Reference in a new issue