Batteries-included agent-harness base, extracted from mort's agent layer. This first cut establishes the module + the zero-coupling core primitives: - lane, dispatchguard, pendingattach, run/progress.go: moved verbatim from mort - config: host config Source seam + env-var default (nil-safe helpers) - deliver: output-egress seam + Discard/Stdout defaults - identity: AdminPolicy + MemberResolver seams (nil-safe) - fanout: programmatic N×M swarm (bounded global + per-key concurrency) - README/CLAUDE.md with the vibe-coded banner; CI with Go gates + the "core stays majordomo+stdlib only" invariant Core builds with stdlib only today; majordomo enters at P1 (model/structured). go build/vet/test -race all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
// Package config is executus's runtime-configuration seam.
|
||||
//
|
||||
// A host supplies a Source so the harness can read tunable knobs (model tiers,
|
||||
// caps, thresholds, lane widths) without depending on any particular config
|
||||
// backend. Mort adapts its DB-backed convar.Manager; Gadfly adapts environment
|
||||
// variables; a brand-new project can use Env (or pass a nil Source and rely on
|
||||
// the code defaults every reader provides).
|
||||
//
|
||||
// Design rules:
|
||||
// - Every accessor takes a code default. A Source is NEVER required to know a
|
||||
// key — readers degrade to the default, so the harness runs with zero config.
|
||||
// - Reads are LIVE: callers read on every use so a host whose backend mutates
|
||||
// at runtime (e.g. convar) propagates without a restart. Sources that cache
|
||||
// (mort's 5-minute convar cache) may additionally implement Reloader to
|
||||
// signal invalidation.
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Source is the host configuration seam. All methods take a default and must be
|
||||
// safe for concurrent use.
|
||||
type Source interface {
|
||||
String(key, def string) string
|
||||
Int(key string, def int) int
|
||||
Float(key string, def float64) float64
|
||||
Bool(key string, def bool) bool
|
||||
}
|
||||
|
||||
// Reloader is an optional capability for Sources whose values can change at
|
||||
// runtime and that can notify watchers (e.g. a tier-reload or cache
|
||||
// invalidation). Sources that do not implement it are simply read live on every
|
||||
// access. Watch returns a cancel func; a nil-safe no-op is acceptable.
|
||||
type Reloader interface {
|
||||
Watch(prefix string, fn func(key string)) (cancel func())
|
||||
}
|
||||
|
||||
// Nil-safe package helpers: callers that may hold a nil Source use these instead
|
||||
// of dereferencing. They let every battery treat config as optional.
|
||||
|
||||
func String(s Source, key, def string) string {
|
||||
if s == nil {
|
||||
return def
|
||||
}
|
||||
return s.String(key, def)
|
||||
}
|
||||
|
||||
func Int(s Source, key string, def int) int {
|
||||
if s == nil {
|
||||
return def
|
||||
}
|
||||
return s.Int(key, def)
|
||||
}
|
||||
|
||||
func Float(s Source, key string, def float64) float64 {
|
||||
if s == nil {
|
||||
return def
|
||||
}
|
||||
return s.Float(key, def)
|
||||
}
|
||||
|
||||
func Bool(s Source, key string, def bool) bool {
|
||||
if s == nil {
|
||||
return def
|
||||
}
|
||||
return s.Bool(key, def)
|
||||
}
|
||||
|
||||
// Env is the default Source: it reads process environment variables. A key is
|
||||
// mapped to an env var name by uppercasing it and replacing every rune outside
|
||||
// [A-Za-z0-9] with '_', then prefixing. So Env("GADFLY_").String("models", "")
|
||||
// reads GADFLY_MODELS, and Env("").Int("model.tier.fast.max_steps", 8) reads
|
||||
// MODEL_TIER_FAST_MAX_STEPS. An unset or unparseable value yields the default.
|
||||
func Env(prefix string) Source { return envSource{prefix: prefix} }
|
||||
|
||||
type envSource struct{ prefix string }
|
||||
|
||||
func (e envSource) envName(key string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(e.prefix)
|
||||
for _, r := range key {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
b.WriteRune(r - 32)
|
||||
case (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9'):
|
||||
b.WriteRune(r)
|
||||
default:
|
||||
b.WriteByte('_')
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (e envSource) raw(key string) (string, bool) {
|
||||
v, ok := os.LookupEnv(e.envName(key))
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return strings.TrimSpace(v), true
|
||||
}
|
||||
|
||||
func (e envSource) String(key, def string) string {
|
||||
if v, ok := e.raw(key); ok && v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func (e envSource) Int(key string, def int) int {
|
||||
if v, ok := e.raw(key); ok {
|
||||
if n, err := strconv.Atoi(v); err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func (e envSource) Float(key string, def float64) float64 {
|
||||
if v, ok := e.raw(key); ok {
|
||||
if f, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func (e envSource) Bool(key string, def bool) bool {
|
||||
if v, ok := e.raw(key); ok {
|
||||
if b, err := strconv.ParseBool(v); err == nil {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEnvNameMapping(t *testing.T) {
|
||||
e := envSource{prefix: "GADFLY_"}
|
||||
cases := map[string]string{
|
||||
"models": "GADFLY_MODELS",
|
||||
"model.tier.fast.max_steps": "GADFLY_MODEL_TIER_FAST_MAX_STEPS",
|
||||
"provider-concurrency": "GADFLY_PROVIDER_CONCURRENCY",
|
||||
"a/b.c": "GADFLY_A_B_C",
|
||||
}
|
||||
for key, want := range cases {
|
||||
if got := e.envName(key); got != want {
|
||||
t.Errorf("envName(%q) = %q, want %q", key, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvReadsAndDefaults(t *testing.T) {
|
||||
t.Setenv("EX_MODELS", "a,b,c")
|
||||
t.Setenv("EX_MAX", "12")
|
||||
t.Setenv("EX_RATIO", "0.7")
|
||||
t.Setenv("EX_ON", "true")
|
||||
t.Setenv("EX_BLANK", "")
|
||||
s := Env("EX_")
|
||||
|
||||
if got := s.String("models", "def"); got != "a,b,c" {
|
||||
t.Errorf("String = %q", got)
|
||||
}
|
||||
if got := s.String("blank", "def"); got != "def" {
|
||||
t.Errorf("blank String should fall back to default, got %q", got)
|
||||
}
|
||||
if got := s.String("missing", "def"); got != "def" {
|
||||
t.Errorf("missing String = %q", got)
|
||||
}
|
||||
if got := s.Int("max", 1); got != 12 {
|
||||
t.Errorf("Int = %d", got)
|
||||
}
|
||||
if got := s.Int("models", 1); got != 1 {
|
||||
t.Errorf("unparseable Int should default, got %d", got)
|
||||
}
|
||||
if got := s.Float("ratio", 1); got != 0.7 {
|
||||
t.Errorf("Float = %v", got)
|
||||
}
|
||||
if got := s.Bool("on", false); got != true {
|
||||
t.Errorf("Bool = %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNilSafeHelpers(t *testing.T) {
|
||||
var s Source // nil
|
||||
if String(s, "k", "d") != "d" || Int(s, "k", 7) != 7 || Float(s, "k", 1.5) != 1.5 || !Bool(s, "k", true) {
|
||||
t.Fatal("nil Source helpers must return defaults")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user