# Lisp Extension Anatomy


You write one `defextension` form, drop it in an extensions directory, and run
`/reload`. Your command, tool, or event handler is live in the running session,
and `/disable` takes it out again with nothing left behind. That round trip —
add it, use it, remove it cleanly — is what an extension's structure is built to
guarantee. This page explains the structure that makes the guarantee hold.

## The form

An extension is a `defextension` form with a name and clauses:

```lisp
(defextension my-extension
  (:requires ...)
  (:provides ...)
  (:metadata ...))
```

`:requires` lists what must already be present for the extension to install —
capabilities, providers, other extensions, tools. `:provides` lists what this
extension adds: its contributions. `:metadata` is an optional plist for
properties like `(:autoload nil)`. Most extensions need only `:provides`;
requirements for the common contribution kinds are derived automatically, so a
`command` clause already carries its dependency on the commands capability
without you naming it.

The name matters beyond labeling. `defextension greet` binds a variable named
`*greet-extension-manifest*`. That variable does not hold an installed
extension. It holds a function of no arguments that, each time you call it,
builds a fresh extension value.

## Manifest as value

The function bound to `*greet-extension-manifest*` is the manifest. Calling it
returns an `extension` value: a record carrying the id, the list of
requirements, and the list of contributions, with each contribution still inert
data. Nothing has touched the running session yet. You can build this value,
inspect it, pass it around, and discard it, and the session is exactly as it
was.

A thunk rather than a single prebuilt value is deliberate. Each call materializes
new contribution objects with their own storage, so the same manifest installs
into many protocols — a REPL, the production boot, a test, an agent's
sub-session — without sharing mutable state between them. The manifest is the
recipe; calling it bakes a fresh batch.

This is the same shape everywhere in kli. The model providers, the tools, the
slash commands, the terminal interface — each is a manifest bound to a
`*...-extension-manifest*` variable, and each is installed the same way you
would install your own. There is no privileged built-in path. You can read these
as real `defextension` forms: the builtin `bash` tool extension, the builtin theme
extension, and cairn as a full external one, a single manifest with a store-opening
effect, its model tools, a live context hot-patch, and its own slash commands, each
carrying a retractor.

## The one step that mutates

Turning the value into running behavior is a single operation:

```lisp
(install-manifest manifest protocol context)
```

`install-manifest` calls the thunk to get a fresh extension, then activates it
against the protocol. Activation is where every effect happens, and it happens in
a recorded way. It first checks the extension's requirements and errors if any
are unmet. Then it walks the contributions in order and installs each one,
pushing every installed contribution onto the protocol's record as it goes. If
any contribution fails midway, activation retracts the ones that already
installed and re-signals the error, so a failed install leaves the protocol where
it started rather than half-changed.

`install-manifest` returns the activated extension. That return value is the
handle you keep, because it is what removal takes:

```lisp
(deactivate-extension protocol extension context)
```

Deactivation reads back the contributions this extension installed and retracts
each one, then forgets the extension. These two calls are the entire lifecycle.
Everything else — `/install`, `/reload`, `/enable`, profile bundles, the boot
sequence — drives these same two operations underneath.

## Contributions and their retractors

A contribution is one unit of behavior an extension provides. The closed kinds
the protocol knows directly are a model-callable tool, a capability provider, a
provider contract, a generic-function method, a live object, and a raw effect.
Other kinds — commands, event handlers, event types, keybindings, themes, message
renderers, status slots, widgets, profiles, settings declarations — are themselves
contributed by extensions that teach the protocol a new kind. A command, for one, is its own
kind: installing it registers a slash command against the commands provider, and
retracting it unregisters that command. The kind taxonomy is extensible the same
way everything else is.

Every kind, without exception, pairs an installer with a retractor. Installing a
tool registers it and makes it callable; retracting it removes it from the
registry. Installing a capability provider files it under its name; retracting it
removes that entry. A method contribution installs a real method on the named
generic function and removes it with `remove-method` on retraction. The pairing
is not a convention you opt into — it is how each kind is defined, so there is no
kind that can be installed but not removed.

The `effect` kind is the general case for behavior that does not fit a named
kind:

```lisp
(effect "my-effect" installer retractor)
```

The installer and the retractor are both functions of `(protocol contribution
context)`. The installer's return value is stored as the contribution's state,
and the retractor reads it back through `(kli/ext:contribution-state
contribution)` to undo precisely what the installer did. The retractor is
required. When an effect genuinely has nothing to clean up you pass `:no-op`
explicitly, which records the choice rather than letting it pass silently. The
duty an effect author carries is symmetry: the retractor must drain exactly what
the installer created — unregister what it registered, restore what it replaced —
because the protocol will call it expecting the session to return to its prior
state.

## Why reversibility is total

Removing an extension drains every contribution it installed and nothing else.
That holds because of how the two halves fit together. Installation records each
contribution as it lands, so removal has an exact list to work from rather than a
guess. Each kind defines its own retractor, so there is no contribution the
remover does not know how to undo. And state lives in per-protocol storage keyed
by the extension, not in global variables, so dropping the protocol drops the
state with it. A global `defvar` for extension state breaks this — a second
protocol overwrites it, and retraction cannot reach it — which is why it is
treated as a bug.

The payoff is the round trip you started with. Because installing is a recorded
transaction and every kind is reversible, `/disable` and `/reload` can take an
extension out mid-session and leave the rest of the session — your context, your
log, your model connection — untouched.

## Where extensions load from

User extensions are plain Lisp files on disk. kli reads them from several roots.
Files under `~/.config/kli/extensions/` load in every session; files under
`<project>/.kli/extensions/` load only in the project kli was launched in. Extra
roots come from the `"extension-dirs"` list in `~/.config/kli/config.json`, and
`kli --extension PATH` loads one file or directory for a single run.

A single `.lisp` file is one extension. A directory containing
`extension.lisp` is one extension unit, loading its files in order with
`extension.lisp` last. A directory without `extension.lisp` is a group: its loose
files are single-file units and its subdirectories recurse. Exactly one
`defextension` per unit; zero or two is an error and the unit is skipped.

When kli loads a source file, it binds the defining protocol so the
`defextension` form not only binds its manifest variable but registers the
manifest for that protocol, which is what lets `/reload` re-index from disk and
reinstall the enabled set. Loading is fail-soft: a broken file warns and is
isolated while the rest keep working, so one bad extension does not take down the
others.

## Related

- [The Live Image](/kli/concepts/the-live-image) — why behavior is editable while
  kli runs, the mechanism this anatomy rests on.
- [Capabilities and Fault Barriers](/kli/concepts/capabilities-and-fault-barriers)
  — the capability set that gates installing, retracting, and recoding.
- [Profiles](/kli/concepts/profiles) — how groups of extensions are bundled and
  selected.
