diff --git a/core/src/main/java/com/google/adk/tools/PersistentAgentTool.java b/core/src/main/java/com/google/adk/tools/PersistentAgentTool.java
new file mode 100644
index 000000000..6ee145a8b
--- /dev/null
+++ b/core/src/main/java/com/google/adk/tools/PersistentAgentTool.java
@@ -0,0 +1,321 @@
+/*
+ * 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.tools;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.adk.JsonBaseModel;
+import com.google.adk.SchemaUtils;
+import com.google.adk.agents.BaseAgent;
+import com.google.adk.agents.LlmAgent;
+import com.google.adk.artifacts.BaseArtifactService;
+import com.google.adk.events.Event;
+import com.google.adk.events.EventActions;
+import com.google.adk.runner.Runner;
+import com.google.adk.sessions.BaseSessionService;
+import com.google.adk.sessions.State;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.genai.types.Content;
+import com.google.genai.types.FunctionDeclaration;
+import com.google.genai.types.Part;
+import com.google.genai.types.Schema;
+import io.reactivex.rxjava3.core.Single;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * An ADK {@link BaseTool} that wraps another {@link BaseAgent}, allowing it to be invoked like a
+ * standard tool within an orchestrator agent.
+ *
+ *
Unlike {@link AgentTool}, this implementation uses {@link Runner} configured with externally
+ * provided {@link BaseSessionService} and {@link BaseArtifactService} to execute the wrapped agent.
+ * This enables integration with persistent session services for improved observability and
+ * debugging in production environments, as all underlying agent events are recorded in the calling
+ * agent's session.
+ *
+ *
This tool performs the following steps during {@link #runAsync}:
+ *
+ *
+ *
Executes the wrapped agent using {@link Runner}, running in the calling agent's session
+ * context.
+ *
Collects all {@link Event}s produced by the execution.
+ *
Iterates through all collected events and applies any state changes ({@link State#REMOVED}
+ * or updates) found in {@code event.actions().stateDelta()} to the {@code toolContext} of the
+ * calling agent.
+ *
Returns a map containing the result of the agent execution, plus a {@code trace} field
+ * which holds the complete list of {@link Event}s from the execution.
+ *
+ */
+public class PersistentAgentTool extends BaseTool {
+
+ private final BaseAgent agent;
+ private final String appName;
+ private final BaseSessionService sessionService;
+ private final BaseArtifactService artifactService;
+
+ /**
+ * Creates a new instance of {@link PersistentAgentTool}.
+ *
+ * @param agent The agent instance to wrap and execute as a tool.
+ * @param appName The application name to use for logging and session management, passed to {@link
+ * Runner}.
+ * @param sessionService The session service to use for agent execution via {@link Runner}.
+ * @param artifactService The artifact service to use for agent execution via {@link Runner}.
+ */
+ public static PersistentAgentTool create(
+ BaseAgent agent,
+ String appName,
+ BaseSessionService sessionService,
+ BaseArtifactService artifactService) {
+ return new PersistentAgentTool(agent, appName, sessionService, artifactService);
+ }
+
+ /**
+ * Creates a new instance of {@link PersistentAgentTool}.
+ *
+ * @param agent The agent instance to wrap and execute as a tool.
+ * @param appName The application name to use for logging and session management, passed to {@link
+ * Runner}.
+ * @param sessionService The session service to use for agent execution via {@link Runner}.
+ * @param artifactService The artifact service to use for agent execution via {@link Runner}.
+ */
+ protected PersistentAgentTool(
+ BaseAgent agent,
+ String appName,
+ BaseSessionService sessionService,
+ BaseArtifactService artifactService) {
+ super(agent.name(), agent.description());
+ this.agent = agent;
+ this.appName = appName;
+ this.sessionService = sessionService;
+ this.artifactService = artifactService;
+ }
+
+ /**
+ * Tries to heuristically find an input schema defined on an {@link LlmAgent} contained within the
+ * wrapped agent structure by traversing down through the *first* sub-agent at each level.
+ *
+ *
This is used to determine if the tool should accept structured input matching a schema, or a
+ * simple {@code request} string.
+ */
+ private Optional getInputSchema(BaseAgent agent) {
+ BaseAgent currentAgent = agent;
+ while (true) {
+ if (currentAgent instanceof LlmAgent) {
+ return ((LlmAgent) currentAgent).inputSchema();
+ }
+ List extends BaseAgent> subAgents = currentAgent.subAgents();
+ if (subAgents == null || subAgents.isEmpty()) {
+ return Optional.empty();
+ }
+ // For composite agents, check the first sub-agent.
+ currentAgent = subAgents.get(0);
+ }
+ }
+
+ /**
+ * Tries to heuristically find an output schema defined on an {@link LlmAgent} contained within
+ * the wrapped agent structure by traversing down through the *last* sub-agent at each level.
+ *
+ *
This is used to determine if the tool's final text output should be parsed as structured
+ * JSON based on a schema, or returned as a simple {@code result} string.
+ */
+ private Optional getOutputSchema(BaseAgent agent) {
+ BaseAgent currentAgent = agent;
+ while (true) {
+ if (currentAgent instanceof LlmAgent) {
+ return ((LlmAgent) currentAgent).outputSchema();
+ }
+ List extends BaseAgent> subAgents = currentAgent.subAgents();
+ if (subAgents == null || subAgents.isEmpty()) {
+ return Optional.empty();
+ }
+ // For composite agents, check the last sub-agent.
+ currentAgent = subAgents.get(subAgents.size() - 1);
+ }
+ }
+
+ /**
+ * Builds the tool's function declaration.
+ *
+ *
If an input schema can be inferred via {@link #getInputSchema}, it is used as the tool's
+ * parameters. Otherwise, it defaults to a single parameter {@code request} of type STRING.
+ */
+ @Override
+ public Optional declaration() {
+ FunctionDeclaration.Builder builder =
+ FunctionDeclaration.builder().description(this.description()).name(this.name());
+
+ Optional agentInputSchema = getInputSchema(agent);
+
+ if (agentInputSchema.isPresent()) {
+ builder.parameters(agentInputSchema.get());
+ } else {
+ builder.parameters(
+ Schema.builder()
+ .type("OBJECT")
+ .properties(ImmutableMap.of("request", Schema.builder().type("STRING").build()))
+ .required(ImmutableList.of("request"))
+ .build());
+ }
+ return Optional.of(builder.build());
+ }
+
+ /**
+ * Executes the wrapped agent with the provided arguments.
+ *
+ *
If the agent has an input schema, {@code args} are validated and serialized to JSON to form
+ * the input {@link Content}. Otherwise, the value of the {@code request} key in {@code args} is
+ * used as text input.
+ *
+ *
The agent is run via {@link Runner}, and all resulting events are collected. State changes
+ * from all events are applied to {@code toolContext.state()}. The content of the *last* event is
+ * used as the tool's result, parsed according to {@link #getOutputSchema} if available.
+ *
+ * @param args The arguments for the tool call, matching either the inferred schema or containing
+ * a {@code request} key.
+ * @param toolContext The context of the tool invocation, including session and state.
+ * @return A map containing the agent's result, plus a {@code trace} key holding all execution
+ * events.
+ */
+ @Override
+ public Single