package gateway import ( "context" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // 本文件拥有从持久化转录重建一个回合对话上下文的工作 // (design/15 §2.2/§4「上下文有界:只保留最近 N 条上下文」、design/02 // §1 主会话承载完整上下文、design/22 §7-5)。一个回合此前只从刚 // 发送的消息开始,使模型完全看不到它正在延续的 // 对话;更早回合的助手话语也没有记录 // 是谁说的,因此切换进会话的角色会把另一个 // 角色的话读成自己的。 // // 播种修好了这两半:新回合携带该会话最近的 // 持久化消息,且每条助手消息都被说话的角色标注 // (model.Message.SenderName),使 loop 的上下文组装能归属它 // (loop/attribution.go)。消息上只走标签 —— 没有任何消息体 // 被改写。 // // 范围说明:被播种的窗口就是该对话,因此 loop 的逐步 // 后台记忆抽取现在读的是这些近期消息,而不是 // 单条刚发送的消息。抽取出的条目仍归到该回合的角色 // 名下(loop.persistMemories,design/22 §4.2),且同一内容的重复抽取 // 会被抽取器去重,但一个在另一角色活跃时首次说出的事实, // 若当时从未被抽取,现在就可能被当前角色拾取。 // 把抽取输入收窄为当前回合的消息是一个 // 独立的决定,留作后续跟进。 // transcriptContextCount 限定一个新回合带入模型上下文的持久化 // 消息数。它与编排器的续跑尾部上限 // (defaultMaxTail)一致,使播种回合与续跑回合保持同样大小; // 超出之后仍由 loop 自身的上下文预算治理压缩。 const transcriptContextCount = 20 // transcriptReader 是播种所需的窄历史视图。*history.Store // 满足它;未满足的存储(测试替身,或未接持久化 // 构建的网关)只是得不到播种上下文,因此编排器 // 完全照旧继续运行。 type transcriptReader interface { Recent(ctx context.Context, sessionID string, n int) ([]history.Message, error) } // startTurnMessages 返回新回合运行其上的对话:该会话 // 已播种的转录,后跟客户端刚发送的消息。转录 // 不可用时,调用方的消息原样返回,这正是 // 既有行为。 func (o *Orchestrator) startTurnMessages(ctx context.Context, sessionID string, messages []model.Message) []model.Message { seed := o.sessionTranscript(ctx, sessionID) if len(seed) == 0 { return messages } return concatMessages(seed, messages...) } // sessionTranscript 读取该会话最近的持久化消息并把它们 // 映射为模型消息(最旧优先)。nil/不可读的存储或读错误 // 都得到 nil,因此聊天绝不被元数据查找阻塞;没有 // 历史的会话也什么都不产出 —— 二者都退化为既有的 // 「只有新消息」上下文。 func (o *Orchestrator) sessionTranscript(ctx context.Context, sessionID string) []model.Message { if o.history == nil || sessionID == "" { return nil } reader, ok := o.history.(transcriptReader) if !ok { return nil } recent, err := reader.Recent(ctx, sessionID, transcriptContextCount) if err != nil { o.log.Debug("turn transcript read", "err", err, "session", sessionID) return nil } out := make([]model.Message, 0, len(recent)) for _, h := range recent { out = append(out, transcriptMessage(h)) } return out } // transcriptMessage 把一条持久化聊天消息映射为回合延续所依据的 // 模型消息。助手消息携带说话的角色(该回合已存的 // persona_id)作为其说话者标签,因此即使会话被切换过, // 上下文也能把它归属到那个角色;非助手消息保持其 // 已存发送者标签(design/16 §4.6 的第三方显示名),绝不 // 被重新标为说话角色。 func transcriptMessage(h history.Message) model.Message { role := historyRoleToModel(h.Role) msg := model.Message{ Role: role, Content: []model.Content{{Type: model.ContentTypeText, Text: h.Content}}, } if h.Image != "" { msg.Content = append(msg.Content, model.Content{Type: model.ContentTypeImage, ImageURL: h.Image}) } if role == model.RoleAssistant { msg.SenderName = h.PersonaID return msg } msg.SenderName = h.SenderName return msg } // historyRoleToModel 把已存聊天角色映射为模型的角色词汇, // 未知角色默认归到用户通道。 func historyRoleToModel(role string) model.Role { switch role { case history.RoleAssistant: return model.RoleAssistant case history.RoleSystem: return model.RoleSystem default: return model.RoleUser } } // assistantTailMessage 为一个已结算回合的回复构建续跑尾部 // 条目,并用说话的角色标注它(design/22 §7-5):该尾部是 // 播种转录的内存孪生,因此之后切换进来的角色必须能 // 在紧接着的那个排队回合上,把更早角色的回复与自己的话 // 区分开。 func assistantTailMessage(text, personaID string) model.Message { msg := model.TextMessage(model.RoleAssistant, text) msg.SenderName = personaID return msg }