Skip to main content

Embedding flux in Rust

flux-sdk embeds the same policy-gated flow engine used by the CLI. You provide a model provider and a workspace root; the SDK wires the Flux-Lang agent loop, built-in operations, safety envelope, and session storage.

There is only one agent turn engine. Client, the CLI, sub-agents, and served agents all assemble flux_flow::engine::FlowEngine, whose turn loop is an authored agent-loop.flux. FlowClient is the other view of the same architecture: it exposes an individual flow's lifecycle directly instead of driving a conversation.

Install

The SDK and its dependency closure are published on crates.io under the codewandler- prefix (the short flux-* package names are owned by unrelated projects):

cargo add codewandler-flux-sdk codewandler-flux-providers

Package names are prefixed, but Rust imports stay short: use flux_sdk::... and use flux_providers::.... The SDK is provider-neutral; concrete Anthropic, OpenAI, OpenRouter, Ollama, Bedrock, and subscription-backed providers live in flux-providers.

Choose a surface

NeedUseWhy
Run conversational agent turnsflux_sdk::ClientThe complete self-hosted Flux-Lang agent loop, session, tools, and envelope behind one run call.
Parse or run one flow explicitlyflux_sdk::FlowClientDirect control over parsing, analysis, optimization, seeded inputs, and execution. This is the recommended AI-app flow API.
Build a flow in Rustflux_sdk::dslTyped builder ergonomics that produce the same DraftAst; execute it with FlowClient.
Build language tooling or a custom hostflux_langThe standalone AST, parser/formatter, analyzer, optimizer, schema, DSL, program declarations, and reference-interpreter traits.
Own engine/session internalsflux_flowAdvanced FlowEngine, guarded runtime adapters, durable store, replay, suspension, and voice-driver integration.

For shell and deployment surfaces, use flux flow run for an already authored flow, flux run for an agent turn, and flux app run for an event-driven multi-agent program.

Client: conversational turns

let provider = Box::new(flux_providers::anthropic::anthropic_from_env()?);
let client = flux_sdk::Client::builder()
.model("anthropic/opus")
.build(provider, ".")?;

let out = client.run("Summarize the README").await?;
println!("{}", out.text); // TurnOutput: text, tool_calls, usage

Each Client turn runs the same FlowEngine as the CLI. Typed stages detect intent and call exact provider-native operation schemas; agent-loop.flux owns evidence gathering, decisions, action-batch approval/execution, repair, and presentation. The default client loop does not ask the model to emit per-turn executable Flux. The explicit op.register exception lets a model propose source for exactly one composite operation; the host parses, analyzes, scopes, and guards it before installation rather than executing it on receipt.

The builder carries allow/deny permission rules, auto_approve(true) for trusted headless use, an agent profile plus authored instructions and context blocks, model/token/iteration limits, explicit agent_loop(AgentLoopSpec), typed register_op(stage_fn(...)) stages, and the OS-sandbox posture. Because a library has no approval UI, reads are pre-allowed and other gated operations deny by default unless you provide a broader policy.

Every SDK agent retains Flux's embedded harness protocol. Client::builder() and AgentSpec::new select AgentProfile::Coding; use .profile(AgentProfile::General).instructions("…") for a custom non-coding persona. .instructions("…") specializes the selected profile and never replaces the harness. This is the pre-1.0 replacement for the former .system_prompt("…") API.

Let the agent ask your user

An embedded conversational client can install a schema-driven question handler. Installing it adds and pre-allows user.ask; an explicit deny still wins. The runtime validates the request before your code sees it and validates the returned value again:

use std::sync::Arc;
use async_trait::async_trait;
use flux_sdk::interaction::{
InteractionCapabilities, InteractionResponse, UserInteraction, UserInteractionRequest,
};

struct ProductUi;

#[async_trait]
impl UserInteraction for ProductUi {
fn capabilities(&self) -> InteractionCapabilities {
InteractionCapabilities::text()
}

async fn request(
&self,
request: UserInteractionRequest,
) -> flux_core::Result<InteractionResponse> {
// Render request.prompt and request.schema, then return reviewed JSON.
todo!()
}
}

let client = flux_sdk::Client::builder()
.with_user_interaction(Arc::new(ProductUi))
.build(provider, ".")?;

Yes/no, enum and unique enum-array schemas naturally map to toggles and selectors. Unsupported shapes can use a JSON editor. Audio-capable hosts return InteractionCapabilities::with_audio() and resolve PromptAudioRef.asset_id inside their own asset store; recordings never enter the runtime response. This interaction contract is deliberately separate from Approver—a product answer cannot approve an effect.

FlowClient: one flow's lifecycle

let client = flux_sdk::FlowClient::builder()
.model("anthropic/opus")
.auto_approve(true)
.build(provider, ".")?; // provider: Arc<dyn flux_provider::Provider>

let ast = client.parse(r#"flow show-doc -> String
$doc = read("README.md")
return $doc"#)?;
client
.analyze(&ast)
.map_err(|d| flux_core::Error::Other(format!("{d:?}")))?;
let out = client.execute(&ast).await?;

Use run_flow as the parse/analyze/execute convenience for stored text plus inputs. The full registration, policy, optimization, result, and suspension contract is in the FlowClient guide.

dsl: author the AST in Rust

use flux_sdk::{dsl::*, FlowClient};
use serde_json::json;

let flow = Flow::named("read_each")
.body(|b| {
b.each("f", lit(json!(["a.txt", "b.txt"])), |e| {
e.collect("contents");
e.body(|b| {
b.call("read", [var("f")]);
});
});
b.ret(var("contents"));
})
.build();

client
.analyze(&flow)
.map_err(|d| flux_core::Error::Other(format!("{d:?}")))?;
let out = client.execute(&flow).await?;

The DSL is a construction convenience, not a second language or a type-checker. It produces the same flux_lang::ast::DraftAst as text parsing; always analyze it against the actual operation catalog before execution.

The lower-level libraries

Most applications should stay on Client or FlowClient. Drop below the SDK only when you need to replace a host boundary rather than configure it:

  • codewandler-flux-lang / flux_lang is the provider- and tool-independent language library. Its reference interpreter runs only through injected OpHost, ValueStore, and FlowSink traits. Use it for parsers, editors, validators, schemas, custom catalogs, or a non-flux host.
  • codewandler-flux-flow / flux_flow adapts the language to a provider, the real tool registry, Executor::dispatch, event/value stores, and agent sinks. Use FlowEngine when the host must own cancellable turns, adaptive batch approval, cross-turn await sessions, replay, or flow-driven voice.

Direct flux-lang interpretation does not silently inherit flux's concrete safety envelope: the embedder supplies the host traits. FlowClient, Client, and FlowEngine already wire operation calls through the envelope and are therefore the safer starting points for effectful applications.

Sub-agents

flux_sdk::subagents re-exports the whole delegation bundle, so a host wires named child agents without depending on flux's orchestration crate directly. Attach it with ClientBuilder::with_sub_agents (conversational) or FlowClient::with_sub_agents (flow); either way the task tool joins the client's catalog and a plan that calls task(role, …) delegates to that role's child agent through the same authorization → approval → guarded-IO envelope. The child's usage is folded into the parent turn's recorded spend.

use std::sync::Arc;

use flux_sdk::subagents::{ProviderFactory, Role, RoleRegistry, SpawnLimits, SubAgents};
use flux_sdk::tools::ToolRegistry;
use flux_sdk::{Client, Provider};

// Roles can be registered in memory (the multi-tenant path) or loaded from `.flux/agents/*.md`
// with `RoleRegistry::try_load` / `try_load_project`.
let roles = RoleRegistry::from_roles([Role {
name: "scout".into(),
description: "read-only reconnaissance".into(),
model: None, // inherit the spawner's default model
thinking: None,
effort: None,
agent_loop: None, // `adaptive`, or inline Flux-Lang for this role's outer loop
tools: Some(Vec::new()), // a leaf: `None` inherits every parent tool, `Some([])` grants none
prompt: "You are a scout. Investigate and report findings tersely.".into(),
}]);

// A fresh provider per child — children cannot share one `Box<dyn Provider>`.
let factory: ProviderFactory = Arc::new(|| Ok(Box::new(build_provider()) as Box<dyn Provider>));

let sub_agents = SubAgents::new(roles, ToolRegistry::new(), factory, "anthropic/opus", 4096)
.with_limits(SpawnLimits {
max_iterations: 30,
max_tokens: 4096,
wall_clock: Some(std::time::Duration::from_secs(120)),
});

let client = Client::builder()
.model("anthropic/opus")
.with_sub_agents(sub_agents)
.build(provider, ".")?;

SubAgents::new(roles, child_base, provider_factory, default_model, max_tokens) takes the child tool surface explicitly rather than reusing the parent's assembled registry, so a child's reachable ops are auditable and independent of parent registration order; each role's tools allowlist subsets it. with_authorization, with_approver, with_audit, and with_reasoning cover the remaining knobs, and with_max_depth (default 1, children are leaves) bounds nesting. with_sub_agents applies a 10-minute wall_clock when the bundle sets none, so a hung child cannot run forever; with_sub_agents_policy is the sibling that also pins an explicit AdaptiveLoopPolicy for every child. Roles authored as markdown parse through try_parse_role, which rejects malformed frontmatter rather than defaulting it — a missing tools key means "inherit the parent's tools" and is therefore security-relevant.

cargo run -p codewandler-flux-sdk --example sub_agent

What else flux_sdk re-exports

The SDK is the single dependency an embedder needs: each module below re-exports a contract from an internal crate so consumer code names only flux_sdk::….

ModuleContractGuide
toolsTool, tool_fn, ToolSpec/Risk, ToolContext, ToolResult, ToolRegistry — custom operations.FlowClient
approvalApprover, ApprovalChoice, RiskApprover, IntentSet — your approval policy.Safety and approvals
interactionUserInteraction, request/response and capability types — schema-driven questions rendered by your host.above
authorizationAuthorizationPolicy, Caller, Trust, ExecutionAuthorization, IdentityCell — the policy floor and resolved identity.Safety and approvals
observeMessage, TurnSummary, RunEvent, ModelCost, EfficiencySummary, RunDiff/DiffRow, the EventStore/FlowStore handles, and the evidence-gating types (ToolGroup, SignalMatch, Observation, KIND_SIGNAL).Sessions
subagentsSubAgents, SpawnLimits, ProviderFactory, Role, RoleRegistry, try_parse_role.above, and Skills and roles
whatifCounterfactual, WhatIf, WhatIfSpec, SweepReport, OffTape, Divergence — world-pinned counterfactuals.Agent Lab
datasourceLiveDatasource, LiveAccess, LiveSchema/LiveEntity, typed filters, Page/PageRequest, Row/Reference.Datasources
voiceVoiceSink, VoiceReply, RealtimeProvider, RealtimeConfig.Realtime voice
recipesParameterized DSL flow builders.below

Four modules are feature-gated and off by default, so their dependency closures stay out of a default build: test (test-kit), providers (providers), plugins (plugins), and pricing (pricing).

Zero-key testing

Provider is a small trait, so tests and examples can drive the real engine and envelope with a hermetic provider and no API key. The repository ships runnable examples:

cargo run -p codewandler-flux-sdk --example client_basic
cargo run -p codewandler-flux-sdk --example parameterized_flow
cargo run -p codewandler-flux-sdk --example dsl_loops

client_basic exercises the adaptive authored loop; parameterized_flow parses and executes authored text; dsl_loops executes a Rust-authored each loop against a temporary workspace.

Recipes

flux_sdk::recipes is a cookbook of parameterized DSL builders that can be analyzed and executed by any FlowClient:

ModuleRecipes
recipes::routingroute_intent
recipes::lookupanswer_with_fallback
recipes::batchmap_each, repeat_until, poll_for, race_first
recipes::resilienceretry_with_backoff, with_timeout, with_budget, try_catch
recipes::fanoutparallel_all
recipes::dispatchmatch_value
recipes::composeresilient_call

The CLI exposes the same cookbook through flux preset list, flux preset help, and flux preset ... --run.