Skip to content
Open
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
44 changes: 32 additions & 12 deletions core/src/main/java/com/google/adk/apps/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@

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;
Expand Down Expand Up @@ -105,10 +103,30 @@ public ContextCacheConfig contextCacheConfig() {
return resumabilityConfig;
}

/**
* Returns a builder holding this app's settings. For an app that runs a node, the builder holds
* {@link #rootNode()} rather than the agent that stands in for it. To switch between a root agent
* and a root node, also clear the current one with {@code null}.
*/
public Builder toBuilder() {
Builder builder = new Builder();
builder.name = name;
if (rootNode != null) {
builder.rootNode = rootNode;
} else {
builder.rootAgent = rootAgent;
}
builder.plugins = plugins;
builder.eventsCompactionConfig = eventsCompactionConfig;
builder.contextCacheConfig = contextCacheConfig;
builder.resumabilityConfig = resumabilityConfig;
return builder;
}

/** Builder for {@link App}. */
public static class Builder {
private String name;
private BaseAgent rootAgent;
private @Nullable BaseAgent rootAgent;
private @Nullable Node rootNode;
private List<? extends Plugin> plugins = ImmutableList.of();
@Nullable private EventsCompactionConfig eventsCompactionConfig;
Expand All @@ -121,20 +139,21 @@ public Builder name(String name) {
return this;
}

/** Sets the root agent, or clears it when {@code null}. */
@CanIgnoreReturnValue
public Builder rootAgent(BaseAgent rootAgent) {
public Builder rootAgent(@Nullable BaseAgent rootAgent) {
this.rootAgent = 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.
* Sets the workflow node the app runs in place of a root agent, or clears it when {@code null}.
* If the given node is an agent, it becomes the root agent.
*/
@CanIgnoreReturnValue
@Experimental
public Builder rootNode(Node rootNode) {
this.rootNode = checkNotNull(rootNode);
public Builder rootNode(@Nullable Node rootNode) {
this.rootNode = rootNode;
return this;
}

Expand All @@ -151,20 +170,20 @@ public Builder plugins(Plugin... plugins) {
}

@CanIgnoreReturnValue
public Builder eventsCompactionConfig(EventsCompactionConfig eventsCompactionConfig) {
public Builder eventsCompactionConfig(@Nullable EventsCompactionConfig eventsCompactionConfig) {
this.eventsCompactionConfig = eventsCompactionConfig;
return this;
}

@CanIgnoreReturnValue
public Builder contextCacheConfig(ContextCacheConfig contextCacheConfig) {
public Builder contextCacheConfig(@Nullable ContextCacheConfig contextCacheConfig) {
this.contextCacheConfig = contextCacheConfig;
return this;
}

@CanIgnoreReturnValue
@Experimental
public Builder resumabilityConfig(ResumabilityConfig resumabilityConfig) {
public Builder resumabilityConfig(@Nullable ResumabilityConfig resumabilityConfig) {
this.resumabilityConfig = resumabilityConfig;
return this;
}
Expand Down Expand Up @@ -200,7 +219,8 @@ private BaseAgent resolveRootAgent() {
return rootAgent;
}
if (rootAgent != null) {
throw new IllegalStateException("Set a root agent or a root node, not both.");
throw new IllegalStateException(
"Set a root agent or a root node, not both; clear the other with null.");
}
return rootNode instanceof BaseAgent agent ? agent : new NodeViewAgent(rootNode);
}
Expand Down
102 changes: 102 additions & 0 deletions core/src/test/java/com/google/adk/apps/AppTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
import static org.junit.Assert.assertThrows;

import com.google.adk.agents.BaseAgent;
import com.google.adk.agents.ContextCacheConfig;
import com.google.adk.agents.NodeViewAgent;
import com.google.adk.plugins.BasePlugin;
import com.google.adk.plugins.Plugin;
import com.google.adk.summarizer.EventsCompactionConfig;
import com.google.adk.testing.TestNode;
import com.google.adk.testing.TestUtils;
import com.google.adk.workflow.Node;
Expand Down Expand Up @@ -95,4 +99,102 @@ public void build_withANodeViewAgentAsTheRootNode_throwsIllegalStateException()

assertThrows(IllegalStateException.class, builder::build);
}

@Test
public void toBuilder_unchanged_buildsAnAppWithTheSameSettings() {
BaseAgent agent = TestUtils.createSubAgent("worker");
Plugin plugin = new BasePlugin("plugin") {};
EventsCompactionConfig compaction = new EventsCompactionConfig(3, 1);
ContextCacheConfig cache = new ContextCacheConfig();
ResumabilityConfig resumability = ResumabilityConfig.builder().resumable(true).build();
App app =
App.builder()
.name("agent_app")
.rootAgent(agent)
.plugins(plugin)
.eventsCompactionConfig(compaction)
.contextCacheConfig(cache)
.resumabilityConfig(resumability)
.build();

App copy = app.toBuilder().build();

assertThat(copy.name()).isEqualTo("agent_app");
assertThat(copy.rootAgent()).isSameInstanceAs(agent);
assertThat(copy.rootNode()).isEmpty();
assertThat(copy.plugins()).containsExactly(plugin);
assertThat(copy.eventsCompactionConfig()).isSameInstanceAs(compaction);
assertThat(copy.contextCacheConfig()).isSameInstanceAs(cache);
assertThat(copy.resumabilityConfig()).isSameInstanceAs(resumability);
}

@Test
public void toBuilder_ofANodeApp_keepsTheRootNode() {
App app = App.builder().name("node_app").rootNode(NODE).build();

App copy = app.toBuilder().build();

assertThat(copy.rootNode()).hasValue(NODE);
assertThat(((NodeViewAgent) copy.rootAgent()).node()).isSameInstanceAs(NODE);
}

@Test
public void toBuilder_changed_buildsAnAppWithTheChanges() {
Plugin plugin = new BasePlugin("plugin") {};
App app = App.builder().name("node_app").rootNode(NODE).build();

App copy = app.toBuilder().name("renamed").plugins(plugin).build();

assertThat(copy.name()).isEqualTo("renamed");
assertThat(copy.plugins()).containsExactly(plugin);
assertThat(copy.rootNode()).hasValue(NODE);
}

@Test
public void toBuilder_ofANodeAppClearingTheNodeAndSettingAnAgent_runsTheAgent() {
BaseAgent agent = TestUtils.createSubAgent("worker");
App app = App.builder().name("node_app").rootNode(NODE).build();

App copy = app.toBuilder().rootNode(null).rootAgent(agent).build();

assertThat(copy.rootAgent()).isSameInstanceAs(agent);
assertThat(copy.rootNode()).isEmpty();
}

@Test
public void toBuilder_ofAnAgentAppClearingTheAgentAndSettingANode_runsTheNode() {
App app = App.builder().name("agent_app").rootAgent(TestUtils.createSubAgent("worker")).build();

App copy = app.toBuilder().rootAgent(null).rootNode(NODE).build();

assertThat(copy.rootNode()).hasValue(NODE);
assertThat(((NodeViewAgent) copy.rootAgent()).node()).isSameInstanceAs(NODE);
}

@Test
public void toBuilder_ofANodeAppSettingAnotherNode_standsInForTheNewNode() {
Node other = TestNode.of("other");
App app = App.builder().name("node_app").rootNode(NODE).build();

App copy = app.toBuilder().rootNode(other).build();

assertThat(copy.rootNode()).hasValue(other);
assertThat(((NodeViewAgent) copy.rootAgent()).node()).isSameInstanceAs(other);
}

@Test
public void toBuilder_ofANodeAppClearingTheNode_throwsIllegalStateException() {
App app = App.builder().name("node_app").rootNode(NODE).build();
App.Builder builder = app.toBuilder().rootNode(null);

assertThrows(IllegalStateException.class, builder::build);
}

@Test
public void toBuilder_ofANodeAppSettingAnAgent_throwsIllegalStateException() {
App app = App.builder().name("node_app").rootNode(NODE).build();
App.Builder builder = app.toBuilder().rootAgent(TestUtils.createSubAgent("worker"));

assertThrows(IllegalStateException.class, builder::build);
}
}
20 changes: 18 additions & 2 deletions dev/src/main/java/com/google/adk/web/AdkWebServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.adk.JsonBaseModel;
import com.google.adk.agents.BaseAgent;
import com.google.adk.apps.App;
import com.google.adk.artifacts.BaseArtifactService;
import com.google.adk.artifacts.InMemoryArtifactService;
import com.google.adk.memory.BaseMemoryService;
Expand All @@ -27,6 +28,7 @@
import com.google.adk.sessions.InMemorySessionService;
import com.google.adk.web.config.BackendUrl;
import com.google.adk.web.config.DevUiAssets;
import com.google.common.collect.ObjectArrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
Expand Down Expand Up @@ -114,9 +116,9 @@
* @return A configured MappingJackson2HttpMessageConverter.
*/
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(

Check warning on line 119 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (21)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 119 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (21)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 119 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (21)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 119 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (21)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 119 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (25)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 119 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (25)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 119 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (25)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 119 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (25)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 119 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (17)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 119 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (17)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 119 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (17)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 119 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (17)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal
ObjectMapper objectMapper) {
return new MappingJackson2HttpMessageConverter(objectMapper);

Check warning on line 121 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (21)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 121 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (25)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal

Check warning on line 121 in dev/src/main/java/com/google/adk/web/AdkWebServer.java

View workflow job for this annotation

GitHub Actions / build-modules (17)

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter in org.springframework.http.converter.json has been deprecated and marked for removal
}

/**
Expand Down Expand Up @@ -172,6 +174,20 @@

// TODO: return a Closeable that stops the server and resets the static state.
public static void start(BaseAgent... agents) {
start("agentLoader", new AgentStaticLoader(agents));
}

/**
* Starts the server with {@code app} and {@code moreApps}, each served under its name. An app can
* run an agent or a workflow.
*
* @throws IllegalArgumentException if two apps share a name
*/
public static void start(App app, App... moreApps) {
start("appLoader", new InMemoryAppLoader(ObjectArrays.concat(app, moreApps)));
}

private static void start(String loaderBeanName, Object loader) {
// Disable CompiledAgentLoader by setting property to prevent its creation
System.setProperty("adk.agents.loader", "static");
// Increase the default websocket buffer size to 10MB to accommodate live API messages.
Expand All @@ -184,10 +200,10 @@
new ApplicationContextInitializer<ConfigurableApplicationContext>() {
@Override
public void initialize(ConfigurableApplicationContext context) {
// Register the AgentStaticLoader bean before context refresh
// Register the loader bean before context refresh
DefaultListableBeanFactory beanFactory =
(DefaultListableBeanFactory) context.getBeanFactory();
beanFactory.registerSingleton("agentLoader", new AgentStaticLoader(agents));
beanFactory.registerSingleton(loaderBeanName, loader);
}
});

Expand Down
44 changes: 44 additions & 0 deletions dev/src/main/java/com/google/adk/web/AppLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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.web;

import com.google.adk.apps.App;
import com.google.common.collect.ImmutableList;
import java.util.Optional;
import javax.annotation.concurrent.ThreadSafe;

/**
* Loads what the ADK Web Server serves: apps, agents and workflows, each as an {@link App} keyed by
* its app name. An agent is served as an app whose root agent it is, and a workflow as an app whose
* root node it is.
*
* <p>When the server has an {@code AppLoader}, it uses it in place of the default {@link
* CompiledAgentLoader}, and fails at startup if another {@link AgentLoader} is configured too.
*/
@ThreadSafe
public interface AppLoader {

/** Returns the names of the available apps, each a valid app name; empty if there are none. */
ImmutableList<String> listApps();

/**
* Returns the app served as {@code appName}, or empty if there is none.
*
* @throws RuntimeException if the app exists but fails to load
*/
Optional<App> loadApp(String appName);
}
55 changes: 55 additions & 0 deletions dev/src/main/java/com/google/adk/web/InMemoryAppLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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.web;

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

import com.google.adk.apps.App;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/** An {@link AppLoader} serving a fixed set of apps, each under its {@link App#name()}. */
public final class InMemoryAppLoader implements AppLoader {

private final ImmutableSortedMap<String, App> apps;

/**
* Serves {@code apps}.
*
* @throws IllegalArgumentException if two apps share a name
*/
public InMemoryAppLoader(App... apps) {
Map<String, App> byName = new HashMap<>();
for (App app : apps) {
checkArgument(byName.putIfAbsent(app.name(), app) == null, "App names must be unique.");
}
this.apps = ImmutableSortedMap.copyOf(byName);
}

@Override
public ImmutableList<String> listApps() {
return apps.keySet().asList();
}

@Override
public Optional<App> loadApp(String appName) {
return Optional.ofNullable(apps.get(appName));
}
}
Loading
Loading