diff --git a/core/src/main/java/com/google/adk/workflow/EdgesBuilder.java b/core/src/main/java/com/google/adk/workflow/EdgesBuilder.java new file mode 100644 index 000000000..ea5081142 --- /dev/null +++ b/core/src/main/java/com/google/adk/workflow/EdgesBuilder.java @@ -0,0 +1,368 @@ +/* + * Copyright 2026 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.workflow; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.google.adk.annotations.Experimental; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; + +/** + * A builder for the edges of a workflow graph, declared as chains of calls that start at {@link + * #from} or {@link #fromStart}. Create one with {@code new EdgesBuilder()} and call {@link #build}, + * or let {@link Workflow.Builder#edges(Consumer)} do both. A node is identified by its instance, so + * create each node once. + * + *
{@code
+ * Workflow support =
+ * Workflow.builder()
+ * .name("support_triage")
+ * .edges(
+ * g -> {
+ * g.fromStart()
+ * .chain(classify, category)
+ * .route(
+ * r -> {
+ * r.on("billing").then(billing);
+ * r.otherwise(summarize);
+ * });
+ * g.from(billing).then(summarize);
+ * })
+ * .build();
+ * }
+ */
+@Experimental
+public final class EdgesBuilder {
+
+ private final ListFor example: {@code .edges(g -> g.fromStart().chain(a, b))}.
+ */
+ @CanIgnoreReturnValue
+ public Builder edges(Consumer super EdgesBuilder> graphDefinition) {
+ EdgesBuilder builder = new EdgesBuilder();
+ graphDefinition.accept(builder);
+ return edges(builder.build());
+ }
+
@CanIgnoreReturnValue
public Builder description(String description) {
this.description = checkNotNull(description);
diff --git a/core/src/test/java/com/google/adk/workflow/EdgesBuilderTest.java b/core/src/test/java/com/google/adk/workflow/EdgesBuilderTest.java
new file mode 100644
index 000000000..248dcf4f7
--- /dev/null
+++ b/core/src/test/java/com/google/adk/workflow/EdgesBuilderTest.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright 2026 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.workflow;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class EdgesBuilderTest {
+
+ private final EdgesBuilder builder = new EdgesBuilder();
+ private final Node a = new StubNode("a");
+ private final Node b = new StubNode("b");
+ private final Node c = new StubNode("c");
+ private final Node d = new StubNode("d");
+ private final JoinNode join = JoinNode.builder().name("join").build();
+
+ @Test
+ public void build_withoutEdges_returnsAnEmptyList() {
+ assertThat(builder.build()).isEmpty();
+ }
+
+ @Test
+ public void fromStart_chainOfThens_connectsEachNodeToTheNext() {
+ builder.fromStart().then(a).then(b);
+
+ assertThat(builder.build())
+ .containsExactly(new Edge(Start.INSTANCE, a), new Edge(a, b))
+ .inOrder();
+ }
+
+ @Test
+ public void from_startsTheChainAtTheNode() {
+ builder.from(a).then(b);
+
+ assertThat(builder.build()).containsExactly(new Edge(a, b));
+ }
+
+ @Test
+ public void from_rejectsNull() {
+ assertThrows(NullPointerException.class, () -> builder.from((Node) null));
+ }
+
+ @Test
+ public void chain_severalNodes_connectsEachNodeToTheNext() {
+ builder.fromStart().chain(a, b, c);
+
+ assertThat(builder.build())
+ .containsExactly(new Edge(Start.INSTANCE, a), new Edge(a, b), new Edge(b, c))
+ .inOrder();
+ }
+
+ @Test
+ public void chain_twoNodes_continuesFromTheLastOne() {
+ builder.from(a).chain(b, c).then(d);
+
+ assertThat(builder.build())
+ .containsExactly(new Edge(a, b), new Edge(b, c), new Edge(c, d))
+ .inOrder();
+ }
+
+ @Test
+ public void chain_withANullNode_throwsAndAddsNoEdges() {
+ EdgesBuilder.EdgeChain chain = builder.from(a);
+
+ assertThrows(NullPointerException.class, () -> chain.chain(b, c, (Node) null));
+ assertThat(builder.build()).isEmpty();
+ }
+
+ @Test
+ public void then_collection_fansOutToEach() {
+ builder.fromStart().then(a).then(ImmutableList.of(b, c, d));
+
+ assertThat(builder.build())
+ .containsExactly(
+ new Edge(Start.INSTANCE, a), new Edge(a, b), new Edge(a, c), new Edge(a, d))
+ .inOrder();
+ }
+
+ @Test
+ public void then_emptyCollection_addsNoEdges() {
+ builder.fromStart().then(a).then(ImmutableList.of());
+
+ assertThat(builder.build()).containsExactly(new Edge(Start.INSTANCE, a));
+ }
+
+ @Test
+ public void then_collectionWithANull_throwsAndAddsNoEdges() {
+ EdgesBuilder.EdgeChain chain = builder.from(a);
+
+ assertThrows(NullPointerException.class, () -> chain.then(Arrays.asList(b, null)));
+ assertThat(builder.build()).isEmpty();
+ }
+
+ @Test
+ public void then_collection_continuesAsAGroupThatJoins() {
+ builder.fromStart().then(ImmutableList.of(b, c)).joinTo(join).then(d);
+
+ assertThat(builder.build())
+ .containsExactly(
+ new Edge(Start.INSTANCE, b),
+ new Edge(Start.INSTANCE, c),
+ new Edge(b, join),
+ new Edge(c, join),
+ new Edge(join, d))
+ .inOrder();
+ }
+
+ @Test
+ public void then_severalNodes_fansOutToEachAndContinuesAsAGroup() {
+ builder.from(a).then(b, c, d).joinTo(join);
+
+ assertThat(builder.build())
+ .containsExactly(
+ new Edge(a, b),
+ new Edge(a, c),
+ new Edge(a, d),
+ new Edge(b, join),
+ new Edge(c, join),
+ new Edge(d, join))
+ .inOrder();
+ }
+
+ @Test
+ public void then_severalNodesWithANull_throwsAndAddsNoEdges() {
+ EdgesBuilder.EdgeChain chain = builder.from(a);
+
+ assertThrows(NullPointerException.class, () -> chain.then(b, c, (Node) null));
+ assertThat(builder.build()).isEmpty();
+ }
+
+ @Test
+ public void from_collection_joinsWithoutAFanOut() {
+ builder.from(ImmutableList.of(b, c)).joinTo(join).then(d);
+
+ assertThat(builder.build())
+ .containsExactly(new Edge(b, join), new Edge(c, join), new Edge(join, d))
+ .inOrder();
+ }
+
+ @Test
+ public void from_collectionWithANull_throws() {
+ assertThrows(NullPointerException.class, () -> builder.from(Arrays.asList(b, null)));
+ }
+
+ @Test
+ public void from_severalNodes_joinsWithoutAFanOut() {
+ builder.from(a, b, c).joinTo(join).then(d);
+
+ assertThat(builder.build())
+ .containsExactly(new Edge(a, join), new Edge(b, join), new Edge(c, join), new Edge(join, d))
+ .inOrder();
+ }
+
+ @Test
+ public void from_severalNodesWithANull_throws() {
+ assertThrows(NullPointerException.class, () -> builder.from(a, b, (Node) null));
+ }
+
+ @Test
+ public void joinTo_emptyGroup_addsNoEdgesAndContinuesFromTheJoin() {
+ builder.from(ImmutableList.of()).joinTo(join).then(d);
+
+ assertThat(builder.build()).containsExactly(new Edge(join, d));
+ }
+
+ @Test
+ public void joinTo_rejectsNull() {
+ // An empty group adds no edge that would reject the null join on its own.
+ EdgesBuilder.NodeGroupChain group = builder.from(ImmutableList.of());
+
+ assertThrows(NullPointerException.class, () -> group.joinTo(null));
+ }
+
+ @Test
+ public void route_everyRouteKind_routesFromTheCurrentNodeToEachTarget() {
+ ImmutableList