Verified against aomi-sdk@08b21f9 on 2026-06-29.
cdylib) over a stable C ABI. You write typed tools in Rust, compile to a .so (Linux) or .dylib (macOS), and the runtime loads it without a restart.
This is the public plugin surface. You build against the
aomi-sdk crate, and your tools cross the FFI boundary as JSON. The runtime and loader are not part of this crate, so you never link against host internals. The compatibility gate is the SDK version: the host and your plugin must build against the same aomi-sdk version.The four pieces
A minimal App needs four things:1
An app struct
A marker type that ties everything together. It must be
Clone + Default + Send + Sync + 'static. A unit struct works well.2
A typed args struct
Deserialized from the incoming JSON. Derive
Deserialize and JsonSchema. The doc comments on each field are model facing: they become the parameter schema the LLM reads when it picks your tool.3
A tool implementation
A struct that implements
DynAomiTool. This is your actual logic.4
The dyn_aomi_app! macro
This generates the manifest, the dispatch router, and the FFI exports the host calls. You never write the C ABI by hand.
Cargo.toml
The crate must build as acdylib, and it depends on aomi-sdk.
Pin
aomi-sdk exactly (=3.0.3), not with a caret range. The runtime gates plugin loading on an exact SDK version match, so the pin must equal the platform’s required_sdk_version (see platform.json in the platform repo). A mismatch fails before your App ever loads.aomi-sdk exports schemars and serde_json again from inside itself, so your tool code can reach them through the crate (for example aomi_sdk::schemars::JsonSchema) without managing version alignment yourself.The DynAomiTool trait
Every tool is one struct that implements DynAomiTool. The trait carries two associated types, three consts, and the run methods.
The runtime auto generates each tool’s parameter schema from
Args using JsonSchema. You do not write JSON Schema by hand.
The tool context
Everyrun call receives a DynToolCallCtx. It is a small projection of the host context with only what your tool needs.
Async tools
For long running or streaming work, setIS_ASYNC = true and implement run_async instead of run. The host polls for updates through the DynAsyncSink. You push intermediate values with emit, signal the terminal result with complete, report a failure with fail, and check for host cancellation with is_canceled.
The dyn_aomi_app! macro
dyn_aomi_app! is the one macro you call per App. It wires your tools into a dispatch router, builds the plugin manifest the host reads, and generates the FFI exports the runtime calls across the C ABI. You do not implement DynAomiApp or any C function by hand.
Secrets
If your App needs external credentials, declare them asSecret slots. Each slot has a canonical name in SCREAMING_SNAKE_CASE, a one sentence description shown in the settings UI, and a required flag.
required: true, the host gates App load on the user having filled that slot in the runtime secret vault. At tool call time the host pre resolves the slots and injects the raw values into ctx.secrets. Your tool reads them with resolve_secret_value, which never logs, persists, or echoes the value to the model.
resolve_secret_value checks three sources in order and returns your missing_message if none resolve:
1
Explicit argument
The value the caller passed in, if any.
2
Injected vault secret
ctx.secrets[name], injected by the host from the per App vault.3
Environment variable
A fallback
name env var, used by the CLI and tests where no vault is in scope.Host namespaces
Thenamespaces field lists host side capability sets the runtime injects alongside your own tools. The default is ["evm-core"], which most Apps want for EVM wallet flows.
How the runtime hot loads your plugin
You ship a compiled.so or .dylib. The runtime does the rest. It calls the generated FFI exports across the C ABI, reads your manifest, validates the SDK version, and serves tool calls. When a new build arrives, the runtime swaps it in atomically.
All data crosses the boundary as JSON serialized C strings. Active chat sessions keep the old plugin while new sessions pick up the new one, so a reload needs no restart.
Testing your tools
Theaomi_sdk::testing module lets you unit test tools without loading the full FFI plugin. Build a context with TestCtxBuilder, then call run_tool for sync tools or run_async_tool for streaming tools.
TestCtxBuilder also lets you seed state attributes with .attribute(...) and inject a resolved secret with .secret(...), so you can simulate exactly what the host would pass.
run_async_tool returns (updates, terminal): every non terminal emit value in order, plus the terminal complete payload.
A full example, start to finish
A minimal greeter App, straight from the crate docs:sdk/examples/app-template-http in the aomi-labs/aomi-sdk repo shows the recommended file split for a real App: src/lib.rs for the manifest and preamble, src/client.rs for the HTTP client and typed args, and src/tool.rs for the tool implementations.
Next steps
Building an App
The full workflow for authoring, structuring, and publishing an App.
CLI toolchain
aomi-build to scaffold, compile, and deploy plugins, and aomi-run to exercise them locally.