Skip to content

Commit dbc973b

Browse files
damianmomotgooglecopybara-github
authored andcommitted
feat: make BaseAgent extend BaseNode so every agent is a workflow node
PiperOrigin-RevId: 997839818
1 parent a358de8 commit dbc973b

3 files changed

Lines changed: 78 additions & 38 deletions

File tree

‎core/src/main/java/com/google/adk/agents/BaseAgent.java‎

Lines changed: 26 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,21 @@
1818

1919
import static com.google.common.base.Preconditions.checkArgument;
2020
import static com.google.common.base.Strings.isNullOrEmpty;
21+
import static com.google.common.base.Strings.nullToEmpty;
2122
import static com.google.common.collect.ImmutableList.toImmutableList;
2223
import static java.lang.String.format;
2324

2425
import com.google.adk.agents.Callbacks.AfterAgentCallback;
2526
import com.google.adk.agents.Callbacks.BeforeAgentCallback;
27+
import com.google.adk.annotations.Experimental;
2628
import com.google.adk.events.Event;
2729
import com.google.adk.events.EventActions;
2830
import com.google.adk.plugins.Plugin;
2931
import com.google.adk.telemetry.Instrumentation;
3032
import com.google.adk.telemetry.Instrumentation.AgentInvocation;
3133
import com.google.adk.telemetry.Tracing;
3234
import com.google.adk.utils.AgentEnums.AgentOrigin;
35+
import com.google.adk.workflow.BaseNode;
3336
import com.google.common.collect.ImmutableList;
3437
import com.google.errorprone.annotations.CanIgnoreReturnValue;
3538
import com.google.errorprone.annotations.DoNotCall;
@@ -49,21 +52,12 @@
4952
import org.jspecify.annotations.Nullable;
5053

5154
/** Base class for all agents. */
52-
public abstract class BaseAgent {
55+
public abstract class BaseAgent extends BaseNode {
5356

5457
// Pattern for valid agent names.
5558
private static final String IDENTIFIER_REGEX = "^_?[a-zA-Z0-9]*([. _-][a-zA-Z0-9]+)*$";
5659
private static final Pattern IDENTIFIER_PATTERN = Pattern.compile(IDENTIFIER_REGEX);
5760

58-
/** The agent's name. Must be a unique identifier within the agent tree. */
59-
private final String name;
60-
61-
/**
62-
* One line description about the agent's capability. The system can use this for decision-making
63-
* when delegating control to different agents.
64-
*/
65-
private final String description;
66-
6761
/**
6862
* The parent agent in the agent tree. Note that one agent cannot be added to two different
6963
* parents' sub-agents lists.
@@ -79,7 +73,7 @@ public abstract class BaseAgent {
7973
* Creates a new BaseAgent.
8074
*
8175
* @param name Unique agent name. Cannot be "user" (reserved).
82-
* @param description Agent purpose.
76+
* @param description Agent purpose; null is treated as an empty string.
8377
* @param subAgents Agents managed by this agent.
8478
* @param beforeAgentCallback Callbacks before agent execution. Invoked in order until one doesn't
8579
* return null.
@@ -88,16 +82,14 @@ public abstract class BaseAgent {
8882
*/
8983
public BaseAgent(
9084
String name,
91-
String description,
85+
@Nullable String description,
9286
@Nullable List<? extends BaseAgent> subAgents,
9387
@Nullable List<? extends BeforeAgentCallback> beforeAgentCallback,
9488
@Nullable List<? extends AfterAgentCallback> afterAgentCallback) {
95-
validateAgentName(name);
96-
this.name = name;
97-
this.description = description;
89+
super(validateAgentName(name), nullToEmpty(description));
9890
this.parentAgent = null;
9991
this.subAgents = (subAgents != null) ? ImmutableList.copyOf(subAgents) : ImmutableList.of();
100-
validateSubAgents(this.name, this.subAgents);
92+
validateSubAgents(name, this.subAgents);
10193
this.beforeAgentCallback =
10294
(beforeAgentCallback != null)
10395
? ImmutableList.copyOf(beforeAgentCallback)
@@ -128,10 +120,12 @@ public Completable close() {
128120
* Validates the agent name.
129121
*
130122
* @param name The agent name to validate.
123+
* @return the validated name.
131124
* @throws IllegalArgumentException if the agent name is null, empty, or does not match the
132125
* identifier pattern.
133126
*/
134-
private static void validateAgentName(String name) {
127+
@CanIgnoreReturnValue
128+
private static String validateAgentName(String name) {
135129
if (isNullOrEmpty(name)) {
136130
throw new IllegalArgumentException("Agent name cannot be null or empty.");
137131
}
@@ -141,6 +135,7 @@ private static void validateAgentName(String name) {
141135
}
142136
checkArgument(
143137
!name.equals(Role.USER), "Agent name cannot be 'user'; reserved for end-user input.");
138+
return name;
144139
}
145140

146141
/**
@@ -173,24 +168,6 @@ private static void validateSubAgents(
173168
}
174169
}
175170

176-
/**
177-
* Gets the agent's unique name.
178-
*
179-
* @return the unique name of the agent.
180-
*/
181-
public final String name() {
182-
return name;
183-
}
184-
185-
/**
186-
* Gets the one-line description of the agent's capability.
187-
*
188-
* @return the description of the agent.
189-
*/
190-
public final String description() {
191-
return description;
192-
}
193-
194171
/**
195172
* Retrieves the parent agent in the agent tree.
196173
*
@@ -312,7 +289,7 @@ private InvocationContext createInvocationContext(InvocationContext parentContex
312289
* @return stream of agent-generated events.
313290
*/
314291
public Flowable<Event> runAsync(InvocationContext parentContext) {
315-
return run(parentContext, this::runAsyncImpl);
292+
return runWithCallbacks(parentContext, this::runAsyncImpl);
316293
}
317294

318295
/**
@@ -322,7 +299,7 @@ public Flowable<Event> runAsync(InvocationContext parentContext) {
322299
* @param runImplementation The agent-specific logic to run.
323300
* @return stream of agent-generated events.
324301
*/
325-
private Flowable<Event> run(
302+
private Flowable<Event> runWithCallbacks(
326303
InvocationContext parentContext,
327304
Function<InvocationContext, Flowable<Event>> runImplementation) {
328305
return Flowable.using(
@@ -468,7 +445,7 @@ private Maybe<Event> callCallback(
468445
* @return stream of agent-generated events.
469446
*/
470447
public Flowable<Event> runLive(InvocationContext parentContext) {
471-
return run(parentContext, this::runLiveImpl);
448+
return runWithCallbacks(parentContext, this::runLiveImpl);
472449
}
473450

474451
/**
@@ -510,6 +487,17 @@ private Event checkpointEvent(InvocationContext context, EventActions actions) {
510487
.build();
511488
}
512489

490+
/**
491+
* Runs this agent as a workflow node through its {@link #runAsync} lifecycle, including its
492+
* callbacks. The node input is not used.
493+
*/
494+
@Experimental
495+
@Override
496+
public Flowable<Event> runNode(
497+
com.google.adk.agents.Context context, @Nullable Object nodeInput) {
498+
return runAsync(context.invocationContext());
499+
}
500+
513501
/**
514502
* Agent-specific asynchronous logic.
515503
*

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,10 +1029,12 @@ public List<? extends OnToolErrorCallback> canonicalOnToolErrorCallbacks() {
10291029
return onToolErrorCallback;
10301030
}
10311031

1032+
@Override
10321033
public Optional<Schema> inputSchema() {
10331034
return inputSchema;
10341035
}
10351036

1037+
@Override
10361038
public Optional<Schema> outputSchema() {
10371039
return outputSchema;
10381040
}

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.google.adk.testing.TestBaseAgent;
2727
import com.google.adk.testing.TestCallback;
2828
import com.google.adk.testing.TestUtils;
29+
import com.google.adk.workflow.Node;
2930
import com.google.common.collect.ImmutableList;
3031
import com.google.genai.types.Content;
3132
import com.google.genai.types.Part;
@@ -112,6 +113,13 @@ public void constructor_setsNameAndDescription() {
112113
assertThat(agent.description()).isEqualTo(description);
113114
}
114115

116+
@Test
117+
public void constructor_nullDescription_setsEmptyDescription() {
118+
TestBaseAgent agent = new TestBaseAgent("testName", null, null, ImmutableList.of(), null, null);
119+
120+
assertThat(agent.description()).isEmpty();
121+
}
122+
115123
@Test
116124
public void findAgent_returnsCorrectAgent() {
117125
TestBaseAgent subSubAgent =
@@ -399,6 +407,34 @@ public void runAsync_firstAfterCallbackReturnsContent_skipsSecondAfterCallback()
399407
assertThat(afterCallback2.wasCalled()).isFalse();
400408
}
401409

410+
@Test
411+
public void runNode_runsAgentWithItsCallbacks() {
412+
var runAsyncImpl = TestCallback.<Void>returningEmpty();
413+
Content runAsyncImplContent = Content.fromParts(Part.fromText("main_output"));
414+
Content afterCallbackContent = Content.fromParts(Part.fromText("after_callback_output"));
415+
var beforeCallback = TestCallback.<Content>returningEmpty();
416+
var afterCallback = TestCallback.returning(afterCallbackContent);
417+
TestBaseAgent agent =
418+
new TestBaseAgent(
419+
TEST_AGENT_NAME,
420+
TEST_AGENT_DESCRIPTION,
421+
ImmutableList.of(beforeCallback.asBeforeAgentCallback()),
422+
ImmutableList.of(afterCallback.asAfterAgentCallback()),
423+
runAsyncImpl.asRunAsyncImplSupplier(runAsyncImplContent));
424+
Context context =
425+
new CallbackContext(TestUtils.createInvocationContext(agent), /* eventActions= */ null);
426+
Node node = agent;
427+
428+
List<Event> results =
429+
node.runNode(context, /* nodeInput= */ "unused").cast(Event.class).toList().blockingGet();
430+
431+
assertThat(results).hasSize(2);
432+
assertThat(results.get(0).content()).hasValue(runAsyncImplContent);
433+
assertThat(results.get(1).content()).hasValue(afterCallbackContent);
434+
assertThat(beforeCallback.wasCalled()).isTrue();
435+
assertThat(runAsyncImpl.wasCalled()).isTrue();
436+
}
437+
402438
@Test
403439
public void canonicalCallbacks_returnsEmptyListWhenNull() {
404440
TestBaseAgent agent =
@@ -637,6 +673,20 @@ public void runLive_firstAfterCallbackReturnsContent_skipsSecondAfterCallback()
637673
assertThat(afterCallback2.wasCalled()).isFalse();
638674
}
639675

676+
@Test
677+
public void constructor_nullName_throwsIllegalArgumentException() {
678+
assertThrows(
679+
IllegalArgumentException.class,
680+
() -> new TestBaseAgent(null, "description", null, null, null));
681+
}
682+
683+
@Test
684+
public void constructor_emptyName_throwsIllegalArgumentException() {
685+
assertThrows(
686+
IllegalArgumentException.class,
687+
() -> new TestBaseAgent("", "description", null, null, null));
688+
}
689+
640690
@Test
641691
public void constructor_invalidName_throwsIllegalArgumentException() {
642692
assertThrows(

0 commit comments

Comments
 (0)