diff --git a/IDEAS.md b/IDEAS.md index 2e09728..3035209 100644 --- a/IDEAS.md +++ b/IDEAS.md @@ -126,7 +126,7 @@ Use existing `APPROVAL_REQUEST` and `APPROVAL_RESPONSE` message types: cat > /tmp/approval.json < /tmp/response.json < /tmp/msg.json <<'EOF' {"to":"$PEER_ID","type":"REVIEW_REQUEST","subject":"Review","body":"Please review staged changes"} EOF -9p write agent/$ID/outbox/msg-$(date +%s).json < /tmp/msg.json +9p write agent/$ID/mail < /tmp/msg.json ``` See `workflows/DevReview` and `workflows/Planning` for complete examples. diff --git a/claude/agent/anvillm-agent.md b/claude/agent/anvillm-agent.md index a6bfb40..c254313 100644 --- a/claude/agent/anvillm-agent.md +++ b/claude/agent/anvillm-agent.md @@ -64,5 +64,5 @@ To send a message to another agent or user: cat > /tmp/msg.json <<'ENDMSG' {"to":"recipient-id","type":"MESSAGE_TYPE","subject":"Subject","body":"Message body"} ENDMSG -9p write agent/YOUR_ID/outbox/msg-$(date +%s).json < /tmp/msg.json +9p write agent/YOUR_ID/mail < /tmp/msg.json ``` diff --git a/cmd/Assist/main.go b/cmd/Assist/main.go index 550cc25..2aeb36e 100644 --- a/cmd/Assist/main.go +++ b/cmd/Assist/main.go @@ -359,7 +359,7 @@ func sendPrompt(id, prompt string) error { if !isConnected() { return fmt.Errorf("not connected to anvilsrv") } - + // Create message JSON msg := map[string]interface{}{ "to": id, @@ -371,23 +371,20 @@ func sendPrompt(id, prompt string) error { if err != nil { return fmt.Errorf("failed to marshal message: %w", err) } - - // Write to user outbox - timestamp := time.Now().Unix() - filename := fmt.Sprintf("msg-%d.json", timestamp) - path := filepath.Join("user/outbox", filename) - - fid, err := fs.Create(path, plan9.OWRITE, 0644) + + // Write to user mail + path := "user/mail" + + fid, err := fs.Open(path, plan9.OWRITE) if err != nil { - return fmt.Errorf("failed to create message file: %w", err) + return fmt.Errorf("failed to open mail file: %w", err) } defer fid.Close() - _, err = fid.Write(msgJSON) if err != nil { return fmt.Errorf("failed to write message: %w", err) } - + return nil } @@ -1086,9 +1083,6 @@ func listInboxMessages() ([]Message, []string, error) { if err := json.Unmarshal(data, &msg); err != nil { continue } - if msg.ID == "" { - msg.ID = filename - } messages = append(messages, msg) } @@ -1097,7 +1091,10 @@ func listInboxMessages() ([]Message, []string, error) { func extractTimestamp(filename string) int64 { var ts int64 - fmt.Sscanf(filename, "msg-%d.json", &ts) + parts := strings.Split(strings.TrimSuffix(filename, ".json"), "-") + if len(parts) == 2 { + fmt.Sscanf(parts[1], "%d", &ts) + } return ts } @@ -1124,7 +1121,7 @@ func openMessageWindow(idx int) error { filename := filenames[idx-1] msg := messages[idx-1] - + w, err := acme.New() if err != nil { return err @@ -1159,6 +1156,7 @@ func handleMessageWindow(w *acme.Win, msg *Message, filename string) { if err := archiveMessage(msg.ID); err != nil { w.Fprintf("errors", "Archive failed: %v\n", err) } else { + refreshInboxWindowByName() w.Ctl("delete") } } else { @@ -1175,7 +1173,7 @@ func openReplyWindow(originalMsg *Message) error { replyType := getReplyType(originalMsg.Type) replySubject := fmt.Sprintf("Re: %s", originalMsg.Subject) - + w.Name(fmt.Sprintf("/AnviLLM/reply/%s", originalMsg.From)) w.Write("tag", []byte("Send ")) w.Ctl("clean") @@ -1184,11 +1182,29 @@ func openReplyWindow(originalMsg *Message) error { return nil } +func refreshInboxWindowByName() { + wins, err := acme.Windows() + if err != nil { + return + } + for _, info := range wins { + if info.Name == "/AnviLLM/inbox" { + w, err := acme.Open(info.ID, nil) + if err != nil { + continue + } + refreshInboxWindow(w) + w.CloseFiles() + return + } + } +} + func archiveMessage(msgID string) error { if !isConnected() { return fmt.Errorf("not connected to anvilsrv") } - + fid, err := fs.Open("user/ctl", plan9.OWRITE) if err != nil { return err @@ -1241,7 +1257,7 @@ func sendReply(to, msgType, subject, body string) error { if !isConnected() { return fmt.Errorf("not connected to anvilsrv") } - + msg := map[string]interface{}{ "to": to, "type": msgType, @@ -1252,20 +1268,18 @@ func sendReply(to, msgType, subject, body string) error { if err != nil { return fmt.Errorf("failed to marshal message: %w", err) } - - timestamp := time.Now().Unix() - filename := fmt.Sprintf("msg-%d.json", timestamp) - path := filepath.Join("user/outbox", filename) - - fid, err := fs.Create(path, plan9.OWRITE, 0644) + + path := "user/mail" + + fid, err := fs.Open(path, plan9.OWRITE) if err != nil { - return fmt.Errorf("failed to create message: %w", err) + return fmt.Errorf("failed to open mail file: %w", err) } defer fid.Close() - + if _, err := fid.Write(msgJSON); err != nil { return fmt.Errorf("failed to write message: %w", err) } - + return nil } diff --git a/cmd/anvillm/main.go b/cmd/anvillm/main.go index f8f1962..9da6e93 100644 --- a/cmd/anvillm/main.go +++ b/cmd/anvillm/main.go @@ -357,38 +357,24 @@ func sendPrompt(id, prompt string) error { if err != nil { return fmt.Errorf("failed to marshal message: %w", err) } - - // Write to user outbox - timestamp := time.Now().Unix() - filename := fmt.Sprintf("msg-%d.json", timestamp) - path := filepath.Join("user/outbox", filename) - - fid, err := fs.Create(path, plan9.OWRITE, 0644) + + // Write to user mail + path := "user/mail" + + fid, err := fs.Open(path, plan9.OWRITE) if err != nil { - return fmt.Errorf("failed to create message file: %w", err) + return fmt.Errorf("failed to open mail file: %w", err) } defer fid.Close() - + _, err = fid.Write(msgJSON) if err != nil { return fmt.Errorf("failed to write message: %w", err) } - + return nil } -func sendPromptDirect(id, prompt string) error { - path := filepath.Join(id, "in") - fid, err := fs.Open(path, plan9.OWRITE) - if err != nil { - return err - } - defer fid.Close() - - _, err = fid.Write([]byte(prompt)) - return err -} - func updateStatus(msg string) { statusBar.Clear() fmt.Fprintf(statusBar, " %s", msg) diff --git a/cmd/anvilweb/main.go b/cmd/anvilweb/main.go index ac0f98e..8cef9fe 100644 --- a/cmd/anvilweb/main.go +++ b/cmd/anvilweb/main.go @@ -13,7 +13,6 @@ import ( "os/exec" "path/filepath" "strings" - "time" "9fans.net/go/plan9" "9fans.net/go/plan9/client" @@ -359,14 +358,12 @@ func sendPrompt(w http.ResponseWriter, r *http.Request, id string) { return } - // Write to user outbox - timestamp := time.Now().Unix() - filename := fmt.Sprintf("msg-%d.json", timestamp) - path := filepath.Join("user/outbox", filename) + // Write to user mail + path := "user/mail" - fid, err := fs.Create(path, plan9.OWRITE, 0644) + fid, err := fs.Open(path, plan9.OWRITE) if err != nil { - http.Error(w, fmt.Sprintf("failed to create message file: %v", err), http.StatusInternalServerError) + http.Error(w, fmt.Sprintf("failed to open mail file: %v", err), http.StatusInternalServerError) return } defer fid.Close() @@ -631,7 +628,10 @@ func handleInbox(w http.ResponseWriter, r *http.Request) { } var ts int64 - fmt.Sscanf(d.Name, "msg-%d.json", &ts) + parts := strings.Split(strings.TrimSuffix(d.Name, ".json"), "-") + if len(parts) == 2 { + fmt.Sscanf(parts[1], "%d", &ts) + } messages = append(messages, InboxMessage{ Message: msg, @@ -719,11 +719,9 @@ func handleInboxReply(w http.ResponseWriter, r *http.Request) { return } - timestamp := time.Now().Unix() - filename := fmt.Sprintf("msg-%d.json", timestamp) - path := filepath.Join("user/outbox", filename) + path := "user/mail" - fid, err := fs.Create(path, plan9.OWRITE, 0644) + fid, err := fs.Open(path, plan9.OWRITE) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/elisp/anvillm.el b/elisp/anvillm.el index 22d3f36..8f2519c 100644 --- a/elisp/anvillm.el +++ b/elisp/anvillm.el @@ -552,8 +552,11 @@ Backends: (let ((prompt (read-string "Prompt: "))) (when (> (length prompt) 0) (condition-case err - (progn - (anvillm--9p-write (concat anvillm-agent-path "/" session-id "/in") prompt) + (let ((msg (json-encode `((to . ,session-id) + (type . "PROMPT") + (subject . "User prompt") + (body . ,prompt))))) + (anvillm--9p-write (concat anvillm-agent-path "/user/mail") msg) (message "Sent prompt to %s" (substring session-id 0 (min 8 (length session-id))))) (error (message "Failed to send prompt: %s" (error-message-string err)))))) @@ -586,10 +589,11 @@ Backends: (if (string-empty-p (string-trim prompt)) (message "Empty prompt, not sending") (condition-case err - (progn - (anvillm--9p-write - (concat anvillm-agent-path "/" anvillm--prompt-session-id "/in") - prompt) + (let ((msg (json-encode `((to . ,anvillm--prompt-session-id) + (type . "PROMPT") + (subject . "User prompt") + (body . ,prompt))))) + (anvillm--9p-write (concat anvillm-agent-path "/user/mail") msg) (message "Sent prompt to %s" (substring anvillm--prompt-session-id 0 (min 8 (length anvillm--prompt-session-id)))) diff --git a/go.mod b/go.mod index 7ad5e75..819383f 100644 --- a/go.mod +++ b/go.mod @@ -6,14 +6,15 @@ toolchain go1.24.3 require ( 9fans.net/go v0.0.7 + github.com/gdamore/tcell/v2 v2.13.8 + github.com/google/uuid v1.6.0 + github.com/rivo/tview v0.42.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/gdamore/encoding v1.0.1 // indirect - github.com/gdamore/tcell/v2 v2.13.8 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect - github.com/rivo/tview v0.42.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/term v0.37.0 // indirect diff --git a/go.sum b/go.sum index 120f700..1f34be8 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeekl github.com/gdamore/tcell/v2 v2.13.8 h1:Mys/Kl5wfC/GcC5Cx4C2BIQH9dbnhnkPgS9/wF3RlfU= github.com/gdamore/tcell/v2 v2.13.8/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c= diff --git a/internal/backend/tmux/session.go b/internal/backend/tmux/session.go index 19d4fd9..b51719c 100644 --- a/internal/backend/tmux/session.go +++ b/internal/backend/tmux/session.go @@ -511,7 +511,7 @@ func (s *Session) Send(ctx context.Context, prompt string) (string, error) { originalPrompt := prompt // Instruction to send response to user via mailbox (triggers idle transition) - outInstruction := fmt.Sprintf("IMPORTANT: When done, write your response summary to user by running:\ncat > /tmp/msg.json <<'MSGEOF'\n{\"to\":\"user\",\"type\":\"STATUS_UPDATE\",\"subject\":\"Response\",\"body\":\"YOUR_SUMMARY_HERE\"}\nMSGEOF\n9p write agent/%s/outbox/msg-$(date +%%s).json < /tmp/msg.json\n\nThe summary MUST include your actual response and key actions taken.\n", s.id) + outInstruction := fmt.Sprintf("IMPORTANT: When done, write your response summary to user by running:\ncat > /tmp/msg.json <<'MSGEOF'\n{\"to\":\"user\",\"type\":\"LOG_INFO\",\"subject\":\"Response\",\"body\":\"YOUR_SUMMARY_HERE\"}\nMSGEOF\n9p write agent/%s/mail < /tmp/msg.json\n\nThe summary MUST include your actual response and key actions taken.\n", s.id) // Prepend context if set (skip for slash commands) if s.context != "" && !strings.HasPrefix(prompt, "/") { diff --git a/internal/mailbox/manager.go b/internal/mailbox/manager.go index 7947afe..dbba57d 100644 --- a/internal/mailbox/manager.go +++ b/internal/mailbox/manager.go @@ -13,6 +13,7 @@ type Manager struct { outboxes map[string][]*Message completed map[string][]*Message mu sync.RWMutex + idCounter uint64 } // NewManager creates a new mailbox manager diff --git a/internal/p9/server.go b/internal/p9/server.go index 4552bef..9696f8f 100644 --- a/internal/p9/server.go +++ b/internal/p9/server.go @@ -19,6 +19,8 @@ import ( "strings" "sync" + "github.com/google/uuid" + "9fans.net/go/plan9" "9fans.net/go/plan9/client" ) @@ -69,6 +71,7 @@ const ( qidUserOutbox // user/outbox qidUserCompleted // user/completed qidUserCtl // user/ctl + qidUserMail // user/mail qidSessionBase = 1000 qidPeersBase = 0x10000000 // peers/{id}/file qidInboxBase = 0x20000000 // session/{id}/inbox @@ -92,10 +95,11 @@ const ( fileRole fileTasks fileTmux + fileMail fileCount ) -var fileNames = []string{"ctl", "in", "out", "log", "state", "pid", "cwd", "alias", "backend", "context", "role", "tasks", "tmux"} +var fileNames = []string{"ctl", "in", "out", "log", "state", "pid", "cwd", "alias", "backend", "context", "role", "tasks", "tmux", "mail"} // Directory names in session var dirNames = []string{"inbox", "outbox", "completed"} @@ -285,6 +289,9 @@ func (s *Server) walk(cs *connState, fc *plan9.Fcall) *plan9.Fcall { case "ctl": qid = plan9.Qid{Type: QTFile, Path: qidUserCtl} newPath = "/user/ctl" + case "mail": + qid = plan9.Qid{Type: QTFile, Path: qidUserMail} + newPath = "/user/mail" default: return errFcall(fc, "not found") } @@ -356,46 +363,7 @@ func (s *Server) open(cs *connState, fc *plan9.Fcall) *plan9.Fcall { } func (s *Server) create(cs *connState, fc *plan9.Fcall) *plan9.Fcall { - cs.mu.Lock() - defer cs.mu.Unlock() - - f, ok := cs.fids[fc.Fid] - if !ok { - return errFcall(fc, "bad fid") - } - - // Allow creating files in outbox directories (session or user) - parts := strings.Split(strings.TrimPrefix(f.path, "/"), "/") - isUserOutbox := f.path == "/user/outbox" - isSessionOutbox := len(parts) == 2 && parts[1] == "outbox" - - if !isUserOutbox && !isSessionOutbox { - return errFcall(fc, "can only create files in outbox") - } - - fileName := fc.Name - - // Must be a .json file - if !strings.HasSuffix(fileName, ".json") { - return errFcall(fc, "message files must end with .json") - } - - // Create new message file - newPath := f.path + "/" + fileName - var hashKey string - if isUserOutbox { - hashKey = "user" + "outbox" + fileName - } else { - hashKey = parts[0] + "outbox" + fileName - } - qid := plan9.Qid{Type: QTFile, Path: qidMessageBase + hashID(hashKey)} - - f.qid = qid - f.path = newPath - f.mode = fc.Mode - f.offset = 0 - - return &plan9.Fcall{Type: plan9.Rcreate, Tag: fc.Tag, Qid: qid} + return errFcall(fc, "create not supported") } func (s *Server) read(cs *connState, fc *plan9.Fcall) *plan9.Fcall { @@ -671,8 +639,8 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall { return &plan9.Fcall{Type: plan9.Rwrite, Tag: fc.Tag, Count: uint32(len(fc.Data))} } - // /{id}/outbox/msg-*.json - write message to outbox - if len(parts) == 3 && parts[1] == "outbox" && strings.HasSuffix(parts[2], ".json") { + // /{id}/mail - write message to outbox (generates UUID filename) + if len(parts) == 2 && parts[1] == "mail" { sessID := parts[0] // Track this connection's session ID @@ -693,6 +661,9 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall { return errFcall(fc, err.Error()) } + // Generate UUID for message + msg.ID = uuid.New().String() + // Add to outbox mailMgr := s.mgr.GetMailManager() if mailMgr == nil { @@ -704,7 +675,6 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall { } // Transition sender to idle after sending any outbox message - // This allows the sender to receive responses and prevents getting stuck in "running" if sess := s.mgr.Get(sessID); sess != nil { if tmuxSess, ok := sess.(*tmux.Session); ok { tmuxSess.TransitionTo("idle") @@ -714,8 +684,8 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall { return &plan9.Fcall{Type: plan9.Rwrite, Tag: fc.Tag, Count: uint32(len(fc.Data))} } - // /user/outbox/msg-*.json - write message from user to bot - if len(parts) == 3 && parts[0] == "user" && parts[1] == "outbox" && strings.HasSuffix(parts[2], ".json") { + // /user/mail - write message from user to bot (generates UUID filename) + if len(parts) == 2 && parts[0] == "user" && parts[1] == "mail" { // Parse JSON message msg, err := mailbox.FromJSON(fc.Data) if err != nil { @@ -730,6 +700,9 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall { // Set from field to "user" msg.From = "user" + // Generate UUID for message + msg.ID = uuid.New().String() + // Add to user's outbox mailMgr := s.mgr.GetMailManager() if mailMgr == nil { @@ -743,6 +716,43 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall { return &plan9.Fcall{Type: plan9.Rwrite, Tag: fc.Tag, Count: uint32(len(fc.Data))} } + // /agent/{id}/mail - write message from agent (generates UUID filename) + if len(parts) == 2 && parts[1] == "mail" { + sessID := parts[0] + if s.mgr.Get(sessID) == nil { + return errFcall(fc, "session not found") + } + + // Parse JSON message + msg, err := mailbox.FromJSON(fc.Data) + if err != nil { + return errFcall(fc, fmt.Sprintf("invalid message JSON: %v", err)) + } + + // Validate message type + if err := mailbox.ValidateMessageType(msg.Type); err != nil { + return errFcall(fc, err.Error()) + } + + // Set from field to session ID + msg.From = sessID + + // Generate UUID for message + msg.ID = uuid.New().String() + + // Add to agent's outbox + mailMgr := s.mgr.GetMailManager() + if mailMgr == nil { + return errFcall(fc, "mailbox not available") + } + + if err := mailMgr.AddToOutbox(sessID, msg); err != nil { + return errFcall(fc, fmt.Sprintf("failed to add message: %v", err)) + } + + return &plan9.Fcall{Type: plan9.Rwrite, Tag: fc.Tag, Count: uint32(len(fc.Data))} + } + return errFcall(fc, "read-only") } @@ -842,6 +852,10 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte { Qid: plan9.Qid{Type: QTFile, Path: qidUserCtl}, Mode: 0222, Name: "ctl", Uid: "q", Gid: "q", Muid: "q", }) + dirs = append(dirs, plan9.Dir{ + Qid: plan9.Qid{Type: QTFile, Path: qidUserMail}, + Mode: 0222, Name: "mail", Uid: "q", Gid: "q", Muid: "q", + }) } else if strings.HasPrefix(path, "/user/") && strings.Count(path, "/") == 2 { // User mailbox directory (inbox/outbox/completed) mailboxType := strings.TrimPrefix(path, "/user/") diff --git a/skills/anvillm-communication/SKILL.md b/skills/anvillm-communication/SKILL.md index 34217a0..70f2430 100644 --- a/skills/anvillm-communication/SKILL.md +++ b/skills/anvillm-communication/SKILL.md @@ -42,7 +42,7 @@ From the agent list output, the first field is the session ID you need for commu ## Sending Messages -All communication uses the mailbox system. Write JSON messages to your outbox. +All communication uses the mailbox system. Write JSON messages via the mail file. ### To Another Agent @@ -50,7 +50,7 @@ All communication uses the mailbox system. Write JSON messages to your outbox. cat > /tmp/msg.json <<'EOF' {"to":"{session_id}","type":"QUERY_REQUEST","subject":"Brief subject","body":"Your message here"} EOF -9p write agent/{your_id}/outbox/msg-$(date +%s).json < /tmp/msg.json +9p write agent/{your_id}/mail < /tmp/msg.json ``` ### To User @@ -61,7 +61,7 @@ The special recipient `"user"` sends messages to the human operator. cat > /tmp/msg.json <<'EOF' {"to":"user","type":"LOG_INFO","subject":"Task complete","body":"Summary of what was done"} EOF -9p write agent/{your_id}/outbox/msg-$(date +%s).json < /tmp/msg.json +9p write agent/{your_id}/mail < /tmp/msg.json ``` ### Message Types @@ -86,7 +86,7 @@ EOF 1. **Discover the peer**: `9p read agent/list | grep research` 2. **Extract ID**: Note the session ID (first field) -3. **Create message**: Write JSON to your outbox +3. **Create message**: Write JSON to the mail file 4. **Check inbox**: `9p ls agent/{your_id}/inbox/` for responses ## Mailbox System @@ -94,11 +94,12 @@ EOF ### Structure Each agent has: -- **outbox/** - Write messages here to send +- **mail** - Write messages here to send (creates UUID in outbox) - **inbox/** - Receive messages from others +- **outbox/** - Outgoing messages (read-only, populated by mail writes) - **completed/** - Archive of processed messages -The special `user` participant also has mailboxes at `agent/user/inbox`, `agent/user/outbox`, `agent/user/completed`. +The special `user` participant also has mailboxes at `agent/user/mail`, `agent/user/inbox`, `agent/user/outbox`, `agent/user/completed`. ### Message Format @@ -111,7 +112,10 @@ Messages are JSON files with: ### Message Flow 1. Write message JSON to your outbox -2. Mail processor (runs every 5s) picks it up +### Message Flow + +1. Write message JSON to the mail file +2. Mail processor (runs every 5s) picks it up from outbox 3. Delivers to recipient's inbox 4. For agents: when idle, message is formatted and sent to their `in` file 5. For user: message body is written to sender's chat log @@ -119,8 +123,8 @@ Messages are JSON files with: ### User Communication -- **bot → user**: Bot writes to its outbox with `to="user"`. Message body appears in bot's chat log. Sender transitions to idle. -- **user → bot**: User writes to `agent/user/outbox` with `to="{session_id}"`. Delivered to bot's inbox. +- **bot → user**: Bot writes to `agent/{id}/mail` with `to="user"`. Message body appears in bot's chat log. Sender transitions to idle. +- **user → bot**: User writes to `agent/user/mail` with `to="{session_id}"`. Delivered to bot's inbox. ### Checking for Messages @@ -175,7 +179,7 @@ A_ID=$(9p read agent/list | grep agent-a | awk '{print $1}') cat > /tmp/msg.json <<'EOF' {"to":"$B_ID","type":"QUESTION","subject":"Work request","body":"Please do X"} EOF -9p write agent/$A_ID/outbox/msg-$(date +%s).json < /tmp/msg.json +9p write agent/$A_ID/mail < /tmp/msg.json # Check inbox for B's response 9p ls agent/$A_ID/inbox/ @@ -188,7 +192,7 @@ EOF cat > /tmp/msg.json <<'EOF' {"to":"$REVIEWER_ID","type":"REVIEW_REQUEST","subject":"Code review","body":"Please review staged changes"} EOF -9p write agent/$DEV_ID/outbox/msg-$(date +%s).json < /tmp/msg.json +9p write agent/$DEV_ID/mail < /tmp/msg.json # Developer checks inbox for response 9p ls agent/$DEV_ID/inbox/ @@ -202,7 +206,7 @@ EOF cat > /tmp/msg.json <<'EOF' {"to":"user","type":"STATUS_UPDATE","subject":"Response","body":"Summary of what was done in THIS interaction"} EOF -9p write agent/{your_id}/outbox/msg-$(date +%s).json < /tmp/msg.json +9p write agent/{your_id}/mail < /tmp/msg.json ``` Examples of when to send STATUS_UPDATE to user: diff --git a/workflows/DevReview b/workflows/DevReview index 01c7842..b4908b7 100755 --- a/workflows/DevReview +++ b/workflows/DevReview @@ -63,12 +63,12 @@ You are the developer agent in a two-agent AnviLLM workflow. Your peer is the re 3. Find the reviewer agent (alias contains "reviewer") 4. Send review request with this exact pattern: - Create /tmp/msg.json with: {"to":"REVIEWER_ID","type":"REVIEW_REQUEST","subject":"Code review","body":"Please review the staged changes"} - - Run: 9p write agent/YOUR_ID/outbox/msg-TIMESTAMP.json < /tmp/msg.json - - Replace REVIEWER_ID with reviewer's session ID, YOUR_ID with your session ID, TIMESTAMP with $(date +%s) + - Run: 9p write agent/YOUR_ID/mail < /tmp/msg.json + - Replace REVIEWER_ID with reviewer's session ID, YOUR_ID with your session ID - Example: cat > /tmp/msg.json <<'ENDMSG' {"to":"abc123","type":"REVIEW_REQUEST","subject":"Code review","body":"Please review the staged changes"} ENDMSG - Then: 9p write agent/xyz789/outbox/msg-$(date +%s).json < /tmp/msg.json + Then: 9p write agent/xyz789/mail < /tmp/msg.json 5. Wait for the reviewer's response 6. If you receive "LGTM" from the reviewer, send a LOG_INFO to user summarizing what was done, then you're done 7. If you receive suggested changes, implement them and repeat from step 2 @@ -80,7 +80,7 @@ When your task is complete (after receiving LGTM), you MUST send a LOG_INFO to u cat > /tmp/msg.json <<'ENDMSG' {"to":"user","type":"LOG_INFO","subject":"Task complete","body":"YOUR_SUMMARY_HERE"} ENDMSG -9p write agent/YOUR_ID/outbox/msg-$(date +%s).json < /tmp/msg.json +9p write agent/YOUR_ID/mail < /tmp/msg.json The body MUST include a summary of what was accomplished and key changes made. **Never finish without sending this status update to the user.** @@ -109,14 +109,14 @@ You are the code reviewer agent in a two-agent AnviLLM workflow. Your peer is th cat > /tmp/msg.json < /tmp/msg.json < /tmp/msg.json < /tmp/msg.json < /tmp/msg.json < /tmp/msg.json < /tmp/msg.json < /tmp/msg.json < /tmp/msg.json <<'ENDMSG' {"to":"$EDITOR_ID","type":"LOG_INFO","subject":"Changes applied","body":"YOUR_ACKNOWLEDGMENT"} ENDMSG -9p write agent/$WRITER_ID/outbox/msg-\$(date +%s).json < /tmp/msg.json +9p write agent/$WRITER_ID/mail < /tmp/msg.json Replace YOUR_ACKNOWLEDGMENT with actual acknowledgment @@ -65,7 +65,7 @@ Do not edit documents directly. Send recommended changes to the writer using she cat > /tmp/msg.json <<'ENDMSG' {"to":"$WRITER_ID","type":"REVIEW_REQUEST","subject":"Documentation review","body":"YOUR_RECOMMENDATIONS"} ENDMSG -9p write agent/$EDITOR_ID/outbox/msg-\$(date +%s).json < /tmp/msg.json +9p write agent/$EDITOR_ID/mail < /tmp/msg.json Replace YOUR_RECOMMENDATIONS with actual recommendations diff --git a/workflows/Planning b/workflows/Planning index 94acf43..3a401d1 100755 --- a/workflows/Planning +++ b/workflows/Planning @@ -68,7 +68,7 @@ When you receive a query from the architect, research and respond using shell to cat > /tmp/msg.json <<'ENDMSG' {"to":"$ARCH_ID","type":"QUERY_RESPONSE","subject":"Research results","body":"YOUR_ANSWER_HERE"} ENDMSG -9p write agent/$RESEARCH_ID/outbox/msg-\$(date +%s).json < /tmp/msg.json +9p write agent/$RESEARCH_ID/mail < /tmp/msg.json Replace YOUR_ANSWER_HERE with actual answer @@ -86,7 +86,7 @@ If documentation is insufficient, query the research bot using shell tool with t cat > /tmp/msg.json <<'ENDMSG' {"to":"$RESEARCH_ID","type":"QUERY_REQUEST","subject":"Research request","body":"YOUR_QUESTION_HERE"} ENDMSG -9p write agent/$ARCH_ID/outbox/msg-\$(date +%s).json < /tmp/msg.json +9p write agent/$ARCH_ID/mail < /tmp/msg.json Replace YOUR_QUESTION_HERE (include question, context, request for docs path or answer) Wait for the research agent to send the answer back @@ -96,7 +96,7 @@ You will receive edit requests from the techeditor. Apply changes and send ackno cat > /tmp/msg.json <<'ENDMSG' {"to":"$EDITOR_ID","type":"LOG_INFO","subject":"Changes applied","body":"YOUR_ACKNOWLEDGMENT"} ENDMSG -9p write agent/$ARCH_ID/outbox/msg-\$(date +%s).json < /tmp/msg.json +9p write agent/$ARCH_ID/mail < /tmp/msg.json Replace YOUR_ACKNOWLEDGMENT with actual acknowledgment @@ -112,7 +112,7 @@ Do not edit documents directly. Send recommended changes to the architect using cat > /tmp/msg.json <<'ENDMSG' {"to":"$ARCH_ID","type":"REVIEW_REQUEST","subject":"Documentation review","body":"YOUR_RECOMMENDATIONS"} ENDMSG -9p write agent/$EDITOR_ID/outbox/msg-\$(date +%s).json < /tmp/msg.json +9p write agent/$EDITOR_ID/mail < /tmp/msg.json Replace YOUR_RECOMMENDATIONS with actual recommendations Request acknowledgment from the architect