Extensions
June 9, 2026 ยท View on GitHub
Extensions are the plugin system for Lemon. They allow you to add custom tools, hooks, and capabilities to the coding agent without modifying the core codebase.
Quick Start
- Create a file in
~/.lemon/agent/extensions/(global) or.lemon/extensions/(project-local) - Implement the
CodingAgent.Extensions.Extensionbehaviour - Trust the directory with
[runtime.extensions] auto_load_default_paths = true, or add an explicit[runtime] extension_paths = [...]entry - The extension will be loaded on session start
By default, Lemon scans default extension directories for diagnostics and
manifest metadata without executing third-party code. Code execution requires
either explicit extension_paths configuration or
auto_load_default_paths = true for the default global/project directories.
Set [runtime.extensions] enabled = false to disable all extension code
execution while keeping manifest diagnostics and support-bundle visibility.
Extension Behaviour
Every extension must implement the CodingAgent.Extensions.Extension behaviour with at least name/0 and version/0:
defmodule MyExtension do
@behaviour CodingAgent.Extensions.Extension
@impl true
def name, do: "my-extension"
@impl true
def version, do: "1.0.0"
# Optional: provide custom tools
@impl true
def tools(_cwd), do: []
# Optional: register hooks
@impl true
def hooks, do: []
end
Callbacks
| Callback | Required | Description |
|---|---|---|
name/0 | Yes | Returns the extension's unique name (lowercase with hyphens) |
version/0 | Yes | Returns the semantic version string |
tools/1 | No | Returns a list of AgentCore.Types.AgentTool structs |
hooks/0 | No | Returns a keyword list of event hooks |
capabilities/0 | No | Returns a list of capability atoms (e.g., [:tools, :hooks]) |
config_schema/0 | No | Returns a JSON Schema-like map for configuration options |
providers/0 | No | Returns a list of provider specifications for registration |
Providing Tools
Extensions can provide custom tools that the agent can use:
@impl true
def tools(_cwd) do
[
%AgentCore.Types.AgentTool{
name: "my_tool",
description: "What the tool does",
parameters: %{
"type" => "object",
"properties" => %{
"input" => %{
"type" => "string",
"description" => "The input parameter"
}
},
"required" => ["input"]
},
label: "My Tool",
execute: fn _id, %{"input" => input}, _signal, _on_update ->
%AgentCore.Types.AgentToolResult{
content: [%{type: "text", text: "Result: #{input}"}]
}
end
}
]
end
Tool Parameters
name- The tool identifier (used in LLM tool calls)description- What the tool does (shown to the LLM)parameters- JSON Schema for tool parameterslabel- Human-readable display nameexecute- Function with signature(tool_use_id, args, abort_signal, on_update) -> AgentToolResult
Tool Execute Function
The execute function receives:
tool_use_id- Unique ID for this tool invocationargs- Map of parsed arguments matching your parameters schemaabort_signal- AnAbortSignalfor checking if execution should stopon_update- Callback for streaming partial updates
Return an AgentCore.Types.AgentToolResult:
%AgentCore.Types.AgentToolResult{
content: [%{type: "text", text: "output"}],
is_error: false # optional, defaults to false
}
Registering Hooks
Hooks allow extensions to respond to agent lifecycle events:
@impl true
def hooks do
[
on_agent_start: fn -> :ok end,
on_agent_end: fn messages -> :ok end,
on_turn_start: fn -> :ok end,
on_turn_end: fn message, tool_results -> :ok end,
on_message_start: fn message -> :ok end,
on_message_end: fn message -> :ok end,
on_tool_execution_start: fn id, name, args -> :ok end,
on_tool_execution_end: fn id, name, result, is_error -> :ok end
]
end
Available Hooks
| Hook | Arguments | Description |
|---|---|---|
on_agent_start | none | Called when agent run starts |
on_agent_end | messages | Called when agent run ends |
on_turn_start | none | Called when a new turn starts |
on_turn_end | message, tool_results | Called when turn ends |
on_message_start | message | Called when message processing starts |
on_message_end | message | Called when message processing ends |
on_tool_execution_start | id, name, args | Called when tool starts |
on_tool_execution_end | id, name, result, is_error | Called when tool ends |
Hook errors are caught and logged but don't stop other hooks or the agent.
Declaring Capabilities
Extensions can declare their capabilities for discovery and filtering:
@impl true
def capabilities, do: [:tools, :hooks]
Common Capability Atoms
| Capability | Description |
|---|---|
:tools | Extension provides custom tools |
:hooks | Extension provides event hooks |
:prompts | Extension provides custom prompts/skills |
:resources | Extension provides resources (CLAUDE.md, etc.) |
:mcp | Extension connects to MCP servers |
UIs can use capabilities to filter extensions by functionality.
Configuration Schema
Extensions can declare a configuration schema to enable UIs to render settings forms:
@impl true
def config_schema do
%{
"type" => "object",
"properties" => %{
"api_key" => %{
"type" => "string",
"description" => "API key for the service",
"secret" => true
},
"timeout" => %{
"type" => "integer",
"description" => "Request timeout in milliseconds",
"default" => 5000
},
"enabled" => %{
"type" => "boolean",
"description" => "Enable this extension",
"default" => true
}
},
"required" => ["api_key"]
}
end
The schema follows JSON Schema conventions with optional extensions:
secret: true- Indicates the field should be masked in UIsdefault- Default value for the field
Registering Providers
Extensions can register custom providers that integrate with the core system.
Model providers add AI model backends. Memory providers implement
LemonCore.MemoryProvider and can participate in safety-screened memory ingest
and scoped search_memory fan-out through LemonCore.MemoryProviders.
@impl true
def providers do
[
%{
type: :model,
name: :my_custom_model,
module: MyExtension.CustomModelProvider,
config: %{api_key_env: "MY_API_KEY"}
},
%{
type: :memory,
name: :team_memory,
module: MyExtension.TeamMemoryProvider
}
]
end
Provider Specification Fields
| Field | Type | Description |
|---|---|---|
type | atom or string | The provider type (:model / "model" or :memory / "memory") |
name | atom or string | Unique identifier for the provider |
module | module | The module implementing the provider behaviour |
config | map | Optional configuration passed to the provider |
Provider Registration Process
- At session startup: Model providers are collected and registered into
Ai.ProviderRegistry; memory providers are collected and registered intoLemonCore.MemoryProviders - On extension reload: Old providers are unregistered, extensions are reloaded, and new providers are registered
- Conflict detection: When multiple extensions register the same provider name, the first (alphabetically by module name) wins
Provider Precedence
Similar to tools, providers follow a precedence order:
- Built-in providers always win - Core model providers and the built-in local memory provider take priority
- First loaded extension wins - Extensions are sorted alphabetically by module name
Provider Conflicts in Status Report
Provider registration conflicts are included in the extension status report:
%{
# ... other fields ...
provider_registration: %{
registered: [
%{type: :model, name: :custom_model, module: MyProvider, extension: MyExtension}
],
conflicts: [
%{
type: :model,
name: :conflicting_model,
winner: ExtensionA, # or :builtin if shadowed by core
shadowed: [ExtensionB, ExtensionC]
}
],
total_registered: 1,
total_conflicts: 1
}
}
Implementing a Model Provider
Model providers must implement the Ai.Provider behaviour:
defmodule MyExtension.CustomModelProvider do
@behaviour Ai.Provider
@impl true
def stream(model, context, opts) do
# Streaming implementation
{:ok, event_stream}
end
@impl true
def provider_id, do: :my_provider
@impl true
def api_id, do: :my_custom_model
end
See the Ai.Provider module documentation for full details on implementing providers.
Memory providers must implement the LemonCore.MemoryProvider behaviour:
defmodule MyExtension.TeamMemoryProvider do
@behaviour LemonCore.MemoryProvider
@impl true
def put(%LemonCore.MemoryDocument{} = doc, opts) do
# Store a safety-screened memory document in your backend.
:ok
end
@impl true
def search(query, opts) do
# Return a list of LemonCore.MemoryDocument structs.
[]
end
end
Memory provider config supports:
| Field | Description |
|---|---|
enabled | Set to false to register but keep the provider disabled |
scopes | Optional list of session, agent, workspace, and all scopes |
timeout_ms | Per-provider search/ingest timeout |
label | Human-readable provider label for local diagnostics only |
LemonCore.MemoryProviders always keeps local SQLite as the built-in provider,
isolates provider failures, deduplicates search results by document id, and
reports only redacted provider shape through memory.status and
support bundles.
Extension Discovery
Extension manifests are discovered from:
- Global directory:
~/.lemon/agent/extensions/ - Project-local directory:
.lemon/extensions/(relative to working directory)
Extension code is loaded from:
- Explicit runtime paths:
[runtime] extension_paths = ["./trusted-extensions"] - Default directories only when
[runtime.extensions] auto_load_default_paths = true
Supported file patterns:
*.exand*.exsfiles in the extensions directory*/lib/**/*.exfiles for complex extensions with subdirectories
[runtime]
extension_paths = ["./trusted-extensions"]
[runtime.extensions]
enabled = true
auto_load_default_paths = false
The same default-directory switch can be set with
LEMON_EXTENSIONS_AUTO_LOAD_DEFAULT_PATHS=true. Set
LEMON_EXTENSIONS_ENABLED=false as an emergency stop for extension code
execution. Treat extension_paths as an explicit trust boundary: Lemon can
compile and execute files in those directories when extensions are enabled.
Extension Manifests
Extension packages may include lemon_extension.json, extension.json, or
.lemon-extension.json at the extension root or one directory below an
extension directory. Manifests are metadata only: Lemon support diagnostics read
them as JSON without compiling or loading extension code.
Supported manifest fields:
{
"schema_version": 1,
"name": "my-extension",
"version": "1.0.0",
"capabilities": ["tools", "hooks", "memory_provider"],
"providers": [
{"type": "model", "name": "custom-model"},
{"type": "memory", "name": "team-memory"}
],
"host": {"type": "beam"},
"distribution": {"source": "git", "url": "https://example.com/my-extension.git"},
"audit": {"status": "pending"}
}
extension_diagnostics.json exposes only aggregate manifest
shape: valid/invalid counts, capability counts, provider-type counts, host-type
counts, distribution source counts, audit-status counts, and a redacted
host-runtime summary. The host-runtime summary distinguishes BEAM extension
loading, WASM sidecar configuration, and manifest-only MCP/external hosts, and
reports degraded or manifest-only counts without starting default-directory
plugin code. Raw manifest contents, source paths, plugin names, provider names,
distribution URLs, load-error messages, and file contents are not included in
support diagnostics.
Validate manifests before publishing or installing a package:
mix lemon.extension.validate path/to/extension
mix lemon.extension.validate --json path/to/extension
The validator accepts a manifest path or an extension directory, does not load extension code, and fails if required package fields are missing or unsupported capability-hosting metadata is declared.
BEAM Host Proof
The local BEAM extension-host proof exercises the runtime trust boundary without starting channel adapters or loading default directories implicitly:
mix run --no-start scripts/live_extension_host_smoke.exs
The proof creates a temporary project, confirms the default .lemon/extensions
directory is not executed without trust, loads an explicitly configured
extension path, executes an extension tool through CodingAgent.ToolRegistry,
verifies redacted extension tool execution telemetry, and confirms built-in
tools win namespace conflicts. It also proves [runtime.extensions] enabled = false blocks explicit-path extension execution without loading extension code.
It writes the redacted artifact
.lemon/proofs/extension-host-smoke-latest.json with aggregate counts only.
This is BEAM extension-host proof, not public registry, WASM, MCP, or sandbox
execution proof.
WASM Host Telemetry Proof
The WASM tool boundary also has a focused telemetry proof for the wrapper that invokes discovered WASM tools through the sidecar session:
MIX_ENV=test mix run scripts/live_wasm_telemetry_smoke.exs
The proof exercises successful execution, returned sidecar errors, and a dead
sidecar exit. It verifies [:coding_agent, :wasm, :tool, :start],
[:coding_agent, :wasm, :tool, :stop], and
[:coding_agent, :wasm, :tool, :exception] events with hashed WASM paths and
tool-call ids. The proof artifact is
.lemon/proofs/wasm-tool-telemetry-latest.json and records only aggregate
counts, host-boundary flags, and redaction booleans.
extensions.status exposes the same proof status, check status,
host-boundary flags, proof hash, and redaction summary.
This is wrapper telemetry proof for the WASM execution boundary. It is not public registry, broad sandbox policy, MCP, or marketplace parity proof.
WASM Policy Proof
The WASM policy proof exercises the default approval boundary for discovered WASM tools:
MIX_ENV=test mix run scripts/live_wasm_policy_smoke.exs
The proof verifies that WASM tools declaring http, tool_invoke, or exec
capabilities require approval by default, that tools without those risky
capabilities execute without approval, and that an explicit
approvals.<tool> = never policy can override the default. It writes
.lemon/proofs/wasm-policy-latest.json with aggregate counts, policy-boundary
flags, and redaction booleans.
This is a policy-wrapper proof. It does not prove full WASM runtime sandboxing, public registry review, or marketplace install/update flows.
WASM Lifecycle Proof
The WASM sidecar lifecycle proof exercises the lower-level supervised sidecar session boundary:
MIX_ENV=test mix run scripts/live_wasm_lifecycle_smoke.exs
The proof starts a temporary sidecar, discovers a tool, invokes it, reads
running status, stops the sidecar, and verifies discover/invoke telemetry uses
hashed session, cwd, and tool metadata. It writes
.lemon/proofs/wasm-lifecycle-latest.json with aggregate counts,
lifecycle-boundary flags, and redaction booleans.
This is per-session sidecar lifecycle proof. It does not prove full runtime sandboxing, public marketplace hosting, or broad WASM tool-package parity.
Registry Audit Proof
The registry audit proof exercises Lemon's code-free install/update review boundary for extension packages:
MIX_ENV=test mix run scripts/live_extension_registry_audit_smoke.exs
The proof validates a temporary extension registry index, classifies audited
packages as installable, blocks unaudited or blocked packages, detects a newer
audited update candidate, and verifies the registry audit does not execute
extension code. It writes
.lemon/proofs/extension-registry-audit-latest.json with aggregate counts,
registry-boundary flags, and redaction booleans.
This proves the registry metadata review workflow, not full marketplace distribution, sandboxed non-BEAM execution, or bundled plugin breadth.
Example Extension
See examples/extensions/hello_world_extension.ex for a complete working example.
defmodule HelloWorldExtension do
@behaviour CodingAgent.Extensions.Extension
@impl true
def name, do: "hello-world"
@impl true
def version, do: "1.0.0"
@impl true
def tools(_cwd) do
[
%AgentCore.Types.AgentTool{
name: "hello",
description: "Says hello to someone",
parameters: %{
"type" => "object",
"properties" => %{
"name" => %{"type" => "string", "description" => "Name to greet"}
},
"required" => ["name"]
},
label: "Hello",
execute: fn _id, %{"name" => name}, _signal, _on_update ->
%AgentCore.Types.AgentToolResult{
content: [%{type: "text", text: "Hello, #{name}!"}]
}
end
}
]
end
@impl true
def hooks do
[
on_agent_start: fn -> IO.puts("Agent started!") end
]
end
end
Tool Precedence
All tools (built-in and extension) are assembled through CodingAgent.ToolRegistry, which provides:
- Centralized conflict detection
- Per-session enable/disable via
:disabledand:enabled_onlyoptions - Extension path customization via
:extension_pathsoption
When multiple tools share the same name, the following precedence rules apply:
- Built-in tools always win - Core tools (read, write, edit, bash, etc.) take priority over extension tools
- First loaded extension wins - Extensions are loaded in alphabetical order by module name, so earlier modules take precedence
When a conflict is detected, a warning is logged that includes:
- The conflicting tool name
- The module that was shadowed
- Whether it was shadowed by a built-in or an earlier extension
Example warning:
[warning] Tool name conflict: extension tool 'read' from MyExtension is shadowed by built-in tool
This ensures deterministic tool resolution across sessions while allowing extensions to add new tools without risking silent conflicts.
Extension Metadata API
For UIs and diagnostics, the extension system provides functions to query loaded extension metadata:
# Get metadata for specific extensions
info = CodingAgent.Extensions.get_info(extensions)
# => [%{name: "my-ext", version: "1.0.0", module: MyExt, source_path: "/path/to/ext.ex",
# capabilities: [:tools, :hooks], config_schema: %{"type" => "object", ...}}]
# Get source path for a specific extension module
path = CodingAgent.Extensions.get_source_path(MyExtension)
# => "/home/user/.lemon/agent/extensions/my_extension.ex"
# List all loaded extensions (global view)
all_extensions = CodingAgent.Extensions.list_extensions()
# => [%{name: "ext1", version: "1.0.0", module: Ext1, source_path: "...", ...}, ...]
# Find duplicate tool names across extensions (before merging with built-ins)
duplicates = CodingAgent.Extensions.find_duplicate_tools(extensions, cwd)
# => %{"my_tool" => [ExtensionA, ExtensionB]}
Each extension metadata map includes:
name- The extension's name (fromname/0callback)version- The extension's version (fromversion/0callback)module- The Elixir module implementing the extensionsource_path- The file path from which the extension was loadedcapabilities- List of capability atoms (defaults to[]if not implemented)config_schema- JSON Schema-like map for configuration (defaults to%{}if not implemented)
Tool Conflict Report API
For plugin observability, the ToolRegistry provides a structured conflict report that shows how tool name conflicts are resolved and captures extension load failures:
report = CodingAgent.ToolRegistry.tool_conflict_report(cwd)
# => %{
# conflicts: [
# %{
# tool_name: "read",
# winner: :builtin,
# shadowed: [{:extension, MyExtension}]
# },
# %{
# tool_name: "custom_tool",
# winner: {:extension, ExtensionA},
# shadowed: [{:extension, ExtensionB}]
# }
# ],
# total_tools: 14,
# builtin_count: 13,
# extension_count: 1,
# shadowed_count: 2,
# load_errors: [
# %{
# source_path: "/path/to/broken_extension.ex",
# error: %CompileError{...},
# error_message: "Compile error: unexpected token"
# }
# ]
# }
The report includes:
conflicts- List of conflict entries, each containing:tool_name- The conflicting tool namewinner- Source that won (:builtinor{:extension, module()})shadowed- List of{:extension, module()}tuples that were shadowed
total_tools- Total number of tools available after conflict resolutionbuiltin_count- Number of built-in toolsextension_count- Number of extension tools (after shadowing)shadowed_count- Total number of shadowed toolsload_errors- List of extension load errors, each containing:source_path- Path to the file that failed to loaderror- The error exception/termerror_message- Human-readable error message
This is useful for:
- Debugging why a custom tool isn't appearing
- Building UIs that show plugin health/status
- Detecting extension conflicts before they cause issues
- Identifying broken extensions that failed to compile or load
Extension tool execution also emits redacted telemetry through
[:coding_agent, :extension, :tool, :start],
[:coding_agent, :extension, :tool, :stop], and
[:coding_agent, :extension, :tool, :exception]. Event metadata includes the
BEAM host label, tool name, hashed extension identity, hashed tool-call id,
status, duration, and redacted exception type. It does not include raw params,
raw call ids, source paths, or extension file contents.
WASM tool wrappers emit the matching redacted host telemetry through
[:coding_agent, :wasm, :tool, :start],
[:coding_agent, :wasm, :tool, :stop], and
[:coding_agent, :wasm, :tool, :exception]. Metadata is bounded to the WASM
host label, tool name, hashed WASM path, hashed tool-call id, status, duration,
and redacted exception type.
Control-Plane Status
Operators can inspect extension/plugin health without entering an agent session
through the read-only extensions.status control-plane method:
{
"method": "extensions.status",
"params": {
"cwd": "/path/to/project"
}
}
The method loads project/global extensions, reports loaded extension names,
versions, capabilities, config-schema presence, load/validation error counts,
tool-conflict resolution, extension-provided provider names, extension-host
execution telemetry proof shape, WASM wrapper telemetry proof shape, and WASM
status shape when available. It redacts raw source paths, load-error messages,
config schemas, provider modules, and path-like WASM metadata, replacing
sensitive path fields with hashes. LemonCore.Doctor.ExtensionDiagnostics
reports global, project, and configured extension directory existence,
extension-file counts, manifest counts and aggregate manifest shape, nested
library-file counts, and file/path hashes without loading plugin code or
exposing raw source paths, file contents, manifest contents, distribution URLs,
or load-error messages. When
.lemon/proofs/extension-host-smoke-latest.json exists, the same diagnostics
surface includes a redacted execution_telemetry summary with proof status,
proof hash, completed/failed counts, telemetry-check status, config/env
disabled-check status, disabled explicit-path block status, and redaction
booleans. When .lemon/proofs/wasm-tool-telemetry-latest.json exists, the
diagnostics also include a redacted wasm_telemetry summary with proof status,
proof hash, success/error/exception/redaction check status, host-boundary flags,
and redaction booleans. When .lemon/proofs/wasm-policy-latest.json exists,
diagnostics include a redacted wasm_policy summary with proof status,
approval-default check status, override status, policy-boundary flags, and
redaction booleans. When
.lemon/proofs/extension-registry-audit-latest.json exists, diagnostics include
a redacted registry_audit summary with install/update proof status,
installable/blocked/update counts, no-code-load status, proof hash, and
redaction booleans. When .lemon/proofs/wasm-lifecycle-latest.json exists,
diagnostics include a redacted wasm_lifecycle summary with discover/invoke,
status, stop, proof hash, lifecycle-boundary flags, and redaction booleans.
mix lemon.doctor --verbose includes extensions.telemetry,
extensions.wasm_telemetry, extensions.wasm_policy, and
extensions.registry_audit, and extensions.wasm_lifecycle checks backed by
these redacted summaries.
extensions.telemetry passes only when the latest extension-host smoke proof
completed the redacted start/stop/exception telemetry check and the config/env
disabled-mode execution block checks. extensions.wasm_telemetry passes only
when the latest WASM smoke proof completed success, sidecar-error,
sidecar-exit, and redaction checks. extensions.wasm_policy passes only when
the latest WASM policy proof completed risky-capability approval-default and
explicit-override checks. extensions.registry_audit passes only when the
latest registry audit proof validates the code-free index, blocks unaudited
installs, detects an audited update candidate, avoids code loading, and keeps
sensitive registry values redacted. extensions.wasm_lifecycle passes only
when the latest lifecycle proof completed discover/invoke telemetry, running
status, stop termination, and redaction checks.
Extension Status Report
At session startup, a comprehensive extension status report is built and published as an event. This provides a single source of truth for UI/CLI consumption about extension health.
Getting the Status Report
# From the session state
state = Session.get_state(session)
report = state.extension_status_report
# Or via the dedicated API
report = Session.get_extension_status_report(session)
Subscribing to the Status Report Event
The report is also published as an event after session initialization:
# Subscribe to session events
unsub = Session.subscribe(session)
receive do
{:session_event, _session_id, {:extension_status_report, report}} ->
IO.inspect(report, label: "Extension Status")
end
Status Report Structure
%{
# Successfully loaded extensions with metadata
extensions: [
%{
name: "my-extension",
version: "1.0.0",
module: MyExtension,
source_path: "/path/to/extension.ex",
capabilities: [:tools, :hooks],
config_schema: %{...}
}
],
# Extensions that failed to load
load_errors: [
%{
source_path: "/path/to/bad_extension.ex",
error: %CompileError{...},
error_message: "Compile error: unexpected token"
}
],
# Tool conflict report from ToolRegistry
tool_conflicts: %{
conflicts: [...],
total_tools: 14,
builtin_count: 13,
extension_count: 1,
shadowed_count: 0
},
# Provider registration report
provider_registration: %{
registered: [
%{type: :model, name: :custom_model, module: CustomProvider, extension: MyExtension}
],
conflicts: [],
total_registered: 1,
total_conflicts: 0
},
# Summary counts
total_loaded: 2,
total_errors: 1,
loaded_at: 1706745600000
}
Loading Extensions with Error Tracking
For programmatic use, you can load extensions and capture errors:
{:ok, extensions, load_errors, validation_errors} = CodingAgent.Extensions.load_extensions_with_errors([
"~/.lemon/agent/extensions",
"/path/to/project/.lemon/extensions"
])
# Register extension providers
provider_report = CodingAgent.Extensions.register_extension_providers(extensions)
# Build a status report manually
report = CodingAgent.Extensions.build_status_report(extensions, load_errors,
cwd: "/project",
provider_registration: provider_report
)
This is useful for:
- CLI status displays showing extension health
- Web UIs rendering extension management panels
- Diagnostic tools troubleshooting extension issues
- Build systems validating extension configurations
Extensions Status Tool
The agent has access to a built-in extensions_status tool that allows it to self-diagnose plugin loading issues, conflicts, and reload extensions during a session. This is useful for debugging when:
- An expected tool isn't available
- Extensions fail to load with syntax or compile errors
- Tool name conflicts prevent extension tools from being used
- Extensions have been added, modified, or removed during the session
Using the Tool
The agent can call the tool with optional action and include_details parameters:
# Summary view (default)
extensions_status {}
# Detailed view with source paths and modules
extensions_status {"include_details": true}
# Reload extensions (re-discover and refresh tool registry)
extensions_status {"action": "reload"}
# Reload with details
extensions_status {"action": "reload", "include_details": true}
Actions
| Action | Description |
|---|---|
status | Get the current extension status report (default) |
reload | Re-discover extensions from all paths, refresh the tool registry, and update the status report |
Output Format
The tool returns a markdown-formatted report including:
# Extension Status Report
- **Extensions loaded:** 2
- **Load errors:** 1
- **Loaded at:** 2024-01-31 15:30:00 UTC
## Loaded Extensions
- **my-extension** v1.0.0 (tools, hooks)
- **another-ext** v2.0.0
## Load Errors
- `/path/to/bad_extension.ex`
- Compile error: unexpected token at line 15
## Tool Registry
- **Total tools:** 17
- **Built-in:** 16
- **From extensions:** 1
- **Shadowed:** 0
When include_details: true, extension entries also show:
- Source file path
- Module name
- Whether the extension has a config schema
Reloading Extensions
The reload action allows the agent to refresh the extension system without restarting the session. This is useful when:
- New extension files have been added to the extensions directories
- Existing extensions have been modified
- Extensions have been removed and should no longer be loaded
When reload is triggered:
- All currently loaded extension modules are purged from the code server
- Extensions are re-discovered from all configured paths
- The tool registry is rebuilt with conflict detection
- The session's tools and hooks are updated
- A new
{:extension_status_report, report}event is broadcast
# Reload via tool (from within agent)
extensions_status {"action": "reload"}
# Reload via Session API (from external code)
{:ok, report} = CodingAgent.Session.reload_extensions(session)
Fallback Mode and Cached Errors
When the extensions_status tool is called without an active session context (e.g., no session_id or session not found), it operates in "fallback mode". In this mode, it retrieves:
- Loaded extensions from the global extension registry
- Tool conflicts computed for the current working directory
- Cached load errors from the most recent
load_extensions_with_errors/1call
This allows observability into extension issues even when no session context exists. The cached errors include:
- Source path of the failed extension
- Error type and message
- Timestamp of when extensions were last loaded
# Retrieve cached errors programmatically
{errors, loaded_at} = CodingAgent.Extensions.last_load_errors()
# => {[%{source_path: "/path/to/bad.ex", error_message: "Syntax error..."}], 1706745600000}
The fallback title in the tool result also includes error counts:
"2 loaded, 1 errors (no session context)"- when there are load errors"2 loaded, 1 conflicts (session not found)"- when there are tool conflicts"2 loaded, 1 errors, 1 conflicts (no session context)"- when both exist
Best Practices
- Use descriptive names - Tool names should clearly indicate what they do
- Provide helpful descriptions - The LLM uses descriptions to decide when to use tools
- Validate inputs - Check arguments before processing
- Handle errors gracefully - Return
is_error: truewith a helpful message - Keep hooks lightweight - Don't block the agent with slow hook processing
- Use the
cwdparameter - Make tools context-aware when needed - Use unique tool names - Avoid naming tools the same as built-ins or other extensions