Custom Events and Unification

A custom event connects a Java notification to an ASTRA plan. Use it when the occurrence matters, such as a device callback or completion notice. Use a term or formula alongside it when the agent also needs to query current state.

The following alternative to the polling device uses the same "on"/"off" vocabulary as Chapter 5. Its Java action posts a notification after changing the simulated device. It does not publish ordinary beliefs automatically.

Defining a Custom Event

Create src/main/java/example/LightChanged.java:

package example;

import astra.event.Event;
import astra.reasoner.util.LogicVisitor;
import astra.term.Term;

public class LightChanged implements Event {
    private final Term state;

    public LightChanged(Term state) {
        this.state = state;
    }

    public Term state() {
        return state;
    }

    @Override
    public Object getSource() {
        return null;
    }

    @Override
    public String signature() {
        return "$light:";
    }

    @Override
    public Event accept(LogicVisitor visitor) {
        return new LightChanged((Term) state.accept(visitor));
    }
}

The event carries an ASTRA term so it can represent either the emitted string value or a variable in a plan’s event pattern. accept applies the interpreter’s visitor to that term. The signature identifies the event family used for plan matching.

Implementing an EventUnifier

Create src/main/java/example/LightChangedUnifier.java:

package example;

import java.util.HashMap;
import java.util.Map;
import astra.core.Agent;
import astra.reasoner.EventUnifier;
import astra.reasoner.Unifier;
import astra.term.Term;

public class LightChangedUnifier implements EventUnifier<LightChanged> {
    @Override
    public Map<Integer, Term> unify(
            LightChanged source, LightChanged target, Agent agent) {
        return Unifier.unify(
            new Term[] { source.state() },
            new Term[] { target.state() },
            new HashMap<Integer, Term>(), agent);
    }
}

The unifier delegates matching of corresponding terms to ASTRA. Matching binds variables in the plan pattern or fails when a literal pattern does not match the emitted value.

Linking the Event to the Module

Create src/main/java/example/EventLightDevice.java:

package example;

import astra.core.Module;
import astra.event.Event;
import astra.reasoner.Unifier;
import astra.term.Primitive;
import astra.term.Term;

public class EventLightDevice extends Module {
    static {
        Unifier.eventFactory.put(LightChanged.class, new LightChangedUnifier());
    }

    private volatile String state = "off";

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

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

    @EVENT(symbols = {}, types = {"string"}, signature = "$light:")
    public Event changed(Term state) {
        return new LightChanged(state);
    }
}

The static block registers the event’s unifier. The changed method constructs a pattern for ASTRA’s $device.changed(...) syntax. Its annotation uses the same signature as the event class. types describes the ASTRA argument types because the Java parameters themselves are Term objects. An empty symbols list means the event has no + or - prefix.

The action posts a notification only for an actual change. In a real integration, an external callback can enqueue the same event. Posting an event does not wait for a matching plan to finish.

Using the Module

Create src/main/astra/EventController.astra:

agent EventController {
    module example.EventLightDevice device;
    module Console console;
    module System system;
    types observations { formula reported(string); }

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

    plan $device.changed(string State) {
        +reported(State);
    }
}

The event plan stores evidence that the notification was processed. A term query supplies the current device state. Run with -Dastra.main=EventController using the sample project.

This module emits no initial event: its initial state is available through state(). A notification is not a persistent current-state belief. If an application needs one, update it deliberately in the event plan, with the replacement policy appropriate to the domain.

Sensors or Events?

A sensor publishes sampled state on reasoning cycles; a custom event represents a particular occurrence. Event delivery avoids polling when Java already supplies callbacks, but transient events cannot answer a later query about current state. Terms and formulae provide that complementary interface.

For observation-backed goals, use bounded confirmation waits. For direct state queries, see Custom Terms and Formulae.