feat: agent run loop, Generate[T], reflect-derived schemas

Phase 5:
- agent/: model + system prompt + toolboxes composition; bounded
  tool-dispatch loop (default 10 steps); panic-proof tool execution;
  unknown-tool and duplicate-name handling; history continuation; step
  observers; partial results on ErrMaxSteps/errors (ADR-0012)
- llm.SchemaFor[T]: strict-compatible JSON schemas from Go types
  (nullable pointers, description/enum tags, recursion rejected)
- majordomo.Generate[T]: typed structured output with fence-stripping
  decode and model-naming errors
- README agents/structured-output sections + matrix synced

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 13:10:18 +02:00
parent 1ca607906d
commit 7dab4112ff
10 changed files with 1211 additions and 7 deletions
+40 -7
View File
@@ -200,13 +200,46 @@ resp, _ := m.Generate(ctx, req, majordomo.WithSchema(schemaJSON, "answer"))
```
Maps to OpenAI `response_format: json_schema`, Anthropic
`output_config.format`, and Ollama `format`. A generic `Generate[T]` helper
(schema from your struct, unmarshal into it) lands with the agent phase.
`output_config.format`, Ollama `format`, and Google `responseJsonSchema`.
## Agents & skills *(pending — Phases 56)*
The typed helper derives the schema from your struct (all fields required,
`additionalProperties:false`, pointers nullable; `description:"..."` and
`enum:"a,b,c"` tags supported) and unmarshals the result:
Agents = model + system prompt + toolboxes, running a tool-dispatch loop;
skills = reusable instruction+tool bundles attachable to any agent.
```go
type Verdict struct {
Guilty bool `json:"guilty"`
Why string `json:"why" description:"one-sentence rationale"`
}
v, err := majordomo.Generate[Verdict](ctx, m, req)
```
## Agents
An agent is a model + system prompt + toolboxes, run as a tool-dispatch
loop until the model answers (or `MaxSteps`):
```go
import "gitea.stevedudenhoeffer.com/steve/majordomo/agent"
a := agent.New(m, "You are a research assistant.",
agent.WithToolbox(searchTools),
agent.WithMaxSteps(8),
agent.WithStepObserver(func(s agent.Step) { log.Printf("step %d", s.Index) }),
)
res, err := a.Run(ctx, "What changed in Go 1.26?")
// res.Output, res.Steps, res.Usage; res.Messages round-trips via
// agent.WithHistory for conversation continuation.
```
The loop never panics: tool handler errors and panics become error results
the model can react to; unknown tools likewise; duplicate tool names across
toolboxes fail loudly. On `agent.ErrMaxSteps` (and on model errors) the
partial result with the full transcript is still returned.
## Skills *(pending — Phase 6)*
Skills = reusable instruction+tool bundles attachable to any agent.
## Feature/provider support matrix
@@ -229,8 +262,8 @@ Notes: Ollama has no native tool_choice — `"none"` drops the tools;
Cross-cutting: Parse grammar ✅ · aliases/tiers ✅ · failover chains ✅ ·
health tracking/backoff ✅ · LLM_* env DSNs ✅ · media pipeline ✅
(per-target normalization in chains) · agent loop pending · skills pending
· `Generate[T]` pending.
(per-target normalization in chains) · agent loop ✅ · `Generate[T]` +
schema derivation ✅ · skills pending.
## Development