rpi-extensions
The host side of the plugin system: discovers, loads and drives Rust cdylib
plugins, and bridges their tools and events into the runtime's native async
types.
The ABI contract itself lives in
rpi-plugin-sdk; this crate is the
other half.
Install
[]
= "0.3"
Most users never depend on this directly — the rpi CLI uses it, and the
harness consumes its adapters through injection.
What it does
- Loading.
load_oneloads a single library,load_dirscans a directory, andload_sessionresolves the whole set for a session. Entrypoints are resolved through the singlerpi_plugin_registersymbol. Plugins must use the unified ABI; incompatible registrations are reported as load failures. - Registry.
ExtensionRegistryholds what plugins registered: tools, providers, handlers, renderers and resource-discovery callbacks, with a deterministic snapshot the runtime can read. - Tool bridge.
PluginToolAdapterimplements the agent'sAgentTooltrait on top of a plugin's four-function lifecycle (execute→poll→cancel→destroy). The blockingpollloop runs onspawn_blockinginside the ambient runtime — the adapter does not own a runtime of its own. Tools registered withregister_tool_with_wakeuse short, serialized blocking calls and sleep asynchronously onPending, without retaining an idle blocking worker. Wakes during callback installation or poll are retained. Cancellation or caller drop cancels, unregisters the callback and destroys the handle exactly once, while retaining the library and invocation context through cleanup. - Partial results. Tool progress flows back through an unbounded channel and
is forwarded to
on_update, behind acatch_unwindtrampoline so a panicking callback cannot unwind across the FFI boundary. - Lifecycle events. Dispatch wrappers for data, empty and lifecycle events, a veto path that lets a handler refuse startup, and a tee emitter so a plugin can observe events without stealing them from the TUI.
- Providers.
PluggableProviderandExtensionProviderHookslet a plugin serve a model or patch request options. A plugin that registersregister_streaming_provideremits per-chunk envelopes that the host maps ontoAssistantMessageEvents as they arrive, so a plugin provider renders progressively instead of appearing all at once when the response completes. The same request fn also receives aProviderStreamControl, and a plugin that polls it stops generating on a cancelled run instead of streaming a response nobody will read — one registration, and the plugin decides whether to check.
Crate dependencies
rpi-extensions depends on rpi-plugin-sdk, rpi-ai and rpi-agent only —
never on rpi-harness. The harness receives these adapters as trait objects, so
the dependency graph stays acyclic.
Where plugins are discovered
| Location | Scope |
|---|---|
.rpi/extensions |
Project (preferred) |
~/.rpi/agent/extensions |
Global |
--extensions-dir <path> |
Explicit, added by the CLI |
Installing and developing
Docs
- Crate docs: https://docs.rs/rpi-extensions
- Extension authoring guide
- Plugin ABI contract
- Ready-to-install extensions
License
Plugins may register the optional two-phase lifecycle hooks. Reload calls
begin_shutdown outside session locks to cancel work and reject new requests.
The last FFI/string-free keepalive schedules finish_shutdown on a blocking
cleanup thread before unloading the library. A failed finish permanently
retains the DLL and callback context and emits a diagnostic; cleanup has no
unload timeout. The same ownership path handles partial registration failure
and ordinary session teardown.
This changes the published Rust LoadedPlugin layout: library is now a
ManuallyDrop<Library>, and private ownership fields prevent external struct
literals or field moves. Use load_one and retain the returned owner until all
plugin calls finish. This source API change needs a breaking release; compiled
plugins keep their existing ABI compatibility.