Documentation menu

Concepts

Plugins

What a plugin is, where elelem loads plugins from and in what order, and how to reload them.

A plugin is any Ruby file or gem that calls Elelem.configure. Providers, tools, commands, and the terminal renderer are all plugins. Core has no other extension point.

This is the open/closed principle: the core is closed for modification and open for extension. You add a model, a tool, or an interface by writing a plugin, never by changing elelem itself. The project follows the same rule; in 0.11.0 every provider, tool, and interface moved out of the core into its own gem.

# ~/.agents/plugins/hello.rb
Elelem.configure do |config|
  config.setup(:hello) do |session|
    session.commands.register("hello", description: "Say hello") do |name|
      session.output.say("hello, #{name || "world"}")
    end
  end
end

Restart elelem and type /hello.

Elelem.configure #

Elelem.configure { |config| ... }

The block receives the registration object. It has four methods, and each returns config, so calls can be chained:

MethodRegisters
config.provider(name) { |**options| ... }A provider factory. See Providers.
config.setup(name) { |session| ... }A setup block that adds tools, hooks, or commands.
config.output { ... }A factory for the output object.
config.input { |commands:| ... }A factory for the input object.

Names are stored as strings. Elelem.configure only records blocks; nothing runs until elelem builds a session.

Setup blocks #

A setup block receives the Session: the toolbox, commands, output, input, conversation, and provider. It runs once each time a session is built, after the provider, input, and output exist.

config.setup(:audit) do |session|
  session.toolbox.after { |args, result, tool_name:| File.write("audit.log", "#{tool_name} #{args}\n", mode: "a") }
end

Setup blocks run in the order their names were first registered. Registering a block under a name that already exists replaces the block and keeps its place in the order. That is how you override a built-in: a plugin file with config.setup(:execute) { ... } replaces the execute setup from elelem-builtins.

An exception inside a setup block is not caught. At startup it stops elelem with elelem: <message>.

Where plugins come from #

Plugin files #

elelem loads every *.rb file in these directories, in this order:

  1. ~/.agents/plugins/: yours, for every project.
  2. .agents/plugins/: the project’s, relative to the current directory.

Within a directory, files load in alphabetical order. Subdirectories are ignored, and elelem does not search parent directories for .agents/plugins/.

Files load once per process, the first time elelem needs a provider, an input, an output, or the setup blocks. They load with Ruby’s load, so /reload can run them again.

Note: A project’s .agents/plugins/ is Ruby code, and elelem runs it when you start elelem in that directory. Read it before running elelem in a repository you did not write.

Gems #

A gem is a plugin when it calls Elelem.configure as it loads. elelem requires gems in three cases:

  • exe/elelem requires elelem/builtins if elelem-builtins is installed.
  • -p NAME requires elelem/NAME if no provider called NAME is registered yet.
  • Your plugin file requires it.

Every other gem needs a one-line plugin file. To load elelem-tools:

# ~/.agents/plugins/tools.rb
require "elelem/tools"

The same works for elelem-mcp, elelem-skills, and any other plugin gem.

Load order #

When you run elelem chat, registration happens in this order:

  1. Core registers the stub provider.
  2. exe/elelem requires elelem-builtins, which registers its setups and its terminal output.
  3. Plugin files load: ~/.agents/plugins/*.rb, then .agents/plugins/*.rb. Gems they require register as they load.
  4. If -p NAME is still unknown, elelem/NAME is required.

Setup blocks then run in that same order of registration.

Input and output factories #

Replace how elelem reads and prints by registering factories:

Elelem.configure do |config|
  config.output { MyOutput.new }
  config.input { |commands:| MyInput.new(commands: commands) }
end

The output factory is called with no arguments. The input factory is called with commands:, the session’s slash commands, for tab completion. The last factory registered wins. elelem-builtins registers an output that renders Markdown with glow; elelem-ratatui registers both.

An output object must respond to:

say(text, as: nil)          # print a line; as: :markdown or :error
print(text)                 # print without a newline
thinking(text)              # the model's thinking
doing(name, args, state: "+") # a tool call
waiting                     # called before each provider request
display_file(path, fallback: nil)

An input object must respond to ask(prompt), returning the line or nil to end the session, and interactive?. Subclassing Elelem::Output gives you every method; override the ones you need.

Factories apply to elelem chat only. elelem ask always uses the core Output and a NullInput that never prompts.

Reloading #

/reload, from elelem-builtins, calls Elelem::Config.reload(session):

  1. Forget every registered setup block.
  2. Remove every tool, hook, and command from the session.
  3. Load the plugin files again.
  4. Run the setup blocks registered by that load.

Provider factories and input and output factories are kept. The conversation is kept.

Note: Gems are not loaded again, because Ruby’s require runs a file only once. After /reload, only setups from plugin files are back. Tools and commands from gems, including /reload itself from elelem-builtins, are gone until you restart elelem. /reload is for iterating on a plugin file.

When a plugin fails to load #

Each plugin file loads inside a rescue for StandardError and ScriptError, so a syntax error or a missing gem skips that file and the rest still load. elelem reports it with warn:

elelem: failed to load plugin /home/you/.agents/plugins/broken.rb: ...

Note: exe/elelem starts Ruby with -W0, which silences warn. When you run the elelem command, a broken plugin file is skipped without a message. If a tool or command is missing, check the file with ruby -c ~/.agents/plugins/NAME.rb.

Writing a plugin gem #

Name the gem elelem-NAME and put the entry point at lib/elelem/NAME.rb, so require "elelem/NAME" loads it. For a provider, register it under NAME too, and -p NAME will find the gem without a plugin file.

# lib/elelem/wc.rb
require "elelem"

Elelem.configure do |config|
  config.setup(:wc) do |session|
    session.toolbox.add("wc", description: "Count lines in a file",
                              params: { path: { type: "string" } }, required: ["path"]) do |args|
      { lines: File.foreach(args["path"]).count }
    end
  end
end

The full list of methods is in the Plugin API.