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 |
|
Obtain a value for an expression |
|
Test or query module state |
|
Refresh observations during reasoning |
|
React to a transient callback or notification |
|
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.