后端: 1. newAgent 运行态重置双保险落地,并补齐写工具后的实时排程预览刷新 - 更新 model/common_state.go:新增 ResetForNextRun,统一清理 round/plan/rough_build/allow_reorder/terminal 等执行期临时状态 - 更新 node/chat.go + service/agentsvc/agent_newagent.go:在“无 pending 且上一轮已 done”时分别于 chat 主入口与 loadOrCreateRuntimeState 冷加载处执行兜底重置,覆盖正常新一轮对话与断线恢复场景 - 更新 model/graph_run_state.go + node/agent_nodes.go + node/execute.go:写工具执行后立即刷新 Redis 排程预览,Deliver 继续保留最终覆盖写,保证前端能及时看到最新操作结果 2. 顺序守卫从“直接中止”改为“优先自动复原 suggested 相对顺序” - 更新 node/order_guard.go:检测到 suggested 顺序被打乱后,不再直接 abort;改为复用当前坑位按 baseline 自动回填,并在复原失败时仅记录诊断日志后继续交付 - 更新 tools/state.go:ScheduleState 新增 RuntimeQueue 运行态快照字段,支持队列化处理与断线恢复 3. 多任务微调工具链升级:新增筛选/队列工具并替换首空位查询口径 - 新建 tools/read_filter_tools.go + tools/runtime_queue.go + tools/queue_tools.go:新增 query_available_slots / query_target_tasks / queue_pop_head / queue_apply_head_move / queue_skip_head / queue_status,支持“先筛选目标,再逐项处理”的稳定微调链路 - 更新 tools/registry.go + tools/write_tools.go + tools/read_helpers.go:移除 find_first_free 注册口径;batch_move 限制为最多 2 条,超过时引导改走队列逐项处理;queue_apply_head_move 纳入写工具集合 4. 复合规划工具扩充,并改为在 newAgent/tools 本地实现以规避循环导入 - 更新 tools/compound_tools.go + tools/registry.go:spread_even 正式接入,并与 min_context_switch 一起作为复合写工具保留在 newAgent/tools 内部实现,不再依赖外层 logic 5. prompt 与工具文档同步升级,明确当前用户诉求锚点与队列化执行约束 - 更新 prompt/execute.go + prompt/execute_context.go + prompt/plan.go:执行提示默认引导 query_target_tasks(enqueue=true) → queue_pop_head → query_available_slots → queue_apply_head_move / queue_skip_head;补齐 batch_move 上限、spread_even 使用边界、顺序策略与工具 JSON 返回示例 - 更新 prompt/execute_context.go:将“初始用户目标”改为“当前用户诉求”,并保留首轮目标来源;旧 observation 折叠文案改为“当前工具调用结果已经被使用过,当前无需使用,为节省上下文空间,已折叠” - 更新 tools/SCHEDULE_TOOLS.md:同步补齐 query_* / queue_* / spread_even / min_context_switch 的说明、限制与返回示例 6. 同步更新调试日志文件 前端:无 仓库:无
273 lines
9.3 KiB
Go
273 lines
9.3 KiB
Go
package newagenttools
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
type queueTaskSlot struct {
|
||
Day int `json:"day"`
|
||
Week int `json:"week"`
|
||
DayOfWeek int `json:"day_of_week"`
|
||
SlotStart int `json:"slot_start"`
|
||
SlotEnd int `json:"slot_end"`
|
||
}
|
||
|
||
type queueTaskItem struct {
|
||
TaskID int `json:"task_id"`
|
||
Name string `json:"name"`
|
||
Category string `json:"category,omitempty"`
|
||
Status string `json:"status"`
|
||
Duration int `json:"duration,omitempty"`
|
||
TaskClassID int `json:"task_class_id,omitempty"`
|
||
Slots []queueTaskSlot `json:"slots,omitempty"`
|
||
}
|
||
|
||
type queuePopHeadResult struct {
|
||
Tool string `json:"tool"`
|
||
HasHead bool `json:"has_head"`
|
||
PendingCount int `json:"pending_count"`
|
||
CompletedCount int `json:"completed_count"`
|
||
SkippedCount int `json:"skipped_count"`
|
||
Current *queueTaskItem `json:"current,omitempty"`
|
||
LastError string `json:"last_error,omitempty"`
|
||
}
|
||
|
||
type queueApplyHeadMoveResult struct {
|
||
Tool string `json:"tool"`
|
||
Success bool `json:"success"`
|
||
TaskID int `json:"task_id,omitempty"`
|
||
CurrentAttempt int `json:"current_attempt,omitempty"`
|
||
PendingCount int `json:"pending_count"`
|
||
CompletedCount int `json:"completed_count"`
|
||
SkippedCount int `json:"skipped_count"`
|
||
Result string `json:"result"`
|
||
}
|
||
|
||
type queueSkipHeadResult struct {
|
||
Tool string `json:"tool"`
|
||
Success bool `json:"success"`
|
||
SkippedTaskID int `json:"skipped_task_id,omitempty"`
|
||
PendingCount int `json:"pending_count"`
|
||
SkippedCount int `json:"skipped_count"`
|
||
Reason string `json:"reason,omitempty"`
|
||
}
|
||
|
||
type queueStatusResult struct {
|
||
Tool string `json:"tool"`
|
||
PendingCount int `json:"pending_count"`
|
||
CompletedCount int `json:"completed_count"`
|
||
SkippedCount int `json:"skipped_count"`
|
||
CurrentTaskID int `json:"current_task_id,omitempty"`
|
||
CurrentAttempt int `json:"current_attempt,omitempty"`
|
||
LastError string `json:"last_error,omitempty"`
|
||
NextTaskIDs []int `json:"next_task_ids,omitempty"`
|
||
Current *queueTaskItem `json:"current,omitempty"`
|
||
}
|
||
|
||
// QueuePopHead 从队列弹出队首任务(若已有 current 则复用),并返回当前处理对象。
|
||
//
|
||
// 步骤化说明:
|
||
// 1. 先保证队列容器存在,避免空指针;
|
||
// 2. 若 current 已存在,直接复用,确保 apply/skip 前不会切换处理对象;
|
||
// 3. 若 current 为空则从 pending 弹出队首;
|
||
// 4. 若没有可处理任务,返回 has_head=false,由 LLM 收口或重筛选。
|
||
func QueuePopHead(state *ScheduleState, _ map[string]any) string {
|
||
if state == nil {
|
||
return `{"tool":"queue_pop_head","has_head":false,"error":"state is nil"}`
|
||
}
|
||
queue := ensureTaskProcessingQueue(state)
|
||
taskID := popOrGetCurrentTaskID(state)
|
||
|
||
result := queuePopHeadResult{
|
||
Tool: "queue_pop_head",
|
||
HasHead: taskID > 0,
|
||
PendingCount: len(queue.PendingTaskIDs),
|
||
CompletedCount: len(queue.CompletedTaskIDs),
|
||
SkippedCount: len(queue.SkippedTaskIDs),
|
||
LastError: strings.TrimSpace(queue.LastError),
|
||
}
|
||
if taskID > 0 {
|
||
result.Current = buildQueueTaskItem(state, taskID)
|
||
}
|
||
return mustJSON(result, "queue_pop_head")
|
||
}
|
||
|
||
// QueueApplyHeadMove 将当前队首任务移动到指定位置,成功后自动完成并出队。
|
||
//
|
||
// 步骤化说明:
|
||
// 1. 只能处理 current 任务,禁止越级指定 task_id,避免 LLM 绕过队列直接乱改;
|
||
// 2. 成功时标记 completed 并清空 current;
|
||
// 3. 失败时保留 current 并累加 attempt,让 LLM 继续换坑位重试或 skip。
|
||
func QueueApplyHeadMove(state *ScheduleState, args map[string]any) string {
|
||
if state == nil {
|
||
return `{"tool":"queue_apply_head_move","success":false,"result":"state is nil"}`
|
||
}
|
||
queue := ensureTaskProcessingQueue(state)
|
||
currentID := queue.CurrentTaskID
|
||
if currentID <= 0 {
|
||
return mustJSON(queueApplyHeadMoveResult{
|
||
Tool: "queue_apply_head_move",
|
||
Success: false,
|
||
PendingCount: len(queue.PendingTaskIDs),
|
||
CompletedCount: len(queue.CompletedTaskIDs),
|
||
SkippedCount: len(queue.SkippedTaskIDs),
|
||
Result: "队列中没有正在处理的任务。请先调用 queue_pop_head。",
|
||
}, "queue_apply_head_move")
|
||
}
|
||
|
||
newDay, ok := argsInt(args, "new_day")
|
||
if !ok {
|
||
return mustJSON(queueApplyHeadMoveResult{
|
||
Tool: "queue_apply_head_move",
|
||
Success: false,
|
||
TaskID: currentID,
|
||
CurrentAttempt: queue.CurrentAttempts,
|
||
PendingCount: len(queue.PendingTaskIDs),
|
||
CompletedCount: len(queue.CompletedTaskIDs),
|
||
SkippedCount: len(queue.SkippedTaskIDs),
|
||
Result: "缺少必填参数 new_day。",
|
||
}, "queue_apply_head_move")
|
||
}
|
||
newSlotStart, ok := argsInt(args, "new_slot_start")
|
||
if !ok {
|
||
return mustJSON(queueApplyHeadMoveResult{
|
||
Tool: "queue_apply_head_move",
|
||
Success: false,
|
||
TaskID: currentID,
|
||
CurrentAttempt: queue.CurrentAttempts,
|
||
PendingCount: len(queue.PendingTaskIDs),
|
||
CompletedCount: len(queue.CompletedTaskIDs),
|
||
SkippedCount: len(queue.SkippedTaskIDs),
|
||
Result: "缺少必填参数 new_slot_start。",
|
||
}, "queue_apply_head_move")
|
||
}
|
||
|
||
// 1. 真正执行仍复用既有 move 校验链路,避免重复实现一套冲突判断。
|
||
// 2. 失败时仅更新队列 attempt,不改 current,确保同一任务可继续重试。
|
||
resultText := Move(state, currentID, newDay, newSlotStart)
|
||
success := !strings.Contains(resultText, "移动失败")
|
||
if success {
|
||
markCurrentTaskCompleted(state)
|
||
} else {
|
||
bumpCurrentTaskAttempt(state, resultText)
|
||
}
|
||
|
||
queue = ensureTaskProcessingQueue(state)
|
||
return mustJSON(queueApplyHeadMoveResult{
|
||
Tool: "queue_apply_head_move",
|
||
Success: success,
|
||
TaskID: currentID,
|
||
CurrentAttempt: queue.CurrentAttempts,
|
||
PendingCount: len(queue.PendingTaskIDs),
|
||
CompletedCount: len(queue.CompletedTaskIDs),
|
||
SkippedCount: len(queue.SkippedTaskIDs),
|
||
Result: strings.TrimSpace(resultText),
|
||
}, "queue_apply_head_move")
|
||
}
|
||
|
||
// QueueSkipHead 跳过当前队首任务。
|
||
//
|
||
// 职责边界:
|
||
// 1. 只修改队列运行态,不改排程结果;
|
||
// 2. current 必须存在,否则返回失败提示;
|
||
// 3. 跳过后由下一轮 queue_pop_head 继续取下一项。
|
||
func QueueSkipHead(state *ScheduleState, args map[string]any) string {
|
||
if state == nil {
|
||
return `{"tool":"queue_skip_head","success":false,"reason":"state is nil"}`
|
||
}
|
||
queue := ensureTaskProcessingQueue(state)
|
||
currentID := queue.CurrentTaskID
|
||
if currentID <= 0 {
|
||
return mustJSON(queueSkipHeadResult{
|
||
Tool: "queue_skip_head",
|
||
Success: false,
|
||
PendingCount: len(queue.PendingTaskIDs),
|
||
SkippedCount: len(queue.SkippedTaskIDs),
|
||
Reason: "没有可跳过的 current 任务,请先 queue_pop_head。",
|
||
}, "queue_skip_head")
|
||
}
|
||
|
||
reason := ""
|
||
if raw, ok := argsString(args, "reason"); ok {
|
||
reason = strings.TrimSpace(raw)
|
||
}
|
||
markCurrentTaskSkipped(state)
|
||
queue = ensureTaskProcessingQueue(state)
|
||
return mustJSON(queueSkipHeadResult{
|
||
Tool: "queue_skip_head",
|
||
Success: true,
|
||
SkippedTaskID: currentID,
|
||
PendingCount: len(queue.PendingTaskIDs),
|
||
SkippedCount: len(queue.SkippedTaskIDs),
|
||
Reason: reason,
|
||
}, "queue_skip_head")
|
||
}
|
||
|
||
// QueueStatus 查询当前队列状态。
|
||
func QueueStatus(state *ScheduleState, _ map[string]any) string {
|
||
if state == nil {
|
||
return `{"tool":"queue_status","pending_count":0,"completed_count":0,"skipped_count":0,"last_error":"state is nil"}`
|
||
}
|
||
queue := ensureTaskProcessingQueue(state)
|
||
nextIDs := queue.PendingTaskIDs
|
||
if len(nextIDs) > 5 {
|
||
nextIDs = nextIDs[:5]
|
||
}
|
||
|
||
result := queueStatusResult{
|
||
Tool: "queue_status",
|
||
PendingCount: len(queue.PendingTaskIDs),
|
||
CompletedCount: len(queue.CompletedTaskIDs),
|
||
SkippedCount: len(queue.SkippedTaskIDs),
|
||
CurrentTaskID: queue.CurrentTaskID,
|
||
CurrentAttempt: queue.CurrentAttempts,
|
||
LastError: strings.TrimSpace(queue.LastError),
|
||
NextTaskIDs: append([]int(nil), nextIDs...),
|
||
}
|
||
if queue.CurrentTaskID > 0 {
|
||
result.Current = buildQueueTaskItem(state, queue.CurrentTaskID)
|
||
}
|
||
return mustJSON(result, "queue_status")
|
||
}
|
||
|
||
// buildQueueTaskItem 构造队列任务快照,供 pop/status 返回。
|
||
func buildQueueTaskItem(state *ScheduleState, taskID int) *queueTaskItem {
|
||
task := state.TaskByStateID(taskID)
|
||
if task == nil {
|
||
return nil
|
||
}
|
||
item := &queueTaskItem{
|
||
TaskID: task.StateID,
|
||
Name: strings.TrimSpace(task.Name),
|
||
Category: strings.TrimSpace(task.Category),
|
||
Status: buildTaskStatusLabel(*task),
|
||
Duration: task.Duration,
|
||
TaskClassID: task.TaskClassID,
|
||
Slots: make([]queueTaskSlot, 0, len(task.Slots)),
|
||
}
|
||
for _, slot := range task.Slots {
|
||
week, dayOfWeek, ok := state.DayToWeekDay(slot.Day)
|
||
if !ok {
|
||
continue
|
||
}
|
||
item.Slots = append(item.Slots, queueTaskSlot{
|
||
Day: slot.Day,
|
||
Week: week,
|
||
DayOfWeek: dayOfWeek,
|
||
SlotStart: slot.SlotStart,
|
||
SlotEnd: slot.SlotEnd,
|
||
})
|
||
}
|
||
return item
|
||
}
|
||
|
||
func mustJSON(v any, toolName string) string {
|
||
raw, err := json.Marshal(v)
|
||
if err != nil {
|
||
return fmt.Sprintf(`{"tool":"%s","success":false,"error":"json encode failed"}`, toolName)
|
||
}
|
||
return string(raw)
|
||
}
|