Tools¶
Tool model¶
EAA tools are stateful Python objects derived from BaseTool. Tool methods are
exposed by decorating them with @tool(name=...).
A typical tool looks like this:
from typing import Annotated
from eaa_core.tool.base import BaseTool, tool
class ExampleTool(BaseTool):
@tool(name="example_tool.add")
def add(
self,
a: Annotated[float, "The first addend."],
b: Annotated[float, "The second addend."],
) -> float:
return a + b
When a BaseTool instance is created, it discovers decorated methods and builds
exposed_tools metadata. The task manager registers that metadata with its
SerialToolExecutor.
Tool execution normalizes every result into a JSON object:
- dictionary returns are passed through
- scalar returns are wrapped as
{"result": ...} - string or list outputs that look like image paths are surfaced as
{"img_path": ...} image_pathandimage_pathskeys are normalized toimg_path
Image-producing tools should return {"img_path": "/path/to/image.png"} or a
list of image paths under img_path.
Registering tools¶
Pass tools at construction time:
task_manager = BaseTaskManager(
llm_config=llm_config,
tools=[example_tool],
)
Register additional tools after construction with the task-manager API:
task_manager.register_tools(extra_tool)
task_manager.register_tools([first_tool, second_tool])
Tool names must be unique across model-visible exposed methods. If a new tool would duplicate already registered exposed tool names, it is skipped.
Payload contracts¶
The LLM-driven workflows of EAA does not enforce strict contracts with tools. However, if a tool yields images that the LLM agent should see, or produces large data arrays that should be used by the agent for subsequent analysis, adhering to certain contracts can be make the image or data passing easier.
Image paths¶
To let the agent "see" an image produced by the tool, save the image as a
PNG file in the tool and return a JSON object with an img_path field:
{
"img_path": "/path/to/image.png"
}
Data arrays¶
If the agent needs to access data arrays generated by a tool, the tool should save the data as an NPY or TIFF (for 2D image data) file and return the path. If the tool is an MCP or HTTP server, ensure the path is accessible by the agent process.
Getting data buffer in logic-driven workflows¶
A logic-driven task manager should wrap MCP tool clients using MCPRPCWrapper
(see Using external MCP servers). To use the buffer
sync functionality of the wrapper, the MCP server should expose a get_attribute_payload
tool. See Creating MCP Servers for more details.
Thread safety and serial execution¶
EAA intentionally executes tools serially through SerialToolExecutor.
Why this matters:
- many experiment tools are stateful
- tool calls often mutate instrument state
- parallel tool calls would make ordering and rollback ambiguous
The executor therefore runs assistant-requested tool calls one at a time and records a normalized tool message for each result. This is the default safety model for the current codebase.
Approval gates¶
Each tool instance can require approval by setting require_approval=True. A
decorated method can also supply its own require_approval value or predicate.
When a tool call needs approval, the task manager routes the decision through
its normal input path, including the WebUI path when use_webui=True.
tool = ExampleTool(require_approval=True)
task_manager.register_tools(tool)
Built-in tools¶
BaseTaskManager creates a ToolManager at task_manager.tool_manager. By
default, the tool manager registers user-provided tools first, then these
built-in tools:
simple_python_eval_tool.evaluate_python_expression: evaluates simple Python expressionsfile_system_tool.read_file,write_file,edit_file,replace_file_lines: workspace-aware text file operationsimage_rendering_tool.render_image_for_agent: renders PNG, JPEG, TIFF, and first-page PDF files for model inspectionimage_captioning.toggle_auto_image_captioner: toggles automatic captions for images returned by toolspython_coding_tool.execute: runs Python source in a subprocessbash_coding_tool.execute: runs Bash source in a subprocessuv_tool.uv: runs boundeduvcommands from a configured working directorysubagent_tool.launch_subagent,list_registered_task_managers,launch_subtask_manager: launches delegated chat agents or registered task-manager workflows
Disable built-in tools through the tool manager:
task_manager.tool_manager.disable_file_system_tool()
task_manager.tool_manager.disable_image_rendering_tool()
task_manager.tool_manager.disable_workspace_tool()
task_manager.tool_manager.disable_python_coding_tool()
task_manager.tool_manager.disable_bash_coding_tool()
task_manager.tool_manager.disable_coding_tool()
task_manager.tool_manager.disable_uv_tool()
task_manager.tool_manager.disable_subagent_tool()
task_manager.tool_manager.disable_image_captioning()
task_manager.tool_manager.disable_simple_python_eval_tool()
Workspace tools¶
Workspace tools are provided by eaa_core.tool.workspace.
FileSystemTool accepts:
workspace_path: root used to resolve relative pathsread_whitelist_paths: additional trusted directories that do not require approval for file-path operations in the current implementationrequire_approval: fallback approval setting
ImageRenderingTool accepts:
workspace_path: root used to resolve relative image pathsrender_directory: directory where temporary rendered PNGs are writtenrequire_approval: fallback approval setting
UvTool accepts:
working_directory: directory whereuvcommands runrequire_approval: fallback setting, although the exposeduv_tool.uvmethod always requires approval
To replace default workspace tool handles, pass configured instances in the task-manager constructor:
from eaa_core.tool.workspace import FileSystemTool, ImageRenderingTool, UvTool
workspace_tool = FileSystemTool(
workspace_path="/data/beamtime/run-42",
read_whitelist_paths=[
"/data/beamtime/run-42/skills",
"/data/shared/reference-docs",
],
)
image_tool = ImageRenderingTool(
workspace_path="/data/beamtime/run-42",
render_directory="/data/beamtime/run-42/.tmp/rendered_images",
)
uv_tool = UvTool(working_directory="/data/beamtime/run-42")
task_manager = BaseTaskManager(
llm_config=llm_config,
tools=[workspace_tool, image_tool, uv_tool],
skill_dirs=["/data/beamtime/run-42/skills"],
)
You can also disable a default tool and register a replacement after construction:
task_manager.tool_manager.disable_file_system_tool()
task_manager.register_tools(
FileSystemTool(
workspace_path="/data/beamtime/run-42",
read_whitelist_paths=["/data/shared/reference-docs"],
)
)
Coding tools¶
The Python and Bash coding tools are enabled by default and require approval by
default. Configure approval through ToolManager:
task_manager.tool_manager.set_coding_tool_request_approval(False)
The same setting is available inside chat:
/setcodingtoolapproval false
Configure sandboxing for both coding tools:
task_manager.tool_manager.set_coding_tool_sandbox_type(
"bubblewrap",
visible_dirs=["/data/shared", "/data/beamtime/run-42"],
)
Supported sandbox values are None, "bubblewrap", and "container". The
chat command equivalent is:
/setcodingtoolsandboxtype bubblewrap /data/shared /data/beamtime/run-42
Use none in the chat command to execute directly on the host:
/setcodingtoolsandboxtype none
Subagent tool¶
The default subagent tool exposes two patterns.
subagent_tool.launch_subagent creates a fresh BaseTaskManager that inherits
the parent LLM config, memory config, registered tools except the subagent tool,
and skill directories. It runs one delegated chat task and returns the final
assistant response.
subagent_tool.launch_subtask_manager runs a pre-registered task manager's
run() method. Register those managers through the tool handle:
analysis_manager = MyAnalysisTaskManager(
name="analysis",
llm_config=llm_config,
tools=[analysis_tool],
)
task_manager.tool_manager.subagent_tool.add_task_managers(analysis_manager)
Registered task managers must have a non-empty unique name and a callable
run method. The model can inspect the registry with
subagent_tool.list_registered_task_managers and launch one with:
{
"task_manager_name": "analysis",
"task_manager_kwargs": {
"sample_id": "sample-17"
}
}
Subagents created by launch_subagent are marked with is_subagent=True, so
they do not receive another subagent-launching tool.
Image captioning¶
image_captioning.toggle_auto_image_captioner updates
task_manager.auto_image_captioner_enabled. When enabled, image-bearing tool
results are followed by an automatic image-captioning step using the task
manager's main chat model.
Disable the control tool if you do not want the model to toggle this behavior:
task_manager.tool_manager.disable_image_captioning()
Serving tools as MCP servers¶
EAA can expose any BaseTool instance as an MCP server by wrapping it with the
helpers in eaa_core.tool.mcp_server.
from eaa_core.tool.example_calculator import CalculatorTool
from eaa_core.tool.mcp_server import run_mcp_server_from_tools
run_mcp_server_from_tools(
tools=CalculatorTool(),
server_name="Calculator MCP Server",
)
The MCP wrapper preserves the normalized EAA JSON result contract by publishing an object output schema.
Using external MCP servers¶
EAA can also consume remote MCP tools through MCPTool in
eaa_core.tool.mcp_client. The wrapper connects to one or more MCP servers
using a FastMCP-compatible configuration and exposes the remote tools through
the normal BaseTool interface.
To create MCP tool servers that control instruments at experiment endstations, we strongly recommend following the async-safe server pattern to avoid issues related to thread-safety and async event loop conflicts. Find more details in Creating MCP Servers.
from eaa_core.tool.mcp_client import MCPTool
mcp_tool = MCPTool(
{
"mcpServers": {
"image_acquisition": {
"command": "python",
"args": ["./image_acquisition_mcp_server.py"],
}
}
}
)
task_manager.register_tools(mcp_tool)
To connect to an MCP server over HTTP from a different machine, configure the
client with a named entry under mcpServers and point it at the server's MCP
endpoint:
from eaa_core.tool.mcp_client import MCPTool
mcp_tool = MCPTool(
{
"mcpServers": {
"calculator": {
"url": "http://SERVER_IP:8050/mcp",
"transport": "http",
}
}
}
)
The server side should be started with HTTP transport enabled, for example:
from eaa_core.tool.example_calculator import CalculatorTool
from eaa_core.tool.mcp_server import run_mcp_server_from_tools
run_mcp_server_from_tools(
tools=CalculatorTool(),
server_name="Calculator MCP Server",
transport="http",
host="0.0.0.0",
port=8050,
path="/mcp",
)
Do not pass only {"url": ..., "transport": "http"} to MCPTool. The FastMCP
client expects a full config object with one or more named servers inside
mcpServers.
Calling MCP tools from logic-driven task managers¶
MCPTool is a BaseTool wrapper for model-visible tool calls. Logic-driven
task managers sometimes need the opposite shape: fixed Python method calls such
as acquire_image(y=..., x=...) or set_parameters([...]), while the remote
MCP server exposes facility-specific names and argument names.
Use MCPRPCWrapper from eaa_core.tool.mcp_adapter for that case. It is not
itself a model-visible BaseTool; it is a small RPC-style adapter around an
MCPTool client for direct Python calls.
from eaa_core.tool.mcp_adapter import MCPRPCWrapper
from eaa_core.tool.mcp_client import MCPTool
mcp_client = MCPTool(
{
"mcpServers": {
"beamline": {
"url": "http://BEAMLINE_HOST:8050/mcp",
"transport": "http",
}
}
}
)
acquisition = MCPRPCWrapper(
mcp_tool_client=mcp_client,
mappings={
"acquire_image": {
"remote": "collect_detector_frame",
"arguments": {
"position_y": "stage_y",
"position_x": "stage_x",
"fov_size": "field_of_view_um",
},
"sync": {
"last_image": "detector.last_frame",
"last_metadata": "detector.last_metadata",
},
}
},
)
result = acquisition.acquire_image(
position_y=10.0,
position_x=20.0,
fov_size=5.0,
)
image = acquisition.last_image
metadata = acquisition.last_metadata
Each mapping key is the local method name used by the task manager. The
remote value is the MCP tool to call. MCPRPCWrapper accepts either the exact
remote name or a uniquely matching dotted suffix, so get_attribute_payload
can match a remote tool named detector.get_attribute_payload if that suffix is
unique.
The optional arguments dictionary maps local argument names to remote MCP
argument names. Arguments not listed there are forwarded with their local name.
NumPy arrays and NumPy scalars are converted to JSON-serializable values before
the MCP call.
The optional sync dictionary maps local wrapper attribute names to remote
server-side attribute names. After a successful call, the wrapper calls the
remote get_attribute_payload(name=...) support tool and stores decoded values
on the wrapper. Array payloads encoded with BaseTool.encode_array_payload() or
the compatible format in
Creating MCP Servers are
decoded back to NumPy arrays.
For parameter-tuning style workflows, MCPRPCWrapper also has local helper
methods for parameter_history, and the module provides
set_parameters_with_local_history:
from eaa_core.tool.mcp_adapter import MCPRPCWrapper, set_parameters_with_local_history
parameter_tool = MCPRPCWrapper(
mcp_tool_client=mcp_client,
mappings={
"set_parameters": {
"remote": "set_mirror_voltages",
"arguments": {"parameters": "voltages"},
}
},
initial_attributes={
"parameter_names": ["m1", "m2"],
"parameter_history": {"m1": [], "m2": []},
},
local_methods={"set_parameters": set_parameters_with_local_history},
)
parameter_tool.set_parameters([0.1, 0.2])
latest = parameter_tool.get_parameter_at_iteration(-1)
Notes:
- EAA normalizes tool results to JSON for tools served through the EAA MCP server helper.
- Arbitrary third-party MCP servers may still return non-EAA payloads, so the
agent loop only treats results as image-bearing when an
img_pathis present. - Chat interactions and agent-selected tool calls can use facility-specific MCP
tool names directly. Logic-driven task managers with fixed Python calls should
use
MCPRPCWrapperwhen they need local-to-remote tool and argument mapping.