Skip to content

Commit 60005a3

Browse files
damianmomotgooglecopybara-github
authored andcommitted
feat: run a workflow graph
PiperOrigin-RevId: 1000294791
1 parent 1ae5078 commit 60005a3

23 files changed

Lines changed: 2946 additions & 74 deletions

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,8 @@ public ImmutableList<? extends AfterAgentCallback> canonicalAfterAgentCallbacks(
276276
private InvocationContext createInvocationContext(
277277
InvocationContext parentContext, boolean keepBranch) {
278278
InvocationContext.Builder builder = parentContext.toBuilder();
279-
builder.agent(this);
279+
// An agent's context runs that agent, not the node that its parent context ran.
280+
builder.agent(this).node(null);
280281
if (!keepBranch) {
281282
// Check for branch to be truthy (not None, not empty string),
282283
parentContext

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import com.google.adk.sessions.BaseSessionService;
3434
import com.google.adk.sessions.Session;
3535
import com.google.adk.summarizer.EventsCompactionConfig;
36+
import com.google.adk.workflow.Node;
3637
import com.google.common.collect.ImmutableList;
3738
import com.google.common.collect.ImmutableMap;
3839
import com.google.common.collect.ImmutableSet;
@@ -84,6 +85,7 @@ public class InvocationContext {
8485

8586
@Nullable private String branch;
8687
private BaseAgent agent;
88+
private final @Nullable Node node;
8789
private boolean endInvocation;
8890

8991
protected InvocationContext(Builder builder) {
@@ -97,6 +99,7 @@ protected InvocationContext(Builder builder) {
9799
// Resolved here so subclasses that construct from a builder without build() also get an id.
98100
this.invocationId = builder.resolveInvocationId();
99101
this.agent = builder.agent;
102+
this.node = builder.node;
100103
this.session = builder.session;
101104
this.userContent = builder.userContent;
102105
this.runConfig = builder.runConfig;
@@ -182,6 +185,15 @@ public BaseAgent agent() {
182185
return agent;
183186
}
184187

188+
/**
189+
* Returns the workflow node this invocation runs, or empty when it runs an agent. When it runs a
190+
* node, {@link #agent()} returns a {@link NodeViewAgent} that stands in for it.
191+
*/
192+
@Experimental
193+
public Optional<Node> node() {
194+
return Optional.ofNullable(node);
195+
}
196+
185197
/** Returns the session associated with this invocation. */
186198
public Session session() {
187199
return session;
@@ -653,6 +665,7 @@ private Builder(InvocationContext context) {
653665
this.invocationId = context.invocationId;
654666
this.invocationIdSet = true;
655667
this.agent = context.agent;
668+
this.node = context.node;
656669
this.session = context.session;
657670
this.userContent = context.userContent;
658671
this.runConfig = context.runConfig;
@@ -684,6 +697,7 @@ private Builder(InvocationContext context) {
684697
// Distinguishes "never set" (an id is minted from the UuidProvider) from an explicit null.
685698
private boolean invocationIdSet;
686699
private BaseAgent agent;
700+
private @Nullable Node node;
687701
private Session session;
688702
@Nullable private Content userContent = null;
689703
private RunConfig runConfig = RunConfig.builder().build();
@@ -796,6 +810,17 @@ public Builder agent(BaseAgent agent) {
796810
return this;
797811
}
798812

813+
/**
814+
* Sets the workflow node the invocation runs, or null when it runs an agent. When the
815+
* invocation runs a node, its agent is a {@link NodeViewAgent} that stands in for the node.
816+
*/
817+
@CanIgnoreReturnValue
818+
@Experimental
819+
public Builder node(@Nullable Node node) {
820+
this.node = node;
821+
return this;
822+
}
823+
799824
/**
800825
* Sets the session associated with this invocation.
801826
*
@@ -968,6 +993,9 @@ private static void validate(Builder builder) {
968993
if (builder.agent == null) {
969994
throw new IllegalStateException("Agent must be set.");
970995
}
996+
if (builder.agent instanceof NodeViewAgent && builder.node == null) {
997+
throw new IllegalStateException("A node view agent requires the node it stands in for.");
998+
}
971999
if (builder.session == null) {
9721000
throw new IllegalStateException("Session must be set.");
9731001
}
@@ -994,6 +1022,7 @@ public boolean equals(Object o) {
9941022
&& Objects.equals(branch, that.branch)
9951023
&& Objects.equals(invocationId, that.invocationId)
9961024
&& Objects.equals(agent, that.agent)
1025+
&& Objects.equals(node, that.node)
9971026
&& Objects.equals(session, that.session)
9981027
&& Objects.equals(userContent, that.userContent)
9991028
&& Objects.equals(runConfig, that.runConfig)
@@ -1019,6 +1048,7 @@ public int hashCode() {
10191048
branch,
10201049
invocationId,
10211050
agent,
1051+
node,
10221052
session,
10231053
userContent,
10241054
runConfig,
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.adk.agents;
18+
19+
import static com.google.common.base.Preconditions.checkArgument;
20+
21+
import com.google.adk.annotations.Experimental;
22+
import com.google.adk.events.Event;
23+
import com.google.adk.workflow.Node;
24+
import io.reactivex.rxjava3.core.Flowable;
25+
26+
/**
27+
* A read-only agent that stands in for a non-agent workflow node so code reading the agent of an
28+
* app, runner, or invocation keeps working when they run a node. It never runs; the runner runs the
29+
* node itself.
30+
*/
31+
@Experimental
32+
public final class NodeViewAgent extends BaseAgent {
33+
34+
private final Node node;
35+
36+
/**
37+
* Creates an agent that stands in for {@code node}, using its name and description.
38+
*
39+
* @throws IllegalArgumentException if {@code node} is an agent, or if its name is not a valid
40+
* agent name.
41+
*/
42+
public NodeViewAgent(Node node) {
43+
super(
44+
node.name(),
45+
node.description(),
46+
/* subAgents= */ null,
47+
/* beforeAgentCallback= */ null,
48+
/* afterAgentCallback= */ null);
49+
checkArgument(!(node instanceof BaseAgent), "An agent needs no node view.");
50+
this.node = node;
51+
}
52+
53+
/** Returns the node this agent stands in for. */
54+
public Node node() {
55+
return node;
56+
}
57+
58+
// Callers get this error instead of the context check in BaseAgent.runAsync and runLive.
59+
@Override
60+
public Flowable<Event> runAsync(InvocationContext parentContext) {
61+
return Flowable.error(neverRuns());
62+
}
63+
64+
@Override
65+
public Flowable<Event> runLive(InvocationContext parentContext) {
66+
return Flowable.error(neverRuns());
67+
}
68+
69+
@Override
70+
protected Flowable<Event> runAsyncImpl(InvocationContext invocationContext) {
71+
return Flowable.error(neverRuns());
72+
}
73+
74+
@Override
75+
protected Flowable<Event> runLiveImpl(InvocationContext invocationContext) {
76+
return Flowable.error(neverRuns());
77+
}
78+
79+
private UnsupportedOperationException neverRuns() {
80+
return new UnsupportedOperationException(
81+
"A node view agent never runs; it stands in for node '" + name() + "'.");
82+
}
83+
}

‎core/src/main/java/com/google/adk/apps/App.java‎

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,20 @@
1616

1717
package com.google.adk.apps;
1818

19+
import static com.google.common.base.Preconditions.checkNotNull;
20+
1921
import com.google.adk.agents.BaseAgent;
2022
import com.google.adk.agents.ContextCacheConfig;
23+
import com.google.adk.agents.NodeViewAgent;
2124
import com.google.adk.agents.Role;
2225
import com.google.adk.annotations.Experimental;
2326
import com.google.adk.plugins.Plugin;
2427
import com.google.adk.summarizer.EventsCompactionConfig;
28+
import com.google.adk.workflow.Node;
2529
import com.google.common.collect.ImmutableList;
2630
import com.google.errorprone.annotations.CanIgnoreReturnValue;
2731
import java.util.List;
32+
import java.util.Optional;
2833
import java.util.regex.Pattern;
2934
import org.jspecify.annotations.Nullable;
3035

@@ -41,6 +46,7 @@ public class App {
4146

4247
private final String name;
4348
private final BaseAgent rootAgent;
49+
private final @Nullable Node rootNode;
4450
private final ImmutableList<? extends Plugin> plugins;
4551
private final @Nullable EventsCompactionConfig eventsCompactionConfig;
4652
private final @Nullable ContextCacheConfig contextCacheConfig;
@@ -49,12 +55,14 @@ public class App {
4955
private App(
5056
String name,
5157
BaseAgent rootAgent,
58+
@Nullable Node rootNode,
5259
List<? extends Plugin> plugins,
5360
@Nullable EventsCompactionConfig eventsCompactionConfig,
5461
@Nullable ContextCacheConfig contextCacheConfig,
5562
@Nullable ResumabilityConfig resumabilityConfig) {
5663
this.name = name;
5764
this.rootAgent = rootAgent;
65+
this.rootNode = rootNode;
5866
this.plugins = ImmutableList.copyOf(plugins);
5967
this.eventsCompactionConfig = eventsCompactionConfig;
6068
this.contextCacheConfig = contextCacheConfig;
@@ -69,6 +77,15 @@ public BaseAgent rootAgent() {
6977
return rootAgent;
7078
}
7179

80+
/**
81+
* Returns the workflow node the app runs, or empty when it runs an agent tree. When it runs a
82+
* node, {@link #rootAgent()} returns a {@link NodeViewAgent} that stands in for it.
83+
*/
84+
@Experimental
85+
public Optional<Node> rootNode() {
86+
return Optional.ofNullable(rootNode);
87+
}
88+
7289
public List<? extends Plugin> plugins() {
7390
return plugins;
7491
}
@@ -92,6 +109,7 @@ public ContextCacheConfig contextCacheConfig() {
92109
public static class Builder {
93110
private String name;
94111
private BaseAgent rootAgent;
112+
private @Nullable Node rootNode;
95113
private List<? extends Plugin> plugins = ImmutableList.of();
96114
@Nullable private EventsCompactionConfig eventsCompactionConfig;
97115
@Nullable private ContextCacheConfig contextCacheConfig;
@@ -109,6 +127,17 @@ public Builder rootAgent(BaseAgent rootAgent) {
109127
return this;
110128
}
111129

130+
/**
131+
* Sets the workflow node the app runs in place of a root agent. If the given node is an agent,
132+
* it becomes the root agent.
133+
*/
134+
@CanIgnoreReturnValue
135+
@Experimental
136+
public Builder rootNode(Node rootNode) {
137+
this.rootNode = checkNotNull(rootNode);
138+
return this;
139+
}
140+
112141
@CanIgnoreReturnValue
113142
public Builder plugins(List<? extends Plugin> plugins) {
114143
this.plugins = plugins;
@@ -144,12 +173,36 @@ public App build() {
144173
if (name == null) {
145174
throw new IllegalStateException("App name must be provided.");
146175
}
147-
if (rootAgent == null) {
148-
throw new IllegalStateException("Root agent must be provided.");
149-
}
176+
BaseAgent buildRootAgent = resolveRootAgent();
150177
validateAppName(name);
151178
return new App(
152-
name, rootAgent, plugins, eventsCompactionConfig, contextCacheConfig, resumabilityConfig);
179+
name,
180+
buildRootAgent,
181+
// An agent set as the root node is the root agent, so the app has no root node.
182+
rootNode instanceof BaseAgent ? null : rootNode,
183+
plugins,
184+
eventsCompactionConfig,
185+
contextCacheConfig,
186+
resumabilityConfig);
187+
}
188+
189+
/** Returns the root agent, or the agent that stands in for the root node. */
190+
private BaseAgent resolveRootAgent() {
191+
if (rootAgent instanceof NodeViewAgent || rootNode instanceof NodeViewAgent) {
192+
throw new IllegalStateException(
193+
"A node view agent cannot be the root; set the node it stands in for as the root"
194+
+ " node.");
195+
}
196+
if (rootNode == null) {
197+
if (rootAgent == null) {
198+
throw new IllegalStateException("Root agent or root node must be provided.");
199+
}
200+
return rootAgent;
201+
}
202+
if (rootAgent != null) {
203+
throw new IllegalStateException("Set a root agent or a root node, not both.");
204+
}
205+
return rootNode instanceof BaseAgent agent ? agent : new NodeViewAgent(rootNode);
153206
}
154207
}
155208

0 commit comments

Comments
 (0)