X-Git-Url: http://git.giacomolli.uk/onechat/blobdiff_plain/2ad578a24f252706976cddb41bee6cc969f251cd..723f6e18adc6482ed913e1618c8a76e981720c2b:/daemon_message.go?ds=inline diff --git a/daemon_message.go b/daemon_message.go index db15db8..40a99a9 100644 --- a/daemon_message.go +++ b/daemon_message.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "errors" "fmt" @@ -15,15 +16,37 @@ import ( // 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) } } @@ -76,8 +99,10 @@ type DaemonMessages struct { 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 @@ -125,6 +150,83 @@ func (m *DaemonMessages) save() error { 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 {