Skip to content

Latest commit

 

History

History
150 lines (106 loc) · 8.25 KB

message-queue.md

File metadata and controls

150 lines (106 loc) · 8.25 KB

排队与插队

本文说明回合运行中继续发消息的两种方式,以及它们在终端上的表现。相关行为和色值都有测试保证,见文末「测试覆盖」。

两种方式的区别

回合运行中你还可以继续输入,消息有两种发送方式:

  • 排队:等这一回合全部结束后,它作为新的一回合开始处理。
  • 插队:在这一回合的下一个步骤开始之前交给模型,和当前这次请求一起处理。

差别不在排在哪条队列里,而在模型什么时候能看到它。

为什么会差一个回合

一个回合由若干步骤组成。每个步骤是一次模型请求,以及这次请求调用的工具。工具执行完、下一次模型请求还没发出的那段时间点,就是一个步骤边界。

上游把待发消息放在两条队列里(@deepseek-ai/dsh-agent 的 Agent.inbox):

队列 由谁写入 模型什么时候看到
next-turn followup() —— 排队 当前回合结束之后,作为新的一回合
next-step steer() —— 插队 当前回合的下一个步骤边界,和当前请求一起

两条队列的取出顺序写在上游 packages/core/agent-loop/src/inbox.ts 的 claim() 里:

claim(target, turn) {
  const claimed = this.mutate('next-step', 0, this.nextStep.length, [], false)
  if (target === 'next-turn') claimed.push(...this.mutate('next-turn', 0, 1, [], false))
  for (const message of claimed) this.dispatch.emit('agent/inbox/claimed', { message, turn })
  return claimed
}

第一行取走全部 next-step,第二行只在新回合开始时取走一条 next-turn。所以:

  • 每个步骤边界都会取走全部插队消息,它们进入这一步的请求;
  • 排队消息只有在新回合开始时才被取走一条,剩下的继续等下一个回合。

同一份文件里还有两条相关行为:

  • inject() 只放入模型可见的上下文,不会唤醒空闲中的执行流程。终端用它处理 @会话 快照,不用作用户消息的通道。
  • 取消时,cancel() 默认把两条队列都清空。手动删除和清空会留下带 outcome: 'canceled' 的 inbox 事件;而步骤边界的取走不带这个标记。终端就是靠这一点区分「被取消」和「已被取走」,详见下文「会话区里的标记」。

一个例子

你让它重构 X,它开始做:

你:重构 X
 ├─ 步骤 1  读文件
 ├─ 步骤 2  跑测试
 │            ← 你在这时输入了第二句话
 ├─ 步骤 3  ……
 └─ 步骤 4  收尾,回合结束
第二句话怎么发 模型什么时候看到 结果
排队 步骤 4 之后,新回合的第一步 它已经按原来的方案改完了,你这时才补充
插队 步骤 2 和步骤 3 之间,和这一步一起 它还没开始步骤 3,可以立刻按新要求做

简单说:排队是等它做完再说,插队是在它下一步动手之前就告诉它。

界面上的三种情况

情况 怎么发出 会话区里的标记 整行底色(以 dark 为例)
普通消息 空闲时直接回车 无标记,提示符 ❯ 亮品牌蓝
排队 回合运行中回车(默认) ❯ 加行尾的「排队」 警示琥珀
插队 空输入框按 Tab 切到插队后回车;或在 /queue 面板里对一条按回车 ↳ 加行尾的「插队」 紫罗兰

有一条判断规则需要留意:只有在提交那一刻已经有回合在运行,才记为排队或插队。 空闲时提交的消息一律算普通消息,即使当时选的是插队 —— 没有回合可以插,它本来就会立刻开始一个新回合。

怎么操作

位置 按键 作用
输入框为空 Tab 切换下一条消息是排队还是插队
输入框为空 Delete 取消最新一条排队消息
输入框有内容 Tab 仍是命令、技能和 @ 引用的补全
/queue 面板 ↑ ↓ / PageUp / PageDown / g / G 选择、翻页、跳到首尾
/queue 面板 Enter 把选中的排队消息改为插队(只在回合运行中可用)
/queue 面板 e 修改文字;图片和文件附件原样保留,行尾标为只读
/queue 面板 d 从队列里删除
任意位置 Esc / Ctrl+C 取消当前回合:排队消息保留并接着发出,插队消息随这一回合丢掉

/queue 面板只管理排队那一队消息。插队属于正在运行的那一回合,所以不在面板里,也无法单独取消。

会话区里的标记

上游在步骤边界取走消息时,会先写一条普通的 inbox 事件把队列行去掉,之后才写真正的用户消息。所以想标出消息是怎么发来的,不能等到用户消息出现时再去查队列 —— 那时队列行已经没了。

做法是在投影层另外记一份:

  • 插入 next-turn、且当时回合在运行 → 记为排队;
  • 插入 next-step、且当时回合在运行 → 记为插队;
  • 其余情况不记,按普通消息显示;
  • 用户消息出现时,取用这条记录并把它删掉;只有带 canceled 的取消事件才提前丢弃。

这份记录随事件流一起重建,所以退出重开、恢复历史会话之后标记仍然在。实时折叠和历史重放走同一套规则,结果由测试保证一致。

颜色

三种颜色由每套主题自己提供,不是写死的:

主题 终端底色 普通消息 排队 插队
dark 黑 [103, 158, 254] [245, 158, 11] [167, 139, 250]
light 白 [47, 76, 143] [124, 53, 10] [109, 40, 217]
prismatic 黑 [240, 171, 252] [245, 158, 11] [103, 232, 249]
rainbow 黑 本次随机出的品牌亮色 本次随机出的警示色 本次随机出的代码色

整行底色是算出来的:把该颜色按 22% 混到主题的终端底色上。所以切换主题或者重新随机 rainbow 之后,底色会跟着一起变。

浅色主题三种颜色都单独加深过。原因是警示色在白底上本身只有 4.5:1,再混一层它自己的浅色底会掉到 3.99:1,所以要另取一组既能压在底色上、也能直接压在白底上的值。

实测对比度(左边是文字压在整行底色上,右边是文字直接压在终端底色上):

主题 普通消息 排队 插队
dark 5.9 / 7.9 7.0 / 9.8 5.8 / 7.7
light 5.8 / 8.2 6.1 / 8.9 4.9 / 7.1
prismatic 8.0 / 11.9 7.0 / 9.8 9.3 / 14.5

都满足正文的 AA 要求(不低于 4.5:1)。测试会读出实际生成的底色 RGB 再计算,并检查底色既不同于文字颜色也不同于终端底色、三种颜色互不相同、rainbow 的三种颜色确实取自本次随机结果。

只有 256 色以上的终端才画底色,16 色终端只保留文字颜色 —— 与 /diff 的判断一致,避免饱和的系统色盖住文字。

已知限制

  • 空闲时提交的消息一律显示为普通消息,即使当时选的是插队。
  • 按 Esc 取消回合时,插队消息会一起丢掉(它属于被取消的那一回合);排队消息会保留并接着发出。
  • 一回合结束时如果队列里还有消息,它们会依次作为新的回合继续处理。
  • /queue 面板只认完全匹配的命令:/queue clear 这类输入会当作普通文本发给模型。

测试覆盖

覆盖内容 位置
三种来源的判断,包含「先取走、后写入」的顺序、取消后丢弃、空闲时不标记 tests/projection.spec.ts
实时折叠与历史重放结果一致 tests/projection.spec.ts
整行同色、铺满宽度、排队和插队的预览行各自颜色 tests/lines.spec.ts
提问里粘贴的 ```diff 保留自己的红绿 tests/lines.spec.ts
主题色的对比度、计算关系、16 色降级 tests/theme.spec.ts
按 Tab 之后分别调用 steer 和 dispatch tests/app.spec.ts
三种底色确实出现在终端输出里 tests/app.spec.ts
面板的 d 删除、删除键不误删、长队列的 g/G 与分页 tests/panels.spec.ts
导出时带上来来源标记 tests/export.spec.ts