Files
smartmate/backend/newAgent/tools/SCHEDULE_TOOLS.md
LoveLosita 821c2cde5d Version: 0.9.10.dev.260409
后端:
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. 同步更新调试日志文件
前端:无
仓库:无
2026-04-09 16:17:56 +08:00

22 KiB
Raw Blame History

日程工具设计文档

本文档定义了 newAgent 日程调度场景下的工具层设计。 工具是 LLM 与日程数据之间的唯一边界——LLM 只能通过工具的输入/输出与日程交互,永远不直接接触原始数据。


1. 设计原则

  1. 工具即边界LLM 通过工具感知和修改日程,不直接接触 state 或数据库
  2. 自然语言返回:工具返回值为自然语言 + 轻结构缩进、列表LLM 直接理解
  3. 只报事实,不做判断:读工具只报当前真实状态,不附建议/推荐/假设;写工具只报变更后的事实
  4. 操作前自动校验:写工具在执行前自动检测冲突和锁定,失败时 state 不变
  5. State 内操作:所有写工具只修改内存中的 state不直接写库整个方案完成后由 Confirm 节点统一写库

2. 索引体系

LLM 不接触真实的日期和星期,使用两级整数索引:

  • 天索引day:规划窗口内的天数编号,从 1 开始连续递增

    • 例如规划窗口为第1周周三至第3周周一共13天编号为第1天~第13天
    • 工具层负责 day ↔ 真实日期 的映射
    • 规划窗口由 Plan 节点向用户确认,不明确时走 ask_user
  • 时段索引slot:每天内的节课编号,范围 1-12

    • 标准节次1-2, 3-4, 5-6, 7-8, 9-10, 11-12共6个标准段
    • 连堂课可能跨越1-33连堂、1-44连堂、9-124连堂
    • 任务时段用 (slot_start, slot_end) 表示,例如 (1, 4) = 第1-4节
  • 任务定位day + slot_start + slot_end例如 (3, 1, 4) = 第3天第1-4节


3. State 数据结构

State 是工具层的操作对象,存在于内存中,不直接暴露给 LLM。

3.1 整体结构

{
  "window": {
    "total_days": 13,
    "day_mapping": [
      { "day_index": 1,  "week": 5, "day_of_week": 1 },
      { "day_index": 2,  "week": 5, "day_of_week": 2 },
      { "day_index": 3,  "week": 5, "day_of_week": 3 },
      { "day_index": 4,  "week": 5, "day_of_week": 4 },
      { "day_index": 5,  "week": 5, "day_of_week": 5 },
      { "day_index": 6,  "week": 5, "day_of_week": 6 },
      { "day_index": 7,  "week": 5, "day_of_week": 7 },
      { "day_index": 8,  "week": 6, "day_of_week": 1 },
      { "day_index": 9,  "week": 6, "day_of_week": 2 },
      { "day_index": 10, "week": 6, "day_of_week": 3 },
      { "day_index": 11, "week": 6, "day_of_week": 4 },
      { "day_index": 12, "week": 6, "day_of_week": 5 },
      { "day_index": 13, "week": 6, "day_of_week": 6 }
    ]
  },
  "tasks": [
    {
      "state_id": 1,
      "source": "event",
      "source_id": 101,
      "name": "高等数学",
      "category": "课程",
      "status": "existing",
      "locked": true,
      "slots": [
        { "day": 1, "slot_start": 1, "slot_end": 2 },
        { "day": 4, "slot_start": 1, "slot_end": 2 },
        { "day": 8, "slot_start": 1, "slot_end": 2 }
      ]
    },
    {
      "state_id": 2,
      "source": "event",
      "source_id": 102,
      "name": "思政(水课)",
      "category": "课程",
      "status": "existing",
      "locked": false,
      "can_embed": true,
      "slots": [
        { "day": 2, "slot_start": 1, "slot_end": 2 }
      ]
    },
    {
      "state_id": 3,
      "source": "task_item",
      "source_id": 201,
      "name": "复习线代",
      "category": "学习",
      "status": "pending",
      "duration": 3,
      "category_id": 10
    }
  ]
}

3.2 字段说明

任务通用字段:

字段 类型 说明
state_id int State 内唯一 ID递增工具层和 LLM 使用此 ID 交互
source string 数据来源:"event" = 来自 ScheduleEvent"task_item" = 来自 TaskClassItem
source_id int 原表主键ScheduleEvent.ID 或 TaskClassItem.ID写库时用于反查
name string 任务名称,来自 ScheduleEvent.Name 或 TaskClassItem.Content
category string 类别名,来自 TaskClass.Name如"课程"、"学习"、"作业"
status string "existing"(已安排/已确定)
locked bool 是否锁定。推导规则ScheduleEvent.Type="course" 且 CanBeEmbed=false 时为 true
slots array 已安排任务的时段列表,每项含 day/slot_start/slot_end
duration int 待安排/已预排任务需要的连续时段数pending / suggested 任务常见)
category_id int 所属 TaskClass 的 ID仅 source=task_item 时有值)

嵌入任务相关字段(仅 can_embed=true 的任务):

字段 类型 说明
can_embed bool 该时段是否允许嵌入其他任务,来自 ScheduleEvent.CanBeEmbedded
embedded_by int 被哪个 state_id 的任务嵌入(宿主视角)
embed_host int 嵌入到哪个 state_id 的时段里(嵌入任务视角)

3.3 数据来源与映射

existing 任务(从数据库加载):

State 字段 数据库来源
source_id ScheduleEvent.ID
name ScheduleEvent.Name
category ScheduleEvent.Type"course"→"课程""task"→取关联 TaskClass.Name
locked ScheduleEvent.Type="course" 且 CanBeEmbedded=false
can_embed ScheduleEvent.CanBeEmbedded
slots 查 Schedule 表WHERE event_id=? AND week/day_of_week IN 窗口范围),按 section 连续段压缩

pending 任务(从数据库加载):

State 字段 数据库来源
source_id TaskClassItem.ID
name TaskClassItem.Content
category 关联 TaskClass.Name通过 CategoryID
duration 由 TaskClass.TotalSlots / Item 数量推算,或固定为 2
category_id TaskClassItem.CategoryID

3.4 Section 压缩/解压

数据库中 Schedule 表逐节存储每节一条记录State 中压缩为连续范围:

DB 记录:
  Schedule(event_id=101, week=5, day_of_week=1, section=1)
  Schedule(event_id=101, week=5, day_of_week=1, section=2)

压缩为 State
  { "day": 1, "slot_start": 1, "slot_end": 2 }

反向操作(写库时):将 slot_start/slot_end 展开为逐条 Schedule 记录插入。

3.5 Day 映射

工具层通过 day_mapping 数组完成 day_index ↔ (week, day_of_week) 的双向转换:

  • 读操作:从 Schedule 表查到 (week=5, day_of_week=1, section=1),通过 day_mapping 反查 day_index=1
  • 写操作LLM 指定 day=3通过 day_mapping 查到 (week=5, day_of_week=3),用于构造 Schedule 记录
  • 规划窗口:由 Plan 节点确认范围,工具层初始化时生成 day_mapping

4. 读工具

4.1 get_overview

获取规划窗口总览(任务视角,全量返回)。

行为约束:

  • 保留课程占位统计例如“第1天占2/12”避免误判可用空间。
  • 每日明细只展开任务(非课程),课程不进入任务明细列表。
  • 在当前阶段(窗口通常不超过 30 天)直接全量返回,不做截断。

入参:

返回示例:

规划窗口共13天每天12个时段总计156个时段。
当前已占用48个空闲108个。课程占位条目7个仅用于占位统计任务条目已安排(existing)1个、已预排(suggested)2个、待安排(pending)3个。

每日概况:
第1天总占6/12课程占6/12任务占0/12 — 任务:无
第2天总占2/12课程占2/12任务占0/12 — 任务:无
第3天总占2/12课程占0/12任务占2/12 — 任务:[35]第一章随机事件与概率(suggested,第5-6节)
第4天总占4/12课程占2/12任务占2/12 — 任务:[36]第二章随机变量(suggested,第7-8节)
...

任务清单(全量,已过滤课程):
[35]第一章随机事件与概率 | 状态:suggested | 类别:概率论 | 时段:第3天(5-6节)
[36]第二章随机变量 | 状态:suggested | 类别:概率论 | 时段:第4天(7-8节)
[37]第三章多维随机变量 | 状态:pending | 类别:概率论 | 需2个连续时段

4.2 query_range

查看某天(或某天某段)的细粒度占用详情。

入参:

字段 类型 必填 说明
day int 天索引
slot_start int 起始节次,不传则返回整天
slot_end int 结束节次,不传则返回整天

返回示例(查整天):

第4天 全天:

第1-2节[1]高等数学(固定)
第3-4节[6]线代
第5-6节空
第7-8节空
第9-10节[8]程序设计
第11-12节空

连续空闲区第5-8节(4时段)、第11-12节(2时段)
可嵌入第1-2节已有[1]高等数学(固定,不可嵌入)

返回示例(查具体范围):

第4天 第5-8节

第5节空
第6节空
第7节空
第8节空

该范围4个时段全部空闲。

4.3 query_available_slots

查询候选坑位池(结构化返回):默认先返回“纯空位”,不足时再补“可嵌入位”。

入参:

字段 类型 必填 说明
span / duration int 目标连续时段长度,默认 2
limit int 返回候选上限,默认 12
allow_embed bool 是否允许补可嵌入位,默认 true
day / day_start / day_end int 天级范围过滤(day 与区间互斥)
day_scope string all / workday / weekend
day_of_week []int 星期过滤1-7
week / week_filter / week_from / week_to int / []int 周级过滤
slot_type / slot_types string / []string pure/empty/strict 会强制只返回纯空位
exclude_sections []int 排除节次1-12
after_section / before_section int 只返回区间之后/之前的候选
section_from + section_to int 精确节次区间查询(需同时提供)

返回示例:

{
  "tool": "query_available_slots",
  "count": 12,
  "strict_count": 8,
  "embedded_count": 4,
  "fallback_used": true,
  "day_scope": "all",
  "day_of_week": [],
  "week_filter": [12],
  "week_from": 12,
  "week_to": 12,
  "span": 2,
  "allow_embed": true,
  "exclude_sections": [],
  "slots": [
    {
      "day": 5,
      "week": 12,
      "day_of_week": 3,
      "slot_start": 1,
      "slot_end": 2,
      "slot_type": "empty"
    }
  ]
}

4.4 list_tasks

列出任务清单,可按类别和状态过滤。

入参:

字段 类型 必填 说明
category string 过滤类别(对应 TaskClass.Name如"课程"、"学习";不支持 task_class_ids 列表)
status string existing / suggested / pending / all默认 all仅支持单值不支持 existing,suggested 这类拼接)

返回示例(待安排):

待安排任务共3个

[3]复习线代 — 需3个连续时段类别学习
[7]写实验报告 — 需2个连续时段类别作业
[9]小组讨论 — 需2个连续时段类别学习

返回示例(全部):

共9个任务已安排6个待安排3个。

已安排:
  [1]高等数学(课程,固定) — 第1天(1-2节) 第4天(1-2节) 第8天(1-2节)
  [2]英语(课程) — 第1天(3-4节) 第6天(1-2节)
  [4]体育(课程) — 第1天(5-6节)
  [5]物理(课程) — 第2天(3-4节) 第8天(3-4节)
  [6]线代(学习) — 第4天(3-4节)
  [8]程序设计(课程) — 第4天(9-10节)
  [10]思政(课程,可嵌入) — 第7天(1-2节)

待安排:
  [3]复习线代(学习) — 需3时段
  [7]写实验报告(作业) — 需2时段
  [9]小组讨论(学习) — 需2时段

4.5 get_task_info

查询单个任务的详细信息。

入参:

字段 类型 必填 说明
task_id int 任务 ID

返回示例(普通任务):

[1]高等数学
类别:课程 | 状态:已安排(固定)
来源:课程表
占用时段:
  第1天 第1-2节
  第4天 第1-2节
  第8天 第1-2节

返回示例(可嵌入任务):

[10]思政
类别:课程 | 状态:已安排
来源:课程表
可嵌入:是(允许在此时段嵌入其他任务)
占用时段:
  第7天 第1-2节
当前嵌入任务:无

5. 写工具

5.1 place

将待安排任务预排到指定位置。

入参:

字段 类型 必填 说明
task_id int 待安排任务的 ID
day int 目标天索引
slot_start int 目标起始节次

成功返回:

已将 [3]复习线代 放到第5天第1-3节。
第5天当前占用[3]复习线代(1-3节)占用3/12。
待安排任务剩余2个。

失败返回(冲突):

放置失败第5天第1-2节已被 [4]体育 占用。
第5天当前占用[4]体育(1-4节)占用4/12。空闲时段第5-12节。

失败返回(状态错误):

放置失败:[1]高等数学 不是待安排任务,无法放置。

成功返回(嵌入到水课):

已将 [7]写实验报告 嵌入到第7天第1-2节宿主[10]思政)。
第7天当前占用[10]思政(1-2节) [7]写实验报告(嵌入1-2节)占用2/12。
待安排任务剩余2个。

5.2 move

移动已预排任务(仅 suggested到新位置。

入参:

字段 类型 必填 说明
task_id int 任务 ID
new_day int 目标天索引
new_slot_start int 目标起始节次

成功返回:

已将 [6]线代 从第4天第3-4节移至第9天第1-2节。
第4天当前占用[1]高等数学(1-2节) [8]程序设计(9-10节)占用4/12。
第9天当前占用[6]线代(1-2节)占用2/12。

失败返回(冲突):

移动失败第9天第1-2节已被 [9]小组讨论 占用。
第9天当前占用[9]小组讨论(1-2节)占用2/12。空闲时段第3-12节。

失败返回(锁定):

移动失败:[1]高等数学 是固定课程,不可移动。

失败返回(状态错误):

移动失败:[3]复习线代 当前为待安排状态,请使用 place 放置。
移动失败:[2]英语 当前为已安排existing任务不允许 move仅 suggested 任务可移动。

5.3 swap

交换两个已落位任务的位置。

入参:

字段 类型 必填 说明
task_a int 任务 A 的 ID
task_b int 任务 B 的 ID

成功返回:

交换完成:
  [2]英语第1天第3-4节 → 第6天第1-2节
  [6]线代第6天第1-2节 → 第1天第3-4节
第1天当前占用[1]高等数学(1-2节) [6]线代(3-4节) [4]体育(5-6节)占用6/12。
第6天当前占用[2]英语(1-2节)占用2/12。

失败返回(时长不匹配):

交换失败:[5]物理 占4个时段[2]英语 占2个时段时长不同无法直接交换。

失败返回(任一任务锁定):

交换失败:[1]高等数学 是固定课程,不可交换。

5.4 batch_move

批量原子移动多个任务(仅 suggested单次最多 2 条),要么全部成功,要么全部回滚。

入参:

字段 类型 必填 说明
moves array 每项包含 task_id, new_day, new_slot_start

成功返回:

批量移动完成2个任务全部成功
  [2]英语 → 第3天第1-2节
  [6]线代 → 第5天第3-4节
第3天当前占用[2]英语(1-2节)占用2/12。
第5天当前占用[6]线代(3-4节)占用2/12。

失败返回(超出上限):

批量移动失败:当前最多支持 2 条移动请求。请改用队列化逐项处理queue_pop_head + queue_apply_head_move

失败返回:

批量移动失败,全部回滚,无任何变更。
冲突:[6]线代 → 第5天第3-4节该位置已被 [3]复习线代(1-3节) 占用。

5.5 unplace

将已落位任务恢复为待安排状态。

入参:

字段 类型 必填 说明
task_id int 任务 ID

成功返回:

已将 [3]复习线代 从第5天第1-3节移除恢复为待安排状态。
第5天当前占用0/12。
待安排任务剩余1个。

失败返回(锁定):

移除失败:[1]高等数学 是固定课程,不可移除。

5.6 min_context_switch

在给定任务集合内重排 suggested 任务,尽量把同类任务排成连续块,以减少上下文切换。

使用约束:

  • 仅在用户明确说明“允许打乱顺序”时调用。
  • 仅支持 suggested 且已落位任务。
  • 工具只在传入集合内部重排,不会主动改动集合外任务。

入参:

字段 类型 必填 说明
task_ids array[int] 参与重排的任务 ID 列表(至少 2 个)
task_id int 兼容单值参数,不建议新调用使用

成功返回:

最少上下文切换重排完成:共处理 6 个任务,上下文切换次数 5 -> 2。
本次调整:
  [35]概率第一章第3天(星期3)第1-2节 -> 第2天(星期2)第5-6节
  [41]概率第二章第4天(星期4)第1-2节 -> 第3天(星期3)第1-2节
第2天当前占用...
第3天当前占用...
第4天当前占用...

失败返回(未授权顺序重排时应由上层拦截):

已拒绝执行 min_context_switch当前未授权打乱顺序。如需使用该工具请先由用户明确说明“允许打乱顺序”。

5.7 queue_pop_head

弹出并返回当前队首任务;若已有 current 则复用,保证一次只处理一个任务。

入参:

返回示例:

{"tool":"queue_pop_head","has_head":true,"pending_count":5,"current":{"task_id":35,"name":"示例任务","status":"suggested","slots":[{"day":3,"week":12,"day_of_week":1,"slot_start":5,"slot_end":6}]}}

5.8 queue_apply_head_move

将当前队首任务移动到指定位置并自动出队。仅作用于 current不接受 task_id。

入参:

字段 类型 必填 说明
new_day int 目标 day
new_slot_start int 目标起始节次

返回示例:

{"tool":"queue_apply_head_move","success":true,"task_id":35,"pending_count":4,"completed_count":2,"result":"已将 [35]... 从第3天第5-6节移至第5天第3-4节。"}

5.9 queue_skip_head

跳过当前队首任务(不改日程),标记为 skipped 并继续后续队列。

入参:

字段 类型 必填 说明
reason string 跳过原因

返回示例:

{"tool":"queue_skip_head","success":true,"skipped_task_id":35,"pending_count":4,"skipped_count":1}

5.10 queue_status

查看当前待处理队列状态pending/current/completed/skipped

入参:

返回示例:

{"tool":"queue_status","pending_count":5,"completed_count":1,"skipped_count":0,"current_task_id":35,"current_attempt":1}

5.11 spread_even

在给定任务集合内执行“均匀化铺开”:
先按筛选条件收集候选坑位,再用确定性规划器生成移动方案并原子提交。

入参:

字段 类型 必填 说明
task_ids array[int] 参与均匀化的任务 ID 列表(至少 2 个)
task_id int 兼容单值参数,不建议新调用使用
day/day_start/day_end int 天级范围过滤
day_scope string all / workday / weekend
day_of_week array[int] 星期过滤1~7
week/week_filter/week_from/week_to int/array[int] 周级过滤
limit int 每个跨度的候选坑位上限(内部会按任务数自动放大)
allow_embed bool 是否允许补充可嵌入位,默认 true
exclude_sections array[int] 排除节次
after_section/before_section int 节次边界过滤

成功返回:

均匀化调整完成:共处理 6 个任务,候选坑位 24 个。
本次调整:
  [35]第一章复习第3天(星期3)第5-6节 -> 第5天(星期5)第1-2节
  [41]第二章练习第4天(星期4)第5-6节 -> 第6天(星期6)第1-2节
第5天当前占用...
第6天当前占用...

失败返回(候选不足):

均匀化调整失败:跨度=2 可用坑位不足required=4, got=2。

6. 公共规则

冲突检测

  • 所有写操作执行前自动检测目标位置是否冲突
  • 冲突时拒绝操作,返回冲突任务名称和占用节次
  • state 保持不变

锁定保护

  • locked=true 的任务move / swap / unplace 直接拒绝
  • place 新任务到锁定时段同样拒绝

状态约束

  • pending 任务只能 place不能 move / swap / unplace
  • suggested 任务可以 move / swap / unplace / spread_even / min_context_switch
  • existing 任务不能 move / batch_move / spread_even / min_context_switch仅作已安排事实层
  • 状态不符时返回明确错误信息

返回格式

  • 返回值为自然语言 + 轻结构(缩进、列表)
  • 占用信息始终附带每个任务的具体节次范围
  • 读工具只报当前真实状态,不做假设
  • 写工具只报变更后的事实,不附建议

ID 规范

  • LLM 可见的任务 ID 为 state_id(递增整数),不暴露 source/source_id
  • state_id 由工具层在加载 state 时分配,不区分来源
  • source + source_id 为内部字段,仅在写库时使用,不对 LLM 可见

嵌入任务规则

  • can_embed=true 的任务(水课)允许其他任务嵌入到同一时段
  • 嵌入任务占位时不触发冲突检测(与宿主共存)
  • query_available_slots 返回候选坑位池(先纯空位,必要时补可嵌入位)
  • place 到可嵌入时段时,若已有宿主任务,自动标记 embed_host 关系
  • 嵌入任务的 locked 继承宿主:宿主不可移动时,嵌入任务也不可单独移动

数据库交互

  • State 初始化:从 Schedule + ScheduleEvent 加载 existing 任务,从 TaskClassItem 加载 pending 任务;粗排或工具预排成功后,任务转为 suggested
  • State 落库Confirm 节点统一处理,将 state 变更转换为 Schedule/ScheduleEvent/TaskClassItem 的增删改
  • 落库时使用 source + source_id 定位原记录,使用 day_mapping 将 day_index 转回 (week, day_of_week)
  • 落库时将 (slot_start, slot_end) 展开为逐条 Schedule 记录