Expand description
Stable #[repr(C)] ABI contract for rpi Rust-native (cdylib) plugins.
rpi loads extensions as compiled Rust cdylibs (.dll/.so/.dylib) via
libloading — not TS/jiti. Because we control both sides, the plugin is
Rust, but the boundary is still a hand-defined C ABI: the two sides may be
compiled with different Rust versions / crate versions, so no Rust type with
a non-C repr or a Drop impl may cross. This crate defines exactly those
crossing types and the registration contract.
§Soundness rules (load-bearing — verified by an adversarial review)
- Every crossing type is
#[repr(C)]. Enums used in unions carry#[repr(u32)]so the discriminant width is pinned. - No
Droptype crosses.Vec/String/serde_json::Value/Optionof those /Resultnever appear in the ABI. Owned data crosses asStbString(ptr+len) with an explicitfree_stringthe producer exports.StbStringisCopy(raw pointers areCopy); copying duplicates the pointer, not the allocation, so each allocation is freed exactly once by the side that received it (seeStbStringdocs). - Owned → JSON round-trip. Structured host data ([
StableJsonValue], tool params,AgentToolResult, events) crosses as a JSON string in aStbString.serde_jsonwithpreserve_order+arbitrary_precisionmust be enabled consistently on host AND plugin or integers >u64/i64lose precision and object keys may reorder — documented as a an ABI-wide limit. Tool args from the model rarely carry overflow ints, but it is never silent. - Unions are all-
Copypayloads.EventPayload/StepResultPayloadvariants are#[repr(C)]structs of primitives orStbStringonly, so the union isCopy-able and a wrong-variant read isunsafe(caller discriminates bytag). - Unwinding never crosses the ABI. Every host→plugin and plugin→host
call is
extern "C"; both sides wrap dispatch incatch_unwind(abort-on-unwind / log-and-drop). A poisoned mutex or panicking emitter cannot unwind into the other side.
§Lifetime: the 4-function handle
A registered tool drives an execution through four plugin-exported
functions (see ToolExecuteFn / ToolPollFn / ToolCancelFn /
ToolDestroyFn) — execute→StepHandle (plugin-allocates), poll
(non-blocking, borrows the handle, returns StepResult), cancel
(sets an internal AtomicBool flag; idempotent; does NOT free;
thread-safe), destroy (frees; idempotent; called exactly once by the
blocking driver). cancel ≠ destroy: conflating them is a UAF /
double-free. The adapter’s blocking driver calls poll in a loop until
Done/Err, forwards Pending partials, and calls destroy once on exit.
poll is non-blocking and MUST observe the cancel flag and return
Done/Err within a bounded number of polls; otherwise a cancelled call
leaks a spawn_blocking thread forever (those tasks run to completion
regardless of outer-future drop).
The crate is std (not no_std): the ABI types are #[repr(C)] POD with
no Drop — that is what makes the boundary sound — but plugins and the host
are ordinary binaries with std, so the constructor/reader helpers and tests
use String/Vec/serde_json directly. The json feature keeps
serde_json optional for a plugin that wants to skip it.
Modules§
- json
- Helpers to cross structured data as JSON-in-
StbString. See the module docs for the precision/order limit.
Macros§
- export_
plugin - Export a unified ABI plugin entrypoint under
rpi_plugin_register.
Structs§
- Event
Data - A generic JSON-data payload for the long-tail events whose structured shape
the host serializes wholesale (
context,before_provider_request, model select, resources_discover response, etc.). The plugin reads the fields it needs. - Event
Empty - No-payload marker for events that carry none (e.g.
session_shutdown). Carries a dummy byte so the empty-struct isn’t flagged FFI-unsafe byimproper_ctypes(zero-sized C structs are rejected regardless ofrepr(C)). - Event
Error - An error/failure payload.
- Event
Message - A serialized message lifecycle payload (
message_start/message_update/message_end).messageis JSON. Formessage_update, it follows native Pi’s envelope and carriesassistantMessageEvent; streaming consumers must read its realtext_delta.deltainstead of diffing a cumulative snapshot. - Event
Tool Call - A tool-call payload (
tool_call,tool_execution_start/update).tool_call_id+tool_nameare raw strings;paramsis the JSON args. - Event
Tool Result - A tool-result payload (
tool_result,tool_execution_end). - Plugin
Api - The host-provided API struct. The first two fields (
abi_versionandstruct_size) are contract-identity markers that survive future signature changes: a plugin reads them from the pointer before anything else, so even if the entrypoint arity changes, the version check remains robust. - Stable
Plugin Event - One event dispatched to a plugin handler. The host translates its native
AgentEvent/HarnessEventinto this and calls every registered handler for thetag(dispatch wrapped incatch_unwind). Ownership of theStbStrings passes to the handler; the handler frees them via the host’sfree_string. - Stable
Tool Schema - A tool’s provider-facing schema crossing the ABI.
name/descriptionare raw strings;parametersis a JSON Schema serialized to a JSON string (the host parses it into its nativeschemars::Schema). - StbDone
- Terminal success payload.
resultis a JSONAgentToolResult. - StbErr
- Terminal failure payload.
messageis a UTF-8 error string. - StbPending
- A partial/progress result emitted during
Pending.progressis a JSONAgentToolResult(the same shapeon_updatecarries) — the host forwards it to the adapter’son_updatecallback. May be empty. - StbString
- An owned UTF-8 string crossing the ABI as a
(ptr, len)pair. - StbString
Ref - A borrowed, non-owning view of a string passed as an input to an FFI call. The callee MUST NOT free it and MUST NOT retain it past the call.
- Step
Result - Return value of
ToolPollFn. The blocking driver matches ontag, reads the matching payload, and breaks the loop onDone/Err. - Unknown
Runtime Action Id - Error returned when a plugin passes a numeric runtime-action id that this ABI does not define.
Enums§
- Event
Tag - Discriminant for
StablePluginEvent, one variant per pion()category (33 total) plus one rpi-specific category (34 total).#[repr(u32)]pins the discriminant width. - Runtime
Action Id - Identifier for a host runtime action the plugin may invoke via
PluginApi::runtime_action. One slot dispatches all actions; the numeric id crosses the FFI boundary as au32and the host validates it withTryFrom<u32>before constructing this enum. Args/results cross as JSON strings. - Step
Result Tag - Discriminant for
StepResult.#[repr(u32)]pins the width so the union payload is sound across compilers.
Constants§
- EVENT_
HANDLER_ ABORT - Veto: stop the current lifecycle phase. Honored only by the host’s
lifecycle dispatch; the observe fan-out logs + continues (see the module
docs on
EVENT_HANDLER_CONTINUE). - EVENT_
HANDLER_ CLAIMED - Claim: the handler consumed this event and the host should not fall back
to its own default handling. Honored only by dispatch paths that ask
extensions to arbitrate (currently
EventTag::Inputkey routing, viaEVENT_HANDLER_CLAIMED); the ordinary observe fan-out treats it like any other nonzero (log + continue). - EVENT_
HANDLER_ CONTINUE - Return codes an
EventHandlerFnmay return. - EVENT_
HANDLER_ ERROR - The handler handled an error but the fan-out should continue (default nonzero semantics — equivalent to any other nonzero code).
- EVENT_
TAG_ COUNT - Number of event categories —
37(33 Pion()categories + 1 rpi-specificEventTag::BeforeTuiStart+ 2 UI prompt events + 1 rpi-specificEventTag::EditorChange). A test assertsEVENT_TAG_COUNT == 37so a future edit that adds/removes a tag is caught. - HOST_
CONTEXT_ KEY - Reserved key the host adds to a plugin tool call’s arguments.
- HOST_
NAME - The application name of the host that loads plugins:
"rpi". - HOST_
VERSION - Version of the
HOST_NAMEhost that this SDK ships with. - REGISTER_
PANIC_ STATUS - Status code a plugin’s register entrypoint returns when its body panicked.
- REGISTER_
SYMBOL_ UNIFIED - The unversioned symbol for the unified ABI.
- RPI_
PLUGIN_ ABI_ VERSION_ UNIFIED - The unified ABI version.
- SESSION_
ID_ ENV - Environment variable carrying the id of the session this process serves.
- SUBAGENT_
PARENT_ DEPTH_ ENV - See
SUBAGENT_PARENT_TRACE_ID_ENV. Nesting depth, so a grandchild knows how far down it is. - SUBAGENT_
PARENT_ ENV - Every
SUBAGENT_PARENT_TRACE_ID_ENVsibling, as one slice. - SUBAGENT_
PARENT_ SESSION_ ID_ ENV - See
SUBAGENT_PARENT_TRACE_ID_ENV. The parent’s session id. - SUBAGENT_
PARENT_ SPAN_ ID_ ENV - See
SUBAGENT_PARENT_TRACE_ID_ENV. The parent turn’s root observation id. - SUBAGENT_
PARENT_ TRACE_ ID_ ENV - Environment variable a tracing extension publishes to name the trace a
nested
rpishould attach to, and reads back to decide it is a subagent.
Functions§
- guard
- Run
body, converting a panic intoErrinstead of letting it unwind. - guard_
or - Like
guard, but returnsfallbackon panic. Handy for entrypoints that must return a value rather than aResult. - register_
entrypoint ⚠ - Unified ABI entrypoint helper: reads
abi_versionandstruct_sizefrom the struct pointer before dereferencing anything else. This survives future signature changes because the pointer is always the first argument.
Type Aliases§
- Before
Agent Start Fn before_agent_starthandler signature. UnlikeEventHandlerFn(fire-and-forget,i32only), this carries an owningoutso the plugin can hand back a system prompt to install for the run — pi’sBeforeAgentStartEventResult(extensions/types.ts:1394), whosesystemPromptbecomesforceSystemPrompt(runner.ts:1347).- Command
Handler Fn - A generic command-handler fn (for
register_command).args_jsonis a borrowed{"args":"...","command":"/..."}envelope;outis owning JSON output reclaimed with the hostfree_string. The TUI understands{kind:"message",text},{kind:"selector",items:[...]},{kind:"editor",initialText}, and{kind:"input",title,placeholder}responses; selector/editor/input submissions call the same handler with anactionfield inargs. - Event
Handler Fn - Handler fn pointer registered via
register_event_handler(tag, handler).user_datais the plugin’s opaque context. Return0on success; nonzero signals a handled error (the host logs it; dispatch continues to other handlers — one handler’s error does not abort the fan-out). The one exception: when the host dispatches a lifecycle event through its veto-aware path, returningEVENT_HANDLER_ABORTstops the fan-out and aborts that phase (see the constants above). - Free
String Fn - Function pointer a plugin exports to free a
StbStringit produced. Idempotent: freeing an already-freed or emptyStbStringis a no-op. - Provider
Request Fn - A provider-injection factory fn (for
register_provider).req_jsonis a borrowed request envelope;outis an owning response the plugin frees. The host wraps this into aProviderimpl (B4/B5). - Render
Fn - A render/transform fn (for the renderer registrars).
input_jsonis borrowed;outis owning output the plugin frees via hostfree_string. Markdown handlers return{markdown:"..."}; message/entry handlers return{text:"...",markdown?:true}or{lines:["..."]}for terminal UI. - Resources
Discover Fn resources_discoverhandler signature (B5b). UnlikeEventHandlerFn(fire-and-forget,i32only), this carries an owningoutso the plugin can hand{skillPaths, promptPaths, themePaths}back to the host.cwdandreasonare borrowed inputs (StbStringRef);outis plugin-produced and reclaimed via theplugin_free_stringthe host stored alongside the handler at registration.user_datais the plugin’s opaque context. Returns0on success (host readsout); nonzero on a handled error (host logs + skips this handler, fan-out continues — mirrors pirunner.ts:1179-1188).- RpiPlugin
Register Unified - Unified ABI plugin entrypoint signature:
(api). The version is read fromapi->abi_version. - Runtime
Action Fn - Runtime-action signature:
runtime_action(action_id, args_json, out, user_data) -> i32.args_jsonis a borrowed input (StbStringRef);outis an owning output (StbString) the host produces and the plugin frees via the host’sfree_string.action_idis deliberately a rawu32, not a Rust enum: an unknown value must be rejected as a normal protocol error rather than materializing an invalid enum discriminant. Returns0on success, nonzero on error. - Step
Handle - Opaque, plugin-allocated handle for one tool execution drive. Produced by
ToolExecuteFn, polled byToolPollFn, cancelled byToolCancelFn, freed byToolDestroyFn(exactly once, idempotent). - Tool
Cancel Fn cancel(handle). Sets an internalAtomicBool(SeqCst) cancel flag. Idempotent, thread-safe, does NOT free. The poll loop observes it.- Tool
Destroy Fn destroy(handle). Frees the handle. Idempotent; called exactly once by the blocking driver on exit (after the loop seesDone/Err, or after cancel propagated). Null handle is a no-op.- Tool
Execute Fn execute(tool_call_id, params) -> StepHandle. Plugin-allocates a drive handle and begins the work (non-blocking — the real progress comes viapoll).tool_call_idis a borrowedStbStringRef(valid for the call);paramsis an owning JSON string of the tool-call arguments (the plugin frees it via the host’sfree_string). Returns null on allocation failure.- Tool
Partial Cb - Partial-result callback the blocking driver passes to
poll, wrapped incatch_unwindon the host side. The plugin invokes it synchronously insidepoll()when it has aPendingpartial — never retained, never invoked afterDone/Err. - Tool
Poll Fn poll(handle, partial_cb, user_data) -> StepResult. Non-blocking. Must observe the cancel flag (set byToolCancelFn) and returnDone/Errwithin a bounded number of polls. Borrowshandle(does not free it).
Unions§
- Event
Payload - Payload union for
StablePluginEvent. All variants are#[repr(C)]structs ofStbString/ primitives (Copy), so the union is Copy. Discriminate byStablePluginEvent::tagbefore reading. - Step
Result Payload - The
poll()return value. Read thepayloadvariant matchingtag.