]> git.giacomolli.uk - onechat/blob - turn.go
051c74df822c2c95b4299e40327f5cf0cad304ee
[onechat] / turn.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "path/filepath"
7 "sync"
8 "time"
9 )
10
11 // newTurnLoop returns the loop of agent with its memory. Its calls go through
12 // client, and notify gets each event. The clock gives the date of a turn.
13 func newTurnLoop(agent *Agent, memory *Memory, client ChainClient, clock func() time.Time, notify func(Event)) (*TurnLoop, error) {
14 layout, err := loadViewLayout(filepath.Join(agent.dir, viewLayoutFileName))
15 if err != nil {
16 return nil, err
17 }
18
19 l := &TurnLoop{
20 agent: agent,
21 memory: memory,
22 client: client,
23 layout: layout,
24 clock: clock,
25 notify: notify,
26 wake: make(chan struct{}, 1),
27 }
28
29 l.tools = &Toolbox{
30 log: l,
31 chat: memory,
32 workdir: agent.config.Workdir,
33 }
34
35 return l, nil
36 }
37
38 // Event tells the clients what the agent does as it happens. Kind is the kind
39 // of a logged message (user, reply, tool, echo), or one of ready, busy, idle,
40 // and error. ID is the ID of a logged message. Source names the client that
41 // sent a user message, such as terminal or telegram, so that each client can
42 // show the messages of the others.
43 type Event struct {
44 Kind string `json:"kind"`
45 ID int `json:"id,omitempty"`
46 Text string `json:"text,omitempty"`
47 Source string `json:"source,omitempty"`
48 }
49
50 // TurnLoop runs the turns of one agent. A user message goes to the log when it
51 // comes, and it wakes the loop. A turn waits until every message before it is
52 // summarized. Then the turn runs a Chain whose first request holds the prompt,
53 // the tools, and the input of turnInput.
54 //
55 // The view goes out as the messages of a ViewLayout, so the first request of a
56 // turn reads the view of the turn before from the prompt cache. User messages
57 // that come during a turn join the next request of its chain after the tool
58 // results.
59 type TurnLoop struct {
60 agent *Agent
61 memory *Memory
62 client ChainClient
63 tools *Toolbox
64 layout *ViewLayout
65 clock func() time.Time
66 notify func(Event)
67 wake chan struct{}
68
69 mutex sync.Mutex
70 pending []Message
71 cancel context.CancelFunc
72 }
73
74 // Submit logs a user message from the client source and wakes the loop. The
75 // message joins the next request: the first one of a new turn, or the next one
76 // of a running turn.
77 func (l *TurnLoop) Submit(source, text string) (Message, error) {
78 message, err := l.record("user", source, text)
79 if err != nil {
80 return Message{}, err
81 }
82
83 l.mutex.Lock()
84 l.pending = append(l.pending, message)
85 l.mutex.Unlock()
86
87 select {
88 case l.wake <- struct{}{}:
89 default:
90 }
91
92 return message, nil
93 }
94
95 // Interrupt ends the running turn. A running tool is killed, and the turn sends
96 // no further request.
97 func (l *TurnLoop) Interrupt() bool {
98 l.mutex.Lock()
99 defer l.mutex.Unlock()
100
101 if l.cancel == nil {
102 return false
103 }
104
105 l.cancel()
106
107 return true
108 }
109
110 // Busy tells if a turn runs.
111 func (l *TurnLoop) Busy() bool {
112 l.mutex.Lock()
113 defer l.mutex.Unlock()
114
115 return l.cancel != nil
116 }
117
118 // Append logs a message and tells the clients. The toolbox logs its calls and
119 // results through it.
120 func (l *TurnLoop) Append(kind, text string) (Message, error) {
121 return l.record(kind, "", text)
122 }
123
124 // Flush makes the log durable.
125 func (l *TurnLoop) Flush(ctx context.Context) error {
126 return l.memory.Flush(ctx)
127 }
128
129 // Run runs a turn each time user messages wait, until ctx ends. It returns the
130 // error of a failed log write, which stops the agent.
131 func (l *TurnLoop) Run(ctx context.Context) error {
132 for {
133 for l.waiting() {
134 err := l.turn(ctx)
135 if err != nil {
136 return err
137 }
138 }
139
140 select {
141 case <-l.wake:
142 case <-ctx.Done():
143 return ctx.Err()
144 }
145 }
146 }
147
148 // record logs a message and tells the clients, with the client source of a user
149 // message.
150 func (l *TurnLoop) record(kind, source, text string) (Message, error) {
151 message, err := l.memory.Append(kind, text)
152 if err != nil {
153 return Message{}, err
154 }
155
156 l.notify(Event{
157 Kind: kind,
158 ID: message.ID,
159 Text: text,
160 Source: source,
161 })
162
163 return message, nil
164 }
165
166 // waiting tells if user messages wait for a request.
167 func (l *TurnLoop) waiting() bool {
168 l.mutex.Lock()
169 defer l.mutex.Unlock()
170
171 return len(l.pending) > 0
172 }
173
174 // turn runs one turn. A failed or interrupted model call ends the turn and
175 // tells the clients. The messages of the turn stay in the log and the view, and
176 // the next turn starts only with a new message, so a lasting failure such as a
177 // needed login does not repeat. Only a failed log write is an error of turn.
178 func (l *TurnLoop) turn(ctx context.Context) error {
179 ctx, cancel := context.WithCancel(ctx)
180 defer cancel()
181
182 l.mutex.Lock()
183 l.cancel = cancel
184 l.mutex.Unlock()
185
186 defer func() {
187 l.mutex.Lock()
188 l.cancel = nil
189 l.mutex.Unlock()
190 }()
191
192 l.notify(Event{
193 Kind: "busy",
194 })
195
196 err := l.chain(ctx)
197 if err != nil && !errors.Is(err, errChainLogWrite) {
198 l.notify(Event{
199 Kind: "error",
200 Text: turnError(ctx, err),
201 })
202
203 err = nil
204 }
205
206 l.notify(Event{
207 Kind: "idle",
208 })
209
210 return err
211 }
212
213 // chain runs the request chain of a turn until the final answer.
214 func (l *TurnLoop) chain(ctx context.Context) error {
215 messages := l.take()
216 if len(messages) == 0 {
217 return nil
218 }
219
220 err := l.memory.Wait(ctx)
221 if err != nil {
222 return err
223 }
224
225 instructions, err := l.agent.Prompt()
226 if err != nil {
227 return err
228 }
229
230 skip := make(map[int]bool, len(messages))
231 for _, message := range messages {
232 skip[message.ID] = true
233 }
234
235 view, err := l.layout.Split(l.memory.ViewLines(skip))
236 if err != nil {
237 return err
238 }
239
240 chain := &Chain{
241 client: l.client,
242 tools: l.tools,
243 request: ModelRequest{
244 Model: l.agent.config.Model,
245 Effort: l.agent.config.Effort,
246 Instructions: instructions,
247 Tools: promptTools(),
248 },
249 take: l.take,
250 }
251
252 _, err = chain.Run(ctx, turnInput(view, l.state(), messages))
253
254 return err
255 }
256
257 // take returns the user messages that wait for a request, and removes them.
258 func (l *TurnLoop) take() []Message {
259 l.mutex.Lock()
260 defer l.mutex.Unlock()
261
262 messages := l.pending
263 l.pending = nil
264
265 return messages
266 }
267
268 // state returns the per-turn state: the end of the view, and the date and time
269 // of the turn.
270 func (l *TurnLoop) state() string {
271 return "</chat>\n\nNow: " + l.clock().Format("Monday 2006-01-02 15:04 -07:00")
272 }
273
274 // turnError returns the text of the error that ended a turn.
275 func turnError(ctx context.Context, err error) string {
276 if ctx.Err() != nil {
277 return "interrupted"
278 }
279
280 return err.Error()
281 }
282
283 // turnInput returns the input of the first request of a turn, stable parts
284 // first: a user message for each view message, one with the per-turn state,
285 // and one for each new message, whole. The view leaves out the leaf lines of
286 // the new messages.
287 func turnInput(view []string, state string, messages []Message) []any {
288 input := make([]any, 0, len(view)+1+len(messages))
289 for _, text := range view {
290 input = append(input, modelUserMessage(text))
291 }
292
293 input = append(input, modelUserMessage(state))
294 for _, message := range messages {
295 input = append(input, modelUserMessage(message.Text))
296 }
297
298 return input
299 }