From 31bf98411c55f3d26d3c7d7b54dcea5342ce528a Mon Sep 17 00:00:00 2001 From: Smokey <150986568+jacisjake@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:48:51 -0600 Subject: [PATCH] feat(relay): Quill relays owner asks as nest requests (#40) --- nestapi.go | 24 ++++++++++------- quill.go | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 88 insertions(+), 12 deletions(-) diff --git a/nestapi.go b/nestapi.go index da44549..ab1fb86 100644 --- a/nestapi.go +++ b/nestapi.go @@ -181,14 +181,22 @@ func (s *Server) postNestMessage(w http.ResponseWriter, r *http.Request) { } s.emitNestMessage(record) + s.triggerResponse(nest, record) - // Event-driven trigger (no idle loops): - // conspiracy -> several invited crows, each from its own lens - // any other request addressed to a member crow -> one must-respond reply - if kind == "request" { + writeJSON(w, http.StatusCreated, nestRecord(record)) +} + +// triggerResponse is the event-driven response rule (no idle loops): a +// conspiracy invites several crows, each from its own lens; any other request +// gets exactly one responder through the matching path; a deliver gets one +// divergence check. +// It runs for owner-posted messages and for harness-posted ones alike, so a +// Quill relay answers the named crow exactly like a direct request (#40). +func (s *Server) triggerResponse(nest Nest, record Message) { + if record.Kind == "request" { switch { - case mode == "conspiracy" && invited > 0: - go s.respondConspiracy(nest, record, invited) + case record.Mode == "conspiracy" && record.Invited > 0: + go s.respondConspiracy(nest, record, record.Invited) case record.To == "Elder": go s.respondElder(nest, record) case record.To == "Quill": @@ -200,11 +208,9 @@ func (s *Server) postNestMessage(w http.ResponseWriter, r *http.Request) { } } - if kind == "deliver" { + if record.Kind == "deliver" { go s.checkDivergence(nest, record) } - - writeJSON(w, http.StatusCreated, nestRecord(record)) } var modePostable = map[string]bool{"solo": true, "conspiracy": true, "split": true} diff --git a/quill.go b/quill.go index 8292c9e..bde0b3c 100644 --- a/quill.go +++ b/quill.go @@ -1,13 +1,22 @@ package main import ( + "encoding/json" "fmt" + "slices" "strings" "time" ) const quillSystem = `You are Quill crow, the owner's assistant and point of contact in a nest of AI crows. -Answer the owner in one or two plain sentences. You are text-only: no web, no files, no tools.` +Answer the owner in one or two plain sentences. You are text-only: no web, no files, no tools. +If the owner's message asks ANOTHER named member crow to do work, your entire reply must begin with the literal first line +RELAY {"crow":"","task":""} +and contain nothing else. Otherwise answer normally and never start your reply with RELAY.` + +// relayPrefix marks a structured relay inside Quill's one reply (D3, #40): +// the same Flock.ask call decides the relay and its payload. +const relayPrefix = "RELAY " const quillHistoryCap = 20 @@ -40,7 +49,8 @@ func quillHistory(all []Message, excludeID int) string { // respondQuill answers a message addressed to Quill, the assistant service // role (CHATROOM_SPEC section 6). D2: reads the nest for status before -// composing its reply. +// composing its reply. D3 (#40): a reply opening with the RELAY line becomes +// a nest request from Quill to the named member crow instead. func (s *Server) respondQuill(nest Nest, req Message) { budget := &Budget{name: "quill-reply", limit: s.p.G.TokenBudgetPerItem} system := systemFor("Quill", quillSystem, false) @@ -50,11 +60,29 @@ func (s *Server) respondQuill(nest Nest, req Message) { if err != nil { return } + answer = strings.TrimSpace(answer) + if strings.HasPrefix(answer, relayPrefix) { + var payload struct { + Crow string `json:"crow"` + Task string `json:"task"` + } + broken := json.Unmarshal([]byte(strings.TrimSpace(strings.TrimPrefix(answer, relayPrefix))), &payload) != nil + if !broken && slices.Contains(nest.Crows, payload.Crow) { + s.postRelay(nest, req, payload.Crow, payload.Task) + return + } + name := "" + if !broken { + name = payload.Crow + } + s.postQuillStall(nest, req, name) + return + } reply := Message{ NestID: nest.ID, Kind: "deliver", Author: "Quill", - Text: strings.TrimSpace(answer), + Text: answer, To: req.Author, RefersTo: req.MsgID(), TaggedBy: "harness", @@ -64,3 +92,45 @@ func (s *Server) respondQuill(nest Nest, req Message) { s.emitNestMessage(record) } } + +// postRelay appends Quill's relayed request to a member crow and runs it +// through the normal response trigger, so the addressed crow must-respond as +// if the owner had asked it directly. +func (s *Server) postRelay(nest Nest, req Message, crow, task string) { + relay := Message{ + NestID: nest.ID, + Kind: "request", + Author: "Quill", + Text: task, + To: crow, + RefersTo: req.MsgID(), + TaggedBy: "harness", + Timestamp: time.Now().UTC().Format(time.RFC3339), + } + if record, err := s.msgs.Append(relay); err == nil { + s.emitNestMessage(record) + s.triggerResponse(nest, record) + } +} + +// postQuillStall is the only other thing a RELAY reply can produce: one +// plain say back to the requester, naming the crow when Quill named one. +func (s *Server) postQuillStall(nest Nest, req Message, crow string) { + text := "I could not find that crow in this nest." + if crow != "" { + text = "I could not find " + crow + " in this nest." + } + stall := Message{ + NestID: nest.ID, + Kind: "say", + Author: "Quill", + Text: text, + To: req.Author, + RefersTo: req.MsgID(), + TaggedBy: "harness", + Timestamp: time.Now().UTC().Format(time.RFC3339), + } + if record, err := s.msgs.Append(stall); err == nil { + s.emitNestMessage(record) + } +}