Add web server, event stream, and Rive contract
- crows serve: HTTP API, Server-Sent Events per list, web approvals with a 10-minute fail-safe timeout; localhost only by default - Shared pipeline for command line and server; trust.changed events - EVENTS.md: server/UI contract; RIVE_CONTRACT.md: data-binding spec for the animator (one Crow artboard for v1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQnSw4RRWp8uqpj3nsnnXF
This commit is contained in:
12 files changed
+1320
-129
No files matched your search
@@ -58,12 +58,24 @@ func (b *Budget) Check() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Budget) Spent() int {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.spent
|
||||
}
|
||||
|
||||
func (b *Budget) Add(n int) {
|
||||
b.mu.Lock()
|
||||
b.spent += n
|
||||
b.mu.Unlock()
|
||||
}
|
||||
|
||||
// Watcher hears every takeoff and landing (Sentry and Elder included). It may be nil.
|
||||
type Watcher interface {
|
||||
Flying(crow string)
|
||||
Landed(crow string, tokens int, err error)
|
||||
}
|
||||
|
||||
type Flock struct {
|
||||
brain Brain
|
||||
g Guardrails
|
||||
@@ -91,7 +103,7 @@ func (f *Flock) pick(n int) []string {
|
||||
}
|
||||
|
||||
// ask is the only way any crow touches the model: budget, flock cap, timeout, log.
|
||||
func (f *Flock) ask(ctx context.Context, crow, system, prompt string, budgets ...*Budget) (string, error) {
|
||||
func (f *Flock) ask(ctx context.Context, w Watcher, crow, system, prompt string, budgets ...*Budget) (string, error) {
|
||||
for _, b := range budgets {
|
||||
if err := b.Check(); err != nil {
|
||||
f.log.Write(Event{Kind: "grounded", Crow: crow, Error: err.Error()})
|
||||
@@ -101,6 +113,9 @@ func (f *Flock) ask(ctx context.Context, crow, system, prompt string, budgets ..
|
||||
f.sem <- struct{}{}
|
||||
defer func() { <-f.sem }()
|
||||
|
||||
if w != nil {
|
||||
w.Flying(crow)
|
||||
}
|
||||
cctx, cancel := context.WithTimeout(ctx, f.g.CrowTimeout)
|
||||
defer cancel()
|
||||
start := time.Now()
|
||||
@@ -116,11 +131,14 @@ func (f *Flock) ask(ctx context.Context, crow, system, prompt string, budgets ..
|
||||
e.Error = err.Error()
|
||||
}
|
||||
f.log.Write(e)
|
||||
if w != nil {
|
||||
w.Landed(crow, tokens, err)
|
||||
}
|
||||
return text, err
|
||||
}
|
||||
|
||||
func (f *Flock) Triage(ctx context.Context, item string, budgets ...*Budget) (Triage, error) {
|
||||
text, err := f.ask(ctx, "Sentry", sentrySystem, "ITEM: "+item, budgets...)
|
||||
func (f *Flock) Triage(ctx context.Context, w Watcher, item string, budgets ...*Budget) (Triage, error) {
|
||||
text, err := f.ask(ctx, w, "Sentry", sentrySystem, "ITEM: "+item, budgets...)
|
||||
if err != nil {
|
||||
return Triage{}, err
|
||||
}
|
||||
@@ -151,16 +169,16 @@ func (f *Flock) Triage(ctx context.Context, item string, budgets ...*Budget) (Tr
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (f *Flock) Execute(ctx context.Context, item string, t Triage, budgets ...*Budget) (Outcome, error) {
|
||||
func (f *Flock) Execute(ctx context.Context, w Watcher, item string, t Triage, budgets ...*Budget) (Outcome, error) {
|
||||
switch t.Mode {
|
||||
case "split":
|
||||
// Split depth is 1: subtasks go straight to crows, never back to the Sentry.
|
||||
crows := f.pick(len(t.Subtasks))
|
||||
reports, err := f.fanOut(ctx, crows, t.Subtasks, budgets...)
|
||||
reports, err := f.fanOut(ctx, w, crows, t.Subtasks, budgets...)
|
||||
if err != nil {
|
||||
return Outcome{}, err
|
||||
}
|
||||
return f.reconcile(ctx, item, crows, t.Subtasks, reports, budgets...)
|
||||
return f.reconcile(ctx, w, item, crows, t.Subtasks, reports, budgets...)
|
||||
case "murder":
|
||||
n := min(f.g.MurderSize, f.g.MaxCrowsInFlight)
|
||||
crows := f.pick(n)
|
||||
@@ -168,19 +186,19 @@ func (f *Flock) Execute(ctx context.Context, item string, t Triage, budgets ...*
|
||||
for i := range tasks {
|
||||
tasks[i] = item
|
||||
}
|
||||
reports, err := f.fanOut(ctx, crows, tasks, budgets...)
|
||||
reports, err := f.fanOut(ctx, w, crows, tasks, budgets...)
|
||||
if err != nil {
|
||||
return Outcome{}, err
|
||||
}
|
||||
return f.reconcile(ctx, item, crows, tasks, reports, budgets...)
|
||||
return f.reconcile(ctx, w, item, crows, tasks, reports, budgets...)
|
||||
default:
|
||||
c := f.pick(1)[0]
|
||||
ans, err := f.ask(ctx, c, crowSystem, "TASK: "+item, budgets...)
|
||||
ans, err := f.ask(ctx, w, c, crowSystem, "TASK: "+item, budgets...)
|
||||
return Outcome{Answer: ans, Crows: []string{c}}, err
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Flock) fanOut(ctx context.Context, crows, tasks []string, budgets ...*Budget) ([]string, error) {
|
||||
func (f *Flock) fanOut(ctx context.Context, w Watcher, crows, tasks []string, budgets ...*Budget) ([]string, error) {
|
||||
reports := make([]string, len(tasks))
|
||||
errs := make([]error, len(tasks))
|
||||
var wg sync.WaitGroup
|
||||
@@ -188,7 +206,7 @@ func (f *Flock) fanOut(ctx context.Context, crows, tasks []string, budgets ...*B
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
reports[i], errs[i] = f.ask(ctx, crows[i], crowSystem, "TASK: "+tasks[i], budgets...)
|
||||
reports[i], errs[i] = f.ask(ctx, w, crows[i], crowSystem, "TASK: "+tasks[i], budgets...)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
@@ -206,12 +224,12 @@ func (f *Flock) fanOut(ctx context.Context, crows, tasks []string, budgets ...*B
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
func (f *Flock) reconcile(ctx context.Context, item string, crows, tasks, reports []string, budgets ...*Budget) (Outcome, error) {
|
||||
func (f *Flock) reconcile(ctx context.Context, w Watcher, item string, crows, tasks, reports []string, budgets ...*Budget) (Outcome, error) {
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "ORIGINAL TASK: %s\n\n", item)
|
||||
for i := range reports {
|
||||
fmt.Fprintf(&sb, "--- %s on %q:\n%s\n\n", crows[i], tasks[i], reports[i])
|
||||
}
|
||||
ans, err := f.ask(ctx, "Elder", elderSystem, sb.String(), budgets...)
|
||||
ans, err := f.ask(ctx, w, "Elder", elderSystem, sb.String(), budgets...)
|
||||
return Outcome{Answer: ans, Crows: append(append([]string{}, crows...), "Elder")}, err
|
||||
}
|
||||
Reference in new issue
Block a user