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":

rust
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:

rust
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:

rust
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:

rust
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.

Each crate's rustdoc opens with an API map; start at docs.rs/oino-core. The crate map has the full list.