17 // subagentDirName is the directory of the subagents in the directory of
19 subagentDirName = "subagents"
21 // subagentNameDraws is the most names that Create draws before it
23 subagentNameDraws = 1000
26 // Subagents holds the subagents of one agent. Each subagent has a directory
27 // named for it, which holds its log. A log takes an exclusive lock, so
28 // Subagents opens the log of a name once and keeps it open until Close. Its
29 // mutex protects the open logs.
30 type Subagents struct {
32 clock func() time.Time
36 open map[string]*Subagent
37 reserved map[string]bool
40 // Subagent is one subagent, its directory, and its log. Its mutex protects the
41 // log, because the run of the subagent writes it while the agent reads it with
43 type Subagent struct {
51 // newSubagents returns the subagents in dir, one directory for each name. The
52 // clock gives the dates of their messages, and draw gives the names of new
54 func newSubagents(dir string, clock func() time.Time, draw func() string) *Subagents {
59 open: make(map[string]*Subagent),
60 reserved: make(map[string]bool),
64 // Create makes the directory and the log of a new subagent. It draws a name
65 // again when the directory of the name exists.
66 func (s *Subagents) Create(ctx context.Context) (*Subagent, error) {
67 err := os.MkdirAll(s.dir, 0o700)
69 return nil, fmt.Errorf("create subagent directory: %w", err)
72 for range subagentNameDraws {
76 reserved := s.reserved[name]
83 err = os.Mkdir(filepath.Join(s.dir, name), 0o700)
84 if errors.Is(err, fs.ErrExist) {
89 return nil, fmt.Errorf("create subagent directory: %w", err)
92 return s.Get(ctx, name)
95 return nil, fmt.Errorf("no free subagent name after %d draws", subagentNameDraws)
98 // Reserve keeps a child name out of future subagent draws. An existing subagent
99 // directory or a reserved name is refused.
100 func (s *Subagents) Reserve(name string) error {
102 defer s.mutex.Unlock()
104 if s.reserved[name] {
105 return fmt.Errorf("name %q is already used", name)
108 _, err := os.Lstat(filepath.Join(s.dir, name))
110 return fmt.Errorf("name %q is already used by a subagent", name)
113 if !errors.Is(err, os.ErrNotExist) {
114 return fmt.Errorf("check subagent name: %w", err)
117 s.reserved[name] = true
122 // Release frees a reservation after a child fails to publish. It does not
123 // remove a subagent directory or change an open log.
124 func (s *Subagents) Release(name string) {
126 defer s.mutex.Unlock()
128 delete(s.reserved, name)
131 // Get returns the subagent name with its log open. A name that is not valid or
132 // that has no directory is an unknown agent.
133 func (s *Subagents) Get(ctx context.Context, name string) (*Subagent, error) {
135 defer s.mutex.Unlock()
137 subagent, found := s.open[name]
142 unknown := fmt.Errorf("no agent %s", name)
143 if !subagentNameValid(name) {
147 root := filepath.Join(s.dir, name)
148 info, err := os.Stat(root)
149 if errors.Is(err, fs.ErrNotExist) {
154 return nil, fmt.Errorf("read subagent directory: %w", err)
161 log, err := openLog(ctx, root, s.clock)
163 return nil, fmt.Errorf("open log of %s: %w", name, err)
166 syncer, err := openSyncer(root)
168 return nil, errors.Join(fmt.Errorf("open log of %s: %w", name, err), log.Close())
171 subagent = &Subagent{
178 s.open[name] = subagent
183 // Snapshot flushes and locks the open logs while snapshot reads the subagent
184 // files. snapshot must not call Subagents or an open Subagent.
185 func (s *Subagents) Snapshot(ctx context.Context, snapshot func() error) error {
187 defer s.mutex.Unlock()
189 for _, subagent := range s.open {
190 subagent.mutex.Lock()
191 defer subagent.mutex.Unlock()
193 err := subagent.Flush(ctx)
195 return fmt.Errorf("flush subagent snapshot: %w", err)
201 return fmt.Errorf("copy subagent snapshot: %w", err)
207 // Close makes the writes of each open log durable and closes it.
208 func (s *Subagents) Close() error {
210 defer s.mutex.Unlock()
214 for name, subagent := range s.open {
215 err = errors.Join(err, subagent.close())
223 // Append logs a message of the subagent.
224 func (a *Subagent) Append(kind, text string) (Message, error) {
226 defer a.mutex.Unlock()
228 message, err := a.log.Append(kind, text)
230 return Message{}, err
238 // Flush returns when every write before the call is durable.
239 func (a *Subagent) Flush(ctx context.Context) error {
240 return a.syncer.Flush(ctx)
243 // First returns the first message of the log.
244 func (a *Subagent) First() (Message, error) {
246 defer a.mutex.Unlock()
248 return a.log.Message(0)
251 // Show returns the whole log, each message as "i|kind: text", with a blank line
253 func (a *Subagent) Show() (string, error) {
255 defer a.mutex.Unlock()
257 lines := make([]string, 0, a.log.Len())
258 for id := range a.log.Len() {
259 message, err := a.log.Message(id)
264 lines = append(lines, strconv.Itoa(id)+"|"+leafSource(message))
267 return strings.Join(lines, "\n\n"), nil
270 // WorkDir returns the directory where the tools of the subagent run: work in
272 func (a *Subagent) WorkDir() string {
273 return filepath.Join(a.dir, "work")
276 // close makes the writes durable and closes the log.
277 func (a *Subagent) close() error {
279 defer a.mutex.Unlock()
281 return errors.Join(a.syncer.Close(), a.log.Close())