diff --git a/README.md b/README.md index d8ed6f8..a4112ca 100644 --- a/README.md +++ b/README.md @@ -202,106 +202,7 @@ A session can spawn ephemeral child sessions to handle a focused task, then tear **Sub-agents as function calls** — the parent primes the child with exactly the context it needs (excerpts from its own `chat`, relevant beads, local file contents) and receives a single focused reply. Failures are isolated; a child that errors or stalls can be killed and retried without affecting the parent. The pattern composes naturally: a sub-agent can itself spawn sub-agents, building a call tree bounded only by the number of sessions open at once. -The coordination logic can be written in any language that reads and writes files. The following Go example fans out N sub-agents concurrently, waits for all replies, and collects the results: - -```go -package main - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "sync" - "time" -) - -var base = filepath.Join(os.Getenv("HOME"), "mnt", "ollie") - -func main() { - tasks := []string{ - "summarize the key ideas in functional programming", - "summarize the key ideas in object-oriented programming", - "summarize the key ideas in logic programming", - } - for i, reply := range runSubAgents(tasks) { - fmt.Printf("=== agent %d ===\n%s\n", i+1, reply) - } -} - -func runSubAgents(tasks []string) []string { - type result struct { - idx int - reply string - } - results := make([]string, len(tasks)) - ch := make(chan result, len(tasks)) - var wg sync.WaitGroup - - for i, task := range tasks { - wg.Add(1) - go func(idx int, task string) { - defer wg.Done() - sid := spawnSession() - defer killSession(sid) - os.WriteFile(filepath.Join(base, sid, "prompt"), []byte(task), 0644) - ch <- result{idx, waitReply(filepath.Join(base, sid, "reply"))} - }(i, task) - } - go func() { wg.Wait(); close(ch) }() - for r := range ch { - results[r.idx] = r.reply - } - return results -} - -// spawnSession is serialised to avoid a race between snapshot and detection. -var spawnMu sync.Mutex - -func spawnSession() string { - spawnMu.Lock() - defer spawnMu.Unlock() - before := sessionIDs() - os.WriteFile(filepath.Join(base, "ctl"), []byte("new\n"), 0644) - for { - for id := range sessionIDs() { - if !before[id] { - return id - } - } - time.Sleep(100 * time.Millisecond) - } -} - -func sessionIDs() map[string]bool { - entries, _ := os.ReadDir(base) - ids := make(map[string]bool) - for _, e := range entries { - if e.IsDir() { - ids[e.Name()] = true - } - } - return ids -} - -func waitReply(path string) string { - for { - if info, err := os.Stat(path); err == nil && info.Size() > 0 { - data, _ := os.ReadFile(path) - return strings.TrimSpace(string(data)) - } - time.Sleep(500 * time.Millisecond) - } -} - -func killSession(sid string) { - os.WriteFile(filepath.Join(base, "ctl"), []byte("kill "+sid+"\n"), 0644) -} -``` - -Each goroutine spawns, primes, waits, and cleans up independently. The only serialised step is session creation, to avoid a race between snapshotting existing sessions and detecting the new one. - -The same pattern works as a reusable agent tool. `spawn_subagents.py` reads a JSON spec from stdin, fans out the work concurrently, and returns a JSON array of results — a single `execute_tool` call from the parent's perspective. Note: this requires adding Python language support to `execute_code`, `execute_tool`, and `execute_pipe`, which is low effort. +`spawn_subagents.py` reads a JSON spec from stdin, fans out the work concurrently, and returns a JSON array of results — a single `execute_tool` call from the parent's perspective. Note: this requires adding Python language support to `execute_code`, `execute_tool`, and `execute_pipe`, which is low effort. ```python #!/usr/bin/env python3