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.

  1. A task arrives and no existing tool fits
  2. Call component.dir() for the real write path (pip install and portable builds differ — do not guess)
  3. Grab a template or call generate, writing the skeleton under components/
  4. Fill in the _cmd_ methods and their docstrings
  5. Wait 3 seconds or call reload(), then confirm it shows up in the tool list
  6. 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.

CommandPurpose
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_removeList / 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.

EventAction
New file appearsInstantiate, register, wrap as tool, persist to config
mtime changedUnload the old instance and reload; on_load runs again
File removedRemove 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 — ShellTool
  • memory — MemoryTool
  • file — 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 generated

Why 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.

Key Point

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.