docs: examples for every hard requirement + mort migration blueprint

Phase 7: nine runnable examples/ programs (parse, failover chains with
trailing alias, tiers, LLM_* env providers, multimodal, tool loop,
Generate[T], agent, skills); docs/mort-migration.md mapping mort's
go-llm/go-agentkit usage onto majordomo APIs with the planned additive
library extensions and conversion order; README finalized with the
complete matrix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 13:17:20 +02:00
parent 76ecf0e49e
commit 97513141dc
13 changed files with 595 additions and 5 deletions
+25
View File
@@ -0,0 +1,25 @@
# Examples
One runnable program per hard requirement. Each takes `-model` (a full
majordomo spec — single target, chain, or alias) and reads provider keys
from the environment (`OLLAMA_API_KEY`, `OPENAI_API_KEY`,
`ANTHROPIC_API_KEY`, `GOOGLE_API_KEY`, `LLM_*` DSNs).
| Example | Shows |
|---|---|
| `parse/` | resolving a spec and a plain chat |
| `failover/` | failover chains, incl. the trailing-alias form, + health behavior |
| `tiers/` | registering custom tiers (aliases) and parsing them |
| `envproviders/` | `LLM_*` env-DSN providers (foreman) as first-class chain elements |
| `multimodal/` | attaching an image; capability-aware normalization |
| `tools/` | the canonical tool-call loop against the raw Model API |
| `structured/` | typed structured output via `Generate[T]` |
| `agent/` | an agent with a toolbox running the tool-dispatch loop |
| `skillexample/` | attaching the ready-made clock + calc skills to an agent |
| `live/` | the Phase 8 live-validation harness (real Ollama Cloud; needs `.env`) |
Run any of them like:
```bash
OLLAMA_API_KEY=... go run ./examples/parse -model "ollama-cloud/minimax-m3:cloud"
```
+66
View File
@@ -0,0 +1,66 @@
// Command agent demonstrates the agent loop: model + system prompt +
// toolbox, run to completion with step observation.
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"gitea.stevedudenhoeffer.com/steve/majordomo"
"gitea.stevedudenhoeffer.com/steve/majordomo/agent"
)
func main() {
model := flag.String("model", "ollama-cloud/minimax-m3:cloud", "model spec")
flag.Parse()
files := majordomo.NewToolbox("files",
majordomo.Tool{
Name: "list_dir",
Description: "List the files in a directory.",
Parameters: json.RawMessage(`{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}`),
Handler: func(_ context.Context, args json.RawMessage) (any, error) {
var p struct {
Path string `json:"path"`
}
if err := json.Unmarshal(args, &p); err != nil {
return nil, err
}
entries, err := os.ReadDir(p.Path)
if err != nil {
return nil, err
}
names := make([]string, 0, len(entries))
for _, e := range entries {
names = append(names, e.Name())
}
return names, nil
},
},
)
m, err := majordomo.Parse(*model)
if err != nil {
log.Fatalf("parse: %v", err)
}
a := agent.New(m, "You are a filesystem assistant. Use your tools; never invent file names.",
agent.WithToolbox(files),
agent.WithMaxSteps(6),
agent.WithStepObserver(func(s agent.Step) {
for _, call := range s.Response.ToolCalls {
fmt.Printf(" step %d: %s(%s)\n", s.Index, call.Name, call.Arguments)
}
}),
)
res, err := a.Run(context.Background(), "What Go files are in the current directory?")
if err != nil {
log.Fatalf("run: %v", err)
}
fmt.Printf("\n%s\n(%d steps, %d tokens)\n", res.Output, len(res.Steps), res.Usage.Total())
}
+42
View File
@@ -0,0 +1,42 @@
// Command envproviders demonstrates LLM_* env-DSN provider definitions
// (go-llm parity): named providers built entirely from the environment,
// first-class in Parse, chains, and tiers.
//
// export LLM_M1=foreman://test-token-change-me@foreman-m1.orgrimmar.dudenhoeffer.casa
// export LLM_M5=foreman://test-token-change-me@foreman-m5.orgrimmar.dudenhoeffer.casa
// go run ./examples/envproviders
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"gitea.stevedudenhoeffer.com/steve/majordomo"
)
func main() {
spec := flag.String("model", "m5/qwen3:30b,m1/qwen3:30b,thinking", "spec mixing env providers, built-ins, and aliases")
flag.Parse()
if os.Getenv("LLM_M1") == "" && os.Getenv("LLM_M5") == "" {
log.Println("note: no LLM_M1/LLM_M5 set — set foreman DSNs to run this against real daemons")
}
reg := majordomo.New() // eagerly loads every LLM_* var; unknown names also resolve lazily
reg.RegisterAlias("thinking", "ollama-cloud/minimax-m3:cloud")
m, err := reg.Parse(*spec)
if err != nil {
log.Fatalf("parse %q: %v", *spec, err)
}
resp, err := m.Generate(context.Background(), majordomo.Request{
Messages: []majordomo.Message{majordomo.UserText("Which machine are you running on? One sentence.")},
})
if err != nil {
log.Fatalf("generate: %v", err)
}
fmt.Printf("[%s] %s\n", resp.Model, resp.Text())
}
+44
View File
@@ -0,0 +1,44 @@
// Command failover demonstrates ordered failover chains with health
// tracking — including the README's flagship examples: a plain chain and
// the same chain with a registered alias expanded inline at the tail.
package main
import (
"context"
"flag"
"fmt"
"log"
"gitea.stevedudenhoeffer.com/steve/majordomo"
)
func main() {
flag.Parse()
reg := majordomo.New()
reg.RegisterAlias("thinking", "anthropic/opus-4.8,ollama-cloud/minimax-m3:cloud")
// Plain chain: try head-to-tail. One transient blip on a target is
// retried in place; repeated failures bench the target (exponential
// cooldown) and the chain advances.
plain := "ollama-cloud/minimax-m3:cloud,ollama-cloud/kimi-k2.6:cloud,anthropic/opus-4.8"
// Same chain with the registered alias appended: "thinking" expands
// inline as the tail (duplicates are dropped, first occurrence wins).
withAlias := plain + ",thinking"
for _, spec := range []string{plain, withAlias} {
m, err := reg.Parse(spec)
if err != nil {
log.Fatalf("parse %q: %v", spec, err)
}
resp, err := m.Generate(context.Background(), majordomo.Request{
Messages: []majordomo.Message{majordomo.UserText("In one sentence: why do failover chains beat single targets?")},
})
if err != nil {
// The joined error names every target and why it failed.
log.Fatalf("chain exhausted:\n%v", err)
}
fmt.Printf("spec: %s\n served by %s: %s\n\n", spec, resp.Model, resp.Text())
}
}
+47
View File
@@ -0,0 +1,47 @@
// Command multimodal demonstrates capability-aware image input: attach an
// image without knowing the target's limits — majordomo sniffs the real
// format, downscales, re-encodes, and enforces counts/sizes against the
// actual serving target before the request goes out.
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"gitea.stevedudenhoeffer.com/steve/majordomo"
)
func main() {
model := flag.String("model", "ollama-cloud/kimi-k2.6:cloud", "vision-capable model spec")
path := flag.String("image", "", "path to a jpeg/png/gif image (required)")
flag.Parse()
if *path == "" {
log.Fatal("usage: multimodal -image photo.jpg [-model spec]")
}
data, err := os.ReadFile(*path)
if err != nil {
log.Fatalf("read image: %v", err)
}
m, err := majordomo.Parse(*model)
if err != nil {
log.Fatalf("parse: %v", err)
}
// The declared MIME may even be wrong — the media pipeline sniffs the
// bytes and corrects it.
resp, err := m.Generate(context.Background(), majordomo.Request{
Messages: []majordomo.Message{majordomo.UserParts(
majordomo.Text("Describe this image in two sentences."),
majordomo.Image("image/jpeg", data),
)},
})
if err != nil {
log.Fatalf("generate: %v", err)
}
fmt.Printf("[%s] %s\n", resp.Model, resp.Text())
}
+32
View File
@@ -0,0 +1,32 @@
// Command parse demonstrates the resolver: one spec string in, one Model
// out, regardless of whether it names a single target, a chain, or a tier.
package main
import (
"context"
"flag"
"fmt"
"log"
"gitea.stevedudenhoeffer.com/steve/majordomo"
)
func main() {
model := flag.String("model", "ollama-cloud/minimax-m3:cloud", "model spec (provider/model, chain, or alias)")
prompt := flag.String("prompt", "Say hello in five words.", "prompt to send")
flag.Parse()
m, err := majordomo.Parse(*model)
if err != nil {
log.Fatalf("parse %q: %v", *model, err)
}
resp, err := m.Generate(context.Background(), majordomo.Request{
Messages: []majordomo.Message{majordomo.UserText(*prompt)},
})
if err != nil {
log.Fatalf("generate: %v", err)
}
fmt.Printf("[%s] %s\n", resp.Model, resp.Text())
fmt.Printf("usage: %d in / %d out tokens\n", resp.Usage.InputTokens, resp.Usage.OutputTokens)
}
+42
View File
@@ -0,0 +1,42 @@
// Command skillexample demonstrates skills: reusable instruction+tool
// bundles attached to an agent on demand — here the ready-made clock and
// calc skills, which require tool use for time and arithmetic.
package main
import (
"context"
"flag"
"fmt"
"log"
"gitea.stevedudenhoeffer.com/steve/majordomo"
"gitea.stevedudenhoeffer.com/steve/majordomo/agent"
"gitea.stevedudenhoeffer.com/steve/majordomo/skill/calc"
"gitea.stevedudenhoeffer.com/steve/majordomo/skill/clock"
)
func main() {
model := flag.String("model", "ollama-cloud/minimax-m3:cloud", "model spec")
flag.Parse()
m, err := majordomo.Parse(*model)
if err != nil {
log.Fatalf("parse: %v", err)
}
a := agent.New(m, "You are a precise assistant.")
a.AddSkill(clock.New()) // time_now / time_convert + instructions
a.AddSkill(calc.New()) // calculate + instructions
res, err := a.Run(context.Background(),
"How many minutes are left until midnight UTC? Use your tools, then show the calculation.")
if err != nil {
log.Fatalf("run: %v", err)
}
fmt.Println(res.Output)
for _, step := range res.Steps {
for _, r := range step.Results {
fmt.Printf(" tool %s → %s\n", r.Name, r.Content)
}
}
}
+48
View File
@@ -0,0 +1,48 @@
// Command structured demonstrates typed structured output: the JSON schema
// is derived from a Go struct, mapped to the provider's native mechanism,
// and the response is unmarshaled back into the struct.
package main
import (
"context"
"flag"
"fmt"
"log"
"gitea.stevedudenhoeffer.com/steve/majordomo"
)
// Recipe is the shape we want back. Field tags document and constrain the
// schema (`description`, `enum`); pointers mark nullable fields.
type Recipe struct {
Name string `json:"name"`
Servings int `json:"servings"`
Difficulty string `json:"difficulty" enum:"easy,medium,hard"`
Steps []string `json:"steps" description:"short imperative steps"`
WinePair *string `json:"wine_pairing" description:"null if none appropriate"`
}
func main() {
model := flag.String("model", "ollama-cloud/minimax-m3:cloud", "model spec")
flag.Parse()
m, err := majordomo.Parse(*model)
if err != nil {
log.Fatalf("parse: %v", err)
}
recipe, err := majordomo.Generate[Recipe](context.Background(), m, majordomo.Request{
Messages: []majordomo.Message{majordomo.UserText("A weeknight mushroom risotto.")},
})
if err != nil {
log.Fatalf("generate: %v", err)
}
fmt.Printf("%s (serves %d, %s)\n", recipe.Name, recipe.Servings, recipe.Difficulty)
for i, s := range recipe.Steps {
fmt.Printf(" %d. %s\n", i+1, s)
}
if recipe.WinePair != nil {
fmt.Printf("wine: %s\n", *recipe.WinePair)
}
}
+36
View File
@@ -0,0 +1,36 @@
// Command tiers demonstrates custom tiers: register an alias once, then
// address models by task shape ("thinking", "workhorse") instead of
// hard-coding model strings at every call site.
package main
import (
"context"
"flag"
"fmt"
"log"
"gitea.stevedudenhoeffer.com/steve/majordomo"
)
func main() {
flag.Parse()
reg := majordomo.New()
reg.RegisterAlias("thinking", "ollama-cloud/minimax-m3:cloud,ollama-cloud/kimi-k2.6:cloud")
reg.RegisterAlias("workhorse", "ollama-cloud/minimax-m2.7:cloud,ollama-cloud/qwen3-coder:480b-cloud")
// Tiers can reference other tiers; expansion is recursive (with cycle
// detection), so "best-effort" is thinking with a workhorse tail.
reg.RegisterAlias("best-effort", "thinking,workhorse")
m, err := reg.Parse("best-effort")
if err != nil {
log.Fatalf("parse: %v", err)
}
resp, err := m.Generate(context.Background(), majordomo.Request{
Messages: []majordomo.Message{majordomo.UserText("One sentence: what is a model tier?")},
})
if err != nil {
log.Fatalf("generate: %v", err)
}
fmt.Printf("[%s] %s\n", resp.Model, resp.Text())
}
+65
View File
@@ -0,0 +1,65 @@
// Command tools demonstrates the canonical tool-call loop against the raw
// Model API: offer tools, execute the calls the model makes, feed results
// back, repeat until a final answer. (The agent package automates exactly
// this loop; see examples/agent.)
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"gitea.stevedudenhoeffer.com/steve/majordomo"
)
func main() {
model := flag.String("model", "ollama-cloud/minimax-m3:cloud", "model spec")
flag.Parse()
weather := majordomo.Tool{
Name: "get_weather",
Description: "Get the current weather for a city.",
Parameters: json.RawMessage(`{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}`),
Handler: func(_ context.Context, args json.RawMessage) (any, error) {
var p struct {
City string `json:"city"`
}
if err := json.Unmarshal(args, &p); err != nil {
return nil, err
}
// A real implementation would call a weather API.
return map[string]any{"city": p.City, "temp_c": 21, "sky": "partly cloudy"}, nil
},
}
box := majordomo.NewToolbox("demo", weather)
m, err := majordomo.Parse(*model)
if err != nil {
log.Fatalf("parse: %v", err)
}
ctx := context.Background()
msgs := []majordomo.Message{majordomo.UserText("What's the weather in Tokyo right now?")}
for range 5 {
resp, err := m.Generate(ctx, majordomo.Request{Messages: msgs}, majordomo.WithToolbox(box))
if err != nil {
log.Fatalf("generate: %v", err)
}
msgs = append(msgs, resp.Message())
if len(resp.ToolCalls) == 0 {
fmt.Printf("[%s] %s\n", resp.Model, resp.Text())
return
}
results := make([]majordomo.ToolResult, 0, len(resp.ToolCalls))
for _, call := range resp.ToolCalls {
fmt.Printf("→ model called %s(%s)\n", call.Name, call.Arguments)
results = append(results, box.Execute(ctx, call))
}
msgs = append(msgs, majordomo.ToolResultsMessage(results...))
}
log.Fatal("no final answer within 5 steps")
}