feat: foundations — canonical types, Parse grammar, env DSNs, health, chains
Phase 1 of the majordomo build: - llm/ canonical contract (messages, parts, tools, capabilities, streaming, Model/Provider, error classification) - health/ clock-injected tracker (threshold bench, exponential capped cooldown, reset-on-success) - root Registry + Parse (verbatim model ids, inline recursive alias expansion with cycle detection, chain dedup), LLM_* env-DSN providers (go-llm parity: lazy fallback + eager LoadEnv), health-aware chain executor behind the Model interface - provider/fake scriptable test provider; hermetic test suite incl. the trailing-thinking chain and foreman:// env loading - ADRs 0001-0008, CLAUDE.md, README (honest matrix), CI workflow, docs/phase-1-design.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,2 +1,216 @@
|
||||
# majordomo
|
||||
|
||||
A clean-slate Go library for building LLM-backed agents: one canonical API
|
||||
over many model providers, a parseable model naming / failover / tiering
|
||||
system with built-in health tracking, capability-aware multimodality, tool
|
||||
calls, structured output, and composable agents and skills.
|
||||
|
||||
> **Status:** under construction, phase by phase. The
|
||||
> [support matrix](#featureprovider-support-matrix) below is kept honest:
|
||||
> *pending* means not built yet, and this README is updated in the same
|
||||
> commit as the behavior it describes.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
go get gitea.stevedudenhoeffer.com/steve/majordomo
|
||||
```
|
||||
|
||||
Requires Go 1.26+.
|
||||
|
||||
## Quickstart
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.stevedudenhoeffer.com/steve/majordomo"
|
||||
)
|
||||
|
||||
func main() {
|
||||
reg := majordomo.New() // built-ins + LLM_* env providers
|
||||
|
||||
m, err := reg.Parse("ollama-cloud/minimax-m3:cloud")
|
||||
if err != nil { panic(err) }
|
||||
|
||||
resp, err := m.Generate(context.Background(), majordomo.Request{
|
||||
Messages: []majordomo.Message{majordomo.UserText("hello!")},
|
||||
})
|
||||
if err != nil { panic(err) }
|
||||
fmt.Println(resp.Text())
|
||||
}
|
||||
```
|
||||
|
||||
`majordomo.Parse(...)` (package level) uses a lazily-built default registry
|
||||
if you don't need isolation.
|
||||
|
||||
## Model specs: targets, chains, tiers
|
||||
|
||||
A model spec is a comma-separated **failover chain**; each element is either
|
||||
a `provider/model` target or a registered **alias** (tier):
|
||||
|
||||
```go
|
||||
// Try minimax-m3 first; on failure kimi-k2.6; finally fall back to opus-4.8.
|
||||
m, _ := reg.Parse("ollama-cloud/minimax-m3:cloud,ollama-cloud/kimi-k2.6:cloud,anthropic/opus-4.8")
|
||||
|
||||
// Identical, with the registered alias "thinking" appended and expanded
|
||||
// in place as the tail of the chain:
|
||||
m, _ = reg.Parse("ollama-cloud/minimax-m3:cloud,ollama-cloud/kimi-k2.6:cloud,anthropic/opus-4.8,thinking")
|
||||
```
|
||||
|
||||
Everything after the **first `/`** (up to the next comma) is the model id,
|
||||
passed to the provider **verbatim** — tags (`:cloud`, `:30b`) and ids with
|
||||
extra slashes survive intact. majordomo never validates ids against a
|
||||
catalog.
|
||||
|
||||
### Custom tiers (aliases)
|
||||
|
||||
```go
|
||||
reg.RegisterAlias("thinking", "anthropic/opus-4.8,ollama-cloud/minimax-m3:cloud")
|
||||
reg.RegisterAlias("workhorse", "ollama-cloud/minimax-m2.7:cloud,ollama-cloud/qwen3-coder:480b-cloud")
|
||||
|
||||
m, _ := reg.Parse("thinking") // a chain, same Model interface as a single target
|
||||
```
|
||||
|
||||
Aliases may appear anywhere in a chain (head, middle, tail), may reference
|
||||
other aliases, and expand inline and recursively; cycles are detected and
|
||||
returned as errors.
|
||||
|
||||
### Failover & health
|
||||
|
||||
Chains are health-tracked per target:
|
||||
|
||||
- A **single transient error** (429/5xx, timeout, connection failure) is
|
||||
retried once on the same target.
|
||||
- **Repeated transient errors** (default: 2 consecutive failed attempts)
|
||||
bench the target — chains skip it until its cooldown expires (exponential:
|
||||
5s, 10s, 20s, ... capped at 5m). Any success resets it.
|
||||
- `model not found` advances down the chain without penalty; auth/malformed
|
||||
errors fail fast (failing over can't fix a bad key). All knobs are
|
||||
configurable via `WithChainConfig` / `WithHealthConfig`.
|
||||
- If every element fails, you get one joined error naming each target and
|
||||
why it failed.
|
||||
|
||||
## Providers
|
||||
|
||||
### Built-in env vars
|
||||
|
||||
| Provider | Spec name | Key env var | Default endpoint |
|
||||
|----------|-----------|-------------|------------------|
|
||||
| OpenAI (+compatible) | `openai` | `OPENAI_API_KEY` | api.openai.com *(pending)* |
|
||||
| Anthropic (+compatible) | `anthropic` | `ANTHROPIC_API_KEY` | api.anthropic.com *(pending)* |
|
||||
| Google (Gemini) | `google` | `GOOGLE_API_KEY` / `GEMINI_API_KEY` | Gen AI API *(pending)* |
|
||||
| Ollama Cloud | `ollama-cloud` | `OLLAMA_API_KEY` | https://ollama.com *(pending)* |
|
||||
| Ollama (local) | `ollama` | — | `OLLAMA_HOST` or http://localhost:11434 *(pending)* |
|
||||
| foreman | `foreman` | — (token via DSN) | requires DSN/base URL *(pending)* |
|
||||
|
||||
### `LLM_*` env-DSN provider definitions
|
||||
|
||||
Define named providers entirely from the environment (go-llm parity):
|
||||
|
||||
```
|
||||
LLM_M1=foreman://test-token-change-me@foreman-m1.orgrimmar.dudenhoeffer.casa
|
||||
LLM_M5=foreman://test-token-change-me@foreman-m5.orgrimmar.dudenhoeffer.casa
|
||||
```
|
||||
|
||||
defines providers `m1` and `m5` (foreman targets — native Ollama wire
|
||||
protocol behind a bearer token). They are first-class in `Parse`, chains,
|
||||
and aliases:
|
||||
|
||||
```go
|
||||
m, _ := reg.Parse("m5/qwen3:30b,m1/qwen3:30b,thinking")
|
||||
```
|
||||
|
||||
DSN format: `scheme://[token@]host[/path]`, scheme ∈ `foreman`, `ollama`,
|
||||
`ollama-cloud`, `openai`, `anthropic`, `google`/`gemini`, or any scheme you
|
||||
add with `RegisterScheme`. The token is the credential (bearer token / API
|
||||
key); the base URL is always `https://host[/path]`. `New()` loads `LLM_*`
|
||||
vars eagerly; unknown provider names also resolve lazily at Parse time
|
||||
(`my-prov/x` → `LLM_MY_PROV`).
|
||||
|
||||
### Custom providers
|
||||
|
||||
Implement the two-method `Provider` interface and register it:
|
||||
|
||||
```go
|
||||
reg.RegisterProvider(myProvider) // now "myprovider/model-x" parses, chains, aliases
|
||||
```
|
||||
|
||||
## Multimodality *(pending — Phase 3)*
|
||||
|
||||
Attach images without knowing the target's limits; majordomo normalizes
|
||||
(downscale, re-encode, count/size limits) against the resolved target's
|
||||
declared capabilities and rejects clearly what cannot fit.
|
||||
|
||||
```go
|
||||
resp, err := m.Generate(ctx, majordomo.Request{
|
||||
Messages: []majordomo.Message{
|
||||
majordomo.UserParts(majordomo.Text("what's in this image?"),
|
||||
majordomo.Image("image/png", pngBytes)),
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Tool calls *(canonical API ready; provider wiring pending — Phase 3)*
|
||||
|
||||
```go
|
||||
weather := majordomo.Tool{
|
||||
Name: "get_weather",
|
||||
Description: "Current weather for a city",
|
||||
Parameters: json.RawMessage(`{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}`),
|
||||
Handler: func(ctx context.Context, args json.RawMessage) (any, error) {
|
||||
var p struct{ City string `json:"city"` }
|
||||
_ = json.Unmarshal(args, &p)
|
||||
return map[string]any{"city": p.City, "temp_c": 21}, nil
|
||||
},
|
||||
}
|
||||
resp, _ := m.Generate(ctx, req, majordomo.WithTools(weather))
|
||||
// resp.ToolCalls → execute → append ToolResultsMessage → continue
|
||||
```
|
||||
|
||||
## Structured output *(canonical API ready; provider wiring pending — Phase 3)*
|
||||
|
||||
```go
|
||||
resp, _ := m.Generate(ctx, req, majordomo.WithSchema(schemaJSON, "answer"))
|
||||
```
|
||||
|
||||
A generic `Generate[T]` helper (schema from your struct, unmarshal into it)
|
||||
lands with the agent phase.
|
||||
|
||||
## Agents & skills *(pending — Phases 5–6)*
|
||||
|
||||
Agents = model + system prompt + toolboxes, running a tool-dispatch loop;
|
||||
skills = reusable instruction+tool bundles attachable to any agent.
|
||||
|
||||
## Feature/provider support matrix
|
||||
|
||||
| Provider | Resolve/Parse | Chat | Streaming | Tools | Structured | Images | Env DSN |
|
||||
|----------------------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
| OpenAI (+compatible) | ✅ | pending | pending | pending | pending | pending | ✅ |
|
||||
| Anthropic (+compat) | ✅ | pending | pending | pending | pending | pending | ✅ |
|
||||
| Google (Gemini) | ✅ | pending | pending | pending | pending | pending | ✅ |
|
||||
| Ollama Cloud | ✅ | pending | pending | pending | pending | pending | ✅ |
|
||||
| Ollama (local) | ✅ | pending | pending | pending | pending | pending | ✅ |
|
||||
| foreman | ✅ | pending | pending | pending | pending | pending | ✅ |
|
||||
| fake (testing) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — |
|
||||
|
||||
Cross-cutting: Parse grammar ✅ · aliases/tiers ✅ · failover chains ✅ ·
|
||||
health tracking/backoff ✅ · LLM_* env DSNs ✅ · media pipeline pending ·
|
||||
agent loop pending · skills pending · `Generate[T]` pending.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
go build ./... && go vet ./... && go test -race -count=1 ./...
|
||||
```
|
||||
|
||||
The default test suite is fully hermetic (no network, no credentials).
|
||||
Live integration tests (Phase 8) are gated behind the `live` build tag and
|
||||
read `.env` (see `.env.example`; never commit `.env`).
|
||||
|
||||
Design decisions are recorded in [docs/adr/](docs/adr/README.md);
|
||||
conventions in [CLAUDE.md](CLAUDE.md); build history in
|
||||
[progress.md](progress.md).
|
||||
|
||||
Reference in New Issue
Block a user