Version: 0.9.75.dev.260505

后端:
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 迁移面
This commit is contained in:
Losita
2026-05-05 16:00:57 +08:00
parent e1819c5653
commit d7184b776b
174 changed files with 2189 additions and 1236 deletions

View File

@@ -0,0 +1,272 @@
package schedule
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)
}