Modules#
All ECA features are built-in modules: tools, agents, MCP, providers, hooks and more. Modules are how you extend ECA and configure how a feature works.
You can turn any module off, and write your own in Clojure. ECA runs them with SCI, a Clojure interpreter, so there's nothing to build or install.
Tip
Ask ECA to write a module for you: its built-in eca-manage skill tells it how.
All built-in modules
Each module links to its code.
| Module | What it does |
|---|---|
| Core | |
eca/core |
Chats, config, models and the module runtime. It can't be turned off. |
| Tools | |
eca/filesystem |
Read, write, edit, move and search files. |
eca/shell |
Run shell commands and background jobs. |
eca/git |
Run git commands. |
eca/editor |
Diagnostics, definitions and references from the editor. |
eca/ask-user |
Let the model ask you a question. |
eca/task |
A task list the model keeps for each chat. |
| Agents | |
eca/agents |
Agents from config and Markdown files, and choosing one for each chat. |
eca/subagents |
Let an agent hand a task to a subagent and get its answer. |
eca/agent-code |
The code agent, for coding tasks. The default agent. |
eca/agent-plan |
The plan agent: plans changes without making them. |
eca/subagent-explorer |
The explorer subagent: finds and reads code without changing it. |
eca/subagent-general |
The general subagent: research and multi-step tasks. |
| Skills, rules and MCP | |
eca/skills |
Skills: SKILL.md folders the model loads when it needs them. |
eca/rules |
Rules: global, project and path-scoped. |
eca/mcp |
MCP servers: their tools, prompts, resources and instructions. |
eca/tool-search |
Hides MCP tools behind search_tools when there are too many. |
| Your extensions | |
eca/custom-tools |
Tools from the customTools config: shell command templates. |
eca/custom-commands |
Commands from Markdown files and the commands config. |
eca/hooks |
Your shell hooks. |
eca/plugins |
Plugin marketplaces: install, update and remove bundles. |
| Chat | |
eca/approval |
Tool approval from the toolCall.approval rules. |
eca/compact-as-summary |
When the context fills up, the chat model summarizes the chat. |
eca/repo-map |
A map of the repository, for the repoMap context. |
eca/manage |
Set up, debug and change ECA: /init, /skill-create, and the eca-info and eca-manage skills. |
| LLM APIs | |
eca/api-anthropic |
The Anthropic Messages API. |
eca/api-openai-responses |
The OpenAI Responses API. |
eca/api-openai-chat |
The OpenAI Chat Completions API, which OpenAI-compatible providers speak. |
eca/api-ollama |
The Ollama chat API. |
eca/api-bedrock |
The AWS Bedrock Converse API. |
| Providers | |
eca/anthropic |
Claude models from Anthropic: a Max subscription, a Console key or a pasted key. |
eca/azure |
Azure OpenAI, at your resource URL. |
eca/bedrock |
Models on AWS Bedrock, with a bearer token. |
eca/copilot |
GitHub Copilot models, with your account's model list and quota. |
eca/deepseek |
DeepSeek models. |
eca/google |
Gemini models, through Google's OpenAI-compatible API. |
eca/litellm |
A LiteLLM proxy, at your URL. |
eca/lmstudio |
Local models through LM Studio. |
eca/mistral |
Mistral models. |
eca/moonshot |
Moonshot's Kimi models. |
eca/ollama |
Local models through Ollama. |
eca/openai |
GPT models from OpenAI: a ChatGPT subscription or a pasted key. |
eca/openrouter |
Models from many vendors, through OpenRouter. |
eca/z-ai |
Z.ai's GLM models, through its Anthropic-compatible API. |
| Off by default | |
eca/remote |
The remote server, for web clients like web.eca.dev. remote.enabled turns it on. |
eca/otel |
Exports ECA's metrics with OpenTelemetry. An otlp config turns it on. |
eca/repl |
An nREPL inside ECA. See A REPL on ECA. |
Managing modules#
/modules lists all modules, whether they're active and what they add.
/modules disable eca/mcp
/modules enable eca/mcp
/modules reload acme/jira
These changes last for the session. To keep them, set them in your config:
{
"modules": {
"eca/mcp": {"enabled": false},
"acme/jira": {"source": "~/dev/eca-jira/jira.clj"}
}
}
enabledturns a module on or off. Modules that require it turn off with it.sourceloads an external module from a file or folder.priorityorders the module's event handlers, higher first. It's0by default, and-100for built-in modules.
Writing a module#
A module is a Clojure namespace that defines a module map. Save this file and ECA loads it in a few seconds, then again every time you save it:
(ns me.hello)
(def module
{:id :me/hello
:description "Greets people."
:tools {"greet" {:description "Greet someone by name."
:parameters {:type "object"
:properties {"name" {:type "string"}}
:required ["name"]}
:handler (fn [_ctx {:strs [name]}]
(str "Hello, " name "!"))}}
:commands {"hello" {:description "Say hello."
:handler (fn [_ctx _args] "Hello from my module!")}}})
The model now has a hello__greet tool, and you have a /hello command. If the file has an error, /modules shows it, with its line.
ECA looks for modules in these places. When two define the same module id, the first one wins:
eca server --module <path>, for one session.modules.<id>.sourcein your config..eca/modules/in each workspace.~/.config/eca/modules/.- The
modules/folder of installed plugins.
A module can also be a folder with a module.clj. Its other namespaces load from the same folder, like acme.jira.client from acme/jira/client.clj.
Trust
Modules run with ECA's permissions. A workspace's modules load without asking, the same way its hooks run.
Config#
A module declares the config keys it uses in :config-spec. Users set them at the root of the config, like ECA's own keys:
(ns acme.jira
(:require [eca.api :as eca]
[eca.api.http :as http]))
(defn- fetch-issue [ctx key]
(let [{:keys [jiraUrl jiraToken]} (eca/config ctx)]
(:body (http/get (str jiraUrl "/rest/api/3/issue/" key)
{:oauth-token jiraToken :as :json}))))
(def module
{:id :acme/jira
:description "Jira issues."
:config-spec {:jiraUrl {:type :string :doc "Jira base URL, e.g. https://acme.atlassian.net"}
:jiraToken {:type :string :secret? true :default "${env:JIRA_TOKEN}"}}
:tools {"issue" {:description "Fetch a Jira issue: summary, description, status and comments."
:parameters {:type "object" :properties {"key" {:type "string"}} :required ["key"]}
:handler (fn [ctx {:strs [key]}] (pr-str (fetch-issue ctx key)))}}
:commands {"jira" {:description "Start working on a Jira issue."
:arguments [{:name "key" :required true}]
:handler (fn [_ctx {:keys [key]}]
{:prompt (str "Read Jira issue " key " with jira__issue, then implement it.")})}}})
{
"jiraUrl": "https://acme.atlassian.net"
}
- Types:
:string,:number,:boolean,:enum(with:values),:mapand:array. - A map without
:typegroups keys:{:autoVerify {:onEdit {:type :string}}}. :secret? truemasks the value when ECA shows the config.- Entry names of a
:mapkey stay strings, as inmcpServers.
ECA warns about config keys that neither it nor a loaded module knows, at startup and in /doctor.
Tools#
:tools maps each tool's name to its definition:
| Key | |
|---|---|
:description |
Tells the model what the tool does. |
:parameters |
The tool's arguments, as a JSON Schema. |
:handler |
(fn [ctx args]), with string keys in args. Returns a string, (eca/text s) or (eca/error s). |
:enabled-fn |
Optional (fn [ctx]). The model gets the tool only when it returns true. |
:require-approval-fn |
Optional (fn [ctx args]). When it returns true, ECA asks before running the call, even if your config allows the tool. |
:summary-fn |
Optional (fn [ctx args]). The short text editors show for the call. |
:approval-keys-fn |
Optional (fn [args]). What "approve and remember" remembers, like each command of a shell call. |
:replaces |
Optional. A built-in tool it takes over, like "eca__read_file". |
A tool's full name is <prefix>__<name>: jira__issue above. The prefix is the name in the module's :id, unless the module sets :prefix. Use the full name in approval rules and disabledTools, or just jira for all the module's tools. Like any tool, ECA asks before running it unless your config allows it.
Commands#
:commands maps each command's name to its definition:
| Key | |
|---|---|
:handler |
(fn [ctx args]). args has each declared argument as a keyword, plus :raw, the text after the command. |
:description |
Shown in the command list. |
:arguments |
Optional. [{:name "key" :required true}]. |
The handler returns a string to show in the chat, {:prompt "..."} to send a message to the model as if you typed it, or nil.
You type /name, or /<prefix>:<name> when two modules have a command with the same name.
Events#
Event handlers let a module react to what ECA does, change it, or give an answer ECA asks for. This one rejects writes to .env files:
(def module
{:id :acme/guard
:event-handlers
{:tool/before-call {:match {:tool "eca__write_file"}
:handler (fn [_ctx {:keys [arguments]}]
(when (re-find #"\.env$" (get arguments "path" ""))
{:block "Writing .env files isn't allowed."}))}}})
There are three kinds of events:
- notify: something happened.
- transform: the handler can change the event, or stop it with
{:block "reason"}or{:stop "reason"}. - handle: the first handler that answers wins.
A handler is a function (fn [ctx event]), a vector of them, or a map with :handler and optional :match, :priority and :timeout-ms. :match filters events by key, with a value, a set or a regex. Module handlers run before your shell hooks; when a module blocks or stops something, the hooks don't run.
Every chat event also has :chat-id, :agent, :model, :variant, and :parent-chat-id in a subagent's chat.
| Event | Kind | Keys | Changes it accepts |
|---|---|---|---|
:session/started |
notify | :workspaces |
— |
:session/ended |
notify | — | — |
:chat/input |
transform | :text, :source (:user or :module). Runs before commands and skills. |
:text, :handled (a reply, or true: the model isn't called) |
:chat/started |
notify | :resumed?. Runs on a chat's first prompt. |
:additional-context (added to the chat's startup context), :stop |
:chat/before-request |
transform | :prompt. Runs once per user message. |
:prompt, :additional-context, :block, :stop |
:llm/before-request |
transform | :request (:model, :instructions, :past-messages, :user-messages, :tools), :history, :since-checkpoint, :turn, :budget. Runs before every LLM call of a chat, tool loop included. |
:request's messages, :instructions (not in the tool loop) and :tools. This changes what the model sees, not the chat history. :model can't change yet. |
:llm/request-failed |
handle | :request (:model, :user-messages), :error (:type like :context-overflow, :message, :status). Runs after ECA's own retries, once per turn. |
{:retry true} to send it again, or {:compact {:trigger :instructions}} to compact and resume. Without an answer, ECA handles the error. |
:chat/after-request |
notify | :response |
:follow-up (sent as the next user message), :stop |
:chat/message-added |
notify | :message (:role, :content) |
— |
:chat/title |
handle | :messages, :retitle? |
{:title ".."}, used instead of the model's |
:chat/ended |
notify | :chat-id, :agent, :model |
— |
:chat/status-changed |
notify | :status, :pending-approval-ids, :running-tool-ids... |
— |
:chat/usage-updated |
notify | :usage, :session-tokens, :context-limit, :context-used |
— |
:tool/before-call |
transform | :tool (full name), :server, :arguments, :approval, :tool-call-id |
:arguments, :approval, :reason, :block, :stop, :additional-context (added to the call's output for the model) |
:tool/after-call |
transform | :tool, :server, :arguments, :result, :error?, :tool-call-id. Runs before the model, the editor and postToolCall hooks see the result. |
:result (a result map or a string), :additional-context (for the model only), :stop (after this batch of calls) |
:chat/before-compact |
transform | :trigger (:manual, :auto or :overflow), :instructions |
:block (skips the compaction), :stop, :instructions |
:chat/compact |
handle | :trigger, :instructions. Asked before eca/compact-as-summary writes its summary. |
{:status :done :resume? true :message ".."}, or :status :failed. Write the result with checkpoint!. An interrupted turn goes on unless :resume? false. |
:chat/after-compact |
notify | :trigger, :checkpoint (:marker, :messages). Runs after a checkpoint is written. |
:additional-context (added to the summary), :stop |
:shell/before-exec |
transform | :command, :cwd, :env, :timeout-ms, :background?, :source (:tool, :hook or :module, named by :tool, :hook and :hook-type, or :module). Runs for every shell command ECA runs: shell, git and custom tools, shell hooks and eca/shell!. |
:command, :cwd, :env |
:config/resolve |
transform | :config. Runs after all config layers merge; read the config from the event, not through eca.api. |
:config, except its modules key |
:models/resolve |
transform | :models: full model name → capabilities |
:models |
A module can also define its own events in :defines and send them with (eca/emit! ctx event-id event).
Module keys#
| Key | |
|---|---|
:id |
Required. A qualified keyword like :acme/jira. :eca/* is for built-in modules. |
:description, :version |
Shown in /modules. |
:api |
The module API version it's written for: 1. |
:requires |
Ids of modules it needs. It starts after them, and stays off without them. |
:priority |
Order of its event handlers, higher first. 0 by default. |
:default-enabled? |
false ships the module turned off. |
:config-spec |
The config keys it owns. See Config. |
:config-defaults |
Defaults for keys other modules own, like an approval rule for its tool: {:toolCall {:approval {:allow {"jira__issue" {}}}}}. |
:start, :stop |
(fn [ctx]). What :start returns is the module's state, (eca/state ctx). |
:tools, :commands |
See Tools and Commands. |
:prefix |
Prefix of its tools and commands. The name in :id by default. |
:system-prompts |
Sections it adds to the system prompt: [{:id :acme/conventions :after :eca/rules :content "..."}]. :content is a string or (fn [ctx]). :before or :after place it next to another section; :dynamic? true is for content that changes every turn. |
:event-handlers |
{event-id handler}. See Events. |
:defines |
Events it offers other modules: {:events {:acme/approved {:kind :notify :doc ".."}}}. |
:providers, :apis |
Model providers and LLM APIs. |
eca.api#
Every function takes the module's ctx as its first argument.
| Area | Functions |
|---|---|
| Module | config, state, module-id, client, log, workspaces |
| Chat | chat, messages ({:since-checkpoint? true} for what the model sees), chat-state and swap-chat-state! (plain data saved with the chat), send-prompt!, add-context! (for the next request), checkpoint! ({:messages [..] :label ".." :data {..}}: the model sees only what comes after it), request-compaction! ({:trigger :instructions}: compacts before the next LLM request) |
| User | notify!, progress! (in a tool), ui?, confirm, select, input |
| Events | emit! |
| Tools and commands | tools, call-tool!, register-tool!, unregister-tool!, register-command!, unregister-command! |
| LLM | complete!, models. /costs and /modules show each module's LLM calls. |
| Runtime | background!, data-dir, shell! |
| Results | text, error |
Other namespaces:
eca.api.json:parse,generate.eca.api.http:request,get,post,put,delete, through ECA's HTTP client, with its proxy and TLS settings.eca.api.fs:read-file,write-file!,exists?,directory?,list-dir,glob,create-dirs!,delete!,path,parent,file-name,last-modified-ms.eca.api.test:mock-ctx,run-tool,run-command,run-event,recorded, to test a module outside ECA's session.eca.api.dev:ctx,modules,install!,uninstall!,reload!, for the REPL.eca.api.debug:db,chat-raw,config-raw,trace!,untrace!. Read-only, and only while the REPL runs.
Inside SCI, modules can also use clojure.string, clojure.set, clojure.walk and clojure.edn, and Java interop on basic classes like String, Integer, Double and Exception. There's no slurp or spit: use eca.api.fs and eca/shell!.
A REPL on ECA#
The eca/repl module runs an nREPL inside ECA. Turn it on with /modules enable eca/repl, or with "modules": {"eca/repl": {"enabled": true}} in your config.
Each ECA picks a free port and tells the model, so you can ask ECA to use its own REPL, e.g. "check in your REPL why my module failed". It needs an nREPL client it can run, like brepl.
To connect your editor, /repl shows the port; repl.port sets a fixed one. It only accepts local connections.
Limits#
- Event handlers of external modules time out after 10 seconds, unless they set
:timeout-ms.:startgets 30 seconds. - A module that fails 5 times in a row is turned off for the session, and you're told.
printlnin a module goes to ECA's log, not to the editor.