Component Development · Part 2 of 3 — Hands-on

Custom Components

This part builds one working component from scratch: where the file goes, how the class is named, and how methods become commands the LLM can call.

Prerequisite: Part 1 covers the load pipeline, the three registries and the sandbox model. Without it, much of the behaviour below will look arbitrary.

Where components live

Call component.dir() for the real path. It differs between pip install and the portable build — do not guess.

# Query the real components directory (pip install and portable builds differ)
component.dir()

# pip install mode → user data dir
# ~/.local/share/cellium/components/
# portable build → program directory
# ./components/

Minimal component

A cell_name plus a few _cmd_ methods is all you need; everything else is generated.

# components/hash_tool.py
from typing import Any, Dict
from app.core.interface.base_cell import BaseCell


class HashTool(BaseCell):
    @property
    def cell_name(self) -> str:
        return "hash_tool"          # tool name, must be unique

    def _cmd_md5(self, text: str) -> Dict[str, Any]:
        """Compute the MD5 of a text

        Args:
            text: text to hash
        """
        import hashlib
        return {"success": True, "md5": hashlib.md5(text.encode()).hexdigest()}

    def _cmd_help(self, topic: str = "") -> Dict[str, Any]:
        """Show usage of this component"""
        return {"success": True, "usage": "hash_tool.md5(text=...)"}
Naming

The suffix after _cmd_ is the command name; the method docstring becomes the tool description the LLM sees. Write it clearly or the model will call it wrong.

How parameters reach the LLM

The JSON Schema is derived from the signature and the docstring Args section (rules in Part 1). Just keep the following in line:

In your codeSeen by the LLM
cell_namecell_name
_cmd_xxx_cmd_xxx method
First docstring lineTool / command description
Args: entriesParameter description
def _cmd_query(self, keyword: str, limit: int = 5) -> Dict[str, Any]:
    """Query by keyword

    Args:
        keyword: search keyword
        limit: max number of results
    """

# JSON Schema generated for the LLM:
# {"name": "hash_tool", "parameters": {
#   "properties": {
#     "keyword": {"type": "string", "description": "search keyword"},
#     "limit":   {"type": "integer", "description": "max number of results"}}}}

Let the agent write it

Three ways, pick by complexity:

Get a template

# Fetch a template: minimal / full / example / background
component.template(style="background")

Generate a single-file component

# Single-file component → components/hash_tool.py
component.generate(
  name="hash_tool",
  description="File hash calculator",
  commands='[{"name":"md5","desc":"Compute MD5"},{"name":"sha256","desc":"Compute SHA256"}]'
)

Generate a package component

Use the package form when the feature needs several modules; the main class goes in core.py.

component.generate_package(
  name="data_pipeline",
  description="Data processing pipeline",
  commands='[{"name":"run","desc":"Run the pipeline"},{"name":"status","desc":"Show status"}]',
  modules='["utils","monitor"]'
)

# Generated layout:
# components/data_pipeline/
# ├── __init__.py      # exports the main component class
# ├── core.py          # main component class
# ├── utils.py         # extra module
# └── monitor.py

Verify it works

The load pipeline is covered in Part 1; this section is only about confirming your component actually works.

  1. component.list() shows it, so it is registered
  2. component.info(name) shows the command table and any load error
  3. Call one command with real input and check the returned structure
  4. Edits reload within 3 seconds; package-internal edits need component.reload()
# Trigger a scan manually instead of waiting 3 seconds
component.reload()

When to leave the sandbox

Subprocess isolation is the default; the model is explained in Part 1. Only two cases justify the no-sandbox whitelist: you need in-memory state shared across commands, or calls regularly approach the 120-second timeout. Stateless computation is safer left in the sandbox.

component.sandbox_list()                    # built-in + custom whitelist
component.sandbox_add(name="hash_tool")   # add to whitelist, hot reload now
component.sandbox_remove(name="hash_tool") # remove from whitelist, restore isolation
Sandbox

Whitelist changes trigger a hot reload immediately. Built-in entries (component, scheduler, skill_manager, skill_installer, sub_agent, web_fetch, web_search and the four IM file components) are hard-coded and cannot be removed.

Debug commands

CommandPurpose
component.list()List all components and their commands
component.info(name)Inspect one component: class name, file path, commands, load state
component.reload()Trigger a scan manually instead of waiting 3 seconds
component.cleanup()Clear component caches and stale sandboxes (dry_run previews by default)

Pitfalls

  • cell_name must be globally unique; duplicates override the earlier one.
  • Always return a Dict, never a bare string — otherwise the LLM gets an unparsable object.
  • Do not do heavy work at import time (DB connections, HTTP calls); the sandbox subprocess will stall.
  • Sandboxed components cannot see main-process memory. Share state via the whitelist, files, or an API.
  • No restart needed after edits, but syntax errors make that component fail to load — check with component.info().