X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/6e3e9c5c10f3d35bae7e9846334974a09418aef7..b2fc2d6a737dd5bab25e57ba822dd07258de93e6:/subagent.go?ds=sidebyside diff --git a/subagent.go b/subagent.go index 0248751..4cb6381 100644 --- a/subagent.go +++ b/subagent.go @@ -28,10 +28,11 @@ const ( // 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), + dir: dir, + clock: clock, + draw: draw, + open: make(map[string]*Subagent), + reserved: make(map[string]bool), } } @@ -44,8 +45,9 @@ 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 } // Create makes the directory and the log of a new subagent. It draws a name @@ -58,6 +60,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 +84,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,6 +159,7 @@ func (s *Subagents) Get(ctx context.Context, name string) (*Subagent, error) { subagent = &Subagent{ name: name, + dir: root, log: log, syncer: syncer, } @@ -123,24 +168,52 @@ func (s *Subagents) Get(ctx context.Context, name string) (*Subagent, error) { 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. +// 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 @@ -166,6 +239,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 +266,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()