5. Observing and acting through modules

A module is the boundary between an agent’s decisions and Java functionality. The agent chooses a desired state; a module action attempts a change; a query or observation reports the result. This chapter replaces the light controller’s mental simulation with a Java device.

Start with the built-in modules whenever they provide what you need. Declare a custom module when you need application-specific Java functionality. Use CArtAgO or EIS when an environment already exposes one of those interfaces.

Create the device module

Put this complete class in src/main/java/example/LightDevice.java:

package example;

import astra.core.Module;
import astra.formula.Formula;
import astra.formula.Predicate;
import astra.term.Primitive;
import astra.term.Term;

public class LightDevice extends Module {
    private volatile String state = "off";
    private String observed;
    private Predicate belief;

    @ACTION
    public boolean set(String target) {
        if (!target.equals("on") && !target.equals("off")) return false;
        state = target;
        return true;
    }

    @TERM
    public String state() {
        return state;
    }

    @FORMULA
    public Formula is(String expected) {
        return state.equals(expected) ? Predicate.TRUE : Predicate.FALSE;
    }

    @SENSOR
    public void sense() {
        String current = state;
        if (current.equals(observed)) return;
        if (belief != null) agent.beliefs().dropBelief(belief);
        belief = new Predicate("light", new Term[] {
            Primitive.newPrimitive(current)
        });
        agent.beliefs().addBelief(belief);
        observed = current;
    }
}

The device is simulated by a Java field. set rejects unsupported targets and changes that field. It does not assert the desired belief. The sensor publishes an initial observation and subsequent changes into this agent’s belief base, removing the previous light state.

volatile makes the field visible across the action and sensing threads. A real device adapter needs an appropriate concurrency policy for its own state and callbacks. Keep sensors quick: they run as part of the reasoning cycle, so long network operations should not block them.

Use the device from an agent

Put this program in src/main/astra/DeviceController.astra:

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

    types lighting {
        formula light(string);
    }

    plan +!main(list args) {
        wait(light("off"), 2000);
        !light("on");
        console.println("observed light = " + device.state());
        !light("off");
        system.exit();
    }

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

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

Run mvn clean compile astra:run -Dastra.main=DeviceController. The initial wait establishes that the first observation is available. The goal plan performs the action and waits for the sensor’s evidence. A successful Java action and its observation are separate steps, even for this fast simulation.

The same device can be queried directly. string State = device.state(); uses a term in an expression. if (device.is("on")) { ... } uses a formula as a condition. Neither query automatically produces a belief-change event.

Choose an observation mechanism

Need

Mechanism

Execute a concrete Java operation

@ACTION returning success or failure

Obtain a value for an expression

@TERM returning a typed value

Test or query module state

@FORMULA returning a formula

Refresh observations during reasoning

@SENSOR returning void

React to a transient callback or notification

@EVENT with an event class and unifier

A sensor is useful when state must be polled. A custom event is useful when Java already receives a notification. Events describe occurrences; a later observer cannot query a past notification as current state unless the application retains it. Combine events with terms or formulae when you need both notifications and current-state queries.

Each module declaration creates an agent-associated instance. Two agents declaring LightDevice get separate simulated devices. If the application needs one shared physical device, put ownership and shared state in an explicit service or environment rather than assuming module instances are shared.

Next: Handling uncertainty and failure.

Further Details