This document captures the architectural direction for librecode after the initial extension-runtime work.
It is intentionally opinionated. The purpose is to keep future work aligned around a small runtime kernel instead of accidentally rebuilding a fixed chat application through a growing set of product-specific APIs.
Related docs:
docs/adr/0001-programmable-runtime.mddocs/extension-runtime.mddocs/extension-api.mddocs/extension-roadmap.mddocs/rendering-boundary.md
librecode should become a programmable terminal runtime.
The default AI chat UI is the bundled product built on top of that runtime, not the core identity of the runtime itself.
A useful shorthand:
Go is the product core and runtime kernel. Extensions are optional control layers. Lua is the first runtime adapter.
Go provides the polished default chat experience plus sharp primitives for user customization. Runtime adapters compose those primitives for keymaps, commands, hooks, small overlays, prompt/context tweaks, and custom workflows. Lua is currently the built-in adapter; future adapters may support shell hooks, toolbox executables, MCP, or experimental Go-like extension runtimes.
Important rendering nuance: extension runtimes are useful for control and customization, but Go remains the default UI implementation and fast rendering backend. Complex hot renderers should stay Go-owned unless an opt-in extension can match visual parity and performance through public primitives.
Go should own the pieces that require native integration, performance, persistence, process control, or terminal access:
- terminal input/output and screen flushing
- process lifecycle and signal handling
- extension host management and runtime-adapter loading
- event dispatch and transaction application
- buffers, windows, layout, and low-level UI drawing backends
- measuring, wrapping, clipping, style application, and draw batching
- viewport and virtual-list primitives for large histories
- keymap/command/autocmd registration and dispatch
- jobs, timers, and scheduling primitives
- model/provider clients as callable primitives
- tool execution as callable primitives
- session/database/auth/config stores as callable primitives
- guardrails for core invariants: no deadlocks, panics, corrupted state, or runaway unbounded projections
Go should expose mechanisms for extensions while keeping the default product behavior polished and self-contained.
Extension runtimes should own behavior users choose to add or override:
- custom keymaps and commands
- small overlays and custom windows
- focused composer modes or editor experiments
- prompt/context tweaks
- optional status/footer overlays
- custom tools and workflow hooks
- reskins, alternate UIs, custom workflows, and non-chat applications
The default chat UI should not require extensions. It must remain fast and polished with extensions disabled.
Core host APIs should be named after primitives, not product nouns.
Prefer host APIs like:
buf.*win.*layout.*ui.*event.*keymap.*command.*job.*timer.*model.*tool.*store.*
Avoid new host APIs like:
transcript.appendcomposer.submit_modethinking.showchat.add_messagevim.register_mode
Product-level convenience helpers can still exist in user/project extension modules, not the Go kernel:
local chat = require("my_workflow.chat")
chat.append_message("assistant", "hello")That helper should compose primitive APIs internally, for example by finding a window with role transcript, resolving its buffer, and appending a structured block through generic buffer operations.
These parts fit the direction and should be strengthened:
- extension host/runtime seam with Lua as the built-in adapter
- trusted local Lua extension loading with one Lua state per file
- open standard libraries for the Unix-style footgun model
- event handlers with priorities, consume, and stop semantics
- generic keymap targets by buffer, window, role, or global scope
- the buffer API as the main mutable state surface
- window discovery and mutation APIs
- layout get/set APIs
- low-level UI draw/cursor operations
- per-window renderer ownership via
renderer = "extension" - canonical composer buffer state
- bounded transcript snapshots to prevent render-loop stalls
- docs/ADR split plus gitignored
.librecode/work/for messy planning
These pieces are useful today but should not define the long-term architecture:
| Area | Current value | Migration direction |
|---|---|---|
| Go stock renderer | Provides the default product UI and high-quality hot rendering. | Keep it as the default; allow optional Lua overrides only when explicitly enabled. |
Bounded transcript buffer.blocks snapshot |
Practical extension read-side access without unbounded projections. | Keep bounded and treat as generic structured buffer data, not a separate transcript host API. |
Runtime buffer names like composer, transcript, status |
Useful default buffers and roles. | Treat them as conventional defaults, not privileged API concepts. |
action.run("...") |
Simple bridge for host actions. | Move toward generic commands/events and lower-level primitives; keep only kernel actions in Go. |
| Default layout generation in Go | Gives the app a stable default layout. | Keep as the default; expose layout primitives for optional overrides. |
These patterns fight the target architecture:
- new product-specific Go APIs for transcript/composer/thinking/tool presentation
- special Vim/composer mode registration hooks
- unbounded transcript/text projections during render or streaming events
- hidden extension behavior that bypasses event transactions
- APIs that mutate application state outside an active event/scheduled transaction
- treating
transcript,composer, orstatusas more than default buffers/windows/roles
The recent idea of librecode.transcript.append() and librecode.transcript.clear() is intentionally rejected as a core API direction. The desired replacement is generic buffer/object mutation plus optional Lua helper modules.
The current runtime can already support meaningful customization:
- extensions can intercept keys with priorities
- extensions can mutate the composer buffer
- extensions can find the composer window by role and get its bound buffer
- extensions can own a window renderer and draw directly into it
- extensions can implement optional composer modes and render overrides in Lua
- render and resize events exist
- layout/window mutations apply back to the terminal runtime
- transcript/thinking/tools/status are exposed as lightweight buffers or metadata surfaces
This is enough to prove the model, but not enough to make the entire app replaceable yet.
Recent architecture review reinforced the same direction: keep the core product polished, then expose lifecycle customization around the agent loop.
The most valuable ideas to adopt are:
- lifecycle events across sessions, turns, context building, provider requests, and tool calls
- tool middleware that can observe, modify, reject, or synthesize tool calls/results
- extension tools that are model-visible through the same registry as built-ins
- deterministic shell hooks for teams
- skill-bundled MCP/toolboxes and richer AGENTS.md hierarchy
- markdown subagents and a
Tasktool - a
/specplanning mode with an optional separate planning model
The lesson is not to move default UI into extensions. The lesson is to make agent behavior and workflow policy extensible while Go keeps the default UI fast and coherent.
The next major architecture pillar is explicit lifecycle/tool/context seams. This work should be delivered as stacked, reviewable PRs:
- lifecycle event contracts and dispatch
- session/input/turn lifecycle instrumentation
- context build hooks, bounded context contributions, and token breakdown
- provider request/response/error hooks
- built-in tool middleware
- unified tool registry for built-ins and extension tools
- diagnostics, examples, and hardening
The default terminal UI and assistant behavior must remain Go-owned and stable throughout this stack. Extension runtimes should observe and customize through typed host contracts rather than taking over hot UI paths.
Design constraints for these seams:
- payloads are bounded and redacted by default
- mutation contracts are explicit per event
- extension errors are visible and cannot corrupt the agent loop
- tool decisions are auditable
- context contributions have labels, token estimates, and budgets
- provider hooks never expose auth headers or secrets
- tests cover the no-extension path and at least one extension-modified path
The remaining architectural gaps are mostly about ownership:
- default transcript/tool rendering is Go-owned by design
- transcript is not a true structured buffer yet; it is a bounded snapshot plus metadata buffer
- layout is not fully canonical; Go still derives the default stock layout
- the assistant prompt/model/tool loop is still primarily Go-owned
- jobs/processes and a general scheduler are missing
- extension renderers can draw, but there is no full highlight/extmark/namespace model yet
- generic rendering primitives are still too small for transcript-quality parity
- session/model/tool stores are not exposed as generic runtime primitives
- the default chat UI is Go-owned; Lua extensions are optional
Buffers are mutable state containers. They can hold plain text now and should evolve toward structured block/object buffers.
Examples:
- composer text
- transcript/message blocks
- thinking blocks
- tool result blocks
- status text
- scratch extension buffers
Buffers should exist independently of whether they are visible.
Windows are views onto buffers.
A window owns view-specific data:
- buffer binding
- role
- position and dimensions
- viewport/scroll
- cursor position
- renderer owner
- metadata
Multiple windows should be able to show the same buffer differently.
Layout arranges windows on the screen.
The long-term default chat layout should be regular layout state, not a hardcoded renderer assumption.
ui.* is the low-level grid drawing layer for extensions that own a window renderer.
Go should back the hot/terminal-correct pieces of rendering. Lua should compose them.
Current first-pass primitives include terminal-width measurement, truncation, padding, wrapping, drawing text/lines/spans/boxes, drawing batches, clearing windows/regions, setting cursors, theme-token discovery, viewports, and virtual-list helpers for large histories.
Longer term this should still gain:
- namespace-scoped highlights
- extmarks/virtual text
- richer window viewport/scroll APIs
- renderer registration helpers
Events are the control plane.
Extensions should be able to observe, mutate, consume, stop, and emit events. Default behavior should increasingly be implemented as event handlers plus primitive mutations.
A programmable runtime needs non-blocking work primitives:
- spawn jobs
- schedule callbacks
- debounce/throttle
- timers and intervals
Timers are now partially implemented (timer.defer, timer.interval, timer.stop). Job/process spawning and a general scheduler still need to be added before deeper Lua-owned runtime orchestration.
Before adding more APIs, classify them:
- primitive/kernel API: OK in Go
- product convenience API: implement in Lua
- compatibility API: document as temporary and do not expand
Status: implemented for the current runtime surface.
Generic buffer operations now cover text, structured blocks, metadata variables, and clearing buffers. Transcript data is exposed as bounded blocks on the regular transcript buffer instead of through a transcript-specific Go API.
Shape:
lc.buf.append("transcript", {
kind = "message",
role = "assistant",
text = "hello",
metadata = {},
})
lc.buf.clear("transcript")
lc.buf.get_blocks("transcript", start, stop)
lc.buf.set_blocks("transcript", start, stop, blocks)
lc.buf.delete_blocks("transcript", start, stop)
lc.buf.get_var("transcript", "snapshot_count")
lc.buf.set_var("transcript", "owner", "my-extension")This keeps transcript behavior generic while still supporting chat-like data.
Optional user/project Lua modules may wrap primitives:
- workflow-specific helper modules under a project extension root
- optional chat/composer/status helpers maintained outside the Go host API
These modules should compose primitives and be replaceable.
Keep default layout/status/composer behavior in Go. Allow user extensions to override specific windows or add overlays where parity is achievable.
Do not force-migrate complex hot renderers such as transcript rendering. Go keeps mature stock rendering for quality and performance while Lua remains an optional control/customization layer.
Expose model/tool/session primitives and lifecycle events so Lua can own more of the assistant flow:
- prompt preparation
- model request creation
- model delta handling
- tool activity mapping
- session message persistence policy
Eventually support a bare mode that loads only the kernel and user-selected extensions, without the stock chat distribution.
Before adding an API, ask:
- Is this a primitive or a product noun?
- Could this be implemented in Lua using existing primitives?
- Does it require native Go integration, persistence, or terminal/process access?
- Does it preserve bounded work on render/stream events?
- Does it mutate through an event/scheduled transaction?
- Is it useful for applications other than chat?
- If it is compatibility-only, is that clearly documented?
If the answer points to optional customization, build it as a user/project Lua extension. If it is core default UX, keep it in Go.