]> git.giacomolli.uk - onechat/blob - daemon_children.go
Give the root tools to start and message children
[onechat] / daemon_children.go
1 package main
2
3 import (
4         "context"
5         "errors"
6         "fmt"
7         "sync"
8 )
9
10 // DaemonChildren starts children made by the root during a daemon run. The root
11 // calls Create in its turn goroutine. Close runs only after that loop stops.
12 // The daemon context, not the calling turn, owns each child's lifetime.
13 type DaemonChildren struct {
14         ctx      context.Context
15         cancel   context.CancelFunc
16         root     *TurnLoop
17         messages *DaemonMessages
18         opened   DaemonAgents
19         workers  sync.WaitGroup
20         mutex    sync.Mutex
21         failure  error
22 }
23
24 // Create makes and opens a child, registers its queue, and starts its loop.
25 // Registration holds the queue lock through file creation, so a backup cannot
26 // copy a child before its open files are part of the snapshot.
27 func (c *DaemonChildren) Create(name, responsibility string) (string, error) {
28         if c.ctx.Err() != nil {
29                 return "", c.ctx.Err()
30         }
31
32         if c.root.openChild == nil {
33                 return "", errors.New("child creation is not available")
34         }
35
36         var opened *DaemonAgent
37
38         loop, err := c.messages.AddChild(c.root.agent.config.Name, name, func() (*TurnLoop, error) {
39                 agent, err := c.root.agent.CreateChild(name, responsibility, c.root.runs.subagents)
40                 if err != nil {
41                         return nil, fmt.Errorf("create child config: %w", err)
42                 }
43
44                 opened, err = c.root.openChild(c.ctx, agent)
45                 if err != nil {
46                         return nil, fmt.Errorf("open child: %w", err)
47                 }
48
49                 loop, err := opened.Loop(nil, func(Event) {
50                 })
51                 if err != nil {
52                         return nil, errors.Join(fmt.Errorf("open child loop: %w", err), opened.Close())
53                 }
54
55                 return loop, nil
56         })
57         if err != nil {
58                 return "", fmt.Errorf("start child %q: %w", name, err)
59         }
60
61         c.opened = append(c.opened, opened)
62         c.workers.Add(1)
63
64         go func() {
65                 defer c.workers.Done()
66
67                 err := daemonRunChild(c.ctx, loop, opened.schedules)
68                 if err != nil {
69                         opened.logger.Error("child stopped", "err", err)
70                         c.mutex.Lock()
71
72                         c.failure = errors.Join(c.failure, err)
73                         c.mutex.Unlock()
74                 }
75
76                 c.cancel()
77         }()
78
79         c.root.logger.Info("child started", "name", name)
80
81         return "Started " + name + ". Use tell to send it a task.", nil
82 }
83
84 // Close waits for the runtime children, closes their files, and returns their
85 // failures. The caller stops the daemon context before this call.
86 func (c *DaemonChildren) Close() error {
87         c.workers.Wait()
88
89         return errors.Join(c.failure, c.opened.Close())
90 }