Crow personas and generated banter
- personas.go: backstory, prompt policy and output policy per crow; Hugin and Munin grounded in Grimnismal 20 and Gylfaginning 38; every working crow also follows a common output policy - Personas go into each crow's instructions; Sentry keeps JSON-only - banter.go: retrieve backstory, relationships and recent flock memory, then the speaking crow generates one line in character; max 2 per list against the list budget, never blocks work, unusable lines dropped - crow.banter event, GET /api/crows, `crows personas` -> PERSONAS.md - UI: epithets on tiles and messages, banter as quiet asides Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQnSw4RRWp8uqpj3nsnnXF
This commit is contained in:
13 files changed
+824
-8
No files matched your search
+37
@@ -3,10 +3,12 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Emitter receives the events in EVENTS.md. The web server streams them; the
|
||||
@@ -47,6 +49,9 @@ type Pipeline struct {
|
||||
Log *FlightLog
|
||||
G Guardrails
|
||||
Approver Approver
|
||||
|
||||
mem *flockMemory // shared memory banter retrieves from
|
||||
memOnce sync.Once
|
||||
}
|
||||
|
||||
func itemID(listID string, i int) string { return fmt.Sprintf("%s-%d", listID, i+1) }
|
||||
@@ -63,6 +68,8 @@ func (p *Pipeline) Run(ctx context.Context, l List, em Emitter) ([]itemResult, *
|
||||
}
|
||||
p.Log.Write(Event{Kind: "list", Requester: l.Requester, Note: fmt.Sprintf("%s (%d items)", l.Source, len(l.Items))})
|
||||
em.Emit("list.started", map[string]any{"items": items, "requester": l.Requester, "nest_id": l.Nest.ID})
|
||||
chat := &chatter{f: p.Flock, mem: p.memory(), em: em, budget: listBudget, nest: l.Nest.ID, roll: rand.Float64}
|
||||
ctx = withChatter(ctx, chat)
|
||||
|
||||
results := make([]itemResult, len(l.Items))
|
||||
if l.Parallel {
|
||||
@@ -81,6 +88,7 @@ func (p *Pipeline) Run(ctx context.Context, l List, em Emitter) ([]itemResult, *
|
||||
}
|
||||
}
|
||||
|
||||
chat.wait(20 * time.Second) // let in-flight banter land before the list closes
|
||||
p.save()
|
||||
em.Emit("list.finished", map[string]any{"tokens_used": listBudget.Spent(), "token_budget": listBudget.limit})
|
||||
return results, listBudget
|
||||
@@ -133,6 +141,14 @@ func (p *Pipeline) item(ctx context.Context, l List, i int, listBudget *Budget,
|
||||
}
|
||||
needOutput := lowest < Regular
|
||||
em.Emit("item.answered", map[string]any{"item_id": id, "answer": out.Answer, "crows": out.Crows, "needs_approval": needOutput})
|
||||
chat := chatterFrom(ctx)
|
||||
chat.remember(fmt.Sprintf("%s answered %q", strings.Join(out.Crows, ", "), item))
|
||||
for _, pair := range [][2]string{{"Hugin", "Munin"}, {"Corvina", "Edgar"}, {"Pepper", "Jet"}} {
|
||||
if slices.Contains(out.Crows, pair[0]) && slices.Contains(out.Crows, pair[1]) {
|
||||
chat.moment(ctx, id, pair[1], pair[0], fmt.Sprintf("%s and %s just worked on the same task together: %q.", pair[0], pair[1], item))
|
||||
break
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case needOutput:
|
||||
r.Verdict = p.judge(ctx, Approval{ListID: l.ID, ItemID: id, Item: item, Target: "output"}, out.Crows, t.TaskType, em)
|
||||
@@ -158,8 +174,23 @@ func (p *Pipeline) judge(ctx context.Context, a Approval, holders []string, task
|
||||
v = "rejected"
|
||||
}
|
||||
em.Emit("verdict", map[string]any{"item_id": a.ItemID, "target": a.Target, "verdict": v})
|
||||
chat := chatterFrom(ctx)
|
||||
what := "answer to"
|
||||
if a.Target == "triage" {
|
||||
what = "plan for"
|
||||
}
|
||||
chat.remember(fmt.Sprintf("%s's %s %q was %s by the human", strings.Join(holders, " and "), what, a.Item, v))
|
||||
if v != "approved" && len(holders) > 0 {
|
||||
subject := holders[rand.IntN(len(holders))]
|
||||
chat.moment(ctx, a.ItemID, subject, "", fmt.Sprintf("The human just %s %s's %s %q.", map[string]string{"rejected": "rejected", "timeout": "ignored, until it timed out,"}[v], subject, what, a.Item))
|
||||
}
|
||||
for _, c := range p.Ledger.Judge(holders, taskType, v == "approved", p.G.PromoteAfter) {
|
||||
em.Emit("trust.changed", map[string]any{"holder": c.Holder, "task_type": c.TaskType, "from": c.From, "to": c.To})
|
||||
if c.Holder != "flock" && c.To > c.From {
|
||||
names := []string{"stranger", "regular", "trusted"}
|
||||
chat.remember(fmt.Sprintf("%s leveled up to %s on %s", c.Holder, names[c.To], c.TaskType))
|
||||
chat.moment(ctx, a.ItemID, c.Holder, "", fmt.Sprintf("%s just leveled up: now %s on %s tasks.", c.Holder, names[c.To], c.TaskType))
|
||||
}
|
||||
}
|
||||
if a.Target == "triage" {
|
||||
p.Log.Write(Event{Kind: "verdict", Item: a.Item, Crow: "Sentry", TaskType: taskType, Verdict: v})
|
||||
@@ -202,3 +233,9 @@ func nonNil(s []string) []string {
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// memory is the flock's short shared memory, kept across lists for banter.
|
||||
func (p *Pipeline) memory() *flockMemory {
|
||||
p.memOnce.Do(func() { p.mem = &flockMemory{} })
|
||||
return p.mem
|
||||
}
|
||||
Reference in new issue
Block a user