Skip to content

Commit ef983ef

Browse files
[agents] Introduce planner agent type. Implement GOAP (#1232)
Introduce new "planner" agent type, in addition to existing "graph" and "functional". These agents can work iteratively by building, executing and updating the plan. Two planner strategies are provided out of the box - simple LLM and GOAP. New agent type and built-in planners are implemented as a separate optional dependency (module) - `agents-planner` --------- Co-authored-by: Andrey Bragin <[email protected]>
1 parent 009ed2c commit ef983ef

File tree

29 files changed

+2115
-13
lines changed

29 files changed

+2115
-13
lines changed

agents/agents-core/src/commonMain/kotlin/ai/koog/agents/core/agent/entity/AIAgentState.kt

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,28 @@ import ai.koog.agents.core.utils.ActiveProperty
44
import kotlinx.coroutines.sync.Mutex
55
import kotlinx.coroutines.sync.withLock
66

7+
/**
8+
* Represents the state of an AI agent.
9+
*/
710
@OptIn(ExperimentalStdlibApi::class)
8-
internal class AIAgentState internal constructor(
11+
public class AIAgentState(
912
iterations: Int = 0,
1013
) : AutoCloseable {
11-
var iterations: Int by ActiveProperty(iterations) { isActive }
14+
/**
15+
* The number of iterations that have been completed since the agent was created.
16+
*/
17+
public var iterations: Int by ActiveProperty(iterations) { isActive }
1218

1319
private var isActive = true
1420

1521
override fun close() {
1622
isActive = false
1723
}
1824

19-
internal fun copy(): AIAgentState {
25+
/**
26+
* Creates a copy of the current state.
27+
*/
28+
public fun copy(): AIAgentState {
2029
return AIAgentState(
2130
iterations = iterations
2231
)
@@ -33,12 +42,16 @@ internal class AIAgentState internal constructor(
3342
* @constructor Creates a new instance of AIAgentStateManager with the initial state,
3443
* defaulting to a new `AIAgentState` if not provided.
3544
*/
36-
public class AIAgentStateManager internal constructor(
45+
public class AIAgentStateManager(
3746
private var state: AIAgentState = AIAgentState()
3847
) {
3948
private val mutex = Mutex()
4049

41-
internal suspend fun <T> withStateLock(block: suspend (AIAgentState) -> T): T = mutex.withLock {
50+
/**
51+
* Executes the provided suspending [block] of code with exclusive access to the current state.
52+
* @return The result of [block].
53+
*/
54+
public suspend fun <T> withStateLock(block: suspend (AIAgentState) -> T): T = mutex.withLock {
4255
val result = block(state)
4356
val newState = AIAgentState(
4457
iterations = state.iterations

agents/agents-core/src/commonMain/kotlin/ai/koog/agents/core/agent/entity/AIAgentStorage.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ public class AIAgentStorageKey<T : Any>(public val name: String) {
2121
* @param name The name of the storage key, used to uniquely identify it.
2222
* @return A new instance of [AIAgentStorageKey] for the specified type.
2323
*/
24-
public inline fun <reified T : Any> createStorageKey(name: String): AIAgentStorageKey<T> = AIAgentStorageKey<T>(name)
24+
public fun <T : Any> createStorageKey(name: String): AIAgentStorageKey<T> = AIAgentStorageKey(name)
2525

2626
/**
2727
* Concurrent-safe key-value storage for an agent.
2828
* You can create typed keys for your data using the [createStorageKey] function and
2929
* set and retrieve data using it by calling [set] and [get].
3030
*
3131
*/
32-
public class AIAgentStorage internal constructor() {
32+
public class AIAgentStorage {
3333
private val mutex = Mutex()
3434
private val storage = mutableMapOf<AIAgentStorageKey<*>, Any>()
3535

agents/agents-core/src/commonMain/kotlin/ai/koog/agents/core/agent/entity/AIAgentSubgraph.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ public open class AIAgentSubgraph<TInput, TOutput>(
222222

223223
@OptIn(InternalAgentsApi::class)
224224
private suspend fun executeWithInnerContext(context: AIAgentGraphContextBase, initialInput: TInput): TOutput? {
225-
logger.info { formatLog(context, "Executing subgraph '$name'") }
225+
logger.debug { formatLog(context, "Executing subgraph '$name'") }
226226

227227
var currentNode: AIAgentNodeBase<*, *> = start
228228
var currentInput: Any? = initialInput

agents/agents-core/src/commonMain/kotlin/ai/koog/agents/core/agent/exception/AIAgentException.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public open class AIAgentException(problem: String, throwable: Throwable? = null
2727
* @param node The node in which the agent becomes stuck.
2828
* @param output The output produced by the node that doesn't match any edge conditions.
2929
*/
30-
internal class AIAgentStuckInTheNodeException(node: AIAgentNodeBase<*, *>, output: Any?) :
30+
public class AIAgentStuckInTheNodeException(node: AIAgentNodeBase<*, *>, output: Any?) :
3131
AIAgentException(
3232
"When executing agent graph, stuck in node ${node.name} " +
3333
"because output $output doesn't match any condition on available edges."
@@ -57,5 +57,5 @@ public class AIAgentMaxNumberOfIterationsReachedException(maxNumberOfIterations:
5757
*
5858
* @param message A descriptive message explaining the reason for termination.
5959
*/
60-
internal class AIAgentTerminationByClientException(message: String) :
60+
public class AIAgentTerminationByClientException(message: String) :
6161
AIAgentException("Agent was canceled by the client ($message)")

agents/agents-core/src/commonMain/kotlin/ai/koog/agents/core/environment/ContextualAgentEnvironment.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import io.github.oshai.kotlinlogging.KotlinLogging
77
import kotlin.uuid.ExperimentalUuidApi
88
import kotlin.uuid.Uuid
99

10-
internal class ContextualAgentEnvironment(
10+
@Suppress("MissingKDocForPublicAPI")
11+
public class ContextualAgentEnvironment(
1112
private val environment: AIAgentEnvironment,
1213
private val context: AIAgentContext,
1314
) : AIAgentEnvironment {
1415

15-
companion object {
16+
private companion object {
1617
private val logger = KotlinLogging.logger { }
1718
}
1819

agents/agents-core/src/commonMain/kotlin/ai/koog/agents/core/environment/GenericAgentEnvironment.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import ai.koog.agents.core.tools.annotations.InternalAgentToolsApi
88
import ai.koog.prompt.message.Message
99
import io.github.oshai.kotlinlogging.KLogger
1010

11-
internal class GenericAgentEnvironment(
11+
/**
12+
* Represents base agent environment with generic abstractions.
13+
*/
14+
public class GenericAgentEnvironment(
1215
private val agentId: String,
1316
private val logger: KLogger,
1417
private val toolRegistry: ToolRegistry,

agents/agents-planner/Module.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Module agents-planner
2+
3+
Module for implementing planning capabilities in AI agents.
4+
5+
## Overview
6+
7+
The agents-planner module provides components for creating AI agents that can plan and execute multi-step tasks through iterative planning cycles.
8+
Each planning cycle builds or updates a plan based on the current state, executes a step from the plan, and checks if the goal has been achieved.
9+
10+
This module also provides GOAP (Goal-Oriented Action Planning) that use search algorithms to find optimal action sequences based on predefined goals and actions.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import ai.koog.gradle.publish.maven.Publishing.publishToMaven
2+
3+
group = rootProject.group
4+
version = rootProject.version
5+
6+
plugins {
7+
id("ai.kotlin.multiplatform")
8+
alias(libs.plugins.kotlin.serialization)
9+
}
10+
11+
kotlin {
12+
sourceSets {
13+
commonMain {
14+
dependencies {
15+
api(project(":agents:agents-core"))
16+
api(project(":agents:agents-ext"))
17+
api(project(":agents:agents-features:agents-features-memory"))
18+
api(project(":agents:agents-utils"))
19+
api(project(":prompt:prompt-executor:prompt-executor-model"))
20+
api(project(":prompt:prompt-structure"))
21+
22+
api(project(":prompt:prompt-markdown"))
23+
}
24+
}
25+
26+
commonTest {
27+
dependencies {
28+
implementation(kotlin("test"))
29+
implementation(libs.kotlinx.coroutines.test)
30+
implementation(project(":agents:agents-test"))
31+
}
32+
}
33+
34+
jvmTest {
35+
dependencies {
36+
implementation(kotlin("test-junit5"))
37+
}
38+
}
39+
}
40+
41+
explicitApi()
42+
}
43+
44+
publishToMaven()
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package ai.koog.agents.planner
2+
3+
import ai.koog.agents.core.agent.context.AIAgentContext
4+
import ai.koog.agents.core.agent.context.AIAgentFunctionalContext
5+
import ai.koog.agents.core.agent.exception.AIAgentMaxNumberOfIterationsReachedException
6+
import io.github.oshai.kotlinlogging.KotlinLogging
7+
import kotlin.reflect.KType
8+
9+
/**
10+
* An abstract base planner component, which can be used to implement different types of AI agent planner execution flows.
11+
*
12+
* An entry point is an [execute] method, which accepts an initial arbitrary [State] and returns the final [State] after the execution.
13+
*
14+
* Planner flow works as follows:
15+
* 1. Build a plan: [buildPlan]
16+
* 2. Execute a step in the plan: [executeStep]
17+
* 3. Repeat steps 1 and 2 until the plan is considered completed. Then the final [State] is returned.
18+
*
19+
* @property stateType [KType] of the [State].
20+
*/
21+
public abstract class AIAgentPlanner<State, Plan>(
22+
public val stateType: KType,
23+
) {
24+
private companion object {
25+
private val logger = KotlinLogging.logger { }
26+
}
27+
28+
/**
29+
* Builds a plan
30+
*/
31+
protected abstract suspend fun buildPlan(
32+
context: AIAgentFunctionalContext,
33+
state: State,
34+
plan: Plan?
35+
): Plan
36+
37+
/**
38+
* Executes a step in the plan.
39+
*/
40+
protected abstract suspend fun executeStep(
41+
context: AIAgentFunctionalContext,
42+
state: State,
43+
plan: Plan
44+
): State
45+
46+
/**
47+
* Checks if the plan is completed.
48+
*/
49+
protected abstract suspend fun isPlanCompleted(
50+
context: AIAgentFunctionalContext,
51+
state: State,
52+
plan: Plan
53+
): Boolean
54+
55+
/**
56+
* Executes the main loop for the planner, which involves building and executing plans iteratively until
57+
* the plan is considered successfully completed or a max number of iterations is reached.
58+
*
59+
* @param context AI Agent's context
60+
* @param input The initial state to be used as the starting point for the execution process.
61+
* @return The final state after the execution of the plans.
62+
* @throws AIAgentMaxNumberOfIterationsReachedException If the maximum number of iterations defined in the agent's
63+
* configuration is exceeded.
64+
*/
65+
public suspend fun execute(
66+
context: AIAgentFunctionalContext,
67+
input: State
68+
): State {
69+
logger.debug { formatLog(context, "Starting planner execution") }
70+
var state = input
71+
var plan: Plan = buildPlan(context, state, null)
72+
73+
while (!isPlanCompleted(context, state, plan)) {
74+
val iterations = context.stateManager.withStateLock { state ->
75+
if (++state.iterations > context.config.maxAgentIterations) {
76+
logger.error {
77+
formatLog(
78+
context,
79+
"Max iterations limit (${context.config.maxAgentIterations}) reached"
80+
)
81+
}
82+
throw AIAgentMaxNumberOfIterationsReachedException(context.config.maxAgentIterations)
83+
}
84+
85+
state.iterations
86+
}
87+
88+
logger.debug { formatLog(context, "Executing plan step #$iterations") }
89+
state = executeStep(context, state, plan)
90+
plan = buildPlan(context, state, plan)
91+
logger.debug { formatLog(context, "Finished executing plan step #$iterations") }
92+
}
93+
94+
logger.debug { formatLog(context, "Finished planner execution") }
95+
return state
96+
}
97+
98+
private fun formatLog(context: AIAgentContext, message: String): String =
99+
"$message [${context.strategyName}, ${context.runId}]"
100+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package ai.koog.agents.planner
2+
3+
import ai.koog.agents.core.feature.AIAgentFeature
4+
import ai.koog.agents.core.feature.config.FeatureConfig
5+
6+
/**
7+
* Represents a planner-specific AI agent feature that can be installed into an [AIAgentPlannerPipeline].
8+
*
9+
* @param TConfig The type of configuration required for the feature, extending [FeatureConfig].
10+
* @param TFeatureImpl The type representing the concrete implementation of the feature.
11+
*/
12+
public interface AIAgentPlannerFeature<TConfig : FeatureConfig, TFeatureImpl : Any> : AIAgentFeature<TConfig, TFeatureImpl> {
13+
/**
14+
* Installs the feature into the specified [pipeline].
15+
* @return The implementation of the feature.
16+
*/
17+
public fun install(config: TConfig, pipeline: AIAgentPlannerPipeline): TFeatureImpl
18+
}

0 commit comments

Comments
 (0)