BDI · Beliefs · Desires · Intentions

AgentSpeak(L)

The programming language of cognitive agents. Instead of writing how step by step, you declare what your agent believes, what it wants, and the plans it can use — the BDI reasoning cycle does the rest.

Why a Language for Agents?

From procedural code to mental attitudes

AgentSpeak(L) was proposed by Anand Rao in 1996 as a formalization of the Belief–Desire–Intention model of rational agency, and was popularized by the Jason interpreter. It treats an agent not as an object with methods, but as an entity with mental attitudes: it holds beliefs about the world, adopts goals it desires to achieve, and commits to intentions — concrete plans it executes, suspends or abandons as the world changes.

This makes AgentSpeak ideal for systems that must react to a changing environment while pursuing long-term objectives: robots that re-plan when a corridor is blocked, traders that revise positions when the market moves, or smart-home controllers that balance comfort and energy. In SPADE, the SPADE BDI plugin runs AgentSpeak programs inside real XMPP agents, so reasoning agents and ordinary behaviour-based agents coexist in the same distributed system.

Beliefs

Facts the agent holds about the world, as logical predicates. They change as the agent perceives and communicates.

temperature(31).
Goals

States the agent wants to bring about. Adding a goal triggers the search for an applicable plan.

!keep_comfortable.
Plans

Recipes that say how to react to an event when a context holds — the agent's know-how.

+!g : c <- body.

The Reasoning Cycle

What the interpreter does on every iteration

1
Perceive & update beliefs

New percepts and messages revise the belief base, generating events like +temperature(31).

2
Select an event

One pending event — a belief change or a new goal — is picked for processing.

3
Find an applicable plan

Plans whose trigger matches the event are filtered by their context, checked against the beliefs with unification.

4
Execute one step of an intention

The selected plan becomes an intention; actions run, subgoals push new plans, beliefs are updated — and the cycle repeats.

Learn AgentSpeak — Try It Online

AgentSpeak programs are made of three ingredients: beliefs (what the agent knows), goals (what it wants to achieve) and plans (how it achieves them). The reasoning cycle reacts to events — a new belief, a new goal — by selecting an applicable plan and executing it.

+!stay_cool : temperature(T) & T > 25 <- !find_shade; !drink_water.
Trigger+!g new goal, +b belief added, -b belief removed Context — conditions checked against the belief base with Prolog-style unification Body — actions, subgoals (!g), belief updates (+b/-b), internal actions (.print, .send)
agent_program.asl
Reasoning trace
Final belief base

This playground runs a didactic subset of AgentSpeak(L) in your browser. The real SPADE BDI plugin embeds a full AgentSpeak interpreter (based on agentspeak-python) with KQML communication between distributed XMPP agents — install it with uv add spade_bdi.

Language reference

The Complete AgentSpeak Manual

Every construct of the language in one place — syntax, beliefs, rules, goals, plans, operators, lists, the internal-action library and KQML communication — mapped onto how you actually run it with SPADE BDI.

01Lexical syntax

AgentSpeak borrows its term language from Prolog. Everything is built from a few kinds of token. The single rule that trips up newcomers: names starting with a lowercase letter are constants (atoms); names starting with an uppercase letter or _ are logic variables.

ElementExamplesNotes
Atom / constantjohn, red, 'New York'Lowercase start, or quoted for spaces/caps.
Number42, -3, 3.14Integers and floats.
String"hello world"Double-quoted text.
VariableX, Temp, _CostUppercase / underscore start. Bound by unification.
Anonymous variable_"Don't care" — matches anything, never bound.
Structure / predicateloc(robot, 3, 4)A functor with arguments — the core data shape.
List[a, b, c], [H|T]See Lists & strings.
Comment// line, /* block */Ignored by the interpreter.
// constants vs. variables
position(robot, 3, 4).   // robot, 3, 4 are constants
+!move(Dir) <- step(Dir).   // Dir is a variable, bound when the goal runs

color('dark green').     // quoted atom keeps the space and case

02Beliefs & annotations

The belief base is the agent's view of the world: a set of ground predicates, each ending in a full stop. Beliefs are added and removed as the agent perceives the environment, receives messages, or updates its own mental notes.

temperature(22).
location(kitchen).
is_sunny(true).
friend(alice).
friend(bob).

Strong (explicit) negation uses ~. It states that something is known to be false, which is different from simply not being in the belief base:

~raining.          // the agent knows it is NOT raining
~enemy(alice).     // alice is explicitly known not to be an enemy

Every belief carries annotations in square brackets that record where it came from. The interpreter adds source(...) automatically; you can read or match on it inside a context.

AnnotationMeaning
[source(percept)]Came from sensing the environment.
[source(self)]The agent added it itself (a mental note).
[source(alice)]Received from another agent named alice.
colour(box1, red)[source(percept)].

// only trust a belief that did not come from bob
+!act : open(door)[source(S)] & S \== bob <- go_through.

03Rules

Beyond plain facts, the belief base can hold Prolog-style rules that derive new conclusions on demand. A rule has the form Head :- Body and is used whenever the head is queried in a plan context.

// facts
parent(tom, bob).
parent(bob, ann).

// rule: a grandparent is the parent of a parent
grandparent(X, Z) :- parent(X, Y) & parent(Y, Z).

// querying the rule inside a context
+!check : grandparent(tom, ann) <- .print("tom is ann's grandparent").

Rules never appear as events — they are not added or removed dynamically — they exist purely to keep the belief base small and let contexts stay declarative.

04Goals

There are two kinds of goal, distinguished by their prefix:

GoalPrefixWhat it does
Achievement goal!"Make this true." Triggers the search for a plan that brings the state about.
Test goal?"Find out the value." Queries the belief base and binds variables — no plan needed if the belief exists.
!greet_everyone.        // initial achievement goal (runs at start-up)

+!report <-
    ?temperature(T);    // test goal: bind T from the belief base
    .print("Current temperature is", T).

A goal placed at the top level of the file (e.g. !start.) is an initial goal: the agent adopts it as soon as it boots.

05Plans

Plans are the agent's know-how. Each one says: when this event happens, and if this condition holds, do this. The three parts are separated by : and <-, and the plan ends in a full stop.

+!stay_cool : temperature(T) & T > 25 <- !find_shade; !drink_water.
└──┬───┘   └────────────┬─────────────┘   └────────────┬───────────┘
triggering event       context (guard)               plan body
  • Triggering event — the change that makes this plan relevant (a new goal, a new belief…). See below.
  • Context — a logical condition checked against the belief base with unification. If it fails the plan is skipped. Omit it (with the :) to mean "always applicable".
  • Body — a sequence of formulas separated by ;, run in order once the plan is selected.

Multiple plans can share a trigger. The interpreter tries them top to bottom and commits to the first whose context holds — so order your plans from most specific to most general, like a fall-through:

+!greet(Who) : friend(Who)      <- .print("Hey", Who, "!").
+!greet(Who) : true             <- .print("Hello", Who).   // fallback

06Triggering events

A plan fires on an event — a change in the agent's mental state. The sign (+/-) is addition or deletion; the optional ! or ? marks a goal rather than a belief.

TriggerFires when…
+bBelief b is added (percept, message or mental note).
-bBelief b is removed.
+!gAchievement goal g is adopted — the common case.
-!gAchievement goal g failed — use for recovery/clean-up.
+?gTest goal g is posted.
-?gTest goal g failed to find an answer.
// react to perceiving a new temperature reading
+temperature(T) : T > 30 <- .print("It's getting hot!"); !cool_down.

// recover when an achievement goal fails
-!deliver(Pkg) <- .print("Delivery failed, retrying"); !deliver(Pkg).

07The plan body

The body is a ;-separated sequence of formulas. Each kind of formula is marked by its leading symbol:

FormulaSymbolEffect
Environment actionactActs on the world (e.g. move(left)). In SPADE BDI, a Python action.
Achievement subgoal!gPursue g now, in the same intention.
New-focus subgoal!!gPursue g as a separate, concurrent intention.
Test goal?gQuery the belief base, binding variables.
Add belief (mental note)+bInsert b with source(self).
Remove belief-bDelete a matching belief.
Update belief-+bRemove the old b then add the new one (atomic replace).
Internal action.name(…)Library call that runs inside the agent. See below.
Constraint / expressionX = …Unify or test in-line; fails the plan step if false.
+!process_order(Item, Qty) : stock(Item, N) & N >= Qty <-
    Remaining = N - Qty;            // expression: bind Remaining
    -+stock(Item, Remaining);       // update the stock belief
    .print("Shipping", Qty, "x", Item);
    pack(Item, Qty);                // environment action
    !!notify_warehouse(Item);       // spin off a concurrent intention
    +shipped(Item).                 // mental note

08Operators

Operators appear in plan contexts and in body expressions.

Arithmetic
+ - * /Add, subtract, multiply, divide.
**Power.
div modInteger division, remainder.
Relational
> >= < <=Numeric comparison.
==Term equality (no binding).
\\==Term inequality.
=Unification (binds variables).
Logical (context)
&Conjunction — all must hold.
|Disjunction — any may hold.
notNegation as failure — true when the goal can't be proven.
+!buy(Item) : price(Item, P) & funds(F) & F >= P & not blacklisted(Item) <-
    NewFunds = F - P;
    -+funds(NewFunds);
    .print("Bought", Item, "- balance:", NewFunds).

Mind the difference: == compares terms as they are, while = unifies them and can bind a variable. not p (negation as failure) succeeds when p is simply absent; ~p (strong negation) is a belief that p is positively false.

09Lists & strings

Lists are ordinary terms written between square brackets. The head/tail pattern [H|T] is the workhorse for recursion: H unifies with the first element, T with the rest.

[]                 // empty list
[a, b, c]          // three elements
[H | T]            // H = a, T = [b, c]  (when matched against [a,b,c])
[a, b | Rest]      // first two fixed, Rest = the tail
[name(ann), 42, "hi"]   // elements can be any term

Recursing over a list with two plans — one for the empty case, one for head/tail:

+!sum([], 0).
+!sum([H|T], S) <- !sum(T, Rest); S = H + Rest.

+!greet_all([]).
+!greet_all([H|T]) <- .print("Hello", H); !greet_all(T).

Practical example combining both: take a list of usernames, build a message string, and notify each user one by one.

// users waiting for a reminder
pending_users([alice, bob, carol]).

+!notify_pending <-
    ?pending_users(Users);
    !notify_list(Users, "[SPADE] reminder for ").

+!notify_list([], _Prefix) <-
    .print("All reminders sent").

+!notify_list([User|Rest], Prefix) <-
    .concat(Prefix, User, Msg);      // string concatenation
    .length(Rest, Left);             // list length helper
    .print(Msg, "(left:", Left, ")");
    .send(User, tell, reminder_ready(User));
    !notify_list(Rest, Prefix).

Common list and string utilities live in the internal-action library — a few of the most used ones:

ActionPurpose
.length(L, N)Bind N to the number of elements (or string length).
.member(X, L)Succeeds if X is in L; can enumerate.
.nth(I, L, X)Bind X to the element at index I.
.concat(A, B, C)Concatenate lists or strings into C.
.sort(L, S) · .reverse(L, R)Order or reverse a list.
.max(L, M) · .min(L, M)Largest / smallest element.
.substring(Sub, Str)Test or locate a substring.

10Internal actions

Internal actions start with a dot and run inside the agent (unlike environment actions, which affect the world). Standard-library actions use a bare .name; user-defined ones live in a library namespace like my_lib.action. They never generate events and can fail, which fails the plan step.

CategoryActions
Output.print, .println
Introspection.my_name(N), .findall(T, Q, L), .count(Q, N), .setof
Goals & plans.desire, .intend, .drop_goal, .drop_intention, .succeed_goal, .fail_goal, .add_plan
Lists & strings.length, .member, .nth, .sort, .concat, .reverse, .max, .min
Communication.send, .broadcast
Time & control.wait, .at, .random(X), .date, .time
Agent lifecycle.create_agent, .kill_agent, .stopMAS
+!summarise <-
    .findall(Name, friend(Name), Friends);   // collect all matches
    .length(Friends, N);
    .print("I have", N, "friends:", Friends).

The exact set of available internal actions depends on the interpreter. The browser playground above implements a didactic subset (.print, .send, beliefs & subgoals); SPADE BDI ships the fuller python-agentspeak standard library and lets you add your own in Python (next section).

11Communication

Agents talk by sending each other speech acts with .send(Receiver, Performative, Content). The KQML-style performative says what the message means — and the receiver's mind updates automatically. In SPADE BDI these travel as real XMPP messages between distributed agents.

PerformativeEffect on the receiver
tellAdds Content to its belief base, tagged source(sender).
untellRemoves that belief.
achieveAdopts Content as a new goal !Content.
unachieveDrops that goal.
askOne / askAllRequests one / every answer that unifies with Content.
tellHow / untellHowShares / retracts a plan (know-how).
// inform another agent of a fact
.send(robot1, tell, location(kitchen));

// delegate a goal to a coordinator
.send(coordinator, achieve, patrol_area(zone_a));

// the receiver reacts to the freshly-told belief
+location(Room)[source(Ag)] <-
    .print(Ag, "reports something is in the", Room).

12AgentSpeak in SPADE

With the SPADE BDI plugin you put your program in an .asl file and hand it to a BDIAgent. From Python you can seed, read and react to beliefs while the reasoning cycle runs over XMPP.

import spade
from spade_bdi.bdi import BDIAgent

async def main():
    agent = BDIAgent("agent@server", "password", "agent.asl")
    await agent.start()

    # seed and query the belief base from Python
    agent.set_belief("temperature", 30)
    print(agent.get_belief("temperature"))

if __name__ == "__main__":
    spade.run(main())

To expose new internal actions or functions to AgentSpeak, subclass BDIAgent and override add_custom_actions:

import agentspeak
from spade_bdi.bdi import BDIAgent

class MyAgent(BDIAgent):
    def add_custom_actions(self, actions):

        # a function usable in expressions:  X = .square(5)
        @actions.add_function(".square", (int,))
        def square(x):
            return x * x

        # an action usable in a plan body:  .greet("world")
        @actions.add(".greet", 1)
        def _greet(agent, term, intention):
            who = agentspeak.grounded(term.args[0], intention.scope)
            print("Hello", who)
            yield

From AgentSpeak these look just like built-ins: .greet("world") in a plan body, or N = .square(5) in a context. Custom actions are the bridge between declarative reasoning and the full Python ecosystem.

Run AgentSpeak in Real Distributed Agents

With the SPADE BDI plugin, your AgentSpeak programs run inside real XMPP agents: they exchange beliefs and goals with KQML performatives (tell, untell, achieve) across machines, and mix freely with behaviour-based and LLM-powered agents.

$ uv add spade_bdi