Files
Cyrene_v2/internal/services/backthink/roundstate.go
T
AskaEth 321cc3b763
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(backthink): 思考轮工具面补齐(裁决 15)—— 能力面接入 + 只读 capability_list + 本轮状态快照
- 缺口 d:状态在**思考前**读一次,同一份快照既喂思考轮又喂 Decide(读不到 = Known=false,绝不伪装成中性状态)
- 缺口 c:新增只读工具 capability_list(owner-only、三重输出边界、读写隔离),她可自查当下真实可用面
- 缺口 b:内置允许集补 transcode / capability_list(此前 CAPACITY_ENABLED=true 时新工具会从模型可见面消失)
- 缺口 e:guidancePrompt 旧文案承诺的「IoT/日历/提醒」在全仓根本没有对应工具(chat 侧也没有)⇒ 按诚实化改为「与主人对话同等工具面 + 指名自查入口」,不再有空头支票
- (a) 未做且**不自创**:那三把工具无任何设计规格(名字/schema/读写边界/输出上限)⇒ 已报回并登记进 design/20(裁决:走能力/插件/MCP 暴露路线)
- 变异实测:允许集去掉新工具 ⇒ TestDecide_AllowedSets + 组合根跨层测试红;丢掉状态快照 ⇒ TestLoopThinker_Think_InjectsRoundStateBlock 红
- 已知弱项(如实登记):组合根那行 wireCapabilitySurface() 调用本身无测试覆盖(与既有 wireTranscode 同惯例)
2026-10-05 13:54:57 +08:00

89 lines
3.2 KiB
Go

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
}