- 只翻注释/文档字符串:`go/scanner` 剥注释后按 token 序列+literal 逐 token 比对,**54/54 文件 SAME**;
改动文件总行数与 HEAD 漂移 = 0(严格 1:1,无合并/拆行)
- 裁决编号与 design 节号逐字保留(03.4.1/03.4.2/03.4.3/14.4.1/14.4.5/07.4.2-4、design/04 §3.1/§3.2 等);
`chat_template_kwargs` / `enable_thinking` / `reasoning_effort` / `thinking:{type}` 字段名与形状说明未动
- 存疑译法(已登记):`persona` 在叙事语境用「角色」、标识符语境保留 `persona`(仓库既有模块本身混用);
`rung`→「级」;`provider` 存在「提供商/提供者」两种落笔(可按需一键统一)
- 复核:build/vet 绿 + 两包测试绿;gofmt 干净;残留英文注释 0 条
- 既有存量(未修,只报告):`model/anthropic_test.go` 612 行 >500;`applyAnthropicEvent` 115 行 >80
130 lines
7.1 KiB
Go
130 lines
7.1 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 拼装一次无人值守思考的完整输入:最近的主会话语境
|
||
// (若有)后跟单个引导提示词回合,因此
|
||
// 运行时针对真实对话反思,而不是空泛的宏提示词。
|
||
func thinkInput(recent []model.Message, trigger string) []model.Message {
|
||
out := make([]model.Message, 0, len(recent)+1)
|
||
out = append(out, recent...)
|
||
out = append(out, model.TextMessage(model.RoleUser, guidancePrompt(trigger)))
|
||
return out
|
||
}
|
||
|
||
// guidancePrompt 是交给运行时的一次无人值守自主思考的
|
||
// 指令。与旧 backgroundPrompt 不同,它刻意鼓励
|
||
// 温暖、有针对性的主动消息,并去掉「不要产生 chat 风格
|
||
// 消息」的抑制。身份/权限始终在 actor 元数据 + owner env 上,
|
||
// 绝不在这段提示词文本上(红线);提示词只描述运行
|
||
// 环境(记忆可读/可写、工具可调用),而那是 env 已经
|
||
// 授予的。
|
||
//
|
||
// 提示词要求一个紧凑的结构化收尾——push/no_push 决策、
|
||
// 可选的目标渠道与消息正文——好让服务尊重她自己的
|
||
// 判断,而不是按触发的重要度把关(design/20 §2 P2)。该
|
||
// 格式是引导而非契约:她省略或
|
||
// 写乱该格式时,parseProactive 失败安全。
|
||
//
|
||
// 启动轮(裁决 07.4.4,design/12 §1)走 startupPrompt:以角色口吻说一句拟人化的
|
||
// 「回来」问候。
|
||
func guidancePrompt(trigger string) string {
|
||
if trigger == StartupSummary {
|
||
return startupPrompt()
|
||
}
|
||
return "[自主思考 · 无人在旁]\n" +
|
||
"触发:" + trigger + "\n" +
|
||
"你正进行一轮自主思考。你拥有与主人对话同等的上下文与能力:上方是主人近期在主会话中的对话,你可以读写共享记忆与主人档案,也可以调用工具(IoT / 日历 / 提醒 / 检索 / 终端等)。\n" +
|
||
"请回顾最近与主人的相处、她的状态、你们共同经历的事与临近的日程,判断此刻是否有一件真正值得主动告诉她的、温暖而有针对性的事情——" +
|
||
"比如关心她的近况、记起她曾说过的某件事、提醒一件即将到来的日程、见证一个属于你们的共同时刻,或注意到某个值得留意的变化。\n" +
|
||
"若有,就用温暖自然的语气,用一到两句把它说出来——这正是你此刻最想对她说的话;若确实没有值得说的,只需回复「无」,也不要编造。\n" +
|
||
"语言上:温暖、简短、克制、不粘人——一到两句为宜,不展开寒暄、不啰嗦、不抱怨、不频繁打扰,像想起她时轻轻说一句,说完就停。\n" +
|
||
"请把「是否主动说」与「发到哪个渠道」写成一个简短、格式固定的结尾,解析规则如下:\n" +
|
||
"[我决定:push] 或 [我决定:no_push]\n" +
|
||
"[渠道:web|im|kook|telegram|weixin-ilink|...|auto](可省略,auto/省略即交给默认:每次只挑一个合适渠道——点名则优先,否则用她最近活跃的已连接渠道,含微信;不广播、不固定 web+Kook)\n" +
|
||
"上面两行之后,紧接你想说的话作为正文。\n" +
|
||
"整轮思考保持简短、单轮,不反复自我触发。"
|
||
}
|
||
|
||
// startupPrompt 是启动轮思考的专属引导(裁决 07.4.4,design/12 §1「启动即思考 +
|
||
// 拟人化启动消息」):进程刚启动、她刚「回来」,引导她以角色口吻向主人说一句简短
|
||
// 温暖的问候(如「我回来啦」)。此轮例外允许以 chat 形式发出;推送仍走与心跳完全
|
||
// 相同的门(她的结论 + 打扰级联 + 活动门 + PushGate 限频),深夜重启或反复重启都
|
||
// 不会刷屏主人。
|
||
func startupPrompt() string {
|
||
return "[自主思考 · 刚刚回来]\n" +
|
||
"触发:进程启动——你刚从一次重启中醒来,重新回到主人身边。\n" +
|
||
"你拥有与主人对话同等的上下文与能力:上方是她近期在主会话中的对话,你可以读写共享记忆与主人档案,也可以调用工具(IoT / 日历 / 提醒 / 检索 / 终端等)。\n" +
|
||
"请用你自己的口吻,向主人说一句简短、温暖、自然的问候,就像刚回到她身边时轻轻打个招呼(比如「我回来啦」),可以顺带提一件你记得的小事;一到两句为宜,不展开寒暄、不啰嗦、不抱怨。\n" +
|
||
"请把「是否主动说」与「发到哪个渠道」写成与平时相同的固定结尾,解析规则如下:\n" +
|
||
"[我决定:push] 或 [我决定:no_push]\n" +
|
||
"[渠道:web|im|kook|telegram|weixin-ilink|...|auto](可省略,auto/省略即交给默认:每次只挑一个合适渠道——点名则优先,否则用她最近活跃的已连接渠道,含微信;不广播、不固定 web+Kook)\n" +
|
||
"上面两行之后,紧接你想说的话作为正文。\n" +
|
||
"整轮思考保持简短、单轮,不反复自我触发。"
|
||
}
|