Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion core/src/main/java/com/google/adk/agents/Context.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@
import com.google.adk.events.ToolConfirmation;
import com.google.adk.memory.SearchMemoryResponse;
import com.google.adk.sessions.State;
import com.google.adk.workflow.DynamicNodeFailedException;
import com.google.adk.workflow.Node;
import com.google.adk.workflow.NodeExecutionState;
import com.google.adk.workflow.NodeRunner;
import com.google.adk.workflow.Route;
import com.google.adk.workflow.RunNodeConfig;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.genai.types.Part;
Expand Down Expand Up @@ -285,7 +289,8 @@ public final void eventAuthor(String eventAuthor) {
}

/**
* Returns this workflow node's output, or empty if it has produced none.
* Returns this workflow node's output, which is the output of its {@code useAsOutput} child once
* that child emits one, or empty if it has none.
*
* @throws IllegalStateException if this is not the context of a workflow node.
*/
Expand Down Expand Up @@ -315,4 +320,46 @@ public final Optional<ImmutableList<Route>> routes() {
public final void routes(List<? extends Route> routes) {
requireNodeState().selectRoutes(routes);
}

/**
* Runs {@code node} as a child of this workflow node without an input, as {@link #runNode(Node,
* Object, RunNodeConfig)} does with the default {@link RunNodeConfig}.
*/
@Experimental
public final Maybe<Object> runNode(Node node) {
return runNode(node, /* nodeInput= */ null);
}

/**
* Runs {@code node} as a child of this workflow node, as {@link #runNode(Node, Object,
* RunNodeConfig)} does with the default {@link RunNodeConfig}.
*
* @throws IllegalArgumentException if {@code nodeInput} is a {@link RunNodeConfig}, which belongs
* in the three-argument overload.
*/
@Experimental
public final Maybe<Object> runNode(Node node, @Nullable Object nodeInput) {
Preconditions.checkArgument(
!(nodeInput instanceof RunNodeConfig),
"Pass the RunNodeConfig as the third argument of runNode, not as the node input.");
return runNode(node, nodeInput, RunNodeConfig.builder().build());
}

/**
* Returns a {@link Maybe} that, when subscribed, runs {@code node} as a child of this workflow
* node and emits the child's output, or completes empty if it has none. Include it in this node's
* run and catch only failures you handle, such as {@link DynamicNodeFailedException}: when the
* child waits, it fails with an internal {@link RuntimeException} that must propagate to end this
* node's run, so that it waits too.
*
* <p>It fails with {@link DynamicNodeFailedException} if the child fails; with {@link
* IllegalStateException} if this is not a workflow node's context, if this node's {@link
* Node#rerunOnResume} is false, or if {@code config} delegates an output that this node already
* has or has delegated; and with {@link IllegalArgumentException} if the child's name cannot form
* a node path.
*/
@Experimental
public final Maybe<Object> runNode(Node node, @Nullable Object nodeInput, RunNodeConfig config) {
return NodeRunner.runChild(this, node, nodeInput, config);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@
import com.google.adk.annotations.Experimental;

/**
* An exception that {@link NodeRunner#runChild} signals when the child node fails. If the caller
* does not handle this exception, the caller fails with the child's error without emitting an error
* event of its own.
* An exception that {@link com.google.adk.agents.Context#runNode} signals when the child node
* fails. If the caller does not handle this exception, the caller fails with the child's error
* without emitting an error event of its own.
*/
@Experimental
final class DynamicNodeFailedException extends RuntimeException {
public final class DynamicNodeFailedException extends RuntimeException {

private final String errorNodePath;

Expand Down
19 changes: 6 additions & 13 deletions core/src/main/java/com/google/adk/workflow/NodeRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
* The runner for one workflow node. It stamps every emitted event with its author, node path, and
* branch before forwarding it, and records the node's output, routes, interrupts, and failure on
* the node state of the returned context. {@link #runRoot} runs a node as the root of an
* invocation, and {@code runChild} runs one as a child that another node starts itself.
* invocation, and {@link #runChild} runs one as a child that another node starts itself.
*/
@Experimental
public final class NodeRunner {
Expand Down Expand Up @@ -179,19 +179,12 @@ public static Flowable<Event> runRoot(
}

/**
* Returns a {@link Maybe} that, when subscribed, runs {@code node} as a child of the node whose
* context is {@code parent} and emits the child's output, or completes empty if it has none. The
* caller includes it in its run and catches only failures it handles, such as {@link
* DynamicNodeFailedException}: when the child waits, it fails with an internal {@link
* RuntimeException} that must propagate to end the caller's run, so that the caller waits too.
*
* <p>It fails with {@link DynamicNodeFailedException} if the child fails; with {@link
* IllegalStateException} if {@code parent} is not a workflow node's context, if the caller's
* {@link Node#rerunOnResume} is false, or if {@code config} delegates an output that the caller
* already has or has delegated; and with {@link IllegalArgumentException} if the child's name
* cannot form a node path.
* Runs {@code node} as a child of the node whose context is {@code parent}, as {@link
* Context#runNode(Node, Object, RunNodeConfig)} describes. Nodes call that method; this one is
* public only so {@link Context} can call it from another package.
*/
static Maybe<Object> runChild(
@Experimental
public static Maybe<Object> runChild(
Context parent, Node node, @Nullable Object nodeInput, RunNodeConfig config) {
checkNotNull(parent);
checkNotNull(node);
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/java/com/google/adk/workflow/RunNodeConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
import java.util.Optional;
import org.jspecify.annotations.Nullable;

/** Configuration for running a child node with {@link NodeRunner#runChild}. */
/** Configuration for running a child node with {@link com.google.adk.agents.Context#runNode}. */
@Experimental
@AutoValue
abstract class RunNodeConfig {
public abstract class RunNodeConfig {

/**
* Returns the ID of the child's run, which names the run in node paths and branches. When empty,
Expand Down
22 changes: 22 additions & 0 deletions core/src/test/java/com/google/adk/agents/ContextTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@
import com.google.adk.runner.Runner;
import com.google.adk.sessions.Session;
import com.google.adk.sessions.State;
import com.google.adk.testing.TestNode;
import com.google.adk.tools.FunctionTool;
import com.google.adk.tools.ToolContext;
import com.google.adk.workflow.Node;
import com.google.adk.workflow.RunNodeConfig;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.genai.types.Content;
Expand Down Expand Up @@ -94,6 +97,25 @@ public void nodeMethods_onCallbackContext_throwIllegalStateException() {
assertThrows(IllegalStateException.class, () -> context.routes(ImmutableList.of()));
}

@Test
public void runNode_onCallbackContext_failsWithIllegalStateException() {
CallbackContext context = new CallbackContext(newInvocationContext(), null);

context.runNode(TestNode.of("child")).test().assertError(IllegalStateException.class);
}

@Test
public void runNode_configPassedAsTheInput_throwsIllegalArgumentException() {
CallbackContext context = new CallbackContext(newInvocationContext(), null);
Node child = TestNode.of("child");
RunNodeConfig config = RunNodeConfig.builder().useAsOutput(true).build();

IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> context.runNode(child, config));

assertThat(thrown).hasMessageThat().contains("third argument");
}

@Test
public void searchMemory_onCallbackContext_findsRememberedEvents() {
InvocationContext invocationContext = newInvocationContext();
Expand Down
Loading
Loading