feat(loop,gateway): 13.4.5 目标态 —— 回合内 steer(不打断、新消息进同一上下文流)+ steer_ack 帧
规格=design/15 末尾「13.4.5 目标态规格」(不变量 I1/I2/I3)。调研依据:豆包无公开技术文档(不编造); 同类成熟 Agent 产品无一做同会话并行回合,共同范式= queue + 回合内 steer(Claude Code / Cline PR #13958 / Codex Queue·Steer / open-webui / LangGraph)。 - 注入点=**回合内边界**(上一模型步 + 它那批工具调用都已结束、下一次模型调用还没开始): driveTurn 每 step 组装前调用一次 SteerSource(run.go)——完成 step 只能靠工具批次推进, 故唯一可注入处就是「工具批次结束之后」,循环里没有任何「起第二个回合」的分支(**I1 结构性成立**) - **I2 恰好一次**:注入只进**本轮 view、绝不写回 msgs**(checkpoint 与记忆提取面保持干净); 被注入的消息从 pending 移除,不会由自己的回合再答一次 - **I3 转向不取消工具**:注入调用点在 runToolCalls **之后**(此刻无工具在飞),循环从不因注入 cancel 任何 ctx; 边界序列有测试钉死(恰为 [1 2 …],无批次中途值) - 判定口径= **pending ∩ 转录**:候选是本会话 pending 条目,每个边界重读 history.Recent 验证已落库, 按**行 id** 去重、按 pending FIFO 注入。**不用时间戳**(Windows time.Now() 步长 ~0.44ms,连发常同毫秒并列, 任何 ms 基线都会漏注入或重复注入);跨重启不成立也不需要(在飞回合与 pending 都在内存) - 新下行帧 `steer_ack`:`{type, sessionId, data:{messageId, atStep}}`,语义=「已被本回合吃进、尚未被正面回答」 (驱动前端灰态);**不进 frameReachesAll** ⇒ fail-closed 只发 owner+admin(测试双钉);经 hub 广播、不经 bus - 组合根**零改动**:per-turn 状态用 ctx 承载(loop.WithSteerSource),编排器在回合启动/续跑时挂 - 守卫:session_orchestrator.go 加完接线后 505 行 ⇒ 按职责纯搬出 session_events.go(常量取值未改、行为零变化) - 复核:build/vet 绿 + **全树 0 FAIL** + linux/darwin 交叉编译绿;loop 6 例(含 3 子测)+ gateway 2 例 E2E; **4 条变异反证**(删 consumePending → 模型步数 4≠3;注入写回 msgs → 快照被污染;批次中途注入 → 边界 [1 1 2]; 把帧加进白名单 → viewer 收到)均已还原 - 未做(登记):IM 自有 Chatter 落库的消息不参与 steer(有意,避免重复作答);pending 条目无自落库 id ⇒ 配对用 (内容,图片) 逐行认领;同回合超 20 条用户消息时超出者留 pending 由各自回合作答(不丢)
This commit is contained in:
1 parent
8c6c796a29
commit
3d8822c922
9 files changed
+1306
-22
No files matched your search
@@ -85,6 +85,12 @@ func (l *Loop) Run(ctx context.Context, sessionID string, messages []model.Messa
|
||||
func (l *Loop) driveTurn(ctx context.Context, sessionID string, msgs []model.Message, startStep int, res *Result) (*Result, error) {
|
||||
toolState := &toolLoopState{prevFP: make(map[string]bool)}
|
||||
step := startStep
|
||||
// steered 累积本回合在**回合内边界**注入的用户消息(design/15 §2.4,裁决
|
||||
// 13.4.5)。它们只并入每一步的模型可见视图,绝不写回 msgs —— checkpoint 快照
|
||||
// 与后台记忆提取因此保持干净;又因为每个后续 step 都重新并入,注入内容对模型
|
||||
// 而言始终是**同一个窗口**的一部分(一致性),后到的消息也绝不会顶掉旧窗口
|
||||
// 已产出的内容(完整性,I1)。
|
||||
var steered []model.Message
|
||||
// 未落定退出的回合不留下「可恢复的在途态」(裁决 13.4.5:下一条消息必须
|
||||
// 看到自己的最新上下文,而不是被放弃回合的旧窗口顶掉)。判别口径(主人
|
||||
// 2026-10-04 裁决,「只有崩溃才允许恢复」):**崩溃**时本 defer 根本不执行,
|
||||
@@ -111,7 +117,15 @@ func (l *Loop) driveTurn(ctx context.Context, sessionID string, msgs []model.Mes
|
||||
// 然后为本轮组装模型可见上下文(全新记忆
|
||||
// 注入 + transform 钩子),使每一步都看到最新上下文。
|
||||
msgs = l.compactIfNeeded(ctx, msgs)
|
||||
// 回合内边界(design/15 §2.0 I3):上一个模型步与它那一批工具调用都已结束、
|
||||
// 下一次模型调用还没开始 —— 此刻没有任何工具在飞,因此在这里注入不可能取消
|
||||
// 任何已发起的工具;新到的用户消息则对**同一回合**的下一个模型步立即可见
|
||||
//(design/15 §2.4 最新性)。循环绝不因此新开第二个回合(I1)。
|
||||
steered = l.steerAtBoundary(ctx, sessionID, step, steered)
|
||||
view := l.assembleContext(ctx, msgs, step)
|
||||
if len(steered) > 0 {
|
||||
view = append(view, steered...)
|
||||
}
|
||||
|
||||
rc, roundSpan := l.span(ctx, "loop.step", trace.SpanKindStateTransition)
|
||||
outcome, err := l.step(rc, sessionID, view)
|
||||
|
||||
Reference in new issue
Block a user