.agents/skills/engine-plugin-authoring/SKILL.md
engines/)Two related runbooks: adding a brand-new engine plugin, and adding a modality to one
that already exists. Read engines/AGENTS.md first for the concepts these steps
assume (op-table vtable, manifest, the valid-engine checklist, the 4-file skeleton,
the engine↔runtime rule). This skill is the "how", that file is the "what/why".
Invariant that applies to both procedures: never invent a new modality just to
ship an engine — fill an existing rac_engine_vtable_t slot. Adding a brand-new
primitive (a new RAC_PRIMITIVE_* + vtable slot) is a commons ABI change, covered
by core/AGENTS.md → "Adding a new capability interface", not an engine change.
Mirror sherpa (multi-modality, standalone .so) or cloud (single modality,
no runtime) as your template; both are the cleanest references for the 4-file
skeleton described in engines/AGENTS.md.
engines/<name>/ with the 4-file skeleton: rac_plugin_entry_<name>.cpp,
rac_backend_<name>_register.cpp (skip it if there's no extra bring-up, like
neurt), rac_static_register_<name>.cpp, plus your impl files.rac_plugin_entry_<name>.cpp:
name = snake_case identity (the library/framework you wrap, or your own
codebase unit — NEVER a modality name; see the naming rule in
engines/AGENTS.md).primitives[] = what you serve; fill the matching vtable slots; leave every
other slot explicit NULL (the all-NULL tripwire depends on this).runtimes[] — only devices your compute actually depends on (NULL for
cloud/HTTP engines) — see THE RULE in engines/AGENTS.md.formats[] = the RAC_MODEL_FORMAT_ID_* values you accept (NULL if there's
no local model file).priority, availability (PUBLIC/PRIVATE), package_owner/package_name.create → initialize → use → cleanup → destroy (see engines/AGENTS.md for the
exact signatures). Use RAC_DEFINE_CREATE_ADAPTER(primitive, name)
(core/include/rac/plugin/rac_plugin_entry.h) when create is a plain forward
onto rac_<primitive>_<name>_create — sherpa uses it for STT/TTS/VAD. Engines
with richer create flows (llamacpp, onnx, neurt, qhexrt) hand-write it.capability_check when the engine is platform- or binary-gated: use the
shared 3-way helper rac_engine_unavailable_capability(platform_supported, backend_present) from engines/common/rac_engine_unavailable.h. When the
backend can be entirely absent at build time (a private prebuilt archive, a
sibling private repo), emit the stub with RAC_ENGINE_UNAVAILABLE_PLUGIN(name, display, cap_fn) from the same header — see qhexrt and neurt for the two-mode
(ROUTABLE vs STUB) pattern this produces.RAC_PLUGIN_ENTRY_DEF(<name>) returning the vtable; a
rac_backend_<name>_register()/_unregister() pair if you have extra bring-up
(called directly by SDK bridges on dynamic-link hosts); a
rac_static_register_<name>.cpp shim using RAC_STATIC_REGISTER_BACKEND(<name>)
(routes through the register fn) or RAC_STATIC_PLUGIN_REGISTER(<name>) (calls
the entry directly) for iOS/WASM static hosts.rac_add_engine_plugin(<name> SOURCES … LINK_LIBRARIES … AVAILABILITY … PACKAGE_OWNER … PACKAGE_NAME …) (cmake/plugins.cmake) from
engines/<name>/CMakeLists.txt, fronted by your own
option(RAC_BACKEND_<NAME> "…" <default>) + if(NOT RAC_BACKEND_<NAME>) return() endif() self-gate (each engine owns its own option — engines/CMakeLists.txt
just unconditionally add_subdirectory()s and lets the child decide). Add
add_subdirectory(<name>) there. Declare primitives/runtimes/formats ONLY
in the C manifest, never in CMake, so the build graph can't drift from what the
router routes.System.loadLibrary +
nativeRegister, add a JNI bridge with RAC_DEFINE_ENGINE_JNI_BRIDGE(...)
(standalone per-engine .so — onnx, llamacpp) or
RAC_DEFINE_ENGINE_JNI_BRIDGE_NO_ONLOAD(...) (folded into a host lib that
already owns JNI_OnLoad, e.g. cloud → librunanywhere_jni.so) from
engines/common/rac_engine_jni_bridge.h. The JVM class-path token in the macro
call must match the Kotlin *Bridge class byte-for-byte.rac_force_load(<app> PLUGINS <name>) in the host's CMakeLists — so the
static-init Registrar TU survives linker stripping.scripts/validation/gates/check_plugin_natives.py's REQUIRED_OPS dict (the
ops-table symbol name(s) your entry TU references only on the routable
branch) — package-sdk.sh calls this automatically, so an unregistered
engine's stub/shell builds ship undetected. Check which FILE actually
compiles rac_plugin_entry_<name>.cpp before assuming the carrier
(runanywhere_<name>) is what to gate: sherpa/onnx/llamacpp compile it into
the carrier itself (an undefined symbol reference there is the evidence),
but neurt's rac_add_engine_plugin() call puts it on the ENGINE target
(rac_backend_neurt) instead — the carrier there is empty boilerplate with
zero symbols. Add the engine id to EVIDENCE_IN_BACKEND_FILE if it follows
neurt's shape; discovered by actually building neurt for real and running
nm -a on both files, not by reading the CMakeLists and assuming.Multi-modality is the whole point of an identity-named engine. cloud and neurt
are explicitly built for this and carry the recipe inline as a comment next to
their vtable.
g_cloud_tts_ops,
typically backed by a per-modality provider adapter under providers/).rac_engine_vtable_t
(tts_ops / llm_ops / embedding_ops / …) — every other slot must stay
explicit NULL.primitives[].formats[]/runtimes[] only if the new modality actually needs them
(per THE RULE — don't declare a runtime your new op-table doesn't depend on).No new plugin, no rename, no ABI bump — the engine already owns one
rac_engine_vtable_t with all ten primitive slots; you're just filling another one.
Grep the target engine's rac_plugin_entry_<name>.cpp for the phrase "To add a"
— cloud and neurt both leave a comment naming exactly which slots are still free
and what to add to primitives[].