feat: complete v0.3.1 agent workflow
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"agentu/internal/safeio"
|
||||
"agentu/pkg/llm"
|
||||
)
|
||||
|
||||
@@ -54,8 +55,11 @@ func NewStore(dir string) (*Store, error) {
|
||||
|
||||
// Save writes a session to disk.
|
||||
func (s *Store) Save(sess *Session) error {
|
||||
if sess.ID == "" {
|
||||
return fmt.Errorf("session ID is required")
|
||||
if sess == nil {
|
||||
return fmt.Errorf("session is required")
|
||||
}
|
||||
if err := validateSessionID(sess.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
sess.UpdatedAt = time.Now().UTC()
|
||||
if sess.CreatedAt.IsZero() {
|
||||
@@ -66,14 +70,17 @@ func (s *Store) Save(sess *Session) error {
|
||||
return fmt.Errorf("marshal session: %w", err)
|
||||
}
|
||||
path := s.filePath(sess.ID)
|
||||
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||
if err := safeio.AtomicWriteFile(path, data, 0o644); err != nil {
|
||||
return fmt.Errorf("write session: %w", err)
|
||||
}
|
||||
return nil
|
||||
return s.updateIndex(metaFromSession(sess))
|
||||
}
|
||||
|
||||
// Load reads a session by ID.
|
||||
func (s *Store) Load(id string) (*Session, error) {
|
||||
if err := validateSessionID(id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path := s.filePath(id)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
@@ -91,13 +98,27 @@ func (s *Store) Load(id string) (*Session, error) {
|
||||
|
||||
// List returns metadata for all sessions, sorted by updated_at descending.
|
||||
func (s *Store) List() ([]SessionMeta, error) {
|
||||
if metas, err := s.loadIndex(); err == nil {
|
||||
return metas, nil
|
||||
}
|
||||
metas, err := s.scanSessions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.saveIndexFromSessions(metas); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return metas, nil
|
||||
}
|
||||
|
||||
func (s *Store) scanSessions() ([]SessionMeta, error) {
|
||||
entries, err := os.ReadDir(s.dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read session dir: %w", err)
|
||||
}
|
||||
var metas []SessionMeta
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
|
||||
if entry.IsDir() || entry.Name() == sessionIndexFile || !strings.HasSuffix(entry.Name(), ".json") {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(s.dir, entry.Name())
|
||||
@@ -127,6 +148,9 @@ func (s *Store) List() ([]SessionMeta, error) {
|
||||
|
||||
// Delete removes a session file.
|
||||
func (s *Store) Delete(id string) error {
|
||||
if err := validateSessionID(id); err != nil {
|
||||
return err
|
||||
}
|
||||
path := s.filePath(id)
|
||||
if err := os.Remove(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@@ -134,13 +158,64 @@ func (s *Store) Delete(id string) error {
|
||||
}
|
||||
return fmt.Errorf("delete session: %w", err)
|
||||
}
|
||||
return nil
|
||||
metas, err := s.loadIndex()
|
||||
if err != nil {
|
||||
metas, err = s.scanSessions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
filtered := metas[:0]
|
||||
for _, meta := range metas {
|
||||
if meta.ID != id {
|
||||
filtered = append(filtered, meta)
|
||||
}
|
||||
}
|
||||
return s.saveIndexFromSessions(filtered)
|
||||
}
|
||||
|
||||
func (s *Store) filePath(id string) string {
|
||||
return filepath.Join(s.dir, id+".json")
|
||||
}
|
||||
|
||||
func validateSessionID(id string) error {
|
||||
if id == "" {
|
||||
return fmt.Errorf("session ID is required")
|
||||
}
|
||||
if id == "." || id == ".." {
|
||||
return fmt.Errorf("invalid session ID %q", id)
|
||||
}
|
||||
for _, r := range id {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' || r == '.' {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("invalid session ID %q", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) updateIndex(meta SessionMeta) error {
|
||||
metas, err := s.loadIndex()
|
||||
if err != nil {
|
||||
metas, err = s.scanSessions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
found := false
|
||||
for i := range metas {
|
||||
if metas[i].ID == meta.ID {
|
||||
metas[i] = meta
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
metas = append(metas, meta)
|
||||
}
|
||||
return s.saveIndexFromSessions(metas)
|
||||
}
|
||||
|
||||
// GenerateID creates a random 8-character hex session ID.
|
||||
func GenerateID() string {
|
||||
b := make([]byte, 4)
|
||||
|
||||
Reference in New Issue
Block a user