package service import ( "context" "gitea.stevedudenhoeffer.com/steve/pansy/internal/domain" ) // Chat persistence for the garden assistant (#56). The run loop itself lives in // internal/agent; this is the part that needs pansy's permission checks. // maxRetainedTurns caps how much of a thread is kept in context. Retained turns // are a working memory, not an archive — a season of chat replayed into every // prompt would cost more than it informs. const maxRetainedTurns = 20 // AgentHistory returns the recent messages of the actor's own thread for a // garden they can edit, oldest first. // // Requires EDITOR, not viewer: the assistant acts, so being able to talk to it // about a garden is the same permission as being able to change it. A viewer // asking it to plant something would get a refusal from every tool anyway, and // offering the conversation would be offering something that cannot work. func (s *Service) AgentHistory(ctx context.Context, actorID, gardenID int64) ([]domain.AgentMessage, error) { if _, err := s.requireGardenRole(ctx, actorID, gardenID, roleEditor); err != nil { return nil, err } conv, err := s.store.EnsureConversation(ctx, gardenID, actorID) if err != nil { return nil, err } return s.store.ListAgentMessages(ctx, conv, maxRetainedTurns*2) } // RecordAgentExchange stores one user message and the assistant's reply, and // returns the reply row. Called by the runtime after a turn completes. func (s *Service) RecordAgentExchange(ctx context.Context, actorID, gardenID int64, userMessage, reply string, changeSetID *int64) (*domain.AgentMessage, error) { if _, err := s.requireGardenRole(ctx, actorID, gardenID, roleEditor); err != nil { return nil, err } conv, err := s.store.EnsureConversation(ctx, gardenID, actorID) if err != nil { return nil, err } if _, err := s.store.AppendAgentMessage(ctx, &domain.AgentMessage{ ConversationID: conv, Role: domain.AgentRoleUser, Body: userMessage, }); err != nil { return nil, err } return s.store.AppendAgentMessage(ctx, &domain.AgentMessage{ ConversationID: conv, Role: domain.AgentRoleAssistant, Body: reply, ChangeSetID: changeSetID, }) } // ClearAgentHistory drops the actor's thread for a garden — the "start over" // escape hatch when a conversation has gone somewhere unhelpful. func (s *Service) ClearAgentHistory(ctx context.Context, actorID, gardenID int64) error { if _, err := s.requireGardenRole(ctx, actorID, gardenID, roleEditor); err != nil { return err } return s.store.DeleteConversation(ctx, gardenID, actorID) }