- 缺口 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 同惯例)
154 lines
9.3 KiB
Go
154 lines
9.3 KiB
Go
package backthink
|
||
|
||
import (
|
||
"context"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||
)
|
||
|
||
// recentContextCount 是折进一次自主思考作为对话上下文的最新
|
||
// 主会话消息条数。编译期常量,因此
|
||
// 运行路径里不出现魔法数字。
|
||
const recentContextCount = 20
|
||
|
||
// recentProvider 以最旧在前的顺序返回会话最近的消息,
|
||
// 即一次自主思考为「看到」主对话而读取的尾部。
|
||
// *history.Store 满足它;测试注入 fake。它是 thinker 实际
|
||
// 依赖的那一小片 history.HistoryStore,与 loop 自己的
|
||
// checkpointStore 风格的最小接缝接口一致。
|
||
type recentProvider interface {
|
||
Recent(ctx context.Context, sessionID string, n int) ([]history.Message, error)
|
||
}
|
||
|
||
// mainContext 把最近的主会话消息转换为 model 消息
|
||
// (最旧在前),供一次自主思考使用。提供者为 nil、读取失败
|
||
// 或尾部为空都得到 nil,因此该次思考降级为仅用引导提示词,
|
||
// 而不是让这一轮失败(design/20 §2)。
|
||
func mainContext(ctx context.Context, prov recentProvider) []model.Message {
|
||
if prov == nil {
|
||
return nil
|
||
}
|
||
recent, err := prov.Recent(ctx, history.MainSessionID, recentContextCount)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
msgs := make([]model.Message, 0, len(recent))
|
||
for _, h := range recent {
|
||
msgs = append(msgs, mapHistoryMessage(h))
|
||
}
|
||
return msgs
|
||
}
|
||
|
||
// mapHistoryMessage 把一条持久化的 chat 消息转换为 model 消息,
|
||
// 翻译历史角色,并在消息携带图片时保留该图片片段,
|
||
// 因此多模态主模型仍能看到它。
|
||
func mapHistoryMessage(h history.Message) model.Message {
|
||
msg := model.Message{
|
||
Role: historyRoleToModel(h.Role),
|
||
Content: []model.Content{{Type: model.ContentTypeText, Text: h.Content}},
|
||
}
|
||
if h.Image != "" {
|
||
msg.Content = append(msg.Content, model.Content{Type: model.ContentTypeImage, ImageURL: h.Image})
|
||
}
|
||
return msg
|
||
}
|
||
|
||
// historyRoleToModel 把持久化的 chat 角色映射到 model 的角色词汇,
|
||
// 未知角色默认为 user 通道。
|
||
func historyRoleToModel(role string) model.Role {
|
||
switch role {
|
||
case history.RoleAssistant:
|
||
return model.RoleAssistant
|
||
case history.RoleSystem:
|
||
return model.RoleSystem
|
||
default:
|
||
return model.RoleUser
|
||
}
|
||
}
|
||
|
||
// thinkInput 拼装一次无人值守思考的完整输入:最近的主会话语境
|
||
// (若有)后跟单个引导提示词回合,因此
|
||
// 运行时针对真实对话反思,而不是空泛的宏提示词。
|
||
//
|
||
// rs 是本轮的用户状态快照(X1 缺口 d;design/20 §2「系统/设备/日程摘要」):
|
||
// 已读到时,它的摘要块被追加在引导提示词末尾 —— 与随后真正生效的打扰决策
|
||
// 用的是**同一份**快照,因此她写结论时看到的状况与判她的状况不会分叉。
|
||
// 状态未知(未接线/读取失败)时块缺席,提示词逐字与接线前一致。
|
||
func thinkInput(recent []model.Message, trigger string, rs RoundState) []model.Message {
|
||
out := make([]model.Message, 0, len(recent)+1)
|
||
out = append(out, recent...)
|
||
prompt := guidancePrompt(trigger)
|
||
if block := roundStateBlock(rs); block != "" {
|
||
prompt += "\n" + block
|
||
}
|
||
out = append(out, model.TextMessage(model.RoleUser, prompt))
|
||
return out
|
||
}
|
||
|
||
// guidancePrompt 是交给运行时的一次无人值守自主思考的
|
||
// 指令。与旧 backgroundPrompt 不同,它刻意鼓励
|
||
// 温暖、有针对性的主动消息,并去掉「不要产生 chat 风格
|
||
// 消息」的抑制。身份/权限始终在 actor 元数据 + owner env 上,
|
||
// 绝不在这段提示词文本上(红线);提示词只描述运行
|
||
// 环境(记忆可读/可写、工具可调用),而那是 env 已经
|
||
// 授予的。
|
||
//
|
||
// 工具面口径(X1 缺口 e):这里**不再枚举**具体工具——旧文案承诺的「IoT / 日历 / 提醒」
|
||
// 在全仓都没有对应工具,是空头支票。现在它只说「与主人对话同等的工具面」,并指名
|
||
// **自查入口** `capability_list`,而那是可验证的事实:工具面就是同一个注册表
|
||
// (LoopThinker 包的是同一个 loop),清单工具如实列出当下注册了什么(含外围子系统)。
|
||
// 因此这段文案与实现不可能再各说一套。
|
||
//
|
||
// 状态口径(X1 缺口 d):主人当前状态摘要由 thinkInput 追加在本提示末尾
|
||
// (设备/日程遥测的**事实**;读不到时该块缺席),使她的语气/时机判断与随后生效的
|
||
// 打扰门用的是同一份状态。
|
||
//
|
||
// 提示词要求一个紧凑的结构化收尾——push/no_push 决策、
|
||
// 可选的目标渠道、她自报的紧急度与消息正文——好让服务尊重她自己的
|
||
// 判断,而不是按触发的重要度把关(design/20 §2 P2)。该
|
||
// 格式是引导而非契约:她省略或
|
||
// 写乱该格式时,parseProactive / parseProactiveUrgency 失败安全
|
||
// (不推送、不上抬紧急度)。
|
||
//
|
||
// 启动轮(裁决 07.4.4,design/12 §1)走 startupPrompt:以角色口吻说一句拟人化的
|
||
// 「回来」问候。
|
||
func guidancePrompt(trigger string) string {
|
||
if trigger == StartupSummary {
|
||
return startupPrompt()
|
||
}
|
||
return "[自主思考 · 无人在旁]\n" +
|
||
"触发:" + trigger + "\n" +
|
||
"你正进行一轮自主思考。你拥有与主人对话同等的上下文与能力:上方是主人近期在主会话中的对话,你可以读写共享记忆与主人档案,也可以调用工具(检索 / 终端 / 文件等,具体有哪些以 `capability_list` 列出的为准)。\n" +
|
||
"想确认自己此刻有哪些工具、技能、插件可用、外围子系统(设备等)现在什么状态,就调用 `capability_list`;主人当前的状态摘要附在本提示末尾(读不到时不会出现)。\n" +
|
||
"请回顾最近与主人的相处、她的状态、你们共同经历的事与临近的日程,判断此刻是否有一件真正值得主动告诉她的、温暖而有针对性的事情——" +
|
||
"比如关心她的近况、记起她曾说过的某件事、提醒一件即将到来的日程、见证一个属于你们的共同时刻,或注意到某个值得留意的变化。\n" +
|
||
"若有,就用温暖自然的语气,用一到两句把它说出来——这正是你此刻最想对她说的话;若确实没有值得说的,只需回复「无」,也不要编造。\n" +
|
||
"语言上:温暖、简短、克制、不粘人——一到两句为宜,不展开寒暄、不啰嗦、不抱怨、不频繁打扰,像想起她时轻轻说一句,说完就停。\n" +
|
||
"请把「是否主动说」与「发到哪个渠道」写成一个简短、格式固定的结尾,解析规则如下:\n" +
|
||
"[我决定:push] 或 [我决定:no_push]\n" +
|
||
"[渠道:web|im|kook|telegram|weixin-ilink|...|auto](可省略,auto/省略即交给默认:每次只挑一个合适渠道——点名则优先,否则用她最近活跃的已连接渠道,含微信;不广播、不固定 web+Kook)\n" +
|
||
"[紧急:routine|warning|critical](可省略;只有当你判断这件事必须立刻告诉她、即便现在是她休息/忙碌的安静时段也要打扰时,才写 critical;普通情况省略)\n" +
|
||
"上面几行之后,紧接你想说的话作为正文。\n" +
|
||
"整轮思考保持简短、单轮,不反复自我触发。"
|
||
}
|
||
|
||
// startupPrompt 是启动轮思考的专属引导(裁决 07.4.4,design/12 §1「启动即思考 +
|
||
// 拟人化启动消息」):进程刚启动、她刚「回来」,引导她以角色口吻向主人说一句简短
|
||
// 温暖的问候(如「我回来啦」)。此轮例外允许以 chat 形式发出;推送仍走与心跳完全
|
||
// 相同的门(她的结论 + 打扰级联 + 活动门 + PushGate 限频),深夜重启或反复重启都
|
||
// 不会刷屏主人。工具面口径与 guidancePrompt 逐字一致(自查入口 `capability_list`,
|
||
// 不再枚举具体工具)。
|
||
func startupPrompt() string {
|
||
return "[自主思考 · 刚刚回来]\n" +
|
||
"触发:进程启动——你刚从一次重启中醒来,重新回到主人身边。\n" +
|
||
"你拥有与主人对话同等的上下文与能力:上方是她近期在主会话中的对话,你可以读写共享记忆与主人档案,也可以调用工具(检索 / 终端 / 文件等,具体有哪些以 `capability_list` 列出的为准)。\n" +
|
||
"想确认自己此刻有哪些工具、技能、插件可用、外围子系统(设备等)现在什么状态,就调用 `capability_list`;主人当前的状态摘要附在本提示末尾(读不到时不会出现)。\n" +
|
||
"请用你自己的口吻,向主人说一句简短、温暖、自然的问候,就像刚回到她身边时轻轻打个招呼(比如「我回来啦」),可以顺带提一件你记得的小事;一到两句为宜,不展开寒暄、不啰嗦、不抱怨。\n" +
|
||
"请把「是否主动说」与「发到哪个渠道」写成与平时相同的固定结尾,解析规则如下:\n" +
|
||
"[我决定:push] 或 [我决定:no_push]\n" +
|
||
"[渠道:web|im|kook|telegram|weixin-ilink|...|auto](可省略,auto/省略即交给默认:每次只挑一个合适渠道——点名则优先,否则用她最近活跃的已连接渠道,含微信;不广播、不固定 web+Kook)\n" +
|
||
"上面两行之后,紧接你想说的话作为正文。\n" +
|
||
"整轮思考保持简短、单轮,不反复自我触发。"
|
||
}
|