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.
4.8 KiB
4.8 KiB
essential, status, last_updated, project, depends_on
| essential | status | last_updated | project | depends_on | |
|---|---|---|---|---|---|
| patterns | complete | 2026-04-02 | gnoma |
|
Patterns
Discriminated Unions
- What: Struct with a
Typefield 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
switchstatements. Go lacks sum types — this is the pragmatic equivalent. - Example:
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.Streaminterface, all provider adapters - Why: Matches 3 of 4 SDKs (Anthropic, OpenAI, Mistral) natively. Gives consumer explicit backpressure control. Supports
Close()for resource cleanup, unlikeiter.Seq. Only Google needs a goroutine bridge. - Example:
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.Responsefrom a sequence ofstream.Eventvalues. 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:
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:
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:
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:
// 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
ProviderErrorwith classification. Consumers extract using Go 1.26'serrors.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:
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