1. A first agent
You create the project and an agent that answers with a language model, or echoes when no API key is set.
Create the project
cd riverbase-workspace
cargo new docs-assistant
docs-assistant/Cargo.toml
[package]
name = "docs-assistant"
version = "0.1.0"
edition = "2021"
[dependencies]
beam_agents = { path = "../riverbeam/crates/beam_agents" }
beam_core = { path = "../riverbeam/crates/beam_core" }
beam_types = { path = "../riverbeam/crates/beam_types" }
riverbase_core = { path = "../riverbase/crates/riverbase_core", features = ["nats-io"] }
async-trait = "0.1"
serde_json = "1"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
uuid = { version = "1", features = ["v4", "v5", "serde"] }
The agent
RigAgentRunner sends the conversation to a completion client and runs any tools the model asks
for. Give it a skill: a name, a system prompt, and the names of the tools it may use.
docs-assistant/src/agents.rs
use std::sync::Arc;
use beam_agents::{CompletionClient, EchoClient, OpenAiCompatible, RigAgentRunner, Skill, Tool};
/// A live model when BEAM_API_KEY is set; an echo client otherwise.
pub fn completion_client() -> Arc<dyn CompletionClient> {
match OpenAiCompatible::from_env() {
Some(client) => Arc::new(client),
None => Arc::new(EchoClient),
}
}
/// The question-answering agent. It may use every tool it is given.
pub fn assistant(tools: Vec<Arc<dyn Tool>>) -> RigAgentRunner {
let skill = Skill {
name: "assistant".into(),
system_prompt: "You answer questions about our documentation. Be brief.".into(),
tools: tools.iter().map(|tool| tool.spec().name).collect(),
};
RigAgentRunner::new("assistant", completion_client(), tools, skill)
}
Invoke it
docs-assistant/src/main.rs
mod agents;
use std::sync::Arc;
use beam_core::{BeamConfig, RiverBeam};
use beam_types::InvokeRequest;
use serde_json::json;
use uuid::Uuid;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let beam = RiverBeam::new(BeamConfig::default());
beam.register_agent(Arc::new(agents::assistant(Vec::new())));
let request: InvokeRequest = serde_json::from_value(json!({
"agent_name": "assistant",
"session_id": Uuid::new_v4(),
"content": [{"type": "text", "text": "What is a session?"}]
}))?;
let result = beam.invoke_result(request).await?;
for part in &result.content {
if let Some(text) = &part.text {
println!("{text}");
}
}
Ok(())
}
Run it
Without a key, the agent echoes:
cargo run
Echo: What is a session?
With a key, it answers with the model. OpenAiCompatible::from_env defaults to NVIDIA's endpoint;
point it anywhere OpenAI-compatible:
export BEAM_API_KEY=…
export BEAM_API_BASE_URL=https://api.openai.com/v1
export BEAM_MODEL=gpt-4o-mini
cargo run
What happened
invoke_result ran the invoke pipeline: it created a session,
interaction and run in the in-memory store, saved your message, loaded the (empty) history, and
called the agent. The agent returned its frames; the final done frame carried the AgentResult
you printed.
Next: give the model a tool.