GOAP agents
GOAP is an algorithmic planning approach that uses A* search find optimal action sequences that satisfy the goal conditions while minimizing the total cost. Unlike LLM-based planners that use an LLM to generate plans, a GOAP agent algorithmically discovers action sequences based on predefined goals and actions.
GOAP planners work with three main concepts:
- State: Represents the current state of the world.
- Actions: Define what can be done, including preconditions, effects (beliefs), costs, and execution logic.
- Goals: Define target conditions, heuristic costs, and value functions.
Prerequisites
Ensure your environment and project meet the following requirements:
- JDK 17+
- Kotlin 2.2.0+
- Gradle 8.0+ or Maven 3.8+
Add the Koog package as a dependency:
Get an API key from an LLM provider or run a local LLM via Ollama. For more information, see Quickstart.
Examples on this page assume that you have set the OPENAI_API_KEY environment variable.
In Koog, you define a GOAP agent using a DSL by declaratively specifying the goals and actions.
To create a GOAP agent, you need to:
- Define the state as a data class with properties representing various aspects specific to your goal.
- Create a GOAPPlanner instance using the goap() function.
- Wrap the planner with AIAgentPlannerStrategy and pass it to the PlannerAIAgent constructor.
Note
The planner selects individual actions and their sequence. Each action includes a precondition that must hold true for the action to be executed and a belief that defines the predicted outcome. For more information about beliefs, see State beliefs compared to actual execution.
In the following example, GOAP handles high-level planning for creating an article (outline → draft → review → publish), while the LLM performs the actual content generation within each action.
// Define a state for content creation
data class ContentState(
val topic: String,
val hasOutline: Boolean = false,
val outline: String = "",
val hasDraft: Boolean = false,
val draft: String = "",
val hasReview: Boolean = false,
val isPublished: Boolean = false
): GoapAgentState<String, String>(topic) {
override fun provideOutput(): String = draft
}
// Create GOAP planner with LLM-powered actions
val planner = AIAgentPlannerStrategy.goap("content-planner", ::ContentState) {
// Define actions with preconditions and beliefs
action(
name = "Create outline",
precondition = { state -> !state.hasOutline },
belief = { state -> state.copy(hasOutline = true, outline = "Outline") },
cost = { 1.0 }
) { ctx, state ->
// Use LLM to create the outline
val response = ctx.llm.writeSession {
appendPrompt {
user("Create a detailed outline for an article about: ${state.topic}")
}
requestLLM()
}
state.copy(hasOutline = true, outline = response.content)
}
action(
name = "Write draft",
precondition = { state -> state.hasOutline && !state.hasDraft },
belief = { state -> state.copy(hasDraft = true, draft = "Draft") },
cost = { 2.0 }
) { ctx, state ->
// Use LLM to write the draft
val response = ctx.llm.writeSession {
appendPrompt {
user("Write an article based on this outline:\n${state.outline}")
}
requestLLM()
}
state.copy(hasDraft = true, draft = response.content)
}
action(
name = "Review content",
precondition = { state -> state.hasDraft && !state.hasReview },
belief = { state -> state.copy(hasReview = true) },
cost = { 1.0 }
) { ctx, state ->
// Use LLM to review the draft
val response = ctx.llm.writeSession {
appendPrompt {
user("Review this article and suggest improvements:\n${state.draft}")
}
requestLLM()
}
println("Review feedback: ${response.content}")
state.copy(hasReview = true)
}
action(
name = "Publish",
precondition = { state -> state.hasReview && !state.isPublished },
belief = { state -> state.copy(isPublished = true) },
cost = { 1.0 }
) { ctx, state ->
println("Publishing article...")
state.copy(isPublished = true)
}
// Define the goal with a completion condition
goal(
name = "Published article",
description = "Complete and publish the article",
condition = { state -> state.isPublished }
)
}
// Create and run the agent
val agentConfig = AIAgentConfig(
prompt = prompt("writer") {
system("You are a professional content writer.")
},
model = OpenAIModels.Chat.GPT4o,
maxAgentIterations = 20
)
val agent = AIAgent(
promptExecutor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")),
strategy = planner,
agentConfig = agentConfig
)
suspend fun main() {
val result = agent.run("The Future of AI in Software Development")
println("Final state: $result")
}
// Define a state for content creation
static class ContentState extends GoapAgentState<String, String> {
public String topic;
public boolean hasOutline = false;
public String outline = "";
public boolean hasDraft = false;
public String draft = "";
public boolean hasReview = false;
public boolean isPublished = false;
public ContentState(String topic) {
super(topic);
this.topic = topic;
}
public ContentState copy(boolean hasOutline, String outline, boolean hasDraft,
String draft, boolean hasReview, boolean isPublished) {
ContentState state = new ContentState(topic);
state.hasOutline = hasOutline;
state.outline = outline;
state.hasDraft = hasDraft;
state.draft = draft;
state.hasReview = hasReview;
state.isPublished = isPublished;
return state;
}
@Override
public String provideOutput() {
return draft;
}
}
public static void main(String[] args) {
var promptExecutor = PromptExecutor.builder()
.openAI("OPENAI_API_KEY")
.build();
var strategy = AIAgentPlannerStrategy.builder("content-planner")
.goap(ContentState::new)
.action("Create outline", builder -> builder
.precondition(state -> !state.hasOutline)
.belief(state -> state.copy(true, "Outline", false, "", false, false))
.cost(state -> 1.0)
.execute((context, state) -> {
String response = context.llm().writeSession(session -> {
session.appendPrompt(prompt -> {
prompt.user("Create a detailed outline for an article about: " + state.topic);
return null;
});
return session.requestLLM().getContent();
});
return state.copy(true, response, state.hasDraft, state.draft,
state.hasReview, state.isPublished);
})
)
.action("Write draft", builder -> builder
.precondition(state -> state.hasOutline && !state.hasDraft)
.belief(state -> state.copy(state.hasOutline, state.outline, true, "Draft", false, false))
.cost(state -> 2.0)
.execute((context, state) -> {
String response = context.llm().writeSession(session -> {
session.appendPrompt(prompt -> {
prompt.user("Write an article based on this outline:\n" + state.outline);
return null;
});
return session.requestLLM().getContent();
});
return state.copy(state.hasOutline, state.outline, true, response,
state.hasReview, state.isPublished);
})
)
.action("Review content", builder -> builder
.precondition(state -> state.hasDraft && !state.hasReview)
.belief(state -> state.copy(state.hasOutline, state.outline, state.hasDraft,
state.draft, true, false))
.cost(state -> 1.0)
.execute((context, state) -> {
String response = context.llm().writeSession(session -> {
session.appendPrompt(prompt -> {
prompt.user("Review this article and suggest improvements:\n" + state.draft);
return null;
});
return session.requestLLM().getContent();
});
System.out.println("Review feedback: " + response);
return state.copy(state.hasOutline, state.outline, state.hasDraft,
state.draft, true, state.isPublished);
})
)
.action("Publish", builder -> builder
.precondition(state -> state.hasReview && !state.isPublished)
.belief(state -> state.copy(state.hasOutline, state.outline, state.hasDraft,
state.draft, state.hasReview, true))
.cost(state -> 1.0)
.execute((context, state) -> {
System.out.println("Publishing article...");
return state.copy(state.hasOutline, state.outline, state.hasDraft,
state.draft, state.hasReview, true);
})
)
.goal("Published article", builder -> builder
.description("Complete and publish the article")
.condition(state -> state.isPublished)
)
.build();
var agent = AIAgent.builder()
.plannerStrategy(strategy)
.promptExecutor(promptExecutor)
.llmModel(OpenAIModels.Chat.GPT4o)
.systemPrompt("You are a professional content writer.")
.maxIterations(20)
.build();
String result = agent.run("The Future of AI in Software Development");
System.out.println("Final state: " + result);
}
Custom cost functions
As A* search uses cost as a factor in finding the optimal sequence of actions, you can define custom cost functions for actions and goals to guide the planner:
State beliefs compared to actual execution
GOAP distinguishes between the concepts of beliefs (optimistic predictions) and actual execution:
- Belief: What the planner thinks will happen, used for planning.
- Execution: What actually happens, used for real state updates.
This allows the planner to make plans based on expected outcomes while handling actual results properly:
action(
name = "Attempt complex task",
precondition = { state -> !state.taskComplete },
belief = { state ->
// Optimistic belief: task will succeed
state.copy(taskComplete = true)
},
cost = { 5.0 }
) { ctx, state ->
// Actual execution might fail or have different results
val success = performComplexTask()
state.copy(
taskComplete = success,
attempts = state.attempts + 1
)
}
.action("Attempt complex task", builder -> builder
.precondition(state -> !state.taskComplete)
.belief(state -> {
// Optimistic belief: task will succeed
return state.copy(true, state.attempts);
})
.cost(state -> 5.0)
.execute((context, state) -> {
// Actual execution might fail or have different results
boolean success = performComplexTask();
return state.copy(success, state.attempts + 1);
})
)