GPU_GUARD_MONOREPO/docs/superpowers/plans/2026-04-19-neta-agent-runtime-kernel.md

892 lines
31 KiB
Markdown
Raw Permalink Normal View History

2026-05-20 21:39:12 +08:00
# 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 文件作为主路径。
- `file``mysql` 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编写失败的类型消费测试**
```typescript
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运行测试确认失败**
```bash
pnpm --filter @neta/backend test -- --runInBand test/session_tree_types.test.ts
```
预期:
- 失败,原因是 `session-tree/types.js` 尚不存在。
- [ ] **步骤 3实现 `types.ts`**
核心类型必须包含:
```typescript
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运行测试确认通过**
```bash
pnpm --filter @neta/backend test -- --runInBand test/session_tree_types.test.ts
```
预期:
- 通过。
- [ ] **步骤 5提交变更**
```bash
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编写路径测试**
```typescript
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.ts` 与 `path.ts`**
实现要求:
- `createSessionTreeSessionId()` 使用 `uuidv7()` 或等价时间有序 UUID。
- `createSessionTreeEntryId(existingIds)` 生成短 ID并检查碰撞。
- `getPathToLeaf()` 必须检测 missing leaf、missing parent、cycle。
- `groupChildrenByParent()` 必须以 append 顺序稳定排序。
- `resolveLatestLabels()` 必须以后写 label 覆盖旧 label。
- [ ] **步骤 3运行测试确认通过**
```bash
pnpm --filter @neta/backend test -- --runInBand test/session_tree_path.test.ts
```
预期:
- 通过。
- [ ] **步骤 4提交变更**
```bash
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编写上下文构建测试**
```typescript
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_change``model_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。
- 返回值包含 `messages``thinkingLevel``model``sourceEntryIds`
- [ ] **步骤 3运行测试确认通过**
```bash
pnpm --filter @neta/backend test -- --runInBand test/session_tree_context_builder.test.ts
```
预期:
- 通过。
- [ ] **步骤 4提交变更**
```bash
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 测试复用:
```typescript
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.ts` 与 `snapshot.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 测试**
```bash
pnpm --filter @neta/backend test -- --runInBand test/session_tree_provider_contract.test.ts
```
预期:
- 如果只是导出 contract 工厂且未实例化 provider则通过。
- [ ] **步骤 4提交变更**
```bash
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 测试**
测试必须验证真实文件协议:
```typescript
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` 只做 `SessionTreeProvider``pi_session_file.ts` 的适配。
- `appendMessage()` 等语义方法必须调用同一 append entry 通道。
- `createBranchedSession()` 必须复制 root 到 leaf 路径并保留路径内 label。
- [ ] **步骤 4运行测试确认通过**
```bash
pnpm --filter @neta/backend test -- --runInBand test/session_tree_file_provider.test.ts
```
预期:
- 通过。
- [ ] **步骤 5提交变更**
```bash
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扩展实体导出测试**
```typescript
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` 字段:`sessionId``provider``rootEntryId``leafEntryId``cwd``sessionFile``parentSessionId``agentId``userId``title``status``metadata`
- `agent_session_entry.ts` 字段:`sessionId``entryId``parentEntryId``entrySeq``type``message``summary``firstKeptEntryId``tokensBefore``thinkingLevel``provider``modelId``customType``data``display``targetEntryId``label``details``sourceRuntime``status`
- 两个实体继承 `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_session``neta_agent_session_entry`
- 不新增 SQL migration 文件作为主路径。
- 修改后用 MCP `describe_table` 确认字段存在。
- [ ] **步骤 5运行测试确认通过**
```bash
pnpm --filter @neta/backend test -- --runInBand test/entity_exports.test.ts
```
预期:
- 通过。
- [ ] **步骤 6提交变更**
```bash
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运行测试确认通过**
```bash
pnpm --filter @neta/backend test -- --runInBand test/session_tree_mysql_provider.test.ts
```
预期:
- 通过。
- [ ] **步骤 4提交变更**
```bash
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 测试**
```typescript
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()` 只接受 `file``mysql`
- `NetaClawAgentEntity` 新增 `sessionProvider` 字段,默认 `file`
- 字段注释使用中文,避免现有乱码注释继续扩散。
- [ ] **步骤 3运行测试确认通过**
```bash
pnpm --filter @neta/backend test -- --runInBand test/session_tree_provider_factory.test.ts
```
预期:
- 通过。
- [ ] **步骤 4提交变更**
```bash
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 测试**
```bash
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运行相关旧测试**
```bash
pnpm --filter @neta/backend test -- --runInBand test/netaclaw_session.test.ts test/chat_orchestrator.test.ts test/subagent_service.test.ts
```
预期:
- 全部通过,或明确记录旧接口将被后续计划替换导致的失败原因。
- [ ] **步骤 3运行后端构建**
```bash
pnpm --filter @neta/backend build
```
预期:
- 构建成功。
- [ ] **步骤 4检查工作区**
```bash
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 资源层、管理后台页面和视觉系统。
- 占位内容检查:没有 `TBD``TODO``implement later`
- 类型一致性:统一使用 `id/parentId` 表达 Pi entry使用 `sessionId/rootEntryId/leafEntryId` 表达 Neta session meta。
- 架构防错:明确禁止 `session.json + entries.jsonl`、禁止 completed entry 任意 update、禁止把 tool_call/tool_result 作为第一版事实源 entry。