Build image / build-and-push (push) Successful in 6s
Co-authored-by: Steve Dudenhoeffer <[email protected]>
63 lines
2.5 KiB
Go
63 lines
2.5 KiB
Go
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)
|
|
}
|