Verified against aomi-sdk@main on 2026-06-04.
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:An app struct
A marker type that ties everything together. It must be
Clone + Default + Send + Sync + 'static. A unit struct works well.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.Cargo.toml
The crate must build as acdylib, and it depends on aomi-sdk.
Pin
aomi-sdk exactly (=0.1.20), 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 CI 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.
| Member | What it is |
|---|---|
type App | The app struct this tool belongs to. Ties the tool to one App. |
type Args | Your typed args struct. Must derive Deserialize and JsonSchema. |
NAME | The unique tool name the LLM calls, such as get_token_price. |
DESCRIPTION | One line shown to the LLM for tool selection. Write it for the model. |
IS_ASYNC | false by default. Set true to stream results over time. |
run | Synchronous logic. Override this for normal tools. |
run_async | Streaming logic. Override this when IS_ASYNC = true. |
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.
| Field | Meaning |
|---|---|
app | Your app struct. |
name | The App key the runtime uses, such as "defi". |
version | Semver string for this App, such as "0.1.0". |
preamble | The system prompt injected into the LLM context for this App. |
tools | The list of tool structs to register. |
secrets | Optional. The named credentials this App declares. See below. |
namespaces | Host namespaces this App needs. Defaults to ["evm-core"]. |
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:
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.
| You want to… | Set namespaces to |
|---|---|
| Use the default EVM wallet flow | omit the field, or ["evm-core"] |
| Ship a namespace only App (no wallet flow) | ["database"] |
| Opt out of host namespaces entirely | [] |
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 and compile plugins, and aomi-git to deploy them.