package ai import "context" // Coach turns a free-text intent into a validated Proposal. type Coach interface { Coach(ctx context.Context, intent string) (Proposal, error) } // Backend is one way to reach an LLM CLI. Adapters differ only in the command // and arguments they run; each returns the model's text answer. type Backend interface { Run(ctx context.Context, prompt string) (string, error) Name() string } // Service implements Coach over any Backend. type Service struct { backend Backend } func NewService(b Backend) *Service { return &Service{backend: b} } func (s *Service) Coach(ctx context.Context, intent string) (Proposal, error) { out, err := s.backend.Run(ctx, buildPrompt(intent)) if err != nil { return Proposal{}, err } return parseProposal(out) } func buildPrompt(intent string) string { return `You are a focus coach. The user gives a rough intent for a work session. Turn it into ONE concrete, actionable commitment. Respond with ONLY a JSON object, no prose and no code fences, exactly this shape: {"next_action": "", "success_condition": "", "timebox_minutes": } Rules: - next_action: a single concrete imperative action, doable now. - success_condition: observable and verifiable; how you'd know it is done. - timebox_minutes: a realistic integer number of minutes for the action. User intent: ` + intent } // DriftJudge decides whether the current window is on-task for a commitment. // It takes primitives, not domain/evidence types, so ai stays a leaf package. type DriftJudge interface { JudgeDrift(ctx context.Context, commitment, windowClass, windowTitle string) (Verdict, error) } // JudgeDrift makes Service satisfy DriftJudge over the same backend as Coach. func (s *Service) JudgeDrift(ctx context.Context, commitment, windowClass, windowTitle string) (Verdict, error) { out, err := s.backend.Run(ctx, buildDriftPrompt(commitment, windowClass, windowTitle)) if err != nil { return Verdict{}, err } return parseVerdict(out) } func buildDriftPrompt(commitment, class, title string) string { return `You are a focus monitor. The user committed to a task. Decide whether their CURRENT window is on-task or a distraction. Respond with ONLY a JSON object, no prose and no code fences, exactly this shape: {"on_task": , "reason": ""} Rules: - on_task: true if the window plausibly serves the commitment, false if it is a distraction. - reason: one short sentence. REQUIRED when on_task is false. Commitment: ` + commitment + ` Current window class: ` + class + ` Current window title: ` + title }