]> git.giacomolli.uk - onechat/blobdiff - daemon_message.go
Give the root tools to start and message children
[onechat] / daemon_message.go
index db15db8073c4624a5c9bf57b3fdadaac46601f76..8415e49d4b7110d596eaed1013e79e6f29ce09b7 100644 (file)
@@ -1,6 +1,7 @@
 package main
 
 import (
 package main
 
 import (
+       "context"
        "encoding/json"
        "errors"
        "fmt"
        "encoding/json"
        "errors"
        "fmt"
@@ -16,17 +17,26 @@ import (
 // finishes the turn that takes it, so a stop can interrupt delivery or a turn.
 func openDaemonMessages(loops []*TurnLoop) (*DaemonMessages, error) {
        m := &DaemonMessages{
 // finishes the turn that takes it, so a stop can interrupt delivery or a turn.
 func openDaemonMessages(loops []*TurnLoop) (*DaemonMessages, error) {
        m := &DaemonMessages{
-               path:  filepath.Join(loops[0].agent.dir, "messages.json"),
-               loops: make(map[string]*TurnLoop),
+               path:    filepath.Join(loops[0].agent.dir, "messages.json"),
+               ordered: append([]*TurnLoop(nil), loops...),
+               loops:   make(map[string]*TurnLoop),
        }
        for _, loop := range loops {
                m.loops[loop.agent.config.Name] = loop
                loop.messages = m
        }
        for _, loop := range loops {
                m.loops[loop.agent.config.Name] = loop
                loop.messages = m
+               loop.tools.messages = m
                if loop.agent.config.Parent == "" {
                        loop.logger = loop.logger.With("name", loop.agent.config.Name)
                }
        }
 
                if loop.agent.config.Parent == "" {
                        loop.logger = loop.logger.With("name", loop.agent.config.Name)
                }
        }
 
+       for _, child := range loops[1:] {
+               err := loops[0].runs.subagents.Reserve(child.agent.config.Name)
+               if err != nil {
+                       return nil, fmt.Errorf("reserve child name: %w", err)
+               }
+       }
+
        data, err := os.ReadFile(m.path)
        if err != nil && !errors.Is(err, os.ErrNotExist) {
                return nil, fmt.Errorf("read agent messages: %w", err)
        data, err := os.ReadFile(m.path)
        if err != nil && !errors.Is(err, os.ErrNotExist) {
                return nil, fmt.Errorf("read agent messages: %w", err)
@@ -76,6 +86,7 @@ type DaemonMessages struct {
        mutex   sync.Mutex
        path    string
        loops   map[string]*TurnLoop
        mutex   sync.Mutex
        path    string
        loops   map[string]*TurnLoop
+       ordered []*TurnLoop
        state   DaemonMessageFile
        failure error
 }
        state   DaemonMessageFile
        failure error
 }
@@ -125,6 +136,83 @@ func (m *DaemonMessages) save() error {
        return nil
 }
 
        return nil
 }
 
+// AddChild holds the queue lock while open makes a child's files and loop. The
+// loop joins backups before its first task can be delivered.
+func (m *DaemonMessages) AddChild(parent, name string, open func() (*TurnLoop, error)) (*TurnLoop, error) {
+       m.mutex.Lock()
+       defer m.mutex.Unlock()
+
+       if m.loops[name] != nil {
+               return nil, fmt.Errorf("name %q is already used", name)
+       }
+
+       root := m.loops[parent]
+       if root == nil || root.agent.config.Parent != "" {
+               return nil, errors.New("only the root can create children")
+       }
+
+       if m.failure != nil {
+               return nil, fmt.Errorf("agent message queue stopped: %w", m.failure)
+       }
+
+       loop, err := open()
+       if err != nil {
+               return nil, fmt.Errorf("open child: %w", err)
+       }
+
+       loop.messages = m
+       loop.tools.messages = m
+       m.loops[name] = loop
+       m.ordered = append(m.ordered, loop)
+
+       return loop, nil
+}
+
+// Child returns only a direct child of parent. Loops live until the daemon
+// stops, so the returned loop stays open for the caller's turn.
+func (m *DaemonMessages) Child(parent, name string) *TurnLoop {
+       m.mutex.Lock()
+       defer m.mutex.Unlock()
+
+       loop := m.loops[name]
+       if loop == nil || loop.agent.config.Parent != parent {
+               return nil
+       }
+
+       return loop
+}
+
+// ChildState lists the names and responsibilities of a parent's children in
+// name order. It returns no heading when the parent has no children.
+func (m *DaemonMessages) ChildState(parent string) string {
+       m.mutex.Lock()
+       defer m.mutex.Unlock()
+
+       var lines []string
+
+       for name, loop := range m.loops {
+               if loop.agent.config.Parent == parent {
+                       lines = append(lines, name+": "+loop.agent.config.Responsibility)
+               }
+       }
+
+       if len(lines) == 0 {
+               return ""
+       }
+
+       slices.Sort(lines)
+
+       return "\n\nChildren:\n" + strings.Join(lines, "\n")
+}
+
+// SnapshotAgents locks the queue and all registered loops, including children
+// made during this daemon run, before snapshot copies their files.
+func (m *DaemonMessages) SnapshotAgents(ctx context.Context, snapshot func() error) error {
+       return m.Snapshot(func() error {
+               return daemonSnapshot(ctx, m.ordered, snapshot)
+       })
+}
+
 // Tell queues a user message from parent to its own child, then wakes the
 // child. It is the daemon entry point for parent-to-child messages.
 func (m *DaemonMessages) Tell(parent, child, text string) error {
 // Tell queues a user message from parent to its own child, then wakes the
 // child. It is the daemon entry point for parent-to-child messages.
 func (m *DaemonMessages) Tell(parent, child, text string) error {