Class DecisionSchedulerPlanner

java.lang.Object
dev.langchain4j.agentic.patterns.decisionscheduler.DecisionSchedulerPlanner
All Implemented Interfaces:
Planner

@Experimental public class DecisionSchedulerPlanner extends Object implements Planner
A planner that chooses, one step at a time, which of its subagents to invoke next, by asking a 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: ...?".
The texts of both questions and the levels can be replaced through the builder. The planner identifies a subagent by its name, which is also the name of its question. Several subagents can have the same name: each of them is then identified by its 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.

  • Field Details

    • DEFAULT_GOAL_QUESTION_TEMPLATE

      public static final PromptTemplate 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

      public static final PromptTemplate 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

      public static final List<String> DEFAULT_USEFULNESS_LEVELS
      The default levels of the usefulness question, from the lowest to the highest.
    • DEFAULT_MAX_INVOCATIONS

      public static final int DEFAULT_MAX_INVOCATIONS
      The default maximum number of subagent invocations.
      See Also:
  • Method Details

    • builder

      public static DecisionSchedulerPlanner.Builder builder()
    • init

      public void init(InitPlanningContext initPlanningContext)
      Specified by:
      init in interface Planner
    • firstAction

      public Action firstAction(PlanningContext planningContext)
      Description copied from interface: Planner
      Returns the first action to execute when the planner starts (or resumes). Defaults to delegating to Planner.nextAction(PlanningContext).
      Specified by:
      firstAction in interface Planner
      Parameters:
      planningContext - the current planning context
      Returns:
      the first action to execute
    • nextAction

      public Action nextAction(PlanningContext planningContext)
      Description copied from interface: Planner
      Determines the next action to execute based on the result of the previous agent invocation.
      Specified by:
      nextAction in interface Planner
      Parameters:
      planningContext - the current planning context, including the previous agent's result
      Returns:
      the next action to execute
    • executionState

      public Map<String,Object> executionState()
      Description copied from interface: Planner
      Returns the planner's current execution state as a map of serializable values. This state is persisted to the AgenticScope after 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) and Planner.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:
      executionState in interface Planner
      Returns:
      a map of state entries to persist, or an empty map if no state needs saving
    • restoreExecutionState

      public void restoreExecutionState(Map<String,Object> state)
      Description copied from interface: Planner
      Restores the planner's execution state from a previously saved map. Called by the execution loop before Planner.firstAction(PlanningContext) when recovering from a persisted scope.
      Specified by:
      restoreExecutionState in interface Planner
      Parameters:
      state - the previously saved execution state
    • topology

      public AgenticSystemTopology topology()
      Description copied from interface: Planner
      Returns the topology of the agentic system managed by this planner.
      Specified by:
      topology in interface Planner
      Returns:
      the topology (defaults to AgenticSystemTopology.SEQUENCE)