// Package agent holds the turn loop and the conversation state it runs on. // // It must not import the ui package: the eval runner in Phase 3 drives this // loop headless, and anything terminal-shaped here would make that impossible. package agent import ( "sync" "time" "harness/internal/llm" ) // ReadRecord is when a file was last read and what its mtime was at the time. // Phase 2's edit_file uses this to refuse edits to files the model has not read // and to force a re-read when the file has changed underneath it. type ReadRecord struct { At time.Time Mod time.Time } // State is the conversation history plus the per-session file-read tracking. type State struct { mu sync.Mutex messages []llm.Message reads map[string]ReadRecord usage llm.Usage turns int } func NewState() *State { return &State{reads: map[string]ReadRecord{}} } // MarkRead implements tools.ReadTracker. func (s *State) MarkRead(path string, mod time.Time) { s.mu.Lock() defer s.mu.Unlock() s.reads[path] = ReadRecord{At: time.Now(), Mod: mod} } // LastRead returns the read record for path, if the file has been read. func (s *State) LastRead(path string) (ReadRecord, bool) { s.mu.Lock() defer s.mu.Unlock() r, ok := s.reads[path] return r, ok } // Append adds a message to the history. func (s *State) Append(m llm.Message) { s.mu.Lock() defer s.mu.Unlock() s.messages = append(s.messages, m) } // History returns a copy of the conversation so far. func (s *State) History() []llm.Message { s.mu.Lock() defer s.mu.Unlock() return append([]llm.Message(nil), s.messages...) } // Len is the number of messages in the history. func (s *State) Len() int { s.mu.Lock() defer s.mu.Unlock() return len(s.messages) } // AddUsage accumulates the token counts of one response. func (s *State) AddUsage(u llm.Usage) { s.mu.Lock() defer s.mu.Unlock() s.usage.InputTokens += u.InputTokens s.usage.OutputTokens += u.OutputTokens s.usage.CacheReadInputTokens += u.CacheReadInputTokens s.usage.CacheCreationInputTokens += u.CacheCreationInputTokens s.turns++ } // Usage is the running total for the session, and the number of model turns it // took to get there. func (s *State) Usage() (llm.Usage, int) { s.mu.Lock() defer s.mu.Unlock() return s.usage, s.turns }