Build
Embedding the SDK
Run the oino agent inside your own Rust program, in process or over RPC.
Frontends embed the runtime in process (the TUI does) or drive a separate oino --mode rpc process (the typed RPC client does). Both sit on the same contract, SessionBackend from oino-client: commands in, one ordered event stream out, dialog answers back through ui_respond.
Crates#
| Crate | Role |
|---|---|
oino-core |
Services (per working directory), AgentSessionRuntime (the session under management), extension runner, compaction, export |
oino-client |
SessionBackend trait, RPC command records, InProcessBackend, a scripted fake for tests |
oino-wasm-host |
wasmtime host for extension components; CoreLoader plugs packages into oino-core |
oino-rpc |
JSONL codec, an RPC server over any backend, and RpcClient/RpcBackend to drive oino --mode rpc |
oino-ai |
message, model and stream types with fixed JSON shapes |
The layering is strict: oino-core never depends on wasmtime; it sees extensions only through the loader trait. oino-agent, oino-tools, oino-session and friends sit below core and are usable on their own.
In process, no extensions#
Services::create loads settings, discovers resources and prepares the extension registry; AgentSessionRuntime::create opens the first session; InProcessBackend wraps the runtime. This is the 40-line example from the quick start, verified to compile against the published oino-core = "1" / oino-client = "1":
use std::{path::PathBuf, sync::Arc};
use indexmap::IndexMap;
use oino_core::extensions::api::ExtensionMode;
use oino_core::runtime::{AgentSessionRuntime, SessionOptions};
use oino_core::services::{ResourceFlags, Services, ServicesOptions, StaticLoader};
// `cwd` and `agent_dir` are PathBufs; agent_dir defaults to $HOME/.oino (OINO_AGENT_DIR)
let services = Services::create(ServicesOptions {
cwd,
agent_dir, // $HOME/.oino by default
docs_root: PathBuf::new(),
mode: ExtensionMode::Print, // Tui | Print | Json | Rpc | Acp | Desktop | Mobile
has_ui: false,
flags: ResourceFlags::default(),
extension_flag_values: IndexMap::new(),
loader: Arc::new(StaticLoader(Vec::new())),
approve: None, // project trust decision
models: None, // registry override; default loads auth.json + models.json
settings: None, // settings manager override
}).await?;
let runtime = Arc::new(
AgentSessionRuntime::create(services, SessionOptions::default()).await?,
);SessionOptions picks the session source (New, ContinueRecent, Open(path), Resume(id), Fork), an explicit model and thinking level, a scoped model list and the tool selection. StaticLoader(Vec::new()) loads no components, which also means no providers: for model access you need provider packages (next section) or a custom registry passed as models.
In process, with extension packages#
Real embedders discover packages on disk and hand them to the wasmtime host:
use std::{path::PathBuf, sync::Arc};
use oino_wasm_host::{HostConfig, WasmRuntime, WasmRuntimeOptions};
use oino_wasm_host::core_bridge::{CoreLoader, SourcedPackage};
use oino_wasm_host::packages::discover;
let (packages, problems) = discover(&[agent_dir.join("extensions")], true);
let packages: Vec<SourcedPackage> = packages
.into_iter()
.map(|p| SourcedPackage { package: p, project: false })
.collect();
let wasm = WasmRuntime::new(WasmRuntimeOptions::new(HostConfig::new(
agent_dir.clone(), cwd,
)))?;
let loader = Arc::new(CoreLoader::new(Arc::new(wasm), packages));Pass that loader in ServicesOptions and the components' tools, commands, providers and settings namespaces appear the same way they do in the CLI. Check problems and surface them; a package that fails validation is skipped. The CLI's own loader (crates/oino/src/loader.rs and startup.rs in the repository) is the complete worked example, including distribution share dirs, -e paths and safe mode.
Driving the session#
InProcessBackend::new(runtime) implements SessionBackend:
use futures::StreamExt;
use oino_client::{AgentCommand, InProcessBackend, RpcCommand, SessionBackend};
let backend = InProcessBackend::new(runtime);
let mut events = backend.events();
backend.send(RpcCommand::Agent(AgentCommand::Prompt {
message: "Refactor this module".into(),
images: Vec::new(),
streaming_behavior: None,
})).await?;
while let Some(event) = events.next().await {
// ClientEvent::Session(...) | Ui(...) | ExtensionError(...) | Oino(...)
}The command vocabulary is the RPC command set (prompt, steer, follow_up, abort, set_model, compact, fork, ...) plus the _oino/* extensions, so everything you can do from the wire you can do in process. oino_client::FrontendApi wraps the common ones in typed helpers (completions, panels, shortcuts, settings, sign-in, rewind).
For tests, oino_client::fake::FakeBackend replays scripted events and replies.
Out of process#
Spawn the CLI in RPC mode and talk JSONL:
use oino_client::protocol::ClientInfo;
use oino_rpc::{ChildCommand, RpcBackend, Transport};
let backend = RpcBackend::connect(
Transport::StdioChild(ChildCommand {
program: "oino".into(),
args: vec!["--mode".into(), "rpc".into()],
cwd: Some(project_dir),
env: vec![("ANTHROPIC_API_KEY".into(), key)],
}),
ClientInfo { name: "my-app".into(), version: "0.1.0".into() },
None,
)
.await?;connect also performs the _oino/hello handshake. RpcBackend is a SessionBackend like any other, with events decoded by EventDecoder. RpcClient on its own exposes events() as raw JSON when you are not using the typed stack. The wire format itself is documented in clients, and is simple enough to implement from any language: one JSON object per line, commands with ids, responses and events interleaved.
Where to read next#
Each crate's rustdoc opens with an API map; start at docs.rs/oino-core. The crate map has the full list.