efcb5a2901
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.
3.6 KiB
3.6 KiB
essential, status, last_updated, project, depends_on
| essential | status | last_updated | project | depends_on | |
|---|---|---|---|---|---|
| constraints | complete | 2026-04-02 | gnoma |
|
Constraints & Trade-offs
Non-Functional Requirements
| Constraint | Target | Measurement |
|---|---|---|
| First token latency | Dominated by provider, not gnoma overhead | Time from Submit() to first EventTextDelta |
| Binary size | < 20 MB (static, no CGO) | ls -lh bin/gnoma |
| Memory per session | < 50 MB baseline (excluding context window) | runtime.MemStats |
| Startup time | < 200ms to TUI ready | Wall clock from exec to first render |
| Provider support | 5+ providers from M2 | Count of passing provider integration tests |
| Context window | Up to 200k tokens managed | Token tracker reports |
Trade-offs
Single binary over daemon architecture
- Chose: Single Go binary, goroutines + channels for all communication
- Over: Client-server split with gRPC IPC (gnoma + gnomad)
- Because: Simpler deployment, no daemon lifecycle, no protobuf codegen. Go's goroutine model provides sufficient isolation.
- Consequence: True process isolation for tool sandboxing requires future work. Multi-client scenarios (IDE + TUI) need serve mode added later.
Pull-based stream over channels or iter.Seq
- Chose:
Next() / Current() / Err() / Close()interface - Over: Channel-based streaming or Go 1.23+
iter.Seqrange functions - Because: Matches 3 of 4 SDKs natively (zero-overhead adapter). Supports explicit resource cleanup via
Close(). Consumer controls backpressure. - Consequence: Google's range-based SDK needs a goroutine bridge. Slightly more verbose than range-based iteration.
json.RawMessage passthrough over typed schemas
- Chose: Tool parameters and arguments as
json.RawMessage - Over: Typed JSON Schema library or code-generated types
- Because: Zero-cost passthrough — no serialize/deserialize between provider and tool. No JSON Schema library as a core dependency.
- Consequence: Schema validation happens at tool boundaries, not centrally. Type safety relies on tool implementations parsing their own args.
Sequential tool execution (MVP) over parallel
- Chose: Execute tools one at a time in the agentic loop
- Over: Parallel execution via errgroup with read/write partitioning
- Because: Simpler to test, debug, and implement permission prompts. Parallel execution adds complexity around error collection and ordering.
- Consequence: Multiple tool calls in a single turn execute sequentially. Performance impact is minimal for most workloads. Parallel execution planned for post-MVP.
Discriminated union structs over interface hierarchies
- Chose: Struct with Type discriminant field for Content and Event types
- Over: Interface-based variant types (e.g.,
TextContent,ToolCallContentimplementingContent) - Because: Zero allocation, cache-friendly, works with switch exhaustiveness. Go interfaces for data variants incur boxing overhead.
- Consequence: Adding a new content type requires updating switch statements. Acceptable for a small, stable set of variants.
Mistral as M1 reference provider over Anthropic
- Chose: Implement Mistral adapter first as the reference
- Over: Starting with Anthropic (richest content model)
- Because: User maintains the Mistral Go SDK, knows its internals. Good baseline — similar to OpenAI's API shape. Anthropic's unique features (thinking blocks, cache tokens) are better added as an M2 extension.
- Consequence: Thinking block support tested later. Cache token tracking added with Anthropic provider.
Changelog
- 2026-04-02: Initial version