Version: 0.9.5.dev.260407

后端:
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 的根因
前端:无 仓库:无
This commit is contained in:
Losita
2026-04-07 23:58:00 +08:00
parent 07d307fe07
commit cdedd3c968
14 changed files with 599 additions and 174 deletions

View File

@@ -0,0 +1,118 @@
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
}