后端: 1.收口阶段 6 agent 结构迁移,将 newAgent 内核与 agentsvc 编排层迁入 services/agent - 切换 Agent 启动装配与 HTTP handler 直连 agent sv,移除旧 service agent bridge - 补齐 Agent 对 memory、task、task-class、schedule 的 RPC 适配与契约字段 - 扩展 schedule、task、task-class RPC/contract 支撑 Agent 查询、写入与 provider 切流 - 更新迁移文档、README 与相关注释,明确 agent 当前切流点和剩余 memory 迁移面
185 lines
5.9 KiB
Go
185 lines
5.9 KiB
Go
package schedule
|
||
|
||
import "fmt"
|
||
|
||
// validateLocalOrderForSinglePlacement 校验单个任务落到目标时段后,是否仍满足同任务类内部顺序约束。
|
||
//
|
||
// 职责边界:
|
||
// 1. 只负责“同任务类内部顺序”这一条规则,不负责冲突、锁定、范围合法性;
|
||
// 2. 采用“克隆态 + 假设落位”方式校验,避免直接污染真实 state;
|
||
// 3. 若任务不属于 task_item / 缺少 task_order / 当前无边界约束,直接放行。
|
||
func validateLocalOrderForSinglePlacement(state *ScheduleState, taskID int, targetSlots []TaskSlot) error {
|
||
if len(targetSlots) == 0 {
|
||
return nil
|
||
}
|
||
return validateLocalOrderBatchPlacement(state, map[int][]TaskSlot{
|
||
taskID: cloneScheduleTaskSlots(targetSlots),
|
||
})
|
||
}
|
||
|
||
// validateLocalOrderBatchPlacement 在“多任务同时变更”的假设下做顺序约束校验。
|
||
//
|
||
// 职责边界:
|
||
// 1. 先把所有候选落位一次性写入克隆态,再统一校验,避免批量局部调整时出现伪冲突;
|
||
// 2. 只校验 proposals 中涉及的任务,因为只要这些任务仍处于各自前驱/后继之间,就不会破坏同类整体顺序;
|
||
// 3. 返回首个命中的中文错误,供写工具直接透传给 LLM。
|
||
func validateLocalOrderBatchPlacement(state *ScheduleState, proposals map[int][]TaskSlot) error {
|
||
if state == nil || len(proposals) == 0 {
|
||
return nil
|
||
}
|
||
|
||
clone := state.Clone()
|
||
for taskID, slots := range proposals {
|
||
task := clone.TaskByStateID(taskID)
|
||
if task == nil {
|
||
return fmt.Errorf("顺序约束校验失败:任务ID %d 不存在", taskID)
|
||
}
|
||
task.Slots = cloneScheduleTaskSlots(slots)
|
||
}
|
||
|
||
for taskID := range proposals {
|
||
if err := validateTaskLocalOrderOnState(clone, taskID); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// validateTaskLocalOrderOnState 判断某个任务在当前假设态下,是否仍处于同任务类前驱/后继之间。
|
||
func validateTaskLocalOrderOnState(state *ScheduleState, taskID int) error {
|
||
task := state.TaskByStateID(taskID)
|
||
if task == nil {
|
||
return fmt.Errorf("顺序约束校验失败:任务ID %d 不存在", taskID)
|
||
}
|
||
if !shouldEnforceTaskLocalOrder(*task) || len(task.Slots) == 0 {
|
||
return nil
|
||
}
|
||
|
||
prevTask, nextTask := findTaskClassNeighbors(state, *task)
|
||
targetStartDay, targetStartSlot, _ := earliestScheduleTaskSlot(task.Slots)
|
||
targetEndDay, _, targetEndSlot := latestScheduleTaskSlot(task.Slots)
|
||
|
||
if prevTask != nil && len(prevTask.Slots) > 0 {
|
||
prevEndDay, _, prevEndSlot := latestScheduleTaskSlot(prevTask.Slots)
|
||
if !isStrictlyAfter(targetStartDay, targetStartSlot, prevEndDay, prevEndSlot) {
|
||
return fmt.Errorf(
|
||
"顺序约束不满足:[%d]%s 不能放到%s。它必须晚于同任务类前一个任务 %s 的结束位置(%s)。",
|
||
task.StateID,
|
||
task.Name,
|
||
formatTaskSlotsBriefWithState(state, task.Slots),
|
||
formatTaskLabel(*prevTask),
|
||
formatTaskSlotsBriefWithState(state, prevTask.Slots),
|
||
)
|
||
}
|
||
}
|
||
|
||
if nextTask != nil && len(nextTask.Slots) > 0 {
|
||
nextStartDay, nextStartSlot, _ := earliestScheduleTaskSlot(nextTask.Slots)
|
||
if !isStrictlyBefore(targetEndDay, targetEndSlot, nextStartDay, nextStartSlot) {
|
||
return fmt.Errorf(
|
||
"顺序约束不满足:[%d]%s 不能放到%s。它必须早于同任务类后一个任务 %s 的开始位置(%s)。",
|
||
task.StateID,
|
||
task.Name,
|
||
formatTaskSlotsBriefWithState(state, task.Slots),
|
||
formatTaskLabel(*nextTask),
|
||
formatTaskSlotsBriefWithState(state, nextTask.Slots),
|
||
)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// shouldEnforceTaskLocalOrder 判断任务是否需要参与“同任务类内部顺序”约束。
|
||
func shouldEnforceTaskLocalOrder(task ScheduleTask) bool {
|
||
return task.Source == "task_item" && task.TaskClassID > 0 && task.TaskOrder > 0
|
||
}
|
||
|
||
// findTaskClassNeighbors 查找同任务类中 order 紧邻当前任务的前驱与后继。
|
||
func findTaskClassNeighbors(state *ScheduleState, task ScheduleTask) (prevTask *ScheduleTask, nextTask *ScheduleTask) {
|
||
if state == nil || !shouldEnforceTaskLocalOrder(task) {
|
||
return nil, nil
|
||
}
|
||
|
||
for i := range state.Tasks {
|
||
candidate := &state.Tasks[i]
|
||
if candidate.StateID == task.StateID {
|
||
continue
|
||
}
|
||
if !shouldEnforceTaskLocalOrder(*candidate) {
|
||
continue
|
||
}
|
||
if candidate.TaskClassID != task.TaskClassID {
|
||
continue
|
||
}
|
||
|
||
if candidate.TaskOrder < task.TaskOrder {
|
||
if prevTask == nil || candidate.TaskOrder > prevTask.TaskOrder {
|
||
prevTask = candidate
|
||
}
|
||
continue
|
||
}
|
||
if candidate.TaskOrder > task.TaskOrder {
|
||
if nextTask == nil || candidate.TaskOrder < nextTask.TaskOrder {
|
||
nextTask = candidate
|
||
}
|
||
}
|
||
}
|
||
return prevTask, nextTask
|
||
}
|
||
|
||
func earliestScheduleTaskSlot(slots []TaskSlot) (day int, slotStart int, slotEnd int) {
|
||
if len(slots) == 0 {
|
||
return 0, 0, 0
|
||
}
|
||
best := slots[0]
|
||
for i := 1; i < len(slots); i++ {
|
||
current := slots[i]
|
||
if current.Day < best.Day ||
|
||
(current.Day == best.Day && current.SlotStart < best.SlotStart) ||
|
||
(current.Day == best.Day && current.SlotStart == best.SlotStart && current.SlotEnd < best.SlotEnd) {
|
||
best = current
|
||
}
|
||
}
|
||
return best.Day, best.SlotStart, best.SlotEnd
|
||
}
|
||
|
||
func latestScheduleTaskSlot(slots []TaskSlot) (day int, slotStart int, slotEnd int) {
|
||
if len(slots) == 0 {
|
||
return 0, 0, 0
|
||
}
|
||
best := slots[0]
|
||
for i := 1; i < len(slots); i++ {
|
||
current := slots[i]
|
||
if current.Day > best.Day ||
|
||
(current.Day == best.Day && current.SlotEnd > best.SlotEnd) ||
|
||
(current.Day == best.Day && current.SlotEnd == best.SlotEnd && current.SlotStart > best.SlotStart) {
|
||
best = current
|
||
}
|
||
}
|
||
return best.Day, best.SlotStart, best.SlotEnd
|
||
}
|
||
|
||
func isStrictlyAfter(dayA, slotA, dayB, slotB int) bool {
|
||
if dayA != dayB {
|
||
return dayA > dayB
|
||
}
|
||
return slotA > slotB
|
||
}
|
||
|
||
func isStrictlyBefore(dayA, slotA, dayB, slotB int) bool {
|
||
if dayA != dayB {
|
||
return dayA < dayB
|
||
}
|
||
return slotA < slotB
|
||
}
|
||
|
||
func cloneScheduleTaskSlots(src []TaskSlot) []TaskSlot {
|
||
if len(src) == 0 {
|
||
return nil
|
||
}
|
||
dst := make([]TaskSlot, len(src))
|
||
copy(dst, src)
|
||
return dst
|
||
}
|