+package main
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+)
+
+const (
+ // subagentDirName is the directory of the subagents in the directory of
+ // an agent.
+ subagentDirName = "subagents"
+
+ // subagentNameDraws is the most names that Create draws before it
+ // fails.
+ 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
+// mutex protects the open logs.
+type Subagents struct {
+ dir string
+ clock func() time.Time
+ draw func() string
+
+ mutex sync.Mutex
+ open map[string]*Subagent
+}
+
+// Create makes the directory and the log of a new subagent. It draws a name
+// again when the directory of the name exists.
+func (s *Subagents) Create(ctx context.Context) (*Subagent, error) {
+ err := os.MkdirAll(s.dir, 0o700)
+ if err != nil {
+ return nil, fmt.Errorf("create subagent directory: %w", err)
+ }
+
+ for range subagentNameDraws {
+ name := s.draw()
+ err = os.Mkdir(filepath.Join(s.dir, name), 0o700)
+ if errors.Is(err, fs.ErrExist) {
+ continue
+ }
+
+ if err != nil {
+ return nil, fmt.Errorf("create subagent directory: %w", err)
+ }
+
+ return s.Get(ctx, name)
+ }
+
+ return nil, fmt.Errorf("no free subagent name after %d draws", subagentNameDraws)
+}
+
+// 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) {
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+
+ subagent, found := s.open[name]
+ if found {
+ return subagent, nil
+ }
+
+ unknown := fmt.Errorf("no agent %s", name)
+ if !subagentNameValid(name) {
+ return nil, unknown
+ }
+
+ root := filepath.Join(s.dir, name)
+ info, err := os.Stat(root)
+ if errors.Is(err, fs.ErrNotExist) {
+ return nil, unknown
+ }
+
+ if err != nil {
+ return nil, fmt.Errorf("read subagent directory: %w", err)
+ }
+
+ if !info.IsDir() {
+ return nil, unknown
+ }
+
+ log, err := openLog(ctx, root, s.clock)
+ if err != nil {
+ return nil, fmt.Errorf("open log of %s: %w", name, err)
+ }
+
+ syncer, err := openSyncer(root)
+ if err != nil {
+ return nil, errors.Join(fmt.Errorf("open log of %s: %w", name, err), log.Close())
+ }
+
+ subagent = &Subagent{
+ name: name,
+ log: log,
+ syncer: syncer,
+ }
+ s.open[name] = subagent
+
+ return subagent, 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()
+ defer a.mutex.Unlock()
+
+ message, err := a.log.Append(kind, text)
+ if err != nil {
+ return Message{}, err
+ }
+
+ a.syncer.Written()
+
+ return message, nil
+}
+
+// Flush returns when every write before the call is durable.
+func (a *Subagent) Flush(ctx context.Context) error {
+ return a.syncer.Flush(ctx)
+}
+
+// Show returns the whole log, each message as "i|kind: text", with a blank line
+// between messages.
+func (a *Subagent) Show() (string, error) {
+ a.mutex.Lock()
+ defer a.mutex.Unlock()
+
+ lines := make([]string, 0, a.log.Len())
+ for id := range a.log.Len() {
+ message, err := a.log.Message(id)
+ if err != nil {
+ return "", err
+ }
+
+ lines = append(lines, strconv.Itoa(id)+"|"+leafSource(message))
+ }
+
+ return strings.Join(lines, "\n\n"), nil
+}
+
+// close makes the writes durable and closes the log.
+func (a *Subagent) close() error {
+ a.mutex.Lock()
+ defer a.mutex.Unlock()
+
+ return errors.Join(a.syncer.Close(), a.log.Close())
+}