A robot in Rust
A Rust program can skip the C ABI and drive seyd_core::Agent directly. It is the same type seydd and seyd-ffi are built on, so a Rust host gets exactly the daemon’s behaviour with none of its plumbing.
The host contract
Section titled “The host contract”Agent owns everything between a configuration and a running robot: sockets, candidate discovery, the certificate, the QUIC endpoint, the session engine, signaling, port-mapping lease renewal, certificate rotation and re-gathering on a network change. A host supplies media and consumes events, and owns only what differs by form factor. seydd owns TOML, RTSP/RTP inputs and UDP sinks; seyd-ffi owns C marshalling. If you find yourself writing lifecycle code in a host, it belongs in Agent; otherwise the form factors drift.
The API is small:
AgentConfig: the robot’s configuration, independent of where it came from.robot_id,signal_url,credential_path,quic_port,ipv6,port_mapping,host_override,qos_profile,max_sessions,channels,agent_version,recovery_ladderandrelay.AgentConfig::default()carries the same defaults asseydd.toml.ChannelSpec: one channel, withid,kind,name,codec,fpsand optional simulcastlayers. Ids are what the pilot sees and what pushes and events refer to.Agent::start(cfg): binds, discovers, announces, and returns(Agent, Receiver<AgentEvent>)once pilots may connect. The maintenance loop runs in the background untilstop()or until the receiver is dropped.push_video(channel, VideoFrame)andpush_video_layer: hand an encoded picture to the engine.VideoFrameisdata,keyframeandcapture_ts_us. Never blocks; a frame that finds the slot full is dropped by admission control, and keyframes never are.push_message(channel, &[u8]): a message on a sensor channel.AgentEvent:SessionStarted,SessionEnded,Command,RequestedConfig,RecoveryRequest,LayerChanged,SignalConnected,SignalDisconnected,SignalDeniedandNatReport.stop(): the maintenance loop exits, the endpoint closes and in-flight sessions are torn down.Agentis cheap to clone; every clone refers to the same agent.
Also on the handle: set_qos_profile, set_status, session_count, counters, profile and channels.
The minimal host
Section titled “The minimal host”packages/seyd-core/examples/minimal_host.rs is the whole contract in one file: an AgentConfig from argv, a sensor channel and a command channel, start, a counter pushed once a second, every event printed, stop on Ctrl-C. It is a Cargo example, so cargo build -p seyd-core --examples compiles it and this page cannot show code that does not build.
// Copyright 2026 Anton Gravestam// SPDX-License-Identifier: Apache-2.0//! minimal_host — the smallest host of [`seyd_core::Agent`].//!//! A *host* supplies media and consumes [`AgentEvent`]s; the agent owns//! everything else (sockets, discovery, certificate, signaling, lease renewal,//! cert rotation). This program is the whole contract in one file: build an//! [`AgentConfig`], declare a sensor channel and a command channel, start,//! push a counter once a second, print every event, stop on Ctrl-C.//!//! cargo run -p seyd-core --example minimal_host -- <robot_id> <signal_url> [credential_path]//!//! `seydd` is the reference host (packages/seydd/src/main.rs): the same calls,//! plus TOML, RTSP/RTP inputs and UDP sinks. Add video with//! [`Agent::push_video`], handing it the access units your encoder already//! produces; Seyd never decodes or re-encodes.
use seyd_core::{Agent, AgentConfig, AgentEvent, ChannelKind, ChannelSpec};use std::path::PathBuf;use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[tokio::main]async fn main() -> anyhow::Result<()> { let args: Vec<String> = std::env::args().collect(); if args.len() < 3 { eprintln!("usage: minimal_host <robot_id> <signal_url> [credential_path]"); std::process::exit(2); }
// Channels are numbered from 1 in declaration order; that numbering is // what the pilot sees and what `push_message` and `Command` refer to. let telemetry = ChannelSpec { id: 1, kind: ChannelKind::Sensor, name: "telemetry".into(), codec: "json".into(), fps: 0, layers: Vec::new(), max_bitrate_kbps: 0, }; let drive = ChannelSpec { id: 2, kind: ChannelKind::Command, name: "drive".into(), codec: "json".into(), fps: 0, layers: Vec::new(), max_bitrate_kbps: 0, };
// Everything not set here keeps the same default as `seydd.toml`. let cfg = AgentConfig { robot_id: args[1].clone(), signal_url: args[2].clone(), credential_path: PathBuf::from(args.get(3).map(String::as_str).unwrap_or("./robot.key")), qos_profile: "latency".into(), channels: vec![telemetry.clone(), drive], agent_version: "minimal_host/0.1".into(), ..AgentConfig::default() };
// Binds, discovers candidates, announces to the cloud; returns once pilots // may connect. The receiver is the host's side of the agent. let (agent, mut events) = Agent::start(cfg).await?; println!("robot {} online; pilots may connect", args[1]);
// A 1 Hz counter on the sensor channel. `push_message` never blocks, so // this can run on any task or thread. let sensor = agent.clone(); let ticker = tokio::spawn(async move { let mut seq: u64 = 0; loop { let t_us = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_micros() as u64) .unwrap_or(0); let msg = serde_json::json!({ "seq": seq, "t_us": t_us }); sensor.push_message(telemetry.id, msg.to_string().as_bytes()); seq += 1; tokio::time::sleep(Duration::from_secs(1)).await; } });
// The host's event loop. Nothing here is protocol: it is the robot's own // policy about what a command means and what the publisher is told. loop { tokio::select! { ev = events.recv() => { let Some(ev) = ev else { break }; match ev { AgentEvent::Command { channel, payload } => { println!("command on channel {channel}: {}", String::from_utf8_lossy(&payload)); } AgentEvent::SessionEnded { signal_id, reason } => { // Park actuators here: this fires whether the pilot said // goodbye or simply vanished. println!("session {signal_id} ended ({reason}); {} left", agent.session_count()); } other => println!("{other:?}"), } } _ = tokio::signal::ctrl_c() => { println!("stopping"); break; } } }
ticker.abort(); agent.stop(); Ok(())}Run it against the deployed cloud with an enrolled robot id (see Enrolment and access):
cargo run -p seyd-core --example minimal_host -- <robot_id> wss://seyd-signal-flj7s44j4a-ew.a.run.app/ws ./robot.keyThe example installs no log subscriber, so the agent’s own tracing is silent and only the events print. Add tracing_subscriber::fmt().init() to see what seydd logs.
How seydd drives the same agent
Section titled “How seydd drives the same agent”packages/seydd/src/main.rs is the reference host. Read it next to the example and the split is plain:
- It builds
ChannelSpecs from the TOML, one per[[channel]], numbered from 1 in file order, and hands them toAgent::startinside anAgentConfig. - For each video channel it spawns one input task per simulcast layer and calls
push_video_layerwith every access unit; a single-input channel is layer 0. For each sensor channel it spawns a UDP listener and callspush_messageper datagram. - Its event loop is daemon policy and nothing else:
Commandgoes out as a UDP datagram to the channel’soutput;RequestedConfig,RecoveryRequestandLayerChangedbecome publisher-control JSON;SessionStartedandSessionEndedbecomesessionmessages. Signaling events are ignored, because the agent has already handled them. - Ctrl-C calls
agent.stop().
Enrolment is the one thing the daemon does outside the agent today: seydd enrol posts the public key with a token, using seyd_signal_client::Identity, which is the same key file format every host reads.
What a Rust host must still do
Section titled “What a Rust host must still do”The rules are the same as for every other form factor. Push whole encoded access units and pass the encoder’s capture_ts_us; Seyd never decodes or re-encodes. Do not block on the event receiver’s thread. Park actuators on SessionEnded, which fires whether the pilot said goodbye or vanished. Act on RequestedConfig and RecoveryRequest: keyframes are on demand (ADR 0009), and a publisher that ignores them leaves a joining pilot waiting up to maxGopMs. The details are on The publisher contract.
The Rust reference lists the crates, and the rustdoc for the agent is at seyd_core.