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
endRestart 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:
| Method | Registers |
|---|---|
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") }
endSetup 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:
~/.agents/plugins/: yours, for every project..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/elelemrequireselelem/builtinsifelelem-builtinsis installed.-p NAMErequireselelem/NAMEif no provider calledNAMEis 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:
- Core registers the
stubprovider. exe/elelemrequireselelem-builtins, which registers its setups and its terminal output.- Plugin files load:
~/.agents/plugins/*.rb, then.agents/plugins/*.rb. Gems they require register as they load. - If
-p NAMEis still unknown,elelem/NAMEis 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) }
endThe 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):
- Forget every registered setup block.
- Remove every tool, hook, and command from the session.
- Load the plugin files again.
- 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
requireruns a file only once. After/reload, only setups from plugin files are back. Tools and commands from gems, including/reloaditself fromelelem-builtins, are gone until you restart elelem./reloadis 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/elelemstarts Ruby with-W0, which silenceswarn. When you run theelelemcommand, a broken plugin file is skipped without a message. If a tool or command is missing, check the file withruby -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
endThe full list of methods is in the Plugin API.