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
3 changes: 2 additions & 1 deletion core/src/main/java/com/google/adk/agents/BaseAgent.java
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,8 @@ public ImmutableList<? extends AfterAgentCallback> canonicalAfterAgentCallbacks(
private InvocationContext createInvocationContext(
InvocationContext parentContext, boolean keepBranch) {
InvocationContext.Builder builder = parentContext.toBuilder();
builder.agent(this);
// An agent's context runs that agent, not the node that its parent context ran.
builder.agent(this).node(null);
if (!keepBranch) {
// Check for branch to be truthy (not None, not empty string),
parentContext
Expand Down
30 changes: 30 additions & 0 deletions core/src/main/java/com/google/adk/agents/InvocationContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import com.google.adk.sessions.BaseSessionService;
import com.google.adk.sessions.Session;
import com.google.adk.summarizer.EventsCompactionConfig;
import com.google.adk.workflow.Node;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
Expand Down Expand Up @@ -84,6 +85,7 @@ public class InvocationContext {

@Nullable private String branch;
private BaseAgent agent;
private final @Nullable Node node;
private boolean endInvocation;

protected InvocationContext(Builder builder) {
Expand All @@ -97,6 +99,7 @@ protected InvocationContext(Builder builder) {
// Resolved here so subclasses that construct from a builder without build() also get an id.
this.invocationId = builder.resolveInvocationId();
this.agent = builder.agent;
this.node = builder.node;
this.session = builder.session;
this.userContent = builder.userContent;
this.runConfig = builder.runConfig;
Expand Down Expand Up @@ -182,6 +185,15 @@ public BaseAgent agent() {
return agent;
}

/**
* Returns the workflow node this invocation runs, or empty when it runs an agent. When it runs a
* node, {@link #agent()} returns a {@link NodeViewAgent} that stands in for it.
*/
@Experimental
public Optional<Node> node() {
return Optional.ofNullable(node);
}

/** Returns the session associated with this invocation. */
public Session session() {
return session;
Expand Down Expand Up @@ -653,6 +665,7 @@ private Builder(InvocationContext context) {
this.invocationId = context.invocationId;
this.invocationIdSet = true;
this.agent = context.agent;
this.node = context.node;
this.session = context.session;
this.userContent = context.userContent;
this.runConfig = context.runConfig;
Expand Down Expand Up @@ -684,6 +697,7 @@ private Builder(InvocationContext context) {
// Distinguishes "never set" (an id is minted from the UuidProvider) from an explicit null.
private boolean invocationIdSet;
private BaseAgent agent;
private @Nullable Node node;
private Session session;
@Nullable private Content userContent = null;
private RunConfig runConfig = RunConfig.builder().build();
Expand Down Expand Up @@ -796,6 +810,17 @@ public Builder agent(BaseAgent agent) {
return this;
}

/**
* Sets the workflow node the invocation runs, or null when it runs an agent. When the
* invocation runs a node, its agent is a {@link NodeViewAgent} that stands in for the node.
*/
@CanIgnoreReturnValue
@Experimental
public Builder node(@Nullable Node node) {
this.node = node;
return this;
}

/**
* Sets the session associated with this invocation.
*
Expand Down Expand Up @@ -968,6 +993,9 @@ private static void validate(Builder builder) {
if (builder.agent == null) {
throw new IllegalStateException("Agent must be set.");
}
if (builder.agent instanceof NodeViewAgent && builder.node == null) {
throw new IllegalStateException("A node view agent requires the node it stands in for.");
}
if (builder.session == null) {
throw new IllegalStateException("Session must be set.");
}
Expand All @@ -994,6 +1022,7 @@ public boolean equals(Object o) {
&& Objects.equals(branch, that.branch)
&& Objects.equals(invocationId, that.invocationId)
&& Objects.equals(agent, that.agent)
&& Objects.equals(node, that.node)
&& Objects.equals(session, that.session)
&& Objects.equals(userContent, that.userContent)
&& Objects.equals(runConfig, that.runConfig)
Expand All @@ -1019,6 +1048,7 @@ public int hashCode() {
branch,
invocationId,
agent,
node,
session,
userContent,
runConfig,
Expand Down
83 changes: 83 additions & 0 deletions core/src/main/java/com/google/adk/agents/NodeViewAgent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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.agents;

import static com.google.common.base.Preconditions.checkArgument;

import com.google.adk.annotations.Experimental;
import com.google.adk.events.Event;
import com.google.adk.workflow.Node;
import io.reactivex.rxjava3.core.Flowable;

/**
* A read-only agent that stands in for a non-agent workflow node so code reading the agent of an
* app, runner, or invocation keeps working when they run a node. It never runs; the runner runs the
* node itself.
*/
@Experimental
public final class NodeViewAgent extends BaseAgent {

private final Node node;

/**
* Creates an agent that stands in for {@code node}, using its name and description.
*
* @throws IllegalArgumentException if {@code node} is an agent, or if its name is not a valid
* agent name.
*/
public NodeViewAgent(Node node) {
super(
node.name(),
node.description(),
/* subAgents= */ null,
/* beforeAgentCallback= */ null,
/* afterAgentCallback= */ null);
checkArgument(!(node instanceof BaseAgent), "An agent needs no node view.");
this.node = node;
}

/** Returns the node this agent stands in for. */
public Node node() {
return node;
}

// Callers get this error instead of the context check in BaseAgent.runAsync and runLive.
@Override
public Flowable<Event> runAsync(InvocationContext parentContext) {
return Flowable.error(neverRuns());
}

@Override
public Flowable<Event> runLive(InvocationContext parentContext) {
return Flowable.error(neverRuns());
}

@Override
protected Flowable<Event> runAsyncImpl(InvocationContext invocationContext) {
return Flowable.error(neverRuns());
}

@Override
protected Flowable<Event> runLiveImpl(InvocationContext invocationContext) {
return Flowable.error(neverRuns());
}

private UnsupportedOperationException neverRuns() {
return new UnsupportedOperationException(
"A node view agent never runs; it stands in for node '" + name() + "'.");
}
}
61 changes: 57 additions & 4 deletions core/src/main/java/com/google/adk/apps/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,20 @@

package com.google.adk.apps;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.adk.agents.BaseAgent;
import com.google.adk.agents.ContextCacheConfig;
import com.google.adk.agents.NodeViewAgent;
import com.google.adk.agents.Role;
import com.google.adk.annotations.Experimental;
import com.google.adk.plugins.Plugin;
import com.google.adk.summarizer.EventsCompactionConfig;
import com.google.adk.workflow.Node;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import org.jspecify.annotations.Nullable;

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

private final String name;
private final BaseAgent rootAgent;
private final @Nullable Node rootNode;
private final ImmutableList<? extends Plugin> plugins;
private final @Nullable EventsCompactionConfig eventsCompactionConfig;
private final @Nullable ContextCacheConfig contextCacheConfig;
Expand All @@ -49,12 +55,14 @@ public class App {
private App(
String name,
BaseAgent rootAgent,
@Nullable Node rootNode,
List<? extends Plugin> plugins,
@Nullable EventsCompactionConfig eventsCompactionConfig,
@Nullable ContextCacheConfig contextCacheConfig,
@Nullable ResumabilityConfig resumabilityConfig) {
this.name = name;
this.rootAgent = rootAgent;
this.rootNode = rootNode;
this.plugins = ImmutableList.copyOf(plugins);
this.eventsCompactionConfig = eventsCompactionConfig;
this.contextCacheConfig = contextCacheConfig;
Expand All @@ -69,6 +77,15 @@ public BaseAgent rootAgent() {
return rootAgent;
}

/**
* Returns the workflow node the app runs, or empty when it runs an agent tree. When it runs a
* node, {@link #rootAgent()} returns a {@link NodeViewAgent} that stands in for it.
*/
@Experimental
public Optional<Node> rootNode() {
return Optional.ofNullable(rootNode);
}

public List<? extends Plugin> plugins() {
return plugins;
}
Expand All @@ -92,6 +109,7 @@ public ContextCacheConfig contextCacheConfig() {
public static class Builder {
private String name;
private BaseAgent rootAgent;
private @Nullable Node rootNode;
private List<? extends Plugin> plugins = ImmutableList.of();
@Nullable private EventsCompactionConfig eventsCompactionConfig;
@Nullable private ContextCacheConfig contextCacheConfig;
Expand All @@ -109,6 +127,17 @@ public Builder rootAgent(BaseAgent rootAgent) {
return this;
}

/**
* Sets the workflow node the app runs in place of a root agent. If the given node is an agent,
* it becomes the root agent.
*/
@CanIgnoreReturnValue
@Experimental
public Builder rootNode(Node rootNode) {
this.rootNode = checkNotNull(rootNode);
return this;
}

@CanIgnoreReturnValue
public Builder plugins(List<? extends Plugin> plugins) {
this.plugins = plugins;
Expand Down Expand Up @@ -144,12 +173,36 @@ public App build() {
if (name == null) {
throw new IllegalStateException("App name must be provided.");
}
if (rootAgent == null) {
throw new IllegalStateException("Root agent must be provided.");
}
BaseAgent buildRootAgent = resolveRootAgent();
validateAppName(name);
return new App(
name, rootAgent, plugins, eventsCompactionConfig, contextCacheConfig, resumabilityConfig);
name,
buildRootAgent,
// An agent set as the root node is the root agent, so the app has no root node.
rootNode instanceof BaseAgent ? null : rootNode,
plugins,
eventsCompactionConfig,
contextCacheConfig,
resumabilityConfig);
}

/** Returns the root agent, or the agent that stands in for the root node. */
private BaseAgent resolveRootAgent() {
if (rootAgent instanceof NodeViewAgent || rootNode instanceof NodeViewAgent) {
throw new IllegalStateException(
"A node view agent cannot be the root; set the node it stands in for as the root"
+ " node.");
}
if (rootNode == null) {
if (rootAgent == null) {
throw new IllegalStateException("Root agent or root node must be provided.");
}
return rootAgent;
}
if (rootAgent != null) {
throw new IllegalStateException("Set a root agent or a root node, not both.");
}
return rootNode instanceof BaseAgent agent ? agent : new NodeViewAgent(rootNode);
}
}

Expand Down
Loading
Loading