9 unstable releases (3 breaking)
Uses new Rust 2024
| new 0.5.9 | Jul 1, 2026 |
|---|---|
| 0.5.8 | Jun 30, 2026 |
| 0.4.1 | Jun 29, 2026 |
| 0.3.3 | Jun 26, 2026 |
| 0.2.2 | Jun 25, 2026 |
#168 in Development tools
1.5MB
29K
SLoC
behest
Rust AI agent runtime primitives for typed tools, provider-neutral LLMs, streaming, storage, and observability
English · 简体中文
What this is
behest is a Rust-native toolkit for building production AI agent runtimes. It provides strongly typed contracts for LLM providers, streaming chat, tool calling, embeddings, runtime execution, storage, queues, RAG, and observability.
Use it when you need explicit control over model providers, tool execution, runtime state, persistence, and operational boundaries — without hiding the agent loop behind opaque framework magic.
Status: early foundation crate. Public APIs are intentionally compact, strongly typed, and documented. Current crate version:
0.5.9.
Why use behest
- Rust-native runtime core: edition 2024, strict linting, typed APIs, explicit errors, and no hidden runtime assumptions.
- Provider-neutral LLM layer: OpenAI, Anthropic, local models, proxies, or internal providers can implement the same contracts.
- Typed tool boundary: tools are declared with JSON Schema and executed through explicit registries.
- Streaming-first agent loop: model events, tool calls, persistence, and runtime events are modeled as first-class paths.
- Production surfaces: session gates, snapshots, compaction, retry policy, queues, storage backends, health checks, tracing, and optional OpenTelemetry.
Quick start
[dependencies]
behest = "0.5.9"
Create a provider-neutral chat request:
use behest::prelude::*;
let request = ChatRequest::new(ModelName::new("example-model"))
.with_message(Message::system_text("You are concise."))
.with_user_text("Summarize this project in one sentence.");
Register providers in a registry and route requests:
use behest::prelude::*;
let registry = ProviderRegistry::new();
let provider_id = ProviderId::new("my-provider");
// Register a ChatProvider implementation first.
// registry.register_chat(my_provider);
// Then route through the neutral registry.
// let response = registry.complete(&provider_id, request).await?;
More examples in examples/.
Why behest
behest /bɪˈhest/ — n. a person's orders or command.
At the behest of the user, the agent acts.
The core of an agent runtime is not "autonomous consciousness" but controlled delegation: the user issues an intent, and the system composes context, invokes models, executes tools, persists state, publishes events within explicit boundaries — auditable, recoverable, constrainable, and replaceable.
The name behest deliberately avoids inflated metaphors like "brain / cognition / intelligence". It only states an engineering fact: tool-calling, streaming, memory, queue, RAG, and snapshots exist because someone gave an order.
What's inside
| Area | Capability |
|---|---|
| Provider contracts | ChatProvider, EmbeddingProvider, request / response models, stream events, provider capabilities |
| Provider registry | In-memory routing for chat and embedding providers |
| Chat model types | messages, content parts, tool calls, response formats, token usage, finish reasons |
| Tool runtime | Tool, FunctionTool, ExternalTool, ToolRegistry, schema generation, execution dispatch |
| Agent runtime | context building, model calls, tool loop, session persistence, event emission |
| Managed runtime | ManagedRuntime unified container, coordinated lifecycle, typed component access, hot-reload |
| Hot-swap reload | drain-aware component replacement with pre/post hooks |
| Drain helper | DrainGuard<T> reference-counted guard for tracking outstanding Arc references |
| Health aggregation | HealthStatus::aggregate, healthz_response, readiness gates |
| Runtime invocation | RuntimeInvocation, EmitRequest, EventKind, Control, transport-neutral emit/on facade |
| Runtime stream | RuntimeEventStore, RuntimeStreamAdapter, RuntimeSubscriptionHub, replay + live fanout |
| Runtime safety | session gate, runtime policy, input admission, doom-loop detection, tool output truncation |
| Storage | memory stores, Redis, SQLx, MongoDB, object storage, Qdrant embeddings |
| Context and RAG | context adapters, static/function adapters, optional RAG adapter |
| Queues | optional event publishing through NATS or Redis Streams |
| Configuration | builder, file-based config, environment variable loading, secret indirection |
| Observability | tracing and optional OpenTelemetry integration |
Implement a custom provider
behest does not force one vendor SDK into the core. Implement ChatProvider for any model backend, gateway, local inference service, or internal provider.
use async_trait::async_trait;
use behest::prelude::*;
struct EchoProvider {
id: ProviderId,
}
#[async_trait]
impl ChatProvider for EchoProvider {
fn id(&self) -> ProviderId {
self.id.clone()
}
fn capabilities(&self) -> ProviderCapabilities {
ProviderCapabilities::chat()
}
async fn complete(&self, request: ChatRequest) -> ProviderResult<ChatResponse> {
Ok(ChatResponse {
provider: self.id.clone(),
model: request.model,
message: Message::assistant_text("echo"),
finish_reason: FinishReason::Stop,
usage: None,
raw: None,
})
}
}
Streaming providers can override stream.
Define and execute tools
Tools are explicit runtime objects. Each tool exposes a stable name, a human-readable description, and a JSON Schema argument contract.
use behest::prelude::*;
use serde_json::{json, Value};
let tool = FunctionTool::new(
"echo",
"Echoes the input message.",
json!({
"type": "object",
"properties": {
"message": { "type": "string" }
},
"required": ["message"]
}),
|args: Value| async move {
Ok(args.get("message").cloned().unwrap_or_else(|| Value::Null))
},
)
.read_only()
.concurrency_safe();
let registry = ToolRegistry::new();
registry.register(tool);
Tool calls returned by a provider can be executed through the registry:
use behest::prelude::*;
use serde_json::json;
let call = ToolCall::new("call_1", "echo", json!({ "message": "hello" }));
let output = registry.execute(&call).await?;
Runtime model
At the runtime layer, AgentRuntime orchestrates the full agent loop, while ManagedRuntime provides a unified container for production deployments:
use behest::prelude::*;
let config = AgentConfig::builder()
.with_file("behest.toml")?
.with_env("BEHEST")?
.build()?;
// One-call construction of a fully configured ManagedRuntime.
let managed = config.build_managed().await?;
// Lifecycle: init → start → serve → stop
managed.init_all().await?;
managed.start_all().await?;
managed.serve().await?; // blocks until shutdown signal
managed.stop_all().await?;
The runtime loop:
RunRequest
-> load or create session
-> admit input
-> build context
-> call model provider
-> stream / persist assistant output
-> execute tool calls
-> append tool results
-> repeat until completion, limit, or error
-> emit AgentEvent values
The runtime brings together:
ProviderRegistryContextPipelineToolRuntimeRuntimeStoreRuntimePolicyCompactionServiceSessionGate- optional event publisher
- optional snapshot store
- optional background job pool
Configuration
AgentConfig supports layered configuration:
- defaults
- file sources
- environment variables
- manual builder setters
use behest::prelude::*;
let config = AgentConfig::builder()
.with_file("behest.toml")?
.with_env("BEHEST")?
.build()?;
let runtime = config.into_runtime().await?;
Secrets can be loaded through env:VAR_NAME indirection:
[providers.openai]
api_key = "env:OPENAI_API_KEY"
See behest.toml example for full configuration structure.
Provider adapters
Concrete provider adapters are feature-gated.
| Feature | Adapter | Chat | Stream | Embeddings | Tools |
|---|---|---|---|---|---|
openai |
OpenAiChatAdapter, OpenAiEmbeddingAdapter |
yes | yes | yes | yes |
anthropic |
AnthropicChatAdapter |
yes | yes | no | yes |
Enable adapters:
[dependencies]
behest = { version = "0.5.9", features = ["openai", "anthropic"] }
Feature flags
Click to expand full feature list
Default:
| Feature | Description |
|---|---|
tls-rustls |
Default TLS stack using rustls |
Provider adapters:
| Feature | Description |
|---|---|
openai |
OpenAI-compatible chat and embedding adapters |
anthropic |
Anthropic-compatible chat adapter |
TLS:
| Feature | Description |
|---|---|
tls-rustls |
Enable rustls TLS integration for HTTP / enabled backends |
tls-native |
Enable native TLS integration for HTTP / enabled backends |
Storage:
| Feature | Description |
|---|---|
redis |
Redis-backed store support and Redis Streams primitives |
redis-cluster |
Redis Cluster support; implies redis |
sqlx-postgres |
SQLx PostgreSQL store support |
sqlx-mysql |
SQLx MySQL store support |
sqlx-sqlite |
SQLx SQLite store support |
mongodb |
MongoDB session store support |
object_store |
Object storage support, including AWS S3 |
storage-all |
Redis, PostgreSQL, MySQL, SQLite, and MongoDB storage features |
RAG:
| Feature | Description |
|---|---|
rag |
Core RAG context adapter |
qdrant |
Qdrant embedding store backend |
tantivy |
Tantivy backend support |
rag-all |
Enables rag, qdrant, and tantivy |
Queues:
| Feature | Description |
|---|---|
queue |
Core event publisher traits |
nats |
NATS event publisher |
queue-all |
Enables queue, nats, and redis |
Observability:
| Feature | Description |
|---|---|
otel |
OpenTelemetry tracing integration |
Convenience profile:
| Feature | Description |
|---|---|
full |
Opinionated full runtime profile: OpenAI, Anthropic, Redis, Redis Cluster, NATS, PostgreSQL, MongoDB, OpenTelemetry, all RAG backends, all queue backends, and object storage. It intentionally does not enable sqlx-mysql or sqlx-sqlite. |
Example with selected features:
[dependencies]
behest = {
version = "0.5.9",
default-features = false,
features = ["tls-rustls", "openai", "anthropic", "redis", "queue", "nats"]
}
Error model
behest exposes typed error categories instead of stringly framework failures:
ProviderErrorToolErrorStorageErrorContextErrorRuntimeError- top-level
Error - crate-level
Result<T>
Provider errors distinguish unsupported capabilities, retryable failures, transport failures, invalid responses, and adapter-specific errors.
Tool errors distinguish missing tools, invalid arguments, execution failures, timeouts, and unimplemented external tools.
Lint policy
The crate is intentionally strict:
unsafe_code = "forbid"missing_docs = "deny"unreachable_pub = "deny"clippy::all = "deny"dbg_macro = "deny"expect_used = "deny"todo = "deny"unimplemented = "deny"unwrap_used = "deny"
This project treats public API clarity and failure-path hygiene as part of the runtime contract.
Development
# Format
cargo fmt --all --check
# Check all targets and features
cargo check --all-targets --all-features --locked
# Lint
cargo clippy --all-targets --all-features --locked -- -D warnings
# Test
cargo test --all-features --locked
# Build documentation
RUSTDOCFLAGS="-D warnings" cargo doc --all-features --no-deps --locked
Run the complete local verification set:
cargo fmt --all --check && \
cargo check --all-targets --all-features --locked && \
cargo clippy --all-targets --all-features --locked -- -D warnings && \
cargo test --all-features --locked && \
RUSTDOCFLAGS="-D warnings" cargo doc --all-features --no-deps --locked
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
Dependencies
~46–77MB
~1.5M SLoC