Architecture¶
Keen Code is a terminal-based AI coding agent that follows an event-driven, tool-based architecture.
High-Level Overview¶
User Input → REPL (Bubble Tea) → Session → LLM Client → Provider (API)
↓
Tool Registry
↓
Tools (read_file, bash, etc.)
↓
Filesystem Guard (Permission System)
Entry Point¶
cmd/main.go
└── NewRootCommand(version)
└── cobra.Command
└── Runs REPL via repl.RunREPL()
Core Components¶
CLI Command (internal/cli/cmd/root.go)¶
The root command initializes:
1. Provider registry (providers.Load())
2. Config loader (config.NewLoader())
3. Global config (loader.Load())
4. Resolves configuration and starts the REPL
REPL (internal/cli/repl/)¶
The interactive terminal interface built with Bubble Tea:
repl.RunREPL()
└── Bubble Tea application
├── StreamHandler - handles LLM response streaming
├── CommandHandlers - processes slash commands (/btw, /model, /compact, etc.)
├── SessionState - manages session context
└── UI Widgets - renders output, permissions, viewport
Key files:
- repl.go - Main REPL loop and state management
- stream_handler.go - Processes LLM stream events
- command_handlers.go - Slash commands
- session_state.go - Session context management
LLM Layer (internal/llm/)¶
Unified interface for multiple AI providers:
type LLMClient interface {
StreamChat(ctx context.Context, messages []Message, toolRegistry *tools.Registry) (<-chan StreamEvent, error)
Reset()
}
Implementations:
- AnthropicClient - Direct Anthropic SDK integration
- OpenAIResponsesClient - OpenAI Responses API (GPT models)
- OpenAICompatibleClient - OpenAI-compatible API (DeepSeek, Moonshot, Z.ai, OpenCode Go non-MiniMax models)
- GenkitClient - Firebase Genkit for Google AI
MiniMax uses AnthropicClient with https://api.minimax.io/anthropic.
OpenCode Go is routed by model family: minimax-m2.* uses AnthropicClient, while GLM, Kimi, DeepSeek, MiMo, and Qwen models use OpenAICompatibleClient.
Tools (internal/tools/)¶
Built-in tools the LLM can call:
| Tool | File | Purpose |
|---|---|---|
| read_file | read_file.go |
Read file contents |
| write_file | write_file.go |
Create/overwrite files |
| edit_file | edit_file.go |
Targeted string replacement |
| glob | glob.go |
Find files by pattern |
| grep | grep.go |
Search file contents |
| bash | bash.go |
Execute shell commands |
All tools use filesystem.Guard for permission checks and PermissionRequester for user prompts.
Side Questions (/btw)¶
The /btw <question> command starts a one-shot helper stream for quick side questions. It builds a request from the current conversation plus llm.BuildBtwPrompt(), disables tool access, and renders the answer in a separate overlay. The side-question exchange is kept out of the main conversation and session transcript, but overlay history can be reopened with /btw during the current REPL session.
Session (internal/session/)¶
Event-sourced session management:
Sessions stored in ~/.keen/sessions/
└── {namespace}/
└── {session-id}/
└── transcript_events.jsonl
Events:
- session_started - Session initialization
- user_message - User input
- assistant_turn - AI response with transcript
- compaction_applied - Context compaction
Permission System (internal/filesystem/)¶
type Guard struct {
workingDir string
blockedPaths []string
gitignore *GitAwareness
}
Policy:
- Working directory: Granted for reads, Pending for writes
- System paths (/etc, /usr, etc.): Denied
- Gitignored paths: Blocked
- Skills directories: Always allowed
Skills (internal/skills/)¶
Discoverable skill modules with YAML frontmatter:
type Skill struct {
Name string
Description string
Location string
}
Discovery roots:
- <working-dir>/.agents/skills/
- <working-dir>/.keen/skills/
- ~/.agents/skills/
- ~/.keen/skills/
- Bundled (embedded in binary)
Config (internal/config/)¶
Hierarchical configuration:
1. Session config (command-line arguments)
2. Global config (~/.keen/configs.json)
3. Provider defaults
type GlobalConfig struct {
ActiveProvider string
ActiveModel string
ThinkingEffort string
Providers map[string]ProviderConfig
}
Auth (internal/auth/)¶
Authentication management: - API key storage and lookup - OAuth flow for OpenAI Codex (PKCE-based)
Data Flow: User Message to Response¶
- User Input - User types message in REPL
- Command Check - Check if it's a slash command
- Session Append - Append
user_messageevent to session - Build Context - Reconstruct conversation from events using
projection.BuildConversation() - LLM Request - Call
StreamChat()with messages and tool registry - Stream Events - Process events: chunks, tool calls, usage stats
- Tool Execution - Execute tools, emit results back to LLM
- Response Complete - Append
assistant_turnevent with transcript - UI Update - Render response in terminal
Key Interfaces¶
LLMClient¶
// internal/llm/client.go
type LLMClient interface {
StreamChat(ctx context.Context, messages []Message, toolRegistry *tools.Registry) (<-chan StreamEvent, error)
Reset()
}
Tool¶
// internal/tools/tool.go
type Tool interface {
Name() string
Description() string
InputSchema() map[string]any
Execute(ctx context.Context, input any) (any, error)
}
PermissionRequester¶
// internal/tools/permission.go
type PermissionRequester interface {
RequestPermission(ctx context.Context, toolName, path, resolvedPath string, isDangerous bool) (bool, error)
}
DiffEmitter¶
// internal/tools/diff.go
type DiffEmitter interface {
EmitDiff(lines []EditDiffLine)
}
Stream Events¶
LLM clients emit a unified stream of events:
type StreamEvent struct {
Type StreamEventType
Content string // text/reasoning chunks
ToolCall *ToolCall // tool start/end
Usage *TokenUsage // token usage stats
Error error // errors
Attempt int // retry attempt
}
Types:
- StreamEventTypeChunk - Text content
- StreamEventTypeReasoningChunk - Thinking content
- StreamEventTypeToolStart - Tool execution started
- StreamEventTypeToolEnd - Tool execution completed
- StreamEventTypeUsage - Token usage
- StreamEventTypeDone - Response complete
- StreamEventTypeError - Unrecoverable error
- StreamEventTypeRetry - Retrying after error
- StreamEventTypeIncomplete - Turn limit reached
Directory Structure¶
keen-code/
├── cmd/
│ └── main.go # Entry point
├── providers/
│ ├── loader.go # Provider registry
│ └── registry.yaml # Provider/model metadata
├── internal/
│ ├── auth/ # Authentication (OAuth, API keys)
│ ├── cli/
│ │ ├── cmd/ # Cobra commands
│ │ └── repl/ # Bubble Tea REPL
│ ├── config/ # Configuration management
│ ├── filesystem/ # Guard and GitAwareness
│ ├── llm/ # LLM client implementations
│ ├── session/ # Session management
│ ├── skills/ # Skills system
│ └── tools/ # Built-in tools
├── docs/ # Documentation
└── npm/ # npm wrapper package
Dependencies¶
Key external libraries:
- Bubble Tea (github.com/charmbracelet/bubbletea) - TUI framework
- Cobra (github.com/spf13/cobra) - CLI framework
- Anthropic SDK (github.com/anthropics/anthropic-sdk-go) - Claude integration
- OpenAI Go (github.com/openai/openai-go) - OpenAI-compatible APIs
- Genkit (github.com/firebase/genkit/go) - Google AI integration
- go-git (github.com/go-git/go-git/v5) - Gitignore parsing
- go-udiff (github.com/aymanbagabas/go-udiff) - Diff computation