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=...)"}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 code | Seen by the LLM |
|---|---|
cell_name | cell_name |
_cmd_xxx | _cmd_xxx method |
| First docstring line | Tool / command description |
| Args: entries | Parameter 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.pyVerify it works
The load pipeline is covered in Part 1; this section is only about confirming your component actually works.
- component.list() shows it, so it is registered
- component.info(name) shows the command table and any load error
- Call one command with real input and check the returned structure
- 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 isolationWhitelist 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
| Command | Purpose |
|---|---|
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().
