GPU_GUARD_MONOREPO/docs/superpowers/plans/2026-04-12-agent-memory-system.md
2026-05-20 21:39:12 +08:00

27 KiB
Raw Blame History

Agent 长期记忆系统 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 为 NetaClaw Agent 添加跨会话长期记忆,支持 MySQL FULLTEXT 和 SQLite FTS5 双后端Agent 粒度可配。

Architecture: Provider 抽象层 + 双后端实现。MemoryProvider 接口统一 save/search/delete 操作MysqlMemoryProvider 用 FULLTEXT ngramSqliteMemoryProvider 用 FTS5 trigram。工厂函数根据 Agent 配置选择后端。memory_save / memory_recall 两个工具注入 Agent 工具列表prefetch 在每轮对话前检索相关记忆注入 system prompt。

Tech Stack: TypeScript, TypeORM (MySQL), better-sqlite3 (SQLite FTS5), @sinclair/typebox (tool schema)

Spec: docs/superpowers/specs/2026-04-12-agent-memory-system-design.md


File Structure

src/modules/netaclaw/
├── memory/
│   ├── provider.ts           # MemoryProvider 接口 + MemoryEntry 类型 + AgentMemoryConfig
│   ├── factory.ts            # createMemoryProvider 工厂函数
│   ├── mysql_provider.ts     # MysqlMemoryProvider
│   ├── sqlite_provider.ts    # SqliteMemoryProvider
│   └── prefetch.ts           # prefetchMemory() + formatMemoryContext()
├── entity/
│   └── memory.ts             # NetaClawMemoryEntity (新增)
├── tools/builtin/
│   └── memory.ts             # memory_save + memory_recall 工具

Modified files:

  • src/modules/netaclaw/runtime/agent.ts — AgentRunParams 新增 memoryContext消息注入
  • src/modules/netaclaw/controller/chat.ts — prefetch 调用 + memory 工具注入
  • src/entities.ts — 注册 NetaClawMemoryEntity

Task 1: Install dependencies + Create MemoryEntry types and MemoryProvider interface

Files:

  • Create: src/modules/netaclaw/memory/provider.ts

  • Step 1: Install better-sqlite3

cd packages/backend && pnpm add better-sqlite3 && pnpm add -D @types/better-sqlite3
  • Step 2: Create provider.ts with types and interface

Create src/modules/netaclaw/memory/provider.ts:

/**
 * 长期记忆 Provider 抽象层
 */

export type MemoryType = 'user' | 'project' | 'feedback' | 'reference';

export interface MemoryEntry {
  id: number;
  agentName: string;
  userId: string;
  type: MemoryType;
  name: string;
  content: string;
  description: string;
  metadata?: Record<string, unknown>;
  createdAt: Date;
  updatedAt: Date;
}

export interface MemorySearchOpts {
  agentName: string;
  userId: string;
  type?: MemoryType;
  limit?: number;
}

export interface AgentMemoryConfig {
  enabled: boolean;
  backend: 'mysql' | 'sqlite';
  sqlitePath?: string;
  prefetchLimit?: number;
}

export interface MemoryProvider {
  save(entry: Omit<MemoryEntry, 'id' | 'createdAt' | 'updatedAt'>): Promise<MemoryEntry>;
  update(id: number, partial: Partial<Pick<MemoryEntry, 'name' | 'content' | 'description' | 'type' | 'metadata'>>): Promise<MemoryEntry>;
  delete(id: number): Promise<void>;
  search(query: string, opts: MemorySearchOpts): Promise<MemoryEntry[]>;
  list(opts: MemorySearchOpts): Promise<MemoryEntry[]>;
  getById(id: number): Promise<MemoryEntry | null>;
  close?(): Promise<void>;
}
  • Step 3: Verify no TypeScript errors
cd packages/backend && npx tsc --noEmit src/modules/netaclaw/memory/provider.ts
  • Step 4: Commit
git add src/modules/netaclaw/memory/provider.ts package.json pnpm-lock.yaml
git commit -m "feat(memory): add MemoryProvider interface and types + install better-sqlite3"

Task 2: Create NetaClawMemoryEntity for MySQL

Files:

  • Create: src/modules/netaclaw/entity/memory.ts

  • Modify: src/entities.ts

  • Step 1: Create the entity file

Create src/modules/netaclaw/entity/memory.ts:

import { BaseEntity } from '../../base/entity/base.js';
import { Column, Entity, Index } from 'typeorm';

/**
 * NetaClaw 长期记忆
 */
@Entity('netaclaw_memory')
export class NetaClawMemoryEntity extends BaseEntity {
  @Index()
  @Column({ comment: 'Agent名称', length: 100 })
  agentName: string;

  @Index()
  @Column({ comment: '用户ID', length: 100 })
  userId: string;

  @Index()
  @Column({ comment: '记忆类型: user/project/feedback/reference', length: 20 })
  type: string;

  @Column({ comment: '记忆标题', length: 255 })
  name: string;

  @Column({ type: 'text', comment: '记忆正文' })
  content: string;

  @Column({ comment: '一行描述', length: 500, default: '' })
  description: string;

  @Column({ type: 'json', comment: '元数据', nullable: true })
  metadata: Record<string, unknown>;
}

Note: FULLTEXT 索引需要通过 migration 或手动 SQL 添加TypeORM 不直接支持 WITH PARSER ngram。在 Step 3 中处理。

  • Step 2: Register entity in src/entities.ts

src/entities.ts 中,在 import * as entity29 from './modules/netaclaw/entity/model_channel'; 之后添加:

import * as entity30 from './modules/netaclaw/entity/memory';

entities 数组末尾(] 之前)添加:

  ...Object.values(entity30),
  • Step 3: Create SQL migration for FULLTEXT index

Create src/modules/netaclaw/memory/migration.sql (reference file, run manually or via init):

-- 在 TypeORM 自动建表后执行,添加 ngram FULLTEXT 索引
ALTER TABLE netaclaw_memory
  ADD FULLTEXT INDEX ft_content (name, content, description) WITH PARSER ngram;
  • Step 4: Commit
git add src/modules/netaclaw/entity/memory.ts src/entities.ts src/modules/netaclaw/memory/migration.sql
git commit -m "feat(memory): add NetaClawMemoryEntity with FULLTEXT index migration"

Task 3: Implement MysqlMemoryProvider

Files:

  • Create: src/modules/netaclaw/memory/mysql_provider.ts

  • Step 1: Implement MysqlMemoryProvider

Create src/modules/netaclaw/memory/mysql_provider.ts:

import { Repository } from 'typeorm';
import { NetaClawMemoryEntity } from '../entity/memory.js';
import { MemoryProvider, MemoryEntry, MemorySearchOpts } from './provider.js';

function toEntry(e: NetaClawMemoryEntity): MemoryEntry {
  return {
    id: e.id,
    agentName: e.agentName,
    userId: e.userId,
    type: e.type as MemoryEntry['type'],
    name: e.name,
    content: e.content,
    description: e.description,
    metadata: e.metadata,
    createdAt: e.createTime,
    updatedAt: e.updateTime,
  };
}

export class MysqlMemoryProvider implements MemoryProvider {
  constructor(private repo: Repository<NetaClawMemoryEntity>) {}

  async save(entry: Omit<MemoryEntry, 'id' | 'createdAt' | 'updatedAt'>): Promise<MemoryEntry> {
    const saved = await this.repo.save({
      agentName: entry.agentName,
      userId: entry.userId,
      type: entry.type,
      name: entry.name,
      content: entry.content,
      description: entry.description ?? '',
      metadata: entry.metadata,
    });
    return toEntry(saved);
  }

  async update(id: number, partial: Partial<Pick<MemoryEntry, 'name' | 'content' | 'description' | 'type' | 'metadata'>>): Promise<MemoryEntry> {
    await this.repo.update(id, partial);
    const updated = await this.repo.findOneByOrFail({ id });
    return toEntry(updated);
  }

  async delete(id: number): Promise<void> {
    await this.repo.delete(id);
  }

  async search(query: string, opts: MemorySearchOpts): Promise<MemoryEntry[]> {
    const limit = opts.limit ?? 10;
    let qb = this.repo.createQueryBuilder('m')
      .where('m.agentName = :agentName', { agentName: opts.agentName })
      .andWhere('m.userId = :userId', { userId: opts.userId })
      .andWhere(`MATCH(m.name, m.content, m.description) AGAINST(:query IN BOOLEAN MODE)`, { query })
      .orderBy(`MATCH(m.name, m.content, m.description) AGAINST(:query IN BOOLEAN MODE)`, 'DESC')
      .limit(limit);

    if (opts.type) {
      qb = qb.andWhere('m.type = :type', { type: opts.type });
    }

    const results = await qb.getMany();
    return results.map(toEntry);
  }

  async list(opts: MemorySearchOpts): Promise<MemoryEntry[]> {
    const where: any = { agentName: opts.agentName, userId: opts.userId };
    if (opts.type) where.type = opts.type;
    const results = await this.repo.find({
      where,
      order: { updateTime: 'DESC' },
      take: opts.limit ?? 10,
    });
    return results.map(toEntry);
  }

  async getById(id: number): Promise<MemoryEntry | null> {
    const e = await this.repo.findOneBy({ id });
    return e ? toEntry(e) : null;
  }
}
  • Step 2: Verify no TypeScript errors
cd packages/backend && npx tsc --noEmit
  • Step 3: Commit
git add src/modules/netaclaw/memory/mysql_provider.ts
git commit -m "feat(memory): implement MysqlMemoryProvider with FULLTEXT search"

Task 4: Implement SqliteMemoryProvider

Files:

  • Create: src/modules/netaclaw/memory/sqlite_provider.ts

  • Step 1: Implement SqliteMemoryProvider

Create src/modules/netaclaw/memory/sqlite_provider.ts:

import Database from 'better-sqlite3';
import * as fs from 'fs';
import * as path from 'path';
import { MemoryProvider, MemoryEntry, MemorySearchOpts } from './provider.js';

const INIT_SQL = `
CREATE TABLE IF NOT EXISTS memory (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  agent_name TEXT NOT NULL,
  user_id TEXT NOT NULL,
  type TEXT NOT NULL CHECK(type IN ('user', 'project', 'feedback', 'reference')),
  name TEXT NOT NULL,
  content TEXT NOT NULL,
  description TEXT NOT NULL DEFAULT '',
  metadata TEXT,
  created_at TEXT DEFAULT (datetime('now')),
  updated_at TEXT DEFAULT (datetime('now'))
);

CREATE INDEX IF NOT EXISTS idx_agent_user ON memory(agent_name, user_id);
`;

const FTS_SQL = `
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
  name, content, description,
  content='memory', content_rowid='id',
  tokenize='trigram'
);
`;

const TRIGGER_SQL = `
CREATE TRIGGER IF NOT EXISTS memory_ai AFTER INSERT ON memory BEGIN
  INSERT INTO memory_fts(rowid, name, content, description)
  VALUES (new.id, new.name, new.content, new.description);
END;

CREATE TRIGGER IF NOT EXISTS memory_ad AFTER DELETE ON memory BEGIN
  INSERT INTO memory_fts(memory_fts, rowid, name, content, description)
  VALUES ('delete', old.id, old.name, old.content, old.description);
END;

CREATE TRIGGER IF NOT EXISTS memory_au AFTER UPDATE ON memory BEGIN
  INSERT INTO memory_fts(memory_fts, rowid, name, content, description)
  VALUES ('delete', old.id, old.name, old.content, old.description);
  INSERT INTO memory_fts(rowid, name, content, description)
  VALUES (new.id, new.name, new.content, new.description);
END;
`;

function toEntry(row: any): MemoryEntry {
  return {
    id: row.id,
    agentName: row.agent_name,
    userId: row.user_id,
    type: row.type,
    name: row.name,
    content: row.content,
    description: row.description,
    metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
    createdAt: new Date(row.created_at),
    updatedAt: new Date(row.updated_at),
  };
}

export class SqliteMemoryProvider implements MemoryProvider {
  private db: Database.Database;

  constructor(dbPath?: string) {
    const resolvedPath = dbPath ?? path.resolve(process.cwd(), 'data/memory/memory.db');
    fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
    this.db = new Database(resolvedPath);
    this.db.pragma('journal_mode = WAL');
    this.db.exec(INIT_SQL);
    this.db.exec(FTS_SQL);
    this.db.exec(TRIGGER_SQL);
  }

  async save(entry: Omit<MemoryEntry, 'id' | 'createdAt' | 'updatedAt'>): Promise<MemoryEntry> {
    const stmt = this.db.prepare(`
      INSERT INTO memory (agent_name, user_id, type, name, content, description, metadata)
      VALUES (?, ?, ?, ?, ?, ?, ?)
    `);
    const info = stmt.run(
      entry.agentName, entry.userId, entry.type,
      entry.name, entry.content, entry.description ?? '',
      entry.metadata ? JSON.stringify(entry.metadata) : null,
    );
    return (await this.getById(info.lastInsertRowid as number))!;
  }

  async update(id: number, partial: Partial<Pick<MemoryEntry, 'name' | 'content' | 'description' | 'type' | 'metadata'>>): Promise<MemoryEntry> {
    const sets: string[] = [];
    const values: any[] = [];
    if (partial.name !== undefined) { sets.push('name = ?'); values.push(partial.name); }
    if (partial.content !== undefined) { sets.push('content = ?'); values.push(partial.content); }
    if (partial.description !== undefined) { sets.push('description = ?'); values.push(partial.description); }
    if (partial.type !== undefined) { sets.push('type = ?'); values.push(partial.type); }
    if (partial.metadata !== undefined) { sets.push('metadata = ?'); values.push(JSON.stringify(partial.metadata)); }
    sets.push("updated_at = datetime('now')");
    values.push(id);
    this.db.prepare(`UPDATE memory SET ${sets.join(', ')} WHERE id = ?`).run(...values);
    return (await this.getById(id))!;
  }

  async delete(id: number): Promise<void> {
    this.db.prepare('DELETE FROM memory WHERE id = ?').run(id);
  }

  async search(query: string, opts: MemorySearchOpts): Promise<MemoryEntry[]> {
    const limit = opts.limit ?? 10;
    // FTS5 trigram tokenizer: wrap query for substring match
    const ftsQuery = `"${query.replace(/"/g, '""')}"`;
    let sql = `
      SELECT m.* FROM memory m
      JOIN memory_fts f ON m.id = f.rowid
      WHERE memory_fts MATCH ?
        AND m.agent_name = ?
        AND m.user_id = ?
    `;
    const params: any[] = [ftsQuery, opts.agentName, opts.userId];
    if (opts.type) { sql += ' AND m.type = ?'; params.push(opts.type); }
    sql += ' ORDER BY rank LIMIT ?';
    params.push(limit);
    const rows = this.db.prepare(sql).all(...params);
    return rows.map(toEntry);
  }

  async list(opts: MemorySearchOpts): Promise<MemoryEntry[]> {
    let sql = 'SELECT * FROM memory WHERE agent_name = ? AND user_id = ?';
    const params: any[] = [opts.agentName, opts.userId];
    if (opts.type) { sql += ' AND type = ?'; params.push(opts.type); }
    sql += ' ORDER BY updated_at DESC LIMIT ?';
    params.push(opts.limit ?? 10);
    const rows = this.db.prepare(sql).all(...params);
    return rows.map(toEntry);
  }

  async getById(id: number): Promise<MemoryEntry | null> {
    const row = this.db.prepare('SELECT * FROM memory WHERE id = ?').get(id);
    return row ? toEntry(row) : null;
  }

  async close(): Promise<void> {
    this.db.close();
  }
}
  • Step 2: Verify no TypeScript errors
cd packages/backend && npx tsc --noEmit
  • Step 3: Commit
git add src/modules/netaclaw/memory/sqlite_provider.ts
git commit -m "feat(memory): implement SqliteMemoryProvider with FTS5 trigram search"

Task 5: Create factory function

Files:

  • Create: src/modules/netaclaw/memory/factory.ts

  • Step 1: Implement createMemoryProvider

Create src/modules/netaclaw/memory/factory.ts:

import { Repository } from 'typeorm';
import { NetaClawMemoryEntity } from '../entity/memory.js';
import { MemoryProvider, AgentMemoryConfig } from './provider.js';
import { MysqlMemoryProvider } from './mysql_provider.js';
import { SqliteMemoryProvider } from './sqlite_provider.js';

export function createMemoryProvider(
  config: AgentMemoryConfig,
  mysqlRepo?: Repository<NetaClawMemoryEntity>,
): MemoryProvider {
  if (config.backend === 'sqlite') {
    return new SqliteMemoryProvider(config.sqlitePath);
  }
  if (!mysqlRepo) {
    throw new Error('MysqlMemoryProvider requires a TypeORM repository');
  }
  return new MysqlMemoryProvider(mysqlRepo);
}
  • Step 2: Commit
git add src/modules/netaclaw/memory/factory.ts
git commit -m "feat(memory): add createMemoryProvider factory"

Task 6: Create prefetch module

Files:

  • Create: src/modules/netaclaw/memory/prefetch.ts

  • Step 1: Implement prefetchMemory and formatMemoryContext

Create src/modules/netaclaw/memory/prefetch.ts:

import { MemoryProvider, MemoryEntry, MemorySearchOpts } from './provider.js';

export function formatMemoryContext(entries: MemoryEntry[]): string {
  if (entries.length === 0) return '';
  const lines = entries.map(e => `[${e.type}] ${e.name}\n${e.content}`);
  return `以下是与当前对话可能相关的长期记忆:\n\n${lines.join('\n\n')}`;
}

export async function prefetchMemory(
  provider: MemoryProvider,
  userMessage: string,
  agentName: string,
  userId: string,
  limit = 5,
): Promise<string> {
  const opts: MemorySearchOpts = { agentName, userId, limit };
  let entries: MemoryEntry[];
  try {
    entries = await provider.search(userMessage, opts);
  } catch {
    // search 失败时 fallback 到 list例如 query 为空或 FTS 语法错误)
    entries = await provider.list(opts);
  }
  return formatMemoryContext(entries);
}
  • Step 2: Commit
git add src/modules/netaclaw/memory/prefetch.ts
git commit -m "feat(memory): add prefetchMemory with formatted context output"

Task 7: Create memory tools (memory_save + memory_recall)

Files:

  • Create: src/modules/netaclaw/tools/builtin/memory.ts

  • Step 1: Implement memory tools

Create src/modules/netaclaw/tools/builtin/memory.ts:

import { Type, Static } from '@sinclair/typebox';
import { AnyAgentTool } from '../common.js';
import { MemoryProvider, MemoryType } from '../../memory/provider.js';

const MemoryTypeSchema = Type.Union([
  Type.Literal('user'),
  Type.Literal('project'),
  Type.Literal('feedback'),
  Type.Literal('reference'),
]);

// --- memory_save ---

const memorySaveParams = Type.Object({
  action: Type.Union([Type.Literal('create'), Type.Literal('update'), Type.Literal('delete')]),
  name: Type.String({ description: '记忆标题' }),
  type: MemoryTypeSchema,
  content: Type.Optional(Type.String({ description: '记忆正文' })),
  description: Type.Optional(Type.String({ description: '一行描述' })),
  id: Type.Optional(Type.Number({ description: '更新/删除时的记忆 ID' })),
});

type MemorySaveParams = Static<typeof memorySaveParams>;

export function createMemorySaveTool(
  provider: MemoryProvider,
  agentName: string,
  userId: string,
): AnyAgentTool {
  return {
    name: 'memory_save',
    label: '保存记忆',
    description: '存储、更新或删除长期记忆。记忆会在未来对话中自动召回。',
    parameters: memorySaveParams,
    async execute(_id: string, params: MemorySaveParams): Promise<string> {
      if (params.action === 'create') {
        const entry = await provider.save({
          agentName,
          userId,
          type: params.type as MemoryType,
          name: params.name,
          content: params.content ?? '',
          description: params.description ?? '',
        });
        return `记忆已保存 (id=${entry.id}): ${entry.name}`;
      }
      if (params.action === 'update') {
        if (!params.id) return '错误: 更新操作需要提供 id';
        const entry = await provider.update(params.id, {
          name: params.name,
          type: params.type as MemoryType,
          content: params.content,
          description: params.description,
        });
        return `记忆已更新 (id=${entry.id}): ${entry.name}`;
      }
      if (params.action === 'delete') {
        if (!params.id) return '错误: 删除操作需要提供 id';
        await provider.delete(params.id);
        return `记忆已删除 (id=${params.id})`;
      }
      return '错误: 未知操作';
    },
  };
}

// --- memory_recall ---

const memoryRecallParams = Type.Object({
  query: Type.String({ description: '搜索关键词' }),
  type: Type.Optional(MemoryTypeSchema),
  limit: Type.Optional(Type.Number({ description: '返回条数,默认 5', default: 5 })),
});

type MemoryRecallParams = Static<typeof memoryRecallParams>;

export function createMemoryRecallTool(
  provider: MemoryProvider,
  agentName: string,
  userId: string,
): AnyAgentTool {
  return {
    name: 'memory_recall',
    label: '检索记忆',
    description: '搜索长期记忆中的相关信息。',
    parameters: memoryRecallParams,
    async execute(_id: string, params: MemoryRecallParams): Promise<string> {
      const entries = await provider.search(params.query, {
        agentName,
        userId,
        type: params.type as MemoryType | undefined,
        limit: params.limit ?? 5,
      });
      if (entries.length === 0) return '未找到相关记忆。';
      return entries.map(e => `[${e.type}] (id=${e.id}) ${e.name}\n${e.content}`).join('\n\n');
    },
  };
}
  • Step 2: Verify no TypeScript errors
cd packages/backend && npx tsc --noEmit
  • Step 3: Commit
git add src/modules/netaclaw/tools/builtin/memory.ts
git commit -m "feat(memory): add memory_save and memory_recall agent tools"

Task 8: Modify runtime/agent.ts to support memoryContext injection

Files:

  • Modify: src/modules/netaclaw/runtime/agent.ts

  • Step 1: Add memoryContext to AgentRunParams and inject into messages

In src/modules/netaclaw/runtime/agent.ts:

Add memoryContext?: string; to the AgentRunParams interface (after history?: LLMMessage[];):

export interface AgentRunParams {
  agentConfig: AgentConfig;
  tools: AnyAgentTool[];
  userMessage: string;
  history?: LLMMessage[];
  memoryContext?: string;
  onToken?: (text: string) => void;
  onThinking?: (text: string) => void;
  onToolCall?: (name: string, args: Record<string, unknown>) => void;
  onToolResult?: (name: string, result: string) => void;
}

Update the runAgent function destructuring to include memoryContext:

const { agentConfig, tools, userMessage, history = [], memoryContext,
  onToken, onThinking, onToolCall, onToolResult } = params;

Update the messages array — append memoryContext into the single system prompt (Anthropic provider only reads the first system message, so a second system message would be silently dropped):

const systemContent = memoryContext
  ? `${agentConfig.systemPrompt}\n\n<memory-context>\n${memoryContext}\n</memory-context>`
  : agentConfig.systemPrompt;

const messages: LLMMessage[] = [
  { role: 'system', content: systemContent },
  ...history,
  { role: 'user', content: userMessage },
];
  • Step 2: Verify no TypeScript errors
cd packages/backend && npx tsc --noEmit
  • Step 3: Commit
git add src/modules/netaclaw/runtime/agent.ts
git commit -m "feat(memory): inject memoryContext into agent message assembly"

Task 9: Integrate memory into controller/chat.ts

Files:

  • Modify: src/modules/netaclaw/controller/chat.ts

  • Step 1: Add imports, inject repo, wire up prefetch + tools

In src/modules/netaclaw/controller/chat.ts, add imports at the top:

import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { NetaClawMemoryEntity } from '../entity/memory.js';
import { NetaClawAgentService } from '../service/agent.js';
import { AnyAgentTool } from '../tools/common.js';
import { AgentMemoryConfig } from '../memory/provider.js';
import { createMemoryProvider } from '../memory/factory.js';
import { prefetchMemory } from '../memory/prefetch.js';
import { createMemorySaveTool, createMemoryRecallTool } from '../tools/builtin/memory.js';

Add the repository and service injections inside the class (after skillLoader):

@InjectEntityModel(NetaClawMemoryEntity)
memoryRepo: Repository<NetaClawMemoryEntity>;

@Inject()
agentService: NetaClawAgentService;

Update the chat method. First, add userId to the body type:

async chat(@Body() body: { sessionId?: string; message: string; agentName?: string; userId?: string }) {

After const agentName = ... (line 30), load the agent entity from DB:

const agentEntity = await this.agentService.agentRepo.findOneBy({ name: agentName });

Then after const systemPrompt = ... and before const agentConfig = ..., add memory logic:

// --- 记忆系统 ---
const memoryConfig: AgentMemoryConfig | undefined =
  (agentEntity?.config as any)?.memory as AgentMemoryConfig | undefined;

let memoryContext: string | undefined;
let memoryTools: AnyAgentTool[] = [];

if (memoryConfig?.enabled) {
  const provider = createMemoryProvider(memoryConfig, this.memoryRepo);
  const userId = body.userId ?? 'anonymous';

  memoryContext = await prefetchMemory(provider, body.message, agentName, userId, memoryConfig.prefetchLimit);
  memoryTools = [
    createMemorySaveTool(provider, agentName, userId),
    createMemoryRecallTool(provider, agentName, userId),
  ];
}

Update the systemPrompt to append memory instructions when enabled:

let finalSystemPrompt = systemPrompt;
if (memoryConfig?.enabled) {
  finalSystemPrompt += MEMORY_SYSTEM_PROMPT;
}

Add the constant at the top of the file (after imports):

const MEMORY_SYSTEM_PROMPT = `

## 记忆系统
你拥有长期记忆能力。使用 memory_save 工具存储重要信息,使用 memory_recall 工具检索过往记忆。

记忆类型:
- user: 用户画像(偏好、角色、习惯)
- project: 项目知识(进展、决策、约束)
- feedback: 行为反馈(用户对你行为的纠正或确认)
- reference: 引用(外部资源链接、文档地址)

存储原则:
- 当用户透露个人偏好、角色、习惯时,存为 user 类型
- 当了解到项目进展、决策、约束时,存为 project 类型
- 当用户纠正或确认你的行为时,存为 feedback 类型
- 当提到外部资源链接时,存为 reference 类型
- 更新已有记忆而非创建重复条目
- 只存储对未来对话有价值的信息`;

Update the agentConfig to use finalSystemPrompt:

const agentConfig: AgentConfig = {
  name: agentName,
  systemPrompt: finalSystemPrompt,
  // ... rest unchanged
};

Update the tools array to include memory tools:

const tools = [bashTool, readFileTool, writeFileTool, listDirTool, ...memoryTools];

Update the runAgent call to pass memoryContext:

const result = await runAgent({
  agentConfig,
  tools,
  userMessage: body.message,
  history: history.slice(0, -1),
  memoryContext,
});
  • Step 2: Verify no TypeScript errors
cd packages/backend && npx tsc --noEmit
  • Step 3: Commit
git add src/modules/netaclaw/controller/chat.ts
git commit -m "feat(memory): integrate memory prefetch + tools into chat controller"

Task 10: Final verification

  • Step 1: Verify full TypeScript compilation
cd packages/backend && npx tsc --noEmit

Expected: No errors.

  • Step 2: Verify all new files exist
ls -la src/modules/netaclaw/memory/
ls -la src/modules/netaclaw/tools/builtin/memory.ts
ls -la src/modules/netaclaw/entity/memory.ts

Expected: provider.ts, factory.ts, mysql_provider.ts, sqlite_provider.ts, prefetch.ts, migration.sql in memory/; memory.ts in tools/builtin/; memory.ts in entity/.

  • Step 3: Commit all remaining changes
git add -A && git status
git commit -m "feat(memory): complete long-term memory system implementation"