Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,16 @@

package com.google.adk.flows.llmflows;

import static com.google.common.collect.ImmutableList.toImmutableList;

import com.google.adk.agents.BaseAgent;
import com.google.adk.agents.InvocationContext;
import com.google.adk.agents.LlmAgent;
import com.google.adk.events.EventActions;
import com.google.adk.models.LlmRequest;
import com.google.adk.tools.Annotations.Schema;
import com.google.adk.tools.FunctionTool;
import com.google.adk.tools.ToolContext;
import com.google.adk.tools.TransferToAgentTool;
import com.google.common.collect.ImmutableList;
import io.reactivex.rxjava3.core.Single;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

Expand Down Expand Up @@ -55,23 +54,15 @@ public Single<RequestProcessor.RequestProcessingResult> processRequest(
.appendInstructions(
ImmutableList.of(buildTargetAgentsInstructions(agent, transferTargets)));

FunctionTool agentTransferTool = createTransferToAgentTool();
// Offering only the reachable agents keeps the model from naming one that does not exist.
TransferToAgentTool agentTransferTool =
TransferToAgentTool.create(
transferTargets.stream().map(BaseAgent::name).collect(toImmutableList()));
agentTransferTool.processLlmRequest(builder, ToolContext.builder(context).build());
return Single.just(
RequestProcessor.RequestProcessingResult.create(builder.build(), ImmutableList.of()));
}

private FunctionTool createTransferToAgentTool() {
Method transferToAgentMethod;
try {
transferToAgentMethod =
AgentTransfer.class.getMethod("transferToAgent", String.class, ToolContext.class);
} catch (NoSuchMethodException e) {
throw new IllegalStateException(e);
}
return FunctionTool.create(transferToAgentMethod);
}

/** Builds a string with the target agent’s name and description. */
private String buildTargetAgentsInfo(BaseAgent targetAgent) {
return String.format(
Expand Down Expand Up @@ -144,24 +135,4 @@ private List<BaseAgent> getTransferTargets(LlmAgent agent) {

return transferTargets;
}

@Schema(
name = "transfer_to_agent",
description =
"""
Transfer the question to another agent.

This tool hands off control to another agent when it's more suitable to
answer the user's question according to the agent's description.

Args:
agent_name: the agent name to transfer to.
\
""")
public static void transferToAgent(
@Schema(name = "agent_name") String agentName,
@Schema(optional = true) ToolContext toolContext) {
EventActions eventActions = toolContext.eventActions();
toolContext.setActions(eventActions.toBuilder().transferToAgent(agentName).build());
}
}
51 changes: 51 additions & 0 deletions core/src/main/java/com/google/adk/tools/BaseToolConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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.tools;

import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.google.adk.JsonBaseModel;

/**
* The base class for the config of a single tool.
*
* <p>A tool that takes arguments in a config document gives them a type by extending this class
* with one property per argument, and filling it from the {@link BaseTool.ToolArgsConfig} of the
* entry that named the tool:
*
* <pre>{@code
* MyToolConfig config =
* JsonBaseModel.getMapper().convertValue(args.getAdditionalProperties(), MyToolConfig.class);
* }</pre>
*
* <p>An argument the subclass does not declare is refused there rather than dropped, so a
* misspelled key in the document reaches the author as an error instead of as an argument that
* silently went missing.
*
* <p>Experimental. The shape of this type may change.
*/
public abstract class BaseToolConfig extends JsonBaseModel {

protected BaseToolConfig() {}

// A key that matches no property of the subclass arrives here, which is where it can be refused:
// the shared mapper skips unknown keys rather than failing on them.
@JsonAnySetter
private void refuseUndeclaredArg(String name, Object value) {
throw new IllegalArgumentException(
String.format("Unknown arg \"%s\" for tool config %s.", name, getClass().getName()));
}
}
36 changes: 36 additions & 0 deletions core/src/main/java/com/google/adk/tools/ToolErrorType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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.tools;

/**
* How a tool failed, in the HTTP error vocabulary OpenTelemetry semantic conventions use.
*
* <p>The constant's own name is the reported form: it is what a caller routes on and what fills the
* {@code error.type} span attribute, so it has to read the same here as it does in the other ADK
* languages.
*/
public enum ToolErrorType {
BAD_REQUEST,
UNAUTHORIZED,
FORBIDDEN,
NOT_FOUND,
REQUEST_TIMEOUT,
INTERNAL_SERVER_ERROR,
BAD_GATEWAY,
SERVICE_UNAVAILABLE,
GATEWAY_TIMEOUT
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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.tools;

import java.util.Optional;
import org.jspecify.annotations.Nullable;

/**
* Indicates that a tool failed while it was running.
*
* <p>A failure carries two things. The message is a sentence a user and a model both end up
* reading, so it should name the tool and say what went wrong. The error type beside it is the
* machine-readable half: it lets a caller tell a timeout apart from a refusal without parsing the
* prose, and it is what fills the {@code error.type} span attribute. Classifying a failure is
* optional, and one that was not classified reports {@link Optional#empty()}.
*
* <p>No constructor accepts a null error type. A failure with nothing to say about how it failed
* uses {@link #ToolExecutionException(String)} or {@link #ToolExecutionException(String,
* Throwable)} instead.
*
* <p>This is unchecked, so a tool body can fail inside an RxJava operator and reach the subscriber
* intact.
*/
public class ToolExecutionException extends RuntimeException {

private final @Nullable ToolErrorType errorType;

/** Reports a failure with no classification. */
public ToolExecutionException(String message) {
super(message);
this.errorType = null;
}

/** Reports a failure with no classification, brought about by {@code cause}. */
public ToolExecutionException(String message, Throwable cause) {
super(message, cause);
this.errorType = null;
}

/** Reports a failure classified as {@code errorType}. */
public ToolExecutionException(String message, ToolErrorType errorType) {
super(message);
this.errorType = errorType;
}

/** Reports a failure classified as {@code errorType}, brought about by {@code cause}. */
public ToolExecutionException(String message, ToolErrorType errorType, Throwable cause) {
super(message, cause);
this.errorType = errorType;
}

/** How the tool failed, empty if the failure was never classified. */
public Optional<ToolErrorType> errorType() {
return Optional.ofNullable(errorType);
}
}
115 changes: 115 additions & 0 deletions core/src/main/java/com/google/adk/tools/TransferToAgentTool.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* 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.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.genai.types.FunctionDeclaration;
import com.google.genai.types.Schema;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
* A {@link FunctionTool} that hands the conversation to another agent, restricted to the agents
* that exist.
*
* <p>The {@code agent_name} parameter carries those agents as an enum in its schema, so a model
* cannot transfer to an agent it was never offered.
*
* <p>This is the one definition of the {@code transfer_to_agent} tool. The auto flow's transfer
* request processor builds one of these per turn, from the agents that turn can reach, so what the
* model is told about transferring is written in exactly one place.
*/
public final class TransferToAgentTool extends FunctionTool {

private static final String AGENT_NAME_PARAMETER = "agent_name";

private static final Method TRANSFER_TO_AGENT = transferToAgentMethod();

private final Optional<FunctionDeclaration> declaration;

/**
* Returns a transfer tool that offers the model exactly {@code agentNames} to transfer to.
*
* @param agentNames the valid agent names that can be transferred to.
*/
public static TransferToAgentTool create(List<String> agentNames) {
return new TransferToAgentTool(ImmutableList.copyOf(agentNames));
}

private TransferToAgentTool(ImmutableList<String> agentNames) {
super(/* instance= */ null, TRANSFER_TO_AGENT, /* isLongRunning= */ false);
this.declaration = super.declaration().map(decl -> restrictAgentName(decl, agentNames));
}

@Override
public Optional<FunctionDeclaration> declaration() {
return declaration;
}

/**
* Returns {@code declaration} with its agent name parameter constrained to {@code agentNames}.
*/
private static FunctionDeclaration restrictAgentName(
FunctionDeclaration declaration, ImmutableList<String> agentNames) {
Schema parameters = declaration.parameters().orElse(null);
if (parameters == null) {
return declaration;
}
Map<String, Schema> properties = parameters.properties().orElse(ImmutableMap.of());
Schema agentName = properties.get(AGENT_NAME_PARAMETER);
if (agentName == null) {
return declaration;
}
Map<String, Schema> restricted = new LinkedHashMap<>(properties);
restricted.put(AGENT_NAME_PARAMETER, agentName.toBuilder().enum_(agentNames).build());
return declaration.toBuilder()
.parameters(parameters.toBuilder().properties(restricted).build())
.build();
}

private static Method transferToAgentMethod() {
try {
return TransferToAgentTool.class.getMethod(
"transferToAgent", String.class, ToolContext.class);
} catch (NoSuchMethodException e) {
throw new IllegalStateException(e);
}
}

@Annotations.Schema(
name = "transfer_to_agent",
description =
"""
Transfer the query to another agent.

Use this tool to hand off control to another agent that is more suitable to
answer the user's query according to the agent's description.

Args:
agent_name: the agent name to transfer to.
\
""")
public static void transferToAgent(
@Annotations.Schema(name = AGENT_NAME_PARAMETER) String agentName,
@Annotations.Schema(optional = true) ToolContext toolContext) {
toolContext.setActions(toolContext.actions().toBuilder().transferToAgent(agentName).build());
}
}
Loading
Loading