package backthink import ( "context" "fmt" "strings" ) // 本文件把**本轮的用户状态快照**从打扰决策送到思考轮(X1 缺口 d;design/20 §2 // 「上下文 = 主会话最近 N 条 + 记忆召回 + 系统/设备/日程摘要」)。 // // 为什么需要它:状态此前只在 Thinker.Think 返回**之后**被读取、只喂给 Decide, // 于是她写结论时完全不知道主人此刻在睡/在忙/心率多少 —— 她的语气与时机判断 // 与随后真正生效的打扰门脱节(缺口 d)。现在服务在每轮开头读一次,同一份快照 // 同时交给思考轮(渲染成上下文块)与 Decide(打扰门),两侧不再各读一次。 // RoundState 是一轮思考携带的用户状态快照。 type RoundState struct { // State 是读到的状态本身。 State UserState // Known 报告这份状态是否真的读到了:状态源未接线或读取失败时为 false。 // 未知时思考轮**不注入**状态块 —— 宁可不渲染,也绝不渲染一份「全是 false」 // 的假状态去误导她的判断。 Known bool } // roundStateKey 是本轮状态在 ctx 上的键。 type roundStateKey struct{} // WithRoundState 把本轮的用户状态快照挂到 ctx 上。与 loop.WithSessionEnv 同一 // 直觉:**每轮**的元数据随 context 走,因此并发轮次(事件驱动 + 心跳可能同时在跑) // 绝不互相串状态,Thinker 也不必持有可变字段。 func WithRoundState(ctx context.Context, rs RoundState) context.Context { if ctx == nil { return nil } return context.WithValue(ctx, roundStateKey{}, rs) } // RoundStateFrom 读回本轮的状态快照;未挂载时返回零值(Known=false), // 语义为「这一轮没有状态可用」。 func RoundStateFrom(ctx context.Context) RoundState { if ctx == nil { return RoundState{} } rs, _ := ctx.Value(roundStateKey{}).(RoundState) return rs } // roundStateBlock 把状态快照渲染成提示词里的一段上下文块;未知时返回空串 // (调用方据此不注入任何状态描述)。它只写事实,不授权、不判身份。 func roundStateBlock(rs RoundState) string { if !rs.Known { return "" } var b strings.Builder b.WriteString("【主人此刻的状态】(来自设备/日程遥测,只描述事实)\n") if parts := rs.parts(); len(parts) > 0 { b.WriteString(strings.Join(parts, " · ")) } else { b.WriteString("未观测到睡眠/工作/忙碌信号,心率与无聊度未知") } b.WriteString("\n请结合它判断语气与时机;她睡着或正忙时,除了真正紧急的事不必打扰。") return b.String() } // parts 把已观测到的信号渲染成短语;零值字段(未观测到)不出现, // 因此这段文本绝不把「未知」说成「否」。 func (rs RoundState) parts() []string { s := rs.State out := make([]string, 0, 5) if s.HeartRateBPM > 0 { out = append(out, fmt.Sprintf("心率 %d bpm", s.HeartRateBPM)) } if s.Sleeping { out = append(out, "睡眠中") } if s.Working { out = append(out, "工作中") } if s.Busy { out = append(out, "忙碌") } if s.Boredom > 0 { out = append(out, fmt.Sprintf("无聊度 %.2f", s.Boredom)) } return out }