package backthink import ( "context" "fmt" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop" ) // LoopThinker 把 *loop.Loop 适配为 Thinker。它是生产接线, // 让一次自主后台轮驱动完整的工具循环(记忆 // 注入、模型步骤、工具调用),同时看到主会话的最近对话并在 owner 环境下运行。 // // 它包的就是**主人对话所用的那一个 loop**(组合根传 a.loop),因此「与 chat 同一套 // 工具注册表」不是承诺而是结构事实:两侧共用同一个 dispatcher/registry。 type LoopThinker struct { loop *loop.Loop recent recentProvider } // NewLoopThinker 把 l 包装成基于给定最近消息提供者的 Thinker // (生产中是 *history.Store;nil 会让思考降级为仅用引导提示词, // 见 mainContext)。 func NewLoopThinker(l *loop.Loop, recent recentProvider) *LoopThinker { return &LoopThinker{loop: l, recent: recent} } // Think 经被包装的 loop 跑一轮无人值守思考并返回 // 结论文本。它继承主会话的最近对话作为输入 // 上下文,并注入 owner 会话环境(MemWritable=true、ActorOwner、 // 完整工具集),因此该次思考能读写记忆并调用工具。loop 的 // Run 会话 id 是本服务自己的(不是主会话 id),所以该次思考绝不会 // 与活跃主会话的持久化检查点存储冲突(design/20)。 // // 本轮的用户状态快照由服务经 ctx 挂上(WithRoundState,X1 缺口 d): // 读到时它被渲染成引导提示词末尾的状态块,与随后的打扰决策同一份; // 未挂载时输入逐字与接线前一致。未注入 loop 时返回 ErrNoLoop。 func (t *LoopThinker) Think(ctx context.Context, sessionID, trigger string) (string, error) { if t.loop == nil { return "", ErrNoLoop } recent := mainContext(ctx, t.recent) messages := thinkInput(recent, trigger, RoundStateFrom(ctx)) envCtx := loop.WithSessionEnv(ctx, loop.OwnerEnv()) // 静音 UI 广播:后台反思不得把其中间 // 推理泄漏进面向用户的 chat 流(它只需要结论)。 res, err := t.loop.Run(loop.WithMutedBroadcast(envCtx), sessionID, messages) if err != nil { return "", fmt.Errorf("backthink: think: %w", err) } return res.Text, nil }