package gateway import ( "context" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/transcript" ) // 本文件拥有「一个新回合的对话上下文从哪里来」的网关侧**决策与时刻** // (design/15 §2.2/§4「上下文有界:只保留最近 N 条上下文」、design/02 §1 主会话 // 承载完整上下文、design/22 §7-5)。重建本身(读最近 N 条 + 翻成模型消息)住在 // internal/runtime/transcript —— 它与渠道桥的 IM 主人私聊回合是**同一份实现**, // 两条车道的模型可见历史因此不会漂移(IM 与 Web 只在呈现形态上不同:IM 只发 chat 文本)。 // // 一个回合此前只从刚发送的消息开始,使模型完全看不到它正在延续的对话;更早回合的 // 助手话语也没有记录是谁说的,因此切换进会话的角色会把另一个角色的话读成自己的。 // // 播种修好了这两半:新回合携带该会话最近的持久化消息,且每条助手消息都被说话的角色 // 标注(model.Message.SenderName),使 loop 的上下文组装能归属它 // (loop/attribution.go)。消息上只走标签 —— 没有任何消息体被改写。 // // 范围说明:被播种的窗口就是该对话,因此 loop 的逐步后台记忆抽取现在读的是这些近期 // 消息,而不是单条刚发送的消息。抽取出的条目仍归到该回合的角色名下(loop.persistMemories, // design/22 §4.2),且同一内容的重复抽取会被抽取器去重,但一个在另一角色活跃时首次说出 // 的事实,若当时从未被抽取,现在就可能被当前角色拾取。把抽取输入收窄为当前回合的消息是 // 一个独立的决定,留作后续跟进。 // startTurnMessages 返回新回合运行其上的对话:该会话已播种的转录,后跟客户端刚发送的 // 消息。转录不可用时,调用方的消息原样返回,这正是既有行为。 func (o *Orchestrator) startTurnMessages(ctx context.Context, sessionID string, messages []model.Message) []model.Message { seed := transcript.Read(ctx, o.history, sessionID) if len(seed) == 0 { return messages } return concatMessages(seed, messages...) } // seedTurnLocked 为新回合播种对话(持久化转录在刚发送的消息之前),并把播种后的 // 尾部记进会话状态。**必须在 o.mu 下调用**(它读写 st.tail;与 admitLocked / // claimClientMsgLocked 同一约定),调用方负责解锁 —— 因此它从 // session_orchestrator.go 的 submitInternal 里拆出来,让那个函数留在「函数 ≤80 // 行」的硬约束内。 // // 播种的时刻是刻意的:它在 persistUserMessages 写入新消息**之前**,因此刚发送的 // 消息绝不在上下文中重复(design/15 §2.2/§4、design/22 §7-5)。没有历史(或空 // 会话)的存储让该回合只使用调用方给出的消息。它是在状态锁下的有界索引读取 // (transcript.ContextCount 行),而状态锁从不会被运行中的回合持有,因此它无法把 // Submit 堵在回合后面。 func (o *Orchestrator) seedTurnLocked(ctx context.Context, st *sessionState, sessionID string, messages []model.Message) []model.Message { turn := o.startTurnMessages(ctx, sessionID, messages) st.tail = o.boundedTail(turn) return turn } // assistantTailMessage 为一个已结算回合的回复构建续跑尾部 // 条目,并用说话的角色标注它(design/22 §7-5):该尾部是 // 播种转录的内存孪生,因此之后切换进来的角色必须能 // 在紧接着的那个排队回合上,把更早角色的回复与自己的话 // 区分开。 func assistantTailMessage(text, personaID string) model.Message { msg := model.TextMessage(model.RoleAssistant, text) msg.SenderName = personaID return msg }