Merge pull request #48 from jacisjake/task/40-quill-relay

(#40) Relay — Quill posts requests on the owner's behalf
This commit was merged in pull request #48.
This commit is contained in:
jacisjake authored and GitHub committed 2026-10-28 08:58:08 -06:00
commit 9781bfa3b3
4 files changed
+252 -12

No files matched your search

+2
View File
@@ -103,6 +103,8 @@ Jev choice-tags every posted message (`kind` question). Above its high band, Jev
A nest is a room where a team of crows works together. You create a nest and pick its core crows; when a job needs more crows than the core has, the Sentry borrows from the rest of the flock. The Sentry, Elder and Quill serve every nest and are never core members. Quill (assistant crow, CHATROOM_SPEC section 6) answers `request` messages addressed to it with one `deliver` from Quill to the requester.
A relay (D3, task #40): when the owner's request asks another named member crow to do work, Quill posts a `request` authored by Quill — `to` the named crow, `refers_to` the owner message it came from, `tagged_by: "harness"` — and nothing else; the addressed crow answers through the normal must-respond loop. If Quill cannot name a member of this nest, it posts one `say` back to the requester instead and the request rests there. No new fields; relay rides the existing record shape.
- The default nest is `todo`, with an empty core: its crows come from the whole flock, so nothing there is ever "borrowed".
- Crows are picked core-first (rotating within the core), then borrowed from outside it (rotating), still capped by the flock guardrails.
- Nests persist on the server (`.crows/nests.json`).
+15 -9
View File
@@ -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}
+73 -3
View File
@@ -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":"<name>","task":"<text>"}
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)
}
}
+162
View File
@@ -0,0 +1,162 @@
package main
import (
"context"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
)
// relayBrain records every (system, prompt) pair and answers with scripted
// replies in call order, falling back to plain chatter once they run out.
type relayBrain struct {
mu sync.Mutex
pairs [][2]string
replies []string
calls int
}
func (b *relayBrain) Name() string { return "relay-rec" }
func (b *relayBrain) Ask(ctx context.Context, system, prompt string) (string, int, error) {
b.mu.Lock()
defer b.mu.Unlock()
b.pairs = append(b.pairs, [2]string{system, prompt})
reply := "just chatter"
if b.calls < len(b.replies) {
reply = b.replies[b.calls]
}
b.calls++
return reply, 10, nil
}
func newRelayServer(t *testing.T, replies ...string) (*httptest.Server, *Server, *relayBrain) {
t.Helper()
ts, s := newTestNestServer(t)
rb := &relayBrain{replies: replies}
s.p.Flock.brain = rb
return ts, s, rb
}
// askQuill posts the owner's request addressed to Quill and returns the
// stored record of that message.
func askQuill(t *testing.T, ts *httptest.Server, id, text string) map[string]any {
t.Helper()
code, req := post(t, ts.URL+"/api/nests/"+id+"/messages",
`{"author":"owner","to":"Quill","kind":"request","text":"`+text+`"}`)
if code != http.StatusCreated {
t.Fatalf("owner request: %d %v", code, req)
}
return req
}
func countBy(t *testing.T, ts *httptest.Server, id, author string, kinds ...string) (total int, matched int) {
t.Helper()
for _, m := range nestMessages(t, ts, id) {
if m["author"] != author {
continue
}
total++
for _, k := range kinds {
if m["kind"] == k {
matched++
}
}
}
return
}
func TestQuillRelayPostsRequest(t *testing.T) {
ts, _, _ := newRelayServer(t,
`RELAY {"crow":"Hugin","task":"draft the tagline"}`,
"KIND: deliver\nSo many crows, one banner.")
id := newNest(t, ts, "Lab", `"Hugin","Munin"`)
req := askQuill(t, ts, id, "get Hugin to draft the tagline")
msgs := awaitMessages(t, ts, id, 3)
var relay map[string]any
for _, m := range msgs {
if m["author"] == "Quill" && m["kind"] == "request" {
relay = m
}
}
if relay == nil {
t.Fatalf("Quill never posted the relay request: %v", msgs)
}
if relay["to"] != "Hugin" {
t.Fatalf("relay must address Hugin, got %v", relay["to"])
}
if relay["refers_to"] != req["msg_id"] {
t.Fatalf("relay must refer to the owner message %v, got %v", req["msg_id"], relay["refers_to"])
}
if relay["text"] != "draft the tagline" {
t.Fatalf("relay text must carry the task, got %v", relay["text"])
}
hugin := false
for _, m := range msgs {
if m["author"] == "Hugin" {
hugin = true
}
}
if !hugin {
t.Fatalf("the addressed crow must answer the relay through must-respond: %v", msgs)
}
}
func TestQuillRelayUnknownCrowStalls(t *testing.T) {
ts, _, _ := newRelayServer(t, `RELAY {"crow":"Soot","task":"blacken the roof"}`)
id := newNest(t, ts, "Lab", `"Hugin","Munin"`)
askQuill(t, ts, id, "get Soot to blacken the roof")
awaitMessages(t, ts, id, 2)
time.Sleep(200 * time.Millisecond)
msgs := nestMessages(t, ts, id)
total, requests := countBy(t, ts, id, "Quill", "request")
if total != 1 || requests != 0 {
t.Fatalf("want exactly one Quill message and zero Quill requests, got %v in %v", msgs, total)
}
var stall map[string]any
for _, m := range msgs {
if m["author"] == "Quill" {
stall = m
}
}
if stall == nil || stall["kind"] != "say" {
t.Fatalf("the unknown-crow reply must be a Quill say, got %v", stall)
}
if stall["to"] != "owner" {
t.Fatalf("the say must go back to the requester, got %v", stall["to"])
}
}
func TestQuillRelayNotAnsweredTwice(t *testing.T) {
ts, _, _ := newRelayServer(t,
`RELAY {"crow":"Hugin","task":"draft the tagline"}`,
"KIND: deliver\nSo many crows, one banner.")
id := newNest(t, ts, "Lab", `"Hugin","Munin"`)
askQuill(t, ts, id, "get Hugin to draft the tagline")
awaitMessages(t, ts, id, 3)
time.Sleep(200 * time.Millisecond)
total, delivers := countBy(t, ts, id, "Quill", "deliver")
if total != 1 {
t.Fatalf("a successful relay must leave Quill with exactly one message, got %d", total)
}
if delivers != 0 {
t.Fatalf("a relay must not also deliver, got %d delivers", delivers)
}
}
func TestQuillRelayGoesThroughFlock(t *testing.T) {
ts, s := newTestNestServer(t)
id := newNest(t, ts, "Lab", `"Hugin","Munin"`)
s.p.Flock.brain = &relayBrain{replies: []string{`RELAY {"crow":"Hugin","task":"draft the tagline"}`}}
s.p.Flock.sem = make(chan struct{}, 0)
askQuill(t, ts, id, "get Hugin to draft the tagline")
time.Sleep(300 * time.Millisecond)
if got := nestMessages(t, ts, id); len(got) != 1 {
t.Fatalf("a saturated flock means ask fails and Quill posts nothing: %v", got)
}
}