# elelem

> The core gem: the agent loop, the plugin registry, and the elelem command. Tools, slash commands, and model providers all come from plugins.

Maturity: ready. Install: `gem install elelem`. Source: https://src.mokhan.ca/elelem/elelem/


The 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

```bash
gem install elelem elelem-builtins
```

`elelem` 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.md` found 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

```text
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 help
```

`chat` 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.

```bash
elelem chat -p anthropic
```

`ask` runs one turn and exits. If stdin is a pipe, its contents are
appended to the prompt in a fenced code block.

```bash
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.

```bash
git ls-files lib | elelem files
```

```text
<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:

```text
elelem: unknown provider: "nosuch". Available: stub. Run: gem install elelem-nosuch
```

## Write a plugin

A plugin is a Ruby file that calls `Elelem.configure`. Put this in
`.agents/plugins/hello.rb`:

```ruby
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
end
```

`config` 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:

```ruby
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
end
```

The same API is available from Ruby without the REPL:

```ruby
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 stub` is 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 from `elelem-builtins`.
- `exe/elelem` runs Ruby with `-W0`, which silences `warn`. A plugin file
  that fails to load is skipped without a message, and the hint to install
  `elelem-builtins` is not shown. Set `ELELEM_LOG_LEVEL=debug` for logger
  output; it does not cover these two messages.
- `/reload` clears every setup block, then re-runs the files in the plugin
  directories. Setup blocks that came from a gem loaded with `require` are
  not registered again, so their tools and commands disappear until you
  restart. This includes everything in `elelem-builtins`, `/reload` itself
  among them.
- There is no session persistence in core. Quitting loses the conversation.

## Development

```bash
git clone https://git.mokhan.ca/elelem/elelem.git
cd elelem
bin/setup
bin/test
bin/lint
```

`bin/setup` runs `bundle install` and `mise install`. `bin/run` runs
`exe/elelem` through Bundler. Patches go by email; see
[git-send-email.io](https://git-send-email.io/).


## Environment

| Variable | Default | Description |
|---|---|---|
| `ELELEM_LOG_LEVEL` | `warn` | Log level for messages written to stderr (debug, info, warn, error, fatal). |

Canonical: https://elelem.sh/plugins/elelem/

