Plugins / Core
elelem
The agent loop. Nothing else.
The core gem: the agent loop, the plugin registry, and the elelem command. Tools, slash commands, and model providers all come from plugins.
gem install elelemThe core gem is the harness: a loop that sends the conversation to a model,
runs the tools the model asks for, and repeats until the model answers
without asking for a tool. It also loads plugins and provides the elelem
command. It ships with no file tools, no shell access, and no real model.
Those come from plugins, so the core stays small enough to read in one
sitting.
Install #
gem install elelem elelem-builtinselelem alone runs, but has nothing to do: it registers one provider,
stub, and no tools. elelem-builtins adds the default tools and slash
commands. The elelem executable requires it automatically when it is
installed. To talk to a real model, install a provider plugin and pass its
name with -p, for example gem install elelem-anthropic and
elelem chat -p anthropic.
Configure #
There is no configuration file. Configuration is Ruby: any .rb file in
one of these directories is loaded at startup, in this order:
| Directory | Scope |
|---|---|
~/.agents/plugins/ | every project |
.agents/plugins/ | the current working directory |
Only files directly in those directories are loaded, not subdirectories.
Files are loaded with load, so /reload runs them again.
The system prompt is built from these files on every request:
~/.agents/AGENTS.md, if it exists.- The first
AGENTS.mdfound walking up from the current directory.
When both exist, the global file comes first. They are inserted into the
default prompt, which starts with “You are a terminal coding agent.” Text
passed with --system-prompt is placed before all of it.
Use #
Usage: elelem [command] [options] [args]
Commands:
chat Interactive REPL (default)
ask <prompt> One-shot query (reads stdin if piped)
files Output files as XML (no options)
help Show this help
Options:
-p, --provider NAME Provider to use (default: stub)
--model NAME Model to use, passed to the provider
--system-prompt TEXT Role text prepended to the system prompt
-h, --help Show this helpchat starts the interactive loop. A line that starts with / runs a slash
command; any other line is a prompt for the model. An unknown slash command
prints the list of known ones. Ctrl-D ends the session.
elelem chat -p anthropicask runs one turn and exits. If stdin is a pipe, its contents are
appended to the prompt in a fenced code block.
git diff | elelem ask -p ollama "Write a commit message for this diff"When stdout is a terminal, you see the reply as it is produced. When stdout
is piped, progress goes to stderr and only the final reply is written to
stdout. ask runs without an interactive input, so nothing can stop to ask
you a question.
files prints files in an XML envelope you can paste into any model. With
no stdin it uses git ls-files; otherwise it reads one path per line from
stdin.
git ls-files lib | elelem files<documents>
<document index="1"><source>lib/elelem.rb</source><document_content><![CDATA[...]]></document_content></document>
</documents>--model is passed to the provider as a model: option. What it accepts
depends on the provider.
If -p NAME is not registered by a loaded plugin, elelem tries
require "elelem/NAME", so an installed provider gem works without a
plugin file. If that fails, it stops with:
elelem: unknown provider: "nosuch". Available: stub. Run: gem install elelem-nosuchWrite a plugin #
A plugin is a Ruby file that calls Elelem.configure. Put this in
.agents/plugins/hello.rb:
Elelem.configure do |config|
config.setup(:hello) do |agent|
agent.toolbox.add("hello",
description: "Greet someone",
params: { name: { type: "string" } },
required: ["name"]) do |args|
{ greeting: "hello #{args["name"]}" }
end
agent.commands.register("hello", description: "Say hello") do |args|
agent.output.say "hello #{args}"
end
end
endconfig accepts four kinds of registration:
| Call | Registers |
|---|---|
config.provider(:name) { |**options| ... } | a model provider, selected with -p name |
config.setup(:name) { |agent| ... } | a setup block, run against every new session |
config.output { ... } | the object that renders output (the last one registered wins) |
config.input { |**options| ... } | the object that reads user input |
The agent passed to a setup block is the session. It exposes toolbox,
commands, conversation, output, input, provider (readable and
writable), context, and turn(prompt).
Tools. toolbox.add(name, description:, params:, required:, aliases:)
takes a block that receives the arguments as a string-keyed hash and
returns a hash. params is the properties object of a JSON Schema, and
arguments are validated against it before the block runs. A returned hash
with an :error key is a failure. Results are sent back to the model as
JSON with ok: true or ok: false added. An exception in a tool becomes a
failure result instead of ending the session.
Hooks. toolbox.before(name) { |args| } and
toolbox.after(name) { |args, result| } run around one tool. Without a
name they run around every tool and also receive tool_name:. Raising in a
before hook stops the call; the error goes back to the model.
Commands. commands.register(name, description:, completions:) adds a
slash command. The block receives the text after the command name, or
nil. completions is an array or a lambda returning one, used for tab
completion.
Providers. A provider responds to fetch(messages, tools) and yields
events: { type: "saying", text: }, { type: "thinking", text: }, and
{ type: "doing", id:, name:, arguments: } for a tool call. The built-in
stub shows the whole contract:
class StubProvider
def fetch(messages, tools = [], &block)
block.call(type: "thinking", text: "considering the request")
return block.call(type: "saying", text: "done.") if messages.last[:role] == "tool"
block.call(type: "saying", text: "let me check.")
return unless (tool = tools.first)
block.call(type: "doing", id: "stub-1", name: tool[:function][:name], arguments: {})
end
endThe same API is available from Ruby without the REPL:
require "elelem"
reply = Elelem.ask("What does this repo do?", provider: "anthropic")How it works #
Elelem::CLI parses arguments and builds a session through the registry.
The first build loads the plugin directories, then creates the provider,
the agent, the input, and the output, and runs every setup block against
the session.
Each turn, the agent sends the system prompt, the conversation, and the tool definitions to the provider. If the reply contains tool calls, it runs each one through the toolbox, appends the results, and asks again. When a reply has no tool calls, the turn ends.
Only the user prompt and the final reply of each turn are kept in the conversation. Intermediate tool calls and their results are sent to the model during the turn and then dropped.
Errors are contained. A provider error ends the turn with the error as the reply. A tool error becomes a failure result. A slash command error is printed and the loop continues. A plugin file that raises, including one with a syntax error, is skipped.
Limitations #
- Without plugins, elelem cannot read files, run commands, or reach a
model.
-p stubis the default provider and only exists so a bare install starts and exits cleanly. - The default output prints replies as plain fenced text. Rendering and the
.progress indicator come fromelelem-builtins. exe/elelemruns Ruby with-W0, which silenceswarn. A plugin file that fails to load is skipped without a message, and the hint to installelelem-builtinsis not shown. SetELELEM_LOG_LEVEL=debugfor logger output; it does not cover these two messages./reloadclears every setup block, then re-runs the files in the plugin directories. Setup blocks that came from a gem loaded withrequireare not registered again, so their tools and commands disappear until you restart. This includes everything inelelem-builtins,/reloaditself among them.- There is no session persistence in core. Quitting loses the conversation.
Development #
git clone https://git.mokhan.ca/elelem/elelem.git
cd elelem
bin/setup
bin/test
bin/lintbin/setup runs bundle install and mise install. bin/run runs
exe/elelem through Bundler. Patches go by email; see
git-send-email.io.
Environment #
| Variable | Default | Description |
|---|---|---|
ELELEM_LOG_LEVEL | warn | Log level for messages written to stderr (debug, info, warn, error, fatal). |