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.

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 two 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 in the body. This ends the enclosing goal immediately, as if its goal condition had become true.

Without a goal condition, a goal is meant to be achieved when its body finishes, as for a normal plan. In ASTRA 2.0.13, however, a goal without a goal condition that is adopted as a sub-goal (with !) does not resume the plan that adopted it when its body finishes. Until this is fixed, either give the goal a goal condition or end its body with done;:

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

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

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

In ASTRA 2.0.13, executing done; inside an inner plan (rather than the body) ends the goal but also raises an internal error, so prefer a goal condition when the decision to stop is made by a reactive plan.

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.