(#26) Elder synthesis on request #33
No files matched your search
@@ -0,0 +1,91 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// echoBrain returns the prompt itself, so an Elder merge contains the reports.
|
||||||
|
type echoBrain struct{}
|
||||||
|
|
||||||
|
func (echoBrain) Name() string { return "echo" }
|
||||||
|
func (echoBrain) Ask(ctx context.Context, system, prompt string) (string, int, error) {
|
||||||
|
return prompt, 10, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEchoServer(t *testing.T) (*httptest.Server, *Server) {
|
||||||
|
t.Helper()
|
||||||
|
ts, s := newTestNestServer(t)
|
||||||
|
s.p.Flock.brain = echoBrain{}
|
||||||
|
return ts, s
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestElderAddressableInNest(t *testing.T) {
|
||||||
|
ts, s := newEchoServer(t)
|
||||||
|
id := newNest(t, ts, "Lab", `"Hugin","Munin"`)
|
||||||
|
r := postRequest(t, ts, id, "conspiracy", "is this plan risky")
|
||||||
|
postDeliver(t, ts, id, "Hugin", r, "the mint Condition is fragile")
|
||||||
|
postDeliver(t, ts, id, "Munin", r, "the audit trail says otherwise")
|
||||||
|
|
||||||
|
code, _ := postMsg(t, ts, id, `{"author":"owner","kind":"request","to":"Elder","text":"merge this","refers_to":"`+r+`"}`)
|
||||||
|
if code != http.StatusCreated {
|
||||||
|
t.Fatalf("elder request: %d", code)
|
||||||
|
}
|
||||||
|
|
||||||
|
elders := awaitMsgsBy(t, ts, id, "Elder")
|
||||||
|
if len(elders) != 1 {
|
||||||
|
t.Fatalf("want exactly one Elder message, got %v", elders)
|
||||||
|
}
|
||||||
|
m := elders[0]
|
||||||
|
text, _ := m["text"].(string)
|
||||||
|
for _, want := range []string{"fragile", "otherwise"} {
|
||||||
|
if !strings.Contains(text, want) {
|
||||||
|
t.Fatalf("Elder merge must carry both reports (missing %q): %q", want, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m["refers_to"] != r || m["to"] != "owner" || m["kind"] != "deliver" {
|
||||||
|
t.Fatalf("Elder reply shape wrong: %v", m)
|
||||||
|
}
|
||||||
|
_ = s
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestElderOnlyMergesOnRequest(t *testing.T) {
|
||||||
|
ts, s := newEchoServer(t)
|
||||||
|
id := newNest(t, ts, "Lab", `"Hugin","Munin"`)
|
||||||
|
r := postRequest(t, ts, id, "conspiracy", "is this plan risky")
|
||||||
|
postDeliver(t, ts, id, "Hugin", r, "ship it")
|
||||||
|
postDeliver(t, ts, id, "Munin", r, "ship it too")
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
if got := awaitMsgsBy(t, ts, id, "Elder"); len(got) != 0 {
|
||||||
|
t.Fatalf("Elder must stay silent unless asked, got %v", got)
|
||||||
|
}
|
||||||
|
_ = s
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseAddressingServiceRole(t *testing.T) {
|
||||||
|
ts, s := newTestNestServer(t)
|
||||||
|
id := newNest(t, ts, "Lab", `"Hugin"`)
|
||||||
|
if got := parseAddressing("please @Elder merge this", nestMembers(nestOf(s, id))); got != "Elder" {
|
||||||
|
t.Fatalf("Elder must be an addressable service handle, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func awaitMsgsBy(t *testing.T, ts *httptest.Server, id, author string) []map[string]any {
|
||||||
|
t.Helper()
|
||||||
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for {
|
||||||
|
var out []map[string]any
|
||||||
|
for _, m := range nestMessages(t, ts, id) {
|
||||||
|
if m["author"] == author {
|
||||||
|
out = append(out, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(out) > 0 || time.Now().After(deadline) {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
-2
@@ -28,9 +28,14 @@ KIND: deliver (here is the result)
|
|||||||
KIND: decline (with a reason)
|
KIND: decline (with a reason)
|
||||||
Then, on the next line, your message text.`
|
Then, on the next line, your message text.`
|
||||||
|
|
||||||
// nestMembers lists who may be addressed in a nest: its crows plus the owner.
|
// serviceRoles are addressable in a nest but never members: they only act when
|
||||||
|
// a crow's or the owner's message asks them to.
|
||||||
|
var serviceRoles = []string{"Elder"}
|
||||||
|
|
||||||
|
// nestMembers lists who may be addressed in a nest: its crows, the owner, and
|
||||||
|
// the service roles.
|
||||||
func nestMembers(nest Nest) []string {
|
func nestMembers(nest Nest) []string {
|
||||||
return append(append([]string{}, nest.Crows...), "owner")
|
return append(append(append([]string{}, nest.Crows...), "owner"), serviceRoles...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) getNestMessages(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) getNestMessages(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -179,6 +184,8 @@ func (s *Server) postNestMessage(w http.ResponseWriter, r *http.Request) {
|
|||||||
switch {
|
switch {
|
||||||
case mode == "conspiracy" && invited > 0:
|
case mode == "conspiracy" && invited > 0:
|
||||||
go s.respondConspiracy(nest, record, invited)
|
go s.respondConspiracy(nest, record, invited)
|
||||||
|
case record.To == "Elder":
|
||||||
|
go s.respondElder(nest, record)
|
||||||
case slices.Contains(nest.Crows, record.To):
|
case slices.Contains(nest.Crows, record.To):
|
||||||
go s.respondToRequest(nest, record)
|
go s.respondToRequest(nest, record)
|
||||||
}
|
}
|
||||||
@@ -308,6 +315,48 @@ func (s *Server) respondOne(nest Nest, req Message, crow string, lens bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// respondElder merges the reports on a thread: the Elder only speaks when a
|
||||||
|
// message asks it to (CHATROOM_SPEC section 6 — synthesis is requested, not a
|
||||||
|
// fixed step). It answers whoever opened the thread it merges.
|
||||||
|
func (s *Server) respondElder(nest Nest, req Message) {
|
||||||
|
if req.RefersTo == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
target, found := s.msgs.ByMsgID(req.RefersTo)
|
||||||
|
if !found {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var reports []string
|
||||||
|
for _, m := range s.msgs.ReportsTo(nest.ID, target.MsgID(), "deliver") {
|
||||||
|
if slices.Contains(nest.Crows, m.Author) {
|
||||||
|
reports = append(reports, "["+m.Author+"] "+m.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(reports) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
budget := &Budget{name: "elder-merge", limit: s.p.G.TokenBudgetPerItem}
|
||||||
|
system := systemFor("Elder", elderSystem, true)
|
||||||
|
prompt := "REQUEST: " + target.Text + "\n--- reports to merge ---\n" + strings.Join(reports, "\n--- \n")
|
||||||
|
answer, err := s.p.Flock.ask(s.ctx, nil, "Elder", system, prompt, budget)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reply := Message{
|
||||||
|
NestID: nest.ID,
|
||||||
|
Kind: "deliver",
|
||||||
|
Author: "Elder",
|
||||||
|
Text: strings.TrimSpace(answer),
|
||||||
|
To: target.Author,
|
||||||
|
RefersTo: target.MsgID(),
|
||||||
|
TaggedBy: "harness",
|
||||||
|
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
if record, err := s.msgs.Append(reply); err == nil {
|
||||||
|
s.emitNestMessage(record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// checkDivergence asks Jev whether this deliver restates earlier delivers that
|
// checkDivergence asks Jev whether this deliver restates earlier delivers that
|
||||||
// answer the same request. Above the band the crow gets a review nudge; the
|
// answer the same request. Above the band the crow gets a review nudge; the
|
||||||
// deliver itself stays in the log (nudges never silence).
|
// deliver itself stays in the log (nudges never silence).
|
||||||
|
|||||||
+19
@@ -158,6 +158,25 @@ func (s *MessageStore) NudgeFor(nestID, targetMsgID string) (Message, bool) {
|
|||||||
return Message{}, false
|
return Message{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReportsTo lists messages that answer a given referent, oldest first.
|
||||||
|
func (s *MessageStore) ReportsTo(nestID, referentID string, kinds ...string) []Message {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
var out []Message
|
||||||
|
for _, m := range s.log[nestID] {
|
||||||
|
if m.RefersTo != referentID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, k := range kinds {
|
||||||
|
if m.Kind == k {
|
||||||
|
out = append(out, m)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func (s *MessageStore) save(nestID string) error {
|
func (s *MessageStore) save(nestID string) error {
|
||||||
if err := os.MkdirAll(s.dir, 0o755); err != nil {
|
if err := os.MkdirAll(s.dir, 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
Reference in new issue
Block a user