LLMSkillHub
mirroredCopied from dylanroscover/Embody under MIT. dylanroscover has not claimed this page, and is not affiliated with LLM SkillHub.

dylanroscover / create-extension

MUST READ before calling create_extension. Required parameters, lifecycle methods, and wiring steps.

0
SKILL.md
6.0 KB

<!-- Generated by Embody/Envoy - Do not remove this comment - sha:6b85e409552f32cb -->

Create Extension Workflow

Use the create_extension MCP tool to create a fully-wired TD extension:

create_extension(
    parent_path='/path/to/parent',
    class_name='MyFeatureExt',
    name='MyFeature',           # Optional: COMP name
    code='...',                 # Optional: initial Python code
    promote=True,               # Optional: promote extension methods
    ext_name='MyFeature',       # Optional: extension name
    ext_index=0                 # Optional: extension index
)

This creates: baseCOMP + text DAT + extension wiring, initialized and ready to use.

Wiring by hand: the Extension Object parameter is a constant-mode Str par whose VALUE is Python (par.ext0object.val = "op('./MyFeatureExt').module.MyFeatureExt(me)" -- the Extensions page is the ext sequence on 2025 builds: ext0object, ext0name, ext0promote; there is no extension1). Never set its .expr: the extension comes back None with extensionsReady True and errors() empty; the only trace is a SyntaxError ... Context:(Extension 1) on scriptErrors() (get_op_errors kind: 'script').

Extension Lifecycle Methods

Always implement these in your extension class:

class MyFeatureExt:
    def __init__(self, ownerComp: COMP) -> None:
        self.ownerComp = ownerComp

    def onDestroyTD(self) -> None:
        """Called on old instance before TD reinitializes. Clean up callbacks, timers, etc."""
        pass

    def onInitTD(self) -> None:
        """Called at end of frame after init. Safe to access other extensions and cooked network."""
        pass

Naming Convention

Extension classes and source DATs must follow the NameExt convention:

  • Class: MyFeatureExt
  • DAT: MyFeatureExt (matches class name)

Three Namespace Tiers

TD promotes every capitalized member -- methods and class constants alike. There is no per-member opt-out, so the capital letter is the access modifier. Pick the tier by who calls it:

TierNameReached asWho calls it
1. Public APIUpperCamelCaseop.MyFeature.DoSomething()A user or an agent, deliberately
2. WiringlowerCamelCaseop.MyFeature.ext.MyFeature.onFrame()The COMP's own exec / callback / parexec DATs
3. Private_lowerCamelCaseinside the class onlyThe class itself

Test: could a user or an agent reasonably call this ON the COMP? If not, it is not tier 1. Promoting a frame hook is a design flaw, not a shortcut.

What makes an over-wide tier 1 harmful is concrete, not cosmetic: co-mounted extensions share one namespace and TD documents no precedence for a duplicate, so one name silently wins; and every promoted name -- constants included -- is reachable by any getattr(comp, name) dispatcher. Promoted members do NOT appear in dir(), so this is not an autocomplete argument.

Two traps:

  • .ext.<Name> resolves by the Extension Name parameter, not the class name. If ext_name='MyFeature' and the class is MyFeatureExt, then op.MyFeature.ext.MyFeature.x() works and op.MyFeature.ext.MyFeatureExt.x() raises.
  • op.MyFeature requires a Global OP Shortcut, and create_extension never sets one. Straight out of the tool, op.MyFeature raises AttributeError. Reach the COMP by a relative path (op('./MyFeature'), parent.Host.op('MyFeature')), or set par.opshortcut deliberately -- it is globally unique, so assigning a name already in use silently steals it from the previous holder. Reserve it for genuine project-wide singletons.

Pass parent_shortcut when descendants need to reach the COMP. It sets par.parentshortcut, which is what makes parent.MyFeature resolve and keeps the COMP's own callback DATs off depth-coupled parent() chains:

create_extension(parent_path='/project1', class_name='MyFeatureExt',
                 name='MyFeature', ext_name='MyFeature',
                 parent_shortcut='MyFeature')

It refuses to overwrite a shortcut the COMP already declares -- re-pointing one silently redirects every reference already using it -- and says so in the result's warning.

Both shortcuts can also be set by hand:

comp.par.parentshortcut = 'MyFeature'   # descendants reach it as parent.MyFeature
comp.par.opshortcut = 'MyFeature'       # project-wide: op.MyFeature -- singletons only

NEVER cache extension references in variables -- always call inline. Cached refs go stale on reinit.

Referencing From Inside the Extension

Capture the owner once and navigate from it. self.ownerComp is rung 1 of the referencing ladder in td-python.md; the bare global parent() is banned in an extension class body.

class MyFeatureExt:
    def __init__(self, ownerComp: COMP) -> None:
        self.ownerComp = ownerComp          # rung 1 -- everything else hangs off this

    def rebuild(self):
        target = self.ownerComp.op('./render1')     # own children
        cfg = self.ownerComp.par.Mode.eval()        # own parameters

Extensions Inside TDXN COMPs

If the extension lives inside a TDXN-strategy COMP (or the extension's ownerComp is one), onInitTD will fire before TDXN import reconstructs the network. Any state set up during onInitTD - created operators, parameter values, stored data - is overwritten when ImportNetwork runs with clear_first=True.

Always defer initialization:

def onInitTD(self):
    """Defer setup so it runs after TDXN import completes."""
    run('args[0].postInit()', self, delayFrames=5)

def postInit(self):
    """Safe to set up state here - TDXN import is complete."""
    # Create operators, set parameters, initialize state
    pass

This applies on project open, after every Ctrl+S (strip/restore cycle), and on manual TDXN reimport. The deferred method must be idempotent - it may run multiple times.