Files
AskaEth 9b903a89bc
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(runtime): IM 主人私聊补历史转录(与 Web 主会话同源:抽共享 transcript 包 + OwnerEnvFor 单入口)
2026-10-05 11:36:18 +08:00

66 lines
3.8 KiB
Go
Raw Permalink 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 (
"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
}