Skip to content

Commit d827eae

Browse files
Doris26copybara-github
authored andcommitted
feat: Add support for configuring subagents in ADK agents via YAML
BaseAgentConfig now supports defining subagents.LlmAgent and ConfigAgentUtils are updated to load and link subagents from configuration files. Adds a multi_agent example demonstrating hierarchical agent setup. PiperOrigin-RevId: 800967504
1 parent 97f02ab commit d827eae

File tree

5 files changed

+350
-5
lines changed

5 files changed

+350
-5
lines changed

core/src/main/java/com/google/adk/agents/BaseAgentConfig.java

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,121 @@
1717
package com.google.adk.agents;
1818

1919
import com.fasterxml.jackson.annotation.JsonProperty;
20+
import java.util.List;
2021

2122
/**
22-
* Base configuration for all agents.
23+
* Base configuration for all agents with subagent support.
2324
*
2425
* <p>TODO: Config agent features are not yet ready for public use.
2526
*/
2627
public class BaseAgentConfig {
2728
private String name;
2829
private String description = "";
2930
private String agentClass;
31+
private List<AgentRefConfig> subAgents;
32+
33+
/**
34+
* Configuration for referencing other agents (subagents). Supports both config-based references
35+
* (YAML files) and programmatic references (Java classes).
36+
*/
37+
public static class AgentRefConfig {
38+
private String name;
39+
private String configPath;
40+
private String className;
41+
private String staticField;
42+
43+
public AgentRefConfig() {}
44+
45+
/**
46+
* Constructor for config-based agent reference.
47+
*
48+
* @param name The name of the subagent
49+
* @param configPath The path to the subagent's config file
50+
*/
51+
public AgentRefConfig(String name, String configPath) {
52+
this.name = name;
53+
this.configPath = configPath;
54+
}
55+
56+
/**
57+
* Constructor for programmatic agent reference.
58+
*
59+
* @param name The name of the subagent
60+
* @param className The Java class name
61+
* @param staticField Optional static field name
62+
*/
63+
public AgentRefConfig(String name, String className, String staticField) {
64+
this.name = name;
65+
this.className = className;
66+
this.staticField = staticField;
67+
}
68+
69+
@JsonProperty("name")
70+
public String name() {
71+
return name;
72+
}
73+
74+
public void setName(String name) {
75+
this.name = name;
76+
}
77+
78+
@JsonProperty("config_path")
79+
public String configPath() {
80+
return configPath;
81+
}
82+
83+
public void setConfigPath(String configPath) {
84+
this.configPath = configPath;
85+
}
86+
87+
@JsonProperty("class_name")
88+
public String className() {
89+
return className;
90+
}
91+
92+
public void setClassName(String className) {
93+
this.className = className;
94+
}
95+
96+
@JsonProperty("static_field")
97+
public String staticField() {
98+
return staticField;
99+
}
100+
101+
public void setStaticField(String staticField) {
102+
this.staticField = staticField;
103+
}
104+
105+
@Override
106+
public String toString() {
107+
if (configPath != null) {
108+
return "AgentRefConfig{name='" + name + "', configPath='" + configPath + "'}";
109+
} else {
110+
return "AgentRefConfig{name='"
111+
+ name
112+
+ "', className='"
113+
+ className
114+
+ "', staticField='"
115+
+ staticField
116+
+ "'}";
117+
}
118+
}
119+
}
120+
121+
public BaseAgentConfig() {}
122+
123+
/**
124+
* Constructor with basic fields.
125+
*
126+
* @param name The agent name
127+
* @param description The agent description
128+
* @param agentClass The agent class name
129+
*/
130+
public BaseAgentConfig(String name, String description, String agentClass) {
131+
this.name = name;
132+
this.description = description;
133+
this.agentClass = agentClass;
134+
}
30135

31136
@JsonProperty(value = "name", required = true)
32137
public String name() {
@@ -54,4 +159,13 @@ public String agentClass() {
54159
public void setAgentClass(String agentClass) {
55160
this.agentClass = agentClass;
56161
}
162+
163+
@JsonProperty("sub_agents")
164+
public List<AgentRefConfig> subAgents() {
165+
return subAgents;
166+
}
167+
168+
public void setSubAgents(List<AgentRefConfig> subAgents) {
169+
this.subAgents = subAgents;
170+
}
57171
}

core/src/main/java/com/google/adk/agents/ConfigAgentUtils.java

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,16 @@
2020
import com.fasterxml.jackson.databind.ObjectMapper;
2121
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
2222
import com.google.adk.utils.ComponentRegistry;
23+
import com.google.common.collect.ImmutableList;
2324
import java.io.File;
2425
import java.io.FileInputStream;
2526
import java.io.IOException;
2627
import java.io.InputStream;
28+
import java.nio.file.Files;
29+
import java.nio.file.Path;
30+
import java.nio.file.Paths;
31+
import java.util.ArrayList;
32+
import java.util.List;
2733
import org.slf4j.Logger;
2834
import org.slf4j.LoggerFactory;
2935

@@ -78,6 +84,99 @@ public static BaseAgent fromConfig(String configPath) throws ConfigurationExcept
7884
}
7985
}
8086

87+
/**
88+
* Resolves subagent configurations into actual BaseAgent instances. This method is used by
89+
* concrete agent implementations to resolve their subagents.
90+
*
91+
* @param subAgentConfigs The list of subagent configurations
92+
* @param configAbsPath The absolute path to the parent config file for resolving relative paths
93+
* @return A list of resolved BaseAgent instances
94+
* @throws ConfigurationException if any subagent fails to resolve
95+
*/
96+
public static ImmutableList<BaseAgent> resolveSubAgents(
97+
List<BaseAgentConfig.AgentRefConfig> subAgentConfigs, String configAbsPath)
98+
throws ConfigurationException {
99+
100+
if (subAgentConfigs == null || subAgentConfigs.isEmpty()) {
101+
return ImmutableList.of();
102+
}
103+
104+
List<BaseAgent> resolvedSubAgents = new ArrayList<>();
105+
Path configDir = Paths.get(configAbsPath).getParent();
106+
107+
for (BaseAgentConfig.AgentRefConfig subAgentConfig : subAgentConfigs) {
108+
try {
109+
BaseAgent subAgent = resolveSubAgent(subAgentConfig, configDir);
110+
resolvedSubAgents.add(subAgent);
111+
logger.debug("Successfully resolved subagent: {}", subAgent.name());
112+
} catch (Exception e) {
113+
String errorMsg =
114+
"Failed to resolve subagent: "
115+
+ (subAgentConfig.name() != null ? subAgentConfig.name() : "unnamed");
116+
logger.error(errorMsg, e);
117+
throw new ConfigurationException(errorMsg, e);
118+
}
119+
}
120+
121+
return ImmutableList.copyOf(resolvedSubAgents);
122+
}
123+
124+
/**
125+
* Resolves a single subagent configuration into a BaseAgent instance.
126+
*
127+
* @param subAgentConfig The subagent configuration
128+
* @param configDir The directory containing the parent config file
129+
* @return The resolved BaseAgent instance
130+
* @throws ConfigurationException if the subagent cannot be resolved
131+
*/
132+
private static BaseAgent resolveSubAgent(
133+
BaseAgentConfig.AgentRefConfig subAgentConfig, Path configDir) throws ConfigurationException {
134+
135+
if (subAgentConfig.configPath() != null && !subAgentConfig.configPath().trim().isEmpty()) {
136+
return resolveSubAgentFromConfigPath(subAgentConfig, configDir);
137+
}
138+
139+
// TODO: Add support for programmatic subagent resolution (className/staticField).
140+
if (subAgentConfig.className() != null || subAgentConfig.staticField() != null) {
141+
throw new ConfigurationException(
142+
"Programmatic subagent resolution (className/staticField) is not yet supported for"
143+
+ " subagent: "
144+
+ subAgentConfig.name());
145+
}
146+
147+
throw new ConfigurationException(
148+
"Subagent configuration for '"
149+
+ subAgentConfig.name()
150+
+ "' must specify 'configPath'."
151+
+ " Programmatic references (className/staticField) are not yet supported.");
152+
}
153+
154+
/** Resolves a subagent from a configuration file path. */
155+
private static BaseAgent resolveSubAgentFromConfigPath(
156+
BaseAgentConfig.AgentRefConfig subAgentConfig, Path configDir) throws ConfigurationException {
157+
158+
String configPath = subAgentConfig.configPath().trim();
159+
Path subAgentConfigPath;
160+
161+
if (Path.of(configPath).isAbsolute()) {
162+
subAgentConfigPath = Path.of(configPath);
163+
} else {
164+
subAgentConfigPath = configDir.resolve(configPath);
165+
}
166+
167+
if (!Files.exists(subAgentConfigPath)) {
168+
throw new ConfigurationException("Subagent config file not found: " + subAgentConfigPath);
169+
}
170+
171+
try {
172+
// Recursive call to load the subagent from its config file
173+
return fromConfig(subAgentConfigPath.toString());
174+
} catch (Exception e) {
175+
throw new ConfigurationException(
176+
"Failed to load subagent from config: " + subAgentConfigPath, e);
177+
}
178+
}
179+
81180
/**
82181
* Load configuration from a YAML file path as a specific type.
83182
*

core/src/main/java/com/google/adk/agents/LlmAgent.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -882,14 +882,13 @@ private Model resolveModelInternal() {
882882
}
883883

884884
/**
885-
* Creates an LlmAgent from configuration.
885+
* Creates an LlmAgent from configuration with full subagent support.
886886
*
887887
* @param config the agent configuration
888888
* @param configAbsPath The absolute path to the agent config file. This is needed for resolving
889-
* relative paths for e.g. tools.
889+
* relative paths for e.g. tools and subagents.
890890
* @return the configured LlmAgent
891891
* @throws ConfigurationException if the configuration is invalid
892-
* <p>TODO: Config agent features are not yet ready for public use.
893892
*/
894893
public static LlmAgent fromConfig(LlmAgentConfig config, String configAbsPath)
895894
throws ConfigurationException {
@@ -922,6 +921,12 @@ public static LlmAgent fromConfig(LlmAgentConfig config, String configAbsPath)
922921
} catch (ConfigurationException e) {
923922
throw new ConfigurationException("Error resolving tools for agent " + config.name(), e);
924923
}
924+
// Resolve and add subagents using the utility class
925+
if (config.subAgents() != null && !config.subAgents().isEmpty()) {
926+
ImmutableList<BaseAgent> subAgents =
927+
ConfigAgentUtils.resolveSubAgents(config.subAgents(), configAbsPath);
928+
builder.subAgents(subAgents);
929+
}
925930

926931
// Set optional transfer configuration
927932
if (config.disallowTransferToParent() != null) {
@@ -939,7 +944,10 @@ public static LlmAgent fromConfig(LlmAgentConfig config, String configAbsPath)
939944

940945
// Build and return the agent
941946
LlmAgent agent = builder.build();
942-
logger.info("Successfully created LlmAgent: {}", agent.name());
947+
logger.info(
948+
"Successfully created LlmAgent: {} with {} subagents",
949+
agent.name(),
950+
agent.subAgents() != null ? agent.subAgents().size() : 0);
943951

944952
return agent;
945953
}

core/src/test/java/com/google/adk/agents/BaseAgentTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ public final class BaseAgentTest {
3838
private static final String TEST_AGENT_NAME = "testAgent";
3939
private static final String TEST_AGENT_DESCRIPTION = "A test agent";
4040

41+
@Test
42+
public void constructor_setsNameAndDescription() {
43+
String name = "testName";
44+
String description = "testDescription";
45+
TestBaseAgent agent = new TestBaseAgent(name, description, ImmutableList.of(), null, null);
46+
47+
assertThat(agent.name()).isEqualTo(name);
48+
assertThat(agent.description()).isEqualTo(description);
49+
}
50+
4151
@Test
4252
public void
4353
runAsync_beforeAgentCallbackReturnsContent_endsInvocationAndSkipsRunAsyncImplAndAfterCallback() {

0 commit comments

Comments
 (0)