From eb50db01645c5235c31a654490bb1b3e76054e7f Mon Sep 17 00:00:00 2001 From: CrazyBoyM Date: Fri, 23 Jan 2026 01:17:59 +0800 Subject: [PATCH] refactor: remove unused Checkpointer dead code The Checkpointer interface and implementations (Memory, File, Redis) were designed but never integrated into the Agent runtime. Store.snapshot already provides all the functionality needed for fork/resume operations. Removed: - src/core/checkpointer.ts (interface + MemoryCheckpointer) - src/core/checkpointers/ (FileCheckpointer, RedisCheckpointer) - tests/unit/core/checkpointer.test.ts Updated: - src/index.ts: removed Checkpointer exports - README.md: fixed example to use agent.snapshot() instead of agent.checkpoint() - docs/ROADMAP.md: replaced "Checkpointer" with "Snapshot/Fork" - tests/README.md: removed Checkpointer reference This removes ~500 lines of dead code without affecting any functionality. Co-Authored-By: Claude Opus 4.5 --- README.md | 8 +- docs/ROADMAP.md | 2 +- src/core/checkpointer.ts | 164 --------------------------- src/core/checkpointers/file.ts | 135 ---------------------- src/core/checkpointers/index.ts | 3 - src/core/checkpointers/redis.ts | 151 ------------------------ src/index.ts | 8 -- tests/README.md | 2 +- tests/unit/core/checkpointer.test.ts | 55 --------- 9 files changed, 6 insertions(+), 522 deletions(-) delete mode 100644 src/core/checkpointer.ts delete mode 100644 src/core/checkpointers/file.ts delete mode 100644 src/core/checkpointers/index.ts delete mode 100644 src/core/checkpointers/redis.ts delete mode 100644 tests/unit/core/checkpointer.test.ts diff --git a/README.md b/README.md index 1e07094..3754d1e 100644 --- a/README.md +++ b/README.md @@ -145,12 +145,12 @@ On crash: Resume from last safe breakpoint, auto-seal incomplete tool calls ### 3. Fork & Trajectory Exploration ```typescript -// Create a checkpoint at current state -const checkpointId = await agent.checkpoint('before-decision'); +// Create a snapshot at current state +const snapshotId = await agent.snapshot('before-decision'); // Fork to explore different paths -const explorerA = await agent.fork(checkpointId); -const explorerB = await agent.fork(checkpointId); +const explorerA = await agent.fork(snapshotId); +const explorerB = await agent.fork(snapshotId); await explorerA.chat('Try approach A'); await explorerB.chat('Try approach B'); diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index ba00487..dd23b70 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -17,7 +17,7 @@ | Multi-provider | Stable | Anthropic, OpenAI, Gemini, DeepSeek, Qwen, GLM... | | Tool System | Stable | Built-in + MCP protocol | | AgentPool | Stable | Up to 50 agents per process | -| Checkpointer | Stable | Memory, File, Redis implementations | +| Snapshot/Fork | Stable | Explore different agent trajectories | | Context Compression | Stable | Automatic history management | | Hook System | Stable | Pre/post model and tool hooks | diff --git a/src/core/checkpointer.ts b/src/core/checkpointer.ts deleted file mode 100644 index ef6baa7..0000000 --- a/src/core/checkpointer.ts +++ /dev/null @@ -1,164 +0,0 @@ -import type { Message, ToolCallRecord } from './types'; -import type { ToolDescriptor } from '../tools/registry'; - -/** - * Agent 状态快照 - */ -export interface AgentState { - status: 'ready' | 'working' | 'paused' | 'completed' | 'failed'; - stepCount: number; - lastSfpIndex: number; - lastBookmark?: { - seq: number; - timestamp: number; - }; -} - -/** - * Checkpoint 数据结构 - */ -export interface Checkpoint { - id: string; - agentId: string; - sessionId?: string; - timestamp: number; - version: string; - - // Agent 状态 - state: AgentState; - messages: Message[]; - toolRecords: ToolCallRecord[]; - - // 工具恢复信息 - tools: ToolDescriptor[]; - - // 配置 - config: { - model: string; - systemPrompt?: string; - templateId?: string; - }; - - // 元数据 - metadata: { - isForkPoint?: boolean; - parentCheckpointId?: string; - tags?: string[]; - [key: string]: any; - }; -} - -/** - * Checkpoint 元数据(列表时使用) - */ -export interface CheckpointMetadata { - id: string; - agentId: string; - sessionId?: string; - timestamp: number; - isForkPoint?: boolean; - tags?: string[]; -} - -/** - * Checkpointer 接口 - * - * 提供可选的持久化机制,解耦 Store 强依赖 - */ -export interface Checkpointer { - /** - * 保存 checkpoint - */ - save(checkpoint: Checkpoint): Promise; - - /** - * 加载 checkpoint - */ - load(checkpointId: string): Promise; - - /** - * 列出 Agent 的所有 checkpoints - */ - list( - agentId: string, - options?: { - sessionId?: string; - limit?: number; - offset?: number; - } - ): Promise; - - /** - * 删除 checkpoint - */ - delete(checkpointId: string): Promise; - - /** - * Fork checkpoint(可选) - */ - fork?(checkpointId: string, newAgentId: string): Promise; -} - -/** - * 内存 Checkpointer(默认实现) - */ -export class MemoryCheckpointer implements Checkpointer { - private checkpoints = new Map(); - - async save(checkpoint: Checkpoint): Promise { - this.checkpoints.set(checkpoint.id, JSON.parse(JSON.stringify(checkpoint))); - return checkpoint.id; - } - - async load(checkpointId: string): Promise { - const checkpoint = this.checkpoints.get(checkpointId); - return checkpoint ? JSON.parse(JSON.stringify(checkpoint)) : null; - } - - async list( - agentId: string, - options?: { sessionId?: string; limit?: number; offset?: number } - ): Promise { - const allCheckpoints = Array.from(this.checkpoints.values()) - .filter((cp) => cp.agentId === agentId) - .filter((cp) => !options?.sessionId || cp.sessionId === options.sessionId) - .sort((a, b) => b.timestamp - a.timestamp); - - const start = options?.offset || 0; - const end = options?.limit ? start + options.limit : undefined; - const slice = allCheckpoints.slice(start, end); - - return slice.map((cp) => ({ - id: cp.id, - agentId: cp.agentId, - sessionId: cp.sessionId, - timestamp: cp.timestamp, - isForkPoint: cp.metadata.isForkPoint, - tags: cp.metadata.tags, - })); - } - - async delete(checkpointId: string): Promise { - this.checkpoints.delete(checkpointId); - } - - async fork(checkpointId: string, newAgentId: string): Promise { - const original = await this.load(checkpointId); - if (!original) { - throw new Error(`Checkpoint not found: ${checkpointId}`); - } - - const forked: Checkpoint = { - ...original, - id: `${newAgentId}-${Date.now()}`, - agentId: newAgentId, - timestamp: Date.now(), - metadata: { - ...original.metadata, - parentCheckpointId: checkpointId, - }, - }; - - return await this.save(forked); - } -} diff --git a/src/core/checkpointers/file.ts b/src/core/checkpointers/file.ts deleted file mode 100644 index 6233297..0000000 --- a/src/core/checkpointers/file.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { promises as fs } from 'fs'; -import * as path from 'path'; -import type { Checkpointer, Checkpoint, CheckpointMetadata } from '../checkpointer'; - -/** - * File-based Checkpointer - * - * 将 checkpoints 保存到本地文件系统 - */ -export class FileCheckpointer implements Checkpointer { - constructor(private readonly baseDir: string) {} - - async save(checkpoint: Checkpoint): Promise { - await this.ensureDir(); - const agentDir = path.join(this.baseDir, checkpoint.agentId); - await fs.mkdir(agentDir, { recursive: true }); - - const filePath = path.join(agentDir, `${checkpoint.id}.json`); - await fs.writeFile(filePath, JSON.stringify(checkpoint, null, 2), 'utf-8'); - - return checkpoint.id; - } - - async load(checkpointId: string): Promise { - try { - // 扫描所有 agent 目录查找 checkpoint - const agentDirs = await fs.readdir(this.baseDir); - - for (const agentId of agentDirs) { - const agentDir = path.join(this.baseDir, agentId); - const stat = await fs.stat(agentDir); - - if (!stat.isDirectory()) continue; - - const filePath = path.join(agentDir, `${checkpointId}.json`); - try { - const content = await fs.readFile(filePath, 'utf-8'); - return JSON.parse(content); - } catch { - continue; - } - } - - return null; - } catch { - return null; - } - } - - async list( - agentId: string, - options?: { sessionId?: string; limit?: number; offset?: number } - ): Promise { - const agentDir = path.join(this.baseDir, agentId); - - try { - const files = await fs.readdir(agentDir); - const checkpoints: CheckpointMetadata[] = []; - - for (const file of files) { - if (!file.endsWith('.json')) continue; - - const filePath = path.join(agentDir, file); - const content = await fs.readFile(filePath, 'utf-8'); - const checkpoint: Checkpoint = JSON.parse(content); - - if (options?.sessionId && checkpoint.sessionId !== options.sessionId) { - continue; - } - - checkpoints.push({ - id: checkpoint.id, - agentId: checkpoint.agentId, - sessionId: checkpoint.sessionId, - timestamp: checkpoint.timestamp, - isForkPoint: checkpoint.metadata.isForkPoint, - tags: checkpoint.metadata.tags, - }); - } - - // 按时间排序 - checkpoints.sort((a, b) => b.timestamp - a.timestamp); - - // 分页 - const start = options?.offset || 0; - const end = options?.limit ? start + options.limit : undefined; - - return checkpoints.slice(start, end); - } catch { - return []; - } - } - - async delete(checkpointId: string): Promise { - try { - const agentDirs = await fs.readdir(this.baseDir); - - for (const agentId of agentDirs) { - const filePath = path.join(this.baseDir, agentId, `${checkpointId}.json`); - try { - await fs.unlink(filePath); - return; - } catch { - continue; - } - } - } catch { - // Ignore errors - } - } - - async fork(checkpointId: string, newAgentId: string): Promise { - const original = await this.load(checkpointId); - if (!original) { - throw new Error(`Checkpoint not found: ${checkpointId}`); - } - - const forked: Checkpoint = { - ...original, - id: `${newAgentId}-${Date.now()}`, - agentId: newAgentId, - timestamp: Date.now(), - metadata: { - ...original.metadata, - parentCheckpointId: checkpointId, - }, - }; - - return await this.save(forked); - } - - private async ensureDir(): Promise { - await fs.mkdir(this.baseDir, { recursive: true }); - } -} diff --git a/src/core/checkpointers/index.ts b/src/core/checkpointers/index.ts deleted file mode 100644 index fd1b026..0000000 --- a/src/core/checkpointers/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { MemoryCheckpointer } from '../checkpointer'; -export { FileCheckpointer } from './file'; -export { RedisCheckpointer } from './redis'; diff --git a/src/core/checkpointers/redis.ts b/src/core/checkpointers/redis.ts deleted file mode 100644 index 3d20d51..0000000 --- a/src/core/checkpointers/redis.ts +++ /dev/null @@ -1,151 +0,0 @@ -import type { Checkpointer, Checkpoint, CheckpointMetadata } from '../checkpointer'; - -/** - * Redis 配置 - */ -export interface RedisCheckpointerConfig { - host?: string; - port?: number; - password?: string; - db?: number; - keyPrefix?: string; - ttl?: number; // TTL in seconds -} - -/** - * Redis-based Checkpointer - * - * 使用 Redis 存储 checkpoints(需要 ioredis) - */ -export class RedisCheckpointer implements Checkpointer { - private redis: any; - private keyPrefix: string; - private ttl?: number; - - constructor(config: RedisCheckpointerConfig = {}) { - this.keyPrefix = config.keyPrefix || 'kode:checkpoint:'; - this.ttl = config.ttl; - - // 延迟加载 ioredis(可选依赖) - try { - const Redis = require('ioredis'); - this.redis = new Redis({ - host: config.host || 'localhost', - port: config.port || 6379, - password: config.password, - db: config.db || 0, - }); - } catch (error) { - throw new Error( - 'ioredis is required for RedisCheckpointer. Install it with: npm install ioredis' - ); - } - } - - async save(checkpoint: Checkpoint): Promise { - const key = this.getKey(checkpoint.id); - const value = JSON.stringify(checkpoint); - - if (this.ttl) { - await this.redis.setex(key, this.ttl, value); - } else { - await this.redis.set(key, value); - } - - // 添加到 agent 的索引 - const indexKey = this.getIndexKey(checkpoint.agentId); - await this.redis.zadd(indexKey, checkpoint.timestamp, checkpoint.id); - - return checkpoint.id; - } - - async load(checkpointId: string): Promise { - const key = this.getKey(checkpointId); - const value = await this.redis.get(key); - - if (!value) return null; - - return JSON.parse(value); - } - - async list( - agentId: string, - options?: { sessionId?: string; limit?: number; offset?: number } - ): Promise { - const indexKey = this.getIndexKey(agentId); - - // 按时间倒序获取 checkpoint IDs - const start = options?.offset || 0; - const end = options?.limit ? start + options.limit - 1 : -1; - const ids = await this.redis.zrevrange(indexKey, start, end); - - const checkpoints: CheckpointMetadata[] = []; - - for (const id of ids) { - const checkpoint = await this.load(id); - if (!checkpoint) continue; - - if (options?.sessionId && checkpoint.sessionId !== options.sessionId) { - continue; - } - - checkpoints.push({ - id: checkpoint.id, - agentId: checkpoint.agentId, - sessionId: checkpoint.sessionId, - timestamp: checkpoint.timestamp, - isForkPoint: checkpoint.metadata.isForkPoint, - tags: checkpoint.metadata.tags, - }); - } - - return checkpoints; - } - - async delete(checkpointId: string): Promise { - const checkpoint = await this.load(checkpointId); - if (!checkpoint) return; - - // 删除 checkpoint - const key = this.getKey(checkpointId); - await this.redis.del(key); - - // 从索引中移除 - const indexKey = this.getIndexKey(checkpoint.agentId); - await this.redis.zrem(indexKey, checkpointId); - } - - async fork(checkpointId: string, newAgentId: string): Promise { - const original = await this.load(checkpointId); - if (!original) { - throw new Error(`Checkpoint not found: ${checkpointId}`); - } - - const forked: Checkpoint = { - ...original, - id: `${newAgentId}:${Date.now()}`, - agentId: newAgentId, - timestamp: Date.now(), - metadata: { - ...original.metadata, - parentCheckpointId: checkpointId, - }, - }; - - return await this.save(forked); - } - - async disconnect(): Promise { - if (this.redis) { - await this.redis.quit(); - } - } - - private getKey(checkpointId: string): string { - return `${this.keyPrefix}${checkpointId}`; - } - - private getIndexKey(agentId: string): string { - return `${this.keyPrefix}index:${agentId}`; - } -} diff --git a/src/index.ts b/src/index.ts index 7e1bb3c..1609c8a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -54,14 +54,6 @@ export { PermissionEvaluationContext, PermissionDecision, } from './core/permission-modes'; -export { - Checkpointer, - Checkpoint, - CheckpointMetadata, - AgentState, - MemoryCheckpointer, -} from './core/checkpointer'; -export { FileCheckpointer, RedisCheckpointer } from './core/checkpointers'; // Types export * from './core/types'; diff --git a/tests/README.md b/tests/README.md index 7e411dd..0179eeb 100644 --- a/tests/README.md +++ b/tests/README.md @@ -108,7 +108,7 @@ export async function run() { - Scheduler & TimeBridge、MessageQueue、ContextManager、FilePool - 基础设施:JSONStore WAL、LocalSandbox 边界与危险命令拦截 - 内置工具:文件、Bash、Todo 工具执行 -- 其他:Checkpointer、ToolRunner、AgentId 等辅助模块 +- 其他:ToolRunner、AgentId 等辅助模块 ### 集成测试 - 真实模型多轮对话与流式输出 diff --git a/tests/unit/core/checkpointer.test.ts b/tests/unit/core/checkpointer.test.ts deleted file mode 100644 index b397c45..0000000 --- a/tests/unit/core/checkpointer.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { MemoryCheckpointer, Checkpoint } from '../../../src/core/checkpointer'; -import { TestRunner, expect } from '../../helpers/utils'; - -const runner = new TestRunner('Checkpointer'); - -const baseCheckpoint: Checkpoint = { - id: 'cp-1', - agentId: 'agent-1', - timestamp: Date.now(), - version: '1', - state: { status: 'ready', stepCount: 0, lastSfpIndex: -1 }, - messages: [], - toolRecords: [], - tools: [], - config: { model: 'mock' }, - metadata: {}, -}; - -runner - .test('保存、加载、列出和删除', async () => { - const cp = new MemoryCheckpointer(); - await cp.save(baseCheckpoint); - - const loaded = await cp.load('cp-1'); - expect.toBeTruthy(loaded); - - const list = await cp.list('agent-1'); - expect.toEqual(list.length, 1); - - await cp.delete('cp-1'); - expect.toBeTruthy(await cp.load('cp-1') === null); - }) - - .test('fork 创建新快照', async () => { - const cp = new MemoryCheckpointer(); - await cp.save(baseCheckpoint); - - const forkId = await cp.fork('cp-1', 'agent-2'); - expect.toBeTruthy(forkId); - - const list = await cp.list('agent-2'); - expect.toEqual(list.length, 1); - expect.toContain(list[0].id, 'agent-2'); - }); - -export async function run() { - return await runner.run(); -} - -if (require.main === module) { - run().catch((err) => { - console.error(err); - process.exitCode = 1; - }); -}