892 lines
31 KiB
Markdown
892 lines
31 KiB
Markdown
# 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 entry;entry 通过 `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` entry;tool 状态作为 `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。
|