Skip to main content

Rust Client

The Rust client uses Tokio + async-tungstenite under the hood. Public types live in the pie_client crate.

Installation

[dependencies]
pie-client = "*"
tokio = { version = "1", features = ["full"] }
anyhow = "1"

Quick start

use pie_client::{Client, ParsedPrivateKey, ProcessEvent};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = Client::connect("ws://127.0.0.1:8080").await?;

// Public-key auth (Ed25519/RSA/ECDSA OpenSSH or PEM).
let key = ParsedPrivateKey::from_file("~/.ssh/id_ed25519")?;
client.authenticate("alice", &Some(key)).await?;

// input is a JSON string. Use serde_json::to_string(&MyInput { … })?.
let mut process = client
.launch_process(
"text-completion@0.1.0".into(),
r#"{"prompt":"Hello","max_tokens":64}"#.into(),
true,
None,
)
.await?;

loop {
match process.recv().await? {
ProcessEvent::Stdout(s) => print!("{s}"),
ProcessEvent::Return(v) => { println!("\n[done] {v}"); break; }
ProcessEvent::Error(e) => { eprintln!("\n[error] {e}"); break; }
_ => {}
}
}

client.close().await?;
Ok(())
}

Client

Created with Client::connect(ws_host).await. Internally it spawns reader and writer tasks; they are torn down by client.close().await.

Authentication

impl Client {
pub async fn authenticate(
&self,
username: &str,
private_key: &Option<ParsedPrivateKey>,
) -> Result<()>;

pub async fn auth_by_token(&self, token: &str) -> Result<()>;
}

authenticate runs a challenge-response handshake. auth_by_token is for backend ↔ engine internal connections. If the server has [auth].enabled = false, authenticate returns Ok(()) even with a None key.

Programs

pub async fn check_program(
&self,
inferlet: &str, // "name@major.minor.patch"
wasm_path: Option<&Path>,
manifest_path: Option<&Path>,
) -> Result<bool>;

pub async fn add_program(
&self,
wasm_path: &Path,
manifest_path: &Path,
force_overwrite: bool,
) -> Result<()>;

check_program validates name@version. When both paths are given, hashes are sent so the server can confirm the local copy matches what's installed. program_exists is a backwards-compatibility alias for check_program.

add_program uploads a local build in 256 KiB chunks.

Processes

pub async fn launch_process(
&self,
inferlet: String,
input: String, // JSON
capture_outputs: bool,
token_budget: Option<usize>,
) -> Result<Process>;

pub async fn attach_process(&self, process_id: &str) -> Result<Process>;

pub async fn list_processes(&self) -> Result<Vec<String>>;

pub async fn terminate_process(&self, process_id: &str) -> Result<()>;

pub async fn ping(&self) -> Result<()>;

pub async fn query<T: ToString>(&self, subject: T, record: String) -> Result<String>;

pub async fn register_mcp_server(
&self,
name: &str,
transport: &str,
command: Option<&str>,
args: Option<Vec<String>>,
url: Option<&str>,
) -> Result<()>;

register_mcp_server exposes a local MCP server to inferlets in this session. The MCP request side of the bridge is currently a stub. See handle_server_message in client/rust/src/client.rs.

Process

pub struct Process { /* … */ }

impl Process {
pub fn id(&self) -> &str;

pub async fn signal<T: ToString>(&self, message: T) -> Result<()>;
pub async fn transfer_file(&self, blob: &[u8]) -> Result<()>;
pub async fn recv(&mut self) -> Result<ProcessEvent>;
pub fn try_recv(&mut self) -> Result<Option<ProcessEvent>>;
}
MethodDescription
id()The process UUID.
signal(msg)Fire-and-forget string signal to the running inferlet.
transfer_file(bytes)Upload bytes to the process (chunked).
recv()Await the next event.
try_recv()Non-blocking variant: returns None when no event is queued.

ProcessEvent

pub enum ProcessEvent {
Stdout(String),
Stderr(String),
Message(String),
File(Vec<u8>),
Return(String), // JSON-encoded Output; process exits cleanly after this
Error(String), // process aborted
}

Match on the variant to dispatch:

match process.recv().await? {
ProcessEvent::Stdout(s) => print!("{s}"),
ProcessEvent::Stderr(s) => eprint!("{s}"),
ProcessEvent::Message(m) => log::info!("inferlet: {m}"),
ProcessEvent::File(b) => save_blob(&b),
ProcessEvent::Return(v) => return Ok(serde_json::from_str(&v)?),
ProcessEvent::Error(e) => anyhow::bail!("process error: {e}"),
}

ParsedPrivateKey

Loads SSH/PEM private keys for authenticate. Supports OpenSSH (RSA / Ed25519 / ECDSA), PKCS#8 PEM, PKCS#1 PEM. ECDSA curves: P-256, P-384. RSA keys must be ≥ 2048 bits.

let key = ParsedPrivateKey::from_file("~/.ssh/id_ed25519")?;
// or
let key = ParsedPrivateKey::parse(&pem_string)?;

Detached / reattach

Process is just a handle. Drop the Client, reconnect, and reattach by id:

let pid = process.id().to_string();
client.close().await?;

// later …
let client = Client::connect(uri).await?;
client.authenticate("alice", &Some(key)).await?;
let mut process = client.attach_process(&pid).await?;
let event = process.recv().await?;

While disconnected, the server buffers events for you.

Utilities

pub fn hash_blob(blob: &[u8]) -> String;

Returns the hex blake3 hash. Useful for pre-computing program_hash when talking to the server directly.