go-bt is a Behavior Tree library for Go.
Designed for background workers, game AI, mundane task automation, and asynchronous logic.
Instead of time.Sleep or infinite while loops, go-bt uses a cooperative multitasking model. Nodes return state instantly, yielding control back to the supervisor.
- Stateless Nodes: Nodes (
Sequence,Selector, etc.) do not hold runtime state. All temporal memory and execution states live entirely within the genericBTContext[T]. - The Magic Numbers: Every node's
Runmethod returns exactly one of three integers:1(Success): The task is done and successful.0(Running): The task needs more time (e.g., waiting on I/O, or sleeping). It yields the thread.-1(Failure): The task failed.
- First-Class Context:
BTContext[T]directly embeds Go's standardcontext.Context, ensuring your trees natively respect global cancellation tokens and timeouts. - Time-Travel Testing: The engine uses an injected clock directly on the context, allowing you to instantly test a 5-minute
TimeoutorSleepnode in your unit tests without actually waiting.
go-bt provides a minimalist set of core nodes that can be combined to create any logical control flow:
- Composites:
Selector(Stateless Priority / Fallback),Sequence(Stateless AND gate),MemSequence(Stateful chronological execution) - Decorators:
Inverter(NOT gate),Optional(Swallows failures),Timeout,Retry,Repeat - Leaves:
Condition(Instant memory read),Action(Execution),Sleep(Non-blocking wait)
Your blackboard is the custom state your tree will read and manipulate. Because the library uses Go Generics, it can be any struct.
type WorkerState struct {
IsConnected bool
PendingTasks int
}You can use the go-bt nodes to assemble your logic. This example asserts that the worker is connected before attempting to process tasks:
package main
import (
"time"
"context"
"go-bt/core"
"go-bt/composite"
"go-bt/leaf"
)
func BuildWorkerTree(cancel context.CancelFunc) core.Command[WorkerState] {
return composite.NewSelector(
composite.NewSequence(
composite.NewSelector(
leaf.NewCondition(func(blackboard *WorkerState) bool {
// Assert connected
// Here you'd check whether the connection is valid, etc.
return blackboard.IsConnected
}),
leaf.NewAction(func(ctx *core.BTContext[WorkerState]) int {
// Make a network connection
// Here you'd actually connect to a database
ctx.Blackboard.IsConnected = true
return 1
}),
),
leaf.NewAction(func(ctx *core.BTContext[WorkerState]) int {
// Process Tasks if connected
if ctx.Blackboard.PendingTasks <= 0 {
cancel()
return 1
}
ctx.Blackboard.PendingTasks--
return 1
}),
),
)
}go-bt provides a concurrent, panic-safe Supervisor to drive your tree. It spins up in the background and continuously ticks the logic without blocking your main application thread.
func main() {
state := &WorkerState{IsConnected: false, PendingTasks: 2}
// Initialize the context with a global cancellation token
ctx, cancel := context.WithCancel(context.Background())
btCtx := core.NewBTContext(ctx, state)
tree := BuildWorkerTree(cancel)
// Create a Supervisor that ticks every 100ms
supervisor := core.NewSupervisor(tree, 100*time.Millisecond, func(err any) {
println("Tree panicked, safely recovered:", err)
})
// Start the daemon in the background
wg := supervisor.Start(btCtx)
// Wait for the global context to be cancelled
wg.Wait()
}You can check examples in the example directory.
Testing temporal logic (like Sleep or Timeout) in CI/CD pipelines usually results in slow, flaky tests. go-bt solves this by exposing the Now function directly on the BTContext.
In your tests, simply replace the standard clock with a mock closure. This allows you to artificially advance time by hours or days in a single microsecond, instantly triggering your Timeout or Sleep node's success/failure states to assert structural correctness.