Files
antidrift/internal/ai/coach.go
T
2026-05-31 16:45:59 -04:00

75 lines
2.6 KiB
Go

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": "<one concrete action to start now>", "success_condition": "<observable, verifiable done state>", "timebox_minutes": <integer, typically 15-50>}
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": <true or false>, "reason": "<short explanation, one sentence>"}
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
}