Plans
Plans are the agent's know-how in the BDI model: they define how it reacts to events, i.e. new goals, belief changes, and failures. A plan is made of three parts:
adding.goal {
// trigger
} onlyWhen {
// guard (optional)
} triggers {
// body
}
Plans are declared in hasPlanLibrary { }.
Triggers
A trigger selects the kind of event and decides whether the plan is relevant for it:
| Trigger | Fires when |
|---|---|
adding.goal { } | a goal is added |
failing.goal { } | a goal fails |
removing.goal { } | a goal is removed |
adding.belief { } | a belief is added |
removing.belief { } | a belief is removed |
The trigger block receives the goal or belief as this and returns either null (the plan is not relevant)
or a context value, which is then available to the guard and body as context:
adding.belief {
takeIf { it.text == "Ping!" }
} triggers {
val (text, sender) = context
// ...
}
With goals of a sealed type, a cast is all a trigger needs: adding.goal { this as? Serve } is relevant for every
Serve goal, which becomes the context. Incarnations can provide matching helpers that build the context for you,
such as matchingGoal { } in the Prolog incarnation, where the context holds the variables bound by unification.
Guards
onlyWhen { } makes a relevant plan applicable only if a condition holds in the current state.
It can read beliefs and context, and returns the (possibly refined) context or null.
- Kotlin
- Prolog
The thermostat heats a room that is too cold, unless it is heating already:
adding.belief {
takeIf { it.temperature < it.target - TOLERANCE }
} onlyWhen {
context.takeIf { it.mode != Mode.HEATING }
} triggers {
agent.print("It's ${context.temperature}°C, heating up to ${context.target}°C")
hvac.heat()
}
satisfies { } proves a query against the belief base, and adds its bindings to the context:
prologPlan {
adding.goal {
matchingGoal { "start"(N, X) }
} onlyWhen {
satisfies { (N lowerThan X) and (S `is` (N + 1)) }
} triggers {
agent.print("Counting... ", N)
agent.achieve(goal { "start"(S, X) })
}
}
Plan selection
A plan is relevant for an event when its trigger returns a context, and applicable when its guard returns one too. When several plans are relevant, the first applicable one in the plan library is chosen:
If no plan is applicable, or the body throws, a goal fails, and failing.goal plans get a
chance to recover it. A belief event, or a goal removal, with no applicable plan is simply ignored: belief changes that
no plan cares about are normal.
Since the first applicable plan wins, order your plans from the most specific to the most general: a catch-all plan added first would shadow all the others. The Blocks World showcase relies on this.
Bodies
The body, triggers { }, is a suspend Kotlin lambda. Anything Kotlin can do, a plan body can do:
call functions, use delay(...), launch computations. Its receiver provides:
agent— the agent's state:believe,forget,beliefs,print, the sub-goal functionsachieve,alsoAchieveandachieveWithResult, andwait(filter, timeout)to suspend until a matching event happens (see Wait for events and use time);node— the node the agent lives in (e.g.node.terminateNode()). It is not part of the plan scope: it comes from the enclosingagent { }/node { }builder, or from theplans { node -> }parameter;context— the value produced by the trigger and guard;- any skill in scope.
In AgentSpeak and Jason, the reasoning cycle executes one step of a plan, e.g. one action, at a time. A JaKtA plan body
has no such steps: it runs without interruption until it suspends — on achieve, delay, wait or any other
suspend call — and the agent handles other events and intentions only then. Suspension points are the steps; see
Intentions are coroutines.
X.value<T>() extracts the Kotlin value bound to a logic variable, e.g. blocksWorld.move(X.value(), Y.value()).
Failure plans
failing.goal { } plans handle goal failures, e.g. to retry after changing the agent's knowledge:
- Kotlin
- Prolog
When the barista cannot prepare a coffee, it buys beans and prepares it again; the cup it returns completes the failed goal:
failing.goal { this as? Prepare } triggers {
agent.print("I could not prepare the ${context.coffee}: buying beans and trying again")
agent.believe("beans")
val cup: String = agent.achieveWithResult(context)
cup
}
prologPlan {
failing.goal {
matchingGoal { "start"(B) }
} triggers {
agent.print("start failed, retrying")
agent.alsoAchieve(goal { "start"(B) })
}
}
Reusable plan libraries
plans { node -> ... } builds a list of plans outside an agent, which can then be installed with
withPredefinedPlans(...). The node parameter lets the plans use node-bound skills:
- Kotlin
- Prolog
val pingPlans = plans<ToldBelief, String, Any> { node ->
context(MessagingSkill(node)) {
adding.goal {
takeIf { it == "start" }
} triggers {
agent.sendTo(bob, "Ping!")
}
}
}
agent(alice) {
// ...
withPredefinedPlans(pingPlans)
}
val pingPlans = plans { node ->
context(MessagingSkill(node)) {
prologPlan {
adding.goal {
matchingGoal { Atom.of("start") }
} triggers {
agent.tellTo(bob, belief { "ping"(1) })
}
}
}
}
agent(alice) {
// ...
withPredefinedPlans(pingPlans)
}