137 lines
4.3 KiB
Go
137 lines
4.3 KiB
Go
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.
|
|
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
|
|
|
|
// quillHistory builds a block of the most recent nest messages (excluding the
|
|
// requesting message), formatted as "[author|kind] text" with text truncated
|
|
// to 200 runes. Returns a single line when no earlier messages exist.
|
|
func quillHistory(all []Message, excludeID int) string {
|
|
var filtered []Message
|
|
for _, m := range all {
|
|
if m.ID != excludeID {
|
|
filtered = append(filtered, m)
|
|
}
|
|
}
|
|
if len(filtered) == 0 {
|
|
return "The nest has no earlier messages."
|
|
}
|
|
if len(filtered) > quillHistoryCap {
|
|
filtered = filtered[len(filtered)-quillHistoryCap:]
|
|
}
|
|
var sb strings.Builder
|
|
for _, m := range filtered {
|
|
text := []rune(m.Text)
|
|
if len(text) > 200 {
|
|
text = text[:200]
|
|
}
|
|
sb.WriteString(fmt.Sprintf("[%s|%s] %s\n", m.Author, m.Kind, string(text)))
|
|
}
|
|
return strings.TrimRight(sb.String(), "\n")
|
|
}
|
|
|
|
// 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. 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)
|
|
history := quillHistory(s.msgs.All(nest.ID), req.ID)
|
|
prompt := history + "\n---\nFROM: " + req.Author + "\nMESSAGE: " + req.Text
|
|
answer, err := s.p.Flock.ask(s.ctx, nil, "Quill", system, prompt, budget)
|
|
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: answer,
|
|
To: req.Author,
|
|
RefersTo: req.MsgID(),
|
|
TaggedBy: "harness",
|
|
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
|
}
|
|
if record, err := s.msgs.Append(reply); err == nil {
|
|
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)
|
|
}
|
|
}
|