GPU_GUARD_MONOREPO/docs/superpowers/plans/2026-04-19-neta-agent-runtime-kernel.md
2026-05-20 21:39:12 +08:00

31 KiB
Raw Blame History

Neta Agent 运行时内核实施计划

给自动化实施代理: 必须使用子技能:superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans,按任务逐项实施本计划。步骤使用复选框(- [ ])语法跟踪进度。

目标: 建立 Pi-first 树状会话内核,包括 Pi 兼容 session entry tree、SessionTreeProvider 抽象、默认 file provider、MySQL provider、leaf 驱动上下文构建、snapshot 和 agent 级 session provider 选择。

架构: 第一原则是“先复刻 Pi 的 session 语义,再适配 Neta provider”。File provider 必须采用 Pi 单文件 JSONL session 协议:第一行 session header后续每行 session entryentry 通过 id/parentId 组成树;leafId 只表示当前位置;历史 entry 原则上 append-only。MySQL provider 必须复刻同一语义,不能反过来要求 file provider 适配数据库表思维。

技术栈: Midway/NestJS 风格 service、TypeScript、TypeORM、MySQL、Node fs JSONL、Jest、pnpm。


架构硬约束

本计划实施时必须遵守以下约束:

  • 不兼容旧线性 message/session 接口。
  • 旧历史数据允许删除,不做迁移脚本。
  • 数据库结构修改实施阶段使用 MCP 直接执行,不新增 SQL 文件作为主路径。
  • filemysql provider 必须通过同一组契约测试。
  • file provider 必须高强度移植 Pi 的 SessionManager 文件协议和纯逻辑。
  • 不得把 file provider 设计成 session.json + entries.jsonl 双文件。
  • 不得把第一版事实源拆成独立 tool_call/tool_result entrytool 状态作为 AgentMessage payload 或前端流式投影事件处理。
  • 不得只用 content/summary 字段表示消息;必须保留完整 message payload。
  • completed entry 默认不可修改;updateEntry() 只允许用于 in-flight 流式补写或明确的 patch 事件。

Pi 直接复用范围

优先从以下 Pi 文件移植:

  • C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/coding-agent/src/core/session-manager.ts
  • C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/coding-agent/src/core/compaction/compaction.ts
  • C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/coding-agent/src/core/compaction/branch-summarization.ts
  • C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/coding-agent/src/core/compaction/utils.ts

必须移植或等价实现:

  • SessionHeader / SessionEntry union。
  • 单文件 JSONL session 协议。
  • parseSessionEntries()loadEntriesFromFile() 的容错解析。
  • leaf-to-root path 构建。
  • buildSessionContext() 的 compaction、branch summary、custom message、thinking/model setting 处理。
  • branch()resetLeaf()branchWithSummary()createBranchedSession() 的纯逻辑。
  • compaction 的 firstKeptEntryId、split turn、previous summary 合并策略。

文件结构

新增后端内核模块:

  • packages/backend/src/modules/netaclaw/session-tree/types.ts
    • 定义 Pi 兼容 session header、entry union、provider DTO、snapshot DTO。
  • packages/backend/src/modules/netaclaw/session-tree/id.ts
    • 定义 session id 与 entry id 生成器。
  • packages/backend/src/modules/netaclaw/session-tree/path.ts
    • 定义 entry index、leaf path、tree、common ancestor、label 解析纯函数。
  • packages/backend/src/modules/netaclaw/session-tree/context_builder.ts
    • 移植 Pi buildSessionContext() 语义。
  • packages/backend/src/modules/netaclaw/session-tree/snapshot.ts
    • 构建前端/API snapshot payload。
  • packages/backend/src/modules/netaclaw/session-tree/provider.ts
    • 定义 SessionTreeProvider 契约。
  • packages/backend/src/modules/netaclaw/session-tree/pi_session_file.ts
    • 封装 Pi 单文件 JSONL session 文件读写与纯逻辑。
  • packages/backend/src/modules/netaclaw/session-tree/file_provider.ts
    • pi_session_file.ts 适配为 SessionTreeProvider
  • packages/backend/src/modules/netaclaw/session-tree/mysql_provider.ts
    • 将 MySQL 表适配为同一 SessionTreeProvider
  • packages/backend/src/modules/netaclaw/session-tree/provider_factory.ts
    • 解析 agent 配置中的 sessionProvider

新增 TypeORM 实体:

  • packages/backend/src/modules/netaclaw/entity/agent_session.ts
  • packages/backend/src/modules/netaclaw/entity/agent_session_entry.ts

修改现有文件:

  • packages/backend/src/modules/netaclaw/entity/agent.ts
  • packages/backend/src/entities.ts

新增测试:

  • packages/backend/test/session_tree_types.test.ts
  • packages/backend/test/session_tree_path.test.ts
  • packages/backend/test/session_tree_context_builder.test.ts
  • packages/backend/test/session_tree_file_provider.test.ts
  • packages/backend/test/session_tree_mysql_provider.test.ts
  • packages/backend/test/session_tree_provider_contract.test.ts
  • packages/backend/test/session_tree_provider_factory.test.ts
  • packages/backend/test/entity_exports.test.ts

任务 1定义 Pi 兼容会话类型

文件:

  • 新增:packages/backend/src/modules/netaclaw/session-tree/types.ts

  • 测试:packages/backend/test/session_tree_types.test.ts

  • 步骤 1编写失败的类型消费测试

import type {
  SessionTreeEntry,
  SessionTreeHeader,
  SessionTreeSession,
} from '../src/modules/netaclaw/session-tree/types.js';

describe('session tree types', () => {
  it('represents a Pi-compatible session header and entries', () => {
    const header: SessionTreeHeader = {
      type: 'session',
      version: 1,
      id: 's1',
      timestamp: '2026-04-19T00:00:00.000Z',
      cwd: 'C:/workspace',
    };

    const entry: SessionTreeEntry = {
      type: 'message',
      id: 'e1',
      parentId: null,
      timestamp: '2026-04-19T00:00:00.000Z',
      message: { role: 'user', content: 'hello' },
    };

    const session: SessionTreeSession = {
      sessionId: header.id,
      provider: 'file',
      rootEntryId: entry.id,
      leafEntryId: entry.id,
      cwd: header.cwd,
      status: 'active',
      createdAt: header.timestamp,
      updatedAt: header.timestamp,
    };

    expect(session.leafEntryId).toBe(entry.id);
  });
});
  • 步骤 2运行测试确认失败
pnpm --filter @neta/backend test -- --runInBand test/session_tree_types.test.ts

预期:

  • 失败,原因是 session-tree/types.js 尚不存在。

  • 步骤 3实现 types.ts

核心类型必须包含:

export type SessionTreeProviderKind = 'file' | 'mysql';

export interface SessionTreeHeader {
  type: 'session';
  version: number;
  id: string;
  timestamp: string;
  cwd: string;
  parentSession?: string;
}

export interface SessionTreeEntryBase {
  type: string;
  id: string;
  parentId: string | null;
  timestamp: string;
}

export interface SessionTreeMessage {
  role: string;
  content?: unknown;
  provider?: string;
  model?: string;
  usage?: Record<string, unknown>;
  stopReason?: string;
  timestamp?: number;
  [key: string]: unknown;
}

export interface SessionMessageEntry extends SessionTreeEntryBase {
  type: 'message';
  message: SessionTreeMessage;
}

export interface ThinkingLevelChangeEntry extends SessionTreeEntryBase {
  type: 'thinking_level_change';
  thinkingLevel: string;
}

export interface ModelChangeEntry extends SessionTreeEntryBase {
  type: 'model_change';
  provider: string;
  modelId: string;
}

export interface CompactionEntry<T = unknown> extends SessionTreeEntryBase {
  type: 'compaction';
  summary: string;
  firstKeptEntryId: string;
  tokensBefore: number;
  details?: T;
  fromHook?: boolean;
}

export interface BranchSummaryEntry<T = unknown> extends SessionTreeEntryBase {
  type: 'branch_summary';
  fromId: string;
  summary: string;
  details?: T;
  fromHook?: boolean;
}

export interface CustomEntry<T = unknown> extends SessionTreeEntryBase {
  type: 'custom';
  customType: string;
  data?: T;
}

export interface CustomMessageEntry<T = unknown> extends SessionTreeEntryBase {
  type: 'custom_message';
  customType: string;
  content: unknown;
  details?: T;
  display: boolean;
}

export interface LabelEntry extends SessionTreeEntryBase {
  type: 'label';
  targetId: string;
  label: string | undefined;
}

export interface SessionInfoEntry extends SessionTreeEntryBase {
  type: 'session_info';
  name?: string;
}

export type SessionTreeEntry =
  | SessionMessageEntry
  | ThinkingLevelChangeEntry
  | ModelChangeEntry
  | CompactionEntry
  | BranchSummaryEntry
  | CustomEntry
  | CustomMessageEntry
  | LabelEntry
  | SessionInfoEntry;

export interface SessionTreeSession {
  sessionId: string;
  provider: SessionTreeProviderKind;
  rootEntryId: string | null;
  leafEntryId: string | null;
  cwd?: string | null;
  sessionFile?: string | null;
  parentSessionId?: string | null;
  agentId?: number | null;
  userId?: string | null;
  title?: string | null;
  status: 'active' | 'archived' | 'deleted';
  metadata?: Record<string, unknown>;
  createdAt: string;
  updatedAt: string;
}
  • 步骤 4运行测试确认通过
pnpm --filter @neta/backend test -- --runInBand test/session_tree_types.test.ts

预期:

  • 通过。

  • 步骤 5提交变更

git add packages/backend/src/modules/netaclaw/session-tree/types.ts packages/backend/test/session_tree_types.test.ts
git commit -m "feat(agent-runtime): add pi-compatible session tree types"

任务 2实现 ID 与树路径纯函数

文件:

  • 新增:packages/backend/src/modules/netaclaw/session-tree/id.ts

  • 新增:packages/backend/src/modules/netaclaw/session-tree/path.ts

  • 测试:packages/backend/test/session_tree_path.test.ts

  • 步骤 1编写路径测试

import {
  buildEntryIndex,
  findCommonAncestorEntryId,
  getPathToLeaf,
  groupChildrenByParent,
  resolveLatestLabels,
} from '../src/modules/netaclaw/session-tree/path.js';
import type { SessionTreeEntry } from '../src/modules/netaclaw/session-tree/types.js';

const entry = (id: string, parentId: string | null): SessionTreeEntry => ({
  type: 'message',
  id,
  parentId,
  timestamp: `2026-04-19T00:00:0${id.length}.000Z`,
  message: { role: 'user', content: id },
});

describe('session tree path utilities', () => {
  it('builds root-to-leaf path', () => {
    expect(getPathToLeaf([entry('a', null), entry('b', 'a'), entry('c', 'b')], 'c').map(e => e.id)).toEqual([
      'a',
      'b',
      'c',
    ]);
  });

  it('rejects missing leaf and cycles', () => {
    expect(() => getPathToLeaf([entry('a', null)], 'missing')).toThrow('Entry missing not found');
    expect(() => getPathToLeaf([entry('a', 'c'), entry('b', 'a'), entry('c', 'b')], 'c')).toThrow('Cycle detected');
  });

  it('finds common ancestor', () => {
    const entries = [entry('a', null), entry('b', 'a'), entry('c', 'b'), entry('d', 'b')];
    expect(findCommonAncestorEntryId(entries, 'c', 'd')).toBe('b');
  });

  it('groups children and resolves latest labels', () => {
    const label1: SessionTreeEntry = {
      type: 'label',
      id: 'l1',
      parentId: 'a',
      timestamp: '2026-04-19T00:00:02.000Z',
      targetId: 'a',
      label: 'old',
    };
    const label2: SessionTreeEntry = {
      type: 'label',
      id: 'l2',
      parentId: 'l1',
      timestamp: '2026-04-19T00:00:03.000Z',
      targetId: 'a',
      label: 'new',
    };
    expect(groupChildrenByParent([entry('a', null), entry('b', 'a')]).__root__).toEqual(['a']);
    expect(resolveLatestLabels([entry('a', null), label1, label2]).get('a')?.label).toBe('new');
    expect(buildEntryIndex([entry('a', null)]).get('a')?.id).toBe('a');
  });
});
  • 步骤 2实现 id.tspath.ts

实现要求:

  • createSessionTreeSessionId() 使用 uuidv7() 或等价时间有序 UUID。

  • createSessionTreeEntryId(existingIds) 生成短 ID并检查碰撞。

  • getPathToLeaf() 必须检测 missing leaf、missing parent、cycle。

  • groupChildrenByParent() 必须以 append 顺序稳定排序。

  • resolveLatestLabels() 必须以后写 label 覆盖旧 label。

  • 步骤 3运行测试确认通过

pnpm --filter @neta/backend test -- --runInBand test/session_tree_path.test.ts

预期:

  • 通过。

  • 步骤 4提交变更

git add packages/backend/src/modules/netaclaw/session-tree/id.ts packages/backend/src/modules/netaclaw/session-tree/path.ts packages/backend/test/session_tree_path.test.ts
git commit -m "feat(agent-runtime): add session tree path utilities"

任务 3移植上下文构建语义

文件:

  • 新增:packages/backend/src/modules/netaclaw/session-tree/context_builder.ts

  • 测试:packages/backend/test/session_tree_context_builder.test.ts

  • 步骤 1编写上下文构建测试

import { buildSessionContext } from '../src/modules/netaclaw/session-tree/context_builder.js';
import type { SessionTreeEntry } from '../src/modules/netaclaw/session-tree/types.js';

const message = (id: string, parentId: string | null, role: string, content: string): SessionTreeEntry => ({
  type: 'message',
  id,
  parentId,
  timestamp: `2026-04-19T00:00:00.000Z`,
  message: { role, content },
});

describe('session tree context builder', () => {
  it('walks the active path and excludes sibling branches', () => {
    const entries = [
      message('u1', null, 'user', 'root'),
      message('a1', 'u1', 'assistant', 'branch a'),
      message('a2', 'u1', 'assistant', 'branch b'),
    ];
    expect(buildSessionContext(entries, 'a2').messages.map(m => m.content)).toEqual(['root', 'branch b']);
  });

  it('handles compaction using firstKeptEntryId', () => {
    const entries: SessionTreeEntry[] = [
      message('u1', null, 'user', 'old'),
      message('u2', 'u1', 'user', 'kept'),
      {
        type: 'compaction',
        id: 'c1',
        parentId: 'u2',
        timestamp: '2026-04-19T00:00:01.000Z',
        summary: 'summary',
        firstKeptEntryId: 'u2',
        tokensBefore: 1000,
      },
      message('a1', 'c1', 'assistant', 'after'),
    ];
    expect(buildSessionContext(entries, 'a1').messages.map(m => m.role)).toEqual([
      'compactionSummary',
      'user',
      'assistant',
    ]);
    expect(buildSessionContext(entries, 'a1').messages.map(m => m.content ?? m.summary)).toEqual([
      'summary',
      'kept',
      'after',
    ]);
  });

  it('tracks thinking and model settings without adding them as messages', () => {
    const entries: SessionTreeEntry[] = [
      message('u1', null, 'user', 'hello'),
      { type: 'thinking_level_change', id: 't1', parentId: 'u1', timestamp: '2026-04-19T00:00:01.000Z', thinkingLevel: 'high' },
      { type: 'model_change', id: 'm1', parentId: 't1', timestamp: '2026-04-19T00:00:02.000Z', provider: 'openai', modelId: 'gpt-x' },
    ];
    const context = buildSessionContext(entries, 'm1');
    expect(context.messages).toEqual([{ role: 'user', content: 'hello' }]);
    expect(context.thinkingLevel).toBe('high');
    expect(context.model).toEqual({ provider: 'openai', modelId: 'gpt-x' });
  });
});
  • 步骤 2实现 context_builder.ts

实现要求:

  • 以 Pi buildSessionContext(entries, leafId, byId) 为基准移植。

  • 必须先从 leaf 回溯到 root 得到 path。

  • 必须解析 thinking_level_changemodel_change、assistant message provider/model。

  • 必须识别 active path 上最新 compaction。

  • 有 compaction 时输出compaction summary、firstKeptEntryId 到 compaction 前的保留消息、compaction 后消息。

  • 无 compaction 时输出message、custom_message、branch_summary。

  • label、session_info、custom、thinking_level_change、model_change 不直接生成 LLM message。

  • 返回值包含 messagesthinkingLevelmodelsourceEntryIds

  • 步骤 3运行测试确认通过

pnpm --filter @neta/backend test -- --runInBand test/session_tree_context_builder.test.ts

预期:

  • 通过。

  • 步骤 4提交变更

git add packages/backend/src/modules/netaclaw/session-tree/context_builder.ts packages/backend/test/session_tree_context_builder.test.ts
git commit -m "feat(agent-runtime): add pi-style session context builder"

任务 4定义 Provider 契约与 Snapshot

文件:

  • 新增:packages/backend/src/modules/netaclaw/session-tree/provider.ts

  • 新增:packages/backend/src/modules/netaclaw/session-tree/snapshot.ts

  • 测试:packages/backend/test/session_tree_provider_contract.test.ts

  • 步骤 1编写 provider contract 工厂

契约测试必须作为函数导出,由 file/mysql provider 测试复用:

import type { SessionTreeProvider } from '../src/modules/netaclaw/session-tree/provider.js';

export function runSessionTreeProviderContract(name: string, createProvider: () => SessionTreeProvider) {
  describe(`${name} session tree provider contract`, () => {
    it('creates a session, appends messages, and advances leaf', async () => {
      const provider = createProvider();
      await provider.createSession({ sessionId: 's1', provider: name === 'mysql' ? 'mysql' : 'file', cwd: 'C:/workspace' });
      const user = await provider.appendMessage('s1', { role: 'user', content: 'hello' });
      const assistant = await provider.appendMessage('s1', { role: 'assistant', content: 'hi' });
      expect((await provider.getSession('s1'))?.leafEntryId).toBe(assistant.id);
      expect((await provider.getActivePath('s1')).map(e => e.id)).toEqual([user.id, assistant.id]);
    });

    it('branches without deleting sibling history', async () => {
      const provider = createProvider();
      await provider.createSession({ sessionId: 's1', provider: name === 'mysql' ? 'mysql' : 'file' });
      const root = await provider.appendMessage('s1', { role: 'user', content: 'root' });
      const branchA = await provider.appendMessage('s1', { role: 'assistant', content: 'a' });
      await provider.switchLeaf('s1', root.id);
      const branchB = await provider.appendMessage('s1', { role: 'assistant', content: 'b' });
      expect((await provider.listEntries('s1')).map(e => e.id).sort()).toEqual([root.id, branchA.id, branchB.id].sort());
      expect((await provider.getActivePath('s1')).map(e => e.id)).toEqual([root.id, branchB.id]);
    });

    it('supports branch summary, compaction, labels, and resetLeaf', async () => {
      const provider = createProvider();
      await provider.createSession({ sessionId: 's1', provider: name === 'mysql' ? 'mysql' : 'file' });
      const root = await provider.appendMessage('s1', { role: 'user', content: 'root' });
      await provider.appendLabelChange('s1', root.id, '入口');
      const kept = await provider.appendMessage('s1', { role: 'user', content: 'kept' });
      await provider.appendCompaction('s1', { summary: 'summary', firstKeptEntryId: kept.id, tokensBefore: 1000 });
      await provider.switchLeaf('s1', root.id);
      const summary = await provider.appendBranchSummary('s1', { branchFromEntryId: root.id, summary: 'branch summary' });
      expect(summary.type).toBe('branch_summary');
      await provider.resetLeaf('s1');
      const newRoot = await provider.appendMessage('s1', { role: 'user', content: 'new root' });
      expect(newRoot.parentId).toBeNull();
      expect((await provider.getSnapshot('s1')).labelsByEntryId[root.id]?.label).toBe('入口');
    });
  });
}
  • 步骤 2实现 provider.tssnapshot.ts

Provider 必须包含:

  • createSession()
  • getSession()
  • updateSession()
  • deleteSession()
  • listEntries()
  • appendEntry()
  • appendMessage()
  • appendThinkingLevelChange()
  • appendModelChange()
  • appendCompaction()
  • appendBranchSummary()
  • appendLabelChange()
  • appendSessionInfo()
  • updateInFlightEntry()
  • switchLeaf()
  • resetLeaf()
  • createBranchedSession()
  • getActivePath()
  • getSnapshot()

Snapshot 必须包含:

  • session

  • entries

  • activePath

  • childrenByParentId

  • labelsByEntryId

  • runtimeContext

  • 步骤 3运行契约 scaffold 测试

pnpm --filter @neta/backend test -- --runInBand test/session_tree_provider_contract.test.ts

预期:

  • 如果只是导出 contract 工厂且未实例化 provider则通过。

  • 步骤 4提交变更

git add packages/backend/src/modules/netaclaw/session-tree/provider.ts packages/backend/src/modules/netaclaw/session-tree/snapshot.ts packages/backend/test/session_tree_provider_contract.test.ts
git commit -m "feat(agent-runtime): define session tree provider contract"

任务 5实现 Pi 单文件 JSONL File Provider

文件:

  • 新增:packages/backend/src/modules/netaclaw/session-tree/pi_session_file.ts

  • 新增:packages/backend/src/modules/netaclaw/session-tree/file_provider.ts

  • 测试:packages/backend/test/session_tree_file_provider.test.ts

  • 步骤 1编写 file provider 测试

测试必须验证真实文件协议:

import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { FileSessionTreeProvider } from '../src/modules/netaclaw/session-tree/file_provider.js';
import { runSessionTreeProviderContract } from './session_tree_provider_contract.js';

describe('FileSessionTreeProvider', () => {
  let dir: string;

  beforeEach(() => {
    dir = mkdtempSync(join(tmpdir(), 'neta-session-tree-'));
  });

  afterEach(() => {
    rmSync(dir, { recursive: true, force: true });
  });

  runSessionTreeProviderContract('file', () => new FileSessionTreeProvider({ rootDir: dir }));

  it('stores one JSONL file with session header as first line', async () => {
    const provider = new FileSessionTreeProvider({ rootDir: dir });
    await provider.createSession({ sessionId: 's1', provider: 'file', cwd: 'C:/workspace' });
    await provider.appendMessage('s1', { role: 'user', content: 'hello' });
    const session = await provider.getSession('s1');
    const content = readFileSync(session!.sessionFile!, 'utf-8').trim().split(/\r?\n/).map(line => JSON.parse(line));
    expect(content[0].type).toBe('session');
    expect(content[1].type).toBe('message');
    expect(content[1].parentId).toBeNull();
  });
});
  • 步骤 2实现 pi_session_file.ts

实现要求:

  • 迁移 Pi 的 parseSessionEntries()loadEntriesFromFile()getTree()branch()resetLeaf()branchWithSummary()createBranchedSession() 思路。

  • 单 session 对应一个 .jsonl 文件。

  • header 第一行只写一次。

  • append entry 只追加一行。

  • label 通过 label entry 解析,不修改 target entry。

  • updateInFlightEntry() 不得频繁重写整个文件;首版可以将 in-flight entry 在内存中合并,完成时追加 completed entry。

  • 步骤 3实现 file_provider.ts

实现要求:

  • FileSessionTreeProvider 只做 SessionTreeProviderpi_session_file.ts 的适配。

  • appendMessage() 等语义方法必须调用同一 append entry 通道。

  • createBranchedSession() 必须复制 root 到 leaf 路径并保留路径内 label。

  • 步骤 4运行测试确认通过

pnpm --filter @neta/backend test -- --runInBand test/session_tree_file_provider.test.ts

预期:

  • 通过。

  • 步骤 5提交变更

git add packages/backend/src/modules/netaclaw/session-tree/pi_session_file.ts packages/backend/src/modules/netaclaw/session-tree/file_provider.ts packages/backend/test/session_tree_file_provider.test.ts
git commit -m "feat(agent-runtime): add pi-style file session provider"

任务 6新增 MySQL 实体并通过 MCP 更新数据库

文件:

  • 新增:packages/backend/src/modules/netaclaw/entity/agent_session.ts

  • 新增:packages/backend/src/modules/netaclaw/entity/agent_session_entry.ts

  • 修改:packages/backend/src/entities.ts

  • 测试:packages/backend/test/entity_exports.test.ts

  • 步骤 1扩展实体导出测试

import { entities } from '../src/entities.js';
import { NetaClawAgentSessionEntity } from '../src/modules/netaclaw/entity/agent_session.js';
import { NetaClawAgentSessionEntryEntity } from '../src/modules/netaclaw/entity/agent_session_entry.js';

describe('entities exports', () => {
  it('exports session tree entities', () => {
    expect(entities).toContain(NetaClawAgentSessionEntity);
    expect(entities).toContain(NetaClawAgentSessionEntryEntity);
  });
});
  • 步骤 2新增实体

实体要求:

  • agent_session.ts 字段:sessionIdproviderrootEntryIdleafEntryIdcwdsessionFileparentSessionIdagentIduserIdtitlestatusmetadata

  • agent_session_entry.ts 字段:sessionIdentryIdparentEntryIdentrySeqtypemessagesummaryfirstKeptEntryIdtokensBeforethinkingLevelprovidermodelIdcustomTypedatadisplaytargetEntryIdlabeldetailssourceRuntimestatus

  • 两个实体继承 packages/backend/src/modules/base/entity/base.ts 中的 BaseEntity

  • sessionId + entryId 必须唯一。

  • entrySeq 用于稳定排序,不能只依赖 createTime 字符串。

  • 步骤 3更新实体导出

packages/backend/src/entities.ts 中按现有风格导入并展开两个实体模块。

  • 步骤 4使用 MCP 修改数据库

执行要求:

  • 使用 MCP MySQL 工具直接删除旧历史会话/消息数据。

  • 使用 MCP MySQL 工具创建或重建 neta_agent_sessionneta_agent_session_entry

  • 不新增 SQL migration 文件作为主路径。

  • 修改后用 MCP describe_table 确认字段存在。

  • 步骤 5运行测试确认通过

pnpm --filter @neta/backend test -- --runInBand test/entity_exports.test.ts

预期:

  • 通过。

  • 步骤 6提交变更

git add packages/backend/src/modules/netaclaw/entity/agent_session.ts packages/backend/src/modules/netaclaw/entity/agent_session_entry.ts packages/backend/src/entities.ts packages/backend/test/entity_exports.test.ts
git commit -m "feat(agent-runtime): add session tree mysql entities"

任务 7实现 MySQL Provider

文件:

  • 新增:packages/backend/src/modules/netaclaw/session-tree/mysql_provider.ts

  • 测试:packages/backend/test/session_tree_mysql_provider.test.ts

  • 步骤 1编写 MySQL provider 测试

测试要求:

  • 复用 runSessionTreeProviderContract('mysql', ...)

  • 使用 repository mock 或 Midway mock repository。

  • 验证 entrySeq 稳定排序。

  • 验证 message JSON 无损保存。

  • 验证 compaction 与 file provider 的 context 结果一致。

  • 步骤 2实现 mysql_provider.ts

实现要求:

  • MySQL provider 是 Pi entry log 的表映射,不是独立语义。

  • appendEntry() 创建新 row默认推进 leaf。

  • appendBranchSummary() 等价于 Pi branchWithSummary()

  • appendLabelChange() 追加 label entry不修改 target entry。

  • createBranchedSession() 复制 root 到 leaf path 到新 session。

  • completed entry 禁止普通 update。

  • 步骤 3运行测试确认通过

pnpm --filter @neta/backend test -- --runInBand test/session_tree_mysql_provider.test.ts

预期:

  • 通过。

  • 步骤 4提交变更

git add packages/backend/src/modules/netaclaw/session-tree/mysql_provider.ts packages/backend/test/session_tree_mysql_provider.test.ts
git commit -m "feat(agent-runtime): add mysql session tree provider"

任务 8接入 Agent 配置中的 Session Provider

文件:

  • 新增:packages/backend/src/modules/netaclaw/session-tree/provider_factory.ts

  • 修改:packages/backend/src/modules/netaclaw/entity/agent.ts

  • 测试:packages/backend/test/session_tree_provider_factory.test.ts

  • 步骤 1编写 provider factory 测试

import { resolveSessionProviderKind } from '../src/modules/netaclaw/session-tree/provider_factory.js';

describe('session tree provider factory', () => {
  it('defaults to file provider', () => {
    expect(resolveSessionProviderKind(null)).toBe('file');
    expect(resolveSessionProviderKind({})).toBe('file');
  });

  it('accepts file and mysql only', () => {
    expect(resolveSessionProviderKind({ sessionProvider: 'file' })).toBe('file');
    expect(resolveSessionProviderKind({ sessionProvider: 'mysql' })).toBe('mysql');
    expect(resolveSessionProviderKind({ sessionProvider: 'redis' })).toBe('file');
  });
});
  • 步骤 2实现 provider factory 与 agent 字段

实现要求:

  • resolveSessionProviderKind() 只接受 filemysql

  • NetaClawAgentEntity 新增 sessionProvider 字段,默认 file

  • 字段注释使用中文,避免现有乱码注释继续扩散。

  • 步骤 3运行测试确认通过

pnpm --filter @neta/backend test -- --runInBand test/session_tree_provider_factory.test.ts

预期:

  • 通过。

  • 步骤 4提交变更

git add packages/backend/src/modules/netaclaw/session-tree/provider_factory.ts packages/backend/src/modules/netaclaw/entity/agent.ts packages/backend/test/session_tree_provider_factory.test.ts
git commit -m "feat(agent-runtime): add session provider selection"

任务 9运行聚焦验证

文件:

  • 不预期修改源码。

  • 步骤 1运行新 session-tree 测试

pnpm --filter @neta/backend test -- --runInBand test/session_tree_types.test.ts test/session_tree_path.test.ts test/session_tree_context_builder.test.ts test/session_tree_file_provider.test.ts test/session_tree_mysql_provider.test.ts test/session_tree_provider_factory.test.ts test/entity_exports.test.ts

预期:

  • 全部通过。

  • 步骤 2运行相关旧测试

pnpm --filter @neta/backend test -- --runInBand test/netaclaw_session.test.ts test/chat_orchestrator.test.ts test/subagent_service.test.ts

预期:

  • 全部通过,或明确记录旧接口将被后续计划替换导致的失败原因。

  • 步骤 3运行后端构建

pnpm --filter @neta/backend build

预期:

  • 构建成功。

  • 步骤 4检查工作区

git status --short

预期:

  • 只包含本任务相关变更。

自检

  • 设计覆盖:本计划覆盖运行时内核子集,包括 Pi 兼容 entry tree、file provider、MySQL provider、context builder、snapshot 和 agent provider 选择。
  • 直接复用:明确要求移植 Pi SessionManager、context builder、branch、compaction 关键语义。
  • 范围控制:不包含对话 WebSocket 协议、前端 UI、subagent 进程编排、skill/tool/model 资源层、管理后台页面和视觉系统。
  • 占位内容检查:没有 TBDTODOimplement later
  • 类型一致性:统一使用 id/parentId 表达 Pi entry使用 sessionId/rootEntryId/leafEntryId 表达 Neta session meta。
  • 架构防错:明确禁止 session.json + entries.jsonl、禁止 completed entry 任意 update、禁止把 tool_call/tool_result 作为第一版事实源 entry。