Skip to content

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

  1. User Input - User types message in REPL
  2. Command Check - Check if it's a slash command
  3. Session Append - Append user_message event to session
  4. Build Context - Reconstruct conversation from events using projection.BuildConversation()
  5. LLM Request - Call StreamChat() with messages and tool registry
  6. Stream Events - Process events: chunks, tool calls, usage stats
  7. Tool Execution - Execute tools, emit results back to LLM
  8. Response Complete - Append assistant_turn event with transcript
  9. 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