# Phi Rust extension SDK (`ext/rust`)
Rust is a **first-class language for Phi extensions** — on par with the Go SDK
in [`ext/go`](../go). Same PXB wire protocol on stdin/stdout, same host
features (LLM tools, slash commands, intercepts, event subscriptions, confirm
dialogs), byte-for-byte interop, and the same install flow (`phi.yaml` + a
binary under `~/.phi/extensions/<name>/`). The only dependencies are
`serde`/`serde_json` at the JSON edges (tool schemas, confirm payloads) plus
`tokio` (`rt` feature) to drive async tool handlers; the
PXB wire codec stays hand-rolled — no reflection, no runtime protocol deps.
Wire compatibility with the Go SDK is pinned byte-for-byte by golden tests
against `ext/go/pxb/testdata/*.bin` (`tests/pxb_test.rs`).
## Layout
| `src/pxb/` | Wire protocol: frames (`codec`), tagged fields (`fields`), message codecs (`msg`), types/events (`types`) |
| `src/phi/` | Author SDK: `Extension`, `Tool`, `Command`, `Context`, `Schema` |
| `examples/` | Runnable extensions: `hello` (commands, intercepts, subscribe), `full` (tool, confirm, submit) |
| `tests/` | Golden byte-compat + end-to-end fake-host tests |
## Authoring
The crate publishes to crates.io; release tags are `ext/rust/vX.Y.Z`. Until
the first publish, depend on it from the repo (`main`); after a release, use
the published version:
```toml
[dependencies]
# pre-release: tracks the latest main
phi-ext = { git = "https://github.com/pulseaiclub/phi", branch = "main" }
# release: published crate version (tagged ext/rust/vX.Y.Z)
# phi-ext = "0.1.0"
```
```rust,no_run
use phi_ext::{phi, pxb};
fn main() -> Result<(), phi::Error> {
let mut m = phi::Extension::new("hello", "0.1.0");
m.register_command("hello", phi::Command::new("Say hi", |_args, ctx| {
ctx.notify("info", "Hello!");
// ctx.submit("follow-up"); // after /hello returns
// ctx.send_user_message("…"); // enqueue a turn anytime
Ok(())
}));
// .needs_args() → picker/bare "/plan" fills "/plan " for the user to finish
m.register_command(
"plan",
phi::Command::new("plan mode — /plan on|off|status", |_args, _ctx| Ok(())).needs_args(),
);
m.on_user_input(|_ev| None); // return Some(UserInputResult { handled: true, .. }) to swallow
m.on_tool_call(|_ev| None); // return Some(ToolCallResult { block: true, reason: "…", .. }) to deny
m.on_tool_result(|_ev| None); // return Some(ToolResultResult { stop: true, .. }) to end the loop
m.on_turn_stopping(|_ev| None); // return Some(TurnStoppingResult { continue_: true, message: "…", .. }) to steer
m.subscribe(pxb::Event::SessionStart, |_ev| {});
m.run()
}
```
Tools are usually IO-bound, so handlers may be async — the SDK runs them on a
single-threaded tokio runtime that blocks the PXB loop the same way a sync
handler would (the host waits for the result anyway):
```rust,no_run
use phi_ext::phi;
m.register_tool(phi::Tool::new_async(
"fetch",
"GET a URL and return its length",
phi::Schema::object().property("url", phi::Schema::string()),
|args| async move {
// args is owned (Vec<u8>), so `async move` can capture it directly.
// Network / IO calls (reqwest, tokio, …) go here.
Ok(phi::ToolResult {
content: format!("got {} bytes of args", args.len()),
..Default::default()
})
},
));
```
Sync handlers keep working unchanged via [`phi::Tool::new`] — the two share
one storage type, so a tool can switch to async without touching its schema.
Command handlers get a [`phi::Context`](src/phi.rs) for host interaction:
`notify`, `set_status`, `submit`, `send_user_message`, `confirm`,
`confirm_opts`. Commands themselves stay synchronous: their `Context` reads
nested PXB frames off the same pipe, which only works on the loop thread.
Build and install (a `phi.yaml` manifest must live next to the binary):
```bash
cargo build --release --example hello
mkdir -p ~/.phi/extensions/hello
cp target/release/examples/hello phi.yaml ~/.phi/extensions/hello/
```
Reload in the TUI: **Ctrl+K → extensions → reload**.
PXB codec numbers (vs Go / JSON lines) live in the root
[README](../../README.md#extensions). Re-run the probe with
`cargo run --release --example bench`.
## Development
```bash
cargo test # unit + golden + end-to-end fake-host tests
cargo fmt --check
cargo clippy --all-targets -- -D warnings
```
The run loop is single-threaded; the borrow checker replaces the Go SDK's
mutexes (handlers get `&mut` state and cannot alias the registry).