ChiaTool ======== ``ChiaTool`` is the primitive CHIA uses to allow an **AI agent** to orchestrate control of the flow. Where ``chia_remote(...)`` creates a node that your flow dispatches programmatically — a programmatic edge in the task graph — a ``ChiaTool`` stands up an `MCP `_ server on a worker that an *agent* can call on its own initiative. That is an **agentic edge**: you give the agent a set of tools and a prompt, and the agent decides which tools to call, when, and with what arguments. This page explains the MCP concepts the tool layer leans on, walks through a tool's lifecycle, shows how tools attach to an agent, and how to define your own tools. .. contents:: :local: :depth: 2 A few MCP concepts ------------------ CHIA tools speak the `Model Context Protocol `_ (MCP), the open standard agents use to discover and call external tools over HTTP. - **Tool (method)** — a single callable the agent can invoke, with a name and a docstring/type signature. The agent reads those to decide *when* to call the tool and *how* to fill in its arguments, so a tool method's docstring is part of its interface, not just documentation. - **Tool server** — a small HTTP server (FastAPI + uvicorn) that hosts one ``ChiaTool``'s methods and speaks MCP. CHIA deploys it onto a worker as a Ray **actor** so it persists across many agent calls (see :doc:`/user_guides/chia_function` for the actor/worker vocabulary). Its MCP endpoint is:: http://{host}:{port}/{name}/mcp - **Resources / placement** — like a node, a tool server is pinned onto a worker that advertises the resources it needs. You pass these as ``task_options`` (see :ref:`tool-placement`), so the agent's bash tool lands on, say, the worker that holds a Chipyard checkout. Anatomy of a tool ----------------- CHIA provides a library of ready-made tools and a base class, :mod:`chia.base.tools.ChiaTool`, for writing your own. A tool is a plain Python **class** that subclasses ``ChiaTool``, bundles the tool's configuration and state, and registers one or more **methods** as the callable tools the agent sees. :mod:`chia.base.tools.BashTool` is a representative example: it gives an agent a bash interface to the worker it is deployed on. The class holds the working directory and registers a single ``run_command`` method: .. code-block:: python class BashTool(ChiaTool): def setup(self, work_dir="/"): self.work_dir = work_dir self.mcp.add_tool(self.run_command, name=f"{self.name}_run_command") def run_command(self, command: str) -> str: """Run a bash command and return combined stdout/stderr.""" ... # runs in the container bt = BashTool(name="bash", work_dir="/home/", task_options={"resources": {"chipyard": 1}}) To create a ``ChiaTool``, you create a subclass which defines a ``setup()`` method where you register all of the tools with ``self.mcp.add_tool(...)``. The base class runs its own initializer behind the scenes — creating the ``self.mcp`` server object your tools attach to — then calls your ``setup()`` method to register the tools, then deploys the running server behind the scenes. The constructor of the child class forwards arguments to the setup call (like ``work_dir``) and takes two additional arguments: ``name`` and ``task_options``. The former is a human readable string used by the agent to reference the tool, and the latter is used to determine where to place the tool server, and includes things like resource specifications and placement groups. Alternatively, if you would rather write the constructor by hand, an :ref:`explicit initialization idiom ` is supported and can be used instead of ``setup()``. The tool lifecycle ------------------- Constructing a tool deploys it ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Unlike a node, which only runs when you call ``chia_remote``, **instantiating** a ``ChiaTool`` immediately stands up its server on a worker: once ``setup()`` has registered the tools, the base deploys the server automatically. So construction is the deploy step: .. code-block:: python from chia.base.tools.BashTool import BashTool chipyard_bash = BashTool( name = "chipyard_bash", work_dir="/home/ray/chipyard", task_options={"resources": {"chipyard": 1}}, # land on a chipyard worker ) # The MCP server is now live at # http://{chipyard_bash.hostname}:{chipyard_bash.port}/chipyard_bash/mcp Attaching a tool to an agent ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Pass the tool (or several) to any CHIA provided LLM or agent's ``prompt`` node via the ``tools`` argument. The agent receives each tool's MCP endpoint, discovers the methods, and calls them as needed to satisfy the prompt: .. code-block:: python from chia.base.ChiaFunction import get prompt = ( "Use the chipyard_bash tool to add a printf to the Rocket core's " "Chisel source that fires when instret reaches 1000." ) result = get(llm.prompt.chia_remote(llm, prompt, tools=[chipyard_bash])) print(result.result) Here the agent itself is a node — ``llm.prompt`` is dispatched with ``chia_remote`` — and the tool it was handed lets it reach out and act on the cluster while it runs. The :doc:`/getting-started/quickstart` builds up exactly this pattern, ending with an agent editing RTL through a ``BashTool``. Stopping a tool ~~~~~~~~~~~~~~~ A tool server stays running until you stop it. Call ``stop()`` once the agent work that needs it is done; it shuts uvicorn down and kills the actor, freeing the port and the worker slot: .. code-block:: python chipyard_bash.stop() .. _tools-calling-nodes: Bridging tools back to nodes ---------------------------- A tool method is ordinary Python, so it can dispatch ``@ChiaFunction`` nodes just like the rest of your flow. Because the agent expects a concrete return value (not an ``ObjectRef``), use :ref:`chia_remote_blocking `, which dispatches remotely and returns the unwrapped result. This is a composition point between the two primitives: the agent makes one tool call (an agentic edge), and that call fans out real, scheduled cluster work (programmatic edges) before returning a value the agent can reason about. Defining your own tool ----------------------- Subclass ``ChiaTool`` and define a ``setup()`` method that registers your tools, giving each method a clear docstring (which is given to the agent to describe the tool) and typed signature — the agent relies on both to call it correctly. :mod:`chia.base.tools.ChiaToolTemplate` is a copyable starting point. Here's an example: .. code-block:: python from chia.base.tools.ChiaTool import ChiaTool class GreetingTool(ChiaTool): def setup(self, greeting="Hello"): self.greeting = greeting self.mcp.add_tool(self.greet, name=f"{self.name}_greet") def greet(self, who: str) -> str: """Return a greeting for *who*. Args: who: The name to greet. """ return f"{self.greeting}, {who}! This is {self.name}." A tool method may return **any** type — MCP always serializes the result for the agent, falling back to a JSON (then ``str``) rendering of arbitrary objects. Annotating the method's return type (as ``greet`` does with ``-> str``) can allow MCP to provide the agent a schema for the return value in some cases. The following return types are formatted more cleanly and are preferred: - ``str`` — handed to the agent as plain text. - ``dict``, a pydantic ``BaseModel``, a ``TypedDict``, or a dataclass — returned as structured, machine-readable JSON. - ``list`` / ``tuple``. - ``None`` — an empty result. - MCP ``Image`` / ``Audio`` content objects (from ``mcp.server.fastmcp``) — for image or audio payloads, instead of raw ``bytes``. For tools that wrap long-running work, CHIA also provides asynchronous variants (:mod:`chia.base.tools.AsyncBashTool`, :mod:`chia.base.tools.AsyncJobTool`) that split a job into a *submit* method and a *status* method so the agent can poll rather than block. .. _tool-placement: Tool placement and resources ---------------------------- ``task_options`` is the dict of Ray scheduling constraints applied to the tool's server actor — the tool analog of a node's per-call ``.options(...)``. The most common entry is ``resources``, which pins the server onto a worker advertising the right capability: .. code-block:: python # Land the bash tool on a worker that has a chipyard checkout. BashTool("chipyard_bash", "/home/ray/chipyard", task_options={"resources": {"chipyard": 1}}) Because the server runs *on* that worker, anything the tool's methods do — run a command, read a file, dispatch a node — happens with that worker's filesystem and resources in reach. Omitting ``task_options`` lets the server land on any available worker. .. _explicit-init-idiom: Advanced -------- The explicit initialization idiom ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Before the ``setup()`` hook existed, every tool was written with an explicit ``__init__`` that called the base class's construction steps by hand. This style is **still fully supported**: define ``__init__`` instead of ``setup()`` and CHIA leaves it untouched. You might prefer it when you want the tool's constructor signature to be explicit (for introspection or IDE help), or to match tools already written this way. An ``__init__`` written this way follows a fixed three-step order, and the order matters: 1. **``super().__init__(name, task_options=...)``** creates the ``FastMCP`` instance (``self.mcp``) the tools attach to. It does not start a server yet. 2. **``self.mcp.add_tool(self.method, name=...)``** registers each method as an MCP tool. This must happen *after* step 1 (so ``self.mcp`` exists) and *before* step 3 (so the server has its tools when it starts serving). Call it once per method you want to expose. 3. **``super().__post_init__()``** deploys the server: it creates a Ray actor (constrained by ``task_options``), which probes for a free port, starts uvicorn in a background thread, and registers the running tool. After this returns, ``self.hostname`` and ``self.port`` point at the live endpoint. The same ``BashTool`` written explicitly: .. code-block:: python class BashTool(ChiaTool): def __init__(self, name, work_dir="/", task_options=None): super().__init__(name, task_options=task_options) # 1. init base self.work_dir = work_dir self.mcp.add_tool(self.run_command, name=f"{name}_run_command") # 2. register super().__post_init__() # 3. deploy def run_command(self, command: str) -> str: """Run a bash command and return combined stdout/stderr.""" ... The two styles are interchangeable and can coexist in the same codebase: a subclass that defines ``setup()`` gets the automatic bracketing, while one that defines ``__init__`` keeps full manual control. Intermediate base classes ~~~~~~~~~~~~~~~~~~~~~~~~~~~ An *intermediate* base sits between ``ChiaTool`` and a concrete tool, bundling functionality that several tools share — :mod:`chia.base.tools.AsyncJobTool` (the base for the async variants above) is the canonical example, adding a background-job runner its subclasses inherit. Writing one correctly hinges on a single fact: the ``setup()`` idiom's generated ``__init__`` calls ``ChiaTool.__init__`` **directly**, skipping every intermediate ``__init__`` in between. So a ``setup()``-style subclass of your intermediate never runs your intermediate's ``__init__``. Make the intermediate work whether or not its own ``__init__`` runs: Initialize state lazily, not in** ``__init__``. E.g. provide an idempotent ``_ensure_X()`` that creates the state on first use, and call it at the top of every helper that needs it. This is the rule that makes the class work under both idioms with no per-subclass boilerplate: .. code-block:: python def _ensure_job_state(self): if not hasattr(self, "_job_lock"): self._init_job_state() def _job_start(self, work): self._ensure_job_state() # works whether or not __init__ ran ... This covers state that can be created lazily with sensible defaults. It does not transparently cover eager work that depends on constructor arguments (e.g. opening a connection from a ``dsn=`` passed in). For that, you may requier the leaf's ``setup()`` pass the arguments into the super class explicitly. See also -------- - :doc:`/user_guides/chia_function` — the programmatic counterpart; tools call nodes via ``chia_remote_blocking``. - :doc:`/getting-started/quickstart` — a hands-on flow that ends with an agent using a ``BashTool`` to edit RTL. - :doc:`/concepts/overview` — how nodes, tools, agents, workers, and clusters fit together. - :doc:`/user_guides/profiling` — recording and visualizing a flow's execution, including agent calls.