A distributed behavior tree system built with Elixir. It combines three things into one project:
- A behavior tree engine
- A cluster orchestrator (like a mini-Kubernetes)
- An adaptive load balancer that learns and improves on its own
A behavior tree is a way to organize decision-making into a tree of nodes. The tree gets "ticked" over and over. Each tick, every node does its job and reports back with one of three answers:
- Success - "I did what I needed to do"
- Failure - "I could not do what I needed to do"
- Running - "I am still working on it"
The tree is made up of different types of nodes:
- Sequence - Runs its children one by one from left to right. If any child fails, the whole sequence fails. Think of it like an AND gate.
- Selector - Tries its children one by one from left to right. If any child succeeds, the whole selector succeeds. Think of it like an OR gate.
- Parallel - Runs all its children at the same time. You can set rules for how many need to succeed or fail.
- Action - A leaf node that actually does something (calls a function).
- Condition - A leaf node that checks if something is true or false.
- Decorator - Wraps another node and changes its behavior (like retrying on failure or flipping the result).
The core engine that makes everything else work. Trees are just data (Elixir structs), not processes. A GenServer called the Ticker drives the tree by ticking it on a timer.
Key features:
- Trees are plain data structures, so they can be saved, sent across nodes, or modified at runtime
- Any struct can be a tree node by implementing the
Behave.Bt.Tickableprotocol - A shared Blackboard (backed by ETS) lets nodes pass information to each other
- The TreeMutator lets you change the tree while it is running
Quick example:
import Behave.Bt.TreeBuilder
tree = tree(:my_tree,
sequence([
condition(fn ctx ->
Behave.Bt.Blackboard.get(ctx.blackboard, :ready) == true
end),
action(fn ctx ->
IO.puts("Doing the thing!")
{:success, ctx}
end)
])
)
bb = Behave.Bt.Blackboard.new()
Behave.Bt.Blackboard.put(bb, :ready, true)
{:ok, ticker} = Behave.Bt.Ticker.start_link(tree: tree, blackboard: bb)
Behave.Bt.Ticker.tick_once(ticker)A mini-Kubernetes that uses behavior trees to manage workloads across multiple connected Elixir nodes. It handles three main jobs:
Scheduling - When you want to run a workload, the scheduling tree picks the best node (the one with the fewest running pods) and starts it there.
Health Checking - A behavior tree periodically checks if every pod is still alive and healthy. If a pod fails its health check three times in a row, it gets marked as failed.
Self-Healing - When a pod is marked as failed, the self-healing tree kicks in automatically:
- Detects the failure
- Drains traffic from the broken pod
- Stops the broken pod
- Reschedules it on a healthy node
- Checks that the new pod started correctly (retries up to 5 times)
It uses libcluster for node discovery and Horde for distributed process management. The behavior trees themselves run under Horde, so if the node running a tree goes down, the tree automatically moves to another node and picks up where it left off.
Pods are just GenServers that represent your workloads. You describe what you want with a PodSpec:
spec = %Behave.Cluster.PodSpec{
id: :my_worker,
module: MyApp.Worker,
args: [some: :config],
replicas: 2,
restart_policy: :always
}
Behave.Cluster.Scheduler.schedule(spec)A load balancer where the behavior tree rewrites itself based on how backends are performing. It watches latency, error rates, and throughput, then adjusts its routing strategy automatically.
How it works:
The routing tree is a selector with one branch per backend. Each branch has:
- A SuccessRateGate decorator that blocks the branch if the backend's success rate drops too low (like a circuit breaker)
- A MetricsTracker decorator that records performance data after each request
- A Strategy node that picks which backend to send the request to
Three routing strategies are available:
- Round Robin - Takes turns sending to each backend
- Least Connections - Sends to the backend with the fewest active requests
- Weighted - Sends to backends based on assigned weights
The TreeEvolver runs every few seconds and looks at the metrics. It:
- Removes branches for backends with very high error rates
- Moves the best-performing backend to the front of the list
- Adds backends back when they recover
- Logs every change it makes
Example:
# Start the load balancer with some backends
Behave.LoadBalancer.BackendPool.add_backend(:api_1, latency_ms: 5, error_rate: 0.01)
Behave.LoadBalancer.BackendPool.add_backend(:api_2, latency_ms: 20, error_rate: 0.05)
Behave.LoadBalancer.BackendPool.add_backend(:api_3, latency_ms: 10, error_rate: 0.0)
# Route a request (the tree picks the best backend)
{:ok, result} = Behave.LoadBalancer.RequestRouter.route_request()
# Change a backend's behavior at runtime (chaos testing)
Behave.LoadBalancer.Backend.update_profile(:api_1, error_rate: 0.9)
# The TreeEvolver will automatically detect the degradation and route around itYou need Elixir 1.15 or later.
# Get dependencies
mix deps.get
# Compile
mix compile
# Run tests
mix test
# Start an interactive session
iex -S mixIn your config/config.exs:
config :behave,
cluster_enabled: false, # Set to true to start the cluster orchestrator
load_balancer_enabled: true # Set to true to start the load balancerFor clustering, you also need to start your nodes with names:
iex --sname node1 -S mix
iex --sname node2 -S mixmix testThe tests cover:
- All behavior tree node types (sequence, selector, parallel, decorator)
- Tree mutation and the builder DSL
- The Ticker GenServer
- Backend simulation and metrics collection
- Load balancer strategies
- Pod lifecycle and scheduling
- Self-healing tree structure
lib/
behave/
bt/ # Behavior tree engine (the core)
cluster/ # Distributed cluster orchestrator
load_balancer/ # Adaptive load balancer
test/
behave/
bt/ # Engine tests
cluster/ # Cluster tests
load_balancer/ # Load balancer tests
- libcluster - Automatic cluster formation
- Horde - Distributed supervisor and registry
MIT