Skip to content

Commit cecad8f

Browse files
damianmomotgooglecopybara-github
authored andcommitted
feat: run a workflow graph
PiperOrigin-RevId: 1003793324
1 parent 66eab8c commit cecad8f

15 files changed

Lines changed: 1495 additions & 12 deletions

‎core/src/main/java/com/google/adk/runner/Runner.java‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@
5656
import com.google.adk.tools.BaseTool;
5757
import com.google.adk.tools.FunctionTool;
5858
import com.google.adk.utils.CollectionUtils;
59+
import com.google.adk.workflow.Edge;
5960
import com.google.adk.workflow.Node;
6061
import com.google.adk.workflow.NodeRunner;
62+
import com.google.adk.workflow.Workflow;
6163
import com.google.common.base.Preconditions;
6264
import com.google.common.collect.ImmutableList;
6365
import com.google.common.collect.MapMaker;
@@ -81,6 +83,7 @@
8183
import java.util.ArrayList;
8284
import java.util.Arrays;
8385
import java.util.HashSet;
86+
import java.util.LinkedHashSet;
8487
import java.util.List;
8588
import java.util.Map;
8689
import java.util.Objects;
@@ -474,10 +477,42 @@ public PluginManager pluginManager() {
474477
public Completable close() {
475478
List<Completable> completables = new ArrayList<>();
476479
completables.add(agent.close());
480+
if (node != null) {
481+
Set<BaseAgent> nodeAgents = new LinkedHashSet<>();
482+
collectAgents(node, nodeAgents);
483+
// Closing an agent closes its sub-agents, so skip agents whose ancestor is closed here.
484+
nodeAgents.stream()
485+
.filter(nodeAgent -> !hasAncestorIn(nodeAgent, nodeAgents))
486+
.forEach(nodeAgent -> completables.add(nodeAgent.close()));
487+
}
477488
completables.add(this.pluginManager.close());
478489
return Completable.mergeDelayError(completables);
479490
}
480491

492+
/** Adds the agents that {@code node} runs, through its own graph and nested workflows. */
493+
private static void collectAgents(Node node, Set<BaseAgent> agents) {
494+
if (node instanceof BaseAgent agent) {
495+
agents.add(agent);
496+
} else if (node instanceof Workflow workflow) {
497+
for (Edge edge : workflow.edges()) {
498+
collectAgents(edge.from(), agents);
499+
collectAgents(edge.to(), agents);
500+
}
501+
}
502+
}
503+
504+
/** Returns whether {@code agents} contains an ancestor of {@code agent}. */
505+
private static boolean hasAncestorIn(BaseAgent agent, Set<BaseAgent> agents) {
506+
for (BaseAgent ancestor = agent.parentAgent();
507+
ancestor != null;
508+
ancestor = ancestor.parentAgent()) {
509+
if (agents.contains(ancestor)) {
510+
return true;
511+
}
512+
}
513+
return false;
514+
}
515+
481516
/**
482517
* Prepares and creates the user-authored {@link Event}, saving any input blobs as artifacts and
483518
* attaching state deltas if configured.

‎core/src/main/java/com/google/adk/workflow/NodeExecutionState.java‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@
2525
import com.google.common.collect.ImmutableSet;
2626
import com.google.errorprone.annotations.CanIgnoreReturnValue;
2727
import java.util.Collection;
28+
import java.util.HashMap;
2829
import java.util.LinkedHashSet;
2930
import java.util.List;
31+
import java.util.Map;
3032
import java.util.Optional;
3133
import java.util.Set;
3234
import org.jspecify.annotations.Nullable;
@@ -43,6 +45,8 @@ public final class NodeExecutionState {
4345
private final @Nullable Context parent;
4446
private final String runId;
4547
private final String nodePath;
48+
private final ImmutableList<String> outputFor;
49+
private final Map<String, Integer> childRunCounts = new HashMap<>();
4650
private String eventAuthor;
4751
private final Set<String> interruptIds = new LinkedHashSet<>();
4852
private @Nullable Object output;
@@ -66,6 +70,10 @@ private NodeExecutionState(Builder builder) {
6670
node.name(),
6771
runId,
6872
/* separator= */ '/');
73+
this.outputFor =
74+
builder.useAsOutput && parentState != null
75+
? ImmutableList.<String>builder().add(nodePath).addAll(parentState.outputFor).build()
76+
: ImmutableList.of(nodePath);
6977
this.eventAuthor = parentState == null ? "" : parentState.eventAuthor;
7078
}
7179

@@ -156,6 +164,19 @@ NodeRunner.EventSink eventSink() {
156164
return eventSink;
157165
}
158166

167+
/**
168+
* Returns the paths of the nodes that use this run's output as their output, starting with this
169+
* run's path.
170+
*/
171+
ImmutableList<String> outputFor() {
172+
return outputFor;
173+
}
174+
175+
/** Returns the next run ID for a child node named {@code name}, counting from {@code "1"}. */
176+
String nextChildRunId(String name) {
177+
return String.valueOf(childRunCounts.merge(name, 1, Integer::sum));
178+
}
179+
159180
/** Returns whether an event has carried the node's output. */
160181
boolean outputEmitted() {
161182
return outputEmitted;
@@ -194,6 +215,7 @@ static final class Builder {
194215
private NodeRunner.@Nullable EventSink eventSink;
195216
private @Nullable Context parent;
196217
private @Nullable String runId;
218+
private boolean useAsOutput;
197219

198220
private Builder() {}
199221

@@ -228,6 +250,13 @@ Builder runId(String runId) {
228250
return this;
229251
}
230252

253+
/** Sets whether this run's output is also its parent's output. */
254+
@CanIgnoreReturnValue
255+
Builder useAsOutput(boolean useAsOutput) {
256+
this.useAsOutput = useAsOutput;
257+
return this;
258+
}
259+
231260
/**
232261
* Builds the state.
233262
*

‎core/src/main/java/com/google/adk/workflow/NodeRunner.java‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import com.google.adk.annotations.Experimental;
2727
import com.google.adk.events.Event;
2828
import com.google.adk.events.EventActions;
29-
import com.google.common.collect.ImmutableList;
3029
import com.google.errorprone.annotations.CanIgnoreReturnValue;
3130
import com.google.genai.types.FinishReason;
3231
import io.reactivex.rxjava3.core.Completable;
@@ -68,6 +67,7 @@ interface EventSink {
6867
private final EventSink eventSink;
6968
private final @Nullable Context parent;
7069
private final String runId;
70+
private final boolean useAsOutput;
7171
private final boolean useSubBranch;
7272
private final @Nullable String overrideBranch;
7373

@@ -89,6 +89,7 @@ private NodeRunner(Builder builder) {
8989
this.node = builder.node;
9090
this.parent = builder.parent;
9191
this.runId = builder.runId;
92+
this.useAsOutput = builder.useAsOutput;
9293
this.useSubBranch = builder.useSubBranch;
9394
this.overrideBranch = builder.overrideBranch;
9495
}
@@ -185,6 +186,7 @@ Single<Context> run(@Nullable Object nodeInput) {
185186
.eventSink(eventSink)
186187
.parent(parent)
187188
.runId(runId)
189+
.useAsOutput(useAsOutput)
188190
.build());
189191
return new Activation(context).run(nodeInput).toSingleDefault(context);
190192
});
@@ -355,8 +357,7 @@ private Event stamp(Event event) {
355357
.nodeInfo(
356358
nodeInfo.toBuilder()
357359
.path(nodeState.nodePath())
358-
.outputFor(
359-
carriesOutput ? ImmutableList.of(nodeState.nodePath()) : nodeInfo.outputFor())
360+
.outputFor(carriesOutput ? nodeState.outputFor() : nodeInfo.outputFor())
360361
.build())
361362
.build();
362363
}
@@ -419,6 +420,7 @@ static final class Builder {
419420
private @Nullable EventSink eventSink;
420421
private @Nullable Context parent;
421422
private String runId = "1";
423+
private boolean useAsOutput;
422424
private boolean useSubBranch;
423425
private @Nullable String overrideBranch;
424426

@@ -459,6 +461,16 @@ Builder runId(String runId) {
459461
return this;
460462
}
461463

464+
/**
465+
* Sets whether the node's output is also its parent's output, as when a terminal node produces
466+
* the output of its workflow.
467+
*/
468+
@CanIgnoreReturnValue
469+
Builder useAsOutput(boolean useAsOutput) {
470+
this.useAsOutput = useAsOutput;
471+
return this;
472+
}
473+
462474
/**
463475
* Sets whether the node runs on a sub-branch of its base branch, as a node fanned out from its
464476
* parent does. Branches isolate one parallel branch's events from a sibling's model history.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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.workflow;
18+
19+
import com.google.adk.annotations.Experimental;
20+
import com.google.common.collect.ImmutableList;
21+
22+
/** A node's progress within one run of a workflow graph. */
23+
@Experimental
24+
final class NodeState {
25+
26+
/** Where the node stands in the current run. */
27+
NodeStatus status;
28+
29+
/** The IDs of the interrupts the node is waiting on, empty unless it is waiting. */
30+
ImmutableList<String> interrupts = ImmutableList.of();
31+
32+
NodeState(NodeStatus status) {
33+
this.status = status;
34+
}
35+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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.workflow;
18+
19+
import com.google.adk.annotations.Experimental;
20+
21+
/** The status of a node within one run of a workflow graph. */
22+
@Experimental
23+
enum NodeStatus {
24+
/** Running now. */
25+
RUNNING,
26+
27+
/** Finished; a node that waits for output finishes only once it produces an output or routes. */
28+
COMPLETED,
29+
30+
/** Paused, either awaiting a user's answers or awaiting another trigger. */
31+
WAITING
32+
}

0 commit comments

Comments
 (0)