package main
import (
+ "context"
"encoding/json"
"errors"
"fmt"
// messages before the loops start. A queue entry stays until its recipient
// finishes the turn that takes it, so a stop can interrupt delivery or a turn.
func openDaemonMessages(loops []*TurnLoop) (*DaemonMessages, error) {
+ routes, err := openRoutes(filepath.Join(loops[0].agent.dir, "routes.json"), loops[0].logger)
+ if err != nil {
+ return nil, fmt.Errorf("open agent routes: %w", err)
+ }
+
m := &DaemonMessages{
- path: filepath.Join(loops[0].agent.dir, "messages.json"),
- loops: make(map[string]*TurnLoop),
+ routes: routes,
+ 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
+ loop.tools.messages = m
if loop.agent.config.Parent == "" {
loop.logger = loop.logger.With("name", loop.agent.config.Name)
+ loop.tools.routes = routes
+ if loop.tools.whatsapp != nil {
+ loop.tools.whatsapp.mutex.Lock()
+
+ loop.tools.whatsapp.routes = routes
+ loop.tools.whatsapp.mutex.Unlock()
+ }
+ }
+ }
+
+ 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)
}
}
mutex sync.Mutex
path string
loops map[string]*TurnLoop
+ ordered []*TurnLoop
state DaemonMessageFile
failure error
+ routes *Routes
}
// deliver records one queued message and wakes its recipient. The caller holds
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 {