Fantasy's hardcoded responsesModelIDs list gates whether a model uses the Responses API or Chat Completions code path. When a new model (e.g. gpt-5.5) is added via `kit update-models` but fantasy hasn't been updated yet, the type mismatch between *ResponsesProviderOptions and *ProviderOptions causes a crash. - Add isResponsesAPIModel()/isResponsesReasoningModel() helpers that supplement fantasy's checks with prefix-based heuristics for modern OpenAI model families (gpt-4.1+, gpt-5+, o-series, codex, chatgpt) - Add RegisterResponsesModels() using go:linkname to append missing model IDs from our database into fantasy's internal slices at init time and after ReloadGlobalRegistry() - Replace all direct openai.IsResponsesModel/IsResponsesReasoningModel calls in providers.go with the new helpers - Merge embedded + cached model databases instead of cache-only fallback - Bump fantasy v0.19.0 -> v0.20.0 to match existing import usage - Document the technique and model-family update process in AGENTS.md
7.9 KiB
KIT Agent Guidelines
Build/Test Commands
- Build:
go build -o output/kit ./cmd/kit - Test all:
go test -race ./... - Test single:
go test -race ./cmd -run TestScriptExecution - Lint:
go vet ./... - Format:
go fmt ./...
Code Style
- Imports: stdlib → third-party → local (blank lines between)
- Naming: camelCase (unexported), PascalCase (exported)
- Errors: Always check, wrap with
fmt.Errorf("context: %w", err) - Logging: Use
github.com/charmbracelet/logstructured logging - Types: Prefer
anyoverinterface{} - JSON: snake_case tags with
omitemptywhere appropriate - Context: First parameter for blocking operations
Architecture
- Multi-provider LLM support via
llm.Providerinterface - MCP client-server for tool integration
- Builtin servers: bash, fetch, todo, fs
- Extension system (
internal/extensions/): Yaegi-interpreted Go, 13 lifecycle events, custom tools/commands/widgets/overlays/editor interceptors - TUI (
internal/ui/): Bubble Tea v2 parent-child model (AppModel→InputComponent,StreamComponent, etc.) - Decoupling pattern:
cmd/root.gohas converter functions (e.g.widgetProviderForUI()) that bridgeinternal/extensions/types tointernal/ui/types — the UI never imports extensions directly - Public SDK (
pkg/kit/): The public-facing Go SDK for embedding Kit as a library. See rules below.
Public SDK (pkg/kit/) Rules
pkg/kit/ is the public API surface consumed by external Go developers. All exported symbols, types, function names, and godoc comments in this package are part of the SDK contract.
No Dependency Name Leakage
Internal dependency names (e.g. charm.land/fantasy, library-specific jargon) must not appear in:
- Exported function/method names — use generic terms (
LLM,Provider,Message) instead of library names - Exported type names — type aliases should use domain names (e.g.
LLMMessage, notFantasyMessage) - Godoc comments on exported symbols — these are visible in
go docoutput and pkg.go.dev - Struct field names and tags on exported types
Using dependency types directly in function bodies (private implementation) is fine — that's invisible to SDK consumers.
Naming Conventions for SDK Symbols
- Type aliases re-exporting dependency types: use
LLM*prefix (e.g.LLMMessage,LLMUsage,LLMResponse) - Conversion helpers: use
ConvertToLLM*/ConvertFromLLM*(not the dependency name) - Provider queries: use
GetLLMProviders(notGetFantasyProviders) - When wrapping internal methods, the
pkg/kit/name should be dependency-agnostic even if theinternal/method still uses the old name
Deprecation Pattern
When renaming a public SDK symbol, keep the old name as a deprecated wrapper for one release cycle:
// Deprecated: Use NewName instead.
func OldName() { return NewName() }
Key Patterns
Yaegi (Extension Interpreter) Gotchas
- No interfaces across boundary: All extension-facing API types must be concrete structs, never interfaces. Yaegi crashes on interface wrapper generation.
- Function field bug: Named function references assigned to struct fields return zero values across the interpreter boundary. Always use anonymous closure literals:
// WRONG: ctx.SetEditor(ext.EditorConfig{HandleKey: myHandler}) // RIGHT: ctx.SetEditor(ext.EditorConfig{HandleKey: func(k, t string) ext.EditorKeyAction { return myHandler(k, t) }}) - Symbol exports: Every new type exposed to extensions must be added to
internal/extensions/symbols.go
BubbleTea Integration
- No
prog.Send()from insideUpdate(): Callingprog.Send()synchronously within a BubbleTeaUpdate()handler deadlocks the event loop. Usego appInstance.NotifyWidgetUpdate()(async goroutine) instead. - Height measurement:
distributeHeight()inmodel.gomust measure using the same render path asView(). If an interceptor wraps rendering, measure with the wrapper too, or layout will mismatch. - Channel-based prompts: Extension prompt calls (PromptSelect, etc.) block on a
chan PromptResponse. Extension slash commands run in dedicated goroutines (nottea.Cmd) to avoid stalling BubbleTea's Cmd scheduler.
Extension State Management
- Thread-safe maps on Runner: Widget/header/footer/editor state lives on the Runner with
sync.RWMutex, queried by UI via callbacks - Context function fields: The
Contextstruct uses function fields (Print func(string),SetWidget func(WidgetConfig)) wired by closures incmd/root.go - Package-level vars in extensions: Yaegi supports package-level variables captured in closures — this is how extensions maintain state across event callbacks
OpenAI Responses API Model Registration
Fantasy's OpenAI provider routes models through either the Responses API or Chat Completions based on a hardcoded responsesModelIDs list. When OpenAI releases a new model (e.g. gpt-5.5) and it's added to our database via kit update-models, fantasy may not know about it yet, causing a type-mismatch crash (*ResponsesProviderOptions vs *ProviderOptions).
How we handle it (internal/models/responses_models.go):
isResponsesAPIModel()/isResponsesReasoningModel()— supplement fantasy's checks with prefix-based heuristics (gpt-4.1+,gpt-5+,o1/o3/o4,codex,chatgpt-)RegisterResponsesModels()— uses//go:linknameto append new model IDs from our database into fantasy's unexportedresponsesModelIDs/responsesReasoningModelIDsslices at init time and afterReloadGlobalRegistry()- All call sites in
providers.gouse our helpers (isResponsesAPIModel,isResponsesReasoningModel) instead ofopenai.IsResponsesModel/openai.IsResponsesReasoningModeldirectly
To add a brand-new OpenAI model family:
- If the model ID starts with an existing prefix in
isResponsesAPIModel(), it works automatically - If it's a new prefix (e.g.
o5-*), add it to the prefix lists in bothisResponsesAPIModel()and (if reasoning)isResponsesReasoningModel()ininternal/models/providers.go - Run
kit update-modelsto pull the model metadata —RegisterResponsesModels()handles the rest - Tests:
internal/models/responses_models_test.go
Unicode in Widget Text
- Widget content renders through
lipgloss.Style.Render()which preserves ANSI escape codes - Use rune-based width calculations (
len([]rune(s))) not byte length (len(s)) when aligning box-drawing characters or multi-byte symbols
Testing
Interactive TUI Testing with tmux
Use tmux to test Kit interactively without blocking the agent:
tmux new-session -d -s kittest -x 120 -y 40 "output/kit -e examples/extensions/my-ext.go --no-session 2>kit_stderr.log"
sleep 3
tmux capture-pane -t kittest -p # read screen
tmux send-keys -t kittest '/command' Enter # send input
tmux kill-session -t kittest # cleanup
Non-Interactive Kit (Subprocess Spawning)
Extensions can spawn Kit as a subprocess for sub-agent patterns:
kit --quiet --no-session --no-extensions --system-prompt /path/to/prompt.txt --model provider/model "question"
Positional args are the prompt. @file args attach file content. Key flags: --quiet (stdout only, no TUI), --no-session (ephemeral), --no-extensions (prevent recursive loading), --system-prompt (string or file path).
External Repo Research
- ALWAYS use
btcato search external repos (e.g. iteratr, other reference codebases) - Never guess or manually search the filesystem for external projects
- Example:
btca ask -r https://github.com/user/repo -q "How does X work?" - See
.agents/skills/btca-cli/SKILL.mdfor full btca usage
BTCA Configured Resources
The following external repositories are configured in btca.config.jsonc for research:
- bubbletea
- lipgloss
- bubbles
- glamour
- fantasy
- catwalk
- crush
- pi
- iteratr
- yaegi
- acp-go-sdk
- opencode
- herald
- herald-md