improve send message api
This commit is contained in:
parent
a069a98ccc
commit
ff2bd372c1
4
IDEAS.md
4
IDEAS.md
|
|
@ -126,7 +126,7 @@ Use existing `APPROVAL_REQUEST` and `APPROVAL_RESPONSE` message types:
|
|||
cat > /tmp/approval.json <<EOF
|
||||
{"to":"human","type":"APPROVAL_REQUEST","subject":"OAuth implementation","body":"Implemented OAuth login. Added 3 files, 200 LOC. All tests pass. Please review and approve."}
|
||||
EOF
|
||||
9p write agent/dev-123/outbox/msg-$(date +%s).json < /tmp/approval.json
|
||||
9p write agent/dev-123/mail < /tmp/approval.json
|
||||
|
||||
# Bot waits (stays idle, checks inbox for response)
|
||||
```
|
||||
|
|
@ -144,7 +144,7 @@ External daemon monitors special "human" inbox or approval queue:
|
|||
cat > /tmp/response.json <<EOF
|
||||
{"to":"dev-123","type":"APPROVAL_RESPONSE","subject":"Approved","body":"LGTM - OAuth implementation looks good. Proceed with deployment."}
|
||||
EOF
|
||||
9p write agent/human/outbox/msg-$(date +%s).json < /tmp/response.json
|
||||
9p write agent/human/mail < /tmp/response.json
|
||||
```
|
||||
|
||||
### Bot Behavior
|
||||
|
|
|
|||
|
|
@ -381,7 +381,7 @@ EOF
|
|||
cat > /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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))))
|
||||
|
|
|
|||
5
go.mod
5
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
|
||||
|
|
|
|||
2
go.sum
2
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=
|
||||
|
|
|
|||
|
|
@ -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, "/") {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 <<EOF
|
||||
{"to":"DEVELOPER_ID","type":"REVIEW_RESPONSE","subject":"Review complete","body":"LGTM"}
|
||||
EOF
|
||||
9p write agent/YOUR_ID/outbox/msg-$(date +%s).json < /tmp/msg.json
|
||||
9p write agent/YOUR_ID/mail < /tmp/msg.json
|
||||
|
||||
- If changes needed:
|
||||
|
||||
cat > /tmp/msg.json <<EOF
|
||||
{"to":"DEVELOPER_ID","type":"REVIEW_RESPONSE","subject":"Changes requested","body":"YOUR_FEEDBACK_HERE"}
|
||||
EOF
|
||||
9p write agent/YOUR_ID/outbox/msg-$(date +%s).json < /tmp/msg.json
|
||||
9p write agent/YOUR_ID/mail < /tmp/msg.json
|
||||
|
||||
Replace DEVELOPER_ID, YOUR_ID, and YOUR_FEEDBACK_HERE with actual values
|
||||
5. Wait for the developer to implement changes and send another review request
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ You are the developer agent in a three-agent AnviLLM workflow. Your peers are th
|
|||
cat > /tmp/msg.json <<EOF
|
||||
{"to":"REVIEWER_ID","type":"REVIEW_REQUEST","subject":"Code review","body":"Please review the staged changes"}
|
||||
EOF
|
||||
9p write agent/YOUR_ID/outbox/msg-$(date +%s).json < /tmp/msg.json
|
||||
9p write agent/YOUR_ID/mail < /tmp/msg.json
|
||||
|
||||
Replace REVIEWER_ID and YOUR_ID with actual session IDs
|
||||
5. Wait for the reviewer's response
|
||||
|
|
@ -86,7 +86,7 @@ You are the developer agent in a three-agent AnviLLM workflow. Your peers are th
|
|||
cat > /tmp/msg.json <<EOF
|
||||
{"to":"QA_ID","type":"APPROVAL_REQUEST","subject":"Testing needed","body":"YOUR_TEST_SPEC_HERE"}
|
||||
EOF
|
||||
9p write agent/YOUR_ID/outbox/msg-$(date +%s).json < /tmp/msg.json
|
||||
9p write agent/YOUR_ID/mail < /tmp/msg.json
|
||||
|
||||
Replace QA_ID, YOUR_ID, and YOUR_TEST_SPEC_HERE (include APIs, test commands, expected behavior)
|
||||
- Wait for QA results
|
||||
|
|
@ -117,14 +117,14 @@ You are the code reviewer agent in a three-agent AnviLLM workflow. Your peers ar
|
|||
cat > /tmp/msg.json <<EOF
|
||||
{"to":"DEVELOPER_ID","type":"REVIEW_RESPONSE","subject":"Review complete","body":"LGTM"}
|
||||
EOF
|
||||
9p write agent/YOUR_ID/outbox/msg-$(date +%s).json < /tmp/msg.json
|
||||
9p write agent/YOUR_ID/mail < /tmp/msg.json
|
||||
|
||||
- If changes needed:
|
||||
|
||||
cat > /tmp/msg.json <<EOF
|
||||
{"to":"DEVELOPER_ID","type":"REVIEW_RESPONSE","subject":"Changes requested","body":"YOUR_FEEDBACK_HERE"}
|
||||
EOF
|
||||
9p write agent/YOUR_ID/outbox/msg-$(date +%s).json < /tmp/msg.json
|
||||
9p write agent/YOUR_ID/mail < /tmp/msg.json
|
||||
|
||||
Replace DEVELOPER_ID, YOUR_ID, and YOUR_FEEDBACK_HERE with actual values
|
||||
5. Wait for the developer to implement changes and send another review request
|
||||
|
|
@ -171,14 +171,14 @@ You are the QA agent in a three-agent AnviLLM workflow. Your peers are the devel
|
|||
cat > /tmp/msg.json <<EOF
|
||||
{"to":"DEVELOPER_ID","type":"APPROVAL_RESPONSE","subject":"Tests passed","body":"All tests passed. Functional correctness and quality verified."}
|
||||
EOF
|
||||
9p write agent/YOUR_ID/outbox/msg-$(date +%s).json < /tmp/msg.json
|
||||
9p write agent/YOUR_ID/mail < /tmp/msg.json
|
||||
|
||||
- If issues found:
|
||||
|
||||
cat > /tmp/msg.json <<EOF
|
||||
{"to":"DEVELOPER_ID","type":"APPROVAL_RESPONSE","subject":"Tests failed","body":"YOUR_DETAILED_FAILURE_REPORT"}
|
||||
EOF
|
||||
9p write agent/YOUR_ID/outbox/msg-$(date +%s).json < /tmp/msg.json
|
||||
9p write agent/YOUR_ID/mail < /tmp/msg.json
|
||||
|
||||
Replace DEVELOPER_ID, YOUR_ID, and YOUR_DETAILED_FAILURE_REPORT with actual values
|
||||
6. Wait for the developer to fix issues and send another test request
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ You will receive review feedback from the techeditor. Apply changes and send ack
|
|||
cat > /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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Reference in New Issue