Class DecisionSchedulerPlanner
- All Implemented Interfaces:
Planner
DecisionModel how useful each of them would be now, and that stops when the decision model considers a
goal reached. Compared with a supervisor, each step costs one decision call instead of a call to a chat model, but
a decision model cannot generate the arguments of the next agent: every subagent must read its inputs from the
AgenticScope.
At each step the candidates are the subagents that can run now: all their mandatory arguments are in the scope, their activation condition holds, and they either never ran or had one of their arguments changed since their last run. An argument is mandatory when it has no default value and is not optional. The planner then sends a single request to the decision model with:
- one scale question per candidate, by default "How useful would it be to run the agent '...' (its description) now, to reach this goal: ...?", with the levels "USELESS", "MARGINAL", "USEFUL" and "ESSENTIAL". The score of the candidate is the mean level of the answer, scaled between 0 (the lowest level) and 1 (the highest level);
- one yes/no question, by default "Has this goal been reached: ...?".
id instead, and the decision
model tells them apart by their descriptions. A subagent without a description is described by its name.
If the probability that the goal has been reached is at least the goal threshold the planner stops. Otherwise it
invokes the candidate with the highest score, the first declared one when several have the same score, provided
that this score is at least the activation threshold. The planner also stops when no subagent can run, when no
candidate reaches the activation threshold, and after the maximum number of invocations, where the decision model
is not asked again and a warning is logged. In all cases the result is the value found in the scope under the
output key of the planner agent. When one of the subagents writes that output key, the goal question is not asked
until the key is in the scope, since the goal cannot be reached before the result exists, and stopping in one of
these three ways before then fails with an IllegalStateException saying why the planner stopped, so that
giving up cannot be mistaken for an empty result: a caller that wants a fallback has to catch it.
The input of the decision is made of the arguments of the planner agent itself, read from the scope, plus the
outputs of the subagents that are in the scope, each under its output key. The arguments are sent as they are, so
they can be strings, numbers, booleans, maps and lists of them, or Contents, and an argument of any other
type is rejected by the decision model with an error explaining which types are supported: the application decides
which of its objects are sent to the provider of the decision model. The outputs of the subagents are sent as they
are when they are such values, and converted to maps and lists through a JSON round trip otherwise, which
serializes all their fields, private ones included. This state is sent at every step to the provider of the
decision model, which can differ from the provider of the chat models of the subagents: the outputs that must not
reach it can be left out of the input through DecisionSchedulerPlanner.Builder.excludedOutputKeys(String...). The decision model
sees this state only: it is not told which subagents already ran, and the work of a subagent without an output
key, or whose output key is left out, is invisible to it.
A subagent whose description cannot be read because it is unavailable, like an A2A agent whose server is down, is left out of the candidates for the rest of the invocation, with a warning. A failure of the decision model fails the invocation.
The planner saves as its execution state the number of invocations, the subagents that cannot run again yet and the subagent being invoked, so that after a suspension or a crash it resumes from that subagent. This state refers to the subagents by their names, so it does not depend on the order in which they are declared. Only the subagents sharing a name are referred to by their ids, which contain their position, and have to be declared in the same position when the planner is resumed. When a subagent waits for a human the planner suspends without asking the decision model, and decides on the answer when it is resumed. The state is saved only after each decision, so a crash during a decision call makes the subagent that had just completed run again, and a crash before the first subagent completes makes the planner decide again. A subagent whose output can be delayed, like a human-in-the-loop or an asynchronous agent, must declare an output key, since the planner reads its answer from the scope: the decision following an asynchronous subagent waits for its answer, unless its output key is left out of the input.
Each step is a greedy choice with no lookahead, so this planner fits the tasks where the most useful next step can be told from the current state, not those that need a plan of several steps. The scores and the probabilities are not calibrated in the same way by every decision model, so both thresholds must be tuned on the decision model in use and tuned again when it changes. The state and the descriptions of the subagents, like those of remote A2A agents, are untrusted input to the decision, so a crafted request, agent output or description can influence it: this planner is not a security boundary, and a subagent with side effects must be guarded on its own, for instance through an activation condition.
A planner instance holds the state of a single invocation and is not thread-safe, so the agentic system must be
given a supplier creating a new instance at each invocation, for example
() -> DecisionSchedulerPlanner.builder().decisionModel(decisionModel).goal("...").goalThreshold(0.6)
.activationThreshold(0.3).build(), while the decision model can be shared. An instance that is initialized a
second time fails with an IllegalStateException.
-
Nested Class Summary
Nested Classes -
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final PromptTemplateThe default template of the yes/no question asking whether the goal has been reached, with the variable{{goal}}.static final intThe default maximum number of subagent invocations.The default levels of the usefulness question, from the lowest to the highest.static final PromptTemplateThe default template of the scale question asking how useful it would be to invoke a subagent now, with the variables{{name}}and{{description}}of the subagent, and{{goal}}. -
Method Summary
Modifier and TypeMethodDescriptionbuilder()Returns the planner's current execution state as a map of serializable values.firstAction(PlanningContext planningContext) Returns the first action to execute when the planner starts (or resumes).voidinit(InitPlanningContext initPlanningContext) nextAction(PlanningContext planningContext) Determines the next action to execute based on the result of the previous agent invocation.voidrestoreExecutionState(Map<String, Object> state) Restores the planner's execution state from a previously saved map.topology()Returns the topology of the agentic system managed by this planner.
-
Field Details
-
DEFAULT_GOAL_QUESTION_TEMPLATE
The default template of the yes/no question asking whether the goal has been reached, with the variable{{goal}}. -
DEFAULT_USEFULNESS_QUESTION_TEMPLATE
The default template of the scale question asking how useful it would be to invoke a subagent now, with the variables{{name}}and{{description}}of the subagent, and{{goal}}. -
DEFAULT_USEFULNESS_LEVELS
-
DEFAULT_MAX_INVOCATIONS
public static final int DEFAULT_MAX_INVOCATIONSThe default maximum number of subagent invocations.- See Also:
-
-
Method Details
-
builder
-
init
-
firstAction
Description copied from interface:PlannerReturns the first action to execute when the planner starts (or resumes). Defaults to delegating toPlanner.nextAction(PlanningContext).- Specified by:
firstActionin interfacePlanner- Parameters:
planningContext- the current planning context- Returns:
- the first action to execute
-
nextAction
Description copied from interface:PlannerDetermines the next action to execute based on the result of the previous agent invocation.- Specified by:
nextActionin interfacePlanner- Parameters:
planningContext- the current planning context, including the previous agent's result- Returns:
- the next action to execute
-
executionState
Description copied from interface:PlannerReturns the planner's current execution state as a map of serializable values. This state is persisted to theAgenticScopeafter each agent invocation, enabling the planner to resume from the correct position after a crash.The returned state must be such that, when passed to
Planner.restoreExecutionState(Map)andPlanner.firstAction(PlanningContext)is called, the planner produces the correct resume action.Stateless planners (e.g., parallel, conditional) can use the default empty implementation.
- Specified by:
executionStatein interfacePlanner- Returns:
- a map of state entries to persist, or an empty map if no state needs saving
-
restoreExecutionState
Description copied from interface:PlannerRestores the planner's execution state from a previously saved map. Called by the execution loop beforePlanner.firstAction(PlanningContext)when recovering from a persisted scope.- Specified by:
restoreExecutionStatein interfacePlanner- Parameters:
state- the previously saved execution state
-
topology
Description copied from interface:PlannerReturns the topology of the agentic system managed by this planner.- Specified by:
topologyin interfacePlanner- Returns:
- the topology (defaults to
AgenticSystemTopology.SEQUENCE)
-