ADR-008: Functional Options Pattern for Agents
Status: Accepted
Date: 2026-05-09
Context
Each of Embercore’s five agents requires configuration: which LLM provider to use, which model, whether to use Hestia for persistence, and agent-specific settings. We needed a pattern that is:
- Consistent across all agents
- Extensible — new options can be added without breaking existing call sites
- Self-documenting — option names describe what they configure
- Go-idiomatic — follows established community patterns
Alternatives considered:
- Config struct parameter —
NewApollo(config ApolloConfig). Simple, but adding a field requires updating all callers. Doesn’t distinguish “not set” from “zero value.” - Builder pattern —
NewApollo().WithProvider(p).WithModel(m).Build(). Verbose, requires a separate builder type. - Functional options —
NewApollo(WithProvider(p), WithModel(m)). Extensible, self-documenting, idiomatic Go.
Decision
All agents use the functional options pattern with a consistent set of option functions.
Pattern implementation
Each agent defines an option type and constructor:
// agents/apollo/apollo.go
type Option func(*Apollo)
func WithProvider(p provider.Provider) Option {
return func(a *Apollo) { a.provider = p }
}
func WithHestia(h *hestia.Hestia) Option {
return func(a *Apollo) { a.hestia = h }
}
func WithModel(model string) Option {
return func(a *Apollo) { a.model = model }
}
func New(opts ...Option) *Apollo {
a := &Apollo{ /* defaults */ }
for _, opt := range opts {
opt(a)
}
return a
}
Common options across agents
| Option | Purpose | Used by |
|---|---|---|
WithProvider(p) |
Set the LLM provider | All agents that call LLMs |
WithHestia(h) |
Enable persistence | Athena, Hermes, Hephaestus, Apollo |
WithModel(m) |
Override the default model | Apollo, Hephaestus |
WithRunID(id) |
Associate with a specific run | Hephaestus |
WithCheckpointHandler(h) |
Set checkpoint callback | Hermes |
Agent-specific options
- Hermes:
WithCheckpointHandler(fn)— sets the callback invoked at checkpoint steps - Hephaestus:
WithRunID(id)— links build artifacts to a specific execution run
Usage examples
// Minimal — just a provider
athena := athena.New(athena.WithProvider(p))
// Full configuration
hermes := hermes.New(
hermes.WithProvider(p),
hermes.WithHestia(h),
hermes.WithCheckpointHandler(interactiveApproval),
)
// With model override for local provider
apollo := apollo.New(
apollo.WithProvider(ollamaProvider),
apollo.WithModel("llama3:70b"),
apollo.WithHestia(h),
)
Consequences
Benefits:
- Adding a new option is a non-breaking change — existing callers don’t need to update
- Options are self-documenting:
WithProvider,WithHestia,WithModelread naturally - Consistent pattern across all five agents reduces cognitive load for contributors
- Easy to set sensible defaults and override only what’s needed
- Pairs well with the BYOK provider pattern (ADR-003) — provider selection is injected, not hard-coded
Trade-offs:
- Each agent defines its own
Optiontype — not shared across agents (intentional: agent-specific options likeWithRunIDdon’t apply everywhere) - Compile-time type safety is weaker than a struct — invalid option combinations aren’t caught until runtime
- Discoverability requires reading source or docs — IDE autocomplete shows function names but not which agent they belong to
Related decisions: