feat: 添加会话持久化和 Todo 工具

会话持久化:
- 新增 SessionManager 和 SessionStorage,支持会话自动保存和恢复
- 会话数据存储在 ~/.local/share/ai-assist/,遵循 XDG 规范
- 支持对话历史、已发现工具、待办事项的持久化
- 启动时自动恢复同一工作目录的上次会话
- 支持会话归档和历史会话管理

Todo 工具:
- 新增 todoread 工具:读取当前会话的待办事项列表
- 新增 todowrite 工具:创建和更新待办事项
- 支持 pending/in_progress/completed 三种状态
- 待办事项随会话自动持久化

其他改进:
- ToolResult 类型新增可选的 metadata 字段
- Agent 支持会话管理器集成
- clearHistory 改为异步方法
This commit is contained in:
2025-12-10 22:55:37 +08:00
parent bc1ece3dad
commit 1e0ecc2de7
13 changed files with 862 additions and 4 deletions
+4
View File
@@ -6,6 +6,7 @@ import { bashTool } from './shell/index.js';
// 核心工具
import { toolSearchTool } from './tool-search.js';
import { todoReadTool, todoWriteTool } from './todo/index.js';
// 文件系统工具
import {
@@ -27,6 +28,8 @@ const allToolsWithMetadata: ToolWithMetadata[] = [
// 核心工具 (deferLoading: false)
toolSearchTool,
bashTool,
todoReadTool,
todoWriteTool,
// 文件系统工具 (deferLoading: true)
readFileTool,
@@ -48,6 +51,7 @@ toolRegistry.registerAll(allToolsWithMetadata);
// 导出
export { toolRegistry } from './registry.js';
export { toolSearchTool } from './tool-search.js';
export { todoManager } from './todo/index.js';
export type { ToolWithMetadata, ToolMetadata, ToolCategory, ToolSearchResult } from './types.js';
// 兼容旧代码:导出所有工具数组(基础 Tool 类型)
+3
View File
@@ -0,0 +1,3 @@
export { todoReadTool } from './todoread.js';
export { todoWriteTool } from './todowrite.js';
export { todoManager } from './todo-manager.js';
+106
View File
@@ -0,0 +1,106 @@
import type { Todo, TodoStatus } from '../../session/types.js';
import type { SessionManager } from '../../session/index.js';
/**
* Todo 管理器
* 提供对当前会话 todo 列表的操作接口
*/
class TodoManager {
private sessionManager: SessionManager | null = null;
/**
* 设置会话管理器
*/
setSessionManager(manager: SessionManager): void {
this.sessionManager = manager;
}
/**
* 获取当前 todo 列表
*/
getTodos(): Todo[] {
if (!this.sessionManager) {
return [];
}
return this.sessionManager.getTodos();
}
/**
* 更新 todo 列表
*/
async setTodos(todos: Todo[]): Promise<void> {
if (!this.sessionManager) {
return;
}
await this.sessionManager.setTodos(todos);
}
/**
* 添加单个 todo
*/
async addTodo(content: string, status: TodoStatus = 'pending'): Promise<Todo> {
const todos = this.getTodos();
const now = new Date().toISOString();
const newTodo: Todo = {
id: this.generateId(),
content,
status,
createdAt: now,
updatedAt: now,
};
todos.push(newTodo);
await this.setTodos(todos);
return newTodo;
}
/**
* 更新 todo 状态
*/
async updateTodoStatus(id: string, status: TodoStatus): Promise<boolean> {
const todos = this.getTodos();
const todo = todos.find((t) => t.id === id);
if (!todo) return false;
todo.status = status;
todo.updatedAt = new Date().toISOString();
await this.setTodos(todos);
return true;
}
/**
* 删除 todo
*/
async deleteTodo(id: string): Promise<boolean> {
const todos = this.getTodos();
const index = todos.findIndex((t) => t.id === id);
if (index === -1) return false;
todos.splice(index, 1);
await this.setTodos(todos);
return true;
}
/**
* 清空所有 todo
*/
async clearTodos(): Promise<void> {
await this.setTodos([]);
}
/**
* 生成唯一 ID
*/
private generateId(): string {
return Math.random().toString(36).substring(2, 10);
}
/**
* 检查是否已初始化
*/
isInitialized(): boolean {
return this.sessionManager !== null;
}
}
// 导出单例
export const todoManager = new TodoManager();
+51
View File
@@ -0,0 +1,51 @@
import type { ToolResult } from '../../types/index.js';
import type { ToolWithMetadata } from '../types.js';
import { todoManager } from './todo-manager.js';
export const todoReadTool: ToolWithMetadata = {
name: 'todoread',
description: `读取当前会话的待办事项列表。
使用场景:
- 在对话开始时查看待处理的任务
- 开始新任务前了解当前进度
- 用户询问之前的任务或计划时
- 不确定下一步做什么时
- 完成任务后更新对剩余工作的理解
- 每隔几条消息检查一次以确保进度正常
返回格式:
- 返回 JSON 格式的待办事项列表
- 每个事项包含 id、content(内容)、status(状态)
- 状态:pending(待处理)、in_progress(进行中)、completed(已完成)`,
metadata: {
name: 'todoread',
category: 'core',
description: '读取待办事项列表',
keywords: ['todo', 'task', 'list', 'read', '待办', '任务', '列表', '进度'],
deferLoading: false, // 核心工具,始终加载
},
parameters: {},
execute: async (): Promise<ToolResult> => {
if (!todoManager.isInitialized()) {
return {
success: false,
output: '',
error: '会话管理器未初始化,无法读取待办事项',
};
}
const todos = todoManager.getTodos();
const pendingCount = todos.filter((t) => t.status !== 'completed').length;
return {
success: true,
output: JSON.stringify(todos, null, 2),
metadata: {
todos,
pendingCount,
totalCount: todos.length,
},
};
},
};
+139
View File
@@ -0,0 +1,139 @@
import type { ToolResult } from '../../types/index.js';
import type { ToolWithMetadata } from '../types.js';
import type { Todo, TodoStatus } from '../../session/types.js';
import { todoManager } from './todo-manager.js';
/**
* 验证 todo 项
*/
function validateTodo(item: unknown): item is { content: string; status: TodoStatus } {
if (typeof item !== 'object' || item === null) return false;
const obj = item as Record<string, unknown>;
if (typeof obj.content !== 'string' || obj.content.trim() === '') return false;
if (!['pending', 'in_progress', 'completed'].includes(obj.status as string)) return false;
return true;
}
export const todoWriteTool: ToolWithMetadata = {
name: 'todowrite',
description: `创建和管理当前会话的待办事项列表。用于跟踪进度、组织复杂任务,并向用户展示工作进展。
## 使用场景
主动使用此工具的情况:
1. 复杂多步骤任务 - 任务需要 3 个或更多步骤
2. 非平凡的复杂任务 - 需要仔细规划或多个操作的任务
3. 用户明确要求使用待办列表
4. 用户提供多个任务 - 用户给出编号列表或逗号分隔的任务
5. 收到新指令后 - 立即将用户需求记录为待办
6. 开始处理任务时 - 将其标记为 in_progress
7. 完成任务后 - 标记为 completed 并添加发现的后续任务
## 不使用此工具的情况
跳过使用的情况:
1. 只有单个简单任务
2. 任务太简单,跟踪没有意义
3. 任务可以在 3 个简单步骤内完成
4. 纯粹的对话或信息性请求
## 任务状态
- pending: 待处理,尚未开始
- in_progress: 进行中(同一时间只能有一个)
- completed: 已完成
## 任务管理规则
- 实时更新任务状态
- 完成后立即标记(不要批量标记)
- 同一时间只有一个任务处于 in_progress
- 完成当前任务后再开始新任务`,
metadata: {
name: 'todowrite',
category: 'core',
description: '创建和更新待办事项列表',
keywords: ['todo', 'task', 'write', 'update', 'create', '待办', '任务', '创建', '更新'],
deferLoading: false, // 核心工具,始终加载
},
parameters: {
todos: {
type: 'array',
description:
'更新后的待办事项列表。每个事项包含 content(任务内容)和 statuspending/in_progress/completed',
required: true,
},
},
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
if (!todoManager.isInitialized()) {
return {
success: false,
output: '',
error: '会话管理器未初始化,无法更新待办事项',
};
}
const todosInput = params.todos;
if (!Array.isArray(todosInput)) {
return {
success: false,
output: '',
error: 'todos 参数必须是数组',
};
}
// 验证并转换输入
const now = new Date().toISOString();
const existingTodos = todoManager.getTodos();
const existingMap = new Map(existingTodos.map((t) => [t.content, t]));
const newTodos: Todo[] = [];
for (let i = 0; i < todosInput.length; i++) {
const item = todosInput[i];
if (!validateTodo(item)) {
return {
success: false,
output: '',
error: `${i + 1} 个待办事项格式无效。需要 { content: string, status: 'pending' | 'in_progress' | 'completed' }`,
};
}
// 查找是否已存在(通过内容匹配)
const existing = existingMap.get(item.content);
if (existing) {
// 更新现有项
newTodos.push({
...existing,
status: item.status,
updatedAt: now,
});
} else {
// 创建新项
newTodos.push({
id: Math.random().toString(36).substring(2, 10),
content: item.content,
status: item.status,
createdAt: now,
updatedAt: now,
});
}
}
await todoManager.setTodos(newTodos);
const pendingCount = newTodos.filter((t) => t.status !== 'completed').length;
const completedCount = newTodos.filter((t) => t.status === 'completed').length;
const inProgressCount = newTodos.filter((t) => t.status === 'in_progress').length;
return {
success: true,
output: `待办事项已更新: ${pendingCount} 待处理, ${inProgressCount} 进行中, ${completedCount} 已完成`,
metadata: {
todos: newTodos,
pendingCount,
inProgressCount,
completedCount,
},
};
},
};