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 }