Component Self-Extension
The agent's capabilities are not fixed. When no existing tool fits, it writes a component on the spot, drops it on disk, and can use it 3 seconds later — that is the self-extension loop.
The loop
A self-extension run usually goes through these six steps. Stop and fix on failure instead of stacking work on top of a broken component.
- A task arrives and no existing tool fits
- Call component.dir() for the real write path (pip install and portable builds differ — do not guess)
- Grab a template or call generate, writing the skeleton under components/
- Fill in the _cmd_ methods and their docstrings
- Wait 3 seconds or call reload(), then confirm it shows up in the tool list
- Call it once with real input — only a passing call counts as done
component tool commands
The built-in component tool is the entry point: 11 subcommands covering generation through troubleshooting.
| Command | Purpose |
|---|---|
template(style) | Fetch template code: minimal / full / example / background |
generate(name, description, commands) | Generate a single-file component into components/ |
generate_package(name, description, commands, modules) | Generate a package component (folder + modules) for complex features |
list / info(name) | List registered components, with search and paging |
reload() | Manually trigger a full scan and reload |
dir() | Return the real components directory path |
sandbox_list / sandbox_add / sandbox_remove | List / add / remove no-sandbox whitelist entries; effective immediately |
cleanup(dry_run, force) | Clear load errors, stale whitelist entries and orphan sandbox processes |
install(package, upgrade) | Install a component package from PyPI (cellium-comp-*) |
Generate and activate
# Generate a skeleton
component.generate("hash_tool", "File hash calculator",
commands='[{"name":"md5","desc":"Calculate MD5 hash"},{"name":"sha256","desc":"Calculate SHA256 hash"}]')
# File lands on disk → watcher picks it up within 3s → auto-loaded → callable immediately
hash_tool.md5(input_data="/etc/hostname")Let the model look up usage
The component tool's description tracks its command table. When unsure about a command's arguments, call help or info once instead of guessing repeatedly.
Hot-plug watcher
ComponentWatcher snapshots path → mtime every 3 seconds, handling top-level .py files only.
| Event | Action |
|---|---|
| New file appears | Instantiate, register, wrap as tool, persist to config |
| mtime changed | Unload the old instance and reload; on_load runs again |
| File removed | Remove from memory and tool registry, clean up config |
Note: package components are compared by the max mtime of all .py inside, but the watcher itself only scans top-level .py files. Edits inside a package need component.reload().
Component spec
Follow this skeleton and both the parameter schema and tool description come out right.
from typing import Any, Dict
from app.core.interface.base_cell import BaseCell
class HashTool(BaseCell):
"""File hash calculator. Supports md5 / sha256."""
@property
def cell_name(self) -> str:
return "hash_tool"
def _cmd_md5(self, input_data: str = "", **kwargs) -> Dict[str, Any]:
"""Compute the MD5 of a file
Args:
input_data: file path
**kwargs: session_id / platform_context are injected by the framework
Returns:
{"success": bool, "md5": str}
"""
import hashlib
h = hashlib.md5()
with open(input_data, "rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
h.update(chunk)
return {"success": True, "md5": h.hexdigest()}- cell_name is a @property returning a lowercase, globally unique name — it becomes the tool name
- Command methods need the _cmd_ prefix; the first docstring line is the description and Args: holds parameter docs
- Always return a Dict (never a bare string); report errors via an error field instead of raising
- The framework injects session_id / platform_context into kwargs when available
Reserved tool names
shell— ShellToolmemory— MemoryToolfile— FileTool
Sandbox pitfalls
Subprocess execution by default — most cases of works locally, fails as a component trace back to this.
- The subprocess cannot see main-process memory; share state across commands via files or move the component into the whitelist
- The process is recycled after 300s idle and module-level variables are lost; do not rely on globals for durable state
- A single command over 120s is a timeout; split long work into steps or run it in the background
Directory layout
components/
├── __init__.py
├── _example_component.py # official template (leading _ = not loaded)
├── component_builder.py # the component tool itself
├── scheduler.py # scheduled tasks
├── skill_manager.py # Skill index
├── skill_installer.py # Skill installer
├── sub_agent.py # parallel sub-agents
├── web_fetch.py / web_search.py
├── qq_files.py / telegram_files.py / feishu_files.py / weixin_files.py
├── skills/ # Skill packages
└── my_tool.py # components the agent generatedWhy it is usable immediately
AgentLoop does not freeze the tool list at startup — it re-reads ComponentToolRegistry every turn. So the next turn after loading, the LLM already sees the new tool. No process restart needed.
Boundaries of self-extension: it may generate components, but must not auto-install pip dependencies, must not edit built-in component files, and must not claim success without verification. Generate → activate → call with real input: all three must pass.
