Failure Handling

Plans do not always succeed. An action can fail, a belief that a plan relies on may not be there, or a sub-goal may have no plan that can achieve it. ASTRA gives you two ways of dealing with failure:

  • try/recover statements, which handle a failure inside the plan where it happens, in the same way as exception handling in Java;

  • goal failure events (^!goal(...)), which let you write separate plans that respond when a goal could not be achieved.

A third event, the missing plan event (~!goal(...)), lets a plan step in when a goal has no plan at all, before anything has failed.

What counts as a failure

A statement fails, and with it the plan that contains it, when:

  • a module action fails (the Java method returns false or throws an exception), including S.fail() from the System module;

  • the fail; statement is executed;

  • a query(...) cannot be matched against the agent’s beliefs;

  • a sub-goal (!g) or test goal (?f) has no applicable plan, and (for a sub-goal) no missing plan plan handles it;

  • a sub-goal’s plan fails, and the failure is not handled further down.

try / recover

A try statement runs a block of statements. If any of them fails, the rest of the block is skipped and the recover block runs instead. Either way, the plan then continues with the statement after the try:

try {
    C.println("trying");
    S.fail();
    C.println("not reached");
} recover {
    C.println("recovered");
}
C.println("continues");
[main] trying
[main] recovered
[main] continues

The try block can contain sub-goals. If a sub-goal fails and the failure is not handled by the sub-goal itself (see below), the recover block of the calling plan runs:

try {
    !nowhere();          // there is no plan for !nowhere()
} recover {
    C.println("could not achieve !nowhere()");
}

If a statement in the recover block fails, that failure is handled in the normal way, as if the try statement itself had failed.

Goal failure events

When a plan that was adopted to achieve a goal fails, ASTRA generates a goal failure event for that goal. It is written with ^ in place of +:

plan ^!connect() {
    C.println("could not connect");
}

A plan for ^!g(...) is selected like any other plan, so it can use variables and a context. When it finishes, the failure counts as handled: the plan that adopted the goal continues as if the goal had been achieved. This also works for goals adopted with !!, which run as their own intention, and for failures inside the inner plans of an encapsulated goal.

A common use is to retry a goal. In this example, connecting fails twice before it succeeds. Each failure is handled by re-adopting the goal:

agent Connector {
    module Console C;
    module System S;

    types connection {
        formula attempts(int);
    }

    initial attempts(0);

    plan +!main(list args) {
        !connect();
        C.println("connected");
        S.exit();
    }

    plan +!connect() : attempts(int N) & N < 2 {
        -attempts(N);
        +attempts(N + 1);
        C.println("attempt " + (N + 1) + " fails");
        S.fail();
    }

    plan +!connect() {
        C.println("attempt succeeds");
    }

    plan ^!connect() {
        !connect();
    }
}
[main] attempt 1 fails
[main] attempt 2 fails
[main] attempt succeeds
[main] connected

Make sure that a retry cannot go on for ever: here, the attempts(...) belief limits the number of failing attempts.

Goal failure plans are also how the Practical Reasoning Programming Style writes its Recovery Plans.

Goal completion events

When a +!goal(...) plan completes, ASTRA raises a goal completion event, written with - in place of +. A plan for it can tidy up after a goal has been achieved:

plan -!connect() {
    C.println("connected");
}

-!goal(...) follows only a +!goal(...) plan that completes. It is not raised when the goal fails, or when a ^! or ~! plan achieves the goal instead.

Goals with no plan

A sub-goal fails when it has no plan that applies to it: either no plan’s trigger matches the goal (no relevant plan), or no matching plan’s context holds (no applicable plan). A missing plan event, written with ~ in place of +, gives the agent a chance to deal with this instead. ~!goal(...) is raised in both cases.

A common use is to create the missing plan. Here, the agent compiles a plan for !greet(...) the first time it needs one (see Compiler), then adopts the goal again:

agent Greeter {
    module Console C;
    module System S;
    module Compiler compiler;

    plan +!main(list args) {
        !greet("world");
        C.println("greeted");
        !greet("again");
    }

    plan ~!greet(string name) {
        C.println("no plan for greet: adding one");
        S.addRule(compiler.compileRule("+!greet(string who) { C.println(\"hello \" + who); }"));
        !greet(name);
    }
}
[main] no plan for greet: adding one
[main] hello world
[main] greeted
[main] hello again

The second !greet("again") uses the new plan, so ~!greet(...) is not raised again.

A ~! plan behaves like a ^! plan:

  • It runs in the intention that adopted the goal, and the goal succeeds or fails with it. Variables that it binds are passed back to the plan that adopted the goal, as for a +! plan.

  • When it completes, the goal counts as achieved. That is why the example adopts !greet(name) again: to use the plan it has just added.

  • If it fails, the goal fails, and the failure is handled as usual by a try statement around the sub-goal or a ^! plan for the adopting plan’s goal. No ^! event is raised for the goal itself, because no plan was adopted for it:

plan +!main(list args) {
    try {
        !open("door");
    } recover {
        C.println("could not open the door");
    }
}

plan ~!open(string thing) {
    C.println("no plan to open " + thing);
    fail;
}
[main] no plan to open door
[main] could not open the door
  • While a ~! plan for a goal is running, a goal with the same name and number of arguments that has no plan fails as usual, rather than raising ~! again. So a ~! plan that adopts its goal again cannot loop.

  • Goals adopted with !!, which run as their own intention, raise ~! in the same way.

Only goals with a ~! plan are affected: any other goal with no plan fails as before. This lets an agent choose, goal by goal, whether a missing plan is a failure or something it can deal with.

Which handler is used?

When a statement fails, ASTRA works outwards from the plan where the failure happened until it finds something that can handle it:

  1. a try statement around the failing statement, in the same plan;

  2. a ^!g(...) plan for the goal that the failing plan was trying to achieve;

  3. a try statement around the sub-goal in the plan that adopted that goal;

  4. a ^! plan for that plan’s goal, and so on, up to the top of the intention.

So a ^! plan for a goal takes priority over a try statement in the plan that adopted the goal. In the example below, !fetch() has its own failure plan, so the caller’s recover block is not used:

plan +!main(list args) {
    try {
        !fetch();
    } recover {
        C.println("not used: ^!fetch() handled the failure");
    }
    C.println("main continues");
}

plan +!fetch() { S.fail(); }
plan ^!fetch() { C.println("^!fetch() handled the failure"); }
[main] ^!fetch() handled the failure
[main] main continues

A failure deep inside a goal can be handled at any level above it. For example, if !outer() adopts !inner() and the plan for !inner() fails, a ^!outer() plan handles the failure when there is no ^!inner() plan.

Unhandled failures

If nothing handles a failure, the whole intention fails: ASTRA prints the failed statement and the goals that led to it, and drops the intention. Only that intention is affected. The agent’s other intentions, such as goals adopted with !!, carry on:

[main] Failed Action: S.fail()
[main] +!worker()

A ^! plan can itself fail. In that case the failure carries on outwards, as if the goal’s plan had failed without a failure plan.

Choosing between them

  • Use try/recover when the plan that is doing the work knows what to do if one particular step fails, such as using a default value or skipping an optional step.

  • Use goal failure plans when the response belongs to the goal rather than to one plan: for example, retrying, trying a different approach, or tidying up after any of the goal’s plans fails.