Sign in

    mnemo

    Hermes-compatible memory / skills / session / prompt layer for AI agents (MoonBit native + JS)

    memory
    agent
    hermes
    mcp
    session
    prompt
    llm
    Download zip
    Author
    Version
    0.2.2
    License
    MIT
    Last updated
    6 months ago
    Downloads
    68

    #mnemo

    CI

    Hermes Agent 記憶層の MoonBit 移植 — Node.js proto + MCP/pi アダプター付き

    411 tests (JS) / 116 tests (native) passing  |  MoonBit 0.1.20260409  |  Node.js 24+

    Note: This project is named mnemo (short for mnemonic). The hermes-agent upstream is still called hermes.


    #What is this?

    Hermes Agent (Nous Research, Python) は AI エージェントのための記憶・スキル・セッション管理層です。mnemo はその中核サブシステム (MemoryStore / SkillsStore / SessionDB / session_search / PromptBuilder) を MoonBit (native + JS 両 backend) に移植したプロジェクトです。

    proto/ は Node.js/TypeScript で書かれたリファレンス実装です。goldens (contract fixture) の生成専用で本番には使いません。本番は src/ の MoonBit 実装で、 contract/ に収められた tool schemas・ファイル形式・SQL スキーマ・セキュリティパターン・ golden fixtures が両実装の単一のソース・オブ・トゥルースです。

    本番ビルドは moon build --target js --release で dist/src.js が生成されます。 npm 配布時はこれを dist/src.cjs にリネームしてアダプターへ同梱し (adapter package.json の "type": "module" の下でも CommonJS として解決させるため)、 アダプターは require() で読み込み globalThis.__mnemo_api 経由で 13 tool を呼び出します。

    アダプターは 2 つあります:

    • @mizchi/mnemo-mcp (adapters/mcp-server/) — MCP stdio サーバー。 Claude Code から claude mcp add で登録すると 13 tool が直接使えます。
    • @mizchi/mnemo-pi (adapters/pi-extension/) — pi-mono 拡張。 pi.extensions で読み込むと mnemo:memory_add などが pi から利用できます。

    hermes との機能パリティは約 99% です。詳細は docs/PARITY_AUDIT.md。


    #Quick Start

    # Prerequisites: Node.js 24+, pnpm, moon, just pnpm install just test # proto 280 + moonbit 121 + adapters 10 = all green (411) just build-dist # dist/src.js を生成 (adapters が src.cjs として参照)

    個別実行:

    just test-proto # proto vitest (280 件、うち 135 が goldens-lint) just goldens # proto で golden JSON を再生成 (45 ファイル) just test-mbt # MoonBit inline tests on JS backend (121 件) just test-adapters # MCP server (7) + pi extension (3) adapter tests

    just test の実行順は test-proto → goldens → test-mbt → build-dist → test-adapters です。 goldens を更新してから MoonBit 側をテストしないとパリティがずれ、adapters は build-dist した src.js を参照します。

    #Native target (cross-platform)

    MoonBit native backend 対応済。cross-target コア (memory / skills / session_db / prompt / lock / sqlite_ffi / fts5 / security / frontmatter) を libsqlite3 + C stub 経由で走らせられます。

    # Nix 利用者(推奨)— `nix develop` が sqlite + NIX_LDFLAGS を設定 nix develop just test-mbt-native # 116/121 tests green (JS-only Promise concurrency excluded) # 手動設定 # Linux: sudo apt-get install libsqlite3-dev # macOS: xcode-select --install もしくは brew install sqlite just build-native # MoonBit native compile just test-mbt-native # native test runner

    Node.js adapter (mnemo_api.mbt) と session_search.mbt の Promise.all 並列化オーケストレーションは targets: [js] で JS 限定。その他の コアライブラリ(memory / skills / session_db / search_core / prompt_builder / security / fts5 / goldens)はすべて native でも走り、 116 / 121 tests が通ります(残りは JS-only な Promise.all concurrency テスト 5 件)。


    #Tools (13 tools)

    すべての tool schema は contract/tool-schemas/ に JSON Schema として置かれています。

    Tool説明
    memory_addMEMORY.md / USER.md に新しいエントリを追加する
    memory_replace既存エントリをキーで置換する
    memory_removeエントリをキーで削除する (複数ヒットは ambiguous_match で拒否)
    memory_readMEMORY.md または USER.md の全エントリを読む
    skills_listplatform / available_tools / disabled[] でフィルタしたスキル一覧を返す
    skill_view単一スキルのフルコンテンツ (frontmatter + 本文) を返す
    session_create新しいセッションを作成する
    session_appendセッションにメッセージを追記する
    session_getセッション ID でセッションとメッセージを取得する
    session_listセッション一覧を返す (最新順)
    session_searchFTS5 全文検索 + 並列 LLM 要約 (proto / MoonBit 双方で concurrency 3)
    session_resolveセッション ID prefix (UUID 先頭 8 字など) からフル ID を解決する
    prompt_buildidentity → memory → user → skills → context → SOUL の順で system prompt を構築する (disabled_skills[] 対応)


    #Adapter 統合

    #Claude Code (MCP)

    # ローカル開発: claude mcp add mnemo -- node /path/to/adapters/mcp-server/bin/mnemo-mcp # 公開後 (予定): claude mcp add mnemo -- npx -y @mizchi/mnemo-mcp

    Claude Code セッション中に memory_add などのツールが直接呼べるようになります。 詳細: adapters/mcp-server/README.md

    #pi-mono (pi extension)

    pi.extensions に以下を追加してください:

    { "pi": { "extensions": ["/path/to/adapters/pi-extension/src/index.ts"] } }


    #環境変数

    変数デフォルト説明
    MNEMO_HOME~/.mnemomemories・skills・sessions.db のルートディレクトリ
    MNEMO_CONTRACT_DIR—adapter から contract/ を参照するときのパス (通常は自動解決)
    OPENROUTER_API_KEY—LLM 要約を有効化する場合の API キー
    MNEMO_SUMMARIZER_API_KEY—OPENROUTER_API_KEY の代替
    MNEMO_SUMMARIZER_MODEL—要約に使う LLM モデル名 (OpenRouter 形式、例 openai/gpt-4o-mini)
    MNEMO_SEARCH_CONCURRENCY3session_search の並列 LLM 要約数 (1-5、proto + MoonBit)
    MNEMO_LOCK_STALE_TTL_MS30000memories/.writing sentinel が stale と判定されるまでの ms

    MNEMO_SUMMARIZER_MODEL + OPENROUTER_API_KEY が設定されている場合、 session_search は OpenRouter 経由で LLM 要約を生成し結果に付与します。 未設定時は raw preview にフォールバックします。


    #ドキュメント

    ファイル内容
    docs/ARCHITECTURE.md3 層構造・サブシステム詳細・MoonBit JS 橋渡し
    docs/PARITY_AUDIT.mdhermes 再現率の詳細 (機能ごとの対応状況)
    docs/MOONBIT_GOTCHAS.mdMoonBit API 差分・既知の制限
    docs/superpowers/specs/設計仕様
    docs/superpowers/plans/Phase 別実装計画 (Phase 0–10)。Phase 11-12 は個別 commit としてのみ


    #Phase / Tag 一覧

    実装は以下のフェーズに分割して進行し、各フェーズ完了時に git tag を打っています。 プロジェクトは途中で ermes → mnemo に改名しましたが、 過去のタグ名は歴史の整合性を保つため ermes-phase* のまま残しています。

    Tag内容
    ermes-phase1-memoryMemoryStore (MEMORY.md / USER.md)
    ermes-phase2-skillsSkillsStore (SKILL.md + frontmatter)
    ermes-phase3-sessiondbSessionDB (node:sqlite, FTS5)
    ermes-phase5-prompt-builderPromptBuilder (identity → SOUL)
    ermes-phase6-adaptersMCP server + pi extension adapters
    ermes-phase8-asyncasync/await 移行 (moonbitlang/async)
    ermes-phase9-safetyセキュリティパターン + FTS5 sanitize
    ermes-phase9-completeparity 完成 (safety + LLM + WAL + schema)
    ermes-phase10-completemodel guidance + Int64 統一 + session_resolve + LICENSE/docs/CI

    Phase 10 以降の強化は個別 commit として main に積まれています:
    • Phase 10 以降の tier-2: skills disabled filter / MoonBit session_search concurrency / stale lock recovery / atomic write cleanup / goldens lint / MCP stdio bugfixes
    • Phase 11 (cross-platform): SessionDB → mizchi/sqlite、fs/env → moonbitlang/x/fs + core/env、native_stub.c で O_EXCL / fsync+rename の C primitives、lock/memory_store/async_utils/hello の backend split
    • Phase 12 (goldens + search_core cross-target): goldens_*.mbt の node:fs FFI 除去、search_core.mbt を session_search.mbt から抽出してピュア helper を cross-target に

    git tag | grep -E "^(ermes|mnemo)-"


    #Architecture (概略)

    contract/ ← single source of truth tool-schemas/ ← 13 tool の JSON Schema file-formats/ ← MEMORY.md / USER.md / SKILL.md エンコード仕様 sql/schema.sql ← SessionDB DDL + migrations/ security/ ← インジェクション検出パターン (13 項目) goldens/ ← proto が生成する fixture JSON (45 ファイル) proto/ ← Node.js/TS throwaway (golden 生成専用) src/ ← MoonBit 本番実装 (native + JS) *.mbt ← cross-target コア (memory / skills / session_db / search_core / prompt / security / fts5 / goldens) *_js.mbt ← JS backend 固有 (lock / memory_store / async_utils / hello) *_native.mbt ← native backend 固有 (同上、extern "C" via native_stub.c) mnemo_api.mbt ← Node.js adapter glue (JS-only) session_search.mbt ← Promise.all LLM orchestration (JS-only) native_stub.c ← POSIX primitives (O_EXCL / stat mtime / fsync+rename) dist/src.js ← moon build --target js --release 出力 npm 配布時は adapters/*/dist/src.cjs にリネームコピー adapters/ mcp-server/ ← @mizchi/mnemo-mcp (Claude Code, JS 専用) pi-extension/ ← @mizchi/mnemo-pi (pi-mono, JS 専用)

    詳細は docs/ARCHITECTURE.md を参照してください。


    #License

    MIT — Copyright (c) 2026 mizchi

    AddResult

    pub enum AddResult {
    AddOk(Int, Int)
    AddErr(String, String)
    }

    BuildPromptOptions

    pub struct BuildPromptOptions {
    hermes_home : String
    cwd : String
    platform : String
    model : String
    available_tools : Array[String]
    disabled_skills : Array[String]
    }

    FtsResult

    pub struct FtsResult {
    session_id : String
    message_id : Int
    content : String
    }

    JsRow

    pub struct JsRow {
    column_index : Map[String, Int]
    cells : Array[
    SqlValue
    ]
    }

    JsRows

    pub struct JsRows {
    column_index : Map[String, Int]
    rows : Array[Array[
    SqlValue
    ]]
    }

    A fully-materialized result set. Each row is an Array[SqlValue] indexed by the order columns appear in the SELECT. The name→index map lets callers continue to write stmt_get_text(rows, row_idx, "col").

    MemoryStore

    pub struct MemoryStore {
    root_dir : String
    entries_memory : Array[String]
    entries_user : Array[String]
    snapshot_memory : String
    snapshot_user : String
    max_memory : Int
    max_user : Int
    }

    MemoryStore::add

    async fn MemoryStore::add(self : MemoryStore, target : Target, content : String) -> AddResult
    Add a new entry. Acquires a per-directory write lock before touching disk.

    MemoryStore::list

    fn MemoryStore::list(self : MemoryStore, target : Target) -> Array[String]
    Return a copy of entries for the given target.

    MemoryStore::load

    fn MemoryStore::load(root_dir : String, max_memory? : Int, max_user? : Int) -> MemoryStore
    Load (or create) a MemoryStore from the given root directory.

    MemoryStore::remove

    async fn MemoryStore::remove(self : MemoryStore, target : Target, old_substring : String) -> OpResult
    If multiple entries match → ambiguous_match (no modification).

    MemoryStore::replace

    async fn MemoryStore::replace(self : MemoryStore, target : Target, old_substring : String, new_content : String) -> OpResult
    Replace the first entry containing old_substring with new_content.

    MemoryStore::snapshot

    fn MemoryStore::snapshot(self : MemoryStore, target : Target) -> String
    Return the frozen snapshot for the given target (taken at load time).

    MsgForFormat

    pub struct MsgForFormat {
    role : String
    content : String
    tool_name : String
    tool_calls : String
    }

    MsgRow

    pub struct MsgRow {
    id : Int
    role : String
    content : String
    tool_name : String
    reasoning_details : String?
    codex_reasoning_items : String?
    }

    OpResult

    pub enum OpResult {
    OpOk(Int, Int)
    OpErr(String, String)
    }

    PatternEntry

    type PatternEntry

    ScanResult

    pub enum ScanResult {
    ScanOk
    ScanBlocked(String)
    }

    SearchResult

    pub struct SearchResult {
    session_id : String
    when : String
    source : String
    model : String
    summary : String
    }

    SessionDb

    pub struct SessionDb {
    db : SqliteDb
    db_path : String
    write_count : Int
    checkpoint_interval : Int
    }

    SessionRow

    pub struct SessionRow {
    id : String
    source : String
    user_id : String?
    model : String?
    system_prompt : String
    parent_session_id : String?
    started_at : Int64
    ended_at : Int64?
    end_reason : String?
    message_count : Int
    input_tokens : Int
    output_tokens : Int
    cache_read_tokens : Int
    cache_write_tokens : Int
    title : String?
    }

    SessionSearchResponse

    pub struct SessionSearchResponse {
    ok : Bool
    query : String
    results : Array[SearchResult]
    count : Int
    sessions_searched : Int
    }

    SkillEntry

    pub struct SkillEntry {
    name : String
    skill_name : String
    description : String
    category : String
    content : String
    frontmatter : SkillFrontmatter
    }

    SkillFrontmatter

    pub struct SkillFrontmatter {
    name : String?
    description : String?
    platforms : Array[String]
    fallback_for_tools : Array[String]
    fallback_for_toolsets : Array[String]
    requires_tools : Array[String]
    requires_toolsets : Array[String]
    tags : Array[String]
    }

    Parsed SKILL.md frontmatter (flat fields only for MVP).

    SkillsStore

    pub struct SkillsStore {
    skills : Array[SkillEntry]
    }

    SqliteDb

    pub struct SqliteDb {
    inner :
    Database

    }

    SqliteStmt

    pub struct SqliteStmt {
    inner :
    Statement

    pending : Array[
    SqlValue
    ]
    }

    Target

    pub enum Target {
    Memory
    User
    }

    ViewResult

    pub enum ViewResult {
    ViewOk(String, String)
    ViewErr(String)
    }

    _row_at

    fn _row_at(rows : JsRows, idx : Int) -> JsRow

    _row_get_int

    fn _row_get_int(row : JsRow, col : String) -> Int

    _row_get_int64

    fn _row_get_int64(row : JsRow, col : String) -> Int64

    _row_get_text

    fn _row_get_text(row : JsRow, col : String) -> String

    _row_is_null

    fn _row_is_null(row : JsRow, col : String) -> Bool

    _rows_length

    fn _rows_length(rows : JsRows) -> Int

    _stmt_all

    fn _stmt_all(stmt : SqliteStmt) -> JsRows

    _stmt_bind_int

    fn _stmt_bind_int(stmt : SqliteStmt, i : Int, val : Int) -> Unit

    _stmt_bind_int64

    fn _stmt_bind_int64(stmt : SqliteStmt, i : Int, val : Int64) -> Unit

    _stmt_bind_null

    fn _stmt_bind_null(stmt : SqliteStmt, i : Int) -> Unit

    _stmt_bind_text

    fn _stmt_bind_text(stmt : SqliteStmt, i : Int, val : String) -> Unit

    _stmt_buffer_init

    fn _stmt_buffer_init() -> Unit

    No-op kept for source compatibility with the previous FFI. The old JS implementation stashed pending bindings in globalThis.__mnemo_stmt_buf and needed this one-time init; the new wrapper carries pending values inside the SqliteStmt value itself, so initialization is implicit.

    _stmt_run

    fn _stmt_run(stmt : SqliteStmt) -> Unit

    append_message

    async fn append_message(sdb : SessionDb, session_id : String, role : String, timestamp : Int64, content? : String?, tool_name? : String?, token_count? : Int?, reasoning_details? : String?, codex_reasoning_items? : String?) -> Int

    build_system_prompt

    fn build_system_prompt(opts : BuildPromptOptions) -> String

    close_session_db

    fn close_session_db(sdb : SessionDb) -> Unit

    create_session

    async fn create_session(sdb : SessionDb, source : String, system_prompt : String, model? : String?, user_id? : String?, parent_session_id? : String?) -> String

    db_exec

    fn db_exec(db : SqliteDb, sql : String) -> Unit

    default_lock_stale_ttl_ms

    let default_lock_stale_ttl_ms : Int
    almost certainly crashed — memory writes are sub-millisecond atomic rename.

    delim

    let delim : String

    end_session

    async fn end_session(sdb : SessionDb, session_id : String, end_reason : String) -> Unit

    format_conversation

    fn format_conversation(messages : Array[MsgForFormat]) -> String

    get_compression_tip

    fn get_compression_tip(sdb : SessionDb, session_id : String) -> String

    get_messages

    fn get_messages(sdb : SessionDb, session_id : String) -> Array[MsgRow]

    get_session

    fn get_session(sdb : SessionDb, session_id : String) -> SessionRow?
    Get a single session by ID. Returns None if not found.

    last_insert_rowid

    fn last_insert_rowid(db : SqliteDb) -> Int

    list_sessions

    fn list_sessions(sdb : SessionDb, limit? : Int, offset? : Int, source? : String) -> (Array[SessionRow], Int)
    List sessions with optional source filter, limit, and offset. Returns (rows, total).

    max_session_chars

    let max_session_chars : Int

    open_db

    fn open_db(path : String) -> SqliteDb

    open_session_db

    fn open_session_db(db_path : String) -> SessionDb

    open_session_db_opts

    fn open_session_db_opts(db_path : String, checkpoint_interval? : Int) -> SessionDb

    parse_frontmatter

    fn parse_frontmatter(content : String) -> SkillFrontmatter
    Parse SKILL.md frontmatter from the full file content.

    pb_find_context_file

    fn pb_find_context_file(cwd : String) -> String

    pb_load_soul_md

    fn pb_load_soul_md(hermes_home : String) -> String

    pb_truncate_content

    fn pb_truncate_content(content : String) -> String

    prepare

    fn prepare(db : SqliteDb, sql : String) -> SqliteStmt

    regex_spike

    fn regex_spike() -> Bool

    regex_spike_regex

    fn regex_spike_regex() -> Bool

    register_api

    fn register_api() -> Unit

    render_block

    fn render_block(target : String, entries : Array[String], max_chars : Int) -> String
    Render a memory block as a formatted string. Returns empty string if entries is empty.

    resolve_session_id

    fn resolve_session_id(sdb : SessionDb, prefix : String) -> Result[String, String]
    surface the ambiguous message.

    rows_length

    fn rows_length(rows : JsRows) -> Int

    sanitize_fts5_query

    fn sanitize_fts5_query(q : String) -> String
    Sanitize a user-supplied string for safe use in FTS5 MATCH expressions. Returns an empty string when input is blank — caller must treat that as "no search" and return an empty result set.

    Pure MoonBit implementation — cross-target. Tokenize on any ASCII whitespace (space, tab, newline, carriage return, form feed, vertical tab), wrap each non-empty token in double quotes, and double any inner " to escape it.

    scan_content

    fn scan_content(content : String) -> ScanResult
    Scan content for security issues. Returns ScanOk if safe, ScanBlocked(reason) if a pattern matches.

    search_messages

    fn search_messages(sdb : SessionDb, query : String, limit? : Int, role_filter? : String?) -> Array[FtsResult]

    async fn session_search(sdb : SessionDb, query : String, limit? : Int, role_filter? : String?) -> SessionSearchResponse

    skills_store_build_prompt

    fn skills_store_build_prompt(store : SkillsStore, available_tools : Array[String]?, platform_hint : String?, disabled? : Array[String]) -> String

    skills_store_list

    fn skills_store_list(store : SkillsStore, available_tools : Array[String]?, platform_hint : String?, available_toolsets? : Array[String]?, disabled? : Array[String]) -> Array[SkillEntry]

    skills_store_load

    fn skills_store_load(skills_dir : String, external_dirs : Array[String]) -> SkillsStore

    skills_store_view

    fn skills_store_view(store : SkillsStore, name : String) -> ViewResult

    sleep_ms

    async fn sleep_ms(ms : Int) -> Unit

    Cooperative sleep: yields to the event loop for approximately ms ms.

    spike_sqlite

    fn spike_sqlite() -> Unit

    stmt_all

    fn stmt_all(stmt : SqliteStmt) -> JsRows

    stmt_bind_int

    fn stmt_bind_int(stmt : SqliteStmt, i : Int, val : Int) -> Unit

    stmt_bind_int64

    fn stmt_bind_int64(stmt : SqliteStmt, i : Int, val : Int64) -> Unit

    stmt_bind_null

    fn stmt_bind_null(stmt : SqliteStmt, i : Int) -> Unit

    stmt_bind_text

    fn stmt_bind_text(stmt : SqliteStmt, i : Int, val : String) -> Unit

    stmt_finalize

    fn stmt_finalize(stmt : SqliteStmt) -> Unit

    stmt_get_int

    fn stmt_get_int(rows : JsRows, idx : Int, col : String) -> Int

    stmt_get_int64

    fn stmt_get_int64(rows : JsRows, idx : Int, col : String) -> Int64

    stmt_get_int64_opt

    fn stmt_get_int64_opt(rows : JsRows, idx : Int, col : String) -> Int64?

    stmt_get_int_opt

    fn stmt_get_int_opt(rows : JsRows, idx : Int, col : String) -> Int?

    stmt_get_text

    fn stmt_get_text(rows : JsRows, idx : Int, col : String) -> String

    stmt_get_text_opt

    fn stmt_get_text_opt(rows : JsRows, idx : Int, col : String) -> String?

    stmt_run

    fn stmt_run(stmt : SqliteStmt) -> Unit

    tool_memory_add

    async fn tool_memory_add(json_args : String) -> String

    tool_memory_read

    fn tool_memory_read(json_args : String) -> String

    tool_memory_remove

    async fn tool_memory_remove(json_args : String) -> String

    tool_memory_replace

    async fn tool_memory_replace(json_args : String) -> String

    tool_prompt_build

    fn tool_prompt_build(json_args : String) -> String

    tool_session_append

    async fn tool_session_append(json_args : String) -> String

    tool_session_create

    async fn tool_session_create(json_args : String) -> String

    tool_session_get

    fn tool_session_get(json_args : String) -> String

    tool_session_list

    fn tool_session_list(json_args : String) -> String

    tool_session_resolve

    fn tool_session_resolve(json_args : String) -> String

    async fn tool_session_search(json_args : String) -> String

    tool_skill_view

    fn tool_skill_view(json_args : String) -> String

    tool_skills_list

    fn tool_skills_list(json_args : String) -> String

    truncate_around_matches

    fn truncate_around_matches(full_text : String, query : String, max_chars? : Int) -> String

    tx_begin_immediate

    fn tx_begin_immediate(db : SqliteDb) -> Unit

    tx_commit

    fn tx_commit(db : SqliteDb) -> Unit

    tx_rollback

    fn tx_rollback(db : SqliteDb) -> Unit

    wal_checkpoint_every_n_writes

    let wal_checkpoint_every_n_writes : Int

    with_lock

    async fn[T] with_lock(path : String, f : async () -> T) -> T
    MoonBit test scheduler sometimes mishandles on repeated runs.

    with_write_tx

    async fn with_write_tx(sdb : SessionDb, f : () -> Unit) -> Unit