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
6 changes: 4 additions & 2 deletions core/src/main/java/com/google/adk/agents/LlmAgent.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.adk.agents;

import static com.google.common.base.Strings.isNullOrEmpty;
Expand Down Expand Up @@ -56,6 +55,7 @@
import com.google.adk.tools.BaseTool.ToolArgsConfig;
import com.google.adk.tools.BaseTool.ToolConfig;
import com.google.adk.tools.BaseToolset;
import com.google.adk.tools.internal.ToolConstraints;
import com.google.adk.utils.ComponentRegistry;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
Expand Down Expand Up @@ -626,7 +626,9 @@ protected void validate() {

public LlmAgent build() {
validate();
return new LlmAgent(this);
LlmAgent built = new LlmAgent(this);
ToolConstraints.validateAgentTree(built);
return built;
}
}

Expand Down
19 changes: 19 additions & 0 deletions core/src/main/java/com/google/adk/tools/BuiltInTool.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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;

/** Marker interface for built-in (special) tools with framework-level constraints. */
public interface BuiltInTool {}
150 changes: 150 additions & 0 deletions core/src/main/java/com/google/adk/tools/internal/ToolConstraints.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* 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.internal;

import com.google.adk.agents.BaseAgent;
import com.google.adk.agents.LlmAgent;
import com.google.adk.tools.BuiltInTool;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/** Guardrails for built-in tools usage. */
public final class ToolConstraints {

private static final String DOC =
"See docs: https://google.github.io/adk-docs/tools/built-in-tools/#limitations";

private ToolConstraints() {}

/** Public entry: validate the whole agent subtree. */
public static void validateAgentTree(BaseAgent agent) {
Objects.requireNonNull(agent, "agent");
if (!(agent instanceof LlmAgent llm)) {
// Only validate tool combinations for LlmAgent
return;
}
validateSingleAgent(llm);
for (BaseAgent child : subAgentsCompat(llm)) {
validateAgentTree(child);
}
}

/** Validate constraints on a single LlmAgent. */
private static void validateSingleAgent(LlmAgent agent) {
List<Object> tools = declaredToolsCompat(agent);

long builtinCount = tools.stream().filter(t -> t instanceof BuiltInTool).count();
boolean hasBuiltin = builtinCount > 0;
boolean hasOthers = tools.stream().anyMatch(t -> !(t instanceof BuiltInTool));

// A1: Only one built-in tool is allowed in a single Agent, and it cannot coexist with other
// tools.
if (builtinCount > 1 || (hasBuiltin && hasOthers)) {
throw new IllegalStateException(errorMixedBuiltins(agent.name(), tools));
}

// A2: Subagents are not allowed to use built-in tools
if (hasBuiltin && parentAgentCompat(agent) != null) {
throw new IllegalStateException(errorBuiltinInSubAgent(agent.name()));
}
}

// ---------- Friendly error messages ----------

private static String errorMixedBuiltins(String agentName, List<Object> tools) {
String toolNames =
tools.stream()
.map(t -> t == null ? "null" : t.getClass().getSimpleName())
.collect(Collectors.joining(", "));
return "[Built-in tools limitation violated] Agent `"
+ agentName
+ "` has invalid tool mix: "
+ toolNames
+ ". Only one built-in tool allowed, and it cannot be used with other tools. "
+ DOC;
}

private static String errorBuiltinInSubAgent(String agentName) {
return "[Built-in tools limitation violated] Sub-agent `"
+ agentName
+ "` cannot use a built-in tool. Built-ins must be used only at the root/single agent. "
+ DOC;
}

// ---------- Compatibility helpers (avoid hard-coding method names) ----------

/** Return the declared tools list using best-effort method discovery. */
@SuppressWarnings("unchecked")
private static List<Object> declaredToolsCompat(LlmAgent agent) {
// Priority: tools() → declaredTools() → getTools()
for (String m : new String[] {"tools", "declaredTools", "getTools"}) {
try {
var mh =
MethodHandles.publicLookup()
.findVirtual(agent.getClass(), m, MethodType.methodType(List.class));
Object result = mh.invoke(agent);
if (result instanceof List<?>) {
return new ArrayList<>((List<?>) result);
}
} catch (Throwable ignore) {
// try next
}
}
// Returns empty if not found
return Collections.emptyList();
}

/** Return sub agents using best-effort method discovery. */
@SuppressWarnings("unchecked")
private static List<BaseAgent> subAgentsCompat(LlmAgent agent) {
for (String m : new String[] {"subAgents", "children", "getSubAgents"}) {
try {
var mh =
MethodHandles.publicLookup()
.findVirtual(agent.getClass(), m, MethodType.methodType(List.class));
Object result = mh.invoke(agent);
if (result instanceof List<?>) {
List<?> raw = (List<?>) result;
return raw.stream().filter(e -> e instanceof BaseAgent).map(e -> (BaseAgent) e).toList();
}
} catch (Throwable ignore) {
// try next
}
}
return Collections.emptyList();
}

/** Return parent agent if available. */
private static BaseAgent parentAgentCompat(LlmAgent agent) {
for (String m : new String[] {"parentAgent", "getParent", "getParentAgent"}) {
try {
var mh =
MethodHandles.publicLookup()
.findVirtual(agent.getClass(), m, MethodType.methodType(BaseAgent.class));
Object result = mh.invoke(agent);
return (result instanceof BaseAgent) ? (BaseAgent) result : null;
} catch (Throwable ignore) {
// try next
}
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -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.tools.internal;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

import com.google.adk.agents.BaseAgent;
import com.google.adk.agents.LlmAgent;
import com.google.adk.tools.BuiltInTool;
import java.util.List;
import org.junit.jupiter.api.Test;

/** Unit tests for ToolConstraints guardrails (pure white-box, no BaseTool dependency). */
final class ToolConstraintsTest {

// only rely on the BuiltInTool marker interface to distinguish whether it is built-in
static final class FakeBuiltinTool implements BuiltInTool {}

static final class FakeNormalTool {}

@SuppressWarnings("unchecked")
private static LlmAgent mockAgent(
String name, BaseAgent parent, List<?> tools, List<BaseAgent> subs) {

LlmAgent a = mock(LlmAgent.class);

when(a.name()).thenReturn(name);

when(a.parentAgent()).thenReturn(parent);
when(a.subAgents()).thenReturn((List) subs);
when(a.tools()).thenReturn((List) tools);

return a;
}

@Test
void singleBuiltin_only_passes() {
LlmAgent root = mockAgent("root", null, List.of(new FakeBuiltinTool()), List.of());
assertDoesNotThrow(() -> ToolConstraints.validateAgentTree(root));
}

@Test
void builtin_plus_normal_throws() {
LlmAgent root =
mockAgent("root", null, List.of(new FakeBuiltinTool(), new FakeNormalTool()), List.of());
assertThrows(IllegalStateException.class, () -> ToolConstraints.validateAgentTree(root));
}

@Test
void two_builtins_throws() {
LlmAgent root =
mockAgent("root", null, List.of(new FakeBuiltinTool(), new FakeBuiltinTool()), List.of());
assertThrows(IllegalStateException.class, () -> ToolConstraints.validateAgentTree(root));
}

@Test
void builtin_in_subAgent_throws() {
LlmAgent child =
mockAgent("child", mock(LlmAgent.class), List.of(new FakeBuiltinTool()), List.of());
LlmAgent root = mockAgent("root", null, List.of(new FakeNormalTool()), List.of(child));
assertThrows(IllegalStateException.class, () -> ToolConstraints.validateAgentTree(root));
}

@Test
void only_normal_tools_in_tree_passes() {
LlmAgent child =
mockAgent("child", mock(LlmAgent.class), List.of(new FakeNormalTool()), List.of());
LlmAgent root = mockAgent("root", null, List.of(new FakeNormalTool()), List.of(child));
assertDoesNotThrow(() -> ToolConstraints.validateAgentTree(root));
}
}