Files
Cyrene_v2/internal/services/backthink/thinker.go
T
Aska 93aff2fea5
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(backthink): 自主思考强化 P1——主会话上下文+owner env+去压制(design/20)
backthink 从"心跳空转(独立sess-backthink+写死nothing to report宏)"改成"看得见主人、
能读写记忆、能调工具":think 读主会话最近20条(history.Recent,倒序映射)作为上下文 +
 重写引导prompt(温暖/有针对性/主动表达,删掉 Do not produce chat-style message) + 注入
 loop.OwnerEnv(MemWritable=true/ActorOwner/全工具)。
- context.go: mainContext/recentProvider/thinkInput/guidancePrompt
- thinker.go: LoopThinker 加 recent;Think 读主会话上下文+OwnerEnv+MutedBroadcast
- wire.go: NewLoopThinker(a.loop, a.history)
- 刻意偏离(安全): loop.Run session id 仍用 sess-backthink(不写"web"),避免与用户实时回合
  checkpoint/中断窗口复用污染;上下文来源+运行环境=主会话/owner env,目标达成。红线靠actor元数据。
- 测试: prompt无压制/有鼓励;主会话消息映射;nil退化;端到端Think(owner env 生效)
验收: build/vet/test 全绿+windows/darwin交叉编译绿. P2(主动推送+渠道选择)后置.
2026-09-07 19:21:09 +08:00

48 lines
1.9 KiB
Go

package backthink
import (
"context"
"fmt"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
)
// LoopThinker adapts a *loop.Loop into a Thinker. It is the production wiring
// that lets an autonomous background pass drive the full tool loop (memory
// injection, model steps, tool calls such as IoT/calendar/reminders) while seeing
// the main session's recent conversation and running under the owner environment.
type LoopThinker struct {
loop *loop.Loop
recent recentProvider
}
// NewLoopThinker wraps l as a Thinker over the given recent-messages provider
// (*history.Store in production; nil degrades the think to the guidance prompt
// alone, see mainContext).
func NewLoopThinker(l *loop.Loop, recent recentProvider) *LoopThinker {
return &LoopThinker{loop: l, recent: recent}
}
// Think runs one unattended think through the wrapped loop and returns the
// conclusion text. It inherits the main session's recent conversation as input
// context and injects the owner session environment (MemWritable=true, ActorOwner,
// full tool set) so the think can read/write memory and call tools. The loop's
// Run session id is the service's own (not the main session id) so the think never
// collides with the live main session's durable checkpoint store (design/20).
// It returns ErrNoLoop when no loop was injected.
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)
envCtx := loop.WithSessionEnv(ctx, loop.OwnerEnv())
// Mute UI broadcast: background reflection must not leak its intermediate
// reasoning into the user-facing chat stream (it only needs the conclusion).
res, err := t.loop.Run(loop.WithMutedBroadcast(envCtx), sessionID, messages)
if err != nil {
return "", fmt.Errorf("backthink: think: %w", err)
}
return res.Text, nil
}