Files
gnoma/docs/essentials/patterns.md
vikingowl efcb5a2901 docs: add project essentials (12/12 complete)
Vision, domain model, architecture, patterns, process flows,
UML diagrams, API contracts, tech stack, constraints, milestones
(M1-M11), decision log (6 ADRs), and risk register.

Key decisions: single binary, pull-based streaming, Mistral as M1
reference provider, discriminated unions, multi-provider collaboration
as core identity.
2026-04-02 18:09:07 +02:00

136 lines
4.8 KiB
Markdown

---
essential: patterns
status: complete
last_updated: 2026-04-02
project: gnoma
depends_on: [architecture]
---
# Patterns
## Discriminated Unions
- **What:** Struct with a `Type` field discriminant; exactly one payload field is set per type value. Used instead of Go interfaces for data variants.
- **Where:** `message.Content`, `stream.Event`
- **Why:** Zero allocation (no interface boxing), cache-friendly, works with `switch` statements. Go lacks sum types — this is the pragmatic equivalent.
- **Example:**
```go
type Content struct {
Type ContentType
Text string // set when Type == ContentText
ToolCall *ToolCall // set when Type == ContentToolCall
ToolResult *ToolResult // set when Type == ContentToolResult
Thinking *Thinking // set when Type == ContentThinking
}
switch c.Type {
case ContentText:
fmt.Print(c.Text)
case ContentToolCall:
execute(c.ToolCall)
}
```
## Pull-Based Stream Iterator
- **What:** `Next() / Current() / Err() / Close()` interface for consuming streaming data.
- **Where:** `stream.Stream` interface, all provider adapters
- **Why:** Matches 3 of 4 SDKs (Anthropic, OpenAI, Mistral) natively. Gives consumer explicit backpressure control. Supports `Close()` for resource cleanup, unlike `iter.Seq`. Only Google needs a goroutine bridge.
- **Example:**
```go
for s.Next() {
event := s.Current()
process(event)
}
if err := s.Err(); err != nil {
handle(err)
}
s.Close()
```
## Accumulator
- **What:** Shared component that assembles a `message.Response` from a sequence of `stream.Event` values. Separated from provider-specific translation.
- **Where:** `stream.Accumulator`, used by every provider adapter
- **Why:** Provider adapters become thin translation layers. Accumulation logic (text building, tool call JSON fragment assembly, thinking blocks) is tested once, not per-provider.
- **Example:**
```go
acc := stream.NewAccumulator()
for s.Next() {
acc.Apply(s.Current())
}
response := acc.Response()
```
## Factory Registry
- **What:** Map of names to factory functions. Creates instances on demand with config.
- **Where:** `provider.Registry`, `tool.Registry`
- **Why:** Decouples creation from usage. Makes testing easy — register mock factories. Enables dynamic provider switching.
- **Example:**
```go
registry.Register("mistral", mistral.NewProvider)
provider, err := registry.Create("mistral", cfg)
```
## Functional Options
- **What:** Variadic option functions for configuring complex objects.
- **Where:** Session creation, provider construction
- **Why:** Clean API for objects with many optional parameters. Self-documenting, extensible without breaking changes.
- **Example:**
```go
session, err := manager.NewSession(
WithProvider(mistral),
WithModel("mistral-large-latest"),
WithMaxTurns(20),
)
```
## Callback Event Propagation
- **What:** The engine accepts a `Callback func(stream.Event)` and calls it for each event. The session wraps this to push events into a channel.
- **Where:** `engine.Submit()` → `session/local.go`
- **Why:** Keeps the engine testable without concurrency. The engine knows nothing about channels, TUI, or goroutines. The session implementation decides how to propagate events.
- **Example:**
```go
// In session/local.go:
cb := func(evt stream.Event) {
select {
case s.events <- evt:
case <-ctx.Done():
}
}
turn, err := s.engine.Submit(ctx, input, cb)
```
## Error Wrapping with errors.AsType
- **What:** Provider adapters wrap SDK errors into typed `ProviderError` with classification. Consumers extract using Go 1.26's `errors.AsType[E]`.
- **Where:** All provider adapters, retry logic, engine error handling
- **Why:** Enables error classification (transient vs auth vs bad request) for retry decisions. Type-safe extraction without pointer indirection.
- **Example:**
```go
if pErr, ok := errors.AsType[*ProviderError](err); ok {
if pErr.Retryable {
// exponential backoff
}
}
```
## Anti-Patterns
Patterns explicitly avoided in this project:
| Anti-Pattern | Why we avoid it | What to do instead |
|---|---|---|
| Interface-based unions | Heap allocation, type assertion overhead, no exhaustive matching | Discriminated union structs with Type field |
| Channel-based streams | Requires goroutine management, harder to control backpressure | Pull-based iterator interface |
| Global state | Untestable, race-prone, hidden dependencies | Dependency injection via config structs |
| Shared mutable state between elfs | Race conditions, complex synchronization | Each elf owns its own engine; communicate via channels |
| Over-abstraction | Premature generalization obscures intent | Three similar lines > one premature abstraction |
## Changelog
- 2026-04-02: Initial version