> ## Documentation Index
> Fetch the complete documentation index at: https://aomi.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Rust SDK

> Reference the Rust traits and helpers used to implement typed Aomi plugin tools.

<Info>Verified against published `aomi-sdk` 5.1.1 at commit `2ef3e04` on 2026-09-22.</Info>

The [`aomi-sdk` crate](https://crates.io/crates/aomi-sdk) is the public Rust API
for implementing an Aomi plugin. It defines typed tools, call context, secrets,
async results, registration, host namespaces, and test helpers.

This is an API-focused reference. To choose a project layout, write a preamble,
and package the plugin as an App, start with [Aomi App](/docs/build/plugins/aomi-app).

<Note>
  Build only against the public `aomi-sdk` crate. Your plugin exchanges JSON
  values with the host and does not link to private runtime crates.
</Note>

## SDK surface

| API              | Use it to                                                       |
| ---------------- | --------------------------------------------------------------- |
| `DynAomiTool`    | Define a typed synchronous or async tool                        |
| `DynToolCallCtx` | Read session attributes and injected secrets                    |
| `DynAsyncSink`   | Emit progress and complete an async tool call                   |
| `dyn_aomi_app!`  | Register the App type, tools, preamble, secrets, and namespaces |
| `Secret`         | Declare a credential slot required by the plugin                |
| `TestCtxBuilder` | Test a tool with host-like attributes and secrets               |

The crate re-exports compatible `schemars` and `serde_json` modules. Using
`aomi_sdk::schemars` and `aomi_sdk::serde_json` avoids dependency-version drift
in schema and JSON types.

The public crate and its authoring examples live in the
[aomi-sdk repository](https://github.com/aomi-labs/aomi-sdk). The hosted
backend consumes the crate and loads its ABI; private backend crates are not
part of the plugin API.

## The `DynAomiTool` trait

Implement one `DynAomiTool` per operation exposed to the model.

```rust theme={null}
pub trait DynAomiTool: Send + Sync + 'static {
    type App: DynAomiApp;
    type Args: DeserializeOwned + JsonSchema + Send + 'static;

    const NAME: &'static str;
    const DESCRIPTION: &'static str;
    const IS_ASYNC: bool = false;

    fn run(
        _app: &Self::App,
        _args: Self::Args,
        _ctx: DynToolCallCtx,
    ) -> Result<Value, String>;

    fn run_with_routes(
        app: &Self::App,
        args: Self::Args,
        ctx: DynToolCallCtx,
    ) -> Result<ToolReturn, String>;

    fn run_async(
        _app: &Self::App,
        _args: Self::Args,
        _ctx: DynToolCallCtx,
        _sink: DynAsyncSink,
    ) -> Result<(), String>;
}
```

| Member            | Meaning                                                                              |
| ----------------- | ------------------------------------------------------------------------------------ |
| `type App`        | The marker or state type shared by tools in one plugin                               |
| `type Args`       | The typed input decoded from the model's JSON arguments                              |
| `NAME`            | Unique tool name the model calls                                                     |
| `DESCRIPTION`     | Model-facing summary used during tool selection                                      |
| `IS_ASYNC`        | Set to `true` when the tool uses `run_async`                                         |
| `run`             | Synchronous implementation for normal tools                                          |
| `run_with_routes` | Synchronous implementation that returns a payload plus host-managed follow-up routes |
| `run_async`       | Async or streaming implementation that writes to a sink                              |

Argument types derive `Deserialize` and `JsonSchema`. Field doc comments become
parameter descriptions in the generated schema.

```rust theme={null}
use aomi_sdk::schemars::JsonSchema;
use serde::Deserialize;

#[derive(Debug, Deserialize, JsonSchema)]
struct GreetArgs {
    /// Person to greet, such as `Ada Lovelace`
    name: String,
}
```

```rust theme={null}
use aomi_sdk::{DynAomiTool, DynToolCallCtx};
use serde_json::Value;

struct Greet;

impl DynAomiTool for Greet {
    type App = MyApp;
    type Args = GreetArgs;
    const NAME: &'static str = "greet";
    const DESCRIPTION: &'static str = "Greet one person by name.";

    fn run(
        _app: &MyApp,
        args: GreetArgs,
        _ctx: DynToolCallCtx,
    ) -> Result<Value, String> {
        Ok(serde_json::json!({ "message": format!("Hello, {}!", args.name) }))
    }
}
```

Return concise error strings that tell the model whether it should correct an
argument, ask the user, or stop.

## Tool-call context

Every tool receives a `DynToolCallCtx` containing its call identity and the
host data exposed to the plugin.

```rust theme={null}
pub struct DynToolCallCtx {
    pub session_id: String,
    pub tool_name: String,
    pub call_id: String,
    pub state_attributes: Map<String, Value>,
    pub secrets: HashMap<String, String>,
}
```

Read nested attributes with typed helpers:

```rust theme={null}
fn run(
    _app: &MyApp,
    _args: Args,
    ctx: DynToolCallCtx,
) -> Result<Value, String> {
    let org_id = ctx
        .attribute_u64(&["user", "org_id"])
        .ok_or("This tool requires a user organization.")?;
    let name = ctx
        .attribute_string(&["user", "name"])
        .unwrap_or_default();

    Ok(serde_json::json!({ "org_id": org_id, "name": name }))
}
```

Treat state attributes as input for the current call. Do not assume an
attribute exists unless the host capability that supplies it is active.

## Async tools

Set `IS_ASYNC = true` and implement `run_async` for long-running work. Use the
`DynAsyncSink` to send progress and one terminal result.

```rust theme={null}
impl DynAomiTool for StreamingTool {
    type App = MyApp;
    type Args = StreamArgs;
    const NAME: &'static str = "stream_report";
    const DESCRIPTION: &'static str = "Build a report and stream its progress.";
    const IS_ASYNC: bool = true;

    fn run_async(
        _app: &MyApp,
        args: StreamArgs,
        _ctx: DynToolCallCtx,
        sink: DynAsyncSink,
    ) -> Result<(), String> {
        sink.emit(serde_json::json!({ "step": 1 }))
            .map_err(|error| error.to_string())?;
        sink.complete(serde_json::json!({ "report": args.topic }))
            .map_err(|error| error.to_string())?;
        Ok(())
    }
}
```

* `emit` sends a non-terminal update.
* `complete` sends the terminal result.
* `fail` reports a terminal error.
* `is_canceled` lets expensive work stop after host cancellation.

<Warning>
  Pass bare JSON values to `emit`. Only the terminal `complete` result may use
  a routed return envelope.
</Warning>

## Routed and multistep tools

A tool can suggest a follow-up host action by returning a routed `ToolReturn`.
Use this when one result naturally supplies the arguments for the next step,
such as a quote followed by a signature request.

Override `run_with_routes`, then attach an `on_return` step:

```rust theme={null}
use aomi_sdk::{RouteStep, ToolReturn};
use serde_json::json;

ToolReturn::with_route(
    json!({ "status": "awaiting_signature" }),
    RouteStep::on_return(
        "evm_commit_message",
        json!({ "typed_data": "..." }),
    )
    .prompt("Suggested next step: request the signature."),
)
```

The route is a continuation hint, not permission to execute. Aomi presents the
step to the model and still applies the App policy, guards, and signing policy
before the next tool runs. Use `bind_as` on a producer and `after(...).awaits`
for a callback-driven continuation. The host injects the bound callback
artifact into the awaiting arguments; do not rebuild or manually copy wallet
payloads, transaction hashes, signatures, quote IDs, or route IDs.

## The `dyn_aomi_app!` macro

Call `dyn_aomi_app!` once in `src/lib.rs`. It registers the plugin manifest and
dispatches tool calls to their typed implementations.

```rust theme={null}
aomi_sdk::dyn_aomi_app!(
    app = MyApp,
    name = "greeter",
    version = "0.1.0",
    preamble = PREAMBLE,
    tools = [Greet],
    secrets = [API_KEY],
    namespaces = ["evm-core"],
);
```

| Field        | Meaning                                                              |
| ------------ | -------------------------------------------------------------------- |
| `app`        | App type shared by the registered tools                              |
| `name`       | Stable App key                                                       |
| `version`    | Plugin version string                                                |
| `preamble`   | App-specific system prompt                                           |
| `tools`      | Tool types available to the model                                    |
| `secrets`    | Optional credential slots declared by the plugin                     |
| `namespaces` | Host capability sets requested by the plugin; defaults to `evm-core` |

The macro is the only registration entry point you need. Do not implement the
low-level plugin boundary by hand.

## Secrets

Declare each external credential as a `Secret` slot. The name is canonical,
the description appears in configuration surfaces, and `required` determines
whether the App can load without a value. A declaration is operator-owned by
default.

```rust theme={null}
use aomi_sdk::Secret;

const API_KEY: Secret = Secret::new(
    "EXCHANGE_API_KEY",
    "API key from the exchange dashboard.",
    true,
);
```

Register the slot with `secrets = [API_KEY]`. At call time, read it with
`resolve_secret_value`:

```rust theme={null}
let key = aomi_sdk::resolve_secret_value(
    &ctx,
    args.api_key.as_deref(),
    "EXCHANGE_API_KEY",
    "Add EXCHANGE_API_KEY in App settings before using this tool.",
)?;
```

The helper resolves, in order:

1. an explicit argument;
2. the credential injected into `ctx.secrets`; and
3. an environment variable used by local CLI and tests.

Do not log, persist, or include the resolved value in tool output.

### User-owned credentials

Call `.user_owned()` when every authenticated user must supply a separate
credential:

```rust theme={null}
const API_KEY: Secret = Secret::new(
    "EXCHANGE_API_KEY",
    "API key from the exchange dashboard.",
    true,
)
.user_owned();
```

Read a user-owned value only from the injected call context:

```rust theme={null}
let key = aomi_sdk::resolve_user_secret_value(
    &ctx,
    "EXCHANGE_API_KEY",
    "Add EXCHANGE_API_KEY in App settings before using this tool.",
)?;
```

`resolve_user_secret_value` intentionally has no tool-argument or process
environment fallback. This prevents a missing user credential from silently
using a builder or backend operator key. The host redacts common result and
error paths, but plugin code is trusted native code: never log, persist, or
return a credential.

## Host namespaces

`namespaces` requests host capability sets that accompany your plugin tools.

| App requirement                            | Registration value                   |
| ------------------------------------------ | ------------------------------------ |
| Default EVM wallet and transaction flow    | Omit the field or use `["evm-core"]` |
| EVM reads and staging without commit tools | `["evm-reads", "evm-sim"]`           |
| All SVM reads and write lanes              | `["svm-core"]`                       |
| SVM reads plus instruction-based writes    | `["svm-reads", "svm-write-ix"]`      |
| SVM reads plus venue-built transactions    | `["svm-reads", "svm-write-tx"]`      |
| External API calls with no host tools      | Use `[]`                             |

Declare only capabilities the App uses. If the App stages transactions,
describe the expected stage, simulate, and commit sequence in its preamble so
the model invokes the host tools in the correct order.

## Testing tools

Use `aomi_sdk::testing` to test typed tools without loading the compiled plugin.

```rust theme={null}
use aomi_sdk::testing::{run_tool, TestCtxBuilder};
use serde_json::json;

#[test]
fn greet_returns_message() {
    let ctx = TestCtxBuilder::new("greet").build();
    let result = run_tool::<Greet>(&MyApp, json!({ "name": "Ada" }), ctx)
        .expect("greet should succeed");

    assert_eq!(result.value["message"], "Hello, Ada!");
}
```

Seed the same inputs a host call would provide:

```rust theme={null}
let ctx = TestCtxBuilder::new("place_order")
    .secret("EXCHANGE_API_KEY", "test-key")
    .attribute("user", json!({ "org_id": 42 }))
    .build();
```

For an async tool, `run_async_tool` returns `(updates, terminal)`: the emitted
values in order and the terminal payload.

## Next steps

<CardGroup cols={2}>
  <Card title="Aomi App" icon="hammer" href="/docs/build/plugins/aomi-app">
    Structure the crate, write its preamble, and configure its package.
  </Card>

  <Card title="End-to-end testing" icon="flask" href="/docs/build/toolchain/aomi-build#the-end-to-end-test">
    Test realistic App conversations with a canonical `test.json` journey.
  </Card>

  <Card title="CLI toolchain" icon="terminal" href="/docs/build/toolchain/aomi-build">
    Compile, run, deploy, and activate the plugin.
  </Card>
</CardGroup>
