# Bonsai behavior trees: parallel nodes, background jobs and a tick you have to find yourself

> Bonsai implements behavior trees in Rust and ships the same implementation to Python as bonsai-bt, covering sequence, select, loop and parallel nodes for game and robotics agents. The node vocabulary is documented in detail, while the ticking model and the live visualiser are not.

**Sollimann/bonsai** — Rust implementation of behavior trees for deterministic AI (now with Python bindings)

- Repository: https://github.com/Sollimann/bonsai
- Stars: 1,086 · Forks: 42
- Language: Rust
- License: MIT
- Published: 2026-10-14 · Updated: 2026-10-14 · Language: en
- Canonical page: https://hysenlabs.com/projects/sollimann-bonsai

## Ordering rules that live in a tree instead of a switch

Bonsai is a Rust implementation of behavior trees, published on crates.io as `bonsai-bt` and on PyPI under the same name for Python callers. Its one-line description calls the code deterministic AI in Rust with Python bindings, and the Cargo workspace at the repository root splits the code into three members: the library itself, the examples, and the Python binding.

The problem it takes on is that agent logic written as nested conditionals gets expensive to edit. Each new state adds branches that touch every state already there. A behavior tree moves that ordering into data. Each node runs a process, returns a signal, and the parent chooses the next child from that signal, so a new branch is a new node instead of a new clause in an else chain. The topic list carries `finite-state-machine` right next to `behavior-tree`, and the difference between those two approaches is the whole point here. A hand-written state machine keeps every transition in one table and fires whenever the code happens to poll the current state, which is convenient right up to the point where a second system wants to drive the same agent. A tree writes the order where the children are declared, and a failing child hands control back to its own parent.

## Success, Failure and Running travel back to the parent

Every node in a Bonsai tree ends with one of three signals, and those three are the entire control protocol. `Success` and `Failure` mean the process has finished, while `Running` says it is not done yet and should be visited again later. The parent picks what runs next from that value and from nothing else.

The `Behavior` enum turns the three signals into control flow. `Sequence([A, B])` moves to the second child when the first succeeds, `Select([A, B])` tries the first child and falls through to the second when the first fails, `Invert(A)` swaps success for failure, and `If(condition, A, B)` takes its branch from the condition's signal. No node in the tree reads the world by itself. Whatever data a node needs travels with the tree, and that is the part that connects to the deterministic AI framing: the same tree over the same inputs reaches the same decisions, so a replay does not need a recorded timeline to line up.

## Loops, parallel branches, and the order After claims

`While(LoopCondition, [A, B])` repeats its children for as long as the condition node keeps running, and the condition is checked between the children rather than only before the first one. `WhileAll` opens with the same sentence, word for word, and the difference sits in a single clause: after all children have completed successfully, the condition is checked. Two entries in that list read identically until the final line, which is worth knowing before either gets copied into a tree of your own.

The parallel entries run children at the same time and wait on different outcomes. `WhenAll([A, B])` waits for every child to succeed, `WhenAny([A, B])` for the first success, and `Race([A, B])` for whichever child finishes first, whether it succeeded or failed. `After([A, B])` is the odd one out. It runs the two children at the same time and still holds the second child back until the first one succeeds. Those two requirements pull against each other, since a branch that cannot start before another branch succeeds is a dependency, and `Sequence` already covers that case without claiming concurrency. Nothing else in the project states what `After` synchronises on, so the ordering claim rests on that one entry.

## Long jobs run on background threads behind a channel

Nodes are meant to return instantly. A tree that blocks inside one action stops ticking everything above it, so work lasting seconds or minutes has to leave the traversal before it starts. The approach here is to dispatch that work into background threads and read the task's status back through a channel, with the node reporting `Running` until the channel reports otherwise. The project points at an async drone example in the `/examples` folder for the pattern, and the Rust sources sit under `examples/src/`.

That mechanism also marks the edge of the design. A status channel is not a scheduler. The write-up names no priority, no timeout, no cancellation and no cap on how many threads a tree may open at once, so a tree that spawns a thread per query will do exactly that. Anything that must be interrupted, retried on a timer or bounded belongs in the layer you already control, not in the node.

## A wildcard dependency line and a PyPI package of the same name

Rust callers add one line to `Cargo.toml`. The snippet the project gives is a wildcard, so the file you write does not pin a version:

```toml
[dependencies]
bonsai-bt = "*"
```

That line resolves `bonsai-bt` on crates.io, the package name behind this repository. A wildcard that short leaves the version unpinned in the file itself, so a later rebuild can resolve a different version than the one you ran your tests against. The badges pin the floor for that version: minimum rustc 1.72, and docs.rs carries the API pages. Pin the version by hand once you know which one you are shipping.

Python callers take the same implementation from PyPI under the same package name:

```bash
pip install bonsai-bt
```

After that command succeeds you get the importable module and the examples that live in `bonsai-py/`, while the Rust examples stay in the `examples/` folder.

## Three workspace members, one published crate

The root manifest declares a workspace on resolver 2 and builds the examples next to the library, so a broken example fails the same build as a broken node:

```toml
[workspace]
resolver = "2"
members = ["bonsai", "examples", "bonsai-py"]
```

Publishing runs through the Makefile, and it names exactly one package out of the three members:

```makefile
dry-run:
	cargo publish --package bonsai-bt --dry-run

publish:
	cargo publish --package bonsai-bt
```

So `examples` is compiled and never shipped, and `bonsai-py` builds in the same workspace while reaching users through a different channel. Running the `dry-run` target first checks the packaged manifest without uploading anything. The Makefile also lists all three targets under `.PHONY`, so they never collide with a file that happens to carry one of those names.

## ALSA and udev libraries appear only in the Makefile

Contributors are told to install system libraries before building, and the instructions live in the Makefile rather than in any guide:

```makefile
.PHONY: install-dev-deps dry-run publish

install-dev-deps:
	sudo apt-get update && sudo apt-get install libudev-dev pkg-config librust-alsa-sys-dev
```

Two of those three packages have nothing to do with deciding which behavior runs next. `librust-alsa-sys-dev` is the ALSA audio binding and `libudev-dev` is device access, with `pkg-config` supplying the compiler glue that locates both. No guide text explains why a behavior tree library needs an audio binding or a device library at build time, and the rest of the file list is ordinary Rust tooling: `.pre-commit-config.yaml`, `.rustfmt.toml`, `codecov.yml`, `CONTRIBUTING.md`, `DEVELOPMENT.md`. Anyone on macOS, Windows or a Linux distribution without those packages has no documented substitute, and the `sudo` in the line means the target will not run unattended.

## The live visualiser and the tick API arrive undocumented

The last push was on 2026-09-08, the LICENSE file is MIT, and the newest release, v0.12.0 from 2026-05-14, added live visualisation of behavior trees.

No command, flag or port for starting that visualiser appears anywhere in the README, and the v0.9.0 API change to `tick`, the blackboard and `State` is not explained there either. The usage text stops after the dependency line and the pip install line, then hands the rest to two linked files, `docs/concepts/README.md` and `examples/README.md`. `WaitForever` turns up once, as the condition for an endless `While`, which tells you the name exists and nothing about how a running loop is interrupted.

The topics promise more than the pages deliver. Alongside `behavior-tree` and `deterministic` sit `ros`, `ros2`, `bevy`, `unreal-engine` and `piston`, and not one integration step for any of them is given. The contents list also links an anchor called `#similar-crates` under the label Honorable Mentions, and that anchor has no body in the README, so the alternatives it points at are never named.

## Conclusion

Bonsai fits teams that want behavior ordering held as data instead of nested branches, in Rust or Python, with parallel primitives and a thread plus channel pattern already worked out. It does not fit anyone expecting ROS, Bevy or Unreal wiring, a dependency line that pins a version, or a documented way to start the live visualiser. Before adopting, read `docs/concepts/README.md` and the sources under `examples/src/`, since the README hands the ticking model to both, and pin the crate version in your own `Cargo.toml`.

## FAQ

### How do I install Bonsai in a Rust project?

Add the crate under its published name to the `[dependencies]` section of your `Cargo.toml`, exactly as the snippet `bonsai-bt = "*"` shows. That wildcard leaves the version unpinned in the file, so set it to a specific version once you have picked one.

### How do I run a long-running task inside a Bonsai behavior tree without blocking it?

Dispatch the work into background threads and read its status back through a channel, while the node returns `Running` until that channel reports completion. The project points at an async drone example in the `/examples` folder for the pattern.

### Which loop and parallel primitives does Bonsai provide?

Loops come as `While`, `WhileAll` and the `While(WaitForever, [A, B])` idiom, while parallel entries are `WhenAll`, `WhenAny`, `Race` and `After`. The condition in a `While` is checked between children, and in `WhileAll` only after every child has succeeded.

### What does Bonsai need before it builds from source?

A minimum rustc of 1.72 per the build badge, and on a Debian-family system the Makefile target `install-dev-deps`, which runs `sudo apt-get update && sudo apt-get install libudev-dev pkg-config librust-alsa-sys-dev`. No equivalent setup path is documented for macOS or Windows.

### Does Bonsai integrate with ROS or with game engines such as Bevy and Unreal Engine?

Those names, including `ros`, `ros2`, `bevy` and `unreal-engine`, sit in the repository topic list only. The documentation covers the crates.io package and the PyPI package, and gives no integration steps for any of them.

### Is Bonsai still being worked on?

The last push on the default branch `main` was on 2026-09-08 and the repository is not archived. Its own contents list links a Kanban board on GitHub where the planned work is tracked.

## Sources

- [Issues](https://github.com/Sollimann/bonsai/issues)
- [License: MIT](https://github.com/Sollimann/bonsai/blob/main/LICENSE)
- [README](https://github.com/Sollimann/bonsai/blob/main/README.md)
- [Releases](https://github.com/Sollimann/bonsai/releases)
- [Sollimann/bonsai on GitHub](https://github.com/Sollimann/bonsai)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/sollimann-bonsai
