]> git.giacomolli.uk - onechat/blobdiff - subagent.go
Separate multiline Go statements
[onechat] / subagent.go
index 024875161aae991f57028182c1e560ad7983745a..437aeb58d589ef7a545a1cdf1a95e3787bb0a2fa 100644 (file)
@@ -23,18 +23,6 @@ const (
        subagentNameDraws = 1000
 )
 
-// newSubagents returns the subagents in dir, one directory for each name. The
-// clock gives the dates of their messages, and draw gives the names of new
-// subagents.
-func newSubagents(dir string, clock func() time.Time, draw func() string) *Subagents {
-       return &Subagents{
-               dir:   dir,
-               clock: clock,
-               draw:  draw,
-               open:  make(map[string]*Subagent),
-       }
-}
-
 // Subagents holds the subagents of one agent. Each subagent has a directory
 // named for it, which holds its log. A log takes an exclusive lock, so
 // Subagents opens the log of a name once and keeps it open until Close. Its
@@ -44,8 +32,33 @@ type Subagents struct {
        clock func() time.Time
        draw  func() string
 
-       mutex sync.Mutex
-       open  map[string]*Subagent
+       mutex    sync.Mutex
+       open     map[string]*Subagent
+       reserved map[string]bool
+}
+
+// Subagent is one subagent, its directory, and its log. Its mutex protects the
+// log, because the run of the subagent writes it while the agent reads it with
+// zoom.
+type Subagent struct {
+       name   string
+       dir    string
+       mutex  sync.Mutex
+       log    *Log
+       syncer *Syncer
+}
+
+// newSubagents returns the subagents in dir, one directory for each name. The
+// clock gives the dates of their messages, and draw gives the names of new
+// subagents.
+func newSubagents(dir string, clock func() time.Time, draw func() string) *Subagents {
+       return &Subagents{
+               dir:      dir,
+               clock:    clock,
+               draw:     draw,
+               open:     make(map[string]*Subagent),
+               reserved: make(map[string]bool),
+       }
 }
 
 // Create makes the directory and the log of a new subagent. It draws a name
@@ -58,6 +71,15 @@ func (s *Subagents) Create(ctx context.Context) (*Subagent, error) {
 
        for range subagentNameDraws {
                name := s.draw()
+               s.mutex.Lock()
+
+               reserved := s.reserved[name]
+               s.mutex.Unlock()
+
+               if reserved {
+                       continue
+               }
+
                err = os.Mkdir(filepath.Join(s.dir, name), 0o700)
                if errors.Is(err, fs.ErrExist) {
                        continue
@@ -73,6 +95,39 @@ func (s *Subagents) Create(ctx context.Context) (*Subagent, error) {
        return nil, fmt.Errorf("no free subagent name after %d draws", subagentNameDraws)
 }
 
+// Reserve keeps a child name out of future subagent draws. An existing subagent
+// directory or a reserved name is refused.
+func (s *Subagents) Reserve(name string) error {
+       s.mutex.Lock()
+       defer s.mutex.Unlock()
+
+       if s.reserved[name] {
+               return fmt.Errorf("name %q is already used", name)
+       }
+
+       _, err := os.Lstat(filepath.Join(s.dir, name))
+       if err == nil {
+               return fmt.Errorf("name %q is already used by a subagent", name)
+       }
+
+       if !errors.Is(err, os.ErrNotExist) {
+               return fmt.Errorf("check subagent name: %w", err)
+       }
+
+       s.reserved[name] = true
+
+       return nil
+}
+
+// Release frees a reservation after a child fails to publish. It does not
+// remove a subagent directory or change an open log.
+func (s *Subagents) Release(name string) {
+       s.mutex.Lock()
+       defer s.mutex.Unlock()
+
+       delete(s.reserved, name)
+}
+
 // Get returns the subagent name with its log open. A name that is not valid or
 // that has no directory is an unknown agent.
 func (s *Subagents) Get(ctx context.Context, name string) (*Subagent, error) {
@@ -115,37 +170,56 @@ func (s *Subagents) Get(ctx context.Context, name string) (*Subagent, error) {
 
        subagent = &Subagent{
                name:   name,
+               dir:    root,
                log:    log,
                syncer: syncer,
        }
+
        s.open[name] = subagent
 
        return subagent, nil
 }
 
+// Snapshot flushes and locks the open logs while snapshot reads the subagent
+// files. snapshot must not call Subagents or an open Subagent.
+func (s *Subagents) Snapshot(ctx context.Context, snapshot func() error) error {
+       s.mutex.Lock()
+       defer s.mutex.Unlock()
+
+       for _, subagent := range s.open {
+               subagent.mutex.Lock()
+               defer subagent.mutex.Unlock()
+
+               err := subagent.Flush(ctx)
+               if err != nil {
+                       return fmt.Errorf("flush subagent snapshot: %w", err)
+               }
+       }
+
+       err := snapshot()
+       if err != nil {
+               return fmt.Errorf("copy subagent snapshot: %w", err)
+       }
+
+       return nil
+}
+
 // Close makes the writes of each open log durable and closes it.
 func (s *Subagents) Close() error {
        s.mutex.Lock()
        defer s.mutex.Unlock()
 
        var err error
+
        for name, subagent := range s.open {
                err = errors.Join(err, subagent.close())
+
                delete(s.open, name)
        }
 
        return err
 }
 
-// Subagent is one subagent and its log. Its mutex protects the log, because
-// the run of the subagent writes it while the agent reads it with zoom.
-type Subagent struct {
-       name   string
-       mutex  sync.Mutex
-       log    *Log
-       syncer *Syncer
-}
-
 // Append logs a message of the subagent.
 func (a *Subagent) Append(kind, text string) (Message, error) {
        a.mutex.Lock()
@@ -166,6 +240,14 @@ func (a *Subagent) Flush(ctx context.Context) error {
        return a.syncer.Flush(ctx)
 }
 
+// First returns the first message of the log.
+func (a *Subagent) First() (Message, error) {
+       a.mutex.Lock()
+       defer a.mutex.Unlock()
+
+       return a.log.Message(0)
+}
+
 // Show returns the whole log, each message as "i|kind: text", with a blank line
 // between messages.
 func (a *Subagent) Show() (string, error) {
@@ -185,6 +267,12 @@ func (a *Subagent) Show() (string, error) {
        return strings.Join(lines, "\n\n"), nil
 }
 
+// WorkDir returns the directory where the tools of the subagent run: work in
+// its directory.
+func (a *Subagent) WorkDir() string {
+       return filepath.Join(a.dir, "work")
+}
+
 // close makes the writes durable and closes the log.
 func (a *Subagent) close() error {
        a.mutex.Lock()