6. Handling uncertainty and failure

An agent may lack a needed fact, an action may fail, or the environment may take time to reflect a change. Treat these as different situations. Choosing the right check makes both the program’s behavior and its failure paths easier to understand.

Choose a knowledge check

Construct

When it succeeds

When the formula does not hold

if (f)

Selects the true branch and binds matching variables

Selects the alternative branch, if provided

query(f);

Continues with matching bindings

Fails the plan

?f;

Uses existing knowledge or a test-goal plan

Fails if no applicable plan can supply the answer

wait(f, timeout);

Continues when the formula holds

Suspends until it holds or the timeout fails the wait

Use a query for a prerequisite that must already be known. Use a test goal when the agent can actively find out. Use a wait when another process supplies the information, such as a sensor reporting the effect of a device action.

A test-goal plan must bind its requested outputs. Its completion is trusted: ASTRA does not automatically recheck that the requested belief now holds. End the plan with an explicit query when that confirmation matters. The detailed Test Goals page demonstrates fetching and caching missing knowledge.

Bound external waits

The light device plan uses wait(light(Target), 2000). If the action succeeds but the sensor never observes the result, the goal fails after two seconds. The timeout is part of this demonstration’s contract; choose a suitable value for the actual device.

A timeout does not undo an external operation. The light may change after the caller has stopped waiting. After a timeout, reconcile the observed state before retrying an operation whose effects cannot safely be repeated.

Recover at the right level

This complete program uses the module from Chapter 5 and handles an unsupported target locally:

agent ReliableController {
    module example.LightDevice device;
    module Console console;
    module System system;

    types lighting { formula light(string); }

    plan +!main(list args) {
        wait(light("off"), 2000);
        try {
            !light("dim");
        } recover {
            console.println("unsupported target rejected");
        }
        !light("on");
        console.println("valid target achieved");
        system.exit();
    }

    plan +!light(string Target) : light(Target) { }

    plan +!light(string Target) {
        device.set(Target);
        wait(light(Target), 2000);
    }
}

Run with -Dastra.main=ReliableController. The device rejects "dim", so the caller’s recovery block runs. It then requests a valid state and completes normally.

Use try/recover when the calling plan has a specific alternative for a failed step. A goal failure plan, plan ^!light(string Target), is appropriate when recovery belongs to that goal across all of its achievement plans.

A successfully completed failure plan counts as handling the goal’s failure, so its caller continues. Do not merely print an error in a failure plan if the caller must know the goal remains unachieved. Either establish a genuine fallback result, explicitly report the outcome through a protocol, or let failure propagate. A goal’s own failure handler takes precedence over a recovery block surrounding the caller’s subgoal.

Preserve evidence and progress

Belief updates and device operations are not transactions: changes already made are not automatically rolled back after failure. Record enough context to know what succeeded and what still needs repair. Bound retry counts, and retry only when there is a reason to expect progress.

Failure normally ends one intention, not all of the agent’s other work. That distinction becomes important when several requests or ongoing activities run concurrently.

Next: Managing ongoing activities.

Further Details