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; - }); -}