Teleo-Reactive Functions

ASTRA stands for AgentSpeak(TR) Agents: alongside AgentSpeak(L) plans, it supports the teleo-reactive (TR) programs proposed by Nils Nilsson (see References). A plan is a sequence of steps that is worked through once. A TR program is different: it is an ordered list of condition → action rules that the agent checks again and again, on every cycle, always doing whatever the first rule whose condition holds says. If the situation changes, because the last action worked, because it failed, or because something else changed the world, the agent simply finds itself under a different rule and carries on from there.

This makes TR programs a good fit for behaviour that has to keep reacting to the current situation until a goal is reached, such as controlling a device, moving towards a position, or keeping a value within a range. In ASTRA, a TR program is called a function. Plans start and stop functions, and functions and plans work together through the agent’s beliefs.

Syntax

function name(type Param, ...) {
    condition1 -> action1
    condition2 -> action2
    ...
}

Part

Meaning

name(type Param, ...)

The function’s name and typed parameters, as in a goal. A function can have no parameters.

condition

Any formula that could appear in a plan context. Variables declared in the condition, such as int X in count(int X), can be used in the action.

->

Separates a rule’s condition from its action.

action

One action, or a block of actions in braces (see Actions). Rules are not separated by semi-colons.

Each agent can run at most one function at a time. Plans control it with two statements:

Statement

Meaning

activate name(...);

Start the function. The plan fails if a function is already running.

deactivate;

Stop the running function. The plan fails if no function is running.

activate and deactivate are reserved words, so they cannot be used as the names of beliefs, goals or actions.

How a function runs

Once a function has been started:

  1. It is evaluated on every cycle, after the agent has executed a step of one of its intentions.

  2. The first rule whose condition holds is selected. The rules are checked in the order in which they are written, and the action of the first one whose condition holds is executed. If no condition holds, nothing happens on that cycle and the function keeps running.

  3. The action is executed again on the next cycle if the same rule is still selected. A TR action describes what to do while the condition holds, not something to do once.

  4. Belief updates take effect at the start of the next cycle. A rule that removes count(0) and adds count(1) sees count(1) on the following cycle, not straight away. Adding a belief that the agent already has does not create a new +belief event, so a rule that adds the same belief on every cycle only triggers a plan once.

  5. The function keeps running until it is stopped, either by deactivate in a plan, by a deactivate action in the function itself, or because of an error. While a function is running, the agent never becomes idle: it evaluates the function on every cycle even when nothing has changed.

Because the first rule that applies always wins, rules are normally written with the goal first: the first rule says what to do when the goal has been achieved (often stop, or nothing), and each later rule handles a situation that is further from the goal. Nilsson calls this the regression property: each action should bring about the condition of a rule higher up the list.

Actions

A rule’s action can be one of the following:

Action

Meaning

+belief(...) / -belief(...)

Add or remove a belief.

module.action(...)

Perform a module action, such as C.println("..."). The action runs immediately, as part of the cycle, so it should be quick.

name(...)

Call another function: its rules are checked straight away, in the same cycle, and the first one that holds is executed.

deactivate

Stop the function.

{ action; action; ... }

Perform several actions in order. The semi-colon after the last action is optional.

A function call or deactivate ends the rule, so in a block it must be the last action; the compiler rejects a block in which another action follows them. If a module action in a block fails, the rest of the block is skipped on that cycle; the function itself keeps running.

These are the only actions that a function can perform. To do anything else, such as adopt a goal or send a message, add a belief and handle it with a plan, as in the shuttle example.

Example: counting to a target

This agent counts up from 0. The function’s first rule is the goal: when the count has reached the target, it prints a message and stops the function. The second rule moves the count one step closer:

agent Counter {
    module Console C;
    types counter { formula count(int); }
    initial count(0);

    rule +!main(list args) {
        activate countTo(3);
    }

    function countTo(int N) {
        count(N) -> { C.println("reached " + N); deactivate }
        count(int X) -> { C.println("count is " + X); -count(X); +count(X + 1) }
    }
}

The output is:

[main] count is 0
[main] count is 1
[main] count is 2
[main] reached 3

On the first cycle, count(3) does not hold, so the second rule is selected with X bound to 0. Its belief updates take effect at the start of the next cycle, when count(1) is checked, and so on until count(3) holds and the first rule stops the function.

Calling other functions

A function can call another function to do part of its work. This agent moves a shuttle along a track to position 5. The goTo function decides which way to move and calls move to take a step. When the shuttle arrives, goTo adds the belief arrived(5), and a plan reacts to that belief by printing a message and stopping the function:

agent Shuttle {
    module Console C;
    types shuttle { formula at(int); formula arrived(int); }
    initial at(2);

    rule +!main(list args) {
        activate goTo(5);
    }

    rule +arrived(int P) {
        C.println("arrived at " + P);
        deactivate;
    }

    function goTo(int Target) {
        at(Target) -> +arrived(Target)
        at(int P) & P < Target -> move(1)
        true -> move(-1)
    }

    function move(int D) {
        at(int P) -> { C.println("at " + P); -at(P); +at(P + D) }
    }
}

The output is:

[main] at 2
[main] at 3
[main] at 4
[main] arrived at 5

The last rule of goTo uses true as its condition, so it applies whenever the rules above it do not: here, when the shuttle is past the target and has to move back. Ending a function with a true rule is a common way to make sure that some rule always applies.

goTo keeps adding arrived(5) on every cycle until the plan stops it, but because the agent already has that belief, the +arrived plan runs only once.

Starting and stopping functions from plans

activate only starts the function: the plan carries on with its next statement straight away, and the function runs alongside the agent’s intentions until it is stopped. A function’s arguments can be any terms, including variables bound in the plan, for example activate goTo(T);.

Because an agent can only run one function at a time, activate fails if a function is already running, and deactivate fails if none is. Like any other failure, this can be handled with try/recover (see Failure Handling):

agent AlreadyActive {
    module Console C;
    types hold { formula holding(); }

    rule +!main(list args) {
        activate hold();
        try {
            activate hold();
        } recover {
            C.println("a function is already active");
        }
        deactivate;
    }

    function hold() {
        true -> +holding()
    }
}

The output is:

[main] a function is already active

To switch from one function to another, stop the running function first and then start the new one. A function cannot use activate itself: to hand over to another function, call it.

Errors

The compiler reports the following as errors:

  • activate or a function call that names a function that is not declared, or that passes the wrong number of arguments;

  • two functions with the same name in one agent (functions are identified by name alone, so f(int) and f(int, int) cannot both be declared);

  • a rule without ->, or with a missing or unknown action;

  • an empty block, or a block in which a function call or deactivate is not the last action;

  • activate used as an action inside a function;

  • brackets after deactivate, or around the function in activate (write activate f(1);, not activate(f(1));).

Some problems can only be found while the agent is running. In each of these cases the agent prints an error, together with the chain of function calls that led to it, and stops the function; the agent’s plans are not affected:

  • a function that calls itself again with the same arguments in the same cycle, directly or through other functions (for example, f() calls g(), which calls f()): this would never end;

  • more than 100 nested function calls in one cycle, for example a function that calls itself with a different argument each time;

  • a call whose arguments do not match the function’s parameters, or (with activate F;, where F is a variable) a function that does not exist.

Functions and inheritance

Functions are inherited like plans: an agent can start or call a function declared in any of its parent agents. An agent can also replace an inherited function by declaring one with the same name. The agent’s own function is then used for every call of that name, even if it has a different number of parameters (see Multiple Inheritance).

References

Nilsson, N.J. (1994) Teleo-Reactive Programs for Agent Control. Journal of Artificial Intelligence Research 1, pp. 139–158.