Files
Cyrene_v2/internal/services/gateway/session_state.go
T
AskaEth c0dc435770 feat(gateway): 忙时并行帧层(turn_begin + steer_ack.turnId + interrupt.turnId)
新下行帧 turn_begin:编排器每启动一路恰好一帧(单路 kind:"turn" / 并行路 "parallel"),
data{kind, messageIds, leg, activeTurns};角色门 fail-closed 仅 owner+admin,未进 frameReachesAll。
steer_ack 增帧壳 turnId(与该路 turn_begin 同源)—— 并行下没有它配不上正确的 turn.end。
上行 interrupt.turnId(仅 scope:"current" 生效):精确停某一路;目标不存在 ⇒ 定向
{type:"error",data:{code:"bad_request"}},不广播、不动任何会话;省略 turnId = v0.65 原口径(全部在途路)。
帧序按实现:session_busy / pending_drain → message_persisted(user) → turn_begin
(规范草案 §2.9 的时序图需按实现更正:messageIds 必须是已落库的权威 id,故只能在落库之后)。
测试:编排器侧 + 真 WS/JWT 端到端;变异 5 条(每路一帧 / 帧壳 turnId / 目标化打断 / 取消筛选)。
Docs 待升 v0.66(由监工登记)。
2026-10-05 09:38:11 +08:00

134 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package gateway
import "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
// 本文件归属会话编排器的**状态查询与状态帧出口**:谁在读「这个会话忙不忙」,
// 以及状态迁移怎么交给传输层。它从 session_orchestrator.go 拆出,使那个文件只留
// 消息状态机(Submit / pending / 续跑 / 结算),并保持在「单文件 ≤500 行」的
// 硬约束内(与 session_events.go / session_tail.go 同一理由)。
// StateSink 把每会话状态转移(busy/pending/idle/interrupt)报告给
// 传输层。网关把它接到 WS hub 广播上。
type StateSink func(sessionID, typ string, data map[string]any)
// TurnStateSink 是 StateSink 的**带 turnId 变体**(design/15 §2.5.9):帧壳上的
// `turnId` 是客户端按回合两级分桶的依据,而少数帧(`turn_begin`、`steer_ack`)
// 必须让客户端知道**是哪一路**,只有 data 不足以表达。
//
// 它刻意是**另一个**回调而不是改 StateSink 的签名:既有接线点(server.go、
// 大量测试)的签名因此零改动,且空 turnID 时明确退化为现签名(帧壳上没有
// turnId,与今天逐字相同)。未接线该变体时,带 turnId 的状态帧退化为普通帧
// ——绝不因此丢一条状态迁移(状态是增益,不是回合能否跑完的前提)。
type TurnStateSink func(sessionID, turnID, typ string, data map[string]any)
// SetMode 提升某会话所报告的忙碌状态通道(thinking →
// executing → subagent)。它是 loop/subagent 钩子可调用的注入点;
// 任何非空闲模式都让会话保持忙碌,而 idle 解除忙碌。为 nil 的会话
// 被惰性创建,因此钩子可以在第一条消息之前设模式。
//
// 注意它与「在途路」的关系:真正的忙碌由 st.legs 承载(有路在飞就是忙),
// mode 只是给客户端看的通道标签。因此 SetMode 不会被并行的第二路顶掉,
// 也不会顶掉第二路的忙态。
//
// **已知偏离(诚实登记)**:design/15 §2.5.4 要求多路下 mode「取最重的一路」
// (subagent > executing > thinking)。那需要一个**按路**的模式源,而今天
// 提升模式下井的唯一入口是本函数(会话级、且生产路径上暂无调用方)—— 因此本
// 切片如实保留「会话级 mode:新路起于 thinking、全部收尾才回 idle」的既有语义,
// 不伪造一个按路的轻/重判定。等模式提升改成按路注入时,这里再实现加权。
func (o *Orchestrator) SetMode(sessionID string, mode Mode) {
sessionID = o.normalize(sessionID)
o.mu.Lock()
st := o.states[sessionID]
if st == nil {
st = &sessionState{mode: mode}
o.states[sessionID] = st
} else {
st.mode = mode
}
activeTurns := len(st.legs)
o.mu.Unlock()
if mode == ModeIdle {
o.emitState(sessionID, EventSessionIdle, busyPayload(sessionID, mode, activeTurns))
return
}
o.emitState(sessionID, EventSessionBusy, busyPayload(sessionID, mode, activeTurns))
}
// Busy 报告该会话当前是否有在飞回合。
func (o *Orchestrator) Busy(sessionID string) bool {
o.mu.Lock()
defer o.mu.Unlock()
st := o.states[o.normalize(sessionID)]
return st != nil && st.mode != ModeIdle
}
// ActiveTurns 报告该会话当前的在途路数(design/15 §2.6 的 `activeTurns`)。
// 单路时它非 0 即 1,与「忙不忙」同值;并行时它才是真正的路数 —— 恰好是所有路
// 都收尾后才报 0 的那个判据(design/15 §2.5.3)。
func (o *Orchestrator) ActiveTurns(sessionID string) int {
o.mu.Lock()
defer o.mu.Unlock()
st := o.states[o.normalize(sessionID)]
if st == nil {
return 0
}
return len(st.legs)
}
// QueueDepth 报告某会话的待处理回合数。
func (o *Orchestrator) QueueDepth(sessionID string) int {
o.mu.Lock()
defer o.mu.Unlock()
st := o.states[o.normalize(sessionID)]
if st == nil {
return 0
}
return len(st.pending)
}
// ModeOf 报告某会话当前的忙碌状态通道(未知时为 idle)。
func (o *Orchestrator) ModeOf(sessionID string) Mode {
o.mu.Lock()
defer o.mu.Unlock()
st := o.states[o.normalize(sessionID)]
if st == nil {
return ModeIdle
}
return st.mode
}
// normalize 把空会话 id 映射为默认值,使状态 map 对省略它的
// 调用方保持稳定(Web 主会话)。
func (o *Orchestrator) normalize(sessionID string) string {
if sessionID == "" {
return defaultSessionID
}
return sessionID
}
// emitState 在接线了传输层时把状态转移转发给它。
func (o *Orchestrator) emitState(sessionID, typ string, data map[string]any) {
if o.emit != nil {
o.emit(sessionID, typ, data)
}
}
// emitTurnState 发出一个**帧壳带 turnId** 的状态转移(design/15 §2.5.9)。
// 未接线带 turnId 的变体时退化为普通状态帧(空 turnID 与现签名等价),
// 因此未接线的构建与既有测试绝不因此丢掉一条状态迁移。
func (o *Orchestrator) emitTurnState(sessionID, turnID, typ string, data map[string]any) {
if o.emitTurn != nil {
o.emitTurn(sessionID, turnID, typ, data)
return
}
o.emitState(sessionID, typ, data)
}
// notifyPersisted 在接线了传输钩子时把刚持久化的消息交给它,
// 使网关能广播 `message_persisted` 以进行跨端同步。
func (o *Orchestrator) notifyPersisted(msg history.Message) {
if o.onPersisted != nil {
o.onPersisted(msg)
}
}