package engine // Dream 任务状态追踪. // // DreamTaskState 表示一次 Dream 巩固任务的完整生命周期,包括: // - 任务阶段(starting → updating,首次 Edit/Write 时翻转) // - 实时进度(每个 assistant 轮次的文本摘要 + 工具调用计数) // - 文件触达记录(filesTouched:用于完成通知"Improved N memories") // // 升华改进(ELEVATED): 早期实现在 addDreamTurn 里有"空轮跳过"优化-- // // text=="" && toolUseCount==0 && newTouched==[] 时直接 return task,不触发重渲染. // 我们保留了这个优化,防止纯事件噪声(如空 assistant 回复)触发无意义的状态更新. import ( "sync" "time" ) // DreamStatus 是 Dream 任务状态枚举. type DreamStatus string const ( DreamStatusStarting DreamStatus = "starting" DreamStatusOrienting DreamStatus = "orienting" DreamStatusGathering DreamStatus = "gathering" DreamStatusConsolidating DreamStatus = "consolidating" DreamStatusPruning DreamStatus = "pruning" DreamStatusCompleted DreamStatus = "completed" DreamStatusFailed DreamStatus = "failed" ) // maxDreamTurns 是 DreamTaskState.Turns 的滚动窗口大小. // 保留最近 N 个 assistant 轮次用于进度展示,防止长 Dream 任务无限增长. // 早期实现使用相同的 30 条上限 (MAX_TURNS = 30). const maxDreamTurns = 30 // DreamTurn 是 Dream agent 一个 assistant 轮次的进度摘要. // // 升华改进(ELEVATED): 早期实现 中 DreamTurn 包含 text + toolUseCount, // toolUse 折叠为计数而非枚举--因为 Dream 的工具调用细节对用户无意义(读取了哪些文件), // 只有"改动了几次"和"改动了哪些记忆文件"才有意义. // 我们保持相同设计. type DreamTurn struct { Text string // assistant 回复的文本内容(摘要/推理) ToolUseCount int // 本轮工具调用次数(折叠,不枚举) } // DreamTaskState 是一次 Dream 巩固任务的状态. type DreamTaskState struct { ID string `json:"id"` Status DreamStatus `json:"status"` Phase string `json:"phase"` SessionsReviewed int `json:"sessions_reviewed"` // filesTouchedSet 是 FilesTouched 的去重辅助集合(不序列化). // 升华改进(ELEVATED): 早期方案 AddFileTouched 用 O(n) 线性扫描去重-- // Dream 任务中一个文件可能被 Edit 几十次,集合越大查找越慢. // 引入 map 将单次查找从 O(n) 降至 O(1),内存开销忽略不计. filesTouchedSet map[string]struct{} // FilesTouched 是 Dream agent 通过 Edit/Write 工具触达的文件路径列表. // // 精妙之处(CLEVER): 通过 onMessage 回调从 tool_use 事件提取 file_path, // 而不是在 Dream 完成后扫描 memory dir 的 mtime-- // 事件流提取:精确,不受并发写入干扰. // mtime 扫描:有竞态,其他进程写文件会误报. // filesTouched 用于完成提示"Improved N memories",精确比完整更重要. FilesTouched []string `json:"files_touched"` // Turns 是最近 maxDreamTurns 个 assistant 轮次的进度摘要(滚动窗口). Turns []DreamTurn `json:"turns,omitempty"` StartTime time.Time `json:"start_time"` EndTime time.Time `json:"end_time,omitempty"` Error string `json:"error,omitempty"` } // DreamTaskStore 管理 Dream 任务状态,线程安全. type DreamTaskStore struct { mu sync.RWMutex tasks map[string]*DreamTaskState } // NewDreamTaskStore 创建一个新的 Dream 任务状态存储. func NewDreamTaskStore() *DreamTaskStore { return &DreamTaskStore{ tasks: make(map[string]*DreamTaskState), } } // Register 注册一个新的 Dream 任务. func (s *DreamTaskStore) Register(task *DreamTaskState) { s.mu.Lock() defer s.mu.Unlock() s.tasks[task.ID] = task } // Update 更新指定任务的状态和阶段. func (s *DreamTaskStore) Update(id string, status DreamStatus, phase string) { s.mu.Lock() defer s.mu.Unlock() if task, ok := s.tasks[id]; ok { task.Status = status task.Phase = phase if status == DreamStatusCompleted || status == DreamStatusFailed { task.EndTime = time.Now() } } } // SetError 标记任务失败并记录错误. func (s *DreamTaskStore) SetError(id string, errMsg string) { s.mu.Lock() defer s.mu.Unlock() if task, ok := s.tasks[id]; ok { task.Status = DreamStatusFailed task.Error = errMsg task.EndTime = time.Now() } } // AddFileTouched 记录任务修改了一个文件(去重). func (s *DreamTaskStore) AddFileTouched(id string, path string) { s.mu.Lock() defer s.mu.Unlock() task, ok := s.tasks[id] if !ok { return } // 惰性初始化去重 map(Register 时不预知文件数量,避免无意义的 map 分配) if task.filesTouchedSet == nil { task.filesTouchedSet = make(map[string]struct{}, len(task.FilesTouched)) for _, p := range task.FilesTouched { task.filesTouchedSet[p] = struct{}{} } } if _, exists := task.filesTouchedSet[path]; exists { return // 已记录,O(1) 检查 } task.filesTouchedSet[path] = struct{}{} task.FilesTouched = append(task.FilesTouched, path) } // AddTurn 追加一个 assistant 轮次进度摘要. // // 精妙之处(CLEVER): 空轮跳过优化--text=="" && toolUseCount==0 时不写入, // 避免纯噪声更新(如模型返回空文本)触发消费层不必要的刷新. // 滚动窗口:超过 maxDreamTurns 时丢弃最老的一条,保持内存可控. func (s *DreamTaskStore) AddTurn(id string, turn DreamTurn) { if turn.Text == "" && turn.ToolUseCount == 0 { return // 空轮跳过 } s.mu.Lock() defer s.mu.Unlock() task, ok := s.tasks[id] if !ok { return } // 滚动窗口:保留最近 maxDreamTurns 条. // 精妙之处(CLEVER): 在 append 前检查 >= 而非 >,截断为 maxDreamTurns-1 条-- // append 后恰好 maxDreamTurns,而不是 maxDreamTurns+1. // 若用 > 并截断到 maxDreamTurns:len==maxDreamTurns 时不截断,append 后变成 // maxDreamTurns+1;下一轮截断到 maxDreamTurns 再 append 又回 maxDreamTurns+1, // 永远振荡在 maxDreamTurns+1,测试期望的上界 maxDreamTurns 永远无法满足. // 替代方案: - 可行,但截尾部语义相同,不如截头部直观. if len(task.Turns) >= maxDreamTurns { task.Turns = task.Turns[len(task.Turns)-(maxDreamTurns-1):] } task.Turns = append(task.Turns, turn) } // Get 获取指定任务的状态(返回深度副本). func (s *DreamTaskStore) Get(id string) *DreamTaskState { s.mu.RLock() defer s.mu.RUnlock() task, ok := s.tasks[id] if !ok { return nil } return copyDreamTaskState(task) } // Latest 获取最近一次 Dream 任务状态(按开始时间倒序). func (s *DreamTaskStore) Latest() *DreamTaskState { s.mu.RLock() defer s.mu.RUnlock() var latest *DreamTaskState for _, task := range s.tasks { if latest == nil || task.StartTime.After(latest.StartTime) { latest = task } } if latest == nil { return nil } return copyDreamTaskState(latest) } // copyDreamTaskState 返回 DreamTaskState 的深度副本(切片字段独立分配). func copyDreamTaskState(task *DreamTaskState) *DreamTaskState { cp := *task cp.FilesTouched = make([]string, len(task.FilesTouched)) copy(cp.FilesTouched, task.FilesTouched) cp.Turns = make([]DreamTurn, len(task.Turns)) copy(cp.Turns, task.Turns) return &cp }