Encapsulated Goals

In a standard ASTRA program, every plan is declared at the top level of the agent. This works well for plans that achieve goals, but many strategies also involve reacting to things that happen while the goal is being pursued. Those reactions have to be written as separate top-level plans, so:

  • the behaviour for one goal ends up spread across several unrelated plans;

  • a reactive plan is active all of the time, even when the goal it supports is not being pursued, so it has to check for itself whether it is relevant;

  • the goal that a reactive plan serves is never stated in the code, only in the programmer’s head.

Encapsulated goals solve this by letting you declare a goal together with everything needed to achieve it: a body, the plans that react to events while the goal is being pursued, the plans for its sub-goals, and a condition that says when the goal has been achieved. The idea comes from AgentSpeak(ER), which was prototyped in ASTRA (see References). The syntax used by ASTRA today differs a little from the paper: an encapsulated goal is declared with the goal keyword, and the plans inside it with the plan keyword.

Syntax

goal +!g(...) : context <: goal-condition {
    body {
        // statements executed when the goal is adopted
    }

    // reactive plans: active only while the goal is being pursued
    plan +belief(...) : context { ... }

    // plans for sub-goals that are only used by this goal
    plan +!subgoal(...) : context { ... }

    // nested encapsulated goals
    goal +!other(...) <: condition { ... }
}

Part

Required

Meaning

+!g(...)

Yes

The goal event that the encapsulated goal handles, as for a normal plan.

: context

No

When the goal can be used, as for a normal plan.

<: goal-condition

No (but see below)

A formula that says when the goal has been achieved. It can be any formula that could appear in a context.

body { ... }

No

Statements that are executed when the goal is adopted. A goal that only reacts to events does not need a body.

plan ...

No

Plans that are only relevant while this goal is active.

goal ...

No

Encapsulated goals that are only relevant while this goal is active.

How an encapsulated goal runs

When the agent adopts a goal, it selects an encapsulated goal in the same way as it selects a plan: by matching the event and checking the context. Then:

  1. The body is executed. It behaves exactly like the body of a plan.

  2. The inner plans become active. While the goal is being pursued, any event that matches one of its inner plans is handled by that plan. Inner plans run as part of the same intention as the goal, rather than in a new intention, and they can use the goal’s variables.

  3. The goal condition is checked on every cycle. As soon as the agent believes that it holds, the goal is achieved. Anything that the goal is still doing (its body, or an inner plan) is stopped, and the plan that adopted the goal continues.

  4. When the goal ends, its inner plans stop being relevant. They are never visible outside the goal, so other plans cannot use them, and events that arrive when the goal is not active are not handled by them.

Events that arrive close together

Because the inner plans of a goal share the goal’s intention, they do not run side by side. When an event arrives while an inner plan is still running, the plan for the new event is placed on top of it and runs first; the earlier plan continues once the newer one has finished. For example, if a goal has the inner plan

plan +order(string X) {
    C.println("start " + X);
    C.println("finish " + X);
}

and the beliefs order("A"), order("B") and order("C") are added one straight after the other, the output is:

[main] start A
[main] start B
[main] start C
[main] finish C
[main] finish B
[main] finish A

The same plan written at the top level of the agent handles each event in its own intention, so the orders are handled side by side instead.

When the goal ends, whether because its goal condition holds or because of done;, inner plans that have not finished are stopped as well. If the goal above also had an inner plan that executes done; when a closing() belief is added, and closing() arrived straight after the orders, neither order would be finished. Keep this in mind when an inner plan does several steps and events can arrive close together.

Example: stopping a long-running activity

This agent counts down from 10, pausing between each number. A separate intention adds the belief stopped() after 350 milliseconds. Because stopped() is the goal condition, the goal is achieved at that point, even though its body has not finished:

agent Countdown {
    module Console C;
    module System S;

    types count {
        formula stopped();
    }

    plan +!main(list args) {
        !!stopper(350);
        !count(10);
        C.println("count finished");
        S.exit();
    }

    goal +!count(int N) <: stopped() {
        body {
            int i = N;
            while (i > 0) {
                C.println(i);
                S.sleep(100);
                i--;
            }
        }
    }

    plan +!stopper(int T) {
        S.sleep(T);
        +stopped();
    }
}
[main] 10
[main] 9
[main] 8
[main] 7
[main] count finished

Without encapsulated goals, the loop would have to check stopped() itself on every iteration, or a separate top-level plan would have to react to +stopped() and somehow interrupt it.

Example: a goal that reacts to events

Some goals are achieved entirely by reacting to events. This agent keeps a house clean while it is open: whenever it comes to believe that a room is dirty, it cleans it. The goal condition is closed(), so the agent stops reacting once the house is closed.

agent Cleaner {
    module Console C;
    module System S;

    types clean {
        formula dirty(string);
        formula closed();
    }

    plan +!main(list args) {
        +dirty("hall");
        !!keepClean("kitchen");
        S.sleep(100);
        +dirty("kitchen");
        S.sleep(100);
        +dirty("lounge");
        S.sleep(100);
        +closed();
        S.sleep(100);
        +dirty("attic");
        S.sleep(100);
        foreach (dirty(string R)) {
            C.println("still dirty: " + R);
        }
        S.exit();
    }

    goal +!keepClean(string first) <: closed() {
        body {
            C.println("keeping the house clean, starting with the " + first);
        }

        plan +dirty(string R) {
            !tidy(R);
        }

        plan +!tidy(string R) {
            C.println("cleaning the " + R + " (first room was " + first + ")");
            -dirty(R);
        }
    }
}
[main] keeping the house clean, starting with the kitchen
[main] cleaning the kitchen (first room was kitchen)
[main] cleaning the lounge (first room was kitchen)
[main] still dirty: hall
[main] still dirty: attic

Notice that:

  • the hall became dirty before the goal was adopted, and the attic after the goal was achieved, so neither was cleaned: the +dirty(...) plan is only active while the goal is;

  • the inner plan for !tidy(...) uses first, a variable of the goal;

  • the goal is adopted with !!, so it runs as its own intention alongside +!main(...).

A goal that should be pursued for as long as the agent runs (a maintenance goal) can use false as its goal condition, since false never holds.

Example: succeed or give up

Because the goal condition can be any formula, it can describe both success and the situations in which the goal should be abandoned. This agent tries to visit a room, but gives up if the room is locked:

agent Visitor {
    module Console C;
    module System S;

    types visit {
        formula at(string);
        formula locked(string);
    }

    initial locked("cellar");

    plan +!main(list args) {
        !visit("cellar");
        C.println("finished with the cellar");
        S.exit();
    }

    // achieved when the agent is in the room, or abandoned if the room is locked
    goal +!visit(string room) <: at(room) | locked(room) {
        body {
            C.println("walking to the " + room);
            S.sleep(1000);
            +at(room);
        }
    }
}
[main] walking to the cellar
[main] finished with the cellar

The cellar is locked, so the goal condition holds straight away and the agent stops without waiting for the walk to finish. Note that ending a goal through its goal condition does not tell the caller why it ended; if that matters, check the relevant beliefs after the goal (here, at(room) or locked(room)).

Ending a goal

There are three ways for an encapsulated goal to end:

  • The goal condition holds. This is the recommended approach, because it states explicitly what achieving the goal means.

  • The done; statement is executed, either in the body or in one of the goal’s inner plans. This ends the enclosing goal immediately, as if its goal condition had become true.

  • The goal has no goal condition and its body finishes, as for a normal plan. If an inner plan of the goal is still running at that point, the goal ends when that plan finishes.

In each case, the plan that adopted the goal (with !) then continues with its next statement. In the example below, !work() has no goal condition, so it ends when its body finishes, and after work is printed next:

plan +!main(list args) {
    !work();
    C.println("after work");
}

goal +!work() {
    body {
        !tidy("desk");
    }

    plan +!tidy(string R) {
        C.println("tidying the " + R);
    }
}

Leaving out the goal condition is the same as writing <: done, which you can use to make the default explicit: goal +!work() <: done { ... }. done can only be used on its own as a goal condition; it cannot be combined with other formulae.

When the decision to stop is made by a reactive plan, that plan can end the goal with done;. Here the goal handles dirty(...) beliefs until it is told to stop:

goal +!watch() <: false {
    plan +dirty("stop") {
        done;
    }

    plan +dirty(string R) {
        C.println("cleaning the " + R);
    }
}

Inner plans are tried in order, so the first plan handles dirty("stop") and the second handles every other room.

References

Ricci, A., Bordini, R.H., Hübner, J.F., Collier, R. (2018) AgentSpeak(ER): Enhanced Encapsulation in Agent Plans. In: Engineering Multi-Agent Systems (EMAS 2018), pp. 34–51. Springer, Cham. See also the other AgentSpeak(ER) papers listed under Publications.