mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
fix(scheduler)!: D1 更正为「中断从上一个任务之前的完整状态开始」,并实现现场合回
用户明确语义(我此前对 D1 的解析就是错的——当时回答里的“A”指的是 git 选项, D1 实际要的是方案 B): 中断打断时,上个任务到达以来的所有上下文现场被保护(含 toolcall), 然后中断在「上个任务前的那个完整状态」上开始运行; 中断结束后再把被挂起的任务与其上下文现场加载回中断任务之上,并继续运行。 实现: - 删除 SeedMsgs 与 D1=A 的“只读前缀”机制:中断任务不再继承被打断任务的任何内容, 它就是普通新任务,正常走完整 prepare(system prompt + timeline + 自己的输入) - TaskFrame 新增 PrefixLen(基础前缀长度)与 InputBlocks; stepPrepare 在 buildMessages 之后记录 PrefixLen - 新增 rebaseFramePrefix:恢复时重建基础前缀(中断已提交进 a.context, 重建的 timeline 含中断效果=“加载回中断之上”),再把本任务自己的尾部 (Stage 上下文 + 工具轮产物 + 占位)接回;并补回 IsInterrupt 标记与多模态块 - resumeTask 在 runTaskSteps 之前调用 rebaseFramePrefix - 设计稿 §5.3 改写为「已定:D1=B」并写明实现对应;§6.2 补“重建前缀→接回尾部”; §12 的 D1 行更新 测试: - TestPreempt_HigherPreemptsAndResumes 改为断言「中断不继承、恢复后看得见中断内容」 - 新增 TestPreempt_ResumeRebaseRestoresTailDecorations(前缀重建后尾部装饰补回) - 原 TestPreempt_SeedPathDoesNotLeakInterruptFlag 随之删除(机制已不存在) 验收:agent 全量 + -race;全仓 build/vet 通过
This commit is contained in:
@ -203,14 +203,35 @@ CriticalSection:step 标记 nonPreemptible = true,或任务进入声明区
|
||||
| `_consolidation_` 中 | 降级为排队 | `_consolidation_` 是后台临界区 → B 入 `pendingInterrupts`,临界区结束后求值 |
|
||||
| `a.interceptCh` 满 | 降级为排队 | 不存在该队列;`pendingInterrupts` 有界丢弃 |
|
||||
|
||||
### 5.3 中断任务与被打断任务的关系(**待确认 D1**)
|
||||
### 5.3 中断任务与被打断任务的关系(**已定:D1 = 方案 B**)
|
||||
|
||||
两选一:
|
||||
> 用户明确:
|
||||
> *“中断打断时,上个任务到达以来的所有上下文现场被保护(含 toolcall),
|
||||
> 然后中断在**上个任务前的那个完整状态**上开始运行。中断运行结束,再把被挂起的
|
||||
> 任务与其上下文现场**加载回中断任务之上**,并继续运行。”*
|
||||
|
||||
- **方案 A(推荐)**:中断任务以**运行任务帧的只读前缀**为上下文(模型能看到"当前进行到哪一步"),但其产出**不合并回**被打断任务;被打断任务恢复后,对中断内容一无所知(除了可选的一行机器生成的提示)。
|
||||
- **方案 B**:中断任务只看到**任务开始前的持久状态**(提交点在 `S_COMMIT`,所以 Context 层天然停在 pre-task 状态)。代价:模型失去"进行中"的上下文,"别搜了改成 X"这类指令难以处理。
|
||||
因此语义是:
|
||||
|
||||
> D1 默认取 **方案 A**:可用性更好,且不违反"帧由调度器独占"。
|
||||
1. **被挂起任务的现场 = 它自到达以来累积的全部上下文(含 toolcall 结果)**,
|
||||
原样保存在 `TaskFrame` 里。
|
||||
2. **中断任务从「上一个任务之前的完整状态」开始运行**——它**看不到**被打断
|
||||
任务的任何部分进展。等价于:中断任务就是一个普通新任务,正常走 `S_PREPARE`
|
||||
(重建 system prompt + timeline + 自己的输入)。
|
||||
3. **中断结束后,把被挂起任务与其现场加载回「中断任务之上」再继续**:
|
||||
中断已提交的那段上下文留在**下面**(成为重建前缀的一部分),本任务自己的
|
||||
现场接回**其上**。
|
||||
|
||||
实现对应(`internal/agent/core/task.go`):
|
||||
|
||||
- `TaskFrame.PrefixLen` 记录 prepare 段构建的**基础前缀**长度
|
||||
(system + timeline + 用户输入);其后的 Stage 上下文与工具轮产物都是“自己的现场”。
|
||||
- `rebaseFramePrefix(f)`:恢复时重建基础前缀(因中断结束已把它的输入/输出提交进
|
||||
`a.context`,重建出的 timeline 已含中断效果),再把 `f.Msgs[PrefixLen:]` 原样接回;
|
||||
并补回 prepare 段的両处尾部改写(`IsInterrupt` 的 `[中断消息]` 标记、输入多模态块)。
|
||||
- 调用点:`resumeTask` 在 `runTaskSteps` **之前**调用它。
|
||||
|
||||
> 代价(已知且接受):中断看不到“进行到哪一步”,所以“别搜了改成 X”这类指令
|
||||
> 只能靠它自己重新理解;换来的是中断起点总是一个**一致的完整状态**。
|
||||
|
||||
---
|
||||
|
||||
@ -232,7 +253,15 @@ running.state = done_for_now
|
||||
|
||||
### 6.2 恢复
|
||||
|
||||
从 `suspendPool` 取出后,从 `frame.step` 继续执行;`frame.msgs` 原样复用。
|
||||
从 `suspendPool` 取出后:
|
||||
|
||||
1. **重建基础前缀**(`rebaseFramePrefix`)—— 此时中断任务已结束并提交,
|
||||
重建出的 timeline 包含中断的输入/输出,即“现场加载回中断任务之上”;
|
||||
2. 把本任务自己的尾部(Stage 上下文 + 工具轮产物 + 占位)原样接回;
|
||||
3. 从 `frame.Step` 继续执行。
|
||||
|
||||
被丢弃的只有那次**不完整的 LLM 请求**(幂等),已执行的工具与已累积的
|
||||
`toolResults` 全部保留。
|
||||
|
||||
### 6.3 嵌套
|
||||
|
||||
@ -404,7 +433,7 @@ v1 采纳:**`S_TOOL_EXEC` / ONNX / CAS 属于临界区,调度器在这些 st
|
||||
|
||||
| 编号 | 问题 | 默认取值 |
|
||||
|---|---|---|
|
||||
| **D1** | 中断任务的上下文:方案 A(可见运行任务的只读前缀)还是方案 B(只见 pre-task 持久状态) | **A** |
|
||||
| **D1** | 中断任务的上下文 | **方案 B(已定)**:中断从上一个任务之前的完整状态开始;恢复时把被挂起任务的现场加载回中断之上 |
|
||||
| **D2** | 阻塞 step 处置:v1 全部声明为临界区(调度器可被阻塞)还是引入异步 step | **v1 = 临界区**;异步 step 留到 v2 |
|
||||
| **D3** | `pendingInterrupts` 与 `readyQueue` 是否合一 | **保持分离**(中断请求带 interrupt 语义,取出时以中断语义启动);但**共用同一个排序键** |
|
||||
| **D4** | readyQueue 满时:阻塞发送方 or 返回错误 | **阻塞发送方 + 计数**(与现状一致,避免丢用户输入) |
|
||||
|
||||
Reference in New Issue
Block a user