ESA is a single-package Go CLI tool (package main) for personalized AI agents.
All source files live at the repository root -- there are no sub-packages.
Requires Go 1.23.6+ (toolchain go1.24.3 per go.mod).
# Build
go build
# Run all tests
go test -v ./...
# Run a single test function
go test -v -run TestFunctionName
# Run a specific subtest
go test -v -run TestValidateAgent/valid_agent
# Run multiple tests by regex
go test -v -run "TestAgent|TestParseModel"
# Install dependencies
go mod download
# Lint (standard Go tools only, no golangci-lint configured)
gofmt -l .
go vet ./...
# Run the binary
./esa "your prompt"
./esa +agent_name "your prompt"
./esa --serve --port 8080 # web server mode
./esa --repl # interactive REPL
./esa --debug "prompt" # debug LLM requests/responses
./esa --show-commands "prompt"
./esa --show-tool-calls "prompt"
./esa -c "follow up" # continue last conversation
./esa -C session-id "prompt" # continue specific conversation
./esa -r # retry last conversationCI runs go test -v ./... on push/PR to master. No race detection or coverage in CI.
| File | Purpose |
|---|---|
main.go |
Entry point -- creates and executes Cobra root command |
cli.go |
CLI flags (CLIOptions struct), Cobra command setup |
agent.go |
Agent TOML loading, validation, struct definitions |
agent_util.go |
Agent string parsing (+name, paths, builtins) |
application.go |
Core orchestrator: conversation flow, LLM streaming, tool execution |
llm.go |
LLM provider abstraction (LLMClient, LLMStream interfaces) and OpenAI wrapper |
anthropic.go |
Native Anthropic Messages API client with SSE streaming |
client.go |
LLM client instantiation: routes to Anthropic or OpenAI-compatible providers |
config.go |
Global config at ~/.config/esa/config.toml |
function.go |
Function execution: JSON arg parsing, template substitution, shell exec |
print.go |
Output formatting (text, markdown, JSON, HTML) |
repl.go |
Interactive REPL with /help, /model, /agent, /editor, /config commands |
server.go |
HTTP + WebSocket web server with embedded UI |
stats.go |
Usage statistics collection and display |
utils.go |
Shared utilities (path expansion, history files, providers) |
builtins.go |
//go:embed for builtin agent TOML files |
web_embed.go |
//go:embed for web UI assets |
builtins/ |
Embedded agent configs: default.toml, new.toml, auto.toml |
examples/ |
Sample agent TOML configurations |
docs/ |
Detailed documentation (agent creation) |
web/ |
Embedded web interface (HTML/JS/CSS) |
- Tabs for indentation (standard
gofmt) - No explicit line length limit
- Run
gofmtbefore committing
Two groups separated by a blank line -- standard library first, then third-party:
import (
"fmt"
"os"
"strings"
"github.com/fatih/color"
"github.com/spf13/cobra"
)No third group needed since everything is package main.
- Exported constants: PascalCase with descriptive prefixes (
DefaultConfigPath,DefaultAgentsDir) - Unexported constants: camelCase (
historyTimeFormat,defaultModel,maxRetryCount) - Error message constants: camelCase with
errprefix (errFailedToLoadConfig) - Types/Structs: PascalCase nouns; config types end with
Config(FunctionConfig,ProviderConfig), info types withInfo, stats types withStats - Unexported types: camelCase (
providerInfo,webSession) - Exported functions: PascalCase; constructors use
Newprefix (NewApplication,NewAgent) - Unexported functions: camelCase; use prefixes:
handlefor handlers,printfor output,setupfor initialization,is/needsfor boolean checks - Method receivers: Short names --
app,c,m,s,sc - Struct fields: PascalCase for serialized fields with
toml:"name"orjson:"name"tags; camelCase for internal fields (baseURL,apiKeyEnvar) - Files: lowercase, underscores for multi-word (
agent_util.go)
- Return
erroras the last return value - Check errors immediately, return early:
if err != nil { return nil, fmt.Errorf("failed to load agent %s: %w", name, err) }
- Wrap errors with
fmt.Errorf("context: %w", err)using%wverb - Define error message constants for repeated strings:
const errFailedToLoadConfig = "failed to load global config"
- Custom error types for structured errors (
CacheErrorwithOperation,Path,Errfields) log.Fatalffor unrecoverable runtime errors;os.Exit(1)only inmain.go- Silent
continueacceptable in non-critical loops (stats processing, file iteration)
- Constructors return pointers:
func NewApplication(opts *CLIOptions) (*Application, error) - Resource-allocating functions return cleanup funcs:
func (app *Application) initializeRuntime() (cleanup func(), err error) { ... } // caller: defer cleanup()
- All struct methods use pointer receivers (no value receivers)
- Mutex-protected structs use
Internalsuffix for lock-holder methods:func (c *Client) Stop() error { c.mu.Lock(); defer c.mu.Unlock(); c.stopInternal() } func (c *Client) stopInternal() { ... }
- Exported items: doc comments starting with the name (
// LoadConfig loads the configuration...) - Inline comments for non-obvious logic
- Section comments in long functions:
// --- Print Header --- TODO:andFIXME:with context for known issues
All tests are in package main at root. No test sub-packages, no testdata/ directory.
- Table-driven tests with
t.Run()subtests (strongly preferred):tests := []struct { name string input string want string wantErr bool }{ ... } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ... }) }
- Test slice always named
tests, iteration variable alwaystt - First struct field is always
namefor subtest identification - Expected values use
wantprefix (wantProvider,wantErr,wantIndex) - No assertion library -- use
t.Errorffor failures,t.Fatalffor setup errors:if got != tt.want { t.Errorf("funcName() = %v, want %v", got, tt.want) }
t.TempDir()for temporary directories (preferred over manualos.MkdirTemp)t.Setenv()for environment variables (auto-cleanup)- Save/restore global state with
deferwhen modifying package-level vars - All test data is inline (TOML strings, struct literals) -- no fixture files
| Package | Purpose |
|---|---|
github.com/spf13/cobra |
CLI framework |
github.com/BurntSushi/toml |
TOML config parsing |
github.com/sashabaranov/go-openai |
OpenAI-compatible LLM client |
github.com/charmbracelet/glamour |
Terminal markdown rendering |
github.com/fatih/color |
Terminal color output |
github.com/gorilla/websocket |
WebSocket for web server |
golang.org/x/term |
Terminal raw mode input |
- Add field to
CLIOptionsstruct incli.go - Register the flag in
createRootCommand() - Handle in the main
RunEfunction - Write table-driven tests following existing patterns