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:
JianFeeeee
2026-09-13 06:24:20 +08:00
parent 8372f5bd8f
commit d4764e3682
8 changed files with 159 additions and 80 deletions

View File

@ -51,7 +51,7 @@ func TestLifecycle_NormalCommitsOnceAndReplies(t *testing.T) {
a := newLifecycleAgent(t, sp, bus, NewStageHost())
evt, respCh := textEvent("cli", "你好")
if _, out := a.runInputTask(evt, nil); out != outcomeDone {
if _, out := a.runInputTask(evt); out != outcomeDone {
t.Fatalf("runInputTask=%v期望 outcomeDone", out)
}
@ -90,13 +90,13 @@ func TestLifecycle_DuplicateSkippedHasTerminal(t *testing.T) {
a := newLifecycleAgent(t, sp, bus, NewStageHost())
e1, _ := textEvent("webui", "同样的消息")
if _, out := a.runInputTask(e1, nil); out != outcomeDone {
if _, out := a.runInputTask(e1); out != outcomeDone {
t.Fatalf("首次输入=%v期望 outcomeDone", out)
}
after1, outputs1 := a.context.Len(), outputs
e2, ch2 := textEvent("webui", "同样的消息")
if _, out := a.runInputTask(e2, nil); out != outcomeDone {
if _, out := a.runInputTask(e2); out != outcomeDone {
t.Fatalf("去重输入应正常返回(不挂起),实际 %v", out)
}
if a.context.Len() != after1 {
@ -130,7 +130,7 @@ func TestLifecycle_OnInputShortCircuit(t *testing.T) {
a := newLifecycleAgent(t, sp, events.NewBus(), sh)
evt, respCh := textEvent("cli", "任意")
if _, out := a.runInputTask(evt, nil); out != outcomeDone {
if _, out := a.runInputTask(evt); out != outcomeDone {
t.Fatalf("短路任务=%v期望 outcomeDone", out)
}
select {
@ -159,7 +159,7 @@ func TestLifecycle_ErrorPathTerminal(t *testing.T) {
a := newLifecycleAgent(t, sp, bus, NewStageHost())
evt, respCh := textEvent("cli", "会失败")
if _, out := a.runInputTask(evt, nil); out != outcomeFailed {
if _, out := a.runInputTask(evt); out != outcomeFailed {
t.Fatalf("runInputTask=%v期望 outcomeFailed", out)
}
select {
@ -196,7 +196,7 @@ func TestLifecycle_ConsolidationRouted(t *testing.T) {
evt, respCh := textEvent("system", "整理任务")
evt.OutputChannel = channelConsolidation
if _, out := a.runInputTask(evt, nil); out != outcomeDone {
if _, out := a.runInputTask(evt); out != outcomeDone {
t.Fatalf("consolidation=%v期望 outcomeDone", out)
}
if len(respCh) != 0 {