runAsyncImpl(InvocationContext ctx) {
+ return Flowable.empty();
+ }
+}
+```
+
+**Context creation** — tests create a `PlanningContext` with `InMemorySessionService` and a `ConcurrentHashMap` for state:
+
+```java
+PlanningContext context = createPlanningContext(agents, new ConcurrentHashMap<>());
+```
+
+**Plan walking** — tests walk the planning loop by calling `firstAction`/`nextAction` until `Done`:
+
+```java
+PlannerAction action = planner.firstAction(context).blockingGet();
+while (action instanceof PlannerAction.RunAgents runAgents) {
+ simulateSuccess(context, agentNames(runAgents));
+ action = planner.nextAction(context).blockingGet();
+}
+```
+
+**State injection** — tests simulate agent output by directly updating `context.state()`:
+
+```java
+context.state().put("person", "Alice");
+context.state().put("sign", "Aries");
+```
+
+**Strategy equivalence** — A\* vs DFS equivalence is verified on multiple topologies:
+
+```java
+assertThat(astarGroups).isEqualTo(dfsGroups);
+```
+
+### Test Coverage
+
+| Test Class | Focus | Notable Scenarios |
+|-----------|-------|-------------------|
+| `PlannerAgentTest` | Integration loop | State sharing, maxIterations, NoOp handling |
+| `SequentialPlannerTest` | Ordering | Cursor reset, empty agents |
+| `ParallelPlannerTest` | Fan-out | Single agent, empty agents |
+| `LoopPlannerTest` | Cycling | maxCycles, escalate event detection |
+| `SupervisorPlannerTest` | LLM interaction | Prompt construction, decision history, error fallback |
+| `AStarSearchStrategyTest` | Graph search | Linear, diamond, deep chains, cycle detection, DFS equivalence |
+| `GoalOrientedPlannerTest` | GOAP planning | Dependency resolution, parallel grouping, output validation |
+| `ReplanningTest` | Failure handling | Counter reset, max attempts, policy comparison |
+| `CouncilTopologyTest` | Complex GOAP | 9-agent DAG, partial failure, cross-strategy equivalence |
+| `P2PPlannerTest` | Reactive activation | Value-change detection, exit conditions, maxInvocations |
+| `P2PCouncilTopologyTest` | Complex P2P | Wave activation, iterative refinement, termination |
+
+### Running Tests
+
+```bash
+mvn test -pl contrib/planners
+```
+
+---
+
+## 9. Package Reference
+
+### Source Layout
+
+```
+contrib/planners/src/main/java/com/google/adk/
+├── agents/
+│ ├── Planner.java
+│ ├── PlannerAction.java
+│ ├── PlannerAgent.java
+│ └── PlanningContext.java
+└── planner/
+ ├── SequentialPlanner.java
+ ├── ParallelPlanner.java
+ ├── LoopPlanner.java
+ ├── SupervisorPlanner.java
+ ├── goap/
+ │ ├── GoalOrientedPlanner.java
+ │ ├── AgentMetadata.java
+ │ ├── SearchStrategy.java
+ │ ├── DfsSearchStrategy.java
+ │ ├── AStarSearchStrategy.java
+ │ ├── DependencyGraphSearch.java
+ │ ├── GoalOrientedSearchGraph.java
+ │ └── ReplanPolicy.java
+ └── p2p/
+ ├── P2PPlanner.java
+ └── AgentActivator.java
+```
+
+### Class Index
+
+| Package | Class | Type | Purpose |
+|---------|-------|------|---------|
+| `agents` | `Planner` | interface | Strategy for selecting next agent(s) |
+| `agents` | `PlannerAction` | sealed interface | Four-variant action result type |
+| `agents` | `PlannerAgent` | class | Orchestrating agent that runs the planning loop |
+| `agents` | `PlanningContext` | class | State, events, and agents available to planners |
+| `planner` | `SequentialPlanner` | final class | One-at-a-time sequential execution |
+| `planner` | `ParallelPlanner` | final class | All-at-once parallel execution |
+| `planner` | `LoopPlanner` | final class | Cyclic execution with escalation detection |
+| `planner` | `SupervisorPlanner` | final class | LLM-driven dynamic agent selection |
+| `planner.goap` | `GoalOrientedPlanner` | final class | Dependency-resolved planning with replanning |
+| `planner.goap` | `AgentMetadata` | record | Agent input/output key declarations |
+| `planner.goap` | `SearchStrategy` | interface | Strategy for dependency graph search |
+| `planner.goap` | `DfsSearchStrategy` | final class | Backward-chaining DFS search |
+| `planner.goap` | `AStarSearchStrategy` | final class | Forward A* search with admissible heuristic |
+| `planner.goap` | `DependencyGraphSearch` | final class | Topological search and parallel level assignment |
+| `planner.goap` | `GoalOrientedSearchGraph` | final class | Immutable dependency graph data structure |
+| `planner.goap` | `ReplanPolicy` | sealed interface | Failure handling policy (Ignore/FailStop/Replan) |
+| `planner.p2p` | `P2PPlanner` | final class | Reactive dynamic activation with refinement |
+| `planner.p2p` | `AgentActivator` | final class (pkg) | Per-agent activation state tracking |
+
+---
+
+## 10. License
+
+```
+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.
+```
diff --git a/contrib/planners/pom.xml b/contrib/planners/pom.xml
new file mode 100644
index 000000000..96f6bba8d
--- /dev/null
+++ b/contrib/planners/pom.xml
@@ -0,0 +1,72 @@
+
+
+
+ 4.0.0
+
+
+ com.google.adk
+ google-adk-parent
+ 1.0.1-SNAPSHOT
+ ../../pom.xml
+
+
+ google-adk-planners
+ Agent Development Kit - Planners
+ Built-in planner implementations for the ADK PlannerAgent, including GOAP (Goal-Oriented Action Planning), P2P (Peer-to-Peer), and Supervisor planners.
+
+
+
+
+ com.google.adk
+ google-adk
+ ${project.version}
+
+
+ com.google.genai
+ google-genai
+
+
+
+
+ com.google.adk
+ google-adk
+ ${project.version}
+ test-jar
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ com.google.truth
+ truth
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
diff --git a/contrib/planners/src/main/java/com/google/adk/agents/Planner.java b/contrib/planners/src/main/java/com/google/adk/agents/Planner.java
new file mode 100644
index 000000000..cc6e741a2
--- /dev/null
+++ b/contrib/planners/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/contrib/planners/src/main/java/com/google/adk/agents/PlannerAction.java b/contrib/planners/src/main/java/com/google/adk/agents/PlannerAction.java
new file mode 100644
index 000000000..f05dfaf1e
--- /dev/null
+++ b/contrib/planners/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/contrib/planners/src/main/java/com/google/adk/agents/PlannerAgent.java b/contrib/planners/src/main/java/com/google/adk/agents/PlannerAgent.java
new file mode 100644
index 000000000..909845b16
--- /dev/null
+++ b/contrib/planners/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().orElse(null))
+ .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/contrib/planners/src/main/java/com/google/adk/agents/PlanningContext.java b/contrib/planners/src/main/java/com/google/adk/agents/PlanningContext.java
new file mode 100644
index 000000000..be8c81d99
--- /dev/null
+++ b/contrib/planners/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.Map;
+import java.util.Optional;
+
+/**
+ * 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 Map 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/contrib/planners/src/main/java/com/google/adk/planner/LoopPlanner.java b/contrib/planners/src/main/java/com/google/adk/planner/LoopPlanner.java
new file mode 100644
index 000000000..445e9679a
--- /dev/null
+++ b/contrib/planners/src/main/java/com/google/adk/planner/LoopPlanner.java
@@ -0,0 +1,90 @@
+/*
+ * 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.common.collect.ImmutableList;
+import io.reactivex.rxjava3.core.Single;
+import java.util.List;
+
+/**
+ * A planner that cycles through sub-agents repeatedly, stopping when an escalate event is detected
+ * or the maximum number of cycles is reached.
+ */
+public final class LoopPlanner implements Planner {
+
+ private final int maxCycles;
+ // Mutable state — planners are used within a single reactive pipeline and are not thread-safe.
+ private int cursor;
+ private int cycleCount;
+ private ImmutableList agents;
+
+ public LoopPlanner(int maxCycles) {
+ this.maxCycles = maxCycles;
+ }
+
+ @Override
+ public void init(PlanningContext context) {
+ agents = context.availableAgents();
+ cursor = 0;
+ cycleCount = 0;
+ }
+
+ @Override
+ public Single firstAction(PlanningContext context) {
+ cursor = 0;
+ cycleCount = 0;
+ return selectNext(context);
+ }
+
+ @Override
+ public Single nextAction(PlanningContext context) {
+ if (hasEscalateEvent(context.events())) {
+ return Single.just(new PlannerAction.Done());
+ }
+ return selectNext(context);
+ }
+
+ private Single selectNext(PlanningContext context) {
+ if (agents == null || agents.isEmpty()) {
+ return Single.just(new PlannerAction.Done());
+ }
+
+ int idx = cursor++;
+ if (idx >= agents.size()) {
+ int cycle = ++cycleCount;
+ if (cycle >= maxCycles) {
+ return Single.just(new PlannerAction.Done());
+ }
+ cursor = 1;
+ idx = 0;
+ }
+ return Single.just(new PlannerAction.RunAgents(agents.get(idx)));
+ }
+
+ private static boolean hasEscalateEvent(List events) {
+ if (events.isEmpty()) {
+ return false;
+ }
+ Event lastEvent = events.get(events.size() - 1);
+ return lastEvent.actions().escalate().orElse(false);
+ }
+}
diff --git a/contrib/planners/src/main/java/com/google/adk/planner/ParallelPlanner.java b/contrib/planners/src/main/java/com/google/adk/planner/ParallelPlanner.java
new file mode 100644
index 000000000..ec6e5c909
--- /dev/null
+++ b/contrib/planners/src/main/java/com/google/adk/planner/ParallelPlanner.java
@@ -0,0 +1,39 @@
+/*
+ * 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.Planner;
+import com.google.adk.agents.PlannerAction;
+import com.google.adk.agents.PlanningContext;
+import io.reactivex.rxjava3.core.Single;
+
+/** A planner that runs all sub-agents in parallel, then completes. */
+public final class ParallelPlanner implements Planner {
+
+ @Override
+ public Single firstAction(PlanningContext context) {
+ if (context.availableAgents().isEmpty()) {
+ return Single.just(new PlannerAction.Done());
+ }
+ return Single.just(new PlannerAction.RunAgents(context.availableAgents()));
+ }
+
+ @Override
+ public Single nextAction(PlanningContext context) {
+ return Single.just(new PlannerAction.Done());
+ }
+}
diff --git a/contrib/planners/src/main/java/com/google/adk/planner/SequentialPlanner.java b/contrib/planners/src/main/java/com/google/adk/planner/SequentialPlanner.java
new file mode 100644
index 000000000..1ace681ad
--- /dev/null
+++ b/contrib/planners/src/main/java/com/google/adk/planner/SequentialPlanner.java
@@ -0,0 +1,56 @@
+/*
+ * 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.common.collect.ImmutableList;
+import io.reactivex.rxjava3.core.Single;
+
+/** A planner that runs sub-agents one at a time in order. */
+public final class SequentialPlanner implements Planner {
+
+ // Mutable state — planners are used within a single reactive pipeline and are not thread-safe.
+ private int cursor;
+ private ImmutableList agents;
+
+ @Override
+ public void init(PlanningContext context) {
+ agents = context.availableAgents();
+ cursor = 0;
+ }
+
+ @Override
+ public Single firstAction(PlanningContext context) {
+ cursor = 0;
+ return selectNext();
+ }
+
+ @Override
+ public Single nextAction(PlanningContext context) {
+ return selectNext();
+ }
+
+ private Single selectNext() {
+ if (agents == null || cursor >= agents.size()) {
+ return Single.just(new PlannerAction.Done());
+ }
+ return Single.just(new PlannerAction.RunAgents(agents.get(cursor++)));
+ }
+}
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..9f40b3514
--- /dev/null
+++ b/contrib/planners/src/main/java/com/google/adk/planner/SupervisorPlanner.java
@@ -0,0 +1,208 @@
+/*
+ * 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.ArrayList;
+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 static final int DEFAULT_MAX_EVENTS = 20;
+
+ private final BaseLlm llm;
+ private final Optional systemInstruction;
+ private final int maxEvents;
+ private final List decisionHistory = new ArrayList<>();
+
+ public SupervisorPlanner(BaseLlm llm, String systemInstruction, int maxEvents) {
+ this.llm = llm;
+ this.systemInstruction = Optional.ofNullable(systemInstruction);
+ this.maxEvents = maxEvents;
+ }
+
+ public SupervisorPlanner(BaseLlm llm, String systemInstruction) {
+ this(llm, systemInstruction, DEFAULT_MAX_EVENTS);
+ }
+
+ public SupervisorPlanner(BaseLlm llm) {
+ this(llm, null, DEFAULT_MAX_EVENTS);
+ }
+
+ @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);
+ PlannerAction action = parseResponse(text, context);
+ recordDecision(action);
+ return action;
+ })
+ .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() - maxEvents);
+ for (int i = start; i < events.size(); i++) {
+ Event event = events.get(i);
+ sb.append("- ")
+ .append(event.author())
+ .append(": ")
+ .append(event.stringifyContent())
+ .append("\n");
+ }
+ }
+
+ if (!decisionHistory.isEmpty()) {
+ sb.append("\nPrevious decisions (in order):\n");
+ for (int i = 0; i < decisionHistory.size(); i++) {
+ sb.append(i + 1).append(". ").append(decisionHistory.get(i)).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);
+ }
+
+ private void recordDecision(PlannerAction action) {
+ if (action instanceof PlannerAction.RunAgents run) {
+ decisionHistory.add(
+ "Run: " + run.agents().stream().map(BaseAgent::name).collect(Collectors.joining(", ")));
+ } else if (action instanceof PlannerAction.DoneWithResult done) {
+ decisionHistory.add("Done: " + done.result());
+ } else if (action instanceof PlannerAction.Done) {
+ decisionHistory.add("Done");
+ }
+ }
+}
diff --git a/contrib/planners/src/main/java/com/google/adk/planner/goap/AStarSearchStrategy.java b/contrib/planners/src/main/java/com/google/adk/planner/goap/AStarSearchStrategy.java
new file mode 100644
index 000000000..3c28722f2
--- /dev/null
+++ b/contrib/planners/src/main/java/com/google/adk/planner/goap/AStarSearchStrategy.java
@@ -0,0 +1,168 @@
+/*
+ * 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.ImmutableSet;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.PriorityQueue;
+import java.util.Queue;
+import java.util.Set;
+
+/**
+ * A* forward search strategy that explores from preconditions toward the goal, activating agents
+ * whose inputs are all satisfied.
+ *
+ * Uses a priority queue ordered by f-score (g + h) where:
+ *
+ *
+ * - g = number of agents activated so far (uniform cost)
+ *
- h = admissible heuristic counting unsatisfied dependencies reachable backward from goal
+ *
+ *
+ * After finding the goal, reconstructs the agent path and delegates to {@link
+ * DependencyGraphSearch#assignParallelLevels} for parallel grouping.
+ */
+public final class AStarSearchStrategy implements SearchStrategy {
+
+ /** Immutable search state: the set of output keys that have been "activated" (produced). */
+ private record SearchState(ImmutableSet activatedKeys) {}
+
+ /** Priority queue entry tracking cost, heuristic, and parent chain for path reconstruction. */
+ private record StateScore(
+ SearchState state, double gScore, double fScore, String lastActivatedAgent, StateScore parent)
+ implements Comparable {
+
+ @Override
+ public int compareTo(StateScore other) {
+ return Double.compare(this.fScore, other.fScore);
+ }
+ }
+
+ @Override
+ public ImmutableList> searchGrouped(
+ GoalOrientedSearchGraph graph,
+ List metadata,
+ Collection preconditions,
+ String goal) {
+
+ ImmutableSet initialActivated = ImmutableSet.copyOf(preconditions);
+
+ // Goal already satisfied
+ if (initialActivated.contains(goal)) {
+ return ImmutableList.of();
+ }
+
+ PriorityQueue openSet = new PriorityQueue<>();
+ Set> visited = new HashSet<>();
+
+ SearchState startState = new SearchState(initialActivated);
+ double h0 = heuristic(graph, startState, goal);
+ openSet.add(new StateScore(startState, 0.0, h0, null, null));
+
+ while (!openSet.isEmpty()) {
+ StateScore current = openSet.poll();
+
+ if (current.state.activatedKeys.contains(goal)) {
+ ImmutableList agentPath = reconstructPath(current);
+ return DependencyGraphSearch.assignParallelLevels(
+ agentPath, metadata, preconditions, graph);
+ }
+
+ if (!visited.add(current.state.activatedKeys)) {
+ continue;
+ }
+
+ // Find activatable agents: those whose ALL inputKeys are in activatedKeys
+ for (AgentMetadata agent : metadata) {
+ if (current.state.activatedKeys.contains(agent.outputKey())) {
+ continue; // already activated
+ }
+ if (!current.state.activatedKeys.containsAll(agent.inputKeys())) {
+ continue; // not all inputs satisfied
+ }
+
+ ImmutableSet newActivated =
+ ImmutableSet.builder()
+ .addAll(current.state.activatedKeys)
+ .add(agent.outputKey())
+ .build();
+
+ if (visited.contains(newActivated)) {
+ continue;
+ }
+
+ SearchState newState = new SearchState(newActivated);
+ double newG = current.gScore + 1.0;
+ double newH = heuristic(graph, newState, goal);
+ double newF = newG + newH;
+
+ openSet.add(new StateScore(newState, newG, newF, agent.agentName(), current));
+ }
+ }
+
+ throw new IllegalStateException(
+ "Cannot reach goal '"
+ + goal
+ + "': no sequence of agents can produce it from the given preconditions.");
+ }
+
+ /**
+ * Admissible heuristic: counts unsatisfied output keys reachable backward from the goal.
+ *
+ * Each unsatisfied key requires at least one agent to produce it, so this never overestimates.
+ */
+ private static double heuristic(GoalOrientedSearchGraph graph, SearchState state, String goal) {
+ Queue queue = new ArrayDeque<>();
+ Set seen = new HashSet<>();
+ int unsatisfied = 0;
+
+ queue.add(goal);
+ while (!queue.isEmpty()) {
+ String key = queue.poll();
+ if (!seen.add(key)) {
+ continue;
+ }
+ if (!state.activatedKeys.contains(key)) {
+ unsatisfied++;
+ if (graph.contains(key)) {
+ for (String dep : graph.getDependencies(key)) {
+ queue.add(dep);
+ }
+ }
+ }
+ }
+ return unsatisfied;
+ }
+
+ /** Reconstructs the ordered agent path by following the parent chain. */
+ private static ImmutableList reconstructPath(StateScore goalState) {
+ List path = new ArrayList<>();
+ StateScore current = goalState;
+ while (current != null && current.lastActivatedAgent != null) {
+ path.add(current.lastActivatedAgent);
+ current = current.parent;
+ }
+ Collections.reverse(path);
+ return ImmutableList.copyOf(path);
+ }
+}
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..0a730f413
--- /dev/null
+++ b/contrib/planners/src/main/java/com/google/adk/planner/goap/DependencyGraphSearch.java
@@ -0,0 +1,190 @@
+/*
+ * 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.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+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);
+ }
+
+ /**
+ * Groups agents into parallelizable execution levels.
+ *
+ * Each group contains agents whose dependencies are all satisfied by agents in earlier groups
+ * or by initial preconditions. Agents within the same group are independent and can run in
+ * parallel.
+ *
+ * @param graph the dependency graph
+ * @param metadata agent metadata used to compute dependency levels
+ * @param preconditions state keys already available
+ * @param goal the target output key
+ * @return ordered list of agent groups; agents within each group can run in parallel
+ * @throws IllegalStateException if a dependency cannot be resolved or a cycle is detected
+ */
+ public static ImmutableList> searchGrouped(
+ GoalOrientedSearchGraph graph,
+ List metadata,
+ Collection preconditions,
+ String goal) {
+
+ ImmutableList flatOrder = search(graph, preconditions, goal);
+ return assignParallelLevels(flatOrder, metadata, preconditions, graph);
+ }
+
+ /**
+ * Assigns agents from a flat execution order into parallelizable groups based on dependency
+ * depth.
+ *
+ * Each agent's level is {@code 1 + max(level of its dependency agents)}. Agents at the same
+ * level have no mutual dependencies and can run in parallel.
+ *
+ * @param flatOrder ordered list of agent names (topological order)
+ * @param metadata agent metadata for dependency lookup
+ * @param preconditions state keys already available
+ * @param graph the dependency graph
+ * @return ordered list of agent groups for parallel execution
+ */
+ static ImmutableList> assignParallelLevels(
+ ImmutableList flatOrder,
+ List metadata,
+ Collection preconditions,
+ GoalOrientedSearchGraph graph) {
+
+ if (flatOrder.isEmpty()) {
+ return ImmutableList.of();
+ }
+
+ Map agentToMeta = new HashMap<>();
+ for (AgentMetadata m : metadata) {
+ agentToMeta.put(m.agentName(), m);
+ }
+
+ // Assign execution levels: level = 1 + max(level of dependency agents).
+ // Agents at the same level have no mutual dependencies and can run in parallel.
+ Set preconSet = new HashSet<>(preconditions);
+ Map agentLevel = new LinkedHashMap<>();
+
+ for (String agentName : flatOrder) {
+ AgentMetadata meta = agentToMeta.get(agentName);
+ int maxDepLevel = -1;
+
+ for (String inputKey : meta.inputKeys()) {
+ if (preconSet.contains(inputKey)) {
+ continue;
+ }
+ String producerAgent = graph.getProducerAgent(inputKey);
+ if (producerAgent != null && agentLevel.containsKey(producerAgent)) {
+ maxDepLevel = Math.max(maxDepLevel, agentLevel.get(producerAgent));
+ }
+ }
+
+ agentLevel.put(agentName, maxDepLevel + 1);
+ }
+
+ int maxLevel = agentLevel.values().stream().mapToInt(Integer::intValue).max().orElse(0);
+ ImmutableList.Builder> groups = ImmutableList.builder();
+ for (int level = 0; level <= maxLevel; level++) {
+ final int l = level;
+ ImmutableList group =
+ flatOrder.stream()
+ .filter(name -> agentLevel.get(name) == l)
+ .collect(ImmutableList.toImmutableList());
+ if (!group.isEmpty()) {
+ groups.add(group);
+ }
+ }
+
+ return groups.build();
+ }
+
+ 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/DfsSearchStrategy.java b/contrib/planners/src/main/java/com/google/adk/planner/goap/DfsSearchStrategy.java
new file mode 100644
index 000000000..964e7911b
--- /dev/null
+++ b/contrib/planners/src/main/java/com/google/adk/planner/goap/DfsSearchStrategy.java
@@ -0,0 +1,38 @@
+/*
+ * 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.List;
+
+/**
+ * Backward-chaining DFS search strategy with parallel grouping.
+ *
+ * Delegates to {@link DependencyGraphSearch} for the actual algorithm.
+ */
+public final class DfsSearchStrategy implements SearchStrategy {
+
+ @Override
+ public ImmutableList> searchGrouped(
+ GoalOrientedSearchGraph graph,
+ List metadata,
+ Collection preconditions,
+ String goal) {
+ return DependencyGraphSearch.searchGrouped(graph, metadata, preconditions, goal);
+ }
+}
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..b340b8129
--- /dev/null
+++ b/contrib/planners/src/main/java/com/google/adk/planner/goap/GoalOrientedPlanner.java
@@ -0,0 +1,216 @@
+/*
+ * 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.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+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 groups: [A, B] → [C] → [D]
+ * (A and B are independent and run in parallel)
+ *
+ *
+ * Supports configurable failure handling via {@link ReplanPolicy}:
+ *
+ *
+ * - {@link ReplanPolicy.Ignore} — proceed regardless of missing outputs (default)
+ *
- {@link ReplanPolicy.FailStop} — halt on first missing output
+ *
- {@link ReplanPolicy.Replan} — recompute the remaining plan from current world state
+ *
+ *
+ * Supports pluggable search strategies via {@link SearchStrategy}: backward-chaining DFS ({@link
+ * DfsSearchStrategy}) or forward A* ({@link AStarSearchStrategy}).
+ */
+public final class GoalOrientedPlanner implements Planner {
+
+ private static final Logger logger = LoggerFactory.getLogger(GoalOrientedPlanner.class);
+
+ private final String goal;
+ private final List metadata;
+ private final SearchStrategy searchStrategy;
+ private final ReplanPolicy replanPolicy;
+ // Mutable state — planners are used within a single reactive pipeline and are not thread-safe.
+ private ImmutableList> executionGroups;
+ private Map agentNameToOutputKey;
+ private int cursor;
+ private int replanCount;
+
+ public GoalOrientedPlanner(String goal, List metadata) {
+ this(goal, metadata, new DfsSearchStrategy(), new ReplanPolicy.Ignore());
+ }
+
+ public GoalOrientedPlanner(String goal, List metadata, boolean validateOutputs) {
+ this(
+ goal,
+ metadata,
+ new DfsSearchStrategy(),
+ validateOutputs ? new ReplanPolicy.FailStop() : new ReplanPolicy.Ignore());
+ }
+
+ public GoalOrientedPlanner(
+ String goal,
+ List metadata,
+ SearchStrategy searchStrategy,
+ ReplanPolicy replanPolicy) {
+ this.goal = goal;
+ this.metadata = metadata;
+ this.searchStrategy = searchStrategy;
+ this.replanPolicy = replanPolicy;
+ }
+
+ @Override
+ public void init(PlanningContext context) {
+ buildPlan(context);
+ replanCount = 0;
+ }
+
+ @Override
+ public Single firstAction(PlanningContext context) {
+ cursor = 0;
+ return selectNext();
+ }
+
+ @Override
+ public Single nextAction(PlanningContext context) {
+ if (cursor > 0 && executionGroups != null) {
+ List missingOutputs = findMissingOutputs(executionGroups.get(cursor - 1), context);
+
+ if (!missingOutputs.isEmpty()) {
+ if (replanPolicy instanceof ReplanPolicy.FailStop) {
+ String message =
+ "Execution stopped: missing expected outputs from previous group: "
+ + String.join(", ", missingOutputs);
+ logger.warn(message);
+ return Single.just(new PlannerAction.DoneWithResult(message));
+ } else if (replanPolicy instanceof ReplanPolicy.Replan replan) {
+ if (replanCount >= replan.maxAttempts()) {
+ String message =
+ "Execution stopped: max replan attempts ("
+ + replan.maxAttempts()
+ + ") exhausted. Still missing: "
+ + String.join(", ", missingOutputs);
+ logger.warn(message);
+ return Single.just(new PlannerAction.DoneWithResult(message));
+ }
+
+ replanCount++;
+ logger.info(
+ "Replanning (attempt {}/{}). Current state keys: {}. Missing outputs: {}",
+ replanCount,
+ replan.maxAttempts(),
+ context.state().keySet(),
+ missingOutputs);
+
+ try {
+ buildPlan(context);
+ } catch (IllegalStateException e) {
+ String message = "Replanning failed: " + e.getMessage();
+ logger.warn(message);
+ return Single.just(new PlannerAction.DoneWithResult(message));
+ }
+
+ if (executionGroups.isEmpty()) {
+ return Single.just(new PlannerAction.Done());
+ }
+
+ logger.info("Replanned execution groups: {}", executionGroupNames());
+ }
+ // ReplanPolicy.Ignore: proceed with current plan
+ } else {
+ // Previous group succeeded — reset consecutive replan counter
+ replanCount = 0;
+ }
+ }
+ return selectNext();
+ }
+
+ private void buildPlan(PlanningContext context) {
+ GoalOrientedSearchGraph graph = new GoalOrientedSearchGraph(metadata);
+ ImmutableList> agentGroups =
+ searchStrategy.searchGrouped(graph, metadata, context.state().keySet(), goal);
+
+ logger.info("GoalOrientedPlanner resolved execution groups: {}", agentGroups);
+
+ executionGroups =
+ agentGroups.stream()
+ .map(
+ group ->
+ group.stream().map(context::findAgent).collect(ImmutableList.toImmutableList()))
+ .collect(ImmutableList.toImmutableList());
+ cursor = 0;
+
+ agentNameToOutputKey = new HashMap<>();
+ for (AgentMetadata m : metadata) {
+ agentNameToOutputKey.put(m.agentName(), m.outputKey());
+ }
+ }
+
+ private List findMissingOutputs(ImmutableList group, PlanningContext context) {
+ List missing = new ArrayList<>();
+ for (BaseAgent agent : group) {
+ String expectedOutput = agentNameToOutputKey.get(agent.name());
+ if (expectedOutput != null && !context.state().containsKey(expectedOutput)) {
+ missing.add(agent.name() + " -> " + expectedOutput);
+ logger.warn(
+ "GoalOrientedPlanner: agent '{}' did not produce expected output key '{}'",
+ agent.name(),
+ expectedOutput);
+ }
+ }
+ return missing;
+ }
+
+ private List> executionGroupNames() {
+ return executionGroups.stream()
+ .map(group -> group.stream().map(BaseAgent::name).toList())
+ .toList();
+ }
+
+ private Single selectNext() {
+ if (executionGroups == null || cursor >= executionGroups.size()) {
+ return Single.just(new PlannerAction.Done());
+ }
+ ImmutableList group = executionGroups.get(cursor++);
+ return Single.just(new PlannerAction.RunAgents(group));
+ }
+}
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..21243c632
--- /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 backward-chaining dependency resolution.
+ */
+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/goap/ReplanPolicy.java b/contrib/planners/src/main/java/com/google/adk/planner/goap/ReplanPolicy.java
new file mode 100644
index 000000000..7cffe593e
--- /dev/null
+++ b/contrib/planners/src/main/java/com/google/adk/planner/goap/ReplanPolicy.java
@@ -0,0 +1,45 @@
+/*
+ * 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;
+
+/**
+ * Policy governing how the planner reacts to missing expected outputs after an agent group
+ * executes.
+ */
+public sealed interface ReplanPolicy
+ permits ReplanPolicy.FailStop, ReplanPolicy.Replan, ReplanPolicy.Ignore {
+
+ /** Stop immediately on failure with an error message. */
+ record FailStop() implements ReplanPolicy {}
+
+ /**
+ * Attempt to recompute the remaining plan from current world state.
+ *
+ * @param maxAttempts maximum number of consecutive replan attempts before falling back to
+ * fail-stop. Must be {@code >= 1}.
+ */
+ record Replan(int maxAttempts) implements ReplanPolicy {
+ public Replan {
+ if (maxAttempts < 1) {
+ throw new IllegalArgumentException("maxAttempts must be >= 1, got " + maxAttempts);
+ }
+ }
+ }
+
+ /** Ignore failures and proceed with the remaining plan as-is. */
+ record Ignore() implements ReplanPolicy {}
+}
diff --git a/contrib/planners/src/main/java/com/google/adk/planner/goap/SearchStrategy.java b/contrib/planners/src/main/java/com/google/adk/planner/goap/SearchStrategy.java
new file mode 100644
index 000000000..23734c465
--- /dev/null
+++ b/contrib/planners/src/main/java/com/google/adk/planner/goap/SearchStrategy.java
@@ -0,0 +1,47 @@
+/*
+ * 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.List;
+
+/**
+ * Strategy for searching a dependency graph to find ordered agent execution groups.
+ *
+ * Given a graph, agent metadata, available preconditions, and a goal output key, produces an
+ * ordered list of agent groups where agents within each group are independent and can run in
+ * parallel.
+ */
+public interface SearchStrategy {
+
+ /**
+ * Searches for agent execution groups that produce the goal.
+ *
+ * @param graph the dependency graph
+ * @param metadata agent metadata
+ * @param preconditions state keys already available
+ * @param goal the target output key
+ * @return ordered list of agent groups for parallel execution
+ * @throws IllegalStateException if the goal cannot be reached
+ */
+ ImmutableList> searchGrouped(
+ GoalOrientedSearchGraph graph,
+ List metadata,
+ Collection preconditions,
+ String goal);
+}
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..b52edac6c
--- /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.Map;
+
+/**
+ * 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(Map 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..79095e0c6
--- /dev/null
+++ b/contrib/planners/src/main/java/com/google/adk/planner/p2p/P2PPlanner.java
@@ -0,0 +1,173 @@
+/*
+ * 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.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+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