package model import "context" // 本文件承载「本次调用属于哪个会话」的 context 通道(切片 U1 留的接线缺口)。 // // 背景:模型适配器跑在 loop 之下、没有会话概念,于是 `llm_usage.session_id` // 一直是 NULL —— 列与索引在切片 U1 已建好(迁移 0017),缺的是**从循环边界 // 把会话标识带到适配器边界**的那一跳(usage.go / usage_store.go 此前都留了 // 注释说明这一点)。没有这一跳,「哪次调用贵在哪个会话身上」这个问题永远答不出来。 // // 通道形态与 tool/turncontext.go、loop/session_ctx.go 同族:key 由**消费方** // (本包)持有,上游 loop 在 Run 顶部播种一次;此后所有经该 ctx 发起的模型调用 // —— 流式的 ChatStream 与非流式的 Chat(agent.RunStep)**两条**路径,以及 // fallback 链、persona 栈这些原样透传 ctx 的包装 —— 在适配器内部都能读到它。 // 反过来(key 放在 loop 里)做不到:本包在 loop 之下,导入 loop 会成环(铁律: // 单二进制内模块间只走进程内接口 + 依赖注入)。 // // 为什么不挂在 model.Request 上:Request 是要编码成线格式发给提供商的那份边界 // 对象,而会话标识属于**本地记账元数据** —— 挂上去就等于给「将来某个适配器顺手 // 把它序列化出去」留了口子,且 buildRequest 之外的调用点(后台流程、直接调用 // 适配器的包装)全都要各自记得填。ctx 值只在进程内流动,覆盖面还更全。 // // 纪律:它只是记账标签,**绝不**参与身份/权限判定(红线:身份由 channel 层 // actor 元数据 + 策略引擎决定,绝不靠任何透传字段推断)。未播种时读到空串, // 落库据此写 NULL 而不是空串。 // sessionIDKey 是本次模型调用所属会话 id 的 context key。 type sessionIDKey struct{} // WithSessionID 把会话 id 挂到 ctx 上(循环在 Run 顶部调用一次)。 // // 空 id 是空操作:不挂载即「本次调用没有会话归属」(管理面板试连、未接循环的 // 后台流程),读取侧返回空串,用量记 NULL —— 与接线前逐字一致。 func WithSessionID(ctx context.Context, sessionID string) context.Context { if ctx == nil || sessionID == "" { return ctx } return context.WithValue(ctx, sessionIDKey{}, sessionID) } // SessionIDFrom 返回 ctx 上挂载的会话 id;未挂载(或 ctx 为 nil)时返回空串。 func SessionIDFrom(ctx context.Context) string { if ctx == nil { return "" } s, _ := ctx.Value(sessionIDKey{}).(string) return s }