So Many Crows v1: Sentry triage, crows, guardrails, trust ledger
Go command-line tool. Sentry classifies each todo item as solo, split, or murder; text-only crows fly under budget, timeout, and flock caps; trust is earned per crow and per task type; abusive requesters get a mild, fading cooldown. Runs offline with a mock brain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQnSw4RRWp8uqpj3nsnnXF
This commit is contained in:
10 files changed
+999
No files matched your search
@@ -0,0 +1,141 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Brain is whatever model the crows think with.
|
||||
type Brain interface {
|
||||
Name() string
|
||||
Ask(ctx context.Context, system, prompt string) (text string, tokens int, err error)
|
||||
}
|
||||
|
||||
// NewBrain uses Claude when ANTHROPIC_API_KEY is set, otherwise an offline mock.
|
||||
func NewBrain() Brain {
|
||||
key := os.Getenv("ANTHROPIC_API_KEY")
|
||||
if key == "" {
|
||||
return MockBrain{}
|
||||
}
|
||||
model := os.Getenv("CROWS_MODEL")
|
||||
if model == "" {
|
||||
model = "claude-sonnet-5-5"
|
||||
}
|
||||
return &AnthropicBrain{key: key, model: model, client: &http.Client{}}
|
||||
}
|
||||
|
||||
type AnthropicBrain struct {
|
||||
key, model string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func (b *AnthropicBrain) Name() string { return "Claude (" + b.model + ")" }
|
||||
|
||||
func (b *AnthropicBrain) Ask(ctx context.Context, system, prompt string) (string, int, error) {
|
||||
body, _ := json.Marshal(map[string]any{
|
||||
"model": b.model,
|
||||
"max_tokens": 2048,
|
||||
"system": system,
|
||||
"messages": []map[string]string{{"role": "user", "content": prompt}},
|
||||
})
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.anthropic.com/v1/messages", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
req.Header.Set("x-api-key", b.key)
|
||||
req.Header.Set("anthropic-version", "2023-06-01")
|
||||
req.Header.Set("content-type", "application/json")
|
||||
|
||||
resp, err := b.client.Do(req)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
raw, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", 0, fmt.Errorf("model API returned %d: %s", resp.StatusCode, raw)
|
||||
}
|
||||
var out struct {
|
||||
Content []struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
Usage struct {
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
} `json:"usage"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &out); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
var sb strings.Builder
|
||||
for _, c := range out.Content {
|
||||
if c.Type == "text" {
|
||||
sb.WriteString(c.Text)
|
||||
}
|
||||
}
|
||||
return sb.String(), out.Usage.InputTokens + out.Usage.OutputTokens, nil
|
||||
}
|
||||
|
||||
// MockBrain lets the whole flow run offline with no API key.
|
||||
type MockBrain struct{}
|
||||
|
||||
func (MockBrain) Name() string { return "offline mock" }
|
||||
|
||||
func (MockBrain) Ask(ctx context.Context, system, prompt string) (string, int, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
tokens := (len(system)+len(prompt))/4 + 40
|
||||
switch {
|
||||
case strings.Contains(system, markerSentry):
|
||||
return mockTriage(after(prompt, "ITEM:")), tokens, nil
|
||||
case strings.Contains(system, markerElder):
|
||||
return "[mock] Elder's reconciled answer from " + fmt.Sprint(strings.Count(prompt, "--- ")) + " reports.", tokens, nil
|
||||
default:
|
||||
return "[mock] Handled: " + after(prompt, "TASK:"), tokens, nil
|
||||
}
|
||||
}
|
||||
|
||||
func after(s, marker string) string {
|
||||
if i := strings.LastIndex(s, marker); i >= 0 {
|
||||
return strings.TrimSpace(s[i+len(marker):])
|
||||
}
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
func mockTriage(item string) string {
|
||||
l := strings.ToLower(item)
|
||||
tt := "other"
|
||||
switch {
|
||||
case hasAny(l, "find", "look up", "what is", "who ", "when "):
|
||||
tt = "retrieval"
|
||||
case hasAny(l, "write", "draft"):
|
||||
tt = "writing"
|
||||
case hasAny(l, "compare", "analyze", "should"):
|
||||
tt = "analysis"
|
||||
}
|
||||
t := Triage{Mode: "solo", TaskType: tt, Reason: "simple, one crow can handle it"}
|
||||
if parts := strings.Split(item, " and "); len(parts) > 1 {
|
||||
t = Triage{Mode: "split", TaskType: tt, Subtasks: parts, Reason: "multiple distinct parts"}
|
||||
} else if hasAny(l, "should", "decide", "best") {
|
||||
t = Triage{Mode: "murder", TaskType: tt, Reason: "judgment call, worth several opinions"}
|
||||
}
|
||||
b, _ := json.Marshal(t)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func hasAny(s string, subs ...string) bool {
|
||||
for _, x := range subs {
|
||||
if strings.Contains(s, x) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in new issue
Block a user