diff --git a/src/agent-memory/docs/user_manual.md b/src/agent-memory/docs/user_manual.md index 2e0254b0d..bd8b7ab9a 100644 --- a/src/agent-memory/docs/user_manual.md +++ b/src/agent-memory/docs/user_manual.md @@ -1,313 +1,299 @@ -# agent-memory User Manual (English) +# Agent Memory (agent-memory) -> 中文版本见 [`user_manual.zh.md`](./user_manual.zh.md). +agent-memory is ANOLISA's file-form memory MCP server, providing AI agents with a persistent, searchable, sandboxed memory space. Agents read and write memory like a filesystem; the system injects relevant context into subsequent turns via BM25/vector hybrid retrieval and automatic capture/recall, reducing repeated communication and improving task continuity. -`agent-memory` is a Linux-only Rust [MCP](https://modelcontextprotocol.io/) -server that gives an AI agent persistent, sandboxed, file-shaped memory. -This manual covers the architecture, installation, configuration, the -21 MCP tools the server exposes (25 including the four `memory_task_*` cross-session task tools), how to integrate from a client / SDK, -and how to verify a deployment. +- **File-form memory**: read/write memory with filesystem semantics via MCP tools; namespace isolation and path sandboxing. +- **Hybrid semantic search**: BM25 + dense vector + RRF fusion with automatic fallback. +- **Auto capture & recall**: automatically extracts observations at conversation end (deduped) and injects relevant memory when building the next prompt. +- **Safe injection**: prompt-injection detection and escaping for memory content injected into LLM prompts. +- **Versioning & snapshots**: optional auto git commit + tar.gz snapshots for file-level and mount-level rollback. -## Table of Contents +--- -1. [Overview](#1-overview) -2. [Architecture](#2-architecture) -3. [Installation](#3-installation) -4. [Configuration](#4-configuration) -5. [Feature Reference](#5-feature-reference) -6. [Tool API Reference](#6-tool-api-reference) -7. [SDK / Client Integration Guide](#7-sdk--client-integration-guide) -8. [Testing & Verification Guide](#8-testing--verification-guide) -9. [Troubleshooting](#9-troubleshooting) +## Installation ---- +### Via anolisa CLI (recommended) -## 1. Overview +```bash +anolisa install agent-memory +``` -### What is `agent-memory` +Produces: `agent-memory` binary, default config, MCP service descriptor, systemd user template, tmpfiles rule, OpenClaw adapter bundle. -`agent-memory` is a single-binary MCP server that turns a directory on -the local filesystem into a structured memory store an agent can read -and write through 19 well-defined tools. Unlike a conversation-window -or vector-DB-only memory, the store is: +### RPM package (AnolisOS / RHEL) -- **File-shaped** — the agent thinks in paths (`notes/x.md`, - `decisions/2026-05/db-pick.md`), the same shape humans use. -- **Sandboxed** — every file open is anchored at the mount root via - `openat2(RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS)`; the kernel rejects - `..`, symlinks, and meta-directory access. -- **Versioned** — optional git auto-commit and tar.gz snapshots give - rollback at file and at mount granularity. -- **Searchable** — a SQLite FTS5 BM25 index runs in the background so - full-text queries are sub-millisecond. +```bash +sudo yum install agent-memory +``` -### Who it's for +RPM installs to system-level FHS paths: -- Agent runtimes (Claude Code, Cursor, Continue, custom rmcp-based - clients) that want a persistent scratchpad. -- Multi-agent systems where one agent's notes need to outlive its - process and be readable by another. -- Operators who need an audit trail (`mem_log`, JSONL audit, journald) - and recovery (`mem_revert`, `mem_snapshot_restore`). +| Purpose | Path | +|------|------| +| Service binary | `/usr/bin/agent-memory` | +| Default config | `/usr/share/anolisa/agent-memory/default.toml` | +| MCP service descriptor (auto-discovery) | `/usr/share/anolisa/mcp-servers/agent-memory.json` | +| systemd user template | `/usr/lib/systemd/user/anolisa-memory@.service` | +| tmpfiles rule (creates `/run/anolisa/{,sessions}`) | `/usr/lib/tmpfiles.d/anolisa-memory.conf` | +| OpenClaw adapter bundle | `/usr/share/anolisa/adapters/agent-memory/` | +| Docs | `/usr/share/doc/agent-memory/` | -### Threat model in one paragraph +### Source build (developers) -The server treats the agent as an untrusted process that may try to -escape the mount, plant symlinks, mass-delete, or DoS via large -payloads. The kernel-level `RESOLVE_BENEATH` flag, the explicit -reserved-path set (`.anolisa`, `.git`, `.gitignore`), and per-call -size caps (`max_read_bytes`, `max_write_bytes`, `max_append_bytes`) -close the common-case attacks. Profile gating, audit logs, and -snapshots are defence-in-depth and recovery aids. +```bash +git clone https://github.com/alibaba/anolisa.git +cd anolisa/src/agent-memory + +make build # cargo build --release --locked +sudo make install # install to /usr/local +``` + +Build deps: Rust ≥ 1.85 (edition 2024; CI pins 1.89 to share the monorepo toolchain), cmake (libgit2 vendored), systemd-devel (journald audit fan-out). + +### Cross-platform development + +Runtime is Linux-only (depends on user_namespace, mount(2), cgroup v2, inotify, journald). On macOS / Windows use the remote flow: + +```bash +make remote-build # push branch and ssh to a Linux host for cargo build +make remote-test # same + tests + clippy +``` --- -## 2. Architecture +## Integration + +### Claude Code / Cursor / Continue / any stdio MCP client -### Layered diagram +Add to your MCP config: +```json +{ + "mcpServers": { + "agent-memory": { + "command": "/usr/bin/agent-memory", + "args": [], + "env": { + "USER_ID": "alice", + "MEMORY_PROFILE": "advanced" + } + } + } +} +``` + +`/usr/share/anolisa/mcp-servers/agent-memory.json` lists all 37 tool names for auto-discovering clients. + +### OpenClaw + +The bundled plugin forwards 4 memory-contract tools (`memory_search`, `memory_get`, `memory_observe`, `memory_get_context`) to agent-memory: + +```bash +bash /usr/share/anolisa/adapters/agent-memory/openclaw/scripts/install.sh +openclaw gateway restart ``` -+--------------------------------------------------------+ -| MCP client (Claude Code / Cursor / custom) | -| stdio JSON-RPC 2.0 | -+----------------------------+---------------------------+ - | -+----------------------------v---------------------------+ -| MemoryMcpServer (rmcp) | -| tools/list -> profile-filtered | -| tools/call -> profile-gated, returns Result<> | -+----------------------------+---------------------------+ - | -+----------------------------v---------------------------+ -| MemoryService | -| dispatches to tool impls; owns mount, index, git, | -| snapshot, audit, session handles | -+----+------------+--------------+-----------+-----------+ - | | | | -+----v---+ +----v-----+ +-----v----+ +---v-----+ -| Mount | | Index | | Git repo | | Snapshot| -| (auto/ | | (SQLite | | (libgit2 | | (tar.gz)| -| user- | | FTS5) | | vendored| | | -| land/ | | | | | | | -| userns| | | | | | | -+--------+ +----------+ +----------+ +---------+ - | | | | - +------+-----+--------+-----+-----+-----+ - | | | -+-----------v--------------v-----------v----+ -| safe_fs: openat2 RESOLVE_BENEATH | NO_SYM | -| fdopendir + fstatat + unlinkat | -+--------------------+----------------------+ - | -+--------------------v----------------------+ -| Per-namespace mount: ~/.anolisa/memory// -| user-files (notes/, decisions/, ...) | -| .anolisa/ (audit.log, index.db, ...) | -+-------------------------------------------+ + +Or via anolisa adapter management: + +```bash +anolisa adapter enable agent-memory openclaw +anolisa adapter status agent-memory ``` -### Mount strategies +**Prerequisite**: `openclaw` CLI on `$PATH`. The script logs clearly and exits 0 if missing — rerun after installing OpenClaw. `yum remove agent-memory` triggers `%preun` to call the uninstall script, leaving no orphaned config. + +Plugin contract ↔ agent-memory MCP tool mapping: + +| OpenClaw contract | agent-memory MCP tool | +|---|---| +| `memory_search` | `memory_search` (BM25 default; `mode=vector\|hybrid` with embedding) | +| `memory_get` | `mem_read` | +| `memory_observe` | `memory_observe` | +| `memory_get_context` | `memory_get_context` | + +Plugin config (via OpenClaw UI or `openclaw.json` `plugins.entries["memory-anolisa"].config`): + +| Key | Default | Purpose | +|---|---|---| +| `binaryPath` | auto-discovery: `$PATH` → `/usr/bin/agent-memory` → `/usr/local/bin/agent-memory` → `~/.local/bin/agent-memory` | absolute binary path | +| `userId` | env `USER_ID` → OS `uid` → env `$USER` | namespace `user_id`; same validation as Rust side | +| `profile` | `advanced` | profile gate, passed as `MEMORY_PROFILE` env | +| `maxReadBytes` | `1048576` (1 MiB) | `mem_read` cap, passed as `MEMORY_MAX_READ_BYTES` | +| `maxWriteBytes` | `16777216` (16 MiB) | `mem_write` cap, passed as `MEMORY_MAX_WRITE_BYTES` | +| `sessionId` | env `MEMORY_SESSION_ID` → new `ses_` | namespace session; must be fixed | +| `sessionDir` | env `MEMORY_SESSION_DIR` → `/run/anolisa/sessions` | session scratch + log root | -| Strategy | When | What happens | -|----------|------|---------------| -| `userland` (default) | always works | Mount is just a directory; sandbox enforced by `openat2`. | -| `userns` | Linux ≥ 4.6, kernel allows unprivileged user namespaces | At startup the process `unshare`s into a fresh user + mount namespace, overlays a private tmpfs on `/mnt`, bind-mounts the backing dir there. Host-side processes see nothing under `/mnt/memory//`. | -| `auto` | runtime-detected | Tries `userns` first; falls back to `userland` on any error. The retry path is robust against partial mount-stage failures (the `unshare` / maps stage runs at most once; mount steps are idempotent). | +The plugin passes a minimal env allowlist to the subprocess (`PATH`, `HOME`, `USER`, `USER_ID`, `LANG`/`LC_ALL`/`LC_CTYPE`, `TZ`, `TMPDIR`, `XDG_RUNTIME_DIR`, and all `MEMORY_`/`RUST_`-prefixed vars); other env does not leak. `USER_ID` matches exactly — `USER_IDX` is not allowed. + +--- -### Per-namespace layout +## MCP tool set (37 tools) + +All tools are invoked via MCP `tools/call` with JSON object arguments. Errors return `CallToolResult { isError: true }` so clients can distinguish business errors from "successful but content contains 'failed'". Profile is enforced at both `tools/list` and `tools/call`. + +### Tier A — file operations (11) + +| Tool | Required | Optional | Returns | +|------|------|------|------| +| `mem_read` | `path` | — | UTF-8 file content | +| `mem_write` | `path`, `content` | `overwrite` | `wrote N bytes to ` | +| `mem_append` | `path`, `content` | — | `appended N bytes to ` | +| `mem_edit` | `path`, `old_str`, `new_str` | — | `edited ` (`old_str` must match exactly once) | +| `mem_list` | — | `dir`, `recursive`, `glob` | `{name, type, size, mtime}` array | +| `mem_grep` | `pattern` | `dir`, `type`, `max`, `case_insensitive` | `{path, line, text}` array | +| `mem_diff` | `path1`, `path2` | — | unified diff | +| `mem_mkdir` | `path` | — | `created ` | +| `mem_remove` | `path` | `recursive` | `removed ` | +| `mem_promote` | `session_path`, `store_path` | — | atomically move session scratch file into the persistent store | +| `mem_session_log` | — | — | current session JSONL | + +### Tier B — structured retrieval (6) + +| Tool | Required | Optional | Returns | +|------|------|------|------| +| `memory_search` | `query` | `top_k` (default 5), `mode` (bm25/vector/hybrid), `category` | `{path, score, snippet, suspicious}` array | +| `memory_observe` | `content` | `hint`, `type` | `observed at notes/observed/.md` | +| `memory_get_context` | — | `max_tokens` (default 2048) | markdown preview of recently modified files; each entry has `suspicious` | +| `memory_sessions` | — | `limit` (default 10) | historical session list | +| `memory_timeline` | — | — | cross-session timeline | +| `mem_index_refresh` | — | — | force-rebuild the FTS5 index | + +### Tier C — governance & versioning (7) + +| Tool | Required | Optional | Returns | +|------|------|------|------| +| `mem_snapshot` | — | `name` | `{id, name, created_at, size, backend}` | +| `mem_snapshot_list` | — | — | array sorted by `created_at` | +| `mem_snapshot_restore` | `id` | — | `restored ` | +| `mem_log` | — | `limit` (default 20), `path` | `{hash, summary, author, time}` array (requires git) | +| `mem_revert` | `path` | — | `reverted (commit )` (requires git) | +| `mem_consolidate` | — | — | `consolidation complete: N facts written` | +| `mem_compact` | — | — | `compacted N files to cold storage` | + +### Sovereignty & import/export (13) + +| Tool | Description | +|------|------| +| `memory_about` | memory store metadata | +| `memory_auto_created` | query auto-extracted facts | +| `memory_consent` | grant/revoke memory operations | +| `memory_forget` | delete specific memory entries | +| `mem_export` | export the memory store to an archive | +| `mem_import` | import memory from an archive | +| `memory_task_save` | save task context | +| `memory_task_list` | list saved tasks | +| `memory_task_resume` | resume task context | +| `memory_task_close` | close a task | +| `memory_summary` | memory store statistics overview | +| `memory_session_context` | session-start context injection | +| `mem_dream` | user profile synthesis | + +### Error code semantics + +| MCP code | Meaning | +|------------|------| +| `-32601` METHOD_NOT_FOUND | tool hidden by current profile | +| `-32602` INVALID_PARAMS | missing or wrong-type param | +| `-32603` INTERNAL_ERROR | server fault | +| `isError: true` | tool ran but returned a business error (path missing, sandbox rejection, size limit, etc.) | + +--- + +## Core features + +### File-form memory + +Agents organize memory by path, matching the human filesystem model: + +``` +notes/day1.md +decisions/2026-05/db-pick.md +context/project-overview.md +``` + +Namespace layout: ``` ~/.anolisa/memory/user-/ # mount root ├── README.md # auto-generated overview -├── notes/ # free-form agent notes -├── decisions/ # (example user-defined dirs) -└── .anolisa/ # OS-managed, agent cannot write +├── notes/ # free-form notes +├── decisions/ # user-defined subdirs +└── .anolisa/ # OS-managed, not writable by agents ├── manifest.toml # namespace metadata ├── audit.log # JSONL tool-call audit - ├── index.db # FTS5 SQLite database - ├── snapshots/ # tar.gz archives + sidecars - ├── trash/ # rollback entries from restore + ├── index.db # FTS5 SQLite + ├── snapshots/ # tar.gz archives + sidecar + ├── trash/ # entries retained on restore └── git/ # bare git mirror (when git enabled) ``` -> Indicative layout — items under `.anolisa/` are populated lazily as -> features are exercised (e.g. `git/` only exists with -> `MEMORY_GIT_ENABLED=true`). - -### Per-session layout +Session dir (tmpfs, 0700): ``` -/run/anolisa/sessions// # tmpfs, mode 0700 -├── meta.toml # session metadata -├── log.jsonl # per-session tool-call log -└── scratch/ # session-only working files; - # use mem_promote to persist +/run/anolisa/sessions// +├── meta.toml +├── log.jsonl +└── scratch/ # session-only drafts; promoted via mem_promote ``` -### Index worker +### Sandbox protection -A background tokio task watches the mount via `inotify`, batches events -through a 200 ms debounce window, and applies them in a single SQLite -transaction. The tokenizer is `trigram` for CJK robustness; the schema -is versioned so a future format change can migrate cleanly. On -inotify overflow (`IN_Q_OVERFLOW`) the worker falls back to a full -rescan instead of dropping events silently. +Every file open is anchored at the mount root via kernel `openat2(RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS)`: -### Audit and observability +- Rejects `..` traversal +- Rejects symlinks (including mid-call replacement; recursive deletes use `fdopendir` + `fstatat(AT_SYMLINK_NOFOLLOW)` + `unlinkat` so swaps can't race) +- Rejects access to metadata dirs (`.anolisa`, `.git`, `.gitignore` via `TargetIsReserved`) +- `mem_snapshot_restore` filters tar entry types — rejects `Symlink`/`Hardlink`/`Device`/`Fifo` +- Oversized payloads rejected per `max_*_bytes` -Every successful tool call appends a line to -`/.anolisa/audit.log` and (optionally) -`/run/anolisa/sessions//log.jsonl`. With `audit.journald=true` -each line is also fanned out to systemd-journald with structured -fields (`MESSAGE_ID`, `AGENT_MEMORY_TOOL`, ...) so operators can -filter with `journalctl`. Errors return through MCP as -`CallToolResult { isError: true }` so the client distinguishes failure -from a successful call whose payload happens to start with "failed". +**Mount strategies**: ---- - -## 3. Installation - -### From RPM (recommended, AnolisOS / RHEL family) +| Strategy | When | Behavior | +|------|------|------| +| `userland` (default) | any environment | mount is just a directory; sandbox enforced by `openat2` | +| `userns` | Linux ≥ 4.6 with unprivileged user namespace | `unshare` into a new user+mount namespace, mount a private tmpfs, then bind-mount the backing dir; host-side processes can't see `/mnt/memory//` | +| `auto` | runtime probe | try `userns`; fall back to `userland` on any error | -```bash -sudo yum install agent-memory -``` +### Version control -The package installs: - -- `/usr/bin/agent-memory` — the server binary -- `/usr/share/anolisa/agent-memory/default.toml` — default config -- `/usr/share/anolisa/mcp-servers/agent-memory.json` — MCP server - descriptor for auto-discovery -- `/usr/lib/systemd/user/anolisa-memory@.service` — opt-in systemd - user template -- `/usr/lib/tmpfiles.d/anolisa-memory.conf` — creates - `/run/anolisa/{,sessions}` at boot with `0700` -- `/usr/share/anolisa/adapters/agent-memory/` — OpenClaw plugin - bundle (manifest, source, prebuilt `dist/index.js`, install scripts) -- `/usr/share/doc/agent-memory/{CHANGELOG.md, user_manual.md, user_manual.zh.md}` - -### Installing the OpenClaw plugin (optional) - -[OpenClaw](https://github.com/openclaw) is an Anolis OS agent gateway -that consumes plugins via its own contract (different from raw MCP -stdio). If you also run an MCP-direct client (Claude Code, Cursor, -Continue) on the same host pointed at `/usr/bin/agent-memory` via -`mcp-server.json`, that client sees all 19 native tools (`mem_*` + -`memory_*`); the OpenClaw plugin separately exposes a 4-tool subset -to OpenClaw users under contract names. The two paths can coexist — -each agent sees only the tool set its own runtime advertises. - -Register the bundled plugin so the four memory contract tools -(`memory_search`, `memory_get`, `memory_observe`, -`memory_get_context`) call into agent-memory: - -**Prerequisite**: the `openclaw` CLI must be on `$PATH`. The script -detects this and exits 0 (with a clear log line) when the CLI is -missing, so re-run after installing OpenClaw. +Optional auto git commit (libgit2 vendored): ```bash -bash /usr/share/anolisa/adapters/agent-memory/openclaw/scripts/install.sh -openclaw gateway restart +MEMORY_GIT_ENABLED=true MEMORY_GIT_AUTO_COMMIT=true agent-memory ``` -OpenClaw's security scanner flags `child_process.spawn` as a -"dangerous code pattern", but the plugin uses spawn exclusively -to launch the agent-memory MCP server as a stdio subprocess — -the standard MCP transport mechanism, not arbitrary shell -execution. The install script bypasses the scanner by default. -To go through the regular (blocking) safe-install path instead, -set `AGENT_MEMORY_SAFE_INSTALL=1` when invoking the script. +With git on, `mem_log` exposes change history and `mem_revert` gives the agent a real "undo" button. `mem_snapshot*` provides mount-wide tar.gz point-in-time backups independent of git. -Uninstall (removes the plugin from `~/.openclaw/extensions/` and cleans -`openclaw.json`'s `plugins.{allow,entries,slots}`): +### Full-text search -```bash -bash /usr/share/anolisa/adapters/agent-memory/openclaw/scripts/uninstall.sh -``` +SQLite FTS5 BM25 index, sub-millisecond queries. A background tokio task watches the mount via `inotify`; events are debounced 200 ms and applied in a single transaction. Tokenizer is `trigram` (CJK-friendly). `IN_Q_OVERFLOW` triggers a full rescan — events are never silently dropped. -When the agent-memory RPM is uninstalled (`yum remove agent-memory`), -the spec's `%preun` runs the uninstall script automatically — no -orphan plugin in the OpenClaw config. `jq` is preferred for editing -`openclaw.json`; `python3` is used as a fallback when `jq` is missing. +### Hybrid vector search -The plugin's contract-name mapping: +BM25 + dense vector hybrid retrieval, fused via RRF (Reciprocal Rank Fusion, k=60). Vectors come from a pluggable Embedding Provider: -| OpenClaw contract | agent-memory MCP tool | -|---|---| -| `memory_search` | `memory_search` (Tier B, BM25 default; `mode=vector\|hybrid` with embedding) | -| `memory_get` | `mem_read` (Tier A) | -| `memory_observe` | `memory_observe` (Tier B) | -| `memory_get_context` | `memory_get_context` (Tier B) | - -The plugin's MCP `clientInfo.version` always matches the -agent-memory RPM version — esbuild injects it at bundle time from -`Cargo.toml` via the Makefile `sync-versions` target, so an -upgrade automatically updates what OpenClaw sees. - -Plugin config (set via OpenClaw's plugin config UI or `openclaw.json` -`plugins.entries["memory-anolisa"].config`): - -| Key | Type | Default | Effect | -|---|---|---|---| -| `binaryPath` | string | auto-detect: `$PATH`-resolved `agent-memory`, then `/usr/bin/agent-memory`, `/usr/local/bin/agent-memory`, `~/.local/bin/agent-memory` | absolute path to the agent-memory binary | -| `userId` | string | env `USER_ID` → OS `uid` (via `process.getuid()`) → env `$USER` | namespace `user_id` for the memory mount; validated against the same rules as the Rust side (no `..` / `/` / `\` / control chars, ≤128 bytes) | -| `profile` | `basic` / `advanced` / `expert` | `advanced` | profile gate (§4) — set in the plugin config; the plugin spawns `agent-memory serve` with `MEMORY_PROFILE=` env, so a `MEMORY_PROFILE` set in the systemd unit or shell **is overridden** by the plugin config | -| `maxReadBytes` | integer (1..4 GiB) | `1048576` (1 MiB) | cap on a single `mem_read`; mirrored to `MEMORY_MAX_READ_BYTES` env on the child | -| `maxWriteBytes` | integer (1..4 GiB) | `16777216` (16 MiB) | cap on a single `mem_write`; mirrored to `MEMORY_MAX_WRITE_BYTES` env on the child | -| `sessionId` | string (`ses_` shape) | env `MEMORY_SESSION_ID` → a freshly-generated `ses_` pinned for the client's lifetime | namespace mount session; mirrored to `MEMORY_SESSION_ID` env. Pinning matters: a fresh value per spawn would defeat `mem_promote` (the scratch dir would not survive a respawn) | -| `sessionDir` | string | env `MEMORY_SESSION_DIR` → `/run/anolisa/sessions` (created at boot by `anolisa-memory.conf` tmpfiles snippet) | base dir for session scratch + log; mirrored to `MEMORY_SESSION_DIR` env | - -The plugin passes a minimal env allowlist (`PATH`, `HOME`, `USER`, -`USER_ID`, `LANG`, `LC_ALL`, `LC_CTYPE`, `TZ`, `TMPDIR`, -`XDG_RUNTIME_DIR`, plus anything starting with `MEMORY_` / `RUST_`) -to the child; unrelated parent env stays in the OpenClaw process and -does not leak into `agent-memory`. `USER_ID` is matched exactly, so -look-alikes such as `USER_IDX` are not forwarded. - -> **Compatibility note**: the adapter's `manifest.json` declares -> `compatibleVersions: ">=5.0.0"`. OpenClaw publishes under CalVer -> (e.g. `2026.5.7`), and the constraint is informational only — -> the plugin uses only the stable `openclaw/plugin-sdk` surface and -> has been validated against the 5.x SDK shape. If a future -> OpenClaw release breaks the plugin-sdk contract, bump the -> `compatibleVersions` field and republish. - -### From source +| Provider | Configuration | Notes | +|----------|---------|------| +| OpenAI | `MEMORY_EMBEDDING_BACKEND=openai` + `OPENAI_API_KEY` | calls OpenAI Embeddings API | +| Ollama | `MEMORY_EMBEDDING_BACKEND=ollama` + `OLLAMA_BASE_URL` | local Ollama instance | -```bash -git clone https://github.com/alibaba/anolisa.git -cd anolisa/src/agent-memory -make build # cargo build --release --locked -sudo make install # copies binary + config under /usr/local -``` +`memory_search` supports `mode`: `bm25` (default) / `vector` (cosine similarity) / `hybrid` (RRF fusion). Without embedding config, `vector`/`hybrid` auto-degrade to BM25 — no error. -Build requirements: Rust ≥ 1.85 (edition 2024 needs 1.85; CI pins -1.89.0 to match the rest of the monorepo's Linux Rust crates so a -single toolchain image covers them all), cmake (libgit2 vendored -build), systemd-devel (for the journald audit fan-out). +### Auto consolidation -### Cross-platform development +On shutdown, automatically extracts atomic facts from the session audit log (`mem_consolidate`) using 6 heuristic rules (zero LLM calls) — identifies high-frequency paths, search patterns, etc., and persists them as structured memory. Also manually triggerable via the `mem_consolidate` tool. Includes episodic memory extraction and conflict detection (BM25 threshold). -`agent-memory` is Linux-only at runtime. On macOS / Windows use the -remote build flow: +### Audit & observability -```bash -# from src/agent-memory/ -make remote-build # push branch + ssh into a Linux dev host, cargo build -make remote-test # same + run the test suite + clippy -``` +Every successful tool call appends a JSONL line to `/.anolisa/audit.log`; with sessions enabled, also to `/run/anolisa/sessions//log.jsonl`. `audit.journald=true` fans out to systemd-journald with structured fields (`MESSAGE_ID`, `AGENT_MEMORY_TOOL`, etc.) for `journalctl --user-unit=anolisa-memory@` filtering. --- -## 4. Configuration +## Configuration -### Configuration file +### Config file -Default location: `~/.anolisa/memory.toml`. Unknown fields are -rejected (`serde(deny_unknown_fields)`) so typos hard-fail at load. -A minimal config: +Default location: `~/.anolisa/memory.toml`. All structs enable `serde(deny_unknown_fields)` — typos hard-fail at load. Minimal config: ```toml [global] @@ -331,9 +317,9 @@ strategy = "auto" # auto | userland | userns [memory.index] enabled = true -time_decay_lambda = 0.01 # 0 = disable time decay -time_decay_alpha = 0.3 # time weight in score -cold_after_days = 30 +time_decay_lambda = 0.01 +time_decay_alpha = 0.3 +cold_after_days = 30 exclude_cold_on_search = true [memory.audit] @@ -348,202 +334,90 @@ enabled = false auto_commit = true [memory.consolidation] -enabled = true -max_facts = 20 -min_tool_calls = 3 -episodic_enabled = true -min_episode_steps = 3 +enabled = true +max_facts = 20 +min_tool_calls = 3 +episodic_enabled = true +min_episode_steps = 3 max_episodes_per_session = 10 -conflict_detection = true -conflict_bm25_threshold = -2.0 +conflict_detection = true +conflict_bm25_threshold = -2.0 ``` -### Environment overrides - -Every config field has an `MEMORY_*` env override; useful for tests -and one-off invocations. - -| Env var | Equivalent | Notes | -|---------|------------|-------| -| `USER_ID` | `global.user_id` | Validated; invalid input warned & dropped. | -| `MEMORY_BASE_DIR` | `memory.paths.base_dir` | | -| `MEMORY_PROFILE` | `memory.profile` | `basic` / `advanced` / `expert` | -| `MEMORY_SESSION_DIR` | `memory.session.base_dir` | | -| `MEMORY_SESSION_END` | `memory.session.end_action` | | -| `MEMORY_MOUNT_STRATEGY` | `memory.mount.strategy` | | -| `MEMORY_INDEX_ENABLED` | `memory.index.enabled` | systemd-style truthy/falsy | -| `MEMORY_AUDIT_JOURNALD` | `memory.audit.journald` | | -| `MEMORY_CGROUP_ENABLED` | `memory.cgroup.enabled` | | -| `MEMORY_CGROUP_MEMORY_MAX` | `memory.cgroup.memory_max` | `512M` / `2G` / bytes | -| `MEMORY_GIT_ENABLED` | `memory.git.enabled` | | -| `MEMORY_GIT_AUTO_COMMIT` | `memory.git.auto_commit` | | -| `MEMORY_MAX_READ_BYTES` | `memory.max_read_bytes` | | -| `MEMORY_MAX_WRITE_BYTES` | `memory.max_write_bytes` | | -| `MEMORY_MAX_APPEND_BYTES` | `memory.max_append_bytes` | | -| `MEMORY_INDEX_TIME_DECAY_LAMBDA` | `memory.index.time_decay_lambda` | must be ≥ 0.0 | -| `MEMORY_INDEX_TIME_DECAY_ALPHA` | `memory.index.time_decay_alpha` | must be 0.0–1.0 | -| `MEMORY_INDEX_COLD_AFTER_DAYS` | `memory.index.cold_after_days` | | -| `MEMORY_INDEX_EXCLUDE_COLD` | `memory.index.exclude_cold_on_search` | | -| `MEMORY_CONSOLIDATION_ENABLED` | `memory.consolidation.enabled` | | -| `MEMORY_CONSOLIDATION_MAX_FACTS` | `memory.consolidation.max_facts` | | -| `MEMORY_CONSOLIDATION_MIN_CALLS` | `memory.consolidation.min_tool_calls` | | -| `MEMORY_EPISODIC_ENABLED` | `memory.consolidation.episodic_enabled` | | -| `MEMORY_MIN_EPISODE_STEPS` | `memory.consolidation.min_episode_steps` | | -| `MEMORY_MAX_EPISODES` | `memory.consolidation.max_episodes_per_session` | | -| `MEMORY_CONFLICT_DETECTION` | `memory.consolidation.conflict_detection` | | -| `MEMORY_CONFLICT_THRESHOLD` | `memory.consolidation.conflict_bm25_threshold` | | -| `MEMORY_SESSION_ID` | (runtime-only) | Pins the agent run to a specific session id under `MEMORY_SESSION_DIR`. Required for `mem_promote`; see § 7. | +### Environment variables + +Every config key has a matching `MEMORY_*` env var. Priority: **env > config.toml > default**. + +| Variable | Description | Default | +|----------|------|------| +| `USER_ID` | user identity (validated; invalid values warn-and-ignore) | — | +| `MEMORY_PROFILE` | profile (basic/advanced/expert) | advanced | +| `MEMORY_BASE_DIR` | memory store root | `~/.anolisa/memory` | +| `MEMORY_SESSION_DIR` | session root | `/run/anolisa/sessions` | +| `MEMORY_SESSION_ID` | fixed session id (required for `mem_promote`) | new `ses_` | +| `MEMORY_SESSION_END` | session end action (discard/keep) | discard | +| `MEMORY_MOUNT_STRATEGY` | mount strategy (auto/userland/userns) | auto | +| `MEMORY_MAX_READ_BYTES` | per-read cap | 1 MiB | +| `MEMORY_MAX_WRITE_BYTES` | per-write cap | 16 MiB | +| `MEMORY_MAX_APPEND_BYTES` | per-append cap | 4 MiB | +| `MEMORY_INDEX_ENABLED` | enable FTS5 index | true | +| `MEMORY_INDEX_TIME_DECAY_LAMBDA` | time decay (≥0) | 0.01 | +| `MEMORY_INDEX_TIME_DECAY_ALPHA` | time weight ratio (0–1) | 0.3 | +| `MEMORY_INDEX_COLD_AFTER_DAYS` | cold archive days | 30 | +| `MEMORY_INDEX_EXCLUDE_COLD` | exclude cold from search | true | +| `MEMORY_AUDIT_JOURNALD` | fan out to journald | false | +| `MEMORY_CGROUP_ENABLED` | enable cgroup limits | false | +| `MEMORY_CGROUP_MEMORY_MAX` | cgroup memory cap | 512M | +| `MEMORY_GIT_ENABLED` | enable git versioning | false | +| `MEMORY_GIT_AUTO_COMMIT` | auto commit | true | +| `MEMORY_EMBEDDING_BACKEND` | embedding backend (none/openai/ollama) | none | +| `MEMORY_OPENAI_API_KEY` | OpenAI API key (falls back to `OPENAI_API_KEY`) | — | +| `MEMORY_OPENAI_MODEL` | OpenAI embedding model | text-embedding-3-small | +| `MEMORY_OLLAMA_MODEL` | Ollama embedding model | nomic-embed-text | +| `MEMORY_OLLAMA_BASE_URL` | Ollama base URL | http://localhost:11434 | +| `MEMORY_CONSOLIDATION_ENABLED` | enable auto consolidation | true | +| `MEMORY_CONSOLIDATION_MAX_FACTS` | max facts per run | 20 | +| `MEMORY_CONSOLIDATION_MIN_CALLS` | min tool-call threshold | 3 | +| `MEMORY_EPISODIC_ENABLED` | episodic extraction | true | +| `MEMORY_MIN_EPISODE_STEPS` | min episode steps | 3 | +| `MEMORY_MAX_EPISODES` | max episodes per session | 10 | +| `MEMORY_CONFLICT_DETECTION` | conflict detection | true | +| `MEMORY_CONFLICT_THRESHOLD` | BM25 conflict threshold | -2.0 | + +Data storage: `~/.anolisa/memory//`. ### Profiles -Profiles are a UX hint (not a security boundary), enforced at both -`tools/list` and `tools/call`: - -- **basic** — all 25 tools listed; weak models can still benefit from - the structured Tier B API. -- **advanced** (default) — all 25 tools listed; strong models are - expected to prefer Tier A file ops. -- **expert** — Tier B (`memory_search`, `memory_observe`, - `memory_get_context`) is hidden from `tools/list` and rejected at - `tools/call` with `METHOD_NOT_FOUND`. Frontier models that already - know how to navigate a filesystem need only Tier A and Tier C. - ---- - -## 5. Feature Reference +Profiles are UX hints, not security boundaries, but enforced at both `tools/list` and `tools/call`: -### Tier A — File operations (11 tools) +- **basic** — all 37 tools shown; weaker models can use the Tier B structured API. +- **advanced** (default) — all 37 tools shown; stronger models should prefer Tier A file ops. +- **expert** — hides Tier B (`memory_search`, `memory_observe`, `memory_get_context`, `mem_consolidate`, `memory_forget`, `memory_consent`); `tools/call` returns `METHOD_NOT_FOUND`. For proficient models that only need Tier A and Tier C. -`mem_read` / `mem_write` / `mem_append` / `mem_edit` / `mem_list` / -`mem_grep` / `mem_diff` / `mem_mkdir` / `mem_remove` / `mem_promote` / -`mem_session_log`. +### Embedding config -The agent thinks in mount-relative paths. Reserved prefixes (`.anolisa`, -`.git`, `.gitignore`) are refused at write time. `mem_edit` requires -exactly one match for `old_str` (zero or many → error) so it cannot -quietly clobber the wrong region. `mem_promote` moves a file from the -session's `scratch/` into the persistent store atomically. - -### Tier B — Structured search (3 tools) - -`memory_search` runs a keyword (BM25) query against the FTS5 index and -returns ranked snippets. When an embedding backend is configured -(OpenAI or Ollama), `mode="vector"` enables pure semantic search and -`mode="hybrid"` fuses BM25 + vector results with reciprocal rank -fusion for the best of both worlds. - -`memory_observe` writes a small frontmatter + -content blob under `notes/observed/.md` so the agent has a -zero-decision way to record a thought. `memory_get_context` assembles -a token-bounded markdown preview of the most recently modified files — -useful at the start of a turn to remind the agent what's in store. - -### Tier C — Governance (5 tools) - -`mem_snapshot` / `mem_snapshot_list` / `mem_snapshot_restore` give -mount-wide point-in-time backups (tar.gz with sidecar metadata). -`mem_log` and `mem_revert` operate on the optional git mirror — useful -for "I edited the wrong file three turns ago" recovery. - -### Sandbox guarantees - -- Path traversal (`..`, absolute paths, `\0`) → kernel-rejected by - `openat2`. -- Symlink swap mid-call → kernel-rejected by `RESOLVE_NO_SYMLINKS`; - recursive removal uses `fdopendir` + `fstatat(AT_SYMLINK_NOFOLLOW)` - + `unlinkat` so swaps cannot race. -- Reserved-path overwrite (`.anolisa/audit.log`, `.gitignore`, ...) → - rejected by `TargetIsReserved`. -- Oversize payloads → rejected against `max_*_bytes` caps. -- `mem_snapshot_restore`-induced symlink injection → tarball entry-type - filter rejects `Symlink` / `Hardlink` / `Device` / `Fifo`. +```toml +[memory.embedding] +backend = "openai" # or "ollama" +api_key = "" # empty: auto-read OPENAI_API_KEY +model = "text-embedding-3-small" +# Ollama: backend = "ollama", model = "nomic-embed-text", base_url = "http://localhost:11434" +``` --- -## 6. Tool API Reference +## Use cases -All tools speak MCP `tools/call` with a JSON arguments object. Errors -come back as `CallToolResult { isError: true, content: [{type: "text", -text: ""}] }` so a client can branch on `isError`. - -### Tier A - -| Tool | Required | Optional | Returns | -|------|----------|----------|---------| -| `mem_read` | `path` | — | UTF-8 file content | -| `mem_write` | `path`, `content` | `overwrite` | `wrote N bytes to ` | -| `mem_append` | `path`, `content` | — | `appended N bytes to ` | -| `mem_edit` | `path`, `old_str`, `new_str` | — | `edited ` | -| `mem_list` | — | `dir`, `recursive`, `glob` | JSON array of `{name, type, size, mtime}` | -| `mem_grep` | `pattern` | `dir`, `type`, `max`, `case_insensitive` | JSON array of `{path, line, text}` | -| `mem_diff` | `path1`, `path2` | — | unified diff | -| `mem_mkdir` | `path` | — | `created ` | -| `mem_remove` | `path` | `recursive` | `removed ` | -| `mem_promote` | `session_path`, `store_path` | — | `promoted N bytes: -> ` | -| `mem_session_log` | — | — | session JSONL or `(session log is empty)` | - -### Tier B - -| Tool | Required | Optional | Returns | -|------|----------|----------|---------| -| `memory_search` | `query` | `top_k` (default 5), `mode` (bm25/vector/hybrid), `category` | JSON array of `{path, score, snippet, suspicious}` | -| `memory_observe` | `content` | `hint` | `observed at notes/observed/.md` | -| `memory_get_context` | — | `max_tokens` (default 2048) | markdown preview | - -### Tier C - -| Tool | Required | Optional | Returns | -|------|----------|----------|---------| -| `mem_snapshot` | — | `name` | JSON `{id, name, created_at, size, backend}` | -| `mem_snapshot_list` | — | — | JSON array, oldest → newest | -| `mem_snapshot_restore` | `id` | — | `restored ` | -| `mem_log` | — | `limit` (default 20), `path` | JSON array of `{hash, summary, author, time}` | -| `mem_revert` | `path` | — | `reverted (commit )` | -| `mem_consolidate` | — | — | `consolidation complete: N facts written` | -| `mem_compact` | — | — | `compacted N files to cold storage` | - -### Error code semantics - -| MCP error code | Meaning | -|----------------|---------| -| `-32601` METHOD_NOT_FOUND | tool hidden under current profile | -| `-32602` INVALID_PARAMS | missing / mistyped argument | -| `-32603` INTERNAL_ERROR | server-side failure | -| `isError: true` content | tool ran but returned a domain error (path not found, sandbox refusal, size cap exceeded, ...) | +- Cross-session persistence of notes and decisions (Claude Code, Cursor, Continue, custom rmcp clients). +- Multi-agent systems where Agent A writes and Agent B reads shared notes. +- Operation audit and state recovery (`mem_log`, JSONL audit, journald, `mem_revert`, `mem_snapshot_restore`). +- Multi-turn "draft first, persist when decided" pattern (`mem_promote` atomically moves files from session scratch into the persistent store). --- -## 7. SDK / Client Integration Guide - -### Wiring up MCP-compatible clients - -#### Claude Code (`.claude/settings.json`) - -```json -{ - "mcpServers": { - "agent-memory": { - "command": "/usr/bin/agent-memory", - "args": [], - "env": { - "USER_ID": "alice", - "MEMORY_PROFILE": "advanced" - } - } - } -} -``` - -#### Cursor / Continue / any MCP client over stdio - -Point the client at the binary with the same `command` / `args` / -`env` shape. The descriptor at -`/usr/share/anolisa/mcp-servers/agent-memory.json` lists the 19 tool -names so a client that auto-discovers MCP servers picks them up. +## SDK / client integration -### Programmatic clients - -#### Python (using the official `mcp` SDK) +### Python (official `mcp` SDK) ```python import asyncio @@ -552,8 +426,7 @@ from mcp.client.stdio import stdio_client async def main(): server = StdioServerParameters( - command="/usr/bin/agent-memory", - args=[], + command="/usr/bin/agent-memory", args=[], env={"USER_ID": "alice"}, ) async with stdio_client(server) as (read, write): @@ -561,39 +434,34 @@ async def main(): await session.initialize() tools = await session.list_tools() print([t.name for t in tools.tools]) - result = await session.call_tool( "mem_write", {"path": "notes/from-python.md", "content": "hello"}, ) assert not result.isError - print(result.content[0].text) asyncio.run(main()) ``` -#### TypeScript (`@modelcontextprotocol/sdk`) +### TypeScript (`@modelcontextprotocol/sdk`) ```typescript import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; const transport = new StdioClientTransport({ - command: "/usr/bin/agent-memory", - args: [], + command: "/usr/bin/agent-memory", args: [], env: { USER_ID: "alice" }, }); const client = new Client({ name: "my-app", version: "1.0.0" }, {}); await client.connect(transport); - const result = await client.callTool({ name: "mem_grep", arguments: { pattern: "TODO", recursive: true, max: 50 }, }); -console.log(result.isError ? "failed" : result.content); ``` -#### Rust (via `rmcp`) +### Rust (`rmcp`) ```rust use rmcp::transport::child_process::ChildProcessTransport; @@ -604,92 +472,49 @@ let transport = ChildProcessTransport::new( ).await?; let client = ().serve(transport).await?; let tools = client.list_tools(Default::default()).await?; -let resp = client.call_tool(rmcp::model::CallToolRequestParam { - name: "mem_read".into(), - arguments: Some(serde_json::json!({"path": "notes/x.md"}) - .as_object().unwrap().clone()), -}).await?; ``` -### Promote-flow integration (multi-turn pattern) - -For agents that need a "draft now, persist on commit" pattern: - -1. Set `MEMORY_SESSION_ID=` and - `MEMORY_SESSION_DIR=/run/anolisa/sessions` per agent run. -2. Agent writes drafts to the session scratch (the runtime is - responsible for staging files into - `/run/anolisa/sessions//scratch/`). -3. When the agent decides "this is worth keeping", call `mem_promote` - to atomically move the file into the persistent store. - -### Observability hooks +### Promote workflow (multi-turn) -- `audit.journald=true` — fan out every call to - `journalctl --user-unit=anolisa-memory@`. -- `mem_session_log` — read the per-session JSONL from inside the agent - to self-reflect on what it has done this turn. -- `mem_log` (with git enabled) — surface change history to the agent; - combine with `mem_revert` to give it a real "undo" button. +1. Set `MEMORY_SESSION_ID=` and `MEMORY_SESSION_DIR=/run/anolisa/sessions` for each agent run. +2. Agent writes drafts to `/run/anolisa/sessions//scratch/`. +3. When a draft is worth keeping, the agent calls `mem_promote` to atomically move it into the persistent store. --- -## 8. Testing & Verification Guide +## Testing & verification -### 8.1 Automated tests +### Automated tests ```bash cd src/agent-memory cargo fmt --check cargo clippy -- -D warnings -cargo test # all suites -cargo test --test e2e_agent_test # 21-tool E2E -cargo test --test mcp_integration_test # protocol level +cargo test # full suite +cargo test --test e2e_agent_test # tool E2E +cargo test --test mcp_integration_test # protocol layer cargo test --test linux_userns_test -- --ignored # needs unprivileged userns +make smoke # one-shot end-to-end smoke ``` -The CI job in `ci.yaml` runs `fmt --check`, `clippy -D warnings`, and -`cargo test` on Rust 1.89. +CI runs `fmt --check` + `clippy -D warnings` + `cargo test` on Rust 1.89. -### 8.2 Interactive `mcp-harness` - -`mcp-harness` is an example binary that drives the server via stdio -and gives you a REPL for manual tool calls. +### Interactive `mcp-harness` ```bash cargo run --example mcp-harness -- /tmp/mem-test ``` | Command | Description | -|---------|-------------| -| `list` | List all visible tools | -| `call ` | Invoke a tool | -| `help` | Command reference | -| `quit` | Tear down server, exit | - -Sample session: +|------|------| +| `list` | list visible tools | +| `call ` | invoke a tool | +| `help` | help | +| `quit` | quit | -``` -mcp> call mem_mkdir {"path": "notes"} -Result: created notes -mcp> call mem_write {"path": "notes/day1.md", "content": "Hello world"} -Result: wrote 11 bytes to notes/day1.md -mcp> call mem_read {"path": "notes/day1.md"} -Result: Hello world -``` +Scenarios: `--scenario full` / `git --git` / `promote` / `--verbose` (prints JSON-RPC). -Pre-built scenarios (no manual asserts; you visually verify): - -```bash -cargo run --example mcp-harness -- /tmp/mem-test --scenario full -cargo run --example mcp-harness -- /tmp/mem-test --scenario git --git -cargo run --example mcp-harness -- /tmp/mem-test --scenario promote -cargo run --example mcp-harness -- /tmp/mem-test --verbose # log JSON-RPC -``` - -### 8.3 Raw JSON-RPC (protocol-level debugging) - -Start the server and pipe JSON-RPC lines to its stdin: +### Raw JSON-RPC (protocol-level debugging) ```bash mkdir -p /tmp/mem-test/__sessions__ @@ -700,123 +525,62 @@ USER_ID=tester \ agent-memory ``` -Handshake: +Handshake + tool call: ```json {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"manual","version":"1.0"}}} {"jsonrpc":"2.0","method":"notifications/initialized"} -``` - -Tool call: - -```json {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"mem_write","arguments":{"path":"test.md","content":"hello"}}} ``` -Expected response shape: - -```json -{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"wrote 5 bytes to test.md"}],"isError":false}} -``` - -### 8.4 Sandbox verification - -Confirm the kernel sandbox refuses each escape vector: +### Sandbox escape verification ```json -{"jsonrpc":"2.0","id":10,"method":"tools/call","params":{"name":"mem_read","arguments":{"path":"../../etc/passwd"}}} +{"name":"mem_read","arguments":{"path":"../../etc/passwd"}} ``` → `isError: true`, message `path outside mount root`. ```json -{"jsonrpc":"2.0","id":11,"method":"tools/call","params":{"name":"mem_write","arguments":{"path":".anolisa/audit.log","content":"x"}}} +{"name":"mem_write","arguments":{"path":".anolisa/audit.log","content":"x"}} ``` → `isError: true`, message `target is reserved`. -```json -{"jsonrpc":"2.0","id":12,"method":"tools/call","params":{"name":"mem_read","arguments":{"path":"a/b/symlink-to-etc-passwd"}}} -``` -→ `isError: true`, message `path outside mount root` (kernel ELOOP). - -### 8.5 Per-tool verification procedures - -Each procedure assumes either the harness REPL (`call `) -or raw JSON-RPC. Run inside `mcp-harness` for the shortest loop. - -- **mem_mkdir** — `call mem_mkdir {"path":"d"}` → response contains - `created`. Verify with `call mem_list {"recursive": true}`. -- **mem_write / mem_read** — write `Hello world\n`, read it back, byte - match. Re-write with `overwrite=false` should error. -- **mem_append** — append `+more`, re-read, content equals - `original+more`. -- **mem_edit** — write `foo bar baz`, edit `bar` → `qux`, read back - `foo qux baz`. Repeat with `bar` (now absent) → error - `match count 0`. -- **mem_list** — create nested dirs and files; recursive list shows all - paths plus `README.md` from init. -- **mem_grep** — write two files containing distinct keywords; grep - for one keyword surfaces only the matching file with `path / line / - text`. -- **mem_diff** — diff two files, output starts with `--- ` / `+++ ` - unified-diff headers. -- **mem_remove** — remove a file, subsequent read errors `not found`. -- **mem_promote** — pre-create `MEMORY_SESSION_DIR//scratch/x.md`, - set env, call promote, read the destination. -- **mem_session_log** — call any 3 tools, then `mem_session_log` returns - 3 JSONL lines. -- **memory_observe** — observe twice; `mem_list notes/observed` - recursively shows two ULID-named files. -- **memory_search** — observe with keyword `kappa`, wait ~500 ms, - search for `kappa`, the observed file is in the result. -- **memory_get_context** — write 5 files with distinct first lines, - `memory_get_context {max_tokens: 200}` previews them. -- **mem_snapshot / list** — snapshot, list, expect entry; size > 0; - `id` starts with `snap_`. -- **mem_snapshot_restore** — write v1, snapshot, write v2, restore - snapshot, read returns v1; `.anolisa/trash/-/` contains v2. -- **mem_log** — enable git, write three versions of the same file, - `mem_log {path: "..."}` returns ≥3 commits. -- **mem_revert** — enable git, write v3, revert, read returns the last - committed (v2) content. - -### 8.6 Smoke test (single command) - -The Makefile ships a self-contained smoke test that drives 5 tools -through the server and verifies the responses: +--- + +## Troubleshooting + +### Diagnostic tools ```bash -cd src/agent-memory -make smoke -``` +# Component-level diagnosis + auto-fix +anolisa doctor agent-memory --fix -A green `==> Smoke test PASSED` is the minimum signal a deployment is -working end-to-end. +# Adapter status +anolisa adapter status agent-memory ---- +# Debug startup +RUST_LOG=agent_memory=debug agent-memory +``` -## 9. Troubleshooting +### Common issues | Symptom | Likely cause | Fix | -|---------|--------------|-----| -| `unshare(NEWUSER\|NEWNS): EPERM` at startup | unprivileged user namespaces disabled | `sysctl kernel.unprivileged_userns_clone=1`, or set `MEMORY_MOUNT_STRATEGY=userland`. | -| `tmpfs /mnt: EBUSY` | something else owns `/mnt` in the new namespace | The retry path treats EBUSY as success; if it persists, restart the process. | -| `cargo build` fails on macOS / Windows with `libsystemd`/`nix` errors | host is not Linux | Use `make remote-build` / `remote-test`. | -| `tools/call memory_search` → `METHOD_NOT_FOUND` | `MEMORY_PROFILE=expert` hides Tier B | Switch to `advanced` or call file-tool equivalents. | -| Config typo silently ignored | the binary used to default-fill misspelt fields | This is now a hard error: read the load-time stderr message and fix the key. | -| `mem_log` returns `[]` even after writes | git versioning disabled | `MEMORY_GIT_ENABLED=true MEMORY_GIT_AUTO_COMMIT=true`. | -| Index search returns nothing for fresh content | inotify event still in the 200 ms debounce window | Retry; or call `mem_grep` (regex over filesystem, no index). | -| `mem_promote` errors `session not found` | `MEMORY_SESSION_ID` / `MEMORY_SESSION_DIR` not set or scratch missing | See § 7 promote-flow integration. | - -For deeper diagnosis, run with `RUST_LOG=agent_memory=debug` and -inspect both the server stderr and `/.anolisa/audit.log`. +|------|----------|------| +| startup `unshare(NEWUSER\|NEWNS): EPERM` | unprivileged user namespace disabled | `sysctl kernel.unprivileged_userns_clone=1`, or `MEMORY_MOUNT_STRATEGY=userland` | +| `tmpfs /mnt: EBUSY` | `/mnt` occupied in new namespace | restart the process | +| macOS / Windows `cargo build` fails on `libsystemd`/`nix` | non-Linux host | `make remote-build` / `remote-test` | +| `tools/call memory_search` returns `METHOD_NOT_FOUND` | `MEMORY_PROFILE=expert` hides Tier B | switch to `advanced`, or use Tier A directly | +| config typos silently ignored | — | now hard-fail; check startup stderr | +| `mem_log` returns `[]` despite writes | git versioning not enabled | `MEMORY_GIT_ENABLED=true MEMORY_GIT_AUTO_COMMIT=true` | +| search misses just-written content | inside the 200 ms debounce window | retry, or use `mem_grep` (regex on the filesystem, no index) | +| `mem_promote` reports `session not found` | `MEMORY_SESSION_ID`/`MEMORY_SESSION_DIR` unset or scratch missing | see Promote workflow | +| OpenClaw plugin not loaded | `openclaw` CLI not on PATH | rerun `install.sh` after installing OpenClaw | +| state out of sync after manual dnf | — | `anolisa repair agent-memory` / `anolisa forget` / `anolisa adopt` | + +For deeper investigation: start with `RUST_LOG=agent_memory=debug` and inspect both stderr and `/.anolisa/audit.log`. --- -## License - -Apache-2.0. See `LICENSE` shipped with the package. - -## Reporting issues - -[`github.com/alibaba/anolisa/issues`](https://github.com/alibaba/anolisa/issues), -component `memory`. +**License**: Apache-2.0 +**Version**: 0.1.0 +**Document version**: 2.0 (aligned with ANOLISA-design user-guide structure) diff --git a/src/agent-memory/docs/user_manual.zh.md b/src/agent-memory/docs/user_manual.zh.md index 8f9a119a6..dcef1a200 100644 --- a/src/agent-memory/docs/user_manual.zh.md +++ b/src/agent-memory/docs/user_manual.zh.md @@ -1,122 +1,223 @@ -# agent-memory 用户手册(中文) +# Agent 记忆(agent-memory) -> English version: [`user_manual.md`](./user_manual.md). +agent-memory 是 ANOLISA 的文件形态记忆 MCP 服务器,为 AI Agent 提供持久化、可搜索、受沙箱保护的记忆空间。Agent 像操作文件系统一样读写记忆,系统通过 BM25/向量混合检索、自动捕获与召回机制,把相关上下文注入后续对话,从而减少重复沟通并提升任务连贯性。 -`agent-memory` 是一个仅运行于 Linux 的 Rust [MCP](https://modelcontextprotocol.io/) -服务端,为 AI Agent 提供持久化、沙箱化、以文件为形态的记忆能力。 -本手册涵盖架构、安装、配置、25 个 MCP 工具规格、客户端 / SDK 接入指南 -以及部署后的功能测试验证流程。 +- **文件形态记忆**:通过 MCP 工具以文件系统语义读写记忆,支持命名空间隔离和路径沙箱。 +- **混合语义搜索**:BM25 + 稠密向量 + RRF 融合,支持自动 fallback,召回相关记忆片段。 +- **自动捕获与召回**:在对话结束时自动提取观察并去重,在下一轮构建提示时注入相关记忆。 +- **安全注入机制**:对注入 LLM 提示的记忆内容做提示注入检测和转义包装,降低攻击面。 +- **版本化与快照**:可选 git 自动提交 + tar.gz 快照,提供文件级与 mount 级回滚。 -## 目录 +--- -1. [简介](#1-简介) -2. [架构设计](#2-架构设计) -3. [安装部署](#3-安装部署) -4. [配置说明](#4-配置说明) -5. [主要功能](#5-主要功能) -6. [接口(Tool API)参考](#6-接口tool-api参考) -7. [SDK / 客户端开发指南](#7-sdk--客户端开发指南) -8. [功能测试与验证](#8-功能测试与验证) -9. [故障排查](#9-故障排查) +## 安装 ---- +### 通过 anolisa CLI(推荐) + +```bash +anolisa install agent-memory +``` + +安装产物:`agent-memory` 二进制、默认配置、MCP 服务描述符、systemd user 模板、tmpfiles 规则、OpenClaw 适配器 bundle。 -## 1. 简介 +### RPM 包(AnolisOS / RHEL 系) -### 什么是 `agent-memory` +```bash +sudo yum install agent-memory +``` -`agent-memory` 是一个单二进制 MCP 服务端,把本地文件系统中的一个目录变成 -结构化的"记忆仓",AI Agent 可以通过 19 个明确定义的工具读写它。 -与会话上下文窗口或仅向量库的方案不同,这种"记忆"具有: +RPM 安装到系统级 FHS 路径: -- **文件形态** —— Agent 以路径思考(`notes/x.md`、 - `decisions/2026-05/db-pick.md`),与人类的文件系统模型一致。 -- **沙箱隔离** —— 每次文件 open 都锚定在 mount root,通过 - `openat2(RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS)` 让内核拒绝 - `..`、symlink 以及元目录访问。 -- **可版本化** —— 可选的 git 自动 commit + tar.gz 快照分别提供 - 文件级 / mount 级回滚。 -- **可检索** —— 后台运行 SQLite FTS5 BM25 索引(全文检索次毫秒级), - 可选搭配 OpenAI / Ollama embedding 后端启用稠密向量语义搜索及 - BM25+向量混合检索(RRF 融合排序)。 +| 用途 | 路径 | +|------|------| +| 服务二进制 | `/usr/bin/agent-memory` | +| 默认配置 | `/usr/share/anolisa/agent-memory/default.toml` | +| MCP 服务描述符(自动发现) | `/usr/share/anolisa/mcp-servers/agent-memory.json` | +| systemd user 模板 | `/usr/lib/systemd/user/anolisa-memory@.service` | +| tmpfiles 规则(创建 `/run/anolisa/{,sessions}`) | `/usr/lib/tmpfiles.d/anolisa-memory.conf` | +| OpenClaw 适配器 bundle | `/usr/share/anolisa/adapters/agent-memory/` | +| 文档 | `/usr/share/doc/agent-memory/` | -### 适用场景 +### 源码构建(开发者) -- 需要持久化草稿区的 Agent 运行时(Claude Code、Cursor、Continue、 - 自研 rmcp 客户端等)。 -- 多 Agent 系统中需要 Agent A 写、Agent B 读的笔记跨进程共享。 -- 需要审计链路(`mem_log`、JSONL 审计、journald)和恢复手段 - (`mem_revert`、`mem_snapshot_restore`)的运维方。 +```bash +git clone https://github.com/alibaba/anolisa.git +cd anolisa/src/agent-memory -### 一段话讲清威胁模型 +make build # cargo build --release --locked +sudo make install # 安装到 /usr/local 下 +``` -服务端把 Agent 视为不可信进程:可能尝试越界读写、植入 symlink、 -批量删除或通过超大 payload 拒绝服务。内核级 `RESOLVE_BENEATH` -+ 显式保留路径集(`.anolisa`、`.git`、`.gitignore`)+ 单次调用大小上限 -(`max_read_bytes` / `max_write_bytes` / `max_append_bytes`)封住常见 -攻击面。Profile 门控、审计日志和快照属于纵深防御和故障恢复机制。 +构建依赖:Rust ≥ 1.85(edition 2024;CI 钉到 1.89 与 monorepo 共享 toolchain)、cmake(libgit2 vendored 构建)、systemd-devel(journald 审计 fan-out)。 + +### 跨平台开发 + +运行时仅支持 Linux(依赖 user_namespace、mount(2)、cgroup v2、inotify、journald)。macOS / Windows 请用远端构建: + +```bash +make remote-build # push 分支并 ssh 到 Linux 主机执行 cargo build +make remote-test # 同上 + 跑测试 + clippy +``` --- -## 2. 架构设计 +## 集成配置 -### 分层结构 +### Claude Code / Cursor / Continue / 任意 stdio MCP 客户端 +在 MCP 配置中添加: + +```json +{ + "mcpServers": { + "agent-memory": { + "command": "/usr/bin/agent-memory", + "args": [], + "env": { + "USER_ID": "alice", + "MEMORY_PROFILE": "advanced" + } + } + } +} ``` -+--------------------------------------------------------+ -| MCP 客户端 (Claude Code / Cursor / 自研) | -| stdio JSON-RPC 2.0 | -+----------------------------+---------------------------+ - | -+----------------------------v---------------------------+ -| MemoryMcpServer (rmcp) | -| tools/list -> 按 profile 过滤 | -| tools/call -> 入口处再做 profile 校验,返回 Result<> | -+----------------------------+---------------------------+ - | -+----------------------------v---------------------------+ -| MemoryService | -| 分发到具体 tool 实现;持有 mount / index / git / | -| snapshot / audit / session 句柄 | -+----+------------+--------------+-----------+-----------+ - | | | | -+----v---+ +----v-----+ +-----v----+ +---v-----+ -| Mount | | Index | | Git repo | | Snapshot| -| (auto/ | | (SQLite | | (libgit2 | | (tar.gz)| -| user- | | FTS5) | | vendored| | | -| land/ | | | | | | | -| userns| | | | | | | -+--------+ +----------+ +----------+ +---------+ - | | | | - +------+-----+--------+-----+-----+-----+ - | | | -+-----------v--------------v-----------v----+ -| safe_fs: openat2 RESOLVE_BENEATH | NO_SYM | -| fdopendir + fstatat + unlinkat | -+--------------------+----------------------+ - | -+--------------------v----------------------+ -| 命名空间 mount: ~/.anolisa/memory// | -| 用户数据 (notes/, decisions/, ...) | -| .anolisa/ (audit.log, index.db, ...) | -+-------------------------------------------+ + +`/usr/share/anolisa/mcp-servers/agent-memory.json` 描述符列出全部 37 个工具名,支持自动发现的客户端可直接识别。 + +### OpenClaw + +随包附带的插件把 4 个 memory contract 工具(`memory_search`、`memory_get`、`memory_observe`、`memory_get_context`)转发到 agent-memory: + +```bash +bash /usr/share/anolisa/adapters/agent-memory/openclaw/scripts/install.sh +openclaw gateway restart ``` -### Mount 策略 +或通过 anolisa 适配器管理: -| 策略 | 适用 | 行为 | -|------|------|------| -| `userland`(默认) | 任意环境 | mount 仅是普通目录,沙箱由 `openat2` 强制。 | -| `userns` | Linux ≥ 4.6,且内核允许 unprivileged user namespace | 启动时 `unshare` 进入新的 user + mount namespace,在 `/mnt` 上挂一层私有 tmpfs,再把 backing 目录 bind-mount 进去。宿主侧进程看不到 `/mnt/memory//` 下的内容。 | -| `auto` | 运行时探测 | 先尝试 `userns`;任何错误均回退到 `userland`。回退路径对部分失败具有鲁棒性(`unshare` / maps 阶段最多执行一次;mount 步骤幂等可重试)。 | +```bash +anolisa adapter enable agent-memory openclaw +anolisa adapter status agent-memory +``` + +**前置条件**:`openclaw` CLI 在 `$PATH` 上。脚本缺失时输出明确日志并以 0 退出,安装 OpenClaw 后重跑即可。`yum remove agent-memory` 时 spec 的 `%preun` 自动调用 uninstall 脚本,配置不残留孤立项。 + +插件 contract 名 ↔ agent-memory MCP 工具映射: + +| OpenClaw contract | agent-memory MCP 工具 | +|---|---| +| `memory_search` | `memory_search`(BM25 默认;配置 embedding 后支持 `mode=vector\|hybrid`) | +| `memory_get` | `mem_read` | +| `memory_observe` | `memory_observe` | +| `memory_get_context` | `memory_get_context` | + +插件配置(通过 OpenClaw UI 或 `openclaw.json` 的 `plugins.entries["memory-anolisa"].config`): + +| 键 | 默认 | 作用 | +|---|---|---| +| `binaryPath` | 自动发现:`$PATH` → `/usr/bin/agent-memory` → `/usr/local/bin/agent-memory` → `~/.local/bin/agent-memory` | 二进制绝对路径 | +| `userId` | env `USER_ID` → OS `uid` → env `$USER` | 命名空间 `user_id`;校验规则与 Rust 侧一致 | +| `profile` | `advanced` | profile 门控,以 `MEMORY_PROFILE` env 启动子进程 | +| `maxReadBytes` | `1048576`(1 MiB) | 单次 `mem_read` 上限,以 `MEMORY_MAX_READ_BYTES` env 传入 | +| `maxWriteBytes` | `16777216`(16 MiB) | 单次 `mem_write` 上限,以 `MEMORY_MAX_WRITE_BYTES` env 传入 | +| `sessionId` | env `MEMORY_SESSION_ID` → 新生成 `ses_` | 命名空间挂载会话,必须固定 | +| `sessionDir` | env `MEMORY_SESSION_DIR` → `/run/anolisa/sessions` | session scratch + log 根目录 | + +插件给子进程传最小 env allowlist(`PATH`、`HOME`、`USER`、`USER_ID`、`LANG`/`LC_ALL`/`LC_CTYPE`、`TZ`、`TMPDIR`、`XDG_RUNTIME_DIR` 及所有 `MEMORY_`/`RUST_` 前缀变量),其它 env 不泄漏。`USER_ID` 精确匹配,`USER_IDX` 等前缀变量不放行。 + +--- + +## MCP 工具集(37 个) -### 命名空间内的目录结构 +所有工具通过 MCP `tools/call` 调用,参数为 JSON 对象。错误以 `CallToolResult { isError: true }` 返回,客户端可据此区分"成功但内容含 failed 字面"与真实错误。Profile 在 `tools/list` 与 `tools/call` 两层校验。 + +### Tier A — 文件操作(11 个) + +| 工具 | 必填 | 可选 | 返回 | +|------|------|------|------| +| `mem_read` | `path` | — | UTF-8 文件内容 | +| `mem_write` | `path`、`content` | `overwrite` | `wrote N bytes to ` | +| `mem_append` | `path`、`content` | — | `appended N bytes to ` | +| `mem_edit` | `path`、`old_str`、`new_str` | — | `edited `(`old_str` 须唯一命中) | +| `mem_list` | — | `dir`、`recursive`、`glob` | `{name, type, size, mtime}` 数组 | +| `mem_grep` | `pattern` | `dir`、`type`、`max`、`case_insensitive` | `{path, line, text}` 数组 | +| `mem_diff` | `path1`、`path2` | — | unified diff | +| `mem_mkdir` | `path` | — | `created ` | +| `mem_remove` | `path` | `recursive` | `removed ` | +| `mem_promote` | `session_path`、`store_path` | — | 把会话 scratch 文件原子移入持久化仓 | +| `mem_session_log` | — | — | 当前会话 JSONL | + +### Tier B — 结构化检索(6 个) + +| 工具 | 必填 | 可选 | 返回 | +|------|------|------|------| +| `memory_search` | `query` | `top_k`(默认 5)、`mode`(bm25/vector/hybrid)、`category` | `{path, score, snippet, suspicious}` 数组 | +| `memory_observe` | `content` | `hint`、`type` | `observed at notes/observed/.md` | +| `memory_get_context` | — | `max_tokens`(默认 2048) | 最近修改文件的 markdown 预览,每条含 `suspicious` | +| `memory_sessions` | — | `limit`(默认 10) | 历史会话列表 | +| `memory_timeline` | — | — | 跨会话时间线 | +| `mem_index_refresh` | — | — | 强制重建 FTS5 索引 | + +### Tier C — 治理与版本(7 个) + +| 工具 | 必填 | 可选 | 返回 | +|------|------|------|------| +| `mem_snapshot` | — | `name` | `{id, name, created_at, size, backend}` | +| `mem_snapshot_list` | — | — | 按 `created_at` 升序数组 | +| `mem_snapshot_restore` | `id` | — | `restored ` | +| `mem_log` | — | `limit`(默认 20)、`path` | `{hash, summary, author, time}` 数组(需启用 git) | +| `mem_revert` | `path` | — | `reverted (commit )`(需启用 git) | +| `mem_consolidate` | — | — | `consolidation complete: N facts written` | +| `mem_compact` | — | — | `compacted N files to cold storage` | + +### 主权与导入导出(13 个) + +| 工具 | 说明 | +|------|------| +| `memory_about` | 记忆仓元信息 | +| `memory_auto_created` | 查询自动提取的事实 | +| `memory_consent` | 同意/撤回记忆操作 | +| `memory_forget` | 删除指定记忆条目 | +| `mem_export` | 导出记忆仓到归档 | +| `mem_import` | 从归档导入记忆 | +| `memory_task_save` | 保存任务上下文 | +| `memory_task_list` | 列出已保存任务 | +| `memory_task_resume` | 恢复任务上下文 | +| `memory_task_close` | 关闭任务 | +| `memory_summary` | 记忆仓统计概览 | +| `memory_session_context` | 会话启动上下文注入 | +| `mem_dream` | 用户画像合成 | + +### 错误码语义 + +| MCP 错误码 | 含义 | +|------------|------| +| `-32601` METHOD_NOT_FOUND | 当前 profile 隐藏了该工具 | +| `-32602` INVALID_PARAMS | 缺参或类型错 | +| `-32603` INTERNAL_ERROR | 服务端故障 | +| `isError: true` | 工具运行了但返回业务错误(路径不存在、被沙箱拒绝、大小超限等) | + +--- + +## 核心特性 + +### 文件形态记忆 + +Agent 用路径组织记忆,与人类文件系统模型一致: + +``` +notes/day1.md +decisions/2026-05/db-pick.md +context/project-overview.md +``` + +命名空间内的目录结构: ``` ~/.anolisa/memory/user-/ # mount root ├── README.md # 自动生成的概览 ├── notes/ # 自由形态笔记 -├── decisions/ # (示例:用户自定义子目录) +├── decisions/ # 用户自定义子目录 └── .anolisa/ # OS 管理,Agent 不可写 ├── manifest.toml # 命名空间元数据 ├── audit.log # JSONL 工具调用审计 @@ -126,166 +227,73 @@ └── git/ # bare git 镜像(启用 git 后才有) ``` -> 仅为代表性结构 —— `.anolisa/` 下的内容按需懒加载(如 `git/` -> 仅在 `MEMORY_GIT_ENABLED=true` 时存在)。 - -### 会话目录结构 +会话目录(tmpfs,权限 0700): ``` -/run/anolisa/sessions// # tmpfs,权限 0700 -├── meta.toml # 会话元数据 -├── log.jsonl # 当前会话工具调用日志 -└── scratch/ # 仅会话内的草稿, - # 通过 mem_promote 持久化 +/run/anolisa/sessions// +├── meta.toml +├── log.jsonl +└── scratch/ # 仅会话内草稿,通过 mem_promote 持久化 ``` -### 索引 worker +### 沙箱保护 -后台 tokio 任务通过 `inotify` 监听 mount,事件经 200 ms debounce 窗口 -聚合后,在单个 SQLite 事务中应用。分词器使用 `trigram`(对中文 / 日文友好), -schema 自带版本号便于未来无损迁移。当 inotify 队列溢出 -(`IN_Q_OVERFLOW`)时,worker 会自动触发全量 rescan,而不会静默丢事件。 +每次文件打开通过内核级 `openat2(RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS)` 锚定 mount root: -### 审计与可观测性 +- 拒绝 `..` 路径穿越 +- 拒绝符号链接(含调用中途的 symlink 替换;递归删除用 `fdopendir` + `fstatat(AT_SYMLINK_NOFOLLOW)` + `unlinkat`,让 swap 无法 race) +- 拒绝访问元数据目录(`.anolisa`、`.git`、`.gitignore` 由 `TargetIsReserved` 拒绝) +- `mem_snapshot_restore` 中 tar entry-type 过滤拒绝 `Symlink`/`Hardlink`/`Device`/`Fifo` +- payload 超大按 `max_*_bytes` 配置拒绝 -每次成功的工具调用都会向 `/.anolisa/audit.log` 追加一行 JSONL, -若启用了会话还会写入 `/run/anolisa/sessions//log.jsonl`。 -当 `audit.journald=true` 时,每行还会被 fan-out 到 systemd-journald, -带结构化字段(`MESSAGE_ID`、`AGENT_MEMORY_TOOL` 等),便于 `journalctl` -过滤。错误以 MCP 的 `CallToolResult { isError: true }` 形式返回, -让客户端能与"成功但内容包含 'failed' 字面"区分开。 +**Mount 策略**: ---- +| 策略 | 适用 | 行为 | +|------|------|------| +| `userland`(默认) | 任意环境 | mount 仅普通目录,沙箱由 `openat2` 强制 | +| `userns` | Linux ≥ 4.6 且允许 unprivileged user namespace | `unshare` 进入新 user+mount namespace,挂私有 tmpfs 再 bind-mount backing 目录;宿主侧进程看不到 `/mnt/memory//` | +| `auto` | 运行时探测 | 先尝试 `userns`,任何错误回退 `userland` | -## 3. 安装部署 +### 版本控制 -### 通过 RPM 安装(AnolisOS / RHEL 系,推荐) +可选自动 Git 提交(libgit2 vendored): ```bash -sudo yum install agent-memory +MEMORY_GIT_ENABLED=true MEMORY_GIT_AUTO_COMMIT=true agent-memory ``` -软件包安装内容: - -- `/usr/bin/agent-memory` —— 服务二进制 -- `/usr/share/anolisa/agent-memory/default.toml` —— 默认配置 -- `/usr/share/anolisa/mcp-servers/agent-memory.json` —— MCP 服务描述符 - (供自动发现) -- `/usr/lib/systemd/user/anolisa-memory@.service` —— 可选的 systemd - user 模板单元 -- `/usr/lib/tmpfiles.d/anolisa-memory.conf` —— 启动时创建 - `/run/anolisa/{,sessions}`(权限 0700) -- `/usr/share/anolisa/adapters/agent-memory/` —— OpenClaw 插件 bundle - (manifest、源码、预构建 `dist/index.js`、安装脚本) -- `/usr/share/doc/agent-memory/{CHANGELOG.md, user_manual.md, user_manual.zh.md}` - -### 安装 OpenClaw 插件(可选) - -[OpenClaw](https://github.com/openclaw) 是 Anolis OS 的 Agent 网关, -通过其自有契约消费插件(与裸 MCP stdio 不同)。如果同一台机上还有 -通过 `mcp-server.json` 直连 `/usr/bin/agent-memory` 的 MCP 客户端 -(Claude Code、Cursor、Continue 等),该客户端会看到全部 19 个原生工具 -(`mem_*` + `memory_*`);本 OpenClaw 插件则独立向 OpenClaw 暴露 -4 个 contract 名的子集。两条链路可共存 —— 每个 Agent 只看到所在 -runtime 实际广告的工具集。 - -注册随包附带的插件即可让 4 个 memory contract 工具(`memory_search`、 -`memory_get`、`memory_observe`、`memory_get_context`)转发到 -agent-memory: - -**前置条件**:`openclaw` CLI 必须在 `$PATH` 上。脚本会检测此条件, -缺失时输出明确日志并以 0 退出 —— 安装 OpenClaw 之后重跑即可。 +启用后 `mem_log` 暴露变更历史,配合 `mem_revert` 给 Agent 一个真正的"撤销"按钮。`mem_snapshot*` 提供 mount 范围的 tar.gz 时间点备份,独立于 git。 -```bash -bash /usr/share/anolisa/adapters/agent-memory/openclaw/scripts/install.sh -openclaw gateway restart -``` +### 全文搜索 -OpenClaw 安全扫描器将 `child_process.spawn` 标记为"危险代码模式",但插件仅用 spawn 启动 agent-memory MCP 服务端作为 stdio 子进程——这是标准 MCP 传输机制而非任意 shell 执行。安装脚本默认绕过扫描器。若需走常规(可能阻断的)安全安装路径,可设 `AGENT_MEMORY_SAFE_INSTALL=1` 调用脚本。 +SQLite FTS5 BM25 索引,亚毫秒级查询。后台 tokio 任务通过 `inotify` 监听 mount,事件经 200 ms debounce 聚合后在单事务中应用。分词器用 `trigram`(对中文/日文友好)。`IN_Q_OVERFLOW` 时自动触发全量 rescan,不静默丢事件。 -卸载(从 `~/.openclaw/extensions/` 移除插件并清理 `openclaw.json` 的 -`plugins.{allow,entries,slots}` 条目): +### 混合向量搜索 -```bash -bash /usr/share/anolisa/adapters/agent-memory/openclaw/scripts/uninstall.sh -``` +BM25 + 稠密向量混合检索,通过 RRF(Reciprocal Rank Fusion, k=60)融合排序。向量由可插拔 Embedding Provider 生成: -`yum remove agent-memory` 时 spec 的 `%preun` 会自动调用 uninstall -脚本,OpenClaw 配置不会残留孤立插件项。`jq` 优先用于改写 -`openclaw.json`;缺失时回退到 `python3`。 +| Provider | 配置方式 | 说明 | +|----------|---------|------| +| OpenAI | `MEMORY_EMBEDDING_BACKEND=openai` + `OPENAI_API_KEY` | 调用 OpenAI Embeddings API | +| Ollama | `MEMORY_EMBEDDING_BACKEND=ollama` + `OLLAMA_BASE_URL` | 本地 Ollama 实例 | -插件 contract 名 ↔ agent-memory MCP 工具映射: +`memory_search` 支持 `mode` 参数:`bm25`(默认)/ `vector`(余弦相似度)/ `hybrid`(RRF 融合)。未配置 embedding 时 `vector`/`hybrid` 自动降级为 BM25,不报错。 -| OpenClaw contract | agent-memory MCP 工具 | -|---|---| -| `memory_search` | `memory_search`(Tier B,BM25 默认;配置 embedding 后支持 `mode=vector\|hybrid`) | -| `memory_get` | `mem_read`(Tier A) | -| `memory_observe` | `memory_observe`(Tier B) | -| `memory_get_context` | `memory_get_context`(Tier B) | - -插件 MCP `clientInfo.version` 始终与 agent-memory RPM 版本一致 —— -esbuild 在打包时通过 Makefile `sync-versions` target 从 `Cargo.toml` -注入版本号,因此升级 RPM 后 OpenClaw 看到的插件版本会自动跟进。 - -插件配置(通过 OpenClaw 插件配置 UI 或 `openclaw.json` 的 -`plugins.entries["memory-anolisa"].config` 设置): - -| 键 | 类型 | 默认 | 作用 | -|---|---|---|---| -| `binaryPath` | string | 自动发现:先 `$PATH` 中的 `agent-memory`,再依次 `/usr/bin/agent-memory`、`/usr/local/bin/agent-memory`、`~/.local/bin/agent-memory` | agent-memory 二进制绝对路径 | -| `userId` | string | env `USER_ID` → OS `uid`(`process.getuid()`)→ env `$USER` | 命名空间 `user_id`;校验规则与 Rust 侧一致(不含 `..` / `/` / `\` / 控制字符,长度 ≤128 字节) | -| `profile` | `basic` / `advanced` / `expert` | `advanced` | profile 门控(§4)—— 插件以 `MEMORY_PROFILE=` env 启动 `agent-memory serve`,因此 systemd unit 或 shell 中预设的 `MEMORY_PROFILE` **会被插件配置覆盖** | -| `maxReadBytes` | integer (1..4 GiB) | `1048576`(1 MiB) | 单次 `mem_read` 上限;以 `MEMORY_MAX_READ_BYTES` env 传给子进程 | -| `maxWriteBytes` | integer (1..4 GiB) | `16777216`(16 MiB) | 单次 `mem_write` 上限;以 `MEMORY_MAX_WRITE_BYTES` env 传给子进程 | -| `sessionId` | string(`ses_` 形式) | env `MEMORY_SESSION_ID` → 新生成的 `ses_` 并在 client 生命周期内固定 | 命名空间挂载会话;以 `MEMORY_SESSION_ID` env 传给子进程。一定要固定 —— 若每次 spawn 都不同会导致 `mem_promote` 永远找不到旧 scratch | -| `sessionDir` | string | env `MEMORY_SESSION_DIR` → `/run/anolisa/sessions`(由 `anolisa-memory.conf` tmpfiles 在 boot 时创建) | session scratch + log 根目录;以 `MEMORY_SESSION_DIR` env 传给子进程 | - -插件给子进程传递一个最小的 env allowlist(`PATH`、`HOME`、`USER`、 -`USER_ID`、`LANG`、`LC_ALL`、`LC_CTYPE`、`TZ`、`TMPDIR`、 -`XDG_RUNTIME_DIR`,以及所有以 `MEMORY_` / `RUST_` 起头的变量), -其它来自 OpenClaw 进程的 env 不会泄漏到 agent-memory 子进程。 -`USER_ID` 是精确匹配,类似 `USER_IDX` 这种"挂着"前缀的变量不会被放过。 - -> **兼容性说明**:adapter `manifest.json` 声明 -> `compatibleVersions: ">=5.0.0"`。OpenClaw 实际用 CalVer 发布 -> (例如 `2026.5.7`),该约束仅作信息提示 —— 插件只用了稳定的 -> `openclaw/plugin-sdk` 表面,并在 5.x SDK 形态下验证过。如果未来 -> OpenClaw 破坏了 plugin-sdk 契约,应 bump `compatibleVersions` -> 后重新发布。 - -### 源码构建 +### 自动 Consolidation -```bash -git clone https://github.com/alibaba/anolisa.git -cd anolisa/src/agent-memory -make build # cargo build --release --locked -sudo make install # 安装到 /usr/local 下 -``` +服务关闭时自动从会话审计日志中提取原子事实(`mem_consolidate`),使用 6 条启发式规则(零 LLM 调用)识别高频路径、搜索模式等行为特征并持久化为结构化记忆。也可通过 `mem_consolidate` 工具手动触发。含情景记忆提取与冲突检测(BM25 阈值)。 -构建依赖:Rust ≥ 1.85(edition 2024 需 1.85;CI 钉到 1.89.0 -是为了与 monorepo 中其他 Linux Rust 子项目用同一镜像 toolchain)、 -cmake(libgit2 vendored 构建)、systemd-devel(journald 审计 fan-out 所需)。 - -### 跨平台开发 +### 审计与可观测性 -`agent-memory` 运行时仅支持 Linux。在 macOS / Windows 上请使用远端 -构建流程: - -```bash -# 在 src/agent-memory/ 下 -make remote-build # push 分支并 ssh 到 Linux 主机执行 cargo build -make remote-test # 同上 + 跑测试 + clippy -``` +每次成功工具调用向 `/.anolisa/audit.log` 追加 JSONL,启用会话还写入 `/run/anolisa/sessions//log.jsonl`。`audit.journald=true` 时 fan-out 到 systemd-journald,带结构化字段(`MESSAGE_ID`、`AGENT_MEMORY_TOOL` 等),便于 `journalctl --user-unit=anolisa-memory@` 过滤。 --- -## 4. 配置说明 +## 配置 ### 配置文件 -默认位置:`~/.anolisa/memory.toml`。所有 struct 都启用了 -`serde(deny_unknown_fields)`,配置项写错(拼写错误)会在加载时硬失败。 -最小配置示例: +默认位置:`~/.anolisa/memory.toml`。所有 struct 启用 `serde(deny_unknown_fields)`,配置项拼写错误会硬失败。最小配置: ```toml [global] @@ -309,9 +317,9 @@ strategy = "auto" # auto | userland | userns [memory.index] enabled = true -time_decay_lambda = 0.01 # 时间衰减系数,0=关闭 -time_decay_alpha = 0.3 # 时间权重占比 -cold_after_days = 30 # 多少天后归档为冷数据 +time_decay_lambda = 0.01 +time_decay_alpha = 0.3 +cold_after_days = 30 exclude_cold_on_search = true [memory.audit] @@ -326,228 +334,90 @@ enabled = false auto_commit = true [memory.consolidation] -enabled = true # 会话结束时自动提取事实 -max_facts = 20 # 每次最多提取 -min_tool_calls = 3 # 最少调用次数门槛 -episodic_enabled = true # 情景记忆提取 -min_episode_steps = 3 # 情景最少步骤数 +enabled = true +max_facts = 20 +min_tool_calls = 3 +episodic_enabled = true +min_episode_steps = 3 max_episodes_per_session = 10 -conflict_detection = true # 冲突检测 -conflict_bm25_threshold = -2.0 # BM25 冲突阈值 +conflict_detection = true +conflict_bm25_threshold = -2.0 ``` -### 环境变量覆盖 - -每个配置项都有对应的 `MEMORY_*` 环境变量,便于测试 / 临时调用: - -| 环境变量 | 对应配置 | 说明 | -|----------|----------|------| -| `USER_ID` | `global.user_id` | 经过校验;非法值会被 warn 后忽略。 | -| `MEMORY_BASE_DIR` | `memory.paths.base_dir` | | -| `MEMORY_PROFILE` | `memory.profile` | `basic` / `advanced` / `expert` | -| `MEMORY_SESSION_DIR` | `memory.session.base_dir` | | -| `MEMORY_SESSION_END` | `memory.session.end_action` | | -| `MEMORY_MOUNT_STRATEGY` | `memory.mount.strategy` | | -| `MEMORY_INDEX_ENABLED` | `memory.index.enabled` | systemd 风格的 truthy/falsy | -| `MEMORY_AUDIT_JOURNALD` | `memory.audit.journald` | | -| `MEMORY_CGROUP_ENABLED` | `memory.cgroup.enabled` | | -| `MEMORY_CGROUP_MEMORY_MAX` | `memory.cgroup.memory_max` | `512M` / `2G` / 字节数 | -| `MEMORY_GIT_ENABLED` | `memory.git.enabled` | | -| `MEMORY_GIT_AUTO_COMMIT` | `memory.git.auto_commit` | | -| `MEMORY_MAX_READ_BYTES` | `memory.max_read_bytes` | | -| `MEMORY_MAX_WRITE_BYTES` | `memory.max_write_bytes` | | -| `MEMORY_MAX_APPEND_BYTES` | `memory.max_append_bytes` | | -| `MEMORY_EMBEDDING_BACKEND` | `memory.embedding.backend` | `none` / `openai` / `ollama` | -| `MEMORY_OPENAI_API_KEY` | `memory.embedding.api_key` | 为空时回退到 `OPENAI_API_KEY` | -| `MEMORY_OPENAI_MODEL` | `memory.embedding.model` | 默认 `text-embedding-3-small` | -| `MEMORY_OLLAMA_MODEL` | `memory.embedding.model` | 默认 `nomic-embed-text` | -| `MEMORY_OLLAMA_BASE_URL` | `memory.embedding.base_url` | 默认 `http://localhost:11434` | -| `MEMORY_INDEX_TIME_DECAY_LAMBDA` | `memory.index.time_decay_lambda` | 需 ≥ 0.0 | -| `MEMORY_INDEX_TIME_DECAY_ALPHA` | `memory.index.time_decay_alpha` | 需 0.0–1.0 | -| `MEMORY_INDEX_COLD_AFTER_DAYS` | `memory.index.cold_after_days` | | -| `MEMORY_INDEX_EXCLUDE_COLD` | `memory.index.exclude_cold_on_search` | | -| `MEMORY_CONSOLIDATION_ENABLED` | `memory.consolidation.enabled` | | -| `MEMORY_CONSOLIDATION_MAX_FACTS` | `memory.consolidation.max_facts` | | -| `MEMORY_CONSOLIDATION_MIN_CALLS` | `memory.consolidation.min_tool_calls` | | -| `MEMORY_EPISODIC_ENABLED` | `memory.consolidation.episodic_enabled` | | -| `MEMORY_MIN_EPISODE_STEPS` | `memory.consolidation.min_episode_steps` | | -| `MEMORY_MAX_EPISODES` | `memory.consolidation.max_episodes_per_session` | | -| `MEMORY_CONFLICT_DETECTION` | `memory.consolidation.conflict_detection` | | -| `MEMORY_CONFLICT_THRESHOLD` | `memory.consolidation.conflict_bm25_threshold` | | -| `MEMORY_SESSION_ID` | (仅运行时) | 把当前 Agent 运行固定到 `MEMORY_SESSION_DIR` 下指定的 session id;`mem_promote` 必须设置,详见 §7。 | +### 环境变量 + +每个配置项都有对应 `MEMORY_*` 环境变量,优先级:**env > config.toml > default**。 + +| 环境变量 | 说明 | 默认 | +|----------|------|------| +| `USER_ID` | 用户标识(校验;非法值 warn 后忽略) | — | +| `MEMORY_PROFILE` | 配置档位(basic/advanced/expert) | advanced | +| `MEMORY_BASE_DIR` | 记忆仓根目录 | `~/.anolisa/memory` | +| `MEMORY_SESSION_DIR` | 会话根目录 | `/run/anolisa/sessions` | +| `MEMORY_SESSION_ID` | 固定当前会话 id(`mem_promote` 必须设) | 新生成 `ses_` | +| `MEMORY_SESSION_END` | 会话结束动作(discard/keep) | discard | +| `MEMORY_MOUNT_STRATEGY` | mount 策略(auto/userland/userns) | auto | +| `MEMORY_MAX_READ_BYTES` | 单次读取上限 | 1 MiB | +| `MEMORY_MAX_WRITE_BYTES` | 单次写入上限 | 16 MiB | +| `MEMORY_MAX_APPEND_BYTES` | 单次追加上限 | 4 MiB | +| `MEMORY_INDEX_ENABLED` | 启用 FTS5 索引 | true | +| `MEMORY_INDEX_TIME_DECAY_LAMBDA` | 时间衰减系数(≥0) | 0.01 | +| `MEMORY_INDEX_TIME_DECAY_ALPHA` | 时间权重占比(0–1) | 0.3 | +| `MEMORY_INDEX_COLD_AFTER_DAYS` | 冷数据归档天数 | 30 | +| `MEMORY_INDEX_EXCLUDE_COLD` | 搜索排除冷数据 | true | +| `MEMORY_AUDIT_JOURNALD` | fan-out 到 journald | false | +| `MEMORY_CGROUP_ENABLED` | 启用 cgroup 限制 | false | +| `MEMORY_CGROUP_MEMORY_MAX` | cgroup 内存上限 | 512M | +| `MEMORY_GIT_ENABLED` | 启用 git 版本控制 | false | +| `MEMORY_GIT_AUTO_COMMIT` | 自动提交 | true | +| `MEMORY_EMBEDDING_BACKEND` | embedding 后端(none/openai/ollama) | none | +| `MEMORY_OPENAI_API_KEY` | OpenAI API key(空时回退 `OPENAI_API_KEY`) | — | +| `MEMORY_OPENAI_MODEL` | OpenAI embedding 模型 | text-embedding-3-small | +| `MEMORY_OLLAMA_MODEL` | Ollama embedding 模型 | nomic-embed-text | +| `MEMORY_OLLAMA_BASE_URL` | Ollama base URL | http://localhost:11434 | +| `MEMORY_CONSOLIDATION_ENABLED` | 启用自动 consolidation | true | +| `MEMORY_CONSOLIDATION_MAX_FACTS` | 每次最多提取事实数 | 20 | +| `MEMORY_CONSOLIDATION_MIN_CALLS` | 最少调用次数门槛 | 3 | +| `MEMORY_EPISODIC_ENABLED` | 情景记忆提取 | true | +| `MEMORY_MIN_EPISODE_STEPS` | 情景最少步骤数 | 3 | +| `MEMORY_MAX_EPISODES` | 每会话最多情景数 | 10 | +| `MEMORY_CONFLICT_DETECTION` | 冲突检测 | true | +| `MEMORY_CONFLICT_THRESHOLD` | BM25 冲突阈值 | -2.0 | + +数据存储:`~/.anolisa/memory//`。 ### Profile 含义 -Profile 是 UX 提示而非安全边界,但在 `tools/list` 和 `tools/call` -两层都做了校验: - -- **basic** —— 25 个工具全部展示;弱模型也能用 Tier B 的结构化 API。 -- **advanced**(默认) —— 25 个工具全部展示;强模型应优先使用 Tier A - 文件操作。 -- **expert** —— 隐藏 Tier B(`memory_search`、`memory_observe`、 - `memory_get_context`),且 `tools/call` 调用会以 `METHOD_NOT_FOUND` - 拒绝。已经熟练操作文件系统的前沿模型只需要 Tier A 与 Tier C。 - +Profile 是 UX 提示而非安全边界,但在 `tools/list` 和 `tools/call` 两层校验: -### Embedding / 语义搜索 +- **basic** —— 37 个工具全部展示;弱模型也能用 Tier B 的结构化 API。 +- **advanced**(默认)—— 37 个工具全部展示;强模型应优先使用 Tier A 文件操作。 +- **expert** —— 隐藏 Tier B(`memory_search`、`memory_observe`、`memory_get_context`、`mem_consolidate`、`memory_forget`、`memory_consent`),`tools/call` 调用会以 `METHOD_NOT_FOUND` 拒绝。熟练操作文件系统的前沿模型只需 Tier A 与 Tier C. -配置 `[memory.embedding]` 段启用稠密向量语义搜索: +### Embedding 配置 ```toml [memory.embedding] -backend = "openai" -api_key = "" # 为空时自动读 OPENAI_API_KEY 环境变量 +backend = "openai" # 或 "ollama" +api_key = "" # 空时自动读 OPENAI_API_KEY 环境变量 model = "text-embedding-3-small" +# Ollama: backend = "ollama", model = "nomic-embed-text", base_url = "http://localhost:11434" ``` -或使用本地 Ollama(无需 API key): - -```toml -[memory.embedding] -backend = "ollama" -model = "nomic-embed-text" -base_url = "http://localhost:11434" -``` - -启用后,`memory_search` 支持 `mode` 参数: -- `mode="bm25"`(默认)—— 纯关键词搜索,向后兼容 -- `mode="vector"` —— 纯语义搜索(余弦相似度) -- `mode="hybrid"` —— RRF (k=60) 融合 BM25 + 向量排序 - -未配置 embedding 时,`vector` / `hybrid` 模式会自动降级为 BM25,不会报错。 ---- - -## 5. 主要功能 - -### Tier A —— 文件操作(11 个工具) - -`mem_read` / `mem_write` / `mem_append` / `mem_edit` / `mem_list` / -`mem_grep` / `mem_diff` / `mem_mkdir` / `mem_remove` / `mem_promote` / -`mem_session_log`。 - -Agent 以 mount 相对路径思考。保留前缀(`.anolisa`、`.git`、`.gitignore`) -在写入时被拒绝。`mem_edit` 要求 `old_str` 恰好命中一次(0 次或多次都 -报错),避免悄悄改错位置。`mem_promote` 把会话 `scratch/` 中的文件原子 -移入持久化仓。 - -### Tier B —— 结构化检索(3 个工具) - -`memory_search` 在 FTS5 索引上跑 BM25 关键字查询(默认),返回排序片段。 -当配置了 embedding 后端(OpenAI 或 Ollama)后,`mode="vector"` 启用 -纯语义搜索,`mode="hybrid"` 则以 RRF(Reciprocal Rank Fusion, k=60) -融合 BM25 和向量结果,兼顾关键词匹配与语义理解。无 embedding 时 -vector/hybrid 会自动降级为 BM25,不会报错。 - -`memory_observe` 把一段内容连同 frontmatter 写到 -`notes/observed/.md`,让 Agent "零决策"地记下一个想法。 -`memory_get_context` 按 token 上限拼出最近修改文件的 markdown 预览, -适合在每次回合开始时让 Agent "看一眼仓里都有什么"。返回的每条结果 -含 `suspicious` 字段,标记是否命中注入检测模式。 - -### Tier C —— 治理(5 个工具) - -`mem_snapshot` / `mem_snapshot_list` / `mem_snapshot_restore` 提供 -mount 范围的时间点备份(tar.gz + sidecar 元数据)。`mem_log` 与 -`mem_revert` 操作可选的 git 镜像 —— 适用于 "我三回合前改错文件了" 这种 -回滚需求。 - -### 沙箱保证 - -- 路径穿越(`..`、绝对路径、`\0`) → 内核通过 `openat2` 拒绝。 -- 调用中途的 symlink 替换 → 由 `RESOLVE_NO_SYMLINKS` 内核级阻止; - 递归删除使用 `fdopendir` + `fstatat(AT_SYMLINK_NOFOLLOW)` + - `unlinkat`,让 swap 无法 race。 -- 写覆盖保留路径(`.anolisa/audit.log`、`.gitignore` 等) → - 由 `TargetIsReserved` 拒绝。 -- payload 超大 → 按 `max_*_bytes` 配置拒绝。 -- `mem_snapshot_restore` 中混入 symlink → tar entry-type 过滤 - 拒绝 `Symlink` / `Hardlink` / `Device` / `Fifo`。 - --- -## 6. 接口(Tool API)参考 - -所有工具都通过 MCP `tools/call` 调用,参数为 JSON 对象。错误以 -`CallToolResult { isError: true, content: [{type: "text", text: -"<原因>"}] }` 形式返回,客户端可据此分支处理。 - -### Tier A - -| 工具 | 必填 | 可选 | 返回 | -|------|------|------|------| -| `mem_read` | `path` | — | UTF-8 文件内容 | -| `mem_write` | `path`、`content` | `overwrite` | `wrote N bytes to ` | -| `mem_append` | `path`、`content` | — | `appended N bytes to ` | -| `mem_edit` | `path`、`old_str`、`new_str` | — | `edited ` | -| `mem_list` | — | `dir`、`recursive`、`glob` | `{name, type, size, mtime}` 数组 | -| `mem_grep` | `pattern` | `dir`、`type`、`max`、`case_insensitive` | `{path, line, text}` 数组 | -| `mem_diff` | `path1`、`path2` | — | unified diff | -| `mem_mkdir` | `path` | — | `created ` | -| `mem_remove` | `path` | `recursive` | `removed ` | -| `mem_promote` | `session_path`、`store_path` | — | `promoted N bytes: -> ` | -| `mem_session_log` | — | — | 会话 JSONL 或 `(session log is empty)` | - -### Tier B - -| 工具 | 必填 | 可选 | 返回 | -|------|------|------|------| -| `memory_search` | `query` | `top_k`(默认 5)、`mode`(bm25/vector/hybrid)、`category`(lesson/interest/…) | `{path, score, snippet, suspicious}` 数组 | -| `memory_observe` | `content` | `hint` | `observed at notes/observed/.md` | -| `memory_get_context` | — | `max_tokens`(默认 2048) | markdown 预览 | - -### Tier C - -| 工具 | 必填 | 可选 | 返回 | -|------|------|------|------| -| `mem_snapshot` | — | `name` | JSON `{id, name, created_at, size, backend}` | -| `mem_snapshot_list` | — | — | 按 created_at 升序的数组 | -| `mem_snapshot_restore` | `id` | — | `restored ` | -| `mem_log` | — | `limit`(默认 20)、`path` | `{hash, summary, author, time}` 数组 | -| `mem_revert` | `path` | — | `reverted (commit )` | -| `mem_consolidate` | — | — | `consolidation complete: N facts written` | -| `mem_compact` | — | — | `compacted N files to cold storage` | - -### 错误码语义 +## 适用场景 -| MCP 错误码 | 含义 | -|------------|------| -| `-32601` METHOD_NOT_FOUND | 当前 profile 隐藏了该工具 | -| `-32602` INVALID_PARAMS | 缺参或类型错 | -| `-32603` INTERNAL_ERROR | 服务端故障 | -| `isError: true` | 工具运行了但返回了业务错误(路径不存在、被沙箱拒绝、大小超限等) | +- Agent 跨会话持久化笔记和决策(Claude Code、Cursor、Continue、自研 rmcp 客户端等)。 +- 多 Agent 系统中 Agent A 写、Agent B 读的笔记跨进程共享。 +- 操作审计和状态恢复(`mem_log`、JSONL 审计、journald、`mem_revert`、`mem_snapshot_restore`)。 +- "先草稿、决定后才持久化"的多回合模式(`mem_promote` 从 session scratch 原子移入持久化仓)。 --- -## 7. SDK / 客户端开发指南 - -### 接入 MCP 兼容客户端 - -#### Claude Code(`.claude/settings.json`) - -```json -{ - "mcpServers": { - "agent-memory": { - "command": "/usr/bin/agent-memory", - "args": [], - "env": { - "USER_ID": "alice", - "MEMORY_PROFILE": "advanced" - } - } - } -} -``` - -#### Cursor / Continue / 任意 stdio MCP 客户端 +## SDK / 客户端开发指南 -按相同的 `command` / `args` / `env` 形态指向二进制即可。 -`/usr/share/anolisa/mcp-servers/agent-memory.json` 描述符列出了 -全部 25 个工具名,支持自动发现的客户端能直接识别。 - -### 程序化接入 - -#### Python(官方 `mcp` SDK) +### Python(官方 `mcp` SDK) ```python import asyncio @@ -556,8 +426,7 @@ from mcp.client.stdio import stdio_client async def main(): server = StdioServerParameters( - command="/usr/bin/agent-memory", - args=[], + command="/usr/bin/agent-memory", args=[], env={"USER_ID": "alice"}, ) async with stdio_client(server) as (read, write): @@ -565,39 +434,34 @@ async def main(): await session.initialize() tools = await session.list_tools() print([t.name for t in tools.tools]) - result = await session.call_tool( "mem_write", {"path": "notes/from-python.md", "content": "hello"}, ) assert not result.isError - print(result.content[0].text) asyncio.run(main()) ``` -#### TypeScript(`@modelcontextprotocol/sdk`) +### TypeScript(`@modelcontextprotocol/sdk`) ```typescript import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; const transport = new StdioClientTransport({ - command: "/usr/bin/agent-memory", - args: [], + command: "/usr/bin/agent-memory", args: [], env: { USER_ID: "alice" }, }); const client = new Client({ name: "my-app", version: "1.0.0" }, {}); await client.connect(transport); - const result = await client.callTool({ name: "mem_grep", arguments: { pattern: "TODO", recursive: true, max: 50 }, }); -console.log(result.isError ? "failed" : result.content); ``` -#### Rust(`rmcp`) +### Rust(`rmcp`) ```rust use rmcp::transport::child_process::ChildProcessTransport; @@ -608,55 +472,34 @@ let transport = ChildProcessTransport::new( ).await?; let client = ().serve(transport).await?; let tools = client.list_tools(Default::default()).await?; -let resp = client.call_tool(rmcp::model::CallToolRequestParam { - name: "mem_read".into(), - arguments: Some(serde_json::json!({"path": "notes/x.md"}) - .as_object().unwrap().clone()), -}).await?; ``` -### Promote 工作流接入(多回合模式) - -适用于"先草稿、决定后才持久化"的 Agent: - -1. 每次 Agent 运行设置 `MEMORY_SESSION_ID=` 和 - `MEMORY_SESSION_DIR=/run/anolisa/sessions`。 -2. Agent 把草稿写到 `/run/anolisa/sessions//scratch/` 下 - (由运行时把文件落到 scratch 目录)。 -3. Agent 决定"这条值得保留"时,调用 `mem_promote` 原子地把文件 - 移入持久化仓。 - -### 可观测性接入点 +### Promote 工作流(多回合模式) -- `audit.journald=true` —— 每次调用 fan-out 到 - `journalctl --user-unit=anolisa-memory@`。 -- `mem_session_log` —— Agent 自身可读取本回合的 JSONL,做"自我反思"。 -- `mem_log`(启用 git 后) —— 把变更历史暴露给 Agent;配合 - `mem_revert` 给 Agent 一个真正的"撤销"按钮。 +1. 每次 Agent 运行设 `MEMORY_SESSION_ID=` 和 `MEMORY_SESSION_DIR=/run/anolisa/sessions`。 +2. Agent 把草稿写到 `/run/anolisa/sessions//scratch/`。 +3. Agent 决定"这条值得保留"时调用 `mem_promote` 原子移入持久化仓。 --- -## 8. 功能测试与验证 +## 功能测试与验证 -### 8.1 自动化测试 +### 自动化测试 ```bash cd src/agent-memory cargo fmt --check cargo clippy -- -D warnings -cargo test # 全部 suite -cargo test --test e2e_agent_test # 19 工具 E2E -cargo test --test mcp_integration_test # 协议层 -cargo test --test linux_userns_test -- --ignored # 需要 unprivileged userns +cargo test # 全部 suite +cargo test --test e2e_agent_test # 工具 E2E +cargo test --test mcp_integration_test # 协议层 +cargo test --test linux_userns_test -- --ignored # 需 unprivileged userns +make smoke # 一键端到端冒烟 ``` -`ci.yaml` 上的 CI Job 会跑 `fmt --check` + `clippy -D warnings` + -`cargo test`,Rust 版本锁定 1.89。 +CI 跑 `fmt --check` + `clippy -D warnings` + `cargo test`,Rust 锁定 1.89。 -### 8.2 交互式 `mcp-harness` - -`mcp-harness` 是一个 example 二进制,通过 stdio 驱动服务端,并提供 -REPL 用于手动调用工具: +### 交互式 `mcp-harness` ```bash cargo run --example mcp-harness -- /tmp/mem-test @@ -665,33 +508,13 @@ cargo run --example mcp-harness -- /tmp/mem-test | 命令 | 说明 | |------|------| | `list` | 列出当前可见工具 | -| `call ` | 调用某个工具 | -| `help` | 显示命令帮助 | -| `quit` | 关闭服务并退出 | - -示例会话: +| `call ` | 调用工具 | +| `help` | 帮助 | +| `quit` | 退出 | -``` -mcp> call mem_mkdir {"path": "notes"} -Result: created notes -mcp> call mem_write {"path": "notes/day1.md", "content": "Hello world"} -Result: wrote 11 bytes to notes/day1.md -mcp> call mem_read {"path": "notes/day1.md"} -Result: Hello world -``` +预置场景:`--scenario full` / `git --git` / `promote` / `--verbose`(打印 JSON-RPC)。 -预置场景(无断言,由人观察输出确认): - -```bash -cargo run --example mcp-harness -- /tmp/mem-test --scenario full -cargo run --example mcp-harness -- /tmp/mem-test --scenario git --git -cargo run --example mcp-harness -- /tmp/mem-test --scenario promote -cargo run --example mcp-harness -- /tmp/mem-test --verbose # 打印 JSON-RPC -``` - -### 8.3 直发 JSON-RPC(协议级调试) - -启动服务并向其 stdin 喂 JSON-RPC: +### 直发 JSON-RPC(协议级调试) ```bash mkdir -p /tmp/mem-test/__sessions__ @@ -702,120 +525,62 @@ USER_ID=tester \ agent-memory ``` -握手: +握手 + 工具调用: ```json {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"manual","version":"1.0"}}} {"jsonrpc":"2.0","method":"notifications/initialized"} -``` - -工具调用: - -```json {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"mem_write","arguments":{"path":"test.md","content":"hello"}}} ``` -预期响应: - -```json -{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"wrote 5 bytes to test.md"}],"isError":false}} -``` - -### 8.4 沙箱越界验证 - -确认内核沙箱拒绝以下逃逸: +### 沙箱越界验证 ```json -{"jsonrpc":"2.0","id":10,"method":"tools/call","params":{"name":"mem_read","arguments":{"path":"../../etc/passwd"}}} +{"name":"mem_read","arguments":{"path":"../../etc/passwd"}} ``` → `isError: true`,消息 `path outside mount root`。 ```json -{"jsonrpc":"2.0","id":11,"method":"tools/call","params":{"name":"mem_write","arguments":{"path":".anolisa/audit.log","content":"x"}}} +{"name":"mem_write","arguments":{"path":".anolisa/audit.log","content":"x"}} ``` → `isError: true`,消息 `target is reserved`。 -```json -{"jsonrpc":"2.0","id":12,"method":"tools/call","params":{"name":"mem_read","arguments":{"path":"a/b/symlink-to-etc-passwd"}}} -``` -→ `isError: true`,消息 `path outside mount root`(内核 ELOOP)。 - -### 8.5 单工具验证流程 - -下列流程既可在 harness REPL 中(`call `)执行,也可 -通过 raw JSON-RPC 验证。在 `mcp-harness` 中跑最直接: - -- **mem_mkdir** —— `call mem_mkdir {"path":"d"}`,响应包含 `created`; - 再 `call mem_list {"recursive": true}` 验证。 -- **mem_write / mem_read** —— 写入 `Hello world\n`,读回字节级一致; - `overwrite=false` 重写应返回错误。 -- **mem_append** —— 追加 `+more`,再读,内容等于 `原文+more`。 -- **mem_edit** —— 写入 `foo bar baz`,把 `bar` 编辑为 `qux`, - 读回得 `foo qux baz`;再次执行(`bar` 已不存在)应报错 - `match count 0`。 -- **mem_list** —— 创建嵌套目录与文件,递归列表应包含全部路径, - 外加 init 时自动产生的 `README.md`。 -- **mem_grep** —— 写入两个含不同关键词的文件,搜索其中一个关键词 - 应只命中匹配文件,且每个 hit 含 `path / line / text`。 -- **mem_diff** —— 对两个文件 diff,输出以 `--- ` / `+++ ` 行起始 - 的 unified diff。 -- **mem_remove** —— 删除文件后再读应报错 `not found`。 -- **mem_promote** —— 预创建 `MEMORY_SESSION_DIR//scratch/x.md`, - 设置环境变量后调用 promote,再读目标路径。 -- **mem_session_log** —— 任意调用 3 次工具后 `mem_session_log` 应返 - 回 3 行 JSONL。 -- **memory_observe** —— 调用两次 observe;递归列 `notes/observed` - 应有两个 ULID 命名的文件。 -- **memory_search** —— 用关键字 `kappa` observe,等待 ~500 ms,再 - search `kappa`,结果包含该 observe 文件。 -- **memory_get_context** —— 写入 5 个有不同首行的文件, - `memory_get_context {max_tokens: 200}` 返回的预览能见到它们。 -- **mem_snapshot / list** —— 创建快照后 list 应有条目; - size > 0;id 以 `snap_` 起头。 -- **mem_snapshot_restore** —— 写 v1,快照,写 v2,restore 快照后 - 读回得 v1;`.anolisa/trash/-/` 中保留有 v2。 -- **mem_log** —— 启用 git 后写入同一文件三个版本, - `mem_log {path: "..."}` 至少返回 3 条 commit。 -- **mem_revert** —— 启用 git 后,写 v3,revert,再读得最近一次提交 - 内容(v2)。 - -### 8.6 一键冒烟测试 - -Makefile 自带一个独立 smoke target,会驱动 5 个工具走完整流程并 -校验响应: +--- + +## 故障排查 + +### 诊断工具 ```bash -cd src/agent-memory -make smoke -``` +# 组件级诊断 + 自动修复 +anolisa doctor agent-memory --fix -看到绿色的 `==> Smoke test PASSED` 即可认为部署端到端正常。 +# 适配器状态 +anolisa adapter status agent-memory ---- +# 启动调试 +RUST_LOG=agent_memory=debug agent-memory +``` -## 9. 故障排查 +### 常见问题 | 症状 | 可能原因 | 处理 | |------|----------|------| -| 启动报 `unshare(NEWUSER\|NEWNS): EPERM` | unprivileged user namespace 被禁 | `sysctl kernel.unprivileged_userns_clone=1`,或者改 `MEMORY_MOUNT_STRATEGY=userland`。 | -| `tmpfs /mnt: EBUSY` | 新 namespace 中 `/mnt` 已被其他 mount 占据 | 重试逻辑会把 EBUSY 视作成功;如仍持续,重启进程。 | -| macOS / Windows 上 `cargo build` 报 `libsystemd` / `nix` 错 | 宿主非 Linux | 改用 `make remote-build` / `remote-test`。 | -| `tools/call memory_search` 返 `METHOD_NOT_FOUND` | `MEMORY_PROFILE=expert` 隐藏了 Tier B | 切回 `advanced`,或直接用 Tier A 文件工具。 | -| 配置项 typo 被悄悄忽略 | 旧版本会 default-fill 错字段 | 现已硬失败:看启动 stderr 的报错并修正。 | -| `mem_log` 返回 `[]` 即使有写入 | git 版本控制未启用 | `MEMORY_GIT_ENABLED=true MEMORY_GIT_AUTO_COMMIT=true`。 | -| 索引检索对刚写入的内容查不到 | 还在 200 ms debounce 窗口内 | 重试;或改用 `mem_grep`(直接走文件系统正则,不依赖索引)。 | -| `mem_promote` 报 `session not found` | `MEMORY_SESSION_ID` / `MEMORY_SESSION_DIR` 未设或 scratch 不存在 | 见 §7 Promote 工作流接入。 | - -更深入的排查:用 `RUST_LOG=agent_memory=debug` 启动,同时检查 -服务端 stderr 与 `/.anolisa/audit.log`。 +| 启动报 `unshare(NEWUSER\|NEWNS): EPERM` | unprivileged user namespace 被禁 | `sysctl kernel.unprivileged_userns_clone=1`,或 `MEMORY_MOUNT_STRATEGY=userland` | +| `tmpfs /mnt: EBUSY` | 新 namespace 中 `/mnt` 已被占用 | 重启进程 | +| macOS / Windows `cargo build` 报 `libsystemd`/`nix` 错 | 宿主非 Linux | `make remote-build` / `remote-test` | +| `tools/call memory_search` 返 `METHOD_NOT_FOUND` | `MEMORY_PROFILE=expert` 隐藏 Tier B | 切回 `advanced`,或直接用 Tier A | +| 配置项 typo 被悄悄忽略 | — | 现已硬失败,看启动 stderr 报错并修正 | +| `mem_log` 返回 `[]` 即使有写入 | git 版本控制未启用 | `MEMORY_GIT_ENABLED=true MEMORY_GIT_AUTO_COMMIT=true` | +| 索引检索对刚写入的内容查不到 | 还在 200 ms debounce 窗口内 | 重试,或用 `mem_grep`(直接走文件系统正则,不依赖索引) | +| `mem_promote` 报 `session not found` | `MEMORY_SESSION_ID`/`MEMORY_SESSION_DIR` 未设或 scratch 不存在 | 见 Promote 工作流 | +| OpenClaw 插件未加载 | `openclaw` CLI 不在 PATH | 安装 OpenClaw 后重跑 `install.sh` | +| 手动 dnf 操作后状态不同步 | — | `anolisa repair agent-memory` / `anolisa forget` / `anolisa adopt` | + +深入排查:`RUST_LOG=agent_memory=debug` 启动,检查服务端 stderr 与 `/.anolisa/audit.log`。 --- -## 许可证 - -Apache-2.0。详见随包发布的 `LICENSE`。 - -## 反馈问题 - -[`github.com/alibaba/anolisa/issues`](https://github.com/alibaba/anolisa/issues), -组件 `memory`。 +**许可证**:Apache-2.0 +**版本**:0.1.0 +**文档版本**:2.0(对齐 ANOLISA-design user-guide 结构)