Files
gnoma/docs/essentials/constraints.md
T
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

3.6 KiB

essential, status, last_updated, project, depends_on
essential status last_updated project depends_on
constraints complete 2026-04-02 gnoma
domain-model

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.Seq range 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, ToolCallContent implementing Content)
  • 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