selectNext() {
+ int idx = cursor.getAndIncrement();
+ if (agents == null || idx >= agents.size()) {
+ return Single.just(new PlannerAction.Done());
+ }
+ return Single.just(new PlannerAction.RunAgents(agents.get(idx)));
+ }
+}
diff --git a/contrib/planners/src/main/java/com/google/adk/planner/SupervisorPlanner.java b/contrib/planners/src/main/java/com/google/adk/planner/SupervisorPlanner.java
new file mode 100644
index 000000000..0a75b54c1
--- /dev/null
+++ b/contrib/planners/src/main/java/com/google/adk/planner/SupervisorPlanner.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.adk.planner;
+
+import com.google.adk.agents.BaseAgent;
+import com.google.adk.agents.Planner;
+import com.google.adk.agents.PlannerAction;
+import com.google.adk.agents.PlanningContext;
+import com.google.adk.events.Event;
+import com.google.adk.models.BaseLlm;
+import com.google.adk.models.LlmRequest;
+import com.google.adk.models.LlmResponse;
+import com.google.common.collect.ImmutableList;
+import com.google.genai.types.Content;
+import com.google.genai.types.GenerateContentConfig;
+import com.google.genai.types.Part;
+import io.reactivex.rxjava3.core.Single;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A planner that uses an LLM to dynamically decide which sub-agent(s) to run next.
+ *
+ * The LLM is given a system prompt describing the available agents and their descriptions, the
+ * current state, and recent events. It responds with the agent name(s) to run, "DONE", or "DONE:
+ * summary".
+ */
+public final class SupervisorPlanner implements Planner {
+
+ private static final Logger logger = LoggerFactory.getLogger(SupervisorPlanner.class);
+
+ private final BaseLlm llm;
+ private final Optional systemInstruction;
+
+ public SupervisorPlanner(BaseLlm llm, String systemInstruction) {
+ this.llm = llm;
+ this.systemInstruction = Optional.ofNullable(systemInstruction);
+ }
+
+ public SupervisorPlanner(BaseLlm llm) {
+ this(llm, null);
+ }
+
+ @Override
+ public Single firstAction(PlanningContext context) {
+ return askLlm(context);
+ }
+
+ @Override
+ public Single nextAction(PlanningContext context) {
+ return askLlm(context);
+ }
+
+ private Single askLlm(PlanningContext context) {
+ String prompt = buildPrompt(context);
+ LlmRequest.Builder requestBuilder =
+ LlmRequest.builder()
+ .contents(
+ ImmutableList.of(
+ Content.builder().role("user").parts(Part.fromText(prompt)).build()));
+ systemInstruction.ifPresent(
+ si ->
+ requestBuilder.config(
+ GenerateContentConfig.builder()
+ .systemInstruction(Content.fromParts(Part.fromText(si)))
+ .build()));
+ LlmRequest request = requestBuilder.build();
+
+ return llm.generateContent(request, false)
+ .lastOrError()
+ .map(
+ response -> {
+ String text = extractText(response);
+ return parseResponse(text, context);
+ })
+ .onErrorReturn(
+ error -> {
+ logger.warn("LLM call failed in SupervisorPlanner, returning Done", error);
+ return new PlannerAction.Done();
+ });
+ }
+
+ private String buildPrompt(PlanningContext context) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("You are a supervisor deciding which agent to run next.\n\n");
+ sb.append("Available agents:\n");
+ for (BaseAgent agent : context.availableAgents()) {
+ sb.append("- ").append(agent.name()).append(": ").append(agent.description()).append("\n");
+ }
+ sb.append("\nCurrent state keys: ").append(context.state().keySet()).append("\n");
+
+ List events = context.events();
+ if (!events.isEmpty()) {
+ sb.append("\nRecent events:\n");
+ int start = Math.max(0, events.size() - 5);
+ for (int i = start; i < events.size(); i++) {
+ Event event = events.get(i);
+ sb.append("- ")
+ .append(event.author())
+ .append(": ")
+ .append(event.stringifyContent())
+ .append("\n");
+ }
+ }
+
+ context
+ .userContent()
+ .ifPresent(
+ content -> sb.append("\nOriginal user request: ").append(content.text()).append("\n"));
+
+ sb.append(
+ "\nRespond with exactly one of:\n"
+ + "- The name of the agent to run next\n"
+ + "- Multiple agent names separated by commas (to run in parallel)\n"
+ + "- DONE (if the task is complete)\n"
+ + "- DONE: (if complete with a summary)\n"
+ + "\nRespond with only the agent name(s) or DONE, nothing else.");
+ return sb.toString();
+ }
+
+ private String extractText(LlmResponse response) {
+ return response.content().flatMap(Content::parts).stream()
+ .flatMap(List::stream)
+ .flatMap(part -> part.text().stream())
+ .collect(Collectors.joining())
+ .trim();
+ }
+
+ private PlannerAction parseResponse(String text, PlanningContext context) {
+ if (text.isEmpty()) {
+ return new PlannerAction.Done();
+ }
+
+ String upper = text.toUpperCase().trim();
+ if (upper.equals("DONE")) {
+ return new PlannerAction.Done();
+ }
+ if (upper.startsWith("DONE:")) {
+ String summary = text.substring(text.indexOf(':') + 1).trim();
+ return new PlannerAction.DoneWithResult(summary);
+ }
+
+ // Try to parse as agent name(s)
+ String[] parts = text.split(",");
+ ImmutableList.Builder agentsBuilder = ImmutableList.builder();
+ for (String part : parts) {
+ String agentName = part.trim();
+ try {
+ agentsBuilder.add(context.findAgent(agentName));
+ } catch (IllegalArgumentException e) {
+ logger.warn("LLM returned unknown agent name '{}', treating as Done", agentName);
+ return new PlannerAction.Done();
+ }
+ }
+ ImmutableList agents = agentsBuilder.build();
+ if (agents.isEmpty()) {
+ return new PlannerAction.Done();
+ }
+ return new PlannerAction.RunAgents(agents);
+ }
+}
diff --git a/contrib/planners/src/main/java/com/google/adk/planner/goap/AgentMetadata.java b/contrib/planners/src/main/java/com/google/adk/planner/goap/AgentMetadata.java
new file mode 100644
index 000000000..5280a35aa
--- /dev/null
+++ b/contrib/planners/src/main/java/com/google/adk/planner/goap/AgentMetadata.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.adk.planner.goap;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Declares what state keys an agent reads (inputs) and writes (output).
+ *
+ * Used by {@link GoalOrientedPlanner} and {@link com.google.adk.planner.p2p.P2PPlanner} for
+ * dependency resolution.
+ *
+ * @param agentName the name of the agent (must match {@link
+ * com.google.adk.agents.BaseAgent#name()})
+ * @param inputKeys the state keys this agent reads as inputs
+ * @param outputKey the state key this agent produces as output
+ */
+public record AgentMetadata(String agentName, ImmutableList inputKeys, String outputKey) {}
diff --git a/contrib/planners/src/main/java/com/google/adk/planner/goap/DependencyGraphSearch.java b/contrib/planners/src/main/java/com/google/adk/planner/goap/DependencyGraphSearch.java
new file mode 100644
index 000000000..2f8caf5a3
--- /dev/null
+++ b/contrib/planners/src/main/java/com/google/adk/planner/goap/DependencyGraphSearch.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.adk.planner.goap;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * Performs a topological search on the dependency graph to find the ordered list of agents that
+ * must execute to produce a goal output, given a set of initial preconditions (state keys already
+ * available).
+ *
+ * The search works backward from the goal: for each unsatisfied dependency, it finds the agent
+ * that produces it and recursively resolves that agent's dependencies. Uses recursive DFS to ensure
+ * correct topological ordering.
+ */
+public final class DependencyGraphSearch {
+
+ private DependencyGraphSearch() {}
+
+ /**
+ * Finds the ordered list of agent names that must execute to produce the goal.
+ *
+ * @param graph the dependency graph built from agent metadata
+ * @param preconditions state keys already available (no agent needed to produce them)
+ * @param goal the target output key to produce
+ * @return ordered list of agent names, from first to execute to last
+ * @throws IllegalStateException if a dependency cannot be resolved or a cycle is detected
+ */
+ public static ImmutableList search(
+ GoalOrientedSearchGraph graph, Collection preconditions, String goal) {
+
+ Set satisfied = new HashSet<>(preconditions);
+ LinkedHashSet executionOrder = new LinkedHashSet<>();
+ Set visiting = new HashSet<>();
+
+ resolve(graph, goal, satisfied, visiting, executionOrder);
+
+ return ImmutableList.copyOf(executionOrder);
+ }
+
+ private static void resolve(
+ GoalOrientedSearchGraph graph,
+ String outputKey,
+ Set satisfied,
+ Set visiting,
+ LinkedHashSet executionOrder) {
+
+ if (satisfied.contains(outputKey)) {
+ return;
+ }
+
+ if (!graph.contains(outputKey)) {
+ throw new IllegalStateException(
+ "Cannot resolve dependency '"
+ + outputKey
+ + "': no agent produces this output key. "
+ + "Check that all required AgentMetadata entries are provided.");
+ }
+
+ if (!visiting.add(outputKey)) {
+ throw new IllegalStateException(
+ "Circular dependency detected involving output key: " + outputKey);
+ }
+
+ // Recursively resolve all dependencies first
+ for (String dep : graph.getDependencies(outputKey)) {
+ resolve(graph, dep, satisfied, visiting, executionOrder);
+ }
+
+ // All dependencies are now satisfied; add this agent
+ String agentName = graph.getProducerAgent(outputKey);
+ if (agentName != null) {
+ executionOrder.add(agentName);
+ }
+ satisfied.add(outputKey);
+ visiting.remove(outputKey);
+ }
+}
diff --git a/contrib/planners/src/main/java/com/google/adk/planner/goap/GoalOrientedPlanner.java b/contrib/planners/src/main/java/com/google/adk/planner/goap/GoalOrientedPlanner.java
new file mode 100644
index 000000000..acf7e733c
--- /dev/null
+++ b/contrib/planners/src/main/java/com/google/adk/planner/goap/GoalOrientedPlanner.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.adk.planner.goap;
+
+import com.google.adk.agents.BaseAgent;
+import com.google.adk.agents.Planner;
+import com.google.adk.agents.PlannerAction;
+import com.google.adk.agents.PlanningContext;
+import com.google.common.collect.ImmutableList;
+import io.reactivex.rxjava3.core.Single;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A planner that resolves agent execution order based on input/output dependencies and a target
+ * goal (output key).
+ *
+ * Given agent metadata declaring what each agent reads (inputKeys) and writes (outputKey), this
+ * planner uses backward-chaining dependency resolution to compute the execution path from initial
+ * preconditions to the goal.
+ *
+ *
Example:
+ *
+ *
+ * Agent A: inputs=[], output="person"
+ * Agent B: inputs=[], output="sign"
+ * Agent C: inputs=["person", "sign"], output="horoscope"
+ * Agent D: inputs=["person", "horoscope"], output="writeup"
+ * Goal: "writeup"
+ *
+ * Resolved path: A → B → C → D
+ *
+ */
+public final class GoalOrientedPlanner implements Planner {
+
+ private static final Logger logger = LoggerFactory.getLogger(GoalOrientedPlanner.class);
+
+ private final String goal;
+ private final List metadata;
+ private ImmutableList executionPath;
+ private final AtomicInteger cursor = new AtomicInteger(0);
+
+ public GoalOrientedPlanner(String goal, List metadata) {
+ this.goal = goal;
+ this.metadata = metadata;
+ }
+
+ @Override
+ public void init(PlanningContext context) {
+ GoalOrientedSearchGraph graph = new GoalOrientedSearchGraph(metadata);
+ ImmutableList agentOrder =
+ DependencyGraphSearch.search(graph, context.state().keySet(), goal);
+
+ logger.info("GoalOrientedPlanner resolved execution order: {}", agentOrder);
+
+ executionPath =
+ agentOrder.stream().map(context::findAgent).collect(ImmutableList.toImmutableList());
+ cursor.set(0);
+ }
+
+ @Override
+ public Single firstAction(PlanningContext context) {
+ cursor.set(0);
+ return selectNext();
+ }
+
+ @Override
+ public Single nextAction(PlanningContext context) {
+ return selectNext();
+ }
+
+ private Single selectNext() {
+ int idx = cursor.getAndIncrement();
+ if (executionPath == null || idx >= executionPath.size()) {
+ return Single.just(new PlannerAction.Done());
+ }
+ return Single.just(new PlannerAction.RunAgents(executionPath.get(idx)));
+ }
+}
diff --git a/contrib/planners/src/main/java/com/google/adk/planner/goap/GoalOrientedSearchGraph.java b/contrib/planners/src/main/java/com/google/adk/planner/goap/GoalOrientedSearchGraph.java
new file mode 100644
index 000000000..95a217d59
--- /dev/null
+++ b/contrib/planners/src/main/java/com/google/adk/planner/goap/GoalOrientedSearchGraph.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.adk.planner.goap;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.util.List;
+
+/**
+ * Transforms {@link AgentMetadata} into a dependency graph where:
+ *
+ *
+ * - Each output key maps to the agent that produces it
+ *
- Each output key maps to the input keys (dependencies) required to produce it
+ *
+ *
+ * Used by {@link DependencyGraphSearch} for A* search.
+ */
+public final class GoalOrientedSearchGraph {
+
+ private final ImmutableMap outputKeyToAgent;
+ private final ImmutableMap> outputKeyToDependencies;
+
+ public GoalOrientedSearchGraph(List metadata) {
+ ImmutableMap.Builder agentMap = ImmutableMap.builder();
+ ImmutableMap.Builder> depMap = ImmutableMap.builder();
+
+ for (AgentMetadata m : metadata) {
+ agentMap.put(m.outputKey(), m.agentName());
+ depMap.put(m.outputKey(), m.inputKeys());
+ }
+
+ this.outputKeyToAgent = agentMap.buildOrThrow();
+ this.outputKeyToDependencies = depMap.buildOrThrow();
+ }
+
+ /** Returns the input keys (dependencies) needed to produce the given output key. */
+ public ImmutableList getDependencies(String outputKey) {
+ ImmutableList deps = outputKeyToDependencies.get(outputKey);
+ if (deps == null) {
+ return ImmutableList.of();
+ }
+ return deps;
+ }
+
+ /** Returns the agent name that produces the given output key. */
+ public String getProducerAgent(String outputKey) {
+ return outputKeyToAgent.get(outputKey);
+ }
+
+ /** Returns true if the given output key is known in this graph. */
+ public boolean contains(String outputKey) {
+ return outputKeyToAgent.containsKey(outputKey);
+ }
+}
diff --git a/contrib/planners/src/main/java/com/google/adk/planner/p2p/AgentActivator.java b/contrib/planners/src/main/java/com/google/adk/planner/p2p/AgentActivator.java
new file mode 100644
index 000000000..aae9e3e68
--- /dev/null
+++ b/contrib/planners/src/main/java/com/google/adk/planner/p2p/AgentActivator.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.adk.planner.p2p;
+
+import com.google.adk.planner.goap.AgentMetadata;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * Tracks activation state for a single agent in P2P planning.
+ *
+ * An agent can activate when: it is not currently executing, it is marked as should-execute, and
+ * all its input keys are present in the session state.
+ */
+final class AgentActivator {
+
+ private final AgentMetadata metadata;
+ private boolean executing = false;
+ private boolean shouldExecute = true;
+
+ AgentActivator(AgentMetadata metadata) {
+ this.metadata = metadata;
+ }
+
+ /** Returns the agent name this activator manages. */
+ String agentName() {
+ return metadata.agentName();
+ }
+
+ /** Returns true if the agent can be activated given the current state. */
+ boolean canActivate(ConcurrentMap state) {
+ return !executing
+ && shouldExecute
+ && metadata.inputKeys().stream().allMatch(state::containsKey);
+ }
+
+ /** Marks the agent as currently executing. */
+ void startExecution() {
+ executing = true;
+ shouldExecute = false;
+ }
+
+ /** Marks the agent as finished executing. */
+ void finishExecution() {
+ executing = false;
+ }
+
+ /**
+ * Called when another agent produces output. If the produced key is one of this agent's inputs,
+ * marks this agent for re-execution.
+ */
+ void onStateChanged(String producedKey) {
+ if (metadata.inputKeys().contains(producedKey)) {
+ shouldExecute = true;
+ }
+ }
+}
diff --git a/contrib/planners/src/main/java/com/google/adk/planner/p2p/P2PPlanner.java b/contrib/planners/src/main/java/com/google/adk/planner/p2p/P2PPlanner.java
new file mode 100644
index 000000000..735286026
--- /dev/null
+++ b/contrib/planners/src/main/java/com/google/adk/planner/p2p/P2PPlanner.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.adk.planner.p2p;
+
+import com.google.adk.agents.BaseAgent;
+import com.google.adk.agents.Planner;
+import com.google.adk.agents.PlannerAction;
+import com.google.adk.agents.PlanningContext;
+import com.google.adk.planner.goap.AgentMetadata;
+import com.google.common.collect.ImmutableList;
+import io.reactivex.rxjava3.core.Single;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiPredicate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A peer-to-peer planner where agents activate dynamically as their input dependencies become
+ * available in session state.
+ *
+ * Key behaviors:
+ *
+ *
+ * - Multiple agents can activate in parallel when their inputs are satisfied
+ *
- When an agent produces output, other agents whose inputs are now satisfied activate
+ *
- Agents can re-execute when their inputs change (iterative refinement)
+ *
- Terminates on maxInvocations or a custom exit condition
+ *
+ *
+ * Example: Research collaboration where a critic's feedback causes hypothesis refinement:
+ *
+ *
+ * LiteratureAgent (needs: topic) → researchFindings
+ * HypothesisAgent (needs: topic, researchFindings) → hypothesis
+ * CriticAgent (needs: topic, hypothesis) → critique
+ * ScorerAgent (needs: topic, hypothesis, critique) → score
+ * Exit when: score >= 0.85
+ *
+ */
+public final class P2PPlanner implements Planner {
+
+ private static final Logger logger = LoggerFactory.getLogger(P2PPlanner.class);
+
+ private final List metadata;
+ private final int maxInvocations;
+ private final BiPredicate, Integer> exitCondition;
+ private Map activators;
+ private final AtomicInteger invocationCount = new AtomicInteger(0);
+
+ /**
+ * Creates a P2P planner with a custom exit condition.
+ *
+ * @param metadata agent input/output declarations
+ * @param maxInvocations maximum total agent invocations before termination
+ * @param exitCondition predicate tested on (state, invocationCount); returns true to stop
+ */
+ public P2PPlanner(
+ List metadata,
+ int maxInvocations,
+ BiPredicate, Integer> exitCondition) {
+ this.metadata = metadata;
+ this.maxInvocations = maxInvocations;
+ this.exitCondition = exitCondition;
+ }
+
+ /** Creates a P2P planner that exits only on maxInvocations. */
+ public P2PPlanner(List metadata, int maxInvocations) {
+ this(metadata, maxInvocations, (state, count) -> false);
+ }
+
+ @Override
+ public void init(PlanningContext context) {
+ activators = new LinkedHashMap<>();
+ for (AgentMetadata m : metadata) {
+ activators.put(m.agentName(), new AgentActivator(m));
+ }
+ invocationCount.set(0);
+ }
+
+ @Override
+ public Single firstAction(PlanningContext context) {
+ return findReadyAgents(context);
+ }
+
+ @Override
+ public Single nextAction(PlanningContext context) {
+ int count = invocationCount.get();
+
+ // Check exit condition
+ if (exitCondition.test(context.state(), count)) {
+ logger.info("P2PPlanner exit condition met at invocation {}", count);
+ return Single.just(new PlannerAction.Done());
+ }
+
+ // Mark previously executing agents as finished and notify state changes
+ for (AgentActivator activator : activators.values()) {
+ activator.finishExecution();
+ }
+
+ // Notify all activators about state changes from recently produced keys
+ for (AgentMetadata m : metadata) {
+ if (context.state().containsKey(m.outputKey())) {
+ for (AgentActivator activator : activators.values()) {
+ activator.onStateChanged(m.outputKey());
+ }
+ }
+ }
+
+ return findReadyAgents(context);
+ }
+
+ private Single findReadyAgents(PlanningContext context) {
+ if (invocationCount.get() >= maxInvocations) {
+ logger.info("P2PPlanner reached maxInvocations={}", maxInvocations);
+ return Single.just(new PlannerAction.Done());
+ }
+
+ ImmutableList.Builder readyAgents = ImmutableList.builder();
+ for (AgentActivator activator : activators.values()) {
+ if (activator.canActivate(context.state())) {
+ readyAgents.add(context.findAgent(activator.agentName()));
+ activator.startExecution();
+ invocationCount.incrementAndGet();
+ }
+ }
+
+ ImmutableList agents = readyAgents.build();
+ if (agents.isEmpty()) {
+ logger.info("P2PPlanner: no agents can activate, done");
+ return Single.just(new PlannerAction.Done());
+ }
+
+ logger.info(
+ "P2PPlanner activating {} agent(s): {}",
+ agents.size(),
+ agents.stream().map(BaseAgent::name).toList());
+ return Single.just(new PlannerAction.RunAgents(agents));
+ }
+}
diff --git a/contrib/planners/src/test/java/com/google/adk/planner/SupervisorPlannerTest.java b/contrib/planners/src/test/java/com/google/adk/planner/SupervisorPlannerTest.java
new file mode 100644
index 000000000..9c65277f6
--- /dev/null
+++ b/contrib/planners/src/test/java/com/google/adk/planner/SupervisorPlannerTest.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.adk.planner;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.adk.agents.BaseAgent;
+import com.google.adk.agents.InvocationContext;
+import com.google.adk.agents.PlannerAction;
+import com.google.adk.agents.PlanningContext;
+import com.google.adk.events.Event;
+import com.google.adk.models.BaseLlm;
+import com.google.adk.models.LlmResponse;
+import com.google.adk.sessions.InMemorySessionService;
+import com.google.adk.sessions.Session;
+import com.google.common.collect.ImmutableList;
+import com.google.genai.types.Content;
+import com.google.genai.types.Part;
+import io.reactivex.rxjava3.core.Flowable;
+import java.util.concurrent.ConcurrentHashMap;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link SupervisorPlanner}. */
+class SupervisorPlannerTest {
+
+ private static final class SimpleTestAgent extends BaseAgent {
+ SimpleTestAgent(String name) {
+ super(name, "test agent " + name, ImmutableList.of(), null, null);
+ }
+
+ @Override
+ protected Flowable runAsyncImpl(InvocationContext ctx) {
+ return Flowable.empty();
+ }
+
+ @Override
+ protected Flowable runLiveImpl(InvocationContext ctx) {
+ return Flowable.empty();
+ }
+ }
+
+ @Test
+ void firstAction_parsesAgentNameFromLlm() {
+ BaseLlm mockLlm = mock(BaseLlm.class);
+ LlmResponse response = createTextResponse("agentA");
+ when(mockLlm.generateContent(any(), eq(false))).thenReturn(Flowable.just(response));
+
+ SimpleTestAgent agentA = new SimpleTestAgent("agentA");
+ SimpleTestAgent agentB = new SimpleTestAgent("agentB");
+
+ SupervisorPlanner planner = new SupervisorPlanner(mockLlm, "You are a supervisor.");
+ PlanningContext context =
+ createPlanningContext(ImmutableList.of(agentA, agentB), new ConcurrentHashMap<>());
+
+ PlannerAction action = planner.firstAction(context).blockingGet();
+ assertThat(action).isInstanceOf(PlannerAction.RunAgents.class);
+ PlannerAction.RunAgents runAgents = (PlannerAction.RunAgents) action;
+ assertThat(runAgents.agents()).hasSize(1);
+ assertThat(runAgents.agents().get(0).name()).isEqualTo("agentA");
+ }
+
+ @Test
+ void firstAction_parsesDoneFromLlm() {
+ BaseLlm mockLlm = mock(BaseLlm.class);
+ LlmResponse response = createTextResponse("DONE");
+ when(mockLlm.generateContent(any(), eq(false))).thenReturn(Flowable.just(response));
+
+ SimpleTestAgent agentA = new SimpleTestAgent("agentA");
+
+ SupervisorPlanner planner = new SupervisorPlanner(mockLlm);
+ PlanningContext context =
+ createPlanningContext(ImmutableList.of(agentA), new ConcurrentHashMap<>());
+
+ PlannerAction action = planner.firstAction(context).blockingGet();
+ assertThat(action).isInstanceOf(PlannerAction.Done.class);
+ }
+
+ @Test
+ void firstAction_parsesDoneWithResultFromLlm() {
+ BaseLlm mockLlm = mock(BaseLlm.class);
+ LlmResponse response = createTextResponse("DONE: Task completed successfully");
+ when(mockLlm.generateContent(any(), eq(false))).thenReturn(Flowable.just(response));
+
+ SimpleTestAgent agentA = new SimpleTestAgent("agentA");
+
+ SupervisorPlanner planner = new SupervisorPlanner(mockLlm);
+ PlanningContext context =
+ createPlanningContext(ImmutableList.of(agentA), new ConcurrentHashMap<>());
+
+ PlannerAction action = planner.firstAction(context).blockingGet();
+ assertThat(action).isInstanceOf(PlannerAction.DoneWithResult.class);
+ assertThat(((PlannerAction.DoneWithResult) action).result())
+ .isEqualTo("Task completed successfully");
+ }
+
+ @Test
+ void nextAction_fallsToDoneOnUnrecognizedAgent() {
+ BaseLlm mockLlm = mock(BaseLlm.class);
+ LlmResponse response = createTextResponse("unknownAgent");
+ when(mockLlm.generateContent(any(), eq(false))).thenReturn(Flowable.just(response));
+
+ SimpleTestAgent agentA = new SimpleTestAgent("agentA");
+
+ SupervisorPlanner planner = new SupervisorPlanner(mockLlm);
+ PlanningContext context =
+ createPlanningContext(ImmutableList.of(agentA), new ConcurrentHashMap<>());
+
+ PlannerAction action = planner.nextAction(context).blockingGet();
+ assertThat(action).isInstanceOf(PlannerAction.Done.class);
+ }
+
+ @Test
+ void firstAction_fallsToDoneOnLlmError() {
+ BaseLlm mockLlm = mock(BaseLlm.class);
+ when(mockLlm.generateContent(any(), eq(false)))
+ .thenReturn(Flowable.error(new RuntimeException("LLM error")));
+
+ SimpleTestAgent agentA = new SimpleTestAgent("agentA");
+
+ SupervisorPlanner planner = new SupervisorPlanner(mockLlm);
+ PlanningContext context =
+ createPlanningContext(ImmutableList.of(agentA), new ConcurrentHashMap<>());
+
+ PlannerAction action = planner.firstAction(context).blockingGet();
+ assertThat(action).isInstanceOf(PlannerAction.Done.class);
+ }
+
+ private static LlmResponse createTextResponse(String text) {
+ return LlmResponse.builder()
+ .content(Content.builder().role("model").parts(Part.fromText(text)).build())
+ .build();
+ }
+
+ private static PlanningContext createPlanningContext(
+ ImmutableList agents, ConcurrentHashMap state) {
+ InMemorySessionService sessionService = new InMemorySessionService();
+ Session session = sessionService.createSession("test-app", "test-user").blockingGet();
+ session.state().putAll(state);
+
+ BaseAgent rootAgent = agents.isEmpty() ? new SimpleTestAgent("root") : agents.get(0);
+ InvocationContext invocationContext =
+ InvocationContext.builder()
+ .sessionService(sessionService)
+ .invocationId("test-invocation")
+ .agent(rootAgent)
+ .session(session)
+ .build();
+
+ return new PlanningContext(invocationContext, agents);
+ }
+}
diff --git a/contrib/planners/src/test/java/com/google/adk/planner/goap/GoalOrientedPlannerTest.java b/contrib/planners/src/test/java/com/google/adk/planner/goap/GoalOrientedPlannerTest.java
new file mode 100644
index 000000000..b6c0fcb16
--- /dev/null
+++ b/contrib/planners/src/test/java/com/google/adk/planner/goap/GoalOrientedPlannerTest.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.adk.planner.goap;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.google.adk.agents.BaseAgent;
+import com.google.adk.agents.InvocationContext;
+import com.google.adk.agents.PlannerAction;
+import com.google.adk.agents.PlanningContext;
+import com.google.adk.events.Event;
+import com.google.common.collect.ImmutableList;
+import io.reactivex.rxjava3.core.Flowable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link GoalOrientedPlanner}, {@link DependencyGraphSearch}, and related classes.
+ */
+class GoalOrientedPlannerTest {
+
+ /** Simple test agent for planner tests. */
+ private static final class SimpleTestAgent extends BaseAgent {
+ SimpleTestAgent(String name) {
+ super(name, "test agent " + name, ImmutableList.of(), null, null);
+ }
+
+ @Override
+ protected Flowable runAsyncImpl(InvocationContext ctx) {
+ return Flowable.empty();
+ }
+
+ @Override
+ protected Flowable runLiveImpl(InvocationContext ctx) {
+ return Flowable.empty();
+ }
+ }
+
+ @Test
+ void dependencyGraphSearch_resolvesLinearChain() {
+ // A → B → C (linear chain)
+ List metadata =
+ List.of(
+ new AgentMetadata("agentA", ImmutableList.of(), "outputA"),
+ new AgentMetadata("agentB", ImmutableList.of("outputA"), "outputB"),
+ new AgentMetadata("agentC", ImmutableList.of("outputB"), "outputC"));
+
+ GoalOrientedSearchGraph graph = new GoalOrientedSearchGraph(metadata);
+ ImmutableList path = DependencyGraphSearch.search(graph, List.of(), "outputC");
+
+ assertThat(path).containsExactly("agentA", "agentB", "agentC").inOrder();
+ }
+
+ @Test
+ void dependencyGraphSearch_resolvesMultipleInputs() {
+ // A and B have no deps; C needs both A and B outputs
+ List metadata =
+ List.of(
+ new AgentMetadata("agentA", ImmutableList.of(), "outputA"),
+ new AgentMetadata("agentB", ImmutableList.of(), "outputB"),
+ new AgentMetadata("agentC", ImmutableList.of("outputA", "outputB"), "outputC"));
+
+ GoalOrientedSearchGraph graph = new GoalOrientedSearchGraph(metadata);
+ ImmutableList path = DependencyGraphSearch.search(graph, List.of(), "outputC");
+
+ assertThat(path).containsExactly("agentA", "agentB", "agentC");
+ // C must come after both A and B
+ assertThat(path.indexOf("agentC")).isGreaterThan(path.indexOf("agentA"));
+ assertThat(path.indexOf("agentC")).isGreaterThan(path.indexOf("agentB"));
+ }
+
+ @Test
+ void dependencyGraphSearch_handlesDiamondDependencies() {
+ // A → B, A → C, B+C → D (diamond)
+ List metadata =
+ List.of(
+ new AgentMetadata("agentA", ImmutableList.of(), "outputA"),
+ new AgentMetadata("agentB", ImmutableList.of("outputA"), "outputB"),
+ new AgentMetadata("agentC", ImmutableList.of("outputA"), "outputC"),
+ new AgentMetadata("agentD", ImmutableList.of("outputB", "outputC"), "outputD"));
+
+ GoalOrientedSearchGraph graph = new GoalOrientedSearchGraph(metadata);
+ ImmutableList path = DependencyGraphSearch.search(graph, List.of(), "outputD");
+
+ assertThat(path).containsExactly("agentA", "agentB", "agentC", "agentD");
+ assertThat(path.indexOf("agentA")).isLessThan(path.indexOf("agentB"));
+ assertThat(path.indexOf("agentA")).isLessThan(path.indexOf("agentC"));
+ assertThat(path.indexOf("agentD")).isGreaterThan(path.indexOf("agentB"));
+ assertThat(path.indexOf("agentD")).isGreaterThan(path.indexOf("agentC"));
+ }
+
+ @Test
+ void dependencyGraphSearch_skipsSatisfiedPreconditions() {
+ // A and B produce outputs; C needs both.
+ // But outputA is already available as a precondition.
+ List metadata =
+ List.of(
+ new AgentMetadata("agentA", ImmutableList.of(), "outputA"),
+ new AgentMetadata("agentB", ImmutableList.of(), "outputB"),
+ new AgentMetadata("agentC", ImmutableList.of("outputA", "outputB"), "outputC"));
+
+ GoalOrientedSearchGraph graph = new GoalOrientedSearchGraph(metadata);
+ ImmutableList path = DependencyGraphSearch.search(graph, List.of("outputA"), "outputC");
+
+ // agentA should be skipped since outputA is already available
+ assertThat(path).containsExactly("agentB", "agentC").inOrder();
+ }
+
+ @Test
+ void dependencyGraphSearch_throwsOnUnresolvableDependency() {
+ // agentB needs "missing" which no agent produces
+ List metadata =
+ List.of(new AgentMetadata("agentB", ImmutableList.of("missing"), "outputB"));
+
+ GoalOrientedSearchGraph graph = new GoalOrientedSearchGraph(metadata);
+
+ assertThrows(
+ IllegalStateException.class,
+ () -> DependencyGraphSearch.search(graph, List.of(), "outputB"));
+ }
+
+ @Test
+ void goalOrientedPlanner_producesCorrectExecutionPath() {
+ // Horoscope example: person + sign → horoscope → writeup
+ SimpleTestAgent personExtractor = new SimpleTestAgent("personExtractor");
+ SimpleTestAgent signExtractor = new SimpleTestAgent("signExtractor");
+ SimpleTestAgent horoscopeGenerator = new SimpleTestAgent("horoscopeGenerator");
+ SimpleTestAgent writer = new SimpleTestAgent("writer");
+
+ List metadata =
+ List.of(
+ new AgentMetadata("personExtractor", ImmutableList.of("prompt"), "person"),
+ new AgentMetadata("signExtractor", ImmutableList.of("prompt"), "sign"),
+ new AgentMetadata(
+ "horoscopeGenerator", ImmutableList.of("person", "sign"), "horoscope"),
+ new AgentMetadata("writer", ImmutableList.of("person", "horoscope"), "writeup"));
+
+ GoalOrientedPlanner planner = new GoalOrientedPlanner("writeup", metadata);
+ ImmutableList agents =
+ ImmutableList.of(personExtractor, signExtractor, horoscopeGenerator, writer);
+
+ ConcurrentHashMap state = new ConcurrentHashMap<>();
+ state.put("prompt", "My name is Mario and my zodiac sign is pisces");
+
+ PlanningContext context = createPlanningContext(agents, state);
+ planner.init(context);
+
+ // Collect the full execution path
+ List executionOrder = new ArrayList<>();
+ PlannerAction action = planner.firstAction(context).blockingGet();
+ while (action instanceof PlannerAction.RunAgents runAgents) {
+ for (BaseAgent agent : runAgents.agents()) {
+ executionOrder.add(agent.name());
+ }
+ action = planner.nextAction(context).blockingGet();
+ }
+
+ assertThat(executionOrder)
+ .containsExactly("personExtractor", "signExtractor", "horoscopeGenerator", "writer");
+ // horoscopeGenerator must come after both extractors
+ assertThat(executionOrder.indexOf("horoscopeGenerator"))
+ .isGreaterThan(executionOrder.indexOf("personExtractor"));
+ assertThat(executionOrder.indexOf("horoscopeGenerator"))
+ .isGreaterThan(executionOrder.indexOf("signExtractor"));
+ // writer must come after horoscopeGenerator
+ assertThat(executionOrder.indexOf("writer"))
+ .isGreaterThan(executionOrder.indexOf("horoscopeGenerator"));
+ }
+
+ @Test
+ void goalOrientedPlanner_returnsDoneWhenPathComplete() {
+ SimpleTestAgent agentA = new SimpleTestAgent("agentA");
+
+ List metadata =
+ List.of(new AgentMetadata("agentA", ImmutableList.of(), "outputA"));
+
+ GoalOrientedPlanner planner = new GoalOrientedPlanner("outputA", metadata);
+ PlanningContext context =
+ createPlanningContext(ImmutableList.of(agentA), new ConcurrentHashMap<>());
+ planner.init(context);
+
+ PlannerAction first = planner.firstAction(context).blockingGet();
+ assertThat(first).isInstanceOf(PlannerAction.RunAgents.class);
+
+ PlannerAction second = planner.nextAction(context).blockingGet();
+ assertThat(second).isInstanceOf(PlannerAction.Done.class);
+ }
+
+ private static PlanningContext createPlanningContext(
+ ImmutableList agents, ConcurrentHashMap state) {
+ // Create a minimal InvocationContext for testing
+ com.google.adk.sessions.InMemorySessionService sessionService =
+ new com.google.adk.sessions.InMemorySessionService();
+ com.google.adk.sessions.Session session =
+ sessionService.createSession("test-app", "test-user").blockingGet();
+ session.state().putAll(state);
+
+ BaseAgent rootAgent = agents.isEmpty() ? new SimpleTestAgent("root") : agents.get(0);
+ InvocationContext invocationContext =
+ InvocationContext.builder()
+ .sessionService(sessionService)
+ .invocationId("test-invocation")
+ .agent(rootAgent)
+ .session(session)
+ .build();
+
+ return new PlanningContext(invocationContext, agents);
+ }
+}
diff --git a/contrib/planners/src/test/java/com/google/adk/planner/p2p/P2PPlannerTest.java b/contrib/planners/src/test/java/com/google/adk/planner/p2p/P2PPlannerTest.java
new file mode 100644
index 000000000..8640f5dbc
--- /dev/null
+++ b/contrib/planners/src/test/java/com/google/adk/planner/p2p/P2PPlannerTest.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.adk.planner.p2p;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.adk.agents.BaseAgent;
+import com.google.adk.agents.InvocationContext;
+import com.google.adk.agents.PlannerAction;
+import com.google.adk.agents.PlanningContext;
+import com.google.adk.events.Event;
+import com.google.adk.planner.goap.AgentMetadata;
+import com.google.adk.sessions.InMemorySessionService;
+import com.google.adk.sessions.Session;
+import com.google.common.collect.ImmutableList;
+import io.reactivex.rxjava3.core.Flowable;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link P2PPlanner}. */
+class P2PPlannerTest {
+
+ private static final class SimpleTestAgent extends BaseAgent {
+ SimpleTestAgent(String name) {
+ super(name, "test agent " + name, ImmutableList.of(), null, null);
+ }
+
+ @Override
+ protected Flowable runAsyncImpl(InvocationContext ctx) {
+ return Flowable.empty();
+ }
+
+ @Override
+ protected Flowable runLiveImpl(InvocationContext ctx) {
+ return Flowable.empty();
+ }
+ }
+
+ @Test
+ void firstAction_activatesAgentsWithSatisfiedInputs() {
+ SimpleTestAgent agentA = new SimpleTestAgent("agentA");
+
+ List metadata =
+ List.of(new AgentMetadata("agentA", ImmutableList.of("topic"), "findings"));
+
+ ConcurrentHashMap state = new ConcurrentHashMap<>();
+ state.put("topic", "quantum computing");
+
+ P2PPlanner planner = new P2PPlanner(metadata, 10);
+ PlanningContext context = createPlanningContext(ImmutableList.of(agentA), state);
+ planner.init(context);
+
+ PlannerAction action = planner.firstAction(context).blockingGet();
+ assertThat(action).isInstanceOf(PlannerAction.RunAgents.class);
+ PlannerAction.RunAgents runAgents = (PlannerAction.RunAgents) action;
+ assertThat(runAgents.agents()).hasSize(1);
+ assertThat(runAgents.agents().get(0).name()).isEqualTo("agentA");
+ }
+
+ @Test
+ void firstAction_activatesMultipleAgentsInParallel() {
+ SimpleTestAgent agentA = new SimpleTestAgent("agentA");
+ SimpleTestAgent agentB = new SimpleTestAgent("agentB");
+
+ List metadata =
+ List.of(
+ new AgentMetadata("agentA", ImmutableList.of("topic"), "findingsA"),
+ new AgentMetadata("agentB", ImmutableList.of("topic"), "findingsB"));
+
+ ConcurrentHashMap state = new ConcurrentHashMap<>();
+ state.put("topic", "quantum computing");
+
+ P2PPlanner planner = new P2PPlanner(metadata, 10);
+ PlanningContext context = createPlanningContext(ImmutableList.of(agentA, agentB), state);
+ planner.init(context);
+
+ PlannerAction action = planner.firstAction(context).blockingGet();
+ assertThat(action).isInstanceOf(PlannerAction.RunAgents.class);
+ PlannerAction.RunAgents runAgents = (PlannerAction.RunAgents) action;
+ assertThat(runAgents.agents()).hasSize(2);
+ }
+
+ @Test
+ void firstAction_doesNotActivateAgentsWithUnsatisfiedInputs() {
+ SimpleTestAgent agentA = new SimpleTestAgent("agentA");
+ SimpleTestAgent agentB = new SimpleTestAgent("agentB");
+
+ List metadata =
+ List.of(
+ new AgentMetadata("agentA", ImmutableList.of("topic"), "findings"),
+ new AgentMetadata("agentB", ImmutableList.of("topic", "findings"), "hypothesis"));
+
+ ConcurrentHashMap state = new ConcurrentHashMap<>();
+ state.put("topic", "quantum computing");
+ // "findings" is not yet available, so agentB should not activate
+
+ P2PPlanner planner = new P2PPlanner(metadata, 10);
+ PlanningContext context = createPlanningContext(ImmutableList.of(agentA, agentB), state);
+ planner.init(context);
+
+ PlannerAction action = planner.firstAction(context).blockingGet();
+ assertThat(action).isInstanceOf(PlannerAction.RunAgents.class);
+ PlannerAction.RunAgents runAgents = (PlannerAction.RunAgents) action;
+ assertThat(runAgents.agents()).hasSize(1);
+ assertThat(runAgents.agents().get(0).name()).isEqualTo("agentA");
+ }
+
+ @Test
+ void nextAction_activatesNewAgentsWhenInputsBecomeAvailable() {
+ SimpleTestAgent agentA = new SimpleTestAgent("agentA");
+ SimpleTestAgent agentB = new SimpleTestAgent("agentB");
+
+ List metadata =
+ List.of(
+ new AgentMetadata("agentA", ImmutableList.of("topic"), "findings"),
+ new AgentMetadata("agentB", ImmutableList.of("topic", "findings"), "hypothesis"));
+
+ ConcurrentHashMap state = new ConcurrentHashMap<>();
+ state.put("topic", "quantum computing");
+
+ P2PPlanner planner = new P2PPlanner(metadata, 10);
+ PlanningContext context = createPlanningContext(ImmutableList.of(agentA, agentB), state);
+ planner.init(context);
+
+ // First action: only agentA can activate
+ planner.firstAction(context).blockingGet();
+
+ // Simulate agentA producing "findings" — must update the session state directly
+ context.state().put("findings", "some research findings");
+
+ // Next action: agentB should now be able to activate
+ PlannerAction action = planner.nextAction(context).blockingGet();
+ assertThat(action).isInstanceOf(PlannerAction.RunAgents.class);
+ PlannerAction.RunAgents runAgents = (PlannerAction.RunAgents) action;
+ assertThat(runAgents.agents()).hasSize(1);
+ assertThat(runAgents.agents().get(0).name()).isEqualTo("agentB");
+ }
+
+ @Test
+ void nextAction_terminatesOnExitCondition() {
+ SimpleTestAgent agentA = new SimpleTestAgent("agentA");
+
+ List metadata =
+ List.of(new AgentMetadata("agentA", ImmutableList.of("topic"), "score"));
+
+ ConcurrentHashMap state = new ConcurrentHashMap<>();
+ state.put("topic", "quantum computing");
+
+ P2PPlanner planner =
+ new P2PPlanner(
+ metadata,
+ 10,
+ (s, count) -> {
+ Object score = s.get("score");
+ return score instanceof Number && ((Number) score).doubleValue() >= 0.85;
+ });
+
+ PlanningContext context = createPlanningContext(ImmutableList.of(agentA), state);
+ planner.init(context);
+
+ // First action: agentA activates
+ planner.firstAction(context).blockingGet();
+
+ // Simulate score being produced above threshold — must update session state directly
+ context.state().put("score", 0.9);
+
+ PlannerAction action = planner.nextAction(context).blockingGet();
+ assertThat(action).isInstanceOf(PlannerAction.Done.class);
+ }
+
+ @Test
+ void nextAction_terminatesOnMaxInvocations() {
+ SimpleTestAgent agentA = new SimpleTestAgent("agentA");
+
+ List metadata =
+ List.of(new AgentMetadata("agentA", ImmutableList.of("topic"), "output"));
+
+ ConcurrentHashMap state = new ConcurrentHashMap<>();
+ state.put("topic", "quantum computing");
+
+ P2PPlanner planner = new P2PPlanner(metadata, 1); // only 1 invocation allowed
+
+ PlanningContext context = createPlanningContext(ImmutableList.of(agentA), state);
+ planner.init(context);
+
+ // First action consumes the single allowed invocation
+ planner.firstAction(context).blockingGet();
+
+ // Next should be Done due to maxInvocations
+ PlannerAction action = planner.nextAction(context).blockingGet();
+ assertThat(action).isInstanceOf(PlannerAction.Done.class);
+ }
+
+ @Test
+ void nextAction_returnsDoneWhenNoAgentsCanActivate() {
+ SimpleTestAgent agentA = new SimpleTestAgent("agentA");
+
+ List metadata =
+ List.of(new AgentMetadata("agentA", ImmutableList.of("missingInput"), "output"));
+
+ ConcurrentHashMap state = new ConcurrentHashMap<>();
+
+ P2PPlanner planner = new P2PPlanner(metadata, 10);
+ PlanningContext context = createPlanningContext(ImmutableList.of(agentA), state);
+ planner.init(context);
+
+ PlannerAction action = planner.firstAction(context).blockingGet();
+ assertThat(action).isInstanceOf(PlannerAction.Done.class);
+ }
+
+ private static PlanningContext createPlanningContext(
+ ImmutableList agents, ConcurrentHashMap state) {
+ InMemorySessionService sessionService = new InMemorySessionService();
+ Session session = sessionService.createSession("test-app", "test-user").blockingGet();
+ session.state().putAll(state);
+
+ BaseAgent rootAgent = agents.isEmpty() ? new SimpleTestAgent("root") : agents.get(0);
+ InvocationContext invocationContext =
+ InvocationContext.builder()
+ .sessionService(sessionService)
+ .invocationId("test-invocation")
+ .agent(rootAgent)
+ .session(session)
+ .build();
+
+ return new PlanningContext(invocationContext, agents);
+ }
+}
diff --git a/contrib/spring-ai/README.md b/contrib/spring-ai/README.md
index c45f0e033..588b3d122 100644
--- a/contrib/spring-ai/README.md
+++ b/contrib/spring-ai/README.md
@@ -32,7 +32,7 @@ To use ADK Java with the Spring AI integration in your application, add the foll
org.springframework.ai
spring-ai-bom
- 1.1.0-M3
+ 1.1.1
pom
import
@@ -115,7 +115,7 @@ Add the Spring AI provider dependencies for the AI services you want to use:
17
- 1.1.0-M3
+ 1.1.1
0.3.1-SNAPSHOT
diff --git a/contrib/spring-ai/pom.xml b/contrib/spring-ai/pom.xml
index 64346b1c3..68c22f380 100644
--- a/contrib/spring-ai/pom.xml
+++ b/contrib/spring-ai/pom.xml
@@ -29,7 +29,7 @@
Spring AI integration for the Agent Development Kit.
- 1.1.0
+ 1.1.1
1.21.3
diff --git a/core/src/main/java/com/google/adk/agents/Planner.java b/core/src/main/java/com/google/adk/agents/Planner.java
new file mode 100644
index 000000000..cc6e741a2
--- /dev/null
+++ b/core/src/main/java/com/google/adk/agents/Planner.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.adk.agents;
+
+import io.reactivex.rxjava3.core.Single;
+
+/**
+ * Strategy interface for planning which sub-agent(s) to execute next.
+ *
+ * A {@code Planner} is used by {@link PlannerAgent} to dynamically determine execution order at
+ * runtime. The planning loop works as follows:
+ *
+ *
+ * - {@link #init} is called once before the loop starts
+ *
- {@link #firstAction} returns the first action to execute
+ *
- The selected agent(s) execute, producing events and updating session state
+ *
- {@link #nextAction} is called with updated context to decide what to do next
+ *
- Steps 3-4 repeat until {@link PlannerAction.Done} or max iterations
+ *
+ *
+ * Returns {@link Single}{@code } to support both synchronous planners (wrap in
+ * {@code Single.just()}) and asynchronous planners that call an LLM.
+ */
+public interface Planner {
+
+ /**
+ * Initialize the planner with context and available agents. Called once before the planning loop
+ * starts.
+ *
+ * Default implementation is a no-op. Override to perform setup like building dependency
+ * graphs.
+ */
+ default void init(PlanningContext context) {}
+
+ /** Select the first action to execute. */
+ Single firstAction(PlanningContext context);
+
+ /** Select the next action based on updated state and events. */
+ Single nextAction(PlanningContext context);
+}
diff --git a/core/src/main/java/com/google/adk/agents/PlannerAction.java b/core/src/main/java/com/google/adk/agents/PlannerAction.java
new file mode 100644
index 000000000..f05dfaf1e
--- /dev/null
+++ b/core/src/main/java/com/google/adk/agents/PlannerAction.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.adk.agents;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Represents the next action a {@link Planner} wants the {@link PlannerAgent} to take.
+ *
+ * This is a sealed interface with four variants:
+ *
+ *
+ * - {@link RunAgents} — execute one or more sub-agents (multiple agents run in parallel)
+ *
- {@link Done} — planning is complete, no result to emit
+ *
- {@link DoneWithResult} — planning is complete with a final text result
+ *
- {@link NoOp} — skip this iteration (no-op), then ask the planner for the next action
+ *
+ */
+public sealed interface PlannerAction
+ permits PlannerAction.RunAgents,
+ PlannerAction.Done,
+ PlannerAction.DoneWithResult,
+ PlannerAction.NoOp {
+
+ /** Run the specified sub-agent(s). Multiple agents are run in parallel. */
+ record RunAgents(ImmutableList agents) implements PlannerAction {
+ public RunAgents(BaseAgent singleAgent) {
+ this(ImmutableList.of(singleAgent));
+ }
+ }
+
+ /** Plan is complete, no result to emit. */
+ record Done() implements PlannerAction {}
+
+ /** Plan is complete with a final text result. */
+ record DoneWithResult(String result) implements PlannerAction {}
+
+ /** Skip this iteration (no-op). */
+ record NoOp() implements PlannerAction {}
+}
diff --git a/core/src/main/java/com/google/adk/agents/PlannerAgent.java b/core/src/main/java/com/google/adk/agents/PlannerAgent.java
new file mode 100644
index 000000000..936d619de
--- /dev/null
+++ b/core/src/main/java/com/google/adk/agents/PlannerAgent.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.adk.agents;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.adk.events.Event;
+import com.google.adk.events.EventActions;
+import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.genai.types.Content;
+import com.google.genai.types.Part;
+import io.reactivex.rxjava3.core.Flowable;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An agent that delegates execution planning to a {@link Planner} strategy.
+ *
+ * The {@code PlannerAgent} owns a set of sub-agents and a planner. At runtime, the planner
+ * inspects session state and decides which sub-agent(s) to run next. This enables dynamic,
+ * goal-oriented agent orchestration — the execution topology is determined at runtime rather than
+ * being fixed at build time.
+ *
+ *
The planning loop:
+ *
+ *
+ * - Planner is initialized with context and available agents
+ *
- Planner returns what to do next via {@link PlannerAction}
+ *
- Selected sub-agent(s) execute, producing events
+ *
- Session state (world state) is updated from events
+ *
- Planner sees updated state and decides the next action
+ *
- Repeat until {@link PlannerAction.Done} or maxIterations
+ *
+ *
+ * Example usage with a custom planner:
+ *
+ *
{@code
+ * PlannerAgent agent = PlannerAgent.builder()
+ * .name("myAgent")
+ * .subAgents(agentA, agentB, agentC)
+ * .planner(new GoalOrientedPlanner("finalOutput", metadata))
+ * .maxIterations(20)
+ * .build();
+ * }
+ */
+public class PlannerAgent extends BaseAgent {
+ private static final Logger logger = LoggerFactory.getLogger(PlannerAgent.class);
+ private static final int DEFAULT_MAX_ITERATIONS = 100;
+
+ private final Planner planner;
+ private final int maxIterations;
+
+ private PlannerAgent(
+ String name,
+ String description,
+ List extends BaseAgent> subAgents,
+ Planner planner,
+ int maxIterations,
+ List beforeAgentCallback,
+ List afterAgentCallback) {
+ super(name, description, subAgents, beforeAgentCallback, afterAgentCallback);
+ this.planner = planner;
+ this.maxIterations = maxIterations;
+ }
+
+ /** Returns the planner strategy used by this agent. */
+ public Planner planner() {
+ return planner;
+ }
+
+ /** Returns the maximum number of planning iterations. */
+ public int maxIterations() {
+ return maxIterations;
+ }
+
+ @Override
+ protected Flowable runAsyncImpl(InvocationContext invocationContext) {
+ List extends BaseAgent> agents = subAgents();
+ if (agents == null || agents.isEmpty()) {
+ return Flowable.empty();
+ }
+
+ ImmutableList available =
+ agents.stream().map(a -> (BaseAgent) a).collect(toImmutableList());
+ PlanningContext planningContext = new PlanningContext(invocationContext, available);
+
+ planner.init(planningContext);
+
+ AtomicInteger iteration = new AtomicInteger(0);
+
+ return planner
+ .firstAction(planningContext)
+ .flatMapPublisher(
+ firstAction ->
+ executeActionAndContinue(
+ firstAction, planningContext, invocationContext, iteration));
+ }
+
+ private Flowable executeActionAndContinue(
+ PlannerAction action,
+ PlanningContext planningContext,
+ InvocationContext invocationContext,
+ AtomicInteger iteration) {
+
+ int current = iteration.getAndIncrement();
+ if (current >= maxIterations) {
+ logger.info("PlannerAgent '{}' reached maxIterations={}", name(), maxIterations);
+ return Flowable.empty();
+ }
+
+ if (action instanceof PlannerAction.Done) {
+ return Flowable.empty();
+ }
+
+ if (action instanceof PlannerAction.DoneWithResult doneWithResult) {
+ Event resultEvent =
+ Event.builder()
+ .id(Event.generateEventId())
+ .invocationId(invocationContext.invocationId())
+ .author(name())
+ .branch(invocationContext.branch())
+ .content(Content.fromParts(Part.fromText(doneWithResult.result())))
+ .actions(EventActions.builder().build())
+ .build();
+ return Flowable.just(resultEvent);
+ }
+
+ if (action instanceof PlannerAction.NoOp) {
+ return Flowable.defer(
+ () ->
+ planner
+ .nextAction(planningContext)
+ .flatMapPublisher(
+ nextAction ->
+ executeActionAndContinue(
+ nextAction, planningContext, invocationContext, iteration)));
+ }
+
+ if (action instanceof PlannerAction.RunAgents runAgents) {
+ Flowable agentEvents;
+ if (runAgents.agents().size() == 1) {
+ agentEvents = runAgents.agents().get(0).runAsync(invocationContext);
+ } else {
+ agentEvents =
+ Flowable.merge(
+ runAgents.agents().stream()
+ .map(agent -> agent.runAsync(invocationContext))
+ .collect(toImmutableList()));
+ }
+
+ return agentEvents.concatWith(
+ Flowable.defer(
+ () ->
+ planner
+ .nextAction(planningContext)
+ .flatMapPublisher(
+ nextAction ->
+ executeActionAndContinue(
+ nextAction, planningContext, invocationContext, iteration))));
+ }
+
+ // Unreachable for sealed interface, but required by compiler
+ return Flowable.empty();
+ }
+
+ @Override
+ protected Flowable runLiveImpl(InvocationContext invocationContext) {
+ return Flowable.error(
+ new UnsupportedOperationException("runLive is not defined for PlannerAgent yet."));
+ }
+
+ /** Returns a new {@link Builder} for creating {@link PlannerAgent} instances. */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Builder for {@link PlannerAgent}. */
+ public static class Builder extends BaseAgent.Builder {
+ private Planner planner;
+ private int maxIterations = DEFAULT_MAX_ITERATIONS;
+
+ @CanIgnoreReturnValue
+ public Builder planner(Planner planner) {
+ this.planner = planner;
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder maxIterations(int maxIterations) {
+ this.maxIterations = maxIterations;
+ return this;
+ }
+
+ @Override
+ public PlannerAgent build() {
+ if (planner == null) {
+ throw new IllegalStateException(
+ "PlannerAgent requires a Planner. Call .planner(...) on the builder.");
+ }
+ return new PlannerAgent(
+ name,
+ description,
+ subAgents,
+ planner,
+ maxIterations,
+ beforeAgentCallback,
+ afterAgentCallback);
+ }
+ }
+}
diff --git a/core/src/main/java/com/google/adk/agents/PlanningContext.java b/core/src/main/java/com/google/adk/agents/PlanningContext.java
new file mode 100644
index 000000000..9ab22d83e
--- /dev/null
+++ b/core/src/main/java/com/google/adk/agents/PlanningContext.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.adk.agents;
+
+import com.google.adk.events.Event;
+import com.google.common.collect.ImmutableList;
+import com.google.genai.types.Content;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * Context provided to a {@link Planner} during the planning loop.
+ *
+ * Wraps an {@link InvocationContext} to expose the session state (world state), events, and
+ * available sub-agents. Planners use this to inspect the current state and decide which agent(s) to
+ * run next.
+ */
+public class PlanningContext {
+
+ private final InvocationContext invocationContext;
+ private final ImmutableList availableAgents;
+
+ public PlanningContext(
+ InvocationContext invocationContext, ImmutableList availableAgents) {
+ this.invocationContext = invocationContext;
+ this.availableAgents = availableAgents;
+ }
+
+ /** Returns the session state — the shared "world state" that agents read and write. */
+ public ConcurrentMap state() {
+ return invocationContext.session().state();
+ }
+
+ /** Returns all events in the current session. */
+ public List events() {
+ return invocationContext.session().events();
+ }
+
+ /** Returns the sub-agents available for the planner to select from. */
+ public ImmutableList availableAgents() {
+ return availableAgents;
+ }
+
+ /** Returns the user content that initiated this invocation, if any. */
+ public Optional userContent() {
+ return invocationContext.userContent();
+ }
+
+ /**
+ * Finds an available agent by name.
+ *
+ * @throws IllegalArgumentException if no agent with the given name is found.
+ */
+ public BaseAgent findAgent(String name) {
+ return availableAgents.stream()
+ .filter(agent -> agent.name().equals(name))
+ .findFirst()
+ .orElseThrow(
+ () ->
+ new IllegalArgumentException(
+ "No available agent with name: "
+ + name
+ + ". Available: "
+ + availableAgents.stream().map(BaseAgent::name).toList()));
+ }
+
+ /** Returns the full {@link InvocationContext} for advanced use cases. */
+ public InvocationContext invocationContext() {
+ return invocationContext;
+ }
+}
diff --git a/core/src/test/java/com/google/adk/agents/PlannerAgentTest.java b/core/src/test/java/com/google/adk/agents/PlannerAgentTest.java
new file mode 100644
index 000000000..bc0166d0e
--- /dev/null
+++ b/core/src/test/java/com/google/adk/agents/PlannerAgentTest.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.adk.agents;
+
+import static com.google.adk.testing.TestUtils.createEvent;
+import static com.google.adk.testing.TestUtils.createInvocationContext;
+import static com.google.adk.testing.TestUtils.createSubAgent;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.adk.events.Event;
+import com.google.adk.events.EventActions;
+import com.google.adk.testing.TestBaseAgent;
+import com.google.common.collect.ImmutableList;
+import io.reactivex.rxjava3.core.Flowable;
+import io.reactivex.rxjava3.core.Single;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link PlannerAgent}. */
+@RunWith(JUnit4.class)
+public final class PlannerAgentTest {
+
+ @Test
+ public void runAsync_withDone_stopsImmediately() {
+ TestBaseAgent subAgent = createSubAgent("sub", createEvent("e1"));
+ Planner donePlanner =
+ new Planner() {
+ @Override
+ public Single firstAction(PlanningContext context) {
+ return Single.just(new PlannerAction.Done());
+ }
+
+ @Override
+ public Single nextAction(PlanningContext context) {
+ return Single.just(new PlannerAction.Done());
+ }
+ };
+
+ PlannerAgent agent =
+ PlannerAgent.builder().name("planner").subAgents(subAgent).planner(donePlanner).build();
+
+ InvocationContext ctx = createInvocationContext(agent);
+ List events = agent.runAsync(ctx).toList().blockingGet();
+
+ assertThat(events).isEmpty();
+ }
+
+ @Test
+ public void runAsync_withDoneWithResult_emitsResultEvent() {
+ TestBaseAgent subAgent = createSubAgent("sub");
+ Planner resultPlanner =
+ new Planner() {
+ @Override
+ public Single firstAction(PlanningContext context) {
+ return Single.just(new PlannerAction.DoneWithResult("final answer"));
+ }
+
+ @Override
+ public Single nextAction(PlanningContext context) {
+ return Single.just(new PlannerAction.Done());
+ }
+ };
+
+ PlannerAgent agent =
+ PlannerAgent.builder().name("planner").subAgents(subAgent).planner(resultPlanner).build();
+
+ InvocationContext ctx = createInvocationContext(agent);
+ List events = agent.runAsync(ctx).toList().blockingGet();
+
+ assertThat(events).hasSize(1);
+ assertThat(events.get(0).content().get().text()).isEqualTo("final answer");
+ }
+
+ @Test
+ public void runAsync_withNoOp_skipsAndContinues() {
+ Event event1 = createEvent("e1");
+ TestBaseAgent subAgent = createSubAgent("sub", event1);
+
+ AtomicInteger callCount = new AtomicInteger(0);
+ Planner noOpThenRunPlanner =
+ new Planner() {
+ @Override
+ public Single firstAction(PlanningContext context) {
+ return Single.just(new PlannerAction.NoOp());
+ }
+
+ @Override
+ public Single nextAction(PlanningContext context) {
+ int count = callCount.incrementAndGet();
+ if (count == 1) {
+ return Single.just(new PlannerAction.RunAgents(context.findAgent("sub")));
+ }
+ return Single.just(new PlannerAction.Done());
+ }
+ };
+
+ PlannerAgent agent =
+ PlannerAgent.builder()
+ .name("planner")
+ .subAgents(subAgent)
+ .planner(noOpThenRunPlanner)
+ .build();
+
+ InvocationContext ctx = createInvocationContext(agent);
+ List events = agent.runAsync(ctx).toList().blockingGet();
+
+ assertThat(events).containsExactly(event1);
+ }
+
+ @Test
+ public void runAsync_withMaxIterations_stopsAtLimit() {
+ TestBaseAgent subAgent = createSubAgent("sub", () -> Flowable.just(createEvent("e")));
+
+ Planner alwaysRunPlanner =
+ new Planner() {
+ @Override
+ public Single firstAction(PlanningContext context) {
+ return Single.just(new PlannerAction.RunAgents(context.findAgent("sub")));
+ }
+
+ @Override
+ public Single nextAction(PlanningContext context) {
+ return Single.just(new PlannerAction.RunAgents(context.findAgent("sub")));
+ }
+ };
+
+ PlannerAgent agent =
+ PlannerAgent.builder()
+ .name("planner")
+ .subAgents(subAgent)
+ .planner(alwaysRunPlanner)
+ .maxIterations(3)
+ .build();
+
+ InvocationContext ctx = createInvocationContext(agent);
+ List events = agent.runAsync(ctx).toList().blockingGet();
+
+ // 3 iterations: first + 2 next calls, each producing 1 event
+ assertThat(events).hasSize(3);
+ }
+
+ @Test
+ public void runAsync_sequentialPlannerPattern() {
+ Event event1 = createEvent("e1");
+ Event event2 = createEvent("e2");
+ Event event3 = createEvent("e3");
+ TestBaseAgent agentA = createSubAgent("agentA", event1);
+ TestBaseAgent agentB = createSubAgent("agentB", event2);
+ TestBaseAgent agentC = createSubAgent("agentC", event3);
+
+ AtomicInteger cursor = new AtomicInteger(0);
+ ImmutableList order = ImmutableList.of("agentA", "agentB", "agentC");
+ Planner seqPlanner =
+ new Planner() {
+ @Override
+ public Single firstAction(PlanningContext context) {
+ cursor.set(0);
+ return selectNext(context);
+ }
+
+ @Override
+ public Single nextAction(PlanningContext context) {
+ return selectNext(context);
+ }
+
+ private Single selectNext(PlanningContext context) {
+ int idx = cursor.getAndIncrement();
+ if (idx >= order.size()) {
+ return Single.just(new PlannerAction.Done());
+ }
+ return Single.just(new PlannerAction.RunAgents(context.findAgent(order.get(idx))));
+ }
+ };
+
+ PlannerAgent agent =
+ PlannerAgent.builder()
+ .name("planner")
+ .subAgents(agentA, agentB, agentC)
+ .planner(seqPlanner)
+ .build();
+
+ InvocationContext ctx = createInvocationContext(agent);
+ List events = agent.runAsync(ctx).toList().blockingGet();
+
+ assertThat(events).containsExactly(event1, event2, event3).inOrder();
+ }
+
+ @Test
+ public void runAsync_withParallelRunAgents_runsMultipleAgents() {
+ Event event1 = createEvent("e1");
+ Event event2 = createEvent("e2");
+ TestBaseAgent agentA = createSubAgent("agentA", event1);
+ TestBaseAgent agentB = createSubAgent("agentB", event2);
+
+ Planner parallelPlanner =
+ new Planner() {
+ @Override
+ public Single firstAction(PlanningContext context) {
+ return Single.just(new PlannerAction.RunAgents(context.availableAgents()));
+ }
+
+ @Override
+ public Single nextAction(PlanningContext context) {
+ return Single.just(new PlannerAction.Done());
+ }
+ };
+
+ PlannerAgent agent =
+ PlannerAgent.builder()
+ .name("planner")
+ .subAgents(agentA, agentB)
+ .planner(parallelPlanner)
+ .build();
+
+ InvocationContext ctx = createInvocationContext(agent);
+ List events = agent.runAsync(ctx).toList().blockingGet();
+
+ assertThat(events).containsExactly(event1, event2);
+ }
+
+ @Test
+ public void runAsync_withEmptySubAgents_returnsEmpty() {
+ Planner planner =
+ new Planner() {
+ @Override
+ public Single firstAction(PlanningContext context) {
+ return Single.just(new PlannerAction.Done());
+ }
+
+ @Override
+ public Single nextAction(PlanningContext context) {
+ return Single.just(new PlannerAction.Done());
+ }
+ };
+
+ PlannerAgent agent =
+ PlannerAgent.builder()
+ .name("planner")
+ .subAgents(ImmutableList.of())
+ .planner(planner)
+ .build();
+
+ InvocationContext ctx = createInvocationContext(agent);
+ List events = agent.runAsync(ctx).toList().blockingGet();
+
+ assertThat(events).isEmpty();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void builder_withoutPlanner_throwsIllegalState() {
+ TestBaseAgent subAgent = createSubAgent("sub");
+ PlannerAgent.builder().name("planner").subAgents(subAgent).build();
+ }
+
+ @Test
+ public void runAsync_stateIsSharedAcrossAgents() {
+ // Agent A writes to state, Agent B reads from state
+ Event eventA =
+ createEvent("eA").toBuilder()
+ .actions(
+ EventActions.builder()
+ .stateDelta(
+ new java.util.concurrent.ConcurrentHashMap<>(
+ java.util.Map.of("key1", "value1")))
+ .build())
+ .build();
+
+ TestBaseAgent agentA = createSubAgent("agentA", eventA);
+ TestBaseAgent agentB = createSubAgent("agentB", createEvent("eB"));
+
+ AtomicInteger cursor = new AtomicInteger(0);
+ Planner seqPlanner =
+ new Planner() {
+ @Override
+ public Single firstAction(PlanningContext context) {
+ cursor.set(0);
+ return nextAction(context);
+ }
+
+ @Override
+ public Single nextAction(PlanningContext context) {
+ int idx = cursor.getAndIncrement();
+ if (idx == 0) {
+ return Single.just(new PlannerAction.RunAgents(context.findAgent("agentA")));
+ }
+ if (idx == 1) {
+ return Single.just(new PlannerAction.RunAgents(context.findAgent("agentB")));
+ }
+ return Single.just(new PlannerAction.Done());
+ }
+ };
+
+ PlannerAgent agent =
+ PlannerAgent.builder()
+ .name("planner")
+ .subAgents(agentA, agentB)
+ .planner(seqPlanner)
+ .build();
+
+ InvocationContext ctx = createInvocationContext(agent);
+ List events = agent.runAsync(ctx).toList().blockingGet();
+
+ // Both events should be emitted
+ assertThat(events).hasSize(2);
+ // State delta from agentA's event should be present
+ assertThat(events.get(0).actions().stateDelta()).containsEntry("key1", "value1");
+ }
+}
diff --git a/tutorials/planner-agents/pom.xml b/tutorials/planner-agents/pom.xml
new file mode 100644
index 000000000..04f007bfd
--- /dev/null
+++ b/tutorials/planner-agents/pom.xml
@@ -0,0 +1,56 @@
+
+
+
+ 4.0.0
+
+
+ com.google.adk
+ google-adk-parent
+ 0.5.1-SNAPSHOT
+ ../../pom.xml
+
+
+ google-adk-tutorials-planner-agents
+ Agent Development Kit - Planner Agents Tutorial
+ Tutorial examples demonstrating GOAP, P2P, and Supervisor planner patterns.
+
+
+
+ com.google.adk
+ google-adk
+ ${project.version}
+
+
+ com.google.adk
+ google-adk-planners
+ ${project.version}
+
+
+ com.google.adk
+ google-adk-dev
+ ${project.version}
+
+
+ com.google.genai
+ google-genai
+
+
+ org.slf4j
+ slf4j-simple
+
+
+
diff --git a/tutorials/planner-agents/src/main/java/com/google/adk/tutorials/planner/goap/HoroscopeWriterExample.java b/tutorials/planner-agents/src/main/java/com/google/adk/tutorials/planner/goap/HoroscopeWriterExample.java
new file mode 100644
index 000000000..53bef2d0e
--- /dev/null
+++ b/tutorials/planner-agents/src/main/java/com/google/adk/tutorials/planner/goap/HoroscopeWriterExample.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.adk.tutorials.planner.goap;
+
+import com.google.adk.agents.BaseAgent;
+import com.google.adk.agents.LlmAgent;
+import com.google.adk.agents.PlannerAgent;
+import com.google.adk.planner.goap.AgentMetadata;
+import com.google.adk.planner.goap.GoalOrientedPlanner;
+import com.google.adk.tools.Annotations.Schema;
+import com.google.adk.tools.FunctionTool;
+import com.google.adk.web.AdkWebServer;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * GOAP Horoscope Writer Example.
+ *
+ * Demonstrates Goal-Oriented Action Planning (GOAP) where agent execution order is automatically
+ * resolved from input/output dependency declarations.
+ *
+ *
Agent dependency graph:
+ *
+ *
+ * personExtractor (prompt → person)
+ * signExtractor (prompt → sign)
+ * horoscopeGenerator (person, sign → horoscope)
+ * writer (person, horoscope → writeup)
+ *
+ *
+ * The GOAP planner automatically resolves: personExtractor → signExtractor → horoscopeGenerator
+ * → writer
+ *
+ *
Usage: Set the GOOGLE_API_KEY environment variable and run with initial state: {@code
+ * {"prompt": "My name is Mario and my zodiac sign is pisces"}}
+ */
+public class HoroscopeWriterExample {
+
+ // Tool methods that write to session state
+ public static Map savePerson(
+ @Schema(name = "person", description = "The extracted person's name") String person) {
+ return Map.of("person", person);
+ }
+
+ public static Map saveSign(
+ @Schema(name = "sign", description = "The extracted zodiac sign") String sign) {
+ return Map.of("sign", sign);
+ }
+
+ public static Map saveHoroscope(
+ @Schema(name = "horoscope", description = "The generated horoscope") String horoscope) {
+ return Map.of("horoscope", horoscope);
+ }
+
+ public static Map saveWriteup(
+ @Schema(name = "writeup", description = "The final amusing horoscope writeup")
+ String writeup) {
+ return Map.of("writeup", writeup);
+ }
+
+ public static final BaseAgent ROOT_AGENT = createAgent();
+
+ private static BaseAgent createAgent() {
+ LlmAgent personExtractor =
+ LlmAgent.builder()
+ .name("personExtractor")
+ .description("Extract person's name from the prompt")
+ .model("gemini-2.0-flash")
+ .instruction(
+ "Extract the person's name from the user's prompt and save it using the"
+ + " savePerson tool. The prompt is: {prompt}")
+ .tools(FunctionTool.create(HoroscopeWriterExample.class, "savePerson"))
+ .build();
+
+ LlmAgent signExtractor =
+ LlmAgent.builder()
+ .name("signExtractor")
+ .description("Extract zodiac sign from the prompt")
+ .model("gemini-2.0-flash")
+ .instruction(
+ "Extract the zodiac sign from the user's prompt and save it using the saveSign"
+ + " tool. The prompt is: {prompt}")
+ .tools(FunctionTool.create(HoroscopeWriterExample.class, "saveSign"))
+ .build();
+
+ LlmAgent horoscopeGenerator =
+ LlmAgent.builder()
+ .name("horoscopeGenerator")
+ .description("Generate a horoscope based on person and sign")
+ .model("gemini-2.0-flash")
+ .instruction(
+ "Generate a horoscope for person={person} with zodiac sign={sign}. Save it using"
+ + " the saveHoroscope tool.")
+ .tools(FunctionTool.create(HoroscopeWriterExample.class, "saveHoroscope"))
+ .build();
+
+ LlmAgent writer =
+ LlmAgent.builder()
+ .name("writer")
+ .description("Write amusing horoscope writeup")
+ .model("gemini-2.0-flash")
+ .instruction(
+ "Write an amusing horoscope writeup for person={person} based on their"
+ + " horoscope={horoscope}. Make it fun and entertaining. Save using the"
+ + " saveWriteup tool.")
+ .tools(FunctionTool.create(HoroscopeWriterExample.class, "saveWriteup"))
+ .build();
+
+ List metadata =
+ List.of(
+ new AgentMetadata("personExtractor", ImmutableList.of("prompt"), "person"),
+ new AgentMetadata("signExtractor", ImmutableList.of("prompt"), "sign"),
+ new AgentMetadata(
+ "horoscopeGenerator", ImmutableList.of("person", "sign"), "horoscope"),
+ new AgentMetadata("writer", ImmutableList.of("person", "horoscope"), "writeup"));
+
+ return PlannerAgent.builder()
+ .name("horoscopeWriter")
+ .description("Generates personalized amusing horoscopes using GOAP planning")
+ .subAgents(personExtractor, signExtractor, horoscopeGenerator, writer)
+ .planner(new GoalOrientedPlanner("writeup", metadata))
+ .build();
+ }
+
+ public static void main(String[] args) {
+ AdkWebServer.start(ROOT_AGENT);
+ }
+}
diff --git a/tutorials/planner-agents/src/main/java/com/google/adk/tutorials/planner/goap/StoryWriterExample.java b/tutorials/planner-agents/src/main/java/com/google/adk/tutorials/planner/goap/StoryWriterExample.java
new file mode 100644
index 000000000..61932f34b
--- /dev/null
+++ b/tutorials/planner-agents/src/main/java/com/google/adk/tutorials/planner/goap/StoryWriterExample.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.adk.tutorials.planner.goap;
+
+import com.google.adk.agents.BaseAgent;
+import com.google.adk.agents.LlmAgent;
+import com.google.adk.agents.LoopAgent;
+import com.google.adk.agents.PlannerAgent;
+import com.google.adk.planner.goap.AgentMetadata;
+import com.google.adk.planner.goap.GoalOrientedPlanner;
+import com.google.adk.tools.Annotations.Schema;
+import com.google.adk.tools.FunctionTool;
+import com.google.adk.web.AdkWebServer;
+import com.google.common.collect.ImmutableList;
+import io.reactivex.rxjava3.core.Maybe;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * GOAP Story Writer Example with embedded feedback loop.
+ *
+ * Demonstrates a GOAP-planned pipeline with an inner loop: story generation → style editing +
+ * scoring (loops until quality threshold) → audience adaptation.
+ *
+ *
Agent dependency graph:
+ *
+ *
+ * storyGenerator (topic → story)
+ * styleReviewLoop (story, style → styledStory) [inner LoopAgent]
+ * audienceEditor (styledStory, audience → finalStory)
+ *
+ *
+ * Usage: Set GOOGLE_API_KEY and run with initial state: {@code {"topic": "dragons and a wizard",
+ * "style": "fantasy", "audience": "young adults"}}
+ */
+public class StoryWriterExample {
+
+ public static Map saveStory(
+ @Schema(name = "story", description = "The generated story") String story) {
+ return Map.of("story", story);
+ }
+
+ public static Map saveStyledStory(
+ @Schema(name = "styledStory", description = "The style-edited story") String styledStory) {
+ return Map.of("styledStory", styledStory);
+ }
+
+ public static Map saveScore(
+ @Schema(name = "score", description = "Style quality score 0.0-1.0") String score) {
+ return Map.of("score", score);
+ }
+
+ public static Map saveFinalStory(
+ @Schema(name = "finalStory", description = "The audience-adapted final story")
+ String finalStory) {
+ return Map.of("finalStory", finalStory);
+ }
+
+ public static final BaseAgent ROOT_AGENT = createAgent();
+
+ private static BaseAgent createAgent() {
+ LlmAgent storyGenerator =
+ LlmAgent.builder()
+ .name("storyGenerator")
+ .description("Generate initial story from topic")
+ .model("gemini-2.0-flash")
+ .instruction("Write a short story about {topic}. Save it using the saveStory tool.")
+ .tools(FunctionTool.create(StoryWriterExample.class, "saveStory"))
+ .build();
+
+ LlmAgent styleEditor =
+ LlmAgent.builder()
+ .name("styleEditor")
+ .description("Edit story for target style")
+ .model("gemini-2.0-flash")
+ .instruction(
+ "Edit the story at {story} to match the {style} style. Save the result using the"
+ + " saveStyledStory tool.")
+ .tools(FunctionTool.create(StoryWriterExample.class, "saveStyledStory"))
+ .build();
+
+ LlmAgent styleScorer =
+ LlmAgent.builder()
+ .name("styleScorer")
+ .description("Score story style quality 0.0-1.0")
+ .model("gemini-2.0-flash")
+ .instruction(
+ "Score how well the story at {styledStory} matches the target style={style}."
+ + " Provide a score from 0.0 to 1.0. Save the score using the saveScore tool.")
+ .tools(FunctionTool.create(StoryWriterExample.class, "saveScore"))
+ .build();
+
+ // Style review loop: alternates between editing and scoring until quality >= 0.8
+ LoopAgent styleReviewLoop =
+ LoopAgent.builder()
+ .name("styleReviewLoop")
+ .description("Iteratively edit and score story style until quality threshold is met")
+ .subAgents(styleEditor, styleScorer)
+ .maxIterations(5)
+ .afterAgentCallback(
+ ctx -> {
+ Object scoreObj = ctx.state().get("score");
+ if (scoreObj != null) {
+ double score = Double.parseDouble(scoreObj.toString());
+ if (score >= 0.8) {
+ ctx.eventActions().setEscalate(true);
+ }
+ }
+ return Maybe.empty();
+ })
+ .build();
+
+ LlmAgent audienceEditor =
+ LlmAgent.builder()
+ .name("audienceEditor")
+ .description("Adapt story for target audience")
+ .model("gemini-2.0-flash")
+ .instruction(
+ "Edit the story at {styledStory} to be appropriate for the target"
+ + " audience={audience}. Save the result using the saveFinalStory tool.")
+ .tools(FunctionTool.create(StoryWriterExample.class, "saveFinalStory"))
+ .build();
+
+ List metadata =
+ List.of(
+ new AgentMetadata("storyGenerator", ImmutableList.of("topic"), "story"),
+ new AgentMetadata("styleReviewLoop", ImmutableList.of("story", "style"), "styledStory"),
+ new AgentMetadata(
+ "audienceEditor", ImmutableList.of("styledStory", "audience"), "finalStory"));
+
+ return PlannerAgent.builder()
+ .name("storyWriter")
+ .description("Writes styled stories for target audiences using GOAP planning")
+ .subAgents(storyGenerator, styleReviewLoop, audienceEditor)
+ .planner(new GoalOrientedPlanner("finalStory", metadata))
+ .build();
+ }
+
+ public static void main(String[] args) {
+ AdkWebServer.start(ROOT_AGENT);
+ }
+}
diff --git a/tutorials/planner-agents/src/main/java/com/google/adk/tutorials/planner/p2p/ResearchCollaborationExample.java b/tutorials/planner-agents/src/main/java/com/google/adk/tutorials/planner/p2p/ResearchCollaborationExample.java
new file mode 100644
index 000000000..da174fa2c
--- /dev/null
+++ b/tutorials/planner-agents/src/main/java/com/google/adk/tutorials/planner/p2p/ResearchCollaborationExample.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.adk.tutorials.planner.p2p;
+
+import com.google.adk.agents.BaseAgent;
+import com.google.adk.agents.LlmAgent;
+import com.google.adk.agents.PlannerAgent;
+import com.google.adk.planner.goap.AgentMetadata;
+import com.google.adk.planner.p2p.P2PPlanner;
+import com.google.adk.tools.Annotations.Schema;
+import com.google.adk.tools.FunctionTool;
+import com.google.adk.web.AdkWebServer;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * P2P Research Collaboration Example.
+ *
+ * Demonstrates Peer-to-Peer planning where agents activate dynamically as their input
+ * dependencies become available. Supports iterative refinement — when a critic updates their
+ * critique, the hypothesis agent re-activates.
+ *
+ *
Agent activation flow:
+ *
+ *
+ * Wave 1: literatureAgent starts (has: topic)
+ * Wave 2: hypothesisAgent starts (has: topic + researchFindings)
+ * Wave 3: criticAgent starts (has: topic + hypothesis)
+ * Wave 4: scorerAgent starts (has: topic + hypothesis + critique)
+ * → If score < 0.85: hypothesisAgent re-activates (input "critique" changed)
+ * → Loop continues until score >= 0.85 or maxInvocations
+ *
+ *
+ * Usage: Set GOOGLE_API_KEY and run with initial state: {@code {"topic": "quantum entanglement
+ * effects on information transfer"}}
+ */
+public class ResearchCollaborationExample {
+
+ public static Map saveFindings(
+ @Schema(name = "researchFindings", description = "Research findings from literature review")
+ String researchFindings) {
+ return Map.of("researchFindings", researchFindings);
+ }
+
+ public static Map saveHypothesis(
+ @Schema(name = "hypothesis", description = "The formulated research hypothesis")
+ String hypothesis) {
+ return Map.of("hypothesis", hypothesis);
+ }
+
+ public static Map saveCritique(
+ @Schema(name = "critique", description = "Critical evaluation of the hypothesis")
+ String critique) {
+ return Map.of("critique", critique);
+ }
+
+ public static Map saveScore(
+ @Schema(name = "score", description = "Hypothesis quality score 0.0-1.0") String score) {
+ return Map.of("score", score);
+ }
+
+ public static final BaseAgent ROOT_AGENT = createAgent();
+
+ private static BaseAgent createAgent() {
+ LlmAgent literatureAgent =
+ LlmAgent.builder()
+ .name("literatureAgent")
+ .description("Search scientific literature on a topic")
+ .model("gemini-2.0-flash")
+ .instruction(
+ "Research the topic={topic}. Provide a comprehensive literature review. Save the"
+ + " findings using the saveFindings tool.")
+ .tools(FunctionTool.create(ResearchCollaborationExample.class, "saveFindings"))
+ .build();
+
+ LlmAgent hypothesisAgent =
+ LlmAgent.builder()
+ .name("hypothesisAgent")
+ .description("Formulate hypothesis based on research")
+ .model("gemini-2.0-flash")
+ .instruction(
+ "Based on topic={topic} and findings={researchFindings}, formulate a testable"
+ + " research hypothesis. If a critique is available at {critique}, improve your"
+ + " hypothesis to address the critique. Save using the saveHypothesis tool.")
+ .tools(FunctionTool.create(ResearchCollaborationExample.class, "saveHypothesis"))
+ .build();
+
+ LlmAgent criticAgent =
+ LlmAgent.builder()
+ .name("criticAgent")
+ .description("Critically evaluate a hypothesis")
+ .model("gemini-2.0-flash")
+ .instruction(
+ "Evaluate the hypothesis={hypothesis} for topic={topic}. Identify strengths,"
+ + " weaknesses, and suggestions for improvement. Save using the saveCritique"
+ + " tool.")
+ .tools(FunctionTool.create(ResearchCollaborationExample.class, "saveCritique"))
+ .build();
+
+ LlmAgent scorerAgent =
+ LlmAgent.builder()
+ .name("scorerAgent")
+ .description("Score hypothesis quality 0.0-1.0")
+ .model("gemini-2.0-flash")
+ .instruction(
+ "Score the hypothesis={hypothesis} considering the critique={critique} for"
+ + " topic={topic}. Rate from 0.0 (poor) to 1.0 (excellent). Save a numeric"
+ + " score using the saveScore tool.")
+ .tools(FunctionTool.create(ResearchCollaborationExample.class, "saveScore"))
+ .build();
+
+ List metadata =
+ List.of(
+ new AgentMetadata("literatureAgent", ImmutableList.of("topic"), "researchFindings"),
+ new AgentMetadata(
+ "hypothesisAgent", ImmutableList.of("topic", "researchFindings"), "hypothesis"),
+ new AgentMetadata("criticAgent", ImmutableList.of("topic", "hypothesis"), "critique"),
+ new AgentMetadata(
+ "scorerAgent", ImmutableList.of("topic", "hypothesis", "critique"), "score"));
+
+ // Exit when score >= 0.85 or after 10 total invocations
+ return PlannerAgent.builder()
+ .name("researcher")
+ .description("Collaborative research with iterative hypothesis refinement")
+ .subAgents(literatureAgent, hypothesisAgent, criticAgent, scorerAgent)
+ .planner(
+ new P2PPlanner(
+ metadata,
+ 10,
+ (state, count) -> {
+ Object scoreVal = state.get("score");
+ return scoreVal instanceof Number && ((Number) scoreVal).doubleValue() >= 0.85;
+ }))
+ .maxIterations(20)
+ .build();
+ }
+
+ public static void main(String[] args) {
+ AdkWebServer.start(ROOT_AGENT);
+ }
+}
diff --git a/tutorials/planner-agents/src/main/java/com/google/adk/tutorials/planner/supervisor/SupervisorExample.java b/tutorials/planner-agents/src/main/java/com/google/adk/tutorials/planner/supervisor/SupervisorExample.java
new file mode 100644
index 000000000..2075d64cf
--- /dev/null
+++ b/tutorials/planner-agents/src/main/java/com/google/adk/tutorials/planner/supervisor/SupervisorExample.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.adk.tutorials.planner.supervisor;
+
+import com.google.adk.agents.BaseAgent;
+import com.google.adk.agents.LlmAgent;
+import com.google.adk.agents.PlannerAgent;
+import com.google.adk.models.Gemini;
+import com.google.adk.planner.SupervisorPlanner;
+import com.google.adk.web.AdkWebServer;
+
+/**
+ * Supervisor Planner Example.
+ *
+ * Demonstrates LLM-driven agent coordination where the supervisor LLM dynamically decides which
+ * sub-agent to run next based on the current state and conversation history.
+ *
+ *
The supervisor LLM evaluates available agents and decides the sequence: it might run research
+ * first, then writing, then review — or might skip steps if the task is simple.
+ */
+public class SupervisorExample {
+
+ public static final BaseAgent ROOT_AGENT = createAgent();
+
+ private static BaseAgent createAgent() {
+ LlmAgent researchAgent =
+ LlmAgent.builder()
+ .name("researchAgent")
+ .description("Researches a topic and provides comprehensive findings")
+ .model("gemini-2.0-flash")
+ .instruction("Research the user's topic thoroughly. Provide detailed findings.")
+ .build();
+
+ LlmAgent writerAgent =
+ LlmAgent.builder()
+ .name("writerAgent")
+ .description("Writes clear, well-structured content based on research")
+ .model("gemini-2.0-flash")
+ .instruction(
+ "Write clear, well-structured content based on the research findings. Create"
+ + " engaging prose.")
+ .build();
+
+ LlmAgent reviewerAgent =
+ LlmAgent.builder()
+ .name("reviewerAgent")
+ .description("Reviews content for accuracy, clarity, and completeness")
+ .model("gemini-2.0-flash")
+ .instruction(
+ "Review the written content for accuracy, clarity, and completeness. Suggest"
+ + " improvements if needed.")
+ .build();
+
+ return PlannerAgent.builder()
+ .name("supervisorAgent")
+ .description("LLM-supervised task coordination")
+ .subAgents(researchAgent, writerAgent, reviewerAgent)
+ .planner(
+ new SupervisorPlanner(
+ Gemini.builder().modelName("gemini-2.0-flash").build(),
+ "You are a project manager coordinating research, writing, and review agents."
+ + " Decide which agent to run next based on what has been accomplished so far."
+ + " Run researchAgent first, then writerAgent, then reviewerAgent. After all"
+ + " three have run, respond with DONE."))
+ .maxIterations(10)
+ .build();
+ }
+
+ public static void main(String[] args) {
+ AdkWebServer.start(ROOT_AGENT);
+ }
+}