conductor-oss/conductor · error · IllegalArgumentException

PLAN_EXECUTE strategy requires ``planner=<Agent>`` on the pa

Error message

PLAN_EXECUTE strategy requires ``planner=<Agent>`` on the parent agent. The legacy ``agents=[planner, fallback]`` positional shape is no longer accepted — set the named slots ``planner=`` (required) and ``fallback=`` (optional) instead.

What it means

Thrown when a PLAN_EXECUTE-strategy agent is compiled without a 'planner' sub-agent configured. The compiler requires config.getPlanner() to be non-null — this replaces the old positional agents=[planner, fallback] shape that the Python SDK already rejected at construction time. The Java compiler mirrors that hard cut so both SDK and hand-crafted JSON callers get the same migration error.

Source

Thrown at agentspan/src/main/java/org/conductoross/conductor/ai/agentspan/runtime/compiler/MultiAgentCompiler.java:2544

    // run fallback agent (agentic LLM, bounded turns).
    //
    // The JSON plan describes a DAG of operations.  Each operation is either
    // "static" (tool call with known args) or "generated" (LLM produces args).
    // Static ops compile to SIMPLE tasks.  Generated ops compile to
    // LLM_CHAT_COMPLETE → INLINE(parse) → SIMPLE(apply) chains running in
    // parallel within each step.

    private WorkflowDef compilePlanExecute(AgentConfig config) {
        // Named-slot resolution. PLAN_EXECUTE requires ``planner=``;
        // ``fallback=`` is optional. The Python SDK rejects the legacy
        // ``agents=[planner, fallback]`` positional shape at construction
        // time (see Agent.__init__); we mirror that hard cut here so the
        // Java SDK and any HTTP caller crafting JSON by hand fail with the
        // same migration message instead of silently quasi-working.
        AgentConfig plannerConfig = config.getPlanner();
        AgentConfig fallbackConfig = config.getFallback();
        if (plannerConfig == null) {
            throw new IllegalArgumentException(
                    "PLAN_EXECUTE strategy requires ``planner=<Agent>`` on the parent agent. "
                            + "The legacy ``agents=[planner, fallback]`` positional shape is no "
                            + "longer accepted — set the named slots ``planner=`` (required) and "
                            + "``fallback=`` (optional) instead.");
        }

        // Parent-level ``tools`` is the canonical plan-executable set. The
        // planner is told which tools are available (so it can't hallucinate
        // names), PAC validates ``op.tool`` names against this set, and PAC
        // wraps each emitted SIMPLE task with the tool's input guardrails
        // (if any). Empty/null degrades gracefully — no allowlist check, no
        // guardrail wrapping; the recommended shape always sets tools.
        List<ToolConfig> parentTools = config.getTools() != null ? config.getTools() : List.of();

        // Warn when a tool's guardrail uses a non-RAISE on_fail and there's
        // no fallback agent to recover. In plan mode, RETRY/FIX/HUMAN all
        // collapse to TERMINATE on the dynamic plan SUB_WORKFLOW; without a
        // configured fallback, the whole pipeline just fails — the user

View on GitHub (pinned to cf7c3e4a8a)

Solutions

  1. Set the named 'planner' field on the parent AgentConfig to the agent that produces the JSON plan, e.g. planner=<AgentConfig>.
  2. If you also need a fallback agent, set the named 'fallback' field (optional) instead of relying on positional agents=[...].
  3. Remove the legacy positional agents=[planner, fallback] list and migrate to named slots.

Example fix

// before (legacy positional shape)
AgentConfig.builder()
    .strategy(Strategy.PLAN_EXECUTE)
    .agents(List.of(planner, fallback))
    .build();
// after (named slots)
AgentConfig.builder()
    .strategy(Strategy.PLAN_EXECUTE)
    .planner(planner)
    .fallback(fallback)  // optional
    .build();
Defensive patterns

Strategy: validation

Validate before calling

void validatePlanExecute(AgentConfig config) {
    if (config.getStrategy() == AgentConfig.Strategy.PLAN_EXECUTE) {
        if (config.getPlanner() == null) {
            throw new IllegalArgumentException(
                "PLAN_EXECUTE requires planner=<Agent> on the parent agent. "
                + "Set the named 'planner' field (required) and optionally 'fallback'.");
        }
    }
}

Try / catch

try {
    compiler.compile(agentConfig);
} catch (IllegalArgumentException e) {
    if (e.getMessage().contains("PLAN_EXECUTE strategy requires")) {
        // migrate from agents=[...] positional to named planner=/fallback= slots
    }
    throw e;
}

Prevention

When it happens

Trigger: An AgentConfig with strategy=PLAN_EXECUTE where config.getPlanner() returns null. This happens when the planner slot was never set, or when the caller used the deprecated positional agents list instead of the named planner= field.

Common situations: Upgrading from an older API version that accepted agents=[planner, fallback] positionally, or writing PLAN_EXECUTE config by hand in JSON without knowing about the named planner= field. Also common when migrating from the Python SDK's older API.

Related errors


AI-assisted analysis of conductor-oss/conductor@cf7c3e4a8a (2026-08-14). Data as JSON: /api/errors/70f27d60e25b2170. Report an issue: GitHub.