Skip to content

FFI Bindings

The agentzero-ffi crate exposes the AgentZero agent runtime to non-Rust languages through a single unified crate with feature-gated backends:

LanguageBackendFeature flagOutput
SwiftUniFFIuniffi.swift + .h + .modulemap
KotlinUniFFIuniffi.kt
PythonUniFFIuniffi.py
TypeScript/Node.jsnapi-rsnode.node native addon
  • Rust toolchain (1.80+)
  • just task runner
  • For Node.js bindings: Node.js 18+ and npm

Generate all three at once:

Terminal window
just ffi

Or generate individually:

Terminal window
just ffi-swift
just ffi-kotlin
just ffi-python

Generated files appear in:

crates/agentzero-ffi/bindings/
swift/
kotlin/
python/
Terminal window
just ffi-node

This produces a native .node addon in target/release/.

All language bindings expose the same core API through the AgentZeroController:

TypeDescription
AgentZeroConfigConfiguration: config path, workspace root, provider/model overrides
AgentResponseAgent reply: text + metrics_json
ChatMessageHistory entry: role, content, timestamp_ms
AgentStatusEnum: Idle, Running, Error { message }
AgentZeroErrorEnum: ConfigError, RuntimeError, ProviderError, TimeoutError
MethodDescription
new(config)Create a controller with full configuration
with_defaults(config_path, workspace_root)Create with minimal config
send_message(msg)Send a message and get the agent’s response
send_message_async(msg)Send a message asynchronously (Node.js only)
status()Get the current agent status
get_history()Retrieve conversation history
clear_history()Clear conversation history
get_config()Read the current configuration
update_config(c)Update configuration for subsequent calls
register_tool(name, description)Register a custom tool from the host language
registered_tool_names()List names of all registered FFI tools
version()Return the crate version string
import AgentZeroFFI
let config = AgentZeroConfig(
configPath: "agentzero.toml",
workspaceRoot: FileManager.default.currentDirectoryPath,
provider: "anthropic",
model: nil,
profile: nil
)
let controller = AgentZeroController(config: config)
do {
let response = try controller.sendMessage(message: "Hello from Swift!")
print(response.text)
} catch let error as AgentZeroError {
print("Error: \(error)")
}
import uniffi.agentzero_ffi.*
val config = AgentZeroConfig(
configPath = "agentzero.toml",
workspaceRoot = System.getProperty("user.dir"),
provider = "anthropic",
model = null,
profile = null
)
val controller = AgentZeroController(config)
try {
val response = controller.sendMessage("Hello from Kotlin!")
println(response.text)
} catch (e: AgentZeroError) {
println("Error: $e")
}
from agentzero_ffi import AgentZeroConfig, AgentZeroController, AgentZeroError
config = AgentZeroConfig(
config_path="agentzero.toml",
workspace_root=".",
provider="anthropic",
model=None,
profile=None,
)
controller = AgentZeroController(config)
try:
response = controller.send_message("Hello from Python!")
print(response.text)
except AgentZeroError as e:
print(f"Error: {e}")
import { AgentZeroController } from "agentzero-ffi";
const controller = new AgentZeroController({
configPath: "agentzero.toml",
workspaceRoot: process.cwd(),
provider: "anthropic",
model: undefined,
profile: undefined,
});
const response = controller.sendMessage("Hello from TypeScript!");
console.log(response.text);

The Node.js bindings include additional methods not available in UniFFI bindings:

// Register a custom tool
controller.registerTool("my_tool", "Description of what this tool does");
// List registered tools
const tools = controller.registeredToolNames();
console.log(tools); // ["my_tool"]
// Async message sending (non-blocking)
const response = await controller.sendMessageAsync("Hello!");
console.log(response.text);

sendMessageAsync() runs the agent on a separate thread via spawn_blocking, keeping the Node.js event loop free.

The crate uses a single-crate, dual-backend design:

agentzero-ffi/
src/
lib.rs # Core types + AgentZeroController + UniFFI scaffolding
node_bindings.rs # napi-rs wrappers (behind "node" feature)
uniffi-bindgen.rs # UniFFI CLI for generating bindings
build.rs # Conditional napi-build setup
package.json # npm metadata for napi-rs
  • uniffi feature (default) compiles UniFFI scaffolding and derive macros. The uniffi-bindgen binary uses the compiled .dylib to generate Swift/Kotlin/Python source files.
  • node feature (opt-in) compiles napi-rs wrappers that delegate to the same AgentZeroController. Building produces a .node native addon.

Both backends share the same Rust implementation — the controller manages a global Tokio runtime and bridges synchronous FFI calls to the async agentzero-infra runtime module.

Build a universal static library for iOS simulators and devices:

Terminal window
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios
cargo build -p agentzero-ffi --release --target aarch64-apple-ios
cargo build -p agentzero-ffi --release --target aarch64-apple-ios-sim
just ffi-swift

Then link libagentzero_ffi.a and the generated .swift/.h files into your Xcode project. See the iOS support plan for the full XCFramework packaging workflow.

Terminal window
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
# Requires Android NDK — set ANDROID_NDK_HOME
cargo build -p agentzero-ffi --release --target aarch64-linux-android
just ffi-kotlin

See the Android guide for NDK setup and Gradle integration details.

The uniffi::setup_scaffolding!() macro must live in the crate root (lib.rs), not in a submodule. If you see this error after restructuring, ensure the macro call is directly in lib.rs.

The bindgen binary requires the uniffi-cli feature:

Terminal window
cargo run -p agentzero-ffi --features uniffi-cli --bin uniffi-bindgen generate \
--library target/release/libagentzero_ffi.dylib \
--language swift \
--out-dir bindings/swift

Ensure Node.js 18+ is installed and the node feature is enabled:

Terminal window
cargo build -p agentzero-ffi --release --no-default-features --features node