- 缺口 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 同惯例)
89 lines
3.2 KiB
Go
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
|
|
}
|