Chuyển tới nội dung chính

Agents, tools and skills

An agent is anything that implements the Agent trait from beam_agents. You register agents on RiverBeam, and requests pick one by agent_name.

The Agent trait​

#[async_trait]
pub trait Agent: Send + Sync {
fn metadata(&self) -> AgentMetadata;

async fn stream(
&self,
input: &AgentInput,
context: &AgentContext,
) -> RiverbaseResult<Vec<StreamChunk>>;

async fn health_check(&self) -> bool {
true
}
}
  • metadata describes the agent: name, description, supported_modalities, output_modalities, tags, and version. A request whose content uses a modality the agent does not list fails with BEM-040. The router also reads the tags. With the built-in text-only model, any non-text content fails earlier, at routing, with BEM-041.
  • stream returns the agent's frames. The last frame must have type done and carry the AgentResult; otherwise the pipeline fails with BEM-086. An error frame fails with BEM-087.
  • input is the request (content, session_id, run_id, metadata, auth_context, actions). context holds the conversation history, the resolved ModelConfig, and the request metadata.

Register agents on the pipeline:

use std::sync::Arc;

beam.register_agent(Arc::new(my_agent));
beam.register_echo_agent("assistant"); // EchoAgent: replies "Echo: <input>"

list_agents() returns the metadata of every registered agent, and registry().get_metadata(name) returns one (BEM-020 if it is not registered).

RigAgentRunner​

RigAgentRunner is the built-in tool-loop agent. It sends the conversation and the tool specs to a completion client, runs any tool calls it gets back, and repeats until the model answers with text.

worker/src/agents.rs
use std::sync::Arc;

use beam_agents::{sandbox_tools, CompletionClient, EchoClient, OpenAiCompatible, RigAgentRunner};
use beam_sandbox::{SandboxRoot, WorkingTree};

// A live model when BEAM_API_KEY (or NVIDIA_API_KEY) is set; an echo client otherwise.
let client: Arc<dyn CompletionClient> = match OpenAiCompatible::from_env() {
Some(client) => Arc::new(client),
None => Arc::new(EchoClient),
};

// Tools work inside one jailed directory. "main" and "master" can never be pushed.
let tree = WorkingTree::new(
SandboxRoot::new("/srv/agent-work/docs")?,
vec!["main".into(), "master".into()],
);
tree.init()?;

let author = RigAgentRunner::for_skill("author", client, sandbox_tools(Some(tree)));
beam.register_agent(Arc::new(author));

init creates an empty repository. Before the agent can push, the tree needs a first commit, a branch of its own (checkout_branch(name, true)), and an origin (add_origin(url)). Push sends no credentials, so use a remote that needs none. Tutorial step 5 sets this up.

How a run goes:

  1. It publishes a placeholder and a thinking frame.
  2. It calls the client with the history (prefixed by the skill's system prompt) and the tool specs.
  3. On tool calls, it runs each tool, records a ToolCallRecord, adds a tool frame, and loops. It stops with BEM-088 after 8 rounds, and fails with BEM-089 when the model names a tool it does not have. An error from a tool ends the whole run; it is not passed back to the model.
  4. On text, it emits a chunk frame and the done frame.

Before running a tool whose spec has confirm_required: true, it returns an interrupted result instead, so a person can approve it. See Human approval.

Request metadata can skip approval

The runner skips the approval step when the request has metadata.confirm set to true. Request metadata comes from the client, so strip confirm in your API layer unless the caller is allowed to approve their own actions.

Constructors:

ConstructorTools the agent gets
RigAgentRunner::for_skill(skill_name, client, tools)The tools you pass that appear in the skill's allowlist. An unknown skill name gets an empty system prompt and all the tools.
RigAgentRunner::new(name, client, tools, skill)All the tools you pass; the skill supplies only the system prompt.

Completion clients​

CompletionClient is the model boundary:

#[async_trait]
pub trait CompletionClient: Send + Sync {
async fn complete(&self, messages: &[Value], tools: &[ToolSpec]) -> RiverbaseResult<Completion>;
}

Completion is either Text(String) or ToolCalls(Vec<PendingToolCall>). Messages use the OpenAI chat format.

ClientBehaviour
OpenAiCompatiblePOST {base_url}/chat/completions with bearer auth. Works with any OpenAI-compatible endpoint.
EchoClientEchoes the last user message. No network.
ScriptedClient::new(steps)Returns the given completions in order, for tests. Fails with BEM-081 when it runs out.

OpenAiCompatible::new(base_url, api_key, model) builds a client directly. from_env() reads:

VariableDefault
BEAM_API_KEY, then GFS_BEAM_API_KEY, then NVIDIA_API_KEYnone; from_env returns None without a key
BEAM_API_BASE_URL, then GFS_BEAM_BASE_URLhttps://integrate.api.nvidia.com/v1
BEAM_MODEL, then GFS_BEAM_MODELdeepseek-ai/deepseek-v4-pro-0813

Requests use temperature 1.0, top_p 0.95, max_tokens 4096 (change it with with_max_tokens), a 60-second timeout, and the extra body {"chat_template_kwargs": {"thinking": false}}. Replace the extra body with with_extra_body. Never commit API keys.

The client sends its own model. The model the router resolves reaches the agent in context.model_config_resolved, and RigAgentRunner does not use it.

Tools​

A tool implements Tool:

#[async_trait]
pub trait Tool: Send + Sync {
fn spec(&self) -> ToolSpec;
async fn invoke(&self, args: Value, input: &AgentInput) -> RiverbaseResult<Value>;
}

ToolSpec has a name, a description, confirm_required, and parameters (a JSON Schema object). json_params(properties, required) builds the schema.

worker/src/tools.rs
use async_trait::async_trait;
use beam_agents::{json_params, Tool, ToolSpec};
use beam_types::AgentInput;
use riverbase_core::RiverbaseResult;
use serde_json::{json, Value};

pub struct WordCount;

#[async_trait]
impl Tool for WordCount {
fn spec(&self) -> ToolSpec {
ToolSpec {
name: "word_count".into(),
description: "Count the words in a text.".into(),
confirm_required: false,
parameters: json_params(json!({"text": {"type": "string"}}), &["text"]),
}
}

async fn invoke(&self, args: Value, _input: &AgentInput) -> RiverbaseResult<Value> {
let text = args.get("text").and_then(Value::as_str).unwrap_or_default();
Ok(json!({ "words": text.split_whitespace().count() }))
}
}

Sandbox tools​

sandbox_tools(tree) returns nine tools that work inside a beam_sandbox working tree:

ToolArgumentsNotes
list_treepath (optional)Sorted file list; skips .git
read_filepath
write_filepath, bodyCreates parent directories
git_status
git_diff
git_loglimit (default 20)
git_addpath"" or "." stages everything
git_commitmessageReturns the commit id
git_pushref (optional)confirm_required; pushes to origin; protected refs fail with BEM-328

SandboxRoot rejects absolute paths, .., and anything that resolves outside the root (BEM-300 to BEM-305). WorkingTree adds git through libgit2, and its protected_refs can never be pushed.

Always bind a working tree

With sandbox_tools(None), the tools read the root directory from the request's metadata.sandbox_root, and the protected refs from metadata.protected_refs (default main and master). A client could then choose any directory on the host. In a service, always pass Some(tree) with a root you control.

Skills​

A Skill is a name, a system prompt, and a tool allowlist. builtin_skills() lists them and skill_by_name(name) finds one.

SkillPurposeShipped tools it allows
authorEdit files on a change branch and commitAll nine sandbox tools
consumeAnswer questions with citations; read onlynone
summarizeSummarize a change or conversationlist_tree, read_file, git_diff, git_log
commit-notesDraft a commit messagegit_status, git_diff, git_log
reviewReview a change and cite pathslist_tree, read_file, git_diff, git_log
gfs-publishPublish a reviewed change, confirming firstnone (publish_docset is a host tool)

The allowlists also name tools that a host application provides, such as search, get_page and review_change. for_skill keeps only the tools you actually pass, so supply your own Tool implementations under those names to enable them.

Current limitations​

  • RigAgentRunner does not use rig-core; it is Riverbeam's own tool loop over a CompletionClient.
  • The remember tool spec exists, but invoking it returns BEM-109. Call RiverBeam::memory_remember from your host code instead.
  • Responses are not streamed token by token. The client is called with stream: false, and frames are published after the agent finishes.
  • RigAgentRunner reports zero tokens and no model_used in its results.
  • The model receives the latest user message twice; see Invoke pipeline.
  • WorkingTree::checkout_branch does not check protected refs.