Callable Goals and Dynamic Invocation

ASTRA 2.5.1 adds callable goal metadata and the invoke statement. These are language and runtime features: they work without an LLM, MCP or any other protocol. GoalInvocation belongs to astra-interpreter; the Goals module is astra.lang.Goals, provided by astra-apis.

Invoking a goal represented as text

This complete agent doubles 7 using an ordinary achievement goal:

import astra.core.GoalInvocation;

agent MainGoals {
    module Goals goals;
    module Console C;
    module System S;
    types example { formula multiplier(int); }
    initial multiplier(2);

    rule +!main([]) {
        GoalInvocation call = goals.fromString("!double(7, int result)");
        invoke(call);
        C.println("Native goal result: " + goals.resultAsInt(call, "result"));
        S.exit();
    }
    rule +!double(int value, int result) : multiplier(int factor) {
        result = value * factor;
    }
}

It prints Native goal result: 14. No annotation is necessary for this direct invocation: double is selected using the calling agent’s normal rules and guards. The runnable MainGoals example is maintained in the separate examples project.

fromString treats ground literals as inputs and explicitly typed variables as outputs. The text !double(7, int result) supplies the integer input 7 and names an integer output result. The name inside the string is not a variable in the surrounding ASTRA rule. Read its binding from the completed invocation instead.

The parser accepts strings, characters, booleans, integers, longs with an L/l suffix, floats with an F/f suffix, finite doubles, ground lists and ground function terms. Lists and function terms may be nested. String escapes include \n, \r, \t, \b, \f, quotes and backslashes. Output types are int, long, float, double, string, boolean, char, list and funct. It also requires a leading ! and accepts an optional trailing semicolon. Invalid goal text raises a runtime exception with the message Goal was not declared: xxx, where xxx is the original string. ASTRA 2.5.3 requires this validation; ASTRA 2.5.2 and earlier also accept strings without the leading !.

It does not evaluate arithmetic expressions, module calls or untyped variables. For example, !double(3+4, int result) and !double(7, Result) are invalid. Goal text is limited to 32768 characters, 128 parameters and 64 levels of data nesting. Every output must have a distinct name.

Native subgoal semantics

invoke(call) is a statement, distinct from an ordinary goal named invoke. Existing !invoke(...) goals and module methods such as module.invoke(...) retain their meaning. The belief-query statement query(...) is unchanged.

The interpreter queues an achievement-goal event linked to the parent rule executor, exactly as for a normal subgoal. The parent intention suspends while the subgoal runs; other intentions may continue. Normal rule selection, guards, beliefs and output binding apply. Java does not create an agent, inject an independent top-level intention or run a reasoning job.

When the subgoal succeeds, its output bindings are captured in the invocation. Every declared output must be bound to a compatible value. Missing goals, failed subgoals and unbound outputs fail the statement and participate in normal try/recover handling:

GoalInvocation call = goals.fromString("!double(-1, int result)");
try {
    invoke(call);
    int result = goals.resultAsInt(call, "result");
    C.println(result);
} recover {
    C.println("The dynamic subgoal failed");
}

Whether this particular goal fails depends on the agent’s rules and guards. Create a fresh GoalInvocation for each execution. An invocation executes once, and successful output readers fail before completion, after failure, or when the requested output name or type does not match.

Callable metadata

Annotate an achievement-goal rule when another component should discover its callable contract:

@Callable(
    description="Double an integer",
    inputs=["value"],
    outputs=["result"]
)
rule +!double(int value, int result) : multiplier(int factor) {
    result = value * factor;
}

All three annotation attributes are required. The description is a string literal. Inputs and outputs are lists of parameter names; empty lists are valid. Every parameter must be a distinct, explicitly typed variable and appear in exactly one of the two lists. Parameter names refer to the annotated rule’s goal arguments, not variables in its body. Input/output positions follow the goal signature, regardless of the order in the annotation lists.

Place the annotation immediately before a rule or plan for a +!goal(...) event. Belief events, removal events and encapsulated goals are not annotation sites in this implementation. Annotate a signature once per agent class. Other rules for that signature remain eligible for normal selection; the annotation never forces a specific rule or bypasses its guard.

The compiler emits a CallableGoal contract with the name, description, parameter types and directions. goals.callables() returns these descriptors as an ASTRA list. Contracts are inherited; a subclass’s explicitly annotated signature takes precedence over the matching inherited contract. A subclass can also provide additional rules while using inherited metadata.

Java integrations can read agent.getASTRAClass().callables() and construct a GoalInvocation from a contract and named, typed input terms. The annotation is protocol-independent and does not publish a service or register a model tool.

Goals module reference

Term

Result

fromString(string text)

A fresh GoalInvocation for the textual goal.

callables()

A list of the agent’s CallableGoal descriptors.

resultAsInt(call, name)

Integer output.

resultAsLong(call, name)

Long output.

resultAsFloat(call, name)

Float output.

resultAsDouble(call, name)

Double output.

resultAsBoolean(call, name)

Boolean output.

resultAsString(call, name)

String output.

resultAsChar(call, name)

Character output.

resultAsList(call, name)

List output.

resultAsFunct(call, name)

Function-term output.

Using callable goals with MCP

ReActMCP is one consumer of this language feature. Calling tools.registerCallables() explicitly imports the agent’s callable contracts into its model tool catalogue. The tools module converts model arguments into typed inputs, prepares a GoalInvocation, and serializes completed outputs. The ASTRA invoke statement executes the subgoal. Applications need no handwritten JSON conversion or !invoke wrapper for annotated goals.