Utility AI Planner vs GOAP in Embabel: Choosing the Right Planning Strategy

Embabel agents support three distinct planning strategies—GOAP for goal-directed planning, Utility AI for continuous action scoring, and Hybrid for reactive goal achievement—configured via the PlannerType enum and instantiated through DefaultPlannerFactory.

The embabel/embabel-agent framework provides a flexible, pluggable planning architecture that lets developers choose between classical goal-oriented planning and emergent utility-based behavior. Understanding the technical differences between Utility AI planner vs GOAP implementations is essential for tuning agent reactivity, computational efficiency, and decision quality in complex environments.

Understanding the Three Planner Types

The framework defines all strategies in PlannerType.kt (embabel-agent-api/src/main/kotlin/com/embabel/agent/api/common/PlannerType.kt), with each variant optimized for different behavioral requirements.

GOAP (Goal-Oriented Action Planning)

GOAP (PlannerType.GOAP) implements a classical A* search over world state graphs. The GoapPlanner requires explicit goals (needsGoals = true) and operates on a WorldStateDeterminer that maps mutable world models to deterministic state graphs.

The algorithm expands actions whose preconditions match the current state, applies their effects to generate new states, and uses a heuristic to estimate distance to the goal. This guarantees that if a solution path exists through the action space, the planner will find the optimal sequence. The A* implementation is validated in AStarGoapPlannerTest.kt (embabel-agent-api/src/test/kotlin/com/embabel/plan/goap/astar/AStarGoapPlannerTest.kt).

Use when: The problem can be expressed as a clear end-state, and you need guaranteed plan completion (e.g., "obtain item X" or "reach location Y").

Utility AI Planner

UTILITY (PlannerType.UTILITY) takes an opportunistic approach where needsGoals = false. The UtilityPlanner iterates over all available actions each tick, invoking action.evaluateUtility(context) to score possibilities based on dynamically changing conditions.

The action with the highest net utility value executes immediately. Because there is no goal constraint or search tree expansion, the planner can run indefinitely as long as actions generate positive utility, creating emergent behavior without explicit foresight.

Use when: You need fast-reactive, open-ended behavior where the agent constantly adapts to a shifting utility landscape (e.g., combat tactics, resource gathering priorities).

Hybrid Utility Planner

HYBRID (PlannerType.HYBRID) combines both paradigms. Implemented in HybridUtilityPlanner.kt (embabel-agent-api/src/main/kotlin/com/embabel/plan/utility/HybridUtilityPlanner.kt), it uses the utility-scoring loop for action selection but checks process.getGoalState() after each execution.

If any registered goal becomes satisfied, the process terminates immediately. This provides the reactive speed of utility AI with the clean exit semantics of GOAP.

Use when: You need fast-reactive behavior but require definite termination upon goal achievement (e.g., research actions that fire opportunistically while a "NIRVANA" placeholder goal remains pending).

How Planner Selection Works

The DefaultPlannerFactory.kt (embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/support/DefaultPlannerFactory.kt) centralizes instantiation logic. It examines ProcessOptions.getPlannerType() and returns the appropriate concrete implementation:

when (po.getPlannerType()) {
    PlannerType.GOAP    -> GoapPlanner(worldStateDeterminer)
    PlannerType.UTILITY -> UtilityPlanner()
    PlannerType.HYBRID  -> HybridUtilityPlanner(worldStateDeterminer)
    PlannerType.SUPERVISOR -> SupervisorPlanner()
}

Configuration via ProcessOptions

Programmatic configuration occurs through ProcessOptions.kt (embabel-agent-api/src/main/kotlin/com/embabel/agent/api/common/ProcessOptions.kt). You specify the planner type before launching an agent process:

// GOAP planning with explicit goal requirements
val goapOptions = ProcessOptions.DEFAULT.withPlannerType(PlannerType.GOAP)
val goapProcess = agent.runProcess(goapOptions)

// Utility planning for continuous evaluation
val utilityOptions = ProcessOptions.DEFAULT.withPlannerType(PlannerType.UTILITY)
val utilityProcess = agent.runProcess(utilityOptions)

Annotation-Based Selection

For compile-time configuration, annotate the agent class with @Agent, setting the planner attribute to the desired PlannerType. The annotation processor generates the appropriate ProcessOptions automatically:

@Agent(
    description = "Exploration bot that uses utility-based decisions",
    planner = PlannerType.UTILITY
)
class ExplorerAgent

Implementation Examples

The following patterns demonstrate practical usage of each planner type within the Embabel framework.

Switching Planners Programmatically

import com.embabel.agent.api.common.PlannerType
import com.embabel.agent.api.common.ProcessOptions

// Configure for goal-oriented planning
val strategicOptions = ProcessOptions.DEFAULT
    .withPlannerType(PlannerType.GOAP)

// Configure for reactive utility scoring
val tacticalOptions = ProcessOptions.DEFAULT
    .withPlannerType(PlannerType.UTILITY)

// Launch with selected strategy
val process = agent.runProcess(strategicOptions)

Using the Hybrid Planner for Goal-Bounded Utility

import com.embabel.plan.utility.HybridUtilityPlanner
import com.embabel.agent.spi.world.WorldStateDeterminer

val hybridPlanner = HybridUtilityPlanner(worldStateDeterminer)

// Execution loop terminates when goal is satisfied
while (!process.isGoalSatisfied()) {
    val nextAction = hybridPlanner.selectBestAction()
    process.execute(nextAction)
}

Declarative Agent Configuration

import com.embabel.agent.api.common.PlannerType
import com.embabel.agent.api.annotation.Agent

@Agent(
    description = "Combat agent using hybrid planning",
    planner = PlannerType.HYBRID
)
class CombatAgent {
    // Agent implementation
}

Summary

  • GOAP uses A* graph search through world states and requires explicit goals; best for guaranteed plan completion where end-states are clearly defined.
  • Utility AI scores actions each tick via evaluateUtility(context) without goals; ideal for emergent, opportunistic behavior in dynamic environments.
  • Hybrid combines utility scoring with goal checking, offering reactive speed with deterministic termination.
  • Configuration happens through ProcessOptions programmatically or @Agent annotations, with DefaultPlannerFactory handling instantiation based on PlannerType enum values.

Frequently Asked Questions

When should I choose Utility AI over GOAP in Embabel?

Choose Utility AI when your agent must react instantly to changing conditions without waiting for a full plan calculation, and when success doesn't require a specific sequence of states. Use GOAP when the agent must achieve a precise world state configuration and you can afford the computational cost of A* search through the action graph.

How does the Hybrid planner affect performance compared to pure Utility AI?

The Hybrid planner adds minimal overhead to pure Utility AI—only an additional process.getGoalState() check after each action execution. According to the HybridUtilityPlanner.kt implementation, this single boolean check terminates the loop early when goals satisfy, often reducing total execution time compared to letting a Utility planner run indefinitely.

Can I switch planners at runtime for the same agent instance?

No. The DefaultPlannerFactory instantiates the concrete planner (GoapPlanner, UtilityPlanner, or HybridUtilityPlanner) when agent.runProcess() is called based on the ProcessOptions provided. To change strategies, you must start a new process with different ProcessOptions or redefine the agent class with a different @Agent(planner = ...) annotation value.

What happens if I provide goals to a Utility planner?

The Utility planner ignores goal states entirely (needsGoals = false). Even if you register goals in the process configuration, the UtilityPlanner will not check them during execution. Only GOAP and HYBRID planners examine goal satisfaction, with the latter using it specifically for termination logic.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →