--- essential: domain-model status: complete last_updated: 2026-04-02 project: gnoma depends_on: [vision] --- # Domain Model ## Entity Relationships ```mermaid classDiagram class Session { +id: string +state: SessionState +Send(input) error +Events() chan Event +Cancel() } class Engine { +history: []Message +usage: Usage +Submit(input, callback) Turn +SetProvider(provider) +SetModel(model) } class Message { +Role: Role +Content: []Content +HasToolCalls() bool +ToolCalls() []ToolCall +TextContent() string } class Content { +Type: ContentType +Text: string +ToolCall: ToolCall +ToolResult: ToolResult +Thinking: Thinking } class Provider { <> +Stream(req) Stream +Name() string } class Stream { <> +Next() bool +Current() Event +Err() error +Close() error } class Tool { <> +Name() string +Execute(args) Result +IsReadOnly() bool } class Turn { +Messages: []Message +Usage: Usage +Rounds: int } class Elf { <> +ID() string +Send(msg) error +Events() chan Event +Wait() ElfResult } Session "1" --> "1" Engine : owns Engine "1" --> "1" Provider : uses Engine "1" --> "*" Tool : executes Engine "1" --> "*" Message : history Engine "1" --> "*" Turn : produces Message "1" --> "*" Content : contains Provider "1" --> "*" Stream : creates Stream "1" --> "*" Event : yields Session "1" --> "*" Elf : spawns (future) Elf "1" --> "1" Engine : owns ``` ## Glossary | Term | Definition | Example | |------|-----------|---------| | gnoma | The host application — single binary, agentic coding assistant | `gnoma "list files"` | | Elf | A sub-agent (goroutine) with its own engine, history, and provider. Named after the elf owl. | Background elf exploring `auth/` on Ollama | | Session | A conversation boundary between UI and engine. Owns one engine, communicates via channels. | TUI session, CLI pipe session | | Engine | The agentic loop orchestrator. Manages history, streams from provider, executes tools, loops until done. | Engine running on Mistral with 5 tools | | Provider | An LLM backend adapter. Translates gnoma types to/from SDK-specific types. | Anthropic provider, OpenAI-compat provider | | Stream | Pull-based iterator over streaming events from a provider. Unified interface across all SDKs. | `for s.Next() { e := s.Current() }` | | Event | A single streaming delta — text chunk, tool call fragment, thinking trace, or usage update. | `EventTextDelta{Text: "hello"}` | | Message | A single turn in conversation history. Contains one or more Content blocks. | User text message, assistant message with tool calls | | Content | A discriminated union within a Message — text, tool call, tool result, or thinking block. | `Content{Type: ContentToolCall, ToolCall: &ToolCall{...}}` | | ToolCall | The model's request to invoke a tool, with ID, name, and JSON arguments. | `{ID: "tc_1", Name: "bash", Args: {"command": "ls"}}` | | ToolResult | The output of executing a tool, correlated to a ToolCall by ID. | `{ToolCallID: "tc_1", Content: "file1.go\nfile2.go"}` | | Turn | The result of a complete agentic loop — may span multiple API calls and tool executions. | Turn with 3 rounds: stream → tool → stream → tool → stream → done | | Accumulator | Assembles a complete Response from a sequence of streaming Events. Shared across all providers. | Text fragments → complete assistant message | | Callback | Function the engine calls for each streaming event, enabling real-time UI updates. | `func(evt stream.Event) { ch <- evt }` | | Round | A single API call within a Turn. A turn with 2 tool-use loops has 3 rounds. | Round 1: initial query. Round 2: after tool results. | | Routing | Directing tasks to different providers based on capability, cost, or latency rules. | Complex reasoning → Claude, quick lookups → local Qwen | ## Invariants Rules that must always hold true in the domain: - A Message always has at least one Content block - A ToolResult always references a ToolCall.ID from the preceding assistant message - A Session owns exactly one Engine; an Engine is owned by exactly one Session - An Elf owns its own Engine — no shared mutable state between elfs - The Accumulator produces exactly one Response per stream consumption - Content.Type determines which payload field is set — exactly one is non-nil - Thinking.Signature must round-trip unchanged through message history (Anthropic requirement) - Tool execution only happens when StopReason == ToolUse - Stream.Close() must be called after consumption, regardless of error state - Provider.Stream() is the only network boundary — all tool execution is local ## Changelog - 2026-04-02: Initial version