Concepts
How it works
The agent loop step by step, the objects behind it, and the path a single tool call takes.
elelem is a loop: send the conversation to a model, run the tools it asks for, send the results back, and stop when it asks for nothing. This page walks through that loop and names the objects involved, so the Plugin API makes sense.
The objects #
| Object | Job |
|---|---|
Repl | Reads lines from the input and dispatches each one: a slash command or a turn. |
Session | Bundles an agent, an input, and the slash commands. Setup blocks receive the session. |
Agent | Runs one turn: calls the provider, runs tools, repeats until done. Holds the provider, toolbox, output, and transcript. |
Transcript | The conversation plus the system prompt. agent.conversation returns this. |
Conversation | An ordered list of messages with roles user, assistant, or tool. |
Toolbox | Tools the model can call, plus before and after hooks. |
Commands | Slash commands you can type. |
Input, Output | Reading your lines and printing to the terminal. Plugins can replace both. |
A Session forwards provider, provider=, toolbox, output, conversation, context, and turn to its agent, so plugin code rarely touches the agent directly.
Startup #
When you run elelem chat -p anthropic:
exe/elelemloads core, which registers thestubprovider. It then requireselelem-builtinsif it is installed.- The CLI parses options and calls
Elelem.start. - elelem loads plugin files from
~/.agents/plugins/and.agents/plugins/. See Plugins. - It builds the provider named by
-p. If no plugin registeredanthropic, it requires theelelem/anthropicgem and looks again. See Providers. - It builds the output and input (a plugin’s factory if one is registered, otherwise the core
OutputandInput), creates the agent and the session, and runs every setup block against the session. Setup blocks add tools, hooks, and commands. - The
Replprintselelem v0.13.0and prompts with>.
A turn #
You type fix the failing test and press Enter.
- Dispatch. The REPL skips empty lines. A line starting with
/runs a slash command. Anything else becomes a turn. - Record the prompt. The agent appends
{ role: "user", content: "fix the failing test" }to the conversation. - Build the messages. The transcript renders the system prompt fresh, then appends the conversation, then any tool calls and results from earlier in this turn.
- Call the provider. The agent calls
provider.fetch(messages, tools), wheretoolsis the toolbox as a list of JSON Schema function definitions. The provider streams events back through a block:sayingevents add to the reply text.thinkingevents print dimmed.doingevents are tool calls, collected for the next step.
- Print the reply so far, as Markdown.
- Stop or continue. If the provider asked for no tools, the turn is over. Otherwise the agent runs each tool call through the toolbox, appends an assistant message carrying the tool calls and one
toolmessage per result, and goes back to step 3. - Record the reply. The text of the last response is appended as
{ role: "assistant", content: ... }and returned.
you ──> Repl ──> Agent#turn
│
├─> provider.fetch(system + history + this turn's tool traffic, tools)
│ └─ events: saying / thinking / doing
│
├─ no tool calls? ──> save reply, return
│
└─ tool calls ──> Toolbox#run each ──> results ──> back to fetchWhat the conversation keeps #
Only your prompt and the final reply are saved to the conversation. The tool calls and tool results of a turn exist only while that turn runs. On the next turn, the model sees the system prompt and the user and assistant messages, not the earlier tool output. Use /context to see exactly what is kept.
When the provider fails #
If fetch raises, the agent logs a warning, prints the message as an error, and ends the turn with the reply Error: <message>. That reply is saved to the conversation like any other. The REPL keeps running.
How a tool call flows #
The provider emits a doing event:
{ type: "doing", id: "toolu_01", name: "read", arguments: { "path" => "README.md" } }The agent prints + read({"path" => "README.md"}) and calls toolbox.run("read", arguments):
- Resolve the name. An alias maps to its tool, so
openbecomesread. An unknown name gets a stand-in tool whose name ends in?. - Validate. The arguments are checked against the tool’s JSON Schema. On failure, the result is an error and nothing else runs: no hooks, no handler. An unknown tool fails here with
unknown tool: "name". Available: read, write, execute. - Before hooks. Global hooks first, then hooks for this tool.
- Handler. The tool’s block runs with the arguments hash. Keys are strings, because they come from JSON.
- After hooks. Global hooks first, then hooks for this tool. They receive the handler’s return value.
- Wrap the result. If the returned hash has an
:errorkey, the result is a failure (ok: false); otherwise a success (ok: true). - Catch exceptions. If a hook or the handler raises, the result is a failure carrying the error message, the tool name, and the arguments. A
beforehook can refuse a call this way.
The agent serializes the result to JSON and sends it back as a tool message:
{"content":"# My project\n...","path":"README.md","ok":true}A validation failure looks like this:
{"error":"object at root is missing required properties: path","ok":false}The model reads the error and usually tries again. Nothing in elelem retries for it.
Where to go next #
- Providers: the exact
fetchcontract. - Tools and commands: writing tools, hooks, and slash commands.