diff --git a/core/src/main/java/com/google/adk/plugins/reflectandretry/ReflectAndRetryToolPlugin.java b/core/src/main/java/com/google/adk/plugins/reflectandretry/ReflectAndRetryToolPlugin.java
new file mode 100644
index 000000000..a676ec0a7
--- /dev/null
+++ b/core/src/main/java/com/google/adk/plugins/reflectandretry/ReflectAndRetryToolPlugin.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2026 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.plugins.reflectandretry;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.adk.plugins.BasePlugin;
+import com.google.adk.tools.BaseTool;
+import com.google.adk.tools.ToolContext;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import io.reactivex.rxjava3.core.Maybe;
+import java.util.Map;
+
+/**
+ * Provides self-healing error recovery for tool failures.
+ *
+ *
This plugin intercepts tool failures, hands the model structured guidance for reflection and
+ * correction, and lets it retry up to a configurable limit. Failure counts are tracked per tool
+ * within a scope, so a success with one tool resets that tool's counter without forgiving
+ * another's.
+ *
+ *
Port of adk-python's {@code ReflectAndRetryToolPlugin} ({@code
+ * plugins/reflect_retry_tool_plugin.py}).
+ *
+ *
Example:
+ *
+ *
{@code
+ * Runner runner =
+ * new Runner(
+ * agent,
+ * APP_NAME,
+ * artifactService,
+ * sessionService,
+ * ImmutableList.of(new ReflectAndRetryToolPlugin(3)));
+ * }
+ *
+ * {@link #scopeKey} and {@link #extractErrorFromResult} are {@code protected} because adk-python
+ * documents both as overridable.
+ */
+public class ReflectAndRetryToolPlugin extends BasePlugin {
+
+ private static final String DEFAULT_NAME = "reflect_retry_tool_plugin";
+ private static final int DEFAULT_MAX_RETRIES = 3;
+ private static final String GLOBAL_SCOPE_KEY = "__global_reflect_and_retry_scope__";
+ private static final String NEGATIVE_RETRIES = "maxRetries must be non-negative, but was %s";
+
+ /** The observed failure count when retrying is disabled: the one failure being reported. */
+ private static final int FIRST_FAILURE = 1;
+
+ private final int maxRetries;
+ private final boolean throwExceptionIfRetryExceeded;
+ private final TrackingScope trackingScope;
+ private final ToolFailureTracker failures = new ToolFailureTracker();
+
+ /** Three retries, throwing when exceeded, tracked per invocation. */
+ public ReflectAndRetryToolPlugin() {
+ this(DEFAULT_NAME, DEFAULT_MAX_RETRIES, true, TrackingScope.INVOCATION);
+ }
+
+ /** As above, with a custom retry limit. */
+ public ReflectAndRetryToolPlugin(int maxRetries) {
+ this(DEFAULT_NAME, maxRetries, true, TrackingScope.INVOCATION);
+ }
+
+ /**
+ * @param name plugin instance identifier
+ * @param maxRetries maximum consecutive failures before giving up; {@code 0} disables retrying
+ * @param throwExceptionIfRetryExceeded whether to propagate the final error once the limit is
+ * reached, rather than returning guidance
+ * @param trackingScope lifetime of the failure counters
+ * @throws IllegalArgumentException if {@code maxRetries} is negative
+ * @throws NullPointerException if {@code trackingScope} is null
+ */
+ public ReflectAndRetryToolPlugin(
+ String name,
+ int maxRetries,
+ boolean throwExceptionIfRetryExceeded,
+ TrackingScope trackingScope) {
+ super(name);
+ checkArgument(maxRetries >= 0, NEGATIVE_RETRIES, maxRetries);
+ this.maxRetries = maxRetries;
+ this.throwExceptionIfRetryExceeded = throwExceptionIfRetryExceeded;
+ this.trackingScope = checkNotNull(trackingScope, "trackingScope cannot be null");
+ }
+
+ /**
+ * Resets the tool's failure count on success, or routes an error carried inside an otherwise
+ * successful result into the retry logic.
+ *
+ *
A result this plugin produced earlier is passed straight through: reflecting on a reflection
+ * would count a single tool failure twice.
+ */
+ @Override
+ public Maybe