]> git.giacomolli.uk - onechat/blob - subagent.go
Name the tools that check the Go rules
[onechat] / subagent.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "io/fs"
8 "os"
9 "path/filepath"
10 "strconv"
11 "strings"
12 "sync"
13 "time"
14 )
15
16 const (
17 // subagentDirName is the directory of the subagents in the directory of
18 // an agent.
19 subagentDirName = "subagents"
20
21 // subagentNameDraws is the most names that Create draws before it
22 // fails.
23 subagentNameDraws = 1000
24 )
25
26 // newSubagents returns the subagents in dir, one directory for each name. The
27 // clock gives the dates of their messages, and draw gives the names of new
28 // subagents.
29 func newSubagents(dir string, clock func() time.Time, draw func() string) *Subagents {
30 return &Subagents{
31 dir: dir,
32 clock: clock,
33 draw: draw,
34 open: make(map[string]*Subagent),
35 }
36 }
37
38 // Subagents holds the subagents of one agent. Each subagent has a directory
39 // named for it, which holds its log. A log takes an exclusive lock, so
40 // Subagents opens the log of a name once and keeps it open until Close. Its
41 // mutex protects the open logs.
42 type Subagents struct {
43 dir string
44 clock func() time.Time
45 draw func() string
46
47 mutex sync.Mutex
48 open map[string]*Subagent
49 }
50
51 // Create makes the directory and the log of a new subagent. It draws a name
52 // again when the directory of the name exists.
53 func (s *Subagents) Create(ctx context.Context) (*Subagent, error) {
54 err := os.MkdirAll(s.dir, 0o700)
55 if err != nil {
56 return nil, fmt.Errorf("create subagent directory: %w", err)
57 }
58
59 for range subagentNameDraws {
60 name := s.draw()
61 err = os.Mkdir(filepath.Join(s.dir, name), 0o700)
62 if errors.Is(err, fs.ErrExist) {
63 continue
64 }
65
66 if err != nil {
67 return nil, fmt.Errorf("create subagent directory: %w", err)
68 }
69
70 return s.Get(ctx, name)
71 }
72
73 return nil, fmt.Errorf("no free subagent name after %d draws", subagentNameDraws)
74 }
75
76 // Get returns the subagent name with its log open. A name that is not valid or
77 // that has no directory is an unknown agent.
78 func (s *Subagents) Get(ctx context.Context, name string) (*Subagent, error) {
79 s.mutex.Lock()
80 defer s.mutex.Unlock()
81
82 subagent, found := s.open[name]
83 if found {
84 return subagent, nil
85 }
86
87 unknown := fmt.Errorf("no agent %s", name)
88 if !subagentNameValid(name) {
89 return nil, unknown
90 }
91
92 root := filepath.Join(s.dir, name)
93 info, err := os.Stat(root)
94 if errors.Is(err, fs.ErrNotExist) {
95 return nil, unknown
96 }
97
98 if err != nil {
99 return nil, fmt.Errorf("read subagent directory: %w", err)
100 }
101
102 if !info.IsDir() {
103 return nil, unknown
104 }
105
106 log, err := openLog(ctx, root, s.clock)
107 if err != nil {
108 return nil, fmt.Errorf("open log of %s: %w", name, err)
109 }
110
111 syncer, err := openSyncer(root)
112 if err != nil {
113 return nil, errors.Join(fmt.Errorf("open log of %s: %w", name, err), log.Close())
114 }
115
116 subagent = &Subagent{
117 name: name,
118 dir: root,
119 log: log,
120 syncer: syncer,
121 }
122 s.open[name] = subagent
123
124 return subagent, nil
125 }
126
127 // Close makes the writes of each open log durable and closes it.
128 func (s *Subagents) Close() error {
129 s.mutex.Lock()
130 defer s.mutex.Unlock()
131
132 var err error
133
134 for name, subagent := range s.open {
135 err = errors.Join(err, subagent.close())
136
137 delete(s.open, name)
138 }
139
140 return err
141 }
142
143 // Subagent is one subagent, its directory, and its log. Its mutex protects the
144 // log, because the run of the subagent writes it while the agent reads it with
145 // zoom.
146 type Subagent struct {
147 name string
148 dir string
149 mutex sync.Mutex
150 log *Log
151 syncer *Syncer
152 }
153
154 // Append logs a message of the subagent.
155 func (a *Subagent) Append(kind, text string) (Message, error) {
156 a.mutex.Lock()
157 defer a.mutex.Unlock()
158
159 message, err := a.log.Append(kind, text)
160 if err != nil {
161 return Message{}, err
162 }
163
164 a.syncer.Written()
165
166 return message, nil
167 }
168
169 // Flush returns when every write before the call is durable.
170 func (a *Subagent) Flush(ctx context.Context) error {
171 return a.syncer.Flush(ctx)
172 }
173
174 // First returns the first message of the log.
175 func (a *Subagent) First() (Message, error) {
176 a.mutex.Lock()
177 defer a.mutex.Unlock()
178
179 return a.log.Message(0)
180 }
181
182 // Show returns the whole log, each message as "i|kind: text", with a blank line
183 // between messages.
184 func (a *Subagent) Show() (string, error) {
185 a.mutex.Lock()
186 defer a.mutex.Unlock()
187
188 lines := make([]string, 0, a.log.Len())
189 for id := range a.log.Len() {
190 message, err := a.log.Message(id)
191 if err != nil {
192 return "", err
193 }
194
195 lines = append(lines, strconv.Itoa(id)+"|"+leafSource(message))
196 }
197
198 return strings.Join(lines, "\n\n"), nil
199 }
200
201 // WorkDir returns the directory where the tools of the subagent run: work in
202 // its directory.
203 func (a *Subagent) WorkDir() string {
204 return filepath.Join(a.dir, "work")
205 }
206
207 // close makes the writes durable and closes the log.
208 func (a *Subagent) close() error {
209 a.mutex.Lock()
210 defer a.mutex.Unlock()
211
212 return errors.Join(a.syncer.Close(), a.log.Close())
213 }