后端: 1.粗排链路收口(按 task_class_ids 精确加载 ScheduleState + 规划窗口抗脏数据) - 更新conv/schedule_provider.go:新增 LoadScheduleStateForTaskClasses;优先按本轮任务类加载窗口;buildWindowFromTaskClasses 改为逐条过滤坏日期,避免 DayMapping 被全量任务类污染 - 更新model/state_store.go:新增 ScopedScheduleStateProvider 可选接口 - 更新model/graph_run_state.go:EnsureScheduleState 首次加载时优先走 scoped provider,再做 scope 裁剪 2.粗排建议态语义统一(pending/existing → pending/suggested/existing) - 新建tools/status.go:统一 IsPendingTask / IsSuggestedTask / IsExistingTask / scope 过滤逻辑 - 更新node/rough_build.go:粗排回写后任务显式转 suggested;pending 统计仅看“真实 pending” - 更新tools/state.go:ScheduleTask.Status/Slots/Duration 注释补齐 suggested 语义 - 更新tools/read_helpers.go + read_tools.go:overview/list_tasks/task_info 支持 suggested 展示;占用计算按“已落位任务”统一处理 - 更新tools/write_helpers.go + write_tools.go:place/move/swap/unplace 全量切到 suggested/existing/pending 新语义 - 更新tools/registry.go + SCHEDULE_TOOLS.md:工具描述、参数枚举、文档口径同步到 suggested 语义 - 更新conv/schedule_preview.go:预览层统一通过 IsSuggestedTask 输出 suggested,兼容旧快照 - 更新service/agentsvc/agent_newagent.go:预览 debug 摘要改为 pending/suggested/existing 三态统计 3.粗排调试增强 - 更新node/rough_build.go:新增 applied/day_mapping_miss/task_item_match_miss 统计及样本日志,便于排查 placement 未落回 state 的根因 前端:无 仓库:无
119 lines
4.4 KiB
Go
119 lines
4.4 KiB
Go
package newagenttools
|
||
|
||
import "slices"
|
||
|
||
// 任务状态常量。
|
||
//
|
||
// 说明:
|
||
// 1. existing 表示“数据库里已经存在的已安排事实”,例如课程表事件、已持久化任务块;
|
||
// 2. suggested 表示“当前轮内存态里的建议落位”,来源可能是粗排结果,也可能是用户确认后的工具预排;
|
||
// 3. pending 表示“仍未落位的真实待安排任务”。
|
||
const (
|
||
TaskStatusExisting = "existing"
|
||
TaskStatusSuggested = "suggested"
|
||
TaskStatusPending = "pending"
|
||
)
|
||
|
||
// IsPendingTask 判断任务是否属于“真实待安排”状态。
|
||
//
|
||
// 并行迁移说明:
|
||
// 1. 只有 pending 且没有 Slots,才视为真正未落位;
|
||
// 2. 旧快照里可能存在“pending 但已有 Slots”的粗排遗留形态,这类任务不应继续算作待安排;
|
||
// 3. 这样可以在不强制清洗旧快照的前提下,先把新旧语义统一到“pending=无落位”。
|
||
func IsPendingTask(task ScheduleTask) bool {
|
||
return task.Status == TaskStatusPending && len(task.Slots) == 0
|
||
}
|
||
|
||
// IsSuggestedTask 判断任务是否属于“建议落位 / 可优化”状态。
|
||
//
|
||
// 并行迁移说明:
|
||
// 1. 新语义使用显式 suggested 状态;
|
||
// 2. 兼容旧 rough_build 快照:pending + Slots 视为 suggested;
|
||
// 3. 兼容旧 place 快照:existing + source=task_item + Duration>0 + Slots 视为 suggested。
|
||
func IsSuggestedTask(task ScheduleTask) bool {
|
||
if len(task.Slots) == 0 {
|
||
return false
|
||
}
|
||
if task.Status == TaskStatusSuggested {
|
||
return true
|
||
}
|
||
if task.Status == TaskStatusPending {
|
||
return true
|
||
}
|
||
if task.Status == TaskStatusExisting && task.Source == "task_item" && task.Duration > 0 {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// IsExistingTask 判断任务是否属于“已确定事实层”。
|
||
//
|
||
// 说明:
|
||
// 1. 这里会主动排除 suggested 兼容形态,避免旧快照里的 existing+Duration>0 被误当成已确定任务;
|
||
// 2. 这样 list_tasks / get_overview 才能稳定区分“事实层 existing”和“建议层 suggested”。
|
||
func IsExistingTask(task ScheduleTask) bool {
|
||
return task.Status == TaskStatusExisting && !IsSuggestedTask(task)
|
||
}
|
||
|
||
// IsPlacedTask 判断任务当前是否已经拥有可操作的落位。
|
||
//
|
||
// 说明:
|
||
// 1. existing 和 suggested 都属于“已落位”;
|
||
// 2. pending 只有在并行迁移兼容形态(pending + Slots)下,才会被 IsSuggestedTask 吸收进来。
|
||
func IsPlacedTask(task ScheduleTask) bool {
|
||
return IsExistingTask(task) || IsSuggestedTask(task)
|
||
}
|
||
|
||
// IsTaskInRequestedClassScope 判断 task_item 是否属于“本轮请求涉及的任务类范围”。
|
||
//
|
||
// 说明:
|
||
// 1. task_class_ids 为空时,视为不做范围裁剪,统一返回 true;
|
||
// 2. 仅 source=task_item 才有 task_class_id 语义,event 不参与该判断;
|
||
// 3. 迁移期若 task_item 缺失 TaskClassID,则在有显式 scope 时按“不在范围内”处理,
|
||
// 避免把域外 pending 误混进本轮粗排/微调。
|
||
func IsTaskInRequestedClassScope(task ScheduleTask, taskClassIDs []int) bool {
|
||
if len(taskClassIDs) == 0 {
|
||
return true
|
||
}
|
||
if task.Source != "task_item" {
|
||
return false
|
||
}
|
||
return task.TaskClassID > 0 && slices.Contains(taskClassIDs, task.TaskClassID)
|
||
}
|
||
|
||
// FilterScheduleStateForTaskClassScope 按“本轮请求的任务类范围”裁剪工具态里的域外 pending。
|
||
//
|
||
// 步骤说明:
|
||
// 1. existing / suggested 一律保留,因为它们已经是事实层或建议层落位,会参与冲突判断;
|
||
// 2. 仅移除“域外真实 pending”,避免粗排校验和读工具把别的任务类误算进来;
|
||
// 3. TaskClasses 元数据也同步按 scope 裁剪,避免 prompt/工具读到无关约束;
|
||
// 4. 这里做就地裁剪,调用方无需再维护第二份 scoped state。
|
||
func FilterScheduleStateForTaskClassScope(state *ScheduleState, taskClassIDs []int) {
|
||
if state == nil || len(taskClassIDs) == 0 {
|
||
return
|
||
}
|
||
|
||
filteredTasks := make([]ScheduleTask, 0, len(state.Tasks))
|
||
for _, task := range state.Tasks {
|
||
if !IsPendingTask(task) {
|
||
filteredTasks = append(filteredTasks, task)
|
||
continue
|
||
}
|
||
if IsTaskInRequestedClassScope(task, taskClassIDs) {
|
||
filteredTasks = append(filteredTasks, task)
|
||
}
|
||
}
|
||
state.Tasks = filteredTasks
|
||
|
||
if len(state.TaskClasses) == 0 {
|
||
return
|
||
}
|
||
filteredMetas := make([]TaskClassMeta, 0, len(state.TaskClasses))
|
||
for _, meta := range state.TaskClasses {
|
||
if slices.Contains(taskClassIDs, meta.ID) {
|
||
filteredMetas = append(filteredMetas, meta)
|
||
}
|
||
}
|
||
state.TaskClasses = filteredMetas
|
||
}
|