Practical Reasoning Programming Style

ASTRA gives you a lot of freedom in how you write an agent. The same behaviour can be written with a handful of rules, with a single rule full of if statements and while loops, or anything in between. That freedom makes it easy to fall back on procedural habits from other languages and end up with agent programs that are hard to read and that do not make use of what makes AgentSpeak(L) different: goals, delayed decision making and rule selection based on context.

This guide describes a programming style that puts structure on how rules are written. It is based on the paper Towards a Distinct Programming Style for AgentSpeak(L) (Collier, Beaumont and Ciortea, EUMAS 2025 — see References), which develops the style for the AgentSpeak(L) family of languages. Here, the style is presented directly in ASTRA syntax.

This guide assumes that you are familiar with the material in ASTRA Concepts and have worked through Designing Agent Programs in ASTRA, which uses the same light switch scenario.

Why “Practical Reasoning”?

Practical reasoning is reasoning about what to do, rather than what to believe. In the Belief-Desire-Intention (BDI) model that AgentSpeak(L) is built on, it is usually split into two activities:

  • Deliberation: deciding what the agent should try to achieve.

  • Means-end reasoning: working out how to achieve it.

In AgentSpeak(L), it is not obvious where these two activities live in a program: intentions are created in response to events, and goals tend to be used as steps along the way. The style makes the link explicit. When the agent notices that the environment is in an undesirable state, it commits to a goal that describes a better future state; separate sets of rules then describe how that state is brought about.

The plan types at a glance

Every rule written in this style has one of the roles below. The rest of this guide explains each one.

Plan type

Triggered by

What its body does

Typical shape in ASTRA

Deliberation

A belief event

Adopts one declarative goal

plan +b : ctx { !g; }

Achievement

A goal event

Performs module actions, then confirms the goal state

plan +!g : ctx { m.act(); wait(g); }

Serendipity

A goal event when the goal is already true

Nothing

plan +!g : g { }

Decomposition

A goal event

Adopts sub-goals only

plan +!g : ctx { !g1; !g2; }

Repair

A goal event when a condition is missing

Achieves the missing condition, then retries the goal

plan +!g : ~c { !c; !g; }

Recovery

A goal failure event (^!g)

Retries the goal when the failure can be recovered from

plan ^!g : ctx { !g; }

Reactive

A belief event

A small number of module actions, no goals

plan +b : ctx { m.act(); }

Achievement, Serendipity, Decomposition and Repair plans are all Means-End Reasoning plans: they handle a goal event.

Reading AgentSpeak(L) examples in ASTRA

If you read the original paper, or other AgentSpeak(L) material, the main differences in notation are:

AgentSpeak(L) / Jason

ASTRA

+b : c <- body.

plan +b : c { body }

not b

~b

.act(X) (an action)

module.act(X), using a declared module

-!g (goal failure, as in Jason)

^!g

h :- body. (an inference rule)

inference h :- body;

atoms such as on

strings ("on") or constants (ON)

untyped variables (X)

typed variables (string X, list L)

The running example

The first part of this guide uses a light that should always match the state of its switch. The environment is provided by a module (here called ls) that:

  • maintains two beliefs, switch(string) and light(string), each with the value "on" or "off";

  • provides an action, ls.setLight(string), that turns the light on or off.

A complete version of this program, including a module with a simple user interface, is available on GitLab.

Principle 1: Deliberation Plans

When the agent notices a change that leaves the environment in an undesirable state, it should adopt a goal that describes the desirable state it wants to bring about.

A Deliberation Plan is triggered by a belief event and its body adopts a single goal:

plan +switch("off") { !light("off"); }
plan +switch("on") { !light("on"); }

The goal !light("off") describes a state of the world (the light being off), not an activity. This is the most important constraint in the style: goals are declarative. Goals such as !turnLightOff() or !setLight("off") describe things to do, so rules that adopt them are not Deliberation Plans.

The goal also mirrors a belief: once !light("off") has been achieved, the agent should believe light("off").

Principle 2: Means-End Reasoning Plans

Each goal should have a set of rules that bring about the state that it describes, and the goal only counts as achieved when the agent believes that state holds.

The rules for !light(...) are:

plan +!light("off") : light("on") { ls.setLight("off"); wait(light("off")); }
plan +!light("off") { }
plan +!light("on") : light("off") { ls.setLight("on"); wait(light("on")); }
plan +!light("on") { }

The first and third rules are Achievement Plans: they perform the action that changes the environment. The second and fourth rules are Serendipity Plans: they cover the case where the goal is already true, so there is nothing to do. Serendipity plans are not optional: the rules for a goal must cover every way in which the goal can be satisfied. Because ASTRA selects the first applicable rule, the empty rule is written last and acts as the default.

Confirming that the goal has been achieved

AgentSpeak(L) treats a goal as achieved when its plan completes. This style is stricter: the agent must believe that the goal state holds. In ASTRA, there are two ways to check this as the last step of an Achievement Plan:

  • ?light("off") is a test goal. If the agent already believes light("off"), it succeeds immediately. If not, ASTRA raises a test goal event (+?light("off")) and the intention fails if no rule handles it.

  • wait(light("off")) suspends the rule until the agent believes light("off"). An optional second argument gives a timeout, e.g. wait(light("off"), 2000).

When the effect of an action is only perceived later, for example when beliefs are updated by a sensor or by an EIS environment, wait(...) is the safer choice. Both examples in this guide use it.

If you do not want to adopt the goal at all when the light is already in the right state, you can add the check to the Deliberation Plans instead:

plan +switch("off") : light("on") { !light("off"); }
plan +switch("on") : light("off") { !light("on"); }

The style does not prefer one option over the other, but whichever form the Deliberation Plans take, the Means-End Reasoning Plans for a goal must still cover all of the cases.

Principle 3: Repair Plans

If a Means-End Reasoning Plan depends on more than one condition, write a Repair Plan for each condition that makes that condition true and then retries the goal.

The light switch is too simple to need repairs, so this principle uses the Towerworld example described below. There, an agent controls a gripper that can pick up and put down blocks. Picking up block X only works if the gripper is empty and nothing is on top of X:

plan +!holding(string X) : holding(X) { }
plan +!holding(string X) : empty(GRIPPER) & free(X) { ei.pickup(X); wait(holding(X)); }

These rules do not cover the cases where the gripper is holding something else, or where X is covered by another block. Each missing condition gets a Repair Plan that first achieves the condition and then re-adopts the original goal:

plan +!holding(string X) : ~empty(GRIPPER) { !empty(GRIPPER); !holding(X); }
plan +!holding(string X) : ~free(X) { !free(X); !holding(X); }

Notice that the repair step is itself a goal (!empty(GRIPPER), !free(X)) and not an action. How that goal is achieved is left to its own Means-End Reasoning Plans.

Writing Repair Plans in this regular ~c { !c; !g; } shape is only possible if each condition can be expressed as a goal. Without the empty(...) predicate, the condition would have been “not holding anything”, which cannot be written as a goal. Introducing empty(GRIPPER) through domain modelling is what makes the repair readable.

Principle 4: Keep goals and actions apart

Think of each intention as a tree whose internal nodes are goals and whose leaves are actions. The children of any node should be either all goals or all actions, never a mix.

This leads to two kinds of Means-End Reasoning Plan:

  • Decomposition Plans break a goal into sub-goals and contain no module actions.

  • Achievement Plans achieve the goal directly using module actions and contain no sub-goals. Serendipity Plans, which contain no actions at all, are a special case.

For example, building a tower is decomposed into sub-goals:

plan +!tower([string X]) { !on(X, TABLE); }
plan +!tower([string H|list T]) : head(T, string Y) { !tower(T); !on(H, Y); }

while putting a block down is achieved with an action:

plan +!on(string X, string Y) : holding(X) & free(Y) { ei.putdown(X, Y); wait(on(X, Y)); }

If you find yourself writing a rule that calls a module action and adopts a sub-goal, split it: move the action into an Achievement Plan for a new goal.

Principle 5: Recovery Plans

Where an unexpected failure can be recovered from, add a rule that handles the failure and re-adopts the goal.

Repair Plans deal with problems that you can anticipate (a condition that is false when the goal is adopted). Recovery Plans deal with things that go wrong while a plan is running, such as an action failing.

In ASTRA, when the rule handling a goal fails, the agent raises a goal failure event, written ^!goal(...). In Towerworld, the user can move blocks while the agent is working, so the block that the agent is about to put something on may be covered before the action completes. The agent can recover by re-adopting the goal, which then uses the Repair Plans:

plan ^!on(string X, string Y) : ~free(Y) { !on(X, Y); }

The context restricts the recovery to the one situation that the agent knows how to fix. For any other cause of failure there is no applicable rule, so the failure is not handled.

Principle 6: Reactive Plans

When the right response to an event is a single action (or a short sequence of actions), react directly instead of adopting a goal.

Not every behaviour needs deliberation. If the only thing that ever happens when the switch changes is that the light is changed to match it, goals are overkill:

plan +switch("off") : light("on") { ls.setLight("off"); }
plan +switch("on") : light("off") { ls.setLight("on"); }

A Reactive Plan contains module actions only: no goals and no test goals. Reactive Plans become hard to maintain as the number of situations they need to handle grows; when that happens, switch to Deliberation and Means-End Reasoning Plans.

Principle 7: Domain Modelling

Identify the objects and predicates that describe the environment, including any that are needed to express the agent’s goals, before writing the rules that use them.

The environment usually defines some predicates for you (on(X, Y) and holding(X) in Towerworld), but the agent’s goals often need concepts that the environment does not provide. Because this style is built around goals that describe future states, a clear model of those states is essential, and a good model often suggests how the rules should be written.

In ASTRA, you declare the predicates in a types block, and derive new ones using inference rules. For example, Towerworld has no concept of a tower. Modelling a tower as a list of blocks, from top to bottom, gives:

inference tower([string X]) :- on(X, TABLE);
inference tower([string H|list T]) :- tower(T) & head(T, string X) & on(H, X);
inference head([string H|list T], H) :- true;

A single block is a tower if it is on the table. A longer list is a tower if its tail is a tower and its first block is on top of the first block of the tail. The two Decomposition Plans for !tower(...) in Principle 4 follow the same two cases.

Domain modelling can be done incrementally, but try to model each part of the domain before writing the rules for it.

Example 1: Light Switch

Combining the Deliberation Plans and Serendipity Plans from Principles 1 and 2 gives:

plan +switch(string S) { !light(S); }

plan +!light("off") : light("on") { ls.setLight("off"); wait(light("off")); }
plan +!light("on") : light("off") { ls.setLight("on"); wait(light("on")); }
plan +!light(string S) { }

The two Achievement Plans are nearly identical. The reason is a gap in the domain model: the agent knows that lights are on or off, but not how they change between the two. Adding a transition(From, To) predicate captures this, and reduces the Achievement Plans to one:

agent Main {
    module LightSwitch ls;

    types lights {
        formula switch(string);
        formula light(string);
        formula transition(string, string);
    }

    constant string ON = "on";
    constant string OFF = "off";

    initial transition(ON, OFF), transition(OFF, ON);

    // Deliberation Plan
    plan +switch(string S) { !light(S); }

    // Means-End Reasoning Plans for !light(...)
    plan +!light(string S) : light(string T) & transition(T, S) { ls.setLight(S); wait(light(S)); }
    plan +!light(string S) { }
}

The result separates deciding (the light should match the switch) from achieving (how to change the light), and the extra domain knowledge keeps both parts short.

Example 2: Towerworld

Towerworld is a classic AI problem in which an agent uses a gripper to arrange blocks into towers. This version uses the Tower environment from EISHub, which ASTRA accesses through the EIS module.

The environment provides:

  • block(X): X is a block (blocks are labelled "a", "b", "c", …);

  • on(X, Y): block X is on Y, where Y is another block or "table";

  • holding(X): the gripper is holding X (there is no holding belief when the gripper is empty);

  • pickup(X): an action that picks up X, which needs an empty gripper and nothing on top of X;

  • putdown(X, Y): an action that puts the held block X on Y, which needs Y to be clear (the table is always clear).

Domain modelling adds four predicates:

Predicate

Meaning

How it is defined

free(X)

Nothing is on top of X

Inferred; free(TABLE) is an initial belief

empty(GRIPPER)

The gripper is not holding anything

Inferred

tower(L)

The list of blocks L (top first) currently forms a tower

Inferred

target(L)

The tower L should be built

Added when the agent is given a task

Adding a target(...) belief is the change in the environment that drives the agent: a single Deliberation Plan turns it into the goal !tower(L). The remaining rules are Means-End Reasoning Plans, grouped by goal:

/**
 * Example Agent Program written in the Practical Reasoning Programming Style
 */
agent Main {
    module Console C;
    module EIS ei;

    types tower {
        formula block(string);
        formula free(string);
        formula empty(string);
        formula tower(list);
        formula target(list);
        formula on(string, string);
        formula holding(string);
        formula head(list, string);
    }

    constant string TABLE = "table";
    constant string GRIPPER = "gripper";

    // Domain model
    inference on(string X, string Y) :- ei.on(X, Y);
    inference holding(string X) :- ei.holding(X);
    inference free(string X) :- ~on(string Y, X);
    inference tower([string X]) :- on(X, TABLE);
    inference tower([string H|list T]) :- tower(T) & head(T, string X) & on(H, X);
    inference head([string H|list T], H) :- true;
    inference empty(GRIPPER) :- ~holding(string X);

    initial free(TABLE);

    plan +!main(list args) {
        ei.launch("hw", "dependency/tower-1.3.0.jar");
        ei.init();
        ei.start();
        ei.link(GRIPPER);
    }

    // Set a target tower when blocks are created in the environment
    plan +$ei.event(block("c")) { +target(["a", "b", "c"]); }
    plan +$ei.event(block("a")) { +target(["a"]); }

    // Deliberation Plan
    plan +target(list L) { !tower(L); }

    // !tower(...): Serendipity and Decomposition Plans
    plan +!tower(list L) : tower(L) { }
    plan +!tower([string X]) { !on(X, TABLE); }
    plan +!tower([string H|list T]) : head(T, string Y) { !tower(T); !on(H, Y); }

    // !on(...): Serendipity, Achievement and Repair Plans
    plan +!on(string X, string Y) : on(X, Y) { }
    plan +!on(string X, string Y) : holding(X) & free(Y) { ei.putdown(X, Y); wait(on(X, Y)); }
    plan +!on(string X, string Y) : ~holding(X) { !holding(X); !on(X, Y); }
    plan +!on(string X, string Y) : ~free(Y) { !free(Y); !on(X, Y); }

    // !holding(...): Serendipity, Achievement and Repair Plans
    plan +!holding(string X) : holding(X) { }
    plan +!holding(string X) : empty(GRIPPER) & free(X) { ei.pickup(X); wait(holding(X)); }
    plan +!holding(string X) : ~empty(GRIPPER) { !empty(GRIPPER); !holding(X); }
    plan +!holding(string X) : ~free(X) { !free(X); !holding(X); }

    // !empty(...): Serendipity and Decomposition Plans
    plan +!empty(GRIPPER) : empty(GRIPPER) { }
    plan +!empty(GRIPPER) : holding(string Y) { !on(Y, TABLE); }

    // !free(...): Serendipity and Decomposition Plans
    plan +!free(string X) : free(X) { }
    plan +!free(string X) : on(string Y, X) { !on(Y, TABLE); ?free(X); }

    // Recovery Plan: the target block was covered while we were moving
    plan ^!on(string X, string Y) : ~free(Y) { !on(X, Y); }
}

The !tower(...) rules have a context (head(T, string Y)) but no Repair Plan. That is because the condition is always true for a list with more than one element, so there is nothing to repair.

The full project, including the Maven build file that downloads the Tower environment, is available on GitLab.

Checklist

When reviewing an agent program written in this style, check that:

  • every goal describes a state of the world, not an activity;

  • every Deliberation Plan adopts a goal and does nothing else;

  • every goal has a Serendipity Plan for the case where it is already true;

  • every Achievement Plan ends by confirming the goal state (wait(...) or ?...);

  • every Means-End Reasoning Plan with more than one condition has a Repair Plan for each condition;

  • no rule mixes module actions and sub-goals;

  • goal failure (^!g) is handled where, and only where, the agent knows how to recover;

  • simple, single-action responses are written as Reactive Plans;

  • the predicates the goals need are part of the domain model, using types and inference rules.

References

Collier, R., Beaumont, K., Ciortea, A. (2026) Towards a Distinct Programming Style for AgentSpeak(L). In: Baldoni, M. et al. (eds.) Multi-Agent Systems. EUMAS 2025. Lecture Notes in Computer Science, vol. 16258, pp. 215–231. Springer, Cham. https://doi.org/10.1007/978-3-032-22817-8_13