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.
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user