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 |
|---|---|
|
A fresh |
|
A list of the agent’s |
|
Integer output. |
|
Long output. |
|
Float output. |
|
Double output. |
|
Boolean output. |
|
String output. |
|
Character output. |
|
List output. |
|
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.