LemonAgent.ToolRegistry (lemon_agent v0.1.0)

View Source

Runtime registry for agent tools contributed by apps outside the platform.

The built-in tool lists (CodingAgent.ToolRegistry, CodingAgent.Tools, LemonMcp.ToolAdapter) are compile-time and name the modules they include. That works for tools the platform owns, but a satellite integration — one the platform must not know about at compile time — has nowhere to appear. It registers here instead, at boot:

LemonAgent.ToolRegistry.register(:my_tool, MyIntegration.Tools.MyTool)

A registered module implements the same contract as a built-in: tool/1 and tool/2 returning an LemonAgent.Types.AgentTool — see that module for the tool shape and the execute contract. Registrations live in :persistent_term, so they survive a supervisor restart and can be made before the consuming app starts.

Precedence

Built-ins win. A registration whose name collides with a built-in is kept in the registry but ignored by the consumer that already has that name, so a satellite can never silently replace a platform tool. Consumers get this one of two ways, and both are equivalent: filtering with available/1 (CodingAgent.ToolRegistry), or merging their own built-ins last (CodingAgent.Tools, LemonMcp.ToolAdapter).

The rule is worth stating in terms of what it protects rather than as a merge order. CodingAgent.Tools.all_tools/2 is what get_tool/3 resolves through, so it decides which module actually runs when the model emits a call. The schema the model was shown for bash came from the built-in; if a registration could take that name, the thing that runs would not be the thing that was described. That is the failure the precedence rule exists to prevent, which is why it has to hold on every path rather than on the one a consumer happened to write carefully.

The satellite pattern

apps/x_api is the worked example. It is an ordinary umbrella app that the platform has no compile-time knowledge of: nothing in coding_agent or lemon_mcp names it. Its application callback registers its three tools at boot, guarded so the app still starts in a build where the agent runtime is absent:

defp register_tools do
  if Code.ensure_loaded?(LemonAgent.ToolRegistry) do
    Enum.each(@tools, fn {name, module} ->
      LemonAgent.ToolRegistry.register(name, module)
    end)
  end

  :ok
end

XApi.Tools.PostToX is then a plain tool module — tool/1, tool/2, execute/4 — that returns a "not configured" result rather than raising when its credentials are missing, so the tool is safe to register unconditionally. A package outside this repo plugs in exactly the same way; registering from the application callback is what makes ordering irrelevant.

Summary

Functions

Every registration, in registration order, filtered to modules that are actually loadable in this build.

Registrations excluding taken names, for a consumer merging them into its own built-in list.

Register module under name, replacing any previous registration for it.

Remove the registration for name.

Types

entry()

@type entry() :: {tool_name(), module()}

tool_name()

@type tool_name() :: atom()

Functions

all()

@spec all() :: [entry()]

Every registration, in registration order, filtered to modules that are actually loadable in this build.

Callers turn these into tools by calling module.tool/2, so an entry whose module is not in the build would be an UndefinedFunctionError at the call site rather than a missing tool. Registration order is preserved.

available(taken)

@spec available(taken :: [tool_name()]) :: [entry()]

Registrations excluding taken names, for a consumer merging them into its own built-in list.

register(name, module)

@spec register(tool_name(), module()) :: :ok

Register module under name, replacing any previous registration for it.

unregister(name)

@spec unregister(tool_name()) :: :ok

Remove the registration for name.